From fda5a359dbebca027e5827b33508d578ba287107 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sat, 3 Oct 2026 22:57:35 +0000 Subject: [PATCH 1/5] feat(kits): add oci-engine to the opt-in catalog Expose the functional oci-engine kit through the generic configure catalog and cover catalog-name expansion to the pinned patterns ref. Co-authored-by: OpenCode [gpt-5.5] --- acq.backends/common.sh | 7 +++-- test/bats/135-configure.bats | 27 ++++++++++++++++++- test/bats/95-kit-ref-persistence.bats | 37 +++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/acq.backends/common.sh b/acq.backends/common.sh index 7e783ce8..becc2984 100644 --- a/acq.backends/common.sh +++ b/acq.backends/common.sh @@ -110,13 +110,16 @@ fi # directory under PATTERNS_KIT_DIR; DESC[i] is the one-line picker description. # `prime-agent` is intentionally omitted: it is a skeleton at the current pin # (see the patterns kits.yaml parity note) and would be non-functional if -# offered. Add it here once it is functional. +# offered. Add it here once it is functional. Heavier kits such as `oci-engine` +# still belong here when they are functional; the picker description must make +# their create-time cost clear. # shellcheck disable=SC2034 # consumed by `acq configure` (cross-function reader) -ACQ_OPTIN_KIT_NAMES=(openchamber paseo) +ACQ_OPTIN_KIT_NAMES=(openchamber paseo oci-engine) # shellcheck disable=SC2034 ACQ_OPTIN_KIT_DESCS=( "Browser UI for OpenCode alongside the terminal TUI (publishes ports 3000/4096)" "Self-hosted Paseo browser web UI for coding agents (one port, loopback only)" + "Rootless podman for docker run / docker compose inside the sandbox (installs packages)" ) # _acq_optin_kit_ref NAME — echo the fully-pinned git+https kit ref for an opt-in diff --git a/test/bats/135-configure.bats b/test/bats/135-configure.bats index 11c30224..a2a5e7d1 100644 --- a/test/bats/135-configure.bats +++ b/test/bats/135-configure.bats @@ -152,6 +152,18 @@ _cfg_src() { assert_output --partial 'dir=integrations/isolation/acq-kits/openchamber' } +@test "catalog: oci-engine is offered and expands to the pinned ref" { + run bash -c ' + ACQ_SOURCE_ONLY=1 . "'"$ACQ"'" >/dev/null 2>&1 + printf "%s\n" "${ACQ_OPTIN_KIT_NAMES[@]}" + _acq_optin_kit_ref oci-engine + ' + assert_success + assert_line 'oci-engine' + assert_output --partial 'ref=2dd2ad6ad5f63b842d732424bed5ba9aebeed676' + assert_output --partial 'dir=integrations/isolation/acq-kits/oci-engine' +} + @test "catalog: prime-agent is intentionally NOT offered (skeleton at pin)" { run bash -c ' ACQ_SOURCE_ONLY=1 . "'"$ACQ"'" >/dev/null 2>&1 @@ -160,6 +172,7 @@ _cfg_src() { assert_success assert_line 'openchamber' assert_line 'paseo' + assert_line 'oci-engine' refute_line 'prime-agent' } @@ -358,6 +371,17 @@ _cfg_src() { assert_line 'scope_github_token: yes' } +@test "configure: selecting oci-engine persists the catalog name" { + export XDG_CONFIG_HOME="$STUBDIR/xdg-oci" + run env ACQ_BACKEND=sbx ACQ_PROMPT_TEST_INPUT="DOWN DOWN SPACE ENTER n" "$ACQ" configure + assert_success + assert_output --partial 'oci-engine' + assert_output --partial 'Rootless podman for docker run / docker compose' + run cat "$XDG_CONFIG_HOME/acq/config.yaml" + assert_line 'extra_kits: oci-engine' + assert_line 'scope_github_token: no' +} + # --------------------------------------------------------------------------- # configured-default extra-kit application + env precedence # --------------------------------------------------------------------------- @@ -365,7 +389,7 @@ _cfg_src() { @test "apply: configured extra_kits expand to pinned refs" { export XDG_CONFIG_HOME="$STUBDIR/xdg5" mkdir -p "$XDG_CONFIG_HOME/acq" - printf 'extra_kits: openchamber paseo\n' > "$XDG_CONFIG_HOME/acq/config.yaml" + printf 'extra_kits: openchamber paseo oci-engine\n' > "$XDG_CONFIG_HOME/acq/config.yaml" run bash -c ' ACQ_SOURCE_ONLY=1 . "'"$ACQ"'" >/dev/null 2>&1 ACQ_EXTRA_KITS=""; ACQ_EXTRA_KITS_FROM_ENV="" @@ -375,6 +399,7 @@ _cfg_src() { assert_success assert_output --partial 'dir=integrations/isolation/acq-kits/openchamber' assert_output --partial 'dir=integrations/isolation/acq-kits/paseo' + assert_output --partial 'dir=integrations/isolation/acq-kits/oci-engine' } @test "apply: unknown configured extra_kits names fail closed" { diff --git a/test/bats/95-kit-ref-persistence.bats b/test/bats/95-kit-ref-persistence.bats index 20595d62..1ce8472b 100644 --- a/test/bats/95-kit-ref-persistence.bats +++ b/test/bats/95-kit-ref-persistence.bats @@ -55,6 +55,43 @@ load 'helper' assert_regex "$ACQ_EXTRA_KITS" '/kits/b' } +@test "cli-kits: configured oci-engine catalog kit survives fresh-shell start" { + export XDG_CONFIG_HOME="$STUBDIR/xdg-oci-start" + mkdir -p "$XDG_CONFIG_HOME/acq" + printf 'extra_kits: oci-engine\n' > "$XDG_CONFIG_HOME/acq/config.yaml" + local okit="$STUBDIR/okit" + mkdir -p "$okit/files" + cat > "$okit/spec.yaml" <<'SPEC' +schemaVersion: "hybrid/v1" +kind: mixin +name: oci-engine +files: + - path: /home/agent/oci-engine-marker + mode: "0644" + source: files/marker +SPEC + printf 'OCI_ENGINE_MARKER\n' > "$okit/files/marker" + printf 'ocistartbox\n' > "$STUBDIR/.msb_sandbox_list" + printf 'ocistartbox\n' > "$STUBDIR/.msb_running_list" + load_acq + ACQ_EXTRA_KITS="" + # shellcheck disable=SC2034 # read by sourced _acq_apply_configured_extra_kits + ACQ_EXTRA_KITS_FROM_ENV="" + _acq_apply_configured_extra_kits + _build_kit_list shell + acq_cli_kits_write msb ocistartbox + ACQ_EXTRA_KITS=""; ACQ_CLI_KITS=() + : > "$CALLS" + run env ACQ_MSB_KIT_LOCAL_DIR="$okit" ACQ_BACKEND=msb XDG_CONFIG_HOME="$XDG_CONFIG_HOME" "$ACQ" start ocistartbox + assert_success + assert_output --partial "started 'ocistartbox'" + local log; log=$(cat "$CALLS") + assert_regex "$log" 'ocistartbox:/home/agent/oci-engine-marker' + run cat "$(_acq_cli_kits_file msb ocistartbox)" + assert_output --partial 'dir=integrations/isolation/acq-kits/oci-engine' + assert_output --partial 'ref=2dd2ad6ad5f63b842d732424bed5ba9aebeed676' +} + @test "cli-kits: backend keying — an sbx read does not see an msb record" { load_acq ACQ_CLI_KITS=("/kits/msbonly"); ACQ_EXTRA_KITS="" From b555c3b71d79ca808eeba63c69af30b48d934cc1 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sat, 3 Oct 2026 22:58:01 +0000 Subject: [PATCH 2/5] feat(msb)!: retire oci adapter auto-install Remove the msb-owned podman installation, short-name-mode knob, and unconditional OCI device grants. OCI support now belongs to the opt-in oci-engine catalog kit instead of the adapter default. BREAKING CHANGE: New msb sandboxes no longer install podman automatically. Users who need docker run or docker compose inside the sandbox must select oci-engine with acq configure, or pass the pinned oci-engine kit ref through ACQ_EXTRA_KITS or --kit. Closes: GSA-TTS/agentic-coding-quickstart#505 Co-authored-by: OpenCode [gpt-5.5] --- acq.backends/msb.sh | 366 +------------------------------ scripts/test-acq-lib.sh | 19 -- test/bats/72-msb-agent-user.bats | 77 +------ 3 files changed, 12 insertions(+), 450 deletions(-) diff --git a/acq.backends/msb.sh b/acq.backends/msb.sh index 9b0dae66..62143381 100644 --- a/acq.backends/msb.sh +++ b/acq.backends/msb.sh @@ -508,7 +508,7 @@ ACQ_MSB_NPM_HOSTS="${ACQ_MSB_NPM_HOSTS:-registry.npmjs.org}" # # ALL tiers keep deny-by-default EXCEPT open; the tier only sizes the baseline # allowlist. This normalizes to a lowercase enum, fail-closed to `balanced` on an -# invalid value (matching ACQ_MSB_SHORT_NAME_MODE's validator). +# invalid value. # # DEPRECATED ALIAS — `ACQ_MSB_BALANCED_EGRESS` predates the neutral tier and is # retained for one deprecation window. It maps into the tier fail-safe (tighter, @@ -560,103 +560,6 @@ unset _acq_net_tier_source _acq_msb_balanced_egress_alias # never used to provision cannot abort an unrelated acq invocation. ACQ_NETWORK_TIER_CONFIRM_OPEN="${ACQ_NETWORK_TIER_CONFIRM_OPEN:-}" -# --------------------------------------------------------------------------- -# OCI container engine (podman) — ensure agents can run OCI images (ADR-0020) -# --------------------------------------------------------------------------- -# Agents frequently need to run OCI images inside the sandbox (e.g. `docker run`, -# bringing up a docker-compose.yaml). The default image ships the Docker CLI + -# compose plugin, but msb's microVM init (/init.krun) never starts dockerd, so -# the Docker socket is dead; and dockerd's overlay2 storage driver cannot sit on -# the sandbox's already-overlay root without a disk-backed data volume. Rather -# than retrofit the msb docker:dind entrypoint recipe (a daemon we would have to -# start and keep alive across restarts, plus a per-sandbox disk-backed volume), -# the adapter provisions **podman** — a daemonless engine that forks runc/crun -# per invocation (no socket, no restart lifecycle), uses fuse-overlayfs on the -# overlay root (no disk-backed volume), and needs no nested virtualization. We -# run podman ROOTLESS as the agent user: the install step installs the rootless -# prerequisites (uidmap for newuidmap/newgidmap, passt + slirp4netns for rootless -# networking) from the same mirror in the same step as podman itself, and grants -# the agent access to /dev/net/tun (group-scoped) so rootless networking can set -# up. Rootless keeps containers unprivileged (defense-in-depth), aligns container/ -# host UIDs, and lets the agent invoke podman directly (no sudo wrapper). See -# ADR-0020 and _acq_msb_ensure_oci. -# -# podman is CLI-compatible with docker for the run/build/compose workflows this -# targets, and the bundled Docker CLI is non-functional here anyway (dead -# socket), so we alias `docker` -> podman (in /usr/local/bin, ahead of /usr/bin) -# so both `docker run …` and `docker compose …` route to podman. `docker compose` -# resolves to `podman compose`, which drives the installed podman-compose -# provider — this is what makes docker-compose.yaml files usable (the standalone -# `docker-compose` CLI is deprecated in favour of the `docker compose` -# subcommand, so we do not provide a separate `docker-compose` binary). -# -# We also make unadorned image names resolve to Docker Hub by default (stock -# podman sends many short names to quay.io and has no default search registry), -# to reduce migration burden for users whose code assumes `docker run ` -# means Docker Hub. See _acq_msb_ensure_oci step 4 and ADR-0020. -# -# Toggle: on by default. Set ACQ_MSB_ENSURE_OCI=0 (or empty) to skip the step -# entirely (e.g. a base image that bakes its own working engine, or a lean -# sandbox that needs no OCI support). Normalized to exactly "1" (on) or "" (off): -# an unset value defaults on; "0"/"false"/"no"/"off"/empty are off -# (case-insensitive); anything else is on. -ACQ_MSB_ENSURE_OCI="${ACQ_MSB_ENSURE_OCI-1}" -case "$(printf '%s' "$ACQ_MSB_ENSURE_OCI" | tr '[:upper:]' '[:lower:]')" in - ""|0|false|no|off) ACQ_MSB_ENSURE_OCI="" ;; - *) ACQ_MSB_ENSURE_OCI="1" ;; -esac - -# The packages installed to provide the OCI engine (space-separated). Override -# for a different set or an internal mirror's package names. podman-compose is -# the `docker compose` / `podman compose` provider. fuse-overlayfs lets podman -# use the `overlay` graph driver on msb's overlay ROOT filesystem (the kernel -# `overlay` driver refuses to stack on overlayfs); without it the adapter falls -# back to the `vfs` driver, which works everywhere but is disk-heavy. uidmap -# (newuidmap/newgidmap), passt, and slirp4netns are the ROOTLESS prerequisites: -# uidmap provides the setuid helpers rootless podman needs to map the subuid/ -# subgid ranges (already present for `agent`), and passt/slirp4netns provide -# rootless container networking. See ADR-0020 and _acq_msb_ensure_oci. The install -# uses the OS package mirror (apt/dnf/apk), which under the default balanced egress -# baseline (ADR-0018) is already reachable (archive.ubuntu.com / ports.ubuntu.com -# / security.ubuntu.com / *.debian.org are in the vendored host list). With -# ACQ_NETWORK_TIER=strict (kit hosts only), or a base whose egress is otherwise -# narrowed, the mirror is unreachable and the install fails soft (a clear warning; -# provision continues; OCI is simply unavailable). -ACQ_MSB_PODMAN_PKGS="${ACQ_MSB_PODMAN_PKGS:-podman podman-compose fuse-overlayfs uidmap passt slirp4netns}" - -# podman short-name resolution mode written into the docker-first registries -# drop-in (/etc/containers/registries.conf.d/00-acq-docker-first.conf). This -# governs what happens when the agent runs an UNQUALIFIED image name (e.g. -# `docker run nginx`) that is not already fully qualified to a registry. -# -# DEFAULT: "enforcing" (least-privilege / prompt-injection defense). Per the -# PR #302 review (3-model consensus + reviewer), "permissive" is the WRONG -# default for a federal sandbox running a prompt-injectable agent: it silently -# resolves ambiguous short names, which removes the defense against image -# substitution / typosquatting (an injected `docker run nginx` could resolve to -# docker.io//nginx without any prompt). We KEEP -# unqualified-search-registries = ["docker.io"] below, so unqualified names -# still resolve deterministically to Docker Hub and migration ergonomics are -# preserved; "enforcing" only fails closed on interactively-ambiguous short -# names instead of silently resolving them. Because there is a single search -# registry, "enforcing" costs essentially no day-to-day ergonomics. -# -# Setting ACQ_MSB_SHORT_NAME_MODE=permissive is an EXPLICIT operator override -# that REMOVES the typosquatting / image-substitution guardrail. Only podman's -# accepted values are allowed: enforcing | permissive | disabled. Any other -# value (including empty) is rejected and falls back to "enforcing" -# (fail-closed), with a warning. -ACQ_MSB_SHORT_NAME_MODE="${ACQ_MSB_SHORT_NAME_MODE:-enforcing}" -_acq_msb_short_name_mode_lc="$(printf '%s' "$ACQ_MSB_SHORT_NAME_MODE" | tr '[:upper:]' '[:lower:]')" -case "$_acq_msb_short_name_mode_lc" in - enforcing|permissive|disabled) ACQ_MSB_SHORT_NAME_MODE="$_acq_msb_short_name_mode_lc" ;; - *) - printf 'msb: WARNING: invalid ACQ_MSB_SHORT_NAME_MODE=%s (expected enforcing|permissive|disabled); falling back to enforcing\n' "$ACQ_MSB_SHORT_NAME_MODE" >&2 - ACQ_MSB_SHORT_NAME_MODE="enforcing" - ;; -esac -unset _acq_msb_short_name_mode_lc - # Path to the vendored host list (a verbatim mirror of `sbx policy inspect # local-policy`; see acq.backends/msb-balanced-hosts.txt). Override to point at a # site-specific list. ACQ_SCRIPT_DIR is exported by the acq entry point (and set @@ -1114,10 +1017,6 @@ acq_backend_start() { [ "$_start_rc" -eq 0 ] || return "$_start_rc" _acq_msb_wait_for_exec_ready "$_name" || \ echo "acq(msb): warning: $_name did not become exec-ready after start." >&2 - # Re-grant the rootless-podman device nodes (/dev/net/tun, /dev/fuse): /dev is a - # devtmpfs re-created each boot, so the provision-time grant is lost across - # restart. Cheap + idempotent; no-op when ENSURE_OCI is disabled or absent. - _acq_msb_grant_oci_devs "$_name" # The host ssh-agent forward's --vsock route persists in the sandbox config # across stop/start, but the in-guest socat bridge process dies on stop, so it # must be (re)started here too. Gated on the persisted marker (no provision ran @@ -3735,18 +3634,6 @@ EOF acq_spin_stop "Preparing the agent user" acq_debug "msb provision: agent user ready ($name)" - # Ensure an OCI container engine (podman) so agents can run OCI images - # (docker run / docker compose). Idempotent + marker-gated; FAILS SOFT (a - # warning, never aborting provision) if the engine cannot be installed — e.g. - # the OS package mirror is unreachable under a narrowed egress. See ADR-0020. - acq_debug "msb provision: ensuring OCI engine ($name)" - if [ -n "$ACQ_MSB_ENSURE_OCI" ]; then - acq_spin_start "Ensuring an OCI engine (podman)" - _acq_msb_ensure_oci "$name" - acq_spin_stop "Ensuring an OCI engine (podman)" - fi - acq_debug "msb provision: OCI engine step done ($name)" - # Install the requested agent binary (sbx bakes it into the template image; on # a plain msb base acq must install it). Idempotent + marker-gated; a no-op for # `shell`, a clear warning for an agent with no known recipe. @@ -4313,8 +4200,8 @@ _acq_msb_check_socat() { # exposes the forwarded host ssh-agent as a unix socket at # ACQ_MSB_SSH_AGENT_GUEST_SOCK. The --vsock route persists across msb stop/start, # but the socat process dies on stop, so this runs on provision AND on -# acq_backend_start (mirrors _acq_msb_grant_oci_devs). Fail-soft: warns, never -# aborts. Works from EITHER the in-provision flag OR the persisted marker (start +# acq_backend_start. Fail-soft: warns, never aborts. Works from EITHER the +# in-provision flag OR the persisted marker (start # has no provision flag set), so the guest sock path is resolved from whichever # source is authoritative for the call. See ADR-0021. _acq_msb_start_ssh_agent_bridge() { @@ -4620,253 +4507,6 @@ _acq_msb_has_ssh_agent_vsock_route() { printf '%s\n' "$_flat" | grep -Eq "port[:=]${_port}\$" 2>/dev/null } -# --------------------------------------------------------------------------- -# _acq_msb_grant_oci_devs NAME — grant the agent access to the device nodes -# rootless podman needs (/dev/net/tun, /dev/fuse) -# --------------------------------------------------------------------------- -# Rootless podman needs unprivileged access to two root-only device nodes on the -# default image: -# - /dev/net/tun (crw------- root root): the network backend (netavark→pasta, -# or slirp4netns) must open it to set up container networking. -# - /dev/fuse (crw------- root root): the fuse-overlayfs storage driver (our -# PREFERRED driver on the overlay root) must open it to mount image layers. -# Without it `podman info` still passes but `podman run` fails at mount time -# with "fuse: failed to open /dev/fuse: Permission denied" — the exact -# info-OK-but-run-FAILS split seen on the host. -# Group-scope each to the agent (chown root:agent, chmod 0660) — inside the -# microVM only (the security boundary), narrower than world-writable, no new host -# attack surface. -# -# Called on EVERY provision pass (before the OCI install marker gate) AND on -# restart (acq_backend_start), because /dev is a devtmpfs re-created at each boot -# — a one-time grant would be lost after `msb start`. Idempotent, cheap, and a -# best-effort no-op for any device that is absent or when ENSURE_OCI is disabled. -_acq_msb_grant_oci_devs() { - local name="$1" - [ -n "$ACQ_MSB_ENSURE_OCI" ] || return 0 - _acq_msb_cli exec "$name" -u 0 -- sh -c ' - for _dev in /dev/net/tun /dev/fuse; do - if [ -e "$_dev" ]; then - chown root:agent "$_dev" 2>/dev/null || true - chmod 0660 "$_dev" 2>/dev/null || true - fi - done' \ - >/dev/null 2>&1 || true -} - -# --------------------------------------------------------------------------- -# _acq_msb_ensure_oci NAME — ensure an OCI container engine (podman) is usable -# --------------------------------------------------------------------------- -# Guarantee agents can run OCI images inside the sandbox (`docker run`, -# `docker compose up`, etc.). See the ACQ_MSB_ENSURE_OCI block above for the -# rationale (podman over dind; ROOTLESS podman run as the agent user; alias -# docker->podman). This step is idempotent + marker-gated and FAILS SOFT: if the -# engine cannot be provisioned (mirror unreachable under a narrowed egress, -# unknown package manager, rootless prereqs absent, etc.) it warns and returns 0 -# — provision continues, OCI is simply unavailable, exactly like the -# agent-install and prereq-check steps. The package INSTALL runs as root -# (`-u 0`, needed to install), but the engine RUNS rootless as the agent user. -_acq_msb_ensure_oci() { - local name="$1" - [ -n "$ACQ_MSB_ENSURE_OCI" ] || return 0 - - # Grant the rootless-podman device nodes (/dev/net/tun for networking, - # /dev/fuse for the fuse-overlayfs storage driver) on EVERY provision pass, - # BEFORE the install-marker short-circuit below. /dev is a devtmpfs re-created - # each boot, so this must not be gated behind the (persistent) install marker, - # and it is also re-applied on restart from acq_backend_start. - _acq_msb_grant_oci_devs "$name" - - local marker="/var/lib/acq/oci-ready" - if _acq_msb_cli exec "$name" -u 0 -- sh -c "test -f '$marker'" >/dev/null 2>&1; then - return 0 - fi - - # ACQ_MSB_PODMAN_PKGS is operator-controlled config, but it is interpolated - # into a root `sh -c` string below, so charset-guard it (package names are - # word-safe: letters, digits, . _ + - and spaces). Refuse anything else rather - # than risk shell injection into the elevated install. - case "$ACQ_MSB_PODMAN_PKGS" in - *[!A-Za-z0-9._+\ -]*) - echo "acq(msb): warning: ACQ_MSB_PODMAN_PKGS contains unsafe characters; skipping OCI setup." >&2 - return 0 - ;; - esac - - acq_debug "msb: ensuring an OCI engine (podman) in $name" - - # Root setup phase: install podman if absent (distro-detected, non-interactive), - # configure a storage driver that works on msb's overlay root, grant the agent - # access to /dev/net/tun (rootless networking needs it), write a Docker-Hub-first - # registries config, and wire the docker->podman alias. The engine itself RUNS - # ROOTLESS as the agent (verified separately, below) — only this install/config - # phase needs root. The whole block is best-effort; a non-zero exit is caught - # below and treated as non-fatal. - # - # The default image ships a (non-functional) docker CLI, so rather than gate on - # its absence we place our wrapper in /usr/local/bin (ahead of /usr/bin on the - # default PATH) to SHADOW it — the bundled docker talks to a dead socket, - # whereas our wrapper routes to the working podman engine. We overwrite our own - # wrapper idempotently but never touch the base image's /usr/bin/docker. - # - # STORAGE DRIVER: msb's sandbox root filesystem is itself an overlay mount - # (/.msb/rootfs/...). podman's default KERNEL `overlay` graph driver CANNOT stack - # on an overlay root — `podman info` fails with "'overlay' is not supported over - # overlayfs, a mount_program is required". This applies to BOTH rootful and - # rootless. So we write /etc/containers/storage.conf (honored by rootless as its - # lowest-precedence source) selecting a driver that works on an overlay root: - # - PREFER `overlay` + `mount_program=fuse-overlayfs` when fuse-overlayfs is - # present (msb provides /dev/fuse; this is the fast, thin-on-disk path), else - # - FALL BACK to `vfs`, which works everywhere with no extra package or - # /dev/fuse (correct but disk-heavy — full copy per layer). - # - # ROOTLESS NETWORKING: rootless podman's network backend (netavark→pasta, or - # slirp4netns) must open /dev/net/tun, which is root-only (crw------- root root) - # on the default image. We group-scope it to the agent (chown root:agent, 0660) - # so the unprivileged agent can set up container networking. This is inside the - # microVM only (the security boundary) — no new host attack surface. Applied on - # EVERY provision pass (outside the install marker gate) because the device node - # can be re-created with default perms across restarts. - # - # REGISTRY RESOLUTION (Docker-Hub-first, ADR-0020): stock podman resolves many - # unadorned short names to quay.io (e.g. `hello-world` -> quay.io/podman/hello) - # and has no default unqualified search registry. Users migrating from Docker - # assume `docker run nginx` means Docker Hub. So we write a system - # registries.conf setting unqualified-search-registries=["docker.io"] + - # short-name-mode="$SHORT_NAME_MODE", and a shortnames drop-in remapping the - # podman `hello`/`hello-world` aliases back to docker.io/library/hello-world. - # This diverges from stock podman deliberately to reduce migration burden. - # - # SHORT-NAME MODE (PR #302 review): the default is "enforcing", NOT permissive. - # Because there is a single search registry (docker.io), unqualified names STILL - # resolve deterministically to Docker Hub — migration ergonomics are preserved. - # "enforcing" only fails closed on interactively-ambiguous short names instead - # of silently resolving them, which is the least-privilege / prompt-injection - # defense a federal sandbox wants: an injected `docker run nginx` cannot be - # silently substituted (typosquatting / image substitution) without a qualified - # name or an explicit alias. Operators MAY opt into "permissive" (removing that - # guardrail) via ACQ_MSB_SHORT_NAME_MODE; see the env-var comment above. - # - # We add fuse-overlayfs + the rootless prereqs (uidmap, passt, slirp4netns) to - # the install set so the preferred path is available on the default (apt) image; - # if the mirror lacks fuse-overlayfs the vfs fallback still yields a working - # engine. - if _acq_msb_cli exec "$name" -u 0 -e "PODMAN_PKGS=$ACQ_MSB_PODMAN_PKGS" -e "SHORT_NAME_MODE=$ACQ_MSB_SHORT_NAME_MODE" -- sh -c ' - set -e - # 1) Ensure the podman binary is present (idempotent). - if ! command -v podman >/dev/null 2>&1; then - if command -v apt-get >/dev/null 2>&1; then - export DEBIAN_FRONTEND=noninteractive - apt-get update - # shellcheck disable=SC2086 - apt-get install -y --no-install-recommends $PODMAN_PKGS - elif command -v dnf >/dev/null 2>&1; then - # shellcheck disable=SC2086 - dnf install -y $PODMAN_PKGS - elif command -v apk >/dev/null 2>&1; then - # shellcheck disable=SC2086 - apk add --no-cache $PODMAN_PKGS - else - echo "acq(msb): no supported package manager (apt-get/dnf/apk) to install podman" >&2 - exit 1 - fi - fi - # 2) Select a storage driver that works on msb'"'"'s overlay root. Only write - # the config if we have not already (idempotent); do not clobber an operator - # file that already names a driver. Prefer fuse-overlayfs, else vfs. - if ! grep -q '"'"'^[[:space:]]*driver'"'"' /etc/containers/storage.conf 2>/dev/null; then - mkdir -p /etc/containers - _fuse="" - for _c in /usr/bin/fuse-overlayfs /usr/local/bin/fuse-overlayfs /bin/fuse-overlayfs; do - [ -x "$_c" ] && _fuse="$_c" && break - done - if [ -z "$_fuse" ] && command -v fuse-overlayfs >/dev/null 2>&1; then - _fuse=$(command -v fuse-overlayfs) - fi - if [ -n "$_fuse" ] && [ -e /dev/fuse ]; then - printf "[storage]\ndriver = \"overlay\"\n[storage.options.overlay]\nmount_program = \"%s\"\n" "$_fuse" \ - > /etc/containers/storage.conf - else - printf "[storage]\ndriver = \"vfs\"\n" > /etc/containers/storage.conf - fi - fi - # 3) Grant the agent access to /dev/net/tun + /dev/fuse for rootless podman — - # handled by _acq_msb_grant_oci_devs (called un-gated above AND on restart), - # NOT here, because /dev is a devtmpfs re-created each boot: a grant baked - # behind the install marker would be lost after `msb start`. (No-op here.) - # 4) Docker-Hub-first registry resolution (ADR-0020). System-level so it - # applies to the rootless agent (read as the lowest-precedence source). - # Idempotent: overwrite our own files each pass. - mkdir -p /etc/containers/registries.conf.d - printf "unqualified-search-registries = [\"docker.io\"]\nshort-name-mode = \"$SHORT_NAME_MODE\"\n" \ - > /etc/containers/registries.conf.d/00-acq-docker-first.conf - printf "[aliases]\n\"hello-world\" = \"docker.io/library/hello-world\"\n\"hello\" = \"docker.io/library/hello-world\"\n" \ - > /etc/containers/registries.conf.d/01-acq-shortnames.conf - # 5) Alias docker -> podman in /usr/local/bin (ahead of /usr/bin on PATH), so - # `docker run …` and `docker compose …` route to the podman engine. A tiny - # exec wrapper (not a symlink) so `docker compose` -> `podman compose` - # dispatches through podman'"'"'s compose provider (podman-compose). - # Plain `podman` (NOT sudo): the engine runs ROOTLESS as the agent user, so - # the agent invokes podman directly. The heredoc is FLUSH-LEFT so the - # shebang is not indented; `\$@` is escaped so the guest writes the LITERAL - # `"$@"` into the wrapper. - mkdir -p /usr/local/bin - cat > /usr/local/bin/docker </dev/null 2>&1; then - # Root setup succeeded. Now VERIFY the engine works ROOTLESS as the agent user - # — the way agents actually use it. A bare `podman info` as root would prove - # the wrong thing (rootful), so we probe as the agent. CRUCIALLY we do more - # than `podman info`: info does NOT open /dev/fuse or mount a layer, so it - # passes even when the fuse-overlayfs storage mount would fail (the exact - # /dev/fuse-permission trap). We therefore verify with a real LAYER MOUNT: a - # `podman build` FROM scratch (no registry pull, no egress). If it fails with - # the configured driver, the agent forces a USER-level vfs storage.conf and - # retries once (covers a base whose overlay+fuse combo is still rejected under - # rootless). Only a successful build writes the ready marker. - if _acq_msb_cli exec "$name" -u agent -e HOME=/home/agent -- sh -c ' - _oci_selftest() { - d=$(mktemp -d) || return 1 - printf "FROM scratch\nCOPY hi /hi\n" > "$d/Containerfile" - echo hi > "$d/hi" - podman build -q -t acq-oci-selftest:local "$d" >/dev/null 2>&1 - rc=$? - podman rmi -f acq-oci-selftest:local >/dev/null 2>&1 || true - rm -rf "$d" - return $rc - } - _oci_selftest && exit 0 - # Retry once with a user-level vfs storage.conf (overlay+fuse rejected). - mkdir -p "$HOME/.config/containers" - printf "[storage]\ndriver = \"vfs\"\n" > "$HOME/.config/containers/storage.conf" - _oci_selftest - ' >/dev/null 2>&1; then - # Best-effort: mark ready so we do not re-run the (network-bound) install on - # every provision/restart. (The /dev/net/tun grant and config writes above - # are cheap + idempotent and re-run each pass regardless of this marker.) - _acq_msb_cli exec "$name" -u 0 -- sh -c "mkdir -p /var/lib/acq && touch '$marker'" >/dev/null 2>&1 || true - acq_debug "msb: OCI engine (rootless podman) ready in $name" - return 0 - fi - fi - - # Fail soft (ADR-0020 / the balanced-egress-off case). Name the mirror hosts and - # the rootless prereqs so the operator can allow-list / bake them into ACQ_MSB_IMAGE. - echo "acq(msb): warning: could not provision an OCI engine (rootless podman) in '$name'." >&2 - echo "acq(msb): Agents will not be able to run OCI images (docker run / docker compose)." >&2 - echo "acq(msb): Most likely the OS package mirror is unreachable: the default balanced" >&2 - echo "acq(msb): egress (ADR-0018) allows it, but ACQ_NETWORK_TIER=strict or a narrowed" >&2 - echo "acq(msb): custom base blocks archive.ubuntu.com / ports.ubuntu.com / *.debian.org," >&2 - echo "acq(msb): or the rootless prereqs (podman, fuse-overlayfs, uidmap, passt," >&2 - echo "acq(msb): slirp4netns) could not be installed / rootless podman could not start." >&2 - echo "acq(msb): Bake those into ACQ_MSB_IMAGE, widen egress, or set ACQ_MSB_ENSURE_OCI=0" >&2 - echo "acq(msb): to silence this warning." >&2 - return 0 -} - # --------------------------------------------------------------------------- # Session context shared by exec/attach/shell # --------------------------------------------------------------------------- diff --git a/scripts/test-acq-lib.sh b/scripts/test-acq-lib.sh index 30b90d37..b1efc240 100644 --- a/scripts/test-acq-lib.sh +++ b/scripts/test-acq-lib.sh @@ -362,25 +362,6 @@ case "$_msb_sub" in # heal's marker-hit shell-sync path can be exercised. *"test -f '/var/lib/acq/agent-user-ready'"*) [ "${STUB_AGENT_USER_READY:-0}" = "1" ] && exit 0 || exit 1 ;; - # The OCI-engine setup is TWO msb-exec `sh -c` blocks: (1) a root (`-u 0`) - # install/config block carrying `/usr/local/bin/docker`, and (2) a rootless - # verify block (`-u agent`) that runs a `podman build` layer-mount self-test - # (tagged acq-oci-selftest). Match the root block by its docker-alias marker - # and the verify block by the self-test image tag. STUB_OCI_SETUP_FAIL=1 - # models the ROOT block failing; STUB_OCI_VERIFY_FAIL=1 models the rootless - # build self-test failing (engine/storage unusable). Either failure must make - # provision FAIL SOFT (warn, rc 0, no marker). Match these FIRST (before the - # generic command-v / test-f cases below would swallow them). - *"/usr/local/bin/docker"*) - [ "${STUB_OCI_SETUP_FAIL:-0}" = "1" ] && exit 1 || exit 0 ;; - *"acq-oci-selftest"*) - [ "${STUB_OCI_VERIFY_FAIL:-0}" = "1" ] && exit 1 || exit 0 ;; - # The OCI-ready marker probe (`test -f '/var/lib/acq/oci-ready'`) gates the - # setup block. Match it BEFORE the generic `test -f` (markers absent) case - # below. Default ABSENT (exit 1) so the OCI step runs; STUB_OCI_READY=1 - # makes the marker present (exit 0) to exercise the marker-gated skip. - *"test -f '/var/lib/acq/oci-ready'"*) - [ "${STUB_OCI_READY:-0}" = "1" ] && exit 0 || exit 1 ;; # `command -v ` (agent-presence probe): controllable so the install # path can be exercised. By default the agent is ABSENT (exit 1) so install # runs; STUB_AGENT_PRESENT=1 makes it "present" (skips install). The prereq diff --git a/test/bats/72-msb-agent-user.bats b/test/bats/72-msb-agent-user.bats index f1f822e7..ee0b65d2 100644 --- a/test/bats/72-msb-agent-user.bats +++ b/test/bats/72-msb-agent-user.bats @@ -6,8 +6,8 @@ # msb provision: agent-user creation + uid-1000 kit commands as `agent`, the # Docker base-image contract (sudo + proxy env_keep), agent install + npm-failure # disambiguation (#321), attach launching the recorded agent with a PTY, exec as -# the agent user, injection guards, and the OCI-engine (podman) setup. Provisions -# run in isolated subshells; assertions read $CALLS. +# the agent user, injection guards, and absence of adapter-owned OCI setup. +# Provisions run in isolated subshells; assertions read $CALLS. # # shellcheck shell=bats @@ -205,76 +205,17 @@ _attach() { # PRE_SNIPPET NAME assert_regex "$log" 'injattach -- /bin/sh -l' } -@test "msb: OCI (podman) engine setup runs as root, configures storage, verifies rootless" { +@test "msb: provision without oci-engine does not install podman or grant OCI devices" { _provision ocibox shell 'export ACQ_SECRET_STORE_DIR="'"$STUBDIR"'/oci-secrets"' local log; log=$(cat "$CALLS") - assert_regex "$log" 'PODMAN_PKGS=' - assert_regex "$log" '/usr/local/bin/docker' - assert_regex "$log" "touch '/var/lib/acq/oci-ready'" - assert_regex "$log" '/etc/containers/storage\.conf' - assert_regex "$log" 'driver = ..vfs..' - assert_regex "$log" 'mount_program' - assert_regex "$log" 'fuse-overlayfs' - assert_regex "$log" 'uidmap' - assert_regex "$log" 'passt' - assert_regex "$log" 'slirp4netns' - assert_regex "$log" 'exec podman' - refute_regex "$log" 'exec sudo -n podman' - assert_regex "$log" '/dev/net/tun' - assert_regex "$log" '/dev/fuse' - assert_regex "$log" 'chown root:agent' - assert_regex "$log" 'unqualified-search-registries = ...docker.io...' - assert_regex "$log" 'docker\.io/library/hello-world' - assert_regex "$log" 'short-name-mode = ...SHORT_NAME_MODE.' - assert_regex "$log" 'SHORT_NAME_MODE=enforcing' - refute_regex "$log" 'SHORT_NAME_MODE=permissive' - assert_regex "$log" 'msb exec ocibox -u 0 -e PODMAN_PKGS=' - assert_regex "$log" 'msb exec ocibox -u agent -e HOME=/home/agent' - assert_regex "$log" 'acq-oci-selftest' -} - -@test "msb: ACQ_MSB_SHORT_NAME_MODE=permissive threads the permissive value" { - _provision ocipermbox shell 'export ACQ_SECRET_STORE_DIR="'"$STUBDIR"'/oci-perm-secrets" ACQ_MSB_SHORT_NAME_MODE=permissive' - local log; log=$(cat "$CALLS") - assert_regex "$log" 'SHORT_NAME_MODE=permissive' - refute_regex "$log" 'SHORT_NAME_MODE=enforcing' -} - -@test "msb: an invalid ACQ_MSB_SHORT_NAME_MODE warns and falls back to enforcing" { - _provision ocibadbox shell 'export ACQ_SECRET_STORE_DIR="'"$STUBDIR"'/oci-bad-secrets" ACQ_MSB_SHORT_NAME_MODE="bogus; rm -rf /"' - assert_output --partial 'invalid ACQ_MSB_SHORT_NAME_MODE' - local log; log=$(cat "$CALLS") - assert_regex "$log" 'SHORT_NAME_MODE=enforcing' - refute_regex "$log" 'SHORT_NAME_MODE=bogus' - refute_regex "$log" 'rm -rf /' -} - -@test "msb: the OCI setup is skipped when the ready marker already exists" { - _provision ocirdybox shell 'export ACQ_SECRET_STORE_DIR="'"$STUBDIR"'/ociready-secrets" STUB_OCI_READY=1' - local log; log=$(cat "$CALLS") - assert_regex "$log" "test -f '/var/lib/acq/oci-ready'" refute_regex "$log" 'PODMAN_PKGS=' refute_regex "$log" '/usr/local/bin/docker' -} - -@test "msb: ACQ_MSB_ENSURE_OCI=0 skips the OCI step entirely" { - _provision ocioffbox shell 'export ACQ_SECRET_STORE_DIR="'"$STUBDIR"'/ocioff-secrets" ACQ_MSB_ENSURE_OCI=0' - local log; log=$(cat "$CALLS") - refute_regex "$log" 'PODMAN_PKGS=' - refute_regex "$log" 'oci-ready' -} - -@test "msb: an OCI setup failure is fail-soft (rc 0, warns, marker not touched)" { - _provision ocifailbox shell 'export ACQ_SECRET_STORE_DIR="'"$STUBDIR"'/ocifail-secrets" STUB_OCI_SETUP_FAIL=1' - assert_success - assert_output --partial 'could not provision an OCI engine' - refute_regex "$(cat "$CALLS")" "touch '/var/lib/acq/oci-ready'" -} - -@test "msb: an unsafe ACQ_MSB_PODMAN_PKGS is refused and never reaches an exec" { - _provision ociinjbox shell 'export ACQ_SECRET_STORE_DIR="'"$STUBDIR"'/ociinj-secrets" ACQ_MSB_PODMAN_PKGS="podman;rm -rf"' - assert_output --partial 'unsafe characters' - refute_regex "$(cat "$CALLS")" 'rm -rf' + refute_regex "$log" "touch '/var/lib/acq/oci-ready'" + refute_regex "$log" '/etc/containers/storage\.conf' + refute_regex "$log" 'acq-oci-selftest' + refute_regex "$log" '/dev/net/tun' + refute_regex "$log" '/dev/fuse' + refute_regex "$log" 'chown root:agent' } # msb session parity: sbx is a full session transport, so From 7d679601087a48cfd7e881c8363b705a3225f599 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sat, 3 Oct 2026 22:58:25 +0000 Subject: [PATCH 3/5] docs(kits): document oci-engine catalog migration Update the live verifier to exercise oci-engine through the generic kit path and document the breaking migration away from msb adapter provisioning. Co-authored-by: OpenCode [gpt-5.5] --- docs/BACKEND_GUIDE.md | 73 ++- docs/CONCEPTS.md | 6 +- docs/KNOWN_FAILURE_MODES.md | 66 ++- docs/adr/0020-msb-oci-engine-via-podman.md | 463 +++--------------- ...msb-host-ssh-agent-forwarding-via-vsock.md | 5 +- docs/howto/msb.md | 4 +- scripts/verify-backends | 245 +++++---- 7 files changed, 312 insertions(+), 550 deletions(-) diff --git a/docs/BACKEND_GUIDE.md b/docs/BACKEND_GUIDE.md index c63b2148..dd524d2f 100644 --- a/docs/BACKEND_GUIDE.md +++ b/docs/BACKEND_GUIDE.md @@ -3,7 +3,7 @@ title: "acq Backend Guide" description: "Per-backend strengths, tradeoffs, and configuration for acq" status: canonical tier: 2 -last_updated: "2026-09-30" +last_updated: "2026-10-03" audience: "developers" keywords: ["acq", "backend", "sbx", "msb", "microsandbox", "tradeoffs"] related_files: ["docs/howto/acq.md", "docs/howto/msb.md", "docs/howto/sbx.md", "docs/CONCEPTS.md", "docs/adr/0010-acq-pluggable-backends.md", "docs/adr/0011-msb-backend-and-neutral-kits.md", "docs/adr/0014-neutral-port-publish-and-background-vocab.md", "docs/adr/0015-msb-post-hoc-port-publish-via-ssh.md", "docs/adr/0034-host-port-selection-and-publish-override.md"] @@ -196,8 +196,6 @@ Tunables: | `ACQ_OPENCODE_POSTINSTALL_TIMEOUT` | `120` | Seconds to bound opencode's in-guest `postinstall.mjs` (which fetches a platform binary) so a wedged registry can't hang `acq run`; used only when the guest provides `timeout` | | `ACQ_MSB_OPENCODE_PKG` | `opencode-ai` | npm package spec for the opencode install (pin e.g. `opencode-ai@1.2.3`) | | `ACQ_MSB_NPM_HOSTS` | `registry.npmjs.org` | npm registry host(s) to allow-list for the agent install (space-separated; set for an internal mirror) | -| `ACQ_MSB_ENSURE_OCI` | `1` (on) | Provision an OCI container engine (podman) at create so agents can run OCI images (`docker run`, `docker compose`). Installs `ACQ_MSB_PODMAN_PKGS` and aliases `docker` → `podman`. Set `0`/`false`/`no`/`off`/empty to skip (e.g. a base that bakes its own working engine). Fails soft if the OS package mirror is unreachable. See ADR-0020. | -| `ACQ_MSB_PODMAN_PKGS` | `podman podman-compose` | Packages installed to provide the OCI engine (space-separated). `podman-compose` is the `docker compose` / `podman compose` provider. Override for a different set or an internal mirror's names. | | `ACQ_NETWORK_TIER` | `balanced` | Neutral egress posture (`strict`\|`balanced`\|`open`), the backend-agnostic selector defined by the agentic-coding-patterns network-tiers contract (ADR-0002). **All tiers are deny-by-default except `open`**; the tier only sizes the baseline allowlist. `strict` = `--net-default-egress deny` + gateway DNS + the kits' own `caps.network.allow` hosts ONLY (recommended for GFE / high-assurance). `balanced` = the same deny-default + the curated sbx-`balanced` baseline (ADR-0018) unioned with the kit hosts. `open` = **unrestricted egress** (no deny-default); testing only, never for GFE, and refused unless `ACQ_NETWORK_TIER_CONFIRM_OPEN=1`. Invalid values fail closed to `balanced`. | | `ACQ_NETWORK_TIER_CONFIRM_OPEN` | (unset) | Required confirmation for `ACQ_NETWORK_TIER=open`. Set to `1` to acknowledge that the sandbox runs with unrestricted egress; otherwise `open` is refused at provision time (fail-closed). Treated like `--privileged` — never a default. | | `ACQ_MSB_BALANCED_EGRESS` | (deprecated) | **Deprecated alias** for `ACQ_NETWORK_TIER`; retained for one deprecation window and removed in a future major. A `1`/on value maps to `ACQ_NETWORK_TIER=balanced`; a `0`/`false`/`no`/`off`/empty value maps to `ACQ_NETWORK_TIER=strict` (deny-by-default, kit hosts only — a former "off" no longer means permissive; an upgrade never silently loosens egress). `ACQ_NETWORK_TIER` wins when both are set, and a one-time notice is printed. Migrate to `ACQ_NETWORK_TIER`; use `open` if you truly need unrestricted egress. | @@ -625,11 +623,11 @@ mirroring the sbx - The four kit prerequisites present: `node`, `git`, `curl`, `update-ca-certificates`. -For OCI-run support (`docker run` / `docker compose`), the adapter installs -podman at provision (see [Running OCI images inside the sandbox](#running-oci-images-inside-the-sandbox-podman)), -so a base image does **not** need a container engine baked in — only a supported -package manager (apt-get/dnf/apk) and mirror reachability. Bake podman in (and -set `ACQ_MSB_ENSURE_OCI=0`) only if you want to skip the runtime install. +For OCI-run support (`docker run` / `docker compose`), opt into the +`oci-engine` catalog kit (see [Running OCI images inside the sandbox](#running-oci-images-inside-the-sandbox-podman)). +The base image does **not** need a container engine baked in if the kit can reach +its package mirrors; alternatively, use a base that already includes a working +engine and skip the kit. **Build on Docker's `sandbox-templates:*` images to get all of these for free** — msb derives the same agent-specific image naming convention as sbx when no @@ -679,38 +677,33 @@ If the image requires registry auth, log in with your container tooling (e.g. ### Running OCI images inside the sandbox (podman) Agents often need to run OCI images from inside the sandbox — `docker run` an -image, or bring up a `docker-compose.yaml`. The msb adapter guarantees this -capability the same way it guarantees the base-image contract: idempotently, no -matter what the base image brings. - -The default image ships the Docker CLI, but msb's microVM init (`/init.krun`) -never starts `dockerd`, so the Docker socket is dead — and Docker's `overlay2` -storage driver cannot sit on the sandbox's already-overlay root without a -disk-backed data volume. Rather than retrofit the msb docker-in-docker recipe (a -daemon to start and keep alive across restarts, plus a per-sandbox disk-backed -volume), the adapter provisions **podman** at create time and aliases `docker` → -`podman`: - -- **podman is daemonless** — no socket to start/poll, no restart lifecycle — uses - `fuse-overlayfs` on the overlay root (no disk-backed volume), and needs no - nested virtualization (containers are `runc`/`crun` processes, not VMs). -- It runs **rootful** via the agent's passwordless sudo, which avoids the - rootless prerequisites (`uidmap`, `passt`/pasta) a lean base lacks. -- A tiny `docker` → `podman` wrapper is placed in `/usr/local/bin` (ahead of - `/usr/bin`), so `docker run …` **and** `docker compose …` route to podman. The - base image's `/usr/bin/docker` is never modified. `docker compose` resolves to - `podman compose`, driven by the installed `podman-compose` provider — so - `docker-compose.yaml` files work. (The standalone `docker-compose` CLI is - deprecated in favour of the `docker compose` subcommand, so no separate - `docker-compose` binary is provided.) - -The install uses the OS package mirror, which under the default balanced egress -baseline (ADR-0018) is already reachable — no extra net-rule needed. With -`ACQ_NETWORK_TIER=strict`, or a custom base whose egress is narrowed, the -mirror is unreachable and the step **fails soft** (a warning; provision -continues; OCI is simply unavailable). Turn the step off entirely with -`ACQ_MSB_ENSURE_OCI=0` (e.g. a base that bakes its own working engine), or point -`ACQ_MSB_PODMAN_PKGS` at a different package set / internal mirror. See ADR-0020. +image, or bring up a `docker-compose.yaml`. This capability is now an **opt-in +catalog kit**, not an msb adapter default. + +Enable it through the catalog: + +```bash +acq configure +# select: oci-engine +``` + +You can also apply it for one sandbox by passing the pinned kit ref through +`--kit`, or by exporting that same ref in `ACQ_EXTRA_KITS`: + +```bash +acq run opencode . --kit 'git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=2dd2ad6ad5f63b842d732424bed5ba9aebeed676&dir=integrations/isolation/acq-kits/oci-engine' +``` + +The kit provisions **rootless podman** and aliases `docker` to `podman`, so +`docker run` and `docker compose` work inside the sandbox without a Docker daemon. +It is backend-neutral: the same catalog choice applies on sbx and msb. + +This is a breaking change for msb users: sandboxes no longer get podman by +default. Existing sandboxes keep whatever was already installed, but new +sandboxes must opt into `oci-engine` if they need an in-sandbox container engine. +The old msb-only environment toggles for adapter provisioning are removed; use +`acq configure`, `ACQ_EXTRA_KITS`, or `--kit` instead. See ADR-0020 for the +superseded adapter-era decision and ADR-0031 for the catalog mechanism. ### Secrets diff --git a/docs/CONCEPTS.md b/docs/CONCEPTS.md index 1c282af4..f9c0dffa 100644 --- a/docs/CONCEPTS.md +++ b/docs/CONCEPTS.md @@ -3,7 +3,7 @@ title: "acq Concepts" description: "Backend-neutral concepts for working with acq sandboxes (workspaces, mounts, published ports)" status: canonical tier: 2 -last_updated: "2026-09-30" +last_updated: "2026-10-03" audience: "developers" keywords: ["acq", "concepts", "workspace", "mount", "ports", "publish", "backend-neutral", "sbx", "msb"] related_files: ["docs/howto/acq.md", "docs/BACKEND_GUIDE.md", "docs/adr/0010-acq-pluggable-backends.md", "docs/adr/0011-msb-backend-and-neutral-kits.md", "docs/adr/0034-host-port-selection-and-publish-override.md"] @@ -265,6 +265,7 @@ flags on every run. [x] git-ssh-sign SSH-based git commit signing … (always applied) ❯ [x] openchamber Browser UI for OpenCode alongside the terminal TUI [ ] paseo Self-hosted Paseo browser web UI for coding agents + [ ] oci-engine Rootless podman for docker run / docker compose ``` - The **four built-in kits** (`zscaler-ca-certificate`, `usai-provider`, @@ -276,6 +277,9 @@ flags on every run. - At `acq create`, the picker is shown again **pre-populated with your saved defaults**, so a single sandbox can enable or disable a kit without changing the global default (the deviation is remembered for that sandbox). +- Select **`oci-engine`** when the sandbox needs `docker run` or + `docker compose`. It installs rootless podman inside the sandbox and is not + enabled by default. - The **GitHub-token** preference only pre-answers the scoping prompt; the fine-grained token is still minted per-sandbox (see [`acq github-scope`](howto/acq.md)). diff --git a/docs/KNOWN_FAILURE_MODES.md b/docs/KNOWN_FAILURE_MODES.md index 3d34b183..7b5874d6 100644 --- a/docs/KNOWN_FAILURE_MODES.md +++ b/docs/KNOWN_FAILURE_MODES.md @@ -3,7 +3,7 @@ title: "Known Failure Modes" description: "Real-world failure patterns when using Docker SBX + USAi + agent frameworks" status: canonical tier: 2 -last_updated: "2026-09-30" +last_updated: "2026-10-03" audience: "developers" keywords: ["debugging", "troubleshooting", "sbx", "usai", "failures"] --- @@ -647,7 +647,7 @@ worked on sbx 0.35.0–0.37.x, where `sbx kit add` recreated the container with augmented kit set while preserving state. > **sbx 0.38 caveat.** On sbx >= 0.38, `sbx kit add` no longer applies -> startup-bearing kits mid-life (see [Section 35](#35-re-attach-heal-loop-warns-every-time-on-sbx-038--sbx-kit-add-refuses-startup-bearing-kits)), +> startup-bearing kits mid-life (see [Section 36](#36-re-attach-heal-loop-warns-every-time-on-sbx-038--sbx-kit-add-refuses-startup-bearing-kits)), > and every built-in acq kit declares startup commands. In-place healing of the > built-in bundle therefore does **not** work on 0.38+: to add or refresh the > bundle you must recreate the sandbox (`acq rm && acq run`). acq detects the @@ -1746,7 +1746,51 @@ acq exec -- sh /home/agent/openchamber-start.sh & --- -## 34. Git signing fails on msb: socat missing or msb < 0.6.9 +## 34. `docker run` or `docker compose` Missing Inside a Sandbox + +### Symptoms + +- `docker run`, `docker compose`, or `podman` is not found inside a sandbox. +- On msb, a new sandbox no longer gets podman automatically. + +### Root Cause + +`oci-engine` is an opt-in catalog kit. acq no longer installs podman from the msb +adapter by default, and sbx/msb now use the same mechanism for in-sandbox OCI +support. + +### Fix + +Enable the kit for future sandboxes: + +```bash +acq configure +# select: oci-engine +``` + +For a single sandbox, pass the pinned kit ref with `--kit` or include it in +`ACQ_EXTRA_KITS`: + +```bash +acq run opencode . --kit 'git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=2dd2ad6ad5f63b842d732424bed5ba9aebeed676&dir=integrations/isolation/acq-kits/oci-engine' +``` + +Existing sandboxes that were created before this change keep whatever podman +state they already have, but new sandboxes require the opt-in kit unless the base +image already includes a working engine. + +### Prevention / Status + +- Breaking change in the `oci-engine` catalog migration. Covered by the + `acq configure` catalog tests, the configured-kit fresh-shell start + round-trip, the msb no-default-OCI regression test, and the dedicated live + `scripts/verify-backends --only msb --oci-only` path. +- ADR-0020 is superseded for the adapter-owned mechanism; ADR-0030 and ADR-0031 + describe the kit/catalog path. + +--- + +## 35. Git signing fails on msb: socat missing or msb < 0.6.9 ### Symptoms @@ -1866,7 +1910,7 @@ See [ADR-0021](adr/0021-msb-host-ssh-agent-forwarding-via-vsock.md) --- -## 35. Re-attach Heal Loop Warns Every Time on sbx 0.38 — `sbx kit add` Refuses Startup-Bearing Kits +## 36. Re-attach Heal Loop Warns Every Time on sbx 0.38 — `sbx kit add` Refuses Startup-Bearing Kits ### Symptoms @@ -1956,7 +2000,7 @@ heal-loop cases). --- -## 36. msb Create-Time Published Port Returns Empty Response for a Loopback-Only Guest Service +## 37. msb Create-Time Published Port Returns Empty Response for a Loopback-Only Guest Service ### Symptoms @@ -2028,7 +2072,7 @@ from inside the sandbox and can reach guest `127.0.0.1:6767`. --- -## 37. Commit signing fails in the sandbox: `user.signingKey needs to be set` / `No signature` +## 38. Commit signing fails in the sandbox: `user.signingKey needs to be set` / `No signature` ### Symptoms @@ -2085,7 +2129,7 @@ Notes: reachable, so a rebase with `SSH_AUTH_SOCK` set does not need a separate `--amend -S` pass. -## 38. Spurious `M` (modified) diffs on scripts across the host/sandbox mount +## 39. Spurious `M` (modified) diffs on scripts across the host/sandbox mount ### Symptoms @@ -2135,7 +2179,7 @@ amending the wrong branch. --- -## 39. Windows preview: `bash.exe` resolves to the WSL shim, WHP is not a feature flag, and the execution policy blocks scripts +## 40. Windows preview: `bash.exe` resolves to the WSL shim, WHP is not a feature flag, and the execution policy blocks scripts ### Symptoms @@ -2208,7 +2252,7 @@ amending the wrong branch. --- -## 40. Recovering agent work reads the guest-writable scratch clone (residual risk) +## 41. Recovering agent work reads the guest-writable scratch clone (residual risk) ### Symptoms @@ -2251,7 +2295,7 @@ an untrusted agent could write to. --- -## 41. A Required Check Backed by a `paths:`-Filtered Workflow Deadlocks Unrelated PRs +## 42. A Required Check Backed by a `paths:`-Filtered Workflow Deadlocks Unrelated PRs ### Symptoms @@ -2319,7 +2363,7 @@ adding them to branch protection. --- -## 42. A Second Parallel Sandbox From the Same Kit Is Unreachable on Its Published Port (msb) +## 43. A Second Parallel Sandbox From the Same Kit Is Unreachable on Its Published Port (msb) ### Symptoms diff --git a/docs/adr/0020-msb-oci-engine-via-podman.md b/docs/adr/0020-msb-oci-engine-via-podman.md index a317fd64..58721dce 100644 --- a/docs/adr/0020-msb-oci-engine-via-podman.md +++ b/docs/adr/0020-msb-oci-engine-via-podman.md @@ -1,6 +1,6 @@ --- title: "Ensure an OCI container engine in msb sandboxes via rootless podman, not docker-in-docker" -status: accepted +status: superseded date: 2026-08-11 decision_makers: ["Bret Mogilefsky"] category: architecture @@ -9,394 +9,87 @@ impact_level: low ato_relevance: no risk_treatment: accept supersedes: [] +superseded_by: ["docs/adr/0030-agent-kits-on-devenv-base.md", "docs/adr/0031-interactive-acq-configure.md"] --- # ADR-0020: Ensure an OCI container engine in msb sandboxes via rootless podman -> **Revision note (2026-08-11, pre-merge, no PR yet):** This ADR originally chose -> **rootful** podman to avoid a create-time download dependency on the rootless -> prerequisites (`uidmap`, `passt`). Once we had a working implementation, that -> premise did not hold: the adapter *already* installs `podman podman-compose -> fuse-overlayfs` from the OS mirror at provision, so the rootless prereqs are two -> more packages from the *same mirror in the same install step* — not a new -> dependency class. Meanwhile rootful required a `sudo`-wrapping `docker` alias and -> produced a rootful/rootless probe mismatch (containers ran as root inside the -> VM, and the verifier had to probe via sudo). We therefore **reversed the decision -> to rootless** and revised this ADR in place (the branch had never merged). The -> superseded rootful reasoning is preserved under *Alternatives considered*. - -## Context - -Agents working inside an msb sandbox frequently need to run OCI images — pull -and `docker run` an image, or bring up a `docker-compose.yaml`. Just as the msb -adapter idempotently guarantees the base-image *contract* (an `agent` user, -`/home/agent`, passwordless sudo, proxy `env_keep`; see -[ADR-0011](0011-msb-backend-and-neutral-kits.md) and `_acq_msb_ensure_agent_user`), -we want to guarantee an OCI-run *capability* regardless of what the base image -brings. - -The situation on the default image -(`docker/sandbox-templates:shell-docker`, an Ubuntu base) was investigated live -inside an msb sandbox: - -- The **Docker CLI, compose v2 plugin, `dockerd`, `containerd`, and `runc` are - all installed**, but msb's microVM init is `/init.krun` — **not** systemd or - any service manager — so **nothing starts `dockerd`**. The Docker socket - (`/var/run/docker.sock`) does not exist, and every `docker` command fails with - `Cannot connect to the Docker daemon`. -- The sandbox **root filesystem is already an `overlay` mount** - (`/.msb/rootfs/...`). Docker's default `overlay2` storage driver **cannot be - stacked on an overlay root** without a dedicated lower-level filesystem — - which is exactly why the msb project's own - [docker-in-a-sandbox recipe](https://github.com/superradcompany/microsandbox/blob/main/docs/examples/docker/docker-in-sandbox.mdx) - boots the `docker:dind` image *as the sandbox entrypoint* and mounts a - **disk-backed** named volume at `/var/lib/docker`. -- **`/dev/kvm` is absent** (no nested virtualization) — but that only rules out - nested *VMs*, not containers, which run as ordinary `runc`/`crun` processes. -- The agent user has **passwordless sudo** (guaranteed by the base-image - contract), and `/dev/fuse` is present. -- The kernel supports **unprivileged user namespaces** (`max_user_namespaces` is - large, no restrictive `unprivileged_userns_clone` knob), and `/etc/subuid` + - `/etc/subgid` already grant the `agent` user a `100000:65536` range — the - prerequisites for **rootless** containers. The rootless *userland* tools - (`newuidmap`/`newgidmap` from `uidmap`, `passt`/`slirp4netns` for networking) - are **not** preinstalled, and `/dev/net/tun` is root-only (`crw------- root - root`) — both fixable at provision. - -So the bundled Docker is inert here, and the blessed way to make it work (the -dind recipe) is an *image/entrypoint* strategy that does not fit our adapter: -`acq` boots a general base image and launches the agent — it does not own the -sandbox entrypoint, and would have to (a) start `dockerd` itself post-boot and -poll the socket, (b) keep that daemon alive across every `msb start`/restart -(the same restart-durability problem [ADR-0017](0017-msb-create-time-startup-script-staging.md) -already wrestles with), and (c) provision a per-sandbox disk-backed -`--mount-named docker-data:/var/lib/docker:kind=disk` volume and tear it down. - -## Decision - -> **Revision note (2026-10-02, ADR-0030 transition):** this adapter-owned path -> remains the default while the shared `oci-engine` capability kit is introduced -> through the generic opt-in catalog described by ADR-0031. The catalog entry and -> adapter retirement are tracked by GSA-TTS/agentic-coding-quickstart#505. - -Provision **podman** at provision time as the OCI engine, run it **rootless as -the agent user**, and alias `docker` → `podman` so both `docker run …` and -`docker compose …` work inside the sandbox. Implemented as a new idempotent, -marker-gated step `_acq_msb_ensure_oci`, called from `acq_backend_provision` -right after `_acq_msb_ensure_agent_user`. - -1. **podman over docker-in-docker.** podman is **daemonless** — each invocation - forks `runc`/`crun` directly, so there is no socket to start, poll, or keep - alive across restarts (eliminating the dind lifecycle problem entirely). It - uses **`fuse-overlayfs`** (we have `/dev/fuse`) or `vfs` on the overlay root, - so **no disk-backed volume** is required. It needs **no nested - virtualization**. And it is CLI-compatible with docker for the - run/build/compose workflows this targets. - - **Storage driver on an overlay root.** podman's default **kernel `overlay`** - graph driver **cannot stack on msb's overlay root** — `podman info` fails with - `'overlay' is not supported over overlayfs, a mount_program is required`. This - applies to **both** rootful and rootless. The adapter therefore writes - `/etc/containers/storage.conf` **before** the engine verify, selecting a driver - that works on an overlay root: - - **Preferred:** `overlay` + `mount_program = fuse-overlayfs` when - `fuse-overlayfs` is installed (fast, thin-on-disk; msb provides `/dev/fuse`). - - **Fallback:** `vfs`, which works everywhere with no extra package and no - `/dev/fuse` (correct but disk-heavy — a full copy per layer). Used when - `fuse-overlayfs` is unavailable, and as a last-resort retry (a **user-level** - `~/.config/containers/storage.conf`) if the rootless verify still fails. - The system file is honored by rootless podman as its lowest-precedence source. - The adapter does not clobber an operator-provided `storage.conf` that already - names a `driver`. - -2. **Rootless, run as the agent user.** The package **install** runs as root - (`msb exec -u 0`, needed to install), but the **engine runs rootless** as the - unprivileged `agent`. Rootless needs four things the default image lacks, - which the adapter provides at provision: - - **`uidmap`** (`newuidmap`/`newgidmap`) — the setuid helpers that map the - agent's `/etc/subuid`/`/etc/subgid` `100000:65536` range (already present). - - **`passt` + `slirp4netns`** — rootless container networking backends. - - **`/dev/net/tun` access** — root-only by default (`crw------- root root`); - the rootless network backend must open it. The adapter **group-scopes** it to - the agent (`chown root:agent`, `chmod 0660`) — see item 6. - - **`/dev/fuse` access** — likewise root-only by default; the `fuse-overlayfs` - storage driver (our preferred driver on the overlay root) must open it to - mount image layers. Without it `podman info` still passes but `podman run` - fails at mount time (`fuse: failed to open /dev/fuse: Permission denied`) — - the exact info-OK-but-run-FAILS split first seen on the host. Group-scoped to - the agent the same way — see item 6. - - These are installed from the **same OS mirror in the same `apt-get install`** - as podman itself, so rootless adds **no new dependency class** over the engine - we already download. Running rootless keeps containers **unprivileged even - inside the microVM** (defense-in-depth), **aligns container/host UIDs** for - bind mounts, and lets the agent invoke podman **directly** (no sudo wrapper, - no rootful/rootless probe mismatch). Verified live: with the prereqs installed, - a `driver="vfs"` (or fuse-overlayfs) storage config, and `/dev/net/tun` + - `/dev/fuse` group-scoped, `podman info` and `docker run --rm docker.io/library/hello-world` - both succeed **as the agent user** (`rootless=true`). - -3. **Alias `docker` → `podman` (do not fix the bundled Docker).** A tiny exec - wrapper is written to **`/usr/local/bin/docker`** (ahead of `/usr/bin` on the - default PATH), shadowing the bundled-but-non-functional `/usr/bin/docker`. We - never touch the base image's `/usr/bin/docker`. Because the bundled Docker CLI - does not work here anyway (dead socket), shadowing it loses nothing and makes - both `docker run` and `docker compose` route to the working podman engine. - - **The wrapper execs a plain `podman "$@"`** (no sudo): the engine runs rootless - as the agent, so the agent invokes podman directly. This is why the - `scripts/verify-backends` OCI check probes `docker info` **as the agent** (via - `acq exec`, which already runs as the agent) — it exercises the real usable - path (rootless prereqs + storage driver + alias) with no privilege escalation. - -4. **`docker compose`, not `docker-compose`.** The standalone `docker-compose` - CLI is deprecated in favour of the `docker compose` subcommand. With the alias - in place, `docker compose …` becomes `podman compose …`, which dispatches - through the installed **`podman-compose`** provider. So we install - `podman podman-compose` (plus the storage/rootless prereqs, `ACQ_MSB_PODMAN_PKGS`) - and do **not** ship a separate `docker-compose` binary. The goal is that - `docker-compose.yaml` files work, not that the deprecated CLI name exists. - -5. **Docker-Hub-first image resolution.** Stock podman resolves many unadorned - short names to **quay.io** (e.g. `hello-world` → `quay.io/podman/hello`) and - ships **no** default unqualified search registry, so `docker run nginx` does - not "just work" the way Docker users expect. To reduce migration burden for - users whose code assumes Docker Hub, the adapter writes system drop-ins: - - `registries.conf.d/00-acq-docker-first.conf`: - `unqualified-search-registries = ["docker.io"]` + `short-name-mode = "enforcing"`. - - `registries.conf.d/01-acq-shortnames.conf`: `[aliases]` remapping - `hello`/`hello-world` to `docker.io/library/hello-world` (a user drop-in - override cleanly wins over the stock alias — no merge conflict). - These are system-level so they apply to the rootless agent. This **deliberately - diverges from stock podman**; it is a config-only change and Docker Hub's CDNs - are in the balanced egress baseline (see Consequences). - - **Short-name mode defaults to `enforcing` (least-privilege / prompt-injection - defense).** Because there is a **single** unqualified search registry - (`docker.io`), unqualified names still resolve **deterministically** to Docker - Hub — migration ergonomics are preserved. `enforcing` only fails **closed** on - interactively-ambiguous short names instead of silently resolving them, so an - injected `docker run nginx` on the prompt-injectable agent path cannot be - silently substituted (typosquatting / image substitution). With one search - registry, enforcing costs essentially no day-to-day ergonomics. Operators MAY - opt into `permissive` (or `disabled`) via **`ACQ_MSB_SHORT_NAME_MODE`**; an - invalid value warns and falls back to `enforcing` (fail-closed). Setting - `permissive` is an explicit operator override that **removes** the - typosquatting / image-substitution guardrail. - - > **Revision note (post PR #302 review).** The permissive default was - > reconsidered after review: a 3-model consensus plus the reviewer recommended - > **AGAINST** `permissive` as a federal-sandbox default, because it removes the - > defense against image substitution / typosquatting for a prompt-injectable - > agent. The default was changed to `enforcing`; `permissive` remains available - > as an explicit opt-in. - -6. **Idempotent + marker-gated + fail-soft, with an un-gated device-node - grant.** The heavy install/config step returns early if the marker - `/var/lib/acq/oci-ready` exists. It verifies **rootless** `podman info` (as the - agent) before writing the marker. If the engine cannot be provisioned it - **warns and returns 0** — provision continues, OCI is simply unavailable — - mirroring the agent-install and prereq-check steps (§Failure Handling, - `AGENTS.md`). Toggle off with `ACQ_MSB_ENSURE_OCI=0`. - - The device-node grants (`/dev/net/tun` for networking, `/dev/fuse` for - fuse-overlayfs) are handled by a **separate `_acq_msb_grant_oci_devs` helper - called on EVERY provision pass (before the install-marker gate) AND on restart - (`acq_backend_start`)** — because `/dev` is a devtmpfs re-created at each boot, - a grant baked behind the persistent marker would be lost after `msb start`. The - helper is cheap, idempotent, and a best-effort no-op for any device that is - absent or when `ACQ_MSB_ENSURE_OCI` is disabled. NOTE: because rootless - `podman info` (the verify) does NOT open `/dev/fuse` but `podman run` does, the - grant covering `/dev/fuse` must NOT be gated behind the `podman info` verify — - omitting it caused a real host regression where the engine check passed but - `docker run` failed at layer-mount time. +> **Status: SUPERSEDED by [ADR-0030](0030-agent-kits-on-devenv-base.md) and +> [ADR-0031](0031-interactive-acq-configure.md).** The rootless-podman and +> podman-over-docker-in-docker decisions remain the design basis, but the +> implementation moved out of the msb adapter and into the neutral `oci-engine` +> patterns kit. OCI support is no longer installed by default on msb; users opt +> in through `acq configure`, `ACQ_EXTRA_KITS`, or `--kit`. This ADR is retained +> for adapter-era rationale and historical context. + +## Context and Problem Statement + +Agents working inside a sandbox sometimes need to run OCI images, for example +`docker run` or `docker compose`. The default msb image included Docker client +bits, but msb did not start `dockerd`, and Docker's `overlay2` storage driver did +not fit the already-overlay sandbox root without a dedicated disk-backed data +volume. + +The adapter-era design tried to guarantee OCI support for every msb sandbox. That +made msb and sbx asymmetric: msb sandboxes received podman whether they asked for +it, while sbx sandboxes did not. ADR-0030 later moved capability provisioning to +neutral patterns kits, and ADR-0031 added a discoverable opt-in kit catalog. + +## Superseding Decision + +OCI support is now provided by the **`oci-engine` catalog kit**, sourced from the +same pinned `agentic-coding-patterns` bundle as the built-in kits. Users select it +with `acq configure` for a durable default, or pass its pinned kit ref through +`ACQ_EXTRA_KITS` / `--kit` for explicit per-run use. + +This is a breaking change for msb users: new msb sandboxes no longer install an +OCI engine by default. Existing sandboxes keep whatever was already installed; +new sandboxes that need `docker run` or `docker compose` must opt into the kit. + +## Decisions That Still Hold + +1. **podman over docker-in-docker.** podman is daemonless, avoids managing a + long-lived Docker daemon inside the sandbox, and does not require nested + virtualization. +2. **Rootless execution.** The engine should run as the unprivileged `agent` user + so in-sandbox containers do not require a rootful daemon or a sudo-wrapped + client path. +3. **`docker` compatibility.** The user-facing command path should support common + Docker workflows by routing `docker run` and `docker compose` to podman. +4. **Overlay-root handling.** The implementation must account for storage on an + overlay root, preferring fuse-overlayfs when available and falling back to a + safe working storage driver when necessary. +5. **Short-name safety.** Unqualified image-name behavior should fail closed or be + explicitly configured by the kit, because silent short-name resolution can + enable image substitution or typosquatting attacks. ## Consequences -- **OCI-run capability is guaranteed** on the default image and any base with a - supported package manager (apt-get/dnf/apk), without owning the entrypoint, - managing a daemon lifecycle, or provisioning a disk-backed volume. -- **Depends on the OS package mirror being reachable at provision.** Unlike the - npm-registry allow-rule (which had to be *added* because the registry was not - otherwise reachable), the Ubuntu/Debian mirror hosts - (`archive.ubuntu.com`, `ports.ubuntu.com` — this arch is arm64, so `ports` — - `security.ubuntu.com`, `**.debian.org`, `launchpad.net`) are **already in the - default balanced egress baseline** ([ADR-0018](0018-msb-balanced-egress-baseline.md)), - so no new net-rule is needed under the default policy. With - `ACQ_NETWORK_TIER=strict` (or the deprecated `ACQ_MSB_BALANCED_EGRESS=0`, which - now maps to `strict`), or a custom base whose egress is otherwise - narrowed, the mirror is unreachable and the step **fails soft** — chosen - deliberately so an operator's explicit egress narrowing is respected rather - than silently re-widened (the warning names the mirror hosts and the - `ACQ_MSB_ENSURE_OCI=0` escape hatch). -- **Behavioral parity with Docker is high but not total.** podman's docker-CLI - compatibility covers the run/build/compose workflows this targets; uncommon - edge cases (Docker build-secret syntax, some compose v3 keys, `buildx`-specific - features) may differ. Acceptable for agent workflows; the bundled Docker was - non-functional here regardless. -- **Unadorned image names resolve to Docker Hub, not stock podman defaults.** The - Docker-Hub-first drop-ins (Decision 5) mean `docker run nginx` / `hello-world` - behave as Docker users expect, at the cost of a deliberate divergence from stock - podman resolution. A user who wants stock behavior can remove/override the acq - drop-ins with their own `~/.config/containers/registries.conf.d/*` (user config - wins). Docker Hub's blob CDNs (`**.production.cloudflare.docker.com`, - `**.production.cloudfront.docker.com`) are in the balanced egress baseline, so - the default pull path is reachable; arbitrary other registries are not - guaranteed reachable under the default policy. -- **Security posture (SC-7 / SI-10) — improved vs. the superseded rootful design.** - No new external services; a configuration/adapter change (CM-2/CM-6/CM-7; - SA-8/SA-15). `ACQ_MSB_PODMAN_PKGS` is charset-guarded (`[A-Za-z0-9._+ -]`) - before it is interpolated into the root `sh -c`, so operator config cannot - inject shell into the elevated install (SI-10). **Containers run rootless (as - the agent) inside the microVM** — strictly less privilege than the superseded - rootful design, which ran containers as root. The `/dev/net/tun` and - `/dev/fuse` group-scopes to the agent are inside the microVM only (the sandbox - is the security boundary per `AGENTS.md`), and are narrower than the - world-writable alternative; they do not expand the host attack surface. -- **Adds a runtime package install to provision.** First provision on a fresh - sandbox pays an `apt-get install` cost (now `podman podman-compose fuse-overlayfs - uidmap passt slirp4netns`); the marker makes it a one-time cost per sandbox - (skipped on restart / re-provision). The rootless prereqs come from the same - mirror in the same step — no additional dependency class over the engine itself. - -## Alternatives considered - -- **Follow the msb docker-in-docker recipe (start `dockerd`, disk-backed - `/var/lib/docker`).** Officially blessed and gives full Docker API fidelity, - but it is an entrypoint strategy retrofitted poorly onto an adapter that boots - a general base and runs an agent: it needs a post-boot daemon start + socket - poll, a daemon kept alive across restarts, and a per-sandbox disk-backed volume - with its own teardown. Rejected as materially more complex and stateful than - podman for no benefit in the target workflows. -- **Rootful podman via the agent's passwordless sudo (the ORIGINAL, now - SUPERSEDED decision).** The first version of this ADR chose rootful to avoid a - create-time download of the rootless prerequisites (`uidmap`, `passt`). We - reversed it because: - - The premise did not hold: the adapter already downloads `podman - podman-compose fuse-overlayfs` from the OS mirror at provision, so the - rootless prereqs (`uidmap`, `passt`, `slirp4netns`) are two-to-three more - packages from the *same mirror in the same install step* — not a new - dependency class or failure mode. - - Rootful forced a `docker` alias that shells through `sudo -n podman` and a - rootful/rootless probe mismatch (the engine was provisioned/verified rootful - as root, but agents and the verifier run unprivileged), which was the source - of a real, subtle verifier failure and general fragility. - - Rootful ran containers **as root** inside the microVM; rootless is strictly - less privilege for the same capability. - Rootless does require granting the agent access to `/dev/net/tun` and - `/dev/fuse` for its network backend and fuse-overlayfs storage, but those are - small, group-scoped, in-VM changes. Net: rootless removes complexity *and* - improves the security posture, so rootful was rejected. -- **Do nothing when `dockerd` is present (literal task reading).** Would leave - the default image's dead socket / broken `docker compose` as-is. Rejected: the - bundled engine does not actually work under msb, so "present" is not "usable." -- **Always symlink/override in a way that shadows a working custom Docker.** We - place the wrapper in `/usr/local/bin` and never remove `/usr/bin/docker`; an - operator who bakes a genuinely working Docker into a custom `ACQ_MSB_IMAGE` can - set `ACQ_MSB_ENSURE_OCI=0`. -- **World-writable device nodes (`chmod 0666` on `/dev/net/tun`, `/dev/fuse`).** - Simpler than group-scoping, but broader than necessary. We chose `chown - root:agent` + `chmod 0660` to scope device access to the agent. -- **Leave stock podman image resolution (document a user opt-in instead).** - Lowest surprise vs. upstream podman, but leaves `docker run hello-world`/`nginx` - hitting quay/failing for users migrating from Docker. Rejected in favour of the - Docker-Hub-first default (Decision 5) to minimize migration burden; the behavior - is documented and user-overridable. -- **`short-name-mode = "permissive"` as the DEFAULT (the ORIGINAL Decision 5).** - The first version of this ADR shipped `permissive` so ambiguous short names - resolve without prompting. Reconsidered after PR #302 review (a 3-model - consensus plus the reviewer recommended against it) and **rejected as the - default**: `permissive` removes the defense against image substitution / - typosquatting on the prompt-injectable agent path (an injected - `docker run nginx` could silently resolve to `docker.io//nginx`), and - it yields **no DX benefit** here because the single `unqualified-search-registries - = ["docker.io"]` entry already gives deterministic Docker-Hub resolution. The - default is now `enforcing` (fail-closed on ambiguity); `permissive`/`disabled` - remain available as an explicit operator opt-in via `ACQ_MSB_SHORT_NAME_MODE`. -- **Disk-backed named volume for container storage (the msb dind recipe's - approach) instead of fuse-overlayfs — DEFERRED, uncertain payoff.** The msb - [docker-in-sandbox recipe](https://github.com/superradcompany/microsandbox/blob/main/docs/examples/docker/docker-in-sandbox.mdx) - mounts a `--mount-named …:/var/lib/docker:kind=disk` ext4 volume so the engine's - storage sits on a real filesystem, letting it use the **native kernel `overlay`** - driver instead of fuse-overlayfs. Applied to our rootless design this could give - native-overlay performance AND drop the `/dev/fuse` grant. We deferred it because: - - **The performance payoff is unquantified.** fuse-overlayfs adds FUSE overhead, - but for the target agent workflows (pull/build/run) it is unmeasured whether - native overlay is meaningfully faster here; the current path is already - verified working end-to-end. - - **It adds a persistent, per-sandbox stateful artifact acq must own.** A named - volume survives `msb rm` (the recipe calls out `msb volume rm` as a separate - cleanup), so acq would take on volume create/reuse/GC lifecycle — reintroducing - exactly the "per-sandbox disk-backed volume" statefulness we cited as a reason - to avoid the dind recipe in the first place. - - **Empirical unknowns.** Rootless podman's graphroot is under `$HOME`, not - `/var/lib/docker`; whether native kernel overlay works rootless on that ext4 - mount without `/dev/fuse`, and whether `msb create` (not just `msb run`) - threads `--mount-named …kind=disk`, are unverified and need a host spike. - This is recorded as a **future option with an uncertain efficiency payoff**, to - be pursued only if fuse-overlayfs overhead is shown to matter and the - volume-lifecycle cost is judged worth it. The disk-backed volume is orthogonal to - the (rejected) dind *daemon* strategy — only the recipe's storage half is in - scope here. - -## Verification - -- Offline: `scripts/test-acq` covers the default path — the root (`-u 0`) - install/config block (threads `ACQ_MSB_PODMAN_PKGS` via `-e PODMAN_PKGS=`, - writes the storage driver, the Docker-Hub-first registries drop-ins, and the - plain `docker`→`podman` alias) AND the separate rootless verify block - (`-u agent`), the `/dev/net/tun` + `/dev/fuse` group-scope grants, the rootless - prereq packages (uidmap/passt/slirp4netns), marker-gated skip, the `ACQ_MSB_ENSURE_OCI=0` - toggle-off, fail-soft on setup/verify failure (rc 0, warning, no marker), and the - `ACQ_MSB_PODMAN_PKGS` charset-guard against injection. -- Live (verified on the assessment host / in an msb-equivalent sandbox, - 2026-08-11): - - `podman info` FAILED with the kernel `overlay`-over-overlayfs error until - `storage.conf` selected `vfs` (or overlay + fuse-overlayfs); afterward it - reported the expected `graphDriverName`. - - **Rootless as the agent user:** with `uidmap`/`passt`/`slirp4netns` installed, - a vfs/fuse-overlayfs storage config, and `/dev/net/tun` + `/dev/fuse` - group-scoped to the agent, `podman info` reported `rootless=true` and - `docker run --rm docker.io/library/hello-world` printed "Hello from Docker!" - (rc 0) — all as the unprivileged agent, no sudo. - - **`/dev/fuse` gotcha (found on the first host run):** with only `/dev/net/tun` - granted and the PREFERRED overlay+fuse-overlayfs driver selected, `podman info` - (rootless) PASSED but `podman run`/`podman build` FAILED at mount time with - `fuse: failed to open /dev/fuse: Permission denied` — because `podman info` - does not open `/dev/fuse` but the fuse-overlayfs layer mount does. Group-scoping - `/dev/fuse` to the agent (alongside `/dev/net/tun`) fixed it. This is why the - grant helper covers BOTH devices and is not gated behind the `podman info` - verify. `scripts/verify-backends` now includes a **local `docker build` (FROM - scratch)** check that exercises exactly this layer-mount path with no registry - egress, so the regression class is caught unambiguously (verified live: build - succeeds with `/dev/fuse` granted, fails with it denied). - - **Registry/egress:** the bare `hello-world` short name resolves to - `quay.io/podman/hello`, whose blob CDN `cdn01.quay.io` is NOT in the balanced - baseline (this was the perennial WARN). The fully-qualified - `docker.io/library/hello-world` pulls via the balanced-allowlisted Docker Hub - CDNs and succeeds — hence both the Docker-Hub-first default AND the verifier's - use of the fully-qualified image. - - Re-confirm end-to-end (plus `docker compose up` from a small - `docker-compose.yaml`) on a KVM-capable host via `scripts/verify-backends` on - the quarterly re-verification cadence (cannot run inside a sandbox, per - [ADR-0011](0011-msb-backend-and-neutral-kits.md)). - -## Links / tracking - -- Emitter: `_acq_msb_ensure_oci` + `_acq_msb_grant_oci_devs` in - `acq.backends/msb.sh`; wired in `acq_backend_provision` (both) and - `acq_backend_start` (grant-oci-devs, for restart) -- Toggles: `ACQ_MSB_ENSURE_OCI` (default on), `ACQ_MSB_PODMAN_PKGS`, - `ACQ_MSB_SHORT_NAME_MODE` (default `enforcing`) -- Related: [ADR-0011](0011-msb-backend-and-neutral-kits.md) (msb backend + - base-image contract), [ADR-0018](0018-msb-balanced-egress-baseline.md) - (balanced egress — the mirror hosts the install relies on), - [ADR-0017](0017-msb-create-time-startup-script-staging.md) (restart-durability - problem that dind would have re-introduced) -- msb evidence: docker-in-sandbox recipe requires `docker:dind` entrypoint + - disk-backed `/var/lib/docker` - (`docs/examples/docker/docker-in-sandbox.mdx`); microVM init is `/init.krun` - (no service manager); root FS is overlay-backed (observed live) -- podman evidence: daemonless run model and rootless prerequisites - (`newuidmap`/`newgidmap` from shadow-utils, `passt`/pasta) — podman rootless - tutorial and basic-networking guide +- **Backend parity.** The same opt-in kit applies on sbx and msb instead of only + the msb adapter provisioning podman. +- **Narrower default exposure.** The adapter no longer grants OCI-related device + access unconditionally during provision or start. The kit owns those grants and + can gate them on the engine being present, including revoking stale grants when + absent. +- **Clearer default.** Sandboxes that do not need container-in-sandbox support no + longer pay the create-time package-install cost or require package-mirror + egress. +- **Explicit migration.** Users who relied on default msb podman must select + `oci-engine` with `acq configure`, or pass the pinned kit ref through + `ACQ_EXTRA_KITS` / `--kit`. + +## Validation + +- Offline tests cover catalog exposure, configured-name expansion to the pinned + kit ref, configured kit persistence, fresh-shell `acq start` reload, and the + absence of adapter-owned podman install/device grants when `oci-engine` is not + selected. +- `scripts/verify-backends --only msb --oci-only` provisions a dedicated live msb + sandbox with the `oci-engine` kit and verifies rootless podman, the `docker` + alias, a local `docker build`, and a `docker run` when registry egress allows. + +## References + +- [ADR-0030: Agent kits on a devenv base](0030-agent-kits-on-devenv-base.md) +- [ADR-0031: Interactive `acq configure` for extra kits and token-scoping defaults](0031-interactive-acq-configure.md) +- [Known Failure Modes: `docker run` or `docker compose` missing inside a sandbox](../KNOWN_FAILURE_MODES.md#34-docker-run-or-docker-compose-missing-inside-a-sandbox) diff --git a/docs/adr/0021-msb-host-ssh-agent-forwarding-via-vsock.md b/docs/adr/0021-msb-host-ssh-agent-forwarding-via-vsock.md index fbd52781..0c959119 100644 --- a/docs/adr/0021-msb-host-ssh-agent-forwarding-via-vsock.md +++ b/docs/adr/0021-msb-host-ssh-agent-forwarding-via-vsock.md @@ -124,8 +124,7 @@ any interactive `git`/`ssh` see the agent. The `--vsock` route persists across `msb stop`/`start` (it is part of the sandbox config), but the `socat` bridge **process** dies on stop. So the bridge is -re-started in `acq_backend_start`, the same way the OCI device-node re-grant is -re-applied on resume. +re-started in `acq_backend_start` before kit startup services are healed. ### Re-attach to a running sandbox (bridge + `SSH_AUTH_SOCK` must be re-driven) @@ -202,7 +201,7 @@ not used — the host agent socket path is platform-dependent (launchd may keep stable; a plain `ssh-agent` rotates it), so only a live connect is authoritative. Refreshing the route automatically on resume is left as future work pending an msb capability to update a `--vsock` route in place. See -`docs/KNOWN_FAILURE_MODES.md` §34 ("forwarded agent unreachable after a host +`docs/KNOWN_FAILURE_MODES.md` §35 ("forwarded agent unreachable after a host reboot"). ### Version gate diff --git a/docs/howto/msb.md b/docs/howto/msb.md index 44b2d002..69f82591 100644 --- a/docs/howto/msb.md +++ b/docs/howto/msb.md @@ -3,7 +3,7 @@ title: "msb How-To Guide" description: "Detailed how-to for the msb (microsandbox) backend behind acq" status: canonical tier: 2 -last_updated: "2026-09-24" +last_updated: "2026-10-03" audience: "developers" keywords: ["msb", "microsandbox", "backend", "acq", "howto", "sandbox"] related_files: ["docs/BACKEND_GUIDE.md", "docs/CONCEPTS.md", "docs/howto/acq.md", "docs/howto/sbx.md", "docs/adr/0011-msb-backend-and-neutral-kits.md", "docs/adr/0024-neutral-user-facing-docs-vs-backend-specific.md"] @@ -251,7 +251,7 @@ abstract — use the raw `msb` command: msb exposes a large set of `ACQ_MSB_*` (and neutral `ACQ_*`) tunables — base image and pull policy, network egress tier, memory/CPU, DNS, host ssh-agent -forwarding, post-hoc port publishing, and OCI-engine provisioning. Rather than +forwarding, post-hoc port publishing, and opt-in OCI-engine support. Rather than duplicate them here, the authoritative reference is the [Backend Guide → msb Backend](../BACKEND_GUIDE.md#msb-backend-microsandbox-default), including: diff --git a/scripts/verify-backends b/scripts/verify-backends index 1c4cb6bd..0210ed0d 100755 --- a/scripts/verify-backends +++ b/scripts/verify-backends @@ -33,9 +33,10 @@ # 5. The migration-sensitive boot contract stays intact: the verifier's # ACQ_EXTRA_KITS volume mounts at boot, acq exec starts in the workspace as # the agent user, and an opencode sandbox re-attaches through `acq run NAME -- -# --version`. Offline bats additionally freeze the fresh `acq run opencode .` -# dispatch surface: --clone / ACQ_CLONE, ACQ_WORKSPACE, --kit ordering, -# ACQ_IMAGE, boot volumes, and login-shell shape. +# --version`. On msb, the optional `oci-engine` catalog kit is verified in a +# separate sandbox. Offline bats additionally freeze the fresh `acq run +# opencode .` dispatch surface: --clone / ACQ_CLONE, ACQ_WORKSPACE, --kit +# ordering, ACQ_IMAGE, boot volumes, and login-shell shape. # A backend that is NOT installed is SKIPPED (its row prints "skipped"). # # This CANNOT run inside an sbx/msb sandbox (no nested sandboxes) — see ADR-0011 @@ -53,11 +54,10 @@ # (default: always remove) for manual poking. # --only BACKEND Run just one backend (sbx|msb), skipping # the other — handy when iterating. -# --oci-only Skip the slow create + kit-apply and run -# ONLY the OCI (podman) checks against an -# already-existing exec-ready sandbox. -# Pair with a prior `--only msb -k` run so -# the sandbox is left in place to reuse. +# --oci-only Skip the broad backend checks and run +# ONLY the OCI (podman) checks. Creates a +# dedicated sandbox with the oci-engine kit +# unless an exec-ready one already exists. # ACQ_VERIFY_USE_GH_AUTH=1 Allow the GitHub seed to fall back to # `gh auth token`. OFF by default because a # keychain-backed gh can block on a @@ -854,8 +854,17 @@ verify_backend() { _stop_ephemeral_agent fi - # 5) OCI engine (podman) usable — msb only (ADR-0020). - _verify_oci_checks "$backend" "$name" + # 5) Default sandbox has no adapter-owned OCI install; OCI is opt-in via the + # oci-engine catalog kit, verified separately below. + if [ "$backend" = "msb" ]; then + if run_acq "exec no-default-oci" --backend "$backend" exec "$name" -- sh -c \ + 'test ! -e /var/lib/acq/oci-ready && ! command -v podman >/dev/null 2>&1'; then + pass "${backend}: OCI engine is not installed by default" + else + fail "${backend}: OCI engine appeared without selecting the oci-engine kit" + dump "no-default-oci output" "$LAST_OUT" + fi + fi # 6) Neutral kit volumes (ADR-0023): the verifier's fixture kit (appended to # ACQ_EXTRA_KITS at setup) declares a 256m block volume at /acq-verify-vol. @@ -1149,86 +1158,101 @@ _verify_ver_ge() { return 0 } -# _verify_oci_checks BACKEND NAME — the OCI engine + run checks (msb only, -# ADR-0020). Factored out of verify_backend so the --oci-only fast path can -# re-run JUST these against an existing sandbox without the slow create/kit-apply. -# acq provisions rootless podman and aliases docker->podman so agents can run OCI -# images; skipped when ACQ_MSB_ENSURE_OCI=0 (operator opted out). -_verify_oci_checks() { - local backend="$1" name="$2" - [ "$backend" = "msb" ] || return 0 - case "$(printf '%s' "${ACQ_MSB_ENSURE_OCI-1}" | tr '[:upper:]' '[:lower:]')" in - ""|0|false|no|off) - skip "${backend}: OCI engine check (ACQ_MSB_ENSURE_OCI disabled)" ;; - *) - # docker (our wrapper) resolves to podman, and the engine reports info. - # The engine runs ROOTLESS as the agent user (ADR-0020), and `acq exec` - # already runs as the agent, so `docker info` (= plain `podman info`) - # exercises the real usable path: the rootless prereqs, the storage driver - # on the overlay root, and the docker->podman alias. - if run_acq "exec oci-engine" --backend "$backend" exec "$name" -- sh -c \ - 'command -v podman >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1'; then - pass "${backend}: OCI engine (rootless podman) present and docker->podman aliased" - else - fail "${backend}: OCI engine (podman) not usable (see ADR-0020)" - dump "oci-engine output" "$LAST_OUT" - fi - # LOCAL BUILD (no registry): build a trivial `FROM scratch` image. This - # exercises the storage-driver LAYER MOUNT — the exact operation that - # `docker info` does NOT perform but every real build/run does. It is what - # caught the rootless /dev/fuse regression: with the fuse-overlayfs driver - # but /dev/fuse ungranted, `docker info` PASSES yet the layer mount fails - # ("fuse: failed to open /dev/fuse: Permission denied"). Because it needs - # neither egress nor a registry, a failure here is UNAMBIGUOUSLY an - # engine/storage fault (unlike the hello-world run below, which depends on - # Docker Hub egress) — and `docker build` is itself a capability agents use. - # We assert the BUILD succeeds; the build mounts layers, which is the mount - # path we need to prove. `FROM scratch` needs no base image (no pull). - if run_acq "exec oci-build" --backend "$backend" exec "$name" -- sh -c ' - set -e - d=$(mktemp -d) - printf "FROM scratch\nCOPY hi /hi\n" > "$d/Containerfile" - echo hi > "$d/hi" - docker build -q -t acq-oci-selftest:local "$d" >/dev/null 2>&1 - rc=$? - docker rmi -f acq-oci-selftest:local >/dev/null 2>&1 || true - rm -rf "$d" - exit $rc'; then - pass "${backend}: local 'docker build' (rootless, fuse-overlayfs mount) works" - else - fail "${backend}: local 'docker build' failed — storage-driver mount fault (see ADR-0020)" - dump "oci-build output" "$LAST_OUT" - fi - # End-to-end: run a trivial container through the docker alias. We use the - # FULLY-QUALIFIED docker.io/library/hello-world (NOT the bare `hello-world` - # short name, which podman's stock shortnames map to quay.io/podman/hello — - # whose blob CDN cdn01.quay.io is NOT in the balanced baseline). Docker - # Hub's blob CDNs (**.production.cloudflare.docker.com / - # **.production.cloudfront.docker.com) ARE in the balanced baseline, so this - # pull+run succeeds under the default policy. - # - # Escalation: distinguish "engine can't run a container" (a real FAIL) from - # "registry egress is blocked" (environmental WARN). If the run fails, probe - # registry reachability (a 401 from registry-1.docker.io/v2/ means egress is - # fine and the failure is the engine → FAIL; anything else means egress is - # the culprit → WARN). - if run_acq "exec oci-run" --backend "$backend" exec "$name" -- sh -c \ - 'docker run --rm docker.io/library/hello-world >/dev/null 2>&1'; then - pass "${backend}: 'docker run' (via podman) executes an OCI image" - else - local _oci_run_out="$LAST_OUT" - if run_acq "exec oci-egress-probe" --backend "$backend" exec "$name" -- sh -c \ - '[ "$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 https://registry-1.docker.io/v2/)" = "401" ]'; then - # Registry egress works, but the container did not run -> genuine engine fault. - fail "${backend}: 'docker run' failed though Docker Hub egress is reachable (engine fault)" - dump "oci-run output" "$_oci_run_out" - else - warn "${backend}: 'docker run hello-world' did not complete (registry egress?); engine wiring OK" - dump "oci-run output" "$_oci_run_out" - fi - fi - ;; - esac +# verify_oci_kit — provision a dedicated msb sandbox with the opt-in oci-engine +# catalog kit, then prove the docker->podman path. This is a live check: without +# msb or a sandbox-capable host it SKIPs/BLOCKS loudly rather than passing. +verify_oci_kit() { + local backend="msb" name created="" + echo "" + echo "== backend: msb (oci-engine kit) ==" + + command -v msb >/dev/null 2>&1 || { skip "msb: oci-engine kit skipped (msb not installed)"; return 0; } + _verify_backend_enabled msb || return 0 + if ! msb doctor >/dev/null 2>&1; then + skip "msb: oci-engine kit skipped (msb doctor reports host virtualization is not ready)" + return 0 + fi + + name=$( + # shellcheck disable=SC1090 + ACQ_SOURCE_ONLY=1 ACQ_SCRIPT_DIR="$REPO_ROOT" . "$ACQ" >/dev/null 2>&1 + derive_name shell "$WORKDIR-oci" 2>/dev/null + ) + [ -n "$name" ] || name="shell-$(basename "$WORKDIR" | tr '[:upper:]' '[:lower:]')-oci" + printf ' sandbox name: %s\n' "$name" + + if [ -n "${OCI_ONLY:-}" ] && run_acq "exists-oci-kit" --backend "$backend" exec "$name" -- true >/dev/null 2>&1; then + printf ' (--oci-only: reusing existing exec-ready sandbox %s)\n' "$name" + else + seed_usai_secret "$backend" "$name" + if [ "$USAI_SEEDED_FOR" != "$name" ]; then + skip "${backend}: oci-engine kit skipped (verifier did not seed sandbox-scoped USAi key)" + return 0 + fi + local oci_kit_ref + oci_kit_ref=$( + # shellcheck disable=SC1090 + ACQ_SOURCE_ONLY=1 ACQ_SCRIPT_DIR="$REPO_ROOT" . "$ACQ" >/dev/null 2>&1 + _acq_optin_kit_ref oci-engine + ) + printf ' creating sandbox %s with oci-engine kit (booting + package install can take a few minutes)...\n' "$name" + if ! run_acq "create-oci-kit" --backend "$backend" create shell "$WORKDIR" --name "$name" --kit "$oci_kit_ref"; then + fail "${backend}: provision with oci-engine kit failed" + dump "acq create output" "$LAST_OUT" + unseed_usai_secret "$backend" "$name" + [ -n "$KEEP" ] || run_acq "rm-oci-kit" --backend "$backend" rm "$name" >/dev/null 2>&1 || true + return + fi + created=1 + unseed_usai_secret "$backend" "$name" + fi + + if run_acq "exec oci-engine" --backend "$backend" exec "$name" -- sh -c \ + 'command -v podman >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1'; then + pass "${backend}: oci-engine kit installed rootless podman and docker alias" + else + fail "${backend}: oci-engine kit did not provide a usable podman engine" + dump "oci-engine output" "$LAST_OUT" + fi + + if run_acq "exec oci-build" --backend "$backend" exec "$name" -- sh -c ' + set -e + d=$(mktemp -d) + printf "FROM scratch\nCOPY hi /hi\n" > "$d/Containerfile" + echo hi > "$d/hi" + docker build -q -t acq-oci-selftest:local "$d" >/dev/null 2>&1 + rc=$? + docker rmi -f acq-oci-selftest:local >/dev/null 2>&1 || true + rm -rf "$d" + exit $rc'; then + pass "${backend}: oci-engine kit supports local docker build" + else + fail "${backend}: oci-engine kit local docker build failed" + dump "oci-build output" "$LAST_OUT" + fi + + if run_acq "exec oci-run" --backend "$backend" exec "$name" -- sh -c \ + 'docker run --rm docker.io/library/hello-world >/dev/null 2>&1'; then + pass "${backend}: oci-engine kit runs an OCI image via docker alias" + else + local _oci_run_out="$LAST_OUT" + if run_acq "exec oci-egress-probe" --backend "$backend" exec "$name" -- sh -c \ + '[ "$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 https://registry-1.docker.io/v2/)" = "401" ]'; then + fail "${backend}: docker run failed though Docker Hub egress is reachable" + dump "oci-run output" "$_oci_run_out" + else + warn "${backend}: docker run hello-world did not complete (registry egress?); engine wiring OK" + dump "oci-run output" "$_oci_run_out" + fi + fi + + if [ -n "$created" ]; then + if [ -n "$KEEP" ] && [ "$FAIL" -gt 0 ]; then + printf ' (keeping sandbox %s; --keep set)\n' "$name" + else + run_acq "rm-oci-kit" --backend "$backend" rm "$name" >/dev/null 2>&1 || true + fi + fi } # verify_agent_install BACKEND — provision an `opencode` sandbox (not `shell`) @@ -1332,27 +1356,32 @@ if [ -x "${REPO_ROOT}/scripts/fix-exec-bits" ] || [ -f "${REPO_ROOT}/scripts/fix unset _fix_out fi -# sbx -if command -v sbx >/dev/null 2>&1; then - if _verify_backend_enabled sbx; then - verify_backend sbx - verify_agent_install sbx - fi +if [ -n "${OCI_ONLY:-}" ]; then + verify_oci_kit else - if _verify_backend_enabled sbx; then - echo ""; echo "== backend: sbx =="; skip "sbx: not installed" + # sbx + if command -v sbx >/dev/null 2>&1; then + if _verify_backend_enabled sbx; then + verify_backend sbx + verify_agent_install sbx + fi + else + if _verify_backend_enabled sbx; then + echo ""; echo "== backend: sbx =="; skip "sbx: not installed" + fi fi -fi -# msb -if command -v msb >/dev/null 2>&1; then - if _verify_backend_enabled msb; then - verify_backend msb - verify_agent_install msb - fi -else - if _verify_backend_enabled msb; then - echo ""; echo "== backend: msb =="; skip "msb: not installed" + # msb + if command -v msb >/dev/null 2>&1; then + if _verify_backend_enabled msb; then + verify_backend msb + verify_agent_install msb + verify_oci_kit + fi + else + if _verify_backend_enabled msb; then + echo ""; echo "== backend: msb =="; skip "msb: not installed" + fi fi fi From c25e4ad80fc2790ea1b89bcb468542f14e0520a2 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 4 Oct 2026 01:12:28 +0000 Subject: [PATCH 4/5] chore(kits): pin patterns v1.11.0 Move the built-in patterns bundle from v1.10.0 (2dd2ad6ad5f63b842d732424bed5ba9aebeed676) to v1.11.0 (f2227413df78dc5ef6580b5e29c59542dfe1f66a). Included patterns commits: - 36038a6 feat(acq-kits): add opencode agent-harness kit (SHA-pinned release binary) (GSA-TTS/agentic-coding-patterns#455) - 73606d7 fix(usai-provider): make the catalog drift check report real drift (GSA-TTS/agentic-coding-patterns#459) - 56500ba fix(usai-provider): drop embedding models from the chat-model catalog (GSA-TTS/agentic-coding-patterns#462) The oci-engine kit this PR adds to the opt-in catalog is still present at the new ref, so catalog selection is unchanged. The opencode agent kit becomes fetchable at this pin for the first time, but it stays dormant: acq_agent_builtin_kit_enabled returns false for every agent, so _build_kit_list still yields four built-in kits and no agent kit is applied. Enabling it is separate work tracked by GSA-TTS/agentic-coding-quickstart#485. provider-facts/usai.env remains absent at v1.11.0, so the provider-facts loader still returns 2 (artifact absent, tolerated) and the transitional fallback defaults remain the only reachable path. GSA-TTS/agentic-coding-quickstart#481 is unaffected. Behavior change riding along with the bump: the usai-provider kit applies to every sandbox, and its model catalog no longer advertises the cohere_english_v3 embedding model (13 lines removed from files/home/usai-config/opencode.jsonc), with 7 lines added to scripts/sync-usai-models.mjs so the drift check reports real drift. Co-authored-by: OpenCode [claude-opus-5] --- acq.backends/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acq.backends/common.sh b/acq.backends/common.sh index becc2984..d2fc4cf0 100644 --- a/acq.backends/common.sh +++ b/acq.backends/common.sh @@ -36,7 +36,7 @@ PATTERNS_KIT_REPO="git+https://github.com/GSA-TTS/agentic-coding-patterns.git" # the bundle-version anchor recorded in a sandbox's host-side provenance record # (see ACQ_BUILTIN_BUNDLE below + the provenance helpers) so acq can tell a # stale sandbox from a current one. -PATTERNS_KIT_REF="2dd2ad6ad5f63b842d732424bed5ba9aebeed676" # agentic-coding-patterns v1.10.0 +PATTERNS_KIT_REF="f2227413df78dc5ef6580b5e29c59542dfe1f66a" # agentic-coding-patterns v1.11.0 PATTERNS_KIT_DIR="integrations/isolation/acq-kits" USAI_PROVIDER_KIT_NAME="usai-provider" From 93aae9c456c301b5ecd7e542e402d1172d6f927a Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 4 Oct 2026 01:13:32 +0000 Subject: [PATCH 5/5] test(kits): align oci catalog checks with v1.11.0 Update documentation examples and bats assertions to match the patterns v1.11.0 pin. Refs: GSA-TTS/agentic-coding-quickstart#505 Co-authored-by: OpenCode [gpt-5.5] --- docs/BACKEND_GUIDE.md | 2 +- docs/KNOWN_FAILURE_MODES.md | 2 +- test/bats/135-configure.bats | 2 +- test/bats/95-kit-ref-persistence.bats | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/BACKEND_GUIDE.md b/docs/BACKEND_GUIDE.md index dd524d2f..cc855f52 100644 --- a/docs/BACKEND_GUIDE.md +++ b/docs/BACKEND_GUIDE.md @@ -691,7 +691,7 @@ You can also apply it for one sandbox by passing the pinned kit ref through `--kit`, or by exporting that same ref in `ACQ_EXTRA_KITS`: ```bash -acq run opencode . --kit 'git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=2dd2ad6ad5f63b842d732424bed5ba9aebeed676&dir=integrations/isolation/acq-kits/oci-engine' +acq run opencode . --kit 'git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=f2227413df78dc5ef6580b5e29c59542dfe1f66a&dir=integrations/isolation/acq-kits/oci-engine' ``` The kit provisions **rootless podman** and aliases `docker` to `podman`, so diff --git a/docs/KNOWN_FAILURE_MODES.md b/docs/KNOWN_FAILURE_MODES.md index 7b5874d6..1d8f86a4 100644 --- a/docs/KNOWN_FAILURE_MODES.md +++ b/docs/KNOWN_FAILURE_MODES.md @@ -1772,7 +1772,7 @@ For a single sandbox, pass the pinned kit ref with `--kit` or include it in `ACQ_EXTRA_KITS`: ```bash -acq run opencode . --kit 'git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=2dd2ad6ad5f63b842d732424bed5ba9aebeed676&dir=integrations/isolation/acq-kits/oci-engine' +acq run opencode . --kit 'git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=f2227413df78dc5ef6580b5e29c59542dfe1f66a&dir=integrations/isolation/acq-kits/oci-engine' ``` Existing sandboxes that were created before this change keep whatever podman diff --git a/test/bats/135-configure.bats b/test/bats/135-configure.bats index a2a5e7d1..bf7e9eea 100644 --- a/test/bats/135-configure.bats +++ b/test/bats/135-configure.bats @@ -160,7 +160,7 @@ _cfg_src() { ' assert_success assert_line 'oci-engine' - assert_output --partial 'ref=2dd2ad6ad5f63b842d732424bed5ba9aebeed676' + assert_output --partial 'ref=f2227413df78dc5ef6580b5e29c59542dfe1f66a' assert_output --partial 'dir=integrations/isolation/acq-kits/oci-engine' } diff --git a/test/bats/95-kit-ref-persistence.bats b/test/bats/95-kit-ref-persistence.bats index 1ce8472b..c9bfe6c7 100644 --- a/test/bats/95-kit-ref-persistence.bats +++ b/test/bats/95-kit-ref-persistence.bats @@ -89,7 +89,7 @@ SPEC assert_regex "$log" 'ocistartbox:/home/agent/oci-engine-marker' run cat "$(_acq_cli_kits_file msb ocistartbox)" assert_output --partial 'dir=integrations/isolation/acq-kits/oci-engine' - assert_output --partial 'ref=2dd2ad6ad5f63b842d732424bed5ba9aebeed676' + assert_output --partial 'ref=f2227413df78dc5ef6580b5e29c59542dfe1f66a' } @test "cli-kits: backend keying — an sbx read does not see an msb record" {