README leads with the finding that motivated the repository -- pi emits the skills section only when a tool named 'read' is active, so Curator's --no-tools --skill combination made its policy unreachable -- with the measured before/after table and instructions to reproduce it at zero token cost. AGENTS.md sets seven rules for anyone changing this repository. The third is the one that matters most: verify pi's behaviour with a probe rather than inferring it from the docs. Three claims in the first draft of these documents were wrong and were only corrected by running one. The _template scenario carries the isolation defaults and inline warnings at the places where mistakes have already cost time: --no-tools disabling the skills mechanism, cwd anchoring .pi discovery, the read override being mandatory rather than optional, and allowed-tools frontmatter not being enforced in 0.84.3.
5.2 KiB
Authoring a Scenario
Practical checklist. Rationale lives in
isolation-baseline.md,
personality-layering.md and
gateway-patterns.md.
1. Decide whether you need a dedicated agent at all
You do when all of these hold:
- the task is narrow and repeats,
- it needs backend facts that only your service can supply,
- and a wrong answer has a cost (a write, a purchase, a deletion).
You do not when the task is one-off exploration — use interactive pi for that.
2. Copy the template
cp -r scenarios/_template scenarios/<name>
Fill in every <PLACEHOLDER> in profile.toml. It is the single source of truth
for the launch contract; deploy-scenario.sh renders it into
<workspace>/.pi/launch.json, and your gateway should read that file and fail
closed if it is missing. Silently running without --no-extensions widens the
agent's reach.
3. Write the personality
.pi/SYSTEM.md replaces pi's default prompt, and the replacement branch
contributes neither the tool list nor the guidelines. Cover all six
sections: identity and negative identity, tool overview, fact authority, write
discipline, untrusted data, output discipline.
Do not include any path to pi's own documentation. The default prompt does, along with an instruction to read it and follow cross-references — for a non-coding agent that is a ready-made escalation path for injected text.
Durable domain responsibilities go in .pi/APPEND_SYSTEM.md, not AGENTS.md:
context files layer from every parent directory and AGENTS.override.md does not
stop that. -nc does, and system-prompt files are unaffected by it.
4. Write the extension
Build on shared/extensions/pi-guard-base.ts:
import { installGuard, registerRestrictedRead, registerBridgeTools, textResult }
from "<repo>/shared/extensions/pi-guard-base.ts";
const ALLOWED = ["read", "my_query", "my_propose"];
export default function myGuard(pi: ExtensionAPI) {
registerRestrictedRead(pi, {
roots: [`${WS}/.pi/skills`], // MUST include the skill dirs
base: WS,
extensions: [".md"],
});
registerBridgeTools(pi, { baseUrl: process.env.MY_BRIDGE_URL!, token: process.env.MY_BRIDGE_TOKEN }, specs);
installGuard(pi, { scenario: "<name>", allowedTools: ALLOWED });
}
Non-negotiable:
- A restricted
readis mandatory. pi emits the skills section only when a tool namedreadis active, and skill bodies load through it. Without it everySKILL.mdis dead. The built-inreadaccepts absolute paths and can reach your credential files, so override it rather than enabling it. - Every tool needs
promptSnippet, otherwise it is callable but absent from the prose tool list. - Every tool truncates its own output (50 KB / 2000 lines).
- Throw to signal failure. Returning an error-shaped object does not mark the call failed.
- Writes do not belong to the agent. Expose a
propose_*tool that records a plan and echoes the resolved identity; let deterministic code decide.
5. Write the gateway
Use shared/lib/py/pi_rpc.py. It gives you a long-lived RPC process, session
rotation, a per-turn deadline enforced with abort, process-group cleanup, a
minimal env, and receipts harvested from tool_execution_end.
from pi_rpc import PiLaunchConfig, PiRpcClient
client = PiRpcClient(PiLaunchConfig(
pi_bin="/home/claw/.npm-global/bin/pi",
workspace=Path("/home/claw/pi-workspaces/<name>"),
session_dir=Path("/home/claw/.local/share/pi-<name>/sessions"),
provider="zenmux", model="...", thinking="high",
session_id_prefix="<name>-<subject>",
extensions=(ext_path,), skills=(skill_dir,),
receipt_tools=frozenset({"my_write"}),
))
result = await client.prompt(user_text)
reply = result.receipts[0] if result.receipts else result.text
Render any user-visible state change from result.receipts, never from
result.text. Prose guardrails have already failed once in production: with
has_file: false the model still wrote "已成功加入库中".
6. Split stateless work out
Classification, extraction and synthesis need no session, no skills and a lower
thinking level. Run them with --no-session and a terminating tool carrying
constrainedSampling — the tool's parameters is your output schema. Do not
regex JSON out of prose, and do not share a session with the conversational role;
alternating "only output JSON" and "do not output JSON" across turns causes mode
confusion.
7. Verify before deploying
scripts/deploy-scenario.sh <name> # dry run; checks referenced paths
shared/extensions/tests/run-guard-checks.sh # adapt for your tool set
scripts/pi-diff.sh <name>
Assert, with a probe rather than by reading:
- the system prompt does not contain
expert coding assistant <available_skills>lists only your skills- active tools are exactly your allowlist
readreportssource: cli, notbuiltin- reads of an outside path, a
../traversal and an absolute path to your credential file are all denied
8. Record conformance
Add a row to the table in isolation-baseline.md. If a
layer is a known gap, say so and link the follow-up rather than leaving it blank.