diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 211b4080a65..d28a4325bde 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,6 +13,17 @@ }, "codespaces": { "repositories": { + "gh-aw": { + "permissions": { + "actions": "write", + "checks": "write", + "contents": "write", + "discussions": "read", + "issues": "read", + "pull-requests": "write", + "workflows": "write" + } + }, "github/gh-aw": { "permissions": { "actions": "write", diff --git a/.github/aw/configure-agentic-engine.md b/.github/aw/configure-agentic-engine.md new file mode 100644 index 00000000000..641971bcee2 --- /dev/null +++ b/.github/aw/configure-agentic-engine.md @@ -0,0 +1,106 @@ +--- +description: Guide for configuring new declarative agentic engines — engine definition fields, auth wiring, behavior blocks, and validation. +--- + +# Configure a New Agentic Engine + +Use this guide when adding or updating a built-in engine definition under `pkg/workflow/data/engines/` or when reviewing a proposed declarative engine configuration. + +## Prefer declarative engine definitions + +For CLI-style engines, define behavior in `pkg/workflow/data/engines/.md`. + +- prefer frontmatter-defined `engine.behaviors` over a bespoke Go wrapper +- keep install, config, execution, MCP, manifest, and capability metadata in the engine markdown file +- add Go changes only when the runtime cannot be expressed with the current declarative behavior schema + +Built-in engine files are embedded from `pkg/workflow/data/engines/*.md`. Adding a new built-in engine should normally start with a new markdown file there. + +## Gather the engine contract first + +Before editing files, identify: + +1. the stable engine `id` and display name +2. whether it can reuse an existing `runtime-id` or needs a new runtime adapter +3. the install source, package manager, package name, binary name, and version +4. the required secrets and whether they follow universal provider routing or engine-specific auth +5. any config file path, default content, and merge behavior +6. the execution command, fixed args, model wiring, and MCP config wiring +7. engine-owned manifest files or directories that must be protected from untrusted PR edits + +## Engine definition shape + +```aw wrap +engine: + id: auggie + display-name: Auggie + experimental: true + auth: + - role: session + secret: AUGMENT_SESSION_AUTH + behaviors: + supported-env-var-keys: + - AUGMENT_SESSION_AUTH + installation: + package-manager: npm + package-name: "@augmentcode/auggie" + version: "1.0.0" + step-name: Install Auggie + binary-name: auggie + include-node-setup: true + config-file: + path: .auggie.json + step-name: Write Auggie Config + content: '{"sandbox":"workspace-write"}' + merge-strategy: json-merge + execution: + command-name: auggie + args: [run] + step-name: Execute Auggie CLI + model-env-var: AUGGIE_MODEL + mcp-config-env-var: AUGGIE_MCP_CONFIG + write-timestamp: true +``` + +## Field guide + +- `engine.id` is the public identifier used by workflow authors in `engine: `. +- `display-name` and `description` should be human-readable because they surface in validation and docs. +- `runtime-id` is only needed when the definition reuses a different registered runtime adapter. +- `experimental: true` should be set for engines that are not yet considered stable. +- `provider` and `models` describe provider defaults and supported model metadata. +- `auth` declares engine-specific secret bindings forwarded into the runtime environment. +- `behaviors.capabilities` advertises runtime support such as `max-turns`, `tools-allowlist`, or `native-agent-file`. +- `behaviors.manifest` lists engine-owned files and path prefixes that affect runtime behavior. +- `behaviors.installation` defines CLI installation and optional verification steps. +- `behaviors.config-file` writes engine config before execution; use `json-merge` when the file must merge with rendered MCP content. +- `behaviors.execution` defines the command, fixed args, model binding, MCP binding, and timestamp behavior. +- `behaviors.mcp.config-path` points to the file where rendered MCP configuration should be written. + +## Auth and provider rules + +- prefer `secret-strategy: universal-llm-consumer` when the engine can reuse shared provider/model routing +- pair that with `execution.provider-env-mode: universal-llm-consumer` when the CLI expects provider env vars +- use `engine.auth` only for engine-specific secrets that must be injected directly into the CLI runtime +- keep `supported-env-var-keys` aligned with the env var names the CLI actually accepts +- do not hard-code credential values in markdown, Go, or tests + +## Validation loop + +1. add or update `pkg/workflow/data/engines/.md` +2. if the schema surface changes, update `pkg/parser/schemas/main_workflow_schema.json` +3. if the engine is user-facing, update `docs/src/content/docs/reference/engines.md` and any generated schema reference as needed +4. if a new built-in engine ID is added, update tests that assert the catalog contents +5. run: + +```bash +make build +go test ./pkg/workflow/... ./pkg/parser/... +``` + +## Anti-patterns + +- do not add a new bespoke `*_engine.go` wrapper for behavior that already fits `engine.behaviors` +- do not store install metadata, CLI args, or config-file templates partly in Go and partly in markdown without a clear need +- do not omit manifest files for engine-owned config that changes runtime behavior +- do not use a mismatched `runtime-id` unless an existing runtime adapter is intentionally being reused diff --git a/.github/aw/github-agentic-workflows.md b/.github/aw/github-agentic-workflows.md index 11b22d257dd..55f1ca78be8 100644 --- a/.github/aw/github-agentic-workflows.md +++ b/.github/aw/github-agentic-workflows.md @@ -125,6 +125,7 @@ Permissions: `pull-requests: read` only; all writes route through `add-comment` | Safe outputs index | [safe-outputs.md](safe-outputs.md) | | Trigger patterns | [triggers.md](triggers.md) | | Context expressions and `{{#if}}` templates | [context.md](context.md) | +| Declarative engine configuration | [configure-agentic-engine.md](configure-agentic-engine.md) | | CLI commands and MCP equivalents | [cli-commands.md](cli-commands.md) | | Network configuration | [network.md](network.md) | | Memory and persistence | [memory.md](memory.md) | diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md index 72145f10bda..499e6113b76 100644 --- a/.github/skills/agentic-workflows/SKILL.md +++ b/.github/skills/agentic-workflows/SKILL.md @@ -22,6 +22,7 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/charts-trending.md` - `.github/aw/charts.md` - `.github/aw/cli-commands.md` +- `.github/aw/configure-agentic-engine.md` - `.github/aw/context.md` - `.github/aw/create-agentic-workflow-trigger-details.md` - `.github/aw/create-agentic-workflow.md` @@ -74,6 +75,7 @@ Load these files from `github/gh-aw` (they are not available locally). After loading the matching workflow prompt or skill, follow it directly: - Design workflows from scratch via interview: `.github/aw/designer.md` - Create new workflows: `.github/aw/create-agentic-workflow.md` +- Configure or add declarative engines: `.github/aw/configure-agentic-engine.md` - Update existing workflows: `.github/aw/update-agentic-workflow.md` - Debug, audit, or investigate workflows: `.github/aw/debug-agentic-workflow.md` - Upgrade workflows and fix deprecations: `.github/aw/upgrade-agentic-workflows.md` diff --git a/.github/workflows/aw.json b/.github/workflows/aw.json index 841e1508554..eb2cac13e0a 100644 --- a/.github/workflows/aw.json +++ b/.github/workflows/aw.json @@ -1,8 +1,9 @@ { - "utc": "-08:00", "auto_upgrade": true, + "ghes": true, "maintenance": { "action_failure_issue_expires": 12, "label_triggers": true - } + }, + "utc": "-08:00" } diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index f4a7b05b460..024638b15d3 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a0d4730bb5f10b0c4234b34e1ac2429854c79c531c23f8e5c183640a44c898fd","body_hash":"ff1e979a13078c84305141cc3861a5a2a120a2c8497fa48e4fc18115ca12a1c3","strict":true,"agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.70"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3e3e227a88e7a89937ece87542b25a65c2757d3b710dfd3a3bb0420e99d75078","body_hash":"ff1e979a13078c84305141cc3861a5a2a120a2c8497fa48e4fc18115ca12a1c3","strict":true,"agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.70"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"924ae3a1cded613372ab5595356fb5720e22ba16","version":"v6.5.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.31","digest":"sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.31@sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.31","digest":"sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.31@sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.31","digest":"sha256:7c63bc4e57d6eac1be996bb793a5a2d74d40b15a616003f4b6805a457046c673","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.31@sha256:7c63bc4e57d6eac1be996bb793a5a2d74d40b15a616003f4b6805a457046c673"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.31","digest":"sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.31@sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1909,7 +1909,7 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): - timeout-minutes: 15 + timeout-minutes: 8 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -1956,7 +1956,7 @@ jobs: GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_TIMEOUT_MINUTES: 15 + GH_AW_TIMEOUT_MINUTES: 8 GH_AW_VERSION: dev GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} GITHUB_API_URL: ${{ github.api_url }} @@ -2513,7 +2513,7 @@ jobs: GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "15" + GH_AW_TIMEOUT_MINUTES: "8" GH_AW_CACHE_MEMORY_ENABLED: "true" GH_AW_CACHE_MEMORY_RESTORE_0_MATCHED_KEY: ${{ needs.agent.outputs.cache_memory_restore_0_matched_key || '' }} GH_AW_CACHE_MEMORY_RESTORE_0_CACHE_HIT: ${{ needs.agent.outputs.cache_memory_restore_0_cache_hit || 'false' }} diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 9867990470a..b10d212d046 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -158,7 +158,7 @@ safe-outputs: run-started: "📰 BREAKING: [{workflow_name}]({run_url}) is now investigating this {event_type}. Sources say the story is developing..." run-success: "📰 VERDICT: [{workflow_name}]({run_url}) has concluded. All systems operational. This is a developing story. 🎤" run-failure: "📰 DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident..." -timeout-minutes: 15 +timeout-minutes: 8 experiments: caveman: [yes, no] subagent_model: [small, large] diff --git a/.github/workflows/smoke-crush.lock.yml b/.github/workflows/smoke-crush.lock.yml index dc824efebf3..8ce8ae9515f 100644 --- a/.github/workflows/smoke-crush.lock.yml +++ b/.github/workflows/smoke-crush.lock.yml @@ -237,7 +237,7 @@ jobs: await main(); - name: Validate ANTHROPIC_API_KEY secret id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" ANTHROPIC_API_KEY 'Crush CLI' https://github.github.com/gh-aw/reference/engines/#crush + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" ANTHROPIC_API_KEY Crush https://github.com/charmbracelet/crush env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - name: Check for OAuth tokens @@ -570,7 +570,7 @@ jobs: package-manager-cache: false - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.31 --rootless - - name: Install Crush CLI + - name: Install Crush run: npm install -g @charmland/crush@0.59.0 env: NPM_CONFIG_MIN_RELEASE_AGE: '3' @@ -903,9 +903,15 @@ jobs: - name: Write Crush Config run: | umask 077 - mkdir -p "$GITHUB_WORKSPACE" + mkdir -p "$(dirname "$GITHUB_WORKSPACE/.crush.json")" CONFIG="$GITHUB_WORKSPACE/.crush.json" - BASE_CONFIG='{"agent":{"build":{"permission":{"bash":"allow","edit":"allow","read":"allow","glob":"allow","grep":"allow","write":"allow","webfetch":"allow","websearch":"allow","external_directory":"allow"}}}}' + BASE_CONFIG='{ + "permission": { + "edit": "allow", + "bash": "allow", + "external_directory": "allow" + } + }' if [ -f "$CONFIG" ]; then MERGED=$(jq -n --argjson base "$BASE_CONFIG" --argjson existing "$(cat "$CONFIG")" '$existing * $base') echo "$MERGED" > "$CONFIG" @@ -915,6 +921,7 @@ jobs: chmod 600 "$CONFIG" - name: Execute Crush CLI id: agentic_execution + timeout-minutes: 15 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -938,8 +945,8 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && cd "${GITHUB_WORKSPACE}" && crush run --verbose "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ANTHROPIC_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && cd "${GITHUB_WORKSPACE}" && crush run --verbose "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_BASE_URL: http://host.docker.internal:10000 @@ -949,7 +956,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GITHUB_WORKSPACE: ${{ github.workspace }} - NO_PROXY: localhost,127.0.0.1 + NO_PROXY: localhost,127.0.0.1,host.docker.internal,host.docker.internal:10000,host.docker.internal:10001,host.docker.internal:10002,host.docker.internal:10003,172.30.0.30,172.30.0.30:10000,172.30.0.30:10001,172.30.0.30:10002,172.30.0.30:10003 RUNNER_TEMP: ${{ runner.temp }} TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - name: Configure Git credentials @@ -1563,7 +1570,7 @@ jobs: package-manager-cache: false - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.31 - - name: Install Crush CLI + - name: Install Crush run: npm install -g @charmland/crush@0.59.0 env: NPM_CONFIG_MIN_RELEASE_AGE: '3' @@ -1574,9 +1581,15 @@ jobs: continue-on-error: true run: | umask 077 - mkdir -p "$GITHUB_WORKSPACE" + mkdir -p "$(dirname "$GITHUB_WORKSPACE/.crush.json")" CONFIG="$GITHUB_WORKSPACE/.crush.json" - BASE_CONFIG='{"agent":{"build":{"permission":{"bash":"allow","edit":"allow","read":"allow","glob":"allow","grep":"allow","write":"allow","webfetch":"allow","websearch":"allow","external_directory":"allow"}}}}' + BASE_CONFIG='{ + "permission": { + "edit": "allow", + "bash": "allow", + "external_directory": "allow" + } + }' if [ -f "$CONFIG" ]; then MERGED=$(jq -n --argjson base "$BASE_CONFIG" --argjson existing "$(cat "$CONFIG")" '$existing * $base') echo "$MERGED" > "$CONFIG" @@ -1588,6 +1601,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true id: detection_agentic_execution + timeout-minutes: 20 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -1613,8 +1627,8 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --log-level info --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && cd "${GITHUB_WORKSPACE}" && crush run --verbose "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ANTHROPIC_API_KEY --log-level info --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && cd "${GITHUB_WORKSPACE}" && crush run --verbose "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_BASE_URL: http://host.docker.internal:10000 @@ -1622,7 +1636,7 @@ jobs: GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GITHUB_WORKSPACE: ${{ github.workspace }} - NO_PROXY: localhost,127.0.0.1 + NO_PROXY: localhost,127.0.0.1,host.docker.internal,host.docker.internal:10000,host.docker.internal:10001,host.docker.internal:10002,host.docker.internal:10003,172.30.0.30,172.30.0.30:10000,172.30.0.30:10001,172.30.0.30:10002,172.30.0.30:10003 RUNNER_TEMP: ${{ runner.temp }} TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - name: Parse threat detection token usage for step summary diff --git a/.github/workflows/smoke-opencode.lock.yml b/.github/workflows/smoke-opencode.lock.yml index 08145aee6c9..1404690b048 100644 --- a/.github/workflows/smoke-opencode.lock.yml +++ b/.github/workflows/smoke-opencode.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"1f2828d89a07fdfcf41990e440495f1911d86e0318407d46df2d4a49a24f0236","body_hash":"6aed5308f601e778cb17f8633be1ffa70ea5f86dc607b36e5d66243bd9945634","strict":true,"agent_id":"opencode","agent_model":"copilot/gpt-5","engine_versions":{"opencode":"1.2.14"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"973e48f7f29ade8ce06bc5086e8a105ed6df5299d231e16baefaded2c82b9e81","body_hash":"6aed5308f601e778cb17f8633be1ffa70ea5f86dc607b36e5d66243bd9945634","strict":true,"agent_id":"opencode","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"opencode":"1.2.14"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.31","digest":"sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.31@sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.31","digest":"sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.31@sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.31","digest":"sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.31@sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}],"has_pull_request":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -156,7 +156,7 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "opencode" GH_AW_INFO_ENGINE_NAME: "OpenCode" - GH_AW_INFO_MODEL: "copilot/gpt-5" + GH_AW_INFO_MODEL: "copilot/claude-sonnet-4.5" GH_AW_INFO_VERSION: "1.2.14" GH_AW_INFO_AGENT_VERSION: "1.2.14" GH_AW_INFO_WORKFLOW_NAME: "Smoke OpenCode" @@ -237,7 +237,7 @@ jobs: await main(); - name: Validate COPILOT_GITHUB_TOKEN secret id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'OpenCode CLI' https://github.github.com/gh-aw/reference/engines/#opencode + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN OpenCode https://opencode.ai/docs env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Check for OAuth tokens @@ -286,7 +286,7 @@ jobs: id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.grafana.net,*.sentry.io,127.0.0.1,::1,api.githubcopilot.com,api.snapcraft.io,app.renovatebot.com,appveyor.com,archive.ubuntu.com,azure.archive.ubuntu.com,badgen.net,circleci.com,codacy.com,codeclimate.com,codecov.io,codeload.github.com,coveralls.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deepsource.io,docs.github.com,drone.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,img.shields.io,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,localhost,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,readthedocs.io,readthedocs.org,registry.npmjs.org,renovatebot.com,s.symcb.com,s.symcd.com,security.ubuntu.com,semaphoreci.com,shields.io,snyk.io,sonarcloud.io,sonarqube.com,travis-ci.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.grafana.net,*.sentry.io,127.0.0.1,::1,api.githubcopilot.com,api.snapcraft.io,app.renovatebot.com,appveyor.com,archive.ubuntu.com,azure.archive.ubuntu.com,badgen.net,circleci.com,codacy.com,codeclimate.com,codecov.io,codeload.github.com,coveralls.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deepsource.io,docs.github.com,drone.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,img.shields.io,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,localhost,models.dev,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,opencode.ai,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,readthedocs.io,readthedocs.org,registry.npmjs.org,renovatebot.com,s.symcb.com,s.symcd.com,security.ubuntu.com,semaphoreci.com,shields.io,snyk.io,sonarcloud.io,sonarqube.com,travis-ci.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -575,10 +575,12 @@ jobs: package-manager-cache: false - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.31 --rootless - - name: Install OpenCode CLI + - name: Install OpenCode run: npm install --ignore-scripts -g opencode-ai@1.2.14 env: NPM_CONFIG_MIN_RELEASE_AGE: '3' + - name: Verify OpenCode CLI installation + run: opencode --version - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -906,9 +908,37 @@ jobs: - name: Write OpenCode Config run: | umask 077 - mkdir -p "$GITHUB_WORKSPACE" + mkdir -p "$(dirname "$GITHUB_WORKSPACE/opencode.jsonc")" CONFIG="$GITHUB_WORKSPACE/opencode.jsonc" - BASE_CONFIG='{"agent":{"build":{"permissions":{"bash":"allow","edit":"allow","read":"allow","glob":"allow","grep":"allow","write":"allow","webfetch":"allow","websearch":"allow"}}}}' + BASE_CONFIG='{ + "agent": { + "build": { + "permission": { + "bash": "allow", + "edit": "allow", + "read": "allow", + "glob": "allow", + "grep": "allow", + "webfetch": "allow", + "websearch": "allow", + "external_directory": "allow" + } + } + }, + "autoupdate": false, + "disabled_providers": ["opencode", "openai"], + "provider": { + "awf-proxy": { + "api": "http://172.30.0.30:10002", + "options": { + "apiKey": "awf-copilot-proxy" + }, + "models": { + "claude-sonnet-4.5": {} + } + } + } + }' if [ -f "$CONFIG" ]; then MERGED=$(jq -n --argjson base "$BASE_CONFIG" --argjson existing "$(cat "$CONFIG")" '$existing * $base') echo "$MERGED" > "$CONFIG" @@ -918,12 +948,13 @@ jobs: chmod 600 "$CONFIG" - name: Execute OpenCode CLI id: agentic_execution + timeout-minutes: 10 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.31/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"*.grafana.net\",\"*.sentry.io\",\"api.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.31,squid=sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494,agent=sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1,agent-act=sha256:58fee05c1c54ba5ca1e7056b3aaea30281841d5899093002e2c650710c50540f,api-proxy=sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63,cli-proxy=sha256:7c63bc4e57d6eac1be996bb793a5a2d74d40b15a616003f4b6805a457046c673\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.31/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"*.grafana.net\",\"*.sentry.io\",\"api.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"models.dev\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"opencode.ai\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.31,squid=sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494,agent=sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1,agent-act=sha256:58fee05c1c54ba5ca1e7056b3aaea30281841d5899093002e2c650710c50540f,api-proxy=sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63,cli-proxy=sha256:7c63bc4e57d6eac1be996bb793a5a2d74d40b15a616003f4b6805a457046c673\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -941,8 +972,8 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && cd "${GITHUB_WORKSPACE}" && opencode run --print-logs --log-level DEBUG "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && cd "${GITHUB_WORKSPACE}" && opencode run --print-logs --log-level DEBUG "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -951,11 +982,13 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GITHUB_COPILOT_BASE_URL: http://host.docker.internal:10002 GITHUB_WORKSPACE: ${{ github.workspace }} - NO_PROXY: localhost,127.0.0.1 + NO_PROXY: localhost,127.0.0.1,host.docker.internal,host.docker.internal:10000,host.docker.internal:10001,host.docker.internal:10002,host.docker.internal:10003,172.30.0.30,172.30.0.30:10000,172.30.0.30:10001,172.30.0.30:10002,172.30.0.30:10003 OPENAI_API_KEY: ${{ secrets.COPILOT_GITHUB_TOKEN }} - OPENCODE_MODEL: copilot/gpt-5 + OPENAI_BASE_URL: http://host.docker.internal:10002 + OPENCODE_MODEL: awf-proxy/claude-sonnet-4.5 RUNNER_TEMP: ${{ runner.temp }} TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} + XDG_DATA_HOME: /tmp/opencode-data - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -1002,7 +1035,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.grafana.net,*.sentry.io,127.0.0.1,::1,api.githubcopilot.com,api.snapcraft.io,app.renovatebot.com,appveyor.com,archive.ubuntu.com,azure.archive.ubuntu.com,badgen.net,circleci.com,codacy.com,codeclimate.com,codecov.io,codeload.github.com,coveralls.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deepsource.io,docs.github.com,drone.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,img.shields.io,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,localhost,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,readthedocs.io,readthedocs.org,registry.npmjs.org,renovatebot.com,s.symcb.com,s.symcd.com,security.ubuntu.com,semaphoreci.com,shields.io,snyk.io,sonarcloud.io,sonarqube.com,travis-ci.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.grafana.net,*.sentry.io,127.0.0.1,::1,api.githubcopilot.com,api.snapcraft.io,app.renovatebot.com,appveyor.com,archive.ubuntu.com,azure.archive.ubuntu.com,badgen.net,circleci.com,codacy.com,codeclimate.com,codecov.io,codeload.github.com,coveralls.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deepsource.io,docs.github.com,drone.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,img.shields.io,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,localhost,models.dev,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,opencode.ai,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,readthedocs.io,readthedocs.org,registry.npmjs.org,renovatebot.com,s.symcb.com,s.symcd.com,security.ubuntu.com,semaphoreci.com,shields.io,snyk.io,sonarcloud.io,sonarqube.com,travis-ci.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_COMMANDS: "[\"smoke-opencode\"]" @@ -1567,18 +1600,48 @@ jobs: package-manager-cache: false - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.31 - - name: Install OpenCode CLI + - name: Install OpenCode run: npm install --ignore-scripts -g opencode-ai@1.2.14 env: NPM_CONFIG_MIN_RELEASE_AGE: '3' + - name: Verify OpenCode CLI installation + run: opencode --version - name: Write OpenCode Config if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true run: | umask 077 - mkdir -p "$GITHUB_WORKSPACE" + mkdir -p "$(dirname "$GITHUB_WORKSPACE/opencode.jsonc")" CONFIG="$GITHUB_WORKSPACE/opencode.jsonc" - BASE_CONFIG='{"agent":{"build":{"permissions":{"bash":"allow","edit":"allow","read":"allow","glob":"allow","grep":"allow","write":"allow","webfetch":"allow","websearch":"allow"}}}}' + BASE_CONFIG='{ + "agent": { + "build": { + "permission": { + "bash": "allow", + "edit": "allow", + "read": "allow", + "glob": "allow", + "grep": "allow", + "webfetch": "allow", + "websearch": "allow", + "external_directory": "allow" + } + } + }, + "autoupdate": false, + "disabled_providers": ["opencode", "openai"], + "provider": { + "awf-proxy": { + "api": "http://172.30.0.30:10002", + "options": { + "apiKey": "awf-copilot-proxy" + }, + "models": { + "claude-sonnet-4.5": {} + } + } + } + }' if [ -f "$CONFIG" ]; then MERGED=$(jq -n --argjson base "$BASE_CONFIG" --argjson existing "$(cat "$CONFIG")" '$existing * $base') echo "$MERGED" > "$CONFIG" @@ -1590,12 +1653,13 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true id: detection_agentic_execution + timeout-minutes: 20 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.31/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"raw.githubusercontent.com\",\"registry.npmjs.org\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.31,squid=sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494,agent=sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1,agent-act=sha256:58fee05c1c54ba5ca1e7056b3aaea30281841d5899093002e2c650710c50540f,api-proxy=sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63,cli-proxy=sha256:7c63bc4e57d6eac1be996bb793a5a2d74d40b15a616003f4b6805a457046c673\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.31/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"models.dev\",\"opencode.ai\",\"raw.githubusercontent.com\",\"registry.npmjs.org\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.31,squid=sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494,agent=sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1,agent-act=sha256:58fee05c1c54ba5ca1e7056b3aaea30281841d5899093002e2c650710c50540f,api-proxy=sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63,cli-proxy=sha256:7c63bc4e57d6eac1be996bb793a5a2d74d40b15a616003f4b6805a457046c673\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -1615,19 +1679,21 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --log-level info --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && cd "${GITHUB_WORKSPACE}" && opencode run --print-logs --log-level DEBUG "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export no_proxy="${NO_PROXY:-}" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && cd "${GITHUB_WORKSPACE}" && opencode run --print-logs --log-level DEBUG "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GITHUB_COPILOT_BASE_URL: http://host.docker.internal:10002 GITHUB_WORKSPACE: ${{ github.workspace }} - NO_PROXY: localhost,127.0.0.1 + NO_PROXY: localhost,127.0.0.1,host.docker.internal,host.docker.internal:10000,host.docker.internal:10001,host.docker.internal:10002,host.docker.internal:10003,172.30.0.30,172.30.0.30:10000,172.30.0.30:10001,172.30.0.30:10002,172.30.0.30:10003 OPENAI_API_KEY: ${{ secrets.COPILOT_GITHUB_TOKEN }} - OPENCODE_MODEL: copilot/gpt-5 + OPENAI_BASE_URL: http://host.docker.internal:10002 + OPENCODE_MODEL: awf-proxy/claude-sonnet-4.5 RUNNER_TEMP: ${{ runner.temp }} TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} + XDG_DATA_HOME: /tmp/opencode-data - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1762,7 +1828,7 @@ jobs: GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "opencode" - GH_AW_ENGINE_MODEL: "copilot/gpt-5" + GH_AW_ENGINE_MODEL: "copilot/claude-sonnet-4.5" GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 🔥 *[{workflow_name}]({run_url}) — Powered by OpenCode*{ai_credits_suffix}{history_link}\",\"runStarted\":\"🔥 OpenCode initializing... [{workflow_name}]({run_url}) begins on this {event_type}...\",\"runSuccess\":\"🚀 [{workflow_name}]({run_url}) **MISSION COMPLETE!** OpenCode delivered. 🔥\",\"runFailure\":\"⚠️ [{workflow_name}]({run_url}) {status}. OpenCode encountered unexpected challenges...\"}" @@ -1835,7 +1901,7 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.grafana.net,*.sentry.io,127.0.0.1,::1,api.githubcopilot.com,api.snapcraft.io,app.renovatebot.com,appveyor.com,archive.ubuntu.com,azure.archive.ubuntu.com,badgen.net,circleci.com,codacy.com,codeclimate.com,codecov.io,codeload.github.com,coveralls.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deepsource.io,docs.github.com,drone.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,img.shields.io,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,localhost,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,readthedocs.io,readthedocs.org,registry.npmjs.org,renovatebot.com,s.symcb.com,s.symcd.com,security.ubuntu.com,semaphoreci.com,shields.io,snyk.io,sonarcloud.io,sonarqube.com,travis-ci.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.grafana.net,*.sentry.io,127.0.0.1,::1,api.githubcopilot.com,api.snapcraft.io,app.renovatebot.com,appveyor.com,archive.ubuntu.com,azure.archive.ubuntu.com,badgen.net,circleci.com,codacy.com,codeclimate.com,codecov.io,codeload.github.com,coveralls.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deepsource.io,docs.github.com,drone.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,img.shields.io,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,localhost,models.dev,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,opencode.ai,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,readthedocs.io,readthedocs.org,registry.npmjs.org,renovatebot.com,s.symcb.com,s.symcd.com,security.ubuntu.com,semaphoreci.com,shields.io,snyk.io,sonarcloud.io,sonarqube.com,travis-ci.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-opencode\"]},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-opencode\",\"expires\":2,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" diff --git a/.github/workflows/smoke-opencode.md b/.github/workflows/smoke-opencode.md index 04b51831ed8..571a87aabeb 100644 --- a/.github/workflows/smoke-opencode.md +++ b/.github/workflows/smoke-opencode.md @@ -20,7 +20,7 @@ permissions: name: Smoke OpenCode engine: id: opencode - model: copilot/gpt-5 + model: copilot/claude-sonnet-4.5 strict: true imports: - shared/gh.md diff --git a/actions/setup/js/route_slash_command.cjs b/actions/setup/js/route_slash_command.cjs index fd50980346e..a8577aa13d8 100644 --- a/actions/setup/js/route_slash_command.cjs +++ b/actions/setup/js/route_slash_command.cjs @@ -612,7 +612,9 @@ function isDisabledWorkflowDispatchError(error) { */ function resolveMatchingSlashRoutes(slashRouteMap, actualCommand) { /** @type {Array<{workflow?: unknown, events?: unknown, ai_reaction?: unknown, status_comment?: unknown}>} */ - const matchedRoutes = []; + const specificRoutes = []; + /** @type {Array<{workflow?: unknown, events?: unknown, ai_reaction?: unknown, status_comment?: unknown}>} */ + const catchAllRoutes = []; const seen = new Set(); for (const [configuredCommand, configuredRoutes] of Object.entries(slashRouteMap)) { @@ -620,6 +622,11 @@ function resolveMatchingSlashRoutes(slashRouteMap, actualCommand) { continue; } + // Catch-all ("*") routes are only used as a fallback when no specific + // routes match. Keeping them separate ensures that a command like + // /smoke-opencode dispatches only smoke-opencode and not skillet. + const isCatchAll = configuredCommand === "*"; + for (const route of configuredRoutes) { // Keep the de-duplication key explicit so routes that differ only by // status-comment behavior remain distinct dispatch targets. @@ -628,11 +635,15 @@ function resolveMatchingSlashRoutes(slashRouteMap, actualCommand) { continue; } seen.add(key); - matchedRoutes.push(route); + if (isCatchAll) { + catchAllRoutes.push(route); + } else { + specificRoutes.push(route); + } } } - return matchedRoutes; + return specificRoutes.length > 0 ? specificRoutes : catchAllRoutes; } async function main() { diff --git a/actions/setup/js/route_slash_command.test.cjs b/actions/setup/js/route_slash_command.test.cjs index 8c5bc04103d..a6c09ccbdbb 100644 --- a/actions/setup/js/route_slash_command.test.cjs +++ b/actions/setup/js/route_slash_command.test.cjs @@ -779,6 +779,19 @@ describe("route_slash_command", () => { expect(awContext.command_name).toBe("developer"); }); + it("does not dispatch catch-all skillet when a specific route matches", async () => { + process.env.GH_AW_SLASH_ROUTING = JSON.stringify({ + "*": [{ workflow: "skillet", events: ["issue_comment"] }], + "smoke-opencode": [{ workflow: "smoke-opencode", events: ["issue_comment"] }], + }); + globals.context.payload.comment.body = "/smoke-opencode"; + + await main(); + + expect(dispatchCalls).toHaveLength(1); + expect(dispatchCalls[0].workflow_id).toBe("smoke-opencode.lock.yml"); + }); + it("does not dispatch smoke-copilot-sdk when command is smoke-copilot", async () => { process.env.GH_AW_SLASH_ROUTING = JSON.stringify({ "smoke-copilot": [{ workflow: "smoke-copilot", events: ["issue_comment"] }], diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index a062d895991..65ec7953bc8 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -2592,11 +2592,21 @@ engine: # (optional) description: "Description of the workflow" + # Marks the engine as experimental so compiled workflows can surface a warning + # (optional) + experimental: true + # Runtime adapter identifier. Maps to the CodingAgentEngine registered in the # engine registry. Defaults to id when omitted. # (optional) runtime-id: "example-value" + # Optional engine-specific secret bindings for behavior-defined engines + # (optional) + auth: + - role: session + secret: AUGMENT_SESSION_AUTH + # Provider metadata for the engine # (optional) provider: @@ -2669,6 +2679,98 @@ engine: options: {} + # Declarative custom-engine runtime behavior used to materialize a shared CLI + # engine from frontmatter + # (optional) + behaviors: + # Secret resolution mode (for example universal provider/model routing) + # (optional) + secret-strategy: universal-llm-consumer + + # Secret or env var keys accepted through engine.env + # (optional) + supported-env-var-keys: + - AUGMENT_SESSION_AUTH + + # Capability flags exposed by the engine runtime + # (optional) + capabilities: + max-turns: true + + # Files and directories treated as engine manifests + # (optional) + manifest: + files: + - .auggie.json + path-prefixes: + - .auggie/ + + # CLI installation metadata + # (optional) + installation: + package-manager: npm + package-name: "@augmentcode/auggie" + version: "1.0.0" + step-name: Install Auggie + binary-name: auggie + include-node-setup: true + post-install-scripts: false + cooldown: true + verify-command: auggie --version + verify-step-name: Verify Auggie CLI installation + docs-url: https://docs.augmentcode.com + + # Optional config file written before execution + # (optional) + config-file: + path: .auggie.json + step-name: Write Auggie Config + content: '{"sandbox":"workspace-write"}' + merge-strategy: json-merge + + # Shared execution pattern + # (optional) + execution: + command-name: auggie + args: + - run + step-name: Execute Auggie CLI + model-env-var: AUGGIE_MODEL + # Optional: rewrite the provider prefix in the model string before setting + # the model env var. For example, if set to "openai" and the engine.model + # is "copilot/gpt-5", the env var receives "openai/gpt-5" so the CLI uses + # its own openai provider instead of an unrecognised "copilot" prefix. + # model-env-provider-prefix: openai + model-flag: --model + mcp-config-env-var: AUGGIE_MCP_CONFIG + mcp-config-flag: --mcp-config + write-timestamp: true + provider-env-mode: universal-llm-consumer + + # MCP config rendering target + # (optional) + mcp: + config-path: .auggie.json + + # Optional Node.js harness script for engines that need dynamic /reflect-based + # configuration at runtime. When set, the script is written to + # ${RUNNER_TEMP}/gh-aw/actions/_harness.cjs before execution and the + # engine is launched as: + # node [args...] + # The harness can read process.env.GH_AW_PROMPT for the prompt file path. + # When the AWF firewall is active, process.env.AWF_REFLECT_ENABLED is set to "1" + # so the harness can read /reflect data to dynamically configure the engine CLI. + # (optional) + harness-script: | + "use strict"; + // Example minimal harness — replace with your engine's logic. + const fs = require("fs"); + const { spawnSync } = require("child_process"); + const cmd = process.argv[2]; + const args = process.argv.slice(3); + const prompt = fs.readFileSync(process.env.GH_AW_PROMPT, "utf8"); + spawnSync(cmd, [...args, prompt], { stdio: "inherit" }); + # Format 5: MCP gateway configuration for shared workflows. Declares engine.mcp # settings (tool-timeout, session-timeout) that consumers inherit during import # without specifying an engine identifier. The engine is always inherited from the diff --git a/pkg/cli/data/agentic_workflows_fallback_aw_files.json b/pkg/cli/data/agentic_workflows_fallback_aw_files.json index b555b3cc59d..8685f237053 100644 --- a/pkg/cli/data/agentic_workflows_fallback_aw_files.json +++ b/pkg/cli/data/agentic_workflows_fallback_aw_files.json @@ -6,6 +6,7 @@ "charts-trending.md", "charts.md", "cli-commands.md", + "configure-agentic-engine.md", "context.md", "create-agentic-workflow-trigger-details.md", "create-agentic-workflow.md", diff --git a/pkg/cli/data/agentic_workflows_skill.md b/pkg/cli/data/agentic_workflows_skill.md index 7b5c5d9707f..574d1e794fe 100644 --- a/pkg/cli/data/agentic_workflows_skill.md +++ b/pkg/cli/data/agentic_workflows_skill.md @@ -19,6 +19,7 @@ Load these files from `github/gh-aw` (they are not available locally). After loading the matching workflow prompt or skill, follow it directly: - Design workflows from scratch via interview: `.github/aw/designer.md` - Create new workflows: `.github/aw/create-agentic-workflow.md` +- Configure or add declarative engines: `.github/aw/configure-agentic-engine.md` - Update existing workflows: `.github/aw/update-agentic-workflow.md` - Debug, audit, or investigate workflows: `.github/aw/debug-agentic-workflow.md` - Upgrade workflows and fix deprecations: `.github/aw/upgrade-agentic-workflows.md` diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 5a7f6de08b4..ede74c2ae2e 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -139,6 +139,27 @@ const ( AntigravityLLMGatewayPort = GeminiLLMGatewayPort ) +// AWFNoProxyHosts is the value for the NO_PROXY and no_proxy environment variables +// in the AWF agent execution environment. Both plain hostnames and explicit host:port +// forms are listed because some HTTP client runtimes (e.g. Bun, which backs OpenCode) +// compare the full host:port string against the list rather than stripping the port +// before matching, causing them to route through Squid even when the hostname alone +// appears in the list. +// AWFAPIProxyContainerIP is also listed so that requests from custom providers +// (e.g. OpenCode's awf-proxy provider) routed to the internal api-proxy sidecar +// bypass Squid instead of being forwarded through it. +const AWFNoProxyHosts = "localhost,127.0.0.1," + + "host.docker.internal," + + "host.docker.internal:10000," + + "host.docker.internal:10001," + + "host.docker.internal:10002," + + "host.docker.internal:10003," + + AWFAPIProxyContainerIP + "," + + AWFAPIProxyContainerIP + ":10000," + + AWFAPIProxyContainerIP + ":10001," + + AWFAPIProxyContainerIP + ":10002," + + AWFAPIProxyContainerIP + ":10003" + // DefaultGitHubLockdown is the default value for the GitHub MCP server lockdown setting. // Lockdown mode restricts the GitHub MCP server to the triggering repository only. // Defaults to false (lockdown disabled). diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 49e36f5f333..55714a89d1e 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -12563,10 +12563,33 @@ "type": "string", "description": "Human-readable description of the engine" }, + "experimental": { + "type": "boolean", + "description": "Marks the engine as experimental so compiled workflows can surface an explicit warning." + }, "runtime-id": { "type": "string", "description": "Runtime adapter identifier. Maps to the CodingAgentEngine registered in the engine registry. Defaults to id when omitted." }, + "auth": { + "type": "array", + "description": "Optional engine-specific secret bindings for behavior-defined engines.", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "Logical authentication role name." + }, + "secret": { + "type": "string", + "description": "GitHub Actions secret name exposed to the engine runtime." + } + }, + "required": ["role", "secret"], + "additionalProperties": false + } + }, "provider": { "type": "object", "description": "Provider metadata for the engine", @@ -12661,6 +12684,107 @@ "type": "object", "description": "Additional engine-specific options", "additionalProperties": true + }, + "behaviors": { + "type": "object", + "description": "Declarative custom-engine runtime behavior used to materialize a shared CLI engine from frontmatter.", + "properties": { + "secret-strategy": { + "type": "string", + "description": "Secret resolution mode for the engine (for example 'universal-llm-consumer')." + }, + "supported-env-var-keys": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Secret or env var keys the engine accepts through engine.env." + }, + "capabilities": { + "type": "object", + "properties": { + "tools-allowlist": { "type": "boolean" }, + "max-turns": { "type": "boolean" }, + "web-search": { "type": "boolean" }, + "max-continuations": { "type": "boolean" }, + "native-agent-file": { "type": "boolean" }, + "bare-mode": { "type": "boolean" } + }, + "additionalProperties": false + }, + "manifest": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { "type": "string" } + }, + "path-prefixes": { + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false + }, + "installation": { + "type": "object", + "properties": { + "package-manager": { "type": "string" }, + "package-name": { "type": "string" }, + "version": { "type": "string" }, + "step-name": { "type": "string" }, + "binary-name": { "type": "string" }, + "include-node-setup": { "type": "boolean" }, + "post-install-scripts": { "type": "boolean" }, + "cooldown": { "type": "boolean" }, + "verify-command": { "type": "string" }, + "verify-step-name": { "type": "string" }, + "docs-url": { "type": "string" } + }, + "additionalProperties": false + }, + "config-file": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "step-name": { "type": "string" }, + "content": { "type": "string" }, + "merge-strategy": { "type": "string" } + }, + "additionalProperties": false + }, + "execution": { + "type": "object", + "properties": { + "command-name": { "type": "string" }, + "args": { + "type": "array", + "items": { "type": "string" } + }, + "step-name": { "type": "string" }, + "model-env-var": { "type": "string" }, + "model-env-provider-prefix": { "type": "string" }, + "model-flag": { "type": "string" }, + "mcp-config-env-var": { "type": "string" }, + "mcp-config-flag": { "type": "string" }, + "write-timestamp": { "type": "boolean" }, + "provider-env-mode": { "type": "string" } + }, + "additionalProperties": false + }, + "mcp": { + "type": "object", + "properties": { + "config-path": { "type": "string" } + }, + "additionalProperties": false + }, + "harness-script": { + "type": "string", + "description": "JavaScript source of a Node.js harness that spawns the engine CLI. When set, the script is written to ${RUNNER_TEMP}/gh-aw/actions/_harness.cjs before execution and the engine is launched as: node [args...]. The harness reads process.env.GH_AW_PROMPT for the prompt-file path and, when the AWF firewall is active, process.env.AWF_REFLECT_ENABLED is set to '1' so the harness can read /reflect data to dynamically configure the engine CLI." + } + }, + "additionalProperties": false } }, "required": ["id", "display-name"], diff --git a/pkg/workflow/agent_validation_model_test.go b/pkg/workflow/agent_validation_model_test.go index 14c4e2de298..2f17b3b54dd 100644 --- a/pkg/workflow/agent_validation_model_test.go +++ b/pkg/workflow/agent_validation_model_test.go @@ -11,6 +11,10 @@ import ( func TestValidateUniversalLLMConsumerModel(t *testing.T) { compiler := NewCompiler() + opencodeEngine, err := newBuiltinBehaviorDefinedEngine("opencode") + require.NoError(t, err) + crushEngine, err := newBuiltinBehaviorDefinedEngine("crush") + require.NoError(t, err) t.Run("non universal engine skips validation", func(t *testing.T) { err := compiler.validateUniversalLLMConsumerModel( @@ -31,7 +35,7 @@ func TestValidateUniversalLLMConsumerModel(t *testing.T) { "id": "opencode", }, }, - NewOpenCodeEngine(), + opencodeEngine, ) require.Error(t, err, "Missing model should fail for opencode") assert.Contains(t, err.Error(), "engine.model is required for engine 'opencode'") @@ -45,7 +49,7 @@ func TestValidateUniversalLLMConsumerModel(t *testing.T) { "model": "gpt-4.1", }, }, - NewCrushEngine(), + crushEngine, ) require.Error(t, err, "Unqualified model should fail for crush") assert.Contains(t, err.Error(), "provider/model format") @@ -59,7 +63,7 @@ func TestValidateUniversalLLMConsumerModel(t *testing.T) { "model": "groq/llama-4", }, }, - NewOpenCodeEngine(), + opencodeEngine, ) require.Error(t, err, "Unsupported provider should fail") assert.Contains(t, err.Error(), "unsupported provider") @@ -73,7 +77,7 @@ func TestValidateUniversalLLMConsumerModel(t *testing.T) { "model": "anthropic/claude-sonnet-4", }, }, - NewCrushEngine(), + crushEngine, ) assert.NoError(t, err, "Supported provider/model should pass") }) diff --git a/pkg/workflow/agentic_engine.go b/pkg/workflow/agentic_engine.go index 38b8900bc31..65d5a7b422e 100644 --- a/pkg/workflow/agentic_engine.go +++ b/pkg/workflow/agentic_engine.go @@ -492,10 +492,15 @@ func NewEngineRegistry() *EngineRegistry { NewCopilotEngine(), NewGeminiEngine(), NewAntigravityEngine(), - NewOpenCodeEngine(), - NewCrushEngine(), NewPiEngine(), } + for _, id := range []string{"opencode", "crush"} { + engine, err := newBuiltinBehaviorDefinedEngine(id) + if err != nil { + panic(fmt.Sprintf("BUG: failed to load built-in behavior engine %q: %v", id, err)) + } + builtins = append(builtins, engine) + } for _, engine := range builtins { if err := registry.Register(engine); err != nil { panic(fmt.Sprintf("BUG: failed to register built-in engine: %v", err)) diff --git a/pkg/workflow/behavior_defined_engine.go b/pkg/workflow/behavior_defined_engine.go new file mode 100644 index 00000000000..e2b621b83e6 --- /dev/null +++ b/pkg/workflow/behavior_defined_engine.go @@ -0,0 +1,543 @@ +package workflow + +import ( + "encoding/json" + "errors" + "fmt" + "maps" + "slices" + "strings" + "time" + + "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/setutil" + "github.com/goccy/go-yaml" +) + +const ( + behaviorSecretStrategyUniversalLLMConsumer = "universal-llm-consumer" + behaviorProviderEnvModeUniversalLLMConsumer = "universal-llm-consumer" + behaviorConfigMergeJSON = "json-merge" +) + +var behaviorDefinedEngineLog = logger.New("workflow:behavior_defined_engine") + +// BehaviorDefinedEngine is a declarative CodingAgentEngine built from an engine +// definition's behaviors block. +type BehaviorDefinedEngine struct { + UniversalLLMConsumerEngine + definition *EngineDefinition +} + +func NewBehaviorDefinedEngine(def *EngineDefinition) (*BehaviorDefinedEngine, error) { + if def == nil { + return nil, errors.New("engine definition is required") + } + if def.Behaviors == nil { + return nil, fmt.Errorf("engine definition %q is missing behaviors", def.ID) + } + engine := &BehaviorDefinedEngine{ + UniversalLLMConsumerEngine: UniversalLLMConsumerEngine{ + BaseEngine: BaseEngine{ + id: def.ID, + displayName: def.DisplayName, + description: def.Description, + experimental: def.Experimental, + ghSkillAgentName: def.GHSkillAgentName, + capabilities: def.Behaviors.Capabilities.ToRuntimeCapabilities(), + }, + }, + definition: def, + } + return engine, nil +} + +func newBuiltinBehaviorDefinedEngine(id string) (*BehaviorDefinedEngine, error) { + def, err := getBuiltinEngineDefinition(id) + if err != nil { + return nil, err + } + return NewBehaviorDefinedEngine(def) +} + +func (e *BehaviorDefinedEngine) behavior() *EngineBehaviorDefinition { + if e == nil || e.definition == nil { + return nil + } + return e.definition.Behaviors +} + +func (e *BehaviorDefinedEngine) usesUniversalLLMConsumer() bool { + behavior := e.behavior() + return behavior != nil && behavior.SecretStrategy == behaviorSecretStrategyUniversalLLMConsumer +} + +func (e *BehaviorDefinedEngine) GetModelEnvVarName() string { + behavior := e.behavior() + if behavior == nil || behavior.Execution == nil { + return "" + } + return behavior.Execution.ModelEnvVarName +} + +func (e *BehaviorDefinedEngine) GetRequiredSecretNames(workflowData *WorkflowData) []string { + if e.usesUniversalLLMConsumer() { + return e.GetUniversalRequiredSecretNames(workflowData) + } + + seen := make(map[string]struct{}) + var secrets []string + addSecret := func(secret string) { + if secret == "" || setutil.Contains(seen, secret) { + return + } + seen[secret] = struct{}{} + secrets = append(secrets, secret) + } + for _, binding := range e.definition.Auth { + addSecret(binding.Secret) + } + for _, secret := range collectCommonMCPSecrets(workflowData) { + addSecret(secret) + } + parsedTools, tools := extractToolsConfig(workflowData) + if hasGitHubTool(parsedTools) { + addSecret("GITHUB_MCP_SERVER_TOKEN") + } + for varName := range collectHTTPMCPHeaderSecrets(tools) { + addSecret(varName) + } + return secrets +} + +func (e *BehaviorDefinedEngine) GetSupportedEnvVarKeys() []string { + behavior := e.behavior() + if behavior == nil { + return nil + } + if len(behavior.SupportedEnvVarKeys) > 0 { + return behavior.SupportedEnvVarKeys + } + keys := make([]string, 0, len(e.definition.Auth)) + for _, binding := range e.definition.Auth { + if binding.Secret != "" { + keys = append(keys, binding.Secret) + } + } + slices.Sort(keys) + return slices.Compact(keys) +} + +func (e *BehaviorDefinedEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHubActionStep { + behavior := e.behavior() + if behavior == nil || behavior.Installation == nil { + return GitHubActionStep{} + } + if e.usesUniversalLLMConsumer() { + return e.GetUniversalSecretValidationStep( + workflowData, + e.definition.DisplayName, + behavior.Installation.DocumentationURL, + ) + } + secrets := e.GetRequiredSecretNames(workflowData) + if len(secrets) == 0 { + return GitHubActionStep{} + } + return BuildDefaultSecretValidationStep(workflowData, secrets, e.definition.DisplayName, behavior.Installation.DocumentationURL) +} + +func (e *BehaviorDefinedEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubActionStep { + behavior := e.behavior() + if behavior == nil || behavior.Installation == nil { + return nil + } + if workflowData != nil && workflowData.EngineConfig != nil && workflowData.EngineConfig.Command != "" { + return nil + } + + install := behavior.Installation + if install.PackageManager != "npm" { + return nil + } + version := install.Version + if workflowData != nil && workflowData.EngineConfig != nil && workflowData.EngineConfig.Version != "" { + version = workflowData.EngineConfig.Version + } + + npmSteps := GenerateNpmInstallSteps( + install.PackageName, + version, + install.StepName, + install.BinaryName, + install.IncludeNodeSetup, + install.PostInstallScripts, + install.Cooldown, + ) + if install.VerifyCommand != "" { + npmSteps = append(npmSteps, GitHubActionStep{ + " - name: " + install.VerifyStepName, + " run: " + install.VerifyCommand, + }) + } + return BuildNpmEngineInstallStepsWithAWF(npmSteps, workflowData) +} + +func (e *BehaviorDefinedEngine) GetAgentManifestFiles() []string { + behavior := e.behavior() + if behavior == nil || behavior.Manifest == nil { + return nil + } + return behavior.Manifest.Files +} + +func (e *BehaviorDefinedEngine) GetAgentManifestPathPrefixes() []string { + behavior := e.behavior() + if behavior == nil || behavior.Manifest == nil { + return nil + } + return behavior.Manifest.PathPrefixes +} + +func (e *BehaviorDefinedEngine) RenderMCPConfig(sb *strings.Builder, tools map[string]any, mcpTools []string, workflowData *WorkflowData) error { + behavior := e.behavior() + if behavior == nil || behavior.MCP == nil || behavior.MCP.ConfigPath == "" { + return nil + } + return renderDefaultJSONMCPConfig(sb, tools, mcpTools, workflowData, behavior.MCP.ConfigPath) +} + +// harnessScriptHeredocDelimiter is the shell heredoc delimiter used when writing +// the harness script to disk. It is intentionally long and project-specific so that +// it is extremely unlikely to appear at the start of a line in any JavaScript harness. +const harnessScriptHeredocDelimiter = "GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF" + +// harnessScriptFilename returns the filename (not path) for the engine's harness script. +func (e *BehaviorDefinedEngine) harnessScriptFilename() string { + return e.GetID() + "_harness.cjs" +} + +// buildHarnessWriteStep generates a GitHub Actions step that writes the behavior-defined +// engine's harness-script content to ${RUNNER_TEMP}/gh-aw/actions/_harness.cjs +// so it can be executed as a Node.js harness during the engine execution step. +// Returns nil and logs a warning if the harness script contains the heredoc delimiter, +// which would break the generated shell command. +func (e *BehaviorDefinedEngine) buildHarnessWriteStep() GitHubActionStep { + behavior := e.behavior() + if behavior == nil || behavior.HarnessScript == "" { + return nil + } + // Safety check: if the harness script contains the heredoc delimiter at the start + // of any line, the heredoc would be terminated prematurely. Detect this at + // compile time and log a clear error rather than generating a broken step. + if strings.Contains(behavior.HarnessScript, "\n"+harnessScriptHeredocDelimiter) || + strings.HasPrefix(behavior.HarnessScript, harnessScriptHeredocDelimiter) { + behaviorDefinedEngineLog.Printf( + "WARNING: engine %q harness-script contains heredoc delimiter %q; harness write step skipped", + e.GetID(), harnessScriptHeredocDelimiter, + ) + return nil + } + filename := e.harnessScriptFilename() + command := fmt.Sprintf( + "mkdir -p %[1]s\ncat <<'%[4]s' > %[1]s/%[2]s\n%[3]s\n%[4]s\nchmod 755 %[1]s/%[2]s", + SetupActionDestinationShell, + filename, + behavior.HarnessScript, + harnessScriptHeredocDelimiter, + ) + stepLines := []string{" - name: Write " + e.GetDisplayName() + " harness script"} + stepLines = FormatStepWithCommandAndEnv(stepLines, command, nil) + return GitHubActionStep(stepLines) +} + +func (e *BehaviorDefinedEngine) GetExecutionSteps(workflowData *WorkflowData, logFile string) []GitHubActionStep { + behavior := e.behavior() + if behavior == nil || behavior.Execution == nil { + return nil + } + + var steps []GitHubActionStep + if configStep := e.buildConfigFileStep(); len(configStep) > 0 { + steps = append(steps, configStep) + } + if harnessStep := e.buildHarnessWriteStep(); len(harnessStep) > 0 { + steps = append(steps, harnessStep) + } + + exec := behavior.Execution + commandName := exec.CommandName + if workflowData != nil && workflowData.EngineConfig != nil && workflowData.EngineConfig.Command != "" { + commandName = workflowData.EngineConfig.Command + } + + var engineCommand string + if behavior.HarnessScript != "" { + // Harness execution: the harness is responsible for reading GH_AW_PROMPT and + // spawning the engine CLI. Pass the shell-escaped command name and configured args + // so the harness can forward them or use them to build the full command. + harnessArgs := []string{shellEscapeArg(commandName)} + if len(exec.Args) > 0 { + harnessArgs = append(harnessArgs, shellJoinArgs(exec.Args)) + } + harnessPath := SetupActionDestinationShell + "/" + e.harnessScriptFilename() + engineCommand = fmt.Sprintf("%s %s %s", nodeRuntimeResolutionCommand, harnessPath, strings.Join(harnessArgs, " ")) + } else { + commandParts := []string{commandName} + if len(exec.Args) > 0 { + commandParts = append(commandParts, shellJoinArgs(exec.Args)) + } + if modelFragment := e.modelFlagFragment(exec, workflowData); modelFragment != "" { + commandParts = append(commandParts, modelFragment) + } + if mcpFragment := e.mcpFlagFragment(exec, workflowData); mcpFragment != "" { + commandParts = append(commandParts, mcpFragment) + } + commandParts = append(commandParts, fmt.Sprintf(`"$(cat %s)"`, constants.AwPromptsFile)) + engineCommand = strings.Join(commandParts, " ") + engineCommand = getWorkspaceCommandPrefixFor(workflowData.EngineConfig) + engineCommand + } + + firewallEnabled := isFirewallEnabled(workflowData) + // harness-script requires the AWF API proxy sidecar (/reflect) to dynamically + // configure the agentic engine at runtime. Force AWF execution when a harness is + // present unless the sandbox has been explicitly disabled via sandbox.agent: false, + // which also prevents the /reflect endpoint from being available. + if behavior.HarnessScript != "" && !isFirewallDisabledBySandboxAgent(workflowData) { + firewallEnabled = true + } + var command string + if firewallEnabled { + command = e.buildFirewallCommand(exec, workflowData, logFile, engineCommand) + } else if exec.WriteTimestamp { + command = fmt.Sprintf("set -o pipefail\nexport no_proxy=\"${NO_PROXY:-}\"\nprintf '%%s' \"$(date +%%s%%3N)\" > %s\n%s 2>&1 | tee -a %s", + AgentCLIStartMsPath, engineCommand, logFile) + } else { + command = fmt.Sprintf("set -o pipefail\nexport no_proxy=\"${NO_PROXY:-}\"\n%s 2>&1 | tee -a %s", engineCommand, logFile) + } + + env := map[string]string{ + "GH_AW_PROMPT": constants.AwPromptsFile, + "GITHUB_WORKSPACE": "${{ github.workspace }}", + "RUNNER_TEMP": "${{ runner.temp }}", + // Set NO_PROXY so that the AWF agent's HTTP client skips the squid proxy + // for local endpoints. The lowercase no_proxy variant is exported inside + // the run script rather than as a YAML env key because GitHub's workflow + // parser rejects case-insensitive duplicate env keys (NO_PROXY/no_proxy), + // which causes workflow_dispatch to fail with "failed to parse workflow". + "NO_PROXY": constants.AWFNoProxyHosts, + } + injectWorkflowCallNetworkAllowedEnv(env, workflowData) + + // Apply static env vars declared in the engine definition first so that + // the dynamic AWF vars below can still override them if needed. + maps.Copy(env, exec.Env) + + if exec.ProviderEnvMode == behaviorProviderEnvModeUniversalLLMConsumer { + e.ApplyUniversalProviderEnv(env, workflowData, firewallEnabled) + } + + if exec.MCPConfigEnvVar != "" && HasMCPServers(workflowData) { + if behavior.ConfigFile != nil { + env[exec.MCPConfigEnvVar] = "${{ github.workspace }}/" + behavior.ConfigFile.Path + } else { + mcpPath := constants.McpServersJsonPathExpr + if behavior.MCP != nil && behavior.MCP.ConfigPath != "" { + mcpPath = behavior.MCP.ConfigPath + } + env[exec.MCPConfigEnvVar] = mcpPath + } + } + + for _, binding := range e.definition.Auth { + if binding.Secret != "" { + env[binding.Secret] = "${{ secrets." + binding.Secret + " }}" + } + } + + // When a harness script is present and the AWF firewall is running, signal to the + // harness that the AWF API proxy sidecar is available so it can read /reflect data. + if behavior.HarnessScript != "" && firewallEnabled { + env["AWF_REFLECT_ENABLED"] = "1" + } + + applySafeOutputEnvToMap(env, workflowData) + applyTraceContextEnvToMap(env) + applyOptionalEngineToolTimeouts(env, workflowData) + applyEngineMaxTurnsEnv(env, workflowData) + applyEngineCwdEnv(env, workflowData) + applyEngineAndAgentEnv(env, workflowData, behaviorDefinedEngineLog) + applyMCPScriptsSecretEnv(env, workflowData) + + if exec.ModelEnvVarName != "" { + if workflowData != nil && workflowData.EngineConfig != nil && workflowData.EngineConfig.Model != "" { + modelVal := workflowData.EngineConfig.Model + if exec.ModelEnvProviderPrefix != "" { + if parts := strings.SplitN(modelVal, "/", 2); len(parts) == 2 { + modelVal = exec.ModelEnvProviderPrefix + "/" + parts[1] + } + } + env[exec.ModelEnvVarName] = modelVal + } + } + + stepLines := []string{ + " - name: " + exec.StepName, + " id: agentic_execution", + } + if workflowData != nil && workflowData.TimeoutMinutes != "" { + timeoutValue := strings.TrimPrefix(workflowData.TimeoutMinutes, "timeout-minutes: ") + stepLines = append(stepLines, " timeout-minutes: "+timeoutValue) + } else { + stepLines = append(stepLines, fmt.Sprintf(" timeout-minutes: %d", int(constants.DefaultAgenticWorkflowTimeout/time.Minute))) + } + filteredEnv := FilterEnvForSecrets(env, e.GetRequiredSecretNames(workflowData)) + addCliProxyGHTokenToEnv(filteredEnv, workflowData) + stepLines = FormatStepWithCommandAndEnv(stepLines, command, filteredEnv) + steps = append(steps, GitHubActionStep(stepLines)) + return steps +} + +func (e *BehaviorDefinedEngine) modelFlagFragment(exec *EngineExecutionDefinition, workflowData *WorkflowData) string { + if exec.ModelEnvVarName == "" || exec.ModelFlag == "" { + return "" + } + if workflowData == nil || workflowData.EngineConfig == nil || workflowData.EngineConfig.Model == "" { + return "" + } + return fmt.Sprintf(`%s "$%s"`, exec.ModelFlag, exec.ModelEnvVarName) +} + +func (e *BehaviorDefinedEngine) mcpFlagFragment(exec *EngineExecutionDefinition, workflowData *WorkflowData) string { + if exec.MCPConfigFlag == "" || !HasMCPServers(workflowData) { + return "" + } + path := constants.McpServersJsonPathExpr + if behavior := e.behavior(); behavior != nil && behavior.MCP != nil { + if behavior.MCP.ConfigPath != "" { + path = behavior.MCP.ConfigPath + } + } + return shellJoinArgs([]string{exec.MCPConfigFlag, path}) +} + +func (e *BehaviorDefinedEngine) buildFirewallCommand(exec *EngineExecutionDefinition, workflowData *WorkflowData, logFile, engineCommand string) string { + allowedDomains := e.allowedDomains(workflowData) + // Propagate no_proxy inside the AWF container. --env-all forwards NO_PROXY + // from the YAML env block, but Bun (and other runtimes) also check the + // lowercase variant, so we export it explicitly from the uppercase value. + engineCommandWithPath := fmt.Sprintf("export no_proxy=\"${NO_PROXY:-}\" && %s && %s", GetNpmBinPathSetup(), engineCommand) + if mcpCLIPath := GetMCPCLIPathSetup(workflowData); mcpCLIPath != "" { + engineCommandWithPath = fmt.Sprintf("%s && %s", mcpCLIPath, engineCommandWithPath) + } + + return BuildAWFCommand(AWFCommandConfig{ + EngineName: e.GetID(), + EngineCommand: engineCommandWithPath, + LogFile: logFile, + WorkflowData: workflowData, + UsesTTY: false, + AllowedDomains: allowedDomains, + ExcludeEnvVarNames: ComputeAWFExcludeEnvVarNames(workflowData, e.GetRequiredSecretNames(workflowData)), + }) +} + +func (e *BehaviorDefinedEngine) allowedDomains(workflowData *WorkflowData) string { + engineName := constants.EngineName(e.GetID()) + if e.usesUniversalLLMConsumer() && workflowData != nil && workflowData.EngineConfig != nil { + return mustGetAllowedDomainsForEngineWithModel(engineName, workflowData.EngineConfig.Model, workflowData.NetworkPermissions, workflowData.Tools, workflowData.Runtimes) + } + return GetAllowedDomainsForEngine(engineName, workflowData.NetworkPermissions, workflowData.Tools, workflowData.Runtimes) +} + +func (e *BehaviorDefinedEngine) buildConfigFileStep() GitHubActionStep { + behavior := e.behavior() + if behavior == nil || behavior.ConfigFile == nil || behavior.ConfigFile.Path == "" { + return nil + } + config := behavior.ConfigFile + command := fmt.Sprintf(`umask 077 +mkdir -p "$(dirname "$GITHUB_WORKSPACE/%s")" +CONFIG="$GITHUB_WORKSPACE/%s" +BASE_CONFIG='%s' +if [ -f "$CONFIG" ]; then + MERGED=$(jq -n --argjson base "$BASE_CONFIG" --argjson existing "$(cat "$CONFIG")" '$existing * $base') + echo "$MERGED" > "$CONFIG" +else + echo "$BASE_CONFIG" > "$CONFIG" +fi +chmod 600 "$CONFIG"`, config.Path, config.Path, config.Content) + if config.MergeStrategy != behaviorConfigMergeJSON { + command = fmt.Sprintf(`umask 077 +mkdir -p "$(dirname "$GITHUB_WORKSPACE/%s")" +cat <<'EOF' > "$GITHUB_WORKSPACE/%s" +%s +EOF +chmod 600 "$GITHUB_WORKSPACE/%s"`, config.Path, config.Path, config.Content, config.Path) + } + + stepLines := []string{" - name: " + config.StepName} + stepLines = FormatStepWithCommandAndEnv(stepLines, command, nil) + return GitHubActionStep(stepLines) +} + +func isEngineDefinitionForm(def *EngineDefinition) bool { + if def == nil { + return false + } + // Treat richer metadata-only objects as shared engine definitions. Plain engine + // config objects ("id", "model", "env", etc.) should continue down the normal + // EngineConfig path instead of being registered as catalog entries. + if def.DisplayName != "" || def.RuntimeID != "" || def.Experimental || def.GHSkillAgentName != "" || def.Behaviors != nil || len(def.Auth) > 0 { + return true + } + if def.Provider.Name != "" || def.Provider.Auth != nil || def.Provider.Request != nil { + return true + } + return def.Models.Default != "" || len(def.Models.Supported) > 0 || len(def.Options) > 0 +} + +func parseEngineDefinitionFromJSON(engineJSON string) (*EngineDefinition, error) { + if engineJSON == "" { + return nil, nil + } + var engineData any + if err := json.Unmarshal([]byte(engineJSON), &engineData); err != nil { + return nil, fmt.Errorf("failed to parse engine JSON: %w", err) + } + if _, ok := engineData.(map[string]any); !ok { + return nil, nil + } + yamlBytes, err := yaml.Marshal(engineData) + if err != nil { + return nil, fmt.Errorf("failed to convert engine JSON to yaml: %w", err) + } + var def EngineDefinition + if err := yaml.Unmarshal(yamlBytes, &def); err != nil { + return nil, fmt.Errorf("failed to parse engine definition: %w", err) + } + if def.RuntimeID == "" { + def.RuntimeID = def.ID + } + return &def, nil +} + +func (c *Compiler) registerNamedEngineDefinitionFromJSON(engineJSON string) error { + def, err := parseEngineDefinitionFromJSON(engineJSON) + if err != nil || !isEngineDefinitionForm(def) { + return err + } + if def.Behaviors != nil { + engine, buildErr := NewBehaviorDefinedEngine(def) + if buildErr != nil { + return buildErr + } + if regErr := c.engineRegistry.Register(engine); regErr != nil { + return regErr + } + def.RuntimeID = engine.GetID() + } + c.engineCatalog.Register(def) + return nil +} diff --git a/pkg/workflow/behavior_defined_engine_harness_test.go b/pkg/workflow/behavior_defined_engine_harness_test.go new file mode 100644 index 00000000000..ce6322bb1c7 --- /dev/null +++ b/pkg/workflow/behavior_defined_engine_harness_test.go @@ -0,0 +1,162 @@ +//go:build !integration + +package workflow + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newHarnessEngineDefinition returns a minimal EngineDefinition with harness-script set. +func newHarnessEngineDefinition() *EngineDefinition { + return &EngineDefinition{ + ID: "testharness", + DisplayName: "TestHarness", + Description: "A test engine with a harness script", + Behaviors: &EngineBehaviorDefinition{ + Execution: &EngineExecutionDefinition{ + CommandName: "testharness-cli", + Args: []string{"run"}, + StepName: "Execute TestHarness CLI", + }, + HarnessScript: `"use strict"; +// Minimal test harness +const cmd = process.argv[2]; +const { spawnSync } = require("child_process"); +spawnSync(cmd, process.argv.slice(3), { stdio: "inherit" }); +`, + }, + } +} + +// TestBehaviorDefinedEngineHarnessScript verifies that harness-script is wired correctly +// into the engine's execution steps. +func TestBehaviorDefinedEngineHarnessScript(t *testing.T) { + def := newHarnessEngineDefinition() + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + + t.Run("harness_write_step_included", func(t *testing.T) { + workflowData := &WorkflowData{Name: "test"} + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + + // Steps: [harness write, execution] + require.Len(t, steps, 2, "should generate harness-write step and execution step") + + harnessStepContent := strings.Join(steps[0], "\n") + assert.Contains(t, harnessStepContent, "Write TestHarness harness script", "step name should include engine display name") + assert.Contains(t, harnessStepContent, "testharness_harness.cjs", "step should write the correct harness filename") + assert.Contains(t, harnessStepContent, "gh-aw/actions", "step should write to the setup action destination directory") + assert.Contains(t, harnessStepContent, harnessScriptHeredocDelimiter, "step should use heredoc delimiter") + assert.Contains(t, harnessStepContent, "use strict", "step should embed harness script content") + }) + + t.Run("execution_step_uses_node_harness", func(t *testing.T) { + workflowData := &WorkflowData{Name: "test"} + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + require.Len(t, steps, 2, "should generate harness-write step and execution step") + + execStepContent := strings.Join(steps[1], "\n") + assert.Contains(t, execStepContent, "id: agentic_execution", "execution step should have agentic_execution ID") + assert.Contains(t, execStepContent, "GH_AW_NODE_EXEC", "execution should use node runtime resolution command") + assert.Contains(t, execStepContent, "testharness_harness.cjs", "execution should invoke the harness file") + assert.Contains(t, execStepContent, "testharness-cli", "execution should pass command name to harness") + // When harness is set, inline prompt substitution must NOT appear + assert.NotContains(t, execStepContent, `"$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"`, "harness execution must not include inline prompt substitution") + }) + + t.Run("awf_reflect_enabled_when_firewall_on", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test", + NetworkPermissions: &NetworkPermissions{ + Allowed: []string{"defaults"}, + Firewall: &FirewallConfig{Enabled: true}, + }, + } + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + require.GreaterOrEqual(t, len(steps), 2, "should have at least harness-write and execution steps") + + // AWF_REFLECT_ENABLED must be set when firewall is on and harness-script is present + execStepContent := strings.Join(steps[len(steps)-1], "\n") + assert.Contains(t, execStepContent, "AWF_REFLECT_ENABLED: 1", "AWF_REFLECT_ENABLED must be set when harness-script and firewall are both active") + }) + + t.Run("awf_forced_and_reflect_enabled_without_explicit_firewall", func(t *testing.T) { + // harness-script always forces AWF so the harness can read /reflect from the API proxy. + // AWF_REFLECT_ENABLED must therefore be present even when no explicit firewall is configured. + workflowData := &WorkflowData{Name: "test"} + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + require.GreaterOrEqual(t, len(steps), 2, "should have at least harness-write and execution steps") + + execStepContent := strings.Join(steps[len(steps)-1], "\n") + assert.Contains(t, execStepContent, "AWF_REFLECT_ENABLED: 1", "AWF_REFLECT_ENABLED must be set when harness-script forces AWF execution") + assert.Contains(t, execStepContent, "sudo", "execution step must use AWF when harness-script is present") + }) + + t.Run("env_vars_still_set", func(t *testing.T) { + workflowData := &WorkflowData{Name: "test"} + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + require.GreaterOrEqual(t, len(steps), 2) + + execStepContent := strings.Join(steps[len(steps)-1], "\n") + assert.Contains(t, execStepContent, "GH_AW_PROMPT:", "GH_AW_PROMPT must be set for harness to read prompt file") + assert.Contains(t, execStepContent, "RUNNER_TEMP:", "RUNNER_TEMP must be set") + }) + + t.Run("harness_filename", func(t *testing.T) { + assert.Equal(t, "testharness_harness.cjs", engine.harnessScriptFilename()) + }) + + t.Run("heredoc_delimiter_collision_skips_harness_write_step", func(t *testing.T) { + // An engine whose harness-script contains the heredoc delimiter at line start must + // not generate a harness write step (to avoid premature heredoc termination). + collisionDef := &EngineDefinition{ + ID: "collision", + DisplayName: "Collision", + Behaviors: &EngineBehaviorDefinition{ + Execution: &EngineExecutionDefinition{ + CommandName: "collision-cli", + StepName: "Execute", + }, + HarnessScript: "// legit JS\n" + harnessScriptHeredocDelimiter + "\nconsole.log('hi');", + }, + } + eng, err := NewBehaviorDefinedEngine(collisionDef) + require.NoError(t, err) + harnessStep := eng.buildHarnessWriteStep() + assert.Nil(t, harnessStep, "harness write step must be skipped when script contains the heredoc delimiter") + }) +} + +// TestBehaviorDefinedEngineNoHarnessScript verifies that engines without harness-script +// continue to use the direct command execution path (inline prompt substitution). +func TestBehaviorDefinedEngineNoHarnessScript(t *testing.T) { + def := &EngineDefinition{ + ID: "noharness", + DisplayName: "NoHarness", + Behaviors: &EngineBehaviorDefinition{ + Execution: &EngineExecutionDefinition{ + CommandName: "noharness-cli", + Args: []string{"run"}, + StepName: "Execute NoHarness CLI", + }, + }, + } + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + + workflowData := &WorkflowData{Name: "test"} + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + + // No config-file, no harness: only the execution step + require.Len(t, steps, 1, "should generate only execution step when no harness-script and no config-file") + + execStepContent := strings.Join(steps[0], "\n") + assert.Contains(t, execStepContent, "noharness-cli run", "should invoke the command directly") + assert.Contains(t, execStepContent, `"$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"`, "direct execution must include inline prompt substitution") + assert.NotContains(t, execStepContent, "GH_AW_NODE_EXEC", "should not use node harness when harness-script is absent") + assert.NotContains(t, execStepContent, "GHAW_HARNESS_SCRIPT_EOF", "no harness write step should be present") +} diff --git a/pkg/workflow/compiler_orchestrator_engine.go b/pkg/workflow/compiler_orchestrator_engine.go index 71c39fe9025..e2f05c77b5d 100644 --- a/pkg/workflow/compiler_orchestrator_engine.go +++ b/pkg/workflow/compiler_orchestrator_engine.go @@ -271,6 +271,11 @@ func (c *Compiler) resolveEngineFromIncludesAndImports( if finalEngineSetting != "" { engineSetting = finalEngineSetting } + for _, engineJSON := range allEngines { + if err := c.registerNamedEngineDefinitionFromJSON(engineJSON); err != nil { + return "", nil, fmt.Errorf("failed to register engine definition from included file: %w", err) + } + } if engineConfig == nil && len(allEngines) > 0 { orchestratorEngineLog.Printf("Extracting engine config from included file") engineConfig, err = c.extractEngineConfigFromJSON(allEngines[0]) diff --git a/pkg/workflow/crush_engine.go b/pkg/workflow/crush_engine.go deleted file mode 100644 index 5d7fea6efe4..00000000000 --- a/pkg/workflow/crush_engine.go +++ /dev/null @@ -1,180 +0,0 @@ -package workflow - -import ( - "fmt" - - "github.com/github/gh-aw/pkg/constants" - "github.com/github/gh-aw/pkg/logger" -) - -var crushLog = logger.New("workflow:crush_engine") - -// CrushEngine represents the Crush CLI agentic engine. -// Crush is a provider-agnostic, open-source AI coding agent with broader BYOK -// (Bring Your Own Key) support, but gh-aw currently supports a subset of -// providers for engine.model validation: copilot, anthropic, openai, and codex. -type CrushEngine struct { - UniversalLLMConsumerEngine -} - -func NewCrushEngine() *CrushEngine { - return &CrushEngine{ - UniversalLLMConsumerEngine: UniversalLLMConsumerEngine{ - BaseEngine: BaseEngine{ - id: "crush", - displayName: "Crush", - description: "Crush CLI with headless mode and multi-provider LLM support", - experimental: true, // Start as experimental until smoke tests pass consistently - capabilities: EngineCapabilities{ - ToolsAllowlist: false, // Crush manages its own tool permissions via .crush.json - MaxTurns: true, // AWF max-turns is supported for Crush runs - WebSearch: false, // Has built-in websearch but not exposed via gh-aw neutral tools yet - }, - }, - }, - } -} - -// GetModelEnvVarName returns the native environment variable name that the Crush CLI uses -// for model selection. Setting CRUSH_MODEL is equivalent to passing --model to the CLI. -func (e *CrushEngine) GetModelEnvVarName() string { - return constants.CrushCLIModelEnvVar -} - -// GetRequiredSecretNames returns the list of secrets required by the Crush engine. -// By default, Crush routes through the Copilot API using COPILOT_GITHUB_TOKEN -// (or ${{ github.token }} when permissions.copilot-requests is set to write). -// Additional provider API keys can be added via engine.env overrides. -func (e *CrushEngine) GetRequiredSecretNames(workflowData *WorkflowData) []string { - crushLog.Print("Collecting required secrets for Crush engine") - return e.GetUniversalRequiredSecretNames(workflowData) -} - -// GetSupportedEnvVarKeys returns the engine.env variable names that the Crush engine -// supports as defined in the AWF specification. Crush is a multi-provider engine so all -// provider API keys are valid engine.env overrides. -func (e *CrushEngine) GetSupportedEnvVarKeys() []string { - return []string{ - constants.CopilotGitHubToken, - constants.AnthropicAPIKey, - constants.CodexAPIKey, - constants.OpenAIAPIKey, - } -} - -// GetInstallationSteps returns the GitHub Actions steps needed to install Crush CLI -func (e *CrushEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubActionStep { - crushLog.Printf("Generating installation steps for Crush engine: workflow=%s", workflowData.Name) - - // Skip installation if custom command is specified - if workflowData.EngineConfig != nil && workflowData.EngineConfig.Command != "" { - crushLog.Printf("Skipping installation steps: custom command specified (%s)", workflowData.EngineConfig.Command) - return []GitHubActionStep{} - } - - // Use version from engine config if provided, otherwise default to pinned version - version := string(constants.DefaultCrushVersion) - if workflowData.EngineConfig != nil && workflowData.EngineConfig.Version != "" { - version = workflowData.EngineConfig.Version - } - - // Crush requires post-install scripts (native binaries) so --ignore-scripts must - // NOT be passed. This is intentionally different from other engine installs. - npmSteps := GenerateNpmInstallSteps( - "@charmland/crush", - version, - "Install Crush CLI", - "crush", - true, // Include Node.js setup - true, // Crush requires post-install scripts for native binaries - resolveRuntimeCooldown(workflowData, "node"), - ) - - // Run crush --version to verify the installation and force any deferred binary downloads - commandName := "crush" - if workflowData.EngineConfig != nil && workflowData.EngineConfig.Command != "" { - commandName = workflowData.EngineConfig.Command - } - versionStep := GitHubActionStep{ - " - name: Verify Crush CLI installation", - " run: " + commandName + " --version", - } - npmSteps = append(npmSteps, versionStep) - - return BuildNpmEngineInstallStepsWithAWF(npmSteps, workflowData) -} - -// GetSecretValidationStep returns the secret validation step for the Crush engine. -// Returns an empty step if permissions.copilot-requests is write (uses GitHub Actions token). -func (e *CrushEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHubActionStep { - return e.GetUniversalSecretValidationStep( - workflowData, - "Crush CLI", - "https://github.github.com/gh-aw/reference/engines/#crush", - ) -} - -// GetAgentManifestFiles returns Crush-specific instruction files that should be -// treated as security-sensitive manifests. Modifying these files can change the -// agent's instructions, permissions, or configuration on the next run. -// .crush.json is the primary Crush config file; AGENTS.md is the cross-engine -// convention that Crush also reads. -func (e *CrushEngine) GetAgentManifestFiles() []string { - return []string{".crush.json", "AGENTS.md"} -} - -// GetAgentManifestPathPrefixes returns Crush-specific config directory prefixes -// that must be protected from fork PR injection. -// The .crush/ directory contains agent configuration, instructions, and other -// settings that could alter agent behaviour. -func (e *CrushEngine) GetAgentManifestPathPrefixes() []string { - return []string{".crush/"} -} - -// GetDeclaredOutputFiles returns the output files that Crush may produce. -func (e *CrushEngine) GetDeclaredOutputFiles() []string { - return []string{} -} - -// GetExecutionSteps returns the GitHub Actions steps for executing Crush -func (e *CrushEngine) GetExecutionSteps(workflowData *WorkflowData, logFile string) []GitHubActionStep { - crushLog.Printf("Generating execution steps for Crush engine: workflow=%s, firewall=%v", - workflowData.Name, isFirewallEnabled(workflowData)) - - return e.BuildCLIEngineExecutionSteps(workflowData, logFile, UniversalCLIEngineExecutionConfig{ - EngineConstant: constants.CrushEngine, - DefaultCommandName: "crush", - ExtraCLIArgs: []string{"--verbose"}, - MCPConfigFile: ".crush.json", - StepName: "Execute Crush CLI", - ConfigStep: e.generateCrushConfigStep(workflowData), - ModelEnvVarName: constants.CrushCLIModelEnvVar, - WriteTimestamp: true, - }) -} - -// generateCrushConfigStep writes .crush.json with all permissions set to allow -// to prevent CI hanging on permission prompts. -func (e *CrushEngine) generateCrushConfigStep(_ *WorkflowData) GitHubActionStep { - // Build the config JSON with all permissions set to allow - // OpenCode/Crush uses "permission" (singular) — "permissions" (plural) is silently ignored. - // "external_directory" must be "allow" in non-interactive CI mode (defaults to "ask" → implicit deny). - configJSON := `{"agent":{"build":{"permission":{"bash":"allow","edit":"allow","read":"allow","glob":"allow","grep":"allow","write":"allow","webfetch":"allow","websearch":"allow","external_directory":"allow"}}}}` - - // Shell command to write or merge the config with restrictive permissions - command := fmt.Sprintf(`umask 077 -mkdir -p "$GITHUB_WORKSPACE" -CONFIG="$GITHUB_WORKSPACE/.crush.json" -BASE_CONFIG='%s' -if [ -f "$CONFIG" ]; then - MERGED=$(jq -n --argjson base "$BASE_CONFIG" --argjson existing "$(cat "$CONFIG")" '$existing * $base') - echo "$MERGED" > "$CONFIG" -else - echo "$BASE_CONFIG" > "$CONFIG" -fi -chmod 600 "$CONFIG"`, configJSON) - - stepLines := []string{" - name: Write Crush Config"} - stepLines = FormatStepWithCommandAndEnv(stepLines, command, nil) - return GitHubActionStep(stepLines) -} diff --git a/pkg/workflow/crush_engine_test.go b/pkg/workflow/crush_engine_test.go index 1eda12778d0..f0126b5fb10 100644 --- a/pkg/workflow/crush_engine_test.go +++ b/pkg/workflow/crush_engine_test.go @@ -6,12 +6,14 @@ import ( "strings" "testing" + "github.com/github/gh-aw/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCrushEngine(t *testing.T) { - engine := NewCrushEngine() + engine, err := newBuiltinBehaviorDefinedEngine("crush") + require.NoError(t, err) t.Run("engine identity", func(t *testing.T) { assert.Equal(t, "crush", engine.GetID(), "Engine ID should be 'crush'") @@ -148,7 +150,8 @@ func TestCrushEngine(t *testing.T) { } func TestCrushEngineInstallation(t *testing.T) { - engine := NewCrushEngine() + engine, err := newBuiltinBehaviorDefinedEngine("crush") + require.NoError(t, err) t.Run("standard installation", func(t *testing.T) { workflowData := &WorkflowData{ @@ -226,7 +229,8 @@ func TestCrushEngineInstallation(t *testing.T) { } func TestCrushEngineExecution(t *testing.T) { - engine := NewCrushEngine() + engine, err := newBuiltinBehaviorDefinedEngine("crush") + require.NoError(t, err) t.Run("basic execution", func(t *testing.T) { workflowData := &WorkflowData{ @@ -245,7 +249,7 @@ func TestCrushEngineExecution(t *testing.T) { assert.Contains(t, stepContent, `"$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"`, "Should include prompt argument") assert.Contains(t, stepContent, "/tmp/test.log", "Should include log file") assert.Contains(t, stepContent, "OPENAI_API_KEY: ${{ secrets.COPILOT_GITHUB_TOKEN }}", "Should set OPENAI_API_KEY from COPILOT_GITHUB_TOKEN") - assert.Contains(t, stepContent, "NO_PROXY: localhost,127.0.0.1", "Should set NO_PROXY env var") + assert.Contains(t, stepContent, "NO_PROXY: "+constants.AWFNoProxyHosts, "Should set NO_PROXY env var") }) t.Run("basic execution with copilot-requests permission", func(t *testing.T) { @@ -378,14 +382,15 @@ func TestCrushEngineExecution(t *testing.T) { assert.Contains(t, configContent, "Write Crush Config", "First step should be Write Crush Config") assert.Contains(t, configContent, ".crush.json", "Config step should reference .crush.json") assert.Contains(t, configContent, `"permission"`, "Config step should use 'permission' (singular, not 'permissions')") - assert.Contains(t, configContent, `"external_directory":"allow"`, "Config step should allow external_directory for non-interactive CI") + assert.Contains(t, configContent, `"external_directory": "allow"`, "Config step should allow external_directory for non-interactive CI") assert.NotContains(t, configContent, `"permissions"`, "Config step must NOT use 'permissions' (plural) — silently ignored by OpenCode)") assert.Contains(t, execContent, "Execute Crush CLI", "Second step should be Execute Crush CLI") }) } func TestCrushEngineFirewallIntegration(t *testing.T) { - engine := NewCrushEngine() + engine, err := newBuiltinBehaviorDefinedEngine("crush") + require.NoError(t, err) t.Run("firewall enabled", func(t *testing.T) { workflowData := &WorkflowData{ diff --git a/pkg/workflow/crush_mcp.go b/pkg/workflow/crush_mcp.go deleted file mode 100644 index 54a705a850f..00000000000 --- a/pkg/workflow/crush_mcp.go +++ /dev/null @@ -1,18 +0,0 @@ -package workflow - -import ( - "strings" - - "github.com/github/gh-aw/pkg/constants" - "github.com/github/gh-aw/pkg/logger" -) - -var crushMCPLog = logger.New("workflow:crush_mcp") - -// RenderMCPConfig renders MCP server configuration for Crush CLI -func (e *CrushEngine) RenderMCPConfig(sb *strings.Builder, tools map[string]any, mcpTools []string, workflowData *WorkflowData) error { - crushMCPLog.Printf("Rendering MCP config for Crush: tool_count=%d, mcp_tool_count=%d", len(tools), len(mcpTools)) - - // Crush uses JSON format without Copilot-specific fields and multi-line args - return renderDefaultJSONMCPConfig(sb, tools, mcpTools, workflowData, constants.TmpMcpServersJsonPath) -} diff --git a/pkg/workflow/data/engines/crush.md b/pkg/workflow/data/engines/crush.md index b36025b3ff2..ba5c71135ab 100644 --- a/pkg/workflow/data/engines/crush.md +++ b/pkg/workflow/data/engines/crush.md @@ -4,8 +4,55 @@ engine: display-name: Crush description: Crush CLI with headless mode and multi-provider LLM support runtime-id: crush + experimental: true provider: name: github + behaviors: + secret-strategy: universal-llm-consumer + capabilities: + max-turns: true + manifest: + files: + - .crush.json + - AGENTS.md + path-prefixes: + - .crush/ + installation: + package-manager: npm + package-name: "@charmland/crush" + version: "0.59.0" + step-name: Install Crush + binary-name: crush + include-node-setup: true + post-install-scripts: true + cooldown: true + verify-command: crush --version + verify-step-name: Verify Crush CLI installation + docs-url: https://github.com/charmbracelet/crush + config-file: + path: .crush.json + step-name: Write Crush Config + content: |- + { + "permission": { + "edit": "allow", + "bash": "allow", + "external_directory": "allow" + } + } + merge-strategy: json-merge + execution: + command-name: crush + args: + - run + - --verbose + step-name: Execute Crush CLI + model-env-var: CRUSH_MODEL + mcp-config-env-var: GH_AW_MCP_CONFIG + write-timestamp: true + provider-env-mode: universal-llm-consumer + mcp: + config-path: .crush.json ---