diff --git a/PROVIDERS-SPEC.md b/PROVIDERS-SPEC.md index 368151d..40423ab 100644 --- a/PROVIDERS-SPEC.md +++ b/PROVIDERS-SPEC.md @@ -4,7 +4,7 @@ Three config files drive the harness: - **`providers.toml`** — provider definitions (inputs, types, checks) - **`openshell.toml`** — which providers to enable, inference model -- **`agents/*.toml`** — per-agent sandbox config (image, command, env vars) +- **`profiles/*.toml`** — per-agent sandbox config (image, command, env vars) ## providers.toml @@ -71,7 +71,7 @@ model = "claude-sonnet-4-6" If absent, all providers are enabled. -## agents/*.toml +## profiles/*.toml Per-agent sandbox configuration. `sandbox-podman.sh` and `sandbox-ocp.sh` read these. diff --git a/README.md b/README.md index 5f5b9a6..3197465 100644 --- a/README.md +++ b/README.md @@ -8,46 +8,79 @@ Deploy AI agent sandboxes on Podman (local) or OpenShift using [OpenShell](https - **GitHub** via gh CLI - Network policy enforcement per sandbox +## Prerequisites + +- [OpenShell CLI](https://github.com/NVIDIA/OpenShell) (`openshell`) +- Python 3.11+ (for TOML parsing) +- Podman (local) or kubectl + helm (OCP) +- `gcloud auth application-default login` (for Vertex AI) + +Optional: `gws` CLI (Google Workspace), `bats` (for unit tests) + +## Setup + +```bash +# Add harness CLI to PATH +export PATH="$PWD/bin:$PATH" + +# See available commands +harness +``` + ## Quick Start (Local) ```bash -# 1. Install OpenShell +# Install OpenShell if you haven't curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh -# 2. Verify gateway -./deploy-podman.sh - -# 3. Register providers (one-time) +# Set credentials export GITHUB_TOKEN="ghp_..." export JIRA_API_TOKEN="..." export ANTHROPIC_VERTEX_PROJECT_ID="my-project" export CLOUD_ML_REGION="us-east5" -./setup-providers.sh -# 4. Launch a sandbox -./sandbox-podman.sh +# Create a sandbox (deploys gateway + registers providers if needed) +harness new --local ``` ## Quick Start (OpenShift) ```bash -# 1. Deploy gateway to cluster -./deploy-ocp.sh +# Create a sandbox on the cluster +harness new --remote +``` -# 2. Store credentials + register providers -./setup-creds.sh -./setup-providers.sh +## CLI Reference -# 3. Launch a sandbox -./sandbox-ocp.sh ``` +harness new [--local|--remote] [--profile NAME] [SANDBOX_NAME] + Create a new sandbox. Auto-deploys gateway and registers providers if needed. -## Agent Configs +harness connect [SANDBOX_NAME] + Reconnect to a running sandbox. -Sandboxes are configured via `agents/*.toml`: +harness deploy [--local|--remote] + Deploy or verify the gateway without creating a sandbox. + +harness teardown [--sandboxes] [--providers] [--k8s] + Tear down sandboxes, providers, or k8s resources. + +harness preflight + Check environment prerequisites. + +harness providers + Register providers with the gateway. + +harness test [podman|ocp|all] [--full] + End-to-end validation. +``` + +## Profiles + +Sandboxes are configured via `profiles/*.toml`: ```toml -# agents/default.toml +# profiles/default.toml name = "agent" image = "quay.io/rcochran/openshell:sandbox" command = "claude --bare" @@ -58,49 +91,47 @@ ANTHROPIC_BASE_URL = "https://inference.local" JIRA_URL = "https://mysite.atlassian.net" ``` -Launch with a specific config: `./sandbox-podman.sh research` (uses `agents/research.toml`). +Use a specific profile: `harness new --profile coder` ## Testing ```bash -bats test/preflight.bats # unit tests (29 tests) -./test-flow.sh podman --full # full local validation -./test-flow.sh ocp --full # full OCP validation -make test # build images + test both +harness test podman --full # full local validation +harness test ocp --full # full OCP validation +bats test/preflight.bats # unit tests (29 tests) +make test # build images + test both ``` ## Files -| File | Purpose | +| Path | Purpose | |------|---------| -| `agents/default.toml` | Agent config (image, command, providers, env vars) | +| `bin/harness` | CLI entry point | +| `bin/scripts/` | Subcommand scripts (new, deploy, teardown, etc.) | +| `bin/scripts/lib/` | Shared libraries (profile parsing, providers, common) | +| `profiles/default.toml` | Default sandbox profile | | `providers.toml` | Provider definitions (env/file/check inputs) | -| `openshell.toml` | Which providers to enable, inference model | -| `deploy-podman.sh` | Verify local gateway is running | -| `deploy-ocp.sh` | Deploy OpenShell to OpenShift (Helm + route) | -| `setup-providers.sh` | Register providers with the gateway | -| `setup-creds.sh` | Store GWS + Atlassian config in cluster (OCP only) | -| `sandbox-podman.sh` | Launch sandbox locally | -| `sandbox-ocp.sh` | Launch sandbox on OpenShift | -| `teardown.sh` | Tear down sandboxes, providers, k8s resources | -| `test-flow.sh` | End-to-end validation | -| `openshell-harness-preflight.sh` | Pre-flight environment check | +| `openshell.toml` | Which providers to enable, upstream version pin | +| `sandbox/` | Sandbox image (Dockerfile, startup.sh, policy.yaml, CLAUDE.md) | +| `sandbox/launcher/` | In-cluster launcher image (for OCP sandboxes) | +| `test/` | Tests (preflight.bats, test-flow.sh) | +| `values-ocp.yaml` | Helm values for OpenShift deployment | | `AGENTS.md` | Project principles and workaround tracking | -## Sandbox Usage +## Why Use a Sandbox? -```bash -openshell sandbox connect # reconnect -openshell sandbox list # list running -openshell sandbox delete # delete -``` +Compared to running Claude Code locally: +- **Credential isolation** — sandbox never sees real API tokens (proxy-resolved placeholders) +- **Network policy** — per-binary egress rules (policy.yaml controls which processes reach which hosts) +- **Reproducible environment** — pinned tool versions in Dockerfile +- **Team sharing** — OCP deployment with mTLS, shared gateway, per-user sandboxes ## Architecture ``` Your Mac OpenShift Cluster ┌──────────┐ ┌──────────────────────────────┐ -│ openshell│ OpenShift Route │ Gateway (StatefulSet) │ +│ harness │ OpenShift Route │ Gateway (StatefulSet) │ │ CLI ├──────────────────►│ ├─ gRPC API │ │ │ TLS passthrough │ ├─ inference.local proxy │ │ │ mTLS :443 │ ├─ Provider credential store │ diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..c5a8183 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,252 @@ +# OpenShell Harness Specification + +This document specifies the behavior of the OpenShell Harness independently of its implementation. Any conforming implementation (bash, Go, Rust, Python) should produce the same observable behavior. + +## Overview + +The harness deploys and manages AI agent sandboxes on two platforms: +- **Local** — Podman containers via a local OpenShell gateway +- **Remote** — Kubernetes pods via an OpenShift-hosted OpenShell gateway + +Each sandbox is an isolated container with a Claude Code agent, credential providers, MCP servers, network policies, and uploaded configuration. + +--- + +## CLI + +The harness exposes a single entry point (`harness`) with subcommands. + +### `harness new [--local|--remote] [--profile NAME] [--name SANDBOX_NAME] [--no-tty]` + +Create a new sandbox. This is the primary command. It performs these steps in order: + +1. **Ensure gateway** — if `--local`, verify the local podman gateway is running. If `--remote`, deploy to OpenShift (Helm chart, CRDs, SCCs, route, mTLS certs). If neither, check for an active gateway. +2. **Ensure providers** — if no providers are registered on the gateway, run provider registration. +3. **Ensure credentials** (remote only) — if K8s secrets for GWS/Atlassian don't exist, create them. +4. **Parse profile** — read `profiles/.toml` (default: `default`). +5. **Stage files** — write `sandbox.env` from profile `[env]`, export GWS credentials. +6. **Create sandbox** — call `openshell sandbox create` with `--from` (image), `--provider` (each provider), `--upload` (staged files), and the startup command. Retry up to 5 times for supervisor race conditions. + +If `--no-tty` is passed, the sandbox runs `startup.sh` and exits (for testing). Otherwise, it runs `startup.sh` then execs into the configured command (e.g., `claude --bare`). + +If `--name` is not provided, the sandbox name comes from the profile's `name` field. + +### `harness connect [SANDBOX_NAME]` + +Reconnect to a running sandbox via `openshell sandbox connect`. + +### `harness deploy [--local|--remote]` + +Deploy or verify the gateway without creating a sandbox. + +**Local:** Find a gateway with endpoint `127.0.0.1`, select it, verify it responds. + +**Remote:** Create namespace, install CRDs, grant SCCs, deploy Helm chart from OCI registry (`oci://ghcr.io/nvidia/openshell/helm-chart`), create TLS passthrough route, register CLI gateway with mTLS certs from the cluster. + +### `harness teardown [--sandboxes] [--providers] [--k8s]` + +Tear down resources. Default (no flags) tears down everything applicable. + +- `--sandboxes` — delete all sandboxes on the active gateway +- `--providers` — delete all providers and inference config (requires no running sandboxes) +- `--k8s` — Helm uninstall, delete CRDs, SCCs, secrets, namespace, and gateway config + +### `harness preflight` + +Read-only environment check. Validates all inputs defined in `providers.toml` for each enabled provider in `openshell.toml`. Reports per-input status with `✓`/`✗` prefixes. + +### `harness providers` + +Register credential providers with the gateway. Reads provider types and credentials from environment variables. Supports `--force` to delete and recreate all providers. + +### `harness test [podman|ocp|all] [--full]` + +End-to-end validation. Quick mode: deploy → providers → gateway check → teardown. Full mode adds: sandbox create → verify env vars, GWS creds, MCP config, Claude responds → sandbox delete → teardown. + +--- + +## Configuration + +### `providers.toml` + +Catalog of provider definitions. Each `[[providers]]` entry has: + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | yes | Unique identifier | +| `type` | yes | `"openshell"` or `"custom"` | +| `description` | yes | Shown in preflight | +| `required` | no | If true, `--strict` preflight fails when inputs missing | +| `method` | no | Registration method (e.g., `"from-gcloud-adc"`) | +| `upstream` | no | Link to upstream issue (custom providers) | + +Each provider has an `inputs` array of inline tables: + +| Field | Required | Description | +|-------|----------|-------------| +| `key` | yes | Env var name, file path, or shell command | +| `kind` | yes | `"env"`, `"file"`, or `"check"` | +| `secret` | no | Mask value in preflight output | + +### `openshell.toml` + +Deployment configuration: + +```toml +providers = ["github", "vertex-local", "atlassian"] +providers-custom = ["gws"] + +[inference] +model = "claude-sonnet-4-6" + +[upstream] +chart-version = "0.0.55" +``` + +### `profiles/.toml` + +Per-sandbox configuration: + +```toml +name = "agent" +image = "quay.io/rcochran/openshell:sandbox" +command = "claude --bare" +keep = true +providers = ["github", "vertex-local", "atlassian"] + +[env] +ANTHROPIC_BASE_URL = "https://inference.local" +ANTHROPIC_API_KEY = "sk-ant-openshell-proxy-managed" +JIRA_URL = "https://mysite.atlassian.net" +JIRA_USERNAME = "user@example.com" +``` + +| Field | Default | Description | +|-------|---------|-------------| +| `name` | `"agent"` | Sandbox name (overridden by `--name`) | +| `image` | none | Container image for the sandbox | +| `command` | `"claude --bare"` | Command to exec after startup | +| `keep` | `true` | Keep sandbox alive after command exits | +| `providers` | `[]` | Provider names to attach | +| `[env]` | `{}` | Environment variables injected into the sandbox | + +--- + +## Sandbox Lifecycle + +### Creation + +1. Profile parsed → `SANDBOX_IMAGE`, `SANDBOX_COMMAND`, `SANDBOX_PROVIDERS`, `SANDBOX_ENV` +2. Files staged to `/tmp/openshell/`: + - `sandbox.env` — export statements from `[env]` section + - `credentials.json` — GWS OAuth credentials (if available) + - `client_secret.json` — GWS OAuth client config (if available) +3. `openshell sandbox create` called with: + - `--from ` — sandbox container image + - `--provider ` — for each provider + - `--upload /tmp/openshell:/sandbox/.config` — files land at `/sandbox/.config/openshell/` + - `-- bash -c '. /sandbox/startup.sh && exec '` (tty mode) + - `-- bash /sandbox/startup.sh` (no-tty mode) +4. On failure (supervisor race), delete sandbox and retry (up to 5 times, 5s between) + +### Startup (inside sandbox) + +`startup.sh` runs once at creation: +1. Source `/sandbox/.config/openshell/sandbox.env` → append to `.bashrc` +2. Run `gh auth setup-git` + +### Connection + +`openshell sandbox connect ` opens an interactive session. Environment variables from `.bashrc` are inherited by Claude Code and its MCP servers. + +### Deletion + +`openshell sandbox delete ` or `harness teardown --sandboxes`. + +--- + +## Credential Flow + +| Credential | Provider Type | How It Works | +|------------|--------------|--------------| +| GitHub | `github` | PAT stored in gateway, proxy-managed. Sandbox sees `GITHUB_TOKEN` placeholder. | +| Vertex AI | `google-vertex-ai` | ADC-based OAuth via `--from-gcloud-adc`. Gateway refreshes tokens automatically. Inference routed through `inference.local`. | +| Atlassian | `atlassian` | API token stored in gateway. Proxy resolves base64 Basic auth header. `JIRA_URL`/`JIRA_USERNAME` injected via `sandbox.env`. | +| GWS | Custom (file upload) | Decrypted OAuth credentials uploaded to `/sandbox/.config/openshell/`. Not proxy-managed. | + +--- + +## Sandbox Image + +The sandbox image extends `ghcr.io/nvidia/openshell-community/sandboxes/base:latest` with: +- `mcp-atlassian` — Jira/Confluence MCP server +- `gws` CLI — Google Workspace +- `policy.yaml` — network egress rules +- `CLAUDE.md` — agent instructions +- `settings.json` — Claude Code permissions (`defaultMode: bypassPermissions`) +- `.mcp.json` — MCP server configuration (auto-loaded by Claude Code) +- `startup.sh` — runtime env setup + +The image is multi-arch (`linux/amd64` + `linux/arm64`), built with `docker buildx`. + +--- + +## Network Policy + +`sandbox/policy.yaml` controls which processes can reach which hosts: + +| Policy | Hosts | Binaries | +|--------|-------|----------| +| `claude_telemetry` | `*.anthropic.com`, `downloads.claude.ai` | claude, node | +| `github_git` | `github.com` (GET info/refs, POST git-upload-pack only) | git | +| `github_downloads` | `*.githubusercontent.com`, `codeload.github.com` | curl, gh, git, uv | +| `google_workspace` | `*.googleapis.com`, `oauth2.googleapis.com` | gws | +| `pypi` | `pypi.org`, `files.pythonhosted.org` | python, pip, uv | +| `npm` | `registry.npmjs.org` | npm, node | + +Git push (`git-receive-pack`) is blocked by default. + +--- + +## OCP-Specific + +### Gateway Deployment + +- Helm chart from `oci://ghcr.io/nvidia/openshell/helm-chart` (version pinned in `openshell.toml`) +- TLS passthrough route at `gateway-openshell.` +- mTLS: client certs copied from `openshell-client-tls` K8s secret to `~/.config/openshell/gateways//mtls/` +- `allowUnauthenticatedUsers: true` (mTLS is the auth layer) + +### In-Cluster Launcher + +For OCP sandboxes, a Kubernetes Job runs the launcher image (`sandbox/launcher/`): +1. Register gateway via `http://` trick (avoids cert generation probe), patch to `https://` + mTLS +2. Parse profile from mounted ConfigMap +3. Build provider flags, create sandbox with retry +4. Upload files (GWS creds, sandbox.env) +5. Run startup.sh via `sandbox exec` + +The launcher connects to the gateway at `https://openshell.openshell.svc.cluster.local:8080` using mounted mTLS certs from `openshell-client-tls`. + +--- + +## Testing + +### Unit Tests (`test/preflight.bats`) + +29 bats tests covering the preflight check engine (`lib/providers.py`). Uses stubbed CLI, isolated temp dirs, and no gateway dependency. Tests: +- env inputs (set/missing/secret/masked) +- file inputs (exists/missing/metadata extraction) +- check inputs (pass/fail/env expansion) +- provider status (all pass/any fail/required/optional) +- config filtering (enabled/disabled/no config) +- CLI detection (present/missing) +- Gateway detection (podman/k8s) + +### Integration Tests (`test/test-flow.sh`) + +End-to-end validation requiring a live gateway: +- Quick mode: deploy → providers → gateway check → teardown +- Full mode: + sandbox create → verify env/GWS/MCP/Claude → delete → teardown +- Targets: `podman`, `ocp`, `all` +- Strips ANSI codes from CLI output for reliable parsing diff --git a/bin/scripts/lib/parse-profile.py b/bin/scripts/lib/parse-profile.py new file mode 100644 index 0000000..3900f0f --- /dev/null +++ b/bin/scripts/lib/parse-profile.py @@ -0,0 +1,40 @@ +#!/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 index fc99dd9..f261f5f 100644 --- a/bin/scripts/lib/profile.sh +++ b/bin/scripts/lib/profile.sh @@ -1,32 +1,20 @@ #!/usr/bin/env bash -# Agent config parsing helpers. +# Profile parsing helpers. # # Source from any script: # source "$(dirname "$0")/lib/profile.sh" # # Usage: -# parse_agent agents/default.toml +# parse_profile profiles/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; } +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - 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")" +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. @@ -45,10 +33,6 @@ build_provider_flags() { # 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" diff --git a/bin/scripts/new.sh b/bin/scripts/new.sh index 54035cb..3a6f439 100755 --- a/bin/scripts/new.sh +++ b/bin/scripts/new.sh @@ -62,7 +62,7 @@ fi # ── 4. Parse profile ───────────────────────────────────────────────── PROFILE_FILE="$HARNESS_DIR/profiles/${PROFILE}.toml" NAME_OVERRIDE="$SANDBOX_NAME" -parse_agent "$PROFILE_FILE" +parse_profile "$PROFILE_FILE" [[ -n "$NAME_OVERRIDE" ]] && SANDBOX_NAME="$NAME_OVERRIDE" echo "" diff --git a/bin/scripts/providers.sh b/bin/scripts/providers.sh index d5e168d..4c9f10d 100755 --- a/bin/scripts/providers.sh +++ b/bin/scripts/providers.sh @@ -61,7 +61,7 @@ if $FORCE; then "$CLI" provider profile delete "$id" 2>/dev/null || true done fi -"$CLI" provider profile import --from "$SCRIPT_DIR/sandbox/profiles/" 2>/dev/null || echo " (already imported)" +"$CLI" provider profile import --from "$HARNESS_DIR/sandbox/profiles/" 2>/dev/null || echo " (already imported)" echo "" echo "=== Registering providers ===" diff --git a/sandbox-local.sh b/sandbox-local.sh deleted file mode 100755 index 0cd998b..0000000 --- a/sandbox-local.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env bash -# Launch a sandbox on the local Podman/Docker gateway. -# -# Uses the same providers as the OCP workflow (setup-providers.sh). -# GWS credentials are exported from the local gws CLI. -# Atlassian URL/username come from env vars (not K8s secrets). -# -# Prerequisites: -# ./deploy-local.sh # verify gateway running -# ./setup-providers.sh # register providers -# -# Usage: -# ./sandbox-local.sh # interactive Claude session -# ./sandbox-local.sh --name my-sandbox # custom name -# ./sandbox-local.sh --rejoin my-sandbox # reconnect -# ./sandbox-local.sh --no-keep # delete after exit -set -euo pipefail - -CLI="${OPENSHELL_CLI:-openshell}" -command -v "$CLI" &>/dev/null || { echo "ERROR: openshell CLI not found."; exit 1; } - -# Validate we're targeting a local gateway, not the OCP one -GW="${OPENSHELL_GATEWAY:-}" -if [[ "$GW" == "ocp" ]]; then - echo "ERROR: OPENSHELL_GATEWAY=ocp — this script is for local gateways." - echo " Use ./sandbox.sh for OpenShift, or: export OPENSHELL_GATEWAY=" - exit 1 -fi - -# ── Parse args ───────────────────────────────────────────────────────── -EXTRA=() -while [[ $# -gt 0 ]]; do - case $1 in - --rejoin) - echo "Reconnecting to sandbox: $2" - exec "$CLI" sandbox connect "$2" - ;; - --name|--editor) EXTRA+=("$1" "$2"); shift 2 ;; - --no-keep) EXTRA+=("$1"); shift ;; - *) echo "Unknown argument: $1"; exit 1 ;; - esac -done - -# ── Detect registered providers ──────────────────────────────────────── -PROVIDERS=() -for name in github vertex-local atlassian; do - if "$CLI" provider get "$name" &>/dev/null; then - PROVIDERS+=(--provider "$name") - fi -done - -echo "=== Providers ===" -for name in github vertex-local atlassian; do - if "$CLI" provider get "$name" &>/dev/null; then - echo " $name: attached" - else - echo " $name: not registered (skipping)" - fi -done - -# ── Stage files for upload ───────────────────────────────────────────── -UPLOAD_ARGS=() -STAGE="" - -# GWS: export decrypted credentials -echo "" -echo "=== Credentials ===" -if command -v gws &>/dev/null && gws auth status &>/dev/null; then - STAGE=$(mktemp -d) - mkdir -p "$STAGE/creds/gws-config" - if gws auth export --unmasked > "$STAGE/creds/gws-config/credentials.json" 2>/dev/null; then - GWS_DIR="${GWS_CONFIG_DIR:-$HOME/.config/gws}" - [[ -f "$GWS_DIR/client_secret.json" ]] && cp "$GWS_DIR/client_secret.json" "$STAGE/creds/gws-config/" - chmod 600 "$STAGE/creds/gws-config"/* - UPLOAD_ARGS=(--upload "$STAGE/creds:/sandbox/.harness") - echo " GWS: exported" - else - echo " GWS: export failed (skipping)" - rm -rf "$STAGE/creds/gws-config" - fi -else - echo " GWS: not authenticated (skipping)" -fi - -# Atlassian non-secret config -if [[ -n "${JIRA_URL:-}" ]]; then - STAGE="${STAGE:-$(mktemp -d)}" - mkdir -p "$STAGE/creds" - python3 -c "import json,sys; json.dump({'jira_url':sys.argv[1],'jira_username':sys.argv[2]},open(sys.argv[3],'w'))" \ - "$JIRA_URL" "${JIRA_USERNAME:-}" "$STAGE/creds/atlassian.json" - UPLOAD_ARGS=(--upload "$STAGE/creds:/sandbox/.harness") - echo " Atlassian: $JIRA_URL" -else - echo " Atlassian: JIRA_URL not set (skipping)" -fi - -# ── Create sandbox ───────────────────────────────────────────────────── -echo "" -echo "=== Creating sandbox ===" -exec "$CLI" sandbox create \ - --tty \ - ${PROVIDERS[@]+"${PROVIDERS[@]}"} \ - ${UPLOAD_ARGS[@]+"${UPLOAD_ARGS[@]}"} \ - ${EXTRA[@]+"${EXTRA[@]}"} \ - -- bash -c '. /sandbox/startup.sh && exec claude --bare' diff --git a/sandbox/configure-mcp.py b/sandbox/configure-mcp.py deleted file mode 100644 index dea76a0..0000000 --- a/sandbox/configure-mcp.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -"""Generate .claude.json MCP server config from environment variables. - -Writes /sandbox/.claude.json with configured MCP servers based on -which credentials are available. Skips servers whose credentials are -not set. - -JIRA_URL and JIRA_USERNAME are read from an uploaded config file -(not secrets — no need for provider placeholders). JIRA_API_TOKEN -comes from the atlassian provider as a placeholder resolved by the proxy. -""" - -import json -import os -import stat - -config = { - "autoUpdates": False, - "hasCompletedOnboarding": True, - "projects": { - "/sandbox": { - "hasTrustDialogAccepted": True, - "allowedTools": [], - }, - }, - "mcpServers": {}, -} - -# ── Atlassian (sooperset/mcp-atlassian) ──────────────────────────────── -# JIRA_URL and JIRA_USERNAME come from either: -# - An uploaded atlassian.json file (sandbox.sh / local workflow) -# - Environment variables injected via secretKeyRef (in-cluster launcher) -atlassian_config = "/sandbox/.harness/creds/atlassian.json" -jira_url = "" -jira_username = "" -if os.path.isfile(atlassian_config): - try: - with open(atlassian_config) as f: - atlassian = json.load(f) - jira_url = atlassian.get("jira_url", "") - jira_username = atlassian.get("jira_username", "") - except (json.JSONDecodeError, OSError) as e: - print(f"WARNING: could not read atlassian config: {e}", flush=True) -# Fall back to env vars (set by in-cluster launcher via secretKeyRef) -if not jira_url: - jira_url = os.environ.get("JIRA_URL", "") -if not jira_username: - jira_username = os.environ.get("JIRA_USERNAME", "") - -if jira_url: - jira_api_token = os.environ.get("JIRA_API_TOKEN", "") - config["mcpServers"]["atlassian"] = { - "type": "stdio", - "command": "/sandbox/.venv/bin/mcp-atlassian", - "args": [], - "env": { - "JIRA_URL": jira_url, - "JIRA_USERNAME": jira_username, - "JIRA_API_TOKEN": jira_api_token, - "CONFLUENCE_URL": jira_url.rstrip("/") + "/wiki", - "CONFLUENCE_USERNAME": jira_username, - "CONFLUENCE_API_TOKEN": jira_api_token, - "READ_ONLY_MODE": os.environ.get("READ_ONLY_MODE", "true"), - }, - } - -path = "/sandbox/.claude.json" -with open(path, "w") as f: - json.dump(config, f, indent=2) -os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)