pi-mono/apps/p2/test/p2/mcp-environments.test.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

55 lines
2.6 KiB
TypeScript

// The supervisor's pi-env connections (plugins/mcp/environments.ts): one per environment and configuration revision,
// shared by its users, closed with the last one; an edited environment is another connection. Connections here never
// start (a command that does not exist): a closed one refuses at once with "Connection closed", an open one tries to
// start and fails to spawn.
import assert from "node:assert/strict";
import { test } from "node:test";
import { Connection } from "@earendil-works/pi-env";
import { type EnvConfig, envRevision } from "../../plugins/envs/config.ts";
import { EnvConnections } from "../../plugins/mcp/environments.ts";
const GPU: EnvConfig = { type: "ssh", host: "gpu.example" };
const EDITED: EnvConfig = { type: "ssh", host: "gpu.example", port: 2222 };
/** Whether `connection` was closed (it refuses at once) rather than open (it tries to start the daemon). */
const isClosed = async (connection: Connection): Promise<boolean> =>
connection.info().then(
() => false,
(error: Error) => error.message === "Connection closed",
);
test("users of one environment share its connection; the last release closes it", async () => {
const opened: string[] = [];
const envs = new EnvConnections((name) => {
opened.push(name);
return new Connection({ command: ["/nonexistent/pi-env"] });
});
const first = envs.acquire("gpu", GPU, envRevision(GPU));
const second = envs.acquire("gpu", GPU, envRevision(GPU));
assert.equal(second.connection, first.connection);
const other = envs.acquire("box", GPU, envRevision(GPU));
assert.notEqual(other.connection, first.connection);
assert.deepEqual(opened, ["gpu", "box"]);
first.release();
assert.equal(await isClosed(second.connection), false);
second.release();
assert.equal(await isClosed(second.connection), true);
assert.equal(await isClosed(other.connection), false);
other.release();
// A new user after the last one opens a new connection.
const again = envs.acquire("gpu", GPU, envRevision(GPU));
assert.notEqual(again.connection, first.connection);
assert.deepEqual(opened, ["gpu", "box", "gpu"]);
again.release();
});
test("an edited environment is another connection; the old one stays for its users until they release it", async () => {
const envs = new EnvConnections(() => new Connection({ command: ["/nonexistent/pi-env"] }));
const before = envs.acquire("gpu", GPU, envRevision(GPU));
const after = envs.acquire("gpu", EDITED, envRevision(EDITED));
assert.notEqual(after.connection, before.connection);
before.release();
assert.equal(await isClosed(before.connection), true);
assert.equal(await isClosed(after.connection), false);
after.release();
});