Repository navigation
feat(integrations): add neutral hybrid/v1 acq-kits + schema + registry - #221
Conversation
First slice of Phase 2 Part A (neutral acq-kits). Adds the backend-agnostic kit JSON Schema (schemaVersion: "hybrid/v1") that the four converted kits will validate against, and turns the old sbx-kits/ README into a one-release redirect pointing at the new integrations/isolation/acq-kits/ home. Kits, registry, and the migration ADR land in follow-up commits on this branch. Part A of the acq Phase 2 handoff (agentic-coding-quickstart#202). AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
Convert the usai-provider sbx kit to the neutral hybrid/v1 spec under integrations/isolation/acq-kits/usai-provider/. Payloads (opencode.jsonc, merge-global-config.mjs) carried verbatim; the node tooling and tests (package.json, scripts/sync-usai-models.mjs, tests/) move here so the new home is the single source of truth. Also: - extend kit-hybrid-v1 schema with files[].source (payload file under the kit's files/ tree) as an alternative to inline content, for multi-line payloads. - add integrations/isolation/acq-kits/validate-kits.py: schema-validates every acq-kit spec and checks source paths + known-backend keys + README presence (the backend-agnostic gate; live per-backend checks stay in scripts/verify). Verification: - validate-kits.py: usai-provider OK - node --test tests/**: 41 pass / 0 fail - bash -n scripts/verify: clean - scan_unsafe_shell.py acq-kits/: 0 errors, 0 warnings Part A of the acq Phase 2 handoff (agentic-coding-quickstart#202). AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
Convert the playbook sbx kit to the neutral hybrid/v1 spec. Per the acq design doc §6, the ~80-line inline startup shell is extracted into a standalone, lintable, testable script at files/home/playbook-clone.sh (dropped to /home/agent/playbook-clone.sh, run at startup). Clone-integrity SHA gate, idempotency, and graceful non-fatal degradation are unchanged. New decision record: extract-clone-script-to-file.md. Verification: - validate-kits.py: agentic-coding-playbook OK - bash -n playbook-clone.sh + scripts/verify: clean - scan_unsafe_shell.py acq-kits/: 0 errors, 0 warnings - ran playbook-clone.sh with a bad ref: exits 0, no clone left behind (graceful degradation path confirmed) Part A of the acq Phase 2 handoff (agentic-coding-quickstart#202). AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
Convert the Zscaler CA sbx kit to the neutral hybrid/v1 spec. This kit is the showcase for backend_shortcuts: declares backend_shortcuts.msb.trust_host_cas so the msb adapter uses its native --trust-host-cas primitive (and skips the file-drop), while sbx (and ppp later) fall through to the file-drop + update-ca-certificates path. Behavioral parity: the guest trusts the Zscaler CA either way (README parity note). The public Zscaler Root CA moves from an inline spec PEM to a files/ payload (files/home/zscaler-ca.crt) with the initFiles phase hint, preserving the exact create-time ordering. scripts/verify now reads the payload (falls back to inline). Verification: - validate-kits.py: zscaler-ca-certificate OK - openssl fingerprint == documented Zscaler Root CA (04:F6:1F:...:1A:53) - bash -n scripts/verify: clean - scan_unsafe_shell.py acq-kits/: 0 errors, 0 warnings Part A of the acq Phase 2 handoff (agentic-coding-quickstart#202). AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
Convert the vendored git-ssh-sign kit to the neutral hybrid/v1 spec. The signing-time key-resolution command moves from an inline initFiles blob to a files/ payload (files/home/.config/git/ssh-signing-key-command, mode 0755); the system-gitconfig install command carries the install phase. No backend shortcut — SSH-agent forwarding is the shared mechanism, enabled natively per backend (sbx socket forward; msb 'ssh authorize'; ppp 'podman machine ssh -A'), noted in the README parity table. Apache-2.0 provenance retained: LICENSE + NOTICE (updated with the hybrid/v1 port note) + docs/decisions/vendored-from-sbx-kits-contrib.md + docs/security-skill-intake.md carried over. Verification: - validate-kits.py: git-ssh-sign OK (all 4 kits valid) - bash -n ssh-signing-key-command + scripts/verify: clean - scan_unsafe_shell.py acq-kits/: 0 errors, 0 warnings Part A of the acq Phase 2 handoff (agentic-coding-quickstart#202). AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
Add integrations/isolation/acq-kits/kits.yaml (name -> backends + parity prose, sourced from the acq design doc §6) and the acq-kits/ area README. Extend validate-kits.py with a registry cross-check: kits.yaml must list exactly the kits present (no missing, no unknown entries). For Phase 2 each kit lists backends: [sbx, msb] (ppp arrives in Phase 3). Verification: - validate-kits.py: all 4 kits valid + registry cross-check passes - kits.yaml parses as YAML Part A of the acq Phase 2 handoff (agentic-coding-quickstart#202). AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
Area-scoped ADR (integrations/isolation/docs/decisions/0001) recording the sbx-kits -> acq-kits move: why the neutral hybrid/v1 spec, the four kits' new shape, the msb --trust-host-cas shortcut for zscaler-ca, the registry, the dir-name normalization, and the one-release redirect. Also records deferred live per-backend verification (no nested sandboxes) and the Part B follow-ups gated on this PR's merge SHA. Verification: - relative links resolved (schema, registry, repo-wide ADR) - validate-kits.py + validate_repo.py green Part A of the acq Phase 2 handoff (agentic-coding-quickstart#202). AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
…MD028) Markdown Lint CI flagged MD028 (blank line inside blockquote) at integrations/isolation/acq-kits/git-ssh-sign/README.md:16 — two consecutive blockquotes separated by a blank line. Join them with a '>' continuation line. AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
…lize usai-provider name Address review feedback from PR #221: - validate-kits.py: guard against a non-dict spec (empty/scalar spec.yaml) before the dict-shaped checks so a malformed kit is reported as invalid rather than crashing the whole run with an AttributeError; also validate the registry root is a mapping. - Wire the offline gate into CI + one-command entry points so the ADR claim ('runs offline in CI') is true: 'make validate-kits' / 'make test-kits' (folded into 'make ci'), a 'validate-acq-kits' pre-commit hook, and CI steps in ci.yml (Pattern Validation runs the schema gate; new 'acq-kits Tests' job runs the node suites). ADR + acq-kits README updated to point at them. - Normalize the lingering '-kit' suffix to match the normalized dir/spec name: package.json name -> usai-provider, and the ownership marker comment usai-provider:owns-opencode-config (opencode.jsonc, merge test, ADR-0002, README) — the marker is a fresh token, no existing sandboxes depend on it. Verification: - validate-kits.py: 4/4 valid; malformed empty+scalar specs now report cleanly (exit 1, no traceback) - node --test: 41/41 pass (marker rename reflected in tests) - validate_repo.py + scan_unsafe_shell.py: clean - ci.yml / .pre-commit-config.yaml parse as YAML Part A of the acq Phase 2 handoff (agentic-coding-quickstart#202). AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
Review finding L1 (documentation-only): the signing-key command trusts whatever key the forwarded SSH agent presents first and writes allowed_signers keyed to a possibly-unset email. Add a header comment stating this is a sandbox self-signing convenience and the emitted allowed_signers file must not be reused as an authorization root outside the ephemeral sandbox. No behavior change. AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
|
@wz-gsa can you run a review loop here? I already did a few locally using the playbook skills so it should be pretty clean. I need to get this one merged ASAP to unblock GSA-TTS/agentic-coding-quickstart#202 |
wz-gsa
left a comment
There was a problem hiding this comment.
Adversarial review — #221 feat(integrations): neutral hybrid/v1 acq-kits + schema + registry. Verdict: request changes. No blockers — but harden the validator you're selling as the safety net before it lands.
Big PR, and mostly good. I checked drift, provenance, schema, CI, and the validator. What holds up: the re-homed merge-global-config.mjs and sync-usai-models.mjs are byte-identical to the versions already reviewed (opencode.jsonc differs only in the ownership-marker string — kit rename, benign). git-ssh-sign vendoring is clean — Apache-2.0 LICENSE + NOTICE + provenance SHA + ADR + intake record, and the signing-key-command touches only the public key, no eval/exec, shellcheck-clean. CI Actions are SHA-pinned, permissions: contents:read, no injection surface. Schema is additionalProperties:false on the structured objects; registry matches disk; per-kit phase usage matches intent. Good work.
Two things to fix, both in validate-kits.py — which is the gate this PR advertises as its safety net, so it has to actually hold:
1. It's not fail-closed. An empty spec.yaml (yaml.safe_load → None) or a top-level list crashes validate_kit with a raw AttributeError: 'NoneType' object has no attribute 'get' — and in my repro it exited 0, i.e. a stacktrace, not a clean FAIL: with non-zero exit. Worse, the registry-name harvest wraps the same load in a bare except Exception: pass, so a malformed spec silently drops out of the drift cross-check — masking the exact error that block exists to catch. Fix: guard if not isinstance(spec, dict): return ["<kit>: spec.yaml is not a mapping"] early, and narrow the bare except.
2. Can-pass-but-broken. The validator checks files[].source exists on the host, but never that commands[] argv reference in-guest paths that any files[] actually drops. Verified: a kit whose commands[] runs /home/agent/NEVER-DROPPED.sh (dropped by nothing) passes with zero errors. A re-home typo sails through the offline gate and only fails live. A cheap best-effort files[].path ↔ commands[] consistency check (warn-level) would catch real regressions — which is the whole point of a re-home gate.
Two smaller ones, non-blocking:
backend_shortcuts.{sbx,msb,ppp}/backend_extras.*are baretype:object— arbitrary content validates (I confirmed{"run":"rm -rf /"}passes). The schema comment and ADR say "kept in-spec so review covers it," but schema-wise nothing constrains it. It's a deliberate escape hatch, fine — just downgrade the doc claim from "review covers it" to "human-review-only; not schema-enforced."pppis a pure YAGNI enum slot — it's in the schema,KNOWN_BACKENDS, and prose, with no adapter or consumer anywhere.msbearns its place (real in-flight backend + an exercisedzscaler.backend_shortcuts.msbshortcut);pppdoesn't yet. Drop it from the enum until a Phase-3 consumer exists, or mark it explicitly reserved-unused (right now it reads as supported).
The abstraction is justified — msb is a real second backend, not speculation — and it's well documented. This isn't a redesign. Fix the two validator gaps so the gate actually fails closed, and it's an approve.
Address @wz-gsa CHANGES_REQUESTED on PR #221 — make the validator the PR sells as its safety net actually fail closed. Finding 1 (fail-closed): already landed in the prior commit — a non-dict/empty spec.yaml now returns a clean per-kit error (exit 1, no AttributeError traceback), and the registry name-harvest catch is narrowed to (yaml.YAMLError, OSError) instead of bare Exception. Re-confirmed with empty and top-level-list spec repros. Finding 2 (can-pass-but-broken): add a best-effort commands[] <-> files[] consistency check. A kit-owned payload path (/home/... .sh/.mjs/.py/.crt/.pem) referenced by commands[] argv but dropped by no files[] entry is almost always a re-home typo. Emitted as a WARN by default (heuristic: a command may create a file at runtime); a new --strict flag promotes warnings to failures, and the Makefile / pre-commit / CI gate all run --strict so a re-home typo fails the offline gate instead of only failing live. Nits: - backend_shortcuts/backend_extras: schema descriptions + ADR + acq-kits README now state the per-backend values are unconstrained objects, human-review-only and NOT schema-enforced (was 'kept in-spec so review covers it'). - ppp: kept (Podman backend is in active development, imminent Phase 3) but explicitly documented as a RESERVED slot with no consumer yet, in the schema descriptions, ADR, and README — so it no longer reads as already-supported. Verification: - validate-kits.py --strict: 4/4 valid, 0 warnings - negative repros: empty/list spec -> exit 1; commands-without-drop -> WARN (exit 0 default) / exit 1 (--strict) - node --test: 41/41; validate_repo + scan_unsafe_shell clean; schema check_schema OK; unknown backend key still rejected, ppp extras accepted - ci.yml / .pre-commit-config.yaml parse as YAML Part A of the acq Phase 2 handoff (agentic-coding-quickstart#202). AI-assisted (OpenCode Agent, model: claude_4_8_opus). Co-authored-by: OpenCode Agent <[email protected]>
…release gate met Part A (patterns PR #221: neutral hybrid/v1 acq-kits + kit-hybrid-v1 schema + registry) merged to agentic-coding-patterns main and shipped in the v1.6.0 release. Flip PATTERNS_KIT_REF from the provisional pre-merge PR-head SHA to the v1.6.0 release commit e387c59bb2f743eb321bfb56a8ac71a6abb185ae, satisfying the Phase 2 handoff §4.1 hard gate. Pinning to a release tag mirrors Phase 1's v1.5.0 pin; v1.6.0 includes Part A unchanged (it also adds an unrelated sbx kit + a validation script — the acq-kits and schema are identical to the #221 merge). Verified against the pinned commit: live sparse-fetch of all four acq-kits, and neutral->sbx-v2 translation + kit_validate of each, all pass. - common.sh: PATTERNS_KIT_REF -> e387c59…; drop the provisional-pin TODO. - test-acq: ref assertions -> e387c59. - ADR-0011 + acq-design appendix: release gate marked satisfied. test-acq 124/124; test-migrate-or-halt 57/57; markdownlint clean; shellcheck 0.11.0 (--severity=warning) clean. Refs: GSA-TTS/agentic-coding-patterns#221 Co-authored-by: OpenCode Agent <[email protected]>
…cope + create-scrub Address adversarial review of PR #202. FAIL (medium) — root command injection via unvalidated kit `mode`. A kit spec's files[].mode was interpolated unquoted into a root `sh -c "chmod $mode '$path'"`, so a hostile/typo'd spec (patterns repo / ACQ_EXTRA_KITS) with `mode: "0644; wget|sh"` ran arbitrary code as root in the guest. Fixes: - kit_spec_files now DROPS any record whose mode isn't octal ([0-7]{3,4}) or whose path/source contains shell metacharacters/whitespace (warns to stderr). - kit_spec_commands DROPS any command whose user isn't a bare uid/safe username or whose phase isn't install|initFiles|startup (both reach `msb exec -u`). - msb _acq_msb_copy_file_verified re-validates path up front and mode at use, and runs `chmod`/`chown` as argv (msb exec -u 0 -- chmod MODE PATH) instead of an `sh -c` string — no interpolation into a shell. - kit_validate scans the RAW spec so a bad mode/phase/user is REPORTED (not just silently dropped) and makes `acq kit validate` fail. Command argv was already base64-tokenized + passed as separate argv (not a joined sh -c), and net-allow hosts were already charset-validated; this closes the mode/path/user gap. The mode/user validation also belongs in patterns #221's validate-kits.py at the gate (noted for that PR); acq validates defensively regardless. CONCERN (medium) — document that --tls-intercept is guest-global. ADR-0011 now states interception covers all guest egress (guest auto-trusts the interception CA), while secret substitution stays host-scoped — a broader posture than sbx's proxy injection, and the price of the microVM boundary. Added SI-10 to controls. CONCERN (low) — set -e skipped the create-failure secret scrub. Under `set -euo pipefail` a failing `msb create` aborted before the rc capture, so the error block AND the transient-secret unset never ran. Use `|| _create_rc=$?`. Nit — secret store: get now falls back to the file backend under ACQ_SECRET_ALLOW_ARGV so a value stored without the flag is still found. Doc nits: ADR date 2026-07-16, "135 checks" (was 79). Tests: +5 security cases (hostile mode dropped, msb chmod argv/refusal, hostile command user dropped, `acq kit validate` reports mode/phase). test-acq 135/135; test-migrate-or-halt 57/57; markdownlint clean; shellcheck 0.11.0 clean. Refs: #202, #203, GSA-TTS/agentic-coding-patterns#221 Co-authored-by: OpenCode Agent <[email protected]>
#224) * feat(acq-kits): convert openchamber to hybrid/v1 and move to acq-kits/ Closes #223. The openchamber kit (added in #220) was left in the deprecated sbx-kits/ tree as a schemaVersion "2" sbx spec when #221 migrated the other four kits to the neutral hybrid/v1 acq-kits/ layout. This converts it to match. Changes: - Move integrations/isolation/sbx-kits/openchamber/ -> acq-kits/openchamber/. - Rewrite spec.yaml as schemaVersion: "hybrid/v1": neutral caps.network, a files[] entry, and a startup-phase command. The former inline startup body is extracted to files/home/openchamber-start.sh (backend-agnostic, lintable/testable) — the same pattern agentic-coding-playbook uses. This reverses the old inline-startup-script ADR, which is safe now: that ADR worked around sbx's create-time static-file *content* hook (#118); the neutral files[].source drop is a different, working mechanism. - Port publishing + background startup have no neutral vocabulary yet, so they live in backend_extras.sbx (publishedPorts + background). openchamber is therefore sbx-only for now; msb/ppp support is deferred until acq defines a neutral port-publish + background vocab (noted in the README parity table + kits.yaml, tracked as a follow-up on #223). - Add openchamber to the kits.yaml registry (backends: [sbx] + parity prose). - Rewrite README as a hybrid/v1 kit with a backend-parity section; retarget TROUBLESHOOTING and scripts/verify to acq (verify now runs the neutral validate-kits.py gate and only attempts the live sbx run under RUN_SBX=1, since the neutral spec needs acq's kit-translate for the sbx port hint). - Update the openchamber docs/decisions for the new home; drop the interim "Still here: openchamber" note from sbx-kits/README.md. Verification (offline gate; live per-backend run deferred, no nested sandboxes here — same posture as ADR-0001): - validate-kits.py --strict: all 5 kits OK (openchamber included) - scripts/validate_repo.py: all validators passed - scripts/scan_unsafe_shell.py: 0 errors - markdownlint-cli2 (integrations/isolation/**/*.md): 0 errors - sh -n openchamber-start.sh; bash -n scripts/verify: clean - scripts/verify (offline default): 2/2 passed, live checks skipped Co-authored-by: OpenCode Agent <[email protected]> * fix(acq-kits): drop bash-only $RANDOM from openchamber password fallback openchamber-start.sh is #!/bin/sh but the last-ditch password fallback used $RANDOM, a bash-ism undefined in POSIX sh (shellcheck SC3028). The branch is effectively unreachable (only if both openssl and /dev/urandom fail), but now that the script is standalone and lintable, keep it sh-portable: replace $RANDOM with a pid + nanosecond-timestamp + awk-PRNG mix. Shebang stays #!/bin/sh (the neutral kits run under sh/dash). Flagged in the #224 review. Co-authored-by: OpenCode Agent <[email protected]> --------- Co-authored-by: OpenCode Agent <[email protected]>
* docs(acq): relabel phases and add Phase 2 handoff Relabel the acq-design.md implementation appendix so the primary axis is a phase number (Phase 1-4) with the release version kept as a secondary column, and update inline references accordingly. No deviation substance changed. Add docs/explorations/acq-handoff-2.0.md: a two-part Phase 2 handoff (patterns repo first for the neutral hybrid/v1 kits, then quickstart for the msb backend and sbx conversion) with a hard SHA gate between the parts, known-drift callouts, and out-of-scope fences (no ppp/Phase 3, no qsbx removal/Phase 4). AI-assisted. Co-authored-by: OpenCode Agent <[email protected]> * feat(acq): add msb backend and neutral hybrid/v1 kit translation Add a second isolation backend (msb, microsandbox) to acq and introduce the neutral hybrid/v1 kit vocabulary so sbx and msb share one kit set. Part B of the Phase 2 handoff (docs/explorations/acq-handoff-2.0.md). - acq.backends/kit-translate.sh: shared neutral-spec layer (awk parser, no yq). Fetches kit refs (remote git+https#ref=&dir= via sparse checkout, or local dir), dispatches backend_shortcuts.<backend>, synthesizes an sbx-v2 kit dir for the sbx path, and provides kit_validate. Multi-line command bodies are base64-framed so literal block scalars survive the shell pipeline. - acq.backends/msb.sh: full microsandbox adapter (ADR-0010 contract). Drives neutral ops directly: caps.network.allow -> --net-rule, files[] -> msb copy, commands[] -> msb exec (install marker-gated, tested+written as uid 0), zscaler shortcut -> --trust-host-cas, USAi key -> --secret ENV@HOST. Flags verified against msb 0.6.6. - common.sh: repoint PATTERNS_KIT_REF/DIR to the neutral acq-kits/ tree; source kit-translate; _auto_detect_backend gains msb (sbx preferred); real msb probe in acq doctor. - sbx.sh: route kit application through kit-translate (synthesize local sbx-v2 kits before sbx --kit / sbx kit add). ACQ_SBX_KIT_PASSTHROUGH offline hatch. Observable behavior for sbx users is unchanged. - acq: real msb row in `backend list`; new `kit list|validate|apply`. - scripts/verify-backends: per-installed-backend live E2E check (new). - scripts/test-acq: msb + acq kit + neutral->sbx-v2 translation cases (79/79). - docs: BACKEND_GUIDE msb flipped to shipped; QUICKSTART backend-chooser + msb flow; new ADR-0011; acq-design Phase 2 appendix; repoint 3 dangling acq-handoff-1.1.md refs to ADR-0010. PATTERNS_KIT_REF is a PROVISIONAL pre-merge pin at the patterns #221 PR head; it carries a TODO and MUST be flipped to the Part A merge SHA before undrafting (handoff §4.1 hard gate). Live `acq run --backend msb` E2E is deferred to a sandbox-capable host (no nested sandboxes; needs /dev/kvm) — see ADR-0011. qsbx and its migration tests are untouched (removal is Phase 4). Refs: docs/explorations/acq-handoff-2.0.md Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): correct sbx install-cmd form and msb net-rule target grammar Two provision-blocking bugs surfaced by the first live host run of scripts/verify-backends (both were latent — the offline harness never exercised a real sbx/msb create): - sbx: the neutral->sbx-v2 synthesizer emitted every command as an argv sequence, but sbx's `commands.install[].command` field is typed as a shell STRING (startup/initFiles accept a seq). git-ssh-sign (install phase) failed with "invalid spec.yaml: yaml: cannot unmarshal !!seq into string". kit_translate now emits a `sh -c SCRIPT` command as a YAML block-scalar string (matching the pre-Phase-2 sbx kits) and keeps non-shell argv (e.g. node ...) as a sequence. - msb: net rules used `allow@domain:HOST`, but in msb's net-rule grammar the `:` after the target is the PROTO separator, so msb read the target as the single label "domain" and aborted ("`domain` is ambiguous ... Use `domain=domain`"). Use the explicit literal-hostname form `allow@domain=HOST`. Also harden the msb install-marker idempotency: the run-once marker under /var/lib/acq is root-owned, so the existence TEST now runs as uid 0 too (it was already WRITTEN as uid 0). Previously a non-root install phase could fail to see the marker and re-run every apply. The four pinned kits install as uid 0, so behavior there is unchanged. Tests: scripts/test-acq gains regression coverage for the install-string vs argv-seq shape and the msb `domain=` net-rule form (85/85, offline). Docs/ADR net-rule examples updated to `domain=HOST`. Co-authored-by: OpenCode Agent <[email protected]> * feat(acq): add debug tracing and surface provision errors in verify-backends verify-backends swallowed the real create error behind `>/dev/null 2>&1`, so a failing run only said "provision failed" and forced a separate manual re-run to see why. Make one run self-diagnosing: - verify-backends: run every acq command through a run_acq helper that captures combined stdout+stderr; on ANY step failure print the exact command line and the captured output (dump). Add per-backend preflight (sbx version / msb --version + msb doctor readiness note), print the derived sandbox name, and add -v/--verbose (stream commands live) and -k/--keep (leave a failing sandbox up for inspection). - common.sh: add acq_debug (ACQ_DEBUG=1 -> "acq[debug]: ..." on stderr; never prints secret values, only command shapes). - sbx.sh / msb.sh: trace the create command line via acq_debug; msb create failure now also prints the flags + image and hints ACQ_DEBUG=1. - kit-translate.sh: capture and surface git fetch/checkout stderr on failure (was `-q`/discarded), and acq_debug the fetch target — the most likely provision blocker given the provisional pre-merge patterns pin. - BACKEND_GUIDE: add a Troubleshooting section documenting ACQ_DEBUG and the verify-backends flags. No behavior change to the happy path. Offline suites unchanged: test-acq 85/85, test-migrate-or-halt 57/57, lint:md clean. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): per-phase sbx command typing, pullable msb base image + prereq bootstrap Two more provision blockers from the live host run: - sbx: commands.install[].command is typed as a shell STRING but commands.startup[]/initFiles[] are typed as an argv SEQUENCE. The previous fix keyed off the `sh -c` shape (not the phase), so it emitted the playbook/zscaler STARTUP `sh -c` commands as strings too, tripping the mirror-image error "cannot unmarshal !!str `PLAYBOO...` into []string". kit_translate now keys off PHASE: install -> block string, startup/initFiles -> argv seq. - msb: the default image (ghcr.io/gsa-tts/.../opencode:latest) required registry auth and 401'd. Default to a public docker.io/library/debian:stable-slim (pullable without auth). Since a plain base lacks the kits' runtime tools, add _acq_msb_bootstrap_base: a marker-gated, apt/apk-auto-detecting install of node/git/curl/ca-certificates run before kits are applied. Override with ACQ_MSB_IMAGE (+ ACQ_MSB_SKIP_BOOTSTRAP when the image already bakes them in). msb create failures now also hint about registry auth for ghcr/private images. Tests: test-acq gains per-phase typing regressions (install=string, sh -c startup=seq) — 87/87 offline. BACKEND_GUIDE documents the base-image contract + ACQ_MSB_IMAGE/ACQ_MSB_SKIP_BOOTSTRAP; ADR-0011 records both the per-phase typing rule and the base-image/bootstrap decision. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): ship msb base image with kit prerequisites instead of runtime install The live run showed the runtime apt-install path cannot work: the kit net-rules lock egress to the kits' own hosts (api.gsa.usai.gov, github.com, codeload.github.com), so the Debian package mirror (deb.debian.org) is unreachable during provision — apt failed and node/git/curl never installed, so three msb checks failed. Fix: bake the prerequisites into the base image instead of installing at runtime. - Default ACQ_MSB_IMAGE -> docker.io/library/node:22-bookworm, which is built on buildpack-deps:bookworm-scm and already ships node, git, curl, and ca-certificates; pullable from Docker Hub without auth. - Replace the apt/apk bootstrap with a prerequisite PRESENCE CHECK (_acq_msb_check_prereqs): verify node/git/curl/update-ca-certificates exist in the guest and warn clearly (with the missing set) if a custom ACQ_MSB_IMAGE lacks them. No runtime install — nothing to fetch through the locked egress. Skip with ACQ_MSB_SKIP_PREREQ_CHECK=1. sbx path is unaffected and now passes end-to-end on the host (all four checks). Tests: test-acq adds prereq-check coverage (warns on missing, silent when present, honors skip) — 90/90 offline. BACKEND_GUIDE + ADR-0011 updated for the node:22-bookworm default, the no-runtime-install rationale (locked egress), and the ACQ_MSB_IMAGE prerequisite contract. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): msb exec-ready wait, bare-FQDN net rules, backend-aware USAi check Three issues from the live msb run (sbx passes end-to-end): - Race: `msb create` boots the guest in the background, but the adapter copied kit files / ran startup commands immediately — so the usai startup `node merge-global-config.mjs` ran before the .mjs copy was durable (MODULE_NOT_FOUND). Add _acq_msb_wait_for_exec_ready (poll `msb exec echo ok` up to ACQ_MSB_EXEC_READY_TIMEOUT, default 60s) after create and BEFORE applying kits — mirrors sbx.sh's readiness wait. - Net rules: emit the BARE FQDN target `allow@HOST` (msb's own --help example is `[email protected]:tcp:443`) instead of `allow@domain=HOST`. The `domain=` form was a wrong guess that can break DNS resolution of the allowed host (the live run showed "Could not resolve api.gsa.usai.gov" despite the allow). NOTE: not live-verified here (no /dev/kvm); matches the documented grammar. - USAi check (verify-backends): make it backend-aware. sbx holds the real key in the guest (proxy injection) so a bearer curl returns 200. msb binds the key with `--secret ENV@HOST` — the guest value is a proxy-managed SENTINEL and the real key never enters the VM — so a guest-side bearer check can't prove 200. For msb, assert (a) the secret is bound and (b) the models host is reachable (any HTTP status back = resolution+egress OK; auth is rewritten on the wire). Tests: test-acq net-rule cases updated to the bare-FQDN form (91/91 offline). ADR-0011 net-rule examples updated. The bare-FQDN egress fix and the full msb create->kit->exec loop remain to be confirmed on a sandbox-capable host. Co-authored-by: OpenCode Agent <[email protected]> * feat(acq): acq-owned backend-neutral secret store; verified msb file copy De-sbx-ify credentials (the design's §7.5 unified secret model, as a thin bash subset) and fix the msb file-copy race — the last two things blocking msb end-to-end. Secret store (new acq.backends/secret-store.sh): - One acq-owned store keyed acq.<service> / acq.<sandbox>.<service> with sandbox-scope precedence (lifts the Phase-1 sbx global/sandbox scope into acq). - OS keychain when available (macOS `security`, Linux `secret-tool`), 0600 file fallback under $XDG_DATA_HOME/acq/secrets. Value read from TTY/stdin, never argv; never serialized. - `acq secret set` (both backends) now WRITES here, then feeds the active backend: sbx pipes into `sbx secret set`/`set-custom`; msb reads it back at provision and binds `--secret ENV@HOST` from a transient env var. USAi -> api.gsa.usai.gov; GitHub -> github.com + api.github.com. The real value never enters the guest and never appears in argv. - Replaces the msb "export USAI_API_KEY then --secret" stopgap and the sbx-store-only USAi path. Both USAi and the GitHub token now flow through the same acq store. msb file-copy race (the .mjs MODULE_NOT_FOUND): - `msb copy` could return before a subsequent `msb exec` observed the write, so the usai startup `node merge-global-config.mjs` ran before the .mjs was present. Add _acq_msb_copy_file_verified: copy, then poll `test -s` in-guest (ACQ_MSB_COPY_SETTLE_TIMEOUT, default 20s) with one explicit re-copy, and fail the kit apply loudly if the file never appears — instead of running commands against a missing file. Tests (test-acq 104/104, offline): - secret-store unit tests (scope precedence, has/resolve, 0600 perms); - refactored `acq secret set` feed tests (built-in vs set-custom, value never in argv, value stored in acq store); - msb secret set (usai/github) stores in acq store; - msb provision binds USAi + GitHub via --secret and never leaks values to argv. make_stubs points ACQ_SECRET_STORE_DIR at a throwaway file store (no real keychain touched). Docs: BACKEND_GUIDE Secrets section rewritten for the acq store + msb/sbx feed + sbx->acq migration note; ADR-0011 and acq-design appendix record the store as the bash subset of §7.5 (full Go/keychain/MITM component still future). verify-backends: USAi check now looks for the key in the acq store (or host env fallback) rather than expecting a guest-visible value. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): align sbx secret feed with the real sbx CLI contract The first host run of the new secret store exposed two wrong assumptions about how sbx ingests secret values (I had guessed stdin+--force for both forms): - Built-in services (github, ...): `sbx secret set <service>` reads the value on STDIN and has NO stdin --force. If the secret already exists it PROMPTS "Overwrite? (y/N)", which consumed our piped token and cancelled. Fix: pre-check `sbx secret ls`; if present, stop with an exact `sbx secret rm …` hint (non-destructive, per decision) instead of piping into the prompt. Otherwise pipe the value to `sbx secret set` as before. - Custom endpoints (usai): `sbx secret set-custom` does NOT read stdin — the value only arrives via --value/--token (argv-visible) and it errors on "already exists" (no --force). It emitted its own "Enter secret:" prompt and then failed. Fix: never put the value on argv. From a terminal, run `set-custom` interactively so sbx prompts once; when piped/non-interactive (or on msb), store in the acq store and print the exact `sbx secret set-custom …` command. Repeat --host for every mapped host. In all paths the value is already saved in the acq store (source of truth); the sbx step only configures sbx's injector, and the real value never lands on argv. Also verified the sbx CLI shapes from `sbx secret {set,set-custom} --help` (0.35.x): set-custom flags are --host (repeatable) / --env / --value / --token / --placeholder; set has --force only with --token. Tests: rewired §5 to the real contract (built-in stdin feed + existing->rm-hint; custom piped->store+print-command with value-never-in-argv asserted on both stdout and the sbx call log; msb usai stores + no sbx set-custom). test-acq 106/106. BACKEND_GUIDE + ADR-0011 document the per-type feed + non-destructive overwrite behavior. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): msb creates the agent/uid-1000 + /home/agent user the kits require Root cause of the persistent msb MODULE_NOT_FOUND + playbook-clone failures, confirmed by a host probe: it was NOT a copy race (msb exec/copy persistence and paths are correct). The kits assume the sbx agent-template contract — an `agent` user, HOME=/home/agent, addressed as `-u 1000`. A plain OCI base doesn't provide it: node:22-bookworm ships a `node` user at uid 1000 (HOME=/home/node) and no `agent`. So the usai startup `node /home/agent/usai-config/merge-global-config.mjs` and the playbook clone ran as the wrong user against a home that isn't /home/agent (the error even showed file:///home/agent/... failing under node). Fix in acq.backends/msb.sh: - _acq_msb_ensure_agent_user: at provision (after exec-ready, before applying kits), create an `agent` user with HOME=/home/agent if missing — idempotent, marker-gated, offline (useradd on Debian/RHEL, adduser on Alpine). Prefers uid 1000 but falls back to a free uid if taken (kits address the user by name). - _acq_msb_exec_command: map a kit `user: "1000"` (or "agent") to `-u agent -e HOME=/home/agent` rather than literal `-u 1000` (which is the base image's `node`). Root/other users unchanged. - _acq_msb_copy_file_verified: chown staged files under /home/agent to `agent` so the agent user can read/execute them (they are copied as root). Also verified msb create-time knobs exist (--user/--workdir/--env/--copy-file) and recorded that exec/copy persistence is fine (the earlier "copy race" note is corrected in the design appendix + ADR-0011). Tests: test-acq gains an msb provision case asserting the agent user is created, a uid-1000 kit command runs as `agent` with HOME=/home/agent (NOT -u 1000), and staged /home/agent files are chowned to agent. 110/110 offline; lint clean. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): msb kit-apply stdin bug, DNS nameserver, workspace mount, fail-closed Live msb bring-up (KVM host) exposed four real bugs; a host probe confirmed each root cause (msb exec/copy persistence and paths are fine — earlier "copy race" theory was wrong): - Kit apply dropped every file/command after the first. The files and commands apply loops used `while read … done <<heredoc` whose body calls `msb copy`/ `msb exec`; a real `msb` consumes the loop's stdin, so iteration stopped after record #1. This was the actual cause of the missing merge-global-config.mjs (usai's 2nd file) and the MODULE_NOT_FOUND. Fix: buffer the parser output into an array first, then iterate. - Guest DNS failed for allow-listed hosts. msb hands the guest the host's resolvers, but a corporate/VPN resolver (172.16.x/Zscaler) is unreachable from the microVM, so nothing resolved ("Could not resolve api.gsa.usai.gov"). Pass --dns-nameserver (ACQ_MSB_DNS_NAMESERVER, default 1.1.1.1; empty to omit). Verified: models API -> 401 (no key), github -> 200. - Workspace mount. msb won't create the host mount path and mishandles an identical host:guest /tmp mount (starts but the mount silently doesn't appear). Mount the host workspace at a fixed guest path (ACQ_MSB_WORKSPACE, default /home/agent/workspace) and error if the host path doesn't exist. - False success on failed start. `msb create` returns 0 even when the guest later fails to START (async boot) — so bad mounts looked like passes. Treat a sandbox that isn't exec-ready within ACQ_MSB_EXEC_READY_TIMEOUT as a HARD provision failure, with `msb logs --source system` guidance. Also creates the kits' `agent`/HOME=/home/agent user contract (prior commit) and now proven end-to-end alongside these fixes. Tests: the msb stub now DRAINS STDIN like the real CLI (so the harness catches the loop-stdin bug), plus regressions for full multi-file/multi-command apply, --dns-nameserver default/override/disable, not-ready-aborts, and missing-workspace-errors. test-acq 123/123; test-migrate-or-halt 57/57; lint clean. Docs (BACKEND_GUIDE, ADR-0011, acq-design appendix) updated. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): msb enable TLS interception for secrets; defer github git-auth (#203) Finalize the msb secret path against how microsandbox actually works (docs + 0.6.6 CLI behavior, verified on a KVM host), and stop chasing the private playbook clone. - Enable --tls-intercept on msb create (ACQ_MSB_NO_TLS_INTERCEPT to opt out). microsandbox only substitutes a secret placeholder over intercepted TLS; the interception CA is auto-trusted in the guest (plain HTTPS = 200), so no extra CA install. The USAi Authorization-header substitution needs this. - Bind ONLY [email protected]. Drop the GitHub --secret binding: msb 0.6.6 does not substitute the placeholder for git's HTTPS clone to github.com (works for the USAi header, not git — verified across request shapes, single/multi binding, header vs URL, h1 vs h2), and binding the same placeholder on multiple github hosts also trips microsandbox #1170. The private playbook clone is therefore skipped on msb (kit is non-fatal, warns); sbx is unaffected. Tracked in #203 (+ upstream #756/#768/#1170). - acq secret set now re-feeds running sandboxes via `msb modify --secret` (usai), so a rotated key applies without recreate (value via env, never argv). - Harden _acq_msb_net_rules_into: validate each kit-spec host against a hostname/wildcard allowlist before the eval-by-name append, so a malformed or malicious spec value can't inject shell metacharacters. Docs: BACKEND_GUIDE gains an msb "Known limitations" section (private-repo clone) + the TLS-interception/msb-modify secret details; ADR-0011 and the acq-design appendix record the TLS-interception requirement, the git-substitution gap, and the live-verification results. Tests: --tls-intercept asserted, USAi single-host binding asserted, github NOT bound asserted, value-never-in-argv retained. test-acq 124/124; test-migrate-or-halt 57/57; lint clean. Refs: #203 Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): verify-backends WARNs (not FAILs) on the known msb playbook gap The msb private-repo playbook clone is a known, tracked limitation (#203), not a regression, so it shouldn't fail an otherwise-green msb run. Add a warn() level: on msb a missing playbook clone/symlink is a WARN referencing #203 (surfaced loudly every run, does NOT count toward failures); on sbx it stays a hard requirement. The summary line now reports warnings and the run still exits 0 when the only issue is a warned limitation. Refs: #203 Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): clear shellcheck warnings failing the pre-commit CI check The pre-commit shellcheck hook (--severity=warning) failed PR #202 on pre-existing/new SC warnings: - common.sh: ACQ_KIT_NAMES flagged SC2034 (unused) — it's consumed by `acq kit list` in the entry point; add a scoped `# shellcheck disable=SC2034`. - kit-translate.sh kit_validate: drop the unused `m`/`ph` locals (SC2034); only path + source are validated. - test-acq / verify-backends: annotate the dynamic `. "$ACQ"` / adapter sources with `# shellcheck disable=SC1090` (+ `source=` directives) like load_acq already does; use the `desc` arg in run_acq's verbose line so it isn't unused (SC2034). Verified with shellcheck 0.11.0 (the CI-pinned version) --severity=warning across every tracked shell file (by extension + shebang): 0 findings. Offline suites unchanged: test-acq 124/124, test-migrate-or-halt 57/57, markdownlint clean. Co-authored-by: OpenCode Agent <[email protected]> * feat(acq): pin PATTERNS_KIT_REF to patterns v1.6.0 (Part A merged) — release gate met Part A (patterns PR #221: neutral hybrid/v1 acq-kits + kit-hybrid-v1 schema + registry) merged to agentic-coding-patterns main and shipped in the v1.6.0 release. Flip PATTERNS_KIT_REF from the provisional pre-merge PR-head SHA to the v1.6.0 release commit e387c59bb2f743eb321bfb56a8ac71a6abb185ae, satisfying the Phase 2 handoff §4.1 hard gate. Pinning to a release tag mirrors Phase 1's v1.5.0 pin; v1.6.0 includes Part A unchanged (it also adds an unrelated sbx kit + a validation script — the acq-kits and schema are identical to the #221 merge). Verified against the pinned commit: live sparse-fetch of all four acq-kits, and neutral->sbx-v2 translation + kit_validate of each, all pass. - common.sh: PATTERNS_KIT_REF -> e387c59…; drop the provisional-pin TODO. - test-acq: ref assertions -> e387c59. - ADR-0011 + acq-design appendix: release gate marked satisfied. test-acq 124/124; test-migrate-or-halt 57/57; markdownlint clean; shellcheck 0.11.0 (--severity=warning) clean. Refs: GSA-TTS/agentic-coding-patterns#221 Co-authored-by: OpenCode Agent <[email protected]> * docs(acq): point ppp backend at GSA-TTS/ppp (Phase 3, in development) ppp is no longer "deferred/not scheduled" — it is in active development at GSA-TTS/ppp for Phase 3. Update the backend table, the ppp section, the deferred list, and ADR-0011 to link the repo and note it will be additive (same adapter contract + neutral kits). Also tidy the deferred list to name the Go/keychain §7.5 component and quickstart#203 explicitly. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq): validate untrusted kit fields (root chmod injection); TLS-scope + create-scrub Address adversarial review of PR #202. FAIL (medium) — root command injection via unvalidated kit `mode`. A kit spec's files[].mode was interpolated unquoted into a root `sh -c "chmod $mode '$path'"`, so a hostile/typo'd spec (patterns repo / ACQ_EXTRA_KITS) with `mode: "0644; wget|sh"` ran arbitrary code as root in the guest. Fixes: - kit_spec_files now DROPS any record whose mode isn't octal ([0-7]{3,4}) or whose path/source contains shell metacharacters/whitespace (warns to stderr). - kit_spec_commands DROPS any command whose user isn't a bare uid/safe username or whose phase isn't install|initFiles|startup (both reach `msb exec -u`). - msb _acq_msb_copy_file_verified re-validates path up front and mode at use, and runs `chmod`/`chown` as argv (msb exec -u 0 -- chmod MODE PATH) instead of an `sh -c` string — no interpolation into a shell. - kit_validate scans the RAW spec so a bad mode/phase/user is REPORTED (not just silently dropped) and makes `acq kit validate` fail. Command argv was already base64-tokenized + passed as separate argv (not a joined sh -c), and net-allow hosts were already charset-validated; this closes the mode/path/user gap. The mode/user validation also belongs in patterns #221's validate-kits.py at the gate (noted for that PR); acq validates defensively regardless. CONCERN (medium) — document that --tls-intercept is guest-global. ADR-0011 now states interception covers all guest egress (guest auto-trusts the interception CA), while secret substitution stays host-scoped — a broader posture than sbx's proxy injection, and the price of the microVM boundary. Added SI-10 to controls. CONCERN (low) — set -e skipped the create-failure secret scrub. Under `set -euo pipefail` a failing `msb create` aborted before the rc capture, so the error block AND the transient-secret unset never ran. Use `|| _create_rc=$?`. Nit — secret store: get now falls back to the file backend under ACQ_SECRET_ALLOW_ARGV so a value stored without the flag is still found. Doc nits: ADR date 2026-07-16, "135 checks" (was 79). Tests: +5 security cases (hostile mode dropped, msb chmod argv/refusal, hostile command user dropped, `acq kit validate` reports mode/phase). test-acq 135/135; test-migrate-or-halt 57/57; markdownlint clean; shellcheck 0.11.0 clean. Refs: #202, #203, GSA-TTS/agentic-coding-patterns#221 Co-authored-by: OpenCode Agent <[email protected]> * feat(acq): add environment vocabulary to hybrid/v1 kit translation hybrid/v1 had no way to express guest environment variables; the translate layer implemented only caps.network.allow, files[], commands[], and agentContext. A downstream team (quickstart#202 review, comment 5004158773) needs environment.variables-style config (OPENCODE_CONFIG, OPENCODE_TUI_CONFIG, GITLAB_HOST) as a first-class kit mechanism. Add env-var support across the translate layer: - kit-translate.sh: new kit_spec_env accessor (awk, mirrors kit_spec_net_allow) that VALIDATES env var names (^[A-Za-z_][A-Za-z0-9_]*$) and drops unsafe ones with a stderr warning, consistent with the existing mode/path/user hardening. - kit_translate_to_sbx: emit an sbx-v2 environment.variables block (the native sbx guest-env mechanism the pre-Phase-2 playbook/openchamber kits used). - msb.sh: thread each NAME=value onto the kit's lifecycle commands as 'msb exec -e NAME=value' (msb's native per-exec env flag). - kit_validate: report an invalid env var name (not silently drop it). - test-acq: env parsed; unsafe name dropped+warned; sbx-v2 emission; msb --env emission; validate reports bad name (135 -> 148 tests, all offline). Static string values only (YAGNI); secrets stay on the credential path, never the kit spec. Docs (ADR-0011, BACKEND_GUIDE) updated. The authoritative schema + validator land in a separate patterns PR; PATTERNS_KIT_REF is deliberately LEFT at e387c59 (v1.6.0) and bumped only after that PR merges (fail-closed cross-repo gating). Refs: GSA-TTS/agentic-coding-patterns#227 Co-authored-by: OpenCode Agent <[email protected]> * feat(acq): bump PATTERNS_KIT_REF to patterns v1.7.0 (environment vocabulary) Patterns v1.7.0 is released (commit 9c277c09ed4ad45fd11709d6b048a58adc785443), shipping the hybrid/v1 `environment` schema property + field-level validator (#227) and its shell-safety doc note (#228), plus the openchamber conversion (#224). Flip PATTERNS_KIT_REF from v1.6.0 (e387c59) to the v1.7.0 release commit so the acq translate layer's env-var support (kit_spec_env) is backed by a released schema. Verified the schema at v1.7.0 has the `environment` property and that all five acq-kits fetch + translate + validate + parse env against it. - common.sh: PATTERNS_KIT_REF -> 9c277c0 (v1.7.0); comment updated. - test-acq: ref assertions -> 9c277c09. - ADR-0011 + BACKEND_GUIDE + acq-design: cross-repo env-schema dependency marked satisfied (was pending); release-gate note points at v1.7.0. test-acq 148/148; test-migrate-or-halt 57/57; markdownlint clean; shellcheck 0.11.0 clean. Refs: GSA-TTS/agentic-coding-patterns#227, #228, #202 Co-authored-by: OpenCode Agent <[email protected]> --------- Co-authored-by: OpenCode Agent <[email protected]>
…t step (#233 Increment A) quickstart#221 is merged: acq's neutral→sbx translator now carries backend_extras.sbx.publishedPorts, so applying the openchamber kit publishes container ports 3000/4096 to the host at create time with no manual `acq ports --publish`. Adopt that here (Increment A of #233): - scripts/verify step 2b: instead of always publishing manually, VERIFY the ports were auto-published at create; only fall back to an explicit publish on an older acq that predates #221 (emitted as a non-fatal WARN, not a FAIL). Add a warn() helper for that non-fatal path. - kits.yaml + README: replace the "translator does not yet carry publishedPorts; publish manually" wording with "carried at create (quickstart#221); older acq falls back" and reframe the remaining gap as msb port/background plumbing. Increment B (msb/ppp parity) stays blocked and #233 stays open: acq still has no neutral port-publish/background vocabulary and the msb adapter consumes neither backend_extras.msb ports nor a background flag. No behavior change to the wrapper or startup script. Co-authored-by: OpenCode Agent <[email protected]>
* feat(acq-kits): openchamber wrapper owns a shared opencode server Redesign the openchamber kit so the browser UI and an optional terminal TUI share one live OpenCode session by default, and simplify what the kit's startup manages. - Ship an `opencode` wrapper at ~/.local/bin/opencode (first on the base-image PATH, so it shadows the real binary and is what the sandbox entrypoint runs). With no args it idempotently starts a shared `opencode serve` on 0.0.0.0:4096, prints host-connect instructions, and offers to attach a TUI; with args it passes through to the real opencode. - Reduce the startup script to managing OpenChamber only (install on first boot + respawn supervisor in skip-start mode); it no longer starts or supervises `opencode serve` — the wrapper owns that, on demand. - Publish both container ports 3000 (OpenChamber) and 4096 (shared server); run the shared server unsecured (the sandbox is the security boundary). - Rework scripts/verify: three-state offline gate (SKIP when python deps missing, real error on failure), a preferred RUN_ACQ live path (creates via acq, publishes 3000/4096, exercises the wrapper + on-demand server), and a gated RUN_SBX path that requires a pre-translated sbx-v2 kit. - Drop the "*.npmjs.org" allow host: registry.npmjs.org serves metadata and tarballs, and a leading-* host breaks acq's (unquoted) neutral->sbx translation. Documented in-spec. - Add docs/decisions/wrapper-entrypoint-owns-server.md and annotate the superseded premise of openchamber-mixin-not-sandbox-kit.md. Update README, TROUBLESHOOTING, and kits.yaml. Ports need a manual `acq ports --publish` under acq until the upstream publishedPorts translation (quickstart#221) reaches the pinned kit ref; tracked as #233. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq-kits): keep openchamber sandbox alive (wrapper foregrounds server) The openchamber `opencode` wrapper is the sandbox entrypoint (it shadows the real binary via PATH). Its no-arg path backgrounded `opencode serve` with `nohup … &` and then returned (`exit 0`), so PID 1 exited and the sandbox transitioned running -> stopped moments after `acq run` — visible as a sandbox that "stops on its own" when you decline the TUI prompt or run non-interactively. Introduced in this branch's wrapper redesign. A Docker sandbox is "running" only while its entrypoint / PID 1 is alive. Fix the wrapper's no-TUI path to hold a foreground process as PID 1: - if it started the server here, `wait` on that background job; - if a server already owns :4096, park on a liveness loop and only `exec` a fresh foreground server if the existing one disappears (avoids a double-bind that, under `set -e`, would itself kill the entrypoint). The interactive "yes -> opencode attach" path already `exec`ed, so it was unaffected. - files/.local/bin/opencode: foreground handoff + "must stay PID 1" note. - spec.yaml, README, docs/decisions/wrapper-entrypoint-owns-server.md: describe the foreground handoff and record the regression it prevents. - TROUBLESHOOTING.md: add "sandbox stops on its own right after acq run"; update the `acq exec … opencode` recipes to start the server detached (the wrapper now foregrounds). - scripts/verify: add step 9 entrypoint-liveness regression guard (assert the no-arg wrapper blocks rather than returning); start the wrapper detached in step 5 so the live check doesn't hang. Verified: host-side `RUN_ACQ=1 scripts/verify` passes all checks (including the new liveness guard) on a sandbox-capable host. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq-kits): keep openchamber sandbox alive after the attached TUI is quit The prior fix kept the sandbox running on the declined/non-interactive path, but the "yes, attach a TUI" path still `exec`ed `opencode attach`, making the TUI itself PID 1. Quitting the TUI then ended the entrypoint and stopped the sandbox (and the OpenChamber browser UI with it) — contradicting the kit's premise that the browser and TUI share one long-lived server. Run the attached TUI as a CHILD (not exec) with `|| true`, so quitting it returns to the wrapper. Extract the PID-1-hold into a hold_pid1() function that every no-arg path ends on (decline, non-interactive, and attach-then-quit), so the sandbox and the shared server survive a TUI exit. - files/.local/bin/opencode: TUI as child + hold_pid1() shared tail. - spec.yaml, README, docs/decisions/wrapper-entrypoint-owns-server.md: document TUI-exit survival; ADR records both fixed variants (return, exec). - TROUBLESHOOTING.md: add "sandbox stops when I quit the attached TUI". - scripts/verify: add step 10 guard asserting the attach line is not exec'd and the wrapper ends in a hold_pid1 call. Verified: host-side `RUN_ACQ=1 scripts/verify` passes all checks, including the new step 10 TUI-exit-survival guard. Co-authored-by: OpenCode Agent <[email protected]> * docs(acq-kits): fix MD028 blank-line-in-blockquote in openchamber README markdownlint MD028/no-blanks-blockquote: a bare blank line separated two adjacent blockquotes in the openchamber README. Join them with a `>`-only line so they read as one blockquote and the Markdown Lint CI check passes. Co-authored-by: OpenCode Agent <[email protected]> * docs(acq-kits): use acq (not sbx) for user-facing commands in openchamber docs The openchamber README/TROUBLESHOOTING are the acq-facing docs, but a few runnable examples still invoked the raw `sbx` backend CLI. Since acq passes unintercepted commands and extra args straight through to the backend, the acq form is equivalent and consistent: - `sbx ports … [--publish …]` -> `acq ports …` - `sbx stop … && sbx run …` -> `acq stop … && acq run …` - `sbx policy log …` -> `acq policy log …` (passthrough) - drop the parenthetical "(sbx ports …)" hints next to `acq ports`. Left unchanged (correctly): references to the `sbx` *backend*, the sbx-v2 *schema*, `backend_extras.sbx.*` spec fields, the historical `sbx-kits/` path, and the RUN_SBX advanced/legacy path — these name the backend or internals, not commands a user runs. Co-authored-by: OpenCode Agent <[email protected]> * chore(deps): clear linter advisories (js-yaml 4.3.0, linkify-it 5.0.2) Markdown Lint CI gates on npm audit --audit-level=moderate; this branch predated the fix and was red on js-yaml GHSA-52cp-r559-cp3m + linkify-it GHSA-v245-v573-v5vm. Bump the linter overrides (js-yaml 4.2.0->4.3.0, +linkify-it 5.0.2) and regenerate the lockfile. 0 vulnerabilities at moderate. Co-authored-by: OpenCode Agent <[email protected]> * fix(acq-kits): harden openchamber wrapper failure path per #234 review Address the review must-fixes on the PID-1 hold. All are failure-path hardening, not a redesign. 1. Self-heal the hold (Medium, must-fix). The old `started_here` branch did a bare `wait`; if the server it launched died (crash/OOM/lost race), `wait` returned, hold_pid1 returned, and PID 1 exited -> sandbox stopped — the exact bug this wrapper prevents. Replace the asymmetric two-branch hold with ONE self-healing loop: poll the server and re-launch it if it ever stops answering, so a dead server can never let the loop return. 2. Close the serve-launch TOCTOU (Medium). Bring-up detected only via `curl /health`, so two near-simultaneous no-arg starts both `nohup … serve` and raced the :4096 bind (loser dies). Add an atomic mkdir lock (serve_launch_once) with a PID file and stale-lock reclaim (dead holder -> lock reclaimed), so exactly one wrapper launches. 3. Move the serve log off a predictable /tmp path (Low). Now ~/.local/state/openchamber/opencode-serve.log in a 0700 dir; docs/verify updated to read it there. 4. Security note: state the loopback-only guarantee depends on the adapter publishing loopback-scoped, which is acq's publishedPorts translation (quickstart#221). Also update publishedPorts docs now that quickstart#221 is MERGED: acq carries backend_extras.sbx.publishedPorts, so ports 3000/4096 publish at create time with no manual step (older acq still gets the documented fallback). verify's step 2b now re-publishes idempotently (works on old and new acq). scripts/verify: add step 11 — a live self-heal regression guard that brings the server up, KILLs it, and asserts a server answers again (hold_pid1 re-launched; PID 1 held). Verified: shellcheck-clean parses (sh -n / bash -n); offline simulations of the lock (8 racers -> 1 launch), stale-lock reclaim, and the self-heal loop (server death -> reheal, loop never returns); markdownlint 0 errors. Co-authored-by: OpenCode Agent <[email protected]> * docs(acq-kits): adopt acq publishedPorts translation, drop manual port step (#233 Increment A) quickstart#221 is merged: acq's neutral→sbx translator now carries backend_extras.sbx.publishedPorts, so applying the openchamber kit publishes container ports 3000/4096 to the host at create time with no manual `acq ports --publish`. Adopt that here (Increment A of #233): - scripts/verify step 2b: instead of always publishing manually, VERIFY the ports were auto-published at create; only fall back to an explicit publish on an older acq that predates #221 (emitted as a non-fatal WARN, not a FAIL). Add a warn() helper for that non-fatal path. - kits.yaml + README: replace the "translator does not yet carry publishedPorts; publish manually" wording with "carried at create (quickstart#221); older acq falls back" and reframe the remaining gap as msb port/background plumbing. Increment B (msb/ppp parity) stays blocked and #233 stays open: acq still has no neutral port-publish/background vocabulary and the msb adapter consumes neither backend_extras.msb ports nor a background flag. No behavior change to the wrapper or startup script. Co-authored-by: OpenCode Agent <[email protected]> --------- Co-authored-by: William Zujkowski <[email protected]>
Context
Phase 2 of the
acqpluggable-backend effort (handoff + design docs inagentic-coding-quickstart, tracking PR GSA-TTS/agentic-coding-quickstart#202)adds a second isolation backend (
msb) toacq. To let two backends share onekit vocabulary, it introduces a neutral
schemaVersion: "hybrid/v1"kit spec.This PR is Part A (the patterns-repo half): author the neutral kits, their
JSON schema, and a registry; leave the old
sbx-kits/as a one-release redirect.Part B (the
msbadapter +kit-translate.sh+acq kit …in the quickstartrepo) is gated on the merge SHA this PR produces — do not start Part B's kit-ref
repoint until this merges (handoff §4.1).
Per a reviewer decision, the new neutral home is
integrations/isolation/acq-kits/(not the handoff'skits/) to make itexplicit the kits target
acq— which selects a backend — rather thansbxalone.
Plan (all landed)
schemas/kit-hybrid-v1.schema.json— neutral kit JSON Schemasbx-kits/README.md→acq-kits/acq-kits/usai-provider/(hybrid/v1 + moved node tooling/tests)acq-kits/agentic-coding-playbook/(+ extractedfiles/home/playbook-clone.sh)acq-kits/zscaler-ca-certificate/(+backend_shortcuts.msb.trust_host_cas)acq-kits/git-ssh-sign/(+ vendored LICENSE/NOTICE)acq-kits/kits.yamlregistry +acq-kits/README.md+validate-kits.pyintegrations/isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.mdWhat changed (summary)
hybrid/v1capturescaps.network.allow,files[](contentorsource:+ optionalphase),commands[](
phase/user/command),agentContext, andbackend_shortcuts/backend_extras. A repo validator enforces the cross-field rules JSON Schemacan't (source resolution, known-backend keys, README presence, registry
cross-check).
startup shell was extracted to a tested
playbook-clone.sh; the Zscaler CAmoved from an inline PEM to a
files/payload and gained the msb--trust-host-casshortcut (thebackend_shortcutsshowcase).kits.yaml(name →[sbx, msb]+ parity prose).sbx-kits/README.mdpoints toacq-kits/; old kitcontents left intact (nothing deleted).
name(dropped the-kitsuffix).Verification transcript
Run in the project environment (
python3withjsonschema==4.26.0,node v22):Deferred (documented in the ADR): live
acq runper-backend end-to-endverification cannot run inside an sbx sandbox (no nested sandboxes), same posture
as quickstart ADR-0009/0010. Each kit's
scripts/verifyruns on asandbox-capable host; msb
--trust-host-casand SSH-forwarding parity arecovered by Part B's
scripts/verify-backends.Rollback
Revert the squash-merge commit. The old
sbx-kits/kit contents are retainedunchanged (only the README became a redirect), so anything currently pinning
sbx-kits/…keeps working; reverting restores the original README.Security impact
acq(Part B). No secrets added — credentials remain injected at runtime bythe backend (documented per kit).
verified above; no private key).
caps.network.allowegress lists are carried over verbatim from thereviewed sbx kits (no new hosts).
git-ssh-signretains its Apache-2.0LICENSE/NOTICEand the vendoring +public-source intake records.
AI-assisted (OpenCode Agent, model:
claude_4_8_opus). All commits SSH-signed.