Skip to content

feat(acq): choose the host port per sandbox, and let --publish pin it - #526

Merged
mogul merged 8 commits into
mainfrom
bbogado/334-host-port-override
Oct 2, 2026
Merged

mogul merged 8 commits into
mainfrom
bbogado/334-host-port-override

Conversation

@basiliskus

@basiliskus basiliskus commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two parts, because the issue body's premise is wrong for msb and its second comment says so.

A (bug). acq's neutral publishedPorts parser defaulted an omitted host: to the guest port, so every sandbox from one kit requested the same host port. msb accepts the duplicate silently — the second parallel sandbox starts clean and is simply unreachable, while localhost:<port> serves the first one's service. Reproduced on main with msb 0.7.3 by the issue reporter.

B (feature). There was no run-time way to pick a host port, so parallel sandboxes could not be given predictable, distinct addresses without pinning host: in the kit spec — which reintroduces the collision for every sandbox from that kit.

What changed

  • An empty host column now means "backend chooses" and gets a free loopback port per sandbox. A set one is used, or fails the create — never silently substituted.
  • Contention is probed against the real host listener, not msb inspect. msb inspect reports the mapping msb was asked for and keeps reporting it even when another process owns the port, so it cannot answer "is this port available". That reporting gap is acq ports reports a create-time mapping as live when it only echoes declared config (stale forwarder holds the port) #520 and is not fixed here.
  • When the probe cannot run at all, the publish proceeds with one notice per provision — for an explicitly requested port as well as a chosen one. "acq did not check" is never allowed to look like "acq checked and it was free". Only a measured conflict fails the create.
  • Published-port records now accumulate across kits and are emitted once, deduped by guest port (last wins), mirroring the volumes union in ADR-0023. This also fixes a latent bug: two kits publishing the same guest port previously emitted two conflicting -p flags.
  • acq run/create --publish HOST:GUEST — repeatable, acq-owned, honored only before the -- separator, never forwarded to the backend CLI. It overrides both a host-less and a pinned kit entry.
  • A host port requested for more than one guest port in one create (two --publish pins, or a pin that reuses a kit's host:) fails the create. The host probe cannot catch this because nothing is bound until msb create runs. A chosen free port likewise skips any host port the create already maps.
  • Two explicit refusals instead of silence: sbx (which chooses the host port itself and exposes no knob) errors and points at acq ports NAME --publish; a re-attach, by AGENT PATH or by sandbox name, prints an ignored-flag note, matching --image and --clone.
  • The throwaway sandbox that checks a fresh USAi key publishes no ports, so it never contends with the real sandbox for a pinned host port.

Related Issues

Fixes #334

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 stating plainly: a kit that omits host: no longer gets host == guest. ADR-0014's original rule said it should, and that sentence is now marked superseded with an update note. sbx was already doing the new thing (its translator drops the host column and sbx-v2 keys on container), so this brings msb to sbx's semantics rather than inventing a third.

Checklist

  • I have read CONTRIBUTING.md
  • My commits follow the Conventional Commits format
  • 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 at fc2fb26, from CI:

check command result
offline suite (bash 3.2) ./scripts/test-acq-bats 1..553, 553 ok / 0 not ok
shell lint pre-commit shellcheck + shellcheck-bats hooks (xargs -r -n8 shellcheck --severity=warning) pass
markdown lint npm run lint:md pass

Baseline on main was 1..529. 553 = 529 + 24 new tests in test/bats/113-host-port-override.bats.

Coverage includes: no-host: default; two sandboxes from one kit get different host ports (the Part A regression); a pinned host: honored; a contended explicit port fails with no substitute anywhere in the argv; retry exhaustion; an unprobeable host reported exactly once across both the chosen and explicit branches, and separately through a kit-pinned host: and through --publish; two-kit dedupe to a single -p; --publish emits -p and never appears in backend argv; repeatable and =VALUE forms; overrides of both host-less and pinned kit entries; the -- separator both directions with a vacuity guard; sbx refusal before any sbx create; re-attach note on both re-attach forms with no msb create; a host port reused across guest ports (two pins, and a pin vs. a kit host:) failing the create; a chosen port skipping already-mapped host ports; the key-check sandbox publishing no ports even with pins and a kit host: present; and malformed values (6868, abc:6767, 6767:abc, 99999:6767, 0:6767, 06868:6767, missing value, duplicate guest).

New tests were verified to fail on pre-change source and pass after, with the tests held constant. Two pre-existing tests in 105-ports-background.bats asserted the old host == guest default; both were updated to the new contract rather than deleted.

ADR-0014 and ADR-0034 were linted manually (0 errors) since the gate's glob does not reach docs/adr/.

Additional Notes

Known gaps

  • Live run on msb 0.7.5 (macOS) at fc2fb26, through acq create with a local kit publishing guest 8765 and no host::

    • two sandboxes from that kit got different host ports (36676, 51570), each bound by msb on 127.0.0.1;
    • --publish 18765:8765 mapped and bound host 18765;
    • a second sandbox pinning 18765 failed with the "already in use" error, and no sandbox was created;
    • --publish 18766:80 --publish 18766:443 failed with the "requested for more than one guest port" error, and no sandbox was created.

    Not exercised live: a request served end-to-end through a published port (no guest listener was started), and scripts/verify-ports-live, which covers the post-hoc acq ports --publish path rather than create-time publish.

  • Race window. The probe is point-in-time: a port free at probe time can be taken before msb create binds. What msb does then is not something this change can promise — the evidence in hand is that msb accepts a duplicate host mapping without complaint, which is the very silence Part A exists to remove. ADR-0034 states this rather than claiming a loud failure.

  • /dev/tcp edge cases unexercised: TIME_WAIT, a firewall that drops loopback SYNs (no timeout wraps the connect), IPv6-only listeners, and a bash built without net redirections. The last degrades to unknown by design.

  • pre-commit / gitleaks were unavailable locally, so those hooks and the YAML/JSON and file-hygiene checks did not run; CI is the gate. Trailing whitespace and EOF newlines were checked manually on every touched file.

  • Some commits are unsigned. The sandbox's forwarded SSH agent had no keys, so commit.gpgsign could not be satisfied. Since this repo squash-merges, the landed commit carries the merger's signature and the unsigned intermediates never reach main.

  • ADR number 0034 may need renumbering: open PRs claim 0030 three ways, plus 0031, 0032, and 0033.

Review focus suggestions

_acq_msb_port_flags_from_records is where the cases meet: empty host (pick a free port) versus explicit host, the latter checked by _acq_msb_explicit_host_ok (reused within this create, busy on the host, or unprobeable). _acq_msb_probe_unknown_notice is shared by both branches under one once-per-provision guard — a re-inlined copy would reintroduce a double notice, which one test specifically pins. Whether the mid-life --publish semantics and the sbx refusal are the right calls is a judgment question rather than a defect.

Note the new tests assert recorded msb/sbx argv. They prove acq constructs the intended commands; they do not prove msb binds, rejects, or reports the ports correctly.

Rollback

Revert the squash commit. No state migration and no persisted-format change: the mapping is baked into the create argv, so existing sandboxes are unaffected either way and only sandboxes created after the change behave differently.

Security Impact

No change to authentication, secrets handling, or egress. The new untrusted-input surface is the --publish value, range-validated (digits-only and length-capped before any arithmetic, 1..65535, leading zeros refused) before it can reach a backend argv, per the SI-10 discipline ADR-0014 already applies to kit-declared ports. The probe connects only to 127.0.0.1 and opens no listener. Attack surface narrows slightly: a contended host port now fails loudly instead of yielding a mapping that silently reaches a different process's service — the exact condition that, in the field, presented as a provider 401 Authentication failed because a request reached a daemon outside the TLS-interception path.


AI-assisted: implemented by an AI agent under human direction, with each design decision approved by the human. Human review is still required before merge.

…guest

The neutral publishedPorts parser defaulted an omitted `host:` to the guest
port, so every sandbox created from one kit requested the SAME host port. msb
accepts the duplicate mapping silently, so a second parallel sandbox started
clean and was simply unreachable — the first one owned the port and served all
host traffic for it.

The host column is now emitted VERBATIM (empty when the kit did not declare
`host:`) by both the neutral and the deprecated sbx-only parser: "asked for a
specific host port" and "left the host port to the backend" are different
intents and only the consumer can act on the difference. The msb adapter then
picks a FREE loopback port per sandbox for an empty column, and FAILS the create
when an explicitly requested host port is already taken rather than silently
substituting another one. Contention is probed against the real host listener
(bash /dev/tcp on the literal IPv4 loopback), not against msb's declared
mapping; an unprobeable shell warns once and publishes anyway.

Published-port records now accumulate across kits and are emitted once after the
kit loop, deduped by guest port (last wins) — mirroring the volumes union, and
giving a later CLI override one place to compose.

sbx is unaffected: its translator never re-emitted the host column, so an empty
column is the neutral spelling of the behavior sbx already had.

Refs: #334
A kit's publishedPorts entry declares a guest port; acq picks the host side.
There was no way to say which host port at launch time, so parallel sandboxes
from one kit could not be given predictable, distinct addresses without editing
the kit spec (which would pin every sandbox to the same port).

`acq run/create --publish HOST:GUEST` (repeatable) now does that. The flag is
acq-owned and is stripped before dispatch — `msb create` has its own `-p`, which
the adapter synthesizes from kit records, and sbx has none — and, like the other
acq-owned flags, is honored only BEFORE the `--` separator, so an identical flag
meant for the inner agent passes through verbatim.

The override joins the published-port records after every kit's, so the guest-
keyed last-wins union makes it beat both a kit's pinned host port and the free
port acq would have chosen. A host port already in use fails the create; there is
no fallback to the post-hoc `msb ssh serve` + `ssh -L` tunnel, which reaches
guest loopback rather than the guest IP the create-time publish targets.

Both sides of HOST:GUEST are required: a bare port is ambiguous, and the guest
port belongs to the kit rather than to the launcher. Values are range-checked
before they reach a backend argv.

Two explicit refusals rather than silence:
  * on sbx, which assigns the host port itself and exposes no knob, the flag is
    an error naming `acq ports NAME --publish` as the post-hoc alternative;
  * on a re-attach, where the mapping is already fixed in the create argv, it
    prints the same "ignored when re-attaching" note as --image and --clone.

Refs: #334
ADR-0034 captures both halves of the change and the reasoning that is not
obvious from the code: why an absent host column must stay absent, why a
contended port fails instead of moving, why the probe measures the real host
listener rather than the backend's declared mapping, why "cannot tell" publishes
with a notice rather than guessing either way, and why --publish is refused on
sbx and on a re-attach.

CONCEPTS gains a user-facing "Published Ports" section (read the mapping with
`acq ports`, pin it with `--publish`, the two limits). BACKEND_GUIDE notes the
per-sandbox host port under its msb ports limitation. KNOWN_FAILURE_MODES #42
documents the symptom as a user would hit it — two sandboxes, one reachable —
with the recreate-to-fix step, since the mapping is baked into the create args.
The acq how-to's command table was missing `acq ports` entirely.

Refs: #334
… too

Review finding: the explicit-host branch treated only a `busy` verdict as
exceptional, so when the availability probe could not run at all, a kit's pinned
`host:` or a `--publish` override was emitted with rc 0 and no notice — the one
case where the caller NAMED the address, and therefore the case where a wrong
answer is most visible to them, was the silent one. It also contradicted the
visibility driver recorded in ADR-0034: a chosen port, a refused port, or an
"I could not tell".

The status is now captured once (no double probe) in a `case`, and `unknown`
reuses the same once-per-provision notice as the chosen-port branch, extracted
into _acq_msb_probe_unknown_notice so both branches share one implementation and
_acq_msb_port_flags_from_records stays within the 50-line limit. It still
proceeds rather than failing: the fail-loud rule is about measured contention,
not about a host acq cannot measure.

Flag position: the acq-owned run/create flags are recognized only AFTER the
subcommand. Placed before it they fall into the generic unknown-subcommand
passthrough and reach the backend CLI — pre-existing behavior shared by --clone
and --no-update-check, so this documents the restriction in the usage text and
ADR-0034 rather than making one flag inconsistent with its siblings. A
family-wide fix is tracked separately.

Docs corrections from the same review: ADR-0014's "when host is omitted it
defaults to guest" now carries a supersession note (its decision text is left
intact, per the repo's append-an-Update convention); ADR-0034 no longer promises
that msb fails loudly on a post-probe race, which the evidence in
KNOWN_FAILURE_MODES contradicts; and the tracker references that had landed in a
code comment and in ADR body prose are rewritten to state the fact and cite the
ADR, leaving the issue links only in the ADR's Links section.

Refs: #334
The flag-position entry carried a "to be filled in when filed" placeholder while
the issue did not exist yet; it now cites the filed issue, and the scaffolding
about who could post it is dropped — that was process chatter, not durable
content. The explanation of what the trap is and why it is tracked separately
from this change is unchanged.

Also records the lint-coverage gap found while writing this ADR: the markdownlint
glob is `docs/*.md`, so `docs/adr/` — this file included — is never checked, which
is why the ADRs in this change were linted by hand.

Refs: #334
Refs: #524
Refs: #525
…host ports

- The throwaway key-check sandbox now publishes no ports, so it no longer
  contends with the real sandbox for a --publish pin or a kit's pinned host:
  port (which failed its create and silently skipped the key check).
- A host port requested for more than one guest port in one create now fails
  loudly instead of handing msb two -p flags for it; a chosen free port also
  skips any host port the create already maps.
- `acq run NAME --publish ...` on an existing sandbox now says the flag is
  ignored, like the AGENT PATH re-attach path already did.

Refs: #526
The reused-host-port check pushed _acq_msb_port_flags_from_records past the
50-line function limit. Move the explicit-host checks (already mapped in this
create, busy on the host, unprobeable) into _acq_msb_explicit_host_ok; behavior
is unchanged.

Also mark the two array assignments in the key-check test as consumed by the
sourced provision (SC2034), matching the file's existing pattern.

Refs: #526
@basiliskus
basiliskus marked this pull request as ready for review October 1, 2026 18:00
@basiliskus
basiliskus requested a review from a team as a code owner October 1, 2026 18:00

@mogul mogul 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.

Adversarial review complete. I do not see a blocking correctness, security, or portability issue in the PR-only diff.

Review focus covered the host-port record parser, --publish extraction/validation and separator handling, msb create-time -p synthesis, explicit-port contention handling, same-create host-port reuse, sbx refusal, re-attach behavior, key-check suppression of publish flags, docs/ADR consistency, and the new regression tests.

Verification performed locally:

  • ./scripts/test-acq-bats test/bats/113-host-port-override.bats -> 24 passed
  • git diff --check origin/main...HEAD -> passed
  • ./scripts/test-acq-bats -> reached 468 passing tests before the 5-minute local timeout; no failures observed. CI shows the complete offline suite and lint checks green.

Below the bar: I filed #539 for the _ACQ_MSB_HOST_PROBE_NOTICE_SHOWN latch being initialized at source time rather than reset per acq_backend_provision. The normal CLI path provisions one user sandbox per process, and the key-check provision suppresses ports, so I do not think this should block merge.

AI-assisted review: performed by OpenCode as an AI agent under human direction.

@mogul
mogul merged commit 410f332 into main Oct 2, 2026
8 checks passed
@mogul
mogul deleted the bbogado/334-host-port-override branch October 2, 2026 23:47
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.

acq: allow choosing the HOST port for a published guest port at run/create time

2 participants