diff --git a/.github/workflows/shared/pydantic.md b/.github/workflows/shared/pydantic.md index 12fe0f1073d..15e9e577d20 100644 --- a/.github/workflows/shared/pydantic.md +++ b/.github/workflows/shared/pydantic.md @@ -1,15 +1,17 @@ --- runtimes: - uv: - version: latest + python: + version: "3.12" pre-agent-steps: - - name: Predownload Pydantic AI CLI - run: uv run pai --version + - name: Preinstall Pydantic AI CLI + run: | + python3 -m pip install --quiet --user --disable-pip-version-check "pydantic-ai==$GH_AW_ENGINE_VERSION" + "$HOME/.local/bin/pai" --version engine: id: pydantic-ai - version: "0.1.0" + version: "2.26.0" display-name: Pydantic AI - description: Pydantic AI headless coding agent CLI with MCP support + description: Pydantic AI CLI (pai) running one-shot prompts with MCP tool support experimental: true mcp: true provider: @@ -28,21 +30,127 @@ engine: - raw.githubusercontent.com - api.github.com - objects.githubusercontent.com + - pypi.org + - files.pythonhosted.org provider-domains: copilot: api.githubcopilot.com anthropic: api.anthropic.com openai: api.openai.com execution: - command-name: uv - args: - - run - - pai - - run + command-name: pai step-name: Execute Pydantic AI CLI model-env-var: PAI_MODEL - mcp-config-env-var: GH_AW_MCP_CONFIG write-timestamp: true provider-env-mode: universal-llm-consumer + harness-script: | + const { spawnSync } = require("child_process"); + const { existsSync, readFileSync } = require("fs"); + const { join } = require("path"); + const { homedir } = require("os"); + + const [command, ...commandArgs] = process.argv.slice(2); + + const promptFile = process.env.GH_AW_PROMPT; + if (!promptFile) { + throw new Error("GH_AW_PROMPT is not set"); + } + const workspace = process.env.GITHUB_WORKSPACE; + if (!workspace) { + throw new Error("GITHUB_WORKSPACE is not set"); + } + + const localBin = join(homedir(), ".local", "bin"); + const env = { ...process.env, PATH: `${localBin}:${process.env.PATH || ""}` }; + delete env.COPILOT_GITHUB_TOKEN; + // The AWF api-proxy selects the upstream provider by the port the client connects + // to and injects the real credentials itself, ignoring the inbound key. AWF rewrites + // OPENAI_BASE_URL inside the sandbox to the proxy's OpenAI port, which forwards to + // api.openai.com, so `pai` — which is configured only through the environment — is + // pointed at the port that steers to the configured provider instead, the same + // endpoint Aider and OpenCode use, with the usual placeholder key. + env.OPENAI_API_KEY = "awf-copilot-proxy"; + env.OPENAI_BASE_URL = "http://172.30.0.30:10002"; + + const args = [...commandArgs]; + // `pai` sends the model name verbatim, minus the `openai-chat:` provider marker + // that selects its OpenAI-compatible client, so the bare model ID reaches the + // api-proxy — which steers to the configured provider by the port it is reached + // on, not by a prefix in the model name: Copilot rejects `copilot/` with + // `model_not_supported`. The proxy exposes Copilot Claude models under their + // dotted IDs, so `copilot/claude-sonnet-4-5` becomes `claude-sonnet-4.5`. + const model = env.PAI_MODEL?.replace(/^.*\//, "").replace(/^(claude-(?:haiku|sonnet|opus)-\d+)-(\d+)$/, "$1.$2"); + if (model) { + args.push("-m", `openai-chat:${model}`); + } + const agentSpec = join(workspace, ".pydantic-ai", "agent.json"); + if (existsSync(agentSpec)) { + args.push("-a", agentSpec); + } + args.push(readFileSync(promptFile, "utf8")); + + const result = spawnSync(command, args, { cwd: workspace, encoding: "utf8", env }); + process.stdout.write(result.stdout || ""); + process.stderr.write(result.stderr || ""); + if (result.error || result.status !== 0) { + throw new Error(`Pydantic AI execution failed: ${result.error?.message || `exit code ${result.status}`}`); + } + mcp: + config-path: .pydantic-ai/agent.json + config-adapter: | + // Converts the MCP gateway's standard HTTP-based configuration into a + // Pydantic AI agent spec (https://ai.pydantic.dev), which is the only way + // the `pai` CLI can be given MCP servers: each gateway server becomes an + // `MCP` capability entry and the spec file is passed via `pai -a `. + // An agent spec must declare a `model`, but the harness always appends + // `-m openai-chat:` when the workflow declares a model, which takes + // precedence. The value below is only a valid-by-construction fallback for + // workflows that do not declare a model. + const fs = require("fs"); + const path = require("path"); + + const requireEnvVar = name => { + const value = process.env[name]; + if (!value) throw new Error(`${name} environment variable is required`); + return value; + }; + + const gatewayOutputPath = requireEnvVar("MCP_GATEWAY_OUTPUT"); + const workspace = requireEnvVar("GITHUB_WORKSPACE"); + const gatewayDomain = process.env.MCP_GATEWAY_DOMAIN || "host.docker.internal"; + const gatewayPort = requireEnvVar("MCP_GATEWAY_PORT"); + const gatewayURL = `http://${gatewayDomain}:${gatewayPort}`; + + let cliServers; + try { + cliServers = new Set(JSON.parse(process.env.GH_AW_MCP_CLI_SERVERS || "[]")); + } catch (error) { + throw new Error(`Failed to parse GH_AW_MCP_CLI_SERVERS: ${error instanceof Error ? error.message : String(error)}`); + } + + const gatewayOutput = JSON.parse(fs.readFileSync(gatewayOutputPath, "utf8")); + const rawServers = gatewayOutput.mcpServers; + const servers = rawServers && typeof rawServers === "object" && !Array.isArray(rawServers) ? rawServers : {}; + + const capabilities = []; + for (const [name, entry] of Object.entries(servers)) { + if (cliServers.has(name) || !entry || typeof entry !== "object") continue; + if (typeof entry.url !== "string") { + console.log(`Skipping MCP server ${name}: the Pydantic AI CLI only supports HTTP MCP servers`); + continue; + } + const mcp = { + id: name, + url: entry.url.replace(/^http:\/\/[^/]+\/mcp\//, `${gatewayURL}/mcp/`), + }; + if (entry.headers && typeof entry.headers === "object") mcp.headers = entry.headers; + capabilities.push({ MCP: mcp }); + } + + const configPath = path.join(workspace, ".pydantic-ai", "agent.json"); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, JSON.stringify({ model: "openai-chat:gpt-5", capabilities }, null, 2), { mode: 0o600 }); + fs.chmodSync(configPath, 0o600); + console.log(`Wrote ${capabilities.length} MCP server(s) to ${configPath}`); log-parser: | function parseLog(logContent) { const lines = logContent.split("\n"); @@ -118,9 +226,8 @@ engine: diff --git a/.github/workflows/smoke-pydantic.lock.yml b/.github/workflows/smoke-pydantic.lock.yml index 9eccbe7aa24..1ba1b5924e4 100644 --- a/.github/workflows/smoke-pydantic.lock.yml +++ b/.github/workflows/smoke-pydantic.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3b81121d5279867c4821ef514fe9986af629622dcac560871defc0fcbc07f2a1","body_hash":"5d4605d11ff843d9619387c94b8f6fcec25c492df7ff7fe7c8f157f5e30c767f","strict":true,"agent_id":"pydantic-ai","agent_model":"copilot/claude-sonnet-4-5","engine_versions":{"pydantic-ai":"0.1.0"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","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":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-python","sha":"5fda3b95a4ea91299a34e894583c3862153e4b97","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"astral-sh/setup-uv","sha":"c771a70e6277c0a99b617c7a806ffedaca235ff9","version":"v9.0.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}],"has_pull_request":true} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f0aa608f8ed57b7151bf3f9d6267eca83d4682b45e388d3cf1d4aed083111cae","body_hash":"8980e76a3f3c0797925d9bf85481c0d32a0045424e79d4ec205f164bb8b3af33","strict":true,"agent_id":"pydantic-ai","agent_model":"copilot/claude-sonnet-4-5","engine_versions":{"pydantic-ai":"2.26.0"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","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":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/setup-python","sha":"5fda3b95a4ea91299a34e894583c3862153e4b97","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}],"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 # # ___ _ _ @@ -42,9 +42,9 @@ # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4 @@ -134,7 +134,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Pydantic AI" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-pydantic.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "0.1.0" + GH_AW_INFO_VERSION: "2.26.0" GH_AW_INFO_ENGINE_ID: "pydantic-ai" - name: Generate agentic run info id: generate_aw_info @@ -142,8 +142,8 @@ jobs: GH_AW_INFO_ENGINE_ID: "pydantic-ai" GH_AW_INFO_ENGINE_NAME: "Pydantic AI" GH_AW_INFO_MODEL: "copilot/claude-sonnet-4-5" - GH_AW_INFO_VERSION: "0.1.0" - GH_AW_INFO_AGENT_VERSION: "0.1.0" + GH_AW_INFO_VERSION: "2.26.0" + GH_AW_INFO_AGENT_VERSION: "2.26.0" GH_AW_INFO_WORKFLOW_NAME: "Smoke Pydantic AI" GH_AW_INFO_EXPERIMENTAL: "true" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "false" @@ -460,7 +460,7 @@ jobs: GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 - GH_AW_ENGINE_VERSION: "0.1.0" + GH_AW_ENGINE_VERSION: "2.26.0" GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} @@ -499,7 +499,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Pydantic AI" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-pydantic.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "0.1.0" + GH_AW_INFO_VERSION: "2.26.0" GH_AW_INFO_ENGINE_ID: "pydantic-ai" - name: Set runtime paths id: set-runtime-paths @@ -517,11 +517,6 @@ jobs: uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12' - - name: Setup uv - # zizmor: ignore[github_action_from_unverified_creator_used] - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - with: - version: 'latest' - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise @@ -553,6 +548,13 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.44 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -578,8 +580,10 @@ jobs: env: GH_AW_SKILL_DIR: ".pydantic-ai/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - - name: Predownload Pydantic AI CLI - run: uv run pai --version + - name: Preinstall Pydantic AI CLI + run: | + python3 -m pip install --quiet --user --disable-pip-version-check "pydantic-ai==$GH_AW_ENGINE_VERSION" + "$HOME/.local/bin/pai" --version - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7 ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627 ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8 ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 @@ -780,6 +784,67 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); await main(); + - name: Write Pydantic AI MCP config adapter script + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_MCP_CONFIG_ADAPTER_SCRIPT_7e1a4d2c_EOF' > "${RUNNER_TEMP}/gh-aw/actions/pydantic-ai_mcp_config_adapter.cjs" + // Converts the MCP gateway's standard HTTP-based configuration into a + // Pydantic AI agent spec (https://ai.pydantic.dev), which is the only way + // the `pai` CLI can be given MCP servers: each gateway server becomes an + // `MCP` capability entry and the spec file is passed via `pai -a `. + // An agent spec must declare a `model`, but the harness always appends + // `-m openai-chat:` when the workflow declares a model, which takes + // precedence. The value below is only a valid-by-construction fallback for + // workflows that do not declare a model. + const fs = require("fs"); + const path = require("path"); + + const requireEnvVar = name => { + const value = process.env[name]; + if (!value) throw new Error(`${name} environment variable is required`); + return value; + }; + + const gatewayOutputPath = requireEnvVar("MCP_GATEWAY_OUTPUT"); + const workspace = requireEnvVar("GITHUB_WORKSPACE"); + const gatewayDomain = process.env.MCP_GATEWAY_DOMAIN || "host.docker.internal"; + const gatewayPort = requireEnvVar("MCP_GATEWAY_PORT"); + const gatewayURL = `http://${gatewayDomain}:${gatewayPort}`; + + let cliServers; + try { + cliServers = new Set(JSON.parse(process.env.GH_AW_MCP_CLI_SERVERS || "[]")); + } catch (error) { + throw new Error(`Failed to parse GH_AW_MCP_CLI_SERVERS: ${error instanceof Error ? error.message : String(error)}`); + } + + const gatewayOutput = JSON.parse(fs.readFileSync(gatewayOutputPath, "utf8")); + const rawServers = gatewayOutput.mcpServers; + const servers = rawServers && typeof rawServers === "object" && !Array.isArray(rawServers) ? rawServers : {}; + + const capabilities = []; + for (const [name, entry] of Object.entries(servers)) { + if (cliServers.has(name) || !entry || typeof entry !== "object") continue; + if (typeof entry.url !== "string") { + console.log(`Skipping MCP server ${name}: the Pydantic AI CLI only supports HTTP MCP servers`); + continue; + } + const mcp = { + id: name, + url: entry.url.replace(/^http:\/\/[^/]+\/mcp\//, `${gatewayURL}/mcp/`), + }; + if (entry.headers && typeof entry.headers === "object") mcp.headers = entry.headers; + capabilities.push({ MCP: mcp }); + } + + const configPath = path.join(workspace, ".pydantic-ai", "agent.json"); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, JSON.stringify({ model: "openai-chat:gpt-5", capabilities }, null, 2), { mode: 0o600 }); + fs.chmodSync(configPath, 0o600); + console.log(`Wrote ${capabilities.length} MCP server(s) to ${configPath}`); + + GHAW_MCP_CONFIG_ADAPTER_SCRIPT_7e1a4d2c_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/pydantic-ai_mcp_config_adapter.cjs" - name: Start MCP Gateway id: start-mcp-gateway env: @@ -809,6 +874,7 @@ jobs: export DEBUG="*" export GH_AW_ENGINE="pydantic-ai" + export GH_AW_MCP_CONFIG_ADAPTER="pydantic-ai_mcp_config_adapter.cjs" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" @@ -897,6 +963,64 @@ jobs: id: pre_agent_audit continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Write Pydantic AI harness script + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF' > "${RUNNER_TEMP}/gh-aw/actions/pydantic-ai_harness.cjs" + const { spawnSync } = require("child_process"); + const { existsSync, readFileSync } = require("fs"); + const { join } = require("path"); + const { homedir } = require("os"); + + const [command, ...commandArgs] = process.argv.slice(2); + + const promptFile = process.env.GH_AW_PROMPT; + if (!promptFile) { + throw new Error("GH_AW_PROMPT is not set"); + } + const workspace = process.env.GITHUB_WORKSPACE; + if (!workspace) { + throw new Error("GITHUB_WORKSPACE is not set"); + } + + const localBin = join(homedir(), ".local", "bin"); + const env = { ...process.env, PATH: `${localBin}:${process.env.PATH || ""}` }; + delete env.COPILOT_GITHUB_TOKEN; + // The AWF api-proxy selects the upstream provider by the port the client connects + // to and injects the real credentials itself, ignoring the inbound key. AWF rewrites + // OPENAI_BASE_URL inside the sandbox to the proxy's OpenAI port, which forwards to + // api.openai.com, so `pai` — which is configured only through the environment — is + // pointed at the port that steers to the configured provider instead, the same + // endpoint Aider and OpenCode use, with the usual placeholder key. + env.OPENAI_API_KEY = "awf-copilot-proxy"; + env.OPENAI_BASE_URL = "http://172.30.0.30:10002"; + + const args = [...commandArgs]; + // `pai` sends the model name verbatim, minus the `openai-chat:` provider marker + // that selects its OpenAI-compatible client, so the bare model ID reaches the + // api-proxy — which steers to the configured provider by the port it is reached + // on, not by a prefix in the model name: Copilot rejects `copilot/` with + // `model_not_supported`. The proxy exposes Copilot Claude models under their + // dotted IDs, so `copilot/claude-sonnet-4-5` becomes `claude-sonnet-4.5`. + const model = env.PAI_MODEL?.replace(/^.*\//, "").replace(/^(claude-(?:haiku|sonnet|opus)-\d+)-(\d+)$/, "$1.$2"); + if (model) { + args.push("-m", `openai-chat:${model}`); + } + const agentSpec = join(workspace, ".pydantic-ai", "agent.json"); + if (existsSync(agentSpec)) { + args.push("-a", agentSpec); + } + args.push(readFileSync(promptFile, "utf8")); + + const result = spawnSync(command, args, { cwd: workspace, encoding: "utf8", env }); + process.stdout.write(result.stdout || ""); + process.stderr.write(result.stderr || ""); + if (result.error || result.status !== 0) { + throw new Error(`Pydantic AI execution failed: ${result.error?.message || `exit code ${result.status}`}`); + } + + GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/pydantic-ai_harness.cjs" - name: Write Pydantic AI log parser script if: always() run: | @@ -1017,13 +1141,13 @@ jobs: 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 --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --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}" && uv run pai run "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /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 && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/pydantic-ai_harness.cjs pai' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: + AWF_REFLECT_ENABLED: 1 COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_ENGINE_VERSION: 0.1.0 + GH_AW_ENGINE_VERSION: 2.26.0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_MCP_CONFIG: ${{ runner.temp }}/gh-aw/mcp-config/mcp-servers.json GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GITHUB_COPILOT_BASE_URL: http://host.docker.internal:10002 @@ -1214,7 +1338,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Pydantic AI" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-pydantic.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "0.1.0" + GH_AW_INFO_VERSION: "2.26.0" GH_AW_INFO_ENGINE_ID: "pydantic-ai" - name: Download agent output artifact id: download-agent-output @@ -1481,7 +1605,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Pydantic AI" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-pydantic.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "0.1.0" + GH_AW_INFO_VERSION: "2.26.0" GH_AW_INFO_ENGINE_ID: "pydantic-ai" - name: Download agent output artifact id: download-agent-output @@ -1554,6 +1678,73 @@ jobs: touch /tmp/gh-aw/threat-detection/detection.log rm -f /tmp/gh-aw/step-summary.md touch /tmp/gh-aw/step-summary.md + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.44 + - name: Write Pydantic AI harness script + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF' > "${RUNNER_TEMP}/gh-aw/actions/pydantic-ai_harness.cjs" + const { spawnSync } = require("child_process"); + const { existsSync, readFileSync } = require("fs"); + const { join } = require("path"); + const { homedir } = require("os"); + + const [command, ...commandArgs] = process.argv.slice(2); + + const promptFile = process.env.GH_AW_PROMPT; + if (!promptFile) { + throw new Error("GH_AW_PROMPT is not set"); + } + const workspace = process.env.GITHUB_WORKSPACE; + if (!workspace) { + throw new Error("GITHUB_WORKSPACE is not set"); + } + + const localBin = join(homedir(), ".local", "bin"); + const env = { ...process.env, PATH: `${localBin}:${process.env.PATH || ""}` }; + delete env.COPILOT_GITHUB_TOKEN; + // The AWF api-proxy selects the upstream provider by the port the client connects + // to and injects the real credentials itself, ignoring the inbound key. AWF rewrites + // OPENAI_BASE_URL inside the sandbox to the proxy's OpenAI port, which forwards to + // api.openai.com, so `pai` — which is configured only through the environment — is + // pointed at the port that steers to the configured provider instead, the same + // endpoint Aider and OpenCode use, with the usual placeholder key. + env.OPENAI_API_KEY = "awf-copilot-proxy"; + env.OPENAI_BASE_URL = "http://172.30.0.30:10002"; + + const args = [...commandArgs]; + // `pai` sends the model name verbatim, minus the `openai-chat:` provider marker + // that selects its OpenAI-compatible client, so the bare model ID reaches the + // api-proxy — which steers to the configured provider by the port it is reached + // on, not by a prefix in the model name: Copilot rejects `copilot/` with + // `model_not_supported`. The proxy exposes Copilot Claude models under their + // dotted IDs, so `copilot/claude-sonnet-4-5` becomes `claude-sonnet-4.5`. + const model = env.PAI_MODEL?.replace(/^.*\//, "").replace(/^(claude-(?:haiku|sonnet|opus)-\d+)-(\d+)$/, "$1.$2"); + if (model) { + args.push("-m", `openai-chat:${model}`); + } + const agentSpec = join(workspace, ".pydantic-ai", "agent.json"); + if (existsSync(agentSpec)) { + args.push("-a", agentSpec); + } + args.push(readFileSync(promptFile, "utf8")); + + const result = spawnSync(command, args, { cwd: workspace, encoding: "utf8", env }); + process.stdout.write(result.stdout || ""); + process.stderr.write(result.stderr || ""); + if (result.error || result.status !== 0) { + throw new Error(`Pydantic AI execution failed: ${result.error?.message || `exit code ${result.status}`}`); + } + + GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/pydantic-ai_harness.cjs" - name: Write Pydantic AI log parser script if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1658,7 +1849,7 @@ jobs: 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\":\"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/github/gh-aw-firewall/releases/download/v0.27.44/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.github.com\",\"api.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"objects.githubusercontent.com\",\"raw.githubusercontent.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*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*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"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\":[\"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*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"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\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"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-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"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.44,squid=sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627,agent=sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4,api-proxy=sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7,cli-proxy=sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff\"},\"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\":\"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/github/gh-aw-firewall/releases/download/v0.27.44/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.github.com\",\"api.githubcopilot.com\",\"files.pythonhosted.org\",\"github.com\",\"host.docker.internal\",\"objects.githubusercontent.com\",\"pypi.org\",\"raw.githubusercontent.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*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*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"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\":[\"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*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"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\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"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-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"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.44,squid=sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627,agent=sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4,api-proxy=sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7,cli-proxy=sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff\"},\"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="" @@ -1679,11 +1870,12 @@ jobs: 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 --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --log-level info --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}" && uv run pai run "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /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 && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/pydantic-ai_harness.cjs pai' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: GITHUB_STEP_SUMMARY: /tmp/gh-aw/step-summary.md + AWF_REFLECT_ENABLED: 1 COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_ENGINE_VERSION: 0.1.0 + GH_AW_ENGINE_VERSION: 2.26.0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1800,7 +1992,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Pydantic AI" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-pydantic.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "0.1.0" + GH_AW_INFO_VERSION: "2.26.0" GH_AW_INFO_ENGINE_ID: "pydantic-ai" - name: Check command position id: check_command_position @@ -1849,7 +2041,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "pydantic-ai" GH_AW_ENGINE_MODEL: "copilot/claude-sonnet-4-5" - GH_AW_ENGINE_VERSION: "0.1.0" + GH_AW_ENGINE_VERSION: "2.26.0" GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} @@ -1893,7 +2085,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Pydantic AI" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-pydantic.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "0.1.0" + GH_AW_INFO_VERSION: "2.26.0" GH_AW_INFO_ENGINE_ID: "pydantic-ai" - name: Download agent output artifact id: download-agent-output diff --git a/.github/workflows/smoke-pydantic.md b/.github/workflows/smoke-pydantic.md index 3076e8c2e3f..69751ab06b7 100644 --- a/.github/workflows/smoke-pydantic.md +++ b/.github/workflows/smoke-pydantic.md @@ -61,9 +61,8 @@ sandbox: ## Test Requirements -1. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-pydantic-${{ github.run_id }}.txt` with content "Smoke test passed for Pydantic AI" (create the directory if it doesn't exist) -2. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) -3. **Repository Access Testing**: Run `git log --oneline -1` in the repository checkout and confirm a commit is reported +1. **Model Connectivity Testing**: Answer the question "What is 2 + 2?" in a single short line. +2. **MCP Tool Testing**: Confirm that the `safeoutputs` MCP tools are available to you. ## Output diff --git a/pkg/workflow/behavior_defined_engine.go b/pkg/workflow/behavior_defined_engine.go index bee3735c76d..a623c920358 100644 --- a/pkg/workflow/behavior_defined_engine.go +++ b/pkg/workflow/behavior_defined_engine.go @@ -179,14 +179,19 @@ func (e *BehaviorDefinedEngine) GetInstallationSteps(workflowData *WorkflowData) // is declared for the engine's CLI itself. if behavior.Installation == nil { if behavior.HarnessScript == "" { - return nil + // Engines that install their CLI through `pre-agent-steps` (e.g. Pydantic AI) + // declare no installation block at all, but the agent still runs inside the + // firewall sandbox, so the AWF binary must be installed. + return BuildNpmEngineInstallStepsWithAWF(nil, workflowData) } return BuildNpmEngineInstallStepsWithAWF([]GitHubActionStep{GenerateNodeJsSetupStep()}, workflowData) } install := behavior.Installation if install.PackageManager != "npm" { - return nil + // Non-npm installations are performed by the engine's own steps, but the AWF + // binary is still required to run the agent inside the firewall sandbox. + return BuildNpmEngineInstallStepsWithAWF(nil, workflowData) } version := install.Version if workflowData != nil && workflowData.EngineConfig != nil && workflowData.EngineConfig.Version != "" {