feat(sandbox): user-provided git submodule credentials per virtual MCP#4885
Merged
Conversation
pedrofrxncx
approved these changes
Jul 21, 2026
Private git submodules live in repos the main clone's per-repo GitHub App token can't reach, so a sandbox clone leaves them empty. Let users supply a per-host PAT (stored as a vault secret) on the virtual MCP; Studio resolves it on every SANDBOX_START and hands the token to the daemon on a git-only channel so `git submodule update` can authenticate. - Daemon writes a short-lived .git-credentials file (0o600, outside the repo) read by credential.helper=store for the submodule fetch, then deletes it. The token never enters the env bag the dev server sees, never appears in argv, and is never persisted into the repo's .git/config. `insteadOf` rewrites (no token) turn git@host:/ssh:// submodule URLs into HTTPS so the store credential applies. - Opt-in and best-effort: no-op without credentials or a .gitmodules; a fetch failure warns and leaves the working tree intact rather than failing clone. - UI: a "Submodule credentials" editor in the virtual MCP Sandbox card, reusing the secret picker + create-secret dialog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multi-perspective review (correctness, security, scope, architecture, testing, performance) surfaced 3 blockers + hardening. Fixes: Blockers - Security: the resolved PAT was echoed verbatim by GET/PUT /_sandbox/config, which is browser-reachable via sandbox-proxy — turning any org-scoped secret into a plaintext read for any member with VM-edit + start. Redact git.repository.submoduleCredentials from both the read handler and the apply echo (daemon keeps them in-memory for the clone), mirroring how env values never leave the daemon. Regression tests added. - user-desktop provider silently dropped the credentials: the link daemon's postConfig cherry-picked only cloneUrl+branch. Add submoduleCredentials to RepoRef and forward it so the default linked-dev path works. - Best-effort contract was not enforced against throws: a credentials-file write (EACCES/ENOSPC) would reject spawnClone and fail the whole clone. Wrap runSubmoduleUpdate in try/catch (warn + continue); rm always runs. Hardening - Single-source the host regex: export SUBMODULE_HOST_RE from mesh-sdk, enforce it in SubmoduleCredentialSchema, import it in the UI (daemon keeps its copy with a sync comment — it can't depend on mesh-sdk). - Extract shared secret-picker primitives (SecretPickerValue, ScopeIcon, SECRET_NAME_RE) into runtime-card/secret-picker.tsx; both field editors use it. - Tests: build-config-payload pass-through (+ first non-null-repo case) and spawnClone submodule integration (best-effort failure, invalid-host skip, no-.gitmodules no-op). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rite fails Verification of the remediation flagged the write/spawn-throw catch branch in runSubmoduleUpdate as untested (only the non-zero-exit branch was covered). Add a case that points the credentials-file path at a missing dir so writeFile throws ENOENT, proving the clone still succeeds and warns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
guitavano
force-pushed
the
guitavano/init-zee-submodule
branch
from
July 23, 2026 11:24
8334b4f to
c086ee6
Compare
decocms Bot
pushed a commit
that referenced
this pull request
Jul 23, 2026
PR: #4885 feat(sandbox): user-provided git submodule credentials per virtual MCP Bump type: minor - decocms (apps/mesh/package.json): 4.104.0 -> 4.105.0 - @decocms/mesh-sdk (packages/mesh-sdk/package.json): 1.30.0 -> 1.31.0 - @decocms/sandbox (packages/sandbox/package.json): 1.18.3 -> 1.19.0 - deploy/helm/sandbox-env (chart 0.9.11) (deploy/helm/sandbox-env/values.yaml deploy/helm/sandbox-env/Chart.yaml): image.tag/appVersion -> 1.19.0 Deploy-Scope: both
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
Private git submodules live in repositories the main clone's per-repo GitHub App token can't reach (a different repo/org), so today a sandbox clone leaves them empty and the dev server fails with
module not found. This lets a user supply a per-host PAT (stored as a vault secret) on the virtual MCP; Studio resolves it on everySANDBOX_STARTand hands the token to the daemon on a git-only channel sogit submodule updatecan authenticate.How it works
envbag the dev server sees, never appears in argv, and is never persisted into the repo's.git/config. It lives only in a short-lived credentials file (mode0o600, next to the no-op askpass, outside the repo) read bycredential.helper=store, then deleted.insteadOfrewrites (which carry no token) turngit@host:/ssh://git@host/submodule URLs into HTTPS so the store credential applies — covering SSH-form submodules likegit@github.com:ZeeDog/zee.git..gitmodules; a fetch failure warns and leaves the working tree intact rather than failing the whole clone.UI
A "Submodule credentials" editor in the virtual MCP Sandbox card (below Environment variables), reusing the existing secret picker + create-secret dialog. Each row =
{ host, secret }. Incomplete rows are stripped from the autosave payload.Testing
bun run check(all workspaces) ✓ ·bun run lint0 errors ✓ ·knipclean ✓ ·bun run fmt✓clone.test.ts(+7 — host validation, dedupe, token percent-encoding, insteadOf/helper argv, no-token-in-argv),helpers.test.ts(+3 — defensive metadata reader).start.test.ts101/101, config-store/payload 47/47.ZeeDog/zee, addgithub.com→ that secret in the virtual MCP UI, then start the sandbox.Notes for reviewers
submoduleCredentialsrides throughensureOpts.repo(preserved bywithFreshCloneUrl), so autonomous recovery re-clones with submodules. Cost: the long-lived PAT is persisted insandbox_runner_statealongside thecloneUrltoken (which is short-lived ~1h by design). If we'd rather not persist it, we can strip it and accept that recovery drops submodules until the nextSANDBOX_START— happy to change.x-access-tokenis correct for GitHub (the target case). GitLab accepts it; Bitbucket may need a different username — extensible later.Out of scope (agreed)
Native SSH deploy keys · active token rotation · per-host username.
🤖 Generated with Claude Code
Summary by cubic
Adds per-host submodule PATs to virtual MCPs so sandboxes can fetch private submodules during clone. Tokens are resolved at start, forwarded to the daemon over a git-only channel, and never exposed to the app or config APIs.
New Features
SecretPickerextracted for consistency.{host, token}; missing or inaccessible secrets are skipped (best-effort). Forwarded through the link daemon/provider so linked desktop sandboxes receive them..git-credentialsfile, applies SSH→HTTPSinsteadOfrewrites, runs a shallow recursivegit submodule update, then deletes the file. Tokens are percent-encoded, never placed in env/argv, and never persisted in repo config.SUBMODULE_HOST_REin@decocms/mesh-sdkand enforced in the schema/UI; invalid hosts are skipped and hosts are deduped.submoduleCredentialsadded to SDK and provider types and forwarded in the config payload.Bug Fixes
/_sandbox/configresponses; tokens remain only in the daemon’s in-memory store..gitmodulesno-op), and the credentials-file write failure catch.Written for commit c086ee6. Summary will update on new commits.