Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[ENV-A1] ADR: define sandbox backend contract, trust boundaries and rollout compatibility

Open
#267 2 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Active
Tech stack
linux, python
Domain
backend, security

Research direction

Read EPIC #266 and accepted ADRs 0002, 0004, 0005, 0006, and 0008, then inspect the listed surfaces: docs/adr, src/autoforge/sandbox.py, sandbox_policy.py, sandbox_bwrap.py, executor.py, executor_duplex.py, and docs/agent-guides/architecture.md. Draft an ADR defining the backend contract, trust boundaries, capability matrix, rollout decisions, and an auditable test plan; coordinate any state schema change with #23. Done means the ADR includes the create → stage → run → export → verify → cleanup sequence and maps each preserved invariant.

Written by the indexing model from the issue text.

Description

enhancement

Artifact: implementation-ready ADR design issue. Parent EPIC: #266. Workstream: A. Size: L. Baseline: main 711ff09 (2026-10-21). No implementation is authorized by this planning action.

Objective / scoped deliverable

Define host-vs-isolated execution model and authoritative interface from existing LaunchDescription to backend create/exec/collect/cleanup; capability conformance, launch classes (REMOTE/LOCAL/validation/premerge), private runtime identity and process supervision; explicitly reject Sandcastle runtime/branch ownership. Decide whether execution abstractions wrap executor requests or extend the existing bwrap prepare seam, failure taxonomy, pure plan and atomic persisted state. Compare Docker/Podman/remote tradeoffs and include a threat model (host socket, root, filesystem, network, secrets, repo hooks).

Existing foundations and boundaries

Read EPIC #266 and relevant accepted ADRs 0002, 0004, 0005, 0006, 0008. The deterministic controller retains every workflow transition, Git/GitHub write, read-back, ReviewCertificate and merge authorization. Do not embed Sandcastle's run(), replace the Python agent providers or introduce an arbitrary privileged mount. Preserve the Linux bwrap baseline. Coordinate session/interaction overlaps with #147/#126. If state schema must change, obtain protocol-number coordination on #23 before the migration.

Dependencies

  • Hard: none (foundational design; #158 and #176 already closed).
  • This issue must not be considered complete by filing a follow-up for a core acceptance criterion; split decisions before implementation only if the EPIC ledger is updated.

Relevant implementation surfaces (starting points, not an exhaustive mandate)

docs/adr, src/autoforge/sandbox.py, sandbox_policy.py, sandbox_bwrap.py, executor.py, executor_duplex.py, docs/agent-guides/architecture.md

Acceptance criteria

  • ADR approved with an interface and sequence diagram for create → stage → run → export → verify → cleanup
  • Preserve unaffected invariants and explicitly amend superseded decisions in dated, reviewed ADR amendments, with a written invariant-by-invariant map that marks each invariant of ADRs 0002/0004/0005/0006/0008 as preserved or amended (and, if amended, by which amendment); see "Planning reconciliation (2026-10-23)" below for the decisions the Docker backend is known to amend
  • Defines explicit fail-closed platform capability matrix, LOCAL-vs-REMOTE semantics and migration/rollout decisions
  • Lists config/state impact and delegates state protocol number assignment to #23; no number claimed in planning
  • No code/working runtime required; reviewers can implement independent children without reopening architecture questions
  • Existing regression suite passes; focused negative and crash-window tests are attached to the PR, or a design-only issue includes an auditable test plan.
  • Update documentation and backend capability/rollout status when user-facing behavior changes; do not represent a skipped live test as a pass.

Exclusions / anti-goals

Do not weaken sandbox.mode: required, allow agent-held GitHub write credentials, expose host .git common-dir/state to isolated agents, silently apply raw REMOTE diffs, or invent a second lifecycle controller. EPIC #255 Public Alpha is not blocked by this task.

Verification guidance

Use offline fake executable/provider fixtures first, then explicitly gated disposable-repository real-backend tests when applicable. Record exact candidate/base SHAs and externally observed failures, ensure secure redaction, and demonstrate idempotent cleanup and resume.

Design input: Dev Containers' host-owned identity and disposable runtime

  • ADR captures the principle “身份留在宿主机、执行环境可以重建”: a long-lived operator-controlled credential/session authority is decoupled from a replaceable container/run workspace. Define the distinct ownership and lifetime of: (1) provider login/refresh state, (2) one-run projected secret/capability, (3) agent conversation/session data, (4) Git candidate artifacts, (5) controller GitHub write credentials.
  • Draw identity/secret projection and container destroy/re-create/reconcile sequences: a replacement sandbox obtains a new scoped projection from the host-managed authority, never restores full host HOME, credential helper, SSH agent or GitHub token, and never relies on copying an arbitrary database mid-transaction.
  • Threat-model interactive VS Code Dev Containers vs. untrusted unattended agents. The Dev Containers spec describes configurable environments/secrets, not a universal OAuth broker; VS Code Git credential forwarding cannot be imported as an AutoForge GitHub-auth mechanism.
  • Assign concrete contract to #270 (delivery), #290 (Claude Code/OpenCode 2.x/Pi refresh/store), and #272 (lifecycle/recovery), without introducing a second controller or requiring Codex support.

Reference: Dev Containers first-class secrets.

Workspace and host-command authority decision (2026-10-23)

  • Make #292's host-prepared exact-SHA snapshot → read-only /source mount → private writable /workspace the Docker MVP default; explicitly decide how self-contained private Git objects are seeded from #269 without mounting the host's linked-worktree common-dir.
  • ADR must specify all code modifications, compilation, dependency installation and test execution inside the container, with no shell/build fallback on the host. Host's gh performs GitHub Issues/PR/checks and approved controller-owned effects; host's hardened git performs strict candidate import, verified fetch/push.
  • Distinguish source snapshot from operator's live/dirty checkout: never implicitly bind a mutable project tree, $HOME, .git admin, host credentials, sockets or controller state. Clarify that read-only bind by itself does not secure against later host-side mutation of its source path: stage and verify immutable content.
  • Include optional writable issue-only bind as a separately reviewed future alternative, not the default or a way around exact candidate verification.

Planning reconciliation (2026-10-23)

Artifact: planning update from the 2026-10-23 reconciliation of EPIC #266 with its architecture review (#266 comment). Baseline re-read: main 388ecb3. Planning only: nothing here implements a feature or checks off a criterion. The earlier sections stay authoritative where this one does not change them.

Why "preserves" became "amends"

The review found that a container backend cannot preserve every accepted decision. It contradicts some of them as written. Quietly preserving them would leave two incompatible contracts, so this ADR must amend each one explicitly. These are the known amendments (anchors verified on 388ecb3):

Accepted decision What it says today Why Docker cannot preserve it Amendment owner
ADR 0005 §2.6 / D6.1 (docs/adr/0005-outer-sandbox-and-host-resource-capability-model.md:700, :717, :724) Chooses Design A, the host-linked worktree: the common dir is projected read-only, with a private object directory overlaid on the shared store. Rejects Design C, a satellite clone per issue The Docker MVP mounts no host common dir at all (#266, #270). Its objects come from a seeded private repository inside the container, which is a satellite/private-repository design. The amendment records how the choice between host-linked worktree and private repository is made per backend, and why §2.6's reasons for rejecting C do or do not apply #267 (decision), #292 / #269 (mechanism)
ADR 0005 D6.3 (:747) Candidate identity is the issue worktree's HEAD, read through the common dir, in every mode A Docker launch has no host worktree HEAD to read. The candidate is an exported, strictly imported artifact #267 (decision), #269 (engine candidate source)
ADR 0005 D6.5 (:835) Strict import of the private object directory after every enforced launch Keep the strictness. The input changes from a host private object dir to a container-produced, untrusted pack #269
ADR 0005 D1.2 (:316) "Sandboxed" is one fact about one launch, meaning the outer bwrap sandbox was enforced Two backends need a backend-qualified fact. An enforced Docker launch and an enforced bwrap launch make different guarantees #267, #268 (SandboxFacts), #273 (diagnostics)
ADR 0005 D9.1 (:1139) Launches share the host network namespace, with abstract Unix sockets scoped by Landlock The Docker MVP uses a bridge network or --network none, never --network host, so the launch has its own network namespace and the Landlock abstract-socket scope is not the mechanism that isolates it #267, #270
ADR 0005 D14.1 (:1512) Capability is proven by a real minimal bwrap-shaped launch. The table requires Linux 6.12+ with Landlock ABI 6 Docker needs its own probe and capability table: runtime identity, rootless or rootful, image digest, mount and user enforcement #267, #273
ADR 0005 §9 (:1896, :1906, :1907) Non-goals: no container runtime dependency, no daemon, no egress mediation A Docker backend is a container runtime dependency on a daemon. Egress mediation stays a non-goal of the MVP and moves to #294 #267
ADR 0002 §4e (docs/adr/0002-executor-nothing-outlives-the-invocation.md:373, :414) and ADR 0005 D10.6 (:1265) The outer process is the executor's child. Controller death kills the sandbox through --die-with-parent (PR_SET_PDEATHSIG). Inner leftovers die with the PID namespace A container belongs to the Docker daemon, not to the controller. Killing the docker CLI child does not stop the container, so the death-kill guarantee cannot hold as written #267 (replacement contract below), #272 (implementation)
ADR 0005 D15.3 (:1579) sandbox.mode is configuration and is not pinned across steps Already being amended by #280 (LAUNCH-E F1). #273's backend pin must use the same mechanism, not a second one #280, then #273
ADR 0006 D6.1 (docs/adr/0006-candidate-validation-evidence-and-review-certificates.md:611) The candidate's objects are in the shared store before materialization Preserved. The Docker path still imports strictly into the shared store first (#269). It is listed here so the map shows it was checked —

ADR 0004 (authority boundary, typed effects, D7.3/D7.5 hardening and re-hash) and ADR 0008 (run identity, claims, lock L1 around index-pack) are expected to be preserved. The map must still list each invariant.

Decisions this ADR must record
  1. Authority. The host controller keeps every workflow transition, Git and GitHub write, read-back, ReviewCertificate and merge authorization. A container runs agents and repository commands, and holds no GitHub credential.

  2. Source vs workspace (default). /source is a controller-prepared, exact-SHA, read-only snapshot built by reusing materialize() (src/autoforge/materialize.py:690). /workspace is sandbox-private and writable. Option C (copy-in/copy-out without a bind mount) is used only where a bind is impossible or the operator forces it (decision 12, owner decision 2026-10-23). It does not replace this default on runtimes proven to share the controller's filesystem.

  3. Lifetimes. Per launch, per phase and per run: how long each of these lives, and who owns it. They are the image, the container, the /workspace volume, the projected model credential (#290), the agent session data (#278) and the candidate artifact (#269).

  4. FIX seeding. A FIX launch starts from the last verified candidate, the one the controller imported and bound to the run. It never starts from an earlier container's unverified workspace.

  5. Candidate import ownership. #269 owns the semantics of turning a container-produced artifact into a strictly imported candidate, and owns the engine's candidate-source abstraction. The bwrap path keeps reading the worktree HEAD.

  6. Supervision. The D10.6 replacement contract below. #272 implements it.

  7. Image. Who builds the image, digest pinning, where the agent CLIs come from, and toolchains. #271 owns the contract, #273 owns the diagnostics. Owner decision (2026-10-23): AutoForge installs the agent CLIs (Claude Code, OpenCode 2.x, Pi) itself, and the operator may configure MCP servers and Skills that AutoForge installs together with them (comment).
    The ADR must settle how:

    • Where installation runs. It happens when AutoForge builds the image, inside a build container and never on the host, at pinned versions. The image is cached under a content key, so an unchanged configuration reuses it (least resources).
    • Where MCP and Skills configuration comes from. Operator configuration only, under strict key validation. Never from the repository or an agent.
    • The conflict with today's isolation policy. Installing is not enabling. Today every launch runs with MCP servers, plugins, hooks and skills off, because an integration can carry its own credential or reach GitHub. That is Claude Code's --safe-mode --strict-mcp-config (src/autoforge/providers.py:786), Pi's --no-extensions --no-skills, OpenCode's refusal of integrations, #165, docs/pi-policy.md and docs/sdk-policy.md. Enabling an operator-configured integration in a Docker launch needs a recorded policy amendment, which must state:
      • what each integration may load;
      • which credentials it may receive (#290's projection rules; never a GitHub credential);
      • that it runs inside the same container under the same network policy.
    • Diagnostics. #273 shows each installed CLI, MCP server and Skill with its version, and whether it is enabled.
  8. Backend selection and capability semantics. sandbox.backend is separate from sandbox.mode. What required and preferred mean for Docker. Owner decision (2026-10-23): rootless Podman on Linux joins the MVP as a second container runtime (#275, moved from Wave C). A backend is selectable only after its lifecycle and cleanup land (#272), not merely after its launch tests pass. A run's backend is pinned (#273, sharing #280's mechanism). Owner decision (2026-10-23): one container backend. Podman is a runtime it detects, not a second sandbox.backend value. The ADR must still settle:

    • The value's name. A neutral name such as container avoids calling Podman docker. Until the ADR names it, issue text says docker.
    • Which runtime is used when both are present. A deterministic detection order, and whether an operator setting may name one. A repository setting never may.
    • The runtime pin. The detected runtime is part of SandboxFacts (#268) and is pinned with the run (#273). A resume, step or unblock that detects a different runtime is refused, the same way a different backend is. Whether a new version of the same runtime only re-runs the capability probe is for the ADR to decide.
  9. Network. The bridge vs --network none mapping per launch class, and the documented residuals R3/R4 from #257. Bridge networking does not restrict traffic to model endpoints. Egress mediation is #294 (post-MVP).

  10. Rootless vs rootful. What each mode means for the trust boundary. Whether rootful is supported, refused, or supported only with explicit diagnostics. #270 supplies the input. Owner decision (2026-10-23): rootless only. A rootful daemon is refused before anything of the step is saved or launched, and doctor names the reason (#273). The ADR records the rootless trust boundary (#270).

  11. Git history depth. How much history the seeded private repository carries, measured against ADR 0005 D6.7, which accepts exposing the operator's unpushed work in REMOTE today. Decide whether the Docker path exposes less, for example only objects reachable from the base SHA. Owner guidance (2026-10-23): use as few resources as possible, as long as AutoForge still runs correctly. Recommended default, for the ADR to confirm (analysis in the comment):

    • the base commit only (depth 1), plus the verified candidate's own commits;
    • no tags, other branches, remote or reflogs;
    • more history or tags only through explicit operator settings, for builds that need git describe-style data.
  12. Read-only /source bind vs copy-in/copy-out (Option C). Option C drops the /source bind. It streams a seed pack (the base or the current candidate) into a fresh volume and exports the candidate's pack the same way, so no host path is mounted into the agent's container. It removes the host-side-mutation, rootless UID-mapping and SELinux-relabel problems of a bind, and costs more for large repositories. Owner decision (2026-10-23): the choice is made per runtime, by proven capability, not one global default.

    • Bind, when the runtime is proven to share the controller host's filesystem (local rootless Docker, local rootless Podman). /source is a read-only bind of the controller-owned staging directory. The staged tree is re-proved before hand-off (#292). This needs no copy and uses the fewest resources. It keeps the owner's earlier decision for these runtimes.
    • Copy-in/copy-out everywhere else. That covers remote execution sandboxes (#276, #277), a daemon inside a VM (Docker Desktop, podman machine), and a DOCKER_HOST or docker context that points elsewhere. On those, a bind path would resolve on another machine. No separate /source is sent. Only the workspace seed pack is copied in, because it already carries the exact tree.
    • Proof, not names. #271's probe decides which case applies by checking a nonce file in the staging root from inside the container. "Local", "remote", a docker basename or a context name is never taken as proof.
    • Operator override, one direction only. An operator may force copy-in on a runtime that can bind, for example to avoid SELinux relabelling or UID mapping. Forcing a bind where the probe did not prove a shared filesystem is never allowed.
    • The output is the same everywhere. It is a bounded Git pack plus manifest, strictly imported (#269). rsync may only serve as a transport optimisation underneath the Git-object integrity check. It is never the content boundary, and never the way files come back, because that would be applying a raw diff.
    • Terminology. "Remote environment" here means a remote execution sandbox. It is not AutoForge's REMOTE workflow mode.

    The optional writable issue-only bind (above) is a separate, later alternative. Neither it nor copy-in is a way around exact candidate verification.

D10.6 replacement contract (Docker)

The ADR must replace "controller death kills the sandbox" for container backends with a contract the daemon model can actually meet:

  • Every container and volume gets a run-scoped name and ownership labels: run key, issue, phase, launch id, and a per-launch nonce the controller records before creating it. Nothing is stopped or removed unless its labels and nonce match a journal entry.
  • The controller supervises a launch through a liveness channel separate from the agent's stdin. Pi RPC duplex and OpenCode 2.x stdin are never borrowed.
  • On every exit path the controller can run (success, failure, timeout, stop, Ctrl-C), it stops and removes the container and its secret projection, and it journals that intent first.
  • On step, resume, unblock and scheduler restart, reconciliation finds this run's leftovers by label and nonce and removes them. A container whose ownership cannot be proven is reported and leads to BLOCKED. It is never guessed at or force-removed.
  • Residual, stated plainly: after SIGKILL of the controller, a closed laptop or a reboot, a container can keep running until the next reconciliation or until it exits by itself. --rm, docker rm -f, --init and startup cleanup reduce this window. None of them guarantees immediate death. Docs and diagnostics must never claim otherwise. Under the owner's 2026-10-23 decision below, a death-watch and an in-container deadline bound this window.
  • Owner decision (2026-10-23): a brief, bounded survival is an accepted residual under sandbox.mode: required. It follows the recommended contract in the comment. #272 implements and measures it, #273 reports it, and #274 proves it on rootless Docker and rootless Podman.
    • A death-watch as PID 1.
      • The container's PID 1 is an AutoForge-supplied watcher. It runs under a different UID from the agent.
      • It reads a liveness stream that the controller keeps open. Only the controller holds the write end, which is close-on-exec. The stream reaches the container over a runtime channel separate from the agent's stdin (#272 designs and proves that transport).
      • When the controller dies for any reason, the stream reaches end-of-file and PID 1 exits. The kernel then ends every process in the container's PID namespace.
      • This relies on #270's policy that the agent runs as a non-root user with all capabilities dropped.
    • A hard deadline inside the container. PID 1 enforces the launch's runtime limit, so a failed watch still ends at a known time.
    • Reconciliation (above) removes the remains: stopped containers, volumes and credential projections. It does not bound how long a container lives.
    • Proof under required. The probe and doctor kill a stand-in parent and check that the container is gone within a stated, measured time. A host that cannot prove this is refused under required before anything of the step is saved or launched.
    • What is accepted:
      • the watch's detection delay;
      • only if the watch itself fails, survival up to the in-container deadline.
    • What is not accepted: a survival bounded only by the next reconciliation is not accepted under required. No immediate-death claim is made for --rm, rm -f, --init or startup cleanup.
    • Other cases. No restart policy is set, so nothing comes back after a reboot. A suspended laptop runs nothing, the controller included.
Ownership map (who owns what after this reconciliation)
Concern Owner
ADR amendments and the 12 decisions above #267
Backend-neutral lifecycle seam, backend identity in SandboxFacts #268
Container-produced candidate import, engine candidate source, FIX seeding #269 (semantics), #271 (wiring), #274 (proof)
Container security policy, network mapping, credential-delivery mechanism, residuals R3/R4 #270
Host-side exact-SHA snapshot and private-workspace seed staging (backend-neutral) #292
Model credential lifecycle for Claude Code, OpenCode 2.x and Pi (no Codex) #290
Docker wiring, image contract, EXPORT_COMMAND in a container, runtime identity detection, shared-filesystem probe #271
Rootless Podman on Linux as a second runtime (moved into the MVP 2026-10-23) #275
D10.6 replacement: journal, labels, reconciliation, orphan and projection cleanup #272
Backend selection and pin, doctor and diagnostics #273
End-to-end real-backend proof (MVP gate) #274
Egress mediation (post-MVP) #294
Config and state impact

The persisted-state needs of #269, #272 and #273 are in one consolidated coordination request on #23 (comment). It assigns no protocol number. The ADR must also state the FINGERPRINT_SCHEMA = 1 constraint (src/autoforge/config_fingerprint.py): the recorded configuration refuses any other schema on load, so adding a backend key needs an explicit absent-key rule or a separate state field.

Additional acceptance criteria (planning reconciliation)
  • Each amendment in the table above is written as a dated, reviewed amendment section in the affected ADR (or an explicit "no amendment needed" with its reason), and the invariant-by-invariant map links to it
  • The 12 decisions above are recorded, each with its rejected alternatives; decision 12's per-runtime rule (bind only where the probe proves a shared filesystem, copy-in otherwise) is recorded
  • The D10.6 replacement contract, its residual, and the owner's decision (a brief survival bounded by the death-watch and the in-container deadline is accepted under sandbox.mode: required; a host that cannot prove the watch is refused) are recorded; no text claims --rm, rm -f, --init or startup cleanup gives an unconditional immediate-death guarantee
  • Decision 8 is recorded: one container backend with a detected runtime. The backend value's name, the detection rule when both runtimes are present, and the runtime pin are settled
  • The ownership map is reflected in each child's acceptance criteria, so no child depends on a later child's acceptance to complete its own (the #292↔#271 cycle is removed)
  • The ADR states that #255 Public Alpha does not depend on this EPIC
Dominant language
Python
Stars
0
Forks
0
Avg merge
5h 51m
Merged PRs (30d)
66

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from iceboundrock/AutoForge

All issues in iceboundrock/AutoForge

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.