Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
`<project>/.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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions agent-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<stable-harness-id>`. Do not use the
Expand Down Expand Up @@ -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

Expand Down
12 changes: 10 additions & 2 deletions buzz-acp.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions claude-settings.local.example.json
Original file line number Diff line number Diff line change
@@ -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:*)"
]
}
}
6 changes: 5 additions & 1 deletion compatibility.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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",
Expand Down
19 changes: 19 additions & 0 deletions reference.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ 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/);
});

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(
Expand All @@ -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}$/);
Expand Down Expand Up @@ -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,}/);
Expand Down
34 changes: 32 additions & 2 deletions run-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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[@]}"
Loading