feat(coding-agent): managed-acp provider + per-user GitHub token provisioning (draft)#967
Draft
aatchison wants to merge 4 commits into
Draft
feat(coding-agent): managed-acp provider + per-user GitHub token provisioning (draft)#967aatchison wants to merge 4 commits into
aatchison wants to merge 4 commits into
Conversation
…connect Integrate the self-hosted Cline coding agent as a managed-acp agent. On WS connect the backend authenticates the developer (Better-Auth bearer), asks the coding-agent broker to provision a per-user GitHub token (so Cline acts AS the developer — their commits/PRs), then proxies ACP frames to the workspace shim. - coding-agent/provision.ts: provisionWorkspaceToken() — POST /github/provision to the broker with a service token + the Better-Auth user.id; maps 409 → not_connected, other non-2xx → failed. The user-to-server token never transits Thunderbolt; the broker writes it straight into the workspace Secret. - coding-agent/provider.ts: advertises a single managed-acp websocket agent pointing at /v1/coding-agent/ws, only when CODING_AGENT_WORKSPACE_WS_URL is set. - coding-agent/proxy.ts: a thin bidirectional WS bridge to the workspace shim (buffers client frames until upstream open). Injectable upstream for tests. - coding-agent/routes.ts: WS /v1/coding-agent/ws — auth + provision in open() (close 4001 unauthorized, 4002 github-not-connected so the UI can prompt connect, 4003 provisioning/config failure), then bridge. - config: CODING_AGENT_WORKSPACE_WS_URL / CODING_AGENT_BROKER_URL / CODING_AGENT_SERVICE_TOKEN. Mounted in index.ts next to Haystack. Mirrors the Haystack managed-acp provider's auth-in-open() pattern. 12 tests (provision mapping, provider advertisement gating, proxy buffering/piping/ teardown); type-check + eslint clean. Note: committed with git directly rather than /thunderpush (authored from an external integration repo). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, limits, tests Actions the multi-lens review of the P3a patch. Ports the discipline of the existing universal relay (backend/src/proxy/ws.ts). Lifecycle / concurrency (proxy.ts, routes.ts): - single `closing` flag guards every upstream listener; `open` flush aborts if already closing; `message`/`send` ignored after teardown (no send on a closed socket). - client-disconnect-during-await no longer leaks the upstream: routes tracks `clientClosed` on ws.data and disposes if the client left during provisioning. - connect timeout bounds a hung upstream; bounded pre-connect queue (count+bytes) closes 4008 on overflow instead of growing unboundedly. Errors / resilience (provision.ts, routes.ts): - provision fetch now has a timeout (AbortSignal) and retries transient 5xx / network errors; 409/501/other-4xx are terminal (no retry). - open() wraps provision + proxy construction in try/catch and closes the socket (Elysia onError does NOT cover WS callbacks) — a broker outage or bad upstream URL closes cleanly instead of leaking an unhandled rejection / hung socket. Contract / correctness: - upstream close codes pass through (reserved 1005/1006/1015 → 1011); 4003 is now reserved strictly for pre-session provisioning failure, not mid-session drops. - broker 501 maps to `disabled` (proceed read-only), distinct from `failed`. - exhaustive ProvisionResult switch with a `never` default. Security / observability: - upstream-provided close reason is logged server-side but never relayed to the client (generic reason only). - one reused logger (not per-connection); structured logs on every provision outcome and upstream close/error. Types / config: - single typed `wsData(ws)` accessor replaces scattered `as unknown as` casts; literal-union event types on the upstream socket. - settings: trim the coding-agent fields; superRefine pairs brokerUrl<->token so a half-config fails at boot, not opaquely at connect. Honesty: routes doc + a startup WARN make the single-shared-workspace caveat explicit (per-user provisioning, shared workspace — single-user/PoC only). Tests (12 -> 33): proxy buffering/overflow/timeout/dispose/error/reserved-code; provision retry/timeout/disabled/network; NEW routes.test.ts WS auth-gate (4001) + provision close-codes (4002/4003) mirroring haystack/routes.test.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Semgrep Security ScanNo security issues found. |
|
Preview environment deployed 🚀
Stack: Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — |
PR Metrics
Updated Fri, 12 Jun 2026 16:27:46 GMT · run #1875 |
… fix CI 5x timeout The backend CI job runs the suite 5x with a 10-min cap; the prior routes.test.ts spun a DB-backed Elysia server per close-code (5 tests x 5 runs = 25 app spins) and tipped it over the timeout. - Refactor routes.ts: extract the open/message/close bodies into exported, deps-injected handlers (handleCodingAgentOpen/Message/Close + CodingAgentOpenCtx + a structural ws type). Thread createUpstream into the proxy and add an injectable `provision` seam. Replace `let result` with a `tryProvision` helper (const, early-return) per the style guide. - Rewrite routes.test.ts as fast unit tests (no bound port): auth 4001 (no bearer / unauthenticated), provision ok/disabled/not_connected(4002)/throw (4003)/exhaustive-default(4003)/not-configured(4003), client-closed-during-open abort, upstream-construct-failure(4003), message no-op/verbatim/JSON, close dispose, + a createCodingAgentRoutes construction/ WARN-branch test. Whole suite now runs in ~2s with zero server spins. - proxy.test.ts: cover all reserved abnormal close codes (1005/1006/1015 -> 1011) with a positive control, and the queueBytes byte-budget overflow path. Coverage: provider/provision 100%, proxy 100% lines, routes.ts 90% lines (only the Elysia delegation boilerplate uncovered — exercised repo-wide by the haystack WS suite). 49 tests; tsc + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the broker's per-user GitHub OAuth authorize URL + connection status to the built-in assistant so a developer can connect GitHub from chat. Backend (reuses #967's broker URL + service-token wiring): - github.ts: read-only broker client (GET /github/authorize-url, /github/status) mirroring provision.ts — service-token auth + x-tb-user-id, timeout/retry, body-free reasons so a broker error body never reaches logs. - github-routes.ts: authenticated GET /coding-agent/github/{authorize-url,status}. The developer's user.id is resolved server-side from the Better-Auth session ({ auth: true }) and forwarded to the broker — never a request/tool argument, so the model cannot act as another user. Returns a discriminated DTO (configured/ok/disabled/failed) in all reachable cases. - mounted in backend/src/index.ts alongside the existing coding-agent routes. Frontend: - src/integrations/coding-agent/{api,tools}.ts: github_connect / github_status ToolConfigs (no-arg schemas) calling the backend via the authenticated HttpClient; registered in getAvailableTools (always offered — backend reports configured:false cleanly when the broker isn't wired). Tests: unit-only against injected deps (no server/WS spin) — broker client mapping, route DTOs + that the broker is called with the session user.id, and the client tools' message shaping. type-check + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related: the broker, workspace operator, agent (Cline + ACP shim) container, and ArgoCD deploy manifests this provider talks to all live in thunderbird/thunderbolt-coding-agent — see its broker
POST /github/provisionendpoint (the contract this PR'sprovision.tscalls) and the multi-user GitHub-auth design doc.What
Adds a
coding-agentmanaged-acp provider (mirrors the Haystack provider) that surfaces a self-hosted Cline workspace agent. On WS connect the backend authenticates the developer (Better-Auth bearer), asks a broker to provision a per-user GitHub App user-to-server token (so the agent commits/opens PRs as the developer, not a shared bot), then proxies ACP frames to the workspace shim.coding-agent/provision.ts—POST /github/provisionto the broker with a service token + the Better-Authuser.id. Timeout + retry on transient 5xx/network;409→not_connected,501→disabled, other→failed. The GitHub token never transits Thunderbolt — the broker writes it straight into the workspace Secret.coding-agent/provider.ts— advertises one managed-acp websocket agent at/v1/coding-agent/ws, only whenCODING_AGENT_WORKSPACE_WS_URLis set.coding-agent/proxy.ts— a bounded, lifecycle-guarded bidirectional WS bridge to the workspace shim (ports the discipline ofbackend/src/proxy/ws.ts:closingflag, connect timeout, capped pre-connect queue).coding-agent/routes.ts—WS /v1/coding-agent/ws: auth + provision inopen()wrapped so a broker/network failure closes the socket (ElysiaonErrordoesn't cover WS callbacks). Close codes: 4001 unauthorized, 4002 github-not-connected (UI should prompt connect), 4003 provisioning failed / not configured.CODING_AGENT_WORKSPACE_WS_URL/CODING_AGENT_BROKER_URL/CODING_AGENT_SERVICE_TOKEN;superRefinepairs broker URL ↔ service token.Per-user provisioning is wired, but all sessions currently proxy to one shared
CODING_AGENT_WORKSPACE_WS_URL— per-user workspace routing doesn't exist yet, so concurrent users would share one workspace and the last-provisioned token. A startupWARNmakes this explicit. Single-user / PoC-safe only; per-user workspace routing is the next increment.Tests
bun test— provision (retry/timeout/disabled/mapping), provider (advertisement gating), proxy (buffering/overflow/timeout/dispose/error/reserved-codes), and a WS routes auth-gate + provision-close-code suite mirroringhaystack/routes.test.ts. P3b adds:github.test.ts(broker-client mapping incl. bad-body / never-carries-end-user-token),github-routes.test.ts(DTOs + 401 + asserts the broker is called with the sessionuser.id, driven via.handle()against injected seams — no server/WS spin, CI-5x-safe), andintegrations/coding-agent/tools.test.ts(no-arg schema + message shaping).tsc+eslintclean on all new files.P3b —
github_connect/github_statusassistant toolsAdds the user-facing GitHub-connect surface for the built-in assistant (the design's "MCP management plane"), reusing this PR's broker base URL + service token:
backend/coding-agent/github.ts— read-only broker client (GET /github/authorize-url,GET /github/status), mirroringprovision.ts: service-token auth +x-tb-user-id, timeout/retry,501→disabled, body-freefailedreasons so a broker error body never reaches logs.backend/coding-agent/github-routes.ts— authenticatedGET /v1/coding-agent/github/{authorize-url,status}. The developer'suser.idis resolved server-side from the Better-Auth session ({ auth: true }) and forwarded to the broker — it's never a request/tool argument, so the model invoking the tool cannot act as another user. Returns a discriminated DTO (configured/ok/disabled/failed) in all reachable cases; mounted alongside the WS route inbackend/src/index.ts.src/integrations/coding-agent/{api,tools}.ts—github_connect(returnsConnect your GitHub: <url>) andgithub_status(connected / not-connected)ToolConfigs with no model-supplied args, calling the backend via the authenticatedHttpClient. Registered ingetAvailableTools(always offered — backend reportsconfigured: falsecleanly when the broker isn't wired).Why the identity can't be spoofed: the only place
user.identers is the resolved session on the backend; the tool schema is an empty object, and the client never sends an id. This matches the WS provider's identity model.Runtime/deploy note: these tools assume the Thunderbolt backend can reach the broker at
CODING_AGENT_BROKER_URLwithCODING_AGENT_SERVICE_TOKEN(same wiring as provisioning) — verify in-cluster reachability before relying on them in a deployment.Relationship to native Thunderbolt Workspaces (THU-549/550)
Heads-up on a terminology collision: this PR's "workspace" means a compute sandbox (a pod running Cline + a repo checkout), which is a different axis from the native Workspaces tenancy primitive being built in THU-549 (Foundation) / THU-550 (data layer) — that one is a team/trust-domain with
workspace_idscoping, membership, and invite-by-email.They're complementary, not conflicting:
user.id(a GitHub account is personal, not per-team), so the provisioning model here is unaffected by Workspaces.(user.id, workspaceId)and depend on the THU-550 primitive rather than inventing a parallel tenancy. Tracked separately.available_commands_update→ slash menu) surfaces this agent's commands for free once Cline advertises them.Not in this PR
Per-user sandbox routing (to integrate with native Workspaces / THU-550) — still the single-shared-workspace limitation above.
🤖 Generated with Claude Code