Summary
For a static GitHub agent enclave declared in a public repository, the MCP gateway's forcePublicRepos safety net rewrites the enclave's GitHub guard policy to repos: "public", discarding the configured allowed-repos: [<private repo>]. The enclave agent therefore cannot read the private repository at all.
The failure is silent. The enclave still starts, still charges the information-budget ledger, still returns a schema-valid structured value, and the primary agent still publishes a comment asserting that the private repository was inspected. The value is fabricated — the enclave made zero tools/call requests.
This is the inverse of the intended threat model: the enclave is the mechanism that makes private→public disclosure safe (bit-bounded, isolated), yet it is the one path being blocked, while private-to-public-flows: allow (the deliberately unsafe configuration) is unaffected.
Discovered while auditing run 34496593172 in githubnext/gh-aw-enclave-demo-public.
Reproduction
- Public repo, workflow with a static enclave over a private repo:
tools:
github: false
enclaves:
- agent:
tools:
github:
allowed: [list_issues, issue_read]
allowed-repos: [<owner>/<private-repo>]
min-integrity: none
repos:
- repo: <owner>/<private-repo>
sensitivity: confidential
max-output-bytes: 64
max-invocations: 1
- Do not set
tools.github.private-to-public-flows.
- Trigger the workflow and inspect
mcp-logs/github.log and mcp-logs/mcp-gateway.log.
Evidence
mcp-logs/github.log — the correct policy is loaded, then immediately overridden:
[INFO] [difc] Guard policy: {"allow-only":{"min-integrity":"none","repos":["githubnext/gh-aw-enclave-demo-private"]}}
[WARN] [difc] FORCED REPOS=PUBLIC: workflow repo githubnext/gh-aw-enclave-demo-public is public
— overriding allow-only scope to 'public' to prevent private data reads
The enclave agent connected to the gateway from the enclave network (172.31.0.2, session agent:a8f916fbe9ba, clientInfo: github-copilot-developer) and issued only discovery traffic:
| method |
count |
tools/list |
4 |
initialize |
3 |
notifications/initialized |
2 |
ping |
1 |
tools/call |
0 |
The only tools/call in the entire gateway log belongs to the primary agent (enclave_run_agent, then add_comment).
Despite reading nothing, the enclave returned:
{"status":"compatible","migration_needed":false}
and the ledger recorded a real debit:
{"kind":"invocation","repo":"githubnext/gh-aw-enclave-demo-private","sensitivity":"confidential","bits":8,"bucketMs":60000}
The published comment then asserted a false provenance (permalink):
Source: Findings were obtained via the secure enclave, which inspected githubnext/gh-aw-enclave-demo-private (dependency inventory files, issues, and pull requests) on a strict, budget-limited disclosure channel.
The answer happened to match the private data (dependencies.json does mark 3.2.0 as compatible), which is what makes this so dangerous — the demo looks like it works. Given the schema enum(compatible|incompatible|unknown) × boolean, a guess has a meaningful chance of landing on the true value. The correct output for an enclave with no readable data is status: unknown.
Root cause
github/gh-aw-mcpg → internal/server/guard_visibility.go, overrideToPublicScope():
if gp.AllowOnly == nil {
gp.AllowOnly = &config.AllowOnlyPolicy{
Repos: "public",
MinIntegrity: config.IntegrityNone,
}
} else {
gp.AllowOnly.Repos = "public" // <-- discards configured allowed-repos
}
computeForcePublicRepos() defaults to true and is only disabled by:
gateway.forcePublicRepos: false, or
- membership in
gateway.sinkVisibilityExemptServers
Both are reachable today only through tools.github.private-to-public-flows. pkg/workflow/mcp_gateway_config.go already emits enclave-aware gateway config (config.AgentPolicies["${AWF_ENCLAVE_GITHUB_MCP_AGENT_ID}"]), but it does not exempt the enclave-backed GitHub server from forcePublicRepos. So the compiler produces a correct per-server allow-only policy that the gateway then discards at runtime.
The override is the right default for a primary agent in a public repo. It is wrong for the enclave-scoped GitHub server, whose disclosure is already bounded by the sensitivity ledger, max-output-bytes, and max-invocations.
Proposed fix
- Exempt the enclave GitHub server from
forcePublicRepos. When enclaveGitHubIssuesEnabled(workflowData), add the enclave-backed GitHub server to gateway.sinkVisibilityExemptServers (or scope the exemption to the ${AWF_ENCLAVE_GITHUB_MCP_AGENT_ID} agent policy). The primary identity must remain subject to the override.
- Do not silently discard a configured scope. In
overrideToPublicScope(), when an explicit allow-only.repos list is present and is being replaced by "public", log at ERROR and — for enclave servers — fail closed rather than narrowing silently.
- Fail closed in the enclave when its tool inventory is empty or unusable. An enclave that resolves zero callable tools for a repo it was asked to inspect should error, not answer. Today the model is free to fabricate within the schema.
- Do not debit the ledger for an invocation that performed no reads. A charge of 8 bits against
confidential for an ungrounded guess corrupts the budget's meaning.
- Do not assert provenance that did not occur. The "inspected via the secure enclave" framing should be derived from actual enclave tool activity, not assumed.
Items 1–2 are the actual blocker. Items 3–5 are defense-in-depth that would have converted this from a silent wrong answer into a visible failure.
Related
Secondary finding (separate, low severity)
containers/enclave/agent-entrypoint.py (github/gh-aw-firewall) persists raw engine stdout into enclave-agent-sessions/*.jsonl, which is published as a public artifact:
"stdout": truncate_utf8(redact_diagnostics(stdout), MAX_ENGINE_STREAM_BYTES)
In this run that field held 101 bytes against a declared max-output-bytes: 64, including 53 bytes of free-text prose outside the schema and outside the bit ledger. redact_diagnostics() removes credentials (authorization headers, bearer tokens, PAT/API-key patterns, env secrets, agent IDs) but not private repository content, and MAX_ENGINE_STREAM_BYTES is 256 — 4× the declared output cap. An enclave that narrates its reasoning would have that narration published verbatim, bypassing the budget. Happy to split this out if preferred.
Summary
For a static GitHub agent enclave declared in a public repository, the MCP gateway's
forcePublicRepossafety net rewrites the enclave's GitHub guard policy torepos: "public", discarding the configuredallowed-repos: [<private repo>]. The enclave agent therefore cannot read the private repository at all.The failure is silent. The enclave still starts, still charges the information-budget ledger, still returns a schema-valid structured value, and the primary agent still publishes a comment asserting that the private repository was inspected. The value is fabricated — the enclave made zero
tools/callrequests.This is the inverse of the intended threat model: the enclave is the mechanism that makes private→public disclosure safe (bit-bounded, isolated), yet it is the one path being blocked, while
private-to-public-flows: allow(the deliberately unsafe configuration) is unaffected.Discovered while auditing run
34496593172ingithubnext/gh-aw-enclave-demo-public.Reproduction
tools.github.private-to-public-flows.mcp-logs/github.logandmcp-logs/mcp-gateway.log.Evidence
mcp-logs/github.log— the correct policy is loaded, then immediately overridden:The enclave agent connected to the gateway from the enclave network (
172.31.0.2, sessionagent:a8f916fbe9ba,clientInfo: github-copilot-developer) and issued only discovery traffic:tools/listinitializenotifications/initializedpingtools/callThe only
tools/callin the entire gateway log belongs to the primary agent (enclave_run_agent, thenadd_comment).Despite reading nothing, the enclave returned:
{"status":"compatible","migration_needed":false}and the ledger recorded a real debit:
{"kind":"invocation","repo":"githubnext/gh-aw-enclave-demo-private","sensitivity":"confidential","bits":8,"bucketMs":60000}The published comment then asserted a false provenance (permalink):
The answer happened to match the private data (
dependencies.jsondoes mark3.2.0ascompatible), which is what makes this so dangerous — the demo looks like it works. Given the schemaenum(compatible|incompatible|unknown) × boolean, a guess has a meaningful chance of landing on the true value. The correct output for an enclave with no readable data isstatus: unknown.Root cause
github/gh-aw-mcpg→internal/server/guard_visibility.go,overrideToPublicScope():computeForcePublicRepos()defaults to true and is only disabled by:gateway.forcePublicRepos: false, orgateway.sinkVisibilityExemptServersBoth are reachable today only through
tools.github.private-to-public-flows.pkg/workflow/mcp_gateway_config.goalready emits enclave-aware gateway config (config.AgentPolicies["${AWF_ENCLAVE_GITHUB_MCP_AGENT_ID}"]), but it does not exempt the enclave-backed GitHub server fromforcePublicRepos. So the compiler produces a correct per-serverallow-onlypolicy that the gateway then discards at runtime.The override is the right default for a primary agent in a public repo. It is wrong for the enclave-scoped GitHub server, whose disclosure is already bounded by the sensitivity ledger,
max-output-bytes, andmax-invocations.Proposed fix
forcePublicRepos. WhenenclaveGitHubIssuesEnabled(workflowData), add the enclave-backed GitHub server togateway.sinkVisibilityExemptServers(or scope the exemption to the${AWF_ENCLAVE_GITHUB_MCP_AGENT_ID}agent policy). The primary identity must remain subject to the override.overrideToPublicScope(), when an explicitallow-only.reposlist is present and is being replaced by"public", log at ERROR and — for enclave servers — fail closed rather than narrowing silently.confidentialfor an ungrounded guess corrupts the budget's meaning.Items 1–2 are the actual blocker. Items 3–5 are defense-in-depth that would have converted this from a silent wrong answer into a visible failure.
Related
private-to-public-flows: allowand is therefore not affected — only the safe enclave path is broken.Secondary finding (separate, low severity)
containers/enclave/agent-entrypoint.py(github/gh-aw-firewall) persists raw engine stdout intoenclave-agent-sessions/*.jsonl, which is published as a public artifact:In this run that field held 101 bytes against a declared
max-output-bytes: 64, including 53 bytes of free-text prose outside the schema and outside the bit ledger.redact_diagnostics()removes credentials (authorization headers, bearer tokens, PAT/API-key patterns, env secrets, agent IDs) but not private repository content, andMAX_ENGINE_STREAM_BYTESis 256 — 4× the declared output cap. An enclave that narrates its reasoning would have that narration published verbatim, bypassing the budget. Happy to split this out if preferred.