Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/awf-config-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1825,6 +1825,16 @@ the first supporting AWF release. Older AWF versions reject the closed

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.

`buildEnclaveMcpgUpstreamContract()` emits the mcpg upstream `Authorization`
header as a literal `AWF_ENCLAVE_MCP_CAPABILITY` environment-variable-reference
template, never the resolved capability value, and takes no environment
argument so it cannot bake a real secret into the contract. Any config adapter
that renders this contract into an engine/tool config file MUST copy the
template verbatim and resolve it from the MCP client's own process environment
at request time; persisting the resolved capability to any agent-readable file
(including under `GITHUB_WORKSPACE`) would make it readable via ordinary
file-read tools. See github/gh-aw-firewall#7787.

For `issues-read-v1`, the compiler supplies
`AWF_ENCLAVE_GITHUB_PROXY_CONTAINER`,
`AWF_ENCLAVE_GITHUB_PROXY_IDENTITY`,
Expand Down
16 changes: 16 additions & 0 deletions docs/enclaves-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ executor tools. The compiler generates a fresh 64-character lowercase
hexadecimal capability, substitutes it into the mcpg authorization header, and
passes it to AWF without exposing it to the primary agent.

### Workspace credential exposure

`buildEnclaveMcpgUpstreamContract()` intentionally emits the mcpg upstream
`Authorization` header as the literal environment-variable-reference template
`ENCLAVE_MCP_AUTHORIZATION_HEADER_TEMPLATE` (`Bearer` followed by a reference
to `AWF_ENCLAVE_MCP_CAPABILITY`), never the resolved 64-character hexadecimal
capability. This function takes no environment argument, so it structurally
cannot bake a real secret into the contract it returns. Any config-adapter
that renders this contract into an engine or tool config file (for example
under `GITHUB_WORKSPACE`) MUST copy this template string verbatim and let the
MCP client resolve it from its own process environment at request time.
Resolving the reference and persisting the literal capability to any
agent-readable file defeats this control and makes the gateway credential
readable by ordinary file-read tools, not only by code execution or
process-memory access. See github/gh-aw-firewall#7787.

`gh-aw-mcpg` may start before the enclave server. While the backend is
unavailable, mcpg returns retryable HTTP `503 backend_unavailable`; AWF retries
the complete `initialize` handshake with bounded 500 ms backoff until
Expand Down
12 changes: 12 additions & 0 deletions src/enclave/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import execa from 'execa';
import { normalizeEnclavesConfig } from '../parsers/enclave-parser';
import type { WrapperConfig } from '../types';
import {
ENCLAVE_MCP_CAPABILITY_ENV,
ENCLAVE_MCP_GATEWAY_RUN_LABEL,
assertEnclaveGatewayReady,
buildEnclaveMcpgUpstreamContract,
Expand Down Expand Up @@ -189,6 +190,17 @@ describe('enclave mcpg handoff', () => {
});
});

it('never bakes a resolved capability into the mcpg upstream contract (github/gh-aw-firewall#7787)', () => {
const header = buildEnclaveMcpgUpstreamContract(config()).server.headers.Authorization;
// The header must remain a literal environment-variable-reference template
// so downstream config adapters can only write a reference (never the
// resolved secret) into any file under GITHUB_WORKSPACE or elsewhere.
const expectedTemplate = 'Bearer ' + '$' + `{${ENCLAVE_MCP_CAPABILITY_ENV}}`;
expect(header).toBe(expectedTemplate);
const hexCapabilityPattern = new RegExp('^Bearer ' + '[0-9a-f]' + '{64}$');
expect(hexCapabilityPattern.test(header)).toBe(false);
});

it('keeps readiness contracts byte-equivalent to the server tool definitions', () => {
expect(enclaveGatewayTestHelpers.expectedTools(config(true))).toEqual([
enclaveProtocol.TOOL,
Expand Down
16 changes: 15 additions & 1 deletion src/enclave/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ export const ENCLAVE_MCP_SERVER_NAME = 'awf-enclave';
export const ENCLAVE_MCP_UPSTREAM_URL = 'http://awf-enclave-mcp:8080/mcp';
const MCP_GATEWAY_API_KEY_ENV = 'MCP_GATEWAY_API_KEY';

/**
* Literal environment-variable-reference template for the mcpg upstream
* `Authorization` header. This is intentionally NOT the resolved capability
* value: the compiler handoff contract must carry only this template string
* into any generated engine/tool config so the real capability is never
* written to a file under `GITHUB_WORKSPACE` (or any other agent-readable
* path). Consumers of the contract MUST preserve this string verbatim and
* resolve `${AWF_ENCLAVE_MCP_CAPABILITY}` from the process environment only
* at request time, never persisting the resolved value to disk. See
* github/gh-aw-firewall#7787.
*/
export const ENCLAVE_MCP_AUTHORIZATION_HEADER_TEMPLATE =
'Bearer ' + '$' + `{${ENCLAVE_MCP_CAPABILITY_ENV}}`;

const DEFAULT_GATEWAY_CONTAINER = 'awmg-mcpg';
const DEFAULT_READINESS_TIMEOUT_MS = 120_000;
const REQUEST_TIMEOUT_MS = 5_000;
Expand Down Expand Up @@ -148,7 +162,7 @@ export function buildEnclaveMcpgUpstreamContract(config: WrapperConfig): Enclave
server: {
type: 'http',
url: ENCLAVE_MCP_UPSTREAM_URL,
headers: { Authorization: 'Bearer ' + '$' + `{${ENCLAVE_MCP_CAPABILITY_ENV}}` },
headers: { Authorization: ENCLAVE_MCP_AUTHORIZATION_HEADER_TEMPLATE },
tools: expectedTools(config).map((tool) => String(tool.name)),
connectTimeout: 120,
toolTimeout: ENCLAVE_MCP_OPERATION_TIMEOUT_SECONDS,
Expand Down