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.
Files changed (60) hide show
  1. package/.pi-session-mcp-cli/config.js +11 -1
  2. package/.pi-session-mcp-cli/doctor-cli.js +5 -1
  3. package/.pi-session-mcp-cli/doctor.js +22 -1
  4. package/.pi-session-mcp-cli/execution-profile.js +1 -0
  5. package/.pi-session-mcp-cli/extension-spec.js +21 -0
  6. package/.pi-session-mcp-cli/pi-extensions.js +67 -0
  7. package/CHANGELOG.md +54 -0
  8. package/CONTRIBUTING.md +1 -1
  9. package/README.md +2 -2
  10. package/SECURITY.md +1 -1
  11. package/dist/capabilities.d.ts +1 -0
  12. package/dist/capabilities.js +2 -1
  13. package/dist/client-adapters/mcp-verifier.js +5 -2
  14. package/dist/config.d.ts +1 -0
  15. package/dist/config.js +11 -1
  16. package/dist/doctor-cli.js +5 -1
  17. package/dist/doctor.d.ts +4 -1
  18. package/dist/doctor.js +22 -1
  19. package/dist/execution-profile.d.ts +3 -0
  20. package/dist/execution-profile.js +1 -0
  21. package/dist/extension-spec.d.ts +15 -0
  22. package/dist/extension-spec.js +21 -0
  23. package/dist/main.js +6 -2
  24. package/dist/pi-adapter.d.ts +1 -1
  25. package/dist/pi-adapter.js +4 -0
  26. package/dist/pi-extensions.d.ts +17 -0
  27. package/dist/pi-extensions.js +67 -0
  28. package/dist/sdk-pi-adapter.d.ts +24 -6
  29. package/dist/sdk-pi-adapter.js +253 -86
  30. package/dist/server.js +1 -0
  31. package/dist/session-tools.d.ts +9 -0
  32. package/dist/session-tools.js +25 -0
  33. package/dist/setup-command-guardian.js +29 -73
  34. package/dist/setup-main.js +2 -1
  35. package/dist/setup-ownership.js +18 -28
  36. package/dist/setup-platform.d.ts +57 -0
  37. package/dist/setup-platform.js +195 -0
  38. package/dist/setup-process.d.ts +6 -3
  39. package/dist/setup-process.js +85 -93
  40. package/dist/setup.js +2 -1
  41. package/dist/stdout-guard-install.d.ts +5 -0
  42. package/dist/stdout-guard-install.js +6 -0
  43. package/dist/stdout-guard.d.ts +20 -0
  44. package/dist/stdout-guard.js +72 -0
  45. package/docs/acceptance.md +245 -0
  46. package/docs/adr/0005-macos-setup-platform.md +84 -0
  47. package/docs/adr/0006-pi-extension-allowlist.md +58 -0
  48. package/docs/architecture.md +1 -1
  49. package/docs/client-setup-design.md +21 -9
  50. package/docs/client-setup.md +56 -10
  51. package/docs/doctor-design.md +9 -2
  52. package/docs/installation.md +129 -4
  53. package/docs/macos-setup-design.md +386 -0
  54. package/docs/pi-extensions-design.md +421 -0
  55. package/docs/releasing.md +1 -1
  56. package/docs/superpowers/plans/2026-10-10-macos-setup.md +1725 -0
  57. package/docs/superpowers/plans/2026-10-11-pi-extensions.md +2450 -0
  58. package/docs/threat-model.md +54 -18
  59. package/docs/tool-contracts.md +8 -3
  60. package/package.json +1 -1
@@ -1,9 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { readdirSync, readFileSync } from "node:fs";
3
+ // Source-mode tests run this guardian through Node type stripping, which cannot
4
+ // map a static `./setup-platform.js` import to its `.ts` source.
5
+ const platform = await import(import.meta.url.endsWith(".ts") ? "./setup-platform.ts" : "./setup-platform.js");
6
+ const platformName = platform.currentSetupPlatform();
7
+ const table = platformName ? platform.createProcessTable(platformName) : undefined;
4
8
  const TERM_GRACE_MS = 100;
5
9
  const CLEANUP_REPORT_MS = 2_000;
6
10
  const RETRY_MS = 25;
11
+ // Each macOS scan starts `ps`, so cleanup loops poll no faster than the table allows.
12
+ const CLEANUP_RETRY_MS = Math.max(RETRY_MS, table?.pollMs ?? RETRY_MS);
7
13
  let active;
8
14
  let shuttingDown = false;
9
15
  // Source-mode signal injection is available only to credential-free tests. The
@@ -54,65 +60,10 @@ function parseRequest(value) {
54
60
  return undefined;
55
61
  return { type: "run", id: input.id, command: input.command, args: input.args, timeoutMs: input.timeoutMs, maxOutputBytes: input.maxOutputBytes };
56
62
  }
57
- function readProcessGroup(pid) {
58
- const line = readFileSync(`/proc/${pid}/stat`, "utf8").trim();
59
- const close = line.lastIndexOf(") ");
60
- const state = line[close + 2];
61
- if (close < 3 || !state || !/^[A-Za-z]$/.test(state) || line[close + 3] !== " ")
62
- throw new Error("process_stat_invalid");
63
- const fields = line.slice(close + 4).trim().split(/\s+/);
64
- const group = Number(fields[1]);
65
- if (!Number.isSafeInteger(group) || group < 0)
66
- throw new Error("process_group_invalid");
67
- return { group, state };
68
- }
69
- function processTableReadable() {
70
- let sawSelf = false;
71
- try {
72
- for (const entry of readdirSync("/proc", { withFileTypes: true })) {
73
- if (!entry.isDirectory() || !/^\d+$/.test(entry.name))
74
- continue;
75
- try {
76
- const pid = Number(entry.name);
77
- readProcessGroup(pid);
78
- if (pid === process.pid)
79
- sawSelf = true;
80
- }
81
- catch (error) {
82
- if (error.code !== "ENOENT")
83
- return false;
84
- }
85
- }
86
- }
87
- catch {
88
- return false;
89
- }
90
- return sawSelf;
91
- }
92
- function groupState(group) {
93
- if (forceUnknownForSourceTest)
94
- return "unknown";
95
- let entries;
96
- try {
97
- entries = readdirSync("/proc", { withFileTypes: true });
98
- }
99
- catch {
63
+ async function groupState(group) {
64
+ if (forceUnknownForSourceTest || !table)
100
65
  return "unknown";
101
- }
102
- for (const entry of entries) {
103
- if (!entry.isDirectory() || !/^\d+$/.test(entry.name))
104
- continue;
105
- try {
106
- const processState = readProcessGroup(Number(entry.name));
107
- if (processState.group === group && processState.state !== "Z")
108
- return "alive";
109
- }
110
- catch (error) {
111
- if (error.code !== "ENOENT")
112
- return "unknown";
113
- }
114
- }
115
- return "gone";
66
+ return table.groupState(group);
116
67
  }
117
68
  function signalGroup(group, signal) {
118
69
  try {
@@ -176,7 +127,7 @@ function start(request) {
176
127
  signalGroup(execution.group, "SIGKILL");
177
128
  const reportDeadline = Date.now() + CLEANUP_REPORT_MS;
178
129
  while (true) {
179
- const state = groupState(execution.group);
130
+ const state = await groupState(execution.group);
180
131
  if (state === "gone") {
181
132
  finish(reportedCode);
182
133
  active = undefined;
@@ -187,7 +138,7 @@ function start(request) {
187
138
  if (!execution.resultSent && Date.now() >= reportDeadline)
188
139
  finish(126);
189
140
  signalGroup(execution.group, "SIGKILL");
190
- await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
141
+ await new Promise((resolve) => setTimeout(resolve, CLEANUP_RETRY_MS));
191
142
  }
192
143
  };
193
144
  execution.timeout = setTimeout(() => { void cleanup(124); }, request.timeoutMs);
@@ -202,15 +153,20 @@ function start(request) {
202
153
  child.once("close", (code) => {
203
154
  if (execution.cleanupStarted)
204
155
  return;
205
- const state = groupState(execution.group);
206
- if (state === "gone") {
207
- finish(code ?? 1);
208
- active = undefined;
209
- if (shuttingDown || !process.connected)
210
- exitWhenSafe();
211
- }
212
- else
213
- void cleanup(code ?? 1);
156
+ if (execution.timeout)
157
+ clearTimeout(execution.timeout);
158
+ void groupState(execution.group).then((state) => {
159
+ if (execution.cleanupStarted)
160
+ return;
161
+ if (state === "gone") {
162
+ finish(code ?? 1);
163
+ active = undefined;
164
+ if (shuttingDown || !process.connected)
165
+ exitWhenSafe();
166
+ }
167
+ else
168
+ void cleanup(code ?? 1);
169
+ });
214
170
  });
215
171
  }
216
172
  process.on("message", (value) => {
@@ -253,7 +209,7 @@ async function terminateActive() {
253
209
  signalGroup(execution.group, "SIGKILL");
254
210
  const reportDeadline = Date.now() + CLEANUP_REPORT_MS;
255
211
  while (true) {
256
- const state = groupState(execution.group);
212
+ const state = await groupState(execution.group);
257
213
  if (state === "gone") {
258
214
  if (!execution.resultSent) {
259
215
  execution.resultSent = true;
@@ -269,7 +225,7 @@ async function terminateActive() {
269
225
  send({ type: "result", id: execution.id, exitCode: 126, stdout: "", stderr: "" });
270
226
  }
271
227
  signalGroup(execution.group, "SIGKILL");
272
- await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
228
+ await new Promise((resolve) => setTimeout(resolve, CLEANUP_RETRY_MS));
273
229
  }
274
230
  }
275
231
  process.on("disconnect", () => {
@@ -287,7 +243,7 @@ process.once("SIGTERM", () => { shuttingDown = true; if (active)
287
243
  void terminateActive();
288
244
  else
289
245
  exitWhenSafe(); });
290
- if (processTableReadable())
246
+ if (table && await table.readable())
291
247
  send({ type: "ready" });
292
248
  else {
293
249
  clearInterval(parentWatch);
@@ -3,6 +3,7 @@ import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { isEntryPoint } from "./entry-point.js";
5
5
  import { createProcessRunner } from "./setup-process.js";
6
+ import { currentSetupPlatform } from "./setup-platform.js";
6
7
  import { createProductionSetup, isSupportedTarget } from "./setup.js";
7
8
  import { formatSetupHuman, formatSetupResult, parseSetupArgs } from "./setup-cli.js";
8
9
  import { result } from "./setup-result.js";
@@ -17,7 +18,7 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
17
18
  return write(cliFailure("dry-run", [], "usage_invalid", 64), json);
18
19
  }
19
20
  const operation = request.operation ?? "dry-run";
20
- if (process.platform !== "linux")
21
+ if (!currentSetupPlatform())
21
22
  return write(cliFailure(operation, request.targets, "platform_unsupported", 1), request.json);
22
23
  const config = process.env.PI_SESSION_MCP_CONFIG;
23
24
  if (!config)
@@ -1,8 +1,9 @@
1
1
  import { constants } from "node:fs";
2
- import { chmod, lstat, mkdir, open, readFile, rename, rm, stat, statfs } from "node:fs/promises";
2
+ import { chmod, lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { dirname } from "node:path";
5
5
  import { acquireSetupGuardian } from "./setup-process.js";
6
+ import { assertSupportedFilesystem, currentSetupPlatform, openLockedDarwin } from "./setup-platform.js";
6
7
  /** Safe default for one process; callers needing recovery inject a durable store. */
7
8
  export class MemoryOwnershipStore {
8
9
  entries = new Map();
@@ -48,10 +49,11 @@ export class DurableOwnershipStore {
48
49
  return this.withLock(operation);
49
50
  }
50
51
  async withLock(operation) {
51
- if (process.platform !== "linux")
52
+ const platform = currentSetupPlatform();
53
+ if (!platform)
52
54
  throw new Error("platform_unsupported");
53
55
  const directory = dirname(this.path);
54
- await ensureOwnershipDirectory(directory);
56
+ await ensureOwnershipDirectory(directory, platform);
55
57
  const controller = new AbortController();
56
58
  const abort = () => controller.abort();
57
59
  this.options.signal?.addEventListener("abort", abort, { once: true });
@@ -62,9 +64,14 @@ export class DurableOwnershipStore {
62
64
  throw new Error("operation_aborted");
63
65
  };
64
66
  const lockPath = `${this.path}.flock`;
67
+ const acquireTimeoutMs = this.options.acquireTimeoutMs ?? 30_000;
65
68
  let lockHandle;
66
69
  try {
67
- lockHandle = await open(lockPath, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
70
+ // macOS takes the kernel lock at open time; on Linux the trusted flock
71
+ // locks this same open file description in acquireSetupGuardian.
72
+ lockHandle = platform === "darwin"
73
+ ? await openLockedDarwin(lockPath, { timeoutMs: acquireTimeoutMs, signal: controller.signal })
74
+ : await open(lockPath, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
68
75
  await lockHandle.chmod(0o600);
69
76
  await lockHandle.sync();
70
77
  await syncDirectory(directory);
@@ -73,10 +80,11 @@ export class DurableOwnershipStore {
73
80
  throw new Error("ownership_lock_invalid");
74
81
  await this.options.afterLockOpen?.();
75
82
  }
76
- catch {
83
+ catch (error) {
77
84
  this.options.signal?.removeEventListener("abort", abort);
78
85
  await lockHandle?.close().catch(() => undefined);
79
- throw new Error("ownership_lock_unavailable");
86
+ const code = error.message;
87
+ throw new Error(["ownership_lock_busy", "operation_aborted"].includes(code) ? code : "ownership_lock_unavailable");
80
88
  }
81
89
  const validateLock = async () => {
82
90
  const [descriptor, linked] = await Promise.all([lockHandle.stat(), stat(lockPath)]);
@@ -85,7 +93,7 @@ export class DurableOwnershipStore {
85
93
  };
86
94
  let guardian;
87
95
  try {
88
- guardian = await acquireSetupGuardian(lockHandle.fd, this.options.acquireTimeoutMs ?? 30_000, controller.signal, validateLock);
96
+ guardian = await acquireSetupGuardian(lockHandle.fd, acquireTimeoutMs, controller.signal, validateLock);
89
97
  }
90
98
  catch (error) {
91
99
  this.options.signal?.removeEventListener("abort", abort);
@@ -153,20 +161,7 @@ export class DurableOwnershipStore {
153
161
  await rm(temporary, { force: true }).catch(() => undefined);
154
162
  } }
155
163
  }
156
- // Kernel flock and directory fsync semantics are accepted only on the local
157
- // Linux filesystems covered by the setup test matrix.
158
- const SUPPORTED_LOCAL_FILESYSTEMS = new Set([
159
- 0xef53, // ext2/3/4
160
- 0x58465342, // XFS
161
- 0x9123683e, // Btrfs
162
- 0x01021994, // tmpfs
163
- 0x794c7630, // overlayfs
164
- 0x2fc12fc1, // ZFS
165
- 0xf2f52010, // F2FS
166
- 0x24051905, // UBIFS
167
- 0xca451a4e, // bcachefs
168
- ]);
169
- async function ensureOwnershipDirectory(directory) {
164
+ async function ensureOwnershipDirectory(directory, platform) {
170
165
  const missing = [];
171
166
  let cursor = directory;
172
167
  while (true) {
@@ -186,7 +181,7 @@ async function ensureOwnershipDirectory(directory) {
186
181
  cursor = parent;
187
182
  }
188
183
  }
189
- await assertSupportedFilesystem(cursor);
184
+ await assertSupportedFilesystem(cursor, platform);
190
185
  for (const candidate of missing.reverse()) {
191
186
  try {
192
187
  await mkdir(candidate, { mode: 0o700 });
@@ -203,12 +198,7 @@ async function ensureOwnershipDirectory(directory) {
203
198
  await syncDirectory(dirname(candidate));
204
199
  }
205
200
  await chmod(directory, 0o700);
206
- await assertSupportedFilesystem(directory);
207
- }
208
- async function assertSupportedFilesystem(path) {
209
- const filesystem = await statfs(path);
210
- if (!SUPPORTED_LOCAL_FILESYSTEMS.has(filesystem.type))
211
- throw new Error("ownership_filesystem_unsupported");
201
+ await assertSupportedFilesystem(directory, platform);
212
202
  }
213
203
  async function syncDirectory(path) {
214
204
  const handle = await open(path, "r");
@@ -0,0 +1,57 @@
1
+ import type { Stats } from "node:fs";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ /**
4
+ * Operating-system facts behind the setup fence. This module has no relative
5
+ * imports: the transaction guardian and source-mode harnesses load it through
6
+ * Node type stripping, which cannot map a `.js` specifier to its `.ts` source.
7
+ */
8
+ export type SetupPlatform = "linux" | "darwin";
9
+ export type ProcessGroupState = "alive" | "gone" | "unknown";
10
+ export interface ProcessEntry {
11
+ readonly pid: number;
12
+ readonly group: number;
13
+ readonly state: string;
14
+ }
15
+ export interface ProcessTable {
16
+ /** "alive" if any non-zombie process has this process group ID. */
17
+ groupState(group: number): Promise<ProcessGroupState>;
18
+ /** True only if a scan is determinate and contains the calling process. */
19
+ readable(): Promise<boolean>;
20
+ /** Poll interval for cleanup loops. */
21
+ readonly pollMs: number;
22
+ }
23
+ export interface PsOutput {
24
+ readonly exitCode: number;
25
+ readonly stdout: string;
26
+ }
27
+ export interface ProcessTableDependencies {
28
+ /** Test seam for the macOS `ps` call; undefined means it could not run. */
29
+ readonly runPs?: () => Promise<PsOutput | undefined>;
30
+ readonly selfPid?: number;
31
+ }
32
+ export declare function currentSetupPlatform(platform?: NodeJS.Platform): SetupPlatform | undefined;
33
+ /** A regular, root-owned executable that no group or other user can replace. */
34
+ export declare function isTrustedSystemBinary(path: string, statFile?: (path: string) => Pick<Stats, "isFile" | "uid" | "mode">): boolean;
35
+ export declare const DARWIN_PS_PATH = "/bin/ps";
36
+ /** Parses `ps -A -o pid=,pgid=,stat=`; any unexpected line rejects the whole sample. */
37
+ export declare function parsePsTable(output: string): readonly ProcessEntry[] | undefined;
38
+ export declare function createProcessTable(platform: SetupPlatform, dependencies?: ProcessTableDependencies): ProcessTable;
39
+ export type StatfsReader = (path: string) => Promise<{
40
+ readonly type: number;
41
+ }>;
42
+ export declare function assertSupportedFilesystem(path: string, platform: SetupPlatform, readStatfs?: StatfsReader): Promise<void>;
43
+ /** From XNU `bsd/sys/fcntl.h`; Node's `fs.constants` does not export these. */
44
+ export declare const DARWIN_O_NONBLOCK = 4;
45
+ export declare const DARWIN_O_EXLOCK = 32;
46
+ export interface DarwinLockOptions {
47
+ readonly timeoutMs: number;
48
+ readonly signal?: AbortSignal;
49
+ readonly retryMs?: number;
50
+ readonly openFile?: (path: string, flags: number, mode: number) => Promise<FileHandle>;
51
+ }
52
+ /**
53
+ * Opens the lock file with an exclusive lock on its open file description,
54
+ * which has flock(2) semantics on macOS. A busy lock is retried until the
55
+ * acquisition bound; a failed attempt holds no descriptor.
56
+ */
57
+ export declare function openLockedDarwin(path: string, options: DarwinLockOptions): Promise<FileHandle>;
@@ -0,0 +1,195 @@
1
+ import { execFile } from "node:child_process";
2
+ import { constants, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { open, statfs } from "node:fs/promises";
4
+ export function currentSetupPlatform(platform = process.platform) {
5
+ return platform === "linux" || platform === "darwin" ? platform : undefined;
6
+ }
7
+ /** A regular, root-owned executable that no group or other user can replace. */
8
+ export function isTrustedSystemBinary(path, statFile = statSync) {
9
+ try {
10
+ const file = statFile(path);
11
+ return file.isFile() && file.uid === 0 && (file.mode & 0o022) === 0 && (file.mode & 0o111) !== 0;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ function readLinuxProcessGroup(pid) {
18
+ const line = readFileSync(`/proc/${pid}/stat`, "utf8").trim();
19
+ const close = line.lastIndexOf(") ");
20
+ const state = line[close + 2];
21
+ if (close < 3 || !state || !/^[A-Za-z]$/.test(state) || line[close + 3] !== " ")
22
+ throw new Error("process_stat_invalid");
23
+ const fields = line.slice(close + 4).trim().split(/\s+/);
24
+ const group = Number(fields[1]);
25
+ if (!Number.isSafeInteger(group) || group < 0)
26
+ throw new Error("process_group_invalid");
27
+ return { group, state };
28
+ }
29
+ function linuxGroupState(group) {
30
+ let entries;
31
+ try {
32
+ entries = readdirSync("/proc", { withFileTypes: true });
33
+ }
34
+ catch {
35
+ return "unknown";
36
+ }
37
+ for (const entry of entries) {
38
+ if (!entry.isDirectory() || !/^\d+$/.test(entry.name))
39
+ continue;
40
+ try {
41
+ const sample = readLinuxProcessGroup(Number(entry.name));
42
+ if (sample.group === group && sample.state !== "Z")
43
+ return "alive";
44
+ }
45
+ catch (error) {
46
+ if (error.code !== "ENOENT")
47
+ return "unknown";
48
+ }
49
+ }
50
+ return "gone";
51
+ }
52
+ function linuxTableReadable(selfPid) {
53
+ let sawSelf = false;
54
+ try {
55
+ for (const entry of readdirSync("/proc", { withFileTypes: true })) {
56
+ if (!entry.isDirectory() || !/^\d+$/.test(entry.name))
57
+ continue;
58
+ try {
59
+ const pid = Number(entry.name);
60
+ readLinuxProcessGroup(pid);
61
+ if (pid === selfPid)
62
+ sawSelf = true;
63
+ }
64
+ catch (error) {
65
+ if (error.code !== "ENOENT")
66
+ return false;
67
+ }
68
+ }
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ return sawSelf;
74
+ }
75
+ export const DARWIN_PS_PATH = "/bin/ps";
76
+ const PS_ARGUMENTS = ["-A", "-o", "pid=,pgid=,stat="];
77
+ const PS_TIMEOUT_MS = 2_000;
78
+ const PS_MAX_OUTPUT_BYTES = 1_048_576;
79
+ /** Parses `ps -A -o pid=,pgid=,stat=`; any unexpected line rejects the whole sample. */
80
+ export function parsePsTable(output) {
81
+ const body = output.endsWith("\n") ? output.slice(0, -1) : output;
82
+ if (body.length === 0)
83
+ return undefined;
84
+ const entries = [];
85
+ for (const line of body.split("\n")) {
86
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line);
87
+ if (!match)
88
+ return undefined;
89
+ const pid = Number(match[1]);
90
+ const group = Number(match[2]);
91
+ if (!Number.isSafeInteger(pid) || !Number.isSafeInteger(group))
92
+ return undefined;
93
+ entries.push({ pid, group, state: match[3] });
94
+ }
95
+ return entries;
96
+ }
97
+ function runDarwinPs() {
98
+ if (!isTrustedSystemBinary(DARWIN_PS_PATH))
99
+ return Promise.resolve(undefined);
100
+ return new Promise((resolve) => {
101
+ try {
102
+ execFile(DARWIN_PS_PATH, [...PS_ARGUMENTS], { env: { LC_ALL: "C" }, timeout: PS_TIMEOUT_MS, maxBuffer: PS_MAX_OUTPUT_BYTES, encoding: "utf8" }, (error, stdout) => {
103
+ // A non-zero exit, timeout, output overflow or spawn failure is indeterminate.
104
+ resolve(error ? undefined : { exitCode: 0, stdout });
105
+ });
106
+ }
107
+ catch {
108
+ resolve(undefined);
109
+ }
110
+ });
111
+ }
112
+ export function createProcessTable(platform, dependencies = {}) {
113
+ const selfPid = dependencies.selfPid ?? process.pid;
114
+ if (platform === "linux") {
115
+ return { pollMs: 10, groupState: async (group) => linuxGroupState(group), readable: async () => linuxTableReadable(selfPid) };
116
+ }
117
+ const runPs = dependencies.runPs ?? runDarwinPs;
118
+ const scan = async () => {
119
+ const output = await runPs();
120
+ if (!output || output.exitCode !== 0)
121
+ return undefined;
122
+ const entries = parsePsTable(output.stdout);
123
+ // A sample without the caller cannot be a complete process table.
124
+ return entries?.some((entry) => entry.pid === selfPid) ? entries : undefined;
125
+ };
126
+ return {
127
+ pollMs: 50,
128
+ async groupState(group) {
129
+ const entries = await scan();
130
+ if (!entries)
131
+ return "unknown";
132
+ return entries.some((entry) => entry.group === group && !entry.state.startsWith("Z")) ? "alive" : "gone";
133
+ },
134
+ async readable() { return (await scan()) !== undefined; },
135
+ };
136
+ }
137
+ // Kernel flock and directory fsync semantics are accepted only on the local
138
+ // Linux filesystems covered by the setup test matrix.
139
+ const SUPPORTED_LINUX_FILESYSTEMS = new Set([
140
+ 0xef53, // ext2/3/4
141
+ 0x58465342, // XFS
142
+ 0x9123683e, // Btrfs
143
+ 0x01021994, // tmpfs
144
+ 0x794c7630, // overlayfs
145
+ 0x2fc12fc1, // ZFS
146
+ 0xf2f52010, // F2FS
147
+ 0x24051905, // UBIFS
148
+ 0xca451a4e, // bcachefs
149
+ ]);
150
+ export async function assertSupportedFilesystem(path, platform, readStatfs = (target) => statfs(target)) {
151
+ const filesystem = await readStatfs(path);
152
+ if (platform === "linux") {
153
+ if (!SUPPORTED_LINUX_FILESYSTEMS.has(filesystem.type))
154
+ throw new Error("ownership_filesystem_unsupported");
155
+ return;
156
+ }
157
+ // macOS assigns f_type dynamically, so the directory must share the type of
158
+ // `/`, the sealed APFS system volume on every macOS that Node.js 22 supports.
159
+ if (filesystem.type !== (await readStatfs("/")).type)
160
+ throw new Error("ownership_filesystem_unsupported");
161
+ }
162
+ /** From XNU `bsd/sys/fcntl.h`; Node's `fs.constants` does not export these. */
163
+ export const DARWIN_O_NONBLOCK = 0x4;
164
+ export const DARWIN_O_EXLOCK = 0x20;
165
+ /**
166
+ * Opens the lock file with an exclusive lock on its open file description,
167
+ * which has flock(2) semantics on macOS. A busy lock is retried until the
168
+ * acquisition bound; a failed attempt holds no descriptor.
169
+ */
170
+ export async function openLockedDarwin(path, options) {
171
+ const openFile = options.openFile ?? ((target, flags, mode) => open(target, flags, mode));
172
+ const flags = constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW | DARWIN_O_EXLOCK | DARWIN_O_NONBLOCK;
173
+ const boundMs = Number.isFinite(options.timeoutMs) ? Math.max(1_000, Math.min(options.timeoutMs, 120_000)) : 120_000;
174
+ const deadline = Date.now() + boundMs;
175
+ const retryMs = options.retryMs ?? 50;
176
+ while (true) {
177
+ if (options.signal?.aborted)
178
+ throw new Error("operation_aborted");
179
+ try {
180
+ return await openFile(path, flags, 0o600);
181
+ }
182
+ catch (error) {
183
+ const code = error.code;
184
+ if (code !== "EAGAIN" && code !== "EWOULDBLOCK")
185
+ throw new Error("ownership_lock_unavailable");
186
+ }
187
+ if (Date.now() >= deadline)
188
+ throw new Error("ownership_lock_busy");
189
+ await new Promise((resolve) => {
190
+ const done = () => { clearTimeout(timer); options.signal?.removeEventListener("abort", done); resolve(); };
191
+ const timer = setTimeout(done, retryMs);
192
+ options.signal?.addEventListener("abort", done, { once: true });
193
+ });
194
+ }
195
+ }
@@ -23,8 +23,11 @@ export interface McpLauncherOptions {
23
23
  /** Starts the local MCP server with only the controlled configuration environment. */
24
24
  export declare function createMcpStdioLauncher(options: McpLauncherOptions): (signal: AbortSignal) => Promise<McpStdioProcess>;
25
25
  /**
26
- * Acquires the Linux kernel fence and starts the transaction guardian. The
27
- * guardian inherits the locked file description, so parent death cannot release
28
- * the fence while a client mutation or descendant is still live.
26
+ * Starts the transaction guardian with the kernel fence held. On Linux the
27
+ * trusted util-linux `flock` locks the caller's open file description first;
28
+ * on macOS the caller passes a descriptor from `openLockedDarwin`, which is
29
+ * locked at open time. The guardian inherits the locked file description, so
30
+ * parent death cannot release the fence while a client mutation or
31
+ * descendant is still live.
29
32
  */
30
33
  export declare function acquireSetupGuardian(lockFd: number, timeoutMs: number, signal?: AbortSignal, validateFence?: () => Promise<void>): Promise<SetupGuardian>;