New internal component (platform/gateway/) — the only student-facing path to
disposable Lab environments. Browsers never reach a Lab container, the
Provisioner, or Postgres directly.
Routes:
- /api/environment/{status,launch,reset} — student-facing glue (X-Student-Id),
calls the Provisioner internally (shared secret never reaches the browser).
Reuses an existing non-terminal environment on launch rather than always
provisioning a new one; reset destroys+recreates the same environment row.
- /lab/<token>/* — HTTP+WS proxy into that one student's Lab container,
resolved by opaque routing token via a new Provisioner endpoint
(GET /environments/by-token/:token). No raw container IP/port ever reaches
the browser. Rejects invalid tokens (404), unready environments (202/503),
and cross-student access via an optional ?sid= ownership check (403) — see
docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md "Security" for why token-alone,
student-id-alone, and env-id-alone are each individually insufficient and
this is explicitly pilot-grade, not production auth.
- everything else — proxied to the persistent identity/curriculum backend
(roster, login, curriculum, progress, platform-shell frontend), or to the
student's own Lab if a valid lab-session cookie is present.
Routing design note: the existing Lab UI makes root-relative calls
(fetch('/api/scenarios'), the /shell-ws WebSocket) with no idea it might be
served under a /lab/<token>/ prefix — preserving that UI unmodified was a
hard requirement. Solved with an HttpOnly, SameSite=Lax cookie set on the
first /lab/<token>/ response: every later request from that browser carries
it, so the unmodified Lab UI's own unprefixed calls resolve correctly once a
student has navigated into a lab session, with zero changes to its code.
/api/environment/* is explicitly exempted from cookie-based routing so it
never gets hijacked by an active lab session.
Also added platform/docker-compose.dev.yml (the full local stack: Postgres +
egress-proxy + Provisioner + a persistent "identity" instance + Gateway) and
platform/README.md tying the components together. The "identity" instance
required an explicit `network_mode: bridge` — under Compose's own
auto-created bridge network, k3s failed at startup with "no default routes
found in /proc/net/route"; documented as a known limitation, not fully
root-caused.
Found and fixed during testing: express.json() applied globally broke
http-proxy's re-streaming of proxied POST bodies (e.g. POST /api/roster hung
forever, since the body had already been consumed by the parser before
proxy.web() tried to forward it). Removed — none of the Gateway's own routes
actually need a parsed body.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
48 lines
2.5 KiB
Markdown
48 lines
2.5 KiB
Markdown
# RootNode Academy — Platform
|
|
|
|
The full multi-student platform, layered on top of the unmodified Lab image at the repo root.
|
|
Start here, then go to the component you need:
|
|
|
|
- `provisioner/` — Postgres-backed environment registry and lifecycle. Design:
|
|
`docs/LAB_ARCHITECTURE.md`, `docs/PHASE1_PLATFORM_FOUNDATION.md`. Operational reference:
|
|
`provisioner/README.md`.
|
|
- `gateway/` — the only student-facing path to a Lab (HTTP + WebSocket proxy, environment-aware
|
|
routing, student identity glue). Design: `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`.
|
|
- `egress-proxy/` — shared allowlisting proxy every Lab's outbound traffic passes through.
|
|
- `docker-compose.dev.yml` — the full stack (Postgres, egress proxy, Provisioner, a persistent
|
|
"identity" instance, Gateway) on one machine. This is the Phase 1/2A **local dev exception**
|
|
— production is the two-VM Proxmox split described in `docs/LAB_ARCHITECTURE.md` §1.
|
|
|
|
## Running the full stack locally
|
|
|
|
```bash
|
|
cd platform
|
|
echo "PROVISIONER_SHARED_SECRET=$(openssl rand -hex 32)" > .env
|
|
docker compose -f docker-compose.dev.yml build
|
|
docker compose -f docker-compose.dev.yml up -d
|
|
```
|
|
|
|
Wait for `identity` to boot (it runs a real k3s cluster, same as any Lab — check with
|
|
`curl http://localhost:7554/api/health`), then the whole platform is reachable at
|
|
`http://localhost:4200` (the Gateway) — that's the one origin a student's browser would use.
|
|
|
|
## Tests
|
|
|
|
- `provisioner/test/run.js` — fast, isolated Provisioner tests (network/container/firewall
|
|
mechanics, no Gateway/identity involved). Run against `provisioner/docker-compose.dev.yml`.
|
|
- `test/e2e-phase2a.js` — slow, full-stack tests against the compose file above: roster →
|
|
login → curriculum → launch → Gateway routing → cross-student rejection → progress relay →
|
|
reset. Real k3s boots, budget several minutes. Run whenever the Gateway, Provisioner API
|
|
surface, or the identity backend's progress/roster/curriculum routes change.
|
|
|
|
Neither substitutes for the manual real-Lab-image checks (node-pty, terminal WebSocket,
|
|
`kubectl`, scenario validation) in `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` §8 — those need a
|
|
browser-equivalent client and aren't automated here.
|
|
|
|
## Known limitations
|
|
|
|
See `provisioner/README.md` (Phase 1) and `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` §9
|
|
(Phase 2A) for the full, honest lists — including the `identity` service's `network_mode:
|
|
bridge` requirement and its unused-k3s resource cost, both flagged as real, not swept under
|
|
the rug.
|