pi-daemon1.3

Log 05 · Profiles

The operator decides what a session gets.

A profile is a reviewed recipe for sessions: where they may work, which tools exist, which models, MCP servers and extension modules they get, what their tools’ environment holds and what must wait for a person. Clients only pick a profile by name and fill in nonsecret values.

01Anatomy

Every field has one effect

What each profile field becomes Left, a profile's fields in four groups; right, where each takes effect. cwdRoots confine every session's working directory and file resources. agentDir supplies auth.json, models.json and settings.json to the worker's model runtime and settings. model and thinking are each new session's defaults. tools fills the worker's registry with exactly those built-ins; extensions load trusted Durable modules into it; mcpServers are started per open session. envFiles, sessionEnvFiles and sessionEnv build each tool's environment. concurrency is the profile's turn gate; requireApproval and requireResultApproval install approval hooks for the listed tools. profiles.personalWHEREcwdRootsthe only places a session may workagentDirauth · models · settings, as TUI PiWHAT IT RUNSmodel · thinkingdefaults for new sessionstoolsbuilt-ins that exist at allextensionstrusted Durable modulesmcpServersper open sessionENVIRONMENTenvFilesthe profile’s variablessessionEnvFilesper session, found from its cwdsessionEnvnonsecret, set by a clientGATESconcurrencyturns at once, default 16requireApprovalcalls wait for a clientrequireResultApprovalresults wait toosession cwd · resourcesmodel runtime · settingsroot conversation configthe worker’s registryMCP clients · tool_searchexecution environmentturn gateapproval hooks
Fig. 5.1Every profile runs in its own worker, so each field acts on that process alone.
profiles:
  personal:
    cwdRoots: [~/src, ~/notes]          # required; symlinks resolved
    model: anthropic/claude-sonnet-4-5
    thinking: medium                    # off · minimal · low · medium · high · xhigh · max
    tools: [read, write, edit, bash]    # the default: all four
    extensions: [./extensions/personal.mjs]
    mcpServers:
      files: { command: mcp-files, args: [--stdio] }
      remote: { url: https://mcp.example/mcp }
    envFiles: [./personal.env]
    requireApproval: [bash]
  reader:
    cwdRoots: [~/src]
    tools: [read]                       # write, edit and bash do not exist here

Each profile appears to AHP clients as an agent named after it. Creating a session asks for a cwd under its roots, a model and a thinking level, with the profile’s defaults filled in. In Nix, the same fields sit under services.pi-daemon.profiles.

02Environment

Credentials reach tools, never clients

How a tool's environment is built Four layers, each overriding the one above for the variables it sets: the daemon's own environment; the profile's envFiles; the session's env files, found from its cwd by the profile's sessionEnvFiles patterns such as {cwd}/../env.sh; and the session's own values, which a client may set only for the variable names the profile lists in sessionEnv. The result is built in the profile's worker each time a bash call, an ! command or a terminal starts, and when a session's MCP server starts. The daemon never changes its own process environment, so concurrent sessions never see each other's values, and none of it reaches the server process, the logs or any client. LATER LAYERS WINthe daemon’s environmentinherited by the serviceprofile envFilesKEY=value files: the profile’s credentialssession env filessessionEnvFiles from the session’s cwd, e.g. {cwd}/../env.shsession valuessessionEnv names only; nonsecret; set by a clientmainbash! commandsterminalsMCP serversread in the workereach time a tool, command or terminal startsa rewritten fileapplies from its next usenever reachesthe server process, logs or clientsnever set on the daemonso sessions cannot see each other
Fig. 5.2Clients never supply secrets. Per-session credentials live in files beside each session’s directory, read only in the worker.

sessionEnvFiles is how an application gives each of its agents its own identity and token without sending them over a protocol: with {cwd}/../env.sh, an agent working in ~/work/agents/a7/repo gets the variables in ~/work/agents/a7/env.sh in its bash, ! commands, terminals and MCP servers. A missing file is skipped; a rewritten one applies from its next use.

03Tools

A tool a profile does not list does not exist

ToolWhat it does
readreads files in the session’s environment; images (jpg, png, gif, webp, bmp) are resized to provider limits and attached for the model, as in TUI Pi
write, editchange files; changes to one file are serialised; a chat’s changes form its changeset
bashruns in the session’s cwd with its layered environment, output streamed and committed as it runs
ask_userpi-daemon’s own extension: asks the person a question, free text or a choice, and waits for any client to answer. serve does not register it by itself; a profile adds it with a module that re-exports InputExtension
subagentpi-daemon’s own extension: delegates a self-contained task to a child conversation owned by the call; added the same way, from SubagentExtension

The first four are Durable’s coding tools, registered per worker from the profile’s tools list. A profile with tools: [read] is a read-only profile: there is no bash to misuse, because none was ever registered.

04Modules

Behaviour as trusted Durable extensions

Where a profile's module can act A run, left to right: a generation task prepares a model request, gets the response, and its tool calls become tool tasks; when the round's tools are done the next generation runs, until the model yields an answer. Hook points, as diamonds on the line they act on: beforeRequest can replace a request's messages; afterResponse sees each response; beforeTool can block a call or rewrite its arguments; afterTool can replace its result; afterTools runs once a round is done; onYield can continue the run with another user message; and on the compaction task, beforeCompact can decline a compaction or supply the summary. Below, observe() taps the same run as read-only, coding-agent-style events and cannot steer it. pi-daemon's own approvals are beforeTool and afterTool hooks. GENERATION TASKCOMPACTIONTOOL TASKSOBSERVE()requestresponsenext requestanswerbeforeRequestreplace messagesafterResponsesee itafterToolsround doneonYieldcontinue the runcompactionbeforeCompactdecline or summarisebashreadbeforeToolafterToolblock or rewrite the call · replace the resultagent_startmessage_endtool_execution_endturn_startagent_endfrozen copies; awaited per event; a slow observer sees Durable’s coalesced events
Fig. 5.3Hooks can steer; an observer only watches. Anything that changes what the agent does must be a hook.
// ~/.config/pi/daemon/extensions/gate.mjs
export const extension = {
  name: "gate",
  hooks: [{ task: "pi.tool", handlers: { beforeTool: (call) =>
    call.name === "bash" ? { block: "bash is paused" } : undefined } }],
  sections: [{ key: "house-rules", render: () => "Keep answers short." }],
};
export function observe(event) {
  if (event.type === "agent_end") process.stderr.write("turn finished\n");
}
  • What a module exportsextension: hooks, tools, prompt sections, wrappers and tasks, or a function returning them. observe: a read-only observer of agent events. Its name may not clash with another module, builtin-tools or pi-prompt.
  • Hot reloadEditing the file reloads it: the next model request uses the new version. Work already running keeps the code it started with.
  • FailureA module that fails to load stops its profile’s worker from starting. A reload that fails keeps the last good version and reports invalid_extension.
  • CommandsA module can export commands: /name runs in the session’s worker instead of a model turn, from any client, even while the chat is busy, at most once per request id. It can notify the session’s clients and keep up to 64 KiB of per-session state that survives restarts.
  • What reload does not doOnly the edited file is re-imported, not modules it imports, and each reload keeps the old version in memory until the worker restarts.

05MCP

The main way capability arrives

  • Per sessionEach open session connects its profile’s servers itself: stdio servers run in its cwd with its layered environment plus the server’s own env; url servers are reached over streamable HTTP.
  • Named and foundTools are mcp__<server>__<tool>. Up to 32 MCP tools are all offered to the model; beyond that it gets tool_search, which loads the best matches from its next call on.
  • RestartedA server that crashes or fails to start is retried with backoff, and at once when one of its tools is called. A call it dies during is an error result. Opening a session waits up to 10 seconds; a slower server adds its tools when it is up.
  • Visible and controllableAHP clients see each server and its state, the session’s tools, skills, prompt templates and extensions, and can stop and start a server. A stopped server stays stopped until a client starts it, or its worker restarts.

Clients cannot add MCP servers; they come only from the profile.

06TUI Pi parity

The same agent you run in a terminal

Like TUI PiHow
system promptPi’s preamble (or SYSTEM.md), tool list and rules, APPEND_SYSTEM.md, AGENTS.md and CLAUDE.md, skills and the cwd, read from the profile’s agentDir and the session’s directory as TUI Pi reads them with extensions off and the project trusted
models and loginsthe same auth.json and models.json through Pi’s model runtime; a /login in TUI Pi reaches running sessions, and a refreshed OAuth token is written back under Pi’s file lock
settingssettings.json compaction, retry, steeringMode and followUpMode, live: edits apply to the next turn
commands/compact, /model, /thinking, /fork, /new, prompt templates, /skill: commands and ! shell commands over AHP
Unlike TUI PiWhy
coding-agent extensionsDurable cannot load them. Their behaviour comes back as Durable modules, MCP servers or, for UI, client-side extensions.
project .pi/settings.jsonnot read; the profile’s agentDir decides
prompt files edited mid-runread once per worker per directory, so prompt caching works; restart the worker to pick up edits
Pi packagesskills from configured packages are used only if already installed; the daemon never installs anything

07Approvals

Calls that wait for a person, across restarts

A tool call that waits for a person The profile lists bash in requireApproval. The model calls bash; the approval extension's beforeTool hook holds the call, and AHP clients see it pending confirmation. The worker restarts while it waits; the call is still waiting afterwards, because the wait and any decision are Durable state of the call's tool task. A client dispatches chat/toolCallConfirmed approving it; the first decision wins and is echoed; the call runs. A denied call would be cancelled and the model told it was blocked, with the client's reason. call bash1 held: pending-confirmation2 chat/toolCallReady, not confirmed3 the worker restarts; the call is still waiting chat/toolCallConfirmed · approve4 decision, stored with the task5 bash runs6 echo · then output and result7 modelthe agentmodelthe agent beforeToolapproval hookbeforeToolapproval hook /ahpprojection/ahpprojection Culturea personCulturea person
Fig. 5.4The wait and the decision are state of the call’s Durable tool task: a restart neither loses an approval nor runs an unapproved call.

requireResultApproval works the same way after the call: an afterTool hook holds the result until a client confirms it, and a denied result reaches the model as an error. The first decision wins; others are rejected to their sender. Edited tool input is not supported.

08Applications

An application is a profile plus a client

A coordinator such as Cacophony brings its agent behaviour as a profile — its own Durable module for pause gates and lifecycle records, its MCP servers for commands, sessionEnvFiles for each agent’s identity — and drives its agents over AHP with turns, cancels and session lifecycle. Its worker is its own process with its own turn gate, so its fleet can neither crash nor starve your personal sessions.

Driving sessions from an application