Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
94feabe
feat(acp): add external Codex Buzz worker
StephGlansberg Jul 26, 2026
a04cd14
feat(acp): add external Claude CLI Buzz worker
StephGlansberg Jul 26, 2026
10d9995
fix(acp): reuse standard Claude login
StephGlansberg Jul 26, 2026
59b06e6
fix(acp): bind Claude worker to existing seat
StephGlansberg Jul 26, 2026
92622a0
fix(acp): lock Claude worker activation
StephGlansberg Jul 26, 2026
c474615
fix(acp): move Claude runtime to Data volume
StephGlansberg Jul 26, 2026
1356d77
fix(acp): reuse trusted Buzz harness for Claude
StephGlansberg Jul 26, 2026
df750a2
fix(acp): stage Claude signer on Data volume
StephGlansberg Jul 26, 2026
ab618dc
feat(buzz-acp): separate supervisor and session cwd
StephGlansberg Jul 26, 2026
8bec698
fix(external-cli): pin Claude worker to trusted Node
StephGlansberg Jul 26, 2026
b097d72
feat(buzz): add Cursor CLI external worker
StephGlansberg Jul 26, 2026
8aed28d
fix(external-cli): enforce Codex launch projection
StephGlansberg Jul 26, 2026
f9bb569
feat(aeon): add Grok Buzz ACP coding seat
StephGlansberg Jul 27, 2026
aeeb7e9
refactor(buzz): unify external ACP arguments
StephGlansberg Jul 27, 2026
57ab06b
test(buzz): contain Grok ACP model arguments
StephGlansberg Jul 27, 2026
cd70fa9
feat(acp): admit Aspect-directed frontier workers
StephGlansberg Jul 27, 2026
fa7b713
feat(acp): run frontier CLI seats in Buzz
StephGlansberg Jul 27, 2026
c958283
Merge remote-tracking branch 'origin/main' into codex/buzz-frontier-c…
StephGlansberg Jul 27, 2026
a71ebb8
chore(acp): pin merged frontier harness
StephGlansberg Jul 27, 2026
f695d14
fix(acp): survive cold frontier startup
StephGlansberg Jul 27, 2026
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
2 changes: 1 addition & 1 deletion crates/buzz-acp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ evalexpr = { workspace = true }
# Process-group kill (safe wrapper around killpg) — Unix-only; kill_process_group
# has a #[cfg(not(unix))] fallback in acp.rs.
[target.'cfg(unix)'.dependencies]
nix = { version = "0.31", default-features = false, features = ["signal"] }
nix = { version = "0.31", default-features = false, features = ["fs", "signal", "user"] }

[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
Expand Down
11 changes: 6 additions & 5 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,19 +135,20 @@ Controls which authors' events the harness forwards to the agent. Events from di

| Flag | Env Var | Default | Description |
|------|---------|---------|-------------|
| `--respond-to` | `BUZZ_ACP_RESPOND_TO` | `owner-only` | Author gate mode: `owner-only`, `allowlist`, `anyone`, `nobody`. |
| `--respond-to-allowlist` | `BUZZ_ACP_RESPOND_TO_ALLOWLIST` | — | Comma-separated 64-char hex pubkeys (required when mode is `allowlist`). Owner is always implicitly included. |
| `--respond-to` | `BUZZ_ACP_RESPOND_TO` | `owner-only` | Author gate mode: `owner-only`, `allowlist`, `strict-allowlist`, `anyone`, `nobody`. |
| `--respond-to-allowlist` | `BUZZ_ACP_RESPOND_TO_ALLOWLIST` | — | Comma-separated 64-char hex pubkeys (required by both allowlist modes). Owner is always implicitly included. |

**Modes:**

| Mode | Behavior |
|------|----------|
| `owner-only` | Forward only events from the agent's registered owner. If no owner is set, all events are dropped until the owner is resolved. |
| `allowlist` | Forward events from the listed pubkeys plus the owner. |
| `owner-only` | Forward events from the agent's registered owner and verified same-owner siblings. If no owner is set, all events are dropped until the owner is resolved. |
| `allowlist` | Forward events from the listed pubkeys, the owner, and verified same-owner siblings. |
| `strict-allowlist` | In channels, forward events only from the listed pubkeys and the owner. In DMs, only the owner is accepted. Same-owner siblings receive no implicit authority. |
| `anyone` | Forward all events (no author filtering). |
| `nobody` | Drop all inbound events. Agent only acts on heartbeat prompts. |

The gate applies to **all** inbound events — @mentions, DMs, thread replies, and any event delivered by the relay. Owner control commands are checked **before** the gate, so the owner can still manage the harness regardless of mode:
The gate applies to **all** inbound events — @mentions, DMs, thread replies, and any event delivered by the relay. DMs intentionally ignore explicit allowlist entries: the owner and verified same-owner siblings are accepted by the legacy responding modes, while `strict-allowlist` accepts only the owner. Owner control commands are checked **before** the gate, so the owner can still manage the harness regardless of mode:

| Command | Effect |
|---------|--------|
Expand Down
64 changes: 59 additions & 5 deletions crates/buzz-acp/src/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ pub struct AcpClient {
goose_usage: UsageTracker,
}

fn harness_bound_agent_env(key: &str) -> bool {
matches!(key, "BUZZ_RELAY_URL" | "BUZZ_PRIVATE_KEY")
}

/// Recursively merge `overlay` into `base`, with `overlay` winning on scalar/shape
/// collisions. When both sides have an object for the same key, the merge recurses so
/// unrelated nested keys from `base` are preserved.
Expand Down Expand Up @@ -422,6 +426,17 @@ impl AcpClient {
// Ensure the child is killed when the AcpClient is dropped (best-effort).
// Callers MUST still call shutdown().await for guaranteed cleanup.
.kill_on_drop(true);
// Buzz signing authority is opt-in through `extra_env`; never inherit
// credentials from the harness process into an arbitrary ACP child.
for key in [
"BUZZ_RELAY_URL",
"BUZZ_PRIVATE_KEY",
"BUZZ_ACP_PRIVATE_KEY",
"BUZZ_PRIVATE_KEY_FILE",
"BUZZ_EXPECTED_PUBLIC_KEY",
Comment on lines +431 to +436

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Strip the mirrored Nostr signer when publisher access is disabled

When Desktop launches the harness with git-credential-nostr available, managed_agents/runtime.rs sets NOSTR_PRIVATE_KEY to the same secret as BUZZ_PRIVATE_KEY; because this removal list does not clear it, --no-agent-publisher-credentials still exposes the signer to the arbitrary ACP child, which can invoke the credential helper to create signed NIP-98 authentication and access relay-hosted repositories. Remove NOSTR_PRIVATE_KEY at every spawn and add it back only through the publisher-credential opt-in path.

Useful? React with 👍 / 👎.

] {
cmd.env_remove(key);
}

// Per-persona env vars (e.g., GOOSE_PROVIDER, BUZZ_AGENT_PROVIDER).
// For most keys, operator precedence wins: skip injection if already set
Expand Down Expand Up @@ -452,7 +467,7 @@ impl AcpClient {
// Handled by build_codex_config_env; skip here to avoid double-setting.
continue;
}
if std::env::var(key).is_err() {
if harness_bound_agent_env(key) || std::env::var(key).is_err() {
cmd.env(key, value);
}
}
Expand Down Expand Up @@ -537,10 +552,23 @@ impl AcpClient {
/// Must be called exactly once, before any other ACP method.
/// The caller may inspect `agentCapabilities` in the returned value.
pub async fn initialize(&mut self) -> Result<serde_json::Value, AcpError> {
self.initialize_with_timeout(Self::REQUEST_TIMEOUT).await
}

/// Initialize with a caller-owned deadline.
///
/// Supervisors use a longer bound for cold agent processes; probes retain
/// the normal request deadline through [`Self::initialize`].
pub async fn initialize_with_timeout(
&mut self,
timeout: std::time::Duration,
) -> Result<serde_json::Value, AcpError> {
// Requesting version 2 is an intentional temporary pin — we are squatting
// on ACP v2 ahead of the upstream ACP RFD. Revisit when that RFD merges.
let params = build_initialize_params();
let result = self.send_request("initialize", params).await?;
let result = self
.send_request_with_timeout("initialize", params, timeout)
.await?;
tracing::debug!(target: "acp::init", "initialize response: {result}");
Ok(result)
}
Expand Down Expand Up @@ -988,6 +1016,16 @@ impl AcpClient {
&mut self,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, AcpError> {
self.send_request_with_timeout(method, params, Self::REQUEST_TIMEOUT)
.await
}

async fn send_request_with_timeout(
&mut self,
method: &str,
params: serde_json::Value,
timeout: std::time::Duration,
) -> Result<serde_json::Value, AcpError> {
let id = self.next_id;
self.next_id += 1;
Expand All @@ -1004,13 +1042,17 @@ impl AcpClient {
// Wrap write + read in a single timeout so a hung agent can't block forever.
// We cannot use an async block that borrows `self` mutably across two awaits
// inside timeout(), so we sequence them with early-return on timeout.
let timeout = Self::REQUEST_TIMEOUT;
let deadline = tokio::time::Instant::now() + timeout;
match tokio::time::timeout(timeout, self.write_ndjson(&msg)).await {
Ok(result) => result?,
Err(_) => return Err(AcpError::Timeout(timeout)),
}

match tokio::time::timeout(timeout, self.read_until_response(id)).await {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(AcpError::Timeout(timeout));
}
match tokio::time::timeout(remaining, self.read_until_response(id)).await {
Ok(result) => result,
Err(_) => Err(AcpError::Timeout(timeout)),
}
Expand Down Expand Up @@ -1888,7 +1930,11 @@ pub fn resolve_model_switch_method(
// 1. Search stable configOptions for a "model"-category entry whose
// options contain a value matching desired_model.
for config_opt in extract_model_config_options(session_new_result) {
let config_id = match config_opt.get("configId").and_then(|v| v.as_str()) {
let config_id = match config_opt
.get("configId")
.or_else(|| config_opt.get("id"))
.and_then(|v| v.as_str())
{
Some(id) => id,
None => continue,
};
Expand Down Expand Up @@ -3777,4 +3823,12 @@ mod tests {
"error must mention sandbox_workspace_write"
);
}

#[test]
fn harness_buzz_credentials_override_parent_environment() {
assert!(harness_bound_agent_env("BUZZ_RELAY_URL"));
assert!(harness_bound_agent_env("BUZZ_PRIVATE_KEY"));
assert!(!harness_bound_agent_env("CODEX_CONFIG"));
assert!(!harness_bound_agent_env("INITIAL_AGENT_MODE"));
}
}
Loading