From cc5a69e93eb0b6149bee38e17d1589436935097c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 01:35:42 +0000 Subject: [PATCH 1/6] Sync AWF schema and wire model multipliers into config generation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .changeset/patch-awf-spec-sync-v0-25-43.md | 5 + pkg/workflow/awf_config.go | 11 ++ pkg/workflow/awf_config_test.go | 28 +++++ pkg/workflow/schemas/awf-config.schema.json | 132 ++++++++++++++++++-- specs/awf-config-sources-spec.md | 4 + 5 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 .changeset/patch-awf-spec-sync-v0-25-43.md diff --git a/.changeset/patch-awf-spec-sync-v0-25-43.md b/.changeset/patch-awf-spec-sync-v0-25-43.md new file mode 100644 index 00000000000..adf0c3b1bf3 --- /dev/null +++ b/.changeset/patch-awf-spec-sync-v0-25-43.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Sync embedded AWF config schema with the latest `gh-aw-firewall` spec updates and start emitting `apiProxy.modelMultipliers` in generated AWF config when `engine.token-weights.multipliers` is configured. diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index c9398af35a3..8984b331678 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -156,6 +156,9 @@ type AWFAPIProxyConfig struct { // MaxEffectiveTokens is the explicit ET budget enforced by the API proxy. MaxEffectiveTokens int64 `json:"maxEffectiveTokens,omitempty"` + // ModelMultipliers configures per-model ET accounting multipliers in AWF. + ModelMultipliers map[string]float64 `json:"modelMultipliers,omitempty"` + // Targets holds per-provider API target overrides. // Supported keys: "openai", "anthropic", "copilot", "gemini" Targets map[string]*AWFAPITargetConfig `json:"targets,omitempty"` @@ -254,6 +257,14 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { MaxEffectiveTokens: maxEffectiveTokens, } + if config.WorkflowData != nil && + config.WorkflowData.EngineConfig != nil && + config.WorkflowData.EngineConfig.TokenWeights != nil && + len(config.WorkflowData.EngineConfig.TokenWeights.Multipliers) > 0 { + apiProxy.ModelMultipliers = config.WorkflowData.EngineConfig.TokenWeights.Multipliers + awfConfigLog.Printf("API proxy: %d model multipliers configured", len(apiProxy.ModelMultipliers)) + } + targets := map[string]*AWFAPITargetConfig{} if openaiTarget := extractAPITargetHost(config.WorkflowData, "OPENAI_BASE_URL"); openaiTarget != "" { diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index a05dd909b59..f57b850af6b 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -117,6 +118,33 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.Contains(t, jsonStr, `"maxEffectiveTokens":424242`, "apiProxy should emit configured maxEffectiveTokens") }) + t.Run("engine token-weights multipliers are emitted in apiProxy modelMultipliers", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + TokenWeights: &types.TokenWeights{ + Multipliers: map[string]float64{ + "gpt-5": 1.2, + "gpt-5-mini": 0.8, + }, + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"modelMultipliers"`, "apiProxy should emit modelMultipliers") + assert.Contains(t, jsonStr, `"gpt-5":1.2`, "apiProxy should include configured model multiplier") + assert.Contains(t, jsonStr, `"gpt-5-mini":0.8`, "apiProxy should include configured model multiplier") + }) + t.Run("anthropic API target is included in apiProxy targets", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "claude", diff --git a/pkg/workflow/schemas/awf-config.schema.json b/pkg/workflow/schemas/awf-config.schema.json index 43ca2823334..80d956e78cf 100644 --- a/pkg/workflow/schemas/awf-config.schema.json +++ b/pkg/workflow/schemas/awf-config.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/github/gh-aw-firewall/releases/download/v0.25.38/awf-config.schema.json", + "$id": "https://raw.githubusercontent.com/github/gh-aw-firewall/main/docs/awf-config.schema.json", "title": "AWF Configuration", "description": "JSON/YAML configuration for awf CLI. CLI flags override config file values. See https://github.com/github/gh-aw-firewall for documentation.", "type": "object", @@ -49,12 +49,7 @@ "properties": { "enabled": { "type": "boolean", - "description": "Enable the API proxy sidecar container." - }, - "maxEffectiveTokens": { - "type": "integer", - "minimum": 1, - "description": "Maximum effective-token (ET) budget enforced by the API proxy." + "description": "Enable the API proxy sidecar container. When enabled, source credentials (OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_GITHUB_TOKEN, COPILOT_API_KEY, GEMINI_API_KEY) are held exclusively in the sidecar and excluded from the agent environment. The agent receives proxy-routing base URLs instead. See docs/awf-config-spec.md §9 for credential isolation semantics." }, "enableOpenCode": { "type": "boolean", @@ -66,9 +61,30 @@ }, "anthropicCacheTailTtl": { "type": "string", - "enum": ["5m", "1h"], + "enum": [ + "5m", + "1h" + ], "description": "TTL for Anthropic cache tail optimization. Only applies when anthropicAutoCache is enabled. Allowed values: \"5m\" or \"1h\"." }, + "maxEffectiveTokens": { + "type": "integer", + "minimum": 1, + "description": "Maximum cumulative effective tokens allowed for a run. When reached, the API proxy rejects subsequent requests with HTTP 429 and error type 'effective_tokens_limit_exceeded'. Tokens are weighted: input ×1, cache-read ×0.1, output ×4, reasoning ×4. See spec §10." + }, + "modelMultipliers": { + "type": "object", + "description": "Per-model multipliers for effective token accounting. Each model's weighted tokens are multiplied by this value before accumulation. Defaults to 1 for unlisted models. See spec §10.2.", + "additionalProperties": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "maxRuns": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of LLM invocations allowed for a run. When reached, the API proxy rejects subsequent requests with HTTP 429 and error type 'max_runs_exceeded'. See spec §11." + }, "targets": { "type": "object", "description": "Override upstream API endpoints for each provider.", @@ -101,6 +117,93 @@ "type": "string" } } + }, + "auth": { + "type": "object", + "description": "Authentication configuration for the API proxy sidecar. Enables OIDC-based credential exchange (e.g., GitHub OIDC → Azure AD, AWS STS, or GCP Workload Identity). See docs/awf-config-spec.md §9.5.", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": ["github-oidc"], + "description": "Authentication type. Currently only 'github-oidc' is supported. Maps to AWF_AUTH_TYPE." + }, + "provider": { + "type": "string", + "enum": ["azure", "aws", "gcp"], + "description": "Cloud provider for OIDC token exchange. Determines which token exchange protocol is used. Maps to AWF_AUTH_PROVIDER.", + "default": "azure" + }, + "oidcAudience": { + "type": "string", + "description": "Audience claim for the GitHub OIDC token. Provider-specific defaults apply when omitted: Azure='api://AzureADTokenExchange', AWS='sts.amazonaws.com', GCP=workloadIdentityProvider value. Maps to AWF_AUTH_OIDC_AUDIENCE." + }, + "azureTenantId": { + "type": "string", + "description": "Azure AD tenant ID for federated credential exchange. Required when provider is 'azure'. Maps to AWF_AUTH_AZURE_TENANT_ID." + }, + "azureClientId": { + "type": "string", + "description": "Azure AD application (client) ID for the federated credential. Required when provider is 'azure'. Maps to AWF_AUTH_AZURE_CLIENT_ID." + }, + "azureScope": { + "type": "string", + "description": "Azure token scope. Maps to AWF_AUTH_AZURE_SCOPE.", + "default": "https://cognitiveservices.azure.com/.default" + }, + "azureCloud": { + "type": "string", + "enum": ["public", "usgovernment", "china"], + "description": "Azure cloud environment. Maps to AWF_AUTH_AZURE_CLOUD.", + "default": "public" + }, + "awsRoleArn": { + "type": "string", + "description": "AWS IAM role ARN to assume via OIDC federation. Required when provider is 'aws'. Maps to AWF_AUTH_AWS_ROLE_ARN." + }, + "awsRegion": { + "type": "string", + "description": "AWS region for the Bedrock endpoint. Required when provider is 'aws'. Maps to AWF_AUTH_AWS_REGION." + }, + "awsRoleSessionName": { + "type": "string", + "description": "Session name for the AWS STS AssumeRoleWithWebIdentity call. Maps to AWF_AUTH_AWS_ROLE_SESSION_NAME.", + "default": "awf-oidc-session" + }, + "gcpWorkloadIdentityProvider": { + "type": "string", + "description": "Full resource name of the GCP Workload Identity Provider (projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID). Required when provider is 'gcp'. Maps to AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER." + }, + "gcpServiceAccount": { + "type": "string", + "description": "GCP service account email to impersonate. When omitted, the federated token is used directly (requires direct resource access grants on the principal). Maps to AWF_AUTH_GCP_SERVICE_ACCOUNT." + }, + "gcpScope": { + "type": "string", + "description": "OAuth2 scope for GCP token. Maps to AWF_AUTH_GCP_SCOPE.", + "default": "https://www.googleapis.com/auth/cloud-platform" + } + }, + "required": ["type"], + "if": { + "properties": { "provider": { "const": "aws" } }, + "required": ["provider"] + }, + "then": { + "required": ["awsRoleArn", "awsRegion"] + }, + "else": { + "if": { + "properties": { "provider": { "const": "gcp" } }, + "required": ["provider"] + }, + "then": { + "required": ["gcpWorkloadIdentityProvider"] + }, + "else": { + "required": ["azureTenantId", "azureClientId"] + } + } } } }, @@ -219,12 +322,16 @@ "dockerHost": { "type": "string", "description": "Docker daemon socket or host to connect to (e.g. \"unix:///var/run/docker.sock\")." + }, + "dockerHostPathPrefix": { + "type": "string", + "description": "Prefix bind-mount source paths so the Docker daemon can resolve runner filesystem paths. Required for ARC DinD sidecar runners where the runner and daemon have separate filesystems. Example: \"/host\". Kernel virtual filesystems (/dev, /sys, /proc) are automatically excluded from prefixing." } } }, "environment": { "type": "object", - "description": "Environment variable propagation into the agent container.", + "description": "Environment variable propagation into the agent container. Merge behavior is: AWF-reserved variables are set by AWF and are not overridden by envAll or envFile; if envAll is true, host environment variables are forwarded next; envFile is then applied only for variables not already present, so it does not override envAll; CLI -e/--env has highest precedence and may override any variable, including AWF-reserved ones. When apiProxy.enabled is true, source credentials (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) are excluded from the agent and held in the API proxy sidecar. See docs/awf-config-spec.md §8–9 for credential isolation rules.", "additionalProperties": false, "properties": { "envFile": { @@ -251,7 +358,12 @@ "properties": { "logLevel": { "type": "string", - "enum": ["debug", "info", "warn", "error"], + "enum": [ + "debug", + "info", + "warn", + "error" + ], "description": "Log verbosity level. Defaults to \"info\"." }, "diagnosticLogs": { diff --git a/specs/awf-config-sources-spec.md b/specs/awf-config-sources-spec.md index 1e41dab4b35..cc7b1e6304e 100644 --- a/specs/awf-config-sources-spec.md +++ b/specs/awf-config-sources-spec.md @@ -57,6 +57,10 @@ The following fields previously existed in schema but were missed in spec CLI ma | `apiProxy.anthropicAutoCache` | `--anthropic-auto-cache` | | `apiProxy.anthropicCacheTailTtl` | `--anthropic-cache-tail-ttl` | | `apiProxy.models` | config-only (model alias rewriting) | +| `apiProxy.modelMultipliers` | config-only (effective-token accounting) | +| `apiProxy.maxRuns` | config-only (LLM invocation hard cap) | +| `apiProxy.auth.*` | config-only (maps to `AWF_AUTH_*` env vars) | +| `container.dockerHostPathPrefix` | `--docker-host-path-prefix` | Agents SHOULD treat this class of mismatch as a regression signal and open a corrective PR when detected. From 43b25d16199e101149d8ca6e2b3c4671998026bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 01:37:16 +0000 Subject: [PATCH 2/6] Refine AWF model multipliers mapping and expand tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/awf_config.go | 14 +++++++++----- pkg/workflow/awf_config_test.go | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 8984b331678..07176715473 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -257,11 +257,8 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { MaxEffectiveTokens: maxEffectiveTokens, } - if config.WorkflowData != nil && - config.WorkflowData.EngineConfig != nil && - config.WorkflowData.EngineConfig.TokenWeights != nil && - len(config.WorkflowData.EngineConfig.TokenWeights.Multipliers) > 0 { - apiProxy.ModelMultipliers = config.WorkflowData.EngineConfig.TokenWeights.Multipliers + if modelMultipliers := extractModelMultipliers(config.WorkflowData); len(modelMultipliers) > 0 { + apiProxy.ModelMultipliers = modelMultipliers awfConfigLog.Printf("API proxy: %d model multipliers configured", len(apiProxy.ModelMultipliers)) } @@ -335,3 +332,10 @@ func splitDomainList(domains string) []string { } return result } + +func extractModelMultipliers(workflowData *WorkflowData) map[string]float64 { + if workflowData == nil || workflowData.EngineConfig == nil || workflowData.EngineConfig.TokenWeights == nil { + return nil + } + return workflowData.EngineConfig.TokenWeights.Multipliers +} diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index f57b850af6b..3a357c1340a 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -145,6 +145,28 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.Contains(t, jsonStr, `"gpt-5-mini":0.8`, "apiProxy should include configured model multiplier") }) + t.Run("apiProxy modelMultipliers omitted when engine token-weights multipliers are empty", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + TokenWeights: &types.TokenWeights{ + Multipliers: map[string]float64{}, + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.NotContains(t, jsonStr, `"modelMultipliers"`, "apiProxy should omit modelMultipliers when empty") + }) + t.Run("anthropic API target is included in apiProxy targets", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "claude", From 637243d2778bb1861a35dc20cbcef15784748689 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 01:38:08 +0000 Subject: [PATCH 3/6] Harden AWF model multipliers extraction Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/awf_config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 07176715473..ff3a9babe1b 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -337,5 +337,8 @@ func extractModelMultipliers(workflowData *WorkflowData) map[string]float64 { if workflowData == nil || workflowData.EngineConfig == nil || workflowData.EngineConfig.TokenWeights == nil { return nil } + if len(workflowData.EngineConfig.TokenWeights.Multipliers) == 0 { + return nil + } return workflowData.EngineConfig.TokenWeights.Multipliers } From 0992994c93c34a7710f9a2fe8caba53cc6ba385c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 02:54:31 +0000 Subject: [PATCH 4/6] Add draft ADR-31398 for AWF v0.25.43 schema sync and modelMultipliers wire-up --- ...irewall-spec-and-wire-model-multipliers.md | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 docs/adr/31398-sync-awf-firewall-spec-and-wire-model-multipliers.md diff --git a/docs/adr/31398-sync-awf-firewall-spec-and-wire-model-multipliers.md b/docs/adr/31398-sync-awf-firewall-spec-and-wire-model-multipliers.md new file mode 100644 index 00000000000..0fb3adbd25f --- /dev/null +++ b/docs/adr/31398-sync-awf-firewall-spec-and-wire-model-multipliers.md @@ -0,0 +1,95 @@ +--- +name: ADR-31398 sync AWF v0.25.43 schema and wire apiProxy.modelMultipliers +description: Records the decision to track the gh-aw-firewall config schema in lockstep and to source apiProxy.modelMultipliers from existing engine.token-weights.multipliers rather than a new frontmatter field +type: project +--- + +# ADR-31398: Sync AWF v0.25.43 schema and wire `apiProxy.modelMultipliers` from existing `engine.token-weights.multipliers` + +**Date**: 2026-05-11 +**Status**: Draft +**Deciders**: pelikhan (PR author), to be confirmed + +--- + +## Part 1 — Narrative (Human-Friendly) + +### Context + +`gh-aw` embeds the `gh-aw-firewall` (AWF) JSON schema at `pkg/workflow/schemas/awf-config.schema.json` and generates a populated AWF config object at compile time from workflow frontmatter. The embedded schema was last synced against firewall `v0.25.38`; since then, upstream firewall `v0.25.43` introduced new config surface — `apiProxy.modelMultipliers`, `apiProxy.maxRuns`, `apiProxy.auth.*` (GitHub-OIDC → Azure/AWS/GCP exchange), and `container.dockerHostPathPrefix`. Workflow frontmatter already carries a `engine.token-weights.multipliers` map that captures per-model effective-token weights, but the generated AWF config was not emitting it because the schema field did not yet exist. Without a sync, valid AWF capabilities are silently unreachable from `gh-aw`, and `specs/awf-config-sources-spec.md` (which tracks "known drift") goes out of date. + +### Decision + +We will sync the embedded AWF schema to the firewall `main` snapshot covering `v0.25.43`, and we will wire the newly available `apiProxy.modelMultipliers` field from the **existing** `engine.token-weights.multipliers` workflow input rather than introducing a new frontmatter key. Emission is conditional: when the multipliers map is absent or empty, the generated AWF config **omits** the field entirely (relying on AWF's per-model default of `1`). The drift-tracking spec (`specs/awf-config-sources-spec.md`) is updated in the same PR so the newly surfaced config paths are explicitly recorded. + +### Alternatives Considered + +#### Alternative 1: Introduce a new dedicated frontmatter field for AWF model multipliers + +Add a new `firewall.api-proxy.model-multipliers` (or similar) frontmatter block dedicated to AWF enforcement, distinct from `engine.token-weights.multipliers`. Rejected because the multiplier values represent the same domain concept (per-model effective-token weighting), and splitting them into two configuration surfaces would force authors to keep the two in sync manually, with no benefit. The existing `engine.token-weights.multipliers` field is already typed as `map[string]float64` and is structurally identical to what the AWF schema requires. + +#### Alternative 2: Defer the schema sync until a user requests one of the new fields + +Leave the embedded schema pinned at `v0.25.38` and only sync when a concrete user need surfaces. Rejected because schema drift accumulates silently — each missed sync makes the next one larger and increases the chance that `gh-aw` validates a config that the live firewall would reject (or vice versa). Tracking drift via `awf-config-sources-spec.md` only works when the schema is kept current. + +#### Alternative 3: Sync the schema but do not wire `modelMultipliers` in this PR + +Land the schema update only and treat `modelMultipliers` emission as a follow-up. Rejected as a close call: the wire-up is small (≈18 lines of Go plus a helper) and is the highest-value field in the sync because `gh-aw` already carries the data. Splitting it out would leave the field visible-but-unused in the schema and require a second PR to deliver value users can already configure. + +### Consequences + +#### Positive +- `gh-aw` no longer silently drops per-model token weights when firewall enforcement is enabled — the multipliers configured under `engine.token-weights.multipliers` now reach the API proxy and influence its 429 budget enforcement. +- Schema drift is eliminated for `v0.25.43`, restoring the invariant that `gh-aw`'s embedded schema and the live firewall accept the same documents. +- The drift-tracking spec records the four newly surfaced config paths, making the next sync's diff easier to reason about. +- Authors get the new feature without learning a new frontmatter key. + +#### Negative +- `engine.token-weights.multipliers` now has two distinct effects (internal token accounting **and** AWF proxy-side hard enforcement of effective-token budgets), so changing this map can produce 429 responses at runtime rather than purely affecting accounting. This dual semantics is not obvious from the field name. +- Embedded-schema sync coupling: `gh-aw` releases now have a soft dependency on the firewall spec cadence — every firewall schema change requires a corresponding `gh-aw` PR to stay drift-free. +- The schema sync adds substantial new surface (`apiProxy.auth.*` OIDC config with conditional `if/then/else` validation for Azure/AWS/GCP) that is **not** yet wired through from frontmatter; readers of the schema may incorrectly assume those paths are reachable via `gh-aw`. + +#### Neutral +- Conditional emission (omit when empty) preserves backward compatibility with workflows that do not set multipliers — generated AWF configs are byte-for-byte identical to the pre-PR output in that case. +- Two new test cases (emission when configured, omission when empty) anchor the behavior; future changes to `extractModelMultipliers` will trip them. +- A future ADR may be needed when `apiProxy.maxRuns`, `apiProxy.auth.*`, or `container.dockerHostPathPrefix` are wired through from frontmatter — those decisions are deferred and not in scope here. + +--- + +## Part 2 — Normative Specification (RFC 2119) + +> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +### Embedded schema sync + +1. `pkg/workflow/schemas/awf-config.schema.json` **MUST** track the published `gh-aw-firewall` schema for the version (or snapshot) that `gh-aw` is integrated against. +2. The schema's `$id` **MUST** point to a stable, fetchable URL identifying the upstream source of truth (a release artifact URL or the `main`-branch raw URL is acceptable). +3. When a firewall schema change adds new config paths, `specs/awf-config-sources-spec.md` **MUST** be updated in the same PR to record those paths under its "known drift" coverage table. +4. Schema syncs **MUST NOT** narrow the accepted document set in ways that reject configurations the previous embedded schema accepted, unless the upstream firewall also rejects them. + +### `apiProxy.modelMultipliers` wire-up + +1. When the workflow's `engine.token-weights.multipliers` map is non-empty, `BuildAWFConfigJSON` **MUST** emit those entries verbatim as `apiProxy.modelMultipliers` in the generated AWF config. +2. When `engine.token-weights.multipliers` is absent, `nil`, or empty, `BuildAWFConfigJSON` **MUST** omit the `apiProxy.modelMultipliers` key entirely from the generated AWF config (the `omitempty` JSON tag is required on the struct field). +3. The implementation **MUST NOT** introduce a separate AWF-specific frontmatter field for per-model multipliers; `engine.token-weights.multipliers` is the single source of truth. +4. The multiplier values **MUST** be passed through unchanged; the implementation **MUST NOT** apply normalization, scaling, or default-injection on the path between frontmatter and AWF config. +5. The helper that extracts multipliers **MUST** be nil-safe with respect to `WorkflowData`, `EngineConfig`, and `TokenWeights` so that workflows without an engine configuration do not panic. + +### Test coverage + +1. `pkg/workflow/awf_config_test.go` **MUST** include a test that asserts `apiProxy.modelMultipliers` is emitted with the configured values when `engine.token-weights.multipliers` is non-empty. +2. `pkg/workflow/awf_config_test.go` **MUST** include a test that asserts `apiProxy.modelMultipliers` is **not** present in the generated JSON when `engine.token-weights.multipliers` is empty. +3. Tests **SHOULD** assert on the raw generated JSON string (e.g., `assert.Contains` / `assert.NotContains` on a marshalled config) rather than the in-memory struct, to detect regressions in `omitempty` handling. + +### Out of scope for this ADR + +1. Wire-up of `apiProxy.maxRuns`, `apiProxy.auth.*`, and `container.dockerHostPathPrefix` from frontmatter is **NOT REQUIRED** by this ADR and **MAY** be addressed in subsequent ADRs. +2. Validation that frontmatter multiplier values fall within a sensible numeric range is **NOT REQUIRED** by this ADR; the AWF schema's `exclusiveMinimum: 0` constraint is the authoritative validator at the firewall layer. + +### Conformance + +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/25647329877) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* From b63cb2b6611f370a3c27178bf8485d734c8187dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 03:07:16 +0000 Subject: [PATCH 5/6] Wire user-rate-limit max-runs-per-window to apiProxy.maxRuns Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/awf_config.go | 14 ++++++++++++++ pkg/workflow/awf_config_test.go | 24 +++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index ff3a9babe1b..efc15203da6 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -159,6 +159,9 @@ type AWFAPIProxyConfig struct { // ModelMultipliers configures per-model ET accounting multipliers in AWF. ModelMultipliers map[string]float64 `json:"modelMultipliers,omitempty"` + // MaxRuns caps the number of LLM API calls AWF allows in a run. + MaxRuns int `json:"maxRuns,omitempty"` + // Targets holds per-provider API target overrides. // Supported keys: "openai", "anthropic", "copilot", "gemini" Targets map[string]*AWFAPITargetConfig `json:"targets,omitempty"` @@ -261,6 +264,10 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { apiProxy.ModelMultipliers = modelMultipliers awfConfigLog.Printf("API proxy: %d model multipliers configured", len(apiProxy.ModelMultipliers)) } + if maxRuns := extractAPIMaxRuns(config.WorkflowData); maxRuns > 0 { + apiProxy.MaxRuns = maxRuns + awfConfigLog.Printf("API proxy: maxRuns=%d", apiProxy.MaxRuns) + } targets := map[string]*AWFAPITargetConfig{} @@ -342,3 +349,10 @@ func extractModelMultipliers(workflowData *WorkflowData) map[string]float64 { } return workflowData.EngineConfig.TokenWeights.Multipliers } + +func extractAPIMaxRuns(workflowData *WorkflowData) int { + if workflowData == nil || workflowData.RateLimit == nil { + return 0 + } + return workflowData.RateLimit.Max +} diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index 3a357c1340a..53b00097a41 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -118,6 +118,28 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.Contains(t, jsonStr, `"maxEffectiveTokens":424242`, "apiProxy should emit configured maxEffectiveTokens") }) + t.Run("user-rate-limit max-runs-per-window is emitted as apiProxy maxRuns", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "copilot", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "copilot", + }, + RateLimit: &RateLimitConfig{ + Max: 7, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + assert.Contains(t, jsonStr, `"maxRuns":7`, "apiProxy should emit configured maxRuns") + }) + t.Run("engine token-weights multipliers are emitted in apiProxy modelMultipliers", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "copilot", @@ -127,7 +149,7 @@ func TestBuildAWFConfigJSON(t *testing.T) { ID: "copilot", TokenWeights: &types.TokenWeights{ Multipliers: map[string]float64{ - "gpt-5": 1.2, + "gpt-5": 1.2, "gpt-5-mini": 0.8, }, }, From c6b33e8f65e60c1395bf7785c847ede93f394d17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 03:54:48 +0000 Subject: [PATCH 6/6] Revert apiProxy maxRuns mapping change Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/awf_config.go | 14 -------------- pkg/workflow/awf_config_test.go | 24 +----------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index efc15203da6..ff3a9babe1b 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -159,9 +159,6 @@ type AWFAPIProxyConfig struct { // ModelMultipliers configures per-model ET accounting multipliers in AWF. ModelMultipliers map[string]float64 `json:"modelMultipliers,omitempty"` - // MaxRuns caps the number of LLM API calls AWF allows in a run. - MaxRuns int `json:"maxRuns,omitempty"` - // Targets holds per-provider API target overrides. // Supported keys: "openai", "anthropic", "copilot", "gemini" Targets map[string]*AWFAPITargetConfig `json:"targets,omitempty"` @@ -264,10 +261,6 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { apiProxy.ModelMultipliers = modelMultipliers awfConfigLog.Printf("API proxy: %d model multipliers configured", len(apiProxy.ModelMultipliers)) } - if maxRuns := extractAPIMaxRuns(config.WorkflowData); maxRuns > 0 { - apiProxy.MaxRuns = maxRuns - awfConfigLog.Printf("API proxy: maxRuns=%d", apiProxy.MaxRuns) - } targets := map[string]*AWFAPITargetConfig{} @@ -349,10 +342,3 @@ func extractModelMultipliers(workflowData *WorkflowData) map[string]float64 { } return workflowData.EngineConfig.TokenWeights.Multipliers } - -func extractAPIMaxRuns(workflowData *WorkflowData) int { - if workflowData == nil || workflowData.RateLimit == nil { - return 0 - } - return workflowData.RateLimit.Max -} diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index 53b00097a41..3a357c1340a 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -118,28 +118,6 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.Contains(t, jsonStr, `"maxEffectiveTokens":424242`, "apiProxy should emit configured maxEffectiveTokens") }) - t.Run("user-rate-limit max-runs-per-window is emitted as apiProxy maxRuns", func(t *testing.T) { - config := AWFCommandConfig{ - EngineName: "copilot", - AllowedDomains: "github.com", - WorkflowData: &WorkflowData{ - EngineConfig: &EngineConfig{ - ID: "copilot", - }, - RateLimit: &RateLimitConfig{ - Max: 7, - }, - NetworkPermissions: &NetworkPermissions{ - Firewall: &FirewallConfig{Enabled: true}, - }, - }, - } - - jsonStr, err := BuildAWFConfigJSON(config) - require.NoError(t, err) - assert.Contains(t, jsonStr, `"maxRuns":7`, "apiProxy should emit configured maxRuns") - }) - t.Run("engine token-weights multipliers are emitted in apiProxy modelMultipliers", func(t *testing.T) { config := AWFCommandConfig{ EngineName: "copilot", @@ -149,7 +127,7 @@ func TestBuildAWFConfigJSON(t *testing.T) { ID: "copilot", TokenWeights: &types.TokenWeights{ Multipliers: map[string]float64{ - "gpt-5": 1.2, + "gpt-5": 1.2, "gpt-5-mini": 0.8, }, },