Harden GitHub token injection authorization boundary#1252
Merged
simple-agent-manager[bot] merged 7 commits intoJun 8, 2026
Merged
Conversation
9 tasks
…back token The git-token hardening removed the durable callback token from the on-disk credential helper script (the helper now performs a local /git-credential exchange via the VM agent's in-memory workspace callback). Update TestIntegration_GitCredentialHelperFullFlow to match: assert the token is ABSENT and the /git-credential endpoint + port are PRESENT, instead of asserting the (now-removed) embedded token. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
a6b3788 to
a082ea6
Compare
a082ea6 to
d68bc0c
Compare
|
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.



Summary
Hardens the GitHub token injection authorization boundary so that
POST /api/workspaces/:id/git-tokenbecomes the hard final authorization gate before any installation token is minted, with defense-in-depth preflight gates on the upstream entry points that can trigger workspace/agent work.Core boundary (
apps/api/src/routes/workspaces/runtime.ts)verifyWorkspaceGitHubOwnerAccess()runs beforebackfillProjectGithubRepoId()/resolveWorkspaceGitHubTokenOptions()/getInstallationToken():getGitHubUserAccessTokenForOwner→ 403 fail-closed if absent (never callsgetInstallationToken).assertRepositoryAccessagainst the exact repo.githubRepoId≠ live id) → 403.repositoryName→ 403 otherwise.repositoryIds: [verifiedRepoId]).Owner-token lookup without a session cookie (
apps/api/src/services/github-user-access-token.ts)getGitHubUserAccessTokenWithHeaders(env, headers, userId, flow).getGitHubUserAccessToken(c, userId)keeps the request-header flow.getGitHubUserAccessTokenForOwner(env, userId, flow)uses emptyHeadersso VM-agent callback routes (no session cookie) can resolve the owner's token.Defense-in-depth preflight gates (fail-closed, all 403 on missing access)
apps/api/src/routes/projects/_helpers.ts: sharedrequireRepositoryOwnerAccess()(short-circuits non-GitHub projects, verifies owned installation, owner token,assertRepositoryAccess, repo ID drift).apps/api/src/routes/workspaces/lifecycle.ts:/restart,/rebuild.apps/api/src/routes/workspaces/agent-sessions.ts:POST /:id/agent-sessions.apps/api/src/services/trigger-submit.ts: beforestartTaskRunnerDO(trigger/cron/webhook submit).mcp/dispatch-tool.ts,mcp/orchestration-tools.ts,sam-session/tools/dispatch-task.ts,sam-session/tools/retry-subtask.ts,project-orchestrator/scheduling.ts,tasks/run.ts.Reduce reusable static token exposure (VM agent, Go)
packages/vm-agent/internal/server/git_credential.go: replacedisValidCallbackAuthwithisAuthorizedGitCredentialRequest— validates a bearer when present, otherwise requires a local (loopback / private-IP) exchange. The git credential helper script no longer carries a reusable bearer token.packages/vm-agent/internal/acp/session_host_startup.go: always strips any inheritedGH_TOKENand re-fetches a fresh scoped token viaGitTokenFetcherat ACP start (was only fetched whenGH_TOKENwas absent).packages/vm-agent/internal/bootstrap/bootstrap.go: stops writing a staticGH_TOKENinto/etc/sam/env; credential-helper render no longer embedsCallbackToken.Validation
pnpm lint— 0 errorspnpm typecheck— 16/16 packagespnpm test— api (323 + 11) and web (2252) suites pass; build 9/9internal/acp/gateway_test.go,internal/server/git_credential_test.go,internal/bootstrap/bootstrap_test.go) are covered by CI. See "Untested Gaps".Staging Verification (REQUIRED for all code changes — merge-blocking)
Deploy Stagingrun 27119618337 succeeded on headSha 22b569e (= current HEAD). Both the staging API Worker and the R2 VM-agent binary reflect this branch.https://api.sammy.partyas the staging smoke user; provisioned + exercised real infrastructure (see evidence)./git-tokenboundary + hardened credential helper + ACP strip-and-refetch were exercised end-to-end on a fresh VM running the new binary (rules 13, 22, 27).01KTK03TXEBNZX2QMMDCVM2NQT(IP91.98.193.139) provisioned via the installation/account-level platform credential, heartbeat healthy at 06:55:40 UTC. Node created after the green deploy, so it downloaded the new VM-agent binary from R2 (rule 27).N/A: no UI changesStaging Verification Evidence (positive)
01KTK03TXEBNZX2QMMDCVM2NQT, IP91.98.193.139, statusrunning, heartbeat 06:55:40 UTCserverspresentation2025/crewai@ 06:56:30 → "Populating volume from host clone" → "Volume populated" → "Wrote credential helper to host"/git-token+ hardened helperserverspresentation2025/hono@ 07:05:36 → "Volume populated" + "Wrote credential helper to host"serverspresentation2025/elysia@ 07:15:53 → "Volume populated" + "Wrote credential helper to host"running01KTK1CXHKRRMBTGEZPBF2NHVX→ statusrunning(clean devcontainer)01KTK1MZ74JH6XTGD4QWXE8TZ7→ statusrunning, no error; "MCP servers registered for agent session"requireWorkspaceAgentGitHubAccessdefense-in-depth gate (passed for legitimate owner) ANDsession_host_startup.gostrip-GH_TOKEN+re-fetch-fresh-scoped-token against the hardened boundary. A 403 from the boundary, or a failed strip-and-refetch, would have prevented the session from reachingrunning.Why the ACP
runningstate is the proof:session_host_startup.gonow unconditionally strips any inheritedGH_TOKENand re-fetches a fresh scoped token fromPOST /api/workspaces/:id/git-token(the hardened boundary) at ACP start. The agent session reachingrunningwith no error means that fresh-token fetch succeeded through the full hardened gate (owner-OAuth check →assertRepositoryAccess→ repo-id-drift → single-repo mint) for a legitimately-authorized owner. The defense-in-depthrequireWorkspaceAgentGitHubAccessgate onPOST /:id/agent-sessionslikewise passed (did not false-positive 403 a legitimate owner).No raw token values were read or logged at any point.
End-to-End Verification
Data Flow Trace
Live Write-Path Verification (single-repo scope confirmed)
Drove the container terminal PTY (same scoped-token machinery an agent uses: bind-mounted git credential helper +
ghwrapper + single-repo installation token) to prove the hardened boundary mints a token that can write to the user-owned repo only, never upstream.GET /installation/repositories→total_count: 1(onlyserverspresentation2025/elysia)elysiajs/elysiais not in the grant, so writes to it are physically impossibleorigin = github.com/serverspresentation2025/elysia.git;ghauthed assammy-party[bot]; everyghcall used explicit--repo serverspresentation2025/elysiagit pushgh-token-write-verifypushed (push_rc=0, sha44e498e)serverspresentation2025/elysia#3—head.repoANDbase.repobothserverspresentation2025/elysia(no fork / cross-repo)serverspresentation2025/elysia#4—pull_request: null(genuine issue)elysiajs/elysia; public read metadata is world-readable and is not write access.total_count:1makes upstream writes structurally impossible.Conclusion: an agent in this workspace can create an issue and open a PR with a pushed branch — but only in the user-owned
serverspresentation2025/elysiarepo. The single-reporepositoryIds:[id]mint at the/git-tokenboundary is the enforcing safety net.Bounded Gaps (remaining)
Interactive— NOW LIVE-VERIFIED (see "Live Write-Path Verification" below). Originally listed as a bounded gap (the chat→ACPgit push+ PR/issue creation inside the container/sessions/:id/promptbridge was unavailable because the workspace was provisioned standalone, sochatSessionIdwas unset). Verified instead by driving the container terminal PTY (wss://ws-{id}.{BASE_DOMAIN}/terminal/ws), which exercises the same bind-mounted credential helper + single-repo scoped installation token an agent uses. Push, PR, and issue all succeeded — and all three were structurally confined to the user-owned repo.assertRepositoryAccessdenial unit tests.githubCliPolicyprofile path — no agent profile with a custom policy exists on staging to exercise it live; covered by unit tests.Residual Risks
/git-tokenboundary — a local process can still obtain a token, but only the narrowly-scoped one the boundary would mint.Specialist Review Evidence
packages/vm-agent/internal/server/git_credential.goby limiting bearerless local exchange to the primary workspace and adding a regression test for secondary-workspace denial. Control-plane error bodies are no longer copied into VM-agent log errors. Unknown repo providers now fail closed inapps/api/src/routes/projects/_helpers.ts.Agent Preflight
Classification
External References
GitHub official documentation for GitHub App installation access tokens and repository-scoped installation tokens: https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app. Existing in-repo contracts for
assertRepositoryAccess,getInstallationToken, workspace callback auth, and VM-agent credential-helper behavior were also reviewed before changing the token boundary.Codebase Impact Analysis
Credential-boundary changes span
apps/api/src/routes/workspaces/runtime.ts,apps/api/src/routes/projects/_helpers.ts, workspace/task spawn gates underapps/api/src/routes/workspaces/,apps/api/src/routes/mcp/,apps/api/src/services/trigger-submit.ts, and VM-agent credential handling underpackages/vm-agent/internal/server/,packages/vm-agent/internal/acp/, andpackages/vm-agent/internal/bootstrap/. The final follow-up commitd68bc0c5is scoped to_helpers.ts,git_credential.go, andgit_credential_test.go.Documentation & Specs
N/A: no public user-facing setup or behavior documentation changed; the security boundary and residual risks are documented in this PR body, and repo markdown outside public docs is not treated as user documentation.
Constitution & Risk Check
Checked fail-fast boundary behavior and no-hardcoded-values risk. The GitHub mint path fails closed before privileged token minting when owner OAuth/repo access/repo id checks fail; bearerless local VM-agent credential exchange is now constrained to the primary workspace only, preventing local cross-workspace token pivoting on warm nodes. Unknown repository providers now fail closed rather than silently bypassing GitHub owner-intersection checks.
Staging Verification (2026-06-08)
GraphQL: Could not resolve to a Repository; no hono issue/PR URL appeared.