mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
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.
This commit is contained in:
parent
6d3e342b25
commit
2a32a605a9
51 changed files with 3240 additions and 322 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Remote environments
|
||||
|
||||
`plugins/envs` manages SSH environments. Tools and workspace files live on the remote host; the worker,
|
||||
`plugins/envs` manages SSH environments and sandboxes (Gondolin VMs on an SSH environment, see Sandboxes). Tools and workspace files live on the remote host; the worker,
|
||||
Durable history, provider credentials and model calls stay on this device. Remote sessions never fall back
|
||||
to local execution when configuration, trust or a connection is unavailable.
|
||||
|
||||
|
|
@ -34,6 +34,63 @@ own location still works on that machine. Open chats stay connected. Subagents s
|
|||
where they are, on their machine, and keep running their tools there. Terminals already running stay on the machine
|
||||
they were started on; new ones open where the viewed conversation works.
|
||||
|
||||
## Sandboxes
|
||||
|
||||
A sandbox is a Gondolin VM (earendil-works/gondolin: QEMU with KVM, host-side network policy) on an SSH environment,
|
||||
its host. In the Envs sheet it is listed under its host, and the host's **Save and test** reports whether it can run
|
||||
sandboxes: natively (Linux, `/dev/kvm` usable, Node >= 22, QEMU >= 7.2), through Docker (Linux, `/dev/kvm`, a usable
|
||||
`docker`; e.g. Ubuntu 22.04, whose QEMU 6.2 is too old), or not, with the reason. Only the owner creates, edits,
|
||||
starts, stops and deletes sandboxes, in the Envs sheet; plugins pick an existing one (env listings carry `type`).
|
||||
|
||||
```json
|
||||
"box1": {
|
||||
"type": "gondolin",
|
||||
"host": "slayer",
|
||||
"image": "alpine-base:latest",
|
||||
"mounts": [{ "host": "~/sbx/box1", "guest": "/workspace" }, { "host": "~/datasets", "guest": "/data", "readonly": true }],
|
||||
"allowHosts": ["registry.npmjs.org", "*.github.com"],
|
||||
"cpus": 2,
|
||||
"memoryMb": 2048
|
||||
}
|
||||
```
|
||||
|
||||
- **What the VM sees:** its own root file system (lost when the VM restarts) and the listed host folders; nothing else
|
||||
of the host. In Docker mode folders must be inside the host user's home (only the home is shared with the container).
|
||||
A writable folder must not contain or be inside `~/.pi` (p2's launcher and daemons run from there on the host).
|
||||
`/opt/pi-env` (the daemon) and `/.p2-vfs` (Gondolin's own mount point) are reserved in the VM.
|
||||
- **Network:** HTTP(S) to `allowHosts` only (Gondolin's patterns: `*` alone is any host, `*` inside a pattern any
|
||||
substring); everything else is refused with 403 before the host resolves the name, so names do not leak to DNS. An
|
||||
empty or missing list allows no host. Allowed names that resolve to private addresses are refused too. Other
|
||||
protocols are not routed.
|
||||
- **Lifecycle:** one long-lived VM per sandbox, shared by every session working in it. It starts at the first
|
||||
connection (or **Start**, **Save and start**, **Test**) and runs until **Stop**, deleting the sandbox or moving it to
|
||||
another host (both stop it first), the VM dying (the supervisor notices within seconds; the next connection starts
|
||||
a new one) or a host reboot. Other configuration changes apply at its next start: the sheet says so while the
|
||||
running VM is older, and offers **Save and restart**.
|
||||
- **Connection:** the worker connects to the host over SSH as for the host itself, deploys pi-env's daemon there
|
||||
(the same Linux binary runs in the VM) and p2's launcher (`plugins/envs/sandbox/`, under the host's
|
||||
`~/.pi/p2/sandbox/<content hash>/`, checked by hash at every deployment; versions of other p2 installations stay
|
||||
until unused for 30 days). It runs `p2-sandbox.sh start` (which may take minutes the first time: Docker image,
|
||||
`npm ci` from the shipped lockfile, guest image download, boot) and then `p2-sandbox.sh daemon <key> <config>
|
||||
<daemon> -- serve --token …`. The launcher picks native or Docker (image `p2-sandbox:<Dockerfile hash>` with Node and
|
||||
QEMU only, container `p2-sandbox-<uid>-<Dockerfile hash>` with the home mounted at the same path and `/dev/kvm`
|
||||
passed in), starts the sandbox's supervisor if needed (`p2-sandbox.mjs serve`, one process holding the VM; socket
|
||||
and log under `~/.pi/p2/sandbox/run/<key>/`, `<key>` being the sandbox's name prefixed by a hash of this p2
|
||||
installation's machine and data directory), copies the daemon into the VM and passes its stdin and stdout through.
|
||||
Tools, the workspace, terminals, uploads and `send_file` then work as on any remote environment. Hosts must be
|
||||
Linux. Connections to a sandbox are keyed by its configuration and its host's: editing either opens new ones.
|
||||
- **Confinement:** a conversation whose environment is a sandbox cannot leave it. `location_envs` shows only the
|
||||
sandbox; `location_dirs` and `location_move` take paths inside it; `subagent` spawn/configure place subagents only
|
||||
in it; one-shot agents (`tools.agent`) inherit their caller's location (the `p2.inherited-location` document), so a
|
||||
subagent's sandbox holds for the agents its scripts start. `AgentLocations.move` refuses to move a sandboxed session
|
||||
out too, and `subagent` configure keeps a subagent working in a sandbox there whoever configures it. Only the
|
||||
owner's Relocate moves a session out (the web UI asks first, also for a move to another sandbox). Confinement is decided per
|
||||
conversation from its current location and the environment's configured `type`, never its name:
|
||||
`conversationConfinement(harness.environments, id, ctx)` (`@p2/app/v1/worker`), backed by
|
||||
`HarnessEnvironmentEntry.sandbox`. The `terminal` tool lists and drives only the session's terminals in the
|
||||
caller's sandbox. Tools that run on the p2 machine or reach unconfined places (MCP, ports, conversations, …) are a
|
||||
session tool policy's job, not the location's.
|
||||
|
||||
## Data and execution
|
||||
|
||||
`<P2_DATA_DIR>/envs.json` is independently observed by the server and workers. Invalid edits keep the last valid
|
||||
|
|
@ -83,7 +140,9 @@ run commands on those hosts. Sharing is not filesystem isolation.
|
|||
## Tests
|
||||
|
||||
From `apps/p2`: `npm run check`; `npm test`; focused:
|
||||
`node --import ./src/source.ts --test test/p2/envs.test.ts`.
|
||||
`node --import ./src/source.ts --test test/p2/envs.test.ts` and `test/p2/sandboxes.test.ts` (configuration,
|
||||
launcher arguments, confinement; with `P2_TEST_GONDOLIN=1` also a real VM through the launcher and a session in a
|
||||
sandbox over a loopback sshd, which need `/dev/kvm`, QEMU >= 7.2 or Docker, sshd and pi-env's packaged Linux daemon).
|
||||
|
||||
The tests use fake providers, temporary data/workspaces and server port 0. A disposable loopback sshd has its own
|
||||
host key, authorized client key and remote HOME. The SSH case skips if sshd or a local pi-env daemon build is absent;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue