Repository navigation
Description
What happens
A kit startup step that should act only on a --clone workspace (write agent instructions into it, install a repo-local permission gate) needs a way to tell the disposable clone apart from a passthrough mount of the real host checkout, because writing into the latter pollutes the user's checkout. Neither backend gives the guest a deliberate signal for this, and the accidental signals differ per backend.
On sbx, --clone copies the repo into a guest block device: the workspace is an ext4 mount (/dev/vde), the host source is additionally mounted read-only at /run/sandbox/source, and sbx exports WORKSPACE_DIR, SANDBOX_NAME, and SANDBOX_VM_ID. Kits have been using findmnt -t ext4 as the discriminator.
On msb, the emulated --clone (ADR-0027) mounts the host-side scratch clone at the original path over virtiofs, exactly like a passthrough mount: no ext4 workspace, no /run/sandbox, and no workspace or sandbox environment variables at all. findmnt -t ext4 returns nothing, so the same step is a silent no-op on the default backend. The only remaining in-guest difference is the scratch's remote.origin.url being a host path, and #453 (correctly) removes that.
Net effect: a kit cannot be written backend-neutrally for "do X in the clone, never in the real checkout". The team-data kit's identity-devops injection (team AGENTS.md + a project-layer .opencode/opencode.jsonc that denies agent git commit/git push) is a no-op on msb for this reason, and its verify harness now carries it as a known gap.
Why this is acq's to fix
- ADR-0027 deliberately made the msb clone indistinguishable from a non-clone run so the agent's experience is identical. That is right for the agent, but it leaves kits with no signal, and msb --clone: scratch clone's origin is the host checkout path, so push and fetch in the guest target the clone itself #453 removes the last accidental one.
- ADR-0011 and ADR-0027 promise one kit vocabulary that behaves the same on both backends. A kit that works on sbx through an sbx implementation detail and silently no-ops on msb breaks that promise. When the no-op is a permission gate that denies agent commit and push, it is the failure mode acq exists to prevent.
- Only acq knows the answer. The clone decision is made on the host at create time; no guest-side heuristic can recover it once the origin URL is normalized.
Repro
acq main d2f4bdc, msb 0.6.17, sbx 0.42.1, macOS host, ACQ_IMAGE=ghcr.io/gsa-tts/agentic-coding-patterns/devenv-opencode:1.0.0.
$ acq create opencode /tmp/probe --clone --name probe-msb-clone
$ acq exec probe-msb-clone -- sh -c 'findmnt -rn -t ext4 -o TARGET; echo ---; findmnt -rn -o TARGET,FSTYPE -T "$PWD"; env | grep -E "^(ACQ|WORKSPACE|SANDBOX)"; ls /run/sandbox'
---
/tmp/probe virtiofs
ls: cannot access '/run/sandbox': No such file or directory
The same probe against a non-clone msb sandbox of the same path prints an identical virtiofs line (the only ext4 mount there is a kit volume, /nix). On sbx the clone probe prints /tmp/probe /dev/vde under ext4, /tmp/probe ext4 for the workspace, WORKSPACE_DIR=/tmp/probe, and /run/sandbox/source.
Proposed fix
Export two guest-visible, backend-neutral variables, set at create time on both backends:
ACQ_WORKSPACE=<guest path of the primary workspace>, always. This is the neutral counterpart of sbx'sWORKSPACE_DIR. The msb startup script (/.msb/scripts/acq-startup) never enters the workspace, so a kit cannot derive the path from cwd; on msb the path is the original host path, on sbx it is the in-guest clone path.ACQ_CLONE=1, only when the primary is a disposable clone. The name already exists host-side as the flag's env equivalent, peracq --help.
Keeping the two facts separate means a non-clone kit gets a neutral workspace path for free, and no second clone-specific path variable is needed.
Kits then replace the mount-type heuristic with [ "${ACQ_CLONE:-0}" = 1 ] && ws="$ACQ_WORKSPACE", which fails closed on both backends.
A marker file inside the scratch (for example .git/acq-clone) would also work but needs a .git/info/exclude entry and a post-create write on sbx's native clone, so the environment route is cleaner.
Delivery notes
- msb. Kit
environment[]reaches the guest today through per-exec-eflags and the persisted/var/lib/acq/kit-envfile; the ADR-0017 startup script gets neither unless the tokens are baked in at generation time.msb createhas a native--env K=Vflag that acq does not use yet. If it propagates intomsb execand attach sessions, it covers all three channels in one place and survives native restarts; probe that before choosing. If the variables ride the kit-env file instead,_acq_msb_reset_kit_envon a kit re-apply drops them and they must be re-added. - sbx. Add the two entries to the synthesized kit's
environment.variablesmap. - Docs. Add both names to the
environmentsection ofdocs/BACKEND_GUIDE.mdas kit contract, and amend ADR-0027 to record that the guest gets a deliberate clone marker. Otherwise the variables become the next accidental signal a kit depends on. - Kit side. The team-data verify harness should assert the marker is present on a
--clonesandbox. The guard above fails closed into exactly today's silent no-op, which is correct for safety but leaves a regression invisible without a presence check.
Sequencing
Land after #455 (fixes #453), since both touch the msb clone setup in acq.backends/msb.sh. Extend test/bats/116-clone-option.bats rather than adding a new file.
Related: #453, ADR-0027, ADR-0017.
AI-assisted: yes.
Activity
basiliskus commentedon Sep 9, 2026
One correction to the repro and a data point for the proposal.
msb does already persist the primary workspace path in the guest: the provisioner writes it to
/var/lib/acq/workspace(acq.backends/msb.sh, the "Record the guest workspace path too" block, added for name-only re-attach). It is a root-written file, not an environment variable, and it is written for every msb primary whether or not--clonewas passed. So theACQ_WORKSPACEhalf of the proposal has an existing source of truth on msb; the genuinely missing piece is the clone marker.That file must not be read as a clone signal. I tested a kit that used it as the msb-side clone discovery: in a non-clone sandbox of a checkout whose origin matched, the startup step wrote
AGENTS.md,.opencode/opencode.jsonc, and two.git/info/excludelines into the real host checkout through the passthrough mount. That is exactly the pollution the clone/non-clone distinction exists to prevent, and it is whyACQ_CLONEneeds to be a separate, clone-only fact rather than derivable from the workspace path.AI-assisted: yes.