From d3ae82884fc41586ab44c1c9b37aa061d9d4e9f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:21:57 +0000 Subject: [PATCH 01/10] Initial plan From a13fb09ce20198b4c7254beb2a301d6e2853f568 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:29:21 +0000 Subject: [PATCH 02/10] plan: wire BYOK extra headers/body fields/sessionId in frontmatter Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/skills/agentic-workflows/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md index 615a51e551e..3fc711d4035 100644 --- a/.github/skills/agentic-workflows/SKILL.md +++ b/.github/skills/agentic-workflows/SKILL.md @@ -49,6 +49,7 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/optimize-agentic-workflow.md` - `.github/aw/patterns.md` - `.github/aw/pr-reviewer.md` +- `.github/aw/release-workflow.md` - `.github/aw/report.md` - `.github/aw/reuse.md` - `.github/aw/safe-outputs-automation.md` From 78efcc1f2de493a3564237b817b0717e2b3a8698 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:38:56 +0000 Subject: [PATCH 03/10] feat: expose BYOK extraHeaders/extraBodyFields/sessionId in frontmatter for copilot engine Wire sandbox.agent.targets.copilot.{extraHeaders,extraBodyFields,sessionId} frontmatter fields through to the AWF config JSON at apiProxy.targets.copilot. These map to AWF_BYOK_EXTRA_HEADERS, AWF_BYOK_EXTRA_BODY_FIELDS, and AWF_PROVIDER_SESSION_ID in the sidecar, enabling workflow authors to attach custom headers and body fields to Copilot BYOK upstream requests without hand-editing generated AWF JSON. - Add ExtraHeaders, ExtraBodyFields, SessionId to AgentAPIProxyTargetConfig (sandbox.go) - Add ExtraHeaders, ExtraBodyFields, SessionId to AWFAPITargetConfig (awf_config.go) - Add extractCopilotTargetConfig helper (engine_api_targets.go) - Wire BYOK fields in BuildAWFConfigJSON for copilot target (awf_config.go) - Add 6 unit tests + 1 schema compliance test (awf_config_test.go) - Update awf-config-sources-spec.md drift table with 3 new rows - Update file org test to forbid extractCopilotTargetConfig from awf_helpers.go - Update README.md exported type docs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/README.md | 4 +- pkg/workflow/awf_config.go | 39 ++++ pkg/workflow/awf_config_test.go | 201 ++++++++++++++++++ pkg/workflow/engine_api_targets.go | 13 ++ ...gine_api_targets_file_organization_test.go | 1 + pkg/workflow/sandbox.go | 25 +++ specs/awf-config-sources-spec.md | 3 + 7 files changed, 284 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/README.md b/pkg/workflow/README.md index 636dff2a857..7fdc2fd52a0 100644 --- a/pkg/workflow/README.md +++ b/pkg/workflow/README.md @@ -761,7 +761,7 @@ This appendix is generated from the current non-test Go source files in this pac | `artifacts.go` | `ArtifactDownloadConfig` | `type ArtifactDownloadConfig struct { ArtifactName string // Name of the artifact to download (e.g., "agent-output", "prompt") ArtifactFilename string // Filename inside the artifact directory (e.g., "agent_output.json", "prompt.txt") DownloadPath string // Path where artifact will be downloaded (e.g., "/tmp/gh-aw/safeoutputs/") SetupEnvStep bool // Whether to add environment variable setup step EnvVarName string // Environment variable name to set (e.g., "GH_AW_AGENT_OUTPUT") StepName string // Optional custom step name (defaults to "Download {artifact} artifact") IfCondition string // Optional conditional expression for the step (e.g., "needs.agent.outputs.has_patch == 'true'") StepID string // Optional step ID; when set, the env-setup step is gated on this step's success }` | ArtifactDownloadConfig holds configuration for building artifact download steps | | `auto_update_workflow.go` | `GenerateAutoUpdateWorkflowOptions` | `type GenerateAutoUpdateWorkflowOptions struct { // Context is used for action reference resolution in non-dev modes. // When nil, context.Background() is used. Context context.Context // WorkflowDir is the directory where the workflow file will be written. WorkflowDir string // Enabled indicates whether auto-updates are enabled in the repo config. Enabled bool // RepoSlug is the owner/repo slug used to deterministically scatter the // weekly cron schedule across different repositories. Pass an empty string // when the slug is not available; scattering will still succeed using only // the workflow identifier as seed. RepoSlug string // SetupActionRef is the resolved reference for the gh-aw actions/setup action. // For example: "./actions/setup" (dev mode) or "github/gh-aw/actions/setup@" (release mode). // When empty, "./actions/setup" is used as a fallback. SetupActionRef string // GitHubScriptPin is the pinned reference for actions/github-script. // When empty, getActionPin("actions/github-script") is used as a fallback. GitHubScriptPin string // ActionMode controls how CLI install steps and command prefixes are generated. // Defaults to ActionModeDev when empty. ActionMode ActionMode // Version is the gh-aw version used by generateInstallCLISteps in non-dev modes. Version string // ActionTag optionally overrides the setup-cli version tag in non-dev modes. ActionTag string // Resolver optionally resolves setup-cli action tags to SHA-pinned refs. Resolver SHAResolver }` | GenerateAutoUpdateWorkflowOptions configures an auto-update workflow generation run. | | `awf_config.go` | `AWFAPIProxyConfig` | `type AWFAPIProxyConfig struct { // Enabled enables the API proxy sidecar for LLM gateway credential isolation. // Maps to: --enable-api-proxy Enabled bool `json:"enabled"` // EnableTokenSteering enables budget-warning system message injection near ET budget exhaustion. EnableTokenSteering bool `json:"enableTokenSteering,omitempty"` // MaxRuns is the maximum number of LLM invocations allowed for a run. MaxRuns int `json:"maxRuns,omitempty"` // MaxTurnCacheMisses is the maximum number of consecutive cache misses allowed for a run. MaxTurnCacheMisses int `json:"maxCacheMisses,omitempty"` // MaxAICredits is the explicit per-run AI credits budget enforced by the API proxy. MaxAICredits int64 `json:"maxAiCredits,omitempty"` // ModelFallback configures the model fallback policy for unresolved model selections. // When nil, the AWF default (enabled=true, strategy=middle_power) is used. // Set enabled=false to prevent AWF from silently rewriting deployment names, which // is needed for BYOK Azure OpenAI deployments where rewriting causes HTTP 404. ModelFallback *AWFModelFallbackConfig `json:"modelFallback,omitempty"` // ModelMultipliers configures per-model ET accounting multipliers in AWF. ModelMultipliers map[string]float64 `json:"modelMultipliers,omitempty"` // Targets holds per-provider API target overrides. // Supported keys: "openai", "anthropic", "copilot", "gemini" // The "gemini" target is also used for Antigravity engine routing. Targets map[string]*AWFAPITargetConfig `json:"targets,omitempty"` // Models contains model alias and fallback policy definitions. // Keys are alias names (empty string "" = default policy); values are ordered // lists of vendor/modelid patterns or other alias names to try in sequence. // AWF resolves aliases recursively; loops are not permitted. // Per the AWF config schema, this lives under apiProxy.models. Models map[string][]string `json:"models,omitempty"` // AllowedModels is the explicit allowlist policy for model names/patterns. AllowedModels []string `json:"allowedModels,omitempty"` // DisallowedModels is the explicit denylist policy for model names/patterns. DisallowedModels []string `json:"disallowedModels,omitempty"` }` | AWFAPIProxyConfig is the "apiProxy" section of the AWF config file. | -| `awf_config.go` | `AWFAPITargetConfig` | `type AWFAPITargetConfig struct { // Host is the hostname (and optional port) of the API endpoint. Host string `json:"host,omitempty"` // AuthHeader is the custom authentication header name sent with API requests. // When set, the raw API key is sent as ": " instead of the // provider default (e.g. "Authorization: ******" for OpenAI, or // "x-api-key: " for Anthropic). This supports gateways like Azure OpenAI // that require "api-key: " in place of the standard provider scheme. // Maps to: --openai-api-auth-header / --anthropic-api-auth-header AuthHeader string `json:"authHeader,omitempty"` }` | AWFAPITargetConfig is a single API proxy target entry. | +| `awf_config.go` | `AWFAPITargetConfig` | `type AWFAPITargetConfig struct { // Host is the hostname (and optional port) of the API endpoint. Host string `json:"host,omitempty"` // AuthHeader is the custom authentication header name sent with API requests. // When set, the raw API key is sent as ": " instead of the // provider default (e.g. "Authorization: ******" for OpenAI, or // "x-api-key: " for Anthropic). This supports gateways like Azure OpenAI // that require "api-key: " in place of the standard provider scheme. // Maps to: --openai-api-auth-header / --anthropic-api-auth-header AuthHeader string `json:"authHeader,omitempty"` // ExtraHeaders holds additional non-sensitive headers injected on Copilot BYOK upstream // requests. Only valid for the "copilot" provider target. // Maps to AWF_BYOK_EXTRA_HEADERS in the sidecar. ExtraHeaders map[string]string `json:"extraHeaders,omitempty"` // ExtraBodyFields holds additional non-sensitive JSON body fields injected on Copilot BYOK // upstream requests. Only valid for the "copilot" provider target. // Maps to AWF_BYOK_EXTRA_BODY_FIELDS in the sidecar. ExtraBodyFields map[string]string `json:"extraBodyFields,omitempty"` // SessionId is an opt-in session identifier injected as the x-session-id request header // and session_id body field on Copilot BYOK upstream requests. Only valid for the // "copilot" provider target. // Maps to AWF_PROVIDER_SESSION_ID in the sidecar. SessionId string `json:"sessionId,omitempty"` }` | AWFAPITargetConfig is a single API proxy target entry. | | `awf_config.go` | `AWFChrootConfig` | `type AWFChrootConfig struct { // BinariesSourcePath is the runner-side directory to overlay at /usr/local/bin // inside chroot mode for split-filesystem ARC/DinD runners. BinariesSourcePath string `json:"binariesSourcePath,omitempty"` // Identity configures identity values applied after chroot pivot to override // HOME/USER/LOGNAME defaults inside chroot mode. Identity *AWFChrootIdentityConfig `json:"identity,omitempty"` }` | AWFChrootConfig is the "chroot" section of the AWF config file. | | `awf_config.go` | `AWFChrootIdentityConfig` | `type AWFChrootIdentityConfig struct { // User is the USER/LOGNAME string to export inside chroot mode. User string `json:"user,omitempty"` // UID is the UID hint used for chroot identity synthesis and user switching. // Must be >= 1 (root is not supported). UID int `json:"uid,omitempty"` // GID is the GID hint used for chroot identity synthesis and user switching. // Must be >= 1. GID int `json:"gid,omitempty"` // Home is the home directory path to export inside chroot mode. Home string `json:"home,omitempty"` }` | AWFChrootIdentityConfig is the "chroot. | | `awf_config.go` | `AWFConfigFile` | `type AWFConfigFile struct { // Schema is the JSON schema reference for IDE auto-complete support. Schema string `json:"$schema,omitempty"` // Runner contains runner topology metadata that AWF uses to activate // topology-specific behaviors (split-filesystem handling, network isolation, // tool cache redirection, sysroot image selection). Runner *AWFRunnerConfig `json:"runner,omitempty"` // Network contains network egress control configuration. Network *AWFNetworkConfig `json:"network,omitempty"` // Platform contains GitHub deployment metadata used by AWF auth handling. Platform *AWFPlatformConfig `json:"platform,omitempty"` // APIProxy contains API proxy (LLM gateway) configuration. APIProxy *AWFAPIProxyConfig `json:"apiProxy,omitempty"` // Container contains container execution configuration. Container *AWFContainerConfig `json:"container,omitempty"` // Logging contains logging and diagnostics configuration. Logging *AWFLoggingConfig `json:"logging,omitempty"` // Chroot contains chroot execution overrides for split-filesystem ARC/DinD runners. // This field is not populated at compile time; it is injected at runtime when DinD topology is detected. Chroot *AWFChrootConfig `json:"chroot,omitempty"` }` | AWFConfigFile represents the AWF configuration file schema. | @@ -864,7 +864,7 @@ This appendix is generated from the current non-test Go source files in this pac | `safe_update_manifest.go` | `GHAWManifestAction` | `type GHAWManifestAction struct { Repo string `json:"repo"` SHA string `json:"sha"` Version string `json:"version,omitempty"` }` | GHAWManifestAction represents a single GitHub Action referenced in a compiled workflow. | | `safe_update_manifest.go` | `GHAWManifestResolutionFailure` | `type GHAWManifestResolutionFailure struct { Repo string `json:"repo"` Ref string `json:"ref"` ErrorType string `json:"error_type"` }` | GHAWManifestResolutionFailure represents an action-ref pinning failure captured during compilation. | | `samples_replay.go` | `SampleEntry` | `type SampleEntry struct { // Tool is the snake_case MCP tool name (e.g. "create_pull_request"). Tool string `json:"tool"` // Arguments are passed verbatim as the MCP `tools/call` arguments. // Sample sidecar fields (e.g. `patch`) have already been stripped. Arguments map[string]any `json:"arguments"` // Sidecars carries fields stripped from Arguments that need out-of-band // pre-staging by the driver (e.g. `patch` for create_pull_request). Sidecars map[string]any `json:"sidecars,omitempty"` }` | SampleEntry is the per-call payload consumed by apply_samples. | -| `sandbox.go` | `AgentAPIProxyTargetConfig` | `type AgentAPIProxyTargetConfig struct { // AuthHeader is the custom authentication header name sent with API requests. // When set, the raw API key is sent as ": " instead of the // provider default ("Authorization" for OpenAI, "x-api-key" for Anthropic). // Example: "api-key" for Azure OpenAI gateways. AuthHeader string `yaml:"authHeader,omitempty"` }` | AgentAPIProxyTargetConfig configures a single LLM provider's API proxy target. | +| `sandbox.go` | `AgentAPIProxyTargetConfig` | `type AgentAPIProxyTargetConfig struct { // AuthHeader is the custom authentication header name sent with API requests. // When set, the raw API key is sent as ": " instead of the // provider default ("Authorization" for OpenAI, "x-api-key" for Anthropic). // Example: "api-key" for Azure OpenAI gateways. AuthHeader string `yaml:"authHeader,omitempty"` // ExtraHeaders holds additional non-sensitive headers to include on Copilot BYOK // upstream requests. Applies only to the "copilot" provider target. // Maps to apiProxy.targets.copilot.extraHeaders in the AWF config (AWF_BYOK_EXTRA_HEADERS). ExtraHeaders map[string]string `yaml:"extraHeaders,omitempty"` // ExtraBodyFields holds additional non-sensitive JSON body fields to include on Copilot // BYOK upstream requests. Applies only to the "copilot" provider target. // Maps to apiProxy.targets.copilot.extraBodyFields in the AWF config (AWF_BYOK_EXTRA_BODY_FIELDS). ExtraBodyFields map[string]string `yaml:"extraBodyFields,omitempty"` // SessionId is an opt-in session identifier injected as the x-session-id request header // and session_id body field on Copilot BYOK upstream requests. Applies only to the // "copilot" provider target. // Maps to apiProxy.targets.copilot.sessionId in the AWF config (AWF_PROVIDER_SESSION_ID). SessionId string `yaml:"sessionId,omitempty"` }` | AgentAPIProxyTargetConfig configures a single LLM provider's API proxy target. | | `sandbox.go` | `AgentRuntime` | `type AgentRuntime string` | AgentRuntime represents the container runtime to use for the agent container. | | `script_registry.go` | `ScriptRegistry` | `type ScriptRegistry struct { mu sync.RWMutex scripts map[string]*scriptEntry }` | ScriptRegistry manages script metadata and custom action paths. | | `secret_extraction.go` | `SecretExpression` | `type SecretExpression struct { VarName string // The secret variable name (e.g., "DD_API_KEY") FullExpr string // The full expression (e.g., "${{ secrets.DD_API_KEY }}") }` | SecretExpression represents a parsed secret expression | diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 7328df86bd3..c10fda1e2c4 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -294,6 +294,22 @@ type AWFAPITargetConfig struct { // that require "api-key: " in place of the standard provider scheme. // Maps to: --openai-api-auth-header / --anthropic-api-auth-header AuthHeader string `json:"authHeader,omitempty"` + + // ExtraHeaders holds additional non-sensitive headers injected on Copilot BYOK upstream + // requests. Only valid for the "copilot" provider target (copilotTarget in the AWF schema). + // Maps to AWF_BYOK_EXTRA_HEADERS in the sidecar. + ExtraHeaders map[string]string `json:"extraHeaders,omitempty"` + + // ExtraBodyFields holds additional non-sensitive JSON body fields injected on Copilot BYOK + // upstream requests. Only valid for the "copilot" provider target. + // Maps to AWF_BYOK_EXTRA_BODY_FIELDS in the sidecar. + ExtraBodyFields map[string]string `json:"extraBodyFields,omitempty"` + + // SessionId is an opt-in session identifier injected as the x-session-id request header + // and session_id body field on Copilot BYOK upstream requests. Only valid for the + // "copilot" provider target. Must be set explicitly; never auto-derived from GITHUB_RUN_ID. + // Maps to AWF_PROVIDER_SESSION_ID in the sidecar. + SessionId string `json:"sessionId,omitempty"` } // AWFContainerConfig is the "container" section of the AWF config file. @@ -544,6 +560,29 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { targets["copilot"] = &AWFAPITargetConfig{Host: copilotTarget} awfConfigLog.Printf("API proxy: custom copilot target=%s", copilotTarget) } + + // Apply BYOK supplemental fields from sandbox.agent.targets.copilot frontmatter. + // extraHeaders, extraBodyFields, and sessionId are Copilot-specific and map to + // AWF_BYOK_EXTRA_HEADERS, AWF_BYOK_EXTRA_BODY_FIELDS, and AWF_PROVIDER_SESSION_ID. + if copilotFrontmatter := extractCopilotTargetConfig(config.WorkflowData); copilotFrontmatter != nil { + existing, ok := targets["copilot"] + if !ok { + existing = &AWFAPITargetConfig{} + targets["copilot"] = existing + } + if len(copilotFrontmatter.ExtraHeaders) > 0 { + existing.ExtraHeaders = copilotFrontmatter.ExtraHeaders + awfConfigLog.Printf("API proxy: copilot extraHeaders configured (%d header(s))", len(copilotFrontmatter.ExtraHeaders)) + } + if len(copilotFrontmatter.ExtraBodyFields) > 0 { + existing.ExtraBodyFields = copilotFrontmatter.ExtraBodyFields + awfConfigLog.Printf("API proxy: copilot extraBodyFields configured (%d field(s))", len(copilotFrontmatter.ExtraBodyFields)) + } + if copilotFrontmatter.SessionId != "" { + existing.SessionId = copilotFrontmatter.SessionId + awfConfigLog.Printf("API proxy: copilot sessionId configured") + } + } if antigravityTarget := GetAntigravityAPITarget(config.WorkflowData, config.EngineName); antigravityTarget != "" { // Route the Antigravity-resolved API target through the "gemini" provider key // to match AWF's supported target providers. diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index 7b9a8e23d33..fda36bdfad8 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -787,6 +787,178 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.NotContains(t, jsonStr, `"authHeader"`, "authHeader should be absent when not configured") }) + t.Run("copilot extraHeaders from frontmatter sandbox.agent.targets.copilot are included", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "copilot": { + ExtraHeaders: map[string]string{ + "x-openrouter-title": "my-workflow", + "http-referer": "https://github.com/org/repo", + }, + }, + }, + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"copilot"`, "should include copilot target") + assert.Contains(t, jsonStr, `"extraHeaders"`, "should include extraHeaders in copilot target") + assert.Contains(t, jsonStr, `"x-openrouter-title"`, "should include x-openrouter-title header key") + assert.Contains(t, jsonStr, `"my-workflow"`, "should include header value") + }) + + t.Run("copilot extraBodyFields from frontmatter sandbox.agent.targets.copilot are included", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "copilot": { + ExtraBodyFields: map[string]string{ + "custom-field": "custom-value", + }, + }, + }, + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"extraBodyFields"`, "should include extraBodyFields in copilot target") + assert.Contains(t, jsonStr, `"custom-field"`, "should include body field key") + assert.Contains(t, jsonStr, `"custom-value"`, "should include body field value") + }) + + t.Run("copilot sessionId from frontmatter sandbox.agent.targets.copilot is included", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "copilot": { + SessionId: "${{ github.run_id }}", + }, + }, + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"sessionId"`, "should include sessionId in copilot target") + assert.Contains(t, jsonStr, `"${{ github.run_id }}"`, "should include sessionId value") + }) + + t.Run("copilot BYOK fields coexist with host from GetCopilotAPITarget", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + APITarget: "copilot-gateway.internal", + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "copilot": { + ExtraHeaders: map[string]string{ + "x-title": "my-workflow", + }, + SessionId: "run-123", + }, + }, + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, "copilot-gateway.internal", "should include host from api-target") + assert.Contains(t, jsonStr, `"extraHeaders"`, "should include extraHeaders alongside host") + assert.Contains(t, jsonStr, `"sessionId"`, "should include sessionId alongside host") + }) + + t.Run("copilot BYOK fields create entry even without host override", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "copilot": { + ExtraHeaders: map[string]string{ + "x-title": "my-workflow", + }, + }, + }, + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"copilot"`, "should include copilot target entry even without host") + assert.Contains(t, jsonStr, `"extraHeaders"`, "should include extraHeaders") + assert.NotContains(t, jsonStr, `"host":""`, "should not emit empty host") + }) + + t.Run("copilot BYOK fields are absent when not configured", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.NotContains(t, jsonStr, `"extraHeaders"`, "extraHeaders should be absent when not configured") + assert.NotContains(t, jsonStr, `"extraBodyFields"`, "extraBodyFields should be absent when not configured") + assert.NotContains(t, jsonStr, `"sessionId"`, "sessionId should be absent when not configured") + }) + t.Run("sandbox agent platform is emitted in awf platform config", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "copilot", @@ -1222,6 +1394,35 @@ func TestBuildAWFConfigJSON_SchemaCompliance(t *testing.T) { }(), }, }, + { + name: "config with copilot BYOK extra headers and sessionId", + config: AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "copilot": { + ExtraHeaders: map[string]string{ + "x-openrouter-title": "my-workflow", + "http-referer": "https://github.com/org/repo", + }, + ExtraBodyFields: map[string]string{ + "custom-field": "custom-value", + }, + SessionId: "run-12345", + }, + }, + }, + }, + }, + }, + }, } for _, tc := range cases { diff --git a/pkg/workflow/engine_api_targets.go b/pkg/workflow/engine_api_targets.go index 149f7ecb98f..f9f09fba6a6 100644 --- a/pkg/workflow/engine_api_targets.go +++ b/pkg/workflow/engine_api_targets.go @@ -133,6 +133,19 @@ func extractAPITargetAuthHeader(workflowData *WorkflowData, provider string) str return target.AuthHeader } +// extractCopilotTargetConfig returns the AgentAPIProxyTargetConfig for the "copilot" provider +// from the sandbox.agent.targets frontmatter section. Returns nil if not configured. +func extractCopilotTargetConfig(workflowData *WorkflowData) *AgentAPIProxyTargetConfig { + if workflowData == nil || workflowData.SandboxConfig == nil || workflowData.SandboxConfig.Agent == nil { + return nil + } + targets := workflowData.SandboxConfig.Agent.Targets + if targets == nil { + return nil + } + return targets["copilot"] +} + // GetCopilotAPITarget returns the effective Copilot API target hostname, checking in order: // 1. engine.api-target (explicit, takes precedence) // 2. GITHUB_COPILOT_BASE_URL in engine.env (implicit, derived from the configured Copilot base URL) diff --git a/pkg/workflow/engine_api_targets_file_organization_test.go b/pkg/workflow/engine_api_targets_file_organization_test.go index b66daf79bc8..ad09ff840e9 100644 --- a/pkg/workflow/engine_api_targets_file_organization_test.go +++ b/pkg/workflow/engine_api_targets_file_organization_test.go @@ -18,6 +18,7 @@ func TestAWFHelpersDoesNotContainEngineAPITargetHelpers(t *testing.T) { "func extractAPITargetHost(", "func extractAPIBasePath(", "func extractAPITargetAuthHeader(", + "func extractCopilotTargetConfig(", "func GetCopilotAPITarget(", "func extractLiteralEngineEnvHost(", "func GetCopilotAllowlistTargets(", diff --git a/pkg/workflow/sandbox.go b/pkg/workflow/sandbox.go index e49de49e4e9..b1c282f07b2 100644 --- a/pkg/workflow/sandbox.go +++ b/pkg/workflow/sandbox.go @@ -103,6 +103,31 @@ type AgentAPIProxyTargetConfig struct { // provider default ("Authorization" for OpenAI, "x-api-key" for Anthropic). // Example: "api-key" for Azure OpenAI gateways. AuthHeader string `yaml:"authHeader,omitempty"` + + // ExtraHeaders holds additional non-sensitive headers to include on Copilot BYOK + // upstream requests. Applies only to the "copilot" provider target. + // Maps to apiProxy.targets.copilot.extraHeaders in the AWF config (AWF_BYOK_EXTRA_HEADERS). + // Example: + // sandbox: + // agent: + // targets: + // copilot: + // extraHeaders: + // x-openrouter-title: my-workflow + // http-referer: https://github.com/org/repo + ExtraHeaders map[string]string `yaml:"extraHeaders,omitempty"` + + // ExtraBodyFields holds additional non-sensitive JSON body fields to include on Copilot + // BYOK upstream requests. Applies only to the "copilot" provider target. + // Maps to apiProxy.targets.copilot.extraBodyFields in the AWF config (AWF_BYOK_EXTRA_BODY_FIELDS). + ExtraBodyFields map[string]string `yaml:"extraBodyFields,omitempty"` + + // SessionId is an opt-in session identifier injected as the x-session-id request header + // and session_id body field on Copilot BYOK upstream requests. Applies only to the + // "copilot" provider target. Strict OpenAI-compatible servers (e.g. Azure OpenAI) reject + // the unknown body field with HTTP 400, so this value must be set explicitly. + // Maps to apiProxy.targets.copilot.sessionId in the AWF config (AWF_PROVIDER_SESSION_ID). + SessionId string `yaml:"sessionId,omitempty"` } // SandboxRuntimeConfig represents the Anthropic Sandbox Runtime configuration diff --git a/specs/awf-config-sources-spec.md b/specs/awf-config-sources-spec.md index ba2f79f4a82..6690e4e256e 100644 --- a/specs/awf-config-sources-spec.md +++ b/specs/awf-config-sources-spec.md @@ -63,6 +63,9 @@ The following fields previously existed in schema but were missed in spec CLI ma | `apiProxy.auth.*` | config-only (maps to `AWF_AUTH_*` env vars) | | `apiProxy.targets.openai.authHeader` | `--openai-api-auth-header` (frontmatter: `sandbox.agent.targets.openai.authHeader`) | | `apiProxy.targets.anthropic.authHeader` | `--anthropic-api-auth-header` (frontmatter: `sandbox.agent.targets.anthropic.authHeader`) | +| `apiProxy.targets.copilot.extraHeaders` | config-only (frontmatter: `sandbox.agent.targets.copilot.extraHeaders`; maps to `AWF_BYOK_EXTRA_HEADERS`) | +| `apiProxy.targets.copilot.extraBodyFields` | config-only (frontmatter: `sandbox.agent.targets.copilot.extraBodyFields`; maps to `AWF_BYOK_EXTRA_BODY_FIELDS`) | +| `apiProxy.targets.copilot.sessionId` | config-only (frontmatter: `sandbox.agent.targets.copilot.sessionId`; maps to `AWF_PROVIDER_SESSION_ID`) | | `container.dockerHostPathPrefix` | `--docker-host-path-prefix` | Agents SHOULD treat this class of mismatch as a regression signal and open a corrective PR when detected. From 4a89a535e07eb8c81161888199e9e39b9accf84e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:58:49 +0000 Subject: [PATCH 04/10] docs(adr): add draft ADR-48096 for BYOK extraHeaders/extraBodyFields/sessionId frontmatter exposure ADR generated by Design Decision Gate for PR #48096. --- ...yok-extra-fields-in-copilot-frontmatter.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/adr/48096-expose-byok-extra-fields-in-copilot-frontmatter.md diff --git a/docs/adr/48096-expose-byok-extra-fields-in-copilot-frontmatter.md b/docs/adr/48096-expose-byok-extra-fields-in-copilot-frontmatter.md new file mode 100644 index 00000000000..ce058500380 --- /dev/null +++ b/docs/adr/48096-expose-byok-extra-fields-in-copilot-frontmatter.md @@ -0,0 +1,43 @@ +# ADR-48096: Expose BYOK extraHeaders, extraBodyFields, and sessionId in Copilot Frontmatter + +**Date**: 2026-07-26 +**Status**: Draft +**Deciders**: pelikhan, copilot-swe-agent + +--- + +### Context + +The AWF proxy sidecar already accepts three Copilot BYOK-specific injection parameters — `AWF_BYOK_EXTRA_HEADERS`, `AWF_BYOK_EXTRA_BODY_FIELDS`, and `AWF_PROVIDER_SESSION_ID` — that let operators append custom HTTP headers, JSON body fields, and a session identifier to upstream Copilot BYOK requests. gh-aw provided no frontmatter surface for these parameters, so workflow authors who needed them (e.g., to route through OpenRouter with required `x-openrouter-title` or `http-referer` headers) had to hand-edit the generated AWF JSON config. This hand-edited config was silently overwritten on every `gh aw compile`, making the workaround fragile and unsupported. + +### Decision + +We will expose `extraHeaders`, `extraBodyFields`, and `sessionId` as explicit fields in the `sandbox.agent.targets.copilot` frontmatter section (`AgentAPIProxyTargetConfig`), and wire them through `BuildAWFConfigJSON` into the `apiProxy.targets.copilot` section of the generated AWF config. A dedicated `extractCopilotTargetConfig` helper (mirroring `extractAPITargetAuthHeader`) reads these fields from the parsed frontmatter and merges them into an existing or newly created copilot target entry. `sessionId` is opt-in only and is never auto-derived from `GITHUB_RUN_ID`, because strict OpenAI-compatible upstreams (e.g., Azure OpenAI) reject the unknown `session_id` body field with HTTP 400. + +### Alternatives Considered + +#### Alternative 1: Expose fields as top-level engine config rather than under `sandbox.agent.targets.copilot` + +Placing `extraHeaders` and `extraBodyFields` directly on the `engine` or `sandbox.agent` frontmatter block would have been a shorter path. This was rejected because it would be inconsistent with the established convention: per-provider API proxy overrides already live under `sandbox.agent.targets.`, and `authHeader` already follows this pattern for OpenAI and Anthropic targets. Deviating would have fragmented the per-provider config surface. + +#### Alternative 2: Auto-derive `sessionId` from `GITHUB_RUN_ID` at compile time + +Automatically populating `sessionId` from the `GITHUB_RUN_ID` environment variable would reduce boilerplate for tracking purposes. This was rejected because strict OpenAI-compatible servers (including Azure OpenAI, a primary BYOK target) reject requests containing an unknown `session_id` body field with HTTP 400. Auto-derivation would silently break BYOK configurations that work today. Opt-in is the only safe default. + +### Consequences + +#### Positive +- Workflow authors targeting OpenRouter-style BYOK providers can configure required routing headers (`x-openrouter-title`, `http-referer`) and custom body fields declaratively in frontmatter, without post-compile JSON edits that would be overwritten. +- The implementation follows existing patterns (`extractAPITargetAuthHeader` → `extractCopilotTargetConfig`), keeping the codebase consistent and the approach auditable against prior art. + +#### Negative +- `ExtraHeaders`, `ExtraBodyFields`, and `SessionId` are added to the generic `AWFAPITargetConfig` struct, but are semantically valid only for the `copilot` provider target. Code comments call this out, but misapplication to other providers (e.g., `openai`, `anthropic`) would be silently ignored rather than rejected. +- `sessionId` requires explicit opt-in with the full expression (e.g., `${{ github.run_id }}`). Authors wanting automatic session correlation must add it manually; there is no convenience default. + +#### Neutral +- The sidecar env vars (`AWF_BYOK_EXTRA_HEADERS`, `AWF_BYOK_EXTRA_BODY_FIELDS`, `AWF_PROVIDER_SESSION_ID`) already existed in the AWF proxy sidecar; this PR is a pure frontmatter-to-AWF-config wiring change with no sidecar modifications. +- The spec drift table (`specs/awf-config-sources-spec.md`) now tracks the three new mappings, extending the existing pattern for `apiProxy.targets.*.authHeader`. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From 804bca1568005d4320f0086b57ce98fa3eeb9780 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 04:30:22 +0000 Subject: [PATCH 05/10] fix(schema): add copilot target to sandbox.agent.targets with extraHeaders, extraBodyFields, sessionId fields The strict frontmatter schema was missing the copilot entry under sandbox.agent.targets, causing the documented BYOK fields to be rejected before reaching Go struct parsing. Changes: - Add `copilot` key to the targets object, pointing to a new `sandbox_copilot_api_target` $def - New def includes authHeader, extraHeaders (object of strings), extraBodyFields (object of strings), and sessionId (string) - additionalProperties: false enforces schema strictness for each Add 6 schema validation tests covering: extraHeaders accepted, extraBodyFields accepted, sessionId accepted, all three together, non-string header value rejected, unknown field rejected. Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/parser/schema_test.go | 133 +++++++++++++++++++ pkg/parser/schemas/main_workflow_schema.json | 32 +++++ 2 files changed, 165 insertions(+) diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 86f389daa73..e623558493f 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -2486,6 +2486,139 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets t.Error("unknown provider in sandbox.agent.targets should be rejected") } }) + + t.Run("copilot extraHeaders map is accepted", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "copilot", + "sandbox": map[string]any{ + "agent": map[string]any{ + "targets": map[string]any{ + "copilot": map[string]any{ + "extraHeaders": map[string]any{ + "x-openrouter-title": "my-workflow", + "http-referer": "https://github.com/org/repo", + }, + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-copilot-extra-headers-test.md") + if err != nil { + t.Errorf("valid copilot extraHeaders should be accepted, got error: %v", err) + } + }) + + t.Run("copilot extraBodyFields map is accepted", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "copilot", + "sandbox": map[string]any{ + "agent": map[string]any{ + "targets": map[string]any{ + "copilot": map[string]any{ + "extraBodyFields": map[string]any{ + "custom-field": "custom-value", + }, + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-copilot-extra-body-fields-test.md") + if err != nil { + t.Errorf("valid copilot extraBodyFields should be accepted, got error: %v", err) + } + }) + + t.Run("copilot sessionId string is accepted", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "copilot", + "sandbox": map[string]any{ + "agent": map[string]any{ + "targets": map[string]any{ + "copilot": map[string]any{ + "sessionId": "${{ github.run_id }}", + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-copilot-session-id-test.md") + if err != nil { + t.Errorf("valid copilot sessionId should be accepted, got error: %v", err) + } + }) + + t.Run("copilot all three BYOK fields together are accepted", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "copilot", + "sandbox": map[string]any{ + "agent": map[string]any{ + "targets": map[string]any{ + "copilot": map[string]any{ + "extraHeaders": map[string]any{ + "x-openrouter-title": "my-workflow", + }, + "extraBodyFields": map[string]any{ + "custom-field": "custom-value", + }, + "sessionId": "${{ github.run_id }}", + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-copilot-byok-all-fields-test.md") + if err != nil { + t.Errorf("all three copilot BYOK fields together should be accepted, got error: %v", err) + } + }) + + t.Run("copilot non-string extraHeaders value is rejected", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "copilot", + "sandbox": map[string]any{ + "agent": map[string]any{ + "targets": map[string]any{ + "copilot": map[string]any{ + "extraHeaders": map[string]any{ + "x-count": 42, + }, + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-copilot-extra-headers-invalid-test.md") + if err == nil { + t.Error("non-string extraHeaders value should be rejected by schema validation") + } + }) + + t.Run("copilot unknown field in target is rejected", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "copilot", + "sandbox": map[string]any{ + "agent": map[string]any{ + "targets": map[string]any{ + "copilot": map[string]any{ + "unknownField": "value", + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-copilot-unknown-field-test.md") + if err == nil { + t.Error("unknown field in copilot target should be rejected by schema validation") + } + }) } // TestValidateMainWorkflowFrontmatter_OnPermissionsVulnerabilityAlerts validates that diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 5ee805b2450..a720b3272f0 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3515,6 +3515,9 @@ }, "anthropic": { "$ref": "#/$defs/sandbox_agent_api_target" + }, + "copilot": { + "$ref": "#/$defs/sandbox_copilot_api_target" } }, "additionalProperties": false @@ -13802,6 +13805,35 @@ }, "additionalProperties": false }, + "sandbox_copilot_api_target": { + "type": "object", + "description": "AWF API proxy target configuration for the Copilot BYOK provider. Supports injecting additional headers, body fields, and an optional session ID on upstream requests.", + "properties": { + "authHeader": { + "type": "string", + "description": "Custom authentication header name to use when forwarding requests to the Copilot API. Example: \"api-key\" for Azure OpenAI gateways." + }, + "extraHeaders": { + "type": "object", + "description": "Additional non-sensitive HTTP headers to inject on Copilot BYOK upstream requests. Maps to AWF_BYOK_EXTRA_HEADERS. Example: { \"x-openrouter-title\": \"my-workflow\" }.", + "additionalProperties": { + "type": "string" + } + }, + "extraBodyFields": { + "type": "object", + "description": "Additional non-sensitive JSON body fields to inject on Copilot BYOK upstream requests. Maps to AWF_BYOK_EXTRA_BODY_FIELDS. Example: { \"custom-field\": \"custom-value\" }.", + "additionalProperties": { + "type": "string" + } + }, + "sessionId": { + "type": "string", + "description": "Optional session identifier injected as the x-session-id request header and session_id body field on Copilot BYOK upstream requests. Maps to AWF_PROVIDER_SESSION_ID. Must be set explicitly — strict OpenAI-compatible upstreams (e.g. Azure OpenAI) reject the unknown session_id body field with HTTP 400. Example: \"${{ github.run_id }}\"." + } + }, + "additionalProperties": false + }, "numeric_or_numeric_string": { "description": "Accepts a JSON number, or a numeric string in integer, decimal, or scientific notation (for example: 123, 12.34, 1.23e-4, +5.67E+8). Rejects trailing/non-numeric formats (for example: 123abc, free, 1.2.3).", "anyOf": [ From a122521e29d9553270c31d5ffb5d63e0be1cec88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 04:31:34 +0000 Subject: [PATCH 06/10] fix(schema): clarify sessionId description - optional, only set when upstream supports it Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index a720b3272f0..cf7950a4524 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -13829,7 +13829,7 @@ }, "sessionId": { "type": "string", - "description": "Optional session identifier injected as the x-session-id request header and session_id body field on Copilot BYOK upstream requests. Maps to AWF_PROVIDER_SESSION_ID. Must be set explicitly — strict OpenAI-compatible upstreams (e.g. Azure OpenAI) reject the unknown session_id body field with HTTP 400. Example: \"${{ github.run_id }}\"." + "description": "Optional session identifier injected as the x-session-id request header and session_id body field on Copilot BYOK upstream requests. Maps to AWF_PROVIDER_SESSION_ID. Only set this field when your upstream supports it — strict OpenAI-compatible upstreams (e.g. Azure OpenAI) reject the unknown session_id body field with HTTP 400. Example: \"${{ github.run_id }}\"." } }, "additionalProperties": false From 0ceba08b5a5ff22e78387a94ac8cfa04a4ba5d60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:03:39 +0000 Subject: [PATCH 07/10] fix: extract sandbox.agent.targets from frontmatter; add e2e BYOK compilation test Two issues addressed: 1. extractAgentSandboxConfig in frontmatter_extraction_security.go never extracted the `targets` key from the frontmatter map, so WorkflowData.SandboxConfig.Agent.Targets was always nil even when sandbox.agent.targets was present in frontmatter. Add full extraction for authHeader, extraHeaders, extraBodyFields, sessionId. 2. Add TestCopilotBYOKExtraFieldsInCompiledWorkflow: an end-to-end compilation test that writes a workflow with sandbox.agent.targets.copilot fields in frontmatter, compiles it, and verifies extraHeaders/extraBodyFields/sessionId appear in the generated AWF config JSON in the lock file. Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- ...ilot_byok_extra_fields_compilation_test.go | 92 +++++++++++++++++++ .../frontmatter_extraction_security.go | 38 ++++++++ 2 files changed, 130 insertions(+) create mode 100644 pkg/workflow/copilot_byok_extra_fields_compilation_test.go diff --git a/pkg/workflow/copilot_byok_extra_fields_compilation_test.go b/pkg/workflow/copilot_byok_extra_fields_compilation_test.go new file mode 100644 index 00000000000..447f021f168 --- /dev/null +++ b/pkg/workflow/copilot_byok_extra_fields_compilation_test.go @@ -0,0 +1,92 @@ +//go:build !integration + +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/github/gh-aw/pkg/stringutil" + "github.com/github/gh-aw/pkg/testutil" + "github.com/stretchr/testify/require" +) + +// TestCopilotBYOKExtraFieldsInCompiledWorkflow is an end-to-end compilation test +// verifying that sandbox.agent.targets.copilot.extraHeaders, extraBodyFields, and +// sessionId in frontmatter flow all the way through compilation into the AWF config +// JSON embedded in the generated .lock.yml file. +// +// This test covers the full path: frontmatter schema validation → WorkflowData parsing +// → BuildAWFConfigJSON → lock file printf script. +func TestCopilotBYOKExtraFieldsInCompiledWorkflow(t *testing.T) { + workflow := `--- +on: push +permissions: + contents: read +engine: + id: copilot + env: + COPILOT_PROVIDER_BASE_URL: "https://openrouter.ai/api/v1" + COPILOT_PROVIDER_API_KEY: "${{ secrets.OPENROUTER_API_KEY }}" +sandbox: + agent: + targets: + copilot: + extraHeaders: + x-openrouter-title: my-workflow + http-referer: https://github.com/org/repo + extraBodyFields: + custom-field: custom-value + sessionId: "${{ github.run_id }}" +strict: false +safe-outputs: + create-issue: +--- + +# BYOK workflow with extraHeaders, extraBodyFields, and sessionId +` + + tmpDir := testutil.TempDir(t, "copilot-byok-extra-fields") + testFile := filepath.Join(tmpDir, "byok-extra-fields.md") + require.NoError(t, os.WriteFile(testFile, []byte(workflow), 0644), "failed to write workflow file") + + compiler := NewCompiler() + require.NoError(t, compiler.CompileWorkflow(testFile), "failed to compile workflow") + + lockContent, err := os.ReadFile(stringutil.MarkdownToLockFile(testFile)) + require.NoError(t, err, "failed to read compiled lock file") + lockStr := string(lockContent) + + // extraHeaders should appear in the AWF config JSON (either escaped or unescaped, + // depending on whether shell-escaping was applied to the printf argument). + if !strings.Contains(lockStr, `"extraHeaders"`) && !strings.Contains(lockStr, `\"extraHeaders\"`) { + t.Error("expected extraHeaders to be present in compiled AWF config JSON") + } + if !strings.Contains(lockStr, `"x-openrouter-title"`) && !strings.Contains(lockStr, `\"x-openrouter-title\"`) { + t.Error("expected x-openrouter-title header key in compiled AWF config JSON") + } + if !strings.Contains(lockStr, `"my-workflow"`) && !strings.Contains(lockStr, `\"my-workflow\"`) { + t.Error("expected my-workflow header value in compiled AWF config JSON") + } + + // extraBodyFields should appear in the AWF config JSON. + if !strings.Contains(lockStr, `"extraBodyFields"`) && !strings.Contains(lockStr, `\"extraBodyFields\"`) { + t.Error("expected extraBodyFields to be present in compiled AWF config JSON") + } + if !strings.Contains(lockStr, `"custom-field"`) && !strings.Contains(lockStr, `\"custom-field\"`) { + t.Error("expected custom-field body field key in compiled AWF config JSON") + } + if !strings.Contains(lockStr, `"custom-value"`) && !strings.Contains(lockStr, `\"custom-value\"`) { + t.Error("expected custom-value body field value in compiled AWF config JSON") + } + + // sessionId should appear in the AWF config JSON. + if !strings.Contains(lockStr, `"sessionId"`) && !strings.Contains(lockStr, `\"sessionId\"`) { + t.Error("expected sessionId to be present in compiled AWF config JSON") + } + if !strings.Contains(lockStr, `github.run_id`) { + t.Error("expected github.run_id expression in compiled AWF config JSON sessionId") + } +} diff --git a/pkg/workflow/frontmatter_extraction_security.go b/pkg/workflow/frontmatter_extraction_security.go index b5aa5b922d6..446c29afba4 100644 --- a/pkg/workflow/frontmatter_extraction_security.go +++ b/pkg/workflow/frontmatter_extraction_security.go @@ -301,6 +301,44 @@ func (c *Compiler) extractAgentSandboxConfig(agentVal any) *AgentSandboxConfig { } } + // Extract targets (per-provider API proxy target overrides, e.g. authHeader, extraHeaders) + if targetsVal, hasTargets := agentObj["targets"]; hasTargets { + if targetsObj, ok := targetsVal.(map[string]any); ok { + agentConfig.Targets = make(map[string]*AgentAPIProxyTargetConfig) + for provider, targetAny := range targetsObj { + targetObj, ok := targetAny.(map[string]any) + if !ok { + continue + } + targetConfig := &AgentAPIProxyTargetConfig{} + if authHeader, ok := targetObj["authHeader"].(string); ok { + targetConfig.AuthHeader = authHeader + } + if extraHeaders, ok := targetObj["extraHeaders"].(map[string]any); ok { + targetConfig.ExtraHeaders = make(map[string]string) + for k, v := range extraHeaders { + if s, ok := v.(string); ok { + targetConfig.ExtraHeaders[k] = s + } + } + } + if extraBodyFields, ok := targetObj["extraBodyFields"].(map[string]any); ok { + targetConfig.ExtraBodyFields = make(map[string]string) + for k, v := range extraBodyFields { + if s, ok := v.(string); ok { + targetConfig.ExtraBodyFields[k] = s + } + } + } + if sessionId, ok := targetObj["sessionId"].(string); ok { + targetConfig.SessionId = sessionId + } + agentConfig.Targets[provider] = targetConfig + } + frontmatterExtractionSecurityLog.Printf("Extracted sandbox.agent.targets: %d provider(s)", len(agentConfig.Targets)) + } + } + return agentConfig } From 37b296ecf173c336545e98c71c724a7e66ad0b57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:05:11 +0000 Subject: [PATCH 08/10] test: strengthen sessionId assertion to check full expression syntax Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../copilot_byok_extra_fields_compilation_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/workflow/copilot_byok_extra_fields_compilation_test.go b/pkg/workflow/copilot_byok_extra_fields_compilation_test.go index 447f021f168..22dde13e971 100644 --- a/pkg/workflow/copilot_byok_extra_fields_compilation_test.go +++ b/pkg/workflow/copilot_byok_extra_fields_compilation_test.go @@ -82,11 +82,13 @@ safe-outputs: t.Error("expected custom-value body field value in compiled AWF config JSON") } - // sessionId should appear in the AWF config JSON. + // sessionId should appear in the AWF config JSON with the full expression preserved. if !strings.Contains(lockStr, `"sessionId"`) && !strings.Contains(lockStr, `\"sessionId\"`) { t.Error("expected sessionId to be present in compiled AWF config JSON") } - if !strings.Contains(lockStr, `github.run_id`) { - t.Error("expected github.run_id expression in compiled AWF config JSON sessionId") + // The ${{ github.run_id }} GitHub Actions expression must be preserved verbatim so + // it is evaluated at runtime. Check for the full expression syntax. + if !strings.Contains(lockStr, `${{ github.run_id }}`) { + t.Error("expected full '${{ github.run_id }}' expression in compiled AWF config JSON sessionId") } } From d04b095313b3b1d982567365e813022adaaaad29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:53:22 +0000 Subject: [PATCH 09/10] fix: wire copilot authHeader from frontmatter into AWF config; add test Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/awf_config.go | 4 ++++ pkg/workflow/awf_config_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index c10fda1e2c4..9efd20b06cf 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -570,6 +570,10 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { existing = &AWFAPITargetConfig{} targets["copilot"] = existing } + if copilotFrontmatter.AuthHeader != "" { + existing.AuthHeader = copilotFrontmatter.AuthHeader + awfConfigLog.Printf("API proxy: copilot authHeader=%s", copilotFrontmatter.AuthHeader) + } if len(copilotFrontmatter.ExtraHeaders) > 0 { existing.ExtraHeaders = copilotFrontmatter.ExtraHeaders awfConfigLog.Printf("API proxy: copilot extraHeaders configured (%d header(s))", len(copilotFrontmatter.ExtraHeaders)) diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index fda36bdfad8..4cb6c0d00ba 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -876,6 +876,34 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.Contains(t, jsonStr, `"${{ github.run_id }}"`, "should include sessionId value") }) + t.Run("copilot authHeader from frontmatter sandbox.agent.targets.copilot is included", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "copilot": { + AuthHeader: "api-key", + }, + }, + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"copilot"`, "should include copilot target") + assert.Contains(t, jsonStr, `"authHeader":"api-key"`, "should include copilot authHeader in apiProxy targets") + assert.NotContains(t, jsonStr, `"host":""`, "should not emit empty host when only authHeader is set") + }) + t.Run("copilot BYOK fields coexist with host from GetCopilotAPITarget", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "copilot", From 7d2dbc47ba9ddfc43bfb72514d6d2813df4a530e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:57:32 +0000 Subject: [PATCH 10/10] fix: add release-workflow.md to fallback AW files JSON Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/data/agentic_workflows_fallback_aw_files.json | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cli/data/agentic_workflows_fallback_aw_files.json b/pkg/cli/data/agentic_workflows_fallback_aw_files.json index a0d35bace50..2165df7dc76 100644 --- a/pkg/cli/data/agentic_workflows_fallback_aw_files.json +++ b/pkg/cli/data/agentic_workflows_fallback_aw_files.json @@ -33,6 +33,7 @@ "optimize-agentic-workflow.md", "patterns.md", "pr-reviewer.md", + "release-workflow.md", "report.md", "reuse.md", "safe-outputs-automation.md",