Skip to content

refactor: centralize runtime mount allow/deny lists in a declarative config#6339

Merged
lpcox merged 5 commits into
mainfrom
centralize-mount-policy
Jul 18, 2026
Merged

refactor: centralize runtime mount allow/deny lists in a declarative config#6339
lpcox merged 5 commits into
mainfrom
centralize-mount-policy

Conversation

@lpcox

@lpcox lpcox commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Problem

There was no centralized allow/deny list of files and directories for all runtimes. What the host filesystem exposes to an agent was hand-maintained across five separate modules, with a different representation per runtime:

  • system-mounts.ts — system dir allow list (compose)
  • etc-mounts.ts/etc file allow list (compose)
  • home-whitelist.ts$HOME tool-subdir allow list + forbidden-dir guard (shared)
  • credential-hiding.ts — credential-file deny list as /dev/null overlays (compose)
  • sbx-manager.ts — credential deny list as a per-parent map (sbx microVM)

Because the two credential lists were maintained independently they had drifted: compose masked .cargo/credentials but not .copilot/config.json / .claude/.credentials.json / .gemini/*, while sbx did the opposite. This is exactly the centralization deferred in #6336.

Fix

Introduce a single declarative source of truth:

  • src/config/sandbox-mount-policy.json — the allow lists (system dirs, /etc files, $HOME tool subdirs) and deny lists (forbidden $HOME credential dirs, on-disk credential stores), each entry annotated with a reason.
  • src/config/mount-policy.ts — loads, validates (relative paths, no .., unique paths, files only on dir entries), and exposes typed accessors.

Every runtime now derives its mounts from the policy:

Module Change
system-mounts.ts reads system.directories.{default,sysroot}
etc-mounts.ts reads system.etc (DinD staging / identity synthesis logic unchanged)
home-whitelist.ts now a thin re-export shim (HOME_TOOL_SUBDIRS, HOME_FORBIDDEN_SUBDIRS)
credential-hiding.ts compose /dev/null overlays derived from credentialFilesToHide()
sbx-manager.ts microVM move-aside/restore derived from credentialEntriesUnderMountedParents()

The two backends still use their native mechanisms (compose /dev/null file overlays; sbx directory move-aside), but from the same list, so they can't drift again.

Security improvement (closes the compose gap)

Centralizing means compose now also masks .claude/.credentials.json, .copilot/config.json, .cargo/credentials.toml and .gemini/* token files — the gap explicitly flagged in #6336. Masking a non-existent path via /dev/null is a no-op, and agents receive credentials via the api-proxy/environment, not these on-disk stores, so this is safe.

Packaging

The JSON is imported statically (resolveJsonModule), so tsc emits it to dist/config/ and the esbuild release bundle inlines it — no runtime file read or extra copy step. Verified node scripts/build-bundle.mjs inlines the policy and the bundle runs.

Verification

  • npm run build clean · npm run type-check clean
  • npm test: 243 suites / 3836 tests pass (added src/config/mount-policy.test.ts with loader + invariant coverage; updated credential-hiding / home-whitelist / sbx tests)
  • ESLint: 0 errors on changed files
  • Docs: docs/mount-policy.md

Notes

  • system.* is compose-only (Docker + gVisor); the sbx microVM gets system libraries from its guest image, so it doesn't consume that section.
  • The dynamic logic (DinD path staging, /etc/passwd/group synthesis, sysroot) stays in code — only the static path lists moved to the config.

…config

The allow lists (system dirs, /etc files, $HOME tool subdirs) and deny lists
(forbidden $HOME credential dirs, on-disk credential stores) were hand-maintained
in five separate modules — system-mounts.ts, etc-mounts.ts, home-whitelist.ts,
credential-hiding.ts and sbx-manager.ts — one representation per runtime, which
let them drift (e.g. compose masked .cargo/credentials but not
.copilot/config.json, while sbx did the opposite).

Introduce src/config/sandbox-mount-policy.json as the single declarative source
of truth, loaded and validated by src/config/mount-policy.ts. All runtimes now
derive their mounts from it:

- system-mounts.ts / etc-mounts.ts read the compose allow lists from the policy
- home-whitelist.ts becomes a thin re-export shim
- credential-hiding.ts (compose /dev/null overlays) and sbx-manager.ts (microVM
  move-aside/restore) both derive from the one credentials list

This also closes the compose gap: .claude/.credentials.json, .copilot/config.json
and .gemini/* token files are now masked in compose mode too.

The JSON is imported statically (resolveJsonModule) so it ships via tsc to dist/
and is inlined by the esbuild release bundle — no extra packaging step. Docs in
docs/mount-policy.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 23717692-af7a-4e03-a156-5b696c3f01bd
Copilot AI review requested due to automatic review settings July 18, 2026 03:24
@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Documentation Preview

Documentation build failed for this PR. View logs.

Built from commit 819290a

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Coverage Regression Detected

This PR decreases test coverage. Please add tests to maintain coverage levels.

Overall Coverage

Metric Base PR Delta
Lines 98.94% 98.75% 📉 -0.19%
Statements 98.86% 98.69% 📉 -0.17%
Functions 99.35% 99.24% 📉 -0.11%
Branches 95.16% 94.86% 📉 -0.30%
📁 Per-file Coverage Changes (3 files)
File Lines (Before → After) Statements (Before → After)
src/sbx-manager.ts 96.6% → 96.6% (+0.00%) 96.2% → 96.2% (-0.02%)
src/services/agent-volumes/etc-mounts.ts 98.3% → 98.4% (+0.03%) 98.3% → 98.4% (+0.05%)
src/log-directory-setup.ts 96.2% → 100.0% (+3.78%) 96.3% → 100.0% (+3.71%)
✨ New Files (1 files)
  • src/config/mount-policy.ts: 78.1% lines

Coverage comparison generated by scripts/ci/compare-coverage.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Centralizes sandbox filesystem mount policy across Compose, gVisor, and sbx runtimes.

Changes:

  • Adds a declarative mount-policy JSON with typed accessors and validation.
  • Migrates system, home, /etc, and credential mounts to the shared policy.
  • Adds policy tests and documentation.
Show a summary per file
File Description
src/config/sandbox-mount-policy.json Defines centralized mount allow/deny lists.
src/config/mount-policy.ts Loads and exposes the policy.
src/config/mount-policy.test.ts Tests policy accessors and invariants.
src/services/agent-volumes/system-mounts.ts Derives system mounts from policy.
src/services/agent-volumes/etc-mounts.ts Derives /etc mounts from policy.
src/services/agent-volumes/home-whitelist.ts Re-exports centralized home lists.
src/services/agent-volumes/home-whitelist.test.ts Tests compatibility exports.
src/services/agent-volumes/credential-hiding.ts Derives credential overlays from policy.
src/services/agent-volumes/credential-hiding.test.ts Tests policy-derived overlays.
src/sbx-manager.ts Derives sbx credential scrubbing from policy.
docs/mount-policy.md Documents policy structure and usage.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (1)

src/config/mount-policy.ts:175

  • Directory entries without files are silently omitted from Compose masking. Since Compose mounts .config wholesale (home-strategy.ts:46-47), the policy's .config/heroku, .config/hub, and .config/op credential directories remain readable there even though they are declared deny entries; only sbx enforces them. Add a Compose directory-overlay mechanism (for example, an empty directory/volume) or reject directory entries that Compose cannot enforce.
    } else if (entry.files) {
      for (const f of entry.files) {
        files.push(`${entry.path}/${f}`);
      }
    }
  • Files reviewed: 11/11 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment on lines +58 to +62
function assertStringArray(value: unknown, label: string): readonly string[] {
if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {
fail(`${label} must be an array of strings`);
}
return value as readonly string[];
Comment thread src/sbx-manager.ts Outdated
* Whitelisted `$HOME` subdirs the sbx backend mounts wholesale into the microVM:
* the shared tool-dir allow list plus the agent-state dirs `.copilot`/`.gemini`.
*/
const SBX_MOUNTED_HOME_SUBDIRS: readonly string[] = ['.copilot', ...HOME_TOOL_SUBDIRS, '.gemini'];
@lpcox

lpcox commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot Fix the code for all comments in this review thread.

When a review comment includes a suggested change, apply the suggestion exactly.

Do not make changes beyond what is described in the linked review thread.

Copilot AI commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

@copilot Fix the code for all comments in this review thread.

When a review comment includes a suggested change, app...

Done in the latest commit. Both review comments addressed:

Path invariant validation (src/config/mount-policy.ts):

  • assertStringArray is now supplemented by two context-specific validators:
    • assertHomeSubdirArray — rejects empty strings, / separators, .. components, absolute paths, and duplicates (used for home.toolSubdirs and home.forbiddenSubdirs)
    • assertAbsolutePathArray — requires a leading /, rejects .. and duplicates (used for system.directories.* and system.etc)
  • parseCredentials now validates files entries within dir credentials: no / (plain filenames only), no .., no empties, no duplicates within the same entry

Centralize .copilot/.gemini (src/config/sandbox-mount-policy.json, src/sbx-manager.ts, src/services/agent-volumes/home-strategy.ts):

  • Both dirs added to home.toolSubdirs in the policy
  • SBX_MOUNTED_HOME_SUBDIRS removed; sbx now iterates HOME_TOOL_SUBDIRS directly
  • Compose home-strategy.ts: .gemini conditional block removed (the loop now handles it); the loop skips .copilot since its special handling (existence check + session-state/logs sub-mounts) already runs above it

Copilot finished work on behalf of lpcox July 18, 2026 04:36
@github-actions

Copy link
Copy Markdown
Contributor

✅ Copilot review passed with no inline comments.

@lpcox Add the ready-for-aw label to this PR to trigger agentic CI smoke tests.

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🌑 The shadows whisper... Smoke Codex failed. The oracle requires further meditation...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

📰 VERDICT: Smoke Docker Sbx has concluded. All systems operational. This is a developing story. 🎤

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🔑 Smoke Copilot PAT reports failed. PAT auth path may have issues...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

📰 DEVELOPING STORY: Smoke Copilot reports failed. Our correspondents are investigating the incident...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

📡 Smoke OTel Tracing reports failed. OTel tracing regression detected. ⚠️

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Copilot BYOK AOAI (Entra) reports failed. AOAI BYOK (Entra) mode investigation needed...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🔌 Smoke Services — Service connectivity failed ⚠️

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Contribution Check failed. Please review the logs for details.

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🛡️ Smoke Copilot Network Isolation reports failed while checking network isolation. Investigate the egress model.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Security Guard has started processing this pull request

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Build Test Failed Build Test Suite - See logs for details

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Copilot BYOK AOAI (api-key) reports failed. AOAI BYOK (api-key) mode investigation needed...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Chroot tests failed Smoke Chroot failed - See logs for details.

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Gemini completed. All facets verified. 💎

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Claude passed

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Copilot BYOK reports failed. BYOK mode investigation needed...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

📡 Smoke OTel Tracing reports failed. OTel tracing regression detected. ⚠️

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Build Test Failed Build Test Suite - See logs for details

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🔌 Smoke Services — Service connectivity failed ⚠️

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Copilot BYOK AOAI (Entra) reports failed. AOAI BYOK (Entra) mode investigation needed...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Chroot tests failed Smoke Chroot failed - See logs for details.

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

📰 VERDICT: Smoke Docker Sbx has concluded. All systems operational. This is a developing story. 🎤

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Contribution Check failed. Please review the logs for details.

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Copilot BYOK reports failed. BYOK mode investigation needed...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🔑 Smoke Copilot PAT reports failed. PAT auth path may have issues...

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Security Guard has started processing this pull request

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🌑 The shadows whisper... Smoke Codex failed. The oracle requires further meditation...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Gemini completed. All facets verified. 💎

Smoke test completed with partial failures. Comment added to PR #6339.

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Copilot BYOK AOAI (api-key) reports failed. AOAI BYOK (api-key) mode investigation needed...

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🛡️ Smoke Copilot Network Isolation reports failed while checking network isolation. Investigate the egress model.

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Smoke Claude passed

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Coverage Regression Detected

This PR decreases test coverage. Please add tests to maintain coverage levels.

Overall Coverage

Metric Base PR Delta
Lines 98.94% 98.68% 📉 -0.26%
Statements 98.86% 98.49% 📉 -0.37%
Functions 99.35% 99.24% 📉 -0.11%
Branches 95.16% 94.55% 📉 -0.61%
📁 Per-file Coverage Changes (4 files)
File Lines (Before → After) Statements (Before → After)
src/sbx-manager.ts 96.6% → 96.6% (-0.05%) 96.2% → 96.1% (-0.07%)
src/services/agent-volumes/etc-mounts.ts 98.3% → 98.4% (+0.03%) 98.3% → 98.4% (+0.05%)
src/artifact-permissions.ts 97.3% → 97.4% (+0.14%) 97.3% → 97.4% (+0.14%)
src/log-directory-setup.ts 96.2% → 100.0% (+3.78%) 96.3% → 100.0% (+3.71%)
✨ New Files (1 files)
  • src/config/mount-policy.ts: 80.2% lines

Coverage comparison generated by scripts/ci/compare-coverage.ts

@github-actions

Copy link
Copy Markdown
Contributor

Smoke Test: Claude Engine Validation

Check Result
API Status ✅ PASS
GH Check ✅ PASS
File Status ✅ PASS

Overall Result: PASS

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

Generated by Smoke Claude for #6339 · 55.4 AIC · ⊞ 3.3K ·
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

Smoke Test: Gemini Engine Validation

  • GitHub MCP Testing: ❌ (Filtered by secrecy policy)
  • GitHub.com Connectivity: ❌ (curl status 000)
  • File Writing Testing: ✅ PASS
  • Bash Tool Testing: ✅ PASS

Overall status: FAIL

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • localhost

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "localhost"

See Network Configuration for more information.

💎 Faceted by Smoke Gemini
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smoke Test: Docker Sbx Validation

Test Result
GitHub MCP connectivity ✅ Connected
GitHub.com HTTP check ⚠️ Template var not expanded
File write/read ⚠️ Template var not expanded

Overall: PARTIAL — MCP test passed; pre-step output variables (${{ steps.smoke-data.outputs.* }}) were not expanded before agent execution, so HTTP and file results could not be verified.

PR author: @lpcox

📰 BREAKING: Report filed by Smoke Docker Sbx
Add label ready-for-aw to run again

@lpcox
lpcox merged commit ac1ae7c into main Jul 18, 2026
130 of 144 checks passed
@lpcox
lpcox deleted the centralize-mount-policy branch July 18, 2026 15:34
lpcox added a commit that referenced this pull request Jul 18, 2026
The centralize-mount-policy change (#6339) added `.copilot/config.json`
to the credential deny list with the reason "Copilot CLI persisted auth
token". That is incorrect: config.json holds only experiment-assignment
cache, first-launch timestamps, tips-shown flags and installed-plugin
metadata — no token. The Copilot CLI's actual credentials live elsewhere
(e.g. its sqlite stores), and `~/.copilot` is already bind-mounted rw as
a tool subdir, so masking config.json protected nothing.

Worse, it broke the Copilot CLI: compose masks credential files with a
read-only /dev/null overlay, and the CLI reads AND rewrites config.json
at startup (via temp-file + atomic rename), so it crashed silently at
launch (exit 1, zero output) — failing smoke-copilot consistently. A
rename can't target a bind-mounted file anyway, so masking that path is
fundamentally incompatible with how the CLI writes its config.

Fix: remove `.copilot/config.json` from the credentials deny list. The
CLI now reads/writes its config normally, matching pre-#6339 behavior.
Updated the sbx/compose tests, sbx-manager comment, and sbx-integration
docs that used config.json as their example credential path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4bdbe67a-cb99-41a2-944b-49cfd949360e
lpcox added a commit that referenced this pull request Jul 18, 2026
The centralize-mount-policy change (#6339) added `.copilot/config.json`
to the credential deny list with the reason "Copilot CLI persisted auth
token". That is incorrect: config.json holds only experiment-assignment
cache, first-launch timestamps, tips-shown flags and installed-plugin
metadata — no token. The Copilot CLI's actual credentials live elsewhere
(e.g. its sqlite stores), and `~/.copilot` is already bind-mounted rw as
a tool subdir, so masking config.json protected nothing.

Worse, it broke the Copilot CLI: compose masks credential files with a
read-only /dev/null overlay, and the CLI reads AND rewrites config.json
at startup (via temp-file + atomic rename), so it crashed silently at
launch (exit 1, zero output) — failing smoke-copilot consistently. A
rename can't target a bind-mounted file anyway, so masking that path is
fundamentally incompatible with how the CLI writes its config.

Fix: remove `.copilot/config.json` from the credentials deny list. The
CLI now reads/writes its config normally, matching pre-#6339 behavior.
Updated the sbx/compose tests, sbx-manager comment, and sbx-integration
docs that used config.json as their example credential path.


Copilot-Session: 4bdbe67a-cb99-41a2-944b-49cfd949360e

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants