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)