Skip to content

feat(sdk): add strict Dockerfile sandbox launch - #16

Merged
tianyuzhou95 merged 12 commits into
inclusionAI:mainfrom
Peng-YM:feat/dockerfile-sandbox-launch
Aug 20, 2026
Merged

feat(sdk): add strict Dockerfile sandbox launch#16
tianyuzhou95 merged 12 commits into
inclusionAI:mainfrom
Peng-YM:feat/dockerfile-sandbox-launch

Conversation

@Peng-YM

@Peng-YM Peng-YM commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Design RFC: #17

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/main 53c86e0; the current branch head is
0f5a969. This independent DCO commit does not rewrite earlier history.

Public API

DockerfileLaunch is the single immutable configuration value for this
supported capability:

@dataclass(frozen=True)
class DockerfileLaunch:
    context: DockerContext
    auto_start_cmd: bool = True
    run_timeout: int = 600

Sandbox(..., *, dockerfile: DockerfileLaunch | None = None)

Sandbox now exposes only dockerfile for this path. image, rootfs, and
dockerfile are mutually exclusive, so at most one creation source may be
provided. The previous separate feature-specific constructor controls have been removed. Preflight stays
context-oriented: check_direct_launch(context) still accepts a
DockerContext and reports diagnostics before construction.

from akernel_sdk import (
    DockerfileLaunch, LocalDockerContext, Sandbox, check_direct_launch,
)

context = LocalDockerContext("Dockerfile", context_dir=".")
check = check_direct_launch(context)
if not check.direct_launchable:
    raise RuntimeError(check.reasons)

with Sandbox(
    dockerfile=DockerfileLaunch(context, auto_start_cmd=True, run_timeout=300)
) as sandbox:
    startup = sandbox.startup_command
    if startup is not None:
        result = startup.wait(timeout=60)
        print(result.exit_code, result.stderr)

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

FROM supplies only the sandbox root filesystem. Its OCI ENV, USER,
WORKDIR, CMD, and ENTRYPOINT configuration is not inherited. Direct launch
applies only the supported Dockerfile declarations, reruns RUN, COPY, and
ADD for every launch, and creates no cache or snapshot.

The strict subset supports one literal FROM, shell-form RUN, local
shell-form COPY/ADD, literal --chown, safe local-tar ADD, literal ENV,
absolute WORKDIR, named USER, EXPOSE, and shell- or exec-form
CMD/ENTRYPOINT. Multi-stage input, remote ADD, build arguments and
expansion, unsupported flags/instructions, and syntax the SDK cannot model are
rejected before execution. Sandbox(dockerfile=...) parses strictly before
backend creation; apply_dockerfile() independently rejects unsupported parsed
content. startup_command confirms dispatch, not application health.

Documentation split

The supported-capability guide is structured as:

  1. scope and root-filesystem semantics;
  2. quick start plus DockerfileLaunch configuration and precheck;
  3. supported/rejected syntax;
  4. DockerContext and .dockerignore contract;
  5. COPY/ADD, startup, security, and failure behavior;
  6. external-build fallback, verification, and licensing.

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

  • Current commit 0f5a969 passed make sdk-check: 241 unit tests, Ruff,
    and mypy for 25 source files.
  • The quality gate was previously run with both umask 0022 and umask 0002;
    the two affected fixtures are explicitly mode 0644. The SDK wheel and sdist
    build also passed.
  • make deploy-script-check passed.
  • The recorded real standalone runsc deployment passed all 9/9 maintained
    example 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.
  • Docker-ignore differential coverage remains 87,417 supported pattern/path
    comparisons against moby/patternmatcher, with explicit fail-closed tests for
    unsupported RE2-only forms.

Annotation architecture in 0f5a969

DockerfileLaunch now lives in the lightweight
akernel_sdk._dockerfile_launch value module. Sandbox and the runner import
only runtime-resolvable public types from this module; the Dockerfile parser and
backend modules remain lazy. The compatibility import from _dockerfile is
retained. get_type_hints is covered for Sandbox.__init__ and
apply_dockerfile across three import orders, with a 91-public-callable scan.

The new commit changes these nine paths:

  • AGENTS.md
  • sdk/python/README.md
  • sdk/python/akernel_sdk/__init__.py
  • sdk/python/akernel_sdk/_dockerfile.py
  • sdk/python/akernel_sdk/_dockerfile_launch.py
  • sdk/python/akernel_sdk/_dockerfile_runner.py
  • sdk/python/akernel_sdk/sandbox.py
  • sdk/python/docs/launch-from-dockerfile.md
  • sdk/python/tests/unit/test_types.py

Licensing

The parser dependency is dockerfile-parse (BSD-3-Clause).
.dockerignore behavior follows moby/patternmatcher (Apache-2.0) through a
backend-neutral Python implementation. No Docker engine, BuildKit, registry
component, or pathspec runtime dependency is added.

@Peng-YM
Peng-YM force-pushed the feat/dockerfile-sandbox-launch branch 5 times, most recently from 4412924 to 445cecf Compare August 7, 2026 05:36

@tianyuzhou95 tianyuzhou95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. SDK-side ADD URL creates an SSRF boundary violation. _dockerfile_runner.py lines 234-244 downloads remote URLs with urllib in 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 remote ADD must be an explicit trusted-input-only opt-in with an enforceable network policy.

  2. The build context bypasses .dockerignore and does not implement consistent Docker context semantics. _dockerfile_runner.py lines 246-275 passes a local directory directly to copy_from_local, so COPY . ... can upload files such as credentials, .env, or .git even 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.

  3. Valid Dockerfile syntax is accepted as directly launchable but executed incorrectly. _dockerfile.py lines 433-487 parses JSON-form COPY/ADD with shlex, so COPY ["a b", "/dest/"] becomes source "[a b," and destination "/dest/]". Exec-form RUN is also treated as a shell string, and flags such as --chmod and --link are silently discarded. In addition, _dockerfile_runner.py lines 129-139 resets relative WORKDIR values instead of resolving them against the previous directory and ignores ARG, even though build arguments affect FROM, later instructions, and RUN. Unsupported syntax must either be implemented or rejected by check_direct_launch; it must not return success and then run with different semantics.

  4. The evaluator does not inherit the base image configuration. It initializes build state as empty environment, /, and root, and resolves CMD/ENTRYPOINT only from instructions in the current Dockerfile. A Dockerfile containing only FROM nginx, for example, is reported as launchable but does not start the inherited nginx command; inherited USER, WORKDIR, CMD, and ENTRYPOINT are 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.

  5. The documented warning policy is silent in the main API. sandbox.py lines 292-303 discards the DockerfileApplyResult, so warnings for ignored instructions never reach callers of Sandbox(context=...). Unknown instructions also remain warnings under strict=True. Please reject unsupported behavior by default, or emit/expose the warnings and make strict mode consistently reject every ignored instruction.

  6. CMD/ENTRYPOINT resolution and readiness do not match the stated contract. _dockerfile.py lines 154-167 drops a shell-form CMD when paired with an exec-form ENTRYPOINT, 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.

@Peng-YM
Peng-YM force-pushed the feat/dockerfile-sandbox-launch branch from 445cecf to 4f2a35e Compare August 12, 2026 07:44
@Peng-YM Peng-YM changed the title feat(sdk): add Dockerfile sandbox-launch path with in-sandbox execution feat(sdk): add strict Dockerfile sandbox launch Aug 12, 2026
@Peng-YM

Peng-YM commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. I addressed each blocking finding in commit 4f2a35e, and rebased the branch onto the current upstream/main. Here is the point-by-point resolution.

1. SDK-side ADD URL / SSRF

Resolved by removing remote downloads from the execution path entirely.

  • Any URL source in ADD is now classified as remote_add by check_direct_launch().
  • Strict parsing rejects it before a backend session or sandbox is created.
  • The runner no longer contains an urllib download path.
  • Non-strict parsing remains diagnostics-only and cannot be passed to execution when unsupported items are present.

This is intentionally fail-closed rather than an opt-in host-side fetch.

2. .dockerignore and consistent context semantics

Resolved with a shared, filtered context manifest used by both local and custom DockerContext implementations.

  • The manifest is built from DockerContext.walk() and reads file content only through DockerContext.open().
  • Root .dockerignore rules are applied with Docker-style ordering, negation, directory, and wildcard behavior.
  • COPY/ADD support literal files, directories, ., and wildcard expansion with deterministic ordering.
  • Dockerfile and .dockerignore are reserved and are never copied by COPY ..
  • Local and non-local contexts now use the same source-selection and destination logic.
  • Invalid, duplicate, escaping, colliding, and ignored-only sources fail before sandbox mutation.
  • LocalDockerContext.open() uses descriptor-relative, no-follow opens and rejects symlinks, preventing context escape and symlink races.

3. Accepted syntax executed with different semantics

Resolved by narrowing the executable subset and making unsupported syntax fail closed.

check_direct_launch() and strict parsing now reject, among other cases:

  • JSON-form COPY/ADD
  • exec-form RUN
  • RUN flags such as --mount, --network, and unknown flags
  • relative WORKDIR
  • ARG
  • FROM flags or variable expansion
  • COPY/ADD --chmod, --link, --from, and unknown flags
  • unsupported build-time variable expansion
  • ignored or unknown instructions

Supported shell-form instructions retain their defined semantics. Unsupported constructs produce stable reasons such as unsupported_syntax, remote_add, or multi_stage; they are never converted into executable build instructions.

4. Base image configuration inheritance

Resolved by explicitly narrowing and documenting the contract: FROM supplies the root filesystem only.

The direct-launch path does not claim to inherit OCI image ENV, USER, WORKDIR, CMD, or ENTRYPOINT. A directly launched Dockerfile must declare any configuration it needs. check_direct_launch() reports this rootfs-only limitation in its successful result, and the README, RFC issue, and PR description all state it explicitly.

Full OCI image-config inheritance remains outside the scope of this change rather than being approximated incorrectly.

5. Warning policy and strict execution

Resolved by separating diagnostics from execution.

  • Sandbox(context=...) always invokes parse_dockerfile(..., strict=True) before backend creation.
  • Strict mode rejects every unsupported or ignored instruction.
  • apply_dockerfile() independently rejects a parsed object containing unsupported items, so a non-strict diagnostic result cannot be executed accidentally.
  • Non-strict parsing is only for check_direct_launch() and diagnostic inspection.
  • The constructor stores the returned startup handle instead of discarding the relevant apply result.

There is therefore no silent-warning execution path in the main API.

6. CMD/ENTRYPOINT resolution and readiness

Resolved by preserving command form explicitly and implementing the merge matrix rather than inferring form from whitespace.

  • Shell forms normalize to ('/bin/sh', '-c', command).
  • Exec ENTRYPOINT + exec CMD concatenates argument vectors.
  • Exec ENTRYPOINT + shell CMD appends /bin/sh -c <command>.
  • Shell ENTRYPOINT ignores CMD, matching Docker behavior.
  • One-element exec-form commands remain exec form.
  • Launching uses shlex.join(argv) without whitespace heuristics.

The readiness contract is now precise: construction guarantees sandbox readiness and successful background command dispatch, not application health. The returned CommandHandle is exposed as Sandbox.startup_command, allowing callers to inspect or wait for the process. Dispatch failure raises DockerfileBuildError and triggers constructor rollback.

Backend-neutral rebase and cleanup

The branch is rebased onto the backend-neutral SDK architecture and now creates SandboxSpec, loads the selected backend, and operates through BackendSession, Commands, and Filesystem. Any parse/apply/dispatch failure terminates and closes the session, including detached sandboxes, while preserving the original exception.

Validation

  • make sdk-check: 196 tests passed, Ruff passed, mypy passed for 24 source files.
  • Live standalone coverage passed for .dockerignore + COPY ., shell RUN, ENV/WORKDIR/USER, exec ENTRYPOINT + CMD, COPY --chown, local-tar ADD, remote-ADD preflight rejection, and precise ownership behavior without recursive changes to pre-existing files.
  • The current public standalone image does not expose /api/sandbox/v1 for the default openyuanrong-sandbox backend, so live validation used the optional openyuanrong-sdk backend through the same public SDK facade; this limitation is disclosed in the PR and RFC.

Could you please take another look when convenient?

@tianyuzhou95 tianyuzhou95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. The explicit root working directory is delegated to the backend default.
    _dockerfile_runner.py lines 253-257
    passes cwd=None whenever the accumulated Dockerfile WORKDIR is /, and
    the startup path repeats this at lines 741-746. None means "use the backend
    default", not /; the actor backend creates a temporary default directory
    and resolves a missing cwd to it. Consequently, FROM ubuntu followed by
    RUN pwd runs under /tmp/sandbox_*, and a Dockerfile without WORKDIR
    starts its CMD there 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 to None.

  2. COPY drops source file metadata, including the executable bit.
    _materialize()
    recreates every selected source with open(..., "wb"), but neither the
    context abstraction nor the copy plan records its mode. In a minimal local
    reproduction, a 0755 entrypoint.sh was staged as 0664; no later chmod
    restores it. Thus the supported pattern
    COPY entrypoint.sh /usr/local/bin/entrypoint commonly 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.

  3. 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 empty empty/ directory,
    check_direct_launch() reports COPY 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).

  4. A USER before local tar ADD makes the supported ADD fail.
    _extract_tar()
    creates the destination through the root-owned filesystem facade and then
    wraps tar xf with the accumulated non-root USER. For example, USER app
    followed by ADD app.tar /opt/app/ attempts to extract as app into 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 --chown applied afterward, rather
    than applying runtime USER to tar extraction.

  5. Valid USER user:group and numeric USER forms are silently changed.
    wrap_user()
    unconditionally discards the group. USER app:staff therefore runs with
    app's default group instead of staff. USER 1000:1001 is also reported
    as directly launchable, but becomes runuser -u 1000; runuser treats that
    as a user name and fails when no passwd entry named 1000 exists, 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.

@Peng-YM

Peng-YM commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the focused follow-up. I addressed all five findings in a separate commit, cc53726, without rewriting the original implementation commit.

1. Explicit root working directory

The Dockerfile runner now passes the accumulated absolute workdir to every commands.run() call, including the baseline /. This covers RUN, startup dispatch, tar path checks, tar extraction, chmod, and chown; it no longer delegates / to a backend-specific cwd=None default.

Regression coverage asserts that every runner command receives / when no WORKDIR is declared. The live example also verifies both RUN pwd and the resolved ENTRYPOINT + CMD startup process execute in /.

2. COPY file metadata and executable mode

The public context protocol now exposes a frozen DockerContextEntry(path, kind, mode) for each file and directory. mode is constrained to a native integer permission value from 0o000 through 0o777; non-native integer subclasses are rejected before they can reach shell formatting.

LocalDockerContext.walk() records stat.S_IMODE for regular files and directories. Custom contexts use the same structured protocol. COPY materializes file content during preflight and then restores each selected child entry's mode through an exact, non-recursive, root-owned chmod. The literal source-directory root remains a content container and does not overwrite the destination container's mode, matching Docker's directory-copy semantics.

The unit suite covers 0755, ordinary file modes, directory modes, multiple literal directories, and root destinations. The real-sandbox example confirms an executable remains 0755.

3. Empty directories in directory COPY

The 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, ., and wildcard expansion retain selected directory entries. The runner creates empty and nested directories while preserving child directory modes. It also handles COPY src /, COPY empty /, and multiple literal sources sharing one destination marker without modifying / itself.

.dockerignore filtering now applies to directory entries as well. Unit tests cover ignored empty directories, top-level and nested empty directories, literal-directory root targets, and local/custom context parity. The maintained live example exercises both COPY . and an explicit empty-directory COPY.

4. USER before local tar ADD

COPY and ADD now always use builder/root ownership semantics and no longer receive the accumulated runtime USER. Local tar extraction runs as root even after USER app; validated --chown is applied afterward to the exact outputs of the current instruction.

The previous test that expected runuser around tar xf was corrected. Unit tests now assert the wrapper is absent, --chown remains exact and non-recursive, and pre-existing destination files retain their ownership. A real standalone section runs USER app before local tar ADD and completes successfully.

5. Group-qualified and numeric USER forms

The direct-launch subset now explicitly supports named users only, such as app and root. USER app:staff, numeric UID, and UID:GID forms are classified as unsupported_syntax by non-strict diagnostics and rejected by strict parsing before backend creation. wrap_user() performs the same defensive validation and no longer strips a group or treats a numeric identity as a username.

The README, RFC, public API documentation, maintenance contract, and example prechecks now describe and exercise this narrower fail-closed rule.

Validation

  • make sdk-check: 208 tests passed; Ruff passed; mypy passed for 24 source files.
  • Independent final verification: no remaining P0/P1 findings for this review set.
  • Real standalone runsc validation through the backend-neutral facade passed all five maintained sections, including:
    • executable and directory permission preservation;
    • top-level, nested, and literal empty-directory copies;
    • root cwd for RUN and startup dispatch;
    • USER app followed by builder/root local tar extraction;
    • fail-closed remote ADD, USER app:staff, and numeric UID:GID prechecks.

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 tianyuzhou95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. 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 matched dir1 as 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.

  2. Unreadable directories are silently copied as empty directories.
    LocalDockerContext.walk()
    calls os.walk() without an onerror callback. 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
    000 directory containing required.txt was emitted as one empty directory
    entry; COPY blocked /app then succeeded with only mkdir /app and silently
    dropped required.txt. This violates the RFC's fail-closed context contract.
    Please make traversal errors raise DockerContextError and add a regression
    covering an unreadable selected directory.

  3. 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
    MemoryDockerContext hard-codes 0644/0755. With the common umask 0002,
    local entries are 0664/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 forcing umask 0022. Please assign explicit modes in this test
    so make sdk-check is hermetic.

Ruff and mypy pass locally. The updated head currently has no GitHub check runs.

@Peng-YM

Peng-YM commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the latest verification. I addressed all three findings in a new, separate commit, 1f4870c; the earlier commits remain unchanged.

1. Wildcard directory sources

Wildcard-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 sub/dir1/dir2/foo, the SDK now creates /subdest/dir2/foo and does not create /subdest/dir1. The same result is produced when the destination omits the trailing slash.

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 --chown handling; existing destinations and / are not changed.

During independent verification, I also found and corrected a related source-pattern issue: Docker uses Go filepath.Match-style segment matching, so ** does not recursively cross /. The SDK now matches one path segment at a time. For example, modules/** matches the immediate one and two directories and copies their contents to /dest/x.py and /dest/y.txt, while src/**/*.py does not match src/a.py.

2. Unreadable context directories

LocalDockerContext.walk() now provides an onerror callback to os.walk(). Any traversal failure is immediately wrapped as DockerContextError, includes the affected relative path, and preserves the original OSError as its cause. Directory and file lstat() failures follow the same fail-closed path.

Regression tests inject PermissionError from os.scandir() for a non-empty directory containing required.txt. They verify that:

  • direct walk() fails instead of returning an empty directory entry;
  • manifest construction preserves the error and cause chain;
  • COPY blocked /app fails before any filesystem or command operation in the sandbox.

3. Umask-independent quality gate

The local/custom context parity fixture now explicitly sets context files to 0644 and directories to 0755. It does not alter process-global umask and continues to compare permission metadata.

The full quality gate was run from a subshell with umask 0002:

211 tests passed
Ruff passed
mypy passed for 24 source files

Additional validation

  • Independent final verifier: APPROVE, with no P0/P1 findings.
  • The maintained five-section example passed again against the real standalone runsc deployment.
  • Its core section now executes COPY wild/* /srv/wild/ and verifies /srv/wild/dir2/foo exists while /srv/wild/dir1 does not.
  • Existing coverage for .dockerignore, file/directory modes, empty directories, root cwd, COPY --chown, builder/root tar ADD, strict USER, and preflight side-effect prevention remains green.

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 tianyuzhou95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sdk/python/akernel_sdk/_dockercontext.py Outdated
Comment thread sdk/python/akernel_sdk/_dockerfile_runner.py
@Peng-YM

Peng-YM commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the follow-up. Both blocking inline comments are addressed in a new, separate commit, 5736dc1; no earlier commit was rewritten.

Go-compatible source patterns

The SDK no longer delegates Dockerfile source matching to Python fnmatch. It now precompiles the strict no-escape subset of Go filepath.Match before inspecting context entries:

  • only a leading ^ negates a character class;
  • ! is an ordinary class character;
  • *, ?, classes, ranges, Unicode codepoints, and single-segment ** follow Go behavior;
  • malformed or empty classes and invalid ranges fail closed, including for an empty context;
  • malformed patterns fail before context file reads and before sandbox operations;
  • backslash escapes remain explicitly rejected by the documented strict subset.

The new unit coverage includes [!a].txt, [^a].txt, ranges, Unicode ?, **, and multiple malformed forms. Independent verification compared 1,368 pattern/name combinations against Go filepath.Match without a result or error-state mismatch.

Multi-source wildcard destinations

_ContextSelection now retains the top-level wildcard expansion count after .dockerignore filtering. A matched directory root counts once regardless of how many descendants it contains; each directly matched file counts once.

When one wildcard expands to multiple top-level sources, the runner now requires the destination to end in / and rejects the Dockerfile before materialization or any sandbox side effect. Tests cover:

  • two directories;
  • two files;
  • one directory plus one file;
  • valid trailing-slash variants;
  • valid single-directory and single-file expansions without a trailing slash;
  • ignored sources, destination markers, collisions, modes, and exact --chown behavior.

Validation

  • (umask 0002; make sdk-check): 214 tests passed; Ruff passed; mypy passed for 24 source files.
  • Independent final verifier: APPROVE, no P0/P1/P2 findings.
  • The maintained five-section example passed again on the real standalone runsc deployment through the backend-neutral facade.
  • The RFC issue and PR description now document character-class semantics, malformed-pattern rejection, top-level expansion counting, and the 214-test result.

I also replied directly to both inline discussions. Could you please take another look when convenient?

@Peng-YM

Peng-YM commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

The latest upstream/main (ab160fe) has been merged in a new, standalone merge commit: d1001ce.

The two conflicts were resolved by preserving both sides:

  • sdk/python/akernel_sdk/sandbox.py: kept the new network_policy API together with context, auto_start_cmd, and build_run_timeout.
  • sdk/python/README.md: documented both constructor surfaces.
  • Wrapped the upstream address assertion that otherwise failed Ruff's line-length check.

Validation after the merge:

  • make sdk-check: 224 tests passed.
  • Ruff: passed.
  • mypy: passed for 24 source files.
  • Real standalone examples/dockerfile_launch.py: all 5 sections passed, including filtered COPY, ENTRYPOINT + CMD, COPY --chown, local tar extraction, and fail-closed prechecks.

The branch is clean, pushed, and now includes the current target branch. @tianyuzhou95, could you please continue with the next review round?

@Peng-YM
Peng-YM force-pushed the feat/dockerfile-sandbox-launch branch from d1001ce to aa8b94b Compare August 18, 2026 04:39
@Peng-YM

Peng-YM commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

I rebased this branch onto the latest main (a5670cd) and force-pushed the rewritten history. The current head is aa8b94b.

The latest main branch introduced the all-in-one Standalone E2E job. This PR now also adds dockerfile_launch.py to that job, so the current branch will exercise the Dockerfile direct-launch path in the freshly built standalone image.

The new workflow run is waiting for approval because it comes from a fork:
https://github.com/inclusionAI/AKernel/actions/runs/32099920266

@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:

  • 224 SDK unit tests passed
  • Ruff passed
  • mypy passed for 24 source files
  • deployment script syntax check passed

@tianyuzhou95 tianyuzhou95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sdk/python/akernel_sdk/_dockercontext.py Outdated
Comment thread sdk/python/akernel_sdk/_dockercontext.py
@Peng-YM

Peng-YM commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

The latest .dockerignore review findings are addressed in two new commits:

  • 1f3a8bf — Moby-compatible ignore preprocessing/matching, virtual ancestors for re-included descendants, parent-result reuse, bounded directory-stack memory, fail-closed RE2-only escapes, dependency/docs/tests.
  • ab2475a — expanded standalone coverage.

Key validation:

  • 231 unit tests passed; Ruff and mypy passed.
  • Deployment script checks and wheel build passed; the wheel has no pathspec dependency.
  • 87,417 supported pattern/path comparisons against moby/patternmatcher: no mismatch.
  • Real BuildKit parity passed for embedded **, cleaned paths, column-one comments, descendant re-inclusion, and COPY dot/literal/wildcard layouts.
  • Independent review result: APPROVE, with no remaining P0-P2.
  • The real standalone example passed all 9 sections.

The standalone example now adds one fresh sandbox per startup regression:

  • exec-form ENTRYPOINT without CMD;
  • shell-form CMD with WORKDIR;
  • shell-form ENTRYPOINT ignoring CMD;
  • auto_start_cmd=False suppressing dispatch.

RFC issue #17 and the PR description are updated. The new CI run is waiting for fork-workflow approval:
https://github.com/inclusionAI/AKernel/actions/runs/32125039298

@tianyuzhou95, could you please approve the run and continue the next review round?

@tianyuzhou95 tianyuzhou95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sdk/python/akernel_sdk/_dockercontext.py Outdated
Comment thread sdk/python/akernel_sdk/_dockercontext.py Outdated
@Peng-YM

Peng-YM commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Docker context behavior now follows the reviewed contract

New standalone commit: 5031036 (fix(sdk): align Dockerfile ignore context). Earlier commits were not amended or rebased.

  • The adjacent <Dockerfile>.dockerignore is selected ahead of the root .dockerignore whenever it exists, including an empty companion. Root fallback happens only when the companion is absent. The companion must be a regular file and is read through the no-follow path.
  • Dockerfile and ignore control files that physically belong to the filesystem context are now visible to COPY/ADD unless the active matcher excludes them. Inline Dockerfiles are not synthesized into the manifest, and external Dockerfiles with companions remain outside it.

I replied to and resolved both blocking inline discussions with the corresponding regression coverage. Validation for this revision: make sdk-check passed all 237 unit tests, Ruff, and mypy; make deploy-script-check passed; and the maintained nine-section example previously passed all nine sections against the real standalone runsc deployment.

@tianyuzhou95, could you please continue the review when convenient?

@tianyuzhou95 tianyuzhou95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread sdk/python/tests/unit/test_dockercontext.py
@Peng-YM

Peng-YM commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: f2cd220 is an independent, test-only commit that explicitly sets both fixtures to 0644. The 237-test quality gate passes under both umask 0022 and umask 0002. I replied to and resolved the new fixture-mode thread, and resolved the two older Go matcher and wildcard source-count threads that already had detailed replies. I did not rerun standalone because this commit changes only test fixture modes and leaves runtime behavior unchanged; the prior nine-section standalone evidence remains applicable. Please continue the review.

@tianyuzhou95 tianyuzhou95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sdk/python/README.md Outdated
Comment thread sdk/python/README.md Outdated
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>
@Peng-YM
Peng-YM force-pushed the feat/dockerfile-sandbox-launch branch from f2cd220 to 7506ecd Compare August 19, 2026 14:03
@Peng-YM

Peng-YM commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Rebase update:

  • Rebased onto upstream/main at 53c86e0; the new head is 7506ecd. The branch was updated with an explicit --force-with-lease.
  • The only conflict was in sandbox.py: Dockerfile direct-launch was retained while preserving upstream's backend-owned support for arbitrary non-empty runtime values. xpu and storage are no longer rejected by the SDK.
  • Upstream's equivalent style commit was skipped normally during rebase. All 9/9 branch commits carry DCO sign-offs.
  • All 237 tests pass under both umask 0022 and umask 0002; Ruff, mypy, and deployment-script checks also pass. The real standalone run passed all 9/9 sections and left 0 instances.

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>
@Peng-YM

Peng-YM commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up in independent DCO commit a18dee5 (docs(sdk): mark Dockerfile launch as supported). No existing commit was amended or rebased.

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 34b3215 remain unchanged: DockerfileLaunch is the single immutable launch configuration, image, rootfs, and dockerfile are mutually exclusive, and check_direct_launch(context) remains the preflight API. The README retains a short late entry; the complete contract remains in sdk/python/docs/launch-from-dockerfile.md.

Earlier validation remains applicable: make sdk-check passed 241 tests with Ruff and mypy, make deploy-script-check passed, and the real standalone runsc example passed 9/9 sections. Commit a18dee5 changes docs and docstrings only, so standalone was intentionally not rerun. Please continue the review.

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>
@Peng-YM

Peng-YM commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

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 0f5a969 resolves the latest annotation and lifecycle review threads without rewriting earlier commits. DockerfileLaunch now lives in lightweight akernel_sdk._dockerfile_launch; Sandbox and apply_dockerfile() annotations resolve at runtime while parser and backend imports remain lazy. Compatibility import behavior is retained.

Current validation: make sdk-check passed 241 unit tests, Ruff, and mypy for 25 source files. The existing evidence also includes both umask 0022 and umask 0002 quality-gate runs, wheel and sdist build, make deploy-script-check, and real standalone runsc 9/9 sections in recorded 52-second and 47.07-second runs. The standalone example was not rerun for this revision because direct-launch runtime semantics are unchanged.

@tianyuzhou95 tianyuzhou95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sdk/python/akernel_sdk/sandbox.py
Comment thread sdk/python/README.md Outdated
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>
@Peng-YM

Peng-YM commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: runtime annotations and lifecycle wording

New independent DCO commit 0f5a969 (fix(sdk): resolve Dockerfile launch annotations) is pushed normally; no earlier commit was amended or rebased.

Both latest review threads are resolved individually:

  • get_type_hints: DockerfileLaunch is now in lightweight _dockerfile_launch.py; Sandbox and apply_dockerfile() annotations resolve at runtime while parser and backend imports remain lazy. Regression coverage exercises three import orders and the 91-public-callable scan.
  • Lifecycle: I accepted the concern that tests do not prove permanent API maturity and removed the permanent-API and only-backward-compatible-additions promises. The product decision remains that Dockerfile direct launch is supported and remains available. Its API may evolve with production experience, with documentation and migration guidance for material changes. The strict subset remains fail-closed.

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 make sdk-check passed 241 tests, Ruff, and mypy for 25 source files. Recorded evidence also includes both umask gates, wheel and sdist build, deployment-script checks, and standalone runsc 9/9 sections in 52-second and 47.07-second runs. The standalone example was not rerun because this revision does not change runtime semantics.

Could you please continue the review?

@tianyuzhou95
tianyuzhou95 merged commit b66f270 into inclusionAI:main Aug 20, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants