diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 31457e47..9cac527f 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -135,13 +135,22 @@ if a new inbox message arrives before this turn ends"). The ``` claude --print --verbose --output-format stream-json --model \ - --continue --settings /run/hive/claude-settings.json \ + --effort --continue \ --system-prompt-file /run/hive/claude-system-prompt.md \ --mcp-config /run/hive/claude-mcp-config.json --strict-mcp-config \ --tools --allowedTools # wake prompt piped over stdin ``` +Static settings are no longer passed with `--settings`: hive-enforced +settings ship at `/etc/claude-code/managed-settings.json` (claude-code's +canonical managed-settings path — precedence #1, read-only, +un-overridable), wired in `nix/templates/harness-base.nix` from the +`prompts/claude-settings.json` asset. `effortLevel` is deliberately not +in that file — effort is controlled live via the `--effort` flag +(`HIVE_DEFAULT_EFFORT` / the per-agent UI slider), which managed scope +would otherwise lock. + `` is read from `Bus::model()` on each turn. The initial default is set by `hyperhive.model` in the agent's `agent.nix` (NixOS option; propagates via `HIVE_DEFAULT_MODEL` env var; falls @@ -172,9 +181,9 @@ percentage-of-window ctx badge. `--continue` keeps a persistent session per agent (claude stores sessions in `~/.claude/projects/`, which is bind-mounted -persistently). Auto-compact and auto-memory are disabled via -`--settings` because hyperhive owns compaction — see -[Compaction](#compaction) below. +persistently). Auto-compact and auto-memory are disabled via the +managed settings at `/etc/claude-code/managed-settings.json` because +hyperhive owns compaction — see [Compaction](#compaction) below. A one-shot `--continue` suppression is available via `POST /api/new-session` (or `/new-session` slash command in the per-agent terminal) — `Bus::take_skip_continue()` flips an @@ -183,8 +192,9 @@ per-agent terminal) — `Bus::take_skip_continue()` flips an ### Compaction -claude's own in-session auto-compact is off (`--settings`); hyperhive -owns it explicitly in `turn::drive_turn`. There are two triggers: +claude's own in-session auto-compact is off (via the managed settings +at `/etc/claude-code/managed-settings.json`); hyperhive owns it +explicitly in `turn::drive_turn`. There are two triggers: - **Reactive** — claude-code prints `Prompt is too long` (the `PROMPT_TOO_LONG_MARKER`). The session is *already* past the context @@ -255,14 +265,12 @@ next turn picks it up like any other inbox message. ### On-boot files -`hive_ag3nt::turn::write_*` writes three files next to the per-agent +`hive_ag3nt::turn::write_*` writes two files next to the per-agent socket at `/run/hive/` once at startup: - `claude-mcp-config.json` — re-invokes the running binary as `mcp` child (so the same binary serves as harness + as claude's MCP child process). -- `claude-settings.json` — the `--settings` blob (auto-compact and - auto-memory off, effortLevel medium). - `claude-system-prompt.md` — rendered from `hive-ag3nt/prompts/system.md` by `hive_ag3nt::prompt::render`: HTML-comment markers (`...`, @@ -620,8 +628,9 @@ status hint moved to the wake prompt + UI header. - Allowed built-ins: `Edit`, `Glob`, `Grep`, `Read`, `Write`. - Tool-group-gated built-ins: `WebFetch`, `WebSearch` (added when the `web_tools` tool group is enabled — see P3RM1SS10NS tab). -- Denied by omission or `claude-settings.json` deny list: `Bash`, - `Task`, `NotebookEdit`, `TodoWrite`. +- Denied by omission or the managed-settings deny list + (`/etc/claude-code/managed-settings.json`): `Bash`, `Task`, + `NotebookEdit`, `TodoWrite`. - Allowed MCP tools: as listed above (by tool group). `Bash` is disallowed — shell execution goes through diff --git a/hive-ag3nt/prompts/claude-settings.json b/hive-ag3nt/prompts/claude-settings.json index a433e8e7..34f8aee8 100644 --- a/hive-ag3nt/prompts/claude-settings.json +++ b/hive-ag3nt/prompts/claude-settings.json @@ -1,7 +1,6 @@ { "autoCompactEnabled": false, "autoMemoryEnabled": false, - "effortLevel": "medium", "permissions": { "deny": ["Bash", "Task", "TodoWrite"] } diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 5f3a2aa6..8782c4dd 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -9,7 +9,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use anyhow::{Context, Result, bail}; +use anyhow::{Result, bail}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::process::Command; @@ -17,12 +17,13 @@ use crate::events::{Bus, LiveEvent}; use crate::login::LoginState; use crate::mcp; -// `--settings` JSON is read at runtime from -// `$HIVE_ASSETS_DIR/prompts/claude-settings.json` via -// `hive_sh4re::assets::claude_settings()`. We turn off claude's -// in-session auto-compaction and its cross-session auto-memory because -// hyperhive owns those concerns (`/compact` on overflow, notes -// persistence under `/state`). Unknown keys are silently ignored by +// Hive-enforced claude settings ship at `/etc/claude-code/managed-settings.json` +// (wired in `nix/templates/harness-base.nix` from the `prompts/claude-settings.json` +// asset). claude-code auto-discovers that managed path — precedence #1, +// read-only, un-overridable — so the harness no longer passes `--settings`. +// We turn off claude's in-session auto-compaction and its cross-session +// auto-memory because hyperhive owns those concerns (`/compact` on overflow, +// notes persistence under `/state`). Unknown keys are silently ignored by // claude-code; if a key gets renamed we'll spot it because the // corresponding behavior will start firing mid-turn again. @@ -95,8 +96,10 @@ conversation to go on. Do not start new work or reply to anyone — just write y and end the turn."; /// The set of files claude reads on every invocation: the MCP server -/// config (`--mcp-config`), static settings (`--settings`), and the -/// pre-rendered role/tools system prompt (`--system-prompt-file`). +/// config (`--mcp-config`) and the pre-rendered role/tools system +/// prompt (`--system-prompt-file`). Static settings are no longer +/// passed here — they live at `/etc/claude-code/managed-settings.json` +/// and claude auto-discovers them. /// Materialised once at harness startup; shared between the turn loop /// and the operator-driven `/compact` path so both invocations look /// identical to claude (same MCP surface, same allowed tools, same @@ -104,7 +107,6 @@ and end the turn."; #[derive(Clone)] pub struct TurnFiles { pub mcp_config: PathBuf, - pub settings: PathBuf, pub system_prompt: PathBuf, } @@ -118,7 +120,6 @@ impl TurnFiles { pub async fn prepare(socket: &Path, label: &str) -> Result { Ok(Self { mcp_config: write_mcp_config(socket).await?, - settings: write_settings(socket).await?, system_prompt: write_system_prompt(socket, label).await?, }) } @@ -145,33 +146,6 @@ pub async fn write_mcp_config(socket: &Path) -> Result { Ok(path) } -/// Drop the static `--settings` JSON next to the MCP config so we can -/// pass a path (`--settings `) instead of an ever-growing inline -/// blob — the CLI argv has a finite length budget. -/// -/// # Errors -/// -/// Returns an error if the settings file cannot be written. -pub async fn write_settings(_socket: &Path) -> Result { - let parent = crate::paths::config_dir(); - tokio::fs::create_dir_all(&parent).await.ok(); - let path = parent.join("claude-settings.json"); - // Source-of-truth is `$HIVE_ASSETS_DIR/prompts/claude-settings.json`; - // copy through the per-agent runtime dir so claude reads it from the - // same socket-adjacent location every time and so a future override - // (per-agent settings JSON layer) drops in cleanly. - let src = hive_sh4re::assets::claude_settings(); - tokio::fs::copy(&src, &path).await.with_context(|| { - format!( - "copy claude settings from {} to {}", - src.display(), - path.display() - ) - })?; - tracing::info!(path = %path.display(), "wrote claude settings"); - Ok(path) -} - /// Thin re-export of [`crate::prompt::write_system_prompt`] for /// callers that already import this module. The actual rendering + /// marker-block logic lives in `prompt.rs`; this is just the public @@ -579,8 +553,9 @@ fn session_refreshed(prev: DirSnapshot, now: DirSnapshot) -> bool { /// live event bus. Prompt goes over stdin (variadic /// `--allowedTools`/`--tools` would otherwise eat a trailing positional /// prompt). The session is persistent across turns via `--continue` and -/// claude's in-session auto-compact is disabled via `--settings` so it -/// doesn't stall mid-turn — hyperhive owns compaction. +/// claude's in-session auto-compact is disabled via the managed +/// settings at `/etc/claude-code/managed-settings.json` so it doesn't +/// stall mid-turn — hyperhive owns compaction. pub async fn run_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutcome { match run_claude(prompt, files, bus).await { Ok((true, _, _)) => TurnOutcome::PromptTooLong, @@ -675,9 +650,7 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<(bool, .arg("--model") .arg(&model) .arg("--effort") - .arg(&effort) - .arg("--settings") - .arg(&files.settings); + .arg(&effort); if resume { cmd.arg("--continue"); } diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index 2c253193..5444a394 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -51,9 +51,9 @@ struct AppState { bus: Bus, socket: PathBuf, /// Same `TurnFiles` the harness's turn loop uses. Shared so - /// `/api/compact` re-uses the exact MCP config / system prompt / - /// settings claude saw on the last regular turn — keeps the - /// session shape identical across compact + normal turns. + /// `/api/compact` re-uses the exact MCP config / system prompt + /// claude saw on the last regular turn — keeps the session shape + /// identical across compact + normal turns. files: TurnFiles, /// Prevents `/api/compact` from racing with an in-flight normal turn. turn_lock: TurnLock, diff --git a/hive-sh4re/src/assets.rs b/hive-sh4re/src/assets.rs index 7bca5e17..aa353f46 100644 --- a/hive-sh4re/src/assets.rs +++ b/hive-sh4re/src/assets.rs @@ -90,10 +90,3 @@ pub fn config_org_avatar_png() -> PathBuf { pub fn prompt_template() -> PathBuf { dir().join("prompts/system.md") } - -/// `$HIVE_ASSETS_DIR/prompts/claude-settings.json` — the static -/// `--settings` JSON every claude invocation reads. -#[must_use] -pub fn claude_settings() -> PathBuf { - dir().join("prompts/claude-settings.json") -} diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 97348269..c081d777 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -1066,6 +1066,16 @@ in environment.etc."hyperhive/claude-plugins-auto-update.json".text = builtins.toJSON config.hyperhive.claudePluginsAutoUpdate; + # Hive-enforced claude settings. claude-code auto-discovers managed + # settings at this canonical Linux path (precedence #1, read-only, + # un-overridable by user/project/CLI) — so the harness no longer + # passes `--settings` or copies the blob per turn. effortLevel is + # deliberately NOT shipped here: effort is controlled live via the + # `--effort` CLI flag (HIVE_DEFAULT_EFFORT / the per-agent UI slider), + # which managed scope would otherwise override and lock. + environment.etc."claude-code/managed-settings.json".source = + "${pkgs.hyperhive-assets}/share/hyperhive/prompts/claude-settings.json"; + # Merged frontend static tree. Base = `${frontend.dist}/agent/`, # then each `extraFiles` entry is laid on top at its `target` # path. The runCommand derivation aborts on overwrite so a