Skip to content

feat(dev): PTY-backed Claude Code / Codex host sessions in the Workbench (#600 PR 3) - #675

Merged
ScriptedAlchemy merged 21 commits into
mainfrom
wb600-pr3-sessions
Sep 6, 2026
Merged

feat(dev): PTY-backed Claude Code / Codex host sessions in the Workbench (#600 PR 3)#675
ScriptedAlchemy merged 21 commits into
mainfrom
wb600-pr3-sessions

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 6, 2026

Copy link
Copy Markdown
Owner

PR 3 of #600 (owner decision 05:31: go-full). PTY-backed Claude Code / Codex sessions embedded in the Workbench, attached to the dev plugin through the existing dev install / epoch mechanism, correlated into the PR 2b trace.

What lands

  • Server (packages/agent-bundle/src/dev/sessions/): HostSessionService (PTY allocation via @lydell/node-pty, loaded lazily from the project root; max 4 live sessions; 256 KiB scrollback; terminate = SIGTERM then SIGKILL after 2 s; restart carries restartOf and the prompt) and HostSessionRoutes under /api/sessions — list, create, get, SSE stream (state / base64 output / end), input, resize, terminate, restart, delete — every route behind the same mutation-session authority as /api/invocations, loopback only. Fixed argv (claude [prompt], codex [prompt]), cwd = project root, no approval-bypass flag is ever passed. A host is launchable only while its dev install (--install-host) is attached to the adopted epoch. Diagnostics AB8260AB8266.
  • Correlation: the host's own session id is the sessionId join key as soon as its first hook receipt arrives. The generated wrapper adds devSession (from AGENT_BUNDLE_DEV_SESSION) to the receipt, the server records the alias, publishes session.attached, and later lifecycle entries carry the host's id. The dev proxy sends x-agent-bundle-dev-session (when AGENT_BUNDLE_DEV_SESSION reaches it) and always x-agent-bundle-dev-pid; Codex forwards only a whitelisted env to stdio MCP servers, so HostSessionRoutes falls back to resolving the Workbench session from the proxy's process ancestry (HostSessionService.sessionForProcess). The MCP trace sink resolves the alias lazily per frame. New trace source session.
  • Workbench: Host sessions destination (/sessions?session=<id>), xterm.js terminal (@xterm/xterm + fit addon, MIT, bundled by the Workbench Rsbuild config), launch buttons with availability reasons, authority strip (project root, epoch, install destination, pid, state), Terminate / Restart / Forget, Open in Claude / Open in Codex on route workspaces seeding a prompt, Trace links both ways.
  • Dependencies: @lydell/node-pty@1.2.0-beta.15 (MIT, prebuilt) is a root workspace devDependency — never named in packages/agent-bundle/package.json, never in a plugin artifact. Consumers add it to their own dev deps to enable Sessions; without it AB8260 disables launch and nothing else changes.
  • Docs: Workbench guide "Host sessions" section (en + zh), dev-server-http.mdx (/api/sessions*, SSE frames, session source), runtime-environment.mdx (AGENT_BUNDLE_DEV_SESSION), docs/diagnostics.md.

Security posture

No remote PTY exposure: the routes bind on the foreground server's loopback listener and require its mutation session (cookie/bearer + browser-origin check). Launch is an explicit POST /api/sessions with a fixed host and optional prompt; the request never carries a command, argv, cwd, or env. The host's own permission/consent flow is untouched. The PTY environment is the dev server's environment plus AGENT_BUNDLE_DEV_TRACE_URL/TOKEN (already the hook-receipt contract) and AGENT_BUNDLE_DEV_SESSION; the foreground session token is never passed to the child. Project-root authority (path, epoch, install destination, pid) is shown in the Sessions pane. Trace entries carry ids and byte counts, never prompt text, PTY bytes, or credentials.

Acceptance

Deterministic (CI): packages/workbench/tests/sessions.e2e.test.ts (integration pool, 1440×900) runs a copy of examples/audiobook-curator with the dev server started with installHosts: ['claude','codex'] under an isolated HOME/CLAUDE_CONFIG_DIR/CODEX_HOME, PATH prefixed with packages/agent-bundle/tests/support/fake-host-cli/ (dependency-free Node scripts that behave like a host attached to the dev install: answer plugin list --json / marketplace add, run the installed plugin's SessionStart hook command from hooks.json with a host-shaped payload, spawn the .mcp.json dev proxy and drive initializetools/listtools/call; the Codex fake forwards only the host's whitelisted env, so its correlation is proven through proxy PID ancestry). Asserts: launch Claude → banner in the terminal → authority strip shows project root / epoch / install / pid → traceSessionId becomes the host's own session id → Trace deep link lands on one group containing session.*, hook.received, and tools/call entries → group header links back to the session; terminate → Terminated; restart → new id with Restart of <old>; the same for Codex; Open in Claude from /routes/mcp/curator/tool/search_audible lands on Sessions with the seeded prompt in the terminal; an unauthenticated POST /api/sessions from the page is refused; hosts off PATH disable launch with the reason shown.

Real hosts (this machine, examples/audiobook-curator, 1440×900): launched the real claude through the pane; terminal renders Claude Code inside the Workbench; authority strip shows the project root, epoch and the dev install path; Terminate → TERMINATED · EXIT 143; Restart → new session with Restart of hs_…; the Trace shows session.started / session.terminated / session.started(restartOf) correlated on the session id; the PTY child carries AGENT_BUNDLE_DEV_SESSION=<hs_…> and the existing AGENT_BUNDLE_DEV_TRACE_URL/TOKEN, not the foreground token; no approval-bypass flag is passed (the "bypass permissions" banner in the screenshot is this machine's own ~/.claude/settings.json defaultMode). Running a tool through the real hosts is blocked here: claude auth statusloggedIn: false, and codex is out of usage — an owner-side credential gap, covered until then by the fake-host acceptance and the opt-in AGENT_BUNDLE_NATIVE_HOST_CONTRACTS=1 packages/agent-bundle/tests/native-host-sessions.test.ts.

Found during acceptance, pre-existing and out of scope: restarting agent-bundle dev --install-host … after a dev install exists fails AB7202/AB7004 because the installer's compare refuses the generation symlink layout (Claude) and Codex refuses re-adding the marketplace from a new temp source — filed as a follow-up on #600.

Local gate (merge gate)

Run on wb600-pr3-sessions at 71149d85c2 (the final commit, containing origin/main c864e11a82 #674):

Command Result
pnpm build pass
pnpm typecheck pass
pnpm lint pass (1475 files)
pnpm test:unit 4385 passed, 6 skipped, 0 failed
pnpm test:route-unit 89 passed
pnpm test:projection 190 passed
pnpm test:integration:run (whole pool incl. the Workbench browser pool and the new sessions.e2e.test.ts) 100 files, 1161 passed, 4 skipped, 0 failed
pnpm test:packed 39 passed, 1 skipped (the opt-in native smoke), 0 failed
pnpm docs:site:build pass — language parity ok, 0 broken links / 28299 anchors

The same gate ran green on the pre-review commit cb4d1f2045 (there test:packed first hit one 30 s timeout in the untouched public-api-packed.test.tsnpm install under load, no assertion reached — and passed on re-run). The diff touches the hook wrapper (events/trace-receipt.ts), the dev proxy, the foreground server, the Workbench and the docs, hence the whole integration pool, the packed pool and the docs site.

Deslop

Model: Claude Fable 5.1, 5 edits over the full diff vs origin/main: dropped the agent-bundle/contracts/host-sessions public entry S1 had added (no consumer — the Workbench imports contracts by relative source path like every other contract, and no other contract is a public entry); removed the unused disposer plumbing from the PTY adapter (PtyProcess.onData/onExit now return void; the fakes followed); replaced the SSE stream's replay buffer with an existence check before the head is written; used HOST_SESSION_UNKNOWN_CODE instead of a repeated 'AB8262' literal; deleted the lane notes file S4 committed. Behavior unchanged.

Self-review

Reviewer: change-risk-reviewer on Claude Fable 5.1 (thinking-high), full diff vs origin/main. Round 1: no blockers; 3 should-fix, 3 nits.

# Finding Disposition
1 The seeded prompt is an unconstrained argv element — --dangerously-skip-permissions or a subcommand word would be parsed by the host CLI, contradicting the docs Fixed (a34bf5e223): POST /api/sessions refuses a blank prompt or one starting with - (AB8261), test added; docs state the prompt is the host's positional argument. Subcommand-shaped first words are not rejected — a real prompt may start with mcp, and the caller is the authenticated local developer
2 The hook wrapper forwarded any non-empty AGENT_BUNDLE_DEV_SESSION; the server 400s non-hs_ values, so a stray shell variable would silently drop every hook receipt Fixed: the wrapper gates devSession on isHostSessionId, like the proxy; test added
3 Scrollback replay emitted one SSE record per retained PTY chunk and could overflow the 128-record backpressure limit on reconnect Fixed: replay is one output frame (Buffer.concat); service test and en/zh reference updated
4 attach() was first-wins; Claude Code issues a new session_id on /clear, splitting the Trace group Fixed: re-attach on a new host id publishes another session.attached; docs mention /clear and pre-receipt hs_… grouping
5 Docs said only the SessionStart receipt carries devSession; every receipt does Fixed in both locales
6 workbench-server.ts closes over hostSessions ~200 lines before its const (TDZ if invoked early) Dismissed: the closure runs only on a hook receipt, which needs hookReceiptUrl set after listen, i.e. after every const in the function is initialised — the same shape the file already uses for hookReceiptUrl

Residual risks the reviewer noted, accepted: x-agent-bundle-dev-session/-pid on /mcp/host/* are loopback client claims (a local process could mis-attribute its own MCP frames to a session in the Trace; no PTY access or privilege — /mcp/host/* already trusted loopback callers before this PR). Round 2 (same reviewer, on the fix commit): findings 1–3, 5 resolved; 6 dismissal accepted; no new blockers. One new should-fix on 4: last-wins on every receipt would let a nested host run inside the PTY (claude -p … from a Bash tool, the plugin's own tests) flip the alias on each tool hook. Fixed (71149d85c2): the alias moves only on a session/start receipt, so /clear is followed and a nested run can move it at most once; test with an interleaved tool/before receipt carrying another id; docs say "a session/start receipt with a new host id". The final gate below ran on that commit.

Closes #600 after merge (landing summary posted there).

…_SESSION (#600)

Wrappers post a top-level receipt `devSession` and leave the host session_id on
identity. The proxy sends `x-agent-bundle-dev-session` when the id is valid;
receipts reject a bad value with AB8266 and call `attachHostSession`. Host MCP
opens with a lazy sessionId resolver so frames pick up the host id once known.
… the Sessions pane

traceSources gains 'session'; the Trace page headlines a host-session group by
its lifecycle entry (glyph ▣) and offers a Session link back to
/sessions?session=<hs id> whenever a group is joined on an hs_ session id.
…ne, Open in Claude/Codex

Consume the frozen /api/sessions HTTP+SSE contract: strict zod decoders, base64
output frames written to one xterm per session, input/resize posts, Host
sessions nav item with /sessions?session=<id> deep links, Trace link per
session (traceSessionId ?? id), and Open in Claude / Open in Codex beside Run
with the seeded prompts, disabled with the availability reason. Adds the
sessions-* / route-open-in-* test ids and the xterm.js MIT notice.
…cestry — Codex forwards only a whitelisted env to stdio MCP servers
@changeset-bot

changeset-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 71149d8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 6, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@675
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@675
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@675
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@675

commit: 71149d8

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.

Workbench redesign: make dev mode an application explorer with live route rendering, MCP/hooks traces, and embedded host sessions

1 participant