Skip to content

--clone: no guest-visible signal that a workspace is the disposable clone, so kits cannot safely target it on msb #456

Closed

Description

@basiliskus

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's WORKSPACE_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, per acq --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 -e flags and the persisted /var/lib/acq/kit-env file; the ADR-0017 startup script gets neither unless the tokens are baked in at generation time. msb create has a native --env K=V flag that acq does not use yet. If it propagates into msb exec and 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_env on a kit re-apply drops them and they must be re-added.
  • sbx. Add the two entries to the synthesized kit's environment.variables map.
  • Docs. Add both names to the environment section of docs/BACKEND_GUIDE.md as 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 --clone sandbox. 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

  1. self-assigned this
    on Sep 9, 2026
  2. basiliskus commented on Sep 9, 2026

    @basiliskus
    ContributorAuthor

    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 --clone was passed. So the ACQ_WORKSPACE half 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/exclude lines 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 why ACQ_CLONE needs to be a separate, clone-only fact rather than derivable from the workspace path.

    AI-assisted: yes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions