Skip to content

Design: central sensitive-path registry for /tmp/gh-aw with runtime exposure audit #9219

Closed

Description

@lpcox

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:

  1. No single place declares which /tmp/gh-aw subdirectories are sensitive. The set is implicit in Docker's tmpfs: 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.
  2. 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.ts re-parses /proc/self/mountinfo and 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:

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/mountinfo verification in mount-tree.ts to 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 tmpfs list 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

Acceptance criteria

  • A single module declares sensitive paths with rationale and applicable runtimes.
  • Docker's tmpfs: list is derived from it, with /host variants generated rather than hand-written.
  • Cloud Hypervisor derives mask overlays from it and asserts them from /proc/self/mountinfo before virtiofsd starts.
  • mcp-payloads is a documented exemption, not an omission.
  • Unclassified /tmp/gh-aw children 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-aw references 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions