fix(p2): guest turns may use the terminal tool again, as decided (reverts 2b78884e3)

This commit is contained in:
Mario Zechner 2026-11-05 00:05:34 +00:00
parent 2b78884e3a
commit d57c2d2191
3 changed files with 9 additions and 16 deletions

View file

@ -888,9 +888,8 @@ translated phone drawer and intercept chat taps.
viewer or the agent); a panel showing a terminal keeps it, one showing none attaches the newest new one. Archiving a viewer or the agent); a panel showing a terminal keeps it, one showing none attaches the newest new one. Archiving a
session closes its terminals (`SessionArchived`, a host-lifetime contribution; no host is started for it). session closes its terminals (`SessionArchived`, a host-lifetime contribution; no host is started for it).
**Agent terminal tool** (owner decisions): the agent drives the same terminals the owner sees (shared: the owner **Agent terminal tool** (owner decisions): the agent drives the same terminals the owner sees (shared: the owner
watches and types; the agent may read, type into and close any terminal of its session, the owner's too). Only watches and types; the agent may read, type into and close any terminal of its session, the owner's too). Guest
turns the owner started may use it (`origin: "owner"`, owner decision): a terminal may be connected beyond the turns may use it, like bash. Code mode only, like MCP tools (`deferred`, `callers: ["tools"]`; scripts find it with
session's environment (ssh, a waiting sudo prompt), where a guest's bash would not reach. Code mode only, like MCP tools (`deferred`, `callers: ["tools"]`; scripts find it with
`searchTools()`); while code mode is off `tool_search` loads it (installed once by `durableRegistry`, not by each `searchTools()`); while code mode is off `tool_search` loads it (installed once by `durableRegistry`, not by each
plugin: two plugins contributing `tool-search` would be a registry conflict). The description says to use it only plugin: two plugins contributing `tool-search` would be a registry conflict). The description says to use it only
when the user asks for a terminal or for driving an interactive program in one, never instead of bash; no prompt when the user asks for a terminal or for driving an interactive program in one, never instead of bash; no prompt
@ -1066,8 +1065,7 @@ translated phone drawer and intercept chat taps.
`origin` (`"any"` default, or `"owner"`: only a turn the owner started) and `scope` (`"any"` default, `"main"`: only `origin` (`"any"` default, or `"owner"`: only a turn the owner started) and `scope` (`"any"` default, `"main"`: only
top-level conversations, or `"subagent"`: given to subagents only). The worker's Durable registry wraps every tool top-level conversations, or `"subagent"`: given to subagents only). The worker's Durable registry wraps every tool
with requirements (`guardTool`, in `durableRegistry`), so no tool checks them itself. `origin: "owner"`: the location with requirements (`guardTool`, in `durableRegistry`), so no tool checks them itself. `origin: "owner"`: the location
tools, `reload_plugins`, `conversations`, `ports`, MCP tools (the owner's accounts are behind them), `terminal` tools, `reload_plugins`, `conversations`, `ports`, MCP tools (the owner's accounts are behind them). A turn's origin
(the owner's terminals, possibly connected beyond the session's environment). A turn's origin
(`originOf`, `src/runtime/origin.ts`) is the conversation's current run (`pi.live.run.inputs`, each submission's (`originOf`, `src/runtime/origin.ts`) is the conversation's current run (`pi.live.run.inputs`, each submission's
entry through `tx.submission`): the owner's only if every input the run placed is (`entryOrigin`): a person's input entry through `tx.submission`): the owner's only if every input the run placed is (`entryOrigin`): a person's input
by its author in the entry data, an agent-written one by its `origin`. Agent-written inputs record the writing by its author in the entry data, an agent-written one by its `origin`. Agent-written inputs record the writing

View file

@ -1,19 +1,17 @@
// The agent's `terminal` tool (worker): tmux commands as argv on the session's terminals, the ones the owner sees in // The agent's `terminal` tool (worker): tmux commands as argv on the session's terminals, the ones the owner sees in
// the terminal panel. Code mode scripts call it (`tools.terminal({ command, args })`); without code mode `tool_search` // the terminal panel. Code mode scripts call it (`tools.terminal({ command, args })`); without code mode `tool_search`
// loads it, as MCP tools. Only turns the owner started may call it (`origin: "owner"`): the terminals are the owner's // loads it, as MCP tools. Replay-safe: the host does a call with the tool call's key once, and reads start where the
// too, possibly connected where the session's `bash` would not reach (an ssh session, a waiting sudo prompt). // first attempt's did (memoized), so a call a restart interrupted runs again without typing or opening twice.
// Replay-safe: the host does a call with the tool call's key once, and reads start where the first attempt's did
// (memoized), so a call a restart interrupted runs again without typing or opening twice.
import { import {
defineExtension, defineExtension,
defineTool,
type Extension, type Extension,
type ToolExecutionApi, type ToolExecutionApi,
type ToolExecutionResult, type ToolExecutionResult,
} from "@earendil-works/pi-durable"; } from "@earendil-works/pi-durable";
import { arr, bool, int, obj, opt, type Static, str, union } from "@earendil-works/pi-schema"; import { arr, bool, int, obj, opt, type Static, str, union } from "@earendil-works/pi-schema";
import { type Context, isRooted, joinPath, type Location, literal } from "@p2/app/v1"; import { type Context, isRooted, joinPath, type Location, literal } from "@p2/app/v1";
import { defineP2Tool, type P2Tool } from "@p2/app/v1/worker";
import { import {
COMMANDS, COMMANDS,
CommandError, CommandError,
@ -101,11 +99,11 @@ export function createTerminalExtension(
reach: TerminalReach, reach: TerminalReach,
backend: TerminalBackend, backend: TerminalBackend,
locate: (api: ToolExecutionApi, context: Context) => Promise<Location>, locate: (api: ToolExecutionApi, context: Context) => Promise<Location>,
): Extension<P2Tool> { ): Extension {
return defineExtension<P2Tool>({ return defineExtension({
name: TERMINAL_EXTENSION, name: TERMINAL_EXTENSION,
tools: [ tools: [
defineP2Tool({ defineTool({
name: "terminal", name: "terminal",
description: DESCRIPTION, description: DESCRIPTION,
parameters: obj({ parameters: obj({
@ -118,7 +116,6 @@ export function createTerminalExtension(
...(reach === "codemode" ? { callers: ["tools" as const] } : {}), ...(reach === "codemode" ? { callers: ["tools" as const] } : {}),
outputLimits: { maxBytes: OUTPUT_CHARS + 1024, retain: "tail" }, outputLimits: { maxBytes: OUTPUT_CHARS + 1024, retain: "tail" },
replay: "safe", replay: "safe",
origin: "owner",
execute: async (args, api, context) => { execute: async (args, api, context) => {
let command: TerminalCommand; let command: TerminalCommand;
try { try {

View file

@ -108,8 +108,6 @@ test("code mode only while code mode is on; tool_search reaches it otherwise", (
assert.deepEqual(terminal(backend).callers, ["tools"]); assert.deepEqual(terminal(backend).callers, ["tools"]);
assert.equal(terminal(backend).deferred, true); assert.equal(terminal(backend).deferred, true);
assert.equal(terminal(backend).replay, "safe"); assert.equal(terminal(backend).replay, "safe");
// The owner's terminals: only turns the owner started (checked by the worker's registry).
assert.equal(terminal(backend).origin, "owner");
assert.equal(terminal(backend, "tool_search").callers, undefined); assert.equal(terminal(backend, "tool_search").callers, undefined);
assert.equal(terminal(backend, "tool_search").deferred, true); assert.equal(terminal(backend, "tool_search").deferred, true);
assert.match(terminal(backend).description, /Only when the user asks for a terminal/); assert.match(terminal(backend).description, /Only when the user asks for a terminal/);