Repository navigation
fix(secrets): propagate rotation across backends - #509
Conversation
Adversarial Review Follow-UpRan an adversarial review in a sub-agent focused on correctness, secret handling, shell portability, adapter contract consistency, and test gaps. Findings Addressed
New Commit
Verification
AI-assisted: yes |
basiliskus
left a comment
There was a problem hiding this comment.
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)
- Blocking: On the sbx→msb direction the propagation re-binds the stale store value and reports success. sbx's
acq_backend_rotate_keycollects the new key only at sbx's ownset-customprompt and never writes the acq secret store, but the new hook then calls msb'sacq_backend_secret_propagate, which resolves the value viaacq_secret_resolve(acq.backends/msb.sh:5216) and printsacq(msb): applied 'usai' to N running sandbox(es).with rc 0. Reproduced offline: store seeded withOLD-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 sawOLD-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
|
AI-assisted review (OpenCode), advisory — needs a human to confirm before it drives a change. Reviewed at head @basiliskus's blocking finding is correct, and I verified it independently by reading rather than reproducing. Three things it didn't cover. Should-fix: msb's propagate is silent when it applies nothing
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
The first two are legitimately nothing-to-do. The last two are failures. A user who runs Suggest separating the counters — Nit: the secret value crosses a process boundary as an environment variable
ACQ_PROPAGATING_SECRET=1 ACQ_SECRET_TEST_VALUE="${ACQ_SECRET_TEST_VALUE:-}" "$ACQ_RESOLVED_PATH" --backend "$be" secret _propagate …
I checked the exposure rather than assuming it: on Linux 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 Nit:
|
|
Reviewed at Intent still looks right for the reported msb→sbx case: a rotation or Findings (4)
Below the bar: none left from the earlier notes. The hardcoded sbx host, unconditional non-USAi failure, and missing Validation: local Verdict: comment / needs changes at Assisted-by: OpenCode [gpt_5_5_default_v2] |
Review Feedback AddressedPushed Changes
Adversarial ReviewRan 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
AI-assisted: yes |
|
Branch updated from Re-review requested. Post-merge verification:
Note: the merge commit was created with signing disabled for this single command because the configured SSH signing agent socket was unavailable ( AI-assisted: yes |
basiliskus
left a comment
There was a problem hiding this comment.
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:
-
Stale-replay blocker — resolved. Store seeded
OLD-STALE-KEY, thenACQ_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, sinceorigindefaults toset:acq secret set -g usaion sbx. It is safe for a different reason —acq_backend_secret_setwrites the acq store viaacq_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_keyis the only path that skips the store, and that is exactly the path therotateorigin covers. -
Sidecar host preservation — resolved. This was unverified by anyone before; I built the fixture. Stored
usaiwith--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-alt999The alt host and its placeholder are both preserved; no rewrite to
api.gsa.usai.gov. The sidecar atmeta/acq.usaicarriesusai.alt.example.gov<TAB>USAI_API_KEYand propagation reads it. -
Non-USAi false failure — resolved.
ACQ_BACKEND=msb acq secret set -g githubwith an sbx sandbox present and no sbx github secret: exit 0 at this head, matching base1610835. 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. -
Attach-time expiry path — resolved.
ensure_valid_keynow callsacq_propagate_usai_rotationafter a successful rotation (acq.backends/common.sh:2567-2568), and that wrapper passesrotate(acq:532-533), so the guard applies on the path weekly expiry actually drives. I traced every call site ofacq_backend_rotate_keyand of both propagation helpers; the only other propagation caller is thesecret setdispatch atacq:1326, which correctly omits origin and gets thesetdefault. -
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 zeromsb modifycalls. 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
🤖 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]>
…#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
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 sbxrotation.Plan / Changes
acq secret _propagatedispatch for adapter-only secret propagation.acq usai-rotate-api-keyandacq secret set [-g | SANDBOX] SERVICEto other installed backends after the active backend stores/rebinds the secret.acq_backend_secret_propagateto msb and sbx adapters.Verification
git submodule update --init test/vendor/bats-core test/vendor/bats-support test/vendor/bats-assert./scripts/test-acq-bats test/bats/77-usai-rotate.bats./scripts/test-acq-bats test/bats/40-secret-set.batsbash -n acq acq.backends/sbx.sh acq.backends/msb.sh scripts/rotate-apikeyshellcheck --severity=warning acq acq.backends/sbx.sh acq.backends/msb.sh test/bats/77-usai-rotate.batsgit diff --checkRollback
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