diff --git a/.changeset/patch-add-codex-harness-retry.md b/.changeset/patch-add-codex-harness-retry.md new file mode 100644 index 00000000000..908806939e5 --- /dev/null +++ b/.changeset/patch-add-codex-harness-retry.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Added the default Codex engine harness with retry handling for transient execution failures. diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml index fee86d24a63..191d8f03b0e 100644 --- a/.github/workflows/ai-moderator.lock.yml +++ b/.github/workflows/ai-moderator.lock.yml @@ -820,7 +820,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","172.30.0.1","api.openai.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","openai.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","s.symcb.com","s.symcd.com","security.ubuntu.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index d6b49703870..e07f3f522e9 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -843,7 +843,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","172.30.0.1","api.npms.io","api.openai.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","bun.sh","cdn.jsdelivr.net","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","deb.nodesource.com","deno.land","docs.github.com","esm.sh","get.pnpm.io","github-cloud.githubusercontent.com","github-cloud.s3.amazonaws.com","github.blog","github.com","github.githubassets.com","go.dev","golang.org","googleapis.deno.dev","googlechromelabs.github.io","goproxy.io","host.docker.internal","json-schema.org","json.schemastore.org","jsr.io","keyserver.ubuntu.com","lfs.github.com","nodejs.org","npm.pkg.github.com","npmjs.com","npmjs.org","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","openai.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","pkg.go.dev","ppa.launchpad.net","proxy.golang.org","raw.githubusercontent.com","registry.bower.io","registry.npmjs.com","registry.npmjs.org","registry.yarnpkg.com","repo.yarnpkg.com","s.symcb.com","s.symcd.com","security.ubuntu.com","skimdb.npmjs.com","storage.googleapis.com","sum.golang.org","telemetry.vercel.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com","www.npmjs.com","www.npmjs.org","yarnpkg.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config diff --git a/.github/workflows/codex-github-remote-mcp-test.lock.yml b/.github/workflows/codex-github-remote-mcp-test.lock.yml index b2fedfa7114..bb6cb077999 100644 --- a/.github/workflows/codex-github-remote-mcp-test.lock.yml +++ b/.github/workflows/codex-github-remote-mcp-test.lock.yml @@ -678,7 +678,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["172.30.0.1","api.githubcopilot.com","api.openai.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.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","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.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","openai.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","s.symcb.com","s.symcd.com","security.ubuntu.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index b8838c73f86..82963aed017 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -846,7 +846,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["172.30.0.1","api.openai.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.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","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.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","openai.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","s.symcb.com","s.symcd.com","security.ubuntu.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config @@ -1305,18 +1305,18 @@ jobs: DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e CODEX_HOME -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.3' - cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_7b8a1e5eb8c32a09_EOF + cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_575bf5ec56eef5a6_EOF [history] persistence = "none" [shell_environment_policy] inherit = "core" include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"] - GH_AW_MCP_CONFIG_7b8a1e5eb8c32a09_EOF + GH_AW_MCP_CONFIG_575bf5ec56eef5a6_EOF # Generate JSON config for MCP gateway GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_11fc14f9caadd3af_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_81f23a43e8838afb_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { }, @@ -1327,11 +1327,11 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_11fc14f9caadd3af_EOF + GH_AW_MCP_CONFIG_81f23a43e8838afb_EOF # Sync converter output to writable CODEX_HOME for Codex mkdir -p /tmp/gh-aw/mcp-config - cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_454890aa5d189859_EOF + cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_2fa8249a910dcb68_EOF model_provider = "openai-proxy" [model_providers.openai-proxy] name = "OpenAI AWF proxy" @@ -1341,7 +1341,7 @@ jobs: [shell_environment_policy] inherit = "core" include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"] - GH_AW_CODEX_SHELL_POLICY_454890aa5d189859_EOF + GH_AW_CODEX_SHELL_POLICY_2fa8249a910dcb68_EOF awk ' BEGIN { skip_openai_proxy = 0 } /^[[:space:]]*model_provider[[:space:]]*=/ { next } @@ -1364,7 +1364,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["172.30.0.1","api.openai.com","host.docker.internal","openai.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_DETECTION_CODEX:+-c model="$GH_AW_MODEL_DETECTION_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_DETECTION_CODEX:+-c model="$GH_AW_MODEL_DETECTION_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config diff --git a/.github/workflows/grumpy-reviewer.lock.yml b/.github/workflows/grumpy-reviewer.lock.yml index 7c93d3e33ce..284b9e3a2cc 100644 --- a/.github/workflows/grumpy-reviewer.lock.yml +++ b/.github/workflows/grumpy-reviewer.lock.yml @@ -870,7 +870,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["172.30.0.1","api.openai.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.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","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.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","openai.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","s.symcb.com","s.symcd.com","security.ubuntu.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config @@ -1364,18 +1364,18 @@ jobs: DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e CODEX_HOME -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.3' - cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_cfeeba44171e9146_EOF + cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_ae926b35c7461f22_EOF [history] persistence = "none" [shell_environment_policy] inherit = "core" include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"] - GH_AW_MCP_CONFIG_cfeeba44171e9146_EOF + GH_AW_MCP_CONFIG_ae926b35c7461f22_EOF # Generate JSON config for MCP gateway GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_3eaf7b89796608fc_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_2b7b3b712b94aba2_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { }, @@ -1386,11 +1386,11 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_3eaf7b89796608fc_EOF + GH_AW_MCP_CONFIG_2b7b3b712b94aba2_EOF # Sync converter output to writable CODEX_HOME for Codex mkdir -p /tmp/gh-aw/mcp-config - cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_204224df9a0fe864_EOF + cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_db26a09248e499c9_EOF model_provider = "openai-proxy" [model_providers.openai-proxy] name = "OpenAI AWF proxy" @@ -1400,7 +1400,7 @@ jobs: [shell_environment_policy] inherit = "core" include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"] - GH_AW_CODEX_SHELL_POLICY_204224df9a0fe864_EOF + GH_AW_CODEX_SHELL_POLICY_db26a09248e499c9_EOF awk ' BEGIN { skip_openai_proxy = 0 } /^[[:space:]]*model_provider[[:space:]]*=/ { next } @@ -1423,7 +1423,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["172.30.0.1","api.openai.com","host.docker.internal","openai.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_DETECTION_CODEX:+-c model="$GH_AW_MODEL_DETECTION_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_DETECTION_CODEX:+-c model="$GH_AW_MODEL_DETECTION_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml index a3817ad8fb9..e99d6a5a2e2 100644 --- a/.github/workflows/issue-arborist.lock.yml +++ b/.github/workflows/issue-arborist.lock.yml @@ -900,7 +900,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","172.30.0.1","api.openai.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","openai.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","s.symcb.com","s.symcd.com","security.ubuntu.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config @@ -1374,18 +1374,18 @@ jobs: DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e CODEX_HOME -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.3' - cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_aac03c981232e68f_EOF + cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_e3158f4600a557a2_EOF [history] persistence = "none" [shell_environment_policy] inherit = "core" include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"] - GH_AW_MCP_CONFIG_aac03c981232e68f_EOF + GH_AW_MCP_CONFIG_e3158f4600a557a2_EOF # Generate JSON config for MCP gateway GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_85d67fb07599e7d1_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_5714fe0220d059be_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { }, @@ -1396,11 +1396,11 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_85d67fb07599e7d1_EOF + GH_AW_MCP_CONFIG_5714fe0220d059be_EOF # Sync converter output to writable CODEX_HOME for Codex mkdir -p /tmp/gh-aw/mcp-config - cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_f2bb21cb4621120f_EOF + cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_39aa2c61691cf3c8_EOF model_provider = "openai-proxy" [model_providers.openai-proxy] name = "OpenAI AWF proxy" @@ -1410,7 +1410,7 @@ jobs: [shell_environment_policy] inherit = "core" include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"] - GH_AW_CODEX_SHELL_POLICY_f2bb21cb4621120f_EOF + GH_AW_CODEX_SHELL_POLICY_39aa2c61691cf3c8_EOF awk ' BEGIN { skip_openai_proxy = 0 } /^[[:space:]]*model_provider[[:space:]]*=/ { next } @@ -1433,7 +1433,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["172.30.0.1","api.openai.com","host.docker.internal","openai.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_DETECTION_CODEX:+-c model="$GH_AW_MODEL_DETECTION_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_DETECTION_CODEX:+-c model="$GH_AW_MODEL_DETECTION_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config diff --git a/.github/workflows/schema-feature-coverage.lock.yml b/.github/workflows/schema-feature-coverage.lock.yml index c3535bb0d54..7955415dbce 100644 --- a/.github/workflows/schema-feature-coverage.lock.yml +++ b/.github/workflows/schema-feature-coverage.lock.yml @@ -773,7 +773,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","172.30.0.1","api.githubcopilot.com","api.openai.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","openai.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","s.symcb.com","s.symcd.com","security.ubuntu.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config @@ -1233,18 +1233,18 @@ jobs: DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e CODEX_HOME -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.3' - cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_144fe7d2ab818483_EOF + cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_f64eaac23d3ffa00_EOF [history] persistence = "none" [shell_environment_policy] inherit = "core" include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"] - GH_AW_MCP_CONFIG_144fe7d2ab818483_EOF + GH_AW_MCP_CONFIG_f64eaac23d3ffa00_EOF # Generate JSON config for MCP gateway GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_b0dede9b5a175347_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_e8b3f85bbb8c9cdd_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { }, @@ -1255,11 +1255,11 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_b0dede9b5a175347_EOF + GH_AW_MCP_CONFIG_e8b3f85bbb8c9cdd_EOF # Sync converter output to writable CODEX_HOME for Codex mkdir -p /tmp/gh-aw/mcp-config - cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_609ba7748062a654_EOF + cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_e50ff28c1e5b3621_EOF model_provider = "openai-proxy" [model_providers.openai-proxy] name = "OpenAI AWF proxy" @@ -1269,7 +1269,7 @@ jobs: [shell_environment_policy] inherit = "core" include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"] - GH_AW_CODEX_SHELL_POLICY_609ba7748062a654_EOF + GH_AW_CODEX_SHELL_POLICY_e50ff28c1e5b3621_EOF awk ' BEGIN { skip_openai_proxy = 0 } /^[[:space:]]*model_provider[[:space:]]*=/ { next } @@ -1292,7 +1292,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.35/awf-config.schema.json","network":{"allowDomains":["172.30.0.1","api.openai.com","host.docker.internal","openai.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.35"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 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" --env-all --exclude-env CODEX_API_KEY --exclude-env OPENAI_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_DETECTION_CODEX:+-c model="$GH_AW_MODEL_DETECTION_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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 || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex ${GH_AW_MODEL_DETECTION_CODEX:+-c model="$GH_AW_MODEL_DETECTION_CODEX" }exec -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config diff --git a/actions/setup/js/codex_harness.cjs b/actions/setup/js/codex_harness.cjs new file mode 100644 index 00000000000..2cd2d5daf9e --- /dev/null +++ b/actions/setup/js/codex_harness.cjs @@ -0,0 +1,247 @@ +// @ts-check + +/** + * OpenAI Codex CLI Harness with Retry Logic + * + * Wraps the OpenAI Codex CLI command with retry logic for failures that occur after the + * session has been partially executed. Passes all arguments to the codex subprocess, + * transparently forwarding stdin/stdout/stderr. + * + * Retry policy: + * - If the process produced any output (hasOutput) and exits with a non-zero code, the + * session is considered partially executed. The driver retries with a fresh run + * because Codex does not support a --continue-style session resumption. + * - Rate-limit errors (HTTP 429 / "rate_limit_exceeded") and server errors (HTTP 500, + * 503) are well-known transient failure modes and are logged explicitly, but + * any partial-execution failure is retried — not just those specific errors. + * - If the process produced no output (failed to start / auth error before any work), the + * driver does not retry because there is nothing to resume. + * - Retries use exponential backoff: 5s → 10s → 20s (capped at 60s). + * - Maximum 3 retry attempts after the initial run. + * + * Prompt handling: + * - The harness expects a `--prompt-file ` argument in the args list. + * - It reads the file and appends the content as the last positional argument, which is + * where the Codex CLI (`codex exec`) expects the prompt. + * - The `--prompt-file` flag is a harness-only argument and is not forwarded to codex. + * + * Usage: node codex_harness.cjs [args...] + * Example: node codex_harness.cjs codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt + */ + +"use strict"; + +const fs = require("fs"); +const { runProcess, formatDuration, sleep } = require("./process_runner.cjs"); +const { + AWF_API_PROXY_REFLECT_URL, + AWF_REFLECT_OUTPUT_PATH, + AWF_REFLECT_TIMEOUT_MS, + AWF_MODELS_URL_TIMEOUT_MS, + GEMINI_MODEL_NAME_PREFIX, + enrichReflectModels, + extractModelIds, + fetchAWFReflect, + fetchModelsFromUrl, +} = require("./awf_reflect.cjs"); + +// Maximum number of retry attempts after the initial run +const MAX_RETRIES = 3; +// Initial delay in milliseconds before the first retry +const INITIAL_DELAY_MS = 5000; +// Multiplier applied to delay after each retry +const BACKOFF_MULTIPLIER = 2; +// Maximum delay cap in milliseconds +const MAX_DELAY_MS = 60000; + +// Pattern to detect OpenAI rate-limit errors (HTTP 429). +// Matches "rate_limit_exceeded" from the OpenAI error type field and the "429" status code +// that Codex emits when the API rate limit is hit. +const RATE_LIMIT_ERROR_PATTERN = /rate_limit_exceeded|429 Too Many Requests|RateLimitError/i; + +// Pattern to detect OpenAI server-side errors (HTTP 500, 503). +// These are transient infrastructure failures that may resolve on retry. +const SERVER_ERROR_PATTERN = /InternalServerError|ServiceUnavailableError|500 Internal Server Error|503 Service Unavailable/i; + +/** + * Emit a timestamped diagnostic log line to stderr. + * All driver messages are prefixed with "[codex-harness]" so they are easy to + * grep out of the combined agent-stdio.log. + * @param {string} message + */ +function log(message) { + const ts = new Date().toISOString(); + process.stderr.write(`[codex-harness] ${ts} ${message}\n`); +} + +/** + * Determines if the collected output contains an OpenAI rate-limit error. + * @param {string} output - Collected stdout+stderr from the process + * @returns {boolean} + */ +function isRateLimitError(output) { + return RATE_LIMIT_ERROR_PATTERN.test(output); +} + +/** + * Determines if the collected output contains an OpenAI server error. + * @param {string} output - Collected stdout+stderr from the process + * @returns {boolean} + */ +function isServerError(output) { + return SERVER_ERROR_PATTERN.test(output); +} + +/** + * Resolve --prompt-file arguments for the Codex run. + * Strips the --prompt-file pair from args and appends the file content + * as the last positional argument, which is where `codex exec` expects the prompt. + * + * @param {string[]} args + * @returns {string[]} Args with --prompt-file resolved to inline prompt content + */ +function resolveCodexPromptFileArgs(args) { + /** @type {string[]} */ + const filteredArgs = []; + /** @type {string|null} */ + let promptContent = null; + + for (let i = 0; i < args.length; i++) { + if (args[i] !== "--prompt-file") { + filteredArgs.push(args[i]); + continue; + } + + if (i + 1 >= args.length) { + log("warning: --prompt-file provided without a path; leaving arguments unchanged"); + filteredArgs.push(args[i]); + continue; + } + + const promptFile = args[i + 1]; + try { + const stat = fs.statSync(promptFile); + log(`resolved --prompt-file: path=${promptFile} size=${stat.size}B`); + promptContent = fs.readFileSync(promptFile, "utf8"); + } catch (error) { + const err = /** @type {Error} */ error; + // An unreadable prompt file means no task instructions can be delivered to Codex. + // Propagate as a fatal error rather than forwarding the harness-only flag to the + // codex subprocess (which would fail with an "unknown option" error). + throw new Error(`--prompt-file '${promptFile}' is not readable: ${err.message}`); + } + i++; // Skip the prompt-file path argument + } + + // Append the prompt content as the last positional argument (codex exec convention). + if (promptContent !== null) { + filteredArgs.push(promptContent); + } + + return filteredArgs; +} + +/** + * Main entry point: run codex with retry logic for transient API failures. + * Codex does not support --continue session resumption, so all retries are fresh runs. + */ +async function main() { + const [, , command, ...args] = process.argv; + + if (!command) { + process.stderr.write("codex-harness: Usage: node codex_harness.cjs [args...]\n"); + process.exit(1); + } + + log(`starting: command=${command} maxRetries=${MAX_RETRIES} initialDelayMs=${INITIAL_DELAY_MS}` + ` backoffMultiplier=${BACKOFF_MULTIPLIER} maxDelayMs=${MAX_DELAY_MS}` + ` nodeVersion=${process.version} platform=${process.platform}`); + + // Resolve the prompt for the initial run (reads --prompt-file content). + // A missing or unreadable prompt file is treated as a fatal startup error. + let resolvedArgs; + try { + resolvedArgs = resolveCodexPromptFileArgs(args); + } catch (err) { + const e = /** @type {Error} */ err; + log(`fatal: ${e.message}`); + process.exit(1); + } + + // Safe arg list for logging: when --prompt-file was present, the last element of + // resolvedArgs is the resolved prompt content. Replace it with a placeholder so that + // task instructions are never written to stderr or captured in agent logs. + const hadPromptFile = args.includes("--prompt-file"); + const safeArgs = hadPromptFile && resolvedArgs.length > 0 ? [...resolvedArgs.slice(0, -1), ""] : resolvedArgs; + + // Fetch AWF API proxy reflection data before running the agent to capture initial proxy state. + // This is best-effort: failures are logged but do not affect the agent run. + await fetchAWFReflect({ logger: log }); + + let delay = INITIAL_DELAY_MS; + let lastExitCode = 1; + const driverStartTime = Date.now(); + + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + // Codex does not support --continue: every retry is a fresh run from scratch. + // Context from the interrupted session is not recoverable, but transient API + // failures (rate limits, server errors) may resolve on the next attempt. + + if (attempt > 0) { + log(`retry ${attempt}/${MAX_RETRIES}: sleeping ${delay}ms before next attempt (fresh run)`); + await sleep(delay); + delay = Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS); + log(`retry ${attempt}/${MAX_RETRIES}: woke up, next delay cap will be ${Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS)}ms`); + } + + const result = await runProcess({ command, args: resolvedArgs, attempt, log, logArgs: safeArgs }); + lastExitCode = result.exitCode; + + // Success — stop retrying + if (result.exitCode === 0) { + log(`success on attempt ${attempt + 1}: totalDuration=${formatDuration(Date.now() - driverStartTime)}`); + lastExitCode = 0; + break; + } + + const isRateLimit = isRateLimitError(result.output); + const isServer = isServerError(result.output); + log(`attempt ${attempt + 1} failed:` + ` exitCode=${result.exitCode}` + ` isRateLimitError=${isRateLimit}` + ` isServerError=${isServer}` + ` hasOutput=${result.hasOutput}` + ` retriesRemaining=${MAX_RETRIES - attempt}`); + + // Retry when the session was partially executed (has output) or on well-known + // transient errors (rate limit, server error) even without output. + const isTransient = isRateLimit || isServer; + if (attempt < MAX_RETRIES && (result.hasOutput || isTransient)) { + const reason = isRateLimit ? "rate_limit_exceeded (transient)" : isServer ? "server_error (transient)" : "partial execution"; + log(`attempt ${attempt + 1}: ${reason} — will retry as fresh run (attempt ${attempt + 2}/${MAX_RETRIES + 1})`); + continue; + } + + if (attempt >= MAX_RETRIES) { + log(`all ${MAX_RETRIES} retries exhausted — giving up (exitCode=${lastExitCode})`); + } else { + log(`attempt ${attempt + 1}: no output produced — not retrying` + ` (possible causes: binary not found, permission denied, auth failure, or silent startup crash)`); + } + + break; + } + + // Fetch AWF API proxy reflection data and persist to disk for post-run step summary. + await fetchAWFReflect({ logger: log }); + + log(`done: exitCode=${lastExitCode} totalDuration=${formatDuration(Date.now() - driverStartTime)}`); + process.exit(lastExitCode); +} + +if (typeof module !== "undefined" && module.exports) { + module.exports = { + resolveCodexPromptFileArgs, + isRateLimitError, + isServerError, + }; +} + +if (require.main === module) { + main().catch(err => { + log(`unexpected error: ${err.message}`); + process.exit(1); + }); +} diff --git a/actions/setup/js/codex_harness.test.cjs b/actions/setup/js/codex_harness.test.cjs new file mode 100644 index 00000000000..0901c169461 --- /dev/null +++ b/actions/setup/js/codex_harness.test.cjs @@ -0,0 +1,158 @@ +import { describe, it, expect } from "vitest"; +import { createRequire } from "module"; +import fs from "fs"; +import os from "os"; +import path from "path"; + +const require = createRequire(import.meta.url); +const { resolveCodexPromptFileArgs, isRateLimitError, isServerError } = require("./codex_harness.cjs"); + +describe("codex_harness.cjs", () => { + describe("resolveCodexPromptFileArgs", () => { + it("replaces --prompt-file with the file's content as the last positional arg", () => { + const promptFile = path.join(os.tmpdir(), `codex-harness-prompt-${Date.now()}.txt`); + fs.writeFileSync(promptFile, "fix the bug", "utf8"); + try { + const result = resolveCodexPromptFileArgs(["exec", "--dangerously-bypass-approvals-and-sandbox", "--prompt-file", promptFile]); + expect(result).toEqual(["exec", "--dangerously-bypass-approvals-and-sandbox", "fix the bug"]); + } finally { + fs.rmSync(promptFile); + } + }); + + it("appends prompt content as the last arg when only --prompt-file is provided", () => { + const promptFile = path.join(os.tmpdir(), `codex-harness-prompt-${Date.now()}.txt`); + fs.writeFileSync(promptFile, "my task", "utf8"); + try { + const result = resolveCodexPromptFileArgs(["--prompt-file", promptFile]); + expect(result).toEqual(["my task"]); + } finally { + fs.rmSync(promptFile); + } + }); + + it("passes through args that have no --prompt-file", () => { + const result = resolveCodexPromptFileArgs(["exec", "--dangerously-bypass-approvals-and-sandbox"]); + expect(result).toEqual(["exec", "--dangerously-bypass-approvals-and-sandbox"]); + }); + + it("preserves args when --prompt-file is provided without a path", () => { + const result = resolveCodexPromptFileArgs(["exec", "--prompt-file"]); + // When no path follows --prompt-file, it is preserved as-is + expect(result).toEqual(["exec", "--prompt-file"]); + }); + + it("throws when the prompt file does not exist", () => { + const missingFile = path.join(os.tmpdir(), `codex-harness-missing-${Date.now()}.txt`); + expect(() => resolveCodexPromptFileArgs(["--prompt-file", missingFile])).toThrow(`--prompt-file '${missingFile}' is not readable`); + }); + + it("throws when the prompt file cannot be read (directory)", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-harness-dir-")); + try { + expect(() => resolveCodexPromptFileArgs(["--prompt-file", dir])).toThrow(`--prompt-file '${dir}' is not readable`); + } finally { + fs.rmdirSync(dir); + } + }); + }); + + describe("isRateLimitError", () => { + it("returns true for rate_limit_exceeded error", () => { + expect(isRateLimitError("Error: rate_limit_exceeded")).toBe(true); + }); + + it("returns true for 429 Too Many Requests", () => { + expect(isRateLimitError("429 Too Many Requests")).toBe(true); + }); + + it("returns true for RateLimitError", () => { + expect(isRateLimitError("RateLimitError: You exceeded your current quota")).toBe(true); + }); + + it("returns false for unrelated errors", () => { + expect(isRateLimitError("Error: ENOENT: no such file")).toBe(false); + expect(isRateLimitError("Fatal: out of memory")).toBe(false); + expect(isRateLimitError("")).toBe(false); + }); + + it("returns false for a 500 server error", () => { + expect(isRateLimitError("500 Internal Server Error")).toBe(false); + }); + }); + + describe("isServerError", () => { + it("returns true for InternalServerError", () => { + expect(isServerError("InternalServerError: The server had an error processing your request")).toBe(true); + }); + + it("returns true for ServiceUnavailableError", () => { + expect(isServerError("ServiceUnavailableError: The server is temporarily unable to service your request")).toBe(true); + }); + + it("returns true for 500 Internal Server Error", () => { + expect(isServerError("500 Internal Server Error")).toBe(true); + }); + + it("returns true for 503 Service Unavailable", () => { + expect(isServerError("503 Service Unavailable")).toBe(true); + }); + + it("returns false for rate limit errors", () => { + expect(isServerError("rate_limit_exceeded")).toBe(false); + expect(isServerError("429 Too Many Requests")).toBe(false); + }); + + it("returns false for unrelated errors", () => { + expect(isServerError("Error: ENOENT: no such file")).toBe(false); + expect(isServerError("")).toBe(false); + }); + }); + + describe("retry policy: fresh run on partial execution", () => { + const MAX_RETRIES = 3; + + /** + * @param {{hasOutput: boolean, exitCode: number, output: string}} result + * @param {number} attempt + * @returns {boolean} + */ + function shouldRetry(result, attempt) { + if (result.exitCode === 0) return false; + const RATE_LIMIT_ERROR_PATTERN = /rate_limit_exceeded|429 Too Many Requests|RateLimitError/i; + const SERVER_ERROR_PATTERN = /InternalServerError|ServiceUnavailableError|500 Internal Server Error|503 Service Unavailable/i; + const isTransient = RATE_LIMIT_ERROR_PATTERN.test(result.output) || SERVER_ERROR_PATTERN.test(result.output); + return attempt < MAX_RETRIES && (result.hasOutput || isTransient); + } + + it("retries on rate limit error even without output", () => { + const result = { exitCode: 1, hasOutput: false, output: "rate_limit_exceeded" }; + expect(shouldRetry(result, 0)).toBe(true); + }); + + it("retries on server error even without output", () => { + const result = { exitCode: 1, hasOutput: false, output: "InternalServerError" }; + expect(shouldRetry(result, 0)).toBe(true); + }); + + it("retries on any other non-zero exit when session produced output", () => { + const result = { exitCode: 1, hasOutput: true, output: "Error: connection reset" }; + expect(shouldRetry(result, 0)).toBe(true); + }); + + it("does not retry when no output was produced and no transient error", () => { + const result = { exitCode: 1, hasOutput: false, output: "" }; + expect(shouldRetry(result, 0)).toBe(false); + }); + + it("does not retry after retries are exhausted", () => { + const result = { exitCode: 1, hasOutput: true, output: "rate_limit_exceeded" }; + expect(shouldRetry(result, MAX_RETRIES)).toBe(false); + }); + + it("does not retry on success", () => { + const result = { exitCode: 0, hasOutput: true, output: "Task complete" }; + expect(shouldRetry(result, 0)).toBe(false); + }); + }); +}); diff --git a/docs/adr/30035-codex-engine-node-harness-with-retry.md b/docs/adr/30035-codex-engine-node-harness-with-retry.md new file mode 100644 index 00000000000..7b9f345b72a --- /dev/null +++ b/docs/adr/30035-codex-engine-node-harness-with-retry.md @@ -0,0 +1,77 @@ +# ADR-30035: Codex Engine Node.js Harness with Retry Logic + +**Date**: 2026-05-04 +**Status**: Draft +**Deciders**: Unknown + +--- + +## Part 1 — Narrative (Human-Friendly) + +### Context + +The system runs multiple AI agent engines (Claude, Copilot, Codex) as sub-processes inside GitHub Actions workflows. The existing Claude and Copilot engines both use a Node.js harness script (`claude_harness.cjs`, `copilot_harness.cjs`) that wraps the underlying CLI with retry logic, prompt file handling, and diagnostic logging. The Codex engine lacked this wrapper, invoking `codex exec` directly via an inline shell command with the prompt injected as a shell variable (`$INSTRUCTION`). The OpenAI Codex CLI is susceptible to transient API failures (HTTP 429 rate limits, 500/503 server errors) and does not provide a `--continue` session-resumption flag, meaning any transient failure results in total loss of the run with no recovery path. + +### Decision + +We will introduce `codex_harness.cjs`, a Node.js wrapper script that follows the established harness pattern used by the Claude and Copilot engines. The harness reads the prompt from a file (`--prompt-file`) instead of a shell variable, and retries up to three times on transient failures using exponential backoff (5 s → 10 s → 20 s, capped at 60 s). The `CodexEngine` Go struct implements the `HarnessProvider` interface by returning `"codex_harness.cjs"` from `GetHarnessScriptName()`, and `GetExecutionSteps()` is updated to invoke the harness via the shared `nodeRuntimeResolutionCommand` pattern. + +### Alternatives Considered + +#### Alternative 1: Inline Bash Retry Loop + +A `while` / `until` retry loop could be added directly to the shell command already embedded in each `.lock.yml`. This avoids adding a new file but would be duplicated across many workflow files, is difficult to unit-test, and the retry-detection logic (parsing OpenAI error patterns from output) becomes fragile in a one-liner. This approach also does not address the `$INSTRUCTION` shell-variable injection, which has a size limit and quoting hazards for large prompts. + +#### Alternative 2: Upstream Fix in the Codex CLI + +Asking OpenAI/Codex to add native retry and `--continue` support would provide the cleanest solution but is outside our control and has an unpredictable timeline. The workflow reliability problem exists today and needs a local mitigation. + +#### Alternative 3: Shell Wrapper Script (Plain `.sh`) + +A POSIX shell script could implement the retry loop without requiring Node.js. However, the existing harness infrastructure is Node.js-based (process runner, AWF reflect helpers, structured logging to stderr). Introducing a Bash harness for one engine would create an inconsistency, lose the shared `process_runner.cjs` utilities, and make it harder to write fast, isolated unit tests. + +### Consequences + +#### Positive +- Transient rate-limit and server errors are automatically retried, improving overall workflow success rates for Codex-based agents. +- Prompt delivery switches from shell-variable injection (`$INSTRUCTION`) to file-based (`--prompt-file`), removing shell quoting hazards and size limitations for large prompts. +- Consistent harness pattern across all three agent engines simplifies future maintenance and onboarding. +- 23 unit tests covering prompt resolution, error-pattern detection, and retry policy provide regression coverage. + +#### Negative +- All retries for Codex are fresh runs (not continuations), because the Codex CLI has no `--continue` flag. A partially completed run that fails transiently will restart from scratch, wasting tokens and time for the completed portion. +- Node.js must be present in the execution environment; the harness detects it via `GH_AW_NODE_BIN` or falls back to `command -v node`, adding a soft dependency that could fail silently on unusual runners. + +#### Neutral +- Every compiled `.lock.yml` that uses the Codex engine is regenerated to switch from the direct `codex exec "$INSTRUCTION"` invocation to the harness-wrapped form; this is a mechanical change with no behavioral difference beyond the retry wrapper. +- The `--prompt-file` flag is a harness-only argument stripped before passing the remaining args to `codex exec`. + +--- + +## Part 2 — Normative Specification (RFC 2119) + +> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +### Harness Script + +1. The Codex engine **MUST** invoke `codex exec` through `codex_harness.cjs` rather than calling it directly from a shell command. +2. The harness **MUST** accept a `--prompt-file ` argument, read the file contents, and append those contents as the final positional argument to `codex exec`. +3. The `--prompt-file` flag **MUST NOT** be forwarded to the `codex exec` subprocess. +4. The harness **MUST** retry a failed run up to 3 times when the subprocess produced output before exiting with a non-zero code (partial execution). +5. The harness **MUST** apply exponential backoff between retries, starting at 5 seconds, doubling on each attempt, and capping at 60 seconds. +6. The harness **MUST NOT** retry a run that produced no output before failing, as this indicates an unrecoverable error (e.g., authentication failure) rather than a transient one. +7. The harness **SHOULD** emit structured diagnostic log lines prefixed with `[codex-harness]` to stderr so they are distinguishable in aggregated logs. + +### Engine Interface + +1. `CodexEngine` **MUST** implement the `HarnessProvider` interface by returning `"codex_harness.cjs"` from `GetHarnessScriptName()`. +2. `CodexEngine.GetExecutionSteps()` **MUST** construct the execution command using `nodeRuntimeResolutionCommand` and the harness script name, consistent with the pattern used by other engines that implement `HarnessProvider`. +3. Compiled workflow lock files **MUST** reflect the harness invocation pattern and **MUST NOT** use the legacy `INSTRUCTION="$(cat ...)"` shell-variable injection for Codex. + +### Conformance + +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/25295059484) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index 76ef9d7127f..f208f5b0065 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -134,6 +134,12 @@ func (e *CodexEngine) GetAgentManifestPathPrefixes() []string { return []string{".codex/"} } +// GetHarnessScriptName returns the filename of the JavaScript harness script that wraps +// Codex CLI execution with retry logic for transient OpenAI API errors. +func (e *CodexEngine) GetHarnessScriptName() string { + return "codex_harness.cjs" +} + // GetExecutionSteps returns the GitHub Actions steps for executing Codex func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile string) []GitHubActionStep { modelConfigured := workflowData.EngineConfig != nil && workflowData.EngineConfig.Model != "" @@ -203,8 +209,31 @@ func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri commandName = "codex" } - codexCommand := fmt.Sprintf("%s %sexec%s%s%s%s\"$INSTRUCTION\"", - commandName, modelParam, webSearchParam, webFetchParam, fullAutoParam, customArgsParam) + // Determine harness script to wrap codex execution. + // The built-in harness provides retry logic for transient OpenAI API errors + // (rate limits, server errors). A custom engine.harness overrides the built-in one. + harnessScriptName := e.GetHarnessScriptName() + if workflowData.EngineConfig != nil && workflowData.EngineConfig.HarnessScript != "" { + harnessScriptName = workflowData.EngineConfig.HarnessScript + codexEngineLog.Printf("Using custom harness script: %s", harnessScriptName) + } + + // Build the Codex command. + // The default harness (codex_harness.cjs) wraps execution with retry logic and reads the + // prompt via --prompt-file. The else branch is a defensive fallback for the case where + // harnessScriptName is empty (e.g. a future code path that does not set a harness). + var codexCommand string + if harnessScriptName != "" { + // Harness-wrapped execution: the harness reads --prompt-file and passes its content + // as the last positional arg. The harness also provides retry logic. + execPrefix := fmt.Sprintf(`%s %s/%s %s`, nodeRuntimeResolutionCommand, SetupActionDestinationShell, harnessScriptName, commandName) + codexCommand = fmt.Sprintf("%s %sexec%s%s%s%s--prompt-file /tmp/gh-aw/aw-prompts/prompt.txt", + execPrefix, modelParam, webSearchParam, webFetchParam, fullAutoParam, customArgsParam) + } else { + // Without harness: use shell expansion for the prompt (no retry logic). + codexCommand = fmt.Sprintf("%s %sexec%s%s%s%s\"$INSTRUCTION\"", + commandName, modelParam, webSearchParam, webFetchParam, fullAutoParam, customArgsParam) + } // Build the full command with agent file handling and AWF wrapping if enabled var command string @@ -223,20 +252,23 @@ func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri allowedDomains = mergeAPITargetDomains(allowedDomains, workflowData.EngineConfig.APITarget) } - // Build the command with agent file handling if specified - // INSTRUCTION reading is done inside the AWF command to avoid Docker Compose interpolation - // issues with $ characters in the prompt. - // // AWF v0.15.0+ with --env-all handles most PATH setup natively (chroot mode is default): // - GOROOT, JAVA_HOME, etc. are handled via AWF_HOST_PATH and entrypoint.sh // However, npm-installed CLIs (like codex) need hostedtoolcache bin directories in PATH. npmPathSetup := GetNpmBinPathSetup() - // Codex reads prompt inside AWF container (PATH setup + codex command). + // Build the codex command with PATH setup inside the AWF container. // For engines that do not support native agent-file handling (including Codex), - // the compiler prepends the agent file content to prompt.txt so no special - // shell variable juggling is needed here. - codexCommandWithSetup := fmt.Sprintf(`%s && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && %s`, npmPathSetup, codexCommand) + // the compiler prepends the agent file content to prompt.txt. + // When using the harness, --prompt-file is passed directly; otherwise the prompt + // is read via shell variable expansion. + var codexCommandWithSetup string + if harnessScriptName != "" { + // Harness handles prompt reading via --prompt-file; no INSTRUCTION variable needed. + codexCommandWithSetup = fmt.Sprintf(`%s && %s`, npmPathSetup, codexCommand) + } else { + codexCommandWithSetup = fmt.Sprintf(`%s && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && %s`, npmPathSetup, codexCommand) + } // Add MCP CLI bin directory to PATH when cli-proxy is enabled if mcpCLIPath := GetMCPCLIPathSetup(workflowData); mcpCLIPath != "" { codexCommandWithSetup = fmt.Sprintf("%s && %s", mcpCLIPath, codexCommandWithSetup) @@ -262,12 +294,21 @@ func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri // For engines that do not support native agent-file handling (including Codex), // the compiler prepends the agent file content to prompt.txt so no special // shell variable juggling is needed here. - command = fmt.Sprintf(`set -o pipefail + if harnessScriptName != "" { + // Harness handles prompt reading via --prompt-file; no INSTRUCTION variable needed. + command = fmt.Sprintf(`set -o pipefail +touch %s +(umask 177 && touch %s) +mkdir -p "$CODEX_HOME/logs" +%s 2>&1 | tee %s`, AgentStepSummaryPath, logFile, codexCommand, logFile) + } else { + command = fmt.Sprintf(`set -o pipefail touch %s (umask 177 && touch %s) INSTRUCTION="$(cat "$GH_AW_PROMPT")" mkdir -p "$CODEX_HOME/logs" %s 2>&1 | tee %s`, AgentStepSummaryPath, logFile, codexCommand, logFile) + } } // Get effective GitHub token based on precedence: custom token > default diff --git a/pkg/workflow/codex_engine_test.go b/pkg/workflow/codex_engine_test.go index b6aa0fecf62..5df0267c65a 100644 --- a/pkg/workflow/codex_engine_test.go +++ b/pkg/workflow/codex_engine_test.go @@ -1007,3 +1007,78 @@ func TestCodexEngineWithExpressionVersion(t *testing.T) { t.Errorf("Expression should NOT be embedded directly in npm install command, got:\n%s", installStep) } } + +func TestCodexEngineGetHarnessScriptName(t *testing.T) { + engine := NewCodexEngine() + if engine.GetHarnessScriptName() != "codex_harness.cjs" { + t.Errorf("Expected 'codex_harness.cjs', got '%s'", engine.GetHarnessScriptName()) + } +} + +func TestCodexEngineExecutionUsesHarness(t *testing.T) { + engine := NewCodexEngine() + + workflowData := &WorkflowData{ + Name: "test-workflow", + } + steps := engine.GetExecutionSteps(workflowData, "test-log") + if len(steps) != 1 { + t.Fatalf("Expected 1 step for Codex execution, got %d", len(steps)) + } + + stepContent := strings.Join([]string(steps[0]), "\n") + + // Default execution should use the codex_harness.cjs + if !strings.Contains(stepContent, "codex_harness.cjs") { + t.Errorf("Expected codex_harness.cjs in execution step, got:\n%s", stepContent) + } + + // Harness should appear before the prompt file argument + harnessIdx := strings.Index(stepContent, "codex_harness.cjs") + promptFileIdx := strings.Index(stepContent, "--prompt-file") + if harnessIdx == -1 { + t.Fatal("Could not find codex_harness.cjs in step") + } + if promptFileIdx == -1 { + t.Fatal("Could not find --prompt-file in step") + } + if harnessIdx > promptFileIdx { + t.Error("Expected codex_harness.cjs to appear before --prompt-file") + } + + // Should use --prompt-file instead of $INSTRUCTION when harness is active + if !strings.Contains(stepContent, "--prompt-file") { + t.Errorf("Expected --prompt-file in harness-wrapped execution step, got:\n%s", stepContent) + } + if strings.Contains(stepContent, "\"$INSTRUCTION\"") { + t.Errorf("Expected no $INSTRUCTION variable when harness is active, got:\n%s", stepContent) + } +} + +func TestCodexEngineExecutionCustomHarness(t *testing.T) { + engine := NewCodexEngine() + + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "codex", + HarnessScript: "custom_codex_harness.cjs", + }, + } + steps := engine.GetExecutionSteps(workflowData, "test-log") + if len(steps) != 1 { + t.Fatalf("Expected 1 step for Codex execution, got %d", len(steps)) + } + + stepContent := strings.Join([]string(steps[0]), "\n") + + // Should use custom harness script + if !strings.Contains(stepContent, "custom_codex_harness.cjs") { + t.Errorf("Expected custom_codex_harness.cjs in execution step, got:\n%s", stepContent) + } + + // Should NOT use the default harness path + if strings.Contains(stepContent, "actions/codex_harness.cjs") { + t.Errorf("Expected default harness path to be overridden, got:\n%s", stepContent) + } +} diff --git a/pkg/workflow/engine_agent_import_test.go b/pkg/workflow/engine_agent_import_test.go index 4104ace845b..293f5d3d88c 100644 --- a/pkg/workflow/engine_agent_import_test.go +++ b/pkg/workflow/engine_agent_import_test.go @@ -216,9 +216,10 @@ func TestCodexEngineWithAgentFromImports(t *testing.T) { t.Errorf("Codex must NOT invoke awk for agent file reading (found in step); the compiler handles it:\n%s", stepContent) } - // The engine still reads the standard prompt.txt (which has agent content prepended by the compiler). - if !strings.Contains(stepContent, `INSTRUCTION="$(cat "$GH_AW_PROMPT")"`) { - t.Errorf("Expected standard prompt.txt reading in codex command, got:\n%s", stepContent) + // The engine still reads the standard prompt.txt (which has agent content prepended by the compiler) + // via --prompt-file when using the default harness. + if !strings.Contains(stepContent, "--prompt-file /tmp/gh-aw/aw-prompts/prompt.txt") { + t.Errorf("Expected --prompt-file reading in codex command, got:\n%s", stepContent) } // The engine reports that it does not support native agent file handling. @@ -250,9 +251,9 @@ func TestCodexEngineWithoutAgentFile(t *testing.T) { t.Errorf("Did not expect AGENT_CONTENT when agent file is not specified, got:\n%s", stepContent) } - // Should have the standard instruction reading - if !strings.Contains(stepContent, `INSTRUCTION="$(cat "$GH_AW_PROMPT")"`) { - t.Errorf("Expected standard INSTRUCTION reading in codex command, got:\n%s", stepContent) + // Should have the standard instruction reading via --prompt-file (harness handles prompt reading) + if !strings.Contains(stepContent, "--prompt-file /tmp/gh-aw/aw-prompts/prompt.txt") { + t.Errorf("Expected --prompt-file reading in codex command, got:\n%s", stepContent) } } @@ -300,9 +301,9 @@ func TestCodexEngineAWFWithAgentFileReadsPromptTxt(t *testing.T) { t.Errorf("awk must not appear in the Codex AWF step; compiler handles agent file injection:\n%s", stepContent) } - // The container command must still read from prompt.txt. - if !strings.Contains(stepContent, `INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"`) { - t.Errorf("Expected codex to read from prompt.txt in AWF mode, got:\n%s", stepContent) + // The container command must still pass prompt.txt via --prompt-file (harness handles reading). + if !strings.Contains(stepContent, "--prompt-file /tmp/gh-aw/aw-prompts/prompt.txt") { + t.Errorf("Expected codex to use --prompt-file in AWF mode, got:\n%s", stepContent) } } diff --git a/pkg/workflow/engine_args_test.go b/pkg/workflow/engine_args_test.go index e156eaba2fc..8c2da586e38 100644 --- a/pkg/workflow/engine_args_test.go +++ b/pkg/workflow/engine_args_test.go @@ -427,19 +427,19 @@ func TestCodexEngineArgsInjection(t *testing.T) { stepStr := strings.Join(executionStep, "\n") - // Check that args appear in the command before INSTRUCTION + // Check that args appear in the command before --prompt-file if !strings.Contains(stepStr, "--custom-flag value") { t.Errorf("Expected to find '--custom-flag value' in step, got:\n%s", stepStr) } - // Check that args come before "$INSTRUCTION" + // Check that args come before --prompt-file (the last positional arg supplied by the harness) customFlagIdx := strings.Index(stepStr, "--custom-flag value") - instructionIdx := strings.Index(stepStr, "\"$INSTRUCTION\"") - if customFlagIdx == -1 || instructionIdx == -1 { - t.Fatal("Could not find both --custom-flag and $INSTRUCTION in step") + promptFileIdx := strings.Index(stepStr, "--prompt-file") + if customFlagIdx == -1 || promptFileIdx == -1 { + t.Fatal("Could not find both --custom-flag and --prompt-file in step") } - if customFlagIdx > instructionIdx { - t.Error("Expected --custom-flag to come before $INSTRUCTION") + if customFlagIdx > promptFileIdx { + t.Error("Expected --custom-flag to come before --prompt-file") } })