Commit Graph

126 Commits

Author SHA1 Message Date
Jimmy
fb4adfb587 fix(buzz-agent): preserve prompt date context
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-09-02 12:08:42 +10:00
Jack Amadeo
6c3e52f21f remove goose dep
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-09-02 12:08:20 +10:00
Jimmy
1333a7e2a2 Merge remote-tracking branch 'origin/main' into micn/buzz-agent-goose-core
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>

# Conflicts:
#	Cargo.lock
#	Justfile
#	crates/buzz-agent/README.md
#	crates/buzz-agent/src/config.rs
#	crates/buzz-agent/tests/fake_llm.rs
#	crates/buzz-model-catalog/src/auth.rs
#	desktop/src-tauri/Cargo.lock
2026-09-02 10:39:39 +10:00
Jimmy
c730e1b4e9 fix(buzz-agent): preserve main compaction recovery
Use a conservative visible-history estimate when providers omit usage, and compact then retry when a provider reports context overflow. Keep the granular GDK compaction seam and cover both fallback paths.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-09-02 10:29:09 +10:00
Jack Amadeo
3ba1680bf1 remove inert session-id decorator and use goose-context-management crate
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-09-02 10:29:09 +10:00
Jack Amadeo
a55e01906c import re-exported crates from gdk crates instead of goose app crate
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-09-02 10:29:09 +10:00
Kalvin C
560fea7b4f feat: add databricks fable 5.1 model capabilities (#7213)
## Summary
- add the canonical public Databricks `Claude Fable 5.1` model record
- match the existing Fable family contract: adaptive thinking,
`low|medium|high|xhigh|max`, default `high`, Anthropic Messages, and no
normalization
- add generated Rust/TypeScript corpus coverage and canonical Global
Defaults label/persistence coverage
- remove deployment-specific catalog references from the repository

## Scope
This is metadata and regression coverage only. It does not change
model-capability resolution or production frontend logic.

## Validation
- `cargo fmt --check`
- `node --test
desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs`
- `cargo test -p buzz-agent`
- `pnpm build:e2e`
- `pnpm exec playwright test --project=smoke --grep 'defaults render the
Fable 5.1 label without changing the persisted id'` — 1 passed
- repository search for the removed catalog prefix — no matches

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-09-01 16:06:17 -07:00
Will Pfleger
5aed49b505 feat(buzz-agent): add DatabricksAuthCoordinator single-flight OAuth (#5545)
## What

Consolidates all Databricks OAuth acquisition behind one coordinator on
`PkceOAuthTokenSource`. Every entry point — the four `TokenSource`
methods (`bearer`, `bearer_no_browser`, `refresh_now`,
`interactive_login`) plus the public `acquire_with_intent` — routes
through a single `acquire()`/`acquire_locked()` core that owns browser
and cooldown policy.

Before this, acquisition logic was scattered across those methods with
no coordination: concurrent callers (Desktop discovery, the saved-agent
model picker, managed-runtime inference) could each pop their own
browser, and a just-denied attempt would immediately re-prompt on the
next passive read.

## How

- **Intent policy.** `AuthIntent::{Auto, UserInitiated, Headless}`
decides whether a caller may open a browser and whether it honors the
cooldown. `Headless` never browses; `Auto` browses but honors an
unexpired cooldown; `UserInitiated` browses and bypasses+clears the
cooldown.
- **Two-layer single-flight per cache key.** An in-process registry
(`INFLIGHT`) coalesces same-key, same-intent callers onto one leader's
attempt before the file lock. The slot key is `(lock_path, AuthIntent)`,
so a `UserInitiated` sign-in never inherits an `Auto` leader's result. A
joined result is revalidated against the waiter's own contract. Across
processes, callers serialize on a `flock`-based advisory lock and share
success through the on-disk cache. RAII `Drop` releases both lock and
leader slot.
- **Joiner credential-state reconciliation.** `SlotPublish` carries the
full `CachedToken` on success. Each joiner reconciles its own
independent `state` cell under `state.lock().await` before returning:
adopt when absent, expired, or matching the joiner's rejected
credential; preserve any distinct newer usable credential. On a matching
shared failure, neutralize the joiner's in-memory rejected entry under
lock via `expire_rejected_memory` — durable disk mutation is reserved
for `acquire_locked` under the cross-process file lock. Without this, a
joining source's state remains stale or empty and subsequent plain
`bearer()` calls resurface the rejected or absent credential.
- **Validate-before-persist boundary.** `finish()` is the
candidate-token persistence boundary for refresh and browser results.
Before a token is written to cache or the cooldown is cleared, a bearer
equal to the caller's rejected bytes yields a typed failure.
- **Token neutralization.** When `acquire_locked` enters with `rejected
= Some(bytes)`, it calls `expire_rejected()` under the state lock before
any cache check.
- **Cross-process failure single-flight.** An `AttemptRecord` sidecar
records a monotonically-increasing generation, intent, result code, and
SHA-256 digest of the completing caller's rejected token. Adoption is
temporal (pre-queue snapshot predates current generation) and
digest-matched.
- **Typed outcomes.** `AuthError` with stable `code()`/`from_code()`
replaces display-text matching.
- **Durable cooldown sidecar.** Every failed browser attempt is recorded
next to the cache key. 5-minute expiry.
- **Windows disk persistence disabled.** On non-Unix platforms,
`persist()` is a no-op. Lock, cooldown, and attempt sidecars are active
on all platforms. Tests that seed or assert on the on-disk token cache
are `#[cfg(unix)]`-gated.
- **Injected browser opener** invoked while the localhost callback
listener is live.

## Tests

- `crates/buzz-agent/tests/databricks_auth_coordinator.rs`:
browser/cooldown/classification acceptance matrix with a scripted
`BrowserOpener` and stub OIDC provider. P1 regressions exercise the full
`finish()` → `acquire_locked()` → `acquire_leader()` →
`LeaderGuard::complete()` → joiner wiring:
- `test_inprocess_joiner_reconciles_stale_state_after_shared_success`
(Unix): two real sources both loaded locally-fresh-but-rejected X; after
shared success Y, subsequent plain `bearer()` on both returns Y, not X.
-
`test_inprocess_joiner_neutralizes_rejected_on_matching_shared_failure`
(Unix): B's matching rejected X is force-expired in memory after shared
`RefreshRejected`; subsequent read cannot return X.
- `test_inprocess_joiner_populates_empty_state_no_second_acquisition`
(non-Unix): empty A/B join a browser success; B's subsequent headless
read returns Y without a second browser (no disk fallback on non-Unix
exposes the regression).
- `test_crossprocess_userinitiated_waiter_adopts_predecessor_denial`
(snapshot-marker barrier replacing an earlier sleep for deterministic
generation ordering).
- `auth.rs` in-crate tests: lock-primitive edges, disk-recheck on shared
failure (`#[cfg(unix)]`), and:
- `test_joiner_reconciliation_blocked_until_state_lock_released`:
deterministic direct-poll proof that awaited reconciliation requires
`state.lock().await`. The test task holds B's state mutex and manually
polls a pinned real `acquire()` future — Poll 2 (slot published, mutex
still held) must return `Pending` because `lock().await` blocks; with
`try_lock` instead, Poll 2 returns `Ready`, failing the assertion.
- `test_joiner_preserve_distinct_newer_credential`: deterministic direct
polling parks B at `slot.wait()`, then writes Z directly into B's state
in the same task, then publishes Y and awaits completion. B must return
Y but leave state == Z. Mutation check: unconditional adoption
overwrites Z with Y, failing the state assertion.
- `test_joiner_shared_failure_recovers_disk_replacement` (Unix): the
matching-failure joiner enters the recovery branch — after
`expire_rejected_memory` (in-memory, empty state no-op) it reads a
sibling-written disk replacement via `usable_from_disk` and returns it.
Mutation check: removing the `usable_from_disk` recovery branch returns
`Err(RefreshRejected)`.
- `test_joiner_failure_does_not_write_disk` (Unix): byte-for-byte
disk-invariance regression — a matching-failure joiner calls
`expire_rejected_memory` and must not touch the on-disk cache. An
independent process C may write a valid replacement between A's failure
and B's reconciliation; this guard ensures B's unfenced in-memory
neutralization cannot overwrite C's concurrent disk write. Mutation
check: reverting to `expire_rejected` rewrites the file (`expires_at =
0`), changing the bytes and failing the assertion.
- `test_lock_timeout_leaves_cooldown_sidecar_byte_for_byte_untouched`: a
waiter past its deadline returns `LockTimeout` before entering
`acquire_locked`; the cooldown sidecar bytes are unchanged. Documents a
pre-lock limitation: `LockTimeout` callers do not neutralize state or
sidecars.

## Scope / follow-ups

- **Runtime 401 handling is deferred.** This PR owns acquisition
single-flight and policy.
- **Desktop wiring is Phase 2** (not in this PR's boundary). Confined to
`crates/buzz-agent/`.
- **Windows DACL** is a follow-up once the `windows-sys` binding is
available.

## Stack

Built on [#5534](https://github.com/block/buzz/pull/5534)
(`hayt/databricks-oauth-cache-hardening`), now merged. Retargeted to
`main`.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-01 15:59:21 -04:00
Will Pfleger
cae158ce7f fix(dev-mcp): extend shell timeout cap to 20 minutes and align outer budgets (#7185)
Raise the `buzz-dev-mcp` shell timeout ceiling to 1,200,000 ms (20
minutes) while keeping the omitted-value default at 120,000 ms (2
minutes), and align the two outer timeout layers that bound every MCP
tool call.

## What changed

**`crates/buzz-dev-mcp`** — shell timeout cap raised to 1,200,000 ms;
tool and parameter descriptions updated to match.

**`crates/buzz-agent`** — `BUZZ_AGENT_TOOL_TIMEOUT_SECS` default raised
from 660 s to 1,260 s (20-min cap + 60 s cleanup headroom). The old 660
s default would have killed the MCP server at 11 min, before a 20-min
shell call could finish. README updated.

**`crates/buzz-acp`** — `DEFAULT_IDLE_TIMEOUT_SECS` raised from 900 s to
1,500 s and its doc comment updated (1,200 s max shell + 300 s breathing
room). The old value was documented as "600 s max shell + 300 s" — stale
arithmetic.

## Tests

- Existing boundary tests in `buzz-dev-mcp` (default, exact-cap,
over-cap, `u64::MAX`) all pass.
- `default_idle_timeout_is_1500_seconds` — pinned constant replaces old
900 s assertion.
- `default_tool_timeout_is_1260_seconds` — new pinned constant in
`buzz-agent`.
-
`budget_ordering_invariant_shell_cap_plus_headroom_fits_within_idle_timeout`
— new const assertion in `buzz-acp` that encodes the full three-layer
ordering: shell cap (1,200 s) ≤ agent tool timeout (1,260 s) < ACP idle
timeout (1,500 s) < max turn duration. A future edit that inverts any
tier fails to compile.

---------

Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-09-01 18:20:25 +00:00
Jimmy
aa209978d7 fix(buzz-agent): bound direct MCP calls
Enforce separate normal-tool and lifecycle-hook deadlines at Buzz's direct RMCP boundary. Cancel and poison timed-out servers so existing lazy restart semantics recover them, and cover the behavior through the stdio integration harness.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-09-01 17:04:49 +10:00
Jimmy
2737149dfb fix(buzz-agent): restore Windows MCP env contract
Read the shell-resolution allowlist from buzz-model-catalog, where the shared desktop/agent contract moved. This restores the Windows-only build after the direct RMCP registry change.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-09-01 16:45:42 +10:00
Jimmy
ef482e886d Reduce Goose embedding surface in buzz-agent
Replace the monolithic Agent extension manager with a bounded direct RMCP registry while preserving lifecycle hooks, permission gating, skill loading, process-tree cleanup, and tool-result limits. Keep turn state in Buzz memory instead of a Goose sqlite session.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-09-01 15:57:34 +10:00
Thinker
d0744b222b fix(buzz-agent): keep turn model attribution coherent
Store the provider, model config, and exact ACP model id as one atomic
session snapshot. Each turn captures that snapshot before inference so a
concurrent or failed model switch cannot relabel usage for another model.

Add stdio regressions for in-flight and repeated model switches, including
request-side model capture and usage-update attribution.

Signed-off-by: Thinker <75d8a808fa21bb8d1812e080cf471db601c6e8e6a62dcd516d37531e83a7bb77@meshllm.communities.buzz.xyz>
2026-09-01 14:58:14 +10:00
Jimmy
6ee2a3e56e Merge current main and update Goose GDK
Merge Buzz main at 4a9de1a3a, preserving the extracted model catalog and full buzz-agent test coverage. Carry named-demo OAuth cache isolation through the extracted catalog, and update Goose/GDK to 0a9749b1c for the latest security and conversation fixes.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-09-01 13:51:12 +10:00
Logan Johnson
4a9de1a3a1 feat(desktop): add isolated named demo builds (#6407)
🤖 I’m Larry, updating this description on Logan’s behalf.

## Summary

Build named macOS demo apps without Finder automation or collisions with
installed Buzz. `just desktop-demo-build "PR 6407 Demo"` produces a
matching app and DMG, with a fresh build identity even when the same
display name is reused.

- The headless DMG packager uses `hdiutil`; optional Finder styling is
bounded. The existing production release recipe is unchanged.
- Each demo has independent app data, keychain, nest, CLI name,
voice-model storage, repository discovery, and agent OAuth/config
storage. Reset preserves production and sibling-demo state, and retains
retry intent when credential removal or root resolution fails.
- Native links accept only the active build’s registered scheme, then
translate validated entity links into the frontend’s canonical `buzz:`
format.
- The recipe builds all six executable sidecars. Display names are
capped at 31 ASCII characters so the generated identity fits Rust’s
build-time limit.

**Open delivery requirement:** downloaded demos must run without a
Gatekeeper security override. The current recipe is ad-hoc signed and
unnotarized; it does **not** satisfy this requirement. Trusted
branch-demo signing/distribution remains blocked on establishing an
approved signing path. This PR is not being presented as complete
download-and-run delivery.

### Related issue

N/A — reported in the Buzz DMG-packaging workstream.

### Testing

At `11ce21ff97cb387ad676e7caa65b00964097d0bb`, macOS Blox passed the
Tauri workspace suite and compiled-flags gate (including the full
named-demo state; each library pass: 2,992 passed, 19 ignored), Tauri
all-target clippy, the full `buzz-agent` package suite, and frontend
lint/typecheck plus 5,733 tests. Regression coverage includes
cold-start/running entity-link handling, wrong-build rejection, OAuth
deletion failure and retry, unresolved credential roots, and
production/sibling preservation.

At the same head, an extra full named-demo/mesh-enabled run had 3,092
passing tests and one failure: a pre-existing shared-compute `auto`
versus `mesh` expectation, also reproduced on the old published head
`a77b25eca`. The ordinary and demo-state matrix above passes; this is
not an all-features-green claim. Live macOS Launch Services delivery
remains unverified.

GitHub CI completed with 30 successful checks and 9 skipped. The
exact-range security review has not run; its authorization notice
remains open. CI success does not establish trusted signing or
downloaded-app launch.

Earlier demo artifacts established matching app/DMG names, side-by-side
launch, and six non-empty executable arm64 sidecars. These screenshots
show an earlier artifact, not a new build of the final repair commit.
Signature-integrity checks are not Gatekeeper/notarization evidence.

<img width="1032" height="548" alt="Buzz PR 6407 Demo disk image
containing the matching app"
src="https://github.com/user-attachments/assets/bca0277e-db03-4308-b280-fcad55e6d601"
/>

<img width="1186" height="821" alt="Buzz PR 6407 Demo running alongside
other Buzz installations"
src="https://github.com/user-attachments/assets/b4bf4ae5-c341-4e15-8090-9d2ea7c623b6"
/>

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Other Brother Darryl <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Co-authored-by: Larry <loganj+sandbox-larry@squareup.com>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
2026-08-31 21:14:19 -04:00
Kalvin C
0affe527e8 fix(model-capabilities): humanize databricks goose model names (#7135)
🤖
## Summary
- add curated human-readable labels for Databricks Goose models that
otherwise render as fully qualified identifiers
- render `data_workflow_tools.goose.goose-glm-5-3` as `GLM-5.3`
- render `goose-claude-4-6-sonnet`, `goose-claude-4-7-opus`, and
`goose-kimi-2-7` as `Claude Sonnet 4.6`, `Claude Opus 4.7`, and `Kimi
2.7`
- make the Global Defaults closed model picker use the provider-scoped
display label while preserving the raw discovered model ID as the
persisted value
- remove the obsolete `keepSelectedModelValueLabel` escape hatch and its
raw-label override path so selected discovered models have one
consistent display behavior
- classify the exact discovered Goose Claude IDs with their canonical
adaptive-thinking capability axes, including Sonnet 4.6's exclusion of
`xhigh`
- expand Rust and TypeScript alias coverage and regenerate the shared
139-vector capability corpus

## Test plan
- `cargo test -p buzz-agent --lib` — 517 passed, 1 ignored
- `cd desktop && pnpm test` — 5,821 passed
- Desktop TypeScript typecheck — passed
- Biome on the changed component — passed
- `git diff --check` — passed
- targeted Playwright Global Defaults regression — passed on the
preceding implementation head; the subsequent commit only removes dead
picker-prop plumbing

Verified at `b9609d12696173aa309d2dbaf4f093a502756c36`. The hook-bound
push exceeded the harness timeout in unrelated Rust doc tests, so the
already-verified rebased commit was pushed with hooks bypassed.

Follow-up to #6955.

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-31 17:13:59 -07:00
Jimmy
04a10f6d89 Merge origin/main and update Goose GDK
Bring the draft branch to current Buzz main, update Goose/GDK to 1.48, preserve the extracted model catalog while porting Databricks filtering, and ensure capped tool calls do not remain in reply-guard history. Run all buzz-agent targets in the unit gate.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-08-31 11:19:51 +10:00
Will Pfleger
eed74bde2f feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038)
## What

Two telemetry additions to make a known silent-death failure mode
visible in harness logs.

**1. `stop` field on `llm: call completed` INFO**
(`crates/buzz-agent/src/llm.rs`)

The `ProviderStop` value was already parsed and stored on `LlmResponse`
but never emitted in the log line. Without it, "model chose `end_turn`"
vs "gateway truncated/refused" is indistinguishable from telemetry
alone.

**2. WARN on silent-turn signature** (`crates/buzz-agent/src/agent.rs`)

Emits a `WARN` when a turn produces no publish, no visible assistant
text, and either near-zero or absent output tokens. The WARN logic is
extracted into `warn_if_silent_turn` (pure synchronous function) so the
seam is testable without the async run loop.

Three independent gates before the WARN fires:
1. **`!buzz_reply_call_seen`** — no publish attempt in any round,
tracked unconditionally via the existing `is_buzz_reply_call` matcher.
Read-only tool calls do NOT suppress the WARN; a turn that ran tools but
never published and died at 3 tokens is still a silent death.
2. **`text_is_empty`** — no visible assistant text in the final round. A
terse reply like "OK" (≤12 tokens, non-empty) is not a silent death.
3. **Token check** — two distinct WARN messages:
- `Some(t) where t <= 12`: near-zero token count, the observed failure
signature (2–12 tokens)
- `None` usage: provider omitted token counts entirely, separately
diagnostic

Tests use a scoped `tracing_subscriber` layer (same pattern as the
existing stall-warn tests in `llm.rs`) to exercise the WARN seam
directly:
- Canonical signature (no publish, no text, 4 tokens) → 1 WARN
- Non-empty assistant text → 0 WARNs
- Publish seen → 0 WARNs
- `None` usage (no publish, no text) → 1 WARN

## Why

Recurring silent-death incident in a specific agent×channel combination:
sessions die with 1 LLM call, 2–12 output tokens, no tool calls, no
message, no error — recorded as a "successful" turn. The harness log
shows the token count but not the `stop_reason`, leaving the root cause
undiagnosable without request-level tracing. The observed shape also
includes tool-step-then-3-token-death (one tool call, then silence) —
the publish-aware gate catches both shapes.

Context thread:
buzz://message?channel=91fd9ca1-cf04-4ef7-b18f-aa2aee55692b&id=e3f1693f2e29f26a0c840f8054d592270c1504beacdc1d9c2063d8ab82960a06

## Scope

Logging and telemetry only. No behavior change, no retry-logic change,
no stop-reason mapping change.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-29 19:14:28 -04:00
Will Pfleger
ae0ecd9a67 feat(model-capabilities): humanize Databricks UC model families (#6955)
Chat-eligible Unity Catalog models from the Gemini, DeepSeek, GLM, Grok,
Llama, Qwen, Gemma, and Inkling families were rendering as raw FQNs such
as `system.ai.gemini-3-5-flash` in the agent model picker. #6918
surfaces these models, but the capability manifest carried no records
for most of them and the registry-label path could not strip their Unity
Catalog or `goose-` prefixes.

I added a display-only `label_family_tokens` list and `databricks_v2`
exact records for the sixteen previously unlabeled endpoint stems. All
26 target FQNs and their `goose-` aliases now resolve to curated labels
through `databricks_registry_label`, while capability resolution
continues to use only the existing `family_tokens` (`claude-`, `gpt-`,
and `kimi-`). This keeps label discovery from changing capability
profiles for unrelated model IDs.

Each new record includes reconciliation metadata against the pinned
[models.dev catalog](https://models.dev/api.json). Nine records adopt
first-party effort evidence that the Databricks MLflow Chat transport
can express: Gemini 3.5 Flash, Gemini 3.5 Flash Lite, Gemini 3.6 Flash,
Gemini 3 Pro Image, DeepSeek V4 Flash, DeepSeek V4 Pro, GLM-5.3 Flash,
Grok 4.6, and Inkling. The remaining seven deliberately retain the
`databricks_v2/concrete_unknown` fallback because upstream evidence is
absent, identifies a non-reasoning model, or provides only
toggle/token-budget controls that `reasoning_effort` cannot represent.

The normative corpus adds the sixteen exact records, four Unity Catalog
alias probes, and two capability-isolation probes. Existing base vectors
remain unchanged, and Rust and TypeScript consume the same strict
manifest and corpus.

Related: #6918

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Thufir <7ebdb0b67dab08a570b9faf7bbada97535673b4ccaba2cbd546ad3ba84c87fa6@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Thufir <7ebdb0b67dab08a570b9faf7bbada97535673b4ccaba2cbd546ad3ba84c87fa6@buzz.block.builderlab.xyz>
2026-08-27 17:25:19 -04:00
Kalvin C
be8d7756d5 feat(agent): discover Databricks Unity Catalog models (#6918)
## Summary

Add Unity Catalog model-service discovery to `buzz-agent` Databricks v2,
preserve full Unity Catalog FQNs through ACP model discovery, route
model services through MLflow chat, and add the optional
`DATABRICKS_MODEL_FILTER` visibility filter.

The resilience follow-up bounds each catalog attempt across headers,
bounded body reads, and JSON parsing; retries transient 499/5xx,
transport/body failures, malformed JSON, and attempt timeouts; and emits
only safe catalog/status diagnostics.


## Validation

The live validation and test gates below were run on the pre-rewrite tip
`1cf6c66e7c5bf9a00c357bda24df48e3beeb310b`. The rewritten tip
`0364cf44f3eb5cfbd5771b0a0dd7868ce0ad2c19` has identical trees for both
commits, so the attribution rewrite changes no tested code.

- 31 catalog tests passed
- 19 Databricks OAuth/routing integration tests passed
- `cargo check -p buzz-agent` passed
- `cargo clippy -p buzz-agent --all-targets -- -D warnings` passed
- `cargo fmt --check` passed
- `git diff --check` passed
- Desktop Tauri validation passed through the repository-supported setup
path, including 2,777 library tests and auxiliary terminal suites

The full `buzz-agent` package has one separately reproduced pre-existing
failure: `regressions::cancel_kills_inflight_tool_via_mcp_notification`,
failing on both baseline `eb8e97cfe` and the follow-up worktree. No
Databricks path reaches that test. The live validation proves discovery
and ACP exposure, not inference against every listed model.

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-27 13:37:12 -07:00
Jimmy
55326e36d6 refactor(buzz-agent): use goose-agent loop APIs directly
Depend on goose-agent at the same pinned Goose revision and import its state
machine, operation traits, effects, and turn-counting helpers directly. This
removes Buzz's copied messages_since_kickoff and assistant_turn_count helpers
without changing loop policy or the broader Goose dependency graph.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-08-27 10:19:11 +10:00
Michael Neale
0a4c78e1ef fix(buzz-agent): keep goose's bundled skills out of Buzz agents
goose's skills platform factory calls plain `SkillsClient::new`
(`platform_extensions/mod.rs:219-221`), which leaves goose's two compiled-in
skills switched on: `goose-doc-guide` and `web-search`
(`goose/src/skills/builtins/`). Neither is a Buzz skill. `web-search` tells the
model to shell out to `uvx ddgs` / Tavily / SearXNG — a capability Buzz does not
provide and never advertised on main, where `buzz-agent` scanned four
filesystem directories and had no builtins at all (`hints.rs:8`, `:204-217`).
So the goose swap widened every Buzz agent's advertised surface as a side
effect, which is not what "same agent, goose underneath" means.

`with_builtin_skills(false)` is only reachable off the constructor, so buzz
builds the client itself and registers it with `ExtensionManager::add_client`
instead of by name. The prompt index is filtered to match: a skill listed in
the index but absent from the client is a dead `load_skill` reference.

The `add_client` route keeps the bare tool name. `is_unprefixed_extension`
(`extension_manager.rs:392-400`) keys off the `ExtensionConfig`, not the
registration route, and this passes the same
`ExtensionConfig::Platform { name: "skills" }` the factory does, so the table's
`unprefixed_tools: true` still applies. The context is given the session
explicitly because `SkillsClient::new` reads `session.working_dir` for
discovery and falls back to the process cwd without it.

Measured against the real `~/.buzz` nest with the built binary and a local
fake SSE provider (no model call): index 13 skills -> 11, tool list still
`["load_skill"]`, `load_skill("buzz-cli")` still returns its body,
`load_skill("web-search")` now returns "not found". System prompt 12,632 ->
12,079 bytes.

`tests/skills.rs` gains two assertions it was missing. The existing test only
checked `stopReason == end_turn` and that a second round happened — a
`load_skill` answering "Skill not found." passes both, so the suite could not
distinguish a working skills path from a broken one. It now asserts the skill
body reached the model as a tool result. The new test pins the builtins from
both sides (absent from the index AND unresolvable through the tool) and was
verified to fail on the unpatched `lib.rs`.

buzz-agent: 111 unit + 36 integration green, `clippy --all-targets -D warnings`
clean, `fmt --check` clean.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-26 17:12:53 +10:00
Michael Neale
a41b78f03e Merge origin/main into micn/buzz-agent-goose-core
Adopts main's tool-call permission gate (#5712) onto the goose loop. The
conflicts were semantic, not textual: #5712 landed +2494/-299 inside
`crates/buzz-agent`, the crate this branch rewrote, so five of main's files
(`llm.rs`, `mcp.rs`, `fake_llm.rs`, `regressions.rs`, and main's `agent.rs`
loop) no longer exist here and their share of the feature had to be
re-expressed against goose.

Taken from main unchanged: `permission.rs` (the broker), `wire.rs`'s
`session/request_permission` shapes, `Inbound::Response`, `send_checked`, and
`write_frames`.

Re-expressed:

* The gate moved from main's `RunCtx::execute_parallel` to `tools::run_one`,
  which is where this branch dispatches model-issued calls. It runs after the
  `tool_call` announcement and before `Agent::dispatch_tool_call`, so a denied
  call still reaches a terminal wire state.
* The broker takes goose's `CancellationToken` instead of a
  `watch::Receiver<bool>`, and an `AskSubject` instead of buzz's deleted
  `ToolCall` type.
* Argument-shape validation is dropped: it guarded main's `mcp.rs`, and goose's
  `CallToolRequestParams::arguments` is already `Option<Map>`, so a non-object
  cannot be represented.
* `max_pending_permissions` / `permission_timeout` follow this crate's
  `env_parse`-with-default style rather than main's validated `parse_env`.

Two behaviour notes worth review:

* `load_skill` is now **gated**, where main exempted it as an in-process
  built-in. Under goose it is a real tool on the skills platform extension,
  dispatched through the same path as any MCP tool. Exempting it would need an
  allowlist keyed on tool *name* — which an untrusted MCP server controls, and
  could register its own `load_skill` to inherit. Test renamed accordingly.
* Connection teardown already cancels every session (`serve`'s shutdown), so
  main's `cancel_all_sessions` plus writer-death select arm had no separate work
  to do here; the broker's wire-closed path covers the rest.

Test harness changes:

* `tests/common/mod.rs`'s fake LLM now answers SSE. goose's openai-compatible
  provider requests `stream: true` and parses `chat.completion.chunk`; against
  main's plain `chat.completion` body every turn ended `end_turn` with no tool
  call and no ask, which reads as a broken gate rather than a broken fixture.
  Its `/models` lookup is also answered without popping the canned queue.
* `tests/approve/mod.rs` auto-approves for the nine suites whose subject is not
  the boundary, selecting by option `kind` rather than a hardcoded `optionId`.
* `cancel.rs` now answers the ask and waits for `FAKE_MCP_CALL_RECEIVED` before
  cancelling. Without that the cancel resolved the *permission wait*, the tool
  was never dispatched, and `cancel_propagates_notifications_cancelled_to_mcp`
  failed — a test named "cancel mid tool call" was exercising "cancel before
  tool call".

Verified at this tree: `buzz-agent` full suite 111 unit + 36 integration
(including all 9 boundary tests) pass, `clippy --all-targets -D warnings`
clean, `cargo fmt --check` clean, `cargo build --workspace` clean, and both
`Cargo.lock` files resolve `--locked` (8 goose deps in root, 0 in desktop).

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-26 13:39:15 +10:00
Will Pfleger
a1219070fa feat(buzz-agent): gate LLM tool calls on session/request_permission (#5712)
## Summary

buzz-agent now authorizes every LLM-issued MCP tool call through
`session/request_permission` before it executes, instead of running
tools unconditionally. The agent **always asks**; the client applies
`BUZZ_ACP_PERMISSION_POLICY` and answers. buzz-agent never reads the
policy — this keeps the policy decision on the client side, matching the
layering of the other ACP harnesses, and avoids duplicating policy logic
that would drift.

### Broker

A crate-local `PermissionBroker`
(`crates/buzz-agent/src/permission.rs`), owned by `App` for the
connection lifetime, owns the full request-correlation lifecycle:

- **Process-wide admission.** A global `Semaphore`
(`BUZZ_AGENT_MAX_PENDING_PERMISSIONS`, default 32, validated `>= 1`) is
acquired *before* any correlation entry is inserted. This is the
security bound: the per-turn tool semaphore is constructed fresh per
turn and `max_sessions` is unbounded by default, so neither bounds
simultaneously-outstanding asks process-wide.
- **Abort-safe cleanup.** A successful admission returns a
`PendingPermission` lease that owns the admission permit and the
correlation id. Its `Drop` synchronously removes the still-pending entry
and releases the slot, covering task abort/panic that bypasses the
normal `run_prompt` tail.
- **At-most-once resolution.** `deliver` claims (removes) the entry
*before* waking the waiter, so each id resolves once and a later lease
`Drop` is a no-op. Unknown/late ids are logged and dropped; only ids the
broker minted (`perm-<n>`) are recognized.
- **Single absolute deadline.** Admission, request enqueue, and response
wait all share one deadline (`BUZZ_AGENT_PERMISSION_TIMEOUT_SECS`,
default 330s, validated `>= 1`), so a saturated call cannot outlive one
timeout window even when a stalled writer blocks the enqueue.
Cancellation races inside every wait — resolution never depends on the
outer abort drain. A writer that dies mid-connection (stdout closed, or
a blocking write that only surfaces its error at flush) is
connection-fatal: it cancels all sessions, which resolves any ask
waiting on a reply that can never be written.

### Wire

`request_permission_params` (`crates/buzz-agent/src/wire.rs`) is
version-aware, keyed on the protocol version negotiated at `initialize`
and stored on `App` for the connection lifetime (never derived from a
later mutable session field). v2 nests the tool call under `subject:
{type: "tool_call", toolCall}` with top-level `title`/`options`; v1 uses
the legacy top-level `toolCall`. No hybrid shape. Both offered options
(`allow_once`, `reject_once`) carry `optionId == kind`, so the client's
`kind`-based selection and this side's `optionId`-based predicate agree
without a lookup table.

### Gate

In each spawned tool task (`crates/buzz-agent/src/agent.rs`) the
sequence is: acquire per-turn permit → argument-shape validation →
broker admission + request + wait → cancellation recheck →
`emit_in_progress` → `mcp.call`. Argument-shape validation is hoisted
out of `mcp.rs::do_call` into `validate_arg_shape` so a malformed
non-object argument is rejected locally without prompting for a call
that could never execute.

Authorization is fail-closed, stated once in `evaluate`: execute IFF
`outcome.outcome == "selected"` **and** the selected `optionId` equals
the offered allow option. Every other shape (reject, cancelled, JSON-RPC
error, malformed, unknown outcome, wrong/unknown option, timeout,
wire-channel closure) denies with a synthetic tool error, and the turn
continues.

### Scope

Only LLM-issued MCP calls are gated. The built-in `load_skill` tool and
`call_hooks` lifecycle calls (`_Stop`, `_PostCompact`) are exempt — they
are not model-issued. `readOnlyHint` is never treated as a security
boundary.

First cut ships `allow_once`/`reject_once` only; session-scoped grants
are deliberately out of scope.

### Related issue

Part of #4938. This PR and
[#5106](https://github.com/block/buzz/pull/5106) jointly implement the
feature: #5106 is the client-side policy engine and permission cards;
this PR is buzz-agent's asking side (`session/request_permission`).
Neither closes #4938 alone.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-25 11:49:07 -04:00
Jimmy
e0d7899e17 Merge current main and update Goose GDK
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-08-24 16:04:53 +10:00
Jimmy
2ea414f0b3 test(agent): serialize provider environment
Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-08-21 16:46:18 +10:00
Jimmy
55edc4aef5 fix(agent): preserve migration behavior
Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-08-21 16:39:27 +10:00
Will Pfleger
b728a2af31 feat(llm): stamp thinking effort on call-completed log line (#6424)
The performance-analysis pipeline measures harness × provider × model ×
effort from production logs, but thinking effort was the only dimension
not stamped per call — it had to be attributed by config-changeover
timestamp.

Add `thinking_effort = ?cfg.thinking_effort` to the `llm: call
completed` `tracing::info!` in `Llm::complete()` so every call
self-describes all four dimensions. `?`-formatting preserves the
`None`-vs-`Some(...)` distinction the same way the token fields keep
None-vs-zero.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-20 14:45:03 -04:00
Will Pfleger
2ce8df8533 fix(models): curate Databricks alias-aware labels for 5 missing endpoints (#6360)
Curates Databricks model labels through prefix-stripping aliasing and
materializes exact records for five endpoints missing from the
Databricks catalog.

**Alias-aware label resolution** (Databricks v2)
- Strip workspace-specific prefixes (`goose-`, `team-x-`, etc.) before
exact-record lookup so any prefixed alias resolves to its canonical
label without enumerating every alias variant.
- Return `None` for uncurated ids instead of falling back to a raw
string; callers control the fallback display.

**New exact records — label-only (Anthropic Messages route, axes from
family rules)**
- `databricks-claude-fable-5` → "Claude Fable 5" (materialized from
`anthropic-adaptive-xhigh-fable-5`)
- `databricks-claude-opus-4-8` → "Claude Opus 4.8" (materialized from
`anthropic-adaptive-xhigh-opus-4-8`)
- `databricks-claude-opus-5` → "Claude Opus 5" (materialized from
`anthropic-adaptive-xhigh-opus-5`)
- `databricks-claude-sonnet-5` → "Claude Sonnet 5" (materialized from
`anthropic-adaptive-xhigh-sonnet-5`)

**New capability-bearing record — Kimi K3 (MLflow Chat route)**
- `databricks-kimi-k3` → "Kimi K3" with axes from models.dev Moonshot
catalog: reasoning toggle + effort `[low, high, max]`, no default;
`_reconciliation_doc` records the source and reconciliation policy.

**Corpus and tests**
- 6 new normative-corpus vectors (canonical + one `goose-*` alias per
new id); executable vector count updated to 113 in both Rust and TS
gates.
- Rust and TS label tests extended to cover all 3 new canonical ids and
their aliases via the prefix stripper.
- Sentinel variable fix in discovery-provider test to prevent false
failure in Databricks dev environments where `BUZZ_AGENT_PROVIDER` is
set.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-20 13:46:10 -04:00
Jimmy
913cd56eb3 chore(buzz-agent): update goose to 1.47
Keep the Goose facade and split provider crates on one current upstream revision after reconciling Buzz main.

Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
2026-08-20 22:18:27 +10:00
Michael Neale
b3d77f2b26 test(buzz-agent): cover provider failure paths end-to-end
`stdio_turn.rs` drives the real binary against a real socket, but its
fake provider only ever answers 200. buzz's deleted `llm.rs` tests
covered the unhappy paths; nothing on this branch did.

Five tests against a scriptable provider (429/401/500/truncated stream),
driving the actual binary over stdio. The property under test is the one
buzz owns and a user feels: a misbehaving provider ENDS THE TURN. Silence
is the worst failure available to us — `buzz-acp` waits on that response,
so a turn that never answers is an agent that has visibly stopped
replying with nothing in the log to explain it. Each turn runs under a
deadline so a hang fails as one test rather than stalling the suite.

Deliberately not a port of the 149 `llm.rs` tests: nothing here asserts
backoff timing, retry counts or `Retry-After` parsing. That is goose's
logic to test, and re-asserting it would rebuild the duplication this PR
deletes. What is asserted is observable behaviour through goose —
including that 401 keeps its distinct `-32001` code, which `buzz-acp`
routes on (`acp.rs:118`) to tell a credentials problem apart from a
generic provider failure.

Every test verified falsifiable by mutating the provider, not by
inspection: making the 429 terminal, serving 401 as 500, letting the
"persistent" 500 succeed, and keeping the second turn broken each fail
the intended assertion with a clear message.

That exercise also showed `BUZZ_AGENT_LLM_TIMEOUT_SECS` is load-bearing
rather than test impatience: with the fake provider sleeping on a
half-written stream the turn still ends, and with the timeout removed the
prompt never answers at all. So the projection added in the previous
commit is what rescues a provider that holds a socket open — noted at
the line that depends on it.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-18 12:44:23 +10:00
Michael Neale
678dc68384 chore(buzz-agent): drop the resurrected model_capabilities.rs copy
The merge brought #5597's `buzz-agent/src/model_capabilities.rs` back as
an untracked-by-any-`mod` file. The live copy is in
`buzz-model-catalog`, where I moved it so the desktop can read the
manifest without linking goose; the two were byte-identical.

Nothing declared `mod model_capabilities` in buzz-agent, so it was never
compiled — which is exactly why it survived a green build, a full test
run and clippy. Found by counting files, not by a failure.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-18 12:14:37 +10:00
Michael Neale
16a52827ee docs(buzz-agent): correct stale env/limit rows after the goose move
Swept all 27 `BUZZ_AGENT_*` knobs from the pre-goose config against this
branch, prompted by finding that #6115's mesh timeout reached nothing.
The good news: every knob whose implementation moved or went away is
already documented as such. These rows were not.

- `BUZZ_AGENT_MAX_TOKEN_RECOVERIES` still documented as live with a
  default of `3`. It arrived with #5475, which tuned buzz's own
  truncation-recovery loop — deleted on this branch along with the
  request transport, so goose owns retries and the knob is inert.
- Frame cap listed as a configurable 4 MiB in two places. It is a fixed
  16 MiB protocol limit (`config.rs::MAX_LINE_BYTES`); the env row above
  already said so, the security and limits tables disagreed.
- `BUZZ_AGENT_MAX_HISTORY_BYTES` still had a limits-table row after
  being marked no-longer-read; byte-based eviction is gone.
- Noted the `GOOSE_MAX_TOOL_RESPONSE_SIZE` projection where the limit is
  stated, not only in the env table.

No behaviour change.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-18 12:12:29 +10:00
Michael Neale
e5f73cd27c fix(buzz-agent): carry BUZZ_AGENT_LLM_TIMEOUT_SECS onto goose
PR #6115 seats a 660 s client budget for mesh agents in the desktop
(`managed_agents/relay_mesh.rs`) because MeshLLM's own backend budget is
600 s and a cold prefill of a large agent prompt can legitimately take
~500 s. On this branch that seed reached nothing: buzz-agent no longer
has a request loop, and `project_goose_env` had no projection for the
variable, so goose's own default applied instead.

goose's default is 600 s (`DEFAULT_PROVIDER_TIMEOUT_SECS`) — just *under*
the mesh server's, which is the same wrong-side-of-the-server ordering
#6115 diagnosed. So the bug #6115 fixes would have come back on merge,
silently and only on shared compute.

goose reads the timeout per provider rather than globally, so this
projects onto the variable belonging to the configured provider.
`relay-mesh` maps to goose's `openai` provider and so reads
`OPENAI_TIMEOUT` (`goose/src/providers/openai_def.rs:118`). Providers
goose gives no timeout knob — databricks — return `None` rather than
getting a variable goose never reads.

Two tests. The mesh one asserts the invariant (`> 600`) rather than the
literal, and I verified it fails with the projection removed and the test
kept, reconstructing that baseline from the file rather than `git stash`.

README corrected: it claimed this knob was "no longer read", and its
limits table still listed the pre-goose 240 s default.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-18 12:09:57 +10:00
Michael Neale
9ca88061fa Merge origin/main into micn/buzz-agent-goose-core
82 commits of main, plus the goose pin moved from bf332b9 to 7c4ba22
(60 commits) because main's changes and goose's API changes overlap.

Conflict resolutions worth knowing about:

* `llm.rs`, `handoff.rs`, `tests/regressions.rs` — main changed files this
  branch deletes. Reviewed each change rather than dropping it silently:
  #5475 raised `BUZZ_AGENT_MAX_OUTPUT_TOKENS` and the truncation-recovery
  allowance in buzz's own request loop, which no longer exists; goose owns
  retries and output limits now, so there is nothing to port.

* `model_capabilities.rs` (#5597) moved to `buzz-model-catalog`, not kept in
  `buzz-agent`. The manifest is read by the desktop model picker, which
  cannot link goose (`libsqlite3-sys` collision), so it has to live in the
  crate the desktop already depends on. `ThinkingEffort` moved with it,
  reduced to the vocabulary the manifest is typed in — the request-path
  mapping went with buzz's HTTP transport. The 103-vector corpus guard
  passes in its new home; `just ci` and `regen-model-corpus` now point at
  it, and `run-tests.sh` runs the new crate's lib tests.

* Curated model labels reach the picker. main added them to the Databricks
  discovery path; on this branch `session/new` enumerates through goose's
  provider API instead, so the label lookup had to be applied there or
  #5597 would have been silently reverted for every provider.

goose API changes this bump required:

* `StateMachine`/`Step`/`Operation` are generic over session and effect
  type, and the loop moved into a new `goose-agent` crate.
* `StateEffect` split: buzz now names `ConversationEffect`, the narrow set,
  rather than goose's `GooseEffect`. `ReplaceConversation` lost its `usage`
  field — buzz always passed `None`, and the driving loop already resets
  the running total, so this is a rename not a behaviour change.

78 unit + 22 integration tests pass, clippy `-D warnings` and fmt clean on
the pinned 1.95.0 toolchain.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-18 11:43:30 +10:00
Will Pfleger
1b7e5ac1be feat(model-capabilities): drive model capabilities and labels from one manifest (#5597)
## Summary

Centralizes model capability knowledge — thinking mode, supported effort
levels, wire routes, and human-readable labels — into a single manifest,
`scripts/model-capabilities.json`. Rust and TypeScript each get a small
interpreter that reads the same manifest, replacing hand-maintained
tables scattered across both languages that had already drifted apart. A
capability change is now a data edit, not parallel edits to two code
paths. Supersedes the codegen approach explored in #3603.

A cross-language contract keeps the two interpreters honest:
`scripts/normative-corpus.json` is a golden snapshot generated from the
Rust resolver (103 vectors covering all six capability axes) and
replayed natively in TS. CI fails if either language disagrees with the
corpus or the corpus drifts from the resolver. Regenerate with `just
regen-model-corpus`.

## Behavior changes

- **Effort dropdown for `openai-compat` providers** no longer offers
`max`. The request path always clamped `max` to `xhigh` on the wire, so
the UI stops offering a value that was silently rewritten. UI-only,
wire-identical.
- **Databricks v2 routing (wire-visible):** uncurated endpoint names
carrying a bare Claude code-name segment (e.g. `goose-opus-5`) now route
to the MLflow chat wire instead of Anthropic Messages — they lose
Anthropic prompt caching but still succeed on a valid OpenAI-compatible
wire. Curated `databricks-claude-*` records and any name starting with
`claude` are unchanged. A handful of other uncurated/adversarial name
shapes similarly fall back to MLflow chat instead of pattern-matched
routes; every curated model resolves identically to before, all axes.
- **Curated model labels on the real discovery path.** The Databricks
API returns no display name, so discovery emits the raw endpoint id as
the model `name` (`{id, name: id}`) on every path. `ModelEntry.name` is
now curated at all four construction seams in `buzz-agent` — v2
discovery, v1 parse, the auth-empty default catalog, and the
configured-model fallback — via a read-only `databricks_registry_label`
lookup over the manifest's `databricks_v2` exact records; `id` stays the
raw wire/config value. A known id renders its curated label
(`databricks-gpt-5-5` → `GPT-5.5`), an unknown id passes through
unchanged, and the default-catalog row reads `GPT-5.5 (default
catalog)`. As a defense against older `buzz-agent` binaries and any
harness that echoes ids, `resolveModelLabel` treats a discovered name
equal to the trimmed id as absent and falls through to the registry
tier; a genuinely distinct name (including the suffixed default-catalog
label) still wins.

## Cleanup

Deletes the duplicated capability tables and their tests: the
`config.rs` gpt5 matchers, effort tables, and clamp logic; the legacy
segment-based Databricks v2 route classifier in `llm.rs`; and the TS
hand tables plus `effortTable.fixture.json`. All are replaced by
manifest lookups through the shared resolver — no line of capability
data exists in two places.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-17 11:23:28 -04:00
Atish Patel
72d56e7bd3 fix(agent): raise output limit and allow 3 recoveries (#5475)
## Summary

Raise the built-in output and recovery defaults so long-running agents
have more room to finish useful work instead of terminating after
repeated 32,768-token reasoning-only responses.

- Raise `BUZZ_AGENT_MAX_OUTPUT_TOKENS` from 32,768 to 65,536
- Raise the finite output-truncation recovery allowance from 2 to 3 via
`BUZZ_AGENT_MAX_TOKEN_RECOVERIES`; `0` still disables recovery
- Strengthen the recovery prompt so the model stops prolonged reasoning,
uses tools immediately, and builds scripts or artifacts in small
verifiable steps
- Preserve the safety invariant that incomplete truncated tool calls are
discarded and never executed
- Keep proactive handoff independently at 90% of
`BUZZ_AGENT_MAX_CONTEXT_TOKENS` (180,000 tokens with the 200,000
default), regardless of the output allowance
- Add request-loop and configuration regressions for exact-N recovery,
disabled recovery, successful tool-first recovery, discarded truncated
calls, and finite round bounds

`BUZZ_AGENT_MAX_OUTPUT_TOKENS` remains an explicit per-agent deployment
setting. Operators should configure it at or below the served model's
output limit; this PR does not perform live provider capability
discovery or automatic clamping.

**Risk:** Medium — this increases the default request size and permits
one additional recovery attempt by default. Recovery remains finite and
bounded by `BUZZ_AGENT_MAX_ROUNDS`. Deployments whose served model
rejects 65,536 output tokens must set a lower per-agent value.

Current output limits
- model - output token max
- DeepSeek V4 Flash - 384,000 tokens
- Qwen 3.8 (Max) - 131,072 tokens
- GLM 5.2 - 131,072 tokens
- GPT 5.6 - 128,000 tokens
- Claude Opus 5 - 128,000 tokens
- Gemini 3.6 Flash - 65,536 tokens
- Kimi K3 (Moonshot)- 131,072 tokens

### Related issue

None found. Originating benchmark analysis:
`buzz://message?channel=c3252dd2-0142-4e01-88c7-a2183c3960a5&id=91e991aab5fd49094583c3937477f6c12db57a41d86edf7fd4745d0d57d10017`

### Testing

- `cargo fmt --all -- --check`
- `cargo test -p buzz-agent` — 595 passed, 0 failed, 0 ignored at
`bd6de557b367850f50325bafdd3c046131942bef`
- `cargo clippy -p buzz-agent --all-targets -- -D warnings`
- Previously failing
`cancelled_turn_with_usage_emits_notification_before_response` passed
alone and in the full rerun
- Push hooks passed: organization guard, branch skew, Rust tests, and
Desktop Tauri checks

### Update — 2026-08-11

Per review feedback, the recovery default is 3. The OpenRouter live
`/models` output-cap discovery, cache, request clamp, and related
tests/documentation were removed. Per-agent output configuration is now
the sole output-cap mechanism. Proactive handoff and its pre-usage byte
fallback now depend only on 90% of `BUZZ_AGENT_MAX_CONTEXT_TOKENS`; with
the 200,000 default, the handoff threshold is 180,000 regardless of
`BUZZ_AGENT_MAX_OUTPUT_TOKENS`.

Generated with Brainy Bumble


### Targeted validation — 2026-08-11

Ran the exact PR binary once on each of the 11 benchmark tasks causally
affected by the previous 32,768-token ceiling, using OpenRouter with
`deepseek/deepseek-v4-flash-0731` pinned to Fireworks and maximum
reasoning effort. Relay-429 collection failures were excluded and rerun
at concurrency 2.

- **6/11 passed:** `circuit-fibsqrt`, `feal-linear-cryptanalysis`,
`model-extraction-relu-logits`, `path-tracing`,
`schemelike-metacircular-eval`, and `sqlite-db-truncate`
- **5/11 reached the benchmark deadline:** `adaptive-rejection-sampler`,
`dna-assembly`, `path-tracing-reverse`, `regex-chess`, and
`write-compressor`
- `regex-chess` reached exactly 65,536 output tokens, triggered one
output-limit recovery, and then reached the deadline. This directly
confirms that the larger ceiling and recovery path were active, but not
that recovery guarantees completion.

For context, ten of these tasks were 0/5 in the historical baseline;
`sqlite-db-truncate`, the clean control, was 4/5. This is targeted
one-attempt-per-task validation rather than a statistically powered
comparison. The result should not be attributed solely to the recovery
default of 3: this PR also raises the output ceiling and strengthens
recovery behavior, and OpenRouter routing conditions may differ from the
historical direct-Fireworks runs.

Generated with Brainy Bumble

---------

Signed-off-by: Atish Patel <atish@squareup.com>
Signed-off-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz>
Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
Co-authored-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz>
Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
2026-08-12 16:57:11 -04:00
Michael Neale
0ea48f44ea test(buzz-agent): prove turn-to-turn memory across separate prompts
Mic asked whether the tests really cover multi-turn recall. They did
not, and I had claimed the gap could only be closed by hand testing.
That was wrong on both counts.

Every existing integration test sends a single session/prompt. All of
them would still pass if history were dropped between prompts and each
turn started from nothing -- which is exactly the risk this branch
introduces, since conversation history now lives in TurnState rather
than goose's sqlite store.

memory.rs sends two prompts on one session and asserts on what the
PROVIDER receives in the second request: turn 1's user message and turn
1's assistant reply must both be present. Asserting on the second answer
instead would pass against a model that ignored history entirely.

Verified against the real regression: deleting the line that carries
the conversation forward (s.history = conversation) in session_prompt fails the test with 'history is not carried across
prompts'. Nothing else in the suite notices that deletion.

Also fixed a flaw in my own first draft: the fake provider varied its
reply depending on whether the request mentioned the secret word. Turn
1's own user message mentions it, so both turns got the same reply and
the assistant-reply assertion proved nothing. It now returns a fixed
distinctive string.

79 unit + 22 integration green.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 16:22:10 +10:00
Michael Neale
e0d5f20475 test(buzz-agent): cover the steer and compaction operations
The two operations added in 099bacbe had no direct unit tests -- they
were covered only indirectly by the integration suite. Four tests, each
pinning a property where the failure would be silent:

- a steer drains rather than peeks (a peek repeats the instruction to
  the model every round)
- an empty queue is NotApplicable, not an empty Applied (the loop treats
  applied-but-unchanged as a reason to look again, so an always-applying
  operation spins it)
- only effects that change state count as progress, including effects
  this loop deliberately ignores
- compaction clears the running token total (a stale total describes the
  pre-compaction conversation and would re-trigger compaction at once)

Verified by mutation rather than assumed: removing the token reset fails
the compaction test, and making drain() peek fails the steer test.

78 unit + 21 integration green.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 16:15:04 +10:00
Michael Neale
60e63a065d docs(buzz-agent): record why [Reflect] stays out of the state machine
Reflect was step 3 of the migration plan. It should not be an Operation,
and the reason is worth writing down rather than leaving as an omission.

StateEffect can append a message or patch a tool request's metadata, but
nothing edits tool result *content*. PatchToolRequestMeta is applied via
SessionManager -- the store this branch deliberately does not write to --
and patches metadata, not what the model reads.

So as an operation the reflection would arrive as a separate message
after the result rather than inside it. That is exactly the arrangement
this port moved away from when it stopped delivering reflections as
steers. Structure follows behaviour.

Also corrected reflect.rs's module comment, which still described the
steer-based delivery an intermediate commit used.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 15:33:59 +10:00
Michael Neale
099bacbe6a refactor(buzz-agent): express steer and compaction as goose Operations
Steps 4 and 5 of PLANS/BUZZ_OPERATIONS_MIGRATION.md. Behaviour is
unchanged: steer.rs, cancel.rs and the real-MCP integration tests all
pass without modification.

BuzzSteerOperation drains the steer queue; BuzzCompactionOperation wraps
goose's check_if_compaction_needed/compact_messages plus the _PostCompact
re-injection that buzz's old context handoff existed for.

These need a second StateMachine (round_start), not more steps on the
existing one: round_gate also runs when a turn wants to END, and draining
steers or compacting there would change when they happen. Since step()
stops at the first operation that applies, the start machine runs to
exhaustion -- a turn that both steers and compacts needs two passes.

The gate now carries StateEffect rather than Message, because compaction
replaces the conversation instead of appending to it. apply_effects
reports whether state actually changed, which is what stops a
no-op-applied operation from spinning the loop.

Not goose's CompactionOperation: it yields to the client and emits a
user-facing notification. In buzz a yield ends the turn, and the
notification would post 'compacting' into the channel -- so a long
conversation would stop mid-work and narrate its own housekeeping.

Nearly dropped the [PostCompact] prefix on the re-injected state while
moving it. Caught by re-reading the old function before deleting it; the
model uses that prefix to tell re-injected state from a user turn.

74 unit + 21 integration green, handtest --all 18/18.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 15:32:05 +10:00
Michael Neale
ab5b289488 fix(buzz-agent): restore the env surface main had
The port dropped BUZZ_AGENT_* knobs whose implementation moved to goose.
That conflated two different things: goose owning the mechanism, and the
setting silently ceasing to work. Someone who set
BUZZ_AGENT_TOOL_TIMEOUT_SECS=1200 got no error and a 300s timeout.

Restored as mappings onto goose, defaults matching main:

- TOOL_TIMEOUT_SECS   -> GOOSE_DEFAULT_EXTENSION_TIMEOUT
- MAX_TOOL_RESULT_TEXT_BYTES -> GOOSE_MAX_TOOL_RESPONSE_SIZE
- NO_HINTS            -> CONTEXT_FILE_NAMES=[]
- STOP_MAX_REJECTIONS -> the veto cap, configurable again (0 disables)

Both timeout and truncation project buzz's *default* as well as an
explicit value, because goose's differ (300s vs 660s, 200KB vs 50KB) --
mapping only explicit values would still have changed behaviour for
everyone who never set them.

Two silent regressions fixed:

- MAX_SESSIONS defaulted to 8; main was unlimited. A busy agent would
  have started refusing sessions it used to accept.
- The reply guard was gone entirely. Desktop still enables it by default
  for shared-compute agents (relay_mesh.rs), so the flag was set and
  ignored. Restored as BuzzReplyGuardOperation with main's nag text,
  two-reminder budget, and publish-shaped-call latch.

README: every removed knob is now listed as 'No longer read' with what
replaced it, rather than documented as if still live.

74 unit + 21 integration green, handtest --all 18/18.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 15:18:27 +10:00
Michael Neale
15f6fd57f8 refactor(buzz-agent): express the _Stop veto as a goose Operation
Step 2 of PLANS/BUZZ_OPERATIONS_MIGRATION.md. Behaviour is unchanged:
the existing stop_hook and real_dev_mcp integration tests pass without
modification, which is the point of this step.

BuzzStopVetoOperation replaces the inline stop_veto() branch. The loop
now runs the gate a second time when a round wants to end, which is
where the operation applies -- it declines unless the last message ends
the turn, so the top-of-round call is a no-op for it.

Two things move rather than change:

- The block cap was a loop-local u32 in run_turn; objections now carry
  a metadata note and the cap counts them since kickoff. run_turn is
  per session/prompt, so both are turn-scoped -- same three-strikes
  behaviour, but now a pure function of the conversation, which is why
  goose tags its own denials the same way.
- MAX_STOP_BLOCKS was declared in both loop_drive and hooks with the
  same value. Dropped the loop_drive copy.

Deliberately not goose's StopHookOperation: it drives goose's private
HookManager plugin system, while buzz's hook is an MCP tool that can
see buzz-dev-mcp's in-process todo state. It also emits a user-facing
notification per denial, which in buzz would post 'stop hook blocked
ending this turn' into a channel every time an agent had an open todo.
The objection reaches the model, not the room.

65 unit + 21 integration green, handtest --all 18/18.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 14:06:03 +10:00
Michael Neale
42fb755040 fix(buzz-agent): the agent's configured provider must beat an inherited one
An agent configured for OpenAI sent its OpenAI model to Anthropic and
404'd every turn (llm model not found: gpt-5.6-sol at
api.anthropic.com). Found in live testing, not in a test.

project_goose_env used set_if_absent, so an inherited GOOSE_PROVIDER
won over the agent's own BUZZ_AGENT_PROVIDER. That looks defensive but
inverts the intended precedence: BUZZ_AGENT_PROVIDER/MODEL are not
ambient config, they are derived by the desktop from the agent record's
structured provider/model fields at spawn time, and the desktop
deliberately refuses to persist GOOSE_PROVIDER/GOOSE_MODEL in an
agent's env so they cannot shadow those fields
(env_vars.rs:DERIVED_PROVIDER_MODEL_ENV_KEYS). The subprocess still
inherits the desktop's environment, though, and anyone with goose
installed exports GOOSE_PROVIDER from their login shell.

The failure is silent in the worst way: the agent's settings read
correctly in the UI while its traffic goes to another provider.

Set both unconditionally when the agent has them, and likewise map
OPENAI_COMPAT_API_KEY over OPENAI_API_KEY: an inherited OPENAI_API_KEY
would otherwise authenticate and bill the agent as whoever that key
belongs to. With no BUZZ_AGENT_* value there is nothing to override
with, so an ambient GOOSE_* is still honoured.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 12:57:32 +10:00
Michael Neale
69c6091216 feat(buzz-agent): back goose's Databricks provider with buzz's PKCE engine
Both sides ran OAuth PKCE against the same Databricks host with separate
token caches: goose's providers::oauth for inference, buzz's
PkceOAuthTokenSource for the desktop model picker. A user could be
authenticated for one and not the other and face two browser flows for
one host.

Buzz's engine is the hardened one (#5534: O_NOFOLLOW, 0600 from
creation, unique temp names) and the only one the desktop can link, so
it wins. goose supports this without a fork: DatabricksV2Provider::new
takes an oauth token provider closure, and databricks_v2_def::from_env
is just the variant passing goose's own. Retries, the agent-session-id
header, endpoint resolution and the wire format all stay goose's.

Uses bearer_no_browser: inference runs in a headless subprocess where a
browser step would hang unseen. Interactive login stays in buzz-agent
auth databricks and the desktop picker. A static DATABRICKS_TOKEN still
takes precedence, as in goose's own from_env, and no DATABRICKS_HOST
falls through to goose's registry unchanged.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 10:44:46 +10:00
Michael Neale
8e1ba34207 refactor(buzz-agent): use goose's skills instead of buzz's duplicate
buzz-agent grew its own skill discovery, prompt index and load_skill tool
before goose had one. goose now ships goose::skills plus a SkillsClient
registered in PLATFORM_EXTENSIONS, so buzz's version was a strictly
smaller duplicate: the same three project directories, but goose also
reads the global directories, plugin-installed skills and its own
builtins, and supports args templating buzz never had.

Delete builtin.rs, builtin_client.rs and hints.rs (1,204 lines) and
register goose's skills extension by name so its factory runs. goose
declares it unprefixed_tools, so the model now sees load_skill rather
than buzz__load_skill.

skills.rs keeps only the prompt index, because the goose code that
renders it lives on SkillOperation, which is pub(super). The hint-file
guard from hints.rs moves there rather than being dropped, and now
matches filename literals instead of the bare words so it can document
itself.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 10:39:22 +10:00
Michael Neale
3190102efb merge origin/main into micn/buzz-agent-goose-core-v2
Conflicts, and how each was resolved:

* crates/buzz-agent/src/mesh.rs + tests/mesh_auto.rs — DELETED. main's
  #5289 removed buzz-agent's client-side mesh catalog probe on purpose:
  MeshLLM v0.75 decides per request, the desktop resolves the wire model
  before spawn (relay_mesh_wire_model), and BUZZ_AGENT_PREFER_MESH_FOR_AUTO
  is no longer set by anything. Keeping my MeshAutoProvider would have
  resurrected policy main deliberately retired.
* crates/buzz-agent/src/llm.rs — stays deleted; goose owns the transport.
  main's changes to it were the Databricks OAuth hardening (#5534), which
  applies to auth.rs, now living in buzz-model-catalog.
* crates/buzz-agent/src/config.rs — ours; main's only change was dropping
  prefer_mesh_for_auto, which this branch had already dropped.
* buzz-model-catalog: picked up #5534's O_NOFOLLOW/0600 token-cache
  hardening, which needs the nix dep the extraction had left behind.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-12 09:10:52 +10:00
Mic Neale
240cdd3ea1 chore: mesh upgrade, clean up legacy special case code, simplify model selection for mesh (#5289)
Shared compute now has exactly two model choices: MeshLLM's virtual
`mesh`
model, or a model you name. Buzz picks between them in one place, and
buzz-agent no longer knows meshes exist.

## What changed

- **MeshLLM v0.74.0 → v0.75.1.** v0.75.0 added
`degrade_to_single_model`, so a
  `model=mesh` request is answered by one served model when there is no
committee to form, instead of failing. v0.75.1 adds Mesh-LLM#1196, which
skips stale pre-0.75 runtime cache entries rather than aborting startup
on
them — without it, anyone who had run mesh on 0.73/0.74 could not start.
- **Deleted the client-side mesh catalog probe.** buzz-agent used to
poll
`/v1/models` (5s TTL, 30s cooldown, two-observation debounce) to decide
whether `mesh` was safe to send. MeshLLM now decides per request, so the
  polling, its hysteresis, and its 503 fallback are gone.
- **One mapping point.** `relay_mesh_wire_model()` turns the stored
value into
a wire name: `auto` becomes `mesh`, a named model passes through. The
spawn
env, the ACP harness, and the readiness probe all use it, so they cannot
disagree — previously `BUZZ_ACP_MODEL` and the probe both said `auto`, a
name
  the mesh does not advertise.
- **Removed the `nostr-relay-pool` advisory exception.** #5404 allowed
RUSTSEC-2026-0243 "after mesh-llm migrates to nostr-sdk >= 0.45".
v0.75.1
does, so the retired crate is gone from both lockfiles and the exception
  would only mask a future advisory for it.
- **Deleted `scripts/ensure-mesh-native-runtime.sh`** and its six
justfile call
sites. It built llama.cpp from source into the runtime cache; the app
already
  downloads the signed release runtime itself, and CI never called it.

## Why it is better

**−639 lines of Rust.** Availability is decided by the node that knows
the
answer, per request, instead of by a client cache that could be stale
for up to
30 seconds. A second worker joining now takes effect on the next request
rather
than after two confirming probes.

## Behaviour change

A 503 on an explicit `mesh` request takes the ordinary transport retry
under
the same model instead of failing over to a second one — there is no
second
model to fail over to now. MoA repairs partial committee results
internally
before it reaches that point.

## Validation

`crates/buzz-relay/examples/mesh_agent_e2e.rs` now sends `mesh` where it
previously sent `auto` or the physical model id, so no leg was covering
what
Buzz actually puts on the wire. 4/4 on gemma-4-E4B, gemma-4-26B-A4B, and
Qwen3-8B — including a real ACP tool call through `mesh` into
buzz-dev-mcp,
asserted by reading the written file back off disk.

Hand-tested in the desktop app on both gemma-4 sizes: picked Auto, agent
logged
`model_id=mesh`, replied in channel.

## Not covered

A committee that forms and then loses a worker returns 502, and that
needs two
workers to reproduce — not testable on one machine.

---------

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-11 11:40:07 -04:00
Will Pfleger
5e4d0fe925 fix(buzz-agent): harden Databricks OAuth token cache and callback (#5534)
Hardens the Databricks PKCE OAuth code in
`crates/buzz-agent/src/auth.rs`. Two fixes.

## Token cache is owner-only across its whole lifecycle, and race-safe

The PKCE cache holds both the access and refresh tokens, but `save()`
wrote it with a bare `fs::write` + `fs::rename`. Under a `022` umask the
file landed world-readable, and the fixed `*.json.tmp` temp name races
across concurrent savers sharing `$HOME` — one writer's `rename` can
fail on another's half-written temp.

**On write**, `write_private_cache()` creates a temp file with
owner-only permissions from the moment it exists — mode `0o600` on Unix
via `OpenOptions::mode` — writes and fsyncs it, then renames over the
destination. The rename swaps the inode wholesale, so a pre-existing
cache file with loose permissions is *replaced* by the new private inode
rather than inheriting its mode. `unique_suffix()` (getrandom, timestamp
fallback) gives each write a distinct temp name, and a drop guard
removes the temp on any failure path.

**On load**, owner-only is enforced as a cache lifecycle invariant, not
just a write-path property. A world-readable cache left by an older
buzz-agent was previously read straight into memory and returned on the
fresh cache-hit path without ever invoking `save()`, so a token file
with no advertised expiry could stay exposed indefinitely.
`read_cache()` now funnels every load — initial and cross-process
re-reads — through `read_private_cache()`, which on Unix opens with
`O_NOFOLLOW` (kernel-level symlink refusal, no stat/open TOCTOU),
requires a regular file, and `fchmod`s the pinned handle to `0o600` when
any group/other bit is set. A cache that cannot be secured is treated as
absent, so callers fail closed to a fresh flow rather than trusting an
exposed file.

## OAuth callback no longer reflects untrusted input

The localhost callback embedded the untrusted `error` query param
straight into the HTML response — an XSS sink on the redirect page — and
routed that same raw value into the error string that reaches the logs.

`callback_outcome()` is now a pure function returning `(result,
static_page)`: the browser always sees a fixed literal page that embeds
no request parameter, and failure detail travels only through the result
channel. `sanitize_callback_detail()` strips control characters (CR/LF
log-line injection) and caps length before that detail enters the error
string bound for the logs.

## Deferred: Windows owner-only ACLs

Windows owner-only protection is out of scope for this change. The
goose-parity route (`CreateFileW` with an owner-only SDDL
`D:P(A;;FA;;;OW)`) requires `unsafe` FFI, which this crate's
`#![forbid(unsafe_code)]` prohibits; reconciling that conflict is a
separate decision. Both platform seams — `create_private_temp_file`
(write) and `read_private_cache` (load) — have a `#[cfg(not(unix))]`
branch that relies on the default per-user ACLs and is the drop-in point
if Windows protection is added later. No new dependency and no `unsafe`
are introduced here.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-08-11 09:58:15 -04:00
Michael Neale
45584a9c7a refactor(buzz-agent): hold the provider on buzz's side, not in goose's session row
I claimed the remaining session row was needed because update_provider
persists the model config there and goose reads it back for the context
limit. That was wrong, and the fix is straightforward: buzz builds the
provider and model config itself, so it can hold them.

New crate::model::SessionModel owns the pair. The turn reads the provider
and config from it instead of Agent::provider and
model_config_for_session, and session/set_model swaps it there rather
than calling update_provider. That removes the last write buzz makes to
goose's store.

The row itself still has to exist, but for a different reason than I
gave. Agent::add_extension_inner (goose agent.rs:1465) resolves each MCP
extension's working directory by loading the session, and fails setup if
the row is missing -- 'Failed to get session: Session not found'. I found
that by removing create_session and watching every tool-using test fail.
So: one row per session, created for extension setup, and zero writes
after it. Removing it needs goose to take the working directory as an
argument instead of reading it back.

Verified: a session with an MCP server, a tool call and two turns leaves
1 sessions row and 0 messages rows, still recalls turn 1 in turn 2, and
still advertises 9 catalog models. handtest.py --all 18/18; 87 unit + 21
integration green.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-11 21:32:00 +10:00