From a42c828f6f0de0ca79eef08c7227e8a83dc1224a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 21:39:14 +0000 Subject: [PATCH 1/4] Initial plan From b14543122fdf702ea4b131a1faf6f257b4841c8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 21:48:38 +0000 Subject: [PATCH 2/4] feat: publish versioned JSON Schema for AWF config file Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/67604e30-6b7e-4524-8572-239a489b0733 --- .github/workflows/release.yml | 9 + README.md | 2 +- docs/awf-config-spec.md | 4 +- docs/awf-config.schema.json | 281 ++++++++++++++++++++----- package.json | 3 +- scripts/generate-schema.mjs | 386 ++++++++++++++++++++++++++++++++++ src/schema.test.ts | 176 ++++++++++++++++ 7 files changed, 808 insertions(+), 53 deletions(-) create mode 100644 scripts/generate-schema.mjs create mode 100644 src/schema.test.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66293f8d..92d4cef3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -520,6 +520,14 @@ jobs: echo "Generated containers.txt:" cat release/containers.txt + - name: Generate versioned JSON Schema + run: | + mkdir -p release + node scripts/generate-schema.mjs --version ${{ needs.bump-version.outputs.version }} + cp docs/awf-config.schema.json release/awf-config.schema.json + echo "=== Schema preview (first 10 lines) ===" + head -10 release/awf-config.schema.json + - name: Generate checksums run: | cd release @@ -645,6 +653,7 @@ jobs: release/awf-bundle.js release/awf.tgz release/containers.txt + release/awf-config.schema.json release/checksums.txt env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 8f3b3954..d856207e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The `--` separator divides firewall options from the command to run. - [Quick start](docs/quickstart.md) — install, verify, and run your first command - [Usage guide](docs/usage.md) — CLI flags, domain allowlists, examples -- [AWF config schema](docs/awf-config.schema.json) — machine-readable JSON Schema for JSON/YAML configs +- [AWF config schema](docs/awf-config.schema.json) — machine-readable JSON Schema for JSON/YAML configs (also published as a [versioned release asset](https://github.com/github/gh-aw-firewall/releases/latest/download/awf-config.schema.json) for IDE autocomplete) - [AWF config spec](docs/awf-config-spec.md) — normative processing and precedence rules for tooling/compiler integration - [Enterprise configuration](docs/enterprise-configuration.md) — GitHub Enterprise Cloud and Server setup - [Chroot mode](docs/chroot-mode.md) — use host binaries with network isolation diff --git a/docs/awf-config-spec.md b/docs/awf-config-spec.md index 4b4b86f6..3c46cb34 100644 --- a/docs/awf-config-spec.md +++ b/docs/awf-config-spec.md @@ -10,7 +10,9 @@ This document defines the canonical configuration model for AWF (`awf`) and is i The machine-readable schema is published at: -- `docs/awf-config.schema.json` +- `docs/awf-config.schema.json` — live schema (always reflects latest `main`) +- GitHub release asset `awf-config.schema.json` — versioned, stable URL per release + (e.g. `https://github.com/github/gh-aw-firewall/releases/download/v0.23.1/awf-config.schema.json`) ## 1. Conformance diff --git a/docs/awf-config.schema.json b/docs/awf-config.schema.json index 557f02c2..d188bfb9 100644 --- a/docs/awf-config.schema.json +++ b/docs/awf-config.schema.json @@ -2,150 +2,331 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/github/gh-aw-firewall/main/docs/awf-config.schema.json", "title": "AWF Configuration", - "description": "JSON/YAML configuration for awf CLI. CLI flags override config file values.", + "description": "JSON/YAML configuration for awf CLI. CLI flags override config file values. See https://github.com/github/gh-aw-firewall for documentation.", "type": "object", "additionalProperties": false, "properties": { "$schema": { - "type": "string" + "type": "string", + "description": "JSON Schema URL for IDE validation and autocomplete." }, "network": { "type": "object", + "description": "Network egress configuration.", "additionalProperties": false, "properties": { "allowDomains": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + }, + "description": "Domains that the agent is allowed to reach. Both the bare domain and all subdomains are permitted (e.g. \"github.com\" also allows \"api.github.com\")." }, "blockDomains": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + }, + "description": "Domains that are explicitly blocked, overriding allowDomains." }, "dnsServers": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + }, + "description": "DNS servers to use inside the container. Defaults to Google DNS (8.8.8.8, 8.8.4.4). Accepts IPv4 and IPv6 addresses." }, "upstreamProxy": { - "type": "string" + "type": "string", + "description": "Upstream HTTP proxy URL (e.g. \"http://proxy.corp.example.com:8080\"). When set, the AWF Squid proxy forwards traffic through this proxy." } } }, "apiProxy": { "type": "object", + "description": "API proxy sidecar configuration. The sidecar injects real API credentials so the agent never has direct access to them.", "additionalProperties": false, "properties": { - "enabled": { "type": "boolean" }, - "enableOpenCode": { "type": "boolean" }, + "enabled": { + "type": "boolean", + "description": "Enable the API proxy sidecar container." + }, + "enableOpenCode": { + "type": "boolean", + "description": "Enable the OpenCode API proxy endpoint (port 10004)." + }, + "anthropicAutoCache": { + "type": "boolean", + "description": "Automatically apply Anthropic prompt-cache optimizations on /v1/messages requests." + }, + "anthropicCacheTailTtl": { + "type": "string", + "enum": [ + "5m", + "1h" + ], + "description": "TTL for Anthropic cache tail optimization. Requires anthropicAutoCache to be enabled. Allowed values: \"5m\" or \"1h\"." + }, "targets": { "type": "object", + "description": "Override upstream API endpoints for each provider.", "additionalProperties": false, "properties": { - "openai": { "$ref": "#/$defs/providerTarget" }, - "anthropic": { "$ref": "#/$defs/providerTarget" }, - "copilot": { "$ref": "#/$defs/providerHostOnlyTarget" }, - "gemini": { "$ref": "#/$defs/providerTarget" } + "openai": { + "$ref": "#/$defs/providerTarget", + "description": "OpenAI API target override." + }, + "anthropic": { + "$ref": "#/$defs/providerTarget", + "description": "Anthropic API target override." + }, + "copilot": { + "$ref": "#/$defs/providerHostOnlyTarget", + "description": "GitHub Copilot API target override (basePath not supported)." + }, + "gemini": { + "$ref": "#/$defs/providerTarget", + "description": "Google Gemini API target override." + } + } + }, + "models": { + "type": "object", + "description": "Model alias mapping. Keys are canonical model names; values are arrays of alternative names or patterns that should be rewritten to the canonical name.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } } } } }, "security": { "type": "object", + "description": "Security and isolation configuration.", "additionalProperties": false, "properties": { - "sslBump": { "type": "boolean" }, - "enableDlp": { "type": "boolean" }, - "enableHostAccess": { "type": "boolean" }, + "sslBump": { + "type": "boolean", + "description": "Enable SSL bumping (TLS interception) in the Squid proxy. Requires a custom CA certificate." + }, + "enableDlp": { + "type": "boolean", + "description": "Enable Data Loss Prevention (DLP) inspection of outbound traffic." + }, + "enableHostAccess": { + "type": "boolean", + "description": "Mount the host filesystem (read-only for system paths, read-write for the workspace). Enabled by default; set to false to run without host filesystem access." + }, "allowHostPorts": { "oneOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "Host TCP ports the agent may connect to (e.g. local dev services). Accepts a single port string or an array of port strings." }, "allowHostServicePorts": { "oneOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "Named service ports on the host that the agent may connect to. Accepts a single port string or an array of port strings." }, "difcProxy": { "type": "object", + "description": "DIFC (Data-in-Flight Control) proxy configuration.", "additionalProperties": false, "properties": { - "host": { "type": "string" }, - "caCert": { "type": "string" } + "host": { + "type": "string", + "description": "DIFC proxy host." + }, + "caCert": { + "type": "string", + "description": "Path to the CA certificate for DIFC proxy TLS verification." + } } } } }, "container": { "type": "object", + "description": "Container and Docker configuration.", "additionalProperties": false, "properties": { - "memoryLimit": { "type": "string" }, - "agentTimeout": { "type": "integer", "minimum": 1 }, - "enableDind": { "type": "boolean" }, - "workDir": { "type": "string" }, - "containerWorkDir": { "type": "string" }, - "imageRegistry": { "type": "string" }, - "imageTag": { "type": "string" }, - "skipPull": { "type": "boolean" }, - "buildLocal": { "type": "boolean" }, - "agentImage": { "type": "string" }, - "tty": { "type": "boolean" }, - "dockerHost": { "type": "string" } + "memoryLimit": { + "type": "string", + "description": "Docker memory limit for the agent container (e.g. \"4g\", \"512m\"). Uses Docker memory limit syntax." + }, + "agentTimeout": { + "type": "integer", + "minimum": 1, + "description": "Maximum time (in minutes) the agent command is allowed to run." + }, + "enableDind": { + "type": "boolean", + "description": "Enable Docker-in-Docker support inside the agent container." + }, + "workDir": { + "type": "string", + "description": "Host path used as the AWF working directory for generated configs and logs. Defaults to a temporary directory." + }, + "containerWorkDir": { + "type": "string", + "description": "Working directory inside the agent container." + }, + "imageRegistry": { + "type": "string", + "description": "Container image registry to pull from. Defaults to \"ghcr.io/github/gh-aw-firewall\"." + }, + "imageTag": { + "type": "string", + "description": "Container image tag to use. Defaults to \"latest\"." + }, + "skipPull": { + "type": "boolean", + "description": "Skip pulling container images (use locally cached images)." + }, + "buildLocal": { + "type": "boolean", + "description": "Build container images from source instead of pulling from the registry." + }, + "agentImage": { + "type": "string", + "description": "Override the agent container image (e.g. for a GitHub Actions parity image)." + }, + "tty": { + "type": "boolean", + "description": "Allocate a pseudo-TTY for the agent container." + }, + "dockerHost": { + "type": "string", + "description": "Docker daemon socket or host to connect to (e.g. \"unix:///var/run/docker.sock\")." + } } }, "environment": { "type": "object", + "description": "Environment variable propagation into the agent container.", "additionalProperties": false, "properties": { - "envFile": { "type": "string" }, - "envAll": { "type": "boolean" }, + "envFile": { + "type": "string", + "description": "Path to a .env file whose variables are injected into the agent container." + }, + "envAll": { + "type": "boolean", + "description": "Forward all host environment variables into the agent container. Use with caution — may expose secrets." + }, "excludeEnv": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + }, + "description": "Environment variable names to exclude when envAll is true." } } }, "logging": { "type": "object", + "description": "Logging and diagnostics configuration.", "additionalProperties": false, "properties": { "logLevel": { "type": "string", - "enum": ["debug", "info", "warn", "error"] + "enum": [ + "debug", + "info", + "warn", + "error" + ], + "description": "Log verbosity level. Defaults to \"info\"." + }, + "diagnosticLogs": { + "type": "boolean", + "description": "Enable diagnostic logging (Squid access logs, iptables logs). Logs are written to the work directory." + }, + "auditDir": { + "type": "string", + "description": "Directory path for audit logs." + }, + "proxyLogsDir": { + "type": "string", + "description": "Directory path for Squid proxy access logs." }, - "diagnosticLogs": { "type": "boolean" }, - "auditDir": { "type": "string" }, - "proxyLogsDir": { "type": "string" }, - "sessionStateDir": { "type": "string" } + "sessionStateDir": { + "type": "string", + "description": "Directory path for agent session state (e.g. conversation history). Set to \"/tmp/gh-aw/sandbox/agent/session-state\" for Copilot agent runs." + } } }, "rateLimiting": { "type": "object", + "description": "Egress rate limiting configuration.", "additionalProperties": false, "properties": { - "enabled": { "type": "boolean" }, - "requestsPerMinute": { "type": "integer", "minimum": 1 }, - "requestsPerHour": { "type": "integer", "minimum": 1 }, - "bytesPerMinute": { "type": "integer", "minimum": 1 } + "enabled": { + "type": "boolean", + "description": "Enable egress rate limiting." + }, + "requestsPerMinute": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of HTTP requests per minute." + }, + "requestsPerHour": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of HTTP requests per hour." + }, + "bytesPerMinute": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of bytes transferred per minute." + } } } }, "$defs": { "providerTarget": { "type": "object", + "description": "API provider target override.", "additionalProperties": false, "properties": { - "host": { "type": "string" }, - "basePath": { "type": "string" } + "host": { + "type": "string", + "description": "Override the provider API host." + }, + "basePath": { + "type": "string", + "description": "Override the provider API base path." + } } }, "providerHostOnlyTarget": { "type": "object", + "description": "API provider target override (host only; basePath not supported).", "additionalProperties": false, "properties": { - "host": { "type": "string" } + "host": { + "type": "string", + "description": "Override the provider API host." + } } } } diff --git a/package.json b/package.json index 6efee607..ec59dd81 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "docs:preview": "cd docs-site && npm run preview", "benchmark": "npx tsx scripts/ci/benchmark-performance.ts", "benchmark:trend": "npx tsx scripts/ci/benchmark-trend.ts", - "build:bundle": "npm run build && node scripts/build-bundle.mjs" + "build:bundle": "npm run build && node scripts/build-bundle.mjs", + "generate:schema": "node scripts/generate-schema.mjs" }, "keywords": [ "agentic-workflows", diff --git a/scripts/generate-schema.mjs b/scripts/generate-schema.mjs new file mode 100644 index 00000000..51673043 --- /dev/null +++ b/scripts/generate-schema.mjs @@ -0,0 +1,386 @@ +#!/usr/bin/env node + +/** + * Generates the JSON Schema for the AWF config file (docs/awf-config.schema.json). + * + * Usage: + * node scripts/generate-schema.mjs # writes docs/awf-config.schema.json + * node scripts/generate-schema.mjs --version v0.23.1 # embeds a versioned $id + * node scripts/generate-schema.mjs --print # prints to stdout + * + * The schema mirrors the AwfFileConfig TypeScript interface in src/config-file.ts. + * When the interface changes, update this script to match. + */ + +import { writeFileSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const projectRoot = join(__dirname, '..'); + +// --- Parse CLI args --- +const args = process.argv.slice(2); +const versionIdx = args.indexOf('--version'); +const version = versionIdx !== -1 ? args[versionIdx + 1] : null; +const printOnly = args.includes('--print'); + +// --- Build the schema --- +const schemaId = version + ? `https://github.com/github/gh-aw-firewall/releases/download/${version}/awf-config.schema.json` + : 'https://raw.githubusercontent.com/github/gh-aw-firewall/main/docs/awf-config.schema.json'; + +/** @type {object} */ +const schema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: schemaId, + title: 'AWF Configuration', + description: + 'JSON/YAML configuration for awf CLI. CLI flags override config file values. ' + + 'See https://github.com/github/gh-aw-firewall for documentation.', + type: 'object', + additionalProperties: false, + properties: { + $schema: { + type: 'string', + description: 'JSON Schema URL for IDE validation and autocomplete.', + }, + network: { + type: 'object', + description: 'Network egress configuration.', + additionalProperties: false, + properties: { + allowDomains: { + type: 'array', + items: { type: 'string' }, + description: + 'Domains that the agent is allowed to reach. ' + + 'Both the bare domain and all subdomains are permitted (e.g. "github.com" also allows "api.github.com").', + }, + blockDomains: { + type: 'array', + items: { type: 'string' }, + description: 'Domains that are explicitly blocked, overriding allowDomains.', + }, + dnsServers: { + type: 'array', + items: { type: 'string' }, + description: + 'DNS servers to use inside the container. Defaults to Google DNS (8.8.8.8, 8.8.4.4). ' + + 'Accepts IPv4 and IPv6 addresses.', + }, + upstreamProxy: { + type: 'string', + description: + 'Upstream HTTP proxy URL (e.g. "http://proxy.corp.example.com:8080"). ' + + 'When set, the AWF Squid proxy forwards traffic through this proxy.', + }, + }, + }, + apiProxy: { + type: 'object', + description: + 'API proxy sidecar configuration. The sidecar injects real API credentials ' + + 'so the agent never has direct access to them.', + additionalProperties: false, + properties: { + enabled: { + type: 'boolean', + description: 'Enable the API proxy sidecar container.', + }, + enableOpenCode: { + type: 'boolean', + description: 'Enable the OpenCode API proxy endpoint (port 10004).', + }, + anthropicAutoCache: { + type: 'boolean', + description: + 'Automatically apply Anthropic prompt-cache optimizations on /v1/messages requests.', + }, + anthropicCacheTailTtl: { + type: 'string', + enum: ['5m', '1h'], + description: + 'TTL for Anthropic cache tail optimization. ' + + 'Requires anthropicAutoCache to be enabled. Allowed values: "5m" or "1h".', + }, + targets: { + type: 'object', + description: 'Override upstream API endpoints for each provider.', + additionalProperties: false, + properties: { + openai: { + $ref: '#/$defs/providerTarget', + description: 'OpenAI API target override.', + }, + anthropic: { + $ref: '#/$defs/providerTarget', + description: 'Anthropic API target override.', + }, + copilot: { + $ref: '#/$defs/providerHostOnlyTarget', + description: 'GitHub Copilot API target override (basePath not supported).', + }, + gemini: { + $ref: '#/$defs/providerTarget', + description: 'Google Gemini API target override.', + }, + }, + }, + models: { + type: 'object', + description: + 'Model alias mapping. Keys are canonical model names; values are arrays of ' + + 'alternative names or patterns that should be rewritten to the canonical name.', + additionalProperties: { + type: 'array', + items: { type: 'string' }, + }, + }, + }, + }, + security: { + type: 'object', + description: 'Security and isolation configuration.', + additionalProperties: false, + properties: { + sslBump: { + type: 'boolean', + description: + 'Enable SSL bumping (TLS interception) in the Squid proxy. ' + + 'Requires a custom CA certificate.', + }, + enableDlp: { + type: 'boolean', + description: 'Enable Data Loss Prevention (DLP) inspection of outbound traffic.', + }, + enableHostAccess: { + type: 'boolean', + description: + 'Mount the host filesystem (read-only for system paths, read-write for the workspace). ' + + 'Enabled by default; set to false to run without host filesystem access.', + }, + allowHostPorts: { + oneOf: [ + { type: 'string' }, + { type: 'array', items: { type: 'string' } }, + ], + description: + 'Host TCP ports the agent may connect to (e.g. local dev services). ' + + 'Accepts a single port string or an array of port strings.', + }, + allowHostServicePorts: { + oneOf: [ + { type: 'string' }, + { type: 'array', items: { type: 'string' } }, + ], + description: + 'Named service ports on the host that the agent may connect to. ' + + 'Accepts a single port string or an array of port strings.', + }, + difcProxy: { + type: 'object', + description: 'DIFC (Data-in-Flight Control) proxy configuration.', + additionalProperties: false, + properties: { + host: { + type: 'string', + description: 'DIFC proxy host.', + }, + caCert: { + type: 'string', + description: 'Path to the CA certificate for DIFC proxy TLS verification.', + }, + }, + }, + }, + }, + container: { + type: 'object', + description: 'Container and Docker configuration.', + additionalProperties: false, + properties: { + memoryLimit: { + type: 'string', + description: + 'Docker memory limit for the agent container (e.g. "4g", "512m"). ' + + 'Uses Docker memory limit syntax.', + }, + agentTimeout: { + type: 'integer', + minimum: 1, + description: 'Maximum time (in minutes) the agent command is allowed to run.', + }, + enableDind: { + type: 'boolean', + description: 'Enable Docker-in-Docker support inside the agent container.', + }, + workDir: { + type: 'string', + description: + 'Host path used as the AWF working directory for generated configs and logs. ' + + 'Defaults to a temporary directory.', + }, + containerWorkDir: { + type: 'string', + description: 'Working directory inside the agent container.', + }, + imageRegistry: { + type: 'string', + description: + 'Container image registry to pull from. ' + + 'Defaults to "ghcr.io/github/gh-aw-firewall".', + }, + imageTag: { + type: 'string', + description: 'Container image tag to use. Defaults to "latest".', + }, + skipPull: { + type: 'boolean', + description: 'Skip pulling container images (use locally cached images).', + }, + buildLocal: { + type: 'boolean', + description: 'Build container images from source instead of pulling from the registry.', + }, + agentImage: { + type: 'string', + description: + 'Override the agent container image (e.g. for a GitHub Actions parity image).', + }, + tty: { + type: 'boolean', + description: 'Allocate a pseudo-TTY for the agent container.', + }, + dockerHost: { + type: 'string', + description: + 'Docker daemon socket or host to connect to (e.g. "unix:///var/run/docker.sock").', + }, + }, + }, + environment: { + type: 'object', + description: 'Environment variable propagation into the agent container.', + additionalProperties: false, + properties: { + envFile: { + type: 'string', + description: + 'Path to a .env file whose variables are injected into the agent container.', + }, + envAll: { + type: 'boolean', + description: + 'Forward all host environment variables into the agent container. ' + + 'Use with caution — may expose secrets.', + }, + excludeEnv: { + type: 'array', + items: { type: 'string' }, + description: + 'Environment variable names to exclude when envAll is true.', + }, + }, + }, + logging: { + type: 'object', + description: 'Logging and diagnostics configuration.', + additionalProperties: false, + properties: { + logLevel: { + type: 'string', + enum: ['debug', 'info', 'warn', 'error'], + description: 'Log verbosity level. Defaults to "info".', + }, + diagnosticLogs: { + type: 'boolean', + description: + 'Enable diagnostic logging (Squid access logs, iptables logs). ' + + 'Logs are written to the work directory.', + }, + auditDir: { + type: 'string', + description: 'Directory path for audit logs.', + }, + proxyLogsDir: { + type: 'string', + description: 'Directory path for Squid proxy access logs.', + }, + sessionStateDir: { + type: 'string', + description: + 'Directory path for agent session state (e.g. conversation history). ' + + 'Set to "/tmp/gh-aw/sandbox/agent/session-state" for Copilot agent runs.', + }, + }, + }, + rateLimiting: { + type: 'object', + description: 'Egress rate limiting configuration.', + additionalProperties: false, + properties: { + enabled: { + type: 'boolean', + description: 'Enable egress rate limiting.', + }, + requestsPerMinute: { + type: 'integer', + minimum: 1, + description: 'Maximum number of HTTP requests per minute.', + }, + requestsPerHour: { + type: 'integer', + minimum: 1, + description: 'Maximum number of HTTP requests per hour.', + }, + bytesPerMinute: { + type: 'integer', + minimum: 1, + description: 'Maximum number of bytes transferred per minute.', + }, + }, + }, + }, + $defs: { + providerTarget: { + type: 'object', + description: 'API provider target override.', + additionalProperties: false, + properties: { + host: { + type: 'string', + description: 'Override the provider API host.', + }, + basePath: { + type: 'string', + description: 'Override the provider API base path.', + }, + }, + }, + providerHostOnlyTarget: { + type: 'object', + description: 'API provider target override (host only; basePath not supported).', + additionalProperties: false, + properties: { + host: { + type: 'string', + description: 'Override the provider API host.', + }, + }, + }, + }, +}; + +const output = JSON.stringify(schema, null, 2) + '\n'; + +if (printOnly) { + process.stdout.write(output); +} else { + const docsDir = join(projectRoot, 'docs'); + mkdirSync(docsDir, { recursive: true }); + const outPath = join(docsDir, 'awf-config.schema.json'); + writeFileSync(outPath, output); + console.log(`Schema written to ${outPath}`); +} diff --git a/src/schema.test.ts b/src/schema.test.ts new file mode 100644 index 00000000..2511824c --- /dev/null +++ b/src/schema.test.ts @@ -0,0 +1,176 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import Ajv2020 from 'ajv/dist/2020'; + +const schemaPath = path.join(__dirname, '..', 'docs', 'awf-config.schema.json'); + +describe('awf-config.schema.json', () => { + let schema: Record; + let validate: ReturnType; + + beforeAll(() => { + const raw = fs.readFileSync(schemaPath, 'utf8'); + schema = JSON.parse(raw) as Record; + const ajv = new Ajv2020(); + validate = ajv.compile(schema); + }); + + it('is valid JSON and compiles without errors', () => { + expect(schema).toBeDefined(); + expect(validate).toBeDefined(); + }); + + it('has expected top-level metadata', () => { + expect(schema.$schema).toBe('https://json-schema.org/draft/2020-12/schema'); + expect(schema.type).toBe('object'); + expect(schema.additionalProperties).toBe(false); + }); + + it('covers all AwfFileConfig top-level fields', () => { + const properties = schema.properties as Record; + expect(Object.keys(properties)).toEqual( + expect.arrayContaining([ + '$schema', + 'network', + 'apiProxy', + 'security', + 'container', + 'environment', + 'logging', + 'rateLimiting', + ]) + ); + }); + + it('accepts an empty config', () => { + expect(validate({})).toBe(true); + expect(validate.errors).toBeNull(); + }); + + it('accepts a full valid config', () => { + const valid = { + $schema: 'https://github.com/github/gh-aw-firewall/releases/latest/download/awf-config.schema.json', + network: { + allowDomains: ['github.com', 'api.openai.com'], + blockDomains: ['malicious.example.com'], + dnsServers: ['8.8.8.8', '8.8.4.4'], + upstreamProxy: 'http://proxy.corp.example.com:8080', + }, + apiProxy: { + enabled: true, + enableOpenCode: false, + anthropicAutoCache: true, + anthropicCacheTailTtl: '5m', + targets: { + openai: { host: 'api.openai.com', basePath: '/v1' }, + anthropic: { host: 'api.anthropic.com', basePath: '/v1' }, + copilot: { host: 'api.githubcopilot.com' }, + gemini: { host: 'generativelanguage.googleapis.com', basePath: '/v1beta' }, + }, + models: { + 'gpt-4o': ['gpt-4o-2024-11-20', 'gpt-4o-latest'], + }, + }, + security: { + sslBump: false, + enableDlp: false, + enableHostAccess: true, + allowHostPorts: ['5432', '6379'], + allowHostServicePorts: 'postgresql', + difcProxy: { host: 'proxy.example.com', caCert: '/path/to/ca.crt' }, + }, + container: { + memoryLimit: '4g', + agentTimeout: 30, + enableDind: false, + workDir: '/tmp/awf-work', + containerWorkDir: '/workspace', + imageRegistry: 'ghcr.io/github/gh-aw-firewall', + imageTag: 'latest', + skipPull: false, + buildLocal: false, + agentImage: 'ghcr.io/actions/actions-runner:latest', + tty: false, + dockerHost: 'unix:///var/run/docker.sock', + }, + environment: { + envFile: '.env', + envAll: false, + excludeEnv: ['AWS_SECRET_ACCESS_KEY'], + }, + logging: { + logLevel: 'info', + diagnosticLogs: true, + auditDir: '/tmp/awf-audit', + proxyLogsDir: '/tmp/awf-proxy-logs', + sessionStateDir: '/tmp/gh-aw/sandbox/agent/session-state', + }, + rateLimiting: { + enabled: true, + requestsPerMinute: 60, + requestsPerHour: 1000, + bytesPerMinute: 10485760, + }, + }; + expect(validate(valid)).toBe(true); + expect(validate.errors).toBeNull(); + }); + + it('rejects unknown top-level fields', () => { + expect(validate({ unknown: true })).toBe(false); + expect(validate.errors).not.toBeNull(); + }); + + it('rejects unknown network fields', () => { + expect(validate({ network: { unknownField: true } })).toBe(false); + }); + + it('rejects non-string $schema', () => { + expect(validate({ $schema: 123 })).toBe(false); + }); + + it('rejects non-array network.allowDomains', () => { + expect(validate({ network: { allowDomains: 'github.com' } })).toBe(false); + }); + + it('rejects invalid anthropicCacheTailTtl values', () => { + expect(validate({ apiProxy: { anthropicCacheTailTtl: '10m' } })).toBe(false); + expect(validate({ apiProxy: { anthropicCacheTailTtl: '5m' } })).toBe(true); + expect(validate({ apiProxy: { anthropicCacheTailTtl: '1h' } })).toBe(true); + }); + + it('rejects invalid logging.logLevel values', () => { + expect(validate({ logging: { logLevel: 'verbose' } })).toBe(false); + expect(validate({ logging: { logLevel: 'debug' } })).toBe(true); + expect(validate({ logging: { logLevel: 'info' } })).toBe(true); + expect(validate({ logging: { logLevel: 'warn' } })).toBe(true); + expect(validate({ logging: { logLevel: 'error' } })).toBe(true); + }); + + it('rejects non-positive-integer agentTimeout', () => { + expect(validate({ container: { agentTimeout: 0 } })).toBe(false); + expect(validate({ container: { agentTimeout: -1 } })).toBe(false); + expect(validate({ container: { agentTimeout: 1 } })).toBe(true); + }); + + it('rejects non-positive-integer rateLimiting values', () => { + expect(validate({ rateLimiting: { requestsPerMinute: 0 } })).toBe(false); + expect(validate({ rateLimiting: { requestsPerMinute: 1 } })).toBe(true); + expect(validate({ rateLimiting: { bytesPerMinute: -5 } })).toBe(false); + }); + + it('rejects copilot basePath (not supported)', () => { + expect(validate({ apiProxy: { targets: { copilot: { host: 'api.githubcopilot.com', basePath: '/v1' } } } })).toBe(false); + }); + + it('accepts allowHostPorts as string or array of strings', () => { + expect(validate({ security: { allowHostPorts: '5432' } })).toBe(true); + expect(validate({ security: { allowHostPorts: ['5432', '6379'] } })).toBe(true); + expect(validate({ security: { allowHostPorts: 5432 } })).toBe(false); + }); + + it('accepts apiProxy.models as object with string array values', () => { + expect(validate({ apiProxy: { models: { 'gpt-4o': ['gpt-4o-2024-11-20'] } } })).toBe(true); + expect(validate({ apiProxy: { models: { 'gpt-4o': 'not-an-array' } } })).toBe(false); + }); +}); From 88ae896cda041ed5cfdf762fe9fa483e6c51fd22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 21:50:13 +0000 Subject: [PATCH 3/4] fix: use --print in release schema generation step --- .github/workflows/release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92d4cef3..e2c119e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -523,8 +523,7 @@ jobs: - name: Generate versioned JSON Schema run: | mkdir -p release - node scripts/generate-schema.mjs --version ${{ needs.bump-version.outputs.version }} - cp docs/awf-config.schema.json release/awf-config.schema.json + node scripts/generate-schema.mjs --version ${{ needs.bump-version.outputs.version }} --print > release/awf-config.schema.json echo "=== Schema preview (first 10 lines) ===" head -10 release/awf-config.schema.json From c42ce4057873a3449a62a3ae7df88e613cf77a14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 21:59:59 +0000 Subject: [PATCH 4/4] fix: address schema generator review feedback Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/db68ca3d-815c-49d2-8348-f5c8568b7e01 --- docs/awf-config.schema.json | 2 +- package-lock.json | 1 + package.json | 1 + scripts/generate-schema.mjs | 25 ++++++++++++++++++++++--- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/awf-config.schema.json b/docs/awf-config.schema.json index d188bfb9..b21cb502 100644 --- a/docs/awf-config.schema.json +++ b/docs/awf-config.schema.json @@ -65,7 +65,7 @@ "5m", "1h" ], - "description": "TTL for Anthropic cache tail optimization. Requires anthropicAutoCache to be enabled. Allowed values: \"5m\" or \"1h\"." + "description": "TTL for Anthropic cache tail optimization. Only applies when anthropicAutoCache is enabled. Allowed values: \"5m\" or \"1h\"." }, "targets": { "type": "object", diff --git a/package-lock.json b/package-lock.json index e3573820..35b4cab3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@types/node": "^25.6.0", "@typescript-eslint/eslint-plugin": "^8.58.2", "@typescript-eslint/parser": "^8.58.2", + "ajv": "^8.18.0", "babel-jest": "^30.2.0", "esbuild": "^0.25.0", "eslint": "^10.2.1", diff --git a/package.json b/package.json index ec59dd81..1b3033d1 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@types/node": "^25.6.0", "@typescript-eslint/eslint-plugin": "^8.58.2", "@typescript-eslint/parser": "^8.58.2", + "ajv": "^8.18.0", "babel-jest": "^30.2.0", "esbuild": "^0.25.0", "eslint": "^10.2.1", diff --git a/scripts/generate-schema.mjs b/scripts/generate-schema.mjs index 51673043..e4cea762 100644 --- a/scripts/generate-schema.mjs +++ b/scripts/generate-schema.mjs @@ -8,8 +8,9 @@ * node scripts/generate-schema.mjs --version v0.23.1 # embeds a versioned $id * node scripts/generate-schema.mjs --print # prints to stdout * - * The schema mirrors the AwfFileConfig TypeScript interface in src/config-file.ts. - * When the interface changes, update this script to match. + * The schema reflects the validated config surface defined in src/config-file.ts + * (validateAwfFileConfig), not just the AwfFileConfig TypeScript interface. + * When validation rules change (e.g. new fields, enum constraints), update this script to match. */ import { writeFileSync, mkdirSync } from 'fs'; @@ -22,7 +23,25 @@ const projectRoot = join(__dirname, '..'); // --- Parse CLI args --- const args = process.argv.slice(2); + +const knownFlags = new Set(['--version', '--print']); +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (!knownFlags.has(arg)) { + // Skip the value that follows --version + if (args[i - 1] === '--version') continue; + console.error(`Error: unknown argument '${arg}'`); + console.error('Usage: generate-schema.mjs [--version ] [--print]'); + process.exit(1); + } +} + const versionIdx = args.indexOf('--version'); +if (versionIdx !== -1 && (versionIdx + 1 >= args.length || args[versionIdx + 1].startsWith('--'))) { + console.error('Error: --version requires a value (e.g. --version v0.23.1)'); + console.error('Usage: generate-schema.mjs [--version ] [--print]'); + process.exit(1); +} const version = versionIdx !== -1 ? args[versionIdx + 1] : null; const printOnly = args.includes('--print'); @@ -103,7 +122,7 @@ const schema = { enum: ['5m', '1h'], description: 'TTL for Anthropic cache tail optimization. ' + - 'Requires anthropicAutoCache to be enabled. Allowed values: "5m" or "1h".', + 'Only applies when anthropicAutoCache is enabled. Allowed values: "5m" or "1h".', }, targets: { type: 'object',