@narumitw/pi-subagents 0.43.0 → 0.46.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.
@@ -0,0 +1,183 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { getAgentDir, ProjectTrustStore } from "@earendil-works/pi-coding-agent";
4
+ import type { ConsultationCwdPolicy, DelegationCwdPolicy } from "./agents.js";
5
+ import { safeTerminalLine } from "./safe-text.js";
6
+
7
+ export type TargetBoundary = "current-workspace" | "external";
8
+ export type TargetTrustKind =
9
+ | "session-trusted"
10
+ | "session-untrusted"
11
+ | "saved-trusted"
12
+ | "saved-denied"
13
+ | "unsaved"
14
+ | "trust-error";
15
+
16
+ export interface ResolvedTargetTrust {
17
+ kind: TargetTrustKind;
18
+ projectTrusted: boolean;
19
+ sourcePath?: string;
20
+ warning?: string;
21
+ }
22
+
23
+ export interface ResolvedSubagentTarget {
24
+ cwd: string;
25
+ workspace: string;
26
+ boundary: TargetBoundary;
27
+ trust: ResolvedTargetTrust;
28
+ }
29
+
30
+ export interface TargetPolicyAudit {
31
+ cwd: string;
32
+ boundary: TargetBoundary;
33
+ trust: {
34
+ kind: TargetTrustKind;
35
+ projectTrusted: boolean;
36
+ sourcePath?: string;
37
+ warning?: string;
38
+ };
39
+ }
40
+
41
+ export function targetPolicyAudit(target: ResolvedSubagentTarget): TargetPolicyAudit {
42
+ return {
43
+ cwd: safeTerminalLine(target.cwd),
44
+ boundary: target.boundary,
45
+ trust: {
46
+ ...target.trust,
47
+ sourcePath: target.trust.sourcePath ? safeTerminalLine(target.trust.sourcePath) : undefined,
48
+ warning: target.trust.warning ? safeTerminalLine(target.trust.warning, 512) : undefined,
49
+ },
50
+ };
51
+ }
52
+
53
+ export interface ResolveSubagentTargetOptions {
54
+ workspace: string;
55
+ requestedCwd?: string;
56
+ currentProjectTrusted: boolean;
57
+ agentDir?: string;
58
+ }
59
+
60
+ export function resolveSubagentTarget(
61
+ options: ResolveSubagentTargetOptions,
62
+ ): ResolvedSubagentTarget {
63
+ const workspace = canonicalDirectory(options.workspace, "Current workspace");
64
+ const requested = path.resolve(options.workspace, options.requestedCwd ?? options.workspace);
65
+ const cwd = canonicalDirectory(requested, "Subagent working directory");
66
+ const boundary: TargetBoundary = isEqualOrDescendant(cwd, workspace)
67
+ ? "current-workspace"
68
+ : "external";
69
+ if (boundary === "current-workspace") {
70
+ return {
71
+ cwd,
72
+ workspace,
73
+ boundary,
74
+ trust: {
75
+ kind: options.currentProjectTrusted ? "session-trusted" : "session-untrusted",
76
+ projectTrusted: options.currentProjectTrusted,
77
+ sourcePath: workspace,
78
+ },
79
+ };
80
+ }
81
+ try {
82
+ const entry = new ProjectTrustStore(options.agentDir ?? getAgentDir()).getEntry(cwd);
83
+ if (!entry) {
84
+ return {
85
+ cwd,
86
+ workspace,
87
+ boundary,
88
+ trust: { kind: "unsaved", projectTrusted: false },
89
+ };
90
+ }
91
+ return {
92
+ cwd,
93
+ workspace,
94
+ boundary,
95
+ trust: {
96
+ kind: entry.decision ? "saved-trusted" : "saved-denied",
97
+ projectTrusted: entry.decision,
98
+ sourcePath: entry.path,
99
+ },
100
+ };
101
+ } catch {
102
+ return {
103
+ cwd,
104
+ workspace,
105
+ boundary,
106
+ trust: {
107
+ kind: "trust-error",
108
+ projectTrusted: false,
109
+ warning: safeTerminalLine(
110
+ "Could not resolve Pi trust store; protected target resources were disabled. Repair trust with Pi /trust and restart Pi.",
111
+ 512,
112
+ ),
113
+ },
114
+ };
115
+ }
116
+ }
117
+
118
+ export function assertConsultationTargetAllowed(
119
+ target: ResolvedSubagentTarget,
120
+ policy: ConsultationCwdPolicy,
121
+ ): void {
122
+ if (policy === "current-workspace" && target.boundary !== "current-workspace") {
123
+ throw new Error(
124
+ `Read-only consultation target is outside the current workspace: ${safeTerminalLine(target.cwd)}`,
125
+ );
126
+ }
127
+ }
128
+
129
+ export function assertDelegationTargetAllowed(
130
+ target: ResolvedSubagentTarget,
131
+ policy: DelegationCwdPolicy,
132
+ ): void {
133
+ if (target.boundary === "current-workspace" || policy === "anywhere") return;
134
+ if (policy === "trusted-targets" && target.trust.kind === "saved-trusted") return;
135
+ if (policy === "current-workspace") {
136
+ throw new Error(
137
+ `General delegation target is outside the current workspace: ${safeTerminalLine(target.cwd)}`,
138
+ );
139
+ }
140
+ const reason =
141
+ target.trust.kind === "trust-error"
142
+ ? target.trust.warning
143
+ : `Target trust is ${target.trust.kind}.`;
144
+ throw new Error(
145
+ [
146
+ `General delegation target is not a saved-trusted folder: ${safeTerminalLine(target.cwd)}`,
147
+ reason,
148
+ "Open Pi in that folder, manage trust with /trust, restart Pi, or choose Anywhere in /subagents settings.",
149
+ ]
150
+ .filter(Boolean)
151
+ .join(" "),
152
+ );
153
+ }
154
+
155
+ function canonicalDirectory(value: string, label: string): string {
156
+ let canonical: string;
157
+ try {
158
+ canonical = fs.realpathSync(value);
159
+ } catch (error) {
160
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
161
+ throw new Error(`${label} does not exist: ${safeTerminalLine(value)}`);
162
+ }
163
+ throw new Error(
164
+ `${label} cannot be resolved: ${safeTerminalLine(value)}: ${formatError(error)}`,
165
+ );
166
+ }
167
+ if (!fs.statSync(canonical).isDirectory()) {
168
+ throw new Error(`${label} is not a directory: ${safeTerminalLine(canonical)}`);
169
+ }
170
+ return canonical;
171
+ }
172
+
173
+ function isEqualOrDescendant(candidate: string, workspace: string): boolean {
174
+ const relative = path.relative(workspace, candidate);
175
+ return (
176
+ relative === "" ||
177
+ (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))
178
+ );
179
+ }
180
+
181
+ function formatError(error: unknown): string {
182
+ return error instanceof Error ? error.message : String(error);
183
+ }
package/src/execution.ts CHANGED
@@ -4,8 +4,15 @@ import {
4
4
  type AgentConfig,
5
5
  type AgentScope,
6
6
  discoverAgents,
7
+ type SubagentSettings,
7
8
  type SubagentThinkingLevel,
8
9
  } from "./agents.js";
10
+ import {
11
+ assertDelegationTargetAllowed,
12
+ type ResolvedSubagentTarget,
13
+ resolveSubagentTarget,
14
+ targetPolicyAudit,
15
+ } from "./cwd-policy.js";
9
16
  import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
10
17
  import { hasUsableAggregator, type SubagentParams } from "./params.js";
11
18
  import {
@@ -20,7 +27,11 @@ import {
20
27
  type SubagentDetails,
21
28
  } from "./runner.js";
22
29
  import { safeTerminalLine } from "./safe-text.js";
23
- import { readSubagentSettings, resolveSubagentThinkingLevel } from "./settings.js";
30
+ import {
31
+ DEFAULT_DELEGATION_CWD_POLICY,
32
+ readSubagentSettings,
33
+ resolveSubagentThinkingLevel,
34
+ } from "./settings.js";
24
35
 
25
36
  const MAX_PARALLEL_TASKS = 8;
26
37
  const MAX_CONCURRENCY = 4;
@@ -105,6 +116,7 @@ export async function executeSubagent(
105
116
  signal: AbortSignal | undefined,
106
117
  onUpdate: AgentToolUpdateCallback<SubagentDetails> | undefined,
107
118
  ctx: ExtensionContext,
119
+ settingsOverride?: SubagentSettings,
108
120
  ): Promise<AgentToolResult<SubagentDetails> & { isError?: boolean }> {
109
121
  assertSubagentDepthAllowed();
110
122
  const agentScope: AgentScope = params.agentScope ?? "user";
@@ -112,7 +124,7 @@ export async function executeSubagent(
112
124
  throw new Error("Project-local subagent definitions require a trusted project");
113
125
  }
114
126
  const aggregator = hasUsableAggregator(params.aggregator) ? params.aggregator : undefined;
115
- const config = readSubagentSettings();
127
+ const config = settingsOverride ?? readSubagentSettings();
116
128
  const discovery = discoverAgents(ctx.cwd, agentScope, config);
117
129
  const agents = discovery.agents;
118
130
  const confirmProjectAgents = params.confirmProjectAgents ?? true;
@@ -156,6 +168,28 @@ export async function executeSubagent(
156
168
  };
157
169
  }
158
170
 
171
+ const delegationPolicy = config?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY;
172
+ const resolveTarget = (cwd: string | undefined): ResolvedSubagentTarget => {
173
+ const target = resolveSubagentTarget({
174
+ workspace: ctx.cwd,
175
+ requestedCwd: cwd,
176
+ currentProjectTrusted: ctx.isProjectTrusted(),
177
+ });
178
+ assertDelegationTargetAllowed(target, delegationPolicy);
179
+ return target;
180
+ };
181
+ const singleTarget = hasSingle ? resolveTarget(params.cwd) : undefined;
182
+ const chainTargets = params.chain?.map((step) => resolveTarget(step.cwd)) ?? [];
183
+ const parallelTargets = params.tasks?.map((task) => resolveTarget(task.cwd)) ?? [];
184
+ const aggregatorTarget = aggregator ? resolveTarget(aggregator.cwd) : undefined;
185
+ const attachTarget = (result: SingleResult, target: ResolvedSubagentTarget): SingleResult => {
186
+ result.target = targetPolicyAudit(target);
187
+ return result;
188
+ };
189
+ const launchPolicy = (target: ResolvedSubagentTarget) => ({
190
+ projectTrust: target.trust.projectTrusted,
191
+ });
192
+
159
193
  if (agentScope === "project" || agentScope === "both") {
160
194
  const requestedAgentNames = new Set<string>();
161
195
  if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
@@ -180,6 +214,11 @@ export async function executeSubagent(
180
214
  "Run project-local agents?",
181
215
  `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
182
216
  );
217
+ if (signal?.aborted) {
218
+ const error = new Error("Subagent call was aborted during project-agent confirmation");
219
+ error.name = "AbortError";
220
+ throw error;
221
+ }
183
222
  if (!ok) {
184
223
  return {
185
224
  content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
@@ -219,18 +258,24 @@ export async function executeSubagent(
219
258
  }
220
259
  : undefined;
221
260
 
222
- const result = await runSingleAgent(
223
- ctx.cwd,
224
- agents,
225
- step.agent,
226
- taskWithContext,
227
- step.cwd,
228
- i + 1,
229
- signal,
230
- resolveThinkingLevel(step.agent, step.thinkingLevel),
231
- resolveTimeoutMs(step.agent, step.timeoutMs),
232
- chainUpdate,
233
- makeDetails("chain"),
261
+ const target = chainTargets[i];
262
+ const result = attachTarget(
263
+ await runSingleAgent(
264
+ ctx.cwd,
265
+ agents,
266
+ step.agent,
267
+ taskWithContext,
268
+ target.cwd,
269
+ i + 1,
270
+ signal,
271
+ resolveThinkingLevel(step.agent, step.thinkingLevel),
272
+ resolveTimeoutMs(step.agent, step.timeoutMs),
273
+ chainUpdate,
274
+ makeDetails("chain"),
275
+ undefined,
276
+ launchPolicy(target),
277
+ ),
278
+ target,
234
279
  );
235
280
  results.push(result);
236
281
 
@@ -352,24 +397,30 @@ export async function executeSubagent(
352
397
  params.tasks,
353
398
  MAX_CONCURRENCY,
354
399
  async (t, index) => {
355
- const result = await runSingleAgent(
356
- ctx.cwd,
357
- agents,
358
- t.agent,
359
- t.task,
360
- t.cwd,
361
- undefined,
362
- signal,
363
- resolveThinkingLevel(t.agent, t.thinkingLevel),
364
- resolveTimeoutMs(t.agent, t.timeoutMs),
365
- // Per-task update callback
366
- (partial) => {
367
- if (partial.details?.results[0]) {
368
- allResults[index] = { ...partial.details.results[0], exitCode: -1 };
369
- emitParallelUpdate();
370
- }
371
- },
372
- makeDetails("parallel"),
400
+ const target = parallelTargets[index];
401
+ const result = attachTarget(
402
+ await runSingleAgent(
403
+ ctx.cwd,
404
+ agents,
405
+ t.agent,
406
+ t.task,
407
+ target.cwd,
408
+ undefined,
409
+ signal,
410
+ resolveThinkingLevel(t.agent, t.thinkingLevel),
411
+ resolveTimeoutMs(t.agent, t.timeoutMs),
412
+ // Per-task update callback
413
+ (partial) => {
414
+ if (partial.details?.results[0]) {
415
+ allResults[index] = { ...partial.details.results[0], exitCode: -1 };
416
+ emitParallelUpdate();
417
+ }
418
+ },
419
+ makeDetails("parallel"),
420
+ undefined,
421
+ launchPolicy(target),
422
+ ),
423
+ target,
373
424
  );
374
425
  allResults[index] = result;
375
426
  doneCount += 1;
@@ -405,26 +456,32 @@ export async function executeSubagent(
405
456
  : `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`,
406
457
  DEFAULT_MAX_CONTEXT_BYTES,
407
458
  ).text;
408
- aggregatorResult = await runSingleAgent(
409
- ctx.cwd,
410
- agents,
411
- aggregator.agent,
412
- aggregatorTask,
413
- aggregator.cwd,
414
- undefined,
415
- signal,
416
- resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel),
417
- resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
418
- (partial) => {
419
- status.update(fanInStatus(aggregator.agent));
420
- if (onUpdate && partial.details?.results[0]) {
421
- onUpdate({
422
- content: partial.content,
423
- details: makeDetails("parallel")(results, partial.details.results[0]),
424
- });
425
- }
426
- },
427
- makeDetails("parallel"),
459
+ const target = aggregatorTarget as ResolvedSubagentTarget;
460
+ aggregatorResult = attachTarget(
461
+ await runSingleAgent(
462
+ ctx.cwd,
463
+ agents,
464
+ aggregator.agent,
465
+ aggregatorTask,
466
+ target.cwd,
467
+ undefined,
468
+ signal,
469
+ resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel),
470
+ resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
471
+ (partial) => {
472
+ status.update(fanInStatus(aggregator.agent));
473
+ if (onUpdate && partial.details?.results[0]) {
474
+ onUpdate({
475
+ content: partial.content,
476
+ details: makeDetails("parallel")(results, partial.details.results[0]),
477
+ });
478
+ }
479
+ },
480
+ makeDetails("parallel"),
481
+ undefined,
482
+ launchPolicy(target),
483
+ ),
484
+ target,
428
485
  );
429
486
  }
430
487
 
@@ -469,18 +526,24 @@ export async function executeSubagent(
469
526
  const status = startSubagentStatus(ctx, toolCallId, singleStatus(params.agent));
470
527
 
471
528
  try {
472
- const result = await runSingleAgent(
473
- ctx.cwd,
474
- agents,
475
- params.agent,
476
- params.task,
477
- params.cwd,
478
- undefined,
479
- signal,
480
- resolveThinkingLevel(params.agent, params.thinkingLevel),
481
- resolveTimeoutMs(params.agent, params.timeoutMs),
482
- onUpdate,
483
- makeDetails("single"),
529
+ const target = singleTarget as ResolvedSubagentTarget;
530
+ const result = attachTarget(
531
+ await runSingleAgent(
532
+ ctx.cwd,
533
+ agents,
534
+ params.agent,
535
+ params.task,
536
+ target.cwd,
537
+ undefined,
538
+ signal,
539
+ resolveThinkingLevel(params.agent, params.thinkingLevel),
540
+ resolveTimeoutMs(params.agent, params.timeoutMs),
541
+ onUpdate,
542
+ makeDetails("single"),
543
+ undefined,
544
+ launchPolicy(target),
545
+ ),
546
+ target,
484
547
  );
485
548
  const isError = isResultError(result);
486
549
  if (isError) {
@@ -339,7 +339,8 @@ export async function createSdkChildSession(
339
339
  options.agent.cwd,
340
340
  agentDir,
341
341
  options.agentConfig.systemPrompt,
342
- options.agent.agentScope === "project" || options.agent.agentScope === "both",
342
+ options.agent.target?.trust.projectTrusted ??
343
+ (options.agent.agentScope === "project" || options.agent.agentScope === "both"),
343
344
  );
344
345
  const resolved = await resolveChildModel(options);
345
346
  const modelRuntime = await createChildModelRuntime(
@@ -508,8 +509,7 @@ export async function createInProcessResourceLoader(
508
509
  agentSystemPrompt: string,
509
510
  projectTrusted = false,
510
511
  ): Promise<{ loader: DefaultResourceLoader; settingsManager: SettingsManager }> {
511
- const settingsManager = SettingsManager.create(cwd, agentDir);
512
- settingsManager.setProjectTrusted(projectTrusted);
512
+ const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
513
513
  const loader = new DefaultResourceLoader({
514
514
  cwd,
515
515
  agentDir,