pi-session-mcp 0.6.4 → 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.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 +37 -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.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 +216 -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 +25 -10
- package/docs/doctor-design.md +7 -2
- package/docs/installation.md +122 -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
package/dist/sdk-pi-adapter.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { type AgentSessionEvent, type CreateAgentSessionOptions, type CreateAgentSessionResult, DefaultResourceLoader, ModelRuntime, type PromptOptions, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
1
|
+
import { type AgentSessionEvent, type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, DefaultResourceLoader, ModelRuntime, type PromptOptions, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type ExtensionSpec } from "./extension-spec.js";
|
|
3
|
+
import type { ResolvedExecutionProfile, ThinkingLevel } from "./execution-profile.js";
|
|
3
4
|
import { type McpConnectionFactory } from "./mcp-session-tools.js";
|
|
4
5
|
import { type PiSessionAdapter, type PiSessionCreationErrorCode, type PiSessionHandle, type PiTurnOutcome } from "./pi-adapter.js";
|
|
6
|
+
export { PROFILE_TOOLS } from "./session-tools.js";
|
|
5
7
|
type CreateSession = (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
|
|
6
8
|
export declare const AUTH_OPERATION_TIMEOUT_MS = 5000;
|
|
7
9
|
/** Server-side bound for SDK session construction. The SDK does not document cancellation. */
|
|
@@ -9,7 +11,7 @@ export declare const SESSION_CREATION_TIMEOUT_MS = 10000;
|
|
|
9
11
|
export interface SdkPiAdapterDiagnostic {
|
|
10
12
|
readonly level: "error";
|
|
11
13
|
readonly event: "pi_session_creation_failed";
|
|
12
|
-
readonly stage: "runtime" | "provider" | "model" | "check_auth" | "get_auth" | "availability" | "resource_loader" | "mcp_tools" | "create_session" | "postcheck";
|
|
14
|
+
readonly stage: "runtime" | "provider" | "model" | "check_auth" | "get_auth" | "availability" | "resource_loader" | "mcp_tools" | "create_session" | "postcheck" | "extension_resolve" | "extension_load" | "extension_bind";
|
|
13
15
|
readonly code: PiSessionCreationErrorCode;
|
|
14
16
|
readonly executionProfile: string;
|
|
15
17
|
readonly provider: string;
|
|
@@ -18,12 +20,23 @@ export interface SdkPiAdapterDiagnostic {
|
|
|
18
20
|
readonly serverAlias?: string;
|
|
19
21
|
readonly toolAlias?: string;
|
|
20
22
|
readonly discoveredToolCount?: number;
|
|
23
|
+
/** `<name>@<version>` of the extension that failed to resolve; never a path. */
|
|
24
|
+
readonly extension?: string;
|
|
25
|
+
}
|
|
26
|
+
/** The Pi SDK session-lifecycle API used for profiles with extensions; injectable for tests. */
|
|
27
|
+
export interface ExtensionSdk {
|
|
28
|
+
readonly createServices: typeof createAgentSessionServices;
|
|
29
|
+
readonly createSessionFromServices: typeof createAgentSessionFromServices;
|
|
30
|
+
readonly createRuntime: typeof createAgentSessionRuntime;
|
|
21
31
|
}
|
|
22
32
|
export interface SdkPiSessionAdapterOptions {
|
|
23
33
|
readonly modelRuntimeFactory?: () => Promise<ModelRuntime>;
|
|
24
34
|
readonly createSession?: CreateSession;
|
|
25
35
|
readonly diagnosticSink?: (diagnostic: SdkPiAdapterDiagnostic) => void;
|
|
26
36
|
readonly mcpConnectionFactory?: McpConnectionFactory;
|
|
37
|
+
/** Resolves a pinned extension to its installed package directory. */
|
|
38
|
+
readonly resolveExtension?: (extension: ExtensionSpec) => Promise<string>;
|
|
39
|
+
readonly extensionSdk?: ExtensionSdk;
|
|
27
40
|
}
|
|
28
41
|
type SdkAssistantMessage = Extract<Extract<AgentSessionEvent, {
|
|
29
42
|
type: "message_end";
|
|
@@ -40,7 +53,6 @@ export type SdkStopReason = SdkAssistantMessage["stopReason"];
|
|
|
40
53
|
* values outside the SDK union, a missing stop reason included, fail closed.
|
|
41
54
|
*/
|
|
42
55
|
export declare function normalizeAssistantOutcome(message: unknown): PiTurnOutcome;
|
|
43
|
-
export declare const PROFILE_TOOLS: Readonly<Record<PermissionProfile, readonly string[]>>;
|
|
44
56
|
/** Derived from the root-exported `PromptOptions`; the SDK does not export the union itself. */
|
|
45
57
|
export type PromptDisposition = Parameters<NonNullable<PromptOptions["preflightResult"]>>[0];
|
|
46
58
|
/**
|
|
@@ -62,14 +74,20 @@ export declare class SdkPiSessionAdapter implements PiSessionAdapter {
|
|
|
62
74
|
private readonly createSession;
|
|
63
75
|
private readonly diagnosticSink;
|
|
64
76
|
private readonly mcpConnectionFactory;
|
|
77
|
+
private readonly resolveExtension;
|
|
78
|
+
private readonly extensionSdk;
|
|
65
79
|
constructor(options?: SdkPiSessionAdapterOptions);
|
|
66
80
|
create(input: {
|
|
67
81
|
cwd: string;
|
|
68
82
|
executionProfile: ResolvedExecutionProfile;
|
|
69
83
|
signal?: AbortSignal;
|
|
70
84
|
}): Promise<PiSessionHandle>;
|
|
71
|
-
private
|
|
85
|
+
private createWithExtensions;
|
|
86
|
+
/** Provider, model, authentication and availability, exactly as configured. */
|
|
87
|
+
private resolveVerifiedModel;
|
|
88
|
+
private connectMcpTools;
|
|
89
|
+
/** Post-verifies a created session and wraps it; a mismatch disposes it before reporting. */
|
|
90
|
+
private admit;
|
|
72
91
|
private getRuntime;
|
|
73
92
|
private failure;
|
|
74
93
|
}
|
|
75
|
-
export {};
|
package/dist/sdk-pi-adapter.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { createAgentSession, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { PiExtensionError } from "./extension-spec.js";
|
|
2
3
|
import { classifyMcpStartupError, connectSessionMcpTools, } from "./mcp-session-tools.js";
|
|
3
4
|
import { createStdioMcpConnection } from "./mcp-stdio-client.js";
|
|
4
5
|
import { PiSessionCreationError, } from "./pi-adapter.js";
|
|
6
|
+
import { resolvePiExtension } from "./pi-extensions.js";
|
|
7
|
+
import { composeSessionTools, PROFILE_TOOLS } from "./session-tools.js";
|
|
8
|
+
export { PROFILE_TOOLS } from "./session-tools.js";
|
|
5
9
|
export const AUTH_OPERATION_TIMEOUT_MS = 5_000;
|
|
6
10
|
/** Server-side bound for SDK session construction. The SDK does not document cancellation. */
|
|
7
11
|
export const SESSION_CREATION_TIMEOUT_MS = 10_000;
|
|
12
|
+
const DEFAULT_EXTENSION_SDK = {
|
|
13
|
+
createServices: createAgentSessionServices,
|
|
14
|
+
createSessionFromServices: createAgentSessionFromServices,
|
|
15
|
+
createRuntime: createAgentSessionRuntime,
|
|
16
|
+
};
|
|
8
17
|
class AuthDeadlineError extends Error {
|
|
9
18
|
constructor() {
|
|
10
19
|
super("authentication operation timed out");
|
|
@@ -42,6 +51,60 @@ async function withAuthDeadline(operation) {
|
|
|
42
51
|
controller.abort();
|
|
43
52
|
}
|
|
44
53
|
}
|
|
54
|
+
/** Races an SDK operation against the session-creation bound; a late success goes to `disposeLate`. */
|
|
55
|
+
async function withCreationDeadline(start, disposeLate = () => undefined) {
|
|
56
|
+
let operation;
|
|
57
|
+
try {
|
|
58
|
+
operation = Promise.resolve(start());
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new SessionCreationDeadlineError();
|
|
62
|
+
}
|
|
63
|
+
let timedOut = false;
|
|
64
|
+
// This continuation is installed immediately and owns late cleanup. Its rejection
|
|
65
|
+
// branch is required even when the deadline wins the race.
|
|
66
|
+
const observed = operation.then((value) => {
|
|
67
|
+
if (timedOut)
|
|
68
|
+
disposeLate(value);
|
|
69
|
+
return value;
|
|
70
|
+
}, (error) => { throw error; });
|
|
71
|
+
let timer;
|
|
72
|
+
const timeout = new Promise((_, reject) => {
|
|
73
|
+
timer = setTimeout(() => {
|
|
74
|
+
timedOut = true;
|
|
75
|
+
reject(new SessionCreationDeadlineError());
|
|
76
|
+
}, SESSION_CREATION_TIMEOUT_MS);
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
return await Promise.race([observed, timeout]);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (timer)
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Runs a session disposal bounded by the creation deadline; failures and a timeout are swallowed. */
|
|
87
|
+
async function boundedDisposal(dispose) {
|
|
88
|
+
let timer;
|
|
89
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(resolve, SESSION_CREATION_TIMEOUT_MS); });
|
|
90
|
+
let disposal;
|
|
91
|
+
try {
|
|
92
|
+
disposal = Promise.resolve(dispose()).then(() => undefined, () => undefined);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
disposal = Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await Promise.race([disposal, timeout]);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (timer)
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function registeredExtensionTools(services) {
|
|
106
|
+
return services.resourceLoader.getExtensions().extensions.map((extension) => [...extension.tools.keys()]);
|
|
107
|
+
}
|
|
45
108
|
/**
|
|
46
109
|
* Projects the final assistant message of a prompt onto a turn outcome.
|
|
47
110
|
*
|
|
@@ -75,10 +138,6 @@ export function normalizeAssistantOutcome(message) {
|
|
|
75
138
|
}
|
|
76
139
|
}
|
|
77
140
|
}
|
|
78
|
-
export const PROFILE_TOOLS = {
|
|
79
|
-
"read-only": ["read", "grep", "find", "ls"],
|
|
80
|
-
coding: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
|
81
|
-
};
|
|
82
141
|
/**
|
|
83
142
|
* Whether an SDK prompt disposition admits a public turn. The SDK reports how an accepted
|
|
84
143
|
* prompt was dispatched and does not call the hook on rejection: `prompt()` rejects instead,
|
|
@@ -93,8 +152,8 @@ export function admitsTurn(disposition) {
|
|
|
93
152
|
return true;
|
|
94
153
|
// Only while streaming with `streamingBehavior`; Pi Session MCP never passes it and serializes turns.
|
|
95
154
|
case "queued":
|
|
96
|
-
// An extension
|
|
97
|
-
//
|
|
155
|
+
// An extension input handler consumed the prompt without a model turn. Commands never
|
|
156
|
+
// dispatch (`expandPromptTemplates: false`), but allowlisted extensions may handle input.
|
|
98
157
|
case "handled":
|
|
99
158
|
return false;
|
|
100
159
|
default: {
|
|
@@ -108,13 +167,15 @@ class SdkHandle {
|
|
|
108
167
|
session;
|
|
109
168
|
appliedSelection;
|
|
110
169
|
mcpTools;
|
|
170
|
+
disposeSession;
|
|
111
171
|
disposed = false;
|
|
112
172
|
disposal;
|
|
113
173
|
subscriptions = new Set();
|
|
114
|
-
constructor(session, appliedSelection, mcpTools) {
|
|
174
|
+
constructor(session, appliedSelection, mcpTools, disposeSession = () => disposeSdkSession(session)) {
|
|
115
175
|
this.session = session;
|
|
116
176
|
this.appliedSelection = appliedSelection;
|
|
117
177
|
this.mcpTools = mcpTools;
|
|
178
|
+
this.disposeSession = disposeSession;
|
|
118
179
|
}
|
|
119
180
|
get sdkSessionId() { return this.session.sessionId; }
|
|
120
181
|
prompt(text, preflight, onActivity) {
|
|
@@ -159,7 +220,11 @@ class SdkHandle {
|
|
|
159
220
|
this.subscriptions.add(cleanup);
|
|
160
221
|
let promptResult;
|
|
161
222
|
try {
|
|
162
|
-
promptResult = this.session.prompt(text, {
|
|
223
|
+
promptResult = this.session.prompt(text, {
|
|
224
|
+
// Client text is content, never a command: no extension command, skill or template dispatch.
|
|
225
|
+
expandPromptTemplates: false,
|
|
226
|
+
preflightResult: (disposition) => signalPreflight(admitsTurn(disposition)),
|
|
227
|
+
});
|
|
163
228
|
}
|
|
164
229
|
catch {
|
|
165
230
|
cleanup();
|
|
@@ -178,8 +243,14 @@ class SdkHandle {
|
|
|
178
243
|
const closed = this.mcpTools.close();
|
|
179
244
|
for (const cleanup of [...this.subscriptions])
|
|
180
245
|
cleanup();
|
|
181
|
-
|
|
182
|
-
|
|
246
|
+
let sessionClosed;
|
|
247
|
+
try {
|
|
248
|
+
sessionClosed = Promise.resolve(this.disposeSession()).then(() => undefined, () => undefined);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
sessionClosed = Promise.resolve();
|
|
252
|
+
}
|
|
253
|
+
void Promise.all([closed.then(() => undefined, () => undefined), sessionClosed]).then(finish, finish);
|
|
183
254
|
return this.disposal;
|
|
184
255
|
}
|
|
185
256
|
}
|
|
@@ -195,12 +266,9 @@ function isTextBlock(block) {
|
|
|
195
266
|
const candidate = block;
|
|
196
267
|
return candidate.type === "text" && typeof candidate.text === "string";
|
|
197
268
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
cwd,
|
|
202
|
-
agentDir: cwd,
|
|
203
|
-
settingsManager,
|
|
269
|
+
/** Resource-loader options for every embedded session: no discovery, skills, templates or themes. */
|
|
270
|
+
function phase0ResourceLoaderOptions() {
|
|
271
|
+
return {
|
|
204
272
|
noExtensions: true,
|
|
205
273
|
noSkills: true,
|
|
206
274
|
noPromptTemplates: true,
|
|
@@ -210,7 +278,11 @@ export async function createPhase0ResourceLoader(cwd) {
|
|
|
210
278
|
appendSystemPrompt: [],
|
|
211
279
|
systemPromptOverride: () => undefined,
|
|
212
280
|
appendSystemPromptOverride: () => [],
|
|
213
|
-
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export async function createPhase0ResourceLoader(cwd) {
|
|
284
|
+
const settingsManager = SettingsManager.inMemory(undefined, { projectTrusted: false });
|
|
285
|
+
const resourceLoader = new DefaultResourceLoader({ cwd, agentDir: cwd, settingsManager, ...phase0ResourceLoaderOptions() });
|
|
214
286
|
await resourceLoader.reload();
|
|
215
287
|
return { resourceLoader, settingsManager };
|
|
216
288
|
}
|
|
@@ -220,13 +292,20 @@ export class SdkPiSessionAdapter {
|
|
|
220
292
|
createSession;
|
|
221
293
|
diagnosticSink;
|
|
222
294
|
mcpConnectionFactory;
|
|
295
|
+
resolveExtension;
|
|
296
|
+
extensionSdk;
|
|
223
297
|
constructor(options = {}) {
|
|
224
298
|
this.modelRuntimeFactory = options.modelRuntimeFactory ?? (() => ModelRuntime.create({ refreshOnCreate: false }));
|
|
225
299
|
this.createSession = options.createSession ?? createAgentSession;
|
|
226
300
|
this.diagnosticSink = options.diagnosticSink ?? (() => undefined);
|
|
227
301
|
this.mcpConnectionFactory = options.mcpConnectionFactory ?? createStdioMcpConnection;
|
|
302
|
+
this.resolveExtension = options.resolveExtension ?? ((extension) => resolvePiExtension(extension));
|
|
303
|
+
this.extensionSdk = options.extensionSdk ?? DEFAULT_EXTENSION_SDK;
|
|
228
304
|
}
|
|
229
305
|
async create(input) {
|
|
306
|
+
const extensions = input.executionProfile.extensions ?? [];
|
|
307
|
+
if (extensions.length > 0)
|
|
308
|
+
return this.createWithExtensions(input, extensions);
|
|
230
309
|
const { executionProfile } = input;
|
|
231
310
|
let runtime;
|
|
232
311
|
try {
|
|
@@ -235,6 +314,142 @@ export class SdkPiSessionAdapter {
|
|
|
235
314
|
catch {
|
|
236
315
|
throw this.failure(executionProfile, "runtime", "pi_session_creation_failed");
|
|
237
316
|
}
|
|
317
|
+
const model = await this.resolveVerifiedModel(runtime, executionProfile);
|
|
318
|
+
let resourceLoader;
|
|
319
|
+
let settingsManager;
|
|
320
|
+
try {
|
|
321
|
+
({ resourceLoader, settingsManager } = await createPhase0ResourceLoader(input.cwd));
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
throw this.failure(executionProfile, "resource_loader", "pi_session_creation_failed");
|
|
325
|
+
}
|
|
326
|
+
const mcpTools = await this.connectMcpTools(input);
|
|
327
|
+
const tools = [...PROFILE_TOOLS[executionProfile.permissionProfile], ...mcpTools.tools.map((tool) => tool.name)];
|
|
328
|
+
let session;
|
|
329
|
+
try {
|
|
330
|
+
({ session } = await withCreationDeadline(() => this.createSession({
|
|
331
|
+
cwd: input.cwd,
|
|
332
|
+
modelRuntime: runtime,
|
|
333
|
+
model,
|
|
334
|
+
thinkingLevel: executionProfile.thinkingLevel,
|
|
335
|
+
sessionManager: SessionManager.inMemory(input.cwd),
|
|
336
|
+
settingsManager,
|
|
337
|
+
resourceLoader,
|
|
338
|
+
// With an explicit tools list, Pi also needs custom tool names here to activate them.
|
|
339
|
+
tools,
|
|
340
|
+
...(mcpTools.tools.length === 0 ? {} : { customTools: mcpTools.tools }),
|
|
341
|
+
}), (late) => disposeSdkSession(late.session)));
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
await mcpTools.close();
|
|
345
|
+
throw this.failure(executionProfile, "create_session", "pi_session_creation_failed");
|
|
346
|
+
}
|
|
347
|
+
return this.admit(session, executionProfile, tools, mcpTools, () => disposeSdkSession(session), mcpTools.tools.length > 0 ? "external_mcp_activation_mismatch" : undefined);
|
|
348
|
+
}
|
|
349
|
+
async createWithExtensions(input, extensions) {
|
|
350
|
+
const { cwd, executionProfile } = input;
|
|
351
|
+
const directories = [];
|
|
352
|
+
for (const extension of extensions) {
|
|
353
|
+
try {
|
|
354
|
+
directories.push(await this.resolveExtension(extension));
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
throw this.failure(executionProfile, "extension_resolve", error instanceof PiExtensionError ? error.code : "extension_load_failed", { extension: `${extension.name}@${extension.version}` });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Extension providers register into the runtime they load with, and the last
|
|
361
|
+
// registration wins, so every extension session gets a runtime of its own.
|
|
362
|
+
let runtime;
|
|
363
|
+
try {
|
|
364
|
+
runtime = await this.modelRuntimeFactory();
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
throw this.failure(executionProfile, "runtime", "pi_session_creation_failed");
|
|
368
|
+
}
|
|
369
|
+
let services;
|
|
370
|
+
let extensionTools;
|
|
371
|
+
try {
|
|
372
|
+
services = await withCreationDeadline(() => this.extensionSdk.createServices({
|
|
373
|
+
cwd,
|
|
374
|
+
agentDir: cwd,
|
|
375
|
+
modelRuntime: runtime,
|
|
376
|
+
settingsManager: SettingsManager.inMemory(undefined, { projectTrusted: false }),
|
|
377
|
+
resourceLoaderOptions: { ...phase0ResourceLoaderOptions(), additionalExtensionPaths: directories },
|
|
378
|
+
}));
|
|
379
|
+
const loaded = services.resourceLoader.getExtensions();
|
|
380
|
+
if (loaded.errors.length > 0 || services.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
|
|
381
|
+
throw new Error("extension load reported errors");
|
|
382
|
+
}
|
|
383
|
+
extensionTools = registeredExtensionTools(services);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
throw this.failure(executionProfile, "extension_load", "extension_load_failed");
|
|
387
|
+
}
|
|
388
|
+
const model = await this.resolveVerifiedModel(services.modelRuntime, executionProfile);
|
|
389
|
+
const mcpTools = await this.connectMcpTools(input);
|
|
390
|
+
const tools = composeSessionTools(executionProfile.permissionProfile, mcpTools.tools.map((tool) => tool.name), extensionTools);
|
|
391
|
+
if (!tools) {
|
|
392
|
+
await mcpTools.close();
|
|
393
|
+
throw this.failure(executionProfile, "extension_load", "extension_tool_conflict");
|
|
394
|
+
}
|
|
395
|
+
let sessionRuntime;
|
|
396
|
+
try {
|
|
397
|
+
sessionRuntime = await withCreationDeadline(() => this.extensionSdk.createRuntime(async ({ sessionManager }) => ({
|
|
398
|
+
...(await this.extensionSdk.createSessionFromServices({
|
|
399
|
+
services,
|
|
400
|
+
sessionManager,
|
|
401
|
+
model,
|
|
402
|
+
thinkingLevel: executionProfile.thinkingLevel,
|
|
403
|
+
tools,
|
|
404
|
+
...(mcpTools.tools.length === 0 ? {} : { customTools: mcpTools.tools }),
|
|
405
|
+
})),
|
|
406
|
+
services,
|
|
407
|
+
diagnostics: services.diagnostics,
|
|
408
|
+
}), { cwd, agentDir: cwd, sessionManager: SessionManager.inMemory(cwd) }), (late) => { void late.dispose().catch(() => undefined); });
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
await mcpTools.close();
|
|
412
|
+
throw this.failure(executionProfile, "create_session", "pi_session_creation_failed");
|
|
413
|
+
}
|
|
414
|
+
// `AgentSessionRuntime.dispose()` emits `session_shutdown` before disposing the session.
|
|
415
|
+
const disposeRuntime = () => sessionRuntime.dispose().catch(() => undefined);
|
|
416
|
+
// Bounded and concurrent: a hanging `session_shutdown` handler must not keep MCP processes open.
|
|
417
|
+
const cleanUp = async () => {
|
|
418
|
+
await Promise.all([boundedDisposal(disposeRuntime), mcpTools.close()]);
|
|
419
|
+
};
|
|
420
|
+
let bindErrors = 0;
|
|
421
|
+
let bound = false;
|
|
422
|
+
let conflict = false;
|
|
423
|
+
try {
|
|
424
|
+
// Without a UI context the SDK binds its no-op UI; binding emits `session_start`.
|
|
425
|
+
await withCreationDeadline(() => sessionRuntime.session.bindExtensions({
|
|
426
|
+
mode: "print",
|
|
427
|
+
// `ExtensionRunner.emit` routes handler errors here instead of rejecting the bind.
|
|
428
|
+
onError: () => { if (!bound)
|
|
429
|
+
bindErrors += 1; },
|
|
430
|
+
}));
|
|
431
|
+
bound = true;
|
|
432
|
+
if (bindErrors > 0)
|
|
433
|
+
throw new Error("extension hook reported errors while binding");
|
|
434
|
+
// A `session_start` handler may register a tool that replaces a built-in or granted tool.
|
|
435
|
+
// Tools an extension registers later, during a turn, are not re-checked: allowlisted
|
|
436
|
+
// extensions are trusted code.
|
|
437
|
+
const recheck = composeSessionTools(executionProfile.permissionProfile, mcpTools.tools.map((tool) => tool.name), registeredExtensionTools(services));
|
|
438
|
+
if (!recheck || recheck.length !== tools.length || recheck.some((name, index) => name !== tools[index]))
|
|
439
|
+
conflict = true;
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
await cleanUp();
|
|
443
|
+
throw this.failure(executionProfile, "extension_bind", "extension_load_failed");
|
|
444
|
+
}
|
|
445
|
+
if (conflict) {
|
|
446
|
+
await cleanUp();
|
|
447
|
+
throw this.failure(executionProfile, "extension_bind", "extension_tool_conflict");
|
|
448
|
+
}
|
|
449
|
+
return this.admit(sessionRuntime.session, executionProfile, tools, mcpTools, disposeRuntime, mcpTools.tools.length > 0 ? "external_mcp_activation_mismatch" : "execution_selection_mismatch", "subset");
|
|
450
|
+
}
|
|
451
|
+
/** Provider, model, authentication and availability, exactly as configured. */
|
|
452
|
+
async resolveVerifiedModel(runtime, executionProfile) {
|
|
238
453
|
let provider;
|
|
239
454
|
try {
|
|
240
455
|
provider = runtime.getProvider(executionProfile.provider);
|
|
@@ -281,42 +496,21 @@ export class SdkPiSessionAdapter {
|
|
|
281
496
|
if (!available.some((candidate) => candidate.provider === executionProfile.provider && candidate.id === executionProfile.model)) {
|
|
282
497
|
throw this.failure(executionProfile, "availability", "model_unavailable");
|
|
283
498
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
({ resourceLoader, settingsManager } = await createPhase0ResourceLoader(input.cwd));
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
throw this.failure(executionProfile, "resource_loader", "pi_session_creation_failed");
|
|
291
|
-
}
|
|
292
|
-
let mcpTools;
|
|
499
|
+
return model;
|
|
500
|
+
}
|
|
501
|
+
async connectMcpTools(input) {
|
|
293
502
|
try {
|
|
294
|
-
|
|
503
|
+
return await connectSessionMcpTools(input.executionProfile.mcpServers ?? {}, input.executionProfile.permissionProfile, input.cwd, this.mcpConnectionFactory, input.signal === undefined ? {} : { signal: input.signal });
|
|
295
504
|
}
|
|
296
505
|
catch (error) {
|
|
297
506
|
const failure = classifyMcpStartupError(error);
|
|
298
|
-
throw this.failure(executionProfile, "mcp_tools", failure?.code ?? "external_mcp_unavailable", failure);
|
|
299
|
-
}
|
|
300
|
-
const tools = [...PROFILE_TOOLS[executionProfile.permissionProfile], ...mcpTools.tools.map((tool) => tool.name)];
|
|
301
|
-
let session;
|
|
302
|
-
try {
|
|
303
|
-
({ session } = await this.createSessionWithDeadline({
|
|
304
|
-
cwd: input.cwd,
|
|
305
|
-
modelRuntime: runtime,
|
|
306
|
-
model,
|
|
307
|
-
thinkingLevel: executionProfile.thinkingLevel,
|
|
308
|
-
sessionManager: SessionManager.inMemory(input.cwd),
|
|
309
|
-
settingsManager,
|
|
310
|
-
resourceLoader,
|
|
311
|
-
// With an explicit tools list, Pi also needs custom tool names here to activate them.
|
|
312
|
-
tools,
|
|
313
|
-
...(mcpTools.tools.length === 0 ? {} : { customTools: mcpTools.tools }),
|
|
314
|
-
}));
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
await mcpTools.close();
|
|
318
|
-
throw this.failure(executionProfile, "create_session", "pi_session_creation_failed");
|
|
507
|
+
throw this.failure(input.executionProfile, "mcp_tools", failure?.code ?? "external_mcp_unavailable", failure);
|
|
319
508
|
}
|
|
509
|
+
}
|
|
510
|
+
/** Post-verifies a created session and wraps it; a mismatch disposes it before reporting. */
|
|
511
|
+
async admit(session, executionProfile, tools, mcpTools, disposeSession, toolMismatch,
|
|
512
|
+
// `subset` lets an extension narrow its own active tools; nothing outside the allowlist may be active.
|
|
513
|
+
toolCheck = "exact") {
|
|
320
514
|
try {
|
|
321
515
|
if (session.model?.provider !== executionProfile.provider || session.model.id !== executionProfile.model) {
|
|
322
516
|
throw this.failure(executionProfile, "postcheck", "execution_selection_mismatch");
|
|
@@ -324,57 +518,29 @@ export class SdkPiSessionAdapter {
|
|
|
324
518
|
if (session.thinkingLevel !== executionProfile.thinkingLevel) {
|
|
325
519
|
throw this.failure(executionProfile, "postcheck", "thinking_level_unavailable");
|
|
326
520
|
}
|
|
327
|
-
if (
|
|
521
|
+
if (toolMismatch !== undefined) {
|
|
328
522
|
const active = new Set(session.getActiveToolNames());
|
|
329
|
-
|
|
330
|
-
|
|
523
|
+
const allowed = new Set(tools);
|
|
524
|
+
const mismatch = toolCheck === "subset"
|
|
525
|
+
? [...active].some((name) => !allowed.has(name))
|
|
526
|
+
: active.size !== tools.length || tools.some((name) => !active.has(name));
|
|
527
|
+
if (mismatch) {
|
|
528
|
+
throw this.failure(executionProfile, "postcheck", toolMismatch);
|
|
331
529
|
}
|
|
332
530
|
}
|
|
333
531
|
return new SdkHandle(session, {
|
|
334
532
|
provider: session.model.provider,
|
|
335
533
|
model: session.model.id,
|
|
336
534
|
thinkingLevel: session.thinkingLevel,
|
|
337
|
-
}, mcpTools);
|
|
535
|
+
}, mcpTools, disposeSession);
|
|
338
536
|
}
|
|
339
537
|
catch (error) {
|
|
340
|
-
|
|
341
|
-
await mcpTools.close();
|
|
538
|
+
await Promise.all([boundedDisposal(disposeSession), mcpTools.close()]);
|
|
342
539
|
if (error instanceof PiSessionCreationError)
|
|
343
540
|
throw error;
|
|
344
541
|
throw this.failure(executionProfile, "postcheck", "pi_session_creation_failed");
|
|
345
542
|
}
|
|
346
543
|
}
|
|
347
|
-
async createSessionWithDeadline(options) {
|
|
348
|
-
let creation;
|
|
349
|
-
try {
|
|
350
|
-
creation = Promise.resolve(this.createSession(options));
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
throw new SessionCreationDeadlineError();
|
|
354
|
-
}
|
|
355
|
-
let timedOut = false;
|
|
356
|
-
// This continuation is installed immediately and owns late cleanup. Its rejection
|
|
357
|
-
// branch is required even when the deadline wins the race.
|
|
358
|
-
const observed = creation.then((result) => {
|
|
359
|
-
if (timedOut)
|
|
360
|
-
disposeSdkSession(result.session);
|
|
361
|
-
return result;
|
|
362
|
-
}, (error) => { throw error; });
|
|
363
|
-
let timer;
|
|
364
|
-
const timeout = new Promise((_, reject) => {
|
|
365
|
-
timer = setTimeout(() => {
|
|
366
|
-
timedOut = true;
|
|
367
|
-
reject(new SessionCreationDeadlineError());
|
|
368
|
-
}, SESSION_CREATION_TIMEOUT_MS);
|
|
369
|
-
});
|
|
370
|
-
try {
|
|
371
|
-
return await Promise.race([observed, timeout]);
|
|
372
|
-
}
|
|
373
|
-
finally {
|
|
374
|
-
if (timer)
|
|
375
|
-
clearTimeout(timer);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
544
|
getRuntime() {
|
|
379
545
|
if (!this.runtimePromise) {
|
|
380
546
|
const pending = this.modelRuntimeFactory();
|
|
@@ -400,6 +566,7 @@ export class SdkPiSessionAdapter {
|
|
|
400
566
|
...(metadata?.serverAlias === undefined ? {} : { serverAlias: metadata.serverAlias }),
|
|
401
567
|
...(metadata?.toolAlias === undefined ? {} : { toolAlias: metadata.toolAlias }),
|
|
402
568
|
...(metadata?.discoveredToolCount === undefined ? {} : { discoveredToolCount: metadata.discoveredToolCount }),
|
|
569
|
+
...(metadata?.extension === undefined ? {} : { extension: metadata.extension }),
|
|
403
570
|
});
|
|
404
571
|
}
|
|
405
572
|
catch {
|
package/dist/server.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PermissionProfile } from "./execution-profile.js";
|
|
2
|
+
export declare const PROFILE_TOOLS: Readonly<Record<PermissionProfile, readonly string[]>>;
|
|
3
|
+
/**
|
|
4
|
+
* The tool allowlist of a session, or undefined when an extension tool name conflicts.
|
|
5
|
+
* Extension tools are active only in coding profiles, but a conflict fails closed in both,
|
|
6
|
+
* so an extension can never shadow a built-in or granted tool. A name outside the safe
|
|
7
|
+
* character set also fails closed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function composeSessionTools(permissionProfile: PermissionProfile, mcpToolNames: readonly string[], extensionToolNames: readonly (readonly string[])[]): string[] | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const PROFILE_TOOLS = {
|
|
2
|
+
"read-only": ["read", "grep", "find", "ls"],
|
|
3
|
+
coding: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
|
4
|
+
};
|
|
5
|
+
/** The SDK reads `*` in an allowlist as a glob and a leading `+` or `-` as a modifier, so those names are rejected: the first character may not be `-`. */
|
|
6
|
+
const EXTENSION_TOOL_NAME = /^[A-Za-z0-9_.:][A-Za-z0-9_.:-]*$/;
|
|
7
|
+
/**
|
|
8
|
+
* The tool allowlist of a session, or undefined when an extension tool name conflicts.
|
|
9
|
+
* Extension tools are active only in coding profiles, but a conflict fails closed in both,
|
|
10
|
+
* so an extension can never shadow a built-in or granted tool. A name outside the safe
|
|
11
|
+
* character set also fails closed.
|
|
12
|
+
*/
|
|
13
|
+
export function composeSessionTools(permissionProfile, mcpToolNames, extensionToolNames) {
|
|
14
|
+
const reserved = new Set([...PROFILE_TOOLS.coding, ...PROFILE_TOOLS["read-only"], ...mcpToolNames]);
|
|
15
|
+
const extensionTools = [];
|
|
16
|
+
for (const names of extensionToolNames) {
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
if (!EXTENSION_TOOL_NAME.test(name) || reserved.has(name))
|
|
19
|
+
return undefined;
|
|
20
|
+
reserved.add(name);
|
|
21
|
+
extensionTools.push(name);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [...PROFILE_TOOLS[permissionProfile], ...mcpToolNames, ...(permissionProfile === "coding" ? extensionTools : [])];
|
|
25
|
+
}
|