@signalridge/pi-subagents 1.11.0 → 1.12.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/src/index.ts CHANGED
@@ -165,6 +165,9 @@ import {
165
165
  streamToOutputFile,
166
166
  writeInitialEntry,
167
167
  } from "./output-file.js";
168
+ import { registerParentQuestions } from "./parent-questions.js";
169
+ import { captureProjectTrust, isProjectResource } from "./project-trust.js";
170
+ import { captureQuestionOrigin } from "./question-origin.js";
168
171
  import { SubagentScheduler } from "./schedule.js";
169
172
  import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
170
173
  import {
@@ -582,16 +585,18 @@ export default function (pi: ExtensionAPI): void {
582
585
  if (inert) return;
583
586
  if (!rootRuntime) {
584
587
  try {
585
- const startupSettings = loadSettings(ctx.cwd);
588
+ const trust = captureProjectTrust(ctx);
589
+ const startupSettings = loadSettings(trust.cwd, trust.trusted);
586
590
  const startupAgents = loadCustomAgents(
587
- ctx.cwd,
591
+ trust.cwd,
588
592
  startupSettings.strictAgentFiles === true,
593
+ trust.trusted,
589
594
  );
590
595
  rootRuntime = activateRootRuntime(
591
596
  pi,
592
597
  startupSettings,
593
598
  startupAgents,
594
- ctx.cwd,
599
+ ctx,
595
600
  unsubscribeRootContext,
596
601
  );
597
602
  } catch (error: unknown) {
@@ -611,10 +616,12 @@ function activateRootRuntime(
611
616
  pi: ExtensionAPI,
612
617
  startupSettings: SubagentsSettings,
613
618
  startupAgents: Map<string, AgentConfig>,
614
- initialCwd: string,
619
+ initialContext: ExtensionContext,
615
620
  unsubscribeRootContext: () => void,
616
621
  ): RootRuntime {
617
- let sessionCwd = initialCwd;
622
+ let configurationOwner = initialContext;
623
+ let activeTrust = captureProjectTrust(configurationOwner);
624
+ let sessionCwd = activeTrust.cwd;
618
625
  let strictAgentFiles = startupSettings.strictAgentFiles === true;
619
626
 
620
627
  // ---- Register custom notification renderer ----
@@ -668,7 +675,12 @@ function activateRootRuntime(
668
675
 
669
676
  /** Reload agents from project/global custom agent dirs and merge with defaults (called on init and each Agent invocation). */
670
677
  const reloadCustomAgents = (strict = false) => {
671
- const userAgents = loadCustomAgents(sessionCwd, strict);
678
+ const trust = captureProjectTrust(configurationOwner);
679
+ const changed = trust.cwd !== activeTrust.cwd || trust.trusted !== activeTrust.trusted;
680
+ activeTrust = trust;
681
+ sessionCwd = trust.cwd;
682
+ if (changed) applyRuntimeSettings(loadSettings(trust.cwd, trust.trusted));
683
+ const userAgents = loadCustomAgents(trust.cwd, strict, trust.trusted);
672
684
  registerAgents(userAgents);
673
685
  };
674
686
 
@@ -1184,6 +1196,28 @@ function activateRootRuntime(
1184
1196
  // same session. Invalidate pending off-screen mention turns only when the
1185
1197
  // replacement commits: before-switch/tree handlers can still cancel it.
1186
1198
  let sessionGeneration = 0;
1199
+ const parentQuestions = registerParentQuestions(pi, () => currentCtx);
1200
+ manager.setParentQuestionFactory((record, ctx) => {
1201
+ const generation = sessionGeneration;
1202
+ const sessionId = ctx.sessionManager.getSessionId();
1203
+ const originCurrent = captureQuestionOrigin(ctx);
1204
+ return (question, choices, signal) => {
1205
+ // A retained channel can resume after ordinary native appends. Each
1206
+ // question also owns the LIVE complete prefix at its own invocation.
1207
+ const questionOriginCurrent = currentCtx ? captureQuestionOrigin(currentCtx) : () => false;
1208
+ const controller = record.abortController;
1209
+ const owner = JSON.stringify(record.owner);
1210
+ const isCurrent = () => currentCtx !== undefined &&
1211
+ sessionGeneration === generation && boundSessionId === sessionId &&
1212
+ originCurrent(currentCtx) && questionOriginCurrent(currentCtx) &&
1213
+ manager.getRecordMutable(record.id) === record && !record.detached &&
1214
+ record.isBackground === true && record.status === "running" &&
1215
+ record.abortController === controller && !controller?.signal.aborted &&
1216
+ JSON.stringify(record.owner) === owner;
1217
+ return parentQuestions.broker.request({ agentId: record.id, isCurrent }, question, choices,
1218
+ controller ? AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]) : signal);
1219
+ };
1220
+ });
1187
1221
  // Public synchronous bus preflight works in either extension load order. A
1188
1222
  // filtered-out activation has no bound context and must not veto navigation.
1189
1223
  pi.events.on("pi:navigation-preflight", (raw: unknown) => {
@@ -1254,8 +1288,17 @@ function activateRootRuntime(
1254
1288
  manager.detachForBranchChange();
1255
1289
  manager.resetManagedSpawns();
1256
1290
  }
1291
+ parentQuestions.retire();
1257
1292
  sessionGeneration++;
1258
- sessionCwd = ctx.cwd;
1293
+ const replacingConfiguration = currentCtx !== undefined;
1294
+ configurationOwner = ctx;
1295
+ const trust = captureProjectTrust(ctx);
1296
+ sessionCwd = trust.cwd;
1297
+ if (replacingConfiguration) {
1298
+ activeTrust = trust;
1299
+ applyRuntimeSettings(loadSettings(trust.cwd, trust.trusted));
1300
+ }
1301
+ reloadCustomAgents();
1259
1302
  boundSessionId = ctx.sessionManager?.getSessionId?.();
1260
1303
  currentCtx = ctx;
1261
1304
  await claimRpcRegistry(ctx, event.reason === "reload");
@@ -1370,6 +1413,7 @@ function activateRootRuntime(
1370
1413
  const detachCommittedTree = (event: unknown): void => {
1371
1414
  if (!currentCtx || detachedTreeEvent === event) return;
1372
1415
  detachedTreeEvent = event;
1416
+ parentQuestions.retire();
1373
1417
  manager.detachForBranchChange();
1374
1418
  };
1375
1419
  // If workflows' committed-tree handler runs first, it must detach our old
@@ -1386,6 +1430,8 @@ function activateRootRuntime(
1386
1430
  // even when subagents' session_tree handler runs first.
1387
1431
  pi.events.emit("subagents:session_tree_committed", { event });
1388
1432
  currentCtx = ctx;
1433
+ configurationOwner = ctx;
1434
+ reloadCustomAgents();
1389
1435
  scheduler.stop();
1390
1436
  manager.clearCompleted(true);
1391
1437
  manager.resetManagedSpawns();
@@ -1426,6 +1472,7 @@ function activateRootRuntime(
1426
1472
  },
1427
1473
  });
1428
1474
  scheduler.stop();
1475
+ parentQuestions.retire();
1429
1476
  currentCtx = undefined; // reject new spawns; owned cleanup RPC remains bound
1430
1477
  if (pending.length > 0) {
1431
1478
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -1860,8 +1907,8 @@ function activateRootRuntime(
1860
1907
 
1861
1908
  // ---- Agent tool description mode ----
1862
1909
  // "full" (default) keeps the rich Claude Code-style description; "compact"
1863
- // swaps in a ~75% smaller one for small/local models (#91). Read once at
1864
- // tool registration — flipping it applies on the next pi session.
1910
+ // swaps in a ~75% smaller one for small/local models (#91). The description
1911
+ // getter rechecks configuration authority so replacements cannot retain denied text.
1865
1912
  let toolDescriptionMode: ToolDescriptionMode = "full";
1866
1913
  function getToolDescriptionMode(): ToolDescriptionMode {
1867
1914
  return toolDescriptionMode;
@@ -1961,40 +2008,47 @@ function activateRootRuntime(
1961
2008
 
1962
2009
  // Apply the already-validated session settings and emit the same lifecycle
1963
2010
  // event that the normal startup path exposes. Do not re-read process.cwd().
1964
- applySettings(startupSettings, {
1965
- setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
1966
- setDefaultMaxTurns,
1967
- setGraceTurns,
1968
- setDefaultMaxTokens,
1969
- setDefaultMaxToolCalls,
1970
- setDefaultToolTimeout: (ms) => setDefaultToolTimeoutMs(ms),
1971
- setDefaultJoinMode,
1972
- setDefaultModel,
1973
- setSchedulingEnabled,
1974
- setScopeModels: setScopeModelsEnabled,
1975
- setStrictAgentFiles: (enabled) => {
1976
- strictAgentFiles = enabled;
1977
- },
1978
- setDisableDefaultAgents,
1979
- setToolDescriptionMode,
1980
- setFleetView: setFleetViewEnabled,
1981
- setOutputTranscript: setOutputTranscriptDefault,
1982
- setRememberAgents: (enabled) => manager.setRememberAgents(enabled),
1983
- setAgentMentions: (enabled) => {
1984
- agentMentionMode = enabled;
1985
- },
1986
- setSupervisorQuestions: (enabled) => {
1987
- supervisorQuestionsEnabled = enabled;
1988
- manager.setSupervisorQuestions(enabled);
1989
- },
1990
- setMaxSubagentDepth,
1991
- setMaxSubagentSpawnsPerBranch: (n) =>
1992
- manager.setMaxSubagentSpawnsPerBranch(n),
1993
- setFallbackSubagent,
1994
- setWorktreeIsolation: setWorktreeIsolationEnabled,
1995
- setAgentTiers: setAgentTiersSettings,
1996
- });
1997
- pi.events.emit("subagents:settings_loaded", { settings: startupSettings });
2011
+ const baselineSettings: SubagentsSettings = { ...snapshotSettings(), strictAgentFiles: false, agentTiers: {} };
2012
+ function applyRuntimeSettings(settings: SubagentsSettings): void {
2013
+ // Clear optional policies as well: absence must not retain the old project's values.
2014
+ setDefaultModel(settings.defaultModel ?? baselineSettings.defaultModel);
2015
+ setFallbackSubagent(settings.fallbackSubagent ?? baselineSettings.fallbackSubagent);
2016
+ applySettings({ ...baselineSettings, ...settings }, {
2017
+ setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
2018
+ setDefaultMaxTurns,
2019
+ setGraceTurns,
2020
+ setDefaultMaxTokens,
2021
+ setDefaultMaxToolCalls,
2022
+ setDefaultToolTimeout: (ms) => setDefaultToolTimeoutMs(ms),
2023
+ setDefaultJoinMode,
2024
+ setDefaultModel,
2025
+ setSchedulingEnabled,
2026
+ setScopeModels: setScopeModelsEnabled,
2027
+ setStrictAgentFiles: (enabled) => {
2028
+ strictAgentFiles = enabled;
2029
+ },
2030
+ setDisableDefaultAgents,
2031
+ setToolDescriptionMode,
2032
+ setFleetView: setFleetViewEnabled,
2033
+ setOutputTranscript: setOutputTranscriptDefault,
2034
+ setRememberAgents: (enabled) => manager.setRememberAgents(enabled),
2035
+ setAgentMentions: (enabled) => {
2036
+ agentMentionMode = enabled;
2037
+ },
2038
+ setSupervisorQuestions: (enabled) => {
2039
+ supervisorQuestionsEnabled = enabled;
2040
+ manager.setSupervisorQuestions(enabled);
2041
+ },
2042
+ setMaxSubagentDepth,
2043
+ setMaxSubagentSpawnsPerBranch: (n) =>
2044
+ manager.setMaxSubagentSpawnsPerBranch(n),
2045
+ setFallbackSubagent,
2046
+ setWorktreeIsolation: setWorktreeIsolationEnabled,
2047
+ setAgentTiers: setAgentTiersSettings,
2048
+ });
2049
+ pi.events.emit("subagents:settings_loaded", { settings });
2050
+ }
2051
+ applyRuntimeSettings(startupSettings);
1998
2052
 
1999
2053
  // A tier that nothing defines is a typo, and the resolver would only reach it
2000
2054
  // on the first spawn that needs it — possibly minutes in, mid-task. Report it
@@ -2023,7 +2077,7 @@ function activateRootRuntime(
2023
2077
  * prose working. A delivered mention costs no main-model turn; the answer
2024
2078
  * arrives through the ordinary completion notification either way.
2025
2079
  */
2026
- pi.on("input", async (event, ctx) => {
2080
+ pi.on("input", parentQuestions.observeHandledInput(async (event, ctx) => {
2027
2081
  // Never hijack text the extension layer itself submitted (pi.sendMessage,
2028
2082
  // scheduled prompts) — only something a person typed can be a mention.
2029
2083
  if (event.source === "extension" || !isAgentMentionsEnabled())
@@ -2201,7 +2255,7 @@ function activateRootRuntime(
2201
2255
  // In `model` mode an off-screen mention-only turn rewrites the typed task
2202
2256
  // without copying parent history or its request-local context hooks.
2203
2257
  // Nothing reaches the chat, and it starts an ordinary top-level agent.
2204
- const registeredAgentTool = agentToolRef;
2258
+ const registeredAgentTool = agentToolRef ? freshAgentToolDefinition() : undefined;
2205
2259
  if (getAgentMentionMode() === "model" && registeredAgentTool) {
2206
2260
  // The hidden turn is itself a provider call. Refuse before saying
2207
2261
  // "Starting" or spending that call when its inherited parent model is
@@ -2262,7 +2316,7 @@ function activateRootRuntime(
2262
2316
  );
2263
2317
  }
2264
2318
  return { action: "handled" };
2265
- });
2319
+ }));
2266
2320
 
2267
2321
  // ---- Agent tool ----
2268
2322
 
@@ -2294,15 +2348,13 @@ function activateRootRuntime(
2294
2348
  // The catalogue is injected into the description rather than left to a lookup
2295
2349
  // tool: the host has to pick a tier on its first call, and a tool it must
2296
2350
  // remember to call first is a tool it will skip.
2297
- const tierListText = buildAgentTierListText();
2298
- const compactTierListText = buildCompactAgentTierListText();
2299
- const tierSection = tierListText ? `\n\n${tierListText}` : "";
2300
- const compactTierSection = compactTierListText
2301
- ? `\n\n${compactTierListText}`
2302
- : "";
2351
+ const tierSection = (compact = false): string => {
2352
+ const text = compact ? buildCompactAgentTierListText() : buildAgentTierListText();
2353
+ return text ? `\n\n${text}` : "";
2354
+ };
2303
2355
 
2304
- const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
2305
- ${buildCompactTypeListText()}${compactTierSection}
2356
+ const compactAgentToolDescription = () => `Launch an autonomous agent for complex, multi-step tasks. Agent types:
2357
+ ${buildCompactTypeListText()}${tierSection(true)}
2306
2358
 
2307
2359
  Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global).
2308
2360
 
@@ -2314,12 +2366,12 @@ Notes:
2314
2366
  - isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.
2315
2367
  - Raw inherit_context is unavailable; put only an explicitly sanitized summary in the task prompt.`;
2316
2368
 
2317
- const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
2369
+ const fullAgentToolDescription = () => `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
2318
2370
 
2319
2371
  Available agent types and the tools they have access to:
2320
2372
  ${buildTypeListText()}
2321
2373
 
2322
- Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.${tierSection}
2374
+ Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.${tierSection()}
2323
2375
 
2324
2376
  When using the Agent tool, specify a subagent_type parameter to select which agent type to use.
2325
2377
 
@@ -2367,8 +2419,8 @@ Terse command-style prompts produce shallow, generic work.
2367
2419
  // Both carry their own leading blank line, so a template that drops the
2368
2420
  // placeholder inline renders byte-identically to the built-in description
2369
2421
  // whether or not any tier is configured.
2370
- tierList: () => tierSection,
2371
- compactTierList: () => compactTierSection,
2422
+ tierList: () => tierSection(),
2423
+ compactTierList: () => tierSection(true),
2372
2424
  defaultTier: getDefaultAgentTierText,
2373
2425
  agentDir: getAgentDir,
2374
2426
  scheduleGuideline: () => scheduleGuideline,
@@ -2384,10 +2436,12 @@ Terse command-style prompts produce shallow, generic work.
2384
2436
  };
2385
2437
 
2386
2438
  const loadCustomToolDescription = (): string | undefined => {
2439
+ const trust = captureProjectTrust(configurationOwner);
2387
2440
  for (const path of [
2388
- join(sessionCwd, ".pi", "agent-tool-description.md"),
2441
+ ...(trust.trusted ? [join(trust.cwd, ".pi", "agent-tool-description.md")] : []),
2389
2442
  join(getAgentDir(), "agent-tool-description.md"),
2390
2443
  ]) {
2444
+ if (!trust.trusted && isProjectResource(path, trust.cwd)) continue;
2391
2445
  try {
2392
2446
  if (!existsSync(path)) continue;
2393
2447
  const text = readFileSync(path, "utf-8").trim();
@@ -2402,9 +2456,11 @@ Terse command-style prompts produce shallow, generic work.
2402
2456
  return undefined;
2403
2457
  };
2404
2458
 
2405
- const agentToolDescription = (() => {
2459
+ const agentToolDescription = () => {
2460
+ const trust = captureProjectTrust(configurationOwner);
2461
+ if (trust.cwd !== activeTrust.cwd || trust.trusted !== activeTrust.trusted) reloadCustomAgents();
2406
2462
  const mode = getToolDescriptionMode();
2407
- if (mode === "compact") return compactAgentToolDescription;
2463
+ if (mode === "compact") return compactAgentToolDescription();
2408
2464
  if (mode === "custom") {
2409
2465
  const custom = loadCustomToolDescription();
2410
2466
  if (custom) return custom;
@@ -2412,16 +2468,18 @@ Terse command-style prompts produce shallow, generic work.
2412
2468
  '[pi-subagents] toolDescriptionMode is "custom" but no agent-tool-description.md found — using "full"',
2413
2469
  );
2414
2470
  }
2415
- return fullAgentToolDescription;
2416
- })();
2471
+ return fullAgentToolDescription();
2472
+ };
2417
2473
 
2418
2474
  // Captured so the mention clone can hand the copy the REAL registered tool:
2419
2475
  // its handler closes over this activation, which is what makes a clone-driven
2420
2476
  // spawn an ordinary top-level agent rather than something the fork owns.
2477
+ const agentTypeParameterDescription = () =>
2478
+ `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`;
2421
2479
  const agentTool = defineTool({
2422
2480
  name: SUBAGENT_TOOL_NAMES.AGENT,
2423
2481
  label: "Agent",
2424
- description: agentToolDescription,
2482
+ get description() { return agentToolDescription(); },
2425
2483
  promptSnippet: "Launch autonomous sub-agents for complex multi-step tasks",
2426
2484
  promptGuidelines: [
2427
2485
  "Use Agent with specialized agents when the task matches an agent type's description. Subagents are valuable for parallelizing independent queries or for protecting the main context window from excessive results, but should not be used excessively when not needed. Importantly, avoid duplicating work that subagents are already doing — if you delegate research to a subagent, do not also perform the same searches yourself.",
@@ -2438,7 +2496,7 @@ Terse command-style prompts produce shallow, generic work.
2438
2496
  "A short (3-5 word) description of the task (shown in UI).",
2439
2497
  }),
2440
2498
  subagent_type: Type.String({
2441
- description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`,
2499
+ description: agentTypeParameterDescription(),
2442
2500
  }),
2443
2501
  tier: Type.Optional(
2444
2502
  Type.String({
@@ -2458,6 +2516,9 @@ Terse command-style prompts produce shallow, generic work.
2458
2516
  "Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
2459
2517
  }),
2460
2518
  ),
2519
+ supervisor_questions: Type.Optional(Type.Union([Type.Literal("human"), Type.Literal("parent"), Type.Literal("off")], {
2520
+ description: 'Explicit question routing, default human. "parent" is ordinary clarification only (never permission/approval), supported only for fresh non-isolated top-level background calls. No schedules or mode-changing resumes.',
2521
+ })),
2461
2522
  resume: Type.Optional(
2462
2523
  Type.String({
2463
2524
  description:
@@ -2692,6 +2753,16 @@ Terse command-style prompts produce shallow, generic work.
2692
2753
  ...params,
2693
2754
  agentTiers: getAgentTiersSettings(),
2694
2755
  });
2756
+ if (params.supervisor_questions === "parent" &&
2757
+ (!resolvedConfig.runInBackground || resolvedConfig.isolated || params.schedule || params.resume || !isSupervisorQuestionsEnabled())) {
2758
+ throw new Error('supervisor_questions: "parent" requires a fresh non-isolated background Agent. Set run_in_background: true, omit schedule/resume, and enable Supervisor questions. Foreground calls cannot answer while blocked.');
2759
+ }
2760
+ if (params.schedule && params.supervisor_questions !== undefined) {
2761
+ throw new Error('Scheduled calls do not support per-call question routing; omit supervisor_questions.');
2762
+ }
2763
+ if (params.resume && params.supervisor_questions !== undefined) {
2764
+ throw new Error('Resume preserves the child question mode; omit supervisor_questions and use run_in_background: true for a parent-mode child.');
2765
+ }
2695
2766
  if (!params.resume && !params.schedule && resolvedConfig.inheritContext) {
2696
2767
  return textResult(INHERIT_CONTEXT_UNAVAILABLE);
2697
2768
  }
@@ -2720,7 +2791,8 @@ Terse command-style prompts produce shallow, generic work.
2720
2791
  if (!resolvedConfig.agentTierSelected) {
2721
2792
  const scopeVerdict = checkModelScope({
2722
2793
  model,
2723
- cwd: ctx.cwd,
2794
+ cwd: activeTrust.cwd,
2795
+ authority: activeTrust,
2724
2796
  modelRegistry: ctx.modelRegistry,
2725
2797
  callerSupplied: resolvedConfig.modelFromParams,
2726
2798
  agentLabel: customConfig?.displayName ?? subagentType,
@@ -3009,6 +3081,7 @@ Terse command-style prompts produce shallow, generic work.
3009
3081
  : thinking,
3010
3082
  agentTier: resolvedConfig.requestedAgentTier,
3011
3083
  isBackground: true,
3084
+ supervisorQuestions: params.supervisor_questions === "parent" ? "parent" : params.supervisor_questions === "off" ? false : undefined,
3012
3085
  isolation,
3013
3086
  invocation: agentInvocation,
3014
3087
  rootSessionId: ctx.sessionManager.getSessionId(),
@@ -3166,6 +3239,7 @@ Terse command-style prompts produce shallow, generic work.
3166
3239
  : thinking,
3167
3240
  agentTier: resolvedConfig.requestedAgentTier,
3168
3241
  isolation,
3242
+ supervisorQuestions: params.supervisor_questions === "off" ? false : undefined,
3169
3243
  invocation: agentInvocation,
3170
3244
  signal,
3171
3245
  rootSessionId: ctx.sessionManager.getSessionId(),
@@ -3217,6 +3291,42 @@ Terse command-style prompts produce shallow, generic work.
3217
3291
  pi.registerTool(agentTool);
3218
3292
  agentToolRef = agentTool;
3219
3293
 
3294
+ // Pi snapshots description and parameters when wrapping a registered tool.
3295
+ // Trust changes alone emit no host event. Re-register before each new prompt,
3296
+ // ahead of Pi's effective loadout snapshot, using its public post-bind refresh.
3297
+ // Turn end also covers provider continuations after tools revoke authority.
3298
+ // Keep the same exposure/execution policy and retain the active set.
3299
+ function freshAgentToolDefinition() {
3300
+ reloadCustomAgents();
3301
+ return {
3302
+ ...agentTool,
3303
+ description: agentToolDescription(),
3304
+ parameters: {
3305
+ ...agentTool.parameters,
3306
+ properties: {
3307
+ ...agentTool.parameters.properties,
3308
+ subagent_type: { ...agentTool.parameters.properties.subagent_type, description: agentTypeParameterDescription() },
3309
+ tier: { ...agentTool.parameters.properties.tier, description: buildAgentTierParameterDescription() },
3310
+ },
3311
+ },
3312
+ };
3313
+ }
3314
+ const refreshAgentDeclaration = () => {
3315
+ const refreshed = freshAgentToolDefinition();
3316
+ const activeTools = pi.getActiveTools();
3317
+ pi.registerTool(refreshed);
3318
+ // Pi's explicit --tools/SDK tools selector can re-activate every matching
3319
+ // name during registry refresh. A metadata refresh must not undo a later
3320
+ // activation/exposure decision by the host or another extension.
3321
+ const refreshedActive = pi.getActiveTools();
3322
+ if (activeTools.length !== refreshedActive.length || activeTools.some((name, i) => name !== refreshedActive[i])) {
3323
+ pi.setActiveTools(activeTools);
3324
+ }
3325
+ agentToolRef = refreshed;
3326
+ };
3327
+ pi.on("before_agent_start", refreshAgentDeclaration);
3328
+ pi.on("turn_end", refreshAgentDeclaration);
3329
+
3220
3330
  // ---- get_subagent_result tool ----
3221
3331
 
3222
3332
  pi.registerTool(
@@ -4834,7 +4944,7 @@ Do not wrap the response in a markdown code fence. Return only the file contents
4834
4944
  // scoped set would fail the scope check at spawn time.
4835
4945
  if (isScopeModelsEnabled()) {
4836
4946
  const enabled = resolveEnabledModels(
4837
- readEnabledModels(sessionCwd),
4947
+ readEnabledModels(sessionCwd, captureProjectTrust(configurationOwner)),
4838
4948
  ctx.modelRegistry,
4839
4949
  sessionCwd,
4840
4950
  );
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { isModelInScope, type ModelRegistryRef, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
11
+ import type { ProjectTrust } from "./project-trust.js";
11
12
 
12
13
  /**
13
14
  * When enabled, subagent model choices are validated against `enabledModels`
@@ -41,6 +42,8 @@ export type ModelScopeVerdict =
41
42
  export function checkModelScope(args: {
42
43
  model: { provider: string; id: string } | undefined;
43
44
  cwd: string;
45
+ /** Captured config root/trust and retained ancestor denials; omit only for standalone helpers. */
46
+ authority?: ProjectTrust;
44
47
  modelRegistry: ModelRegistryRef;
45
48
  /** True when the model came from the tool call rather than frontmatter. */
46
49
  callerSupplied: boolean;
@@ -52,7 +55,7 @@ export function checkModelScope(args: {
52
55
  const { model, cwd, modelRegistry, callerSupplied, agentLabel, modelInput } = args;
53
56
  if (!scopeModelsEnabled || !model) return { kind: "ok" };
54
57
 
55
- const allowed = resolveEnabledModels(readEnabledModels(cwd), modelRegistry, cwd);
58
+ const allowed = resolveEnabledModels(readEnabledModels(cwd, args.authority), modelRegistry, cwd);
56
59
  if (!allowed || isModelInScope(model, allowed)) return { kind: "ok" };
57
60
 
58
61
  // A tier whose profile says `inherit` names no model of its own, so fall back
@@ -27,6 +27,7 @@ import {
27
27
  streamToOutputFile,
28
28
  writeInitialEntry,
29
29
  } from "./output-file.js";
30
+ import { configurationContext, type ProjectTrust } from "./project-trust.js";
30
31
  import { getForegroundOutcomeNote, getStatusNote, partialOutputSuffix } from "./status-note.js";
31
32
  import type {
32
33
  AgentConfig,
@@ -104,6 +105,8 @@ export interface NestedToolContext {
104
105
  allowedSubagents: "all" | string[];
105
106
  /** Root used for agent/config discovery; may differ from the agent's working directory. */
106
107
  configCwd: string;
108
+ /** Immutable configuration authority captured by the runner, including ancestor denials. */
109
+ projectTrust: ProjectTrust;
107
110
  }
108
111
 
109
112
  function textResult(text: string, isError = false) {
@@ -152,7 +155,9 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
152
155
  // Agents resolve from a registry built for THIS branch's config root (under
153
156
  // worktree isolation, the copy). Never via registerAgents — that is
154
157
  // process-global state shared with the main session and every other agent.
155
- const loadRegistry = () => buildAgentRegistry(loadCustomAgents(context.configCwd));
158
+ const loadRegistry = () => buildAgentRegistry(loadCustomAgents(
159
+ context.configCwd, false, context.projectTrust.trusted, context.projectTrust.deniedRoots,
160
+ ));
156
161
  const allowedTypesIn = (registry: Map<string, AgentConfig>): Set<string> | undefined =>
157
162
  context.allowedSubagents === "all"
158
163
  ? undefined
@@ -250,6 +255,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
250
255
  const scopeVerdict = checkModelScope({
251
256
  model,
252
257
  cwd: context.configCwd,
258
+ authority: context.projectTrust,
253
259
  modelRegistry: ctx.modelRegistry,
254
260
  callerSupplied: invocation.modelFromParams,
255
261
  agentLabel: config?.displayName ?? resolvedType,
@@ -339,7 +345,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
339
345
  }
340
346
  };
341
347
 
342
- // `ctx` is forwarded to the manager unmodified, never captured at tool-build
348
+ // `ctx` is forwarded with configuration-root provenance, never captured at tool-build
343
349
  // time: each AgentSession builds its own ExtensionRunner from that session's
344
350
  // cwd/sessionManager/modelRegistry, so this is the CHILD's context. Capturing
345
351
  // one earlier would silently give a grandchild the wrong worktree base, the
@@ -350,7 +356,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
350
356
  // letting it escape into the child's turn.
351
357
  try {
352
358
  if (invocation.runInBackground) {
353
- const id = context.manager.spawn(context.pi, ctx, resolvedType, params.prompt, {
359
+ const id = context.manager.spawn(context.pi, configurationContext(ctx, context.configCwd, context.projectTrust), resolvedType, params.prompt, {
354
360
  ...options,
355
361
  isBackground: true,
356
362
  });
@@ -362,7 +368,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
362
368
 
363
369
  const { record } = await context.manager.spawnAndWait(
364
370
  context.pi,
365
- ctx,
371
+ configurationContext(ctx, context.configCwd, context.projectTrust),
366
372
  resolvedType,
367
373
  params.prompt,
368
374
  { ...options, signal },