From 0b88aa049c6e41844af06c056a6f4aca7b36e666 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 3 Jun 2026 23:11:50 -0700 Subject: [PATCH 1/2] X-Smart-Branch-Parent: main From 22224c0e26cb38fc3e4d33685f66ed4dd2c9fddd Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 4 Jun 2026 00:05:01 -0700 Subject: [PATCH 2/2] feat: harness CLI with bin/ structure, profiles, unified new.sh - bin/harness: entry point routing to subcommands (new, connect, deploy, teardown, preflight, providers, test) - bin/scripts/new.sh: unified sandbox creation for local + remote, merges sandbox-podman.sh and sandbox-ocp.sh - bin/scripts/deploy.sh: merged deploy-podman.sh and deploy-ocp.sh with --local/--remote flags - profiles/: renamed from agents/, parse via lib/profile.sh - lib/ moved to bin/scripts/lib/ - test-flow.sh moved to test/ - Deleted: deploy-podman.sh, deploy-ocp.sh, sandbox-podman.sh, sandbox-ocp.sh, deploy-local.sh Tested: bats 29/29, podman 13/13, ocp 13/14 (provider flake) Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 6 +- bin/harness | 52 ++++ bin/scripts/connect.sh | 5 + setup-creds.sh => bin/scripts/creds.sh | 0 bin/scripts/deploy.sh | 235 ++++++++++++++++++ {lib => bin/scripts/lib}/common.sh | 0 lib/agent.sh => bin/scripts/lib/profile.sh | 2 +- {lib => bin/scripts/lib}/providers.py | 2 +- bin/scripts/new.sh | 218 ++++++++++++++++ .../scripts/preflight.sh | 0 .../scripts/providers.sh | 3 +- teardown.sh => bin/scripts/teardown.sh | 0 deploy-local.sh | 74 ------ deploy-ocp.sh | 188 -------------- deploy-podman.sh | 58 ----- {agents => profiles}/default.toml | 0 sandbox-ocp.sh | 126 ---------- sandbox-podman.sh | 80 ------ test/preflight.bats | 4 +- test-flow.sh => test/test-flow.sh | 26 +- 20 files changed, 532 insertions(+), 547 deletions(-) create mode 100755 bin/harness create mode 100755 bin/scripts/connect.sh rename setup-creds.sh => bin/scripts/creds.sh (100%) create mode 100755 bin/scripts/deploy.sh rename {lib => bin/scripts/lib}/common.sh (100%) rename lib/agent.sh => bin/scripts/lib/profile.sh (98%) rename {lib => bin/scripts/lib}/providers.py (99%) create mode 100755 bin/scripts/new.sh rename openshell-harness-preflight.sh => bin/scripts/preflight.sh (100%) rename setup-providers.sh => bin/scripts/providers.sh (98%) rename teardown.sh => bin/scripts/teardown.sh (100%) delete mode 100755 deploy-local.sh delete mode 100755 deploy-ocp.sh delete mode 100755 deploy-podman.sh rename {agents => profiles}/default.toml (100%) delete mode 100755 sandbox-ocp.sh delete mode 100755 sandbox-podman.sh rename test-flow.sh => test/test-flow.sh (84%) diff --git a/Makefile b/Makefile index bb0b281..eaa76c6 100644 --- a/Makefile +++ b/Makefile @@ -40,15 +40,15 @@ push-launcher: launcher ## Build + push sandbox and launcher, then run full tests on both platforms test: sandbox push-launcher - ./test-flow.sh all --full + ./test/test-flow.sh all --full ## Build + push sandbox and launcher, then run full podman test test-podman: sandbox push-launcher - ./test-flow.sh podman --full + ./test/test-flow.sh podman --full ## Build + push sandbox and launcher, then run full OCP test test-ocp: sandbox push-launcher - ./test-flow.sh ocp --full + ./test/test-flow.sh ocp --full ## ── Convenience targets ─────────────────────────────────────────────── diff --git a/bin/harness b/bin/harness new file mode 100755 index 0000000..326749c --- /dev/null +++ b/bin/harness @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# OpenShell Harness CLI +# +# Usage: +# harness new [--local|--remote] [--profile NAME] [SANDBOX_NAME] +# harness connect [SANDBOX_NAME] +# harness deploy [--local|--remote] +# harness teardown [--sandboxes] [--providers] [--k8s] +# harness preflight +# harness providers +# harness test [podman|ocp|all] [--full] +set -euo pipefail + +HARNESS_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT_DIR="$HARNESS_DIR/bin/scripts" + +case "${1:-}" in + new) shift; exec "$SCRIPT_DIR/new.sh" "$@" ;; + connect) shift; exec "$SCRIPT_DIR/connect.sh" "$@" ;; + deploy) shift; exec "$SCRIPT_DIR/deploy.sh" "$@" ;; + teardown) shift; exec "$SCRIPT_DIR/teardown.sh" "$@" ;; + preflight) shift; exec "$SCRIPT_DIR/preflight.sh" "$@" ;; + providers) shift; exec "$SCRIPT_DIR/providers.sh" "$@" ;; + test) shift; exec "$HARNESS_DIR/test/test-flow.sh" "$@" ;; + *) + echo "OpenShell Harness" + echo "" + echo "Usage: harness " + echo "" + echo "Commands:" + echo " new [--local|--remote] [--profile NAME] [SANDBOX_NAME]" + echo " Create a new sandbox. Deploys gateway and providers if needed." + echo "" + echo " connect [SANDBOX_NAME]" + echo " Reconnect to a running sandbox." + echo "" + echo " deploy [--local|--remote]" + echo " Deploy or verify the gateway." + echo "" + echo " teardown [--sandboxes] [--providers] [--k8s]" + echo " Tear down sandboxes, providers, or k8s resources." + echo "" + echo " preflight" + echo " Check environment prerequisites." + echo "" + echo " providers" + echo " Register providers with the gateway." + echo "" + echo " test [podman|ocp|all] [--full]" + echo " End-to-end validation." + ;; +esac diff --git a/bin/scripts/connect.sh b/bin/scripts/connect.sh new file mode 100755 index 0000000..2778a0f --- /dev/null +++ b/bin/scripts/connect.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Connect to a running sandbox. +set -euo pipefail +CLI="${OPENSHELL_CLI:-openshell}" +exec "$CLI" sandbox connect "${1:-}" diff --git a/setup-creds.sh b/bin/scripts/creds.sh similarity index 100% rename from setup-creds.sh rename to bin/scripts/creds.sh diff --git a/bin/scripts/deploy.sh b/bin/scripts/deploy.sh new file mode 100755 index 0000000..0946875 --- /dev/null +++ b/bin/scripts/deploy.sh @@ -0,0 +1,235 @@ +#!/usr/bin/env bash +# Deploy or verify an OpenShell gateway. +# +# Usage: +# deploy.sh --local # verify local podman gateway +# deploy.sh --remote # deploy to OpenShift cluster +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +HARNESS_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +PLATFORM="" +while [[ $# -gt 0 ]]; do + case "$1" in + --local) PLATFORM="local"; shift ;; + --remote) PLATFORM="remote"; shift ;; + --kubeconfig) export KUBECONFIG="$2"; shift 2 ;; + *) echo "Usage: $0 [--local|--remote]"; exit 1 ;; + esac +done + +if [[ -z "$PLATFORM" ]]; then + echo "ERROR: specify --local or --remote" + exit 1 +fi + +# ── Local (Podman) ──────────────────────────────────────────────────── +deploy_local() { + CLI="${OPENSHELL_CLI:-openshell}" + command -v "$CLI" &>/dev/null || { + echo "ERROR: openshell CLI not found. Install it first:" + echo " curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" + exit 1 + } + + echo "=== Container Runtime ===" + if command -v podman &>/dev/null; then + echo " ✓ Podman: $(podman --version 2>/dev/null)" + else + echo " ✗ Podman not found" + exit 1 + fi + + echo "" + echo "=== Gateway ===" + LOCAL_GW=$("$CLI" gateway list 2>/dev/null | awk '/127\.0\.0\.1/ {gsub(/^\*/, ""); print $1; exit}') + + if [[ -z "$LOCAL_GW" ]]; then + echo " ✗ No local gateway found" + echo "" + echo " Install OpenShell (auto-registers the gateway):" + echo " curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" + exit 1 + fi + + "$CLI" gateway select "$LOCAL_GW" 2>/dev/null || true + + if "$CLI" inference get &>/dev/null; then + echo " ✓ $LOCAL_GW (active, reachable)" + else + echo " ✗ $LOCAL_GW (not responding)" + echo "" + echo " Start the gateway:" + echo " macOS: brew services start openshell" + echo " Linux: systemctl --user start openshell" + exit 1 + fi + + echo "" + echo "Done." +} + +# ── Remote (OpenShift) ──────────────────────────────────────────────── +deploy_remote() { + for cmd in kubectl helm; do + command -v "$cmd" &>/dev/null || { echo "ERROR: $cmd is required but not found."; exit 1; } + done + + # Read chart version from openshell.toml or env + CHART_VERSION="${OPENSHELL_CHART_VERSION:-}" + if [[ -z "$CHART_VERSION" && -f "$HARNESS_DIR/openshell.toml" ]]; then + CHART_VERSION=$(python3 -c " +import tomllib +with open('$HARNESS_DIR/openshell.toml', 'rb') as f: + print(tomllib.load(f).get('upstream', {}).get('chart-version', '')) +" 2>/dev/null || true) + fi + CHART_VERSION="${CHART_VERSION:-0.0.55}" + CHART="oci://ghcr.io/nvidia/openshell/helm-chart" + + echo "OpenShell chart: $CHART_VERSION" + echo "KUBECONFIG: ${KUBECONFIG:-default}" + echo "" + + echo "=== Step 1: Creating namespace ===" + kubectl create ns openshell 2>/dev/null || true + kubectl label ns openshell \ + pod-security.kubernetes.io/enforce=privileged \ + pod-security.kubernetes.io/warn=privileged \ + --overwrite + + echo "=== Step 2: Installing Sandbox CRD ===" + kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml + + echo "=== Step 3: Granting OpenShift SCCs ===" + for sa in openshell openshell-sandbox default; do + oc adm policy add-scc-to-user privileged -z "$sa" -n openshell 2>/dev/null || true + done + oc adm policy add-scc-to-user anyuid -z openshell -n openshell 2>/dev/null || true + kubectl create clusterrolebinding agent-sandbox-admin \ + --clusterrole=cluster-admin \ + --serviceaccount=agent-sandbox-system:agent-sandbox-controller 2>/dev/null || true + + kubectl apply -n openshell -f - <<'RBAC' +apiVersion: v1 +kind: ServiceAccount +metadata: + name: openshell-launcher + namespace: openshell +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: openshell-launcher + namespace: openshell +rules: + - apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: openshell-launcher + namespace: openshell +subjects: + - kind: ServiceAccount + name: openshell-launcher + namespace: openshell +roleRef: + kind: Role + name: openshell-launcher + apiGroup: rbac.authorization.k8s.io +RBAC + + echo "=== Step 4: Deploying gateway via Helm ===" + SANDBOX_IMAGE="${SANDBOX_IMAGE:-quay.io/rcochran/openshell:sandbox}" + + APPS_DOMAIN=$(kubectl get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}' 2>/dev/null) + if [[ -z "$APPS_DOMAIN" ]]; then + echo "ERROR: Could not determine OpenShift apps domain." + exit 1 + fi + ROUTE_HOST="gateway-openshell.${APPS_DOMAIN}" + + HELM_ARGS=( + --values "$HARNESS_DIR/values-ocp.yaml" + --set server.sandboxImage="$SANDBOX_IMAGE" + --set pkiInitJob.serverDnsNames[0]="$ROUTE_HOST" + ) + + [[ -n "${PULL_SECRET:-}" ]] && HELM_ARGS+=(--set imagePullSecrets[0].name="$PULL_SECRET") + [[ -n "${SANDBOX_PULL_SECRET:-}" ]] && HELM_ARGS+=(--set server.sandboxImagePullSecrets[0].name="$SANDBOX_PULL_SECRET") + + helm upgrade --install openshell "$CHART" --version "$CHART_VERSION" -n openshell \ + "${HELM_ARGS[@]}" + + echo "=== Waiting for gateway ===" + kubectl rollout status statefulset/openshell -n openshell --timeout=300s + + echo "=== Step 5: Creating OpenShift route ===" + if ! kubectl get route gateway -n openshell &>/dev/null; then + cat <<'ROUTE' | kubectl apply -f - +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: gateway + namespace: openshell +spec: + tls: + termination: passthrough + to: + kind: Service + name: openshell + port: + targetPort: grpc +ROUTE + fi + echo " Route: $ROUTE_HOST" + + echo "=== Step 6: Configuring CLI gateway ===" + GATEWAY_NAME="${GATEWAY_NAME:-openshell-remote-ocp}" + GATEWAY_URL="https://${ROUTE_HOST}:443" + CLI="${OPENSHELL_CLI:-openshell}" + + if command -v "$CLI" &>/dev/null; then + for gw in $("$CLI" gateway list 2>/dev/null | grep "$ROUTE_HOST" | awk '{gsub(/^\*/, ""); print $1}'); do + "$CLI" gateway remove "$gw" 2>/dev/null || true + done + + "$CLI" gateway add "$GATEWAY_URL" --name "$GATEWAY_NAME" --local 2>/dev/null || true + + MTLS_DIR="$HOME/.config/openshell/gateways/$GATEWAY_NAME/mtls" + kubectl get secret openshell-client-tls -n openshell \ + -o jsonpath='{.data.ca\.crt}' | base64 -d > "$MTLS_DIR/ca.crt" + kubectl get secret openshell-client-tls -n openshell \ + -o jsonpath='{.data.tls\.crt}' | base64 -d > "$MTLS_DIR/tls.crt" + kubectl get secret openshell-client-tls -n openshell \ + -o jsonpath='{.data.tls\.key}' | base64 -d > "$MTLS_DIR/tls.key" + + "$CLI" gateway select "$GATEWAY_NAME" 2>/dev/null || true + echo " ✓ $GATEWAY_NAME registered (certs from cluster)" + + echo -n " Waiting for gateway..." + for i in $(seq 1 30); do + if "$CLI" inference get &>/dev/null; then + echo " ✓ reachable" + break + fi + sleep 2 + echo -n "." + [[ $i -eq 30 ]] && echo " ✗ timed out (try: openshell inference get)" + done + fi + + echo "" + echo "Done." +} + +# ── Main ────────────────────────────────────────────────────────────── +case "$PLATFORM" in + local) deploy_local ;; + remote) deploy_remote ;; +esac diff --git a/lib/common.sh b/bin/scripts/lib/common.sh similarity index 100% rename from lib/common.sh rename to bin/scripts/lib/common.sh diff --git a/lib/agent.sh b/bin/scripts/lib/profile.sh similarity index 98% rename from lib/agent.sh rename to bin/scripts/lib/profile.sh index 49b03e8..fc99dd9 100644 --- a/lib/agent.sh +++ b/bin/scripts/lib/profile.sh @@ -2,7 +2,7 @@ # Agent config parsing helpers. # # Source from any script: -# source "$(dirname "$0")/lib/agent.sh" +# source "$(dirname "$0")/lib/profile.sh" # # Usage: # parse_agent agents/default.toml diff --git a/lib/providers.py b/bin/scripts/lib/providers.py similarity index 99% rename from lib/providers.py rename to bin/scripts/lib/providers.py index 28f50b1..c8f0b19 100644 --- a/lib/providers.py +++ b/bin/scripts/lib/providers.py @@ -17,7 +17,7 @@ import tomllib from pathlib import Path -ROOT = Path(__file__).parent.parent +ROOT = Path(__file__).parent.parent.parent.parent PROVIDERS_TOML = ROOT / "providers.toml" CONFIG_TOML = ROOT / "openshell.toml" CLI = os.environ.get("OPENSHELL_CLI", "openshell") diff --git a/bin/scripts/new.sh b/bin/scripts/new.sh new file mode 100755 index 0000000..54035cb --- /dev/null +++ b/bin/scripts/new.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# Create a new sandbox. +# +# Ensures the gateway is deployed and providers are registered before creating. +# +# Usage: +# new.sh # active gateway, default profile +# new.sh my-sandbox # named sandbox +# new.sh --local # ensure local gateway +# new.sh --remote # ensure OCP gateway +# new.sh --profile coder # use profiles/coder.toml +# new.sh --no-tty --name test-agent # non-interactive (for testing) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +HARNESS_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +source "$SCRIPT_DIR/lib/common.sh" +source "$SCRIPT_DIR/lib/profile.sh" + +# ── Parse args ──────────────────────────────────────────────────────── +PLATFORM="" +PROFILE="default" +SANDBOX_NAME="" +TTY_FLAG="--tty" +NO_TTY=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --local) PLATFORM="local"; shift ;; + --remote) PLATFORM="remote"; shift ;; + --profile) PROFILE="$2"; shift 2 ;; + --name) SANDBOX_NAME="$2"; shift 2 ;; + --no-tty) TTY_FLAG="--no-tty"; NO_TTY=true; shift ;; + *) SANDBOX_NAME="$1"; shift ;; + esac +done + +# ── 1. Ensure gateway ──────────────────────────────────────────────── +if [[ -n "$PLATFORM" ]]; then + "$SCRIPT_DIR/deploy.sh" "--$PLATFORM" +else + if ! "$CLI" inference get &>/dev/null 2>&1; then + echo "ERROR: No active gateway. Use --local or --remote." + exit 1 + fi +fi + +# ── 2. Ensure providers ────────────────────────────────────────────── +provider_count=$("$CLI" provider list 2>/dev/null | awk 'NR>1' | wc -l | tr -d ' ') +if [[ "$provider_count" -eq 0 ]]; then + "$SCRIPT_DIR/providers.sh" +fi + +# ── 3. If remote, ensure creds ─────────────────────────────────────── +if [[ "$PLATFORM" == "remote" ]]; then + NAMESPACE="${OPENSHELL_NAMESPACE:-openshell}" + if ! kubectl get secret openshell-gws -n "$NAMESPACE" &>/dev/null 2>&1; then + "$SCRIPT_DIR/creds.sh" + fi +fi + +# ── 4. Parse profile ───────────────────────────────────────────────── +PROFILE_FILE="$HARNESS_DIR/profiles/${PROFILE}.toml" +NAME_OVERRIDE="$SANDBOX_NAME" +parse_agent "$PROFILE_FILE" +[[ -n "$NAME_OVERRIDE" ]] && SANDBOX_NAME="$NAME_OVERRIDE" + +echo "" +echo "=== Sandbox ===" +echo " Profile: $PROFILE" +echo " Image: $SANDBOX_IMAGE" + +# ── 5. Build common flags ──────────────────────────────────────────── +echo "" +echo "=== Providers ===" +build_provider_flags + +FROM_FLAGS=() +[[ -n "$SANDBOX_IMAGE" ]] && FROM_FLAGS=(--from "$SANDBOX_IMAGE") + +NAME_FLAGS=() +[[ -n "$SANDBOX_NAME" ]] && NAME_FLAGS=(--name "$SANDBOX_NAME") + +# ── 6. Create sandbox ──────────────────────────────────────────────── +create_local() { + HARNESS_UPLOAD_DIR="/tmp/openshell" + rm -rf "$HARNESS_UPLOAD_DIR" + stage_harness_dir "$HARNESS_UPLOAD_DIR" + + if $NO_TTY; then + CMD=(-- bash /sandbox/startup.sh) + else + CMD=(-- bash -c ". /sandbox/startup.sh && exec $SANDBOX_COMMAND") + fi + + echo "" + echo "=== Creating sandbox ===" + for attempt in 1 2 3 4 5; do + "$CLI" sandbox create \ + $TTY_FLAG \ + ${NAME_FLAGS[@]+"${NAME_FLAGS[@]}"} \ + ${FROM_FLAGS[@]+"${FROM_FLAGS[@]}"} \ + ${PROVIDER_FLAGS[@]+"${PROVIDER_FLAGS[@]}"} \ + --upload "$HARNESS_UPLOAD_DIR:/sandbox/.config" --no-git-ignore \ + "${CMD[@]}" \ + && exit 0 + echo " Attempt $attempt failed (supervisor race), retrying in 5s..." + if [[ -n "$SANDBOX_NAME" ]]; then + "$CLI" sandbox delete "$SANDBOX_NAME" 2>/dev/null || true + else + "$CLI" sandbox delete "$("$CLI" sandbox list 2>/dev/null | strip_ansi | awk 'NR==2{print $1}')" 2>/dev/null || true + fi + sleep 5 + done + echo "ERROR: Failed after 5 attempts." + exit 1 +} + +create_remote() { + NAMESPACE="${OPENSHELL_NAMESPACE:-openshell}" + + kubectl create configmap "sandbox-${SANDBOX_NAME}" -n "$NAMESPACE" \ + --from-file=config.toml="$PROFILE_FILE" \ + --dry-run=client -o yaml | kubectl apply -f - + + if [[ -n "$SANDBOX_ENV" ]]; then + kubectl create configmap "sandbox-${SANDBOX_NAME}-env" -n "$NAMESPACE" \ + --from-literal=sandbox.env="$SANDBOX_ENV" \ + --dry-run=client -o yaml | kubectl apply -f - + fi + + local job_name="sandbox-${SANDBOX_NAME}" + kubectl delete job "$job_name" -n "$NAMESPACE" --force --grace-period=0 2>/dev/null || true + kubectl delete pod -n "$NAMESPACE" -l "job-name=${job_name}" --force --grace-period=0 2>/dev/null || true + + cat </dev/null || true + + kubectl logs -n "$NAMESPACE" -f -l "job-name=${job_name}" 2>/dev/null & + local log_pid=$! + + while true; do + local status + status=$(kubectl get job "$job_name" -n "$NAMESPACE" -o jsonpath='{.status.conditions[0].type}' 2>/dev/null) + [[ "$status" == "Complete" || "$status" == "Failed" || "$status" == "SuccessCriteriaMet" ]] && break + sleep 2 + done + + kill $log_pid 2>/dev/null || true + wait $log_pid 2>/dev/null || true + + echo "" + if [[ "$status" == "Complete" || "$status" == "SuccessCriteriaMet" ]]; then + echo "Sandbox ready. Connect with: harness connect ${SANDBOX_NAME}" + else + echo "Launcher failed." + fi +} + +# ── Main ────────────────────────────────────────────────────────────── +if [[ "$PLATFORM" == "remote" ]]; then + create_remote +else + create_local +fi diff --git a/openshell-harness-preflight.sh b/bin/scripts/preflight.sh similarity index 100% rename from openshell-harness-preflight.sh rename to bin/scripts/preflight.sh diff --git a/setup-providers.sh b/bin/scripts/providers.sh similarity index 98% rename from setup-providers.sh rename to bin/scripts/providers.sh index ab0bbef..d5e168d 100755 --- a/setup-providers.sh +++ b/bin/scripts/providers.sh @@ -23,6 +23,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +HARNESS_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" CLI="${OPENSHELL_CLI:-openshell}" command -v "$CLI" &>/dev/null || { echo "ERROR: openshell CLI not found."; exit 1; } @@ -54,7 +55,7 @@ echo "=== Enabling providers v2 ===" echo "" echo "=== Importing custom profiles ===" if $FORCE; then - for f in "$SCRIPT_DIR"/sandbox/profiles/*.yaml; do + for f in "$HARNESS_DIR"/sandbox/profiles/*.yaml; do [[ -f "$f" ]] || continue id=$(grep '^id:' "$f" | awk '{print $2}') "$CLI" provider profile delete "$id" 2>/dev/null || true diff --git a/teardown.sh b/bin/scripts/teardown.sh similarity index 100% rename from teardown.sh rename to bin/scripts/teardown.sh diff --git a/deploy-local.sh b/deploy-local.sh deleted file mode 100755 index 43dd756..0000000 --- a/deploy-local.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Verify OpenShell is installed and the local gateway is running. -# -# For local development using Podman or Docker. The gateway is managed -# by the OS package manager (Homebrew on macOS, systemd on Linux) — -# this script just checks it's healthy and prints next steps. -# -# Install OpenShell: -# curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh -# -# Usage: -# ./deploy-local.sh -set -euo pipefail - -if [[ "${OPENSHELL_GATEWAY:-}" == "ocp" ]]; then - echo "ERROR: OPENSHELL_GATEWAY=ocp — this script is for local gateways." - echo " Use ./deploy-ocp.sh for OpenShift, or: unset OPENSHELL_GATEWAY" - exit 1 -fi - -CLI="${OPENSHELL_CLI:-openshell}" -command -v "$CLI" &>/dev/null || { - echo "ERROR: openshell CLI not found. Install it first:" - echo " curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" - exit 1 -} - -echo "=== Local Gateway ===" - -# Check if a gateway is registered and reachable -if "$CLI" gateway list &>/dev/null 2>&1 && "$CLI" inference get &>/dev/null 2>&1; then - echo " Status: running" - "$CLI" gateway list 2>/dev/null | head -5 -elif "$CLI" gateway list &>/dev/null 2>&1; then - echo " Status: registered but not responding" - echo "" - echo " Start the gateway:" - echo " macOS: brew services start openshell-gateway" - echo " Linux: systemctl --user start openshell-gateway" - exit 1 -else - echo " Status: no gateway registered" - echo "" - echo " Install OpenShell (auto-starts the gateway):" - echo " curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" - exit 1 -fi - -# Check container runtime -echo "" -echo "=== Container Runtime ===" -if command -v podman &>/dev/null; then - echo " Podman: $(podman --version 2>/dev/null)" - podman info --format '{{.Host.RemoteSocket.Path}}' 2>/dev/null && echo "" || echo " WARNING: podman socket not accessible" -elif command -v docker &>/dev/null; then - echo " Docker: $(docker --version 2>/dev/null)" -else - echo " ERROR: No container runtime found (need podman or docker)" - exit 1 -fi - -echo "" -echo "════════════════════════════════════════════════════" -echo " Local gateway is ready!" -echo "════════════════════════════════════════════════════" -echo "" -echo "Next steps:" -echo "" -echo " 1. Register providers (one-time per gateway):" -echo " ./setup-providers.sh" -echo "" -echo " 2. Launch a sandbox:" -echo " ./sandbox-local.sh" -echo "" diff --git a/deploy-ocp.sh b/deploy-ocp.sh deleted file mode 100755 index 9ac4673..0000000 --- a/deploy-ocp.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env bash -# Deploy OpenShell to an OpenShift cluster using the official Helm chart. -# -# Uses the OCI Helm chart from ghcr.io — no local OpenShell repo clone needed. -# -# Usage: -# ./deploy-ocp.sh # full deploy -# ./deploy-ocp.sh --kubeconfig ./kubeconfig # explicit kubeconfig -# -# Environment variables (all optional, sensible defaults provided): -# OPENSHELL_CHART_VERSION — Helm chart version (default: from openshell.toml or 0.0.55) -# SANDBOX_IMAGE — sandbox image (default: quay.io/rcochran/openshell:sandbox) -# PULL_SECRET — imagePullSecrets name (default: none) -# SANDBOX_PULL_SECRET — sandbox imagePullSecrets name (default: none) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -# ── Dependency checks ────────────────────────────────────────────────── -for cmd in kubectl helm; do - command -v "$cmd" &>/dev/null || { echo "ERROR: $cmd is required but not found."; exit 1; } -done - -while [[ $# -gt 0 ]]; do - case $1 in - --kubeconfig) export KUBECONFIG="$2"; shift 2 ;; - *) echo "Unknown argument: $1"; exit 1 ;; - esac -done - -# Read chart version from openshell.toml or env -CHART_VERSION="${OPENSHELL_CHART_VERSION:-}" -if [[ -z "$CHART_VERSION" && -f "$SCRIPT_DIR/openshell.toml" ]]; then - CHART_VERSION=$(python3 -c " -import tomllib -with open('$SCRIPT_DIR/openshell.toml', 'rb') as f: - print(tomllib.load(f).get('upstream', {}).get('chart-version', '')) -" 2>/dev/null || true) -fi -CHART_VERSION="${CHART_VERSION:-0.0.55}" -CHART="oci://ghcr.io/nvidia/openshell/helm-chart" - -echo "OpenShell chart: $CHART_VERSION" -echo "KUBECONFIG: ${KUBECONFIG:-default}" -echo "" - -# ── Step 1: Namespace ────────────────────────────────────────────────── -echo "=== Step 1: Creating namespace ===" -kubectl create ns openshell 2>/dev/null || true -kubectl label ns openshell \ - pod-security.kubernetes.io/enforce=privileged \ - pod-security.kubernetes.io/warn=privileged \ - --overwrite - -# ── Step 2: Sandbox CRD + controller ────────────────────────────────── -echo "=== Step 2: Installing Sandbox CRD ===" -kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml - -# ── Step 3: OpenShift SCCs ──────────────────────────────────────────── -echo "=== Step 3: Granting OpenShift SCCs ===" -for sa in openshell openshell-sandbox default; do - oc adm policy add-scc-to-user privileged -z "$sa" -n openshell 2>/dev/null || true -done -oc adm policy add-scc-to-user anyuid -z openshell -n openshell 2>/dev/null || true -kubectl create clusterrolebinding agent-sandbox-admin \ - --clusterrole=cluster-admin \ - --serviceaccount=agent-sandbox-system:agent-sandbox-controller 2>/dev/null || true - -# ── Launcher ServiceAccount + RBAC ──────────────────────────────────── -kubectl apply -n openshell -f - <<'RBAC' -apiVersion: v1 -kind: ServiceAccount -metadata: - name: openshell-launcher - namespace: openshell ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: openshell-launcher - namespace: openshell -rules: - - apiGroups: [""] - resources: ["configmaps", "secrets"] - verbs: ["get", "list"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: openshell-launcher - namespace: openshell -subjects: - - kind: ServiceAccount - name: openshell-launcher - namespace: openshell -roleRef: - kind: Role - name: openshell-launcher - apiGroup: rbac.authorization.k8s.io -RBAC - -# ── Step 4: Helm install gateway ────────────────────────────────────── -echo "=== Step 4: Deploying gateway via Helm ===" - -SANDBOX_IMAGE="${SANDBOX_IMAGE:-quay.io/rcochran/openshell:sandbox}" - -APPS_DOMAIN=$(kubectl get ingresses.config.openshift.io cluster -o jsonpath='{.spec.domain}' 2>/dev/null) -if [[ -z "$APPS_DOMAIN" ]]; then - echo "ERROR: Could not determine OpenShift apps domain." - exit 1 -fi -ROUTE_HOST="gateway-openshell.${APPS_DOMAIN}" - -HELM_ARGS=( - --values "$SCRIPT_DIR/values-ocp.yaml" - --set server.sandboxImage="$SANDBOX_IMAGE" - --set pkiInitJob.serverDnsNames[0]="$ROUTE_HOST" -) - -[[ -n "${PULL_SECRET:-}" ]] && HELM_ARGS+=(--set imagePullSecrets[0].name="$PULL_SECRET") -[[ -n "${SANDBOX_PULL_SECRET:-}" ]] && HELM_ARGS+=(--set server.sandboxImagePullSecrets[0].name="$SANDBOX_PULL_SECRET") - -helm upgrade --install openshell "$CHART" --version "$CHART_VERSION" -n openshell \ - "${HELM_ARGS[@]}" - -echo "=== Waiting for gateway ===" -kubectl rollout status statefulset/openshell -n openshell --timeout=300s - -# ── Step 5: OpenShift Route (TLS passthrough) ───────────────────────── -echo "=== Step 5: Creating OpenShift route ===" -if ! kubectl get route gateway -n openshell &>/dev/null; then - cat <<'ROUTE' | kubectl apply -f - -apiVersion: route.openshift.io/v1 -kind: Route -metadata: - name: gateway - namespace: openshell -spec: - tls: - termination: passthrough - to: - kind: Service - name: openshell - port: - targetPort: grpc -ROUTE -fi -echo " Route: $ROUTE_HOST" - -# ── Step 6: Configure CLI gateway ───────────────────────────────────── -echo "=== Step 6: Configuring CLI gateway ===" -GATEWAY_NAME="${GATEWAY_NAME:-openshell-remote-ocp}" -GATEWAY_URL="https://${ROUTE_HOST}:443" -CLI="${OPENSHELL_CLI:-openshell}" - -if command -v "$CLI" &>/dev/null; then - for gw in $("$CLI" gateway list 2>/dev/null | grep "$ROUTE_HOST" | awk '{gsub(/^\*/, ""); print $1}'); do - "$CLI" gateway remove "$gw" 2>/dev/null || true - done - - "$CLI" gateway add "$GATEWAY_URL" --name "$GATEWAY_NAME" --local 2>/dev/null || true - - MTLS_DIR="$HOME/.config/openshell/gateways/$GATEWAY_NAME/mtls" - kubectl get secret openshell-client-tls -n openshell \ - -o jsonpath='{.data.ca\.crt}' | base64 -d > "$MTLS_DIR/ca.crt" - kubectl get secret openshell-client-tls -n openshell \ - -o jsonpath='{.data.tls\.crt}' | base64 -d > "$MTLS_DIR/tls.crt" - kubectl get secret openshell-client-tls -n openshell \ - -o jsonpath='{.data.tls\.key}' | base64 -d > "$MTLS_DIR/tls.key" - - "$CLI" gateway select "$GATEWAY_NAME" 2>/dev/null || true - echo " ✓ $GATEWAY_NAME registered (certs from cluster)" - - echo -n " Waiting for gateway..." - for i in $(seq 1 30); do - if "$CLI" inference get &>/dev/null; then - echo " ✓ reachable" - break - fi - sleep 2 - echo -n "." - [[ $i -eq 30 ]] && echo " ✗ timed out (try: openshell inference get)" - done -fi - -echo "" -echo "Done. Next: ./setup-creds.sh && ./setup-providers.sh && ./sandbox-ocp.sh" diff --git a/deploy-podman.sh b/deploy-podman.sh deleted file mode 100755 index 0c49c0d..0000000 --- a/deploy-podman.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -# Verify OpenShell is installed and the local gateway is running. -# -# For local development using Podman. The gateway is managed by the OS -# package manager (Homebrew on macOS, systemd on Linux) — this script -# checks it's healthy, selects it, and prints next steps. -# -# Install OpenShell: -# curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh -# -# Usage: -# ./deploy-podman.sh -set -euo pipefail - -CLI="${OPENSHELL_CLI:-openshell}" -command -v "$CLI" &>/dev/null || { - echo "ERROR: openshell CLI not found. Install it first:" - echo " curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" - exit 1 -} - -# Check container runtime -echo "=== Container Runtime ===" -if command -v podman &>/dev/null; then - echo " ✓ Podman: $(podman --version 2>/dev/null)" -else - echo " ✗ Podman not found" - exit 1 -fi - -# Find and select the local gateway -echo "" -echo "=== Gateway ===" -LOCAL_GW=$("$CLI" gateway list 2>/dev/null | awk '/127\.0\.0\.1/ {gsub(/^\*/, ""); print $1; exit}') - -if [[ -z "$LOCAL_GW" ]]; then - echo " ✗ No local gateway found" - echo "" - echo " Install OpenShell (auto-registers the gateway):" - echo " curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" - exit 1 -fi - -"$CLI" gateway select "$LOCAL_GW" 2>/dev/null || true - -if "$CLI" inference get &>/dev/null; then - echo " ✓ $LOCAL_GW (active, reachable)" -else - echo " ✗ $LOCAL_GW (not responding)" - echo "" - echo " Start the gateway:" - echo " macOS: brew services start openshell" - echo " Linux: systemctl --user start openshell" - exit 1 -fi - -echo "" -echo "Done. Next: ./setup-providers.sh && ./sandbox-podman.sh" diff --git a/agents/default.toml b/profiles/default.toml similarity index 100% rename from agents/default.toml rename to profiles/default.toml diff --git a/sandbox-ocp.sh b/sandbox-ocp.sh deleted file mode 100755 index fda787b..0000000 --- a/sandbox-ocp.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash -# Launch a sandbox on OpenShift from an agent config. -# -# Prerequisites: -# ./deploy-ocp.sh && ./setup-creds.sh && ./setup-providers.sh -# -# Usage: -# ./sandbox-ocp.sh # uses agents/default.toml -# ./sandbox-ocp.sh research # uses agents/research.toml -# -# To reconnect: openshell sandbox connect -# To delete: openshell sandbox delete -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/lib/common.sh" -source "$SCRIPT_DIR/lib/agent.sh" -require_cli -require_kubectl - -NAMESPACE="${OPENSHELL_NAMESPACE:-openshell}" -AGENT_NAME="${1:-default}" -AGENT_FILE="$SCRIPT_DIR/agents/${AGENT_NAME}.toml" -parse_agent "$AGENT_FILE" - -echo "=== Agent: $AGENT_NAME ===" -echo " Name: $SANDBOX_NAME" -echo " Image: $SANDBOX_IMAGE" -echo " Providers: $SANDBOX_PROVIDERS" - -# Create ConfigMap with agent config (TOML, same format as the source) -kubectl create configmap "sandbox-${SANDBOX_NAME}" -n "$NAMESPACE" \ - --from-file=config.toml="$AGENT_FILE" \ - --dry-run=client -o yaml | kubectl apply -f - - -# Create sandbox.env ConfigMap -if [[ -n "$SANDBOX_ENV" ]]; then - kubectl create configmap "sandbox-${SANDBOX_NAME}-env" -n "$NAMESPACE" \ - --from-literal=sandbox.env="$SANDBOX_ENV" \ - --dry-run=client -o yaml | kubectl apply -f - -fi - -# Generate and apply the Job -JOB_NAME="sandbox-${SANDBOX_NAME}" -echo "" -echo "=== Launching ===" - -# Clean up previous run -kubectl delete job "$JOB_NAME" -n "$NAMESPACE" --force --grace-period=0 2>/dev/null || true -kubectl delete pod -n "$NAMESPACE" -l "job-name=${JOB_NAME}" --force --grace-period=0 2>/dev/null || true - -cat </dev/null || true - -# Stream logs until the job finishes -kubectl logs -n "$NAMESPACE" -f -l "job-name=${JOB_NAME}" 2>/dev/null & -LOG_PID=$! - -while true; do - status=$(kubectl get job "$JOB_NAME" -n "$NAMESPACE" -o jsonpath='{.status.conditions[0].type}' 2>/dev/null) - [[ "$status" == "Complete" || "$status" == "Failed" || "$status" == "SuccessCriteriaMet" ]] && break - sleep 2 -done - -kill $LOG_PID 2>/dev/null || true -wait $LOG_PID 2>/dev/null || true - -echo "" -if [[ "$status" == "Complete" || "$status" == "SuccessCriteriaMet" ]]; then - echo "Sandbox ready. Connect with: openshell sandbox connect $SANDBOX_NAME" -else - echo "Launcher failed. Check logs: kubectl logs -n $NAMESPACE -l job-name=$JOB_NAME" -fi diff --git a/sandbox-podman.sh b/sandbox-podman.sh deleted file mode 100755 index be67060..0000000 --- a/sandbox-podman.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -# Launch a sandbox on the local Podman gateway. -# -# Prerequisites: -# ./deploy-podman.sh # verify gateway running -# ./setup-providers.sh # register providers -# -# Usage: -# ./sandbox-podman.sh # interactive, uses agents/default.toml -# ./sandbox-podman.sh research # uses agents/research.toml -# ./sandbox-podman.sh --name my-agent # named sandbox -# ./sandbox-podman.sh --no-tty --name test # non-interactive (for testing) -# -# To reconnect: openshell sandbox connect -# To delete: openshell sandbox delete -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/lib/common.sh" -source "$SCRIPT_DIR/lib/agent.sh" -require_cli -require_local_gateway - -# Parse args -AGENT_NAME="default" -NAME_OVERRIDE="" -TTY_FLAG="--tty" - -while [[ $# -gt 0 ]]; do - case "$1" in - --name) NAME_OVERRIDE="$2"; shift 2 ;; - --no-tty) TTY_FLAG="--no-tty"; shift ;; - *) AGENT_NAME="$1"; shift ;; - esac -done - -parse_agent "$SCRIPT_DIR/agents/${AGENT_NAME}.toml" - -echo "=== Providers ===" -build_provider_flags - -# Stage files for upload to /sandbox/.config/openshell/ -HARNESS_DIR="/tmp/openshell" -rm -rf "$HARNESS_DIR" -stage_harness_dir "$HARNESS_DIR" - -# Build flags -FROM_FLAGS=() -[[ -n "$SANDBOX_IMAGE" ]] && FROM_FLAGS=(--from "$SANDBOX_IMAGE") - -NAME_FLAGS=() -[[ -n "$NAME_OVERRIDE" ]] && NAME_FLAGS=(--name "$NAME_OVERRIDE") - -if [[ "$TTY_FLAG" == "--tty" ]]; then - CMD=(-- bash -c ". /sandbox/startup.sh && exec $SANDBOX_COMMAND") -else - CMD=(-- bash /sandbox/startup.sh) -fi - -echo "" -echo "=== Creating sandbox ===" -for attempt in 1 2 3 4 5; do - "$CLI" sandbox create \ - $TTY_FLAG \ - ${NAME_FLAGS[@]+"${NAME_FLAGS[@]}"} \ - ${FROM_FLAGS[@]+"${FROM_FLAGS[@]}"} \ - ${PROVIDER_FLAGS[@]+"${PROVIDER_FLAGS[@]}"} \ - --upload "$HARNESS_DIR:/sandbox/.config" --no-git-ignore \ - "${CMD[@]}" \ - && exit 0 - echo " Attempt $attempt failed (supervisor race), retrying in 5s..." - if [[ -n "$NAME_OVERRIDE" ]]; then - "$CLI" sandbox delete "$NAME_OVERRIDE" 2>/dev/null || true - else - "$CLI" sandbox delete "$("$CLI" sandbox list 2>/dev/null | strip_ansi | awk 'NR==2{print $1}')" 2>/dev/null || true - fi - sleep 5 -done -echo "ERROR: Failed after 5 attempts." -exit 1 diff --git a/test/preflight.bats b/test/preflight.bats index 4d4dab3..22a8cbf 100644 --- a/test/preflight.bats +++ b/test/preflight.bats @@ -5,7 +5,7 @@ # and uses temp TOML files to test every configuration path. REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" -PROVIDERS_PY="$REPO_ROOT/lib/providers.py" +PROVIDERS_PY="$REPO_ROOT/bin/scripts/lib/providers.py" setup() { TEST_TMPDIR="$(mktemp -d)" @@ -55,7 +55,7 @@ run_preflight() { # so we patch it with a wrapper python3 -c " import sys, os -sys.path.insert(0, '$REPO_ROOT/lib') +sys.path.insert(0, '$REPO_ROOT/bin/scripts/lib') os.chdir('$TEST_TMPDIR') from pathlib import Path diff --git a/test-flow.sh b/test/test-flow.sh similarity index 84% rename from test-flow.sh rename to test/test-flow.sh index 8172199..506ab63 100755 --- a/test-flow.sh +++ b/test/test-flow.sh @@ -10,8 +10,8 @@ # ./test-flow.sh all --full # full for both set -uo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/lib/agent.sh" +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +source "$SCRIPT_DIR/bin/scripts/lib/profile.sh" CLI="${OPENSHELL_CLI:-openshell}" TARGET="${1:-}" @@ -116,20 +116,20 @@ test_podman() { $FULL && mode="full" echo "=== test-flow: podman ($mode) ===" - step "teardown" "$SCRIPT_DIR/teardown.sh" --sandboxes --providers - step "deploy" "$SCRIPT_DIR/deploy-podman.sh" - step "setup providers" "$SCRIPT_DIR/setup-providers.sh" + step "teardown" "$SCRIPT_DIR/bin/scripts/teardown.sh" --sandboxes --providers + step "deploy" "$SCRIPT_DIR/bin/scripts/deploy.sh" --local + step "setup providers" "$SCRIPT_DIR/bin/scripts/providers.sh" step "gateway reachable" "$CLI" inference get check_providers if $FULL; then local sandbox_name="test-agent" - step_output "sandbox create" "$SCRIPT_DIR/sandbox-podman.sh" --name "$sandbox_name" --no-tty + step_output "sandbox create" "$SCRIPT_DIR/bin/scripts/new.sh" --local --name "$sandbox_name" --no-tty sandbox_verify "$sandbox_name" step "sandbox delete" "$CLI" sandbox delete "$sandbox_name" fi - step "teardown (clean)" "$SCRIPT_DIR/teardown.sh" --sandboxes --providers + step "teardown (clean)" "$SCRIPT_DIR/bin/scripts/teardown.sh" --sandboxes --providers } # ── OCP flow ───────────────────────────────────────────────────────── @@ -139,15 +139,15 @@ test_ocp() { $FULL && mode="full" echo "=== test-flow: ocp ($mode) ===" - step "teardown" "$SCRIPT_DIR/teardown.sh" - step "deploy" "$SCRIPT_DIR/deploy-ocp.sh" - step "setup creds" "$SCRIPT_DIR/setup-creds.sh" - step "setup providers" "$SCRIPT_DIR/setup-providers.sh" + step "teardown" "$SCRIPT_DIR/bin/scripts/teardown.sh" + step "deploy" "$SCRIPT_DIR/bin/scripts/deploy.sh" --remote + step "setup creds" "$SCRIPT_DIR/bin/scripts/creds.sh" + step "setup providers" "$SCRIPT_DIR/bin/scripts/providers.sh" step "gateway reachable" "$CLI" inference get check_providers if $FULL; then - step_output "sandbox create" "$SCRIPT_DIR/sandbox-ocp.sh" + step_output "sandbox create" "$SCRIPT_DIR/bin/scripts/new.sh" --remote local sandbox_name="agent" # Wait for ready @@ -160,7 +160,7 @@ test_ocp() { step "sandbox delete" "$CLI" sandbox delete "$sandbox_name" fi - step "teardown (clean)" "$SCRIPT_DIR/teardown.sh" + step "teardown (clean)" "$SCRIPT_DIR/bin/scripts/teardown.sh" } # ── Main ─────────────────────────────────────────────────────────────