From f45a734048976f885e8b8b8987adab816c7d0496 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Wed, 14 Oct 2026 08:32:41 +0000 Subject: [PATCH] refactor(acq): register experimental openshell backend --- acq | 51 ++++++++-------- acq.backends/common.sh | 87 ++++++++++++++++++++-------- test/bats/20-backend-resolution.bats | 17 ++++++ test/bats/60-help.bats | 5 +- test/bats/70-msb-backend.bats | 4 +- 5 files changed, 112 insertions(+), 52 deletions(-) diff --git a/acq b/acq index 68daa55c..83a37925 100755 --- a/acq +++ b/acq @@ -72,11 +72,11 @@ print_help() { cat <<'HELP' acq — agentic coding quickstart wrapper -acq wraps a sandbox backend (sbx or msb) and applies the shared federal +acq wraps a sandbox backend and applies the shared federal "kits" (USAi provider, playbook, git-ssh-sign, zscaler CA) to each sandbox. Usage: - acq [--backend sbx|msb] [args...] + acq [--backend NAME] [args...] Subcommands: run AGENT PATH [...] [-- AGENT_ARGS...] Create+attach a sandbox, or re-attach @@ -114,7 +114,7 @@ Subcommands: version Show acq version + backend info. doctor [NAME] Backend health check, or non-enforcing image diagnostics for an existing sandbox. - backend list List detected backends (sbx, msb). + backend list List registered backends and availability. backend set NAME Persist the default backend. backend unset Clear the persisted default (auto-detect). configure Interactive setup: pick extra kits and @@ -133,7 +133,7 @@ Any other subcommand is passed through to the active backend's CLI (acq prints a notice first so you know the output/errors below it are the backend's). Global flags (before the subcommand): - --backend sbx|msb Override the active backend for this invocation. + --backend NAME Override the active backend for this invocation. Default: the persisted backend (`acq backend set`), else msb. `msb` runs microVMs via the microsandbox (msb) CLI; `sbx` runs Docker sandboxes via the sbx CLI. @@ -182,6 +182,13 @@ run/create flags: See docs/howto/acq.md and docs/BACKEND_GUIDE.md for more. HELP + printf 'Registered: ' + local _backend_name _backend_sep="" + for _backend_name in "${ACQ_BACKEND_NAMES[@]}"; do + printf '%s%s' "$_backend_sep" "$_backend_name" + _backend_sep=', ' + done + printf '. OpenShell is explicit-only until its adapter is available.\n' } # _acq_secret_usage / _acq_kit_usage — acq's OWN per-verb usage blocks. Printed @@ -245,8 +252,8 @@ _acq_backend_subcmd_usage() { acq backend — select the sandbox backend Usage: - acq backend list List detected/installed backends (sbx, msb) - acq backend set NAME Persist the default backend (sbx | msb) + acq backend list List registered backends and availability + acq backend set NAME Persist the default backend acq backend unset Clear the persisted default (fall back to auto-detect) BACKENDUSAGE } @@ -590,7 +597,7 @@ _acq_parse_secret_scope_for_propagation() { _acq_propagate_secret_to_present_backends() { local service="$1" scope_name="${2:-}" origin="${3:-set}" current="${ACQ_RESOLVED_BACKEND:-}" be rc=0 [ -n "$service" ] || return 0 - for be in msb sbx; do + for be in "${ACQ_AUTODETECT_BACKEND_NAMES[@]}"; do [ "$be" != "$current" ] || continue _backend_installed "$be" || continue if [ "$origin" = "rotate" ] && [ "$current" = "sbx" ] && [ "$be" = "msb" ]; then @@ -751,22 +758,20 @@ case "$subcommand" in # List detected/installed backends. Recover a backend installed in # ~/.local/bin but not on PATH first, so the listing matches what # `acq run` auto-detect will find. - _ensure_local_bin_on_path sbx >/dev/null 2>&1 || true - _ensure_local_bin_on_path msb >/dev/null 2>&1 || true - printf 'Installed backends:\n' - if command -v sbx >/dev/null 2>&1; then - local_ver=$(sbx version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1 | sed 's/^v//' || echo "?") - printf ' sbx v%s\n' "$local_ver" - else - printf ' sbx (not found)\n' - fi - if command -v msb >/dev/null 2>&1; then - local_ver=$(msb --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1 || echo "?") - printf ' msb v%s\n' "$local_ver" - else - printf ' msb (not found)\n' - fi - printf '\nDeferred (not scheduled):\n ppp (Podman-Plus-Proxy)\n' + printf 'Registered backends:\n' + for local_backend in "${ACQ_BACKEND_NAMES[@]}"; do + if acq_backend_is_explicit_only "$local_backend"; then + printf ' %s (explicit-only; adapter not yet available)\n' "$local_backend" + else + _ensure_local_bin_on_path "$local_backend" >/dev/null 2>&1 || true + if command -v "$local_backend" >/dev/null 2>&1; then + local_ver=$(acq_backend_cli_version "$local_backend") + printf ' %s v%s\n' "$local_backend" "$local_ver" + else + printf ' %s (not found)\n' "$local_backend" + fi + fi + done ;; set) shift diff --git a/acq.backends/common.sh b/acq.backends/common.sh index 008a1d75..fe0f5b2c 100644 --- a/acq.backends/common.sh +++ b/acq.backends/common.sh @@ -1759,6 +1759,47 @@ _acq_config_write_field() { return 0 } +# --------------------------------------------------------------------------- +# Backend registry +# --------------------------------------------------------------------------- +# Bash 3.2 does not have associative arrays, so keep backend metadata in one +# registry accessor. OpenShell is deliberately explicit-only until it has an +# adapter; it must never affect the established msb/sbx auto-detection path. +ACQ_BACKEND_NAMES=(sbx msb openshell) +ACQ_AUTODETECT_BACKEND_NAMES=(msb sbx) + +acq_backend_registry_field() { + local name="$1" field="$2" + case "${name}:${field}" in + sbx:explicit_only) printf '0\n' ;; + sbx:version_arg) printf 'version\n' ;; + sbx:version_regex) printf 'v?[0-9]+\.[0-9]+(\.[0-9]+)?\n' ;; + msb:explicit_only) printf '0\n' ;; + msb:version_arg) printf '%s\n' '--version' ;; + msb:version_regex) printf '[0-9]+\.[0-9]+(\.[0-9]+)?\n' ;; + openshell:explicit_only) printf '1\n' ;; + openshell:version_arg) printf '%s\n' '--version' ;; + openshell:version_regex) printf '[0-9]+\.[0-9]+(\.[0-9]+)?\n' ;; + *) return 1 ;; + esac +} + +acq_backend_is_explicit_only() { + [ "$(acq_backend_registry_field "$1" explicit_only 2>/dev/null || true)" = "1" ] +} + +acq_backend_cli_version() { + local name="$1" arg regex version + arg=$(acq_backend_registry_field "$name" version_arg) || return 1 + regex=$(acq_backend_registry_field "$name" version_regex) || return 1 + version=$(command "$name" "$arg" 2>/dev/null | grep -oE "$regex" | head -n1 | sed 's/^v//' || true) + printf '%s\n' "${version:-?}" +} + +acq_backend_registered_names() { + printf '%s\n' "${ACQ_BACKEND_NAMES[@]}" +} + # _ensure_local_bin_on_path — if a backend CLI is not on PATH but IS present in # ~/.local/bin (the default install location for `curl … | sh` installers, e.g. # microsandbox), prepend ~/.local/bin to PATH for THIS process so acq and the @@ -1841,8 +1882,13 @@ _auto_detect_backend() { ACQ_AUTODETECT_BACKEND="" ACQ_AUTODETECT_REASON="" local have_msb=0 have_sbx=0 - _backend_installed msb && have_msb=1 - _backend_installed sbx && have_sbx=1 + local backend + for backend in "${ACQ_AUTODETECT_BACKEND_NAMES[@]}"; do + case "$backend" in + msb) _backend_installed "$backend" && have_msb=1 ;; + sbx) _backend_installed "$backend" && have_sbx=1 ;; + esac + done if [ "$have_msb" -eq 1 ] && [ "$have_sbx" -eq 1 ]; then if _sbx_has_sandboxes; then @@ -3631,34 +3677,25 @@ acq_print_image_contract_doctor() { } acq_print_doctor() { - local sbx_status msb_status + local backend status # Recover a backend installed in ~/.local/bin but not on PATH, so doctor's # verdict matches what `acq run` will actually find (self-repair parity). - _ensure_local_bin_on_path sbx >/dev/null 2>&1 || true - _ensure_local_bin_on_path msb >/dev/null 2>&1 || true - - # Check sbx - if command -v sbx >/dev/null 2>&1; then - local sbx_ver - sbx_ver=$(sbx version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1 | sed 's/^v//' || echo "?") - sbx_status="installed v${sbx_ver}" - else - sbx_status="not found" - fi - - # Check msb (microsandbox). - if command -v msb >/dev/null 2>&1; then - local msb_ver - msb_ver=$(msb --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1 || echo "?") - msb_status="installed v${msb_ver}" - else - msb_status="not found" - fi + for backend in "${ACQ_BACKEND_NAMES[@]}"; do + acq_backend_is_explicit_only "$backend" || _ensure_local_bin_on_path "$backend" >/dev/null 2>&1 || true + done echo "acq: backend health check" - echo " [sbx: ${sbx_status}]" - echo " [msb: ${msb_status}]" + for backend in "${ACQ_BACKEND_NAMES[@]}"; do + if acq_backend_is_explicit_only "$backend"; then + status="explicit-only; adapter not yet available" + elif command -v "$backend" >/dev/null 2>&1; then + status="installed v$(acq_backend_cli_version "$backend")" + else + status="not found" + fi + echo " [$backend: $status]" + done echo "" local config_file diff --git a/test/bats/20-backend-resolution.bats b/test/bats/20-backend-resolution.bats index 415b212e..d60049c5 100644 --- a/test/bats/20-backend-resolution.bats +++ b/test/bats/20-backend-resolution.bats @@ -82,6 +82,23 @@ _resolve() { # ADAPTER EXPLICIT -> prints "|" refute_output --partial 'acq: using' } +@test "backend: OpenShell registration is explicit-only and never auto-detected" { + unset ACQ_BACKEND + export XDG_CONFIG_HOME="$STUBDIR/noconfig" + export ACQ_TEST_INSTALLED_BACKENDS="openshell" + run _resolve sbx '' + assert_failure + + # _resolve captures resolver stderr in $STUBDIR/note while asserting the + # in-process exit status. The CLI invocation below checks the user-facing error. + run cat "$STUBDIR/note" + assert_output --partial 'no backend detected' + + run env ACQ_BACKEND=openshell "$ACQ" ls + assert_failure + assert_output --partial "no backend adapter found for 'openshell'" +} + @test "backend: both installed, no sbx sandboxes -> msb + notice" { unset ACQ_BACKEND export XDG_CONFIG_HOME="$STUBDIR/noconfig" diff --git a/test/bats/60-help.bats b/test/bats/60-help.bats index 6ebf3fa4..554e044d 100644 --- a/test/bats/60-help.bats +++ b/test/bats/60-help.bats @@ -13,11 +13,12 @@ teardown() { acq_teardown_stubs; } load 'helper' -@test "help: acq help shows the banner, --backend list, and appends backend help" { +@test "help: acq help shows the banner, registered backends, and appends backend help" { run env ACQ_BACKEND=sbx "$ACQ" help assert_success assert_output --partial 'acq — agentic coding quickstart wrapper' - assert_output --partial '--backend sbx|msb' + assert_output --partial '--backend NAME' + assert_output --partial 'Registered: sbx, msb, openshell' assert_output --partial 'microsandbox (msb)' assert_output --partial 'SBX-TOPLEVEL-HELP' } diff --git a/test/bats/70-msb-backend.bats b/test/bats/70-msb-backend.bats index be72d4ce..4eb22613 100644 --- a/test/bats/70-msb-backend.bats +++ b/test/bats/70-msb-backend.bats @@ -88,7 +88,7 @@ _with_adapter() { # ADAPTER BODY assert_output --partial '0.6.9' run env "$ACQ" backend list assert_output --partial 'msb v0.6.9' - refute_output --partial 'Coming in 1.2.x' + assert_output --partial 'openshell (explicit-only; adapter not yet available)' } @test "backend set/unset: persists a default then clears it (idempotent)" { @@ -109,7 +109,7 @@ _with_adapter() { # ADAPTER BODY @test "msb: doctor shows the installed msb version, not the old placeholder" { run bash -c 'printf "n\n" | "$1" doctor' _ "$ACQ" assert_output --partial 'msb: installed v0.6.9' - refute_output --partial 'coming in 1.2.x' + assert_output --partial 'openshell: explicit-only; adapter not yet available' } @test "msb: acq_backend_prepare is silent on a ready host" {