pi-session-mcp 0.11.9 → 0.12.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.
- package/.pi-session-mcp-cli/client-adapters/mcp-verifier.js +6 -2
- package/.pi-session-mcp-cli/doctor.js +89 -4
- package/.pi-session-mcp-cli/execution-modes.js +19 -5
- package/.pi-session-mcp-cli/override-lock.js +4 -4
- package/.pi-session-mcp-cli/setup-process.js +5 -4
- package/.pi-session-mcp-cli/writer-boundary.js +10 -4
- package/.pi-session-mcp-cli/writer-fence.js +50 -0
- package/.pi-session-mcp-cli/writer-lease.js +140 -0
- package/.pi-session-mcp-cli/writer-lifecycle.js +55 -23
- package/.pi-session-mcp-cli/writer-state.js +4 -2
- package/CHANGELOG.md +91 -0
- package/README.md +3 -2
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.js +4 -1
- package/dist/child-stdio-transport.d.ts +21 -0
- package/dist/child-stdio-transport.js +88 -0
- package/dist/client-adapters/mcp-verifier.js +6 -2
- package/dist/client-adapters/types.d.ts +1 -0
- package/dist/doctor.d.ts +3 -1
- package/dist/doctor.js +89 -4
- package/dist/execution-modes.d.ts +14 -2
- package/dist/execution-modes.js +19 -5
- package/dist/main.js +9 -2
- package/dist/override-lock.d.ts +2 -0
- package/dist/override-lock.js +4 -4
- package/dist/pi-adapter.d.ts +30 -0
- package/dist/pi-adapter.js +11 -0
- package/dist/refusal-server.d.ts +1 -1
- package/dist/refusal-server.js +24 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +18 -53
- package/dist/session-registry.d.ts +9 -2
- package/dist/session-registry.js +17 -12
- package/dist/setup-cli.js +7 -2
- package/dist/setup-hooks.js +0 -2
- package/dist/setup-modes.js +7 -1
- package/dist/setup-process.d.ts +3 -1
- package/dist/setup-process.js +5 -4
- package/dist/setup-writers.d.ts +5 -0
- package/dist/setup-writers.js +203 -94
- package/dist/tool-schemas.d.ts +339 -0
- package/dist/tool-schemas.js +49 -0
- package/dist/wake-hook.d.ts +4 -5
- package/dist/wake-hook.js +3 -6
- package/dist/writer-acceptance.js +10 -1
- package/dist/writer-boundary.d.ts +6 -4
- package/dist/writer-boundary.js +10 -4
- package/dist/writer-fence.d.ts +12 -0
- package/dist/writer-fence.js +50 -0
- package/dist/writer-launch.d.ts +9 -1
- package/dist/writer-launch.js +127 -30
- package/dist/writer-lease.d.ts +28 -0
- package/dist/writer-lease.js +140 -0
- package/dist/writer-lifecycle.d.ts +7 -1
- package/dist/writer-lifecycle.js +55 -23
- package/dist/writer-relay.d.ts +33 -0
- package/dist/writer-relay.js +359 -0
- package/dist/writer-state.d.ts +13 -1
- package/dist/writer-state.js +4 -2
- package/docs/acceptance.md +172 -0
- package/docs/adr/0012-leased-writer-pool.md +357 -0
- package/docs/architecture.md +61 -13
- package/docs/client-setup.md +11 -15
- package/docs/doctor-design.md +10 -4
- package/docs/execution-modes.md +87 -44
- package/docs/installation.md +43 -22
- package/docs/isolation-setup.md +88 -102
- package/docs/roadmap.md +6 -0
- package/docs/threat-model.md +7 -4
- package/docs/tool-contracts.md +29 -12
- package/package.json +1 -1
- package/scripts/isolation-acceptance.mjs +7 -1
- package/skills/pi-session-implementation/SKILL.md +1 -0
- package/skills/pi-session-orchestration/SKILL.md +6 -5
- package/skills/pi-session-orchestration/references/start-errors.md +5 -1
package/dist/session-registry.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { PiSessionCreationError } from "./pi-adapter.js";
|
|
3
3
|
const REGISTRY_ERROR_MESSAGES = {
|
|
4
|
+
writer_session_lost: "Writer session was lost",
|
|
5
|
+
base_not_applicable: "Base is only applicable to coding sessions on container workspaces",
|
|
4
6
|
unknown_workspace: "Unknown workspace",
|
|
5
7
|
writer_uncollected: "Writer is not at its collected baseline",
|
|
6
8
|
writer_busy: "Writer already has an open coding session",
|
|
@@ -58,7 +60,7 @@ export class SessionRegistry {
|
|
|
58
60
|
this.onTurnTerminal = options.onTurnTerminal;
|
|
59
61
|
this.options = { preflightDeadlineMs: options.preflightDeadlineMs ?? PREFLIGHT_DEADLINE_MS, abortDeadlineMs: options.abortDeadlineMs ?? ABORT_DEADLINE_MS, closeCleanupDeadlineMs: options.closeCleanupDeadlineMs ?? CLOSE_CLEANUP_DEADLINE_MS, shutdownDeadlineMs: options.shutdownDeadlineMs ?? SHUTDOWN_DEADLINE_MS };
|
|
60
62
|
}
|
|
61
|
-
async start(workspace, executionProfile) {
|
|
63
|
+
async start(workspace, executionProfile, options = {}) {
|
|
62
64
|
if (this.stopping)
|
|
63
65
|
throw new RegistryError("server_stopping");
|
|
64
66
|
const cwd = this.workspaces.get(workspace);
|
|
@@ -66,8 +68,8 @@ export class SessionRegistry {
|
|
|
66
68
|
throw new RegistryError("unknown_workspace");
|
|
67
69
|
// Track registration and orphan cleanup too, not just the raw adapter promise.
|
|
68
70
|
const start = this.writerGuard && executionProfile.permissionProfile === "coding"
|
|
69
|
-
? this.guardedCreate(cwd, workspace, executionProfile)
|
|
70
|
-
: this.createAndRegister(cwd, workspace, executionProfile);
|
|
71
|
+
? this.guardedCreate(cwd, workspace, executionProfile, options)
|
|
72
|
+
: this.createAndRegister(cwd, workspace, executionProfile, options);
|
|
71
73
|
this.pendingStarts.add(start);
|
|
72
74
|
try {
|
|
73
75
|
return await start;
|
|
@@ -76,7 +78,7 @@ export class SessionRegistry {
|
|
|
76
78
|
this.pendingStarts.delete(start);
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
|
-
async guardedCreate(cwd, workspace, executionProfile) {
|
|
81
|
+
async guardedCreate(cwd, workspace, executionProfile, options) {
|
|
80
82
|
const previous = this.codingAdmissions.get(workspace) ?? Promise.resolve();
|
|
81
83
|
let release;
|
|
82
84
|
const admission = new Promise((resolve) => { release = resolve; });
|
|
@@ -89,7 +91,7 @@ export class SessionRegistry {
|
|
|
89
91
|
await this.writerGuard.check(workspace, open);
|
|
90
92
|
if (this.stopping)
|
|
91
93
|
throw new RegistryError("server_stopping");
|
|
92
|
-
return await this.createAndRegister(cwd, workspace, executionProfile);
|
|
94
|
+
return await this.createAndRegister(cwd, workspace, executionProfile, options);
|
|
93
95
|
}
|
|
94
96
|
finally {
|
|
95
97
|
release();
|
|
@@ -97,8 +99,8 @@ export class SessionRegistry {
|
|
|
97
99
|
this.codingAdmissions.delete(workspace);
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
|
-
async createAndRegister(cwd, workspace, executionProfile) {
|
|
101
|
-
const handle = await Promise.resolve().then(() => this.adapter.create({ cwd, executionProfile, signal: this.shutdownController.signal }));
|
|
102
|
+
async createAndRegister(cwd, workspace, executionProfile, options) {
|
|
103
|
+
const handle = await Promise.resolve().then(() => this.adapter.create({ cwd, workspace, ...(options.base === undefined ? {} : { base: options.base }), executionProfile, signal: this.shutdownController.signal }));
|
|
102
104
|
try {
|
|
103
105
|
if (this.stopping)
|
|
104
106
|
throw new RegistryError("server_stopping");
|
|
@@ -108,6 +110,7 @@ export class SessionRegistry {
|
|
|
108
110
|
id: randomUUID(), workspace, executionProfile: executionProfile.alias, profile: executionProfile.permissionProfile,
|
|
109
111
|
provider: handle.appliedSelection.provider, model: handle.appliedSelection.model,
|
|
110
112
|
thinkingLevel: handle.appliedSelection.thinkingLevel, state: "idle", createdAt: time, updatedAt: time,
|
|
113
|
+
...(handle.placement ? { writer: handle.placement.writer, base: { ...handle.placement.base } } : {}),
|
|
111
114
|
handle, operation: Promise.resolve(), disposed: false, turns: new Map(), order: [],
|
|
112
115
|
};
|
|
113
116
|
this.sessions.set(entry.id, entry);
|
|
@@ -130,6 +133,8 @@ export class SessionRegistry {
|
|
|
130
133
|
async prompt(id, text) {
|
|
131
134
|
const entry = this.require(id);
|
|
132
135
|
return this.serial(entry, async () => {
|
|
136
|
+
if (entry.handle.isLost?.())
|
|
137
|
+
throw new RegistryError("writer_session_lost");
|
|
133
138
|
if (entry.state === "running" || entry.abortPromise)
|
|
134
139
|
throw new RegistryError("session_running");
|
|
135
140
|
if (entry.state === "closing" || entry.state === "closed")
|
|
@@ -189,7 +194,7 @@ export class SessionRegistry {
|
|
|
189
194
|
const result = await this.withTimeout(Promise.race([
|
|
190
195
|
preflight.then((value) => ({ kind: "preflight", value })),
|
|
191
196
|
operation.then(() => ({ kind: "operation" }), () => ({ kind: "operation" })),
|
|
192
|
-
]), this.options.preflightDeadlineMs, { kind: "timeout" });
|
|
197
|
+
]), entry.handle.deadlines?.preflightMs ?? this.options.preflightDeadlineMs, { kind: "timeout" });
|
|
193
198
|
if (result.kind === "timeout") {
|
|
194
199
|
this.quarantineTimedOutPreflight(entry, turn, operation, previous);
|
|
195
200
|
throw new RegistryError("prompt_timeout");
|
|
@@ -224,7 +229,7 @@ export class SessionRegistry {
|
|
|
224
229
|
}
|
|
225
230
|
return this.view(entry);
|
|
226
231
|
});
|
|
227
|
-
const result = await this.withTimeout(operation.then((session) => ({ kind: "completed", session }), () => ({ kind: "failed" })), this.options.abortDeadlineMs, { kind: "timeout" });
|
|
232
|
+
const result = await this.withTimeout(operation.then((session) => ({ kind: "completed", session }), () => ({ kind: "failed" })), entry.handle.deadlines?.abortMs ?? this.options.abortDeadlineMs, { kind: "timeout" });
|
|
228
233
|
if (result.kind === "timeout")
|
|
229
234
|
throw new RegistryError("abort_timeout");
|
|
230
235
|
if (result.kind === "failed")
|
|
@@ -253,7 +258,7 @@ export class SessionRegistry {
|
|
|
253
258
|
turn.completedAt = time;
|
|
254
259
|
this.notifyTerminal(existing, turn, "close");
|
|
255
260
|
const abortPromise = this.beginAbort(existing, turn);
|
|
256
|
-
await this.withTimeout(abortPromise.then(() => undefined, () => undefined), this.options.closeCleanupDeadlineMs, undefined);
|
|
261
|
+
await this.withTimeout(abortPromise.then(() => undefined, () => undefined), existing.handle.deadlines?.cleanupMs ?? this.options.closeCleanupDeadlineMs, undefined);
|
|
257
262
|
}
|
|
258
263
|
existing.turn = undefined;
|
|
259
264
|
existing.turnPromise = undefined;
|
|
@@ -289,7 +294,7 @@ export class SessionRegistry {
|
|
|
289
294
|
const normalized = normalize(outcome.assistantText);
|
|
290
295
|
turn.state = "completed";
|
|
291
296
|
turn.assistantText = normalized.text;
|
|
292
|
-
turn.truncated = normalized.truncated;
|
|
297
|
+
turn.truncated = normalized.truncated || outcome.truncated === true;
|
|
293
298
|
}
|
|
294
299
|
else if (outcome.status === "aborted")
|
|
295
300
|
turn.state = "aborted";
|
|
@@ -394,7 +399,7 @@ export class SessionRegistry {
|
|
|
394
399
|
async disposeHandle(handle) {
|
|
395
400
|
try {
|
|
396
401
|
const cleanup = Promise.resolve(handle.dispose()).then(() => undefined, () => undefined);
|
|
397
|
-
await this.withTimeout(cleanup, this.options.closeCleanupDeadlineMs, undefined);
|
|
402
|
+
await this.withTimeout(cleanup, handle.deadlines?.cleanupMs ?? this.options.closeCleanupDeadlineMs, undefined);
|
|
398
403
|
}
|
|
399
404
|
catch { /* Cleanup errors must never expose adapter internals. */ }
|
|
400
405
|
}
|
package/dist/setup-cli.js
CHANGED
|
@@ -81,7 +81,7 @@ export function parseSetupArgs(argv) {
|
|
|
81
81
|
customizeSkill = value;
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
84
|
-
if (["--repo", "--mode", "--accept-host-writes", "--writers", "--lifecycle", "--profiles", "--image", "--network", "--cache"].includes(argument)) {
|
|
84
|
+
if (["--repo", "--mode", "--accept-host-writes", "--writers", "--lifecycle", "--profiles", "--image", "--network", "--cache", "--tags"].includes(argument)) {
|
|
85
85
|
if (modeFlags.has(argument))
|
|
86
86
|
throw new Error("repo_argument_repeated");
|
|
87
87
|
modeFlags.add(argument);
|
|
@@ -124,8 +124,13 @@ export function parseSetupArgs(argv) {
|
|
|
124
124
|
throw new Error("network_invalid");
|
|
125
125
|
container.network = value;
|
|
126
126
|
break;
|
|
127
|
+
case "--tags":
|
|
128
|
+
if (value !== "none" && value !== "reachable")
|
|
129
|
+
throw new Error("tags_invalid");
|
|
130
|
+
container.tags = value;
|
|
131
|
+
break;
|
|
127
132
|
case "--cache":
|
|
128
|
-
if (value !== "none" && value !== "go")
|
|
133
|
+
if (value !== "none" && value !== "go" && value !== "uv")
|
|
129
134
|
throw new Error("cache_invalid");
|
|
130
135
|
container.cache = value;
|
|
131
136
|
break;
|
package/dist/setup-hooks.js
CHANGED
|
@@ -33,8 +33,6 @@ export const WAKE_TIMEOUT_MARGIN_SECONDS = 60;
|
|
|
33
33
|
export const WAKE_MATCHER = [
|
|
34
34
|
"mcp__pi-session-mcp__pi_session_prompt",
|
|
35
35
|
"mcp__pi-session-mcp__pi_turn_get",
|
|
36
|
-
"mcp__pi-coding-.*__pi_session_prompt",
|
|
37
|
-
"mcp__pi-coding-.*__pi_turn_get",
|
|
38
36
|
].join("|");
|
|
39
37
|
/** A command referencing this program path marks an entry as pi-session-mcp's. */
|
|
40
38
|
const WAKE_PROGRAM = /\/dist\/wake-main\.js(?=$|[\s"'])/;
|
package/dist/setup-modes.js
CHANGED
|
@@ -60,7 +60,13 @@ export async function changeExecutionMode(configPath, request, dependencies = {}
|
|
|
60
60
|
return done("failed", "container_profiles_required");
|
|
61
61
|
if (request.mode === "container" && !await runtimeUsable())
|
|
62
62
|
return done("failed", "container_runtime_unavailable");
|
|
63
|
-
const entry = mode === "host" ? { mode, acceptHostWrites: true } : mode === "container" ?
|
|
63
|
+
const entry = mode === "host" ? { mode, acceptHostWrites: true } : mode === "container" ? (() => {
|
|
64
|
+
const existing = current.workspaces[request.alias];
|
|
65
|
+
const previousBase = existing?.mode === "container" ? existing.base : undefined;
|
|
66
|
+
const requestedBase = request.baseRef?.replace(/^refs\/heads\//, "");
|
|
67
|
+
const base = requestedBase ?? previousBase ?? request.container?.base;
|
|
68
|
+
return { ...request.container, mode, ...(base === undefined ? {} : { base }) };
|
|
69
|
+
})() : { mode };
|
|
64
70
|
let proposed;
|
|
65
71
|
try {
|
|
66
72
|
proposed = executionModesSchema.parse({ schemaVersion: 1, workspaces: { ...raw.workspaces, [request.alias]: entry } });
|
package/dist/setup-process.d.ts
CHANGED
|
@@ -24,7 +24,9 @@ export declare const MCP_SERVER_SHUTDOWN_GRACE_MS = 2000;
|
|
|
24
24
|
/** Runs only an explicit executable/argv pair, with bounded output and termination. */
|
|
25
25
|
export declare function createProcessRunner(options?: ProcessRunnerOptions): CommandRunner;
|
|
26
26
|
/** Generalized bounded stdio transport for a generated writer wrapper. */
|
|
27
|
-
export declare function createCommandMcpStdioLauncher(command: readonly [string, ...string[]], env: NodeJS.ProcessEnv
|
|
27
|
+
export declare function createCommandMcpStdioLauncher(command: readonly [string, ...string[]], env: NodeJS.ProcessEnv, options?: {
|
|
28
|
+
inheritFds?: readonly number[];
|
|
29
|
+
}): (signal: AbortSignal) => Promise<McpStdioProcess>;
|
|
28
30
|
export interface McpLauncherOptions {
|
|
29
31
|
readonly nodePath: string;
|
|
30
32
|
readonly entryPath: string;
|
package/dist/setup-process.js
CHANGED
|
@@ -56,7 +56,7 @@ export function createProcessRunner(options = {}) {
|
|
|
56
56
|
if (!table)
|
|
57
57
|
return { exitCode: 126, stdout: "", stderr: "" };
|
|
58
58
|
return new Promise((resolve) => {
|
|
59
|
-
const child = spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"], env: runOptions?.env ? { ...(options.env ?? process.env), ...runOptions.env } : options.env, cwd: runOptions?.cwd, detached: process.platform !== "win32" });
|
|
59
|
+
const child = spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe", ...(runOptions?.inheritFds ?? [])], env: runOptions?.env ? { ...(options.env ?? process.env), ...runOptions.env } : options.env, cwd: runOptions?.cwd, detached: process.platform !== "win32" });
|
|
60
60
|
let stdout = "";
|
|
61
61
|
let stderr = "";
|
|
62
62
|
let overflow = false;
|
|
@@ -132,9 +132,9 @@ export function createProcessRunner(options = {}) {
|
|
|
132
132
|
} };
|
|
133
133
|
}
|
|
134
134
|
/** Generalized bounded stdio transport for a generated writer wrapper. */
|
|
135
|
-
export function createCommandMcpStdioLauncher(command, env) {
|
|
135
|
+
export function createCommandMcpStdioLauncher(command, env, options) {
|
|
136
136
|
return async (signal) => {
|
|
137
|
-
const child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe"], env, detached: process.platform !== "win32" });
|
|
137
|
+
const child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe", ...(options?.inheritFds ?? [])], env, detached: process.platform !== "win32" });
|
|
138
138
|
let buffer = "", closed = false, overflow = false;
|
|
139
139
|
child.stdout.on("data", (chunk) => { if (!overflow) {
|
|
140
140
|
buffer += chunk.toString("utf8");
|
|
@@ -415,7 +415,8 @@ export async function acquireSetupGuardian(lockFd, timeoutMs, signal, validateFe
|
|
|
415
415
|
} });
|
|
416
416
|
const runner = {
|
|
417
417
|
run(command, args, runSignal, runOptions) {
|
|
418
|
-
|
|
418
|
+
// The guardian child cannot pass descriptors through; refuse instead of dropping a caller's lease.
|
|
419
|
+
if (runOptions?.env !== undefined || runOptions?.inheritFds !== undefined || released || unsafe || pending || runSignal?.aborted || !child.connected)
|
|
419
420
|
return Promise.resolve({ exitCode: 126, stdout: "", stderr: "" });
|
|
420
421
|
const id = nextId++;
|
|
421
422
|
return new Promise((resolve) => {
|
package/dist/setup-writers.d.ts
CHANGED
|
@@ -6,8 +6,11 @@ import { type OwnershipStore } from "./setup-ownership.js";
|
|
|
6
6
|
import { type SetupResult } from "./setup-result.js";
|
|
7
7
|
import { type WriterRegistrationAdapter } from "./client-adapters/writer-registration.js";
|
|
8
8
|
import { verifyMcpCapabilities } from "./client-adapters/mcp-verifier.js";
|
|
9
|
+
import { acquireWriterLease } from "./writer-lease.js";
|
|
10
|
+
import { ensureWriterAbsent } from "./writer-fence.js";
|
|
9
11
|
import * as lifecycle from "./writer-lifecycle.js";
|
|
10
12
|
import * as acceptance from "./writer-acceptance.js";
|
|
13
|
+
export declare const SETUP_LEASE_WAIT_MS = 10000;
|
|
11
14
|
export type WriterOperation = "collect" | "reset" | "recreate" | "finish" | "remove" | "verify";
|
|
12
15
|
export interface WriterOptions {
|
|
13
16
|
apply: boolean;
|
|
@@ -29,6 +32,8 @@ export interface WriterSetupDependencies {
|
|
|
29
32
|
verify?: typeof verifyMcpCapabilities;
|
|
30
33
|
launch?: typeof createCommandMcpStdioLauncher;
|
|
31
34
|
lock?: typeof acquireOverrideLock;
|
|
35
|
+
lease?: typeof acquireWriterLease;
|
|
36
|
+
ensureAbsent?: typeof ensureWriterAbsent;
|
|
32
37
|
containerRuntime?: () => Promise<boolean>;
|
|
33
38
|
}
|
|
34
39
|
/** Serializes planning, destructive-work refusal, mode commit and writer reconciliation. */
|