From 4feacc961620d5c0687a06d82d09d4b0b669cdda Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 18 Sep 2026 18:14:41 -0700 Subject: [PATCH 1/3] refactor: isolate PR review agent profiles --- scripts/pr-review.sh | 81 +++++-------------------------- scripts/review/README.md | 21 ++++---- scripts/review/agents/codex.sh | 46 ++++++++++++++++++ scripts/review/agents/opencode.sh | 54 +++++++++++++++++++++ test/pr_review_test.go | 18 ++++++- 5 files changed, 140 insertions(+), 80 deletions(-) create mode 100644 scripts/review/agents/codex.sh create mode 100644 scripts/review/agents/opencode.sh diff --git a/scripts/pr-review.sh b/scripts/pr-review.sh index cea5bf5..f3c295f 100755 --- a/scripts/pr-review.sh +++ b/scripts/pr-review.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash # Trusted host orchestration. PR content is data, never runner code. +# shellcheck disable=SC2034,SC2154 # agent profiles are sourced below and share state with this wrapper. set -euo pipefail umask 077 cd "$(dirname "$0")/.." @@ -16,40 +17,17 @@ codex_inference_provider="${CODEX_INFERENCE_PROVIDER:-openai-inference}" codex_model="${CODEX_MODEL:-gpt-5.6-luna}" codex_workspace="${CODEX_WORKSPACE:-}" case "$review_agent" in - opencode|codex) ;; + opencode|codex) + # shellcheck source=/dev/null + source "scripts/review/agents/$review_agent.sh" + ;; *) echo "unsupported review agent: $review_agent" >&2; exit 1 ;; esac [[ "$review_label" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,49}$ ]] || { echo "invalid review label" >&2 exit 1 } -if [[ "$review_agent" == codex ]]; then - [[ "$codex_inference_provider" =~ ^[A-Za-z0-9_.-]+$ && "$codex_model" =~ ^[A-Za-z0-9_.@/-]+$ ]] || { - echo "invalid Codex inference provider or model" >&2 - exit 1 - } - [[ "$codex_workspace" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$ ]] || { - echo "Codex requires a valid pre-provisioned workspace" >&2 - exit 1 - } -fi -configured_target=false -if [[ "$review_agent" == codex ]]; then - workspace="$codex_workspace" - sandbox_name="codex-$(openssl rand -hex 6)" - configured_target=true - github_provider=github-review -else - workspace="${OPENSHELL_WORKSPACE:-rev-$RANDOM-$$}" - sandbox_name="review-$(openssl rand -hex 6)" - if [[ -n "${OPENSHELL_WORKSPACE:-}" ]]; then - configured_target=true - github_provider=github-review - fi -fi -if [[ -z "${github_provider:-}" ]]; then - github_provider="github-review-$RANDOM-$$" -fi +agent_configure max_diff_bytes=262144 created_workspace=false created_vertex_provider=false @@ -153,39 +131,10 @@ run_review() { base="$(jq -er '.base | select(test("^[0-9a-f]{40}$"))' "$REVIEW_DIR/input.json")" (cd "$REVIEW_DIR" && shasum -a 256 -c pr.diff.sha256 >/dev/null) ensure_current - if [[ "$configured_target" != true ]]; then - : "${GITHUB_TOKEN:?set the workflow GitHub token for provider bootstrap}" - fi - if [[ "$review_agent" == opencode && "$configured_target" != true ]]; then - : "${GOOGLE_VERTEX_AI_TOKEN:?set a short-lived Vertex token}" "${VERTEX_AI_PROJECT_ID:?set Vertex project}" - fi - - if [[ "$review_agent" == opencode && "$configured_target" != true ]]; then - timeout 60s openshell workspace create --gateway "$gateway" --name "$workspace" - created_workspace=true - fi - if [[ "$review_agent" == opencode && "$configured_target" != true ]]; then - timeout 60s openshell provider create --gateway "$gateway" --workspace "$workspace" \ - --name vertex-review --type google-vertex-ai --from-existing \ - --config "VERTEX_AI_PROJECT_ID=$VERTEX_AI_PROJECT_ID" --config "VERTEX_AI_REGION=${VERTEX_AI_REGION:-global}" - created_vertex_provider=true - fi - if [[ "$configured_target" != true ]]; then - timeout 60s openshell provider create --gateway "$gateway" --workspace "$workspace" \ - --name "$github_provider" --type github --credential GITHUB_TOKEN - created_github_provider=true - fi - if [[ "$review_agent" == opencode ]]; then - if [[ "$configured_target" != true ]]; then - timeout 60s openshell inference set --gateway "$gateway" --workspace "$workspace" \ - --provider vertex-review --model gemini-2.5-pro --no-verify - fi - workflow_file=tasks/github-pr-reviewer/workflow/opencode-harness.yaml - validator=scripts/review/validate-agent-output.sh - else - workflow_file=tasks/github-pr-reviewer/workflow/codex-harness.yaml - validator=scripts/review/validate-codex-output.sh - fi + agent_require_credentials + agent_setup + workflow_file="$(agent_workflow_file)" + validator="$(agent_validator)" export REVIEW_DIFF="$REVIEW_DIR/pr.diff" export REVIEW_POLICY="$REVIEW_DIR/review-policy.yaml" @@ -218,15 +167,7 @@ run_review() { fi ((apply_status == 0)) || return "$apply_status" ensure_current - if [[ "$review_agent" == opencode ]]; then - jq -Rr 'fromjson? | select(.type == "text") | .part.text' \ - "$REVIEW_DIR/agent.ndjson" > "$REVIEW_DIR/review.txt" - elif [[ -s "$REVIEW_DIR/codex-final.txt" ]]; then - cp "$REVIEW_DIR/codex-final.txt" "$REVIEW_DIR/review.txt" - else - jq -Rr 'fromjson? | select(.type == "item.completed" and .item.type == "agent_message") | .item.text' \ - "$REVIEW_DIR/agent.ndjson" > "$REVIEW_DIR/review.txt" - fi + agent_extract_output state=completed } diff --git a/scripts/review/README.md b/scripts/review/README.md index 8422117..0e34c5c 100644 --- a/scripts/review/README.md +++ b/scripts/review/README.md @@ -4,7 +4,12 @@ These scripts contain behavior that can be reused by multiple workflow archetypes. They accept explicit paths and environment inputs; they do not own provider credentials, workflow policy, model selection, or GitHub permissions. -Current component: +Current components: + +- `agents/codex.sh` and `agents/opencode.sh` are the two PR-review agent + profiles. Each profile owns its trusted task workflow, output validator, + output extraction, and any provider/workspace setup. The shared wrapper does + not need to know how an agent is provisioned. - `validate-agent-output.sh REVIEW_DIR` validates the bounded OpenCode event stream emitted by the PR reviewer. It rejects malformed event-looking lines, @@ -13,12 +18,10 @@ Current component: completion, not finding correctness or whether every external action was appropriate. Comments can already have been posted when this check runs. -This validator is intentionally scoped to the PR-review workflow until a second -workflow demonstrates a stable event and publication contract. It is not a -generic agent-result protocol. +These validators are intentionally scoped to the PR-review workflow until a +second workflow demonstrates a stable event and publication contract. They are +not a generic agent-result protocol. -The PR-specific wrapper remains in `scripts/pr-review.sh` until a second -workflow demonstrates a stable context or lifecycle contract. Future -extractions should preserve this boundary: reusable components validate and -guard execution, while each workflow selects its agent, policy, providers, and -publication behavior. +The PR-specific lifecycle remains in `scripts/pr-review.sh`; future agent +removal should delete the corresponding profile and task bundle without +changing PR eligibility, diff integrity, sandbox cleanup, or artifact handling. diff --git a/scripts/review/agents/codex.sh b/scripts/review/agents/codex.sh new file mode 100644 index 0000000..06c5e6e --- /dev/null +++ b/scripts/review/agents/codex.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Codex PR-review profile, sourced by scripts/pr-review.sh. +# Keep provider and workspace ownership explicit: Codex uses a pre-provisioned +# OpenShell workspace and never creates or deletes its providers. +# shellcheck disable=SC2034,SC2154 # configuration and lifecycle state are shared with the wrapper. + +agent_configure() { + [[ "$codex_inference_provider" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$ && "$codex_model" =~ ^[A-Za-z0-9_.@/-]+$ ]] || { + echo "invalid Codex inference provider or model" >&2 + return 1 + } + [[ "$codex_workspace" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$ ]] || { + echo "Codex requires a valid pre-provisioned workspace" >&2 + return 1 + } + + workspace="$codex_workspace" + sandbox_name="codex-$(openssl rand -hex 6)" + configured_target=true + github_provider=github-review +} + +agent_require_credentials() { + : +} + +agent_setup() { + : +} + +agent_workflow_file() { + printf '%s\n' tasks/github-pr-reviewer/workflow/codex-harness.yaml +} + +agent_validator() { + printf '%s\n' scripts/review/validate-codex-output.sh +} + +agent_extract_output() { + if [[ -s "$REVIEW_DIR/codex-final.txt" ]]; then + cp "$REVIEW_DIR/codex-final.txt" "$REVIEW_DIR/review.txt" + else + jq -Rr 'fromjson? | select(.type == "item.completed" and .item.type == "agent_message") | .item.text' \ + "$REVIEW_DIR/agent.ndjson" > "$REVIEW_DIR/review.txt" + fi +} diff --git a/scripts/review/agents/opencode.sh b/scripts/review/agents/opencode.sh new file mode 100644 index 0000000..b087620 --- /dev/null +++ b/scripts/review/agents/opencode.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# OpenCode PR-review profile, sourced by scripts/pr-review.sh. +# This legacy profile owns the temporary Vertex workspace and provider setup. +# shellcheck disable=SC2034,SC2154 # configuration and lifecycle state are shared with the wrapper. + +agent_configure() { + workspace="${OPENSHELL_WORKSPACE:-rev-$RANDOM-$$}" + sandbox_name="review-$(openssl rand -hex 6)" + configured_target=false + github_provider="github-review" + if [[ -n "${OPENSHELL_WORKSPACE:-}" ]]; then + configured_target=true + else + github_provider="github-review-$RANDOM-$$" + fi +} + +agent_require_credentials() { + 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 +} + +agent_setup() { + [[ "$configured_target" == true ]] && return 0 + + timeout 60s openshell workspace create --gateway "$gateway" --name "$workspace" + created_workspace=true + timeout 60s openshell provider create --gateway "$gateway" --workspace "$workspace" \ + --name vertex-review --type google-vertex-ai --from-existing \ + --config "VERTEX_AI_PROJECT_ID=$VERTEX_AI_PROJECT_ID" \ + --config "VERTEX_AI_REGION=${VERTEX_AI_REGION:-global}" + created_vertex_provider=true + 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() { + printf '%s\n' tasks/github-pr-reviewer/workflow/opencode-harness.yaml +} + +agent_validator() { + printf '%s\n' scripts/review/validate-agent-output.sh +} + +agent_extract_output() { + jq -Rr 'fromjson? | select(.type == "text") | .part.text' \ + "$REVIEW_DIR/agent.ndjson" > "$REVIEW_DIR/review.txt" +} diff --git a/test/pr_review_test.go b/test/pr_review_test.go index e697f60..bb41389 100644 --- a/test/pr_review_test.go +++ b/test/pr_review_test.go @@ -36,6 +36,9 @@ func TestPRReview(t *testing.T) { if err := os.Mkdir(filepath.Join(root, "scripts", "review"), 0o700); err != nil { t.Fatal(err) } + if err := os.Mkdir(filepath.Join(root, "scripts", "review", "agents"), 0o700); err != nil { + t.Fatal(err) + } validator, err := os.ReadFile("../scripts/review/validate-agent-output.sh") if err != nil { t.Fatal(err) @@ -49,8 +52,10 @@ func TestPRReview(t *testing.T) { t.Fatalf("validator must be executable: mode %o", validatorMode) } codexValidator := mustRead(t, "../scripts/review/validate-codex-output.sh") + codexAgent := mustRead(t, "../scripts/review/agents/codex.sh") + opencodeAgent := mustRead(t, "../scripts/review/agents/opencode.sh") taskRunner := mustRead(t, "../scripts/run-task.sh") - for name, data := range map[string][]byte{"scripts/pr-review.sh": script, "scripts/pr-review-local.sh": mustRead(t, "../scripts/pr-review-local.sh"), "scripts/run-task.sh": taskRunner, "scripts/review/validate-agent-output.sh": validator, "scripts/review/validate-codex-output.sh": codexValidator, "harness": []byte(fakeReviewCommand), "openshell": []byte(fakeReviewCommand), "gh": []byte(fakeReviewCommand), "review-policy.yaml": []byte("version: 1\nnetwork_policies: {}\n"), "output": nil, "step-summary": nil} { + for name, data := range map[string][]byte{"scripts/pr-review.sh": script, "scripts/pr-review-local.sh": mustRead(t, "../scripts/pr-review-local.sh"), "scripts/run-task.sh": taskRunner, "scripts/review/agents/codex.sh": codexAgent, "scripts/review/agents/opencode.sh": opencodeAgent, "scripts/review/validate-agent-output.sh": validator, "scripts/review/validate-codex-output.sh": codexValidator, "harness": []byte(fakeReviewCommand), "openshell": []byte(fakeReviewCommand), "gh": []byte(fakeReviewCommand), "review-policy.yaml": []byte("version: 1\nnetwork_policies: {}\n"), "output": nil, "step-summary": nil} { mode := os.FileMode(0o700) if name == "scripts/review/validate-agent-output.sh" { mode = validatorMode @@ -287,6 +292,17 @@ func TestGitHubAppTokenIsHostOnly(t *testing.T) { if !strings.Contains(string(mustRead(t, "../scripts/pr-review.sh")), `REVIEW_SKILL="${REVIEW_SKILL:-$PWD/tasks/github-pr-reviewer/workflow/skills/pr-review/SKILL.md}"`) { t.Fatal("review wrapper default skill must resolve from the trusted checkout") } + if !strings.Contains(string(mustRead(t, "../scripts/pr-review.sh")), `source "scripts/review/agents/$review_agent.sh"`) { + t.Fatal("review wrapper does not load the selected agent profile") + } + for _, agent := range []string{"codex", "opencode"} { + profile := string(mustRead(t, "../scripts/review/agents/"+agent+".sh")) + for _, function := range []string{"agent_configure", "agent_require_credentials", "agent_setup", "agent_workflow_file", "agent_validator", "agent_extract_output"} { + if !strings.Contains(profile, function+"()") { + t.Fatalf("%s agent profile does not implement %s", agent, function) + } + } + } caller := string(mustRead(t, "../.github/workflows/ai-review.yml")) callerWorkflow := parseWorkflow(t, caller) From e04d356006289c7c7a8aeeb382eb8b3cfc677494 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 18 Sep 2026 18:50:15 -0700 Subject: [PATCH 2/3] fix: bootstrap Codex providers in local CI --- .github/workflows/ai-review.yml | 2 ++ .github/workflows/pr-review-reusable.yml | 9 ++++++ docs/ci.md | 9 +++--- scripts/pr-review.sh | 8 +++++ scripts/review/agents/codex.sh | 38 ++++++++++++++++++++---- test/pr_review_test.go | 29 +++++++++++++----- 6 files changed, 79 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index a8a5738..690bbb2 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -20,8 +20,10 @@ jobs: codex-inference-provider: openai-inference codex-model: gpt-5.6-luna codex-workspace: codex-review + codex-bootstrap: true required-providers: '["github-review", "openai-inference"]' openshell-github-app-client-id: ${{ vars.OPENSHELL_GITHUB_APP_CLIENT_ID }} secrets: VERTEX_AI_SERVICE_ACCOUNT_KEY: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_KEY }} OPENSHELL_GITHUB_APP_PRIVATE_KEY: ${{ secrets.OPENSHELL_GITHUB_APP_PRIVATE_KEY }} + OPENSHELL_CODEX_API_KEY: ${{ secrets.OPENSHELL_CODEX_API_KEY }} diff --git a/.github/workflows/pr-review-reusable.yml b/.github/workflows/pr-review-reusable.yml index 763276f..1b2b6f7 100644 --- a/.github/workflows/pr-review-reusable.yml +++ b/.github/workflows/pr-review-reusable.yml @@ -47,6 +47,11 @@ on: required: false type: string default: '' + codex-bootstrap: + description: Create an ephemeral Codex workspace and providers in the local CI gateway + required: false + type: boolean + default: true openshell-github-app-client-id: description: GitHub App Client ID used for the trusted review token required: true @@ -56,6 +61,8 @@ on: required: false OPENSHELL_GITHUB_APP_PRIVATE_KEY: required: true + OPENSHELL_CODEX_API_KEY: + required: false permissions: contents: read @@ -168,6 +175,8 @@ jobs: CODEX_INFERENCE_PROVIDER: ${{ inputs.codex-inference-provider }} CODEX_MODEL: ${{ inputs.codex-model }} CODEX_WORKSPACE: ${{ inputs.codex-workspace }} + CODEX_BOOTSTRAP: ${{ inputs.codex-bootstrap }} + OPENSHELL_CODEX_API_KEY: ${{ secrets.OPENSHELL_CODEX_API_KEY }} TASK_PROVIDERS: ${{ inputs.required-providers }} GITHUB_TOKEN: ${{ steps.openshell-app-token.outputs.token }} REVIEW_REPOSITORY: ${{ github.repository }} diff --git a/docs/ci.md b/docs/ci.md index 8272bf3..cdfd7dc 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -141,10 +141,10 @@ 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, so the caller must arrange a -platform-owned OpenAI-compatible provider first (the default name is -`openai-inference`) in a dedicated workspace. No OpenAI key is passed through the -workflow or sandbox. +`inference.local` Responses API route. 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. Trusted callers select it with: @@ -155,6 +155,7 @@ with: codex-inference-provider: openai-inference codex-model: gpt-5.6-luna codex-workspace: codex-review + codex-bootstrap: true required-providers: '["github-review", "openai-inference"]' ``` diff --git a/scripts/pr-review.sh b/scripts/pr-review.sh index f3c295f..3880fc7 100755 --- a/scripts/pr-review.sh +++ b/scripts/pr-review.sh @@ -32,6 +32,8 @@ max_diff_bytes=262144 created_workspace=false created_vertex_provider=false created_github_provider=false +created_codex_provider=false +created_github_profile=false apply_pid="" head="${REVIEW_HEAD:-}" base="" @@ -80,6 +82,12 @@ cleanup_runtime() { if $created_github_provider; then timeout 30s openshell provider delete --gateway "$gateway" --workspace "$workspace" "$github_provider" || cleanup_status=1 fi + if $created_codex_provider; then + timeout 30s openshell provider delete --gateway "$gateway" --workspace "$workspace" "$codex_inference_provider" || cleanup_status=1 + fi + if $created_github_profile; then + timeout 30s openshell provider profile delete --gateway "$gateway" --workspace "$workspace" github-review || cleanup_status=1 + fi if $created_workspace; then timeout 30s openshell workspace delete --gateway "$gateway" "$workspace" || cleanup_status=1 fi diff --git a/scripts/review/agents/codex.sh b/scripts/review/agents/codex.sh index 06c5e6e..ac170c6 100644 --- a/scripts/review/agents/codex.sh +++ b/scripts/review/agents/codex.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Codex PR-review profile, sourced by scripts/pr-review.sh. # Keep provider and workspace ownership explicit: Codex uses a pre-provisioned -# OpenShell workspace and never creates or deletes its providers. +# OpenShell workspace by default. CI may opt into an ephemeral local gateway +# bootstrap, which keeps the credentials in the gateway and out of the sandbox. # shellcheck disable=SC2034,SC2154 # configuration and lifecycle state are shared with the wrapper. agent_configure() { @@ -14,18 +15,45 @@ agent_configure() { return 1 } - workspace="$codex_workspace" + if [[ "${CODEX_BOOTSTRAP:-false}" == true ]]; then + workspace="codex-$(openssl rand -hex 6)" + configured_target=false + else + workspace="$codex_workspace" + configured_target=true + fi sandbox_name="codex-$(openssl rand -hex 6)" - configured_target=true github_provider=github-review } agent_require_credentials() { - : + if [[ "$configured_target" != true ]]; then + : "${GITHUB_TOKEN:?set the workflow GitHub token for provider bootstrap}" + : "${OPENSHELL_CODEX_API_KEY:?set the OpenAI API key for the local Codex provider}" + fi } agent_setup() { - : + [[ "$configured_target" == true ]] && return 0 + + timeout 60s openshell workspace create --gateway "$gateway" --name "$workspace" + created_workspace=true + + profiles="$(timeout 60s openshell provider list-profiles --gateway "$gateway" --workspace "$workspace" -o json)" + jq -e 'type == "array" and all(.[]; (.id | type) == "string")' <<< "$profiles" >/dev/null + if ! jq -e 'any(.[]; .id == "github-review")' <<< "$profiles" >/dev/null; then + timeout 60s openshell provider profile import --gateway "$gateway" --workspace "$workspace" \ + --file tasks/github-pr-reviewer/openshell/providers/github-review.yaml + created_github_profile=true + fi + 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" \ + --name "$codex_inference_provider" --type openai --credential 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/test/pr_review_test.go b/test/pr_review_test.go index bb41389..e98a142 100644 --- a/test/pr_review_test.go +++ b/test/pr_review_test.go @@ -19,7 +19,7 @@ func TestPRReview(t *testing.T) { if err != nil { t.Fatal(err) } - for _, scenario := range []string{"success", "codex-success", "unlabeled", "stale", "oversized", "tampered", "agent-failure", "provider-failure", "cleanup-failure", "local-success", "local-cancel", "partial-provider-failure", "workspace-failure", "profile-read-failure", "existing-profile", "status-failure", "cancel", "truncated", "malformed-trailing", "incomplete", "empty", "error", "tool_use", "tool_exit", "tool_missing_exit", "read-tool", "tool_recovered", "unrelated-422", "unrelated-422-line", "unrelated-422-comment", "unrelated-comment", "success-then-failure", "comment-position"} { + for _, scenario := range []string{"success", "codex-success", "codex-bootstrap-success", "unlabeled", "stale", "oversized", "tampered", "agent-failure", "provider-failure", "cleanup-failure", "local-success", "local-cancel", "partial-provider-failure", "workspace-failure", "profile-read-failure", "existing-profile", "status-failure", "cancel", "truncated", "malformed-trailing", "incomplete", "empty", "error", "tool_use", "tool_exit", "tool_missing_exit", "read-tool", "tool_recovered", "unrelated-422", "unrelated-422-line", "unrelated-422-comment", "unrelated-comment", "success-then-failure", "comment-position"} { t.Run(scenario, func(t *testing.T) { local := scenario == "provider-failure" || scenario == "cleanup-failure" || scenario == "local-success" || scenario == "local-cancel" || scenario == "partial-provider-failure" || scenario == "workspace-failure" || scenario == "profile-read-failure" || scenario == "existing-profile" root := t.TempDir() @@ -74,9 +74,12 @@ func TestPRReview(t *testing.T) { "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")) - if scenario == "codex-success" { + if strings.HasPrefix(scenario, "codex-") { prepare.Env = append(prepare.Env, "REVIEW_AGENT=codex", "FAKE_AGENT=codex", "GITHUB_TOKEN=fake") } + if scenario == "codex-bootstrap-success" { + prepare.Env = append(prepare.Env, "CODEX_BOOTSTRAP=true", "OPENSHELL_CODEX_API_KEY=fake") + } out, err := prepare.CombinedOutput() if scenario == "oversized" { if err == nil { @@ -128,7 +131,7 @@ func TestPRReview(t *testing.T) { } } err = cmd.Wait() - if (err == nil) != (scenario == "success" || scenario == "codex-success" || scenario == "stale" || scenario == "local-success" || scenario == "existing-profile" || scenario == "comment-position" || scenario == "read-tool" || scenario == "tool_recovered") { + if (err == nil) != (scenario == "success" || scenario == "codex-success" || scenario == "codex-bootstrap-success" || scenario == "stale" || scenario == "local-success" || scenario == "existing-profile" || scenario == "comment-position" || scenario == "read-tool" || scenario == "tool_recovered") { t.Fatalf("unexpected result: %v\n%s", err, logs.String()) } trace, _ := os.ReadFile(filepath.Join(root, "trace")) @@ -138,23 +141,30 @@ func TestPRReview(t *testing.T) { } return } - if scenario != "codex-success" && strings.Contains(string(trace), "sandbox delete") { + if scenario != "codex-success" && scenario != "codex-bootstrap-success" && strings.Contains(string(trace), "sandbox delete") { t.Fatal("review wrapper must leave sandbox cleanup to the runner") } if !local { - if scenario != "codex-success" { + if scenario != "codex-success" && scenario != "codex-bootstrap-success" { for _, action := range []string{"workspace create", "provider create", "inference set"} { if strings.Contains(string(trace), action) { t.Fatalf("existing-target review performed setup or forced a target: %s", trace) } } } - if scenario != "codex-success" && !strings.Contains(string(trace), "target managed-test shared-test") { + if scenario != "codex-success" && scenario != "codex-bootstrap-success" && !strings.Contains(string(trace), "target managed-test shared-test") { t.Fatal("configured target environment was not preserved") } if scenario == "codex-success" && strings.Contains(string(trace), "sandbox delete") { 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"} { + if !strings.Contains(string(trace), action) { + t.Fatalf("Codex bootstrap did not perform %s: %s", action, trace) + } + } + } } else { if strings.Contains(string(trace), "workspace delete") == (scenario == "workspace-failure") { t.Fatalf("incorrect workspace cleanup: %s", trace) @@ -183,7 +193,7 @@ func TestPRReview(t *testing.T) { } } summary, _ := os.ReadFile(filepath.Join(root, "review/summary.md")) - if strings.Contains(string(summary), "AI review: completed") != (scenario == "success" || scenario == "codex-success" || scenario == "local-success" || scenario == "existing-profile" || scenario == "cleanup-failure" || scenario == "comment-position" || scenario == "read-tool" || scenario == "tool_recovered") || strings.Contains(string(summary), "MODEL_OUTPUT") { + if strings.Contains(string(summary), "AI review: completed") != (scenario == "success" || scenario == "codex-success" || scenario == "codex-bootstrap-success" || scenario == "local-success" || scenario == "existing-profile" || scenario == "cleanup-failure" || scenario == "comment-position" || scenario == "read-tool" || scenario == "tool_recovered") || strings.Contains(string(summary), "MODEL_OUTPUT") { t.Fatalf("incorrect or model-controlled summary: %s", summary) } }) @@ -323,6 +333,7 @@ func TestGitHubAppTokenIsHostOnly(t *testing.T) { "codex-inference-provider": "openai-inference", "codex-model": "gpt-5.6-luna", "codex-workspace": "codex-review", + "codex-bootstrap": "true", "required-providers": `["github-review", "openai-inference"]`, } { if reviewJob.With[name] != want { @@ -335,6 +346,7 @@ func TestGitHubAppTokenIsHostOnly(t *testing.T) { for name, want := range map[string]string{ "VERTEX_AI_SERVICE_ACCOUNT_KEY": "${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_KEY }}", "OPENSHELL_GITHUB_APP_PRIVATE_KEY": "${{ secrets.OPENSHELL_GITHUB_APP_PRIVATE_KEY }}", + "OPENSHELL_CODEX_API_KEY": "${{ secrets.OPENSHELL_CODEX_API_KEY }}", } { if reviewJob.Secrets[name] != want { t.Fatalf("caller secret %s is not explicitly forwarded", name) @@ -366,6 +378,9 @@ func TestGitHubAppTokenIsHostOnly(t *testing.T) { t.Fatalf("shared review workflow default %s = %#v, want %s", name, input, want) } } + if input, ok := sharedTrigger.Inputs["codex-bootstrap"]; !ok || input.Default != "true" { + t.Fatalf("shared review workflow default codex-bootstrap = %#v, want true", input) + } if input, ok := sharedTrigger.Inputs["required-providers"]; !ok || input.Default != "[]" { t.Fatalf("shared review workflow default required-providers = %#v, want []", input) } From 0b164bcd06b95db807991f299a1bf2e1727b53b2 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Fri, 18 Sep 2026 19:02:10 -0700 Subject: [PATCH 3/3] ci: pin review caller to bootstrapped harness --- .github/workflows/ai-review.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 690bbb2..7b4322d 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -9,11 +9,11 @@ permissions: jobs: review: - uses: stackrox/harness-openshell/.github/workflows/pr-review-reusable.yml@709f4cc1744e82b8dbea887fa72af98ca539dc6c + uses: stackrox/harness-openshell/.github/workflows/pr-review-reusable.yml@e04d356006289c7c7a8aeeb382eb8b3cfc677494 with: # `uses` pins the workflow adapter; harness-ref pins the checked-out # Harness CLI/scripts. Keep both at the same release commit. - harness-ref: 709f4cc1744e82b8dbea887fa72af98ca539dc6c + harness-ref: e04d356006289c7c7a8aeeb382eb8b3cfc677494 review-label: stackrox-ai-review allow-draft-reviews: false review-agent: codex