diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index ecc885b6a4..e4691d323b 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -1,30 +1,45 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", - "required": ["on"], + "required": [ + "on" + ], "properties": { "name": { "type": "string", "minLength": 1, "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", "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" + ] }, "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" + ] }, "tracker-id": { "type": "string", "minLength": 8, "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", @@ -34,9 +49,18 @@ "minLength": 1 }, "examples": [ - ["automation", "security"], - ["docs", "maintenance"], - ["ci", "testing"] + [ + "automation", + "security" + ], + [ + "docs", + "maintenance" + ], + [ + "ci", + "testing" + ] ] }, "metadata": { @@ -70,7 +94,9 @@ { "type": "object", "description": "Import specification with path and optional inputs", - "required": ["path"], + "required": [ + "path" + ], "additionalProperties": false, "properties": { "path": { @@ -99,10 +125,21 @@ ] }, "examples": [ - ["shared/jqschema.md", "shared/reporting.md"], - ["shared/mcp/gh-aw.md", "shared/jqschema.md", "shared/reporting.md"], - ["../instructions/documentation.instructions.md"], - [".github/agents/my-agent.md"], + [ + "shared/jqschema.md", + "shared/reporting.md" + ], + [ + "shared/mcp/gh-aw.md", + "shared/jqschema.md", + "shared/reporting.md" + ], + [ + "../instructions/documentation.instructions.md" + ], + [ + ".github/agents/my-agent.md" + ], [ { "path": "shared/discussions-data-fetch.md", @@ -120,7 +157,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", @@ -155,7 +198,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", @@ -164,7 +216,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" + ] } } ] @@ -203,7 +264,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", @@ -212,7 +282,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" + ] } } ] @@ -276,16 +355,39 @@ }, "oneOf": [ { - "required": ["branches"], - "not": { "required": ["branches-ignore"] } + "required": [ + "branches" + ], + "not": { + "required": [ + "branches-ignore" + ] + } }, { - "required": ["branches-ignore"], - "not": { "required": ["branches"] } + "required": [ + "branches-ignore" + ], + "not": { + "required": [ + "branches" + ] + } }, { "not": { - "anyOf": [{ "required": ["branches"] }, { "required": ["branches-ignore"] }] + "anyOf": [ + { + "required": [ + "branches" + ] + }, + { + "required": [ + "branches-ignore" + ] + } + ] } } ], @@ -293,16 +395,39 @@ { "oneOf": [ { - "required": ["paths"], - "not": { "required": ["paths-ignore"] } + "required": [ + "paths" + ], + "not": { + "required": [ + "paths-ignore" + ] + } }, { - "required": ["paths-ignore"], - "not": { "required": ["paths"] } + "required": [ + "paths-ignore" + ], + "not": { + "required": [ + "paths" + ] + } }, { "not": { - "anyOf": [{ "required": ["paths"] }, { "required": ["paths-ignore"] }] + "anyOf": [ + { + "required": [ + "paths" + ] + }, + { + "required": [ + "paths-ignore" + ] + } + ] } } ] @@ -417,16 +542,39 @@ "additionalProperties": false, "oneOf": [ { - "required": ["branches"], - "not": { "required": ["branches-ignore"] } + "required": [ + "branches" + ], + "not": { + "required": [ + "branches-ignore" + ] + } }, { - "required": ["branches-ignore"], - "not": { "required": ["branches"] } + "required": [ + "branches-ignore" + ], + "not": { + "required": [ + "branches" + ] + } }, { "not": { - "anyOf": [{ "required": ["branches"] }, { "required": ["branches-ignore"] }] + "anyOf": [ + { + "required": [ + "branches" + ] + }, + { + "required": [ + "branches-ignore" + ] + } + ] } } ], @@ -434,16 +582,39 @@ { "oneOf": [ { - "required": ["paths"], - "not": { "required": ["paths-ignore"] } + "required": [ + "paths" + ], + "not": { + "required": [ + "paths-ignore" + ] + } }, { - "required": ["paths-ignore"], - "not": { "required": ["paths"] } + "required": [ + "paths-ignore" + ], + "not": { + "required": [ + "paths" + ] + } }, { "not": { - "anyOf": [{ "required": ["paths"] }, { "required": ["paths-ignore"] }] + "anyOf": [ + { + "required": [ + "paths" + ] + }, + { + "required": [ + "paths-ignore" + ] + } + ] } } ] @@ -460,7 +631,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": { @@ -496,7 +686,11 @@ "description": "Types of issue comment events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } }, "lock-for-agent": { @@ -515,7 +709,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" + ] } } } @@ -530,7 +738,11 @@ "description": "Types of discussion comment events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } } } @@ -555,7 +767,9 @@ "description": "Cron expression using standard format (e.g., '0 9 * * 1') or human-friendly format (e.g., 'daily at 02:00', 'daily at 3pm', 'daily at 6am', 'weekly on monday', 'weekly on friday at 5pm', 'every 10 minutes', 'every 2h', 'daily at 02:00 utc+9', 'daily at 3pm utc+9'). Human-friendly formats support: daily/weekly/monthly schedules with optional time, interval schedules (minimum 5 minutes), short duration units (m/h/d/w/mo), 12-hour time format (Npm/Nam where N is 1-12), and UTC timezone offsets (utc+N or utc+HH:MM)." } }, - "required": ["cron"], + "required": [ + "cron" + ], "additionalProperties": false } } @@ -594,7 +808,11 @@ }, "type": { "type": "string", - "enum": ["string", "choice", "boolean"], + "enum": [ + "string", + "choice", + "boolean" + ], "description": "Input type" }, "options": { @@ -628,7 +846,11 @@ "description": "Types of workflow run events", "items": { "type": "string", - "enum": ["completed", "requested", "in_progress"] + "enum": [ + "completed", + "requested", + "in_progress" + ] } }, "branches": { @@ -650,16 +872,39 @@ }, "oneOf": [ { - "required": ["branches"], - "not": { "required": ["branches-ignore"] } + "required": [ + "branches" + ], + "not": { + "required": [ + "branches-ignore" + ] + } }, { - "required": ["branches-ignore"], - "not": { "required": ["branches"] } + "required": [ + "branches-ignore" + ], + "not": { + "required": [ + "branches" + ] + } }, { "not": { - "anyOf": [{ "required": ["branches"] }, { "required": ["branches-ignore"] }] + "anyOf": [ + { + "required": [ + "branches" + ] + }, + { + "required": [ + "branches-ignore" + ] + } + ] } } ] @@ -674,7 +919,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" + ] } } } @@ -689,7 +942,11 @@ "description": "Types of pull request review comment events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } } } @@ -704,7 +961,11 @@ "description": "Types of branch protection rule events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } } } @@ -719,7 +980,12 @@ "description": "Types of check run events", "items": { "type": "string", - "enum": ["created", "rerequested", "completed", "requested_action"] + "enum": [ + "created", + "rerequested", + "completed", + "requested_action" + ] } } } @@ -734,7 +1000,9 @@ "description": "Types of check suite events", "items": { "type": "string", - "enum": ["completed"] + "enum": [ + "completed" + ] } } } @@ -827,7 +1095,11 @@ "description": "Types of label events", "items": { "type": "string", - "enum": ["created", "edited", "deleted"] + "enum": [ + "created", + "edited", + "deleted" + ] } } } @@ -842,7 +1114,9 @@ "description": "Types of merge group events", "items": { "type": "string", - "enum": ["checks_requested"] + "enum": [ + "checks_requested" + ] } } } @@ -857,7 +1131,13 @@ "description": "Types of milestone events", "items": { "type": "string", - "enum": ["created", "closed", "opened", "edited", "deleted"] + "enum": [ + "created", + "closed", + "opened", + "edited", + "deleted" + ] } } } @@ -974,16 +1254,39 @@ "additionalProperties": false, "oneOf": [ { - "required": ["branches"], - "not": { "required": ["branches-ignore"] } + "required": [ + "branches" + ], + "not": { + "required": [ + "branches-ignore" + ] + } }, { - "required": ["branches-ignore"], - "not": { "required": ["branches"] } + "required": [ + "branches-ignore" + ], + "not": { + "required": [ + "branches" + ] + } }, { "not": { - "anyOf": [{ "required": ["branches"] }, { "required": ["branches-ignore"] }] + "anyOf": [ + { + "required": [ + "branches" + ] + }, + { + "required": [ + "branches-ignore" + ] + } + ] } } ], @@ -991,16 +1294,39 @@ { "oneOf": [ { - "required": ["paths"], - "not": { "required": ["paths-ignore"] } + "required": [ + "paths" + ], + "not": { + "required": [ + "paths-ignore" + ] + } }, { - "required": ["paths-ignore"], - "not": { "required": ["paths"] } + "required": [ + "paths-ignore" + ], + "not": { + "required": [ + "paths" + ] + } }, { "not": { - "anyOf": [{ "required": ["paths"] }, { "required": ["paths-ignore"] }] + "anyOf": [ + { + "required": [ + "paths" + ] + }, + { + "required": [ + "paths-ignore" + ] + } + ] } } ] @@ -1017,7 +1343,11 @@ "description": "Types of pull request review events", "items": { "type": "string", - "enum": ["submitted", "edited", "dismissed"] + "enum": [ + "submitted", + "edited", + "dismissed" + ] } } } @@ -1032,7 +1362,10 @@ "description": "Types of registry package events", "items": { "type": "string", - "enum": ["published", "updated"] + "enum": [ + "published", + "updated" + ] } } } @@ -1074,7 +1407,9 @@ "description": "Types of watch events", "items": { "type": "string", - "enum": ["started"] + "enum": [ + "started" + ] } } } @@ -1106,7 +1441,11 @@ }, "type": { "type": "string", - "enum": ["string", "number", "boolean"], + "enum": [ + "string", + "number", + "boolean" + ], "description": "Type of the input parameter" }, "default": { @@ -1148,7 +1487,9 @@ }, { "type": "object", - "required": ["query"], + "required": [ + "query" + ], "properties": { "query": { "type": "string", @@ -1174,17 +1515,37 @@ "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." } ], "default": "eyes", "description": "AI reaction to add/remove on triggering item (one of: +1, -1, laugh, confused, heart, hooray, rocket, eyes, none). Use 'none' to disable reactions. Defaults to 'eyes' if not specified.", - "examples": ["eyes", "rocket", "+1", 1, -1, "none"] + "examples": [ + "eyes", + "rocket", + "+1", + 1, + -1, + "none" + ] } }, "additionalProperties": false, @@ -1200,25 +1561,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" + ] } } ] @@ -1245,7 +1618,12 @@ "oneOf": [ { "type": "string", - "enum": ["read-all", "write-all", "read", "write"], + "enum": [ + "read-all", + "write-all", + "read", + "write" + ], "description": "Simple permissions string: 'read-all' (all read permissions), 'write-all' (all write permissions), 'read' or 'write' (basic level)" }, { @@ -1255,76 +1633,137 @@ "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": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "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" + ] }, "pages": { "type": "string", - "enum": ["read", "write", "none"] + "enum": [ + "read", + "write", + "none" + ] }, "pull-requests": { "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" + ] }, "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." } } @@ -1334,7 +1773,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", @@ -1376,10 +1818,14 @@ "additionalProperties": false, "oneOf": [ { - "required": ["uses"] + "required": [ + "uses" + ] }, { - "required": ["run"] + "required": [ + "run" + ] } ], "properties": { @@ -1589,22 +2035,35 @@ ], "examples": [ "ubuntu-latest", - ["ubuntu-latest", "self-hosted"], + [ + "ubuntu-latest", + "self-hosted" + ], { "group": "larger-runners", - "labels": ["ubuntu-latest-8-cores"] + "labels": [ + "ubuntu-latest-8-cores" + ] } ] }, "timeout-minutes": { "type": "integer", "description": "Workflow timeout in minutes (GitHub Actions standard field). Defaults to 20 minutes for agentic workflows. Has sensible defaults and can typically be omitted.", - "examples": [5, 10, 30] + "examples": [ + 5, + 10, + 30 + ] }, "timeout_minutes": { "type": "integer", "description": "Deprecated: Use 'timeout-minutes' instead. Workflow timeout in minutes. Defaults to 20 minutes for agentic workflows.", - "examples": [5, 10, 30], + "examples": [ + 5, + 10, + 30 + ], "deprecated": true }, "concurrency": { @@ -1613,7 +2072,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", @@ -1629,7 +2091,9 @@ "description": "Whether to cancel in-progress workflows in the same concurrency group when a new one starts. Default: false (queue new runs). Set to true for agentic workflows where only the latest run matters (e.g., PR analysis that becomes stale when new commits are pushed)." } }, - "required": ["group"], + "required": [ + "group" + ], "examples": [ { "group": "dev-workflow-${{ github.ref }}", @@ -1706,7 +2170,9 @@ "description": "A deployment URL" } }, - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false } ] @@ -1772,7 +2238,9 @@ "description": "Additional Docker container options" } }, - "required": ["image"], + "required": [ + "image" + ], "additionalProperties": false } ] @@ -1840,7 +2308,9 @@ "description": "Additional Docker container options" } }, - "required": ["image"], + "required": [ + "image" + ], "additionalProperties": false } ] @@ -1852,13 +2322,24 @@ "examples": [ "defaults", { - "allowed": ["defaults", "github"] + "allowed": [ + "defaults", + "github" + ] }, { - "allowed": ["defaults", "python", "node", "*.example.com"] + "allowed": [ + "defaults", + "python", + "node", + "*.example.com" + ] }, { - "allowed": ["api.openai.com", "*.github.com"], + "allowed": [ + "api.openai.com", + "*.github.com" + ], "firewall": { "version": "v1.0.0", "log-level": "debug" @@ -1868,7 +2349,9 @@ "oneOf": [ { "type": "string", - "enum": ["defaults"], + "enum": [ + "defaults" + ], "description": "Use default network permissions (basic infrastructure: certificates, JSON schema, Ubuntu, etc.)" }, { @@ -1899,7 +2382,9 @@ }, { "type": "string", - "enum": ["disable"], + "enum": [ + "disable" + ], "description": "Disable AWF firewall (triggers warning if allowed != *, error in strict mode if allowed is not * or engine does not support firewall)" }, { @@ -1914,14 +2399,27 @@ } }, "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "AWF version to use (empty = latest release). 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 + ] }, "log-level": { "type": "string", "description": "AWF log level (default: info). Valid values: debug, info, warn, error", - "enum": ["debug", "info", "warn", "error"] + "enum": [ + "debug", + "info", + "warn", + "error" + ] } }, "additionalProperties": false @@ -1938,7 +2436,12 @@ "oneOf": [ { "type": "string", - "enum": ["default", "sandbox-runtime", "awf", "srt"], + "enum": [ + "default", + "sandbox-runtime", + "awf", + "srt" + ], "description": "Legacy string format for sandbox type: 'default' for no sandbox, 'sandbox-runtime' or 'srt' for Anthropic Sandbox Runtime, 'awf' for Agent Workflow Firewall" }, { @@ -1947,7 +2450,12 @@ "properties": { "type": { "type": "string", - "enum": ["default", "sandbox-runtime", "awf", "srt"], + "enum": [ + "default", + "sandbox-runtime", + "awf", + "srt" + ], "description": "Legacy sandbox type field (use agent instead)" }, "agent": { @@ -1955,12 +2463,17 @@ "oneOf": [ { "type": "boolean", - "enum": [false], + "enum": [ + false + ], "description": "Set to false to disable the agent firewall" }, { "type": "string", - "enum": ["awf", "srt"], + "enum": [ + "awf", + "srt" + ], "description": "Sandbox type: 'awf' for Agent Workflow Firewall, 'srt' for Sandbox Runtime" }, { @@ -1969,12 +2482,18 @@ "properties": { "id": { "type": "string", - "enum": ["awf", "srt"], + "enum": [ + "awf", + "srt" + ], "description": "Agent identifier (replaces 'type' field in new format): 'awf' for Agent Workflow Firewall, 'srt' for Sandbox Runtime" }, "type": { "type": "string", - "enum": ["awf", "srt"], + "enum": [ + "awf", + "srt" + ], "description": "Legacy: Sandbox type to use (use 'id' instead)" }, "command": { @@ -2003,7 +2522,12 @@ "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" + ] + ] }, "config": { "type": "object", @@ -2117,9 +2641,15 @@ "description": "Container image for the MCP gateway executable" }, "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Optional version/tag for the container image (e.g., 'latest', 'v1.0.0')", - "examples": ["latest", "v1.0.0"] + "examples": [ + "latest", + "v1.0.0" + ] }, "args": { "type": "array", @@ -2161,29 +2691,41 @@ "additionalProperties": false, "anyOf": [ { - "required": ["command"] + "required": [ + "command" + ] }, { - "required": ["container"] + "required": [ + "container" + ] } ], "not": { "allOf": [ { - "required": ["command"] + "required": [ + "command" + ] }, { - "required": ["container"] + "required": [ + "container" + ] } ] }, "allOf": [ { "if": { - "required": ["entrypointArgs"] + "required": [ + "entrypointArgs" + ] }, "then": { - "required": ["container"] + "required": [ + "container" + ] } } ] @@ -2206,7 +2748,10 @@ "type": "srt", "config": { "filesystem": { - "allowWrite": [".", "/tmp"] + "allowWrite": [ + ".", + "/tmp" + ] } } } @@ -2230,7 +2775,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", @@ -2362,13 +2910,24 @@ }, "mode": { "type": "string", - "enum": ["local", "remote"], + "enum": [ + "local", + "remote" + ], "description": "MCP server mode: '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", @@ -2428,16 +2987,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" + ] } ] } @@ -2445,14 +3018,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 ] @@ -2479,10 +3063,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": { @@ -2539,9 +3149,16 @@ "description": "Playwright tool configuration with custom version and domain restrictions", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Optional Playwright container version (e.g., 'v1.41.0', 1.41, 20). Numeric values are automatically converted to strings at runtime.", - "examples": ["v1.41.0", 1.41, 20] + "examples": [ + "v1.41.0", + 1.41, + 20 + ] }, "allowed_domains": { "description": "Domains allowed for Playwright browser network access. Defaults to localhost only for security.", @@ -2583,7 +3200,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", @@ -2659,7 +3279,10 @@ "description": "If true, only restore the cache without saving it back. Uses actions/cache/restore instead of actions/cache. No artifact upload step will be generated." } }, - "required": ["id", "key"], + "required": [ + "id", + "key" + ], "additionalProperties": false }, "minItems": 1, @@ -2703,7 +3326,11 @@ "type": "integer", "minimum": 1, "description": "Timeout in seconds for tool/MCP server operations. Applies to all tools and MCP servers if supported by the engine. Default varies by engine (Claude: 60s, Codex: 120s).", - "examples": [60, 120, 300] + "examples": [ + 60, + 120, + 300 + ] }, "startup-timeout": { "type": "integer", @@ -2722,7 +3349,14 @@ "description": "Short syntax: array of language identifiers to enable (e.g., [\"go\", \"typescript\"])", "items": { "type": "string", - "enum": ["go", "typescript", "python", "java", "rust", "csharp"] + "enum": [ + "go", + "typescript", + "python", + "java", + "rust", + "csharp" + ] } }, { @@ -2730,9 +3364,16 @@ "description": "Serena configuration with custom version and language-specific settings", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Optional Serena MCP version. Numeric values are automatically converted to strings at runtime.", - "examples": ["latest", "0.1.0", 1.0] + "examples": [ + "latest", + "0.1.0", + 1.0 + ] }, "args": { "type": "array", @@ -2755,7 +3396,10 @@ "type": "object", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Go version (e.g., \"1.21\", 1.21)" }, "go-mod-file": { @@ -2781,7 +3425,10 @@ "type": "object", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Node.js version for TypeScript (e.g., \"22\", 22)" } }, @@ -2799,7 +3446,10 @@ "type": "object", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Python version (e.g., \"3.12\", 3.12)" } }, @@ -2817,7 +3467,10 @@ "type": "object", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Java version (e.g., \"21\", 21)" } }, @@ -2835,7 +3488,10 @@ "type": "object", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": "Rust version (e.g., \"stable\", \"1.75\")" } }, @@ -2853,7 +3509,10 @@ "type": "object", "properties": { "version": { - "type": ["string", "number"], + "type": [ + "string", + "number" + ], "description": ".NET version for C# (e.g., \"8.0\", 8.0)" } }, @@ -3032,8 +3691,97 @@ } }, "additionalProperties": { - "description": "Simple tool string", - "type": "string" + "oneOf": [ + { + "type": "string", + "description": "Simple tool string for basic tool configuration" + }, + { + "type": "object", + "description": "MCP server configuration object", + "properties": { + "command": { + "type": "string", + "description": "Command to execute for stdio MCP server" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Arguments for the command" + }, + "env": { + "type": "object", + "patternProperties": { + "^[A-Za-z_][A-Za-z0-9_]*$": { + "type": "string" + } + }, + "description": "Environment variables" + }, + "mode": { + "type": "string", + "enum": [ + "stdio", + "http", + "remote", + "local" + ], + "description": "MCP server mode" + }, + "type": { + "type": "string", + "enum": [ + "stdio", + "http", + "remote", + "local" + ], + "description": "MCP server type" + }, + "version": { + "type": [ + "string", + "number" + ], + "description": "Version of the MCP server" + }, + "toolsets": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Toolsets to enable" + }, + "url": { + "type": "string", + "description": "URL for HTTP mode MCP servers" + }, + "headers": { + "type": "object", + "patternProperties": { + "^[A-Za-z0-9_-]+$": { + "type": "string" + } + }, + "description": "HTTP headers for HTTP mode" + }, + "container": { + "type": "string", + "description": "Container image for the MCP server" + }, + "entrypointArgs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Arguments passed to container entrypoint" + } + }, + "additionalProperties": true + } + ] } }, "command": { @@ -3094,17 +3842,25 @@ "description": "If true, only checks if cache entry exists and skips download" } }, - "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 } @@ -3163,7 +3919,10 @@ "description": "If true, only checks if cache entry exists and skips download" } }, - "required": ["key", "path"], + "required": [ + "key", + "path" + ], "additionalProperties": false } } @@ -3171,9 +3930,8 @@ }, "safe-outputs": { "type": "object", - "$comment": "Strict mode dependency: When strict=true AND permissions contains write values (contents:write, issues:write, or pull-requests:write), safe-outputs must be configured. This relationship is validated in Go code (pkg/workflow/strict_mode_validation.go) via validateStrictPermissions() because it requires complex logic to check if ANY permission property equals 'write', which cannot be expressed concisely in JSON Schema.", - "description": "Safe output processing configuration that automatically creates GitHub issues, comments, and pull requests from AI workflow output without requiring write permissions in the main job", "$comment": "Required if workflow creates or modifies GitHub resources. Operations requiring safe-outputs: add-comment, add-labels, add-reviewer, assign-milestone, assign-to-agent, close-discussion, close-issue, close-pull-request, create-agent-task, create-code-scanning-alert, create-discussion, create-issue, create-pull-request, create-pull-request-review-comment, hide-comment, link-sub-issue, missing-tool, noop, push-to-pull-request-branch, threat-detection, update-discussion, update-issue, update-project, update-pull-request, update-release, upload-asset. See documentation for complete details.", + "description": "Safe output processing configuration that automatically creates GitHub issues, comments, and pull requests from AI workflow output without requiring write permissions in the main job", "properties": { "allowed-domains": { "type": "array", @@ -3262,16 +4020,25 @@ "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] " } ] @@ -3360,9 +4127,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 + ] }, "labels": { "type": "array", @@ -3439,12 +4213,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" } ] @@ -3501,7 +4280,10 @@ "required-category": "Ideas" }, { - "required-labels": ["resolved", "completed"], + "required-labels": [ + "resolved", + "completed" + ], "max": 1 } ] @@ -3606,7 +4388,10 @@ "required-title-prefix": "[refactor] " }, { - "required-labels": ["automated", "stale"], + "required-labels": [ + "automated", + "stale" + ], "max": 10 } ] @@ -3659,7 +4444,10 @@ "required-title-prefix": "[bot] " }, { - "required-labels": ["automated", "outdated"], + "required-labels": [ + "automated", + "outdated" + ], "max": 5 } ] @@ -3708,7 +4496,13 @@ "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).", "items": { "type": "string", - "enum": ["spam", "abuse", "off_topic", "outdated", "resolved"] + "enum": [ + "spam", + "abuse", + "off_topic", + "outdated", + "resolved" + ] } } }, @@ -3775,7 +4569,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": { @@ -3810,13 +4608,19 @@ "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" } ] @@ -3842,7 +4646,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", @@ -4064,7 +4871,10 @@ "minimum": 1 }, "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": { @@ -4250,7 +5060,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)" }, "commit-title-suffix": { @@ -4295,7 +5109,13 @@ "description": "List of allowed reasons for hiding comments. Default: all reasons allowed (spam, abuse, off_topic, outdated, resolved).", "items": { "type": "string", - "enum": ["spam", "abuse", "off_topic", "outdated", "resolved"] + "enum": [ + "spam", + "abuse", + "off_topic", + "outdated", + "resolved" + ] } } }, @@ -4441,7 +5261,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", @@ -4457,7 +5280,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 }}" + ] }, "app": { "type": "object", @@ -4466,17 +5293,25 @@ "app-id": { "type": "string", "description": "GitHub App ID. Should reference a variable (e.g., ${{ vars.APP_ID }}).", - "examples": ["${{ vars.APP_ID }}", "${{ secrets.APP_ID }}"] + "examples": [ + "${{ vars.APP_ID }}", + "${{ secrets.APP_ID }}" + ] }, "private-key": { "type": "string", "description": "GitHub App private key. Should reference a secret (e.g., ${{ secrets.APP_PRIVATE_KEY }}).", - "examples": ["${{ secrets.APP_PRIVATE_KEY }}"] + "examples": [ + "${{ secrets.APP_PRIVATE_KEY }}" + ] }, "owner": { "type": "string", "description": "Optional: The owner of the GitHub App installation. If empty, defaults to the current repository owner.", - "examples": ["my-organization", "${{ github.repository_owner }}"] + "examples": [ + "my-organization", + "${{ github.repository_owner }}" + ] }, "repositories": { "type": "array", @@ -4484,10 +5319,21 @@ "items": { "type": "string" }, - "examples": [["repo1", "repo2"], ["my-repo"]] + "examples": [ + [ + "repo1", + "repo2" + ], + [ + "my-repo" + ] + ] } }, - "required": ["app-id", "private-key"], + "required": [ + "app-id", + "private-key" + ], "additionalProperties": false }, "max-patch-size": { @@ -4634,7 +5480,11 @@ }, "type": { "type": "string", - "enum": ["string", "boolean", "choice"], + "enum": [ + "string", + "boolean", + "choice" + ], "description": "Input parameter type", "default": "string" }, @@ -4671,42 +5521,65 @@ "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})" + ] }, "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})." + ] } }, "additionalProperties": false @@ -4785,7 +5658,9 @@ "oneOf": [ { "type": "string", - "enum": ["all"], + "enum": [ + "all" + ], "description": "Allow any authenticated user to trigger the workflow (\u26a0\ufe0f disables permission checking entirely - use with caution)" }, { @@ -4793,7 +5668,13 @@ "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"], + "enum": [ + "admin", + "maintainer", + "maintain", + "write", + "triage" + ], "description": "Repository permission level: 'admin' (full access), 'maintainer'/'maintain' (repository management), 'write' (push access), 'triage' (issue management)" }, "minItems": 1 @@ -4868,10 +5749,14 @@ "additionalProperties": false, "anyOf": [ { - "required": ["uses"] + "required": [ + "uses" + ] }, { - "required": ["run"] + "required": [ + "run" + ] } ] }, @@ -4880,7 +5765,10 @@ "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 wildcard '*'. (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 wildcard '*' in allowed domains, (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://githubnext.github.io/gh-aw/reference/frontmatter/#strict-mode-strict", - "examples": [true, false] + "examples": [ + true, + false + ] }, "safe-inputs": { "type": "object", @@ -4889,7 +5777,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", @@ -4903,7 +5793,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." }, @@ -4953,46 +5849,69 @@ "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": ["run"], + "required": [ + "run" + ], "not": { "anyOf": [ { - "required": ["script"] + "required": [ + "script" + ] }, { - "required": ["py"] + "required": [ + "py" + ] } ] } }, { - "required": ["py"], + "required": [ + "py" + ], "not": { "anyOf": [ { - "required": ["script"] + "required": [ + "script" + ] }, { - "required": ["run"] + "required": [ + "run" + ] } ] } @@ -5042,7 +5961,9 @@ "properties": { "mode": { "type": "string", - "enum": ["http"], + "enum": [ + "http" + ], "default": "http", "description": "Deprecated: Transport mode for the safe-inputs MCP server. This field is ignored as only 'http' mode is supported. The server always starts as a separate step.", "deprecated": true, @@ -5059,9 +5980,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", @@ -5098,7 +6028,9 @@ } } }, - "required": ["slash_command"] + "required": [ + "slash_command" + ] }, { "properties": { @@ -5108,7 +6040,9 @@ } } }, - "required": ["command"] + "required": [ + "command" + ] } ] } @@ -5127,7 +6061,9 @@ } } }, - "required": ["issue_comment"] + "required": [ + "issue_comment" + ] }, { "properties": { @@ -5137,7 +6073,9 @@ } } }, - "required": ["pull_request_review_comment"] + "required": [ + "pull_request_review_comment" + ] }, { "properties": { @@ -5147,7 +6085,9 @@ } } }, - "required": ["label"] + "required": [ + "label" + ] } ] } @@ -5181,7 +6121,12 @@ "oneOf": [ { "type": "string", - "enum": ["claude", "codex", "copilot", "custom"], + "enum": [ + "claude", + "codex", + "copilot", + "custom" + ], "description": "Simple engine name: 'claude' (default, Claude Code), 'copilot' (GitHub Copilot CLI), 'codex' (OpenAI Codex CLI), or 'custom' (user-defined steps)" }, { @@ -5190,13 +6135,26 @@ "properties": { "id": { "type": "string", - "enum": ["claude", "codex", "custom", "copilot"], + "enum": [ + "claude", + "codex", + "custom", + "copilot" + ], "description": "AI engine identifier: 'claude' (Claude Code), 'codex' (OpenAI Codex CLI), 'copilot' (GitHub Copilot CLI), or 'custom' (user-defined GitHub Actions steps)" }, "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.", - "examples": ["beta", "stable", 20, 3.11] + "examples": [ + "beta", + "stable", + 20, + 3.11 + ] }, "model": { "type": "string", @@ -5234,7 +6192,9 @@ "description": "Whether to cancel in-progress runs of the same concurrency group. Defaults to false for agentic workflow runs." } }, - "required": ["group"], + "required": [ + "group" + ], "additionalProperties": false } ], @@ -5289,7 +6249,9 @@ "description": "Human-readable description of what this pattern matches" } }, - "required": ["pattern"], + "required": [ + "pattern" + ], "additionalProperties": false } }, @@ -5305,7 +6267,9 @@ "description": "Optional array of command-line arguments to pass to the AI engine CLI. These arguments are injected after all other args but before the prompt." } }, - "required": ["id"], + "required": [ + "id" + ], "additionalProperties": false } ] @@ -5316,7 +6280,10 @@ "properties": { "type": { "type": "string", - "enum": ["stdio", "local"], + "enum": [ + "stdio", + "local" + ], "description": "MCP connection type for stdio (local is an alias for stdio)" }, "registry": { @@ -5336,9 +6303,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", @@ -5402,49 +6377,70 @@ "$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) Dependency: 'network' requires 'container' (validated in 'allOf'). (4) Type constraint: When 'type' is 'stdio' or 'local', either 'command' or 'container' is required.", "anyOf": [ { - "required": ["type"] + "required": [ + "type" + ] }, { - "required": ["command"] + "required": [ + "command" + ] }, { - "required": ["container"] + "required": [ + "container" + ] } ], "not": { "allOf": [ { - "required": ["command"] + "required": [ + "command" + ] }, { - "required": ["container"] + "required": [ + "container" + ] } ] }, "allOf": [ { "if": { - "required": ["network"] + "required": [ + "network" + ] }, "then": { - "required": ["container"] + "required": [ + "container" + ] } }, { "if": { "properties": { "type": { - "enum": ["stdio", "local"] + "enum": [ + "stdio", + "local" + ] } } }, "then": { "anyOf": [ { - "required": ["command"] + "required": [ + "command" + ] }, { - "required": ["container"] + "required": [ + "container" + ] } ] } @@ -5487,14 +6483,20 @@ } } }, - "required": ["url"], + "required": [ + "url" + ], "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_]*)*\\s*\\}\\}$", "description": "GitHub token expression using secrets. Pattern details: `[A-Za-z_][A-Za-z0-9_]*` matches a valid secret name (starts with a letter or underscore, followed by letters, digits, or underscores). The full pattern matches expressions like `${{ secrets.NAME }}` or `${{ secrets.NAME1 || secrets.NAME2 }}`.", - "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 }}" + ] } } } diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index 3749519d16..1a61ad2296 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -383,7 +383,7 @@ func (e *CodexEngine) GetSquidLogsSteps(workflowData *WorkflowData) []GitHubActi func (e *CodexEngine) expandNeutralToolsToCodexTools(toolsConfig *ToolsConfig) *ToolsConfig { if toolsConfig == nil { return &ToolsConfig{ - Custom: make(map[string]any), + Custom: make(map[string]MCPServerConfig), raw: make(map[string]any), } } @@ -401,7 +401,7 @@ func (e *CodexEngine) expandNeutralToolsToCodexTools(toolsConfig *ToolsConfig) * SafetyPrompt: toolsConfig.SafetyPrompt, Timeout: toolsConfig.Timeout, StartupTimeout: toolsConfig.StartupTimeout, - Custom: make(map[string]any), + Custom: make(map[string]MCPServerConfig), raw: make(map[string]any), } diff --git a/pkg/workflow/mcp-config.go b/pkg/workflow/mcp-config.go index 067860f96a..7f2fb28b2e 100644 --- a/pkg/workflow/mcp-config.go +++ b/pkg/workflow/mcp-config.go @@ -787,6 +787,7 @@ func getMCPConfig(toolConfig map[string]any, toolName string) (*parser.MCPServer // Validate known properties - fail if unknown properties are found knownProperties := map[string]bool{ "type": true, + "mode": true, // Added for MCPServerConfig struct "command": true, "container": true, "version": true, @@ -798,6 +799,7 @@ func getMCPConfig(toolConfig map[string]any, toolName string) (*parser.MCPServer "headers": true, "registry": true, "allowed": true, + "toolsets": true, // Added for MCPServerConfig struct } for key := range toolConfig { diff --git a/pkg/workflow/runtime_setup.go b/pkg/workflow/runtime_setup.go index cceb77cc78..ac0c61ac42 100644 --- a/pkg/workflow/runtime_setup.go +++ b/pkg/workflow/runtime_setup.go @@ -292,19 +292,11 @@ func detectFromMCPConfigs(tools *ToolsConfig, requirements map[string]*RuntimeRe // Scan custom MCP tools for runtime commands for _, tool := range tools.Custom { - // Handle structured MCP config with command field - if toolMap, ok := tool.(map[string]any); ok { - if command, exists := toolMap["command"]; exists { - if cmdStr, ok := command.(string); ok { - if runtime, found := commandToRuntime[cmdStr]; found { - updateRequiredRuntime(runtime, "", requirements) - } - } + // MCPServerConfig has a Command field directly + if tool.Command != "" { + if runtime, found := commandToRuntime[tool.Command]; found { + updateRequiredRuntime(runtime, "", requirements) } - } else if cmdStr, ok := tool.(string); ok { - // Handle string-format MCP tool (e.g., "npx -y package") - // Parse the command string to detect runtime - detectRuntimeFromCommand(cmdStr, requirements) } } } diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go index 0257cc8c4c..e549c9edb7 100644 --- a/pkg/workflow/tools_parser.go +++ b/pkg/workflow/tools_parser.go @@ -62,13 +62,13 @@ func NewTools(toolsMap map[string]any) *Tools { toolsParserLog.Printf("Creating tools configuration from map with %d entries", len(toolsMap)) if toolsMap == nil { return &Tools{ - Custom: make(map[string]any), + Custom: make(map[string]MCPServerConfig), raw: make(map[string]any), } } tools := &Tools{ - Custom: make(map[string]any), + Custom: make(map[string]MCPServerConfig), raw: make(map[string]any), } @@ -138,7 +138,7 @@ func NewTools(toolsMap map[string]any) *Tools { customCount := 0 for name, config := range toolsMap { if !knownTools[name] { - tools.Custom[name] = config + tools.Custom[name] = parseMCPServerConfig(config) customCount++ } } @@ -511,3 +511,119 @@ func parseStartupTimeoutTool(val any) *int { } return nil } + +// parseMCPServerConfig converts raw MCP server configuration to MCPServerConfig +func parseMCPServerConfig(val any) MCPServerConfig { + config := MCPServerConfig{ + CustomFields: make(map[string]any), + } + + // If val is nil, return empty config + if val == nil { + return config + } + + // If it's not a map, store it as a custom field + configMap, ok := val.(map[string]any) + if !ok { + config.CustomFields["value"] = val + return config + } + + // Parse common MCP server fields + if command, ok := configMap["command"].(string); ok { + config.Command = command + } + + if args, ok := configMap["args"].([]any); ok { + config.Args = make([]string, 0, len(args)) + for _, arg := range args { + if str, ok := arg.(string); ok { + config.Args = append(config.Args, str) + } + } + } + + if env, ok := configMap["env"].(map[string]any); ok { + config.Env = make(map[string]string) + for k, v := range env { + if str, ok := v.(string); ok { + config.Env[k] = str + } + } + } + + if mode, ok := configMap["mode"].(string); ok { + config.Mode = mode + } + + if mcpType, ok := configMap["type"].(string); ok { + config.Type = mcpType + } + + if version, ok := configMap["version"].(string); ok { + config.Version = version + } else if versionNum, ok := configMap["version"].(float64); ok { + config.Version = fmt.Sprintf("%.0f", versionNum) + } + + if toolsets, ok := configMap["toolsets"].([]any); ok { + config.Toolsets = make([]string, 0, len(toolsets)) + for _, item := range toolsets { + if str, ok := item.(string); ok { + config.Toolsets = append(config.Toolsets, str) + } + } + } + + // Parse HTTP-specific fields + if url, ok := configMap["url"].(string); ok { + config.URL = url + } + + if headers, ok := configMap["headers"].(map[string]any); ok { + config.Headers = make(map[string]string) + for k, v := range headers { + if str, ok := v.(string); ok { + config.Headers[k] = str + } + } + } + + // Parse container-specific fields + if container, ok := configMap["container"].(string); ok { + config.Container = container + } + + if entrypointArgs, ok := configMap["entrypointArgs"].([]any); ok { + config.EntrypointArgs = make([]string, 0, len(entrypointArgs)) + for _, arg := range entrypointArgs { + if str, ok := arg.(string); ok { + config.EntrypointArgs = append(config.EntrypointArgs, str) + } + } + } + + // Store any unknown fields in CustomFields + knownFields := map[string]bool{ + "command": true, + "args": true, + "env": true, + "mode": true, + "type": true, + "version": true, + "toolsets": true, + "url": true, + "headers": true, + "container": true, + "entrypointArgs": true, + } + + for key, value := range configMap { + if !knownFields[key] { + config.CustomFields[key] = value + } + } + + return config +} diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index ab92dffa78..fae9a460e8 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -73,7 +73,7 @@ type ToolsConfig struct { StartupTimeout *int `yaml:"startup-timeout,omitempty"` // Custom MCP tools (anything not in the above list) - Custom map[string]any `yaml:",inline"` + Custom map[string]MCPServerConfig `yaml:",inline"` // Raw map for backwards compatibility raw map[string]any @@ -92,6 +92,57 @@ func ParseToolsConfig(toolsMap map[string]any) (*ToolsConfig, error) { return config, nil } +// mcpServerConfigToMap converts an MCPServerConfig to map[string]any for backward compatibility +func mcpServerConfigToMap(config MCPServerConfig) map[string]any { + result := make(map[string]any) + + // Add common fields if they're set + if config.Command != "" { + result["command"] = config.Command + } + if len(config.Args) > 0 { + result["args"] = config.Args + } + if len(config.Env) > 0 { + result["env"] = config.Env + } + if config.Mode != "" { + result["mode"] = config.Mode + } + if config.Type != "" { + result["type"] = config.Type + } + if config.Version != "" { + result["version"] = config.Version + } + if len(config.Toolsets) > 0 { + result["toolsets"] = config.Toolsets + } + + // Add HTTP-specific fields + if config.URL != "" { + result["url"] = config.URL + } + if len(config.Headers) > 0 { + result["headers"] = config.Headers + } + + // Add container-specific fields + if config.Container != "" { + result["container"] = config.Container + } + if len(config.EntrypointArgs) > 0 { + result["entrypointArgs"] = config.EntrypointArgs + } + + // Add custom fields (these override standard fields if there are conflicts) + for key, value := range config.CustomFields { + result[key] = value + } + + return result +} + // ToMap converts the ToolsConfig back to a map[string]any for backward compatibility. // This is useful when interfacing with legacy code that expects map[string]any. func (t *ToolsConfig) ToMap() map[string]any { @@ -152,9 +203,9 @@ func (t *ToolsConfig) ToMap() map[string]any { result["startup-timeout"] = *t.StartupTimeout } - // Add custom tools + // Add custom tools - convert MCPServerConfig to map[string]any for name, config := range t.Custom { - result[name] = config + result[name] = mcpServerConfigToMap(config) } return result @@ -230,6 +281,31 @@ type CacheMemoryToolConfig struct { Raw any `yaml:"-"` } +// MCPServerConfig represents the configuration for a custom MCP server. +// This provides partial type safety for common MCP configuration fields +// while maintaining flexibility for truly dynamic configurations. +type MCPServerConfig struct { + // Common MCP server fields + Command string `yaml:"command,omitempty"` // Command to execute (for stdio mode) + Args []string `yaml:"args,omitempty"` // Arguments for the command + Env map[string]string `yaml:"env,omitempty"` // Environment variables + Mode string `yaml:"mode,omitempty"` // MCP server mode (stdio, http, remote, local) + Type string `yaml:"type,omitempty"` // MCP server type (stdio, http, remote, local) + Version string `yaml:"version,omitempty"` // Version of the MCP server + Toolsets []string `yaml:"toolsets,omitempty"` // Toolsets to enable + + // HTTP-specific fields + URL string `yaml:"url,omitempty"` // URL for HTTP mode MCP servers + Headers map[string]string `yaml:"headers,omitempty"` // HTTP headers for HTTP mode + + // Container-specific fields + Container string `yaml:"container,omitempty"` // Container image for the MCP server + EntrypointArgs []string `yaml:"entrypointArgs,omitempty"` // Arguments passed to container entrypoint + + // For truly dynamic configuration (server-specific fields not covered above) + CustomFields map[string]any `yaml:",inline"` +} + // MCPGatewayRuntimeConfig represents the configuration for the MCP gateway runtime execution // The gateway routes MCP server calls through a unified HTTP endpoint type MCPGatewayRuntimeConfig struct { diff --git a/pkg/workflow/tools_types_test.go b/pkg/workflow/tools_types_test.go index 1ae1c31495..f394828971 100644 --- a/pkg/workflow/tools_types_test.go +++ b/pkg/workflow/tools_types_test.go @@ -76,11 +76,22 @@ func TestNewTools(t *testing.T) { t.Errorf("expected 2 custom tools, got %d", len(tools.Custom)) } - if tools.Custom["my-custom"] == nil { + myCustom, exists := tools.Custom["my-custom"] + if !exists { t.Error("expected my-custom tool in Custom map") + } else { + if myCustom.Command != "node" { + t.Errorf("expected my-custom command to be 'node', got %q", myCustom.Command) + } } - if tools.Custom["another-mcp"] == nil { + + anotherMCP, exists := tools.Custom["another-mcp"] + if !exists { t.Error("expected another-mcp tool in Custom map") + } else { + if anotherMCP.URL != "http://localhost:8080" { + t.Errorf("expected another-mcp URL to be 'http://localhost:8080', got %q", anotherMCP.URL) + } } names := tools.GetToolNames() @@ -584,3 +595,207 @@ func TestToolsConfigToMap(t *testing.T) { } }) } + +func TestParseMCPServerConfig(t *testing.T) { + t.Run("parses stdio MCP server config", func(t *testing.T) { + configMap := map[string]any{ + "command": "node", + "args": []any{"server.js", "--port", "3000"}, + "env": map[string]any{ + "NODE_ENV": "production", + }, + "mode": "stdio", + "version": "1.0.0", + } + + config := parseMCPServerConfig(configMap) + + if config.Command != "node" { + t.Errorf("expected command 'node', got %q", config.Command) + } + + if len(config.Args) != 3 { + t.Errorf("expected 3 args, got %d", len(config.Args)) + } + + if config.Args[0] != "server.js" { + t.Errorf("expected first arg 'server.js', got %q", config.Args[0]) + } + + if config.Env["NODE_ENV"] != "production" { + t.Errorf("expected NODE_ENV 'production', got %q", config.Env["NODE_ENV"]) + } + + if config.Mode != "stdio" { + t.Errorf("expected mode 'stdio', got %q", config.Mode) + } + + if config.Version != "1.0.0" { + t.Errorf("expected version '1.0.0', got %q", config.Version) + } + }) + + t.Run("parses HTTP MCP server config", func(t *testing.T) { + configMap := map[string]any{ + "type": "http", + "url": "http://localhost:8080", + "headers": map[string]any{ + "Authorization": "Bearer token123", + }, + "toolsets": []any{"repos", "issues"}, + } + + config := parseMCPServerConfig(configMap) + + if config.Type != "http" { + t.Errorf("expected type 'http', got %q", config.Type) + } + + if config.URL != "http://localhost:8080" { + t.Errorf("expected URL 'http://localhost:8080', got %q", config.URL) + } + + if config.Headers["Authorization"] != "Bearer token123" { + t.Errorf("expected Authorization header, got %q", config.Headers["Authorization"]) + } + + if len(config.Toolsets) != 2 { + t.Errorf("expected 2 toolsets, got %d", len(config.Toolsets)) + } + }) + + t.Run("parses container MCP server config", func(t *testing.T) { + configMap := map[string]any{ + "container": "ghcr.io/example/mcp-server:latest", + "entrypointArgs": []any{"--config", "/etc/config.json"}, + } + + config := parseMCPServerConfig(configMap) + + if config.Container != "ghcr.io/example/mcp-server:latest" { + t.Errorf("expected container image, got %q", config.Container) + } + + if len(config.EntrypointArgs) != 2 { + t.Errorf("expected 2 entrypoint args, got %d", len(config.EntrypointArgs)) + } + }) + + t.Run("preserves custom fields", func(t *testing.T) { + configMap := map[string]any{ + "command": "node", + "customField1": "value1", + "customField2": 42, + } + + config := parseMCPServerConfig(configMap) + + if config.Command != "node" { + t.Errorf("expected command 'node', got %q", config.Command) + } + + if config.CustomFields["customField1"] != "value1" { + t.Errorf("expected customField1 'value1', got %v", config.CustomFields["customField1"]) + } + + if config.CustomFields["customField2"] != 42 { + t.Errorf("expected customField2 42, got %v", config.CustomFields["customField2"]) + } + }) + + t.Run("handles nil config", func(t *testing.T) { + config := parseMCPServerConfig(nil) + + if config.Command != "" { + t.Errorf("expected empty command, got %q", config.Command) + } + + if len(config.CustomFields) != 0 { + t.Errorf("expected empty CustomFields, got %d entries", len(config.CustomFields)) + } + }) + + t.Run("handles numeric version", func(t *testing.T) { + configMap := map[string]any{ + "version": 2.0, + } + + config := parseMCPServerConfig(configMap) + + if config.Version != "2" { + t.Errorf("expected version '2', got %q", config.Version) + } + }) +} + +func TestMCPServerConfigToMap(t *testing.T) { + t.Run("converts MCPServerConfig to map", func(t *testing.T) { + config := MCPServerConfig{ + Command: "node", + Args: []string{"server.js"}, + Env: map[string]string{ + "NODE_ENV": "production", + }, + Mode: "stdio", + Version: "1.0.0", + Toolsets: []string{"default"}, + } + + result := mcpServerConfigToMap(config) + + if result["command"] != "node" { + t.Errorf("expected command 'node', got %v", result["command"]) + } + + args, ok := result["args"].([]string) + if !ok { + t.Error("expected args to be []string") + } else if len(args) != 1 { + t.Errorf("expected 1 arg, got %d", len(args)) + } + + if result["mode"] != "stdio" { + t.Errorf("expected mode 'stdio', got %v", result["mode"]) + } + }) + + t.Run("includes HTTP fields when set", func(t *testing.T) { + config := MCPServerConfig{ + Type: "http", + URL: "http://localhost:8080", + Headers: map[string]string{ + "Authorization": "Bearer token", + }, + } + + result := mcpServerConfigToMap(config) + + if result["type"] != "http" { + t.Errorf("expected type 'http', got %v", result["type"]) + } + + if result["url"] != "http://localhost:8080" { + t.Errorf("expected URL, got %v", result["url"]) + } + + headers, ok := result["headers"].(map[string]string) + if !ok || len(headers) != 1 { + t.Error("expected headers map") + } + }) + + t.Run("includes custom fields", func(t *testing.T) { + config := MCPServerConfig{ + Command: "node", + CustomFields: map[string]any{ + "customField": "customValue", + }, + } + + result := mcpServerConfigToMap(config) + + if result["customField"] != "customValue" { + t.Errorf("expected customField, got %v", result["customField"]) + } + }) +}