Skip to content

fix(secrets): propagate rotation across backends - #509

Merged
basiliskus merged 5 commits into
mainfrom
fix/secret-rotation-all-backends
Oct 1, 2026
Merged

basiliskus merged 5 commits into
mainfrom
fix/secret-rotation-all-backends

Conversation

@mogul

@mogul mogul commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Context

Users with mixed msb and sbx sandboxes reported that rotating the USAi API key from the default msb path only updated msb sandboxes. Existing sbx sandboxes still required a second explicit --backend sbx rotation.

Plan / Changes

  • Add guarded internal acq secret _propagate dispatch for adapter-only secret propagation.
  • Propagate acq usai-rotate-api-key and acq secret set [-g | SANDBOX] SERVICE to other installed backends after the active backend stores/rebinds the secret.
  • Add acq_backend_secret_propagate to msb and sbx adapters.
  • Reuse msb live refeed and sbx placeholder-preserving USAi update paths without placing secret values on argv.
  • Document the expanded adapter contract in ADR-0012.
  • Add regression coverage for msb rotation updating existing sbx sandboxes.

Verification

  • PASS: git submodule update --init test/vendor/bats-core test/vendor/bats-support test/vendor/bats-assert
  • PASS: ./scripts/test-acq-bats test/bats/77-usai-rotate.bats
  • PASS: ./scripts/test-acq-bats test/bats/40-secret-set.bats
  • PASS: bash -n acq acq.backends/sbx.sh acq.backends/msb.sh scripts/rotate-apikey
  • PASS: shellcheck --severity=warning acq acq.backends/sbx.sh acq.backends/msb.sh test/bats/77-usai-rotate.bats
  • PASS: git diff --check

Rollback

Revert this PR. Secret rotation returns to the previous behavior where only the selected/default backend is updated and users must run explicit per-backend rotation manually.

Security Impact

This changes secret propagation behavior only. It does not log or place secret values on argv. sbx propagation reuses the existing interactive/placeholder-preserving path and fails closed with guidance when it cannot safely re-apply a secret non-interactively.

AI-assisted: yes

@mogul
mogul requested a review from a team as a code owner September 24, 2026 00:32
@mogul

mogul commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Adversarial Review Follow-Up

Ran an adversarial review in a sub-agent focused on correctness, secret handling, shell portability, adapter contract consistency, and test gaps.

Findings Addressed

  • Scoped USAi propagation to sbx incorrectly delegated to the global rotate path. Fixed by making sbx propagation scope-aware and updating the exact scoped/global placeholder with sbx secret set-custom ... --placeholder ....
  • Unsupported sbx propagation for non-USAi services returned success without applying anything. Fixed to fail closed with scope-preserving guidance when affected sbx sandboxes exist.
  • sbx propagation could prompt for a second divergent value in cases where the contract implied replaying the stored value. Tightened ADR language and behavior: safe replay is used where supported; otherwise propagation fails closed rather than leaking or silently succeeding.
  • Added focused tests for scoped USAi propagation and unsupported sbx propagation failure.

New Commit

  • a8927da fix(secrets): fail closed on unsafe sbx propagation

Verification

  • PASS: ./scripts/test-acq-bats test/bats/77-usai-rotate.bats
  • PASS: ./scripts/test-acq-bats test/bats/40-secret-set.bats
  • PASS: bash -n acq acq.backends/sbx.sh acq.backends/msb.sh scripts/rotate-apikey
  • PASS: shellcheck --severity=warning acq acq.backends/sbx.sh acq.backends/msb.sh test/bats/77-usai-rotate.bats test/bats/40-secret-set.bats
  • PASS: git diff --check

AI-assisted: yes

@basiliskus basiliskus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed at a8927da.

Adds a cross-backend secret propagation hook: after the active backend stores or rotates a secret, acq re-invokes itself as acq --backend <other> secret _propagate …, dispatching to a new acq_backend_secret_propagate in each adapter (msb re-feeds running sandboxes from the acq store; sbx re-applies the USAi proxy placeholder and otherwise fails closed). Wires the hook into usai-rotate-api-key and secret set, extends the ADR-0012 adapter contract, and adds two regression tests.

Intent: Matches the stated intent for the msb→sbx direction, which is the reported bug. The reverse direction (sbx→msb) is newly wired but does not actually deliver the new key — see finding 1.

Findings (1)

  1. Blocking: On the sbx→msb direction the propagation re-binds the stale store value and reports success. sbx's acq_backend_rotate_key collects the new key only at sbx's own set-custom prompt and never writes the acq secret store, but the new hook then calls msb's acq_backend_secret_propagate, which resolves the value via acq_secret_resolve (acq.backends/msb.sh:5216) and prints acq(msb): applied 'usai' to N running sandbox(es). with rc 0. Reproduced offline: store seeded with OLD-STALE-KEY, one running msb sandbox, ACQ_BACKEND=sbx acq usai-rotate-api-key → exit 0, Key validated (HTTP 200). You're good to go., applied 'usai' to 1 running sandbox(es), and the msb stub saw OLD-STALE-KEY. This is the failure mode the ADR text added by this PR prohibits: "rather than leaking the value, accepting a divergent value, or silently claiming success" (docs/adr/0012-backend-neutral-key-rotation.md:123-131). Either have sbx rotation write the acq store, or make msb propagation fail closed when the store value is unchanged.
    acq:940

Below the bar: three verified-real 75s worth a cheap fix — sbx propagation hardcodes --host api.gsa.usai.gov --env USAI_API_KEY and ignores the endpoint sidecar, so secret set -g usai --host usai.alt.example.gov silently re-points sbx at the default GSA host (acq.backends/sbx.sh:1268, contrast the sidecar-first _acq_msb_service_binding at acq.backends/msb.sh:248); the non-usai refusal is unconditional rather than gated on _acq_sbx_secret_exists, so secret set -g github on msb now exits 1 whenever any sbx sandbox exists even when sbx holds no such secret and the existing stdin feed would work (measured rc 0 at the merge base, rc 1 at HEAD; acq.backends/sbx.sh:1251); and the hook is not wired at the third rotation dispatch point, ensure_valid_key's acq_backend_rotate_key call on the acq run attach path (acq.backends/common.sh:2511), which is the path weekly key expiry actually drives — an advisory there may be preferable to a blocking propagate.

Verdict: needs changes

Assisted-by: OpenCode [claude-opus-5] team-code-review

@wz-gsa

wz-gsa commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

AI-assisted review (OpenCode), advisory — needs a human to confirm before it drives a change. Reviewed at head a8927da, main at 30f9ecf. Independent capacity; I read @basiliskus's review first and this is deliberately additive to it.

@basiliskus's blocking finding is correct, and I verified it independently by reading rather than reproducing. acq_backend_rotate_key on sbx (acq.backends/sbx.sh:1513-1620) obtains the new key only through sbx secret set-custom, which prompts sbx-side — rg -c 'acq_secret_set' over that whole function returns zero. So the acq store is never updated on the sbx path, and the new hook then hands msb's propagate a stale value that it re-binds and reports as applied. Nothing to add to that; it should block.

Three things it didn't cover.

Should-fix: msb's propagate is silent when it applies nothing

acq.backends/msb.sh:5319-5343:

applied=$(_acq_msb_secret_refeed "$service" "$scope_name" "$_env" "$_host")
if [ "$applied" -gt 0 ]; then
  echo "acq(msb): applied '$service' to $applied running sandbox(es)." >&2
fi

applied == 0 prints nothing and returns 0. And _acq_msb_secret_refeed (msb.sh:5200) reaches that same printf '0\n'; return 0 from four different states:

  • no _env/_host binding for the service
  • ACQ_SECRET_NO_LIVE_REFEED=1 (deliberate opt-out)
  • acq_secret_resolve returned empty — i.e. the store has no value
  • every msb modify call failed (the && applied=$((applied+1)) never fires)

The first two are legitimately nothing-to-do. The last two are failures. A user who runs acq secret set expecting propagation, and whose msb modify calls all failed, sees no output at all and exit 0. That is the degraded-path rule this repo has been bitten by nine times: "I could not apply it" is reporting as "nothing needed applying."

Suggest separating the counters — eligible vs applied — and saying something when eligible > 0 && applied == 0. That also gives @basiliskus's finding a second line of defence: an empty store value would announce itself rather than being silently skipped.

Nit: the secret value crosses a process boundary as an environment variable

acq:507 and :512 re-invoke acq as a child with:

ACQ_PROPAGATING_SECRET=1 ACQ_SECRET_TEST_VALUE="${ACQ_SECRET_TEST_VALUE:-}" "$ACQ_RESOLVED_PATH" --backend "$be" secret _propagate …

ACQ_SECRET_TEST_VALUE is the actual secret when it is set (secret-store.sh:1055-1056 assigns it straight to value), and it is documented as a test-only hatch — "never set it in production" (:1039-1040). So in production it is empty and this is a non-issue.

I checked the exposure rather than assuming it: on Linux /proc/<pid>/environ is 0400 owner-only, and on macOS ps -Eww requires same-uid, so an env var is not world-readable on either platform. Same-uid visibility on a single-user sandbox host is the same trust domain. So: not a vulnerability, and I'm not asking for a change to the mechanism.

What I would change is the comment. The forwarding line carries no note that the variable can hold secret material, and the "never in production" warning lives in a different file. One inline comment at acq:507 naming that would stop a future reader from widening this — for instance by adding a --value flag or logging the child invocation, both of which would put it in argv or a log.

Nit: secret _propagate guards on an env var, which the same untrusted context can set

acq:1315 refuses the internal verb unless ACQ_PROPAGATING_SECRET is set. That's the right instinct and the message is good. But it only prevents accidental use — anything that can invoke acq can also set the variable. Since the verb's effect is "re-bind the stored value to running sandboxes" it isn't a privilege boundary, so this is fine as-is; worth a word in the comment that it's an ergonomics guard rather than a security one, so nobody later relies on it as the latter.

Verified as holding

  • No propagation recursion. The _propagate branch (acq:1313-1324) calls only acq_backend_secret_propagate and never re-enters _acq_propagate_secret_to_present_backends, so the child cannot fan out again. The for be in msb sbx loop also skips "$be" != "$current", so the originating backend is never re-invoked.
  • Fails closed on a missing adapter hook: command -v acq_backend_secret_propagate is checked before dispatch (acq:1319) with a clear message, rather than erroring out of a missing function.
  • Scope handling is symmetric: named scope short-circuits on acq_backend_exists (so a stale name is a no-op, not an error), and the global path short-circuits when msb list -q is empty.
  • ACQ_SECRET_NO_LIVE_REFEED=1 is honoured on the propagate path, so scripts/verify-backends won't reach into unrelated running VMs — the same opt-out the set/rm paths already respect.
  • The value never reaches argv on the propagate path: msb modify --secret "${_env}@${_host}" passes the binding, not the secret, and _acq_msb_secret_refeed redirects </dev/null so a modify can't consume the enclosing while read loop.
  • bin/injection-scan.sh clean.

Method: static analysis at a8927da, plus a local check of env-var visibility across processes (ps -Eww on macOS) to size the nit above rather than assert it. No PR code executed, no sandbox created. UNVERIFIED: I did not reproduce @basiliskus's offline scenario — I confirmed his finding by reading acq_backend_rotate_key and establishing that it never writes the acq store, which is the same conclusion by a different route.

@basiliskus

Copy link
Copy Markdown
Contributor

Reviewed at a8927da again; head is unchanged since the prior CHANGES_REQUESTED review.

Intent still looks right for the reported msb→sbx case: a rotation or secret set on one backend should re-apply the stored secret to existing sandboxes on the other backend without putting the value on argv. The implementation gets part of that path, but it also wires directions and cases that are not actually safe yet.

Findings (4)

  1. Blocking, confirmed by reproduction + trace: sbx→msb propagation can re-bind a stale or missing acq-store value and still report success. acq calls acq_backend_rotate_key and then unconditionally propagates (acq:939-940). On sbx, acq_backend_rotate_key updates only the sbx proxy via sbx secret set-custom; it never writes the acq secret store (acq.backends/sbx.sh:1513-1611; the only acq_secret_set_interactive call in sbx is in acq_backend_secret_set at acq.backends/sbx.sh:1043). The msb propagate path then resolves whatever is in the acq store (acq.backends/msb.sh:5216) and reports success if msb modify runs (acq.backends/msb.sh:5338-5341).

    Reproduced with fake values: acq store seeded with OLD-STALE-KEY, sbx rotation path run, and msb propagation exited 0 with acq(msb): applied 'usai' to 1 running sandbox(es).; the msb stub saw modify sandbox=mymsb USAI_API_KEY=OLD-STALE-KEY. Empty-store variant was also reproduced: sbx rotation printed acq: propagating 'usai' to existing msb sandbox(es), if any., exited 0, and only ran msb list -q with no modify. That means both “wrong value applied” and “nothing applied” are indistinguishable from successful propagation.

    acq:940, acq.backends/sbx.sh:1513, acq.backends/msb.sh:5216, acq.backends/msb.sh:5338

  2. Major, plausible by static trace: sbx propagation hardcodes the default USAi host and ignores the stored endpoint sidecar. For acq secret set -g usai --host usai.alt.example.gov from msb, _acq_msb_service_binding intentionally gives the sidecar precedence (acq.backends/msb.sh:248-267), but sbx propagation only looks up a USAI_API_KEY placeholder and re-runs set-custom with --host api.gsa.usai.gov --env USAI_API_KEY (acq.backends/sbx.sh:1260-1268). A custom USAi-compatible endpoint can therefore be silently re-pointed at the default GSA host during propagation.

    acq.backends/sbx.sh:1268, acq.backends/msb.sh:248

  3. Major, confirmed by measured A/B: non-USAi secret set from msb now exits 1 whenever any sbx sandbox exists, even when the active backend succeeded and sbx has no matching secret to update. After acq_backend_secret_set succeeds, acq invokes propagation (acq:1308-1310). The sbx propagate hook checks only whether the target sbx scope/sandbox exists (acq.backends/sbx.sh:1240-1243) and then returns 1 for every service other than usai (acq.backends/sbx.sh:1251-1256), without first checking _acq_sbx_secret_exists.

    Measured A/B with an sbx sandbox present and no sbx GitHub secret: base 8e6160c stored and applied github on msb with exit 0; head a8927da stored/applied on msb, then printed the sbx “cannot safely overwrite” guidance and exited 1. That is a new failure for a command that fully succeeded on the selected backend.

    acq:1310, acq.backends/sbx.sh:1240, acq.backends/sbx.sh:1251

  4. Major, confirmed by trace: the attach-time expired-key path still bypasses propagation. The path users hit on weekly key expiry is ensure_valid_key, which prompts and calls acq_backend_rotate_key directly (acq.backends/common.sh:2511). The new propagation hook is wired only in top-level usai-rotate-api-key (acq:940) and after secret set (acq:1310). So an expiry-driven rotation during acq run updates only the active backend, leaving mixed-backend sandboxes divergent.

    acq.backends/common.sh:2511, acq:940, acq:1310

Below the bar: none left from the earlier notes. The hardcoded sbx host, unconditional non-USAi failure, and missing ensure_valid_key wiring should all be treated as real findings.

Validation: local ./scripts/test-acq-bats passes (514 ok, 0 not ok). gh pr checks 509 shows all five checks passing at head. ShellCheck was not run locally because shellcheck is not installed here (xargs: shellcheck: No such file or directory); CI pre-commit is green. npm run lint:md was not run because .github/linters/node_modules is absent. Live ./scripts/verify-backends remains a validation gap because it needs a sandbox-capable host and cannot run inside this sandbox.

Verdict: comment / needs changes at a8927da. I would not submit another formal CHANGES_REQUESTED review solely because the PR is already blocked at this exact unchanged head; the substantive verdict is still that this should not merge as-is.

Assisted-by: OpenCode [gpt_5_5_default_v2]

@mogul

mogul commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Pushed 5f60e71 fix(secrets): harden cross-backend propagation.

Changes

  • Prevented sbx -> msb USAi rotation propagation from replaying a stale acq-store value. sbx rotations now skip msb propagation with explicit guidance because sbx prompts through its proxy flow and does not expose the newly-entered value to the acq store.
  • Wired expiry-driven acq run rotations through the same origin-aware propagation helper after successful active-backend rotation.
  • Updated sbx propagation to preserve stored USAi sidecar host/env metadata and select the matching placeholder instead of hardcoding api.gsa.usai.gov.
  • Changed non-USAi sbx propagation to no-op when no matching sbx secret exists, while still failing closed with manual guidance when an existing sbx secret actually needs overwrite.
  • Hardened msb propagation to fail closed when affected sandboxes exist but no acq-store value is available, instead of silently returning success.
  • Updated ADR-0012 to document the safe asymmetry for sbx -> msb rotations.

Adversarial Review

Ran an adversarial code review in a sub-agent focused on cross-backend propagation, stale-store behavior, sidecar host/env handling, failure modes, shell safety, and test gaps. It independently confirmed the known review findings; the fixes above address those findings.

Verification

  • PASS: git submodule update --init test/vendor/bats-core test/vendor/bats-support test/vendor/bats-assert
  • PASS: ./scripts/test-acq-bats test/bats/77-usai-rotate.bats
  • PASS: ./scripts/test-acq-bats test/bats/40-secret-set.bats
  • PASS: ./scripts/test-acq-bats
  • PASS: bash -n acq acq.backends/sbx.sh acq.backends/msb.sh acq.backends/common.sh scripts/rotate-apikey
  • PASS: shellcheck --severity=warning acq acq.backends/sbx.sh acq.backends/msb.sh acq.backends/common.sh test/bats/77-usai-rotate.bats test/bats/40-secret-set.bats
  • PASS: git diff --check
  • PASS: npm run lint:md
  • BLOCKED: gitleaks detect --no-git -v (gitleaks: command not found)

AI-assisted: yes

@mogul

mogul commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Branch updated from main and pushed at 87890b6.

Re-review requested. Post-merge verification:

  • PASS: ./scripts/test-acq-bats test/bats/77-usai-rotate.bats
  • PASS: ./scripts/test-acq-bats test/bats/40-secret-set.bats
  • PASS: ./scripts/test-acq-bats
  • PASS: bash -n acq acq.backends/sbx.sh acq.backends/msb.sh acq.backends/common.sh scripts/rotate-apikey
  • PASS: shellcheck --severity=warning acq acq.backends/sbx.sh acq.backends/msb.sh acq.backends/common.sh test/bats/77-usai-rotate.bats test/bats/40-secret-set.bats
  • PASS: git diff --check origin/main...HEAD
  • PASS: npm run lint:md

Note: the merge commit was created with signing disabled for this single command because the configured SSH signing agent socket was unavailable (Couldn't get agent socket?).

AI-assisted: yes

@mogul
mogul requested a review from basiliskus September 30, 2026 23:00

@basiliskus basiliskus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed at 87890b6c3cc1f0c19ec62e2ac1ec86e8562b21f5.

All four findings from my previous round plus the empty-store case are resolved, and I verified each by reproduction rather than by reading the diff. The sbx→msb asymmetry is the right call: sbx collects the new key at its own proxy prompt and genuinely never exposes it to the acq store, so declining to propagate is more honest than inventing a value.

No findings.

Reproductions, with fake values throughout:

  1. Stale-replay blocker — resolved. Store seeded OLD-STALE-KEY, then ACQ_BACKEND=sbx acq usai-rotate-api-key:

    acq: not propagating sbx USAi rotation to msb; sbx does not expose the
         rotated value to the acq store. Run 'acq --backend msb usai-rotate-api-key'
         to update existing msb sandbox(es).
    exit=0
    msb stub log: (empty — no `msb modify` at all)
    

    Previously this reported applied 'usai' to 1 running sandbox(es) and handed msb the stale key. I also checked the entry point I thought might bypass the new guard, since origin defaults to set: acq secret set -g usai on sbx. It is safe for a different reason — acq_backend_secret_set writes the acq store via acq_secret_set_interactive (acq.backends/sbx.sh:1045) before propagation runs, so the value msb receives is the new one, not a stale one. acq_backend_rotate_key is the only path that skips the store, and that is exactly the path the rotate origin covers.

  2. Sidecar host preservation — resolved. This was unverified by anyone before; I built the fixture. Stored usai with --host usai.alt.example.gov, with sbx holding a matching alt-host placeholder, then propagated from msb:

    sbx secret set-custom --host usai.alt.example.gov --env USAI_API_KEY --placeholder sbxph-alt999
    

    The alt host and its placeholder are both preserved; no rewrite to api.gsa.usai.gov. The sidecar at meta/acq.usai carries usai.alt.example.gov<TAB>USAI_API_KEY and propagation reads it.

  3. Non-USAi false failure — resolved. ACQ_BACKEND=msb acq secret set -g github with an sbx sandbox present and no sbx github secret: exit 0 at this head, matching base 1610835. The round-1 head exited 1 here. The fail-closed path still fires when an sbx secret actually would need overwriting — I saw that branch on the usai case.

  4. Attach-time expiry path — resolved. ensure_valid_key now calls acq_propagate_usai_rotation after a successful rotation (acq.backends/common.sh:2567-2568), and that wrapper passes rotate (acq:532-533), so the guard applies on the path weekly expiry actually drives. I traced every call site of acq_backend_rotate_key and of both propagation helpers; the only other propagation caller is the secret set dispatch at acq:1326, which correctly omits origin and gets the set default.

  5. Empty store — resolved. With msb sandboxes present and no resolvable store value, msb propagation returns 1 with a clear error (acq.backends/msb.sh:5424-5429) instead of the previous silent exit 0 with zero msb modify calls. With no sandboxes it returns 0 before probing, so the no-op case stays quiet.

Below the bar: the sbx→msb skip returns rc 0, so a scripted caller that only checks the exit status will not learn that msb sandboxes are still on the old key. The stderr guidance is clear and the active-backend rotation did succeed, so I would not change it — but a non-zero or distinct status would be friendlier to automation if this ever gets wrapped.

Validation. Ran: full suite ./scripts/test-acq-bats → 536 ok / 0 not ok / 2 skips (both skips are pwsh not available). Checked the four new tests against the prior head a8927da with the new test files copied over — all four fail there, so they are not vacuous:

not ok 4  rotate(sbx): propagation guard does not replay stale store value to msb
not ok 32 secret set(msb): non-usai sbx propagation is no-op when sbx has no secret
not ok 34 secret set(msb): usai propagation preserves sidecar host
not ok 35 secret propagate(msb): missing store value fails closed

Also ran bash -n acq acq.backends/*.sh scripts/rotate-apikey (clean), the repo ShellCheck batch per AGENTS.md (clean — shellcheck is genuinely present in my environment, so that is a real pass), git diff --check (clean), and gh pr checks 509 (all 7 pass). Secret hygiene: no value reaches argv on any path I exercised; ACQ_SECRET_TEST_VALUE is forwarded through the re-invocation but is a test-only store hook.

Not run: ./scripts/verify-backends, which needs a sandbox-capable host — so real cross-backend propagation against live msb and sbx remains unverified by me. Everything above is stubbed-backend evidence. Worth one live run on a capable host before relying on this operationally, given it touches secret material.

Verdict: approve.

Assisted-by: OpenCode [gpt_5_5_default_v2] change-review

@basiliskus
basiliskus merged commit 9641958 into main Oct 1, 2026
7 checks passed
@basiliskus
basiliskus deleted the fix/secret-rotation-all-backends branch October 1, 2026 21:26
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 <[email protected]>
wz-gsa pushed a commit to GSA-TTS/agentic-coding-patterns that referenced this pull request Oct 10, 2026
…#441)

## Summary

Adds `integrations/isolation/docs/scope-layers.md`: how global, team,
and personal `acq` kits stack through `ACQ_EXTRA_KITS`, a per-field
composition table, what belongs in each layer, authoring gotchas, one
list of backend differences, and the parser rules to check against your
`acq`.
Adds the copy-and-rename template `acq-kits/examples/team-kit/`: a valid
hybrid/v1 mixin with one live, harmless value per extension point, a
host-side `env.sh`, a sample ADR, README, TROUBLESHOOTING, and a
`scripts/verify` that asserts the composition live.
Second of three stacked PRs for #421:
#476, then this one, then the
`personal-kit` template.

**Reviewer ask: correct the composition table if any field composes
differently than described**, especially the rows marked "sbx: composed
natively". It was checked against acq v4.0.1
(GSA-TTS/agentic-coding-quickstart `1679530`), which includes the
`environment` and `publishedPorts[]` fixes
(GSA-TTS/agentic-coding-quickstart#519,
GSA-TTS/agentic-coding-quickstart#526) and adds the pinned OpenCode
agent kit between the global and team layers. The `volumes[]` and
`commands[]` rows describe current behavior while the stacked-kit
conflict rule is decided in GSA-TTS/agentic-coding-quickstart#533, which
names this PR as its trigger.

## 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

## Motivation

Every team adopting `acq` builds the same team layer between the global
kits and each repo's agent config, then a personal layer on top.
`ACQ_EXTRA_KITS` supports this, but nothing here said how stacked kits
compose, what belongs in each layer, or gave a validated starting point.
login.gov Team Data runs such a kit in production; this generalizes what
that team learned.

Parser and adapter defects the doc works around, stated once under
"Check against your acq" and filed upstream:

- a trailing `# comment` is kept in top-level fields and in
`commands[]`, where it skips the command or changes an argument
(GSA-TTS/agentic-coding-quickstart#558);
- `environment` values are cut at the first `#`, even inside quotes
(GSA-TTS/agentic-coding-quickstart#559);
- `acq kit validate` passes commands the translator skips or empties
(GSA-TTS/agentic-coding-quickstart#563);
- flow-style argv parses as an empty command
(GSA-TTS/agentic-coding-quickstart#560);
- on msb an empty file fails its drop, and a failed drop stops that
kit's `environment` and `commands[]`
(GSA-TTS/agentic-coding-quickstart#561).

OpenCode 2.x never loads `instructions` files (anomalyco/opencode#51341,
fix in anomalyco/opencode#51422, tracked in
#482), and the template delivers team
conventions that way. Which OpenCode runs depends on the image: the
agent kit installs 2.0.22, but an image that bakes 1.x puts it first on
`PATH`. Checked live on msb 0.7.7 with acq v4.0.1: acq's own template
image for `acq run opencode` (`sandbox-templates:opencode-docker`) runs
1.18.32 and `devenv-opencode:1.0.0` runs 1.18.23, so the conventions
load; `sandbox-templates:shell-docker` has no OpenCode of its own and
runs only 2.0.22, so they are ignored. `scripts/verify` now fails when
2.x is what runs, and did on `shell-docker`. The doc states this in
prose, and the permission sketch follows the global kit's default-allow
posture (ask before posting, deny merge/approve, pipeline runs, CI
variables, and API writes).

Not shipped: an optional `tui.jsonc` with `OPENCODE_TUI_CONFIG`. TUI
settings are mostly personal, a personal kit would replace the team file
whole, and `verify` could assert only that the variable and file exist.

## Testing

- [x] Ran `make validate` locally (all checks pass)
- [x] Ran `make test` (if applicable): 448 passed;
`scripts/tests/test_validate_kits_*.py`: 95 passed
- [x] Tested with at least one real scenario:
`examples/team-kit/scripts/verify` on msb 0.7.7 with acq v4.0.1: 20
passed, 0 failed, 1 skipped (the empty TEAM-SPECIFIC section). Not
re-run on sbx for this revision (the host's sbx session needed `sbx
login`); the doc no longer claims sbx observations.
- [x] Examples are concrete and realistic
- [x] Commit messages follow [Conventional
Commits](https://www.conventionalcommits.org/) format

Also run: `validate-kits.py --strict`, `acq kit validate` on the
template, `ruff check scripts/`, `ruff format --check scripts/`, `make
generate-check`, markdownlint, `shellcheck -s sh env.sh`, and `env.sh`
under sh, bash, zsh, and dash.

## 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 (the reference
implementation is named, not linked; examples use
`example.gov`/`example.org`)
- [x] No customer data or proprietary information
- [x] Pattern defines `prohibited_content` in output contract (if
applicable): not applicable, no SKILL.md pattern

## Quality Checklist

- [x] Plain language used (Grade 10 or below preferred)
- [x] Examples included
- [x] Prerequisites clearly stated

## Validation Results

```
make validate
✓ All validators passed
```

## Related Issues

Relates to #421 (followed by the
`personal-kit` template)
Depends on #476 (placement B)
Relates to GSA-TTS/agentic-coding-quickstart#533 (stacked-kit merge
rules for volumes and startup steps)
Relates to GSA-TTS/agentic-coding-quickstart#517 (symlinked `files[]`
sources on msb)
Relates to GSA-TTS/agentic-coding-quickstart#482 (acq's `~/.rc.d` login
hook; the personal-kit row relies on it)
Relates to #482 (OpenCode 2.x ignores
`instructions`)
Relates to GSA-TTS/agentic-coding-quickstart#557 (`acq configure` and
team kits), GSA-TTS/agentic-coding-quickstart#558,
GSA-TTS/agentic-coding-quickstart#559,
GSA-TTS/agentic-coding-quickstart#560,
GSA-TTS/agentic-coding-quickstart#561,
GSA-TTS/agentic-coding-quickstart#562,
GSA-TTS/agentic-coding-quickstart#563 (filed from this work)
Relates to GSA-TTS/agentic-coding-quickstart#500,
GSA-TTS/agentic-coding-quickstart#509,
GSA-TTS/agentic-coding-quickstart#519,
GSA-TTS/agentic-coding-quickstart#526

## Additional Context

- `scripts/verify` cannot run in this repo's CI (no nested sandboxes);
the offline gate is `validate-kits.py`.
- The template README links the pattern doc through the repository root
with the file path as text: a copied kit cannot follow relative links,
and `blob/main/…` links to files added here would fail link-check until
merge.

## Reviewer Notes

**Reviewers should verify:**

- [ ] The composition table matches acq's behavior on msb, and the rows
marked "sbx: composed natively" describe sbx correctly
- [ ] Backend differences and Check against your acq match what you see
on your acq
- [ ] Safety checklist items confirmed
- [ ] `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.

3 participants