@narumitw/pi-subagents 3.0.1 → 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 +80 -136
- package/dist/child-communication-bridge.ts +9 -12
- 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-UKTWUQRM.js → chunk-YMVSJ5GE.ts} +89 -152
- package/dist/chunks/chunk-YMVSJ5GE.ts.map +7 -0
- package/dist/chunks/chunk-ZOAFWLDJ.ts +130 -0
- package/dist/chunks/chunk-ZOAFWLDJ.ts.map +7 -0
- package/dist/index.ts +1201 -120
- 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 +56 -54
- package/src/attachment-utils.ts +104 -0
- package/src/broker-credentials.ts +132 -54
- package/src/child-communication-bridge.ts +117 -125
- package/src/child-communication-tools.ts +89 -117
- package/src/child-readiness-probe.ts +72 -0
- package/src/completion-renderer.ts +46 -48
- package/src/message-broker.ts +531 -561
- package/src/model-output.ts +25 -28
- package/src/process.ts +942 -619
- package/src/resource-attachments.ts +724 -0
- package/src/runtime.ts +471 -478
- package/src/skill-attachments.ts +302 -0
- package/src/subagents.ts +20 -23
- package/src/timeout.ts +24 -0
- package/src/tools.ts +389 -325
- package/src/types.ts +45 -58
- package/src/widget.ts +85 -91
- package/dist/chunks/chunk-UKTWUQRM.js.map +0 -7
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";
|
|
@@ -52,11 +70,7 @@ function registerCompletionRenderer(pi) {
|
|
|
52
70
|
})
|
|
53
71
|
);
|
|
54
72
|
}
|
|
55
|
-
return createCompletionBox(
|
|
56
|
-
children,
|
|
57
|
-
options.outputPad,
|
|
58
|
-
(text) => theme.bg("customMessageBg", text)
|
|
59
|
-
);
|
|
73
|
+
return createCompletionBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
|
|
60
74
|
});
|
|
61
75
|
}
|
|
62
76
|
|
|
@@ -106,34 +120,26 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
106
120
|
import { fileURLToPath } from "node:url";
|
|
107
121
|
import { getPackageDir } from "@earendil-works/pi-coding-agent";
|
|
108
122
|
var CORE_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
109
|
-
var MAX_TIMEOUT_MS = 2147483647;
|
|
110
|
-
var MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1e3;
|
|
111
123
|
var MAX_OUTPUT_BYTES = 32 * 1024;
|
|
112
124
|
var MAX_ERROR_BYTES = 8 * 1024;
|
|
113
125
|
var MAX_EVENT_LINE_BYTES = 256 * 1024;
|
|
114
126
|
var RPC_RESPONSE_TIMEOUT_MS = 3e4;
|
|
115
127
|
var KILL_GRACE_MS = 1e3;
|
|
116
|
-
|
|
117
|
-
if (timeout === void 0) return void 0;
|
|
118
|
-
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
119
|
-
throw new Error("Invalid timeout: must be a finite number of seconds");
|
|
120
|
-
}
|
|
121
|
-
const timeoutMs = timeout * 1e3;
|
|
122
|
-
if (timeoutMs > MAX_TIMEOUT_MS) {
|
|
123
|
-
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
|
|
124
|
-
}
|
|
125
|
-
return timeoutMs;
|
|
126
|
-
}
|
|
128
|
+
var MAX_WINDOWS_COMMAND_LINE_CODE_UNITS = 32767;
|
|
127
129
|
async function runChild(request) {
|
|
128
130
|
if (request.signal.aborted) return cancelledResult();
|
|
129
131
|
try {
|
|
132
|
+
assertChildCommandCapacity(request);
|
|
130
133
|
const invocation = resolvePiInvocation(buildPiArgs(request));
|
|
131
134
|
return await executeProcess(invocation, request);
|
|
132
135
|
} catch (error) {
|
|
133
136
|
if (request.signal.aborted) return cancelledResult();
|
|
134
137
|
return {
|
|
135
138
|
state: "failed",
|
|
136
|
-
error: truncateText(
|
|
139
|
+
error: truncateText(
|
|
140
|
+
redactAttachmentPaths(error instanceof Error ? error.message : String(error), request),
|
|
141
|
+
MAX_ERROR_BYTES
|
|
142
|
+
).text,
|
|
137
143
|
limitations: [],
|
|
138
144
|
truncated: false
|
|
139
145
|
};
|
|
@@ -148,31 +154,106 @@ function buildPiArgs(request) {
|
|
|
148
154
|
"--no-skills",
|
|
149
155
|
"--no-prompt-templates",
|
|
150
156
|
"-e",
|
|
151
|
-
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(
|
|
152
163
|
"--model",
|
|
153
164
|
request.model,
|
|
154
165
|
"--thinking",
|
|
155
166
|
request.thinkingLevel,
|
|
156
167
|
request.projectTrusted ? "--approve" : "--no-approve"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
args.push("--tools", tools.join(","));
|
|
168
|
+
);
|
|
169
|
+
args.push("--tools", selectedChildTools(request).join(","));
|
|
160
170
|
return args;
|
|
161
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
|
+
}
|
|
162
198
|
function childCommunicationBridgePath() {
|
|
163
199
|
return fileURLToPath(new URL("./child-communication-bridge.ts", import.meta.url));
|
|
164
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
|
+
}
|
|
165
216
|
async function executeProcess(invocation, request) {
|
|
166
217
|
const timeoutMs = resolveTimeoutMs(request.timeout);
|
|
218
|
+
const expectedTools = selectedChildTools(request);
|
|
219
|
+
const expectReadiness = requiresReadinessAttestation(request);
|
|
220
|
+
if (expectReadiness) assertChildBootstrapCapacity(expectedTools);
|
|
167
221
|
let latestOutput = "";
|
|
168
222
|
let terminalOutput;
|
|
169
223
|
let terminalStopReason;
|
|
170
224
|
let errorMessage = "";
|
|
171
225
|
let assistantFailed = false;
|
|
172
226
|
let stderr = "";
|
|
227
|
+
let stderrRedactionCarry = "";
|
|
228
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
229
|
+
const diagnosticAttachmentPaths = attachmentPathVariants(request);
|
|
173
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
|
+
};
|
|
174
253
|
let malformedEvents = 0;
|
|
175
254
|
let rpcCounter = 0;
|
|
255
|
+
let attachmentStartupPending = expectReadiness;
|
|
256
|
+
let attachmentStartupError;
|
|
176
257
|
const pendingCommands = /* @__PURE__ */ new Map();
|
|
177
258
|
let rpcInputError;
|
|
178
259
|
let sendCommand = () => Promise.reject(new Error("Subagent RPC process is unavailable."));
|
|
@@ -193,11 +274,11 @@ async function executeProcess(invocation, request) {
|
|
|
193
274
|
const rejectPendingCommands = (error2) => {
|
|
194
275
|
for (const id of [...pendingCommands.keys()]) rejectPendingCommand(id, error2);
|
|
195
276
|
};
|
|
196
|
-
const resolvePendingCommand = (id) => {
|
|
277
|
+
const resolvePendingCommand = (id, response) => {
|
|
197
278
|
const pending = takePendingCommand(id);
|
|
198
279
|
if (!pending) return;
|
|
199
280
|
try {
|
|
200
|
-
pending.onAccepted?.();
|
|
281
|
+
pending.onAccepted?.(response);
|
|
201
282
|
pending.resolve();
|
|
202
283
|
} catch (error2) {
|
|
203
284
|
pending.reject(error2 instanceof Error ? error2 : new Error(String(error2)));
|
|
@@ -210,16 +291,21 @@ async function executeProcess(invocation, request) {
|
|
|
210
291
|
const decoder = new JsonLineDecoder(
|
|
211
292
|
(value) => {
|
|
212
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
|
+
}
|
|
213
299
|
if (event.type === "response" && typeof event.id === "string") {
|
|
214
300
|
const pending = pendingCommands.get(event.id);
|
|
215
301
|
if (!pending) return;
|
|
216
302
|
if (event.success === true) {
|
|
217
|
-
resolvePendingCommand(event.id);
|
|
303
|
+
resolvePendingCommand(event.id, event);
|
|
218
304
|
} else {
|
|
219
305
|
rejectPendingCommand(
|
|
220
306
|
event.id,
|
|
221
307
|
new Error(
|
|
222
|
-
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.`
|
|
223
309
|
)
|
|
224
310
|
);
|
|
225
311
|
}
|
|
@@ -244,7 +330,7 @@ async function executeProcess(invocation, request) {
|
|
|
244
330
|
assistantFailed = true;
|
|
245
331
|
}
|
|
246
332
|
if (event.message.errorMessage) {
|
|
247
|
-
const limited = truncateText(event.message.errorMessage, MAX_ERROR_BYTES);
|
|
333
|
+
const limited = truncateText(redactAttachmentPaths(event.message.errorMessage, request), MAX_ERROR_BYTES);
|
|
248
334
|
errorMessage = limited.text;
|
|
249
335
|
truncated ||= limited.truncated;
|
|
250
336
|
}
|
|
@@ -252,9 +338,12 @@ async function executeProcess(invocation, request) {
|
|
|
252
338
|
},
|
|
253
339
|
() => {
|
|
254
340
|
malformedEvents++;
|
|
341
|
+
if (attachmentStartupPending && !attachmentStartupError) {
|
|
342
|
+
attachmentStartupError = "Subagent attachment startup emitted malformed or oversized RPC output.";
|
|
343
|
+
}
|
|
255
344
|
}
|
|
256
345
|
);
|
|
257
|
-
const settlement = await new Promise((
|
|
346
|
+
const settlement = await new Promise((resolve4) => {
|
|
258
347
|
let process2;
|
|
259
348
|
let settled = false;
|
|
260
349
|
let finishRequested = false;
|
|
@@ -264,6 +353,9 @@ async function executeProcess(invocation, request) {
|
|
|
264
353
|
let timedOut = false;
|
|
265
354
|
let completed = false;
|
|
266
355
|
let ready = false;
|
|
356
|
+
let attachmentReady = !expectReadiness;
|
|
357
|
+
let promptStarted = false;
|
|
358
|
+
let readinessPipe;
|
|
267
359
|
let deadline;
|
|
268
360
|
let forceClose;
|
|
269
361
|
let escalation;
|
|
@@ -278,8 +370,10 @@ async function executeProcess(invocation, request) {
|
|
|
278
370
|
if (forceClose) clearTimeout(forceClose);
|
|
279
371
|
if (escalation) clearTimeout(escalation);
|
|
280
372
|
request.signal.removeEventListener("abort", onAbort);
|
|
373
|
+
readinessPipe?.removeAllListeners();
|
|
374
|
+
readinessPipe?.destroy();
|
|
281
375
|
rejectPendingCommands(new Error("Subagent RPC process closed."));
|
|
282
|
-
|
|
376
|
+
resolve4({ code, cancelled, timedOut, completed, launchError });
|
|
283
377
|
};
|
|
284
378
|
if (termination) void termination.then(complete, complete);
|
|
285
379
|
else complete();
|
|
@@ -303,6 +397,7 @@ async function executeProcess(invocation, request) {
|
|
|
303
397
|
process2.stdin?.destroy();
|
|
304
398
|
process2.stdout?.destroy();
|
|
305
399
|
process2.stderr?.destroy();
|
|
400
|
+
readinessPipe?.destroy();
|
|
306
401
|
finish(code);
|
|
307
402
|
}, KILL_GRACE_MS * 2);
|
|
308
403
|
forceClose.unref();
|
|
@@ -319,18 +414,18 @@ async function executeProcess(invocation, request) {
|
|
|
319
414
|
};
|
|
320
415
|
onAgentSettled = completeNormally;
|
|
321
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];
|
|
322
423
|
process2 = spawn(invocation.command, invocation.args, {
|
|
323
424
|
cwd: request.cwd,
|
|
324
425
|
detached: globalThis.process.platform !== "win32",
|
|
325
426
|
shell: false,
|
|
326
|
-
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
327
|
-
env:
|
|
328
|
-
...globalThis.process.env,
|
|
329
|
-
...brokerCredentialEnvironment(),
|
|
330
|
-
PI_SUBAGENT_DEPTH: String(
|
|
331
|
-
(Number.parseInt(globalThis.process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1
|
|
332
|
-
)
|
|
333
|
-
}
|
|
427
|
+
stdio: expectReadiness ? ["pipe", "pipe", "pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe", "pipe"],
|
|
428
|
+
env: environment
|
|
334
429
|
});
|
|
335
430
|
} catch (error2) {
|
|
336
431
|
finish(1, error2 instanceof Error ? error2.message : String(error2));
|
|
@@ -385,11 +480,9 @@ async function executeProcess(invocation, request) {
|
|
|
385
480
|
}
|
|
386
481
|
});
|
|
387
482
|
};
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
spawned = true;
|
|
392
|
-
if (settled || cancelled) return;
|
|
483
|
+
const startPrompt = () => {
|
|
484
|
+
if (!spawned || !attachmentReady || promptStarted || settled || cancelled || terminating) return;
|
|
485
|
+
promptStarted = true;
|
|
393
486
|
void sendCommand(
|
|
394
487
|
{ type: "prompt", message: `Task: ${request.task}` },
|
|
395
488
|
() => {
|
|
@@ -418,24 +511,104 @@ async function executeProcess(invocation, request) {
|
|
|
418
511
|
).catch((error2) => {
|
|
419
512
|
if (settled || terminating) return;
|
|
420
513
|
errorMessage = truncateText(
|
|
421
|
-
error2 instanceof Error ? error2.message : String(error2),
|
|
514
|
+
redactAttachmentPaths(error2 instanceof Error ? error2.message : String(error2), request),
|
|
422
515
|
MAX_ERROR_BYTES
|
|
423
516
|
).text;
|
|
424
517
|
terminate(1);
|
|
425
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();
|
|
426
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
|
+
}
|
|
427
603
|
process2.stdout?.on("data", (chunk) => decoder.push(chunk));
|
|
428
|
-
process2.stderr?.on("data", (chunk) =>
|
|
429
|
-
const limited = truncateTail(`${stderr}${chunk.toString()}`, MAX_ERROR_BYTES);
|
|
430
|
-
stderr = limited.text;
|
|
431
|
-
truncated ||= limited.truncated;
|
|
432
|
-
});
|
|
604
|
+
process2.stderr?.on("data", (chunk) => pushStderr(chunk));
|
|
433
605
|
process2.once("close", (code) => {
|
|
606
|
+
flushStderr();
|
|
434
607
|
decoder.finish();
|
|
435
608
|
finish(cancelled ? 130 : timedOut ? 124 : completed ? 0 : code ?? 1);
|
|
436
609
|
});
|
|
437
610
|
process2.once("error", (error2) => {
|
|
438
|
-
const limited = truncateText(error2.message, MAX_ERROR_BYTES);
|
|
611
|
+
const limited = truncateText(redactAttachmentPaths(error2.message, request), MAX_ERROR_BYTES);
|
|
439
612
|
errorMessage = limited.text;
|
|
440
613
|
truncated ||= limited.truncated;
|
|
441
614
|
if (spawned) terminate(1);
|
|
@@ -458,7 +631,12 @@ async function executeProcess(invocation, request) {
|
|
|
458
631
|
credentialPipe.on("error", onCredentialError);
|
|
459
632
|
credentialPipe.once("close", removeCredentialListeners);
|
|
460
633
|
try {
|
|
461
|
-
credentialPipe.end(
|
|
634
|
+
credentialPipe.end(
|
|
635
|
+
serializeChildBootstrap({
|
|
636
|
+
communication: request.communication,
|
|
637
|
+
expectedTools: expectReadiness ? expectedTools : []
|
|
638
|
+
})
|
|
639
|
+
);
|
|
462
640
|
} catch {
|
|
463
641
|
onCredentialError();
|
|
464
642
|
}
|
|
@@ -480,7 +658,17 @@ async function executeProcess(invocation, request) {
|
|
|
480
658
|
truncated
|
|
481
659
|
};
|
|
482
660
|
}
|
|
483
|
-
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;
|
|
484
672
|
if (settlement.completed && terminalStopReason === "stop" && !assistantFailed && !errorMessage) {
|
|
485
673
|
return {
|
|
486
674
|
state: "completed",
|
|
@@ -519,8 +707,8 @@ function resolvePiInvocation(args) {
|
|
|
519
707
|
return { command: globalThis.process.execPath, args };
|
|
520
708
|
}
|
|
521
709
|
const cliPath = fs.realpathSync(path.resolve(packageDirectory, declared));
|
|
522
|
-
const
|
|
523
|
-
if (
|
|
710
|
+
const relative4 = path.relative(packageDirectory, cliPath);
|
|
711
|
+
if (relative4 === ".." || relative4.startsWith(`..${path.sep}`) || path.isAbsolute(relative4)) {
|
|
524
712
|
throw new Error("Pi core bin.pi escapes its package directory.");
|
|
525
713
|
}
|
|
526
714
|
if (!fs.statSync(cliPath).isFile()) throw new Error("Pi core bin.pi is not a file.");
|
|
@@ -544,7 +732,7 @@ function terminateWindowsProcessTree(process2, spawnProcess = spawn, taskkillPat
|
|
|
544
732
|
killImmediateChild(process2);
|
|
545
733
|
return Promise.resolve();
|
|
546
734
|
}
|
|
547
|
-
return new Promise((
|
|
735
|
+
return new Promise((resolve4) => {
|
|
548
736
|
let settled = false;
|
|
549
737
|
let treeKiller;
|
|
550
738
|
let deadline;
|
|
@@ -558,7 +746,7 @@ function terminateWindowsProcessTree(process2, spawnProcess = spawn, taskkillPat
|
|
|
558
746
|
treeKiller.removeListener("close", onClose);
|
|
559
747
|
if (terminateHelper) killImmediateChild(treeKiller);
|
|
560
748
|
if (fallback) killImmediateChild(process2);
|
|
561
|
-
|
|
749
|
+
resolve4();
|
|
562
750
|
};
|
|
563
751
|
try {
|
|
564
752
|
treeKiller = spawnProcess(taskkillPath, ["/PID", String(process2.pid), "/T", "/F"], {
|
|
@@ -567,7 +755,7 @@ function terminateWindowsProcessTree(process2, spawnProcess = spawn, taskkillPat
|
|
|
567
755
|
});
|
|
568
756
|
} catch {
|
|
569
757
|
killImmediateChild(process2);
|
|
570
|
-
|
|
758
|
+
resolve4();
|
|
571
759
|
return;
|
|
572
760
|
}
|
|
573
761
|
treeKiller.once("error", onError);
|
|
@@ -587,6 +775,97 @@ function killImmediateChild(process2) {
|
|
|
587
775
|
} catch {
|
|
588
776
|
}
|
|
589
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
|
+
}
|
|
590
869
|
function abortError(message) {
|
|
591
870
|
const error = new Error(message);
|
|
592
871
|
error.name = "AbortError";
|
|
@@ -672,6 +951,247 @@ var JsonLineDecoder = class {
|
|
|
672
951
|
}
|
|
673
952
|
};
|
|
674
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
|
+
|
|
675
1195
|
// src/types.ts
|
|
676
1196
|
var TERMINAL_JOB_STATES = /* @__PURE__ */ new Set([
|
|
677
1197
|
"completed",
|
|
@@ -680,16 +1200,7 @@ var TERMINAL_JOB_STATES = /* @__PURE__ */ new Set([
|
|
|
680
1200
|
"timed_out",
|
|
681
1201
|
"cancelled"
|
|
682
1202
|
]);
|
|
683
|
-
var CHILD_CORE_TOOL_NAMES = [
|
|
684
|
-
"read",
|
|
685
|
-
"bash",
|
|
686
|
-
"powershell",
|
|
687
|
-
"edit",
|
|
688
|
-
"write",
|
|
689
|
-
"grep",
|
|
690
|
-
"find",
|
|
691
|
-
"ls"
|
|
692
|
-
];
|
|
1203
|
+
var CHILD_CORE_TOOL_NAMES = ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"];
|
|
693
1204
|
var DEFAULT_SUBAGENT_TOOLS = ["read", "grep", "find", "ls"];
|
|
694
1205
|
var SUBAGENT_THINKING_LEVELS = [
|
|
695
1206
|
"off",
|
|
@@ -701,6 +1212,514 @@ var SUBAGENT_THINKING_LEVELS = [
|
|
|
701
1212
|
"max"
|
|
702
1213
|
];
|
|
703
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
|
+
|
|
704
1723
|
// src/runtime.ts
|
|
705
1724
|
var MAX_ACTIVE_JOBS = 8;
|
|
706
1725
|
var MAX_RETAINED_TERMINAL_JOBS = 32;
|
|
@@ -732,6 +1751,9 @@ var SubagentRuntime = class {
|
|
|
732
1751
|
this.sessionActive = true;
|
|
733
1752
|
this.notifyJobsChanged();
|
|
734
1753
|
}
|
|
1754
|
+
getSessionGeneration() {
|
|
1755
|
+
return this.generation;
|
|
1756
|
+
}
|
|
735
1757
|
subscribeJobs(listener) {
|
|
736
1758
|
this.jobListeners.add(listener);
|
|
737
1759
|
return () => this.jobListeners.delete(listener);
|
|
@@ -762,24 +1784,25 @@ var SubagentRuntime = class {
|
|
|
762
1784
|
generation: this.generation
|
|
763
1785
|
});
|
|
764
1786
|
let resolveTerminal;
|
|
765
|
-
const terminal = new Promise((
|
|
766
|
-
resolveTerminal =
|
|
1787
|
+
const terminal = new Promise((resolve4) => {
|
|
1788
|
+
resolveTerminal = resolve4;
|
|
767
1789
|
});
|
|
768
1790
|
let resolveControl;
|
|
769
1791
|
let rejectControl;
|
|
770
|
-
const controlReady = new Promise((
|
|
771
|
-
resolveControl =
|
|
1792
|
+
const controlReady = new Promise((resolve4, reject) => {
|
|
1793
|
+
resolveControl = resolve4;
|
|
772
1794
|
rejectControl = reject;
|
|
773
1795
|
});
|
|
774
1796
|
void controlReady.catch(() => void 0);
|
|
775
1797
|
const controller = new AbortController();
|
|
1798
|
+
const effectiveTools = [.../* @__PURE__ */ new Set([...input.tools, ...input.extensions.flatMap((extension) => extension.tools)])];
|
|
776
1799
|
const job = {
|
|
777
1800
|
jobId,
|
|
778
1801
|
state: "queued",
|
|
779
1802
|
createdAt: this.now(),
|
|
780
1803
|
...input.timeout !== void 0 ? { timeout: input.timeout } : {},
|
|
781
1804
|
controller,
|
|
782
|
-
tools:
|
|
1805
|
+
tools: effectiveTools,
|
|
783
1806
|
terminal,
|
|
784
1807
|
resolveTerminal,
|
|
785
1808
|
controlReady,
|
|
@@ -806,6 +1829,14 @@ var SubagentRuntime = class {
|
|
|
806
1829
|
child = await this.runChild({
|
|
807
1830
|
task: input.task,
|
|
808
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
|
+
),
|
|
809
1840
|
model: input.model,
|
|
810
1841
|
thinkingLevel: input.thinkingLevel,
|
|
811
1842
|
cwd: input.cwd,
|
|
@@ -814,8 +1845,7 @@ var SubagentRuntime = class {
|
|
|
814
1845
|
communication,
|
|
815
1846
|
signal: controller.signal,
|
|
816
1847
|
onControl: (control) => {
|
|
817
|
-
if (job.state !== "running" || job.stopRequest || job.generation !== this.generation)
|
|
818
|
-
return;
|
|
1848
|
+
if (job.state !== "running" || job.stopRequest || job.generation !== this.generation) return;
|
|
819
1849
|
job.control = control;
|
|
820
1850
|
job.resolveControl(control);
|
|
821
1851
|
}
|
|
@@ -912,21 +1942,21 @@ var SubagentRuntime = class {
|
|
|
912
1942
|
let timeout;
|
|
913
1943
|
let onAbort;
|
|
914
1944
|
let unsubscribeMessage = () => void 0;
|
|
915
|
-
const message = new Promise((
|
|
916
|
-
unsubscribeMessage = this.broker.subscribeInboundMessage(() =>
|
|
1945
|
+
const message = new Promise((resolve4) => {
|
|
1946
|
+
unsubscribeMessage = this.broker.subscribeInboundMessage(() => resolve4("message"));
|
|
917
1947
|
});
|
|
918
1948
|
const outcome = await Promise.race([
|
|
919
1949
|
job.terminal.then(() => "terminal"),
|
|
920
1950
|
message,
|
|
921
1951
|
...timeoutMs !== void 0 ? [
|
|
922
|
-
new Promise((
|
|
923
|
-
timeout = setTimeout(() =>
|
|
1952
|
+
new Promise((resolve4) => {
|
|
1953
|
+
timeout = setTimeout(() => resolve4("timeout"), timeoutMs);
|
|
924
1954
|
timeout.unref();
|
|
925
1955
|
})
|
|
926
1956
|
] : [],
|
|
927
1957
|
...signal ? [
|
|
928
|
-
new Promise((
|
|
929
|
-
onAbort = () =>
|
|
1958
|
+
new Promise((resolve4) => {
|
|
1959
|
+
onAbort = () => resolve4("aborted");
|
|
930
1960
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
931
1961
|
})
|
|
932
1962
|
] : []
|
|
@@ -1057,10 +2087,7 @@ var SubagentRuntime = class {
|
|
|
1057
2087
|
if (this.jobs.delete(job.jobId)) this.omittedJobs++;
|
|
1058
2088
|
}
|
|
1059
2089
|
const terminal = [...this.jobs.values()].filter((job) => isTerminal(job.state)).sort((left, right) => (left.finishedAt ?? 0) - (right.finishedAt ?? 0));
|
|
1060
|
-
for (const job of terminal.slice(
|
|
1061
|
-
0,
|
|
1062
|
-
Math.max(0, terminal.length - MAX_RETAINED_TERMINAL_JOBS)
|
|
1063
|
-
)) {
|
|
2090
|
+
for (const job of terminal.slice(0, Math.max(0, terminal.length - MAX_RETAINED_TERMINAL_JOBS))) {
|
|
1064
2091
|
if (this.jobs.delete(job.jobId)) this.omittedJobs++;
|
|
1065
2092
|
}
|
|
1066
2093
|
}
|
|
@@ -1111,7 +2138,7 @@ function abortError2(message) {
|
|
|
1111
2138
|
|
|
1112
2139
|
// src/tools.ts
|
|
1113
2140
|
var MAX_TASK_BYTES = 50 * 1024;
|
|
1114
|
-
var MAX_TOOLS =
|
|
2141
|
+
var MAX_TOOLS = MAX_SELECTED_TOOLS;
|
|
1115
2142
|
var MESSAGE_TYPE = "pi-subagents-message";
|
|
1116
2143
|
var CHILD_CORE_TOOL_SET = new Set(CHILD_CORE_TOOL_NAMES);
|
|
1117
2144
|
var THINKING_LEVEL_SET = new Set(SUBAGENT_THINKING_LEVELS);
|
|
@@ -1132,14 +2159,54 @@ var SpawnParameters = Type.Object(
|
|
|
1132
2159
|
}
|
|
1133
2160
|
)
|
|
1134
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
|
+
),
|
|
1135
2204
|
thinkingLevel: Type.Optional(
|
|
1136
2205
|
StringEnum(SUBAGENT_THINKING_LEVELS, {
|
|
1137
2206
|
description: "Child thinking level. Defaults to the main agent's effective level."
|
|
1138
2207
|
})
|
|
1139
2208
|
),
|
|
1140
|
-
timeout: Type.Optional(
|
|
1141
|
-
Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })
|
|
1142
|
-
)
|
|
2209
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" }))
|
|
1143
2210
|
},
|
|
1144
2211
|
{ additionalProperties: false }
|
|
1145
2212
|
);
|
|
@@ -1156,9 +2223,7 @@ var CancelParameters = Type.Object(
|
|
|
1156
2223
|
var WaitParameters = Type.Object(
|
|
1157
2224
|
{
|
|
1158
2225
|
jobId: Type.String({ description: "Job to wait for.", maxLength: MAX_IDENTIFIER_LENGTH }),
|
|
1159
|
-
timeout: Type.Optional(
|
|
1160
|
-
Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })
|
|
1161
|
-
)
|
|
2226
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" }))
|
|
1162
2227
|
},
|
|
1163
2228
|
{ additionalProperties: false }
|
|
1164
2229
|
);
|
|
@@ -1194,7 +2259,7 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
1194
2259
|
pi.registerTool({
|
|
1195
2260
|
name: "subagent_spawn",
|
|
1196
2261
|
label: "Subagent \xB7 Spawn",
|
|
1197
|
-
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.",
|
|
1198
2263
|
promptSnippet: "Use subagent_spawn to start one Pi subagent job",
|
|
1199
2264
|
parameters: SpawnParameters,
|
|
1200
2265
|
prepareArguments: prepareSpawnArguments,
|
|
@@ -1203,20 +2268,50 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
1203
2268
|
assertNotNested();
|
|
1204
2269
|
const task = validateTask(params.task, "subagent_spawn");
|
|
1205
2270
|
const tools = resolveTools(params.tools);
|
|
1206
|
-
const
|
|
1207
|
-
const
|
|
1208
|
-
|
|
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 }
|
|
1209
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);
|
|
2289
|
+
const thinkingLevel = resolveThinkingLevel(params.thinkingLevel ?? ctx.thinkingLevel ?? pi.getThinkingLevel());
|
|
1210
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");
|
|
1211
2303
|
return toolResult(
|
|
1212
2304
|
runtime.start({
|
|
1213
2305
|
task,
|
|
1214
2306
|
tools,
|
|
2307
|
+
skills: attachments.skills,
|
|
2308
|
+
extensions: attachments.extensions,
|
|
2309
|
+
toolSources: attachments.toolSources,
|
|
1215
2310
|
model,
|
|
1216
2311
|
thinkingLevel,
|
|
1217
|
-
cwd
|
|
2312
|
+
cwd,
|
|
1218
2313
|
timeout: params.timeout,
|
|
1219
|
-
projectTrusted
|
|
2314
|
+
projectTrusted
|
|
1220
2315
|
})
|
|
1221
2316
|
);
|
|
1222
2317
|
}
|
|
@@ -1253,9 +2348,7 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
1253
2348
|
prepareArguments: prepareWaitArguments,
|
|
1254
2349
|
async execute(_toolCallId, params, signal) {
|
|
1255
2350
|
const timeoutMs = resolveTimeoutMs(params.timeout);
|
|
1256
|
-
return toolResult(
|
|
1257
|
-
await runtime.wait(requiredIdentifier(params.jobId, "jobId"), timeoutMs, signal)
|
|
1258
|
-
);
|
|
2351
|
+
return toolResult(await runtime.wait(requiredIdentifier(params.jobId, "jobId"), timeoutMs, signal));
|
|
1259
2352
|
}
|
|
1260
2353
|
});
|
|
1261
2354
|
pi.registerTool({
|
|
@@ -1352,15 +2445,12 @@ function resolveTools(value) {
|
|
|
1352
2445
|
}
|
|
1353
2446
|
return tools;
|
|
1354
2447
|
}
|
|
1355
|
-
function resolveChildModel(ctx) {
|
|
2448
|
+
function resolveChildModel(ctx, hasAttachedExtensions) {
|
|
1356
2449
|
const model = ctx.model;
|
|
1357
|
-
if (!model)
|
|
1358
|
-
throw new Error("Subagent model is unavailable because no main-agent model is selected.");
|
|
2450
|
+
if (!model) throw new Error("Subagent model is unavailable because no main-agent model is selected.");
|
|
1359
2451
|
const provider = sanitizeTerminalText(model.provider).slice(0, 128);
|
|
1360
|
-
if (ctx.modelRegistry.getRegisteredProviderIds().includes(model.provider)) {
|
|
1361
|
-
throw new Error(
|
|
1362
|
-
`Subagent model provider ${provider} is unavailable because children disable parent extensions.`
|
|
1363
|
-
);
|
|
2452
|
+
if (!hasAttachedExtensions && ctx.modelRegistry.getRegisteredProviderIds().includes(model.provider)) {
|
|
2453
|
+
throw new Error(`Subagent model provider ${provider} is unavailable because children disable parent extensions.`);
|
|
1364
2454
|
}
|
|
1365
2455
|
if (ctx.modelRegistry.getProviderAuthStatus(model.provider).source === "runtime") {
|
|
1366
2456
|
throw new Error(
|
|
@@ -1381,15 +2471,6 @@ function prepareSpawnArguments(args) {
|
|
|
1381
2471
|
function prepareWaitArguments(args) {
|
|
1382
2472
|
return prepareTimeoutArguments(args);
|
|
1383
2473
|
}
|
|
1384
|
-
function prepareTimeoutArguments(args) {
|
|
1385
|
-
if (!args || typeof args !== "object") return args;
|
|
1386
|
-
if (!Object.hasOwn(args, "timeoutMs")) return args;
|
|
1387
|
-
const record = args;
|
|
1388
|
-
if (typeof record.timeoutMs !== "number") return record;
|
|
1389
|
-
const { timeoutMs, ...prepared } = record;
|
|
1390
|
-
if (prepared.timeout === void 0) return { ...prepared, timeout: timeoutMs / 1e3 };
|
|
1391
|
-
return prepared;
|
|
1392
|
-
}
|
|
1393
2474
|
function resolveMainSendArguments(params) {
|
|
1394
2475
|
validateMessage(params.message, "Subagent message");
|
|
1395
2476
|
const recipient = optionalIdentifier(params.recipient, "recipient");
|