You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Multi-user & safe-deployment architecture: hub, runtime policy, and containerized solo mode
Goal
Enable multiple users in an org to use pi-web through one standard, org-blessed deployment (no hosted service, no per-team custom setups, no forks) — without losing the single-user simplicity of npx pi-web on a personal machine, and without server.ts growing multi-tenant complexity.
Guiding principle
Multi-user is a deployment topology, not a feature. The core server stays single-user forever; multi-user is achieved by running more copies of the single-user server behind a small router. Single-user is not a "mode" — it is the core, and the multi-user layer composes it.
The agent executes arbitrary shell, so isolation must come from OS/container boundaries, not in-process checks. A chat agent with bash cannot be safely multi-tenanted inside one Node process.
Architecture: three independent layers
flowchart TB
A[alice] & B[bob] --> HUB
subgraph L1["Layer 1: identity — hub (new, separate package)"]
HUB["hub<br/>token → identity → instance"]
end
HUB --> IA & IB
subgraph L2["Layer 2: the app — stock single-user pi-web, one per user"]
IA["alice's instance :9001"]
IB["bob's instance :9002"]
end
subgraph L3["Layer 3: execution — runtimes (#43)"]
RA["alice's docker runner"]
RB["bob's docker runner"]
end
IA --> RA
IB --> RB
Solo, containerized ("safe box") — official Docker image wrapping the whole app; one writable workspace mount; the container boundary replaces all safeguards at once. Recommended for non-technical users who want the agent caged and safeguards they can't accidentally unset (weakening it requires editing the docker run invocation; recovery is docker rm + re-run). Never mount the docker socket.
Org multi-user — hub in front of N copies of shape 1/2/3, spawned per user with per-user HOME, workspace, port, and random PI_WEB_TOKEN.
The hub
A small router (~300 lines): authenticate (start with a static token→identity map, interface shaped for OIDC later), spawn the user's instance on demand (idle-reap), proxy HTTP + WebSocket upgrades with the instance token injected as Authorization.
Its only contract with pi-web:
spawn: CLI + env vars (PORT, HOST, PI_WEB_TOKEN, PI_WEB_CWD, HOME)
speak: public HTTP/WS API with a bearer token — no privileged backdoor
health: readiness check
Because of that contract it can (and should eventually) live as a separate package (pi-web-hub), with pi-web as a runtime dependency. Prototype it in-repo first (hub/ directory, own entrypoint, zero imports from server/) so extraction later is a git mv. instances.ts spawning "container per user" instead of "process per user" is a one-strategy change, converging the org recipe and the solo container image.
Codebase plan
server.ts → shrinks toward routing-only (SessionHost + module extraction per the #43 review)
server/… → core single-user modules
proxy/ → NEW: plumbing extracted from supervisor.ts, no behavior change
httpProxy.ts # proxyHttp + WS upgrade tunnel, generalized to a target port
child.ts # spawn/stop/restart-with-backoff for one pi-web child
supervisor.ts → stays the dev-restart tool (~80 lines), now a thin consumer of proxy/
hub/ → NEW entrypoint (later: separate package)
hub.ts # server + upgrade handler (~150 lines)
auth.ts # Authenticator: token map now, OIDC later
instances.ts # Map<identity, Instance>: spawn on demand, idle-reap
config.ts # hub.json: users, workspace template, runtime policy, extension pack
Sequencing
Land Add runtime-bound sessions and runtime UI #43 with the blocking items (especially SessionHost — the seam everything hangs off) and the server.ts decomposition. Don't build on a moving core.
Extraction PR (pure refactor): pull proxy/child code out of supervisor.ts into proxy/. Dev workflow byte-identical.
Read-only to the application in every mode: loaded at startup/reload, no write API, not part of the mutable settings store. UI may display it, never edit it. Warn loudly at startup if the file is writable by the service user.
Solo dev owns the file → policy is a seatbelt (defaults, accident protection), not a security boundary against themselves.
Hub mode: file written by the hub, owned by a different account, read-only to the user; combined with OS permissions (no docker socket, no access to others' homes) it is actually enforced. Core enforces config; deployment decides who may write config.
Users still choose per session within the allowed envelope; the hub never micromanages sessions or speaks the runtime protocol.
Org standardization via extensibility (no forks)
Org-wide behavior ships as a versioned extension pack the hub provisions into every instance — audit hooks (tool_call/user_bash → log sink), advisory guardrails, org defaults/context, identity footer. Extensions are not the multi-user mechanism (they run inside the process, post-auth, with full trust) — they are how org policy lands without patching core.
Simplicity guardrails (enforceable)
server/ never imports from hub/; hub/ never imports from server/.
server.ts never contains the concepts "user", "identity", "owner".
The hub talks to instances only through the public API (bearer token), never a backdoor.
Any core change the hub needs must also make sense as a single-user feature (runtime policy ✅; ownership checks ❌).
Single-user quickstart in the README stays one command, no mention of the hub.
Hardening items (worth doing regardless)
Constant-time token comparison (crypto.timingSafeEqual) in isAuthorized.
Move the token out of the URL query string (WS: one-time ticket or subprotocol).
Origin/host checks on WebSocket upgrades.
Rate-limit auth failures.
In multi-user deployments: PI_WEB_ALLOW_CUSTOM_RUNTIMES stays off (authenticated host RCE), host sessions disabled by policy, per-user artifact dirs / knownCwds.
Explicit non-goals
No in-process multi-tenancy (per-route owner checks, per-user creds in server.ts).
No hosted/SaaS mode.
No pi-web lockdown system installer — the containerized solo image covers the "safe box" persona with less machinery.
The runtime branch is Layer 3 — finish it as-is (it's the isolation half and valuable to solo users on its own). It is not multi-user by itself: identity, ownership, per-user creds/settings are untouched by it and are handled entirely by the hub layer above.
Multi-user & safe-deployment architecture: hub, runtime policy, and containerized solo mode
Goal
Enable multiple users in an org to use pi-web through one standard, org-blessed deployment (no hosted service, no per-team custom setups, no forks) — without losing the single-user simplicity of
npx pi-webon a personal machine, and withoutserver.tsgrowing multi-tenant complexity.Guiding principle
Multi-user is a deployment topology, not a feature. The core server stays single-user forever; multi-user is achieved by running more copies of the single-user server behind a small router. Single-user is not a "mode" — it is the core, and the multi-user layer composes it.
The agent executes arbitrary shell, so isolation must come from OS/container boundaries, not in-process checks. A chat agent with
bashcannot be safely multi-tenanted inside one Node process.Architecture: three independent layers
flowchart TB A[alice] & B[bob] --> HUB subgraph L1["Layer 1: identity — hub (new, separate package)"] HUB["hub<br/>token → identity → instance"] end HUB --> IA & IB subgraph L2["Layer 2: the app — stock single-user pi-web, one per user"] IA["alice's instance :9001"] IB["bob's instance :9002"] end subgraph L3["Layer 3: execution — runtimes (#43)"] RA["alice's docker runner"] RB["bob's docker runner"] end IA --> RA IB --> RBpi-web-hubpackageserver.ts+server/*SessionHost+ runnersThe layers meet only at stock
server.ts; neither the hub nor the runtimes know about each other. Every combination works without special-casing.Deployment shapes (all wirings of the same parts)
npx pi-web. Today's experience, unchanged forever. No hub code loaded.docker runinvocation; recovery isdocker rm+ re-run). Never mount the docker socket.HOME, workspace, port, and randomPI_WEB_TOKEN.The hub
A small router (~300 lines): authenticate (start with a static token→identity map, interface shaped for OIDC later), spawn the user's instance on demand (idle-reap), proxy HTTP + WebSocket upgrades with the instance token injected as
Authorization.Its only contract with pi-web:
PORT,HOST,PI_WEB_TOKEN,PI_WEB_CWD,HOME)Because of that contract it can (and should eventually) live as a separate package (
pi-web-hub), with pi-web as a runtime dependency. Prototype it in-repo first (hub/directory, own entrypoint, zero imports fromserver/) so extraction later is agit mv.instances.tsspawning "container per user" instead of "process per user" is a one-strategy change, converging the org recipe and the solo container image.Codebase plan
Sequencing
SessionHost— the seam everything hangs off) and theserver.tsdecomposition. Don't build on a moving core.supervisor.tsintoproxy/. Dev workflow byte-identical.Runtime policy (single-user feature; the hub is just one author of it)
Consolidate the scattered runtime flags (
PI_WEB_LOCAL_RUNNER,PI_WEB_ALLOW_CUSTOM_RUNTIMES, docker provider settings) into one policy config:{ "defaultRuntime": "docker", "allowedProviders": ["docker"], "docker": { "image": "org/agent:1.4", "network": "none", "memory": "4g" }, "allowCustomRuntimes": false }Rules:
Org standardization via extensibility (no forks)
Org-wide behavior ships as a versioned extension pack the hub provisions into every instance — audit hooks (
tool_call/user_bash→ log sink), advisory guardrails, org defaults/context, identity footer. Extensions are not the multi-user mechanism (they run inside the process, post-auth, with full trust) — they are how org policy lands without patching core.Simplicity guardrails (enforceable)
server/never imports fromhub/;hub/never imports fromserver/.server.tsnever contains the concepts "user", "identity", "owner".Hardening items (worth doing regardless)
crypto.timingSafeEqual) inisAuthorized.PI_WEB_ALLOW_CUSTOM_RUNTIMESstays off (authenticated host RCE), host sessions disabled by policy, per-user artifact dirs /knownCwds.Explicit non-goals
ownerchecks, per-user creds inserver.ts).pi-web lockdownsystem installer — the containerized solo image covers the "safe box" persona with less machinery.Relationship to #43
The runtime branch is Layer 3 — finish it as-is (it's the isolation half and valuable to solo users on its own). It is not multi-user by itself: identity, ownership, per-user creds/settings are untouched by it and are handled entirely by the hub layer above.