diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 797782eb..31457e47 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -135,21 +135,13 @@ if a new inbox message arrives before this turn ends"). The ``` claude --print --verbose --output-format stream-json --model \ - --effort --continue \ + --continue --settings /run/hive/claude-settings.json \ --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 ``` -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 @@ -180,9 +172,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 the -managed settings at `/etc/claude-code/managed-settings.json` because -hyperhive owns compaction — see [Compaction](#compaction) below. +persistently). Auto-compact and auto-memory are disabled via +`--settings` 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 @@ -191,9 +183,8 @@ per-agent terminal) — `Bus::take_skip_continue()` flips an ### Compaction -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: +claude's own in-session auto-compact is off (`--settings`); 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 @@ -264,12 +255,14 @@ next turn picks it up like any other inbox message. ### On-boot files -`hive_ag3nt::turn::write_*` writes two files next to the per-agent +`hive_ag3nt::turn::write_*` writes three 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 (`...`, @@ -627,9 +620,8 @@ 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 the managed-settings deny list - (`/etc/claude-code/managed-settings.json`): `Bash`, `Task`, - `NotebookEdit`, `TodoWrite`. +- Denied by omission or `claude-settings.json` deny list: `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 34f8aee8..a433e8e7 100644 --- a/hive-ag3nt/prompts/claude-settings.json +++ b/hive-ag3nt/prompts/claude-settings.json @@ -1,6 +1,7 @@ { "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 8782c4dd..5f3a2aa6 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::{Result, bail}; +use anyhow::{Context, Result, bail}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::process::Command; @@ -17,13 +17,12 @@ use crate::events::{Bus, LiveEvent}; use crate::login::LoginState; use crate::mcp; -// 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 +// `--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 // claude-code; if a key gets renamed we'll spot it because the // corresponding behavior will start firing mid-turn again. @@ -96,10 +95,8 @@ 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`) 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. +/// config (`--mcp-config`), static settings (`--settings`), and the +/// pre-rendered role/tools system prompt (`--system-prompt-file`). /// 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 @@ -107,6 +104,7 @@ and end the turn."; #[derive(Clone)] pub struct TurnFiles { pub mcp_config: PathBuf, + pub settings: PathBuf, pub system_prompt: PathBuf, } @@ -120,6 +118,7 @@ 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?, }) } @@ -146,6 +145,33 @@ 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 @@ -553,9 +579,8 @@ 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 the managed -/// settings at `/etc/claude-code/managed-settings.json` so it doesn't -/// stall mid-turn — hyperhive owns compaction. +/// claude's in-session auto-compact is disabled via `--settings` 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, @@ -650,7 +675,9 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<(bool, .arg("--model") .arg(&model) .arg("--effort") - .arg(&effort); + .arg(&effort) + .arg("--settings") + .arg(&files.settings); if resume { cmd.arg("--continue"); } diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index 5444a394..2c253193 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 - /// 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 / + /// settings 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 aa353f46..7bca5e17 100644 --- a/hive-sh4re/src/assets.rs +++ b/hive-sh4re/src/assets.rs @@ -90,3 +90,10 @@ 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 c081d777..97348269 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -1066,16 +1066,6 @@ 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