diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index ce2c873..df6dc87 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -5,9 +5,6 @@ on: branches: [main] tags: ["v*"] pull_request: - paths: - - 'sandbox/**' - - 'build/**' permissions: contents: read @@ -34,19 +31,16 @@ jobs: with: images: ${{ env.IMAGE_BASE }} tags: | - type=raw,value=sandbox,enable={{is_default_branch}} type=semver,pattern=sandbox-v{{version}} - type=ref,event=pr,prefix=sandbox-pr- + type=sha,prefix=sandbox-,format=short - uses: docker/build-push-action@v6 with: context: sandbox platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: | - type=registry,ref=${{ env.IMAGE_BASE }}:sandbox-cache - type=registry,ref=${{ env.IMAGE_BASE }}:sandbox + cache-from: type=registry,ref=${{ env.IMAGE_BASE }}:sandbox-cache cache-to: type=registry,ref=${{ env.IMAGE_BASE }}:sandbox-cache,mode=max runner: @@ -57,7 +51,11 @@ jobs: with: go-version-file: go.mod - name: Build harness binary - run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/runner/harness . + run: | + VERSION=$(git describe --tags --always 2>/dev/null || echo dev) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags "-s -w -X main.version=${VERSION}" \ + -o build/runner/harness . - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: @@ -69,17 +67,14 @@ jobs: with: images: ${{ env.IMAGE_BASE }} tags: | - type=raw,value=runner,enable={{is_default_branch}} type=semver,pattern=runner-v{{version}} - type=ref,event=pr,prefix=runner-pr- + type=sha,prefix=runner-,format=short - uses: docker/build-push-action@v6 with: context: build/runner platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: | - type=registry,ref=${{ env.IMAGE_BASE }}:runner-cache - type=registry,ref=${{ env.IMAGE_BASE }}:runner + cache-from: type=registry,ref=${{ env.IMAGE_BASE }}:runner-cache cache-to: type=registry,ref=${{ env.IMAGE_BASE }}:runner-cache,mode=max diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index e5f8ebe..600c5db 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -31,8 +31,8 @@ jobs: done openshell gateway list - - name: Run CI local - run: make ci-local + - name: Run local integration + run: make test-local - name: Export gateway logs if: always() @@ -69,8 +69,8 @@ jobs: - name: Install helm uses: azure/setup-helm@v4 - - name: Run CI kind - run: make ci-kind + - name: Run kind integration + run: make test-kind - name: Export logs if: always() diff --git a/Makefile b/Makefile index 024fc62..92f19b7 100644 --- a/Makefile +++ b/Makefile @@ -1,65 +1,64 @@ ## OpenShell Harness — build, push, and test ## -## CI targets (no credentials, GHA-safe): -## make ci # unit + bats + lint (~2min) -## make ci-local # ci + local gateway integration -## make ci-kind # ci + kind (self-contained cluster) -## -## Developer targets (credentials available): -## make dev-test-local # pre-commit: unit + bats + local full + ci -## make dev-test-kind # kind: self-contained lifecycle -## make dev-test-remote # OCP: needs KUBECONFIG -## make dev-test-all # all of the above +## Tests (CI mode auto-detects from CI env var): +## make test # unit + bats + lint (~2min) +## make test-local # test + local gateway integration +## make test-kind # test + kind (self-contained cluster) +## make test-remote # test + OCP (needs KUBECONFIG) +## make test-all # all of the above ## ## Images: ## make sandbox # build + push sandbox (multi-arch) ## make runner # build + push runner REGISTRY ?= ghcr.io/robbycochran/harness-openshell -DEV_REGISTRY ?= quay.io/rcochran/openshell -DEV_TAG := dev-$(shell git rev-parse --short HEAD) +CONTAINER_CLI ?= podman PLATFORM := linux/amd64 +VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev) +LDFLAGS := -s -w -X main.version=$(VERSION) -SANDBOX_IMAGE := $(REGISTRY):sandbox -RUNNER_IMAGE := $(REGISTRY):runner -DEV_SANDBOX_IMAGE := $(DEV_REGISTRY):$(DEV_TAG)-sandbox -DEV_RUNNER_IMAGE := $(DEV_REGISTRY):$(DEV_TAG)-runner +DEV_SANDBOX_IMAGE := $(REGISTRY):sandbox-$(VERSION) +DEV_RUNNER_IMAGE := $(REGISTRY):runner-$(VERSION) -.PHONY: cli sandbox push-sandbox cli-runner runner push-runner \ - vet lint ci ci-local ci-kind \ - dev-test-local dev-test-kind dev-test-remote dev-test-all \ - dev-sandbox dev-runner clean help +.PHONY: all cli sandbox push-sandbox cli-runner runner push-runner \ + vet lint test test-local test-kind test-remote test-all \ + dev-sandbox dev-runner dev-push clean help ## ── CLI ────────────────────────────────────────────────────────────── +## Build CLI + sandbox image for local dev +all: cli dev-sandbox + ## Build the harness CLI binary cli: - CGO_ENABLED=0 go build -o harness . - @echo "Built: ./harness" + CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o harness . + @echo "Built: ./harness ($(VERSION))" ## ── Images ──────────────────────────────────────────────────────────── ## Sandbox image (Claude Code + mcp-atlassian + gws, multi-arch) sandbox: sandbox/Dockerfile sandbox/startup.sh \ sandbox/policy.yaml sandbox/CLAUDE.md sandbox/settings.json - docker buildx build --platform linux/amd64,linux/arm64 -t $(SANDBOX_IMAGE) sandbox/ --push - @echo "Built and pushed: $(SANDBOX_IMAGE) (multi-arch)" + -$(CONTAINER_CLI) manifest rm $(SANDBOX_IMAGE) 2>/dev/null + $(CONTAINER_CLI) build --platform linux/amd64 --manifest $(SANDBOX_IMAGE) sandbox/ + $(CONTAINER_CLI) build --platform linux/arm64 --manifest $(SANDBOX_IMAGE) sandbox/ + @echo "Built: $(SANDBOX_IMAGE) (multi-arch)" push-sandbox: sandbox - @echo "Already pushed by buildx" + $(CONTAINER_CLI) manifest push $(SANDBOX_IMAGE) ## Cross-compile harness binary for the runner image cli-runner: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/runner/harness . - @echo "Built: build/runner/harness" + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o build/runner/harness . + @echo "Built: build/runner/harness ($(VERSION))" ## Runner image (harness binary + openshell CLI) runner: cli-runner build/runner/Dockerfile - docker build --platform $(PLATFORM) -t $(RUNNER_IMAGE) build/runner/ + $(CONTAINER_CLI) build --platform $(PLATFORM) -t $(RUNNER_IMAGE) build/runner/ @echo "Built: $(RUNNER_IMAGE)" push-runner: runner - docker push $(RUNNER_IMAGE) + $(CONTAINER_CLI) push $(RUNNER_IMAGE) ## ── Lint targets ───────────────────────────────────────────────────── @@ -76,69 +75,58 @@ lint: $(MAKE) vet; \ fi -## ── CI targets (no credentials, GHA-safe) ──────────────────────────── +## ── Test targets ────────────────────────────────────────────────────── +## CI mode auto-detects from the CI env var (set by GitHub Actions). +## Locally: full tests with credentials. On GHA: no-credential mode. ## Unit tests + bats + lint (fast, ~2min, no gateway needed) -ci: vet +test: vet CGO_ENABLED=0 go test ./... bats test/preflight.bats -## CI + local gateway integration (ci mode, no credentials) -ci-local: cli ci - ./test/test-flow.sh local --ci - -## CI + kind gateway integration (self-contained, isolated kubeconfig) -ci-kind: cli ci - ./test/kind-lifecycle.sh --ci - -## ── Developer targets (credentials available) ──────────────────────── +## Local gateway integration +test-local: cli test + ./test/test-flow.sh local -## Pre-commit local: unit + bats + local full + local CI -## Requires: openshell gateway running locally, provider credentials. -dev-test-local: cli ci - @echo "" - @echo "=== Integration: local (full) ===" - ./test/test-flow.sh local --full - @echo "" - @echo "=== Integration: local (ci) ===" - ./test/test-flow.sh local --ci - -## Kind: unit + bats + kind full (self-contained lifecycle) -## Creates/destroys its own kind cluster. Never touches your OCP kubectl context. +## Kind: self-contained cluster lifecycle ## Builds sandbox image locally and pre-loads into kind (no registry push needed). ## Use KEEP=1 to keep the cluster after tests (for debugging). -dev-test-kind: cli ci - docker build -t $(DEV_SANDBOX_IMAGE) sandbox/ +test-kind: cli test + $(CONTAINER_CLI) build -t $(DEV_SANDBOX_IMAGE) sandbox/ @echo "" SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) ./test/kind-lifecycle.sh $(if $(KEEP),--keep) -## Remote (OCP): unit + bats + OCP full + OCP CI -## Requires: KUBECONFIG set, provider credentials. -## Builds dev images to quay.io (OCP can't pull private ghcr.io). -dev-test-remote: cli ci dev-sandbox dev-runner +## Remote (OCP): requires KUBECONFIG set +test-remote: cli test dev-sandbox dev-runner @test -n "$${KUBECONFIG}" || { echo "ERROR: Set KUBECONFIG for OCP (e.g. export KUBECONFIG=infracluster/kubeconfig)"; exit 1; } @echo "" - @echo "=== Integration: OCP (full) ===" - SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) RUNNER_IMAGE=$(DEV_RUNNER_IMAGE) ./test/test-flow.sh ocp --full - @echo "" - @echo "=== Integration: OCP (ci) ===" - RUNNER_IMAGE=$(DEV_RUNNER_IMAGE) ./test/test-flow.sh ocp --ci + SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) RUNNER_IMAGE=$(DEV_RUNNER_IMAGE) ./test/test-flow.sh ocp ## All: local + kind + remote -dev-test-all: dev-test-local dev-test-kind dev-test-remote +test-all: test-local test-kind test-remote ## ── Dev image builds ───────────────────────────────────────────────── -## Build dev sandbox image to quay.io (multi-arch) +## Build dev sandbox image locally (native arch only) dev-sandbox: - docker buildx build --platform linux/amd64,linux/arm64 -t $(DEV_SANDBOX_IMAGE) sandbox/ --push - @echo "Built and pushed: $(DEV_SANDBOX_IMAGE)" + $(CONTAINER_CLI) build -t $(DEV_SANDBOX_IMAGE) sandbox/ + @echo "Built: $(DEV_SANDBOX_IMAGE)" -## Build dev runner image to quay.io +## Build dev runner image locally dev-runner: cli-runner - docker build --platform $(PLATFORM) -t $(DEV_RUNNER_IMAGE) build/runner/ - docker push $(DEV_RUNNER_IMAGE) - @echo "Built and pushed: $(DEV_RUNNER_IMAGE)" + $(CONTAINER_CLI) build --platform $(PLATFORM) -t $(DEV_RUNNER_IMAGE) build/runner/ + @echo "Built: $(DEV_RUNNER_IMAGE)" + +## Build and push dev images (sandbox: multi-arch, runner: amd64) +dev-push: cli-runner + -$(CONTAINER_CLI) rmi $(DEV_SANDBOX_IMAGE) 2>/dev/null + -$(CONTAINER_CLI) manifest rm $(DEV_SANDBOX_IMAGE) 2>/dev/null + $(CONTAINER_CLI) build --platform linux/amd64 --manifest $(DEV_SANDBOX_IMAGE) sandbox/ + $(CONTAINER_CLI) build --platform linux/arm64 --manifest $(DEV_SANDBOX_IMAGE) sandbox/ + $(CONTAINER_CLI) manifest push $(DEV_SANDBOX_IMAGE) + $(CONTAINER_CLI) build --platform $(PLATFORM) -t $(DEV_RUNNER_IMAGE) build/runner/ + $(CONTAINER_CLI) push $(DEV_RUNNER_IMAGE) + @echo "Pushed: $(DEV_SANDBOX_IMAGE) (multi-arch) $(DEV_RUNNER_IMAGE) (amd64)" ## ── Convenience targets ─────────────────────────────────────────────── diff --git a/SPEC.md b/SPEC.md index 49fdd35..53ef23b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -17,8 +17,7 @@ Agent configs live in `agents/*.yaml`. Each declares the sandbox image, entrypoi ```yaml name: agent -image: ghcr.io/robbycochran/harness-openshell:sandbox -entrypoint: claude --bare +entrypoint: claude tty: true providers: @@ -36,8 +35,8 @@ env: Fields: - `name` (required) -- sandbox name, used for `openshell sandbox connect` -- `image` -- container image for the sandbox -- `entrypoint` -- command to run (default: `claude --bare`) +- `image` -- container image for the sandbox (default: version-matched from ghcr.io, override with `SANDBOX_IMAGE` env) +- `entrypoint` -- command to run (default: `claude`) - `tty` -- enable TTY (default: false) - `task` -- path to a task.md file, passed as argument to entrypoint - `providers` -- list of provider profile references @@ -105,6 +104,23 @@ In-cluster command for the runner Job. Reads agent config from `/etc/openshell/s | `build/runner/Dockerfile` | Runner image: harness binary + openshell CLI | | `sandbox/policy.yaml` | Network egress rules applied to sandboxes | +## Image Tags + +All images are published to `ghcr.io/robbycochran/harness-openshell`. No floating tags (`:latest`, `:sandbox`, `:runner`) are used. + +| Trigger | Sandbox | Runner | +|---------|---------|--------| +| Release `v0.1.2` | `:sandbox-v0.1.2` | `:runner-v0.1.2` | +| Any push/PR | `:sandbox-` | `:runner-` | + +The CLI resolves images from its embedded version (set via `-ldflags` at build time): + +- `v0.1.2` → `:sandbox-v0.1.2` (tagged release) +- `v0.1.2-5-gabc1234` → `:sandbox-v0.1.2-5-gabc1234` (dev build, matches `make dev-sandbox`) +- `dev` → `:sandbox` (bare `go build` without ldflags) + +`SANDBOX_IMAGE` and `RUNNER_IMAGE` env vars override the version-based resolution. + ## Environment Variables | Variable | Purpose | diff --git a/agents/default.yaml b/agents/default.yaml index 627d9d5..ce46633 100644 --- a/agents/default.yaml +++ b/agents/default.yaml @@ -5,8 +5,7 @@ # harness up --agent research # uses agents/research.yaml name: agent -image: ghcr.io/robbycochran/harness-openshell:sandbox -entrypoint: claude --bare +entrypoint: claude tty: true providers: diff --git a/agents/demo.yaml b/agents/demo.yaml index 6614c7f..f021aa1 100644 --- a/agents/demo.yaml +++ b/agents/demo.yaml @@ -4,8 +4,7 @@ # harness up --local --agent demo name: demo -image: ghcr.io/robbycochran/harness-openshell:sandbox -entrypoint: claude --bare -p +entrypoint: claude -p task: demo/DEMO-TASK.md tty: false diff --git a/build/runner/Dockerfile b/build/runner/Dockerfile index 581dcf4..e332d81 100644 --- a/build/runner/Dockerfile +++ b/build/runner/Dockerfile @@ -2,7 +2,7 @@ FROM registry.access.redhat.com/ubi9/ubi-minimal:latest RUN microdnf install -y openssh-clients tar gzip --nodocs && microdnf clean all -ARG OPENSHELL_VERSION=0.0.57 +ARG OPENSHELL_VERSION=0.0.58 RUN curl -LsSf "https://github.com/NVIDIA/OpenShell/releases/download/v${OPENSHELL_VERSION}/openshell-x86_64-unknown-linux-musl.tar.gz" \ | tar xz -C /usr/local/bin openshell diff --git a/cmd/create.go b/cmd/create.go index cf13652..f1f8e3c 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -53,9 +53,16 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command { name = sandboxName } + sandboxImage := agentCfg.Image + if sandboxImage == "" { + sandboxImage = defaultSandboxImage() + } else if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { + sandboxImage = envImage + } + status.Section("Agent") fmt.Printf(" Name: %s\n", name) - fmt.Printf(" Image: %s\n", agentCfg.Image) + fmt.Printf(" Image: %s\n", sandboxImage) // 3. Validate providers are registered status.Section("Providers") @@ -71,16 +78,16 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command { return fmt.Errorf("no providers available — run: harness providers") } - // 4. Run preflight checks - status.Section("Preflight") + // 4. Load provider definitions providersPath := filepath.Join(harnessDir, "providers.toml") - allProviders, err := preflight.LoadProviders(providersPath) - if err != nil { - status.Warn("could not load providers.toml — skipping preflight") - } else { + allProviders, _ := preflight.LoadProviders(providersPath) + + // 5. Run preflight checks (only for unregistered providers) + if len(missing) > 0 && allProviders != nil { + status.Section("Preflight") preflightOK := true for _, p := range allProviders { - if !providerInList(p.Name, providerNames) { + if !providerInList(p.Name, missing) { continue } ok, details := preflight.CheckProvider(p) @@ -101,7 +108,7 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command { } } - // 5. Determine whether the in-cluster runner is needed. + // 6. Determine whether the in-cluster runner is needed. needsRunner := false if !isLocal { needsRunner = profileHasCustomProviders(providerNames, allProviders) @@ -126,11 +133,6 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command { return fmt.Errorf("rendering payload: %w", err) } - sandboxImage := agentCfg.Image - if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { - sandboxImage = envImage - } - cfg := &profile.Config{ Name: name, From: sandboxImage, diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index 887a433..9cc00bb 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -33,7 +33,7 @@ mode = "launcher" [chart] oci = "oci://ghcr.io/nvidia/openshell/helm-chart" -version = "0.0.55" +version = "0.0.58" [chart.crd] url = "https://example.com/crd.yaml" @@ -78,7 +78,7 @@ mode = "direct" [chart] oci = "oci://ghcr.io/nvidia/openshell/helm-chart" -version = "0.0.55" +version = "0.0.58" [chart.crd] url = "https://example.com/crd.yaml" @@ -89,7 +89,7 @@ url = "https://example.com/crd.yaml" func TestDeployFromConfig_OCP_Success(t *testing.T) { dir := setupDeployHarnessDir(t) gwDir := setupOCPGatewayConfig(t, dir) - t.Setenv("OPENSHELL_CHART_VERSION", "0.0.55") + t.Setenv("OPENSHELL_CHART_VERSION", "0.0.58") t.Setenv("OPENSHELL_NAMESPACE", "openshell") t.Setenv("HOME", t.TempDir()) @@ -139,7 +139,7 @@ func TestDeployFromConfig_OCP_Success(t *testing.T) { func TestDeployFromConfig_K8s_NoSCCs(t *testing.T) { dir := setupDeployHarnessDir(t) gwDir := setupK8sGatewayConfig(t, dir) - t.Setenv("OPENSHELL_CHART_VERSION", "0.0.55") + t.Setenv("OPENSHELL_CHART_VERSION", "0.0.58") t.Setenv("OPENSHELL_NAMESPACE", "openshell") t.Setenv("HOME", t.TempDir()) @@ -173,7 +173,7 @@ func TestDeployFromConfig_K8s_NoSCCs(t *testing.T) { func TestDeployFromConfig_HelmFailure(t *testing.T) { dir := setupDeployHarnessDir(t) gwDir := setupOCPGatewayConfig(t, dir) - t.Setenv("OPENSHELL_CHART_VERSION", "0.0.55") + t.Setenv("OPENSHELL_CHART_VERSION", "0.0.58") t.Setenv("OPENSHELL_NAMESPACE", "openshell") gwCfg, err := gateway.LoadConfig(gwDir) @@ -200,7 +200,7 @@ func TestDeployFromConfig_HelmFailure(t *testing.T) { func TestDeployFromConfig_CRDFailure(t *testing.T) { dir := setupDeployHarnessDir(t) gwDir := setupOCPGatewayConfig(t, dir) - t.Setenv("OPENSHELL_CHART_VERSION", "0.0.55") + t.Setenv("OPENSHELL_CHART_VERSION", "0.0.58") t.Setenv("OPENSHELL_NAMESPACE", "openshell") gwCfg, err := gateway.LoadConfig(gwDir) diff --git a/cmd/helpers_test.go b/cmd/helpers_test.go index 3ee2f24..ba9b855 100644 --- a/cmd/helpers_test.go +++ b/cmd/helpers_test.go @@ -42,7 +42,7 @@ func (m *mockGW) SandboxDelete(name string) error { m.deletedNames = append(m.deletedNames, name) return nil } -func (m *mockGW) CLIVersion() string { return "openshell v0.0.55" } +func (m *mockGW) CLIVersion() string { return "openshell v0.0.58" } func (m *mockGW) CLIPath() string { return "/usr/bin/openshell" } func (m *mockGW) InferenceModel() string { return "" } func (m *mockGW) InferenceSet(string, string) error { return nil } diff --git a/cmd/launch.go b/cmd/launch.go index d402fd5..ced117f 100644 --- a/cmd/launch.go +++ b/cmd/launch.go @@ -45,6 +45,8 @@ func runLaunch(cli string) error { if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { agentCfg.Image = envImage + } else if agentCfg.Image == "" { + agentCfg.Image = defaultSandboxImage() } fmt.Println("=== Sandbox Runner ===") diff --git a/cmd/up.go b/cmd/up.go index a83e824..cd83cfe 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -136,9 +136,11 @@ func upRemote(harnessDir string, gwCfg *gateway.GatewayConfig, gw gateway.Gatewa } } - // Resolve sandbox image + // Resolve sandbox image: agent config > SANDBOX_IMAGE env > version default sandboxImage := agentCfg.Image - if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { + if sandboxImage == "" { + sandboxImage = defaultSandboxImage() + } else if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { sandboxImage = envImage } @@ -161,13 +163,15 @@ func upRemote(harnessDir string, gwCfg *gateway.GatewayConfig, gw gateway.Gatewa kc.RunKubectl(ctx, "delete", "job", jobName, "--grace-period=30") kc.RunKubectl(ctx, "delete", "pod", "-l", "job-name="+jobName, "--grace-period=30") - // 6. Apply runner Job + // 6. Apply runner Job (gwCfg provides defaults + RUNNER_IMAGE env override) runnerImage := defaultRunnerImage() runnerSA := "openshell-launcher" gatewayEndpoint := "https://openshell.openshell.svc.cluster.local:8080" mtlsSecret := "openshell-client-tls" if gwCfg != nil { - runnerImage = gwCfg.Images.Runner + if gwCfg.Images.Runner != "" { + runnerImage = gwCfg.Images.Runner + } runnerSA = gwCfg.Launcher.ServiceAccount gatewayEndpoint = gwCfg.Launcher.GatewayEndpoint mtlsSecret = gwCfg.Secrets.MTLS @@ -312,7 +316,9 @@ func upLocal(opts upLocalOpts) error { noTTY := opts.noTTY || agentCfg.NoTTY() sandboxImage := agentCfg.Image - if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { + if sandboxImage == "" { + sandboxImage = defaultSandboxImage() + } else if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { sandboxImage = envImage } @@ -355,15 +361,26 @@ func upLocal(opts upLocalOpts) error { var Version = "dev" +func defaultSandboxImage() string { + if v := os.Getenv("SANDBOX_IMAGE"); v != "" { + return v + } + return versionedImage("sandbox") +} + func defaultRunnerImage() string { if v := os.Getenv("RUNNER_IMAGE"); v != "" { return v } + return versionedImage("runner") +} + +func versionedImage(name string) string { base := "ghcr.io/robbycochran/harness-openshell" - if Version != "dev" && Version != "" { - return base + ":runner-v" + Version + if Version == "" || Version == "dev" { + return base + ":" + name } - return base + ":runner" + return base + ":" + name + "-" + Version } func runnerEnv(gatewayEndpoint, sandboxImage string) []map[string]any { diff --git a/docs/design.md b/docs/design.md index 1362e53..7d25ae6 100644 --- a/docs/design.md +++ b/docs/design.md @@ -149,7 +149,7 @@ Three files, one per domain. No duplication. # Gateway deployment settings. [upstream] -chart-version = "0.0.55" +chart-version = "0.0.58" # Optional overrides (also settable via env vars): # namespace = "openshell" # HARNESS_NAMESPACE diff --git a/docs/proto-migration.md b/docs/proto-migration.md index 404ff51..109b8e1 100644 --- a/docs/proto-migration.md +++ b/docs/proto-migration.md @@ -115,7 +115,7 @@ detection (`.textproto` → `prototext`, `.json` → `protojson`). ### Proto stability -OpenShell is alpha (v0.0.55). The proto could change. However: +OpenShell is alpha (v0.0.58). The proto could change. However: - `SandboxSpec` core fields (`image`, `environment`, `providers`) are stable — they map directly to the CLI's `sandbox create` flags which have been diff --git a/gateways/kind/gateway.toml b/gateways/kind/gateway.toml index 03dbb7b..8a2b850 100644 --- a/gateways/kind/gateway.toml +++ b/gateways/kind/gateway.toml @@ -21,7 +21,7 @@ enabled = ["github", "vertex-local", "atlassian", "gws"] [chart] oci = "oci://ghcr.io/nvidia/openshell/helm-chart" -version = "0.0.55" +version = "0.0.58" [chart.crd] url = "https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml" diff --git a/gateways/ocp/gateway.toml b/gateways/ocp/gateway.toml index 0106b23..7c9968d 100644 --- a/gateways/ocp/gateway.toml +++ b/gateways/ocp/gateway.toml @@ -21,7 +21,7 @@ enabled = ["github", "vertex-local", "atlassian", "gws"] [chart] oci = "oci://ghcr.io/nvidia/openshell/helm-chart" -version = "0.0.55" +version = "0.0.58" [chart.crd] url = "https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml" @@ -31,9 +31,6 @@ values = "values.yaml" [addons] manifests = ["addons/rbac.yaml", "addons/route.yaml"] -[images] -runner = "ghcr.io/robbycochran/harness-openshell:runner" - [ocp] scc-privileged = ["openshell", "openshell-sandbox"] scc-anyuid = ["openshell"] diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 47c8f86..5f7d73b 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -36,7 +36,7 @@ func (c *AgentConfig) NoTTY() bool { func (c *AgentConfig) EffectiveEntrypoint() string { if c.Entrypoint == "" { - return "claude --bare" + return "claude" } return c.Entrypoint } diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 5003bb2..086f957 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -115,8 +115,8 @@ func TestProviderNames(t *testing.T) { func TestEffectiveEntrypoint(t *testing.T) { cfg := &AgentConfig{} - if ep := cfg.EffectiveEntrypoint(); ep != "claude --bare" { - t.Errorf("default = %q, want 'claude --bare'", ep) + if ep := cfg.EffectiveEntrypoint(); ep != "claude" { + t.Errorf("default = %q, want 'claude'", ep) } cfg.Entrypoint = "codex" if ep := cfg.EffectiveEntrypoint(); ep != "codex" { diff --git a/internal/gateway/cli_test.go b/internal/gateway/cli_test.go index 13b6214..dd68a4a 100644 --- a/internal/gateway/cli_test.go +++ b/internal/gateway/cli_test.go @@ -281,11 +281,11 @@ echo "Model: claude-sonnet-4-6" func TestCLIVersion(t *testing.T) { bin := writeStub(t, `#!/bin/bash -echo "openshell v0.0.55" +echo "openshell v0.0.58" `) gw := New(bin) ver := gw.CLIVersion() - if ver != "openshell v0.0.55" { + if ver != "openshell v0.0.58" { t.Errorf("CLIVersion = %q", ver) } } diff --git a/internal/gateway/config.go b/internal/gateway/config.go index f67ad6d..30bf91a 100644 --- a/internal/gateway/config.go +++ b/internal/gateway/config.go @@ -93,9 +93,7 @@ func (c *GatewayConfig) applyDefaults() { if c.Chart.CRD.URL == "" { c.Chart.CRD.URL = "https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml" } - if c.Images.Runner == "" { - c.Images.Runner = "ghcr.io/robbycochran/harness-openshell:runner" - } + // Runner image default left empty -- resolved by defaultRunnerImage() in cmd/up.go if c.Secrets.MTLS == "" { c.Secrets.MTLS = "openshell-client-tls" } diff --git a/internal/preflight/preflight_test.go b/internal/preflight/preflight_test.go index 3c3c907..161d48a 100644 --- a/internal/preflight/preflight_test.go +++ b/internal/preflight/preflight_test.go @@ -74,7 +74,7 @@ providers = ["github", "vertex-local"] providers-custom = ["gws"] [upstream] -chart-version = "0.0.55" +chart-version = "0.0.58" `), 0o644) cfg, err := LoadConfig(path) @@ -90,7 +90,7 @@ chart-version = "0.0.55" if len(cfg.ProvidersCustom) != 1 || cfg.ProvidersCustom[0] != "gws" { t.Errorf("providers-custom = %v", cfg.ProvidersCustom) } - if cfg.Upstream.ChartVersion != "0.0.55" { + if cfg.Upstream.ChartVersion != "0.0.58" { t.Errorf("Upstream.ChartVersion = %q", cfg.Upstream.ChartVersion) } } diff --git a/internal/profile/profile.go b/internal/profile/profile.go index e990b60..04f8a67 100644 --- a/internal/profile/profile.go +++ b/internal/profile/profile.go @@ -64,7 +64,7 @@ func ParseFile(path string) (*Config, error) { cfg.Name = "agent" } if cfg.Command == "" { - cfg.Command = "claude --bare" + cfg.Command = "claude" } return &cfg, nil } diff --git a/internal/profile/profile_test.go b/internal/profile/profile_test.go index 894ae3e..8c4c380 100644 --- a/internal/profile/profile_test.go +++ b/internal/profile/profile_test.go @@ -71,7 +71,7 @@ func TestParseFile_Defaults(t *testing.T) { if cfg.Name != "agent" { t.Errorf("Name = %q, want default 'agent'", cfg.Name) } - if cfg.Command != "claude --bare" { + if cfg.Command != "claude" { t.Errorf("Command = %q, want default", cfg.Command) } if !cfg.KeepSandbox() { diff --git a/openshell.toml b/openshell.toml index 32e83f8..0531f96 100644 --- a/openshell.toml +++ b/openshell.toml @@ -19,4 +19,4 @@ providers = ["github", "vertex-local", "atlassian", "gws"] model = "claude-sonnet-4-6" [upstream] -chart-version = "0.0.55" +chart-version = "0.0.58" diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index 6c3af87..4f75f2a 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -64,16 +64,24 @@ COPY policy.yaml /etc/openshell/policy.yaml # Agent instructions + Claude config COPY CLAUDE.md /sandbox/CLAUDE.md COPY settings.json /sandbox/.claude/settings.json +COPY claude.json /sandbox/.claude.json COPY mcp.json /sandbox/.mcp.json # Runtime script COPY startup.sh /sandbox/startup.sh +# Claude wrapper: move the real binary aside, install wrapper in its place. +# The base image puts claude at /usr/local/bin/claude; we move it to +# /usr/local/lib/claude-real so the wrapper can call it. +RUN mv /usr/local/bin/claude /usr/local/lib/claude-real +COPY bin/claude /usr/local/bin/claude + # Harness config directory (populated at runtime via upload) RUN mkdir -p /sandbox/.config/openshell && chown sandbox:sandbox /sandbox/.config/openshell -RUN chmod +x /sandbox/startup.sh && \ - chown -R sandbox:sandbox /sandbox/.claude /sandbox/.mcp.json /sandbox/.config /sandbox/CLAUDE.md /sandbox/startup.sh +RUN chmod 755 /usr/local/bin/claude && \ + chmod +x /sandbox/startup.sh && \ + chown -R sandbox:sandbox /sandbox/.claude /sandbox/.claude.json /sandbox/.mcp.json /sandbox/.config /sandbox/CLAUDE.md /sandbox/startup.sh USER sandbox diff --git a/sandbox/bin/claude b/sandbox/bin/claude new file mode 100755 index 0000000..3605a94 --- /dev/null +++ b/sandbox/bin/claude @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# Wrapper that sets sandbox defaults so Claude Code starts clean. +exec /usr/local/lib/claude-real --bare --dangerously-skip-permissions "$@" diff --git a/sandbox/claude.json b/sandbox/claude.json new file mode 100644 index 0000000..5af008a --- /dev/null +++ b/sandbox/claude.json @@ -0,0 +1,14 @@ +{ + "hasCompletedOnboarding": true, + "numStartups": 1, + "autoUpdates": false, + "customApiKeyResponses": { + "approved": ["nshell-proxy-managed"] + }, + "projects": { + "/sandbox": { + "hasTrustDialogAccepted": true, + "allowedTools": [] + } + } +} diff --git a/sandbox/settings.json b/sandbox/settings.json index f4ae432..aec0d85 100644 --- a/sandbox/settings.json +++ b/sandbox/settings.json @@ -4,5 +4,10 @@ "allow": ["Bash(*)", "Read(*)", "Write(*)", "Edit(*)", "Glob(*)", "Grep(*)", "WebFetch(*)", "WebSearch(*)", "mcp__*"], "deny": [] }, - "trustedMcpServers": ["atlassian"] + "enableAllProjectMcpServers": true, + "trustedMcpServers": ["atlassian"], + "skipDangerousModePermissionPrompt": true, + "env": { + "CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1" + } } diff --git a/test/kind-lifecycle.sh b/test/kind-lifecycle.sh index 0b5c26d..3449b03 100755 --- a/test/kind-lifecycle.sh +++ b/test/kind-lifecycle.sh @@ -4,9 +4,10 @@ # Creates a kind cluster with an isolated kubeconfig (never touches your # OCP/cloud kubectl context), runs the test flow, then tears down. # +# CI mode auto-detects from the CI env var. Override with --ci. +# # Usage: -# ./test/kind-lifecycle.sh # default mode (full, with creds) -# ./test/kind-lifecycle.sh --ci # ci mode (no creds) +# ./test/kind-lifecycle.sh # full test with credentials # ./test/kind-lifecycle.sh --keep # don't delete cluster after tests # # Works alongside any existing KUBECONFIG. The cluster gets its own temp @@ -17,7 +18,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" CLUSTER_NAME="openshell-test" KIND_KUBECONFIG="" KEEP_CLUSTER=false -TEST_ARGS=("kind" "--full") +TEST_ARGS=("kind") for arg in "$@"; do case "$arg" in diff --git a/test/test-flow.sh b/test/test-flow.sh index ca47a4d..ffbbb3e 100755 --- a/test/test-flow.sh +++ b/test/test-flow.sh @@ -1,21 +1,15 @@ #!/usr/bin/env bash -# End-to-end validation. Validation mode (default/ci) is independent of -# the gateway target (local/kind/ocp). +# End-to-end validation for harness CLI. # -# Validation modes: -# default Expects user credentials (GITHUB_TOKEN, JIRA_API_TOKEN, gcloud ADC, -# gws auth). Tests the full provider chain including GWS token lifecycle. -# ci No credentials required. Validates gateway deploy + sandbox lifecycle -# only. Suitable for GitHub Actions. +# CI mode auto-detects from the CI env var (set by GitHub Actions). +# Override with --ci or --no-providers. # # Usage: -# ./test-flow.sh local # default mode, local gateway -# ./test-flow.sh local --ci # ci mode, local gateway -# ./test-flow.sh kind # default mode, kind cluster -# ./test-flow.sh kind --ci # ci mode, kind cluster (used in GHA) -# ./test-flow.sh ocp [--ci] # OCP variants -# ./test-flow.sh ocp --reuse-gateway # skip deploy/teardown (~50s vs ~130s) -# ./test-flow.sh all [--ci] # all gateways +# ./test-flow.sh local # full test with credentials +# ./test-flow.sh kind # full test on kind cluster +# ./test-flow.sh ocp # full test on OCP +# ./test-flow.sh ocp --reuse-gateway # skip deploy/teardown +# ./test-flow.sh all # all gateways set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" @@ -30,25 +24,29 @@ fi # ── Parse args ────────────────────────────────────────────────────── TARGET="" -FULL=false REUSE_GATEWAY=false NO_PROVIDERS=false PROFILE="default" +# Auto-detect CI mode +if [[ "${CI:-}" == "true" ]]; then + NO_PROVIDERS=true + PROFILE="ci" +fi + for arg in "$@"; do case "$arg" in - --ci) NO_PROVIDERS=true; PROFILE="ci"; FULL=true ;; - --full) FULL=true ;; + --ci) NO_PROVIDERS=true; PROFILE="ci" ;; --reuse-gateway) REUSE_GATEWAY=true ;; --no-providers) NO_PROVIDERS=true ;; - --agent=*) PROFILE="${arg#--agent=}" ;; + --agent=*) PROFILE="${arg#--agent=}" ;; -*) ;; *) [[ -z "$TARGET" ]] && TARGET="$arg" ;; esac done if [[ -z "$TARGET" ]]; then - echo "Usage: $0 [--ci] [--full] [--reuse-gateway]" + echo "Usage: $0 [--ci] [--reuse-gateway]" exit 1 fi @@ -120,15 +118,12 @@ check_providers() { } sandbox_wait() { - # Poll for sandbox Ready, failing fast on k8s bad states (ImagePullBackOff, etc.) - # 60 iterations * 2s = 120s timeout (kind needs extra time for cold image pulls) local name="$1" for i in $(seq 1 60); do local phase phase=$("$CLI" sandbox list 2>/dev/null | strip_ansi | awk -v n="$name" '$1==n {print $NF}') [[ "$phase" == "Ready" ]] && return 0 - # On k8s targets, check for pod bad states so we fail fast instead of timing out if kubectl get pods -n openshell 2>/dev/null | grep -q "ImagePullBackOff\|ErrImagePull\|CrashLoopBackOff"; then local bad bad=$(kubectl get pods -n openshell 2>/dev/null | grep "ImagePullBackOff\|ErrImagePull\|CrashLoopBackOff" | awk '{print $1, $3}' | head -3) @@ -158,7 +153,6 @@ sandbox_verify() { printf " ✓ %-35s\n" "sandbox ready" ((PASS++)) - # Basic exec works (brief wait for SSH readiness after Ready state) sleep 2 step "sandbox: exec" "$CLI" sandbox exec --name "$name" -- echo "hello" @@ -166,7 +160,6 @@ sandbox_verify() { return fi - # Provider-dependent checks (require credentials + inference) step "sandbox: env vars" "$CLI" sandbox exec --name "$name" -- bash -c 'test -n "$ANTHROPIC_BASE_URL"' step "sandbox: gws token placeholder" "$CLI" sandbox exec --name "$name" -- bash -c 'echo "$GOOGLE_WORKSPACE_CLI_TOKEN" | grep -q "openshell:resolve:env"' step "sandbox: gws api call" "$CLI" sandbox exec --name "$name" -- bash -c 'for i in 1 2 3; do curl -sf https://gmail.googleapis.com/gmail/v1/users/me/profile -H "Authorization: Bearer $GOOGLE_WORKSPACE_CLI_TOKEN" -o /dev/null && exit 0; sleep 3; done; exit 1' @@ -190,10 +183,8 @@ summary() { test_errors() { echo "=== test: error scenarios ===" - # Bad profile step_fail "nonexistent profile" "$HARNESS" up --local --agent nonexistent --no-tty - # Teardown idempotency (skip k8s teardown when reusing gateway) if $REUSE_GATEWAY; then step "teardown (first)" "$HARNESS" teardown --sandboxes --providers step "teardown (second)" "$HARNESS" teardown --sandboxes --providers @@ -208,8 +199,7 @@ test_errors() { # ── Local flow ─────────────────────────────────────────────────────── test_local() { - local mode="quick" - $FULL && mode="full" + local mode="full" $NO_PROVIDERS && mode="$mode, no-providers" echo "=== test-flow: local ($mode) ===" @@ -224,26 +214,22 @@ test_local() { step "gateway reachable" "$HARNESS" deploy --local fi - if $FULL; then - local sandbox_name="test-agent" - step_output "sandbox create (up)" "$HARNESS" up --local --name "$sandbox_name" --agent "$PROFILE" --no-tty - sandbox_verify "$sandbox_name" - step "sandbox delete" "$CLI" sandbox delete "$sandbox_name" - - # Test harness create (non-interactive sandbox creation without deploy/providers) - local create_name="test-create" - step_output "sandbox create (create)" "$HARNESS" create --name "$create_name" --agent "$PROFILE" - step "sandbox verify (create)" "$CLI" sandbox exec --name "$create_name" -- echo "hello" - step "sandbox delete (create)" "$CLI" sandbox delete "$create_name" - - if ! $NO_PROVIDERS; then - # Missing providers scenario - echo "" - echo "=== test: missing providers ===" - step "teardown providers" "$HARNESS" teardown --providers - step_output "up with no providers" "$HARNESS" up --local --name test-noprov --no-tty - step "cleanup" "$HARNESS" teardown --sandboxes - fi + local sandbox_name="test-agent" + step_output "sandbox create (up)" "$HARNESS" up --local --name "$sandbox_name" --agent "$PROFILE" --no-tty + sandbox_verify "$sandbox_name" + step "sandbox delete" "$CLI" sandbox delete "$sandbox_name" + + local create_name="test-create" + step_output "sandbox create (create)" "$HARNESS" create --name "$create_name" --agent ci + step "sandbox verify (create)" "$CLI" sandbox exec --name "$create_name" -- echo "hello" + step "sandbox delete (create)" "$CLI" sandbox delete "$create_name" + + if ! $NO_PROVIDERS; then + echo "" + echo "=== test: missing providers ===" + step "teardown providers" "$HARNESS" teardown --providers + step_output "up with no providers" "$HARNESS" up --local --name test-noprov --no-tty + step "cleanup" "$HARNESS" teardown --sandboxes fi step "teardown (clean)" "$HARNESS" teardown --sandboxes --providers @@ -255,17 +241,13 @@ test_gws() { local sandbox_name="$1" echo "=== test: GWS token lifecycle ===" - # Token is a proxy placeholder, never a real token step "gws: token is placeholder" \ "$CLI" sandbox exec --name "$sandbox_name" -- bash -c \ 'echo "$GOOGLE_WORKSPACE_CLI_TOKEN" | grep -q "openshell:resolve:env"' - # Real API call works through proxy (token resolved on the wire) - # Note: sandbox exec rejects newlines in args — keep curl on one line. step "gws: Gmail API via proxy" \ "$CLI" sandbox exec --name "$sandbox_name" -- bash -c 'curl -sf https://gmail.googleapis.com/gmail/v1/users/me/profile -H "Authorization: Bearer $GOOGLE_WORKSPACE_CLI_TOKEN" -o /dev/null' - # Force gateway token rotation, verify sandbox still works openshell provider refresh rotate gws \ --credential-key GOOGLE_WORKSPACE_CLI_TOKEN &>/dev/null step "gws: works after rotation" \ @@ -277,11 +259,10 @@ test_gws() { # ── kind flow ──────────────────────────────────────────────────────── test_kind() { - local mode="quick" - $FULL && mode="full" + local mode="full" + $NO_PROVIDERS && mode="$mode, no-providers" echo "=== test-flow: kind ($mode) ===" - # Verify kind cluster is up if ! kubectl get nodes &>/dev/null; then echo " ERROR: no kind cluster — run: kind create cluster --name openshell" ((FAIL++)) @@ -297,18 +278,16 @@ test_kind() { check_providers fi - if $FULL; then - local sandbox_name="test-kind" - step_output "sandbox create" "$HARNESS" up --name "$sandbox_name" --agent "$PROFILE" --no-tty - sandbox_verify "$sandbox_name" - - if ! $NO_PROVIDERS; then - test_gws "$sandbox_name" - fi + local sandbox_name="test-kind" + step_output "sandbox create" "$HARNESS" up --name "$sandbox_name" --agent "$PROFILE" --no-tty + sandbox_verify "$sandbox_name" - step "sandbox delete" "$CLI" sandbox delete "$sandbox_name" + if ! $NO_PROVIDERS; then + test_gws "$sandbox_name" fi + step "sandbox delete" "$CLI" sandbox delete "$sandbox_name" + step "teardown (clean)" "$HARNESS" teardown --sandboxes --providers --k8s echo "" } @@ -316,18 +295,15 @@ test_kind() { # ── OCP flow ───────────────────────────────────────────────────────── test_ocp() { - local mode="quick" - $FULL && mode="full" + local mode="full" $REUSE_GATEWAY && mode="$mode, reuse-gateway" echo "=== test-flow: ocp ($mode) ===" if $REUSE_GATEWAY; then - # Ensure OCP gateway is selected (error scenarios may have switched to local) OCP_GW=$("$CLI" gateway list 2>/dev/null | strip_ansi | awk '/-remote-/ {gsub(/^\*/, ""); print $1; exit}') [[ -n "$OCP_GW" ]] && "$CLI" gateway select "$OCP_GW" 2>/dev/null || true step "teardown sandboxes+providers" "$HARNESS" teardown --sandboxes --providers - # Deploy only if gateway is not reachable if ! "$CLI" inference get &>/dev/null; then step "deploy" "$HARNESS" deploy --remote else @@ -344,22 +320,18 @@ test_ocp() { check_providers fi - if $FULL; then - local sandbox_name - if $NO_PROVIDERS; then - # ci mode: use harness create (skips provider registration) with public ci profile - sandbox_name="test-ocp" - step_output "sandbox create" "$HARNESS" create --agent=ci --name "$sandbox_name" - else - # default mode: full up (deploy already done above, providers registered) - sandbox_name="agent" - step_output "sandbox create (up)" "$HARNESS" up --remote --name "$sandbox_name" --no-tty - fi - - sandbox_verify "$sandbox_name" - step "sandbox delete" "$CLI" sandbox delete "$sandbox_name" + local sandbox_name + if $NO_PROVIDERS; then + sandbox_name="test-ocp" + step_output "sandbox create" "$HARNESS" create --agent=ci --name "$sandbox_name" + else + sandbox_name="agent" + step_output "sandbox create (up)" "$HARNESS" up --remote --name "$sandbox_name" --no-tty fi + sandbox_verify "$sandbox_name" + step "sandbox delete" "$CLI" sandbox delete "$sandbox_name" + if $REUSE_GATEWAY; then step "teardown (sandboxes+providers)" "$HARNESS" teardown --sandboxes --providers else @@ -378,7 +350,7 @@ case "$TARGET" in all) test_local; echo ""; test_kind; echo ""; test_ocp ;; *) echo "Unknown target: $TARGET" - echo "Usage: $0 [--full]" + echo "Usage: $0 [--ci] [--reuse-gateway]" exit 1 ;; esac