Skip to content

feat(kits)!: adopt pinned agent and capability kits - #474

Merged
mogul merged 37 commits into
mainfrom
refactor/acq-devenv-base-image
Oct 6, 2026
Merged

mogul merged 37 commits into
mainfrom
refactor/acq-devenv-base-image

Conversation

@mogul

@mogul mogul commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Context

AI-assisted implementation for the ADR-0030 migration: move from adapter-owned agent/tool installation toward a generic devenv-enabled base image plus explicit, pinned capability/agent kits from agentic-coding-patterns.

This PR now includes the work that was previously stacked above it:

Discussion issue: #473
Related patterns release: GSA-TTS/agentic-coding-patterns v1.12.0 (56d1f5f49ef928f50e61aa64a93d618b3d21c415)

Closes: #478, #479, #480, #481, #482, #483, #484, #485, #505

#475 remains a separate resource-sizing track and is intentionally not closed by this PR.

BREAKING CHANGE

This PR changes default MSB provisioning behavior.

  1. MSB no longer auto-installs podman/OCI tooling by default.
    Users who need docker run, docker compose, or rootless podman inside MSB sandboxes must opt into the oci-engine kit.

  2. MSB no longer performs native OpenCode npm fallback installation.
    OpenCode is supplied by the pinned opencode agent kit from agentic-coding-patterns v1.12.0. If the kit is unavailable or invalid, acq fails closed instead of silently installing opencode-ai from npm.

  3. Existing MSB sandboxes with adapter-installed podman are not migrated in place.
    Recreate affected sandboxes or add the oci-engine kit before relying on OCI behavior after stop/start.

Migration

For OCI/podman inside MSB sandboxes, choose one of:

acq configure

and select oci-engine, or pass the pinned kit for a one-off sandbox:

acq run opencode . --kit 'git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=56d1f5f49ef928f50e61aa64a93d618b3d21c415&dir=integrations/isolation/acq-kits/oci-engine'

For OpenCode, no separate user action is intended for new sandboxes: the built-in opencode agent kit is selected by acq from the pinned patterns bundle.

Existing sandboxes should be recreated when their provisioned state must match the new defaults.

Summary

  • Add ADR-0030 for agent kits on a devenv-enabled, agent-less base image.
  • Pin PATTERNS_KIT_REF to agentic-coding-patterns v1.12.0 (56d1f5f49ef928f50e61aa64a93d618b3d21c415).
  • Add built-in agent kit metadata/inference and migrate OpenCode install/config/startup knowledge into the pinned opencode kit.
  • Remove the native MSB OpenCode npm fallback path.
  • Split provider facts from agent config rendering and consume provider-facts/usai.env when present.
  • Add the generic opt-in kit catalog path for oci-engine.
  • Remove adapter-owned MSB OCI/podman auto-install, package knobs, short-name-mode plumbing, and unconditional OCI device grants.
  • Add the neutral /home/agent/.rc.d/*.sh shell-snippet contract and bridge it for MSB/SBX.
  • Preserve startup-barrier, grammar-guard, exec-bit self-healing, and live verifier hardening from the stack.
  • Fix SBX USAi presence/rotation paths to honor provider-defined key env names.
  • Improve non-enforcing BYO/devenv doctor diagnostics.

Verification

Latest local verification after rebasing over #548 and collapsing the stack:

  • PASS: bash -n acq acq.backends/*.sh scripts/test-acq-lib.sh scripts/test-acq-bats scripts/fix-exec-bits test/bats/helper.bash scripts/verify-backends
  • PASS: ./scripts/fix-exec-bits --check
  • PASS: git diff --check
  • PASS: npm run lint:md
  • PASS: focused dispatch regression: TMPDIR="$PWD/tmp" ./scripts/test-acq-bats test/bats/30-dispatch-routing.bats (55 ok)
  • PASS: feat(kits)!: make oci-engine an opt-in catalog kit and retire the adapter auto-install #541/chore(kits): stage patterns release pin placeholder #545/refactor(msb): remove native opencode fallback #546 focused suites:
    • test/bats/36-agent-catalog.bats
    • test/bats/50-kit-list-completeness.bats
    • test/bats/55-version.bats
    • test/bats/72-msb-agent-user.bats
    • test/bats/80-kit-subcommands.bats
    • test/bats/85-kit-provenance.bats
    • test/bats/95-kit-ref-persistence.bats
    • test/bats/135-configure.bats
    • test/bats/140-opencode-migration-gate.bats
  • PASS: host live verification previously run on this stack before the final collapse:
    • MSB: 15 passed, 0 failed, 1 skipped
    • SBX: 10 passed, 0 failed, 1 skipped

CI is the source of truth for full offline suite coverage on the collapsed PR.

Rollback

Revert this PR to restore the previous MSB adapter-owned podman auto-install path and native OpenCode npm fallback. Existing sandboxes are not migrated by rollback; recreate affected sandboxes if their provisioned state must match the restored adapter behavior.

Security Impact

Positive security impact with user-visible migration cost:

  • MSB no longer grants OCI-related devices by default for every sandbox.
  • OCI support becomes an explicit opt-in kit selection.
  • OpenCode installation is pinned through the immutable patterns release SHA instead of resolving latest npm package state.
  • Provider facts are parsed as data and fail closed when malformed.
  • The live verifier no longer hides pipe/descriptor stalls or checkout exec-bit drift.

No secrets are introduced. Secret values are not placed on argv, in docs, or in test fixtures.

Review Focus

  • Breaking-change migration text for OCI and OpenCode.
  • MSB legacy sandbox behavior when adapter-installed podman existed before this change.
  • Agent-kit inference and failure mode when the pinned opencode kit is unavailable or malformed.
  • Provider-facts handling and SBX/MSB secret binding with provider-defined key env names.
  • Live verifier changes, especially scripts/verify-backends capture behavior and exec-bit self-healing.

@basiliskus

Copy link
Copy Markdown
Contributor

Support for option 3. The login.gov data team kit already lives in this model: since patterns#395 we consume the generic devenv image and install team tools through kit startup steps, so an agent-less base plus an agent kit is the next step rather than a change of course. Some points from that experience, mostly aimed at the open questions.

Agent install: pin by content, derive the image from the kit. The cost of installing the agent at create is once per sandbox (acq already skips on command -v and a marker), seconds to a minute through the proxy, and noise next to the image pull, the Nix store seed, and a first devenv build. The real risk is losing the pin: today acq's create-time fallback is npm install -g opencode-ai, which resolves to latest, and the only thing pinning OpenCode for us is that the image happens to bake it. On a devenv base the natural install is nix profile install github:NixOS/nixpkgs/<rev>#<agent>: pinned by nixpkgs rev, content-hashed, prebuilt from the Nix cache, identical on both arches, and the same line for every agent nixpkgs carries (the team kit installs fd/glab/jq this way already). Agents outside nixpkgs use the goose-server pattern, a sha256-pinned release binary. I would write that into the ADR as the agent-kit install contract, and make the published agent-populated image an artifact built by applying the kit, so image and kit cannot drift.

Activation: acq should not run it. direnv allow is a per-user trust decision on a content hash, and a first devenv build for identity-devops is a multi-core, tens-of-minutes job with 20G of scratch that we deliberately run under the user's eyes. devenv up is process-compose for services, not shell activation. The contract that has worked: the base image ships nix, devenv, direnv and the shell hook; the workspace owns .envrc; the human approves once; acq's job ends at giving exec and run a login shell in the primary repo.

Create-time vs run-time needs a stated rule, and resources belongs in it. Create-time is anything the backend must know before boot (egress, volumes, ports); run-time is files, env, and startup steps. Missing from the vocabulary today: CPU and memory. acq sizes msb guests at 2 vCPU / 4G from an environment variable only, and a kit cannot say "my builds need 4 / 8G". This bit us this week (terraform from source, OOM territory at 4G, 4x slower than the sbx template). A neutral resources: field, max-merged across kits, is the same two-halves pattern as volumes:.

Supervision: two models are now in flight. This ADR asks whether devenv's process supervision replaces ad hoc loops; #472 runs services outside the sandbox as gateways; goose-server runs an in-guest supervisor via background: true. Worth deciding one story for in-guest services and one for out-of-guest before the vocabulary grows three ways to run a daemon.

usai-provider: split facts from rendering. The provider facts (endpoint, key env var, model catalog) are one thing; the config format is each agent's. One agent-aware kit that emits every agent's format is the coupling this ADR removes from acq, relocated into a kit. Have usai-provider export the facts and each agent kit render them. Related: acq should never merge agent config files; the team kit relies on OpenCode's own project-layer deep-merge for its permission gate, and that semantic belongs to the agent.

Bring-your-own image contract must say where the Nix store lives. The team kit's /nix volume seed assumes a store baked at /nix and shadows it at boot. A contract that only says "has nix, devenv, direnv" breaks that silently.

Egress merge: union only, and visible. "Later wins" is right for env and files and wrong for network. A personal kit widening egress without a trace is the concrete AC-6 case the ADR gestures at.

Team impact if adopted is small: ACQ_IMAGE points at the agent-less image, OpenCode arrives from the built-in agent kit at create, one TROUBLESHOOTING entry changes. Volumes, tools, skills, and injection steps are untouched.

@mogul

mogul commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Great feedback, thank you.

Supervision: two models are now in flight. This ADR asks whether devenv's process supervision replaces ad hoc loops; #472 runs services outside the sandbox as gateways; goose-server runs an in-guest supervisor via background: true. Worth deciding one story for in-guest services and one for out-of-guest before the vocabulary grows three ways to run a daemon.

I've pretty much already settled on leveraging podman-compose for the host side since it's cross-platform between Mac/Linux/Windows, already incorporates dependencies, health-checks, restart policies, etc. (I briefly considered also using devenv for the host side, but then realized that while it works in Mac and Linux, it does not work in Windows.)

mogul added a commit that referenced this pull request Sep 16, 2026
0028 is claimed by the Windows-native secret storage ADR and 0029 by
another in-flight PR, so the agent-kits-on-devenv ADR takes 0030. Rename
only; content unchanged.

Refs: #464, #474
mogul added a commit that referenced this pull request Sep 16, 2026
Records the review outcomes for the questions the ADR left open: a
content-pinned agent install contract; human-owned activation with an
explicit ACQ_ env opt-in for non-interactive use; a create-time vs
run-time rule plus a max-merged resources: field; the native devenv
process manager as the general in-guest supervisor (host side uses
podman-compose); provider facts exported by usai-provider and rendered by
each agent kit; a /nix-anchored bring-your-own-image contract; union-only
visible egress; a personal kit plus a neutral ~/.rc.d hook with documented
use-cases; built-in-bundle-only agent kit inference; an optional OCI
engine kit; declared privileged capabilities under passwordless sudo;
SecretSpec confined to in-sandbox usage; and the stability contract to
preserve during migration.

Leaves three follow-ups open: merge semantics beyond egress, the exact
minimum image contract, and migration sequencing.

Refs: #473, #474
@mogul

mogul commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Pushed commits to this branch. They renumber the ADR to 0030 (0028 and 0029 are claimed by other in-flight work), add a Devenv Baseline and Upstream Impact section recording that the patterns devenv image currently ships devenv 1.11.2 on nixos-25.05 while devenv 2.x first appears in nixos-26.05 / nixpkgs-unstable, and fold in the review feedback: a content-pinned agent-install contract; human-owned activation with an explicit ACQ_ opt-in for non-interactive use; a create-time versus run-time rule; the native devenv process manager as the general in-guest supervisor with podman-compose on the host side; usai-provider exporting facts and each agent kit rendering them; a /nix-anchored bring-your-own-image contract; union-only, visible egress; a personal kit plus a neutral ~/.rc.d hook with documented use-cases; built-in-bundle-only agent kit inference; an optional OCI engine kit; privileged capabilities declared and off by default under passwordless sudo; and SecretSpec confined to in-sandbox usage — leaving three follow-ups open (merge semantics beyond egress, the minimum image contract, and migration sequencing).

A scoped follow-up for updating the devenv image itself based on research into 2.x is tracked at GSA-TTS/agentic-coding-patterns#418.

mogul added a commit that referenced this pull request Sep 16, 2026
Removes the resources: field ask from the create-time versus run-time
section so guest CPU/memory sizing does not complicate the devenv
adoption story, and tracks the user need separately.

Refs: #475, #474
@mogul mogul changed the title docs(adr): propose devenv-based agent kits feat(kits): move to devenv-based agent kits Sep 17, 2026
@mogul

mogul commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Update pushed: 59d10f0 (feat(kits): wire neutral rc snippets).

What changed:

  • Fast-forwarded this branch through a59a4a4 and preserved/reconciled the neutral rc-snippet docs from docs(kits): document neutral rc snippets #511.
  • Added a shared acq_login_profile_rc_block used by both backend adapters.
  • Wired msb to consume the shared rc.d bridge in its login-profile setup.
  • Added sbx create-time bridge installation through post-create sbx exec, with an exec-readiness wait and without using a startup-bearing kit.
  • Added focused Bats coverage for msb/sbx rc.d behavior, including an executable check that the generated block sources snippets in byte order.
  • Updated docs to describe the implemented bash ~/.profile bridge and shell-specific constraints accurately.

Verification:

  • PASS: ./scripts/test-acq-bats test/bats/72-msb-agent-user.bats test/bats/90-sbx-startup-kit.bats (61/61)
  • PASS: printf '%s\n' acq.backends/common.sh acq.backends/msb.sh acq.backends/sbx.sh | xargs -r -n8 shellcheck --severity=warning
  • PASS: git diff --check
  • PASS: npm run lint:md (0 issues)
  • PASS: adversarial AI code review completed; actionable findings addressed before push

Also updated the PR summary with a fully qualified Closes: line for the implementation-increment issues expected to close when this PR merges.

@mogul

mogul commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer orientation

AI-assisted (OpenCode). Independently re-verified at head 668e920 against main @ 33c83c5 before flipping out of draft; the transcript below is my own run, not the authoring session's.

Deliberately dormant — not oversights

Two things a reviewer will reasonably flag as incomplete are intentional, because the producing artifacts are not in the pinned patterns release:

Closes: was trimmed accordingly — #481 and #485 are not closed by this PR, with unblock triggers recorded in the description.

OCI is live but opt-in, default-off

v1.10.0 contains the oci-engine kit, so ACQ_ENABLE_OCI_KIT=1 now genuinely selects it and _oci_kit_selected suppresses the msb adapter's _acq_msb_ensure_oci. Measured gate matrix at the new pin:

ACQ_ENABLE_OCI_KIT=__unset -> selected=no  kitcount=4
ACQ_ENABLE_OCI_KIT=0       -> selected=no  kitcount=4
ACQ_ENABLE_OCI_KIT=false   -> selected=no  kitcount=4
ACQ_ENABLE_OCI_KIT=no      -> selected=no  kitcount=4
ACQ_ENABLE_OCI_KIT=off     -> selected=no  kitcount=4
ACQ_ENABLE_OCI_KIT=1       -> selected=yes kitcount=5

Unset means byte-identical behavior to today.

ADR-0030's open questions are tracked, not unresolved

Review focus

  1. acq_provenance_write in acq.backends/common.sh — the one place a silent defect could hide. This branch and main both added a workspace parameter by different mechanisms (positional $4 here; a workspace_source=host marker on main), and common.sh does not textually conflict — so a naive merge yields two workspace derivations and two printf 'workspace=%s' lines, writing a duplicate key. Resolution kept main's marker-gated path and dropped the positional fallback. test/bats/125-github-scope.bats → "gh-record: provenance refresh preserves only host-marked workspace" is the canary; it passes.
  2. sbx startup barrier × the rc.d bridge. _acq_sbx_wait_for_startup_barrier (from fix(sbx): wait for startup barrier before attach #512) and _acq_sbx_ensure_rc_bridge both run after a successful sbx create; the bridge is installed via post-create sbx exec, deliberately not a startup-bearing kit, to avoid the sbx ≥ 0.38 live-extend refusal.
  3. OCI kit boundary — whether the adapter auto-install suppression can misfire for a user who never sets ACQ_ENABLE_OCI_KIT.
  4. 668e920 (fix(sbx): honor provider key env in usai checks) replaces hardcoded USAI_API_KEY with $USAI_PROVIDER_KEY_ENV through the sbx rotate/secret paths — worth a look given rotation's interrupted-state recovery messaging.

Verification (my independent re-run at 668e920)

PASS  git merge-tree origin/main <head>                 -> no conflicts (MERGEABLE)
PASS  ACQ_BATS_JOBS=4 ./scripts/test-acq-bats           -> 1..632, 632 ok, 0 failures
PASS  ./scripts/test-acq-bats test/bats/125-github-scope.bats -> 14/14 (merge-trap canary)
PASS  ./scripts/test-acq-bats test/bats/66-oci-engine-kit.bats -> 14/14
PASS  bash -n acq acq.backends/*.sh scripts/* test/bats/helper.bash
PASS  shellcheck --severity=warning (acq, adapters, lib, all bats, verify-backends)
PASS  git diff --check
PASS  git ls-remote ... refs/tags/v1.10.0 -> 2dd2ad6ad5f63b842d732424bed5ba9aebeed676
PASS  git grep '6c6753c' / 'v1.9.0' -> no stale pin references outside CHANGELOG
PASS  offline fail-soft: GIT_ALLOW_PROTOCOL=none acq ls -> exit 0
      (rm/ports/secret ls also clean; no provider-facts hard failure)
PASS  pinned-kit probes: oci_ready=yes  provider_facts_rc=2  source=fallback
                         acq_agent_builtin_kit_enabled opencode=false
PASS  npx markdownlint-cli2 (ADR in scope -- verified by probe: 51 files, catches an injected error)

Correction to an earlier claim in this thread: I had reported npm run lint:md as a vacuous signal for ADR changes (#525). That is stale — #527 already unified file selection to globs: ["**/*.md"], so docs/adr/ is linted. I confirmed by injecting a deliberate MD018 violation into docs/adr/: the file count goes 50→51 and the error is caught. 0 issues in 0 files means zero offending files, not zero scanned. #525 is open but appears already fixed in substance.

Merge-order note for maintainers

This PR conflicts with most other open PRs in acq.backends/{msb,sbx}.sh by co-location, not dependency — it consumes no symbol introduced by any of them. Whoever merges second rebases; there is no required ordering. ADR numbering is now collision-free: 0030 (#474), 0031 (#500), 0032 (#510), 0033 (#519), 0034 (#526), 0035 (#504), 0036 (#507). 0037 is next free.

Per AGENTS.md §8.2, this AI-assisted change requires human review before it is relied on; nothing here is self-approved.

@basiliskus

Copy link
Copy Markdown
Contributor

Reviewed at 486cdf4ef91ae7cfcff176a2b223d879a54b9b56.

Thanks for the orientation comment — it made this reviewable, and it held up: I checked all five of your numbered claims and four verify cleanly. Two findings, both in the same family: a gate that fails open rather than closed. Neither is live at the current pin, which is why I'd take them as follow-ups rather than a block, but the second one activates on the next pin bump and that is exactly when you want it loud.

  1. Minor, confirmed by reproduction: the OCI opt-in normalizes whitespace-padded off values to on.
    acq.backends/common.sh:91-93

    The parser lowercases but does not trim, and treats everything outside the exact off-list as enabled:

    case "$(printf '%s' "$ACQ_ENABLE_OCI_KIT" | tr '[:upper:]' '[:lower:]')" in
      ""|0|false|no|off) ACQ_ENABLE_OCI_KIT="" ;;
      *)                 ACQ_ENABLE_OCI_KIT="1" ;;
    esac

    Traced past the flag to actual kit selection, counting oci-engine refs out of _acq_selected_support_kit_refs:

    unset     -> disabled, 0 refs
    0         -> disabled, 0 refs
    0␣        -> ENABLED,  1 ref
    ␣false␣   -> ENABLED,  1 ref
    true      -> ENABLED,  1 ref
    1         -> ENABLED,  1 ref
    

    Concrete failure: a .env line or a quoted export with a stray trailing space — ACQ_ENABLE_OCI_KIT="0 " — silently selects a create-time capability kit and suppresses the adapter's own podman provisioning at acq.backends/msb.sh:3285. The user believes they opted out.

    I rate this minor rather than major because the trigger is narrow and the fix is one tr -d '[:space:]' in the pipeline. But the direction is the wrong one for an opt-in: your gate-matrix comment covers the exact values and they are all correct, so this is specifically the untested whitespace case.

    Worth saying the behaviour is deliberate, not accidental — test/bats/66-oci-engine-kit.bats:235 explicitly asserts "any other value" enables, with 1 true yes on YES enabled anything, and :224 covers "" 0 false no off FALSE No OFF. So the design choice is permissive-on, and the gap is that permissive-on plus no trimming means a typo enables. If permissive-on is intentional, trimming is the minimal fix; if you would rather be strict, an explicit on-list would also close it.

  2. Major, confirmed by trace: an invalid provider-facts artifact is indistinguishable from an absent one, and its diagnostic is suppressed.
    acq.backends/common.sh:338-341

    The loader is careful to separate the two cases — common.sh:320-322 returns 2 for absent and 1 for invalid, with echo "acq: invalid provider facts artifact: $facts" >&2. The wrapper that every dispatch path actually calls throws both away, and redirects the diagnostic to /dev/null:

    acq_provider_facts_load_from_kit_or_fallback() {
      if acq_provider_facts_load_from_kit "$@" 2>/dev/null; then
        :
      else
        acq_provider_facts_use_fallback
        acq_debug "provider facts unavailable from kit; using transitional fallback defaults"
      fi

    Seven dispatch call sites use the wrapper: acq:1004, :1016, :1075, :1410, :1432, :1471, :1549. acq_debug is off by default, so on a malformed artifact the user sees nothing at all and gets fallback USAi defaults.

    You are right that this is dormant today — I confirmed the artifact 404s at the pin, so rc=2 is the only reachable path and the fallback is correct behaviour. The problem is the bump that adds the artifact: a malformed or version-skewed usai.env would then create sandboxes on fallback endpoint/key assumptions while reporting success, which is the half-wired provider migration this PR is explicitly trying to sequence safely. test/bats/50-kit-list-completeness.bats:139 proves the loader rejects invalid data; nothing proves the dispatch path surfaces it.

    Cheapest fix is to let rc=1 through: fall back on 2, fail closed (or at minimum print the diagnostic) on 1.

Your five claims, verified:

  1. Both paths dormant — verified. integrations/isolation/acq-kits/opencode/spec.yaml and .../usai-provider/provider-facts/usai.env both 404 at the v1.10.0 tag, oci-engine/spec.yaml is present, and acq.backends/agents.sh:100 returns false for opencode. The unpinned npm install -g is genuinely unreachable at this pin.
  2. OCI opt-in, default-off — partially. Exact values behave as you reported, including unset. Whitespace does not; see finding 1.
  3. acq_provenance_write merge trap — verified resolved. This was the right thing to point a reviewer at. There are two printf 'workspace=%s' lines in common.sh, at :1801 and :1853, which looks like the duplicate-key symptom until you check the function boundaries: :1801 is inside acq_provenance_write (:1764) and :1853 inside acq_workspace_record_write (:1828) — two different records, one key each. Both are workspace_source=host marker-gated, with no positional $4 fallback surviving. The canary gh-record: provenance refresh preserves only host-marked workspace (test/bats/125-github-scope.bats:151) passes.
  4. sbx barrier × rc.d bridge — no finding.
  5. 668e920 provider key env — no finding; the USAI_PROVIDER_KEY_ENV substitution is consistent through the sbx rotate and secret paths, including the interrupted-state recovery guidance.

Below the bar:

Validation. Ran: full suite ./scripts/test-acq-bats → 633 ok / 0 not ok / 5 skips (you reported 632; the extra is the main merge). Skips are 2 pwsh not available, 3 sbx not on PATH — grammar acceptance UNVERIFIED, 1 msb authorize. Also: the OCI gate matrix above through real kit selection, not just the flag; the provenance duplicate-key check with function-boundary confirmation; the canary test in isolation; acq doctor reachability trace; v1.10.0 artifact presence via the GitHub API; bash -n over acq, the adapters and scripts; the repo ShellCheck batch per AGENTS.md (clean — shellcheck is present in my environment, so that is a real pass); npm run lint:md → 0 issues; gh pr checks 474 all passing.

Not run, and worth naming explicitly given the size of this change. I did not run the new and modified tests against the merge-base ffac0fa, so I cannot tell you whether the 14 new assertions across 140-opencode-migration-gate.bats, 65-doctor-image-contract.bats, 66-oci-engine-kit.bats and the 11 touched files would fail without this change. For a 3200-line change across three adapters that is the check I would most want, and this repo has a specific history here — 71a19cd exists because 116-clone-option.bats's negative assertions could not fail. 66-oci-engine-kit.bats:224/:235 are exactly that shape: both would pass if the gate were deleted entirely and the variable simply never set. I am not asserting they are vacuous; I am saying I have not established that they are not.

Also not run: ./scripts/verify-backends needs a sandbox-capable host, so real create/attach on either backend is unverified by me — and this PR changes provisioning on both. The devenv base image, the rc.d bridge landing in a live guest, and the agent-kit install path are untested by anything I did.

Verdict: comment, not request-changes. Nothing here is live-broken at the current pin, your dormancy analysis is accurate, and the merge trap you flagged is genuinely resolved. I would want finding 2 fixed before the pin bump that lands provider-facts/usai.env — it is a few lines and it is the difference between a loud and a silent failure during the migration this ADR sequences. Finding 1 whenever convenient.

Before merging I would want a human to run ./scripts/verify-backends on a sandbox-capable host and do one real acq create plus attach on each backend, since this changes how both provision and nothing I ran touches a live sandbox.

Assisted-by: OpenCode [gpt_5_5_default_v2] change-review

@basiliskus

Copy link
Copy Markdown
Contributor

Closing the base-comparison gap I flagged in my previous comment, since an open question like that reads as doubt and it should not sit there unresolved.

I copied the three new test files plus scripts/test-acq-lib.sh onto merge-base ffac0fa in a scratch worktree and ran them:

140-opencode-migration-gate.bats    16 tests, 10 fail at base
65-doctor-image-contract.bats        9 tests,  9 fail at base
66-oci-engine-kit.bats              14 tests, 11 fail at base
TOTAL                               39 tests, 30 fail at base

That includes both 66-oci-engine-kit.bats assertions I singled out as the shape that could pass vacuously. They fail at base for exactly the right reason — ffac0fa has no normalizer at all (grep -c ACQ_ENABLE_OCI_KIT acq.backends/common.sh is 0), so the raw value passes through untouched:

test 13 "off-ish values as off"      expected "EMPTY", actual "0"
test 14 "any other value as on"      expected "1",     actual "true"

So those tests assert the normalization itself, not merely the absence of a value. My concern was wrong and I withdraw it.

On the 9 that do pass at base: they are mostly assert_failure cases such as oci-kit: readiness FALSE for a wrong-name spec, and they pass because acq_oci_engine_kit_ready does not exist at base — bats run on a missing function returns non-zero, which satisfies the assertion. That is the unavoidable form of vacuous pass; you cannot write a negative test against a function that does not exist yet. Each is paired with a positive counterpart (tests 9 and 10, readiness TRUE ...) that does fail at base, so the file discriminates as a whole. No action needed.

The other two gaps from that comment stand unchanged: ./scripts/verify-backends still needs a sandbox-capable host, so real create/attach on either backend is unverified by me, and the two findings are unchanged — finding 2 (silent fallback on an invalid provider-facts artifact) is the one worth closing before the pin bump that lands provider-facts/usai.env.

Verdict unchanged: comment, not request-changes.

Assisted-by: OpenCode [gpt_5_5_default_v2] change-review

mogul added 11 commits October 5, 2026 21:52
Co-authored-by: OpenCode [gpt-5.5] <bret.mogilefsky@gsa.gov>
Co-authored-by: OpenCode [gpt-5.5] <bret.mogilefsky@gsa.gov>
…uestions

Refs: #533

Co-authored-by: OpenCode [gpt-5.5] <bret.mogilefsky@gsa.gov>
Co-authored-by: OpenCode [gpt-5.5] <bret.mogilefsky@gsa.gov>
#500's generic opt-in catalog (ADR-0031) supersedes the OCI-specific gate. The oci-engine kit will be added to ACQ_OPTIN_KIT_NAMES in the follow-up tracked by #505.

The msb adapter auto-install path is unchanged by this commit; retiring that path remains part of the follow-up.

Co-authored-by: OpenCode [gpt-5.5] <bret.mogilefsky@gsa.gov>
…back

Co-authored-by: OpenCode [gpt-5.5] <bret.mogilefsky@gsa.gov>
Replace markdownlint-cli2 with markdownlint-cli because the current cli2 dependency graph depends on braces 3.0.3, which has no fixed release for GHSA-vfj7-8cjw-p6xm.

Keep the same markdown rules and exclusions through markdownlint config and ignore files, and install the pinned linter dependencies before the pre-commit workflow invokes the local hook.

Refs: #505

Co-authored-by: OpenCode [gpt-5.5] <bret.mogilefsky@gsa.gov>
verify-backends exports a hermetic XDG_CONFIG_HOME so a run cannot read the
user's real acq config. _acq_config_file resolves under that, so acq never
finds a config file and EVERY verify run looks like a first run to
maybe_first_run_configure, which blocks on an interactive confirm.

The symptom is a verifier that appears to hang: run_acq captures output by
default, so the create step emits nothing and never returns while acq waits on
stdin. create_time_kit_picker is a second gate with the same cause.

Export ACQ_NO_PROMPT=1 alongside the other hermetic-state settings. That forces
the non-interactive path for both config widgets (and for acq configure, which
then reports "configuration unchanged" and makes no writes), so the configured
defaults apply — which is what a verifier wants. Secret VALUE reads are gated on
[ -t 0 ] rather than on the prompt helper, so seeding is unaffected.

The underlying gap is on main: the first-run prompt arrived with
#500 and the verifier was never taught to opt
out. Fixed here because this is the PR whose own ADR-0030 live verification gate
is blocked by it.

Co-authored-by: OpenCode [claude-opus-5] <bret.mogilefsky@gsa.gov>
Step 2 of seed_usai_secret decides whether to create the global sbx custom
secret by grepping the whole `sbx secret ls` listing for USAI_API_KEY. That
also matches a SANDBOX-SCOPED row, including one left behind for a sandbox that
no longer exists. When such a row is present the global row is never created,
step 3's `acq secret has -g usai` returns 1, and every sbx check skips with
"verifier did not seed sandbox-scoped USAi key".

Observed live against sbx v0.46.0 with a stale row:

  CUSTOM SECRETS
  SCOPE         TARGETS            ENV            PLACEHOLDER
  nonexistent   api.gsa.usai.gov   USAI_API_KEY   sbx-cs-naRspB5KyWAPCxB1

The bare grep matches that row, so the seed is skipped and the run reports
1 passed / 2 skipped instead of exercising the kit contract.

Match only a row whose SCOPE column is "(global)", which is the scope step 3
asks about. A genuine pre-existing global row still no-ops as before.

Co-authored-by: OpenCode [claude-opus-5] <bret.mogilefsky@gsa.gov>
@mogul
mogul force-pushed the refactor/acq-devenv-base-image branch from 191d501 to 4c5812a Compare October 5, 2026 22:23
@mogul
mogul force-pushed the refactor/acq-devenv-base-image branch from 92ce87f to 058c1af Compare October 5, 2026 23:45
@mogul mogul changed the title feat(kits): move to devenv-based agent kits feat(kits)!: adopt pinned agent and capability kits Oct 5, 2026
@mogul

mogul commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Release-note / release-please warning for merge:

Please preserve the breaking-change and migration details in the squash commit body. release-please reads the Conventional Commit on main; it will not reliably carry the PR description into the changelog.

Suggested squash commit message:

feat(kits)!: adopt pinned agent and capability kits

Moves agent/tool installation from adapter-owned fallback paths to pinned
patterns kits. 
- Pin updated to agentic-coding-patterns to v1.12.0
(56d1f5f49ef928f50e61aa64a93d618b3d21c415)
- Uses the opencode agent kit for OpenCode
- Makes OCI support an explicit opt-in via the oci-engine kit.

BREAKING CHANGE: acq no longer installs podman/OCI tooling by default. Users
who need docker, docker compose, or rootless podman when the base image doesn't 
provide it must opt into the oci-engine kit with `acq configure`, `--kit`, or
`ACQ_EXTRA_KITS`.

BREAKING CHANGE: acq no longer performs native OpenCode npm fallback
installation. OpenCode is supplied by the pinned opencode agent kit from
agentic-coding-patterns v1.12.0, currently delivering OpenCode v2.0.22. If the 
kit is unavailable or invalid, acq fails closed instead of installing opencode-ai 
from npm.

BREAKING CHANGE: Existing acq sandboxes with adapter-installed podman are not
migrated in place. Recreate affected sandboxes or add the oci-engine kit
before relying on OCI behavior after stop/start.

@mogul

mogul commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

I reverified on the host just in case... All tests pass. Merging!

@mogul
mogul merged commit 6a67063 into main Oct 6, 2026
7 checks passed
@mogul
mogul deleted the refactor/acq-devenv-base-image branch October 6, 2026 00:20
mogul added a commit that referenced this pull request Oct 6, 2026
## Summary

Corrective PR after #474 was
squash-merged with release notes that describe the full collapsed stack,
but the final branch contents only included the lower #474 layer.

This lands the implementation that the merged #474 commit message
already announces as breaking:

- add `oci-engine` to the opt-in kit catalog;
- retire MSB adapter-owned OCI/podman auto-install and unconditional
device grants;
- pin `PATTERNS_KIT_REF` to agentic-coding-patterns v1.12.0
(`56d1f5f49ef928f50e61aa64a93d618b3d21c415`);
- remove the native MSB OpenCode npm fallback so OpenCode is supplied by
the pinned `opencode` agent kit.

## Why This Is Separate

The merge commit for #474 already contains the correct release-please
`BREAKING CHANGE:` footers and migration notes, but the code still had
the old v1.10.0 patterns pin plus the old adapter-owned OCI/OpenCode
paths. This PR makes shipped behavior match the already-merged release
notes before the release PR is merged.

## Verification

- `bash -n acq acq.backends/*.sh scripts/test-acq-lib.sh
scripts/test-acq-bats scripts/fix-exec-bits test/bats/helper.bash
scripts/verify-backends` passed.
- `./scripts/fix-exec-bits --check` passed.
- `git diff --check` passed.
- `npm run lint:md` passed.
- `npm audit --prefix .github/linters --audit-level=high` passed.
- ShellCheck batch passed.
- Focused Bats passed: `TMPDIR="$PWD/tmp" ./scripts/test-acq-bats
test/bats/36-agent-catalog.bats test/bats/50-kit-list-completeness.bats
test/bats/72-msb-agent-user.bats test/bats/95-kit-ref-persistence.bats
test/bats/135-configure.bats test/bats/140-opencode-migration-gate.bats`
(`109 ok`).

## Rollback

Revert this PR to restore the implementation currently on `main`; note
that doing so would again make the code disagree with the already-merged
#474 breaking-change release notes.

## Security Impact

Positive security impact consistent with #474's release notes: OCI
device exposure is opt-in through the `oci-engine` kit, and OpenCode
installation comes from the pinned patterns release instead of native
npm fallback.

AI-assisted. Human review required.
mogul added a commit that referenced this pull request Oct 6, 2026
🤖 I have created a release *beep* *boop*
---


##
[4.0.0](v3.1.0...v4.0.0)
(2026-10-06)


### ⚠ BREAKING CHANGES

* **kits:** acq no longer installs podman/OCI tooling by default. Users who need docker, docker compose, or rootless podman when the base image doesn't provide it must opt into the oci-engine kit with `acq configure`, `--kit`, or `ACQ_EXTRA_KITS`.
* **kits:** acq no longer performs native OpenCode npm fallback installation. OpenCode is supplied by the pinned opencode agent kit from agentic-coding-patterns v1.12.0, currently delivering OpenCode v2.0.22. If the kit is unavailable or invalid, acq fails closed instead of installing opencode-ai from npm.
* **kits:** existing acq sandboxes with adapter-installed podman are not migrated in place. Recreate affected sandboxes or add the oci-engine kit before relying on OCI behavior after stop/start.

### Features

* **acq:** choose the host port per sandbox, and let --publish pin it
([#526](#526))
([410f332](410f332))
* **acq:** export ACQ_WORKSPACE and ACQ_CLONE into the guest on both
backends
([#457](#457))
([e0b9a3b](e0b9a3b))
* **acq:** interactive `acq configure` UI for extra kits + token-scoping
defaults
([#500](#500))
([13d9d3a](13d9d3a))
* **kits:** adopt pinned agent and capability kits
([#474](#474))
([6a67063](6a67063))
* **secrets:** add Windows-native DPAPI secret backend
([#464](#464))
([907f404](907f404))
* **windows:** add acq preview install path
([#463](#463))
([d6d76f4](d6d76f4))


### Bug Fixes

* **acq:** use sandbox workspace for github scoping
([#476](#476))
([1610835](1610835))
* **ci:** bump js-yaml to 4.3.2 to clear a high-severity CPU-DoS
advisory
([#454](#454))
([3217b49](3217b49))
* **ci:** bump smol-toml to 1.7.1 to clear an infinite-loop DoS advisory
([#459](#459))
([c2291bf](c2291bf))
* **ci:** centralize markdownlint coverage
([#548](#548))
([a27c31e](a27c31e))
* **ci:** pin markdownlint-cli2 once, clear GHSA-253c, unify file
selection
([#527](#527))
([e712685](e712685))
* **ci:** unblock unrelated PRs from the bash32 required-check deadlock
([#513](#513))
([187aa57](187aa57))
* **install:** keep curl | sh stdin intact and tap msb formula first
([#462](#462))
([c808d74](c808d74))
* **install:** trust the microsandbox-acq dependency before brew install
([#534](#534))
([ffac0fa](ffac0fa))
* **kits:** land collapsed kit stack implementation
([#549](#549))
([bcd5b9c](bcd5b9c))
* **msb:** allow Playwright browser downloads
([#477](#477))
([0c399c5](0c399c5))
* **msb:** block unsafe 0.7 migration versions
([#510](#510))
([3634046](3634046))
* **msb:** carry every origin URL, keep the scratch private, fail closed
on a copy miss
([#488](#488))
([babcd49](babcd49))
* **msb:** carry the source checkout's git identity into the --clone
scratch
([#443](#443))
([320d784](320d784))
* **msb:** follow the host resolvers and disable DNS rebind protection
in the guest
([#444](#444))
([d2f4bdc](d2f4bdc))
* **msb:** point the --clone scratch's origin at the source checkout's
remote
([#455](#455))
([7f48b94](7f48b94))
* **msb:** refresh ssh authorization for port publish
([#446](#446))
([72a6bd5](72a6bd5))
* **msb:** session parity for exec/attach/shell (cwd, terminal identity,
login shell)
([#427](#427))
([6261eda](6261eda))
* **msb:** skip the recursive home chown when the image baked the agent
user
([#432](#432))
([d8f66a4](d8f66a4))
* **msb:** thread the merged kit env onto kit lifecycle commands
([#519](#519))
([2248a92](2248a92))
* **msb:** translate kit wildcard network rules
([#451](#451))
([e22bbc3](e22bbc3))
* **msb:** verify post-hoc port forward listener
([#530](#530))
([8ca50e1](8ca50e1))
* **msb:** warn on broadened kit wildcard rules
([#460](#460))
([6ded3ce](6ded3ce))
* **msb:** warn when ssh-agent route is missing
([#448](#448))
([3a767a4](3a767a4))
* **sbx:** emit valid setup.install shape for the git-identity mixin
([#492](#492))
([8e6160c](8e6160c))
* **sbx:** raise the version floor to 0.39.0, where sbx create --env
first exists
([#491](#491))
([30f9ecf](30f9ecf))
* **sbx:** wait for startup barrier before attach
([#512](#512))
([33c83c5](33c83c5))
* **secret:** reject multiline test secret values
([#447](#447))
([f2da950](f2da950))
* **secrets:** propagate rotation across backends
([#509](#509))
([9641958](9641958))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Bret Mogilefsky <bret.mogilefsky@gsa.gov>
wz-gsa pushed a commit to GSA-TTS/agentic-coding-patterns that referenced this pull request Oct 9, 2026
…#480)

## Summary

Move the devenv image's `NIXPKGS_REV` to `nixos-26.05`
(`0d9e9b832d03ac387417e16ce1febf73b2e631e1`), which ships devenv 2.1.2,
and bump `VERSION` to `2.0.0` because the devenv major is part of the
image contract. The README records that contract and what a 2.x CLI
changes for repos still locked to 1.x modules. Existing consumers keep
the immutable `1.0.0` tag.

## Eligibility & Accountability

- [x] I am a **federal employee or contractor** (PRs merge with
`@GSA-TTS/agentic-coding-team` code-owner approval; eligibility is
attested here, not authenticated). *Anyone may open issues without
this.*
- [x] **I understand this change and can explain it in my own words** —
I am the author of record and accountable for it.
- [x] By opening this PR, I **attest I have the right to release this
work** under the repository's [CC0-1.0](../LICENSE) dedication.

## AI Assistance Disclosure

- [x] AI tools were used in this contribution
- [x] I verified all AI-generated content: no fabricated/hallucinated
APIs, dependencies, citations, or placeholder content; any AI-suggested
dependency was confirmed to exist.
- [x] Any inherited/reused third-party material retains its original
license and is identified (not represented as public domain).

## Type of Change

- [x] New tool/editor integration (`integrations/`)
- [x] Documentation update
- [x] Infrastructure/tooling change

## Motivation

Toolchain currency for the generic devenv image (item 1 of
#418): devenv 2.x is absent from
`nixos-25.05` and `nixos-25.11` and first appears in `nixos-26.05`. This
PR is only the pin bump plus consumer documentation; size/startup
measurement, the `devenv container` generation model, and persistence
docs from that issue are left for follow-ups.

This satisfies the substrate requirement in
GSA-TTS/agentic-coding-quickstart#474 (ADR-0030: a channel carrying the
intended devenv major, with the major recorded in the image contract),
so it should land before GSA-TTS/agentic-coding-quickstart#474 claims
that requirement is met. The dependency is conceptual: that PR pins no
devenv image tag, so nothing enforces the order. `nixos-26.05` is the
only channel that is both devenv 2.x and release-pinned: 2.2 and later
exist only in `nixpkgs-unstable` (currently 2.4.0), and devenv 2.2
stopped building for `x86_64-darwin` (devenv v2.2 release notes).

## Testing

- [x] Ran `make validate` locally (all checks pass)
- [x] Tested with at least one real scenario
- [x] Commit messages follow [Conventional
Commits](https://www.conventionalcommits.org/) format (see
[CONTRIBUTING.md](../CONTRIBUTING.md#commit-message-format))

Build: `workflow_dispatch` of `devenv-image.yml` on this branch
succeeded on all four legs (shell/opencode × amd64/arm64); the manifest
job skipped as designed off `main`. Run:
https://github.com/GSA-TTS/agentic-coding-patterns/actions/runs/37489740805

Image smoke test (arm64 `devenv-opencode` digest from that run, in an
msb sandbox created through `acq` with a real kit stack): `devenv
version` → `devenv 2.1.2 (aarch64-linux)`; installer Nix 2.24.9 and
direnv 2.37.1 present; non-interactive `bash -c 'devenv version'` works;
`agent:agent` owns `/nix`, `/nix/store` (1775) and `/nix/var/nix/db`. A
minimal devenv 2.x project built in the sandbox: its outputs are
registered in the shared store DB (schema 10) as seen by the installer's
`nix path-info`, `nix store verify` passes, and no group/world-writable
outputs are produced (the single-user-store regression fixed upstream in
cachix/devenv#2757 is absent). Locked GitHub inputs, cache.nixos.org and
cachix fetches all succeeded through the kit-provided CA via devenv's
bundled Nix.

Consumer compatibility (devenv 2.1.2 from the same nixpkgs rev, run
against scratch copies of two team repos that stay locked to 1.x
modules): a Python/venv/tasks repo passes unchanged; a
Ruby/Postgres/`git-hooks` repo fails until the `git-hooks` input is
declared in `devenv.yaml`, then passes including `devenv up -d`
Postgres. `devenv shell` under 2.x rewrites `devenv.lock` (drops
implicit `git-hooks`/`pre-commit-hooks`/`flake-compat` nodes) and a 1.x
CLI adds them back at current HEAD; `.devenv/` is reusable across
majors. These behaviors are what the README section documents.

## Safety Checklist

- [x] No secrets, API keys, tokens, or passwords included
- [x] No PII (names, emails, SSNs, addresses)
- [x] No CUI (Controlled Unclassified Information)
- [x] No internal URLs, hostnames, or system paths
- [x] No customer data or proprietary information

## Quality Checklist

- [x] Plain language used (Grade 10 or below preferred)

## Validation Results

```
make validate

✓ All validators passed
```

## Related Issues

Relates to #418

## Additional Context

The feature-branch build wrote to the shared
`devenv-*:cache-{amd64,arm64}` registry cache tags, so the next `main`
build after merge rebuilds more layers than usual.

Known follow-ups not in this PR: a post-build smoke-test job in
`devenv-image.yml` (fixture with `git-hooks`, `direnv exec . true`,
store-permission check); `devenv processes down` can time out and
SIGKILL the process group without stopping a service's children
(observed with Postgres under the 2.1.2 CLI); the `devenv update` path
(2.x modules, `prek` default, native process manager) could not be
exercised behind a GitHub-API-rate-limited proxy because the bundled
Nix's libgit2 fallback fails with `authentication required but no
callback set`.

## Reviewer Notes

**Reviewers should verify:**

- [ ] `NIXPKGS_REV` resolves to a `nixos-26.05` commit and the
Dockerfile comment matches
- [ ] `VERSION` is `2.0.0` and not previously published
- [ ] README 2.x consumer notes match the migration guide's documented
breaking changes
- [ ] No sensitive information included
- [ ] `make validate` passes in CI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(devenv): implement human-owned activation contract

3 participants