diff --git a/.changeset/minor-expose-api-proxy-model-fallback-frontmatter.md b/.changeset/minor-expose-api-proxy-model-fallback-frontmatter.md new file mode 100644 index 00000000000..9c206fe259a --- /dev/null +++ b/.changeset/minor-expose-api-proxy-model-fallback-frontmatter.md @@ -0,0 +1,13 @@ +--- +"gh-aw": minor +--- + +Expose `sandbox.agent.model-fallback` in the compiler frontmatter so BYOK Azure OpenAI users can disable the middle-power fallback behavior that rewrites deployment names and causes HTTP 404 `DeploymentNotFound` errors. + +Example usage: + +```yaml +sandbox: + agent: + model-fallback: false +``` diff --git a/docs/public/editor/autocomplete-data.json b/docs/public/editor/autocomplete-data.json index dbacfd23d3b..e9d2d0d49a2 100644 --- a/docs/public/editor/autocomplete-data.json +++ b/docs/public/editor/autocomplete-data.json @@ -59,7 +59,10 @@ "inlined-imports": { "type": "boolean", "desc": "If true, inline all imports (including those without inputs) at compilation time in the generated lock.yml instead of...", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "on": { @@ -240,7 +243,9 @@ "skip-if-check-failing": { "type": "null|boolean|object", "desc": "Skip workflow execution if any CI checks on the target branch are failing or pending.", - "enum": [true], + "enum": [ + true + ], "leaf": true }, "skip-roles": { @@ -262,7 +267,15 @@ "roles": { "type": "string|array", "desc": "Repository access roles required to trigger agentic workflows.", - "enum": ["admin", "maintainer", "maintain", "write", "triage", "read", "all"], + "enum": [ + "admin", + "maintainer", + "maintain", + "write", + "triage", + "read", + "all" + ], "leaf": true, "array": true }, @@ -280,7 +293,10 @@ "allow-bot-authored-trigger-comment": { "type": "boolean", "desc": "Allow the bot-posted-menu / user-checks-box pattern: when a workflow posts a checkbox-menu comment as a GitHub App bo...", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "manual-approval": { @@ -291,7 +307,17 @@ "reaction": { "type": "string|integer|object", "desc": "AI reaction to add/remove on triggering item.", - "enum": ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes", "none"], + "enum": [ + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "rocket", + "eyes", + "none" + ], "leaf": true }, "status-comment": { @@ -323,9 +349,11 @@ "desc": "Additional permissions for the pre-activation job." }, "stale-check": { - "type": "boolean", - "desc": "When set to false, disables the frontmatter hash check step in the activation job.", - "enum": [true, false], + "type": "boolean|string", + "desc": "Controls the stale lock file check in the activation job.", + "enum": [ + "full" + ], "leaf": true } } @@ -333,120 +361,195 @@ "permissions": { "type": "string|object", "desc": "GitHub token permissions for the workflow.", - "enum": ["read-all", "write-all"], + "enum": [ + "read-all", + "write-all" + ], "children": { "actions": { "type": "string", "desc": "Permission for GitHub Actions workflows and runs (read: view workflows, write: manage workflows, none: no access)", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "attestations": { "type": "string", "desc": "Permission for artifact attestations (read: view attestations, write: create attestations, none: no access)", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "checks": { "type": "string", "desc": "Permission for repository checks and status checks (read: view checks, write: create/update checks, none: no access)", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "contents": { "type": "string", "desc": "Permission for repository contents (read: view files, write: modify files/branches, none: no access)", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "deployments": { "type": "string", "desc": "Permission for repository deployments (read: view deployments, write: create/update deployments, none: no access)", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "discussions": { "type": "string", "desc": "Permission for repository discussions (read: view discussions, write: create/update discussions, none: no access)", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "id-token": { "type": "string", "desc": "Permission level for OIDC token requests (write/none only - read is not supported).", - "enum": ["write", "none"], + "enum": [ + "write", + "none" + ], "leaf": true }, "issues": { "type": "string", "desc": "Permission for repository issues (read: view issues, write: create/update/close issues, none: no access)", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "models": { "type": "string", "desc": "Permission for GitHub Copilot models (read: access AI models for agentic workflows, none: no access)", - "enum": ["read", "none"], + "enum": [ + "read", + "none" + ], "leaf": true }, "metadata": { "type": "string", "desc": "Permission for repository metadata (read: view repository information, write: update repository metadata, none: no ac...", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "packages": { "type": "string", "desc": "Permission level for GitHub Packages (read/write/none).", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "pages": { "type": "string", "desc": "Permission level for GitHub Pages (read/write/none).", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "pull-requests": { "type": "string", "desc": "Permission level for pull requests (read/write/none).", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "repository-projects": { "type": "string", "desc": "Permission level for repository projects (read/write/none).", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "organization-projects": { "type": "string", "desc": "Permission level for organization projects (read/write/none).", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "security-events": { "type": "string", "desc": "Permission level for security events (read/write/none).", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "statuses": { "type": "string", "desc": "Permission level for commit statuses (read/write/none).", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "vulnerability-alerts": { "type": "string", "desc": "Permission level for Dependabot vulnerability alerts (read/write/none).", - "enum": ["read", "write", "none"], + "enum": [ + "read", + "write", + "none" + ], "leaf": true }, "all": { "type": "string", "desc": "Permission shorthand that applies read access to all permission scopes.", - "enum": ["read"], + "enum": [ + "read" + ], "leaf": true } } @@ -499,13 +602,19 @@ "cancel-in-progress": { "type": "boolean", "desc": "Whether to cancel in-progress workflows in the same concurrency group when a new one starts.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "queue": { "type": "string", "desc": "Pending run queue behavior for this concurrency group.", - "enum": ["single", "max"], + "enum": [ + "single", + "max" + ], "leaf": true }, "job-discriminator": { @@ -523,7 +632,10 @@ "inline-sub-agents": { "type": "boolean", "desc": "Deprecated switch for inline sub-agent support.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "features": { @@ -541,7 +653,10 @@ "storage": { "type": "string", "desc": "Storage backend for experiment state.", - "enum": ["cache", "repo"], + "enum": [ + "cache", + "repo" + ], "leaf": true } } @@ -549,7 +664,10 @@ "disable-model-invocation": { "type": "boolean", "desc": "Controls whether the custom agent should disable model invocation.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "secrets": { @@ -625,7 +743,9 @@ "network": { "type": "string|object", "desc": "Network access control for AI engines using ecosystem identifiers and domain allowlists.", - "enum": ["defaults"], + "enum": [ + "defaults" + ], "children": { "allowed": { "type": "array", @@ -635,7 +755,10 @@ "allowed-input": { "type": "boolean", "desc": "When true and the workflow uses workflow_call, expose a network_allowed string input on the compiled lock file.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "blocked": { @@ -648,29 +771,41 @@ "sandbox": { "type": "string|object", "desc": "Sandbox configuration for AI engines.", - "enum": ["default", "awf"], + "enum": [ + "default", + "awf" + ], "children": { "type": { "type": "string", "desc": "Legacy sandbox type field (use agent instead).", - "enum": ["default", "awf"], + "enum": [ + "default", + "awf" + ], "leaf": true }, "agent": { "type": "boolean|string|object", "desc": "Agent sandbox type: 'awf' uses AWF (Agent Workflow Firewall), or false to disable agent sandbox.", - "enum": ["awf"], + "enum": [ + "awf" + ], "children": { "id": { "type": "string", "desc": "Agent identifier (replaces 'type' field in new format): 'awf' for Agent Workflow Firewall", - "enum": ["awf"], + "enum": [ + "awf" + ], "leaf": true }, "type": { "type": "string", "desc": "Legacy: Sandbox type to use (use 'id' instead)", - "enum": ["awf"], + "enum": [ + "awf" + ], "leaf": true }, "version": { @@ -702,6 +837,11 @@ "desc": "Memory limit for the AWF container (e.g., '4g', '8g').", "leaf": true }, + "model-fallback": { + "type": "boolean|string", + "desc": "Enable or disable model fallback for unresolved model selections.", + "leaf": true + }, "config": { "type": "object", "desc": "Custom sandbox runtime configuration.", @@ -717,7 +857,10 @@ "enableWeakerNestedSandbox": { "type": "boolean", "desc": "Enable weaker nested sandbox mode (recommended: true for Docker access)", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true } } @@ -756,7 +899,10 @@ "enableWeakerNestedSandbox": { "type": "boolean", "desc": "When true, allows nested sandbox processes to run with relaxed restrictions.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true } } @@ -812,7 +958,10 @@ "domain": { "type": "string", "desc": "Gateway domain for URL generation (default: 'host.docker.internal' when agent is enabled, 'localhost' when disabled)", - "enum": ["localhost", "host.docker.internal"], + "enum": [ + "localhost", + "host.docker.internal" + ], "leaf": true }, "keepalive-interval": { @@ -868,6 +1017,17 @@ "desc": "Optional specific LLM model to use (e.g., 'claude-3-5-sonnet-20241022', 'gpt-4').", "leaf": true }, + "permission-mode": { + "type": "string", + "desc": "Claude permission mode override.", + "enum": [ + "auto", + "acceptEdits", + "plan", + "bypassPermissions" + ], + "leaf": true + }, "max-turns": { "type": "integer|string", "desc": "Maximum number of chat iterations per run.", @@ -890,13 +1050,19 @@ "cancel-in-progress": { "type": "boolean", "desc": "Whether to cancel in-progress runs of the same concurrency group.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "queue": { "type": "string", "desc": "Pending run queue behavior for this concurrency group.", - "enum": ["single", "max"], + "enum": [ + "single", + "max" + ], "leaf": true } } @@ -927,7 +1093,9 @@ "type": { "type": "string", "desc": "Authentication type.", - "enum": ["github-oidc"], + "enum": [ + "github-oidc" + ], "leaf": true }, "audience": { @@ -1021,7 +1189,10 @@ "bare": { "type": "boolean", "desc": "When true, disables automatic loading of context and custom instructions by the AI engine.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "mcp": { @@ -1066,19 +1237,26 @@ "children": { "allowed": { "type": "array", - "desc": "List of allowed GitHub API functions (e.g., 'create_issue', 'update_issue', 'add_comment')", + "desc": "List of allowed GitHub API functions.", "array": true }, "mode": { "type": "string", "desc": "GitHub access mode.", - "enum": ["gh-proxy", "local", "remote"], + "enum": [ + "gh-proxy", + "local", + "remote" + ], "leaf": true }, "type": { "type": "string", "desc": "GitHub MCP transport type: 'local' (Docker-based, default) or 'remote' (hosted at api.githubcopilot.com)", - "enum": ["local", "remote"], + "enum": [ + "local", + "remote" + ], "leaf": true }, "version": { @@ -1094,19 +1272,28 @@ "read-only": { "type": "boolean", "desc": "Enable read-only mode to restrict GitHub MCP server to read-only operations only", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "lockdown": { "type": "boolean", "desc": "Enable lockdown mode to limit content surfaced from public repositories (only items authored by users with push access).", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "integrity-proxy": { "type": "boolean", "desc": "Controls DIFC proxy injection for pre-agent gh CLI steps when guard policies (min-integrity) are configured.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "github-token": { @@ -1152,21 +1339,34 @@ "allowed-repos": { "type": "string|array", "desc": "Guard policy: repository access configuration.", - "enum": ["all", "public", "${{ github.repository }}"], + "enum": [ + "all", + "public", + "${{ github.repository }}" + ], "leaf": true, "array": true }, "repos": { "type": "string|array", "desc": "Deprecated.", - "enum": ["all", "public", "${{ github.repository }}"], + "enum": [ + "all", + "public", + "${{ github.repository }}" + ], "leaf": true, "array": true }, "min-integrity": { "type": "string", "desc": "Guard policy: minimum required integrity level for repository access.", - "enum": ["none", "unapproved", "approved", "merged"], + "enum": [ + "none", + "unapproved", + "approved", + "merged" + ], "leaf": true }, "blocked-users": { @@ -1200,13 +1400,22 @@ "disapproval-integrity": { "type": "string", "desc": "Guard policy: integrity level assigned when a disapproval reaction is present.", - "enum": ["none", "unapproved", "approved", "merged"], + "enum": [ + "none", + "unapproved", + "approved", + "merged" + ], "leaf": true }, "endorser-min-integrity": { "type": "string", "desc": "Guard policy: minimum integrity level required for an endorser (reactor) to promote content.", - "enum": ["unapproved", "approved", "merged"], + "enum": [ + "unapproved", + "approved", + "merged" + ], "leaf": true }, "github-app": { @@ -1231,7 +1440,10 @@ "ignore-if-missing": { "type": "boolean", "desc": "If true, skip token minting when client-id/private-key resolve to empty strings at runtime.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "owner": { @@ -1274,7 +1486,7 @@ "leaf": true }, "edit": { - "type": "null|object", + "type": "null|boolean|object", "desc": "File editing tool for reading, creating, and modifying files in the repository", "leaf": true }, @@ -1295,7 +1507,10 @@ "mode": { "type": "string", "desc": "Integration mode: 'cli' (recommended) installs @playwright/cli via npm for token-efficient CLI invocations — use play...", - "enum": ["cli", "mcp"], + "enum": [ + "cli", + "mcp" + ], "leaf": true } } @@ -1327,13 +1542,19 @@ "restore-only": { "type": "boolean", "desc": "If true, only restore the cache without saving it back.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "scope": { "type": "string", "desc": "Cache restore key scope: 'workflow' (default, only restores from same workflow) or 'repo' (restores from any workflow...", - "enum": ["workflow", "repo"], + "enum": [ + "workflow", + "repo" + ], "leaf": true }, "allowed-extensions": { @@ -1376,7 +1597,10 @@ "footer": { "type": "boolean", "desc": "Controls whether AI-generated footer is added to the managed comment.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "github-token": { @@ -1387,7 +1611,10 @@ "staged": { "type": "boolean", "desc": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true } } @@ -1405,7 +1632,10 @@ "cli-proxy": { "type": "boolean", "desc": "When true, each user-facing MCP server is mounted as a standalone CLI tool on PATH.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "serena": { @@ -1459,13 +1689,19 @@ "create-orphan": { "type": "boolean", "desc": "Create orphaned branch if it doesn't exist (default: true)", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "wiki": { "type": "boolean", "desc": "Use the GitHub Wiki git repository instead of the regular repository.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "allowed-extensions": { @@ -1507,13 +1743,19 @@ "fail-on-cache-miss": { "type": "boolean", "desc": "Fail the workflow if cache entry is not found", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "lookup-only": { "type": "boolean", "desc": "If true, only checks if cache entry exists and skips download", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "name": { @@ -1638,6 +1880,11 @@ "desc": "Enable AI agents to create autofixes for code scanning alerts using the GitHub REST API.", "leaf": true }, + "create-check-run": { + "type": "object|null", + "desc": "Enable AI agents to create GitHub Check Runs that surface analysis results in the PR checks UI.", + "leaf": true + }, "add-labels": { "type": "null|object", "desc": "Enable AI agents to add labels to GitHub issues or pull requests based on workflow analysis or classification.", @@ -1760,7 +2007,10 @@ "staged": { "type": "boolean", "desc": "If true, emit step summary messages instead of making GitHub API calls (preview mode)", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "env": { @@ -1811,7 +2061,10 @@ "footer": { "type": "boolean", "desc": "Global footer control for all safe outputs.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "activation-comments": { @@ -1822,13 +2075,19 @@ "group-reports": { "type": "boolean", "desc": "When true, creates a parent '[aw] Failed runs' issue that tracks all workflow failures as sub-issues.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "report-failure-as-issue": { "type": "boolean", "desc": "When false, disables creating failure tracking issues when workflows fail.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "failure-issue-repo": { @@ -1844,7 +2103,10 @@ "id-token": { "type": "string", "desc": "Override the id-token permission for the safe-outputs job.", - "enum": ["write", "none"], + "enum": [ + "write", + "none" + ], "leaf": true }, "concurrency-group": { @@ -1926,8 +2188,42 @@ "if-missing": { "type": "string", "desc": "How to handle missing OTLP endpoint/header values at runtime (for example from unset secrets).", - "enum": ["error", "warn", "ignore"], + "enum": [ + "error", + "warn", + "ignore" + ], "leaf": true + }, + "github-app": { + "type": "object", + "desc": "Optional runtime authentication for OTLP export.", + "children": { + "app-id": { + "type": "string", + "desc": "Deprecated alias for client-id.", + "leaf": true + }, + "client-id": { + "type": "string", + "desc": "GitHub App client ID (e.g., '${{ vars.APP_ID }}').", + "leaf": true + }, + "private-key": { + "type": "string", + "desc": "GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}').", + "leaf": true + }, + "ignore-if-missing": { + "type": "boolean", + "desc": "If true, skip token minting when client-id/private-key resolve to empty strings at runtime.", + "enum": [ + true, + false + ], + "leaf": true + } + } } } } @@ -1993,25 +2289,37 @@ "strict": { "type": "boolean", "desc": "Enable strict mode validation for enhanced security and compliance.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "private": { "type": "boolean", "desc": "Mark the workflow as private, preventing it from being added to other repositories via 'gh aw add'.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "check-for-updates": { "type": "boolean", "desc": "Control whether the compile-agentic version update check runs in the activation job.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "run-install-scripts": { "type": "boolean", "desc": "Allow npm pre/post install scripts to execute during package installation.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "mcp-scripts": { @@ -2025,7 +2333,9 @@ "checkout": { "type": "object|array|boolean", "desc": "Checkout configuration for the agent job.", - "enum": [false], + "enum": [ + false + ], "children": { "repository": { "type": "string", @@ -2055,13 +2365,20 @@ "submodules": { "type": "string|boolean", "desc": "Controls submodule checkout.", - "enum": ["recursive", "true", "false"], + "enum": [ + "recursive", + "true", + "false" + ], "leaf": true }, "lfs": { "type": "boolean", "desc": "Whether to download Git LFS objects.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "token": { @@ -2096,7 +2413,10 @@ "ignore-if-missing": { "type": "boolean", "desc": "If true, skip token minting when client-id/private-key resolve to empty strings at runtime.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "owner": { @@ -2116,181 +2436,300 @@ "administration": { "type": "string", "desc": "Permission level for repository administration (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "codespaces": { "type": "string", "desc": "Permission level for Codespaces (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "codespaces-lifecycle-admin": { "type": "string", "desc": "Permission level for Codespaces lifecycle administration (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "codespaces-metadata": { "type": "string", "desc": "Permission level for Codespaces metadata (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "email-addresses": { "type": "string", "desc": "Permission level for user email addresses (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "environments": { "type": "string", "desc": "Permission level for repository environments (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "git-signing": { "type": "string", "desc": "Permission level for git signing (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "members": { "type": "string", "desc": "Permission level for organization members (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-administration": { "type": "string", "desc": "Permission level for organization administration (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-announcement-banners": { "type": "string", "desc": "Permission level for organization announcement banners (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-codespaces": { "type": "string", "desc": "Permission level for organization Codespaces (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-copilot": { "type": "string", "desc": "Permission level for organization Copilot (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-custom-org-roles": { "type": "string", "desc": "Permission level for organization custom org roles (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-custom-properties": { "type": "string", "desc": "Permission level for organization custom properties (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-custom-repository-roles": { "type": "string", "desc": "Permission level for organization custom repository roles (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-events": { "type": "string", "desc": "Permission level for organization events (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-hooks": { "type": "string", "desc": "Permission level for organization webhooks (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-members": { "type": "string", "desc": "Permission level for organization members management (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-packages": { "type": "string", "desc": "Permission level for organization packages (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-personal-access-token-requests": { "type": "string", "desc": "Permission level for organization personal access token requests (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-personal-access-tokens": { "type": "string", "desc": "Permission level for organization personal access tokens (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-plan": { "type": "string", "desc": "Permission level for organization plan (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-self-hosted-runners": { "type": "string", "desc": "Permission level for organization self-hosted runners (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-user-blocking": { "type": "string", "desc": "Permission level for organization user blocking (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "repository-custom-properties": { "type": "string", "desc": "Permission level for repository custom properties (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "repository-hooks": { "type": "string", "desc": "Permission level for repository webhooks (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "single-file": { "type": "string", "desc": "Permission level for single file access (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "team-discussions": { "type": "string", "desc": "Permission level for team discussions (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "vulnerability-alerts": { "type": "string", "desc": "Permission level for Dependabot vulnerability alerts (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none"], + "enum": [ + "read", + "none" + ], "leaf": true }, "workflows": { "type": "string", "desc": "Permission level for GitHub Actions workflow files (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true } } @@ -2300,7 +2739,10 @@ "current": { "type": "boolean", "desc": "Marks this checkout as the logical current repository for the workflow.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "fetch": { @@ -2312,13 +2754,19 @@ "wiki": { "type": "boolean", "desc": "When true, clones the repository's wiki git instead of the regular repository.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "force-clean-git-credentials": { "type": "boolean", "desc": "When true, persist credentials during checkout, then immediately run a post-checkout cleanup step that removes creden...", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true } }, @@ -2346,7 +2794,10 @@ "ignore-if-missing": { "type": "boolean", "desc": "If true, skip token minting when client-id/private-key resolve to empty strings at runtime.", - "enum": [true, false], + "enum": [ + true, + false + ], "leaf": true }, "owner": { @@ -2366,181 +2817,300 @@ "administration": { "type": "string", "desc": "Permission level for repository administration (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "codespaces": { "type": "string", "desc": "Permission level for Codespaces (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "codespaces-lifecycle-admin": { "type": "string", "desc": "Permission level for Codespaces lifecycle administration (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "codespaces-metadata": { "type": "string", "desc": "Permission level for Codespaces metadata (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "email-addresses": { "type": "string", "desc": "Permission level for user email addresses (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "environments": { "type": "string", "desc": "Permission level for repository environments (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "git-signing": { "type": "string", "desc": "Permission level for git signing (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "members": { "type": "string", "desc": "Permission level for organization members (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-administration": { "type": "string", "desc": "Permission level for organization administration (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-announcement-banners": { "type": "string", "desc": "Permission level for organization announcement banners (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-codespaces": { "type": "string", "desc": "Permission level for organization Codespaces (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-copilot": { "type": "string", "desc": "Permission level for organization Copilot (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-custom-org-roles": { "type": "string", "desc": "Permission level for organization custom org roles (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-custom-properties": { "type": "string", "desc": "Permission level for organization custom properties (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-custom-repository-roles": { "type": "string", "desc": "Permission level for organization custom repository roles (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-events": { "type": "string", "desc": "Permission level for organization events (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-hooks": { "type": "string", "desc": "Permission level for organization webhooks (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-members": { "type": "string", "desc": "Permission level for organization members management (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-packages": { "type": "string", "desc": "Permission level for organization packages (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-personal-access-token-requests": { "type": "string", "desc": "Permission level for organization personal access token requests (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-personal-access-tokens": { "type": "string", "desc": "Permission level for organization personal access tokens (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-plan": { "type": "string", "desc": "Permission level for organization plan (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-self-hosted-runners": { "type": "string", "desc": "Permission level for organization self-hosted runners (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "organization-user-blocking": { "type": "string", "desc": "Permission level for organization user blocking (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "repository-custom-properties": { "type": "string", "desc": "Permission level for repository custom properties (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "repository-hooks": { "type": "string", "desc": "Permission level for repository webhooks (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "single-file": { "type": "string", "desc": "Permission level for single file access (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "team-discussions": { "type": "string", "desc": "Permission level for team discussions (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true }, "vulnerability-alerts": { "type": "string", "desc": "Permission level for Dependabot vulnerability alerts (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none"], + "enum": [ + "read", + "none" + ], "leaf": true }, "workflows": { "type": "string", "desc": "Permission level for GitHub Actions workflow files (read/none; \"write\" is rejected by the compiler).", - "enum": ["read", "none", "write"], + "enum": [ + "read", + "none", + "write" + ], "leaf": true } } @@ -2593,4 +3163,4 @@ "runtimes", "jobs" ] -} +} \ No newline at end of file diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index fe1c42eec28..89887fff245 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -1862,6 +1862,18 @@ sandbox: # (optional) memory: "example-value" + # Enable or disable model fallback for unresolved model selections. Set to false + # for BYOK Azure OpenAI deployments to prevent deployment-name rewriting. Supports + # literal boolean or GitHub Actions expression. + # (optional) + # Accepted formats: + + # Format 1: boolean + model-fallback: true + + # Format 2: GitHub Actions expression that resolves to a boolean at runtime + model-fallback: "example-value" + # Custom sandbox runtime configuration. Note: Network configuration is controlled # by the top-level 'network' field, not here. # (optional) @@ -2520,11 +2532,11 @@ tools: # Format 4: GitHub tools object configuration with restricted function access github: - # List of allowed GitHub API functions (e.g., 'create_issue', 'update_issue', - # 'add_comment') + # List of allowed GitHub API functions. Each entry can be a string tool name + # (e.g., 'issue_read') or an object with per-tool limits (e.g., {name: + # 'issue_read', max-calls: 1}) # (optional) allowed: [] - # Array of strings # GitHub access mode. Prefer 'gh-proxy' for better performance (uses # pre-authenticated gh CLI prompt guidance). Legacy MCP transport values 'local' @@ -7589,6 +7601,29 @@ observability: # (optional) if-missing: "error" + # Optional runtime authentication for OTLP export. Supports GitHub App credentials + # (client-id/app-id + private-key) for token minting, or implicit GitHub OIDC mode + # when the github-app object is present without credentials. + # (optional) + github-app: + # Deprecated alias for client-id. GitHub App ID/client ID (e.g., '${{ vars.APP_ID + # }}'). + # (optional) + app-id: "example-value" + + # GitHub App client ID (e.g., '${{ vars.APP_ID }}'). + # (optional) + client-id: "example-value" + + # GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). + # (optional) + private-key: "example-value" + + # If true, skip token minting when client-id/private-key resolve to empty strings + # at runtime. Defaults to false. + # (optional) + ignore-if-missing: true + # Rate limiting configuration to restrict how frequently users can trigger the # workflow. Helps prevent abuse and resource exhaustion from programmatically # triggered events. diff --git a/pkg/cli/compile_model_fallback_integration_test.go b/pkg/cli/compile_model_fallback_integration_test.go new file mode 100644 index 00000000000..1909ef04f42 --- /dev/null +++ b/pkg/cli/compile_model_fallback_integration_test.go @@ -0,0 +1,37 @@ +//go:build integration + +package cli + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCompileSandboxModelFallbackWorkflow(t *testing.T) { + setup := setupIntegrationTest(t) + defer setup.cleanup() + + srcPath := filepath.Join(projectRoot, "pkg/cli/workflows/test-sandbox-model-fallback.md") + dstPath := filepath.Join(setup.workflowsDir, "test-sandbox-model-fallback.md") + + srcContent, err := os.ReadFile(srcPath) + require.NoError(t, err, "should read source workflow file") + require.NoError(t, os.WriteFile(dstPath, srcContent, 0644), "should write workflow to test dir") + + cmd := exec.Command(setup.binaryPath, "compile", dstPath) + output, err := cmd.CombinedOutput() + require.NoError(t, err, "compile command should succeed\nOutput: %s", string(output)) + + lockPath := filepath.Join(setup.workflowsDir, "test-sandbox-model-fallback.lock.yml") + lockContent, err := os.ReadFile(lockPath) + require.NoError(t, err, "should read lock file") + lockStr := string(lockContent) + + assert.Contains(t, lockStr, `"modelFallback":{"enabled":false}`, + "compiled lock file should embed sandbox.agent.model-fallback in the AWF config JSON") +} diff --git a/pkg/cli/workflows/test-sandbox-model-fallback.md b/pkg/cli/workflows/test-sandbox-model-fallback.md new file mode 100644 index 00000000000..9ba72ab3107 --- /dev/null +++ b/pkg/cli/workflows/test-sandbox-model-fallback.md @@ -0,0 +1,16 @@ +--- +name: Test Sandbox Model Fallback +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +sandbox: + agent: + id: awf + model-fallback: false +--- + +# Test Sandbox Model Fallback + +Verify that sandbox.agent.model-fallback compiles into the AWF config JSON. diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 38bc060c959..d29d1acec87 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -1342,6 +1342,46 @@ func TestMainWorkflowSchema_ProtectedFilesObjectFormStructure(t *testing.T) { } } +func TestMainWorkflowSchema_SandboxAgentModelFallback(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + modelFallback any + }{ + {name: "boolean", modelFallback: false}, + {name: "expression", modelFallback: "${{ inputs.model-fallback }}"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + validFrontmatter := map[string]any{ + "name": "sandbox-agent-model-fallback", + "on": map[string]any{ + "workflow_dispatch": map[string]any{}, + }, + "permissions": map[string]any{ + "contents": "read", + }, + "engine": "copilot", + "sandbox": map[string]any{ + "agent": map[string]any{ + "id": "awf", + "model-fallback": tc.modelFallback, + }, + }, + } + + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(validFrontmatter, "/tmp/gh-aw/sandbox-agent-model-fallback-test.md") + if err != nil { + t.Fatalf("expected sandbox.agent.model-fallback to pass schema validation, got: %v", err) + } + }) + } +} + // TestValidateWithSchema_YAMLIntegerTypes verifies that validateWithSchema accepts // YAML-native integer types (uint64/int64) when the schema expects number/integer. func TestValidateWithSchema_YAMLIntegerTypes(t *testing.T) { diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 11ed9b591ba..8342adf4b8c 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3291,6 +3291,11 @@ "pattern": "^[0-9]+(b|k|m|g|kb|mb|gb|B|K|M|G|KB|MB|GB)$", "examples": ["4g", "8g", "512m"] }, + "model-fallback": { + "$ref": "#/$defs/templatable_boolean", + "description": "Enable or disable model fallback for unresolved model selections. Set to false for BYOK Azure OpenAI deployments to prevent deployment-name rewriting. Supports literal boolean or GitHub Actions expression.", + "examples": [false, "${{ inputs.model-fallback }}"] + }, "config": { "type": "object", "description": "Custom sandbox runtime configuration. Note: Network configuration is controlled by the top-level 'network' field, not here.", diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 317b612221c..525acaf5baa 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -163,6 +163,12 @@ type AWFAPIProxyConfig struct { // MaxEffectiveTokens is the explicit ET budget enforced by the API proxy. MaxEffectiveTokens int64 `json:"maxEffectiveTokens,omitempty"` + // ModelFallback configures the model fallback policy for unresolved model selections. + // When nil, the AWF default (enabled=true, strategy=middle_power) is used. + // Set enabled=false to prevent AWF from silently rewriting deployment names, which + // is needed for BYOK Azure OpenAI deployments where rewriting causes HTTP 404. + ModelFallback *AWFModelFallbackConfig `json:"modelFallback,omitempty"` + // ModelMultipliers configures per-model ET accounting multipliers in AWF. ModelMultipliers map[string]float64 `json:"modelMultipliers,omitempty"` @@ -179,6 +185,15 @@ type AWFAPIProxyConfig struct { Models map[string][]string `json:"models,omitempty"` } +// AWFModelFallbackConfig is the "apiProxy.modelFallback" section of the AWF config file. +// It controls whether model fallback is enabled for unresolved model selections. +type AWFModelFallbackConfig struct { + // Enabled controls whether middle-power fallback is applied when model resolution fails. + // It accepts literal booleans and GitHub Actions expressions. A nil value omits the field, + // letting AWF use its default. + Enabled *TemplatableBool `json:"enabled,omitempty"` +} + // AWFAPITargetConfig is a single API proxy target entry. // Maps to: ---api-target type AWFAPITargetConfig struct { @@ -290,6 +305,15 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { awfConfigLog.Printf("API proxy: %d model multipliers configured", len(apiProxy.ModelMultipliers)) } + if mf := extractModelFallback(config.WorkflowData); mf != nil { + apiProxy.ModelFallback = mf + enabledDisplay := "" + if mf.Enabled != nil { + enabledDisplay = mf.Enabled.String() + } + awfConfigLog.Printf("API proxy: modelFallback configured: enabled=%s", enabledDisplay) + } + targets := map[string]*AWFAPITargetConfig{} if openaiTarget := extractAPITargetHost(config.WorkflowData, "OPENAI_BASE_URL"); openaiTarget != "" { @@ -381,3 +405,24 @@ func extractModelMultipliers(workflowData *WorkflowData) map[string]float64 { } return workflowData.EngineConfig.TokenWeights.Multipliers } + +// extractModelFallback returns an AWFModelFallbackConfig if the workflow has configured +// sandbox.agent.model-fallback, or nil if the field is absent (letting AWF use its default). +func extractModelFallback(workflowData *WorkflowData) *AWFModelFallbackConfig { + if workflowData == nil { + return nil + } + if workflowData.SandboxConfig == nil { + return nil + } + if workflowData.SandboxConfig.Agent == nil { + return nil + } + mf := workflowData.SandboxConfig.Agent.ModelFallback + if mf == nil { + return nil + } + return &AWFModelFallbackConfig{ + Enabled: mf, + } +} diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index 01063d370c2..620771ddd5c 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -454,6 +454,103 @@ 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("model-fallback is emitted when enabled is explicitly set to false", func(t *testing.T) { + disabled := TemplatableBool("false") + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + ModelFallback: &disabled, + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"modelFallback"`, "apiProxy should emit modelFallback when configured") + assert.Contains(t, jsonStr, `"enabled":false`, "apiProxy.modelFallback.enabled should be false") + }) + + t.Run("model-fallback is emitted when enabled is explicitly set to true", func(t *testing.T) { + enabled := TemplatableBool("true") + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + ModelFallback: &enabled, + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"modelFallback"`, "apiProxy should emit modelFallback when configured") + assert.Contains(t, jsonStr, `"enabled":true`, "apiProxy.modelFallback.enabled should be true") + }) + + t.Run("model-fallback supports GitHub Actions expressions", func(t *testing.T) { + expr := TemplatableBool("${{ inputs.model-fallback }}") + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + ModelFallback: &expr, + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"modelFallback"`, "apiProxy should emit modelFallback when configured") + assert.Contains(t, jsonStr, `"enabled":"${{ inputs.model-fallback }}"`, "apiProxy.modelFallback.enabled should preserve expressions") + }) + + t.Run("model-fallback is omitted when not configured in sandbox", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.NotContains(t, jsonStr, `"modelFallback"`, "apiProxy should omit modelFallback when not configured") + }) } // TestBuildAWFConfigSchemaURL verifies that buildAWFConfigSchemaURL returns a release-pinned @@ -631,6 +728,48 @@ func TestBuildAWFConfigJSON_SchemaCompliance(t *testing.T) { }, }, }, + { + name: "config with model-fallback disabled", + config: AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: func() *WorkflowData { + disabled := TemplatableBool("false") + return &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + ModelFallback: &disabled, + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } + }(), + }, + }, + { + name: "config with model-fallback expression", + config: AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: func() *WorkflowData { + expr := TemplatableBool("${{ inputs.model-fallback }}") + return &WorkflowData{ + EngineConfig: &EngineConfig{ID: "copilot"}, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{ + ModelFallback: &expr, + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } + }(), + }, + }, } for _, tc := range cases { diff --git a/pkg/workflow/frontmatter_extraction_security.go b/pkg/workflow/frontmatter_extraction_security.go index 815b251cc51..8c861b2ab28 100644 --- a/pkg/workflow/frontmatter_extraction_security.go +++ b/pkg/workflow/frontmatter_extraction_security.go @@ -243,6 +243,25 @@ func (c *Compiler) extractAgentSandboxConfig(agentVal any) *AgentSandboxConfig { } } + // Extract model-fallback (AWF API proxy model fallback enable/disable flag) + if mfVal, hasMF := agentObj["model-fallback"]; hasMF { + switch v := mfVal.(type) { + case bool: + value := TemplatableBool("false") + if v { + value = TemplatableBool("true") + } + agentConfig.ModelFallback = &value + frontmatterExtractionSecurityLog.Printf("Extracted sandbox.agent.model-fallback") + case string: + if isExpression(v) { + value := TemplatableBool(v) + agentConfig.ModelFallback = &value + frontmatterExtractionSecurityLog.Printf("Extracted sandbox.agent.model-fallback") + } + } + } + return agentConfig } diff --git a/pkg/workflow/frontmatter_extraction_security_test.go b/pkg/workflow/frontmatter_extraction_security_test.go index ee12b26b171..0835d123491 100644 --- a/pkg/workflow/frontmatter_extraction_security_test.go +++ b/pkg/workflow/frontmatter_extraction_security_test.go @@ -24,6 +24,79 @@ func TestExtractAgentSandboxConfigVersion(t *testing.T) { }) } +func TestExtractAgentSandboxConfigModelFallback(t *testing.T) { + compiler := &Compiler{} + + t.Run("extracts sandbox.agent.model-fallback false", func(t *testing.T) { + agentObj := map[string]any{ + "id": "awf", + "model-fallback": false, + } + + config := compiler.extractAgentSandboxConfig(agentObj) + require.NotNil(t, config, "Should extract agent sandbox config") + require.NotNil(t, config.ModelFallback, "Should extract model-fallback") + assert.Equal(t, "false", config.ModelFallback.String(), "Should normalize false to string form") + }) + + t.Run("extracts sandbox.agent.model-fallback true", func(t *testing.T) { + agentObj := map[string]any{ + "id": "awf", + "model-fallback": true, + } + + config := compiler.extractAgentSandboxConfig(agentObj) + require.NotNil(t, config, "Should extract agent sandbox config") + require.NotNil(t, config.ModelFallback, "Should extract model-fallback") + assert.Equal(t, "true", config.ModelFallback.String(), "Should normalize true to string form") + }) + + t.Run("extracts sandbox.agent.model-fallback expression", func(t *testing.T) { + expr := "${{ inputs.model-fallback }}" + agentObj := map[string]any{ + "id": "awf", + "model-fallback": expr, + } + + config := compiler.extractAgentSandboxConfig(agentObj) + require.NotNil(t, config, "Should extract agent sandbox config") + require.NotNil(t, config.ModelFallback, "Should extract model-fallback") + assert.Equal(t, expr, config.ModelFallback.String(), "Should preserve expression") + }) + + t.Run("model-fallback is nil when absent", func(t *testing.T) { + agentObj := map[string]any{ + "id": "awf", + } + + config := compiler.extractAgentSandboxConfig(agentObj) + require.NotNil(t, config, "Should extract agent sandbox config") + assert.Nil(t, config.ModelFallback, "ModelFallback should be nil when not configured") + }) + + t.Run("model-fallback is nil when value is not a boolean or expression", func(t *testing.T) { + agentObj := map[string]any{ + "id": "awf", + "model-fallback": "not-an-expression", + } + + config := compiler.extractAgentSandboxConfig(agentObj) + require.NotNil(t, config, "Should extract agent sandbox config") + assert.Nil(t, config.ModelFallback, "ModelFallback should be nil for invalid strings") + }) + + t.Run("model-fallback is nil when value is an object", func(t *testing.T) { + agentObj := map[string]any{ + "id": "awf", + "model-fallback": map[string]any{"enabled": false}, + } + + config := compiler.extractAgentSandboxConfig(agentObj) + require.NotNil(t, config, "Should extract agent sandbox config") + assert.Nil(t, config.ModelFallback, "ModelFallback should be nil for object value") + }) +} + // TestExtractMCPGatewayConfigPayloadFields tests extraction of payload-related fields // from MCP gateway frontmatter configuration func TestExtractMCPGatewayConfigPayloadFields(t *testing.T) { diff --git a/pkg/workflow/sandbox.go b/pkg/workflow/sandbox.go index 525926167a5..1239c377fac 100644 --- a/pkg/workflow/sandbox.go +++ b/pkg/workflow/sandbox.go @@ -46,16 +46,17 @@ 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") + 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) } // SandboxRuntimeConfig represents the Anthropic Sandbox Runtime configuration diff --git a/pkg/workflow/schemas/awf-config.schema.json b/pkg/workflow/schemas/awf-config.schema.json index e7c2a15410e..c9434985b34 100644 --- a/pkg/workflow/schemas/awf-config.schema.json +++ b/pkg/workflow/schemas/awf-config.schema.json @@ -88,8 +88,17 @@ "additionalProperties": false, "properties": { "enabled": { - "type": "boolean", - "description": "Enable or disable middle-power fallback when model resolution fails." + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^\\$\\{\\{.*\\}\\}$", + "description": "GitHub Actions expression that resolves to a boolean at runtime" + } + ], + "description": "Enable or disable middle-power fallback when model resolution fails. Supports literal booleans and GitHub Actions expressions in compiled workflows." }, "strategy": { "type": "string", diff --git a/pkg/workflow/templatables.go b/pkg/workflow/templatables.go index ccddf1fc8ef..75485f971bb 100644 --- a/pkg/workflow/templatables.go +++ b/pkg/workflow/templatables.go @@ -132,6 +132,30 @@ func (t *TemplatableInt32) Ptr() *TemplatableInt32 { return &v } +// TemplatableBool represents a boolean frontmatter field that also accepts +// GitHub Actions expression strings (e.g. "${{ inputs.enabled }}"). The +// underlying value is always stored as a string: boolean literals as "true" or +// "false", expressions verbatim. +type TemplatableBool string + +// MarshalJSON emits a JSON boolean for literal values and a JSON string for +// GitHub Actions expressions. +func (t *TemplatableBool) MarshalJSON() ([]byte, error) { + switch string(*t) { + case "true": + return json.Marshal(true) + case "false": + return json.Marshal(false) + default: + return json.Marshal(string(*t)) + } +} + +// String returns the underlying string representation of the value. +func (t *TemplatableBool) String() string { + return string(*t) +} + // buildTemplatableBoolEnvVar returns a YAML environment variable entry for a // templatable boolean field. If value is a GitHub Actions expression it is // embedded unquoted so that GitHub Actions can evaluate it at runtime; diff --git a/specs/awf-config-sources-spec.md b/specs/awf-config-sources-spec.md index 11eabdd16fa..aa08f63b58d 100644 --- a/specs/awf-config-sources-spec.md +++ b/specs/awf-config-sources-spec.md @@ -58,6 +58,7 @@ The following fields previously existed in schema but were missed in spec CLI ma | `apiProxy.anthropicCacheTailTtl` | `--anthropic-cache-tail-ttl` | | `apiProxy.models` | config-only (model alias rewriting) | | `apiProxy.modelMultipliers` | config-only (effective-token accounting) | +| `apiProxy.modelFallback` | config-only (model fallback policy; set `sandbox.agent.model-fallback: false` to prevent deployment-name rewriting for BYOK Azure) | | `apiProxy.maxRuns` | config-only (LLM invocation hard cap) | | `apiProxy.auth.*` | config-only (maps to `AWF_AUTH_*` env vars) | | `container.dockerHostPathPrefix` | `--docker-host-path-prefix` |