Repository navigation
fix(msb): thread the merged kit env onto kit lifecycle commands - #519
Conversation
A kit's lifecycle commands ran with only that kit's own environment[], so a `background: true` startup daemon launched agents that inherited none of the other kits' configuration. With a kit that sets OPENCODE_CONFIG (agent instructions plus a default-deny permission layer), daemon-launched agents ran without it while the injected credentials were still present — a silent loss of a policy control. Interactive sessions were already whole-guest (they replay the persisted /var/lib/acq/kit-env marker). Kit environment[] is now guest-wide for install/initFiles/startup too: every lifecycle command runs with the merged env of the full effective kit set, with the same last-value-wins resolution sessions use. The merged set is computed in a pre-pass over the whole kit set in the full-set paths (provision, heal), so it does not depend on kit application order — a per-kit marker read would have worked only when the env-declaring kit happened to be applied first. The mid-life `acq kit apply` path has no full set and falls back to the sandbox's already-persisted merged value; with neither available it degrades to the kit's own env. The non-interactive git guards stay scoped per kit: a guard var contributed by another kit is filtered out of a command's env and acq's own guard value used instead, so one kit setting GIT_TERMINAL_PROMPT cannot disable the guards for a different kit's commands (a prompting kit command blocks provision forever). The staged create-time --script-path body gets the same merged env for consistency. That path is inert today (a bare registration is not auto-run at boot), so it fixes no live symptom; it keeps the two paths from drifting so the ADR-0017 re-verification trap cannot resurface this bug. Known limitation, documented in the ADR: a merged var can now name a path a later kit has not staged yet, turning "no config" into "possibly missing config file" for that window. That is the startup-races-its-prerequisites class tracked as #506, not an env-scoping problem. Refs: #515
Review found a regression against base in the merged-env change: guard ownership was recorded as a set of NAMES, but _acq_msb_merge_kit_env_into has already collapsed a duplicate name to the LAST kit's value before the filter runs. When the owning kit had declared a guard name, the filter kept the merged token without knowing whose value it now held. With kit A declaring GIT_TERMINAL_PROMPT=0 and a later kit B declaring =1, kit A's own command ran with kit B's opt-out and with no GIT_ASKPASS/SSH_ASKPASS at all — where before the merged-env change it correctly got its own 0. One kit could make another kit's command prompt, and a prompting kit command blocks provision forever. Ownership is now the owning kit's guard TOKENS (NAME=value), carried by array name (a guard value may contain spaces and must never be re-split): _acq_msb_own_guard_tokens_into replaces _acq_msb_own_guard_names, and _acq_msb_env_tokens_with_guards_into strips EVERY guard-name token from the merged set regardless of contributor, re-adds this kit's own guard tokens, then appends acq's guard values unless this kit set GIT_TERMINAL_PROMPT itself. That closes both collision shapes — another kit declaring a guard name this kit does not, and another kit declaring the same name with a different value — while a kit's own intentional override still stands for its own commands. Applied to both the exec path and the staged --script-path body, which shared the defect. Also split the ADR-0033 lifecycle-command regressions out of 78-msb-kit-env.bats (which had grown to 553 lines) into a new 79-msb-merged-kit-env.bats. 78 keeps the marker/session-replay coverage it was written for; nothing pre-existing moved or was renumbered. Refs: #515
…mments Comment, doc and test trim only — no behavior change. Verified: with comments and blank lines removed, acq.backends/msb.sh is byte-identical to the previous commit, and the six guard/merge shapes (own-0-vs-later-1, own override =1, owns-no-guard, owns-GIT_ASKPASS-only, no env, plain merged) emit identical -e flags on both the exec and staged-script paths. ADR-0033: 172 -> 113 lines. Kept the problem statement, the order-independence trap, the per-path source table, guard-ownership-by-token with one worked example, the inert-`--script-path` caveat, and the deferred startup-races-its-prerequisites gap. Cut the implementation walkthrough (it duplicated ADR-0011, ADR-0017 and BACKEND_GUIDE), the Positive Consequences list (restated the decision), and compressed Drivers and Options. msb.sh: the merged-env rationale is now stated once, at _acq_msb_merge_kit_env_into (why it must be a pre-pass), and the guard-ownership rule once, at _acq_msb_env_tokens_with_guards_into; the other five sites point at those. Kept every comment recording a verified external fact or a past failure — the ADR-0017 staged-script inertness note, the prompting-kit-command hang, the loop-stdin-consumption and eval-by-name-collision notes. Comment delta vs base is now 138 added lines (was 183); code delta is unchanged at 161. Tests: folded "a kit's own guard override is not clobbered" into the value-collision test, keeping both its assertions (GIT_TERMINAL_PROMPT appears exactly once; the other kit's non-guard var still arrives). Dropped the helper-pinned _acq_msb_dedupe_env_into unit test and re-asserted its observable behavior through the public provision path instead: a value containing spaces and shell metacharacters crosses the merge as one -e token. Refs: #515
The mid-life `acq kit apply` path used the persisted kit-env marker whenever it was non-empty. The append of the kit's own entries only warns on failure, so a stale marker silently dropped the kit's own environment[] from its commands. Layer the kit's own entries after the persisted value instead. Kits with a backend_shortcuts.msb entry skip the generic apply, so their environment[] is never persisted for sessions; leave them out of the merged env so lifecycle commands and sessions agree on the guest env. Refs: #519
|
AI-assisted (OpenCode). Ran the live Docker/KVM verification this PR's own "Known gaps" section flagged as missing — real Test 1: merged env reaches a
|
…v-for-startup-commands
🤖 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>
…#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
Summary
On the
msbbackend, a kit's lifecycle commands ran with only that kit's ownenvironment[]. Interactive sessions already replayed the merged/var/lib/acq/kit-envmarker, but commands started by a kit's lifecycle phase — especiallybackground: truestartup daemons — did not.Consequence: the Paseo kit starts
paseo daemonfrom its own startup command, so every agent Paseo later spawns inherited only the Paseo kit's env. A team kit'sOPENCODE_CONFIG(team instructions plus a default-denyglab/ghpermission layer) was silently missing from those agents, whileGITLAB_TOKEN/GITHUB_TOKENwere still present. Silent, and security-relevant.This PR makes kit
environment[]guest-wide for lifecycle commands, with the same last-value-wins ordering sessions already use, and pins guard ownership per kit.Design
A pre-pass over the full effective kit set computes the merged env before any kit is applied:
acq_backend_provisionacq_backend_ensure_kits_appliedacq_backend_apply_kit(mid-life)Why a pre-pass and not a marker read inside
_acq_msb_run_commands. The marker is appended inside the same per-kit apply loop that runs commands, so a per-kit read is correct when the env-declaring kit happens to be applied first and silently wrong in the reverse order. Order-independence is asserted in both directions.Guard ownership is by token, not by name. The non-interactive git guards (
GIT_TERMINAL_PROMPT=0,GIT_ASKPASS=/bin/false,SSH_ASKPASS=/bin/false) were previously decided by matching the kit's own env forGIT_TERMINAL_PROMPT. Naively widening the threaded env would let one kit'sGIT_TERMINAL_PROMPT=1disable guards for every kit's commands — the adapter's own comment records that a prompting kit command once hung provision indefinitely._acq_msb_env_tokens_with_guards_intonow strips all guard-name tokens from the merged set unconditionally, re-adds only the owning kit's, then appends acq's defaults unless that kit setGIT_TERMINAL_PROMPTitself. A command can therefore only ever receive its own kit's guard value or the adapter's.The create-time staged
--script-pathbody got the same treatment. That path is inert today (ADR-0017: a bare--script-pathregistration is staged on the guest PATH, never auto-run at boot), so this is consistency work, not part of the reported symptom — it keeps the ADR-0017 version-bump re-verification note from becoming a live bug later.Related Issues
Fixes #515
Conflict note: #504 (ADR-0030, host-authoritative config) rewrites the same kit-env functions and test files, replacing the guest marker with a host-authoritative store. This PR was deliberately built on current
mainrather than waiting. The marker read is isolated in one named helper (_acq_msb_persisted_kit_env_into) that #504 can repoint, but merge work is expected.Type of Change
Behavior change worth calling out explicitly: a kit's
environment[]now reaches other kits' lifecycle commands. That is the intent, and it widens what an extra or personal kit can influence. Recorded in ADR-0033.Checklist
feat:,fix:,docs:)Test Results
All run at
58b83de:./scripts/test-acq-bats1..527, 527 ok / 0 not okprintf '%s\n' acq acq.backends/*.sh scripts/test-acq-lib.sh scripts/test-acq-bats test/bats/*.bats test/bats/helper.bash | xargs -r -n8 shellcheck --severity=warningnpm run lint:mdBaseline at
187aa57was1..518. 527 = 518 + 9 new tests.New offline coverage (
test/bats/79-msb-merged-kit-env.bats, plus two staged-body tests in73-msb-kits-startup.bats): merged env reaches abackground: truedaemon and every lifecycle phase; the merged env is identical in both kit application orders; duplicate names resolve last-kit-wins; one kit'sGIT_TERMINAL_PROMPTdoes not strip another kit's guards; two kits declaring the same guard name with different values each keep their own; mid-lifekit applyrecovers merged env from the marker; the heal path merges with the daemon kit applied first; the staged body carries merged env with per-kit guards.Each new test was verified to fail before the change and pass after, with the implementation reverted and the tests held constant.
Additional Notes
Known gaps
/proc/<paseo-daemon>/environreproduction was never executed. Everything is verified at the adapter/argv and staged-body level against the stubbed suite. Per this repo's periodic-re-verification practice, this should be confirmed on a sandbox-capable host before the issue is considered closed.-eprecedence unverified against a livemsb. When a kit declares its ownGIT_ASKPASS, the command receives both it and acq's/bin/false.mainemits the identical order for that input, so this is preserved rather than introduced — but that acq's value wins by flag order is inferred from flag order, not observed at runtime.pre-commit/gitleakswere not available locally, so those hooks and the YAML/JSON and file-hygiene checks were not executed; they must pass in CI. Trailing whitespace and EOF newlines were verified manually on every touched file.OPENCODE_CONFIGpointing at a file the team kit copies later), converting "no team config" into "possibly broken team config" for that window. This is an ordering/barrier decision, not an env-scoping one, and is the same class as thesbxstartup race in fix(sbx): acq run does not wait for kit startup-phase config writes before attach, causing a stale-config race #506. Recorded in ADR-0033's consequences.Review focus suggestions
_acq_msb_env_tokens_with_guards_intois the security-relevant core. The_builtinparallel-array bookkeeping in_acq_msb_heal_kit_setgates the provenance write that claims a sandbox carries the current pinned bundle, so a two-loop split there is worth a look. Whether the mid-life marker fallback is the right semantics foracq kit applyis a judgment call rather than a defect.Note that the new tests assert recorded
msb execargv and generated script text. They prove acq constructs the intended commands; they do not provemsbexecutes them with the intended final environment.Rollback
Revert the three commits, or revert the branch merge. No state migration, no persisted format change: the
/var/lib/acq/kit-envmarker's format and write path are unchanged, so an existing sandbox is unaffected by a revert and keeps working with either version of the adapter.AI-assisted: implemented, reviewed and trimmed by AI agents under human direction, with the human approving each step. An independent fresh-context review agent found one major defect (guard ownership tracked by name, which let a later kit's
GIT_TERMINAL_PROMPTvalue reach another kit's command and skip the askpass guards — a regression againstmainfor that input); it was fixed ineb9284band re-verified. A subsequent scope review led to the58b83detrim, which removed ~156 lines of duplicated rationale with the code left provably byte-identical. Human review is still required before merge.