@narumitw/pi-subagents 3.0.2 → 3.1.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/README.md +58 -49
- package/dist/child-communication-bridge.ts +7 -5
- package/dist/child-communication-bridge.ts.map +2 -2
- package/dist/child-readiness-probe.ts +68 -0
- package/dist/child-readiness-probe.ts.map +7 -0
- package/dist/chunks/chunk-3FCHXUKZ.ts +98 -0
- package/dist/chunks/chunk-3FCHXUKZ.ts.map +7 -0
- package/dist/chunks/{chunk-7JQOB375.ts → chunk-YMVSJ5GE.ts} +84 -132
- package/dist/chunks/{chunk-7JQOB375.ts.map → chunk-YMVSJ5GE.ts.map} +4 -4
- package/dist/chunks/chunk-ZOAFWLDJ.ts +130 -0
- package/dist/chunks/chunk-ZOAFWLDJ.ts.map +7 -0
- package/dist/index.ts +1195 -81
- package/dist/index.ts.map +4 -4
- package/docs/attachments.md +37 -0
- package/docs/design-principles.md +8 -2
- package/docs/messaging.md +23 -0
- package/docs/tools.md +22 -56
- package/package.json +4 -1
- package/src/attachment-utils.ts +104 -0
- package/src/broker-credentials.ts +106 -28
- package/src/child-communication-bridge.ts +3 -3
- package/src/child-communication-tools.ts +2 -24
- package/src/child-readiness-probe.ts +72 -0
- package/src/process.ts +391 -47
- package/src/resource-attachments.ts +724 -0
- package/src/runtime.ts +18 -1
- package/src/skill-attachments.ts +302 -0
- package/src/timeout.ts +24 -0
- package/src/tools.ts +96 -18
- package/src/types.ts +4 -0
package/dist/index.ts
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
2
|
// @ts-nocheck -- generated entries use .ts extensions for Pi's Jiti loader.
|
|
3
3
|
import {
|
|
4
|
-
BROKER_CREDENTIAL_FD,
|
|
5
4
|
CHILD_COMMUNICATION_TOOL_NAMES,
|
|
5
|
+
prepareTimeoutArguments,
|
|
6
|
+
resolveTimeoutMs
|
|
7
|
+
} from "./chunks/chunk-ZOAFWLDJ.ts";
|
|
8
|
+
import {
|
|
9
|
+
addAttachmentIgnoreRules,
|
|
10
|
+
isAbortError,
|
|
11
|
+
isRecord,
|
|
12
|
+
isWithin,
|
|
13
|
+
realpath,
|
|
14
|
+
throwIfAttachmentAborted,
|
|
15
|
+
toPosixPath,
|
|
16
|
+
toolSourceId
|
|
17
|
+
} from "./chunks/chunk-3FCHXUKZ.ts";
|
|
18
|
+
import {
|
|
19
|
+
BROKER_CREDENTIAL_FD,
|
|
20
|
+
CHILD_READINESS_FD,
|
|
21
|
+
CHILD_READINESS_FD_ENV,
|
|
6
22
|
MAX_IDENTIFIER_LENGTH,
|
|
7
23
|
MAX_MESSAGE_BYTES,
|
|
24
|
+
MAX_READINESS_FRAME_BYTES,
|
|
8
25
|
MessageBroker,
|
|
9
|
-
|
|
26
|
+
assertChildBootstrapCapacity,
|
|
27
|
+
childBootstrapEnvironment,
|
|
10
28
|
sanitizeTerminalText,
|
|
11
|
-
|
|
29
|
+
serializeChildBootstrap,
|
|
12
30
|
validateMessage
|
|
13
|
-
} from "./chunks/chunk-
|
|
31
|
+
} from "./chunks/chunk-YMVSJ5GE.ts";
|
|
14
32
|
|
|
15
33
|
// src/completion-renderer.ts
|
|
16
34
|
import { getMarkdownTheme, keyText } from "@earendil-works/pi-coding-agent";
|
|
@@ -102,34 +120,26 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
102
120
|
import { fileURLToPath } from "node:url";
|
|
103
121
|
import { getPackageDir } from "@earendil-works/pi-coding-agent";
|
|
104
122
|
var CORE_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
105
|
-
var MAX_TIMEOUT_MS = 2147483647;
|
|
106
|
-
var MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1e3;
|
|
107
123
|
var MAX_OUTPUT_BYTES = 32 * 1024;
|
|
108
124
|
var MAX_ERROR_BYTES = 8 * 1024;
|
|
109
125
|
var MAX_EVENT_LINE_BYTES = 256 * 1024;
|
|
110
126
|
var RPC_RESPONSE_TIMEOUT_MS = 3e4;
|
|
111
127
|
var KILL_GRACE_MS = 1e3;
|
|
112
|
-
|
|
113
|
-
if (timeout === void 0) return void 0;
|
|
114
|
-
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
115
|
-
throw new Error("Invalid timeout: must be a finite number of seconds");
|
|
116
|
-
}
|
|
117
|
-
const timeoutMs = timeout * 1e3;
|
|
118
|
-
if (timeoutMs > MAX_TIMEOUT_MS) {
|
|
119
|
-
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
|
|
120
|
-
}
|
|
121
|
-
return timeoutMs;
|
|
122
|
-
}
|
|
128
|
+
var MAX_WINDOWS_COMMAND_LINE_CODE_UNITS = 32767;
|
|
123
129
|
async function runChild(request) {
|
|
124
130
|
if (request.signal.aborted) return cancelledResult();
|
|
125
131
|
try {
|
|
132
|
+
assertChildCommandCapacity(request);
|
|
126
133
|
const invocation = resolvePiInvocation(buildPiArgs(request));
|
|
127
134
|
return await executeProcess(invocation, request);
|
|
128
135
|
} catch (error) {
|
|
129
136
|
if (request.signal.aborted) return cancelledResult();
|
|
130
137
|
return {
|
|
131
138
|
state: "failed",
|
|
132
|
-
error: truncateText(
|
|
139
|
+
error: truncateText(
|
|
140
|
+
redactAttachmentPaths(error instanceof Error ? error.message : String(error), request),
|
|
141
|
+
MAX_ERROR_BYTES
|
|
142
|
+
).text,
|
|
133
143
|
limitations: [],
|
|
134
144
|
truncated: false
|
|
135
145
|
};
|
|
@@ -144,31 +154,106 @@ function buildPiArgs(request) {
|
|
|
144
154
|
"--no-skills",
|
|
145
155
|
"--no-prompt-templates",
|
|
146
156
|
"-e",
|
|
147
|
-
childCommunicationBridgePath()
|
|
157
|
+
childCommunicationBridgePath()
|
|
158
|
+
];
|
|
159
|
+
for (const extension of request.extensions) args.push("-e", extension.path);
|
|
160
|
+
if (requiresReadinessAttestation(request)) args.push("-e", childReadinessProbePath());
|
|
161
|
+
for (const skill of request.skills) args.push("--skill", skill);
|
|
162
|
+
args.push(
|
|
148
163
|
"--model",
|
|
149
164
|
request.model,
|
|
150
165
|
"--thinking",
|
|
151
166
|
request.thinkingLevel,
|
|
152
167
|
request.projectTrusted ? "--approve" : "--no-approve"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
args.push("--tools", tools.join(","));
|
|
168
|
+
);
|
|
169
|
+
args.push("--tools", selectedChildTools(request).join(","));
|
|
156
170
|
return args;
|
|
157
171
|
}
|
|
172
|
+
function assertChildCommandCapacity(request, platform = process.platform) {
|
|
173
|
+
if (platform !== "win32") return;
|
|
174
|
+
const invocation = resolvePiInvocation(buildPiArgs(request));
|
|
175
|
+
const commandLineLength = [invocation.command, ...invocation.args].map(quoteWindowsArgument).join(" ").length;
|
|
176
|
+
if (commandLineLength + 1 > MAX_WINDOWS_COMMAND_LINE_CODE_UNITS) {
|
|
177
|
+
throw new Error("Subagent child command line exceeds the Windows process limit.");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function quoteWindowsArgument(value) {
|
|
181
|
+
if (value.length > 0 && !/[\s"]/u.test(value)) return value;
|
|
182
|
+
let quoted = '"';
|
|
183
|
+
let backslashes = 0;
|
|
184
|
+
for (const character of value) {
|
|
185
|
+
if (character === "\\") {
|
|
186
|
+
backslashes++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (character === '"') {
|
|
190
|
+
quoted += `${"\\".repeat(backslashes * 2 + 1)}"`;
|
|
191
|
+
} else {
|
|
192
|
+
quoted += `${"\\".repeat(backslashes)}${character}`;
|
|
193
|
+
}
|
|
194
|
+
backslashes = 0;
|
|
195
|
+
}
|
|
196
|
+
return `${quoted}${"\\".repeat(backslashes * 2)}"`;
|
|
197
|
+
}
|
|
158
198
|
function childCommunicationBridgePath() {
|
|
159
199
|
return fileURLToPath(new URL("./child-communication-bridge.ts", import.meta.url));
|
|
160
200
|
}
|
|
201
|
+
function childReadinessProbePath() {
|
|
202
|
+
return fileURLToPath(new URL("./child-readiness-probe.ts", import.meta.url));
|
|
203
|
+
}
|
|
204
|
+
function requiresReadinessAttestation(request) {
|
|
205
|
+
return request.extensions.length > 0;
|
|
206
|
+
}
|
|
207
|
+
function selectedChildTools(request) {
|
|
208
|
+
return [
|
|
209
|
+
.../* @__PURE__ */ new Set([
|
|
210
|
+
...request.tools,
|
|
211
|
+
...CHILD_COMMUNICATION_TOOL_NAMES,
|
|
212
|
+
...request.extensions.flatMap((extension) => extension.tools)
|
|
213
|
+
])
|
|
214
|
+
];
|
|
215
|
+
}
|
|
161
216
|
async function executeProcess(invocation, request) {
|
|
162
217
|
const timeoutMs = resolveTimeoutMs(request.timeout);
|
|
218
|
+
const expectedTools = selectedChildTools(request);
|
|
219
|
+
const expectReadiness = requiresReadinessAttestation(request);
|
|
220
|
+
if (expectReadiness) assertChildBootstrapCapacity(expectedTools);
|
|
163
221
|
let latestOutput = "";
|
|
164
222
|
let terminalOutput;
|
|
165
223
|
let terminalStopReason;
|
|
166
224
|
let errorMessage = "";
|
|
167
225
|
let assistantFailed = false;
|
|
168
226
|
let stderr = "";
|
|
227
|
+
let stderrRedactionCarry = "";
|
|
228
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
229
|
+
const diagnosticAttachmentPaths = attachmentPathVariants(request);
|
|
169
230
|
let truncated = false;
|
|
231
|
+
const appendStderr = (value) => {
|
|
232
|
+
if (!value) return;
|
|
233
|
+
const limited = truncateTail(`${stderr}${value}`, MAX_ERROR_BYTES);
|
|
234
|
+
stderr = limited.text;
|
|
235
|
+
truncated ||= limited.truncated;
|
|
236
|
+
};
|
|
237
|
+
const pushStderr = (chunk) => {
|
|
238
|
+
const combined = `${stderrRedactionCarry}${typeof chunk === "string" ? chunk : stderrDecoder.write(chunk)}`;
|
|
239
|
+
const carryLength = attachmentPathCarryLength(combined, diagnosticAttachmentPaths);
|
|
240
|
+
const boundary = combined.length - carryLength;
|
|
241
|
+
appendStderr(redactAttachmentPathVariants(combined.slice(0, boundary), diagnosticAttachmentPaths));
|
|
242
|
+
stderrRedactionCarry = combined.slice(boundary);
|
|
243
|
+
};
|
|
244
|
+
const flushStderr = () => {
|
|
245
|
+
const combined = `${stderrRedactionCarry}${stderrDecoder.end()}`;
|
|
246
|
+
const carryLength = attachmentPathCarryLength(combined, diagnosticAttachmentPaths);
|
|
247
|
+
const boundary = combined.length - carryLength;
|
|
248
|
+
appendStderr(
|
|
249
|
+
`${redactAttachmentPathVariants(combined.slice(0, boundary), diagnosticAttachmentPaths)}${carryLength > 0 ? "[attachment path]" : ""}`
|
|
250
|
+
);
|
|
251
|
+
stderrRedactionCarry = "";
|
|
252
|
+
};
|
|
170
253
|
let malformedEvents = 0;
|
|
171
254
|
let rpcCounter = 0;
|
|
255
|
+
let attachmentStartupPending = expectReadiness;
|
|
256
|
+
let attachmentStartupError;
|
|
172
257
|
const pendingCommands = /* @__PURE__ */ new Map();
|
|
173
258
|
let rpcInputError;
|
|
174
259
|
let sendCommand = () => Promise.reject(new Error("Subagent RPC process is unavailable."));
|
|
@@ -189,11 +274,11 @@ async function executeProcess(invocation, request) {
|
|
|
189
274
|
const rejectPendingCommands = (error2) => {
|
|
190
275
|
for (const id of [...pendingCommands.keys()]) rejectPendingCommand(id, error2);
|
|
191
276
|
};
|
|
192
|
-
const resolvePendingCommand = (id) => {
|
|
277
|
+
const resolvePendingCommand = (id, response) => {
|
|
193
278
|
const pending = takePendingCommand(id);
|
|
194
279
|
if (!pending) return;
|
|
195
280
|
try {
|
|
196
|
-
pending.onAccepted?.();
|
|
281
|
+
pending.onAccepted?.(response);
|
|
197
282
|
pending.resolve();
|
|
198
283
|
} catch (error2) {
|
|
199
284
|
pending.reject(error2 instanceof Error ? error2 : new Error(String(error2)));
|
|
@@ -206,16 +291,21 @@ async function executeProcess(invocation, request) {
|
|
|
206
291
|
const decoder = new JsonLineDecoder(
|
|
207
292
|
(value) => {
|
|
208
293
|
const event = value;
|
|
294
|
+
if (expectReadiness && event.type === "extension_error" && (event.event === "session_start" || event.event === "resources_discover") && !attachmentStartupError) {
|
|
295
|
+
const detail = typeof event.error === "string" && event.error ? redactAttachmentPaths(sanitizeTerminalText(event.error), request) : "Unknown error.";
|
|
296
|
+
attachmentStartupError = `Subagent attachment startup failed during ${event.event}: ${detail}`;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
209
299
|
if (event.type === "response" && typeof event.id === "string") {
|
|
210
300
|
const pending = pendingCommands.get(event.id);
|
|
211
301
|
if (!pending) return;
|
|
212
302
|
if (event.success === true) {
|
|
213
|
-
resolvePendingCommand(event.id);
|
|
303
|
+
resolvePendingCommand(event.id, event);
|
|
214
304
|
} else {
|
|
215
305
|
rejectPendingCommand(
|
|
216
306
|
event.id,
|
|
217
307
|
new Error(
|
|
218
|
-
typeof event.error === "string" ? event.error : `Subagent RPC ${pending.command} command failed.`
|
|
308
|
+
pending.command === "get_commands" ? "Subagent child resource attestation failed." : typeof event.error === "string" ? event.error : `Subagent RPC ${pending.command} command failed.`
|
|
219
309
|
)
|
|
220
310
|
);
|
|
221
311
|
}
|
|
@@ -240,7 +330,7 @@ async function executeProcess(invocation, request) {
|
|
|
240
330
|
assistantFailed = true;
|
|
241
331
|
}
|
|
242
332
|
if (event.message.errorMessage) {
|
|
243
|
-
const limited = truncateText(event.message.errorMessage, MAX_ERROR_BYTES);
|
|
333
|
+
const limited = truncateText(redactAttachmentPaths(event.message.errorMessage, request), MAX_ERROR_BYTES);
|
|
244
334
|
errorMessage = limited.text;
|
|
245
335
|
truncated ||= limited.truncated;
|
|
246
336
|
}
|
|
@@ -248,9 +338,12 @@ async function executeProcess(invocation, request) {
|
|
|
248
338
|
},
|
|
249
339
|
() => {
|
|
250
340
|
malformedEvents++;
|
|
341
|
+
if (attachmentStartupPending && !attachmentStartupError) {
|
|
342
|
+
attachmentStartupError = "Subagent attachment startup emitted malformed or oversized RPC output.";
|
|
343
|
+
}
|
|
251
344
|
}
|
|
252
345
|
);
|
|
253
|
-
const settlement = await new Promise((
|
|
346
|
+
const settlement = await new Promise((resolve4) => {
|
|
254
347
|
let process2;
|
|
255
348
|
let settled = false;
|
|
256
349
|
let finishRequested = false;
|
|
@@ -260,6 +353,9 @@ async function executeProcess(invocation, request) {
|
|
|
260
353
|
let timedOut = false;
|
|
261
354
|
let completed = false;
|
|
262
355
|
let ready = false;
|
|
356
|
+
let attachmentReady = !expectReadiness;
|
|
357
|
+
let promptStarted = false;
|
|
358
|
+
let readinessPipe;
|
|
263
359
|
let deadline;
|
|
264
360
|
let forceClose;
|
|
265
361
|
let escalation;
|
|
@@ -274,8 +370,10 @@ async function executeProcess(invocation, request) {
|
|
|
274
370
|
if (forceClose) clearTimeout(forceClose);
|
|
275
371
|
if (escalation) clearTimeout(escalation);
|
|
276
372
|
request.signal.removeEventListener("abort", onAbort);
|
|
373
|
+
readinessPipe?.removeAllListeners();
|
|
374
|
+
readinessPipe?.destroy();
|
|
277
375
|
rejectPendingCommands(new Error("Subagent RPC process closed."));
|
|
278
|
-
|
|
376
|
+
resolve4({ code, cancelled, timedOut, completed, launchError });
|
|
279
377
|
};
|
|
280
378
|
if (termination) void termination.then(complete, complete);
|
|
281
379
|
else complete();
|
|
@@ -299,6 +397,7 @@ async function executeProcess(invocation, request) {
|
|
|
299
397
|
process2.stdin?.destroy();
|
|
300
398
|
process2.stdout?.destroy();
|
|
301
399
|
process2.stderr?.destroy();
|
|
400
|
+
readinessPipe?.destroy();
|
|
302
401
|
finish(code);
|
|
303
402
|
}, KILL_GRACE_MS * 2);
|
|
304
403
|
forceClose.unref();
|
|
@@ -315,16 +414,18 @@ async function executeProcess(invocation, request) {
|
|
|
315
414
|
};
|
|
316
415
|
onAgentSettled = completeNormally;
|
|
317
416
|
try {
|
|
417
|
+
const environment = {
|
|
418
|
+
...globalThis.process.env,
|
|
419
|
+
...childBootstrapEnvironment(expectReadiness),
|
|
420
|
+
PI_SUBAGENT_DEPTH: String((Number.parseInt(globalThis.process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1)
|
|
421
|
+
};
|
|
422
|
+
if (!expectReadiness) delete environment[CHILD_READINESS_FD_ENV];
|
|
318
423
|
process2 = spawn(invocation.command, invocation.args, {
|
|
319
424
|
cwd: request.cwd,
|
|
320
425
|
detached: globalThis.process.platform !== "win32",
|
|
321
426
|
shell: false,
|
|
322
|
-
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
323
|
-
env:
|
|
324
|
-
...globalThis.process.env,
|
|
325
|
-
...brokerCredentialEnvironment(),
|
|
326
|
-
PI_SUBAGENT_DEPTH: String((Number.parseInt(globalThis.process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1)
|
|
327
|
-
}
|
|
427
|
+
stdio: expectReadiness ? ["pipe", "pipe", "pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe", "pipe"],
|
|
428
|
+
env: environment
|
|
328
429
|
});
|
|
329
430
|
} catch (error2) {
|
|
330
431
|
finish(1, error2 instanceof Error ? error2.message : String(error2));
|
|
@@ -379,11 +480,9 @@ async function executeProcess(invocation, request) {
|
|
|
379
480
|
}
|
|
380
481
|
});
|
|
381
482
|
};
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
spawned = true;
|
|
386
|
-
if (settled || cancelled) return;
|
|
483
|
+
const startPrompt = () => {
|
|
484
|
+
if (!spawned || !attachmentReady || promptStarted || settled || cancelled || terminating) return;
|
|
485
|
+
promptStarted = true;
|
|
387
486
|
void sendCommand(
|
|
388
487
|
{ type: "prompt", message: `Task: ${request.task}` },
|
|
389
488
|
() => {
|
|
@@ -411,22 +510,105 @@ async function executeProcess(invocation, request) {
|
|
|
411
510
|
request.signal
|
|
412
511
|
).catch((error2) => {
|
|
413
512
|
if (settled || terminating) return;
|
|
414
|
-
errorMessage = truncateText(
|
|
513
|
+
errorMessage = truncateText(
|
|
514
|
+
redactAttachmentPaths(error2 instanceof Error ? error2.message : String(error2), request),
|
|
515
|
+
MAX_ERROR_BYTES
|
|
516
|
+
).text;
|
|
415
517
|
terminate(1);
|
|
416
518
|
});
|
|
519
|
+
};
|
|
520
|
+
const failReadiness = (message) => {
|
|
521
|
+
if (settled || terminating || attachmentReady) return;
|
|
522
|
+
errorMessage = truncateText(redactAttachmentPaths(message, request), MAX_ERROR_BYTES).text;
|
|
523
|
+
terminate(1);
|
|
524
|
+
};
|
|
525
|
+
const confirmAttachmentStartup = () => {
|
|
526
|
+
void sendCommand({ type: "get_state" }, void 0, request.signal).then(async () => {
|
|
527
|
+
if (attachmentStartupError) throw new Error(attachmentStartupError);
|
|
528
|
+
if (!request.projectTrusted) {
|
|
529
|
+
await sendCommand(
|
|
530
|
+
{ type: "get_commands" },
|
|
531
|
+
(response) => assertTrustedChildResources(response.data, request.cwd),
|
|
532
|
+
request.signal
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (attachmentStartupError) throw new Error(attachmentStartupError);
|
|
536
|
+
attachmentStartupPending = false;
|
|
537
|
+
attachmentReady = true;
|
|
538
|
+
startPrompt();
|
|
539
|
+
}).catch((error2) => {
|
|
540
|
+
failReadiness(error2 instanceof Error ? error2.message : String(error2));
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
544
|
+
if (request.signal.aborted) onAbort();
|
|
545
|
+
process2.once("spawn", () => {
|
|
546
|
+
spawned = true;
|
|
547
|
+
startPrompt();
|
|
417
548
|
});
|
|
549
|
+
if (expectReadiness) {
|
|
550
|
+
const candidate = process2.stdio[CHILD_READINESS_FD];
|
|
551
|
+
if (!candidate || !("on" in candidate)) {
|
|
552
|
+
errorMessage = "Subagent readiness pipe is unavailable.";
|
|
553
|
+
terminate(1);
|
|
554
|
+
} else {
|
|
555
|
+
readinessPipe = candidate;
|
|
556
|
+
let buffer = Buffer.alloc(0);
|
|
557
|
+
let readinessSettled = false;
|
|
558
|
+
readinessPipe.on("data", (chunk) => {
|
|
559
|
+
if (readinessSettled || terminating) return;
|
|
560
|
+
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
561
|
+
if (buffer.byteLength > MAX_READINESS_FRAME_BYTES) {
|
|
562
|
+
readinessSettled = true;
|
|
563
|
+
failReadiness("Subagent readiness frame exceeded its size limit.");
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
readinessPipe.once("end", () => {
|
|
567
|
+
if (readinessSettled || terminating) return;
|
|
568
|
+
readinessSettled = true;
|
|
569
|
+
let frame;
|
|
570
|
+
try {
|
|
571
|
+
frame = parseReadinessFrame(buffer, expectedTools.length);
|
|
572
|
+
} catch (error2) {
|
|
573
|
+
failReadiness(error2 instanceof Error ? error2.message : String(error2));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (!frame.ok) {
|
|
577
|
+
failReadiness(frame.error);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
for (const extension of request.extensions) {
|
|
581
|
+
for (const tool of extension.tools) {
|
|
582
|
+
const source = frame.sources[expectedTools.indexOf(tool)];
|
|
583
|
+
if (!source || !Object.hasOwn(request.toolSources, tool) || !request.toolSources[tool]?.includes(source)) {
|
|
584
|
+
failReadiness(`Subagent extension tool ${tool} was not provided by its requested attachment.`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
confirmAttachmentStartup();
|
|
590
|
+
});
|
|
591
|
+
readinessPipe.once("error", () => {
|
|
592
|
+
if (readinessSettled || terminating) return;
|
|
593
|
+
readinessSettled = true;
|
|
594
|
+
failReadiness("Subagent readiness transfer failed.");
|
|
595
|
+
});
|
|
596
|
+
readinessPipe.once("close", () => {
|
|
597
|
+
if (readinessSettled || terminating) return;
|
|
598
|
+
readinessSettled = true;
|
|
599
|
+
failReadiness("Subagent readiness pipe closed without a result.");
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
418
603
|
process2.stdout?.on("data", (chunk) => decoder.push(chunk));
|
|
419
|
-
process2.stderr?.on("data", (chunk) =>
|
|
420
|
-
const limited = truncateTail(`${stderr}${chunk.toString()}`, MAX_ERROR_BYTES);
|
|
421
|
-
stderr = limited.text;
|
|
422
|
-
truncated ||= limited.truncated;
|
|
423
|
-
});
|
|
604
|
+
process2.stderr?.on("data", (chunk) => pushStderr(chunk));
|
|
424
605
|
process2.once("close", (code) => {
|
|
606
|
+
flushStderr();
|
|
425
607
|
decoder.finish();
|
|
426
608
|
finish(cancelled ? 130 : timedOut ? 124 : completed ? 0 : code ?? 1);
|
|
427
609
|
});
|
|
428
610
|
process2.once("error", (error2) => {
|
|
429
|
-
const limited = truncateText(error2.message, MAX_ERROR_BYTES);
|
|
611
|
+
const limited = truncateText(redactAttachmentPaths(error2.message, request), MAX_ERROR_BYTES);
|
|
430
612
|
errorMessage = limited.text;
|
|
431
613
|
truncated ||= limited.truncated;
|
|
432
614
|
if (spawned) terminate(1);
|
|
@@ -449,7 +631,12 @@ async function executeProcess(invocation, request) {
|
|
|
449
631
|
credentialPipe.on("error", onCredentialError);
|
|
450
632
|
credentialPipe.once("close", removeCredentialListeners);
|
|
451
633
|
try {
|
|
452
|
-
credentialPipe.end(
|
|
634
|
+
credentialPipe.end(
|
|
635
|
+
serializeChildBootstrap({
|
|
636
|
+
communication: request.communication,
|
|
637
|
+
expectedTools: expectReadiness ? expectedTools : []
|
|
638
|
+
})
|
|
639
|
+
);
|
|
453
640
|
} catch {
|
|
454
641
|
onCredentialError();
|
|
455
642
|
}
|
|
@@ -471,7 +658,17 @@ async function executeProcess(invocation, request) {
|
|
|
471
658
|
truncated
|
|
472
659
|
};
|
|
473
660
|
}
|
|
474
|
-
const
|
|
661
|
+
const redactedStderr = truncateTail(redactAttachmentPaths(stderr.trim(), request), MAX_ERROR_BYTES);
|
|
662
|
+
const combinedError = combineErrors(
|
|
663
|
+
redactAttachmentPaths(settlement.launchError ?? "", request),
|
|
664
|
+
redactAttachmentPaths(errorMessage, request),
|
|
665
|
+
redactedStderr.text
|
|
666
|
+
);
|
|
667
|
+
const error = combinedError.text;
|
|
668
|
+
if ((combinedError.truncated || redactedStderr.truncated) && !truncated) {
|
|
669
|
+
limitations.push("Child output was truncated to runtime limits.");
|
|
670
|
+
}
|
|
671
|
+
truncated ||= combinedError.truncated || redactedStderr.truncated;
|
|
475
672
|
if (settlement.completed && terminalStopReason === "stop" && !assistantFailed && !errorMessage) {
|
|
476
673
|
return {
|
|
477
674
|
state: "completed",
|
|
@@ -510,8 +707,8 @@ function resolvePiInvocation(args) {
|
|
|
510
707
|
return { command: globalThis.process.execPath, args };
|
|
511
708
|
}
|
|
512
709
|
const cliPath = fs.realpathSync(path.resolve(packageDirectory, declared));
|
|
513
|
-
const
|
|
514
|
-
if (
|
|
710
|
+
const relative4 = path.relative(packageDirectory, cliPath);
|
|
711
|
+
if (relative4 === ".." || relative4.startsWith(`..${path.sep}`) || path.isAbsolute(relative4)) {
|
|
515
712
|
throw new Error("Pi core bin.pi escapes its package directory.");
|
|
516
713
|
}
|
|
517
714
|
if (!fs.statSync(cliPath).isFile()) throw new Error("Pi core bin.pi is not a file.");
|
|
@@ -535,7 +732,7 @@ function terminateWindowsProcessTree(process2, spawnProcess = spawn, taskkillPat
|
|
|
535
732
|
killImmediateChild(process2);
|
|
536
733
|
return Promise.resolve();
|
|
537
734
|
}
|
|
538
|
-
return new Promise((
|
|
735
|
+
return new Promise((resolve4) => {
|
|
539
736
|
let settled = false;
|
|
540
737
|
let treeKiller;
|
|
541
738
|
let deadline;
|
|
@@ -549,7 +746,7 @@ function terminateWindowsProcessTree(process2, spawnProcess = spawn, taskkillPat
|
|
|
549
746
|
treeKiller.removeListener("close", onClose);
|
|
550
747
|
if (terminateHelper) killImmediateChild(treeKiller);
|
|
551
748
|
if (fallback) killImmediateChild(process2);
|
|
552
|
-
|
|
749
|
+
resolve4();
|
|
553
750
|
};
|
|
554
751
|
try {
|
|
555
752
|
treeKiller = spawnProcess(taskkillPath, ["/PID", String(process2.pid), "/T", "/F"], {
|
|
@@ -558,7 +755,7 @@ function terminateWindowsProcessTree(process2, spawnProcess = spawn, taskkillPat
|
|
|
558
755
|
});
|
|
559
756
|
} catch {
|
|
560
757
|
killImmediateChild(process2);
|
|
561
|
-
|
|
758
|
+
resolve4();
|
|
562
759
|
return;
|
|
563
760
|
}
|
|
564
761
|
treeKiller.once("error", onError);
|
|
@@ -578,6 +775,97 @@ function killImmediateChild(process2) {
|
|
|
578
775
|
} catch {
|
|
579
776
|
}
|
|
580
777
|
}
|
|
778
|
+
function assertTrustedChildResources(value, cwd) {
|
|
779
|
+
const invalid = () => new Error("Subagent child resource attestation returned invalid commands.");
|
|
780
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid();
|
|
781
|
+
const commands = value.commands;
|
|
782
|
+
if (!Array.isArray(commands)) throw invalid();
|
|
783
|
+
let canonicalCwd;
|
|
784
|
+
try {
|
|
785
|
+
canonicalCwd = fs.realpathSync(cwd);
|
|
786
|
+
} catch {
|
|
787
|
+
throw invalid();
|
|
788
|
+
}
|
|
789
|
+
for (const command of commands) {
|
|
790
|
+
if (!command || typeof command !== "object" || Array.isArray(command)) throw invalid();
|
|
791
|
+
const entry = command;
|
|
792
|
+
if (entry.source !== "skill" && entry.source !== "prompt") continue;
|
|
793
|
+
const resourcePath = entry.sourceInfo?.path;
|
|
794
|
+
if (typeof resourcePath !== "string" || !resourcePath) throw invalid();
|
|
795
|
+
const lexicalPath = path.resolve(cwd, resourcePath);
|
|
796
|
+
let canonicalPath;
|
|
797
|
+
try {
|
|
798
|
+
canonicalPath = fs.realpathSync(lexicalPath);
|
|
799
|
+
} catch {
|
|
800
|
+
throw invalid();
|
|
801
|
+
}
|
|
802
|
+
if (isWithin(cwd, lexicalPath) || isWithin(canonicalCwd, canonicalPath)) {
|
|
803
|
+
throw new Error("Subagent child cannot load project resources because the project is not trusted.");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function parseReadinessFrame(buffer, expectedToolCount) {
|
|
808
|
+
if (buffer.byteLength === 0) {
|
|
809
|
+
throw new Error("Subagent readiness pipe closed without a result.");
|
|
810
|
+
}
|
|
811
|
+
const text = buffer.toString("utf8");
|
|
812
|
+
const newline = text.indexOf("\n");
|
|
813
|
+
if (newline < 0 || newline !== text.length - 1) {
|
|
814
|
+
throw new Error("Subagent readiness pipe returned an invalid frame.");
|
|
815
|
+
}
|
|
816
|
+
let value;
|
|
817
|
+
try {
|
|
818
|
+
value = JSON.parse(text.slice(0, newline));
|
|
819
|
+
} catch {
|
|
820
|
+
throw new Error("Subagent readiness pipe returned malformed JSON.");
|
|
821
|
+
}
|
|
822
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
823
|
+
throw new Error("Subagent readiness pipe returned an invalid result.");
|
|
824
|
+
}
|
|
825
|
+
const record = value;
|
|
826
|
+
if (record.ok === true && Object.keys(record).length === 2 && Array.isArray(record.sources) && record.sources.length === expectedToolCount && record.sources.every((source) => typeof source === "string" && /^[a-f0-9]{64}$/u.test(source))) {
|
|
827
|
+
return { ok: true, sources: record.sources };
|
|
828
|
+
}
|
|
829
|
+
if (record.ok === false && Object.keys(record).length === 2 && typeof record.error === "string" && record.error.length > 0 && Buffer.byteLength(record.error, "utf8") <= MAX_ERROR_BYTES) {
|
|
830
|
+
return { ok: false, error: sanitizeTerminalText(record.error) };
|
|
831
|
+
}
|
|
832
|
+
throw new Error("Subagent readiness pipe returned an invalid result.");
|
|
833
|
+
}
|
|
834
|
+
function redactAttachmentPaths(value, request) {
|
|
835
|
+
return redactAttachmentPathVariants(value, attachmentPathVariants(request));
|
|
836
|
+
}
|
|
837
|
+
function redactAttachmentPathVariants(value, candidates) {
|
|
838
|
+
let redacted = value;
|
|
839
|
+
for (const candidate of candidates) redacted = redacted.replaceAll(candidate, "[attachment path]");
|
|
840
|
+
return redacted;
|
|
841
|
+
}
|
|
842
|
+
function attachmentPathCarryLength(value, candidates) {
|
|
843
|
+
const maximum = Math.min(
|
|
844
|
+
value.length,
|
|
845
|
+
candidates.reduce((length, candidate) => Math.max(length, candidate.length - 1), 0)
|
|
846
|
+
);
|
|
847
|
+
for (let length = maximum; length > 0; length--) {
|
|
848
|
+
const suffix = value.slice(-length);
|
|
849
|
+
if (candidates.some((candidate) => candidate.length > length && candidate.startsWith(suffix))) return length;
|
|
850
|
+
}
|
|
851
|
+
return 0;
|
|
852
|
+
}
|
|
853
|
+
function attachmentPathVariants(request) {
|
|
854
|
+
const variants = /* @__PURE__ */ new Set();
|
|
855
|
+
const paths = [...request.skills, ...request.extensions.map((extension) => extension.path)];
|
|
856
|
+
for (const resourcePath of paths) {
|
|
857
|
+
for (const spelling of [resourcePath, resourcePath.replaceAll("\\", "/"), resourcePath.replaceAll("/", "\\")]) {
|
|
858
|
+
if (!spelling) continue;
|
|
859
|
+
variants.add(spelling);
|
|
860
|
+
variants.add(JSON.stringify(spelling).slice(1, -1));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return [...variants].sort((left, right) => right.length - left.length);
|
|
864
|
+
}
|
|
865
|
+
function combineErrors(...messages) {
|
|
866
|
+
const unique = [...new Set(messages.filter((message) => Boolean(message)))];
|
|
867
|
+
return truncateText(unique.join("\n"), MAX_ERROR_BYTES);
|
|
868
|
+
}
|
|
581
869
|
function abortError(message) {
|
|
582
870
|
const error = new Error(message);
|
|
583
871
|
error.name = "AbortError";
|
|
@@ -663,6 +951,247 @@ var JsonLineDecoder = class {
|
|
|
663
951
|
}
|
|
664
952
|
};
|
|
665
953
|
|
|
954
|
+
// src/resource-attachments.ts
|
|
955
|
+
import { existsSync, statSync as statSync3 } from "node:fs";
|
|
956
|
+
import {
|
|
957
|
+
opendir as opendirAsync2,
|
|
958
|
+
readFile as readFileAsync,
|
|
959
|
+
realpath as realpathAsync2,
|
|
960
|
+
stat as statAsync2
|
|
961
|
+
} from "node:fs/promises";
|
|
962
|
+
import * as path3 from "node:path";
|
|
963
|
+
import { DefaultPackageManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
964
|
+
import ignore2 from "ignore";
|
|
965
|
+
|
|
966
|
+
// src/skill-attachments.ts
|
|
967
|
+
import { statSync as statSync2 } from "node:fs";
|
|
968
|
+
import { opendir as opendirAsync, realpath as realpathAsync, stat as statAsync } from "node:fs/promises";
|
|
969
|
+
import * as path2 from "node:path";
|
|
970
|
+
import { loadSkills } from "@earendil-works/pi-coding-agent";
|
|
971
|
+
import ignore from "ignore";
|
|
972
|
+
var MAX_SKILL_SCAN_DEPTH = 32;
|
|
973
|
+
var MAX_SKILL_SCAN_ENTRIES = 4096;
|
|
974
|
+
var MAX_SKILL_SCAN_BYTES = 4 * 1024 * 1024;
|
|
975
|
+
var MAX_SKILL_IGNORE_BYTES = 1024 * 1024;
|
|
976
|
+
async function assertLoadableSkills(skillPaths, cwd, canonicalCwd, projectTrusted, signal) {
|
|
977
|
+
const scanState = {
|
|
978
|
+
entries: 0,
|
|
979
|
+
skillBytes: 0,
|
|
980
|
+
ignoreBytes: 0,
|
|
981
|
+
ancestors: /* @__PURE__ */ new Set(),
|
|
982
|
+
signal
|
|
983
|
+
};
|
|
984
|
+
for (const skillPath of skillPaths) {
|
|
985
|
+
throwIfAttachmentAborted(signal);
|
|
986
|
+
const candidates = await collectBoundedSkillCandidates(skillPath, scanState);
|
|
987
|
+
throwIfAttachmentAborted(signal);
|
|
988
|
+
const result = loadExplicitSkills([skillPath], cwd);
|
|
989
|
+
if (result.skills.length === 0) {
|
|
990
|
+
throw new Error("Subagent skill path must contain at least one loadable Pi skill.");
|
|
991
|
+
}
|
|
992
|
+
assertNoUntrustedProjectSkills(result, cwd, canonicalCwd, projectTrusted);
|
|
993
|
+
assertNoOmittedSkillDiagnostics(result);
|
|
994
|
+
assertNoSkillNameCollisions(result.diagnostics);
|
|
995
|
+
await assertNoSilentlyOmittedSkills(skillPath, cwd, result, canonicalCwd, projectTrusted, candidates, signal);
|
|
996
|
+
}
|
|
997
|
+
if (skillPaths.length > 1) {
|
|
998
|
+
throwIfAttachmentAborted(signal);
|
|
999
|
+
const result = loadExplicitSkills(skillPaths, cwd);
|
|
1000
|
+
assertNoUntrustedProjectSkills(result, cwd, canonicalCwd, projectTrusted);
|
|
1001
|
+
assertNoSkillNameCollisions(result.diagnostics);
|
|
1002
|
+
}
|
|
1003
|
+
return scanState;
|
|
1004
|
+
}
|
|
1005
|
+
function loadExplicitSkills(skillPaths, cwd) {
|
|
1006
|
+
return loadSkills({ cwd, agentDir: cwd, skillPaths, includeDefaults: false });
|
|
1007
|
+
}
|
|
1008
|
+
function assertNoOmittedSkillDiagnostics(result) {
|
|
1009
|
+
const loadedPaths = new Set(result.skills.map((skill) => skill.filePath));
|
|
1010
|
+
const omitted = result.diagnostics.some(
|
|
1011
|
+
(diagnostic) => diagnostic.type === "error" || diagnostic.type === "warning" && (!diagnostic.path || !loadedPaths.has(diagnostic.path))
|
|
1012
|
+
);
|
|
1013
|
+
if (omitted) {
|
|
1014
|
+
throw new Error("Subagent skill attachment must not contain an invalid or unreadable declared skill.");
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function assertNoSkillNameCollisions(diagnostics) {
|
|
1018
|
+
if (diagnostics.some((diagnostic) => diagnostic.type === "collision" && diagnostic.collision?.resourceType === "skill")) {
|
|
1019
|
+
throw new Error("Subagent skill attachments must not contain duplicate skill names.");
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function assertNoUntrustedProjectSkills(result, cwd, canonicalCwd, projectTrusted) {
|
|
1023
|
+
if (projectTrusted) return;
|
|
1024
|
+
for (const skill of result.skills) {
|
|
1025
|
+
const lexicalPath = path2.resolve(skill.filePath);
|
|
1026
|
+
const canonicalPath = realpath(skill.filePath, "Loaded subagent skill");
|
|
1027
|
+
if (isWithin(cwd, lexicalPath) || isWithin(canonicalCwd, canonicalPath)) {
|
|
1028
|
+
throw new Error("Subagent skill cannot load a project path because the project is not trusted.");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async function assertNoSilentlyOmittedSkills(skillPath, cwd, result, canonicalCwd, projectTrusted, candidates, signal) {
|
|
1033
|
+
if (!statSync2(skillPath).isDirectory()) return;
|
|
1034
|
+
const loadedPaths = new Set(result.skills.map((skill) => realpath(skill.filePath, "Loaded subagent skill")));
|
|
1035
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
1036
|
+
if (index > 0 && index % 64 === 0) await new Promise((resolve4) => setImmediate(resolve4));
|
|
1037
|
+
throwIfAttachmentAborted(signal);
|
|
1038
|
+
const candidateResult = loadExplicitSkills([candidate], cwd);
|
|
1039
|
+
assertNoUntrustedProjectSkills(candidateResult, cwd, canonicalCwd, projectTrusted);
|
|
1040
|
+
if (candidateResult.skills.length === 0) {
|
|
1041
|
+
if (path2.basename(candidate) === "SKILL.md") throwInvalidDeclaredSkill();
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
const loaded = candidateResult.skills.some(
|
|
1045
|
+
(skill) => loadedPaths.has(realpath(skill.filePath, "Loaded subagent skill"))
|
|
1046
|
+
);
|
|
1047
|
+
if (!loaded) throwInvalidDeclaredSkill();
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async function collectBoundedSkillCandidates(skillPath, state) {
|
|
1051
|
+
throwIfAttachmentAborted(state.signal);
|
|
1052
|
+
const stats = await statAsync(skillPath);
|
|
1053
|
+
throwIfAttachmentAborted(state.signal);
|
|
1054
|
+
if (!stats.isDirectory()) {
|
|
1055
|
+
if (stats.isFile() && skillPath.endsWith(".md")) addSkillBytes(state, stats.size);
|
|
1056
|
+
return [];
|
|
1057
|
+
}
|
|
1058
|
+
return collectSkillCandidates(skillPath, true, ignore(), skillPath, 0, state);
|
|
1059
|
+
}
|
|
1060
|
+
async function collectSkillCandidates(directory, includeRootMarkdown, ignoreMatcher, rootDirectory, depth, state) {
|
|
1061
|
+
throwIfAttachmentAborted(state.signal);
|
|
1062
|
+
if (depth > MAX_SKILL_SCAN_DEPTH) throwSkillScanLimit();
|
|
1063
|
+
let canonicalDirectory;
|
|
1064
|
+
try {
|
|
1065
|
+
canonicalDirectory = await realpathAsync(directory);
|
|
1066
|
+
} catch {
|
|
1067
|
+
throwInvalidDeclaredSkill();
|
|
1068
|
+
}
|
|
1069
|
+
throwIfAttachmentAborted(state.signal);
|
|
1070
|
+
if (state.ancestors.has(canonicalDirectory)) {
|
|
1071
|
+
throw new Error("Subagent skill attachment must not contain a recursive directory link.");
|
|
1072
|
+
}
|
|
1073
|
+
state.ancestors.add(canonicalDirectory);
|
|
1074
|
+
try {
|
|
1075
|
+
await addAttachmentIgnoreRules(ignoreMatcher, directory, rootDirectory, state.signal, (_ignorePath, bytes) => {
|
|
1076
|
+
state.ignoreBytes += bytes;
|
|
1077
|
+
if (state.ignoreBytes > MAX_SKILL_IGNORE_BYTES) throwSkillScanLimit();
|
|
1078
|
+
});
|
|
1079
|
+
let directoryHandle;
|
|
1080
|
+
try {
|
|
1081
|
+
directoryHandle = await opendirAsync(directory);
|
|
1082
|
+
} catch {
|
|
1083
|
+
throwInvalidDeclaredSkill();
|
|
1084
|
+
}
|
|
1085
|
+
const entries = [];
|
|
1086
|
+
try {
|
|
1087
|
+
for await (const entry of directoryHandle) {
|
|
1088
|
+
throwIfAttachmentAborted(state.signal);
|
|
1089
|
+
state.entries++;
|
|
1090
|
+
if (state.entries > MAX_SKILL_SCAN_ENTRIES) throwSkillScanLimit();
|
|
1091
|
+
entries.push(entry);
|
|
1092
|
+
}
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (isAbortError(error) || isSkillScanLimitError(error)) throw error;
|
|
1095
|
+
throwInvalidDeclaredSkill();
|
|
1096
|
+
}
|
|
1097
|
+
const rootSkill = entries.find((entry) => entry.name === "SKILL.md");
|
|
1098
|
+
if (rootSkill) {
|
|
1099
|
+
const rootPath = path2.join(directory, rootSkill.name);
|
|
1100
|
+
const relativePath = toPosixPath(path2.relative(rootDirectory, rootPath));
|
|
1101
|
+
if (!ignoreMatcher.ignores(relativePath)) {
|
|
1102
|
+
let rootStats;
|
|
1103
|
+
try {
|
|
1104
|
+
rootStats = await statAsync(rootPath);
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
if (isAbortError(error)) throw error;
|
|
1107
|
+
throwInvalidDeclaredSkill();
|
|
1108
|
+
}
|
|
1109
|
+
throwIfAttachmentAborted(state.signal);
|
|
1110
|
+
if (rootStats.isFile()) {
|
|
1111
|
+
addSkillBytes(state, rootStats.size);
|
|
1112
|
+
return [rootPath];
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const candidates = [];
|
|
1117
|
+
for (const entry of entries) {
|
|
1118
|
+
throwIfAttachmentAborted(state.signal);
|
|
1119
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1120
|
+
const candidate = path2.join(directory, entry.name);
|
|
1121
|
+
const relativePath = toPosixPath(path2.relative(rootDirectory, candidate));
|
|
1122
|
+
let candidateStats;
|
|
1123
|
+
try {
|
|
1124
|
+
candidateStats = await statAsync(candidate);
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (isAbortError(error)) throw error;
|
|
1127
|
+
throwIfAttachmentAborted(state.signal);
|
|
1128
|
+
if (!ignoreMatcher.ignores(relativePath) && (entry.name === "SKILL.md" || includeRootMarkdown && entry.name.endsWith(".md"))) {
|
|
1129
|
+
throwInvalidDeclaredSkill();
|
|
1130
|
+
}
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
throwIfAttachmentAborted(state.signal);
|
|
1134
|
+
if (ignoreMatcher.ignores(candidateStats.isDirectory() ? `${relativePath}/` : relativePath)) continue;
|
|
1135
|
+
if (candidateStats.isDirectory()) {
|
|
1136
|
+
candidates.push(
|
|
1137
|
+
...await collectSkillCandidates(candidate, false, ignoreMatcher, rootDirectory, depth + 1, state)
|
|
1138
|
+
);
|
|
1139
|
+
} else if (candidateStats.isFile() && (entry.name === "SKILL.md" || includeRootMarkdown && entry.name.endsWith(".md"))) {
|
|
1140
|
+
addSkillBytes(state, candidateStats.size);
|
|
1141
|
+
candidates.push(candidate);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return candidates;
|
|
1145
|
+
} finally {
|
|
1146
|
+
state.ancestors.delete(canonicalDirectory);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function addSkillBytes(state, bytes) {
|
|
1150
|
+
state.skillBytes += bytes;
|
|
1151
|
+
if (state.skillBytes > MAX_SKILL_SCAN_BYTES) throwSkillScanLimit();
|
|
1152
|
+
}
|
|
1153
|
+
function isSkillScanLimitError(error) {
|
|
1154
|
+
return error instanceof Error && error.name === "SkillScanLimitError";
|
|
1155
|
+
}
|
|
1156
|
+
function throwSkillScanLimit() {
|
|
1157
|
+
const error = new Error(
|
|
1158
|
+
`Subagent skill attachment exceeds traversal limits (${MAX_SKILL_SCAN_ENTRIES} entries, depth ${MAX_SKILL_SCAN_DEPTH}, ${MAX_SKILL_SCAN_BYTES} skill bytes, or ${MAX_SKILL_IGNORE_BYTES} ignore bytes).`
|
|
1159
|
+
);
|
|
1160
|
+
error.name = "SkillScanLimitError";
|
|
1161
|
+
throw error;
|
|
1162
|
+
}
|
|
1163
|
+
function throwInvalidDeclaredSkill() {
|
|
1164
|
+
throw new Error("Subagent skill attachment must not contain an invalid or unreadable declared skill.");
|
|
1165
|
+
}
|
|
1166
|
+
async function assertLoadablePackageSkills(packageSkillPaths, explicitSkillPaths, cwd, scanState) {
|
|
1167
|
+
const uniquePackageSkillPaths = deduplicatePackageSkillPaths(packageSkillPaths, explicitSkillPaths);
|
|
1168
|
+
for (const [index, skillPath] of uniquePackageSkillPaths.entries()) {
|
|
1169
|
+
if (index > 0 && index % 64 === 0) await new Promise((resolve4) => setImmediate(resolve4));
|
|
1170
|
+
throwIfAttachmentAborted(scanState.signal);
|
|
1171
|
+
await collectBoundedSkillCandidates(skillPath, scanState);
|
|
1172
|
+
}
|
|
1173
|
+
for (const [index, skillPath] of uniquePackageSkillPaths.entries()) {
|
|
1174
|
+
if (index > 0 && index % 64 === 0) await new Promise((resolve4) => setImmediate(resolve4));
|
|
1175
|
+
throwIfAttachmentAborted(scanState.signal);
|
|
1176
|
+
const result = loadExplicitSkills([skillPath], cwd);
|
|
1177
|
+
if (result.skills.length === 0) throwInvalidDeclaredSkill();
|
|
1178
|
+
assertNoOmittedSkillDiagnostics(result);
|
|
1179
|
+
}
|
|
1180
|
+
throwIfAttachmentAborted(scanState.signal);
|
|
1181
|
+
assertNoSkillNameCollisions(loadExplicitSkills([...uniquePackageSkillPaths, ...explicitSkillPaths], cwd).diagnostics);
|
|
1182
|
+
}
|
|
1183
|
+
function deduplicatePackageSkillPaths(packageSkillPaths, explicitSkillPaths) {
|
|
1184
|
+
const seenPaths = new Set(explicitSkillPaths.map((skillPath) => realpath(skillPath, "Loaded subagent skill")));
|
|
1185
|
+
const uniquePaths = [];
|
|
1186
|
+
for (const skillPath of packageSkillPaths) {
|
|
1187
|
+
const canonicalPath = realpath(skillPath, "Loaded subagent package skill");
|
|
1188
|
+
if (seenPaths.has(canonicalPath)) continue;
|
|
1189
|
+
seenPaths.add(canonicalPath);
|
|
1190
|
+
uniquePaths.push(skillPath);
|
|
1191
|
+
}
|
|
1192
|
+
return uniquePaths;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
666
1195
|
// src/types.ts
|
|
667
1196
|
var TERMINAL_JOB_STATES = /* @__PURE__ */ new Set([
|
|
668
1197
|
"completed",
|
|
@@ -683,6 +1212,514 @@ var SUBAGENT_THINKING_LEVELS = [
|
|
|
683
1212
|
"max"
|
|
684
1213
|
];
|
|
685
1214
|
|
|
1215
|
+
// src/resource-attachments.ts
|
|
1216
|
+
var MAX_ATTACHED_SKILLS = 16;
|
|
1217
|
+
var MAX_ATTACHED_EXTENSIONS = 16;
|
|
1218
|
+
var MAX_SELECTED_TOOLS = 64;
|
|
1219
|
+
var MAX_RESOURCE_PATH_BYTES = 4 * 1024;
|
|
1220
|
+
var MAX_EXTENSION_TOOL_NAME_LENGTH = 128;
|
|
1221
|
+
var MAX_EXTENSION_SCAN_DEPTH = 32;
|
|
1222
|
+
var MAX_EXTENSION_SCAN_ENTRIES = 4096;
|
|
1223
|
+
var MAX_EXTENSION_METADATA_BYTES = 1024 * 1024;
|
|
1224
|
+
var RESERVED_CHILD_TOOL_NAMES = /* @__PURE__ */ new Set([...CHILD_CORE_TOOL_NAMES, ...CHILD_COMMUNICATION_TOOL_NAMES]);
|
|
1225
|
+
async function resolveResourceAttachments(input, options) {
|
|
1226
|
+
const cwd = path3.resolve(options.cwd);
|
|
1227
|
+
const canonicalCwd = realpath(cwd, "Subagent working directory");
|
|
1228
|
+
const skillInputs = optionalArray(input.skills, "skills", MAX_ATTACHED_SKILLS);
|
|
1229
|
+
const extensionInputs = optionalArray(input.extensions, "extensions", MAX_ATTACHED_EXTENSIONS);
|
|
1230
|
+
const skills = [];
|
|
1231
|
+
const seenSkills = /* @__PURE__ */ new Set();
|
|
1232
|
+
for (const candidate of skillInputs) {
|
|
1233
|
+
const resolved = resolveResourcePath(candidate, "skill", cwd, canonicalCwd, options.projectTrusted);
|
|
1234
|
+
if (!seenSkills.has(resolved)) {
|
|
1235
|
+
seenSkills.add(resolved);
|
|
1236
|
+
skills.push(resolved);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
const skillScanState = await assertLoadableSkills(skills, cwd, canonicalCwd, options.projectTrusted, options.signal);
|
|
1240
|
+
const extensions = [];
|
|
1241
|
+
const extensionsByPath = /* @__PURE__ */ new Map();
|
|
1242
|
+
const entrypointsByPath = /* @__PURE__ */ new Map();
|
|
1243
|
+
const packageSkillPaths = [];
|
|
1244
|
+
for (const candidate of extensionInputs) {
|
|
1245
|
+
if (!isRecord(candidate) || Object.keys(candidate).some((key) => key !== "path" && key !== "tools")) {
|
|
1246
|
+
throw new Error("Each subagent extension must contain only path and tools.");
|
|
1247
|
+
}
|
|
1248
|
+
const resolved = resolveResourcePath(candidate.path, "extension", cwd, canonicalCwd, options.projectTrusted);
|
|
1249
|
+
const toolInputs = optionalArray(candidate.tools, "extension tools", MAX_SELECTED_TOOLS, false);
|
|
1250
|
+
const tools = toolInputs.map(resolveExtensionToolName);
|
|
1251
|
+
for (const tool of tools) {
|
|
1252
|
+
if (RESERVED_CHILD_TOOL_NAMES.has(tool)) {
|
|
1253
|
+
throw new Error(`Subagent extension tool name conflicts with the built-in ${tool} tool.`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
let attachment = extensionsByPath.get(resolved);
|
|
1257
|
+
if (!attachment) {
|
|
1258
|
+
const resources = await assertResolvableExtensionAttachment(
|
|
1259
|
+
resolved,
|
|
1260
|
+
cwd,
|
|
1261
|
+
canonicalCwd,
|
|
1262
|
+
options.projectTrusted,
|
|
1263
|
+
options.signal
|
|
1264
|
+
);
|
|
1265
|
+
packageSkillPaths.push(...resources.skills);
|
|
1266
|
+
entrypointsByPath.set(resolved, resources.entrypoints);
|
|
1267
|
+
attachment = { path: resolved, tools: [] };
|
|
1268
|
+
extensionsByPath.set(resolved, attachment);
|
|
1269
|
+
extensions.push(attachment);
|
|
1270
|
+
}
|
|
1271
|
+
for (const tool of tools) {
|
|
1272
|
+
if (!attachment.tools.includes(tool)) attachment.tools.push(tool);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (packageSkillPaths.length > 0) {
|
|
1276
|
+
await assertLoadablePackageSkills(packageSkillPaths, skills, cwd, skillScanState);
|
|
1277
|
+
}
|
|
1278
|
+
const effectiveTools = [.../* @__PURE__ */ new Set([...options.coreTools, ...extensions.flatMap((extension) => extension.tools)])];
|
|
1279
|
+
if (effectiveTools.length > MAX_SELECTED_TOOLS) {
|
|
1280
|
+
throw new Error(`Subagent jobs may select at most ${MAX_SELECTED_TOOLS} total tools.`);
|
|
1281
|
+
}
|
|
1282
|
+
const toolSources = /* @__PURE__ */ new Map();
|
|
1283
|
+
const firstSourceIdByCanonicalPath = /* @__PURE__ */ new Map();
|
|
1284
|
+
for (const extension of extensions) {
|
|
1285
|
+
const sourceIds = (entrypointsByPath.get(extension.path) ?? []).map((entrypoint) => {
|
|
1286
|
+
const canonicalPath = realpath(entrypoint, "Subagent extension entrypoint");
|
|
1287
|
+
const sourceId = firstSourceIdByCanonicalPath.get(canonicalPath) ?? toolSourceId(entrypoint);
|
|
1288
|
+
firstSourceIdByCanonicalPath.set(canonicalPath, sourceId);
|
|
1289
|
+
return sourceId;
|
|
1290
|
+
});
|
|
1291
|
+
for (const tool of extension.tools) {
|
|
1292
|
+
if (toolSources.has(tool)) {
|
|
1293
|
+
throw new Error(`Subagent extension tool ${tool} is requested by multiple attachments.`);
|
|
1294
|
+
}
|
|
1295
|
+
toolSources.set(tool, sourceIds);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return { skills, extensions, effectiveTools, toolSources: Object.fromEntries(toolSources) };
|
|
1299
|
+
}
|
|
1300
|
+
function optionalArray(value, field, maxItems, optional = true) {
|
|
1301
|
+
if (value === void 0 && optional) return [];
|
|
1302
|
+
if (!Array.isArray(value)) throw new Error(`Subagent ${field} must be an array.`);
|
|
1303
|
+
if (value.length > maxItems) throw new Error(`Subagent ${field} may contain at most ${maxItems} entries.`);
|
|
1304
|
+
return value;
|
|
1305
|
+
}
|
|
1306
|
+
function resolveResourcePath(value, kind, cwd, canonicalCwd, projectTrusted) {
|
|
1307
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Subagent ${kind} path is required.`);
|
|
1308
|
+
const resourcePath = value.trim();
|
|
1309
|
+
if (hasControlCharacter(resourcePath)) throw new Error(`Subagent ${kind} path must not contain control characters.`);
|
|
1310
|
+
if (Buffer.byteLength(resourcePath, "utf8") > MAX_RESOURCE_PATH_BYTES) {
|
|
1311
|
+
throw new Error(`Subagent ${kind} path must be at most ${MAX_RESOURCE_PATH_BYTES} UTF-8 bytes.`);
|
|
1312
|
+
}
|
|
1313
|
+
if (!isLocalPath(resourcePath)) throw new Error(`Subagent ${kind} must use a local path.`);
|
|
1314
|
+
const lexicalPath = path3.resolve(cwd, resourcePath);
|
|
1315
|
+
const canonicalPath = realpath(lexicalPath, `Subagent ${kind}`);
|
|
1316
|
+
const stats = statSync3(canonicalPath);
|
|
1317
|
+
if (!stats.isFile() && !stats.isDirectory()) {
|
|
1318
|
+
throw new Error(`Subagent ${kind} path must reference a file or directory.`);
|
|
1319
|
+
}
|
|
1320
|
+
if (!projectTrusted && (isWithin(cwd, lexicalPath) || isWithin(canonicalCwd, canonicalPath))) {
|
|
1321
|
+
throw new Error(`Subagent ${kind} cannot load a project path because the project is not trusted.`);
|
|
1322
|
+
}
|
|
1323
|
+
return canonicalPath;
|
|
1324
|
+
}
|
|
1325
|
+
async function assertResolvableExtensionAttachment(extensionPath, cwd, canonicalCwd, projectTrusted, signal) {
|
|
1326
|
+
await inspectExtensionAttachment(extensionPath, cwd, canonicalCwd, projectTrusted, signal);
|
|
1327
|
+
throwIfAttachmentAborted(signal);
|
|
1328
|
+
const resolved = await resolveAttachedExtensionResources(extensionPath, cwd);
|
|
1329
|
+
throwIfAttachmentAborted(signal);
|
|
1330
|
+
const entrypoints = enabledResourcePaths(resolved.extensions);
|
|
1331
|
+
if (entrypoints.length === 0) {
|
|
1332
|
+
throw new Error("Subagent extension directory must contain at least one loadable Pi extension entrypoint.");
|
|
1333
|
+
}
|
|
1334
|
+
const loadPaths = entrypoints.flatMap(resolveDirectExtensionLoadPaths);
|
|
1335
|
+
for (const entrypoint of loadPaths) {
|
|
1336
|
+
assertTrustedResolvedPath(entrypoint, "extension entrypoint", cwd, canonicalCwd, projectTrusted);
|
|
1337
|
+
}
|
|
1338
|
+
const packageSkills = enabledResourcePaths(resolved.skills);
|
|
1339
|
+
const packageResources = [
|
|
1340
|
+
packageSkills,
|
|
1341
|
+
enabledResourcePaths(resolved.prompts),
|
|
1342
|
+
enabledResourcePaths(resolved.themes)
|
|
1343
|
+
].flat();
|
|
1344
|
+
for (const resource of packageResources) {
|
|
1345
|
+
assertTrustedResolvedPath(resource, "extension package resource", cwd, canonicalCwd, projectTrusted);
|
|
1346
|
+
}
|
|
1347
|
+
return { skills: packageSkills, entrypoints: loadPaths };
|
|
1348
|
+
}
|
|
1349
|
+
function resolveDirectExtensionLoadPaths(entrypoint) {
|
|
1350
|
+
if (!statSync3(entrypoint).isDirectory()) return [entrypoint];
|
|
1351
|
+
for (const filename of ["index.ts", "index.js"]) {
|
|
1352
|
+
const indexPath = path3.join(entrypoint, filename);
|
|
1353
|
+
if (existsSync(indexPath) && statSync3(indexPath).isFile()) return [entrypoint, indexPath];
|
|
1354
|
+
}
|
|
1355
|
+
throwUnresolvableExtensionEntrypoint();
|
|
1356
|
+
}
|
|
1357
|
+
async function resolveAttachedExtensionResources(extensionPath, cwd) {
|
|
1358
|
+
const packageManager = new DefaultPackageManager({
|
|
1359
|
+
cwd,
|
|
1360
|
+
agentDir: cwd,
|
|
1361
|
+
settingsManager: SettingsManager.inMemory()
|
|
1362
|
+
});
|
|
1363
|
+
return packageManager.resolveExtensionSources([extensionPath], { temporary: true });
|
|
1364
|
+
}
|
|
1365
|
+
function enabledResourcePaths(resources) {
|
|
1366
|
+
return resources.filter((resource) => resource.enabled).map((resource) => resource.path);
|
|
1367
|
+
}
|
|
1368
|
+
function assertTrustedResolvedPath(resourcePath, label, cwd, canonicalCwd, projectTrusted) {
|
|
1369
|
+
const canonicalPath = realpath(resourcePath, `Subagent ${label}`);
|
|
1370
|
+
const stats = statSync3(canonicalPath);
|
|
1371
|
+
if (!stats.isFile() && !stats.isDirectory()) {
|
|
1372
|
+
throw new Error(`Subagent ${label} must reference a file or directory.`);
|
|
1373
|
+
}
|
|
1374
|
+
assertProjectPathTrusted(resourcePath, canonicalPath, label, cwd, canonicalCwd, projectTrusted);
|
|
1375
|
+
}
|
|
1376
|
+
function assertProjectPathTrusted(lexicalPath, canonicalPath, label, cwd, canonicalCwd, projectTrusted) {
|
|
1377
|
+
if (!projectTrusted && (isWithin(cwd, path3.resolve(lexicalPath)) || isWithin(canonicalCwd, canonicalPath))) {
|
|
1378
|
+
throw new Error(`Subagent ${label} cannot load a project path because the project is not trusted.`);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
async function inspectExtensionAttachment(extensionPath, cwd, canonicalCwd, projectTrusted, signal) {
|
|
1382
|
+
if (!statSync3(extensionPath).isDirectory()) return;
|
|
1383
|
+
const state = {
|
|
1384
|
+
entries: 0,
|
|
1385
|
+
metadataBytes: 0,
|
|
1386
|
+
ancestors: /* @__PURE__ */ new Set(),
|
|
1387
|
+
cwd,
|
|
1388
|
+
canonicalCwd,
|
|
1389
|
+
projectTrusted,
|
|
1390
|
+
signal
|
|
1391
|
+
};
|
|
1392
|
+
const inspected = await inspectPiManifest(extensionPath, state);
|
|
1393
|
+
if (inspected.hasPiManifest) {
|
|
1394
|
+
await inspectDeclaredExtensionEntries(extensionPath, inspected.manifest?.extensions, true, state);
|
|
1395
|
+
for (const resourceType of ["skills", "prompts", "themes"]) {
|
|
1396
|
+
await inspectDeclaredPackageResourceEntries(
|
|
1397
|
+
extensionPath,
|
|
1398
|
+
inspected.manifest?.[resourceType],
|
|
1399
|
+
resourceType,
|
|
1400
|
+
state
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const resourceDirectories = ["extensions", "skills", "prompts", "themes"].map(
|
|
1406
|
+
(name) => path3.join(extensionPath, name)
|
|
1407
|
+
);
|
|
1408
|
+
const resourceStats = await Promise.all(resourceDirectories.map((directory) => statIfPresent(directory)));
|
|
1409
|
+
if (resourceStats.some((stats) => stats?.isDirectory())) {
|
|
1410
|
+
if (resourceStats[0]?.isDirectory()) await inspectAutoExtensionDirectory(resourceDirectories[0], true, state);
|
|
1411
|
+
for (const [index, resourceType] of ["skills", "prompts", "themes"].entries()) {
|
|
1412
|
+
if (resourceStats[index + 1]?.isDirectory()) {
|
|
1413
|
+
await inspectPackageResourceDirectory(resourceDirectories[index + 1], resourceType, state);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
await inspectAutoExtensionDirectory(extensionPath, true, state);
|
|
1419
|
+
}
|
|
1420
|
+
async function inspectAutoExtensionDirectory(directory, discoverContents, state, enforceTrust = true) {
|
|
1421
|
+
throwIfAttachmentAborted(state.signal);
|
|
1422
|
+
if (enforceTrust) assertExtensionPackagePathTrusted(directory, "extension entrypoint", state);
|
|
1423
|
+
const inspected = await inspectPiManifest(directory, state, enforceTrust);
|
|
1424
|
+
const sourceEntries = inspected.manifest?.extensions?.filter((entrypoint) => !isExtensionOverridePattern(entrypoint));
|
|
1425
|
+
if (sourceEntries && sourceEntries.length > 0) {
|
|
1426
|
+
await inspectDeclaredExtensionEntries(directory, inspected.manifest?.extensions, false, state);
|
|
1427
|
+
return true;
|
|
1428
|
+
}
|
|
1429
|
+
if (await hasRegularExtensionIndex(directory, state, enforceTrust)) return true;
|
|
1430
|
+
if (!discoverContents) return false;
|
|
1431
|
+
const ignoreMatcher = ignore2();
|
|
1432
|
+
await addPackageIgnoreRules(ignoreMatcher, directory, directory, state, enforceTrust);
|
|
1433
|
+
const entries = await readBoundedPackageEntries(directory, state, "reject");
|
|
1434
|
+
let found = false;
|
|
1435
|
+
for (const entry of entries) {
|
|
1436
|
+
throwIfAttachmentAborted(state.signal);
|
|
1437
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1438
|
+
const entryPath = path3.join(directory, entry.name);
|
|
1439
|
+
const entryStats = await statIfPresent(entryPath);
|
|
1440
|
+
if (!entryStats) continue;
|
|
1441
|
+
const relativePath = toPosixPath(path3.relative(directory, entryPath));
|
|
1442
|
+
if (ignoreMatcher.ignores(entryStats.isDirectory() ? `${relativePath}/` : relativePath)) continue;
|
|
1443
|
+
if (entryStats.isFile()) {
|
|
1444
|
+
if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".js")) continue;
|
|
1445
|
+
if (enforceTrust) assertExtensionPackagePathTrusted(entryPath, "extension entrypoint", state);
|
|
1446
|
+
found = true;
|
|
1447
|
+
} else if (entryStats.isDirectory()) {
|
|
1448
|
+
if (enforceTrust) assertExtensionPackagePathTrusted(entryPath, "extension entrypoint", state);
|
|
1449
|
+
if (await inspectAutoExtensionDirectory(entryPath, false, state, enforceTrust)) found = true;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return found;
|
|
1453
|
+
}
|
|
1454
|
+
async function inspectDeclaredExtensionEntries(directory, entries, expandDirectories, state) {
|
|
1455
|
+
if (!entries) return;
|
|
1456
|
+
for (const entrypoint of entries) {
|
|
1457
|
+
throwIfAttachmentAborted(state.signal);
|
|
1458
|
+
state.entries++;
|
|
1459
|
+
if (state.entries > MAX_EXTENSION_SCAN_ENTRIES) throwExtensionScanLimit();
|
|
1460
|
+
if (isExtensionOverridePattern(entrypoint)) continue;
|
|
1461
|
+
if (hasExtensionGlob(entrypoint)) {
|
|
1462
|
+
throw new Error("Subagent extension package must not contain glob entrypoint declarations.");
|
|
1463
|
+
}
|
|
1464
|
+
const resolved = path3.resolve(directory, entrypoint);
|
|
1465
|
+
const resolvedStats = await statIfPresent(resolved);
|
|
1466
|
+
if (!resolvedStats) throwUnresolvableExtensionEntrypoint();
|
|
1467
|
+
if (resolvedStats.isFile()) continue;
|
|
1468
|
+
if (!resolvedStats.isDirectory()) throwUnresolvableExtensionEntrypoint();
|
|
1469
|
+
if (expandDirectories) {
|
|
1470
|
+
if (!await inspectAutoExtensionDirectory(resolved, true, state, false)) throwUnresolvableExtensionEntrypoint();
|
|
1471
|
+
} else if (!await hasRegularExtensionIndex(resolved, state, false)) {
|
|
1472
|
+
throwUnresolvableExtensionEntrypoint();
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
async function inspectDeclaredPackageResourceEntries(directory, entries, resourceType, state) {
|
|
1477
|
+
if (!entries) return;
|
|
1478
|
+
for (const entrypoint of entries) {
|
|
1479
|
+
throwIfAttachmentAborted(state.signal);
|
|
1480
|
+
state.entries++;
|
|
1481
|
+
if (state.entries > MAX_EXTENSION_SCAN_ENTRIES) throwExtensionScanLimit();
|
|
1482
|
+
if (isExtensionOverridePattern(entrypoint)) continue;
|
|
1483
|
+
if (hasExtensionGlob(entrypoint)) {
|
|
1484
|
+
throw new Error("Subagent extension package must not contain glob resource declarations.");
|
|
1485
|
+
}
|
|
1486
|
+
const resolved = path3.resolve(directory, entrypoint);
|
|
1487
|
+
const resolvedStats = await statIfPresent(resolved);
|
|
1488
|
+
throwIfAttachmentAborted(state.signal);
|
|
1489
|
+
if (resourceType === "skills" && (!resolvedStats || !resolvedStats.isFile() && !resolvedStats.isDirectory())) {
|
|
1490
|
+
throw new Error("Subagent extension package must not contain a missing or unreadable declared skill.");
|
|
1491
|
+
}
|
|
1492
|
+
if (resolvedStats?.isDirectory()) await inspectPackageResourceDirectory(resolved, resourceType, state, false);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
async function inspectPackageResourceDirectory(directory, resourceType, state, enforceTrust = true) {
|
|
1496
|
+
await inspectPackageResourceTree(directory, resourceType, ignore2(), directory, 0, state, enforceTrust);
|
|
1497
|
+
}
|
|
1498
|
+
async function inspectPackageResourceTree(directory, resourceType, ignoreMatcher, rootDirectory, depth, state, enforceTrust) {
|
|
1499
|
+
const canonicalDirectory = await enterPackageResourceDirectory(directory, resourceType, depth, state, enforceTrust);
|
|
1500
|
+
if (!canonicalDirectory) return;
|
|
1501
|
+
try {
|
|
1502
|
+
await addPackageIgnoreRules(ignoreMatcher, directory, rootDirectory, state, enforceTrust);
|
|
1503
|
+
const entries = await readBoundedPackageEntries(
|
|
1504
|
+
directory,
|
|
1505
|
+
state,
|
|
1506
|
+
resourceType === "skills" ? "reject-skill" : "skip"
|
|
1507
|
+
);
|
|
1508
|
+
if (resourceType === "skills") {
|
|
1509
|
+
const rootSkill = entries.find((entry) => entry.name === "SKILL.md");
|
|
1510
|
+
if (rootSkill) {
|
|
1511
|
+
const skillPath = path3.join(directory, rootSkill.name);
|
|
1512
|
+
const skillStats = await statIfPresent(skillPath);
|
|
1513
|
+
const relativePath = toPosixPath(path3.relative(rootDirectory, skillPath));
|
|
1514
|
+
if (skillStats?.isFile() && !ignoreMatcher.ignores(relativePath)) {
|
|
1515
|
+
if (enforceTrust) assertExtensionPackagePathTrusted(skillPath, "extension package resource", state);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
for (const entry of entries) {
|
|
1521
|
+
throwIfAttachmentAborted(state.signal);
|
|
1522
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1523
|
+
const entryPath = path3.join(directory, entry.name);
|
|
1524
|
+
const relativePath = toPosixPath(path3.relative(rootDirectory, entryPath));
|
|
1525
|
+
const entryStats = await statIfPresent(entryPath);
|
|
1526
|
+
throwIfAttachmentAborted(state.signal);
|
|
1527
|
+
const isSkillCandidate = resourceType === "skills" && (entry.name === "SKILL.md" || directory === rootDirectory && entry.name.endsWith(".md"));
|
|
1528
|
+
if (isSkillCandidate && (!entryStats || !entryStats.isFile() && !entryStats.isDirectory()) && !ignoreMatcher.ignores(relativePath) && !ignoreMatcher.ignores(`${relativePath}/`)) {
|
|
1529
|
+
throw new Error("Subagent extension package must not contain an invalid or unreadable declared skill.");
|
|
1530
|
+
}
|
|
1531
|
+
if (!entryStats) continue;
|
|
1532
|
+
if (ignoreMatcher.ignores(entryStats.isDirectory() ? `${relativePath}/` : relativePath)) continue;
|
|
1533
|
+
if (entryStats.isFile()) {
|
|
1534
|
+
const isResourceFile = entry.name.endsWith(resourceType === "themes" ? ".json" : ".md") && (resourceType !== "skills" || directory === rootDirectory);
|
|
1535
|
+
if (enforceTrust && isResourceFile) {
|
|
1536
|
+
assertExtensionPackagePathTrusted(entryPath, "extension package resource", state);
|
|
1537
|
+
}
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (!entryStats.isDirectory()) continue;
|
|
1541
|
+
if (enforceTrust) assertExtensionPackagePathTrusted(entryPath, "extension package resource", state);
|
|
1542
|
+
await inspectPackageResourceTree(
|
|
1543
|
+
entryPath,
|
|
1544
|
+
resourceType,
|
|
1545
|
+
ignoreMatcher,
|
|
1546
|
+
rootDirectory,
|
|
1547
|
+
depth + 1,
|
|
1548
|
+
state,
|
|
1549
|
+
enforceTrust
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
} finally {
|
|
1553
|
+
state.ancestors.delete(canonicalDirectory);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
async function enterPackageResourceDirectory(directory, resourceType, depth, state, enforceTrust) {
|
|
1557
|
+
throwIfAttachmentAborted(state.signal);
|
|
1558
|
+
if (depth > MAX_EXTENSION_SCAN_DEPTH) throwExtensionScanLimit();
|
|
1559
|
+
let canonicalDirectory;
|
|
1560
|
+
try {
|
|
1561
|
+
canonicalDirectory = await realpathAsync2(directory);
|
|
1562
|
+
} catch {
|
|
1563
|
+
throwIfAttachmentAborted(state.signal);
|
|
1564
|
+
if (resourceType === "skills") throwUnreadablePackageSkillDirectory();
|
|
1565
|
+
return void 0;
|
|
1566
|
+
}
|
|
1567
|
+
throwIfAttachmentAborted(state.signal);
|
|
1568
|
+
if (enforceTrust) {
|
|
1569
|
+
assertProjectPathTrusted(
|
|
1570
|
+
directory,
|
|
1571
|
+
canonicalDirectory,
|
|
1572
|
+
"extension package resource",
|
|
1573
|
+
state.cwd,
|
|
1574
|
+
state.canonicalCwd,
|
|
1575
|
+
state.projectTrusted
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
if (state.ancestors.has(canonicalDirectory)) {
|
|
1579
|
+
throw new Error("Subagent extension package must not contain a recursive resource directory link.");
|
|
1580
|
+
}
|
|
1581
|
+
state.ancestors.add(canonicalDirectory);
|
|
1582
|
+
return canonicalDirectory;
|
|
1583
|
+
}
|
|
1584
|
+
async function inspectPiManifest(directory, state, enforceTrust = true) {
|
|
1585
|
+
const manifestPath = path3.join(directory, "package.json");
|
|
1586
|
+
const manifestStats = await statIfPresent(manifestPath);
|
|
1587
|
+
if (!manifestStats) return { hasPiManifest: false };
|
|
1588
|
+
if (!manifestStats.isFile()) {
|
|
1589
|
+
throw new Error("Subagent extension package manifest must be a regular file.");
|
|
1590
|
+
}
|
|
1591
|
+
if (enforceTrust) assertExtensionPackagePathTrusted(manifestPath, "extension package resource", state);
|
|
1592
|
+
state.metadataBytes += manifestStats.size;
|
|
1593
|
+
if (state.metadataBytes > MAX_EXTENSION_METADATA_BYTES) throwExtensionScanLimit();
|
|
1594
|
+
let document;
|
|
1595
|
+
try {
|
|
1596
|
+
document = JSON.parse(
|
|
1597
|
+
(await readFileAsync(manifestPath, { encoding: "utf8", signal: state.signal })).replace(/^\uFEFF/u, "")
|
|
1598
|
+
);
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
if (isAbortError(error)) throw error;
|
|
1601
|
+
throwIfAttachmentAborted(state.signal);
|
|
1602
|
+
return { hasPiManifest: false };
|
|
1603
|
+
}
|
|
1604
|
+
if (!isRecord(document) || !isRecord(document.pi)) return { hasPiManifest: false };
|
|
1605
|
+
return {
|
|
1606
|
+
hasPiManifest: true,
|
|
1607
|
+
manifest: {
|
|
1608
|
+
extensions: readManifestEntries(document.pi, "extensions"),
|
|
1609
|
+
skills: readManifestEntries(document.pi, "skills"),
|
|
1610
|
+
prompts: readManifestEntries(document.pi, "prompts"),
|
|
1611
|
+
themes: readManifestEntries(document.pi, "themes")
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
function readManifestEntries(manifest, resourceType) {
|
|
1616
|
+
const entries = manifest[resourceType];
|
|
1617
|
+
return Array.isArray(entries) && entries.every((entrypoint) => typeof entrypoint === "string") ? entries : void 0;
|
|
1618
|
+
}
|
|
1619
|
+
async function addPackageIgnoreRules(ignoreMatcher, directory, rootDirectory, state, enforceTrust) {
|
|
1620
|
+
await addAttachmentIgnoreRules(ignoreMatcher, directory, rootDirectory, state.signal, (ignorePath, bytes) => {
|
|
1621
|
+
if (enforceTrust) assertExtensionPackagePathTrusted(ignorePath, "extension package resource", state);
|
|
1622
|
+
state.metadataBytes += bytes;
|
|
1623
|
+
if (state.metadataBytes > MAX_EXTENSION_METADATA_BYTES) throwExtensionScanLimit();
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
async function readBoundedPackageEntries(directory, state, onFailure) {
|
|
1627
|
+
let directoryHandle;
|
|
1628
|
+
try {
|
|
1629
|
+
directoryHandle = await opendirAsync2(directory);
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
if (isAbortError(error)) throw error;
|
|
1632
|
+
throwIfAttachmentAborted(state.signal);
|
|
1633
|
+
return failedPackageDirectoryRead(onFailure);
|
|
1634
|
+
}
|
|
1635
|
+
const entries = [];
|
|
1636
|
+
try {
|
|
1637
|
+
for await (const entry of directoryHandle) {
|
|
1638
|
+
throwIfAttachmentAborted(state.signal);
|
|
1639
|
+
state.entries++;
|
|
1640
|
+
if (state.entries > MAX_EXTENSION_SCAN_ENTRIES) throwExtensionScanLimit();
|
|
1641
|
+
entries.push(entry);
|
|
1642
|
+
}
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
if (isAbortError(error) || isExtensionScanLimitError(error)) throw error;
|
|
1645
|
+
throwIfAttachmentAborted(state.signal);
|
|
1646
|
+
return failedPackageDirectoryRead(onFailure);
|
|
1647
|
+
}
|
|
1648
|
+
throwIfAttachmentAborted(state.signal);
|
|
1649
|
+
return entries;
|
|
1650
|
+
}
|
|
1651
|
+
function failedPackageDirectoryRead(onFailure) {
|
|
1652
|
+
if (onFailure === "reject") throwUnresolvableExtensionEntrypoint();
|
|
1653
|
+
if (onFailure === "reject-skill") throwUnreadablePackageSkillDirectory();
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
function throwUnreadablePackageSkillDirectory() {
|
|
1657
|
+
throw new Error("Subagent extension package must not contain an unreadable declared skill directory.");
|
|
1658
|
+
}
|
|
1659
|
+
async function hasRegularExtensionIndex(directory, state, enforceTrust = true) {
|
|
1660
|
+
for (const filename of ["index.ts", "index.js"]) {
|
|
1661
|
+
const indexPath = path3.join(directory, filename);
|
|
1662
|
+
const stats = await statIfPresent(indexPath);
|
|
1663
|
+
if (stats) {
|
|
1664
|
+
if (enforceTrust) assertExtensionPackagePathTrusted(indexPath, "extension entrypoint", state);
|
|
1665
|
+
return stats.isFile();
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return false;
|
|
1669
|
+
}
|
|
1670
|
+
function assertExtensionPackagePathTrusted(resourcePath, label, state) {
|
|
1671
|
+
if (state.projectTrusted) return;
|
|
1672
|
+
const canonicalPath = realpath(resourcePath, `Subagent ${label}`);
|
|
1673
|
+
assertProjectPathTrusted(resourcePath, canonicalPath, label, state.cwd, state.canonicalCwd, state.projectTrusted);
|
|
1674
|
+
}
|
|
1675
|
+
async function statIfPresent(value) {
|
|
1676
|
+
try {
|
|
1677
|
+
return await statAsync2(value);
|
|
1678
|
+
} catch {
|
|
1679
|
+
return void 0;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
function throwUnresolvableExtensionEntrypoint() {
|
|
1683
|
+
throw new Error("Subagent extension package must not contain a missing or unresolvable declared entrypoint.");
|
|
1684
|
+
}
|
|
1685
|
+
function isExtensionScanLimitError(error) {
|
|
1686
|
+
return error instanceof Error && error.name === "ExtensionScanLimitError";
|
|
1687
|
+
}
|
|
1688
|
+
function throwExtensionScanLimit() {
|
|
1689
|
+
const error = new Error(
|
|
1690
|
+
`Subagent extension attachment exceeds preflight limits (${MAX_EXTENSION_SCAN_ENTRIES} entries, depth ${MAX_EXTENSION_SCAN_DEPTH}, or ${MAX_EXTENSION_METADATA_BYTES} metadata bytes).`
|
|
1691
|
+
);
|
|
1692
|
+
error.name = "ExtensionScanLimitError";
|
|
1693
|
+
throw error;
|
|
1694
|
+
}
|
|
1695
|
+
function isExtensionOverridePattern(value) {
|
|
1696
|
+
return value.startsWith("!") || value.startsWith("+") || value.startsWith("-");
|
|
1697
|
+
}
|
|
1698
|
+
function hasExtensionGlob(value) {
|
|
1699
|
+
return value.includes("*") || value.includes("?");
|
|
1700
|
+
}
|
|
1701
|
+
function resolveExtensionToolName(value) {
|
|
1702
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("Subagent extension tool name is required.");
|
|
1703
|
+
const name = value.trim();
|
|
1704
|
+
if (name.length > MAX_EXTENSION_TOOL_NAME_LENGTH || name.includes(",") || hasControlCharacter(name)) {
|
|
1705
|
+
throw new Error(
|
|
1706
|
+
`Subagent extension tool name must be at most ${MAX_EXTENSION_TOOL_NAME_LENGTH} characters without commas or control characters.`
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
return name;
|
|
1710
|
+
}
|
|
1711
|
+
function isLocalPath(value) {
|
|
1712
|
+
if (value.startsWith("//") || value.startsWith("\\\\")) return false;
|
|
1713
|
+
if (path3.isAbsolute(value) || path3.win32.isAbsolute(value)) return true;
|
|
1714
|
+
return !/^[a-z][a-z0-9+.-]*:/iu.test(value);
|
|
1715
|
+
}
|
|
1716
|
+
function hasControlCharacter(value) {
|
|
1717
|
+
return [...value].some((character) => {
|
|
1718
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
1719
|
+
return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
|
|
686
1723
|
// src/runtime.ts
|
|
687
1724
|
var MAX_ACTIVE_JOBS = 8;
|
|
688
1725
|
var MAX_RETAINED_TERMINAL_JOBS = 32;
|
|
@@ -714,6 +1751,9 @@ var SubagentRuntime = class {
|
|
|
714
1751
|
this.sessionActive = true;
|
|
715
1752
|
this.notifyJobsChanged();
|
|
716
1753
|
}
|
|
1754
|
+
getSessionGeneration() {
|
|
1755
|
+
return this.generation;
|
|
1756
|
+
}
|
|
717
1757
|
subscribeJobs(listener) {
|
|
718
1758
|
this.jobListeners.add(listener);
|
|
719
1759
|
return () => this.jobListeners.delete(listener);
|
|
@@ -744,24 +1784,25 @@ var SubagentRuntime = class {
|
|
|
744
1784
|
generation: this.generation
|
|
745
1785
|
});
|
|
746
1786
|
let resolveTerminal;
|
|
747
|
-
const terminal = new Promise((
|
|
748
|
-
resolveTerminal =
|
|
1787
|
+
const terminal = new Promise((resolve4) => {
|
|
1788
|
+
resolveTerminal = resolve4;
|
|
749
1789
|
});
|
|
750
1790
|
let resolveControl;
|
|
751
1791
|
let rejectControl;
|
|
752
|
-
const controlReady = new Promise((
|
|
753
|
-
resolveControl =
|
|
1792
|
+
const controlReady = new Promise((resolve4, reject) => {
|
|
1793
|
+
resolveControl = resolve4;
|
|
754
1794
|
rejectControl = reject;
|
|
755
1795
|
});
|
|
756
1796
|
void controlReady.catch(() => void 0);
|
|
757
1797
|
const controller = new AbortController();
|
|
1798
|
+
const effectiveTools = [.../* @__PURE__ */ new Set([...input.tools, ...input.extensions.flatMap((extension) => extension.tools)])];
|
|
758
1799
|
const job = {
|
|
759
1800
|
jobId,
|
|
760
1801
|
state: "queued",
|
|
761
1802
|
createdAt: this.now(),
|
|
762
1803
|
...input.timeout !== void 0 ? { timeout: input.timeout } : {},
|
|
763
1804
|
controller,
|
|
764
|
-
tools:
|
|
1805
|
+
tools: effectiveTools,
|
|
765
1806
|
terminal,
|
|
766
1807
|
resolveTerminal,
|
|
767
1808
|
controlReady,
|
|
@@ -788,6 +1829,14 @@ var SubagentRuntime = class {
|
|
|
788
1829
|
child = await this.runChild({
|
|
789
1830
|
task: input.task,
|
|
790
1831
|
tools: [...input.tools],
|
|
1832
|
+
skills: [...input.skills],
|
|
1833
|
+
extensions: input.extensions.map((extension) => ({
|
|
1834
|
+
path: extension.path,
|
|
1835
|
+
tools: [...extension.tools]
|
|
1836
|
+
})),
|
|
1837
|
+
toolSources: Object.fromEntries(
|
|
1838
|
+
Object.entries(input.toolSources).map(([tool, sources]) => [tool, [...sources]])
|
|
1839
|
+
),
|
|
791
1840
|
model: input.model,
|
|
792
1841
|
thinkingLevel: input.thinkingLevel,
|
|
793
1842
|
cwd: input.cwd,
|
|
@@ -893,21 +1942,21 @@ var SubagentRuntime = class {
|
|
|
893
1942
|
let timeout;
|
|
894
1943
|
let onAbort;
|
|
895
1944
|
let unsubscribeMessage = () => void 0;
|
|
896
|
-
const message = new Promise((
|
|
897
|
-
unsubscribeMessage = this.broker.subscribeInboundMessage(() =>
|
|
1945
|
+
const message = new Promise((resolve4) => {
|
|
1946
|
+
unsubscribeMessage = this.broker.subscribeInboundMessage(() => resolve4("message"));
|
|
898
1947
|
});
|
|
899
1948
|
const outcome = await Promise.race([
|
|
900
1949
|
job.terminal.then(() => "terminal"),
|
|
901
1950
|
message,
|
|
902
1951
|
...timeoutMs !== void 0 ? [
|
|
903
|
-
new Promise((
|
|
904
|
-
timeout = setTimeout(() =>
|
|
1952
|
+
new Promise((resolve4) => {
|
|
1953
|
+
timeout = setTimeout(() => resolve4("timeout"), timeoutMs);
|
|
905
1954
|
timeout.unref();
|
|
906
1955
|
})
|
|
907
1956
|
] : [],
|
|
908
1957
|
...signal ? [
|
|
909
|
-
new Promise((
|
|
910
|
-
onAbort = () =>
|
|
1958
|
+
new Promise((resolve4) => {
|
|
1959
|
+
onAbort = () => resolve4("aborted");
|
|
911
1960
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
912
1961
|
})
|
|
913
1962
|
] : []
|
|
@@ -1089,7 +2138,7 @@ function abortError2(message) {
|
|
|
1089
2138
|
|
|
1090
2139
|
// src/tools.ts
|
|
1091
2140
|
var MAX_TASK_BYTES = 50 * 1024;
|
|
1092
|
-
var MAX_TOOLS =
|
|
2141
|
+
var MAX_TOOLS = MAX_SELECTED_TOOLS;
|
|
1093
2142
|
var MESSAGE_TYPE = "pi-subagents-message";
|
|
1094
2143
|
var CHILD_CORE_TOOL_SET = new Set(CHILD_CORE_TOOL_NAMES);
|
|
1095
2144
|
var THINKING_LEVEL_SET = new Set(SUBAGENT_THINKING_LEVELS);
|
|
@@ -1110,6 +2159,48 @@ var SpawnParameters = Type.Object(
|
|
|
1110
2159
|
}
|
|
1111
2160
|
)
|
|
1112
2161
|
),
|
|
2162
|
+
skills: Type.Optional(
|
|
2163
|
+
Type.Array(
|
|
2164
|
+
Type.String({
|
|
2165
|
+
description: "Local Markdown skill file or directory whose declared Pi skills all load and have names unique across attachments, resolved from the child working directory.",
|
|
2166
|
+
minLength: 1,
|
|
2167
|
+
maxLength: MAX_RESOURCE_PATH_BYTES
|
|
2168
|
+
}),
|
|
2169
|
+
{
|
|
2170
|
+
description: "Explicit local skills available to the child. Automatic skill discovery stays disabled.",
|
|
2171
|
+
maxItems: MAX_ATTACHED_SKILLS
|
|
2172
|
+
}
|
|
2173
|
+
)
|
|
2174
|
+
),
|
|
2175
|
+
extensions: Type.Optional(
|
|
2176
|
+
Type.Array(
|
|
2177
|
+
Type.Object(
|
|
2178
|
+
{
|
|
2179
|
+
path: Type.String({
|
|
2180
|
+
description: "Local extension file or directory path, resolved from the child working directory.",
|
|
2181
|
+
minLength: 1,
|
|
2182
|
+
maxLength: MAX_RESOURCE_PATH_BYTES
|
|
2183
|
+
}),
|
|
2184
|
+
tools: Type.Array(
|
|
2185
|
+
Type.String({
|
|
2186
|
+
description: "Exact extension-specific tool name to activate initially; built-in names are rejected.",
|
|
2187
|
+
minLength: 1,
|
|
2188
|
+
maxLength: MAX_EXTENSION_TOOL_NAME_LENGTH
|
|
2189
|
+
}),
|
|
2190
|
+
{
|
|
2191
|
+
description: "Extension tools to activate. Use an empty list for lifecycle or provider behavior only. All child tool names share a 16 KiB startup bootstrap.",
|
|
2192
|
+
maxItems: MAX_SELECTED_TOOLS
|
|
2193
|
+
}
|
|
2194
|
+
)
|
|
2195
|
+
},
|
|
2196
|
+
{ additionalProperties: false }
|
|
2197
|
+
),
|
|
2198
|
+
{
|
|
2199
|
+
description: "Explicit trusted local extensions loaded in the isolated child process.",
|
|
2200
|
+
maxItems: MAX_ATTACHED_EXTENSIONS
|
|
2201
|
+
}
|
|
2202
|
+
)
|
|
2203
|
+
),
|
|
1113
2204
|
thinkingLevel: Type.Optional(
|
|
1114
2205
|
StringEnum(SUBAGENT_THINKING_LEVELS, {
|
|
1115
2206
|
description: "Child thinking level. Defaults to the main agent's effective level."
|
|
@@ -1168,7 +2259,7 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
1168
2259
|
pi.registerTool({
|
|
1169
2260
|
name: "subagent_spawn",
|
|
1170
2261
|
label: "Subagent \xB7 Spawn",
|
|
1171
|
-
description: "Use subagent_spawn to start one Pi subagent job and return its jobId immediately. The task defines the child's specialization
|
|
2262
|
+
description: "Use subagent_spawn to start one Pi subagent job and return its jobId immediately. The task defines the child's specialization; selected core tools and explicit trusted local skills or extensions define its initial capabilities. The job may ask the main agent questions and publishes one asynchronous completion when terminal.",
|
|
1172
2263
|
promptSnippet: "Use subagent_spawn to start one Pi subagent job",
|
|
1173
2264
|
parameters: SpawnParameters,
|
|
1174
2265
|
prepareArguments: prepareSpawnArguments,
|
|
@@ -1177,18 +2268,50 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
1177
2268
|
assertNotNested();
|
|
1178
2269
|
const task = validateTask(params.task, "subagent_spawn");
|
|
1179
2270
|
const tools = resolveTools(params.tools);
|
|
1180
|
-
const
|
|
2271
|
+
const cwd = ctx.cwd;
|
|
2272
|
+
const projectTrusted = ctx.isProjectTrusted();
|
|
2273
|
+
const sessionGeneration = runtime.getSessionGeneration();
|
|
2274
|
+
const attachments = await resolveResourceAttachments(
|
|
2275
|
+
{ skills: params.skills, extensions: params.extensions },
|
|
2276
|
+
{ cwd, projectTrusted, coreTools: tools, signal }
|
|
2277
|
+
);
|
|
2278
|
+
throwIfAborted2(signal, "Subagent spawn was cancelled");
|
|
2279
|
+
if (runtime.getSessionGeneration() !== sessionGeneration) {
|
|
2280
|
+
throw new Error("Subagent spawn session changed during attachment validation; retry the request.");
|
|
2281
|
+
}
|
|
2282
|
+
if (ctx.cwd !== cwd || ctx.isProjectTrusted() !== projectTrusted) {
|
|
2283
|
+
throw new Error("Subagent spawn context changed during attachment validation; retry the request.");
|
|
2284
|
+
}
|
|
2285
|
+
if (attachments.extensions.length > 0) {
|
|
2286
|
+
assertChildBootstrapCapacity([.../* @__PURE__ */ new Set([...attachments.effectiveTools, ...CHILD_COMMUNICATION_TOOL_NAMES])]);
|
|
2287
|
+
}
|
|
2288
|
+
const model = resolveChildModel(ctx, attachments.extensions.length > 0);
|
|
1181
2289
|
const thinkingLevel = resolveThinkingLevel(params.thinkingLevel ?? ctx.thinkingLevel ?? pi.getThinkingLevel());
|
|
1182
2290
|
resolveTimeoutMs(params.timeout);
|
|
2291
|
+
assertChildCommandCapacity(
|
|
2292
|
+
{
|
|
2293
|
+
tools,
|
|
2294
|
+
skills: attachments.skills,
|
|
2295
|
+
extensions: attachments.extensions,
|
|
2296
|
+
model,
|
|
2297
|
+
thinkingLevel,
|
|
2298
|
+
projectTrusted
|
|
2299
|
+
},
|
|
2300
|
+
dependencies.platform
|
|
2301
|
+
);
|
|
2302
|
+
throwIfAborted2(signal, "Subagent spawn was cancelled");
|
|
1183
2303
|
return toolResult(
|
|
1184
2304
|
runtime.start({
|
|
1185
2305
|
task,
|
|
1186
2306
|
tools,
|
|
2307
|
+
skills: attachments.skills,
|
|
2308
|
+
extensions: attachments.extensions,
|
|
2309
|
+
toolSources: attachments.toolSources,
|
|
1187
2310
|
model,
|
|
1188
2311
|
thinkingLevel,
|
|
1189
|
-
cwd
|
|
2312
|
+
cwd,
|
|
1190
2313
|
timeout: params.timeout,
|
|
1191
|
-
projectTrusted
|
|
2314
|
+
projectTrusted
|
|
1192
2315
|
})
|
|
1193
2316
|
);
|
|
1194
2317
|
}
|
|
@@ -1322,11 +2445,11 @@ function resolveTools(value) {
|
|
|
1322
2445
|
}
|
|
1323
2446
|
return tools;
|
|
1324
2447
|
}
|
|
1325
|
-
function resolveChildModel(ctx) {
|
|
2448
|
+
function resolveChildModel(ctx, hasAttachedExtensions) {
|
|
1326
2449
|
const model = ctx.model;
|
|
1327
2450
|
if (!model) throw new Error("Subagent model is unavailable because no main-agent model is selected.");
|
|
1328
2451
|
const provider = sanitizeTerminalText(model.provider).slice(0, 128);
|
|
1329
|
-
if (ctx.modelRegistry.getRegisteredProviderIds().includes(model.provider)) {
|
|
2452
|
+
if (!hasAttachedExtensions && ctx.modelRegistry.getRegisteredProviderIds().includes(model.provider)) {
|
|
1330
2453
|
throw new Error(`Subagent model provider ${provider} is unavailable because children disable parent extensions.`);
|
|
1331
2454
|
}
|
|
1332
2455
|
if (ctx.modelRegistry.getProviderAuthStatus(model.provider).source === "runtime") {
|
|
@@ -1348,15 +2471,6 @@ function prepareSpawnArguments(args) {
|
|
|
1348
2471
|
function prepareWaitArguments(args) {
|
|
1349
2472
|
return prepareTimeoutArguments(args);
|
|
1350
2473
|
}
|
|
1351
|
-
function prepareTimeoutArguments(args) {
|
|
1352
|
-
if (!args || typeof args !== "object") return args;
|
|
1353
|
-
if (!Object.hasOwn(args, "timeoutMs")) return args;
|
|
1354
|
-
const record = args;
|
|
1355
|
-
if (typeof record.timeoutMs !== "number") return record;
|
|
1356
|
-
const { timeoutMs, ...prepared } = record;
|
|
1357
|
-
if (prepared.timeout === void 0) return { ...prepared, timeout: timeoutMs / 1e3 };
|
|
1358
|
-
return prepared;
|
|
1359
|
-
}
|
|
1360
2474
|
function resolveMainSendArguments(params) {
|
|
1361
2475
|
validateMessage(params.message, "Subagent message");
|
|
1362
2476
|
const recipient = optionalIdentifier(params.recipient, "recipient");
|