/** * child/nested.ts — C1 nested subagents: the `subagent` tool registered * INSIDE the child adapter, so a child can spawn its own grandchild agents * (self-hosted delegation). * * Three locks (benchmark tintinweb nested-tools.ts §4c; nicopreme env-based * recursion guard): * 1. Depth cap — the tool is registered ONLY when PI_SUBAGENTS_NESTED=1 AND * PI_SUBAGENTS_DEPTH < PI_SUBAGENTS_MAX_DEPTH (the engine sets DEPTH=1 * and MAX_DEPTH from the config maxSubagentDepth, clamped 0..4 — 0/1 = * off). Every grandchild spawn propagates PI_SUBAGENTS_DEPTH+1, so the * recursion is bounded by construction. * 2. Strict allowlist — PI_SUBAGENTS_ALLOWED_SUBAGENTS (CSV agent names, or * `all`). An agent NOT on the list is REFUSED with a message; there is * NEVER a fallback to another agent. An empty allowlist refuses * everything. * 3. Inherited security — the grandchild env inherits the child's ZOB_* * path-policy vars (allowed/forbidden/sandbox) untouched, and the * grandchild loads the SAME child adapter via `-e`, so the write-safety * guard applies recursively. The child's OWN escalation/steer channel * envs (PI_SUBAGENTS_RUN_ID / PI_SUBAGENTS_ESCALATION_DIR / * PI_SUBAGENTS_STEER_FILE) are NOT inherited: a grandchild reports * through this tool result and its child owns the ask_master channel * (no escalation-file collisions on the shared name). * * The grandchild spawn REUSES the compiled lanes code (buildIsolatedChildArgs * + buildChildFlags + NdjsonStreamParser + attachBoundedAbort + injectable * SpawnFn): child/nested.ts compiles to dist/child/nested.js, so * `../src/lanes/*.js` resolves inside dist. Zero @earendil-works/* imports * (invariant I9); never spawns a REAL pi in tests (SpawnFn is injectable). */ import type { ExtensionAPI } from "./pi-types.js"; import type { ChildResult } from "../src/core/types.js"; import { type AgentCard } from "../src/registry/agents.js"; import { type SpawnFn } from "../src/lanes/spawn.js"; /** Master switch set by the engine when the dispatch enables nesting (C1). */ export declare const NESTED_ENV = "PI_SUBAGENTS_NESTED"; /** Current nesting depth (engine-spawned children start at 1). */ export declare const DEPTH_ENV = "PI_SUBAGENTS_DEPTH"; /** Maximum nesting depth (from config maxSubagentDepth, clamp 0..4; 0/1 = off). */ export declare const MAX_DEPTH_ENV = "PI_SUBAGENTS_MAX_DEPTH"; /** Strict CSV allowlist of grandchild agent names (`all` = any). */ export declare const ALLOWED_SUBAGENTS_ENV = "PI_SUBAGENTS_ALLOWED_SUBAGENTS"; /** Absolute agents dir used to resolve grandchild agent cards. */ export declare const AGENTS_DIR_ENV = "PI_SUBAGENTS_AGENTS_DIR"; /** pi binary override for the grandchild spawn (default `pi`). */ export declare const PI_COMMAND_ENV = "PI_SUBAGENTS_PI"; /** Child adapter path override passed to the grandchild via `-e`. */ export declare const CHILD_EXTENSION_ENV = "PI_SUBAGENTS_CHILD_EXTENSION"; /** Grandchild hard timeout in ms (default 10 min, floored at 1 s). */ export declare const NESTED_TIMEOUT_ENV = "PI_SUBAGENTS_NESTED_TIMEOUT_MS"; export declare const SUBAGENT_TOOL_NAME = "subagent"; /** Byte cap on the grandchild result returned to the child's context. */ export declare const NESTED_OUTPUT_LIMIT_BYTES = 4000; /** Default grandchild hard timeout (10 minutes). */ export declare const DEFAULT_NESTED_TIMEOUT_MS: number; /** Current nesting depth (>= 0; absent/invalid = 0). */ export declare function readDepthEnv(env: NodeJS.ProcessEnv): number; /** Maximum nesting depth, clamped to [0, 4] (absent/invalid = default 2). */ export declare function readMaxDepthEnv(env: NodeJS.ProcessEnv): number; /** Grandchild hard timeout in ms (invalid/absent = 10 min, floored at 1 s). */ export declare function readTimeoutEnv(env: NodeJS.ProcessEnv, override?: number): number; /** * Parse the strict allowlist CSV. `all` (any position, case-insensitive) * means any agent is allowed; an EMPTY/absent value yields `[]`, which * refuses everything (strict: no implicit wildcard). */ export declare function parseAllowlist(raw: string | undefined): string[] | "all"; /** Strict membership check (case-insensitive name match; `all` = any). */ export declare function agentAllowed(name: string, allowlist: string[] | "all"): boolean; /** * Lock 1: the nested tool may exist ONLY when the master switch is exactly * `1` AND the current depth is strictly below the cap. depth >= max => the * tool is NOT registered (clean refusal — the child cannot nest further). */ export declare function nestedToolEnabled(env: NodeJS.ProcessEnv): boolean; /** Default child adapter path: this module's sibling index.js (dist/child). */ export declare function defaultChildAdapterPath(): string; /** Resolve a grandchild agent card from the agents dir (case-insensitive). */ export declare function findNestedAgentCard(name: string, agentsDir: string): AgentCard | undefined; export interface NestedGrandchildInput { card: AgentCard; task: string; cwd: string; /** Current depth; the grandchild runs at depth + 1. */ depth: number; maxDepth: number; spawn?: SpawnFn; piCommand?: string; childExtension?: string; timeoutMs?: number; /** Cooperative abort propagated from the tool call. */ signal?: AbortSignal; /** Base env (defaults to process.env — inherits the ZOB_* security vars). */ env?: NodeJS.ProcessEnv; } /** * Spawn a one-shot grandchild pi via the lanes code (args + NDJSON parser + * bounded abort), with PI_SUBAGENTS_DEPTH+1 and the SAME security envs * (natural inheritance — ZOB_* vars are copied verbatim). The child's own * escalation/steer channel envs are stripped (see file header, lock 3). * Session files live in a throwaway temp dir removed after the run. */ export declare function spawnNestedGrandchild(input: NestedGrandchildInput): Promise; /** Injection context for tests / hosts (fake pi SpawnFn, cwd, timeout). */ export interface NestedToolContext { cwd?: string; spawn?: SpawnFn; timeoutMs?: number; } /** * Register the nested `subagent` tool on the child adapter. * * Registered ONLY when PI_SUBAGENTS_NESTED=1 AND PI_SUBAGENTS_DEPTH < * PI_SUBAGENTS_MAX_DEPTH (lock 1). On call the tool enforces the strict * allowlist (lock 2: refused agents are NEVER replaced by a fallback), then * spawns the grandchild through the lanes code with the inherited security * envs (lock 3) and returns the CAPPED result to the child. */ export declare function registerNestedSubagentTool(pi: ExtensionAPI, ctx?: NestedToolContext): void;