pi-session-mcp 0.6.3 → 0.7.0
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.
- package/.pi-session-mcp-cli/config.js +11 -1
- package/.pi-session-mcp-cli/doctor-cli.js +5 -1
- package/.pi-session-mcp-cli/doctor.js +22 -1
- package/.pi-session-mcp-cli/execution-profile.js +1 -0
- package/.pi-session-mcp-cli/extension-spec.js +21 -0
- package/.pi-session-mcp-cli/pi-extensions.js +67 -0
- package/CHANGELOG.md +54 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +2 -2
- package/SECURITY.md +1 -1
- package/dist/capabilities.d.ts +1 -0
- package/dist/capabilities.js +2 -1
- package/dist/client-adapters/mcp-verifier.js +5 -2
- package/dist/config.d.ts +1 -0
- package/dist/config.js +11 -1
- package/dist/doctor-cli.js +5 -1
- package/dist/doctor.d.ts +4 -1
- package/dist/doctor.js +22 -1
- package/dist/execution-profile.d.ts +3 -0
- package/dist/execution-profile.js +1 -0
- package/dist/extension-spec.d.ts +15 -0
- package/dist/extension-spec.js +21 -0
- package/dist/main.js +6 -2
- package/dist/pi-adapter.d.ts +1 -1
- package/dist/pi-adapter.js +4 -0
- package/dist/pi-extensions.d.ts +17 -0
- package/dist/pi-extensions.js +67 -0
- package/dist/sdk-pi-adapter.d.ts +24 -6
- package/dist/sdk-pi-adapter.js +253 -86
- package/dist/server.js +1 -0
- package/dist/session-tools.d.ts +9 -0
- package/dist/session-tools.js +25 -0
- package/dist/setup-command-guardian.js +29 -73
- package/dist/setup-main.js +2 -1
- package/dist/setup-ownership.js +18 -28
- package/dist/setup-platform.d.ts +57 -0
- package/dist/setup-platform.js +195 -0
- package/dist/setup-process.d.ts +6 -3
- package/dist/setup-process.js +85 -93
- package/dist/setup.js +2 -1
- package/dist/stdout-guard-install.d.ts +5 -0
- package/dist/stdout-guard-install.js +6 -0
- package/dist/stdout-guard.d.ts +20 -0
- package/dist/stdout-guard.js +72 -0
- package/docs/acceptance.md +245 -0
- package/docs/adr/0005-macos-setup-platform.md +84 -0
- package/docs/adr/0006-pi-extension-allowlist.md +58 -0
- package/docs/architecture.md +1 -1
- package/docs/client-setup-design.md +21 -9
- package/docs/client-setup.md +56 -10
- package/docs/doctor-design.md +9 -2
- package/docs/installation.md +129 -4
- package/docs/macos-setup-design.md +386 -0
- package/docs/pi-extensions-design.md +421 -0
- package/docs/releasing.md +1 -1
- package/docs/superpowers/plans/2026-10-10-macos-setup.md +1725 -0
- package/docs/superpowers/plans/2026-10-11-pi-extensions.md +2450 -0
- package/docs/threat-model.md +54 -18
- package/docs/tool-contracts.md +8 -3
- package/package.json +1 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# macOS setup platform design
|
|
2
|
+
|
|
3
|
+
**Status:** Implemented (2026-10-10); Level-2 acceptance on real hardware is
|
|
4
|
+
recorded for Claude Code (2026-10-11) and pending for Codex. Tracking issue: #32. Follows #28 and PR #30. The decision record is
|
|
5
|
+
[ADR 0005](adr/0005-macos-setup-platform.md).
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
|
|
9
|
+
The complete setup lifecycle (`--dry-run`, `--apply`, `--verify`,
|
|
10
|
+
`--rollback`, `--remove`) works on macOS with the same guarantees as on Linux:
|
|
11
|
+
|
|
12
|
+
- a kernel lock fences the whole inspect → pending → client mutation → owned
|
|
13
|
+
transaction;
|
|
14
|
+
- the fence is released only after a determinate process-table scan confirms
|
|
15
|
+
that no non-zombie member of a client command's process group remains;
|
|
16
|
+
- the ownership directory sits on a filesystem whose lock and fsync semantics
|
|
17
|
+
are accepted.
|
|
18
|
+
|
|
19
|
+
macOS counts as accepted only after Level-2 setup acceptance on real hardware
|
|
20
|
+
(see [Acceptance on real hardware](#acceptance-on-real-hardware)). Until then,
|
|
21
|
+
the documentation calls it CI-tested.
|
|
22
|
+
|
|
23
|
+
## Non-goals
|
|
24
|
+
|
|
25
|
+
- Windows and every platform other than `linux` and `darwin` keep failing
|
|
26
|
+
closed with `platform_unsupported` before any client command is spawned.
|
|
27
|
+
- Intel Macs are not part of the CI matrix. The code path is identical, so
|
|
28
|
+
they are expected to work but are documented as untested.
|
|
29
|
+
- The ownership location stays `~/.local/state/pi-session-mcp/` on both
|
|
30
|
+
platforms.
|
|
31
|
+
- No new public result codes, statuses, exit codes or result schema changes.
|
|
32
|
+
- No release or version change; that is a separate release change.
|
|
33
|
+
|
|
34
|
+
## Current Linux dependencies
|
|
35
|
+
|
|
36
|
+
| Concern | Linux mechanism | Location |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| Kernel lock | util-linux `flock -E 75 -w N 3` locks the open file description that the parent opened; the guardian inherits it as fd 3 | `src/setup-process.ts` `acquireSetupGuardian` |
|
|
39
|
+
| Process-group proof | `/proc/<pid>/stat` scan; `Z` entries do not count as live | `src/setup-process.ts` `processGroupState`, `src/setup-command-guardian.ts` `groupState`, `processTableReadable` |
|
|
40
|
+
| Filesystem | `statfs().type` allowlist of Linux magic numbers | `src/setup-ownership.ts` `SUPPORTED_LOCAL_FILESYSTEMS` |
|
|
41
|
+
| Platform gates | `process.platform !== "linux"` | `src/setup-main.ts`, `src/setup-process.ts` (runner, guardian), `src/setup-ownership.ts` |
|
|
42
|
+
|
|
43
|
+
Facts that rule out a direct port, with sources from the research on
|
|
44
|
+
2026-10-10:
|
|
45
|
+
|
|
46
|
+
- macOS ships no `flock(1)`.
|
|
47
|
+
- macOS has no `/proc`.
|
|
48
|
+
- On macOS, `statfs` `f_type` is a dynamically assigned VFS type index, not a
|
|
49
|
+
stable magic number (Apple Developer Forums thread 87745). Node does not
|
|
50
|
+
expose `f_fstypename`.
|
|
51
|
+
|
|
52
|
+
## Design
|
|
53
|
+
|
|
54
|
+
### Platform module
|
|
55
|
+
|
|
56
|
+
A new module `src/setup-platform.ts` owns every operating-system fact that the
|
|
57
|
+
fence relies on. It has no relative imports, so the guardian can load it in
|
|
58
|
+
both source and built form.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export type SetupPlatform = "linux" | "darwin";
|
|
62
|
+
export function currentSetupPlatform(platform?: NodeJS.Platform): SetupPlatform | undefined;
|
|
63
|
+
|
|
64
|
+
export type ProcessGroupState = "alive" | "gone" | "unknown";
|
|
65
|
+
export interface ProcessTable {
|
|
66
|
+
/** "alive" if any non-zombie process has this process group ID. */
|
|
67
|
+
groupState(group: number): Promise<ProcessGroupState>;
|
|
68
|
+
/** True only if a scan is trustworthy and contains the calling process. */
|
|
69
|
+
readable(): Promise<boolean>;
|
|
70
|
+
/** Poll interval for cleanup loops: 10 ms on Linux, 50 ms on macOS. */
|
|
71
|
+
readonly pollMs: number;
|
|
72
|
+
}
|
|
73
|
+
export function createProcessTable(platform: SetupPlatform): ProcessTable;
|
|
74
|
+
|
|
75
|
+
export function parsePsTable(output: string): readonly ProcessEntry[] | undefined;
|
|
76
|
+
export function isTrustedSystemBinary(path: string): boolean;
|
|
77
|
+
export function assertSupportedFilesystem(path: string, platform: SetupPlatform): Promise<void>;
|
|
78
|
+
export function openLockedDarwin(path: string, options: DarwinLockOptions): Promise<FileHandle>;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The exact signatures may gain injected dependencies for tests, such as `stat`,
|
|
82
|
+
`statfs`, `open` and the `ps` executor. The behavior below is the contract.
|
|
83
|
+
|
|
84
|
+
`isTrustedSystemBinary` is the existing `flock` check, moved and shared: a
|
|
85
|
+
regular file owned by uid 0, not group- or world-writable, and executable.
|
|
86
|
+
|
|
87
|
+
### Kernel lock
|
|
88
|
+
|
|
89
|
+
**Linux (unchanged):**
|
|
90
|
+
|
|
91
|
+
1. The parent opens the lock file with `O_CREAT | O_RDWR | O_NOFOLLOW`.
|
|
92
|
+
2. It validates the file.
|
|
93
|
+
3. A trusted `flock -E 75 -w N 3` locks the shared open file description.
|
|
94
|
+
4. The guardian inherits that description as fd 3.
|
|
95
|
+
|
|
96
|
+
**macOS:**
|
|
97
|
+
|
|
98
|
+
1. The parent opens the lock file with
|
|
99
|
+
`O_CREAT | O_RDWR | O_NOFOLLOW | O_EXLOCK | O_NONBLOCK`.
|
|
100
|
+
- `O_EXLOCK` (`0x20`) and `O_NONBLOCK` (`0x4`) come from XNU
|
|
101
|
+
`bsd/sys/fcntl.h`. `fs.constants` does not export `O_EXLOCK`, so it is a
|
|
102
|
+
named constant in the module with that citation.
|
|
103
|
+
- XNU takes an `F_FLOCK` write lock on the open file description at open
|
|
104
|
+
time. libuv passes the flags through unchanged, adding only
|
|
105
|
+
`O_CLOEXEC`.
|
|
106
|
+
2. `EAGAIN` or `EWOULDBLOCK` means the lock is held elsewhere. The open is
|
|
107
|
+
retried every 50 ms until the existing acquisition timeout
|
|
108
|
+
(`acquireTimeoutMs`, default 30 s, at most 120 s); then it fails with
|
|
109
|
+
`ownership_lock_busy`.
|
|
110
|
+
- An abort signal ends the loop with `operation_aborted`.
|
|
111
|
+
- Every other error, including `EOPNOTSUPP` or `ENOTSUP` on a filesystem
|
|
112
|
+
without locking, is `ownership_lock_unavailable`.
|
|
113
|
+
- A failed attempt returns no descriptor and cannot release another
|
|
114
|
+
holder's lock, because these are flock semantics on the open file
|
|
115
|
+
description.
|
|
116
|
+
3. The rest is the Linux path, unchanged:
|
|
117
|
+
- `chmod 0600`;
|
|
118
|
+
- fsync of the file and its directory;
|
|
119
|
+
- the inode comparison between descriptor and path;
|
|
120
|
+
- the `afterLockOpen` test hook;
|
|
121
|
+
- `validateFence`;
|
|
122
|
+
- starting the guardian with the locked descriptor as fd 3;
|
|
123
|
+
- the second validation.
|
|
124
|
+
4. `acquireSetupGuardian` derives the lock step from the platform:
|
|
125
|
+
- on Linux it runs the util-linux step;
|
|
126
|
+
- on macOS it skips it, because the caller passes a descriptor from
|
|
127
|
+
`openLockedDarwin` that is already locked.
|
|
128
|
+
|
|
129
|
+
There is no lock-mode parameter, so a caller cannot select the
|
|
130
|
+
already-locked mode for an unlocked descriptor by mistake. The guardian
|
|
131
|
+
start, IPC protocol and release are shared.
|
|
132
|
+
|
|
133
|
+
### Process-group proof
|
|
134
|
+
|
|
135
|
+
**Linux:** the existing `/proc` scan moves unchanged into the platform module.
|
|
136
|
+
The two copies in `setup-process.ts` and `setup-command-guardian.ts` become
|
|
137
|
+
one.
|
|
138
|
+
|
|
139
|
+
**macOS:** one scan runs `/bin/ps -A -o pid=,pgid=,stat=`. The call:
|
|
140
|
+
|
|
141
|
+
- uses the absolute path;
|
|
142
|
+
- passes `isTrustedSystemBinary` before every call;
|
|
143
|
+
- runs with fixed arguments, an environment of only `LC_ALL=C`, a 2 s timeout
|
|
144
|
+
and a 1 MiB output cap;
|
|
145
|
+
- is not detached, so `ps` is never a member of the group under test.
|
|
146
|
+
|
|
147
|
+
`parsePsTable` accepts only lines that match `^\s*(\d+)\s+(\d+)\s+(\S+)\s*$`.
|
|
148
|
+
Any other line rejects the whole output.
|
|
149
|
+
|
|
150
|
+
The scan result is `unknown` if any of these holds:
|
|
151
|
+
|
|
152
|
+
- `ps` is untrusted, fails to start, times out or exits non-zero;
|
|
153
|
+
- the output exceeds the cap or fails to parse;
|
|
154
|
+
- the output does not contain the calling process's own PID.
|
|
155
|
+
|
|
156
|
+
Otherwise the result is `alive` if any entry has the requested process group
|
|
157
|
+
ID and a `stat` that does not start with `Z`, and `gone` if none does.
|
|
158
|
+
|
|
159
|
+
`readable()` is true only for a scan that is not `unknown`. The guardian sends
|
|
160
|
+
`ready` only after `readable()` succeeds, as it does today with `/proc`.
|
|
161
|
+
|
|
162
|
+
`kill(-pgid, 0)` is deliberately not used as proof. The XNU source treats
|
|
163
|
+
zombie-only groups inconsistently.
|
|
164
|
+
|
|
165
|
+
**Callers become asynchronous:**
|
|
166
|
+
|
|
167
|
+
- the process runner's `finishAfterGroup`;
|
|
168
|
+
- the MCP launcher's `kill`;
|
|
169
|
+
- the guardian's `groupState` loops.
|
|
170
|
+
|
|
171
|
+
Deadlines stay as they are (2 s report deadline, 1 s launcher cleanup). Only
|
|
172
|
+
the poll interval comes from `ProcessTable.pollMs`.
|
|
173
|
+
|
|
174
|
+
**Guardian loading.** The guardian is started as `.ts` in source-mode tests
|
|
175
|
+
(Node type stripping) and as `.js` in production. A static relative import
|
|
176
|
+
would fail in source mode. The guardian therefore loads the platform module
|
|
177
|
+
with a dynamic import that picks `./setup-platform.ts` or
|
|
178
|
+
`./setup-platform.js` next to itself. This is the same selection that
|
|
179
|
+
`setup-process.ts` already uses to locate the guardian. `setup-platform.js`
|
|
180
|
+
becomes a mandatory regular, non-symlink release artifact next to `main.js`
|
|
181
|
+
and `setup-command-guardian.js` in `src/setup.ts`. The ownership fingerprint
|
|
182
|
+
already binds every built runtime module.
|
|
183
|
+
|
|
184
|
+
### Filesystem rule
|
|
185
|
+
|
|
186
|
+
**Linux:** unchanged magic-number allowlist.
|
|
187
|
+
|
|
188
|
+
**macOS:** the ownership directory, and the nearest existing ancestor that is
|
|
189
|
+
checked before creation, must have the same `statfs().type` as `/`. The
|
|
190
|
+
reasoning:
|
|
191
|
+
|
|
192
|
+
- The minimum macOS that Node.js 22 supports is macOS 11.
|
|
193
|
+
- From macOS 11 on, `/` is the sealed APFS system volume, so its type is
|
|
194
|
+
APFS in every case.
|
|
195
|
+
- `f_type` identifies a filesystem type for the running kernel, so equality
|
|
196
|
+
with `/` means APFS.
|
|
197
|
+
- APFS volumes are local.
|
|
198
|
+
- HFS+, exFAT, MS-DOS, SMB, NFS, AFP and every other type fail closed with
|
|
199
|
+
`ownership_filesystem_unsupported`.
|
|
200
|
+
|
|
201
|
+
Directory fsync uses the existing `FileHandle.sync()`. On macOS, libuv already
|
|
202
|
+
tries `F_FULLFSYNC`, then `F_BARRIERFSYNC`, then `fsync`. Whether this
|
|
203
|
+
succeeds on a directory descriptor on APFS is unverified, so a macOS CI test
|
|
204
|
+
asserts it. A failure fails closed.
|
|
205
|
+
|
|
206
|
+
### Platform gates
|
|
207
|
+
|
|
208
|
+
`process.platform !== "linux"` becomes `currentSetupPlatform() === undefined`
|
|
209
|
+
in these places:
|
|
210
|
+
|
|
211
|
+
- `setup-main.ts`, before any client command;
|
|
212
|
+
- the process runner, which returns exit code 126;
|
|
213
|
+
- `acquireSetupGuardian`;
|
|
214
|
+
- `DurableOwnershipStore.withLock`.
|
|
215
|
+
|
|
216
|
+
## Failure semantics
|
|
217
|
+
|
|
218
|
+
Public behavior is identical on both platforms:
|
|
219
|
+
|
|
220
|
+
- The stable codes are unchanged. The internal lock and filesystem errors are
|
|
221
|
+
`ownership_lock_busy`, `ownership_lock_unavailable` and
|
|
222
|
+
`ownership_filesystem_unsupported`. They never reach a report. When one
|
|
223
|
+
ends an ownership transaction, the CLI reports `operation_failed` with
|
|
224
|
+
exit 1, or `operation_interrupted` with exit 130 or 143 after a signal. This
|
|
225
|
+
is the same as on Linux. Failures inside a transaction keep their existing
|
|
226
|
+
public codes, such as `ownership_unavailable` and `cleanup_failed`.
|
|
227
|
+
- An `unknown` process-group state while the guardian holds the fence keeps
|
|
228
|
+
the fence and reports exit code 126 to the caller. Cleanup continues in the
|
|
229
|
+
detached guardian.
|
|
230
|
+
- Exit codes stay `0`, `1`, `64`, `130` and `143`.
|
|
231
|
+
- Results never contain paths, command arguments, command output or raw
|
|
232
|
+
errors. `ps` output never leaves the platform module.
|
|
233
|
+
|
|
234
|
+
## Trust boundary and limits
|
|
235
|
+
|
|
236
|
+
These are recorded in `threat-model.md`; `client-setup-design.md` names the platform requirements:
|
|
237
|
+
|
|
238
|
+
- On macOS, the proof that no live group member remains rests on `/bin/ps`,
|
|
239
|
+
which sits on the sealed, SIP-protected system volume, together with the
|
|
240
|
+
uid and mode check.
|
|
241
|
+
- Tampering by the same UID with the stable lock path stays outside the
|
|
242
|
+
fence's trust boundary on both platforms.
|
|
243
|
+
- On both platforms, the fence does not cover descendants that escape the
|
|
244
|
+
process group, and it does not promise PID reaping.
|
|
245
|
+
- The macOS lock depends on the documented `O_EXLOCK` behavior of open(2):
|
|
246
|
+
flock(2) semantics, with `EOPNOTSUPP` on filesystems without locking.
|
|
247
|
+
|
|
248
|
+
## Testing
|
|
249
|
+
|
|
250
|
+
**Unit tests, run on Linux and macOS with injected dependencies:**
|
|
251
|
+
|
|
252
|
+
- `parsePsTable`:
|
|
253
|
+
- a live group, a zombie-only group and a foreign group;
|
|
254
|
+
- a malformed line, an extra header line, empty output;
|
|
255
|
+
- a missing own PID, a non-zero exit, output over the cap.
|
|
256
|
+
- `isTrustedSystemBinary`: not root-owned, group-writable, world-writable,
|
|
257
|
+
not executable, not a regular file, missing. Like the existing `flock`
|
|
258
|
+
check, it follows symlinks, so Linux behavior is unchanged.
|
|
259
|
+
- macOS filesystem rule: same type, different type, and `statfs` failure.
|
|
260
|
+
- macOS lock loop:
|
|
261
|
+
- `EAGAIN` then success; persistent `EAGAIN` until the timeout gives
|
|
262
|
+
`ownership_lock_busy`;
|
|
263
|
+
- `EOPNOTSUPP` gives `ownership_lock_unavailable`;
|
|
264
|
+
- an abort during the wait gives `operation_aborted`;
|
|
265
|
+
- no descriptor leaks on any of these paths.
|
|
266
|
+
- `currentSetupPlatform`: `linux`, `darwin`, `win32`, `freebsd`.
|
|
267
|
+
|
|
268
|
+
**Integration tests:** their gate changes from `process.platform !== "linux"`
|
|
269
|
+
to "setup platform supported", so they also run on macOS:
|
|
270
|
+
|
|
271
|
+
- `test/setup-ownership.test.ts`: the durable store, including real lock
|
|
272
|
+
contention between processes;
|
|
273
|
+
- `test/setup-process.test.ts`: the runner terminates a real process group,
|
|
274
|
+
and fence retention on an indeterminate scan;
|
|
275
|
+
- `test/setup-main.test.ts`: the `SIGINT` and `SIGTERM` cleanup with exit
|
|
276
|
+
codes 130 and 143, and the stubborn process group;
|
|
277
|
+
- `test/setup-lifecycle.test.ts`: the corrupt durable ownership store.
|
|
278
|
+
|
|
279
|
+
**macOS-only tests:**
|
|
280
|
+
|
|
281
|
+
- `O_EXLOCK` semantics:
|
|
282
|
+
- a second non-blocking open is busy while the first descriptor is open;
|
|
283
|
+
- a child process that inherited the descriptor keeps the lock after the
|
|
284
|
+
parent closes its copy;
|
|
285
|
+
- the lock is free after the last close.
|
|
286
|
+
- Directory fsync on the test's temporary APFS directory succeeds.
|
|
287
|
+
- `os.tmpdir()` passes the macOS filesystem rule.
|
|
288
|
+
|
|
289
|
+
**Server lifecycle:** `test/process-fixture.ts` reads `/proc/<pid>/stat` and
|
|
290
|
+
`/proc/<pid>/cmdline`. It moves to a platform helper that uses `/proc` on
|
|
291
|
+
Linux and `/bin/ps -p <pid> -o stat=,command=` on macOS. Then
|
|
292
|
+
`test/process-stdin-eof.test.ts` also runs on macOS and proves that the server
|
|
293
|
+
exits on stdin EOF there. This is a separate commit.
|
|
294
|
+
|
|
295
|
+
**Load sensitivity:** no timeout is raised in advance. Flaky tests observed on
|
|
296
|
+
macOS runners are treated individually, with the cause recorded.
|
|
297
|
+
|
|
298
|
+
## CI
|
|
299
|
+
|
|
300
|
+
The ruleset "Protect main" requires a status check named `verify`. A plain
|
|
301
|
+
matrix would rename it to `verify (macos-15)` and so on, and block every merge.
|
|
302
|
+
The workflow therefore has two jobs:
|
|
303
|
+
|
|
304
|
+
- **`test`:** a matrix over `os: [ubuntu-latest, macos-15, macos-26]` with
|
|
305
|
+
`fail-fast: false`, the same steps as today, and Node pinned to `22.19.0`.
|
|
306
|
+
- **`verify`:** depends on `test`, always runs, and passes only if every
|
|
307
|
+
platform passed. It keeps the required check name, so the ruleset does
|
|
308
|
+
not change.
|
|
309
|
+
|
|
310
|
+
Standard GitHub-hosted runners are free for this public repository. The Pi SDK
|
|
311
|
+
canary stays on Linux.
|
|
312
|
+
|
|
313
|
+
## Documentation and governance
|
|
314
|
+
|
|
315
|
+
- **ADR 0005, "macOS as a supported setup platform":**
|
|
316
|
+
- It records that the B-type revisit trigger of ADR 0003 (repeated setup
|
|
317
|
+
friction) fired with #28.
|
|
318
|
+
- It records the chosen approach and the rejected alternatives:
|
|
319
|
+
- a `lockf(1)` helper with `/sbin/mount` parsing, rejected because the fd
|
|
320
|
+
mode of `lockf` on macOS 15 and the `mount` output format are
|
|
321
|
+
unverified;
|
|
322
|
+
- a weaker macOS profile without the kernel fence or the process-group
|
|
323
|
+
proof.
|
|
324
|
+
- It sets the condition that macOS counts as accepted only after Level-2
|
|
325
|
+
acceptance on real hardware.
|
|
326
|
+
- Its revisit triggers: a change in the `ps` output format, in `O_EXLOCK`
|
|
327
|
+
semantics, or in the minimum macOS version that Node supports.
|
|
328
|
+
- **`AGENTS.md`:** the rule "Mutating setup is Linux-only and requires …"
|
|
329
|
+
becomes a rule for Linux or macOS, each with its platform's requirements.
|
|
330
|
+
This change to an agent rule is called out in the PR.
|
|
331
|
+
- **`docs/client-setup-design.md` and `docs/threat-model.md`:** platform
|
|
332
|
+
requirements and the trust boundary for both platforms.
|
|
333
|
+
- **`docs/installation.md`:**
|
|
334
|
+
- `setup` on macOS;
|
|
335
|
+
- "Register without setup" stays as a general alternative, without a macOS
|
|
336
|
+
special case.
|
|
337
|
+
- **`README.md`:** the Status line names Linux and macOS, the macOS CI
|
|
338
|
+
versions, Intel as untested, and the acceptance status.
|
|
339
|
+
- **`docs/acceptance.md`:** the macOS Level-2 checklist and its evidence
|
|
340
|
+
format.
|
|
341
|
+
- **`CHANGELOG.md`:** an Unreleased entry.
|
|
342
|
+
|
|
343
|
+
## Acceptance on real hardware
|
|
344
|
+
|
|
345
|
+
A Level-2 run on the owner's Mac, from the PR branch build, records:
|
|
346
|
+
|
|
347
|
+
- **Environment:** macOS version, architecture, Node version, Claude Code and
|
|
348
|
+
Codex versions.
|
|
349
|
+
- **Before mutating:** `doctor`, and `setup --dry-run` for
|
|
350
|
+
`claude-code:user:pi-session-mcp` and `codex:user:pi-session-mcp`.
|
|
351
|
+
- **The lifecycle, for each target:** `--apply`, `--verify`, `--rollback`,
|
|
352
|
+
then `--apply` and `--remove`.
|
|
353
|
+
- **Lock contention:** two `--apply` runs for the same target, started in
|
|
354
|
+
parallel.
|
|
355
|
+
- The second waits for the first, because the acquisition timeout is 30 s.
|
|
356
|
+
- It then reports the registration as already equivalent.
|
|
357
|
+
- Both exit with 0, and the ownership record stays readable.
|
|
358
|
+
- A waiting run that reports `operation_failed` with exit 1 did not get the
|
|
359
|
+
lock within the timeout.
|
|
360
|
+
- **Interruption:** Ctrl-C during `--apply` gives exit code 130, leaves no
|
|
361
|
+
process from the client command's group behind, and a following run
|
|
362
|
+
reconciles.
|
|
363
|
+
- **Evidence:** exit codes and the sanitized human and JSON results. No paths,
|
|
364
|
+
environment values or secrets are recorded.
|
|
365
|
+
|
|
366
|
+
The evidence is committed to the same PR before merge.
|
|
367
|
+
|
|
368
|
+
## Delivery
|
|
369
|
+
|
|
370
|
+
One branch, `feat/macos-setup`, and one PR referencing #32. Separate commits,
|
|
371
|
+
in this order:
|
|
372
|
+
|
|
373
|
+
1. platform module;
|
|
374
|
+
2. macOS lock;
|
|
375
|
+
3. process table and guardian;
|
|
376
|
+
4. filesystem rule;
|
|
377
|
+
5. server lifecycle fixture;
|
|
378
|
+
6. CI matrix;
|
|
379
|
+
7. documentation, ADR 0005 and `AGENTS.md`;
|
|
380
|
+
8. acceptance evidence.
|
|
381
|
+
|
|
382
|
+
The merge requires all of the following:
|
|
383
|
+
|
|
384
|
+
- green CI on all three runners;
|
|
385
|
+
- reviews without open findings;
|
|
386
|
+
- the owner's evidence from the Mac.
|