Files
rootnode-academy/platform
ced4568andClaude Sonnet 5 046a0fba18 fix(capstone): enforce unlock rule server-side
POST /api/assessments/:definitionId/start only ever checked that the
definition existed and was active — it never checked unlock_rule. GET
/api/capstone correctly computed and displayed lock state, and the
student UI never exposed a start action while locked, but a direct POST
to this route bypassed the check entirely (documented as a known gap in
docs/PHASE6A_ROOT_COURSE_FOUNDATION.md §10). Student UI state is not the
enforcement boundary.

Fixed by binding to the exact same computeCurriculumState() gate
/api/capstone already relies on, rather than re-parsing
definition.unlock_rule independently — one computation of 'is this
unlocked,' not two that could drift. Only a definition the curriculum
path actually names as ITS capstone is gated this way; an ordinary exam
is completely unaffected. Returns a clean 403 { error: 'locked',
blocking_stage_ids } — the same shape /api/capstone already exposes.

Found while fixing this: e2e-phase5b.js's own capstone-completion test
had been passing only because of this exact gap — it never included the
real-ops stage in its completion loop before attempting to start the
capstone, and the missing unlock check let it succeed anyway. Fixed
alongside the platform code (REAL_OPS added to ALL_NON_CAPSTONE), not
routed around.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:05:18 -05:00
..

RootNode Academy — Platform

The full multi-student platform, layered on top of the Lab image at the repo root (backend/ entrypoint/Dockerfile unmodified; frontend bundle gained Phase 3B learning UX, see docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §10–11). 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.
  • api/ — the lightweight Platform API: students, cohorts and cohort membership, curriculum, detailed scenario attempt telemetry, progressive hints, curriculum completion rules, the exam + capstone assessment engine, achievements, per-instructor accounts, persistent student/ instructor sessions, RBAC, audit logging, real student credentials + remote classrooms (Phase 5A), self-paced programs/enrollment + the website integration boundary (Phase 5B), and a provider-neutral enrollment-communications pipeline (activation/enrollment-confirmation/ password-reset email, Phase 5C), the ROOT_ course/module/activity content model layered over curriculum/assessments (Phase 6A), and the instructor/admin control plane. No k3s, no --privileged — this replaced Phase 2A's "identity" instance, which was a full Lab image booting an unused k3s cluster just to serve these APIs. Design: docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md (split + Postgres migration), docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md (sessions, accounts, RBAC, audit), docs/PHASE3B_LEARNING_AND_ASSESSMENT.md (learning/assessment engine), docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md (student credentials, remote classroom), docs/PHASE5B_SELF_PACED_ENROLLMENT.md (programs, enrollment, website integration boundary), docs/PHASE5C_ENROLLMENT_COMMUNICATIONS.md (notifications, email delivery), docs/PHASE6A_ROOT_COURSE_FOUNDATION.md (course/module/activity content model, gold-standard module). Operational reference: api/README.md.
  • gateway/ — the only student-facing path to a Lab (HTTP + WebSocket proxy, environment-aware routing derived from a server-controlled session — not a client-suppliable header/param — and, as of Phase 3B, server-side gating of progressive hint reveal). Design: docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md (routing), docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md (persistent sessions, CSRF), docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §3 (hint gating).
  • egress-proxy/ — shared allowlisting proxy every Lab's outbound traffic passes through.
  • lab-router/ — Phase 4.1 addition. Runs on the Lab Host VM (or dev-local's stand-in in local dev, though direct mode there never exercises it); resolves an opaque routing token to a local Lab container purely from Docker state, over mTLS, so the Gateway never needs a Docker-internal address. Design: docs/PHASE4_1_PREPRODUCTION_HARDENING.md §1-3.
  • docker-compose.dev.yml — the full stack (Postgres, egress proxy, Provisioner, Platform API, Gateway) on one machine. This is the Phase 1/2A/2B local dev exception — production is the two-VM Proxmox split described in docs/LAB_ARCHITECTURE.md §1.

Running the full stack locally

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

# First run only — create the owner account (no ADMIN_PASSWORD anymore, see
# docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md):
cd api
PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
  node bootstrap-owner.js [email protected] "Your Name" "a-real-password-12-chars-plus"

No k3s boot to wait for at this layer anymore (Platform API starts instantly) — the whole platform is reachable at http://localhost:4200 (the Gateway) right away: students at /, instructors at /admin (sign in with the owner account you just bootstrapped, then create further instructor accounts from the Instructors tab).

Tests

  • provisioner/test/run.js — fast, isolated Provisioner tests (network/container/firewall mechanics, no Gateway/Platform API involved). Run against provisioner/docker-compose.dev.yml.
  • test/e2e-phase2a.js — student-flow regression through real session cookies: roster → login → curriculum → launch → Gateway routing → cross-student rejection → progress relay → reset.
  • test/e2e-phase2b.js — the classroom acceptance workflow: instructor creates a cohort, adds students, assigns curriculum, Prepare Labs (concurrent), students complete different scenarios, instructor resets one, instructor destroys all cohort Labs after class — progress survives every step.
  • test/e2e-phase3a.js — persistent sessions (including a real Platform API restart), instructor auth (no enumeration, rate limiting), RBAC across owner/admin/instructor, cohort-scoped instructor access, CSRF enforcement, password change, and audit logging.
  • test/e2e-phase3b.js — attempt telemetry, progressive hint sequencing/persistence through a Lab reset, curriculum stage/Capstone lock-unlock, the exam engine (random draw, server timer, expiry, restart persistence), the Capstone workflow end to end, two-student assessment isolation, and instructor learning analytics/RBAC.
  • test/e2e-phase5a.js — real student credentials (activation/password reset/login), remote classroom sessions (join/heartbeat/announcements/help requests), and instructor conversion of a pilot student to a credentialed account.
  • test/e2e-phase5b.js — programs/enrollment, entitlement gating (pending/future/expired/ cancelled), server-derived completion, disposable-Lab persistence across destroy/recreate, and the website/commerce integration boundary (idempotency, concurrency, RBAC).
  • test/e2e-phase5c.js — the notification lifecycle (queue/retry/failure/exhaustion), activation/enrollment-confirmation/password-reset email delivery via the deterministic dev provider, minor/no-email contact-email resolution, and admin notification RBAC.
  • test/e2e-phase6a.js — course/module/activity hierarchy and ordering, prerequisite-based locking, all three completion behaviors (manual marker/scenario/assessment-derived), resume, cross-student isolation, RBAC for self-paced (no-cohort) students, and draft-content hiding.

All e2e suites need OWNER_EMAIL / OWNER_PASSWORD set to a real bootstrapped instructor account (see "Running the full stack locally" above) — the old ADMIN_PASSWORD mechanism is gone. They're slow (real k3s boots, e2e-phase3b.js also runs a real API restart) and need the full stack up. None of them substitute 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. The one exception is Phase 6A's gold-standard module, which DOES get a real-Lab dress rehearsal — infra/scripts/drills/run-phase6a-course-rehearsal.sh.

Known limitations

See provisioner/README.md (Phase 1), docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md §9 (Phase 2A), docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md (Phase 3A identity limitations), and docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §20 (Phase 3B learning/assessment limitations) for the full, honest lists. Phase 3B's "no automatic abandoned-attempt detection" and "no per-cohort needs-attention threshold configuration" limitations were both closed in Phase 4.1 (§13/§17 of docs/PHASE4_1_PREPRODUCTION_HARDENING.md); its raw-JSON pool_spec limitation was replaced with a structured editor (§18) with an owner-only raw-JSON fallback still available. Phase 4.1's own known limitation: real remote/self-paced delivery needs student email/password authentication, not built — see docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md.

Running this in production

../infra/ has the full production infrastructure package (Ansible, Terraform, firewall, monitoring, backup, CI/CD) for deploying the Platform API/Gateway/Provisioner/Postgres onto a dedicated rn-platform-01 VM, with student Labs — and a new lab-router service (../platform/lab-router/) — on a separate rn-lab-host-01 VM. See ../docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md for the overview and ../docs/PHASE4_1_PREPRODUCTION_HARDENING.md for how Gateway-to-Lab reachability across two real VMs was closed (a Lab Host Router + mTLS data plane, simulation-tested in ../infra/scripts/simulate-two-host/) and for the rest of Phase 4.1's pre-production hardening.