Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
ef4a84a
test(acq): add offline provider adoption verification
mogul Oct 22, 2026
7ea0091
test(verify): prepare provider adoption validation
mogul Oct 22, 2026
9e59d9a
fix(msb): canonicalize host-config dir before --volume mount
mogul Oct 24, 2026
afabe26
fix(sbx): mount provider generation after agent
mogul Oct 24, 2026
636bc2f
test(sbx): assert provider root token position
mogul Oct 24, 2026
55dd201
fix(verify): consolidate live backend corrections
mogul Oct 24, 2026
0bdf2a7
test(verify): preserve live provider adoption evidence
mogul Oct 24, 2026
a9032d9
docs: record successful provider adoption matrix
mogul Oct 24, 2026
767f30b
test(msb): supply canonicalizer in adapter signal seam
mogul Oct 24, 2026
ed9b1ac
test(msb): retain projection seam in canonical mount test
mogul Oct 25, 2026
97270b5
fix(sbx): place provider mount after workspace
mogul Oct 25, 2026
1e5fca5
test(sbx): expect provider mount after workspace
mogul Oct 25, 2026
b15f4f6
test(backends): refresh create argv expectations
mogul Oct 25, 2026
e47e1e0
docs: record final provider-neutral backend matrix
mogul Oct 25, 2026
91f3d1a
docs: record literal MSB OpenCode credential flow
mogul Oct 25, 2026
47ac658
docs: record literal SBX OpenCode credential flow
mogul Oct 25, 2026
fb912d7
fix(provider-input): report malformed manifest errors
mogul Oct 25, 2026
6a9186a
docs: record malformed provider validation matrix
mogul Oct 25, 2026
580fe80
feat(providers): support explicit provider kit selection
mogul Oct 25, 2026
1b883f9
fix(providers): support empty selection under nounset
mogul Oct 25, 2026
1d243dc
docs: record authoritative empty provider matrix
mogul Oct 25, 2026
522c699
test(acq): refresh provider selection CI expectations
mogul Oct 25, 2026
f0bb5d6
test(msb): isolate external signal fixture state
mogul Oct 25, 2026
d9499e5
fix(sbx): move extra kit state to host
mogul Oct 25, 2026
384c3f6
test(verify): bind provider model check to the projected snapshot ide…
mogul Oct 26, 2026
dfb0aa2
docs(verify): use --no-provider-kits for the removal procedure; add p…
mogul Oct 26, 2026
479dfb3
fix(verify): avoid heredoc-in-command-substitution; tighten removal p…
mogul Oct 26, 2026
61631d9
fix(verify): fail the identity check when `opencode models` exits non…
mogul Oct 26, 2026
b8bb051
docs(verify): add an explicit merge gate; mark removal assertions pen…
mogul Oct 26, 2026
d9f2ce5
docs(verify): tighten merge gate wording per review
mogul Oct 26, 2026
ec8d5e1
Merge remote-tracking branch 'origin/main' into feat/601-provider-neu…
mogul Oct 26, 2026
761f706
docs(verify): record live validation results at ec8d5e17 and update t…
mogul Oct 27, 2026
3f7a44c
feat(sbx): trust host state for built-in kit heal; add MSB forged-mar…
mogul Oct 25, 2026
97775b4
fix(msb): decide readonly host-config mount from host-recorded create…
mogul Oct 25, 2026
7d480d0
docs: record accepted residual risk for msb passwd shell and sbx barrier
mogul Oct 25, 2026
b82613e
docs(adr): propose sbx trusted-startup parity options
mogul Oct 25, 2026
ac61280
docs(adr): map session-state acceptance to branch evidence
mogul Oct 25, 2026
e674b96
fix(msb): fail closed on ro-host-config disagreement and pin provisio…
mogul Oct 25, 2026
da798cd
docs: scope ADR-0036 sbx claims, soften ADR-0035 record, refine KFM 4…
mogul Oct 25, 2026
18cf1e3
fix(msb): distinguish absent vs writable ro host-config mount in prob…
mogul Oct 25, 2026
1e340bb
docs: correct msb version wording, probe outcomes, and sbx barrier to…
mogul Oct 25, 2026
2d6d671
fix(msb): hedge the writable-probe warning and rename a misleading test
mogul Oct 25, 2026
4c54263
fix(msb): fail closed when staging a readonly kit file after a record…
mogul Oct 26, 2026
a326cb7
docs(msb): align comment, error text and ADR wrap with the fail-close…
mogul Oct 26, 2026
8a59493
Merge remote-tracking branch 'origin/main' into feat/601-provider-neu…
mogul Oct 27, 2026
1a1f1d4
Merge remote-tracking branch 'origin/feat/601-provider-neutral-adopti…
mogul Oct 27, 2026
533cf60
fix(sbx,msb): derive kit state from host-observed facts and fail clos…
mogul Oct 27, 2026
7425b2b
docs: describe the barrier and the read-only mount probe as they now …
mogul Oct 27, 2026
6977143
docs(verify): record live validation at 8a594932 with SBX v0.48.0 and…
mogul Oct 27, 2026
b92549d
Merge remote-tracking branch 'origin/feat/601-provider-neutral-adopti…
mogul Oct 27, 2026
c87c8d0
docs(adr): renumber the sbx trusted-startup parity ADR to 0037
mogul Oct 28, 2026
9d3ef1b
fix(msb): stop gating setup on host records of guest-reported success
mogul Oct 28, 2026
09ee696
docs: describe the install and agent-user records as caches of a gues…
mogul Oct 28, 2026
8655f0a
fix(msb): trust the read-only host-config mount only on qualified ver…
mogul Oct 28, 2026
80cd22c
docs: state that read-only mount enforcement is unverified until the …
mogul Oct 28, 2026
dc43b25
fix(msb): classify remount as informational in the ro-mount canary; k…
mogul Oct 28, 2026
beb67aa
docs: explain why an accepted remount does not fail qualification; up…
mogul Oct 28, 2026
d774e22
feat(msb): qualify msb 0.7.7 for readonly kit files from the read-onl…
mogul Oct 28, 2026
978fa53
docs: record the msb 0.7.7 read-only mount qualification and its limits
mogul Oct 28, 2026
75528a0
feat(msb): gate trusted readonly kit files on msb's own mount record,…
mogul Oct 28, 2026
84827f1
docs: describe the msb-recorded read-only mount gate and what it does…
mogul Oct 28, 2026
468907f
style(test): end 84-msb-ro-mount-qualification.bats with a single new…
mogul Oct 28, 2026
4daf57f
feat(kits): generalize provider kits with trusted sources (#642)
mogul Oct 28, 2026
7f4dd08
Merge remote-tracking branch 'origin/feat/601-provider-neutral-adopti…
mogul Oct 28, 2026
5f89c45
style: satisfy CI hooks after the provider-kit squash
mogul Oct 28, 2026
d6a924c
fix(msb): bind provider credentials through the shared service-bindin…
mogul Oct 28, 2026
a33cddf
test(msb): update provider-kit fixtures for the data-driven default p…
mogul Oct 28, 2026
e1d96c3
Merge remote-tracking branch 'origin/feat/601-provider-neutral-adopti…
mogul Oct 28, 2026
4b51caf
fix(sbx): guard empty KIT_SOURCE_PREFIXES expansions for bash 3.2
mogul Oct 28, 2026
90788fc
Merge remote-tracking branch 'origin/feat/601-provider-neutral-adopti…
mogul Oct 28, 2026
0b31772
Merge remote-tracking branch 'origin/main' into feat/503-host-authori…
mogul Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions acq.backends/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2249,9 +2249,10 @@ acq_provenance_status() {
# Reapply the built-in bundle to an existing sandbox and, on success, refresh its
# host-side provenance record. Uses the backend's heal path
# (acq_backend_ensure_kits_applied) with ACQ_FORCE_KIT_REAPPLY=1 so a
# present-but-stale kit is actually re-applied (the sbx feature-probe would
# otherwise skip a kit that is present but built from an older ref). msb already
# re-applies all kits idempotently; the force flag is a no-op there. After a usai
# present-but-stale kit is actually re-applied (the sbx host builtin-kits record
# would otherwise skip a kit recorded as applied at an older ref; the force flag
# bypasses that record). msb re-applies all kits on every heal; there the force
# flag additionally bypasses the host run-once gate for install-phase commands. After a usai
# refresh the kit's own startup global-merge re-runs on next attach, so the
# refreshed model catalog reaches the config. State (sessions, secrets, unrelated
# config, project overrides) is preserved: this is an in-place kit reapply, never
Expand Down Expand Up @@ -4894,7 +4895,7 @@ acq_host_config_clear_instance_state() {
rm -f "$dir"/install-* "$dir"/agent-installed-* \
"$dir"/agent-user-ready \
"$dir"/workspace "$dir"/ssh-auth-sock "$dir"/generic-opencode \
"$dir"/extra-kits 2>/dev/null || true
"$dir"/extra-kits "$dir"/builtin-kits "$dir"/ro-host-config 2>/dev/null || true
return 0
}

Expand Down
322 changes: 290 additions & 32 deletions acq.backends/msb.sh

Large diffs are not rendered by default.

107 changes: 93 additions & 14 deletions acq.backends/sbx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,13 @@ _acq_backend_provision_unlocked() {
echo "acq(sbx): error: sandbox '$name' was created but provider inputs could not be published." >&2
return 1
fi
# Host state is fixed from what the host observed (this create succeeded with
# these kit flags), BEFORE the guest-written startup barrier is consulted. The
# barrier is a readiness signal a guest can satisfy early, so no record may
# depend on it. Clear stale same-name state first, then seed.
acq_host_config_clear_instance_state sbx "$name"
_acq_sbx_seed_extra_kit_record "$name"
_acq_sbx_seed_builtin_kit_record "$name"
acq_spin_start "Waiting for kit startup in '$name'"
if ! _acq_sbx_wait_for_startup_barrier "$name" "$_startup_barrier_token"; then
acq_spin_stop "Waiting for kit startup in '$name'"
Expand All @@ -761,7 +768,6 @@ _acq_backend_provision_unlocked() {
return 1
fi
acq_spin_stop "Waiting for kit startup in '$name'"
acq_host_config_clear_instance_state sbx "$name"
if ! acq_provenance_write sbx "$name" "$agent"; then
echo "acq(sbx): error: could not record provenance for '$name'." >&2
echo " Removing the new sandbox because safe reattach requires complete state." >&2
Expand All @@ -783,7 +789,6 @@ _acq_backend_provision_unlocked() {
fi
fi
acq_workspace_record_write sbx "$name" "$_primary_ws" || true
_acq_sbx_seed_extra_kit_record "$name"
# Backend parity (ADR-0030): sbx DELIVERS ~/.rc.d/*.sh via kit files[] but,
# unlike msb's .profile bridge, nothing SOURCES them at login. Write the same
# bridge here at create time so a kit-dropped snippet actually runs. This is a
Expand Down Expand Up @@ -828,7 +833,7 @@ acq_backend_provision() {
# host record (see acq_backend_ensure_kits_applied step 4). A guest marker is not
# a trust boundary because a passwordless-sudo guest can forge it. Recording the
# original ref once per line lets create and heal agree without trusting guest
# state. Built-in kits are tracked by feature-probe and are not listed here.
# state. Built-in kits are tracked separately (`builtin-kits`, below).
#
# ACQ_CLI_KITS (`--kit` on the CLI) are recorded for completeness even though the
# current sbx heal loop only re-drives ACQ_EXTRA_KITS. This is safe, not a
Expand All @@ -854,6 +859,74 @@ _acq_sbx_seed_extra_kit_record() {
echo "acq: warning: could not record extra kits for '$name'; re-attach may re-attempt them." >&2
}

# ---------------------------------------------------------------------------
# Host-authoritative built-in kit state (host config key `builtin-kits`)
# ---------------------------------------------------------------------------
# The re-attach heal decides whether the Zscaler CA, USAi config and playbook
# built-in kits need (re)applying from this host record, never from a guest
# `sbx exec` probe: a passwordless-sudo guest can create the probed files and so
# control the answer. One line per kit: "<kit-name> <pinned-ref>". Recording the
# ref makes a record from an older bundle stale (the ref no longer matches), so a
# bundle bump re-applies the kit instead of trusting the old record.
#
# Legacy default: a sandbox created before this record existed has no
# `builtin-kits` key. Absent or non-matching host state means "not recorded as
# applied", so the heal re-applies (fail toward re-apply, never toward skip). On
# sbx >= 0.38 that re-apply of a startup-bearing kit is refused with the
# recreate notice; the record is only written on success, so such a sandbox
# keeps prompting for recreate until it is recreated.

# True if KIT at REF is recorded as applied. Usage: ... NAME KIT REF
_acq_sbx_builtin_kit_recorded() {
local recorded
recorded=$(acq_host_config_read sbx "$1" builtin-kits)
grep -Fxq -- "$2 $3" <<<"$recorded"
}

# Record KIT at REF (replacing any older line for KIT). Usage: ... NAME KIT REF
_acq_sbx_builtin_kit_record() {
local name="$1" kit="$2" ref="$3" recorded kept
recorded=$(acq_host_config_read sbx "$name" builtin-kits)
kept=$(printf '%s\n' "$recorded" | awk -v k="$kit " 'NF && index($0, k) != 1')
acq_host_config_write sbx "$name" builtin-kits "${kept}${kept:+$'\n'}${kit} ${ref}" || \
echo "acq: warning: could not record built-in kit '$kit' for '$name'; re-attach may re-attempt it." >&2
}

# Create-time seed: the kits the heal consults, as passed to the `sbx create` that
# the host just observed succeed. This is the host-observable fact; it does not
# claim the guest's kit startup commands succeeded (the startup barrier is a guest
# file and is deliberately not an input here). Seeded before the barrier wait.
_acq_sbx_seed_builtin_kit_record() {
local name="$1" records
records="zscaler-ca-certificate ${ZSCALER_KIT}"
# The selected provider kit, as the heal resolves it (first selected ref). An empty
# selection records nothing; a selection that cannot be resolved records nothing, so
# a later heal re-checks rather than trusting a record acq could not form.
local _prov_refs _prov_ref
if _prov_refs=$(_acq_selected_provider_kit_refs 2>/dev/null); then
_prov_ref=$(printf '%s\n' "$_prov_refs" | head -n1)
[ -z "$_prov_ref" ] || records="${records}"$'\n'"${USAI_PROVIDER_KIT_NAME} ${_prov_ref}"
fi
records="${records}"$'\n'"agentic-coding-playbook ${PLAYBOOK_KIT}"
acq_host_config_write sbx "$name" builtin-kits "$records" || \
echo "acq: warning: could not record built-in kits for '$name'; re-attach may re-apply them." >&2
}

# Heal decision: 0 = apply, 1 = skip. Authority is the host record only. The
# guest probe (SNIPPET, as for _acq_sbx_kit_feature_absent) runs solely under
# ACQ_DEBUG as a non-authoritative diagnostic and never changes the decision.
# Usage: _acq_sbx_builtin_kit_needs_apply NAME KIT REF SNIPPET
_acq_sbx_builtin_kit_needs_apply() {
local name="$1" kit="$2" ref="$3" snippet="$4" _prc=0
[ "${ACQ_FORCE_KIT_REAPPLY:-0}" = "1" ] && return 0
_acq_sbx_builtin_kit_recorded "$name" "$kit" "$ref" || return 0
if [ -n "${ACQ_DEBUG:-}" ]; then
_acq_sbx_kit_feature_absent "$name" "$snippet" || _prc=$?
[ "$_prc" -eq 0 ] && acq_debug "heal(sbx): host records '$kit' applied but the guest probe reports it absent (diagnostic only; not re-applying)"
fi
return 1
}

# ---------------------------------------------------------------------------
# _acq_sbx_ensure_rc_bridge — write the login-profile rc.d sourcing bridge
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1240,10 +1313,11 @@ acq_backend_apply_kit() {
# ---------------------------------------------------------------------------
# acq_backend_ensure_kits_applied — heal a pre-kit sandbox in place
# ---------------------------------------------------------------------------
# Injects any ABSENT built-in kit. When ACQ_FORCE_KIT_REAPPLY=1 (set by
# acq_bundle_reapply for a stale-bundle refresh) it re-adds ALL
# built-in kits even when present, so a kit built from an older ref is actually
# refreshed — the feature-probe alone would skip a present-but-stale kit.
# Injects any built-in kit (Zscaler CA, USAi, playbook) not recorded as applied
# at its current pinned ref in the HOST config store (key `builtin-kits`); the
# guest is never consulted for that decision (see the helpers above). When
# ACQ_FORCE_KIT_REAPPLY=1 (set by acq_bundle_reapply for a stale-bundle refresh)
# it re-adds ALL built-in kits regardless of the record.
# Returns 0 only if every built-in kit is present-or-successfully-applied, so a
# caller can gate a provenance write on real success (never claim currency after
# a failed apply).
Expand All @@ -1266,7 +1340,7 @@ acq_backend_ensure_kits_applied() {
_acq_sbx_ensure_kit_sources_allowed

# Neutral kits must be translated to local sbx-v2 kit dirs before sbx kit add.
local provider_local="" playbook_local zscaler_local
local playbook_local zscaler_local
# The default provider is DATA (provider_kit), so heal resolves the selected
# ref instead of naming the USAi kit. An invalid selection FAILS CLOSED here:
# silently healing without the provider kit would leave a half-configured sandbox.
Expand All @@ -1287,21 +1361,25 @@ acq_backend_ensure_kits_applied() {
# makes an outbound HTTPS request. Behind a TLS-intercepting proxy (Zscaler),
# the USAi and playbook kits fail with a TLS 'unexpected eof' unless the
# intercepting CA is already trusted.
if [ "$force" = "1" ] || _acq_sbx_kit_feature_absent "$name" 'test -e /usr/local/share/ca-certificates/zscaler-ca.crt && echo present'; then
if _acq_sbx_builtin_kit_needs_apply "$name" zscaler-ca-certificate "$ZSCALER_KIT" 'test -e /usr/local/share/ca-certificates/zscaler-ca.crt && echo present'; then
echo "acq: '$name' is missing the Zscaler CA kit; injecting with 'sbx kit add'..." >&2
# `set -e`-safe: _acq_sbx_kit_add's non-zero returns (3=refusal, 1=other) are
# normal signalling — capture the status inline so the loop is not aborted.
_kadd_rc=0; _acq_sbx_kit_add "$name" "$zscaler_local" || _kadd_rc=$?
case $_kadd_rc in
0) healed=1; echo "acq: Zscaler CA kit injected into '$name'." >&2 ;;
0) healed=1; _acq_sbx_builtin_kit_record "$name" zscaler-ca-certificate "$ZSCALER_KIT"; echo "acq: Zscaler CA kit injected into '$name'." >&2 ;;
3) _acq_sbx_print_recreate_notice "$name"; ok=0 ;;
*) echo "acq: warning: 'sbx kit add' (Zscaler CA kit) failed for '$name' (see error above)." >&2; ok=0 ;;
esac
fi

# 2) Default provider kit
# 2) Default provider kit. The kit is DATA (the selected provider ref), and whether
# it needs applying is decided from the host `builtin-kits` record, never from a
# guest probe. The record slot keeps the historical USAi kit name as its key so
# records written by earlier acq versions still match; its ref is the selected
# provider ref, so changing the provider kit re-applies it.
if [ -n "$provider_ref" ] \
&& { [ "$force" = "1" ] || _acq_sbx_kit_feature_absent "$name" "test -f '$USAI_KIT_CONFIG_PATH' && echo present"; }; then
&& _acq_sbx_builtin_kit_needs_apply "$name" "$USAI_PROVIDER_KIT_NAME" "$provider_ref" "test -f '$USAI_KIT_CONFIG_PATH' && echo present"; then
echo "acq: '$name' is missing the provider kit; injecting with 'sbx kit add'..." >&2
_kadd_rc=0; _acq_sbx_kit_add "$name" "$provider_local" || _kadd_rc=$?
case $_kadd_rc in
Expand All @@ -1310,6 +1388,7 @@ acq_backend_ensure_kits_applied() {
sbx exec "$name" -- sh -c \
'f="$HOME/.config/opencode/opencode.jsonc"; if [ -L "$f" ] && [ ! -e "$f" ]; then rm -f "$f"; fi' \
</dev/null >/dev/null 2>&1 || true
_acq_sbx_builtin_kit_record "$name" "$USAI_PROVIDER_KIT_NAME" "$provider_ref"
echo "acq: provider kit injected into '$name'." >&2
;;
3) _acq_sbx_print_recreate_notice "$name"; ok=0 ;;
Expand All @@ -1323,11 +1402,11 @@ acq_backend_ensure_kits_applied() {
# NO .git. The symlink farm consumes AGENTS.md, so its presence is the
# delivery-agnostic signal; probing for .git reported the playbook "absent"
# forever on tarball-provisioned sandboxes.
if [ "$force" = "1" ] || _acq_sbx_kit_feature_absent "$name" 'test -e "$HOME/.agentic-coding-playbook/AGENTS.md" && echo present'; then
if _acq_sbx_builtin_kit_needs_apply "$name" agentic-coding-playbook "$PLAYBOOK_KIT" 'test -e "$HOME/.agentic-coding-playbook/AGENTS.md" && echo present'; then
echo "acq: '$name' is missing the playbook kit; injecting with 'sbx kit add'..." >&2
_kadd_rc=0; _acq_sbx_kit_add "$name" "$playbook_local" || _kadd_rc=$?
case $_kadd_rc in
0) healed=1; echo "acq: playbook kit injected into '$name'. Restart the agent to pick it up." >&2 ;;
0) healed=1; _acq_sbx_builtin_kit_record "$name" agentic-coding-playbook "$PLAYBOOK_KIT"; echo "acq: playbook kit injected into '$name'. Restart the agent to pick it up." >&2 ;;
3) _acq_sbx_print_recreate_notice "$name"; ok=0 ;;
*) echo "acq: warning: 'sbx kit add' (playbook kit) failed for '$name' (see error above)." >&2; ok=0 ;;
esac
Expand Down
Loading
Loading