From f1cf35a9b5055665c2104aa8e8898a52344133da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 12:33:47 +0000 Subject: [PATCH 1/8] Initial plan From ecbfd1de359d2144ddc59dbbd56dc825ed9ba5b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 12:52:59 +0000 Subject: [PATCH 2/8] feat: expose authHeader in awf.apiProxy.targets frontmatter - Add AuthHeader field to AWFAPITargetConfig (awf_config.go) - Add extractAPITargetAuthHeader helper to engine_api_targets.go - Wire BuildAWFConfigJSON to read authHeader from rawFrontmatter - Add awf property + awf_provider_target def to main_workflow_schema.json - Add unit tests for helper and BuildAWFConfigJSON authHeader cases - Add schema validation tests in pkg/parser/schema_test.go - Update specs/awf-config-sources-spec.md drift table Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/parser/schema_test.go | 85 + pkg/parser/schemas/main_workflow_schema.json | 2948 ++++++++++++++---- pkg/workflow/awf_config.go | 28 + pkg/workflow/awf_config_test.go | 114 + pkg/workflow/awf_helpers_test.go | 71 + pkg/workflow/engine_api_targets.go | 54 + specs/awf-config-sources-spec.md | 2 + 7 files changed, 2772 insertions(+), 530 deletions(-) diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 38bc060c959..b95e18d6b07 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -1436,3 +1436,88 @@ func TestMainWorkflowSchema_GitHubAllowedSupportsToolCallLimits(t *testing.T) { t.Fatal("tools.github.allowed[].max alias should not be present") } } + +// TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets verifies that +// the awf.apiProxy.targets frontmatter section is validated by the schema, accepting valid +// authHeader strings and rejecting non-string values. +func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets(t *testing.T) { + t.Run("valid string authHeader for openai is accepted", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "codex", + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": "api-key", + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-auth-header-openai-test.md") + if err != nil { + t.Errorf("valid openai authHeader should be accepted, got error: %v", err) + } + }) + + t.Run("valid string authHeader for anthropic is accepted", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "claude", + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "anthropic": map[string]any{ + "authHeader": "api-key", + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-auth-header-anthropic-test.md") + if err != nil { + t.Errorf("valid anthropic authHeader should be accepted, got error: %v", err) + } + }) + + t.Run("non-string authHeader is rejected", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "codex", + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": 42, + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-auth-header-invalid-test.md") + if err == nil { + t.Error("non-string authHeader should be rejected by schema validation") + } + }) + + t.Run("unknown provider in targets is rejected", func(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "codex", + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "unknown-provider": map[string]any{ + "authHeader": "api-key", + }, + }, + }, + }, + } + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-unknown-provider-test.md") + if err == nil { + t.Error("unknown provider in awf.apiProxy.targets should be rejected") + } + }) +} diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 11ed9b591ba..e3699993c8a 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -5,35 +5,53 @@ "description": "JSON Schema for validating agentic workflow frontmatter configuration", "version": "1.0.0", "type": "object", - "required": ["on"], + "required": [ + "on" + ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 256, "description": "Workflow name that appears in the GitHub Actions interface. If not specified, defaults to the filename without extension.", - "examples": ["Copilot Agent PR Analysis", "Dev Hawk", "Smoke Claude"] + "examples": [ + "Copilot Agent PR Analysis", + "Dev Hawk", + "Smoke Claude" + ] }, "description": { "type": "string", "maxLength": 10000, "description": "Optional workflow description that is rendered as a comment in the generated GitHub Actions YAML file (.lock.yml)", - "examples": ["Quickstart for using the GitHub Actions library"] + "examples": [ + "Quickstart for using the GitHub Actions library" + ] }, "emoji": { "type": "string", "description": "Optional emoji to represent the workflow visually in listings and UI surfaces.", - "examples": ["\ud83e\udd16", "\ud83d\udd0d", "\ud83d\ude80"] + "examples": [ + "\ud83e\udd16", + "\ud83d\udd0d", + "\ud83d\ude80" + ] }, "source": { "type": "string", "description": "Optional source reference indicating where this workflow was added from. Format: owner/repo/path@ref (e.g., githubnext/agentics/workflows/ci-doctor.md@v1.0.0). Rendered as a comment in the generated lock file.", - "examples": ["githubnext/agentics/workflows/ci-doctor.md", "githubnext/agentics/workflows/daily-perf-improver.md@1f181b37d3fe5862ab590648f25a292e345b5de6"] + "examples": [ + "githubnext/agentics/workflows/ci-doctor.md", + "githubnext/agentics/workflows/daily-perf-improver.md@1f181b37d3fe5862ab590648f25a292e345b5de6" + ] }, "redirect": { "type": "string", "description": "Optional workflow location redirect for updates. Format: workflow spec or GitHub URL (e.g., owner/repo/path@ref or https://github.com/owner/repo/blob/main/path.md). When present, update follows this location and rewrites source.", - "examples": ["githubnext/agentics/workflows/ci-doctor-v2.md@main", "https://github.com/githubnext/agentics/blob/main/workflows/ci-doctor-v2.md"] + "examples": [ + "githubnext/agentics/workflows/ci-doctor-v2.md@main", + "https://github.com/githubnext/agentics/blob/main/workflows/ci-doctor-v2.md" + ] }, "tracker-id": { "type": "string", @@ -41,7 +59,11 @@ "maxLength": 128, "pattern": "^[a-zA-Z0-9_-]+$", "description": "Optional tracker identifier to tag all created assets (issues, discussions, comments, pull requests). Must be at least 8 characters and contain only alphanumeric characters, hyphens, and underscores. This identifier will be inserted in the body/description of all created assets to enable searching and retrieving assets associated with this workflow.", - "examples": ["workflow-2024-q1", "team-alpha-bot", "security_audit_v2"] + "examples": [ + "workflow-2024-q1", + "team-alpha-bot", + "security_audit_v2" + ] }, "labels": { "type": "array", @@ -51,9 +73,18 @@ "minLength": 1 }, "examples": [ - ["automation", "security"], - ["docs", "maintenance"], - ["ci", "testing"] + [ + "automation", + "security" + ], + [ + "docs", + "maintenance" + ], + [ + "ci", + "testing" + ] ] }, "metadata": { @@ -90,7 +121,9 @@ { "type": "object", "description": "Import specification with path and optional inputs", - "required": ["path"], + "required": [ + "path" + ], "additionalProperties": false, "properties": { "path": { @@ -136,7 +169,9 @@ { "type": "object", "description": "Import specification with 'uses'/'with' syntax (mirrors GitHub Actions reusable workflow syntax). 'uses' references the workflow path and 'with' provides input values.", - "required": ["uses"], + "required": [ + "uses" + ], "additionalProperties": false, "properties": { "uses": { @@ -221,7 +256,9 @@ { "type": "object", "description": "Import specification with path and optional inputs", - "required": ["path"], + "required": [ + "path" + ], "additionalProperties": false, "properties": { "path": { @@ -267,7 +304,9 @@ { "type": "object", "description": "Import specification with 'uses'/'with' syntax.", - "required": ["uses"], + "required": [ + "uses" + ], "additionalProperties": false, "properties": { "uses": { @@ -339,10 +378,21 @@ } ], "examples": [ - ["shared/jqschema.md", "shared/reporting.md"], - ["shared/mcp/tavily.md", "shared/jqschema.md", "shared/reporting.md"], - ["../instructions/documentation.instructions.md"], - [".github/agents/my-agent.md"], + [ + "shared/jqschema.md", + "shared/reporting.md" + ], + [ + "shared/mcp/tavily.md", + "shared/jqschema.md", + "shared/reporting.md" + ], + [ + "../instructions/documentation.instructions.md" + ], + [ + ".github/agents/my-agent.md" + ], [ { "path": "shared/discussions-data-fetch.md", @@ -352,7 +402,10 @@ } ], { - "aw": ["shared/common-tools.md", "shared/mcp/tavily.md"] + "aw": [ + "shared/common-tools.md", + "shared/mcp/tavily.md" + ] } ] }, @@ -366,25 +419,45 @@ "pattern": "\\$\\{\\{" } }, - "examples": [["triage-issue.md", "label-issue.md"], ["my-custom-action.yml"], ["shared/helper-action.yml", "close-stale.md"]] + "examples": [ + [ + "triage-issue.md", + "label-issue.md" + ], + [ + "my-custom-action.yml" + ], + [ + "shared/helper-action.yml", + "close-stale.md" + ] + ] }, "inlined-imports": { "type": "boolean", "default": false, "description": "If true, inline all imports (including those without inputs) at compilation time in the generated lock.yml instead of using runtime-import macros. When enabled, the frontmatter hash covers the entire markdown body so any change to the content will invalidate the hash.", - "examples": [true, false] + "examples": [ + true, + false + ] }, "on": { "description": "Workflow triggers that define when the agentic workflow should run. Supports standard GitHub Actions trigger events plus special command triggers for /commands (required)", "examples": [ { "issues": { - "types": ["opened"] + "types": [ + "opened" + ] } }, { "pull_request": { - "types": ["opened", "synchronize"] + "types": [ + "opened", + "synchronize" + ] } }, "workflow_dispatch", @@ -398,7 +471,13 @@ "type": "string", "minLength": 1, "description": "Simple trigger event name (e.g., 'push', 'issues', 'pull_request', 'discussion', 'schedule', 'fork', 'create', 'delete', 'public', 'watch', 'workflow_call'), schedule shorthand (e.g., 'daily', 'weekly'), or slash command shorthand (e.g., '/my-bot' expands to slash_command + workflow_dispatch)", - "examples": ["push", "issues", "workflow_dispatch", "daily", "/my-bot"] + "examples": [ + "push", + "issues", + "workflow_dispatch", + "daily", + "/my-bot" + ] }, { "type": "object", @@ -450,7 +529,16 @@ { "type": "string", "description": "Single event name or '*' for all events. Use GitHub Actions event names: 'issues', 'issue_comment', 'pull_request_comment', 'pull_request', 'pull_request_review_comment', 'discussion', 'discussion_comment'.", - "enum": ["*", "issues", "issue_comment", "pull_request_comment", "pull_request", "pull_request_review_comment", "discussion", "discussion_comment"] + "enum": [ + "*", + "issues", + "issue_comment", + "pull_request_comment", + "pull_request", + "pull_request_review_comment", + "discussion", + "discussion_comment" + ] }, { "type": "array", @@ -459,7 +547,16 @@ "items": { "type": "string", "description": "GitHub Actions event name.", - "enum": ["*", "issues", "issue_comment", "pull_request_comment", "pull_request", "pull_request_review_comment", "discussion", "discussion_comment"] + "enum": [ + "*", + "issues", + "issue_comment", + "pull_request_comment", + "pull_request", + "pull_request_review_comment", + "discussion", + "discussion_comment" + ] }, "maxItems": 25 } @@ -468,7 +565,10 @@ "strategy": { "type": "string", "description": "Slash command trigger compilation strategy. 'inline' (default) compiles direct comment listeners in this workflow. 'centralized' compiles this workflow as workflow_dispatch-centric and routes slash events via the generated central trigger workflow.", - "enum": ["inline", "centralized"] + "enum": [ + "inline", + "centralized" + ] } }, "additionalProperties": false @@ -521,7 +621,16 @@ { "type": "string", "description": "Single event name or '*' for all events. Use GitHub Actions event names: 'issues', 'issue_comment', 'pull_request_comment', 'pull_request', 'pull_request_review_comment', 'discussion', 'discussion_comment'.", - "enum": ["*", "issues", "issue_comment", "pull_request_comment", "pull_request", "pull_request_review_comment", "discussion", "discussion_comment"] + "enum": [ + "*", + "issues", + "issue_comment", + "pull_request_comment", + "pull_request", + "pull_request_review_comment", + "discussion", + "discussion_comment" + ] }, { "type": "array", @@ -530,7 +639,16 @@ "items": { "type": "string", "description": "GitHub Actions event name.", - "enum": ["*", "issues", "issue_comment", "pull_request_comment", "pull_request", "pull_request_review_comment", "discussion", "discussion_comment"] + "enum": [ + "*", + "issues", + "issue_comment", + "pull_request_comment", + "pull_request", + "pull_request_review_comment", + "discussion", + "discussion_comment" + ] }, "maxItems": 25 } @@ -601,7 +719,12 @@ { "type": "string", "description": "Single item type or '*' for all types.", - "enum": ["*", "issues", "pull_request", "discussion"] + "enum": [ + "*", + "issues", + "pull_request", + "discussion" + ] }, { "type": "array", @@ -610,7 +733,12 @@ "items": { "type": "string", "description": "Item type.", - "enum": ["*", "issues", "pull_request", "discussion"] + "enum": [ + "*", + "issues", + "pull_request", + "discussion" + ] }, "maxItems": 3 } @@ -623,7 +751,10 @@ "strategy": { "type": "string", "description": "Label command trigger compilation strategy. 'inline' (default) compiles direct labeled listeners in this workflow. 'decentralized' compiles this workflow as workflow_dispatch-centric and routes labeled events via the generated agentic_commands.yml workflow.", - "enum": ["inline", "decentralized"] + "enum": [ + "inline", + "decentralized" + ] } }, "additionalProperties": false @@ -684,25 +815,37 @@ }, "oneOf": [ { - "required": ["branches"], + "required": [ + "branches" + ], "not": { - "required": ["branches-ignore"] + "required": [ + "branches-ignore" + ] } }, { - "required": ["branches-ignore"], + "required": [ + "branches-ignore" + ], "not": { - "required": ["branches"] + "required": [ + "branches" + ] } }, { "not": { "anyOf": [ { - "required": ["branches"] + "required": [ + "branches" + ] }, { - "required": ["branches-ignore"] + "required": [ + "branches-ignore" + ] } ] } @@ -712,25 +855,37 @@ { "oneOf": [ { - "required": ["paths"], + "required": [ + "paths" + ], "not": { - "required": ["paths-ignore"] + "required": [ + "paths-ignore" + ] } }, { - "required": ["paths-ignore"], + "required": [ + "paths-ignore" + ], "not": { - "required": ["paths"] + "required": [ + "paths" + ] } }, { "not": { "anyOf": [ { - "required": ["paths"] + "required": [ + "paths" + ] }, { - "required": ["paths-ignore"] + "required": [ + "paths-ignore" + ] } ] } @@ -850,25 +1005,37 @@ "additionalProperties": false, "oneOf": [ { - "required": ["branches"], + "required": [ + "branches" + ], "not": { - "required": ["branches-ignore"] + "required": [ + "branches-ignore" + ] } }, { - "required": ["branches-ignore"], + "required": [ + "branches-ignore" + ], "not": { - "required": ["branches"] + "required": [ + "branches" + ] } }, { "not": { "anyOf": [ { - "required": ["branches"] + "required": [ + "branches" + ] }, { - "required": ["branches-ignore"] + "required": [ + "branches-ignore" + ] } ] } @@ -878,25 +1045,37 @@ { "oneOf": [ { - "required": ["paths"], + "required": [ + "paths" + ], "not": { - "required": ["paths-ignore"] + "required": [ + "paths-ignore" + ] } }, { - "required": ["paths-ignore"], + "required": [ + "paths-ignore" + ], "not": { - "required": ["paths"] + "required": [ + "paths" + ] } }, { "not": { "anyOf": [ { - "required": ["paths"] + "required": [ + "paths" + ] }, { - "required": ["paths-ignore"] + "required": [ + "paths-ignore" + ] } ] } @@ -915,7 +1094,26 @@ "description": "Types of issue events", "items": { "type": "string", - "enum": ["opened", "edited", "deleted", "transferred", "pinned", "unpinned", "closed", "reopened", "assigned", "unassigned", "labeled", "unlabeled", "locked", "unlocked", "milestoned", "demilestoned", "typed", "untyped"] + "enum": [ + "opened", + "edited", + "deleted", + "transferred", + "pinned", + "unpinned", + "closed", + "reopened", + "assigned", + "unassigned", + "labeled", + "unlabeled", + "locked", + "unlocked", + "milestoned", + "demilestoned", + "typed", + "untyped" + ] } }, "names": { @@ -953,7 +1151,11 @@ "description": "Types of issue comment events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } }, "lock-for-agent": { @@ -972,7 +1174,21 @@ "description": "Types of discussion events", "items": { "type": "string", - "enum": ["created", "edited", "deleted", "transferred", "pinned", "unpinned", "labeled", "unlabeled", "locked", "unlocked", "category_changed", "answered", "unanswered"] + "enum": [ + "created", + "edited", + "deleted", + "transferred", + "pinned", + "unpinned", + "labeled", + "unlabeled", + "locked", + "unlocked", + "category_changed", + "answered", + "unanswered" + ] } } } @@ -987,7 +1203,11 @@ "description": "Types of discussion comment events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } } } @@ -1016,7 +1236,9 @@ "description": "Optional IANA timezone string for timezone-aware scheduling (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo', 'UTC'). When set, the cron expression is interpreted in the specified timezone instead of UTC." } }, - "required": ["cron"], + "required": [ + "cron" + ], "additionalProperties": false }, "maxItems": 10 @@ -1066,7 +1288,13 @@ }, "type": { "type": "string", - "enum": ["string", "choice", "boolean", "number", "environment"], + "enum": [ + "string", + "choice", + "boolean", + "number", + "environment" + ], "description": "Input type. GitHub Actions supports: string (default), boolean, choice (string with predefined options), number, and environment (string referencing a GitHub environment)" }, "options": { @@ -1100,7 +1328,11 @@ "description": "Types of workflow run events", "items": { "type": "string", - "enum": ["completed", "requested", "in_progress"] + "enum": [ + "completed", + "requested", + "in_progress" + ] } }, "branches": { @@ -1122,25 +1354,37 @@ }, "oneOf": [ { - "required": ["branches"], + "required": [ + "branches" + ], "not": { - "required": ["branches-ignore"] + "required": [ + "branches-ignore" + ] } }, { - "required": ["branches-ignore"], + "required": [ + "branches-ignore" + ], "not": { - "required": ["branches"] + "required": [ + "branches" + ] } }, { "not": { "anyOf": [ { - "required": ["branches"] + "required": [ + "branches" + ] }, { - "required": ["branches-ignore"] + "required": [ + "branches-ignore" + ] } ] } @@ -1157,7 +1401,15 @@ "description": "Types of release events", "items": { "type": "string", - "enum": ["published", "unpublished", "created", "edited", "deleted", "prereleased", "released"] + "enum": [ + "published", + "unpublished", + "created", + "edited", + "deleted", + "prereleased", + "released" + ] } } } @@ -1172,7 +1424,11 @@ "description": "Types of pull request review comment events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } } } @@ -1187,7 +1443,11 @@ "description": "Types of branch protection rule events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } } } @@ -1202,7 +1462,12 @@ "description": "Types of check run events", "items": { "type": "string", - "enum": ["created", "rerequested", "completed", "requested_action"] + "enum": [ + "created", + "rerequested", + "completed", + "requested_action" + ] } } } @@ -1217,7 +1482,9 @@ "description": "Types of check suite events", "items": { "type": "string", - "enum": ["completed"] + "enum": [ + "completed" + ] } } } @@ -1277,13 +1544,31 @@ "oneOf": [ { "type": "string", - "enum": ["error", "failure", "pending", "success", "inactive", "in_progress", "queued", "waiting"] + "enum": [ + "error", + "failure", + "pending", + "success", + "inactive", + "in_progress", + "queued", + "waiting" + ] }, { "type": "array", "items": { "type": "string", - "enum": ["error", "failure", "pending", "success", "inactive", "in_progress", "queued", "waiting"] + "enum": [ + "error", + "failure", + "pending", + "success", + "inactive", + "in_progress", + "queued", + "waiting" + ] }, "minItems": 1 } @@ -1329,7 +1614,11 @@ "description": "Types of label events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } } } @@ -1344,7 +1633,9 @@ "description": "Types of merge group events", "items": { "type": "string", - "enum": ["checks_requested"] + "enum": [ + "checks_requested" + ] } } } @@ -1359,7 +1650,13 @@ "description": "Types of milestone events", "items": { "type": "string", - "enum": ["created", "closed", "opened", "edited", "deleted"] + "enum": [ + "created", + "closed", + "opened", + "edited", + "deleted" + ] } } } @@ -1477,25 +1774,37 @@ "additionalProperties": false, "oneOf": [ { - "required": ["branches"], + "required": [ + "branches" + ], "not": { - "required": ["branches-ignore"] + "required": [ + "branches-ignore" + ] } }, { - "required": ["branches-ignore"], + "required": [ + "branches-ignore" + ], "not": { - "required": ["branches"] + "required": [ + "branches" + ] } }, { "not": { "anyOf": [ { - "required": ["branches"] + "required": [ + "branches" + ] }, { - "required": ["branches-ignore"] + "required": [ + "branches-ignore" + ] } ] } @@ -1505,25 +1814,37 @@ { "oneOf": [ { - "required": ["paths"], + "required": [ + "paths" + ], "not": { - "required": ["paths-ignore"] + "required": [ + "paths-ignore" + ] } }, { - "required": ["paths-ignore"], + "required": [ + "paths-ignore" + ], "not": { - "required": ["paths"] + "required": [ + "paths" + ] } }, { "not": { "anyOf": [ { - "required": ["paths"] + "required": [ + "paths" + ] }, { - "required": ["paths-ignore"] + "required": [ + "paths-ignore" + ] } ] } @@ -1542,7 +1863,11 @@ "description": "Types of pull request review events", "items": { "type": "string", - "enum": ["submitted", "edited", "dismissed"] + "enum": [ + "submitted", + "edited", + "dismissed" + ] } } } @@ -1557,7 +1882,10 @@ "description": "Types of registry package events", "items": { "type": "string", - "enum": ["published", "updated"] + "enum": [ + "published", + "updated" + ] } } } @@ -1599,7 +1927,9 @@ "description": "Types of watch events", "items": { "type": "string", - "enum": ["started"] + "enum": [ + "started" + ] } } } @@ -1631,7 +1961,11 @@ }, "type": { "type": "string", - "enum": ["string", "number", "boolean"], + "enum": [ + "string", + "number", + "boolean" + ], "description": "Type of the input parameter" }, "default": { @@ -1673,7 +2007,9 @@ }, { "type": "object", - "required": ["query"], + "required": [ + "query" + ], "properties": { "query": { "type": "string", @@ -1695,7 +2031,9 @@ }, "scope": { "type": "string", - "enum": ["none"], + "enum": [ + "none" + ], "description": "Scope for the search query. Set to 'none' to disable the automatic 'repo:owner/repo' scoping, enabling org-wide or cross-repo queries." } }, @@ -1713,7 +2051,9 @@ }, { "type": "object", - "required": ["query"], + "required": [ + "query" + ], "properties": { "query": { "type": "string", @@ -1726,7 +2066,9 @@ }, "scope": { "type": "string", - "enum": ["none"], + "enum": [ + "none" + ], "description": "Scope for the search query. Set to 'none' to disable the automatic 'repo:owner/repo' scoping, enabling org-wide or cross-repo queries." } }, @@ -1744,7 +2086,9 @@ }, { "type": "boolean", - "enum": [true], + "enum": [ + true + ], "description": "Skip workflow execution if any CI checks on the target branch are currently failing. For pull_request events, checks the base branch. For other events, checks the current ref." }, { @@ -1840,7 +2184,15 @@ "oneOf": [ { "type": "string", - "enum": ["admin", "maintainer", "maintain", "write", "triage", "read", "all"], + "enum": [ + "admin", + "maintainer", + "maintain", + "write", + "triage", + "read", + "all" + ], "description": "Single repository permission level that can trigger the workflow. Use 'all' to allow any authenticated user (\u26a0\ufe0f disables permission checking entirely - use with caution)" }, { @@ -1848,7 +2200,14 @@ "description": "List of repository permission levels that can trigger the workflow. Permission checks are automatically applied to potentially unsafe triggers.", "items": { "type": "string", - "enum": ["admin", "maintainer", "maintain", "write", "triage", "read"], + "enum": [ + "admin", + "maintainer", + "maintain", + "write", + "triage", + "read" + ], "description": "Repository permission level: 'admin' (full access), 'maintainer'/'maintain' (repository management), 'write' (push access), 'triage' (issue management), 'read' (read-only access)" }, "minItems": 1, @@ -1895,11 +2254,24 @@ "oneOf": [ { "type": "string", - "enum": ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes", "none"] + "enum": [ + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "rocket", + "eyes", + "none" + ] }, { "type": "integer", - "enum": [1, -1], + "enum": [ + 1, + -1 + ], "description": "YAML parses +1 and -1 without quotes as integers. These are converted to +1 and -1 strings respectively." }, { @@ -1910,11 +2282,24 @@ "oneOf": [ { "type": "string", - "enum": ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes", "none"] + "enum": [ + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "rocket", + "eyes", + "none" + ] }, { "type": "integer", - "enum": [1, -1], + "enum": [ + 1, + -1 + ], "description": "YAML parses +1 and -1 without quotes as integers. These are converted to +1 and -1 strings respectively." } ], @@ -2001,7 +2386,9 @@ "github-token": { "type": "string", "description": "Custom GitHub token for pre-activation reactions, activation status comments, and skip-if search queries. When specified, overrides the default GITHUB_TOKEN for these operations.", - "examples": ["${{ secrets.MY_GITHUB_TOKEN }}"] + "examples": [ + "${{ secrets.MY_GITHUB_TOKEN }}" + ] }, "github-app": { "$ref": "#/$defs/github_app", @@ -2023,7 +2410,11 @@ "additionalItems": false, "uniqueItems": true, "default": [], - "examples": [["secrets_fetcher"]] + "examples": [ + [ + "secrets_fetcher" + ] + ] }, "steps": { "type": "array", @@ -2135,51 +2526,99 @@ "properties": { "actions": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "checks": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "contents": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "deployments": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "discussions": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "issues": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "packages": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "pages": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "pull-requests": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "repository-projects": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "security-events": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "statuses": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] } }, "additionalProperties": false @@ -2202,7 +2641,9 @@ }, { "type": "string", - "enum": ["full"] + "enum": [ + "full" + ] } ], "description": "Controls the stale lock file check in the activation job. Set to false to disable the check, true (default) to enable frontmatter hash checking, or \"full\" to check both frontmatter and body hashes. Use \"full\" when prompt-body edits should also trigger recompilation detection. Useful when the workflow source files are managed outside the default GitHub repo context (e.g. cross-repo org rulesets) and the stale check is not needed (set false), or when comprehensive drift detection is required (set \"full\")." @@ -2230,25 +2671,37 @@ { "command": { "name": "mergefest", - "events": ["pull_request_comment"] + "events": [ + "pull_request_comment" + ] } }, { "workflow_run": { - "workflows": ["Dev"], - "types": ["completed"], - "branches": ["copilot/**"] + "workflows": [ + "Dev" + ], + "types": [ + "completed" + ], + "branches": [ + "copilot/**" + ] } }, { "pull_request": { - "types": ["ready_for_review"] + "types": [ + "ready_for_review" + ] }, "workflow_dispatch": null }, { "push": { - "branches": ["main"] + "branches": [ + "main" + ] } } ] @@ -2275,7 +2728,10 @@ "oneOf": [ { "type": "string", - "enum": ["read-all", "write-all"], + "enum": [ + "read-all", + "write-all" + ], "description": "Simple permissions string: 'read-all' (all read permissions) or 'write-all' (all write permissions)" }, { @@ -2286,7 +2742,10 @@ "run-name": { "type": "string", "description": "Custom name for workflow runs that appears in the GitHub Actions interface (supports GitHub expressions like ${{ github.event.issue.title }})", - "examples": ["Deploy to ${{ github.event.inputs.environment }}", "Build #${{ github.run_number }}"] + "examples": [ + "Deploy to ${{ github.event.inputs.environment }}", + "Build #${{ github.run_number }}" + ] }, "jobs": { "type": "object", @@ -2312,10 +2771,14 @@ "additionalProperties": false, "oneOf": [ { - "required": ["uses"] + "required": [ + "uses" + ] }, { - "required": ["run"] + "required": [ + "run" + ] } ], "properties": { @@ -2535,7 +2998,9 @@ "description": "The URL to set as the environment URL in the deployment." } }, - "required": ["name"] + "required": [ + "name" + ] } ], "description": "The GitHub Actions environment this job references. When set, any protection rules for the environment must pass before the job runs. Use this to gate jobs on manual approval workflows." @@ -2617,17 +3082,26 @@ "$ref": "#/$defs/github_actions_runs_on", "examples": [ "ubuntu-latest", - ["ubuntu-latest", "self-hosted"], + [ + "ubuntu-latest", + "self-hosted" + ], { "group": "larger-runners", - "labels": ["ubuntu-latest-8-cores"] + "labels": [ + "ubuntu-latest-8-cores" + ] } ] }, "runs-on-slim": { "type": "string", "description": "Runner for all framework/generated jobs (activation, pre-activation, safe-outputs, unlock, APM, etc.). Provides a compile-stable override for generated job runners without requiring a safe-outputs section. Overridden by safe-outputs.runs-on when both are set. Defaults to 'ubuntu-slim'. Use this when your infrastructure does not provide the default runner or when you need consistent runner selection across all jobs.", - "examples": ["self-hosted", "ubuntu-latest", "ubuntu-22.04"] + "examples": [ + "self-hosted", + "ubuntu-latest", + "ubuntu-22.04" + ] }, "timeout-minutes": { "description": "Workflow timeout in minutes (GitHub Actions standard field). Defaults to 20 minutes for agentic workflows. Has sensible defaults and can typically be omitted. Custom runners support longer timeouts beyond the GitHub-hosted runner limit. Supports GitHub Actions expressions (e.g. '${{ inputs.timeout }}') for reusable workflow_call workflows.", @@ -2635,7 +3109,11 @@ { "type": "integer", "minimum": 1, - "examples": [5, 10, 30] + "examples": [ + 5, + 10, + 30 + ] }, { "type": "string", @@ -2650,7 +3128,10 @@ { "type": "string", "description": "Simple concurrency group name to prevent multiple runs in the same group. Use expressions like '${{ github.workflow }}' for per-workflow isolation or '${{ github.ref }}' for per-branch isolation. Agentic workflows automatically generate enhanced concurrency policies using 'gh-aw-{engine-id}' as the default group to limit concurrent AI workloads across all workflows using the same engine.", - "examples": ["my-workflow-group", "workflow-${{ github.ref }}"] + "examples": [ + "my-workflow-group", + "workflow-${{ github.ref }}" + ] }, { "type": "object", @@ -2667,13 +3148,20 @@ }, "queue": { "type": "string", - "enum": ["single", "max"], + "enum": [ + "single", + "max" + ], "description": "Pending run queue behavior for this concurrency group. 'single' (default) allows one pending run and replaces older pending runs. 'max' allows up to 100 pending runs in FIFO order." }, "job-discriminator": { "type": "string", "description": "Additional discriminator expression appended to compiler-generated job-level concurrency groups (agent, output jobs). Use this when multiple workflow instances are dispatched concurrently with different inputs (fan-out pattern) to prevent job-level concurrency groups from colliding. For example, '${{ inputs.finding_id }}' ensures each dispatched run gets a unique job-level group. Supports GitHub Actions expressions. This field is stripped from the compiled lock file (it is a gh-aw extension, not a GitHub Actions field).", - "examples": ["${{ inputs.finding_id }}", "${{ inputs.item_id }}", "${{ github.run_id }}"] + "examples": [ + "${{ inputs.finding_id }}", + "${{ inputs.item_id }}", + "${{ github.run_id }}" + ] } }, "required": [], @@ -2725,7 +3213,9 @@ "inline-sub-agents": { "type": "boolean", "description": "Deprecated switch for inline sub-agent support. Inline sub-agents are enabled by default. Setting this to false is not supported and causes a compilation error.", - "examples": [true] + "examples": [ + true + ] }, "features": { "description": "Feature flags and configuration options for experimental or optional features in the workflow. Each feature can be a boolean flag or a string value. The 'action-tag' feature (string) specifies the tag or SHA to use when referencing actions/setup in compiled workflows (for testing purposes only).", @@ -2754,8 +3244,13 @@ }, "examples": [ { - "sonnet": ["mygateway/*sonnet-v3*"], - "": ["sonnet", "gpt-5-codex"] + "sonnet": [ + "mygateway/*sonnet-v3*" + ], + "": [ + "sonnet", + "gpt-5-codex" + ] } ] }, @@ -2786,7 +3281,9 @@ }, { "type": "object", - "required": ["variants"], + "required": [ + "variants" + ], "properties": { "variants": { "type": "array", @@ -2842,7 +3339,10 @@ "type": "array", "items": { "type": "object", - "required": ["name", "threshold"], + "required": [ + "name", + "threshold" + ], "properties": { "name": { "type": "string", @@ -2866,7 +3366,12 @@ }, "analysis_type": { "type": "string", - "enum": ["t_test", "mann_whitney", "proportion_test", "bayesian_ab"], + "enum": [ + "t_test", + "mann_whitney", + "proportion_test", + "bayesian_ab" + ], "description": "Statistical test to use for automated analysis by the reporting workflow. Valid values: t_test (Welch's two-sample t-test), mann_whitney (non-parametric rank test), proportion_test (two-proportion z-test), bayesian_ab (Bayesian A/B test)." }, "tags": { @@ -2901,15 +3406,24 @@ }, "examples": [ { - "feature1": ["A", "B"] + "feature1": [ + "A", + "B" + ] }, { "prompt_style": { - "variants": ["concise", "verbose"], + "variants": [ + "concise", + "verbose" + ], "description": "Test whether concise vs verbose prompts reduce token consumption", "hypothesis": "H0: no change in tokens. H1: concise reduces by >=15%", "metric": "effective_tokens", - "secondary_metrics": ["duration_ms", "discussion_word_count"], + "secondary_metrics": [ + "duration_ms", + "discussion_word_count" + ], "guardrail_metrics": [ { "name": "success_rate", @@ -2921,23 +3435,35 @@ } ], "min_samples": 25, - "weight": [50, 50], + "weight": [ + 50, + 50 + ], "issue": 1234, "start_date": "2026-05-01", "end_date": "2026-06-15", "analysis_type": "t_test", - "tags": ["cost", "prompting"], + "tags": [ + "cost", + "prompting" + ], "notify": { "issue": 1234 } }, - "model_temp": ["low", "high"] + "model_temp": [ + "low", + "high" + ] } ], "properties": { "storage": { "type": "string", - "enum": ["cache", "repo"], + "enum": [ + "cache", + "repo" + ], "default": "repo", "description": "Storage backend for experiment state. 'repo' (default) persists state to a git branch named 'experiments/{sanitizedWorkflowID}' (workflow ID lowercased with hyphens removed, e.g. 'my-workflow' -> 'experiments/myworkflow') for durability across cache evictions. 'cache' uses GitHub Actions cache (legacy behaviour). Repo storage is recommended because experiment data is valuable and more durable than cache." } @@ -2946,7 +3472,9 @@ "disable-model-invocation": { "type": "boolean", "description": "Controls whether the custom agent should disable model invocation. When set to true, the agent will not make additional model calls.", - "examples": [true] + "examples": [ + true + ] }, "secrets": { "description": "Secret values passed to workflow execution. Secrets can be defined as simple strings (GitHub Actions expressions) or objects with 'value' and 'description' properties. Typically used to provide secrets to MCP servers or custom engines. Note: For passing secrets to reusable workflows, use the jobs..secrets field instead.", @@ -2960,7 +3488,9 @@ { "type": "object", "description": "Secret with metadata", - "required": ["value"], + "required": [ + "value" + ], "properties": { "value": { "type": "string", @@ -3012,7 +3542,9 @@ "description": "A deployment URL" } }, - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false } ] @@ -3080,7 +3612,9 @@ "description": "Additional Docker container options" } }, - "required": ["image"], + "required": [ + "image" + ], "additionalProperties": false } ] @@ -3150,7 +3684,9 @@ "description": "Additional Docker container options" } }, - "required": ["image"], + "required": [ + "image" + ], "additionalProperties": false } ] @@ -3162,16 +3698,26 @@ "examples": [ "defaults", { - "allowed": ["defaults", "github"] + "allowed": [ + "defaults", + "github" + ] }, { - "allowed": ["defaults", "python", "node", "*.example.com"] + "allowed": [ + "defaults", + "python", + "node", + "*.example.com" + ] } ], "oneOf": [ { "type": "string", - "enum": ["defaults"], + "enum": [ + "defaults" + ], "description": "Use default network permissions (basic infrastructure: certificates, JSON schema, Ubuntu, etc.)" }, { @@ -3210,7 +3756,10 @@ "oneOf": [ { "type": "string", - "enum": ["default", "awf"], + "enum": [ + "default", + "awf" + ], "description": "String format for sandbox type: 'default' for no sandbox, 'awf' for Agent Workflow Firewall. Note: Legacy 'srt' and 'sandbox-runtime' values are automatically migrated to 'awf'" }, { @@ -3219,7 +3768,10 @@ "properties": { "type": { "type": "string", - "enum": ["default", "awf"], + "enum": [ + "default", + "awf" + ], "description": "Legacy sandbox type field (use agent instead). Note: Legacy 'srt' and 'sandbox-runtime' values are automatically migrated to 'awf'" }, "agent": { @@ -3233,7 +3785,9 @@ }, { "type": "string", - "enum": ["awf"], + "enum": [ + "awf" + ], "description": "Sandbox type: 'awf' for Agent Workflow Firewall" }, { @@ -3242,12 +3796,16 @@ "properties": { "id": { "type": "string", - "enum": ["awf"], + "enum": [ + "awf" + ], "description": "Agent identifier (replaces 'type' field in new format): 'awf' for Agent Workflow Firewall" }, "type": { "type": "string", - "enum": ["awf"], + "enum": [ + "awf" + ], "description": "Legacy: Sandbox type to use (use 'id' instead)" }, "version": { @@ -3283,13 +3841,22 @@ "pattern": "^[^:]+:[^:]+:(ro|rw)$", "description": "Mount specification in format 'source:destination:mode'" }, - "examples": [["/host/data:/data:ro", "/usr/local/bin/custom-tool:/usr/local/bin/custom-tool:ro"]] + "examples": [ + [ + "/host/data:/data:ro", + "/usr/local/bin/custom-tool:/usr/local/bin/custom-tool:ro" + ] + ] }, "memory": { "type": "string", "description": "Memory limit for the AWF container (e.g., '4g', '8g'). Passed as --memory-limit to AWF. If not specified, AWF's default memory limit is used.", "pattern": "^[0-9]+(b|k|m|g|kb|mb|gb|B|K|M|G|KB|MB|GB)$", - "examples": ["4g", "8g", "512m"] + "examples": [ + "4g", + "8g", + "512m" + ] }, "config": { "type": "object", @@ -3405,16 +3972,26 @@ "description": "Container image for the MCP gateway executable (required)" }, "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "x-internal": true, "description": "Optional version/tag for the container image (e.g., 'latest', 'v1.0.0')", - "examples": ["latest", "v1.0.0"] + "examples": [ + "latest", + "v1.0.0" + ] }, "entrypoint": { "type": "string", "x-internal": true, "description": "Optional custom entrypoint for the MCP gateway container. Overrides the container's default entrypoint.", - "examples": ["/bin/bash", "/custom/start.sh", "/usr/bin/env"] + "examples": [ + "/bin/bash", + "/custom/start.sh", + "/usr/bin/env" + ] }, "args": { "type": "array", @@ -3440,7 +4017,12 @@ "pattern": "^[^:]+:[^:]+:(ro|rw)$", "description": "Mount specification in format 'source:destination:mode'" }, - "examples": [["/host/data:/container/data:ro", "/host/config:/container/config:rw"]] + "examples": [ + [ + "/host/data:/container/data:ro", + "/host/config:/container/config:rw" + ] + ] }, "env": { "type": "object", @@ -3465,15 +4047,23 @@ }, "domain": { "type": "string", - "enum": ["localhost", "host.docker.internal"], + "enum": [ + "localhost", + "host.docker.internal" + ], "description": "Gateway domain for URL generation (default: 'host.docker.internal' when agent is enabled, 'localhost' when disabled)" }, "keepalive-interval": { "type": "integer", "description": "Keepalive ping interval in seconds for HTTP MCP backends. Sends periodic pings to prevent session expiry during long-running agent tasks. Set to -1 to disable keepalive pings. Unset or 0 uses the gateway default (1500 seconds = 25 minutes).", "minimum": -1, - "examples": [-1, 300, 600, 1500] - } + "examples": [ + -1, + 300, + 600, + 1500 + ] + } }, "additionalProperties": false } @@ -3505,7 +4095,10 @@ "if": { "type": "string", "description": "Conditional execution expression", - "examples": ["${{ github.event.workflow_run.event == 'workflow_dispatch' }}", "${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}"] + "examples": [ + "${{ github.event.workflow_run.event == 'workflow_dispatch' }}", + "${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}" + ] }, "steps": { "description": "Custom workflow steps", @@ -3686,7 +4279,9 @@ { "type": "integer", "not": { - "enum": [0] + "enum": [ + 0 + ] }, "description": "Maximum effective-token (ET) budget for AWF API proxy enforcement. Use a negative value to disable budget enforcement and token steering." }, @@ -3737,7 +4332,10 @@ "filesystem": { "type": "stdio", "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-filesystem"] + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem" + ] } }, { @@ -3816,7 +4414,9 @@ { "type": "object", "additionalProperties": false, - "required": ["name"], + "required": [ + "name" + ], "properties": { "name": { "type": "string", @@ -3835,18 +4435,33 @@ }, "mode": { "type": "string", - "enum": ["gh-proxy", "local", "remote"], + "enum": [ + "gh-proxy", + "local", + "remote" + ], "description": "GitHub access mode. Prefer 'gh-proxy' for better performance (uses pre-authenticated gh CLI prompt guidance). Legacy MCP transport values 'local' and 'remote' are accepted for backward compatibility and use GitHub MCP server prompt guidance." }, "type": { "type": "string", - "enum": ["local", "remote"], + "enum": [ + "local", + "remote" + ], "description": "GitHub MCP transport type: 'local' (Docker-based, default) or 'remote' (hosted at api.githubcopilot.com)" }, "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Optional version specification for the GitHub MCP server (used with 'local' type). Can be a string (e.g., 'v1.0.0', 'latest') or number (e.g., 20, 3.11). Numeric values are automatically converted to strings at runtime.", - "examples": ["v1.0.0", "latest", 20, 3.11] + "examples": [ + "v1.0.0", + "latest", + 20, + 3.11 + ] }, "args": { "type": "array", @@ -3900,14 +4515,26 @@ "pattern": "^[^:]+:[^:]+(:(ro|rw))?$", "description": "Mount specification in format 'host:container:mode'" }, - "examples": [["/data:/data:ro", "/tmp:/tmp:rw"], ["/opt:/opt:ro"]] + "examples": [ + [ + "/data:/data:ro", + "/tmp:/tmp:rw" + ], + [ + "/opt:/opt:ro" + ] + ] }, "allowed-repos": { "description": "Guard policy: repository access configuration. Restricts which repositories the agent can access. Use 'all' to allow all repos, 'public' for public repositories only, '${{ github.repository }}' for the current repository, or an array of repository patterns (e.g., 'owner/repo', 'owner/*', 'owner/prefix*').", "oneOf": [ { "type": "string", - "enum": ["all", "public", "${{ github.repository }}"], + "enum": [ + "all", + "public", + "${{ github.repository }}" + ], "description": "Allow access to all repositories ('all'), only public repositories ('public'), or the current repository ('${{ github.repository }}')" }, { @@ -3928,7 +4555,11 @@ "oneOf": [ { "type": "string", - "enum": ["all", "public", "${{ github.repository }}"], + "enum": [ + "all", + "public", + "${{ github.repository }}" + ], "description": "Allow access to all repositories ('all'), only public repositories ('public'), or the current repository ('${{ github.repository }}')" }, { @@ -3945,7 +4576,12 @@ "min-integrity": { "type": "string", "description": "Guard policy: minimum required integrity level for repository access. Restricts the agent to users with at least the specified permission level.", - "enum": ["none", "unapproved", "approved", "merged"] + "enum": [ + "none", + "unapproved", + "approved", + "merged" + ] }, "blocked-users": { "description": "Guard policy: GitHub usernames whose content is unconditionally blocked. Items from these users receive 'blocked' integrity (below 'none') and are always denied, even when 'min-integrity' is 'none'. Cannot be overridden by 'approval-labels'. Requires 'min-integrity' to be set. Accepts an array of usernames, a comma-separated string, a newline-separated string, or a GitHub Actions expression (e.g. '${{ vars.BLOCKED_USERS }}').", @@ -4007,10 +4643,27 @@ "items": { "type": "string", "description": "GitHub ReactionContent enum value", - "enum": ["THUMBS_UP", "THUMBS_DOWN", "HEART", "HOORAY", "CONFUSED", "ROCKET", "EYES", "LAUGH"] + "enum": [ + "THUMBS_UP", + "THUMBS_DOWN", + "HEART", + "HOORAY", + "CONFUSED", + "ROCKET", + "EYES", + "LAUGH" + ] }, - "default": ["THUMBS_UP", "HEART"], - "examples": [["THUMBS_UP", "HEART"]] + "default": [ + "THUMBS_UP", + "HEART" + ], + "examples": [ + [ + "THUMBS_UP", + "HEART" + ] + ] }, "disapproval-reactions": { "type": "array", @@ -4018,21 +4671,47 @@ "items": { "type": "string", "description": "GitHub ReactionContent enum value", - "enum": ["THUMBS_UP", "THUMBS_DOWN", "HEART", "HOORAY", "CONFUSED", "ROCKET", "EYES", "LAUGH"] + "enum": [ + "THUMBS_UP", + "THUMBS_DOWN", + "HEART", + "HOORAY", + "CONFUSED", + "ROCKET", + "EYES", + "LAUGH" + ] }, - "default": ["THUMBS_DOWN", "CONFUSED"], - "examples": [["THUMBS_DOWN", "CONFUSED"]] + "default": [ + "THUMBS_DOWN", + "CONFUSED" + ], + "examples": [ + [ + "THUMBS_DOWN", + "CONFUSED" + ] + ] }, "disapproval-integrity": { "type": "string", "description": "Guard policy: integrity level assigned when a disapproval reaction is present. Optional, defaults to 'none'. Requires the 'integrity-reactions' feature flag and MCPG >= v0.2.18.", - "enum": ["none", "unapproved", "approved", "merged"], + "enum": [ + "none", + "unapproved", + "approved", + "merged" + ], "default": "none" }, "endorser-min-integrity": { "type": "string", "description": "Guard policy: minimum integrity level required for an endorser (reactor) to promote content. Optional, defaults to 'approved'. Requires the 'integrity-reactions' feature flag and MCPG >= v0.2.18.", - "enum": ["unapproved", "approved", "merged"], + "enum": [ + "unapproved", + "approved", + "merged" + ], "default": "approved" }, "github-app": { @@ -4043,16 +4722,30 @@ "additionalProperties": false, "examples": [ { - "toolsets": ["pull_requests", "actions", "repos"] + "toolsets": [ + "pull_requests", + "actions", + "repos" + ] }, { - "allowed": ["search_pull_requests", "pull_request_read", "list_pull_requests", "get_file_contents", "list_commits", "get_commit"] + "allowed": [ + "search_pull_requests", + "pull_request_read", + "list_pull_requests", + "get_file_contents", + "list_commits", + "get_commit" + ] }, { "read-only": true }, { - "toolsets": ["pull_requests", "repos"] + "toolsets": [ + "pull_requests", + "repos" + ] } ] } @@ -4060,14 +4753,25 @@ "examples": [ null, { - "toolsets": ["pull_requests", "actions", "repos"] + "toolsets": [ + "pull_requests", + "actions", + "repos" + ] }, { - "allowed": ["search_pull_requests", "pull_request_read", "get_file_contents"] + "allowed": [ + "search_pull_requests", + "pull_request_read", + "get_file_contents" + ] }, { "read-only": true, - "toolsets": ["repos", "issues"] + "toolsets": [ + "repos", + "issues" + ] }, false ] @@ -4094,10 +4798,36 @@ ], "examples": [ true, - ["git fetch", "git checkout", "git status", "git diff", "git log", "make recompile", "make fmt", "make lint", "make test-unit", "cat", "echo", "ls"], - ["echo", "ls", "cat"], - ["gh pr list *", "gh search prs *", "jq *"], - ["date *", "echo *", "cat", "ls"] + [ + "git fetch", + "git checkout", + "git status", + "git diff", + "git log", + "make recompile", + "make fmt", + "make lint", + "make test-unit", + "cat", + "echo", + "ls" + ], + [ + "echo", + "ls", + "cat" + ], + [ + "gh pr list *", + "gh search prs *", + "jq *" + ], + [ + "date *", + "echo *", + "cat", + "ls" + ] ] }, "web-fetch": { @@ -4178,9 +4908,15 @@ "description": "Playwright tool configuration with custom version and arguments", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Optional version pin. In CLI mode (recommended): the @playwright/cli npm package version (e.g., '0.1.11'). In MCP mode (deprecated): the Playwright browser Docker image version (e.g., 'v1.56.1'). Omit to use the default version.", - "examples": ["0.1.11", "v1.56.1"] + "examples": [ + "0.1.11", + "v1.56.1" + ] }, "args": { "type": "array", @@ -4192,7 +4928,10 @@ "mode": { "type": "string", "description": "Integration mode: 'cli' (recommended) installs @playwright/cli via npm for token-efficient CLI invocations \u2014 use playwright-cli commands in bash and localhost to reach local servers; 'mcp' (deprecated) runs a Docker-based MCP server.", - "enum": ["cli", "mcp"] + "enum": [ + "cli", + "mcp" + ] } }, "additionalProperties": false @@ -4211,7 +4950,10 @@ "description": "Enable agentic-workflows tool with default settings (same as true)" } ], - "examples": [true, null] + "examples": [ + true, + null + ] }, "cache-memory": { "description": "Cache memory MCP configuration for persistent memory storage", @@ -4248,7 +4990,10 @@ }, "scope": { "type": "string", - "enum": ["workflow", "repo"], + "enum": [ + "workflow", + "repo" + ], "default": "workflow", "description": "Cache restore key scope: 'workflow' (default, only restores from same workflow) or 'repo' (restores from any workflow in the repository). Use 'repo' with caution as it allows cross-workflow cache sharing." }, @@ -4301,7 +5046,10 @@ }, "scope": { "type": "string", - "enum": ["workflow", "repo"], + "enum": [ + "workflow", + "repo" + ], "default": "workflow", "description": "Cache restore key scope: 'workflow' (default, only restores from same workflow) or 'repo' (restores from any workflow in the repository). Use 'repo' with caution as it allows cross-workflow cache sharing." }, @@ -4313,7 +5061,10 @@ "description": "List of allowed file extensions (e.g., [\".json\", \".txt\"]). Default: [\".json\", \".jsonl\", \".txt\", \".md\", \".csv\"]" } }, - "required": ["id", "key"], + "required": [ + "id", + "key" + ], "additionalProperties": false }, "minItems": 1, @@ -4404,7 +5155,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -4425,7 +5179,11 @@ { "type": "integer", "minimum": 1, - "examples": [60, 120, 300] + "examples": [ + 60, + 120, + 300 + ] }, { "type": "string", @@ -4440,7 +5198,11 @@ { "type": "integer", "minimum": 1, - "examples": [30, 60, 120] + "examples": [ + 30, + 60, + 120 + ] }, { "type": "string", @@ -4452,7 +5214,9 @@ "cli-proxy": { "type": "boolean", "description": "When true, each user-facing MCP server is mounted as a standalone CLI tool on PATH. The agent can then call MCP servers via shell commands (e.g. 'github issue_read --method get ...'). CLI-mounted servers remain in the MCP gateway config so their containers can start, and are removed only from the agent's final config during convert_gateway_config_*.sh processing. Default: false.", - "examples": [true] + "examples": [ + true + ] }, "serena": { "description": "REMOVED: Built-in support for Serena has been removed. Use the shared/mcp/serena.md workflow instead.", @@ -4736,17 +5500,25 @@ "description": "Optional custom name for the cache step (overrides auto-generated name)" } }, - "required": ["key", "path"], + "required": [ + "key", + "path" + ], "additionalProperties": false, "examples": [ { "key": "node-modules-${{ hashFiles('package-lock.json') }}", "path": "node_modules", - "restore-keys": ["node-modules-"] + "restore-keys": [ + "node-modules-" + ] }, { "key": "build-cache-${{ github.sha }}", - "path": ["dist", ".cache"], + "path": [ + "dist", + ".cache" + ], "restore-keys": "build-cache-", "fail-on-cache-miss": false } @@ -4811,7 +5583,10 @@ "description": "Optional custom name for the cache step (overrides auto-generated name)" } }, - "required": ["key", "path"], + "required": [ + "key", + "path" + ], "additionalProperties": false } } @@ -4825,13 +5600,18 @@ { "create-issue": { "title-prefix": "[AI] ", - "labels": ["automation", "ai-generated"] + "labels": [ + "automation", + "ai-generated" + ] } }, { "create-pull-request": { "title-prefix": "[Bot] ", - "labels": ["bot"] + "labels": [ + "bot" + ] } }, { @@ -4853,7 +5633,23 @@ "items": { "type": "string" }, - "examples": [["repo"], ["repo", "octocat/hello-world"], ["microsoft/vscode", "microsoft/typescript"], ["repo", "${{ github.repository }}"]] + "examples": [ + [ + "repo" + ], + [ + "repo", + "octocat/hello-world" + ], + [ + "microsoft/vscode", + "microsoft/typescript" + ], + [ + "repo", + "${{ github.repository }}" + ] + ] }, "create-issue": { "oneOf": [ @@ -4955,7 +5751,9 @@ }, { "type": "boolean", - "enum": [false], + "enum": [ + false + ], "description": "Set to false to explicitly disable expiration" } ], @@ -4994,28 +5792,43 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false, "examples": [ { "title-prefix": "[ca] ", - "labels": ["automation", "dependencies"], + "labels": [ + "automation", + "dependencies" + ], "assignees": "copilot" }, { "title-prefix": "[duplicate-code] ", - "labels": ["code-quality", "automated-analysis"], + "labels": [ + "code-quality", + "automated-analysis" + ], "assignees": "copilot" }, { - "allowed-repos": ["org/other-repo", "org/another-repo"], + "allowed-repos": [ + "org/other-repo", + "org/another-repo" + ], "title-prefix": "[cross-repo] " }, { "title-prefix": "[weekly-report] ", - "labels": ["report", "automation"], + "labels": [ + "report", + "automation" + ], "close-older-issues": true } ] @@ -5072,7 +5885,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -5127,7 +5943,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -5144,7 +5963,9 @@ { "type": "object", "description": "Configuration for managing GitHub Projects boards. Enable agents to add issues and pull requests to projects, update custom field values (status, priority, effort, dates), create project fields and views. By default it is update-only: if the project does not exist, the job fails with instructions to create it. To allow workflows to create missing projects, explicitly opt in via agent output field create_if_missing=true. Requires a Personal Access Token (PAT) or GitHub App token with Projects permissions (default GITHUB_TOKEN cannot be used). Agent output includes: project (full URL or temporary project ID like aw_XXXXXXXXXXXX or #aw_XXXXXXXXXXXX from create_project), content_type (issue|pull_request|draft_issue), content_number, fields, create_if_missing. For specialized operations, agent can also provide: operation (create_fields|create_view), field_definitions (array of field configs when operation=create_fields), view (view config object when operation=create_view).", - "required": ["project"], + "required": [ + "project" + ], "properties": { "max": { "description": "Maximum number of project operations to perform (default: 10). Each operation may add a project item, or update its fields. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').", @@ -5169,7 +5990,10 @@ "type": "string", "description": "Target project URL for update-project operations. This is required in the configuration for documentation purposes. Agent messages MUST explicitly include the project field in their output - the configured value is not used as a fallback. Must be a valid GitHub Projects v2 URL.", "pattern": "^https://github\\.com/(users|orgs)/([^/]+|<[A-Z_]+>)/projects/(\\d+|<[A-Z_]+>)$", - "examples": ["https://github.com/orgs/myorg/projects/123", "https://github.com/users/username/projects/456"] + "examples": [ + "https://github.com/orgs/myorg/projects/123", + "https://github.com/users/username/projects/456" + ] }, "target-repo": { "description": "Default repository in format 'owner/repo' for cross-repository content resolution. When specified, the agent can use 'target_repo' in agent output to resolve issues or PRs from this repository. Wildcards ('*') are not allowed. Supports GitHub Actions expression syntax (e.g., '${{ vars.TARGET_REPO }}').", @@ -5208,7 +6032,10 @@ "items": { "type": "object", "description": "View configuration for creating project views", - "required": ["name", "layout"], + "required": [ + "name", + "layout" + ], "properties": { "name": { "type": "string", @@ -5216,7 +6043,11 @@ }, "layout": { "type": "string", - "enum": ["table", "board", "roadmap"], + "enum": [ + "table", + "board", + "roadmap" + ], "description": "The layout type of the view" }, "filter": { @@ -5243,7 +6074,10 @@ "description": "Optional array of project custom fields to create up-front.", "items": { "type": "object", - "required": ["name", "data-type"], + "required": [ + "name", + "data-type" + ], "properties": { "name": { "type": "string", @@ -5251,7 +6085,13 @@ }, "data-type": { "type": "string", - "enum": ["DATE", "TEXT", "NUMBER", "SINGLE_SELECT", "ITERATION"], + "enum": [ + "DATE", + "TEXT", + "NUMBER", + "SINGLE_SELECT", + "ITERATION" + ], "description": "The GitHub Projects v2 custom field type" }, "options": { @@ -5268,7 +6108,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false, @@ -5328,7 +6171,10 @@ "items": { "type": "object", "description": "View configuration for creating project views", - "required": ["name", "layout"], + "required": [ + "name", + "layout" + ], "properties": { "name": { "type": "string", @@ -5336,7 +6182,11 @@ }, "layout": { "type": "string", - "enum": ["table", "board", "roadmap"], + "enum": [ + "table", + "board", + "roadmap" + ], "description": "The layout type of the view" }, "filter": { @@ -5363,7 +6213,10 @@ "description": "Optional array of project custom fields to create automatically after project creation.", "items": { "type": "object", - "required": ["name", "data-type"], + "required": [ + "name", + "data-type" + ], "properties": { "name": { "type": "string", @@ -5371,7 +6224,13 @@ }, "data-type": { "type": "string", - "enum": ["DATE", "TEXT", "NUMBER", "SINGLE_SELECT", "ITERATION"], + "enum": [ + "DATE", + "TEXT", + "NUMBER", + "SINGLE_SELECT", + "ITERATION" + ], "description": "The GitHub Projects v2 custom field type" }, "options": { @@ -5388,7 +6247,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -5408,7 +6270,9 @@ { "type": "object", "description": "Configuration for posting status updates to GitHub Projects. Status updates provide stakeholder communication about project progress, health, and timeline. Each update appears in the project's Updates tab and creates a historical record. Requires a Personal Access Token (PAT) or GitHub App token with Projects read & write permission (default GITHUB_TOKEN cannot be used). Typically used by scheduled workflows or orchestrators to post regular progress summaries with status indicators (on-track, at-risk, off-track, complete, inactive), dates, and progress details.", - "required": ["project"], + "required": [ + "project" + ], "properties": { "max": { "description": "Maximum number of status updates to create (default: 1). Typically 1 per orchestrator run. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').", @@ -5433,12 +6297,18 @@ "type": "string", "description": "Target project URL for status update operations. This is required in the configuration for documentation purposes. Agent messages MUST explicitly include the project field in their output - the configured value is not used as a fallback. Must be a valid GitHub Projects v2 URL.", "pattern": "^https://github\\.com/(users|orgs)/([^/]+|<[A-Z_]+>)/projects/(\\d+|<[A-Z_]+>)$", - "examples": ["https://github.com/orgs/myorg/projects/123", "https://github.com/users/username/projects/456"] + "examples": [ + "https://github.com/orgs/myorg/projects/123", + "https://github.com/users/username/projects/456" + ] }, "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false, @@ -5470,9 +6340,16 @@ "description": "Optional prefix for the discussion title" }, "category": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Optional discussion category. Can be a category ID (string or numeric value), category name, or category slug/route. If not specified, uses the first available category. Matched first against category IDs, then against category names, then against category slugs. Numeric values are automatically converted to strings at runtime.", - "examples": ["General", "audits", 123456789] + "examples": [ + "General", + "audits", + 123456789 + ] }, "min-body-length": { "type": "integer", @@ -5554,7 +6431,9 @@ }, { "type": "boolean", - "enum": [false], + "enum": [ + false + ], "description": "Set to false to explicitly disable expiration" } ], @@ -5568,7 +6447,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false, @@ -5590,12 +6472,17 @@ "close-older-discussions": true }, { - "labels": ["weekly-report", "automation"], + "labels": [ + "weekly-report", + "automation" + ], "category": "reports", "close-older-discussions": true }, { - "allowed-repos": ["org/other-repo"], + "allowed-repos": [ + "org/other-repo" + ], "category": "General" } ] @@ -5660,7 +6547,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false, @@ -5669,7 +6559,10 @@ "required-category": "Ideas" }, { - "required-labels": ["resolved", "completed"], + "required-labels": [ + "resolved", + "completed" + ], "max": 1 } ] @@ -5737,7 +6630,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "github-token": { "$ref": "#/$defs/github_token", @@ -5809,11 +6705,18 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "state-reason": { "type": "string", - "enum": ["completed", "not_planned", "duplicate"], + "enum": [ + "completed", + "not_planned", + "duplicate" + ], "default": "completed", "description": "Reason for closing the issue (default: completed)" } @@ -5824,7 +6727,10 @@ "required-title-prefix": "[refactor] " }, { - "required-labels": ["automated", "stale"], + "required-labels": [ + "automated", + "stale" + ], "max": 10 } ] @@ -5889,7 +6795,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false, @@ -5898,7 +6807,10 @@ "required-title-prefix": "[bot] " }, { - "required-labels": ["automated", "outdated"], + "required-labels": [ + "automated", + "outdated" + ], "max": 5 } ] @@ -5963,7 +6875,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false, @@ -5972,7 +6887,10 @@ "required-title-prefix": "[bot] " }, { - "required-labels": ["automated", "ready"], + "required-labels": [ + "automated", + "ready" + ], "max": 1 } ] @@ -6039,7 +6957,14 @@ "description": "List of allowed reasons for hiding older comments when hide-older-comments is enabled. Default: all reasons allowed (spam, abuse, off_topic, outdated, resolved, low_quality).", "items": { "type": "string", - "enum": ["spam", "abuse", "off_topic", "outdated", "resolved", "low_quality"] + "enum": [ + "spam", + "abuse", + "off_topic", + "outdated", + "resolved", + "low_quality" + ] } }, "discussions": { @@ -6079,7 +7004,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false, @@ -6220,7 +7148,11 @@ }, "if-no-changes": { "type": "string", - "enum": ["warn", "error", "ignore"], + "enum": [ + "warn", + "error", + "ignore" + ], "description": "Behavior when no changes to push: 'warn' (default - log warning but succeed), 'error' (fail the action), or 'ignore' (silent success)" }, "allow-empty": { @@ -6348,7 +7280,12 @@ "oneOf": [ { "type": "string", - "enum": ["blocked", "allowed", "fallback-to-issue", "request_review"], + "enum": [ + "blocked", + "allowed", + "fallback-to-issue", + "request_review" + ], "description": "Controls protected-file protection. request_review (default): create the PR but prepend a caution block and submit a REQUEST_CHANGES review for manual scrutiny. blocked: hard-block any patch that modifies package manifests (e.g. package.json, go.mod), engine instruction files (e.g. AGENTS.md, CLAUDE.md) or .github/ files. allowed: allow all changes. fallback-to-issue: push the branch but create a review issue instead of a PR, so a human can review the manifest changes before merging.", "default": "request_review" }, @@ -6364,7 +7301,12 @@ "oneOf": [ { "type": "string", - "enum": ["blocked", "allowed", "fallback-to-issue", "request_review"], + "enum": [ + "blocked", + "allowed", + "fallback-to-issue", + "request_review" + ], "description": "Protection policy. request_review (default): create the PR but prepend a caution block and submit a REQUEST_CHANGES review. blocked: hard-block any patch that modifies protected files. allowed: allow all changes. fallback-to-issue: push the branch but create a review issue instead of a PR.", "default": "request_review" }, @@ -6381,7 +7323,15 @@ "type": "string" }, "description": "List of filenames or path prefixes to remove from the default protected-file set. Items are matched by basename (e.g. \"AGENTS.md\") or path prefix (e.g. \".agents/\"). Use this to allow the agent to modify specific files that are otherwise blocked by default.", - "examples": [["AGENTS.md"], ["AGENTS.md", ".agents/"]] + "examples": [ + [ + "AGENTS.md" + ], + [ + "AGENTS.md", + ".agents/" + ] + ] } }, "additionalProperties": false, @@ -6418,7 +7368,10 @@ "oneOf": [ { "type": "string", - "enum": ["am", "bundle"], + "enum": [ + "am", + "bundle" + ], "default": "bundle", "description": "Transport format for packaging changes. \"bundle\" (default) uses git bundle, which preserves merge commit topology, per-commit authorship, and merge-resolution-only content. \"am\" uses git format-patch/git am." }, @@ -6438,26 +7391,38 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "allow-workflows": { "type": "boolean", "description": "When true, adds workflows: write to the GitHub App token permissions. Required when allowed-files targets .github/workflows/ paths. Requires safe-outputs.github-app to be configured because the workflows permission is a GitHub App-only permission and cannot be granted via GITHUB_TOKEN.", "default": false, - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false, "examples": [ { "title-prefix": "[docs] ", - "labels": ["documentation", "automation"], + "labels": [ + "documentation", + "automation" + ], "reviewers": "copilot", "draft": false }, { "title-prefix": "[security-fix] ", - "labels": ["security", "automated-fix"], + "labels": [ + "security", + "automated-fix" + ], "reviewers": "copilot" } ] @@ -6493,7 +7458,10 @@ "side": { "type": "string", "description": "Side of the diff for comments: 'LEFT' or 'RIGHT' (default: 'RIGHT')", - "enum": ["LEFT", "RIGHT"] + "enum": [ + "LEFT", + "RIGHT" + ] }, "target": { "type": "string", @@ -6517,7 +7485,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -6569,7 +7540,11 @@ }, { "type": "string", - "enum": ["always", "none", "if-body"], + "enum": [ + "always", + "none", + "if-body" + ], "description": "Controls when AI-generated footer is added to the review body: 'always' (default), 'none' (never), or 'if-body' (only when review has body text)." } ], @@ -6594,7 +7569,11 @@ "type": "array", "items": { "type": "string", - "enum": ["APPROVE", "COMMENT", "REQUEST_CHANGES"] + "enum": [ + "APPROVE", + "COMMENT", + "REQUEST_CHANGES" + ] }, "description": "Optional list of allowed review event types. If omitted, all event types (APPROVE, COMMENT, REQUEST_CHANGES) are allowed. Use this to restrict the agent to specific event types, e.g. [COMMENT, REQUEST_CHANGES] to prevent approvals.", "minItems": 1 @@ -6610,7 +7589,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -6681,7 +7663,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -6732,7 +7717,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -6797,7 +7785,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -6836,7 +7827,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -6879,7 +7873,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "github-app": { "$ref": "#/$defs/github_app", @@ -6989,7 +7986,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -7074,7 +8074,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -7175,7 +8178,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -7238,7 +8244,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -7305,7 +8314,10 @@ ] }, "target": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Target issue/PR to assign agents to. Use 'triggering' (default) for the triggering issue/PR, '*' to require explicit issue_number/pull_number, or a specific issue/PR number. With 'triggering', auto-resolves from github.event.issue.number or github.event.pull_request.number." }, "target-repo": { @@ -7339,7 +8351,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -7386,7 +8401,10 @@ ] }, "target": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Target issue to assign users to. Use 'triggering' (default) for the triggering issue, '*' to allow any issue, or a specific issue number." }, "target-repo": { @@ -7412,7 +8430,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -7470,7 +8491,10 @@ ] }, "target": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Target issue to unassign users from. Use 'triggering' (default) for the triggering issue, '*' to allow any issue, or a specific issue number." }, "target-repo": { @@ -7491,7 +8515,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -7572,7 +8599,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -7599,7 +8629,10 @@ "description": "Allow updating issue title - presence of key indicates field can be updated" }, "body": { - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "description": "Allow updating issue body. Set to true to enable body updates, false to disable. For backward compatibility, null (body:) also enables body updates.", "default": true }, @@ -7645,7 +8678,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -7693,7 +8729,11 @@ "operation": { "type": "string", "description": "Default operation for body updates: 'append' (add to end), 'prepend' (add to start), or 'replace' (overwrite completely). Defaults to 'replace' if not specified.", - "enum": ["append", "prepend", "replace"] + "enum": [ + "append", + "prepend", + "replace" + ] }, "footer": { "type": "boolean", @@ -7726,7 +8766,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -7805,7 +8848,10 @@ "staged": { "type": "boolean", "description": "If true, evaluate merge gates and emit preview results without executing the merge API call.", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-title-prefix": { "type": "string", @@ -7899,7 +8945,11 @@ }, "if-no-changes": { "type": "string", - "enum": ["warn", "error", "ignore"], + "enum": [ + "warn", + "error", + "ignore" + ], "description": "Behavior when no changes to push: 'warn' (default - log warning but succeed), 'error' (fail the action), or 'ignore' (silent success)" }, "ignore-missing-branch-failure": { @@ -7924,7 +8974,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "github-token-for-extra-empty-commit": { "type": "string", @@ -7965,7 +9018,11 @@ "oneOf": [ { "type": "string", - "enum": ["blocked", "allowed", "fallback-to-issue"], + "enum": [ + "blocked", + "allowed", + "fallback-to-issue" + ], "description": "Controls protected-file protection. blocked (default): hard-block any patch that modifies package manifests (e.g. package.json, go.mod), engine instruction files (e.g. AGENTS.md, CLAUDE.md) or .github/ files. allowed: allow all changes. fallback-to-issue: create a review issue instead of pushing to the PR branch, so a human can review the changes before applying.", "default": "blocked" }, @@ -7981,7 +9038,11 @@ "oneOf": [ { "type": "string", - "enum": ["blocked", "allowed", "fallback-to-issue"], + "enum": [ + "blocked", + "allowed", + "fallback-to-issue" + ], "description": "Protection policy. blocked (default): hard-block any patch that modifies protected files. allowed: allow all changes. fallback-to-issue: create a review issue instead of pushing.", "default": "blocked" }, @@ -7998,7 +9059,15 @@ "type": "string" }, "description": "List of filenames or path prefixes to remove from the default protected-file set. Items are matched by basename (e.g. \"AGENTS.md\") or path prefix (e.g. \".agents/\"). Use this to allow the agent to modify specific files that are otherwise blocked by default.", - "examples": [["AGENTS.md"], ["AGENTS.md", ".agents/"]] + "examples": [ + [ + "AGENTS.md" + ], + [ + "AGENTS.md", + ".agents/" + ] + ] } }, "additionalProperties": false, @@ -8025,7 +9094,10 @@ "oneOf": [ { "type": "string", - "enum": ["am", "bundle"], + "enum": [ + "am", + "bundle" + ], "default": "bundle", "description": "Transport format for packaging changes. \"bundle\" (default) uses git bundle, which preserves merge commit topology, per-commit authorship, and merge-resolution-only content. \"am\" uses git format-patch/git am." }, @@ -8041,7 +9113,10 @@ "type": "boolean", "description": "When true, adds workflows: write to the GitHub App token permissions. Required when allowed-files targets .github/workflows/ paths. Requires safe-outputs.github-app to be configured because the workflows permission is a GitHub App-only permission and cannot be granted via GITHUB_TOKEN.", "default": false, - "examples": [true, false] + "examples": [ + true, + false + ] }, "check-branch-protection": { "type": "boolean", @@ -8088,7 +9163,14 @@ "description": "List of allowed reasons for hiding comments. Default: all reasons allowed (spam, abuse, off_topic, outdated, resolved, low_quality).", "items": { "type": "string", - "enum": ["spam", "abuse", "off_topic", "outdated", "resolved", "low_quality"] + "enum": [ + "spam", + "abuse", + "off_topic", + "outdated", + "resolved", + "low_quality" + ] } }, "discussions": { @@ -8098,7 +9180,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -8172,7 +9257,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -8244,7 +9332,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "required-labels": { "type": "array", @@ -8310,10 +9401,15 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, - "required": ["workflows"], + "required": [ + "workflows" + ], "additionalProperties": false }, { @@ -8372,7 +9468,13 @@ "properties": { "type": { "type": "string", - "enum": ["string", "number", "boolean", "choice", "environment"], + "enum": [ + "string", + "number", + "boolean", + "choice", + "environment" + ], "description": "Input type" }, "description": { @@ -8419,10 +9521,16 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, - "required": ["workflow", "event_type"], + "required": [ + "workflow", + "event_type" + ], "additionalProperties": false } }, @@ -8465,10 +9573,15 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, - "required": ["workflows"], + "required": [ + "workflows" + ], "additionalProperties": false }, { @@ -8529,7 +9642,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -8591,7 +9707,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -8641,7 +9760,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -8706,7 +9828,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -8798,7 +9923,10 @@ "if-no-files": { "type": "string", "description": "Behaviour when no files match: 'error' (default) or 'ignore'", - "enum": ["error", "ignore"], + "enum": [ + "error", + "ignore" + ], "default": "error" } }, @@ -8811,7 +9939,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub Actions artifact uploads (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -8858,7 +9989,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -8873,7 +10007,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] }, "env": { "type": "object", @@ -8889,7 +10026,11 @@ "github-token": { "$ref": "#/$defs/github_token", "description": "GitHub token to use for safe output jobs. Typically a secret reference like ${{ secrets.GITHUB_TOKEN }} or ${{ secrets.CUSTOM_PAT }}", - "examples": ["${{ secrets.GITHUB_TOKEN }}", "${{ secrets.CUSTOM_PAT }}", "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}"] + "examples": [ + "${{ secrets.GITHUB_TOKEN }}", + "${{ secrets.CUSTOM_PAT }}", + "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" + ] }, "github-app": { "$ref": "#/$defs/github_app", @@ -9075,7 +10216,13 @@ }, "type": { "type": "string", - "enum": ["string", "boolean", "choice", "number", "environment"], + "enum": [ + "string", + "boolean", + "choice", + "number", + "environment" + ], "description": "Input parameter type. Supports: string (default), boolean, choice (string with predefined options), number, and environment (string referencing a GitHub environment)", "default": "string" }, @@ -9171,7 +10318,11 @@ }, "type": { "type": "string", - "enum": ["string", "boolean", "number"], + "enum": [ + "string", + "boolean", + "number" + ], "description": "Input parameter type", "default": "string" } @@ -9186,7 +10337,9 @@ "description": "JavaScript handler body. Write only the code that runs inside the handler for each item \u2014 the compiler generates the full outer wrapper including config input destructuring (`const { channel, message } = config;`) and the handler function (`return async function handleX(item, resolvedTemporaryIds) { ... }`). The body has access to `item` (runtime message with input values), `resolvedTemporaryIds` (map of temporary IDs), and config-destructured local variables for each declared input." } }, - "required": ["script"], + "required": [ + "script" + ], "additionalProperties": false } }, @@ -9199,82 +10352,129 @@ "footer": { "type": "string", "description": "Custom footer message template for AI-generated content. Available placeholders: {workflow_name}, {run_url}, {triggering_number}, {workflow_source}, {workflow_source_url}. Example: '> Generated by [{workflow_name}]({run_url})'", - "examples": ["> Generated by [{workflow_name}]({run_url})", "> AI output from [{workflow_name}]({run_url}) for #{triggering_number}"] + "examples": [ + "> Generated by [{workflow_name}]({run_url})", + "> AI output from [{workflow_name}]({run_url}) for #{triggering_number}" + ] }, "footer-install": { "type": "string", "description": "Custom installation instructions template appended to the footer. Available placeholders: {workflow_source}, {workflow_source_url}. Example: '> Install: `gh aw add {workflow_source}`'", - "examples": ["> Install: `gh aw add {workflow_source}`", "> [Add this workflow]({workflow_source_url})"] + "examples": [ + "> Install: `gh aw add {workflow_source}`", + "> [Add this workflow]({workflow_source_url})" + ] }, "footer-workflow-recompile": { "type": "string", "description": "Custom footer message template for workflow recompile issues. Available placeholders: {workflow_name}, {run_url}, {repository}. Example: '> Workflow sync report by [{workflow_name}]({run_url}) for {repository}'", - "examples": ["> Workflow sync report by [{workflow_name}]({run_url}) for {repository}", "> Maintenance report by [{workflow_name}]({run_url})"] + "examples": [ + "> Workflow sync report by [{workflow_name}]({run_url}) for {repository}", + "> Maintenance report by [{workflow_name}]({run_url})" + ] }, "footer-workflow-recompile-comment": { "type": "string", "description": "Custom footer message template for comments on workflow recompile issues. Available placeholders: {workflow_name}, {run_url}, {repository}. Example: '> Update from [{workflow_name}]({run_url}) for {repository}'", - "examples": ["> Update from [{workflow_name}]({run_url}) for {repository}", "> Maintenance update by [{workflow_name}]({run_url})"] + "examples": [ + "> Update from [{workflow_name}]({run_url}) for {repository}", + "> Maintenance update by [{workflow_name}]({run_url})" + ] }, "staged-title": { "type": "string", "description": "Custom title template for staged mode preview. Available placeholders: {operation}. Example: '\ud83c\udfad Preview: {operation}'", - "examples": ["\ud83c\udfad Preview: {operation}", "## Staged Mode: {operation}"] + "examples": [ + "\ud83c\udfad Preview: {operation}", + "## Staged Mode: {operation}" + ] }, "staged-description": { "type": "string", "description": "Custom description template for staged mode preview. Available placeholders: {operation}. Example: 'The following {operation} would occur if staged mode was disabled:'", - "examples": ["The following {operation} would occur if staged mode was disabled:"] + "examples": [ + "The following {operation} would occur if staged mode was disabled:" + ] }, "run-started": { "type": "string", "description": "Custom message template for workflow activation comment. Available placeholders: {workflow_name}, {run_url}, {event_type}. Default: 'Agentic [{workflow_name}]({run_url}) triggered by this {event_type}.'", - "examples": ["Agentic [{workflow_name}]({run_url}) triggered by this {event_type}.", "[{workflow_name}]({run_url}) started processing this {event_type}."] + "examples": [ + "Agentic [{workflow_name}]({run_url}) triggered by this {event_type}.", + "[{workflow_name}]({run_url}) started processing this {event_type}." + ] }, "run-success": { "type": "string", "description": "Custom message template for successful workflow completion. Available placeholders: {workflow_name}, {run_url}. Default: '\u2705 Agentic [{workflow_name}]({run_url}) completed successfully.'", - "examples": ["\u2705 Agentic [{workflow_name}]({run_url}) completed successfully.", "\u2705 [{workflow_name}]({run_url}) finished."] + "examples": [ + "\u2705 Agentic [{workflow_name}]({run_url}) completed successfully.", + "\u2705 [{workflow_name}]({run_url}) finished." + ] }, "run-failure": { "type": "string", "description": "Custom message template for failed workflow. Available placeholders: {workflow_name}, {run_url}, {status}. Default: '\u274c Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.'", - "examples": ["\u274c Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.", "\u274c [{workflow_name}]({run_url}) {status}."] + "examples": [ + "\u274c Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.", + "\u274c [{workflow_name}]({run_url}) {status}." + ] }, "detection-failure": { "type": "string", "description": "Custom message template for detection job failure. Available placeholders: {workflow_name}, {run_url}. Default: '\u26a0\ufe0f Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.'", - "examples": ["\u26a0\ufe0f Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.", "\u26a0\ufe0f Detection job failed in [{workflow_name}]({run_url})."] + "examples": [ + "\u26a0\ufe0f Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.", + "\u26a0\ufe0f Detection job failed in [{workflow_name}]({run_url})." + ] }, "agent-failure-issue": { "type": "string", "description": "Custom footer template for agent failure tracking issues. Available placeholders: {workflow_name}, {run_url}. Default: '> Agent failure tracked by [{workflow_name}]({run_url})'", - "examples": ["> Agent failure tracked by [{workflow_name}]({run_url})", "> Failure report from [{workflow_name}]({run_url})"] + "examples": [ + "> Agent failure tracked by [{workflow_name}]({run_url})", + "> Failure report from [{workflow_name}]({run_url})" + ] }, "agent-failure-comment": { "type": "string", "description": "Custom footer template for comments on agent failure tracking issues. Available placeholders: {workflow_name}, {run_url}. Default: '> Agent failure update from [{workflow_name}]({run_url})'", - "examples": ["> Agent failure update from [{workflow_name}]({run_url})", "> Update from [{workflow_name}]({run_url})"] + "examples": [ + "> Agent failure update from [{workflow_name}]({run_url})", + "> Update from [{workflow_name}]({run_url})" + ] }, "pull-request-created": { "type": "string", "description": "Custom message template for pull request creation link appended to the activation comment. Available placeholders: {item_number}, {item_url}. Default: 'Pull request created: [#{item_number}]({item_url})'", - "examples": ["Pull request created: [#{item_number}]({item_url})", "[#{item_number}]({item_url}) opened"] + "examples": [ + "Pull request created: [#{item_number}]({item_url})", + "[#{item_number}]({item_url}) opened" + ] }, "issue-created": { "type": "string", "description": "Custom message template for issue creation link appended to the activation comment. Available placeholders: {item_number}, {item_url}. Default: 'Issue created: [#{item_number}]({item_url})'", - "examples": ["Issue created: [#{item_number}]({item_url})", "[#{item_number}]({item_url}) filed"] + "examples": [ + "Issue created: [#{item_number}]({item_url})", + "[#{item_number}]({item_url}) filed" + ] }, "commit-pushed": { "type": "string", "description": "Custom message template for commit push link appended to the activation comment. Available placeholders: {commit_sha}, {short_sha}, {commit_url}. Default: 'Commit pushed: [`{short_sha}`]({commit_url})'", - "examples": ["Commit pushed: [`{short_sha}`]({commit_url})", "[`{short_sha}`]({commit_url}) pushed"] + "examples": [ + "Commit pushed: [`{short_sha}`]({commit_url})", + "[`{short_sha}`]({commit_url}) pushed" + ] }, "body-header": { "type": "string", "description": "Custom header text prepended to every message body generated by safe outputs (issues, comments, pull requests, discussions). Applied after any threat-detection caution alert and before the agent-generated content. Available placeholders: {workflow_name}, {run_url}.", - "examples": ["> \u26a0\ufe0f This content was generated by [{workflow_name}]({run_url}).", "> \ud83e\udd16 AI-generated output \u2014 please review before acting."] + "examples": [ + "> \u26a0\ufe0f This content was generated by [{workflow_name}]({run_url}).", + "> \ud83e\udd16 AI-generated output \u2014 please review before acting." + ] }, "append-only-comments": { "type": "boolean", @@ -9337,31 +10537,50 @@ "type": "boolean", "description": "Global footer control for all safe outputs. When false, omits visible AI-generated footer content from all created/updated entities (issues, PRs, discussions, releases) while still including XML markers for searchability. Individual safe-output types (create-issue, update-issue, etc.) can override this by specifying their own footer field. Defaults to true.", "default": true, - "examples": [false, true] + "examples": [ + false, + true + ] }, "activation-comments": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "When set to false or \"false\", disables all activation and fallback comments entirely (run-started, run-success, run-failure, PR/issue creation links). Supports templatable boolean values including GitHub Actions expressions (e.g. ${{ inputs.activation-comments }}). Default: true", "default": true, - "examples": [false, true, "${{ inputs.activation-comments }}"] + "examples": [ + false, + true, + "${{ inputs.activation-comments }}" + ] }, "group-reports": { "type": "boolean", "description": "When true, creates a parent '[aw] Failed runs' issue that tracks all workflow failures as sub-issues. Helps organize failure tracking but may be unnecessary in smaller repositories. Defaults to false.", "default": false, - "examples": [false, true] + "examples": [ + false, + true + ] }, "report-failure-as-issue": { "type": "boolean", "description": "When false, disables creating failure tracking issues when workflows fail. Useful for workflows where failures are expected or handled elsewhere. Defaults to true.", "default": true, - "examples": [false, true] + "examples": [ + false, + true + ] }, "failure-issue-repo": { "type": "string", "description": "Repository to create failure tracking issues in, in the format 'owner/repo'. Useful when the current repository has issues disabled. Defaults to the current repository.", "pattern": "^[^/]+/[^/]+$", - "examples": ["github/docs-engineering", "myorg/infra-alerts"] + "examples": [ + "github/docs-engineering", + "myorg/infra-alerts" + ] }, "max-bot-mentions": { "description": "Maximum number of bot trigger references (e.g. 'fixes #123', 'closes #456') allowed in output before all of them are neutralized. Default: 10. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max-bot-mentions }}').", @@ -9380,14 +10599,23 @@ }, "id-token": { "type": "string", - "enum": ["write", "none"], + "enum": [ + "write", + "none" + ], "description": "Override the id-token permission for the safe-outputs job. Use 'write' to force-enable the id-token: write permission (required for OIDC authentication with cloud providers). Use 'none' to suppress automatic detection and prevent adding id-token: write even when vault/OIDC actions are detected in steps. By default, the compiler auto-detects known OIDC/vault actions (aws-actions/configure-aws-credentials, azure/login, google-github-actions/auth, hashicorp/vault-action, cyberark/conjur-action) and adds id-token: write automatically.", - "examples": ["write", "none"] + "examples": [ + "write", + "none" + ] }, "concurrency-group": { "type": "string", "description": "Concurrency group for the safe-outputs job. When set, the safe-outputs job will use this concurrency group with cancel-in-progress: false. Supports GitHub Actions expressions.", - "examples": ["my-workflow-safe-outputs", "safe-outputs-${{ github.repository }}"] + "examples": [ + "my-workflow-safe-outputs", + "safe-outputs-${{ github.repository }}" + ] }, "needs": { "type": "array", @@ -9399,7 +10627,11 @@ "additionalItems": false, "uniqueItems": true, "default": [], - "examples": [["secrets_fetcher"]] + "examples": [ + [ + "secrets_fetcher" + ] + ] }, "environment": { "description": "Override the GitHub deployment environment for the safe-outputs job. When set, this environment is used instead of the top-level environment: field. When not set, the top-level environment: field is propagated automatically so that environment-scoped secrets are accessible in the safe-outputs job.", @@ -9421,7 +10653,9 @@ "description": "A deployment URL" } }, - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false } ] @@ -9456,7 +10690,11 @@ "uses": { "type": "string", "description": "The GitHub Action to use. Supports owner/repo@ref, owner/repo/subdir@ref, or ./local/path.", - "examples": ["actions-ecosystem/action-add-labels@v1", "owner/repo@v1", "owner/repo/subdir@v1"] + "examples": [ + "actions-ecosystem/action-add-labels@v1", + "owner/repo@v1", + "owner/repo/subdir@v1" + ] }, "description": { "type": "string", @@ -9501,7 +10739,9 @@ "additionalProperties": false } }, - "required": ["uses"], + "required": [ + "uses" + ], "additionalProperties": false } } @@ -9551,7 +10791,10 @@ "staged": { "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "examples": [true, false] + "examples": [ + true, + false + ] } }, "additionalProperties": false @@ -9611,7 +10854,9 @@ { "type": "object", "description": "A single OTLP endpoint with a URL and optional per-endpoint headers.", - "required": ["url"], + "required": [ + "url" + ], "properties": { "url": { "type": "string", @@ -9641,7 +10886,9 @@ "items": { "type": "object", "description": "A single OTLP endpoint with a URL and optional per-endpoint headers.", - "required": ["url"], + "required": [ + "url" + ], "properties": { "url": { "type": "string", @@ -9686,7 +10933,11 @@ }, "if-missing": { "type": "string", - "enum": ["error", "warn", "ignore"], + "enum": [ + "error", + "warn", + "ignore" + ], "default": "error", "description": "How to handle missing OTLP endpoint/header values at runtime (for example from unset secrets). 'error' fails workflow startup (default), 'warn' logs a warning and skips MCP gateway OTLP configuration, and 'ignore' skips MCP gateway OTLP configuration without warning. This affects MCP gateway setup only; workflow-level OTEL_* environment variables are still injected." }, @@ -9722,7 +10973,9 @@ "user-rate-limit": { "type": "object", "description": "Rate limiting configuration to restrict how frequently users can trigger the workflow. Helps prevent abuse and resource exhaustion from programmatically triggered events.", - "required": ["max-runs-per-window"], + "required": [ + "max-runs-per-window" + ], "properties": { "max-runs-per-window": { "description": "Maximum number of workflow runs allowed per user within the time window. Required field. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').", @@ -9751,7 +11004,16 @@ "description": "Optional list of event types to apply rate limiting to. If not specified, rate limiting applies to all programmatically triggered events (e.g., workflow_dispatch, issue_comment, pull_request_review).", "items": { "type": "string", - "enum": ["workflow_dispatch", "issue_comment", "pull_request_review", "pull_request_review_comment", "issues", "pull_request", "discussion_comment", "discussion"] + "enum": [ + "workflow_dispatch", + "issue_comment", + "pull_request_review", + "pull_request_review_comment", + "issues", + "pull_request", + "discussion_comment", + "discussion" + ] }, "minItems": 1 }, @@ -9760,7 +11022,13 @@ "description": "Optional list of roles that are exempt from rate limiting. Defaults to ['admin', 'maintain', 'write'] if not specified. Users with any of these roles will not be subject to rate limiting checks. To apply rate limiting to all users, set to an empty array: []", "items": { "type": "string", - "enum": ["admin", "maintain", "write", "triage", "read"] + "enum": [ + "admin", + "maintain", + "write", + "triage", + "read" + ] }, "minItems": 0 } @@ -9774,12 +11042,18 @@ { "max-runs-per-window": 10, "window": 30, - "events": ["workflow_dispatch", "issue_comment"] + "events": [ + "workflow_dispatch", + "issue_comment" + ] }, { "max-runs-per-window": 5, "window": 60, - "ignored-roles": ["admin", "maintain"] + "ignored-roles": [ + "admin", + "maintain" + ] } ] }, @@ -9829,7 +11103,16 @@ "description": "Optional list of event types to apply rate limiting to.", "items": { "type": "string", - "enum": ["workflow_dispatch", "issue_comment", "pull_request_review", "pull_request_review_comment", "issues", "pull_request", "discussion_comment", "discussion"] + "enum": [ + "workflow_dispatch", + "issue_comment", + "pull_request_review", + "pull_request_review_comment", + "issues", + "pull_request", + "discussion_comment", + "discussion" + ] }, "minItems": 1 }, @@ -9838,7 +11121,13 @@ "description": "Optional list of roles that are exempt from rate limiting.", "items": { "type": "string", - "enum": ["admin", "maintain", "write", "triage", "read"] + "enum": [ + "admin", + "maintain", + "write", + "triage", + "read" + ] }, "minItems": 0 } @@ -9850,25 +11139,37 @@ "default": true, "$comment": "Strict mode enforces several security constraints that are validated in Go code (pkg/workflow/strict_mode_validation.go) rather than JSON Schema: (1) Write Permissions + Safe Outputs: When strict=true AND permissions contains write values (contents:write, issues:write, pull-requests:write), safe-outputs must be configured. This relationship is too complex for JSON Schema as it requires checking if ANY permission property has a 'write' value. (2) Network Requirements: When strict=true, the 'network' field must be present and cannot contain standalone wildcard '*' (but patterns like '*.example.com' ARE allowed). (3) MCP Container Network: Custom MCP servers with containers require explicit network configuration. (4) Action Pinning: Actions must be pinned to commit SHAs. These are enforced during compilation via validateStrictMode().", "description": "Enable strict mode validation for enhanced security and compliance. Strict mode enforces: (1) Write Permissions - refuses contents:write, issues:write, pull-requests:write; requires safe-outputs instead, (2) Network Configuration - requires explicit network configuration with no standalone wildcard '*' in allowed domains (patterns like '*.example.com' are allowed), (3) Action Pinning - enforces actions pinned to commit SHAs instead of tags/branches, (4) MCP Network - requires network configuration for custom MCP servers with containers, (5) Deprecated Fields - refuses deprecated frontmatter fields. Can be enabled per-workflow via 'strict: true' in frontmatter, or disabled via 'strict: false'. CLI flag takes precedence over frontmatter (gh aw compile --strict enforces strict mode). Defaults to true. See: https://github.github.com/gh-aw/reference/frontmatter/#strict-mode-strict", - "examples": [true, false] + "examples": [ + true, + false + ] }, "private": { "type": "boolean", "default": false, "description": "Mark the workflow as private, preventing it from being added to other repositories via 'gh aw add'. A workflow with private: true is not meant to be shared outside its repository.", - "examples": [true, false] + "examples": [ + true, + false + ] }, "check-for-updates": { "type": "boolean", "default": true, "description": "Control whether the compile-agentic version update check runs in the activation job. When true (default), the activation job downloads config.json from the gh-aw repository and verifies the compiled version is not blocked and meets the minimum supported version. Set to false to disable the check (not allowed in strict mode). See: https://github.github.com/gh-aw/reference/frontmatter/#check-for-updates", - "examples": [true, false] + "examples": [ + true, + false + ] }, "run-install-scripts": { "type": "boolean", "default": false, "description": "Allow npm pre/post install scripts to execute during package installation. By default, --ignore-scripts is added to all generated npm install commands to prevent supply chain attacks via malicious install hooks. Setting run-install-scripts: true disables this protection globally (all runtimes). A supply chain security warning is emitted at compile time; in strict mode this is an error. Per-runtime control is also available via runtimes..run-install-scripts. See: https://github.github.com/gh-aw/reference/frontmatter/#run-install-scripts", - "examples": [false, true] + "examples": [ + false, + true + ] }, "mcp-scripts": { "type": "object", @@ -9877,7 +11178,9 @@ "^([a-ln-z][a-z0-9_-]*|m[a-np-z][a-z0-9_-]*|mo[a-ce-z][a-z0-9_-]*|mod[a-df-z][a-z0-9_-]*|mode[a-z0-9_-]+)$": { "type": "object", "description": "Custom tool definition. The key is the tool name (lowercase alphanumeric with dashes/underscores).", - "required": ["description"], + "required": [ + "description" + ], "properties": { "description": { "type": "string", @@ -9891,7 +11194,13 @@ "properties": { "type": { "type": "string", - "enum": ["string", "number", "boolean", "array", "object"], + "enum": [ + "string", + "number", + "boolean", + "array", + "object" + ], "default": "string", "description": "The JSON schema type of the input parameter." }, @@ -9945,71 +11254,108 @@ "description": "Timeout in seconds for tool execution. Default is 60 seconds. Applies to shell (run) and Python (py) tools.", "default": 60, "minimum": 1, - "examples": [30, 60, 120, 300] + "examples": [ + 30, + 60, + 120, + 300 + ] } }, "additionalProperties": false, "oneOf": [ { - "required": ["script"], + "required": [ + "script" + ], "not": { "anyOf": [ { - "required": ["run"] + "required": [ + "run" + ] }, { - "required": ["py"] + "required": [ + "py" + ] }, { - "required": ["go"] + "required": [ + "go" + ] } ] } }, { - "required": ["run"], + "required": [ + "run" + ], "not": { "anyOf": [ { - "required": ["script"] + "required": [ + "script" + ] }, { - "required": ["py"] + "required": [ + "py" + ] }, { - "required": ["go"] + "required": [ + "go" + ] } ] } }, { - "required": ["py"], + "required": [ + "py" + ], "not": { "anyOf": [ { - "required": ["script"] + "required": [ + "script" + ] }, { - "required": ["run"] + "required": [ + "run" + ] }, { - "required": ["go"] + "required": [ + "go" + ] } ] } }, { - "required": ["go"], + "required": [ + "go" + ], "not": { "anyOf": [ { - "required": ["script"] + "required": [ + "script" + ] }, { - "required": ["run"] + "required": [ + "run" + ] }, { - "required": ["py"] + "required": [ + "py" + ] } ] } @@ -10067,9 +11413,18 @@ "description": "Runtime configuration object identified by runtime ID (e.g., 'node', 'python', 'go')", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Runtime version as a string (e.g., '22', '3.12', 'latest') or number (e.g., 22, 3.12). Numeric values are automatically converted to strings at runtime.", - "examples": ["22", "3.12", "latest", 22, 3.12] + "examples": [ + "22", + "3.12", + "latest", + 22, + 3.12 + ] }, "action-repo": { "type": "string", @@ -10082,19 +11437,31 @@ "if": { "type": "string", "description": "Optional GitHub Actions if condition to control when the runtime setup step runs. Supports standard GitHub Actions expression syntax. Useful for conditionally installing runtimes based on file presence (e.g., \"hashFiles('go.mod') != ''\" to install Go only when go.mod exists).", - "examples": ["hashFiles('go.mod') != ''", "hashFiles('package.json') != ''", "hashFiles('requirements.txt') != '' || hashFiles('pyproject.toml') != ''", "hashFiles('uv.lock') != ''", "github.event_name == 'workflow_dispatch'"] + "examples": [ + "hashFiles('go.mod') != ''", + "hashFiles('package.json') != ''", + "hashFiles('requirements.txt') != '' || hashFiles('pyproject.toml') != ''", + "hashFiles('uv.lock') != ''", + "github.event_name == 'workflow_dispatch'" + ] }, "cooldown": { "type": "boolean", "default": true, "description": "Enable a default 3-day dependency cooldown for installs associated with this runtime. Set to false to disable.", - "examples": [true, false] + "examples": [ + true, + false + ] }, "run-install-scripts": { "type": "boolean", "default": false, "description": "Allow npm pre/post install scripts to execute for this runtime during package installation. Overrides the global run-install-scripts setting for this specific runtime. Only affects runtimes that generate npm install commands (node). A supply chain security warning is emitted at compile time; in strict mode this is an error.", - "examples": [false, true] + "examples": [ + false, + true + ] } }, "additionalProperties": false @@ -10118,7 +11485,9 @@ }, { "type": "boolean", - "enum": [false], + "enum": [ + false + ], "description": "Set to false to disable the default checkout step. The agent job will not check out any repository (dev-mode checkouts are unaffected)." } ] @@ -10150,7 +11519,13 @@ }, "type": { "type": "string", - "enum": ["string", "number", "boolean", "choice", "array"], + "enum": [ + "string", + "number", + "boolean", + "choice", + "array" + ], "description": "The type of the input value." }, "options": { @@ -10166,7 +11541,11 @@ "properties": { "type": { "type": "string", - "enum": ["string", "number", "boolean"], + "enum": [ + "string", + "number", + "boolean" + ], "description": "Type of each array item." } }, @@ -10178,7 +11557,9 @@ { "type": "object", "description": "Input parameter definition for object type (one level deep). Use 'properties' to declare the expected sub-fields.", - "required": ["type"], + "required": [ + "type" + ], "properties": { "description": { "type": "string", @@ -10191,7 +11572,9 @@ }, "type": { "type": "string", - "enum": ["object"], + "enum": [ + "object" + ], "description": "The type 'object' enables structured sub-fields accessible via 'github.aw.import-inputs..'." }, "properties": { @@ -10211,7 +11594,12 @@ "default": {}, "type": { "type": "string", - "enum": ["string", "number", "boolean", "choice"], + "enum": [ + "string", + "number", + "boolean", + "choice" + ], "description": "Type of the sub-property." }, "options": { @@ -10263,6 +11651,33 @@ } } ] + }, + "awf": { + "type": "object", + "description": "AWF (Agent Workflow Firewall) declarative configuration.", + "properties": { + "apiProxy": { + "type": "object", + "description": "API proxy configuration for LLM provider routing.", + "properties": { + "targets": { + "type": "object", + "description": "Per-provider API proxy target overrides.", + "properties": { + "openai": { + "$ref": "#/$defs/awf_provider_target" + }, + "anthropic": { + "$ref": "#/$defs/awf_provider_target" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false, @@ -10290,13 +11705,17 @@ "const": "centralized" } }, - "required": ["strategy"] + "required": [ + "strategy" + ] } } ] } }, - "required": ["slash_command"] + "required": [ + "slash_command" + ] }, { "properties": { @@ -10306,7 +11725,9 @@ } } }, - "required": ["command"] + "required": [ + "command" + ] } ] } @@ -10325,7 +11746,9 @@ } } }, - "required": ["issue_comment"] + "required": [ + "issue_comment" + ] }, { "properties": { @@ -10335,7 +11758,9 @@ } } }, - "required": ["pull_request_review_comment"] + "required": [ + "pull_request_review_comment" + ] }, { "properties": { @@ -10345,7 +11770,9 @@ } } }, - "required": ["label"] + "required": [ + "label" + ] } ] } @@ -10552,9 +11979,18 @@ "description": "AI engine identifier: built-in ('claude', 'codex', 'copilot', 'gemini', 'opencode', 'crush', 'pi') or a named catalog entry" }, "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Optional version of the AI engine action (e.g., 'beta', 'stable', 20). Has sensible defaults and can typically be omitted. Numeric values are automatically converted to strings at runtime. GitHub Actions expressions (e.g., '${{ inputs.engine-version }}') are accepted and compiled with injection-safe env var handling.", - "examples": ["beta", "stable", 20, 3.11, "${{ inputs.engine-version }}"] + "examples": [ + "beta", + "stable", + 20, + 3.11, + "${{ inputs.engine-version }}" + ] }, "model": { "type": "string", @@ -10562,7 +11998,12 @@ }, "permission-mode": { "type": "string", - "enum": ["auto", "acceptEdits", "plan", "bypassPermissions"], + "enum": [ + "auto", + "acceptEdits", + "plan", + "bypassPermissions" + ], "description": "Claude permission mode override. Defaults to acceptEdits (or auto when tools.edit is false)." }, "max-turns": { @@ -10603,11 +12044,16 @@ }, "queue": { "type": "string", - "enum": ["single", "max"], + "enum": [ + "single", + "max" + ], "description": "Pending run queue behavior for this concurrency group. 'single' (default) allows one pending run and replaces older pending runs. 'max' allows up to 100 pending runs in FIFO order." } }, - "required": ["group"], + "required": [ + "group" + ], "additionalProperties": false } ], @@ -10639,7 +12085,9 @@ "properties": { "type": { "type": "string", - "enum": ["github-oidc"], + "enum": [ + "github-oidc" + ], "description": "Authentication type. Currently only 'github-oidc' is supported." }, "audience": { @@ -10664,7 +12112,9 @@ "description": "Optional Azure cloud name (for example, public, usgovernment, china)." } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false }, "config": { @@ -10678,7 +12128,11 @@ "api-target": { "type": "string", "description": "Custom API endpoint hostname for the agentic engine. Used for GitHub Enterprise Cloud (GHEC), GitHub Enterprise Server (GHES), or custom AI endpoints. Example: 'api.acme.ghe.com' for GHEC, 'api.enterprise.githubcopilot.com' for GHES, or custom endpoint hostnames.", - "examples": ["api.acme.ghe.com", "api.enterprise.githubcopilot.com", "api.custom.endpoint.com"] + "examples": [ + "api.acme.ghe.com", + "api.enterprise.githubcopilot.com", + "api.custom.endpoint.com" + ] }, "token-weights": { "type": "object", @@ -10752,18 +12206,31 @@ "session-timeout": { "type": "string", "description": "Session timeout for MCP gateway sessions as a Go duration string (e.g. \"30m\", \"4h\", \"24h\"). Must be at least 5m (no upper bound). Omitted or empty uses the effective gateway default (precedence: this field > MCP_GATEWAY_SESSION_TIMEOUT env var > built-in default 6h). Longer timeouts benefit multi-hour workflows such as large-scale migrations; shorter values free gateway resources sooner.", - "examples": ["30m", "1h", "4h", "6h", "12h"] + "examples": [ + "30m", + "1h", + "4h", + "6h", + "12h" + ] }, "tool-timeout": { "type": "string", "description": "Timeout for individual MCP tool calls as a Go duration string (e.g. \"30s\", \"2m\", \"10m\"). Must be between 10s and 600s inclusive. Omitted or empty uses the gateway built-in default (60s). Use a higher value for slow MCP backends such as full-text search over large indexes.", - "examples": ["30s", "2m", "5m", "10m"] + "examples": [ + "30s", + "2m", + "5m", + "10m" + ] } }, "additionalProperties": false } }, - "required": ["id"], + "required": [ + "id" + ], "additionalProperties": false }, { @@ -10777,15 +12244,32 @@ "id": { "type": "string", "description": "Runtime adapter identifier (e.g. 'codex', 'claude', 'copilot', 'gemini', 'opencode', 'crush', 'pi')", - "examples": ["codex", "claude", "copilot", "gemini", "opencode", "crush", "pi"] + "examples": [ + "codex", + "claude", + "copilot", + "gemini", + "opencode", + "crush", + "pi" + ] }, "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Optional version of the runtime adapter (e.g. '0.105.0', 'beta')", - "examples": ["0.105.0", "beta", "latest"] + "examples": [ + "0.105.0", + "beta", + "latest" + ] } }, - "required": ["id"], + "required": [ + "id" + ], "additionalProperties": false }, "provider": { @@ -10795,12 +12279,21 @@ "id": { "type": "string", "description": "Provider identifier (e.g. 'openai', 'anthropic', 'github', 'google')", - "examples": ["openai", "anthropic", "github", "google"] + "examples": [ + "openai", + "anthropic", + "github", + "google" + ] }, "model": { "type": "string", "description": "Optional specific LLM model to use (e.g. 'gpt-5', 'claude-3-5-sonnet-20241022')", - "examples": ["gpt-5", "claude-3-5-sonnet-20241022", "gpt-4o"] + "examples": [ + "gpt-5", + "claude-3-5-sonnet-20241022", + "gpt-4o" + ] }, "auth": { "type": "object", @@ -10809,37 +12302,58 @@ "secret": { "type": "string", "description": "Name of the GitHub Actions secret that contains the API key for this provider", - "examples": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "CUSTOM_API_KEY"] + "examples": [ + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", + "CUSTOM_API_KEY" + ] }, "strategy": { "type": "string", - "enum": ["api-key", "oauth-client-credentials", "bearer"], + "enum": [ + "api-key", + "oauth-client-credentials", + "bearer" + ], "description": "Authentication strategy for the provider (default: api-key when secret is set)" }, "token-url": { "type": "string", "description": "OAuth 2.0 token endpoint URL. Required when strategy is 'oauth-client-credentials'.", - "examples": ["https://auth.example.com/oauth/token"] + "examples": [ + "https://auth.example.com/oauth/token" + ] }, "client-id": { "type": "string", "description": "GitHub Actions secret name that holds the OAuth client ID. Required when strategy is 'oauth-client-credentials'.", - "examples": ["OAUTH_CLIENT_ID"] + "examples": [ + "OAUTH_CLIENT_ID" + ] }, "client-secret": { "type": "string", "description": "GitHub Actions secret name that holds the OAuth client secret. Required when strategy is 'oauth-client-credentials'.", - "examples": ["OAUTH_CLIENT_SECRET"] + "examples": [ + "OAUTH_CLIENT_SECRET" + ] }, "token-field": { "type": "string", "description": "JSON field name in the token response that contains the access token. Defaults to 'access_token'.", - "examples": ["access_token", "token"] + "examples": [ + "access_token", + "token" + ] }, "header-name": { "type": "string", "description": "HTTP header name to inject the API key or token into (e.g. 'api-key', 'x-api-key'). Required when strategy is not 'bearer'.", - "examples": ["api-key", "x-api-key", "Authorization"] + "examples": [ + "api-key", + "x-api-key", + "Authorization" + ] } }, "additionalProperties": false @@ -10851,7 +12365,9 @@ "path-template": { "type": "string", "description": "URL path template with {model} and other variable placeholders (e.g. '/openai/deployments/{model}/chat/completions')", - "examples": ["/openai/deployments/{model}/chat/completions"] + "examples": [ + "/openai/deployments/{model}/chat/completions" + ] }, "query": { "type": "object", @@ -10889,7 +12405,9 @@ "default": false } }, - "required": ["runtime"], + "required": [ + "runtime" + ], "additionalProperties": false }, { @@ -10930,7 +12448,11 @@ }, "strategy": { "type": "string", - "enum": ["api-key", "oauth-client-credentials", "bearer"], + "enum": [ + "api-key", + "oauth-client-credentials", + "bearer" + ], "description": "Authentication strategy" }, "token-url": { @@ -11017,7 +12539,10 @@ "description": "Name of the GitHub Actions secret that provides credentials for this role" } }, - "required": ["role", "secret"], + "required": [ + "role", + "secret" + ], "additionalProperties": false } }, @@ -11027,7 +12552,10 @@ "additionalProperties": true } }, - "required": ["id", "display-name"], + "required": [ + "id", + "display-name" + ], "additionalProperties": false }, { @@ -11041,18 +12569,31 @@ "session-timeout": { "type": "string", "description": "Session timeout for MCP gateway sessions as a Go duration string (e.g. \"30m\", \"4h\", \"24h\"). Must be at least 5m (no upper bound). Omitted or empty uses the effective gateway default (precedence: this field > MCP_GATEWAY_SESSION_TIMEOUT env var > built-in default 6h).", - "examples": ["30m", "1h", "4h", "6h", "12h"] + "examples": [ + "30m", + "1h", + "4h", + "6h", + "12h" + ] }, "tool-timeout": { "type": "string", "description": "Timeout for individual MCP tool calls as a Go duration string (e.g. \"30s\", \"2m\", \"10m\"). Must be between 10s and 600s inclusive. Omitted or empty uses the gateway built-in default (60s). Use a higher value for slow MCP backends such as full-text search over large indexes.", - "examples": ["30s", "2m", "5m", "10m"] + "examples": [ + "30s", + "2m", + "5m", + "10m" + ] } }, "additionalProperties": false } }, - "required": ["mcp"], + "required": [ + "mcp" + ], "additionalProperties": false }, { @@ -11064,7 +12605,9 @@ "description": "Model preference or size category (e.g. 'small', 'large', 'gpt-4.1'). Applied to the default engine when engine.id is not specified." } }, - "required": ["model"], + "required": [ + "model" + ], "additionalProperties": false } ] @@ -11075,13 +12618,18 @@ "properties": { "type": { "type": "string", - "enum": ["stdio", "local"], + "enum": [ + "stdio", + "local" + ], "description": "MCP connection type for stdio (local is an alias for stdio)" }, "registry": { "type": "string", "description": "URI to the installation location when MCP is installed from a registry", - "examples": ["https://api.mcp.github.com/v0/servers/microsoft/markitdown"] + "examples": [ + "https://api.mcp.github.com/v0/servers/microsoft/markitdown" + ] }, "command": { "type": "string", @@ -11096,9 +12644,17 @@ "description": "Container image for stdio MCP connections" }, "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Optional version/tag for the container image (e.g., 'latest', 'v1.0.0', 20, 3.11). Numeric values are automatically converted to strings at runtime.", - "examples": ["latest", "v1.0.0", 20, 3.11] + "examples": [ + "latest", + "v1.0.0", + 20, + 3.11 + ] }, "args": { "type": "array", @@ -11110,7 +12666,11 @@ "entrypoint": { "type": "string", "description": "Optional entrypoint override for container (equivalent to docker run --entrypoint)", - "examples": ["/bin/sh", "/custom/entrypoint.sh", "python"] + "examples": [ + "/bin/sh", + "/custom/entrypoint.sh", + "python" + ] }, "entrypointArgs": { "type": "array", @@ -11126,7 +12686,15 @@ "pattern": "^[^:]+:[^:]+:(ro|rw)$" }, "description": "Volume mounts for container in format 'source:dest:mode' where mode is 'ro' or 'rw'", - "examples": [["/tmp/data:/data:ro"], ["/workspace:/workspace:rw", "/config:/config:ro"]] + "examples": [ + [ + "/tmp/data:/data:ro" + ], + [ + "/workspace:/workspace:rw", + "/config:/config:ro" + ] + ] }, "env": { "type": "object", @@ -11173,7 +12741,18 @@ "items": { "type": "string" }, - "examples": [["*"], ["store_memory", "retrieve_memory"], ["brave_web_search"]] + "examples": [ + [ + "*" + ], + [ + "store_memory", + "retrieve_memory" + ], + [ + "brave_web_search" + ] + ] }, "proxy-args": { "type": "array", @@ -11187,22 +12766,32 @@ "$comment": "Validation constraints: (1) Mutual exclusion: 'command' and 'container' cannot both be specified. (2) Requirement: Either 'command' or 'container' must be provided (via 'anyOf'). (3) Type constraint: When 'type' is 'stdio' or 'local', either 'command' or 'container' is required. Note: Per-server 'network' field is deprecated and ignored.", "anyOf": [ { - "required": ["type"] + "required": [ + "type" + ] }, { - "required": ["command"] + "required": [ + "command" + ] }, { - "required": ["container"] + "required": [ + "container" + ] } ], "not": { "allOf": [ { - "required": ["command"] + "required": [ + "command" + ] }, { - "required": ["container"] + "required": [ + "container" + ] } ] }, @@ -11211,17 +12800,24 @@ "if": { "properties": { "type": { - "enum": ["stdio", "local"] + "enum": [ + "stdio", + "local" + ] } } }, "then": { "anyOf": [ { - "required": ["command"] + "required": [ + "command" + ] }, { - "required": ["container"] + "required": [ + "container" + ] } ] } @@ -11234,13 +12830,17 @@ "properties": { "type": { "type": "string", - "enum": ["http"], + "enum": [ + "http" + ], "description": "MCP connection type for HTTP" }, "registry": { "type": "string", "description": "URI to the installation location when MCP is installed from a registry", - "examples": ["https://api.mcp.github.com/v0/servers/microsoft/markitdown"] + "examples": [ + "https://api.mcp.github.com/v0/servers/microsoft/markitdown" + ] }, "url": { "type": "string", @@ -11263,13 +12863,26 @@ "items": { "type": "string" }, - "examples": [["*"], ["store_memory", "retrieve_memory"], ["brave_web_search"]] + "examples": [ + [ + "*" + ], + [ + "store_memory", + "retrieve_memory" + ], + [ + "brave_web_search" + ] + ] }, "auth": { "$ref": "#/$defs/http_mcp_auth" } }, - "required": ["url"], + "required": [ + "url" + ], "additionalProperties": false }, "http_mcp_auth": { @@ -11278,7 +12891,9 @@ "properties": { "type": { "type": "string", - "enum": ["github-oidc"], + "enum": [ + "github-oidc" + ], "description": "Authentication type. Currently only 'github-oidc' is supported, which acquires short-lived JWTs from the GitHub Actions OIDC endpoint." }, "audience": { @@ -11287,14 +12902,21 @@ "format": "uri" } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false }, "github_token": { "type": "string", "pattern": "^\\$\\{\\{\\s*(secrets\\.[A-Za-z_][A-Za-z0-9_]*(\\s*\\|\\|\\s*secrets\\.[A-Za-z_][A-Za-z0-9_]*)*|needs\\.[A-Za-z_][A-Za-z0-9_]*\\.outputs\\.[A-Za-z_][A-Za-z0-9_]*)\\s*\\}\\}$", "description": "GitHub token expression. Accepts a secrets expression (e.g., `${{ secrets.NAME }}` or `${{ secrets.NAME1 || secrets.NAME2 }}`) or a job output expression (e.g., `${{ needs.auth.outputs.token }}`). Pattern details: secret names match `[A-Za-z_][A-Za-z0-9_]*`; job IDs and output names in dot notation match `[A-Za-z_][A-Za-z0-9_]*` (identifiers without hyphens).", - "examples": ["${{ secrets.GITHUB_TOKEN }}", "${{ secrets.CUSTOM_PAT }}", "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", "${{ needs.auth.outputs.token }}"] + "examples": [ + "${{ secrets.GITHUB_TOKEN }}", + "${{ secrets.CUSTOM_PAT }}", + "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", + "${{ needs.auth.outputs.token }}" + ] }, "github_app": { "type": "object", @@ -11303,17 +12925,23 @@ "app-id": { "type": "string", "description": "Deprecated alias for client-id. GitHub App ID/client ID (e.g., '${{ vars.APP_ID }}').", - "examples": ["${{ vars.APP_ID }}"] + "examples": [ + "${{ vars.APP_ID }}" + ] }, "client-id": { "type": "string", "description": "GitHub App client ID (e.g., '${{ vars.APP_ID }}'). Required to mint a GitHub App token.", - "examples": ["${{ vars.APP_ID }}"] + "examples": [ + "${{ vars.APP_ID }}" + ] }, "private-key": { "type": "string", "description": "GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). Required to mint a GitHub App token.", - "examples": ["${{ secrets.APP_PRIVATE_KEY }}"] + "examples": [ + "${{ secrets.APP_PRIVATE_KEY }}" + ] }, "ignore-if-missing": { "type": "boolean", @@ -11337,10 +12965,16 @@ }, "anyOf": [ { - "required": ["client-id", "private-key"] + "required": [ + "client-id", + "private-key" + ] }, { - "required": ["app-id", "private-key"] + "required": [ + "app-id", + "private-key" + ] } ], "additionalProperties": false, @@ -11455,10 +13089,14 @@ "additionalProperties": false, "anyOf": [ { - "required": ["uses"] + "required": [ + "uses" + ] }, { - "required": ["run"] + "required": [ + "run" + ] } ] }, @@ -11470,34 +13108,56 @@ "repository": { "type": "string", "description": "Repository to checkout in owner/repo format. Defaults to the current repository.", - "examples": ["owner/repo", "github/gh-aw"] + "examples": [ + "owner/repo", + "github/gh-aw" + ] }, "ref": { "type": "string", "description": "Branch, tag, or SHA to checkout. Defaults to the ref that triggered the workflow.", - "examples": ["main", "v1.0.0", "feature/my-branch"] + "examples": [ + "main", + "v1.0.0", + "feature/my-branch" + ] }, "path": { "type": "string", "description": "Relative path within GITHUB_WORKSPACE to place the checkout. Defaults to the workspace root.", - "examples": [".", "./libs/other-repo", "./workspace"] + "examples": [ + ".", + "./libs/other-repo", + "./workspace" + ] }, "fetch-depth": { "type": "integer", "minimum": 0, "description": "Number of commits to fetch. 0 fetches all history. 1 (default) is a shallow clone. When multiple configs target the same path, the deepest value is used.", - "examples": [0, 1, 10] + "examples": [ + 0, + 1, + 10 + ] }, "sparse-checkout": { "type": "string", "description": "Enable sparse-checkout with newline-separated patterns. When multiple configs target the same path, patterns are merged.", - "examples": [".github/\nsrc/", "docs/"] + "examples": [ + ".github/\nsrc/", + "docs/" + ] }, "submodules": { "oneOf": [ { "type": "string", - "enum": ["recursive", "true", "false"] + "enum": [ + "recursive", + "true", + "false" + ] }, { "type": "boolean" @@ -11512,12 +13172,18 @@ "token": { "type": "string", "description": "Deprecated: Use github-token instead. GitHub token for authentication. Credentials are always removed after checkout (persist-credentials: false is enforced).", - "examples": ["${{ secrets.MY_PAT }}", "${{ secrets.GITHUB_TOKEN }}"] + "examples": [ + "${{ secrets.MY_PAT }}", + "${{ secrets.GITHUB_TOKEN }}" + ] }, "github-token": { "type": "string", "description": "GitHub token for authentication. Use ${{ secrets.MY_TOKEN }} to reference a secret. Mutually exclusive with github-app (and deprecated app). Credentials are always removed after checkout (persist-credentials: false is enforced).", - "examples": ["${{ secrets.MY_PAT }}", "${{ secrets.CROSS_REPO_PAT }}"] + "examples": [ + "${{ secrets.MY_PAT }}", + "${{ secrets.CROSS_REPO_PAT }}" + ] }, "github-app": { "$ref": "#/$defs/github_app", @@ -11542,17 +13208,37 @@ } ], "description": "Additional Git refs to fetch after the checkout. Supported values: \"*\" (all branches), \"refs/pulls/open/*\" (all open pull-request refs), branch names (e.g. \"main\"), or glob patterns (e.g. \"feature/*\").", - "examples": [["*"], ["refs/pulls/open/*"], ["main", "feature/my-branch"], ["feature/*"]] + "examples": [ + [ + "*" + ], + [ + "refs/pulls/open/*" + ], + [ + "main", + "feature/my-branch" + ], + [ + "feature/*" + ] + ] }, "wiki": { "type": "boolean", "description": "When true, clones the repository's wiki git instead of the regular repository. The effective repository becomes \"{repository}.wiki\" (e.g. \"owner/repo.wiki\"). Defaults to false.", - "examples": [true, false] + "examples": [ + true, + false + ] }, "force-clean-git-credentials": { "type": "boolean", "description": "When true, persist credentials during checkout, then immediately run a post-checkout cleanup step that removes credentials from root and submodule git configs. Useful for submodule-safe cleanup behavior.", - "examples": [true, false] + "examples": [ + true, + false + ] } } }, @@ -11563,97 +13249,169 @@ "properties": { "actions": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission for GitHub Actions workflows and runs (read: view workflows, write: manage workflows, none: no access)" }, "attestations": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission for artifact attestations (read: view attestations, write: create attestations, none: no access)" }, "checks": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission for repository checks and status checks (read: view checks, write: create/update checks, none: no access)" }, "contents": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission for repository contents (read: view files, write: modify files/branches, none: no access)" }, "deployments": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission for repository deployments (read: view deployments, write: create/update deployments, none: no access)" }, "discussions": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission for repository discussions (read: view discussions, write: create/update discussions, none: no access)" }, "id-token": { "type": "string", - "enum": ["write", "none"], + "enum": [ + "write", + "none" + ], "description": "Permission level for OIDC token requests (write/none only - read is not supported). Allows workflows to request JWT tokens for cloud provider authentication." }, "issues": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission for repository issues (read: view issues, write: create/update/close issues, none: no access)" }, "models": { "type": "string", - "enum": ["read", "none"], + "enum": [ + "read", + "none" + ], "description": "Permission for GitHub Copilot models (read: access AI models for agentic workflows, none: no access)" }, "metadata": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission for repository metadata (read: view repository information, write: update repository metadata, none: no access)" }, "packages": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission level for GitHub Packages (read/write/none). Controls access to publish, modify, or delete packages." }, "pages": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission level for GitHub Pages (read/write/none). Controls access to deploy and manage GitHub Pages sites." }, "pull-requests": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission level for pull requests (read/write/none). Controls access to create, edit, review, and manage pull requests." }, "repository-projects": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission level for repository projects (read/write/none). Controls access to manage repository-level GitHub Projects boards." }, "organization-projects": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission level for organization projects (read/write/none). Controls access to manage organization-level GitHub Projects boards." }, "security-events": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission level for security events (read/write/none). Controls access to view and manage code scanning alerts and security findings." }, "statuses": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission level for commit statuses (read/write/none). Controls access to create and update commit status checks." }, "vulnerability-alerts": { "type": "string", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "description": "Permission level for Dependabot vulnerability alerts (read/write/none). Allows workflows to access the Dependabot alerts API via GITHUB_TOKEN instead of requiring a PAT or GitHub App." }, "all": { "type": "string", - "enum": ["read"], + "enum": [ + "read" + ], "description": "Permission shorthand that applies read access to all permission scopes. Can be combined with specific write permissions to override individual scopes. 'write' is not allowed for all." } } @@ -11665,152 +13423,271 @@ "properties": { "administration": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for repository administration (read/none; \"write\" is rejected by the compiler). GitHub App-only permission for repository administration." }, "codespaces": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for Codespaces (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "codespaces-lifecycle-admin": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for Codespaces lifecycle administration (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "codespaces-metadata": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for Codespaces metadata (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "email-addresses": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for user email addresses (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "environments": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for repository environments (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "git-signing": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for git signing (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "members": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization members (read/none; \"write\" is rejected by the compiler). Required for org team membership API calls." }, "organization-administration": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization administration (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-announcement-banners": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization announcement banners (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-codespaces": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization Codespaces (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-copilot": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization Copilot (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-custom-org-roles": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization custom org roles (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-custom-properties": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization custom properties (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-custom-repository-roles": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization custom repository roles (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-events": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization events (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-hooks": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization webhooks (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-members": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization members management (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-packages": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization packages (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-personal-access-token-requests": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization personal access token requests (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-personal-access-tokens": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization personal access tokens (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-plan": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization plan (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-self-hosted-runners": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization self-hosted runners (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-user-blocking": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for organization user blocking (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "repository-custom-properties": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for repository custom properties (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "repository-hooks": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for repository webhooks (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "single-file": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for single file access (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "team-discussions": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for team discussions (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "vulnerability-alerts": { "type": "string", - "enum": ["read", "none"], + "enum": [ + "read", + "none" + ], "description": "Permission level for Dependabot vulnerability alerts (read/none; \"write\" is rejected by the compiler). Also available as a GITHUB_TOKEN scope. When used with a GitHub App, forwarded as permission-vulnerability-alerts input." }, "workflows": { "type": "string", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "description": "Permission level for GitHub Actions workflow files (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." } }, @@ -11823,6 +13700,17 @@ "organization-administration": "read" } ] + }, + "awf_provider_target": { + "type": "object", + "description": "AWF API proxy target configuration for a single LLM provider.", + "properties": { + "authHeader": { + "type": "string", + "description": "Custom authentication header name to use when forwarding requests to this provider's API. When set, the raw API key is sent as ': ' instead of the provider default ('Authorization: Bearer' for OpenAI, 'x-api-key' for Anthropic). Example: 'api-key' for Azure OpenAI." + } + }, + "additionalProperties": false } } } diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 317b612221c..7e46ea47860 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -184,6 +184,14 @@ type AWFAPIProxyConfig struct { type AWFAPITargetConfig struct { // Host is the hostname (and optional port) of the API endpoint. Host string `json:"host"` + + // 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: Bearer" for OpenAI or "x-api-key" + // for Anthropic). This supports gateways like Azure OpenAI that require + // "api-key: " instead of the standard ****** scheme. + // Maps to: --openai-api-auth-header / --anthropic-api-auth-header + AuthHeader string `json:"authHeader,omitempty"` } // AWFContainerConfig is the "container" section of the AWF config file. @@ -300,6 +308,26 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { targets["anthropic"] = &AWFAPITargetConfig{Host: anthropicTarget} awfConfigLog.Printf("API proxy: custom anthropic target=%s", anthropicTarget) } + + // Apply authHeader overrides from awf.apiProxy.targets frontmatter. + // These are independent of the host/env-var settings: authHeader can be set + // even when no custom host is configured. + var rawFrontmatter map[string]any + if config.WorkflowData != nil { + rawFrontmatter = config.WorkflowData.RawFrontmatter + } + for _, provider := range []string{"openai", "anthropic"} { + authHeader := extractAPITargetAuthHeader(rawFrontmatter, provider) + if authHeader == "" { + continue + } + if existing, ok := targets[provider]; ok { + existing.AuthHeader = authHeader + } else { + targets[provider] = &AWFAPITargetConfig{AuthHeader: authHeader} + } + awfConfigLog.Printf("API proxy: custom %s authHeader=%s", provider, authHeader) + } if copilotTarget := GetCopilotAPITarget(config.WorkflowData); copilotTarget != "" { targets["copilot"] = &AWFAPITargetConfig{Host: copilotTarget} awfConfigLog.Printf("API proxy: custom copilot target=%s", copilotTarget) diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index 01063d370c2..a38193741ca 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -454,6 +454,120 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.Contains(t, jsonStr, "&&", "JSON output should preserve && in GitHub Actions expressions") assert.NotContains(t, jsonStr, "\\u0026", "JSON output should not HTML-escape '&' characters") }) + + t.Run("openai authHeader from frontmatter awf.apiProxy.targets is included", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "codex", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "codex"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + RawFrontmatter: map[string]any{ + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": "api-key", + }, + }, + }, + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"authHeader":"api-key"`, "should include openai authHeader in apiProxy targets") + assert.Contains(t, jsonStr, `"openai"`, "should include openai target") + }) + + t.Run("anthropic authHeader from frontmatter awf.apiProxy.targets is included", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "claude", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "claude"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + RawFrontmatter: map[string]any{ + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "anthropic": map[string]any{ + "authHeader": "api-key", + }, + }, + }, + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"authHeader":"api-key"`, "should include anthropic authHeader in apiProxy targets") + assert.Contains(t, jsonStr, `"anthropic"`, "should include anthropic target") + }) + + t.Run("authHeader coexists with host from engine.env", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "codex", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "codex", + Env: map[string]string{ + "OPENAI_BASE_URL": "https://azure-openai.internal/v1", + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + RawFrontmatter: map[string]any{ + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": "api-key", + }, + }, + }, + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, "azure-openai.internal", "should include host from OPENAI_BASE_URL") + assert.Contains(t, jsonStr, `"authHeader":"api-key"`, "should include authHeader alongside host") + }) + + t.Run("authHeader is omitted when not configured in frontmatter", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "codex", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "codex", + Env: map[string]string{ + "OPENAI_BASE_URL": "https://my-proxy.internal.example.com/v1", + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.NotContains(t, jsonStr, `"authHeader"`, "authHeader should be absent when not configured") + }) } // TestBuildAWFConfigSchemaURL verifies that buildAWFConfigSchemaURL returns a release-pinned diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 6e1ec5131f2..90036343603 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -272,6 +272,77 @@ func TestAWFCustomAPITargetFlags(t *testing.T) { }) } +// TestExtractAPITargetAuthHeader tests the extractAPITargetAuthHeader function that reads +// the custom auth header name from awf.apiProxy.targets..authHeader in frontmatter. +func TestExtractAPITargetAuthHeader(t *testing.T) { + t.Run("returns authHeader for openai provider", func(t *testing.T) { + raw := map[string]any{ + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": "api-key", + }, + }, + }, + }, + } + result := extractAPITargetAuthHeader(raw, "openai") + assert.Equal(t, "api-key", result) + }) + + t.Run("returns authHeader for anthropic provider", func(t *testing.T) { + raw := map[string]any{ + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "anthropic": map[string]any{ + "authHeader": "x-custom-header", + }, + }, + }, + }, + } + result := extractAPITargetAuthHeader(raw, "anthropic") + assert.Equal(t, "x-custom-header", result) + }) + + t.Run("returns empty string when awf key is absent", func(t *testing.T) { + raw := map[string]any{"engine": "codex"} + assert.Empty(t, extractAPITargetAuthHeader(raw, "openai")) + }) + + t.Run("returns empty string when provider is absent", func(t *testing.T) { + raw := map[string]any{ + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{}, + }, + }, + } + assert.Empty(t, extractAPITargetAuthHeader(raw, "openai")) + }) + + t.Run("returns empty string for nil frontmatter", func(t *testing.T) { + assert.Empty(t, extractAPITargetAuthHeader(nil, "openai")) + }) + + t.Run("returns empty string when authHeader value is not a string", func(t *testing.T) { + raw := map[string]any{ + "awf": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": 42, + }, + }, + }, + }, + } + assert.Empty(t, extractAPITargetAuthHeader(raw, "openai")) + }) +} + // TestExtractAPIBasePath tests the extractAPIBasePath function that extracts // path components from custom API base URLs in engine.env func TestExtractAPIBasePath(t *testing.T) { diff --git a/pkg/workflow/engine_api_targets.go b/pkg/workflow/engine_api_targets.go index 65fa58fdaf7..eba41b96d89 100644 --- a/pkg/workflow/engine_api_targets.go +++ b/pkg/workflow/engine_api_targets.go @@ -108,6 +108,60 @@ func extractAPIBasePath(workflowData *WorkflowData, envVar string) string { return "" } +// extractAPITargetAuthHeader extracts the authHeader value from the awf frontmatter section +// for a given provider (e.g. "openai" or "anthropic"). It reads the following frontmatter path: +// +// awf.apiProxy.targets..authHeader +// +// Returns the header name string (e.g. "api-key") or empty string if not configured. +// A non-string value is treated as absent and returns empty string. +func extractAPITargetAuthHeader(rawFrontmatter map[string]any, provider string) string { + if rawFrontmatter == nil { + return "" + } + awfAny, ok := rawFrontmatter["awf"] + if !ok { + return "" + } + awfMap, ok := awfAny.(map[string]any) + if !ok { + return "" + } + apiProxyAny, ok := awfMap["apiProxy"] + if !ok { + return "" + } + apiProxyMap, ok := apiProxyAny.(map[string]any) + if !ok { + return "" + } + targetsAny, ok := apiProxyMap["targets"] + if !ok { + return "" + } + targetsMap, ok := targetsAny.(map[string]any) + if !ok { + return "" + } + providerAny, ok := targetsMap[provider] + if !ok { + return "" + } + providerMap, ok := providerAny.(map[string]any) + if !ok { + return "" + } + authHeaderAny, ok := providerMap["authHeader"] + if !ok { + return "" + } + authHeader, ok := authHeaderAny.(string) + if !ok { + return "" + } + return authHeader +} + // 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/specs/awf-config-sources-spec.md b/specs/awf-config-sources-spec.md index 11eabdd16fa..bb9c6b31392 100644 --- a/specs/awf-config-sources-spec.md +++ b/specs/awf-config-sources-spec.md @@ -60,6 +60,8 @@ The following fields previously existed in schema but were missed in spec CLI ma | `apiProxy.modelMultipliers` | config-only (effective-token accounting) | | `apiProxy.maxRuns` | config-only (LLM invocation hard cap) | | `apiProxy.auth.*` | config-only (maps to `AWF_AUTH_*` env vars) | +| `apiProxy.targets.openai.authHeader` | `--openai-api-auth-header` (frontmatter: `awf.apiProxy.targets.openai.authHeader`) | +| `apiProxy.targets.anthropic.authHeader` | `--anthropic-api-auth-header` (frontmatter: `awf.apiProxy.targets.anthropic.authHeader`) | | `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 e8465b37efdc53c5d4b97d6f2b2d736a3053ffbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 12:55:47 +0000 Subject: [PATCH 3/8] fix: address code review feedback on authHeader comment and schema description Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 2 +- pkg/workflow/awf_config.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index e3699993c8a..45850290327 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -11654,7 +11654,7 @@ }, "awf": { "type": "object", - "description": "AWF (Agent Workflow Firewall) declarative configuration.", + "description": "AWF (Agent Workflow Firewall) declarative configuration. Settings here are compiled into the AWF config JSON and control API proxy routing and authentication for LLM providers in AWF-enabled workflows.", "properties": { "apiProxy": { "type": "object", diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 7e46ea47860..9eb6a3d61ff 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -187,9 +187,9 @@ type AWFAPITargetConfig 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 (e.g., "Authorization: Bearer" for OpenAI or "x-api-key" - // for Anthropic). This supports gateways like Azure OpenAI that require - // "api-key: " instead of the standard ****** scheme. + // 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"` } From fe79b7a3072acab39359515efd9b3ac309619afb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 13:15:07 +0000 Subject: [PATCH 4/8] docs(adr): draft ADR-35694 for awf authHeader frontmatter exposure --- ...pose-authheader-in-awf-apiproxy-targets.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md diff --git a/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md b/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md new file mode 100644 index 00000000000..1b28daf6193 --- /dev/null +++ b/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md @@ -0,0 +1,83 @@ +# ADR-35694: Expose `authHeader` in `awf.apiProxy.targets` Frontmatter + +**Date**: 2026-05-29 +**Status**: Draft +**Deciders**: Unknown + +--- + +## Part 1 — Narrative (Human-Friendly) + +### Context + +The AWF firewall sidecar (PR #3998) introduced `--openai-api-auth-header` and `--anthropic-api-auth-header` flags, plus matching `apiProxy.targets.{openai,anthropic}.authHeader` fields in the AWF JSON config. These exist to support gateways such as Azure OpenAI, which require API keys to be sent as `api-key: ` rather than the provider default (`Authorization: Bearer ` for OpenAI, `x-api-key: ` for Anthropic). The runtime capability exists in the sidecar, but `gh-aw` workflow authors had no declarative way to set it from their frontmatter — they would have had to hand-edit the generated workflow YAML, which is regenerated on every compile. The authentication-header override is independent of host overrides: a workflow may need a custom header against the standard public provider host, or against a custom host already configured via `OPENAI_BASE_URL` / `ANTHROPIC_BASE_URL`. + +### Decision + +We will expose `authHeader` as a frontmatter field at `awf.apiProxy.targets..authHeader` for `provider ∈ {openai, anthropic}`. The new field is read by a dedicated helper `extractAPITargetAuthHeader` (in `pkg/workflow/engine_api_targets.go`) and applied inside `BuildAWFConfigJSON` (in `pkg/workflow/awf_config.go`) by mutating the existing `AWFAPITargetConfig` entry when one is already present, or creating a header-only entry when no host override exists. The field is emitted with `omitempty` so the generated AWF JSON stays clean when it is not configured. The frontmatter path mirrors the AWF JSON config structure 1:1, preserving the drift-tracking guarantee documented in `specs/awf-config-sources-spec.md`. + +### Alternatives Considered + +#### Alternative 1: Top-level engine field (e.g. `engine.auth-header`) + +We could attach the override to the engine config itself, similar to how `engine.api-target` works for Copilot. Rejected because the auth header is a per-target proxy concern, not an engine concern. It would not scale cleanly to per-provider configuration when a future workflow declares multiple engines, and it would diverge from the AWF JSON config layout that the rest of `apiProxy.targets.*` already follows. + +#### Alternative 2: Reuse `engine.env` with new `OPENAI_API_AUTH_HEADER` / `ANTHROPIC_API_AUTH_HEADER` env vars + +The compiler already reads `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` out of `engine.env` to derive host overrides, so the same channel could carry a header name. Rejected because it conflates HTTP-header-level proxy configuration with engine runtime environment variables. The AWF JSON config groups all per-target proxy settings under `apiProxy.targets.`, and adding env-var-only overrides would break the 1:1 mapping that the drift-tracking spec relies on. + +#### Alternative 3: Auto-detect Azure-style hosts and default `authHeader` to `api-key` + +The compiler could inspect the host of `OPENAI_BASE_URL` and, when it matches an Azure pattern (e.g. `*.openai.azure.com`), automatically emit `authHeader: api-key`. Rejected because it would require maintaining a brittle host-pattern allowlist, would not cover non-Azure gateways with similar requirements, and would silently override user intent in edge cases. Explicit configuration is more predictable. + +### Consequences + +#### Positive +- Workflow authors can target Azure OpenAI (and other gateways requiring custom headers) without forking or hand-editing the generated workflow YAML. +- The schema validates `authHeader` as a string and rejects unknown providers via `additionalProperties: false`, so typos fail at compile time rather than at runtime in the AWF sidecar. +- The frontmatter path mirrors the runtime AWF JSON config 1:1, extending the drift-tracking design in `specs/awf-config-sources-spec.md` rather than introducing a new mapping convention. +- `authHeader` is independent of host overrides, so workflows that need a custom header against the public provider host (or against an already-configured host) can express that without redundant configuration. + +#### Negative +- The workflow frontmatter schema grows by a new nested block (`awf.apiProxy.targets.{openai,anthropic}.authHeader`); the regenerated `main_workflow_schema.json` adds ~1.9k net lines, increasing the surface that schema-based tooling must scan. +- Two truths must be kept in sync: a workflow can configure `authHeader: api-key` without setting a custom host, which is valid but easy to misuse if the public provider rejects the non-standard header. +- The new helper `extractAPITargetAuthHeader` is a per-provider lookup that traverses the same frontmatter path that future per-target fields (e.g. timeouts, retries) would also walk; we accept this duplication for now rather than building a generic per-target extractor. + +#### Neutral +- `AWFAPITargetConfig` now carries `AuthHeader string` alongside `Host string`; entries created solely for the auth-header override (no host) still serialize cleanly because `Host` and `AuthHeader` both use `omitempty`. +- A `specs/awf-config-sources-spec.md` table gains two rows (`apiProxy.targets.openai.authHeader`, `apiProxy.targets.anthropic.authHeader`) to record the new frontmatter ↔ CLI-flag mapping. +- The decision deliberately scopes the feature to `openai` and `anthropic` for now; adding more providers (e.g. `copilot`, `gemini`) requires only extending the `for _, provider := range []string{"openai", "anthropic"}` slice and the schema enum. + +--- + +## Part 2 — Normative Specification (RFC 2119) + +> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +### Frontmatter Schema + +1. The workflow frontmatter **MUST** accept an optional top-level `awf` object whose `apiProxy.targets..authHeader` path holds the custom authentication header name. +2. The set of recognized providers under `awf.apiProxy.targets` **MUST** be restricted via `additionalProperties: false`; unknown provider keys **MUST** be rejected at schema-validation time. +3. The value of `authHeader` **MUST** be a string; non-string values (including numbers, booleans, arrays, and objects) **MUST** be rejected at schema-validation time. +4. Workflows **MAY** set `authHeader` without configuring a custom host for the same provider; the two settings **MUST** be independent. + +### Compiler Behavior + +1. `BuildAWFConfigJSON` **MUST** read `awf.apiProxy.targets..authHeader` from `WorkflowData.RawFrontmatter` for each supported provider and apply it to the emitted AWF JSON config. +2. When a target entry already exists for a provider (e.g. because a custom host was configured), the compiler **MUST** mutate the existing entry in place rather than overwriting it; the host and `authHeader` fields **MUST** coexist. +3. When no target entry exists for a provider, the compiler **MUST** create a header-only entry containing only `authHeader`, leaving `host` unset. +4. The compiler **MUST NOT** emit an `authHeader` field in the AWF JSON output when the frontmatter value is absent, empty, or non-string. +5. The frontmatter-extraction helper **MUST** return an empty string (and the compiler **MUST** treat that as "not configured") when any of the following hold: the frontmatter is `nil`; the `awf` key is missing or not an object; the `apiProxy`, `targets`, or `` keys are missing or not objects; the `authHeader` key is missing or not a string. + +### Drift Tracking + +1. `specs/awf-config-sources-spec.md` **MUST** list every `awf.*` frontmatter path that maps to an AWF JSON config field or AWF CLI flag, including `apiProxy.targets.openai.authHeader` and `apiProxy.targets.anthropic.authHeader`. +2. Any future addition of a per-target proxy field to the AWF JSON config **SHOULD** be exposed via the parallel `awf.apiProxy.targets..` frontmatter path and **MUST** be recorded in the drift-tracking table. + +### Conformance + +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/26638539097) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* From a9d81b4b840e83425f460e620f1a6f767975bbc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 13:16:40 +0000 Subject: [PATCH 5/8] fix: add omitempty to Host field and add authHeader to awf-config schema Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/workflow/awf_config.go | 2 +- pkg/workflow/awf_config_test.go | 2 + pkg/workflow/schemas/awf-config.schema.json | 914 ++++++++++---------- 3 files changed, 483 insertions(+), 435 deletions(-) diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 9eb6a3d61ff..cec6610a755 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -183,7 +183,7 @@ type AWFAPIProxyConfig struct { // Maps to: ---api-target type AWFAPITargetConfig struct { // Host is the hostname (and optional port) of the API endpoint. - Host string `json:"host"` + 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 diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index a38193741ca..2704146abf9 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -482,6 +482,7 @@ func TestBuildAWFConfigJSON(t *testing.T) { require.NoError(t, err) assert.Contains(t, jsonStr, `"authHeader":"api-key"`, "should include openai authHeader in apiProxy targets") assert.Contains(t, jsonStr, `"openai"`, "should include openai target") + assert.NotContains(t, jsonStr, `"host":""`, "should not emit empty host when only authHeader is set") }) t.Run("anthropic authHeader from frontmatter awf.apiProxy.targets is included", func(t *testing.T) { @@ -511,6 +512,7 @@ func TestBuildAWFConfigJSON(t *testing.T) { require.NoError(t, err) assert.Contains(t, jsonStr, `"authHeader":"api-key"`, "should include anthropic authHeader in apiProxy targets") assert.Contains(t, jsonStr, `"anthropic"`, "should include anthropic target") + assert.NotContains(t, jsonStr, `"host":""`, "should not emit empty host when only authHeader is set") }) t.Run("authHeader coexists with host from engine.env", func(t *testing.T) { diff --git a/pkg/workflow/schemas/awf-config.schema.json b/pkg/workflow/schemas/awf-config.schema.json index e7c2a15410e..1f03f09235c 100644 --- a/pkg/workflow/schemas/awf-config.schema.json +++ b/pkg/workflow/schemas/awf-config.schema.json @@ -1,454 +1,500 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/github/gh-aw-firewall/main/docs/awf-config.schema.json", - "title": "AWF Configuration", - "description": "JSON/YAML configuration for awf CLI. CLI flags override config file values. See https://github.com/github/gh-aw-firewall for documentation.", - "type": "object", - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string", - "description": "JSON Schema URL for IDE validation and autocomplete." - }, - "network": { - "type": "object", - "description": "Network egress configuration.", - "additionalProperties": false, - "properties": { - "allowDomains": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Domains that the agent is allowed to reach. Both the bare domain and all subdomains are permitted (e.g. \"github.com\" also allows \"api.github.com\")." - }, - "blockDomains": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Domains that are explicitly blocked, overriding allowDomains." - }, - "dnsServers": { - "type": "array", - "items": { - "type": "string" - }, - "description": "DNS servers to use inside the container. Defaults to Google DNS (8.8.8.8, 8.8.4.4). Accepts IPv4 and IPv6 addresses." - }, - "upstreamProxy": { - "type": "string", - "description": "Upstream HTTP proxy URL (e.g. \"http://proxy.corp.example.com:8080\"). When set, the AWF Squid proxy forwards traffic through this proxy." - } - } - }, - "apiProxy": { - "type": "object", - "description": "API proxy sidecar configuration. The sidecar injects real API credentials so the agent never has direct access to them.", - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean", - "description": "Enable the API proxy sidecar container. When enabled, source credentials (OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_GITHUB_TOKEN, COPILOT_API_KEY, GEMINI_API_KEY) are held exclusively in the sidecar and excluded from the agent environment. The agent receives proxy-routing base URLs instead. See docs/awf-config-spec.md §9 for credential isolation semantics." - }, - "enableTokenSteering": { - "type": "boolean", - "description": "Enable effective token budget steering. When true, the proxy injects budget-warning system messages at 80%, 90%, 95%, and 99% usage to nudge the agent to wrap up. Requires maxEffectiveTokens. Default: false." - }, - "anthropicAutoCache": { - "type": "boolean", - "description": "Automatically apply Anthropic prompt-cache optimizations on /v1/messages requests." - }, - "anthropicCacheTailTtl": { - "type": "string", - "enum": ["5m", "1h"], - "description": "TTL for Anthropic cache tail optimization. Only applies when anthropicAutoCache is enabled. Allowed values: \"5m\" or \"1h\"." - }, - "maxEffectiveTokens": { - "type": "integer", - "minimum": 1, - "description": "Maximum cumulative effective tokens allowed for a run. When reached, the API proxy rejects subsequent requests with HTTP 429 and error type 'effective_tokens_limit_exceeded'. Tokens are weighted: input ×1, cache-read ×0.1, output ×4, reasoning ×4. See spec §10." - }, - "modelMultipliers": { - "type": "object", - "description": "Per-model multipliers for effective token accounting. Each model's weighted tokens are multiplied by this value before accumulation. Defaults to 1 for unlisted models. See spec §10.2.", - "additionalProperties": { - "type": "number", - "exclusiveMinimum": 0 - } - }, - "maxRuns": { - "type": "integer", - "minimum": 1, - "description": "Maximum number of LLM invocations allowed for a run. When reached, the API proxy rejects subsequent requests with HTTP 429 and error type 'max_runs_exceeded'. See spec §11." - }, - "modelFallback": { - "type": "object", - "description": "Model fallback policy for unresolved model selections. Enabled by default as a safety net.", - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean", - "description": "Enable or disable middle-power fallback when model resolution fails." - }, - "strategy": { - "type": "string", - "enum": ["middle_power"], - "description": "Fallback selection strategy. Currently only 'middle_power' is supported." + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/github/gh-aw-firewall/main/docs/awf-config.schema.json", + "title": "AWF Configuration", + "description": "JSON/YAML configuration for awf CLI. CLI flags override config file values. See https://github.com/github/gh-aw-firewall for documentation.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema URL for IDE validation and autocomplete." + }, + "network": { + "type": "object", + "description": "Network egress configuration.", + "additionalProperties": false, + "properties": { + "allowDomains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Domains that the agent is allowed to reach. Both the bare domain and all subdomains are permitted (e.g. \"github.com\" also allows \"api.github.com\")." + }, + "blockDomains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Domains that are explicitly blocked, overriding allowDomains." + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "DNS servers to use inside the container. Defaults to Google DNS (8.8.8.8, 8.8.4.4). Accepts IPv4 and IPv6 addresses." + }, + "upstreamProxy": { + "type": "string", + "description": "Upstream HTTP proxy URL (e.g. \"http://proxy.corp.example.com:8080\"). When set, the AWF Squid proxy forwards traffic through this proxy." + } } - } }, - "targets": { - "type": "object", - "description": "Override upstream API endpoints for each provider.", - "additionalProperties": false, - "properties": { - "openai": { - "$ref": "#/$defs/providerTarget", - "description": "OpenAI API target override." - }, - "anthropic": { - "$ref": "#/$defs/providerTarget", - "description": "Anthropic API target override." - }, - "copilot": { - "$ref": "#/$defs/providerHostOnlyTarget", - "description": "GitHub Copilot API target override (basePath not supported)." - }, - "gemini": { - "$ref": "#/$defs/providerTarget", - "description": "Google Gemini API target override. Deprecated: use 'antigravity' instead." - }, - "antigravity": { - "$ref": "#/$defs/providerTarget", - "description": "Antigravity API target override." + "apiProxy": { + "type": "object", + "description": "API proxy sidecar configuration. The sidecar injects real API credentials so the agent never has direct access to them.", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable the API proxy sidecar container. When enabled, source credentials (OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_GITHUB_TOKEN, COPILOT_API_KEY, GEMINI_API_KEY) are held exclusively in the sidecar and excluded from the agent environment. The agent receives proxy-routing base URLs instead. See docs/awf-config-spec.md \u00a79 for credential isolation semantics." + }, + "enableTokenSteering": { + "type": "boolean", + "description": "Enable effective token budget steering. When true, the proxy injects budget-warning system messages at 80%, 90%, 95%, and 99% usage to nudge the agent to wrap up. Requires maxEffectiveTokens. Default: false." + }, + "anthropicAutoCache": { + "type": "boolean", + "description": "Automatically apply Anthropic prompt-cache optimizations on /v1/messages requests." + }, + "anthropicCacheTailTtl": { + "type": "string", + "enum": [ + "5m", + "1h" + ], + "description": "TTL for Anthropic cache tail optimization. Only applies when anthropicAutoCache is enabled. Allowed values: \"5m\" or \"1h\"." + }, + "maxEffectiveTokens": { + "type": "integer", + "minimum": 1, + "description": "Maximum cumulative effective tokens allowed for a run. When reached, the API proxy rejects subsequent requests with HTTP 429 and error type 'effective_tokens_limit_exceeded'. Tokens are weighted: input \u00d71, cache-read \u00d70.1, output \u00d74, reasoning \u00d74. See spec \u00a710." + }, + "modelMultipliers": { + "type": "object", + "description": "Per-model multipliers for effective token accounting. Each model's weighted tokens are multiplied by this value before accumulation. Defaults to 1 for unlisted models. See spec \u00a710.2.", + "additionalProperties": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "maxRuns": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of LLM invocations allowed for a run. When reached, the API proxy rejects subsequent requests with HTTP 429 and error type 'max_runs_exceeded'. See spec \u00a711." + }, + "modelFallback": { + "type": "object", + "description": "Model fallback policy for unresolved model selections. Enabled by default as a safety net.", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable or disable middle-power fallback when model resolution fails." + }, + "strategy": { + "type": "string", + "enum": [ + "middle_power" + ], + "description": "Fallback selection strategy. Currently only 'middle_power' is supported." + } + } + }, + "targets": { + "type": "object", + "description": "Override upstream API endpoints for each provider.", + "additionalProperties": false, + "properties": { + "openai": { + "$ref": "#/$defs/providerTarget", + "description": "OpenAI API target override." + }, + "anthropic": { + "$ref": "#/$defs/providerTarget", + "description": "Anthropic API target override." + }, + "copilot": { + "$ref": "#/$defs/providerHostOnlyTarget", + "description": "GitHub Copilot API target override (basePath not supported)." + }, + "gemini": { + "$ref": "#/$defs/providerTarget", + "description": "Google Gemini API target override. Deprecated: use 'antigravity' instead." + }, + "antigravity": { + "$ref": "#/$defs/providerTarget", + "description": "Antigravity API target override." + } + } + }, + "models": { + "type": "object", + "description": "Model alias mapping. Keys are canonical model names; values are arrays of alternative names or patterns that should be rewritten to the canonical name.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "auth": { + "type": "object", + "description": "Authentication configuration for the API proxy sidecar. Enables OIDC-based credential exchange (e.g., GitHub OIDC \u2192 Azure AD, AWS STS, or GCP Workload Identity). See docs/awf-config-spec.md \u00a79.5.", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "github-oidc" + ], + "description": "Authentication type. Currently only 'github-oidc' is supported. Maps to AWF_AUTH_TYPE." + }, + "provider": { + "type": "string", + "enum": [ + "azure", + "aws", + "gcp" + ], + "description": "Cloud provider for OIDC token exchange. Determines which token exchange protocol is used. Maps to AWF_AUTH_PROVIDER.", + "default": "azure" + }, + "oidcAudience": { + "type": "string", + "description": "Audience claim for the GitHub OIDC token. Provider-specific defaults apply when omitted: Azure='api://AzureADTokenExchange', AWS='sts.amazonaws.com', GCP=workloadIdentityProvider value. Maps to AWF_AUTH_OIDC_AUDIENCE." + }, + "azureTenantId": { + "type": "string", + "description": "Azure AD tenant ID for federated credential exchange. Required when provider is 'azure'. Maps to AWF_AUTH_AZURE_TENANT_ID." + }, + "azureClientId": { + "type": "string", + "description": "Azure AD application (client) ID for the federated credential. Required when provider is 'azure'. Maps to AWF_AUTH_AZURE_CLIENT_ID." + }, + "azureScope": { + "type": "string", + "description": "Azure token scope. Maps to AWF_AUTH_AZURE_SCOPE.", + "default": "https://cognitiveservices.azure.com/.default" + }, + "azureCloud": { + "type": "string", + "enum": [ + "public", + "usgovernment", + "china" + ], + "description": "Azure cloud environment. Maps to AWF_AUTH_AZURE_CLOUD.", + "default": "public" + }, + "awsRoleArn": { + "type": "string", + "description": "AWS IAM role ARN to assume via OIDC federation. Required when provider is 'aws'. Maps to AWF_AUTH_AWS_ROLE_ARN." + }, + "awsRegion": { + "type": "string", + "description": "AWS region for the Bedrock endpoint. Required when provider is 'aws'. Maps to AWF_AUTH_AWS_REGION." + }, + "awsRoleSessionName": { + "type": "string", + "description": "Session name for the AWS STS AssumeRoleWithWebIdentity call. Maps to AWF_AUTH_AWS_ROLE_SESSION_NAME.", + "default": "awf-oidc-session" + }, + "gcpWorkloadIdentityProvider": { + "type": "string", + "description": "Full resource name of the GCP Workload Identity Provider (projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID). Required when provider is 'gcp'. Maps to AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER." + }, + "gcpServiceAccount": { + "type": "string", + "description": "GCP service account email to impersonate. When omitted, the federated token is used directly (requires direct resource access grants on the principal). Maps to AWF_AUTH_GCP_SERVICE_ACCOUNT." + }, + "gcpScope": { + "type": "string", + "description": "OAuth2 scope for GCP token. Maps to AWF_AUTH_GCP_SCOPE.", + "default": "https://www.googleapis.com/auth/cloud-platform" + } + }, + "required": [ + "type" + ], + "if": { + "properties": { + "provider": { + "const": "aws" + } + }, + "required": [ + "provider" + ] + }, + "then": { + "required": [ + "awsRoleArn", + "awsRegion" + ] + }, + "else": { + "if": { + "properties": { + "provider": { + "const": "gcp" + } + }, + "required": [ + "provider" + ] + }, + "then": { + "required": [ + "gcpWorkloadIdentityProvider" + ] + }, + "else": { + "required": [ + "azureTenantId", + "azureClientId" + ] + } + } + } } - } }, - "models": { - "type": "object", - "description": "Model alias mapping. Keys are canonical model names; values are arrays of alternative names or patterns that should be rewritten to the canonical name.", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" + "security": { + "type": "object", + "description": "Security and isolation configuration.", + "additionalProperties": false, + "properties": { + "sslBump": { + "type": "boolean", + "description": "Enable SSL bumping (TLS interception) in the Squid proxy. Requires a custom CA certificate." + }, + "enableDlp": { + "type": "boolean", + "description": "Enable Data Loss Prevention (DLP) inspection of outbound traffic." + }, + "enableHostAccess": { + "type": "boolean", + "description": "Mount the host filesystem (read-only for system paths, read-write for the workspace). Enabled by default; set to false to run without host filesystem access." + }, + "allowHostPorts": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "Host TCP ports the agent may connect to (e.g. local dev services). Accepts a single port string or an array of port strings." + }, + "allowHostServicePorts": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "Named service ports on the host that the agent may connect to. Accepts a single port string or an array of port strings." + }, + "difcProxy": { + "type": "object", + "description": "DIFC (Data-in-Flight Control) proxy configuration.", + "additionalProperties": false, + "properties": { + "host": { + "type": "string", + "description": "DIFC proxy host." + }, + "caCert": { + "type": "string", + "description": "Path to the CA certificate for DIFC proxy TLS verification." + } + } + } } - } }, - "auth": { - "type": "object", - "description": "Authentication configuration for the API proxy sidecar. Enables OIDC-based credential exchange (e.g., GitHub OIDC → Azure AD, AWS STS, or GCP Workload Identity). See docs/awf-config-spec.md §9.5.", - "additionalProperties": false, - "properties": { - "type": { - "type": "string", - "enum": ["github-oidc"], - "description": "Authentication type. Currently only 'github-oidc' is supported. Maps to AWF_AUTH_TYPE." - }, - "provider": { - "type": "string", - "enum": ["azure", "aws", "gcp"], - "description": "Cloud provider for OIDC token exchange. Determines which token exchange protocol is used. Maps to AWF_AUTH_PROVIDER.", - "default": "azure" - }, - "oidcAudience": { - "type": "string", - "description": "Audience claim for the GitHub OIDC token. Provider-specific defaults apply when omitted: Azure='api://AzureADTokenExchange', AWS='sts.amazonaws.com', GCP=workloadIdentityProvider value. Maps to AWF_AUTH_OIDC_AUDIENCE." - }, - "azureTenantId": { - "type": "string", - "description": "Azure AD tenant ID for federated credential exchange. Required when provider is 'azure'. Maps to AWF_AUTH_AZURE_TENANT_ID." - }, - "azureClientId": { - "type": "string", - "description": "Azure AD application (client) ID for the federated credential. Required when provider is 'azure'. Maps to AWF_AUTH_AZURE_CLIENT_ID." - }, - "azureScope": { - "type": "string", - "description": "Azure token scope. Maps to AWF_AUTH_AZURE_SCOPE.", - "default": "https://cognitiveservices.azure.com/.default" - }, - "azureCloud": { - "type": "string", - "enum": ["public", "usgovernment", "china"], - "description": "Azure cloud environment. Maps to AWF_AUTH_AZURE_CLOUD.", - "default": "public" - }, - "awsRoleArn": { - "type": "string", - "description": "AWS IAM role ARN to assume via OIDC federation. Required when provider is 'aws'. Maps to AWF_AUTH_AWS_ROLE_ARN." - }, - "awsRegion": { - "type": "string", - "description": "AWS region for the Bedrock endpoint. Required when provider is 'aws'. Maps to AWF_AUTH_AWS_REGION." - }, - "awsRoleSessionName": { - "type": "string", - "description": "Session name for the AWS STS AssumeRoleWithWebIdentity call. Maps to AWF_AUTH_AWS_ROLE_SESSION_NAME.", - "default": "awf-oidc-session" - }, - "gcpWorkloadIdentityProvider": { - "type": "string", - "description": "Full resource name of the GCP Workload Identity Provider (projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID). Required when provider is 'gcp'. Maps to AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER." - }, - "gcpServiceAccount": { - "type": "string", - "description": "GCP service account email to impersonate. When omitted, the federated token is used directly (requires direct resource access grants on the principal). Maps to AWF_AUTH_GCP_SERVICE_ACCOUNT." - }, - "gcpScope": { - "type": "string", - "description": "OAuth2 scope for GCP token. Maps to AWF_AUTH_GCP_SCOPE.", - "default": "https://www.googleapis.com/auth/cloud-platform" + "container": { + "type": "object", + "description": "Container and Docker configuration.", + "additionalProperties": false, + "properties": { + "memoryLimit": { + "type": "string", + "description": "Docker memory limit for the agent container (e.g. \"4g\", \"512m\"). Uses Docker memory limit syntax." + }, + "agentTimeout": { + "type": "integer", + "minimum": 1, + "description": "Maximum time (in minutes) the agent command is allowed to run." + }, + "enableDind": { + "type": "boolean", + "description": "Enable Docker-in-Docker support inside the agent container." + }, + "workDir": { + "type": "string", + "description": "Host path used as the AWF working directory for generated configs and logs. Defaults to a temporary directory." + }, + "containerWorkDir": { + "type": "string", + "description": "Working directory inside the agent container." + }, + "imageRegistry": { + "type": "string", + "description": "Container image registry to pull from. Defaults to \"ghcr.io/github/gh-aw-firewall\"." + }, + "imageTag": { + "type": "string", + "description": "Container image tag to use. Defaults to \"latest\"." + }, + "skipPull": { + "type": "boolean", + "description": "Skip pulling container images (use locally cached images)." + }, + "buildLocal": { + "type": "boolean", + "description": "Build container images from source instead of pulling from the registry." + }, + "agentImage": { + "type": "string", + "description": "Override the agent container image (e.g. for a GitHub Actions parity image)." + }, + "tty": { + "type": "boolean", + "description": "Allocate a pseudo-TTY for the agent container." + }, + "dockerHost": { + "type": "string", + "description": "Docker daemon socket or host to connect to (e.g. \"unix:///var/run/docker.sock\")." + }, + "dockerHostPathPrefix": { + "type": "string", + "description": "Prefix bind-mount source paths so the Docker daemon can resolve runner filesystem paths. Required for ARC DinD sidecar runners where the runner and daemon have separate filesystems. Example: \"/host\". Kernel virtual filesystems (/dev, /sys, /proc) are automatically excluded from prefixing." + } } - }, - "required": ["type"], - "if": { - "properties": { "provider": { "const": "aws" } }, - "required": ["provider"] - }, - "then": { - "required": ["awsRoleArn", "awsRegion"] - }, - "else": { - "if": { - "properties": { "provider": { "const": "gcp" } }, - "required": ["provider"] - }, - "then": { - "required": ["gcpWorkloadIdentityProvider"] - }, - "else": { - "required": ["azureTenantId", "azureClientId"] - } - } - } - } - }, - "security": { - "type": "object", - "description": "Security and isolation configuration.", - "additionalProperties": false, - "properties": { - "sslBump": { - "type": "boolean", - "description": "Enable SSL bumping (TLS interception) in the Squid proxy. Requires a custom CA certificate." - }, - "enableDlp": { - "type": "boolean", - "description": "Enable Data Loss Prevention (DLP) inspection of outbound traffic." }, - "enableHostAccess": { - "type": "boolean", - "description": "Mount the host filesystem (read-only for system paths, read-write for the workspace). Enabled by default; set to false to run without host filesystem access." - }, - "allowHostPorts": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } + "environment": { + "type": "object", + "description": "Environment variable propagation into the agent container. Merge behavior is: AWF-reserved variables are set by AWF and are not overridden by envAll or envFile; if envAll is true, host environment variables are forwarded next; envFile is then applied only for variables not already present, so it does not override envAll; CLI -e/--env has highest precedence and may override any variable, including AWF-reserved ones. When apiProxy.enabled is true, source credentials (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) are excluded from the agent and held in the API proxy sidecar. See docs/awf-config-spec.md \u00a78\u20139 for credential isolation rules.", + "additionalProperties": false, + "properties": { + "envFile": { + "type": "string", + "description": "Path to a .env file whose variables are injected into the agent container." + }, + "envAll": { + "type": "boolean", + "description": "Forward all host environment variables into the agent container. Use with caution \u2014 may expose secrets." + }, + "excludeEnv": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Environment variable names to exclude when envAll is true. IMPORTANT: Excluding LLM API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) without setting apiProxy.enabled=true will cause agent authentication failures. Use apiProxy.enabled=true for credential isolation instead of manual exclusion." + } } - ], - "description": "Host TCP ports the agent may connect to (e.g. local dev services). Accepts a single port string or an array of port strings." }, - "allowHostServicePorts": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } + "logging": { + "type": "object", + "description": "Logging and diagnostics configuration.", + "additionalProperties": false, + "properties": { + "logLevel": { + "type": "string", + "enum": [ + "debug", + "info", + "warn", + "error" + ], + "description": "Log verbosity level. Defaults to \"info\"." + }, + "diagnosticLogs": { + "type": "boolean", + "description": "Enable diagnostic logging (Squid access logs, iptables logs). Logs are written to the work directory." + }, + "auditDir": { + "type": "string", + "description": "Directory path for audit logs." + }, + "proxyLogsDir": { + "type": "string", + "description": "Directory path for Squid proxy access logs." + }, + "sessionStateDir": { + "type": "string", + "description": "Directory path for agent session state (e.g. conversation history). Set to \"/tmp/gh-aw/sandbox/agent/session-state\" for Copilot agent runs." + } } - ], - "description": "Named service ports on the host that the agent may connect to. Accepts a single port string or an array of port strings." }, - "difcProxy": { - "type": "object", - "description": "DIFC (Data-in-Flight Control) proxy configuration.", - "additionalProperties": false, - "properties": { - "host": { - "type": "string", - "description": "DIFC proxy host." - }, - "caCert": { - "type": "string", - "description": "Path to the CA certificate for DIFC proxy TLS verification." + "rateLimiting": { + "type": "object", + "description": "Egress rate limiting configuration.", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable egress rate limiting." + }, + "requestsPerMinute": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of HTTP requests per minute." + }, + "requestsPerHour": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of HTTP requests per hour." + }, + "bytesPerMinute": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of bytes transferred per minute." + } } - } } - } }, - "container": { - "type": "object", - "description": "Container and Docker configuration.", - "additionalProperties": false, - "properties": { - "memoryLimit": { - "type": "string", - "description": "Docker memory limit for the agent container (e.g. \"4g\", \"512m\"). Uses Docker memory limit syntax." - }, - "agentTimeout": { - "type": "integer", - "minimum": 1, - "description": "Maximum time (in minutes) the agent command is allowed to run." - }, - "enableDind": { - "type": "boolean", - "description": "Enable Docker-in-Docker support inside the agent container." - }, - "workDir": { - "type": "string", - "description": "Host path used as the AWF working directory for generated configs and logs. Defaults to a temporary directory." - }, - "containerWorkDir": { - "type": "string", - "description": "Working directory inside the agent container." - }, - "imageRegistry": { - "type": "string", - "description": "Container image registry to pull from. Defaults to \"ghcr.io/github/gh-aw-firewall\"." - }, - "imageTag": { - "type": "string", - "description": "Container image tag to use. Defaults to \"latest\"." - }, - "skipPull": { - "type": "boolean", - "description": "Skip pulling container images (use locally cached images)." - }, - "buildLocal": { - "type": "boolean", - "description": "Build container images from source instead of pulling from the registry." - }, - "agentImage": { - "type": "string", - "description": "Override the agent container image (e.g. for a GitHub Actions parity image)." - }, - "tty": { - "type": "boolean", - "description": "Allocate a pseudo-TTY for the agent container." - }, - "dockerHost": { - "type": "string", - "description": "Docker daemon socket or host to connect to (e.g. \"unix:///var/run/docker.sock\")." - }, - "dockerHostPathPrefix": { - "type": "string", - "description": "Prefix bind-mount source paths so the Docker daemon can resolve runner filesystem paths. Required for ARC DinD sidecar runners where the runner and daemon have separate filesystems. Example: \"/host\". Kernel virtual filesystems (/dev, /sys, /proc) are automatically excluded from prefixing." - } - } - }, - "environment": { - "type": "object", - "description": "Environment variable propagation into the agent container. Merge behavior is: AWF-reserved variables are set by AWF and are not overridden by envAll or envFile; if envAll is true, host environment variables are forwarded next; envFile is then applied only for variables not already present, so it does not override envAll; CLI -e/--env has highest precedence and may override any variable, including AWF-reserved ones. When apiProxy.enabled is true, source credentials (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) are excluded from the agent and held in the API proxy sidecar. See docs/awf-config-spec.md §8–9 for credential isolation rules.", - "additionalProperties": false, - "properties": { - "envFile": { - "type": "string", - "description": "Path to a .env file whose variables are injected into the agent container." - }, - "envAll": { - "type": "boolean", - "description": "Forward all host environment variables into the agent container. Use with caution — may expose secrets." - }, - "excludeEnv": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Environment variable names to exclude when envAll is true. IMPORTANT: Excluding LLM API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) without setting apiProxy.enabled=true will cause agent authentication failures. Use apiProxy.enabled=true for credential isolation instead of manual exclusion." - } - } - }, - "logging": { - "type": "object", - "description": "Logging and diagnostics configuration.", - "additionalProperties": false, - "properties": { - "logLevel": { - "type": "string", - "enum": ["debug", "info", "warn", "error"], - "description": "Log verbosity level. Defaults to \"info\"." - }, - "diagnosticLogs": { - "type": "boolean", - "description": "Enable diagnostic logging (Squid access logs, iptables logs). Logs are written to the work directory." - }, - "auditDir": { - "type": "string", - "description": "Directory path for audit logs." - }, - "proxyLogsDir": { - "type": "string", - "description": "Directory path for Squid proxy access logs." - }, - "sessionStateDir": { - "type": "string", - "description": "Directory path for agent session state (e.g. conversation history). Set to \"/tmp/gh-aw/sandbox/agent/session-state\" for Copilot agent runs." - } - } - }, - "rateLimiting": { - "type": "object", - "description": "Egress rate limiting configuration.", - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean", - "description": "Enable egress rate limiting." - }, - "requestsPerMinute": { - "type": "integer", - "minimum": 1, - "description": "Maximum number of HTTP requests per minute." - }, - "requestsPerHour": { - "type": "integer", - "minimum": 1, - "description": "Maximum number of HTTP requests per hour." - }, - "bytesPerMinute": { - "type": "integer", - "minimum": 1, - "description": "Maximum number of bytes transferred per minute." - } - } - } - }, - "$defs": { - "providerTarget": { - "type": "object", - "description": "API provider target override.", - "additionalProperties": false, - "properties": { - "host": { - "type": "string", - "description": "Override the provider API host." + "$defs": { + "providerTarget": { + "type": "object", + "description": "API provider target override.", + "additionalProperties": false, + "properties": { + "host": { + "type": "string", + "description": "Override the provider API host." + }, + "basePath": { + "type": "string", + "description": "Override the provider API base path." + }, + "authHeader": { + "type": "string", + "description": "Custom authentication header name sent with API requests. Overrides the provider default (\"Authorization\" for OpenAI, \"x-api-key\" for Anthropic). Use \"api-key\" for Azure OpenAI gateways." + } + } }, - "basePath": { - "type": "string", - "description": "Override the provider API base path." - } - } - }, - "providerHostOnlyTarget": { - "type": "object", - "description": "API provider target override (host only; basePath not supported).", - "additionalProperties": false, - "properties": { - "host": { - "type": "string", - "description": "Override the provider API host." + "providerHostOnlyTarget": { + "type": "object", + "description": "API provider target override (host only; basePath not supported).", + "additionalProperties": false, + "properties": { + "host": { + "type": "string", + "description": "Override the provider API host." + } + } } - } } - } } From e7c7f833947f4dd14b63faf99569da73136fb518 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 14:16:23 +0000 Subject: [PATCH 6/8] feat: move authHeader frontmatter to sandbox.agent.apiProxy.targets Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...pose-authheader-in-awf-apiproxy-targets.md | 18 ++--- pkg/parser/schema_test.go | 54 ++++++++------ pkg/parser/schemas/main_workflow_schema.json | 51 ++++++-------- pkg/workflow/awf_config.go | 8 +-- pkg/workflow/awf_config_test.go | 40 +++++------ pkg/workflow/awf_helpers_test.go | 70 ++++++++----------- pkg/workflow/engine_api_targets.go | 53 +++----------- pkg/workflow/sandbox.go | 16 +++++ specs/awf-config-sources-spec.md | 4 +- 9 files changed, 138 insertions(+), 176 deletions(-) diff --git a/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md b/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md index 1b28daf6193..a907a6ae4a7 100644 --- a/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md +++ b/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md @@ -1,4 +1,4 @@ -# ADR-35694: Expose `authHeader` in `awf.apiProxy.targets` Frontmatter +# ADR-35694: Expose `authHeader` in `sandbox.agent.apiProxy.targets` Frontmatter **Date**: 2026-05-29 **Status**: Draft @@ -14,7 +14,7 @@ The AWF firewall sidecar (PR #3998) introduced `--openai-api-auth-header` and `- ### Decision -We will expose `authHeader` as a frontmatter field at `awf.apiProxy.targets..authHeader` for `provider ∈ {openai, anthropic}`. The new field is read by a dedicated helper `extractAPITargetAuthHeader` (in `pkg/workflow/engine_api_targets.go`) and applied inside `BuildAWFConfigJSON` (in `pkg/workflow/awf_config.go`) by mutating the existing `AWFAPITargetConfig` entry when one is already present, or creating a header-only entry when no host override exists. The field is emitted with `omitempty` so the generated AWF JSON stays clean when it is not configured. The frontmatter path mirrors the AWF JSON config structure 1:1, preserving the drift-tracking guarantee documented in `specs/awf-config-sources-spec.md`. +We will expose `authHeader` as a frontmatter field at `sandbox.agent.apiProxy.targets..authHeader` for `provider ∈ {openai, anthropic}`. The new field is read by a dedicated helper `extractAPITargetAuthHeader` (in `pkg/workflow/engine_api_targets.go`) and applied inside `BuildAWFConfigJSON` (in `pkg/workflow/awf_config.go`) by mutating the existing `AWFAPITargetConfig` entry when one is already present, or creating a header-only entry when no host override exists. The field is emitted with `omitempty` so the generated AWF JSON stays clean when it is not configured. The frontmatter path mirrors the AWF JSON config structure 1:1, preserving the drift-tracking guarantee documented in `specs/awf-config-sources-spec.md`. ### Alternatives Considered @@ -39,7 +39,7 @@ The compiler could inspect the host of `OPENAI_BASE_URL` and, when it matches an - `authHeader` is independent of host overrides, so workflows that need a custom header against the public provider host (or against an already-configured host) can express that without redundant configuration. #### Negative -- The workflow frontmatter schema grows by a new nested block (`awf.apiProxy.targets.{openai,anthropic}.authHeader`); the regenerated `main_workflow_schema.json` adds ~1.9k net lines, increasing the surface that schema-based tooling must scan. +- The workflow frontmatter schema grows by a new nested block (`sandbox.agent.apiProxy.targets.{openai,anthropic}.authHeader`); the regenerated `main_workflow_schema.json` adds ~1.9k net lines, increasing the surface that schema-based tooling must scan. - Two truths must be kept in sync: a workflow can configure `authHeader: api-key` without setting a custom host, which is valid but easy to misuse if the public provider rejects the non-standard header. - The new helper `extractAPITargetAuthHeader` is a per-provider lookup that traverses the same frontmatter path that future per-target fields (e.g. timeouts, retries) would also walk; we accept this duplication for now rather than building a generic per-target extractor. @@ -56,23 +56,23 @@ The compiler could inspect the host of `OPENAI_BASE_URL` and, when it matches an ### Frontmatter Schema -1. The workflow frontmatter **MUST** accept an optional top-level `awf` object whose `apiProxy.targets..authHeader` path holds the custom authentication header name. -2. The set of recognized providers under `awf.apiProxy.targets` **MUST** be restricted via `additionalProperties: false`; unknown provider keys **MUST** be rejected at schema-validation time. +1. The workflow frontmatter **MUST** accept an optional `sandbox.agent.apiProxy.targets..authHeader` path that holds the custom authentication header name. +2. The set of recognized providers under `sandbox.agent.apiProxy.targets` **MUST** be restricted via `additionalProperties: false`; unknown provider keys **MUST** be rejected at schema-validation time. 3. The value of `authHeader` **MUST** be a string; non-string values (including numbers, booleans, arrays, and objects) **MUST** be rejected at schema-validation time. 4. Workflows **MAY** set `authHeader` without configuring a custom host for the same provider; the two settings **MUST** be independent. ### Compiler Behavior -1. `BuildAWFConfigJSON` **MUST** read `awf.apiProxy.targets..authHeader` from `WorkflowData.RawFrontmatter` for each supported provider and apply it to the emitted AWF JSON config. +1. `BuildAWFConfigJSON` **MUST** read `sandbox.agent.apiProxy.targets..authHeader` from `WorkflowData.SandboxConfig` for each supported provider and apply it to the emitted AWF JSON config. 2. When a target entry already exists for a provider (e.g. because a custom host was configured), the compiler **MUST** mutate the existing entry in place rather than overwriting it; the host and `authHeader` fields **MUST** coexist. 3. When no target entry exists for a provider, the compiler **MUST** create a header-only entry containing only `authHeader`, leaving `host` unset. 4. The compiler **MUST NOT** emit an `authHeader` field in the AWF JSON output when the frontmatter value is absent, empty, or non-string. -5. The frontmatter-extraction helper **MUST** return an empty string (and the compiler **MUST** treat that as "not configured") when any of the following hold: the frontmatter is `nil`; the `awf` key is missing or not an object; the `apiProxy`, `targets`, or `` keys are missing or not objects; the `authHeader` key is missing or not a string. +5. The frontmatter-extraction helper **MUST** return an empty string (and the compiler **MUST** treat that as "not configured") when any of the following hold: `WorkflowData` is `nil`; `SandboxConfig` or `Agent` is `nil`; `APIProxy` or `Targets` is `nil`; the provider key is absent; or `authHeader` is empty. ### Drift Tracking -1. `specs/awf-config-sources-spec.md` **MUST** list every `awf.*` frontmatter path that maps to an AWF JSON config field or AWF CLI flag, including `apiProxy.targets.openai.authHeader` and `apiProxy.targets.anthropic.authHeader`. -2. Any future addition of a per-target proxy field to the AWF JSON config **SHOULD** be exposed via the parallel `awf.apiProxy.targets..` frontmatter path and **MUST** be recorded in the drift-tracking table. +1. `specs/awf-config-sources-spec.md` **MUST** list every frontmatter path that maps to an AWF JSON config field or AWF CLI flag, including `sandbox.agent.apiProxy.targets.openai.authHeader` and `sandbox.agent.apiProxy.targets.anthropic.authHeader`. +2. Any future addition of a per-target proxy field to the AWF JSON config **SHOULD** be exposed via the parallel `sandbox.agent.apiProxy.targets..` frontmatter path and **MUST** be recorded in the drift-tracking table. ### Conformance diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index b95e18d6b07..4dea8713a43 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -1438,18 +1438,20 @@ func TestMainWorkflowSchema_GitHubAllowedSupportsToolCallLimits(t *testing.T) { } // TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets verifies that -// the awf.apiProxy.targets frontmatter section is validated by the schema, accepting valid -// authHeader strings and rejecting non-string values. +// the sandbox.agent.apiProxy.targets frontmatter section is validated by the schema, accepting +// valid authHeader strings and rejecting non-string values. func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets(t *testing.T) { t.Run("valid string authHeader for openai is accepted", func(t *testing.T) { frontmatter := map[string]any{ "on": "push", "engine": "codex", - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "openai": map[string]any{ - "authHeader": "api-key", + "sandbox": map[string]any{ + "agent": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": "api-key", + }, }, }, }, @@ -1465,11 +1467,13 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets frontmatter := map[string]any{ "on": "push", "engine": "claude", - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "anthropic": map[string]any{ - "authHeader": "api-key", + "sandbox": map[string]any{ + "agent": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "anthropic": map[string]any{ + "authHeader": "api-key", + }, }, }, }, @@ -1485,11 +1489,13 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets frontmatter := map[string]any{ "on": "push", "engine": "codex", - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "openai": map[string]any{ - "authHeader": 42, + "sandbox": map[string]any{ + "agent": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": 42, + }, }, }, }, @@ -1505,11 +1511,13 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets frontmatter := map[string]any{ "on": "push", "engine": "codex", - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "unknown-provider": map[string]any{ - "authHeader": "api-key", + "sandbox": map[string]any{ + "agent": map[string]any{ + "apiProxy": map[string]any{ + "targets": map[string]any{ + "unknown-provider": map[string]any{ + "authHeader": "api-key", + }, }, }, }, @@ -1517,7 +1525,7 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets } err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-unknown-provider-test.md") if err == nil { - t.Error("unknown provider in awf.apiProxy.targets should be rejected") + t.Error("unknown provider in sandbox.agent.apiProxy.targets should be rejected") } }) } diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 45850290327..eff1610224c 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3906,6 +3906,26 @@ } }, "additionalProperties": false + }, + "apiProxy": { + "type": "object", + "description": "API proxy configuration for LLM provider routing. Settings are compiled into the AWF config JSON.", + "properties": { + "targets": { + "type": "object", + "description": "Per-provider API proxy target overrides.", + "properties": { + "openai": { + "$ref": "#/$defs/sandbox_agent_api_target" + }, + "anthropic": { + "$ref": "#/$defs/sandbox_agent_api_target" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -11651,33 +11671,6 @@ } } ] - }, - "awf": { - "type": "object", - "description": "AWF (Agent Workflow Firewall) declarative configuration. Settings here are compiled into the AWF config JSON and control API proxy routing and authentication for LLM providers in AWF-enabled workflows.", - "properties": { - "apiProxy": { - "type": "object", - "description": "API proxy configuration for LLM provider routing.", - "properties": { - "targets": { - "type": "object", - "description": "Per-provider API proxy target overrides.", - "properties": { - "openai": { - "$ref": "#/$defs/awf_provider_target" - }, - "anthropic": { - "$ref": "#/$defs/awf_provider_target" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false } }, "additionalProperties": false, @@ -13701,13 +13694,13 @@ } ] }, - "awf_provider_target": { + "sandbox_agent_api_target": { "type": "object", "description": "AWF API proxy target configuration for a single LLM provider.", "properties": { "authHeader": { "type": "string", - "description": "Custom authentication header name to use when forwarding requests to this provider's API. When set, the raw API key is sent as ': ' instead of the provider default ('Authorization: Bearer' for OpenAI, 'x-api-key' for Anthropic). Example: 'api-key' for Azure OpenAI." + "description": "Custom authentication header name to use when forwarding requests to this provider's API. Overrides the provider default (\"Authorization\" for OpenAI, \"x-api-key\" for Anthropic). Example: \"api-key\" for Azure OpenAI gateways." } }, "additionalProperties": false diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index cec6610a755..dda6842038a 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -309,15 +309,11 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { awfConfigLog.Printf("API proxy: custom anthropic target=%s", anthropicTarget) } - // Apply authHeader overrides from awf.apiProxy.targets frontmatter. + // Apply authHeader overrides from sandbox.agent.apiProxy.targets frontmatter. // These are independent of the host/env-var settings: authHeader can be set // even when no custom host is configured. - var rawFrontmatter map[string]any - if config.WorkflowData != nil { - rawFrontmatter = config.WorkflowData.RawFrontmatter - } for _, provider := range []string{"openai", "anthropic"} { - authHeader := extractAPITargetAuthHeader(rawFrontmatter, provider) + authHeader := extractAPITargetAuthHeader(config.WorkflowData, provider) if authHeader == "" { continue } diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index 2704146abf9..e4f212b6d0f 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -455,7 +455,7 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.NotContains(t, jsonStr, "\\u0026", "JSON output should not HTML-escape '&' characters") }) - t.Run("openai authHeader from frontmatter awf.apiProxy.targets is included", func(t *testing.T) { + t.Run("openai authHeader from frontmatter sandbox.agent.apiProxy.targets is included", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "codex", AllowedDomains: "github.com", @@ -464,13 +464,11 @@ func TestBuildAWFConfigJSON(t *testing.T) { NetworkPermissions: &NetworkPermissions{ Firewall: &FirewallConfig{Enabled: true}, }, - RawFrontmatter: map[string]any{ - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "openai": map[string]any{ - "authHeader": "api-key", - }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + APIProxy: &AgentAPIProxyConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "openai": {AuthHeader: "api-key"}, }, }, }, @@ -485,7 +483,7 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.NotContains(t, jsonStr, `"host":""`, "should not emit empty host when only authHeader is set") }) - t.Run("anthropic authHeader from frontmatter awf.apiProxy.targets is included", func(t *testing.T) { + t.Run("anthropic authHeader from frontmatter sandbox.agent.apiProxy.targets is included", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "claude", AllowedDomains: "github.com", @@ -494,13 +492,11 @@ func TestBuildAWFConfigJSON(t *testing.T) { NetworkPermissions: &NetworkPermissions{ Firewall: &FirewallConfig{Enabled: true}, }, - RawFrontmatter: map[string]any{ - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "anthropic": map[string]any{ - "authHeader": "api-key", - }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + APIProxy: &AgentAPIProxyConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "anthropic": {AuthHeader: "api-key"}, }, }, }, @@ -529,13 +525,11 @@ func TestBuildAWFConfigJSON(t *testing.T) { NetworkPermissions: &NetworkPermissions{ Firewall: &FirewallConfig{Enabled: true}, }, - RawFrontmatter: map[string]any{ - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "openai": map[string]any{ - "authHeader": "api-key", - }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + APIProxy: &AgentAPIProxyConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + "openai": {AuthHeader: "api-key"}, }, }, }, diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 90036343603..04691d478bc 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -273,73 +273,61 @@ func TestAWFCustomAPITargetFlags(t *testing.T) { } // TestExtractAPITargetAuthHeader tests the extractAPITargetAuthHeader function that reads -// the custom auth header name from awf.apiProxy.targets..authHeader in frontmatter. +// the custom auth header name from sandbox.agent.apiProxy.targets..authHeader in frontmatter. func TestExtractAPITargetAuthHeader(t *testing.T) { - t.Run("returns authHeader for openai provider", func(t *testing.T) { - raw := map[string]any{ - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "openai": map[string]any{ - "authHeader": "api-key", + makeWorkflowData := func(provider, authHeader string) *WorkflowData { + return &WorkflowData{ + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + APIProxy: &AgentAPIProxyConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{ + provider: {AuthHeader: authHeader}, }, }, }, }, } - result := extractAPITargetAuthHeader(raw, "openai") + } + + t.Run("returns authHeader for openai provider", func(t *testing.T) { + result := extractAPITargetAuthHeader(makeWorkflowData("openai", "api-key"), "openai") assert.Equal(t, "api-key", result) }) t.Run("returns authHeader for anthropic provider", func(t *testing.T) { - raw := map[string]any{ - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "anthropic": map[string]any{ - "authHeader": "x-custom-header", - }, - }, - }, - }, - } - result := extractAPITargetAuthHeader(raw, "anthropic") + result := extractAPITargetAuthHeader(makeWorkflowData("anthropic", "x-custom-header"), "anthropic") assert.Equal(t, "x-custom-header", result) }) - t.Run("returns empty string when awf key is absent", func(t *testing.T) { - raw := map[string]any{"engine": "codex"} - assert.Empty(t, extractAPITargetAuthHeader(raw, "openai")) + t.Run("returns empty string when sandbox config is absent", func(t *testing.T) { + wd := &WorkflowData{EngineConfig: &EngineConfig{ID: "codex"}} + assert.Empty(t, extractAPITargetAuthHeader(wd, "openai")) }) t.Run("returns empty string when provider is absent", func(t *testing.T) { - raw := map[string]any{ - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{}, + wd := &WorkflowData{ + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + APIProxy: &AgentAPIProxyConfig{ + Targets: map[string]*AgentAPIProxyTargetConfig{}, + }, }, }, } - assert.Empty(t, extractAPITargetAuthHeader(raw, "openai")) + assert.Empty(t, extractAPITargetAuthHeader(wd, "openai")) }) - t.Run("returns empty string for nil frontmatter", func(t *testing.T) { + t.Run("returns empty string for nil WorkflowData", func(t *testing.T) { assert.Empty(t, extractAPITargetAuthHeader(nil, "openai")) }) - t.Run("returns empty string when authHeader value is not a string", func(t *testing.T) { - raw := map[string]any{ - "awf": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "openai": map[string]any{ - "authHeader": 42, - }, - }, - }, + t.Run("returns empty string when apiProxy is nil", func(t *testing.T) { + wd := &WorkflowData{ + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{}, }, } - assert.Empty(t, extractAPITargetAuthHeader(raw, "openai")) + assert.Empty(t, extractAPITargetAuthHeader(wd, "openai")) }) } diff --git a/pkg/workflow/engine_api_targets.go b/pkg/workflow/engine_api_targets.go index eba41b96d89..5dee5cecb73 100644 --- a/pkg/workflow/engine_api_targets.go +++ b/pkg/workflow/engine_api_targets.go @@ -108,58 +108,25 @@ func extractAPIBasePath(workflowData *WorkflowData, envVar string) string { return "" } -// extractAPITargetAuthHeader extracts the authHeader value from the awf frontmatter section -// for a given provider (e.g. "openai" or "anthropic"). It reads the following frontmatter path: +// extractAPITargetAuthHeader extracts the authHeader value from the sandbox.agent.apiProxy +// frontmatter section for a given provider (e.g. "openai" or "anthropic"). It reads: // -// awf.apiProxy.targets..authHeader +// sandbox.agent.apiProxy.targets..authHeader // // Returns the header name string (e.g. "api-key") or empty string if not configured. -// A non-string value is treated as absent and returns empty string. -func extractAPITargetAuthHeader(rawFrontmatter map[string]any, provider string) string { - if rawFrontmatter == nil { +func extractAPITargetAuthHeader(workflowData *WorkflowData, provider string) string { + if workflowData == nil || workflowData.SandboxConfig == nil || workflowData.SandboxConfig.Agent == nil { return "" } - awfAny, ok := rawFrontmatter["awf"] - if !ok { + apiProxy := workflowData.SandboxConfig.Agent.APIProxy + if apiProxy == nil || apiProxy.Targets == nil { return "" } - awfMap, ok := awfAny.(map[string]any) - if !ok { + target, ok := apiProxy.Targets[provider] + if !ok || target == nil { return "" } - apiProxyAny, ok := awfMap["apiProxy"] - if !ok { - return "" - } - apiProxyMap, ok := apiProxyAny.(map[string]any) - if !ok { - return "" - } - targetsAny, ok := apiProxyMap["targets"] - if !ok { - return "" - } - targetsMap, ok := targetsAny.(map[string]any) - if !ok { - return "" - } - providerAny, ok := targetsMap[provider] - if !ok { - return "" - } - providerMap, ok := providerAny.(map[string]any) - if !ok { - return "" - } - authHeaderAny, ok := providerMap["authHeader"] - if !ok { - return "" - } - authHeader, ok := authHeaderAny.(string) - if !ok { - return "" - } - return authHeader + return target.AuthHeader } // GetCopilotAPITarget returns the effective Copilot API target hostname, checking in order: diff --git a/pkg/workflow/sandbox.go b/pkg/workflow/sandbox.go index 525926167a5..0733a50b5f8 100644 --- a/pkg/workflow/sandbox.go +++ b/pkg/workflow/sandbox.go @@ -56,6 +56,22 @@ type AgentSandboxConfig struct { Env map[string]string `yaml:"env,omitempty"` // Environment variables to set on the step Mounts []string `yaml:"mounts,omitempty"` // Container mounts to add for AWF (format: "source:dest:mode") Memory string `yaml:"memory,omitempty"` // Memory limit for the AWF container (e.g., "4g", "8g") + APIProxy *AgentAPIProxyConfig `yaml:"apiProxy,omitempty"` // API proxy configuration for LLM provider routing +} + +// AgentAPIProxyConfig holds per-provider API proxy target overrides declared in +// sandbox.agent.apiProxy frontmatter. These are compiled into the AWF config JSON. +type AgentAPIProxyConfig struct { + Targets map[string]*AgentAPIProxyTargetConfig `yaml:"targets,omitempty"` // Per-provider target overrides keyed by provider name (e.g. "openai", "anthropic") +} + +// AgentAPIProxyTargetConfig configures a single LLM provider's API proxy target. +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"` } // SandboxRuntimeConfig represents the Anthropic Sandbox Runtime configuration diff --git a/specs/awf-config-sources-spec.md b/specs/awf-config-sources-spec.md index bb9c6b31392..d85651eee20 100644 --- a/specs/awf-config-sources-spec.md +++ b/specs/awf-config-sources-spec.md @@ -60,8 +60,8 @@ The following fields previously existed in schema but were missed in spec CLI ma | `apiProxy.modelMultipliers` | config-only (effective-token accounting) | | `apiProxy.maxRuns` | config-only (LLM invocation hard cap) | | `apiProxy.auth.*` | config-only (maps to `AWF_AUTH_*` env vars) | -| `apiProxy.targets.openai.authHeader` | `--openai-api-auth-header` (frontmatter: `awf.apiProxy.targets.openai.authHeader`) | -| `apiProxy.targets.anthropic.authHeader` | `--anthropic-api-auth-header` (frontmatter: `awf.apiProxy.targets.anthropic.authHeader`) | +| `apiProxy.targets.openai.authHeader` | `--openai-api-auth-header` (frontmatter: `sandbox.agent.apiProxy.targets.openai.authHeader`) | +| `apiProxy.targets.anthropic.authHeader` | `--anthropic-api-auth-header` (frontmatter: `sandbox.agent.apiProxy.targets.anthropic.authHeader`) | | `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 c400b5812cc8cd1ada3e6351f43ac7442a68c27a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 14:38:45 +0000 Subject: [PATCH 7/8] refactor: rename sandbox.agent.apiProxy.targets to sandbox.agent.targets Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...pose-authheader-in-awf-apiproxy-targets.md | 18 +++++----- pkg/parser/schema_test.go | 36 ++++++++----------- pkg/parser/schemas/main_workflow_schema.json | 21 ++++------- pkg/workflow/awf_config.go | 2 +- pkg/workflow/awf_config_test.go | 22 +++++------- pkg/workflow/awf_helpers_test.go | 14 +++----- pkg/workflow/engine_api_targets.go | 10 +++--- pkg/workflow/sandbox.go | 10 ++---- specs/awf-config-sources-spec.md | 4 +-- 9 files changed, 53 insertions(+), 84 deletions(-) diff --git a/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md b/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md index a907a6ae4a7..75ee00c97bc 100644 --- a/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md +++ b/docs/adr/35694-expose-authheader-in-awf-apiproxy-targets.md @@ -1,4 +1,4 @@ -# ADR-35694: Expose `authHeader` in `sandbox.agent.apiProxy.targets` Frontmatter +# ADR-35694: Expose `authHeader` in `sandbox.agent.targets` Frontmatter **Date**: 2026-05-29 **Status**: Draft @@ -14,7 +14,7 @@ The AWF firewall sidecar (PR #3998) introduced `--openai-api-auth-header` and `- ### Decision -We will expose `authHeader` as a frontmatter field at `sandbox.agent.apiProxy.targets..authHeader` for `provider ∈ {openai, anthropic}`. The new field is read by a dedicated helper `extractAPITargetAuthHeader` (in `pkg/workflow/engine_api_targets.go`) and applied inside `BuildAWFConfigJSON` (in `pkg/workflow/awf_config.go`) by mutating the existing `AWFAPITargetConfig` entry when one is already present, or creating a header-only entry when no host override exists. The field is emitted with `omitempty` so the generated AWF JSON stays clean when it is not configured. The frontmatter path mirrors the AWF JSON config structure 1:1, preserving the drift-tracking guarantee documented in `specs/awf-config-sources-spec.md`. +We will expose `authHeader` as a frontmatter field at `sandbox.agent.targets..authHeader` for `provider ∈ {openai, anthropic}`. The new field is read by a dedicated helper `extractAPITargetAuthHeader` (in `pkg/workflow/engine_api_targets.go`) and applied inside `BuildAWFConfigJSON` (in `pkg/workflow/awf_config.go`) by mutating the existing `AWFAPITargetConfig` entry when one is already present, or creating a header-only entry when no host override exists. The field is emitted with `omitempty` so the generated AWF JSON stays clean when it is not configured. The frontmatter path mirrors the AWF JSON config structure 1:1, preserving the drift-tracking guarantee documented in `specs/awf-config-sources-spec.md`. ### Alternatives Considered @@ -39,7 +39,7 @@ The compiler could inspect the host of `OPENAI_BASE_URL` and, when it matches an - `authHeader` is independent of host overrides, so workflows that need a custom header against the public provider host (or against an already-configured host) can express that without redundant configuration. #### Negative -- The workflow frontmatter schema grows by a new nested block (`sandbox.agent.apiProxy.targets.{openai,anthropic}.authHeader`); the regenerated `main_workflow_schema.json` adds ~1.9k net lines, increasing the surface that schema-based tooling must scan. +- The workflow frontmatter schema grows by a new nested block (`sandbox.agent.targets.{openai,anthropic}.authHeader`); the regenerated `main_workflow_schema.json` adds ~1.9k net lines, increasing the surface that schema-based tooling must scan. - Two truths must be kept in sync: a workflow can configure `authHeader: api-key` without setting a custom host, which is valid but easy to misuse if the public provider rejects the non-standard header. - The new helper `extractAPITargetAuthHeader` is a per-provider lookup that traverses the same frontmatter path that future per-target fields (e.g. timeouts, retries) would also walk; we accept this duplication for now rather than building a generic per-target extractor. @@ -56,23 +56,23 @@ The compiler could inspect the host of `OPENAI_BASE_URL` and, when it matches an ### Frontmatter Schema -1. The workflow frontmatter **MUST** accept an optional `sandbox.agent.apiProxy.targets..authHeader` path that holds the custom authentication header name. -2. The set of recognized providers under `sandbox.agent.apiProxy.targets` **MUST** be restricted via `additionalProperties: false`; unknown provider keys **MUST** be rejected at schema-validation time. +1. The workflow frontmatter **MUST** accept an optional `sandbox.agent.targets..authHeader` path that holds the custom authentication header name. +2. The set of recognized providers under `sandbox.agent.targets` **MUST** be restricted via `additionalProperties: false`; unknown provider keys **MUST** be rejected at schema-validation time. 3. The value of `authHeader` **MUST** be a string; non-string values (including numbers, booleans, arrays, and objects) **MUST** be rejected at schema-validation time. 4. Workflows **MAY** set `authHeader` without configuring a custom host for the same provider; the two settings **MUST** be independent. ### Compiler Behavior -1. `BuildAWFConfigJSON` **MUST** read `sandbox.agent.apiProxy.targets..authHeader` from `WorkflowData.SandboxConfig` for each supported provider and apply it to the emitted AWF JSON config. +1. `BuildAWFConfigJSON` **MUST** read `sandbox.agent.targets..authHeader` from `WorkflowData.SandboxConfig` for each supported provider and apply it to the emitted AWF JSON config. 2. When a target entry already exists for a provider (e.g. because a custom host was configured), the compiler **MUST** mutate the existing entry in place rather than overwriting it; the host and `authHeader` fields **MUST** coexist. 3. When no target entry exists for a provider, the compiler **MUST** create a header-only entry containing only `authHeader`, leaving `host` unset. 4. The compiler **MUST NOT** emit an `authHeader` field in the AWF JSON output when the frontmatter value is absent, empty, or non-string. -5. The frontmatter-extraction helper **MUST** return an empty string (and the compiler **MUST** treat that as "not configured") when any of the following hold: `WorkflowData` is `nil`; `SandboxConfig` or `Agent` is `nil`; `APIProxy` or `Targets` is `nil`; the provider key is absent; or `authHeader` is empty. +5. The frontmatter-extraction helper **MUST** return an empty string (and the compiler **MUST** treat that as "not configured") when any of the following hold: `WorkflowData` is `nil`; `SandboxConfig` or `Agent` is `nil`; `Targets` is `nil`; the provider key is absent; or `authHeader` is empty. ### Drift Tracking -1. `specs/awf-config-sources-spec.md` **MUST** list every frontmatter path that maps to an AWF JSON config field or AWF CLI flag, including `sandbox.agent.apiProxy.targets.openai.authHeader` and `sandbox.agent.apiProxy.targets.anthropic.authHeader`. -2. Any future addition of a per-target proxy field to the AWF JSON config **SHOULD** be exposed via the parallel `sandbox.agent.apiProxy.targets..` frontmatter path and **MUST** be recorded in the drift-tracking table. +1. `specs/awf-config-sources-spec.md` **MUST** list every frontmatter path that maps to an AWF JSON config field or AWF CLI flag, including `sandbox.agent.targets.openai.authHeader` and `sandbox.agent.targets.anthropic.authHeader`. +2. Any future addition of a per-target proxy field to the AWF JSON config **SHOULD** be exposed via the parallel `sandbox.agent.targets..` frontmatter path and **MUST** be recorded in the drift-tracking table. ### Conformance diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 4dea8713a43..d16370e4272 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -1438,7 +1438,7 @@ func TestMainWorkflowSchema_GitHubAllowedSupportsToolCallLimits(t *testing.T) { } // TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets verifies that -// the sandbox.agent.apiProxy.targets frontmatter section is validated by the schema, accepting +// the sandbox.agent.targets frontmatter section is validated by the schema, accepting // valid authHeader strings and rejecting non-string values. func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets(t *testing.T) { t.Run("valid string authHeader for openai is accepted", func(t *testing.T) { @@ -1447,11 +1447,9 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets "engine": "codex", "sandbox": map[string]any{ "agent": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "openai": map[string]any{ - "authHeader": "api-key", - }, + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": "api-key", }, }, }, @@ -1469,11 +1467,9 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets "engine": "claude", "sandbox": map[string]any{ "agent": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "anthropic": map[string]any{ - "authHeader": "api-key", - }, + "targets": map[string]any{ + "anthropic": map[string]any{ + "authHeader": "api-key", }, }, }, @@ -1491,11 +1487,9 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets "engine": "codex", "sandbox": map[string]any{ "agent": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "openai": map[string]any{ - "authHeader": 42, - }, + "targets": map[string]any{ + "openai": map[string]any{ + "authHeader": 42, }, }, }, @@ -1513,11 +1507,9 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets "engine": "codex", "sandbox": map[string]any{ "agent": map[string]any{ - "apiProxy": map[string]any{ - "targets": map[string]any{ - "unknown-provider": map[string]any{ - "authHeader": "api-key", - }, + "targets": map[string]any{ + "unknown-provider": map[string]any{ + "authHeader": "api-key", }, }, }, @@ -1525,7 +1517,7 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AwfApiProxyTargets } err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-unknown-provider-test.md") if err == nil { - t.Error("unknown provider in sandbox.agent.apiProxy.targets should be rejected") + t.Error("unknown provider in sandbox.agent.targets should be rejected") } }) } diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index eff1610224c..4b7b80db682 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3907,22 +3907,15 @@ }, "additionalProperties": false }, - "apiProxy": { + "targets": { "type": "object", - "description": "API proxy configuration for LLM provider routing. Settings are compiled into the AWF config JSON.", + "description": "Per-provider API proxy target overrides. Settings are compiled into the AWF config JSON.", "properties": { - "targets": { - "type": "object", - "description": "Per-provider API proxy target overrides.", - "properties": { - "openai": { - "$ref": "#/$defs/sandbox_agent_api_target" - }, - "anthropic": { - "$ref": "#/$defs/sandbox_agent_api_target" - } - }, - "additionalProperties": false + "openai": { + "$ref": "#/$defs/sandbox_agent_api_target" + }, + "anthropic": { + "$ref": "#/$defs/sandbox_agent_api_target" } }, "additionalProperties": false diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index dda6842038a..22b0914abf1 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -309,7 +309,7 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { awfConfigLog.Printf("API proxy: custom anthropic target=%s", anthropicTarget) } - // Apply authHeader overrides from sandbox.agent.apiProxy.targets frontmatter. + // Apply authHeader overrides from sandbox.agent.targets frontmatter. // These are independent of the host/env-var settings: authHeader can be set // even when no custom host is configured. for _, provider := range []string{"openai", "anthropic"} { diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index e4f212b6d0f..063abdebb29 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -455,7 +455,7 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.NotContains(t, jsonStr, "\\u0026", "JSON output should not HTML-escape '&' characters") }) - t.Run("openai authHeader from frontmatter sandbox.agent.apiProxy.targets is included", func(t *testing.T) { + t.Run("openai authHeader from frontmatter sandbox.agent.targets is included", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "codex", AllowedDomains: "github.com", @@ -466,10 +466,8 @@ func TestBuildAWFConfigJSON(t *testing.T) { }, SandboxConfig: &SandboxConfig{ Agent: &AgentSandboxConfig{ - APIProxy: &AgentAPIProxyConfig{ - Targets: map[string]*AgentAPIProxyTargetConfig{ - "openai": {AuthHeader: "api-key"}, - }, + Targets: map[string]*AgentAPIProxyTargetConfig{ + "openai": {AuthHeader: "api-key"}, }, }, }, @@ -483,7 +481,7 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.NotContains(t, jsonStr, `"host":""`, "should not emit empty host when only authHeader is set") }) - t.Run("anthropic authHeader from frontmatter sandbox.agent.apiProxy.targets is included", func(t *testing.T) { + t.Run("anthropic authHeader from frontmatter sandbox.agent.targets is included", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "claude", AllowedDomains: "github.com", @@ -494,10 +492,8 @@ func TestBuildAWFConfigJSON(t *testing.T) { }, SandboxConfig: &SandboxConfig{ Agent: &AgentSandboxConfig{ - APIProxy: &AgentAPIProxyConfig{ - Targets: map[string]*AgentAPIProxyTargetConfig{ - "anthropic": {AuthHeader: "api-key"}, - }, + Targets: map[string]*AgentAPIProxyTargetConfig{ + "anthropic": {AuthHeader: "api-key"}, }, }, }, @@ -527,10 +523,8 @@ func TestBuildAWFConfigJSON(t *testing.T) { }, SandboxConfig: &SandboxConfig{ Agent: &AgentSandboxConfig{ - APIProxy: &AgentAPIProxyConfig{ - Targets: map[string]*AgentAPIProxyTargetConfig{ - "openai": {AuthHeader: "api-key"}, - }, + Targets: map[string]*AgentAPIProxyTargetConfig{ + "openai": {AuthHeader: "api-key"}, }, }, }, diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 04691d478bc..6f9c1138e51 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -273,16 +273,14 @@ func TestAWFCustomAPITargetFlags(t *testing.T) { } // TestExtractAPITargetAuthHeader tests the extractAPITargetAuthHeader function that reads -// the custom auth header name from sandbox.agent.apiProxy.targets..authHeader in frontmatter. +// the custom auth header name from sandbox.agent.targets..authHeader in frontmatter. func TestExtractAPITargetAuthHeader(t *testing.T) { makeWorkflowData := func(provider, authHeader string) *WorkflowData { return &WorkflowData{ SandboxConfig: &SandboxConfig{ Agent: &AgentSandboxConfig{ - APIProxy: &AgentAPIProxyConfig{ - Targets: map[string]*AgentAPIProxyTargetConfig{ - provider: {AuthHeader: authHeader}, - }, + Targets: map[string]*AgentAPIProxyTargetConfig{ + provider: {AuthHeader: authHeader}, }, }, }, @@ -308,9 +306,7 @@ func TestExtractAPITargetAuthHeader(t *testing.T) { wd := &WorkflowData{ SandboxConfig: &SandboxConfig{ Agent: &AgentSandboxConfig{ - APIProxy: &AgentAPIProxyConfig{ - Targets: map[string]*AgentAPIProxyTargetConfig{}, - }, + Targets: map[string]*AgentAPIProxyTargetConfig{}, }, }, } @@ -321,7 +317,7 @@ func TestExtractAPITargetAuthHeader(t *testing.T) { assert.Empty(t, extractAPITargetAuthHeader(nil, "openai")) }) - t.Run("returns empty string when apiProxy is nil", func(t *testing.T) { + t.Run("returns empty string when targets is nil", func(t *testing.T) { wd := &WorkflowData{ SandboxConfig: &SandboxConfig{ Agent: &AgentSandboxConfig{}, diff --git a/pkg/workflow/engine_api_targets.go b/pkg/workflow/engine_api_targets.go index 5dee5cecb73..88437a48d8a 100644 --- a/pkg/workflow/engine_api_targets.go +++ b/pkg/workflow/engine_api_targets.go @@ -108,21 +108,21 @@ func extractAPIBasePath(workflowData *WorkflowData, envVar string) string { return "" } -// extractAPITargetAuthHeader extracts the authHeader value from the sandbox.agent.apiProxy +// extractAPITargetAuthHeader extracts the authHeader value from the sandbox.agent.targets // frontmatter section for a given provider (e.g. "openai" or "anthropic"). It reads: // -// sandbox.agent.apiProxy.targets..authHeader +// sandbox.agent.targets..authHeader // // Returns the header name string (e.g. "api-key") or empty string if not configured. func extractAPITargetAuthHeader(workflowData *WorkflowData, provider string) string { if workflowData == nil || workflowData.SandboxConfig == nil || workflowData.SandboxConfig.Agent == nil { return "" } - apiProxy := workflowData.SandboxConfig.Agent.APIProxy - if apiProxy == nil || apiProxy.Targets == nil { + targets := workflowData.SandboxConfig.Agent.Targets + if targets == nil { return "" } - target, ok := apiProxy.Targets[provider] + target, ok := targets[provider] if !ok || target == nil { return "" } diff --git a/pkg/workflow/sandbox.go b/pkg/workflow/sandbox.go index 0733a50b5f8..e7ef9d92138 100644 --- a/pkg/workflow/sandbox.go +++ b/pkg/workflow/sandbox.go @@ -55,14 +55,8 @@ type AgentSandboxConfig struct { Args []string `yaml:"args,omitempty"` // Additional arguments to append to the command Env map[string]string `yaml:"env,omitempty"` // Environment variables to set on the step Mounts []string `yaml:"mounts,omitempty"` // Container mounts to add for AWF (format: "source:dest:mode") - Memory string `yaml:"memory,omitempty"` // Memory limit for the AWF container (e.g., "4g", "8g") - APIProxy *AgentAPIProxyConfig `yaml:"apiProxy,omitempty"` // API proxy configuration for LLM provider routing -} - -// AgentAPIProxyConfig holds per-provider API proxy target overrides declared in -// sandbox.agent.apiProxy frontmatter. These are compiled into the AWF config JSON. -type AgentAPIProxyConfig struct { - Targets map[string]*AgentAPIProxyTargetConfig `yaml:"targets,omitempty"` // Per-provider target overrides keyed by provider name (e.g. "openai", "anthropic") + Memory string `yaml:"memory,omitempty"` // Memory limit for the AWF container (e.g., "4g", "8g") + Targets map[string]*AgentAPIProxyTargetConfig `yaml:"targets,omitempty"` // Per-provider API proxy target overrides keyed by provider name (e.g. "openai", "anthropic") } // AgentAPIProxyTargetConfig configures a single LLM provider's API proxy target. diff --git a/specs/awf-config-sources-spec.md b/specs/awf-config-sources-spec.md index d85651eee20..d58d94b4540 100644 --- a/specs/awf-config-sources-spec.md +++ b/specs/awf-config-sources-spec.md @@ -60,8 +60,8 @@ The following fields previously existed in schema but were missed in spec CLI ma | `apiProxy.modelMultipliers` | config-only (effective-token accounting) | | `apiProxy.maxRuns` | config-only (LLM invocation hard cap) | | `apiProxy.auth.*` | config-only (maps to `AWF_AUTH_*` env vars) | -| `apiProxy.targets.openai.authHeader` | `--openai-api-auth-header` (frontmatter: `sandbox.agent.apiProxy.targets.openai.authHeader`) | -| `apiProxy.targets.anthropic.authHeader` | `--anthropic-api-auth-header` (frontmatter: `sandbox.agent.apiProxy.targets.anthropic.authHeader`) | +| `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`) | | `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 8aaf50c36a5d8d52e62b5bc61cfdb4550a948cb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 17:47:24 +0000 Subject: [PATCH 8/8] fix: go fmt alignment in sandbox.go Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/sandbox.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/workflow/sandbox.go b/pkg/workflow/sandbox.go index 72a1fb212ff..91fd4cf455a 100644 --- a/pkg/workflow/sandbox.go +++ b/pkg/workflow/sandbox.go @@ -46,18 +46,18 @@ type SandboxConfig struct { // AgentSandboxConfig represents the agent sandbox configuration type AgentSandboxConfig struct { - ID string `yaml:"id,omitempty"` // Agent ID: "awf" or "srt" (replaces Type in new object format) - Type SandboxType `yaml:"type,omitempty"` // Sandbox type: "awf" or "srt" (legacy, use ID instead) - Version string `yaml:"version,omitempty"` // AWF version override used to install and run the matching firewall version - Disabled bool `yaml:"-"` // True when agent is explicitly set to false (disables firewall). This is a runtime flag, not serialized to YAML. - Config *SandboxRuntimeConfig `yaml:"config,omitempty"` // Custom SRT config (optional) - Command string `yaml:"command,omitempty"` // Custom command to replace AWF or SRT installation - Args []string `yaml:"args,omitempty"` // Additional arguments to append to the command - Env map[string]string `yaml:"env,omitempty"` // Environment variables to set on the step - Mounts []string `yaml:"mounts,omitempty"` // Container mounts to add for AWF (format: "source:dest:mode") - Memory string `yaml:"memory,omitempty"` // Memory limit for the AWF container (e.g., "4g", "8g") - ModelFallback *TemplatableBool `yaml:"model-fallback,omitempty"` // AWF API proxy model fallback enable/disable flag (optional) - Targets map[string]*AgentAPIProxyTargetConfig `yaml:"targets,omitempty"` // Per-provider API proxy target overrides keyed by provider name (e.g. "openai", "anthropic") + ID string `yaml:"id,omitempty"` // Agent ID: "awf" or "srt" (replaces Type in new object format) + Type SandboxType `yaml:"type,omitempty"` // Sandbox type: "awf" or "srt" (legacy, use ID instead) + Version string `yaml:"version,omitempty"` // AWF version override used to install and run the matching firewall version + Disabled bool `yaml:"-"` // True when agent is explicitly set to false (disables firewall). This is a runtime flag, not serialized to YAML. + Config *SandboxRuntimeConfig `yaml:"config,omitempty"` // Custom SRT config (optional) + Command string `yaml:"command,omitempty"` // Custom command to replace AWF or SRT installation + Args []string `yaml:"args,omitempty"` // Additional arguments to append to the command + Env map[string]string `yaml:"env,omitempty"` // Environment variables to set on the step + Mounts []string `yaml:"mounts,omitempty"` // Container mounts to add for AWF (format: "source:dest:mode") + Memory string `yaml:"memory,omitempty"` // Memory limit for the AWF container (e.g., "4g", "8g") + ModelFallback *TemplatableBool `yaml:"model-fallback,omitempty"` // AWF API proxy model fallback enable/disable flag (optional) + Targets map[string]*AgentAPIProxyTargetConfig `yaml:"targets,omitempty"` // Per-provider API proxy target overrides keyed by provider name (e.g. "openai", "anthropic") } // AgentAPIProxyTargetConfig configures a single LLM provider's API proxy target.