feat(sdk): add strict Dockerfile sandbox launch - #16
Conversation
4412924 to
445cecf
Compare
tianyuzhou95
left a comment
There was a problem hiding this comment.
Thanks for putting together the RFC and the reference implementation. The overall direction is useful, and the submitted unit tests, Ruff, and mypy checks pass locally. However, the current implementation has several correctness and security issues that need to be addressed before merge.
Blocking findings
-
SDK-side
ADD URLcreates an SSRF boundary violation._dockerfile_runner.pylines 234-244 downloads remote URLs withurllibin the SDK process and then uploads the response into the sandbox. An agent-supplied Dockerfile can therefore read loopback, link-local, cloud metadata, or private-network endpoints reachable from the SDK host and retrieve the response from inside the sandbox. Restricting redirect schemes does not address the host-level SSRF. The fetch should happen inside the sandbox, or remoteADDmust be an explicit trusted-input-only opt-in with an enforceable network policy. -
The build context bypasses
.dockerignoreand does not implement consistent Docker context semantics._dockerfile_runner.pylines 246-275 passes a local directory directly tocopy_from_local, soCOPY . ...can upload files such as credentials,.env, or.giteven when they are excluded by.dockerignore.DockerContext.walk()is not used, and non-local contexts cannot correctly support directories or wildcards. Please build a filtered context manifest, apply.dockerignore, expand source patterns, and use the same behavior for local and remote contexts. -
Valid Dockerfile syntax is accepted as directly launchable but executed incorrectly.
_dockerfile.pylines 433-487 parses JSON-formCOPY/ADDwithshlex, soCOPY ["a b", "/dest/"]becomes source"[a b,"and destination"/dest/]". Exec-formRUNis also treated as a shell string, and flags such as--chmodand--linkare silently discarded. In addition,_dockerfile_runner.pylines 129-139 resets relativeWORKDIRvalues instead of resolving them against the previous directory and ignoresARG, even though build arguments affectFROM, later instructions, andRUN. Unsupported syntax must either be implemented or rejected bycheck_direct_launch; it must not return success and then run with different semantics. -
The evaluator does not inherit the base image configuration. It initializes build state as empty environment,
/, and root, and resolvesCMD/ENTRYPOINTonly from instructions in the current Dockerfile. A Dockerfile containing onlyFROM nginx, for example, is reported as launchable but does not start the inherited nginx command; inheritedUSER,WORKDIR,CMD, andENTRYPOINTare likewise lost. This needs OCI image-config plumbing or a narrower explicitly documented contract. Without it, the feature cannot claim that the resulting sandbox behaves like the Dockerfile. -
The documented warning policy is silent in the main API.
sandbox.pylines 292-303 discards theDockerfileApplyResult, so warnings for ignored instructions never reach callers ofSandbox(context=...). Unknown instructions also remain warnings understrict=True. Please reject unsupported behavior by default, or emit/expose the warnings and make strict mode consistently reject every ignored instruction. -
CMD/ENTRYPOINTresolution and readiness do not match the stated contract._dockerfile.pylines 154-167 drops a shell-formCMDwhen paired with an exec-formENTRYPOINT, whereas Docker appends/bin/sh -c .... The launcher also infers shell form from whether a single argument contains a space, which breaks valid one-element exec-form commands. Finally, the readiness check only confirms that the sandbox is alive before starting the background command, and the returned process handle is discarded, so an immediately failing application can still produce a successful constructor. Please preserve the parsed command form explicitly and either define an application-start check or describe this as sandbox readiness rather than application readiness.
I recommend revising the RFC around a strict, trusted Dockerfile subset and making unsupported constructs fail closed. The PR also needs to be rebased onto the current backend-neutral SDK architecture before these changes can be integrated.
Add a backend-neutral direct-launch path for a deliberately strict Dockerfile subset. The FROM image supplies only the sandbox root filesystem; explicitly declared RUN, COPY, ADD, ENV, WORKDIR, USER, CMD, and ENTRYPOINT behavior is applied through the public sandbox facades without BuildKit, a Docker daemon, or a registry push. Validate Dockerfiles and build contexts before remote side effects. Unsupported syntax fails closed, remote ADD URLs are rejected, and a filtered manifest applies .dockerignore consistently to local and custom contexts. Local paths use no-follow directory-relative opens, all COPY and ADD inputs are materialized before sandbox operations, tar members are restricted to safe regular files and directories, and ownership changes are limited to outputs of the current instruction. Expose the background CMD or ENTRYPOINT CommandHandle while defining constructor success as sandbox readiness and successful dispatch rather than application health. Integrate cleanup with the backend-neutral BackendSession lifecycle and document the rootfs-only and no-snapshot contract. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
445cecf to
4f2a35e
Compare
|
Thanks for the detailed review. I addressed each blocking finding in commit 1. SDK-side
|
What changed
This PR adds a backend-neutral, strict Dockerfile direct-launch path to the
Python SDK.
Sandbox(context=...)uses the existing publicSandbox,Commands, andFilesystemfacades: it parses one Dockerfile, creates asandbox from
FROM, applies the explicit supported instructions, and exposesthe optional background startup command as
sandbox.startup_command.The branch has been rebased onto the backend-neutral upstream
mainarchitecture. The implementation keeps backend-native conversions behind the
existing boundary rather than coupling Dockerfile launch to either backend.
The public surface includes
DockerContext,LocalDockerContext,parse_dockerfile,check_direct_launch,apply_dockerfile, typed parse/checkand apply results, and constructor options
context,auto_start_cmd, andbuild_run_timeout. The SDK README and maintainedsdk/python/examples/dockerfile_launch.pynow document and exercise thecurrent contract.
Why
Small single-stage Dockerfiles are a common way to describe a development
environment. Direct launch supports a narrow, auditable subset without
requiring BuildKit, a Docker daemon, or a registry push. It is intentionally
not a Docker build replacement: every launch reruns supported
RUN,COPY,and
ADDinstructions and creates no cache or snapshot.Supported contract
FROMis deliberately rootfs-only. The base image supplies the sandbox rootfilesystem, while its OCI
ENV,USER,WORKDIR,CMD, andENTRYPOINTconfiguration is not inherited. The Dockerfile must state required runtime
settings explicitly.
The supported subset is exactly one literal
FROMwith optionalASalias;shell-form
RUN; shell-form localCOPY/ADDsources for files, directories,., and wildcards; literal--chown; literal local tar extraction; literalENV; absoluteWORKDIR; literalUSER;EXPOSEmetadata; and exec- orshell-form
CMD/ENTRYPOINTwith normalized OCI-style argv combination.Multi-stage input,
COPY --from,ARG, remoteADDURLs, JSONCOPY/ADD,exec-form
RUN, relativeWORKDIR, build-time variable expansion, unsupportedflags, and unsupported instructions are rejected. Execution paths fail closed:
Sandbox(context=...)parses strictly,check_direct_launch()returnsFalsewith reason codes, and
apply_dockerfile()rejects parsed unsupported items.Non-strict parsing is diagnostics only.
After instructions complete, the SDK polls sandbox readiness and dispatches the
resolved startup argv in the background. A non-
NoneCommandHandleconfirmsdispatch, not process longevity or application health. Callers can
wait()orkill()the handle and own their application-specific health check. Dockerfileswithout a startup command, or launches with
auto_start_cmd=False, exposeNone.Security and correctness
The context implementation validates the complete manifest and materializes all
COPY/ADDinputs before any sandbox operation. It applies root.dockerignore, filters reserved Dockerfile metadata, rejects local symboliclinks, and validates paths and collisions. Local tar extraction accepts only
regular files and directories with safe paths.
--chownis literal, quoted,and limited to outputs created by the current instruction. Remote
ADDisrejected by design, avoiding network retrieval and host-side SSRF.
For unsupported Dockerfiles or build-once reuse, callers externally pre-build
an image, use
Sandbox(image=...), and explicitly launch the desired command.The SDK does not auto-start image configuration on that path.
Testing
The current unit gate contains 196 tests and passes with Ruff and mypy. It
covers strict parser behavior, startup argv resolution and cleanup, context and
.dockerignorefiltering, pre-upload manifest materialization, collisions,path validation, local tar traversal, ownership validation, and remote
ADDrejection. The maintained example adds independent sections for filtered
COPY ., finiteCMDandENTRYPOINT+CMDstartup handles,COPY --chown,local tar
ADD, and a no-sandbox fail-closed precheck.Real-sandbox follow-up
The maintained five-section example passed against a real standalone
runscdeployment through the backend-neutral facade using
openyuanrong-sdk:COPY .honored.dockerignore; the ignored secret was absent;RUN/ENV/WORKDIR/USERstate reached a finite CMD, and itsstartup_command.wait()completed successfully;ENTRYPOINT+CMDmerged and produced the expected marker;COPY --chownand local tarADDproduced the expected owner and file tree;ADDwas rejected during pre-check without creating a sandbox.Additional live regressions placed pre-existing root-owned files in both COPY
and tar destinations. Those files remained
root:root; only files anddirectories created by the current instruction became
myuser:myuser.The same code is backend-neutral and has unit coverage for both backend
sessions. The default
openyuanrong-sandboxbackend could not be runtime-testedwith the current public standalone image: it lacks the backend's
/api/sandbox/v1route and returns HTTP 404 before sandbox creation.Licensing
The parser dependency is
dockerfile-parse(BSD-3-Clause); context matchinguses
pathspec(MIT). The value-level parsing approach was informed by the E2BPython SDK (MIT). No Docker engine, BuildKit, or registry component is added.