Files
pi-agent-config/scenarios/curator/workspace/.pi/extensions/curator-tools.ts
T
Kai 86f5763bd6 feat(curator): phase 1 — workspace skills, restricted read, slim prompts
- profile.toml: register 5 workspace skills and review-only restricted read; keep no_skills=true (explicit --skill excludes ~/.agents/skills leak).
- curator-tools.ts: registerRestrictedRead rooted at .pi/skills (.md only, 40k cap); allow read through the guard alongside bridge tools.
- SYSTEM.md/APPEND_SYSTEM.md/SYSTEM.structured.md: slim to a capable-companion identity + safety kernel; describe read outside the generated tool markers; regenerate the 7-tool region.
- skills/{curator-router,books,video,music,sources}/SKILL.md: capable tone, domain workflows, evidence discipline, asymmetric write caution.
2026-08-30 05:02:27 -07:00

77 lines
3.0 KiB
TypeScript

/**
* Curator's tools.
*
* Every tool is a proxy to the loopback bridge in `curator/agent_api.py`. The
* schemas and descriptions are fetched from the backend at `/tools` rather than
* declared here, because `registerTool` accepts a plain JSON Schema object and a
* second copy in TypeScript is a copy that drifts. When contracts.py changes,
* this file needs no edit.
*
* What this file adds on top of the shared bridge helper:
*
* - It refuses to start without a bridge URL and token. A silent start would
* produce an agent with no tools that answers from memory instead -- which
* looks like a working system and is the failure mode hardest to notice.
* - It installs the guard, so no built-in tool can be reached even if a future
* pi version changes which tools are on by default.
*
* Nothing here decides whether a write happens. `propose_write` posts a proposal
* and relays the verdict; the decision is in `service.ACTION_RISK`.
*
* `./_shared/` is vendored by scripts/deploy-scenario.sh from shared/extensions/
* so the deployed tree is self-contained. It is not edited in place -- the
* deploy script overwrites it, and pi-diff.sh reports drift against the repo.
*/
import { resolve } from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import {
fetchBridgeSpecs,
installGuard,
registerBridgeTools,
registerRestrictedRead,
} from "./_shared/pi-guard-base.ts";
export default async function activate(pi: ExtensionAPI): Promise<void> {
const baseUrl = process.env.CURATOR_BRIDGE_URL;
const token = process.env.CURATOR_BRIDGE_TOKEN;
if (!baseUrl || !token) {
// Fail loudly. An agent that silently loses its tools still answers, just
// from the model's memory of what a media library might contain.
throw new Error(
"curator-tools: CURATOR_BRIDGE_URL and CURATOR_BRIDGE_TOKEN are required. " +
"Without them the agent would have no way to see the library and would " +
"answer from memory.",
);
}
const bridge = { baseUrl, token, timeoutMs: 45_000 };
const specs = await fetchBridgeSpecs(bridge);
if (specs.length === 0) {
throw new Error("curator-tools: the bridge served an empty tool list");
}
registerRestrictedRead(pi, {
roots: [resolve(process.cwd(), ".pi/skills")],
extensions: [".md"],
maxChars: 40_000,
description:
"读取 Curator 已部署的媒体技能说明;仅允许 .pi/skills 下的 Markdown 文件,其他路径一律拒绝。",
});
// Deny every built-in tool. --no-builtin-tools is set on the command line too;
// this is the second lock, because the flag is a launch argument while this is
// enforced per call. The allow-list is derived from what the backend actually
// serves, so a tool cannot be advertised and then blocked.
installGuard(pi, {
scenario: "curator",
allowedTools: [...specs.map((spec) => spec.name), "read"],
onBlocked: (name: string) =>
console.error(`curator-tools: blocked built-in tool ${name}`),
});
registerBridgeTools(pi, bridge, specs);
}