mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
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.
66 lines
2.7 KiB
TypeScript
66 lines
2.7 KiB
TypeScript
import { defineFacet, forbidden, isWorker, nativeOf } from "@p2/app/v1";
|
|
import { envRevision, listLocalDirectory } from "@p2/app/v1/node";
|
|
import { Hosts } from "@p2/app/v1/server";
|
|
import { EnvManagers, EnvService } from "../envs/contract.ts";
|
|
import { isSandbox } from "../envs/schema.ts";
|
|
import { Catalog, SessionMoves } from "../sessions/contract.ts";
|
|
import { AgentLocations, type EnvInfo } from "./contract.ts";
|
|
|
|
export default defineFacet((host) => {
|
|
// Only a current worker generation, for its own session; the worker checked that the owner started the turn.
|
|
host.serve(AgentLocations, (_params, principal, self) => {
|
|
if (!isWorker(principal)) throw forbidden("location tools are for session workers");
|
|
const { session, generation } = principal;
|
|
const hosts = self.get(Hosts, {});
|
|
const envs = self.get(EnvService, {});
|
|
const managers = nativeOf(self, EnvManagers, "The SSH manager");
|
|
const moves = self.get(SessionMoves, {});
|
|
const catalog = self.get(Catalog, {});
|
|
const current = () => {
|
|
const worker = hosts.workers.value[session];
|
|
if (!worker?.running || worker.generation !== generation) throw forbidden("stale worker");
|
|
};
|
|
return {
|
|
variant: "worker",
|
|
members: {
|
|
envs: async () => {
|
|
current();
|
|
const tests = envs.tests.value;
|
|
return {
|
|
envs: Object.entries(envs.configs.value)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([name, config]): EnvInfo => {
|
|
const last = tests[name]?.revision === envRevision(config) ? tests[name]?.test : undefined;
|
|
return {
|
|
name,
|
|
type: config.type,
|
|
host: config.host,
|
|
...(config.type !== "ssh" || config.user === undefined ? {} : { user: config.user }),
|
|
...(config.type !== "ssh" || config.port === undefined ? {} : { port: config.port }),
|
|
...(last === undefined
|
|
? {}
|
|
: {
|
|
tested: last.ok
|
|
? { ok: true, platform: last.platform, arch: last.arch }
|
|
: { ok: false, error: last.error },
|
|
}),
|
|
};
|
|
}),
|
|
};
|
|
},
|
|
directories: async ({ env, path }) => {
|
|
current();
|
|
return env ? managers().manager.listDirectory(env, path) : listLocalDirectory(path);
|
|
},
|
|
move: async ({ env, cwd }, ctx) => {
|
|
current();
|
|
// The location tools refuse this already; the server keeps a sandboxed session in its sandbox too.
|
|
const from = catalog.sessions.value.find((entry) => entry.id === session)?.env;
|
|
if (from && from !== env && isSandbox(envs.configs.value, from))
|
|
throw forbidden(`this session works in sandbox ${from} and cannot leave it`);
|
|
return moves.relocate({ id: session, env, cwd }, ctx);
|
|
},
|
|
},
|
|
};
|
|
});
|
|
});
|