pi-session-mcp 0.6.4 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.pi-session-mcp-cli/config.js +11 -1
  2. package/.pi-session-mcp-cli/doctor.js +22 -1
  3. package/.pi-session-mcp-cli/execution-profile.js +1 -0
  4. package/.pi-session-mcp-cli/extension-spec.js +26 -0
  5. package/.pi-session-mcp-cli/pi-extensions.js +67 -0
  6. package/CHANGELOG.md +62 -0
  7. package/CONTRIBUTING.md +1 -1
  8. package/README.md +2 -2
  9. package/SECURITY.md +1 -1
  10. package/dist/capabilities.d.ts +1 -0
  11. package/dist/capabilities.js +2 -1
  12. package/dist/client-adapters/mcp-verifier.js +5 -2
  13. package/dist/config.d.ts +1 -0
  14. package/dist/config.js +11 -1
  15. package/dist/doctor.d.ts +4 -1
  16. package/dist/doctor.js +22 -1
  17. package/dist/execution-profile.d.ts +3 -0
  18. package/dist/execution-profile.js +1 -0
  19. package/dist/extension-spec.d.ts +15 -0
  20. package/dist/extension-spec.js +26 -0
  21. package/dist/main.js +6 -2
  22. package/dist/pi-adapter.d.ts +1 -1
  23. package/dist/pi-adapter.js +4 -0
  24. package/dist/pi-extensions.d.ts +17 -0
  25. package/dist/pi-extensions.js +67 -0
  26. package/dist/sdk-pi-adapter.d.ts +24 -6
  27. package/dist/sdk-pi-adapter.js +256 -86
  28. package/dist/server.js +1 -0
  29. package/dist/session-tools.d.ts +9 -0
  30. package/dist/session-tools.js +41 -0
  31. package/dist/setup-command-guardian.js +29 -73
  32. package/dist/setup-main.js +2 -1
  33. package/dist/setup-ownership.js +18 -28
  34. package/dist/setup-platform.d.ts +57 -0
  35. package/dist/setup-platform.js +195 -0
  36. package/dist/setup-process.d.ts +6 -3
  37. package/dist/setup-process.js +85 -93
  38. package/dist/setup.js +2 -1
  39. package/dist/stdout-guard-install.d.ts +5 -0
  40. package/dist/stdout-guard-install.js +6 -0
  41. package/dist/stdout-guard.d.ts +20 -0
  42. package/dist/stdout-guard.js +72 -0
  43. package/docs/acceptance.md +343 -0
  44. package/docs/adr/0005-macos-setup-platform.md +84 -0
  45. package/docs/adr/0006-pi-extension-allowlist.md +58 -0
  46. package/docs/architecture.md +1 -1
  47. package/docs/client-setup-design.md +21 -9
  48. package/docs/client-setup.md +25 -10
  49. package/docs/doctor-design.md +7 -2
  50. package/docs/installation.md +125 -4
  51. package/docs/macos-setup-design.md +386 -0
  52. package/docs/pi-extensions-design.md +437 -0
  53. package/docs/releasing.md +16 -7
  54. package/docs/threat-model.md +54 -18
  55. package/docs/tool-contracts.md +8 -3
  56. package/package.json +2 -1
@@ -124,6 +124,104 @@ that checkout with Doctor reporting the release version, and confirms that the
124
124
  manifests and built runtime files are byte-identical before and after the
125
125
  client cycles.
126
126
 
127
+ ### Tooling
128
+
129
+ `scripts/setup-acceptance.mjs` runs the isolation, the sequence and the pass
130
+ criteria above with the real clients. It uses disposable homes below its work
131
+ directory (default `~/.cache/pi-session-mcp-acceptance`, never below `/tmp`,
132
+ `/var/tmp`, `/dev/shm` or `$TMPDIR`), removes them afterwards (also after Ctrl-C
133
+ or SIGTERM, with exit `130` or `143`) and writes a sanitized JSON result without
134
+ paths or environment values. It exits with `0` when every check passed, `1` when
135
+ a check failed and `2` on a usage, environment or harness error.
136
+
137
+ `--setup` is split on whitespace, so paths in it must not contain spaces. Its
138
+ first token must be an absolute path to an existing file or a command found on
139
+ `PATH`; otherwise the script exits with `2`.
140
+
141
+ The script only reads the real `~/.codex/config.toml` and `~/.claude.json` to
142
+ prove that the MCP registrations in them did not change. A file that does not
143
+ exist counts as the constant digest `absent`; one that exists but cannot be read
144
+ fails the check. A real client session that changes its own MCP registrations
145
+ during the run makes the digest check fail (a false fail), so close or idle real
146
+ clients first.
147
+
148
+ Working tree, after `npm run build` and `npm run build:cli`:
149
+
150
+ ```bash
151
+ node scripts/setup-acceptance.mjs \
152
+ --setup "node $PWD/dist/setup-main.js" --build-dir "$PWD" \
153
+ --label working-tree --out level2-result.json
154
+ ```
155
+
156
+ Published package: install it into a temporary prefix outside `/tmp`, then pass
157
+ the installed executable and package directory.
158
+
159
+ ```bash
160
+ npm install -g --prefix "$PREFIX" pi-session-mcp@<version>
161
+ node scripts/setup-acceptance.mjs \
162
+ --setup "$PREFIX/bin/pi-session-mcp-setup" \
163
+ --build-dir "$PREFIX/lib/node_modules/pi-session-mcp" \
164
+ --label npm-<version> --out level2-result.json
165
+ ```
166
+
167
+ Tag clone: build a fresh clone of the tag (`npm ci`, `npm run build`,
168
+ `npm run build:cli`) and use it like the working tree, with `--setup` and
169
+ `--build-dir` pointing at the clone.
170
+
171
+ `--clients codex` or `--clients claude-code` limits a run to one client, for
172
+ example on a macOS machine without Codex. The script does not cover the macOS
173
+ lock-contention and interruption steps, Level 1, or the tag identity checks
174
+ (tag object, peeled commit, `git describe`). Run those by hand.
175
+
176
+ ### macOS
177
+
178
+ macOS uses the same isolation, sequence and pass criteria, with the additions
179
+ below. A run for Claude Code is recorded under [Status](#status). macOS is
180
+ CI-tested, and it has not passed Level 2 for Codex.
181
+
182
+ - **Environment.** Record:
183
+ - the macOS version and architecture (`sw_vers`, `uname -m`);
184
+ - the Node.js version;
185
+ - the Codex CLI and Claude Code versions.
186
+ - **Disposable home.** It must be on the APFS volume type of `/`. A directory
187
+ below the user's home qualifies.
188
+ - For an ownership directory on another filesystem, `--apply`, `--verify`,
189
+ `--rollback` and `--remove` fail closed with `failed (operation_failed)`
190
+ and exit `1`.
191
+ - Keep the Codex home outside both `$TMPDIR` (normally below `/var/folders`)
192
+ and `/tmp`, for the reason given under [Isolation](#isolation).
193
+ - **Before the sequence:** run `npm run --silent doctor`.
194
+ - **Lock contention.** After step 9, start two `--apply` runs for
195
+ `claude-code:user:pi-session-mcp` at the same time.
196
+ - One run reports `ok (applied)`. The other waits for the lock, then
197
+ reports `unchanged (already_equivalent)`.
198
+ - Both exit with `0`.
199
+ - A following `--remove` reports `ok (removed)`.
200
+ - A waiting run that reports `failed (operation_failed)` with exit `1` did
201
+ not get the lock within the 30 s acquisition bound. That counts as a
202
+ failure of the run.
203
+ - **Interruption.** Start `--apply` for one target and press Ctrl-C while it
204
+ runs.
205
+ - The run exits with `130` and reports `operation_interrupted`.
206
+ - Afterwards `ps -ww -A -o pid=,command=` lists no `claude mcp` or `codex mcp`
207
+ command and no `setup-main.js` or `setup-command-guardian.js` process of
208
+ the build under test.
209
+ - A following `--apply` reports one of three outcomes, depending on where
210
+ the interrupt landed:
211
+ - it reconciles the pending record (`ok (pending_recovered)`);
212
+ - it applies again (`ok (applied)`);
213
+ - it finds the registration already owned
214
+ (`unchanged (already_equivalent)`).
215
+ - A following `--remove` reports `ok (removed)`.
216
+ - If the operation completes before the interrupt arrives, record that and
217
+ repeat. An interrupt that never lands is not a pass.
218
+ - **Processes.** Check that no process remains with `ps -ww` instead of `/proc`;
219
+ without `-ww`, `ps` truncates long commands when it has no terminal.
220
+
221
+ The evidence record names the build (commit or tag), the environment above,
222
+ every command with its exit status, and the sanitized JSON results. It
223
+ contains no paths, environment values or secrets.
224
+
127
225
  ## Level 3: live two-client acceptance
128
226
 
129
227
  Live acceptance delegates real work from Codex and from Claude Code to Pi
@@ -213,6 +311,18 @@ external MCP grants, and client restart or resume.
213
311
  A narrower live check can cover a single path. It must state its scope and does
214
312
  not count as a ten-task cycle.
215
313
 
314
+ ## Pi extension live check
315
+
316
+ Manual, on a machine with Pi, `pi-cursor-sdk@0.5.3` and a stored Cursor key. Run it from a working tree (`npm run build`, then `node dist/main.js`) with a temporary configuration, passed through `PI_SESSION_MCP_CONFIG`, whose profile names `"extensions": ["npm:pi-cursor-sdk@0.5.3"]` and a Cursor model from Pi's catalog.
317
+
318
+ 1. `pi_session_start`, a prompt answered with "PONG", `pi_turn_get` completed, `pi_session_close`.
319
+ 2. Two parallel sessions of that profile, each completing a prompt.
320
+ 3. After close, no Cursor child process of the server remains.
321
+ 4. stderr shows no `foreign_stdout_suppressed` storm and no stdout frame fails to parse.
322
+ 5. Record how the Cursor local runtime starts its child processes (inherited or piped stdio).
323
+
324
+ Record date, source commit, versions and outcome under Status. No secret, path or prompt content goes into the record.
325
+
216
326
  ## Evidence rules
217
327
 
218
328
  - Record only sanitized evidence: statuses, booleans, counts, durations,
@@ -269,3 +379,236 @@ Before its release, `v0.6.4` passed Level 1 and the working-tree Level 2 on its
269
379
  release candidate, with Codex CLI `0.162.0` and Claude Code `2.1.296`. Its
270
380
  server entry point `dist/main.js` is byte-identical to that of `4d333da` and of
271
381
  `v0.6.3`.
382
+
383
+ After its release, `v0.6.4` passed Level 1 on a fresh clone of its tag. It also
384
+ passed Level 2 as published release and as published npm package, with Codex
385
+ CLI `0.162.0` and Claude Code `2.1.296`, on 2026-10-10. The installed
386
+ `pi-session-mcp-doctor` reported a healthy package when run from an unrelated
387
+ working directory.
388
+
389
+ macOS: `setup` supports macOS from the change that tracks #32. That change is
390
+ tested in CI on macOS 15 and macOS 26 (Apple silicon) next to Linux.
391
+
392
+ On 2026-10-11, the build of `d89740c`, the head of the pull request branch,
393
+ passed the working-tree Level 2 on a real Mac with Claude Code as the only
394
+ client:
395
+ - Environment: macOS 26.7.1 (`25G241`) on `arm64`, Node.js `22.23.3` and Claude
396
+ Code `2.1.294`. The disposable home was on the APFS data volume that the
397
+ root volume belongs to.
398
+ - Level 1 on that checkout: `npm ci`, `npm run typecheck`, `npm run build`,
399
+ `npm run build:cli` and `npm test` exited with `0`. The test run reported 531
400
+ passed and 23 skipped. The skipped tests are the 21 Linux `/proc` fixture
401
+ tests, the Linux-only lock-inode replacement test and the probe-only
402
+ controller output test. `npm audit` reported 0 vulnerabilities, and
403
+ `git diff --check` reported nothing. `git status --short` printed nothing
404
+ before the evidence was written.
405
+ - Doctor with the acceptance configuration reported no error.
406
+ - All three Claude Code targets (`user`, `project` and `local`) completed the
407
+ nine-step sequence. Every step reported the expected overall status, finding
408
+ and exit status.
409
+ - After the sequences the `pi-session-mcp` registration was absent, the sentinel
410
+ was still present and the ownership record set was empty. The ownership
411
+ directory had mode `0700` and the ownership file mode `0600`. No server,
412
+ setup or guardian process of the build remained, and the digest of the real
413
+ Claude Code registrations was unchanged. No report contained a path, an
414
+ environment value or a secret.
415
+ - Lock contention: two simultaneous `--apply` runs for
416
+ `claude-code:user:pi-session-mcp` finished within 5 s. One reported
417
+ `ok (applied)` and the other `unchanged (already_equivalent)`. Both exited
418
+ with `0`, and the following `--remove` reported `ok (removed)`.
419
+ - Interruption: SIGINT was sent to the process group of a running `--apply`
420
+ after 0.2 s, 0.5 s and 0.9 s. Each run exited with `130` and reported
421
+ `operation_interrupted`. None left a `claude mcp` command or a setup or
422
+ guardian process behind. In each case the following `--apply` reported
423
+ `ok (applied)` and the following `--remove` reported `ok (removed)`.
424
+
425
+ This run does not cover:
426
+ - Codex, which was not installed on the machine, so macOS has not passed
427
+ setup acceptance for Codex.
428
+ - A Ctrl-C keypress in an interactive terminal. A script sent the signal from a
429
+ shell without a terminal.
430
+ - The `pending_recovered` and `already_equivalent` outcomes after an interrupt.
431
+ The following `--apply` reported `applied` in every case.
432
+ - Intel Macs, the published-release and published-package variants, and
433
+ Level 3.
434
+
435
+ On 2026-10-11 the same build also passed a narrower live check on that Mac:
436
+ - Scope: Claude Code `2.1.294` as the only client, with Pi SDK `1.1.0` on a
437
+ local `qwen3.6-35b-a3b-splash` model served by LM Studio. The server was the
438
+ build of `d89740c`: the sources are identical to it, and `dist/main.js` has
439
+ the SHA-256 `a69bfec2f9f161b74a6d7b067fda9b76d94c294170bc59a771a79e3f29a7123c`.
440
+ - Method: three sequential headless invocations, one per task group, with a
441
+ temporary per-invocation MCP configuration (`--strict-mcp-config`), a
442
+ credential-free configuration with one `read-only` and one `coding` profile,
443
+ and a disposable Pi agent directory. The client could run `sleep` between
444
+ polls. The plan, prompts, oracles and fixture digests were fixed before the
445
+ first invocation, and no task was repeated.
446
+ - Preflight: one bounded request to the model returned HTTP `200` in about 1 s.
447
+ The loaded model was the same and idle before and after every invocation, and
448
+ nothing was loaded or unloaded.
449
+ - Read-only analysis with a same-session follow-up: both turns ended
450
+ `completed`. The first answer contained the expected marker, and the second
451
+ answer was exactly the expected number. The fixture digest (path, mode and
452
+ content) was identical before and after.
453
+ - Bounded coding change with a same-session follow-up: both turns ended
454
+ `completed`. The oracle passed on a snapshot taken between the turns and on
455
+ the final workspace, and that snapshot did not yet contain the second turn's
456
+ change. Only the one allowed file differed, with no added or removed file and
457
+ unchanged modes.
458
+ - Abort and recovery: the turn was `running`, then `aborted` after
459
+ `pi_session_abort`. `pi_session_close` succeeded, `pi_session_list` returned
460
+ no session, and a fresh session started `idle` and closed.
461
+ - Counts: `pi_turn_get` was called 3, 3 and 2 times in the three invocations,
462
+ and no Pi Session MCP tool returned an error.
463
+ - A scan of every Pi Session MCP tool result in the three transcripts found no
464
+ absolute path, configuration file name, endpoint, key or token marker, or stack
465
+ frame. No server or client process remained after any invocation, each
466
+ client's standard error was empty, and the digest of the real Claude Code
467
+ registrations was unchanged.
468
+
469
+ This check does not cover:
470
+ - Codex, or any task from a second client. It is not a ten-task cycle and does
471
+ not satisfy the Level 3 gate.
472
+ - The check that the first turn's terminal view stays unchanged after a second
473
+ turn started. The transcripts do not poll the first turn again.
474
+ - A git-based check of the allowed diff. The coding fixture is not a Git
475
+ repository, so a path, mode and content digest stood in for it.
476
+ - A coding turn that tries to change a file outside the allowed set.
477
+ - Interactive approval dialogs, external MCP grants, and client restart or
478
+ resume.
479
+
480
+ On 2026-10-11 the working-tree build of `eeaa597` on the `feat/pi-extensions`
481
+ branch (#34) passed the Pi extension live check on Linux:
482
+ - Environment: Node.js `26.11.1`, Pi SDK `1.1.0`, and `pi-cursor-sdk@0.5.3`
483
+ installed by Pi with a stored Cursor key. `dist/main.js` has the SHA-256
484
+ `47875d38886528ace6b242df2c6ad79d624c51e330e63c2f401576eadc449ed7`.
485
+ - Method: a scripted JSON-RPC client drove `node dist/main.js` over stdio, with
486
+ a temporary configuration passed through `PI_SESSION_MCP_CONFIG`. The
487
+ configuration had a `coding` and a `read-only` profile, each with provider
488
+ `cursor`, model `auto`, thinking level `off` and
489
+ `"extensions": ["npm:pi-cursor-sdk@0.5.3"]`.
490
+ - The `coding` profile started, its prompt completed with "PONG", and the
491
+ session closed.
492
+ - Two sessions, one of each profile, started in parallel and both prompts
493
+ completed with "PONG".
494
+ - After close, no descendant process of the server remained.
495
+ - No stdout line failed to parse as an MCP frame. stderr carried two
496
+ `foreign_stdout_suppressed` reports for 21 suppressed writes and no other
497
+ event.
498
+ - During turns the Cursor local runtime started `git`, `ssh`, `gh`, `node` and
499
+ the MCP servers configured for Cursor through `npm exec`, in the `read-only`
500
+ profile as well. None of them inherited the server's stdout: each had its own
501
+ pipe, a socket, `/dev/null` or a file as standard output.
502
+ - The first run, on the earlier build of `6740484`, failed to start the
503
+ `coding` profile with `execution_selection_mismatch`: `pi-cursor-sdk`
504
+ deactivates its own `cursor_activate_skill` tool when the session starts.
505
+ `eeaa597` lets an extension narrow its active tools while still rejecting
506
+ any tool outside the allowlist.
507
+
508
+ This check does not cover macOS, Codex or Claude Code as the client, the Cursor
509
+ cloud runtime, or a session that uses Cursor's own tools.
510
+
511
+ Before its release, `v0.7.0` passed Level 1 and the working-tree Level 2 on its
512
+ release candidate on Linux, with Node.js `26.11.1`, Codex CLI `0.162.1` and
513
+ Claude Code `2.1.296`, on 2026-10-11:
514
+ - The first working-tree Level 2, on `2d05e55`, failed. On all four targets
515
+ `--verify` (steps 5 and 8) reported `failed (mcp_failed)` with exit `1`; every
516
+ other step passed. The verifier rejected the `extensions` field that
517
+ `pi_capabilities_get` lists since #34. This is #36.
518
+ - After the fix, the working-tree Level 2 on `2c5ab3d` passed all nine steps on
519
+ all four targets, 96 of 96 checks.
520
+ - The real client registrations were unchanged, no process remained, and the
521
+ built files were byte-identical before and after the client cycles.
522
+ - The run placed one sentinel in each Claude Code scope instead of one per
523
+ client, so its check count differs from earlier records.
524
+ - Level 1 on `09b32a8`: every command exited with `0`.
525
+ - The test run reported 37 files, 660 passed and 5 skipped.
526
+ - `npm audit` and `npm audit --omit=dev` reported 0 vulnerabilities.
527
+ - Doctor reported `packageVersion` `0.7.0` and no finding.
528
+ - The commits after `2c5ab3d` change only tests and documentation. The built
529
+ `dist/` and `.pi-session-mcp-cli/` are byte-identical to those of `2c5ab3d`,
530
+ and `dist/main.js` has the SHA-256
531
+ `47875d38886528ace6b242df2c6ad79d624c51e330e63c2f401576eadc449ed7`.
532
+ - The Codex CLI `0.162.1` contract was recorded. It differs from `0.162.0` only
533
+ in the version line.
534
+
535
+ The Pi extension live check above ran on `eeaa597`, before the final fixes to
536
+ extension tool names and the `read-only` contract test. It has not been
537
+ repeated on the release candidate.
538
+
539
+ After its release, `v0.7.0` passed Level 1 and Level 2 on a fresh clone of its
540
+ tag, and Level 2 as published npm package, on 2026-10-11, on Linux with Node.js
541
+ `26.11.1`, Codex CLI `0.162.1` and Claude Code `2.1.296`:
542
+ - Tag clone: `git describe --tags --exact-match` reported `v0.7.0`. The tag
543
+ object is `a5516ce6e4e159ef1e20ee38aba06672379e9cb7` and the peeled commit
544
+ `6783631d2ae1813707cf798360a0a0f4c58ca66d`.
545
+ - Level 1 on the tag clone:
546
+ - every command exited with `0`;
547
+ - the test run reported 37 files, 660 passed and 5 skipped;
548
+ - `npm audit` and `npm audit --omit=dev` reported 0 vulnerabilities;
549
+ - Doctor reported `packageVersion` `0.7.0` and no finding;
550
+ - `dist/main.js` has the SHA-256
551
+ `47875d38886528ace6b242df2c6ad79d624c51e330e63c2f401576eadc449ed7`.
552
+ - Level 2 passed 96 of 96 checks on the tag clone, and 96 of 96 with the npm
553
+ package installed into a temporary prefix through
554
+ `pi-session-mcp-setup`. In both runs the real client registrations were
555
+ unchanged, no process remained, and the built files were byte-identical
556
+ before and after the client cycles.
557
+ - These runs, and the release candidate's runs above, used the harness that
558
+ #40 later checked in as `scripts/setup-acceptance.mjs`.
559
+ - The checked-in script, after its review fixes, also passed 96 of 96 on the
560
+ tag clone.
561
+ - The npm package has the shasum `c8d73311dbde1bdebd78e58b1f01c7cf3bebf374`.
562
+ The tarball downloaded from the registry had the same SHA-1 and SHA-512
563
+ integrity.
564
+ - Installed package:
565
+ - `pi-session-mcp-doctor` reported a healthy package `0.7.0` when run from an
566
+ unrelated working directory;
567
+ - with a configuration that pins `pi-cursor-sdk@0.5.3` on three profiles,
568
+ `setup --apply` and `--verify` for `claude-code:user:pi-session-mcp`
569
+ reported `applied` and `mcp_verified`.
570
+
571
+ On 2026-10-11 the installed `v0.7.0` package also passed the Pi extension live
572
+ check on Linux:
573
+ - Method: the same scripted JSON-RPC client as above drove the installed
574
+ server. It used the operator's configuration with three `pi-cursor-sdk@0.5.3`
575
+ profiles: one `read-only` and one `coding` profile with
576
+ `claude-opus-5-5@1m` and `claude-opus-5-5@300k` at thinking level `high`, and
577
+ one `coding` profile with `composer-2.5` at thinking level `off`.
578
+ - Each of the three profiles started, completed a prompt with the expected
579
+ one-word answer, and closed. One `coding` and one `read-only` Cursor session started in parallel
580
+ and both completed.
581
+ - Profiles without extensions on three other providers (xAI, Mistral and
582
+ DeepSeek) each completed a prompt.
583
+ - After close no descendant process of the server remained. No stdout line
584
+ failed to parse as an MCP frame; stderr carried four
585
+ `foreign_stdout_suppressed` reports for 12 suppressed writes and no other
586
+ event.
587
+ - The two OpenAI profiles ended `failed` with `turn_failed`. A direct Pi CLI
588
+ call outside the server showed that the account had reached its usage limit,
589
+ so they are not evidence for or against the release.
590
+
591
+ This check does not cover macOS, an MCP client in the loop, or a session that
592
+ uses Cursor's own tools.
593
+
594
+ Before its release, `v0.7.1` passed Level 1 and the working-tree Level 2 on its
595
+ release candidate `82dff20` on Linux, with Node.js `26.11.1`, Codex CLI
596
+ `0.162.1` and Claude Code `2.1.296`, on 2026-10-11:
597
+ - Level 1: every command exited with `0`.
598
+ - The test run reported 38 files, 684 passed and 5 skipped.
599
+ - `npm audit` and `npm audit --omit=dev` reported 0 vulnerabilities.
600
+ - Doctor reported `packageVersion` `0.7.1` and no finding.
601
+ - Level 2 ran through `scripts/setup-acceptance.mjs` on a fresh clone of the
602
+ candidate.
603
+ - All nine steps passed on all four targets, 96 of 96 checks.
604
+ - The real client registrations were unchanged, and no process remained.
605
+ - The built files were byte-identical before and after the client cycles.
606
+ - `dist/main.js` is byte-identical to that of `v0.7.0`. The changed modules are
607
+ the extension tool-name rule, the pin format and the prompt source.
608
+
609
+ The branch build of `8d5b069`, whose sources match this candidate, passed the
610
+ Pi extension live check with the operator's configuration on 2026-10-11:
611
+ - Three `pi-cursor-sdk@0.5.3` profiles and one xAI profile each completed a
612
+ prompt.
613
+ - Two Cursor sessions completed in parallel.
614
+ - No process remained, and no foreign line reached the MCP stdout.
@@ -0,0 +1,84 @@
1
+ # ADR 0005: setup supports macOS with the same fence as on Linux
2
+
3
+ **Status:** Accepted (2026-10-10). macOS counts as accepted only after
4
+ Level-2 setup acceptance on real hardware; see [Acceptance](../acceptance.md).
5
+ That acceptance is recorded for Claude Code (2026-10-11); Codex is still pending.
6
+
7
+ ## Context
8
+
9
+ `setup` refused every operation on macOS with `platform_unsupported`. #28
10
+ reported this from a Mac: Doctor and a manual registration worked, but
11
+ `--verify` and the ownership lifecycle were unavailable. ADR 0003 names
12
+ repeated setup friction beyond version drift as a trigger to revisit this
13
+ area.
14
+
15
+ The setup fence relied on three Linux facts:
16
+
17
+ - util-linux `flock` locks the open file description of the ownership lock
18
+ file, and the transaction guardian inherits it;
19
+ - a `/proc` scan proves that no non-zombie member of a client command's
20
+ process group remains before the fence is released;
21
+ - `statfs` magic numbers identify the local filesystems on which lock and
22
+ fsync semantics are accepted.
23
+
24
+ None of them carries over: macOS ships no `flock(1)` and no `/proc`, and its
25
+ `statfs` `f_type` is a dynamically assigned VFS index rather than a stable
26
+ magic number.
27
+
28
+ ## Decision
29
+
30
+ `setup` supports Linux and macOS. Every other platform still fails closed with
31
+ `platform_unsupported` before any client command is spawned.
32
+
33
+ `src/setup-platform.ts` owns the operating-system facts behind the fence. Its
34
+ macOS primitives are:
35
+
36
+ - **Lock.** The lock file is opened with `O_EXLOCK | O_NONBLOCK`. XNU takes a
37
+ flock(2)-semantics lock on the open file description at open time. A busy
38
+ lock is retried every 50 ms within the existing acquisition bound, and the
39
+ guardian inherits the locked description as on Linux.
40
+ - **Process-group proof.** The trusted system `/bin/ps -A -o pid=,pgid=,stat=`
41
+ is parsed strictly. A scan that fails, times out, has an unexpected line or
42
+ does not list the caller is `unknown`, and `unknown` retains the fence.
43
+ - **Filesystem.** The ownership directory must have the same `statfs` type
44
+ as `/`, which is the APFS system volume on every macOS that Node.js 22
45
+ supports. Any other type fails closed.
46
+
47
+ Public result codes, statuses, exit codes and the result schema do not
48
+ change. CI runs on Linux, macOS 15 and macOS 26. A `verify` job aggregates
49
+ the matrix, so the required status check keeps its name.
50
+
51
+ ## Consequences
52
+
53
+ - Operators on macOS get the same lifecycle, ownership records and
54
+ verification as on Linux, including `--verify` for a registration that
55
+ `setup` made.
56
+ - On macOS, the proof that no live group member remains rests on `/bin/ps`,
57
+ which sits on the sealed system volume, plus the uid and mode check that
58
+ already guarded `flock`. As on Linux, it does not cover descendants that
59
+ escape the process group, and it does not promise PID reaping.
60
+ - An ownership directory on HFS+, exFAT, a network volume or any other
61
+ non-APFS filesystem fails closed on macOS.
62
+ - Intel Macs run the same code path but are not in the CI matrix.
63
+ - The guardian and the process runner load the platform module with a
64
+ dynamic import, because source-mode tests run them through Node type
65
+ stripping. `setup-platform.js` is therefore a mandatory release artifact
66
+ next to `main.js` and `setup-command-guardian.js`.
67
+
68
+ ## Alternatives considered
69
+
70
+ - **A `lockf(1)` helper and `/sbin/mount` parsing.** This would have mirrored
71
+ the Linux shape. Rejected: the descriptor mode of `lockf` on macOS 15 and
72
+ the `mount` output format are unverified.
73
+ - **A weaker macOS profile without the kernel fence or the process-group
74
+ proof.** Rejected: it would break the guarantee that a later setup
75
+ transaction cannot race an orphaned client command.
76
+ - **`kill(-pgid, 0)` as the process-group proof.** Rejected: the XNU source
77
+ handles groups that contain only zombies inconsistently.
78
+
79
+ ## Revisit triggers
80
+
81
+ - The `ps` output format or its `stat` letters change.
82
+ - The `O_EXLOCK` semantics of open(2) change.
83
+ - The minimum macOS version that Node.js supports changes.
84
+ - A request to add Intel Macs to CI, or to support another platform.
@@ -0,0 +1,58 @@
1
+ # ADR 0006: Execution profiles may load pinned Pi extensions
2
+
3
+ **Status:** Accepted (2026-10-11). Issue #34. Design:
4
+ [Pi extensions design](../pi-extensions-design.md).
5
+
6
+ ## Context
7
+
8
+ Embedded sessions loaded no Pi extensions. Providers that exist only as Pi
9
+ extensions, such as `cursor` from `pi-cursor-sdk`, were unusable through Pi
10
+ Session MCP although Pi had them installed and authenticated.
11
+
12
+ ADR 0003 chose maintenance without a new feature phase. Unlike ADR 0005,
13
+ which followed a fired ADR 0003 revisit trigger, this is a scope expansion
14
+ the owner explicitly approved (2026-10-11, issue #34).
15
+
16
+ ## Decision
17
+
18
+ An execution profile may name Pi extensions as
19
+ `npm:<name>@<exact version>`.
20
+ - The server resolves each one from Pi's own user install, normally
21
+ `~/.pi/agent/npm/node_modules/<name>` from `pi install npm:<name>`, and never
22
+ installs anything. A package installed anywhere else, for example with
23
+ `npm i -g`, counts as not installed.
24
+ - A missing package or another installed version fails closed before a session
25
+ exists.
26
+ - An extension loads with its full capabilities: providers, tools, hooks and
27
+ system-prompt changes.
28
+ - In `read-only` profiles the model's tools stay `read`, `grep`, `find` and
29
+ `ls`.
30
+ - In `coding` profiles every extension tool may be active.
31
+ - An extension tool name that collides with another tool fails closed in both
32
+ profiles.
33
+
34
+ A profile with extensions uses the Pi SDK session-lifecycle API with its own
35
+ model runtime per session:
36
+ - `createAgentSessionServices`, then the existing verification, then
37
+ `createAgentSessionFromServices` and `bindExtensions`.
38
+ - Close goes through `AgentSessionRuntime.dispose()`, which emits
39
+ `session_shutdown`.
40
+
41
+ Profiles without extensions keep the previous path. Client prompts are plain
42
+ text for every profile: they never dispatch extension commands, skills or
43
+ templates, and extension input hooks see them with `source: "rpc"`. An
44
+ always-on stdout guard gives the MCP transport the only real stdout writer.
45
+
46
+ ## Consequences
47
+
48
+ - An allowlisted extension is trusted operator code with the user's full
49
+ rights inside the server process. Version pinning prevents silent updates,
50
+ not tampering with the installed package.
51
+ - Extensions may persist their own state; Pi Session MCP itself stays
52
+ in-memory.
53
+ - A child process that an extension starts with inherited stdio can still
54
+ write to the MCP stream directly. This is a documented residual risk.
55
+ - Extension tool names are checked for conflicts after loading and again after
56
+ binding. A tool that an extension registers later, during a turn, is not
57
+ re-checked, because allowlisted extensions are trusted code.
58
+ - Doctor checks configured extensions offline by reading their manifests.
@@ -79,4 +79,4 @@ Public errors are stable and sanitized. They omit caller values, complete prompt
79
79
 
80
80
  ## Scope
81
81
 
82
- The product boundary is local stdio and in-memory-only operation for one trusted local MCP peer. A controller keeps one immutable configuration/profile snapshot for its lifetime; file edits require a genuinely new controller process and do not hot-reload, mark stale, or mutate running sessions. Each new session performs external catalog discovery within the loaded grants, so catalog changes do not require a controller restart, but a running session's active tools remain fixed. Restarting the controller loses all sessions; reconnecting a client reloads configuration only when it actually starts a new controller process. Capability discovery reports captured metadata only and does not prove current-file freshness, server reachability, provider availability, or catalog state. It excludes HTTP, a web UI, persistence/resume, remote access, multi-user behavior, transcript export, streaming, live provider/model discovery, availability probes during capability discovery, and provider credential management. A coding execution profile is an application-level tool selection, not an OS sandbox.
82
+ The product boundary is local stdio and in-memory-only operation for one trusted local MCP peer. Allowlisted Pi extensions may persist state of their own outside this boundary, and their input hooks see every client prompt as `rpc` input, never `interactive`; Pi Session MCP itself keeps no state beyond process memory. A controller keeps one immutable configuration/profile snapshot for its lifetime; file edits require a genuinely new controller process and do not hot-reload, mark stale, or mutate running sessions. Each new session performs external catalog discovery within the loaded grants, so catalog changes do not require a controller restart, but a running session's active tools remain fixed. Restarting the controller loses all sessions; reconnecting a client reloads configuration only when it actually starts a new controller process. Capability discovery reports captured metadata only and does not prove current-file freshness, server reachability, provider availability, or catalog state. It excludes HTTP, a web UI, persistence/resume, remote access, multi-user behavior, transcript export, streaming, live provider/model discovery, availability probes during capability discovery, and provider credential management. A coding execution profile is an application-level tool selection, not an OS sandbox.
@@ -22,22 +22,34 @@ Codex supports only `codex:user:pi-session-mcp`. Claude Code supports `user`,
22
22
  registration output, or scope outside the recorded contracts is `unsupported`,
23
23
  and the client version is reported for information only. The adapters do not
24
24
  parse undocumented client config files.
25
- Setup is currently supported on Linux with a root-owned, non-writable
26
- util-linux `flock` at `/usr/bin/flock` or `/bin/flock`, readable `/proc`, and an
27
- ownership directory on ext2/3/4, XFS, Btrfs, tmpfs, overlayfs, ZFS, F2FS,
28
- UBIFS, or bcachefs. Other filesystems fail closed before lock acquisition.
29
- Every non-Linux platform fails closed with `platform_unsupported` before any
30
- client command is spawned.
25
+ Setup is supported on Linux and macOS
26
+ ([ADR 0005](adr/0005-macos-setup-platform.md)); `src/setup-platform.ts` owns
27
+ the operating-system facts behind the fence.
28
+
29
+ - **Linux:**
30
+ - a root-owned, non-writable util-linux `flock` at `/usr/bin/flock` or
31
+ `/bin/flock`;
32
+ - readable `/proc`;
33
+ - an ownership directory on ext2/3/4, XFS, Btrfs, tmpfs, overlayfs, ZFS,
34
+ F2FS, UBIFS, or bcachefs.
35
+ - **macOS:**
36
+ - the root-owned, non-writable system `/bin/ps`;
37
+ - an ownership directory with the same `statfs` type as `/`, which is the
38
+ APFS system volume on every macOS that Node.js 22 supports. macOS assigns
39
+ `f_type` dynamically, so no fixed number is compared.
40
+
41
+ Other filesystems fail closed before lock acquisition. Every other platform
42
+ fails closed with `platform_unsupported` before any client command is spawned.
31
43
 
32
44
  ## Ownership and transactions
33
45
 
34
46
  Each adapter exposes inspection, apply, verify, and remove operations. Apply first inspects and refuses divergence; equivalent registrations are left unchanged. Before adding an absent registration, setup records a path-free `pending` transaction containing only the target, an opaque fingerprint, the previous `absent` state, and a transaction identifier. Post-apply inspection promotes it to `owned`. Rollback/remove require owned metadata and the same effective fingerprint. Any mismatch fails closed and leaves the registration untouched.
35
47
 
36
- The production bridge derives Node from `process.execPath` and the entry point from the package root; configuration comes only from `PI_SESSION_MCP_CONFIG`. These values are internal command arguments and never CLI arguments, reports, or metadata. The opaque ownership fingerprint also binds `package.json`, the lockfile when the installation has one, and every built runtime JavaScript module. Both `main.js` and the regular, non-symlink `setup-command-guardian.js` are mandatory release artifacts; any runtime symlink fails closed. A package/lock mismatch or later build-content drift therefore fails closed instead of silently reusing ownership. Public client commands run through a bounded `CommandRunner` (timeout, process-group termination, and output cap); undocumented config files are not parsed.
48
+ The production bridge derives Node from `process.execPath` and the entry point from the package root; configuration comes only from `PI_SESSION_MCP_CONFIG`. These values are internal command arguments and never CLI arguments, reports, or metadata. The opaque ownership fingerprint also binds `package.json`, the lockfile when the installation has one, and every built runtime JavaScript module. `main.js` and the regular, non-symlink `setup-command-guardian.js` and `setup-platform.js` are mandatory release artifacts; any runtime symlink fails closed. A package/lock mismatch or later build-content drift therefore fails closed instead of silently reusing ownership. Public client commands run through a bounded `CommandRunner` (timeout, process-group termination, and output cap); undocumented config files are not parsed.
37
49
 
38
- The production CLI uses an atomic durable store with restrictive filesystem modes; the in-memory store exists only for library tests. Neither form persists paths, environment values, secrets, prompts, SDK errors, or auth metadata. On Linux, a stable, never-renamed lock file is held through a kernel `flock` for the complete inspect → pending → client mutation → owned transaction. Newly created ownership directories and every atomic state rename are fsynced through their parent directory before success. There is no clock-based stale recovery: the kernel releases the lock only when every holder of the locked open-file description exits. Missing or untrusted `flock`, an unsupported filesystem, a symlink/non-regular lock file, or an inode mismatch fails closed. The trusted same-UID operator must not unlink or replace the stable lock path while setup is running; setup checks its inode before acquisition, after acquisition, and before exposing transaction access, but same-UID filesystem tampering is outside the fence's trust boundary. A later run reconciles `pending` state: a matching equivalent registration is promoted to owned, an absent registration is safely retried, and any divergence fails closed.
50
+ The production CLI uses an atomic durable store with restrictive filesystem modes; the in-memory store exists only for library tests. Neither form persists paths, environment values, secrets, prompts, SDK errors, or auth metadata. A stable, never-renamed lock file is held through a kernel lock for the complete inspect → pending → client mutation → owned transaction. On Linux, the trusted util-linux `flock` locks the open file description that setup opened. On macOS, setup opens the lock file with `O_EXLOCK | O_NONBLOCK`, which takes a lock with flock(2) semantics on the open file description at open time; a busy lock is retried every 50 ms within the same acquisition bound. Newly created ownership directories and every atomic state rename are fsynced through their parent directory before success. There is no clock-based stale recovery: the kernel releases the lock only when every holder of the locked open-file description exits. A missing or untrusted `flock` or `ps`, an unsupported filesystem, a symlink/non-regular lock file, or an inode mismatch fails closed. The trusted same-UID operator must not unlink or replace the stable lock path while setup is running; setup compares the inode of the open lock file with the path after opening it, again once the fence is held, and before exposing transaction access, but same-UID filesystem tampering is outside the fence's trust boundary. A later run reconciles `pending` state: a matching equivalent registration is promoted to owned, an absent registration is safely retried, and any divergence fails closed.
39
51
 
40
- Mutating public client commands run under a transaction guardian that inherits the locked file description, starts the client in its own process group, and watches both the parent PID and IPC channel. Parent EOF, `SIGINT`, `SIGTERM`, or command timeout causes TERM then KILL. The guardian releases the fence only after `/proc` confirms that no non-zombie group member remains. Indeterminate cleanup returns a stable failure to the caller while the detached guardian retains the fence and continues cleanup, preventing a later setup transaction from racing the orphan. A mutation already accepted by an external client CLI or configuration backend cannot be rolled back automatically; ownership remains `pending` for reconciliation.
52
+ Mutating public client commands run under a transaction guardian that inherits the locked file description, starts the client in its own process group, and watches both the parent PID and IPC channel. Parent EOF, `SIGINT`, `SIGTERM`, or command timeout causes TERM then KILL. The guardian releases the fence only after a determinate process-table scan confirms that no non-zombie group member remains: `/proc` on Linux, and on macOS a strictly parsed `/bin/ps -A -o pid=,pgid=,stat=` that must list the scanning process itself. Indeterminate cleanup returns a stable failure to the caller while the detached guardian retains the fence and continues cleanup, preventing a later setup transaction from racing the orphan. A mutation already accepted by an external client CLI or configuration backend cannot be rolled back automatically; ownership remains `pending` for reconciliation.
41
53
 
42
54
  Here “gone” means no group member can still execute: a zombie has terminated and
43
55
  released its file descriptors, even when its PID awaits an external reaper. The
@@ -7,8 +7,14 @@ installation with upgrade and removal, start with [Installation](installation.md
7
7
  ## Prerequisites
8
8
 
9
9
  - Node.js **22.19.0 or newer**
10
- - Linux with a root-owned util-linux `flock` at `/usr/bin/flock` or `/bin/flock`,
11
- readable `/proc`, and a supported local ownership filesystem
10
+ - Linux or macOS. For `setup` also:
11
+ - on Linux, a root-owned util-linux `flock` at `/usr/bin/flock` or
12
+ `/bin/flock`, readable `/proc`, and a supported local ownership filesystem;
13
+ - on macOS, the system `/bin/ps` and an ownership directory on the APFS
14
+ volume type of `/`.
15
+
16
+ A manual registration does not need these; see
17
+ [Register without setup](installation.md#register-without-setup).
12
18
  - A local checkout of this repository
13
19
  - Existing local Pi authentication only when sending a real prompt
14
20
 
@@ -64,11 +70,20 @@ divergent existing registration is reported and never automatically replaced.
64
70
  Ownership is durable and contains only path-free, secret-free pending/owned
65
71
  state. A kernel-fenced guardian retains transaction ownership until every
66
72
  mutating client process-group member is gone, including after parent signals or
67
- hard parent exit. Non-Linux setup fails closed before client spawn.
68
- Supported ownership filesystems are ext2/3/4, XFS, Btrfs, tmpfs, overlayfs,
69
- ZFS, F2FS, UBIFS, and bcachefs. Network, FUSE, unreadable `/proc`, an untrusted
70
- `flock` binary, or same-UID replacement of the stable lock path are outside the
71
- accepted setup boundary and fail closed where they can be detected.
73
+ hard parent exit. Setup on any platform other than Linux and macOS fails
74
+ closed before client spawn.
75
+
76
+ Supported ownership filesystems:
77
+ - **Linux:** ext2/3/4, XFS, Btrfs, tmpfs, overlayfs, ZFS, F2FS, UBIFS, and
78
+ bcachefs.
79
+ - **macOS:** the APFS volume type of `/`.
80
+
81
+ The following are outside the accepted setup boundary and fail closed where
82
+ they can be detected:
83
+ - network or FUSE filesystems;
84
+ - unreadable `/proc`;
85
+ - an untrusted `flock` or `ps` binary;
86
+ - same-UID replacement of the stable lock path.
72
87
 
73
88
  Verify runs Doctor, checks exact registration equality, and then performs a
74
89
  bounded direct MCP `initialize`, `tools/list`, and
@@ -103,7 +118,7 @@ Create an unversioned JSON configuration file such as `pi-session-mcp.json`:
103
118
  }
104
119
  ```
105
120
 
106
- Configuration is strict. A profile has exactly `default`, `permissionProfile`, `provider`, `model`, and `thinkingLevel`; aliases follow the configured alias naming rules. At least one profile and exactly one default are required, and that default must be `read-only`. `thinkingLevel` is one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Workspace paths resolve relative to the configuration file. Clients can select only configured aliases, never an arbitrary working directory.
121
+ Configuration is strict. A profile has exactly `default`, `permissionProfile`, `provider`, `model`, and `thinkingLevel`, plus the optional `mcpServers` and `extensions` described in [Installation](installation.md); aliases follow the configured alias naming rules. At least one profile and exactly one default are required, and that default must be `read-only`. `thinkingLevel` is one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Workspace paths resolve relative to the configuration file. Clients can select only configured aliases, never an arbitrary working directory.
107
122
 
108
123
  Use placeholders until a locally available provider/model pair is known. Do not put keys, tokens, credential objects, headers, base URLs, or authentication paths in this file.
109
124
 
@@ -186,8 +201,8 @@ Server deadlines are profile-specific authentication/catalog work 5,000 ms per o
186
201
 
187
202
  There are eight tools: `pi_capabilities_get`, `pi_session_start`, `pi_session_list`, `pi_session_get`, `pi_session_prompt`, `pi_session_abort`, `pi_session_close`, and `pi_turn_get`. Capability discovery accepts only `{}`. `pi_session_start` rejects the old `profile` input, raw `provider`, `model`, `thinkingLevel`, credential-like fields, and all unknown fields. Its safe session metadata includes the configured execution-profile alias and post-verified provider/model/effective thinking level; the existing `profile` output remains derived permission metadata.
188
203
 
189
- The returned `thinkingLevel` records the effective Pi session setting. Pi
190
- Control compares it with the configured profile selection before admitting the
204
+ The returned `thinkingLevel` records the effective Pi session setting. The
205
+ server compares it with the configured profile selection before admitting the
191
206
  session; it does not verify provider-side reasoning or measure or cap its
192
207
  reasoning-token budget: a provider may use reasoning tokens even when the session
193
208
  selects `off`. Pi Session MCP does not project thinking content or provider-side