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.
445cecf to
4f2a35e
Compare
|
Thanks for the detailed review. I addressed each blocking finding in commit 1. SDK-side
|
tianyuzhou95
left a comment
There was a problem hiding this comment.
Thanks for the thorough update. The six findings from my previous review are
materially addressed by this revision, and the narrower fail-closed contract is
much clearer. I reran the new Dockerfile/context tests and reviewed the updated
RFC against the implementation. The following correctness issues still block
the direct-launch path.
Blocking findings
-
The explicit root working directory is delegated to the backend default.
_dockerfile_runner.pylines 253-257
passescwd=Nonewhenever the accumulated DockerfileWORKDIRis/, and
the startup path repeats this at lines 741-746.Nonemeans "use the backend
default", not/; the actor backend creates a temporary default directory
and resolves a missing cwd to it. Consequently,FROM ubuntufollowed by
RUN pwdruns under/tmp/sandbox_*, and a Dockerfile withoutWORKDIR
starts itsCMDthere as well, despite the RFC defining/as the baseline.
Please pass the explicit/through for RUN, startup, tar, and ownership
commands instead of converting it toNone. -
COPYdrops source file metadata, including the executable bit.
_materialize()
recreates every selected source withopen(..., "wb"), but neither the
context abstraction nor the copy plan records its mode. In a minimal local
reproduction, a0755entrypoint.shwas staged as0664; no later chmod
restores it. Thus the supported pattern
COPY entrypoint.sh /usr/local/bin/entrypointcommonly produces a
non-executable startup file.
Docker's supported COPY semantics preserve file metadata and permissions.
The context/manifest needs to carry and apply the relevant metadata, or the
advertised COPY contract must fail closed for semantics it cannot preserve. -
Advertised directory COPY cannot represent empty directories.
DockerContext.walk()
enumerates files only, and the manifest derives directories only from those
file paths. For a context containing an emptyempty/directory,
check_direct_launch()reportsCOPY empty/ /srv/empty/as launchable, but
walk()is empty and_validate_copy_plan()fails with "sources select no
files". Since literal directories are explicitly in the strict subset, the
manifest must represent directory entries and the runner must create selected
empty directories (including empty directories nested under non-empty trees). -
A
USERbefore local tarADDmakes the supported ADD fail.
_extract_tar()
creates the destination through the root-owned filesystem facade and then
wrapstar xfwith the accumulated non-rootUSER. For example,USER app
followed byADD app.tar /opt/app/attempts to extract asappinto the
newly root-owned/opt/app, so it normally fails with permission denied.
COPY does not switch to this user either. COPY/ADD should run with builder
ownership semantics, with the validated--chownapplied afterward, rather
than applying runtime USER to tar extraction. -
Valid
USER user:groupand numeric USER forms are silently changed.
wrap_user()
unconditionally discards the group.USER app:stafftherefore runs with
app's default group instead ofstaff.USER 1000:1001is also reported
as directly launchable, but becomesrunuser -u 1000;runusertreats that
as a user name and fails when no passwd entry named1000exists, whereas a
numeric Docker USER does not require such an entry. Please preserve these
forms, or reject the unsupported forms during strict parsing rather than
changing their meaning at execution time.
I ran the 117 directly affected unit tests on Python 3.10, 3.11, and 3.12; all
pass. The focused reproductions above cover cases not present in that suite.
|
Thanks for the focused follow-up. I addressed all five findings in a separate commit, 1. Explicit root working directoryThe Dockerfile runner now passes the accumulated absolute Regression coverage asserts that every runner command receives 2. COPY file metadata and executable modeThe public context protocol now exposes a frozen
The unit suite covers 3. Empty directories in directory COPYThe context manifest now represents directory entries directly instead of inferring them only from files. Local contexts enumerate all directories deterministically, including top-level and nested empty directories; custom contexts must also provide every directory ancestor explicitly. Missing ancestors, duplicate paths, file-as-ancestor conflicts, symlinks, and special files fail closed. Literal directories,
4. USER before local tar ADDCOPY and ADD now always use builder/root ownership semantics and no longer receive the accumulated runtime The previous test that expected 5. Group-qualified and numeric USER formsThe direct-launch subset now explicitly supports named users only, such as The README, RFC, public API documentation, maintenance contract, and example prechecks now describe and exercise this narrower fail-closed rule. Validation
The RFC issue and PR description have also been updated to reflect the corrected public contract and current test evidence. Could you please take another look when convenient? |
tianyuzhou95
left a comment
There was a problem hiding this comment.
Thanks for the focused follow-up. I verified that the five findings from the
previous review are addressed in cc53726: root cwd is now explicit, COPY
modes and empty directories are represented, tar ADD uses builder ownership,
and unsupported USER forms fail closed. Two runtime correctness issues and one
quality-gate issue remain.
Blocking findings
-
Wildcard directory sources retain an extra directory level.
_select_wildcard()
prefixes every child of a matched directory with
posixpath.basename(directory). For a context containing
sub/dir1/dir2/foo,COPY sub/* /subdest/therefore writes
/subdest/dir1/dir2/foo. Docker/BuildKit treats the matcheddir1as a
directory source and copies its contents, so the expected target is
/subdest/dir2/foo; this exact case is covered by BuildKit's upstream
testCopyWildcards.
The strict subset advertises wildcard directory support, so the matched
directory root needs destination-marker semantics rather than being added
to each relative target. -
Unreadable directories are silently copied as empty directories.
LocalDockerContext.walk()
callsos.walk()without anonerrorcallback. Python consequently ignores
a permission error while descending into a directory but still records the
directory entry seen from its parent. In a minimal reproduction, a mode
000directory containingrequired.txtwas emitted as one empty directory
entry;COPY blocked /appthen succeeded with onlymkdir /appand silently
droppedrequired.txt. This violates the RFC's fail-closed context contract.
Please make traversal errors raiseDockerContextErrorand add a regression
covering an unreadable selected directory. -
The claimed 208-test gate depends on the caller's umask.
test_local_and_memory_contexts_match
creates local files and directories using ambient modes, while
MemoryDockerContexthard-codes0644/0755. With the commonumask 0002,
local entries are0664/0775, so the complete unit gate consistently runs
208 tests with one failure on Python 3.10, 3.11, and 3.12. The same gate
passes after forcingumask 0022. Please assign explicit modes in this test
somake sdk-checkis hermetic.
Ruff and mypy pass locally. The updated head currently has no GitHub check runs.
|
Thanks for the latest verification. I addressed all three findings in a new, separate commit, 1. Wildcard directory sourcesWildcard-matched directory roots now use destination-marker semantics. Their contents are merged into the destination without retaining the matched root basename, while directly matched files continue to use their basename. For the reported case: COPY sub/* /subdest/with The implementation also handles mixed file/directory matches, multiple matched directories, empty matched directories, root destinations, and target collisions. Multiple directory markers may share one destination, but colliding non-marker content fails before any sandbox operation. New destination directories are included in exact During independent verification, I also found and corrected a related source-pattern issue: Docker uses Go 2. Unreadable context directories
Regression tests inject
3. Umask-independent quality gateThe local/custom context parity fixture now explicitly sets context files to The full quality gate was run from a subshell with Additional validation
The RFC issue and PR description have been updated with the wildcard matching, traversal failure, 211-test, umask, and live-example evidence. Could you please take another look when convenient? |
tianyuzhou95
left a comment
There was a problem hiding this comment.
Thanks for the update. The three findings from the previous review are addressed in this revision: wildcard-matched directory roots now have the expected destination-marker behavior, local traversal failures fail closed, and the context parity test is independent of the caller's umask. Two Dockerfile-semantics issues still need to be resolved before merge.
|
Thanks for the follow-up. Both blocking inline comments are addressed in a new, separate commit, Go-compatible source patternsThe SDK no longer delegates Dockerfile source matching to Python
The new unit coverage includes Multi-source wildcard destinations
When one wildcard expands to multiple top-level sources, the runner now requires the destination to end in
Validation
I also replied directly to both inline discussions. Could you please take another look when convenient? |
|
The latest The two conflicts were resolved by preserving both sides:
Validation after the merge:
The branch is clean, pushed, and now includes the current target branch. @tianyuzhou95, could you please continue with the next review round? |
d1001ce to
aa8b94b
Compare
|
I rebased this branch onto the latest The latest main branch introduced the all-in-one Standalone E2E job. This PR now also adds The new workflow run is waiting for approval because it comes from a fork: @tianyuzhou95, could you please approve and run this workflow? My current GitHub token has read-only access to the upstream repository, and the Actions approval endpoint requires repository admin permission. Local validation before the force-push:
|
tianyuzhou95
left a comment
There was a problem hiding this comment.
Thanks for the update. The two findings from my previous review are addressed: Docker source patterns now follow the documented Go filepath.Match subset, and wildcard expansions to multiple top-level sources require a trailing-slash destination. I also verified the matcher against 100,000 accepted random pattern/name cases and found no mismatch. Two .dockerignore parity issues still block merge because they can either expose excluded context files or reject valid Docker COPY sources.
|
The latest
Key validation:
The standalone example now adds one fresh sandbox per startup regression:
RFC issue #17 and the PR description are updated. The new CI run is waiting for fork-workflow approval: @tianyuzhou95, could you please approve the run and continue the next review round? |
tianyuzhou95
left a comment
There was a problem hiding this comment.
Thanks for the update. The two findings from my previous review are addressed: the replacement matcher follows the supported Moby semantics, and re-included descendants remain selectable through virtual source directories. I additionally compared 20,263 supported pattern/path cases, including parent-result reuse, without finding a mismatch; the 231 unit tests, Ruff, mypy, and all current GitHub checks also pass. Two build-context selection issues still block merge because the supported COPY path can either expose a file that Docker excludes or silently omit files that Docker copies.
Docker context behavior now follows the reviewed contractNew standalone commit:
I replied to and resolved both blocking inline discussions with the corresponding regression coverage. Validation for this revision: @tianyuzhou95, could you please continue the review when convenient? |
tianyuzhou95
left a comment
There was a problem hiding this comment.
Thanks for the update. The two findings from my previous review are addressed: Dockerfile-specific ignore-file precedence now matches BuildKit, including an empty companion, and control files remain selectable whenever the active matcher permits them. I reran both real BuildKit comparison cases and found matching manifest output. I found no new runtime correctness or security blocker. The 237 unit tests pass under the default umask 0022, and Ruff and mypy are clean. I left one non-blocking test-hermeticity comment below; the latest GitHub Actions run is still awaiting approval (action_required).
|
Follow-up: |
tianyuzhou95
left a comment
There was a problem hiding this comment.
The correctness fixes look good, the umask-dependent fixtures are now explicit, and all current CI checks pass. I have two additional API and documentation design comments for introducing this experimental feature.
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>
Preserve the explicit root working directory and represent Docker build context entries with file type and permission metadata so COPY retains executable modes and empty directories. Run archive extraction with builder ownership and reject USER forms that cannot be represented faithfully by the current command facade. Add focused regressions for directory targets, modes, ownership, strict USER validation, and live-example coverage for the corrected behavior. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
Match Docker source wildcards one path segment at a time and copy the contents of matched directories without retaining their root name. Fail closed when a local context traversal cannot descend into an entry, and keep context parity tests independent of the caller's umask. Add regressions for wildcard directory targets, filepath-style double-star matching, traversal failures, destination ownership, and the maintained live example. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
Match Dockerfile source patterns with Go filepath-compatible character classes and reject malformed patterns before context files are read. Track top-level wildcard expansions so multi-source copies require a directory destination. Add regressions for class negation, malformed ranges, Unicode matching, ignored sources, and wildcard expansion counts. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
Include the maintained Dockerfile direct-launch example in the all-in-one standalone job introduced on main. This makes the rebased pull request exercise parsing, context transfer, in-sandbox build instructions, startup command handling, ownership, archive extraction, and fail-closed prechecks in the same CI environment used for the other SDK examples. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
Replace GitIgnoreSpec with a fail-closed matcher that follows Moby ignore-file preprocessing, ordered parent matching, embedded double-star behavior, path cleaning, and supported character classes. Reject RE2-only escapes instead of treating them as literals so excluded secrets cannot become visible. Keep directory ancestors as virtual sources when later negations re-include descendants, while retaining one top-level wildcard count. Reuse parent match results with a depth-bounded directory stack to avoid repeated matching and unbounded entry-by-pattern storage. Remove the pathspec runtime dependency, document the supported boundary and licensing, and add regressions for Moby parity, virtual literal and wildcard directories, unsupported escapes, prefix-sibling ordering, and bounded parent-result reuse. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
Extend the maintained standalone example from five to nine sections. Exercise ignored-directory descendant re-inclusion through COPY dot, literal-directory, and wildcard sources, and verify excluded siblings remain absent. Add fresh-sandbox startup regressions for ENTRYPOINT without CMD, shell-form CMD, shell-form ENTRYPOINT overriding CMD, and auto_start_cmd=False. Each section asserts its result before the context manager destroys the sandbox, and the complete example remains within the CI timeout. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
Honor an existing adjacent <Dockerfile>.dockerignore before falling back to the root ignore file, including an empty companion. Keep Dockerfile and ignore control files visible to COPY and ADD whenever the active matcher permits them, while preserving inline and external input boundaries and secure no-follow reads.\n\nUpdate the context contract, unit coverage, documentation, and standalone example to verify both behaviors. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
Set the Dockerfile fixture modes explicitly so manifest-mode expectations do not depend on the process umask used by the test runner. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
f2cd220 to
7506ecd
Compare
|
Rebase update:
Please continue the review. |
Encapsulate experimental Dockerfile launch settings in one DockerfileLaunch value, remove the meaningless top-level combination, and split the detailed Experimental guide from the SDK README. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
|
Follow-up in independent DCO commit Product status is now clarified: Dockerfile direct launch is supported and directly usable, will not be deprecated, and may expand only through backward-compatible additions to its documented strict subset. Unsupported inputs retain the existing fail-closed boundary. The configuration and documentation structure from Earlier validation remains applicable: |
Remove the Experimental status from Dockerfile direct launch now that the capability is supported and directly usable. The API will not be deprecated; the documented strict subset may grow only through backward-compatible additions, while unsupported inputs retain the existing fail-closed boundary. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
|
Dockerfile direct launch is a supported AKernel SDK capability and will remain available. Its documented strict subset evolves incrementally with production experience; unsupported inputs continue to fail closed. The specific API surface may evolve, with documentation and migration guidance for material changes. Independent DCO commit Current validation: |
tianyuzhou95
left a comment
There was a problem hiding this comment.
All current checks, including the standalone E2E job, pass, and the parameter encapsulation and documentation split address the earlier design comments. Two public API and lifecycle issues still need follow-up.
Move DockerfileLaunch into a lightweight value module so public annotations resolve at runtime without loading the parser or a backend. Keep parser and backend imports lazy, and calibrate the supported-capability lifecycle wording to allow API evolution with migration guidance. Signed-off-by: Peng-YM <Peng-YM@users.noreply.github.com>
Follow-up: runtime annotations and lifecycle wordingNew independent DCO commit Both latest review threads are resolved individually:
PR #16, RFC #17, and the project status comment now use that same lifecycle wording. The PR and RFC also record the runtime-resolvable annotation architecture, new module, compatibility import, current head, and validation evidence. Validation: current Could you please continue the review? |
Summary
This PR documents a supported, backend-neutral, fail-closed direct-launch
path for a deliberately narrow Dockerfile subset in the Python SDK. Dockerfile direct launch is a supported AKernel SDK capability and will remain available. Its documented strict subset evolves incrementally with production experience; unsupported inputs continue to fail closed. The specific API surface may evolve, with documentation and migration guidance for material changes. It does
not use BuildKit, a Docker daemon, or a registry push. The implementation keeps
backend-native conversions behind the existing SDK boundary.
The current base is
upstream/main53c86e0; the current branch head is0f5a969. This independent DCO commit does not rewrite earlier history.Public API
DockerfileLaunchis the single immutable configuration value for thissupported capability:
Sandboxnow exposes onlydockerfilefor this path.image,rootfs, anddockerfileare mutually exclusive, so at most one creation source may beprovided. The previous separate feature-specific constructor controls have been removed. Preflight stays
context-oriented:
check_direct_launch(context)still accepts aDockerContextand reports diagnostics before construction.The complete API contract, supported subset, context and ignore-file rules,
security boundaries, startup behavior, and examples are in the new
Dockerfile launch guide.
The SDK README intentionally keeps only a short, late supported-capability entry and
links to that guide; the maintained runnable example remains
sdk/python/examples/dockerfile_launch.py.Semantics and boundaries
FROMsupplies only the sandbox root filesystem. Its OCIENV,USER,WORKDIR,CMD, andENTRYPOINTconfiguration is not inherited. Direct launchapplies only the supported Dockerfile declarations, reruns
RUN,COPY, andADDfor every launch, and creates no cache or snapshot.The strict subset supports one literal
FROM, shell-formRUN, localshell-form
COPY/ADD, literal--chown, safe local-tarADD, literalENV,absolute
WORKDIR, namedUSER,EXPOSE, and shell- or exec-formCMD/ENTRYPOINT. Multi-stage input, remoteADD, build arguments andexpansion, unsupported flags/instructions, and syntax the SDK cannot model are
rejected before execution.
Sandbox(dockerfile=...)parses strictly beforebackend creation;
apply_dockerfile()independently rejects unsupported parsedcontent.
startup_commandconfirms dispatch, not application health.Documentation split
The supported-capability guide is structured as:
DockerfileLaunchconfiguration and precheck;DockerContextand.dockerignorecontract;README navigation, the short README entry, the guide, and the runnable example
link to each other and use only the
Sandbox(dockerfile=DockerfileLaunch(...))API.
Verification
0f5a969passedmake sdk-check: 241 unit tests, Ruff,and mypy for 25 source files.
umask 0022andumask 0002;the two affected fixtures are explicitly mode
0644. The SDK wheel and sdistbuild also passed.
make deploy-script-checkpassed.runscdeployment passed all 9/9 maintainedexample sections in 52-second and 47.07-second runs; this annotation and
lifecycle revision did not change direct-launch runtime semantics, so it was
not rerun.
comparisons against
moby/patternmatcher, with explicit fail-closed tests forunsupported RE2-only forms.
Annotation architecture in
0f5a969DockerfileLaunchnow lives in the lightweightakernel_sdk._dockerfile_launchvalue module.Sandboxand the runner importonly runtime-resolvable public types from this module; the Dockerfile parser and
backend modules remain lazy. The compatibility import from
_dockerfileisretained.
get_type_hintsis covered forSandbox.__init__andapply_dockerfileacross three import orders, with a 91-public-callable scan.The new commit changes these nine paths:
AGENTS.mdsdk/python/README.mdsdk/python/akernel_sdk/__init__.pysdk/python/akernel_sdk/_dockerfile.pysdk/python/akernel_sdk/_dockerfile_launch.pysdk/python/akernel_sdk/_dockerfile_runner.pysdk/python/akernel_sdk/sandbox.pysdk/python/docs/launch-from-dockerfile.mdsdk/python/tests/unit/test_types.pyLicensing
The parser dependency is
dockerfile-parse(BSD-3-Clause)..dockerignorebehavior followsmoby/patternmatcher(Apache-2.0) through abackend-neutral Python implementation. No Docker engine, BuildKit, registry
component, or
pathspecruntime dependency is added.