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.
55 lines
2.6 KiB
TypeScript
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();
|
|
});
|