From 4f94f02117c1002534e234097f19d448ba9ad176 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Mon, 28 Sep 2026 13:56:43 -0700 Subject: [PATCH 1/7] chore: upgrade OpenShell to v0.1.2 --- .github/actions/setup-openshell/action.yml | 2 +- .github/workflows/README.md | 4 +- .github/workflows/vertex-smoke.yml | 2 +- .openshell-version | 2 +- README.md | 30 ++-- docs/ci.md | 46 +++--- docs/compatibility.md | 31 ++-- docs/workflow-format.md | 17 +- go.mod | 8 +- go.sum | 36 ++--- runner/README.md | 2 +- runner/cmd/apply.go | 2 +- runner/cmd/apply_service.go | 11 +- runner/cmd/workflow_apply.go | 2 +- runner/cmd/workflow_apply_test.go | 7 +- runner/internal/config/types.go | 4 +- runner/internal/openshell/sdkclient/client.go | 2 +- .../openshell/sdkclient/gateway_test.go | 8 +- .../internal/openshell/sdkclient/inference.go | 37 +---- .../openshell/sdkclient/inference_e2e_test.go | 135 ---------------- .../openshell/sdkclient/inference_test.go | 149 +----------------- .../internal/openshell/sdkclient/sandbox.go | 5 +- .../openshell/sdkclient/sandbox_test.go | 4 +- runner/internal/plan/plan_test.go | 18 +-- runner/internal/plan/render_test.go | 6 +- runner/internal/plan/state_test.go | 22 +-- runner/internal/reconcile/inference_test.go | 2 +- runner/internal/testutil/fake_platform.go | 59 ++++++- scripts/pr-review-local.sh | 4 +- scripts/review/agents/codex.sh | 5 +- scripts/review/agents/opencode.sh | 8 +- tasks/README.md | 6 +- tasks/acs-ci-nightly/README.md | 3 +- tasks/github-pr-merger/workflow/harness.yaml | 8 +- tasks/github-pr-merger/workflow/opencode.json | 4 +- tasks/github-pr-reviewer/README.md | 15 +- tasks/github-pr-reviewer/openshell/README.md | 10 +- .../github-pr-reviewer/openshell/policy.yaml | 5 +- .../workflow/codex-harness.yaml | 17 +- .../github-pr-reviewer/workflow/harness.yaml | 5 +- .../workflow/opencode-harness.yaml | 8 +- .../workflow/opencode-review.json | 4 +- test/github-pr-reviewer-local.sh | 4 +- test/hypershell-haiku-workflow.yaml | 21 ++- test/hypershell-lifecycle.sh | 6 + test/lib/provision.sh | 8 +- test/pr_review_test.go | 14 +- test/suite/run.sh | 2 +- test/vertex-gemini-opencode-workflow.yaml | 11 +- test/vertex-gemini-opencode.sh | 12 +- 50 files changed, 285 insertions(+), 548 deletions(-) delete mode 100644 runner/internal/openshell/sdkclient/inference_e2e_test.go diff --git a/.github/actions/setup-openshell/action.yml b/.github/actions/setup-openshell/action.yml index 0f417b16..aa7be405 100644 --- a/.github/actions/setup-openshell/action.yml +++ b/.github/actions/setup-openshell/action.yml @@ -24,7 +24,7 @@ runs: run: | ready=false for _ in $(seq 1 30); do - if openshell inference get &>/dev/null; then + if openshell status &>/dev/null; then ready=true break fi diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 44ebcbae..e2259bfb 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -23,7 +23,7 @@ missing. The reviewer invokes [`setup-openshell`](../actions/setup-openshell/action.yml) to install the pinned OpenShell CLI and wait for the local CI gateway. The [`scripts/pr-review-local.sh`](../../scripts/pr-review-local.sh) wrapper creates -the temporary workspace/providers and configures inference. It calls +the temporary workspace/providers and attaches them to the sandbox. It calls [`pr-review.sh run`](../../scripts/pr-review.sh) and tears down its setup afterward. The review script stages the diff in `prepare`, checks eligibility, renders the PR-specific policy, invokes the CLI, and validates output. The CLI @@ -32,7 +32,7 @@ composes the task and manages its sandbox lifecycle. The CLI already supports a direct managed-gateway connection. Moving this review job to the intended managed StackRox deployment still requires platform ownership of workspace membership, provider credentials and their refresh or -expiry, matching inference routes, and CI network access. A pre-provisioned +expiry, native provider endpoints, and CI network access. A pre-provisioned provider name does not by itself keep a short-lived GitHub token usable. See [managed reviewer requirements](../../docs/ci.md#managed-reviewer-transition). diff --git a/.github/workflows/vertex-smoke.yml b/.github/workflows/vertex-smoke.yml index 5f97c4c4..6af4d704 100644 --- a/.github/workflows/vertex-smoke.yml +++ b/.github/workflows/vertex-smoke.yml @@ -37,7 +37,7 @@ jobs: - name: Wait for gateway run: | for _ in $(seq 1 30); do - openshell inference get &>/dev/null && exit 0 + openshell status &>/dev/null && exit 0 sleep 1 done openshell gateway list || true diff --git a/.openshell-version b/.openshell-version index bc0256d1..53666008 100644 --- a/.openshell-version +++ b/.openshell-version @@ -1 +1 @@ -v0.0.110 +v0.1.2 diff --git a/README.md b/README.md index a2cae3dc..b63fde7a 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Repository workflow or local caller Trusted setup establishes gateway access and provider credentials before the task runs. The diagram shows the request flow; OpenShell owns sandbox isolation, -inference routing, credential handling, and network policy enforcement. +provider attachment, credential handling, and network policy enforcement. ## Use the reusable PR reviewer @@ -119,7 +119,7 @@ data. The `ai-review` label is explicit opt-in. See |---|---| | [Reusable workflow](.github/workflows/pr-review-reusable.yml) | Trusted checkout, job permissions, App token, and setup/execution steps | | [`setup-openshell`](.github/actions/setup-openshell/action.yml) | Invoke the installer for the pinned OpenShell CLI release and wait for gateway readiness | -| [`scripts/pr-review-local.sh`](scripts/pr-review-local.sh) | Create temporary workspace/providers, configure inference, run the review, and remove its setup resources | +| [`scripts/pr-review-local.sh`](scripts/pr-review-local.sh) | Create temporary workspace/providers, run the review, and remove its setup resources | | [`scripts/pr-review.sh`](scripts/pr-review.sh) | Stage the diff, check PR eligibility, render the PR policy, invoke the CLI, and validate the output | | [`harness` CLI](runner/) | Compose the task and manage its sandbox lifecycle | @@ -129,9 +129,8 @@ been switched to that connection and platform bootstrap contract. In the intended managed deployment, the GitHub job authenticates to a gateway operated outside that job. The platform owns workspace membership, provider -lifecycle, and matching inference routes. The task retains its behavior and -allowed operations when the managed environment supplies equivalent providers, -policy support, and inference configuration. +lifecycle. The task retains its behavior and allowed operations when the managed +environment supplies equivalent providers and policy support. A pre-provisioned provider name still needs usable credentials. The managed integration must establish who mints or refreshes short-lived GitHub App tokens, @@ -151,7 +150,7 @@ make cli ``` Before applying a task, ensure its gateway is reachable, its provider instances -exist, and its inference route and policy are configured. Select an existing +exist, and its provider attachments and policy are configured. Select an existing local gateway with the native CLI, or configure a direct managed target as described in [docs/ci.md](docs/ci.md#workflow-contract). @@ -193,8 +192,8 @@ agent, collects declared output files, and deletes the sandbox. | [tasks/](tasks/) | Task instructions, policy, provider references, image selection, payloads, and outputs | | [runner/](runner/) | Generic `plan`/`apply` composition and sandbox lifecycle | | [images/](images/) | Reusable runtime toolchains | -| Platform administration | Managed gateway access, workspace membership, provider credential lifecycle, and inference configuration | -| OpenShell | Gateway resources, credential-backed proxies, inference routing, policy enforcement, and sandbox isolation | +| Platform administration | Managed gateway access, workspace membership, and provider credential lifecycle | +| OpenShell | Gateway resources, credential-backed providers, native provider endpoints, policy enforcement, and sandbox isolation | The `harness` CLI verifies provider references and asks OpenShell to attach their masked proxy interfaces. Provider provisioning belongs to trusted setup @@ -212,11 +211,12 @@ durable workflow database, scheduler, release history, or rollback mechanism. - GitHub Actions owns run state, labels, artifacts, concurrency, and approvals. OpenShell and the platform own gateway runtime resources. -The PR review task consumes `inference.local`; setup owns its configuration. -The local wrapper configures the route in its temporary workspace, while a -managed platform supplies the matching route before review execution. Other -workflow documents can still explicitly request inference reconciliation. See -[docs/ci.md](docs/ci.md) for the credential and setup contract. +OpenShell v0.1.2 removed managed inference routes and `inference.local`. The PR +review task attaches its inference provider to the sandbox and configures the +agent's native endpoint. Existing workflow documents that still declare an +`inference` block must migrate to `sandbox.providers` and a native client +configuration before apply. See [docs/ci.md](docs/ci.md) for the credential and +setup contract. ## CLI @@ -226,7 +226,7 @@ workflow documents can still explicitly request inference reconciliation. See | `harness workflow apply FILE` | Execute one task headlessly | | `harness workflow apply FILE --attach` | Execute with an attached terminal | | `harness workflow apply FILE --output-dir DIR` | Download declared outputs below `DIR` | -| `harness workflow apply FILE --setup-only` | Verify references and reconcile inference without starting a sandbox | +| `harness workflow apply FILE --setup-only` | Verify provider references without starting a sandbox | `plan` and dry-run output support `-o table|json|yaml`. Interpolated values are redacted from display output; authors must keep credential values out of @@ -250,5 +250,5 @@ make test-suite Gateway lifecycle checks are available through `make test-local`, `make test-kind`, and `make test-remote`. Provider-capability checks require configured credentials. A passing lifecycle check establishes execution and -cleanup behavior; live GitHub effects, inference, and consumer integrations +cleanup behavior; live GitHub effects, provider access, and consumer integrations need their corresponding validation. diff --git a/docs/ci.md b/docs/ci.md index cdfd7dc5..f3efcdf1 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -1,6 +1,6 @@ # CI and live validation -Credential-free PR checks do not establish live inference success. +Credential-free PR checks do not establish live provider access or model success. Image PR checks build without registry login or publication; only main/tag pushes publish images and update the shared registry cache. @@ -17,7 +17,7 @@ administrator account at runtime. `.github/workflows/vertex-smoke.yml` is manually dispatched only. It starts a local OpenShell gateway on a GitHub-hosted runner, obtains a short-lived Google access token from `VERTEX_AI_SERVICE_ACCOUNT_KEY`, and uses it to run OpenCode -with Gemini 2.5 Pro through `inference.local`. It creates and deletes an +with Gemini 2.5 Pro through Vertex's native OpenAI-compatible endpoint. It creates and deletes an isolated workspace, so it does not affect the gateway's default workspace. Repository configuration: @@ -52,8 +52,8 @@ runner loss) cannot execute shell cleanup. Credential-free orchestration tests run as part of `go test ./...`. The service-account project must have access to `gemini-2.5-pro` in the -configured Vertex region. A 404 from the inference setup means the model is -unavailable to that project; do not bypass the check with `--no-verify`. +configured Vertex region. A 404 from provider creation or the native endpoint +means the model is unavailable to that project; do not bypass provider checks. ## Label-driven PR review @@ -97,13 +97,13 @@ an artifact link. Seven-day artifacts hold input revisions, diff/hash, execution metadata, raw output/diagnostics, and `review.txt`. Reviews are advisory inline comments only; they do not approve, request changes, or merge. -The default reviewer runs OpenCode with Gemini 2.5 Pro through `inference.local` -and Google Vertex AI. The model is selected in +The default reviewer runs OpenCode with Gemini 2.5 Pro through Google Vertex +AI's native endpoint. The provider is selected in [`scripts/pr-review-local.sh`](../scripts/pr-review-local.sh) and the agent arguments in [`opencode-harness.yaml`](../tasks/github-pr-reviewer/workflow/opencode-harness.yaml). -The task consumes the existing inference route; it does not configure it. -Keep those selections aligned and verify model access with the CI identity -when changing them. +The task attaches that provider to its sandbox; it does not create providers or +handle credentials. Keep those selections aligned and verify model access with +the CI identity when changing them. The host uses trusted caller default-branch inputs and the pinned `harness-openshell` revision. The sandbox receives the PR diff and attaches the @@ -140,8 +140,8 @@ bash scripts/pr-review-local.sh The reusable workflow also supports `review-agent: codex` with the `stackrox-ai-review` label. This runs the pinned Codex CLI inside OpenShell and -keeps the existing OpenCode/Vertex path available. Codex uses the gateway's -`inference.local` Responses API route. The local CI path creates an ephemeral +keeps the existing OpenCode/Vertex path available. Codex uses the provider's +native OpenAI Responses API endpoint. The local CI path creates an ephemeral workspace with `github-review` and `openai-inference` providers using trusted workflow bootstrap; the API key remains in the gateway and is never passed into the sandbox. @@ -256,8 +256,8 @@ can apply it, a platform administrator must establish three durable resources: 1. add the harness service-account subject to `default-inference` as `user`; 2. create the `vertex-claude-haiku` provider from an identity with Vertex AI prediction access; and -3. set `inference.local` to provider `vertex-claude-haiku` and model - `claude-haiku-4-5@20251001`. +3. attach `vertex-claude-haiku` to the sandbox and configure the agent for + Vertex's native Claude endpoint and model `claude-haiku-4-5@20251001`. For local development credentials, OpenShell's native bootstrap is: @@ -270,20 +270,14 @@ openshell provider create --gateway ADMIN_GATEWAY \ --config VERTEX_AI_PROJECT_ID=PROJECT_ID \ --config VERTEX_AI_REGION=us-east5 -openshell inference set --gateway ADMIN_GATEWAY \ - --workspace default-inference \ - --provider vertex-claude-haiku \ - --model 'claude-haiku-4-5@20251001' +openshell provider get --gateway ADMIN_GATEWAY \ + --workspace default-inference vertex-claude-haiku ``` -Do not use `--no-verify`: a successful inference write is the base-layer proof -that the ADC principal has `aiplatform.endpoints.predict`. After bootstrap, -ordinary applies only read the matching provider and route; they neither need -workspace-admin permission nor receive the Vertex credential in the sandbox. -If a workflow selects a different provider, model, or route, the compatibility -reconciliation performs an admin-only upsert in that workspace. Treat that as -isolated-workspace setup, not a shared-workspace runtime operation; the CLI does -not restore the previous route after the run. +After bootstrap, ordinary applies only read the matching provider and attach it +to the sandbox; they neither need workspace-admin permission nor receive the +Vertex credential in the sandbox. Model selection and request timeouts belong +to the native agent client. Validate from the VPN with: @@ -300,6 +294,8 @@ Provide these values to the local harness process: - `OPENSHELL_OIDC_AUDIENCE`: gateway token audience - `OPENSHELL_OIDC_CLIENT_ID`: user service-account client ID - `OPENSHELL_OIDC_CLIENT_SECRET`: user service-account client secret +- `HYPERSHELL_VERTEX_PROJECT_ID`: Vertex project for the native Claude client +- `HYPERSHELL_VERTEX_REGION`: Vertex region for the native Claude client `test/hypershell-lifecycle.sh` reads them from the git-excluded file named by `HYPERSHELL_SA_ENV` and maps the non-secret connection metadata to the names diff --git a/docs/compatibility.md b/docs/compatibility.md index 6c79c08d..15eee7bd 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,30 +1,31 @@ # Compatibility -Last reviewed: 2026-08-24. +Last reviewed: 2026-09-28. | Component | Repository baseline | Locally exercised | Latest source reviewed | Status | |---|---|---|---|---| -| OpenShell CLI and local gateway | `0.0.110` (`.openshell-version`) | CI `local`+`kind` e2e | `0.0.111` (release) / `0.0.112` (tag) | re-baselined from `0.0.85` to `0.0.110`; `0.0.111` deferred — see note below | -| OpenShell Go SDK (`go.mod`) | `v0.0.0-20260820101241-7909fb5d0f54` (= `v0.0.110` tag) | via unit tests / fake client | matches CLI baseline | aligned with CLI baseline (skew closed) | +| OpenShell CLI and local gateway | `0.1.2` (`.openshell-version`) | static/unit checks; live `local`+`kind` e2e pending | `0.1.2` (release) | upgraded from `0.0.110`; v0.1.x migration applied | +| OpenShell Go SDK (`go.mod`) | `v0.0.0-20260928030816-6648bd0c290e` (= `v0.1.2` source tag) | via unit tests / fake client | matches CLI baseline | aligned with CLI baseline | | Agent Control Plane | no runtime dependency | not installed | `101c0ec` | manifest schema and `acpctl apply` source reviewed; parser/runtime conformance remains open | -| Go | `1.25.0` (toolchain `1.26.1`, `go.mod`) | `1.25`/`1.26` | n/a | build, unit tests, vet, lint, and config suite pass | +| Go | `1.26.0` (`go.mod`) | `1.26.8` | n/a | build, unit tests, vet, shell/action checks, and config suite pass; golangci-lint is blocked by the installed config parser | -## OpenShell v0.0.111 deferred (sandbox-create breaking change) +## OpenShell v0.1.x migration -The re-baseline targets `0.0.110`, not the latest `0.0.111`. In `0.0.111`, -`openshell sandbox create` makes `--upload` **mutually exclusive** with a trailing -`-- ` (`upload: Vec` gains `conflicts_with = "command"`), and adds -a `--detach` flag. The harness always uploads config/payloads *and* runs a command -(`true` headless, `bash run.sh` for task/attach), so every create path breaks at -`0.0.111`. The break landed in `0.0.111` only — every release `0.0.86`–`0.0.110` -keeps the current idiom working. Adopting `0.0.111` requires reworking the create -flow to create-detached → `sandbox upload` → `sandbox exec`/`connect`; that is a -separate, e2e-validated change, tracked with the modernization re-baseline task. +OpenShell `0.1.2` is a breaking release line with coordinated CLI, gateway, and +SDK changes. This repository now pins the CLI/gateway baseline and Go SDK to +`0.1.2`. Existing sandboxes must be recreated when changing from the `0.0.x` +line; the integration tests provision their own sandboxes. + +The earlier `0.0.111` CLI change made `sandbox create --upload` incompatible with +a trailing command. The harness's canonical path now creates sandboxes, uploads +files, and executes commands through the Go SDK, so that CLI-only constraint does +not apply to the workflow runner. The CLI remains used for gateway provisioning +and inspection in the integration scripts. ## OpenShell policy compatibility Harness policy documents stay in the upstream OpenShell policy YAML schema. -OpenShell `0.0.110` includes policy middleware and keeps Z3-backed proving in the +OpenShell `0.1.2` baseline includes policy middleware and keeps Z3-backed proving in the gateway/prover path; the client-side bundled Z3 integration was removed. The harness does not introduce a policy dialect or its own solver. diff --git a/docs/workflow-format.md b/docs/workflow-format.md index 7507d6ac..eed1f13c 100644 --- a/docs/workflow-format.md +++ b/docs/workflow-format.md @@ -20,14 +20,9 @@ target: gateway: openshell workspace: default -inference: - route: inference.local - provider: vertex-review - model: gemini-2.5-pro - sandbox: image: quay.io/example/reviewer:v1 - providers: [github-review] + providers: [github-review, vertex-review] agent: type: opencode @@ -46,8 +41,6 @@ optional. Unknown fields are rejected so a typo cannot silently change a run. - `target` selects the gateway and workspace. Explicit CLI flags and `OPENSHELL_*` environment variables take precedence over these values. -- `inference` selects the gateway inference route and model when needed. Its - `provider` must already exist in OpenShell. - `sandbox` describes the image, policy, environment, provider attachments, payload handling, and cleanup behavior for a run. - `sandbox.providers` names providers that must already exist in OpenShell and @@ -103,9 +96,11 @@ stages the PR diff as data. Interpolated values are redacted from display projections, but the CLI does not attempt to detect credentials embedded as literal YAML values. -Inference route reconciliation currently writes a changed route and therefore -requires workspace-admin access. Shared workspaces should use a matching -bootstrap-owned route; isolated workspaces may use the compatibility write. +OpenShell v0.1.2 removed the workspace-global inference route and +`inference.local`. Attach an existing inference provider through +`sandbox.providers`, configure the agent for that provider's native endpoint, +and select the model in the agent command or configuration. A legacy `inference` +block is rejected during apply so it cannot silently run without model access. ## Compatibility policy diff --git a/go.mod b/go.mod index fb6f3ee5..7bc5eaa6 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/stackrox/harness-openshell go 1.26.0 require ( - github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260820101241-7909fb5d0f54 + github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260928030816-6648bd0c290e github.com/spf13/cobra v1.10.2 golang.org/x/crypto v0.55.0 golang.org/x/oauth2 v0.37.0 @@ -12,12 +12,12 @@ require ( ) require ( - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 golang.org/x/text v0.41.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/grpc v1.82.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index 0d8747d8..5c7f69f0 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260820101241-7909fb5d0f54 h1:aB51LFt0eNce8tdL17VEn38gbRN1jUDrJkB4/S/oTqU= -github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260820101241-7909fb5d0f54/go.mod h1:PPbbhH5tmSfoWzVcb5CXurnMkuJOYpesZmUB1itrlFY= +github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260928030816-6648bd0c290e h1:AQ0RXztmpX11IXjzZniPqwpUScq6ZGN11m9IbB4Zqx0= +github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260928030816-6648bd0c290e/go.mod h1:+wnjuZwBJH2yz8cU1t7FiGK7xPQyxjOY3YZNFcfpL90= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -28,21 +28,21 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -55,10 +55,10 @@ golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/runner/README.md b/runner/README.md index b826b9a5..16ceb8bd 100644 --- a/runner/README.md +++ b/runner/README.md @@ -9,7 +9,7 @@ The runner: - loads and validates a versioned workflow document; - resolves a local or managed OpenShell target; -- verifies referenced providers and reconciles the declared inference route; +- verifies referenced providers and attaches them to the sandbox; - creates a sandbox with the selected image, policy, provider attachments, and command; - uploads source and payloads, observes execution, downloads outputs, and diff --git a/runner/cmd/apply.go b/runner/cmd/apply.go index e3fe87a2..f1b7838d 100644 --- a/runner/cmd/apply.go +++ b/runner/cmd/apply.go @@ -49,7 +49,7 @@ host-interpolated and credential-bearing map values redacted.`, cmd.Flags().StringVar(&entrypoint, "entrypoint", "", "Override the agent executable") cmd.Flags().BoolVar(&attach, "attach", false, "Attach a TTY for interactive execution") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Render the action plan without mutating anything") - cmd.Flags().BoolVar(&setupOnly, "setup-only", false, "Verify provider references and configure inference without running a sandbox") + cmd.Flags().BoolVar(&setupOnly, "setup-only", false, "Verify provider references without running a sandbox") cmd.Flags().StringVarP(&output, "output", "o", "", "Output format: yaml or json (dry-run also supports table)") cmd.Flags().StringVar(&outputDir, "output-dir", "", "Host directory for workflow outputs") cmd.Flags().StringVar(&resultFile, "result-file", "", "Write host-derived execution result JSON to a new file") diff --git a/runner/cmd/apply_service.go b/runner/cmd/apply_service.go index da8fdb60..06a058d4 100644 --- a/runner/cmd/apply_service.go +++ b/runner/cmd/apply_service.go @@ -9,7 +9,6 @@ import ( "github.com/stackrox/harness-openshell/runner/internal/openshell" "github.com/stackrox/harness-openshell/runner/internal/plan" - "github.com/stackrox/harness-openshell/runner/internal/reconcile" "github.com/stackrox/harness-openshell/runner/internal/run" "github.com/stackrox/harness-openshell/runner/internal/status" ) @@ -102,6 +101,9 @@ func executeResolvedWorkflow(ctx context.Context, workflow *resolvedWorkflow, p if err := preflightPlan(p); err != nil { return err } + if inferenceConfigured(workflow.Desired.Spec.Inference) { + return fmt.Errorf("OpenShell v0.1.2 removed managed inference routes; attach a provider through sandbox.providers and configure the agent's native endpoint") + } if err := verifyProviderReferences(ctx, client, workflow.Desired); err != nil { return err } @@ -124,13 +126,6 @@ func executeResolvedWorkflow(ctx context.Context, workflow *resolvedWorkflow, p } opts.Result.setPhase("reconcile") - if inferenceConfigured(workflow.Desired.Spec.Inference) { - result, err := reconcile.ReconcileInference(ctx, client, workflow.Desired.Spec.Inference) - if err != nil { - return fmt.Errorf("reconciling inference: %w", err) - } - status.OKf("inference: %s (model %s)", result.Action, workflow.Desired.Spec.Inference.Model) - } if opts.SetupOnly { status.OK("Setup complete (--setup-only): skipping sandbox creation") return nil diff --git a/runner/cmd/workflow_apply.go b/runner/cmd/workflow_apply.go index df5e64ae..d5cc3aa3 100644 --- a/runner/cmd/workflow_apply.go +++ b/runner/cmd/workflow_apply.go @@ -67,7 +67,7 @@ func preflightPlan(p *plan.Plan) error { case group.Section == plan.SectionProviders && resource.Action == plan.ActionMissing: return fmt.Errorf("referenced provider %q does not exist; create it through platform bootstrap before apply", resource.Name) case group.Section == plan.SectionInference && resource.Action == plan.ActionValidate: - return fmt.Errorf("gateway does not support inference route reconciliation") + return fmt.Errorf("OpenShell v0.1.2 removed managed inference routes; attach a provider through sandbox.providers and configure the agent's native endpoint") } } } diff --git a/runner/cmd/workflow_apply_test.go b/runner/cmd/workflow_apply_test.go index 79d66576..7bcd5268 100644 --- a/runner/cmd/workflow_apply_test.go +++ b/runner/cmd/workflow_apply_test.go @@ -74,7 +74,7 @@ agent: } } -func TestCanonicalInferenceOnlyWorkflowDoesNotInventSandboxRun(t *testing.T) { +func TestLegacyInferenceWorkflowIsRejected(t *testing.T) { t.Setenv("HARNESS_OS_IMAGE", "") dir := t.TempDir() file := filepath.Join(dir, "workflow.yaml") @@ -99,8 +99,8 @@ inference: if err != nil { t.Fatalf("buildPlan: %v", err) } - if err := applyWorkflow(context.Background(), workflow, planned, current, client, applyOptions{}); err != nil { - t.Fatalf("applyWorkflow: %v", err) + if err := applyWorkflow(context.Background(), workflow, planned, current, client, applyOptions{}); err == nil || !strings.Contains(err.Error(), "removed managed inference routes") { + t.Fatalf("applyWorkflow error = %v, want explicit v0.1.2 inference migration error", err) } } @@ -735,6 +735,7 @@ func TestGitHubReviewerCustomSkillUsesWorkflowPayloadPath(t *testing.T) { t.Setenv("REVIEW_SKILL", skillPath) t.Setenv("REVIEW_SANDBOX_NAME", "ai-review") t.Setenv("REVIEW_GITHUB_PROVIDER", "github-review") + t.Setenv("VERTEX_AI_BASE_URL", "https://aiplatform.googleapis.com/v1/projects/test/locations/global/endpoints/openapi") workflow, err := loadWorkflow(workflowPath, "", "", applyOverrides{}) if err != nil { diff --git a/runner/internal/config/types.go b/runner/internal/config/types.go index b3fd1cde..45a8312b 100644 --- a/runner/internal/config/types.go +++ b/runner/internal/config/types.go @@ -74,7 +74,9 @@ type OIDC struct { Audience string `yaml:"audience,omitempty"` } -// Inference specifies the LLM inference route configuration. +// Inference is the removed pre-v0.1 managed inference route configuration. +// It remains parseable so legacy workflows receive an explicit migration error +// instead of silently dropping their model selection. type Inference struct { Route string `yaml:"route,omitempty"` Provider string `yaml:"provider,omitempty"` diff --git a/runner/internal/openshell/sdkclient/client.go b/runner/internal/openshell/sdkclient/client.go index 89acd14f..8cc101d7 100644 --- a/runner/internal/openshell/sdkclient/client.go +++ b/runner/internal/openshell/sdkclient/client.go @@ -165,7 +165,7 @@ func (c *client) Health(ctx context.Context) (openshell.Health, error) { // Providers lists the providers registered in the bound workspace. func (c *client) Providers(ctx context.Context) ([]openshell.Provider, error) { - raw, err := c.raw.Providers().List(ctx, c.workspace) + raw, err := c.raw.Providers().ListAll(ctx, c.workspace) if err != nil { return nil, translate(err) } diff --git a/runner/internal/openshell/sdkclient/gateway_test.go b/runner/internal/openshell/sdkclient/gateway_test.go index 2db46d21..1a898157 100644 --- a/runner/internal/openshell/sdkclient/gateway_test.go +++ b/runner/internal/openshell/sdkclient/gateway_test.go @@ -18,7 +18,7 @@ func TestGatewayInfoMergesConnectionFactsAndHealth(t *testing.T) { ctx := context.Background() fc := fake.NewClient(fake.WithGatewayInfo(&types.GatewayInfo{ Status: types.ServiceStatusHealthy, - Version: "0.0.110", + Version: "0.1.2", })) c := &client{raw: fc, workspace: "default", gatewayName: "prod", gatewayEndpoint: "gw.example:443"} @@ -26,7 +26,7 @@ func TestGatewayInfoMergesConnectionFactsAndHealth(t *testing.T) { if err != nil { t.Fatalf("GatewayInfo: %v", err) } - want := openshell.GatewayInfo{Name: "prod", Endpoint: "gw.example:443", Status: "Healthy", Version: "0.0.110"} + want := openshell.GatewayInfo{Name: "prod", Endpoint: "gw.example:443", Status: "Healthy", Version: "0.1.2"} if got != want { t.Errorf("GatewayInfo: got %+v, want %+v", got, want) } @@ -40,7 +40,7 @@ func TestGatewayInfoInjectionPathLeavesNameEndpointEmpty(t *testing.T) { ctx := context.Background() fc := fake.NewClient(fake.WithGatewayInfo(&types.GatewayInfo{ Status: types.ServiceStatusDegraded, - Version: "0.0.110", + Version: "0.1.2", })) c := NewFromClient(fc, "default") @@ -51,7 +51,7 @@ func TestGatewayInfoInjectionPathLeavesNameEndpointEmpty(t *testing.T) { if got.Name != "" || got.Endpoint != "" { t.Errorf("injection path should leave Name/Endpoint empty, got %+v", got) } - if got.Status != "Degraded" || got.Version != "0.0.110" { + if got.Status != "Degraded" || got.Version != "0.1.2" { t.Errorf("Status/Version not mapped from RPC: %+v", got) } } diff --git a/runner/internal/openshell/sdkclient/inference.go b/runner/internal/openshell/sdkclient/inference.go index c11771a9..9fcaa7de 100644 --- a/runner/internal/openshell/sdkclient/inference.go +++ b/runner/internal/openshell/sdkclient/inference.go @@ -2,52 +2,25 @@ package sdkclient import ( "context" - - v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1" + "fmt" "github.com/stackrox/harness-openshell/runner/internal/openshell" ) -// fromSDKInferenceRoute maps the SDK route view to the minimal harness view. -// Deliberately narrow (least-exposure firewall); widen only when a consumer -// genuinely needs more fields, changing this and openshell.InferenceRoute -// together. -func fromSDKInferenceRoute(r *v1.InferenceRoute) openshell.InferenceRoute { - return openshell.InferenceRoute{ - Provider: r.ProviderName, - Model: r.ModelID, - Route: r.RouteName, - TimeoutSecs: r.TimeoutSecs, - Version: r.Version, - } -} +var errManagedInferenceRemoved = fmt.Errorf("%w: managed inference routes were removed in OpenShell v0.1.0", openshell.ErrUnsupported) // GetInferenceRoute reads the named inference route in the bound workspace. func (c *client) GetInferenceRoute(ctx context.Context, route string) (openshell.InferenceRoute, error) { - r, err := c.raw.Inference().GetRoute(ctx, c.workspace, route) - if err != nil { - return openshell.InferenceRoute{}, translate(err) - } - return fromSDKInferenceRoute(r), nil + return openshell.InferenceRoute{}, errManagedInferenceRemoved } // SetInferenceRoute creates or updates (upserts) an inference route in the bound // workspace. func (c *client) SetInferenceRoute(ctx context.Context, cfg openshell.InferenceRouteConfig) (openshell.InferenceRoute, error) { - r, err := c.raw.Inference().SetRoute(ctx, c.workspace, &v1.InferenceRouteConfig{ - ProviderName: cfg.Provider, - ModelID: cfg.Model, - RouteName: cfg.Route, - NoVerify: cfg.NoVerify, - TimeoutSecs: cfg.TimeoutSecs, - }) - if err != nil { - return openshell.InferenceRoute{}, translate(err) - } - return fromSDKInferenceRoute(r), nil + return openshell.InferenceRoute{}, errManagedInferenceRemoved } // DeleteInferenceRoute removes the named inference route in the bound workspace. func (c *client) DeleteInferenceRoute(ctx context.Context, route string) error { - return translate(c.raw.Inference().DeleteRoute(ctx, c.workspace, route)) + return errManagedInferenceRemoved } diff --git a/runner/internal/openshell/sdkclient/inference_e2e_test.go b/runner/internal/openshell/sdkclient/inference_e2e_test.go deleted file mode 100644 index 1c22e32c..00000000 --- a/runner/internal/openshell/sdkclient/inference_e2e_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package sdkclient_test - -import ( - "context" - "errors" - "os" - "testing" - "time" - - "github.com/stackrox/harness-openshell/runner/internal/openshell" - "github.com/stackrox/harness-openshell/runner/internal/openshell/sdkclient" -) - -// defaultRoute mirrors plan.DefaultInferenceRoute. It is duplicated here (not -// imported) to keep the firewall's e2e test from depending on the plan package. -// A real 0.0.110 gateway accepts only a fixed set of route names — -// "inference.local" and "sandbox-system" — and rejects any other with -// InvalidArgument (verified live 2026-08-25); the harness only ever uses -// "inference.local". -const defaultRoute = "inference.local" - -// TestLiveInferenceRoleProbe verifies the harness mTLS identity can serve the -// inference surface reconcile depends on: the user-role read path always, and — -// when a credentialed provider is supplied — the admin-role write path. -// -// It is skipped unless HARNESS_E2E_GATEWAY names a registered mTLS gateway (the -// same gate as the other live checks). Optional HARNESS_E2E_WORKSPACE overrides -// the workspace. -// -// The admin-role write is the S1 risk gate for PR4b: SetInferenceRoute requires -// the workspace "admin" role, GetInferenceRoute only "user". But a live gateway -// checks Set's preconditions BEFORE the role — the route name must be valid, the -// provider must exist in the workspace, and it must carry a usable credential — -// so the role can only be probed once a credentialed provider exists. Supply its -// name via HARNESS_E2E_INFERENCE_PROVIDER to exercise the write path; without it -// the write probe is skipped (admin was confirmed present on OCP 2026-08-25). -// -// HARNESS_E2E_GATEWAY=openshell go test ./internal/openshell/sdkclient/ -run LiveInferenceRoleProbe -v -func TestLiveInferenceRoleProbe(t *testing.T) { - gw := os.Getenv("HARNESS_E2E_GATEWAY") - if gw == "" { - t.Skip("set HARNESS_E2E_GATEWAY to a registered mTLS gateway to run the inference role probe") - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - c, err := sdkclient.New(ctx, openshell.Target{ - Gateway: gw, - Workspace: os.Getenv("HARNESS_E2E_WORKSPACE"), - }) - if err != nil { - t.Fatalf("sdkclient.New(%q): %v", gw, err) - } - // Close via t.Cleanup, not defer: t.Cleanup callbacks run in LIFO order after - // the test's deferred calls, so a deferred Close would shut the client's gRPC - // connection before the route-restoration cleanup registered below could use - // it. Registered first here, it runs last — after restoration. - t.Cleanup(func() { - if err := c.Close(); err != nil { - t.Errorf("closing client: %v", err) - } - }) - - // Read path (user role). On the default route the gateway returns the route - // if configured, or ErrNotFound if not — both mean the identity can read and - // the gateway serves inference. ErrPermission/ErrUnavailable/InvalidArgument - // are all failures of the surface the harness needs. - if _, err := c.GetInferenceRoute(ctx, defaultRoute); err != nil && !errors.Is(err, openshell.ErrNotFound) { - t.Fatalf("GetInferenceRoute(%q) read path failed: %v", defaultRoute, err) - } - t.Logf("read path OK on gateway %q (user role confirmed, gateway serves inference)", gw) - - provider := os.Getenv("HARNESS_E2E_INFERENCE_PROVIDER") - if provider == "" { - t.Skip("set HARNESS_E2E_INFERENCE_PROVIDER to a registered, credentialed provider to probe the admin write path") - } - - // Write path (admin role) on the default route. NoVerify skips endpoint - // validation so the probe measures the role, not the provider's credentials. - // Read the current route first so cleanup can restore it (the write bumps - // Version); if it was unconfigured, delete to restore that state. - before, beforeErr := c.GetInferenceRoute(ctx, defaultRoute) - // Only ErrNotFound means "no route to restore"; any other read error is a real - // failure. Treating it as absent would make cleanup delete a route that was - // actually there (the read merely failed transiently) after the write succeeds. - if beforeErr != nil && !errors.Is(beforeErr, openshell.ErrNotFound) { - t.Fatalf("GetInferenceRoute(%q) pre-write read failed: %v", defaultRoute, beforeErr) - } - existed := beforeErr == nil - - // Register restoration BEFORE the write. SetInferenceRoute persists the route - // at the gateway before its gRPC response returns, so a write that reports a - // transport/unexpected error may still have changed state; t.Cleanup runs on - // every exit path (including t.Fatalf) so the probe never leaves inference.local - // pointing at probe-model. It uses a fresh context because ctx may be spent by - // the time cleanup runs. It is skipped only on a pre-write permission denial: - // then no write applied and the identity lacks the admin role the restore - // itself would need, so attempting it would just log a spurious error. - permissionDenied := false - t.Cleanup(func() { - if permissionDenied { - return - } - cctx, ccancel := context.WithTimeout(context.Background(), 30*time.Second) - defer ccancel() - if existed { - if _, err := c.SetInferenceRoute(cctx, openshell.InferenceRouteConfig{ - Provider: before.Provider, Model: before.Model, Route: defaultRoute, - NoVerify: true, TimeoutSecs: before.TimeoutSecs, - }); err != nil { - t.Errorf("restoring inference route: %v", err) - } - } else if err := c.DeleteInferenceRoute(cctx, defaultRoute); err != nil { - t.Errorf("cleanup DeleteInferenceRoute(%q): %v", defaultRoute, err) - } - }) - - _, setErr := c.SetInferenceRoute(ctx, openshell.InferenceRouteConfig{ - Provider: provider, - Model: "probe-model", - Route: defaultRoute, - NoVerify: true, - }) - switch { - case setErr == nil: - t.Logf("WRITE path OK on gateway %q: identity HAS the workspace admin role", gw) - case errors.Is(setErr, openshell.ErrPermission): - permissionDenied = true - t.Fatalf("WRITE path DENIED on gateway %q: identity LACKS the workspace admin role "+ - "(reconcile-write will fail until the mTLS identity is granted admin): %v", gw, setErr) - default: - t.Fatalf("SetInferenceRoute returned an unexpected error: %v", setErr) - } -} diff --git a/runner/internal/openshell/sdkclient/inference_test.go b/runner/internal/openshell/sdkclient/inference_test.go index 0b21d635..6c7e08b9 100644 --- a/runner/internal/openshell/sdkclient/inference_test.go +++ b/runner/internal/openshell/sdkclient/inference_test.go @@ -10,152 +10,19 @@ import ( "github.com/stackrox/harness-openshell/runner/internal/openshell" ) -// TestInferenceRoundTrip exercises the full get/set/update/delete lifecycle -// against the SDK fake through the real sdkclient mapping and translation. It -// pins the server-assigned version semantics (1 on create, monotonic on update) -// and that a missing route surfaces the harness ErrNotFound sentinel rather than -// a raw SDK error. -func TestInferenceRoundTrip(t *testing.T) { - ctx := context.Background() +func TestInferenceRoutesUnsupportedInOpenShellV012(t *testing.T) { c := NewFromClient(fake.NewClient(), "default") - - // Get on an empty gateway -> ErrNotFound (translated sentinel). - if _, err := c.GetInferenceRoute(ctx, ""); !errors.Is(err, openshell.ErrNotFound) { - t.Fatalf("GetInferenceRoute on empty gateway: want ErrNotFound, got %v", err) - } - - // Set creates the route at version 1. - created, err := c.SetInferenceRoute(ctx, openshell.InferenceRouteConfig{ - Provider: "gcp", - Model: "claude-opus-4-8", - NoVerify: true, - TimeoutSecs: 90, - }) - if err != nil { - t.Fatalf("SetInferenceRoute create: %v", err) - } - if created.Version != 1 { - t.Errorf("created version: want 1, got %d", created.Version) - } - if created.Provider != "gcp" || created.Model != "claude-opus-4-8" { - t.Errorf("created route mismatch: %+v", created) - } - if created.TimeoutSecs != 90 { - t.Errorf("created TimeoutSecs: want 90, got %d", created.TimeoutSecs) - } - - // Get returns the created route. - got, err := c.GetInferenceRoute(ctx, "") - if err != nil { - t.Fatalf("GetInferenceRoute after create: %v", err) - } - if got.Provider != created.Provider || got.Model != created.Model || got.Version != created.Version { - t.Errorf("round-trip mismatch: set %+v, got %+v", created, got) - } - - // Set with a changed model upserts and bumps the version to 2. - updated, err := c.SetInferenceRoute(ctx, openshell.InferenceRouteConfig{ - Provider: "gcp", - Model: "claude-sonnet-5", - NoVerify: true, - }) - if err != nil { - t.Fatalf("SetInferenceRoute update: %v", err) - } - if updated.Version != 2 { - t.Errorf("updated version: want 2, got %d", updated.Version) - } - if updated.Model != "claude-sonnet-5" { - t.Errorf("updated model: want claude-sonnet-5, got %q", updated.Model) - } - - // Delete removes the route; a subsequent get is ErrNotFound again. - if err := c.DeleteInferenceRoute(ctx, ""); err != nil { - t.Fatalf("DeleteInferenceRoute: %v", err) - } - if _, err := c.GetInferenceRoute(ctx, ""); !errors.Is(err, openshell.ErrNotFound) { - t.Fatalf("GetInferenceRoute after delete: want ErrNotFound, got %v", err) - } -} - -// TestInferenceDeleteIdempotent pins the firewall contract that deleting a -// missing route is not an error (mirrors the SDK's idempotent DeleteRoute). -func TestInferenceDeleteIdempotent(t *testing.T) { ctx := context.Background() - c := NewFromClient(fake.NewClient(), "default") - if err := c.DeleteInferenceRoute(ctx, ""); err != nil { - t.Fatalf("DeleteInferenceRoute on empty gateway: want nil, got %v", err) + if _, err := c.GetInferenceRoute(ctx, "inference.local"); !errors.Is(err, openshell.ErrUnsupported) { + t.Fatalf("GetInferenceRoute: want ErrUnsupported, got %v", err) } -} - -// TestInferenceErrorsTranslated proves all three methods route SDK errors -// through translate to harness sentinels (not raw SDK errors). It uses the -// closed-client trick — the same convention as TestHealthErrorTranslated / -// TestProvidersErrorTranslated — under which the fake returns ErrorUnavailable. -// Without this, a regression dropping translate() on the Set/Delete success -// wrappers would go unnoticed (their success paths never error). -func TestInferenceErrorsTranslated(t *testing.T) { - ctx := context.Background() - c := NewFromClient(fake.NewClient(), "default") - if err := c.Close(); err != nil { - t.Fatalf("Close: %v", err) - } - - if _, err := c.GetInferenceRoute(ctx, ""); !errors.Is(err, openshell.ErrUnavailable) { - t.Errorf("GetInferenceRoute on closed client: want ErrUnavailable, got %v", err) - } - if _, err := c.SetInferenceRoute(ctx, openshell.InferenceRouteConfig{ - Provider: "gcp", Model: "claude-opus-4-8", - }); !errors.Is(err, openshell.ErrUnavailable) { - t.Errorf("SetInferenceRoute on closed client: want ErrUnavailable, got %v", err) - } - if err := c.DeleteInferenceRoute(ctx, ""); !errors.Is(err, openshell.ErrUnavailable) { - t.Errorf("DeleteInferenceRoute on closed client: want ErrUnavailable, got %v", err) - } -} - -// TestInferenceInvalidArgumentTranslated proves user-supplied invalid input -// (a required field left empty) surfaces the harness ErrInvalidArgument sentinel -// rather than a raw SDK *StatusError. Inference is the first firewall caller -// whose required-field input can trigger this. -func TestInferenceInvalidArgumentTranslated(t *testing.T) { - ctx := context.Background() - c := NewFromClient(fake.NewClient(), "default") - - // Empty model (required) -> InvalidArgument from the SDK. if _, err := c.SetInferenceRoute(ctx, openshell.InferenceRouteConfig{ - Provider: "gcp", - }); !errors.Is(err, openshell.ErrInvalidArgument) { - t.Errorf("SetInferenceRoute with empty model: want ErrInvalidArgument, got %v", err) + Provider: "vertex", Model: "gemini-2.5-pro", + }); !errors.Is(err, openshell.ErrUnsupported) { + t.Fatalf("SetInferenceRoute: want ErrUnsupported, got %v", err) } -} - -// TestInferenceNamedRoute proves a non-default route name round-trips -// independently of the default route. -func TestInferenceNamedRoute(t *testing.T) { - ctx := context.Background() - c := NewFromClient(fake.NewClient(), "default") - - if _, err := c.SetInferenceRoute(ctx, openshell.InferenceRouteConfig{ - Provider: "gcp", - Model: "claude-opus-4-8", - Route: "scratch", - NoVerify: true, - }); err != nil { - t.Fatalf("SetInferenceRoute named: %v", err) - } - - got, err := c.GetInferenceRoute(ctx, "scratch") - if err != nil { - t.Fatalf("GetInferenceRoute named: %v", err) - } - if got.Route != "scratch" { - t.Errorf("route name: want scratch, got %q", got.Route) - } - - // The default route remains absent. - if _, err := c.GetInferenceRoute(ctx, ""); !errors.Is(err, openshell.ErrNotFound) { - t.Fatalf("default route should be absent: got %v", err) + if err := c.DeleteInferenceRoute(ctx, "inference.local"); !errors.Is(err, openshell.ErrUnsupported) { + t.Fatalf("DeleteInferenceRoute: want ErrUnsupported, got %v", err) } } diff --git a/runner/internal/openshell/sdkclient/sandbox.go b/runner/internal/openshell/sdkclient/sandbox.go index c7a5e5dc..9b317f18 100644 --- a/runner/internal/openshell/sdkclient/sandbox.go +++ b/runner/internal/openshell/sdkclient/sandbox.go @@ -29,7 +29,7 @@ func fromSDKSandbox(s *v1.Sandbox) openshell.Sandbox { // Sandboxes lists the sandboxes in the bound workspace. func (c *client) Sandboxes(ctx context.Context) ([]openshell.Sandbox, error) { - raw, err := c.raw.Sandboxes().List(ctx, c.workspace) + raw, err := c.raw.Sandboxes().ListAll(ctx, c.workspace) if err != nil { return nil, translate(err) } @@ -52,7 +52,8 @@ func (c *client) GetSandbox(ctx context.Context, name string) (openshell.Sandbox // DeleteSandbox removes the named sandbox in the bound workspace. func (c *client) DeleteSandbox(ctx context.Context, name string) error { - return translate(c.raw.Sandboxes().Delete(ctx, c.workspace, name)) + _, err := c.raw.Sandboxes().Delete(ctx, c.workspace, name) + return translate(err) } // CreateSandbox maps the harness-owned SDK-native creation subset to the diff --git a/runner/internal/openshell/sdkclient/sandbox_test.go b/runner/internal/openshell/sdkclient/sandbox_test.go index 4d7eaa90..ebc86824 100644 --- a/runner/internal/openshell/sdkclient/sandbox_test.go +++ b/runner/internal/openshell/sdkclient/sandbox_test.go @@ -22,7 +22,7 @@ import ( func TestFromSDKSandboxMapsNameAndPhase(t *testing.T) { got := fromSDKSandbox(&types.Sandbox{ Name: "agent-1", - Status: types.SandboxStatus{SandboxName: "echo-should-be-ignored", Phase: types.SandboxReady}, + Status: types.SandboxStatus{Phase: types.SandboxReady}, }) if got.Name != "agent-1" { t.Errorf("Name: got %q, want agent-1 (top-level Name, not Status.SandboxName)", got.Name) @@ -195,7 +195,7 @@ func TestCreateSandboxRejectsMalformedPolicy(t *testing.T) { if err == nil || !strings.Contains(err.Error(), `parsing sandbox policy: unknown policy field "unknown_field"`) { t.Fatalf("error = %v, want clear unknown policy field error", err) } - sandboxes, err := raw.Sandboxes().List(context.Background(), "team") + sandboxes, err := raw.Sandboxes().ListAll(context.Background(), "team") if err != nil || len(sandboxes) != 0 { t.Fatalf("sandbox created despite invalid policy: sandboxes=%v err=%v", sandboxes, err) } diff --git a/runner/internal/plan/plan_test.go b/runner/internal/plan/plan_test.go index 33e2e9c6..7b6e03bc 100644 --- a/runner/internal/plan/plan_test.go +++ b/runner/internal/plan/plan_test.go @@ -19,7 +19,7 @@ func TestBuild_TargetValidateWhenReachable(t *testing.T) { Reachable: true, Health: openshell.Health{ Healthy: true, - Version: "0.0.110", + Version: "0.1.2", }, } @@ -42,7 +42,7 @@ func TestBuild_TargetValidateWhenReachable(t *testing.T) { if res.Name != "test-gateway" { t.Errorf("expected name 'test-gateway', got %s", res.Name) } - if res.Detail != "gateway test-gateway v0.0.110" { + if res.Detail != "gateway test-gateway v0.1.2" { t.Errorf("unexpected detail: %s", res.Detail) } } @@ -112,7 +112,7 @@ func TestBuild_InferenceGroupWhenConfigured(t *testing.T) { current := CurrentState{ Inspected: true, Reachable: true, - Health: openshell.Health{Healthy: true, Version: "0.0.110"}, + Health: openshell.Health{Healthy: true, Version: "0.1.2"}, } plan := Build(desired, current) @@ -300,7 +300,7 @@ func TestBuild_NoInferenceGroupWhenEmpty(t *testing.T) { current := CurrentState{ Inspected: true, Reachable: true, - Health: openshell.Health{Healthy: true, Version: "0.0.110"}, + Health: openshell.Health{Healthy: true, Version: "0.1.2"}, } plan := Build(desired, current) @@ -326,7 +326,7 @@ func TestBuild_RunGroupWithSandbox(t *testing.T) { current := CurrentState{ Inspected: true, Reachable: true, - Health: openshell.Health{Healthy: true, Version: "0.0.110"}, + Health: openshell.Health{Healthy: true, Version: "0.1.2"}, } plan := Build(desired, current) @@ -382,7 +382,7 @@ func TestBuild_RunGroupWithPayloads(t *testing.T) { current := CurrentState{ Inspected: true, Reachable: true, - Health: openshell.Health{Healthy: true, Version: "0.0.110"}, + Health: openshell.Health{Healthy: true, Version: "0.1.2"}, } plan := Build(desired, current) @@ -426,7 +426,7 @@ func TestBuild_RunGroupWithAgent(t *testing.T) { Inspected: true, ProvidersKnown: true, Reachable: true, - Health: openshell.Health{Healthy: true, Version: "0.0.110"}, + Health: openshell.Health{Healthy: true, Version: "0.1.2"}, } plan := Build(desired, current) @@ -470,7 +470,7 @@ func TestBuild_NoRunGroupWhenEmpty(t *testing.T) { current := CurrentState{ Inspected: true, Reachable: true, - Health: openshell.Health{Healthy: true, Version: "0.0.110"}, + Health: openshell.Health{Healthy: true, Version: "0.1.2"}, } plan := Build(desired, current) @@ -493,7 +493,7 @@ func TestPlan_TableSections(t *testing.T) { Inspected: true, ProvidersKnown: true, Reachable: true, - Health: openshell.Health{Healthy: true, Version: "0.0.110"}, + Health: openshell.Health{Healthy: true, Version: "0.1.2"}, Providers: []openshell.Provider{}, } diff --git a/runner/internal/plan/render_test.go b/runner/internal/plan/render_test.go index ce45ee31..10ff8c55 100644 --- a/runner/internal/plan/render_test.go +++ b/runner/internal/plan/render_test.go @@ -39,7 +39,7 @@ func TestTableSections_RepresentativePlan(t *testing.T) { ProvidersKnown: true, Health: openshell.Health{ Healthy: true, - Version: "0.0.110", + Version: "0.1.2", }, Providers: []openshell.Provider{ {Name: "github", Type: "github"}, @@ -106,7 +106,7 @@ func TestPlan_JSONMarshal(t *testing.T) { current := CurrentState{ Inspected: true, Reachable: true, - Health: openshell.Health{Healthy: true, Version: "0.0.110"}, + Health: openshell.Health{Healthy: true, Version: "0.1.2"}, } plan := Build(desired, current) @@ -142,7 +142,7 @@ func TestPlan_YAMLMarshal(t *testing.T) { current := CurrentState{ Inspected: true, Reachable: true, - Health: openshell.Health{Healthy: true, Version: "0.0.110"}, + Health: openshell.Health{Healthy: true, Version: "0.1.2"}, } plan := Build(desired, current) diff --git a/runner/internal/plan/state_test.go b/runner/internal/plan/state_test.go index 19109318..236564a3 100644 --- a/runner/internal/plan/state_test.go +++ b/runner/internal/plan/state_test.go @@ -16,7 +16,7 @@ import ( func TestReadCurrentState_HealthyGateway(t *testing.T) { ctx := context.Background() client := testutil.NewFake("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) desired := &config.Harness{} @@ -34,8 +34,8 @@ func TestReadCurrentState_HealthyGateway(t *testing.T) { if !state.Health.Healthy { t.Error("expected Health.Healthy=true") } - if state.Health.Version != "0.0.110" { - t.Errorf("expected version 0.0.110, got %s", state.Health.Version) + if state.Health.Version != "0.1.2" { + t.Errorf("expected version 0.1.2, got %s", state.Health.Version) } if state.Inference.Capable { t.Error("expected Inference.Capable=false") @@ -45,7 +45,7 @@ func TestReadCurrentState_HealthyGateway(t *testing.T) { func TestReadCurrentState_ProvidersPopulated(t *testing.T) { ctx := context.Background() c, raw := testutil.NewFakeClient("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) raw.AddProvider("default", &types.Provider{Name: "github", Type: "github"}) raw.AddProvider("default", &types.Provider{Name: "gcp", Type: "google-vertex-ai"}) @@ -128,7 +128,7 @@ func TestReadCurrentState_OtherErrorEscalates(t *testing.T) { func TestReadCurrentState_InferenceNotReadWhenUnconfigured(t *testing.T) { ctx := context.Background() client := testutil.NewFake("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) desired := &config.Harness{} @@ -158,7 +158,7 @@ func inferenceDesired() *config.Harness { func TestReadCurrentState_InferencePresent(t *testing.T) { ctx := context.Background() client, _ := testutil.NewFakeClient("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) // Seed the route under the resolved default name so the read finds it. if _, err := client.SetInferenceRoute(ctx, openshell.InferenceRouteConfig{ @@ -187,7 +187,7 @@ func TestReadCurrentState_InferencePresent(t *testing.T) { func TestReadCurrentState_InferenceAbsent(t *testing.T) { ctx := context.Background() client := testutil.NewFake("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) state, err := ReadCurrentState(ctx, client, inferenceDesired()) @@ -206,7 +206,7 @@ func TestReadCurrentState_InferenceAbsent(t *testing.T) { func TestReadCurrentState_InferenceUnsupportedNotCapable(t *testing.T) { ctx := context.Background() base := testutil.NewFake("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) client := &inferenceErrClient{Client: base, getErr: openshell.ErrUnsupported} @@ -228,7 +228,7 @@ func TestReadCurrentState_InferenceTransientErrorKeepsReachable(t *testing.T) { ctx := context.Background() for _, transient := range []error{openshell.ErrUnavailable, openshell.ErrUnauthenticated} { base := testutil.NewFake("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) client := &inferenceErrClient{Client: base, getErr: transient} @@ -248,7 +248,7 @@ func TestReadCurrentState_InferenceTransientErrorKeepsReachable(t *testing.T) { func TestReadCurrentState_InferenceOtherErrorEscalates(t *testing.T) { ctx := context.Background() base := testutil.NewFake("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) client := &inferenceErrClient{Client: base, getErr: openshell.ErrPermission} @@ -272,7 +272,7 @@ func (c *inferenceErrClient) GetInferenceRoute(context.Context, string) (openshe func TestReadCurrentState_OnlyReadMethodsCalled(t *testing.T) { ctx := context.Background() client, fakeClient := testutil.NewFakeClient("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) fakeClient.AddProvider("default", &types.Provider{Name: "github", Type: "github"}) desired := &config.Harness{} diff --git a/runner/internal/reconcile/inference_test.go b/runner/internal/reconcile/inference_test.go index c3706b32..7c1c8c89 100644 --- a/runner/internal/reconcile/inference_test.go +++ b/runner/internal/reconcile/inference_test.go @@ -17,7 +17,7 @@ import ( func healthyClient(t *testing.T) (openshell.Client, *fake.Client) { t.Helper() return testutil.NewFakeClient("default", - fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.0.110"}), + fake.WithHealthResult(&types.HealthResult{Healthy: true, Version: "0.1.2"}), ) } diff --git a/runner/internal/testutil/fake_platform.go b/runner/internal/testutil/fake_platform.go index 0c195979..af1e0059 100644 --- a/runner/internal/testutil/fake_platform.go +++ b/runner/internal/testutil/fake_platform.go @@ -5,6 +5,8 @@ package testutil import ( "context" + "fmt" + "sync" fake "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/fake" @@ -13,9 +15,10 @@ import ( ) // NewFake returns an openshell.Client backed by the SDK fake, exercising the -// REAL sdkclient mapping/translation. Seed the fake via fake.With* options -// before construction. For tests that need to call fake.Client.AddProvider -// after construction, use NewFakeClient instead. +// real sdkclient mapping/translation for v0.1.2-supported resources. Legacy +// inference-route tests use the in-memory compatibility seam below. Seed the +// fake via fake.With* options before construction. For tests that need to call +// fake.Client.AddProvider after construction, use NewFakeClient instead. func NewFake(workspace string, opts ...fake.ClientOption) openshell.Client { c, _ := NewFakeClient(workspace, opts...) return c @@ -27,7 +30,55 @@ func NewFake(workspace string, opts ...fake.ClientOption) openshell.Client { // Implement NewFake in terms of this to avoid duplication. func NewFakeClient(workspace string, opts ...fake.ClientOption) (openshell.Client, *fake.Client) { raw := fake.NewClient(opts...) - return sdkclient.NewFromClient(raw, workspace), raw + return &fakeClient{ + Client: sdkclient.NewFromClient(raw, workspace), + routes: make(map[string]openshell.InferenceRoute), + }, raw +} + +// fakeClient keeps the harness's pre-v0.1 inference-route seam available to +// plan/reconcile unit tests. OpenShell v0.1.2 removed managed inference routes +// from the SDK; production sdkclient returns ErrUnsupported instead. +type fakeClient struct { + openshell.Client + mu sync.Mutex + routes map[string]openshell.InferenceRoute + version uint64 +} + +func (c *fakeClient) GetInferenceRoute(_ context.Context, route string) (openshell.InferenceRoute, error) { + c.mu.Lock() + defer c.mu.Unlock() + r, ok := c.routes[route] + if !ok { + return openshell.InferenceRoute{}, fmt.Errorf("%w: inference route %q", openshell.ErrNotFound, route) + } + return r, nil +} + +func (c *fakeClient) SetInferenceRoute(_ context.Context, cfg openshell.InferenceRouteConfig) (openshell.InferenceRoute, error) { + c.mu.Lock() + defer c.mu.Unlock() + if cfg.Provider == "" || cfg.Model == "" { + return openshell.InferenceRoute{}, fmt.Errorf("%w: provider and model are required", openshell.ErrInvalidArgument) + } + c.version++ + route := cfg.Route + c.routes[route] = openshell.InferenceRoute{ + Provider: cfg.Provider, + Model: cfg.Model, + Route: route, + TimeoutSecs: cfg.TimeoutSecs, + Version: c.version, + } + return c.routes[route], nil +} + +func (c *fakeClient) DeleteInferenceRoute(_ context.Context, route string) error { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.routes, route) + return nil } // FakeFactory returns a Factory closure that ignores its context and Target diff --git a/scripts/pr-review-local.sh b/scripts/pr-review-local.sh index 51683ce5..bd936837 100755 --- a/scripts/pr-review-local.sh +++ b/scripts/pr-review-local.sh @@ -5,6 +5,8 @@ umask 077 cd "$(dirname "$0")/.." : "${GOOGLE_VERTEX_AI_TOKEN:?set a short-lived Vertex token}" "${VERTEX_AI_PROJECT_ID:?set Vertex project}" : "${GITHUB_TOKEN:?set the repository-scoped GitHub App token for bootstrap}" +export VERTEX_AI_PROJECT_ID +export VERTEX_AI_REGION="${VERTEX_AI_REGION:-global}" gateway="${OPENSHELL_GATEWAY:-openshell}" # OpenShell limits resource names to 19 characters. workspace="review-$(openssl rand -hex 6)" @@ -55,8 +57,6 @@ created_vertex=true timeout 60s openshell provider create --gateway "$gateway" --workspace "$workspace" \ --name github-review --type github-review --credential GITHUB_TOKEN created_github=true -timeout 60s openshell inference set --gateway "$gateway" --workspace "$workspace" \ - --provider vertex-review --model gemini-2.5-pro OPENSHELL_GATEWAY="$gateway" OPENSHELL_WORKSPACE="$workspace" bash scripts/pr-review.sh run & review_pid=$! set +e diff --git a/scripts/review/agents/codex.sh b/scripts/review/agents/codex.sh index ac170c62..6aea2c87 100644 --- a/scripts/review/agents/codex.sh +++ b/scripts/review/agents/codex.sh @@ -50,10 +50,9 @@ agent_setup() { --name github-review --type github-review --credential GITHUB_TOKEN created_github_provider=true timeout 60s openshell provider create --gateway "$gateway" --workspace "$workspace" \ - --name "$codex_inference_provider" --type openai --credential OPENSHELL_CODEX_API_KEY + --name "$codex_inference_provider" --type openai \ + --credential "OPENAI_API_KEY=$OPENSHELL_CODEX_API_KEY" created_codex_provider=true - timeout 60s openshell inference set --gateway "$gateway" --workspace "$workspace" \ - --provider "$codex_inference_provider" --model "$codex_model" --no-verify } agent_workflow_file() { diff --git a/scripts/review/agents/opencode.sh b/scripts/review/agents/opencode.sh index b087620e..54a1d650 100644 --- a/scripts/review/agents/opencode.sh +++ b/scripts/review/agents/opencode.sh @@ -16,11 +16,17 @@ agent_configure() { } agent_require_credentials() { + export VERTEX_AI_REGION="${VERTEX_AI_REGION:-global}" if [[ "$configured_target" != true ]]; then : "${GITHUB_TOKEN:?set the workflow GitHub token for provider bootstrap}" : "${GOOGLE_VERTEX_AI_TOKEN:?set a short-lived Vertex token}" \ "${VERTEX_AI_PROJECT_ID:?set Vertex project}" fi + if [[ -n "${VERTEX_AI_PROJECT_ID:-}" ]]; then + local vertex_host="aiplatform.googleapis.com" + [[ "$VERTEX_AI_REGION" == global ]] || vertex_host="${VERTEX_AI_REGION}-aiplatform.googleapis.com" + export VERTEX_AI_BASE_URL="https://${vertex_host}/v1/projects/${VERTEX_AI_PROJECT_ID}/locations/${VERTEX_AI_REGION}/endpoints/openapi" + fi } agent_setup() { @@ -36,8 +42,6 @@ agent_setup() { timeout 60s openshell provider create --gateway "$gateway" --workspace "$workspace" \ --name "$github_provider" --type github --credential GITHUB_TOKEN created_github_provider=true - timeout 60s openshell inference set --gateway "$gateway" --workspace "$workspace" \ - --provider vertex-review --model gemini-2.5-pro --no-verify } agent_workflow_file() { diff --git a/tasks/README.md b/tasks/README.md index 49a31e17..cd42cc4f 100644 --- a/tasks/README.md +++ b/tasks/README.md @@ -20,7 +20,7 @@ optional version 1 harness workflow document. ## Composable execution Task bundles are composable through the version 1 workflow document. The -workflow combines the agent, inference route, sandbox policy, provider +workflow combines the agent, native provider endpoint, sandbox policy, provider attachments, payloads, source checkout, and outputs. The shared [`scripts/run-task.sh`](../scripts/run-task.sh) adapter executes one trusted workflow and captures its result. @@ -53,8 +53,8 @@ or commands from callers. Task bundles reference gateway provider instances; trusted setup or platform administration provisions them. The current reviewer creates temporary providers in [`scripts/pr-review-local.sh`](../scripts/pr-review-local.sh). A managed -deployment should establish workspace membership, provider credential -lifecycle, and matching inference routes centrally. Keeping provider names +deployment should establish workspace membership and provider credential +lifecycle centrally. Keeping provider names stable still requires a way to mint or refresh short-lived credentials. Preserve the task's allowed operations when moving to a managed gateway with equivalent provider and policy support. diff --git a/tasks/acs-ci-nightly/README.md b/tasks/acs-ci-nightly/README.md index ab6329c9..1b45125c 100644 --- a/tasks/acs-ci-nightly/README.md +++ b/tasks/acs-ci-nightly/README.md @@ -34,7 +34,8 @@ their sources, while the Jira updater and Slack publication remain disabled. The platform must provision these gateway-owned resources before applying the workflow: -- `vertex-claude-triage` and the matching `inference.local` route; +- `vertex-claude-triage`, attached to the sandbox and configured for Vertex's + native Claude endpoint; - `atlassian-triage-read`, configured for read-only Jira/Confluence access; - `github-triage-read`, configured for read-only project and issue queries; - `prow-gcs-read`, created from OpenShell's built-in `google-cloud` provider diff --git a/tasks/github-pr-merger/workflow/harness.yaml b/tasks/github-pr-merger/workflow/harness.yaml index 743fdd6b..a956cdfc 100644 --- a/tasks/github-pr-merger/workflow/harness.yaml +++ b/tasks/github-pr-merger/workflow/harness.yaml @@ -1,17 +1,13 @@ version: 1 name: github-pr-merger -inference: - route: inference.local - provider: vertex-review - model: gemini-2.5-pro sandbox: image: ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:aeef1c63f00e2913ea002ccb3aaf925f338b5c5d70e63576f0d95c16a138044e policy: file: ${MERGE_POLICY} - providers: [github-pr-merger] + providers: [github-pr-merger, vertex-review] env: OPENCODE_CONFIG: /sandbox/opencode.json - OPENCODE_VERTEX_API_KEY: sk-openshell-proxy-managed + VERTEX_AI_BASE_URL: ${VERTEX_AI_BASE_URL} MERGE_REPOSITORY: ${MERGE_REPOSITORY} MERGE_PR: ${MERGE_PR} MERGE_HEAD_SHA: ${MERGE_HEAD_SHA} diff --git a/tasks/github-pr-merger/workflow/opencode.json b/tasks/github-pr-merger/workflow/opencode.json index 1e059f62..8a3e1ac0 100644 --- a/tasks/github-pr-merger/workflow/opencode.json +++ b/tasks/github-pr-merger/workflow/opencode.json @@ -17,8 +17,8 @@ "npm": "@ai-sdk/openai-compatible", "name": "Vertex AI through OpenShell", "options": { - "baseURL": "https://inference.local/v1", - "apiKey": "{env:OPENCODE_VERTEX_API_KEY}" + "baseURL": "{env:VERTEX_AI_BASE_URL}", + "apiKey": "{env:GOOGLE_VERTEX_AI_TOKEN}" }, "models": { "gemini-2.5-pro": { diff --git a/tasks/github-pr-reviewer/README.md b/tasks/github-pr-reviewer/README.md index ccdb48a0..71929289 100644 --- a/tasks/github-pr-reviewer/README.md +++ b/tasks/github-pr-reviewer/README.md @@ -9,7 +9,7 @@ metadata and stage the diff as untrusted data. The task's initial composition is deliberately small: -- Codex inference through the gateway's `inference.local` route. +- Codex access through an attached native OpenAI provider. - Read-only GitHub pull-request access, plus inline comments on that exact PR. The shared [`scripts/run-task.sh`](../../scripts/run-task.sh) adapter executes @@ -29,9 +29,9 @@ those task-specific capabilities. temporary workspace from a repository-scoped GitHub App token. A managed integration must supply the instance and its credential lifecycle through trusted setup. The profile contains metadata only, never a credential. -- The OpenCode path must configure `inference.local` for the task's Gemini 2.5 - Pro model. The task consumes that route without reconciling it; the Codex - path uses its pre-provisioned OpenAI-compatible route instead. +- The OpenCode path attaches the Vertex provider and calls Vertex's native + OpenAI-compatible endpoint. The Codex path attaches its OpenAI provider and + uses the native Responses API endpoint. [`scripts/pr-review.sh`](../../scripts/pr-review.sh) prepares the review and delegates task execution to the shared adapter. The local wrapper supplies @@ -66,7 +66,6 @@ Upload the skill, diff, and OpenCode configuration with native `openshell sandbox upload` commands before starting the agent. The `harness` CLI automates this composition and cleanup. -The opt-in Codex variant uses the same policy and review skill. It requires a -pre-provisioned OpenAI-compatible OpenShell inference provider because Codex -uses the Responses API, and a dedicated workspace containing that provider; -the existing Vertex/OpenCode route remains unchanged. +The opt-in Codex variant uses the same policy and review skill. It requires an +OpenShell OpenAI provider because Codex uses the Responses API, and a dedicated +workspace containing that provider. diff --git a/tasks/github-pr-reviewer/openshell/README.md b/tasks/github-pr-reviewer/openshell/README.md index 769b6ea3..d98c657c 100644 --- a/tasks/github-pr-reviewer/openshell/README.md +++ b/tasks/github-pr-reviewer/openshell/README.md @@ -10,11 +10,11 @@ label, approval, merge, or repository-settings access. The sandbox must attach an existing `github-review` provider instance. The endpointless profile in `providers/github-review.yaml` describes the credential -shape but contains no credential value. The OpenCode workflow expects the -gateway's `vertex-review` inference provider. The opt-in Codex workflow expects -an existing OpenAI-compatible provider, named `openai-inference` by default. Both -use the platform-owned `inference.local` route and neither provider is created -by this workload. +shape but contains no credential value. The OpenCode workflow expects an +existing `vertex-review` provider, and the opt-in Codex workflow expects an +existing OpenAI provider, named `openai-inference` by default. Both providers +are attached directly to the sandbox; neither provider is created by this +workload. For a native run, import or adapt the profile, provision the provider through trusted OpenShell administration, attach it to the sandbox, and upload the diff --git a/tasks/github-pr-reviewer/openshell/policy.yaml b/tasks/github-pr-reviewer/openshell/policy.yaml index 239ad7bb..af04288d 100644 --- a/tasks/github-pr-reviewer/openshell/policy.yaml +++ b/tasks/github-pr-reviewer/openshell/policy.yaml @@ -18,8 +18,9 @@ landlock: process: run_as_user: sandbox run_as_group: sandbox -# inference.local is provided by the gateway. No direct GitHub, telemetry, -# package-registry, or other outbound destinations are granted to this worker. +# Provider profiles grant only the native model and GitHub endpoints attached to +# this sandbox. No unrelated telemetry, package-registry, or other outbound +# destinations are granted to this worker. network_policies: github_api: name: github-api diff --git a/tasks/github-pr-reviewer/workflow/codex-harness.yaml b/tasks/github-pr-reviewer/workflow/codex-harness.yaml index a15cbb0d..906b83c9 100644 --- a/tasks/github-pr-reviewer/workflow/codex-harness.yaml +++ b/tasks/github-pr-reviewer/workflow/codex-harness.yaml @@ -1,18 +1,13 @@ version: 1 name: ${REVIEW_SANDBOX_NAME} -inference: - route: inference.local - provider: ${CODEX_INFERENCE_PROVIDER} - model: ${CODEX_MODEL} sandbox: # This published digest contains the pinned Codex CLI and the OpenShell # sandbox contract. The outer policy remains the authority for egress. image: quay.io/rcochran/openshell:sandbox@sha256:d0f4abc84ba314e12e4e40526db55faf8e6ee6009670e17e187f0c24c428a5b5 policy: file: ${REVIEW_POLICY} - providers: ["${REVIEW_GITHUB_PROVIDER}"] + providers: ["${REVIEW_GITHUB_PROVIDER}", "${CODEX_INFERENCE_PROVIDER}"] env: - CODEX_API_KEY: unused CODEX_HOME: /sandbox/.codex REVIEW_REPOSITORY: ${REVIEW_REPOSITORY} REVIEW_PR: ${REVIEW_PR} @@ -25,17 +20,17 @@ payloads: destination: /sandbox/review/skills/pr-review/SKILL.md - content: | model = "${CODEX_MODEL}" - model_provider = "openshell" + model_provider = "openai" model_reasoning_effort = "xhigh" approval_policy = "never" sandbox_mode = "danger-full-access" - [model_providers.openshell] - name = "OpenShell inference" - base_url = "https://inference.local/v1" + [model_providers.openai] + name = "OpenAI" + base_url = "https://api.openai.com/v1" wire_api = "responses" supports_websockets = false - requires_openai_auth = false + requires_openai_auth = true destination: /sandbox/.codex/config.toml outputs: - source: /sandbox/review/codex-final.txt diff --git a/tasks/github-pr-reviewer/workflow/harness.yaml b/tasks/github-pr-reviewer/workflow/harness.yaml index b8e8c030..3fd96958 100644 --- a/tasks/github-pr-reviewer/workflow/harness.yaml +++ b/tasks/github-pr-reviewer/workflow/harness.yaml @@ -1,11 +1,8 @@ version: 1 name: github-pr-review -inference: - route: inference.local - provider: vertex-claude-haiku - model: claude-haiku-4-5@20251001 sandbox: image: ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:aeef1c63f00e2913ea002ccb3aaf925f338b5c5d70e63576f0d95c16a138044e + providers: [vertex-claude-haiku] payloads: - source: REVIEW.md destination: /sandbox/REVIEW.md diff --git a/tasks/github-pr-reviewer/workflow/opencode-harness.yaml b/tasks/github-pr-reviewer/workflow/opencode-harness.yaml index 80b8a470..c79de95d 100644 --- a/tasks/github-pr-reviewer/workflow/opencode-harness.yaml +++ b/tasks/github-pr-reviewer/workflow/opencode-harness.yaml @@ -1,17 +1,13 @@ version: 1 name: ${REVIEW_SANDBOX_NAME} -inference: - route: inference.local - provider: vertex-review - model: gemini-2.5-pro sandbox: image: ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:aeef1c63f00e2913ea002ccb3aaf925f338b5c5d70e63576f0d95c16a138044e policy: file: ${REVIEW_POLICY} - providers: ["${REVIEW_GITHUB_PROVIDER}"] + providers: ["${REVIEW_GITHUB_PROVIDER}", "vertex-review"] env: OPENCODE_CONFIG: /sandbox/opencode-review.json - OPENCODE_VERTEX_API_KEY: sk-openshell-proxy-managed + VERTEX_AI_BASE_URL: ${VERTEX_AI_BASE_URL} REVIEW_REPOSITORY: ${REVIEW_REPOSITORY} REVIEW_PR: ${REVIEW_PR} REVIEW_HEAD: ${REVIEW_HEAD} diff --git a/tasks/github-pr-reviewer/workflow/opencode-review.json b/tasks/github-pr-reviewer/workflow/opencode-review.json index e71b878e..e1898c1a 100644 --- a/tasks/github-pr-reviewer/workflow/opencode-review.json +++ b/tasks/github-pr-reviewer/workflow/opencode-review.json @@ -17,8 +17,8 @@ "npm": "@ai-sdk/openai-compatible", "name": "Vertex AI through OpenShell", "options": { - "baseURL": "https://inference.local/v1", - "apiKey": "{env:OPENCODE_VERTEX_API_KEY}" + "baseURL": "{env:VERTEX_AI_BASE_URL}", + "apiKey": "{env:GOOGLE_VERTEX_AI_TOKEN}" }, "models": { "gemini-2.5-pro": { diff --git a/test/github-pr-reviewer-local.sh b/test/github-pr-reviewer-local.sh index fa1daed9..baf7d162 100755 --- a/test/github-pr-reviewer-local.sh +++ b/test/github-pr-reviewer-local.sh @@ -2,7 +2,7 @@ # Run the deterministic PR reviewer fixture against a local OpenShell gateway. # # This is intentionally local-only: it consumes the preconfigured inference -# route and grants the sandbox no GitHub write capability. +# provider and grants the sandbox no GitHub write capability. set -uo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -12,7 +12,7 @@ WORKFLOW="$ROOT/tasks/github-pr-reviewer/workflow/harness.yaml" EXPECTED="PR_REVIEW_OK sha=fixture-pr-head-20260908" if [[ "${CI:-}" == "true" ]]; then - echo "SKIP: PR reviewer fixture requires a locally reachable inference gateway." + echo "SKIP: PR reviewer fixture requires a locally reachable OpenShell gateway." exit 0 fi [[ -x "$HARNESS" ]] || { echo "ERROR: run make cli first" >&2; exit 1; } diff --git a/test/hypershell-haiku-workflow.yaml b/test/hypershell-haiku-workflow.yaml index c59ae467..7784fde5 100644 --- a/test/hypershell-haiku-workflow.yaml +++ b/test/hypershell-haiku-workflow.yaml @@ -9,20 +9,19 @@ target: issuer: ${HYPERSHELL_OIDC_ISSUER} clientId: ${HYPERSHELL_SANDBOX_SA_ID} audience: ${HYPERSHELL_OIDC_AUDIENCE} -inference: - route: inference.local - provider: vertex-claude-haiku - model: claude-haiku-4-5@20251001 sandbox: image: ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:aeef1c63f00e2913ea002ccb3aaf925f338b5c5d70e63576f0d95c16a138044e + providers: [vertex-claude-haiku] env: - ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed - ANTHROPIC_BASE_URL: https://inference.local + CLAUDE_CODE_USE_VERTEX: "1" + CLAUDE_CODE_SKIP_VERTEX_AUTH: "1" + ANTHROPIC_VERTEX_PROJECT_ID: ${HYPERSHELL_VERTEX_PROJECT_ID} + CLOUD_ML_REGION: ${HYPERSHELL_VERTEX_REGION} CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" agent: - type: claude + type: sh args: - - --print - - --model - - haiku - - Respond with exactly HYPERSHELL_HAIKU_OK and nothing else. + - -c + - | + export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_TOKEN" + exec claude --print --model claude-haiku-4-5@20251001 "Respond with exactly HYPERSHELL_HAIKU_OK and nothing else." diff --git a/test/hypershell-lifecycle.sh b/test/hypershell-lifecycle.sh index 66a20501..60ee66a7 100755 --- a/test/hypershell-lifecycle.sh +++ b/test/hypershell-lifecycle.sh @@ -49,6 +49,8 @@ export HYPERSHELL_OIDC_ISSUER="${OPENSHELL_OIDC_ISSUER:-}" export HYPERSHELL_OIDC_AUDIENCE="${OPENSHELL_OIDC_AUDIENCE:-}" export HYPERSHELL_SANDBOX_SA_ID="${OPENSHELL_OIDC_CLIENT_ID:-}" export OPENSHELL_OIDC_CLIENT_SECRET="${OPENSHELL_OIDC_CLIENT_SECRET:-}" +export HYPERSHELL_VERTEX_PROJECT_ID="${HYPERSHELL_VERTEX_PROJECT_ID:-}" +export HYPERSHELL_VERTEX_REGION="${HYPERSHELL_VERTEX_REGION:-}" miss=() [[ -n "$HYPERSHELL_GATEWAY" ]] || miss+=(HYPERSHELL_GATEWAY) @@ -56,6 +58,10 @@ miss=() [[ -n "$HYPERSHELL_OIDC_AUDIENCE" ]] || miss+=(OPENSHELL_OIDC_AUDIENCE) [[ -n "$HYPERSHELL_SANDBOX_SA_ID" ]] || miss+=(OPENSHELL_OIDC_CLIENT_ID) [[ -n "$OPENSHELL_OIDC_CLIENT_SECRET" ]] || miss+=(OPENSHELL_OIDC_CLIENT_SECRET) +if [[ "${HYPERSHELL_WORKFLOW_FILE##*/}" == hypershell-haiku-workflow.yaml ]]; then + [[ -n "$HYPERSHELL_VERTEX_PROJECT_ID" ]] || miss+=(HYPERSHELL_VERTEX_PROJECT_ID) + [[ -n "$HYPERSHELL_VERTEX_REGION" ]] || miss+=(HYPERSHELL_VERTEX_REGION) +fi ((${#miss[@]}==0)) || { echo "ERROR: $HYPERSHELL_SA_ENV is missing: ${miss[*]}" >&2; exit 1; } secret_state() { [[ -n "${1:-}" ]] && printf 'set (%d chars)' "${#1}" || printf 'MISSING'; } diff --git a/test/lib/provision.sh b/test/lib/provision.sh index fb156a14..c8c5f6c6 100644 --- a/test/lib/provision.sh +++ b/test/lib/provision.sh @@ -25,7 +25,7 @@ _chart_version() { local root ver root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" ver="$(cat "$root/.openshell-version" 2>/dev/null | tr -d 'v[:space:]')" - echo "${ver:-0.0.110}" + echo "${ver:-0.1.2}" } # provision_local: the OpenShell installer already provisioned and started the @@ -41,7 +41,7 @@ provision_local() { "$CLI" gateway select "$gw" || return 1 local i for i in $(seq 1 5); do - "$CLI" inference get &>/dev/null && return 0 + "$CLI" status &>/dev/null && return 0 sleep 3 done echo " ERROR: local gateway $gw not responding" >&2 @@ -95,7 +95,7 @@ EOF "$CLI" gateway select openshell-kind || return 1 for i in $(seq 1 30); do - "$CLI" inference get &>/dev/null && return 0 + "$CLI" status &>/dev/null && return 0 sleep 2 done echo " ERROR: kind gateway not reachable after 60s" >&2 @@ -190,7 +190,7 @@ EOF "$CLI" gateway select openshell-remote-ocp || return 1 for i in $(seq 1 30); do - "$CLI" inference get &>/dev/null && return 0 + "$CLI" status &>/dev/null && return 0 sleep 2 done echo " ERROR: OCP gateway not reachable after 60s" >&2 diff --git a/test/pr_review_test.go b/test/pr_review_test.go index d09ea178..7efc4576 100644 --- a/test/pr_review_test.go +++ b/test/pr_review_test.go @@ -146,7 +146,7 @@ func TestPRReview(t *testing.T) { } if !local { if scenario != "codex-success" && scenario != "codex-bootstrap-success" { - for _, action := range []string{"workspace create", "provider create", "inference set"} { + for _, action := range []string{"workspace create", "provider create"} { if strings.Contains(string(trace), action) { t.Fatalf("existing-target review performed setup or forced a target: %s", trace) } @@ -159,7 +159,7 @@ func TestPRReview(t *testing.T) { t.Fatal("Codex review wrapper attempted to clean up a sandbox it did not create") } if scenario == "codex-bootstrap-success" { - for _, action := range []string{"workspace create", "provider profile import", "provider create", "inference set", "workspace delete"} { + for _, action := range []string{"workspace create", "provider profile import", "provider create", "workflow apply", "workspace delete"} { if !strings.Contains(string(trace), action) { t.Fatalf("Codex bootstrap did not perform %s: %s", action, trace) } @@ -419,18 +419,18 @@ func TestGitHubAppTokenIsHostOnly(t *testing.T) { if err := yaml.Unmarshal(data, &task); err != nil { t.Fatal(err) } - if inference, ok := task["inference"].(map[string]any); !ok || inference["route"] != "inference.local" { - t.Fatal("review task must declare the inference.local route") + if _, ok := task["inference"]; ok { + t.Fatal("review task must not declare the removed inference route") } example := string(data) - if !strings.Contains(example, `providers: ["${REVIEW_GITHUB_PROVIDER}"]`) { - t.Fatal("review workflow does not attach the native GitHub provider") + if !strings.Contains(example, `providers: ["${REVIEW_GITHUB_PROVIDER}", "vertex-review"]`) { + t.Fatal("review workflow does not attach the GitHub and Vertex providers") } if strings.Contains(example, "GITHUB_TOKEN") { t.Fatal("review workflow passes the GitHub token into the sandbox configuration") } codex := string(mustRead(t, "../tasks/github-pr-reviewer/workflow/codex-harness.yaml")) - for _, required := range []string{"type: codex", "CODEX_INFERENCE_PROVIDER", "CODEX_MODEL", "${CODEX_MODEL}", "model_provider = \"openshell\"", "model_reasoning_effort = \"xhigh\"", "approval_policy = \"never\"", "sandbox_mode = \"danger-full-access\"", "base_url = \"https://inference.local/v1\"", "supports_websockets = false", "codex-final.txt"} { + for _, required := range []string{"type: codex", "CODEX_INFERENCE_PROVIDER", "CODEX_MODEL", "${CODEX_MODEL}", "model_provider = \"openai\"", "model_reasoning_effort = \"xhigh\"", "approval_policy = \"never\"", "sandbox_mode = \"danger-full-access\"", "base_url = \"https://api.openai.com/v1\"", "supports_websockets = false", "codex-final.txt"} { if !strings.Contains(codex, required) { t.Fatalf("Codex workflow is missing %s", required) } diff --git a/test/suite/run.sh b/test/suite/run.sh index 2e5a9409..8dc245bb 100755 --- a/test/suite/run.sh +++ b/test/suite/run.sh @@ -72,7 +72,7 @@ echo "=== Workflow plan ===" run_test "plan: table has all sections" bash -c 'out=$("$1" workflow plan -f "$2"); for section in TARGET PROVIDERS INFERENCE RUN; do grep -q "$section" <<<"$out" || exit 1; done' _ "$HARNESS" "$CONFIG" run_test "plan: JSON" bash -c '"$1" workflow plan -f "$2" -o json | python3 -m json.tool >/dev/null' _ "$HARNESS" "$CONFIG" run_test "plan: YAML" bash -c '"$1" workflow plan -f "$2" -o yaml | grep -q "section: providers"' _ "$HARNESS" "$CONFIG" -if $LIVE && "$CLI" inference get >/dev/null 2>&1; then +if $LIVE && "$CLI" status >/dev/null 2>&1; then echo "=== Live SDK lifecycle ===" run_test "live: create and retain" "$HARNESS" workflow apply "$LIFECYCLE" --name suite-sdk-live run_test "live: describe" "$CLI" sandbox get suite-sdk-live diff --git a/test/vertex-gemini-opencode-workflow.yaml b/test/vertex-gemini-opencode-workflow.yaml index 745ca3b1..76cb5d26 100644 --- a/test/vertex-gemini-opencode-workflow.yaml +++ b/test/vertex-gemini-opencode-workflow.yaml @@ -1,14 +1,11 @@ version: 1 name: vertex-gemini -inference: - route: inference.local - provider: vertex-ci - model: gemini-2.5-pro sandbox: image: ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:aeef1c63f00e2913ea002ccb3aaf925f338b5c5d70e63576f0d95c16a138044e + providers: [vertex-ci] env: OPENCODE_CONFIG: /sandbox/opencode-vertex.json - OPENCODE_VERTEX_API_KEY: openshell-proxy-managed + VERTEX_AI_BASE_URL: ${VERTEX_AI_BASE_URL} payloads: - destination: /sandbox/opencode-vertex.json content: | @@ -19,8 +16,8 @@ payloads: "npm": "@ai-sdk/openai-compatible", "name": "Vertex AI through OpenShell", "options": { - "baseURL": "https://inference.local/v1", - "apiKey": "{env:OPENCODE_VERTEX_API_KEY}" + "baseURL": "{env:VERTEX_AI_BASE_URL}", + "apiKey": "{env:GOOGLE_VERTEX_AI_TOKEN}" }, "models": { "gemini-2.5-pro": { diff --git a/test/vertex-gemini-opencode.sh b/test/vertex-gemini-opencode.sh index d14b6845..dc7c40cb 100755 --- a/test/vertex-gemini-opencode.sh +++ b/test/vertex-gemini-opencode.sh @@ -59,13 +59,13 @@ GOOGLE_VERTEX_AI_TOKEN="$TOKEN" \ --config "VERTEX_AI_REGION=$REGION" created_provider=true -"$CLI" inference set \ - --gateway "$GATEWAY" \ - --workspace "$WORKSPACE" \ - --provider "$PROVIDER" \ - --model gemini-2.5-pro - output_file="$(mktemp)" +vertex_host="aiplatform.googleapis.com" +[[ "$REGION" == global ]] || vertex_host="${REGION}-aiplatform.googleapis.com" +export VERTEX_AI_BASE_URL="https://${vertex_host}/v1/projects/${PROJECT}/locations/${REGION}/endpoints/openapi" +VERTEX_AI_PROJECT_ID="$PROJECT" \ +VERTEX_AI_REGION="$REGION" \ +GOOGLE_VERTEX_AI_TOKEN="$TOKEN" \ "$HARNESS" workflow apply "$WORKFLOW" --gateway "$GATEWAY" --workspace "$WORKSPACE" >"$output_file" & apply_pid=$! status=0 From 90a043309fe603df09144a2b2732ceedc3666b87 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 1 Oct 2026 09:47:30 -0700 Subject: [PATCH 2/7] fix: complete OpenShell 0.1.2 integration migration --- Makefile | 2 +- docs/ci.md | 25 +++++++++++-------- images/stackrox/README.md | 6 ++--- .../sandbox-collector-builder/CLAUDE.md | 2 +- images/stackrox/sandbox-default/CLAUDE.md | 2 +- images/stackrox/sandbox-default/policy.yaml | 6 ++--- images/stackrox/sandbox-stackrox-ci/CLAUDE.md | 2 +- runner/internal/run/runner.go | 2 +- scripts/review/agents/codex.sh | 4 +-- scripts/review/agents/opencode.sh | 2 ++ tasks/acs-ci-nightly/README.md | 3 ++- tasks/acs-ci-nightly/openshell/policy.yaml | 4 --- tasks/acs-ci-nightly/workflow/harness.yaml | 18 +++++++------ tasks/basic/workflow/harness.yaml | 19 +++++++++++--- tasks/github-pr-merger/openshell/README.md | 7 ++++-- tasks/github-pr-merger/openshell/policy.yaml | 1 - tasks/github-pr-merger/workflow/harness.yaml | 2 +- tasks/github-pr-reviewer/README.md | 4 +++ .../github-pr-reviewer/openshell/policy.yaml | 1 - .../github-pr-reviewer/workflow/harness.yaml | 18 ++++++++++--- test/ci-workflow.yaml | 2 ++ test/hypershell-haiku-workflow.yaml | 7 +++++- test/hypershell-lifecycle.sh | 2 +- test/lib/provision.sh | 15 +++++++++-- test/pr_review_test.go | 2 +- test/suite/run.sh | 2 +- 26 files changed, 105 insertions(+), 55 deletions(-) diff --git a/Makefile b/Makefile index fc38c33a..922e3935 100644 --- a/Makefile +++ b/Makefile @@ -119,7 +119,7 @@ test-hypershell-haiku: cli HYPERSHELL_EXPECTED_MARKER=HYPERSHELL_HAIKU_OK \ ./test/hypershell-lifecycle.sh -## Local PR reviewer fixture (requires a configured local inference route) +## Local PR reviewer fixture (requires a configured Vertex provider) test-pr-reviewer-local: cli ./test/github-pr-reviewer-local.sh diff --git a/docs/ci.md b/docs/ci.md index f3efcdf1..c644cc01 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -166,8 +166,9 @@ The Codex task fixes reasoning effort to `xhigh`. The outer OpenShell policy continues to control filesystem and GitHub egress, and the Codex path does not require the Vertex service-account secret. -The local wrapper needs a reachable local gateway and a repository-scoped -`GITHUB_TOKEN` for provider bootstrap, in addition to the Vertex variables. +The OpenCode local wrapper needs a reachable local gateway, a repository-scoped +`GITHUB_TOKEN`, and the Vertex variables for provider bootstrap. The Codex +bootstrap path needs `GITHUB_TOKEN` and `OPENSHELL_CODEX_API_KEY`. It creates a fresh workspace, runs the review, and removes its setup resources. A setup or teardown failure fails the command. @@ -178,9 +179,10 @@ preparation instead. Select a registered gateway/workspace with connection (see [workflow contract](#workflow-contract)). The review command uses the selected target and only creates its task sandbox. It needs host `gh` authentication for PR checks, while the platform supplies the `github-review` -provider with usable credentials. OpenCode requires the Gemini 2.5 Pro inference -route; Codex requires the configured `CODEX_INFERENCE_PROVIDER` (default: -`openai-inference`). +provider with usable credentials. OpenCode also needs an attached Vertex +provider and either `VERTEX_AI_PROJECT_ID` (to construct the native endpoint) +or `VERTEX_AI_BASE_URL` for an already configured target. Codex requires the +configured `CODEX_INFERENCE_PROVIDER` (default: `openai-inference`). Unit tests use fake commands, not Vertex. The agent can already publish inline comments directly through the allowed API endpoint. A structured findings @@ -193,7 +195,7 @@ The [reusable workflow](../.github/workflows/pr-review-reusable.yml) invokes the pinned OpenShell CLI and waits for gateway readiness. This CI path uses a local gateway. [`scripts/pr-review-local.sh`](../scripts/pr-review-local.sh) creates a temporary workspace, registers `github-review` and `vertex-review`, -and configures inference. It calls [`pr-review.sh run`](../scripts/pr-review.sh), +and sets the native Vertex endpoint. It calls [`pr-review.sh run`](../scripts/pr-review.sh), then removes the providers, any profile it imported, and the workspace. `pr-review.sh` handles PR checks, diff preparation, policy rendering, and output @@ -220,8 +222,8 @@ agreed managed integration contract: minting or refresh, repository and permission selection, and credential replacement or expiry. Pre-provisioning a provider name does not keep an expired installation token usable. -- **Inference and policy:** a matching provider/model route and equivalent - policy enforcement, so ordinary task runs can use existing references. +- **Inference and policy:** an attached provider, the matching native agent + endpoint and model, and equivalent policy enforcement. Once these requirements are met, replace `setup-openshell`, Google bootstrap, and `pr-review-local.sh` in the job with managed authentication and @@ -275,9 +277,10 @@ openshell provider get --gateway ADMIN_GATEWAY \ ``` After bootstrap, ordinary applies only read the matching provider and attach it -to the sandbox; they neither need workspace-admin permission nor receive the -Vertex credential in the sandbox. Model selection and request timeouts belong -to the native agent client. +to the sandbox; they neither need workspace-admin permission nor receive raw +Vertex credentials. OpenShell projects an opaque token placeholder that the +gateway resolves for authorized Vertex requests. Model selection and request +timeouts belong to the native agent client. Validate from the VPN with: diff --git a/images/stackrox/README.md b/images/stackrox/README.md index d2f795c6..5a3315c7 100644 --- a/images/stackrox/README.md +++ b/images/stackrox/README.md @@ -5,7 +5,7 @@ repository-specific tools. Providers, credentials, skills supplied by a workflow, and task-specific policy remain outside the image. The image does not create or attach providers; a workflow must name providers that are already provisioned and attach them through `sandbox.providers` before -provider credentials or inference routes are available. +their native endpoints and proxy-resolved credential placeholders are available. ## Images @@ -32,8 +32,8 @@ GitHub skill, Atlassian MCP, Google Workspace CLI, and the `gopls` MCP server. Go module and build caches stay below `/sandbox`. It includes a pinned `gcloud` CLI for read-only Prow result analysis and retains a root-owned, isolated Python 3.13 `gsutil` environment for legacy workflows. OpenShell -providers own credentials and inference routes; no service-account keys are -copied into the sandbox. The task policy allows only its fixed executables, +providers own credentials and authorize native endpoints; no service-account +keys are copied into the sandbox. The task policy allows only its fixed executables, not a writable `/sandbox` subtree. The `rox-ci-image` build currently provides an amd64 toolchain, so this profile diff --git a/images/stackrox/sandbox-collector-builder/CLAUDE.md b/images/stackrox/sandbox-collector-builder/CLAUDE.md index edeb2653..3c0c72c4 100644 --- a/images/stackrox/sandbox-collector-builder/CLAUDE.md +++ b/images/stackrox/sandbox-collector-builder/CLAUDE.md @@ -8,7 +8,7 @@ are not part of the image. - Working directory: `/sandbox` - Writable paths: `/sandbox`, `/tmp` -- Inference routes through the gateway proxy at `inference.local` +- Model requests use the native endpoint authorized by an attached provider - Repository build tools from the `collector-builder` build are available, including compilers, make, git, jq, and the StackRox CI toolchain. This profile adds the pinned Go toolchain and `gopls` for Go repository analysis. diff --git a/images/stackrox/sandbox-default/CLAUDE.md b/images/stackrox/sandbox-default/CLAUDE.md index ff23673a..992cade1 100644 --- a/images/stackrox/sandbox-default/CLAUDE.md +++ b/images/stackrox/sandbox-default/CLAUDE.md @@ -6,7 +6,7 @@ You are running inside an OpenShell sandbox. Credentials are injected via the Op - Working directory: `/sandbox` - Writable paths: `/sandbox`, `/tmp` -- Inference routes through the gateway proxy at `inference.local` +- Model requests use the native endpoint authorized by an attached provider - Credentials are managed by OpenShell and cleaned up on sandbox exit ## Tools diff --git a/images/stackrox/sandbox-default/policy.yaml b/images/stackrox/sandbox-default/policy.yaml index 018fd27a..59e200cf 100644 --- a/images/stackrox/sandbox-default/policy.yaml +++ b/images/stackrox/sandbox-default/policy.yaml @@ -9,7 +9,7 @@ version: 1 # # Active providers: # github (builtin) → api.github.com, github.com (read-only) -# google-vertex-ai → Vertex AI + Google OAuth (inference.local routing) +# google-vertex-ai → Vertex AI native endpoints + Google OAuth # atlassian (custom profile) → *.atlassian.net, *.atlassian.com (Basic auth resolved by proxy) # # Endpoints in this file (not covered by any provider profile): @@ -37,8 +37,7 @@ process: run_as_group: sandbox network_policies: - # Agent telemetry (inference goes through inference.local, - # but telemetry/updates still hit upstream directly) + # Agent telemetry and updates (model requests use attached provider endpoints) agent_telemetry: name: agent-telemetry endpoints: @@ -65,7 +64,6 @@ network_policies: - host: github.com port: 443 protocol: rest - tls: terminate enforcement: enforce rules: - allow: diff --git a/images/stackrox/sandbox-stackrox-ci/CLAUDE.md b/images/stackrox/sandbox-stackrox-ci/CLAUDE.md index ed9c9e6c..29727b4d 100644 --- a/images/stackrox/sandbox-stackrox-ci/CLAUDE.md +++ b/images/stackrox/sandbox-stackrox-ci/CLAUDE.md @@ -8,7 +8,7 @@ are not part of the image. - Working directory: `/sandbox` - Writable paths: `/sandbox`, `/tmp` -- Inference routes through the gateway proxy at `inference.local` +- Model requests use the native endpoint authorized by an attached provider - Repository build tools from the `rox-ci-image` build are available, including Go, compilers, make, git, jq, and the StackRox CI toolchain. diff --git a/runner/internal/run/runner.go b/runner/internal/run/runner.go index 7269ddc2..db5316c8 100644 --- a/runner/internal/run/runner.go +++ b/runner/internal/run/runner.go @@ -27,7 +27,7 @@ func Run(ctx context.Context, client openshell.SandboxExecutionClient, req Sandb } if !req.Keep { defer func() { - cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) defer cancel() if err := client.DeleteSandbox(cleanupCtx, req.Name); err != nil { cleanupErr := fmt.Errorf("deleting sandbox %q: %w", req.Name, err) diff --git a/scripts/review/agents/codex.sh b/scripts/review/agents/codex.sh index 6aea2c87..c1506ea2 100644 --- a/scripts/review/agents/codex.sh +++ b/scripts/review/agents/codex.sh @@ -49,9 +49,9 @@ agent_setup() { timeout 60s openshell provider create --gateway "$gateway" --workspace "$workspace" \ --name github-review --type github-review --credential GITHUB_TOKEN created_github_provider=true - timeout 60s openshell provider create --gateway "$gateway" --workspace "$workspace" \ + OPENAI_API_KEY="$OPENSHELL_CODEX_API_KEY" timeout 60s openshell provider create --gateway "$gateway" --workspace "$workspace" \ --name "$codex_inference_provider" --type openai \ - --credential "OPENAI_API_KEY=$OPENSHELL_CODEX_API_KEY" + --credential OPENAI_API_KEY created_codex_provider=true } diff --git a/scripts/review/agents/opencode.sh b/scripts/review/agents/opencode.sh index 54a1d650..023b855d 100644 --- a/scripts/review/agents/opencode.sh +++ b/scripts/review/agents/opencode.sh @@ -26,6 +26,8 @@ agent_require_credentials() { local vertex_host="aiplatform.googleapis.com" [[ "$VERTEX_AI_REGION" == global ]] || vertex_host="${VERTEX_AI_REGION}-aiplatform.googleapis.com" export VERTEX_AI_BASE_URL="https://${vertex_host}/v1/projects/${VERTEX_AI_PROJECT_ID}/locations/${VERTEX_AI_REGION}/endpoints/openapi" + else + export VERTEX_AI_BASE_URL="${VERTEX_AI_BASE_URL:?set VERTEX_AI_BASE_URL for the pre-provisioned Vertex provider}" fi } diff --git a/tasks/acs-ci-nightly/README.md b/tasks/acs-ci-nightly/README.md index 1b45125c..9ae48d6b 100644 --- a/tasks/acs-ci-nightly/README.md +++ b/tasks/acs-ci-nightly/README.md @@ -35,7 +35,8 @@ The platform must provision these gateway-owned resources before applying the workflow: - `vertex-claude-triage`, attached to the sandbox and configured for Vertex's - native Claude endpoint; + native Claude endpoint. The workflow passes only the provider's projected + token placeholder to Claude Code; OpenShell resolves it at the gateway; - `atlassian-triage-read`, configured for read-only Jira/Confluence access; - `github-triage-read`, configured for read-only project and issue queries; - `prow-gcs-read`, created from OpenShell's built-in `google-cloud` provider diff --git a/tasks/acs-ci-nightly/openshell/policy.yaml b/tasks/acs-ci-nightly/openshell/policy.yaml index c4e1edea..a067f872 100644 --- a/tasks/acs-ci-nightly/openshell/policy.yaml +++ b/tasks/acs-ci-nightly/openshell/policy.yaml @@ -31,7 +31,6 @@ network_policies: - host: github.com port: 443 protocol: rest - tls: terminate enforcement: enforce rules: - allow: @@ -49,7 +48,6 @@ network_policies: - host: api.github.com port: 443 protocol: rest - tls: terminate enforcement: enforce credential_binding: provider: github-triage-read @@ -86,7 +84,6 @@ network_policies: - host: "*.atlassian.net" port: 443 protocol: rest - tls: terminate enforcement: enforce credential_binding: provider: atlassian-triage-read @@ -110,7 +107,6 @@ network_policies: - host: storage.googleapis.com port: 443 protocol: rest - tls: terminate enforcement: enforce credential_binding: provider: prow-gcs-read diff --git a/tasks/acs-ci-nightly/workflow/harness.yaml b/tasks/acs-ci-nightly/workflow/harness.yaml index a6bc8168..f9368d18 100644 --- a/tasks/acs-ci-nightly/workflow/harness.yaml +++ b/tasks/acs-ci-nightly/workflow/harness.yaml @@ -1,23 +1,20 @@ version: 1 name: acs-ci-nightly -inference: - route: inference.local - provider: vertex-claude-triage - model: claude-haiku-4-5@20251001 - sandbox: image: ${ACS_TRIAGE_IMAGE} policy: file: ../openshell/policy.yaml providers: + - vertex-claude-triage - atlassian-triage-read - github-triage-read - prow-gcs-read env: - ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed - ANTHROPIC_BASE_URL: https://inference.local + CLAUDE_CODE_USE_VERTEX: "1" + CLAUDE_CODE_SKIP_VERTEX_AUTH: "1" CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" + ANTHROPIC_MODEL: claude-haiku-4-5@20251001 JIRA_URL: https://redhat.atlassian.net CONFLUENCE_URL: https://redhat.atlassian.net/wiki READ_ONLY_MODE: "true" @@ -37,6 +34,13 @@ payloads: # OpenShell injects the short-lived provider token; the ACS repository # owns the bounded Prow/GCS compatibility command. export PATH="/sandbox/scripts:$PATH" + export ANTHROPIC_VERTEX_PROJECT_ID="$VERTEX_AI_PROJECT_ID" + export CLOUD_ML_REGION="$VERTEX_AI_REGION" + if [ -n "$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" ]; then + export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" + else + export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_TOKEN" + fi gsutil() { /bin/bash /sandbox/scripts/gsutil "$@"; } export -f gsutil if [ -n "$GCP_SA_ACCESS_TOKEN" ]; then diff --git a/tasks/basic/workflow/harness.yaml b/tasks/basic/workflow/harness.yaml index be71ccac..a1ef684d 100644 --- a/tasks/basic/workflow/harness.yaml +++ b/tasks/basic/workflow/harness.yaml @@ -5,10 +5,21 @@ sandbox: providers: - google-vertex-ai env: - ANTHROPIC_BASE_URL: https://inference.local - ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed + CLAUDE_CODE_USE_VERTEX: "1" + CLAUDE_CODE_SKIP_VERTEX_AUTH: "1" CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" - ANTHROPIC_MODEL: claude-haiku-4-5-20251001 + ANTHROPIC_MODEL: claude-haiku-4-5@20251001 tty: true agent: - type: claude + type: sh + args: + - -c + - | + export ANTHROPIC_VERTEX_PROJECT_ID="$VERTEX_AI_PROJECT_ID" + export CLOUD_ML_REGION="$VERTEX_AI_REGION" + if [ -n "$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" ]; then + export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" + else + export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_TOKEN" + fi + exec claude diff --git a/tasks/github-pr-merger/openshell/README.md b/tasks/github-pr-merger/openshell/README.md index c8d96f7d..1b510ea5 100644 --- a/tasks/github-pr-merger/openshell/README.md +++ b/tasks/github-pr-merger/openshell/README.md @@ -12,5 +12,8 @@ openshell provider create --name github-pr-merger \ ``` Render `policy.yaml` with `MERGE_REPOSITORY`, `MERGE_PR`, and -`MERGE_HEAD_SHA`, then run the workflow with native OpenShell commands or the -Harness adapter. +`MERGE_HEAD_SHA`. Set `VERTEX_AI_PROJECT_ID` to the project configured for the +attached `vertex-review` provider, with that provider configured for `global`. +The workflow constructs the global Vertex +OpenAI-compatible endpoint from that project before the sandbox starts. Then +run it with native OpenShell commands or the Harness adapter. diff --git a/tasks/github-pr-merger/openshell/policy.yaml b/tasks/github-pr-merger/openshell/policy.yaml index 3d093b45..f1e13a86 100644 --- a/tasks/github-pr-merger/openshell/policy.yaml +++ b/tasks/github-pr-merger/openshell/policy.yaml @@ -28,7 +28,6 @@ network_policies: - host: api.github.com port: 443 protocol: rest - tls: terminate enforcement: enforce credential_binding: provider: github-pr-merger diff --git a/tasks/github-pr-merger/workflow/harness.yaml b/tasks/github-pr-merger/workflow/harness.yaml index a956cdfc..aa689f1f 100644 --- a/tasks/github-pr-merger/workflow/harness.yaml +++ b/tasks/github-pr-merger/workflow/harness.yaml @@ -7,7 +7,7 @@ sandbox: providers: [github-pr-merger, vertex-review] env: OPENCODE_CONFIG: /sandbox/opencode.json - VERTEX_AI_BASE_URL: ${VERTEX_AI_BASE_URL} + VERTEX_AI_BASE_URL: https://aiplatform.googleapis.com/v1/projects/${VERTEX_AI_PROJECT_ID}/locations/global/endpoints/openapi MERGE_REPOSITORY: ${MERGE_REPOSITORY} MERGE_PR: ${MERGE_PR} MERGE_HEAD_SHA: ${MERGE_HEAD_SHA} diff --git a/tasks/github-pr-reviewer/README.md b/tasks/github-pr-reviewer/README.md index 71929289..fc865be3 100644 --- a/tasks/github-pr-reviewer/README.md +++ b/tasks/github-pr-reviewer/README.md @@ -55,10 +55,14 @@ independent validation of the findings. The same inputs can be used with the native OpenShell CLI: ```bash +export VERTEX_AI_PROJECT_ID=YOUR_PROJECT_ID +export VERTEX_AI_BASE_URL="https://aiplatform.googleapis.com/v1/projects/${VERTEX_AI_PROJECT_ID}/locations/global/endpoints/openapi" openshell sandbox create \ --from ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:aeef1c63f00e2913ea002ccb3aaf925f338b5c5d70e63576f0d95c16a138044e \ --policy /tmp/pr-review-policy.yaml \ --provider github-review \ + --provider vertex-review \ + --env "VERTEX_AI_BASE_URL=$VERTEX_AI_BASE_URL" \ -- opencode run --format json ``` diff --git a/tasks/github-pr-reviewer/openshell/policy.yaml b/tasks/github-pr-reviewer/openshell/policy.yaml index af04288d..4317a72f 100644 --- a/tasks/github-pr-reviewer/openshell/policy.yaml +++ b/tasks/github-pr-reviewer/openshell/policy.yaml @@ -28,7 +28,6 @@ network_policies: - host: api.github.com port: 443 protocol: rest - tls: terminate enforcement: enforce credential_binding: provider: ${REVIEW_GITHUB_PROVIDER} diff --git a/tasks/github-pr-reviewer/workflow/harness.yaml b/tasks/github-pr-reviewer/workflow/harness.yaml index 3fd96958..9f1d6192 100644 --- a/tasks/github-pr-reviewer/workflow/harness.yaml +++ b/tasks/github-pr-reviewer/workflow/harness.yaml @@ -3,13 +3,25 @@ name: github-pr-review sandbox: image: ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:aeef1c63f00e2913ea002ccb3aaf925f338b5c5d70e63576f0d95c16a138044e providers: [vertex-claude-haiku] + env: + CLAUDE_CODE_USE_VERTEX: "1" + CLAUDE_CODE_SKIP_VERTEX_AUTH: "1" + CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" payloads: - source: REVIEW.md destination: /sandbox/REVIEW.md - source: fixtures/pr.diff destination: /sandbox/pr.diff agent: - type: claude + type: sh args: - - --print - - Read /sandbox/REVIEW.md and /sandbox/pr.diff. Follow the output contract exactly. + - -c + - | + export ANTHROPIC_VERTEX_PROJECT_ID="$VERTEX_AI_PROJECT_ID" + export CLOUD_ML_REGION="$VERTEX_AI_REGION" + if [ -n "$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" ]; then + export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" + else + export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_TOKEN" + fi + exec claude --print "Read /sandbox/REVIEW.md and /sandbox/pr.diff. Follow the output contract exactly." diff --git a/test/ci-workflow.yaml b/test/ci-workflow.yaml index 91726703..1b7478c3 100644 --- a/test/ci-workflow.yaml +++ b/test/ci-workflow.yaml @@ -2,6 +2,8 @@ version: 1 name: sdk-smoke sandbox: image: ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:aeef1c63f00e2913ea002ccb3aaf925f338b5c5d70e63576f0d95c16a138044e + policy: + file: lifecycle-policy.yaml agent: type: sh args: [-c, "printf 'canonical-sdk-ok\\n'"] diff --git a/test/hypershell-haiku-workflow.yaml b/test/hypershell-haiku-workflow.yaml index 7784fde5..fe4a798c 100644 --- a/test/hypershell-haiku-workflow.yaml +++ b/test/hypershell-haiku-workflow.yaml @@ -23,5 +23,10 @@ agent: args: - -c - | - export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_TOKEN" + # Provider values here are proxy placeholders; refresh material stays at the gateway. + if [ -n "$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" ]; then + export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" + else + export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_TOKEN" + fi exec claude --print --model claude-haiku-4-5@20251001 "Respond with exactly HYPERSHELL_HAIKU_OK and nothing else." diff --git a/test/hypershell-lifecycle.sh b/test/hypershell-lifecycle.sh index 60ee66a7..2ef87cf6 100755 --- a/test/hypershell-lifecycle.sh +++ b/test/hypershell-lifecycle.sh @@ -58,7 +58,7 @@ miss=() [[ -n "$HYPERSHELL_OIDC_AUDIENCE" ]] || miss+=(OPENSHELL_OIDC_AUDIENCE) [[ -n "$HYPERSHELL_SANDBOX_SA_ID" ]] || miss+=(OPENSHELL_OIDC_CLIENT_ID) [[ -n "$OPENSHELL_OIDC_CLIENT_SECRET" ]] || miss+=(OPENSHELL_OIDC_CLIENT_SECRET) -if [[ "${HYPERSHELL_WORKFLOW_FILE##*/}" == hypershell-haiku-workflow.yaml ]]; then +if [[ "${WORKFLOW_FILE##*/}" == hypershell-haiku-workflow.yaml ]]; then [[ -n "$HYPERSHELL_VERTEX_PROJECT_ID" ]] || miss+=(HYPERSHELL_VERTEX_PROJECT_ID) [[ -n "$HYPERSHELL_VERTEX_REGION" ]] || miss+=(HYPERSHELL_VERTEX_REGION) fi diff --git a/test/lib/provision.sh b/test/lib/provision.sh index c8c5f6c6..2f48e256 100644 --- a/test/lib/provision.sh +++ b/test/lib/provision.sh @@ -17,6 +17,17 @@ OPENSHELL_CHART_OCI="${OPENSHELL_CHART_OCI:-oci://ghcr.io/nvidia/openshell/helm-chart}" OPENSHELL_CRD_URL="${OPENSHELL_CRD_URL:-https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v0.5.0/manifest.yaml}" +install_agent_sandbox() { + kubectl apply -f "$OPENSHELL_CRD_URL" || return 1 + kubectl wait --for=condition=Established crd/sandboxes.agents.x-k8s.io --timeout=120s || return 1 + kubectl rollout status deployment/agent-sandbox-controller \ + -n agent-sandbox-system --timeout=300s || return 1 + if ! kubectl api-versions | grep -Fxq 'agents.x-k8s.io/v1beta1'; then + echo " ERROR: Agent Sandbox v1beta1 API is not served" >&2 + return 1 + fi +} + _chart_version() { if [[ -n "${OPENSHELL_CHART_VERSION:-}" ]]; then echo "$OPENSHELL_CHART_VERSION" @@ -60,7 +71,7 @@ provision_kind() { pod-security.kubernetes.io/enforce=privileged \ pod-security.kubernetes.io/warn=privileged --overwrite || return 1 - kubectl apply -f "$OPENSHELL_CRD_URL" || return 1 + install_agent_sandbox || return 1 values="$(mktemp /tmp/os-kind-values-XXXXXX.yaml)" cat > "$values" <<'EOF' @@ -114,7 +125,7 @@ provision_ocp() { kubectl label ns openshell \ pod-security.kubernetes.io/enforce=privileged \ pod-security.kubernetes.io/warn=privileged --overwrite || return 1 - kubectl apply -f "$OPENSHELL_CRD_URL" || return 1 + install_agent_sandbox || return 1 # SCCs (openshift.yaml ocp.scc-*). local sa diff --git a/test/pr_review_test.go b/test/pr_review_test.go index 7efc4576..418abda4 100644 --- a/test/pr_review_test.go +++ b/test/pr_review_test.go @@ -73,7 +73,7 @@ func TestPRReview(t *testing.T) { prepare.Env = append(os.Environ(), "PATH="+root+string(os.PathListSeparator)+os.Getenv("PATH"), "FAKE_SCENARIO="+scenario, "TRACE="+filepath.Join(root, "trace"), "READY="+filepath.Join(root, "ready"), "REVIEW_DIR="+filepath.Join(root, "review"), "REVIEW_REPOSITORY=owner/repo", "REVIEW_PR=1", "REVIEW_HEAD=", "GITHUB_OUTPUT="+filepath.Join(root, "output"), - "GITHUB_STEP_SUMMARY="+stepSummary, "GOOGLE_VERTEX_AI_TOKEN=", "VERTEX_AI_PROJECT_ID=", "GITHUB_TOKEN=", "OPENSHELL_GATEWAY=managed-test", "OPENSHELL_WORKSPACE=shared-test", "REVIEW_AGENT=", "REVIEW_LABEL=stackrox-ai-review", "CODEX_INFERENCE_PROVIDER=fake-openai", "CODEX_MODEL=gpt-5.6-luna", "CODEX_WORKSPACE=codex-workspace", "REVIEW_POLICY_TEMPLATE="+filepath.Join(root, "review-policy.yaml")) + "GITHUB_STEP_SUMMARY="+stepSummary, "GOOGLE_VERTEX_AI_TOKEN=", "VERTEX_AI_PROJECT_ID=", "VERTEX_AI_BASE_URL=https://aiplatform.googleapis.com/v1/projects/test-project/locations/global/endpoints/openapi", "GITHUB_TOKEN=", "OPENSHELL_GATEWAY=managed-test", "OPENSHELL_WORKSPACE=shared-test", "REVIEW_AGENT=", "REVIEW_LABEL=stackrox-ai-review", "CODEX_INFERENCE_PROVIDER=fake-openai", "CODEX_MODEL=gpt-5.6-luna", "CODEX_WORKSPACE=codex-workspace", "REVIEW_POLICY_TEMPLATE="+filepath.Join(root, "review-policy.yaml")) if strings.HasPrefix(scenario, "codex-") { prepare.Env = append(prepare.Env, "REVIEW_AGENT=codex", "FAKE_AGENT=codex", "GITHUB_TOKEN=fake") } diff --git a/test/suite/run.sh b/test/suite/run.sh index 8dc245bb..1b9767a5 100755 --- a/test/suite/run.sh +++ b/test/suite/run.sh @@ -60,7 +60,7 @@ run_test_fail() { echo "=== Canonical configuration ===" run_test "apply: resolved YAML" bash -c '"$1" workflow apply "$2" -o yaml | grep -q "version: 1"' _ "$HARNESS" "$CONFIG" -run_test "reviewer fixture: resolved YAML" bash -c 'out=$("$1" workflow apply "$2" -o yaml) && grep -q "source: REVIEW.md" <<<"$out" && grep -q "source: fixtures/pr.diff" <<<"$out" && grep -q "type: claude" <<<"$out"' _ "$HARNESS" "$ROOT/tasks/github-pr-reviewer/workflow/harness.yaml" +run_test "reviewer fixture: resolved YAML" bash -c 'out=$("$1" workflow apply "$2" -o yaml) && grep -q "source: REVIEW.md" <<<"$out" && grep -q "source: fixtures/pr.diff" <<<"$out" && grep -q "type: sh" <<<"$out"' _ "$HARNESS" "$ROOT/tasks/github-pr-reviewer/workflow/harness.yaml" run_test "apply: resolved JSON" bash -c '"$1" workflow apply "$2" -o json | python3 -m json.tool >/dev/null' _ "$HARNESS" "$CONFIG" run_test "apply: name override" bash -c '"$1" workflow apply "$2" --name overridden -o yaml | grep -q "name: overridden"' _ "$HARNESS" "$CONFIG" run_test "apply: entrypoint override" bash -c '"$1" workflow apply "$2" --entrypoint opencode -o yaml | grep -q "type: opencode"' _ "$HARNESS" "$CONFIG" From d12d4e416552f65311a80c9760033cbccfb7f4a6 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 1 Oct 2026 09:50:24 -0700 Subject: [PATCH 3/7] fix: repin available Collector builder image --- images/stackrox/README.md | 2 +- images/stackrox/sandbox-collector-builder/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/images/stackrox/README.md b/images/stackrox/README.md index 5a3315c7..0119706b 100644 --- a/images/stackrox/README.md +++ b/images/stackrox/README.md @@ -52,7 +52,7 @@ docker build --platform linux/amd64 \ An amd64 image based on the StackRox Collector builder image. The `master` builder manifest is pinned to -`sha256:1ed20fa2c2f650199a20d8625701ff39749b70031d9db178273fea7c4280d48f`. +`sha256:e2a416f82165fa87e705f1955b290a59a1e4a08eafbceec5b510d0d401b486d0`. It keeps the Collector compiler and build toolchain and adds the same OpenShell contract, coding agents, GitHub skill, Atlassian MCP, Google Workspace CLI, and `gopls` support as the StackRox CI profile. It is separate diff --git a/images/stackrox/sandbox-collector-builder/Dockerfile b/images/stackrox/sandbox-collector-builder/Dockerfile index ed6f9735..7526fe2e 100644 --- a/images/stackrox/sandbox-collector-builder/Dockerfile +++ b/images/stackrox/sandbox-collector-builder/Dockerfile @@ -18,7 +18,7 @@ # The tag is master; pin the current multi-architecture manifest digest # so a retag cannot silently change the toolchain. -ARG BASE_IMAGE=quay.io/stackrox-io/collector-builder@sha256:1ed20fa2c2f650199a20d8625701ff39749b70031d9db178273fea7c4280d48f +ARG BASE_IMAGE=quay.io/stackrox-io/collector-builder@sha256:e2a416f82165fa87e705f1955b290a59a1e4a08eafbceec5b510d0d401b486d0 FROM ${BASE_IMAGE} SHELL ["/bin/bash", "-o", "pipefail", "-c"] From 6bcbfe43066a772cdcdb8319a9740908f38d630d Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 1 Oct 2026 10:05:07 -0700 Subject: [PATCH 4/7] fix: pass Vertex client context to Claude workflows --- .github/workflows/pr-review-reusable.yml | 1 + tasks/acs-ci-nightly/README.md | 2 ++ tasks/acs-ci-nightly/workflow/harness.yaml | 4 ++-- tasks/basic/workflow/harness.yaml | 4 ++-- tasks/github-pr-reviewer/README.md | 5 +++++ tasks/github-pr-reviewer/workflow/harness.yaml | 4 ++-- test/suite/run.sh | 3 ++- 7 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-review-reusable.yml b/.github/workflows/pr-review-reusable.yml index 1b2b6f7c..3131c658 100644 --- a/.github/workflows/pr-review-reusable.yml +++ b/.github/workflows/pr-review-reusable.yml @@ -176,6 +176,7 @@ jobs: CODEX_MODEL: ${{ inputs.codex-model }} CODEX_WORKSPACE: ${{ inputs.codex-workspace }} CODEX_BOOTSTRAP: ${{ inputs.codex-bootstrap }} + TASK_TIMEOUT: 12m OPENSHELL_CODEX_API_KEY: ${{ secrets.OPENSHELL_CODEX_API_KEY }} TASK_PROVIDERS: ${{ inputs.required-providers }} GITHUB_TOKEN: ${{ steps.openshell-app-token.outputs.token }} diff --git a/tasks/acs-ci-nightly/README.md b/tasks/acs-ci-nightly/README.md index 9ae48d6b..fcfead9b 100644 --- a/tasks/acs-ci-nightly/README.md +++ b/tasks/acs-ci-nightly/README.md @@ -62,6 +62,8 @@ From a trusted caller with a reachable managed gateway: export ACS_TRIAGE_IMAGE='quay.io/rcochran/openshell:sandbox-stackrox-ci@sha256:' export ACS_TRIAGE_REF='main' export TRIAGE_RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" +export VERTEX_AI_PROJECT_ID='YOUR_VERTEX_PROJECT' +export VERTEX_AI_REGION='us-east5' harness workflow apply tasks/acs-ci-nightly/workflow/harness.yaml \ --output-dir ./triage-artifacts ``` diff --git a/tasks/acs-ci-nightly/workflow/harness.yaml b/tasks/acs-ci-nightly/workflow/harness.yaml index f9368d18..41b276ce 100644 --- a/tasks/acs-ci-nightly/workflow/harness.yaml +++ b/tasks/acs-ci-nightly/workflow/harness.yaml @@ -15,6 +15,8 @@ sandbox: CLAUDE_CODE_SKIP_VERTEX_AUTH: "1" CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" ANTHROPIC_MODEL: claude-haiku-4-5@20251001 + ANTHROPIC_VERTEX_PROJECT_ID: ${VERTEX_AI_PROJECT_ID} + CLOUD_ML_REGION: ${VERTEX_AI_REGION} JIRA_URL: https://redhat.atlassian.net CONFLUENCE_URL: https://redhat.atlassian.net/wiki READ_ONLY_MODE: "true" @@ -34,8 +36,6 @@ payloads: # OpenShell injects the short-lived provider token; the ACS repository # owns the bounded Prow/GCS compatibility command. export PATH="/sandbox/scripts:$PATH" - export ANTHROPIC_VERTEX_PROJECT_ID="$VERTEX_AI_PROJECT_ID" - export CLOUD_ML_REGION="$VERTEX_AI_REGION" if [ -n "$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" ]; then export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" else diff --git a/tasks/basic/workflow/harness.yaml b/tasks/basic/workflow/harness.yaml index a1ef684d..08fe03f2 100644 --- a/tasks/basic/workflow/harness.yaml +++ b/tasks/basic/workflow/harness.yaml @@ -9,14 +9,14 @@ sandbox: CLAUDE_CODE_SKIP_VERTEX_AUTH: "1" CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" ANTHROPIC_MODEL: claude-haiku-4-5@20251001 + ANTHROPIC_VERTEX_PROJECT_ID: ${VERTEX_AI_PROJECT_ID} + CLOUD_ML_REGION: ${VERTEX_AI_REGION} tty: true agent: type: sh args: - -c - | - export ANTHROPIC_VERTEX_PROJECT_ID="$VERTEX_AI_PROJECT_ID" - export CLOUD_ML_REGION="$VERTEX_AI_REGION" if [ -n "$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" ]; then export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" else diff --git a/tasks/github-pr-reviewer/README.md b/tasks/github-pr-reviewer/README.md index fc865be3..02b19ae9 100644 --- a/tasks/github-pr-reviewer/README.md +++ b/tasks/github-pr-reviewer/README.md @@ -33,6 +33,10 @@ those task-specific capabilities. OpenAI-compatible endpoint. The Codex path attaches its OpenAI provider and uses the native Responses API endpoint. +The Claude fixture workflow requires `VERTEX_AI_PROJECT_ID` and +`VERTEX_AI_REGION` for its native client. These are nonsecret values supplied by +the trusted caller; the attached provider owns the credential. + [`scripts/pr-review.sh`](../../scripts/pr-review.sh) prepares the review and delegates task execution to the shared adapter. The local wrapper supplies temporary setup around its `run` command; managed callers supply platform @@ -56,6 +60,7 @@ The same inputs can be used with the native OpenShell CLI: ```bash export VERTEX_AI_PROJECT_ID=YOUR_PROJECT_ID +export VERTEX_AI_REGION=global export VERTEX_AI_BASE_URL="https://aiplatform.googleapis.com/v1/projects/${VERTEX_AI_PROJECT_ID}/locations/global/endpoints/openapi" openshell sandbox create \ --from ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:aeef1c63f00e2913ea002ccb3aaf925f338b5c5d70e63576f0d95c16a138044e \ diff --git a/tasks/github-pr-reviewer/workflow/harness.yaml b/tasks/github-pr-reviewer/workflow/harness.yaml index 9f1d6192..cdbd17d6 100644 --- a/tasks/github-pr-reviewer/workflow/harness.yaml +++ b/tasks/github-pr-reviewer/workflow/harness.yaml @@ -7,6 +7,8 @@ sandbox: CLAUDE_CODE_USE_VERTEX: "1" CLAUDE_CODE_SKIP_VERTEX_AUTH: "1" CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" + ANTHROPIC_VERTEX_PROJECT_ID: ${VERTEX_AI_PROJECT_ID} + CLOUD_ML_REGION: ${VERTEX_AI_REGION} payloads: - source: REVIEW.md destination: /sandbox/REVIEW.md @@ -17,8 +19,6 @@ agent: args: - -c - | - export ANTHROPIC_VERTEX_PROJECT_ID="$VERTEX_AI_PROJECT_ID" - export CLOUD_ML_REGION="$VERTEX_AI_REGION" if [ -n "$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" ]; then export ANTHROPIC_AUTH_TOKEN="$GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN" else diff --git a/test/suite/run.sh b/test/suite/run.sh index 1b9767a5..be78c7e4 100755 --- a/test/suite/run.sh +++ b/test/suite/run.sh @@ -60,7 +60,8 @@ run_test_fail() { echo "=== Canonical configuration ===" run_test "apply: resolved YAML" bash -c '"$1" workflow apply "$2" -o yaml | grep -q "version: 1"' _ "$HARNESS" "$CONFIG" -run_test "reviewer fixture: resolved YAML" bash -c 'out=$("$1" workflow apply "$2" -o yaml) && grep -q "source: REVIEW.md" <<<"$out" && grep -q "source: fixtures/pr.diff" <<<"$out" && grep -q "type: sh" <<<"$out"' _ "$HARNESS" "$ROOT/tasks/github-pr-reviewer/workflow/harness.yaml" +run_test "reviewer fixture: resolved YAML" env VERTEX_AI_PROJECT_ID=fixture-project VERTEX_AI_REGION=us-east5 bash -c 'out=$("$1" workflow apply "$2" -o yaml) && grep -q "source: REVIEW.md" <<<"$out" && grep -q "source: fixtures/pr.diff" <<<"$out" && grep -q "ANTHROPIC_VERTEX_PROJECT_ID:" <<<"$out" && grep -q "type: sh" <<<"$out"' _ "$HARNESS" "$ROOT/tasks/github-pr-reviewer/workflow/harness.yaml" +run_test_fail "reviewer fixture: missing Vertex project" env -u VERTEX_AI_PROJECT_ID VERTEX_AI_REGION=us-east5 "$HARNESS" workflow apply "$ROOT/tasks/github-pr-reviewer/workflow/harness.yaml" -o yaml run_test "apply: resolved JSON" bash -c '"$1" workflow apply "$2" -o json | python3 -m json.tool >/dev/null' _ "$HARNESS" "$CONFIG" run_test "apply: name override" bash -c '"$1" workflow apply "$2" --name overridden -o yaml | grep -q "name: overridden"' _ "$HARNESS" "$CONFIG" run_test "apply: entrypoint override" bash -c '"$1" workflow apply "$2" --entrypoint opencode -o yaml | grep -q "type: opencode"' _ "$HARNESS" "$CONFIG" From ff25382bda7595ef3cc1d157824aa4d1914fa5dc Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 1 Oct 2026 10:08:44 -0700 Subject: [PATCH 5/7] fix: tolerate newly created Agent Sandbox CRD status --- test/lib/provision.sh | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/test/lib/provision.sh b/test/lib/provision.sh index 2f48e256..fbb9a37d 100644 --- a/test/lib/provision.sh +++ b/test/lib/provision.sh @@ -18,14 +18,26 @@ OPENSHELL_CHART_OCI="${OPENSHELL_CHART_OCI:-oci://ghcr.io/nvidia/openshell/helm- OPENSHELL_CRD_URL="${OPENSHELL_CRD_URL:-https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v0.5.0/manifest.yaml}" install_agent_sandbox() { + local i established kubectl apply -f "$OPENSHELL_CRD_URL" || return 1 - kubectl wait --for=condition=Established crd/sandboxes.agents.x-k8s.io --timeout=120s || return 1 - kubectl rollout status deployment/agent-sandbox-controller \ - -n agent-sandbox-system --timeout=300s || return 1 - if ! kubectl api-versions | grep -Fxq 'agents.x-k8s.io/v1beta1'; then - echo " ERROR: Agent Sandbox v1beta1 API is not served" >&2 + for i in $(seq 1 60); do + established="$(kubectl get crd sandboxes.agents.x-k8s.io \ + -o jsonpath='{.status.conditions[?(@.type=="Established")].status}' 2>/dev/null)" + [[ "$established" == True ]] && break + sleep 2 + done + if [[ "$established" != True ]]; then + echo " ERROR: Agent Sandbox CRD did not become Established" >&2 return 1 fi + kubectl rollout status deployment/agent-sandbox-controller \ + -n agent-sandbox-system --timeout=300s || return 1 + for i in $(seq 1 15); do + kubectl api-versions | grep -Fx 'agents.x-k8s.io/v1beta1' >/dev/null && return 0 + sleep 2 + done + echo " ERROR: Agent Sandbox v1beta1 API is not served" >&2 + return 1 } _chart_version() { From 70df268140cb039ae49f27b8de2ecf4f7622457a Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 1 Oct 2026 10:16:59 -0700 Subject: [PATCH 6/7] fix: preserve Claude reviewer model and fixture inputs --- tasks/github-pr-reviewer/workflow/harness.yaml | 1 + test/github-pr-reviewer-local.sh | 2 ++ test/suite/run.sh | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tasks/github-pr-reviewer/workflow/harness.yaml b/tasks/github-pr-reviewer/workflow/harness.yaml index cdbd17d6..6075b407 100644 --- a/tasks/github-pr-reviewer/workflow/harness.yaml +++ b/tasks/github-pr-reviewer/workflow/harness.yaml @@ -7,6 +7,7 @@ sandbox: CLAUDE_CODE_USE_VERTEX: "1" CLAUDE_CODE_SKIP_VERTEX_AUTH: "1" CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" + ANTHROPIC_MODEL: claude-haiku-4-5@20251001 ANTHROPIC_VERTEX_PROJECT_ID: ${VERTEX_AI_PROJECT_ID} CLOUD_ML_REGION: ${VERTEX_AI_REGION} payloads: diff --git a/test/github-pr-reviewer-local.sh b/test/github-pr-reviewer-local.sh index baf7d162..cbe0abbb 100755 --- a/test/github-pr-reviewer-local.sh +++ b/test/github-pr-reviewer-local.sh @@ -16,6 +16,8 @@ if [[ "${CI:-}" == "true" ]]; then exit 0 fi [[ -x "$HARNESS" ]] || { echo "ERROR: run make cli first" >&2; exit 1; } +: "${VERTEX_AI_PROJECT_ID:?set the Vertex project used by vertex-claude-haiku}" +: "${VERTEX_AI_REGION:?set the Vertex region used by vertex-claude-haiku}" name="pr-$(date +%s)-$$" output="" diff --git a/test/suite/run.sh b/test/suite/run.sh index be78c7e4..00aecb6f 100755 --- a/test/suite/run.sh +++ b/test/suite/run.sh @@ -60,8 +60,9 @@ run_test_fail() { echo "=== Canonical configuration ===" run_test "apply: resolved YAML" bash -c '"$1" workflow apply "$2" -o yaml | grep -q "version: 1"' _ "$HARNESS" "$CONFIG" -run_test "reviewer fixture: resolved YAML" env VERTEX_AI_PROJECT_ID=fixture-project VERTEX_AI_REGION=us-east5 bash -c 'out=$("$1" workflow apply "$2" -o yaml) && grep -q "source: REVIEW.md" <<<"$out" && grep -q "source: fixtures/pr.diff" <<<"$out" && grep -q "ANTHROPIC_VERTEX_PROJECT_ID:" <<<"$out" && grep -q "type: sh" <<<"$out"' _ "$HARNESS" "$ROOT/tasks/github-pr-reviewer/workflow/harness.yaml" +run_test "reviewer fixture: resolved YAML" env VERTEX_AI_PROJECT_ID=fixture-project VERTEX_AI_REGION=us-east5 bash -c 'out=$("$1" workflow apply "$2" -o yaml) && grep -q "source: REVIEW.md" <<<"$out" && grep -q "source: fixtures/pr.diff" <<<"$out" && grep -q "ANTHROPIC_MODEL:" <<<"$out" && grep -q "ANTHROPIC_VERTEX_PROJECT_ID:" <<<"$out" && grep -q "type: sh" <<<"$out"' _ "$HARNESS" "$ROOT/tasks/github-pr-reviewer/workflow/harness.yaml" run_test_fail "reviewer fixture: missing Vertex project" env -u VERTEX_AI_PROJECT_ID VERTEX_AI_REGION=us-east5 "$HARNESS" workflow apply "$ROOT/tasks/github-pr-reviewer/workflow/harness.yaml" -o yaml +run_test_fail "reviewer fixture: local preflight" env -u CI -u VERTEX_AI_PROJECT_ID -u VERTEX_AI_REGION "$ROOT/test/github-pr-reviewer-local.sh" run_test "apply: resolved JSON" bash -c '"$1" workflow apply "$2" -o json | python3 -m json.tool >/dev/null' _ "$HARNESS" "$CONFIG" run_test "apply: name override" bash -c '"$1" workflow apply "$2" --name overridden -o yaml | grep -q "name: overridden"' _ "$HARNESS" "$CONFIG" run_test "apply: entrypoint override" bash -c '"$1" workflow apply "$2" --entrypoint opencode -o yaml | grep -q "type: opencode"' _ "$HARNESS" "$CONFIG" From 3d5a93defd8126814cc70b9ff6adf037ca784566 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 1 Oct 2026 10:24:02 -0700 Subject: [PATCH 7/7] docs: show Vertex setup for basic Claude workflow --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index b63fde7a..7059da0e 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,19 @@ Use `--attach` for the same task with a connected terminal. Set `openshell sandbox` commands. Normal execution creates one sandbox, runs the agent, collects declared output files, and deletes the sandbox. +The [basic Claude example](tasks/basic/workflow/harness.yaml) attaches an +existing `google-vertex-ai` provider. Set the provider's nonsecret project and +region on the trusted host before applying it: + +```bash +export VERTEX_AI_PROJECT_ID=YOUR_VERTEX_PROJECT +export VERTEX_AI_REGION=us-east5 +./harness workflow apply tasks/basic/workflow/harness.yaml --attach +``` + +Use the project and region configured for that provider; OpenShell keeps its +credential at the gateway. + ## Ownership and security boundaries | Component | Owns |