diff --git a/Makefile b/Makefile index 6f7285e..c5b4487 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,7 @@ SANDBOX_IMAGE := $(REGISTRY):sandbox LAUNCHER_IMAGE := $(REGISTRY):launcher .PHONY: cli sandbox push-sandbox cli-launcher launcher push-launcher \ - test-unit test test-podman test-ocp \ - test-go-podman test-go-ocp test-all validate clean help + test-unit test test-podman test-ocp test-all validate clean help ## ── CLI ────────────────────────────────────────────────────────────── @@ -56,61 +55,43 @@ test-unit: cd sandbox/launcher && go test ./... bats test/preflight.bats -## Bash + both platforms (full lifecycle, rebuilds images) -test: sandbox push-launcher +## Both platforms (full lifecycle, rebuilds images) +test: cli sandbox push-launcher ./test/test-flow.sh all --full -## Bash + podman (full lifecycle) -test-podman: sandbox push-launcher +## Podman only (full lifecycle) +test-podman: cli sandbox push-launcher ./test/test-flow.sh podman --full -## Bash + OCP (full lifecycle) -test-ocp: sandbox push-launcher +## OCP only (full lifecycle) +test-ocp: cli sandbox push-launcher ./test/test-flow.sh ocp --full -## Go + podman (full lifecycle, rebuilds CLI + images) -test-go-podman: cli sandbox push-launcher - ./test/test-flow.sh podman --full --go - -## Go + OCP (full lifecycle) -test-go-ocp: cli sandbox push-launcher - ./test/test-flow.sh ocp --full --go - -## All 4 combinations: {bash,go} x {podman,ocp} +## All combinations: podman + ocp test-all: cli sandbox push-launcher ./test/test-flow.sh all --full - ./test/test-flow.sh all --full --go -## Full validation: unit tests + bats (both paths) + integration (all 4 combos) +## Full validation: unit tests + bats + integration (podman + OCP) ## Run this before every commit. validate: cli sandbox push-launcher @echo "=== Unit tests ===" CGO_ENABLED=0 go test ./... cd sandbox/launcher && go test ./... @echo "" - @echo "=== Bats (Python) ===" + @echo "=== Bats ===" bats test/preflight.bats @echo "" - @echo "=== Bats (Go) ===" - USE_GO=true bats test/preflight.bats - @echo "" - @echo "=== Integration: bash + podman ===" + @echo "=== Integration: podman ===" ./test/test-flow.sh podman --full @echo "" - @echo "=== Integration: Go + podman ===" - ./test/test-flow.sh podman --full --go - @echo "" - @echo "=== Integration: bash + OCP ===" + @echo "=== Integration: OCP ===" ./test/test-flow.sh ocp --full - @echo "" - @echo "=== Integration: Go + OCP ===" - ./test/test-flow.sh ocp --full --go ## ── Convenience targets ─────────────────────────────────────────────── ## Clean built binaries clean: - rm -f harness sandbox/launcher/openshell sandbox/launcher/launcher + rm -f harness sandbox/launcher/launcher @echo "Cleaned binaries" ## Show available targets diff --git a/bin/harness b/bin/harness deleted file mode 100755 index 326749c..0000000 --- a/bin/harness +++ /dev/null @@ -1,52 +0,0 @@ -#!/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 deleted file mode 100755 index 2778a0f..0000000 --- a/bin/scripts/connect.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -# Connect to a running sandbox. -set -euo pipefail -CLI="${OPENSHELL_CLI:-openshell}" -exec "$CLI" sandbox connect "${1:-}" diff --git a/bin/scripts/creds.sh b/bin/scripts/creds.sh deleted file mode 100755 index f7a3cd9..0000000 --- a/bin/scripts/creds.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -# Store non-provider credentials as K8s Secrets in the openshell namespace. -# -# Run once after deploy-ocp.sh. Credentials are stored in the cluster and -# mounted into sandbox launcher pods — they never transit the kubectl client -# when launching sandboxes. -# -# For local mode, this script is NOT needed — sandbox-podman.sh handles -# credential staging at launch time. -# -# Usage: -# ./setup-creds.sh # create missing secrets -# ./setup-creds.sh --force # delete and recreate all secrets -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/lib/common.sh" - -NAMESPACE="${OPENSHELL_NAMESPACE:-openshell}" -FORCE=false -[[ "${1:-}" == "--force" ]] && FORCE=true - -command -v kubectl &>/dev/null || { echo "ERROR: kubectl is required."; exit 1; } - -kubectl get ns "$NAMESPACE" &>/dev/null || { - echo "ERROR: Namespace '$NAMESPACE' not found. Run ./deploy-ocp.sh first." - exit 1 -} - -secret_exists() { - kubectl get secret "$1" -n "$NAMESPACE" &>/dev/null -} - -echo "=== Setting up cluster credentials ===" -echo " Namespace: $NAMESPACE" -echo "" - -# ── GWS credentials ──────────────────────────────────────────────────── -echo "=== GWS ===" -if $FORCE && secret_exists openshell-gws; then - kubectl delete secret openshell-gws -n "$NAMESPACE" - echo " Deleted existing secret." -fi - -if secret_exists openshell-gws; then - echo " openshell-gws: exists (use --force to recreate)" -else - TMPDIR=$(mktemp -d) - trap 'rm -rf "$TMPDIR"' EXIT - if export_gws_creds "$TMPDIR"; then - ARGS=(--from-file=credentials.json="$TMPDIR/gws-config/credentials.json") - [[ -f "$TMPDIR/gws-config/client_secret.json" ]] && \ - ARGS+=(--from-file=client_secret.json="$TMPDIR/gws-config/client_secret.json") - kubectl create secret generic openshell-gws -n "$NAMESPACE" "${ARGS[@]}" - echo " openshell-gws: created" - fi -fi - -# ── Atlassian non-secret config ──────────────────────────────────────── -echo "" -echo "=== Atlassian ===" -if $FORCE && secret_exists openshell-atlassian; then - kubectl delete secret openshell-atlassian -n "$NAMESPACE" - echo " Deleted existing secret." -fi - -if secret_exists openshell-atlassian; then - echo " openshell-atlassian: exists (use --force to recreate)" -elif [[ -n "${JIRA_URL:-}" ]]; then - kubectl create secret generic openshell-atlassian -n "$NAMESPACE" \ - --from-literal=JIRA_URL="$JIRA_URL" \ - --from-literal=JIRA_USERNAME="${JIRA_USERNAME:-}" - echo " openshell-atlassian: created ($JIRA_URL)" -else - echo " Atlassian: JIRA_URL not set (skipping)" -fi - -echo "" -echo "Done. Run ./openshell-harness-preflight.sh to verify all prerequisites." diff --git a/bin/scripts/deploy.sh b/bin/scripts/deploy.sh deleted file mode 100755 index 0946875..0000000 --- a/bin/scripts/deploy.sh +++ /dev/null @@ -1,235 +0,0 @@ -#!/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/bin/scripts/lib/common.sh b/bin/scripts/lib/common.sh deleted file mode 100644 index 7e41a7c..0000000 --- a/bin/scripts/lib/common.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# Shared helpers for harness scripts. -# -# Source from any script: -# source "$(dirname "$0")/lib/common.sh" - -# ── CLI detection ────────────────────────────────────────────────────── - -CLI="${OPENSHELL_CLI:-openshell}" - -require_cli() { - command -v "$CLI" &>/dev/null || { echo "ERROR: openshell CLI not found."; exit 1; } -} - -require_kubectl() { - command -v kubectl &>/dev/null || { echo "ERROR: kubectl is required."; exit 1; } -} - -# ── Gateway validation ──────────────────────────────────────────────── - -require_local_gateway() { - local gw - gw=$("$CLI" gateway list 2>/dev/null | awk '/127\.0\.0\.1/ {print $1; exit}') - if [[ -z "$gw" ]]; then - echo "ERROR: No local gateway registered." - echo " Install OpenShell or run: ./deploy-podman.sh" - exit 1 - fi -} - -# ── Environment pre-flight ───────────────────────────────────────────── - -check_env() { - local script_dir - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - python3 "$script_dir/lib/providers.py" check "$@" -} - -# ── GWS credential export ───────────────────────────────────────────── -# Used by: setup-creds.sh (K8s secret creation) - -export_gws_creds() { - local outdir="$1" - if ! command -v gws &>/dev/null; then - echo " GWS: not installed (skipping)" - return 1 - fi - if ! gws auth status &>/dev/null; then - echo " GWS: not authenticated (run 'gws auth login')" - return 1 - fi - mkdir -p "$outdir/gws-config" - if gws auth export --unmasked > "$outdir/gws-config/credentials.json" 2>/dev/null; then - local gws_dir="${GWS_CONFIG_DIR:-$HOME/.config/gws}" - [[ -f "$gws_dir/client_secret.json" ]] && cp "$gws_dir/client_secret.json" "$outdir/gws-config/" - chmod 600 "$outdir/gws-config"/* - echo " GWS: exported" - return 0 - else - echo " GWS: export failed (skipping)" - rm -rf "$outdir/gws-config" - return 1 - fi -} diff --git a/bin/scripts/lib/parse-profile.py b/bin/scripts/lib/parse-profile.py deleted file mode 100644 index 3900f0f..0000000 --- a/bin/scripts/lib/parse-profile.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -"""Parse a profile TOML file and output shell variable assignments. - -Usage: - python3 parse-profile.py - -Output (eval-safe shell assignments): - SANDBOX_NAME='agent' - SANDBOX_IMAGE='quay.io/...' - SANDBOX_COMMAND='claude --bare' - SANDBOX_KEEP='true' - SANDBOX_PROVIDERS='github vertex-local atlassian' - SANDBOX_ENV='export KEY=value\n...' -""" -import shlex -import sys - -try: - import tomllib -except ImportError: - import tomli as tomllib - -if len(sys.argv) < 2: - print("Usage: parse-profile.py ", file=sys.stderr) - sys.exit(1) - -with open(sys.argv[1], "rb") as f: - c = tomllib.load(f) - -print(f"SANDBOX_NAME={shlex.quote(c.get('name', 'agent'))}") -print(f"SANDBOX_IMAGE={shlex.quote(c.get('image', ''))}") -print(f"SANDBOX_COMMAND={shlex.quote(c.get('command', 'claude --bare'))}") -print(f"SANDBOX_KEEP={shlex.quote(str(c.get('keep', True)).lower())}") - -providers = c.get("providers", []) -print(f"SANDBOX_PROVIDERS={shlex.quote(' '.join(providers))}") - -env = c.get("env", {}) -lines = [f"export {k}={v}" for k, v in env.items()] -print(f"SANDBOX_ENV={shlex.quote(chr(10).join(lines) + chr(10))}") diff --git a/bin/scripts/lib/profile.sh b/bin/scripts/lib/profile.sh deleted file mode 100644 index f261f5f..0000000 --- a/bin/scripts/lib/profile.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# Profile parsing helpers. -# -# Source from any script: -# source "$(dirname "$0")/lib/profile.sh" -# -# Usage: -# parse_profile profiles/default.toml -# # Sets: SANDBOX_IMAGE, SANDBOX_COMMAND, SANDBOX_NAME, -# # SANDBOX_PROVIDERS, SANDBOX_ENV, SANDBOX_KEEP - -LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -parse_profile() { - local profile_file="$1" - [[ -f "$profile_file" ]] || { echo "ERROR: $profile_file not found."; exit 1; } - eval "$(python3 "$LIB_DIR/parse-profile.py" "$profile_file")" -} - -# Build provider flags array from SANDBOX_PROVIDERS. -# Sets: PROVIDER_FLAGS array -build_provider_flags() { - PROVIDER_FLAGS=() - for name in $SANDBOX_PROVIDERS; do - if "$CLI" provider get "$name" &>/dev/null; then - PROVIDER_FLAGS+=(--provider "$name") - echo " $name: attached" - else - echo " $name: not registered (skipping)" - fi - done -} - -# Stage sandbox.env + GWS credentials to a directory for upload. -# The directory name must be "openshell" so upload lands at /sandbox/.config/openshell/. -stage_harness_dir() { - local dir="$1" - mkdir -p "$dir" - - if [[ -n "${SANDBOX_ENV:-}" ]]; then - echo "$SANDBOX_ENV" > "$dir/sandbox.env" - fi - - if command -v gws &>/dev/null && gws auth status &>/dev/null 2>&1; then - gws auth export --unmasked > "$dir/credentials.json" 2>/dev/null - cp ~/.config/gws/client_secret.json "$dir/client_secret.json" 2>/dev/null || true - echo " GWS: exported" - else - echo " GWS: not configured (skipping)" - fi -} - -# Strip ANSI escape codes from stdin. -strip_ansi() { - sed 's/\x1b\[[0-9;]*m//g' -} diff --git a/bin/scripts/lib/providers.py b/bin/scripts/lib/providers.py deleted file mode 100644 index c8f0b19..0000000 --- a/bin/scripts/lib/providers.py +++ /dev/null @@ -1,319 +0,0 @@ -#!/usr/bin/env python3 -"""Pre-flight checks driven by providers.toml + openshell.toml. - -Usage: - python3 lib/providers.py check # full pre-flight - python3 lib/providers.py check --strict # fail if required missing - python3 lib/providers.py available # available provider names - python3 lib/providers.py names # all enabled provider names -""" - -import json -import os -import re -import shutil -import subprocess -import sys -import tomllib -from pathlib import Path - -ROOT = Path(__file__).parent.parent.parent.parent -PROVIDERS_TOML = ROOT / "providers.toml" -CONFIG_TOML = ROOT / "openshell.toml" -CLI = os.environ.get("OPENSHELL_CLI", "openshell") - - -def load_providers(): - with open(PROVIDERS_TOML, "rb") as f: - return tomllib.load(f).get("providers", []) - - -def load_config(): - if not CONFIG_TOML.exists(): - return {} - with open(CONFIG_TOML, "rb") as f: - return tomllib.load(f) - - -def expand_path(p): - return Path(os.path.expanduser(os.path.expandvars(p))) - - -def run_quiet(cmd, timeout=5, env=None): - try: - r = subprocess.run(cmd, shell=isinstance(cmd, str), capture_output=True, timeout=timeout, env=env) - return r.returncode == 0, r.stdout.decode().strip() - except (subprocess.TimeoutExpired, FileNotFoundError): - return False, "" - - -def mask_value(val, show=4): - if not val or len(val) <= show: - return "***" - return val[:show] + "***" - - -def file_metadata(path): - p = expand_path(path) - if not p.exists(): - return None - try: - with open(p) as f: - data = json.load(f) - meta = {} - if "quota_project_id" in data: - meta["project"] = data.get("quota_project_id", "") - meta["type"] = data.get("type", "") - if "installed" in data: - meta["client_id"] = mask_value(data["installed"].get("client_id", "")) - return meta or None - except (json.JSONDecodeError, KeyError, OSError): - return None - - -def strip_ansi(s): - return re.sub(r'\x1b\[[0-9;]*m', '', s) - - -# ── Input checking ──────────────────────────────────────────────────── - -def check_input(inp): - """Check a single input. Returns (ok, detail_line).""" - key = inp["key"] - kind = inp.get("kind", "env") - secret = inp.get("secret", False) - desc = inp.get("description", "") - - if kind == "env": - val = os.environ.get(key) - if val: - display = mask_value(val) if secret else val - return True, f"✓ local env: {key}={display}" - else: - return False, f"✗ local env: {key} not set → export {key}=..." - - elif kind == "file": - ep = expand_path(key) - if ep.exists(): - meta = file_metadata(key) - if meta and secret: - safe = {k: v for k, v in meta.items() if k in ("project", "type")} - masked = {k: v for k, v in meta.items() if k not in ("project", "type")} - parts = [f"{k}={v}" for k, v in safe.items() if v] - parts += [f"{k}={v}" for k, v in masked.items() if v] - return True, f"✓ local file: {key} ({', '.join(parts)})" if parts else f"✓ local file: {key}" - elif meta: - meta_str = ", ".join(f"{k}={v}" for k, v in meta.items() if v) - return True, f"✓ local file: {key} ({meta_str})" if meta_str else f"✓ local file: {key}" - return True, f"✓ local file: {key}" - else: - return False, f"✗ local file: {key} not found" - - elif kind == "check": - expanded = os.path.expandvars(key) - ok, _ = run_quiet(expanded) - sym = "✓" if ok else "✗" - # Show original key (with ${VAR} references), not expanded values - return ok, f"{sym} check: {key}" - - return False, f"{key}: unknown kind '{kind}'" - - -def check_provider(provider): - """Check all inputs for a provider. Returns (ok, details).""" - issues = 0 - details = [] - for inp in provider.get("inputs", []): - ok, detail = check_input(inp) - if not ok: - issues += 1 - details.append(detail) - return issues == 0, details - - -# ── Enabled providers ───────────────────────────────────────────────── - -def enabled_providers(all_providers, config): - """Filter to enabled providers based on openshell.toml.""" - enabled_names = set() - if config: - enabled_names.update(config.get("providers", [])) - enabled_names.update(config.get("providers-custom", [])) - else: - return all_providers # no config = all enabled - - return [p for p in all_providers if p["name"] in enabled_names] - - -# ── Commands ────────────────────────────────────────────────────────── - -def cmd_check(strict=False): - all_providers = load_providers() - config = load_config() - providers = enabled_providers(all_providers, config) - has_failures = False - - # OpenShell CLI - print("=== OpenShell CLI ===") - cli_exists = shutil.which(CLI) is not None - if not cli_exists: - print(" ✗ not found on PATH") - has_failures = True - else: - _, ver = run_quiet([CLI, "--version"]) - print(f" ✓ {ver or CLI}") - cli_path = shutil.which(CLI) - print(f" {cli_path}") - - # Detect active gateway from CLI - active_gw = "" - if cli_exists: - _, gw_list = run_quiet([CLI, "gateway", "list"]) - for line in gw_list.splitlines(): - if line.startswith("*"): - active_gw = line.split()[1] if len(line.split()) > 1 else "" - break - is_k8s = "-remote-" in active_gw - - # Gateway — only check the relevant one - gw_ok = False - if is_k8s: - print() - print("=== K8s gateway ===") - has_kubectl = shutil.which("kubectl") is not None - - if not has_kubectl: - print(" ✗ kubectl not found") - has_failures = True - else: - cluster_ok, ctx = run_quiet("kubectl config current-context") - if cluster_ok: - print(f" ✓ Cluster: {ctx}") - else: - print(" ✗ No cluster (kubectl not configured)") - has_failures = True - - if cluster_ok and cli_exists: - ocp_ok, out = run_quiet([CLI, "inference", "get"]) - if ocp_ok: - gw_ok = True - model = "" - for line in out.splitlines(): - if "Model:" in line: - model = strip_ansi(line.split("Model:")[1]).strip() - print(f" ✓ Gateway reachable (model: {model})" if model else " ✓ Gateway reachable") - else: - print(" ✗ Gateway unreachable") - else: - print() - print("=== Podman gateway ===") - if cli_exists: - local_ok, out = run_quiet([CLI, "inference", "get"]) - if local_ok: - gw_ok = True - model = "" - for line in out.splitlines(): - if "Model:" in line: - model = strip_ansi(line.split("Model:")[1]).strip() - print(f" ✓ Reachable (model: {model})" if model else " ✓ Reachable") - else: - print(" - Not running") - - if shutil.which("podman"): - _, ver = run_quiet(["podman", "--version"]) - print(f" ✓ Podman: {ver}") - else: - print(" ✗ Podman not found") - has_failures = True - else: - print(" - CLI not available") - - # Registered providers - if cli_exists and gw_ok: - print() - gw_label = "k8s" if is_k8s else "podman" - print(f"=== Registered providers ({gw_label}) ===") - openshell_providers = [p for p in providers if p.get("type") == "openshell"] - for p in openshell_providers: - reg_ok, _ = run_quiet([CLI, "provider", "get", p["name"]]) - if reg_ok: - print(f" ✓ {p['name']}") - else: - print(f" ✗ {p['name']}: not registered — run ./setup-providers.sh") - has_failures = True - - # Provider inputs - print() - print("=== Provider inputs ===") - for p in providers: - ok, details = check_provider(p) - name = p["name"] - desc = p.get("description", "") - required = p.get("required", False) - - if ok: - print(f" ✓ {name}") - else: - print(f" ✗ {name}") - if required: - has_failures = True - print(f" {desc}") - - for d in details: - print(f" {d}") - - upstream = p.get("upstream") - if upstream and not ok: - print(f" upstream: {upstream}") - print() - - # Summary - if has_failures: - print("✗ Not ready — fix issues above") - if strict: - sys.exit(1) - else: - print("✓ Ready to launch") - - -def cmd_available(): - providers = enabled_providers(load_providers(), load_config()) - available = [] - for p in providers: - if p.get("type") != "openshell": - continue - ok, _ = check_provider(p) - if ok: - available.append(p["name"]) - print(" ".join(available)) - - -def cmd_names(): - providers = enabled_providers(load_providers(), load_config()) - names = [p["name"] for p in providers if p.get("type") == "openshell"] - print(" ".join(names)) - - -def cmd_sandbox_env(): - """Print sandbox env vars from openshell.toml [sandbox.env] as export statements.""" - config = load_config() - env = config.get("sandbox", {}).get("env", {}) - for key, val in env.items(): - print(f"export {key}={val}") - - -if __name__ == "__main__": - cmd = sys.argv[1] if len(sys.argv) > 1 else "check" - strict = "--strict" in sys.argv - - if cmd == "check": - cmd_check(strict=strict) - elif cmd == "available": - cmd_available() - elif cmd == "names": - cmd_names() - elif cmd == "sandbox-env": - cmd_sandbox_env() - else: - print(f"Unknown command: {cmd}", file=sys.stderr) - sys.exit(1) diff --git a/bin/scripts/new.sh b/bin/scripts/new.sh deleted file mode 100755 index 3a6f439..0000000 --- a/bin/scripts/new.sh +++ /dev/null @@ -1,218 +0,0 @@ -#!/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_profile "$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/bin/scripts/preflight.sh b/bin/scripts/preflight.sh deleted file mode 100755 index 7a5a6e8..0000000 --- a/bin/scripts/preflight.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -# Pre-flight check for the OpenShell sandbox environment. -# Read-only — prints the status of all prerequisites, no mutations. -# -# Checks: env vars, paths, gateway, registered providers, platform. -# All driven by providers.toml (definitions) + openshell.toml (config). -# -# Usage: -# ./openshell-harness-preflight.sh # best-effort report -# ./openshell-harness-preflight.sh --strict # exit 1 if required providers missing -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -echo "" -echo "╔══════════════════════════════════════════╗" -echo "║ OpenShell Sandbox Pre-flight ║" -echo "╚══════════════════════════════════════════╝" -echo "" - -python3 "$SCRIPT_DIR/lib/providers.py" check "$@" diff --git a/bin/scripts/providers.sh b/bin/scripts/providers.sh deleted file mode 100755 index 4c9f10d..0000000 --- a/bin/scripts/providers.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash -# Register credential providers with the OpenShell gateway. -# -# Skips providers that already exist. Use --force to delete and recreate -# all providers (requires no running sandboxes — run openshell sandbox list | awk 'NR>1{print $1}' | xargs -I{} openshell sandbox delete {} first). -# -# Run once after deploy-ocp.sh. Providers are stored in the gateway database -# and survive redeployments (same PVC). -# -# Prerequisites: -# - Gateway deployed and reachable -# - GITHUB_TOKEN in environment -# - gcloud auth application-default login completed -# - JIRA_API_TOKEN in environment (for Atlassian) -# -# Optional env vars: -# ANTHROPIC_VERTEX_PROJECT_ID — GCP project (falls back to ADC quota_project_id) -# CLOUD_ML_REGION — Vertex AI region (default: us-east5) -# -# Usage: -# ./setup-providers.sh # create missing providers -# ./setup-providers.sh --force # delete and recreate all providers -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; } -command -v jq &>/dev/null || { echo "ERROR: jq is required for ADC parsing."; exit 1; } - -FORCE=false -[[ "${1:-}" == "--force" ]] && FORCE=true - -MODEL="${OPENSHELL_MODEL:-claude-sonnet-4-6}" - -provider_exists() { "$CLI" provider get "$1" &>/dev/null; } - -# ── Force mode: require no running sandboxes ─────────────────────────── -if $FORCE; then - sandboxes=$("$CLI" sandbox list 2>/dev/null | awk 'NR>1 {print $1}') - if [[ -n "$sandboxes" ]]; then - echo "ERROR: Cannot --force with running sandboxes. Run openshell sandbox list | awk 'NR>1{print $1}' | xargs -I{} openshell sandbox delete {} first." - exit 1 - fi - for name in github vertex-local atlassian; do - "$CLI" provider delete "$name" 2>/dev/null || true - done - echo "Deleted existing providers." -fi - -echo "=== Enabling providers v2 ===" -"$CLI" settings set --global --key providers_v2_enabled --value true --yes - -echo "" -echo "=== Importing custom profiles ===" -if $FORCE; then - 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 - done -fi -"$CLI" provider profile import --from "$HARNESS_DIR/sandbox/profiles/" 2>/dev/null || echo " (already imported)" - -echo "" -echo "=== Registering providers ===" - -# ── GitHub ───────────────────────────────────────────────────────────── -# The built-in github profile supports read-only access (clone/fetch). -# For push access, use openshell policy set to add git-receive-pack rules. -# See: https://github.com/NVIDIA/OpenShell/blob/main/docs/get-started/tutorials/github-sandbox.mdx -if [[ -n "${GITHUB_TOKEN:-}" ]]; then - if ! provider_exists github; then - "$CLI" provider create --name github --type github \ - --credential "GITHUB_TOKEN=${GITHUB_TOKEN}" - echo " github — registered" - else - echo " github — exists (use --force to recreate)" - fi -else - echo " github — skipped (GITHUB_TOKEN not set)" -fi - -# ── Vertex AI ────────────────────────────────────────────────────────── -# Project and region: ANTHROPIC_VERTEX_PROJECT_ID / CLOUD_ML_REGION first, -# then fall back to ADC file's quota_project_id. -ADC="${GOOGLE_APPLICATION_CREDENTIALS:-$HOME/.config/gcloud/application_default_credentials.json}" -PROJECT="${ANTHROPIC_VERTEX_PROJECT_ID:-}" -REGION="${CLOUD_ML_REGION:-global}" -[[ -z "$PROJECT" && -f "$ADC" ]] && PROJECT=$(jq -r '.quota_project_id // empty' "$ADC") - -if [[ -f "$ADC" && -n "$PROJECT" ]]; then - if ! provider_exists vertex-local; then - "$CLI" provider create --name vertex-local --type google-vertex-ai \ - --from-gcloud-adc \ - --config "VERTEX_AI_PROJECT_ID=${PROJECT}" \ - --config "VERTEX_AI_REGION=${REGION}" - echo " vertex-local — registered (project: $PROJECT, region: $REGION)" - else - echo " vertex-local — exists (use --force to recreate)" - fi - "$CLI" inference set --provider vertex-local --model "$MODEL" --no-verify - echo " inference — model: $MODEL" -elif [[ ! -f "$ADC" ]]; then - echo " vertex-local — skipped (no ADC file at $ADC)" -else - echo " vertex-local — skipped (no project ID — set ANTHROPIC_VERTEX_PROJECT_ID or run gcloud auth application-default login)" -fi - -# ── Atlassian ────────────────────────────────────────────────────────── -# Only JIRA_API_TOKEN is a provider credential (proxy-resolved in Basic auth). -# JIRA_URL and JIRA_USERNAME are non-secret config uploaded by sandbox-ocp.sh. -if [[ -n "${JIRA_API_TOKEN:-}" ]]; then - if ! provider_exists atlassian; then - "$CLI" provider create --name atlassian --type atlassian \ - --credential "JIRA_API_TOKEN=${JIRA_API_TOKEN}" - echo " atlassian — registered" - else - echo " atlassian — exists (use --force to recreate)" - fi -else - echo " atlassian — skipped (JIRA_API_TOKEN not set)" -fi - -echo "" -echo "=== Providers ===" -"$CLI" provider list - -echo "" -echo "=== Inference ===" -"$CLI" inference get - -echo "" -echo "Done. Launch a sandbox with: ./sandbox-ocp.sh --name my-agent" diff --git a/bin/scripts/teardown.sh b/bin/scripts/teardown.sh deleted file mode 100755 index c280930..0000000 --- a/bin/scripts/teardown.sh +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env bash -# Tear down sandboxes, providers, k8s resources, or all. -# -# Usage: -# ./teardown.sh # delete everything (sandboxes + providers + k8s if active gateway is remote) -# ./teardown.sh --sandboxes # delete sandboxes only -# ./teardown.sh --providers # delete providers only -# ./teardown.sh --k8s # delete k8s resources only (secrets, RBAC, route) -# -# Detects local vs k8s from the active gateway name. -# Safe to run multiple times — each step tolerates missing resources. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/lib/common.sh" - -require_cli - -NAMESPACE="${OPENSHELL_NAMESPACE:-openshell}" - -# Detect active gateway -ACTIVE_GW=$("$CLI" gateway list 2>/dev/null | awk '/^\*/ {print $2}' || true) - -# Detect k8s cluster — just check if the namespace exists -HAS_K8S=false -if command -v kubectl &>/dev/null && kubectl get ns "$NAMESPACE" &>/dev/null 2>&1; then - HAS_K8S=true -fi - -DO_SANDBOXES=false -DO_PROVIDERS=false -DO_K8S=false - -if [[ $# -eq 0 ]]; then - DO_SANDBOXES=true - DO_PROVIDERS=true - DO_K8S=true -fi - -while [[ $# -gt 0 ]]; do - case "$1" in - --sandboxes) DO_SANDBOXES=true ;; - --providers) DO_PROVIDERS=true ;; - --k8s) DO_K8S=true ;; - *) echo "Usage: $0 [--sandboxes] [--providers] [--k8s]"; exit 1 ;; - esac - shift -done - -if [[ -n "$ACTIVE_GW" ]]; then - echo "Active gateway: $ACTIVE_GW" -else - echo "Active gateway: none" -fi -echo "" - -# ── Sandboxes ───────────────────────────────────────────────────────── -if $DO_SANDBOXES; then - echo "=== Sandboxes ===" - if [[ -z "$ACTIVE_GW" ]]; then - echo " No active gateway, skipping" - else - sandboxes=$("$CLI" sandbox list 2>/dev/null | awk 'NR>1 && NF {print $1}' || true) - if [[ -z "${sandboxes// /}" ]]; then - echo " None running" - else - while read -r name; do - echo " Deleting $name" - "$CLI" sandbox delete "$name" 2>/dev/null || true - done <<< "$sandboxes" - fi - fi - echo "" -fi - -# ── Providers ───────────────────────────────────────────────────────── -if $DO_PROVIDERS; then - echo "=== Providers ===" - if [[ -z "$ACTIVE_GW" ]]; then - echo " No active gateway, skipping" - else - remaining=$("$CLI" sandbox list 2>/dev/null | awk 'NR>1 && NF {print $1}' || true) - if [[ -n "${remaining// /}" ]]; then - echo "ERROR: Cannot delete providers with running sandboxes." - echo " Run: ./teardown.sh --sandboxes" - exit 1 - fi - - providers=$("$CLI" provider list 2>/dev/null | awk 'NR>1 && NF {print $1}' || true) - if [[ -z "${providers// /}" ]]; then - echo " None registered" - else - while read -r name; do - echo " Deleting $name" - "$CLI" provider delete "$name" 2>/dev/null || true - done <<< "$providers" - fi - - echo "" - echo "=== Inference ===" - "$CLI" inference remove 2>/dev/null && echo " Cleared" || echo " Already cleared" - fi - echo "" -fi - -# ── K8s resources ───────────────────────────────────────────────────── -if $DO_K8S; then - if ! $HAS_K8S; then - echo "=== K8s ===" - echo " No openshell namespace found, skipping" - else - echo "=== Helm release ===" - helm uninstall openshell -n "$NAMESPACE" 2>/dev/null && \ - echo " Uninstalled" || echo " Not installed" - - echo "" - echo "=== Sandbox CRD ===" - kubectl delete ns agent-sandbox-system 2>/dev/null && \ - echo " Deleted agent-sandbox-system" || echo " Not found" - - echo "" - echo "=== OpenShift SCCs ===" - for sa in openshell openshell-sandbox default; do - oc adm policy remove-scc-from-user privileged -z "$sa" -n "$NAMESPACE" 2>/dev/null || true - done - oc adm policy remove-scc-from-user anyuid -z openshell -n "$NAMESPACE" 2>/dev/null || true - kubectl delete clusterrolebinding agent-sandbox-admin 2>/dev/null || true - echo " Cleared" - - echo "" - echo "=== K8s secrets ===" - for secret in openshell-gws openshell-atlassian; do - kubectl delete secret "$secret" -n "$NAMESPACE" 2>/dev/null && \ - echo " Deleted $secret" || echo " $secret: not found" - done - - echo "" - echo "=== Namespace ===" - kubectl delete ns "$NAMESPACE" 2>/dev/null && \ - echo " Deleted $NAMESPACE" || echo " $NAMESPACE: not found" - - echo "" - echo "=== Gateway config ===" - # Remove all non-local gateways (covers legacy 'ocp' and current 'openshell-remote-ocp') - for gw in $("$CLI" gateway list 2>/dev/null | awk 'NR>1 && !/127\.0\.0\.1/ {gsub(/^\*/, ""); print $1}'); do - "$CLI" gateway remove "$gw" 2>/dev/null && \ - echo " Removed gateway '$gw'" || true - done - # Select local gateway if available - local_gw=$("$CLI" gateway list 2>/dev/null | awk '/127\.0\.0\.1/ {gsub(/^\*/, ""); print $1; exit}') - [[ -n "$local_gw" ]] && "$CLI" gateway select "$local_gw" 2>/dev/null || true - - echo "" - echo "=== Local files ===" - rm -f "$SCRIPT_DIR/.gateway-env" && echo " Removed .gateway-env" || true - rm -rf "$SCRIPT_DIR/.certs" && echo " Removed .certs/" || true - echo "" - fi -fi - -echo "Done." diff --git a/cmd/new.go b/cmd/new.go index 082c2fa..cbae526 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -12,7 +12,6 @@ import ( "github.com/robbycochran/harness-openshell/internal/gateway" "github.com/robbycochran/harness-openshell/internal/k8s" "github.com/robbycochran/harness-openshell/internal/profile" - "github.com/robbycochran/harness-openshell/internal/runner" "github.com/robbycochran/harness-openshell/internal/status" "github.com/spf13/cobra" ) @@ -47,10 +46,7 @@ func NewNewCmd(harnessDir, cli string) *cobra.Command { profileName: profileName, sandboxName: sandboxName, noTTY: noTTY, - runScript: func(name string, args ...string) error { - return runner.RunScript(harnessDir, name, args...) - }, - retrySleep: 5 * time.Second, + retrySleep: 5 * time.Second, }) }, } @@ -71,7 +67,6 @@ type newLocalOpts struct { profileName string sandboxName string noTTY bool - runScript func(name string, args ...string) error retrySleep time.Duration } @@ -236,7 +231,7 @@ func newLocal(opts newLocalOpts) error { // 1. Ensure gateway if opts.ensureLocal { fmt.Println("=== Ensuring local gateway ===") - if err := opts.runScript("deploy.sh", "--local"); err != nil { + if err := deployLocal(gw); err != nil { return fmt.Errorf("deploy failed: %w", err) } } else { @@ -249,7 +244,7 @@ func newLocal(opts newLocalOpts) error { providers, _ := gw.ProviderList() if len(providers) == 0 { status.Section("Registering providers") - if err := opts.runScript("providers.sh"); err != nil { + if err := registerProviders(opts.harnessDir, gw, false); err != nil { return fmt.Errorf("provider registration failed: %w", err) } } diff --git a/cmd/new_test.go b/cmd/new_test.go index c94a00f..1c93172 100644 --- a/cmd/new_test.go +++ b/cmd/new_test.go @@ -86,8 +86,6 @@ FOO = "bar" return dir } -func noopScript(name string, args ...string) error { return nil } - func TestNewLocal_NoGateway(t *testing.T) { dir := setupTestProfile(t) gw := &mockGW{inferenceErr: fmt.Errorf("connection refused")} @@ -97,7 +95,6 @@ func TestNewLocal_NoGateway(t *testing.T) { gw: gw, profileName: "default", noTTY: true, - runScript: noopScript, }) if err == nil { t.Fatal("expected error") @@ -107,28 +104,22 @@ func TestNewLocal_NoGateway(t *testing.T) { } } -func TestNewLocal_NoProviders_CallsScript(t *testing.T) { +func TestNewLocal_NoProviders_RegistersProviders(t *testing.T) { dir := setupTestProfile(t) - scriptCalled := false + os.MkdirAll(filepath.Join(dir, "sandbox", "profiles"), 0o755) gw := &mockGW{ providerList: nil, providers: map[string]bool{}, } - newLocal(newLocalOpts{ + err := newLocal(newLocalOpts{ harnessDir: dir, gw: gw, profileName: "default", noTTY: true, - runScript: func(name string, args ...string) error { - if name == "providers.sh" { - scriptCalled = true - } - return nil - }, }) - if !scriptCalled { - t.Error("expected providers.sh to be called when no providers registered") + if err != nil { + t.Fatalf("newLocal: %v", err) } } @@ -144,7 +135,7 @@ func TestNewLocal_MissingProviders(t *testing.T) { gw: gw, profileName: "default", noTTY: true, - runScript: noopScript, + }) if err != nil { t.Fatalf("newLocal: %v", err) @@ -170,7 +161,7 @@ func TestNewLocal_AllProvidersMissing(t *testing.T) { gw: gw, profileName: "default", noTTY: true, - runScript: noopScript, + }) if err != nil { t.Fatalf("newLocal: %v", err) @@ -190,7 +181,7 @@ func TestNewLocal_ProfileNotFound(t *testing.T) { gw: gw, profileName: "nonexistent", noTTY: true, - runScript: noopScript, + }) if err == nil { t.Fatal("expected error for missing profile") @@ -210,7 +201,7 @@ func TestNewLocal_SandboxCreateRetry(t *testing.T) { gw: gw, profileName: "default", noTTY: true, - runScript: noopScript, + retrySleep: 0, }) if err != nil { @@ -237,7 +228,7 @@ func TestNewLocal_SandboxCreateOpts(t *testing.T) { profileName: "default", sandboxName: "custom-name", noTTY: true, - runScript: noopScript, + }) if err != nil { t.Fatalf("newLocal: %v", err) diff --git a/internal/runner/runner.go b/internal/runner/runner.go deleted file mode 100644 index 8c78654..0000000 --- a/internal/runner/runner.go +++ /dev/null @@ -1,18 +0,0 @@ -package runner - -import ( - "os" - "os/exec" - "path/filepath" -) - -// RunScript executes a bash script from bin/scripts/ with stdout/stderr passthrough. -func RunScript(harnessDir, scriptName string, args ...string) error { - path := filepath.Join(harnessDir, "bin", "scripts", scriptName) - cmd := exec.Command("bash", append([]string{path}, args...)...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Dir = harnessDir - return cmd.Run() -} diff --git a/sandbox/settings.json b/sandbox/settings.json index f4a9787..f4ae432 100644 --- a/sandbox/settings.json +++ b/sandbox/settings.json @@ -3,5 +3,6 @@ "defaultMode": "bypassPermissions", "allow": ["Bash(*)", "Read(*)", "Write(*)", "Edit(*)", "Glob(*)", "Grep(*)", "WebFetch(*)", "WebSearch(*)", "mcp__*"], "deny": [] - } + }, + "trustedMcpServers": ["atlassian"] } diff --git a/test/preflight.bats b/test/preflight.bats index 4568444..7c481f2 100644 --- a/test/preflight.bats +++ b/test/preflight.bats @@ -1,11 +1,10 @@ #!/usr/bin/env bats -# Tests for lib/providers.py preflight check. +# Tests for preflight check (Go implementation). # # Stubs external CLIs (openshell, kubectl, gcloud, podman, curl, gws) # and uses temp TOML files to test every configuration path. REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" -PROVIDERS_PY="$REPO_ROOT/bin/scripts/lib/providers.py" setup() { TEST_TMPDIR="$(mktemp -d)" @@ -18,7 +17,7 @@ setup() { unset OPENSHELL_GATEWAY OPENSHELL_NAMESPACE OPENSHELL_CLI unset GOOGLE_APPLICATION_CREDENTIALS - # Point providers.py at temp TOML files + # Point preflight at temp TOML files export PROVIDERS_TOML="$TEST_TMPDIR/providers.toml" export CONFIG_TOML="$TEST_TMPDIR/openshell.toml" @@ -51,44 +50,19 @@ STUB } run_preflight() { - if [[ "${USE_GO:-}" == "true" ]]; then - # Run via the Go harness binary - local harness="$REPO_ROOT/harness" - local cmd="${1:-check}" - shift 2>/dev/null || true - case "$cmd" in - check) - PROVIDERS_TOML="$PROVIDERS_TOML" CONFIG_TOML="$CONFIG_TOML" \ - "$harness" preflight "$@" - ;; - available|names) - PROVIDERS_TOML="$PROVIDERS_TOML" CONFIG_TOML="$CONFIG_TOML" \ - "$harness" preflight "$cmd" - ;; - esac - else - # Run via Python (original) - python3 -c " -import sys, os -sys.path.insert(0, '$REPO_ROOT/bin/scripts/lib') -os.chdir('$TEST_TMPDIR') - -from pathlib import Path -import providers -providers.PROVIDERS_TOML = Path('$PROVIDERS_TOML') -providers.CONFIG_TOML = Path('$CONFIG_TOML') -providers.CLI = os.environ.get('OPENSHELL_CLI', 'openshell') - -cmd = sys.argv[1] if len(sys.argv) > 1 else 'check' -strict = '--strict' in sys.argv -if cmd == 'check': - providers.cmd_check(strict=strict) -elif cmd == 'available': - providers.cmd_available() -elif cmd == 'names': - providers.cmd_names() -" "$@" - fi + local harness="$REPO_ROOT/harness" + local cmd="${1:-check}" + shift 2>/dev/null || true + case "$cmd" in + check) + PROVIDERS_TOML="$PROVIDERS_TOML" CONFIG_TOML="$CONFIG_TOML" \ + "$harness" preflight "$@" + ;; + available|names) + PROVIDERS_TOML="$PROVIDERS_TOML" CONFIG_TOML="$CONFIG_TOML" \ + "$harness" preflight "$cmd" + ;; + esac } # ── ENV input tests ────────────────────────────────────────────────── diff --git a/test/test-flow.sh b/test/test-flow.sh index 0d32a14..e3d5948 100755 --- a/test/test-flow.sh +++ b/test/test-flow.sh @@ -1,33 +1,32 @@ #!/usr/bin/env bash # End-to-end validation for podman and OCP flows. # -# Supports both the bash (bin/harness) and Go (./harness) entry points. -# Use --go to test the Go binary. Use --full for sandbox lifecycle tests. -# # Usage: -# ./test-flow.sh podman # quick bash: deploy + providers + teardown -# ./test-flow.sh podman --full # full bash: + sandbox + verify integrations -# ./test-flow.sh podman --go # quick Go: same flow via Go binary -# ./test-flow.sh podman --full --go # full Go -# ./test-flow.sh ocp [--full] [--go] # OCP variants +# ./test-flow.sh podman # quick: deploy + providers + teardown +# ./test-flow.sh podman --full # full: + sandbox + verify integrations +# ./test-flow.sh ocp [--full] # OCP variants # ./test-flow.sh ocp --full --reuse-gateway # skip deploy/teardown-k8s (~50s vs ~130s) -# ./test-flow.sh all [--full] [--go] # both platforms +# ./test-flow.sh all [--full] # both platforms set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -source "$SCRIPT_DIR/bin/scripts/lib/profile.sh" +HARNESS="$SCRIPT_DIR/harness" CLI="${OPENSHELL_CLI:-openshell}" +if [[ ! -x "$HARNESS" ]]; then + echo "ERROR: Go binary not found at $HARNESS" + echo " Build it first: make cli" + exit 1 +fi + # ── Parse args ────────────────────────────────────────────────────── TARGET="" FULL=false -USE_GO=false REUSE_GATEWAY=false for arg in "$@"; do case "$arg" in --full) FULL=true ;; - --go) USE_GO=true ;; --reuse-gateway) REUSE_GATEWAY=true ;; -*) ;; *) [[ -z "$TARGET" ]] && TARGET="$arg" ;; @@ -35,26 +34,16 @@ for arg in "$@"; do done if [[ -z "$TARGET" ]]; then - echo "Usage: $0 [--full] [--go] [--reuse-gateway]" + echo "Usage: $0 [--full] [--reuse-gateway]" exit 1 fi -# ── Entry point: bash or Go ───────────────────────────────────────── -if $USE_GO; then - HARNESS="$SCRIPT_DIR/harness" - if [[ ! -x "$HARNESS" ]]; then - echo "ERROR: Go binary not found at $HARNESS" - echo " Build it first: make cli" - exit 1 - fi - IMPL="go" -else - HARNESS="$SCRIPT_DIR/bin/harness" - IMPL="bash" -fi - # ── Helpers ────────────────────────────────────────────────────────── +strip_ansi() { + sed 's/\x1b\[[0-9;]*m//g' +} + PASS=0 FAIL=0 TOTAL_START=$(date +%s) @@ -139,16 +128,16 @@ summary() { local elapsed=$(( $(date +%s) - TOTAL_START )) echo "" if [[ $FAIL -eq 0 ]]; then - echo "${PASS}/${total} passed (${elapsed}s) [${IMPL}]" + echo "${PASS}/${total} passed (${elapsed}s)" else - echo "${PASS}/${total} passed, ${FAIL} failed (${elapsed}s) [${IMPL}]" + echo "${PASS}/${total} passed, ${FAIL} failed (${elapsed}s)" fi } -# ── Error scenarios (run on both paths) ────────────────────────────── +# ── Error scenarios ───────────────────────────────────────────────── test_errors() { - echo "=== test: error scenarios ($IMPL) ===" + echo "=== test: error scenarios ===" # Bad profile step_fail "nonexistent profile" "$HARNESS" new --local --profile nonexistent --no-tty @@ -170,7 +159,7 @@ test_errors() { test_podman() { local mode="quick" $FULL && mode="full" - echo "=== test-flow: podman ($mode) [$IMPL] ===" + echo "=== test-flow: podman ($mode) ===" step "teardown" "$HARNESS" teardown --sandboxes --providers step "deploy" "$HARNESS" deploy --local @@ -186,7 +175,7 @@ test_podman() { # Missing providers scenario echo "" - echo "=== test: missing providers ($IMPL) ===" + echo "=== test: missing providers ===" step "teardown providers" "$HARNESS" teardown --providers step_output "new with no providers" "$HARNESS" new --local --name test-noprov --no-tty step "cleanup" "$HARNESS" teardown --sandboxes @@ -201,7 +190,7 @@ test_ocp() { local mode="quick" $FULL && mode="full" $REUSE_GATEWAY && mode="$mode, reuse-gateway" - echo "=== test-flow: ocp ($mode) [$IMPL] ===" + echo "=== test-flow: ocp ($mode) ===" if $REUSE_GATEWAY; then # Ensure OCP gateway is selected (error scenarios may have switched to local) @@ -220,7 +209,6 @@ test_ocp() { step "deploy" "$HARNESS" deploy --remote fi - step "setup creds" "$SCRIPT_DIR/bin/scripts/creds.sh" step "setup providers" "$HARNESS" providers step "gateway reachable" "$CLI" inference get check_providers @@ -255,7 +243,7 @@ case "$TARGET" in all) test_podman; echo ""; test_ocp ;; *) echo "Unknown target: $TARGET" - echo "Usage: $0 [--full] [--go]" + echo "Usage: $0 [--full]" exit 1 ;; esac