89 Commits
Author SHA1 Message Date
ced4568 e6a5902ddf docs(website): record that the website-side registration/checkout flow is now built
ced4568/RootNode-academy-website (separate GitHub repo, Chase-authorized access) now implements
docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md's full contract on its own side: public offering
lookup, registration (adult + guardian/minor), agreement acceptance, checkout creation, and a
status-polling success page. Its own stale pricing was also corrected to match docs/
ROOTNODE_PRICING_AND_OFFERS_V1.md. It has nothing to talk to yet -- no real Platform API exists
(Gate 4's hardware blocker, unchanged) -- so every RootNode-facing route there returns a clean
503 until ROOTNODE_API_URL/ROOTNODE_INTEGRATION_KEY point at a real deployment.

Updates PHASE10_LAUNCH_READINESS.md Gate 10 and its matrix row, and WINTER_BREAK_LAUNCH_CHECKLIST.md
and WINTER_BREAK_CAMP_OPERATIONS.md's own website-status notes, all of which previously said
'nobody has built the website-side form yet.'
2026-08-28 15:22:10 -05:00
ced4568 bfae2aafe0 docs(business): canonicalize RootNode V1 pricing matrix
docs/ROOTNODE_PRICING_AND_OFFERS_V1.md — the founder-approved, locked V1 pricing matrix (seasonal
camps, instructor-led programs, self-paced, mentoring/workshops, sibling policy, Winter Break
launch terms) recovered from synced conversation history after the original Phase 9.5 commit was
confirmed absent from canonical Git history. Cross-validated against the independently-prepared
RootNode Academy WHM+WHMCS Agent Handoff's own price guardrail table — identical figures.

Does not resolve or invent dates, venue, capacity, tax treatment, refund/cancellation policy, or
legal agreement language — those remain separate, open launch gates.

PHASE10_CANONICAL_STATE_AUDIT.md updated to reflect the recovery — the original Phase 9.5 commit
is still confirmed missing from history, but the pricing decision itself is now canonically
recorded rather than resting only on verbal confirmation. PC #2 inspection remains useful only to
check for an earlier local copy to compare against, not to locate the pricing itself anymore.
2026-08-28 14:06:03 -05:00
ced4568 74033b4cb8 docs(audit): reconcile Phase 10 findings on Phase 9.5, Infisical, and website pricing
Phase 9.5: reconfirmed no canonical commit exists (broader content search across full history —
still zero hits outside this session's own commits); Chase confirmed the real document lives on
PC #2, unpushed. Not duplicated here per Chase's explicit instruction — he owns bringing it to
canonical Gitea.

Infisical: Phase 10's 'project does not exist' finding was incomplete, not wrong about what it
checked — a proper org-scoped API enumeration (8 projects) still shows no RootNode Academy
project visible to the claude-code-migration machine identity, even though Chase confirms it
exists. Root cause narrowed to a missing per-project access grant (Infisical scopes identity
visibility per-project, separate from org membership) rather than the project not existing.
Corrected docs/SECRETS_AND_INFISICAL.md and docs/PHASE10_CANONICAL_STATE_AUDIT.md accordingly,
with the exact one-step fix (grant the identity access via Project Settings -> Access) and the
identity id needed to do it. No duplicate project was created.

Website pricing: a read-only fetch of rootnodeacademy.com found stale founding-price RANGES that
don't match the exact fixed V1 prices Chase gave directly this phase; Chase confirmed the fixed
prices in this repo's docs are correct and current. Noted as a real discrepancy for his own
awareness, not something this repo's scope covers fixing.

Updated the Phase 10 launch matrix with a Category column (TECHNICAL READY / OWNER DECISION
REQUIRED / EXTERNAL BLOCKER / HARDWARE BLOCKER) distinguishing who needs to act on each
remaining gate. No application code changed — audit/documentation reconciliation only.
2026-08-28 13:09:05 -05:00
ced4568 0b2303a3e2 docs(phase10): canonical state audit, secrets inventory, production/security/launch readiness
Gate 1 — canonical state audit: this workstation's HEAD/origin/main and every claimed milestone
SHA (Phase 2A through 9.1) verified consistent; no stranded branches/tags on Gitea; full-history
secret scan clean (no rotation required); Phase 9.5 confirmed absent from canonical history
(pricing was given directly by Chase and used without it); second-PC reconciliation commands
provided since this session cannot see that machine.

Gate 3 — secrets: real inventory derived from process.env greps across every service (not
guessed); Infisical auth fixed (wrong domain was the actual root cause of Phase 9.1's failures —
the identity belongs to infisical.cedshomelab.com, not app.infisical.com) but no RootNode
Academy project exists there yet, contradicting this phase's own authorization — flagged for
Chase to create.

Gates 4-9/15-16 — production: verified, not assumed, that no dedicated server/Proxmox VM has
been provisioned (empty terraform.tfvars, no state file, placeholder group_vars/production.yml)
— IaC validated (tofu validate, ansible --syntax-check both clean) and left unexecuted per this
phase's own 'do not fake production deployment' instruction. rootnodeacademy.com confirmed live
(Cloudflare, real HTTPS) — the separate, not-RootNode-owned website; no Platform API subdomain
exists yet.

Gate 18 — security audit: full review against every regression suite rerun this phase; no
launch-critical defect found.

Gates 19/20 — full regression (131 checks: e2e-phase5b/5c/6f/7/8/9, content validation,
migrations, frontend build) and the Winter Break commerce rehearsal, both rerun clean against
current main.

Gates 10-14/21 — website integration, Winter Break pricing (99/49, real, Stripe-validated),
agreements/legal, tax, and refund policy all reviewed; legal text/tax/refund policy/dates/venue/
capacity correctly left as open owner decisions, not invented.

No application code changed this phase — audit/launch-prep only.
2026-08-28 12:07:25 -05:00
ced4568 a4c0058f8c docs(stripe): document real Stripe TEST-mode validation results
PHASE9_1_STRIPE_TEST_VALIDATION.md rewritten from BLOCKED to the real, proven results: a real
Stripe TEST Checkout, a real TEST payment at the founder-approved $399 Founding Cohort price, a
genuine signed webhook, the existing registration->enrollment conversion, a real
duplicate-delivery proof, a real automated abandoned-checkout proof, a real refund with the
non-cascade rule reconfirmed, full Stripe<->RootNode reconciliation IDs, and the one real defect
found and fixed. STRIPE_INTEGRATION_RUNBOOK.md updated (real-Stripe local testing is no longer
'not yet exercised'; notes the no-sudo Stripe CLI install path and --api-key/--print-secret
usage). WINTER_BREAK_LAUNCH_CHECKLIST.md: Stripe TEST validation checked, price checked ($399
launch / $449 retail per Chase directly), live-mode items correctly still unchecked.
PHASE9_COMMERCE_AND_LAUNCH.md's Phase 9.1 addendum updated to reflect completion.
2026-08-28 09:47:11 -05:00
ced4568 fbf52d8334 test(stripe): add real-Stripe regression coverage and complete the opt-in TEST-mode check
e2e-phase9.js: one new deterministic regression test proving the webhook-correlation fix — a
test event referencing a transaction's own UUID (the exact shape a real Stripe
payment_intent.payment_failed/charge.refunded event now produces) is still matched. 20/20.

stripe-integration-test.js: configurable price/currency (STRIPE_TEST_PRICE_CENTS/
STRIPE_TEST_CURRENCY) and a STRIPE_TEST_MINOR=true guardian+minor path. Once a real payment
lands, now fully automates deep post-payment verification: exactly-one payment_transactions
record, registration/enrollment/cohort/guardian reconciliation, a genuine duplicate-delivery
proof (re-signing the REAL event fetched via stripe.events.retrieve — never a fabricated one),
real student activation + program-visibility, and a real refund with the non-cascade rule
reconfirmed — plus a fully automated, browser-free abandoned-checkout proof via Stripe's own
sessions.expire() API. Still opt-in, still a guaranteed no-op by default, still refuses
live-mode keys.
2026-08-28 09:47:01 -05:00
ced4568 c86b3de611 fix(payment): correlate PaymentIntent/Charge-referencing webhooks to the right transaction
Real Stripe TEST traffic (Phase 9.1) exposed a genuine defect the deterministic TestProvider
never could: payment_intent.payment_failed and charge.refunded events reference Stripe's
PaymentIntent/Charge object, never the Checkout Session, but payment_transactions.
provider_reference only ever stored the Session id. A real failure or refund webhook would have
silently failed to match any transaction (UNKNOWN_TRANSACTION).

- payment-provider.js: session creation now also sets payment_intent_data.metadata (the same
  metadata already carrying our transaction_id), so it propagates onto the PaymentIntent Stripe
  creates. normalizeEvent() (now async) reads transaction_id back from the PaymentIntent's own
  metadata directly for payment_intent.payment_failed, and via one extra paymentIntents.retrieve
  call for charge.refunded (Charges don't inherit PaymentIntent metadata automatically).
- commerce.js: the transaction lookup now matches provider_reference OR transaction_id — one
  additional OR clause, no schema/migration change.
- webhook-routes.js: added the now-required await on verifyAndParseWebhook (TestProvider's
  synchronous return is unaffected).
- docker-compose.dev.yml: wires PAYMENT_PROVIDER/STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET through
  to the api container (previously entirely absent — Phase 9 never needed it against the
  deterministic provider), empty/unset by default matching EMAIL_PROVIDER's own convention.

Verified fixed against a real Stripe TEST refund (Phase 9.1); e2e-phase9.js 20/20 and the Winter
Break commerce rehearsal 13/13 rerun clean after the fix.
2026-08-28 09:46:51 -05:00
ced4568 77fd1869db docs(stripe): document Phase 9.1 blocked status and live-mode readiness checklist
New: PHASE9_1_STRIPE_TEST_VALIDATION.md — baseline verification, the exact Stripe-credential
search performed (none found), the Stripe architecture review (dynamic price_data, unchanged),
what was proven vs. blocked per the mission's own step list, and the live-mode readiness
checklist. Updated: STRIPE_INTEGRATION_RUNBOOK.md and WINTER_BREAK_LAUNCH_CHECKLIST.md to point
at the new opt-in test and the blocked status. PHASE9_COMMERCE_AND_LAUNCH.md gets a short
addendum. No application code changed this phase — no genuine defect was exposed without real
Stripe traffic, so no regression rerun was required.
2026-08-27 10:40:53 -05:00
ced4568 f0ff14af38 test(stripe): add opt-in real Stripe TEST-mode integration check
RUN_STRIPE_INTEGRATION_TEST=true (default: no-op, zero Stripe network traffic). Refuses to run
against a live-mode secret key. Automates what's provable via pure API alone — real Checkout
Session creation verified against Stripe's own retrieve API, server-authoritative pricing proven
against Stripe's own recorded amount even under a tampered caller amount, invalid-signature
webhook rejection with no domain side effect — then polls RootNode's status endpoint for a
human/Stripe-CLI-completed real payment rather than fabricating a signed success event. Never
runs as part of normal regression/CI.
2026-08-27 10:40:42 -05:00
ced4568 80a5f6e457 docs(phase9): document commerce integration and Winter Break launch readiness
New: PHASE9_COMMERCE_AND_LAUNCH.md, WEBSITE_REGISTRATION_AND_CHECKOUT.md,
STRIPE_INTEGRATION_RUNBOOK.md, WINTER_BREAK_LAUNCH_CHECKLIST.md. Updated:
ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (commerce boundary + PHASE 9 BUILT rows),
OFFERING_AND_REGISTRATION_MODEL.md (pricing, the payment/commerce boundary, three-status
principle), WEBSITE_ENROLLMENT_INTEGRATION.md (price fields, real agreement/checkout/webhook
sections replacing Phase 8 placeholders), WINTER_BREAK_CAMP_OPERATIONS.md (real payment step
replacing the manual placeholder).
2026-08-27 09:06:04 -05:00
ced4568 281d60bc2d test(camp): add Winter Break commerce rehearsal
13/13 against a real, freshly-built stack, PAYMENT_PROVIDER=test (no live money): admin
configuration including price/payment-provider readiness, minor + adult registration through
real checkout and a real webhook call, duplicate-webhook idempotency, failed-payment seat
retention, abandoned-checkout seat release, a last-seat checkout race, a real classroom session,
and a real Lab task completed by the newly-activated minor student.
2026-08-27 09:05:54 -05:00
ced4568 8b3e56e402 test(phase9): add commerce integration acceptance suite
19/19: pricing in the public serializer, checkout gated on agreements, server-authoritative
amount, webhook signature verification and idempotency, payment success/failure/abandonment,
last-seat checkout race, reservation reaper, manual refund with no cascade, minor guardian
routing, admin/RBAC payment-data restrictions, audit/metric hygiene, the status-lookup endpoint,
and confirmation that ROOT_ V1/free-offering/classroom flows are unaffected.
2026-08-27 09:05:46 -05:00
ced4568 f0dde9b84a feat(admin): add offering pricing and payment/refund UI
- admin-offerings-routes.js: PATCH /api/admin/offerings/:id (allowlisted fields), GET
  /api/admin/registrations/:id/payments, POST /api/admin/payments/:id/refund
- frontend: Pricing card on offering detail, Payments card (transaction history + record-refund
  action) on registration detail — admin/owner-gated, reusing existing admin UI conventions
2026-08-27 09:05:36 -05:00
ced4568 16622bd9ab feat(webhook): add verified payment webhook route and website checkout/agreement contract
- webhook-routes.js: POST /api/webhooks/payment — signature-verified, idempotent, always 200
  once verified (a forged/invalid signature is a clean quiet 400)
- integration-routes.js: POST /api/integrations/registrations/:id/agreements (closes Phase 8's
  gap — agreement acceptance now on the public registration contract), POST .../:id/checkout
  (server-to-server, amount always server-side), GET .../:id/status (minimal safe status lookup
  for the website's success page — no PII, no amount, no provider reference)
- public-offerings-routes.js: price_amount/price_currency/requires_payment in the public serializer
- metrics.js: checkout/payment/webhook/conversion/seat-released counters, all low-cardinality
- notifications.js: listForRegistration now unions by the registration's own enrollment_id too,
  so the post-conversion activation notification shows up in the registration's communication
  history (previously only pre-conversion notifications were visible)
2026-08-27 09:05:29 -05:00
ced4568 b0e07bb17f feat(commerce): add payment-provider abstraction, commerce layer, and pricing schema
- migration 037: offerings.price_amount/price_currency, offering_registrations.reservation_expires_at,
  payment_transactions, payment_webhook_events (webhook_event_id as real PK — the idempotency mechanism)
- payment-provider.js: StripeProvider (real SDK, HMAC webhook verification) + TestProvider
  (deterministic, in-process, no network) behind one interface, selected via PAYMENT_PROVIDER
- commerce.js: createCheckoutSession (server-side price, never caller-supplied), processWebhookEvent
  (idempotent), handlePaymentSuccess/Failure/CheckoutAbandoned/Refund, reservation reaper support
- offerings.js: computeReservationExpiry, updateOffering (allowlisted PATCH), readiness gained
  price/payment_provider checks, convertRegistrationToEnrollment now fires the activation email
  every other enrollment path already triggers, and links the registration's guardian_id into
  student_guardians on conversion (real bug: a converted minor had no guardian link, so the new
  activation email had nowhere to resolve a recipient to)
- server.js: raw-body middleware scoped to /api/webhooks/payment, reservation-reaper poller
2026-08-27 09:05:19 -05:00
ced4568andClaude Sonnet 5 788fb197e0 docs(phase8): document enrollment operations
New: PHASE8_ENROLLMENT_OPERATIONS.md (phase summary), WINTER_BREAK_CAMP_OPERATIONS.md
(the real operational walkthrough), REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md,
OFFERING_ADMIN_GUIDE.md.

Updated: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (BUILT NOW/PHASE 8 BUILT
table), OFFERING_AND_REGISTRATION_MODEL.md (both ordering bugs, launch readiness,
registration enforcement), STUDENT_GUARDIAN_DATA_MODEL.md (guardian communications now
wired), WEBSITE_ENROLLMENT_INTEGRATION.md (the real, live registration contract with
worked examples, replacing the old speculative future-flow section), platform/README.md
(test list).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:42 -05:00
ced4568andClaude Sonnet 5 aa63d09fb8 test(phase8): add enrollment operations coverage and Winter Break registration rehearsal
- e2e-phase8.js (16/16): offering CRUD/transitions/readiness, registration open/closed
  enforcement, capacity + concurrent final-seat race through the real registration
  endpoints, the public serializer's safety, website integration idempotency (sequential +
  concurrent), adult/minor paths, required-agreement conversion blocking with version
  snapshotting, offering->cohort membership, guardian recipient resolution, RBAC, and
  confirmation ROOT_ V1/self-paced/classroom flows are unaffected.
- test-phase8-winter-camp-registration.js (12/12): the full commercial-path story with no
  payment code — website submits a real minor registration (real guardian created/matched),
  guardian agreement acceptance, a simulated admin-side payment confirmation, conversion to
  real enrollment/cohort, real account activation, an independent adult path, two students
  genuinely competing for the final seat, and a REAL Lab task completed by the minor
  student with the instructor then seeing the real progress.
- e2e-phase7.js: fixed two now-stale test assumptions the new server-authoritative
  registration-window enforcement broke (an offering left in draft that Phase 7's own test
  never transitioned before registering against it) and widened a file-allowlist check for
  the new, legitimate public-offerings-routes.js.

Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32, e2e-phase5c.js 29/29, all fresh-stack.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:32 -05:00
ced4568andClaude Sonnet 5 40362ec0c9 feat(admin): add Offerings operations UI
Offerings list + create; offering detail with overview (valid-transitions-only buttons),
server-computed launch readiness, required agreements, and registrations/roster;
registration detail with lifecycle transitions, agreement status, communication history
(masked recipients), and convert-to-enrollment. Reuses the existing Admin.module.css/
adminApi conventions exactly (ProgramsView.jsx's own list+detail pattern) — no new UI
framework or pattern introduced.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:20 -05:00
ced4568andClaude Sonnet 5 24eee80354 feat(offering): launch readiness, agreement requirements, guardian communications, website registration API
- Registration is now server-authoritative: draft/cancelled/completed/full/window-closed
  offerings reject registration attempts with distinct error codes (assertRegistrationOpen).
- offering_agreement_requirements + conversion-time blocking on missing required
  agreements; accepted_by_type gained 'registrant' for an adult self-accepting before
  their student_id exists.
- computeOfferingReadiness() — a server-derived launch checklist (curriculum, instructor,
  schedule, location, capacity, registration window, agreements, curriculum package).
- Guardian communications wired into the existing Phase 5C notification system: a guardian
  email fallback in resolveRecipient(), a parallel enqueueForRegistration()/
  resolveRegistrationRecipient() pair for pre-account registration-stage notifications
  (registration_received/confirmed/needs_action, agreement_required, camp_operational_update),
  five new RootNode-branded templates. Same table, same worker, same masked admin view —
  no second notification system.
- public-offerings-routes.js: the unauthenticated, explicitly-allowlisted offering read API.
- POST /api/integrations/registrations: the website's idempotent registration submission
  contract, same shared-secret boundary as the existing enrollment integration endpoint.

Two real bugs found and fixed during this phase's own testing before any commit:
(1) the five new notification_type values were wired into the worker before the DB's own
CHECK constraint was widened to accept them — every registration-stage notification was
silently failing until a comprehensive smoke test caught it (036 fixes the constraint;
every previously-silent notification catch now at least logs).
(2) firing a notification right after a registration's insert, before its seat reservation
could still fail and roll the row back, created a real foreign-key violation on rollback —
fixed by reordering createRegistration() to validate -> reserve -> insert -> notify.

See docs/OFFERING_AND_REGISTRATION_MODEL.md and docs/PHASE8_ENROLLMENT_OPERATIONS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:11 -05:00
ced4568andClaude Sonnet 5 df5e39f767 docs(phase7): document the business/product architecture foundation
New: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (canonical business/platform
bridge — course families, delivery modes, BUILT NOW/PHASE 7 BUILT/FUTURE), OFFERING_AND_
REGISTRATION_MODEL.md (lifecycle diagrams, capacity mechanics), STUDENT_GUARDIAN_DATA_
MODEL.md (data minimization, RBAC, legal-review items), SCHOOL_AND_COMMUNITY_DELIVERY_
ARCHITECTURE.md (organization/location mapping, explicitly not multi-tenancy),
PHASE7_PRODUCT_ARCHITECTURE.md (phase summary).

Updated: WEBSITE_ENROLLMENT_INTEGRATION.md (documented future offering/registration flow,
current live contract unchanged), platform/README.md (test list).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 22:44:13 -05:00
ced4568andClaude Sonnet 5 7d870d8e2b test(phase7): add business/product architecture acceptance suite and Winter Camp rehearsal
- e2e-phase7.js (17/17): organizations/locations, guardians, curriculum packages
  (reference-only), offering capacity/transitions, registration lifecycle, agreements,
  registration-to-enrollment conversion, RBAC, age-guidance-independence, and confirmation
  that existing ROOT_ V1 self-paced/classroom flows are unaffected.
- test-phase7-winter-camp.js (18/18): the full Winter Break Camp operational path with NO
  payment code invoked — organization/location/curriculum-package/cohort/offering setup,
  guardian + adult registration, agreement acceptance, a simulated admin-side commerce
  confirmation, conversion to real enrollments and real cohort membership, real account
  activation via real one-time tokens, a real classroom session, and a REAL Lab task
  (linux-inspect-hardware) completed by the minor student inside a real disposable Lab
  container, confirmed to survive a real logout/login.

Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32 (including its own embedded Phase 5A
checks) on independent fresh stacks.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 22:44:02 -05:00
ced4568andClaude Sonnet 5 3510979196 feat(platform): business/product architecture — organizations, guardians, offerings, registrations
- organizations, locations, guardians, student_guardians (033) — additive, no existing
  table's meaning changes. Widens programs.family to accept 'bootcamp'.
- curriculum_packages/curriculum_package_items, offerings, instructor_offerings,
  offering_registrations, agreement_definitions/agreement_acceptances (034) — the full
  purchasable-delivery model. Server-defined status transitions only (no client-controlled
  offering/registration status). Capacity uses a single atomic UPDATE (reserved_seats),
  never a SELECT-count-then-INSERT race.
- offerings.js — business logic: reserveSeat/releaseSeat, transitionOffering/
  transitionRegistration (fixed transition tables), convertRegistrationToEnrollment
  (reuses enrollment.js's existing resolveOrCreateStudent/createManualEnrollment rather
  than duplicating student-matching logic).
- enrollment.js — resolveOrCreateStudent gains an optional, default-off returnToken param
  (used only by offerings.js's admin-only conversion route; the public website integration
  endpoint's contract is completely unchanged, per docs/WEBSITE_ENROLLMENT_INTEGRATION.md).
- admin-offerings-routes.js — the full admin/owner API surface. Registration/guardian
  detail is admin/owner only; offering/location listing is any instructor (access_notes
  stripped for a plain instructor).
- A real bug found and fixed during this phase's own smoke testing before any commit: the
  registration route originally reserved a seat BEFORE validating the registration's own
  fields, letting a validation failure consume a real seat and get masked behind a false
  OFFERING_FULL under a full offering. Fixed: validate (create) first, reserve second, roll
  back the registration row if reservation then genuinely fails.

See docs/ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md and
docs/OFFERING_AND_REGISTRATION_MODEL.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 22:43:52 -05:00
ced4568andClaude Sonnet 5 d066410451 docs(phase6f): document ROOT_ V1 pilot readiness
Release to Docker Hub / Build & Push Docker Image (push) Canceled after 0s
Release to Docker Hub / Create GitHub Release (push) Canceled after 0s
New: PHASE6F_ROOT_V1_PILOT_READINESS.md (phase summary), ROOT_V1_CURRICULUM_MANIFEST.md
(canonical V1 scope), ROOT_V1_PREREQUISITE_MAP.md, ROOT_V1_ASSESSMENT_MAP.md,
ROOT_V1_SYLLABUS.md, ROOT_V1_INSTRUCTOR_GUIDE.md, ROOT_V1_STUDENT_REQUIREMENTS.md,
ROOT_V1_INSTRUCTOR_LED_PILOT.md, WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md.

Updated: ROOT_PROGRAM_ARCHITECTURE.md (course-sequence table reflects real V1 statuses),
ROOT_PILOT_CURRICULUM.md (self-paced-readiness correction), ROOT_PROGRAM_DURATION_MODEL.md
(real post-integration totals: 64.2 V1 hours / 68.3 program-wide), ROOT_CAPSTONE_READINESS.md
(§1b, the new root-09 course-layer gate), platform/README.md (test list).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:46 -05:00
ced4568andClaude Sonnet 5 bd55b74b4f test(pilot): add multi-student and end-to-end ROOT_ V1 rehearsals
- test-phase6f-root-v1-pilot.js (22/22): a real cohort + live classroom session with three
  students in genuinely different states — on track, a real Lab validation failure/help-
  request/fix cycle, and a real admin Lab-reap/recreate with academic-state persistence
  proven, not assumed. Instructor visibility distinguishes all three.
- test-phase6f-root-v1-end-to-end.js (14/14): one student, entirely through the real
  student API, ROOT 01 through Capstone-eligible, including a real nested-Docker
  container-networking fix and an explicit proof that the real rootnode-capstone assessment
  is genuinely startable at the end — not just that the course tree says complete. Found and
  documented (not fixed) a real, pre-existing gap: the legacy six-stage Capstone unlock_rule
  requires two scenario sets (root-07-m01/m02) that are not course-layer prerequisites of
  anything on the path to root-capstone.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:35 -05:00
ced4568andClaude Sonnet 5 2864fe03dc test(phase6f): add V1 pilot acceptance suite; fix regression from real curriculum changes
- New platform/test/e2e-phase6f.js (17 tests): V1 manifest/exclusion correctness, the full
  V1 prerequisite graph, ROOT 01-04 re-verification, the orientation module's unlock
  behavior, optional-activity handling, the strengthened Capstone gate in both directions,
  instructor-notes RBAC on locked (not just draft) content, isolation, and an explicit
  age-independence check.
- foundations-helper.js: completeThroughRoot06/completeThroughRoot10 updated for the new
  practice-bank intro-lesson activities; new completeThroughCapstoneEligible() helper
  (ROOT 07-m03/m04 + full ROOT 09 + ROOT 12) shared by e2e-phase6f.js and both new drills.
- e2e-phase6a/6b/6d/6e.js: resume-pointer assertions updated for the new orientation
  module; e2e-phase6a.js's and e2e-phase6b.js's own inline ROOT 06/07/09/10/12 completion
  sequences updated for the new intro-lesson activities and the new root-09 Capstone
  dependency; e2e-phase6b.js's stale ROOT 09 status assertion and its draft-activity test
  (previously targeting the now fully-excluded ROOT 08) corrected.
- test-phase6d/6e-foundations-rehearsal.js: same orientation-module fix applied to the
  real-Lab dress rehearsals.

All of 6A (23/23) / 6B (26/26) / 6C (17/17) / 6D (21/21) / 6E (23/23) / 6F (17/17) verified
green on independent fresh stacks. See docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:21 -05:00
ced4568andClaude Sonnet 5 f695b7c95d feat(content): author the ROOT_ V1 orientation module and three practice-bank intro lessons
- root-01-m00-welcome-to-root: program overview + Lab-lifecycle orientation (disposable
  Labs, permanent progress, hints, reset/recreate, help-requests).
- One short intro-lesson activity for each of the three bare practice-bank modules
  (root-06-m01, root-07-m01, root-10-m02), explaining what the bank is and why it has no
  per-scenario teaching of its own — the minimum real authoring needed for genuine
  self-paced readiness without reclassifying them optional (see the curriculum migrations).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:09 -05:00
ced4568andClaude Sonnet 5 e48710a0be feat(curriculum): integrate ROOT_ V1 scope — status corrections, ROOT 08 exclusion, Capstone/ROOT 09 gate, orientation
- Correct stale courses.status for ROOT 05/06/07/09/10/12/Capstone (all modules were
  already ready; the course-level field was never bumped in an earlier phase).
- Exclude ROOT 08 (Storage & Scheduling) from ROOT_ V1 via visibility=draft — nothing
  downstream requires it, and leaving it visible mid-sequence would look like a skipped course.
- Self-correct a same-phase mistake: reclassifying the three instructor-led-only practice
  banks (root-06-m01/root-07-m01/root-10-m02) as optional would have broken Capstone
  reachability, since their scenarios are the literal legacy stage-gate content. Kept them
  required and gave each a real intro-lesson activity instead.
- Strengthen root-capstone's prerequisites to require ROOT 09 (Security) alongside ROOT 12
  (Real Ops) — the Capstone's own scenario pool has zero security overlap, and ROOT 09 only
  reached genuine READY status this phase.
- Add a required orientation module (root-01-m00-welcome-to-root) explaining the program,
  the Learn/Practice/Lab/Troubleshoot/Validate loop, and Lab-lifecycle expectations — no
  orientation content existed anywhere before this phase.

See docs/ROOT_V1_CURRICULUM_MANIFEST.md and docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:00 -05:00
ced4568andClaude Sonnet 5 87ad1251ce docs(phase6e): document the ROOT Foundations Completion milestone
Adds PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md (the phase's own record: what was authored, the
dash-vs-bash scenario bug found and fixed, the pre-existing lock-enforcement gap found and
fixed, the regression fallout that fix revealed in earlier suites and why each was a stale test
assumption rather than a platform bug, and honest test/rehearsal/regression results).

Updates ROOT_FOUNDATIONS_PILOT_READINESS.md (full re-audit: all 4 ROOT Foundations courses now
READY, not inherited blindly from Phase 6D), ROOT_PROGRAM_DURATION_MODEL.md (real ROOT 02-04
duration data: 24.7 combined hours, landing inside Phase 6D's own estimated range; 67.9 hours
across the full authored program), ROOT_PILOT_CURRICULUM.md (ROOT 02-04 move from NOT IN PILOT
to IN PILOT with full module-level detail; the pilot is now genuinely self-paced-capable for the
entire foundational half, not just instructor-led), ROOT_PROGRAM_ARCHITECTURE.md and
ROOT_CURRICULUM_COVERAGE_MATRIX.md (course/module/scenario totals), and
ROOT_FOUNDATIONS_ARCHITECTURE.md (marked as-built, not just as-designed).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:09:02 -05:00
ced4568andClaude Sonnet 5 59edceea1f test(phase6e): acceptance suite, real-Lab rehearsal, and regression fixes for the lock fix
Adds platform/test/e2e-phase6e.js (23 checks): full ROOT 02/03/04 hierarchy/content/assessment-
pool verification, the complete ROOT 01->02->03->04->05 chain proven on one student (each
course's completion unlocking exactly the next), the direct-API lock bypass proven blocked on
both a Phase 6E course pair and a pre-existing unrelated pair, existing ROOT 06+/Capstone/
is_required behavior confirmed unaffected, self-paced no-instructor-deferral scan,
instructor-notes exclusion, RBAC, persistence, isolation, version stability, content validation.
23/23 pass against both a warm and a freshly-reset stack.

Adds platform/test/foundations-helper.js — a shared completeRootFoundations01Through04/
completeThroughRoot06/completeThroughRoot10 helper, extracted once multiple suites needed the
identical real-completion sequence as test setup, rather than duplicating it four times.

Adds infra/scripts/drills/{run,test}-phase6e-foundations-rehearsal — the real Platform API/
Gateway/Provisioner/disposable-Lab/nested-Docker/Git/k3s dress rehearsal: one real Lab task per
new course (nginx inspection, a real merge conflict, a real container-networking fix) plus the
full chain to a genuine ROOT 05 unlock, with k3s reconfirmed healthy and untouched at the end.
13/13 pass against a real, freshly-built stack.

Fixes the regression fallout from the (correct) course-routes.js lock fix: several earlier
suites' own historical shortcuts had accumulated small ordering/scenario gaps the missing
enforcement had been silently covering for — none were platform bugs. e2e-phase6a/6b/6c/6d.js
now inject the real ROOT 01-04 (and where needed ROOT 05/06/10) completion sequence via
foundations-helper.js before touching ROOT 05+ content; e2e-phase6b.js's bypass-student flow now
completes deployment-rollback/job-task/cronjob-task/container-logging-mcq explicitly (real
scenarios added in later phases that its original ALL_STAGES list never included); e2e-phase6a/
6d.js's own ROOT 01/05 completion tests now complete each module fully (through its lab and
reflection) before moving to the next, since those courses' own modules carry real intra-course
prerequisites. Two now-obsolete Phase-6D-era assertions in e2e-phase6d.js (ROOT 02-04 stay
PLANNED / stay vacuously unlocked) are updated to assert the opposite, with a comment explaining
Phase 6E intentionally changed that fact. All four suites re-verified green fresh.

Also fixes a real, reproducible dash-vs-bash bug caught while testing the new ROOT 01
rehearsal's ordering fix: `&>file` misparses under this image's actual /bin/sh (dash), breaking
a later `$!`-based kill — the rehearsal script now writes multi-line completion loops with
explicit status assertions, matching the discipline the other suites already use.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:48 -05:00
ced4568andClaude Sonnet 5 a09b7aaa53 fix(curriculum): enforce course/module lock on manual-marker completion
POST /api/student/activities/:activityId/complete previously checked only that the activity
exists and the student holds an active PROGRAM enrollment — never whether the activity's own
course/module was actually unlocked for that student. A direct POST could mark any
manual_marker activity (lesson/demonstration/knowledge_check/guided_exercise/reflection)
complete regardless of the real prerequisite chain, the exact class of gap Phase 6B already
fixed once for the capstone's own start route, just never closed for this more general one.

Found while proving the real ROOT 01->02->03->04->05 chain via direct API (this phase's own
explicit requirement) — and confirmed to predate Phase 6E entirely by reproducing it against an
unrelated, pre-existing ROOT 05/06 pair before touching any Phase 6E content.

Fix: the route now resolves the activity's own course/module through the exact same
buildCourseTree() lock computation every other route already trusts, and returns 403 { error:
'locked' } if either is locked — reusing the existing mechanism, not a second, duplicated check.
Verified live: both a ROOT 02 bypass attempt and a ROOT 06 bypass attempt are now correctly
rejected; legitimate unlocked completions are unaffected.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:28 -05:00
ced4568andClaude Sonnet 5 682ef2de47 feat(root04): author Containers & Docker
Adds all 8 ROOT 04 modules (why containers exist, images & containers, Docker CLI, container
networking, container storage, Dockerfile & image build, Docker Compose, containers →
Kubernetes) — 43 activities, using the nested Docker Engine Phase 6D built and tested. Modules
1, 2, and 8 are conceptual (no lab by design) — the honest bridge module (M8) explicitly names
what Kubernetes adds beyond what this course already taught, without re-teaching Kubernetes
itself.

Adds 5 new hands-on Docker scenarios plus 2 MCQs, all against the real nested Docker Engine:
docker-cli-basics (run/exec/inspect a real container), docker-container-networking (a genuine
"works internally, unreachable from host" port-publishing failure, diagnosed and fixed by
recreating the container with the correct `-p` flag), docker-container-storage (a real
destroy/recreate cycle proving named-volume persistence, not just described), docker-build-image
(a real Dockerfile, a real build, a real custom image run), and docker-compose-multiservice (a
real two-service Compose application with a real published port). All tested end-to-end via the
real scenario-validation API, including negative tests.

Wires rootnode-containers-assessment (2 required + 4 random-draw scenarios) using the existing
Phase 3B assessment engine.

This completes the entire ROOT_ Foundations sequence (ROOT 01-04) — see
docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:14 -05:00
ced4568andClaude Sonnet 5 e68b6f445a feat(root03): author Git & Collaborative Development
Adds all 6 ROOT 03 modules (why version control exists, the Git mental model, everyday Git,
branching & merging, remote repositories, engineering Git workflow) — 31 activities. Uses only
real LOCAL Git repositories throughout — no Gitea account, no network access, and no
RootNode-canonical-repo credential is ever needed or exposed in a student Lab
(docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §4). Modules 1, 2, and 6 are conceptual bookends with no
lab, matching the flexible readiness standard already used in ROOT 01/02.

Adds 3 new hands-on Git scenarios plus 2 MCQs: git-everyday-basics (init/add/commit, validated
by inspecting real `git log`/`git show` state, not just command output strings — Step 6's
explicit requirement), git-branching-and-merge (a REAL merge conflict caused on purpose by two
branches editing the same line, resolved by the student, validated by confirming the conflict
markers are gone and a real two-parent merge commit exists), and git-remote-clone-push (a real
local bare repository standing in for a remote — clone/commit/push validated by inspecting the
REMOTE side directly, not just the student's own clone). All tested end-to-end via the real
scenario-validation API, including negative tests.

Wires rootnode-git-fundamentals-assessment (2 required + 3 random-draw scenarios) using the
existing Phase 3B assessment engine.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:00 -05:00
ced4568andClaude Sonnet 5 e007bcf000 feat(root02): author Linux Administration & Networking
Adds all 6 ROOT 02 modules (Linux system administration, storage & filesystems, the
Networking Mental Model, Linux network inspection, DNS & HTTP, Linux troubleshooting) — 37
activities, each with a real lesson, demonstration/guided exercise, knowledge check, hands-on
Lab, and reflection. The Networking Mental Model module (M3) gets deliberately extra depth per
its "critical module" designation, with no lab by design — conceptual mastery via knowledge
check and guided exercise, matching root-05-m00's own precedent.

Adds 6 new hands-on Linux/networking scenarios plus 2 MCQs: linux-service-inspection (inspects
the platform's own real nginx without touching it — a genuinely safe, real service-inspection
target that avoids ever risking the Lab's own connectivity), linux-service-lifecycle (start/
verify/stop a fully controllable service), linux-storage-inspection, linux-network-inspection
(ip/ss/getent/curl, each framed as answering one specific diagnostic question),
linux-dns-http-diagnosis (a genuine DNS-resolves-but-wrong-port failure, diagnosed and fixed),
and linux-troubleshooting-framework (a real missing-config-file failure, resolved via the
RootNode Observe/Evidence/Hypothesis/Test/Repair/Validate/Prevent framework). All tested
end-to-end via the real scenario-validation API, including negative tests confirming they fail
when unsolved.

Wires rootnode-linux-networking-assessment (2 required + 4 random-draw scenarios) using the
existing Phase 3B assessment engine.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:07:46 -05:00
ced4568andClaude Sonnet 5 b6516a74db docs(phase6d): document the ROOT Foundations milestone
Adds PHASE6D_ROOT_FOUNDATIONS.md (the phase's own record: the generic-scenario-engine finding,
the tested container-runtime decision, what was authored, the bugs found and fixed, and honest
test/regression/rehearsal results) and ROOT_FOUNDATIONS_PILOT_READINESS.md (module-by-module
READY/DEFERRED classification for ROOT 01-04 against the unchanged Phase 6C readiness standard —
ROOT 01 READY, ROOT 02-04 DEFERRED by design).

Adds ROOT_PROGRAM_DURATION_MODEL.md — real per-course instructional-hour totals computed from
actual estimated_duration_minutes values (2,931 minutes / 48.9 hours across all authored
content), not invented, with an explicit note on why instructor-led timing isn't a separate
summable field yet.

Updates ROOT_PROGRAM_ARCHITECTURE.md (ROOT 01's new READY status and the real 01-04 prerequisite
chain), ROOT_PILOT_CURRICULUM.md (ROOT 01 added to the pilot, module-level detail), and
ROOT_CURRICULUM_COVERAGE_MATRIX.md (updated module/scenario totals) to reflect what Phase 6D
actually built, without restating Phase 6C's own Kubernetes-scenario tables.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 13:00:41 -05:00
ced4568andClaude Sonnet 5 f5379afd5c test(phase6d): acceptance suite, real-Lab rehearsal, and 6A/6B resume-point fixes
Adds platform/test/e2e-phase6d.js (20 checks): ROOT 01 hierarchy, ROOT 02-04 honestly empty
(not padded), activity ordering, lab-cannot-be-manually-completed, full course completion via
lesson/lab/assessment, the prerequisite chain proven on two students (one who completed ROOT 01,
one fresh), existing ROOT 05+/Capstone state unaffected, is_required unaffected, self-paced
no-instructor-deferral check, instructor-notes exclusion, RBAC, logout/login persistence,
cross-student isolation, version-bump stability, and content validation. 20/20 pass against both
a warm and a freshly-reset dev stack.

Adds infra/scripts/drills/{run,test}-phase6d-foundations-rehearsal — the real disposable-Lab
dress rehearsal: login -> enroll -> lesson/guided-exercise -> real Lab launch -> real bash
hardware inspection (nproc/free/df, written to a real file) -> real validation -> a genuine
permission denial (a second Linux user actually denied via `su -`) then a real chmod fix ->
remaining modules -> assessment -> course completion -> ROOT 02 unlock -> logout/login
persistence -> instructor visibility of the resulting learning state. 11/11 pass against a real
Lab container, no mocking. (First run caught a real triple-escaping bug in the hardware-report
step's inline heredoc — fixed by writing the script to a file and docker-cp-ing it in instead.)

Fixes two stale hardcoded assumptions in e2e-phase6a.js and e2e-phase6b.js that this phase's own
ROOT 01 addition broke: both suites asserted `resume` landed on ROOT 05's gold module as "the
first real content," which was true before this phase and is now ROOT 01. Both suites' original
intent (resume must never land on a bare-lab bank) is preserved, just against the current front
door. Both suites re-verified green (21/21 and 24/24) against a fresh stack.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 13:00:17 -05:00
ced4568andClaude Sonnet 5 15b616274b feat(root01): author Computing & Linux Foundations, and the real 01->05 prerequisite chain
Adds all 6 ROOT 01 modules (how computers run software, the shell, filesystem navigation,
files & text, processes, users & permissions foundations), each with a real lesson,
demonstration/guided exercise, knowledge check where warranted, a hands-on Lab, and a
reflection — 35 activities, 29 new content files.

Adds 6 new hands-on Linux scenarios (linux-inspect-hardware, linux-shell-basics,
linux-filesystem-navigation, linux-files-and-text, linux-process-management,
linux-permissions-foundations) plus 2 MCQ scenarios, using the existing generic scenario/
validation engine (docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §1) — every one tested end-to-end via
the real scenario setup/validate API, including negative tests confirming they correctly FAIL
when not actually solved (e.g. the process-management scenario fails while the target process
is still running; the permissions scenario is tested against a real `su - labuser` denial).

Wires rootnode-linux-foundations-assessment (2 required + 4 random-draw scenarios) using the
existing Phase 3B assessment engine.

Adds a real, server-side ROOT 01->02->03->04->05 course prerequisite chain
(courses.prerequisites) — verified live on two students: one who completed ROOT 01 sees ROOT 02
unlock; a fresh student sees ROOT 02 correctly locked while 03/04/05 stay vacuously satisfied
(their own prerequisite courses are still empty). ROOT 06+ and the Capstone are unaffected.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 12:59:52 -05:00
ced4568andClaude Sonnet 5 87526bcf7f feat(labs): add a nested Docker Engine to the Lab image for ROOT 04
Installs docker.io + a static docker-compose v2 binary (same "curl a static binary" pattern
already used for k3s) inside the same already-Privileged Lab container that runs k3s.
scripts/entrypoint.sh starts dockerd (vfs storage driver, to avoid nesting overlay-on-overlay)
non-blockingly right after k3s's own API server is confirmed up — a Docker daemon failure only
logs a warning and never breaks the Kubernetes-only Labs that are the vast majority of usage.

This does not weaken the security boundary: the container was already Privileged (required for
k3s, the existing accepted threat model), so a second runtime inside it grants no new host
capability, and the Lab Host's own real Docker socket is never touched — the nested dockerd
listens on its own /var/run/docker.sock entirely inside the guest's filesystem namespace.

Tested empirically against the real production resource caps (2 vCPU, 4096MB, 512 PIDs,
Privileged: true) before merging: k3s + dockerd + a running nginx:alpine container (with a
published port, curled successfully) + a real docker build all together used 884MB/4096MB and
160/512 PIDs — comfortable headroom in every dimension. See docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md
§3 for the full test log.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 12:59:23 -05:00
ced4568andClaude Sonnet 5 3c815cfeea docs(foundations): define ROOT 01-04 architecture and Lab strategy
ROOT_FOUNDATIONS_ARCHITECTURE.md designs all 26 modules across ROOT 01 (Computing & Linux
Foundations), 02 (Linux Administration & Networking), 03 (Git & Collaborative Development), and
04 (Containers & Docker), using the existing course/module/activity model — outcomes,
prerequisites, activities, and Real Ops connections for each, per docs/COURSE_AUTHORING_STANDARD.md.

ROOT_FOUNDATIONS_LAB_STRATEGY.md documents the central finding this phase is built on: the
existing scenario engine (backend/server.js's runCommand()) executes an arbitrary shell command
string and checks its output — nothing about it is Kubernetes-specific. The existing `lab`
activity type and scenario JSON format already fully support Linux and Git exercises with real,
deterministic validation. No shell_lab/git_lab/container_lab activity type is built or needed.

Also documents the container-runtime decision for ROOT 04, evaluated and tested empirically
(not assumed) against the real Lab image and production resource caps — see the following
commit for the implementation.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 12:59:00 -05:00
ced4568andClaude Sonnet 5 bd0a10fbfb docs(phase6c): pilot readiness standard, pilot curriculum manifest, ROOT 11 scope
Adds the four required Phase 6C documents:

- ROOT_PILOT_READINESS_STANDARD.md — the explicit 8-point sufficiency bar
  used to judge every module this phase, replacing any fixed activity-count
  assumption.
- ROOT11_DEVOPS_PLATFORM_SCOPE.md — evaluates all 7 proposed ROOT 11 modules
  individually (READY TO AUTHOR NOW / PARTIAL-LESSON-ONLY / REQUIRES FUTURE
  LAB ENVIRONMENT), with what current infra supports vs. what GitOps/CI-CD/
  IaC/observability would each require.
- ROOT_PILOT_CURRICULUM.md — the authoritative pilot manifest: instructor-led
  as the first pilot's delivery mode, honest READY/PARTIAL/DEFERRED/NOT IN
  PILOT status per course and module, and what is intentionally excluded
  (ROOT 01-04 never authored, ROOT 08 non-capstone-gating and deferred, ROOT
  11 modules 2-7, three practice-bank modules instructor-led-only this pilot).
- PHASE6C_CURRICULUM_COMPLETION.md — the phase's own record of what was
  upgraded, why, the bug found and fixed, and the pilot readiness conclusion.

Updates ROOT_CURRICULUM_COVERAGE_MATRIX.md with the verified before/after
module counts (21->22 modules, 6->16 READY) and per-module upgrade rationale,
and ROOT_CAPSTONE_READINESS.md with a re-review confirming the unlock gate's
6 stages, and ROOT 08/09's non-gating status, are unchanged by this phase's
curriculum expansion.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 11:21:09 -05:00
ced4568andClaude Sonnet 5 01d3764138 feat(frontend): visibility note for standalone assessment-type activities
CourseView.jsx already special-cased 'capstone' activities with a pointer to
the Capstone tab (since only capstone-type activities existed before this
phase). Phase 6C's new rootnode-security-assessment introduces the first
non-capstone 'assessment'-type module activity — without a similar note, a
student would see it in the course tree with no way to discover it needs to
be started from the Assessments tab. Adds the same pattern for 'assessment'
type, naming the assessment by its own title.

No other frontend change was needed: CurriculumView.jsx (instructor view)
already renders activity_type labels, scenario_id/assessment_definition_id,
is_required, instructor_notes availability, and READY/PARTIAL/PLANNED status
pills generically — Phase 6C's new module/activity shapes render correctly
under Phase 6B's existing generic logic, verified by inspection.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 11:20:51 -05:00
ced4568andClaude Sonnet 5 903b0ead43 test(phase6c): acceptance suite, real-k3s rehearsal, and 6A/6B staleness fixes
Adds platform/test/e2e-phase6c.js (17 checks): upgraded-module activity
chains, documented-exception promotions, optional-lab non-blocking behavior,
no-fake-completion-from-viewing, idempotent completion, instructor-notes
exclusion and RBAC on new content, security-assessment pool/draw/cross-student
isolation/completion, the capstone gate's documented indifference to ROOT 09,
ROOT 11's honest partial/lesson-only rendering, and version-bump content
stability — plus running validate-course-content.js as part of the suite.
All 17 pass against both a warm and a freshly-reset dev stack.

Adds infra/scripts/drills/{run,test}-phase6c-security-config-rehearsal —
Section 22's real-k3s dress rehearsal: login -> program -> course -> lesson ->
demonstration -> knowledge-check -> guided-exercise -> real Lab -> real
kubectl (create Secret, apply Pod, wait for Ready) -> validation -> progress
persistence across logout/login -> instructor visibility of the resulting
learning state, walking root-07-m02-configuration-and-secrets (upgraded from
PARTIAL this phase). 10/10 pass against a real k3s Lab, no mocking.

Fixes two stale hardcoded assumptions in e2e-phase6a.js and e2e-phase6b.js
that Phase 6C's own legitimate content changes broke: the gold module
(root-10-m01) now has 7 activities (6 required + 1 new optional lab, not 6),
and root-05-m01 now has 2 required manual-marker activities the course-
completion path must also complete (its new bookending lesson/reflection).
Both suites are back to green (21/21 and 24/24) against a fresh stack.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 11:20:37 -05:00
ced4568andClaude Sonnet 5 7f6e1857c0 feat(curriculum): Phase 6C module upgrades, security assessment bank, ROOT 11 orientation
Upgrades six PARTIAL modules to the gold-module standard (lesson/demonstration/
knowledge-check/guided-exercise/lab(s)/reflection), preserving every existing lab
activity_id and scenario_id: root-06-m03 (jobs/cronjobs/daemonsets), root-07-m04
(ingress/networkpolicy), root-07-m02 (configuration/secrets, converted in place
from a bare 5-lab bank), root-09-m02 (serviceaccounts/rbac), root-09-m03
(network/supply-chain security), root-10-m03 (troubleshooting framework).

root-05-m01 (20-task kubectl-fluency bank) gets a minimal bookending lesson +
reflection rather than a full redesign, since each task already carries its own
explanation. root-12-m01, root-capstone-m01, and root-09-m04 are promoted to
READY via documented exceptions (no new activities) where adding one would
misrepresent their intentional design.

Wires a new rootnode-security-assessment (exam, 60min, hint_policy disabled)
using the existing Phase 3B assessment engine: 3 required hands-on CORE
scenarios + 4 random-draw pools covering SecurityContext, RBAC, network
policy, and the 7 previously-unwired CKS MCQs. Wires the previously-unwired
readiness-probe-task scenario as an OPTIONAL practice lab in root-10-m01,
using the existing is_required model.

Adds root-11-m01-devops-platform-thinking — an honest, lesson-only ROOT 11
orientation module naming DevOps/platform engineering practices already
exercised in ROOT 05-10/12, explicit that GitOps/CI-CD/IaC/observability need
lab infrastructure this platform doesn't yet provide. No fake labs.

Adds infra/scripts/validate-course-content.js: cross-checks the DB against
scenario/content files for duplicate ids, orphans, invalid references, cycles,
sequence collisions, instructor-notes leakage, and (new) self-paced content
deferring explanation to a live instructor. 0 errors against the resulting
22 courses/modules, 163 activities, 98 scenario files.

No application code changed — every gap was satisfiable with existing tables
(course_modules, module_activities, assessment_definitions) and the existing
content-delivery pipeline.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 11:20:17 -05:00
ced4568andClaude Sonnet 5 2db136875f docs(phase6b): document curriculum expansion
PHASE6B_ROOT_CURRICULUM_EXPANSION.md — the phase summary: what was
authored per course, the is_required extension, the capstone unlock
fix, verified scenario coverage before/after (44 -> 88 of 98, 89.8%,
by direct query not estimate), and honest known limitations/Phase 6C
recommendations.

ROOT_CAPSTONE_READINESS.md — the readiness module and the server-side
unlock fix in full detail, including why the module's completion and
the real assessment's unlock gate are deliberately independent systems.

Updated ROOT_CURRICULUM_INVENTORY.md (Phase 6B coverage numbers),
ROOT_PROGRAM_ARCHITECTURE.md (ROOT 08/09 PARTIAL, new module lists per
course), COURSE_CONTENT_MODEL.md (is_required completion rule, a new
content-versioning section per this phase's own requirement),
COURSE_AUTHORING_STANDARD.md (classifying near-duplicate/advanced
scenarios), ENGINEERING_SOP.md (new Phase 6B section),
REMOTE_AND_SELF_PACED_ARCHITECTURE.md (cross-reference), and
platform/README.md (e2e-phase6b.js, the Phase 6B rehearsal, isolation-
per-suite regression guidance).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:09:47 -05:00
ced4568andClaude Sonnet 5 505783e5f7 test(phase6b): add ROOT curriculum acceptance coverage + real-Lab rehearsal
e2e-phase6b.js (24 checks): hierarchy for the 13 new modules across
ROOT 05-10/12/Capstone, prerequisite chains through the newly-inserted
front-door modules, lesson/knowledge-check completion in brand-new
modules, a newly-wired scenario (job-task) completing correctly, the
optional-vs-required (is_required) completion rule, persistence/
isolation/RBAC/draft-hiding proven against the new content, and the
capstone server-side unlock fix in both directions (a direct bypass
attempt rejected with blocking_stage_ids, an ordinary exam unaffected,
a legitimately unlocked attempt still accepted and its completion
reflected in the course content model).

test-phase6b-root-course-rehearsal.js + wrapper (11 checks, real k3s):
one continuous student narrative through a brand-new module (ROOT 07
Services & Service Discovery, not the Phase 6A gold module) — real Lab
launch, the Lab's own setup_commands, a real 'kubectl expose' against
the live cluster, real validate, logout/login/Resume Learning, Lab
destroy/recreate with academic progress intact — plus an instructor
narrative proving Curriculum view, Prepare Labs, and classroom/learning
tooling all still work against the expanded curriculum.

e2e-phase6a.js updated for two of this phase's own intentional,
documented behavior changes (not defects): resume now points at
root-05's new mental-model module instead of the old bare-lab bank, and
root-10's gold module now unlocks the new troubleshooting-framework
module rather than the bare-lab bank directly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:09:20 -05:00
ced4568andClaude Sonnet 5 e0f954e714 feat(instructor): expand curriculum teaching visibility
CurriculumView.jsx's per-module activity table gains a Required column
(required/optional) so an instructor can see at a glance which of
Phase 6B's new CORE vs. GUIDED/INDEPENDENT/ADVANCED activities a
student needs to complete a module. Reuses the existing Classroom/
Prepare Labs tooling unchanged — no second classroom system.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:08:54 -05:00
ced4568andClaude Sonnet 5 54b35e8ad7 feat(student): polish structured ROOT learning flow
CourseView.jsx now shows an '(optional)' label on any activity marked
is_required=false, both in the module activity list and the activity
detail header — the only genuine UX need Phase 6B's new optional/
advanced practice content exposed. No other changes to the existing
Course/Module/Activity navigation, Launch Lab flow, or Practice tab.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:08:28 -05:00
ced4568andClaude Sonnet 5 de6c5b3286 feat(root): build Real Ops and capstone readiness content
root-12-real-ops gains a Detect -> Triage -> Diagnose -> Mitigate ->
Recover -> Verify -> Learn incident-response framing lesson at the
front of the existing 4-incident module, and a post-incident reflection
at the end — the incidents themselves are unchanged, still deliberately
un-guided ('you are responsible for this environment,' not 'complete
this exercise'). No fixes revealed; instructor notes carry the deeper
solution guidance the student-facing lesson intentionally withholds.

root-capstone-m02-capstone-readiness — a new READY orientation module:
what the Capstone measures, how the timed/randomized draw works, time
management and troubleshooting strategy under a clock, and a genuine
self-assessment checklist. Never publishes the current draw or answers.
See docs/ROOT_CAPSTONE_READINESS.md for why this module's completion is
deliberately independent of the real capstone's server-side unlock gate.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:08:01 -05:00
ced4568andClaude Sonnet 5 831486d664 feat(root): expand troubleshooting and observability curriculum
The Phase 6A gold-standard Health Probes module is preserved byte-for-
byte unchanged — it's the template, not a page to rewrite for
consistency.

root-10-m03-troubleshooting-framework — a new PARTIAL module
introducing a repeatable Observe -> Gather Evidence -> Form Hypothesis
-> Test -> Repair -> Validate -> Prevent Recurrence framework, applied
to a CrashLoopBackOff and a broken Service selector. Lesson,
demonstration (the full framework walked through a real
CrashLoopBackOff, command by command), knowledge check, and real Labs
(crashloop-fix, endpoint-fix-task). Sequenced between the gold module
and the existing bare-lab bank, which now sits behind it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:07:34 -05:00
ced4568andClaude Sonnet 5 960a638ec9 feat(root): add Kubernetes security curriculum
Four new modules, moving ROOT 09 off PLANNED and building the
22-scenario CKS-style bank into a coherent course instead of
disconnected exam tasks — see docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md
for the CORE/GUIDED/INDEPENDENT/ADVANCED/ASSESSMENT-BANK classification
behind every placement.

root-09-m01-security-model-and-securitycontext — READY, full
gold-standard pattern: defense in depth, least privilege, hardening a
Pod with SecurityContext. Lesson, demonstration (a Pod before/after
hardening), knowledge check, guided exercise (a hardening checklist),
real Labs (CORE: non-root/drop-capabilities/read-only-fs/
pod-security-context; GUIDED: seccomp; ADVANCED: AppArmor; INDEPENDENT:
the generic pod-security-context duplicate), and a reflection.

root-09-m02-serviceaccounts-and-rbac — PARTIAL: workload identity and
least-privilege permissions. Lesson + knowledge check + real Labs.

root-09-m03-network-and-supplychain-security — PARTIAL: NetworkPolicy
as a security control (default-deny), image/registry trust, TLS
ingress. Lesson + knowledge check + real Labs.

root-09-m04-cluster-hardening-and-runtime — PARTIAL, deliberately
survey-depth: Pod Security Admission and a lighter treatment of
audit-policy/runtime-sandboxing topics that don't belong in the core
daily-driver path.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:07:07 -05:00
ced4568andClaude Sonnet 5 73cea1ba1d feat(root): add storage and scheduling curriculum
Two new PARTIAL modules, moving ROOT 08 off PLANNED for the first time:

root-08-m01-storage-fundamentals — ephemeral vs. persistent storage,
PV/PVC/StorageClass. Lesson + knowledge check + real Labs (emptydir-pod,
pv-pvc-mount, pvc-dynamic-task, storageclass-mcq).

root-08-m02-scheduling-resource-governance — how the scheduler places
Pods and how quotas/limits/priority/disruption budgets shape that
placement. Lesson + knowledge check + real Labs (node-label-selector,
node-taint-toleration, pod-affinity-mcq, priority-class-task,
pod-disruption-budget-task, resource-quota-ns, limitrange-task).

cluster-upgrade-mcq and etcd-backup-mcq deliberately left unwired —
ROOT 11/DevOps territory, out of this phase's scope, not overlooked.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:06:43 -05:00
ced4568andClaude Sonnet 5 dbcc210889 feat(root): author networking and configuration curriculum
root-07-m03-services-and-service-discovery — a new, full gold-standard-
pattern module: the Service/Endpoints/DNS request path, choosing the
right Service type. Lesson, demonstration (a Service with zero
Endpoints — the single fastest diagnostic check for this failure
class), knowledge check, guided exercise (ClusterIP vs. NodePort vs.
LoadBalancer for three real components), a real Lab
(expose-service-basics), and a reflection.

root-07-m04-ingress-networkpolicy-foundations — a new PARTIAL module:
Ingress and NetworkPolicy at the architectural level (the security-
control framing of NetworkPolicy is deliberately deferred to ROOT 09).
Lesson + knowledge check + real Labs (ingress-task, network-policy).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:06:19 -05:00
ced4568andClaude Sonnet 5 86ea035e9b feat(root): author workloads curriculum
root-06-m02-deployments-scaling-updates — a new, full gold-standard-
pattern module: why Deployments/ReplicaSets exist, how a rolling update
actually progresses, and what rollback restores. Lesson, demonstration
(a healthy rollout vs. one stalled on ImagePullBackOff), knowledge
check, guided exercise (planning a safe rollout strategy), real Labs
(rolling-update-task, deployment-rollback), and a reflection.

root-06-m03-jobs-cronjobs-daemonsets — a new PARTIAL module: Jobs,
CronJobs, and DaemonSets as distinct workload shapes from a
Deployment's always-on model. Lesson + knowledge check + real Labs
(job-task, cronjob-task).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:06:00 -05:00
ced4568andClaude Sonnet 5 25409d3bd7 feat(root): author Kubernetes foundations curriculum
root-05-m00-kubernetes-mental-model — a new, full gold-standard-pattern
front-door module for ROOT 05: what Kubernetes solves, the desired-state
model, the control-plane/worker split, and Pods/Namespaces/labels
through kubectl. Lesson, demonstration (watching a Pod get replaced
after manual deletion), knowledge check, guided exercise (planning a
Namespace layout), a real Lab (namespaces-basics), and a reflection —
sequenced ahead of the existing bare kubectl-fluency lab bank.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:05:40 -05:00
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
ced4568andClaude Sonnet 5 bee57d89fd feat(root): wire ROOT 05-10/12/Capstone curriculum expansion
023_phase6b_curriculum.sql — additive on top of 022_course_content.sql:
13 new teaching modules (ROOT 08/09 move from PLANNED to PARTIAL; ROOT
05-07/10 gain a new front-door module sequenced ahead of the existing
bare-lab bank; ROOT 12 gains an incident-response framing lesson +
reflection; Capstone gains a readiness module). No existing row is
deleted; a small number of existing rows have their sequence/
prerequisites updated so a new teaching module supersedes the bare-lab
bank as the front door — never their scenario_id/activity_type/
completion_behavior, so no student's derived completion changes meaning.

is_required (new module_activities column, defaults true — every Phase
6A activity unaffected) lets a module distinguish CORE teaching from
GUIDED/INDEPENDENT-practice duplicates and ADVANCED/optional extras.
course-progress.js's totals() now considers a module complete once
every REQUIRED activity is done, not literally every activity — an
incomplete optional one no longer blocks completion, though it still
counts toward the displayed percentage.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:04:55 -05:00
ced4568andClaude Sonnet 5 82f17a05e8 docs(curriculum): map Phase 6B ROOT coverage
Course -> module -> objective -> lesson/demo/knowledge-check/guided-
exercise -> scenario(s) -> Real Ops connection -> status, for ROOT 05-10,
12, and Capstone, grounded in the Phase 6A inventory. Every scenario
placement has an intentional reason, not a coverage-percentage target —
includes an explicit 'duplicate concepts identified' section for the
near-duplicate scenario pairs (pod-security-context vs.
cks-pod-security-context, rbac-clusterrole vs. cks-rbac-clusterrole,
network-policy's architectural vs. security-control framing) and which
scenarios are deliberately left out of structured curriculum entirely.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:04:30 -05:00
ced4568andClaude Sonnet 5 fd38d06055 docs(phase6a): document RootNode course foundation
COURSE_AUTHORING_STANDARD.md — the standard every future lesson/module
must follow (target learner, prerequisites, measurable objectives,
concept-before-lab, instructor notes, terminology, versioning, review
requirements), grounded in the gold-standard module as the reference
implementation.

PHASE6A_ROOT_COURSE_FOUNDATION.md — the phase summary: youth-ladder
context, the Google Workspace status already recorded in Phase 5C's own
docs, architecture/build summary, honest content states, test results,
explicit non-goals, and known limitations/Phase 6B recommendations.

Updated ENGINEERING_SOP.md (new Phase 6A section + a curriculum-content
cross-reference), REMOTE_AND_SELF_PACED_ARCHITECTURE.md (a 7th
closed gap: self-paced enrollment now resolves to a real course tree,
not just a scenario list), LAB_ARCHITECTURE.md (Phase 6A reuses the
disposable Lab unmodified, verified by a real rehearsal), and
platform/README.md (course model in the api/ summary, new e2e suite
descriptions for 5A/5B/5C/6A which were missing entirely, and the
real-Lab rehearsal script).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:30:11 -05:00
ced4568andClaude Sonnet 5 8ffc923a84 test(phase6a): add course-delivery acceptance coverage + real-Lab rehearsal
e2e-phase6a.js (21 checks): hierarchy/ordering, honest READY/PARTIAL/
PLANNED states, all three completion behaviors (manual marker/
scenario-derived/assessment-derived), prerequisite locking (including
the zero-content-is-vacuously-satisfied rule), resume, persistence
across logout/login, cross-student isolation, RBAC for self-paced
no-cohort students, draft-content hiding, and content-version
stability.

test-phase6a-course-rehearsal.js + run-phase6a-course-rehearsal.sh: a
real-k3s dress rehearsal of the gold-standard module end to end — a
student reads the real lesson content, launches a real disposable Lab,
the Lab's own POST /api/scenarios/:id/setup (called through the
Gateway's existing /lab/:token/* proxy exactly as the browser UI does)
stands up the broken api-gateway deployment, a real kubectl patch fixes
the readiness probe, and the module completes and unlocks the next one.
No mocked KubeKosh anywhere in this suite.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:30:00 -05:00
ced4568andClaude Sonnet 5 db927b8081 feat(instructor): add curriculum teaching view
CurriculumView.jsx — a new 'Curriculum' tab in the admin portal:
program structure (courses -> modules -> activities), mapped scenarios/
assessments, estimated teaching time, and instructor notes rendered per
activity. A student_id lookup shows one student's real per-activity
progress alongside the same structure, for spotting where someone is
stuck. Read-only, no WYSIWYG editor — course/module authoring stays
Git/migration-based this phase per docs/COURSE_CONTENT_MODEL.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:50 -05:00
ced4568andClaude Sonnet 5 8c09ef8f3e feat(student): add structured course learning experience
CourseView.jsx — My Programs -> ROOT_ -> Course -> Module -> Activity ->
Launch Lab -> back -> continue, nested inside the existing self-paced
ProgramsTab rather than a second portal. Renders lesson/demonstration/
knowledge-check/guided-exercise/reflection content via the existing
react-markdown+remark-gfm stack (already used by ScenarioPanel.jsx — no
new dependency). Locked modules/courses are inert, not hidden — the
server is still the actual gate (course-routes.js).

ProgramsTab.jsx gains a 'View Courses' button alongside the existing
'Continue Learning' button — the original direct-to-Lab flow is
byte-for-byte unchanged; both paths call the identical launch sequence
underneath.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:41 -05:00
ced4568andClaude Sonnet 5 28c925069b feat(root): author the health-probes gold-standard module
content/root/root-10-kubernetes-troubleshooting-observability/m01-health-probes/:
lesson (probe concepts) -> demonstration (reading kubectl describe pod
Events for a probe failure) -> knowledge check -> guided exercise
(write a readiness probe on paper) -> Launch Lab intro
(rootnode-probe-port-mismatch) -> reflection (root cause + Real Ops
prevention takeaway). 5 of 6 activities carry instructor notes (demo
plan, misconceptions, timing, discussion questions, extensions).

This is the reference implementation docs/COURSE_AUTHORING_STANDARD.md
documents for all future RootNode lesson authoring.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:32 -05:00
ced4568andClaude Sonnet 5 e2298e95de feat(root): wire course/module/activity routes into the Platform API
course-routes.js — student-facing: the full course tree through an
enrollment (GET /api/student/programs/:id/courses) and the lesson
completion marker (POST /api/student/activities/:id/complete, refused
for anything but a manual_marker activity — a lab/assessment can never
be faked complete through this route).

admin-course-routes.js — instructor-facing, read-only (no WYSIWYG CMS
this phase): program structure with instructor notes
(GET /api/admin/programs/:id/courses) and per-student progress
(GET /api/admin/students/:id/courses), scoped via
requireStudentRelationshipAccess — the same self-paced-no-cohort rule
Phase 5C's notification routes already established, not
requireStudentCohortAccess (which would 403 a plain instructor out of
every self-paced student).

Dockerfile now also COPYs content/ alongside the existing scenarios/ —
same 'read-only, never executed' reasoning as that line.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:24 -05:00
ced4568andClaude Sonnet 5 fe702754cd feat(content): add course/module/activity content model
New tables (courses, course_modules, module_activities,
activity_completions) layered over the existing scenario/assessment/
achievement systems — a lab activity references an existing scenario_id,
an assessment/capstone activity references an existing
assessment_definition_id, neither duplicated. Long-form lesson prose
lives in git-tracked Markdown (content/root/), split into shared vs.
instructor-only sections by a '## Instructor Notes' heading convention
(content.js) rather than a second file or a CMS.

course-progress.js derives completion from EXISTING sources only:
auto_scenario reads the progress table, auto_assessment reads
achievements by assessment_definition_id, manual_marker is the one
genuinely new piece of state (for lesson-only activities with nothing
else to derive completion from). Prerequisite locking treats a
zero-content (PLANNED) prerequisite as vacuously satisfied, so an
honestly-unbuilt course never permanently locks everything after it.

Found by testing: computing a level's prerequisite lock and stripping
its working totals in the same map iteration made the result depend on
iteration order. Fixed with two explicit passes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:13 -05:00
ced4568andClaude Sonnet 5 f152b9a489 docs(curriculum): define the ROOT_ program architecture
13 courses (ROOT 01-12 + Capstone) under the existing single
root-kubernetes-foundations program — root_ is one continuous ladder a
student enrolls in once, not thirteen separate purchases. Honest
READY/PARTIAL/PLANNED states grounded in the actual inventory: no course
reaches READY yet, 5 are fully PLANNED (no Linux/Git/Docker/DevOps
scenario content exists anywhere in this repo), 7 are PARTIAL (real
labs, no lessons yet). Documents the prerequisite graph and why it
deviates from a strictly linear 01->12 chain where the existing content
doesn't actually support one.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:03 -05:00
ced4568andClaude Sonnet 5 e4cbcf6c40 docs(curriculum): inventory existing RootNode learning assets
Full inventory of scenarios/data/ (98 scenarios), scenarios/bundles/,
rootnode-path.json's stage structure, assessment_definitions, and the
programs/achievements model — gathered by inspection before any Phase 6A
design work, nothing modified to produce this.

Only 3 scenarios are RootNode-authored; the other 95 (including all 44
wired into rootnode-path.json) are generic upstream Kubernetes/CKS
content. 54 scenarios exist with no rootnode-path.json stage at all —
a real, working content bank for future ROOT 07/08/09/10 modules,
inventoried and grouped by category rather than left undiscovered.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:28:55 -05:00
ced4568 4c0a950afb fix: retry Google's transient domain-wide-delegation propagation-delay error
Release to Docker Hub / Build & Push Docker Image (push) Canceled after 0s
Release to Docker Hub / Create GitHub Release (push) Canceled after 0s
Live Google Workspace delivery validation surfaced a real, self-resolving
Google-side condition: freshly-authorized domain-wide delegation can take
up to about a minute to propagate across Google's auth infrastructure,
producing intermittent unauthorized_client on the OAuth token exchange
even with correct configuration. gws-provider.js now classifies
unauthorized_client as a temporary (retryable) error instead of permanent,
so the existing bounded-backoff worker absorbs it automatically.

Also documents the alias-vs-delegated-user env passthrough in
docker-compose.dev.yml, and records both real findings (this one and the
GOOGLE_DELEGATED_USER alias distinction) plus the full live-delivery
validation outcome in the runbook and Phase 5C doc.

Validated live: isolated gws-email-test.js send (DKIM/SPF passing,
delivered) and a full real lifecycle test (create student, enrollment,
real activation email, activate, login, see program, real password-reset
email, reset, login with new password) via [email protected].
2026-08-25 21:36:12 -05:00
ced4568 7baf7074e5 fix(email): support a delegated-mailbox alias distinct from the delegated Workspace account
Real-world case found during setup: the sending address ([email protected]) is an
alias of an existing Workspace user, not its own paid seat (a deliberate cost-saving choice).
Domain-wide delegation authorizes a real Workspace ACCOUNT, not an arbitrary alias string, so
using the alias directly as the delegated 'sub' claim would have been wrong. Added
GOOGLE_DELEGATED_USER (the account that actually owns the alias and was granted delegation) as
an optional override, falling back to EMAIL_FROM_ADDRESS unchanged when the sending address is
itself a primary account. EMAIL_FROM_ADDRESS remains the visible From: header in both cases.
Documented in docs/RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md with a concrete example.
2026-08-25 17:38:59 -05:00
ced4568 777efd168d docs(phase5c): enrollment communications documentation + Google Workspace runbook
- PHASE5C_ENROLLMENT_COMMUNICATIONS.md: the full design doc.
- RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md: every REQUIRES GOOGLE WORKSPACE ADMIN ACTION step
  (mailbox, service account, domain-wide delegation scoped to gmail.send only, SPF/DKIM/DMARC),
  environment variables, testing procedure, production secret handling, rotation, revocation,
  troubleshooting, quotas.
- RUNBOOK_STUDENT_ACTIVATION.md: day-to-day operation of the pipeline from /admin.
- WEBSITE_ENROLLMENT_INTEGRATION.md: explicit website-vs-RootNode responsibility boundary,
  the new contact_email field, and closes the 'RootNode doesn't send activation email yet'
  known limitation this doc previously documented.
- RUNBOOK_SELF_PACED_ENROLLMENT.md, RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md: point at the new
  automatic-email behavior and the Resend activation control.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md, ENGINEERING_SOP.md: close the enrollment-
  communication gap; new ENGINEERING_SOP §16.
- PRODUCTION_VALIDATION_CHECKLIST.md, README.md, platform/README.md: cross-references and a
  Phase 5C production-validation section.
2026-08-25 14:45:18 -05:00
ced4568 b38c46aaec test(phase5c): permanent regression suite + enrollment/activation dress rehearsal
- platform/test/e2e-phase5c.js (29 checks): activation-vs-confirmation routing,
  duplicate/concurrent webhook idempotency, temporary/permanent/exhausted retry
  classification, resend/regenerate token invalidation, activation/reset single-use and
  expiration, HTML-injection regression, production token-secrecy gating via a real container
  recreate with COOKIE_SECURE=1, minor/contact-email resolution, RBAC scoping, metrics/audit
  hygiene, and a real Platform API restart.
- infra/scripts/drills/test-phase5c-enrollment-activation.js (10 checks): one continuous
  story — website enrollment through a real rendered/extracted activation email, a real Lab
  launch and scenario completion, a second enrollment's confirmation email, a full
  password-reset round trip, and the minor/no-email contact-email path.
- infra/scripts/gws-email-test.js: the opt-in (RUN_GWS_EMAIL_TEST=true) live Google Workspace
  delivery test — does nothing without the explicit flag, sends exactly one message to one
  operator-configured recipient when enabled.
2026-08-25 14:44:25 -05:00
ced4568 be92be81cd feat(observability): notification delivery metrics, no PII/high-cardinality labels
- rootnode_notifications_total{notification_type,status} (computed fresh from the DB, like
  the existing enrollment_breakdown gauge — never drifts from what's actually stored),
  rootnode_notification_queue_depth, rootnode_notification_oldest_queued_seconds,
  rootnode_notification_delivery_attempts_total{provider,result} (a real in-process counter —
  per-attempt outcomes aren't reconstructable from the DB after the fact),
  rootnode_student_activations_{success,invalid}_total. Every label is a bounded enum —
  never student_id/email/token/notification_id/external_event_id.
2026-08-25 14:43:48 -05:00
ced4568 f25535027c feat(admin): notification operations — resend activation, delivery status, RBAC-scoped views
- notification-routes.js: global list (owner/admin only), per-student list (scoped via
  requireStudentRelationshipAccess), resend-activation (mechanically the same as
  regenerate — Phase 5A's token model already guarantees only the latest is ever valid, so one
  action covers both), a dev-only preview route (unconditionally refuses when
  COOKIE_SECURE=1, mirroring student-auth-routes.js's existing dev_reset_token gate).
- NotificationsView.jsx: owner/admin global notification list, paginated, matching
  AuditView.jsx's exact convention.
- StudentsView.jsx: student detail gained a Resend activation control and a Notifications
  history card; Credentials card now explains the automatic-email behavior.
- AdminPortal.jsx: new Notifications nav entry (owner/admin only).
2026-08-25 14:43:10 -05:00
ced4568 3c7f0ca59c feat(activation): wire notifications into enrollment/auth, real password-reset delivery
- integration-routes.js: accepts student.contact_email, calls notifyForEnrollment on a
  genuinely new enrollment only (never on an idempotent replay — inherits Phase 5B's proven
  enrollment-level idempotency, no separate notification dedup needed).
- admin-routes.js: create-student/convert/reset-password now enqueue the corresponding
  notification using the SAME token already minted synchronously (never mints twice, which
  would silently invalidate the token just handed back in the response). Fixed a real bug:
  request-password-reset would mint a reset token for a never-activated student, which makes
  no sense and would confuse them — now falls through to the generic anti-enumeration response.
- admin-programs-routes.js: manual enrollment triggers the same notification decision as the
  website path.
- student-auth-routes.js: /activate records a metric; /change-password and /reset-password
  both queue a password_changed security notice; /request-password-reset now actually delivers
  email (production still never returns the token — unchanged anti-enumeration/isProduction
  gating, just now with a real send behind it).
- rbac.js: requireStudentRelationshipAccess/checkStudentRelationshipAccess — the notification
  visibility rule (cohort-shared or an enrollment they created), reusable beyond just
  enrollments.
- server.js: wires notification-routes.js and starts the notification worker in main().
- docker-compose.dev.yml: PUBLIC_APP_ORIGIN, EMAIL_FROM_NAME, and a parametrized COOKIE_SECURE
  (was hard-commented-out; needed so e2e-phase5c.js can actually prove production gating).
2026-08-25 14:42:34 -05:00
ced4568 9d382c95c5 feat(email): provider-neutral email service — Google Workspace (Gmail API) + dev provider
- email/provider.js: the seam. EMAIL_PROVIDER unset always defaults to the dev provider,
  regardless of any other setting — misconfiguration means email silently doesn't send for
  real, never that a dev/test run accidentally reaches a real student.
- email/gws-provider.js: Gmail API users.messages.send via a service account with Workspace
  domain-wide delegation, impersonating one dedicated mailbox. No human password, no SMTP
  basic auth, no googleapis/google-auth-library dependency — a hand-rolled RS256 JWT-bearer
  OAuth exchange + one REST call (~120 lines), matching this codebase's established
  hand-roll-before-adding-a-dependency convention.
- email/dev-provider.js: never makes a network call, returns the rendered message back to the
  caller for dev/test inspection. Supports magic +forcetempfail/+forcepermfail recipient
  markers so the permanent test suite can deterministically exercise retry/failure
  classification without needing a real flaky provider.
- email/templates.js: activation/enrollment-confirmation/password-reset/password-changed,
  HTML + plain-text, no external images/tracking/JS. Every interpolated value is HTML-escaped
  in the HTML body (a real injection finding — display_name can come straight from a website
  integration event).
- notification-worker.js: a setInterval poller (same pattern as sessions.js's sweeper), claims
  via notifications.js, renders via templates.js, builds activation/reset URLs from
  PUBLIC_APP_ORIGIN, stores a dev-only preview (never in production) so tests can extract the
  real URL without a mailbox.
2026-08-25 14:42:01 -05:00
ced4568 5183a6af09 feat(notifications): durable notification lifecycle + minor-safe recipient resolution
- migrations/021: notification_events (queued/sending/sent/failed/cancelled, bounded
  exponential backoff via attempt_count/next_attempt_at, a classified last_error_code, never a
  raw stack trace or provider credential).
- notifications.js: enqueue (records a no-recipient case as already-failed rather than
  silently dropping it), claimNext (FOR UPDATE SKIP LOCKED — safe if two worker processes ever
  run), markSent/markFailedOrRetry (bounded retry, terminal-state token scrubbing),
  notifyForEnrollment (the shared activation-vs-confirmation decision every enrollment-creation
  path calls), maskEmail/toAdminView (the only shapes any admin API ever returns).
- enrollment.js: enrollmentContactEmail() — reads back the minor/no-email transitional
  contact-email convention (stored in the existing enrollments.metadata JSONB, never written to
  students.email) that createFromIntegrationEvent/createManualEnrollment now accept.
2026-08-25 14:41:29 -05:00
ced4568 1c08a30c7f docs(phase5b): self-paced enrollment documentation + website integration contract
- PHASE5B_SELF_PACED_ENROLLMENT.md: the full design doc.
- RUNBOOK_SELF_PACED_ENROLLMENT.md: operational runbook for /admin -> Programs.
- WEBSITE_ENROLLMENT_INTEGRATION.md: the complete contract for the separate website team —
  endpoint, auth, request/response schema, idempotency/retry rules, student matching, minor/
  no-email behavior, error responses, what not to send, local testing, production-secret
  handling. Written to be usable without reading the Platform API source.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md: closes the self-registration/curriculum-assignment/
  delivery-mode/enrollment-model gaps this doc previously flagged as Phase 5+ scope.
- ENGINEERING_SOP.md: new §15. LAB_ARCHITECTURE.md, PRODUCTION_VALIDATION_CHECKLIST.md,
  README.md, platform/README.md: cross-references and a Phase 5B production-validation section.
2026-08-25 12:10:13 -05:00
ced4568 23f881884d test(phase5b): permanent regression suite + self-paced dress rehearsal
- platform/test/e2e-phase5b.js (32 checks): program seed, entitlement gating
  (pending/future/expired/cancelled), cross-student isolation, real Lab launch/destroy/
  reaper-reconciliation/recreate persistence, server-derived idempotent completion via a full
  real capstone completion, the integration boundary (auth, idempotency, real concurrency,
  unknown/inactive program rejection, minor/no-email path, no card data), RBAC scoping, a
  regression test for the process-crash bug this suite found, Phase 5A smoke, and a real
  Platform API restart.
- infra/scripts/drills/test-phase5b-selfpaced-rehearsal.js (15 checks): one continuous story —
  activation, a simulated website enrollment event, real Lab work, Lab destroy, a new session,
  Continue Learning, a fresh Lab, full real capstone completion, Completed transition, and a
  second uninvolved student proven unable to reach any of it.
2026-08-25 12:09:34 -05:00
ced4568 f89a769540 feat(observability): enrollment/completion metrics, no PII/high-cardinality labels
- metrics.js: rootnode_enrollments_total{status,delivery_mode}, rootnode_selfpaced_active_
  enrollments, rootnode_program_completions_total{program_id}, rootnode_enrollment_events_
  {created,duplicate,rejected}_total. Every label is a bounded enum or an admin-curated slug —
  never student_id/email/enrollment_id/external_reference.
2026-08-25 12:09:17 -05:00
ced4568 4bc202e5f3 feat(integration): provider-neutral website enrollment boundary
- integration-routes.js: POST /api/integrations/enrollments — separate from student/instructor
  session auth entirely (no cookie read), pre-shared-secret header auth matching the existing
  Gateway<->Provisioner trust-boundary pattern. Idempotent via a DB unique constraint on the
  caller's own event_id (not app-level check-then-insert) — proven safe under real concurrent
  duplicate requests. Student resolution is create-or-match by email OR login_identifier
  (minor/no-email support), reusing Phase 5A's exact credential-issuing path. A small,
  provider-neutral status vocabulary (paid/active/confirmed/pending/awaiting_payment) — never
  a raw payment-provider object model.
- docker-compose.dev.yml: INTEGRATION_SHARED_SECRET, optional in dev (fails closed if unset).
2026-08-25 12:09:02 -05:00
ced4568 1b6da0a966 feat(selfpaced): student-facing My Programs dashboard
- ProgramsTab.jsx: Active/Completed programs, real progress, deterministic Continue Learning
  (reuses the same launch/poll pattern MyLearningTab already established), entitlement-blocked
  states shown with a real reason, never a silent dead end.
- StudentDashboard.jsx: new My Programs tab. api.js: programs/program/launchProgram.
2026-08-25 12:08:44 -05:00
ced4568 17160da89e feat(admin): Programs area, enrollment management, student enrolled-programs view
- admin-programs-routes.js: program CRUD (admin/owner authoring, all roles can view — matches
  assessment_definitions' convention), scoped enrollment listing, manual enroll, activate/
  cancel (via rbac.js's requireEnrollmentAccess).
- ProgramsView.jsx: cohort-picker-free program list/create, per-program detail with
  enrollment table and a manual-enroll form.
- StudentsView.jsx: student detail gained an 'Enrolled programs' card.
- AdminPortal.jsx: new Programs nav entry.
2026-08-25 12:08:29 -05:00
ced4568 611e1ac4ba feat(enrollment): enrollment lifecycle, entitlement gating, self-paced student API
- enrollment.js: server-only status transitions (pending->active->completed, cancel),
  deriveEnrollmentAccess (pure, derived-at-read-time like classroom.js's derivePresence —
  'expired' is never stored, computed from access_ends_at), maybeCompleteEnrollment
  (idempotent, server-derived from the existing capstone achievement).
- environment-view.js: publicEnvView/launchEnvironmentForStudent extracted out of server.js's
  original /api/environment/launch route body so the new entitlement-gated self-paced launch
  reuses the identical disposable-Lab sequence instead of a second copy — behavior for the
  existing cohort/pilot route is completely unchanged.
- program-status.js: combines an enrollment with real curriculum/progress state into one view
  (access, percent-complete, current stage, deterministic next-activity/resume target) — shared
  by the student and admin surfaces so both show identical information.
- selfpaced-routes.js: GET /api/student/programs(/:id), POST .../launch — entitlement enforced
  server-side before ever touching the Provisioner, never just hidden in the UI.
- rbac.js: requireEnrollmentAccess — a plain instructor may manage an enrollment they created
  or that belongs to a student sharing one of their cohorts; a self-paced enrollment can have
  no cohort at all, so the existing cohort-only checks can't cover it.
- server.js: found and fixed a real bug — POST /api/progress/record's handler predated the
  wrap() convention, so an unexpected internal error (a malformed environment_id) crashed the
  entire process instead of just 500ing that request. Now wrapped + input-validated.
2026-08-25 12:08:12 -05:00
ced4568 895e16fea4 feat(programs): first-class program model — init_/compile_/root_ ladder, seeded root-kubernetes-foundations
- migrations/019: programs table (slug PK, matching assessment_definitions/cohorts
  convention), family (init/compile/root) and delivery_mode (self_paced/cohort/hybrid) as real
  CHECK-constrained enums, versioned like assessment_definitions so an edit to
  curriculum_path/delivery_mode never silently changes what an existing enrollment means. Seeds
  exactly one program wrapping the existing RootNode/Kubernetes curriculum and capstone — no
  new curriculum content invented.
- programs.js: list/get/create/update, with automatic version bump only when a
  snapshot-relevant field actually changes.
2026-08-25 12:07:34 -05:00
ced4568 896c230142 docs(phase5a): student identity and remote classroom documentation
Release to Docker Hub / Build & Push Docker Image (push) Canceled after 0s
Release to Docker Hub / Create GitHub Release (push) Canceled after 0s
- PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md: the full design doc.
- RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md, RUNBOOK_REMOTE_CLASSROOM.md: operational runbooks.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md: closes the 'no real student authentication' gap this
  doc previously flagged as the one item blocking self-paced delivery.
- ENGINEERING_SOP.md §5 rewritten (was describing a pre-Phase-2A state no longer true of any
  deployed version of this codebase) plus a new §14; roadmap item 3 updated.
- LAB_ARCHITECTURE.md, PRODUCTION_VALIDATION_CHECKLIST.md, README.md, platform/README.md:
  cross-references and a Phase 5A production-validation section.
2026-08-25 01:20:13 -05:00
ced4568 bfa16d9162 test(phase5a): permanent regression suite + dress rehearsal drill
- platform/test/e2e-phase5a.js (21 checks): the two-student RBAC matrix (student can't reach
  any /api/admin/* route, assigned/unassigned/viewer instructor access) and the full password
  security checklist (correct/wrong/unknown-identifier/deactivated/immediate-deactivation/
  rate-limit/rate-limit-survives-a-real-restart/change-password/session-revocation/
  forgot-password anti-enumeration/admin-issued reset/CSRF), plus a classroom lifecycle smoke
  pass. Replaces the throwaway smoke5a/smoke5b scripts used during development.
- infra/scripts/drills/test-phase5a-rehearsal.js (15 checks): 5 pilot students with real
  seeded pre-existing progress/attempt/achievement history, a live classroom, help requests,
  announcements, a REAL Platform API restart mid-rehearsal proving classroom/session/presence
  recovery, then converting two students mid-rehearsal and confirming zero data was orphaned.
- Both run against a live docker-compose.dev.yml stack over real HTTP, not mocks.
2026-08-25 01:20:02 -05:00
ced4568 7557fdc753 feat(admin): student credential management, CSV import UI, remote classroom control center
- StudentsView: credential status in the roster table, a create-student form
  (email/login-ID/cohort), convert/reset-password/revoke-sessions controls per student, a
  bulk CSV import form. Every one-time activation/reset link is surfaced exactly once, never
  re-fetchable.
- ClassroomView: cohort picker -> schedule/list classes -> a live dashboard (participants with
  presence/Lab-status/current-work, help queue with acknowledge/resolve, current-activity
  setter, announcement poster, start/end/cancel, Prepare Labs). Polls the dashboard endpoint
  rather than a second push channel.
- AdminPortal: new Classroom nav entry. CohortsView's existing quick-add-student call updated
  for createStudent's new object-argument signature.
2026-08-25 01:19:54 -05:00
ced4568 5c7e49c399 feat(student): live student experience — real login, classroom, profile
- LoginScreen (credentialed, now the default entry point) with links to legacy pilot entry
  and forgot-password; SetPasswordScreen (shared activate/reset UI, driven by
  ?activate=/?reset= query params — no router library, matching this app's existing
  state-machine-per-shell convention); ForgotPasswordScreen (anti-enumeration, dev-only
  reset-token surface).
- ClassroomTab: LIVE NOW banner, join, heartbeat only while joined and the tab is mounted,
  guidance-only current-activity link, own Lab status/launch (reuses the My Learning tab's
  exact API), announcements, Need Help form. No chat, no video, no shell access.
- ProfileTab: identity, cohort, progress, achievements, active-session count,
  change-password (gated on has_credentials).
- PlatformShell rewritten as an explicit view state machine; StudentDashboard gained the new
  tabs; api.js/Platform.module.css extended to match.
2026-08-25 01:19:44 -05:00
ced4568 ee8ccb2ab6 feat(classroom): remote classroom backend — lifecycle, presence, help queue, dashboard
- classroom.js: create/start/end/cancel (DB-level partial unique index guarantees at most one
  live classroom per cohort; app layer catches 23505 and returns a clean ALREADY_LIVE), join/
  leave/heartbeat, derivePresence (pure, derived from last_seen_at at read time — no stored
  status to go stale), help-request create/acknowledge/resolve, announcements, current-activity
  (guidance only, never redirects a browser or marks work complete), append-only event log.
- admin-classroom-routes.js: instructor-facing classroom API, RBAC via rbac.js's
  requireClassroomAccess/requireClassroomWriteAccess/requireHelpRequestWriteAccess (resolves
  through to the classroom's cohort, same owner/admin/instructor/viewer rules as everywhere
  else cohort-scoped). The dashboard route (GET /api/admin/classroom-sessions/:id) combines
  real Provisioner Lab state and real academic records with classroom.js's own data — no
  second Lab-state or academic-state system.
- classroom-routes.js: student-facing API (live classroom, join/leave/heartbeat, help-request,
  announcements), requireEnrolledClassroom middleware.
- prepare-labs.js: shared prepareLabsForCohort, extracted so the existing cohort-level and new
  classroom-level Prepare Labs routes share one implementation; maps Phase 4.1 admission
  control's CAPACITY_EXCEEDED into a capacity_waiting per-student result.
2026-08-25 01:19:33 -05:00
ced4568 d34ddae458 feat(student): admin student-account management + CSV import, session hardening
- admin-routes.js: create-with-credentials, /convert, /reset-password, /revoke-sessions,
  /sessions, /import (transactional CSV import — whole file validated, one Postgres
  transaction, never accepts a password/hash column from input). Fixed GET
  /api/admin/students to select an explicit safe column list instead of SELECT * — the new
  password_hash/credential_token_hash columns must never be exposed through this route.
- server.js: /api/login now also blocks on credential_token_hash (not just password_hash) —
  closes a real window where a student mid-conversion stayed reachable via the old
  bare-student_id path. studentSession middleware now re-verifies status='active' on every
  request, not just at session creation, matching instructorSession's existing behavior.
- sessions.js: revokeAllStudentSessions, listStudentSessions.
- rbac.js: extracted checkCohortAccess/checkCohortWriteAccess as shared core functions so the
  new classroom/help-request RBAC (next commit) reuses the exact same authorization rule
  instead of a parallel copy.
- student-csv.js + test: hand-rolled parser (no new dependency), unit tested.
- prometheus.yml.j2: activated the platform-api scrape job.
2026-08-25 01:19:19 -05:00
ced4568 2f09b1a498 feat(student-auth): real student credentials — email/login-ID + password, token-based activation and reset
- migrations/017: email, login_identifier, password_hash, last_login_at, credential_token_*
  on students. Fully additive, both nullable/independent, no destructive change.
- students-auth.js: shared credential/token lifecycle (issue/consume/find), reusing
  instructors' exact scrypt password.js verbatim.
- student-auth-routes.js: /api/student/login, /activate, /change-password,
  /request-password-reset (anti-enumeration, dev-only token echo), /reset-password, /profile.
- metrics.js: hand-rolled /metrics Prometheus exporter, login success/failure counters, no
  student/instructor ID ever used as a label value.
- migrations/018: classroom_sessions/participants/events/announcements/help_requests, with a
  partial unique index enforcing at most one live classroom per cohort at the DB level.
2026-08-25 01:18:59 -05:00
421 changed files with 36293 additions and 208 deletions
+19
View File
@@ -42,6 +42,25 @@ RUN set -eux && \
ln -sf /usr/local/bin/k3s /usr/local/bin/kubectl && \ ln -sf /usr/local/bin/k3s /usr/local/bin/kubectl && \
ln -sf /usr/local/bin/k3s /usr/local/bin/crictl ln -sf /usr/local/bin/k3s /usr/local/bin/crictl
# ── Docker Engine (ROOT 04) + DNS tooling (ROOT 02) ──────────────────────────
# A second, independent container runtime nested inside this already-Privileged Lab container
# (see docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §3 for the security/resource evaluation and the
# real test results this decision is based on) — its own dockerd, its own
# /var/run/docker.sock, entirely separate from the Lab Host's real Docker socket, which is
# never mounted into this image. docker-compose v2 isn't bundled by Ubuntu's docker.io package,
# so fetch the static binary the same way k3s's own binary is fetched above, rather than adding
# a whole new apt repository for one plugin.
RUN apt-get update && apt-get install -y docker.io dnsutils && rm -rf /var/lib/apt/lists/*
RUN set -eux && \
case "$(uname -m)" in \
x86_64) COMPOSE_ARCH="x86_64" ;; \
aarch64) COMPOSE_ARCH="aarch64" ;; \
*) echo "Unsupported arch: $(uname -m)" && exit 1 ;; \
esac && \
mkdir -p /usr/local/lib/docker/cli-plugins && \
curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-${COMPOSE_ARCH}" \
-o /usr/local/lib/docker/cli-plugins/docker-compose && \
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
# ── App files ───────────────────────────────────────────────────────────────── # ── App files ─────────────────────────────────────────────────────────────────
WORKDIR /app WORKDIR /app
+29 -7
View File
@@ -150,12 +150,32 @@ its core:
the browser (`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`, session model in the browser (`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`, session model in
`docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` §3, hint gating in `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` §3, hint gating in
`docs/PHASE3B_LEARNING_AND_ASSESSMENT.md` §3). `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md` §3).
- The student flow (roster login → curriculum dashboard → Launch Lab → practice/assessments/ - The student flow (real login (email or instructor-assigned login ID) or legacy pilot roster
capstone → achievements) lives in `frontend/src/platform/`; the instructor/admin portal entry → curriculum dashboard → Launch Lab → practice/assessments/capstone → achievements →
(per-instructor login, cohorts, students, instructor accounts, audit log, classroom live classroom → profile) lives in `frontend/src/platform/`; the instructor/admin portal
"Prepare Labs," environment controls, learning analytics, assessment/capstone management) (per-instructor login, cohorts, students with credential/CSV-import management, a remote
lives in `frontend/src/admin/` at the `/admin` route — both additions alongside the existing classroom control center, instructor accounts, audit log, environment controls, learning
Lab UI, not a rewrite of it. analytics, assessment/capstone management) lives in `frontend/src/admin/` at the `/admin`
route — both additions alongside the existing Lab UI, not a rewrite of it.
- Students authenticate with real credentials as of Phase 5A (email+password or
login-identifier+password, first-login/reset via a single-use token an instructor relays) —
see `docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md`. Instructors run live, remote-capable
classes from `/admin` (presence, Lab/learning status, help-request queue, announcements,
guidance-only current-activity) — RootNode owns identity/coordination, an external service
(Meet/Zoom/Teams/etc.) owns the actual video/voice.
- Students can now be enrolled in a self-paced program (as of Phase 5B) — "My Programs" on the
student dashboard, entitlement enforced server-side (pending/future/expired/cancelled
enrollments cannot launch a Lab no matter what the browser sends), completion server-derived
from the existing capstone rules. `/admin` gained a Programs area (program authoring, manual
enrollment, activate/cancel). A separate, provider-neutral `POST /api/integrations/enrollments`
boundary — server-to-server only, no browser session involved — is what the future public
`rootnodeacademy.com` website/checkout will call to create these enrollments; see
`docs/PHASE5B_SELF_PACED_ENROLLMENT.md` and `docs/WEBSITE_ENROLLMENT_INTEGRATION.md`.
- As of Phase 5C, RootNode actually tells students what's happening — real activation,
enrollment-confirmation, and password-reset email, delivered asynchronously through a
provider-neutral notification worker (Google Workspace via the Gmail API in production, a
deterministic dev provider everywhere else — no test/dev run ever emails a real person). See
`docs/PHASE5C_ENROLLMENT_COMMUNICATIONS.md` and `docs/RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md`.
**Never publish the raw privileged container** — front it with isolation (that's what the **Never publish the raw privileged container** — front it with isolation (that's what the
Gateway is). The one-container `docker run --privileged -p 7554:80 ...` command above stays Gateway is). The one-container `docker run --privileged -p 7554:80 ...` command above stays
@@ -192,7 +212,9 @@ Key properties of the platform (not this image):
Production topology and what's built vs. not yet: `docs/LAB_ARCHITECTURE.md`, Production topology and what's built vs. not yet: `docs/LAB_ARCHITECTURE.md`,
`docs/PHASE1_PLATFORM_FOUNDATION.md`, `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`, `docs/PHASE1_PLATFORM_FOUNDATION.md`, `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`,
`docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md`, `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md`. `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md`, `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md`,
`docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md`, `docs/PHASE5B_SELF_PACED_ENROLLMENT.md`,
`docs/PHASE5C_ENROLLMENT_COMMUNICATIONS.md`.
The production infrastructure package (Proxmox/Ansible/Terraform/firewall/monitoring/backup/CI) The production infrastructure package (Proxmox/Ansible/Terraform/firewall/monitoring/backup/CI)
that runs all of this on real hardware lives in `infra/` — see that runs all of this on real hardware lives in `infra/` — see
@@ -0,0 +1,66 @@
# Welcome to ROOT_
ROOT_ is RootNode Academy's path from "I can use a computer" to "I can run and repair real
infrastructure." It's built for anyone roughly high-school age and up — including college
students, career changers, help-desk technicians, junior sysadmins, and working developers who
want to move into DevOps, cloud, or platform engineering. There's no strict age cutoff: what
matters is whether you're ready for the material, not how old you are.
## Where ROOT_ goes
```
ROOT 01 Computing & Linux Foundations
ROOT 02 Linux Administration & Networking
ROOT 03 Git & Collaborative Development
ROOT 04 Containers & Docker
ROOT 05 Kubernetes Foundations
ROOT 06 Kubernetes Application Workloads
ROOT 07 Kubernetes Networking & Configuration
ROOT 09 Kubernetes Security
ROOT 10 Kubernetes Troubleshooting & Observability
ROOT 12 RootNode Real Ops
CAPSTONE Operate and Recover a Production-Like Environment
```
Each course builds on real skill from the one before it — you won't be asked to use a tool or
concept that a previous course didn't already teach you. By the end, you'll have operated a real
Kubernetes cluster, diagnosed and fixed real incidents, and demonstrated it in a hands-on
capstone — not answered multiple-choice questions about it.
## How you'll actually learn
Every module follows the same rhythm:
**Learn → Practice → Lab → Troubleshoot → Validate**
- **Learn** — a short lesson introduces one real concept, plainly, with no unexplained jargon.
- **Practice** — a demonstration or guided exercise lets you see the concept applied before
you're on your own with it.
- **Lab** — you do the real thing yourself, in your own disposable Lab environment: a real
Linux shell, real Git, real Docker, and (from ROOT 05 onward) a real Kubernetes cluster.
- **Troubleshoot** — Labs are built to sometimes not go as planned on the first try, on purpose.
Figuring out why something didn't work is as much the skill as doing it right the first time.
- **Validate** — your Lab checks its own result. You'll know immediately whether you actually
solved it, not just whether you think you did.
## Self-paced or instructor-led
ROOT_ is built to work either way. If you're going through it on your own, every module gives you
everything you need without a teacher in the room. If you're in an instructor-led class or camp,
your instructor can see your progress, help when you're stuck, and add their own pacing and
discussion on top of the same real curriculum — nothing about the platform changes between the
two, only who else is watching your progress.
## Assessments and the Capstone
A few courses end in a short formal assessment — a handful of real, timed Lab tasks (not a
multiple-choice quiz) that check you can actually do what the course taught, not just recognize
the right answer. The program ends with the **Capstone**: no more new teaching, just a
production-like environment with real incidents to find and fix, the same way you would on the
job.
## Your progress is permanent — your Lab is not
One thing to know before you start: the Lab environment you work in is disposable and can be
recreated at any time. Your academic progress is not. The next module covers exactly what that
means and why it's nothing to worry about.
@@ -0,0 +1,55 @@
# Your Disposable Lab
Every hands-on Lab in ROOT_ gives you a real, isolated environment — a real Linux shell from
ROOT 01 onward, real Git and Docker from ROOT 03-04, and a real Kubernetes cluster from ROOT 05
onward. It's yours alone while you're working in it. Here's what you need to know before your
first one.
## Disposable means disposable
Your Lab environment can be recreated at any time — when you start a new Lab, when an
instructor resets a classroom session, or if the environment needs to be refreshed. When that
happens, **anything that only exists inside the Lab disappears**: files you created in the
terminal, a local Git repository you initialized, a Docker container or image you built, a
Kubernetes object you deployed. None of that is backed up, and none of it needs to be — it was
never the point.
## Your academic progress is not disposable
What IS permanent, stored on RootNode's servers the moment you earn it: which lessons you've
read, which Labs you've completed, your assessment results, and your position in the course
sequence. Recreating your Lab never erases any of that. You can lose a Lab entirely and pick up
exactly where you left off in the course.
## How you know you actually solved it
Every Lab task validates itself — when you think you're done, you run the check and get a real
pass/fail answer immediately, based on the real state of your environment, not on your own
guess. If it fails, that's useful information, not a penalty: it's telling you specifically what
isn't right yet.
## If you get stuck
Most Labs have hints available, revealed one at a time so you can try the next step yourself
before seeing more. Using a hint doesn't cost you anything or affect your result — it's there to
keep you moving, not to be avoided on principle. A formal, timed assessment may disable hints;
if so, the assessment will tell you plainly before you start.
If a Lab's environment itself seems broken rather than the task being hard (a command that
should work doesn't, the environment won't start), use **Reset Scenario & Environment** to tear
down and rebuild it cleanly rather than fighting a broken environment.
## If you're in an instructor-led session
Your instructor can see your progress in real time and gets notified the moment you use **Need
Help** — no need to wait to be noticed. Use it for anything: a Lab that seems broken, a concept
that didn't click, or just wanting a second pair of eyes before you second-guess a working
solution.
## Returning to the course
When you're done with a Lab — pass or still stuck — you can always return to the course view and
come back to it later. Nothing about leaving a Lab mid-task loses your place.
You're ready. Let's start with how a computer actually runs the software you're about to spend
this program controlling.
@@ -0,0 +1,31 @@
# Knowledge Check: Welcome to ROOT_
---
**1. If your Lab environment is recreated, what happens to a file you saved inside it, and
what happens to your completed Labs so far?**
*Answer:* The file disappears — nothing inside the Lab persists across a recreation. Your
completed Labs and course progress are unaffected — those are stored on RootNode's servers, not
in the Lab, and survive regardless of what happens to any Lab environment.
**2. A Lab task fails your validation check. What should that tell you?**
*Answer:* Specifically what isn't correct yet, based on the real current state of your
environment — it's diagnostic information to act on, not a penalty.
**3. Name one thing to try before assuming a Lab is "just broken."**
*Answer:* Check the available hints (revealed one at a time), and if the environment itself
genuinely seems broken rather than the task being hard, use Reset Scenario & Environment to
rebuild it cleanly.
---
## Instructor Notes
**Facilitation:** This is a good moment to actually demonstrate a Lab reset live if you have
time — students remember "I saw it happen and nothing bad occurred" far better than being told
it's safe.
**Approximate timing:** 3 minutes.
@@ -0,0 +1,66 @@
# How Computers Run Software
## Hardware, OS, and application: three different layers
When you run a program, three distinct layers cooperate:
- **Hardware** — the physical CPU, RAM, and storage. It has no idea what a "file" or a "process"
is; it just executes instructions and stores bits.
- **The operating system (kernel)** — the layer that turns raw hardware into something usable:
it decides which program gets the CPU next, hands out memory, and organizes storage into
files and directories. Linux is a kernel plus a large set of standard tools around it.
- **Applications (userspace)** — the programs you actually run. They ask the kernel for
resources (memory, files, network) through a well-defined interface (system calls) instead of
touching hardware directly.
This module gives you just enough of this model to reason about what you're actually looking at
for the rest of ROOT_ — not a full computer architecture course.
## CPU: the thing that actually executes instructions
A CPU has some number of **cores** — each core can run one stream of instructions at a time.
`nproc` tells you how many cores/threads Linux believes it has available. When a system feels
"slow," it's very often because more processes want CPU time than there are cores to give them.
## RAM: fast, temporary, and finite
RAM holds the data and code a running program is actively using. It's fast but **volatile** —
its contents disappear when power is lost, which is exactly why persistent data lives on
storage instead. `free -h` shows you total memory, how much is in use, and how much is free.
## Storage: slower, but persistent
Storage (an SSD/disk) keeps data even when the machine is off. It's organized into a
**filesystem** — the tree of directories and files you already have some intuition for. `df -h`
shows how much space exists and how much is used on each mounted filesystem.
## Processes: a running program
A **process** is a program actually being executed — code plus its own private memory, tracked
by the kernel with a unique ID (a PID). One program can have many processes running at once
(open two terminals running the same shell, and you have two separate processes). ROOT 01
Module 5 goes much deeper into this; for now, just know: everything you run becomes a process
the kernel is responsible for scheduling onto a CPU core.
## Why this matters for everything after this module
Every concept the rest of ROOT_ builds on is a refinement of this same idea. A container is
(mostly) an isolated process with its own filesystem view. A Kubernetes Pod is a group of
containers the scheduler places onto a specific machine's CPU/RAM. None of that will make sense
as anything other than magic if you don't first have this base model of what a computer is
actually managing.
## Instructor Notes
**Lesson goal:** Give students just enough of a mental model to stop treating "the computer" as
an opaque box — without turning this into a full CS/architecture course.
**Approximate timing:** 15 minutes read; 10 minutes live with discussion.
**Common misconception to watch for:** students often conflate RAM and storage ("my computer
has 8GB" meaning disk space). Draw the volatile-vs-persistent distinction explicitly and early.
**Discussion question:** "Why can't a program just talk to the hardware directly instead of
going through the kernel?" (Answer: the kernel is what lets many programs safely share one CPU/
one block of RAM without stepping on each other — this is worth naming explicitly, as it's the
root of why containers and Kubernetes are even possible.)
@@ -0,0 +1,35 @@
# Guided Exercise: Predict Before You Look
Before you run the real inspection commands in the Lab, commit to a guess — this is what makes
the real numbers meaningful instead of just scrolling past them.
## Your task
On paper or in a scratch file, write down your best GUESS for:
1. How many CPU cores does this machine have?
2. How much total RAM, roughly, in GB?
3. How much free disk space is on the root filesystem?
Then think through: which of these three would you expect a "student Lab" environment to be
MOST constrained on, and why? (Hint: think about what happens if 50 students are each running
their own Lab on one shared physical machine at the same time.)
## A worked shape to check yourself against
There's no single right numeric answer here — the point is the habit of predicting first. A
reasonable answer to the "most constrained" question: memory and CPU are typically the tightest
shared resource on a multi-tenant host, since every additional Lab needs its own slice of both,
while disk is comparatively cheap to over-provision.
## Instructor Notes
**Facilitation:** Have students commit to a number verbally or on a shared doc BEFORE anyone
runs the real commands — the value is in noticing the gap between guess and reality, not in
getting it right.
**Approximate timing:** 5 minutes.
**Optional extension:** ask what would happen to their guess if this were a "real" laptop
they personally own vs. a shared cloud server — good primer for later resource-limit
discussions (ROOT 06's resource requests/limits, ROOT 09's resource governance).
@@ -0,0 +1,27 @@
# Reflection: You Just Looked Inside the Box
## Reflection questions
1. How close was your guess to the real numbers? What surprised you most?
2. Name one thing you now understand about "the computer" that you couldn't have explained
before this module.
3. When you eventually run `kubectl describe pod` and see `requests`/`limits` for CPU and
memory, what do you now expect those numbers to actually be constraining?
## Real Ops takeaway
**Every resource limit you'll ever configure — in Docker, in Kubernetes, in a systemd unit — is
a promise about exactly the three things this module named: CPU, memory, and storage.** You
can't reason well about "why did my container get killed" or "why is my Pod stuck Pending"
without this base model. This module isn't computer-science trivia — it's the vocabulary the
rest of ROOT_ assumes you have.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 3 is worth spending real time on — it's
the first moment students connect this abstract lesson to something they'll type as a real
command many courses from now. Naming that connection explicitly here pays off later.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,58 @@
# Linux and the Shell
## What "Linux" actually means
Linux is a **kernel** — the core piece of software from Module 1 that manages CPU, memory, and
storage. What most people call "a Linux system" (Ubuntu, Debian, Fedora, Alpine...) is a
**distribution**: the Linux kernel plus a chosen set of standard tools, package manager, and
configuration conventions layered on top. Different distributions, same kernel underneath —
which is why a command you learn on Ubuntu mostly still works on Alpine.
## Shell vs. terminal
These get used interchangeably but are different things:
- The **terminal** is the window/program that displays text and sends your keystrokes somewhere.
- The **shell** is the program actually interpreting what you type as commands — `bash` is the
shell this Lab uses. The terminal is just the display; the shell is the brain.
## Command structure
Every command follows roughly the same shape:
```
command [options] [arguments]
```
`ls -la /var/log` — `ls` is the command, `-la` are options (flags) that change its behavior,
`/var/log` is the argument (what to act on). Options can usually be combined (`-l -a` = `-la`).
## Exit codes: how a command reports success or failure
Every command returns a number when it finishes: `0` means success, anything else (1-255) means
some kind of failure. This isn't a Linux quirk — it's the exact mechanism scripts, CI pipelines,
and (much later) Kubernetes health checks all rely on to know whether something worked. You can
read the last command's exit code with the special variable `$?`.
## Getting help without leaving the terminal
- **Tab completion** — press Tab while typing a command or path; the shell completes it for you
or shows you the options. This is a genuine time-saver, not just a convenience.
- **Command history** — press the Up arrow to recall previous commands; `history` lists them.
- **`man <command>`** — the manual page for almost any command, with every option explained.
`man ls`, `man grep`, `man chmod` are all real, useful references — reaching for `man` before
a search engine is a habit worth building now.
## Instructor Notes
**Lesson goal:** Establish shell fluency vocabulary (shell vs. terminal, exit codes, options vs.
arguments) that every later module assumes without re-explaining.
**Approximate timing:** 15 minutes read; 10 minutes live with a short live demo of tab
completion and `man`.
**Common misconception to watch for:** students often think "Linux" and "the terminal" are the
same thing — clarify the kernel/distribution/shell/terminal layering explicitly.
**Demo idea:** live-type a command that fails (e.g. `ls /nonexistent`), then run `echo $?`
immediately after to show the exit code in real time — more convincing than describing it.
@@ -0,0 +1,49 @@
# Demonstration: Watching Exit Codes in Real Time
## OBSERVE
Run a command you know will succeed, then immediately check its exit code:
```
$ true
$ echo $?
0
```
## HYPOTHESIS
Now predict: what will the exit code be if the command fails instead?
## INSPECT
Run a command that fails, and check immediately:
```
$ ls /this-does-not-exist
ls: cannot access '/this-does-not-exist': No such file or directory
$ echo $?
2
```
## CHANGE
Try a few other failing commands and note their codes: `grep nomatch /etc/hostname` (exit 1 —
"no matches found," a different kind of failure than "file not found").
## VERIFY
Notice: exit codes aren't just "0 or 1" — different failures often get different non-zero
codes. `0` always means success; the exact non-zero value's MEANING varies by command, but ANY
non-zero value reliably means "something went wrong," which is the part every script actually
relies on (`if command; then ... else ...`).
## Instructor Notes
**Demo goal:** Make the abstract "exit code" concept concrete by actually watching `$?` change
in real time, immediately after each command — this is far more convincing live than described
in prose.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** running `echo $?` after ANY other command in between (even
`ls`) silently checks the WRONG command's exit code — emphasize "check it immediately."
@@ -0,0 +1,37 @@
# Knowledge Check: Linux and the Shell
---
**1. What's the difference between a shell and a terminal?**
*Answer:* The terminal is the window/program that displays text and captures keystrokes; the
shell (e.g. `bash`) is the program actually interpreting what you type as commands. The terminal
is the display, the shell is the brain.
---
**2. A command exits with code `0`. What does that tell you?**
*Answer:* It succeeded. `0` is the universal "success" convention every script and pipeline
relies on; any non-zero value means some kind of failure occurred.
---
**3. You've typed half a long path and can't remember the rest. What should you press?**
*Answer:* Tab — tab completion finishes the path (or shows the options) without you needing to
type or remember it exactly.
---
**4. Where would you look to learn every option a command supports, without leaving the
terminal?**
*Answer:* Its manual page — `man <command>`.
## Instructor Notes
**Facilitation:** Question 1 is the one students most often blur together — worth confirming
verbally before moving on.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,31 @@
# Guided Exercise: Predict the Exit Code Before You Run It
This is a paper exercise — commit to an answer before checking it in the Lab.
## Your task
For each command below, predict whether its exit code will be `0` (success) or non-zero
(failure), and why:
1. `cat /etc/hostname` (a file that exists)
2. `cat /etc/does-not-exist-at-all` (a file that doesn't)
3. `grep root /etc/passwd` (a pattern that's very likely present)
4. `grep zzz-not-a-real-user /etc/passwd` (a pattern that's very likely absent)
5. `mkdir /tmp/already-exists && mkdir /tmp/already-exists` (running the same `mkdir` twice)
## A worked shape to check yourself against
1. `0` — the file exists and is readable.
2. Non-zero — the file doesn't exist, `cat` fails.
3. `0` — `grep` succeeds (exit 0) when it finds at least one match.
4. Non-zero (specifically `1`) — `grep` "fails" when it finds NO matches, even though nothing
went technically wrong. This is a common surprise: `grep`'s exit code means "did I find a
match," not "did an error occur."
5. The SECOND `mkdir` fails (non-zero) — the directory already exists. The first one succeeds.
## Instructor Notes
**Facilitation:** Item 4 is the one worth pausing on — many students assume "no error message"
means "exit code 0," and `grep`'s no-match convention is a good early counter-example.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,25 @@
# Reflection: Exit Codes Are Everywhere After This
## Reflection questions
1. Before this module, did you know commands returned a machine-readable success/failure
signal at all? What did you assume happened instead?
2. Where do you think exit codes might matter later in ROOT_ — in a script, a CI pipeline, or
a container?
## Real Ops takeaway
**Every automated system you'll ever build or operate checks exit codes, not error messages.**
A script deciding whether to continue, a CI pipeline deciding whether to deploy, and (much
later) a container orchestrator deciding whether a process is healthy, all reduce to the exact
same question this module taught you to answer by hand: did this exit `0` or not? Getting
comfortable checking `$?` now means none of those later systems will feel like magic.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 previews Docker/Kubernetes health
checks well before students get there — useful to name explicitly without going deep yet.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,55 @@
# Navigating the Filesystem
## Everything is one tree, starting at `/`
Linux has exactly one filesystem tree, rooted at `/` (pronounced "root," unrelated to your
username). Every file and directory, no matter what physical disk it's actually on, appears
somewhere under this one tree — there's no separate "C:\" and "D:\" the way Windows shows
multiple drives.
## Absolute vs. relative paths
An **absolute path** starts with `/` and always means the same location no matter where you
currently are: `/home/student/notes.txt`. A **relative path** is interpreted relative to your
CURRENT directory: `notes.txt` means something different depending on where you're standing
when you type it. `.` means "this directory," `..` means "one directory up" — `cd ..` moves up
one level, `cd ../..` moves up two.
## Key commands
- `pwd` — print your current directory (where am I?)
- `ls` — list what's in a directory (`ls -la` shows hidden files and details)
- `cd <path>` — change directory (absolute or relative)
## Hidden files
A file or directory starting with `.` (like `.bashrc` or `.config`) is "hidden" — `ls` skips it
by default, `ls -a` shows it. This isn't a security feature, just a convention to keep
configuration/metadata files out of the way of normal directory listings.
## The important top-level directories (conceptually, not memorized)
You don't need to memorize the entire Filesystem Hierarchy Standard, but recognizing these by
purpose will save you real confusion later:
- **`/etc`** — system-wide configuration files (nginx's config lives at `/etc/nginx/nginx.conf`)
- **`/var`** — variable data that changes while the system runs: logs (`/var/log`), among others
- **`/home`** — personal directories for regular users
- **`/tmp`** — temporary files, safe to assume anything here can disappear on reboot
- **`/usr`** — most installed programs and their supporting files live somewhere under here
- **`/proc`** — not real files on disk at all — a live, kernel-generated view into running
processes and system state (you'll use this again in Module 5)
## Instructor Notes
**Lesson goal:** Give students a working mental model of "one tree, absolute vs. relative
paths" plus just enough top-level-directory recognition to not be lost later — not FHS
memorization.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** students coming from Windows sometimes expect separate
drives/roots — emphasize the single unified tree explicitly.
**Demo idea:** `cd /` then `ls`, narrating what each top-level directory is for as you see it,
rather than listing them in the abstract.
@@ -0,0 +1,43 @@
# Demonstration: The Same Word, Two Different Destinations
## OBSERVE
Starting from `/home/student`, run `cd project` — you land at `/home/student/project`.
## HYPOTHESIS
Now, from `/home/student/project`, what do you predict `cd project` will do this time?
## INSPECT
```
$ pwd
/home/student/project
$ cd project
bash: cd: project: No such file or directory
```
It fails — there's no `project` directory INSIDE `project` (unless you happened to create one).
The exact same relative path meant something completely different depending on where you stood.
## CHANGE
From `/home/student/project`, run `cd ../project` instead — this correctly goes back up one
level, then into `project` again, landing you right back where you started.
## VERIFY
`pwd` after each `cd` to confirm exactly where you ended up — this habit (verify your location
before assuming it) prevents a huge share of "why did my command run somewhere unexpected"
confusion later.
## Instructor Notes
**Demo goal:** Make "relative paths depend on where you are" concrete and slightly surprising,
rather than an abstract rule to memorize.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** students assume a relative path always means the SAME thing
once they've used it once — reinforce that it's re-evaluated fresh every time, from wherever
they currently are.
@@ -0,0 +1,36 @@
# Knowledge Check: Navigating the Filesystem
---
**1. What does `..` mean in a path?**
*Answer:* One directory up from your current location.
---
**2. You're at `/home/student/project`. Does `cd project` do the same thing here as it did at
`/home/student`?**
*Answer:* No — a relative path is re-evaluated from your CURRENT location every time. At
`/home/student`, `project` refers to `/home/student/project`; from inside that same directory,
`project` would refer to `/home/student/project/project`, which likely doesn't exist.
---
**3. A file is "hidden" from a normal `ls`. What actually makes it hidden?**
*Answer:* Its name starts with a `.` — it's purely a naming convention `ls` respects by
default, not a real permission or security feature. `ls -a` shows it.
---
**4. Where would you expect to find a running system's log files?**
*Answer:* `/var/log` — `/var` holds data that changes while the system runs.
## Instructor Notes
**Facilitation:** Question 2 is the one worth confirming out loud — it's the core "gotcha" of
relative paths.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,39 @@
# Guided Exercise: Plan a Directory Structure on Paper First
Before you build anything in the Lab, plan it — this is the same discipline you'll use later
when planning a Kubernetes namespace layout or a Git repository structure.
## Your task
You're setting up a small project called `weather-app`. On paper, sketch a directory tree
containing:
- A place for source code
- A place for documentation
- A place for tests
- A single hidden configuration file at the project's root
Write out the exact `mkdir` and file-creation commands you'd use, including whether each path
you write is absolute or relative.
## A worked shape to check yourself against
```
mkdir -p weather-app/src weather-app/docs weather-app/tests
echo "ready" > weather-app/.config
```
(Or with an absolute path: `mkdir -p /srv/weather-app/src ...`.) The `-p` flag matters here —
without it, `mkdir` refuses to create `weather-app/src` if `weather-app` doesn't already exist.
## Instructor Notes
**Facilitation:** Have students commit to their command in writing before revealing the worked
answer — the value is in noticing what `-p` actually buys them, not just copying a correct
command.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
**Common mistake to watch for:** forgetting `mkdir -p` and getting a "No such file or
directory" error when creating a nested path in one command — a good moment to connect back to
Module 2's exit-code lesson (that error IS a non-zero exit code).
@@ -0,0 +1,27 @@
# Reflection: Structure Isn't Bureaucracy, It's Communication
## Reflection questions
1. Why might a real engineering team insist on a consistent directory structure across every
project, instead of everyone organizing files however they like?
2. Which of `/etc`, `/var`, `/home`, `/tmp`, `/usr`, `/proc` do you feel most confident you could
explain to someone else right now? Which one still feels fuzzy?
## Real Ops takeaway
**A predictable filesystem layout is what lets tools (and other engineers) reason about a
system without asking you first.** `/etc` always means configuration, `/var/log` always means
logs — this convention is exactly why a monitoring tool can be pointed at "check `/var/log`" on
any Linux machine and just work. Container images later inherit this same convention (an
nginx container still keeps its config at `/etc/nginx/nginx.conf`) — you're not learning a
one-off Linux fact, you're learning a convention that outlives this specific module.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 connects directly to why Kubernetes
namespaces and Git repository conventions matter later — a good moment to name that connection
without going deep yet.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,52 @@
# Files and Text
## Basic file operations
- `touch <file>` — create an empty file (or update its timestamp if it exists)
- `mkdir <dir>` — create a directory (`-p` creates any missing parent directories too)
- `cp <src> <dst>` — copy a file
- `mv <src> <dst>` — move OR rename a file (Linux treats renaming as "moving to a new name in
the same place")
- `rm <file>` — delete a file (`-r` for a directory, `-f` to skip confirmation — dangerous
combined: `rm -rf` deletes without asking, permanently)
## Reading text
- `cat <file>` — print the whole file at once
- `less <file>` — page through a file interactively (press `q` to quit) — the right tool for
anything too long to read in one screen
- `head <file>` / `tail <file>` — the first/last 10 lines by default (`-n 20` for a different
count); `tail -f` follows a file live as new lines are appended — exactly how you'd watch a
log file update in real time
## Searching text: `grep`
`grep PATTERN file` prints only the lines matching PATTERN. This is one of the most-used
commands in real operations work — "find the lines that matter" beats "read everything."
## Pipes and redirection: composing commands
- **A pipe (`|`)** sends one command's output as the next command's input:
`cat app.log | grep ERROR` — read the file, then filter it.
- **Redirection (`>`)** sends a command's output into a file instead of the screen (overwriting
it): `grep ERROR app.log > errors.txt`. `>>` appends instead of overwriting.
## stdout vs. stderr
A command actually has TWO separate output streams: **stdout** (normal output) and **stderr**
(error messages), even though both print to your screen by default. This is why
`command 2>/dev/null` (redirecting stream `2`, stderr, to nowhere) can suppress error noise
while still showing normal output — you'll use exactly this pattern in later Labs.
## Instructor Notes
**Lesson goal:** Build real fluency composing small commands together — this is the single
highest-leverage Linux skill for troubleshooting work later in ROOT_.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** students often don't realize stdout/stderr are separate
streams until they see `2>/dev/null` change what's suppressed — worth demonstrating live.
**Demo idea:** run a command that produces both real output and an error in the same call
(e.g. `ls /etc /nonexistent`) and show how `2>/dev/null` changes what appears.
@@ -0,0 +1,56 @@
# Demonstration: Filtering a Log Down to What Matters
## OBSERVE
A raw log file has five lines mixing `INFO` and `ERROR`:
```
$ cat app.log
INFO: boot ok
ERROR: disk full
INFO: retry
ERROR: timeout
INFO: done
```
## HYPOTHESIS
Predict what `grep ERROR app.log` will print, before running it.
## INSPECT
```
$ grep ERROR app.log
ERROR: disk full
ERROR: timeout
```
Only the matching lines — everything else is filtered out.
## CHANGE
Now redirect that filtered result into a new file instead of just viewing it:
```
$ grep ERROR app.log > errors.txt
$ cat errors.txt
ERROR: disk full
ERROR: timeout
```
## VERIFY
Count how many lines actually matched, without eyeballing them: `wc -l < errors.txt` (or
`grep -c ERROR app.log` directly). This composition — filter, then count, then act — is the
exact shape of a huge share of real troubleshooting work.
## Instructor Notes
**Demo goal:** Show the filter → redirect → verify chain as one continuous, composable
workflow, not three unrelated commands.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** using `>` when they meant `>>` and accidentally overwriting a
file they wanted to append to — worth calling out explicitly since it's a real, common mistake
with no undo.
@@ -0,0 +1,37 @@
# Knowledge Check: Files and Text
---
**1. What's the difference between `>` and `>>`?**
*Answer:* `>` overwrites the target file's contents entirely; `>>` appends to the end of it,
preserving what was already there.
---
**2. A command prints both normal output and an error message. What are these two streams
called?**
*Answer:* stdout (normal output) and stderr (error messages) — two separate streams that both
happen to print to your screen by default.
---
**3. You want to watch a log file update live, as new lines are appended, without re-running a
command repeatedly.**
*Answer:* `tail -f <file>` — follows the file and prints new lines as they're written.
---
**4. `cat app.log | grep ERROR`. What does the `|` actually do?**
*Answer:* It pipes `cat`'s output directly into `grep`'s input, so `grep` filters the file's
contents without needing a separate temporary file.
## Instructor Notes
**Facilitation:** Question 2 is worth a live show-of-hands check — many students haven't
consciously registered that stdout/stderr are separate until this module.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,31 @@
# Guided Exercise: Which Tool Actually Answers the Question?
This is a paper exercise — commit to an answer before checking it in the Lab.
## Your task
For each situation, name the ONE command (or short pipeline) you'd reach for first:
1. You want to see just the last 20 lines of a 50,000-line log file.
2. You want to know how many lines in a file contain the word "timeout."
3. You want to watch a log file update live while a process is running.
4. You want to see a file's full contents but it's too long for one screen and you want to
scroll through it interactively.
## A worked shape to check yourself against
1. `tail -n 20 file.log`
2. `grep -c timeout file.log`
3. `tail -f file.log`
4. `less file.log`
Notice: `cat` would technically "work" for #4 too, but it dumps everything at once with no way
to scroll back — `less` is the actual right tool once a file is longer than one screen.
## Instructor Notes
**Facilitation:** Item 4 is worth discussing explicitly — "it works" isn't the same bar as
"it's the right tool for this situation," and that distinction matters more as commands compose
into real troubleshooting workflows.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,26 @@
# Reflection: Filtering Is a Reasoning Skill, Not a Syntax Trick
## Reflection questions
1. Before this module, how would you have found "the important lines" in a huge log file?
How does that compare to `grep`?
2. Which combination (pipe, redirect, or a specific command like `tail -f`) do you expect to
use the most once you're doing real troubleshooting work?
## Real Ops takeaway
**"Read everything and eyeball it" does not scale — filtering does.** Every troubleshooting
framework you'll learn later in ROOT_ (starting with ROOT 02's own version, then again more
formally in ROOT 10) depends on being able to quickly narrow a huge amount of system output down
to the handful of lines that actually matter. `grep`, pipes, and redirection are the first real
tools you have for that — not a syntax exercise, a reasoning skill.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 often produces good answers ("I'd
just scroll and read") that make the value of filtering obvious by contrast — let students
articulate this themselves rather than stating it for them.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,64 @@
# Processes
## What a process actually is
A **process** is a running instance of a program — its own code, its own private memory, and a
unique identifier the kernel tracks called a **PID** (Process ID). Run the same program twice
(open two terminals both running `bash`), and you get two separate processes with two different
PIDs, even though they're running identical code.
## Foreground vs. background
A command you run normally occupies your terminal until it finishes (**foreground**) — you
can't type anything else until it's done. Append `&` to run it in the **background** instead,
freeing your terminal immediately while the process keeps running. `sleep 300 &` starts a
300-second sleep in the background and hands your prompt right back.
## Inspecting processes: `ps` and `top`
- `ps aux` — a snapshot of every process currently running, including its PID, how much CPU/
memory it's using, and the exact command that started it.
- `top` (or `htop`, if installed) — the same idea, but live-updating and sorted by resource
usage, useful for "what's actually eating CPU/memory right now."
## Stopping a process: `kill` and signals
`kill <PID>` doesn't instantly destroy a process — it sends it a **signal**, a message the
process can (usually) choose how to respond to. The default signal, `SIGTERM` (15), politely
asks a process to shut down, giving it a chance to clean up first. `kill -9 <PID>` sends
`SIGKILL`, which the kernel enforces immediately with no chance for the process to react —
appropriate only when a process is truly stuck and ignoring `SIGTERM`. `kill -0 <PID>` is a
special case: it sends NO real signal at all, just checks whether the PID still exists —
exactly how you confirm "did that process actually stop" without side effects.
## Process hierarchy
Every process (except the very first one at boot) has a **parent** — the process that started
it. Your shell is the parent of every command you run from it. This matters later: a parent
process's own lifecycle can affect its children (this is part of why a container, which is
often just one process tree, behaves the way it does when its main process exits).
## Why this matters for containers and Kubernetes
A container is, underneath, mostly an isolated process (or small process tree) with its own
view of the filesystem and network — when people say "a container exited," they mean its main
process exited, exactly the same event you'll cause on purpose in this module's Lab. Kubernetes'
entire job, at its core, is deciding which machine a process (inside a container) runs on and
restarting it if it dies unexpectedly — none of that will feel intuitive without this module's
model of what a process actually is.
## Instructor Notes
**Lesson goal:** Build a real process mental model (PID, foreground/background, signals,
parent/child) that ROOT 04 (containers) and ROOT 06 (Kubernetes workloads) both directly build
on without re-explaining.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** students often think `kill` always means "forcefully
destroy" — clarify that plain `kill` sends a polite, ignorable request (SIGTERM), and `-9` is
the forceful escalation, not the default.
**Discussion question:** "What do you think happens to a container if its main process exits on
its own, versus if something outside kills it?" (Don't need a complete answer yet — just plant
the question for later.)
@@ -0,0 +1,53 @@
# Demonstration: Watching a Process Actually Terminate
## OBSERVE
Start a long-running background process and note its PID:
```
$ sleep 300 &
[1] 1421
```
`1421` is the PID (yours will differ every time).
## HYPOTHESIS
Predict: will `kill -0 1421` print anything if the process is still running?
## INSPECT
```
$ kill -0 1421 && echo "still running"
still running
```
`kill -0` sends no real signal — silence (or, here, our own `echo`) means the process exists.
## CHANGE
Now actually terminate it:
```
$ kill 1421
```
## VERIFY
```
$ kill -0 1421 2>/dev/null && echo "still running" || echo "gone"
gone
```
The exact same check now reports the opposite result — real, observable proof the process is
gone, not an assumption.
## Instructor Notes
**Demo goal:** Make "checking before and after" a visible habit, not just narrated — students
should see the SAME command return a different real answer once state actually changed.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** reaching for `kill -9` by default "to be safe" — reinforce that
plain `kill` (SIGTERM) is the correct first attempt almost always.
@@ -0,0 +1,38 @@
# Knowledge Check: Processes
---
**1. What does a PID identify?**
*Answer:* One specific running instance of a program — the kernel's unique identifier for that
process, distinct from every other running process, even other copies of the same program.
---
**2. What's the actual difference between `kill <pid>` and `kill -9 <pid>`?**
*Answer:* Plain `kill` sends SIGTERM, a polite request the process can choose to handle (clean
up, then exit). `kill -9` sends SIGKILL, which the kernel enforces immediately with no chance
for the process to react — an escalation for when SIGTERM is ignored, not the default choice.
---
**3. You want to confirm a process is still running WITHOUT affecting it in any way. What do
you use?**
*Answer:* `kill -0 <pid>` — sends no real signal, only checks whether the PID still exists.
---
**4. Why might understanding processes matter for understanding containers later?**
*Answer:* A container is, underneath, mostly an isolated process (or small process tree) — a
container "exiting" is really its main process exiting, the exact same event this module's Lab
causes on purpose.
## Instructor Notes
**Facilitation:** Question 2 is worth confirming explicitly — "kill -9 by default" is a common,
worth-correcting habit.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,28 @@
# Guided Exercise: Choosing the Right Signal
This is a paper exercise — commit to an answer before checking it in the Lab.
## Your task
For each situation, decide: plain `kill` (SIGTERM) first, or is `kill -9` (SIGKILL) actually
justified?
1. A normal web server process you want to shut down for a routine restart.
2. A process that received SIGTERM two minutes ago and is still running, showing no signs of
responding.
3. A script you just started by mistake, that hasn't done anything harmful yet.
## A worked shape to check yourself against
1. Plain `kill` — give it the chance to shut down cleanly (close connections, flush data, etc).
2. `kill -9` is now justified — you already gave it the polite chance, and it didn't respond.
3. Plain `kill` first, still — "just started" doesn't change the default; SIGKILL is a last
resort, not a shortcut for convenience.
## Instructor Notes
**Facilitation:** Item 3 sometimes trips students who reach for `-9` out of impatience — worth
reinforcing that SIGTERM-first is the discipline REGARDLESS of urgency, since a process that
ignores SIGTERM is the actual signal that escalation is warranted.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,27 @@
# Reflection: You Just Stopped a Real Process, Not a Simulation
## Reflection questions
1. Before checking `kill -0` after your `kill`, how confident were you the process was
actually gone? What would you have done if it hadn't stopped?
2. Why do you think SIGTERM-before-SIGKILL matters more for a real production service than for
a throwaway script in a Lab?
## Real Ops takeaway
**"I ran the kill command" is not the same as "I confirmed the process is gone."** This module's
Lab required you to verify state, not just take an action and assume it worked — that
verification step is exactly what separates a confident operator from someone hoping for the
best. You'll repeat this exact pattern (act, then verify) constantly for the rest of ROOT_,
starting as soon as ROOT 04's containers and continuing all the way through Kubernetes
troubleshooting.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is a good bridge to ROOT 06's later
discussion of graceful shutdown/`terminationGracePeriodSeconds` — plant the idea now without
naming Kubernetes specifics yet.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,59 @@
# Users and Permissions Foundations
## Users, groups, and ownership
Every file on Linux has an **owner** (a user) and a **group**. `ls -la` shows both: the third
column is the owning user, the fourth is the owning group. Multiple users can belong to the same
group, which is how permissions get shared across a team without giving every individual user
direct ownership.
## Read, write, execute — for three different audiences
Every file has permissions for THREE separate audiences: the owner, the group, and everyone
else ("other"). Each audience gets some combination of:
- **r**ead — can view the file's contents (or list a directory's contents)
- **w**rite — can modify the file (or create/delete files inside a directory)
- e**x**ecute — can run the file as a program (or enter/traverse into a directory)
`ls -la` shows this as a 10-character string like `-rw-r--r--`: the first character is the file
type, then three groups of `rwx` for owner/group/other.
## Reading and setting permissions as octal numbers
Each `rwx` triplet can be written as one digit: read=4, write=2, execute=1, summed together.
`rw-` = 4+2 = `6`. `r--` = `4`. So `644` means: owner can read+write (6), group can read-only
(4), everyone else can read-only (4) — a very common, sensible default for a regular file.
`chmod 644 <file>` sets exactly that.
## `sudo`: temporary elevated privilege
Rather than logging in AS the root user (with permanent full access to everything), `sudo
<command>` runs just that one command with elevated privileges, after checking you're allowed
to. This is deliberate: the less time you spend with full privileges, the smaller the window for
a mistake (or a compromised session) to do serious damage — the exact same reasoning behind
"least privilege," which this module introduces by name for the first time.
## Least privilege: the principle, not just the command
**Least privilege** means: grant exactly the access something needs to do its job, and nothing
more. A user who only needs to read a file shouldn't also be able to write it. A process that
only needs to read one directory shouldn't have write access to the whole filesystem. This
principle isn't specific to Linux permissions — you'll meet it again, in a much more elaborate
form, in ROOT 09's Kubernetes RBAC and SecurityContext content. This module is where it starts.
## Instructor Notes
**Lesson goal:** Build real fluency with rwx/octal permissions AND plant the least-privilege
PRINCIPLE explicitly — not just command syntax, since ROOT 09 depends on students already
having this vocabulary.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** students often memorize "644 = read/write" without being
able to derive it from 4+2+1 — walk through the arithmetic explicitly rather than treating the
numbers as magic.
**Discussion question:** "Why not just make everything readable and writable by everyone, to
avoid permission headaches?" (Good setup for the least-privilege principle — let students
generate the security argument themselves before you confirm it.)
@@ -0,0 +1,55 @@
# Demonstration: A Real Permission Denial, and the Real Fix
## OBSERVE
A file is owned by root with permissions `600` (owner read/write only):
```
$ stat -c '%a %U' data.txt
600 root
```
## HYPOTHESIS
Predict: can a different, non-root user read this file?
## INSPECT
```
$ su - labuser -c "cat data.txt"
cat: data.txt: Permission denied
```
Confirmed — `600` grants nothing to group or other, so `labuser` (neither the owner nor root)
is correctly denied.
## CHANGE
Grant read access to everyone, while keeping write access limited to the owner:
```
$ chmod 644 data.txt
$ stat -c '%a' data.txt
644
```
## VERIFY
```
$ su - labuser -c "cat data.txt"
sensitive
```
The exact same command that failed before now succeeds — the permission bits, not the file's
actual content, were the only thing that changed.
## Instructor Notes
**Demo goal:** Show a permission denial as a real, reproducible fact (not just a described
error message) and the exact minimal fix, not an overcorrection like `chmod 777`.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** students reaching for `chmod 777` "to make the error go away"
— explicitly name why that's wrong (it also grants WRITE to everyone, not just the read access
actually needed) and connect it back to least privilege.
@@ -0,0 +1,38 @@
# Knowledge Check: Users and Permissions Foundations
---
**1. A colleague says "permission denied" and fixes it with `chmod 777`. What's wrong with
that fix?**
*Answer:* `777` grants read, write, AND execute to the owner, group, AND everyone else — almost
always far more access than the actual problem required. It "fixes" the symptom while violating
least privilege, usually granting write access nobody asked for.
---
**2. What does the permission `644` actually mean, broken down?**
*Answer:* Owner: read+write (4+2=6). Group: read-only (4). Other: read-only (4). Nobody but the
owner can modify the file; everyone can read it.
---
**3. Why does `sudo <command>` exist instead of just logging in as root permanently?**
*Answer:* It grants elevated privilege only for the one command that needs it, minimizing the
time spent with full access — reducing the chance a mistake (or a compromised session) causes
serious, wide damage.
---
**4. State the least-privilege principle in your own words.**
*Answer:* Grant exactly the access something needs to do its job, and nothing more.
## Instructor Notes
**Facilitation:** Question 1 is worth discussing as a group — most students have seen `chmod
777` somewhere online as "the fix," and unlearning that habit early matters.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,32 @@
# Guided Exercise: Pick the Minimal Permission, Not the Easy One
This is a paper exercise — commit to an answer before checking it in the Lab.
## Your task
For each situation, write the exact `chmod` octal value that grants the MINIMUM permission
actually needed:
1. A private SSH key file — only the owner should ever read or write it, nobody else, no
execute for anyone.
2. A shared script every user on the system should be able to run, but only the owner should be
able to edit.
3. A log file every user should be able to read (for troubleshooting), but only a logging
service (the owner) should write to it.
## A worked shape to check yourself against
1. `600` — owner read+write (6), nothing for group or other (0, 0). This is the real, standard
permission for private SSH keys — SSH itself refuses to use a key file that's more open than
this.
2. `755` — owner read+write+execute (7), group and other read+execute (5, 5) so they can run it
but not modify it.
3. `644` — owner read+write (6), group and other read-only (4, 4).
## Instructor Notes
**Facilitation:** Item 1 is a good moment to mention that this isn't a hypothetical rule — SSH
genuinely refuses to use an overly-permissive private key file, a real-world consequence of
exactly this principle.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,33 @@
# Reflection: Least Privilege Is a Habit, Not a One-Time Fix
## Reflection questions
1. Before this module, what would you have done the first time you hit a "permission denied"
error? How does that compare to what you'd do now?
2. Name one place outside of file permissions where you'd expect "grant only what's needed" to
matter (a user account, an API key, a service's network access — anything).
## Real Ops takeaway
**Least privilege is the single idea from this entire module that reappears the most across
the rest of ROOT_.** File permissions are just the first, most concrete place you'll apply it —
ROOT 09 applies the exact same principle to Kubernetes RBAC (what can a user or workload
actually DO) and SecurityContext (what can a container actually access). Getting comfortable
asking "what's the minimum this actually needs" now, on a file you can see and touch, makes that
same question much easier to ask later about something more abstract, like a ServiceAccount's
permissions.
This module is complete. This is the end of ROOT 01 — continue to ROOT 02: Linux
Administration & Networking.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 works well as a round-robin — the
variety of answers (API keys, database credentials, network firewall rules) is itself useful
evidence of how far this one principle travels.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
**Milestone note:** this is the last module of ROOT 01 — consider a short recap of the whole
course's arc (hardware → shell → filesystem → files/text → processes → permissions) before
moving on, so students see it as one coherent foundation rather than six separate topics.
@@ -0,0 +1,64 @@
# Linux System Administration
## Five questions every administrator asks
Whenever you're handed an unfamiliar running system, you're really always answering the same
five questions: **What is running? Who started it? Where is its configuration? Where are its
logs? How do I verify its state?** This module builds the vocabulary and tools for all five.
## Packages: how software gets installed
A **package manager** (`apt` on Debian/Ubuntu, `dnf`/`yum` on Fedora/RHEL) installs, updates, and
removes software along with tracking what depends on what — the alternative (manually downloading
and compiling everything) is exactly the kind of unreproducible mess package managers exist to
prevent. You won't install packages by hand often in this Lab, but recognizing `apt install` /
`apt list --installed` matters for reading real systems.
## Services: long-running background programs
A **service** is a program meant to run continuously in the background, outliving any single
terminal session — a web server, a database, a log collector. Unlike a command you run and wait
for, a service starts, keeps running, and is expected to still be there when you come back.
## systemd and systemctl (the concept — not available in this Lab)
On most real Linux servers, **systemd** is the process manager (PID 1) responsible for starting,
stopping, and supervising services, and `systemctl` is the command you'd use to control it
(`systemctl status nginx`, `systemctl restart nginx`, `journalctl -u nginx` for its logs).
**This disposable Lab container does not run systemd** — its PID 1 is a boot script that starts
k3s directly, the same way a real container almost never runs a full init system. This is worth
knowing honestly rather than pretending otherwise: the CONCEPTS below (a running process, its
config file, its logs, verifying its state) are identical whether the supervisor is systemd or
something else — this module's Labs teach those concepts against a service you CAN directly
inspect and control in this environment.
## Configuration files and environment variables
Most services read their settings from either a config file (a specific path, usually under
`/etc/`) or **environment variables** (`env` lists every variable in your current shell;
`export NAME=value` sets one). Knowing WHERE a service's configuration lives is exactly the
"where is its configuration" question from this lesson's opening — you'll answer it for real in
this module's first Lab.
## Logs: where a service tells you what happened
A well-behaved service writes what it's doing to a log — usually a file under `/var/log/`.
Reading logs (with `cat`, `tail`, `tail -f` — from ROOT 01) is almost always the first real step
in figuring out why something isn't working, which Module 6 builds into a full framework.
## Instructor Notes
**Lesson goal:** Establish the five-question mental model (running/started-by/config/logs/
verify) as the organizing frame for the rest of this module, and be explicit and honest that
systemd isn't available in this specific Lab environment rather than silently working around it.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** students who've used systemd elsewhere may expect
`systemctl` to exist here — address this directly and early rather than letting them discover
the error on their own.
**Discussion question:** "If you SSH into an unfamiliar production server and need to figure out
what's running and whether it's healthy, what's the very first command you'd run?" (There's no
single right answer — the point is getting students to articulate their own process before this
module hands them formal tools for it.)
@@ -0,0 +1,56 @@
# Demonstration: Answering the Five Questions For Real
## OBSERVE
This Lab's own terminal is served by a real running service: nginx.
```
$ pgrep -x nginx
906
```
Something with PID 906 is running.
## HYPOTHESIS
Predict: where would nginx's configuration file be, and where would its logs be?
## INSPECT
```
$ cat /etc/nginx/nginx.conf | head -5
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
...
$ ls /var/log/nginx/
access.log error.log
```
## CHANGE
Confirm it's actually listening on the port you'd expect:
```
$ ss -lnt | grep :80
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
```
## VERIFY
All five questions answered with real evidence, not assumptions: it's running (PID 906),
listening on port 80, configured at `/etc/nginx/nginx.conf`, logging to `/var/log/nginx/`. This
exact sequence — process, port, config, logs — is what you'll repeat for almost any unfamiliar
service on any real system.
## Instructor Notes
**Demo goal:** Show the five-question checklist answered end-to-end against a real, already-
running service the students are themselves using right now — more convincing than an
abstract example.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** stopping here at "PID exists" without also confirming it's
actually listening on the expected port — a process existing and a service actually being
reachable are two different facts, both worth checking.
@@ -0,0 +1,39 @@
# Knowledge Check: Linux System Administration
---
**1. What are the five questions this module opened with?**
*Answer:* What is running? Who started it? Where is its configuration? Where are its logs? How
do I verify its state?
---
**2. Why doesn't this Lab environment have `systemctl`?**
*Answer:* This Lab's container doesn't run systemd as PID 1 — its boot script starts k3s
directly instead, the same way most containers don't run a full init system. The concepts
(inspecting a running service's process/config/logs/state) are the same regardless of which
supervisor manages it.
---
**3. A process exists (`pgrep` finds a PID) but the service seems unreachable. What's the next
thing to check?**
*Answer:* Whether it's actually listening on the expected port (`ss -lnt`) — a running process
and a reachable service are two separate facts; the process could have started but failed to
bind its port.
---
**4. Where does a well-behaved Linux service typically write what it's doing?**
*Answer:* A log file, usually under `/var/log/`.
## Instructor Notes
**Facilitation:** Question 2 is worth confirming explicitly if any student has prior systemd
experience — set the expectation before the Lab, not after a confusing error.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,32 @@
# Guided Exercise: Plan Your Inspection Before You Type Anything
This is a paper exercise — commit to a plan before running commands in the Lab.
## Your task
You're told "the payments service seems slow." Before touching a keyboard, write down, in
order, which of these five questions you'd check FIRST, SECOND, and so on, and the one command
you'd use for each:
- Is it running at all?
- Is it listening on its expected port?
- What does its configuration say?
- What do its recent logs say?
- Has anything changed recently?
## A worked shape to check yourself against
A reasonable order: (1) is it running — `pgrep`/`ps`; (2) is it listening — `ss -lnt`; (3) recent
logs — `tail -n 50 <logfile>` (recent activity is usually more informative than config for a
sudden "slow" complaint); (4) configuration — only if logs point at a config-driven cause; (5)
recent changes — depends on context (deployment history, `ls -la` timestamps on config files).
There's no single correct order, but "running, then listening, then logs" before jumping to
config is a defensible default — you're checking the cheapest, fastest-answering questions
first.
## Instructor Notes
**Facilitation:** There's genuine room for disagreement on ordering here — the value is in
students defending their reasoning, not converging on one "correct" sequence.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,27 @@
# Reflection: You Now Have a Repeatable Checklist
## Reflection questions
1. Before this module, if someone said "check whether the service is healthy," what would you
have actually done? How does that compare to the five-question checklist now?
2. You started and stopped a service you fully controlled this module. What's different about
doing the same thing to a service you DIDN'T start, on a machine you don't fully understand
yet?
## Real Ops takeaway
**The five-question checklist works on ANY unfamiliar Linux system, not just this Lab.** Running
process, listening port, config location, logs, and current state — this is genuinely how
experienced engineers approach a machine they've never touched before. You'll use this exact
checklist again in ROOT 04 when a container "doesn't seem to be working," and again in
Kubernetes when a Pod isn't behaving as expected — the tools change, the checklist doesn't.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is worth real discussion time — the
honest answer is "you should be more careful and inspect more before acting," a good early
seed for the least-privilege/caution habits ROOT 09 formalizes much later.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,50 @@
# Storage and Filesystems
## Block devices and partitions, conceptually
A **block device** is the kernel's view of a storage device (a disk, an SSD, a cloud volume) —
`lsblk` lists them. A device can be split into **partitions**, separate sections each formatted
with their own filesystem, though a single-partition disk is common too. You won't partition
anything in this Lab, but recognizing `lsblk`'s output (`sda`, `sda1`, `nvme0n1`...) matters for
reading real systems.
## Filesystems and mount points
A **filesystem** is the format that organizes raw blocks into the files/directories you actually
interact with (ext4, xfs, and — inside this container — overlay, a filesystem UNIQUE to how
containers layer their storage). A filesystem becomes usable by being **mounted** at some
directory path — `/` itself is a mount point, and additional filesystems can be mounted at other
paths (a separate disk mounted at `/data`, for example).
## Persistent vs. temporary storage — a distinction that matters immediately
Not everything written to disk is meant to last. `/tmp` is conventionally temporary (ROOT 01);
a container's own writable layer is, by convention and by ROOT 04's design, meant to be
DISPOSABLE — data written inside a container that isn't explicitly persisted elsewhere disappears
when that container is destroyed. This distinction — is this data supposed to survive, or not —
is the single most important storage question you'll carry forward into ROOT 04's volumes and
eventually Kubernetes' PersistentVolumes.
## Measuring usage: `df` vs. `du`
These answer two DIFFERENT questions, a common point of confusion:
- **`df -h`** — how much space is used/free on an entire mounted FILESYSTEM.
- **`du -sh <path>`** — how much space one specific DIRECTORY's contents actually use.
A filesystem can show plenty of free space overall (`df`) while one specific directory has
unexpectedly ballooned (`du`) — you need both questions answered to diagnose "why is disk full."
## Instructor Notes
**Lesson goal:** Build the persistent-vs-temporary distinction explicitly, since it's the direct
conceptual root of ROOT 04's bind-mount/volume distinction and, later, Kubernetes storage.
**Approximate timing:** 12 minutes read; 8 minutes live.
**Common misconception to watch for:** conflating `df` and `du` — a student surprised that
`df` doesn't show them "which directory is huge" is a good moment to reinforce they answer
different questions.
**Demo idea:** `df -h /` then `du -sh /var/log/*` side by side, narrating the different
question each answers.
@@ -0,0 +1,53 @@
# Demonstration: Two Different Questions, Two Different Commands
## OBSERVE
```
$ df -h /
Filesystem Size Used Avail Use% Mounted on
overlay 937G 199G 691G 23% /
```
The root filesystem overall: 23% used, plenty free.
## HYPOTHESIS
Predict: does this tell you anything about which specific directory is using the most space?
## INSPECT
```
$ du -sh /var/log/* 2>/dev/null
1.2M /var/log/nginx
850K /var/log/k3s.log
```
No — `df` only ever answers "how full is this filesystem overall." Finding a SPECIFIC large
consumer requires `du`, pointed at the directories you suspect.
## CHANGE
Narrow further into a single suspicious directory:
```
$ du -sh /var/log/nginx/*
600K /var/log/nginx/access.log
600K /var/log/nginx/error.log
```
## VERIFY
You now know both the overall filesystem picture (`df`) AND exactly which files are contributing
to it (`du`, narrowed progressively) — two genuinely different tools for two genuinely different
questions, not interchangeable.
## Instructor Notes
**Demo goal:** Make the df/du distinction concrete by actually needing BOTH to answer a
realistic "what's using my disk" question.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** running `du -sh /` directly (extremely slow on a large
filesystem, since it recurses everything) instead of narrowing progressively into suspected
directories first.
@@ -0,0 +1,31 @@
# Knowledge Check: Storage and Filesystems
---
**1. `df -h /` shows only 23% used, but you suspect one directory is unusually large. Does
`df` alone answer that?**
*Answer:* No — `df` reports overall filesystem usage, not per-directory. You need `du -sh <path>`
pointed at the suspect directory (or directories) to answer that.
---
**2. What makes data in a container's writable layer different from data in `/home`?**
*Answer:* By convention and design, a container's own writable layer is meant to be disposable —
data there disappears when the container is destroyed, unless explicitly persisted elsewhere
(a topic ROOT 04 covers directly). `/home` data is expected to persist normally.
---
**3. What does "mounting" a filesystem actually mean?**
*Answer:* Attaching a filesystem so it becomes accessible at a specific directory path — `/`
itself is a mount point, and additional filesystems can be mounted at other paths.
## Instructor Notes
**Facilitation:** Question 2 previews ROOT 04's volume/bind-mount content directly — worth
naming that connection explicitly.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,29 @@
# Guided Exercise: Narrow Down a Disk Problem on Paper
This is a paper exercise — commit to a plan before running commands in the Lab.
## Your task
`df -h /` shows the root filesystem is 95% full. You have three suspect directories:
`/var/log`, `/srv/cache`, and `/home`. Write the exact sequence of `du` commands you'd run to
find the actual culprit, narrowing your search efficiently rather than checking everything with
maximum depth immediately.
## A worked shape to check yourself against
```
du -sh /var/log /srv/cache /home
```
— one command, comparing all three top-level suspects at once. Whichever is largest, THEN
narrow further into it specifically (e.g. `du -sh /srv/cache/*` if that one's the culprit) rather
than deep-diving into all three from the start — this mirrors Module 1's "cheapest, fastest-
answering question first" principle.
## Instructor Notes
**Facilitation:** Point out that running `du -sh` separately three times versus once with all
three paths produces the same information — the point isn't syntax, it's the "compare broadly,
then narrow" strategy.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,27 @@
# Reflection: "It's Probably the Logs" Is a Guess, Not a Diagnosis
## Reflection questions
1. Before measuring, which of the three directories in this module's Lab did you guess would
be largest? Were you right?
2. Why does "persistent vs. temporary" matter enough to be worth memorizing as a question you
ask about EVERY piece of data, not just during a storage module?
## Real Ops takeaway
**Guessing which directory is consuming disk space, without measuring, is exactly the kind of
assumption that wastes real incident-response time.** `du`, applied progressively (broad, then
narrow), replaces guessing with evidence in seconds. And the persistent-vs-temporary question
you just practiced is the exact same question you'll ask again in ROOT 04 about container data,
and again much later about Kubernetes storage — a question worth internalizing now, cheaply,
rather than learning the hard way after data you assumed would persist didn't.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 works well as a quick poll before
revealing the real answer — the mismatch between guesses and reality reinforces the lesson
better than stating it directly.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,104 @@
# Networking Mental Model
## Why this module gets extra attention
Nearly everything you'll do for the rest of ROOT_ — container networking, Kubernetes Services,
Ingress, NetworkPolicy, DNS, cloud infrastructure — is a variation on the SAME small set of
ideas this module introduces. Get this model solid now, and every later networking topic becomes
"the same thing again, one layer up." Skip it, and every later module will feel like memorizing
unrelated facts. The goal here is operational REASONING, not subnet arithmetic — you will not be
asked to calculate a subnet mask by hand.
## The chain, layer by layer
Think of reaching another machine as passing through a fixed sequence of layers, each of which
can independently succeed or fail:
```
Host
↓
Network interface (this machine's connection to a network)
↓
IP address (this machine's address on that network)
↓
Subnet (which other addresses are "local" to reach directly)
↓
Default gateway (where to send anything NOT local)
↓
Routing (the actual path traffic takes)
↓
DNS (translating a name into an IP address)
↓
Port (which specific service on that IP)
↓
TCP / UDP (how the connection itself behaves)
↓
Application protocol (HTTP, etc. — the actual conversation)
```
## Each layer, briefly
- **Network interface** — your machine's actual network connection (`eth0`, etc.). No interface,
no networking, full stop.
- **IP address** — this machine's address on a given network. IPv4 addresses look like
`172.17.0.2`. `127.0.0.1` (aka `localhost`) is special: it always means "this same machine,"
never anywhere else. `0.0.0.0` is different again — when a SERVICE binds to it, it means
"listen on every interface this machine has," not a real destination address.
- **Subnet** — a range of addresses considered "local" to each other, expressed as CIDR
(`172.17.0.0/16` means every address starting `172.17.`). You don't need to calculate subnet
math by hand — you need to recognize that two addresses in the same subnet can talk directly,
while addresses outside it need a gateway.
- **Default gateway** — the address your machine sends traffic to when the destination ISN'T in
its own subnet — effectively "ask this router to figure out the rest."
- **Routing** — the actual decision-making about which path traffic takes; `ip route` shows your
machine's routing table (Module 4 makes this hands-on).
- **DNS** — translates a human-readable name (`example.com`) into an IP address, since IP
addresses aren't what humans want to remember or type.
- **Port** — a single IP address can host MANY services simultaneously; the port number is what
distinguishes "which one." Port 80 conventionally means HTTP, 443 HTTPS, 22 SSH — conventions,
not hard rules.
- **TCP vs. UDP** — TCP is connection-oriented and guarantees delivery/order (most web traffic,
SSH); UDP is connectionless and faster but delivers no guarantees (DNS queries themselves,
video streaming). Client/server: one side (the server) listens on a fixed port waiting for
connections; the other (the client) initiates them.
## The reasoning skill: "where is the failure?"
Given this chain, a real diagnostic question decomposes into a sequence of yes/no checks, each
isolating ONE layer:
- Does the host have an IP address at all?
- Is the destination local (same subnet) or remote?
- Is there a valid route to it?
- Can the name resolve to an IP?
- Can the destination PORT actually be reached?
- Is a service actually listening there?
- Does the application itself respond correctly?
Module 4 makes each of these a real command. Module 5 walks the complete request path end to
end. Module 6 formalizes this into RootNode's troubleshooting framework, applied to networking
specifically.
## Instructor Notes
**Lesson goal:** This module gets outsized weight for a reason — it's the single highest-
leverage conceptual investment in ROOT 02. Do not rush it to "cover the topics"; depth here pays
off across ROOT 04, ROOT 05-10, and beyond.
**Approximate timing:** 25 minutes read; 15-20 minutes live with discussion — longer than a
typical lesson, deliberately.
**Common misconception to watch for:** treating `0.0.0.0` as a real destination address rather
than a "listen on every interface" bind convention — this genuinely confuses students later when
they see it in a container's own listening-port output.
**Discussion question:** Walk through a real failure narrative live: "You `curl` a URL and get
'Connection refused.' Which layers does that rule OUT, and which remain suspects?" (Answer:
rules out DNS entirely — you got FAR enough to attempt a TCP connection — and rules out "nothing
is listening at all" being the ENTIRE problem elsewhere; "refused" specifically means something
actively rejected the connection, most often nothing listening on that exact port.)
**Optional extension:** contrast "Connection refused" vs. "Connection timed out" vs. "Could not
resolve host" as three DIFFERENT failure signatures pointing at different layers — worth a full
discussion pass if time allows, since Module 5 depends on students already sensing this
distinction.
@@ -0,0 +1,56 @@
# Demonstration: Three Failures That Sound Similar But Aren't
## OBSERVE
All three of these LOOK like "it didn't work" — but each one rules out and rules in completely
different layers.
```
$ curl http://this-name-does-not-resolve.invalid/
curl: (6) Could not resolve host: this-name-does-not-resolve.invalid
```
## HYPOTHESIS
Which layer does THIS failure point at — DNS, routing, the port, or the application itself?
## INSPECT
"Could not resolve host" happens BEFORE any network connection is even attempted — it's purely a
DNS failure. No IP was ever found to connect to.
## CHANGE
Now a different failure, against a name that DOES resolve:
```
$ curl http://localhost:9999/
curl: (7) Failed to connect to localhost port 9999: Connection refused
```
DNS/name resolution already succeeded here (`localhost` always resolves). "Connection refused"
means a TCP packet reached the destination and was ACTIVELY rejected — almost always because
nothing is listening on that port.
## VERIFY
A third signature, distinct from both:
```
$ curl http://10.255.255.1:80/
curl: (28) Failed to connect to 10.255.255.1 port 80: Connection timed out
```
No response at all, not even a rejection — usually means the destination is unreachable
(no route, or blocked silently), not that something is running but declining you.
## Instructor Notes
**Demo goal:** Get students to viscerally distinguish these three error MESSAGES as three
different diagnostic starting points, not interchangeable "it's broken" signals.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** treating every connection failure as "the network is down" —
each of these three messages should immediately narrow which layer to investigate next, per
Module 5's troubleshooting exercise.
@@ -0,0 +1,45 @@
# Knowledge Check: Networking Mental Model
---
**1. What's the actual difference between an IP address and a port?**
*Answer:* An IP address identifies a specific machine on a network; a port identifies WHICH
service on that machine, since one IP can host many services simultaneously.
---
**2. `curl` reports "Could not resolve host." Which layer failed?**
*Answer:* DNS — the name never got translated into an IP address, so no connection attempt was
even made.
---
**3. `curl` reports "Connection refused" instead. What does that specifically tell you?**
*Answer:* The name resolved fine and the destination was reachable, but nothing is listening on
the port you tried — the connection attempt was actively rejected, not silently ignored.
---
**4. What is a default gateway for?**
*Answer:* It's where your machine sends traffic whose destination ISN'T in its own local subnet
— effectively "ask this router to figure out how to get there."
---
**5. True or false: `0.0.0.0` is a real destination address you could `curl`.**
*Answer:* False (in the way most students initially assume) — when a SERVICE binds to
`0.0.0.0`, it means "listen on every interface," a bind convention, not a real host you'd
connect to as a destination.
## Instructor Notes
**Facilitation:** Questions 2-3 are worth spending real time on together — the point isn't
memorizing the exact error text, it's the underlying "which layer does this rule out" reasoning.
**Approximate timing:** 8 minutes (slightly longer than a typical knowledge check, given this
module's weight).
@@ -0,0 +1,33 @@
# Guided Exercise: Diagnose From the Symptom Alone
This is a paper exercise — reason through each case using only the mental model, before any
hands-on Lab work.
## Your task
For each symptom, name which layer(s) it rules OUT, and which remain genuine suspects:
1. `ping 8.8.8.8` succeeds, but `curl https://example.com` fails with "Could not resolve host."
2. `dig example.com` returns a correct-looking IP address, but `curl https://example.com` times
out with no response at all.
3. `curl http://internal-service:8080/` returns "Connection refused," but
`curl http://internal-service:9090/` (a different port, same host) succeeds.
## A worked shape to check yourself against
1. Rules out: basic connectivity/routing to the internet (ping succeeded). Remaining suspect:
DNS specifically — something is preventing name resolution even though raw IP connectivity
works.
2. Rules out: DNS (resolution clearly succeeded). Remaining suspects: routing/reachability to
that specific IP, or a firewall silently dropping the connection — "no response at all" is the
timeout signature, not the "actively rejected" signature.
3. Rules out: DNS AND general reachability to that host (port 9090 worked). Remaining suspect:
narrowed all the way down to "nothing is listening on port 8080 specifically" — the most
precisely isolated diagnosis of the three.
## Instructor Notes
**Facilitation:** Item 3 is the strongest example of "isolate one variable" reasoning (same
host, different port) — worth highlighting explicitly as the gold-standard diagnostic pattern.
**Approximate timing:** 10-12 minutes live discussion; 8 minutes self-paced.
@@ -0,0 +1,30 @@
# Reflection: One Model, Reused Everywhere From Here On
## Reflection questions
1. Before this module, how would you have described "the network is broken" to a colleague?
How would you describe it now, more precisely?
2. This module deliberately avoided subnet math. Do you feel that cost you anything real, given
the module's actual goal (reasoning about WHERE a failure is, not calculating addresses)?
## Real Ops takeaway
**Every networking topic for the rest of ROOT_ is this same chain, viewed from a different
altitude.** A container's published port is still "port," a Kubernetes Service is still "how do
I reach the right destination via a stable address," Ingress is still "routing decisions," and
NetworkPolicy is still "which subnets/ports are allowed to reach which." You are not about to
learn a new mental model in ROOT 04 or ROOT 05 — you're about to see this SAME one applied one
layer up, repeatedly. That's why this module got extra weight: the investment compounds.
This module is complete. Continue to Module 4, where these ideas become real commands.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 is worth collecting several answers
before and after — the change in precision is itself the evidence this module worked.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Milestone note:** this is the "critical module" this course was built around — consider
confirming as a group that everyone can walk the full chain (host→interface→IP→subnet→gateway→
routing→DNS→port→TCP/UDP→application) from memory before moving on.
@@ -0,0 +1,74 @@
# Linux Network Inspection
## Every command below is really a question
Module 3 gave you the model. This module gives you the commands — but treat each one as
ANSWERING a specific question, not as syntax to memorize.
## "What addresses does this machine have?" — `ip addr`
```
$ ip -4 addr show eth0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
```
This machine's IPv4 address is `172.17.0.2`, on a `/16` subnet.
## "Where will this machine send traffic?" — `ip route`
```
$ ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2
```
The second line says: anything in `172.17.0.0/16` is reachable directly. The first line (the
`default` route) says: everything else goes through `172.17.0.1` — the default gateway from
Module 3, now a real line in a real table.
## "What TCP ports are listening?" — `ss -lnt`
```
$ ss -lnt
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
```
Something is listening on port 80, on every interface (`0.0.0.0` as a BIND address, exactly the
convention Module 3 flagged). `ss` replaced the older `netstat` on modern Linux — same idea.
## "What does DNS say this name resolves to?" — `getent hosts` / `dig`
```
$ getent hosts example.test
10.0.0.5 example.test
```
`getent hosts` checks the same resolution path the rest of the system uses (local `/etc/hosts`
entries first, then DNS) — a reliable first choice. `dig <name>` queries DNS directly and shows
more detail, useful once you specifically suspect a real DNS server issue rather than a local
override.
## "Can the application actually respond?" — `curl`
```
$ curl -sf http://localhost:80/ -o /dev/null -w '%{http_code}\n'
200
```
Everything before this point (address, route, port, DNS) can be perfect, and the APPLICATION
itself can still be broken — `curl` is the only one of these tools that actually tests the full
chain end to end, including the application's own response.
## Instructor Notes
**Lesson goal:** Convert Module 3's mental model into muscle-memory commands, each explicitly
framed as answering one specific diagnostic question.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating all five commands as redundant ways to "check
the network" — reinforce that each answers a DIFFERENT layer's question, and a real diagnosis
usually needs more than one.
**Demo idea:** run all five in sequence against this Lab's own nginx, narrating which Module 3
layer each one answers.
@@ -0,0 +1,56 @@
# Demonstration: The Same Five Questions, Answered Live
## OBSERVE
```
$ ip -4 addr show eth0
inet 172.17.0.2/16 ...
```
"What address do I have?" — answered.
## HYPOTHESIS
Predict what `ip route` will show before running it — specifically, predict whether there will
be a `default` line.
## INSPECT
```
$ ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 ...
```
Confirmed — a `default` route exists, meaning traffic to anywhere outside `172.17.0.0/16` has
somewhere to go.
## CHANGE
```
$ ss -lnt
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
```
"What's listening?" — port 80, confirmed.
## VERIFY
```
$ curl -sf http://localhost/ -o /dev/null -w '%{http_code}\n'
200
```
The full chain, verified end to end with real evidence at each layer — not "the network seems
fine," but five specific, falsifiable answers.
## Instructor Notes
**Demo goal:** Chain all five commands together as ONE continuous diagnostic narrative, not
five disconnected syntax examples.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** running `curl` first and stopping at "it worked" without ever
having separately confirmed address/route/port — fine when things work, but the WRONG habit for
when they don't (Module 5's Lab requires the full sequence).
@@ -0,0 +1,39 @@
# Knowledge Check: Linux Network Inspection
---
**1. Which command answers "what TCP ports are listening on this machine"?**
*Answer:* `ss -lnt`.
---
**2. Which command answers "where will this machine send traffic that isn't local"?**
*Answer:* `ip route` — specifically, the `default` line in its output.
---
**3. Why is `curl` the only tool in this module that can confirm the APPLICATION itself is
working, not just the network path to it?**
*Answer:* Address, route, port, and DNS can all be perfectly correct while the application
behind them still returns an error or wrong content — `curl` is the only one of these tools that
actually completes an HTTP request and inspects the real response.
---
**4. `getent hosts` and `dig` both resolve names. When would you reach for `dig` specifically
instead of `getent hosts`?**
*Answer:* When you specifically suspect a DNS server issue and want to query DNS directly with
more detail — `getent hosts` checks the same path the rest of the system uses (including local
`/etc/hosts` overrides first), which is usually the right first choice, but doesn't show you as
much about what the actual DNS server itself said.
## Instructor Notes
**Facilitation:** Question 3 reinforces Module 3's "layers can each independently succeed or
fail" idea with a very concrete example.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,28 @@
# Guided Exercise: Which Command Answers This Question?
This is a paper exercise — commit to an answer before checking it in the Lab.
## Your task
For each question, name the ONE command you'd reach for first:
1. "Does this machine have a working IP address at all?"
2. "Is anything listening on port 8080 right now?"
3. "Does `payments.internal` resolve to an address?"
4. "Is the web server actually returning a valid response, not just accepting connections?"
5. "Where does traffic to an address outside my local subnet actually go?"
## A worked shape to check yourself against
1. `ip -4 addr show`
2. `ss -lnt | grep 8080`
3. `getent hosts payments.internal`
4. `curl -sf http://.../ -o /dev/null -w '%{http_code}'`
5. `ip route`
## Instructor Notes
**Facilitation:** Have students justify WHY their chosen command answers the specific question
asked, not just that it's "a networking command" — the goal is precision, not familiarity.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,25 @@
# Reflection: Five Commands, Five Answers, One Diagnosis
## Reflection questions
1. Which of these five commands did you already know before this module? Which was
genuinely new?
2. You now have a real toolkit for Module 3's "where is the failure" question. What's still
missing before you'd feel fully confident diagnosing a real production networking issue?
## Real Ops takeaway
**Five specific commands, each answering one specific question, beat one vague instinct that
"something's wrong with the network."** This is the exact toolkit real engineers reach for
first, in this order, when handed an unfamiliar connectivity problem. Module 5 puts all five
into one continuous diagnostic story; Module 6 formalizes the discipline around using them.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's honest answer is usually
"experience with real, varied failures" — a good moment to set expectations that fluency
compounds with practice, not from one module.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,58 @@
# DNS and HTTP
## The complete request path
Every time a browser or `curl` fetches a URL, it walks the entire Module 3 chain in order:
```
Application (curl / browser)
↓
DNS lookup — translate the name to an IP
↓
IP address obtained
↓
Routing — figure out the path to that IP
↓
TCP connection established — a three-way handshake, underneath curl
↓
HTTP request sent
↓
Server processes it
↓
HTTP response returned
```
Nothing here is new — this is Module 3's chain and Module 4's commands, just walked start to
finish as ONE continuous story instead of five separate checks.
## Four distinct failure categories, not one
Given this path, "it doesn't work" always decomposes into exactly one of four categories:
1. **DNS failure** — the name never resolved to an IP at all (`curl`'s "Could not resolve
host").
2. **Routing/connection failure** — the IP was found, but no TCP connection could be established
— either refused (something's there but nothing's listening) or timed out (no response at
all, usually unreachable or silently blocked).
3. **TCP succeeded, but the application never responds correctly** — a connection was
established, but the server hangs, drops the connection, or the request never completes.
4. **HTTP/application failure** — a full HTTP response comes back, but it's WRONG (a 500 error,
a 404, unexpected content) — everything below the application layer worked fine.
These are genuinely different problems requiring genuinely different fixes — conflating them
(treating a 404 the same as a DNS failure) wastes real diagnostic time.
## Instructor Notes
**Lesson goal:** Make the four-category failure taxonomy explicit and distinct — this is the
direct foundation for this module's diagnostic Lab and for Module 6's formal framework.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating an HTTP 500 error the same category as "the
network is down" — a 500 means the ENTIRE network path worked; the failure is purely in the
application's own logic.
**Discussion question:** "A request returns HTTP 404. Which of the four categories is that, and
what does it rule out?" (Answer: category 4, application/HTTP — it rules out DNS, routing, AND
TCP connectivity entirely, since a real HTTP response came back at all.)
@@ -0,0 +1,57 @@
# Demonstration: Walking the Complete Path, Layer by Layer
## OBSERVE
```
$ getent hosts lab-internal.test
10.99.99.42 lab-internal.test
```
DNS: succeeded.
## HYPOTHESIS
Predict: if the next step (TCP connection) fails, what would that specific error look like?
## INSPECT
```
$ curl http://lab-internal.test:9091/
curl: (7) Failed to connect to lab-internal.test port 9091: Connection refused
```
"Connection refused" — DNS already succeeded (ruled out), the failure is specifically that
nothing is listening on port 9091 at that address.
## CHANGE
Find what IS actually listening, on the correct address:
```
$ ss -lnt | grep LISTEN
LISTEN 0 5 0.0.0.0:9095 0.0.0.0:*
```
The real service is on 9095, not 9091 — a configuration/expectation mismatch, not a DNS or
routing problem.
## VERIFY
```
$ curl http://lab-internal.test:9095/
app running
```
The full path, once pointed at the CORRECT layer that was actually broken (the port, not DNS),
succeeds end to end.
## Instructor Notes
**Demo goal:** Show that a real, plausible-sounding complaint ("DNS must be broken") is
disproven by evidence in seconds, and the REAL cause is one layer downstream.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** jumping straight to "restart everything" instead of walking
the chain layer by layer to find the SPECIFIC broken step — the whole point of this module's
Lab is resisting that shortcut.
@@ -0,0 +1,40 @@
# Knowledge Check: DNS and HTTP
---
**1. Name the four failure categories this module introduced.**
*Answer:* DNS failure, routing/connection failure, TCP-succeeds-but-application-never-responds,
and HTTP/application failure (a full response comes back but it's wrong).
---
**2. A request returns HTTP 500. What does that tell you about DNS and TCP connectivity?**
*Answer:* Both worked completely — a 500 means a full HTTP response was received, so the failure
is purely inside the application's own logic, not the network path to reach it.
---
**3. Why is "connection refused" a MORE specific diagnosis than "timed out"?**
*Answer:* "Refused" means a packet reached the destination and was actively rejected (almost
always: nothing listening on that port) — you know the destination is reachable. "Timed out"
means no response at all, which could mean unreachable, no route, or silently blocked —
genuinely less specific.
---
**4. A teammate says "DNS must be broken" because a request fails. What should you check
BEFORE agreeing?**
*Answer:* Whether the name actually resolves at all (`getent hosts` or `dig`) — many failures
that "feel like DNS" are actually one layer downstream (routing, port, or application), and
DNS itself may be working perfectly.
## Instructor Notes
**Facilitation:** Question 4 mirrors this module's own Lab almost exactly — a good moment to
preview it if not already started.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,32 @@
# Guided Exercise: Categorize the Failure Before You Fix It
This is a paper exercise — commit to a category before checking it in the Lab.
## Your task
For each symptom, name which of the four failure categories (DNS / routing-connection /
TCP-hangs / HTTP-application) it belongs to:
1. `curl: (6) Could not resolve host: api.internal`
2. `curl: (7) Failed to connect to api.internal port 443: Connection refused`
3. A request eventually returns, but with HTTP 502 Bad Gateway
4. A request just hangs for 30 seconds with no response, then finally times out
## A worked shape to check yourself against
1. DNS — the name never resolved.
2. Routing/connection — DNS worked, but nothing accepted the connection on that port.
3. HTTP/application — a full HTTP response came back (502 is a real, complete response), so
DNS/routing/TCP all worked; the failure is in the application layer (often a proxy unable to
reach ITS OWN backend, which is itself worth investigating further).
4. TCP-hangs / ambiguous-timeout — this is the least specific signature; it could mean
unreachable, silently dropped by a firewall, or an application that accepted the connection
but never responds — worth narrowing further with `ss`/`ip route` before concluding.
## Instructor Notes
**Facilitation:** Item 3 (502) is a good one to slow down on — many students initially assume
any 5xx code means "the network is broken," when it actually proves the OPPOSITE for every
layer below the application.
**Approximate timing:** 10-12 minutes live discussion; 8 minutes self-paced.
@@ -0,0 +1,26 @@
# Reflection: You Fixed the Real Problem, Not the Suspected One
## Reflection questions
1. Before running any commands in this module's Lab, what would you have guessed was broken?
Was your guess right?
2. Why does correctly categorizing a failure (DNS vs. routing vs. TCP vs. application) matter
more than just "trying things until something works"?
## Real Ops takeaway
**"DNS must be broken" is one of the most common wrong first guesses in real incident response
— and disproving or confirming it takes seconds, not minutes.** The habit this module builds —
categorize the failure precisely before attempting a fix — is what separates a five-minute
diagnosis from an hour of trying unrelated things. This exact reasoning, applied to Kubernetes
Services/Ingress/DNS later, is one of the most valuable transferable skills in this entire
program.
This module is complete. Continue to the final module of this course.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 works well as a live poll before
revealing the Lab's actual root cause — the gap between instinct and evidence is the lesson.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,63 @@
# Linux Troubleshooting
## The RootNode troubleshooting framework
Every module in ROOT 02 has been building toward this: a repeatable, disciplined process for
diagnosing ANY Linux-level failure, not just the specific ones you've already seen.
```
OBSERVE
↓
GATHER EVIDENCE
↓
FORM HYPOTHESIS
↓
TEST
↓
REPAIR
↓
VALIDATE
↓
PREVENT
```
## Each step, and why skipping it costs you later
- **Observe** — what's the actual, specific symptom? Not "it's broken" — the exact error, the
exact command that failed.
- **Gather evidence** — logs, process state, listening ports, config content — the raw facts,
collected BEFORE guessing at a cause.
- **Form hypothesis** — based on the evidence (not a guess made before looking), what's the most
likely cause?
- **Test** — check whether your hypothesis is actually true, ideally without yet making changes
that could mask the real problem.
- **Repair** — make the minimal change that addresses the CONFIRMED cause, not a broad "try
everything" fix.
- **Validate** — confirm the original symptom is actually gone, with real evidence, not
assumption.
- **Prevent recurrence** — what would stop this from happening again? Skipping this step is how
teams end up fixing the exact same incident repeatedly.
## Why "skip straight to fixing" costs you
Jumping straight from OBSERVE to REPAIR (skipping evidence and hypothesis) means you're guessing
— sometimes you'll get lucky, but you won't have LEARNED anything transferable, and you risk
"fixing" a symptom while leaving the actual cause untouched, guaranteeing a repeat. Every command
from Modules 1, 2, and 4 IS the "gather evidence" step, made concrete.
## Instructor Notes
**Lesson goal:** Consolidate every command this course taught into ONE disciplined process,
directly matching the identical framework `root-10-m03-troubleshooting-framework` teaches at the
Kubernetes level — introduced here first, at the Linux level, so it already feels familiar by
then.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating this as a rigid checklist to recite rather than
an actual habit — the value only shows up when it's applied under a real, unfamiliar failure
(this module's Lab).
**Discussion question:** "Describe a time you fixed something without understanding WHY it
broke. Did it come back?" (Real personal examples land this lesson better than abstract
description.)
@@ -0,0 +1,64 @@
# Demonstration: Every Step, In Order, On a Real Failure
## OBSERVE
A startup script fails: `/srv/demo/start.sh` exits with an error. That's the specific, concrete
symptom — not "the service is down."
## GATHER EVIDENCE
```
$ /srv/demo/start.sh
FATAL: config file not found: /srv/demo/config/settings.conf
```
The script's own error message IS the evidence — no guessing required.
## FORM HYPOTHESIS
The config file at that exact path doesn't exist. That's the specific, testable hypothesis —
not "something's misconfigured."
## TEST
```
$ ls /srv/demo/config/
ls: cannot access '/srv/demo/config/': No such file or directory
```
Confirmed — the directory itself doesn't exist yet, not just the file.
## REPAIR
```
$ mkdir -p /srv/demo/config
$ echo "mode=production" > /srv/demo/config/settings.conf
```
The minimal fix for the CONFIRMED cause — not a broader "reinstall everything" overcorrection.
## VALIDATE
```
$ /srv/demo/start.sh
Started with config: mode=production
```
The original symptom is gone, confirmed with real output, not assumption.
## PREVENT RECURRENCE
A real prevention step here: the script should probably check for and create its config
directory on first run, or the deployment process should ensure the config exists before the
service ever starts — naming this explicitly is what separates a real fix from a lucky one.
## Instructor Notes
**Demo goal:** Show all seven steps applied in strict order to one concrete, realistic failure
— the exact structure this module's Lab requires students to repeat themselves.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** skipping straight to REPAIR (creating the file) without first
articulating the hypothesis and testing it — even when the fix seems obvious, naming the
hypothesis explicitly is the habit this module is building.
@@ -0,0 +1,41 @@
# Knowledge Check: Linux Troubleshooting
---
**1. What's the difference between OBSERVE and GATHER EVIDENCE?**
*Answer:* OBSERVE names the specific symptom (what actually failed, precisely). GATHER EVIDENCE
collects the raw facts (logs, config, process/port state) needed to understand WHY, before
guessing at a cause.
---
**2. Why does FORM HYPOTHESIS come before TEST, rather than jumping straight to REPAIR?**
*Answer:* A hypothesis is a specific, falsifiable guess at the cause based on evidence already
gathered — testing it BEFORE repairing confirms you're about to fix the right thing, rather than
making a change that might not address the actual cause at all.
---
**3. A fix works, but the same failure happens again a week later. Which framework step was
most likely skipped?**
*Answer:* PREVENT RECURRENCE — the immediate symptom was resolved, but whatever allowed the
underlying cause to occur in the first place was never addressed.
---
**4. Why is "repair the minimal confirmed cause" better than "try several things until it
works"?**
*Answer:* A minimal, confirmed repair means you actually know what fixed it and can prevent
recurrence deliberately. Trying multiple things at once means even success leaves you unable to
say WHICH change mattered — and unable to reliably prevent the same failure next time.
## Instructor Notes
**Facilitation:** Question 3 is a good real-world check-in — ask if anyone has a personal
example of a fix that didn't actually stick.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,30 @@
# Guided Exercise: Write Your Hypothesis Before You Look at the Answer
This is a paper exercise — commit to a full framework pass before touching the Lab.
## Your task
You're told: "A script that's supposed to write a `ready` marker file isn't creating it, and
exits with a non-zero code." Before running anything, write out, specifically:
1. What's your OBSERVE statement (the precise symptom, not "it's broken")?
2. What EVIDENCE would you gather first, and with what command?
3. What's ONE specific, testable hypothesis (not "something's wrong with it")?
## A worked shape to check yourself against
1. Observe: "Script X exits non-zero and does not create file Y."
2. Evidence: redirect both stdout and stderr to a file and read it —
`script.sh > log 2>&1; cat log` — the script's own error output is almost always the fastest
evidence source for a startup failure.
3. Hypothesis (once you've actually read the error): something SPECIFIC the error names —
e.g. "the script requires a file at path X that doesn't currently exist" — not a vague
restatement of the symptom.
## Instructor Notes
**Facilitation:** The value here is entirely in getting students to write a hypothesis
SPECIFIC and FALSIFIABLE enough that they could be proven wrong — "something's misconfigured"
fails that bar; "the config file at this exact path is missing" passes it.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,31 @@
# Reflection: This Is the Same Framework You'll Use for the Rest of ROOT_
## Reflection questions
1. Which framework step did you personally find easiest to skip or rush through in this
module's Lab? Why do you think that step specifically tempts shortcuts?
2. You've now used this exact seven-step framework — Observe, Gather Evidence, Form Hypothesis,
Test, Repair, Validate, Prevent Recurrence — at the Linux level. Where do you expect to use
it again?
## Real Ops takeaway
**This entire course, from Module 1's five-question service checklist through Module 5's
failure-category taxonomy, was building toward this one repeatable discipline.** You'll meet
this exact framework again, formally named, in ROOT 10 — applied to Kubernetes instead of raw
Linux. Having already practiced it here means ROOT 10 won't be teaching you a new skill, just
retargeting a skill you already have.
This module — and ROOT 02 — is complete. Continue to ROOT 03: Git & Collaborative Development.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 tends to surface "Prevent Recurrence"
and "Form Hypothesis" (jumping straight to a guessed fix) as the two most commonly admitted
shortcuts — worth normalizing as a real, common temptation rather than a personal failing.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Milestone note:** this is the last module of ROOT 02 — consider a short recap connecting all
six modules (services → storage → networking model → inspection tools → DNS/HTTP → framework)
before moving to ROOT 03.
@@ -0,0 +1,40 @@
# Why Version Control Exists
## The problem: `final.txt`, `final_v2.txt`, `final_v2_ACTUALLY_FINAL.txt`
Before version control, "keeping history" usually meant copying a file with a new name every
time something changed. This fails fast: you lose track of WHAT changed between versions, WHO
changed it, and WHY — and it gets completely unworkable the moment more than one person touches
the same project.
## What version control actually gives you
- **History** — every change, in order, permanently. You can see exactly what a file looked
like at any point in the past.
- **Reproducibility** — you can recreate the EXACT state of an entire project at any past
moment, not just one file.
- **Collaboration** — multiple people can work on the same project without emailing files back
and forth or overwriting each other's work.
- **A source of truth** — one authoritative history everyone agrees is correct, instead of
"whoever has the newest-looking filename."
- **Rollback** — if a change turns out to be wrong, you can return to a known-good prior state
with confidence, not by hoping you kept a backup copy somewhere.
## Why copying files isn't version control
Copying `app.py` to `app_backup.py` before making a risky change LOOKS like version control, but
it has none of version control's real properties: no record of WHY the change was made, no way
to see incremental history, no way to combine two people's simultaneous edits, and no
scalability past a handful of files. Git (and version control generally) tracks CHANGES over
time as a structured history, not disconnected snapshots you have to manually manage.
## Instructor Notes
**Lesson goal:** Motivate Git before teaching a single command — students should feel the real
problem (the `final_v2_ACTUALLY_FINAL.txt` pattern) before the solution makes sense.
**Approximate timing:** 10 minutes read; 8 minutes live.
**Discussion question:** "Has anyone ever lost work, or overwritten someone else's work, because
of exactly this kind of file-copying problem?" (Real personal stories land this lesson far
better than abstract description.)
@@ -0,0 +1,31 @@
# Knowledge Check: Why Version Control Exists
---
**1. Name three real properties version control gives you that copying files as backups
doesn't.**
*Answer:* Any three of: structured history of every change, reproducibility of exact past
states, real collaboration between multiple people, a single agreed-upon source of truth,
confident rollback to a known-good state.
---
**2. Why does `final_v2_ACTUALLY_FINAL.txt` fail as a version-control strategy?**
*Answer:* It has no record of what changed or why between versions, doesn't scale past a
handful of files, and provides no way to merge two people's simultaneous changes.
---
**3. What does "source of truth" mean in the context of version control?**
*Answer:* One authoritative, agreed-upon history of a project that everyone treats as
correct — as opposed to ambiguity over which copy or filename is the "real" current version.
## Instructor Notes
**Facilitation:** Keep this brief — it's checking motivation/vocabulary before the hands-on
modules begin, not testing depth yet.
**Approximate timing:** 4 minutes.
@@ -0,0 +1,25 @@
# Reflection: Naming the Problem You're About to Solve
## Reflection questions
1. Have you personally ever lost work, or been confused about which file version was current,
in a way version control would have prevented?
2. What do you expect to feel DIFFERENT about how you work once you have real history and
rollback available?
## Real Ops takeaway
**Every engineering practice this program builds toward — code review, CI/CD, GitOps,
infrastructure as code — assumes version control as its foundation.** You cannot meaningfully
automate deployment, review changes before they ship, or declare infrastructure as code without
first having a reliable, structured history of what changed and when. This module named the
problem; the rest of ROOT 03 gives you the tool.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 tends to produce genuine, motivating
stories — let students share before moving to Module 2's mental model.
**Approximate timing:** 5 minutes live discussion; 3 minutes self-paced writing.
@@ -0,0 +1,59 @@
# Git Mental Model
## Three places your files can be
Git tracks your project through three distinct areas, and understanding this model is what
makes every Git command make sense — memorizing commands without it just produces confusion the
first time something doesn't behave as expected.
```
Working Tree → Staging Area → Repository (commit history)
(your actual (what you've (permanent,
files, as you marked ready recorded
edit them) to commit) snapshots)
```
- **Working tree** — the actual files on disk, exactly as you're editing them right now.
- **Staging area** (also called "the index") — a holding area for changes you've deliberately
marked as ready to be committed. This is Git's most distinctive design choice: you don't
commit "everything that changed," you commit "exactly what you staged."
- **Repository** — the permanent, recorded history of every commit ever made — this is what
`git log` shows you.
## Why `git add` exists at all
A change starts in your working tree. `git add <file>` moves it (conceptually — the mechanics
are more subtle, but this framing is enough for now) into the staging area. `git commit` takes
EVERYTHING currently staged and permanently records it as a new snapshot in the repository's
history. This two-step process (stage, then commit) exists so you can build up a commit
DELIBERATELY — staging exactly the related changes that belong together, even if your working
tree currently has other, unrelated changes you're not ready to commit yet.
## Core vocabulary
- **Commit** — one permanently recorded snapshot, with a message describing what changed and
why, and a unique identifier (a hash).
- **Branch** — a movable pointer to a specific commit, letting you develop something (a
"feature branch") without affecting the main line of history until you're ready to combine it
back in (Module 4).
- **HEAD** — Git's pointer to "which commit you're currently looking at" — almost always the
tip of whichever branch you have checked out.
- **Remote** — a reference to another copy of the same repository, usually hosted somewhere else
(Module 5).
## Instructor Notes
**Lesson goal:** Establish the three-area model BEFORE any commands — same "model before syntax"
pedagogy as ROOT 01's process model and ROOT 05's Kubernetes mental model. Every subsequent Git
command should be explainable in terms of moving something between these three areas.
**Approximate timing:** 15 minutes read; 10 minutes live, ideally with a literal diagram drawn
live (whiteboard or shared screen) showing files moving between the three areas.
**Common misconception to watch for:** assuming `git commit` commits every changed file
automatically — this is the single most common Git surprise for beginners, and it's exactly
what Module 3's Lab will make concrete.
**Discussion question:** "Why would you ever WANT to stage only some of your changes, not all
of them?" (Answer: to build one commit about ONE logical change, even if your working tree
currently has several unrelated edits in progress.)
@@ -0,0 +1,55 @@
# Demonstration: Watching a File Move Between the Three Areas
## OBSERVE
```
$ echo "draft" > report.txt
$ git status --short
?? report.txt
```
`??` means: Git sees this file, but it's in neither the staging area nor the repository yet —
purely in the working tree.
## HYPOTHESIS
Predict what `git status` will show after `git add report.txt`.
## INSPECT
```
$ git add report.txt
$ git status --short
A report.txt
```
`A` (in the LEFT column, which represents the staging area) means: staged, ready to commit.
## CHANGE
```
$ git commit -m "Add report draft"
$ git status --short
(nothing — clean)
```
## VERIFY
```
$ git log --oneline -1
a1b2c3d Add report draft
```
The file traveled through all three areas exactly as the model predicts: working tree (`??`) →
staging area (`A`) → repository (a permanent commit in `git log`).
## Instructor Notes
**Demo goal:** Make `git status`'s output legible by directly connecting its symbols (`??`, `A`,
clean) to the three-area model just taught, rather than treating them as arbitrary codes to
memorize.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** students reading `git status` output without connecting it
back to WHICH of the three areas each symbol refers to — reinforce the mapping explicitly.
@@ -0,0 +1,36 @@
# Knowledge Check: Git Mental Model
---
**1. Name the three areas a file passes through, in order.**
*Answer:* Working tree → staging area → repository (commit history).
---
**2. What does `git add` actually do, in terms of this model?**
*Answer:* Moves a change from the working tree into the staging area — marking it as ready to
be included in the next commit.
---
**3. If you edit three files but only `git add` one of them, what happens when you `git
commit`?**
*Answer:* Only the staged file is included in the new commit — the other two edited-but-unstaged
files remain as uncommitted changes in the working tree.
---
**4. What is HEAD?**
*Answer:* Git's pointer to the commit you're currently looking at — almost always the tip of
whichever branch you have checked out.
## Instructor Notes
**Facilitation:** Question 3 is the single most important check in this module — if students
get this wrong, revisit the diagram before continuing to Module 3.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,31 @@
# Guided Exercise: Predict `git status` Before You See It
This is a paper exercise — commit to a prediction before checking it in the Lab.
## Your task
Starting from a clean repository (nothing staged, nothing modified), predict what `git status
--short` would show after EACH of these actions, in sequence:
1. `echo "v1" > file.txt` (a brand new file)
2. `git add file.txt`
3. `echo "v2" >> file.txt` (modifying the ALREADY-STAGED file further)
4. `git commit -m "Add file"`
## A worked shape to check yourself against
1. `?? file.txt` — untracked, in the working tree only.
2. `A file.txt` — staged (left column).
3. `AM file.txt` — this is the surprising one: staged (`A`, left column) AND modified again
since staging (`M`, right column) — the staging area has a SNAPSHOT from step 2, and your
further edit in step 3 hasn't been staged yet.
4. Clean (nothing) — but only the STAGED version (from step 2, without the step-3 edit) actually
got committed; the step-3 edit remains as an uncommitted modification afterward.
## Instructor Notes
**Facilitation:** Item 3 is deliberately the hardest — it's the exact scenario that surprises
real beginners ("I thought I already staged this file, why does it show changes again?"). Worth
slowing down on.
**Approximate timing:** 10-12 minutes live discussion; 8 minutes self-paced.
@@ -0,0 +1,25 @@
# Reflection: The Model Explains the Commands, Not the Other Way Around
## Reflection questions
1. Before this module, would `git add` before `git commit` have seemed like an unnecessary
extra step? Why does it actually matter now?
2. Which of the three areas (working tree, staging, repository) do you feel least confident
distinguishing? What would help solidify it?
## Real Ops takeaway
**Every Git confusion you'll ever hit traces back to one of these three areas.** "Why isn't my
change in the commit," "why does `git status` show something weird," "why did my commit include
something I didn't mean to" — all of them are answerable by asking "which of the three areas is
this change actually in right now?" You now have that question available; Module 3 makes it
muscle memory.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is worth a quick poll — knowing where
the class is least confident tells you where to slow down in Module 3's hands-on Lab.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,70 @@
# Everyday Git
## The commands you'll actually use constantly
Module 2 gave you the model. This module gives you the six commands that make up the vast
majority of real, everyday Git usage.
## `git init` — start tracking a project
```
$ git init
```
Creates a new, empty repository in the current directory (a hidden `.git` folder holds
everything). You only run this ONCE per project, at the very beginning.
## `git status` — where do things stand right now?
Shows what's staged, what's modified-but-unstaged, and what's untracked — the single most
useful command for orienting yourself, and one you'll run constantly, almost reflexively,
before and after almost every other Git command.
## `git add` — stage a change
```
$ git add file.txt # stage one specific file
$ git add . # stage everything currently changed
```
## `git commit` — permanently record what's staged
```
$ git commit -m "Add configuration loader"
```
A good commit message describes WHAT changed and, ideally, WHY — future-you (or a teammate)
reading `git log` months later should understand the change without re-reading the entire diff.
## `git log` — see the history
```
$ git log --oneline
a1b2c3d Add configuration loader
9f8e7d6 Initial commit
```
## `git diff` — see exactly what changed, before you commit it
```
$ git diff # unstaged changes vs. the last commit
$ git diff --staged # staged changes vs. the last commit
```
Reading your own diff before committing is one of the highest-value habits in this entire
course — catching an accidental leftover debug line, or a change you didn't mean to include,
BEFORE it becomes a permanent part of history.
## Instructor Notes
**Lesson goal:** Convert Module 2's model into the six commands used in nearly every real Git
session — this is the fluency floor for everything that follows in ROOT 03.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating `git add .` as always safe — it stages
EVERYTHING changed, including files you might not have intended to include; `git status` before
`git add .` is the habit that prevents this.
**Demo idea:** run the full init→status→add→commit→log→diff sequence live, narrating `git
status`'s changing output at each step.
@@ -0,0 +1,56 @@
# Demonstration: The Full Cycle, End to End
## OBSERVE
```
$ git init
$ git status --short
(nothing — empty repository)
```
## HYPOTHESIS
Predict what happens to `git status` after creating a new file.
## INSPECT
```
$ echo "v1" > app.py
$ git status --short
?? app.py
```
## CHANGE
```
$ git add app.py
$ git diff --staged
+v1
$ git commit -m "Add initial app.py"
```
`git diff --staged` shows exactly what's ABOUT to be committed — a real, final check before it
becomes permanent.
## VERIFY
```
$ git log --oneline
3f9a1c2 Add initial app.py
$ git status --short
(clean)
```
The complete cycle, verified with real evidence at each step — not "I think I committed it,"
but confirmed via `git log`.
## Instructor Notes
**Demo goal:** Show `git diff --staged` as the final safety check before a commit becomes
permanent — a habit worth building explicitly, not just mentioning in passing.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** skipping `git diff`/`git diff --staged` entirely and committing
on faith — a good moment to ask "how would you know if you accidentally staged the wrong
thing?"
@@ -0,0 +1,39 @@
# Knowledge Check: Everyday Git
---
**1. What's the difference between `git diff` and `git diff --staged`?**
*Answer:* `git diff` shows unstaged changes (working tree vs. last commit); `git diff --staged`
shows what's currently staged and about to be committed (staging area vs. last commit).
---
**2. Why is `git add .` potentially risky compared to `git add <specific-file>`?**
*Answer:* It stages EVERYTHING currently changed in the directory, which can accidentally
include files you didn't mean to commit — checking `git status` first (or using targeted `git
add`) avoids this.
---
**3. What makes a commit message actually useful months later?**
*Answer:* Describing WHAT changed and ideally WHY, clearly enough that someone (including
future-you) reading `git log` understands the change without re-reading the full diff.
---
**4. You run `git init` in a directory that already has an existing Git repository. What
happens?**
*Answer:* `git init` only needs to run once per project — running it again in an already-
initialized repository is harmless (it won't destroy existing history) but is never actually
necessary.
## Instructor Notes
**Facilitation:** Question 2 is a good moment to establish "check status before staging broadly"
as a house habit for the rest of the course.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,32 @@
# Guided Exercise: Plan a Real Commit Before You Make It
This is a paper exercise — commit to a plan before working in the Lab.
## Your task
You've made three unrelated changes to your working tree: fixed a typo in `README.md`, added a
new function to `utils.py`, and started (but not finished) an unrelated experiment in
`scratch.py` you're not ready to commit. Write out the exact sequence of commands you'd use to
end up with exactly ONE clean commit containing the README typo fix, leaving the other two files
alone.
## A worked shape to check yourself against
```
git status
git add README.md
git diff --staged
git commit -m "Fix typo in README"
git status
```
The key insight: `git add` lets you choose EXACTLY what goes into the next commit, regardless of
what else is sitting, unstaged, in your working tree — `utils.py` and `scratch.py` remain
untouched and uncommitted.
## Instructor Notes
**Facilitation:** This exercise directly tests whether Module 2's staging-area model actually
stuck — if students reach for `git add .`, that's a sign to revisit the model before the Lab.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: You Have Real, Permanent History Now
## Reflection questions
1. Look at your own `git log` from this module's Lab. Is your commit message something a
stranger (or future-you) could understand without more context?
2. Which of the six commands from this module do you expect to type the most, going forward?
## Real Ops takeaway
**A real, permanent, inspectable history of your work now exists — this is the actual thing
Module 1 named as missing from copy-a-file-as-backup.** Every commit you make from here on is
recoverable, comparable, and explainable. Module 4 builds on this by letting multiple lines of
work (branches) develop independently before combining — impossible without the reliable
history you just built the habit of creating.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 works well as a peer-review pass —
have students read a neighbor's commit message and guess what it changed, then compare.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,62 @@
# Branching and Merging
## Why branches exist
Without branches, every commit happens on the same single line of history — risky work and
finished work are indistinguishable, and two people can't develop different things
simultaneously without directly interfering with each other. A **branch** is a movable pointer
to a commit, letting you develop something in isolation and only combine it back into the main
line when it's actually ready.
## Creating and switching branches
```
$ git branch feature-login # create a new branch (doesn't switch to it)
$ git checkout feature-login # switch to it
$ git switch feature-login # the newer, clearer equivalent of checkout for this purpose
$ git checkout -b feature-login # create AND switch in one step
```
Commits made while on `feature-login` only affect that branch — `main` is completely unaffected
until you deliberately combine them.
## Merging: combining two branches' history
```
$ git checkout main
$ git merge feature-login
```
Git attempts to combine the two branches' history automatically. When the two branches changed
DIFFERENT parts of a project, this usually succeeds with no fuss.
## Divergence and conflicts
When both branches changed the SAME lines of the SAME file differently, Git can't automatically
decide which version is correct — this is a **merge conflict**. Git marks the conflicting
section directly in the file:
```
<<<<<<< HEAD
your current branch's version
=======
the incoming branch's version
>>>>>>> feature-login
```
A conflict is not an error you did something wrong — it's Git correctly refusing to silently
guess which change you actually want. You resolve it by editing the file to the content you
actually want (removing the markers), then staging and committing to complete the merge.
## Instructor Notes
**Lesson goal:** Frame merge conflicts as an EXPECTED, normal part of collaborative work — not
a sign of failure — before students hit their first real one in this module's Lab.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** panicking at the sight of conflict markers — reinforce
explicitly that this is Git behaving correctly, not breaking.
**Demo idea:** deliberately narrate a conflict as "Git is asking YOU to make a decision it
can't make safely on its own" rather than "something went wrong."
@@ -0,0 +1,61 @@
# Demonstration: A Conflict, Caused and Resolved on Purpose
## OBSERVE
Two branches both changed line 1 of `config.txt` — `main` to `"env=production"`, `feature` to
`"env=staging"`.
## HYPOTHESIS
Predict what `git merge feature` will do.
## INSPECT
```
$ git merge feature
Auto-merging config.txt
CONFLICT (content): Merge conflict in config.txt
Automatic merge failed; fix conflicts and then commit the result.
```
Exactly as predicted — Git refuses to guess.
## CHANGE
```
$ cat config.txt
<<<<<<< HEAD
env=production
=======
env=staging
>>>>>>> feature
```
Decide the real answer — here, say `env=production` is correct — and edit the file to exactly
that, deleting all three marker lines.
## VERIFY
```
$ git add config.txt
$ git commit -m "Merge feature, keep production config"
$ git log --oneline --graph
* a1b2c3d Merge feature, keep production config
|\
| * 9f8e7d6 Update from feature
* | 3d2c1b0 Update from main
|/
* 0a1b2c3 Initial commit
```
The graph shows exactly what happened: two lines of history, deliberately combined.
## Instructor Notes
**Demo goal:** Walk a real conflict from cause to resolution without skipping any step,
reinforcing that resolution means making an explicit CHOICE, not applying a trick.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** leaving conflict markers accidentally in the "resolved" file
(easy to miss one) — `grep '<<<<<<<' file` before committing catches this reliably.
@@ -0,0 +1,36 @@
# Knowledge Check: Branching and Merging
---
**1. What is a branch, really?**
*Answer:* A movable pointer to a specific commit — not a copy of the whole project, just a
reference letting you develop independently from other branches.
---
**2. Why does a merge conflict happen?**
*Answer:* Both branches changed the SAME lines of the SAME file differently, and Git can't
safely guess which version you actually want — so it stops and asks you to decide.
---
**3. Is a merge conflict a sign something went wrong?**
*Answer:* No — it's Git correctly refusing to silently guess at a decision only a human should
make. It's a normal, expected part of collaborative work.
---
**4. After resolving a conflict by editing the file, what two steps complete the merge?**
*Answer:* Stage the resolved file (`git add`) and commit (`git commit`) — exactly like
completing any other commit, once the conflict markers are gone.
## Instructor Notes
**Facilitation:** Question 3 is worth stating explicitly and repeating — reframing conflicts
as normal, not scary, meaningfully changes how confidently students approach this module's Lab.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,36 @@
# Guided Exercise: Decide the Resolution Before You Touch the File
This is a paper exercise — commit to a decision before resolving anything in the Lab.
## Your task
A merge conflict shows:
```
<<<<<<< HEAD
timeout_seconds = 30
=======
timeout_seconds = 120
>>>>>>> feature
```
You know `main`'s value (30) was set deliberately for production safety, but `feature`'s value
(120) was added while debugging a slow endpoint that genuinely needs more time. Write out: what
final content would you put in the file, and why is that the RIGHT resolution (not just "pick
one side")?
## A worked shape to check yourself against
A well-reasoned resolution here is neither side alone: something like keeping `30` as the
DEFAULT but adding a comment or a separate, more specific setting for the slow endpoint
specifically — the point is that conflict resolution sometimes means genuinely combining intent
from both sides, not mechanically picking `HEAD` or the incoming branch. There's no single
"correct" file content — the value is in justifying whichever choice you make with real
reasoning, not defaulting to whichever side happens to be `HEAD`.
## Instructor Notes
**Facilitation:** The point of this exercise is resisting "just pick a side" as a reflex —
some conflicts genuinely require combining or renegotiating intent, not mechanical selection.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,26 @@
# Reflection: Your First Conflict Wasn't a Disaster
## Reflection questions
1. Before this module, how would you have reacted to seeing conflict markers in a real file
for the first time?
2. What made resolving the conflict in this module's Lab feel manageable (or not)? What would
make a REAL conflict, in a large unfamiliar file, harder?
## Real Ops takeaway
**Merge conflicts are one of the most feared parts of Git for beginners, and one of the most
routine parts of Git for experienced engineers — the difference is just repetition.** You've now
caused one on purpose, resolved it deliberately, and verified the result — exactly the
experience that turns "conflicts are scary" into "conflicts are Tuesday." Module 5 adds a real
remote to this same workflow; conflicts show up there too, and you're now prepared for them.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is worth extending into "what would
make a conflict on a 500-line file, with someone else's unfamiliar code, actually hard" — a good
honest preview of real collaborative work.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,56 @@
# Remote Repositories
## What a remote actually is
Every repository you've worked with so far has been entirely LOCAL — on this one machine. A
**remote** is a reference to another copy of the same repository, typically hosted somewhere
else (a server, or in this Lab, a second local repository playing that role). Remotes are what
make real collaboration possible: multiple people, multiple machines, all synchronizing through
one shared history.
## `origin`: a name, not a special concept
By convention, the remote you cloned FROM is named `origin` — but this is just a label Git
assigns automatically, not a technical requirement. You could name a remote anything; `origin`
is simply the default everyone recognizes.
## The four remote operations
- **`git clone <url>`** — copy an ENTIRE remote repository (full history included) to a new
local directory, automatically configuring it as `origin`.
- **`git fetch`** — download the remote's latest commits WITHOUT changing your own working
files or branches — a safe way to see what's new before deciding what to do about it.
- **`git pull`** — fetch AND immediately merge the remote's changes into your current branch —
effectively `fetch` + `merge` in one step (which is exactly why a `pull` can produce a merge
conflict, the same as Module 4's local merges).
- **`git push`** — upload your local commits to the remote, making them visible to everyone else
using that remote.
## Upstream: your branch's remote counterpart
A local branch can be configured to TRACK a specific remote branch (its "upstream") — once set,
plain `git pull`/`git push` (no extra arguments) know exactly which remote branch to sync
against, rather than requiring you to specify it every time.
## RootNode uses Gitea (this course does not)
RootNode's own engineering team uses Gitea (a self-hosted Git service) for its real work — the
CONCEPTS in this lesson (origin, clone, fetch, pull, push) are identical regardless of which
service hosts a remote (Gitea, GitHub, GitLab, or a private server). This course teaches those
concepts using a disposable LOCAL repository standing in for a remote — no account, no network
access, and no RootNode credentials are ever needed or exposed in this Lab.
## Instructor Notes
**Lesson goal:** Teach remotes as a genuine, transferable concept — students should be able to
work with ANY real Git hosting service afterward, not just this Lab's local simulation.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating `git pull` as a "safe, no-side-effects" command
— it's a merge, and merges can conflict, exactly like Module 4 taught.
**Discussion question:** "Why might a team prefer `git fetch` followed by manually reviewing
changes, over always running `git pull` directly?" (Answer: fetch lets you see what changed
before it touches your own working files/branch — useful caution before merging in unfamiliar
changes.)
@@ -0,0 +1,56 @@
# Demonstration: A Complete Clone-to-Push Round Trip
## OBSERVE
A bare (remote-style) repository already exists, currently empty.
```
$ git clone /srv/demo-remote.git /srv/demo-clone
warning: You appear to have cloned an empty repository.
```
Cloning an empty remote is completely normal — it just means no commits exist there yet.
## HYPOTHESIS
Predict what `git remote -v` will show inside the new clone.
## INSPECT
```
$ cd /srv/demo-clone && git remote -v
origin /srv/demo-remote.git (fetch)
origin /srv/demo-remote.git (push)
```
Automatically configured as `origin` — exactly as the lesson described, with no extra setup.
## CHANGE
```
$ echo "# Demo Project" > README.md
$ git add README.md
$ git commit -m "Add README"
$ git push origin main
```
## VERIFY
```
$ git --git-dir=/srv/demo-remote.git log --oneline
3f9a1c2 Add README
```
Checking the REMOTE directly (not the clone) proves the push actually reached it — the commit
now exists in two places, in sync.
## Instructor Notes
**Demo goal:** Prove the push actually worked by inspecting the REMOTE side independently,
not just trusting the clone's own `git log`.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming a successful `git push` with no error message means
success without ever double-checking — worth reinforcing the "verify independently" habit from
earlier ROOT_ modules.
@@ -0,0 +1,40 @@
# Knowledge Check: Remote Repositories
---
**1. What's the difference between `git fetch` and `git pull`?**
*Answer:* `git fetch` downloads the remote's new commits without touching your working files or
branches. `git pull` does that AND immediately merges them into your current branch — it's
fetch + merge combined.
---
**2. Why can `git pull` produce a merge conflict?**
*Answer:* Because it performs a real merge under the hood — if your local branch and the
remote's branch both changed the same lines differently, the same conflict situation from
Module 4 can occur.
---
**3. Is `origin` a required, special name Git enforces?**
*Answer:* No — it's just the conventional default name Git assigns to the remote you cloned
from. You could name a remote something else; `origin` is a widely recognized convention, not a
technical requirement.
---
**4. Does this course require a GitHub or Gitea account to learn remotes?**
*Answer:* No — this course uses a disposable local bare repository to safely simulate a real
remote, teaching the same concepts (clone/fetch/pull/push/origin) without any account or network
access.
## Instructor Notes
**Facilitation:** Question 2 is worth emphasizing — many students assume `pull` is risk-free
until they hit their first real pull-conflict.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,30 @@
# Guided Exercise: Fetch First, Decide Second
This is a paper exercise — commit to a plan before working in the Lab.
## Your task
You're about to sync with a shared remote that a teammate has been actively pushing to all day.
You're not sure what they changed. Write out the SAFER sequence of commands (as opposed to
running `git pull` immediately), and explain what each step lets you verify before your own
working files are touched.
## A worked shape to check yourself against
```
git fetch origin
git log HEAD..origin/main --oneline
```
`git fetch` downloads the new commits without touching your branch. The `log` command then
shows you exactly what commits exist on the remote that you don't have locally yet — letting you
review WHAT changed before deciding to merge it in with a subsequent `git merge origin/main` (or
`git pull`, now that you already know roughly what to expect).
## Instructor Notes
**Facilitation:** This exercise directly builds the "fetch first, review, then merge" habit
real teams use on shared branches — worth connecting explicitly to why blind `git pull` on a
busy shared branch is a common source of surprise conflicts.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: You Just Practiced Real Collaboration, Safely
## Reflection questions
1. What would have felt different about this module's Lab if the remote were a real hosted
service (Gitea/GitHub) instead of a local simulation? What would have felt the same?
2. Where do you expect `push` to matter most in real engineering work you'll do later?
## Real Ops takeaway
**Everything you just practiced — clone, commit, push, verified independently on the remote
side — is the exact mechanism behind CI/CD and GitOps, which ROOT 11 covers later.** A CI
pipeline triggers because someone pushed; a GitOps deployment happens because a change was
pushed to a Git repository treated as the source of truth. You haven't just learned Git commands
— you've practiced the literal trigger mechanism modern deployment automation is built around.
This module is complete. Continue to the final module of this course.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 previews ROOT 11's GitOps content
directly — worth naming that connection explicitly, even without teaching GitOps itself yet.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,54 @@
# Engineering Git Workflow
## Small commits, meaningful messages
A commit containing "fixed stuff" and 40 unrelated file changes is nearly useless to anyone
reading history later — including future-you. A commit that does ONE logical thing, with a
message describing what and why, is what actually makes `git log` a useful tool rather than
noise. This isn't pedantry — it's what makes rollback (below) and code review actually
practical.
## `main` as a protected source of truth
Real engineering teams treat their main branch as the one everyone trusts to be in a working
state at all times — direct, unreviewed pushes to `main` are typically restricted specifically
BECAUSE it's the shared source of truth every deployment and every other branch is built from.
Feature branches (Module 4) exist precisely so risky, in-progress work never threatens that
trust.
## Review: another set of eyes before history is permanent
Before a change reaches `main`, most real teams have someone else look at it first (a "pull
request" or "merge request," depending on the platform). This isn't about distrust — it's a
second perspective catching mistakes, and a natural point to discuss WHY a change is being made,
not just what it does.
## History and rollback as safety nets
Because every commit is permanent and inspectable, a bad change can be identified precisely (via
`git log`) and reverted deliberately — confident recovery, not panic, when something goes wrong.
This is Module 1's original promise, now fully realized: reproducibility and rollback aren't
theoretical, they're what your daily small-commit discipline actually buys you.
## Why this matters to DevOps, GitOps, and Infrastructure as Code
Everything in this lesson — small deliberate changes, a trusted source of truth, review before
merge, reliable history — is EXACTLY what CI/CD pipelines automate, what GitOps treats a Git
repository as authoritative for, and what Infrastructure as Code relies on to safely manage real
infrastructure through the same discipline you just practiced on plain text files. ROOT 11
builds directly on this course's habits, not a new set of ideas.
## Instructor Notes
**Lesson goal:** Close ROOT 03 by explicitly naming the professional discipline (not just Git
commands) this course was building, and its direct connection to DevOps/GitOps/IaC.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating "review" and "protecting main" as bureaucratic
overhead rather than genuine risk management — connect it back to the real cost of a bad change
reaching a shared source of truth unreviewed.
**Discussion question:** "Why might a team enforce small commits and review even when working
alone on a personal project?" (Good answers: future-you benefits from the same clarity; building
the habit now pays off the moment you DO work with others.)
@@ -0,0 +1,47 @@
# Demonstration: The Same Change, Told Two Ways
## OBSERVE
```
$ git log --oneline
a1b2c3d fixed stuff
```
One commit, an unhelpful message, and (imagine) 15 unrelated files changed together: a bug fix,
a typo correction, and an unrelated half-finished experiment.
## HYPOTHESIS
Predict: six months from now, could you (or a teammate) confidently `git revert` just the bug
fix from this commit, leaving the other 14 files untouched?
## INSPECT
No — reverting this ONE commit reverts everything in it together, including the unrelated
typo fix and experiment, whether you want that or not.
## CHANGE
The same work, done as three separate, well-described commits instead:
```
$ git log --oneline
3f9a1c2 Fix null-pointer crash in payment retry logic
9d8e7f6 Fix typo in onboarding email template
1a2b3c4 WIP: experiment with new caching strategy (not ready)
```
## VERIFY
Now `git revert 3f9a1c2` cleanly undoes JUST the payment bug fix, with zero effect on the other
two — because each commit is one logical, independently-reversible unit of work.
## Instructor Notes
**Demo goal:** Make the practical cost of "fixed stuff" commits concrete — not a style
preference, a genuine loss of capability (precise revert, clear history, easier review).
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** treating this as a purely aesthetic preference rather than a
practical one — reinforce the concrete "can you revert just the bug fix" test.
@@ -0,0 +1,32 @@
# Knowledge Check: Engineering Git Workflow
---
**1. Why does bundling unrelated changes into one commit cost you something real, not just
style points?**
*Answer:* It makes precise operations like `git revert` impossible to apply to just ONE logical
change — reverting the commit reverts everything in it together, whether you want that or not.
---
**2. Why do teams typically restrict direct, unreviewed pushes to `main`?**
*Answer:* `main` is the shared source of truth everything else (deployments, other branches) is
built from — protecting it reduces the chance an unreviewed mistake reaches something everyone
else depends on being trustworthy.
---
**3. Name one concrete way this course's Git discipline connects to GitOps or CI/CD.**
*Answer:* Any reasonable answer connecting small deliberate changes / a trusted source of truth
/ review-before-merge / reliable history to how CI/CD pipelines trigger on pushes or how GitOps
treats a Git repository as the authoritative record of desired infrastructure state.
## Instructor Notes
**Facilitation:** Question 3 is open-ended by design — the goal is students making the
connection in their own words, not reciting a fixed answer.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,30 @@
# Reflection: You've Practiced Real Engineering Discipline, Not Just Syntax
## Reflection questions
1. Look back across all six modules of ROOT 03. Which single habit (small commits, checking
`git status` before staging, fetch-before-pull, resolving conflicts deliberately) do you
think will matter most in real work?
2. This course started with "copying files as backup isn't version control." Can you now
explain, in one sentence, what Git actually gives you that copying never could?
## Real Ops takeaway
**Version control discipline is not a Git-specific skill — it's an engineering discipline that
happens to be exercised through Git.** Small deliberate changes, a trusted shared source of
truth, review before permanent history, and confident rollback are the same principles behind
CI/CD, GitOps, and Infrastructure as Code, all covered later in ROOT 11. You are not starting
that content from zero — you already practiced its foundation here.
This module — and ROOT 03 — is complete. Continue to ROOT 04: Containers & Docker.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is a good closing check — a clear,
confident answer here is real evidence the course's central premise landed.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Milestone note:** this is the last module of ROOT 03 — consider a brief recap connecting all
six modules (why version control → the three-area model → everyday commands → branching/
conflicts → remotes → professional workflow) before moving to ROOT 04.
@@ -0,0 +1,51 @@
# Why Containers Exist
## "It works on my machine"
Every engineer has heard this sentence, and it names a real, expensive problem: an application
that runs perfectly on one machine fails on another because of differences in installed
libraries, OS versions, configuration, or dozens of other environmental details that are easy to
overlook and hard to fully document.
## The problem, precisely
- **Dependency inconsistency** — an app needs Python 3.11 and a specific set of libraries; the
production server has Python 3.9 and different library versions.
- **Non-repeatability** — setting up a working environment by hand, following a README, drifts
over time and between people — "works on my machine" is often true because "my machine" has
accumulated years of undocumented manual setup.
- **Deployment friction** — moving an application from a developer's laptop to a test server to
production means repeating fragile manual setup multiple times, each an opportunity for drift.
## Containers: package the application AND everything it needs, together
A **container** packages an application with its exact dependencies, libraries, and
configuration into one deployable unit — the same container that ran on a developer's laptop
runs, byte-for-byte identically, on a test server or in production. "It works on my machine"
becomes "it works in this container," which is true everywhere that container runs.
## VM vs. container — genuinely different, not just "containers are lighter VMs"
A **virtual machine** virtualizes an entire computer, including its own full operating system
kernel — heavier, slower to start, but with very strong isolation. A **container** shares the
HOST machine's kernel and only isolates the application's process, filesystem view, and network
— dramatically lighter and faster to start (seconds vs. minutes), with somewhat different
isolation properties than a full VM (a well-understood, documented tradeoff, not a hidden flaw).
Neither is strictly "better" — they solve overlapping but distinct problems, and real production
systems often use both together (VMs providing strong host isolation, containers providing fast,
consistent application packaging on top).
## Instructor Notes
**Lesson goal:** Motivate containers through the real, felt problem ("it works on my machine")
before any Docker command — mirrors ROOT 03's "why version control exists" opener.
**Approximate timing:** 12 minutes read; 8 minutes live.
**Common misconception to watch for:** describing containers as simply "lightweight VMs" —
they share the host kernel, which is a genuine architectural difference, not just a size
difference.
**Discussion question:** "Has anyone experienced 'it works on my machine' personally, on either
side of that conversation?" (Real stories make this lesson's premise concrete before the
technical content begins.)
@@ -0,0 +1,49 @@
# Demonstration: What "Shares the Host Kernel" Actually Looks Like
## OBSERVE
```
$ uname -r
5.15.0-real-host-kernel
$ docker run --rm alpine:3.19 uname -r
5.15.0-real-host-kernel
```
## HYPOTHESIS
Predict: would a real virtual machine show the SAME kernel version as its host, the way this
container just did?
## INSPECT
A real VM runs its OWN independent kernel — it would very likely show a DIFFERENT version (or
even a different OS entirely) than its host, because a VM virtualizes the hardware and boots a
complete, separate operating system on top of it.
## CHANGE
```
$ docker run --rm ubuntu:22.04 uname -r
5.15.0-real-host-kernel
```
Even a completely different container OS (`ubuntu` here, vs. `alpine` before) STILL reports the
exact same kernel version — because there's only ONE real kernel involved, the host's.
## VERIFY
This is the concrete, observable meaning of "containers share the host kernel": no matter what
container image you run, `uname -r` always reports the same real kernel underneath — this is
exactly why containers start in a fraction of a second (no separate kernel to boot) and exactly
the tradeoff the lesson named.
## Instructor Notes
**Demo goal:** Make "shares the host kernel" a concrete, checkable fact rather than an abstract
architectural claim.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming different container images run different kernels
because their USERSPACE (installed tools, package manager) looks completely different — the
kernel underneath is unified regardless.
@@ -0,0 +1,33 @@
# Knowledge Check: Why Containers Exist
---
**1. What real problem does "it works on my machine" name?**
*Answer:* Dependency/environment inconsistency between where an application was developed and
where it actually needs to run — differences in libraries, versions, and configuration that are
easy to overlook and hard to fully document by hand.
---
**2. What's the actual architectural difference between a container and a virtual machine, not
just "containers are smaller"?**
*Answer:* A container shares the host machine's kernel and only isolates the application's
process/filesystem/network view. A VM virtualizes an entire computer, including running its own
separate kernel — a genuinely different isolation model, not just a size difference.
---
**3. Why do containers start so much faster than VMs?**
*Answer:* A container doesn't boot a separate operating system/kernel — it starts a process
using the ALREADY-RUNNING host kernel, while a VM has to boot an entire independent OS from
scratch.
## Instructor Notes
**Facilitation:** Question 2 is the one worth confirming explicitly — many students carry the
misconception "container = tiny VM" past this point without correction.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,26 @@
# Reflection: You Now Know What Problem You're About to Solve, Hands-On
## Reflection questions
1. Think back to ROOT 01's "how computers run software" module. How does today's lesson refine
your understanding of what a "process" actually is?
2. Do you expect containers to make the Linux/process/networking skills from ROOT 01-02 less
relevant, or MORE relevant? Why?
## Real Ops takeaway
**Every skill from ROOT 01-02 — processes, filesystems, networking — transfers directly into
containers, because a container is built from exactly those same primitives, just isolated
differently.** You are not about to learn an unrelated new subject; you're about to see the
Linux fundamentals you already have applied through Docker's specific tooling. Module 2 starts
that translation.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's honest answer is "more relevant" —
worth stating explicitly, since students sometimes assume containers are an alternative to
Linux fundamentals rather than built directly on top of them.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,51 @@
# Images and Containers
## The single most important distinction in this course
**An image is a packaged, read-only ARTIFACT. A container is a RUNNING INSTANCE of an image.**
This is exactly analogous to a program's executable file (the image) versus a running process
of that program (the container) — a distinction ROOT 01's process module already gave you
vocabulary for. You can run the SAME image many times, producing many independent containers,
the same way you can run the same program multiple times as separate processes.
## Layers: how an image is actually built
An image is built from a stack of **layers**, each representing one change (install a package,
copy in application code, set a configuration). Layers are cached and shared — if two images
both start `FROM alpine:3.19`, they share that base layer on disk rather than duplicating it,
which is why pulling a new image that shares layers with one you already have is often much
faster than the total image size suggests.
## Registries, repositories, and tags
- **Registry** — a server that stores and distributes images (Docker Hub is the default public
one; a company might run a private registry).
- **Repository** — a named collection of related images (e.g. `nginx`).
- **Tag** — a specific version within a repository (`nginx:alpine`, `nginx:1.25`, `nginx:latest`).
`latest` is just a conventional tag name, not automatically "the newest" in any enforced sense
— relying on it in production is a common, avoidable source of surprise.
## The core commands, mapped to the model
- **`docker pull <image>`** — download an image from a registry, without running it.
- **`docker run <image>`** — create and start a NEW container from an image (pulls automatically
if not already present locally).
- **`docker stop <container>`** — gracefully stop a running container (its process, not the
image).
- **`docker rm <container>`** — delete a stopped container. This does NOT delete the image it
was created from.
## Instructor Notes
**Lesson goal:** Land the image/container distinction as concretely as possible — nearly every
later confusion in this course traces back to conflating the two.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** using "image" and "container" interchangeably in casual
speech — gently correct this early and often, since precise vocabulary here prevents real
confusion later (e.g., "did you delete the container or the image?" matters).
**Discussion question:** "If you run `docker run nginx:alpine` three times, how many images
exist afterward? How many containers?" (Answer: 1 image, 3 containers — a good check before
moving to the hands-on Lab.)
@@ -0,0 +1,59 @@
# Demonstration: One Image, Many Independent Containers
## OBSERVE
```
$ docker images | grep alpine
alpine 3.19 a1b2c3d4e5f6 2 weeks ago 7.4MB
```
Exactly one `alpine:3.19` image exists locally right now.
## HYPOTHESIS
Predict: after running three containers from it, how many images will `docker images` show?
## INSPECT
```
$ docker run -d --name c1 alpine:3.19 sleep 300
$ docker run -d --name c2 alpine:3.19 sleep 300
$ docker run -d --name c3 alpine:3.19 sleep 300
$ docker images | grep alpine
alpine 3.19 a1b2c3d4e5f6 2 weeks ago 7.4MB
```
Still exactly one image — confirmed.
## CHANGE
```
$ docker ps --format '{{.Names}}: {{.ID}}'
c3: 9f8e7d6c5b4a
c2: 3d2c1b0a9f8e
c1: 7c6b5a4d3e2f
```
Three DIFFERENT containers, each with its own ID, all created from that same one image.
## VERIFY
```
$ docker rm -f c1 c2 c3
$ docker images | grep alpine
alpine 3.19 a1b2c3d4e5f6 2 weeks ago 7.4MB
```
Removing all three containers left the image completely untouched — proving containers and the
image they came from have independent lifecycles.
## Instructor Notes
**Demo goal:** Make the 1-image-many-containers relationship undeniable by actually counting
both before and after, rather than just stating it.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming `docker rm` on a container also removes its image —
a good moment to reinforce that images and containers have separate removal commands
(`docker rm` for containers, `docker rmi` for images) for exactly this reason.
@@ -0,0 +1,38 @@
# Knowledge Check: Images and Containers
---
**1. State the image/container distinction in one sentence.**
*Answer:* An image is a packaged, read-only artifact; a container is a running instance created
from an image — the same relationship as a program file and a running process of it.
---
**2. You run `docker rm -f my-container`. Does this delete the image it was created from?**
*Answer:* No — removing a container never removes the image; they have independent lifecycles
and separate removal commands (`docker rm` vs. `docker rmi`).
---
**3. What does the `latest` tag actually guarantee?**
*Answer:* Nothing enforced — it's just a conventional tag name, not automatically "the newest
version" in any technical sense. Relying on it in production is a common source of surprise.
---
**4. What's a layer, and why does layer sharing matter practically?**
*Answer:* A layer is one recorded change in an image's build (installing something, copying
files, etc.). Layers are cached and shared between images that share a base — meaning pulling a
new image that shares layers with one you already have can be much faster than its total
reported size suggests.
## Instructor Notes
**Facilitation:** Question 2 directly reinforces the previous demo — worth confirming everyone
got it right before moving on.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,31 @@
# Guided Exercise: Trace the Counts Through a Sequence of Commands
This is a paper exercise — commit to your answer before checking it in the Lab.
## Your task
Starting from a completely clean system (no images, no containers), predict the number of
IMAGES and the number of CONTAINERS after each step:
1. `docker pull redis:alpine`
2. `docker run -d --name cache1 redis:alpine`
3. `docker run -d --name cache2 redis:alpine`
4. `docker rm -f cache1`
5. `docker rmi redis:alpine` (attempted while `cache2` is still running)
## A worked shape to check yourself against
1. 1 image, 0 containers.
2. 1 image, 1 container.
3. 1 image, 2 containers.
4. 1 image, 1 container (only `cache1` removed).
5. This actually FAILS — Docker refuses to remove an image that a running container (`cache2`)
still depends on, specifically to prevent exactly this kind of accident. Still 1 image, 1
container, plus an error message.
## Instructor Notes
**Facilitation:** Item 5 is the most valuable one — many students haven't seen Docker's
dependency-protection behavior yet, and it's a good real safety feature to highlight.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: A Small Distinction With Large Consequences
## Reflection questions
1. Before this module, would you have known why `docker rm` and `docker rmi` are two separate
commands? Does it make sense now?
2. Where else in ROOT_ have you seen this exact "artifact vs. running instance" pattern before?
(Hint: think back to ROOT 01.)
## Real Ops takeaway
**Precise vocabulary prevents real mistakes.** Confusing "stop the container" with "delete the
image," or assuming `latest` means "the current version," are genuine, common sources of
production incidents — not just semantic nitpicking. The image/container distinction you just
solidified is the foundation every command in the rest of this course builds on.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's intended answer is "program vs.
process," from ROOT 01 — a good moment to explicitly connect the two courses.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,67 @@
# Docker CLI
## The commands you'll use constantly
Module 2 introduced pull/run/stop/rm conceptually. This module adds the rest of the everyday
toolkit — and every one of them maps directly to a Linux concept you already have from ROOT 01.
## `docker ps` — what's running right now
```
$ docker ps
CONTAINER ID IMAGE STATUS NAMES
a1b2c3d4e5f6 nginx:alpine Up 2 minutes web
```
This is the container-level equivalent of ROOT 02's `ps aux` — "what's actually running." Add
`-a` to see stopped containers too, not just running ones.
## `docker logs` — what has it been saying?
```
$ docker logs web
```
The container-level equivalent of reading a service's log file from ROOT 02 — except Docker
captures a container's stdout/stderr automatically, so there's no config needed to find WHERE
the logs live, unlike a traditional service.
## `docker exec` — run a NEW command inside an ALREADY-running container
```
$ docker exec web sh -c 'cat /etc/nginx/nginx.conf'
```
Different from `docker run`, which creates a brand-new container — `exec` reaches INTO an
existing, running one, exactly like ROOT 01's `ps`/inspection commands but scoped to one
specific container's process namespace.
## `docker inspect` — the full, detailed truth about a container
```
$ docker inspect web --format '{{.State.Running}}'
true
```
Returns a large JSON document with everything Docker knows about a container — `--format` lets
you extract just the one field you care about, rather than reading the whole thing.
## `docker stop` vs `docker rm`
`docker stop` gracefully stops a running container (sends SIGTERM, then SIGKILL after a grace
period — literally ROOT 01's process-signals lesson, applied to a container's main process).
`docker rm` deletes a STOPPED container entirely — `docker rm -f` combines both (force-stop then
remove) in one step.
## Instructor Notes
**Lesson goal:** Explicitly map every Docker CLI command back to the equivalent ROOT 01/02
Linux concept students already have — this is a translation exercise, not new material.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** confusing `docker run` (creates a new container) with
`docker exec` (runs a command in an existing one) — a very common early mixup.
**Demo idea:** run `docker exec` against a container, then immediately `docker run` the same
image again, and compare `docker ps` before/after to show the difference concretely.

Some files were not shown because too many files have changed in this diff Show More