Skip to content

Forward Claude Code and goose inference to the agent gateway natively - #2916

Open
philazar wants to merge 4 commits into
coder3/dxa-1154-pi-workforcefrom
coder1/dxa-1156-host-forwarding
Open

philazar wants to merge 4 commits into
coder3/dxa-1154-pi-workforcefrom
coder1/dxa-1156-host-forwarding

Conversation

@philazar

@philazar philazar commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Why

Buzz can send a local agent's turn through an agent gateway that a build configures (#2632). Claude Code and goose make their own model calls, so the app needs a native way to send those calls to the gateway with the turn's context headers. It must not take over the harness's credentials, request bodies or streaming.

What

  • A native loopback transport forwards a local Claude Code harness's model traffic to the build's agent gateway.
  • The same transport forwards goose inference, tied to the goose session it serves.
  • Typed frontend bindings cover the host commands, including associateForward for goose.

How

  • Each turn opens a forwarding generation. The page's frozen context headers are added natively. The harness keeps its own credentials, bodies, streaming, retries and cancellation.
  • Admission follows the page's route revisions. Page reloads and shutdown stop every generation. Only compiled routes are forwarded, and redirects are never followed.
  • The gateway origin, header names, reserved prefixes and per-harness upstream bases come only from the version 2 BUZZ_AGENT_GATEWAY build descriptor. One validator in plugin-manager serves build.rs and the app. It refuses an invalid descriptor without echoing it. Unset means off.
  • A goose generation opens for discovery without a session. It is tied to the session goose reports, once, before the first prompt. Inference must then name that session in agent-session-id. Only goose's serving-endpoints discovery and invocation routes are forwarded.
  • The last commit merges the Pi adapter branch (feat(pi): run gateway-eligible Pi agents through the configured model gateway #2479). The merge had no conflicts.

Risk

Medium in code size (about 3,600 added lines, mostly Rust), low in effect today. No caller turns on forwarding in this PR, and an unset descriptor means off. The Claude and goose callers come in the PRs stacked on this one.

Testing

Not run yet at this head:

  • The full native Rust suite. CI runs it.
  • Native app acceptance with a live gateway. No live run is approved yet.

Bigger picture

Review order: #2632 (shared gateway client) → #2479 (Pi) → this PR (native forwarding) → Claude caller → goose caller. Each PR's diff shows only its own layer. The stack conflicts with current main and needs a main integration before merge. Ready for review does not mean ready to merge.

🤖 Generated with Claude Code

philazar and others added 3 commits November 5, 2026 16:37
Add a native loopback transport that forwards a local Claude Code
harness's model traffic to the build's agent gateway, composing the
page's frozen context headers in natively. The harness keeps its own
credentials, bodies, streaming, retries and cancellation. Admission
follows the page's route revisions, page reloads and shutdown stop every
generation, only compiled routes are forwarded, and redirects are never
followed. Typed frontend bindings cover the four host commands.

The gateway's origin, context header names, reserved prefixes and
per-capability upstream bases come only from the version 2
BUZZ_AGENT_GATEWAY build descriptor. One validator in plugin-manager
serves build.rs, which refuses an invalid descriptor without echoing it
and compiles its canonical form, and the app, which parses that form
again at startup and keeps only what native forwarding uses. Unset means
off. The validator runs the frontend's canonical descriptor vectors.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Philip Azar <[email protected]>
Extend the native transport to goose. A goose generation opens for
discovery without a session and is associated with the session goose
reports, once, before its first prompt; inference must then name that
session in `agent-session-id`. Only goose's serving-endpoints discovery
and invocation routes are forwarded, with plain endpoint names. Its
upstream base comes from the descriptor's `goose` capability, and a build
that configures none leaves goose unforwarded while Claude Code may still
forward. Typed bindings add `associateForward`.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Philip Azar <[email protected]>
No conflicts.

Co-authored-by: Coder1 <db50ef1f1688dc7f3e536c17351e8e994a7466bcbe009794f61d64355c0ffc2e@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Philip Azar <[email protected]>
@philazar
philazar requested a review from a team as a code owner November 6, 2026 23:16
baxen added a commit that referenced this pull request Nov 7, 2026
## Summary

These are fixes for three browser-journey flakes that are failing open
PRs which don't touch the code under test. The change is test-only. No
browser cases are added or removed, and no assertion is relaxed.

The red `Browser journeys (chromium, 3/9)` / `(webkit, 4/14)` shards on
most open PRs come from a different problem:
`sidebar-geometry.spec.mjs:224` is broken on main. #2945 and #2946
already fix it, so this PR leaves it alone.

### 1. `channel-completion.spec.mjs`: cold dev-server load timed out

**Evidence:** `Flake check` on #2935 failed
`channel-completion.spec.mjs:7` on both engines with `page.goto: Timeout
15000ms exceeded` on
`/tests/fixtures/mentions.html?channels&stream&test-controls`
([chromium](https://github.com/block/buzz-app/actions/runs/42317084302/job/128232679087),
[webkit](https://github.com/block/buzz-app/actions/runs/42317084302/job/128232678918)).
#2935 doesn't touch that page.

**Cause:** this is the same cold load #2776 fixed for
`mentions.spec.mjs`. Each test here started its own Vite dev server for
the same ~1,600-module fixture.

**Fix:**
- `beforeAll` builds `mentions.html` once and serves it with
`build`/`preview`, using the same config as `mentions.spec.mjs`.
- The build keeps the dev server's CSS. Tailwind's optimizer and Vite's
CSS minifier each round the 20/14 line-height token to `1.42857`, so
line boxes become `19.99998px`. In WebKit that put the popup `4.03125px`
above the composer, and the test asserts exactly `4`. I confirmed this
locally: it fails 5/5 with either setting left on and passes with both
off. Turning both off keeps the geometry the test checked before.

### 2. `profiles.spec.mjs:940`: reopening Members right after closing
the profile

**Evidence:** `Flake check (webkit, 8/9)` on #2909 failed in repeat 8 of
10
([job](https://github.com/block/buzz-app/actions/runs/42313241314/job/128219877861)).
The trace shows this sequence:
1. `Escape` closes the profile panel.
2. Focus returns to "Manage members of One" within 40ms.
3. `Enter` is pressed 10ms later.
4. The DOM after that `Enter` shows the Profile panel again and no
Members dialog, so the next `row.click` times out.

**Fix:** after each `Escape`, wait for the address to leave the profile
(no `"detail"` in the `#buzz=` hash) before reopening Members. This is a
lifecycle barrier, not a delay. One step in this test already waited for
the panel to unmount before reopening; the other steps didn't wait at
all.

**Product follow-up (not fixed here):** the order of a dismissal's
history return and a quick reopen matters.
- If I delay `history.go` by 300ms, the unpatched test takes a different
path: the dismissal's `go(-1)` never runs, and the URL stays on the
profile visit under the reopened dialog.
- A `browser-history` unit sketch (deferred traversal: `push(channel)`,
`push(panel)`, `dismiss([panel])`, then `push(settings)` before the
traversal commits) lands on `channel`. The new `settings` visit is
undone.

Someone who owns navigation should decide whether a push during a
pending dismissal return should be deferred or replayed. That is a
navigation-history change, not a test fix.

### 3. `timeline.mjs` `edge()`: one wheel gesture stopped short

**Evidence:** `Browser journeys (webkit, 6/14)` on #2937 failed
`history-prefix.spec.mjs:37` with `wheel reaches timeline edge`,
`Received: 199`
([job](https://github.com/block/buzz-app/actions/runs/42320781857/job/128244819852)).
The trace shows the one `-3251px` wheel ended with `scrollend` and the
geometry then held still, so the poll could never pass.

**Fix:** `edge()` repeats the gesture, at most four times, until the
edge is reached. Every extra gesture must reduce the distance, so an
immobile timeline still fails. `upper()` already follows this rule. The
case of already being at the edge is unchanged: it still sends one small
wheel so production can start paging.

## Not in this PR, from the same survey of the 40 most recently updated
open PRs

- `sidebar-geometry.spec.mjs:224` (Drafts & sent row) is broken on main
since #2733 + #2728 crossed. #2945 is green and #2946 is in CI; either
one fixes it.
- `solo-chats.spec.mjs:322` (Share, webkit, on #2927) is being handled
in draft #2933.
- `layout.spec.mjs:803` on #2916 is already fixed on main by #2750.
#2916's merge ref predates it.
- #2911 (thread virtualization) and #2918 (per-channel notifications)
fail consistently in tests their own changes reach, so those look like
regressions for their authors, not flakes.

## Validation

All runs were local on macOS with
`tests/browser/playwright.ci.config.mjs` and `--no-deps`. These are not
hosted CI measurements.

- `channel-completion.spec.mjs`, `--repeat-each=5`, both engines: 20/20
passed with the change. Before the CSS settings were added, the built
fixture failed WebKit 5/5 (`4.03125`).
- `profiles.spec.mjs:940`, `--repeat-each=24 --workers=6`, WebKit:
passed. It also passed with `history.go/back/forward` delayed by 300ms,
in both engines.
- `history-prefix`, `history-loading`, `scroll`, `timeline-setup` and
`new-message-boundary` (all callers of `edge()`/`end()`), both engines:
70/70 passed.
- Full `history-prefix`, `channel-completion` and `profiles` files, both
engines: 43/44 passed. The one failure was `profiles.spec.mjs:6` in
WebKit, a test this PR doesn't touch. It then passed 4/4 both with and
without this change.

**Deferred and remaining gaps:**
- I couldn't reproduce the cold-load timeout or the WebKit stopping
short locally, so I have no local fail-before for those two. The
evidence is the CI traces linked above.
- The other `edge()` callers (24 spec files) are left to CI.
- A green run doesn't prove these flakes are gone.

Signed-off-by: Bradley Axen <[email protected]>
Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
@philazar

Copy link
Copy Markdown
Contributor Author

🤖 Agent review summary (sq agents review panel at ea2e2f0, checked by hand against host_forward.rs)

No blockers. The Behavior & Contracts specialist raised one finding. The Reliability and Security specialists raised none.

1. Revoked generations stay in the registry until the caller closes them — src-tauri/src/host_forward.rs:318-322. Optional; downgraded from P2.
Registry::revoke cancels the generation's streams, but the generation stays in generations. It still counts against MAX_GENERATIONS (64, checked at :459), and Closed is not sent until closeForward. The reviewer suggests removing a generation as soon as it is revoked.

I don't think that change is right for this layer:

  • Keeping the handle is what lets a revoked generation answer 410 generation_revoked instead of 404. Harnesses don't retry a 410.
  • Removing it on revoke would make the caller's later close fail with "No such forwarding generation".
  • It would also break the documented order: Closed is the last event, and it follows close.

The real requirement is on the caller. Every generation that is sealed or revoked must still be closed. Otherwise a long session with many turns can reach the cap of 64, and every later openForward fails until the page reloads. No caller exists in this PR, so please check this in the stacked Claude and goose caller PRs. If you'd like a backstop here, add a line to the closeForward / pushForwardAdmission docs saying that a revoked generation still needs close.

2. A request body over BODY_LIMIT comes back as a retryable 502 — src-tauri/src/host_forward.rs:801, 812-813. Optional.
When the body is over 64 MiB, Limited fails inside send(), and the harness gets 502 upstream_failed. Claude Code retries 5xx responses, so it sends the same oversized body again on each retry before giving up. Returning a non-retryable 413 would give the harness a clear, final error. This would be rare, since only image-heavy turns get near the limit.

Not run: builds, tests, and live gateway or harness acceptance. The PR's own testing checklist is still open.

…t-forwarding

* commit '8c7f436e2cd339655c71ac3d26a2813e5c2b4dfd':
  fix(pi): keep Pi's own model onboarding in gateway mode
philazar added a commit that referenced this pull request Nov 13, 2026
…claude-neutral

Brings coder3's #2479 rework (8c7f436) up through #2916.

* origin/coder1/dxa-1156-host-forwarding:
  fix(pi): keep Pi's own model onboarding in gateway mode

Co-authored-by: Coder1 <db50ef1f1688dc7f3e536c17351e8e994a7466bcbe009794f61d64355c0ffc2e@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Philip Azar <[email protected]>
philazar added a commit that referenced this pull request Nov 13, 2026
Brings in #2479's Pi rework through #2916 and #2917: Pi's own model
onboarding in gateway mode. Only Pi files and the removed gateway
Settings fixture change here.

Co-authored-by: coder2 <4acc49cb93576448ba82e631688c51a40bf03d6a889d78fddf57917d198de10f@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Philip Azar <[email protected]>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant