mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
- 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.
117 lines
5.6 KiB
TypeScript
117 lines
5.6 KiB
TypeScript
import { defineExtension, section } from "@earendil-works/pi-durable";
|
|
import { bool, int, obj, opt, str, union } from "@earendil-works/pi-schema";
|
|
import type { Context } from "@p2/app/v1";
|
|
import { literal } from "@p2/app/v1";
|
|
import { defineP2Tool, isTopLevel, type P2Tool } from "@p2/app/v1/worker";
|
|
import { type ForwardView, MAX_FORWARD_NAME, type PortsReply, type PortsRequest } from "./protocol.ts";
|
|
|
|
/** What the agent learns: every forward, its addresses, who opened it, and the public endpoints spelled out. */
|
|
function describe(reply: PortsReply, self: string): string {
|
|
const label = (forward: ForwardView) =>
|
|
`port ${forward.port}${forward.name ? ` (${JSON.stringify(forward.name)})` : ""}`;
|
|
const opener = (forward: ForwardView) =>
|
|
forward.session === self
|
|
? "opened by this session"
|
|
: forward.session
|
|
? `opened by session ${JSON.stringify(forward.sessionTitle ?? forward.session)}`
|
|
: "added by the owner";
|
|
const lines = reply.forwards.map(
|
|
(forward) =>
|
|
`- ${label(forward)}, ${forward.public ? "PUBLIC" : "private"}, ${opener(forward)}: ${
|
|
forward.url ?? "address not known yet"
|
|
}${forward.publicUrl ? `, public ${forward.publicUrl}` : ""}`,
|
|
);
|
|
const publicOnes = reply.forwards.filter((forward) => forward.public);
|
|
return [
|
|
reply.forwards.length ? "Forwarded ports:" : "No ports are forwarded.",
|
|
...lines,
|
|
publicOnes.length
|
|
? `Public endpoints, open to anyone with the link without signing in: ${publicOnes
|
|
.map((forward) => `${label(forward)} ${forward.publicUrl ?? "(address not known yet)"}`)
|
|
.join("; ")}.`
|
|
: "No forward is public.",
|
|
...(reply.addressesKnown
|
|
? []
|
|
: [
|
|
"Addresses are not known yet: Pi has not been opened through Radius by its owner. They appear once it has; the owner also finds them under Envs → Ports.",
|
|
]),
|
|
].join("\n");
|
|
}
|
|
|
|
export function createPortsExtension(
|
|
queryServer: (query: PortsRequest, context: Context) => Promise<PortsReply>,
|
|
self: () => string,
|
|
) {
|
|
return defineExtension<P2Tool>({
|
|
name: "p2-ports",
|
|
sections: [
|
|
section("ports", async (input, context) =>
|
|
(await isTopLevel(input.read, input.conversationId, context))
|
|
? "The ports tool forwards a local web server (a dev server, say) through Radius so the owner can open it from other devices, and lists all forwards and public endpoints. Forwards are private by default: only the owner can open them, after signing in. Never make a port public, by opening it with public: true or switching an existing one, without asking the user first and getting a yes: a public port is reachable by anyone with the link, without signing in."
|
|
: undefined,
|
|
),
|
|
],
|
|
tools: [
|
|
defineP2Tool({
|
|
name: "ports",
|
|
description: [
|
|
"Forward web servers on this device through Radius, so the owner can open them from anywhere.",
|
|
"list: every forward with its addresses and who opened it, and all public endpoints.",
|
|
"open (port; optional name, public): forward a port and return its address. Private by default (owner only, after signing in). public: true also opens it to anyone with the link, without signing in; ask the user before doing that. Opening a port that is already forwarded, by anyone, keeps its address and updates its name and visibility.",
|
|
"close (port): stop forwarding a port, whoever opened it.",
|
|
"Only ports of this device: a session on a remote environment cannot forward its host's ports yet. A session's forwards close when it is archived.",
|
|
].join("\n"),
|
|
parameters: obj(
|
|
{
|
|
action: union([literal("list"), literal("open"), literal("close")]),
|
|
port: opt(int({ minimum: 1, maximum: 65535 })),
|
|
name: opt(str({ minLength: 1, maxLength: MAX_FORWARD_NAME })),
|
|
public: opt(bool()),
|
|
},
|
|
{ open: true },
|
|
),
|
|
// Every action is idempotent: opening twice returns the same forward, closing twice reports it closed.
|
|
replay: "safe",
|
|
origin: "owner",
|
|
scope: "main",
|
|
execute: async (args, _api, context) => {
|
|
const reply = (text: string, details: Record<string, unknown> = {}, isError = false) => ({
|
|
output: [{ type: "text" as const, text }],
|
|
details: { action: args.action, ...(args.port === undefined ? {} : { port: args.port }), ...details },
|
|
isError,
|
|
});
|
|
let request: PortsRequest;
|
|
if (args.action === "list") request = { action: "list" };
|
|
else if (args.port === undefined) return reply(`${args.action} needs a port.`, {}, true);
|
|
else if (args.action === "open")
|
|
request = {
|
|
action: "open",
|
|
port: args.port,
|
|
...(args.name === undefined ? {} : { name: args.name }),
|
|
...(args.public === undefined ? {} : { public: args.public }),
|
|
};
|
|
else request = { action: "close", port: args.port };
|
|
try {
|
|
const result = await queryServer(request, context);
|
|
const summary =
|
|
request.action === "open" && result.forward
|
|
? `Forwarded port ${result.forward.port}${result.forward.public ? ", public" : ", private"}: ${
|
|
result.forward.url ?? "address not known yet"
|
|
}${result.forward.publicUrl ? ` (public: ${result.forward.publicUrl})` : ""}.`
|
|
: request.action === "close"
|
|
? result.closed
|
|
? `Stopped forwarding port ${request.port}.`
|
|
: `Port ${request.port} was not forwarded.`
|
|
: undefined;
|
|
return reply([summary, describe(result, self())].filter(Boolean).join("\n\n"), {
|
|
...(result.forward ? { forward: result.forward } : {}),
|
|
});
|
|
} catch (error) {
|
|
if (context.abortSignal?.aborted) throw error;
|
|
return reply(error instanceof Error ? error.message : String(error), {}, true);
|
|
}
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
}
|