Skip to content

feat(sandbox): broker SCM credentials for git operations#679

Open
hreiten wants to merge 2 commits into
ColeMurray:mainfrom
watchdog-no:fix/sandbox-scm-credential-helper
Open

feat(sandbox): broker SCM credentials for git operations#679
hreiten wants to merge 2 commits into
ColeMurray:mainfrom
watchdog-no:fix/sandbox-scm-credential-helper

Conversation

@hreiten
Copy link
Copy Markdown
Contributor

@hreiten hreiten commented May 23, 2026

Why

We hit this in our own Open-Inspect deployment: long-running or revisited sessions could no longer run git commands after the GitHub App installation token expired. The common workflow was to kick off an agent, come back more than an hour later, and continue the same session. At that point follow-up fetch/push/ls-remote operations were still using the token captured when the sandbox was created, so git failed even though the session itself was otherwise healthy.

What changed

  • Add a sandbox-authenticated POST /sessions/:id/scm-credentials broker endpoint that mints fresh SCM credentials through the configured provider.
  • Add provider-owned credential-helper auth for GitHub and GitLab, including GitHub installation token expiry metadata.
  • Install a sandbox git credential helper in Modal and Daytona images so git fetch/push/clone/LFS operations request fresh credentials on demand.
  • Stop injecting static git tokens into fresh interactive sandboxes. Keep explicit fallback behavior only for image-build, legacy snapshot, and repo-image compatibility paths.
  • Rewrite stale credentialed origin remotes to plain HTTPS URLs before fetch so older snapshots route through the helper.
  • Add a gh wrapper for GitHub CLI calls, since gh reads env tokens rather than git's credential protocol.

Security and compatibility

The helper fails closed by scoping credential responses to HTTPS, the configured SCM host, and the current session repository path, with Git LFS path support. It caches credentials locally with a short refresh buffer and 0600 permissions, and it refuses stale-cache fallback on broker failures. Live sessions also refuse to fall back to static VCS_CLONE_TOKEN when control-plane context is present but incomplete.

Validation

  • npm test -w @open-inspect/control-plane
  • npm run test:integration -w @open-inspect/control-plane -- test/integration/scm-credentials.test.ts
  • npm run typecheck -w @open-inspect/control-plane
  • npm run lint -w @open-inspect/control-plane
  • npm run build -w @open-inspect/control-plane
  • cd packages/sandbox-runtime && uv run --extra dev pytest tests/test_git_credential_helper.py tests/test_gh_wrapper.py tests/test_entrypoint_build_mode.py tests/test_entrypoint_urls.py -q
  • cd packages/sandbox-runtime && uv run --extra dev ruff check src tests && uv run --extra dev ruff format --check src tests
  • cd packages/modal-infra && uv run --extra dev pytest tests/test_sandbox_env_vars.py tests/test_web_api_create_sandbox.py -q
  • cd packages/modal-infra && uv run --extra dev ruff check src tests && uv run --extra dev ruff format --check src tests
  • python3 -m compileall packages/daytona-infra/src

Summary by CodeRabbit

  • New Features

    • Sandboxes now use a git credential-helper flow: short-lived SCM credentials are fetched on demand from the control plane (fresh sandboxes no longer embed long-lived repo tokens).
  • Documentation

    • Security model, token architecture, and how-tos updated to describe brokered credential delivery and legacy fallback behavior.
  • Infrastructure / Runtime

    • Sandbox images and startup now include and configure a credential-helper and GH wrapper to obtain/refetch credentials at runtime.
  • Tests

    • Expanded integration and unit tests covering credential brokering, provider behaviors, and sandbox boot modes.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5269b0f5-0e01-4bc8-97a9-17a37fad6d37

📥 Commits

Reviewing files that changed from the base of the PR and between 8f52cb7 and 17123c2.

📒 Files selected for processing (4)
  • packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py
  • packages/sandbox-runtime/tests/test_entrypoint_build_mode.py
  • packages/sandbox-runtime/tests/test_gh_wrapper.py
  • packages/sandbox-runtime/tests/test_git_credential_helper.py

📝 Walkthrough

Walkthrough

Refactors git auth to a credential-helper flow: control plane exposes POST /sessions/:id/scm-credentials; providers implement generateCredentialHelperAuth(); sandboxes obtain short-lived per-request credentials via a system git credential helper and a GH CLI wrapper; clone-token embedding is removed (legacy fallbacks preserved).

Changes

Git Credential Helper Brokering

Layer / File(s) Summary
Credential types and control-plane contracts
packages/control-plane/src/source-control/types.ts, packages/control-plane/src/session/contracts.ts
Adds CredentialHelperAuth and registers internal path /internal/scm-credentials.
Control-plane SCM credentials service
packages/control-plane/src/session/scm-credentials-service.ts, packages/control-plane/src/session/scm-credentials-service.test.ts
Implements ScmCredentialsService.getCredentials() calling provider generateCredentialHelperAuth(), validating payloads, and mapping provider errors to HTTP statuses.
Control-plane routing and handler integration
packages/control-plane/src/router.ts, packages/control-plane/src/router.scm-credentials.test.ts, packages/control-plane/src/session/http/handlers/sandbox.handler.ts, packages/control-plane/src/session/http/routes.ts
Adds POST /sessions/:id/scm-credentials, provider gating, and handler that forwards to session DO internal scmCredentials endpoint.
GitHub provider credential helper auth
packages/control-plane/src/source-control/providers/github-provider.ts, packages/control-plane/src/auth/github-app.ts, tests
Adds generateCredentialHelperAuth() using cached installation token+expiry; refactors installation-token caching and error/status propagation.
GitLab provider credential helper auth
packages/control-plane/src/source-control/providers/gitlab-provider.ts, tests
Adds generateCredentialHelperAuth() returning oauth2/PAT with hourly TTL; trims/validates token at construction.
Source control provider factory
packages/control-plane/src/source-control/provider-from-env.ts, packages/control-plane/src/source-control/index.ts, packages/control-plane/src/source-control/provider-from-env.test.ts, packages/control-plane/src/routes/shared.ts
New createSourceControlProviderFromEnv(env) centralizes provider wiring and is re-exported.
Session Durable Object SCM wiring
packages/control-plane/src/session/durable-object.ts
Wires ScmCredentialsService, adds internal scmCredentials route, replaces inline clone-token wiring with env-driven provider factory, removes Daytona clone-token callback.
Daytona provider token-embedding removal
packages/control-plane/src/sandbox/providers/daytona-provider.ts, tests
Removes getCloneToken constructor param and stops embedding VCS_CLONE_TOKEN/GitHub tokens into sandbox env vars; updates factory/constructor and tests.
Sandbox HTTP handler SCM endpoint
packages/control-plane/src/session/http/handlers/sandbox.handler.ts, test
Adds scmCredentials() handler that validates session, calls getScmCredentials(), and returns credentials with Cache-Control: no-store.
Session internal routes
packages/control-plane/src/session/http/routes.ts, test
Registers internal POST route for SessionInternalPaths.scmCredentials.
Source control error classification
packages/control-plane/src/source-control/errors.ts
Broader transient classification: treat any 5xx (500–599) as transient (plus 429).
Sandbox git credential helper implementation
packages/sandbox-runtime/src/sandbox_runtime/credentials/git_credential_helper.py, packages/sandbox-runtime/src/sandbox_runtime/credentials/__init__.py, tests
Adds Python git credential helper implementing get/token actions, strict host/repo authorization, on-disk cache with TTL and file-locking, control-plane refresh via POST, and image-build fallback to VCS_CLONE_TOKEN when appropriate.
Sandbox entrypoint & GH wrapper
packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py, tests
Installs credential-helper shim and GH wrapper, configures git global credential helper and useHttpPath, builds plain HTTPS repo URLs (no embedded secrets), rewrites origin to plain URL, and redacts git stderr.
Modal infra sandbox token policy and APIs
packages/modal-infra/src/sandbox/manager.py, packages/modal-infra/src/web_api.py, tests
Distinguishes fresh boots (no token env injection) from prebuilt/snapshot boots (inject clone_token with fallback marker); api_create_sandbox resolves clone_token only for legacy boot paths.
Infrastructure image updates
packages/daytona-infra/src/toolchain.py, packages/modal-infra/src/images/base.py
Bumps sandbox image tags/cache busters and installs/configures oi-git-credentials shim in images.
Documentation and contribution guidance
README.md, docs/HOW_IT_WORKS.md, docs/adr/0001-single-provider-scm-boundaries.md, docs/provider-contribution-checklist.md, packages/control-plane/README.md, packages/github-bot/README.md
Updates Security Model, HOW_IT_WORKS, ADR and provider checklist to document credential-helper brokering, provider responsibilities, and sandbox prerequisites.
Extensive tests
many test files under packages/control-plane, packages/sandbox-runtime, packages/modal-infra
Adds unit, integration, and concurrency tests covering provider factories, routing, session handling, credential helper behavior, GH wrapper, env-var injection behavior, and image/manager flows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ColeMurray

Poem

🐇 I broker creds in a hop and twirl,

Fresh tokens for each little swirl.
Helpers fetch when git asks "please",
Secrets no longer tucked in keys.
Hooray — secure cloning for every swirl!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py`:
- Around line 62-65: The current entrypoint wrapper condition gates all token
checks behind OI_GITHUB_TOKEN_IS_FALLBACK != "1", which prevents
GITHUB_APP_TOKEN from being treated as a real override when fallback is set;
update the shell conditional around the exec of REAL_GH so that GITHUB_APP_TOKEN
is accepted regardless of OI_GITHUB_TOKEN_IS_FALLBACK (i.e., execute REAL_GH if
GITHUB_APP_TOKEN is set OR if OI_GITHUB_TOKEN_IS_FALLBACK != "1" and either
GH_TOKEN or GITHUB_TOKEN is set), leaving the exec "$REAL_GH" "$@" call intact.
- Around line 230-245: In _ensure_credential_helper_configured, the code sets
git's "credential.helper" even when writing the shim (shim_path.write_text with
shim_body) fails; change the flow so the credential.helper is only configured
when the shim actually exists/wrote successfully: after attempting to write and
chmod shim_path (inside the try/except around shim_path.write_text/chmod) set a
local success flag (e.g., shim_written) or re-check shim_path.exists() and only
proceed to set ("credential.helper", str(shim_path)) when that flag/check is
true; ensure the except block does not fall through to configure
credential.helper pointing at a non-existent shim and keep logging the original
warning (self.log.warn("credential_helper.shim_write_failed", error=str(e))).

In `@packages/sandbox-runtime/tests/test_git_credential_helper.py`:
- Around line 85-590: Tests repeat raw timing literals (3600, 60, 0.1); add
module-level named constants (e.g. ONE_HOUR_SECONDS = 3600, ONE_MINUTE_SECONDS =
60, SHORT_SLEEP_SECONDS = 0.1) and replace the numeric literals used in tests
such as test_get_returns_credentials_on_success,
test_refreshes_when_cache_within_expiry_buffer, test_uses_cache_within_buffer,
test_concurrent_invocations_share_one_refresh (slow_handler sleep),
test_control_plane_response_invalid_expiry_is_fatal,
test_token_action_prints_bare_token, and any other occurrences so all
timeouts/TTLs use the new constants to encode units consistently.
- Around line 551-568: The problem is concurrent per-thread patching of
module-global stdio in run_one(), which can clobber patches when two threads
run; remove the patch.object(helper.sys, "stdin"/"stdout"/"stderr") calls from
inside run_one() and instead apply a single patch around the threaded section
(wrap the t1/t2 start/join block) so the global sys streams are patched once, or
alternatively provide thread-local I/O to helper.main without modifying
helper.sys; update the test_concurrent_invocations_share_one_refresh test to
call run_one() without patching helper.sys and perform any required
sys.stdin/stdout/stderr setup outside the threads (or via thread-local
redirect_context) using the unique symbols run_one(), helper.sys.stdin,
helper.sys.stdout, helper.sys.stderr, and helper.main.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 054d978c-199b-42a9-ae48-0d5e505331e1

📥 Commits

Reviewing files that changed from the base of the PR and between 90473af and 8f52cb7.

📒 Files selected for processing (44)
  • README.md
  • docs/HOW_IT_WORKS.md
  • docs/adr/0001-single-provider-scm-boundaries.md
  • docs/provider-contribution-checklist.md
  • packages/control-plane/README.md
  • packages/control-plane/src/auth/github-app.test.ts
  • packages/control-plane/src/auth/github-app.ts
  • packages/control-plane/src/router.scm-credentials.test.ts
  • packages/control-plane/src/router.ts
  • packages/control-plane/src/routes/shared.ts
  • packages/control-plane/src/sandbox/providers/daytona-provider.test.ts
  • packages/control-plane/src/sandbox/providers/daytona-provider.ts
  • packages/control-plane/src/session/contracts.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.test.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.ts
  • packages/control-plane/src/session/http/routes.test.ts
  • packages/control-plane/src/session/http/routes.ts
  • packages/control-plane/src/session/scm-credentials-service.test.ts
  • packages/control-plane/src/session/scm-credentials-service.ts
  • packages/control-plane/src/source-control/errors.ts
  • packages/control-plane/src/source-control/index.ts
  • packages/control-plane/src/source-control/provider-from-env.test.ts
  • packages/control-plane/src/source-control/provider-from-env.ts
  • packages/control-plane/src/source-control/providers/github-provider.test.ts
  • packages/control-plane/src/source-control/providers/github-provider.ts
  • packages/control-plane/src/source-control/providers/gitlab-provider.test.ts
  • packages/control-plane/src/source-control/providers/gitlab-provider.ts
  • packages/control-plane/src/source-control/types.ts
  • packages/control-plane/test/integration/scm-credentials.test.ts
  • packages/daytona-infra/src/toolchain.py
  • packages/github-bot/README.md
  • packages/modal-infra/src/images/base.py
  • packages/modal-infra/src/sandbox/manager.py
  • packages/modal-infra/src/web_api.py
  • packages/modal-infra/tests/test_sandbox_env_vars.py
  • packages/modal-infra/tests/test_web_api_create_sandbox.py
  • packages/sandbox-runtime/src/sandbox_runtime/credentials/__init__.py
  • packages/sandbox-runtime/src/sandbox_runtime/credentials/git_credential_helper.py
  • packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py
  • packages/sandbox-runtime/tests/test_entrypoint_build_mode.py
  • packages/sandbox-runtime/tests/test_entrypoint_urls.py
  • packages/sandbox-runtime/tests/test_gh_wrapper.py
  • packages/sandbox-runtime/tests/test_git_credential_helper.py

Comment thread packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py
Comment thread packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py Outdated
Comment thread packages/sandbox-runtime/tests/test_git_credential_helper.py
Comment thread packages/sandbox-runtime/tests/test_git_credential_helper.py
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