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
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1014,18 +1014,23 @@ jobs:
mobile-swift:
name: Mobile Swift
runs-on: macos-latest
timeout-minutes: 10
timeout-minutes: 30
needs: [changes]
# Fork divergence: mobile lanes are upstream-only (mac desktop fork).
if: github.repository == 'block/buzz' && needs.changes.outputs.mobile == 'true'
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Install Flutter dependencies
run: cd mobile && flutter pub get
- name: Build
run: swift build --package-path mobile/ios/BuzzPushKit
- name: Build release
run: swift build -c release --package-path mobile/ios/BuzzPushKit
- name: Test
run: swift test --package-path mobile/ios/BuzzPushKit
- name: Build complete unsigned iOS release
run: cd mobile && flutter build ios --release --no-codesign --no-pub
security:
name: Security
runs-on: ubuntu-latest
Expand Down Expand Up @@ -1179,6 +1184,20 @@ jobs:
# Serial: windows_resolver_tests mutate process-global env
# (BUZZ_SHELL/GIT_BASH/SystemRoot) that SharedState::new reads.
run: cargo test -p buzz-dev-mcp --target $env:TARGET -- --test-threads=1
- name: Test (buzz-agent auth coordinator)
# The auth coordinator single-flights on an OS advisory lock, which is
# LockFileEx on Windows; this integration suite drives real second
# processes on the same lock file, so it only exercises the Windows
# lock runtime if it runs ON Windows. Every other job compiles it but
# never executes it. Tests exercised on Windows: lock serialization
# (two coordinators race for the same key), cooldown sidecar sharing
# across processes, attempt-sidecar adoption (UserInitiated waiter
# adopts a predecessor's denial), and the in-process single-flight for
# same-key coalescing. Tests that are UNIX-ONLY and NOT executed here:
# crash-release (flock drop on SIGKILL, guarded by #[cfg(unix)]) and
# cross-process cache success/race (on-disk token handoff, also
# #[cfg(unix)]).
run: cargo test -p buzz-agent --target $env:TARGET --test databricks_auth_coordinator
# Smoke-test the new host-prereq contract: Git for Windows (which provides
# bash) is available on the runner, a shell command round-trips, and bash
# does NOT resolve from System32 (so WSL's launcher is never picked up).
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codex-security-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ jobs:
contents: read
env:
CODEX_MODEL: gpt-5.6-sol
CODEX_REASONING_EFFORT: max
CODEX_REASONING_EFFORT: high
CODEX_REVIEW_API_KEY_PRESENT: ${{ secrets.CODEX_REVIEW_API_KEY != '' }}
REVIEW_CONTEXT: review-context
REVIEW_REPOSITORY: review-target
Expand Down
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,11 +688,13 @@ The mobile app lives in `mobile/` — a Flutter app using Riverpod + Hooks.
over raw `Theme.of(context)` calls.
- **Keep widgets small and composable.** One public widget per file; push
private sub-widgets (`_Foo`) into sibling `part` files under a
`<page>/` folder rather than growing the page file. Hard ceiling:
**1000 lines/file**, enforced across Desktop, Web, and Mobile by the
`<page>/` folder rather than growing the page file. Mobile's hard ceiling is
**1200 lines/file**, enforced with the other surface-specific limits by the
repository-level `just file-size-check` gate (`just check`, CI, and every
pre-push). If the guard trips, **split the file — never bump the limit or add
an override to slip under it.**
pre-push). If an individual file trips the guard, **split the file — never
bump a surface limit or add an override merely to admit that file.**
Deliberate repository-wide policy revisions must update the enforced rules,
tests, and guidance together.
- Feature modules must not import from other feature modules — only from
`shared/`.
- Use `Grid` tokens for spacing, `Radii` for border radius.
Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 15 additions & 8 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -466,14 +466,21 @@ test-unit:
# a regression here silently accepts a tampered or replayed bundle,
# so they must fail the gate rather than merely exist.
cargo nextest run -p buzz-waker
# buzz-agent model-capabilities corpus: the Rust half of the
# cross-language drift guard. `model_capabilities.rs` embeds
# scripts/model-capabilities.json + scripts/normative-corpus.json via
# include_str! and replays the full locked corpus as pure in-process tests (no
# infra). Enumerated explicitly because nothing in CI runs
# `cargo test --workspace`; without this step a manifest edit that
# diverges Rust from the corpus ships green.
cargo nextest run -p buzz-agent --lib
# buzz-agent: two infra-free concerns run together by executing the
# whole crate (lib + integration tests), because nothing in CI runs
# `cargo test --workspace`, so without this stanza neither the crate's
# library tests nor its integration tests execute remotely.
# * model-capabilities corpus (lib): the Rust half of the
# cross-language drift guard. `model_capabilities.rs` embeds
# scripts/model-capabilities.json + scripts/normative-corpus.json via
# include_str! and replays the full locked corpus as pure in-process
# tests; without it a manifest edit that diverges Rust from the
# corpus ships green.
# * OAuth auth coordinator (lib concurrency matrix + databricks
# integration tests): lock single-flight, cooldown, cross-process
# crash recovery — infra-free via a stub OIDC provider and an
# injected browser opener, no network or Postgres.
cargo nextest run -p buzz-agent
# Admin API auth-boundary tests (api::admin in buzz-relay): the NIP-98
# duplicate-tag rejections, the Host/Origin replay-ordering causal pair,
# the admin.localhost origin/advertisement/canonical-URL pins, and the
Expand Down
9 changes: 4 additions & 5 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ Buzz Desktop supports registering any ACP-speaking agent tool as a selectable ru

**Tier-1 — compiled-in runtimes** (Goose, Claude Code, Codex, Buzz Agent): have auto-installers, auth probes, and first-class onboarding. Their IDs (`goose`, `claude`, `codex`, `buzz-agent`) are reserved and cannot be overridden.

**Tier-2 — preset catalog** (Cursor, Oh My Pi, Grok Build, OpenCode, Kimi Code, Amp, Hermes Agent, OpenClaw): static `HarnessDefinition` entries in `desktop/src-tauri/src/managed_agents/discovery.rs` (`PRESET_HARNESSES`). They are always present in the runtime catalog, PATH-probed for availability, not editable or deletable by the user. Displayed with bundled logos; if not installed, a docs link appears instead.
**Tier-2 — preset catalog** (Cursor, Oh My Pi, Pi, Grok Build, OpenCode, Kimi Code, Amp, Hermes Agent, OpenClaw): static `HarnessDefinition` entries in `desktop/src-tauri/src/managed_agents/discovery/presets.rs` (`PRESET_HARNESSES`). They are always present in the runtime catalog, PATH-probed for availability, not editable or deletable by the user. Displayed with bundled logos; if not installed, a docs link appears instead.

> **Note — OpenClaw:** `openclaw acp` is a Gateway-backed bridge; PATH availability shows "Available" even when the OpenClaw Gateway daemon is not running. This is expected tier-2 semantics (same class as a preset with unconfigured auth). The Gateway URL is configured via `OPENCLAW_GATEWAY_URL` (or the equivalent env var from OpenClaw's docs) — set it in the agent's **env vars** in Edit Agent, not in the definition env (the preset definition carries no env entries). Note that `openclaw acp` executes tools inside the Gateway daemon, not the Desktop process, so Desktop-injected `BUZZ_*` env vars do NOT reach the execution locus unless you also set them on the Gateway's own environment.

Expand Down Expand Up @@ -327,10 +327,9 @@ Invalid files (bad JSON, unknown id, empty command) are skipped with a warning a
To add a new runtime to the tier-2 gallery:

1. **Verify the ACP entrypoint** from the vendor's own documentation — do not rely on a PR description alone. Test with the actual binary.
2. **Add a `HarnessDefinition` entry** to the `PRESET_HARNESSES` slice in `desktop/src-tauri/src/managed_agents/discovery.rs`. Fill `id`, `label`, `command`, `args`, `install_instructions_url`, `install_hint`. Leave `env` empty unless the harness requires a specific env var to enable ACP mode.
3. **Add the preset id to `BUILTIN_IDS`** in `desktop/src-tauri/src/managed_agents/custom_harnesses.rs` so custom JSON files cannot shadow it.
4. **Add a bundled logo** (64×64 PNG or optimised SVG) to `desktop/public/harness-logos/<id>.png` and add a corresponding entry to `PRESET_LOGOS` in `desktop/src/features/onboarding/ui/RuntimeIcon.tsx`. Record the source and license in `desktop/public/harness-logos/CREDITS.md`. Only bundle a mark whose upstream license permits redistribution; skipping this step is caught by `presetLogos.test.mjs`, which asserts every `PRESET_HARNESSES` id has a mapped logo that exists on disk.
5. Run `cargo test --lib` and `just desktop-typecheck` to verify everything compiles.
2. **Add a `PresetHarness` entry** to the `PRESET_HARNESSES` slice in `desktop/src-tauri/src/managed_agents/discovery/presets.rs`. Fill `id`, `label`, `command`, `args`, `install_instructions_url`, `install_hint`, and `underlying_cli` when the command wraps a separately installed CLI. Preset ids are automatically reserved so custom JSON files cannot shadow them.
3. **Add a bundled logo** (64×64 PNG or optimised SVG) to `desktop/public/harness-logos/<id>.png` and add a corresponding entry to `PRESET_LOGOS` in `desktop/src/features/onboarding/ui/RuntimeIcon.tsx`. Record the source and license in `desktop/public/harness-logos/CREDITS.md`. Only bundle a mark whose upstream license permits redistribution; skipping this step is caught by `presetLogos.test.mjs`, which asserts every `PRESET_HARNESSES` id has a mapped logo that exists on disk.
4. Run `cargo test --lib` and `just desktop-typecheck` to verify everything compiles.

The built-in `BUILTIN_IDS` set (`goose`, `claude`, `codex`, `buzz-agent`, and all current preset ids) is the reserved namespace; every other id is available for custom harnesses.

Expand Down
49 changes: 44 additions & 5 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ use crate::filter::SubscriptionRule;
///
/// Sized for slow turns where the agent may go silent on its outer ACP channel
/// while running long sub-tools (e.g. a buzz-agent running another agent, or
/// codex/claude doing multi-minute single tool calls). 900s gives 300s of
/// breathing room above the 600s max shell timeout, so legitimate long-running
/// codex/claude doing multi-minute single tool calls). 1500s gives 300s of
/// breathing room above the 1200s max shell timeout, so legitimate long-running
/// tool calls don't race the idle deadline.
/// Override via `--idle-timeout` / `BUZZ_ACP_IDLE_TIMEOUT`.
pub(crate) const DEFAULT_IDLE_TIMEOUT_SECS: u64 = 900;
pub(crate) const DEFAULT_IDLE_TIMEOUT_SECS: u64 = 1_500;

/// Default absolute wall-clock cap per agent turn (2 hours).
/// Override via `--max-turn-duration` / `BUZZ_ACP_MAX_TURN_DURATION`.
Expand Down Expand Up @@ -2757,9 +2757,9 @@ channels = "ALL"
// ── Idle timeout constant + guard (PR #935) ───────────────────────────────

#[test]
fn default_idle_timeout_is_900_seconds() {
fn default_idle_timeout_is_1500_seconds() {
// Lock the constant value so accidental changes are caught.
assert_eq!(DEFAULT_IDLE_TIMEOUT_SECS, 900);
assert_eq!(DEFAULT_IDLE_TIMEOUT_SECS, 1_500);
}

#[test]
Expand All @@ -2779,6 +2779,45 @@ channels = "ALL"
}
}

#[test]
fn budget_ordering_invariant_shell_cap_plus_headroom_fits_within_idle_timeout() {
// Asserts the three-layer budget relationship introduced in PR #7185:
// buzz-dev-mcp MAX_TIMEOUT_MS (1 200 000 ms = 1 200s)
// ≤ buzz-agent BUZZ_AGENT_TOOL_TIMEOUT_SECS default (1 260s)
// < buzz-acp DEFAULT_IDLE_TIMEOUT_SECS (1 500s)
//
// The idle deadline must strictly outlast the agent tool timeout so a
// legitimately long-running tool call is killed by buzz-agent first (at
// 1 260s) rather than the ACP idle watchdog. The 240s gap gives the agent
// time to handle the timeout, emit a response, and reset the idle clock
// before the ACP connection dies.
//
// If any of these constants change the compiler catches the inversion here.
// Cross-crate constants are mirrored as literals; grep for PR #7185 to
// find the authoritative source if you need to update them.
const SHELL_CAP_MS: u64 = 1_200_000; // buzz-dev-mcp MAX_TIMEOUT_MS
const SHELL_CAP_SECS: u64 = SHELL_CAP_MS / 1_000;
const AGENT_TOOL_TIMEOUT_SECS: u64 = 1_260; // buzz-agent BUZZ_AGENT_TOOL_TIMEOUT_SECS default

const {
// Shell cap must not exceed the agent's per-tool-call timeout.
assert!(
SHELL_CAP_SECS <= AGENT_TOOL_TIMEOUT_SECS,
"shell cap must be <= agent tool timeout"
);
// Agent tool timeout must be strictly less than the ACP idle deadline.
assert!(
AGENT_TOOL_TIMEOUT_SECS < DEFAULT_IDLE_TIMEOUT_SECS,
"agent tool timeout must be < ACP idle timeout"
);
// ACP idle timeout must remain below the max turn duration.
assert!(
DEFAULT_IDLE_TIMEOUT_SECS < DEFAULT_MAX_TURN_DURATION_SECS,
"ACP idle timeout must be < max turn duration"
);
}
}

// --- BUZZ_ACP_ALLOWED_RESPOND_TO gate ---

fn parse_allowed_respond_to(raw: &[&str]) -> Result<HashSet<RespondTo>, ConfigError> {
Expand Down
23 changes: 23 additions & 0 deletions crates/buzz-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ path = "src/main.rs"
name = "fake-mcp"
path = "tests/bin/fake_mcp.rs"

# Test-only lock holder: a real second process that takes the coordinator's
# cross-process advisory lock, so the auth tests can prove genuine
# inter-process single-flight and crash-release rather than same-process
# handles. Tiny; only used by the databricks auth integration tests.
[[bin]]
name = "lock-holder"
path = "tests/bin/lock_holder.rs"

# Test-only auth worker: a real second process that runs the PUBLIC auth
# coordinator API (`acquire_with_intent`) with a scripted browser opener and a
# shared temp cache, so the auth tests can prove the cross-process single-flight
# contract end-to-end — durable cooldown sharing and one-grant/one-cache races
# across a genuine process boundary, not two in-process handles. Only used by
# the databricks auth integration tests.
[[bin]]
name = "auth-worker"
path = "tests/bin/auth_worker.rs"

[dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-std", "io-util", "sync", "process", "time", "net"] }
serde = { workspace = true }
Expand All @@ -45,6 +63,11 @@ url = { workspace = true }
urlencoding = "2"
webbrowser = "1"
dirs = "6"
# Cross-process advisory file lock (flock on Unix, LockFileEx on Windows) for
# the auth coordinator's single-flight. Kept off std's `File::try_lock` so the
# crate stays buildable on the repo's declared 1.88 MSRV (those std APIs are
# 1.89+).
fs2 = "0.4"

[target.'cfg(unix)'.dependencies]
nix = { version = "0.31", default-features = false, features = ["signal", "process"] }
Expand Down
4 changes: 2 additions & 2 deletions crates/buzz-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ Everything is environment variables. No flags, no config files. (We are a subpro
| `BUZZ_AGENT_MAX_CONTEXT_TOKENS` | `200000` | Provider context window used by the handoff gate. |
| `BUZZ_AGENT_MAX_HANDOFFS` | `10` | Max context handoffs per session before falling back to truncation. |
| `BUZZ_AGENT_LLM_TIMEOUT_SECS` | `240` | Max seconds with no response bytes before abandoning an LLM call (per-read inactivity, not wall-clock). |
| `BUZZ_AGENT_TOOL_TIMEOUT_SECS` | `660` | Per-tool call timeout in seconds |
| `BUZZ_AGENT_TOOL_TIMEOUT_SECS` | `1260` | Per-tool call timeout in seconds |
| `BUZZ_AGENT_MAX_PARALLEL_TOOLS` | `8` | Max concurrent tool calls per turn (1 = sequential) |
| `BUZZ_AGENT_MAX_SESSIONS` | unlimited | Max concurrent ACP sessions. Sessions are cheap; default has no cap. |
| `BUZZ_AGENT_MAX_LINE_BYTES` | `4194304` | 4 MiB. Hard cap on inbound JSON-RPC frames. |
Expand Down Expand Up @@ -326,7 +326,7 @@ The trust boundary is **the operator who launched the agent**. The harness, MCP
| Tool calls per turn | 64 | `MAX_TOOL_CALLS_PER_TURN` |
| Loop rounds | 0 (unlimited) | `BUZZ_AGENT_MAX_ROUNDS` |
| LLM read inactivity timeout | 240 s | `BUZZ_AGENT_LLM_TIMEOUT_SECS` |
| Tool call timeout | 660 s | `BUZZ_AGENT_TOOL_TIMEOUT_SECS` |
| Tool call timeout | 1260 s | `BUZZ_AGENT_TOOL_TIMEOUT_SECS` |

## What This Is NOT

Expand Down
Loading
Loading