Skip to content

Commit 2188a01

Browse files
authored
docs: align guard references with current behavior (#344)
1 parent 1907f66 commit 2188a01

4 files changed

Lines changed: 45 additions & 87 deletions

File tree

‎docs/ARCHITECTURE.md‎

Lines changed: 9 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -258,63 +258,15 @@ Tool pins are first-observation TOFU, not install-time approval.
258258

259259
### OS confinement (F1 — released v0.16.0)
260260

261-
The relay detects patterns and enforces block decisions on JSON-RPC traffic.
262-
It cannot by itself *contain* the child it spawns — a server that decides to read
263-
`~/.ssh` or write `~/Library/LaunchAgents` never expresses that intent through
264-
inspectable traffic. `--confine` is **OS process enforcement**: it wraps the
265-
relayed child in an OS sandbox with selected secret-path read denials and
266-
write/network restrictions. This finite profile does not cover every credential
267-
location or prevent general exfiltration; configured secrets still reach the child.
268-
Wire inspection/enforcement and process confinement are complementary.
269-
**macOS-only in v1** (Linux `bwrap` deferred); **opt-in** — without
270-
`--confine`, enable/disable behavior is unchanged.
271-
272-
```
273-
mcpm guard enable --confine
274-
│ enrolls each UNWRAPPED stdio server it wraps
275-
▼
276-
~/.mcpm/guard-confine.yaml (+ .integrity) ← source of truth for "server X is confined"
277-
│
278-
│ wrap marker gains two neutral tokens before `--`:
279-
│ --confine-profile-hash <sha256> (binds marker ↔ stored profile)
280-
│ --confine-required (bare flag, replicated into IDE config)
281-
▼
282-
mcpm guard run --inner (spawn-time decision, src/guard/confine/decide.ts)
283-
│
284-
├── CONFINE — enrolled + hash matches + backend available
285-
│ └─ apply.ts wraps the child argv with backend-macos.ts (sandbox-exec)
286-
│ using the derived Seatbelt profile (derive.ts + profile.ts)
287-
│
288-
├── FAIL CLOSED (refuse to start, exit 1) — hash mismatch / malformed hash /
289-
│ stripped marker or wiped store on a require_confine server
290-
│
291-
└── HYBRID (WARN loudly + run UNCONFINED, never silent) — no OS backend
292-
(Linux/CI/Windows) or missing marker/profile on a NON-required server
293-
```
294-
295-
Standard tier (macOS Seatbelt): **read** allow-all *except* a secret-dir denylist
296-
(`~/.ssh`, `~/.aws`, `~/.gnupg`, cloud/gh/npm/docker/kube creds, keychains, browser
297-
cookie stores, MCP client config dirs, and mcpm's own `~/.mcpm`); **write** deny all of
298-
`$HOME` *except* caches, the per-server scratch dir, and system temp/`/dev` — one rule
299-
that blocks the whole persistence class (`~/.zshrc`, LaunchAgents, PATH-shadowing
300-
`~/bin`, git hooks); **net** launcher-classified (npx/uvx/pip/docker/… ⇒ network "all",
301-
everything else ⇒ egress-deny). The confine store reuses the shared
302-
`store-integrity.ts` extracted from `pins.ts` + `policy.ts` and fails closed on
303-
integrity/shape/format-version mismatch. CONFINE events (`confine-applied`,
304-
`confine-hash-mismatch`, `confine-marker-stripped`, …) append to
305-
`guard-events.jsonl`; the OWASP signature catalog count is unchanged.
306-
307-
The `sandbox-exec` path IS exercised in CI: the `confine-macos` job in `ci.yml` runs
308-
`scripts/dogfood-confine.sh` on a `macos-latest` runner, so real kernel enforcement is
309-
verified on every push and PR, not only locally. (The full unit suite is deliberately not
310-
re-run on that leg — a few tests are darwin-env sensitive; the dogfood is the enforcement
311-
contract that matters.)
312-
313-
Honest caveats: net is launcher-permissive (do not read this as general exfil prevention); it
314-
does not defend against a same-user attacker who can rewrite **both** the IDE config
315-
and `~/.mcpm`. A strict tier (read-allowlist / scratch-only-write / host-granular net),
316-
Linux `bwrap`, and the per-server `guard confine <server>` command are deferred (per-server
317-
confine is achievable today via `enable --confine --server X` + `disable --server X`).
261+
The relay optionally applies a macOS Seatbelt profile to the spawned server via
262+
`guard/confine/`. Profile derivation, backend rendering and spawn decisions are
263+
separate modules; the enrollment store and wrapper marker bind the selected
264+
profile. Enrollment alone does not prove a confined launch.
265+
266+
Use the canonical [confinement reference](GUARD.md#os-confinement---confine) for
267+
profile permissions, enrollment, marker tokens and hybrid/fail-closed conditions.
268+
Configured credentials still reach the child, and network-enabled launchers can
269+
send accessible data. The [CI section](#cicd) describes the macOS enforcement job.
318270

319271
## Configuration
320272

‎docs/GUARD.md‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ A URL/HTTP-transport server entry has no `command`, so the stdio MITM relay cann
199199
Consent does **not** add protection — it records that you accepted its absence. The underlying limitation stands: no relay exists for non-stdio transports (a streamable-HTTP MITM is deferred).
200200

201201
### `mcpm guard disable [--client <id>] [--server <name>]`
202-
Reverses the wrap by parsing the wrap marker out of the args and reconstructing the original entry. Falls back to the `.bak` if the wrap pattern is malformed (e.g. user hand-edited the config since enable).
202+
Reconstructs the original entry from a recognized wrap marker. Ambiguous or malformed wrappers are skipped with repair guidance; disable does not automatically restore a `.bak`. Review a trusted client-config backup and recover the original entry before enabling again. See the [wrapper recognition limits](#mcpm-guard-status).
203203

204204
### `mcpm guard enable [...] --confine [off]`
205205
Opt-in OS sandbox for the servers being wrapped (macOS only, shipped in v0.16.0). See **OS confinement (`--confine`)** below. The bare flag enrolls every unwrapped stdio server it wraps at the "standard" tier; `--confine off` is the same as omitting the flag (no enrollment). Respects `--client` / `--server`.
@@ -215,7 +215,7 @@ Wrapper recognition checks the launcher and complete marker, including its origi
215215
Malformed markers, missing package paths and pre-hash legacy wrappers are ambiguous: status/doctor report them without counting them guarded, and enable/disable/update refuse to rewrite them. Restore the original server from a trusted client-config backup, then enable again. Recognition is static configuration evidence: it does not authenticate binary bytes or forged package metadata, prove an old Node still exists, or prove the client loaded the wrapper. A moved script with an arbitrary alias may need manual recovery because its former identity cannot be inferred.
216216

217217
### `mcpm guard demo`
218-
Runs the in-process `prompt-injection` scenario. The output is byte-identical to what the production relay emits, so screenshotting the demo accurately represents what a real block looks like. Additional scenarios (path-exfil, rug-pull) are planned but not yet shipped.
218+
Runs the in-process `prompt-injection` scenario through the pattern engine and prints a demonstration-specific summary. It does not start a server subprocess or run the full relay, pin or policy pipeline; the formatted demo is not a capture of production wire output. The CLI exposes only this scenario.
219219

220220
### `mcpm guard accept-drift <server> [--tool <name>] [--new-hash <sha256:...>] [--remove] [--yes]`
221221

@@ -279,13 +279,17 @@ control sequence.
279279
Shows the vendored OWASP MCP Top 10 signature catalog with id / category / severity / target / description.
280280

281281
### `mcpm guard reset-integrity [--policy] [--yes]`
282-
Regenerates the integrity sidecar after manually editing `~/.mcpm/pins.json` (default) or `~/.mcpm/guard-policy.yaml` (`--policy`).
282+
Regenerates the integrity sidecar for `~/.mcpm/pins.json` (default) or `~/.mcpm/guard-policy.yaml` (`--policy`). Review the file first: `--yes` is required to apply the reset; without it the command prints a warning and exits 1.
283283

284284
Required when:
285285
- You copy `pins.json` between machines
286286
- You hand-edit `guard-policy.yaml` directly (rather than via `mcpm guard mute/unmute/pause`)
287287

288-
The relay refuses to use either file if its sidecar doesn't match — this is the rug-pull defense for the configuration itself.
288+
A pins mismatch refuses relay startup. A policy mismatch warns and uses shipped
289+
default actions for the session, discarding stricter block promotions as well as
290+
mutes and pause state; see [policy fallback](POLICY.md#integrity-sidecar). These
291+
unkeyed hashes detect stale/corrupt content, not authenticity against a same-user
292+
writer who can replace both files. Reset only after reviewing the change.
289293

290294
### `mcpm guard run --inner --server-name <name> -- <command> [args]`
291295
**Internal command**, semver-exempt, not for direct user use. Invoked by wrapped client configs. Requires the `--inner` flag and refuses direct invocation without it.
@@ -310,7 +314,7 @@ On macOS, confinement is applied via Seatbelt (`sandbox-exec`). The one shipped
310314
### Enabling and disabling
311315

312316
```
313-
mcpm guard enable --confine # enroll every wrapped stdio server, standard tier
317+
mcpm guard enable --confine # enroll unwrapped stdio servers, standard tier
314318
mcpm guard enable --confine --server foo # scope to one server
315319
mcpm guard enable --confine off # same as omitting: no enrollment
316320
```
@@ -326,12 +330,12 @@ mcpm guard enable --confine off # same as omitting: no enrollment
326330
Confinement never fails silently. The store (`~/.mcpm/guard-confine.yaml`) is the source of truth for "is server X enrolled".
327331

328332
- **Confine** when the server is enrolled + the profile hash matches + an OS backend is available.
329-
- **Run unconfined with a loud notice** when there is no OS backend (Linux / CI / Windows) or the marker/profile is missing on a **non-required** server. Never silent — backend availability is pre-checked, never inferred from a spawn error.
330-
- **Fail closed (refuse to start, exit 1)** at spawn time on: a profile-hash mismatch (tamper), a malformed hash, a stripped marker on a `require_confine` server, or a wiped store on a `require_confine` server.
333+
- **Run unconfined with a loud notice** on a **non-required** server when there is no OS backend (Linux / CI / Windows) or the marker/profile is missing. Never silent — backend availability is pre-checked, never inferred from a spawn error.
334+
- **Fail closed (refuse to start, exit 1)** at spawn time on: a profile-hash mismatch (tamper), a malformed hash, a stripped marker or missing profile/store on a required server, or an unavailable OS backend when either the marker or stored profile requires confinement.
331335

332336
### Marker tokens
333337

334-
The wrapped config entry carries two new tokens **before** the `--` separator: `--confine-profile-hash <sha256>` (a content hash of the rendered profile, binding the marker to the stored profile) and `--confine-required` (a bare flag, present iff the server is `require_confine`, replicated into the IDE config so it survives a wiped store). Both are **neutral** to `--orig-hash` — excluded from its hash input, and the `--` position is unchanged — so unwrap/disable still work.
338+
The wrapped config entry carries two new tokens **before** the `--` separator: `--confine-profile-hash <sha256>` (a content hash of the stored profile, binding the marker to that profile) and `--confine-required` (a bare flag, present iff the server is `require_confine`, replicated into the IDE config so it survives a wiped store). Both are **neutral** to `--orig-hash` — excluded from its hash input, and the `--` position is unchanged — so unwrap/disable still work.
335339

336340
### Store
337341

0 commit comments

Comments
 (0)