From 7416e78dcbb7d866e6d519705c9bd5f4aa4ea384 Mon Sep 17 00:00:00 2001 From: pythoncheng Date: Tue, 18 Aug 2026 14:19:59 +0800 Subject: [PATCH 1/3] feat(profiles): add aig to the production clawhub profile The clawhub profile now runs skillspector, clawscan-static, and aig together instead of evaluating aig only through the clawhub-aig candidate profile. RenderClawHubPrompt now attaches AIG SARIF evidence to the Codex judge whenever aig produced a result, rather than keying off the profile id, so production runs get the same evidence the clawhub-aig candidate already received. The clawhub-aig candidate profile and the malicious-signal heuristic are left unchanged. --- README.md | 2 +- cmd/clawscan/main_test.go | 11 +++++++++- docs/profiles.md | 2 +- internal/profiles/clawhub/clawscan.yml | 2 ++ internal/profiles/registry_test.go | 4 ++-- internal/profiles/resolver_test.go | 12 ++++++----- internal/runner/runner.go | 4 ++-- internal/runner/runner_test.go | 29 ++++++++++++++++++++++++++ 8 files changed, 54 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 96c6e44..7014116 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ clawscan profiles -v | Profile | Scanners | Judge | | --- | --- | --- | -| `clawhub` | `skillspector`, `clawscan-static` | Codex `gpt-5.5`, high reasoning, bundled ClawHub prompt/schema | +| `clawhub` | `skillspector`, `clawscan-static`, `aig` | Codex `gpt-5.5`, high reasoning, bundled ClawHub prompt/schema | | `openclaw-install-policy` | `skillspector`, `clawscan-static` | none | ### Build a custom profile with `.clawscan.yml` diff --git a/cmd/clawscan/main_test.go b/cmd/clawscan/main_test.go index a9fe3ff..50f9b0f 100644 --- a/cmd/clawscan/main_test.go +++ b/cmd/clawscan/main_test.go @@ -542,7 +542,7 @@ profiles: "Scanners", "clawhub", "built-in", - "skillspector, clawscan-static", + "skillspector, clawscan-static, aig", "clawhub-aig", "skillspector, aig", } { @@ -1041,12 +1041,15 @@ func TestRunCommandUsesBuiltInProfile(t *testing.T) { writeSkill(t, target, "# Profile\n") skillSpectorFixture := filepath.Join(dir, "skillspector.json") writeFile(t, skillSpectorFixture, `{"status":"clean","findings":[]}`) + aigFixture := filepath.Join(dir, "aig.sarif.json") + writeFile(t, aigFixture, `{"version":"2.1.0","runs":[{"results":[]}]}`) stdout := captureStdout(t, func() { if err := run([]string{ target, "--profile", "clawhub", "--scanner-result", "skillspector=" + skillSpectorFixture, + "--scanner-result", "aig=" + aigFixture, "--judge", clawHubReceiptJudgeCommand(), "--sandbox", "off", "--json", @@ -1071,6 +1074,9 @@ func TestRunCommandUsesBuiltInProfile(t *testing.T) { if _, ok := artifact.Scanners["clawscan-static"]; !ok { t.Fatalf("missing clawscan-static scanner: %#v", artifact.Scanners) } + if _, ok := artifact.Scanners["aig"]; !ok { + t.Fatalf("missing aig scanner: %#v", artifact.Scanners) + } if artifact.Judge == nil || artifact.Judge.Status != "completed" { t.Fatalf("judge = %#v", artifact.Judge) } @@ -1082,12 +1088,15 @@ func TestRunCommandDiscoversSkillsWithExplicitProfile(t *testing.T) { writeSkill(t, filepath.Join(dir, "skills", "bar"), "# Bar\n") skillSpectorFixture := filepath.Join(dir, "skillspector.json") writeFile(t, skillSpectorFixture, `{"status":"clean","findings":[]}`) + aigFixture := filepath.Join(dir, "aig.sarif.json") + writeFile(t, aigFixture, `{"version":"2.1.0","runs":[{"results":[]}]}`) t.Chdir(dir) stdout := captureStdout(t, func() { if err := run([]string{ "--profile", "clawhub", "--scanner-result", "skillspector=" + skillSpectorFixture, + "--scanner-result", "aig=" + aigFixture, "--judge", clawHubReceiptJudgeCommand(), "--sandbox", "off", "--json", diff --git a/docs/profiles.md b/docs/profiles.md index 143006a..7763f82 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -55,7 +55,7 @@ clawscan profiles -v | Profile | Scanners | Judge | | --- | --- | --- | -| `clawhub` | `skillspector`, `clawscan-static` | Codex `gpt-5.5`, high reasoning, bundled ClawHub prompt/schema | +| `clawhub` | `skillspector`, `clawscan-static`, `aig` | Codex `gpt-5.5`, high reasoning, bundled ClawHub prompt/schema | | `openclaw-install-policy` | `skillspector`, `clawscan-static` | none | ## Build a custom profile with `.clawscan.yml` diff --git a/internal/profiles/clawhub/clawscan.yml b/internal/profiles/clawhub/clawscan.yml index fec0244..2c822d2 100644 --- a/internal/profiles/clawhub/clawscan.yml +++ b/internal/profiles/clawhub/clawscan.yml @@ -5,11 +5,13 @@ profiles: scanners: - skillspector - clawscan-static + - aig sandbox: env: - OPENAI_API_KEY - CODEX_API_KEY - SKILLSPECTOR_PROVIDER + - LLM_API_KEY judge: command: >- [ -n "$CODEX_API_KEY" ] || export CODEX_API_KEY="$OPENAI_API_KEY"; diff --git a/internal/profiles/registry_test.go b/internal/profiles/registry_test.go index ca3ffb5..9fd472c 100644 --- a/internal/profiles/registry_test.go +++ b/internal/profiles/registry_test.go @@ -26,7 +26,7 @@ func TestDefaultProfileRegistryContainsEmbeddedBuiltIns(t *testing.T) { if !ok { t.Fatal("missing clawhub profile") } - if got := strings.Join(profileScannerIDs(clawhub.profile.Scanners), ","); got != "skillspector,clawscan-static" { + if got := strings.Join(profileScannerIDs(clawhub.profile.Scanners), ","); got != "skillspector,clawscan-static,aig" { t.Fatalf("clawhub scanners = %q", got) } if clawhub.configDir != "clawhub" { @@ -91,7 +91,7 @@ func TestInspectProfilesReturnsBuiltIns(t *testing.T) { if !ok { t.Fatal("missing clawhub profile") } - if got := strings.Join(profileScannerIDs(clawhub.Profile.Scanners), ","); got != "skillspector,clawscan-static" { + if got := strings.Join(profileScannerIDs(clawhub.Profile.Scanners), ","); got != "skillspector,clawscan-static,aig" { t.Fatalf("clawhub scanners = %q", got) } if clawhub.Source != "built-in" { diff --git a/internal/profiles/resolver_test.go b/internal/profiles/resolver_test.go index c6d8c05..9df9f4f 100644 --- a/internal/profiles/resolver_test.go +++ b/internal/profiles/resolver_test.go @@ -30,7 +30,7 @@ func TestResolveArgsUsesEmbeddedClawHubProfile(t *testing.T) { if opts.ConfigSource != "built-in" { t.Fatalf("config source = %q, want built-in", opts.ConfigSource) } - if got := strings.Join(opts.Scanners, ","); got != "skillspector,clawscan-static" { + if got := strings.Join(opts.Scanners, ","); got != "skillspector,clawscan-static,aig" { t.Fatalf("scanners = %q", got) } if len(opts.GateRules) != 0 { @@ -66,7 +66,7 @@ func TestResolveArgsUsesEmbeddedClawHubProfile(t *testing.T) { if string(opts.Judge.Files["clawhub/output.schema.json"]) == "" { t.Fatal("expected embedded clawhub output schema file") } - if got := strings.Join(opts.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER" { + if got := strings.Join(opts.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER,LLM_API_KEY" { t.Fatalf("sandbox env = %q", got) } } @@ -358,7 +358,7 @@ func TestResolveArgsAllowsExplicitProfileWithoutTarget(t *testing.T) { if opts.Target != "" { t.Fatalf("target = %q", opts.Target) } - if got := strings.Join(opts.Scanners, ","); got != "skillspector,clawscan-static" { + if got := strings.Join(opts.Scanners, ","); got != "skillspector,clawscan-static,aig" { t.Fatalf("scanners = %q", got) } } @@ -369,7 +369,9 @@ func TestResolveArgsDoesNotRequireVirusTotalForClawHubProfile(t *testing.T) { t.Fatal(err) } - if err := runner.ValidateRequirements(opts, map[string]string{}); err != nil { + // OPENAI_API_KEY is already required by the ClawHub Codex judge for this + // profile, and aig's requirement is satisfied by that same key. + if err := runner.ValidateRequirements(opts, map[string]string{"OPENAI_API_KEY": "present"}); err != nil { t.Fatalf("unexpected requirement error: %v", err) } if strings.Contains(strings.Join(opts.Sandbox.Env, ","), "VIRUSTOTAL_API_KEY") { @@ -691,7 +693,7 @@ func TestResolveArgsAppliesCLIOverrides(t *testing.T) { if opts.Sandbox.Image != "ghcr.io/acme/runtime:v1" { t.Fatalf("sandbox image = %q", opts.Sandbox.Image) } - if got := strings.Join(opts.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER,ANTHROPIC_API_KEY" { + if got := strings.Join(opts.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER,LLM_API_KEY,ANTHROPIC_API_KEY" { t.Fatalf("sandbox env = %q", got) } } diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 992611a..253ca7d 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -1570,10 +1570,10 @@ func RenderClawHubPrompt(systemPromptSource string, artifact Artifact) (string, return "", err } var supplemental []clawhubprompt.ScannerEvidence - if artifact.Profile == clawHubAIGProfileID { + if aigAnalysis := clawHubAIGAnalysis(artifact); aigAnalysis != nil { supplemental = append(supplemental, clawhubprompt.ScannerEvidence{ Label: "A.I.G SARIF evidence supplied to Codex", - Value: clawHubAIGAnalysis(artifact), + Value: aigAnalysis, }) } return clawhubprompt.Build( diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index 8ee851d..94f951c 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -4564,6 +4564,35 @@ func TestRenderClawHubAIGPromptIncludesAIGEvidence(t *testing.T) { } } +func TestRenderClawHubPromptIncludesAIGEvidenceForProductionProfile(t *testing.T) { + // The production "clawhub" profile (not just the retired "clawhub-aig" + // candidate) now runs aig alongside skillspector and clawscan-static, so + // its SARIF evidence must reach the Codex judge whenever it produced a + // result, regardless of which profile label requested the run. The + // pre-scan malicious-signal heuristic is unchanged by this and still + // keys off clawscan-static for the "clawhub" profile label. + prompt, err := RenderClawHubPrompt("SYSTEM", Artifact{ + Profile: "clawhub", + Scanners: map[string]ScannerResult{ + "skillspector": {Raw: json.RawMessage(`{"status":"clean"}`)}, + "clawscan-static": {Raw: json.RawMessage(`{"schemaVersion":"clawscan-static-v1","findings":[]}`)}, + "aig": {Raw: json.RawMessage(`{"version":"2.1.0","runs":[{"results":[{"ruleId":"T04","level":"error"}]}]}`)}, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "SkillSpector findings supplied to Codex:", + "A.I.G SARIF evidence supplied to Codex:", + `"ruleId": "T04"`, + } { + if !strings.Contains(prompt, want) { + t.Fatalf("prompt missing %q:\n%s", want, prompt) + } + } +} + func TestRenderClawHubPromptIgnoresLegacyVirusTotalEvidence(t *testing.T) { prompt, err := RenderClawHubPrompt("SYSTEM", Artifact{ Context: json.RawMessage(`{"skillSpectorCheckedAt":123}`), From 1a0e00b5e985eae56a7bff1e476d30f70c4e2e5f Mon Sep 17 00:00:00 2001 From: pythoncheng Date: Tue, 18 Aug 2026 15:28:35 +0800 Subject: [PATCH 2/3] fix(runner): let aig reuse CODEX_API_KEY for existing clawhub deployments The clawhub profile's Codex judge command already accepts CODEX_API_KEY as an OPENAI_API_KEY substitute, but aig's own credential requirement only recognized LLM_API_KEY/OPENAI_API_KEY, so promoting aig into the production profile broke existing CODEX_API_KEY-only deployments before any scanner could run. applyRuntimeEnvDefaults now backfills OPENAI_API_KEY from CODEX_API_KEY when aig is requested and no other key is set, mirroring the existing SkillSpector provider default. Also fixes a stale "runs both scanners" description now that the clawhub profile lists three scanners. --- README.md | 5 ++-- docs/profiles.md | 5 ++-- internal/runner/runner.go | 21 ++++++++++++++ internal/runner/runner_test.go | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7014116..05f7a1f 100644 --- a/README.md +++ b/README.md @@ -184,8 +184,9 @@ clawscan ./my-skill --profile clawhub ``` The same profile accepts an explicit OpenClaw plugin directory (or its -`openclaw.plugin.json` manifest), runs both scanners, and renders the -bundled judge prompt with `packageRelease` target context. +`openclaw.plugin.json` manifest), runs the plugin-capable scanners in the +profile, and renders the bundled judge prompt with `packageRelease` target +context. Inspect the built-in profile catalog: diff --git a/docs/profiles.md b/docs/profiles.md index 7763f82..8cde270 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -9,8 +9,9 @@ clawscan ./my-skill --profile clawhub ``` The same profile accepts an explicit OpenClaw plugin directory (or its -`openclaw.plugin.json` manifest), runs both scanners, and renders the -bundled judge prompt with `packageRelease` target context. +`openclaw.plugin.json` manifest), runs the plugin-capable scanners in the +profile, and renders the bundled judge prompt with `packageRelease` target +context. ## Config discovery diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 253ca7d..5f20ea8 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -2199,10 +2199,31 @@ func defaultSkillSpectorOpenAIProvider(env map[string]string) { env["SKILLSPECTOR_PROVIDER"] = "openai" } +// defaultAIGKeyFromCodex lets aig reuse an already-configured CODEX_API_KEY +// when no LLM_API_KEY/OPENAI_API_KEY is set. The ClawHub judge command itself +// already treats CODEX_API_KEY as an accepted OPENAI_API_KEY substitute +// (see internal/profiles/clawhub/clawscan.yml), so aig's own credential +// requirement should honor the same fallback instead of demanding a second, +// separately-named key from existing CODEX_API_KEY-only deployments. +func defaultAIGKeyFromCodex(env map[string]string) { + if env == nil { + return + } + if strings.TrimSpace(env["LLM_API_KEY"]) != "" || strings.TrimSpace(env["OPENAI_API_KEY"]) != "" { + return + } + if codexKey := strings.TrimSpace(env["CODEX_API_KEY"]); codexKey != "" { + env["OPENAI_API_KEY"] = codexKey + } +} + func applyRuntimeEnvDefaults(opts Options, env map[string]string) { if scannerRequested(opts, "skillspector") { defaultSkillSpectorOpenAIProvider(env) } + if scannerRequested(opts, "aig") { + defaultAIGKeyFromCodex(env) + } } func discoverSkillSpectorCommand() []string { diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index 94f951c..e052296 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -2849,6 +2849,57 @@ func TestSkillSpectorDefaultsProviderToOpenAIWhenOpenAIKeyIsPresent(t *testing.T } } +func TestAIGReusesCodexAPIKeyWhenNoOtherKeyIsSet(t *testing.T) { + // The clawhub judge command already treats CODEX_API_KEY as an accepted + // OPENAI_API_KEY substitute, so existing CODEX_API_KEY-only ClawHub + // deployments must keep working once aig joins the profile's scanners. + env := map[string]string{"CODEX_API_KEY": "codex-value"} + defaultAIGKeyFromCodex(env) + if env["OPENAI_API_KEY"] != "codex-value" { + t.Fatalf("OPENAI_API_KEY not backfilled from CODEX_API_KEY: %#v", env) + } + + preferExisting := map[string]string{"CODEX_API_KEY": "codex-value", "OPENAI_API_KEY": "openai-value"} + defaultAIGKeyFromCodex(preferExisting) + if preferExisting["OPENAI_API_KEY"] != "openai-value" { + t.Fatalf("existing OPENAI_API_KEY was overwritten: %#v", preferExisting) + } + + preferLLMKey := map[string]string{"CODEX_API_KEY": "codex-value", "LLM_API_KEY": "llm-value"} + defaultAIGKeyFromCodex(preferLLMKey) + if _, ok := preferLLMKey["OPENAI_API_KEY"]; ok { + t.Fatalf("OPENAI_API_KEY backfilled despite existing LLM_API_KEY: %#v", preferLLMKey) + } + + withoutCodexKey := map[string]string{} + defaultAIGKeyFromCodex(withoutCodexKey) + if _, ok := withoutCodexKey["OPENAI_API_KEY"]; ok { + t.Fatalf("OPENAI_API_KEY backfilled without a CODEX_API_KEY source: %#v", withoutCodexKey) + } +} + +func TestApplyRuntimeEnvDefaultsBackfillsAIGKeyOnlyWhenAIGRequested(t *testing.T) { + requested, err := ParseArgs([]string{"./skill", "--scanner", "aig"}) + if err != nil { + t.Fatal(err) + } + env := map[string]string{"CODEX_API_KEY": "codex-value"} + applyRuntimeEnvDefaults(requested, env) + if err := ValidateRequirements(requested, env); err != nil { + t.Fatalf("unexpected requirement error after backfill: %v", err) + } + + notRequested, err := ParseArgs([]string{"./skill", "--scanner", "clawscan-static"}) + if err != nil { + t.Fatal(err) + } + unaffected := map[string]string{"CODEX_API_KEY": "codex-value"} + applyRuntimeEnvDefaults(notRequested, unaffected) + if _, ok := unaffected["OPENAI_API_KEY"]; ok { + t.Fatalf("OPENAI_API_KEY backfilled without aig in the scanner list: %#v", unaffected) + } +} + func TestRunExecutesAgentVerusScanner(t *testing.T) { dir := t.TempDir() target := filepath.Join(dir, "skill") From 207defc74b684c4bc362b149f1413c12ae1b956c Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Thu, 20 Aug 2026 13:19:27 -0700 Subject: [PATCH 3/3] fix(runner): isolate aig credential fallback --- internal/profiles/clawhub/clawscan.yml | 4 ++ internal/profiles/resolver_test.go | 6 +- internal/runner/runner.go | 35 +++++++---- internal/runner/runner_test.go | 80 ++++++++++++++++++-------- 4 files changed, 88 insertions(+), 37 deletions(-) diff --git a/internal/profiles/clawhub/clawscan.yml b/internal/profiles/clawhub/clawscan.yml index 2c822d2..e4bd6a9 100644 --- a/internal/profiles/clawhub/clawscan.yml +++ b/internal/profiles/clawhub/clawscan.yml @@ -12,6 +12,8 @@ profiles: - CODEX_API_KEY - SKILLSPECTOR_PROVIDER - LLM_API_KEY + - DEFAULT_MODEL + - DEFAULT_BASE_URL judge: command: >- [ -n "$CODEX_API_KEY" ] || export CODEX_API_KEY="$OPENAI_API_KEY"; @@ -40,6 +42,8 @@ profiles: - CODEX_API_KEY - SKILLSPECTOR_PROVIDER - LLM_API_KEY + - DEFAULT_MODEL + - DEFAULT_BASE_URL judge: command: >- [ -n "$CODEX_API_KEY" ] || export CODEX_API_KEY="$OPENAI_API_KEY"; diff --git a/internal/profiles/resolver_test.go b/internal/profiles/resolver_test.go index 9df9f4f..02c863d 100644 --- a/internal/profiles/resolver_test.go +++ b/internal/profiles/resolver_test.go @@ -66,7 +66,7 @@ func TestResolveArgsUsesEmbeddedClawHubProfile(t *testing.T) { if string(opts.Judge.Files["clawhub/output.schema.json"]) == "" { t.Fatal("expected embedded clawhub output schema file") } - if got := strings.Join(opts.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER,LLM_API_KEY" { + if got := strings.Join(opts.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER,LLM_API_KEY,DEFAULT_MODEL,DEFAULT_BASE_URL" { t.Fatalf("sandbox env = %q", got) } } @@ -100,7 +100,7 @@ func TestResolveArgsUsesEmbeddedClawHubAIGCandidateProfile(t *testing.T) { t.Fatalf("candidate judge file %s differs from clawhub", path) } } - if got := strings.Join(candidate.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER,LLM_API_KEY" { + if got := strings.Join(candidate.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER,LLM_API_KEY,DEFAULT_MODEL,DEFAULT_BASE_URL" { t.Fatalf("sandbox env = %q", got) } } @@ -693,7 +693,7 @@ func TestResolveArgsAppliesCLIOverrides(t *testing.T) { if opts.Sandbox.Image != "ghcr.io/acme/runtime:v1" { t.Fatalf("sandbox image = %q", opts.Sandbox.Image) } - if got := strings.Join(opts.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER,LLM_API_KEY,ANTHROPIC_API_KEY" { + if got := strings.Join(opts.Sandbox.Env, ","); got != "OPENAI_API_KEY,CODEX_API_KEY,SKILLSPECTOR_PROVIDER,LLM_API_KEY,DEFAULT_MODEL,DEFAULT_BASE_URL,ANTHROPIC_API_KEY" { t.Fatalf("sandbox env = %q", got) } } diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 5f20ea8..db26c35 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -2199,21 +2199,34 @@ func defaultSkillSpectorOpenAIProvider(env map[string]string) { env["SKILLSPECTOR_PROVIDER"] = "openai" } -// defaultAIGKeyFromCodex lets aig reuse an already-configured CODEX_API_KEY -// when no LLM_API_KEY/OPENAI_API_KEY is set. The ClawHub judge command itself -// already treats CODEX_API_KEY as an accepted OPENAI_API_KEY substitute -// (see internal/profiles/clawhub/clawscan.yml), so aig's own credential -// requirement should honor the same fallback instead of demanding a second, -// separately-named key from existing CODEX_API_KEY-only deployments. -func defaultAIGKeyFromCodex(env map[string]string) { +const ( + defaultAIGOpenAIModel = "gpt-5.5" + defaultAIGOpenAIBaseURL = "https://api.openai.com/v1" +) + +// defaultAIGRuntimeEnv keeps credentials on their intended provider. AIG's +// upstream default is OpenRouter, so an OpenAI or Codex credential must also +// select OpenAI's endpoint and a compatible model. A dedicated LLM_API_KEY +// retains AIG's documented provider defaults. +func defaultAIGRuntimeEnv(env map[string]string) { if env == nil { return } - if strings.TrimSpace(env["LLM_API_KEY"]) != "" || strings.TrimSpace(env["OPENAI_API_KEY"]) != "" { + if strings.TrimSpace(env["LLM_API_KEY"]) != "" { return } - if codexKey := strings.TrimSpace(env["CODEX_API_KEY"]); codexKey != "" { - env["OPENAI_API_KEY"] = codexKey + if strings.TrimSpace(env["OPENAI_API_KEY"]) == "" { + codexKey := strings.TrimSpace(env["CODEX_API_KEY"]) + if codexKey == "" { + return + } + env["LLM_API_KEY"] = codexKey + } + if strings.TrimSpace(env["DEFAULT_MODEL"]) == "" { + env["DEFAULT_MODEL"] = defaultAIGOpenAIModel + } + if strings.TrimSpace(env["DEFAULT_BASE_URL"]) == "" { + env["DEFAULT_BASE_URL"] = defaultAIGOpenAIBaseURL } } @@ -2222,7 +2235,7 @@ func applyRuntimeEnvDefaults(opts Options, env map[string]string) { defaultSkillSpectorOpenAIProvider(env) } if scannerRequested(opts, "aig") { - defaultAIGKeyFromCodex(env) + defaultAIGRuntimeEnv(env) } } diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index e052296..817df76 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -2849,32 +2849,54 @@ func TestSkillSpectorDefaultsProviderToOpenAIWhenOpenAIKeyIsPresent(t *testing.T } } -func TestAIGReusesCodexAPIKeyWhenNoOtherKeyIsSet(t *testing.T) { +func TestAIGRuntimeDefaultsKeepCredentialsOnTheirProvider(t *testing.T) { // The clawhub judge command already treats CODEX_API_KEY as an accepted - // OPENAI_API_KEY substitute, so existing CODEX_API_KEY-only ClawHub - // deployments must keep working once aig joins the profile's scanners. - env := map[string]string{"CODEX_API_KEY": "codex-value"} - defaultAIGKeyFromCodex(env) - if env["OPENAI_API_KEY"] != "codex-value" { - t.Fatalf("OPENAI_API_KEY not backfilled from CODEX_API_KEY: %#v", env) + // OpenAI credential. AIG defaults to OpenRouter, so reuse must also bind the + // request to OpenAI without enabling SkillSpector's OpenAI mode. + env := map[string]string{"CODEX_API_KEY": "fake"} + defaultAIGRuntimeEnv(env) + if env["LLM_API_KEY"] != "fake" { + t.Fatalf("LLM_API_KEY not backfilled from CODEX_API_KEY: %#v", env) + } + if env["DEFAULT_MODEL"] != defaultAIGOpenAIModel || env["DEFAULT_BASE_URL"] != defaultAIGOpenAIBaseURL { + t.Fatalf("AIG OpenAI defaults not applied: %#v", env) + } + if skillSpectorLLMEnabled(env) { + t.Fatalf("AIG fallback unexpectedly enabled SkillSpector LLM mode: %#v", env) + } + + openAI := map[string]string{"OPENAI_API_KEY": "fake"} + defaultAIGRuntimeEnv(openAI) + if openAI["DEFAULT_MODEL"] != defaultAIGOpenAIModel || openAI["DEFAULT_BASE_URL"] != defaultAIGOpenAIBaseURL { + t.Fatalf("OPENAI_API_KEY did not select OpenAI defaults: %#v", openAI) + } + if _, ok := openAI["LLM_API_KEY"]; ok { + t.Fatalf("LLM_API_KEY backfilled despite existing OPENAI_API_KEY: %#v", openAI) } - preferExisting := map[string]string{"CODEX_API_KEY": "codex-value", "OPENAI_API_KEY": "openai-value"} - defaultAIGKeyFromCodex(preferExisting) - if preferExisting["OPENAI_API_KEY"] != "openai-value" { - t.Fatalf("existing OPENAI_API_KEY was overwritten: %#v", preferExisting) + explicitOpenAI := map[string]string{ + "OPENAI_API_KEY": "fake", + "DEFAULT_MODEL": "custom-model", + "DEFAULT_BASE_URL": "https://example.invalid/v1", + } + defaultAIGRuntimeEnv(explicitOpenAI) + if explicitOpenAI["DEFAULT_MODEL"] != "custom-model" || explicitOpenAI["DEFAULT_BASE_URL"] != "https://example.invalid/v1" { + t.Fatalf("explicit AIG provider settings were overwritten: %#v", explicitOpenAI) } - preferLLMKey := map[string]string{"CODEX_API_KEY": "codex-value", "LLM_API_KEY": "llm-value"} - defaultAIGKeyFromCodex(preferLLMKey) - if _, ok := preferLLMKey["OPENAI_API_KEY"]; ok { - t.Fatalf("OPENAI_API_KEY backfilled despite existing LLM_API_KEY: %#v", preferLLMKey) + dedicated := map[string]string{"CODEX_API_KEY": "fake", "OPENAI_API_KEY": "fake", "LLM_API_KEY": "fake"} + defaultAIGRuntimeEnv(dedicated) + if _, ok := dedicated["DEFAULT_MODEL"]; ok { + t.Fatalf("dedicated LLM_API_KEY received an OpenAI model default: %#v", dedicated) + } + if _, ok := dedicated["DEFAULT_BASE_URL"]; ok { + t.Fatalf("dedicated LLM_API_KEY received an OpenAI base URL default: %#v", dedicated) } - withoutCodexKey := map[string]string{} - defaultAIGKeyFromCodex(withoutCodexKey) - if _, ok := withoutCodexKey["OPENAI_API_KEY"]; ok { - t.Fatalf("OPENAI_API_KEY backfilled without a CODEX_API_KEY source: %#v", withoutCodexKey) + withoutKey := map[string]string{} + defaultAIGRuntimeEnv(withoutKey) + if len(withoutKey) != 0 { + t.Fatalf("AIG defaults applied without a credential source: %#v", withoutKey) } } @@ -2883,20 +2905,32 @@ func TestApplyRuntimeEnvDefaultsBackfillsAIGKeyOnlyWhenAIGRequested(t *testing.T if err != nil { t.Fatal(err) } - env := map[string]string{"CODEX_API_KEY": "codex-value"} + env := map[string]string{"CODEX_API_KEY": "fake"} applyRuntimeEnvDefaults(requested, env) if err := ValidateRequirements(requested, env); err != nil { t.Fatalf("unexpected requirement error after backfill: %v", err) } + if env["LLM_API_KEY"] != "fake" { + t.Fatalf("AIG key not backfilled into LLM_API_KEY: %#v", env) + } + if env["DEFAULT_MODEL"] != defaultAIGOpenAIModel || env["DEFAULT_BASE_URL"] != defaultAIGOpenAIBaseURL { + t.Fatalf("AIG OpenAI defaults not applied: %#v", env) + } + if skillSpectorLLMEnabled(env) { + t.Fatalf("AIG fallback unexpectedly enabled SkillSpector LLM mode: %#v", env) + } notRequested, err := ParseArgs([]string{"./skill", "--scanner", "clawscan-static"}) if err != nil { t.Fatal(err) } - unaffected := map[string]string{"CODEX_API_KEY": "codex-value"} + unaffected := map[string]string{"CODEX_API_KEY": "fake"} applyRuntimeEnvDefaults(notRequested, unaffected) - if _, ok := unaffected["OPENAI_API_KEY"]; ok { - t.Fatalf("OPENAI_API_KEY backfilled without aig in the scanner list: %#v", unaffected) + if _, ok := unaffected["LLM_API_KEY"]; ok { + t.Fatalf("LLM_API_KEY backfilled without aig in the scanner list: %#v", unaffected) + } + if _, ok := unaffected["DEFAULT_BASE_URL"]; ok { + t.Fatalf("AIG provider defaults applied without aig in the scanner list: %#v", unaffected) } }