feat(router): add attach endpoint for local clients to connect to router sessions#27039
Closed
mrsimpson wants to merge 617 commits into
Closed
feat(router): add attach endpoint for local clients to connect to router sessions#27039mrsimpson wants to merge 617 commits into
mrsimpson wants to merge 617 commits into
Conversation
…ly to router The cloudflare operator was routing session subdomain tunnel traffic directly to the opencode-router service, bypassing Traefik entirely. This meant the IngressRoute middleware chain (ForwardAuth → oauth2-chain) never ran, so X-Auth-Request-Email was never injected and every request returned 'Missing user identity'. Fix: point ROUTER_SERVICE_URL at traefik-controller so session subdomain traffic flows: Cloudflare → cloudflared → Traefik → IngressRoute → oauth2-chain → ForwardAuth → opencode-router → session pod.
patchNamespacedPod with @kubernetes/client-node v1.4.0 defaults to Content-Type: application/json-patch+json, which requires an array of RFC 6902 operations. Sending a plain merge-patch object caused the k8s API to return 400 'cannot unmarshal object into Go value of type []handlers.jsonPatchOp'. Fix: use op:add with JSON pointer path (RFC 6901 escaping for '/'→'~1') so both updateLastActivity and deleteIdlePods annotation refreshes work.
0.1.1 adds X-Auth-Request-Token to the ForwardAuth middleware's authResponseHeaders so oauth2-proxy forwards the GitHub OAuth token to the router as X-Auth-Request-Token. Without it, ensurePod always received githubToken=undefined and never created the per-session GitHub token Secret, leaving GITHUB_TOKEN absent from session pods.
The deployment already sets DEBUG_HEADERS=true but the router never read it. Add debugHeaders to config and log x-auth-request-token and x-forwarded-access-token on every /api/* call so we can verify whether the GitHub OAuth token reaches the router from Traefik ForwardAuth.
The router code reads DEBUG_HEADERS to log incoming auth headers on /api/* calls, but the env var was missing from the Pulumi deployment spec. Add it so we can diagnose whether X-Auth-Request-Token reaches the router.
Move debug logging to the top of the request handler so it fires for every request regardless of URL, and include the host so we can see both session-subdomain and root-domain traffic. Also add DEBUG_HEADERS to the Pulumi deployment env so the router actually reads it. This will confirm whether X-Auth-Request-Token arrives at the router at all, and whether POST /api/sessions is hitting the expected code path.
oauth2-proxy sets X-Auth-Request-Access-Token (not X-Auth-Request-Token) when --pass-access-token=true is used. Update getGithubToken() to check the correct header first, with fallbacks for backward compatibility. Also fix the debug log to display the correct header name.
0.1.2 fixes the ForwardAuth authResponseHeaders to use X-Auth-Request-Access-Token (what oauth2-proxy actually sets with --pass-access-token=true) instead of X-Auth-Request-Token (which oauth2-proxy never sets). This makes Traefik forward the GitHub OAuth token to the router so GITHUB_TOKEN appears in session pods.
patchNamespacedSecret with a plain object body fails with HTTP 400 'cannot unmarshal object into Go value of type []handlers.jsonPatchOp' because the client defaults to application/json-patch+json which requires an RFC 6902 array. Use replaceNamespacedSecret (PUT) instead — idempotent for a single-key secret and avoids the patch content-type issue entirely.
Grants authenticated users a GitHub token with repo, read:org, workflow and gist scopes — required for typical dev workflows in the code app.
- Add src/models.ts: filterFreeModels/filterPaidModels pure functions + fetchFreeModels/fetchPaidModels async helpers that call OpenRouter API - index.ts: add openrouterApiKey + openrouterFreeApiKey secrets, mount OPENROUTER_API_KEY + OPENROUTER_FREE_API_KEY into opencode-api-keys - index.ts: ConfigMap now uses pulumi.all to embed live model lists at deploy time; openrouter (paid, curated) + openrouter-free (custom @ai-sdk/openai-compatible, all zero-price models) - images/opencode/config/opencode.json: static snapshot of both providers as fallback when ConfigMap is not mounted
…Router secrets to stack config Session pods now use OpenRouter providers exclusively; Anthropic key removed from the mounted secret. Both openrouterApiKey and openrouterFreeApiKey added to Pulumi.dev.yaml (encrypted).
…dels via jq - Add jq to opencode image for deep merge functionality - Simplify ConfigMap to only contain provider.model lists (dynamic) - Static config (agents, skills, plugins, MCP) stays baked in image - Init container deep-merges ConfigMap into baked config with jq - Remove unused code:anthropicApiKey from stack config - Add session pod configuration docs to README.md
The --require path was set in the pod spec, which broke any pod running an older image that didn't yet have /etc/bind-all-interfaces.cjs baked in. Fix: move NODE_OPTIONS=--require=/etc/bind-all-interfaces.cjs into the Dockerfile as an ENV instruction. The file is guaranteed present in the same image layer (COPYed above), so --require always resolves. Old pods running old images simply don't have NODE_OPTIONS set — no crash. Also add a CI test step (before push) that starts a node server inside the built image with listen(5173, 'localhost') and asserts the bound address is 0.0.0.0, failing the build if the patch is broken.
…d ports only Two bugs prevented dev-server port routes from being created: 1. fetchSessionPorts used routerServiceUrl (Traefik/OAuth2) instead of routerAdminUrl (direct ClusterIP). The /api/sessions/:hash/ports GET endpoint sits behind the oauth2 middleware when accessed via Traefik, returning 404 for every poll cycle. Fix: use config.routerAdminUrl which bypasses Traefik and hits the router service directly. 2. The port-watcher plugin pushed all listening ports (hundreds of ephemeral kernel sockets) instead of only the dev-server allowlist. Fix: filter by DEV_PORT_ALLOWLIST before pushing, and additionally filter by TCP state 0A (TCP_LISTEN) to exclude established connections.
POST /api/sessions/:hash/progress was missing the same pre-email-check bypass that POST /ports has. Requests from the in-pod plugin carry only x-pod-secret, never an oauth2 email header, so they hit the 401 gate at line 111 before reaching handleApi. Combined the two pod-push bypasses into one regex (progress|ports) so the pattern stays consistent. Auth is enforced inside handleApi via podSecretStore.verify() — same as before for /ports.
…d context Adds a dev-server skill to the homelab opencode image so agents know how to start dev servers detached (nohup pattern), which ports are auto-exposed (3000, 3001, 4321, 5173, 5174, 8000, 8080, 8888), and how to construct the port-forwarding public URL using OPENCODE_SESSION_HASH. Also fixes the Makefile build-opencode/push-opencode targets which were using homelab/ as Docker build context; the Dockerfile COPYs packages/ and deployment/homelab/ paths that require the monorepo root as context.
Two root causes identified and fixed: 1. Wrong CWD for skills-server: opencode spawns local MCP servers with the git workspace as CWD (/home/opencode/repo). @codemcp/skills-server resolves .agentskills/skills/ relative to CWD, so it failed to find skills installed in ~/.config/opencode/. Fixed by changing the command to use sh -c and passing $HOME/.config/opencode as argv so the shell expands the path. 2. Skills not re-synced on pod restart: the init container only seeded config and ran setup-skills.sh on first pod start. After image updates adding new skills, restarted pods kept the stale config dir and missed new skills. Fixed in pod-manager.ts: .agentskills/, .ade/, and skills-lock.json are now always synced from /etc/opencode-defaults/ and init-scripts/*.sh is always re-run on every pod start (idempotent).
…sionInfo Commit 1328722 swapped the if/else if order so that a resumed pod with an initialMessage annotation would always bootstrap a new session instead of linking to the existing one stored in the PVC's SQLite DB. Restore the original logic: check activity.sessionId first (link to existing session on resume), only fall through to bootstrapPodSession when the pod has no sessions yet (fresh pod). Adds 4 regression tests covering all combinations of sessionId × initialMessage to prevent this from regressing again.
consolidate skill files in the agentskills path
…ter sessions ## Intent Allow local opencode clients to attach to a running router session without going through OAuth, so developers can connect their local CLI directly to a cloud-hosted session. ## Decisions - Use a dedicated attach server on a separate port (ATTACH_PORT, default 4096) that is intentionally not behind oauth2-proxy, so attach subdomain requests can bypass OAuth. - Authenticate via a per-session password stored in a PVC annotation (opencode.ai/attach-password) rather than OAuth tokens. Password is generated at session creation and lazy-created for existing sessions. - Accept the password via HTTP Basic Auth, ?password= query param, or X-Attach-Password header to support different client integration styles. - Restrict the /api/sessions/:hash/attach-info endpoint to the session owner only (403 for everyone else) so passwords are never leaked across users. ## Key changes - packages/opencode-router/src/index.ts: add attach subdomain routing with password auth; extract shared proxyToPod helper; fix wsHandler type (upgrade event signature, not RequestListener); start attachServer on ATTACH_PORT alongside the main server - packages/opencode-router/src/pod-manager.ts: add getOrCreateAttachPassword, generateAttachPassword, getAttachUrl; store attach password in PVC at creation time; expose attachUrl/attachPassword in SessionInfo; remove unused req parameter from getSessionInfo - packages/opencode-router/src/api.ts: add GET /api/sessions/:hash/attach-info endpoint returning attachUrl and attachPassword for session owner - packages/opencode-router/src/config.ts: add attachPort and attachRoutePrefix config options - packages/opencode-router-app/src/session-item.tsx: add Attach button that copies the ready-to-run opencode attach command to clipboard - packages/opencode-router-app/src/api.ts: expose attachUrl/attachPassword in SessionSchema - packages/opencode-router/src/*.test.ts: add unit tests for attach-info API, hostname parsing, password generation, and config defaults
Contributor
|
This PR doesn't fully meet our contributing guidelines and PR template. What needs to be fixed:
Please edit this PR description to address the above within 2 hours, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know. |
Contributor
|
The following comment was made by an LLM, it may be inaccurate: Potential Related PR Found:
The other results (PRs #12822 and #9095) don't appear to be related to this feature. |
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.
Intent
Allow local opencode clients to attach to a running router session without
going through OAuth, so developers can connect their local CLI directly to
a cloud-hosted session.
Decisions
ATTACH_PORT, default4096)that is intentionally not behind
oauth2-proxy, so attach subdomain requestscan bypass OAuth.
(
opencode.ai/attach-password) rather than OAuth tokens. Password isgenerated at session creation and lazy-created for existing sessions.
?password=query param, orX-Attach-Passwordheader to support different client integration styles.GET /api/sessions/:hash/attach-infoto the session owner only(403 for everyone else) so passwords are never leaked across users.
Key changes
index.ts— add attach subdomain routing with password auth; extract sharedproxyToPodhelper used by both HTTP and WS paths; fixwsHandlertype (upgrade event signature, notRequestListener); startattachServeronATTACH_PORTalongside the main serverpod-manager.ts— addgetOrCreateAttachPassword,generateAttachPassword,getAttachUrl; store attach password in PVC at creation time; exposeattachUrl/attachPasswordinSessionInfo; fix accidental removal ofenvblock (OPENCODE_POD_SECRET,OPENCODE_SESSION_HASH,OPENCODE_ROUTER_URL) from pod specapi.ts— addGET /api/sessions/:hash/attach-infoendpoint returningattachUrlandattachPasswordfor session ownerconfig.ts— addattachPortandattachRoutePrefixconfig optionssession-item.tsx— add Attach button that copies the ready-to-runopencode attach <url> --password <pw> -s <sessionId>command to clipboardrouter-app/api.ts— exposeattachUrl/attachPasswordinSessionSchema*.test.ts— unit tests for attach-info API, hostname parsing, password generation, and config defaultsATTACH_PORT4096ATTACH_ROUTE_PREFIXattach-