Summary
When using sandbox.files.writeFiles([{ path, data, owner, group }]) with a nested target path whose parent directories do not exist, the uploaded file receives the requested owner / group, but the auto-created parent directories keep the execd process ownership.
This becomes a problem when execd runs as root while the workload operates as a non-root user: the target file is owned by the requested user, but the parent directories can remain root-owned, so later non-root cleanup such as rm -rf can fail with Permission denied.
Expected behavior
Either:
- parent directories auto-created by
/files/upload should receive the same owner / group when upload metadata includes them, or
- the API / SDK docs should explicitly state that
owner / group only applies to the target file, and callers must call createDirectories() first when parent directory ownership matters.
Source evidence
The JavaScript SDK sends owner, group, and mode only as upload metadata and then calls upload:
sdks/sandbox/javascript/src/adapters/filesystemAdapter.ts:564-572
async writeFiles(entries: WriteEntry[]): Promise<void> {
for (const e of entries) {
const meta: FileMetadata = {
path: e.path,
owner: e.owner,
group: e.group,
mode: e.mode,
};
await this.uploadFile(meta, e.data ?? "");
}
}
In execd, upload auto-creates the parent directory before writing the file:
components/execd/pkg/web/controller/filesystem_upload.go:149-150
targetDir := filepath.Dir(resolvedPath)
if err := os.MkdirAll(targetDir, os.ModePerm); err != nil {
Then upload applies the metadata permission only to the resolved target file path:
components/execd/pkg/web/controller/filesystem_upload.go:100-104
components/execd/pkg/web/controller/filesystem_upload.go:214-215
if uerr := writeUploadFile(resolvedPath, fileHeader); uerr != nil {
return uerr
}
return applyUploadPermission(resolvedPath, meta.Permission)
func applyUploadPermission(resolvedPath string, permission model.Permission) *uploadError {
chmodErr := ChmodFile(resolvedPath, permission)
ChmodFile() operates only on the path passed to it:
components/execd/pkg/web/controller/utils.go:60-76
func ChmodFile(file string, perms model.Permission) error {
abs, err := pathutil.ExpandAbsPath(file)
// ...
return SetFileOwnership(abs, perms.Owner, perms.Group)
}
By contrast, /directories is the API that explicitly creates directories with permission objects:
specs/execd-api.yaml:884-890
Minimal reproduction
- Start a sandbox where execd runs as root and the workload user is non-root, for example
agent.
- Use the JS SDK to upload a nested file whose parent directories do not exist:
await sandbox.files.writeFiles([
{
path: "/home/agent/workspace/project/components/ui/toast.tsx",
data: "export const ok = true;\n",
owner: "agent",
group: "agent",
},
]);
- Inspect ownership:
stat -c '%U:%G %a %n' \
/home/agent/workspace/project/components \
/home/agent/workspace/project/components/ui \
/home/agent/workspace/project/components/ui/toast.tsx
Observed result: the target file is agent:agent, but the auto-created parent directories are owned by the execd process user, e.g. root:root.
- Run cleanup as the non-root workload user:
rm -rf /home/agent/workspace/project/components
This can fail with Permission denied because the parent directories are not writable by the workload user.
Why this matters
From the SDK caller's perspective, passing owner / group to writeFiles() appears to make the write safe for that user. In practice, only the leaf file gets fixed, while missing parents can keep root ownership. This is easy to miss until a later replace/delete operation fails.
Summary
When using
sandbox.files.writeFiles([{ path, data, owner, group }])with a nested target path whose parent directories do not exist, the uploaded file receives the requestedowner/group, but the auto-created parent directories keep the execd process ownership.This becomes a problem when execd runs as root while the workload operates as a non-root user: the target file is owned by the requested user, but the parent directories can remain root-owned, so later non-root cleanup such as
rm -rfcan fail withPermission denied.Expected behavior
Either:
/files/uploadshould receive the sameowner/groupwhen upload metadata includes them, orowner/grouponly applies to the target file, and callers must callcreateDirectories()first when parent directory ownership matters.Source evidence
The JavaScript SDK sends
owner,group, andmodeonly as upload metadata and then calls upload:sdks/sandbox/javascript/src/adapters/filesystemAdapter.ts:564-572In execd, upload auto-creates the parent directory before writing the file:
components/execd/pkg/web/controller/filesystem_upload.go:149-150Then upload applies the metadata permission only to the resolved target file path:
components/execd/pkg/web/controller/filesystem_upload.go:100-104components/execd/pkg/web/controller/filesystem_upload.go:214-215ChmodFile()operates only on the path passed to it:components/execd/pkg/web/controller/utils.go:60-76By contrast,
/directoriesis the API that explicitly creates directories with permission objects:specs/execd-api.yaml:884-890Minimal reproduction
agent.stat -c '%U:%G %a %n' \ /home/agent/workspace/project/components \ /home/agent/workspace/project/components/ui \ /home/agent/workspace/project/components/ui/toast.tsxObserved result: the target file is
agent:agent, but the auto-created parent directories are owned by the execd process user, e.g.root:root.This can fail with
Permission deniedbecause the parent directories are not writable by the workload user.Why this matters
From the SDK caller's perspective, passing
owner/grouptowriteFiles()appears to make the write safe for that user. In practice, only the leaf file gets fixed, while missing parents can keep root ownership. This is easy to miss until a later replace/delete operation fails.