@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/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
- brokerCredentialEnvironment,
26
+ assertChildBootstrapCapacity,
27
+ childBootstrapEnvironment,
10
28
  sanitizeTerminalText,
11
- serializeBrokerCredentials,
29
+ serializeChildBootstrap,
12
30
  validateMessage
13
- } from "./chunks/chunk-UKTWUQRM.js";
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
- function resolveTimeoutMs(timeout) {
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(error instanceof Error ? error.message : String(error), MAX_ERROR_BYTES).text,
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
- const tools = [.../* @__PURE__ */ new Set([...request.tools, ...CHILD_COMMUNICATION_TOOL_NAMES])];
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((resolve2) => {
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
- resolve2({ code, cancelled, timedOut, completed, launchError });
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
- request.signal.addEventListener("abort", onAbort, { once: true });
389
- if (request.signal.aborted) onAbort();
390
- process2.once("spawn", () => {
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(serializeBrokerCredentials(request.communication));
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 error = settlement.launchError || errorMessage || stderr.trim();
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 relative2 = path.relative(packageDirectory, cliPath);
523
- if (relative2 === ".." || relative2.startsWith(`..${path.sep}`) || path.isAbsolute(relative2)) {
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((resolve2) => {
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
- resolve2();
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
- resolve2();
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((resolve2) => {
766
- resolveTerminal = resolve2;
1787
+ const terminal = new Promise((resolve4) => {
1788
+ resolveTerminal = resolve4;
767
1789
  });
768
1790
  let resolveControl;
769
1791
  let rejectControl;
770
- const controlReady = new Promise((resolve2, reject) => {
771
- resolveControl = resolve2;
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: [...input.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((resolve2) => {
916
- unsubscribeMessage = this.broker.subscribeInboundMessage(() => resolve2("message"));
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((resolve2) => {
923
- timeout = setTimeout(() => resolve2("timeout"), timeoutMs);
1952
+ new Promise((resolve4) => {
1953
+ timeout = setTimeout(() => resolve4("timeout"), timeoutMs);
924
1954
  timeout.unref();
925
1955
  })
926
1956
  ] : [],
927
1957
  ...signal ? [
928
- new Promise((resolve2) => {
929
- onAbort = () => resolve2("aborted");
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 = 64;
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, and the selected tools define its capabilities. The job may ask the main agent questions and publishes one asynchronous completion when terminal.",
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 model = resolveChildModel(ctx);
1207
- const thinkingLevel = resolveThinkingLevel(
1208
- params.thinkingLevel ?? ctx.thinkingLevel ?? pi.getThinkingLevel()
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: ctx.cwd,
2312
+ cwd,
1218
2313
  timeout: params.timeout,
1219
- projectTrusted: ctx.isProjectTrusted()
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");