From 0aef927bb6b98ca957904b21be98d27516a3ce27 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 10:31:39 -0700 Subject: [PATCH 01/20] fix: add claude wrapper to skip sandbox startup modals Claude Code shows ~7 interactive first-run prompts (theme, permissions, trust) when launching inside a fresh container. Add a wrapper script at /usr/local/bin/claude that passes --bare --dangerously-skip-permissions so the sandbox starts clean with no modals. Agent configs simplified to just `entrypoint: claude` since the wrapper handles the sandbox-specific flags. --- agents/default.yaml | 2 +- agents/demo.yaml | 2 +- internal/agent/agent.go | 2 +- internal/agent/agent_test.go | 4 ++-- internal/profile/profile.go | 2 +- internal/profile/profile_test.go | 2 +- sandbox/Dockerfile | 6 +++++- sandbox/bin/claude | 3 +++ 8 files changed, 15 insertions(+), 8 deletions(-) create mode 100755 sandbox/bin/claude diff --git a/agents/default.yaml b/agents/default.yaml index 627d9d5..02f33c8 100644 --- a/agents/default.yaml +++ b/agents/default.yaml @@ -6,7 +6,7 @@ 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..a29c9ed 100644 --- a/agents/demo.yaml +++ b/agents/demo.yaml @@ -5,7 +5,7 @@ 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/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/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/sandbox/Dockerfile b/sandbox/Dockerfile index 6c3af87..742864c 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -69,10 +69,14 @@ COPY mcp.json /sandbox/.mcp.json # Runtime script COPY startup.sh /sandbox/startup.sh +# Claude wrapper: sets --bare --dangerously-skip-permissions so the sandbox +# starts clean with no interactive modals. +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 && \ +RUN chmod +x /sandbox/startup.sh /usr/local/bin/claude && \ chown -R sandbox:sandbox /sandbox/.claude /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..6f2e518 --- /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 /sandbox/.claude-code/bin/claude --bare --dangerously-skip-permissions "$@" From 0b72832f389e957ade29ace632fcba770f9eccd7 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 11:11:14 -0700 Subject: [PATCH 02/20] fix: pre-seed claude.json onboarding state to skip all startup modals The sandbox container starts Claude Code fresh each time, triggering the full onboarding flow (theme picker, workspace trust, permission mode, etc). Pre-seed ~/.claude.json with hasCompletedOnboarding=true and autoUpdates=false so it starts clean. Also add enableAllProjectMcpServers and disable spinner tips, feedback surveys, and company announcements in settings.json. --- sandbox/Dockerfile | 3 ++- sandbox/claude.json | 5 +++++ sandbox/settings.json | 8 +++++++- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 sandbox/claude.json diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index 742864c..ac29412 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -64,6 +64,7 @@ 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 @@ -77,7 +78,7 @@ COPY bin/claude /usr/local/bin/claude RUN mkdir -p /sandbox/.config/openshell && chown sandbox:sandbox /sandbox/.config/openshell RUN chmod +x /sandbox/startup.sh /usr/local/bin/claude && \ - chown -R sandbox:sandbox /sandbox/.claude /sandbox/.mcp.json /sandbox/.config /sandbox/CLAUDE.md /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/claude.json b/sandbox/claude.json new file mode 100644 index 0000000..adb5403 --- /dev/null +++ b/sandbox/claude.json @@ -0,0 +1,5 @@ +{ + "hasCompletedOnboarding": true, + "numStartups": 1, + "autoUpdates": false +} diff --git a/sandbox/settings.json b/sandbox/settings.json index f4ae432..ad289be 100644 --- a/sandbox/settings.json +++ b/sandbox/settings.json @@ -4,5 +4,11 @@ "allow": ["Bash(*)", "Read(*)", "Write(*)", "Edit(*)", "Glob(*)", "Grep(*)", "WebFetch(*)", "WebSearch(*)", "mcp__*"], "deny": [] }, - "trustedMcpServers": ["atlassian"] + "enableAllProjectMcpServers": true, + "trustedMcpServers": ["atlassian"], + "spinnerTipsEnabled": false, + "companyAnnouncements": false, + "env": { + "CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1" + } } From 3bbd22a65fc2d17079bc7f2b9f84a6ffe68b0868 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 11:17:24 -0700 Subject: [PATCH 03/20] feat: stamp CLI version from git tag in dev builds make cli and make cli-runner now pass -ldflags with git describe so the binary reports the actual version (e.g. v0.1.2-2-g0b72832) instead of "dev". This also fixes defaultRunnerImage() which uses Version to resolve the correct runner image tag. --- Makefile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 024fc62..265344a 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,8 @@ REGISTRY ?= ghcr.io/robbycochran/harness-openshell DEV_REGISTRY ?= quay.io/rcochran/openshell DEV_TAG := dev-$(shell git rev-parse --short HEAD) 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 @@ -34,8 +36,8 @@ DEV_RUNNER_IMAGE := $(DEV_REGISTRY):$(DEV_TAG)-runner ## 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 ──────────────────────────────────────────────────────────── @@ -50,8 +52,8 @@ push-sandbox: sandbox ## 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 From a92ed86c67cb0ac3054cbf426a048dc1aeb49731 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 11:26:51 -0700 Subject: [PATCH 04/20] feat: single registry, PR image builds, version-stamped runner - Drop quay.io dev registry, use ghcr.io for everything - Push images on PRs (tagged :sandbox-pr-N, :runner-pr-N) - Remove path filter so every PR builds images - Add ldflags to CI runner binary build for version stamp - Fix double-v prefix in defaultRunnerImage (v0.1.2 not vv0.1.2) --- .github/workflows/images.yml | 13 +++++++------ Makefile | 16 +++++++--------- cmd/up.go | 3 ++- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index ce2c873..5c5cbb8 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 @@ -41,7 +38,7 @@ jobs: 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: | @@ -57,7 +54,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: @@ -76,7 +77,7 @@ jobs: 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: | diff --git a/Makefile b/Makefile index 265344a..f5cd267 100644 --- a/Makefile +++ b/Makefile @@ -16,16 +16,15 @@ ## 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) PLATFORM := linux/amd64 VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev) LDFLAGS := -s -w -X main.version=$(VERSION) +DEV_TAG := dev-$(shell git rev-parse --short HEAD) -SANDBOX_IMAGE := $(REGISTRY):sandbox -RUNNER_IMAGE := $(REGISTRY):runner -DEV_SANDBOX_IMAGE := $(DEV_REGISTRY):$(DEV_TAG)-sandbox -DEV_RUNNER_IMAGE := $(DEV_REGISTRY):$(DEV_TAG)-runner +SANDBOX_IMAGE := $(REGISTRY):sandbox +RUNNER_IMAGE := $(REGISTRY):runner +DEV_SANDBOX_IMAGE := $(REGISTRY):$(DEV_TAG)-sandbox +DEV_RUNNER_IMAGE := $(REGISTRY):$(DEV_TAG)-runner .PHONY: cli sandbox push-sandbox cli-runner runner push-runner \ vet lint ci ci-local ci-kind \ @@ -116,7 +115,6 @@ dev-test-kind: cli ci ## 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 @test -n "$${KUBECONFIG}" || { echo "ERROR: Set KUBECONFIG for OCP (e.g. export KUBECONFIG=infracluster/kubeconfig)"; exit 1; } @echo "" @@ -131,12 +129,12 @@ dev-test-all: dev-test-local dev-test-kind dev-test-remote ## ── Dev image builds ───────────────────────────────────────────────── -## Build dev sandbox image to quay.io (multi-arch) +## Build dev sandbox image to ghcr.io dev-sandbox: docker buildx build --platform linux/amd64,linux/arm64 -t $(DEV_SANDBOX_IMAGE) sandbox/ --push @echo "Built and pushed: $(DEV_SANDBOX_IMAGE)" -## Build dev runner image to quay.io +## Build dev runner image to ghcr.io dev-runner: cli-runner docker build --platform $(PLATFORM) -t $(DEV_RUNNER_IMAGE) build/runner/ docker push $(DEV_RUNNER_IMAGE) diff --git a/cmd/up.go b/cmd/up.go index a83e824..7e55e33 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -361,7 +361,8 @@ func defaultRunnerImage() string { } base := "ghcr.io/robbycochran/harness-openshell" if Version != "dev" && Version != "" { - return base + ":runner-v" + Version + v := strings.TrimPrefix(Version, "v") + return base + ":runner-v" + v } return base + ":runner" } From 1113d8d803b12835215a46315357157f29817b50 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 11:34:27 -0700 Subject: [PATCH 05/20] feat: version-matched sandbox images, drop hardcoded image from agents Remove hardcoded image tag from agent configs. The CLI now resolves the sandbox image from the binary version (e.g. :sandbox-v0.1.2 for a tagged release, :sandbox for dev). SANDBOX_IMAGE env still overrides. This means a dev build from a PR branch will pull the matching PR-tagged image automatically. --- SPEC.md | 7 +++---- agents/default.yaml | 1 - agents/demo.yaml | 1 - cmd/create.go | 4 +++- cmd/launch.go | 2 ++ cmd/up.go | 25 ++++++++++++++++++++----- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/SPEC.md b/SPEC.md index 49fdd35..17624b2 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 diff --git a/agents/default.yaml b/agents/default.yaml index 02f33c8..ce46633 100644 --- a/agents/default.yaml +++ b/agents/default.yaml @@ -5,7 +5,6 @@ # harness up --agent research # uses agents/research.yaml name: agent -image: ghcr.io/robbycochran/harness-openshell:sandbox entrypoint: claude tty: true diff --git a/agents/demo.yaml b/agents/demo.yaml index a29c9ed..f021aa1 100644 --- a/agents/demo.yaml +++ b/agents/demo.yaml @@ -4,7 +4,6 @@ # harness up --local --agent demo name: demo -image: ghcr.io/robbycochran/harness-openshell:sandbox entrypoint: claude -p task: demo/DEMO-TASK.md tty: false diff --git a/cmd/create.go b/cmd/create.go index cf13652..00ea032 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -127,7 +127,9 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command { } sandboxImage := agentCfg.Image - if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { + if sandboxImage == "" { + sandboxImage = defaultSandboxImage() + } else if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { sandboxImage = envImage } 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 7e55e33..6c6269c 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 } @@ -312,7 +314,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,16 +359,27 @@ 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 != "" { v := strings.TrimPrefix(Version, "v") - return base + ":runner-v" + v + return base + ":" + name + "-v" + v } - return base + ":runner" + return base + ":" + name } func runnerEnv(gatewayEndpoint, sandboxImage string) []map[string]any { From 49138ebd79860897d56bc1dadf37a94cc61a85dd Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 11:49:34 -0700 Subject: [PATCH 06/20] fix: dev builds fall back to floating image tags versionedImage() now only pins to :sandbox-v0.1.2 for clean semver tags. Dev builds (v0.1.2-5-g1113d8d) fall back to :sandbox/:runner floating tags since no matching versioned image exists. Also fix create command printing blank image when agent config omits the image field -- resolve before printing. --- cmd/create.go | 16 ++++++++-------- cmd/up.go | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index 00ea032..d9871f2 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") @@ -126,13 +133,6 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command { return fmt.Errorf("rendering payload: %w", err) } - sandboxImage := agentCfg.Image - if sandboxImage == "" { - sandboxImage = defaultSandboxImage() - } else if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" { - sandboxImage = envImage - } - cfg := &profile.Config{ Name: name, From: sandboxImage, diff --git a/cmd/up.go b/cmd/up.go index 6c6269c..12c7351 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -375,8 +375,8 @@ func defaultRunnerImage() string { func versionedImage(name string) string { base := "ghcr.io/robbycochran/harness-openshell" - if Version != "dev" && Version != "" { - v := strings.TrimPrefix(Version, "v") + v := strings.TrimPrefix(Version, "v") + if v != "dev" && v != "" && !strings.Contains(v, "-") { return base + ":" + name + "-v" + v } return base + ":" + name From 89387cf0ddfd17cda1ed8e5f7330613a6fafc3d7 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 11:58:14 -0700 Subject: [PATCH 07/20] fix: create command skips preflight for registered providers, fix test create only runs preflight checks for providers that are NOT already registered on the gateway. Previously it checked all providers locally, failing when credentials weren't in the shell env even though the gateway already had them. Also fix test-flow.sh: create test now uses ci profile (no providers) instead of default profile, since create doesn't auto-register. --- cmd/create.go | 16 ++++++++-------- test/test-flow.sh | 6 ++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index d9871f2..f1f8e3c 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -78,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) @@ -108,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) diff --git a/test/test-flow.sh b/test/test-flow.sh index ca47a4d..41252f2 100755 --- a/test/test-flow.sh +++ b/test/test-flow.sh @@ -230,9 +230,11 @@ test_local() { sandbox_verify "$sandbox_name" step "sandbox delete" "$CLI" sandbox delete "$sandbox_name" - # Test harness create (non-interactive sandbox creation without deploy/providers) + # Test harness create (non-interactive sandbox creation without deploy/providers). + # create requires providers to already be registered — use ci profile which + # has no providers, so no registration needed. local create_name="test-create" - step_output "sandbox create (create)" "$HARNESS" create --name "$create_name" --agent "$PROFILE" + 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" From 549383b6830f6a51788698b47fec6dcddcd3fff0 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 12:21:08 -0700 Subject: [PATCH 08/20] fix: wrapper points to correct binary path, fix permissions The base image installs claude at /usr/local/bin/claude. Move it to /usr/local/lib/claude-real before installing the wrapper, so the wrapper can exec the real binary without /root permission issues. Also chmod 755 the wrapper so the sandbox user (uid 1000) can read and execute it. --- sandbox/Dockerfile | 9 ++++++--- sandbox/bin/claude | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index ac29412..4f75f2a 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -70,14 +70,17 @@ COPY mcp.json /sandbox/.mcp.json # Runtime script COPY startup.sh /sandbox/startup.sh -# Claude wrapper: sets --bare --dangerously-skip-permissions so the sandbox -# starts clean with no interactive modals. +# 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 /usr/local/bin/claude && \ +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 index 6f2e518..3605a94 100755 --- a/sandbox/bin/claude +++ b/sandbox/bin/claude @@ -1,3 +1,3 @@ #!/usr/bin/env bash # Wrapper that sets sandbox defaults so Claude Code starts clean. -exec /sandbox/.claude-code/bin/claude --bare --dangerously-skip-permissions "$@" +exec /usr/local/lib/claude-real --bare --dangerously-skip-permissions "$@" From 2103499d4159ee5792cf3f72b98d47e2dbddeaff Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 12:24:08 -0700 Subject: [PATCH 09/20] fix: remove invalid companyAnnouncements field from settings.json companyAnnouncements expects an array, not a boolean. The validation error caused Claude Code to skip the ENTIRE settings file, so bypassPermissions was never applied and all startup modals appeared. --- sandbox/settings.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/sandbox/settings.json b/sandbox/settings.json index ad289be..398265f 100644 --- a/sandbox/settings.json +++ b/sandbox/settings.json @@ -6,8 +6,6 @@ }, "enableAllProjectMcpServers": true, "trustedMcpServers": ["atlassian"], - "spinnerTipsEnabled": false, - "companyAnnouncements": false, "env": { "CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1" } From 166678075dad33ca22c13a2600c7f5189e5c9f08 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 12:35:39 -0700 Subject: [PATCH 10/20] fix: eliminate all Claude Code startup banners in sandbox Three remaining banners fixed: - Workspace trust: pre-seed hasTrustDialogAccepted in claude.json - API key prompt: move key from agent env to apiKeyHelper in settings so Claude Code does not detect it as a custom env var - Bypass permissions warning: add skipDangerousModePermissionPrompt --- agents/default.yaml | 1 - agents/demo.yaml | 1 - sandbox/claude.json | 8 +++++++- sandbox/settings.json | 2 ++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/agents/default.yaml b/agents/default.yaml index ce46633..61f0e63 100644 --- a/agents/default.yaml +++ b/agents/default.yaml @@ -21,5 +21,4 @@ providers: # Proxy config: gateway routes inference through its local proxy. env: ANTHROPIC_BASE_URL: https://inference.local - ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" diff --git a/agents/demo.yaml b/agents/demo.yaml index f021aa1..b6bc090 100644 --- a/agents/demo.yaml +++ b/agents/demo.yaml @@ -19,5 +19,4 @@ providers: env: ANTHROPIC_BASE_URL: https://inference.local - ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" diff --git a/sandbox/claude.json b/sandbox/claude.json index adb5403..4a210a7 100644 --- a/sandbox/claude.json +++ b/sandbox/claude.json @@ -1,5 +1,11 @@ { "hasCompletedOnboarding": true, "numStartups": 1, - "autoUpdates": false + "autoUpdates": false, + "projects": { + "/sandbox": { + "hasTrustDialogAccepted": true, + "allowedTools": [] + } + } } diff --git a/sandbox/settings.json b/sandbox/settings.json index 398265f..56b07fa 100644 --- a/sandbox/settings.json +++ b/sandbox/settings.json @@ -6,6 +6,8 @@ }, "enableAllProjectMcpServers": true, "trustedMcpServers": ["atlassian"], + "skipDangerousModePermissionPrompt": true, + "apiKeyHelper": "echo sk-ant-openshell-proxy-managed", "env": { "CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1" } From 7333a8a028dcf90121cb53a8b7b5d37db2666f64 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 12:48:54 -0700 Subject: [PATCH 11/20] refactor: consolidate test targets, auto-detect CI mode - test-flow.sh: remove --full flag (sandbox tests always run), auto-detect CI mode from CI env var (set by GitHub Actions) - kind-lifecycle.sh: same CI auto-detection, remove --full from default args - Makefile: replace 8 test targets with 4 (test, test-local, test-kind, test-remote, test-all) - integration.yml: update to new target names --- .github/workflows/integration.yml | 8 +- Makefile | 65 +++++--------- test/kind-lifecycle.sh | 7 +- test/test-flow.sh | 140 ++++++++++++------------------ 4 files changed, 83 insertions(+), 137 deletions(-) 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 f5cd267..d0e0d01 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,11 @@ ## 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) @@ -27,8 +23,7 @@ DEV_SANDBOX_IMAGE := $(REGISTRY):$(DEV_TAG)-sandbox DEV_RUNNER_IMAGE := $(REGISTRY):$(DEV_TAG)-runner .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 \ + vet lint test test-local test-kind test-remote test-all \ dev-sandbox dev-runner clean help ## ── CLI ────────────────────────────────────────────────────────────── @@ -77,55 +72,35 @@ 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 +## Local gateway integration +test-local: cli test + ./test/test-flow.sh local -## ── Developer targets (credentials available) ──────────────────────── - -## 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 +test-kind: cli test docker 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. -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 ───────────────────────────────────────────────── 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 41252f2..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,28 +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). - # create requires providers to already be registered — use ci profile which - # has no providers, so no registration needed. - 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 - # 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 @@ -257,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" \ @@ -279,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++)) @@ -299,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 "" } @@ -318,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 @@ -346,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 @@ -380,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 From fe27ed771e5e4fbed5499465565267f231ea6a5f Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 13:01:06 -0700 Subject: [PATCH 12/20] feat: eliminate floating image tags, version-pin everything MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove :sandbox and :runner floating tags from CI - Dev builds resolve to last release tag (v0.1.2-5-g... → :sandbox-v0.1.2) - Remove hardcoded runner image from gateway.toml and config defaults - Add sha-based tags for main branch pushes - Document image tag scheme in SPEC.md --- .github/workflows/images.yml | 12 ++++-------- Makefile | 7 ++----- SPEC.md | 18 ++++++++++++++++++ cmd/up.go | 16 +++++++++++----- gateways/ocp/gateway.toml | 3 --- internal/gateway/config.go | 4 +--- 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 5c5cbb8..9f2a5d6 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -31,9 +31,9 @@ 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- - uses: docker/build-push-action@v6 with: context: sandbox @@ -41,9 +41,7 @@ jobs: 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: @@ -70,9 +68,9 @@ 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- - uses: docker/build-push-action@v6 with: context: build/runner @@ -80,7 +78,5 @@ jobs: 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/Makefile b/Makefile index d0e0d01..4a2efa3 100644 --- a/Makefile +++ b/Makefile @@ -15,12 +15,9 @@ REGISTRY ?= ghcr.io/robbycochran/harness-openshell PLATFORM := linux/amd64 VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev) LDFLAGS := -s -w -X main.version=$(VERSION) -DEV_TAG := dev-$(shell git rev-parse --short HEAD) -SANDBOX_IMAGE := $(REGISTRY):sandbox -RUNNER_IMAGE := $(REGISTRY):runner -DEV_SANDBOX_IMAGE := $(REGISTRY):$(DEV_TAG)-sandbox -DEV_RUNNER_IMAGE := $(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 test test-local test-kind test-remote test-all \ diff --git a/SPEC.md b/SPEC.md index 17624b2..948260b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -104,6 +104,24 @@ 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` | +| PR #53 | `:sandbox-pr-53` | `:runner-pr-53` | +| Main push | `:sandbox-sha-` | `:runner-sha-` | + +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` (dev build, pins to last release tag) +- `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/cmd/up.go b/cmd/up.go index 12c7351..8f078ac 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -163,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 @@ -376,10 +378,14 @@ func defaultRunnerImage() string { func versionedImage(name string) string { base := "ghcr.io/robbycochran/harness-openshell" v := strings.TrimPrefix(Version, "v") - if v != "dev" && v != "" && !strings.Contains(v, "-") { - return base + ":" + name + "-v" + v + if v == "" || v == "dev" { + return base + ":" + name + } + if strings.Contains(v, "-") { + semver := v[:strings.Index(v, "-")] + return base + ":" + name + "-v" + semver } - return base + ":" + name + return base + ":" + name + "-v" + v } func runnerEnv(gatewayEndpoint, sandboxImage string) []map[string]any { diff --git a/gateways/ocp/gateway.toml b/gateways/ocp/gateway.toml index 0106b23..fad25ba 100644 --- a/gateways/ocp/gateway.toml +++ b/gateways/ocp/gateway.toml @@ -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/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" } From 7af2e4bf444b8eee4638155a61b5ab7ee3f1be0b Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 13:05:39 -0700 Subject: [PATCH 13/20] fix: use commit sha for image tags instead of pr- prefix --- .github/workflows/images.yml | 6 ++---- SPEC.md | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 9f2a5d6..df6dc87 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -32,8 +32,7 @@ jobs: images: ${{ env.IMAGE_BASE }} tags: | type=semver,pattern=sandbox-v{{version}} - type=ref,event=pr,prefix=sandbox-pr- - type=sha,prefix=sandbox- + type=sha,prefix=sandbox-,format=short - uses: docker/build-push-action@v6 with: context: sandbox @@ -69,8 +68,7 @@ jobs: images: ${{ env.IMAGE_BASE }} tags: | type=semver,pattern=runner-v{{version}} - type=ref,event=pr,prefix=runner-pr- - type=sha,prefix=runner- + type=sha,prefix=runner-,format=short - uses: docker/build-push-action@v6 with: context: build/runner diff --git a/SPEC.md b/SPEC.md index 948260b..77c1b6e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -111,8 +111,7 @@ All images are published to `ghcr.io/robbycochran/harness-openshell`. No floatin | Trigger | Sandbox | Runner | |---------|---------|--------| | Release `v0.1.2` | `:sandbox-v0.1.2` | `:runner-v0.1.2` | -| PR #53 | `:sandbox-pr-53` | `:runner-pr-53` | -| Main push | `:sandbox-sha-` | `:runner-sha-` | +| Any push/PR | `:sandbox-` | `:runner-` | The CLI resolves images from its embedded version (set via `-ldflags` at build time): From 6ffb800ec9f97948bae332d7f3251939757b9c1b Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 14:07:06 -0700 Subject: [PATCH 14/20] fix: dev builds use exact version tag, not last release v0.1.2-5-gabc1234 now resolves to :sandbox-v0.1.2-5-gabc1234, matching make dev-sandbox. No more falling back to the release tag. --- SPEC.md | 2 +- cmd/up.go | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/SPEC.md b/SPEC.md index 77c1b6e..53ef23b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -116,7 +116,7 @@ All images are published to `ghcr.io/robbycochran/harness-openshell`. No floatin 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` (dev build, pins to last release tag) +- `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. diff --git a/cmd/up.go b/cmd/up.go index 8f078ac..cd83cfe 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -377,15 +377,10 @@ func defaultRunnerImage() string { func versionedImage(name string) string { base := "ghcr.io/robbycochran/harness-openshell" - v := strings.TrimPrefix(Version, "v") - if v == "" || v == "dev" { + if Version == "" || Version == "dev" { return base + ":" + name } - if strings.Contains(v, "-") { - semver := v[:strings.Index(v, "-")] - return base + ":" + name + "-v" + semver - } - return base + ":" + name + "-v" + v + return base + ":" + name + "-" + Version } func runnerEnv(gatewayEndpoint, sandboxImage string) []map[string]any { From 299fe67a0ce2d0f88c19d863a629d49b409db8ac Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 14:08:05 -0700 Subject: [PATCH 15/20] feat: add make all target (cli + dev-sandbox) --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4a2efa3..ae8cd0a 100644 --- a/Makefile +++ b/Makefile @@ -19,12 +19,15 @@ LDFLAGS := -s -w -X main.version=$(VERSION) DEV_SANDBOX_IMAGE := $(REGISTRY):sandbox-$(VERSION) DEV_RUNNER_IMAGE := $(REGISTRY):runner-$(VERSION) -.PHONY: cli sandbox push-sandbox cli-runner runner push-runner \ +.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 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 -ldflags '$(LDFLAGS)' -o harness . From b40436d081d1701430d227630c0fb8052eb27037 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 14:09:18 -0700 Subject: [PATCH 16/20] feat: use podman by default, CONTAINER_CLI=docker to switch Local dev targets (dev-sandbox, dev-runner) now build locally without push. New dev-push target for pushing to registry. Release targets (sandbox, runner) still use buildx for multi-arch. --- Makefile | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index ae8cd0a..4cdb70f 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ ## make runner # build + push runner REGISTRY ?= ghcr.io/robbycochran/harness-openshell +CONTAINER_CLI ?= podman PLATFORM := linux/amd64 VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev) LDFLAGS := -s -w -X main.version=$(VERSION) @@ -21,7 +22,7 @@ DEV_RUNNER_IMAGE := $(REGISTRY):runner-$(VERSION) .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 clean help + dev-sandbox dev-runner dev-push clean help ## ── CLI ────────────────────────────────────────────────────────────── @@ -38,7 +39,7 @@ cli: ## 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 + $(CONTAINER_CLI) buildx build --platform linux/amd64,linux/arm64 -t $(SANDBOX_IMAGE) sandbox/ --push @echo "Built and pushed: $(SANDBOX_IMAGE) (multi-arch)" push-sandbox: sandbox @@ -51,11 +52,11 @@ cli-runner: ## 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 ───────────────────────────────────────────────────── @@ -89,7 +90,7 @@ test-local: cli test ## Builds sandbox image locally and pre-loads into kind (no registry push needed). ## Use KEEP=1 to keep the cluster after tests (for debugging). test-kind: cli test - docker build -t $(DEV_SANDBOX_IMAGE) sandbox/ + $(CONTAINER_CLI) build -t $(DEV_SANDBOX_IMAGE) sandbox/ @echo "" SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) ./test/kind-lifecycle.sh $(if $(KEEP),--keep) @@ -104,16 +105,21 @@ test-all: test-local test-kind test-remote ## ── Dev image builds ───────────────────────────────────────────────── -## Build dev sandbox image to ghcr.io +## Build dev sandbox image locally 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 ghcr.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)" + +## Push dev images to registry +dev-push: dev-sandbox dev-runner + $(CONTAINER_CLI) push $(DEV_SANDBOX_IMAGE) + $(CONTAINER_CLI) push $(DEV_RUNNER_IMAGE) + @echo "Pushed: $(DEV_SANDBOX_IMAGE) $(DEV_RUNNER_IMAGE)" ## ── Convenience targets ─────────────────────────────────────────────── From 38d6652d24439da0f7cc45a1eb0c44c87faa6001 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 14:15:52 -0700 Subject: [PATCH 17/20] fix: restore ANTHROPIC_API_KEY in agent env, remove apiKeyHelper apiKeyHelper doesn't work with --bare mode. The API key prompt was originally caused by the invalid settings.json (companyAnnouncements error skipped the entire file). Now that settings.json is valid, --dangerously-skip-permissions suppresses the key prompt. --- agents/default.yaml | 1 + agents/demo.yaml | 1 + sandbox/settings.json | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/agents/default.yaml b/agents/default.yaml index 61f0e63..ce46633 100644 --- a/agents/default.yaml +++ b/agents/default.yaml @@ -21,4 +21,5 @@ providers: # Proxy config: gateway routes inference through its local proxy. env: ANTHROPIC_BASE_URL: https://inference.local + ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" diff --git a/agents/demo.yaml b/agents/demo.yaml index b6bc090..f021aa1 100644 --- a/agents/demo.yaml +++ b/agents/demo.yaml @@ -19,4 +19,5 @@ providers: env: ANTHROPIC_BASE_URL: https://inference.local + ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" diff --git a/sandbox/settings.json b/sandbox/settings.json index 56b07fa..aec0d85 100644 --- a/sandbox/settings.json +++ b/sandbox/settings.json @@ -7,7 +7,6 @@ "enableAllProjectMcpServers": true, "trustedMcpServers": ["atlassian"], "skipDangerousModePermissionPrompt": true, - "apiKeyHelper": "echo sk-ant-openshell-proxy-managed", "env": { "CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1" } From ca998c2939089168bf1010554a2077ae3705bb72 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 14:20:05 -0700 Subject: [PATCH 18/20] fix: add skipApiKeyPrompt to suppress custom API key confirmation --- sandbox/claude.json | 1 + 1 file changed, 1 insertion(+) diff --git a/sandbox/claude.json b/sandbox/claude.json index 4a210a7..0b2326e 100644 --- a/sandbox/claude.json +++ b/sandbox/claude.json @@ -1,5 +1,6 @@ { "hasCompletedOnboarding": true, + "skipApiKeyPrompt": true, "numStartups": 1, "autoUpdates": false, "projects": { From ce958408868aafc43727bcc183137f8a589e98dd Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 14:22:58 -0700 Subject: [PATCH 19/20] fix: pre-approve API key in claude.json to skip confirmation prompt Claude Code stores approved keys by their last 20 chars in customApiKeyResponses.approved. Pre-seed with the proxy placeholder key suffix. --- sandbox/claude.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sandbox/claude.json b/sandbox/claude.json index 0b2326e..5af008a 100644 --- a/sandbox/claude.json +++ b/sandbox/claude.json @@ -1,8 +1,10 @@ { "hasCompletedOnboarding": true, - "skipApiKeyPrompt": true, "numStartups": 1, "autoUpdates": false, + "customApiKeyResponses": { + "approved": ["nshell-proxy-managed"] + }, "projects": { "/sandbox": { "hasTrustDialogAccepted": true, From a513cac50439c560aadaaf9b419fb22d8423205e Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 8 Jun 2026 16:02:06 -0700 Subject: [PATCH 20/20] feat: bump openshell to 0.0.58, multi-arch dev-push with podman --- Makefile | 23 +++++++++++++++-------- build/runner/Dockerfile | 2 +- cmd/deploy_test.go | 12 ++++++------ cmd/helpers_test.go | 2 +- docs/design.md | 2 +- docs/proto-migration.md | 2 +- gateways/kind/gateway.toml | 2 +- gateways/ocp/gateway.toml | 2 +- internal/gateway/cli_test.go | 4 ++-- internal/preflight/preflight_test.go | 4 ++-- openshell.toml | 2 +- 11 files changed, 32 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index 4cdb70f..92f19b7 100644 --- a/Makefile +++ b/Makefile @@ -39,11 +39,13 @@ cli: ## Sandbox image (Claude Code + mcp-atlassian + gws, multi-arch) sandbox: sandbox/Dockerfile sandbox/startup.sh \ sandbox/policy.yaml sandbox/CLAUDE.md sandbox/settings.json - $(CONTAINER_CLI) 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: @@ -105,7 +107,7 @@ test-all: test-local test-kind test-remote ## ── Dev image builds ───────────────────────────────────────────────── -## Build dev sandbox image locally +## Build dev sandbox image locally (native arch only) dev-sandbox: $(CONTAINER_CLI) build -t $(DEV_SANDBOX_IMAGE) sandbox/ @echo "Built: $(DEV_SANDBOX_IMAGE)" @@ -115,11 +117,16 @@ dev-runner: cli-runner $(CONTAINER_CLI) build --platform $(PLATFORM) -t $(DEV_RUNNER_IMAGE) build/runner/ @echo "Built: $(DEV_RUNNER_IMAGE)" -## Push dev images to registry -dev-push: dev-sandbox dev-runner - $(CONTAINER_CLI) push $(DEV_SANDBOX_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) $(DEV_RUNNER_IMAGE)" + @echo "Pushed: $(DEV_SANDBOX_IMAGE) (multi-arch) $(DEV_RUNNER_IMAGE) (amd64)" ## ── Convenience targets ─────────────────────────────────────────────── 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/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/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 fad25ba..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" 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/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/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"