feat(#1969): ship claude settings via /etc/claude-code/managed-settings.json
This commit is contained in:
parent
70d1cdc859
commit
b231ed2392
6 changed files with 49 additions and 65 deletions
|
|
@ -135,13 +135,22 @@ if a new inbox message arrives before this turn ends"). The
|
|||
|
||||
```
|
||||
claude --print --verbose --output-format stream-json --model <name> \
|
||||
--continue --settings /run/hive/claude-settings.json \
|
||||
--effort <level> --continue \
|
||||
--system-prompt-file /run/hive/claude-system-prompt.md \
|
||||
--mcp-config /run/hive/claude-mcp-config.json --strict-mcp-config \
|
||||
--tools <builtins> --allowedTools <builtins+mcp>
|
||||
# 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.
|
||||
|
||||
`<name>` 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 (`<!-- role:agent -->...<!-- /role:agent -->`,
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"autoCompactEnabled": false,
|
||||
"autoMemoryEnabled": false,
|
||||
"effortLevel": "medium",
|
||||
"permissions": {
|
||||
"deny": ["Bash", "Task", "TodoWrite"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
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<PathBuf> {
|
|||
Ok(path)
|
||||
}
|
||||
|
||||
/// Drop the static `--settings` JSON next to the MCP config so we can
|
||||
/// pass a path (`--settings <file>`) 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<PathBuf> {
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue