pi-mono/apps/p2/plugins/locations/contract.ts
Mario Zechner 2a32a605a9
Some checks are pending
CI / build-check (push) Waiting to run
CI / test (coding-agent) (push) Waiting to run
CI / test (durable) (push) Waiting to run
CI / test (other) (push) Waiting to run
CI / mcp-conformance (push) Waiting to run
feat(p2): Gondolin sandbox environments
A sandbox is a new env type (`type: "gondolin"`): a long-lived Gondolin micro-VM on an SSH environment, with
listed host folders and an HTTP(S) host allowlist. The worker reaches it over SSH to the host, where p2's launcher
(plugins/envs/sandbox: native, or Docker when the host's Node/QEMU are too old) runs pi-env's daemon inside the VM,
so tools, workspace, terminals, uploads and MCP work unchanged. Names outside the allowlist are refused before
the host resolves them.

Conversations working in a sandbox cannot leave it: location tools, subagent spawn/configure, AgentLocations.move,
the terminal tool, and one-shot agents (which now inherit their caller's own location) all keep them there; only the
owner's Relocate moves a session out, after a confirmation. conversationConfinement() exposes it to plugins.

Owner-only management in the Envs sheet: sandboxes listed under their host, host test reports sandbox support,
create/edit/start/stop/restart/test, lock icons in the top bar and sidebar. Tests in test/p2/sandboxes.test.ts
(real VM cases with P2_TEST_GONDOLIN=1) and ui-sandboxes.test.ts; docs in docs/remote-envs.md.
2026-11-05 23:35:09 +00:00

58 lines
2.1 KiB
TypeScript

// The location tools' server side (worker principals only): the environments an agent may move to, their directories,
// and moving the main agent (its session) once its turn has ended.
import { arr, nul, num, obj, opt, type Static, str, union } from "@earendil-works/pi-schema";
import { DirectoryListingSchema, literal, method, remote, serverHost } from "@p2/app/v1";
/** One environment as an agent sees it: never keys or identity files. */
export const EnvInfoSchema = obj({
name: str(),
/** `gondolin`: a sandbox, `host` naming the SSH environment it runs on; conversations working there cannot leave it. */
type: union([literal("ssh"), literal("gondolin")]),
host: str(),
user: opt(str()),
port: opt(num()),
/** The owner's last test of this configuration (Envs), if any. */
tested: opt(
union([obj({ ok: literal(true), platform: str(), arch: str() }), obj({ ok: literal(false), error: str() })]),
),
});
export type EnvInfo = Static<typeof EnvInfoSchema>;
/** The worker attests that the owner started the calling turn; only such turns use the location tools. */
const Owner = literal("owner");
export const AgentLocations = remote("p2.agent-locations", {
version: 1,
params: {},
hostAddress: serverHost,
variants: {
worker: {
methods: {
/** The configured SSH environments and sandboxes (this device is always available besides them). */
envs: method(obj({ origin: Owner }), obj({ envs: arr(EnvInfoSchema) })),
/** Folders of `path` (absolute or `~`-relative) on `env` (null: this device), as the directory picker lists them. */
directories: method(
obj({
origin: Owner,
env: union([str(), nul()]),
path: str({ minLength: 1 }),
}),
DirectoryListingSchema,
),
/**
* Moves the worker's session (`SessionMoves.relocate`); `refused` is why its busy worker could not be fenced
* (nothing moved). A session working in a sandbox only moves within it (only the owner's Relocate leaves it).
*/
move: method(
obj({
origin: Owner,
env: union([str(), nul()]),
cwd: str({ minLength: 1 }),
}),
obj({ refused: union([str(), nul()]) }),
),
},
},
},
});