pi-mono/apps/p2/plugins/ports/contract.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

85 lines
2.9 KiB
TypeScript

import { arr, bool, int, nul, obj, opt, str, union } from "@earendil-works/pi-schema";
import { defineRegistry, literal, local, method, propertiesOf, remote, serverHost } from "@p2/app/v1";
import type { Forwards } from "./store.ts";
export type { Forwards } from "./store.ts";
/** A stored forward (`forwards.json`). */
export const ForwardBaseSchema = obj({
id: str(),
port: int({ minimum: 1, maximum: 65535 }),
name: opt(str()),
public: opt(bool()),
/** The session whose agent opened it (the `ports` tool); absent when the owner added it. */
session: opt(str()),
});
/** A forward as shown to the owner and the agent: who opened it, and its addresses once known. */
export const ForwardSchema = obj({
...propertiesOf(ForwardBaseSchema),
sessionTitle: opt(str()),
/** Owner only, after signing in to Radius. */
url: opt(str()),
/** Anyone with the link; only for public forwards. */
publicUrl: opt(str()),
});
const List = arr(ForwardSchema);
/** The `ports` tool's requests from a session's agent; the server knows which session sends them. */
export const QuerySchema = union([
obj({ action: literal("list") }),
obj({
action: literal("open"),
port: int({ minimum: 1, maximum: 65535 }),
name: opt(str({ maxLength: 40 })),
public: opt(bool()),
}),
obj({ action: literal("close"), port: int({ minimum: 1, maximum: 65535 }) }),
]);
export const ReplySchema = obj({
forwards: List,
/** False while the brain has never been opened through Radius: forwards exist, their addresses are unknown. */
addressesKnown: bool(),
/** `open`: the forward opened or updated. */
forward: opt(ForwardSchema),
/** `close`: whether the port was forwarded. */
closed: opt(bool()),
});
/** The forwards (server, host-internal): their list and Radius origin; the store itself is the `ForwardsNative`. */
export const ForwardStore = local("p2.forward-store", {
version: 2,
params: {},
members: { list: List, radiusOrigin: union([str(), nul()]) },
});
/** The open `ForwardStore`'s forwards: read `.forwards` at each use. */
export const ForwardsNative = defineRegistry<
{ readonly forwards: Forwards },
{ readonly forwards: Forwards } | undefined
>("p2.forwards", { version: 1 });
/** Owner management and worker tool calls share one registry; worker identity fixes its session. */
export const Ports = remote("p2.ports", {
/** 2: the worker's query has no `origin`: only turns the owner started reach the tool (its `origin: "owner"`). */
version: 2,
params: {},
hostAddress: serverHost,
variants: {
owner: {
state: { forwards: List, radiusOrigin: union([str(), nul()]) },
methods: {
add: method(
obj({
port: int({ minimum: 1, maximum: 65535 }),
name: opt(str({ maxLength: 40 })),
public: opt(bool()),
}),
),
remove: method(obj({ id: str() })),
setPublic: method(obj({ id: str(), public: bool() })),
},
},
worker: {
methods: {
query: method(obj({ conversation: literal(1), query: QuerySchema }), ReplySchema),
},
},
},
});