Repository navigation
Conversation
Managed Agents draws two different boundaries. Exec is unconfined, so a command can write anywhere in the guest, but file transfer resolves only under /workspace and answers 403 "path outside workspace" for anything else. Shared backend code assumes /tmp is a safe place for a scratch file, which holds for every other backend and breaks here the moment the file has to be transferred rather than merely written. Two paths were broken. prepareExecutionFiles stages per-command credentials in a /tmp directory and uploads each file into it, so any command needing a credential failed. createExecExport tars the computer into /tmp and reads the archive back, so export failed. Backends now declare where their scratch files belong. scratchDir defaults to /tmp, leaving the other backends byte-identical, and Managed Agents declares /workspace/.qm-scratch, which exec and transfer both see. It sits outside /workspace/home, so credentials still stay out of the home tar and the export. Transfer paths are checked at the backend's own chokepoint as well, so a caller that reaches outside /workspace gets a local error naming the constraint and the offending path instead of a bare remote 403.
The fake answered a transfer for any path by falling through to the host filesystem, so writing /tmp in a test "succeeded" against the machine running the suite while the same call 403s in production. That inversion is why the scratch-directory bug shipped: the double was more permissive than the platform it stands in for. Transfers outside /workspace now fail the way the platform fails them, guest paths map onto a per-session root rather than the host, and command output maps back, so a path printed by mktemp is a guest path the caller can actually transfer. Covers both paths that were broken, that the sweep still finds stale credential directories in the backend's scratch directory, and that a backend declaring no scratch directory still gets /tmp.
Contributor
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.
Fixes the
/tmpvs/workspacefailures reported against thedo-managed-agentsbackend added in #2128.The problem
Managed Agents draws two different boundaries, and they don't agree:
/tmpincluded./workspace. Anything else is refused with403 path outside workspace.Shared backend code assumes
/tmpis a safe place for a scratch file. That assumption holds for every other backend and breaks here the moment the file has to be transferred rather than merely written. Because the scratch file is created by exec, it appears to work; the failure surfaces later, at the upload or download.Two paths were broken:
prepareExecutionFilesstages per-command credentials in a/tmpdirectory and uploads each file into it, so any command needing a credential failed.createExecExporttars the computer into/tmpand reads the archive back, so export failed.Verified against a live sandbox: exec writes and reads
/tmp/probe.txtfine, a transfer of the same path returns 403, a../tmp/...traversal is also refused, and a/workspacepath succeeds.The fix
Backends now declare where their scratch files belong.
scratchDirdefaults to/tmp, so every other backend is byte-identical, and Managed Agents declares/workspace/.qm-scratch— a path exec and transfer both see. It sits outside/workspace/home, so credentials still stay out of the home tar and the export.Transfer paths are validated at the backend's own chokepoint too, so a caller that reaches outside
/workspacegets a local error naming the constraint and the offending path rather than a bare remote 403:This also drops a hardcoded
slice(5)in the credential sweep that assumed the scratch prefix was exactly/tmp/.Why the tests didn't catch it
The fake answered a transfer for any path by falling through to the host filesystem, so writing
/tmpin a test succeeded against the machine running the suite while the same call 403s in production. The double was more permissive than the platform it stands in for, which is exactly how this shipped.The fake now fails outside-
/workspacetransfers the way the platform fails them, maps guest paths onto a per-session root instead of the host, and maps command output back, so a path printed bymktempis a guest path the caller can actually transfer.New tests cover both broken paths, the local guard, the guest refusing outside-
/workspacetransfers, and the stale-credential sweep actually clearing an old credential directory from the scratch directory. Revertingexecute-files.tstomainfails the staging and sweep tests.Verification
managed-agents-sandbox38/38,managed-agents-client26/26,execute-credential-preparation11/11execute-file-credentials15/16 — the one failure needs GNUtouch -dand fails identically onmainfrom macOSFollow-up cleanup
scratchDirFor(profile)). Before, the profile andcreateExecExporteach declared it separately.ManagedAgentsOutsideWorkspaceErroris folded intotransferPath, which throws a plain error that still names the path and the constraint.Live QA
Ran on 2026-10-15 against real DigitalOcean Managed Agents sessions (template
qm-sandbox), using this PR's backend throughcreateSdkManagedAgentsClient. Each session was destroyed afterwards.main(before): credential staging fails with7 PERMISSION_DENIED: path outside workspace: "/tmp/qm-credentials.…/home/.aws/credentials". Export fails the same way on/tmp/agent-computer-export.….This PR at
98c103d4:scratchDirresolves to/workspace/.qm-scratch.A three-hour-old
qm-credentials.*directory planted in the scratch dir is gone afterprepareExecutionFiles(the stale sweep works on the real guest).A file credential stages at
/workspace/.qm-scratch/qm-credentials.<id>/home/.aws/credentials. A command run with the prepared env, the wayexecutemerges it, seesHOMEandAWS_SHARED_CREDENTIALS_FILEpointing there and reads the file back (exit 0).finish()removes the credential directory.exportFiles({ include: ["workspace"] })returns the workspace file with the right contents.End-to-end publish on a dev instance (in-process core from
scripts/api-livetest.ts,SANDBOX_BACKEND=do-managed-agents,DEPLOY_PROVIDER=docker, real model turn): the agent wrote a two-file Node app on the Managed Agents computer and calledappspublish. At this PR the publish returned v1, the container came up, and it served the app's page. Onmainthe same turn fails with[publish failed] do-managed-agents download /tmp/agent-computer-export.… failed: 7 PERMISSION_DENIED: path outside workspace, and no deployment is created.Config matrix and schema
No settings, env vars, or schema changes.
scratchDiris an optional, code-level backend profile property that defaults to/tmp.Wall of shame review report
Reviewed commit:
98c103d428cab0a0d10a17ad06a57c7e0ca2b263Lenses were triaged by claude-haiku-5-5. Each relevant lens was then reviewed by a separate fresh-context claude-opus-5-5 reviewer that read the diff, the full changed files, and this description.
scratchDirprofile field, defaulting to/tmp, threaded into the two broken call sites, with no settings or schema growth. It also removes the brittleslice(5). There is minor flab worth trimming but not blocking: thetransferPathlocal guard duplicates a 403 the platform already returns,scratchDirFor(profile)in managed-agents just re-reads a constant set three lines above, andcreateExecExporttakes a separate defaultedscratchDirparam alongside the profile field./tmpis transferable is replaced by a backend-declaredscratchDir, which bothprepareExecutionFilesandcreateExecExportconsume viascratchDirFor(profile), and the hardcodedslice(5)is removed rather than worked around. ThetransferPathguard rejects bad paths at the backend's single transfer chokepoint instead of adding path rewrites or special-case branches, and the over-permissive fake that hid the bug is corrected rather than patched around.DEFAULT_SCRATCH_DIRandscratchDirFor(profile)in sandbox.ts. Both the credential staging and the managed-agents export read it from the profile instead of declaring/tmptwice, and the hardcodedslice(5)is gone. Two small leftovers:createExecExportkeeps its ownscratchDirdefault rather than taking the profile, and the home-snapshot io still callssession.read/writeFileBytesdirectly instead of going through thetransferPathchokepoint. Neither causes drift today.scratchDirprofile property and a localtransferPathguard, and it keeps using the existing Managed Agents session file APIs (writeFileBytes/readFileBytes) and the shared exec helpers. It does not build any new protocol, queue, or transport, so nothing here duplicates an SDK or library capability. The only custom code is a small path check that mirrors the platform's/workspaceconfinement, plus fixes to the test double, and neither reimplements functionality a maintained integration already provides.scratchDiris a static profile constant, and the existingMaps in managed-agents-sandbox are unchanged. Credential and export scratch files now live in/workspace/.qm-scratch, which survives provider pause at least as well as/tmpdid, and the stale sweep relies on on-disk mtimes rather than in-memory tracking, so there is no new deploy- or crash-fragile state.scratchDiris a code-level backend capability that replaces an implicit hardcoded/tmpassumption and drops theslice(5)prefix hack, so the implicit-mode footprint shrinks. One small redundancy remains:ExecExportDeps.scratchDiris optional and has its ownDEFAULT_SCRATCH_DIRfallback alongsidescratchDirFor. Making it required and always passingscratchDirFor(profile)would leave a single defaulting point.scratchDirprofile property. This follows the same pattern asparksOnTeardown, replacing hidden/tmpassumptions inprepareExecutionFilesandcreateExecExport. Every other backend keeps the byte-identical/tmpdefault, and the fake now refuses outside-/workspacetransfers the way the platform does. Two residual risks remain: a live QA run is still pending, and otherwriteAbsBytescallers such asinstallLayerTools.writeAbswere not audited for non-/workspacetargets, though these would now fail locally with a clear message rather than drift silently.transferPathchecks paths withstartsWithandhasParentPathSegment, and the PR replaces the brittleprefix.slice(5)with structuredscratch/basenamevalues. The only regex growth is in the test fake.GUEST_ROOT_PREFIXuses a lookbehind/lookahead rewrite of/workspaceinside raw shell scripts, extending an existing textual remap. That is fragile but test-only and not a reason to block.transferPathguard) gets one behavioral test, and the PR already dropped tests that re-asserted constants or string-matched the shell script. The one marginal case is "the guest refuses a transfer outside the workspace", which exercises the fake rather than production code. It is cheap, though, and pins the fidelity property whose absence let this bug ship, so it does not warrant a change./workspace/.qm-scratch, read it back with the merged env (exit 0), swept a planted 3h-old directory, confirmedfinish()removed the credential directory, and exported through the scratch tar. Two minor caveats: the claimedmain403 baseline appears in prose, not in the raw output, and the default-backend scripts are not byte-identical. They now addmkdir -p '/tmp'and quote the find root, but that is semantically inert, so it does not block merge.Prod-like QA was reassessed after the live run (see Live QA above). The other lenses carry forward unchanged; the code at this commit has not changed since they were reviewed.