From 95331c94f638401510c2a82a392ae71bee2b8d02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 17:40:42 +0000 Subject: [PATCH 1/6] Plan pi provider compliance fixes Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/schemas/github-workflow.json | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/workflow/schemas/github-workflow.json b/pkg/workflow/schemas/github-workflow.json index c2f1f432201..338994b294a 100644 --- a/pkg/workflow/schemas/github-workflow.json +++ b/pkg/workflow/schemas/github-workflow.json @@ -18,7 +18,7 @@ "properties": { "group": { "$comment": "https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-using-concurrency-to-cancel-any-in-progress-job-or-run-1", - "description": "When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. Any previously pending job or workflow in the concurrency group will be canceled.", + "description": "When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`.", "type": "string" }, "cancel-in-progress": { @@ -32,6 +32,13 @@ "$ref": "#/definitions/expressionSyntax" } ] + }, + "queue": { + "$comment": "https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#example-queueing-multiple-pending-runs", + "description": "Controls how pending jobs or workflow runs are queued within a concurrency group. With the default `single`, at most one run can be pending — additional pending runs cancel the previous one. With `max`, up to 100 runs can be pending and are processed in FIFO order. The combination of `queue: max` and `cancel-in-progress: true` is not allowed.", + "type": "string", + "enum": ["single", "max"], + "default": "single" } }, "required": ["group"], @@ -718,7 +725,7 @@ }, "concurrency": { "$comment": "https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idconcurrency", - "description": "Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. \nYou can also specify concurrency at the workflow level. \nWhen a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. Any previously pending job or workflow in the concurrency group will be canceled. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.", + "description": "Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. \nYou can also specify concurrency at the workflow level. \nWhen a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.", "oneOf": [ { "type": "string" @@ -921,7 +928,7 @@ }, "concurrency": { "$comment": "https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idconcurrency", - "description": "Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. \nYou can also specify concurrency at the workflow level. \nWhen a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. Any previously pending job or workflow in the concurrency group will be canceled. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.", + "description": "Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. \nYou can also specify concurrency at the workflow level. \nWhen a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.", "oneOf": [ { "type": "string" @@ -1780,7 +1787,7 @@ }, "concurrency": { "$comment": "https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#concurrency", - "description": "Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. \nYou can also specify concurrency at the workflow level. \nWhen a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. Any previously pending job or workflow in the concurrency group will be canceled. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.", + "description": "Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. \nYou can also specify concurrency at the workflow level. \nWhen a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.", "oneOf": [ { "type": "string" From 9a792fffdec77860a28f74cc6bb8c4f36675f5d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 17:44:46 +0000 Subject: [PATCH 2/6] Fix pi provider setup and metadata Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/pi_provider.cjs | 117 ++++++++++++++++++-- actions/setup/js/pi_provider.test.cjs | 93 ++++++++++++++++ pkg/constants/constants_test.go | 24 ++++ pkg/constants/engine_constants.go | 17 +-- pkg/workflow/agent_validation.go | 17 +++ pkg/workflow/agent_validation_model_test.go | 33 ++++++ pkg/workflow/compiler_orchestrator_tools.go | 4 + pkg/workflow/data/engines/crush.md | 2 +- pkg/workflow/data/engines/opencode.md | 2 +- pkg/workflow/data/engines/pi.md | 2 +- pkg/workflow/engine_auth_test.go | 3 + pkg/workflow/engine_definition_test.go | 5 +- pkg/workflow/pi_engine.go | 10 +- pkg/workflow/pi_engine_test.go | 1 + 14 files changed, 302 insertions(+), 28 deletions(-) create mode 100644 actions/setup/js/pi_provider.test.cjs diff --git a/actions/setup/js/pi_provider.cjs b/actions/setup/js/pi_provider.cjs index adf137faaaa..fd110074086 100644 --- a/actions/setup/js/pi_provider.cjs +++ b/actions/setup/js/pi_provider.cjs @@ -3,11 +3,12 @@ /** * Pi Provider Extension for gh-aw * - * Calls the AWF API proxy /reflect endpoint at session start to dynamically - * discover the open LLM inference paths configured for this run. This gives - * operators runtime visibility into which provider/model combination is active - * and verifies that the expected gateway port is reachable before the agent - * starts working. + * Registers Pi providers from the AWF-injected environment and calls the AWF + * API proxy /reflect endpoint at session start to dynamically discover the + * open LLM inference paths configured for this run. This gives operators + * runtime visibility into which provider/model combination is active and + * verifies that the expected gateway port is reachable before the agent starts + * working. * * When the model uses provider/model format (e.g. "copilot/claude-sonnet-4"), * the extension logs the matched endpoint so failures can be diagnosed without @@ -18,7 +19,10 @@ * configuration is required. * * Configuration (read from environment variables): - * PI_MODEL The engine.model value; may be "provider/model" or bare "model". + * GH_AW_PI_MODEL The original engine.model value; may be "provider/model" + * or bare "model". Preferred over PI_MODEL so gh-aw can pass + * model context to extensions without changing Pi CLI behavior. + * PI_MODEL Legacy fallback used when GH_AW_PI_MODEL is not set. */ "use strict"; @@ -29,6 +33,15 @@ const { fetchAWFReflect, AWF_API_PROXY_REFLECT_URL, AWF_REFLECT_OUTPUT_PATH, AWF // prettier-ignore const DEFAULT_LOGGER = /** @type {(msg: string) => void} */ (msg => process.stderr.write(`[gh-aw/pi-provider] ${new Date().toISOString()} ${msg}\n`)); +/** + * Return the workflow-configured model string exposed to Pi extensions. + * + * @returns {string} + */ +function getConfiguredModel() { + return process.env.GH_AW_PI_MODEL || process.env.PI_MODEL || ""; +} + /** * Extract the provider prefix from a "provider/model" string. * Returns an empty string when no slash is present (bare model name). @@ -66,14 +79,91 @@ function resolveGatewayUrl(provider) { return `http://api-proxy:${port}`; } +/** + * Register a Pi provider and any aliases. + * + * @param {any} pi + * @param {string[]} names + * @param {Record} config + * @param {(msg: string) => void} logger + */ +function registerProviderAliases(pi, names, config, logger) { + for (const name of names) { + pi.registerProvider(name, config); + logger(`registered provider=${name}`); + } +} + +/** + * Register all supported Pi providers discovered from the environment. + * + * @param {any} pi + * @param {(msg: string) => void} logger + * @returns {number} + */ +function registerConfiguredProviders(pi, logger) { + let registeredCount = 0; + + const copilotToken = process.env.COPILOT_GITHUB_TOKEN || process.env.GITHUB_TOKEN; + if (copilotToken) { + registerProviderAliases( + pi, + ["github-copilot", "copilot"], + { + apiKey: copilotToken, + api: "openai-completions", + ...(process.env.GITHUB_COPILOT_BASE_URL ? { baseUrl: process.env.GITHUB_COPILOT_BASE_URL } : {}), + }, + logger + ); + registeredCount += 2; + } + + if (process.env.ANTHROPIC_API_KEY) { + registerProviderAliases( + pi, + ["anthropic"], + { + apiKey: process.env.ANTHROPIC_API_KEY, + api: "anthropic", + ...(process.env.ANTHROPIC_BASE_URL ? { baseUrl: process.env.ANTHROPIC_BASE_URL } : {}), + }, + logger + ); + registeredCount += 1; + } + + const openAIKey = process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY; + if (openAIKey) { + registerProviderAliases( + pi, + ["openai", "codex"], + { + apiKey: openAIKey, + api: "openai-completions", + ...(process.env.OPENAI_BASE_URL ? { baseUrl: process.env.OPENAI_BASE_URL } : {}), + }, + logger + ); + registeredCount += 2; + } + + if (registeredCount === 0) { + logger("no provider credentials detected for Pi provider registration"); + } + + return registeredCount; +} + /** * Pi provider extension for gh-aw. * - * Subscribes to the `agent_start` and `agent_end` Pi SDK events and calls the AWF /reflect - * endpoint to discover and log the open LLM inference paths before the agent begins its - * first turn and again after it finishes. The post-run fetch is the authoritative snapshot - * used by the step summary; the pre-run fetch captures the initial proxy state for diagnostics - * in case the session exits unexpectedly before reaching `agent_end`. + * Registers providers immediately, then subscribes to the `agent_start` and `agent_end` + * Pi SDK events and calls the AWF /reflect endpoint to discover and log the open LLM + * inference paths before the agent begins its first turn and again after it finishes. + * The post-run fetch is the authoritative snapshot used by the step summary; the pre-run + * fetch captures the initial proxy state for diagnostics in case the session exits + * unexpectedly before reaching `agent_end`. * Both calls are best-effort: any network or parse error is logged but does not abort the * agent session. * @@ -82,9 +172,10 @@ function resolveGatewayUrl(provider) { */ function piProviderExtension(pi) { const log = DEFAULT_LOGGER; + registerConfiguredProviders(pi, log); pi.on("agent_start", async () => { - const model = process.env.PI_MODEL || ""; + const model = getConfiguredModel(); const provider = extractProviderFromModel(model); if (provider) { @@ -123,5 +214,7 @@ function piProviderExtension(pi) { } module.exports = piProviderExtension; +module.exports.getConfiguredModel = getConfiguredModel; module.exports.extractProviderFromModel = extractProviderFromModel; module.exports.resolveGatewayUrl = resolveGatewayUrl; +module.exports.registerConfiguredProviders = registerConfiguredProviders; diff --git a/actions/setup/js/pi_provider.test.cjs b/actions/setup/js/pi_provider.test.cjs new file mode 100644 index 00000000000..72aa1f958f0 --- /dev/null +++ b/actions/setup/js/pi_provider.test.cjs @@ -0,0 +1,93 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("pi_provider.cjs", () => { + let module; + let originalEnv; + let originalFetch; + let stderrOutput; + + beforeEach(async () => { + originalEnv = { ...process.env }; + originalFetch = global.fetch; + stderrOutput = []; + vi.spyOn(process.stderr, "write").mockImplementation(msg => { + stderrOutput.push(String(msg)); + return true; + }); + module = await import("./pi_provider.cjs?" + Date.now()); + }); + + afterEach(() => { + process.env = originalEnv; + global.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it("prefers GH_AW_PI_MODEL over PI_MODEL", () => { + process.env.GH_AW_PI_MODEL = "copilot/claude-sonnet-4"; + process.env.PI_MODEL = "anthropic/claude-opus-4"; + + expect(module.getConfiguredModel()).toBe("copilot/claude-sonnet-4"); + }); + + it("registers configured providers and aliases from the environment", () => { + process.env.COPILOT_GITHUB_TOKEN = "copilot-token"; + process.env.GITHUB_COPILOT_BASE_URL = "https://copilot.example.test"; + process.env.ANTHROPIC_API_KEY = "anthropic-token"; + process.env.ANTHROPIC_BASE_URL = "https://anthropic.example.test"; + process.env.CODEX_API_KEY = "codex-token"; + process.env.OPENAI_BASE_URL = "https://openai.example.test"; + + const calls = []; + const pi = { + registerProvider: vi.fn((name, config) => { + calls.push([name, config]); + }), + on: vi.fn(), + }; + + const count = module.registerConfiguredProviders(pi, () => {}); + + expect(count).toBe(5); + expect(calls).toEqual([ + [ + "github-copilot", + { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }, + ], + [ + "copilot", + { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }, + ], + [ + "anthropic", + { apiKey: "anthropic-token", api: "anthropic", baseUrl: "https://anthropic.example.test" }, + ], + [ + "openai", + { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }, + ], + [ + "codex", + { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }, + ], + ]); + }); + + it("logs the configured provider using GH_AW_PI_MODEL during agent_start", async () => { + process.env.GH_AW_PI_MODEL = "copilot/claude-sonnet-4"; + global.fetch = vi.fn().mockRejectedValue(new Error("network disabled")); + + const handlers = {}; + const pi = { + registerProvider: vi.fn(), + on: vi.fn((event, handler) => { + handlers[event] = handler; + }), + }; + + module.default(pi); + await handlers.agent_start(); + + expect(stderrOutput.some(line => line.includes("provider=copilot model=copilot/claude-sonnet-4"))).toBe(true); + }); +}); diff --git a/pkg/constants/constants_test.go b/pkg/constants/constants_test.go index bc8d6cb2b9d..1e7a527ec36 100644 --- a/pkg/constants/constants_test.go +++ b/pkg/constants/constants_test.go @@ -4,6 +4,7 @@ package constants import ( "path/filepath" + "reflect" "testing" "time" ) @@ -659,3 +660,26 @@ func TestGetAllEngineSecretNames(t *testing.T) { seen[s] = true } } + +func TestEngineOptions_MultiProviderAlternatives(t *testing.T) { + tests := []struct { + engine string + want []string + }{ + {"opencode", []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"}}, + {"crush", []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"}}, + {"pi", []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"}}, + } + + for _, tt := range tests { + t.Run(tt.engine, func(t *testing.T) { + opt := GetEngineOption(tt.engine) + if opt == nil { + t.Fatalf("GetEngineOption(%q) returned nil", tt.engine) + } + if !reflect.DeepEqual(tt.want, opt.AlternativeSecrets) { + t.Fatalf("AlternativeSecrets = %#v, want %#v", opt.AlternativeSecrets, tt.want) + } + }) + } +} diff --git a/pkg/constants/engine_constants.go b/pkg/constants/engine_constants.go index 349f59283a9..dff534836a5 100644 --- a/pkg/constants/engine_constants.go +++ b/pkg/constants/engine_constants.go @@ -94,7 +94,7 @@ var EngineOptions = []EngineOption{ Label: "OpenCode", Description: "OpenCode multi-provider AI coding agent (BYOK)", SecretName: "COPILOT_GITHUB_TOKEN", - AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "GEMINI_API_KEY"}, + AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"}, KeyURL: "https://github.com/anomalyco/opencode", WhenNeeded: "OpenCode engine workflows (default: Copilot routing)", }, @@ -103,17 +103,18 @@ var EngineOptions = []EngineOption{ Label: "Crush", Description: "Crush multi-provider AI coding agent (BYOK)", SecretName: "COPILOT_GITHUB_TOKEN", - AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "GEMINI_API_KEY"}, + AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"}, KeyURL: "https://github.com/charmbracelet/crush#installation", WhenNeeded: "Crush engine workflows (default: Copilot routing)", }, { - Value: string(PiEngine), - Label: "Pi", - Description: "Pi AI coding agent (experimental)", - SecretName: "COPILOT_GITHUB_TOKEN", - KeyURL: "https://github.com/settings/personal-access-tokens/new", - WhenNeeded: "Pi engine workflows", + Value: string(PiEngine), + Label: "Pi", + Description: "Pi AI coding agent (experimental)", + SecretName: "COPILOT_GITHUB_TOKEN", + AlternativeSecrets: []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"}, + KeyURL: "https://github.com/settings/personal-access-tokens/new", + WhenNeeded: "Pi engine workflows", }, } diff --git a/pkg/workflow/agent_validation.go b/pkg/workflow/agent_validation.go index 2cb4a9277c1..40a32d4c67c 100644 --- a/pkg/workflow/agent_validation.go +++ b/pkg/workflow/agent_validation.go @@ -174,6 +174,23 @@ func (c *Compiler) validateUniversalLLMConsumerModel(frontmatter map[string]any, return nil } +// validatePiEngineRequirements validates Pi's required tool configuration. +func (c *Compiler) validatePiEngineRequirements(tools *ToolsConfig, engine CodingAgentEngine) error { + if engine.GetID() != "pi" { + return nil + } + + if tools == nil || tools.GitHub == nil || tools.GitHub.Mode != "gh-proxy" { + return fmt.Errorf("engine 'pi' requires tools.github.mode: gh-proxy") + } + + if !tools.CLIProxy { + return fmt.Errorf("engine 'pi' requires tools.cli-proxy: true") + } + + return nil +} + // validateWebSearchSupport validates that web-search tool is only used with engines that support this feature func (c *Compiler) validateWebSearchSupport(tools map[string]any, engine CodingAgentEngine) { // Check if web-search tool is requested diff --git a/pkg/workflow/agent_validation_model_test.go b/pkg/workflow/agent_validation_model_test.go index 25e32c3847f..14c4e2de298 100644 --- a/pkg/workflow/agent_validation_model_test.go +++ b/pkg/workflow/agent_validation_model_test.go @@ -78,3 +78,36 @@ func TestValidateUniversalLLMConsumerModel(t *testing.T) { assert.NoError(t, err, "Supported provider/model should pass") }) } + +func TestValidatePiEngineRequirements(t *testing.T) { + compiler := NewCompiler() + + t.Run("non pi engine skips validation", func(t *testing.T) { + err := compiler.validatePiEngineRequirements(NewTools(map[string]any{}), NewCopilotEngine()) + assert.NoError(t, err) + }) + + t.Run("pi requires github gh-proxy mode", func(t *testing.T) { + err := compiler.validatePiEngineRequirements(NewTools(map[string]any{ + "github": true, + }), NewPiEngine()) + require.Error(t, err) + assert.Contains(t, err.Error(), "tools.github.mode: gh-proxy") + }) + + t.Run("pi requires cli-proxy", func(t *testing.T) { + err := compiler.validatePiEngineRequirements(NewTools(map[string]any{ + "github": map[string]any{"mode": "gh-proxy"}, + }), NewPiEngine()) + require.Error(t, err) + assert.Contains(t, err.Error(), "tools.cli-proxy: true") + }) + + t.Run("valid pi tool config passes", func(t *testing.T) { + err := compiler.validatePiEngineRequirements(NewTools(map[string]any{ + "github": map[string]any{"mode": "gh-proxy"}, + "cli-proxy": true, + }), NewPiEngine()) + assert.NoError(t, err) + }) +} diff --git a/pkg/workflow/compiler_orchestrator_tools.go b/pkg/workflow/compiler_orchestrator_tools.go index 0fd0f112dd6..379ebb74e05 100644 --- a/pkg/workflow/compiler_orchestrator_tools.go +++ b/pkg/workflow/compiler_orchestrator_tools.go @@ -229,6 +229,10 @@ func (c *Compiler) processToolsAndMarkdown(result *parser.FrontmatterResult, cle return nil, err } + if err := c.validatePiEngineRequirements(NewTools(tools), agenticEngine); err != nil { + return nil, err + } + // Validate web-search support for the current engine (warning only) c.validateWebSearchSupport(tools, agenticEngine) diff --git a/pkg/workflow/data/engines/crush.md b/pkg/workflow/data/engines/crush.md index b095d4d7457..5cb862697a7 100644 --- a/pkg/workflow/data/engines/crush.md +++ b/pkg/workflow/data/engines/crush.md @@ -5,7 +5,7 @@ engine: description: Crush CLI with headless mode and multi-provider LLM support runtime-id: crush provider: - name: crush + name: github auth: - role: api-key secret: COPILOT_GITHUB_TOKEN diff --git a/pkg/workflow/data/engines/opencode.md b/pkg/workflow/data/engines/opencode.md index d5a2aad0f9a..ae5a81d38ae 100644 --- a/pkg/workflow/data/engines/opencode.md +++ b/pkg/workflow/data/engines/opencode.md @@ -5,7 +5,7 @@ engine: description: OpenCode CLI with headless mode and multi-provider LLM support runtime-id: opencode provider: - name: opencode + name: github auth: - role: api-key secret: COPILOT_GITHUB_TOKEN diff --git a/pkg/workflow/data/engines/pi.md b/pkg/workflow/data/engines/pi.md index 2267243be0f..b6fa7940ac4 100644 --- a/pkg/workflow/data/engines/pi.md +++ b/pkg/workflow/data/engines/pi.md @@ -5,7 +5,7 @@ engine: description: Pi AI coding agent (experimental) runtime-id: pi provider: - name: pi + name: github auth: - role: api-key secret: COPILOT_GITHUB_TOKEN diff --git a/pkg/workflow/engine_auth_test.go b/pkg/workflow/engine_auth_test.go index 30770dabc7d..32d0a4c253c 100644 --- a/pkg/workflow/engine_auth_test.go +++ b/pkg/workflow/engine_auth_test.go @@ -405,6 +405,9 @@ func TestBuiltInEngineAuthUnchanged(t *testing.T) { {"codex", "CODEX_API_KEY"}, {"copilot", "COPILOT_GITHUB_TOKEN"}, {"gemini", "GEMINI_API_KEY"}, + {"opencode", "COPILOT_GITHUB_TOKEN"}, + {"crush", "COPILOT_GITHUB_TOKEN"}, + {"pi", "COPILOT_GITHUB_TOKEN"}, } for _, tt := range tests { diff --git a/pkg/workflow/engine_definition_test.go b/pkg/workflow/engine_definition_test.go index 72f2781b3b7..01876564302 100644 --- a/pkg/workflow/engine_definition_test.go +++ b/pkg/workflow/engine_definition_test.go @@ -25,8 +25,9 @@ func TestNewEngineCatalog_BuiltIns(t *testing.T) { {"codex", "Codex", "openai"}, {"copilot", "GitHub Copilot CLI", "github"}, {"gemini", "Google Gemini CLI", "google"}, - {"opencode", "OpenCode", "opencode"}, - {"crush", "Crush", "crush"}, + {"opencode", "OpenCode", "github"}, + {"crush", "Crush", "github"}, + {"pi", "Pi", "github"}, } for _, tt := range tests { diff --git a/pkg/workflow/pi_engine.go b/pkg/workflow/pi_engine.go index bfe7ede01ef..4168ecc54c4 100644 --- a/pkg/workflow/pi_engine.go +++ b/pkg/workflow/pi_engine.go @@ -48,8 +48,9 @@ func NewPiEngine() *PiEngine { } } -// GetModelEnvVarName returns the native environment variable name that the Pi CLI uses -// for model selection. Setting PI_MODEL is equivalent to passing --model to the CLI. +// GetModelEnvVarName returns the legacy Pi model env-var name exposed by gh-aw. +// gh-aw passes the model to the Pi CLI via --model and separately exports the +// original workflow model for extensions. func (e *PiEngine) GetModelEnvVarName() string { return constants.PiCLIModelEnvVar } @@ -275,7 +276,7 @@ func (e *PiEngine) GetExecutionSteps(workflowData *WorkflowData, logFile string) piArgs = append(piArgs, workflowData.EngineConfig.Args...) } - // Pi v0.72+ does not support a PI_MODEL env var; the model must be passed as + // Pi v0.72+ does not support a PI_MODEL env var for CLI model selection; the model must be passed as // the --model CLI flag. When the firewall is enabled we route LLM traffic // through the AWF gateway sidecar by generating a temporary models.json that // registers a custom "aw-gateway" provider pointing at the gateway port. When @@ -379,6 +380,9 @@ touch %s "GITHUB_WORKSPACE": "${{ github.workspace }}", "GITHUB_STEP_SUMMARY": AgentStepSummaryPath, } + if modelConfigured { + env["GH_AW_PI_MODEL"] = workflowData.EngineConfig.Model + } // Inject provider-specific credentials from the backend profile. maps.Copy(env, profile.env) diff --git a/pkg/workflow/pi_engine_test.go b/pkg/workflow/pi_engine_test.go index 3eb4caacccf..f5339bb1102 100644 --- a/pkg/workflow/pi_engine_test.go +++ b/pkg/workflow/pi_engine_test.go @@ -200,6 +200,7 @@ func TestPiEngine_GetExecutionSteps_WithModel(t *testing.T) { assert.Contains(t, stepText, "--model", "Step should pass --model flag to Pi CLI") assert.Contains(t, stepText, "github-copilot", "Non-firewall copilot model should use github-copilot/ provider prefix") assert.Contains(t, stepText, "claude-sonnet-4", "Step should include the model ID portion") + assert.Contains(t, stepText, "GH_AW_PI_MODEL", "Step should expose the original workflow model to Pi extensions") assert.NotContains(t, stepText, "PI_MODEL", "Step should not set the unsupported PI_MODEL env var") } From a5430a0592258d9abcf052c35fcdfbe7f517e973 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 17:46:06 +0000 Subject: [PATCH 3/6] Validate pi provider integration Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/pi_provider.test.cjs | 25 +++++-------------------- pkg/workflow/pi_engine_test.go | 2 +- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/actions/setup/js/pi_provider.test.cjs b/actions/setup/js/pi_provider.test.cjs index 72aa1f958f0..d2adeb24dbf 100644 --- a/actions/setup/js/pi_provider.test.cjs +++ b/actions/setup/js/pi_provider.test.cjs @@ -50,26 +50,11 @@ describe("pi_provider.cjs", () => { expect(count).toBe(5); expect(calls).toEqual([ - [ - "github-copilot", - { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }, - ], - [ - "copilot", - { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }, - ], - [ - "anthropic", - { apiKey: "anthropic-token", api: "anthropic", baseUrl: "https://anthropic.example.test" }, - ], - [ - "openai", - { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }, - ], - [ - "codex", - { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }, - ], + ["github-copilot", { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }], + ["copilot", { apiKey: "copilot-token", api: "openai-completions", baseUrl: "https://copilot.example.test" }], + ["anthropic", { apiKey: "anthropic-token", api: "anthropic", baseUrl: "https://anthropic.example.test" }], + ["openai", { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }], + ["codex", { apiKey: "codex-token", api: "openai-completions", baseUrl: "https://openai.example.test" }], ]); }); diff --git a/pkg/workflow/pi_engine_test.go b/pkg/workflow/pi_engine_test.go index f5339bb1102..abd7a9554f3 100644 --- a/pkg/workflow/pi_engine_test.go +++ b/pkg/workflow/pi_engine_test.go @@ -201,7 +201,7 @@ func TestPiEngine_GetExecutionSteps_WithModel(t *testing.T) { assert.Contains(t, stepText, "github-copilot", "Non-firewall copilot model should use github-copilot/ provider prefix") assert.Contains(t, stepText, "claude-sonnet-4", "Step should include the model ID portion") assert.Contains(t, stepText, "GH_AW_PI_MODEL", "Step should expose the original workflow model to Pi extensions") - assert.NotContains(t, stepText, "PI_MODEL", "Step should not set the unsupported PI_MODEL env var") + assert.NotContains(t, stepText, "\n PI_MODEL:", "Step should not set the unsupported PI_MODEL env var") } func TestPiEngine_GetExecutionSteps_ProviderPrefixCopilot(t *testing.T) { From 17fc540fb7dd272c900c50ae1b16c127bc75b336 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 17:50:00 +0000 Subject: [PATCH 4/6] Polish pi provider test comments Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/pi_provider.cjs | 3 +++ pkg/workflow/pi_engine_test.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/pi_provider.cjs b/actions/setup/js/pi_provider.cjs index fd110074086..4238479a69d 100644 --- a/actions/setup/js/pi_provider.cjs +++ b/actions/setup/js/pi_provider.cjs @@ -35,6 +35,9 @@ const DEFAULT_LOGGER = /** @type {(msg: string) => void} */ (msg => process.stde /** * Return the workflow-configured model string exposed to Pi extensions. + * GH_AW_PI_MODEL takes precedence because gh-aw sets it explicitly for extensions + * while continuing to pass the CLI model via --model. PI_MODEL remains a legacy + * fallback for older callers. * * @returns {string} */ diff --git a/pkg/workflow/pi_engine_test.go b/pkg/workflow/pi_engine_test.go index abd7a9554f3..892d122d790 100644 --- a/pkg/workflow/pi_engine_test.go +++ b/pkg/workflow/pi_engine_test.go @@ -201,7 +201,7 @@ func TestPiEngine_GetExecutionSteps_WithModel(t *testing.T) { assert.Contains(t, stepText, "github-copilot", "Non-firewall copilot model should use github-copilot/ provider prefix") assert.Contains(t, stepText, "claude-sonnet-4", "Step should include the model ID portion") assert.Contains(t, stepText, "GH_AW_PI_MODEL", "Step should expose the original workflow model to Pi extensions") - assert.NotContains(t, stepText, "\n PI_MODEL:", "Step should not set the unsupported PI_MODEL env var") + assert.NotContains(t, stepText, "\n PI_MODEL:", "Step should not set PI_MODEL in the environment when the CLI model is passed via --model") } func TestPiEngine_GetExecutionSteps_ProviderPrefixCopilot(t *testing.T) { From 5612923e0672fee921aa744741719b137aa16e18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:25:43 +0000 Subject: [PATCH 5/6] chore: start addressing PR feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dev.lock.yml | 2 ++ .github/workflows/smoke-pi.lock.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index e0a70adb3d0..7c3dd408e27 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -771,6 +771,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_PHASE: agent + GH_AW_PI_MODEL: copilot/claude-sonnet-4-20250514 GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_VERSION: dev @@ -1255,6 +1256,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_PHASE: detection + GH_AW_PI_MODEL: copilot/claude-sonnet-4-20250514 GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_VERSION: dev GITHUB_AW: true diff --git a/.github/workflows/smoke-pi.lock.yml b/.github/workflows/smoke-pi.lock.yml index 86963d04dc2..bf97479d14b 100644 --- a/.github/workflows/smoke-pi.lock.yml +++ b/.github/workflows/smoke-pi.lock.yml @@ -841,6 +841,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_PHASE: agent + GH_AW_PI_MODEL: copilot/claude-sonnet-4-20250514 GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_VERSION: dev @@ -1342,6 +1343,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_PHASE: detection + GH_AW_PI_MODEL: copilot/claude-sonnet-4-20250514 GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_VERSION: dev GITHUB_AW: true From d816c4867c86e02f4e3ae01865233ad16c67b55a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:39:05 +0000 Subject: [PATCH 6/6] Update dev workflow to Pi with GitHub provider Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dev.lock.yml | 30 +++++++++++++++--------------- .github/workflows/dev.md | 7 +++++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 7c3dd408e27..6d0e03a39c3 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"5b756a1dc53a828105ac79a0d3777e19eb27d8f303b70b3ee0d27a600517e396","agent_id":"pi","agent_model":"copilot/claude-sonnet-4-20250514"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"93f606ec4137ce1d4e464dfc3e2a005b05697bcf07b216a065d1ae25e8c43455","agent_id":"pi","agent_model":"claude-sonnet-4-20250514"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.43"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -147,7 +147,7 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "pi" GH_AW_INFO_ENGINE_NAME: "Pi" - GH_AW_INFO_MODEL: "copilot/claude-sonnet-4-20250514" + GH_AW_INFO_MODEL: "claude-sonnet-4-20250514" GH_AW_INFO_VERSION: "0.72.1" GH_AW_INFO_AGENT_VERSION: "0.72.1" GH_AW_INFO_WORKFLOW_NAME: "Dev" @@ -258,20 +258,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_b88579a1f7017635_EOF' + cat << 'GH_AW_PROMPT_477b040d2fd91ec8_EOF' - GH_AW_PROMPT_b88579a1f7017635_EOF + GH_AW_PROMPT_477b040d2fd91ec8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_b88579a1f7017635_EOF' + cat << 'GH_AW_PROMPT_477b040d2fd91ec8_EOF' Tools: create_issue, missing_tool, missing_data, noop - GH_AW_PROMPT_b88579a1f7017635_EOF + GH_AW_PROMPT_477b040d2fd91ec8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_b88579a1f7017635_EOF' + cat << 'GH_AW_PROMPT_477b040d2fd91ec8_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -300,14 +300,14 @@ jobs: {{/if}} - GH_AW_PROMPT_b88579a1f7017635_EOF + GH_AW_PROMPT_477b040d2fd91ec8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_b88579a1f7017635_EOF' + cat << 'GH_AW_PROMPT_477b040d2fd91ec8_EOF' {{#runtime-import .github/workflows/shared/observability-otlp.md}} {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/dev.md}} - GH_AW_PROMPT_b88579a1f7017635_EOF + GH_AW_PROMPT_477b040d2fd91ec8_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -518,9 +518,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b0544374a465e7c6_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_bd7f9d4e4dd0c216_EOF' {"create_issue":{"expires":168,"max":1,"title_prefix":"[Daily Report] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_b0544374a465e7c6_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_bd7f9d4e4dd0c216_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -771,7 +771,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_PHASE: agent - GH_AW_PI_MODEL: copilot/claude-sonnet-4-20250514 + GH_AW_PI_MODEL: claude-sonnet-4-20250514 GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_VERSION: dev @@ -1256,7 +1256,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_PHASE: detection - GH_AW_PI_MODEL: copilot/claude-sonnet-4-20250514 + GH_AW_PI_MODEL: claude-sonnet-4-20250514 GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_VERSION: dev GITHUB_AW: true @@ -1369,7 +1369,7 @@ jobs: GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "pi" - GH_AW_ENGINE_MODEL: "copilot/claude-sonnet-4-20250514" + GH_AW_ENGINE_MODEL: "claude-sonnet-4-20250514" GH_AW_WORKFLOW_ID: "dev" GH_AW_WORKFLOW_NAME: "Dev" outputs: diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md index 95b50c33127..8f29d707bdc 100644 --- a/.github/workflows/dev.md +++ b/.github/workflows/dev.md @@ -9,8 +9,11 @@ description: Daily status report for gh-aw project timeout-minutes: 30 strict: false engine: - id: pi - model: copilot/claude-sonnet-4-20250514 + runtime: + id: pi + provider: + id: github + model: claude-sonnet-4-20250514 permissions: contents: read