diff --git a/.github/workflows/smoke-codex-firewall.lock.yml b/.github/workflows/smoke-codex-firewall.lock.yml index 417ab70cba..ade1ca7052 100644 --- a/.github/workflows/smoke-codex-firewall.lock.yml +++ b/.github/workflows/smoke-codex-firewall.lock.yml @@ -655,7 +655,7 @@ jobs: node-version: '24' package-manager-cache: false - name: Install Codex - run: npm install -g @openai/codex@0.73.0 + run: npm install -g @openai/codex@0.75.0 - name: Install awf binary run: | echo "Installing awf from release: v0.7.0" @@ -693,7 +693,7 @@ jobs: done } - docker_pull_with_retry ghcr.io/github/github-mcp-server:v0.25.0 + docker_pull_with_retry ghcr.io/github/github-mcp-server:v0.26.3 - name: Write Safe Outputs Config run: | mkdir -p /tmp/gh-aw/safeoutputs @@ -2334,7 +2334,7 @@ jobs: "GITHUB_READ_ONLY=1", "-e", "GITHUB_TOOLSETS=context,repos,issues,pull_requests", - "ghcr.io/github/github-mcp-server:v0.25.0" + "ghcr.io/github/github-mcp-server:v0.26.3" ] env_vars = ["GITHUB_PERSONAL_ACCESS_TOKEN"] @@ -2357,7 +2357,7 @@ jobs: engine_name: "Codex", model: process.env.GH_AW_MODEL_AGENT_CODEX || "", version: "", - agent_version: "0.73.0", + agent_version: "0.75.0", workflow_name: "Smoke Codex Firewall", experimental: true, supports_tools_allowlist: true, @@ -2374,7 +2374,7 @@ jobs: network_mode: "defaults", allowed_domains: ["defaults","github"], firewall_enabled: true, - firewall_version: "", + awf_version: "v0.7.0", steps: { firewall: "squid" }, @@ -2421,7 +2421,7 @@ jobs: '|----------|-------|\n' + `| Mode | ${awInfo.network_mode || 'defaults'} |\n` + `| Firewall | ${awInfo.firewall_enabled ? '✅ Enabled' : '❌ Disabled'} |\n` + - `| Firewall Version | ${awInfo.firewall_version || '(latest)'} |\n` + + `| Firewall Version | ${awInfo.awf_version || '(latest)'} |\n` + '\n' + (networkDetails ? `##### Allowed Domains\n${networkDetails}\n` : '') + ''; @@ -2446,7 +2446,7 @@ jobs: This workflow validates that the Codex engine works correctly with AWF (Application-level Firewall) network sandboxing enabled. - 1. **OpenAI Domain Access**: The workflow should successfully connect to OpenAI APIs (api.openai.com, openai.com) which are in the default allowed domains + 1. **OpenAI Domain Access**: Test that direct curl access to OpenAI APIs (api.openai.com, openai.com) is BLOCKED by the firewall - the Codex CLI itself can access OpenAI (it adds these domains automatically), but raw curl commands should fail since OpenAI is not in the `defaults` or `github` network ecosystems 2. **GitHub MCP Testing**: Review the last 2 merged pull requests in __GH_AW_GITHUB_REPOSITORY__ to verify GitHub MCP server works through the firewall 3. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-codex-firewall-__GH_AW_GITHUB_RUN_ID__.txt` with content "Firewall smoke test passed for Codex at $(date)" 4. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) @@ -5766,9 +5766,168 @@ jobs: validAllowedRequests += stats.allowed; validDeniedRequests += stats.denied; } - let summary = "### 🔥 Firewall Activity\n\n"; + let summary = ""; + summary += "
\n"; + summary += `sandbox agent: ${totalRequests} request${totalRequests !== 1 ? "s" : ""} | `; + summary += `${validAllowedRequests} allowed | `; + summary += `${validDeniedRequests} blocked | `; + summary += `${uniqueDomainCount} unique domain${uniqueDomainCount !== 1 ? "s" : ""}\n\n`; + if (uniqueDomainCount > 0) { + summary += "| Domain | Allowed | Denied |\n"; + summary += "|--------|---------|--------|\n"; + for (const domain of validDomains) { + const stats = requestsByDomain.get(domain); + summary += `| ${domain} | ${stats.allowed} | ${stats.denied} |\n`; + } + } else { + summary += "No firewall activity detected.\n"; + } + summary += "\n
\n\n"; + return summary; + } + const isDirectExecution = typeof module === "undefined" || (typeof require !== "undefined" && typeof require.main !== "undefined" && require.main === module); + if (isDirectExecution) { + main(); + } + - name: Upload Firewall Logs + if: always() + continue-on-error: true + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + with: + name: firewall-logs-smoke-codex-firewall + path: /tmp/gh-aw/sandbox/firewall/logs/ + if-no-files-found: ignore + - name: Parse firewall logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + function sanitizeWorkflowName(name) { + return name + .toLowerCase() + .replace(/[:\\/\s]/g, "-") + .replace(/[^a-z0-9._-]/g, "-"); + } + function main() { + const fs = require("fs"); + const path = require("path"); + try { + const squidLogsDir = `/tmp/gh-aw/sandbox/firewall/logs/`; + if (!fs.existsSync(squidLogsDir)) { + core.info(`No firewall logs directory found at: ${squidLogsDir}`); + return; + } + const files = fs.readdirSync(squidLogsDir).filter(file => file.endsWith(".log")); + if (files.length === 0) { + core.info(`No firewall log files found in: ${squidLogsDir}`); + return; + } + core.info(`Found ${files.length} firewall log file(s)`); + let totalRequests = 0; + let allowedRequests = 0; + let deniedRequests = 0; + const allowedDomains = new Set(); + const deniedDomains = new Set(); + const requestsByDomain = new Map(); + for (const file of files) { + const filePath = path.join(squidLogsDir, file); + core.info(`Parsing firewall log: ${file}`); + const content = fs.readFileSync(filePath, "utf8"); + const lines = content.split("\n").filter(line => line.trim()); + for (const line of lines) { + const entry = parseFirewallLogLine(line); + if (!entry) { + continue; + } + totalRequests++; + const isAllowed = isRequestAllowed(entry.decision, entry.status); + if (isAllowed) { + allowedRequests++; + allowedDomains.add(entry.domain); + } else { + deniedRequests++; + deniedDomains.add(entry.domain); + } + if (!requestsByDomain.has(entry.domain)) { + requestsByDomain.set(entry.domain, { allowed: 0, denied: 0 }); + } + const domainStats = requestsByDomain.get(entry.domain); + if (isAllowed) { + domainStats.allowed++; + } else { + domainStats.denied++; + } + } + } + const summary = generateFirewallSummary({ + totalRequests, + allowedRequests, + deniedRequests, + allowedDomains: Array.from(allowedDomains).sort(), + deniedDomains: Array.from(deniedDomains).sort(), + requestsByDomain, + }); + core.summary.addRaw(summary).write(); + core.info("Firewall log summary generated successfully"); + } catch (error) { + core.setFailed(error instanceof Error ? error : String(error)); + } + } + function parseFirewallLogLine(line) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) { + return null; + } + const fields = trimmed.match(/(?:[^\s"]+|"[^"]*")+/g); + if (!fields || fields.length < 10) { + return null; + } + const timestamp = fields[0]; + if (!/^\d+(\.\d+)?$/.test(timestamp)) { + return null; + } + return { + timestamp, + clientIpPort: fields[1], + domain: fields[2], + destIpPort: fields[3], + proto: fields[4], + method: fields[5], + status: fields[6], + decision: fields[7], + url: fields[8], + userAgent: fields[9]?.replace(/^"|"$/g, "") || "-", + }; + } + function isRequestAllowed(decision, status) { + const statusCode = parseInt(status, 10); + if (statusCode === 200 || statusCode === 206 || statusCode === 304) { + return true; + } + if (decision.includes("TCP_TUNNEL") || decision.includes("TCP_HIT") || decision.includes("TCP_MISS")) { + return true; + } + if (decision.includes("NONE_NONE") || decision.includes("TCP_DENIED") || statusCode === 403 || statusCode === 407) { + return false; + } + return false; + } + function generateFirewallSummary(analysis) { + const { totalRequests, requestsByDomain } = analysis; + const validDomains = Array.from(requestsByDomain.keys()) + .filter(domain => domain !== "-") + .sort(); + const uniqueDomainCount = validDomains.length; + let validAllowedRequests = 0; + let validDeniedRequests = 0; + for (const domain of validDomains) { + const stats = requestsByDomain.get(domain); + validAllowedRequests += stats.allowed; + validDeniedRequests += stats.denied; + } + let summary = ""; summary += "
\n"; - summary += `📊 ${totalRequests} request${totalRequests !== 1 ? "s" : ""} | `; + summary += `sandbox agent: ${totalRequests} request${totalRequests !== 1 ? "s" : ""} | `; summary += `${validAllowedRequests} allowed | `; summary += `${validDeniedRequests} blocked | `; summary += `${uniqueDomainCount} unique domain${uniqueDomainCount !== 1 ? "s" : ""}\n\n`; @@ -6716,7 +6875,7 @@ jobs: node-version: '24' package-manager-cache: false - name: Install Codex - run: npm install -g @openai/codex@0.73.0 + run: npm install -g @openai/codex@0.75.0 - name: Run Codex run: | set -o pipefail diff --git a/.github/workflows/smoke-codex-firewall.md b/.github/workflows/smoke-codex-firewall.md index 9594a403a3..cca5656da5 100644 --- a/.github/workflows/smoke-codex-firewall.md +++ b/.github/workflows/smoke-codex-firewall.md @@ -47,7 +47,7 @@ tools: This workflow validates that the Codex engine works correctly with AWF (Application-level Firewall) network sandboxing enabled. -1. **OpenAI Domain Access**: The workflow should successfully connect to OpenAI APIs (api.openai.com, openai.com) which are in the default allowed domains +1. **OpenAI Domain Access**: Test that direct curl access to OpenAI APIs (api.openai.com, openai.com) is BLOCKED by the firewall - the Codex CLI itself can access OpenAI (it adds these domains automatically), but raw curl commands should fail since OpenAI is not in the `defaults` or `github` network ecosystems 2. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }} to verify GitHub MCP server works through the firewall 3. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-codex-firewall-${{ github.run_id }}.txt` with content "Firewall smoke test passed for Codex at $(date)" 4. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back)