Skip to content

fix(msb): thread the merged kit env onto kit lifecycle commands - #519

Merged
mogul merged 7 commits into
mainfrom
fix/msb-merged-kit-env-for-startup-commands
Oct 4, 2026
Merged

mogul merged 7 commits into
mainfrom
fix/msb-merged-kit-env-for-startup-commands

Conversation

@basiliskus

Copy link
Copy Markdown
Contributor

Summary

On the msb backend, a kit's lifecycle commands ran with only that kit's own environment[]. Interactive sessions already replayed the merged /var/lib/acq/kit-env marker, but commands started by a kit's lifecycle phase — especially background: true startup daemons — did not.

Consequence: the Paseo kit starts paseo daemon from its own startup command, so every agent Paseo later spawns inherited only the Paseo kit's env. A team kit's OPENCODE_CONFIG (team instructions plus a default-deny glab/gh permission layer) was silently missing from those agents, while GITLAB_TOKEN/GITHUB_TOKEN were 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:

path merged env source
acq_backend_provision pre-pass over every fetched spec, before the first apply
acq_backend_ensure_kits_applied same pre-pass; refs resolved in one loop, merged, applied in a second
acq_backend_apply_kit (mid-life) no full set → falls back to the persisted marker
neither the kit's own env (pre-change behavior), fail-soft

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 for GIT_TERMINAL_PROMPT. Naively widening the threaded env would let one kit's GIT_TERMINAL_PROMPT=1 disable 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_into now 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 set GIT_TERMINAL_PROMPT itself. A command can therefore only ever receive its own kit's guard value or the adapter's.

The create-time staged --script-path body got the same treatment. That path is inert today (ADR-0017: a bare --script-path registration 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 main rather 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

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Breaking change (fix or feature that would cause existing functionality to 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

  • I have read CONTRIBUTING.md
  • My commits follow the Conventional Commits format (e.g., feat:, fix:, docs:)
  • I have tested my changes locally
  • I have updated documentation as needed
  • I have not included any secrets, API keys, or sensitive information

Test Results

All run at 58b83de:

check command result
offline suite ./scripts/test-acq-bats 1..527, 527 ok / 0 not ok
shell lint printf '%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=warning rc 0, no findings
markdown lint npm run lint:md 50 files, 0 errors

Baseline at 187aa57 was 1..518. 527 = 518 + 9 new tests.

New offline coverage (test/bats/79-msb-merged-kit-env.bats, plus two staged-body tests in 73-msb-kits-startup.bats): merged env reaches a background: true daemon and every lifecycle phase; the merged env is identical in both kit application orders; duplicate names resolve last-kit-wins; one kit's GIT_TERMINAL_PROMPT does not strip another kit's guards; two kits declaring the same guard name with different values each keep their own; mid-life kit apply recovers 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

  • No live end-to-end run. No Docker/KVM was available, so the issue's original /proc/<paseo-daemon>/environ reproduction 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.
  • Duplicate -e precedence unverified against a live msb. When a kit declares its own GIT_ASKPASS, the command receives both it and acq's /bin/false. main emits 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 / gitleaks were 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.
  • Deliberately not fixed: a merged var can name a path a later kit has not staged yet (e.g. OPENCODE_CONFIG pointing 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 the sbx startup 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.
  • ADR numbering: 0033 was free at branch time, but open PRs claim 0030 (three ways), 0031 and 0032. May need renumbering on merge.

Review focus suggestions

_acq_msb_env_tokens_with_guards_into is the security-relevant core. The _builtin parallel-array bookkeeping in _acq_msb_heal_kit_set gates 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 for acq kit apply is a judgment call rather than a defect.

Note that the new tests assert recorded msb exec argv and generated script text. They prove acq constructs the intended commands; they do not prove msb executes 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-env marker'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_PROMPT value reach another kit's command and skip the askpass guards — a regression against main for that input); it was fixed in eb9284b and re-verified. A subsequent scope review led to the 58b83de trim, which removed ~156 lines of duplicated rationale with the code left provably byte-identical. Human review is still required before merge.

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
@basiliskus
basiliskus marked this pull request as ready for review September 30, 2026 17:13
@basiliskus
basiliskus requested a review from a team as a code owner September 30, 2026 17:13
@wz-gsa

wz-gsa commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

AI-assisted (OpenCode). Ran the live Docker/KVM verification this PR's own "Known gaps" section flagged as missing — real acq create on real msb 0.7.4, not stub-tested.

Test 1: merged env reaches a background: true daemon (the original #515 symptom)

Built two real test kits — a "team" kit declaring environment: {TEAM_MARKER: ...}, and a "daemon" kit with a background: true startup command spawning a long-running shell loop (mirroring Paseo's own daemon pattern). Applied both via acq create, then read the daemon's actual process environment directly from /proc/<pid>/environ — the exact repro mechanism from #515.

Both apply orders tested (team-then-daemon, and reversed daemon-then-team), since the PR description specifically calls out that a per-kit marker read inside the apply loop would be order-dependent:

# daemon's own /proc/<pid>/environ, team kit applied FIRST:
TEAM_MARKER=team-value-123
DAEMON_MARKER=daemon-value-456

# daemon's own /proc/<pid>/environ, REVERSED — daemon kit applied FIRST:
TEAM_MARKER=team-value-123
DAEMON_MARKER=daemon-value-456

Identical in both orders — the merge is genuinely order-independent, and the daemon process genuinely inherits the OTHER kit's env, not just its own.

Test 2: guard-ownership isolation (the regression a fresh-context reviewer caught in eb9284b)

Built a "prompter" kit declaring GIT_TERMINAL_PROMPT=1 as its own intentional override, and a "victim" kit declaring no guard override at all, both as background: true daemons. Tested both apply orders again.

# prompter daemon's own env (either order):
GIT_TERMINAL_PROMPT=1

# victim daemon's own env (either order):
GIT_TERMINAL_PROMPT=0
GIT_ASKPASS=/bin/false
SSH_ASKPASS=/bin/false

The victim kit's commands correctly keep acq's safe defaults regardless of apply order and regardless of the prompter kit's override — no leakage either direction. This directly confirms the fix for the exact failure mode _acq_msb_env_tokens_with_guards_into's own comment warns about.

Net

Both of this PR's self-reported "known gaps" (no live end-to-end run; duplicate--e-precedence inferred rather than observed) are now live-verified for the core merged-env and guard-isolation behavior — not just confirmed at the stubbed/argv-assertion level. I did not test the duplicate--e-flag-precedence-under-live-msb gap specifically (that one's about GIT_ASKPASS appearing twice in one -e list when a kit sets its own); happy to if useful.

Sandboxes created and removed as part of this verification; no persistent state left behind.

@mogul
mogul merged commit 2248a92 into main Oct 4, 2026
6 of 7 checks passed
@mogul
mogul deleted the fix/msb-merged-kit-env-for-startup-commands branch October 4, 2026 05:01
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 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.

msb: kit startup commands (incl. background daemons) get only their own kit's environment[], so daemon-launched agents lose other kits' config

3 participants