pi-session-mcp 0.0.0-stage → 0.6.2

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 (88) hide show
  1. package/.pi-session-mcp-cli/config.js +88 -0
  2. package/.pi-session-mcp-cli/doctor-cli.js +11 -0
  3. package/.pi-session-mcp-cli/doctor.js +178 -0
  4. package/.pi-session-mcp-cli/execution-profile.js +29 -0
  5. package/.pi-session-mcp-cli/mcp-config.js +70 -0
  6. package/.pi-session-mcp-cli/package-metadata.js +32 -0
  7. package/CHANGELOG.md +62 -0
  8. package/CONTRIBUTING.md +57 -0
  9. package/LICENSE +21 -0
  10. package/README.md +183 -2
  11. package/SECURITY.md +37 -0
  12. package/dist/capabilities.d.ts +25 -0
  13. package/dist/capabilities.js +26 -0
  14. package/dist/client-adapters/claude-code.d.ts +4 -0
  15. package/dist/client-adapters/claude-code.js +33 -0
  16. package/dist/client-adapters/codex.d.ts +4 -0
  17. package/dist/client-adapters/codex.js +73 -0
  18. package/dist/client-adapters/command.d.ts +12 -0
  19. package/dist/client-adapters/command.js +25 -0
  20. package/dist/client-adapters/index.d.ts +4 -0
  21. package/dist/client-adapters/index.js +3 -0
  22. package/dist/client-adapters/mcp-verifier.d.ts +20 -0
  23. package/dist/client-adapters/mcp-verifier.js +174 -0
  24. package/dist/client-adapters/types.d.ts +54 -0
  25. package/dist/client-adapters/types.js +1 -0
  26. package/dist/config.d.ts +50 -0
  27. package/dist/config.js +88 -0
  28. package/dist/doctor-cli.d.ts +1 -0
  29. package/dist/doctor-cli.js +11 -0
  30. package/dist/doctor.d.ts +47 -0
  31. package/dist/doctor.js +178 -0
  32. package/dist/entry-point.d.ts +5 -0
  33. package/dist/entry-point.js +16 -0
  34. package/dist/execution-profile.d.ts +37 -0
  35. package/dist/execution-profile.js +29 -0
  36. package/dist/main.d.ts +2 -0
  37. package/dist/main.js +51 -0
  38. package/dist/mcp-config.d.ts +22 -0
  39. package/dist/mcp-config.js +70 -0
  40. package/dist/mcp-session-tools.d.ts +54 -0
  41. package/dist/mcp-session-tools.js +323 -0
  42. package/dist/mcp-stdio-client.d.ts +3 -0
  43. package/dist/mcp-stdio-client.js +59 -0
  44. package/dist/package-metadata.d.ts +18 -0
  45. package/dist/package-metadata.js +32 -0
  46. package/dist/pi-adapter.d.ts +42 -0
  47. package/dist/pi-adapter.js +22 -0
  48. package/dist/process-diagnostics.d.ts +8 -0
  49. package/dist/process-diagnostics.js +9 -0
  50. package/dist/sdk-pi-adapter.d.ts +75 -0
  51. package/dist/sdk-pi-adapter.js +410 -0
  52. package/dist/server.d.ts +5 -0
  53. package/dist/server.js +102 -0
  54. package/dist/session-registry.d.ts +104 -0
  55. package/dist/session-registry.js +373 -0
  56. package/dist/setup-cli.d.ts +11 -0
  57. package/dist/setup-cli.js +38 -0
  58. package/dist/setup-command-guardian.d.ts +2 -0
  59. package/dist/setup-command-guardian.js +297 -0
  60. package/dist/setup-main.d.ts +8 -0
  61. package/dist/setup-main.js +60 -0
  62. package/dist/setup-ownership.d.ts +53 -0
  63. package/dist/setup-ownership.js +228 -0
  64. package/dist/setup-process.d.ts +30 -0
  65. package/dist/setup-process.js +385 -0
  66. package/dist/setup-result.d.ts +22 -0
  67. package/dist/setup-result.js +8 -0
  68. package/dist/setup.d.ts +55 -0
  69. package/dist/setup.js +327 -0
  70. package/dist/shutdown.d.ts +10 -0
  71. package/dist/shutdown.js +52 -0
  72. package/docs/acceptance.md +227 -0
  73. package/docs/adr/0001-stdio-first.md +12 -0
  74. package/docs/adr/0002-embedded-pi-sdk.md +11 -0
  75. package/docs/adr/0003-post-mvp-maintenance.md +60 -0
  76. package/docs/adr/0004-client-contract-not-version.md +31 -0
  77. package/docs/architecture.md +82 -0
  78. package/docs/client-setup-design.md +90 -0
  79. package/docs/client-setup.md +207 -0
  80. package/docs/doctor-design.md +47 -0
  81. package/docs/installation.md +352 -0
  82. package/docs/releasing.md +216 -0
  83. package/docs/session-mcp.md +260 -0
  84. package/docs/threat-model.md +89 -0
  85. package/docs/tool-contracts.md +143 -0
  86. package/package.json +64 -4
  87. package/pi-session-mcp.example.json +21 -0
  88. package/scripts/doctor.mjs +72 -0
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { readdirSync, readFileSync } from "node:fs";
4
+ const TERM_GRACE_MS = 100;
5
+ const CLEANUP_REPORT_MS = 2_000;
6
+ const RETRY_MS = 25;
7
+ let active;
8
+ let shuttingDown = false;
9
+ // Source-mode signal injection is available only to credential-free tests. The
10
+ // accepted production entry is the bound .js artifact, where this branch is off.
11
+ let forceUnknownForSourceTest = false;
12
+ if (process.argv[1]?.endsWith(".ts")) {
13
+ process.on("SIGUSR1", () => { forceUnknownForSourceTest = true; });
14
+ process.on("SIGUSR2", () => { forceUnknownForSourceTest = false; });
15
+ }
16
+ const parentArgument = Number(process.argv[2]);
17
+ const owningParent = Number.isSafeInteger(parentArgument) && parentArgument > 1 ? parentArgument : process.ppid;
18
+ const parentWatch = setInterval(() => {
19
+ if (process.ppid === owningParent)
20
+ return;
21
+ shuttingDown = true;
22
+ if (active)
23
+ void terminateActive();
24
+ else
25
+ exitWhenSafe();
26
+ }, RETRY_MS);
27
+ function send(message) {
28
+ if (process.connected)
29
+ process.send?.(message, undefined, undefined, () => undefined);
30
+ }
31
+ function exitWhenSafe() {
32
+ clearInterval(parentWatch);
33
+ if (process.connected)
34
+ process.disconnect();
35
+ process.exitCode = 0;
36
+ }
37
+ function parseRequest(value) {
38
+ if (!value || typeof value !== "object")
39
+ return undefined;
40
+ const input = value;
41
+ if (input.type === "shutdown" && Object.keys(input).length === 1)
42
+ return { type: "shutdown" };
43
+ if (input.type === "abort" && Object.keys(input).length === 2 && Number.isSafeInteger(input.id))
44
+ return { type: "abort", id: input.id };
45
+ if (input.type !== "run" || Object.keys(input).sort().join(",") !== "args,command,id,maxOutputBytes,timeoutMs,type")
46
+ return undefined;
47
+ if (!Number.isSafeInteger(input.id) || typeof input.command !== "string" || input.command.length === 0 || input.command.length > 4_096)
48
+ return undefined;
49
+ if (!Array.isArray(input.args) || input.args.length > 128 || !input.args.every((argument) => typeof argument === "string" && argument.length <= 16_384))
50
+ return undefined;
51
+ if (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 120_000)
52
+ return undefined;
53
+ if (!Number.isSafeInteger(input.maxOutputBytes) || input.maxOutputBytes < 1 || input.maxOutputBytes > 1_048_576)
54
+ return undefined;
55
+ return { type: "run", id: input.id, command: input.command, args: input.args, timeoutMs: input.timeoutMs, maxOutputBytes: input.maxOutputBytes };
56
+ }
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 {
100
+ 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";
116
+ }
117
+ function signalGroup(group, signal) {
118
+ try {
119
+ process.kill(-group, signal);
120
+ }
121
+ catch { /* absence is confirmed by the scan */ }
122
+ }
123
+ function bounded(value, maximum) {
124
+ return Buffer.from(value).subarray(0, maximum).toString("utf8");
125
+ }
126
+ function start(request) {
127
+ if (active || shuttingDown) {
128
+ send({ type: "result", id: request.id, exitCode: 126, stdout: "", stderr: "" });
129
+ return;
130
+ }
131
+ let child;
132
+ try {
133
+ child = spawn(request.command, [...request.args], { stdio: ["ignore", "pipe", "pipe", 3], detached: true });
134
+ }
135
+ catch {
136
+ send({ type: "result", id: request.id, exitCode: 127, stdout: "", stderr: "" });
137
+ return;
138
+ }
139
+ if (child.pid === undefined || !child.stdout || !child.stderr) {
140
+ child.once("error", () => undefined);
141
+ send({ type: "result", id: request.id, exitCode: 127, stdout: "", stderr: "" });
142
+ return;
143
+ }
144
+ const execution = { id: request.id, child, group: child.pid, cleanupStarted: false, resultSent: false };
145
+ active = execution;
146
+ let stdout = "";
147
+ let stderr = "";
148
+ let overflow = false;
149
+ const collect = (stream) => (chunk) => {
150
+ const next = (stream === "stdout" ? stdout : stderr) + chunk.toString("utf8");
151
+ if (Buffer.byteLength(next, "utf8") > request.maxOutputBytes) {
152
+ overflow = true;
153
+ return;
154
+ }
155
+ if (stream === "stdout")
156
+ stdout = next;
157
+ else
158
+ stderr = next;
159
+ };
160
+ child.stdout.on("data", collect("stdout"));
161
+ child.stderr.on("data", collect("stderr"));
162
+ const finish = (exitCode) => {
163
+ if (execution.resultSent)
164
+ return;
165
+ execution.resultSent = true;
166
+ if (execution.timeout)
167
+ clearTimeout(execution.timeout);
168
+ send({ type: "result", id: request.id, exitCode, stdout: bounded(stdout, request.maxOutputBytes), stderr: overflow ? "output_limit" : bounded(stderr, request.maxOutputBytes) });
169
+ };
170
+ const cleanup = async (reportedCode) => {
171
+ if (execution.cleanupStarted)
172
+ return;
173
+ execution.cleanupStarted = true;
174
+ signalGroup(execution.group, "SIGTERM");
175
+ await new Promise((resolve) => setTimeout(resolve, TERM_GRACE_MS));
176
+ signalGroup(execution.group, "SIGKILL");
177
+ const reportDeadline = Date.now() + CLEANUP_REPORT_MS;
178
+ while (true) {
179
+ const state = groupState(execution.group);
180
+ if (state === "gone") {
181
+ finish(reportedCode);
182
+ active = undefined;
183
+ if (shuttingDown || !process.connected)
184
+ exitWhenSafe();
185
+ return;
186
+ }
187
+ if (!execution.resultSent && Date.now() >= reportDeadline)
188
+ finish(126);
189
+ signalGroup(execution.group, "SIGKILL");
190
+ await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
191
+ }
192
+ };
193
+ execution.timeout = setTimeout(() => { void cleanup(124); }, request.timeoutMs);
194
+ child.once("error", () => {
195
+ if (execution.cleanupStarted)
196
+ return;
197
+ finish(127);
198
+ active = undefined;
199
+ if (shuttingDown || !process.connected)
200
+ exitWhenSafe();
201
+ });
202
+ child.once("close", (code) => {
203
+ if (execution.cleanupStarted)
204
+ 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);
214
+ });
215
+ }
216
+ process.on("message", (value) => {
217
+ const request = parseRequest(value);
218
+ if (!request) {
219
+ shuttingDown = true;
220
+ if (active)
221
+ void terminateActive();
222
+ else {
223
+ if (process.connected)
224
+ process.disconnect();
225
+ process.exitCode = 64;
226
+ }
227
+ return;
228
+ }
229
+ if (request.type === "run") {
230
+ start(request);
231
+ return;
232
+ }
233
+ if (request.type === "abort") {
234
+ if (active?.id === request.id)
235
+ void terminateActive();
236
+ return;
237
+ }
238
+ shuttingDown = true;
239
+ if (active)
240
+ void terminateActive();
241
+ else
242
+ exitWhenSafe();
243
+ });
244
+ async function terminateActive() {
245
+ const execution = active;
246
+ if (!execution || execution.cleanupStarted)
247
+ return;
248
+ execution.cleanupStarted = true;
249
+ if (execution.timeout)
250
+ clearTimeout(execution.timeout);
251
+ signalGroup(execution.group, "SIGTERM");
252
+ await new Promise((resolve) => setTimeout(resolve, TERM_GRACE_MS));
253
+ signalGroup(execution.group, "SIGKILL");
254
+ const reportDeadline = Date.now() + CLEANUP_REPORT_MS;
255
+ while (true) {
256
+ const state = groupState(execution.group);
257
+ if (state === "gone") {
258
+ if (!execution.resultSent) {
259
+ execution.resultSent = true;
260
+ send({ type: "result", id: execution.id, exitCode: 125, stdout: "", stderr: "" });
261
+ }
262
+ active = undefined;
263
+ if (shuttingDown || !process.connected)
264
+ exitWhenSafe();
265
+ return;
266
+ }
267
+ if (!execution.resultSent && Date.now() >= reportDeadline) {
268
+ execution.resultSent = true;
269
+ send({ type: "result", id: execution.id, exitCode: 126, stdout: "", stderr: "" });
270
+ }
271
+ signalGroup(execution.group, "SIGKILL");
272
+ await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
273
+ }
274
+ }
275
+ process.on("disconnect", () => {
276
+ shuttingDown = true;
277
+ if (active)
278
+ void terminateActive();
279
+ else
280
+ exitWhenSafe();
281
+ });
282
+ process.once("SIGINT", () => { shuttingDown = true; if (active)
283
+ void terminateActive();
284
+ else
285
+ exitWhenSafe(); });
286
+ process.once("SIGTERM", () => { shuttingDown = true; if (active)
287
+ void terminateActive();
288
+ else
289
+ exitWhenSafe(); });
290
+ if (processTableReadable())
291
+ send({ type: "ready" });
292
+ else {
293
+ clearInterval(parentWatch);
294
+ if (process.connected)
295
+ process.disconnect();
296
+ process.exitCode = 69;
297
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { createProductionSetup } from "./setup.js";
3
+ /** Executable entry point: `node dist/setup-main.js [operation] --target client:scope:alias`. */
4
+ export interface SetupMainDependencies {
5
+ readonly createSetup?: typeof createProductionSetup;
6
+ readonly packageRoot?: string;
7
+ }
8
+ export declare function main(argv?: readonly string[], dependencies?: SetupMainDependencies): Promise<number>;
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { isEntryPoint } from "./entry-point.js";
5
+ import { createProcessRunner } from "./setup-process.js";
6
+ import { createProductionSetup, isSupportedTarget } from "./setup.js";
7
+ import { formatSetupHuman, formatSetupResult, parseSetupArgs } from "./setup-cli.js";
8
+ import { result } from "./setup-result.js";
9
+ export async function main(argv = process.argv.slice(2), dependencies = {}) {
10
+ let json = argv.includes("--json");
11
+ let request;
12
+ try {
13
+ request = parseSetupArgs(argv);
14
+ json = request.json;
15
+ }
16
+ catch {
17
+ return write(cliFailure("dry-run", [], "usage_invalid", 64), json);
18
+ }
19
+ const operation = request.operation ?? "dry-run";
20
+ if (process.platform !== "linux")
21
+ return write(cliFailure(operation, request.targets, "platform_unsupported", 1), request.json);
22
+ const config = process.env.PI_SESSION_MCP_CONFIG;
23
+ if (!config)
24
+ return write(cliFailure(operation, request.targets, "config_required", 1), request.json);
25
+ const root = dependencies.packageRoot ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
26
+ const controller = new AbortController();
27
+ let interrupted;
28
+ const onInterrupt = () => { interrupted ??= 130; controller.abort(); };
29
+ const onTerminate = () => { interrupted ??= 143; controller.abort(); };
30
+ process.on("SIGINT", onInterrupt);
31
+ process.on("SIGTERM", onTerminate);
32
+ let setup;
33
+ try {
34
+ try {
35
+ setup = await (dependencies.createSetup ?? createProductionSetup)(createProcessRunner({ signal: controller.signal }), root, resolve(config), undefined, controller.signal);
36
+ }
37
+ catch {
38
+ return write(cliFailure(operation, request.targets, interrupted ? "operation_interrupted" : "release_unavailable", interrupted ?? 1), request.json);
39
+ }
40
+ try {
41
+ const output = await setup.run(request);
42
+ return write(interrupted ? cliFailure(operation, request.targets, "operation_interrupted", interrupted) : output, request.json);
43
+ }
44
+ catch {
45
+ return write(cliFailure(operation, request.targets, interrupted ? "operation_interrupted" : "operation_failed", interrupted ?? 1), request.json);
46
+ }
47
+ }
48
+ finally {
49
+ process.removeListener("SIGINT", onInterrupt);
50
+ process.removeListener("SIGTERM", onTerminate);
51
+ }
52
+ }
53
+ function cliFailure(operation, targets, code, exitCode) {
54
+ const safeTargets = targets.filter(isSupportedTarget);
55
+ const reported = safeTargets.length === targets.length && safeTargets.length > 0 ? safeTargets : [{ client: "invalid", scope: "invalid", alias: "invalid" }];
56
+ return result(operation, reported.map((target) => ({ target, status: "failed", code })), "failed", exitCode);
57
+ }
58
+ function write(value, json) { process.stdout.write((json ? formatSetupResult(value) : formatSetupHuman(value)) + "\n"); return value.exitCode; }
59
+ if (isEntryPoint(import.meta.url, process.argv[1]))
60
+ void main().then((code) => { process.exitCode = code; });
@@ -0,0 +1,53 @@
1
+ import type { CommandRunner } from "./client-adapters/types.js";
2
+ import type { SetupTarget } from "./setup-result.js";
3
+ export interface OwnershipRecord {
4
+ readonly target: SetupTarget;
5
+ readonly fingerprint: string;
6
+ readonly previous: "absent" | "equivalent";
7
+ readonly phase: "pending" | "owned";
8
+ readonly transactionId: string;
9
+ }
10
+ export interface RegistrationSnapshot {
11
+ /** Opaque adapter-owned snapshot; implementations must not persist secrets. */
12
+ readonly fingerprint: string;
13
+ readonly payload: unknown;
14
+ }
15
+ export type OwnedRegistration = OwnershipRecord;
16
+ export interface OwnershipAccess {
17
+ get(target: SetupTarget): Promise<OwnershipRecord | undefined>;
18
+ put(registration: OwnershipRecord): Promise<void>;
19
+ delete(target: SetupTarget): Promise<void>;
20
+ /** Throws a stable, path-free error if the transaction fence is unhealthy. */
21
+ assertHealthy?(): void;
22
+ signal?(): AbortSignal;
23
+ readonly mutationRunner?: CommandRunner;
24
+ }
25
+ export interface OwnershipStore extends OwnershipAccess {
26
+ transaction<T>(operation: (access: OwnershipAccess) => Promise<T>): Promise<T>;
27
+ }
28
+ /** Safe default for one process; callers needing recovery inject a durable store. */
29
+ export declare class MemoryOwnershipStore implements OwnershipStore {
30
+ private readonly entries;
31
+ private transactionTail;
32
+ get(target: SetupTarget): Promise<OwnershipRecord | undefined>;
33
+ put(registration: OwnershipRecord): Promise<void>;
34
+ delete(target: SetupTarget): Promise<void>;
35
+ transaction<T>(operation: (access: OwnershipAccess) => Promise<T>): Promise<T>;
36
+ }
37
+ export declare class DurableOwnershipStore implements OwnershipStore {
38
+ private readonly path;
39
+ private readonly options;
40
+ constructor(path: string, options?: {
41
+ readonly acquireTimeoutMs?: number;
42
+ readonly afterLockOpen?: () => Promise<void> | void;
43
+ readonly beforeRename?: () => Promise<void> | void;
44
+ readonly signal?: AbortSignal;
45
+ });
46
+ private read;
47
+ get(target: SetupTarget): Promise<OwnershipRecord | undefined>;
48
+ put(registration: OwnershipRecord): Promise<void>;
49
+ delete(target: SetupTarget): Promise<void>;
50
+ transaction<T>(operation: (access: OwnershipAccess) => Promise<T>): Promise<T>;
51
+ private withLock;
52
+ private write;
53
+ }
@@ -0,0 +1,228 @@
1
+ import { constants } from "node:fs";
2
+ import { chmod, lstat, mkdir, open, readFile, rename, rm, stat, statfs } from "node:fs/promises";
3
+ import { randomUUID } from "node:crypto";
4
+ import { dirname } from "node:path";
5
+ import { acquireSetupGuardian } from "./setup-process.js";
6
+ /** Safe default for one process; callers needing recovery inject a durable store. */
7
+ export class MemoryOwnershipStore {
8
+ entries = new Map();
9
+ transactionTail = Promise.resolve();
10
+ async get(target) { return this.entries.get(key(target)); }
11
+ async put(registration) { this.entries.set(key(registration.target), registration); }
12
+ async delete(target) { this.entries.delete(key(target)); }
13
+ async transaction(operation) {
14
+ const previous = this.transactionTail;
15
+ let release;
16
+ this.transactionTail = new Promise((resolve) => { release = resolve; });
17
+ await previous;
18
+ try {
19
+ return await operation(this);
20
+ }
21
+ finally {
22
+ release();
23
+ }
24
+ }
25
+ }
26
+ export class DurableOwnershipStore {
27
+ path;
28
+ options;
29
+ constructor(path, options = {}) {
30
+ this.path = path;
31
+ this.options = options;
32
+ }
33
+ async read() { let parsed; try {
34
+ if ((await stat(this.path)).size > 65_536)
35
+ throw new Error("ownership_too_large");
36
+ parsed = JSON.parse(await readFile(this.path, "utf8"));
37
+ }
38
+ catch (error) {
39
+ if (error.code === "ENOENT")
40
+ return [];
41
+ throw new Error("ownership_unreadable");
42
+ } if (!Array.isArray(parsed) || parsed.length > 128 || !parsed.every(isRecord))
43
+ throw new Error("ownership_corrupt"); return parsed; }
44
+ async get(target) { return (await this.read()).find((entry) => key(entry.target) === key(target)); }
45
+ async put(registration) { await this.transaction((access) => access.put(registration)); }
46
+ async delete(target) { await this.transaction((access) => access.delete(target)); }
47
+ async transaction(operation) {
48
+ return this.withLock(operation);
49
+ }
50
+ async withLock(operation) {
51
+ if (process.platform !== "linux")
52
+ throw new Error("platform_unsupported");
53
+ const directory = dirname(this.path);
54
+ await ensureOwnershipDirectory(directory);
55
+ const controller = new AbortController();
56
+ const abort = () => controller.abort();
57
+ this.options.signal?.addEventListener("abort", abort, { once: true });
58
+ if (this.options.signal?.aborted)
59
+ controller.abort();
60
+ const assertHealthy = () => {
61
+ if (controller.signal.aborted)
62
+ throw new Error("operation_aborted");
63
+ };
64
+ const lockPath = `${this.path}.flock`;
65
+ let lockHandle;
66
+ try {
67
+ lockHandle = await open(lockPath, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
68
+ await lockHandle.chmod(0o600);
69
+ await lockHandle.sync();
70
+ await syncDirectory(directory);
71
+ const [descriptor, linked] = await Promise.all([lockHandle.stat(), stat(lockPath)]);
72
+ if (!descriptor.isFile() || !linked.isFile() || descriptor.dev !== linked.dev || descriptor.ino !== linked.ino)
73
+ throw new Error("ownership_lock_invalid");
74
+ await this.options.afterLockOpen?.();
75
+ }
76
+ catch {
77
+ this.options.signal?.removeEventListener("abort", abort);
78
+ await lockHandle?.close().catch(() => undefined);
79
+ throw new Error("ownership_lock_unavailable");
80
+ }
81
+ const validateLock = async () => {
82
+ const [descriptor, linked] = await Promise.all([lockHandle.stat(), stat(lockPath)]);
83
+ if (!descriptor.isFile() || !linked.isFile() || descriptor.dev !== linked.dev || descriptor.ino !== linked.ino)
84
+ throw new Error("ownership_lock_invalid");
85
+ };
86
+ let guardian;
87
+ try {
88
+ guardian = await acquireSetupGuardian(lockHandle.fd, this.options.acquireTimeoutMs ?? 30_000, controller.signal, validateLock);
89
+ }
90
+ catch (error) {
91
+ this.options.signal?.removeEventListener("abort", abort);
92
+ await lockHandle.close().catch(() => undefined);
93
+ const code = error.message;
94
+ throw new Error(["ownership_lock_busy", "operation_aborted"].includes(code) ? code : "ownership_lock_unavailable");
95
+ }
96
+ try {
97
+ await validateLock();
98
+ }
99
+ catch {
100
+ this.options.signal?.removeEventListener("abort", abort);
101
+ try {
102
+ await guardian.release();
103
+ }
104
+ finally {
105
+ await lockHandle.close().catch(() => undefined);
106
+ }
107
+ throw new Error("ownership_lock_unavailable");
108
+ }
109
+ const compromise = () => controller.abort();
110
+ guardian.signal.addEventListener("abort", compromise, { once: true });
111
+ try {
112
+ const access = {
113
+ get: async (target) => { assertHealthy(); const value = await this.read(); assertHealthy(); return value.find((entry) => key(entry.target) === key(target)); },
114
+ put: async (registration) => { assertHealthy(); if (!isRecord(registration))
115
+ throw new Error("ownership_invalid"); const entries = (await this.read()).filter((entry) => key(entry.target) !== key(registration.target)); entries.push(registration); assertHealthy(); await this.write(entries, assertHealthy); assertHealthy(); },
116
+ delete: async (target) => { assertHealthy(); const entries = (await this.read()).filter((entry) => key(entry.target) !== key(target)); assertHealthy(); await this.write(entries, assertHealthy); assertHealthy(); },
117
+ assertHealthy,
118
+ signal: () => controller.signal,
119
+ mutationRunner: guardian.runner,
120
+ };
121
+ const value = await operation(access);
122
+ assertHealthy();
123
+ return value;
124
+ }
125
+ finally {
126
+ guardian.signal.removeEventListener("abort", compromise);
127
+ this.options.signal?.removeEventListener("abort", abort);
128
+ try {
129
+ await guardian.release();
130
+ }
131
+ finally {
132
+ await lockHandle.close().catch(() => undefined);
133
+ }
134
+ }
135
+ }
136
+ async write(entries, assertHealthy) { const directory = dirname(this.path); const temporary = `${this.path}.tmp-${process.pid}-${randomUUID()}`; let renamed = false; try {
137
+ const handle = await open(temporary, "wx", 0o600);
138
+ try {
139
+ await handle.writeFile(JSON.stringify(entries));
140
+ await handle.sync();
141
+ }
142
+ finally {
143
+ await handle.close();
144
+ }
145
+ await this.options.beforeRename?.();
146
+ assertHealthy();
147
+ await rename(temporary, this.path);
148
+ renamed = true;
149
+ await syncDirectory(directory);
150
+ }
151
+ finally {
152
+ if (!renamed)
153
+ await rm(temporary, { force: true }).catch(() => undefined);
154
+ } }
155
+ }
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) {
170
+ const missing = [];
171
+ let cursor = directory;
172
+ while (true) {
173
+ try {
174
+ const existing = await lstat(cursor);
175
+ if (!existing.isDirectory() || existing.isSymbolicLink())
176
+ throw new Error("ownership_directory_invalid");
177
+ break;
178
+ }
179
+ catch (error) {
180
+ if (error.code !== "ENOENT")
181
+ throw new Error("ownership_directory_unavailable");
182
+ missing.push(cursor);
183
+ const parent = dirname(cursor);
184
+ if (parent === cursor)
185
+ throw new Error("ownership_directory_unavailable");
186
+ cursor = parent;
187
+ }
188
+ }
189
+ await assertSupportedFilesystem(cursor);
190
+ for (const candidate of missing.reverse()) {
191
+ try {
192
+ await mkdir(candidate, { mode: 0o700 });
193
+ }
194
+ catch (error) {
195
+ if (error.code !== "EEXIST")
196
+ throw new Error("ownership_directory_unavailable");
197
+ }
198
+ const created = await lstat(candidate);
199
+ if (!created.isDirectory() || created.isSymbolicLink())
200
+ throw new Error("ownership_directory_unavailable");
201
+ await chmod(candidate, 0o700);
202
+ await syncDirectory(candidate);
203
+ await syncDirectory(dirname(candidate));
204
+ }
205
+ 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");
212
+ }
213
+ async function syncDirectory(path) {
214
+ const handle = await open(path, "r");
215
+ try {
216
+ await handle.sync();
217
+ }
218
+ finally {
219
+ await handle.close();
220
+ }
221
+ }
222
+ function isRecord(value) { if (!value || typeof value !== "object")
223
+ return false; const x = value; if (!exactKeys(x, ["target", "fingerprint", "previous", "phase", "transactionId"]))
224
+ return false; const t = x.target; if (!t || typeof t !== "object")
225
+ return false; const target = t; if (!exactKeys(target, ["client", "scope", "alias"]))
226
+ return false; const client = target.client; const scope = target.scope; return (client === "codex" || client === "claude-code") && typeof scope === "string" && (client === "codex" ? scope === "user" : ["user", "project", "local"].includes(scope)) && target.alias === "pi-session-mcp" && typeof x.fingerprint === "string" && /^[a-f0-9]{64}$/.test(x.fingerprint) && (x.phase === "pending" || x.phase === "owned") && x.previous === "absent" && typeof x.transactionId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(x.transactionId); }
227
+ function exactKeys(value, expected) { const keys = Object.keys(value).sort(); return keys.length === expected.length && [...expected].sort().every((key, index) => keys[index] === key); }
228
+ const key = (target) => `${target.client}\u0000${target.scope}\u0000${target.alias}`;
@@ -0,0 +1,30 @@
1
+ import type { CommandRunner } from "./client-adapters/types.js";
2
+ import type { McpStdioProcess } from "./client-adapters/mcp-verifier.js";
3
+ export interface ProcessRunnerOptions {
4
+ readonly timeoutMs?: number;
5
+ readonly maxOutputBytes?: number;
6
+ readonly env?: NodeJS.ProcessEnv;
7
+ readonly signal?: AbortSignal;
8
+ }
9
+ export interface SetupGuardian {
10
+ readonly runner: CommandRunner;
11
+ readonly signal: AbortSignal;
12
+ release(): Promise<void>;
13
+ }
14
+ /** Runs only an explicit executable/argv pair, with bounded output and termination. */
15
+ export declare function createProcessRunner(options?: ProcessRunnerOptions): CommandRunner;
16
+ export interface McpLauncherOptions {
17
+ readonly nodePath: string;
18
+ readonly entryPath: string;
19
+ readonly configPath: string;
20
+ readonly timeoutMs?: number;
21
+ readonly maxFrameBytes?: number;
22
+ }
23
+ /** Starts the local MCP server with only the controlled configuration environment. */
24
+ export declare function createMcpStdioLauncher(options: McpLauncherOptions): (signal: AbortSignal) => Promise<McpStdioProcess>;
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.
29
+ */
30
+ export declare function acquireSetupGuardian(lockFd: number, timeoutMs: number, signal?: AbortSignal, validateFence?: () => Promise<void>): Promise<SetupGuardian>;