Skip to content

fix(acq): use sandbox workspace for github scoping - #476

Merged
mogul merged 5 commits into
mainfrom
fix/acq-github-scope-repo-list
Sep 30, 2026
Merged

mogul merged 5 commits into
mainfrom
fix/acq-github-scope-repo-list

Conversation

@mogul

@mogul mogul commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Context

acq github-scope SANDBOX previously defaulted the workspace path to the caller's current directory. That could suggest repositories unrelated to the sandbox when the command was run from a parent directory. The flow also printed one PAT URL per GitHub owner but accepted only one token, even though fine-grained PATs are single-owner and acq stores one sandbox-scoped GitHub token today.

Changes

  • Default pathless acq github-scope SANDBOX to the sandbox's recorded workspace instead of $PWD.
  • Persist the primary workspace path for new SBX and MSB sandboxes.
  • Add fallback workspace discovery for existing sandboxes:
    • SBX reads ACQ_WORKSPACE from the guest.
    • MSB reads the existing /var/lib/acq/workspace marker.
  • Refuse multi-owner GitHub workspaces with a clear explanation instead of asking for one token after showing several owner URLs.
  • Update help text, how-to docs, and ADR-0013 to document the one-owner constraint.
  • Add regression tests for multi-owner refusal, one-owner multi-repo support, and recorded-workspace lookup.
  • Isolate host git identity in an existing SBX heal test so its assertion remains focused on the playbook stale-probe behavior.

Verification

  • git diff --check passed.
  • npm run lint:md passed with 0 issues.
  • 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 passed.
  • ./scripts/test-acq-bats passed: 477/477.

Rollback

Revert commit c4a66c9 to restore the prior github-scope behavior.

Security Impact

Improves least-privilege GitHub token guidance by preventing accidental scoping to unrelated repositories and failing closed for unsupported multi-owner PAT flows. No secret values are logged or placed on argv. AI-assisted.

@mogul
mogul requested a review from a team as a code owner September 16, 2026 16:03
@wz-gsa

wz-gsa commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

AI-assisted review (OpenCode), advisory — needs a human to confirm before it drives a change. Reviewed at head c5c2672; also read #504 at a699af38.

The defect this fixes is real: scoping a token to whatever repos happen to sit in the caller's $PWD is wrong, and the multi-owner refusal is a good addition. Three things about the new fallback, one of which is a collision with #504 rather than a defect here.

1. It collides with #504, and the resolution matters for more than merge order.

#504 now carries ADR-0030 and its implementation, and commit 3 migrates the workspace marker off the guest:

-  ws=$({ _acq_msb_cli exec "$name" -u 0 -- sh -c 'cat /var/lib/acq/workspace …
+  ws=$(acq_host_config_read msb "$name" workspace | tr -d '\r\n')

Both PRs modify _acq_msb_workspace_for, so they will conflict. Worth flagging explicitly because the msb half of this PR's fallback chain reads a value ADR-0030 has just classified as guest-tamperable — whichever lands second should not reintroduce the guest read.

2. The sbx half is not covered by that migration (acq.backends/sbx.sh:572-574).

acq_backend_workspace_for() {
  sbx exec "$1" -- sh -c 'printf "%s" "${ACQ_WORKSPACE:-}"' </dev/null 2>/dev/null || true
}

I grepped #504's diff for acq_backend_workspace_for and ACQ_WORKSPACE — no hits in sbx.sh. So this read survives ADR-0030's migration. It takes a string from inside the sandbox and uses it as a host path: acq:1414 assigns it, acq:1425 passes it to github_scope_sandbox → detect_workspace_repos, which walks the path and enumerates remote.origin.url for every repo it finds, then drives the user through minting a fine-grained PAT for them.

Nothing validates it in between — no absolute-path check, no -d, and notably not preflight_workspace_path, which this file already applies on create (acq:901, acq:962) but not here. Given ADR-0030's own premise (the agent has passwordless sudo, so guest root-ownership is not a trust boundary), a prompt-injected agent can point this at any single-owner tree on the host and have the operator scope a token to repos the sandbox has no relationship to. The multi-owner refusal doesn't help — it only needs to pick a single-owner path.

Quoting is fine throughout; I checked every expansion. This is about scope, not injection.

3. The guest value is persisted as though it were host provenance (acq:1415).

gs_ws=$(acq_backend_workspace_for "$gs_sandbox")
[ -z "$gs_ws" ] || acq_workspace_record_write "${ACQ_RESOLVED_BACKEND}" "$gs_sandbox" "$gs_ws" || true

After one fallback, every later run takes the "recorded" branch and reports using recorded workspace for '<x>': <path> — wording that implies provision-time host provenance. There is no way to tell a host-derived record (sbx.sh:497, msb.sh:3201, both from validated host positionals) from a laundered guest one. This part is covered by neither #503's marker list nor #504.

Fail-closed would be to drop the guest fallback entirely: a sandbox with no record can simply require the explicit PATH argument the error at acq:1417-1422 already asks for. If it's kept, require absolute + -d + preflight_workspace_path, don't persist it, and say in the message that the value came from the guest unverified.

Smaller items: acq_workspace_record_write (common.sh:1330-1333) rewrites the record with awk -F= '$1 != "workspace"' then appends — a newline in the value injects an extra line that the filter then preserves permanently; and when the record does not yet exist the result is a file containing only workspace=…, with no schema=1/backend=. Also sbx exec auto-starts a stopped sandbox, so a pathless github-scope can boot a sandbox as a side effect of what reads like a lookup (inferred from the comment at sbx.sh:620-625; UNVERIFIED — I did not run sbx).

Method: static analysis at head c5c2672 plus reading #504's diff. No PR code executed.

@wz-gsa

wz-gsa commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

AI-assisted adversarial review (nexus-agents pr_review), advisory — needs a human to confirm before it drives a change. Note on panel coverage: 4 of 5 voters hit a tool-output parsing error this run (infra issue, not a finding), so only scope_steward returned a substantive vote. That vote is high-confidence (0.72) with a named, traced repro, so flagging it rather than discarding — but this is 1/5 coverage, not the usual 5/5.

High: msb records the guest workspace path where a host path is required (acq.backends/msb.sh:3261).

acq_workspace_record_write is documented as storing "the primary host workspace" mounted into a sandbox, and its consumer github_scope_sandbox → detect_workspace_repos "$ws" walks the host filesystem for git remotes. The two backends disagree on what they hand it:

  • sbx.sh passes $_primary_ws — a host path (the new dispatch test in this PR asserts exactly that, assert_output --partial "$pic" against a host tmp dir).
  • msb.sh:3261 passes $ACQ_MSB_GUEST_WORKSPACE — a guest-side path. This is confirmed by the sibling acq_backend_workspace_for in the same file, which filters that same value against /home/agent — i.e. it's already known to be a guest path elsewhere in this diff.

On msb — the default backend — acq github-scope SANDBOX (no path arg) will echo a guest path that doesn't exist on the host, detect_workspace_repos finds no git remotes there, and the command silently falls through to "no GitHub repos detected," skipping scoping entirely. That's the exact wrong/no-scope failure mode this PR sets out to fix — just moved from "reads $PWD" to "reads an unusable recorded path," and only on sbx does the new test actually exercise the fixed behavior (the new 30-dispatch-routing.bats case doesn't have an msb equivalent).

Suggested fix: either map guest→host before recording on msb (acq already knows the host mount path at provision time — that's what should be persisted), or have msb's provisioning path capture the host-side value the same way sbx does, rather than the guest env var.

Secondary, non-blocking (raised as conditions if the above is fixed, not separate blockers):

  • The sbx acq_backend_workspace_for fallback execs into the guest, reads $ACQ_WORKSPACE there, and persists that guest-reported value into host provenance — a guest-influenceable value shaping which host repos a PAT gets scoped to. Mitigated by the human-in-the-loop step (path is echoed, user picks repos in the GitHub UI), so medium-at-worst, but worth a comment noting the trust direction, and ideally a check that the resolved path is one acq itself mounted.
  • Owner-casing: mixed-case duplicates of the same real owner would inflate owner_count and cause a false multi-owner refusal. Fails closed, so not a security issue, just a usability rough edge — worth a tr to a canonical case before the dedup check.
  • No command-injection exposure found in the reviewed portion — paths/names only reach quoted arguments or printf, no eval in this diff's own code.
  • The 90-sbx-startup-kit.bats git-identity-isolation change in this PR is unrelated to this PR's stated subject (github-scope) and isn't explained in the PR description — probably an incidental fix bundled in, but worth a one-line callout so a future bisect doesn't have to guess why it's here.

No command-injection, no scope-creep, no over-engineering findings — the added helpers reuse the existing provenance-file/awk/mv machinery rather than introducing a new store. The core defect is specifically the msb/sbx path-domain mismatch above.

@mogul

mogul commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback and pushed commit 48a7aa0.

Changes since the review:

  • Removed the pathless github-scope guest fallback entirely; it now uses only host-side provenance and otherwise fails closed with the explicit WORKSPACE_PATH instruction.
  • Removed the now-unused backend acq_backend_workspace_for hooks so future paths do not accidentally reintroduce guest-derived host provenance.
  • Fixed MSB provisioning to record host-side workspace provenance, including the MSYS host path form, while leaving the guest /var/lib/acq/workspace marker for attach/run only.
  • Added workspace_source=host and made acq_workspace_record_read ignore unmarked legacy workspace records, so previously guest-derived records are not trusted.
  • Hardened workspace record writes against newline/CR injection and non-directory paths.
  • Added regressions for MSB host path recording, MSB --clone recording the original checkout rather than scratch, MSYS host-form provenance, legacy unmarked record rejection, and mixed-case owner handling.

Verification:

  • git diff --check
  • 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
  • npm run lint:md
  • ./test/vendor/bats-core/bin/bats test/bats/116-clone-option.bats
  • ./test/vendor/bats-core/bin/bats test/bats/117-msys-path-forms.bats
  • ./test/vendor/bats-core/bin/bats test/bats/125-github-scope.bats
  • ./test/vendor/bats-core/bin/bats test/bats/30-dispatch-routing.bats
  • ./scripts/test-acq-bats passed: 522/522.

Please re-review when you have a chance.

@basiliskus basiliskus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed at 48a7aa0.

I reviewed the current head with the change-review procedure, focusing on the credential-scoping path and the prior advisory findings. The risky guest-to-host fallback appears removed from the github-scope path: pathless acq github-scope now reads only host-marked provenance and fails closed when no trusted record exists.

  1. Minor: PR description is stale (PR body, no file line)

    The body still says this PR adds fallback workspace discovery where SBX reads ACQ_WORKSPACE from the guest and MSB reads /var/lib/acq/workspace. Current code no longer does that: acq github-scope reads acq_workspace_record_read and errors if no host-marked record exists, and acq_backend_workspace_for is gone. Please update the PR body before merge so the review/merge record does not describe the unsafe fallback that was removed.

Below the bar: the test/bats/90-sbx-startup-kit.bats git-identity isolation change is not directly part of github-scope, but it is small and test-isolation-only, so I would not block on it.

Validation note: I confirmed the working tree is at 48a7aa02064eb3a8c9ce3c35aacec1b111489aef, reviewed the diff from 907f4043d14d65e1b70fb62eef6b1d4d8dac4606, checked the prior advisory comments, ran git diff --check and ShellCheck locally with no output, and inspected gh pr checks 476, which shows all checks passing at this head. I did not locally run ./scripts/test-acq-bats or npm run lint:md in this read-only review session because they can create temp/cache artifacts; I relied on the passing GitHub checks for those.

Verdict: approve/comment at 48a7aa02064eb3a8c9ce3c35aacec1b111489aef.

Assisted-by: OpenCode [usai/gpt_5_5_default_v2] change-review

@mogul
mogul merged commit 1610835 into main Sep 30, 2026
7 checks passed
@mogul
mogul deleted the fix/acq-github-scope-repo-list branch September 30, 2026 16:52
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants