diff --git a/README.md b/README.md index 3d09c3a..bc38bed 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,13 @@ Buzz-specific behavior in [agent-instructions.md](agent-instructions.md). If the agent was created through a path that records its owner, omit the two allowlist variables and retain the launcher's `owner-only` default. +The launcher deliberately exposes the binary through an executable named +`contextstream`. Buzz currently derives the MCP server name from the command +basename, and the canonical name produces the stable +`mcp__contextstream__*` tool prefix expected by ContextStream guards and +permission rules. Pointing Buzz straight at `contextstream-mcp` is not +equivalent for guarded Claude Code sessions. + ### 5. Start a second agent on the same project Use a distinct Buzz identity, but the same ContextStream project: @@ -149,6 +156,29 @@ Use both systems' permission boundaries: The teaching layer is not an authorization layer. Server-enforced read-only access comes from the ContextStream workspace role attached to the credential. +### Claude Code in a headless Buzz agent + +Buzz defaults to `BUZZ_ACP_PERMISSION_MODE=dont-ask`: operations that would +need an interactive prompt are denied because Buzz has no permission dialog. +That is a sound read-only default, but it does not make a coding agent +read/write by itself. + +For a contained read/write project, merge +[claude-settings.local.example.json](claude-settings.local.example.json) into +`/.claude/settings.local.json`, replace the placeholder path, review +the project and Buzz CLI placeholder paths, review the command allowlist, and +then set: + +```bash +export BUZZ_ACP_PERMISSION_MODE=accept-edits +``` + +The example permits reads and edits only under the selected project, the +standalone test/status commands used by the reference, direct Buzz replies, +and canonical ContextStream tools. It explicitly denies common destructive +shell and Git operations. Treat it as a starting policy, not a substitute for +an OS/container sandbox. + ## Headless and hosted agents `contextstream-mcp setup` is preferred on a developer machine. For a headless @@ -176,6 +206,8 @@ binary execution. The compatibility record is in - [run-agent.sh](run-agent.sh) — safe launcher for Goose, Codex, and Claude - [agent-instructions.md](agent-instructions.md) — brief, preserve, handoff, and attribution behavior +- [claude-settings.local.example.json](claude-settings.local.example.json) — + reviewed least-privilege starting policy for headless Claude read/write work - [smoke-contextstream.mjs](smoke-contextstream.mjs) — real stdio MCP and hosted-grounding smoke test - [demo-script.md](demo-script.md) — reproducible 60–90 second flagship demo diff --git a/agent-instructions.md b/agent-instructions.md index 7433132..b0069b0 100644 --- a/agent-instructions.md +++ b/agent-instructions.md @@ -22,6 +22,9 @@ the resolved workspace, project, and session identifiers. Before every turn, call `context` with the user's actual message. If `instruct` is available, read and acknowledge its surfaced instructions around the context call. +Use the MCP tools whose prefix is exactly `mcp__contextstream__`. Do not switch +to a duplicate user-configured ContextStream server under another prefix. + Identify production sessions consistently: use `client_name="buzz-claude"`, `client_name="buzz-codex"`, or `client_name="buzz-goose"` for the matching harness. Custom harnesses use `buzz-custom-`. Do not use the @@ -112,6 +115,10 @@ not materially contribute. Never append a promotional slogan to every reply. require the user's authorization even if ContextStream recommends them. - If the ContextStream credential is read-only, explain that preservation requires a Member, Admin, or Owner credential; do not work around the role. +- When Buzz requires a CLI response, invoke `buzz messages send` directly + rather than hiding it behind an unrelated shell pipeline. +- Run verification commands such as `npm test` as standalone commands so a + least-privilege permission rule can match and audit them precisely. ## Success condition diff --git a/buzz-acp.env.example b/buzz-acp.env.example index cc9ceb0..bbf1ae2 100644 --- a/buzz-acp.env.example +++ b/buzz-acp.env.example @@ -6,8 +6,16 @@ BUZZ_RELAY_URL=ws://localhost:3000 BUZZ_ACP_RESPOND_TO=owner-only # BUZZ_ACP_RESPOND_TO_ALLOWLIST=64_hex_pubkey,another_64_hex_pubkey -# ContextStream is supplied to every ACP session by the launcher. -BUZZ_ACP_MCP_COMMAND=contextstream-mcp +# ContextStream is supplied to every ACP session by the launcher. The launcher +# creates an executable named `contextstream` so ACP exposes the canonical +# mcp__contextstream__* tool prefix. Do not override this with the raw +# contextstream-mcp basename. +# BUZZ_ACP_MCP_COMMAND=/user/cache/contextstream/buzz/bin/contextstream + +# Safe headless default. Claude read/write work also needs explicit project +# allow rules; see claude-settings.local.example.json before choosing +# accept-edits. +BUZZ_ACP_PERMISSION_MODE=dont-ask # On a developer machine, prefer `contextstream-mcp setup` and its protected # credential store. On a headless host, inject this through the platform's diff --git a/claude-settings.local.example.json b/claude-settings.local.example.json new file mode 100644 index 0000000..a671510 --- /dev/null +++ b/claude-settings.local.example.json @@ -0,0 +1,22 @@ +{ + "permissions": { + "allow": [ + "Read(/absolute/path/to/project/**)", + "Glob(/absolute/path/to/project/**)", + "Write(/absolute/path/to/project/**)", + "Edit(/absolute/path/to/project/**)", + "Bash(npm test)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(/absolute/path/to/buzz messages send:*)", + "mcp__contextstream__*" + ], + "deny": [ + "Bash(rm:*)", + "Bash(sudo:*)", + "Bash(git push:*)", + "Bash(git clean:*)", + "Bash(git reset:*)" + ] + } +} diff --git a/compatibility.json b/compatibility.json index 3b309b3..76270cd 100644 --- a/compatibility.json +++ b/compatibility.json @@ -7,6 +7,9 @@ "harness": "buzz-acp", "mcp_transport": "stdio", "configuration_env": "BUZZ_ACP_MCP_COMMAND", + "mcp_name_source": "command file stem", + "canonical_contextstream_server_name": "contextstream", + "current_name_compatibility": "run-agent.sh creates an executable alias named contextstream; a dedicated BUZZ_ACP_MCP_NAME override is proposed upstream", "acp_method": "session/new", "acp_field": "mcpServers", "supported_harnesses": [ @@ -15,7 +18,8 @@ "claude-agent-acp" ], "desktop_per_agent_mcp_override": false, - "desktop_limitation": "Create/update requests accept mcpCommand only for wire compatibility; runtime catalog metadata determines the effective MCP command at spawn time." + "desktop_limitation": "Create/update requests accept mcpCommand only for wire compatibility; runtime catalog metadata determines the effective MCP command at spawn time.", + "claude_headless_default": "dontAsk denies operations that require an interactive permission prompt; read/write work needs an explicitly reviewed project allowlist plus acceptEdits" }, "contextstream": { "command": "contextstream-mcp", diff --git a/reference.test.mjs b/reference.test.mjs index 31b643a..7cdf79e 100644 --- a/reference.test.mjs +++ b/reference.test.mjs @@ -12,6 +12,8 @@ test("launcher supplies ContextStream through the verified Buzz seam", async () const launcher = await read("run-agent.sh"); assert.match(launcher, /BUZZ_ACP_MCP_COMMAND/); assert.match(launcher, /contextstream-mcp/); + assert.match(launcher, /CONTEXTSTREAM_MCP_ALIAS/); + assert.match(launcher, /BUZZ_ACP_MCP_NAME/); assert.match(launcher, /--system-prompt-file/); assert.match(launcher, /BUZZ_ACP_RESPOND_TO:-owner-only/); }); @@ -19,6 +21,7 @@ test("launcher supplies ContextStream through the verified Buzz seam", async () test("teaching includes brief, approval, handoff, and conditional attribution", async () => { const teaching = await read("agent-instructions.md"); assert.match(teaching, /Brief before substantial work/); + assert.match(teaching, /mcp__contextstream__/); assert.match(teaching, /Wait for an authorized human to approve/); assert.match(teaching, /entity\(kind="handoff", action="create"/); assert.match( @@ -27,9 +30,24 @@ test("teaching includes brief, approval, handoff, and conditional attribution", ); }); +test("Claude headless policy is explicit and least-privilege by default", async () => { + const settings = JSON.parse(await read("claude-settings.local.example.json")); + const envExample = await read("buzz-acp.env.example"); + const allowed = settings.permissions.allow.join("\n"); + const denied = settings.permissions.deny.join("\n"); + + assert.match(envExample, /BUZZ_ACP_PERMISSION_MODE=dont-ask/); + assert.match(allowed, /mcp__contextstream__/); + assert.match(allowed, /\/absolute\/path\/to\/buzz messages send/); + assert.doesNotMatch(allowed, /\*buzz messages send/); + assert.match(denied, /git reset/); + assert.match(denied, /rm:/); +}); + test("compatibility record pins the contract that was actually tested", async () => { const compatibility = JSON.parse(await read("compatibility.json")); assert.equal(compatibility.buzz.configuration_env, "BUZZ_ACP_MCP_COMMAND"); + assert.equal(compatibility.buzz.canonical_contextstream_server_name, "contextstream"); assert.equal(compatibility.buzz.acp_field, "mcpServers"); assert.equal(compatibility.buzz.desktop_per_agent_mcp_override, false); assert.match(compatibility.buzz.commit, /^[0-9a-f]{40}$/); @@ -60,6 +78,7 @@ test("examples contain no live-looking ContextStream or Buzz secrets", async () await read("README.md"), await read("buzz-acp.env.example"), await read("demo-script.md"), + await read("claude-settings.local.example.json"), ].join("\n"); assert.doesNotMatch(files, /cs_(live|test)_[A-Za-z0-9]{16,}/); assert.doesNotMatch(files, /nsec1[023456789acdefghjklmnpqrstuvwxyz]{24,}/); diff --git a/run-agent.sh b/run-agent.sh index a0efb43..c14c465 100755 --- a/run-agent.sh +++ b/run-agent.sh @@ -99,6 +99,33 @@ for required_command in contextstream-mcp buzz-acp "$AGENT_COMMAND"; do } done +# Buzz derives the ACP MCP-server name from the command's file stem. The +# canonical `contextstream` name matters because ContextStream-aware harness +# guards and permission rules key on `mcp__contextstream__*`. Keep the installed +# binary untouched and expose it through a private, executable cache alias. +CONTEXTSTREAM_MCP_REAL="$(command -v contextstream-mcp)" +CONTEXTSTREAM_BUZZ_CACHE_ROOT="${CONTEXTSTREAM_BUZZ_BIN_DIR:-${XDG_CACHE_HOME:-${HOME}/.cache}/contextstream/buzz/bin}" +CONTEXTSTREAM_MCP_ALIAS="$CONTEXTSTREAM_BUZZ_CACHE_ROOT/contextstream" +mkdir -p "$CONTEXTSTREAM_BUZZ_CACHE_ROOT" + +if [[ -e "$CONTEXTSTREAM_MCP_ALIAS" || -L "$CONTEXTSTREAM_MCP_ALIAS" ]]; then + if [[ ! -L "$CONTEXTSTREAM_MCP_ALIAS" ]]; then + printf 'Refusing to replace non-symlink MCP alias: %s\n' "$CONTEXTSTREAM_MCP_ALIAS" >&2 + exit 1 + fi + if [[ "$(readlink "$CONTEXTSTREAM_MCP_ALIAS")" != "$CONTEXTSTREAM_MCP_REAL" ]]; then + ln -sfn "$CONTEXTSTREAM_MCP_REAL" "$CONTEXTSTREAM_MCP_ALIAS" + fi +else + ln -s "$CONTEXTSTREAM_MCP_REAL" "$CONTEXTSTREAM_MCP_ALIAS" +fi + +if ! "$CONTEXTSTREAM_MCP_ALIAS" --version >/dev/null 2>&1; then + printf 'The canonical MCP alias is not executable: %s\n' "$CONTEXTSTREAM_MCP_ALIAS" >&2 + printf 'Set CONTEXTSTREAM_BUZZ_BIN_DIR to a user-owned executable directory.\n' >&2 + exit 1 +fi + AUTH_JSON="$(contextstream-mcp verify-key --json)" if [[ "$AUTH_JSON" != *'"valid": true'* && "$AUTH_JSON" != *'"valid":true'* ]]; then printf 'ContextStream authentication is not valid. Run: contextstream-mcp setup\n' >&2 @@ -109,7 +136,7 @@ if [[ "$CHECK_ONLY" = "true" ]]; then printf 'ContextStream for Buzz check passed.\n' printf ' runtime: %s (%s)\n' "$RUNTIME" "$AGENT_COMMAND" printf ' project: %s\n' "$PROJECT_DIR" - printf ' MCP: %s\n' "$(command -v contextstream-mcp)" + printf ' MCP: %s -> %s\n' "$CONTEXTSTREAM_MCP_ALIAS" "$CONTEXTSTREAM_MCP_REAL" printf ' policy: %s\n' "${BUZZ_ACP_RESPOND_TO:-owner-only}" exit 0 fi @@ -123,7 +150,10 @@ export BUZZ_RELAY_URL="${BUZZ_RELAY_URL:-ws://localhost:3000}" export BUZZ_ACP_RESPOND_TO="${BUZZ_ACP_RESPOND_TO:-owner-only}" export BUZZ_ACP_AGENT_COMMAND="$AGENT_COMMAND" export BUZZ_ACP_AGENT_ARGS="$AGENT_ARGS" -export BUZZ_ACP_MCP_COMMAND="$(command -v contextstream-mcp)" +export BUZZ_ACP_MCP_COMMAND="$CONTEXTSTREAM_MCP_ALIAS" +# Harmless on the pinned Buzz commit and ready for the proposed upstream name +# override. The executable alias above is what guarantees compatibility today. +export BUZZ_ACP_MCP_NAME="${BUZZ_ACP_MCP_NAME:-contextstream}" cd "$PROJECT_DIR" exec buzz-acp --system-prompt-file "$INSTRUCTIONS_FILE" "${EXTRA_ARGS[@]}"