diff --git a/README.md b/README.md index 5b38788..52db027 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ make push-sandbox push-launcher push-gateway push-supervisor ./setup-providers.sh # 4. Launch a sandbox -kubectl apply -f sandbox.yaml # or: ./sandbox-ocp.sh ``` @@ -69,7 +68,6 @@ kubectl apply -f sandbox.yaml | `setup-providers.sh` | Register credential providers — works on any gateway | | `sandbox-podman.sh` | Launch sandbox on local gateway (direct CLI) | | `sandbox-ocp.sh` | Launch sandbox on OpenShift (kubectl apply) | -| `teardown-ocp.sh` | Remove all OpenShift resources | | `sandbox/Dockerfile` | Custom sandbox image (extends community base) | | `sandbox/policy.yaml` | Network policy (endpoints not covered by provider profiles) | | `sandbox/startup.sh` | Runtime env, GWS, MCP config | @@ -98,7 +96,6 @@ See [credentials.md](credentials.md) for the full reference. ./sandbox-podman.sh --name dev # named sandbox # OpenShift -./sandbox-ocp.sh # apply sandbox.yaml + show logs ./sandbox-ocp.sh my-config.yaml # use a custom config # Either platform diff --git a/credentials.md b/credentials.md index 314cc92..8356606 100644 --- a/credentials.md +++ b/credentials.md @@ -9,7 +9,7 @@ How credentials flow from your machine into sandboxes. | GitHub token | `github` (built-in) | Bearer auth, proxy-resolved | `setup-providers.sh` | | Vertex AI | `google-vertex-ai` (built-in) | `inference.local` gateway routing, OAuth refresh | `setup-providers.sh` | | Atlassian API token | `atlassian` (custom profile) | Basic auth, proxy decodes base64 + resolves | `setup-providers.sh` | -| Atlassian URL/username | — | Uploaded as `atlassian.json` (not secrets) | `sandbox-podman.sh` / `sandbox.yaml` | +| Atlassian URL/username | — | Uploaded as `atlassian.json` (not secrets) | `sandbox-podman.sh` / `sandbox-ocp.sh` | | GWS OAuth | — | Decrypted export uploaded as files | `sandbox-podman.sh` / `setup-creds.sh` | ## How it works diff --git a/delete-sandboxes.sh b/delete-sandboxes.sh deleted file mode 100755 index c7bc425..0000000 --- a/delete-sandboxes.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -# Delete all running sandboxes. -set -euo pipefail - -export OPENSHELL_GATEWAY="${GATEWAY_NAME:-ocp}" -CLI="${OPENSHELL_CLI:-openshell}" -command -v "$CLI" &>/dev/null || { echo "ERROR: openshell CLI not found."; exit 1; } - -mapfile -t names < <("$CLI" sandbox list 2>/dev/null | awk 'NR>1 {print $1}') -if [[ ${#names[@]} -eq 0 ]]; then - echo "No sandboxes running." - exit 0 -fi - -for name in "${names[@]}"; do - echo "Deleting $name..." - "$CLI" sandbox delete "$name" || echo " WARNING: failed to delete $name" -done - -echo "Done." diff --git a/lib/agent.sh b/lib/agent.sh new file mode 100644 index 0000000..49b03e8 --- /dev/null +++ b/lib/agent.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Agent config parsing helpers. +# +# Source from any script: +# source "$(dirname "$0")/lib/agent.sh" +# +# Usage: +# parse_agent agents/default.toml +# # Sets: SANDBOX_IMAGE, SANDBOX_COMMAND, SANDBOX_NAME, +# # SANDBOX_PROVIDERS, SANDBOX_ENV, SANDBOX_KEEP + +parse_agent() { + local agent_file="$1" + [[ -f "$agent_file" ]] || { echo "ERROR: $agent_file not found."; exit 1; } + + eval "$(python3 -c " +import tomllib, sys, shlex +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))}') +" "$agent_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/. +# +# Usage: +# stage_harness_dir /tmp/openshell +# # Creates: $dir/sandbox.env, $dir/credentials.json, $dir/client_secret.json +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/lib/common.sh b/lib/common.sh index be722c4..7e41a7c 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -6,102 +6,39 @@ # ── CLI detection ────────────────────────────────────────────────────── -# The openshell binary. Override with OPENSHELL_CLI for dev builds. CLI="${OPENSHELL_CLI:-openshell}" -# Guard: exit if openshell CLI is not on PATH. -# Used by: sandbox-podman.sh, sandbox-ocp.sh, openshell-harness-preflight.sh, deploy-podman.sh require_cli() { command -v "$CLI" &>/dev/null || { echo "ERROR: openshell CLI not found."; exit 1; } } -# Guard: exit if kubectl is not on PATH. -# Used by: sandbox-ocp.sh, deploy-ocp.sh, setup-creds.sh, teardown-ocp.sh require_kubectl() { command -v kubectl &>/dev/null || { echo "ERROR: kubectl is required."; exit 1; } } # ── Gateway validation ──────────────────────────────────────────────── -# Get the active gateway name from the CLI. -active_gateway() { - "$CLI" gateway list 2>/dev/null | awk '/^\*/ {print $2}' -} - -# Guard: exit if active gateway is remote (wrong platform). -# Used by: sandbox-podman.sh, deploy-podman.sh require_local_gateway() { local gw - gw=$(active_gateway) - if [[ "$gw" == *"-remote-"* ]]; then - echo "ERROR: Active gateway '$gw' is remote." - echo " Switch to local: openshell gateway select openshell-local-podman" - exit 1 - fi -} - -# Guard: exit if active gateway is not remote. -# Used by: sandbox-ocp.sh -require_ocp_gateway() { - local gw - gw=$(active_gateway) - if [[ "$gw" != *"-remote-"* ]]; then - echo "ERROR: Active gateway '$gw' is not remote." - echo " Switch to remote: openshell gateway select openshell-remote-ocp" + 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 } -# ── Provider detection ───────────────────────────────────────────────── - -# Space-separated provider names to attach. Override with OPENSHELL_PROVIDERS. -PROVIDER_NAMES="${OPENSHELL_PROVIDERS:-github vertex-local atlassian}" - -# Query the gateway for registered providers, build --provider flags. -# Sets PROVIDER_FLAGS array. Skips unregistered providers with a warning. -# Used by: sandbox-podman.sh, openshell-harness-preflight.sh -detect_providers() { - PROVIDER_FLAGS=() - echo "=== Providers ===" - for name in $PROVIDER_NAMES; do - if "$CLI" provider get "$name" &>/dev/null; then - PROVIDER_FLAGS+=(--provider "$name") - echo " $name: attached" - else - echo " $name: not registered (skipping)" - fi - done -} - # ── Environment pre-flight ───────────────────────────────────────────── -# Check which features are enabled based on harness.toml. -# Reads provider definitions and validates env vars, paths, and commands. -# Does not exit — informational only. Use --strict to fail on missing required. -# Used by: openshell-harness-preflight.sh, or standalone: source lib/common.sh && check_env check_env() { local script_dir script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" python3 "$script_dir/lib/providers.py" check "$@" } -# Print space-separated names of providers whose inputs are all available. -# Used by: detect_providers (replaces hardcoded PROVIDER_NAMES when TOML exists) -available_providers() { - local script_dir - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - python3 "$script_dir/lib/providers.py" available -} - -# ── Credential staging ───────────────────────────────────────────────── -# These functions stage credentials into a temp dir for --upload into -# sandboxes. Needed because OpenShell doesn't support file-based -# credential projection yet (#1268, #1423). +# ── GWS credential export ───────────────────────────────────────────── +# Used by: setup-creds.sh (K8s secret creation) -# Export decrypted GWS OAuth credentials into $outdir/gws-config/. -# The gws CLI encrypts credentials with a machine-specific key, so we -# must decrypt and re-export for use inside the sandbox container. -# Used by: sandbox-podman.sh (via stage_sandbox_creds), setup-creds.sh export_gws_creds() { local outdir="$1" if ! command -v gws &>/dev/null; then @@ -125,54 +62,3 @@ export_gws_creds() { return 1 fi } - -# Write a sourceable sandbox.env with non-secret env vars to inject into -# the sandbox. These are values that aren't provider-managed (not secrets) -# but need to be available inside the sandbox. The file is sourced by -# startup.sh. Add new env vars here as integrations grow. -# Used by: sandbox-podman.sh (via stage_sandbox_creds), setup-creds.sh -write_sandbox_env() { - local outfile="$1" - mkdir -p "$(dirname "$outfile")" - local has_vars=false - - { - # Atlassian non-secret config - if [[ -n "${JIRA_URL:-}" ]]; then - echo "export JIRA_URL=\"$JIRA_URL\"" - echo "export JIRA_USERNAME=\"${JIRA_USERNAME:-}\"" - echo "export CONFLUENCE_URL=\"${JIRA_URL%/}/wiki\"" - echo "export CONFLUENCE_USERNAME=\"${JIRA_USERNAME:-}\"" - echo "export CONFLUENCE_API_TOKEN=\"\${JIRA_API_TOKEN:-}\"" - has_vars=true - echo " Atlassian: $JIRA_URL" >&2 - fi - - # Add more env var blocks here as needed: - # if [[ -n "${SOME_VAR:-}" ]]; then - # echo "export SOME_VAR=\"$SOME_VAR\"" - # has_vars=true - # fi - } > "$outfile" - - $has_vars || { rm -f "$outfile"; echo " sandbox.env: no vars to inject (skipping)" >&2; return 1; } - return 0 -} - -# Stage all sandbox credentials into a temp dir for --upload. -# Sets UPLOAD_ARGS (array) and STAGE_DIR (path). The caller should not -# clean up STAGE_DIR if using exec (the upload happens during exec). -# Used by: sandbox-podman.sh -stage_sandbox_creds() { - STAGE_DIR=$(mktemp -d) - local creds="$STAGE_DIR/creds" - mkdir -p "$creds" - local has_uploads=false - - echo "=== Credentials ===" - export_gws_creds "$creds" && has_uploads=true - write_sandbox_env "$creds/sandbox.env" && has_uploads=true - - UPLOAD_ARGS=() - $has_uploads && UPLOAD_ARGS=(--upload "$creds:/sandbox/.harness") -} diff --git a/openshell.toml b/openshell.toml index 3b788f8..728e5ba 100644 --- a/openshell.toml +++ b/openshell.toml @@ -5,7 +5,7 @@ # # Copy this file and customize per deployment: # cp openshell.toml my-team.toml -# ./sandbox-check.sh --config my-team.toml +# ./openshell-harness-preflight.sh # Which providers to register and attach to sandboxes. # Must match names defined in providers.toml. diff --git a/sandbox-check.sh b/sandbox-check.sh deleted file mode 100755 index 7907abc..0000000 --- a/sandbox-check.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash -# Pre-flight check for the OpenShell sandbox environment. -# Read-only — prints the status of all prerequisites, no mutations. -# -# Usage: -# ./sandbox-check.sh -set -euo pipefail - -NAMESPACE="${OPENSHELL_NAMESPACE:-openshell}" -export OPENSHELL_GATEWAY="${GATEWAY_NAME:-ocp}" -CLI="${OPENSHELL_CLI:-openshell}" - -READY=true -fail() { echo " ✗ $*"; READY=false; } -ok() { echo " ✓ $*"; } -skip() { echo " - $*"; } - -echo "" -echo "╔══════════════════════════════════════════╗" -echo "║ OpenShell Sandbox Pre-flight ║" -echo "╚══════════════════════════════════════════╝" - -# ── Gateway ──────────────────────────────────────────────────────────── -echo "" -echo "=== Gateway ===" -if ! command -v "$CLI" &>/dev/null; then - fail "openshell CLI not found" -else - if "$CLI" inference get &>/dev/null 2>&1; then - model=$("$CLI" inference get 2>/dev/null | grep Model: | awk '{print $2}') - provider=$("$CLI" inference get 2>/dev/null | grep Provider: | awk '{print $2}') - ok "Reachable (inference: $model via $provider)" - else - fail "Gateway unreachable — run ./deploy-ocp.sh" - fi -fi - -# ── Providers ────────────────────────────────────────────────────────── -echo "" -echo "=== Providers ===" -if command -v "$CLI" &>/dev/null; then - for name in github vertex-local atlassian; do - if "$CLI" provider get "$name" &>/dev/null 2>&1; then - creds=$("$CLI" provider get "$name" 2>/dev/null | grep "Credential keys:" | sed 's/.*: //') - config=$("$CLI" provider get "$name" 2>/dev/null | grep "Config keys:" | sed 's/.*: //') - ok "$name: registered (credentials: $creds | config: $config)" - else - fail "$name: not registered — run ./setup-providers.sh" - fi - done -fi - -# ── K8s / OCP (only if cluster is available) ────────────────────────── -K8S_AVAILABLE=false -if command -v kubectl &>/dev/null && kubectl get ns "$NAMESPACE" &>/dev/null 2>&1; then - K8S_AVAILABLE=true -fi - -if $K8S_AVAILABLE; then - echo "" - echo "=== K8s Cluster ===" - pods=$(kubectl get pods -n "$NAMESPACE" --no-headers 2>/dev/null | wc -l | tr -d ' ') - ok "Namespace '$NAMESPACE' ($pods pods)" - - echo "" - echo "=== K8s Secrets ===" - for secret in openshell-gws openshell-atlassian; do - if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null 2>&1; then - keys=$(kubectl get secret "$secret" -n "$NAMESPACE" \ - -o jsonpath='{.data}' 2>/dev/null | python3 -c \ - "import json,sys; print(', '.join(json.load(sys.stdin).keys()))" 2>/dev/null || echo "?") - ok "$secret: present ($keys)" - else - skip "$secret: not found (run ./setup-creds.sh to create)" - fi - done - - echo "" - echo "=== Launcher RBAC ===" - if kubectl get serviceaccount openshell-launcher -n "$NAMESPACE" &>/dev/null 2>&1; then - ok "ServiceAccount openshell-launcher: present" - else - fail "ServiceAccount openshell-launcher: missing — run ./deploy-ocp.sh" - fi -else - echo "" - echo "=== Platform ===" - ok "Local mode (no K8s cluster detected)" - if command -v podman &>/dev/null; then - ok "Podman: $(podman --version 2>/dev/null | head -1)" - elif command -v docker &>/dev/null; then - ok "Docker: $(docker --version 2>/dev/null | head -1)" - else - fail "No container runtime (need podman or docker)" - fi -fi - -# ── Summary ──────────────────────────────────────────────────────────── -echo "" -echo "══════════════════════════════════════════════" -if $READY; then - echo " ✓ Ready to launch: kubectl apply -f sandbox.yaml" -else - echo " ✗ Not ready — fix issues above before launching" -fi -echo "══════════════════════════════════════════════" -echo "" diff --git a/sandbox-ocp.sh b/sandbox-ocp.sh index 3fc622b..fda787b 100755 --- a/sandbox-ocp.sh +++ b/sandbox-ocp.sh @@ -14,30 +14,14 @@ 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" -[[ -f "$AGENT_FILE" ]] || { echo "ERROR: $AGENT_FILE not found."; exit 1; } - -# Parse agent config -eval "$(python3 -c " -import tomllib, sys, shlex -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 section as export statements -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))}') -" "$AGENT_FILE")" +parse_agent "$AGENT_FILE" echo "=== Agent: $AGENT_NAME ===" echo " Name: $SANDBOX_NAME" diff --git a/sandbox-podman.sh b/sandbox-podman.sh index 8a2f4f3..be67060 100755 --- a/sandbox-podman.sh +++ b/sandbox-podman.sh @@ -17,76 +17,40 @@ 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" -SANDBOX_NAME="" +NAME_OVERRIDE="" TTY_FLAG="--tty" while [[ $# -gt 0 ]]; do case "$1" in - --name) SANDBOX_NAME="$2"; shift 2 ;; + --name) NAME_OVERRIDE="$2"; shift 2 ;; --no-tty) TTY_FLAG="--no-tty"; shift ;; *) AGENT_NAME="$1"; shift ;; esac done -AGENT_FILE="$SCRIPT_DIR/agents/${AGENT_NAME}.toml" -[[ -f "$AGENT_FILE" ]] || { echo "ERROR: $AGENT_FILE not found."; exit 1; } +parse_agent "$SCRIPT_DIR/agents/${AGENT_NAME}.toml" -# Parse agent config -eval "$(python3 -c " -import tomllib, sys, shlex -with open(sys.argv[1], 'rb') as f: - c = tomllib.load(f) -print(f'SANDBOX_IMAGE={shlex.quote(c.get(\"image\", \"\"))}') -print(f'SANDBOX_COMMAND={shlex.quote(c.get(\"command\", \"claude --bare\"))}') -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))}') -" "$AGENT_FILE")" - -# Build provider flags -PROVIDER_FLAGS=() echo "=== Providers ===" -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 +build_provider_flags # Stage files for upload to /sandbox/.config/openshell/ HARNESS_DIR="/tmp/openshell" -mkdir -p "$HARNESS_DIR" - -if [[ -n "$SANDBOX_ENV" ]]; then - echo "$SANDBOX_ENV" > "$HARNESS_DIR/sandbox.env" - echo " Env: $(wc -l < "$HARNESS_DIR/sandbox.env") vars" -fi - -if command -v gws &>/dev/null && gws auth status &>/dev/null; then - gws auth export --unmasked > "$HARNESS_DIR/credentials.json" 2>/dev/null - cp ~/.config/gws/client_secret.json "$HARNESS_DIR/client_secret.json" 2>/dev/null || true - echo " GWS: exported" -else - echo " GWS: not configured (skipping)" -fi +rm -rf "$HARNESS_DIR" +stage_harness_dir "$HARNESS_DIR" # Build flags FROM_FLAGS=() [[ -n "$SANDBOX_IMAGE" ]] && FROM_FLAGS=(--from "$SANDBOX_IMAGE") NAME_FLAGS=() -[[ -n "$SANDBOX_NAME" ]] && NAME_FLAGS=(--name "$SANDBOX_NAME") +[[ -n "$NAME_OVERRIDE" ]] && NAME_FLAGS=(--name "$NAME_OVERRIDE") -# Command depends on tty mode if [[ "$TTY_FLAG" == "--tty" ]]; then CMD=(-- bash -c ". /sandbox/startup.sh && exec $SANDBOX_COMMAND") else @@ -105,10 +69,10 @@ for attempt in 1 2 3 4 5; do "${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 + if [[ -n "$NAME_OVERRIDE" ]]; then + "$CLI" sandbox delete "$NAME_OVERRIDE" 2>/dev/null || true else - "$CLI" sandbox delete "$("$CLI" sandbox list 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' | awk 'NR==2{print $1}')" 2>/dev/null || true + "$CLI" sandbox delete "$("$CLI" sandbox list 2>/dev/null | strip_ansi | awk 'NR==2{print $1}')" 2>/dev/null || true fi sleep 5 done diff --git a/sandbox.sh b/sandbox.sh deleted file mode 100755 index be58917..0000000 --- a/sandbox.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# Launch an OpenShell sandbox by applying a sandbox YAML file. -# -# Prerequisites (one-time): -# ./deploy-ocp.sh -# ./setup-creds.sh -# ./setup-providers.sh -# -# Usage: -# ./sandbox.sh # apply sandbox.yaml -# ./sandbox.sh my-sandbox.yaml # apply a custom sandbox file -# ./sandbox.sh --rejoin agent # reconnect to existing sandbox -# -# Edit sandbox.yaml to configure name, providers, skills, etc. -set -euo pipefail - -export OPENSHELL_GATEWAY="${GATEWAY_NAME:-${OPENSHELL_GATEWAY:-}}" -NAMESPACE="${OPENSHELL_NAMESPACE:-openshell}" - -# This script is for OpenShift — require explicit OPENSHELL_GATEWAY=ocp -if [[ "$OPENSHELL_GATEWAY" != "ocp" ]]; then - echo "ERROR: This script is for OpenShift (OPENSHELL_GATEWAY=ocp)." - echo " For local sandboxes, use: ./sandbox-local.sh" - echo " For OpenShift: export OPENSHELL_GATEWAY=ocp" - exit 1 -fi - -CLI="${OPENSHELL_CLI:-openshell}" -command -v "$CLI" &>/dev/null || { echo "ERROR: openshell CLI not found."; exit 1; } -command -v kubectl &>/dev/null || { echo "ERROR: kubectl is required."; exit 1; } - -case "${1:-}" in - --rejoin) - echo "Reconnecting to sandbox: $2" - exec "$CLI" sandbox connect "$2" - ;; - *.yaml|*.yml) - SANDBOX_FILE="$1" - ;; - "") - SANDBOX_FILE="sandbox.yaml" - ;; - *) - echo "Usage: $0 [sandbox.yaml | --rejoin ]" - exit 1 - ;; -esac - -[[ -f "$SANDBOX_FILE" ]] || { echo "ERROR: $SANDBOX_FILE not found."; exit 1; } - -echo "=== Applying $SANDBOX_FILE ===" -kubectl apply -n "$NAMESPACE" -f "$SANDBOX_FILE" - -JOB_NAME=$(grep -A5 'kind: Job' "$SANDBOX_FILE" | grep 'name:' | head -1 | awk '{print $2}' | tr -d '"') -echo "" -echo "Waiting for launcher..." -kubectl wait --for=condition=ready pod -n "$NAMESPACE" \ - -l "job-name=${JOB_NAME}" --timeout=120s 2>/dev/null || true - -echo "" -kubectl logs -n "$NAMESPACE" -f -l "job-name=${JOB_NAME}" 2>/dev/null || true diff --git a/sandbox.yaml b/sandbox.yaml deleted file mode 100644 index f97efdd..0000000 --- a/sandbox.yaml +++ /dev/null @@ -1,105 +0,0 @@ -# OpenShell Sandbox — in-cluster launcher -# -# Apply this file to launch a sandbox without any local env vars or files. -# Credentials are read from K8s Secrets mounted into the launcher pod. -# -# Prerequisites: -# ./deploy-ocp.sh # deploys gateway and creates RBAC -# ./setup-creds.sh # stores GWS + Atlassian config in cluster -# ./setup-providers.sh # registers API tokens with gateway -# -# Usage: -# kubectl apply -f sandbox.yaml # launch sandbox -# kubectl delete -f sandbox.yaml # stop sandbox -# -# Configuration: edit the config.yaml section below. ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: sandbox-agent - namespace: openshell -data: - config.yaml: | - name: agent - command: claude --bare - keep: true - - providers: - - github - - vertex-local - - atlassian - - # Skills/plugin marketplaces (future — see sandbox/launcher/SPEC.md) - # skills: - # - repo: https://github.com/stackrox/skills ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: sandbox-agent - namespace: openshell - labels: - app: openshell-sandbox -spec: - backoffLimit: 0 - template: - metadata: - labels: - app: openshell-sandbox - spec: - serviceAccountName: openshell-launcher - restartPolicy: Never - - containers: - - name: launcher - image: quay.io/rcochran/openshell:launcher - imagePullPolicy: Always - - env: - - name: GATEWAY_ENDPOINT - value: "https://openshell.openshell.svc.cluster.local:8080" - - name: HOME - value: "/tmp" - - name: JIRA_URL - valueFrom: - secretKeyRef: - name: openshell-atlassian - key: JIRA_URL - optional: true - - name: JIRA_USERNAME - valueFrom: - secretKeyRef: - name: openshell-atlassian - key: JIRA_USERNAME - optional: true - - volumeMounts: - - name: config - mountPath: /etc/openshell/sandbox - readOnly: true - - name: gws - mountPath: /secrets/gws - readOnly: true - - name: gateway-mtls - mountPath: /secrets/mtls - readOnly: true - - name: sandbox-env - mountPath: /etc/openshell/env - readOnly: true - - volumes: - - name: config - configMap: - name: sandbox-agent - - name: gws - secret: - secretName: openshell-gws - optional: true - - name: gateway-mtls - secret: - secretName: openshell-client-tls - - name: sandbox-env - configMap: - name: sandbox-env - optional: true diff --git a/sandbox/launcher/SPEC.md b/sandbox/launcher/SPEC.md index 9eefd76..310fddb 100644 --- a/sandbox/launcher/SPEC.md +++ b/sandbox/launcher/SPEC.md @@ -5,11 +5,11 @@ > and its PyYAML dependency. In-cluster binary that creates OpenShell sandboxes from a YAML config. -Runs as a Kubernetes Job — `kubectl apply -f sandbox.yaml` triggers it. +Runs as a Kubernetes Job — `./sandbox-ocp.sh` generates and applies it. ## What it does -1. Read `config.yaml` from a mounted ConfigMap +1. Read `config.toml` from a mounted ConfigMap 2. Register the in-cluster gateway using mounted mTLS certs 3. Verify providers exist, skip any that aren't registered 4. Call `openshell sandbox create` with providers and skills config diff --git a/sandbox/startup.sh b/sandbox/startup.sh index 3b49081..79444c6 100644 --- a/sandbox/startup.sh +++ b/sandbox/startup.sh @@ -2,37 +2,13 @@ # Runtime setup for the sandbox. Runs once via launcher's sandbox exec. set -euo pipefail -OPENSHELL_DIR="/sandbox/.config/openshell" - # ── Source env vars from agent config ───────────────────────────────── +OPENSHELL_DIR="/sandbox/.config/openshell" if [[ -f "$OPENSHELL_DIR/sandbox.env" ]]; then . "$OPENSHELL_DIR/sandbox.env" cat "$OPENSHELL_DIR/sandbox.env" >> /sandbox/.bashrc fi -# ── Claude Code config (MCP servers, onboarding) ───────────────────── -cat > /sandbox/.claude.json <<'CLAUDEJSON' -{ - "autoUpdates": false, - "hasCompletedOnboarding": true, - "projects": { - "/sandbox": { - "hasTrustDialogAccepted": true - } - }, - "mcpServers": { - "atlassian": { - "type": "stdio", - "command": "/sandbox/.venv/bin/mcp-atlassian", - "args": [], - "env": { - "READ_ONLY_MODE": "true" - } - } - } -} -CLAUDEJSON - # ── Git auth ────────────────────────────────────────────────────────── gh auth setup-git 2>/dev/null || true diff --git a/setup-providers.sh b/setup-providers.sh index 8c12d8f..ab0bbef 100755 --- a/setup-providers.sh +++ b/setup-providers.sh @@ -2,7 +2,7 @@ # 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 ./delete-sandboxes.sh first). +# 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). @@ -39,7 +39,7 @@ provider_exists() { "$CLI" provider get "$1" &>/dev/null; } 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 ./delete-sandboxes.sh first." + 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 diff --git a/teardown-ocp.sh b/teardown-ocp.sh deleted file mode 100755 index 7ef297a..0000000 --- a/teardown-ocp.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# Tear down OpenShell from the OpenShift cluster. -# Safe to run multiple times — each step tolerates missing resources. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -OPENSHELL_REPO="${OPENSHELL_REPO:-$(cd "$SCRIPT_DIR/../../nvidia/OpenShell" 2>/dev/null && pwd || echo "$SCRIPT_DIR/../OpenShell")}" -GATEWAY_NAME="${GATEWAY_NAME:-ocp}" -export OPENSHELL_GATEWAY="$GATEWAY_NAME" -CLI="${OPENSHELL_CLI:-openshell}" - -for cmd in kubectl helm; do - command -v "$cmd" &>/dev/null || { echo "ERROR: $cmd is required but not found."; exit 1; } -done - -echo "WARNING: This will delete all sandboxes, providers, and cluster resources." -read -r -p "Continue? [y/N] " confirm -[[ "$confirm" =~ ^[yY]$ ]] || { echo "Aborted."; exit 0; } - -echo "=== Deleting sandboxes ===" -if command -v "$CLI" &>/dev/null && "$CLI" gateway info "$GATEWAY_NAME" &>/dev/null; then - "$CLI" sandbox list 2>/dev/null | awk 'NR>1 {print $1}' | while read -r name; do - echo " Deleting sandbox: $name" - "$CLI" sandbox delete "$name" 2>/dev/null || true - done -fi - -echo "=== Uninstalling Helm release ===" -helm uninstall openshell -n openshell 2>/dev/null || echo " (not installed)" - -echo "=== Removing Sandbox CRD ===" -if [[ -f "$OPENSHELL_REPO/deploy/kube/manifests/agent-sandbox.yaml" ]]; then - kubectl delete -f "$OPENSHELL_REPO/deploy/kube/manifests/agent-sandbox.yaml" 2>/dev/null || true -else - kubectl delete ns agent-sandbox-system 2>/dev/null || true -fi - -echo "=== Removing SCCs and cluster role bindings ===" -for sa in openshell openshell-sandbox default; do - oc adm policy remove-scc-from-user privileged -z "$sa" -n openshell 2>/dev/null || true -done -oc adm policy remove-scc-from-user anyuid -z openshell -n openshell 2>/dev/null || true -kubectl delete clusterrolebinding agent-sandbox-admin 2>/dev/null || true - -# Launcher RBAC lives in the namespace so it's deleted with it, -# but remove explicitly for robustness. -kubectl delete rolebinding,role openshell-launcher -n openshell 2>/dev/null || true -kubectl delete serviceaccount openshell-launcher -n openshell 2>/dev/null || true - -echo "=== Deleting namespace ===" -kubectl delete ns openshell 2>/dev/null || echo " (not found)" - -echo "=== Removing local gateway config ===" -if command -v "$CLI" &>/dev/null; then - "$CLI" gateway remove "$GATEWAY_NAME" 2>/dev/null || true -fi - -echo "" -echo "Teardown complete. Run ./deploy-ocp.sh to redeploy." diff --git a/test-flow.sh b/test-flow.sh index 1e0e2ee..8172199 100755 --- a/test-flow.sh +++ b/test-flow.sh @@ -11,6 +11,7 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/lib/agent.sh" CLI="${OPENSHELL_CLI:-openshell}" TARGET="${1:-}" @@ -75,7 +76,7 @@ sandbox_verify() { local name="$1" # Check sandbox is ready local phase - phase=$("$CLI" sandbox list 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' | awk -v n="$name" '$1==n {print $NF}') + phase=$("$CLI" sandbox list 2>/dev/null | strip_ansi | awk -v n="$name" '$1==n {print $NF}') if [[ "$phase" != "Ready" ]]; then printf " ✗ %-35s\n" "sandbox ready" ((FAIL++)) @@ -151,7 +152,7 @@ test_ocp() { # Wait for ready for i in $(seq 1 30); do - local phase=$("$CLI" sandbox list 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' | awk -v n="$sandbox_name" '$1==n {print $NF}') + local phase=$("$CLI" sandbox list 2>/dev/null | strip_ansi | awk -v n="$sandbox_name" '$1==n {print $NF}') [[ "$phase" == "Ready" ]] && break sleep 2 done