Summary
On the inprocess sandbox provider, an attachment that degrades to a workspace copy is written to the runner pod's own disk, while the model's tools read from a different filesystem (the per-conversation Daytona command sandbox and its geesefs mount of the object store). The agent's read then fails with ENOENT and the model tells the user the file "didn't sync into the workspace files".
Reproduced on two independent deployments, so this is not environment specific.
Reproduction
- Create an agent with
harness.kind = pi_core and sandbox.kind = inprocess.
- Attach any image to a chat message and ask the model to read it.
- The delivery record is
outcome=workspace_only, and the agent's read <workingPath> returns ENOENT.
A differential probe created fresh agents and sent the same generated PNG on both stages:
| Deployment |
Sandbox |
Model |
outcome |
agent could read the file |
| AWS staging |
inprocess |
openrouter/deepseek-v4-flash |
workspace_only |
No, ENOENT |
| GKE staging |
inprocess |
vertex_ai/gemini-3.7-flash |
workspace_only |
No, ENOENT |
| AWS staging |
daytona |
openrouter/deepseek-v4-flash |
workspace_only |
Yes, 4402 bytes |
| GKE staging |
daytona |
vertex_ai/gemini-3.7-flash |
workspace_only |
Yes, 4402 bytes |
daytona works on both. inprocess fails on both.
Root cause
materializeWorkingCopy in services/runner/src/engines/sandbox_agent/attachments.ts chooses daytonaMaterialize whenever the sandbox handle exposes mkdirFs / writeFsFile / statFs.
For the inprocess provider that handle is InProcessHarnessHost (services/runner/src/engines/inprocess/harness-host.ts). Its writeFsFile, mkdirFs and statFs use Node fs against the runner process's own disk. The class says so itself:
// ---- Runner files: callers are runner code, never the model; the runner's own disk
The model's tools do not run there. engines/inprocess/conversation-workspace.ts states the design:
All of Pi's tools run in the sandbox, on its mount of the drive; the runner opens no path the model chose and mounts nothing.
and runProcess on that same host object is documented as "the command sandbox, never the runner host". So the attachment is written on one filesystem and read from another.
Verified on a live runner pod: no /dev/fuse, no geesefs process, no FUSE mounts, and the attachment is absent both from the pod's local session directory and from the object store under the session working directory.
Impact
Every non-native attachment is unreadable on inprocess. That includes documents, and images whenever native delivery is unavailable. inprocess is auto-enabled wherever daytona is enabled, so this reaches normal users.
Suggested direction
Materialize the working copy through the command sandbox's filesystem (the store-backed mount) rather than the runner host disk. Either route materializeWorkingCopy to the command sandbox handle, or stop exposing host fs methods on the object the attachment chain receives, so the capability check cannot select the wrong target.
Acceptance
- An
inprocess agent can read an attached file that was delivered as a workspace copy.
- A regression test covers
inprocess specifically. The existing coverage passes because daytona writes to the right place.
Workaround
Set the agent's sandbox to Daytona.
Summary
On the
inprocesssandbox provider, an attachment that degrades to a workspace copy is written to the runner pod's own disk, while the model's tools read from a different filesystem (the per-conversation Daytona command sandbox and its geesefs mount of the object store). The agent'sreadthen fails withENOENTand the model tells the user the file "didn't sync into the workspace files".Reproduced on two independent deployments, so this is not environment specific.
Reproduction
harness.kind = pi_coreandsandbox.kind = inprocess.outcome=workspace_only, and the agent'sread <workingPath>returnsENOENT.A differential probe created fresh agents and sent the same generated PNG on both stages:
daytonaworks on both.inprocessfails on both.Root cause
materializeWorkingCopyinservices/runner/src/engines/sandbox_agent/attachments.tschoosesdaytonaMaterializewhenever the sandbox handle exposesmkdirFs/writeFsFile/statFs.For the
inprocessprovider that handle isInProcessHarnessHost(services/runner/src/engines/inprocess/harness-host.ts). ItswriteFsFile,mkdirFsandstatFsuse Nodefsagainst the runner process's own disk. The class says so itself:The model's tools do not run there.
engines/inprocess/conversation-workspace.tsstates the design:and
runProcesson that same host object is documented as "the command sandbox, never the runner host". So the attachment is written on one filesystem and read from another.Verified on a live runner pod: no
/dev/fuse, no geesefs process, no FUSE mounts, and the attachment is absent both from the pod's local session directory and from the object store under the session working directory.Impact
Every non-native attachment is unreadable on
inprocess. That includes documents, and images whenever native delivery is unavailable.inprocessis auto-enabled whereverdaytonais enabled, so this reaches normal users.Suggested direction
Materialize the working copy through the command sandbox's filesystem (the store-backed mount) rather than the runner host disk. Either route
materializeWorkingCopyto the command sandbox handle, or stop exposing hostfsmethods on the object the attachment chain receives, so the capability check cannot select the wrong target.Acceptance
inprocessagent canreadan attached file that was delivered as a workspace copy.inprocessspecifically. The existing coverage passes becausedaytonawrites to the right place.Workaround
Set the agent's sandbox to Daytona.