Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions acq
Original file line number Diff line number Diff line change
Expand Up @@ -1303,11 +1303,11 @@ case "$subcommand" in
[ -n "$ws" ] || ws="$PWD"
warn_if_no_git_identity "$ws"

# opencode (since v1.15.1) fetches its binary in a postinstall step that
# Legacy/custom opencode images can fetch their binary in a postinstall step that
# some sandbox images skip, so a fresh launch fails with "postinstall
# script was not run". Run it here — on any backend — before attaching an
# opencode agent. Best-effort; never blocks the run.
if [ "$first" = "opencode" ]; then
if [ "$first" = "opencode" ] && [ "${ACQ_DEFAULT_OPENCODE_GENERIC:-}" != "1" ]; then
ensure_opencode_postinstall "$local_name"
fi

Expand Down
4 changes: 4 additions & 0 deletions acq.backends/agents.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
# shellcheck disable=SC2034
ACQ_KNOWN_AGENTS=" claude codex copilot cursor docker-agent droid gemini kiro opencode shell "

# Derived from ghcr.io/gsa-tts/agentic-coding-patterns/devenv-shell:2.0.0 on
# 2026-10-14; covers published Linux amd64 and arm64 manifests.
ACQ_DEFAULT_OPENCODE_DEVENV_IMAGE="ghcr.io/gsa-tts/agentic-coding-patterns/devenv-shell@sha256:7b923fb5e722ad27c751ead836f24145c4cea3fac015a3ba753bd7473891e1e7"

acq_known_agents() {
printf '%s\n' claude codex copilot cursor docker-agent droid gemini kiro opencode shell
}
Expand Down
19 changes: 14 additions & 5 deletions acq.backends/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -904,8 +904,9 @@ _acq_selected_support_kit_refs() {
_acq_selected_builtin_kit_refs() {
local agent="${1:-}" kit
_acq_selected_support_kit_refs || return 1
if [ "${#ACQ_CLI_KITS[@]}" -eq 0 ] && [ -n "$agent" ] \
&& acq_is_known_agent "$agent" && acq_agent_builtin_kit_ready "$agent"; then
if [ -n "$agent" ] && acq_is_known_agent "$agent" \
&& { [ "${#ACQ_CLI_KITS[@]}" -eq 0 ] || [ "${ACQ_REQUIRED_AGENT_KIT:-}" = "$agent" ]; } \
&& acq_agent_builtin_kit_ready "$agent"; then
kit=$(_acq_agent_builtin_kit_ref "$agent") || return 1
printf '%s\n' "$kit"
fi
Expand Down Expand Up @@ -946,8 +947,16 @@ _build_kit_list() {
done < <(_acq_selected_support_kit_refs)
# shellcheck disable=SC2034 # read by adapters/tests after _build_kit_list
ACQ_BUILTIN_SUPPORT_KIT_COUNT="${#KITS[@]}"
if [ "${#ACQ_CLI_KITS[@]}" -eq 0 ] && [ -n "$agent" ] \
&& acq_is_known_agent "$agent" && acq_agent_builtin_kit_ready "$agent"; then
if [ -n "${ACQ_REQUIRED_AGENT_KIT:-}" ] \
&& [ "${ACQ_REQUIRED_AGENT_KIT:-}" = "$agent" ] \
&& ! acq_agent_builtin_kit_ready "$agent"; then
echo "acq: required built-in ${agent} kit is unavailable or has invalid agent metadata." >&2
echo " Refusing the generic default image because it has no bundled ${agent} binary." >&2
return 1
fi
if [ -n "$agent" ] && acq_is_known_agent "$agent" \
&& { [ "${#ACQ_CLI_KITS[@]}" -eq 0 ] || [ "${ACQ_REQUIRED_AGENT_KIT:-}" = "$agent" ]; } \
&& acq_agent_builtin_kit_ready "$agent"; then
kit=$(_acq_agent_builtin_kit_ref "$agent") || return 1
KITS+=("$kit")
fi
Expand Down Expand Up @@ -2442,7 +2451,7 @@ acq_host_config_clear_instance_state() {
case "$dir" in ""|/|/*/) return 0 ;; esac
rm -f "$dir"/install-* "$dir"/agent-installed-* \
"$dir"/agent-user-ready \
"$dir"/workspace "$dir"/ssh-auth-sock 2>/dev/null || true
"$dir"/workspace "$dir"/ssh-auth-sock "$dir"/generic-opencode 2>/dev/null || true
return 0
}

Expand Down
18 changes: 15 additions & 3 deletions acq.backends/msb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ MSB_ROLLBACK_VERSION="0.6.18"
# duplicate snapshots exist. See ADR-0032.
MSB_FIXED_VERSION="0.7.3"

# Default OCI image for provisioned sandboxes. We default to the SAME image
# Default OCI image for provisioned non-OpenCode sandboxes. We default to the SAME image
# family sbx uses: `docker/sandbox-templates:shell-docker`, an Ubuntu-based
# agent template. It ALREADY ships the non-root `agent` user (with passwordless
# sudo), node, git, curl, and ca-certificates (the four kits' runtime
Expand Down Expand Up @@ -192,7 +192,7 @@ _ACQ_MSB_IMAGE_NOTICE_SHOWN=0

# _acq_msb_resolve_image AGENT — set the OCI image `msb create` should use,
# applying the ADR-0022 precedence: explicit ACQ_MSB_IMAGE > neutral
# --image/ACQ_IMAGE > agent-derived sandbox-template image > built-in default.
# --image/ACQ_IMAGE > generic OpenCode base > agent-derived sandbox-template image > built-in default.
# Prints a one-time notice if BOTH the backend var and the neutral image are set
# (backend var wins). Idempotent notice (once per process).
_ACQ_MSB_RESOLVED_IMAGE=""
Expand Down Expand Up @@ -221,6 +221,11 @@ _acq_msb_resolve_image() {
_ACQ_MSB_RESOLVED_IMAGE_SOURCE="neutral"
return 0
fi
if [ "$agent" = "opencode" ]; then
_ACQ_MSB_RESOLVED_IMAGE="$ACQ_DEFAULT_OPENCODE_DEVENV_IMAGE"
_ACQ_MSB_RESOLVED_IMAGE_SOURCE="generic-opencode-default"
return 0
fi
if _ACQ_MSB_RESOLVED_IMAGE=$(acq_agent_template_image "$agent" 2>/dev/null) \
&& [ "$_ACQ_MSB_RESOLVED_IMAGE" != "$_ACQ_MSB_DEFAULT_IMAGE" ]; then
_ACQ_MSB_RESOLVED_IMAGE_SOURCE="agent-default"
Expand Down Expand Up @@ -3108,6 +3113,13 @@ acq_backend_provision() {
_acq_msb_resolve_image "$agent"
local _msb_image="$_ACQ_MSB_RESOLVED_IMAGE"
local _msb_image_source="$_ACQ_MSB_RESOLVED_IMAGE_SOURCE"
if [ "$_msb_image_source" = "generic-opencode-default" ]; then
ACQ_REQUIRED_AGENT_KIT="opencode"
ACQ_DEFAULT_OPENCODE_GENERIC=1
else
ACQ_REQUIRED_AGENT_KIT=""
ACQ_DEFAULT_OPENCODE_GENERIC=""
fi

# Optional pull policy (ACQ_MSB_PULL): forwarded to `msb create --pull`.
# `msb create` reads an image REF and by default pulls if-missing from a
Expand Down Expand Up @@ -3140,7 +3152,7 @@ acq_backend_provision() {
# Zscaler CA trust FIRST so later network-fetching kits (playbook clone, USAi
# validation) succeed behind a TLS-intercepting proxy (e.g. Zscaler).
local kitref kitdir
_acq_msb_select_kits "$agent"
_acq_msb_select_kits "$agent" || return 1
local kits=("${KITS[@]}")

for kitref in "${kits[@]}"; do
Expand Down
148 changes: 139 additions & 9 deletions acq.backends/sbx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,16 @@ _acq_sbx_wait_for_startup_barrier() {
done
}

_acq_sbx_clear_opencode_launch_mode() {
local name="$1"
acq_host_config_clear sbx "$name" generic-opencode
if acq_host_config_has sbx "$name" generic-opencode; then
echo "acq(sbx): error: could not clear stale OpenCode launch mode for '$name'." >&2
echo " Refusing to create because reattach could select stale SBX grammar." >&2
return 1
fi
}

# Probe a feature inside a sandbox. Returns 0=absent, 1=present, 2=probe failed.
# IMPORTANT: `snippet` MUST end with " && echo present" — e.g.:
# "test -f '/path/to/file' && echo present"
Expand Down Expand Up @@ -510,6 +520,14 @@ acq_backend_provision() {
if command -v acq_resolve_neutral_image >/dev/null 2>&1; then
_neutral_image=$(acq_resolve_neutral_image)
fi
if [ "$agent" = "opencode" ] && [ "$_user_template" -eq 0 ] && [ -z "$_neutral_image" ]; then
_tf=(--template "$ACQ_DEFAULT_OPENCODE_DEVENV_IMAGE")
ACQ_REQUIRED_AGENT_KIT="opencode"
ACQ_DEFAULT_OPENCODE_GENERIC=1
else
ACQ_REQUIRED_AGENT_KIT=""
ACQ_DEFAULT_OPENCODE_GENERIC=""
fi
if [ -n "$_neutral_image" ]; then
if [ "$_user_template" -eq 1 ]; then
echo "acq(sbx): both --image ('$_neutral_image') and an explicit --template were given;" >&2
Expand All @@ -527,6 +545,26 @@ acq_backend_provision() {
fi
fi

# SBX derives and injects its native agent kit from the positional agent token
# even when --template selects a generic image. The generic OpenCode path adds
# the pinned OpenCode kit itself, so pass `shell` to SBX to avoid a duplicate
# native `opencode` kit. Keep $agent for acq provenance and attach behavior.
local _sbx_args=("${_stripped[@]}") _arg _prev="" _idx
if [ "${ACQ_DEFAULT_OPENCODE_GENERIC:-}" = "1" ]; then
for _idx in "${!_sbx_args[@]}"; do
_arg="${_sbx_args[$_idx]}"
if _takes_value "$_prev"; then
_prev="$_arg"
continue
fi
case "$_arg" in
-*) _prev="$_arg"; continue ;;
esac
_sbx_args[$_idx]=shell
break
done
fi

# Neutral --clone (ADR-0027): acq owns the flag, so re-inject sbx's native
# `--clone` here (the acq dispatch stripped it and exported ACQ_CLONE).
local _cf=()
Expand All @@ -547,6 +585,7 @@ acq_backend_provision() {
[ "${ACQ_CLONE:-0}" = "1" ] && _ef+=(--env ACQ_CLONE=1)
fi

_build_kit_list "$agent" || return 1
local kf=()
while IFS= read -r line; do kf+=("$line"); done < <(_acq_sbx_kit_flags)
local _git_identity_kit=""
Expand All @@ -556,9 +595,19 @@ acq_backend_provision() {
_startup_barrier_token="acq-$$-$(date +%s)-$RANDOM-$RANDOM"
kf+=(--kit "$(_acq_sbx_startup_barrier_kit "$_startup_barrier_token")")

acq_debug "sbx create --name $name ${_cf[*]:-} ${_ef[*]:-} ${_tf[*]:-} ${kf[*]} ${_stripped[*]:-}"
acq_debug "sbx create --name $name ${_cf[*]:-} ${_ef[*]:-} ${_tf[*]:-} ${kf[*]} ${_sbx_args[*]:-}"
# The name-keyed host record may outlive a sandbox removed outside acq. Clear
# OpenCode's routing mode before create, and refuse to create if it persists.
# A later write failure then leaves no stale mode that could enable reattach.
if [ "$agent" = "opencode" ]; then
if acq_backend_exists "$name"; then
echo "acq(sbx): error: sandbox '$name' already exists; refusing to replace its launch mode." >&2
return 1
fi
_acq_sbx_clear_opencode_launch_mode "$name" || return 1
fi
acq_spin_start "Creating sandbox '$name'"
sbx create --name "$name" ${_cf[@]+"${_cf[@]}"} ${_ef[@]+"${_ef[@]}"} ${_tf[@]+"${_tf[@]}"} "${kf[@]}" ${_stripped[@]+"${_stripped[@]}"}
sbx create --name "$name" ${_cf[@]+"${_cf[@]}"} ${_ef[@]+"${_ef[@]}"} ${_tf[@]+"${_tf[@]}"} "${kf[@]}" ${_sbx_args[@]+"${_sbx_args[@]}"}
local _rc=$?
acq_spin_stop "Creating sandbox '$name'"
# Record host-side bundle provenance ONLY after a successful create — a failed
Expand All @@ -575,7 +624,20 @@ acq_backend_provision() {
return 1
fi
acq_spin_stop "Waiting for kit startup in '$name'"
acq_host_config_clear_instance_state sbx "$name"
acq_provenance_write sbx "$name" "$agent" || true
if [ "$agent" = "opencode" ]; then
# Reattach grammar is determined by the create-time SBX agent token. Persist
# it host-side so a missing record never guesses between shell and OpenCode.
local _opencode_mode=legacy-opencode
[ "${ACQ_DEFAULT_OPENCODE_GENERIC:-}" = "1" ] && _opencode_mode=generic-opencode
if ! acq_host_config_write sbx "$name" generic-opencode "$_opencode_mode"; then
echo "acq(sbx): error: could not record OpenCode launch mode for '$name'." >&2
echo " Refusing to attach because acq cannot safely choose SBX grammar." >&2
echo " Remove and recreate this sandbox." >&2
return 1
fi
fi
acq_workspace_record_write sbx "$name" "$_primary_ws" || true
_acq_sbx_seed_extra_kit_marker "$name"
# Backend parity (ADR-0030): sbx DELIVERS ~/.rc.d/*.sh via kit files[] but,
Expand Down Expand Up @@ -718,10 +780,49 @@ acq_backend_workspace_for() {
_acq_sbx_attach_command() {
local name="$1" agent
agent=$(acq_backend_recorded_agent "$name")
[ "$agent" = "opencode" ] && acq_agent_kit_entrypoint opencode && return 0
[ -n "$agent" ] || agent="bash"
printf '%s\n' "$agent"
}

# SBX requires an agent positional before `-- AGENT_ARGS`. Generic OpenCode
# sandboxes were created with SBX's `shell` token to avoid its native OpenCode
# kit, while the pinned kit supplies `opencode` as the command to run.
_acq_sbx_attach_agent() {
local name="$1" agent
case "$(acq_host_config_read sbx "$name" generic-opencode 2>/dev/null || true)" in
generic-opencode|1)
printf '%s\n' shell
;;
*)
agent=$(acq_backend_recorded_agent "$name")
printf '%s\n' "${agent:-shell}"
;;
esac
}

_acq_sbx_opencode_launch_mode() {
local name="$1" agent mode
agent=$(acq_backend_recorded_agent "$name")
[ "$agent" = "opencode" ] || return 0
mode=$(acq_host_config_read sbx "$name" generic-opencode 2>/dev/null || true)
case "$mode" in
generic-opencode|1) printf '%s\n' generic-opencode; return 0 ;;
legacy-opencode) printf '%s\n' "$mode"; return 0 ;;
"")
echo "acq(sbx): error: cannot determine OpenCode launch mode for '$name'." >&2
echo " Its host launch-mode record is missing, so acq will not fall back" >&2
echo " to legacy SBX grammar. Remove and recreate this sandbox." >&2
return 1
;;
*)
echo "acq(sbx): error: invalid OpenCode launch mode record for '$name'." >&2
echo " Remove and recreate this sandbox." >&2
return 1
;;
esac
}

# ---------------------------------------------------------------------------
# acq_backend_run — run a command inside a sandbox
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -770,32 +871,61 @@ acq_backend_attach() {
echo "acq: refusing to attach to '$name' because kit heal did not finish." >&2
return 1
fi
local agent ws
local agent sbx_agent ws opencode_mode generic_opencode
opencode_mode=$(_acq_sbx_opencode_launch_mode "$name") || return 1
generic_opencode=""
[ "$opencode_mode" = "generic-opencode" ] && generic_opencode=1
agent=$(_acq_sbx_attach_command "$name")
sbx_agent=$(_acq_sbx_attach_agent "$name")
ws=$(acq_backend_workspace_for "$name")
if [ "$#" -gt 0 ] && [ "$1" = "--" ]; then
shift
local _attach_cmd=() _sbx_args=()
if [ "$agent" = "opencode" ] \
&& [ "$generic_opencode" = "1" ]; then
case "${1:-}" in
-*) _attach_cmd=("$agent" "$@") ;; # agent flags, including short options
*) _attach_cmd=("$@") ;; # explicit command escape
esac
else
_attach_cmd=("$@")
fi
if [ "${ACQ_ACTIVATE_PROJECT_ENV:-0}" = "1" ] \
&& command -v acq_guest_exec_script >/dev/null 2>&1; then
if [ -n "$ws" ]; then
sbx run --name "$name" -- env "ACQ_WORKSPACE=$ws" sh -c "$(acq_guest_exec_script)" sh "$agent" "$@"
_sbx_args=(env "ACQ_WORKSPACE=$ws" sh -c "$(acq_guest_exec_script)" sh "${_attach_cmd[@]}")
else
sbx run --name "$name" -- sh -c "$(acq_guest_exec_script)" sh "$agent" "$@"
_sbx_args=(sh -c "$(acq_guest_exec_script)" sh "${_attach_cmd[@]}")
fi
else
sbx run --name "$name" -- "$@"
_sbx_args=("${_attach_cmd[@]}")
fi
if [ "$generic_opencode" = "1" ]; then
_sbx_args=(-lc 'exec "$@"' sh "${_sbx_args[@]}")
fi
sbx run --name "$name" "$sbx_agent" -- "${_sbx_args[@]}"
else
local _sbx_args=()
if [ "${ACQ_ACTIVATE_PROJECT_ENV:-0}" = "1" ] \
&& command -v acq_guest_exec_script >/dev/null 2>&1; then
if [ -n "$ws" ]; then
sbx run --name "$name" -- env "ACQ_WORKSPACE=$ws" sh -c "$(acq_guest_exec_script)" sh "$agent"
_sbx_args=(env "ACQ_WORKSPACE=$ws" sh -c "$(acq_guest_exec_script)" sh "$agent")
else
sbx run --name "$name" -- sh -c "$(acq_guest_exec_script)" sh "$agent"
_sbx_args=(sh -c "$(acq_guest_exec_script)" sh "$agent")
fi
else
sbx run --name "$name"
if [ "$agent" = "opencode" ] \
&& [ "$generic_opencode" = "1" ]; then
_sbx_args=("$agent")
else
sbx run --name "$name"
return
fi
fi
if [ "$generic_opencode" = "1" ]; then
_sbx_args=(-lc 'exec "$@"' sh "${_sbx_args[@]}")
fi
sbx run --name "$name" "$sbx_agent" -- "${_sbx_args[@]}"
fi
}

Expand Down
Loading
Loading