pi-mono/apps/p2/plugins/dev-tools/tool.ts
Mario Zechner 07c9e5b6fd
Some checks failed
CI / build-check (push) Failing after 14m3s
CI / test (coding-agent) (push) Has been cancelled
CI / test (durable) (push) Has been cancelled
CI / test (other) (push) Has been cancelled
CI / mcp-conformance (push) Has been cancelled
feat(p2): tools declare who may call them and where, turn origin from the run's inputs, agents give their own tools
- P2Tool: origin (any|owner) and scope (any|main|subagent) on the tool; the worker's
  Durable registry checks them before each call (guardTool), no tool checks itself.
  owner: location tools, reload_plugins, conversations, ports, MCP tools.
  main: subagent, rename_session, conversations, ports, reload_plugins. subagent: team.
- originOf reads pi.live.run.inputs through tx.submission: the owner's only if every
  input of the run is. Agent-written inputs record the writing turn's origin: subagent
  tasks and one-shot prompts, team messages, reports (answerOrigin), peer messages.
  No peer origin any more.
- Subagents and one-shot agents get their caller's tools (default all, tools narrows),
  except scope main; with codemode the script-only tools; subagents get team. One-shot
  agents keep codemode and agent. A send takes away what the caller can no longer give.
2026-11-04 23:28:14 +00:00

54 lines
2.4 KiB
TypeScript

import { defineExtension, section } from "@earendil-works/pi-durable";
import { arr, obj, opt, str } from "@earendil-works/pi-schema";
import type { Context, DevResult } from "@p2/app/v1";
import { defineP2Tool, isTopLevel, type P2Tool } from "@p2/app/v1/worker";
export function createReloadExtension(
reload: (plugins: readonly string[] | undefined, context: Context) => Promise<DevResult>,
project: boolean | (() => boolean),
) {
const permitted = () => (typeof project === "function" ? project() : project);
return defineExtension<P2Tool>({
name: "p2-dev-tools",
sections: [
section("p2-development", async (input, context) =>
permitted() && (await isTopLevel(input.read, input.conversationId, context))
? "p2 backend saves only mark dirty. After editing p2 plugins, use reload_plugins to apply dirty plugins (or name plugins). Web/UI edits are plugin edits too: reload_plugins rebuilds the web bundle and open pages reload onto it. Runtime edits need an explicit `npm run reload -- --restart` from apps/p2 with this server's P2_DATA_DIR. Only turns the owner started may use reload_plugins."
: undefined,
),
],
tools: [
defineP2Tool({
name: "reload_plugins",
description:
"Explicitly reload p2's dirty backend plugins on the server and all running workers; optional plugins selects names. Returns the committed selection or refusal reason. Only owner-initiated root turns in a local p2 checkout. Runtime code needs npm run reload -- --restart instead.",
parameters: obj(
{
plugins: opt(arr(str({ minLength: 1, maxLength: 100 }), { maxItems: 100 })),
},
{ open: true },
),
// Do not replay a code upgrade after a crash; the caller can explicitly request another reload.
replay: "unsafe",
origin: "owner",
scope: "main",
execute: async (args, _api, context) => {
const reply = (message: string, ok: boolean) => ({
output: [{ type: "text" as const, text: message }],
details: { ok },
isError: !ok,
});
if (!permitted())
return reply("reload_plugins requires a session working on a local p2 checkout", false);
try {
const result = await reload(args.plugins, context);
return reply(result.message, result.ok);
} catch (error) {
if (context.abortSignal?.aborted) throw error;
return reply(error instanceof Error ? error.message : String(error), false);
}
},
}),
],
});
}