diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index ca0aa4101..f771ce6ce 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -272,20 +272,20 @@ "digest": "sha256:3d5dba0b0a139bbb11b5d5b8b44f277d2b18f69cf43090e3c283d750cf864baa", "pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.9@sha256:3d5dba0b0a139bbb11b5d5b8b44f277d2b18f69cf43090e3c283d750cf864baa" }, - "ghcr.io/github/gh-aw-mcpg:v0.4.13": { - "image": "ghcr.io/github/gh-aw-mcpg:v0.4.13", - "digest": "sha256:ec4008521c610e1113ed557ecec0ff64a2c2111e4cfa817bab54d9b7da24c7cc", - "pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.13@sha256:ec4008521c610e1113ed557ecec0ff64a2c2111e4cfa817bab54d9b7da24c7cc" - }, "ghcr.io/github/gh-aw-mcpg:v0.4.14": { "image": "ghcr.io/github/gh-aw-mcpg:v0.4.14", "digest": "sha256:b2f0c2b2f17b5fbe809e5bb99dc185b6ddd70df25295dc63a6d526350334eff5", "pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.14@sha256:b2f0c2b2f17b5fbe809e5bb99dc185b6ddd70df25295dc63a6d526350334eff5" }, + "ghcr.io/github/gh-aw-mcpg:v0.4.15": { + "image": "ghcr.io/github/gh-aw-mcpg:v0.4.15", + "digest": "sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e", + "pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e" + }, "ghcr.io/github/gh-aw-node": { "image": "ghcr.io/github/gh-aw-node", - "digest": "sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b", - "pinned_image": "ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b" + "digest": "sha256:0daa8971fa4732b647150cb6524a6b0804b68d5d24f6f58b5dd1af23bd63fb23", + "pinned_image": "ghcr.io/github/gh-aw-node@sha256:0daa8971fa4732b647150cb6524a6b0804b68d5d24f6f58b5dd1af23bd63fb23" }, "ghcr.io/github/github-mcp-server:v1.11.0": { "image": "ghcr.io/github/github-mcp-server:v1.11.0", @@ -294,8 +294,8 @@ }, "mcr.microsoft.com/playwright/mcp": { "image": "mcr.microsoft.com/playwright/mcp", - "digest": "sha256:3d871c22ea2d4cca0966e2cfb1860e1cb03eb7353725a3d6cffd133296fb04eb", - "pinned_image": "mcr.microsoft.com/playwright/mcp@sha256:3d871c22ea2d4cca0966e2cfb1860e1cb03eb7353725a3d6cffd133296fb04eb" + "digest": "sha256:dda1f7f9b812e22946635c8af7df9288b96d3b9e3f0f1b8576d6823e2031c1de", + "pinned_image": "mcr.microsoft.com/playwright/mcp@sha256:dda1f7f9b812e22946635c8af7df9288b96d3b9e3f0f1b8576d6823e2031c1de" } } } diff --git a/.github/workflows/smoke-enclave-issues-read.lock.yml b/.github/workflows/smoke-enclave-issues-read.lock.yml index 93927529c..af1498c77 100644 --- a/.github/workflows/smoke-enclave-issues-read.lock.yml +++ b/.github/workflows/smoke-enclave-issues-read.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f741f8ef596ebbb682f88ed7e490de9cafe72fcac99cae2d6aed25fa12219cb4","body_hash":"2da025901e8f70a0197dd99c6f702a98be6386e13d4b5ef64f2126e32847136a","compiler_version":"v0.87.10","agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} -# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"bc8c008a419c5b7a29df6f5641edd35fd1c6ea85","version":"v0.87.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.9","digest":"sha256:54b2fb3068efc15a4cc1bd4033f8fa056a9b1779baeba0cb80ae95ea55e7e343","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.9@sha256:54b2fb3068efc15a4cc1bd4033f8fa056a9b1779baeba0cb80ae95ea55e7e343"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.9","digest":"sha256:a0ffb1dc926c6e5a500b336893e032a8f167d3db43c869be886874ef14280bb8","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.9@sha256:a0ffb1dc926c6e5a500b336893e032a8f167d3db43c869be886874ef14280bb8"},{"image":"ghcr.io/github/gh-aw-firewall/enclave-agent:0.28.9","digest":"sha256:8d548153f18d9b44406bebe3f71e2e080c15c89cbf8c0f72e8aa7bc0681efcf2","pinned_image":"ghcr.io/github/gh-aw-firewall/enclave-agent:0.28.9@sha256:8d548153f18d9b44406bebe3f71e2e080c15c89cbf8c0f72e8aa7bc0681efcf2"},{"image":"ghcr.io/github/gh-aw-firewall/enclave-mcp-server:0.28.9","digest":"sha256:9edfa59fe0cf96f86c0be2f3280743a95032cf730526a204f729a06fbe7e4727","pinned_image":"ghcr.io/github/gh-aw-firewall/enclave-mcp-server:0.28.9@sha256:9edfa59fe0cf96f86c0be2f3280743a95032cf730526a204f729a06fbe7e4727"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.9","digest":"sha256:3d5dba0b0a139bbb11b5d5b8b44f277d2b18f69cf43090e3c283d750cf864baa","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.9@sha256:3d5dba0b0a139bbb11b5d5b8b44f277d2b18f69cf43090e3c283d750cf864baa"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.13","digest":"sha256:ec4008521c610e1113ed557ecec0ff64a2c2111e4cfa817bab54d9b7da24c7cc","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.13@sha256:ec4008521c610e1113ed557ecec0ff64a2c2111e4cfa817bab54d9b7da24c7cc"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"awf-enclave","tools":["*"]},{"name":"github","tools":["get_me"]},{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop"]}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"03f5b7d8d46da7a76e41d8abcef8163e981f2512d52d7e5ad20da34bc815439f","body_hash":"41801b25068f65f1690dadce9f3ee28edaece798d3c958b5c9723d0c24df5bc9","compiler_version":"v0.87.10","agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"bc8c008a419c5b7a29df6f5641edd35fd1c6ea85","version":"v0.87.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.9","digest":"sha256:54b2fb3068efc15a4cc1bd4033f8fa056a9b1779baeba0cb80ae95ea55e7e343","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.9@sha256:54b2fb3068efc15a4cc1bd4033f8fa056a9b1779baeba0cb80ae95ea55e7e343"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.9","digest":"sha256:a0ffb1dc926c6e5a500b336893e032a8f167d3db43c869be886874ef14280bb8","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.9@sha256:a0ffb1dc926c6e5a500b336893e032a8f167d3db43c869be886874ef14280bb8"},{"image":"ghcr.io/github/gh-aw-firewall/enclave-agent:0.28.9","digest":"sha256:8d548153f18d9b44406bebe3f71e2e080c15c89cbf8c0f72e8aa7bc0681efcf2","pinned_image":"ghcr.io/github/gh-aw-firewall/enclave-agent:0.28.9@sha256:8d548153f18d9b44406bebe3f71e2e080c15c89cbf8c0f72e8aa7bc0681efcf2"},{"image":"ghcr.io/github/gh-aw-firewall/enclave-mcp-server:0.28.9","digest":"sha256:9edfa59fe0cf96f86c0be2f3280743a95032cf730526a204f729a06fbe7e4727","pinned_image":"ghcr.io/github/gh-aw-firewall/enclave-mcp-server:0.28.9@sha256:9edfa59fe0cf96f86c0be2f3280743a95032cf730526a204f729a06fbe7e4727"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.9","digest":"sha256:3d5dba0b0a139bbb11b5d5b8b44f277d2b18f69cf43090e3c283d750cf864baa","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.9@sha256:3d5dba0b0a139bbb11b5d5b8b44f277d2b18f69cf43090e3c283d750cf864baa"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0daa8971fa4732b647150cb6524a6b0804b68d5d24f6f58b5dd1af23bd63fb23","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0daa8971fa4732b647150cb6524a6b0804b68d5d24f6f58b5dd1af23bd63fb23"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"awf-enclave","tools":["*"]},{"name":"github","tools":["get_me"]},{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop"]}]} # This file was automatically generated by gh-aw (v0.87.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ @@ -50,8 +50,8 @@ # - ghcr.io/github/gh-aw-firewall/enclave-agent:0.28.9@sha256:8d548153f18d9b44406bebe3f71e2e080c15c89cbf8c0f72e8aa7bc0681efcf2 # - ghcr.io/github/gh-aw-firewall/enclave-mcp-server:0.28.9@sha256:9edfa59fe0cf96f86c0be2f3280743a95032cf730526a204f729a06fbe7e4727 # - -# - ghcr.io/github/gh-aw-mcpg:v0.4.13@sha256:ec4008521c610e1113ed557ecec0ff64a2c2111e4cfa817bab54d9b7da24c7cc -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:0daa8971fa4732b647150cb6524a6b0804b68d5d24f6f58b5dd1af23bd63fb23 # - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Smoke Enclave Issues Read" @@ -139,7 +139,7 @@ jobs: GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github"]' GH_AW_INFO_FIREWALL_ENABLED: "true" GH_AW_INFO_AWF_VERSION: "v0.28.9" - GH_AW_INFO_AWMG_VERSION: "v0.4.13" + GH_AW_INFO_AWMG_VERSION: "v0.4.15" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "false" @@ -541,7 +541,7 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/enclave-agent:0.28.9@sha256:8d548153f18d9b44406bebe3f71e2e080c15c89cbf8c0f72e8aa7bc0681efcf2 ghcr.io/github/gh-aw-firewall/enclave-mcp-server:0.28.9@sha256:9edfa59fe0cf96f86c0be2f3280743a95032cf730526a204f729a06fbe7e4727 ghcr.io/github/gh-aw-mcpg:v0.4.13@sha256:ec4008521c610e1113ed557ecec0ff64a2c2111e4cfa817bab54d9b7da24c7cc ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/enclave-agent:0.28.9@sha256:8d548153f18d9b44406bebe3f71e2e080c15c89cbf8c0f72e8aa7bc0681efcf2 ghcr.io/github/gh-aw-firewall/enclave-mcp-server:0.28.9@sha256:9edfa59fe0cf96f86c0be2f3280743a95032cf730526a204f729a06fbe7e4727 ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:0daa8971fa4732b647150cb6524a6b0804b68d5d24f6f58b5dd1af23bd63fb23 ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" @@ -704,7 +704,7 @@ jobs: GITHUB_ENTERPRISE_HOST: ${{ env.GITHUB_ENTERPRISE_HOST }} GITHUB_GRAPHQL_URL: ${{ env.GITHUB_GRAPHQL_URL }} GITHUB_COPILOT_BASE_URL: ${{ env.GITHUB_COPILOT_BASE_URL }} - ENCLAVE_GITHUB_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.4.13' + ENCLAVE_GITHUB_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.4.15' ENCLAVE_GITHUB_PROXY_ALIAS: awf-enclave-github-proxy ENCLAVE_GITHUB_PROXY_POLICY_TEMPLATE: '{"version":1,"workflow_run_id":"","profile":"issues-read-v1","audience":"gh-aw-enclave-github","repositories":[{"repo":"github/gh-aw","sensitivity":"internal"}],"public_min_integrity":"approved","allowed_operations":["issues.comments.list","issues.get","issues.list"],"max_capability_ttl_seconds":600}' run: | @@ -770,7 +770,7 @@ jobs: MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --label com.github.gh-aw.mcpg.run='"${AWF_ENCLAVE_MCP_GATEWAY_IDENTITY}"' --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e AWF_ENCLAVE_MCP_CAPABILITY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.13' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --label com.github.gh-aw.mcpg.run='"${AWF_ENCLAVE_MCP_GATEWAY_IDENTITY}"' --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e AWF_ENCLAVE_MCP_CAPABILITY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) diff --git a/.github/workflows/smoke-enclave-issues-read.md b/.github/workflows/smoke-enclave-issues-read.md index ce59d0996..7870d2be4 100644 --- a/.github/workflows/smoke-enclave-issues-read.md +++ b/.github/workflows/smoke-enclave-issues-read.md @@ -43,7 +43,7 @@ sandbox: id: awf version: v0.28.9 mcp: - version: v0.4.13 + version: v0.4.15 strict: false concurrency: group: smoke-enclave-issues-read @@ -124,18 +124,18 @@ Pass this exact finite-disclosure schema: Give the enclave agent this task: ```text -Use only the narrow gh wrapper and run each command exactly once: +Use only the `github` MCP server and call each tool exactly once: -1. gh api --method GET 'repos/github/gh-aw/issues?per_page=1' -2. gh api --method GET 'repos/github/gh-aw/issues/50920' -3. gh api --method GET 'repos/github/gh-aw/issues/50920/comments?per_page=1' +1. `list_issues` with `owner: "github"`, `repo: "gh-aw"`, and `perPage: 1` +2. `issue_read` with `owner: "github"`, `repo: "gh-aw"`, `method: "get"`, and `issue_number: 50920` +3. `issue_read` with `owner: "github"`, `repo: "gh-aw"`, `method: "get_comments"`, `issue_number: 50920`, and `perPage: 1` Return exactly: {"list_read":true,"issue_read":true,"comments_read":true} -Set a value to false if its command fails, the list or comments response is not +Set a value to false if its tool call fails, the list or comments response is not a JSON array, or the issue response does not contain number 50920. Do not use -stock gh issue commands, GraphQL, search, writes, or any other GitHub endpoint. +GitHub CLI, GraphQL, search, writes, or any other GitHub tool. ``` The test passes only when all three returned booleans are `true`. diff --git a/containers/enclave/Dockerfile b/containers/enclave/Dockerfile index 3829562d5..1f24fdb00 100644 --- a/containers/enclave/Dockerfile +++ b/containers/enclave/Dockerfile @@ -39,12 +39,11 @@ RUN ln -s ../lib/node_modules/@github/copilot/npm-loader.js /usr/local/bin/copil && rm -f /usr/bin/pebble \ && rm -rf /root/.cache/copilot COPY enclave/agent-entrypoint.py /usr/local/bin/run-enclave-agent -COPY enclave/github-cli-wrapper.py /usr/local/bin/gh -RUN chmod 0555 /usr/local/bin/run-enclave-agent /usr/local/bin/gh \ +RUN chmod 0555 /usr/local/bin/run-enclave-agent \ && python3 -m py_compile /usr/local/bin/run-enclave-agent \ - && python3 -m py_compile /usr/local/bin/gh \ && rm -rf /usr/local/bin/__pycache__ \ - && mkdir -p /agent /awf/seed /run/awf-enclave-github + && mkdir -p /agent /awf/seed /run/awf-enclave-github \ + && ! command -v gh FROM node:22.23.2-alpine3.24 AS enclave-mcp-server diff --git a/containers/enclave/agent-entrypoint.py b/containers/enclave/agent-entrypoint.py index 7152a8019..153d1e41a 100644 --- a/containers/enclave/agent-entrypoint.py +++ b/containers/enclave/agent-entrypoint.py @@ -5,6 +5,7 @@ import json import os import re +import shutil import signal import stat import subprocess @@ -21,6 +22,8 @@ TEMP_DIR = Path("/tmp") SHARED_MEMORY_DIR = Path("/dev/shm") COPILOT_BIN = "/usr/local/bin/copilot" +GITHUB_AGENT_ID_PATH = Path("/run/awf-enclave-github/agent-id") +GITHUB_MCP_CONFIG_PATH = AGENT_DIR / "github-mcp.json" MAX_INPUT_BYTES = 64 * 1024 MAX_TRANSCRIPT_BYTES = 1024 * 1024 @@ -77,6 +80,12 @@ def redact_diagnostics(value: str) -> str: for name, secret in os.environ.items(): if secret and secret != "******" and re.search(r"(?:TOKEN|KEY|SECRET|CREDENTIAL)", name): redacted = redacted.replace(secret, "[REDACTED]") + try: + agent_id = GITHUB_AGENT_ID_PATH.read_text(encoding="ascii").strip() + if agent_id: + redacted = redacted.replace(agent_id, "[REDACTED]") + except (OSError, UnicodeDecodeError): + pass return redacted @@ -280,6 +289,14 @@ def run_preflight(copilot_logs: Path) -> bool: ("copilot-log-directory", copilot_logs, "directory", True, True), ] valid = True + if shutil.which("gh") is not None: + append_event({ + "event": "preflight", + "path": "github-cli", + "exists": True, + "type": "forbidden-executable", + }) + valid = False for identifier, path, expected_type, executable, writable in checks: error = preflight_path( identifier, @@ -291,6 +308,15 @@ def run_preflight(copilot_logs: Path) -> bool: if error is not None: safe_os_error(error, f"preflight-{identifier}") valid = False + if os.environ.get("AWF_ENCLAVE_AGENT_GITHUB_ENABLED") == "true": + error = preflight_path( + "github-agent-id", + GITHUB_AGENT_ID_PATH, + "file", + ) + if error is not None: + safe_os_error(error, "preflight-github-agent-id") + valid = False append_progress("preflight-completed", valid=valid) return valid @@ -339,19 +365,18 @@ def build_prompt(task: str, schema_text: str) -> str: github_access = "" if os.environ.get("AWF_ENCLAVE_AGENT_GITHUB_ENABLED") == "true": github_access = ( - " A narrow credential-isolated gh wrapper is available only for REST issue reads. " - "Use `gh api --method GET` with repos/{owner}/{repo}/issues, " - "repos/{owner}/{repo}/issues/{number}, or " - "repos/{owner}/{repo}/issues/{number}/comments. GraphQL, search, writes, " - "and other GitHub paths are unavailable." + " A credential-isolated GitHub MCP server exposes only `list_issues` and " + "`issue_read`. For `issue_read`, only methods `get` and `get_comments` are " + "available. Use those MCP tools for GitHub reads. GitHub CLI, GraphQL, search, " + "writes, and all other GitHub tools are unavailable." ) return ( "You are the native GitHub Copilot CLI running in an AWF enclave-agent enclave.\n" "The repository root is your current directory and is mounted read-only at /awf/seed. " "/agent is an invocation-private writable runtime directory and /tmp is bounded tmpfs. " "You may use your built-in shell, " - "bash, file-reading, and search tools. You have no GitHub MCP, no credentials, no host " - "filesystem, and no network route except AWF's model and optional GitHub proxies." + "bash, file-reading, and search tools. You have no GitHub credentials, no host " + "filesystem, and no network route except AWF's model proxy and optional shared GitHub MCP gateway." f"{github_access}\n\n" "Complete this task:\n" f"{task}\n\n" @@ -359,6 +384,33 @@ def build_prompt(task: str, schema_text: str) -> str: ) +def configure_github_mcp() -> None: + if os.environ.get("AWF_ENCLAVE_AGENT_GITHUB_ENABLED") != "true": + return + if os.environ.get("AWF_ENCLAVE_AGENT_GITHUB_PROFILE") != "issues-read-v1": + raise ValueError("unsupported GitHub MCP profile") + endpoint = os.environ.get("AWF_ENCLAVE_AGENT_GITHUB_MCP_URL", "") + if not re.fullmatch(r"http://[0-9.]+:[0-9]+/mcp/github", endpoint): + raise ValueError("invalid GitHub MCP endpoint") + agent_id = GITHUB_AGENT_ID_PATH.read_text(encoding="ascii").strip() + if not re.fullmatch(r"[A-Za-z0-9_-]{32,128}", agent_id): + raise ValueError("invalid GitHub MCP agent identity") + config = { + "mcpServers": { + "github": { + "type": "http", + "url": endpoint, + "headers": {"Authorization": agent_id}, + } + } + } + GITHUB_MCP_CONFIG_PATH.write_text( + json.dumps(config, separators=(",", ":")), + encoding="utf-8", + ) + GITHUB_MCP_CONFIG_PATH.chmod(0o600) + + def append_engine_result(completed: subprocess.CompletedProcess) -> tuple[str, str]: stdout = completed.stdout.decode("utf-8", errors="replace").strip() stderr = completed.stderr.decode("utf-8", errors="replace") @@ -412,6 +464,11 @@ def main() -> int: append_event({"event": "failure", "category": "engine-failed"}) return EXIT_ENGINE_FAILED copilot_logs = runtime_paths[-1][1] + try: + configure_github_mcp() + except (OSError, UnicodeDecodeError, ValueError): + append_event({"event": "failure", "category": "configuration-invalid"}) + return EXIT_CONFIGURATION_INVALID append_progress( "runtime-paths-ready", paths=[identifier for identifier, _ in runtime_paths], @@ -452,6 +509,8 @@ def main() -> int: "--log-level", "all", "--log-dir", str(copilot_logs), ] + if os.environ.get("AWF_ENCLAVE_AGENT_GITHUB_ENABLED") == "true": + command.extend(["--additional-mcp-config", f"@{GITHUB_MCP_CONFIG_PATH}"]) if max_model_requests is not None: command.extend(["--max-model-requests", max_model_requests]) if max_model_tokens is not None: diff --git a/containers/enclave/agent-executor/docker-enclave-runner.js b/containers/enclave/agent-executor/docker-enclave-runner.js index 4ec91b6b6..e8e31bce5 100644 --- a/containers/enclave/agent-executor/docker-enclave-runner.js +++ b/containers/enclave/agent-executor/docker-enclave-runner.js @@ -26,7 +26,7 @@ class DockerEnclaveRunner { async assertNetworkIsolated() { const expectedMembers = ['awf-enclave-agent-api-proxy@172.31.0.30/24,']; if (this.config.githubEnabled) { - expectedMembers.push('awf-enclave-agent-cli-proxy@172.31.0.40/24,'); + expectedMembers.push(`${this.config.githubGatewayContainer}@172.31.0.40/24,`); } const expectedTopologies = new Set([ `true|bridge|172.31.0.0/24,|${expectedMembers.join('')}`, diff --git a/containers/enclave/agent-executor/enclave-runner-spec.js b/containers/enclave/agent-executor/enclave-runner-spec.js index 07ab34528..d70259299 100644 --- a/containers/enclave/agent-executor/enclave-runner-spec.js +++ b/containers/enclave/agent-executor/enclave-runner-spec.js @@ -12,7 +12,7 @@ * * - `--network `: the enclave joins *only* the * dedicated `internal` enclave-agent network. Its mandatory peer is the AWF - * API proxy; issues-read-v1 adds only the PAT-free AWF CLI proxy. There is + * API proxy; issues-read-v1 adds only compiler-owned shared mcpg. There is * no `awf-net`, no `awf-ext`, no Squid, no general proxy, no primary agent, * no broker, no safe-outputs collector, and no MCP gateway. * - `--read-only` with the repository seed bind-mounted `ro`: the enclave can @@ -125,9 +125,9 @@ function deriveEnclaveContainerSpec({ config, runId, invocationId, seedId, runti launchArgs.push( '--env', 'AWF_ENCLAVE_AGENT_GITHUB_ENABLED=true', '--env', `AWF_ENCLAVE_AGENT_GITHUB_PROFILE=${config.githubProfile}`, - '--env', `AWF_ENCLAVE_AGENT_GITHUB_PROXY_URL=${config.githubProxyUrl}`, + '--env', `AWF_ENCLAVE_AGENT_GITHUB_MCP_URL=${config.githubMcpUrl}`, '-v', - `${hostInvocationDir}/github-capability:${config.enclaveGithubCapabilityPath}:ro`, + `${hostInvocationDir}/github-agent-id:${config.enclaveGithubAgentIdPath}:ro`, ); } diff --git a/containers/enclave/agent-executor/github-capability.js b/containers/enclave/agent-executor/github-capability.js deleted file mode 100644 index 88cc56485..000000000 --- a/containers/enclave/agent-executor/github-capability.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -const crypto = require('crypto'); - -const CAPABILITY_PREFIX = 'awf-egh1'; -const CAPABILITY_AUDIENCE = 'gh-aw-enclave-github'; -const CAPABILITY_PROFILE = 'issues-read-v1'; -const CAPABILITY_OPERATIONS = Object.freeze([ - 'issues.comments.list', - 'issues.get', - 'issues.list', -]); - -function base64url(value) { - return Buffer.from(value).toString('base64url'); -} - -function assertIdentifier(name, value) { - if (typeof value !== 'string' || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(value)) { - throw new Error(`${name} is not a broker-generated identifier`); - } -} - -function mintGithubCapability(params) { - if (typeof params.keyHex !== 'string' || !/^[0-9a-f]{64}$/.test(params.keyHex)) { - throw new Error('GitHub capability root must be 256-bit lowercase hex'); - } - assertIdentifier('runId', params.runId); - assertIdentifier('invocationId', params.invocationId); - if ( - typeof params.repo !== 'string' - || params.repo !== params.repo.toLowerCase() - || !/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/.test(params.repo) - ) { - throw new Error('repo must be a canonical lowercase owner/repo'); - } - if ( - !Number.isSafeInteger(params.notBefore) - || !Number.isSafeInteger(params.expiresAt) - || params.notBefore < 0 - || params.expiresAt <= params.notBefore - ) { - throw new Error('GitHub capability timestamps are invalid'); - } - - const payload = JSON.stringify({ - v: 1, - aud: CAPABILITY_AUDIENCE, - run: params.runId, - inv: params.invocationId, - repo: params.repo, - profile: CAPABILITY_PROFILE, - ops: CAPABILITY_OPERATIONS, - nbf: params.notBefore, - exp: params.expiresAt, - }); - const encodedPayload = base64url(Buffer.from(payload, 'utf8')); - const signingInput = `${CAPABILITY_PREFIX}.${encodedPayload}`; - const mac = crypto - .createHmac('sha256', Buffer.from(params.keyHex, 'hex')) - .update(signingInput, 'ascii') - .digest('base64url'); - return `${signingInput}.${mac}`; -} - -module.exports = { - CAPABILITY_AUDIENCE, - CAPABILITY_OPERATIONS, - CAPABILITY_PREFIX, - CAPABILITY_PROFILE, - mintGithubCapability, -}; diff --git a/containers/enclave/agent-executor/workspace.js b/containers/enclave/agent-executor/workspace.js index 1851d9f65..2ea05af9a 100644 --- a/containers/enclave/agent-executor/workspace.js +++ b/containers/enclave/agent-executor/workspace.js @@ -29,7 +29,7 @@ function invocationLayout(workDir, invocationId) { schemaPath: path.join(root, 'schema.json'), outPath: path.join(root, 'out'), sessionLogPath: path.join(root, 'session.jsonl'), - githubCapabilityPath: path.join(root, 'github-capability'), + githubAgentIdPath: path.join(root, 'github-agent-id'), agentPath: path.join(root, 'agent'), }; } @@ -42,7 +42,7 @@ function invocationLayout(workDir, invocationId) { * it through its `rw` bind mount. */ function createInvocationWorkspace(params) { - const { config, invocationId, task, schema, githubCapability } = params; + const { config, invocationId, task, schema, githubAgentId } = params; const layout = invocationLayout(config.workDir, invocationId); fs.mkdirSync(layout.root, { recursive: true, mode: 0o700 }); @@ -61,12 +61,12 @@ function createInvocationWorkspace(params) { fs.chownSync(layout.agentPath, config.enclaveUid, config.enclaveGid); fs.chmodSync(layout.agentPath, 0o700); if (config.githubEnabled) { - if (typeof githubCapability !== 'string' || githubCapability.length > 4096) { - throw new Error('invalid invocation GitHub capability'); + if (typeof githubAgentId !== 'string' || !/^[A-Za-z0-9_-]{32,128}$/.test(githubAgentId)) { + throw new Error('invalid enclave GitHub MCP agent identity'); } - fs.writeFileSync(layout.githubCapabilityPath, `${githubCapability}\n`, { mode: 0o600 }); - fs.chownSync(layout.githubCapabilityPath, config.enclaveUid, config.enclaveGid); - fs.chmodSync(layout.githubCapabilityPath, 0o400); + fs.writeFileSync(layout.githubAgentIdPath, `${githubAgentId}\n`, { mode: 0o600 }); + fs.chownSync(layout.githubAgentIdPath, config.enclaveUid, config.enclaveGid); + fs.chmodSync(layout.githubAgentIdPath, 0o400); } return layout; diff --git a/containers/enclave/github-cli-wrapper.py b/containers/enclave/github-cli-wrapper.py deleted file mode 100644 index 4ae05fa99..000000000 --- a/containers/enclave/github-cli-wrapper.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -"""Forward the enclave's narrow gh surface to the PAT-free AWF CLI proxy.""" - -import json -import os -import sys -import urllib.error -import urllib.request -from pathlib import Path - -MAX_RESPONSE_BYTES = 10 * 1024 * 1024 - - -def fail(message: str) -> int: - print(f"gh: {message}", file=sys.stderr) - return 1 - - -def main() -> int: - if os.environ.get("AWF_ENCLAVE_AGENT_GITHUB_PROFILE") != "issues-read-v1": - return fail("GitHub CLI access is not enabled") - proxy_url = os.environ.get("AWF_ENCLAVE_AGENT_GITHUB_PROXY_URL", "") - capability_path = Path("/run/awf-enclave-github/capability") - if proxy_url != "http://172.31.0.40:11000": - return fail("GitHub CLI proxy configuration is invalid") - try: - capability = capability_path.read_text(encoding="ascii").strip() - except OSError: - return fail("GitHub CLI capability is unavailable") - if not capability.startswith("awf-egh1.") or len(capability) > 4096: - return fail("GitHub CLI capability is invalid") - - payload = json.dumps( - {"args": sys.argv[1:], "stdin": None}, - separators=(",", ":"), - ).encode("utf-8") - request = urllib.request.Request( - f"{proxy_url}/exec", - data=payload, - method="POST", - headers={ - "Authorization": f"Bearer {capability}", - "Content-Type": "application/json", - }, - ) - try: - with urllib.request.urlopen(request, timeout=60) as response: - body = response.read(MAX_RESPONSE_BYTES + 1) - except urllib.error.HTTPError as error: - try: - detail = json.loads(error.read(4096).decode("utf-8")).get("error") - except (UnicodeDecodeError, json.JSONDecodeError, AttributeError): - detail = None - return fail(detail or f"CLI proxy returned HTTP {error.code}") - except (OSError, urllib.error.URLError): - return fail("CLI proxy unavailable") - if len(body) > MAX_RESPONSE_BYTES: - return fail("CLI proxy response exceeded its bound") - try: - result = json.loads(body.decode("utf-8")) - stdout = result.get("stdout", "") - stderr = result.get("stderr", "") - exit_code = result.get("exitCode", 1) - if not isinstance(stdout, str) or not isinstance(stderr, str) or not isinstance(exit_code, int): - raise ValueError - except (UnicodeDecodeError, json.JSONDecodeError, ValueError, AttributeError): - return fail("CLI proxy returned an invalid response") - sys.stdout.write(stdout) - sys.stderr.write(stderr) - return exit_code if 0 <= exit_code <= 255 else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/containers/enclave/mcp-server/agent-executor.js b/containers/enclave/mcp-server/agent-executor.js index cad788525..c254cc102 100644 --- a/containers/enclave/mcp-server/agent-executor.js +++ b/containers/enclave/mcp-server/agent-executor.js @@ -3,7 +3,6 @@ const { createEnclaveRunner } = require('../agent-executor/enclave-runner'); const agentWorkspace = require('../agent-executor/workspace'); const { validateEnclaveAgentRequest } = require('../agent-executor/framing'); -const { mintGithubCapability } = require('../agent-executor/github-capability'); /** * Adapters that let the unified enclave MCP server drive the audited @@ -60,23 +59,12 @@ function createAgentRequestValidator(maxPromptBytes) { */ const agentWorkspaceAdapter = { createInvocationWorkspace({ config, invocationId, privateRepo, schema, prompt }) { - const now = Math.floor(Date.now() / 1000); - const githubCapability = config.githubEnabled - ? mintGithubCapability({ - keyHex: config.githubCapabilityKey, - runId: config.githubRunIdentity, - invocationId, - repo: privateRepo, - notBefore: now, - expiresAt: now + config.timeoutSeconds + 5, - }) - : undefined; return agentWorkspace.createInvocationWorkspace({ config, invocationId, schema, task: prompt, - githubCapability, + githubAgentId: config.githubEnabled ? config.githubAgentId : undefined, }); }, readQueryOutput(outPath, maxOutputBytes) { diff --git a/containers/enclave/mcp-server/config.js b/containers/enclave/mcp-server/config.js index 94e6ebe07..8b2e50a9d 100644 --- a/containers/enclave/mcp-server/config.js +++ b/containers/enclave/mcp-server/config.js @@ -43,8 +43,7 @@ const AGENT_SUPPORTED_BACKENDS = new Set(['docker', 'gvisor']); const AGENT_SUPPORTED_ENGINES = new Set(['copilot']); const AGENT_SUPPORTED_PROFILES = new Set(['openai', 'anthropic']); const AGENT_CONTAINER_PREFIX = 'awf-enclave-agent'; -const GITHUB_CAPABILITY_FILE = '/run/awf-enclave-mcp/github-capability-key'; -const GITHUB_RUN_IDENTITY_FILE = '/run/awf-enclave-mcp/github-run-identity'; +const GITHUB_AGENT_ID_FILE = '/run/awf-enclave-mcp/github-agent-id'; function requireEnv(name) { const value = process.env[name]; @@ -201,31 +200,26 @@ function loadAgentConfig(server, files = fs) { const cpuLimit = process.env.AWF_ENCLAVE_AGENT_CPU || '1'; const githubEnabled = process.env.AWF_ENCLAVE_AGENT_GITHUB_ENABLED === 'true'; const githubProfile = process.env.AWF_ENCLAVE_AGENT_GITHUB_PROFILE; - const githubProxyUrl = process.env.AWF_ENCLAVE_AGENT_GITHUB_PROXY_URL; - const githubCapabilityKeyPath = process.env.AWF_ENCLAVE_AGENT_GITHUB_CAPABILITY_KEY_PATH; - const githubRunIdentityPath = process.env.AWF_ENCLAVE_AGENT_GITHUB_RUN_IDENTITY_PATH; - let githubCapabilityKey; - let githubRunIdentity; + const githubMcpUrl = process.env.AWF_ENCLAVE_AGENT_GITHUB_MCP_URL; + const githubAgentIdPath = process.env.AWF_ENCLAVE_AGENT_GITHUB_AGENT_ID_PATH; + const githubGatewayContainer = process.env.AWF_ENCLAVE_AGENT_GITHUB_GATEWAY_CONTAINER; + let githubAgentId; if (githubEnabled) { if (githubProfile !== 'issues-read-v1') { throw new Error('AWF_ENCLAVE_AGENT_GITHUB_PROFILE must be issues-read-v1'); } - if (!/^http:\/\/[0-9.]+:[0-9]+$/.test(githubProxyUrl || '')) { - throw new Error('AWF_ENCLAVE_AGENT_GITHUB_PROXY_URL must be a fixed IPv4 HTTP origin'); + if (!/^http:\/\/[0-9.]+:[0-9]+\/mcp\/github$/.test(githubMcpUrl || '')) { + throw new Error('AWF_ENCLAVE_AGENT_GITHUB_MCP_URL must be a fixed IPv4 MCP endpoint'); } - if (githubCapabilityKeyPath !== GITHUB_CAPABILITY_FILE) { - throw new Error('AWF_ENCLAVE_AGENT_GITHUB_CAPABILITY_KEY_PATH is not the fixed private path'); + if (githubAgentIdPath !== GITHUB_AGENT_ID_FILE) { + throw new Error('AWF_ENCLAVE_AGENT_GITHUB_AGENT_ID_PATH is not the fixed private path'); } - if (githubRunIdentityPath !== GITHUB_RUN_IDENTITY_FILE) { - throw new Error('AWF_ENCLAVE_AGENT_GITHUB_RUN_IDENTITY_PATH is not the fixed private path'); + if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{7,127}$/.test(githubGatewayContainer || '')) { + throw new Error('AWF_ENCLAVE_AGENT_GITHUB_GATEWAY_CONTAINER is invalid'); } - githubCapabilityKey = files.readFileSync(githubCapabilityKeyPath, 'utf8').trim(); - if (!/^[0-9a-f]{64}$/.test(githubCapabilityKey)) { - throw new Error('Enclave GitHub capability root is invalid'); - } - githubRunIdentity = files.readFileSync(githubRunIdentityPath, 'utf8').trim(); - if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(githubRunIdentity)) { - throw new Error('Enclave GitHub run identity is invalid'); + githubAgentId = files.readFileSync(githubAgentIdPath, 'utf8').trim(); + if (!/^[A-Za-z0-9_-]{32,128}$/.test(githubAgentId)) { + throw new Error('Enclave GitHub MCP agent identity is invalid'); } } if (!/^(?:[0-9]{1,2})(?:\.[0-9]{1,3})?$/.test(cpuLimit) || Number(cpuLimit) <= 0) { @@ -267,10 +261,10 @@ function loadAgentConfig(server, files = fs) { maxModelTokens: optionalPositiveInt('AWF_ENCLAVE_AGENT_MAX_MODEL_TOKENS'), githubEnabled, githubProfile: githubEnabled ? githubProfile : undefined, - githubProxyUrl: githubEnabled ? githubProxyUrl : undefined, - githubCapabilityKey, - githubRunIdentity, - enclaveGithubCapabilityPath: '/run/awf-enclave-github/capability', + githubMcpUrl: githubEnabled ? githubMcpUrl : undefined, + githubAgentId, + githubGatewayContainer: githubEnabled ? githubGatewayContainer : undefined, + enclaveGithubAgentIdPath: '/run/awf-enclave-github/agent-id', runLabelKey: ENCLAVE_RUN_LABEL, invocationLabelKey: ENCLAVE_INVOCATION_LABEL, containerPrefix: AGENT_CONTAINER_PREFIX, @@ -298,8 +292,7 @@ module.exports = { SEEDS_DIR, CAPABILITY_DIR, WORK_DIR, - GITHUB_CAPABILITY_FILE, - GITHUB_RUN_IDENTITY_FILE, + GITHUB_AGENT_ID_FILE, isAgentExecutorEnabled, isScriptExecutorEnabled, loadAgentConfig, diff --git a/docs/awf-config-spec.md b/docs/awf-config-spec.md index 6cab86852..91a1587b2 100644 --- a/docs/awf-config-spec.md +++ b/docs/awf-config-spec.md @@ -1813,7 +1813,7 @@ enclaves: ``` - **Script executor** — an entry keyed by `script`; launches a no-network, read-only, single-use Python sandbox. An empty `script: {}` object is valid and selects AWF's pinned defaults. -- **Agent executor** — an entry keyed by `agent`; launches a bounded single-use Copilot enclave. `agent.model` is REQUIRED. Optional `agent.github.cli: issues-read-v1` adds the PAT-free AWF CLI proxy as the only additional peer. +- **Agent executor** — an entry keyed by `agent`; launches a bounded single-use Copilot enclave. `agent.model` is REQUIRED. Optional `agent.github.cli: issues-read-v1` adds compiler-owned shared mcpg as the only additional peer. - **Entry-level controls** — `runtime`, `image`, `memoryLimit`, `cpuLimit`, `pidsLimit`, `tmpfsLimit`, `maxOutputBytes`, and `maxInvocations` apply to the entry's selected executor. `script.maxScriptBytes` and agent `maxTaskBytes`, `maxModelRequests`, and `maxModelTokens` remain executor-specific. Network and interpreter are AWF-owned invariants, not input fields. At most one entry MAY exist per executor kind, and each entry MUST declare exactly one executor key. Every entry's `repos` list is merged into one trusted repository catalog: a repository shared by both entries MUST declare the same `sensitivity`, because sensitivity fixes one shared per-run information budget that both executors debit. @@ -1852,23 +1852,20 @@ The primary agent MUST NOT receive a broker socket, wrapper binary, direct serve When the agent executor is enabled, each invocation joins only the dedicated `internal` `awf-enclave-agent` network. Its mandatory peer is the dedicated -enclave API proxy. With `issues-read-v1`, the only additional peer is an -AWF-owned PAT-free CLI proxy at `172.31.0.40:11000`. That proxy is dual-homed -onto the internal `awf-enclave-github-control` network -(`172.29.0.0/24`), where compiler-owned mcpg is attached under the fixed alias -`awf-enclave-github-proxy:18443`. mcpg never joins the enclave network. -Squid, the primary agent, general proxies, safe outputs, the MCP gateway, and -the MCP server itself remain excluded. +enclave API proxy. With `issues-read-v1`, the only additional peer is compiler-owned shared mcpg. +AWF attaches that existing container directly at `172.31.0.40` under the fixed +alias `awf-enclave-github-mcp`; the enclave uses `/mcp/github` on port 8080. +Squid, the primary agent, general proxies, safe outputs, and the enclave MCP +server itself remain excluded. The base compiler handoff from `github/gh-aw#50920` and late backend -rediscovery from `github/gh-aw-mcpg#10784` are present on current defaults: -gh-aw pins mcpg v0.4.10 and mcpg reports MCP Gateway spec 1.16.0. The base floor -remains spec 1.15.0 and a post-v0.4.8 mcpg release. - -`issues-read-v1` additionally requires the first compiler/mcpg releases with -the dedicated proxy policy, v1 token vectors, public-visibility proof, secrecy -labels, and proxy identity/readiness contract. The compiler MUST gate or pin -the first supporting AWF release. Older AWF versions reject the closed +rediscovery from `github/gh-aw-mcpg#10784` are present in mcpg v0.4.15, which +reports MCP Gateway spec 1.16.0. The base floor remains spec 1.15.0 and a +post-v0.4.8 mcpg release. + +`issues-read-v1` additionally requires compiler support for mcpg multi-agent +identities and policies, tracked by `github/gh-aw#57787`. The compiler MUST gate +or pin the first supporting AWF release. Older AWF versions reject the closed `github` field; AWF has no compatibility fallback. While the backend is still starting, mcpg may return retryable HTTP `503 backend_unavailable`. AWF retries `initialize` with bounded backoff until `AWF_ENCLAVE_MCP_READINESS_TIMEOUT_MS` expires, then fails closed before the primary agent starts. @@ -1886,44 +1883,32 @@ configuration or persist the resolved credential under `GITHUB_WORKSPACE` (or any other agent-readable path). The AWF upstream contract cannot enforce this requirement on the downstream output/converter path. -For `issues-read-v1`, the compiler supplies -`AWF_ENCLAVE_GITHUB_PROXY_CONTAINER`, -`AWF_ENCLAVE_GITHUB_PROXY_IDENTITY`, -`AWF_ENCLAVE_GITHUB_PROXY_CA_CERT`, and -`MCP_GATEWAY_ENCLAVE_CAPABILITY_KEY`. The last value MUST be exactly 32 random -bytes encoded as 64 lowercase hexadecimal characters. AWF writes it mode 0600 -beneath the private enclave run root, mounts it read-only only into -`enclave-mcp-server`, and removes it from the host environment. The proxy -identity MUST match `^[a-z0-9][a-z0-9-]{0,63}$` and mcpg policy -`workflow_run_id` byte for byte; AWF stages it in a separate mode-0600 private -file. Policy JSON reaches only mcpg; the AWF CLI proxy receives no PAT, HMAC -root, policy, or caller-configurable endpoint. - -The server mints one short-lived `awf-egh1` HMAC capability per admitted -invocation. Its canonical compact JSON field order is -`v,aud,run,inv,repo,profile,ops,nbf,exp`; the fixed sorted operations are -`issues.comments.list`, `issues.get`, and `issues.list`. The enclave receives -only that token as a read-only file. The `run` claim is the compiler proxy -identity, not AWF's independent random seed-map/container-reconciliation run -ID. Its `gh` wrapper permits only bounded -`gh api` GET calls for: - -- `repos/{owner}/{repo}/issues` -- `repos/{owner}/{repo}/issues/{number}` -- `repos/{owner}/{repo}/issues/{number}/comments` - -The `repo` claim MUST be the exact canonical lowercase `owner/repo` admitted -for that invocation. AWF MUST NOT substitute an owner-wide, wildcard, -all-private, empty, or label-shaped value and does not configure the -invocation's DIFC secrecy label. mcpg derives only the exact -`private:owner/repo` agent tag from the verified claim; public responses carry -empty secrecy and any different private repository carries its own distinct -tag, so the normal `resource secrecy subset-of agent secrecy` check rejects it. - -GraphQL, search, writes, arbitrary paths, absolute or alternate hosts, -traversal, body/input/field flags, auth/config/extensions/aliases, environment -overrides, and shell execution are rejected. Stock `gh issue` commands are not -part of v1 because they commonly use GraphQL. +For `issues-read-v1`, the compiler supplies the shared gateway contract +(`AWF_ENCLAVE_MCP_GATEWAY_CONTAINER`, `AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT`, and +`AWF_ENCLAVE_MCP_GATEWAY_IDENTITY`) plus a distinct +`AWF_ENCLAVE_GITHUB_MCP_AGENT_ID`. The compiler configures that identity in +mcpg `gateway.agentIds` and restricts it with `gateway.agentPolicies` to the +`github` server, these tools, and the trusted enclave repository catalog: + +- `list_issues` +- `issue_read` with method `get` +- `issue_read` with method `get_comments` + +AWF stores the enclave identity in a mode-0600 private file, removes it from +the host environment, and gives each invocation a read-only private copy. The +enclave sends the identity directly as `Authorization` to +`http://172.31.0.40:8080/mcp/github`. It contains no `gh` executable and fails +preflight if one is present. Before primary-agent work begins, AWF initializes +the endpoint and fails closed unless `tools/list` advertises exactly the two +tools above. + +This mcpg identity lasts for the job rather than one invocation. Its repository +policy therefore covers the union of trusted repositories configured for the +enclave agent; it is not independently expired, revoked, or narrowed to the +repository assigned to a particular invocation. This is an explicit tradeoff +of direct shared-mcpg connectivity. AWF's per-invocation process, seed, +admission, shared-ledger debit, finite output schema, and timing controls remain +in force. ### 14.4 Shared ledger and disclosure diff --git a/docs/enclaves-architecture.md b/docs/enclaves-architecture.md index f3435b164..60fe8a8be 100644 --- a/docs/enclaves-architecture.md +++ b/docs/enclaves-architecture.md @@ -9,7 +9,7 @@ Layer 5 establishes one `enclaves` subsystem, one AWF-owned MCP server, and mcpg AWF stages immutable repository seeds on the host, starts one AWF-owned `enclave-mcp-server`, and exposes enabled executors only through `gh-aw-mcpg`. - **Script executor** — `enclave_run_script` runs a bounded Python script in a no-network, read-only, single-use sandbox. -- **Agent executor** — `enclave_run_agent` runs the pinned Copilot engine in a bounded single-use enclave. Its mandatory peer is the dedicated API proxy; `agent.github.cli: issues-read-v1` adds only the PAT-free AWF CLI proxy. +- **Agent executor** — `enclave_run_agent` runs the pinned Copilot engine in a bounded single-use enclave. Its mandatory peer is the dedicated API proxy; `agent.github.cli: issues-read-v1` also permits a direct connection to compiler-owned shared mcpg. - **Shared controls** — the `repos` lists of the `enclaves` entries form the only trusted repository catalog; script and agent calls debit the same per-run repository ledger and share one admission lane. The primary agent never receives a broker socket, wrapper binary, direct MCP server URL, capability, repository seed, ledger state, or alternate transport. @@ -45,19 +45,18 @@ error immediately instead of entering an unbounded fixed-timing queue. - `enclave-mcp-server` joins only the private `awf-enclave-mcp-control` network. - The compiler launches `gh-aw-mcpg`, labels it for the run, and gives AWF the gateway identity plus the private `/mcp/awf-enclave` endpoint. - The server is reachable **only** through that gateway. AWF never publishes the server on a host port and never hands the primary agent a direct route. -- When the agent executor is enabled, each invocation joins only the private `awf-enclave-agent` network. Its steady-state peers are the dedicated API proxy and, only for `issues-read-v1`, the PAT-free AWF CLI proxy. -- The CLI proxy is dual-homed on `awf-enclave-agent` and the internal `awf-enclave-github-control` network. Compiler-owned mcpg joins only the latter under `awf-enclave-github-proxy:18443`; the enclave has no direct mcpg route. +- When the agent executor is enabled, each invocation joins only the private `awf-enclave-agent` network. Its steady-state peers are the dedicated API proxy and, only for `issues-read-v1`, compiler-owned shared mcpg. +- AWF attaches the existing mcpg container directly to that network at `172.31.0.40` with alias `awf-enclave-github-mcp`. No AWF bridge, GitHub CLI, Squid, primary agent, general API proxy, safe-output service, or other peer joins the network. -The base MCP handoff and late backend rediscovery are present on current defaults: -gh-aw pins mcpg v0.4.10, which reports MCP Gateway spec 1.16.0. The earlier -minimum remains spec 1.15.0 and a post-v0.4.8 mcpg release. +The base MCP handoff and late backend rediscovery are present in mcpg v0.4.15, +which reports MCP Gateway spec 1.16.0. The earlier minimum remains spec 1.15.0 +and a post-v0.4.8 mcpg release. -The optional GitHub path additionally requires the first compiler and mcpg -releases implementing `issues-read-v1`, the canonical `awf-egh1` capability, -public-visibility proof, secrecy labels, and the dedicated proxy identity -handoff. The compiler MUST pin or minimum-version-gate the first supporting AWF -release. Older AWF versions reject the closed `github` field; there is no -permissive fallback. +The optional GitHub path additionally requires compiler support for mcpg +multi-agent identities and policies (tracked by `github/gh-aw#57787`). The +compiler MUST pin or minimum-version-gate the first supporting AWF release. +Older AWF versions reject the closed `github` field; there is no permissive +fallback. The compiler-generated upstream uses `connectTimeout: 120` and `toolTimeout: 4860`, covering the maximum 4800-second disclosure bucket, up to @@ -97,7 +96,7 @@ lane, reconciles labelled enclaves, and exits before AWF preserves audit artifacts and disconnects mcpg from the private control network. AWF never stops or removes the externally owned mcpg container. -## Optional issues-read-v1 GitHub access +## Optional `issues-read-v1` GitHub access The profile is disabled by default and configured only on an agent entry: @@ -108,46 +107,41 @@ agent: cli: issues-read-v1 ``` -The compiler provides `AWF_ENCLAVE_GITHUB_PROXY_CONTAINER`, -`AWF_ENCLAVE_GITHUB_PROXY_IDENTITY`, `AWF_ENCLAVE_GITHUB_PROXY_CA_CERT`, and a -64-character lowercase hexadecimal `MCP_GATEWAY_ENCLAVE_CAPABILITY_KEY`. -The identity is the compiler-normalized capability run claim -`gh-aw-egh-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${JOB_HASH}` and matches -mcpg policy `workflow_run_id` byte for byte. AWF writes the identity and HMAC -root to separate mode-0600 private files mounted only into -`enclave-mcp-server`, then removes the root from its host environment. The -compiler passes policy JSON only to mcpg. The AWF CLI proxy has neither the PAT -nor the HMAC root. - -For every admitted invocation, the MCP server mints -`awf-egh1..`. The compact JSON payload fields -are ordered exactly as `v,aud,run,inv,repo,profile,ops,nbf,exp`; the HMAC input -is ASCII `awf-egh1.` followed by the payload encoding. The token is bound to the -run, invocation, assigned repository, fixed profile, fixed sorted operation -set, and deadline, and is mounted read-only into only that single-use enclave. -The payload `run` is the compiler proxy identity, not AWF's independent random -seed-map/container-reconciliation run ID. -The payload `repo` is the exact canonical lowercase `owner/repo` admitted from -the enclave repository catalog. AWF never widens it to an owner, wildcard, or -all-private scope and never sends a DIFC secrecy label. mcpg alone derives the -invocation agent's exact `private:owner/repo` secrecy tag from the verified -claim, labels public response data with empty secrecy, and labels other private -repositories with their own distinct repository tag. - -The enclave-visible `gh` wrapper accepts only `gh api` GET requests for issue -list, issue get, and issue comments REST paths. It rejects GraphQL, search, -writes, absolute URLs, alternate hosts, traversal, body flags, auth/config/ -extension/alias commands, arbitrary endpoints, and environment overrides. -Stock `gh issue list` and `gh issue view --comments` are not supported because -they commonly use GraphQL. - -mcpg validates single-use capabilities, injects its PAT, permits the assigned -repository or a currently proven-public repository, and attaches the -authoritative secrecy label. GitHub response data remains inside the enclave. +The compiler provides the shared gateway handoff +`AWF_ENCLAVE_MCP_GATEWAY_CONTAINER`, `AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT`, and +`AWF_ENCLAVE_MCP_GATEWAY_IDENTITY`, plus a distinct +`AWF_ENCLAVE_GITHUB_MCP_AGENT_ID`. It configures that enclave identity in +mcpg's `gateway.agentIds` and `gateway.agentPolicies`, allowing only the +`github` server, the `list_issues` and `issue_read` tools, and the repositories +from the trusted enclave catalog. The primary agent never receives the enclave +identity. + +AWF validates and stages the enclave identity in a mode-0600 private file, +removes it from the host environment, and copies it into each invocation's +private workspace. The single-use enclave mounts that copy read-only and sends +it directly as the `Authorization` value to +`http://172.31.0.40:8080/mcp/github`. AWF initializes a session through that +endpoint before primary-agent work begins and requires the advertised tool set +to be exactly `list_issues` and `issue_read`. + +The mcpg identity is job-lifetime, not per-invocation. Consequently, mcpg +enforces the union of repositories configured for the enclave agent rather than +binding the credential to one invocation's assigned repository, and the +identity is not independently expired or revoked after each invocation. This +weaker lifetime and repository scope is an explicit tradeoff of direct shared +mcpg access; AWF still isolates each identity file and enclave process and +retains its own per-invocation seed, admission, ledger, output-schema, and +timing controls. + +The enclave image contains no `gh` executable and fails preflight if one is +available. Copilot receives one invocation-private MCP configuration exposing +only the compiler-policy-limited direct mcpg endpoint. mcpg injects its GitHub +credential and enforces the configured tools and repositories. GitHub response +data remains inside the enclave. Only the existing finite-schema result, shared ledger debit, and timing bucket can return to the primary agent. Shutdown drains admissions, removes labelled -enclaves, stops the PAT-free proxy, preserves private audit, disconnects -compiler-owned mcpg, and then removes private state. +enclaves, disconnects compiler-owned mcpg from the enclave network, and then +removes private state. AWF never stops or removes the shared gateway container. ## Coverage after legacy smoke removal diff --git a/src/artifact-preservation.ts b/src/artifact-preservation.ts index de1138142..5784dd8f8 100644 --- a/src/artifact-preservation.ts +++ b/src/artifact-preservation.ts @@ -17,43 +17,6 @@ const ENCLAVE_AUDIT_FILES = [ { source: 'enclave.jsonl', destination: 'enclave.jsonl' }, { source: 'runtime-telemetry.jsonl', destination: 'enclave-runtime.jsonl' }, ] as const; -const MAX_ENCLAVE_GITHUB_AUDIT_BYTES = 10 * 1024 * 1024; - -function copyRegularFileNoFollow(source: string, destination: string): void { - const sourceFd = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); - try { - const stat = fs.fstatSync(sourceFd); - if (!stat.isFile() || stat.size > MAX_ENCLAVE_GITHUB_AUDIT_BYTES) { - throw new Error('Enclave GitHub CLI audit must be a bounded regular file'); - } - - const destinationFd = fs.openSync( - destination, - fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_NOFOLLOW, - 0o600, - ); - try { - const buffer = Buffer.allocUnsafe(Math.min(stat.size, 64 * 1024)); - let remaining = stat.size; - let bytesRead: number; - while ( - remaining > 0 - && (bytesRead = fs.readSync(sourceFd, buffer, 0, Math.min(buffer.length, remaining), null)) > 0 - ) { - let offset = 0; - while (offset < bytesRead) { - offset += fs.writeSync(destinationFd, buffer, offset, bytesRead - offset, null); - } - remaining -= bytesRead; - } - } finally { - fs.closeSync(destinationFd); - } - } finally { - fs.closeSync(sourceFd); - } -} - /** * Copies the iptables audit dump from the init-signal volume to the audit directory. * Must be called BEFORE stopContainers() because `docker compose down -v` destroys @@ -62,13 +25,12 @@ function copyRegularFileNoFollow(source: string, destination: string): void { export function preserveIptablesAudit( workDir: string, auditDir?: string, - requireEnclaveGithubAudit = false, ): boolean { const iptablesAuditSrc = path.join(workDir, 'init-signal', 'iptables-audit.txt'); const enclavePaths = resolveEnclavePaths(workDir); const enclaveRoot = enclavePaths.root; const targetAuditDir = auditDir || path.join(workDir, 'audit'); - if (!fs.existsSync(targetAuditDir)) return !requireEnclaveGithubAudit; + if (!fs.existsSync(targetAuditDir)) return true; let complete = true; if (fs.existsSync(iptablesAuditSrc)) { @@ -81,24 +43,6 @@ export function preserveIptablesAudit( } } - const githubCliAuditSrc = path.join(enclavePaths.githubCliProxyLogsDir, 'access.jsonl'); - try { - const destination = path.join(targetAuditDir, 'enclave-github-cli-access.jsonl'); - copyRegularFileNoFollow(githubCliAuditSrc, destination); - fs.chmodSync(destination, 0o600); - logger.debug('Copied enclave GitHub CLI audit to audit directory'); - } catch (error: unknown) { - if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { - if (requireEnclaveGithubAudit) { - complete = false; - logger.debug('Required enclave GitHub CLI audit was not available for preservation'); - } - } else { - complete = false; - logger.debug('Could not copy enclave GitHub CLI audit:', error); - } - } - if (fs.existsSync(enclaveRoot)) { for (const auditFile of ENCLAVE_AUDIT_FILES) { try { diff --git a/src/commands/main-action.test.ts b/src/commands/main-action.test.ts index 16efa83d1..cf72eba63 100644 --- a/src/commands/main-action.test.ts +++ b/src/commands/main-action.test.ts @@ -836,21 +836,13 @@ describe('createMainAction', () => { expect(mockedDockerManager.preserveIptablesAudit).toHaveBeenCalledWith( MAIN_ACTION_STUB_CONFIG.workDir, - MAIN_ACTION_STUB_CONFIG.auditDir, - false + MAIN_ACTION_STUB_CONFIG.auditDir ); expect(mockedEnclaveGateway.shutdownEnclaveGateway).toHaveBeenCalledWith( MAIN_ACTION_STUB_CONFIG ); - expect(mockedEnclaveGithubGateway.shutdownEnclaveGithubCliProxy) - .toHaveBeenCalledWith(MAIN_ACTION_STUB_CONFIG); expect( mockedEnclaveGateway.shutdownEnclaveGateway.mock.invocationCallOrder[0] - ).toBeLessThan( - mockedEnclaveGithubGateway.shutdownEnclaveGithubCliProxy.mock.invocationCallOrder[0] - ); - expect( - mockedEnclaveGithubGateway.shutdownEnclaveGithubCliProxy.mock.invocationCallOrder[0] ).toBeLessThan( mockedDockerManager.preserveIptablesAudit.mock.invocationCallOrder[0] ); diff --git a/src/commands/main-action.ts b/src/commands/main-action.ts index 944039e87..38717122e 100644 --- a/src/commands/main-action.ts +++ b/src/commands/main-action.ts @@ -39,7 +39,6 @@ import { assertEnclaveGithubGatewayReady, connectEnclaveGithubGateway, disconnectEnclaveGithubGateway, - shutdownEnclaveGithubCliProxy, } from '../enclave/github-gateway'; import type { WrapperConfig } from '../types'; @@ -134,7 +133,6 @@ function buildCleanupFn( // until the subsequent compose down removes them. if (getContainersStarted()) { let enclaveAuditComplete = true; - const enclaveGithubEnabled = config.enclaves?.executors.agent.github?.cli === 'issues-read-v1'; try { await shutdownEnclaveGateway(config); } catch (error) { @@ -144,19 +142,9 @@ function buildCleanupFn( error, ); } - try { - await shutdownEnclaveGithubCliProxy(config); - } catch (error) { - enclaveAuditComplete = false; - logger.warn( - 'Enclave GitHub CLI proxy did not stop cleanly before audit preservation.', - error, - ); - } if (preserveIptablesAudit( config.workDir, config.auditDir, - enclaveGithubEnabled, ) === false) { enclaveAuditComplete = false; logger.warn('One or more protected enclave audit artifacts could not be preserved.'); @@ -166,7 +154,7 @@ function buildCleanupFn( } catch (error) { enclaveAuditComplete = false; logger.warn( - 'Compiler-owned enclave GitHub proxy could not be disconnected cleanly.', + 'Compiler-owned shared MCP gateway could not be disconnected cleanly.', error, ); } diff --git a/src/compose-generator.ts b/src/compose-generator.ts index 9c2293aec..09fe98268 100644 --- a/src/compose-generator.ts +++ b/src/compose-generator.ts @@ -19,8 +19,6 @@ import { ENCLAVE_AGENT_EGRESS_NETWORK, ENCLAVE_AGENT_NETWORK, ENCLAVE_AGENT_SUBNET, - ENCLAVE_GITHUB_CONTROL_SUBNET, - ENCLAVE_GITHUB_CONTROL_NETWORK, ENCLAVE_MCP_CONTROL_NETWORK, } from './enclave/network'; import { buildInternalServiceHosts } from './services/internal-service-hosts'; @@ -217,16 +215,6 @@ export function generateDockerCompose( name: ENCLAVE_AGENT_EGRESS_NETWORK, driver: 'bridge', }; - if (config.enclaves.executors.agent.github?.cli === 'issues-read-v1') { - compose.networks[ENCLAVE_GITHUB_CONTROL_NETWORK] = { - name: ENCLAVE_GITHUB_CONTROL_NETWORK, - driver: 'bridge', - internal: true, - ipam: { - config: [{ subnet: ENCLAVE_GITHUB_CONTROL_SUBNET }], - }, - }; - } } if (config.enclaves?.enabled) { compose.networks[ENCLAVE_MCP_CONTROL_NETWORK] = { diff --git a/src/constants.ts b/src/constants.ts index 167c30326..abe508483 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,7 +11,6 @@ export const DOH_PROXY_CONTAINER_NAME = 'awf-doh-proxy'; export const CLI_PROXY_CONTAINER_NAME = 'awf-cli-proxy'; export const ENCLAVE_MCP_SERVER_CONTAINER_NAME = 'awf-enclave-mcp-server'; export const ENCLAVE_AGENT_API_PROXY_CONTAINER_NAME = 'awf-enclave-agent-api-proxy'; -export const ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME = 'awf-enclave-agent-cli-proxy'; export const LOCAL_ENCLAVE_MCP_SERVER_IMAGE = 'awf-enclave-mcp-server:local'; export const INIT_SIGNAL_DIR = '/run/awf-init'; export const LEGACY_INIT_SIGNAL_DIR = '/tmp/awf-init'; diff --git a/src/container-lifecycle.ts b/src/container-lifecycle.ts index 724e176b2..f31695705 100644 --- a/src/container-lifecycle.ts +++ b/src/container-lifecycle.ts @@ -8,7 +8,6 @@ import { API_PROXY_CONTAINER_NAME, CLI_PROXY_CONTAINER_NAME, ENCLAVE_AGENT_API_PROXY_CONTAINER_NAME, - ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME, ENCLAVE_MCP_SERVER_CONTAINER_NAME, } from './constants'; import { getLocalDockerEnv } from './docker-host'; @@ -312,7 +311,6 @@ export async function startContainers( CLI_PROXY_CONTAINER_NAME, ENCLAVE_MCP_SERVER_CONTAINER_NAME, ENCLAVE_AGENT_API_PROXY_CONTAINER_NAME, - ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME, ], { reject: false, env: getLocalDockerEnv(), diff --git a/src/container-start.test.ts b/src/container-start.test.ts index 865ebda3b..f0d6b704e 100644 --- a/src/container-start.test.ts +++ b/src/container-start.test.ts @@ -30,7 +30,6 @@ describe('startContainers', () => { 'awf-cli-proxy', 'awf-enclave-mcp-server', 'awf-enclave-agent-api-proxy', - 'awf-enclave-agent-cli-proxy', ], expect.objectContaining({ reject: false }) ); diff --git a/src/docker-manager-diagnostics.test.ts b/src/docker-manager-diagnostics.test.ts index 17ff8f0df..b223967e8 100644 --- a/src/docker-manager-diagnostics.test.ts +++ b/src/docker-manager-diagnostics.test.ts @@ -138,44 +138,6 @@ describe('docker-manager diagnostics', () => { expect(fs.existsSync(path.join(auditDir, 'iptables-audit.txt'))).toBe(false); }); - it('preserves the required GitHub CLI audit independently of iptables audit', () => { - const paths = resolveEnclavePaths(getDir()); - fs.mkdirSync(paths.githubCliProxyLogsDir, { recursive: true }); - fs.writeFileSync(path.join(paths.githubCliProxyLogsDir, 'access.jsonl'), '{"event":"exec_done"}\n'); - const auditDir = path.join(getDir(), 'audit'); - fs.mkdirSync(auditDir, { recursive: true }); - mockExecaSync.mockReturnValue({ exitCode: 0, stdout: '', stderr: '' }); - - expect(preserveIptablesAudit(getDir(), auditDir, true)).toBe(true); - - expect(fs.readFileSync( - path.join(auditDir, 'enclave-github-cli-access.jsonl'), - 'utf8', - )).toBe('{"event":"exec_done"}\n'); - fs.rmSync(paths.root, { recursive: true, force: true }); - }); - - it('reports an incomplete protected audit when the required CLI audit is absent', () => { - const auditDir = path.join(getDir(), 'audit'); - fs.mkdirSync(auditDir, { recursive: true }); - - expect(preserveIptablesAudit(getDir(), auditDir, true)).toBe(false); - }); - - it('refuses a symlinked enclave GitHub CLI audit', () => { - const paths = resolveEnclavePaths(getDir()); - const sensitiveFile = path.join(getDir(), 'sensitive-file'); - fs.mkdirSync(paths.githubCliProxyLogsDir, { recursive: true }); - fs.writeFileSync(sensitiveFile, 'must not be copied'); - fs.symlinkSync(sensitiveFile, path.join(paths.githubCliProxyLogsDir, 'access.jsonl')); - const auditDir = path.join(getDir(), 'audit'); - fs.mkdirSync(auditDir, { recursive: true }); - - expect(preserveIptablesAudit(getDir(), auditDir, true)).toBe(false); - expect(fs.existsSync(path.join(auditDir, 'enclave-github-cli-access.jsonl'))).toBe(false); - fs.rmSync(paths.root, { recursive: true, force: true }); - }); - it('should do nothing when target audit directory does not exist', () => { const initSignalDir = path.join(getDir(), 'init-signal'); fs.mkdirSync(initSignalDir, { recursive: true }); diff --git a/src/enclave/agent-entrypoint-diagnostics.test.ts b/src/enclave/agent-entrypoint-diagnostics.test.ts index 5e48ee584..268d9a91d 100644 --- a/src/enclave/agent-entrypoint-diagnostics.test.ts +++ b/src/enclave/agent-entrypoint-diagnostics.test.ts @@ -25,6 +25,7 @@ spec.loader.exec_module(module) root = Path(os.environ["HARNESS_ROOT"]) scenario = os.environ["SCENARIO"] +os.environ["PATH"] = os.environ["TEST_PATH"] module.SEED_DIR = root / "seed" module.TASK_PATH = root / "task.txt" module.SCHEMA_PATH = root / "schema.json" @@ -34,6 +35,23 @@ module.AGENT_DIR = root / "agent" module.TEMP_DIR = root / "tmp" module.SHARED_MEMORY_DIR = root / "shm" module.COPILOT_BIN = str(root / "copilot") +module.GITHUB_AGENT_ID_PATH = root / "github-agent-id" +module.GITHUB_MCP_CONFIG_PATH = module.AGENT_DIR / "github-mcp.json" + +if scenario == "github-config": + module.AGENT_DIR.mkdir() + module.GITHUB_AGENT_ID_PATH.write_text( + os.environ["TEST_GITHUB_AGENT_ID"], + encoding="ascii", + ) + module.configure_github_mcp() + print(json.dumps({ + "exitCode": 0, + "transcript": module.redact_diagnostics(os.environ["TEST_GITHUB_AGENT_ID"]), + "transcriptBytes": 0, + "output": module.GITHUB_MCP_CONFIG_PATH.read_text(encoding="utf-8"), + })) + raise SystemExit(0) if scenario == "bounds": module.SESSION_LOG_PATH.write_text("", encoding="utf-8") @@ -84,6 +102,11 @@ if scenario != "missing-copilot": ) copilot.chmod(0o644 if scenario == "non-executable-copilot" else 0o755) +if scenario == "unexpected-gh": + gh = root / "gh" + gh.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + gh.chmod(0o755) + if scenario == "missing-seed": module.SEED_DIR.rmdir() @@ -133,10 +156,17 @@ function runHarness(scenario: string): HarnessResult { PRIVATE_TASK: 'private prompt sentinel', PRIVATE_PATH: '/private/repository/secret-path', TEST_API_TOKEN: 'test-secret-token-value', + TEST_GITHUB_AGENT_ID: 'enclaveAgentId0123456789abcdef012345', + TEST_PATH: `${root}:/usr/bin:/bin`, AWF_ENCLAVE_AGENT_ENGINE: 'copilot', AWF_ENCLAVE_AGENT_MAX_OUTPUT_BYTES: '1024', AWF_ENCLAVE_AGENT_DEADLINE_SECONDS: scenario === 'timeout' ? '1' : '5', AWF_ENCLAVE_AGENT_MODEL: 'test-model', + ...(scenario === 'github-config' ? { + AWF_ENCLAVE_AGENT_GITHUB_ENABLED: 'true', + AWF_ENCLAVE_AGENT_GITHUB_PROFILE: 'issues-read-v1', + AWF_ENCLAVE_AGENT_GITHUB_MCP_URL: 'http://172.31.0.40:8080/mcp/github', + } : {}), }, }); if (result.status !== 0) { @@ -175,6 +205,37 @@ describe('enclave agent protected entrypoint diagnostics', () => { .toEqual({ event: 'failure', category: 'engine-failed' }); }); + it('fails closed when a gh executable is available', () => { + const result = runHarness('unexpected-gh'); + const transcript = events(result); + + expect(result.exitCode).toBe(24); + expect(transcript).toContainEqual({ + event: 'preflight', + path: 'github-cli', + exists: true, + type: 'forbidden-executable', + }); + }); + + it('writes a private MCP-only configuration and redacts its agent identity', () => { + const result = runHarness('github-config'); + const config = JSON.parse(result.output); + + expect(config).toEqual({ + mcpServers: { + github: { + type: 'http', + url: 'http://172.31.0.40:8080/mcp/github', + headers: { + Authorization: 'enclaveAgentId0123456789abcdef012345', + }, + }, + }, + }); + expect(result.transcript).toBe('[REDACTED]'); + }); + it('identifies a missing working directory without logging its path', () => { const result = runHarness('missing-seed'); const transcript = events(result); diff --git a/src/enclave/agent-runner-spec.test.ts b/src/enclave/agent-runner-spec.test.ts index 0d41bc382..48da82107 100644 --- a/src/enclave/agent-runner-spec.test.ts +++ b/src/enclave/agent-runner-spec.test.ts @@ -22,12 +22,6 @@ const { loadAgentConfig, loadServerConfig } = require(path.join( 'mcp-server', 'config.js', )); -const { mintGithubCapability } = require(path.join( - containersRoot, - 'enclave', - 'agent-executor', - 'github-capability.js', -)); const { agentWorkspaceAdapter } = require(path.join( containersRoot, 'enclave', @@ -62,6 +56,7 @@ const trustedConfig = { runLabelKey: ENCLAVE_RUN_LABEL, invocationLabelKey: ENCLAVE_INVOCATION_LABEL, containerPrefix: 'awf-enclave-agent', + githubGatewayContainer: 'awmg-mcpg', }; describe('unified enclave agent runner specification', () => { @@ -113,14 +108,14 @@ describe('unified enclave agent runner specification', () => { expect(spec.launchArgs).toContain('--entrypoint'); }); - it('mounts only the invocation capability for issues-read-v1', () => { + it('mounts only the gateway agent identity for issues-read-v1', () => { const githubSpec = deriveEnclaveContainerSpec({ config: { ...trustedConfig, githubEnabled: true, githubProfile: 'issues-read-v1', - githubProxyUrl: 'http://172.31.0.40:11000', - enclaveGithubCapabilityPath: '/run/awf-enclave-github/capability', + githubMcpUrl: 'http://172.31.0.40:8080/mcp/github', + enclaveGithubAgentIdPath: '/run/awf-enclave-github/agent-id', }, runId: 'abcdef1234567890', invocationId: '0123456789abcdef', @@ -128,10 +123,10 @@ describe('unified enclave agent runner specification', () => { }); expect(githubSpec.launchArgs).toEqual(expect.arrayContaining([ '--env', 'AWF_ENCLAVE_AGENT_GITHUB_PROFILE=issues-read-v1', - '--env', 'AWF_ENCLAVE_AGENT_GITHUB_PROXY_URL=http://172.31.0.40:11000', + '--env', 'AWF_ENCLAVE_AGENT_GITHUB_MCP_URL=http://172.31.0.40:8080/mcp/github', '-v', - '/daemon/private/enclave/work/0123456789abcdef/github-capability:' + - '/run/awf-enclave-github/capability:ro', + '/daemon/private/enclave/work/0123456789abcdef/github-agent-id:' + + '/run/awf-enclave-github/agent-id:ro', ])); expect(githubSpec.launchArgs.join(' ')).not.toMatch(/MCP_GATEWAY|githubCapabilityKey|GH_TOKEN/); }); @@ -218,7 +213,7 @@ describe('unified enclave agent runner specification', () => { await expect(runner.assertAvailable()).resolves.toBeUndefined(); }); - it('requires the PAT-free CLI proxy as the second steady-state peer when enabled', async () => { + it('requires shared mcpg as the second steady-state peer when enabled', async () => { const runner = createEnclaveRunner( { ...trustedConfig, backend: 'docker', githubEnabled: true }, { @@ -229,7 +224,7 @@ describe('unified enclave agent runner specification', () => { stdout: args[0] === 'network' ? 'true|bridge|172.31.0.0/24,|' + 'awf-enclave-agent-api-proxy@172.31.0.30/24,' + - 'awf-enclave-agent-cli-proxy@172.31.0.40/24,' + 'awmg-mcpg@172.31.0.40/24,' : '[]', stderr: '', }), @@ -267,49 +262,11 @@ describe('unified enclave agent runner specification', () => { expect(calls.some((args) => args[0] === 'run')).toBe(false); }); - describe('issues-read-v1 capability serialization', () => { - it('matches the canonical mcpg v1 vector byte for byte', () => { - expect(mintGithubCapability({ - keyHex: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', - runId: 'run-123', - invocationId: 'inv-456', - repo: 'octo/private', - notBefore: 1787594400, - expiresAt: 1787594520, - })).toBe( - 'awf-egh1.' + - 'eyJ2IjoxLCJhdWQiOiJnaC1hdy1lbmNsYXZlLWdpdGh1YiIsInJ1biI6InJ1bi0xMjMiLCJpbnYiOiJpbnYtNDU2IiwicmVwbyI6Im9jdG8vcHJpdmF0ZSIsInByb2ZpbGUiOiJpc3N1ZXMtcmVhZC12MSIsIm9wcyI6WyJpc3N1ZXMuY29tbWVudHMubGlzdCIsImlzc3Vlcy5nZXQiLCJpc3N1ZXMubGlzdCJdLCJuYmYiOjE3ODc1OTQ0MDAsImV4cCI6MTc4NzU5NDUyMH0.' + - '6fDSD-uxmi_fCZMOFmSuckdNBGx5qcWMI1HCgoXtA3o', - ); - }); - - it.each([ - { keyHex: 'A'.repeat(64) }, - { repo: 'Octo/private' }, - { repo: '' }, - { repo: 'octo' }, - { repo: 'octo/*' }, - { repo: 'private:octo/private' }, - { runId: '../run' }, - { notBefore: -1 }, - { expiresAt: 1787594399 }, - ])('rejects noncanonical signer input (%j)', (override) => { - expect(() => mintGithubCapability({ - keyHex: '0'.repeat(64), - runId: 'run-123', - invocationId: 'inv-456', - repo: 'octo/private', - notBefore: 1787594400, - expiresAt: 1787594520, - ...override, - })).toThrow(); - }); - - it('uses the compiler identity and exact assigned repository as capability claims', () => { - const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-capability-run-')); + describe('issues-read-v1 gateway identity', () => { + it('copies only the compiler-issued enclave identity into an invocation', () => { + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-gateway-identity-')); try { - const seedRunId = '0123456789abcdef0123456789abcdef'; - const githubRunIdentity = 'gh-aw-egh-123456-1-abcdef123456'; + const githubAgentId = 'enclaveAgentId0123456789abcdef012345'; const layout = agentWorkspaceAdapter.createInvocationWorkspace({ config: { workDir, @@ -318,26 +275,16 @@ describe('unified enclave agent runner specification', () => { enclaveGid: process.getgid?.() ?? 0, timeoutSeconds: 120, githubEnabled: true, - githubCapabilityKey: '0'.repeat(64), - githubRunIdentity, + githubAgentId, }, - runId: seedRunId, + runId: '0123456789abcdef0123456789abcdef', invocationId: 'abcdef1234567890', privateRepo: 'octo/private', schema: { type: 'object' }, prompt: 'read one issue', }); - const token = fs.readFileSync(layout.githubCapabilityPath, 'utf8').trim(); - const payload = JSON.parse( - Buffer.from(token.split('.')[1], 'base64url').toString('utf8'), - ); - expect(payload.run).toBe(githubRunIdentity); - expect(payload.run).not.toBe(seedRunId); - expect(payload.repo).toBe('octo/private'); - expect(Object.keys(payload)).toEqual([ - 'v', 'aud', 'run', 'inv', 'repo', 'profile', 'ops', 'nbf', 'exp', - ]); - expect(JSON.stringify(payload)).not.toMatch(/private:|secrecy|label/i); + expect(fs.readFileSync(layout.githubAgentIdPath, 'utf8').trim()).toBe(githubAgentId); + expect(fs.statSync(layout.githubAgentIdPath).mode & 0o777).toBe(0o400); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } @@ -416,24 +363,20 @@ describe('unified enclave agent server configuration', () => { expect(() => loadAgentConfig(server)).toThrow(); }); - it('loads the capability root and compiler run identity only from fixed private files', () => { + it('loads the compiler-issued GitHub MCP identity only from its fixed private file', () => { setEnv({ AWF_ENCLAVE_AGENT_GITHUB_ENABLED: 'true', AWF_ENCLAVE_AGENT_GITHUB_PROFILE: 'issues-read-v1', - AWF_ENCLAVE_AGENT_GITHUB_PROXY_URL: 'http://172.31.0.40:11000', - AWF_ENCLAVE_AGENT_GITHUB_CAPABILITY_KEY_PATH: - '/run/awf-enclave-mcp/github-capability-key', - AWF_ENCLAVE_AGENT_GITHUB_RUN_IDENTITY_PATH: - '/run/awf-enclave-mcp/github-run-identity', + AWF_ENCLAVE_AGENT_GITHUB_MCP_URL: 'http://172.31.0.40:8080/mcp/github', + AWF_ENCLAVE_AGENT_GITHUB_GATEWAY_CONTAINER: 'awmg-mcpg', + AWF_ENCLAVE_AGENT_GITHUB_AGENT_ID_PATH: + '/run/awf-enclave-mcp/github-agent-id', }); const config = loadAgentConfig(server, { - readFileSync: (target: string) => target.endsWith('github-capability-key') - ? '0'.repeat(64) - : 'gh-aw-egh-123456-1-abcdef123456', + readFileSync: () => 'enclaveAgentId0123456789abcdef012345', }); - expect(config.githubCapabilityKey).toBe('0'.repeat(64)); - expect(config.githubRunIdentity).toBe('gh-aw-egh-123456-1-abcdef123456'); - expect(process.env.AWF_ENCLAVE_GITHUB_PROXY_IDENTITY).toBeUndefined(); + expect(config.githubAgentId).toBe('enclaveAgentId0123456789abcdef012345'); + expect(config.githubGatewayContainer).toBe('awmg-mcpg'); }); it('requires an AWF capability before serving either executor', () => { diff --git a/src/enclave/github-gateway.test.ts b/src/enclave/github-gateway.test.ts index 52dafb104..e84a39f15 100644 --- a/src/enclave/github-gateway.test.ts +++ b/src/enclave/github-gateway.test.ts @@ -1,21 +1,25 @@ import * as fs from 'fs'; +import * as http from 'http'; import * as os from 'os'; import * as path from 'path'; import execa from 'execa'; import { normalizeEnclavesConfig } from '../parsers/enclave-parser'; import type { WrapperConfig } from '../types'; import { - ENCLAVE_GITHUB_PROXY_RUN_LABEL, + ENCLAVE_GITHUB_MCP_AGENT_ID_ENV, + assertEnclaveGithubGatewayReady, connectEnclaveGithubGateway, disconnectEnclaveGithubGateway, enclaveGithubGatewayTestHelpers, resolveEnclaveGithubGatewayContract, - shutdownEnclaveGithubCliProxy, } from './github-gateway'; +import { resolveEnclavePaths } from './paths'; jest.mock('execa', () => ({ __esModule: true, default: jest.fn() })); const mockExeca = execa as unknown as jest.Mock; +const AGENT_ID = 'enclaveAgentId0123456789abcdef012345'; + function enabledConfig(): WrapperConfig { return { workDir: '/tmp/awf-test', @@ -29,141 +33,254 @@ function enabledConfig(): WrapperConfig { } as WrapperConfig; } -function network(members = ['awf-enclave-agent-cli-proxy', 'compiler-mcpg']): string { +function handoff(): NodeJS.ProcessEnv { + return { + AWF_ENCLAVE_MCP_GATEWAY_CONTAINER: 'awmg-mcpg', + AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT: 'http://localhost:8080/mcp/awf-enclave', + AWF_ENCLAVE_MCP_GATEWAY_IDENTITY: 'gh-aw-123456-1-job', + [ENCLAVE_GITHUB_MCP_AGENT_ID_ENV]: AGENT_ID, + }; +} + +async function withGatewayServer( + handler: http.RequestListener, + task: (endpoint: string) => Promise, +): Promise { + const server = http.createServer(handler); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', resolve); + }); + try { + const address = server.address(); + if (!address || typeof address === 'string') throw new Error('Test gateway did not bind TCP'); + await task(`http://127.0.0.1:${address.port}/mcp/awf-enclave`); + } finally { + await new Promise((resolve, reject) => server.close(error => ( + error ? reject(error) : resolve() + ))); + } +} + +function network(members = ['awf-enclave-agent-api-proxy', 'awmg-mcpg']): string { return JSON.stringify({ Internal: true, Driver: 'bridge', - IPAM: { Config: [{ Subnet: '172.29.0.0/24' }] }, + IPAM: { Config: [{ Subnet: '172.31.0.0/24' }] }, Containers: Object.fromEntries(members.map((Name, index) => [String(index), { Name }])), }); } -function attachedProxy(aliases = ['compiler-mcpg', 'awf-enclave-github-proxy']): string { +function attachedGateway( + aliases = ['awmg-mcpg', 'awf-enclave-github-mcp'], + ipAddress = '172.31.0.40', +): string { return JSON.stringify({ NetworkSettings: { Networks: { - 'awf-enclave-github-control': { Aliases: aliases }, + 'awf-enclave-agent': { Aliases: aliases, IPAddress: ipAddress }, }, }, }); } -describe('enclave GitHub gateway handoff', () => { - let directory: string; - let caCertPath: string; - let handoff: NodeJS.ProcessEnv; - +describe('direct enclave GitHub MCP handoff', () => { beforeEach(() => { mockExeca.mockReset(); - directory = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-github-gateway-')); - caCertPath = path.join(directory, 'ca.crt'); - fs.writeFileSync(caCertPath, 'test-ca', { mode: 0o600 }); - handoff = { - AWF_ENCLAVE_GITHUB_PROXY_CONTAINER: 'compiler-mcpg', - AWF_ENCLAVE_GITHUB_PROXY_IDENTITY: 'gh-aw-egh-123456-1-abcdef123456', - AWF_ENCLAVE_GITHUB_PROXY_CA_CERT: caCertPath, - }; }); - afterEach(() => { - fs.rmSync(directory, { recursive: true, force: true }); + it('derives the GitHub route from the shared gateway contract', () => { + const contract = resolveEnclaveGithubGatewayContract(enabledConfig(), handoff()); + expect(contract).toMatchObject({ + agentId: AGENT_ID, + containerName: 'awmg-mcpg', + identity: 'gh-aw-123456-1-job', + }); + expect(contract.endpoint.href).toBe('http://localhost:8080/mcp/github'); }); - it('accepts only the fixed compiler handoff fields', () => { - expect(resolveEnclaveGithubGatewayContract(enabledConfig(), handoff)).toEqual({ - containerName: 'compiler-mcpg', - identity: 'gh-aw-egh-123456-1-abcdef123456', - caCertPath, - }); - for (const name of Object.keys(handoff)) { - expect(() => resolveEnclaveGithubGatewayContract( - enabledConfig(), - { ...handoff, [name]: undefined }, - )).toThrow(); - } + it.each([ + 'AWF_ENCLAVE_MCP_GATEWAY_CONTAINER', + 'AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT', + 'AWF_ENCLAVE_MCP_GATEWAY_IDENTITY', + ENCLAVE_GITHUB_MCP_AGENT_ID_ENV, + ])('requires compiler handoff field %s', (name) => { expect(() => resolveEnclaveGithubGatewayContract( enabledConfig(), - { ...handoff, AWF_ENCLAVE_GITHUB_PROXY_IDENTITY: 'job_with_underscores' }, - )).toThrow(/canonical compiler capability run identity/); + { ...handoff(), [name]: undefined }, + )).toThrow(); + }); + + it('resolves the scrubbed agent identity from private staged state', () => { + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-github-agent-id-')); + const config = { ...enabledConfig(), workDir }; + const paths = resolveEnclavePaths(workDir); + try { + fs.mkdirSync(paths.runDir, { recursive: true, mode: 0o700 }); + fs.writeFileSync(paths.githubAgentIdPath, `${AGENT_ID}\n`, { mode: 0o600 }); + expect(resolveEnclaveGithubGatewayContract(config, { + ...handoff(), + [ENCLAVE_GITHUB_MCP_AGENT_ID_ENV]: undefined, + }).agentId).toBe(AGENT_ID); + } finally { + fs.rmSync(workDir, { recursive: true, force: true }); + fs.rmSync(paths.root, { recursive: true, force: true }); + } }); - it('verifies identity before attaching the external proxy', async () => { + it('attaches shared mcpg directly to the enclave network', async () => { mockExeca .mockResolvedValueOnce({ exitCode: 0, stdout: JSON.stringify({ - Name: '/compiler-mcpg', + Name: '/awmg-mcpg', State: { Running: true }, - Config: { Labels: { [ENCLAVE_GITHUB_PROXY_RUN_LABEL]: 'gh-aw-egh-123456-1-abcdef123456' } }, + Config: { Labels: { 'com.github.gh-aw.mcpg.run': 'gh-aw-123456-1-job' } }, HostConfig: { NetworkMode: 'bridge' }, }), stderr: '', }) .mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' }) .mockResolvedValueOnce({ exitCode: 0, stdout: network(), stderr: '' }) - .mockResolvedValueOnce({ exitCode: 0, stdout: attachedProxy(), stderr: '' }); + .mockResolvedValueOnce({ exitCode: 0, stdout: attachedGateway(), stderr: '' }); - await connectEnclaveGithubGateway(enabledConfig(), handoff); + await connectEnclaveGithubGateway(enabledConfig(), handoff()); expect(mockExeca.mock.calls[1][1]).toEqual([ 'network', 'connect', + '--ip', + '172.31.0.40', '--alias', - 'awf-enclave-github-proxy', - 'awf-enclave-github-control', - 'compiler-mcpg', + 'awf-enclave-github-mcp', + 'awf-enclave-agent', + 'awmg-mcpg', ]); }); it.each([ - network(['awf-enclave-agent-cli-proxy']), - network(['awf-enclave-agent-cli-proxy', 'compiler-mcpg', 'unexpected']), + network(['awf-enclave-agent-api-proxy']), + network(['awf-enclave-agent-api-proxy', 'awmg-mcpg', 'unexpected']), JSON.stringify({ Internal: false, Driver: 'bridge', - IPAM: { Config: [{ Subnet: '172.29.0.0/24' }] }, + IPAM: { Config: [{ Subnet: '172.31.0.0/24' }] }, Containers: {}, }), - ])('rejects an inexact control topology', async (inspection) => { + ])('rejects an inexact enclave network topology', async (inspection) => { mockExeca.mockResolvedValue({ exitCode: 0, stdout: inspection, stderr: '' }); await expect( - enclaveGithubGatewayTestHelpers.assertControlNetworkMembership({ - containerName: 'compiler-mcpg', - identity: 'gh-aw-egh-123456-1-abcdef123456', - caCertPath, - }), - ).rejects.toThrow(/unexpected member|fixed isolated bridge/); + enclaveGithubGatewayTestHelpers.assertAgentNetworkMembership( + resolveEnclaveGithubGatewayContract(enabledConfig(), handoff()), + ), + ).rejects.toThrow(/unexpected steady-state member|fixed isolated bridge/); }); - it('rejects an external proxy attachment without the fixed private alias', async () => { + it('rejects a shared gateway attachment without the fixed alias and address', async () => { mockExeca.mockResolvedValue({ exitCode: 0, - stdout: attachedProxy(['compiler-mcpg']), + stdout: attachedGateway(['awmg-mcpg'], '172.31.0.41'), stderr: '', }); await expect( - enclaveGithubGatewayTestHelpers.assertExternalProxyAlias({ - containerName: 'compiler-mcpg', - identity: 'gh-aw-egh-123456-1-abcdef123456', - caCertPath, - }), - ).rejects.toThrow(/fixed private alias/); + enclaveGithubGatewayTestHelpers.assertSharedGatewayAttachment( + resolveEnclaveGithubGatewayContract(enabledConfig(), handoff()), + ), + ).rejects.toThrow(/fixed enclave attachment/); }); - it('stops the PAT-free CLI proxy before audit preservation', async () => { + it('disconnects only the shared gateway from the enclave network', async () => { mockExeca.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' }); - await shutdownEnclaveGithubCliProxy(enabledConfig()); + await disconnectEnclaveGithubGateway(enabledConfig(), handoff()); expect(mockExeca).toHaveBeenCalledWith( 'docker', - ['stop', '--time', '5', 'awf-enclave-agent-cli-proxy'], + ['network', 'disconnect', '-f', 'awf-enclave-agent', 'awmg-mcpg'], expect.any(Object), ); }); - it('fails cleanup when the compiler proxy cannot be disconnected', async () => { - mockExeca.mockResolvedValue({ exitCode: 1, stdout: '', stderr: 'disconnect failed' }); - await expect( - disconnectEnclaveGithubGateway(enabledConfig(), handoff), - ).rejects.toThrow(/Failed to disconnect/); + it('initializes a session and proves the exact direct GitHub tool set', async () => { + mockExeca + .mockResolvedValueOnce({ exitCode: 0, stdout: network(), stderr: '' }) + .mockResolvedValueOnce({ exitCode: 0, stdout: attachedGateway(), stderr: '' }); + const methods: string[] = []; + await withGatewayServer((request, response) => { + const chunks: Buffer[] = []; + request.on('data', chunk => chunks.push(Buffer.from(chunk))); + request.on('end', () => { + expect(request.headers.authorization).toBe(AGENT_ID); + const message = JSON.parse(Buffer.concat(chunks).toString('utf8')); + methods.push(message.method); + if (message.method === 'initialize') { + response.setHeader('Mcp-Session-Id', 'session-1'); + response.end(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} })); + } else if (message.method === 'notifications/initialized') { + expect(request.headers['mcp-session-id']).toBe('session-1'); + response.statusCode = 202; + response.end(); + } else { + expect(request.headers['mcp-session-id']).toBe('session-1'); + response.end(JSON.stringify({ + jsonrpc: '2.0', + id: 2, + result: { tools: [{ name: 'list_issues' }, { name: 'issue_read' }] }, + })); + } + }); + }, async endpoint => { + await expect(assertEnclaveGithubGatewayReady(enabledConfig(), { + ...handoff(), + AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT: endpoint, + }, 2_000)).resolves.toBeUndefined(); + }); + expect(methods).toEqual(['initialize', 'notifications/initialized', 'tools/list']); + }); + + it('rejects an identity failure before listing tools', async () => { + mockExeca + .mockResolvedValueOnce({ exitCode: 0, stdout: network(), stderr: '' }) + .mockResolvedValueOnce({ exitCode: 0, stdout: attachedGateway(), stderr: '' }); + await withGatewayServer((_request, response) => { + response.statusCode = 401; + response.end(); + }, async endpoint => { + await expect(assertEnclaveGithubGatewayReady(enabledConfig(), { + ...handoff(), + AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT: endpoint, + }, 2_000)).rejects.toThrow(/request failed/); + }); + }); + + it('rejects any tool beyond the issues-read-v1 profile', async () => { + mockExeca + .mockResolvedValueOnce({ exitCode: 0, stdout: network(), stderr: '' }) + .mockResolvedValueOnce({ exitCode: 0, stdout: attachedGateway(), stderr: '' }); + await withGatewayServer((request, response) => { + const chunks: Buffer[] = []; + request.on('data', chunk => chunks.push(Buffer.from(chunk))); + request.on('end', () => { + const message = JSON.parse(Buffer.concat(chunks).toString('utf8')); + if (message.method === 'initialize') { + response.setHeader('Mcp-Session-Id', 'session-2'); + response.end(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} })); + } else if (message.method === 'notifications/initialized') { + response.statusCode = 202; + response.end(); + } else { + response.end(JSON.stringify({ + jsonrpc: '2.0', + id: 2, + result: { + tools: [{ name: 'list_issues' }, { name: 'issue_read' }, { name: 'search_code' }], + }, + })); + } + }); + }, async endpoint => { + await expect(assertEnclaveGithubGatewayReady(enabledConfig(), { + ...handoff(), + AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT: endpoint, + }, 2_000)).rejects.toThrow(/exactly the issues-read-v1 tools/); + }); }); }); diff --git a/src/enclave/github-gateway.ts b/src/enclave/github-gateway.ts index 771f284d0..cecac38ce 100644 --- a/src/enclave/github-gateway.ts +++ b/src/enclave/github-gateway.ts @@ -1,26 +1,55 @@ import * as fs from 'fs'; +import * as http from 'http'; import execa from 'execa'; -import { - ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME, -} from '../constants'; +import { ENCLAVE_AGENT_API_PROXY_CONTAINER_NAME } from '../constants'; import { getLocalDockerEnv } from '../docker-host'; import type { WrapperConfig } from '../types'; import { - ENCLAVE_GITHUB_CONTROL_NETWORK, - ENCLAVE_GITHUB_CONTROL_SUBNET, - ENCLAVE_GITHUB_PROXY_ALIAS, - ENCLAVE_GITHUB_PROXY_PORT, + ENCLAVE_MCP_GATEWAY_CONTAINER_ENV, + ENCLAVE_MCP_GATEWAY_ENDPOINT_ENV, + ENCLAVE_MCP_GATEWAY_IDENTITY_ENV, + ENCLAVE_MCP_READINESS_TIMEOUT_ENV, + ENCLAVE_MCP_GATEWAY_RUN_LABEL, +} from './gateway'; +import { + ENCLAVE_AGENT_GITHUB_MCP_IP, + ENCLAVE_AGENT_NETWORK, + ENCLAVE_AGENT_SUBNET, + ENCLAVE_GITHUB_MCP_ALIAS, } from './network'; +import { resolveEnclavePaths } from './paths'; + +export const ENCLAVE_GITHUB_MCP_AGENT_ID_ENV = 'AWF_ENCLAVE_GITHUB_MCP_AGENT_ID'; +export const ENCLAVE_GITHUB_MCP_PORT = 8080; +export const ENCLAVE_GITHUB_MCP_SERVER_NAME = 'github'; +export const ENCLAVE_GITHUB_MCP_INTERNAL_URL = + `http://${ENCLAVE_AGENT_GITHUB_MCP_IP}:${ENCLAVE_GITHUB_MCP_PORT}/mcp/${ENCLAVE_GITHUB_MCP_SERVER_NAME}`; -export const ENCLAVE_GITHUB_PROXY_CONTAINER_ENV = 'AWF_ENCLAVE_GITHUB_PROXY_CONTAINER'; -export const ENCLAVE_GITHUB_PROXY_IDENTITY_ENV = 'AWF_ENCLAVE_GITHUB_PROXY_IDENTITY'; -export const ENCLAVE_GITHUB_PROXY_CA_CERT_ENV = 'AWF_ENCLAVE_GITHUB_PROXY_CA_CERT'; -export const ENCLAVE_GITHUB_PROXY_RUN_LABEL = 'com.github.gh-aw.enclave-github.run'; +const MCP_PROTOCOL_VERSION = '2025-06-18'; +const EXPECTED_TOOLS = ['issue_read', 'list_issues']; +const DEFAULT_READINESS_TIMEOUT_MS = 120_000; +const REQUEST_TIMEOUT_MS = 5_000; +const RETRY_DELAY_MS = 500; +const MAX_RESPONSE_BYTES = 256 * 1024; + +class GithubGatewayReadinessError extends Error { + constructor(message: string, readonly retryable = false) { + super(message); + } +} interface EnclaveGithubGatewayContract { + agentId: string; containerName: string; + endpoint: URL; identity: string; - caCertPath: string; +} + +interface JsonRpcResponse { + jsonrpc?: unknown; + id?: unknown; + result?: unknown; + error?: unknown; } function isEnclaveGithubEnabled(config: WrapperConfig): boolean { @@ -36,47 +65,78 @@ function requiredIdentity(name: string, value: string | undefined): string { return value; } -export function resolveEnclaveGithubGatewayContract( - config: WrapperConfig, - env: NodeJS.ProcessEnv = process.env, -): EnclaveGithubGatewayContract { - if (!isEnclaveGithubEnabled(config)) { - throw new Error('Enclave GitHub gateway contract requested while issues-read-v1 is disabled'); +function resolveGithubEndpoint(rawEndpoint: string | undefined): URL { + let endpoint: URL; + try { + endpoint = new URL(rawEndpoint ?? ''); + } catch { + throw new Error(`${ENCLAVE_MCP_GATEWAY_ENDPOINT_ENV} is missing or invalid`); } - const containerName = requiredIdentity( - ENCLAVE_GITHUB_PROXY_CONTAINER_ENV, - env[ENCLAVE_GITHUB_PROXY_CONTAINER_ENV], - ); - const identity = env[ENCLAVE_GITHUB_PROXY_IDENTITY_ENV] ?? ''; - if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(identity)) { - throw new Error( - `${ENCLAVE_GITHUB_PROXY_IDENTITY_ENV} must be the canonical compiler capability run identity`, - ); + + if ( + endpoint.protocol !== 'http:' + || !['localhost', '127.0.0.1', '[::1]'].includes(endpoint.hostname) + || endpoint.username + || endpoint.password + || endpoint.search + || endpoint.hash + ) { + throw new Error(`${ENCLAVE_MCP_GATEWAY_ENDPOINT_ENV} must be a loopback HTTP endpoint`); } - const caCertPath = env[ENCLAVE_GITHUB_PROXY_CA_CERT_ENV] ?? ''; - if (!caCertPath.startsWith('/')) { - throw new Error(`${ENCLAVE_GITHUB_PROXY_CA_CERT_ENV} must be an absolute path`); + endpoint.pathname = `/mcp/${ENCLAVE_GITHUB_MCP_SERVER_NAME}`; + return endpoint; +} + +function resolveGithubAgentId(config: WrapperConfig, env: NodeJS.ProcessEnv): string | undefined { + if (env[ENCLAVE_GITHUB_MCP_AGENT_ID_ENV]) { + return env[ENCLAVE_GITHUB_MCP_AGENT_ID_ENV]; } - let stat: fs.Stats; + const agentIdPath = resolveEnclavePaths(config.workDir).githubAgentIdPath; + let fd: number | undefined; try { - stat = fs.lstatSync(caCertPath); + fd = fs.openSync(agentIdPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); + const stat = fs.fstatSync(fd); + if (!stat.isFile() || (stat.mode & 0o077) !== 0) return undefined; + return fs.readFileSync(fd, 'ascii').trim(); } catch { - throw new Error(`${ENCLAVE_GITHUB_PROXY_CA_CERT_ENV} is unavailable`); + return undefined; + } finally { + if (fd !== undefined) fs.closeSync(fd); } - if (stat.isSymbolicLink() || !stat.isFile()) { - throw new Error(`${ENCLAVE_GITHUB_PROXY_CA_CERT_ENV} must be a regular non-symlink file`); +} + +export function resolveEnclaveGithubGatewayContract( + config: WrapperConfig, + env: NodeJS.ProcessEnv = process.env, +): EnclaveGithubGatewayContract { + if (!isEnclaveGithubEnabled(config)) { + throw new Error('Enclave GitHub gateway contract requested while issues-read-v1 is disabled'); } - return { containerName, identity, caCertPath }; + return { + agentId: requiredIdentity( + ENCLAVE_GITHUB_MCP_AGENT_ID_ENV, + resolveGithubAgentId(config, env), + ), + containerName: requiredIdentity( + ENCLAVE_MCP_GATEWAY_CONTAINER_ENV, + env[ENCLAVE_MCP_GATEWAY_CONTAINER_ENV], + ), + endpoint: resolveGithubEndpoint(env[ENCLAVE_MCP_GATEWAY_ENDPOINT_ENV]), + identity: requiredIdentity( + ENCLAVE_MCP_GATEWAY_IDENTITY_ENV, + env[ENCLAVE_MCP_GATEWAY_IDENTITY_ENV], + ), + }; } -async function inspectExternalProxy(contract: EnclaveGithubGatewayContract): Promise { +async function inspectSharedGateway(contract: EnclaveGithubGatewayContract): Promise { const result = await execa( 'docker', ['inspect', '--format', '{{json .}}', contract.containerName], { env: getLocalDockerEnv(), reject: false, timeout: 10_000 }, ); if (result.exitCode !== 0) { - throw new Error('Trusted enclave GitHub proxy container is unavailable'); + throw new Error('Trusted shared MCP gateway container is unavailable'); } let inspected: { Name?: string; @@ -87,28 +147,28 @@ async function inspectExternalProxy(contract: EnclaveGithubGatewayContract): Pro try { inspected = JSON.parse(result.stdout); } catch { - throw new Error('Trusted enclave GitHub proxy identity could not be inspected'); + throw new Error('Trusted shared MCP gateway identity could not be inspected'); } if ( inspected.Name !== `/${contract.containerName}` || inspected.State?.Running !== true - || inspected.Config?.Labels?.[ENCLAVE_GITHUB_PROXY_RUN_LABEL] !== contract.identity + || inspected.Config?.Labels?.[ENCLAVE_MCP_GATEWAY_RUN_LABEL] !== contract.identity || inspected.HostConfig?.NetworkMode !== 'bridge' ) { - throw new Error('Trusted enclave GitHub proxy identity did not match the compiler handoff'); + throw new Error('Trusted shared MCP gateway did not match the compiler handoff'); } } -async function assertControlNetworkMembership( +async function assertAgentNetworkMembership( contract: EnclaveGithubGatewayContract, ): Promise { const result = await execa( 'docker', - ['network', 'inspect', '--format', '{{json .}}', ENCLAVE_GITHUB_CONTROL_NETWORK], + ['network', 'inspect', '--format', '{{json .}}', ENCLAVE_AGENT_NETWORK], { env: getLocalDockerEnv(), reject: false, timeout: 10_000 }, ); if (result.exitCode !== 0) { - throw new Error('Enclave GitHub control network is unavailable'); + throw new Error('Enclave agent network is unavailable'); } let network: { Internal?: boolean; @@ -119,26 +179,28 @@ async function assertControlNetworkMembership( try { network = JSON.parse(result.stdout); } catch { - throw new Error('Enclave GitHub control network membership could not be inspected'); + throw new Error('Enclave agent network membership could not be inspected'); } const subnetConfig = network.IPAM?.Config ?? []; if ( network.Internal !== true || network.Driver !== 'bridge' || subnetConfig.length !== 1 - || subnetConfig[0]?.Subnet !== ENCLAVE_GITHUB_CONTROL_SUBNET + || subnetConfig[0]?.Subnet !== ENCLAVE_AGENT_SUBNET ) { - throw new Error('Enclave GitHub control network is not the fixed isolated bridge'); + throw new Error('Enclave agent network is not the fixed isolated bridge'); } - const containers = network.Containers ?? {}; - const names = Object.values(containers).map((entry) => entry.Name).filter(Boolean).sort(); - const expected = [ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME, contract.containerName].sort(); + const names = Object.values(network.Containers ?? {}) + .map((entry) => entry.Name) + .filter(Boolean) + .sort(); + const expected = [ENCLAVE_AGENT_API_PROXY_CONTAINER_NAME, contract.containerName].sort(); if (JSON.stringify(names) !== JSON.stringify(expected)) { - throw new Error('Enclave GitHub control network contains an unexpected member'); + throw new Error('Enclave agent network contains an unexpected steady-state member'); } } -async function assertExternalProxyAlias( +async function assertSharedGatewayAttachment( contract: EnclaveGithubGatewayContract, ): Promise { const result = await execa( @@ -147,39 +209,141 @@ async function assertExternalProxyAlias( { env: getLocalDockerEnv(), reject: false, timeout: 10_000 }, ); if (result.exitCode !== 0) { - throw new Error('Trusted enclave GitHub proxy network attachment is unavailable'); + throw new Error('Shared MCP gateway network attachment is unavailable'); } let inspected: { NetworkSettings?: { - Networks?: Record; + Networks?: Record; }; }; try { inspected = JSON.parse(result.stdout); } catch { - throw new Error('Trusted enclave GitHub proxy network attachment could not be inspected'); + throw new Error('Shared MCP gateway network attachment could not be inspected'); } - const aliases = inspected.NetworkSettings?.Networks?.[ENCLAVE_GITHUB_CONTROL_NETWORK]?.Aliases; - if (!Array.isArray(aliases) || !aliases.includes(ENCLAVE_GITHUB_PROXY_ALIAS)) { - throw new Error('Trusted enclave GitHub proxy is missing its fixed private alias'); + const attachment = inspected.NetworkSettings?.Networks?.[ENCLAVE_AGENT_NETWORK]; + if ( + !Array.isArray(attachment?.Aliases) + || !attachment.Aliases.includes(ENCLAVE_GITHUB_MCP_ALIAS) + || attachment.IPAddress !== ENCLAVE_AGENT_GITHUB_MCP_IP + ) { + throw new Error('Shared MCP gateway is missing its fixed enclave attachment'); } } +function postJsonRpc( + endpoint: URL, + agentId: string, + message: Record, + sessionId?: string, + timeoutMs = REQUEST_TIMEOUT_MS, +): Promise<{ body: JsonRpcResponse; sessionId?: string }> { + return new Promise((resolve, reject) => { + const rejectBounded = (error: Error): void => { + clearTimeout(deadlineTimer); + reject(error); + }; + const payload = Buffer.from(JSON.stringify(message), 'utf8'); + const request = http.request(endpoint, { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + Authorization: agentId, + 'Content-Type': 'application/json', + 'Content-Length': String(payload.length), + ...(sessionId ? { 'Mcp-Session-Id': sessionId } : {}), + }, + timeout: timeoutMs, + }, response => { + const chunks: Buffer[] = []; + let total = 0; + response.on('data', chunk => { + total += chunk.length; + if (total > MAX_RESPONSE_BYTES) { + request.destroy(new GithubGatewayReadinessError( + 'Shared MCP gateway response exceeded its framing bound', + )); + return; + } + chunks.push(Buffer.from(chunk)); + }); + response.on('end', () => { + try { + const raw = Buffer.concat(chunks).toString('utf8'); + if (response.statusCode !== 200 && response.statusCode !== 202) { + if (response.statusCode === 503) { + try { + const unavailable = JSON.parse(raw) as { + error?: unknown; + retryable?: unknown; + }; + if ( + unavailable.error === 'backend_unavailable' + && unavailable.retryable !== false + ) { + rejectBounded(new GithubGatewayReadinessError( + 'Shared MCP gateway backend is not yet available', + true, + )); + return; + } + } catch { + // Treat every undocumented response shape as a terminal failure. + } + } + rejectBounded(new GithubGatewayReadinessError('Shared MCP gateway request failed')); + return; + } + const contentType = String(response.headers['content-type'] ?? ''); + const events = raw + .split(/\r?\n/) + .filter(line => line.startsWith('data:')) + .map(line => line.slice(5).trim()) + .filter(line => line !== '' && line !== '[DONE]'); + const jsonText = contentType.includes('text/event-stream') + ? events[events.length - 1] + : raw; + const returnedSession = response.headers['mcp-session-id']; + clearTimeout(deadlineTimer); + resolve({ + body: !jsonText ? {} : JSON.parse(jsonText) as JsonRpcResponse, + sessionId: typeof returnedSession === 'string' ? returnedSession : sessionId, + }); + } catch { + rejectBounded(new GithubGatewayReadinessError( + 'Shared MCP gateway returned invalid bounded JSON', + )); + } + }); + }); + request.on('timeout', () => request.destroy( + new GithubGatewayReadinessError('Shared MCP gateway request timed out'), + )); + request.on('error', rejectBounded); + const deadlineTimer = setTimeout(() => request.destroy( + new GithubGatewayReadinessError('Shared MCP gateway request timed out'), + ), timeoutMs); + request.end(payload); + }); +} + export async function connectEnclaveGithubGateway( config: WrapperConfig, env: NodeJS.ProcessEnv = process.env, ): Promise { if (!isEnclaveGithubEnabled(config)) return; const contract = resolveEnclaveGithubGatewayContract(config, env); - await inspectExternalProxy(contract); + await inspectSharedGateway(contract); const result = await execa( 'docker', [ 'network', 'connect', + '--ip', + ENCLAVE_AGENT_GITHUB_MCP_IP, '--alias', - ENCLAVE_GITHUB_PROXY_ALIAS, - ENCLAVE_GITHUB_CONTROL_NETWORK, + ENCLAVE_GITHUB_MCP_ALIAS, + ENCLAVE_AGENT_NETWORK, contract.containerName, ], { env: getLocalDockerEnv(), reject: false, timeout: 10_000 }, @@ -188,80 +352,128 @@ export async function connectEnclaveGithubGateway( result.exitCode !== 0 && !/already exists in network|is already attached|already connected/i.test(result.stderr || '') ) { - throw new Error('Failed to attach the trusted enclave GitHub proxy to its private control network'); + throw new Error('Failed to attach the shared MCP gateway to the enclave agent network'); } - await assertControlNetworkMembership(contract); - await assertExternalProxyAlias(contract); + await assertAgentNetworkMembership(contract); + await assertSharedGatewayAttachment(contract); } -export async function assertEnclaveGithubGatewayReady(config: WrapperConfig): Promise { - if (!isEnclaveGithubEnabled(config)) return; - const contract = resolveEnclaveGithubGatewayContract(config); - await assertControlNetworkMembership(contract); - const result = await execa( - 'docker', - [ - 'inspect', - '--format', - '{{.State.Running}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}', - ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME, - ], - { env: getLocalDockerEnv(), reject: false, timeout: 10_000 }, - ); - if (result.exitCode !== 0 || result.stdout.trim() !== 'true|healthy') { - throw new Error('Enclave GitHub CLI proxy did not prove the issues-read-v1 route ready'); +async function proveGithubGatewayReadiness( + contract: EnclaveGithubGatewayContract, + deadline: number, +): Promise { + const requestBudget = (): number => { + const remaining = deadline - Date.now(); + if (remaining <= 0) throw new GithubGatewayReadinessError('Readiness deadline expired'); + return Math.min(REQUEST_TIMEOUT_MS, remaining); + }; + await assertAgentNetworkMembership(contract); + await assertSharedGatewayAttachment(contract); + const initialized = await postJsonRpc(contract.endpoint, contract.agentId, { + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: MCP_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'awf-enclave-readiness', version: '1.0.0' }, + }, + }, undefined, requestBudget()); + if ( + initialized.body.error !== undefined + || initialized.body.id !== 1 + || !initialized.sessionId + ) { + throw new Error('Shared MCP gateway rejected the enclave identity'); } - const upstream = await execa( - 'docker', - [ - 'exec', - ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME, - 'curl', - '--silent', - '--show-error', - '--output', - '/dev/null', - '--cacert', - '/tmp/proxy-tls/ca.crt', - `https://${ENCLAVE_GITHUB_PROXY_ALIAS}:${ENCLAVE_GITHUB_PROXY_PORT}/`, - ], - { env: getLocalDockerEnv(), reject: false, timeout: 10_000 }, - ); - if (upstream.exitCode !== 0) { - throw new Error('Enclave GitHub CLI proxy could not establish the fixed TLS route to mcpg'); + await postJsonRpc(contract.endpoint, contract.agentId, { + jsonrpc: '2.0', + method: 'notifications/initialized', + }, initialized.sessionId, requestBudget()); + const listed = await postJsonRpc(contract.endpoint, contract.agentId, { + jsonrpc: '2.0', + id: 2, + method: 'tools/list', + params: {}, + }, initialized.sessionId, requestBudget()); + const tools = ( + listed.body.result + && typeof listed.body.result === 'object' + && 'tools' in listed.body.result + && Array.isArray(listed.body.result.tools) + ) + ? listed.body.result.tools + .map((tool) => ( + tool && typeof tool === 'object' && 'name' in tool && typeof tool.name === 'string' + ? tool.name + : '' + )) + .filter(Boolean) + .sort() + : []; + if (listed.body.error !== undefined || JSON.stringify(tools) !== JSON.stringify(EXPECTED_TOOLS)) { + throw new Error('Shared MCP gateway did not expose exactly the issues-read-v1 tools'); } } -export async function disconnectEnclaveGithubGateway( +export async function assertEnclaveGithubGatewayReady( config: WrapperConfig, env: NodeJS.ProcessEnv = process.env, + timeoutMs?: number, ): Promise { - if (!isEnclaveGithubEnabled(config) || config.keepContainers) return; + if (!isEnclaveGithubEnabled(config)) return; const contract = resolveEnclaveGithubGatewayContract(config, env); - const result = await execa( - 'docker', - ['network', 'disconnect', '-f', ENCLAVE_GITHUB_CONTROL_NETWORK, contract.containerName], - { env: getLocalDockerEnv(), reject: false, timeout: 10_000 }, - ); - if (result.exitCode !== 0) { - throw new Error('Failed to disconnect the trusted enclave GitHub proxy'); + const configuredTimeout = Number(env[ENCLAVE_MCP_READINESS_TIMEOUT_ENV] + ?? DEFAULT_READINESS_TIMEOUT_MS); + const readinessTimeout = timeoutMs ?? configuredTimeout; + if ( + !Number.isSafeInteger(readinessTimeout) + || readinessTimeout < 1_000 + || readinessTimeout > 600_000 + ) { + throw new Error(`${ENCLAVE_MCP_READINESS_TIMEOUT_ENV} must be between 1000 and 600000`); } + const deadline = Date.now() + readinessTimeout; + let lastError: unknown; + do { + try { + await proveGithubGatewayReadiness(contract, deadline); + return; + } catch (error) { + if (!(error instanceof GithubGatewayReadinessError) || !error.retryable) throw error; + lastError = error; + const remaining = deadline - Date.now(); + if (remaining > 0) { + await new Promise(resolve => setTimeout(resolve, Math.min(RETRY_DELAY_MS, remaining))); + } + } + } while (Date.now() < deadline); + throw new Error( + `Enclave GitHub MCP readiness timed out before primary-agent startup: ${ + lastError instanceof Error ? lastError.message : 'unknown readiness failure' + }`, + ); } -export async function shutdownEnclaveGithubCliProxy(config: WrapperConfig): Promise { +export async function disconnectEnclaveGithubGateway( + config: WrapperConfig, + env: NodeJS.ProcessEnv = process.env, +): Promise { if (!isEnclaveGithubEnabled(config) || config.keepContainers) return; + const contract = resolveEnclaveGithubGatewayContract(config, env); const result = await execa( 'docker', - ['stop', '--time', '5', ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME], - { env: getLocalDockerEnv(), reject: false, timeout: 15_000 }, + ['network', 'disconnect', '-f', ENCLAVE_AGENT_NETWORK, contract.containerName], + { env: getLocalDockerEnv(), reject: false, timeout: 10_000 }, ); if (result.exitCode !== 0) { - throw new Error('Failed to stop the enclave GitHub CLI proxy before audit preservation'); + throw new Error('Failed to disconnect the shared MCP gateway from the enclave agent network'); } } export const enclaveGithubGatewayTestHelpers = { - assertControlNetworkMembership, - assertExternalProxyAlias, - inspectExternalProxy, + assertAgentNetworkMembership, + assertSharedGatewayAttachment, + inspectSharedGateway, + postJsonRpc, }; diff --git a/src/enclave/manager.test.ts b/src/enclave/manager.test.ts index 4b84c2943..d47cc4894 100644 --- a/src/enclave/manager.test.ts +++ b/src/enclave/manager.test.ts @@ -35,19 +35,16 @@ function enclaveEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { GH_TOKEN: 'secret', AWF_ENCLAVE_MCP_CAPABILITY: 'a'.repeat(64), AWF_ENCLAVE_MCP_GATEWAY_IDENTITY: 'test-run-identity', + AWF_ENCLAVE_MCP_GATEWAY_CONTAINER: 'awmg-mcpg', AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT: 'http://127.0.0.1:8080/mcp/awf-enclave', MCP_GATEWAY_API_KEY: 'g'.repeat(48), ...overrides, }; } -function githubEnclaveEnv(workDir: string, overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { - const caCert = path.join(workDir, 'enclave-github-ca.crt'); - fs.writeFileSync(caCert, 'test-ca', { mode: 0o600 }); +function githubEnclaveEnv(_workDir: string, overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { return enclaveEnv({ - AWF_ENCLAVE_GITHUB_PROXY_CONTAINER: 'compiler-mcpg', - AWF_ENCLAVE_GITHUB_PROXY_IDENTITY: 'gh-aw-egh-123456-1-abcdef123456', - AWF_ENCLAVE_GITHUB_PROXY_CA_CERT: caCert, + AWF_ENCLAVE_GITHUB_MCP_AGENT_ID: 'enclaveAgentId0123456789abcdef012345', ...overrides, }); } @@ -230,10 +227,9 @@ describe('prepareEnclaves fail-closed preflight', () => { it.each([ undefined, - 'a'.repeat(63), - 'A'.repeat(64), - 'z'.repeat(64), - ])('rejects a malformed enclave GitHub capability root (%s)', async (root) => { + 'a'.repeat(31), + 'bad identity with spaces', + ])('rejects a malformed enclave GitHub MCP agent identity (%s)', async (agentId) => { await expect(prepareEnclaves(agentConfig(workDir, [{ agent: { model: 'gpt-test', @@ -241,10 +237,10 @@ describe('prepareEnclaves fail-closed preflight', () => { }, repos: [repository], }]), { - env: githubEnclaveEnv(workDir, { MCP_GATEWAY_ENCLAVE_CAPABILITY_KEY: root }), + env: githubEnclaveEnv(workDir, { AWF_ENCLAVE_GITHUB_MCP_AGENT_ID: agentId }), assertPrimaryAvailable: jest.fn(), assertAgentRuntimeAvailable: jest.fn(), - })).rejects.toThrow(/MCP_GATEWAY_ENCLAVE_CAPABILITY_KEY/); + })).rejects.toThrow(/AWF_ENCLAVE_GITHUB_MCP_AGENT_ID/); }); it('rejects the unimplemented sbx script runtime before staging', async () => { @@ -291,8 +287,8 @@ describe('prepareEnclaves fail-closed preflight', () => { expect(paths.ingressRoot.startsWith(workDir)).toBe(false); }); - it('stages the GitHub HMAC root as a private mode-0600 file', async () => { - const root = '0123456789abcdef'.repeat(4); + it('stages the enclave GitHub MCP identity as a private mode-0600 file', async () => { + const agentId = 'enclaveAgentId0123456789abcdef012345'; await prepareEnclaves(agentConfig(workDir, [{ agent: { model: 'gpt-test', @@ -300,20 +296,17 @@ describe('prepareEnclaves fail-closed preflight', () => { }, repos: [repository], }]), { - env: githubEnclaveEnv(workDir, { MCP_GATEWAY_ENCLAVE_CAPABILITY_KEY: root }), + env: githubEnclaveEnv(workDir, { AWF_ENCLAVE_GITHUB_MCP_AGENT_ID: agentId }), gitRunner, assertPrimaryAvailable: jest.fn().mockResolvedValue(undefined), assertAgentRuntimeAvailable: jest.fn().mockResolvedValue(undefined), }); const paths = resolveEnclavePaths(workDir); - expect(fs.readFileSync(paths.githubCapabilityKeyPath, 'utf8').trim()).toBe(root); - expect(fs.statSync(paths.githubCapabilityKeyPath).mode & 0o777).toBe(0o600); - expect(fs.readFileSync(paths.githubRunIdentityPath, 'utf8').trim()) - .toBe('gh-aw-egh-123456-1-abcdef123456'); - expect(fs.statSync(paths.githubRunIdentityPath).mode & 0o777).toBe(0o600); + expect(fs.readFileSync(paths.githubAgentIdPath, 'utf8').trim()).toBe(agentId); + expect(fs.statSync(paths.githubAgentIdPath).mode & 0o777).toBe(0o600); const seedRunId = JSON.parse(fs.readFileSync(paths.seedMapPath, 'utf8')).runId; expect(seedRunId).toMatch(/^[0-9a-f]{32}$/); - expect(seedRunId).not.toBe('gh-aw-egh-123456-1-abcdef123456'); + expect(seedRunId).not.toBe(agentId); }); it('removes labelled orphan containers and both private roots on teardown', async () => { diff --git a/src/enclave/manager.ts b/src/enclave/manager.ts index 93384eace..f9161f09c 100644 --- a/src/enclave/manager.ts +++ b/src/enclave/manager.ts @@ -28,11 +28,12 @@ import { ENCLAVE_MCP_CAPABILITY_ENV, resolveEnclaveGatewayContract, } from './gateway'; -import { resolveEnclaveGithubGatewayContract } from './github-gateway'; +import { + ENCLAVE_GITHUB_MCP_AGENT_ID_ENV, + resolveEnclaveGithubGatewayContract, +} from './github-gateway'; export const ENCLAVE_RUN_LABEL = 'awf.enclave.run'; -export const ENCLAVE_GITHUB_CAPABILITY_KEY_ENV = 'MCP_GATEWAY_ENCLAVE_CAPABILITY_KEY'; - export function isEnclaveScriptEnabled(config: WrapperConfig): boolean { return config.enclaves?.enabled === true && config.enclaves.executors.script.enabled === true; } @@ -70,14 +71,12 @@ function prepareDirectories( ensureDirectory(paths.apiProxyLogsDir, 0o700); // The image's fixed non-root user must create the audit stream through this // bind mount. The parent private root remains 0700 on the host. - ensureDirectory(paths.githubCliProxyLogsDir, 0o733); ensureDirectory(paths.runDir, 0o770); if (process.getuid?.() === 0) { const hostUid = parseInt(getSafeHostUid(), 10); const hostGid = parseInt(getSafeHostGid(), 10); chown(paths.runDir, hostUid, hostGid); chown(paths.apiProxyLogsDir, hostUid, hostGid); - chown(paths.githubCliProxyLogsDir, hostUid, hostGid); } } @@ -143,11 +142,11 @@ export async function prepareEnclaves( if (!token) { errors.push('enclaves require a staging credential in GH_TOKEN or GITHUB_TOKEN on the AWF host'); } - const githubCapabilityKey = env[ENCLAVE_GITHUB_CAPABILITY_KEY_ENV] ?? ''; + const githubAgentId = env[ENCLAVE_GITHUB_MCP_AGENT_ID_ENV] ?? ''; if (isEnclaveGithubEnabled(config)) { - if (!/^[0-9a-f]{64}$/.test(githubCapabilityKey)) { + if (!/^[A-Za-z0-9_-]{32,128}$/.test(githubAgentId)) { errors.push( - `${ENCLAVE_GITHUB_CAPABILITY_KEY_ENV} must contain the compiler-issued 256-bit lowercase hex root`, + `${ENCLAVE_GITHUB_MCP_AGENT_ID_ENV} must contain the compiler-issued enclave gateway identity`, ); } try { @@ -210,10 +209,8 @@ export async function prepareEnclaves( writeExclusive(paths.seedMapPath, serializePrivateRepositorySeedMap(seedMap), 0o600); writeExclusive(paths.capabilityPath, `${env[ENCLAVE_MCP_CAPABILITY_ENV]}\n`, 0o600); if (isEnclaveGithubEnabled(config)) { - writeExclusive(paths.githubCapabilityKeyPath, `${githubCapabilityKey}\n`, 0o600); - const githubRunIdentity = resolveEnclaveGithubGatewayContract(config, env).identity; - writeExclusive(paths.githubRunIdentityPath, `${githubRunIdentity}\n`, 0o600); - if (env === process.env) delete process.env[ENCLAVE_GITHUB_CAPABILITY_KEY_ENV]; + writeExclusive(paths.githubAgentIdPath, `${githubAgentId}\n`, 0o600); + if (env === process.env) delete process.env[ENCLAVE_GITHUB_MCP_AGENT_ID_ENV]; } logger.info(`Enclaves: staged ${staging.seeds.length} immutable seed(s); staging credential discarded.`); } diff --git a/src/enclave/network.ts b/src/enclave/network.ts index 4c6c3ad7b..dd0085809 100644 --- a/src/enclave/network.ts +++ b/src/enclave/network.ts @@ -4,10 +4,11 @@ * An agent enclave is deliberately *not* a member of `awf-net` or `awf-ext`: * it has no Squid route, no general proxy, no DNS route to the internet, and * no path to the primary agent, the enclave MCP server, the safe-outputs - * collector, the MCP gateway, or the CLI proxy. Its only reachable peer is a - * dedicated AWF API proxy instance that joins a separate egress bridge and is - * the only component holding a real provider credential. That proxy's logs, - * metrics, and quota state are private to this subsystem. + * collector, or the MCP gateway. Its only mandatory peer is a dedicated AWF + * API proxy instance that joins a separate egress bridge and is the only + * component holding a real provider credential. When GitHub access is enabled, + * compiler-owned mcpg is the only additional peer and enforces the enclave's + * independent server, tool, and repository policy. * * The enclave MCP server that *launches* these enclaves never joins this * network. It joins only the separate internal MCP control network and reaches @@ -24,12 +25,6 @@ export const ENCLAVE_AGENT_NETWORK = 'awf-enclave-agent'; /** Egress bridge joined only by the dedicated agent-enclave API proxy. */ export const ENCLAVE_AGENT_EGRESS_NETWORK = 'awf-enclave-agent-egress'; -/** Private path between the PAT-free enclave CLI proxy and compiler-owned mcpg. */ -export const ENCLAVE_GITHUB_CONTROL_NETWORK = 'awf-enclave-github-control'; - -/** Fixed subnet for the isolated mcpg control path. */ -export const ENCLAVE_GITHUB_CONTROL_SUBNET = '172.29.0.0/24'; - /** Fixed compiler-owned mcpg identity on the private GitHub control network. */ export const ENCLAVE_GITHUB_PROXY_ALIAS = 'awf-enclave-github-proxy'; @@ -58,11 +53,11 @@ export const ENCLAVE_AGENT_SUBNET = '172.31.0.0/24'; /** Fixed API-proxy address on the agent-enclave network. */ export const ENCLAVE_AGENT_API_PROXY_IP = '172.31.0.30'; -/** Fixed PAT-free CLI-proxy address on the agent-enclave network. */ -export const ENCLAVE_AGENT_CLI_PROXY_IP = '172.31.0.40'; +/** Fixed shared-mcpg address on the agent-enclave network. */ +export const ENCLAVE_AGENT_GITHUB_MCP_IP = '172.31.0.40'; -/** Fixed CLI-proxy address on the isolated mcpg control path. */ -export const ENCLAVE_GITHUB_CLI_PROXY_IP = '172.29.0.10'; +/** Fixed alias for the shared gateway's GitHub MCP route. */ +export const ENCLAVE_GITHUB_MCP_ALIAS = 'awf-enclave-github-mcp'; /** * Fixed DNS alias for the API proxy on the agent-enclave network. diff --git a/src/enclave/paths.test.ts b/src/enclave/paths.test.ts index 262ba9553..ceed46082 100644 --- a/src/enclave/paths.test.ts +++ b/src/enclave/paths.test.ts @@ -8,13 +8,7 @@ describe('resolveEnclavePaths', () => { expect(paths.ingressRoot).toMatch(/^\/private\/awf-enclave-control-/); expect(paths.ingressRoot).not.toContain(paths.root); expect(paths.capabilityPath).toBe(path.join(paths.runDir, 'auth-token')); - expect(paths.githubCapabilityKeyPath).toBe( - path.join(paths.runDir, 'github-capability-key'), - ); - expect(paths.githubRunIdentityPath).toBe( - path.join(paths.runDir, 'github-run-identity'), - ); + expect(paths.githubAgentIdPath).toBe(path.join(paths.runDir, 'github-agent-id')); expect(paths.auditDir.startsWith(paths.root)).toBe(true); - expect(paths.githubCliProxyLogsDir.startsWith(paths.root)).toBe(true); }); }); diff --git a/src/enclave/paths.ts b/src/enclave/paths.ts index 3529ede7b..8098f4f70 100644 --- a/src/enclave/paths.ts +++ b/src/enclave/paths.ts @@ -9,30 +9,24 @@ export interface EnclavePaths { auditDir: string; /** Dedicated agent-enclave API-proxy telemetry. Never agent-visible. */ apiProxyLogsDir: string; - /** Dedicated enclave GitHub CLI policy audit. Never agent-visible. */ - githubCliProxyLogsDir: string; seedMapPath: string; ingressRoot: string; runDir: string; capabilityPath: string; - githubCapabilityKeyPath: string; - githubRunIdentityPath: string; + githubAgentIdPath: string; } export const ENCLAVE_PRIVATE_BASE_DIR = '/var/tmp'; export const ENCLAVE_CAPABILITY_FILENAME = 'auth-token'; -export const ENCLAVE_GITHUB_CAPABILITY_KEY_FILENAME = 'github-capability-key'; -export const ENCLAVE_GITHUB_RUN_IDENTITY_FILENAME = 'github-run-identity'; +export const ENCLAVE_GITHUB_AGENT_ID_FILENAME = 'github-agent-id'; export const ENCLAVE_SERVER_SEEDS_DIR = '/srv/awf/seeds'; export const ENCLAVE_SERVER_WORK_DIR = '/srv/awf/work'; export const ENCLAVE_SERVER_SEED_MAP_PATH = '/srv/awf/seed-map.json'; export const ENCLAVE_SERVER_CAPABILITY_DIR = '/run/awf-enclave-mcp'; export const ENCLAVE_SERVER_CAPABILITY_PATH = `${ENCLAVE_SERVER_CAPABILITY_DIR}/${ENCLAVE_CAPABILITY_FILENAME}`; -export const ENCLAVE_SERVER_GITHUB_CAPABILITY_KEY_PATH = - `${ENCLAVE_SERVER_CAPABILITY_DIR}/${ENCLAVE_GITHUB_CAPABILITY_KEY_FILENAME}`; -export const ENCLAVE_SERVER_GITHUB_RUN_IDENTITY_PATH = - `${ENCLAVE_SERVER_CAPABILITY_DIR}/${ENCLAVE_GITHUB_RUN_IDENTITY_FILENAME}`; +export const ENCLAVE_SERVER_GITHUB_AGENT_ID_PATH = + `${ENCLAVE_SERVER_CAPABILITY_DIR}/${ENCLAVE_GITHUB_AGENT_ID_FILENAME}`; export const ENCLAVE_SERVER_CONTROL_DIR = '/run/awf-enclave-mcp-control'; export const ENCLAVE_SERVER_AUDIT_DIR = '/var/log/awf-enclave'; export const ENCLAVE_SERVER_DOCKER_SOCKET_PATH = '/var/run/docker.sock'; @@ -58,13 +52,11 @@ export function resolveEnclavePaths( controlDir: path.join(root, 'control'), auditDir: path.join(root, 'audit'), apiProxyLogsDir: path.join(root, 'api-proxy-logs'), - githubCliProxyLogsDir: path.join(root, 'github-cli-proxy-logs'), seedMapPath: path.join(root, 'seed-map.json'), ingressRoot, runDir, capabilityPath: path.join(runDir, ENCLAVE_CAPABILITY_FILENAME), - githubCapabilityKeyPath: path.join(runDir, ENCLAVE_GITHUB_CAPABILITY_KEY_FILENAME), - githubRunIdentityPath: path.join(runDir, ENCLAVE_GITHUB_RUN_IDENTITY_FILENAME), + githubAgentIdPath: path.join(runDir, ENCLAVE_GITHUB_AGENT_ID_FILENAME), }; } diff --git a/src/services/agent-environment-options.test.ts b/src/services/agent-environment-options.test.ts index 975c9b207..8e8040342 100644 --- a/src/services/agent-environment-options.test.ts +++ b/src/services/agent-environment-options.test.ts @@ -132,12 +132,12 @@ describe('agent environment: options', () => { } }); - it('does not pass enclave GitHub proxy handoff material with envAll', () => { + it('does not pass enclave GitHub MCP handoff material with envAll', () => { const handoff = { - MCP_GATEWAY_ENCLAVE_CAPABILITY_KEY: '0'.repeat(64), - AWF_ENCLAVE_GITHUB_PROXY_CONTAINER: 'compiler-mcpg', - AWF_ENCLAVE_GITHUB_PROXY_IDENTITY: 'gh-aw-egh-123456-1-abcdef123456', - AWF_ENCLAVE_GITHUB_PROXY_CA_CERT: '/tmp/compiler-ca.crt', + AWF_ENCLAVE_MCP_GATEWAY_CONTAINER: 'compiler-mcpg', + AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT: 'http://127.0.0.1:8080', + AWF_ENCLAVE_MCP_GATEWAY_IDENTITY: 'primaryAgentId0123456789abcdef012345', + AWF_ENCLAVE_GITHUB_MCP_AGENT_ID: 'enclaveAgentId0123456789abcdef012345', }; const original = Object.fromEntries( Object.keys(handoff).map(name => [name, process.env[name]]), diff --git a/src/services/agent-environment/excluded-vars.test.ts b/src/services/agent-environment/excluded-vars.test.ts index 89f3f4090..76d0c6eca 100644 --- a/src/services/agent-environment/excluded-vars.test.ts +++ b/src/services/agent-environment/excluded-vars.test.ts @@ -29,10 +29,7 @@ describe('buildExclusionSet', () => { 'AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT', 'AWF_ENCLAVE_MCP_GATEWAY_CONTAINER', 'AWF_ENCLAVE_MCP_READINESS_TIMEOUT_MS', - 'MCP_GATEWAY_ENCLAVE_CAPABILITY_KEY', - 'AWF_ENCLAVE_GITHUB_PROXY_CONTAINER', - 'AWF_ENCLAVE_GITHUB_PROXY_IDENTITY', - 'AWF_ENCLAVE_GITHUB_PROXY_CA_CERT', + 'AWF_ENCLAVE_GITHUB_MCP_AGENT_ID', ]) { expect(excluded.has(name)).toBe(true); } diff --git a/src/services/agent-environment/excluded-vars.ts b/src/services/agent-environment/excluded-vars.ts index 36738930f..b1154d940 100644 --- a/src/services/agent-environment/excluded-vars.ts +++ b/src/services/agent-environment/excluded-vars.ts @@ -27,10 +27,7 @@ export function buildExclusionSet(config: WrapperConfig): Set { 'AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT', 'AWF_ENCLAVE_MCP_GATEWAY_CONTAINER', 'AWF_ENCLAVE_MCP_READINESS_TIMEOUT_MS', - 'MCP_GATEWAY_ENCLAVE_CAPABILITY_KEY', - 'AWF_ENCLAVE_GITHUB_PROXY_CONTAINER', - 'AWF_ENCLAVE_GITHUB_PROXY_IDENTITY', - 'AWF_ENCLAVE_GITHUB_PROXY_CA_CERT', + 'AWF_ENCLAVE_GITHUB_MCP_AGENT_ID', ]); if (config.enableApiProxy) { diff --git a/src/services/enclave-agent-service.test.ts b/src/services/enclave-agent-service.test.ts index 00703ab69..2ba1ac5ea 100644 --- a/src/services/enclave-agent-service.test.ts +++ b/src/services/enclave-agent-service.test.ts @@ -10,8 +10,6 @@ import { ENCLAVE_AGENT_EGRESS_NETWORK, ENCLAVE_AGENT_NETWORK, ENCLAVE_AGENT_SUBNET, - ENCLAVE_GITHUB_CONTROL_NETWORK, - ENCLAVE_GITHUB_CONTROL_SUBNET, } from '../enclave/network'; function config(overrides: Partial = {}): WrapperConfig { @@ -194,16 +192,14 @@ describe('unified enclave agent executor compose assembly', () => { .toThrow(/at least one enclave executor must be enabled/); }); - it('builds a PAT-free dual-homed CLI proxy only for issues-read-v1', () => { - const directory = fs.mkdtempSync(path.join(__dirname, 'awf-enclave-github-')); - const caCert = path.join(directory, 'ca.crt'); - fs.writeFileSync(caCert, 'test-ca'); + it('configures direct shared-mcpg access only for issues-read-v1', () => { const originalEnv = process.env; process.env = { ...originalEnv, - AWF_ENCLAVE_GITHUB_PROXY_CONTAINER: 'compiler-mcpg', - AWF_ENCLAVE_GITHUB_PROXY_IDENTITY: 'gh-aw-egh-123456-1-abcdef123456', - AWF_ENCLAVE_GITHUB_PROXY_CA_CERT: caCert, + AWF_ENCLAVE_MCP_GATEWAY_CONTAINER: 'compiler-mcpg', + AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT: 'http://127.0.0.1:8080', + AWF_ENCLAVE_MCP_GATEWAY_IDENTITY: 'primaryAgentId0123456789abcdef012345', + AWF_ENCLAVE_GITHUB_MCP_AGENT_ID: 'enclaveAgentId0123456789abcdef012345', }; try { const enclaves = normalizeEnclavesConfig([{ @@ -214,41 +210,19 @@ describe('unified enclave agent executor compose assembly', () => { repos: [{ repo: 'octo/private', sensitivity: 'internal' }], }]); const result = build({ enclaves }); - const proxy = result.agentCliProxyService as Record; - expect(proxy.container_name).toBe('awf-enclave-agent-cli-proxy'); - expect(proxy.networks).toEqual({ - [ENCLAVE_AGENT_NETWORK]: { ipv4_address: '172.31.0.40' }, - [ENCLAVE_GITHUB_CONTROL_NETWORK]: { ipv4_address: '172.29.0.10' }, - }); - expect(proxy.networks).not.toHaveProperty(ENCLAVE_AGENT_EGRESS_NETWORK); - expect(proxy.environment).toMatchObject({ - AWF_CLI_PROXY_MODE: 'enclave', - AWF_CLI_PROXY_PROFILE: 'issues-read-v1', - AWF_DIFC_PROXY_HOST: 'awf-enclave-github-proxy', - AWF_DIFC_PROXY_PORT: '18443', - }); - expect(JSON.stringify(proxy.environment)).not.toMatch(/TOKEN|CAPABILITY|PAT/); - expect(result.service.depends_on).toMatchObject({ - 'enclave-agent-cli-proxy': { condition: 'service_healthy' }, - }); const serverVolumes = result.service.volumes as string[]; expect(serverVolumes).toEqual(expect.arrayContaining([ - expect.stringMatching(/github-capability-key:\/run\/awf-enclave-mcp\/github-capability-key:ro$/), + expect.stringMatching(/github-agent-id:\/run\/awf-enclave-mcp\/github-agent-id:ro$/), ])); expect(result.service.environment).toMatchObject({ - AWF_ENCLAVE_AGENT_GITHUB_RUN_IDENTITY_PATH: - '/run/awf-enclave-mcp/github-run-identity', + AWF_ENCLAVE_AGENT_GITHUB_MCP_URL: 'http://172.31.0.40:8080/mcp/github', + AWF_ENCLAVE_AGENT_GITHUB_AGENT_ID_PATH: + '/run/awf-enclave-mcp/github-agent-id', + AWF_ENCLAVE_AGENT_GITHUB_GATEWAY_CONTAINER: 'compiler-mcpg', }); - expect(JSON.stringify(result.service.environment)) - .not.toContain('gh-aw-egh-123456-1-abcdef123456'); - for (const environment of [proxy.environment, result.service.environment]) { - expect(JSON.stringify(environment)).not.toMatch( - /MCP_GATEWAY_ENCLAVE_POLICY_JSON|SECRECY|DIFC.*LABEL|AGENT.*LABEL/, - ); - } + expect(JSON.stringify(result.service.environment)).not.toContain('primaryAgentId'); } finally { process.env = originalEnv; - fs.rmSync(directory, { recursive: true, force: true }); } }); }); @@ -412,16 +386,14 @@ describe('unified enclave compose topology', () => { }); }); - it('adds the fixed internal GitHub control network only for the opted-in profile', () => { - const directory = fs.mkdtempSync(path.join(__dirname, 'awf-enclave-github-compose-')); - const caCert = path.join(directory, 'ca.crt'); - fs.writeFileSync(caCert, 'test-ca'); + it('does not add a dedicated GitHub bridge service for the opted-in profile', () => { const originalEnv = process.env; process.env = { ...originalEnv, - AWF_ENCLAVE_GITHUB_PROXY_CONTAINER: 'compiler-mcpg', - AWF_ENCLAVE_GITHUB_PROXY_IDENTITY: 'gh-aw-egh-123456-1-abcdef123456', - AWF_ENCLAVE_GITHUB_PROXY_CA_CERT: caCert, + AWF_ENCLAVE_MCP_GATEWAY_CONTAINER: 'compiler-mcpg', + AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT: 'http://127.0.0.1:8080', + AWF_ENCLAVE_MCP_GATEWAY_IDENTITY: 'primaryAgentId0123456789abcdef012345', + AWF_ENCLAVE_GITHUB_MCP_AGENT_ID: 'enclaveAgentId0123456789abcdef012345', }; try { const enclaves = normalizeEnclavesConfig([{ @@ -432,12 +404,7 @@ describe('unified enclave compose topology', () => { repos: [{ repo: 'octo/private', sensitivity: 'internal' }], }]); const compose = generateDockerCompose(composeConfig({ enclaves }), networkConfig); - expect(compose.networks[ENCLAVE_GITHUB_CONTROL_NETWORK]).toMatchObject({ - name: ENCLAVE_GITHUB_CONTROL_NETWORK, - driver: 'bridge', - internal: true, - ipam: { config: [{ subnet: ENCLAVE_GITHUB_CONTROL_SUBNET }] }, - }); + expect(compose.services).not.toHaveProperty('enclave-agent-github-mcp'); const enclaveMembers = Object.entries(compose.services) .filter(([, service]) => Object.prototype.hasOwnProperty.call( (service as Record).networks ?? {}, @@ -445,13 +412,9 @@ describe('unified enclave compose topology', () => { )) .map(([name]) => name) .sort(); - expect(enclaveMembers).toEqual([ - 'enclave-agent-api-proxy', - 'enclave-agent-cli-proxy', - ]); + expect(enclaveMembers).toEqual(['enclave-agent-api-proxy']); } finally { process.env = originalEnv; - fs.rmSync(directory, { recursive: true, force: true }); } }); }); diff --git a/src/services/enclave-mcp-service.ts b/src/services/enclave-mcp-service.ts index 6d003741b..ad7bf2458 100644 --- a/src/services/enclave-mcp-service.ts +++ b/src/services/enclave-mcp-service.ts @@ -1,5 +1,4 @@ import { - ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME, ENCLAVE_AGENT_API_PROXY_CONTAINER_NAME, ENCLAVE_MCP_SERVER_CONTAINER_NAME, LOCAL_ENCLAVE_MCP_SERVER_IMAGE, @@ -10,8 +9,7 @@ import type { EnclaveAgentEngine, EnclaveAgentProfile } from '../types/enclave-o import { ENCLAVE_SERVER_AUDIT_DIR, ENCLAVE_SERVER_CAPABILITY_PATH, - ENCLAVE_SERVER_GITHUB_CAPABILITY_KEY_PATH, - ENCLAVE_SERVER_GITHUB_RUN_IDENTITY_PATH, + ENCLAVE_SERVER_GITHUB_AGENT_ID_PATH, ENCLAVE_SERVER_CONTROL_DIR, ENCLAVE_SERVER_DOCKER_SOCKET_PATH, ENCLAVE_SERVER_SEED_MAP_PATH, @@ -23,13 +21,8 @@ import { import { ENCLAVE_AGENT_API_PROXY_ALIAS, ENCLAVE_AGENT_API_PROXY_IP, - ENCLAVE_AGENT_CLI_PROXY_IP, ENCLAVE_AGENT_EGRESS_NETWORK, ENCLAVE_AGENT_NETWORK, - ENCLAVE_GITHUB_CONTROL_NETWORK, - ENCLAVE_GITHUB_CLI_PROXY_IP, - ENCLAVE_GITHUB_PROXY_ALIAS, - ENCLAVE_GITHUB_PROXY_PORT, ENCLAVE_MCP_CONTROL_ALIAS, ENCLAVE_MCP_CONTROL_NETWORK, } from '../enclave/network'; @@ -48,9 +41,10 @@ import { VERTEX_ENV, } from '../api-proxy-env-constants'; import { buildRuntimeImageRef } from '../image-tag'; -import { assignImageSource } from '../image-tag'; -import { CLI_PROXY_PORT } from '../types'; -import { resolveEnclaveGithubGatewayContract } from '../enclave/github-gateway'; +import { + ENCLAVE_GITHUB_MCP_INTERNAL_URL, + resolveEnclaveGithubGatewayContract, +} from '../enclave/github-gateway'; /** * Compose assembly for the unified enclave MCP server and its executors. @@ -65,7 +59,7 @@ import { resolveEnclaveGithubGatewayContract } from '../enclave/github-gateway'; * - **script enclaves** run with `--network none`. * - **agent enclaves** join *only* the dedicated `internal` * {@link ENCLAVE_AGENT_NETWORK}. Its mandatory peer is a dedicated API proxy; - * issues-read-v1 adds only a PAT-free AWF CLI proxy. No primary agent, Squid, + * issues-read-v1 adds only the policy-isolated shared MCP gateway. No primary agent, Squid, * general proxy, MCP server, safe outputs, or MCP gateway is on that network, * and the API proxy is the only holder of a provider credential. * - the **primary agent** receives no socket, capability, direct URL, private @@ -92,8 +86,6 @@ export interface EnclaveMcpBuildResult { agentImageService?: Record; /** Dedicated credential sidecar for agent enclaves, when that executor runs. */ agentApiProxyService?: Record; - /** PAT-free CLI proxy for the optional issues-read-v1 enclave profile. */ - agentCliProxyService?: Record; service: Record; } @@ -237,61 +229,6 @@ function buildAgentApiProxyService(params: { return service; } -function buildAgentCliProxyService(params: { - config: WrapperConfig; - imageConfig: ImageBuildConfig; - logsPath: string; -}): Record { - const contract = resolveEnclaveGithubGatewayContract(params.config); - const service: Record = { - container_name: ENCLAVE_AGENT_CLI_PROXY_CONTAINER_NAME, - networks: { - [ENCLAVE_AGENT_NETWORK]: { - ipv4_address: ENCLAVE_AGENT_CLI_PROXY_IP, - }, - [ENCLAVE_GITHUB_CONTROL_NETWORK]: { - ipv4_address: ENCLAVE_GITHUB_CLI_PROXY_IP, - }, - }, - volumes: applyHostPathPrefixToVolumes( - [ - `${params.logsPath}:/var/log/cli-proxy:rw`, - `${contract.caCertPath}:/tmp/proxy-tls/ca.crt:ro`, - ], - params.config.dockerHostPathPrefix, - ), - environment: { - AWF_CLI_PROXY_MODE: 'enclave', - AWF_CLI_PROXY_PROFILE: 'issues-read-v1', - AWF_DIFC_PROXY_HOST: ENCLAVE_GITHUB_PROXY_ALIAS, - AWF_DIFC_PROXY_PORT: String(ENCLAVE_GITHUB_PROXY_PORT), - AWF_CLI_PROXY_LOG_DIR: '/var/log/cli-proxy', - }, - healthcheck: { - test: ['CMD', 'curl', '-f', `http://127.0.0.1:${CLI_PROXY_PORT}/health`], - interval: '2s', - timeout: '2s', - retries: 10, - start_period: '5s', - }, - ...buildContainerSecurityHardening({ memLimit: '256m', pidsLimit: 50, cpuShares: 256 }), - restart: 'no', - stop_grace_period: '5s', - }; - assignImageSource(service, { - useGHCR: params.imageConfig.useGHCR, - registry: params.imageConfig.registry, - imageName: 'cli-proxy', - parsedTag: params.imageConfig.parsedTag, - projectRoot: params.imageConfig.projectRoot, - containerDir: 'cli-proxy', - }); - if (params.imageConfig.useGHCR && params.imageConfig.resolveImage) { - service.image = params.imageConfig.resolveImage('cli-proxy'); - } - return service; -} - export function buildEnclaveMcpService(params: EnclaveMcpServiceParams): EnclaveMcpBuildResult { const { config, imageConfig } = params; const enclaves = config.enclaves; @@ -358,6 +295,9 @@ export function buildEnclaveMcpService(params: EnclaveMcpServiceParams): Enclave throw new Error('buildEnclaveMcpService: the enclave agent executor requires network configuration'); } const { imageRef, source } = resolveAgentImage(imageConfig, agent.image); + const githubGatewayContract = agent.github?.cli === 'issues-read-v1' + ? resolveEnclaveGithubGatewayContract(config) + : undefined; result.agentImageService = { ...source, network_mode: 'none', @@ -375,14 +315,6 @@ export function buildEnclaveMcpService(params: EnclaveMcpServiceParams): Enclave engine: agent.engine, profile: agent.profile, }); - if (agent.github?.cli === 'issues-read-v1') { - result.agentCliProxyService = buildAgentCliProxyService({ - config, - imageConfig, - logsPath: paths.githubCliProxyLogsDir, - }); - dependsOn['enclave-agent-cli-proxy'] = { condition: 'service_healthy' }; - } const apiPort = resolveEnclaveAgentApiPort(agent.engine, agent.profile); Object.assign(environment, { AWF_ENCLAVE_AGENT_IMAGE: imageRef, @@ -405,12 +337,9 @@ export function buildEnclaveMcpService(params: EnclaveMcpServiceParams): Enclave AWF_ENCLAVE_AGENT_GITHUB_ENABLED: String(agent.github?.cli === 'issues-read-v1'), ...(agent.github?.cli === 'issues-read-v1' && { AWF_ENCLAVE_AGENT_GITHUB_PROFILE: agent.github.cli, - AWF_ENCLAVE_AGENT_GITHUB_PROXY_URL: - `http://${ENCLAVE_AGENT_CLI_PROXY_IP}:${CLI_PROXY_PORT}`, - AWF_ENCLAVE_AGENT_GITHUB_CAPABILITY_KEY_PATH: - ENCLAVE_SERVER_GITHUB_CAPABILITY_KEY_PATH, - AWF_ENCLAVE_AGENT_GITHUB_RUN_IDENTITY_PATH: - ENCLAVE_SERVER_GITHUB_RUN_IDENTITY_PATH, + AWF_ENCLAVE_AGENT_GITHUB_MCP_URL: ENCLAVE_GITHUB_MCP_INTERNAL_URL, + AWF_ENCLAVE_AGENT_GITHUB_AGENT_ID_PATH: ENCLAVE_SERVER_GITHUB_AGENT_ID_PATH, + AWF_ENCLAVE_AGENT_GITHUB_GATEWAY_CONTAINER: githubGatewayContract?.containerName, }), ...(agent.maxModelRequests !== undefined && { AWF_ENCLAVE_AGENT_MAX_MODEL_REQUESTS: String(agent.maxModelRequests), @@ -436,7 +365,7 @@ export function buildEnclaveMcpService(params: EnclaveMcpServiceParams): Enclave ]; if (agent?.github?.cli === 'issues-read-v1') { serverVolumes.push( - `${paths.githubCapabilityKeyPath}:${ENCLAVE_SERVER_GITHUB_CAPABILITY_KEY_PATH}:ro`, + `${paths.githubAgentIdPath}:${ENCLAVE_SERVER_GITHUB_AGENT_ID_PATH}:ro`, ); } @@ -477,5 +406,4 @@ export const enclaveMcpServiceTestHelpers = { resolveScriptImage, resolveServerImage, toDaemonVisiblePath, - buildAgentCliProxyService, }; diff --git a/src/services/optional-services.ts b/src/services/optional-services.ts index da3846822..badf4fa79 100644 --- a/src/services/optional-services.ts +++ b/src/services/optional-services.ts @@ -348,13 +348,11 @@ function assembleEnclaveMcpService(params: AssembleOptionalServicesParams): void scriptImageService, agentImageService, agentApiProxyService, - agentCliProxyService, service, } = buildEnclaveMcpService({ config, imageConfig, networkConfig: params.networkConfig }); if (scriptImageService) services['enclave-script-image'] = scriptImageService; if (agentImageService) services['enclave-agent-image'] = agentImageService; if (agentApiProxyService) services['enclave-agent-api-proxy'] = agentApiProxyService; - if (agentCliProxyService) services['enclave-agent-cli-proxy'] = agentCliProxyService; services['enclave-mcp-server'] = service; // The gateway readiness gate is host-orchestrated before agent startup. There // is deliberately no agent dependency, mount, environment, or direct URL.