- 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.
77 lines
3.0 KiB
TypeScript
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);
|
|
}
|