Design: central sensitive-path registry for /tmp/gh-aw with runtime exposure audit #9219
Description
Design proposal following #9204 (Cloud Hypervisor exposes mcpg logs) and #9208 (NVX parity).
Motivating correction
#9204 was filed as a novel exposure. It is better understood as a runtime parity regression.
The Docker runtime already masks the exact path in question. src/services/agent-service.ts:65-66 mounts a tmpfs over it inside the agent container:
tmpfs: [
'/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m',
'/host/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m',
`${config.workDir}:rw,noexec,nosuid,size=1m`,
`/host${config.workDir}:rw,noexec,nosuid,size=1m`,
...
]and src/log-directory-setup.ts:123 states the intent explicitly: "Inside the AWF container, it's hidden via tmpfs mount (see generateDockerCompose)." The same block also masks workDir so the agent cannot read docker-compose.yml and extract plaintext tokens.
So the decision that mcp-logs is sensitive was made deliberately, for Docker, and then not carried to Cloud Hypervisor. The problem is not that nobody knew — it is that the knowledge lives in a hardcoded array in one runtime's service builder, where a second runtime has no reason to look.
Problem statement
Two distinct gaps:
- No single place declares which
/tmp/gh-awsubdirectories are sensitive. The set is implicit in Docker'stmpfs:array. Cloud Hypervisor has no equivalent, NVX will need one (NVX: carry over Cloud Hypervisor export read-exposure masking when NVX adopts virtiofs #9208), and nothing links them. - No runtime audit of whether masking actually held. Docker's tmpfs is asserted only by construction. Cloud Hypervisor has a strong verification primitive (
mount-tree.tsre-parses/proc/self/mountinfoand fails closed) but nothing currently tells it what should be masked.
The durable risk is drift: a future component writes sensitive output to a new /tmp/gh-aw subdirectory, and no mechanism notices it became agent-readable on any runtime.
Proposed design
1. A single sensitive-path registry
New module, e.g. src/sensitive-paths.ts, declaring each sensitive path once with its rationale:
{
path: '/tmp/gh-aw/mcp-logs',
reason: 'mcpg logs pre-filter tool-call payloads (see #9204)',
exposure: 'read',
appliesTo: ['docker', 'cloud-hypervisor', 'nvx'],
}Consumers:
- Docker derives its
tmpfs:list from the registry, replacing the hardcoded strings. The/host-prefixed duplication is generated, not hand-maintained — chroot mode mounts/tmpat both locations, and forgetting the second entry is a silent failure. - Cloud Hypervisor derives mask overlays for the staged mount tree (mechanism in Cloud Hypervisor: guest agent can read mcpg pre-filter logs under /tmp/gh-aw #9204).
- NVX consumes the same registry when its export surface grows (NVX: carry over Cloud Hypervisor export read-exposure masking when NVX adopts virtiofs #9208).
Adding a sensitive directory becomes one entry rather than three separate edits in three runtimes.
Initial contents, from current behaviour plus #9204:
| Path | Reason |
|---|---|
/tmp/gh-aw/mcp-logs |
Pre-filter tool-call payloads |
${workDir} |
docker-compose.yml contains plaintext tokens and API keys |
/tmp/gh-aw/sandbox/firewall/logs |
Squid access log — a map of the egress policy |
/tmp/gh-aw/sandbox/firewall/audit |
Firewall audit output |
Explicit non-entry: /tmp/gh-aw/mcp-payloads must stay readable. mcpg instructs the agent to read spilled payloads at payloadPath (gh-aw-mcpg internal/middleware/jqschema.go:39), spilled content is written after the jq filter, and it is session-segmented. The registry should carry this as a documented exemption so a future change does not tidy it into the mask list.
2. Runtime audit, per runtime
- Cloud Hypervisor: extend the existing
/proc/self/mountinfoverification inmount-tree.tsto assert that every registry entry falling inside an export root is genuinely masked, before virtiofsd starts. This reuses a primitive that already fails closed rather than adding a parallel check. - Docker: assert the generated compose
tmpfslist covers every applicable registry entry, including/host-prefixed variants. - NVX: deferred to NVX: carry over Cloud Hypervisor export read-exposure masking when NVX adopts virtiofs #9208; the registry is the contract it will implement against.
3. Fail closed on unclassified paths
The registry alone does not solve drift — it still requires someone to add the entry. Stronger form: at export time, enumerate the immediate children of /tmp/gh-aw and reject any that are neither explicitly exported-safe nor explicitly masked.
This converts silent exposure into a loud, build-time failure. It is deliberately noisy, and the tradeoff is real: every new subdirectory forces a classification decision. That is the point, but it should be a conscious choice — possibly warn-only initially, escalating to hard failure once the current set is classified.
4. Report the resolved set
Emit the resolved mask set into the existing per-run audit output, so exposure is reviewable after the fact rather than only asserted in-process. This also gives CI something to diff when the set changes unexpectedly.
Non-goals
- Changing mcpg's logging behaviour. Whether
EnablePayloadRedaction()should apply outside enclave/delegation profiles is an mcpg-side policy question (noted in Cloud Hypervisor: guest agent can read mcpg pre-filter logs under /tmp/gh-aw #9204). - Relocating
MCP_GATEWAY_LOG_DIR. That is the cheap immediate mitigation in Cloud Hypervisor: guest agent can read mcpg pre-filter logs under /tmp/gh-aw #9204 and is compiler-side in gh-aw; it should land first and is independent of this design.
Acceptance criteria
- A single module declares sensitive paths with rationale and applicable runtimes.
- Docker's
tmpfs:list is derived from it, with/hostvariants generated rather than hand-written. - Cloud Hypervisor derives mask overlays from it and asserts them from
/proc/self/mountinfobefore virtiofsd starts. -
mcp-payloadsis a documented exemption, not an omission. - Unclassified
/tmp/gh-awchildren are detected (warn, then fail). - The resolved mask set appears in per-run audit output.
- Parity between runtimes is asserted in tests, not left to review.
Verification notes
- Docker masking and intent:
src/services/agent-service.ts:45-71,src/log-directory-setup.ts:120-127 - Hardcoded
/tmp/gh-awreferences across the codebase:src/dind-bootstrap.ts,src/dind-probe.ts,src/cloud-hypervisor/exports.ts:95-96,src/types/volume-options.ts - Cloud Hypervisor mount verification primitive:
src/cloud-hypervisor/mount-tree.ts - NVX export surface (currently capped at 2 entries):
src/nvx/workspace-export.ts
Activity