feat(craft): sandbox outputs manifest - #14325
Conversation
Greptile SummaryThe PR adds a bounded, descriptor-relative manifest of sandbox output files and exposes it through Docker exec and the signed Kubernetes sidecar.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Sequence DiagramsequenceDiagram
participant C as Reconciler
participant M as SandboxManager
participant D as Docker exec
participant S as Kubernetes sidecar
participant W as Manifest walker
participant F as Outputs filesystem
C->>M: get_outputs_manifest(sandbox_id, session_id)
alt Docker backend
M->>D: python -E -s -m sandbox_daemon.manifest
D->>W: build_outputs_manifest(session_id)
else Kubernetes backend
M->>S: Signed POST /filesystem/outputs-manifest
S->>W: build_outputs_manifest(session_id)
end
W->>F: fd-relative walk with O_NOFOLLOW
F-->>W: file descriptors and metadata
W-->>M: entries, skip counters, truncated flag
M-->>C: OutputsManifestResponse
Reviews (3): Last reviewed commit: "refactor(craft): name the hashed-file re..." | Re-trigger Greptile |
|
Full-stack Preview (frontend + backend)
|
🖼️ Visual Regression Report
|
One daemon call describes a session's outputs tree: regular files with size, mtime, and sha256, directories, nothing else. Symlinks and special files are counted and never followed. Every descent below the anchor is fd-relative with O_NOFOLLOW. Served over the signed sidecar route on kubernetes and via exec python -m on docker. The docker exec runs the root-owned system interpreter (with its own pydantic) against a root-owned /opt copy of the daemon, so the sandbox user cannot swap the code that describes its outputs. Also pins the labs BuildKit syntax the Dockerfile's COPY --exclude requires, so local builds work without flag overrides.
A directory listing that fails after its open, or a child that vanishes between scandir and stat, previously dropped content silently. Both now increment skipped_unreadable so a partial manifest is distinguishable from a complete one.
8eb7827 to
3003e2a
Compare
| _MISSING_ERRNOS = frozenset({errno.ENOENT, errno.ENOTDIR, errno.ELOOP}) | ||
|
|
||
|
|
||
| @dataclass |
There was a problem hiding this comment.
the wire model is the OutputsManifestResponse inside it, the wrapper only exists to carry the hash budget countdown next to it while the recursion runs. dataclass felt like the right signal that it's walk-internal state, not a contract
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| print(build_outputs_manifest(UUID(sys.argv[1])).model_dump_json()) |
There was a problem hiding this comment.
yep, that's the docker transport. docker_sandbox_manager.get_outputs_manifest execs python -m sandbox_daemon.manifest in the container and parses this stdout (k8s uses the sidecar route instead)
|
|
||
| def _hash_regular_file( | ||
| dir_fd: int, name: str, allowed_bytes: int | ||
| ) -> tuple[os.stat_result, str | None, int] | None: |
There was a problem hiding this comment.
Worth making this a named type?
There was a problem hiding this comment.
yeah fair, took it in 49af94d as a _HashedFile NamedTuple, docstring got shorter too
Review asked twice for a named type over the anonymous three-tuple. The NamedTuple carries the meaning the docstring used to spell out.
Description
Why. The artifact index (PR1, #14319) is only useful if something can fill it, and the reconciler that will (PR4) needs a trustworthy answer to "what is in this session's outputs tree right now". Nothing provides that today:
list_directorywalks one level with no hashes, and the sandbox agent mutates the filesystem concurrently, so a naive pathname walk can be routed anywhere by a symlink swapped in mid-walk. This PR gives the daemon one call that describes the whole outputs tree, hardened against the agent it shares a filesystem with.What.
sandbox_daemon/manifest.py: a bounded, fd-relative walk ofsessions/{sid}/outputs. Every descent below the anchor isO_NOFOLLOW, file metadata comes from fstat on the very descriptor being hashed, so a swapped path can only fail an open, never route the walk or the hash outside outputs. Symlinks and special files are counted, never followed. Ceilings on entries, depth, per-directory children, per-file hash bytes, and a walk-wide hash budget; the response carries skip counters and atruncatedflag so a partial listing is always distinguishable from a complete one.python -m sandbox_daemon.manifestover exec on docker. The docker exec runs the root-owned system interpreter (-E -s, with its own pinned pydantic) against a root-owned/optcopy of the daemon, because the sandbox user owns/workspaceand could otherwise swap the code that describes its outputs.SandboxManager.get_outputs_manifeston both backends and the test stub.COPY --excluderequires, so local builds work without flag overrides.The manifest, concretely — every regular file and directory under outputs/, recursively, paths outputs-relative and sorted. Files carry size, mtime (fstat on the hashed descriptor), and sha256, null past the hash ceilings; directories carry mtime only. The four trailers are the honesty signals, and the reconciler refuses to act unless
truncatedis false andskipped_unreadableis zero:{ "entries": [ {"path": "deck.pptx", "is_directory": false, "size": 48211, "mtime_ns": 1787795635366373007, "sha256": "5891b5b5..."}, {"path": "web", "is_directory": true, "size": null, "mtime_ns": 1787795635366373007, "sha256": null}, {"path": "web/package.json", "is_directory": false, "size": 412, "mtime_ns": 1787795635366373007, "sha256": "b53a5538..."} ], "skipped_symlinks": 1, "skipped_special": 0, "skipped_unreadable": 0, "truncated": false }Nothing calls
get_outputs_manifestyet; the consumer is the reconciler in #14327.How Has This Been Tested?
test_outputs_manifest.py): files/directories described with sizes and hashes, hash tracks content, symlinks skipped and never followed (including a session directory swapped for a symlink yielding an empty manifest), special files skipped, entry-cap truncation flagged, oversize files listed without a hash, an unreadable directory counted rather than silently omitted, and the fullsessions/{sid}/outputsdescent.docker exec -u 1000:1000 -w /opt ... /usr/local/bin/python3 -E -s -m sandbox_daemon.manifest <sid>) — correct manifest JSON, planted symlink skipped, exit 0. This caught and fixed a real bug:/usr/bin/python3does not exist in the image and the system interpreter had no pydantic, so the exec path now installs a root-owned pydantic and uses the real interpreter path.Additional Options