Pi Subagents · Field Guide
Reference
M06

Chapter 6: The Capability Membrane — Resolving Effective Authority

2,077 words · 23 code lines · 11 min

An Agent call is only a request. The child that actually runs is the result of several authorities being resolved in order: an agent definition wins a registry slot; frontmatter locks selected invocation fields; the runner constructs a prompt and resource set; Pi binds the surviving extensions; and tool scope is maintained while the session is live.

Source lock. This chapter describes pi-subagents 0.18.0 at 3f9d35cd078d18a141eb5a6d8f4fc5010d756280 against Pi 0.84.2 at 914cf1472e715297caa30db4b9535d534a9eb718. All source links are commit-pinned.

The practical question is:

If the agent file, the tool call, project settings, and a live extension disagree, which one controls the child?

Configuration passes through several distinct capability boundaries before a tool call runs
Configuration passes through several distinct capability boundaries before a tool call runs

1. Resolve the agent definition before its options

Custom agents are loaded into one map from lowest to highest priority:

global agent directory
  < .agents/agents in the workspace
  < .pi/agents in the project

The key is the frontmatter name, with the filename used only when name is absent or blank. Because later loads overwrite the same map key, .pi/agents wins a name collision. The resulting custom-agent map is then overlaid on the embedded defaults, so a custom Explore can replace the built-in Explore. See loadCustomAgents and buildAgentRegistry.

This is definition precedence, not field merging. If project and global files both declare name: reviewer, the project file replaces the entire global definition. Its missing fields receive loader defaults; they are not filled from the losing file.

Dispatch is resolved separately. Names are case-insensitive only when the match is unambiguous; unknown, disabled, and case-ambiguous types follow fallbackSubagent. A configured none fails closed, while an unset fallback preserves the historical general-purpose fallback. The Agent handler reloads files before every fresh spawn, then resolves the type before starting work (dispatch path).

2. Frontmatter locks invocation fields

Once the type resolves, the governing rule for spawn-time fields is:

effective value = agent frontmatter ?? Agent-call parameter ?? runtime default

This is literal nullish-coalescing in resolveAgentInvocationConfig. It applies independently to model, thinking, max_turns, inherit_context, run_in_background, isolated, and isolation. Therefore an explicit frontmatter false is authoritative; it is not “missing.” Omit the field if callers should be able to choose it.

FieldIf frontmatter and call both omit it
modelparent model
thinkingPi/session resolution
max_turnsproject default, then unlimited
inherit_contextfalse
isolatedfalse
run_in_backgroundtop-level backgroundByDefault; nested spawns use false
isolationordinary checkout; project worktreeIsolation: false also drops a requested worktree

isolation: off deserves special attention. In a call it collapses to no worktree. In frontmatter it first outranks a caller’s worktree, then collapses, making it a real veto. If an allowed worktree is requested but cannot be created, the manager fails loudly instead of falling back to the main checkout (strict creation).

Other fields are definition-only. There are no Agent call parameters that replace tools, disallowed_tools, extensions, exclude_extensions, skills, or prompt_mode. Their loader defaults are also consequential: omitted tools means all Pi built-ins; omitted extensions and skills mean all discovered resources; omitted or unknown prompt_mode means replace (frontmatter parsing).

Microcheck: two independent locks

An agent file sets model: file-model and run_in_background: false. The caller requests model: call-model and run_in_background: true. Which values reach the runner?

Feedback

file-model and false. Resolution is field-by-field, and an explicit false is a value rather than an omission. Removing one frontmatter field would unlock only that field for the caller.


3. Prompt authority and conversation authority are different axes

prompt_mode controls the system prompt:

  • replace: active-agent tag + child/environment header + the file body. The parent system prompt is excluded.
  • append: parent system prompt verbatim + a subagent bridge + active-agent/environment data + the file body under <agent_instructions>.

An append-mode agent with an empty body is called a “pure parent clone” in the prompt tests, but only the parent system prompt is cloned. It does not receive conversation messages. The construction is explicit in buildAgentPrompt.

inherit_context controls the task prompt instead. When true, pi-subagents renders the parent branch as text and prepends it to the new task. It includes user text, assistant text, and compaction summaries; it skips tool-result messages and non-text assistant blocks (buildParentContext). The child is still a new session with its own system prompt and tool registry.

A mention clone is a third mechanism. In agentMentions: model mode, @reviewer ... creates a throwaway in-memory session containing Pi’s actual context messages, the live system prompt, and one callable tool: Agent. That clone spends a model turn deciding how to invoke the real child, then is disposed. It is more faithful than inherit_context, but the real child still follows its own frontmatter (runMentionClone).

Remember the separation:

prompt_mode       -> child system instructions
inherit_context   -> text prefixed to the child's task
mention clone     -> off-screen parent-like session that chooses the Agent call

4. Extensions are discovered, filtered, then bound

The child’s DefaultResourceLoader discovers configuration from configCwd. extensions: true keeps the discovered set; false loads none; an array keeps matching canonical names and adds explicit paths; * keeps all defaults. exclude_extensions is subtracted afterward, so exclusion wins. A tools: ext:foo selector cannot load foo; it can only scope tools inside an extension that extensions allowed (loader setup).

One subtle boundary matters for security: Pi loads modules and executes extension factories before pi-subagents’ extensionsOverride filters the returned extension objects. Pi’s resource loader applies the override after loadFinalExtensionSet (Pi resource loader), while the extension loader imports the module and awaits its factory earlier (Pi extension loader). Filtering prevents the removed extension’s registered handlers and tools from being bound into the child; it does not undo import-time or factory-time side effects.

Surviving extensions are bound with session.bindExtensions, which emits session_start. Extensions such as MCP bridges may register tools there, after resource discovery has finished. Pi’s registerTool writes to the extension’s live tool map and refreshes the registry (Pi registration). That timing explains why a construction-time snapshot is insufficient.

5. Tool scope is live, not a frozen allowlist

The current design has two modes.

When extensions are disabled—either by extensions: false or isolated: true—pi-subagents passes Pi a static tools allowlist derived from the effective built-in set after disallowed_tools, memory-required tool additions, and any admitted nested tools. isolated: true never admits nested tools. Nothing from an extension can register later.

When extensions are enabled, pi-subagents deliberately leaves Pi’s tools allowlist unset. A frozen list would permanently discard names that did not exist at construction. Instead it layers three controls (implementation):

  1. Registry denylist: excludeTools permanently removes unrequested built-ins, disallowed_tools, and pi-subagents’ top-level orchestration tools. Ownership-scoped nested tools explicitly admitted by allowed_subagents are the exception: they are passed as custom tools and re-admitted unless denied. Pi reapplies allow/deny tests on every registry refresh (Pi registry refresh).
  2. Active-set narrowing: after extension binding, inScope() rereads each surviving extension’s live tool map. No ext: selector means tools from all loaded extensions may surface. Any selector flips extension tools to opt-in: ext:foo admits all of foo; ext:foo/search admits only search. disallowed_tools still wins. The same narrowing runs on every turn_end, immediately before Pi prepares the next-turn snapshot.
  3. Call-time veto: turn one has a race-shaped gap. A before_agent_start handler can register and activate an out-of-scope tool after the initial narrowing but before Pi snapshots tools for the provider request. The wrapped agent.beforeToolCall recomputes inScope() and returns { block: true } before execution. Pi’s loop converts that into an error tool result (Pi veto path).

This is defense in depth across time: registry membership, model-visible active tools, and execution-time permission are related but not identical states.

Proof boundary for late registration

The evidence is strong but scoped. Unit tests cover a tool inserted into a live extension map after binding, next-turn_end activation, ext: selection, per-tool narrowing, veto chaining, and persistence across resume (unit tests). A real-Pi template test covers a tool registered during session_start, and a real-Pi reachability test directly invokes the installed veto (veto test).

Unverified end to end: no pinned test has a real model emit an out-of-scope, first-turn call for a tool registered specifically during before_agent_start; the veto is invoked directly. Nor does a real extension timer register selected and unselected tools between later provider turns and then execute them through a model. Source ordering supports both claims, but those full integrations remain proof gaps.

Microcheck: visibility is not permission

During before_agent_start, an allowed extension registers search and write_remote; the agent selects only ext:alpha/search. What can be claimed for the first provider turn and for execution?

Feedback

The initial active-set narrowing may run before this late registration, so source ordering leaves a first-turn model-visibility gap. Execution is narrower: the call-time veto recomputes scope and blocks write_remote even if the provider saw and selected it. A direct veto test supports execution blocking; a real-model reproduction of that exact first-turn race remains unverified.


6. isolated and worktree solve different problems

isolated: true constructs the child without extensions, extension-provided MCP tools, skills, or nested delegation tools. It does not disable an agent definition’s memory:: memory may add built-in tools and inject persistent MEMORY.md content into the system prompt. User-scoped memory can also live outside a worktree. The child normally uses the same checkout.

isolation: worktree means “run in a detached Git worktree created from HEAD.” It does not disable extensions or skills. The copy cannot see staged or uncommitted main-checkout changes. On successful cleanup, dirty files are staged and committed; if the child already committed its work, cleanup need not create another commit. A pi-agent-* branch is created at the resulting worktree HEAD, then the temporary worktree is removed. The error path is only best effort: it attempts removal and reports hasChanges: false, so this mechanism is not a durability guarantee (worktree lifecycle). The two options can be combined.

Neither option is a security sandbox.

MechanismGuarantee at this implementation boundaryDoes not guarantee
Agent-file precedenceA higher-priority definition deterministically replaces a same-name lower oneThat the winning file is trusted or safe
Frontmatter precedenceExplicit locked fields beat conflicting call parametersThat prompts or callers cannot cause harmful actions through remaining capabilities
tools / disallowed_toolsNamed tools are omitted, narrowed, or vetoed through the child session’s registry and execution pathOS-level confinement; a remaining tool such as bash may reproduce broader effects
extensions: false / isolated: trueThe child binds no discovered extension handlers or extension tools; isolated also disables skills and nested toolsA clean prompt or memory boundary; configured persistent memory may still be injected and its path may be reachable through remaining built-in tools. It also does not create a new process, separate user, filesystem jail, network boundary, or credential boundary
Extension include/excludeRemoved extensions do not bind their handlers/tools into the childPrevention of module-import or factory side effects that occurred before filtering
inherit_context: falseParent conversation text is not prefixed to the taskSecrecy of data already present in the task, appended system prompt, files, environment, or services
isolation: worktreeRelative work begins in a committed-HEAD Git copy; successful cleanup preserves the resulting worktree HEAD on a separate branchVisibility of uncommitted main-checkout changes, durability after cleanup failure, or confinement: remaining tools can still modify absolute paths and cause process, network, credential, or other external side effects

Treat these controls as capability shaping inside one Pi runtime. If the threat model requires hostile-code containment, add an actual OS/container/VM sandbox outside pi-subagents.

7. Resolution exercise

Assume all three directories contain an agent named reviewer, but the project file below is in .pi/agents/project.md:

---
name: reviewer
model: acme/file-model
thinking: high
max_turns: 8
inherit_context: false
run_in_background: false
isolated: false
isolation: off
prompt_mode: append
tools: read, grep, ext:alpha/search
disallowed_tools: grep
extensions: alpha, beta
exclude_extensions: beta
---
Review only the authorization boundary.

The caller asks for acme/call-model, minimal thinking, two turns, inherited context, background execution, isolated: true, and isolation: worktree. Both named models are available, both extensions are discoverable, worktrees are enabled, and alpha registers search and write_remote.

Before opening the answer, determine: winning definition; effective invocation fields; system/conversation prompt contents; loaded extensions; callable tools; checkout.

Answer

The .pi/agents definition wins as a whole. Its locked values beat every conflicting call parameter: model acme/file-model, thinking high, eight turns, foreground, no inherited conversation, isolated: false, and no worktree because frontmatter isolation: off vetoes the call.

Append mode includes the parent system prompt, the subagent bridge/environment, and the file body. It does not include the parent conversation because inherit_context resolved to false.

extensions initially admits alpha and beta, then exclude_extensions removes beta. The ext:alpha/search selector admits only alpha’s search; it does not admit write_remote. Built-ins parse as read and grep, then disallowed_tools removes grep. Effective callable tools are therefore read and search. The child runs in the current checkout.


Next: Chapter 7 turns effective authority into a graph: which child may delegate again, and which immediate parent may observe or control each descendant.

Source index

What to retain

  • Spawn-time fields resolve independently as frontmatter ?? Agent-call parameter ?? runtime default; tool and resource fields are definition-only.
  • Extension-enabled tool scope stays live through active-set narrowing and a call-time veto.
  • Tool policy, extension filtering, context inheritance, isolated mode, and worktrees are distinct controls; none is an OS sandbox.