5 Commits
Author SHA1 Message Date
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
52 changed files with 2649 additions and 25 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/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 ─────────────────────────────────────────────────────────────────
WORKDIR /app
@@ -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.
+141
View File
@@ -0,0 +1,141 @@
# Phase 6D — ROOT_ Systems Foundations
Phase 6D's mission: build the foundational half of ROOT_ (ROOT 01-04) so a student reaches
ROOT 05 already fluent in Linux/networking/Git and understanding why Kubernetes exists, using
the existing course/module/activity model throughout.
## Baseline verification
Confirmed at start: branch `main`, clean tree, HEAD = `origin/main` =
`bd0a10fbfb31b05a6264d083f27d85a6f7d5d50c` (the Phase 6C milestone).
## The central architectural finding (Section 6/7)
`backend/server.js`'s scenario engine (`runCommand()`) executes an arbitrary shell command
string from a scenario's `setup_commands`/`validation.commands` and checks its output — nothing
about it is Kubernetes-specific. This means the existing `lab` activity type and
`scenarios/data/*.json` format already fully support Linux and Git exercises with real,
deterministic validation. **No `shell_lab`/`git_lab`/`container_lab` activity type was built —
the speculative multi-type model the authorization raised as a possibility was unnecessary.**
See `docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md`.
## Container runtime decision, tested not assumed (Section 10)
Installed Docker Engine (`docker.io` + a static `docker-compose` v2 binary) inside the same
already-`Privileged: true` Lab container, alongside k3s. This does not weaken the security
boundary — the Lab Host's own Docker socket is never touched; the nested daemon has its own
socket inside the guest. Verified empirically against the real production resource caps (2
vCPU, 4096MB, 512 PIDs): k3s + dockerd + a running `nginx:alpine` container + a real image
build together used 884MB/4096MB and 160/512 PIDs, comfortable headroom in all dimensions.
`Dockerfile` and `scripts/entrypoint.sh` updated accordingly (dockerd starts non-blockingly
alongside k3s; a Docker daemon failure never breaks Kubernetes-only Labs).
## ROOT 01 — Computing & Linux Foundations: fully authored, READY
6 modules, 35 activities, 29 new content files, 6 new hands-on scenario files
(`linux-inspect-hardware`, `linux-shell-basics`, `linux-filesystem-navigation`,
`linux-files-and-text`, `linux-process-management`, `linux-permissions-foundations`) plus 2 MCQ
scenarios, all using the existing generic scenario/validation engine — no new mechanism. Every
lab was tested end-to-end via the real scenario API (setup → solve → validate), including
negative tests confirming they correctly FAIL when not actually solved. A new
`rootnode-linux-foundations-assessment` (2 required + 4 random-draw scenarios) uses the existing
Phase 3B engine.
## ROOT 02-04: architecture designed, content deferred
`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md` designs all remaining 20 modules (6+6+8) in full —
outcomes, activities, and Real Ops connections for each. Given realistic effort constraints,
authoring, testing, and verifying ROOT 01 to genuine READY depth was prioritized over shallow
architecture-only coverage across all four courses (see
`docs/ROOT_FOUNDATIONS_PILOT_READINESS.md` for the full reasoning). ROOT 02-04 remain honestly
`status = 'planned'` with zero modules — not padded to look further along than they are.
## Course prerequisite chain (Section 19)
`courses.prerequisites` now encodes ROOT 01→02→03→04→05 server-side, verified live: a fresh
student sees ROOT 02 correctly LOCKED (ROOT 01 is real, non-empty content) while ROOT 03/04/05
stay unlocked (vacuously satisfied — their own prerequisite courses are still empty). Completing
ROOT 01 unlocks ROOT 02 for that student. ROOT 06+ and the Capstone are completely unaffected
(re-confirmed via regression). No UI-only hiding — this is a real `courses` table constraint the
existing `prerequisitesSatisfied()` engine already enforced correctly before this phase; Phase
6D only populated real data into it.
## Extension point for placement/override (Section 20)
Documented, not built: `courses.prerequisites` is a plain array a future placement assessment or
instructor override could satisfy without a schema change. No bypass button implemented.
## Frontend (Sections 21-23)
No frontend change was needed or made. `CourseView.jsx` and `CurriculumView.jsx` already
generalized correctly to ROOT 01's activity/status shapes (lesson/demonstration/knowledge_check/
guided_exercise/lab/reflection/assessment, `ready`/`planned` status, `is_required`,
`instructor_notes`) — the same generic rendering already proven for ROOT 05-12 in Phase 6C.
Verified by inspection and by the real API responses `e2e-phase6d.js` and the rehearsal exercise.
## New application/platform code and why necessary
- `Dockerfile` + `scripts/entrypoint.sh`: the nested Docker Engine described above — a genuine
platform gap (ROOT 04's course design needs it) that the existing content model couldn't
satisfy on its own, tested and justified in `docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md`.
- No other application code. Every ROOT 01 requirement was satisfiable with the existing
`course_modules`/`module_activities`/`assessment_definitions`/scenario-engine model.
## Bugs found and fixed
- An early draft of the real-Lab rehearsal's hardware-report step used a triple-nested
JS-template/shell/heredoc string that silently produced a malformed file. Caught by actually
running the rehearsal against a real Lab (not assumed to work); fixed by writing the script to
a file and `docker cp`-ing it in rather than an inline heredoc.
- Phase 6D's own content changes made two hardcoded assumptions in `e2e-phase6a.js`/
`e2e-phase6b.js` stale: both suites asserted `resume` pointed at ROOT 05's gold module as "the
first real content," which was true before this phase and is now ROOT 01. Both suites fixed
and re-verified green against a fresh stack.
## Testing (Section 26)
`platform/test/e2e-phase6d.js` — 20/20 passing (fresh stack): ROOT 01 hierarchy, ROOT 02-04
honestly empty, activity ordering, lab-cannot-be-manually-completed, full course completion via
lesson/lab/assessment, prerequisite chain (both the locked and unlocked directions, on two
separate students), existing ROOT 05+ and Capstone state unaffected, is_required rule
unaffected, self-paced no-instructor-deferral check, instructor-notes exclusion, RBAC, logout/
login persistence, cross-student isolation, version-bump stability, and content validation.
## Real-Lab rehearsal (Section 27)
`infra/scripts/drills/test-phase6d-foundations-rehearsal.js` — 11/11 passing against a real
disposable Lab container (no mocking): login → enroll → resume → lesson/guided-exercise →
real Lab launch → real bash hardware inspection (`nproc`/`free`/`df`, written to a real file) →
real validation → real permission-denial-then-fix (`su - labuser`, genuinely denied, then
genuinely allowed after `chmod 644`) → remaining modules → assessment → course completion →
ROOT 02 unlock → logout/login persistence → instructor visibility of the resulting state.
## Regression (Section 30)
Re-ran 6A (21/21), 6B (24/24), 6C (17/17) fresh, each against its own freshly-reset stack per
the established isolation convention — all green after the two resume-point staleness fixes
above. 2A/2B/3A/3B/5A/5B/5C were not re-run: `git diff` for this phase shows no changes to
`platform/api/src/`, gateway, or provisioner code (only the Lab image/entrypoint, migrations,
content, scenarios, tests, and docs), so their surface area was untouched. Frontend build: clean
(`vite build`, unchanged this phase). Lab image: rebuilt and standalone-smoke-tested directly
(see the container runtime section above) in addition to the full-stack rehearsal.
## Content validation
`infra/scripts/validate-course-content.js` — 0 errors against 17 courses/modules with real
content, 201 activities, 106 scenario files (98 + 8 new this phase).
## Known limitations / intentionally deferred
- ROOT 02, 03, 04 course content — architecture designed, not authored.
- ROOT 04's Docker/Compose hands-on labs specifically — platform extension done and verified,
course content not yet written against it.
- Instructor-led vs. self-paced timing is not a separate database field (see
`docs/ROOT_PROGRAM_DURATION_MODEL.md`).
- No placement testing or instructor-override UI (explicitly out of scope this phase).
## Recommended Phase 6E scope
Author ROOT 02 (prioritize the "critical" Networking Mental Model module), then ROOT 03 (Git,
using the already-verified local-repo strategy), then ROOT 04 (content only — the runtime is
already built and tested).
+11
View File
@@ -7,6 +7,17 @@ for each placement (not coverage-percentage padding). Built from
more of demonstration/guided-exercise/reflection), **PLANNED** (structure only, no content),
**GAP** (no suitable scenario exists at all).
## Phase 6D addition
Phase 6D authored ROOT 01 (Computing & Linux Foundations) in full — 6 modules, 35 activities, 6
new hands-on Linux scenarios plus 2 MCQs, all READY. This is the program's first course outside
the Kubernetes/CKS content this matrix was originally scoped to, so it is documented separately
in `docs/ROOT_FOUNDATIONS_ARCHITECTURE.md` and `docs/PHASE6D_ROOT_FOUNDATIONS.md` rather than
folded into the tables below, which remain the ROOT 05+ (Kubernetes) scenario-coverage record.
Post-Phase-6D totals: **28 modules total across all courses with any content — 22 READY, 6
PARTIAL** (Phase 6C's 22 modules — 16 READY, 6 PARTIAL — plus 6 new ROOT 01 modules, all READY).
**106 scenario files** (98 + 8 new).
## Phase 6C verification (superseding Phase 6B's own self-reported counts)
Baseline (start of Phase 6C), run directly against the database and filesystem
+130
View File
@@ -0,0 +1,130 @@
# ROOT Foundations Architecture — ROOT 01-04
Phase 6D §1 — the curriculum architecture for the foundational half of ROOT_, built entirely on
the existing course/module/activity model (`docs/COURSE_CONTENT_MODEL.md`) and the existing
scenario/assessment engines. See `docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` for why no new
activity type or content model was needed. Every course below is a row that already exists in
`courses` (`status='planned'`, seeded by Phase 6A's `022_course_content.sql`) — this phase adds
their modules and activities, and updates their `status`/`objectives`/`prerequisites`.
## Course sequence and prerequisite chain
```
01 Computing & Linux Foundations
↓
02 Linux Administration & Networking
↓
03 Git & Collaborative Development
↓
04 Containers & Docker
↓
05 Kubernetes Foundations (existing)
```
Enforced server-side via `courses.prerequisites` (the same mechanism already gating
05→06→07/09/10→12→Capstone) — not UI-hidden alone. `root-05-kubernetes-foundations.prerequisites`
is updated this phase to add `root-04-containers-and-docker`, so a student cannot reach ROOT 05
without the foundations chain complete. This does not touch the capstone assessment engine's own
`unlock_rule` (still the original 6 `rootnode-path.json` stages) — course-level prerequisite
gating and the capstone's own gate remain the two independent systems `docs/COURSE_CONTENT_MODEL.md`
§5 and `docs/ROOT_CAPSTONE_READINESS.md` §3 already establish.
## Extension point for future placement/override (Step 20)
Not built this phase. The extension point already exists structurally: `courses.prerequisites`
is a plain array a future placement-assessment feature could programmatically satisfy (e.g. by
recording a passing placement score as an `activity_completions`-equivalent fact), or an
instructor override could satisfy by directly marking an enrollment's course complete through
the existing admin layer — neither requires a schema change to unlock. No bypass button is
implemented; this paragraph exists so a future phase doesn't need to reopen the prerequisite
model to add one.
---
## ROOT 01 — Computing & Linux Foundations
Target learner: a technically curious beginner with no assumed prior Linux/CLI experience.
Estimated duration: ~5.5 hours self-paced / ~4 hours instructor-led (see
`docs/ROOT_PROGRAM_DURATION_MODEL.md` for the derived total).
| # | Module | Outcome (verb-first) | Activities | Real Ops connection |
|---|---|---|---|---|
| 1 | How Computers Run Software | Explain what the OS/kernel manages (CPU, RAM, storage, processes) vs. what an application does | lesson, guided_exercise (inspect `/proc/cpuinfo`, `free -h`, `lsblk`, `ps aux` on paper first), lab (`linux-inspect-hardware` — real inspection commands), reflection | This is literally what a container later virtualizes — a Pod's resource requests/limits are promises about exactly these same resources. |
| 2 | Linux & The Shell | Use a shell confidently: run commands, read exit codes, use history/tab-completion/man pages | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-shell-basics`), reflection | Every `kubectl exec` session later drops a student into this exact shell experience. |
| 3 | Navigating the Filesystem | Navigate absolute/relative paths; explain what `/etc`, `/var`, `/home`, `/tmp`, `/usr`, `/proc` are for at a conceptual level | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-filesystem-navigation`), reflection | Container images later have this exact same tree — `/etc/nginx/nginx.conf` isn't a new idea, it's the same filesystem convention. |
| 4 | Files & Text | Manipulate files and text streams; use pipes/redirection to compose commands | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-files-and-text`), reflection | `kubectl logs \| grep` later is the exact same composition skill applied to a different data source. |
| 5 | Processes | Explain the process concept (PID, foreground/background, signals); inspect and terminate a real process | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-process-management`), reflection | A container IS (mostly) one isolated process tree — this module is the direct prerequisite for understanding what a container "contains." |
| 6 | Users & Permissions Foundations | Explain ownership and rwx permissions; use `chmod`/`sudo` correctly; state the least-privilege principle | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-permissions-foundations`), reflection | Direct root of ROOT 09's SecurityContext/RBAC — "least privilege" is introduced here first, in a context students can literally see and touch. |
Assessment: `rootnode-linux-foundations-assessment` (knowledge + state-validation, see §"Assessments" below).
## ROOT 02 — Linux Administration & Networking
Prerequisite: ROOT 01 complete. Target learner: comfortable with the ROOT 01 shell/filesystem/
process/permissions basics, ready to reason about services and networks.
| # | Module | Outcome | Activities | Real Ops connection |
|---|---|---|---|---|
| 1 | Linux System Administration | Explain packages/services/systemd conceptually; read logs; use environment variables and config files | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-admin-services-logs`), reflection | A Kubernetes controller IS a systemd-like supervisor pattern generalized across a cluster — "restart on failure" isn't new in ROOT 06, it's a concept students meet here first. |
| 2 | Storage & Filesystems | Explain block devices/partitions/mounts conceptually; distinguish persistent vs. temporary data; inspect real disk usage | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-storage-inspection`), reflection | Direct root of ROOT 08's PV/PVC model and ROOT 04's bind-mount/volume distinction. |
| 3 | Networking Mental Model | Explain hosts/interfaces/IP/subnet/gateway/DNS/ports/TCP-vs-UDP/client-server at a conceptual level | lesson, demonstration, knowledge_check, guided_exercise, reflection (conceptual module — no lab; see note below) | **Critical module** — the direct prerequisite for reasoning about Pod IPs, Services, Ingress, NetworkPolicy, and DNS later. |
| 4 | Linux Network Inspection | Use `ip addr`/`ip route`/`ss`/`ping`/`curl`/`dig` to answer a specific diagnostic question, not just run syntax | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-network-inspection`), reflection | Exactly the toolkit ROOT 10's troubleshooting framework assumes a student already has. |
| 5 | DNS & HTTP | Trace a request: browser → DNS → IP → TCP → HTTP → server → response | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-dns-http-trace`), reflection | The exact conceptual bridge `docs/ROOT_PROGRAM_ARCHITECTURE.md` names for later Kubernetes Services/Ingress/DNS. |
| 6 | Troubleshooting Linux Systems | Apply Observe→Evidence→Hypothesize→Test→Repair→Validate→Prevent to a Linux-level failure | lesson, demonstration, guided_exercise, lab (`linux-troubleshooting-framework`), reflection | Literally the same framework `root-10-m03-troubleshooting-framework` already teaches — introduced here first, at the Linux level, so it "already feels natural" by ROOT 10 (Step 15's explicit goal). |
Module 3 note: per the readiness standard's "sequence should match the subject," a purely
conceptual mental-model module earns READY through demonstrated reasoning (its knowledge check
and guided exercise), not a forced lab — mirroring `root-05-m00`'s own precedent. Module 4
immediately after it is where the same concepts become hands-on.
Assessment: `rootnode-linux-networking-assessment`.
## ROOT 03 — Git & Collaborative Development
Prerequisite: ROOT 02 complete. Uses only local repositories (a working tree + a local bare
"remote") inside the student's own Lab — no Gitea account, no network credential, no
RootNode-canonical-repo access ever reaches a student Lab (`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md`
§4).
| # | Module | Outcome | Activities | Real Ops connection |
|---|---|---|---|---|
| 1 | Why Version Control Exists | Explain what problem version control solves (history, collaboration, reproducibility, source of truth) | lesson, knowledge_check, reflection | Sets up "Git as source of truth" — the exact idea GitOps (ROOT 11) generalizes. |
| 2 | Git Mental Model | Explain working tree / staging area / repository / commit / branch / remote as a model, before commands | lesson, demonstration, knowledge_check, guided_exercise, reflection | Same "model before commands" pedagogy as ROOT 05's mental-model module. |
| 3 | Everyday Git | Use `status`/`add`/`commit`/`log`/`diff` to make and inspect real changes | lesson, demonstration, knowledge_check, guided_exercise, lab (`git-everyday-basics`), reflection | Every future IaC/GitOps workflow assumes this fluency as a given. |
| 4 | Branching | Create branches, switch between them, merge, resolve a simple conflict | lesson, demonstration, knowledge_check, guided_exercise, lab (`git-branching-and-merge`), reflection | Direct root of CI/CD's branch-triggered pipelines (future ROOT 11 content). |
| 5 | Remote Repositories | Clone/fetch/pull/push against a real local-bare remote; explain `origin` | lesson, demonstration, knowledge_check, guided_exercise, lab (`git-remote-clone-push`), reflection | Direct root of GitOps's "push triggers deployment" model. |
| 6 | Engineering Workflow | Write meaningful commits; explain why `main` is protected and why rollback via history matters | lesson, demonstration, knowledge_check, reflection | Explicit bridge to ROOT 11's "Git as source of truth for infrastructure" framing. |
Assessment: `rootnode-git-fundamentals-assessment`.
## ROOT 04 — Containers & Docker
Prerequisite: ROOT 03 complete. Uses the nested Docker Engine
(`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` §3) — the direct bridge into Kubernetes.
| # | Module | Outcome | Activities | Real Ops connection |
|---|---|---|---|---|
| 1 | Why Containers Exist | Explain the packaging/dependency-isolation/repeatability problem containers solve; contrast VM vs. container | lesson, demonstration, knowledge_check, reflection | Sets up "Kubernetes orchestrates what Docker already isolates" — Module 8's thesis. |
| 2 | Images & Containers | Explain image/layer/container/registry/tag as a model | lesson, demonstration, knowledge_check, guided_exercise, reflection | A Kubernetes Pod's `image:` field is this exact concept, unchanged. |
| 3 | Docker CLI | Pull/run/inspect/log/exec/stop/remove a real container | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-cli-basics`), reflection | `kubectl exec`/`kubectl logs` are the same actions against a different orchestrator. |
| 4 | Container Networking | Explain host-vs-container port, publish a port, reach one container from another | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-container-networking`), reflection | Direct root of Pod IPs/Services — builds explicitly on ROOT 02 M3/M4. |
| 5 | Container Storage | Explain ephemeral container filesystem vs. bind mounts vs. volumes | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-container-storage`), reflection | Direct root of Kubernetes PVC — builds explicitly on ROOT 02 M2. |
| 6 | Dockerfile & Image Build | Write a Dockerfile (`FROM`/`RUN`/`COPY`/`WORKDIR`/`CMD`) and build a real image | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-build-image`), reflection | Direct root of CI/CD's build step (future ROOT 11 content). |
| 7 | Docker Compose | Define and run a real multi-service app (two services, one network, one volume) | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-compose-multiservice`), reflection | Direct conceptual root of a Kubernetes multi-Pod application. |
| 8 | Containers → Kubernetes | Explain what Kubernetes adds on top of what Docker already does (scheduling, desired state, controllers, service discovery, health management, scaling, rollout) | lesson, knowledge_check, reflection | The explicit bridge module — a student should be able to answer "why Kubernetes" in their own words entering ROOT 05. |
Assessment: `rootnode-containers-assessment`.
## Assessments (Step 18)
Four new `assessment_definitions` rows, one per course, using the existing Phase 3B engine
exactly as `rootnode-security-assessment` (Phase 6C) does — mixing knowledge-recall scenarios
(`type: mcq`) with real state-validation scenarios (`type: task`, the same `runCommand`-backed
mechanism every lab already uses). No KubeKosh/Kubernetes scenario is force-fit into a
non-Kubernetes assessment. See `docs/PHASE6D_ROOT_FOUNDATIONS.md` for which are actually wired
this phase vs. deferred.
## Duration model
See `docs/ROOT_PROGRAM_DURATION_MODEL.md` for the derived instructional-hour estimate — computed
from actual `estimated_duration_minutes` values on real activities, not invented.
+117
View File
@@ -0,0 +1,117 @@
# ROOT Foundations Lab Strategy
Phase 6D §7/§10 — what the existing disposable Lab can safely teach for ROOT 01-04, evaluated
by actually reading `backend/server.js` and testing inside a real container, not assumed.
## 1. The scenario engine is already generic — no new activity type needed
`POST /api/scenarios/:id/setup` and `POST /api/scenarios/:id/validate` (`backend/server.js`)
run `setup_commands[].command` / `validation.commands[].command` through `runCommand()`, which
is a bare `child_process.exec(cmd)` — an arbitrary shell command string, checked against
`expected_output` by `checkMatch()` (exact/contains/not_contains/regex). **Nothing about this
is Kubernetes-specific.** Every existing scenario just happens to write `kubectl ...` as its
command string. `isolation_type` (`shared_namespace` etc.) is pure descriptive metadata — grepped
across the entire backend and frontend, it gates no actual behavior.
This means the existing `lab` activity type (`module_activities.activity_type = 'lab'`,
`completion_behavior = 'auto_scenario'`) and the existing `scenarios/data/*.json` format already
fully support Linux, networking-inspection, and Git exercises with real, deterministic
validation — a scenario file with `"command": "test -f /root/lab/notes.txt && echo exists"` is
exactly as valid as one with `"command": "kubectl get pod ..."`. **No `shell_lab`/`git_lab`/
`container_lab` activity type is built or needed.** The speculative multi-type model the
authorization raised as a possibility is unnecessary — it would duplicate a generic mechanism
that already exists.
The only wrinkle: `runCommand` does not pass a `cwd`, so every command runs wherever the backend
process's cwd is; multi-step Linux/Git scenarios must `cd /root/lab-<id> && ...` inside each
command string (exactly how a real student would chain commands), not rely on shell state
persisting between `setup` and `validate` calls beyond what the filesystem/git/process state
itself carries.
## 2. What the Lab image already contains (verified from the real `Dockerfile`)
Ubuntu 22.04 base with `curl wget git vim nano jq bash-completion iproute2 iptables
iputils-ping procps htop mount kmod python3 nginx` already installed, plus k3s (bundling
`kubectl`/`crictl`/containerd) started by `scripts/entrypoint.sh`. The student terminal execs
into this same container via node-pty — there is no separate "Linux sandbox," it's the same
shell k3s runs in.
**Already usable with zero image changes:** shell navigation, file manipulation (`cp`/`mv`/`rm`/
`cat`/`grep`/pipes/redirection), process inspection (`ps`/`top`/`kill`), permissions
(`chmod`/`chown`/`sudo` — the container runs as root, so a permission-teaching scenario should
`useradd` a second user in its `setup_commands` to demonstrate a real permission denial), and
network inspection (`ip addr`/`ip route`/`ss`/`ping`/`curl`).
**Missing, needed for ROOT 02 Module 5 (DNS & HTTP):** `dig`/`nslookup` are NOT installed
(`dnsutils`/`bind9-dnsutils` is absent from the current package list). This is a genuinely small,
justified Dockerfile addition (one more `apt-get install` package, same risk class as everything
else already there) — not a new mechanism. `curl` alone already covers the HTTP half.
## 3. Container runtime decision — tested, not assumed
**Decision: install real Docker Engine (`docker.io`) inside the same already-privileged Lab
container, alongside k3s, as a second independent container runtime with its own nested
`/var/run/docker.sock`.** Podman was not chosen — Docker Engine matches the actual ROOT 04
curriculum content (Docker CLI, Dockerfile, Compose terminology) and required no new privilege.
**Why this doesn't weaken the security boundary:** the student Lab container is already
launched with `Privileged: true` (`platform/provisioner/src/lifecycle.js` — required for k3s,
already the accepted threat model per `docs/LAB_ARCHITECTURE.md` §7: the Lab Host VM, not the
container boundary, is what actually contains a student). A second container runtime running
*inside* an already-fully-privileged guest adds no new host capability. **The Lab Host's own
Docker socket is never touched or exposed** — the nested `dockerd` started inside the guest
listens on its own `/var/run/docker.sock` *inside that guest's filesystem namespace*, entirely
separate from the Lab Host's real Docker daemon that the Provisioner itself talks to.
**Tested empirically** (not assumed) against the exact production resource caps
(`resource-policy.yaml`: 2 vCPU, 4096MB memory, 512 PIDs, `Privileged: true`): built
`rootnode-academy:test` fresh from the real `Dockerfile`, layered `docker.io` on top, launched
it under those exact `docker run` constraints, and ran real workloads inside:
| Step | Result | Memory | PIDs |
|---|---|---|---|
| k3s alone, ready | ✓ | 357MB | 36 |
| + `dockerd --storage-driver=vfs` started | ✓ | — | — |
| `docker run --rm hello-world` | ✓ passed | — | — |
| `docker run -d -p 8080:80 nginx:alpine`, `curl localhost:8080` | ✓ 200, real HTML returned | 866MB | 159 |
| `docker build` a real 2-line Dockerfile, `docker run` the result | ✓ passed | 884MB | 160 |
Comfortable headroom remained in all three dimensions (884MB/4096MB, 160/512 PIDs, CPU settled
after the image pull). `--storage-driver=vfs` was used deliberately — overlay2-on-overlay2
(the Lab Host's own overlay filesystem, nested inside another overlay driver) is a known source
of instability in Docker-in-Docker setups; vfs is slower per-layer but has no such nesting
hazard, and ROOT 04's exercises are small single-purpose images, not build-heavy CI workloads.
**`docker compose` is NOT included by `docker.io`** — Ubuntu's packaged Docker CLI does not ship
the Compose v2 plugin. Fixed the same way the Dockerfile already fetches k3s: download the
static `docker-compose` v2 binary directly (matching the existing "curl a static binary rather
than add a whole package repository" pattern already used for k3s) into
`/usr/local/lib/docker/cli-plugins/docker-compose` so `docker compose` works as a CLI subcommand.
**What was NOT done:** no change to the Provisioner, Lab Router, or Gateway. No change to
`HostConfig` (still `Privileged: true`, same CPU/memory/PIDs caps as every other Lab). No mount
of the Lab Host's `/var/run/docker.sock` into any container — verified this was never written
anywhere in this phase's changes.
## 4. Git strategy
`git` is already installed. All ROOT 03 exercises use **local repositories created inside the
student's own Lab** (`git init` in a scenario's `setup_commands`) — no Gitea account, no network
credentials, no dependency on RootNode's own canonical repository ever touching a student Lab.
A later module ("Remote Repositories") teaches `clone`/`fetch`/`pull`/`push`/`origin` conceptually
and against a **second local bare repository** created in the same Lab (`git init --bare`,
`git remote add origin /root/lab-remote.git`) to give students a genuine push/pull/clone
round-trip without needing any real network service, credential, or RootNode-adjacent
infrastructure — Step 9's explicit constraint.
## 5. Classification summary
| Capability | Classification |
|---|---|
| Shell/filesystem/process/permissions exercises | **SUPPORTED NOW** — zero image or code change |
| Network inspection (`ip`, `ss`, `ping`, `curl`) | **SUPPORTED NOW** |
| DNS tooling (`dig`/`nslookup`) | **REQUIRES SMALL PLATFORM EXTENSION** — one apt package added to the Lab Dockerfile |
| Git exercises (local + local-bare-remote) | **SUPPORTED NOW** — zero image or code change |
| Docker CLI / image build / single containers | **REQUIRES SMALL PLATFORM EXTENSION** — `docker.io` + entrypoint change to start `dockerd`, tested and proven safe/performant above |
| Docker Compose | **REQUIRES SMALL PLATFORM EXTENSION** — static `docker-compose` v2 binary, same pattern as k3s's own binary fetch |
| A distinct `shell_lab`/`git_lab`/`container_lab` activity type | **NOT NEEDED** — the existing generic `lab` + scenario-JSON model already covers all of the above |
+65
View File
@@ -0,0 +1,65 @@
# ROOT Foundations Pilot Readiness — ROOT 01-04
Phase 6D §24 — the honest READY/PARTIAL/DEFERRED classification for ROOT 01-04, using the same
bar `docs/ROOT_PILOT_READINESS_STANDARD.md` established in Phase 6C. The standard is not
lowered because these courses are new.
## ROOT 01 — Computing & Linux Foundations: READY
All 6 modules meet the 8-point standard:
| Module | Status | Evidence |
|---|---|---|
| M1 How Computers Run Software | READY | Lesson, guided exercise, real hands-on lab (`linux-inspect-hardware`), reflection. Verified via `e2e-phase6d.js` and the real-Lab rehearsal (`infra/scripts/drills/test-phase6d-foundations-rehearsal.js`). |
| M2 Linux and the Shell | READY | Full gold pattern (lesson/demo/kc/guided/lab/reflection). Lab (`linux-shell-basics`) tested end-to-end via the real scenario-validation API, including a negative test (fails when unsolved). |
| M3 Navigating the Filesystem | READY | Full gold pattern. Lab (`linux-filesystem-navigation`) tested end-to-end. |
| M4 Files and Text | READY | Full gold pattern. Lab (`linux-files-and-text`) tested end-to-end. |
| M5 Processes | READY | Full gold pattern. Lab (`linux-process-management`) tested end-to-end, including a negative test (fails while the process is still running). |
| M6 Users & Permissions Foundations | READY | Full gold pattern. Lab (`linux-permissions-foundations`) tested end-to-end via the real-Lab rehearsal, including a REAL permission denial (a second Linux user genuinely denied read access) before the fix. |
**Self-paced readiness:** confirmed programmatically — `infra/scripts/validate-course-content.js`'s
deferral-phrase check and `e2e-phase6d.js`'s equivalent live check both scan every ROOT 01
activity's student-visible content for "ask your instructor" or equivalent and found none.
**Instructor-led readiness:** every lesson/demonstration/guided_exercise/reflection activity has
a real Instructor Notes section (timing, misconceptions, discussion questions), verified hidden
from students both statically and via a live API check.
**Assessment:** `rootnode-linux-foundations-assessment` wired and verified live (real draw: 2
required hands-on tasks + 2 random hands-on tasks + 2 random MCQs, 6 scenarios total).
## ROOT 02 — Linux Administration & Networking: DEFERRED
Architecture fully designed in `docs/ROOT_FOUNDATIONS_ARCHITECTURE.md` (6 modules, including the
explicitly "critical" Networking Mental Model module). Zero modules authored this phase — course
remains `status = 'planned'`, honestly, with a real (not fabricated) `prerequisites` gate to
ROOT 01 already in place so the sequence is correct once it IS authored.
## ROOT 03 — Git & Collaborative Development: DEFERRED
Architecture designed (6 modules, local-repository-only strategy documented in
`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` §4). Zero modules authored this phase.
## ROOT 04 — Containers & Docker: DEFERRED (content) / PLATFORM EXTENSION DONE
Architecture designed (8 modules). **The platform extension ROOT 04 needs — a nested Docker
Engine inside the Lab — was built and verified this phase** (`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md`
§3: real `docker run`, `docker build`, and `docker compose` all tested working inside the actual
`rootnode-academy:test` image under production resource constraints). No course content was
authored against it yet — that's a real, substantial content-authoring effort left for a future
phase, now unblocked by this phase's platform work.
## Why ROOT 02-04 were not authored this phase
Given realistic effort constraints in a single phase, authoring, testing, and verifying ROOT 01
to the same rigor as every ROOT 05+ module (real scenarios, live end-to-end verification, a
real-Lab rehearsal, an assessment) was prioritized over shallow architecture-only coverage
across all four courses. This mirrors Phase 6C's own precedent: some content built to a genuine
READY bar, the rest explicitly and honestly DEFERRED with a real reason, rather than every
course padded to look further along than it is (`e2e-phase6d.js`'s own first test explicitly
asserts ROOT 02-04 have zero modules, catching any future accidental over-claim).
## Honest totals
**READY: 1 course (ROOT 01), 6 modules. DEFERRED: 3 courses (ROOT 02-04), 0 modules — by
design, not oversight.**
+21 -9
View File
@@ -21,10 +21,10 @@ future phase pending an authored teaching layer for each. See Section 16 discuss
| Course | Pilot status | Notes |
|---|---|---|
| ROOT 01 Computing & Linux Foundations | **NOT IN PILOT** | Never authored (status `planned`, zero modules). Out of Phase 6C's scope; students are assumed to arrive with basic Linux/CLI comfort for this pilot. |
| ROOT 02 Linux Administration & Networking | **NOT IN PILOT** | Same as above. |
| ROOT 03 Git & Collaborative Development | **NOT IN PILOT** | Same as above. |
| ROOT 04 Containers & Docker | **NOT IN PILOT** | Same as above. |
| ROOT 01 Computing & Linux Foundations | **IN PILOT** (added Phase 6D) | Fully authored, READY — 6 modules, real hands-on Linux Labs, a wired assessment. See `docs/PHASE6D_ROOT_FOUNDATIONS.md`. |
| ROOT 02 Linux Administration & Networking | **NOT IN PILOT** | Architecture designed (`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md`), content not yet authored. |
| ROOT 03 Git & Collaborative Development | **NOT IN PILOT** | Architecture designed, content not yet authored. |
| ROOT 04 Containers & Docker | **NOT IN PILOT** | Architecture designed; its platform extension (nested Docker Engine) is built and tested, content not yet authored. |
| ROOT 05 Kubernetes Foundations | **IN PILOT** | See modules below. |
| ROOT 06 Kubernetes Application Workloads | **IN PILOT** | See modules below. |
| ROOT 07 Kubernetes Networking & Configuration | **IN PILOT** | See modules below. |
@@ -37,6 +37,16 @@ future phase pending an authored teaching layer for each. See Section 16 discuss
## Module-level detail (courses IN pilot only)
### ROOT 01 — Computing & Linux Foundations (added Phase 6D)
| Module | Status | Prerequisite | Outcome | Delivery | Required/Optional | Labs/Assessment |
|---|---|---|---|---|---|---|
| root-01-m01-how-computers-run-software | READY | none (program entry point) | Explain the hardware/kernel/application layering; inspect a real machine's resources | Self-paced or instructor-led | 4 required | 1 lab |
| root-01-m02-linux-and-the-shell | READY | m01 | Read exit codes; use tab completion/history/man pages | Self-paced or instructor-led | 6 required | 1 lab |
| root-01-m03-navigating-the-filesystem | READY | m02 | Navigate absolute/relative paths; explain top-level directories | Self-paced or instructor-led | 6 required | 1 lab |
| root-01-m04-files-and-text | READY | m03 | Manipulate files; filter text with grep/pipes/redirection | Self-paced or instructor-led | 6 required | 1 lab |
| root-01-m05-processes | READY | m04 | Inspect and correctly terminate a real process | Self-paced or instructor-led | 6 required | 1 lab |
| root-01-m06-users-and-permissions-foundations | READY | m05 | Apply rwx permissions and the least-privilege principle | Self-paced or instructor-led | 7 required | 1 lab + **ROOT 01 Linux Foundations Assessment** (6-scenario timed exam) |
### ROOT 05 — Kubernetes Foundations
| Module | Status | Prerequisite | Outcome | Delivery | Required/Optional | Labs/Assessment |
|---|---|---|---|---|---|---|
@@ -99,7 +109,8 @@ instructor-only prep material was created outside the per-activity Instructor No
## What is intentionally NOT in the first pilot
- ROOT 01-04 (never authored).
- ROOT 02-04 (architecture designed in Phase 6D, content not yet authored — ROOT 01 IS now in
the pilot, added Phase 6D).
- ROOT 08 Kubernetes Storage & Scheduling (non-capstone-gating; both modules remain PARTIAL).
- ROOT 11 modules 2-7 (GitOps, CI/CD, IaC, hands-on Observability, hands-on Reliability) — see
scope doc.
@@ -112,7 +123,8 @@ instructor-only prep material was created outside the per-activity Instructor No
## Bottom line
Every module the pilot's required completion path depends on (ROOT 05-07, 09-10, 12, Capstone)
meets the READY bar for instructor-led delivery. Three specific modules run instructor-led only
this pilot. ROOT 08 and most of ROOT 11 are explicitly out of the required path. Given that
scope, the pilot curriculum, delivered instructor-led, is ready.
Every module the pilot's required completion path depends on (ROOT 01, 05-07, 09-10, 12,
Capstone) meets the READY bar for instructor-led delivery, and ROOT 01's self-paced readiness
was verified explicitly (see `docs/PHASE6D_ROOT_FOUNDATIONS.md`). Three specific modules run
instructor-led only this pilot. ROOT 02-04, ROOT 08, and most of ROOT 11 are explicitly out of
the required path. Given that scope, the pilot curriculum, delivered instructor-led, is ready.
+18 -8
View File
@@ -14,14 +14,21 @@ here is grounded in `docs/ROOT_CURRICULUM_INVENTORY.md`, not aspirational.
6 courses gained new teaching modules — the module-level detail and evidence for every new
placement lives in `docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md`, not duplicated here.
**Updated for Phase 6D** (`platform/api/migrations/025_phase6d_root_foundations_root01.sql`,
`docs/PHASE6D_ROOT_FOUNDATIONS.md`): ROOT 01 fully authored and moved to READY — see
`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md` for its module-level design and
`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` for why no new activity type was needed. ROOT 02-04
remain PLANNED (architecture designed, content not yet authored) but now carry a real
prerequisite chain (01→02→03→04→05) enforced server-side.
## Course sequence
| # | Course | Status | Why |
|---|---|---|---|
| 1 | Computing & Linux Foundations | **PLANNED** | No lab content exists in this repository — every scenario is Kubernetes/CKS. |
| 2 | Linux Administration & Networking | **PLANNED** | Same — no Linux-admin scenario content exists. |
| 3 | Git & Collaborative Development | **PLANNED** | No Git scenario content exists. |
| 4 | Containers & Docker | **PLANNED** | No Docker-specific scenario content exists (the Lab itself runs k3s, but no student-facing "learn Docker" scenarios exist). |
| 1 | Computing & Linux Foundations | **READY** | Phase 6D: 6 modules, real Linux/shell scenarios using the existing generic scenario engine, a wired assessment — see `docs/PHASE6D_ROOT_FOUNDATIONS.md`. |
| 2 | Linux Administration & Networking | **PLANNED** | Architecture designed (`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md`); content not yet authored. Real prerequisite: ROOT 01 complete. |
| 3 | Git & Collaborative Development | **PLANNED** | Architecture designed; content not yet authored. Real prerequisite: ROOT 02 complete. |
| 4 | Containers & Docker | **PLANNED** | Architecture designed; the nested-Docker-Engine platform extension it needs was built and verified this phase (`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` §3), course content not yet authored. Real prerequisite: ROOT 03 complete. |
| 5 | Kubernetes Foundations | **PARTIAL** | Phase 6B added a READY front-door module (`root-05-m00-kubernetes-mental-model` — full lesson/demo/knowledge-check/guided-exercise/lab/reflection) sequenced before the existing 20-scenario bare-lab bank (`root-05-m01`), which now sits behind it. |
| 6 | Kubernetes Application Workloads | **PARTIAL** | Phase 6B added a READY `root-06-m02-deployments-scaling-updates` and a PARTIAL `root-06-m03-jobs-cronjobs-daemonsets`, both in front of the existing bare-lab bank. |
| 7 | Kubernetes Networking & Configuration | **PARTIAL** | Phase 6B added a READY `root-07-m03-services-and-service-discovery` and a PARTIAL `root-07-m04-ingress-networkpolicy-foundations`. The existing configuration bank (`root-07-m02`) still has no authored teaching content — an honest, documented gap. |
@@ -60,10 +67,13 @@ one complete module (the gold standard) as the authoring template, not a finishe
CAPSTONE — Operate & Recover
```
01-04 have no prerequisite chain enforced against each other yet (all PLANNED, zero content —
locking a PLANNED course behind another PLANNED course would just be decorative). 05 is the
first course with a real prerequisite gate. See `docs/COURSE_CONTENT_MODEL.md`'s "prerequisite/
locking rule" for why a PLANNED/content-free prerequisite never actually blocks anything.
**Updated for Phase 6D:** 01→02→03→04→05 is now a real, server-side prerequisite chain
(`courses.prerequisites`). Since ROOT 01 is now real content and 02-04 remain PLANNED, ROOT 02
is the first course this chain actually locks (behind ROOT 01); 03/04/05 stay vacuously
satisfied until their own prerequisite course is authored — see
`docs/COURSE_CONTENT_MODEL.md`'s "prerequisite/locking rule" for why a PLANNED/content-free
prerequisite never actually blocks anything, and `docs/PHASE6D_ROOT_FOUNDATIONS.md` for the live
verification of this behavior on both a completed and a fresh student.
## Deviations from the spec's suggested sequence, and why
+60
View File
@@ -0,0 +1,60 @@
# ROOT_ Program Duration Model
Phase 6D §25 — the actual instructional-hour estimate, computed by summing real
`module_activities.estimated_duration_minutes` values from the live database, not invented or
marketed backward from a target number. Run: `SELECT c.course_id, SUM(a.estimated_duration_minutes) ...
GROUP BY c.course_id` against the current schema.
## Per-course totals (self-paced estimate)
| Course | Activities | Minutes | Hours |
|---|---|---|---|
| ROOT 01 Computing & Linux Foundations | 35 | 338 | 5.6 |
| ROOT 02 Linux Administration & Networking | 0 | 0 | 0 (not yet authored) |
| ROOT 03 Git & Collaborative Development | 0 | 0 | 0 (not yet authored) |
| ROOT 04 Containers & Docker | 0 | 0 | 0 (not yet authored) |
| ROOT 05 Kubernetes Foundations | 28 | 403 | 6.7 |
| ROOT 06 Kubernetes Application Workloads | 22 | 295 | 4.9 |
| ROOT 07 Kubernetes Networking & Configuration | 27 | 353 | 5.9 |
| ROOT 08 Kubernetes Storage & Scheduling | 15 | 250 | 4.2 |
| ROOT 09 Kubernetes Security | 40 | 745 | 12.4 |
| ROOT 10 Kubernetes Troubleshooting & Observability | 20 | 277 | 4.6 |
| ROOT 11 DevOps & Platform Engineering | 3 | 35 | 0.6 |
| ROOT 12 Real Ops | 6 | 115 | 1.9 |
| ROOT Capstone | 5 | 120 | 2.0 |
| **Total (all authored content)** | **201** | **2,931** | **48.9** |
## Self-paced vs. instructor-led
The figures above are self-paced estimates — the sum of each activity's own
`estimated_duration_minutes`, which is how long a student working alone is expected to need for
that specific lesson/lab/reflection. This is the one number this phase can honestly derive
programmatically.
**Instructor-led classroom time is NOT separately tracked as a distinct database field** — every
Instructor Notes section already documents approximate live-facilitation timing in prose (e.g.
"10 minutes live discussion; 8 minutes self-paced writing"), but these are per-activity
estimates embedded in Markdown, not a summable column. A future phase could add a distinct
`estimated_duration_minutes_instructor_led` column if precise classroom scheduling becomes a
real, measured need — this phase does not invent that precision now. As a conservative
approximation: instructor-led delivery of the currently-authored curriculum runs roughly the
same total or somewhat longer than the self-paced figure above, once live discussion and
demonstration time (documented per-activity) is added — do not treat 48.9 hours as a hard
classroom-scheduling number without reading the relevant Instructor Notes first.
## What this is NOT
Not a marketing duration, not a promise, not a certification-hours figure. `estimated_duration_minutes`
values were themselves authored as honest guesses (see `docs/COURSE_AUTHORING_STANDARD.md` §4)
— this total will get more accurate as real completion-time data accumulates from actual
students, not before.
## Once ROOT 02-04 are authored
The foundational half of ROOT_ (01-04) is planned at ~6 modules each (see
`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md`) — ROOT 01's actual authored total (5.6 hours across 6
modules) is the only real data point so far; ROOT 02-04 should be expected to land in a similar
5-7 hour range each once authored, giving a rough eventual ROOT 01-04 total near 20-25 hours,
and a full ROOT_ program total near 65-75 hours — stated here as a planning estimate, explicitly
not yet real data, and should be replaced with the real computed total once those three courses
are actually built.
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Orchestrates the Phase 6D §27 ROOT Foundations dress rehearsal: brings up the real dev stack,
# bootstraps an owner, runs test-phase6d-foundations-rehearsal.js (real disposable Lab, real
# bash, real permission denial/fix, real instructor visibility check), tears down.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/../../../platform"
export PROVISIONER_SHARED_SECRET="${PROVISIONER_SHARED_SECRET:-rehearsal-shared-secret-not-for-production}"
export INTEGRATION_SHARED_SECRET="${INTEGRATION_SHARED_SECRET:-dev-integration-secret-not-for-production}"
export LAB_IMAGE_REF="${LAB_IMAGE_REF:-rootnode-academy:test}"
docker compose -f docker-compose.dev.yml up -d --build
cleanup() {
echo "==> Tearing down"
docker ps -aq --filter "name=rootnode-lab-" | xargs -r docker rm -f >/dev/null 2>&1 || true
docker network ls --filter "name=rootnode-lab-net-" --format "{{.Name}}" | xargs -r -n1 docker network rm >/dev/null 2>&1 || true
docker compose -f docker-compose.dev.yml down >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo "==> Waiting for the stack"
for _ in $(seq 1 60); do
curl -sf http://localhost:4300/healthz >/dev/null 2>&1 && curl -sf http://localhost:4200/healthz >/dev/null 2>&1 && break
sleep 2
done
echo "==> Bootstrapping the rehearsal owner"
( cd api && PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
node bootstrap-owner.js [email protected] "P6D Rehearsal Owner" 'RehearsalPass!2026' )
echo "==> Running the rehearsal"
OWNER_EMAIL=[email protected] OWNER_PASSWORD='RehearsalPass!2026' \
node ../infra/scripts/drills/test-phase6d-foundations-rehearsal.js
@@ -0,0 +1,203 @@
#!/usr/bin/env node
// Phase 6D §27 — the ROOT Foundations dress rehearsal. One continuous realistic student
// narrative through ROOT 01 (Computing & Linux Foundations, fully authored this phase) using a
// REAL disposable student Lab end to end: login -> program -> course -> lesson -> guided
// exercise -> real shell interaction -> validated filesystem/process/permission activities ->
// progress persistence -> assessment -> ROOT 02 unlock state, then an instructor narrative
// confirming the resulting learning state is visible. ROOT 02-04 are not rehearsed here since
// they remain unauthored this phase (see docs/PHASE6D_ROOT_FOUNDATIONS.md).
//
// Unlike the ROOT 05+ rehearsals, this one never touches kubectl or asserts anything about
// Kubernetes — ROOT 01 is Linux/shell content, and the assertions below only ever exercise
// real bash/coreutils commands. (The Lab container's own boot sequence still starts k3s before
// signaling ready, same as every other course's Lab — that's an existing image-level fact this
// phase didn't change, not something ROOT 01's content depends on.)
//
// Run via run-phase6d-foundations-rehearsal.sh, which brings up platform/docker-compose.dev.yml
// first.
const assert = require('assert');
const { execSync } = require('child_process');
const { makeSession } = require('../../../platform/test/session-helper');
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
const OWNER_EMAIL = process.env.OWNER_EMAIL;
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
if (!OWNER_EMAIL || !OWNER_PASSWORD) { console.error('Set OWNER_EMAIL and OWNER_PASSWORD'); process.exit(1); }
const RUN_ID = Date.now();
const PROGRAM = 'root-kubernetes-foundations';
let passed = 0, failed = 0;
async function step(name, fn) {
try { await fn(); console.log(` ok — ${name}`); passed++; }
catch (e) { console.error(` FAIL — ${name}\n ${e.stack || e.message}`); failed++; }
}
function moduleFrom(tree, id) { for (const c of tree) { const m = c.modules.find((x) => x.module_id === id); if (m) return m; } return null; }
function courseFrom(tree, id) { return tree.find((c) => c.course_id === id); }
async function main() {
console.log('Phase 6D ROOT Foundations dress rehearsal — real disposable Lab, no mocking\n');
const owner = makeSession(GW);
await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
const { body: created } = await owner.request('POST', '/api/admin/students', {
display_name: 'Rehearsal 6D Student', login_identifier: `p6d-foundations-${RUN_ID}`,
});
const session = makeSession(GW);
let enrollmentId, envId;
await step('student activates credentials and logs in', async () => {
const { status } = await session.request('POST', '/api/student/activate', { token: created.activation_token, password: 'RehearsalPass123!' });
assert.equal(status, 200);
});
await step('an instructor enrolls the student in the ROOT_ program', async () => {
const { status, body } = await owner.request('POST', '/api/admin/enrollments', { student_id: created.student_id, program_id: PROGRAM, status: 'active' });
assert.equal(status, 201);
enrollmentId = body.enrollment_id;
});
await step('resume points at ROOT 01\'s very first activity', async () => {
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
});
await step('student reads ROOT 01 module 1\'s lesson and guided exercise', async () => {
for (const suffix of ['a01-lesson', 'a02-guided-exercise']) {
const { status } = await session.request('POST', `/api/student/activities/root-01-m01-${suffix}/complete`);
assert.equal(status, 200);
}
});
await step('student launches a REAL disposable Lab', async () => {
const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`);
assert.equal(launch.status, 200);
assert.equal(launch.body.status, 'running');
const envRow = (await owner.request('GET', `/api/admin/students/${created.student_id}/environment`)).body.find((e) => e.status === 'running');
envId = envRow.environment_id;
});
let labToken;
await step('student runs the linux-inspect-hardware Lab using REAL shell commands inside the real Lab container', async () => {
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
const mod = moduleFrom(body.courses, 'root-01-m01-how-computers-run-software');
const lab = mod.activities.find((a) => a.scenario_id === 'linux-inspect-hardware');
assert.equal(lab.complete, false);
const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`);
labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1];
const setup = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/setup`);
assert.equal(setup.status, 200);
const containerName = execSync(
`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "SELECT container_name FROM environments WHERE environment_id = '${envId}'"`,
{ cwd: `${__dirname}/../../../platform`, stdio: 'pipe' }
).toString().trim();
// Real bash, run inside the student's actual disposable Lab container — exactly the
// commands the module's lesson/hints teach, not a simulated pass. Written to a script FILE
// on the host and copied in, rather than an inline heredoc, to sidestep JS-template/shell/
// heredoc triple-escaping entirely — a real, reproducible failure hit on the first run of
// this rehearsal (see the fix noted in docs/PHASE6D_ROOT_FOUNDATIONS.md).
const fs = require('fs');
const scriptPath = `/tmp/phase6d-hardware-report-${envId}.sh`;
fs.writeFileSync(scriptPath, [
'#!/bin/bash',
'set -e',
'mkdir -p /srv/hardware-lab',
'CPUS=$(nproc)',
"MEM_MB=$(free -m | awk '/Mem:/ {print $2}')",
'DISK_GB=$(df -BG / | awk \'NR==2{gsub("G","",$2); print $2}\')',
'printf "CPUS=%s\\nMEM_MB=%s\\nDISK_GB=%s\\n" "$CPUS" "$MEM_MB" "$DISK_GB" > /srv/hardware-lab/report.txt',
].join('\n'));
execSync(`docker cp ${scriptPath} ${containerName}:/tmp/hardware-report.sh && docker exec ${containerName} bash /tmp/hardware-report.sh`, { stdio: 'pipe' });
fs.unlinkSync(scriptPath);
const validate = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/validate`);
assert.equal(validate.status, 200);
assert.equal(validate.body.passed, true, `expected the real validation to pass: ${JSON.stringify(validate.body)}`);
const record = await session.request('POST', '/api/progress/record', { scenario_id: 'linux-inspect-hardware', status: 'completed', environment_id: envId });
assert.equal(record.status, 200);
});
await step('student runs the linux-permissions-foundations Lab, including a real permission denial and fix', async () => {
const setup = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-permissions-foundations/setup`);
assert.equal(setup.status, 200);
const containerName = execSync(
`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "SELECT container_name FROM environments WHERE environment_id = '${envId}'"`,
{ cwd: `${__dirname}/../../../platform`, stdio: 'pipe' }
).toString().trim();
// Confirm the real denial exists before fixing it — this is the actual pedagogical point.
let denied = false;
try { execSync(`docker exec ${containerName} su - labuser -c "cat /srv/perm-lab/data.txt"`, { stdio: 'pipe' }); }
catch (e) { denied = /Permission denied/.test(e.stderr ? e.stderr.toString() : ''); }
assert.equal(denied, true, 'expected a REAL permission denial before the fix, not an assumption');
execSync(`docker exec ${containerName} chmod 644 /srv/perm-lab/data.txt`, { stdio: 'pipe' });
const afterFix = execSync(`docker exec ${containerName} su - labuser -c "cat /srv/perm-lab/data.txt"`, { stdio: 'pipe' }).toString().trim();
assert.equal(afterFix, 'sensitive', 'labuser must actually be able to read the file after the real chmod fix');
const validate = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-permissions-foundations/validate`);
assert.equal(validate.status, 200);
assert.equal(validate.body.passed, true, `expected the real validation to pass: ${JSON.stringify(validate.body)}`);
const record = await session.request('POST', '/api/progress/record', { scenario_id: 'linux-permissions-foundations', status: 'completed', environment_id: envId });
assert.equal(record.status, 200);
});
await step('completing the remaining ROOT 01 activities finishes the course and unlocks ROOT 02', async () => {
await session.request('POST', '/api/student/activities/root-01-m01-a04-reflection/complete');
for (const m of ['m02', 'm03', 'm04', 'm05', 'm06']) {
for (const suffix of ['a01-lesson', 'a02-demonstration', 'a03-knowledge-check', 'a04-guided-exercise']) {
await session.request('POST', `/api/student/activities/root-01-${m}-${suffix}/complete`);
}
}
for (const [sid, activity] of [['linux-shell-basics', 'root-01-m02-a06-reflection'], ['linux-filesystem-navigation', 'root-01-m03-a06-reflection'], ['linux-files-and-text', 'root-01-m04-a06-reflection'], ['linux-process-management', 'root-01-m05-a06-reflection']]) {
await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed', environment_id: envId });
await session.request('POST', `/api/student/activities/${activity}/complete`);
}
await session.request('POST', '/api/student/activities/root-01-m06-a06-reflection/complete');
const start = await session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start');
assert.equal(start.status, 201);
for (const s of start.body.scenarios) {
const r = await session.request('POST', '/api/progress/record', { scenario_id: s.scenario_id, status: 'completed', environment_id: envId });
assert.equal(r.status, 200);
}
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(courseFrom(body.courses, 'root-01-computing-linux-foundations').complete, true);
assert.equal(courseFrom(body.courses, 'root-02-linux-administration-networking').locked, false, 'ROOT 02 must unlock now (though it has no content of its own yet)');
});
await step('logout, login — ROOT 01 progress survives all of it', async () => {
await session.request('POST', '/api/logout');
const returning = makeSession(GW);
await returning.request('POST', '/api/student/login', { identifier: `p6d-foundations-${RUN_ID}`, password: 'RehearsalPass123!' });
const { body } = await returning.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(courseFrom(body.courses, 'root-01-computing-linux-foundations').complete, true, 'progress must survive a real logout/login');
});
// ── Instructor narrative — confirming the resulting learning state is visible ───────────
await step('instructor inspects the ROOT 01 module structure and its instructor notes', async () => {
const { status, body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
assert.equal(status, 200);
const mod = moduleFrom(body, 'root-01-m06-users-and-permissions-foundations');
assert.equal(mod.activities.length, 7);
assert.ok(mod.activities[0].instructor_notes, 'instructor must see instructor notes the student view never does');
});
await step('instructor sees this specific student\'s real, completed ROOT 01 progress', async () => {
const { status, body } = await owner.request('GET', `/api/admin/students/${created.student_id}/courses?program_id=${PROGRAM}`);
assert.equal(status, 200);
assert.equal(courseFrom(body, 'root-01-computing-linux-foundations').complete, true);
});
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
}
main().catch((e) => { console.error(e); process.exit(1); });
@@ -0,0 +1,148 @@
-- Phase 6D — ROOT 01: Computing & Linux Foundations. Adds 6 modules/activities under the
-- existing (PLANNED, zero-content) root-01-computing-linux-foundations course row seeded by
-- Phase 6A's 022_course_content.sql. Every lab activity references a NEW scenario file
-- (scenarios/data/linux-*.json) using the existing, generic scenario/validation engine — see
-- docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §1 for why no new activity type or mechanism was
-- needed. Every lesson/demonstration/knowledge_check/guided_exercise/reflection activity
-- references a real content file under content/root/root-01-computing-linux-foundations/.
UPDATE courses SET
status = 'ready',
objectives = '{"Explain what the OS/kernel manages versus an application","Use a Linux shell confidently, including reading exit codes","Navigate the filesystem using absolute and relative paths","Manipulate files and text with pipes and redirection","Inspect and safely terminate a real process","Apply the least-privilege principle to file permissions"}',
updated_at = now()
WHERE course_id = 'root-01-computing-linux-foundations';
INSERT INTO course_modules (module_id, course_id, title, description, sequence, objectives, prerequisites, estimated_duration_minutes, status) VALUES
('root-01-m01-how-computers-run-software', 'root-01-computing-linux-foundations', 'How Computers Run Software',
'Hardware vs. OS vs. application, and the CPU/RAM/storage/process vocabulary the rest of ROOT_ assumes. Target learner: no assumed prior Linux/CLI experience.',
1, '{"Explain the hardware/kernel/application layering","Describe what CPU, RAM, and storage each actually do","Inspect a real machine''s CPU, memory, and disk"}', '{}', 40, 'ready'),
('root-01-m02-linux-and-the-shell', 'root-01-computing-linux-foundations', 'Linux and the Shell',
'What Linux/a distribution/a shell actually are, command structure, and exit codes as the universal success/failure signal.',
2, '{"Distinguish shell, terminal, and distribution","Read and interpret a command''s exit code","Use tab completion, history, and man pages"}', '{root-01-m01-how-computers-run-software}', 45, 'ready'),
('root-01-m03-navigating-the-filesystem', 'root-01-computing-linux-foundations', 'Navigating the Filesystem',
'Absolute vs. relative paths, and what the standard top-level directories are for.',
3, '{"Navigate using absolute and relative paths correctly","Explain the purpose of /etc, /var, /home, /tmp, /usr, /proc","Build a real nested directory structure"}', '{root-01-m02-linux-and-the-shell}', 45, 'ready'),
('root-01-m04-files-and-text', 'root-01-computing-linux-foundations', 'Files and Text',
'File manipulation, reading text, and composing commands with pipes/redirection.',
4, '{"Manipulate files and directories with the core file commands","Filter text with grep","Compose commands using pipes and redirection"}', '{root-01-m03-navigating-the-filesystem}', 45, 'ready'),
('root-01-m05-processes', 'root-01-computing-linux-foundations', 'Processes',
'The process concept, foreground/background, signals, and why this matters for containers later.',
5, '{"Explain what a process and a PID are","Inspect running processes with ps","Terminate a process correctly, verifying the result"}', '{root-01-m04-files-and-text}', 40, 'ready'),
('root-01-m06-users-and-permissions-foundations', 'root-01-computing-linux-foundations', 'Users & Permissions Foundations',
'Ownership, rwx permissions, sudo, and the least-privilege principle this phase introduces for the first time in ROOT_.',
6, '{"Read and set rwx permissions using octal notation","Explain why sudo exists instead of permanent root access","State and apply the least-privilege principle"}', '{root-01-m05-processes}', 45, 'ready')
ON CONFLICT (module_id) DO NOTHING;
INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES
-- M01
('root-01-m01-a01-lesson', 'root-01-m01-how-computers-run-software', 1, 'lesson', 'How Computers Run Software',
'content/root/root-01-computing-linux-foundations/m01-how-computers-run-software/01-lesson-how-computers-run-software.md', NULL, NULL, 'manual_marker', 15, 'ready'),
('root-01-m01-a02-guided-exercise', 'root-01-m01-how-computers-run-software', 2, 'guided_exercise', 'Guided Exercise: Predict Before You Look',
'content/root/root-01-computing-linux-foundations/m01-how-computers-run-software/02-guided-exercise-inspect-hardware-on-paper.md', NULL, NULL, 'manual_marker', 5, 'ready'),
('root-01-m01-a03-lab-inspect-hardware', 'root-01-m01-how-computers-run-software', 3, 'lab', 'Lab: Inspect What This Machine Actually Has',
NULL, 'linux-inspect-hardware', NULL, 'auto_scenario', 10, 'ready'),
('root-01-m01-a04-reflection', 'root-01-m01-how-computers-run-software', 4, 'reflection', 'Reflection: You Just Looked Inside the Box',
'content/root/root-01-computing-linux-foundations/m01-how-computers-run-software/03-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
-- M02
('root-01-m02-a01-lesson', 'root-01-m02-linux-and-the-shell', 1, 'lesson', 'Linux and the Shell',
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/01-lesson-linux-and-the-shell.md', NULL, NULL, 'manual_marker', 15, 'ready'),
('root-01-m02-a02-demonstration', 'root-01-m02-linux-and-the-shell', 2, 'demonstration', 'Demonstration: Watching Exit Codes in Real Time',
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/02-demonstration-exit-codes-in-action.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m02-a03-knowledge-check', 'root-01-m02-linux-and-the-shell', 3, 'knowledge_check', 'Knowledge Check: Linux and the Shell',
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
('root-01-m02-a04-guided-exercise', 'root-01-m02-linux-and-the-shell', 4, 'guided_exercise', 'Guided Exercise: Predict the Exit Code',
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/04-guided-exercise-predicting-exit-codes.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m02-a05-lab-shell-basics', 'root-01-m02-linux-and-the-shell', 5, 'lab', 'Lab: Reading Exit Codes Like an Engineer',
NULL, 'linux-shell-basics', NULL, 'auto_scenario', 10, 'ready'),
('root-01-m02-a06-reflection', 'root-01-m02-linux-and-the-shell', 6, 'reflection', 'Reflection: Exit Codes Are Everywhere After This',
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
-- M03
('root-01-m03-a01-lesson', 'root-01-m03-navigating-the-filesystem', 1, 'lesson', 'Navigating the Filesystem',
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/01-lesson-filesystem-navigation.md', NULL, NULL, 'manual_marker', 15, 'ready'),
('root-01-m03-a02-demonstration', 'root-01-m03-navigating-the-filesystem', 2, 'demonstration', 'Demonstration: The Same Word, Two Different Destinations',
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/02-demonstration-absolute-vs-relative.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m03-a03-knowledge-check', 'root-01-m03-navigating-the-filesystem', 3, 'knowledge_check', 'Knowledge Check: Navigating the Filesystem',
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
('root-01-m03-a04-guided-exercise', 'root-01-m03-navigating-the-filesystem', 4, 'guided_exercise', 'Guided Exercise: Plan a Directory Structure on Paper First',
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/04-guided-exercise-planning-a-structure.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m03-a05-lab-filesystem-navigation', 'root-01-m03-navigating-the-filesystem', 5, 'lab', 'Lab: Build a Real Project Directory Structure',
NULL, 'linux-filesystem-navigation', NULL, 'auto_scenario', 10, 'ready'),
('root-01-m03-a06-reflection', 'root-01-m03-navigating-the-filesystem', 6, 'reflection', 'Reflection: Structure Isn''t Bureaucracy, It''s Communication',
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
-- M04
('root-01-m04-a01-lesson', 'root-01-m04-files-and-text', 1, 'lesson', 'Files and Text',
'content/root/root-01-computing-linux-foundations/m04-files-and-text/01-lesson-files-and-text.md', NULL, NULL, 'manual_marker', 15, 'ready'),
('root-01-m04-a02-demonstration', 'root-01-m04-files-and-text', 2, 'demonstration', 'Demonstration: Filtering a Log Down to What Matters',
'content/root/root-01-computing-linux-foundations/m04-files-and-text/02-demonstration-pipes-and-redirection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m04-a03-knowledge-check', 'root-01-m04-files-and-text', 3, 'knowledge_check', 'Knowledge Check: Files and Text',
'content/root/root-01-computing-linux-foundations/m04-files-and-text/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
('root-01-m04-a04-guided-exercise', 'root-01-m04-files-and-text', 4, 'guided_exercise', 'Guided Exercise: Which Tool Actually Answers the Question?',
'content/root/root-01-computing-linux-foundations/m04-files-and-text/04-guided-exercise-choosing-the-right-tool.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m04-a05-lab-files-and-text', 'root-01-m04-files-and-text', 5, 'lab', 'Lab: Extract the Signal From a Log File',
NULL, 'linux-files-and-text', NULL, 'auto_scenario', 10, 'ready'),
('root-01-m04-a06-reflection', 'root-01-m04-files-and-text', 6, 'reflection', 'Reflection: Filtering Is a Reasoning Skill, Not a Syntax Trick',
'content/root/root-01-computing-linux-foundations/m04-files-and-text/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
-- M05
('root-01-m05-a01-lesson', 'root-01-m05-processes', 1, 'lesson', 'Processes',
'content/root/root-01-computing-linux-foundations/m05-processes/01-lesson-processes.md', NULL, NULL, 'manual_marker', 15, 'ready'),
('root-01-m05-a02-demonstration', 'root-01-m05-processes', 2, 'demonstration', 'Demonstration: Watching a Process Actually Terminate',
'content/root/root-01-computing-linux-foundations/m05-processes/02-demonstration-watching-a-process-die.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m05-a03-knowledge-check', 'root-01-m05-processes', 3, 'knowledge_check', 'Knowledge Check: Processes',
'content/root/root-01-computing-linux-foundations/m05-processes/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
('root-01-m05-a04-guided-exercise', 'root-01-m05-processes', 4, 'guided_exercise', 'Guided Exercise: Choosing the Right Signal',
'content/root/root-01-computing-linux-foundations/m05-processes/04-guided-exercise-signals.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m05-a05-lab-process-management', 'root-01-m05-processes', 5, 'lab', 'Lab: Find and Stop a Runaway Process',
NULL, 'linux-process-management', NULL, 'auto_scenario', 10, 'ready'),
('root-01-m05-a06-reflection', 'root-01-m05-processes', 6, 'reflection', 'Reflection: You Just Stopped a Real Process, Not a Simulation',
'content/root/root-01-computing-linux-foundations/m05-processes/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
-- M06
('root-01-m06-a01-lesson', 'root-01-m06-users-and-permissions-foundations', 1, 'lesson', 'Users and Permissions Foundations',
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/01-lesson-users-and-permissions.md', NULL, NULL, 'manual_marker', 15, 'ready'),
('root-01-m06-a02-demonstration', 'root-01-m06-users-and-permissions-foundations', 2, 'demonstration', 'Demonstration: A Real Permission Denial, and the Real Fix',
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/02-demonstration-permission-denied.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m06-a03-knowledge-check', 'root-01-m06-users-and-permissions-foundations', 3, 'knowledge_check', 'Knowledge Check: Users and Permissions Foundations',
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
('root-01-m06-a04-guided-exercise', 'root-01-m06-users-and-permissions-foundations', 4, 'guided_exercise', 'Guided Exercise: Pick the Minimal Permission, Not the Easy One',
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/04-guided-exercise-least-privilege.md', NULL, NULL, 'manual_marker', 8, 'ready'),
('root-01-m06-a05-lab-permissions', 'root-01-m06-users-and-permissions-foundations', 5, 'lab', 'Lab: Fix a Real Permission Denial',
NULL, 'linux-permissions-foundations', NULL, 'auto_scenario', 10, 'ready'),
('root-01-m06-a06-reflection', 'root-01-m06-users-and-permissions-foundations', 6, 'reflection', 'Reflection: Least Privilege Is a Habit, Not a One-Time Fix',
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready')
ON CONFLICT (activity_id) DO NOTHING;
-- ═══════════════════════════════════════════════════════════════════════════════════════════
-- ROOT 01 course assessment — mixes real state-validation tasks (the same scenarios already
-- used as labs above, plus two more) with knowledge-recall MCQs, via the existing Phase 3B
-- assessment engine. No Kubernetes scenario is force-fit into this non-Kubernetes assessment.
-- ═══════════════════════════════════════════════════════════════════════════════════════════
INSERT INTO assessment_definitions (assessment_definition_id, type, title, description, duration_minutes, hint_policy, pool_spec, completion_rule)
VALUES (
'rootnode-linux-foundations-assessment', 'exam', 'ROOT 01 Linux Foundations Assessment',
'A timed check across shell fluency, filesystem navigation, process management, and least-privilege permissions — combining real state-validation tasks with conceptual reasoning questions.',
30, 'disabled',
'{"required": ["linux-shell-basics", "linux-permissions-foundations"],
"random": [
{"count":2,"from":["linux-inspect-hardware","linux-filesystem-navigation","linux-files-and-text","linux-process-management"]},
{"count":2,"from":["linux-exit-codes-mcq","linux-permissions-mcq"]}
]}',
'{"type":"all_scenarios_passed"}'
) ON CONFLICT (assessment_definition_id) DO NOTHING;
INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES
('root-01-m06-a07-assessment', 'root-01-m06-users-and-permissions-foundations', 7, 'assessment', 'ROOT 01 Linux Foundations Assessment', NULL, NULL, 'rootnode-linux-foundations-assessment', 'auto_assessment', 30, 'ready')
ON CONFLICT (activity_id) DO NOTHING;
-- ═══════════════════════════════════════════════════════════════════════════════════════════
-- Step 19 — real ROOT 01→02→03→04→05 course prerequisite chain, server-side (courses.prerequisites),
-- not UI-hidden alone. root-02/03/04 remain PLANNED with zero activities this phase, so per
-- docs/COURSE_CONTENT_MODEL.md §5's "zero activities is vacuously satisfied" rule, this chain
-- has no practical locking effect YET on 02/03/04 or on 05 (which already only real-gates once
-- root-04 is eventually authored) — it correctly encodes the INTENDED sequence now, so a future
-- phase authoring ROOT 02-04 doesn't need to reopen this decision, without breaking any current
-- Kubernetes-track student's path today (root-05's real prerequisite content, root-04, is still
-- honestly empty).
-- ═══════════════════════════════════════════════════════════════════════════════════════════
UPDATE courses SET prerequisites = '{root-01-computing-linux-foundations}', updated_at = now() WHERE course_id = 'root-02-linux-administration-networking';
UPDATE courses SET prerequisites = '{root-02-linux-administration-networking}', updated_at = now() WHERE course_id = 'root-03-git-collaborative-development';
UPDATE courses SET prerequisites = '{root-03-git-collaborative-development}', updated_at = now() WHERE course_id = 'root-04-containers-and-docker';
UPDATE courses SET prerequisites = '{root-04-containers-and-docker}', updated_at = now() WHERE course_id = 'root-05-kubernetes-foundations';
+9 -6
View File
@@ -122,7 +122,7 @@ async function main() {
assert.equal(status, 200);
tree = body.courses;
const root05 = courseById(tree, 'root-05-kubernetes-foundations');
assert.equal(root05.locked, false); // the first real course is never locked
assert.equal(root05.locked, false); // root-04 (its real Phase 6D prerequisite) has zero activities — vacuously satisfied
assert.equal(root05.complete, false);
const root06 = courseById(tree, 'root-06-kubernetes-application-workloads');
assert.equal(root06.locked, true, 'root-06 must be locked until root-05 is complete');
@@ -130,11 +130,14 @@ async function main() {
await test('resume points at the first activity of the first unlocked course', async () => {
const { body } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(body.resume.course_id, 'root-05-kubernetes-foundations');
// Phase 6B added root-05-m00 (the mental-model module) as the new front door, sequenced
// before the pre-existing bare-lab bank — see docs/PHASE6B_ROOT_CURRICULUM_EXPANSION.md.
assert.equal(body.resume.module_id, 'root-05-m00-kubernetes-mental-model');
assert.equal(body.resume.activity_id, 'root-05-m00-a01-lesson-mental-model');
// Phase 6D authored ROOT 01 (Computing & Linux Foundations) as a real, non-PLANNED course
// sequenced before ROOT 05 — it is now genuinely the program's front door, not ROOT 05's
// own gold module (see docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's original intent —
// "resume lands on the first REAL content, not a bare-lab bank" — is still what's being
// proven, just against the current front door.
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
});
await test('a student cannot mark a lab activity complete manually — only real scenario completion counts', async () => {
+6 -2
View File
@@ -126,9 +126,13 @@ async function main() {
assert.equal(moduleById(body.courses, 'root-10-m03-troubleshooting-framework').locked, true, 'must stay locked behind the gold module even though it is a different course-level dependency chain');
});
await test('resume points at ROOT 05\'s new mental-model module, not the old bare-lab bank', async () => {
await test('resume points at ROOT 01 (Phase 6D\'s new front door), not ROOT 05\'s old bare-lab bank', async () => {
// Phase 6D authored ROOT 01 as real, non-PLANNED content sequenced before ROOT 05 — it is
// now genuinely the program's front door (docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's
// original intent — resume must never land on a bare-lab bank when real front-door content
// exists — is unchanged, just against the current front door.
const { body } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(body.resume.module_id, 'root-05-m00-kubernetes-mental-model');
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
});
await test('lesson/knowledge_check completion works identically in a brand-new module (root-05-m00)', async () => {
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env node
// Phase 6D — ROOT Foundations acceptance test. Covers what this phase actually built: ROOT 01
// (Computing & Linux Foundations, fully authored) and the real ROOT 01->02->03->04->05
// prerequisite chain. ROOT 02-04 remain PLANNED (architecture designed in
// docs/ROOT_FOUNDATIONS_ARCHITECTURE.md, not yet authored — see
// docs/PHASE6D_ROOT_FOUNDATIONS.md for why), so this suite does not assert content that
// doesn't exist. Same conventions as e2e-phase6a/6b/6c.js: assert-based, no framework, live
// docker-compose.dev.yml stack, no real Lab container launched here (see
// infra/scripts/drills/test-phase6d-foundations-rehearsal.js for the real-Lab counterpart).
//
// Usage: cd platform && docker compose -f docker-compose.dev.yml up -d
// (cd api && PLATFORM_DB_URL=... node bootstrap-owner.js OWNER_EMAIL "Owner" OWNER_PASSWORD)
// OWNER_EMAIL=... OWNER_PASSWORD=... node test/e2e-phase6d.js
const assert = require('assert');
const { execSync } = require('child_process');
const { makeSession } = require('./session-helper');
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
const OWNER_EMAIL = process.env.OWNER_EMAIL;
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
if (!OWNER_EMAIL || !OWNER_PASSWORD) {
console.error('Set OWNER_EMAIL and OWNER_PASSWORD (bootstrap one first with platform/api/bootstrap-owner.js)');
process.exit(1);
}
const RUN_ID = Date.now();
const PROGRAM = 'root-kubernetes-foundations';
const ROOT01_MODULES = ['root-01-m01-how-computers-run-software', 'root-01-m02-linux-and-the-shell', 'root-01-m03-navigating-the-filesystem', 'root-01-m04-files-and-text', 'root-01-m05-processes', 'root-01-m06-users-and-permissions-foundations'];
const ROOT01_LAB_SCENARIOS = ['linux-inspect-hardware', 'linux-shell-basics', 'linux-filesystem-navigation', 'linux-files-and-text', 'linux-process-management', 'linux-permissions-foundations'];
let passed = 0, failed = 0;
async function test(name, fn) {
try { await fn(); console.log(` ok — ${name}`); passed++; }
catch (e) { console.error(` FAIL — ${name}\n ${e.message}`); failed++; }
}
async function completeScenario(session, scenarioId) {
const { status } = await session.request('POST', '/api/progress/record', { scenario_id: scenarioId, status: 'completed' });
assert.equal(status, 200, `completing ${scenarioId} should 200`);
}
async function completeActivity(session, id) {
const { status, body } = await session.request('POST', `/api/student/activities/${id}/complete`);
assert.equal(status, 200, `completing ${id} should 200: ${JSON.stringify(body)}`);
}
function courseById(courses, id) { return courses.find((c) => c.course_id === id); }
function moduleById(courses, id) { for (const c of courses) { const m = c.modules.find((x) => x.module_id === id); if (m) return m; } return null; }
function findActivity(courses, id) { for (const c of courses) for (const m of c.modules) { const a = m.activities.find((x) => x.activity_id === id); if (a) return a; } return null; }
function psql(sql) {
return execSync(`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "${sql}"`, { cwd: `${__dirname}/..`, stdio: 'pipe' }).toString().trim();
}
async function newStudent(owner, tag) {
const { body: create } = await owner.request('POST', '/api/admin/students', { display_name: `E2E-6D-${tag}-${RUN_ID}`, login_identifier: `e2e6d-${tag}-${RUN_ID}` });
const s = makeSession(GW);
await s.request('POST', '/api/student/activate', { token: create.activation_token, password: `${tag}Password123!` });
const { body: enroll } = await owner.request('POST', '/api/admin/enrollments', { student_id: create.student_id, program_id: PROGRAM, status: 'active' });
return { session: s, studentId: create.student_id, enrollmentId: enroll.enrollment_id };
}
async function main() {
console.log('Phase 6D ROOT Foundations acceptance test\n');
const owner = makeSession(GW);
await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
let structure;
// ── 1: ROOT 01 hierarchy ─────────────────────────────────────────────────────────────────
await test('ROOT 01 has its 6 modules, in sequence, all READY', async () => {
const { status, body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
assert.equal(status, 200);
structure = body;
const course = courseById(structure, 'root-01-computing-linux-foundations');
assert.equal(course.status, 'ready');
assert.deepEqual(course.modules.map((m) => m.module_id), ROOT01_MODULES);
for (const m of course.modules) assert.equal(m.status, 'ready', `${m.module_id} must be ready`);
});
// ── 2/3/4: ROOT 02/03/04 hierarchy — honestly PLANNED, zero modules, not padded ──────────
await test('ROOT 02/03/04 are honestly PLANNED with zero modules — not padded to look further along than they are', async () => {
for (const id of ['root-02-linux-administration-networking', 'root-03-git-collaborative-development', 'root-04-containers-and-docker']) {
const course = courseById(structure, id);
assert.equal(course.status, 'planned', `${id} must stay honestly planned`);
assert.equal(course.modules.length, 0, `${id} must have zero modules — no fabricated content`);
}
});
// ── 5/6: activity ordering + content actually loads ──────────────────────────────────────
await test('every ROOT 01 module has strictly ascending activity sequence and real lesson content', async () => {
for (const modId of ROOT01_MODULES) {
const mod = moduleById(structure, modId);
for (let i = 1; i < mod.activities.length; i++) assert.ok(mod.activities[i].sequence > mod.activities[i - 1].sequence, `${modId} activity ordering broken`);
const lesson = mod.activities.find((a) => a.activity_type === 'lesson');
assert.ok(lesson.body && lesson.body.length > 300, `${modId}'s lesson must have real content`);
}
});
// ── A real self-paced student walking ROOT 01 end to end ────────────────────────────────
const casey = await newStudent(owner, 'Casey');
await test('resume points at ROOT 01 module 1 activity 1, the very first activity in the whole program', async () => {
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
});
// ── 16: direct API cannot bypass — a lab activity cannot be manually marked complete ─────
await test('a lab activity cannot be marked complete manually — only real scenario completion counts', async () => {
const { status, body } = await casey.session.request('POST', '/api/student/activities/root-01-m01-a03-lab-inspect-hardware/complete');
assert.equal(status, 409);
assert.equal(body.error, 'not_manually_completable');
});
// ── 8/9/10/11: lesson/practical/knowledge-check/assessment completion, walked for real ──
await test('walking ROOT 01 module 1 through the real API completes it', async () => {
await completeActivity(casey.session, 'root-01-m01-a01-lesson');
await completeActivity(casey.session, 'root-01-m01-a02-guided-exercise');
await completeScenario(casey.session, 'linux-inspect-hardware');
await completeActivity(casey.session, 'root-01-m01-a04-reflection');
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
assert.equal(moduleById(body.courses, 'root-01-m01-how-computers-run-software').complete, true);
});
await test('completing every ROOT 01 module and its assessment marks the whole course complete', async () => {
for (const m of ['m02', 'm03', 'm04', 'm05', 'm06']) {
await completeActivity(casey.session, `root-01-${m}-a01-lesson`);
await completeActivity(casey.session, `root-01-${m}-a02-demonstration`);
await completeActivity(casey.session, `root-01-${m}-a03-knowledge-check`);
await completeActivity(casey.session, `root-01-${m}-a04-guided-exercise`);
}
for (const [sid, activity] of [['linux-shell-basics', 'root-01-m02-a06-reflection'], ['linux-filesystem-navigation', 'root-01-m03-a06-reflection'], ['linux-files-and-text', 'root-01-m04-a06-reflection'], ['linux-process-management', 'root-01-m05-a06-reflection']]) {
await completeScenario(casey.session, sid);
await completeActivity(casey.session, activity);
}
await completeScenario(casey.session, 'linux-permissions-foundations');
await completeActivity(casey.session, 'root-01-m06-a06-reflection');
const start = await casey.session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start');
assert.equal(start.status, 201);
for (const s of start.body.scenarios) await completeScenario(casey.session, s.scenario_id);
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
const course = courseById(body.courses, 'root-01-computing-linux-foundations');
assert.equal(course.complete, true);
assert.equal(course.percent_complete, 100);
});
// ── 12/15: ROOT 01 unlocks ROOT 02 at the course level, for THIS student ─────────────────
await test('completing ROOT 01 unlocks ROOT 02 for this student', async () => {
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').locked, false);
});
// ── A second, fresh student proves ROOT 02 stays LOCKED without ROOT 01 ─────────────────
const bob = await newStudent(owner, 'Bob');
await test('ROOT 02 is locked for a freshly-enrolled student who has not touched ROOT 01', async () => {
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').locked, false, 'ROOT 01 itself is never locked — it is the first course');
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').locked, true, 'ROOT 02 must be locked until ROOT 01 is complete');
});
await test('ROOT 03/04/05 stay unlocked (vacuously satisfied) since their own prerequisite courses have zero content yet', async () => {
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, false);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, false);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, false);
});
// ── 26/27: existing ROOT 05+ and Capstone state is untouched by this phase ──────────────
await test('ROOT 06 stays locked behind ROOT 05 exactly as before this phase (unaffected by the new 01-04 chain)', async () => {
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-06-kubernetes-application-workloads').locked, true);
});
// ── 17: optional activities still behave correctly elsewhere in the curriculum ──────────
await test('the pre-existing is_required rule for other courses is unaffected by this phase', async () => {
const mod = moduleById(structure, 'root-10-m01-health-probes-and-troubleshooting');
assert.equal(mod.activities.filter((a) => a.is_required === false).length, 1, 'Phase 6C\'s optional readiness-probe lab must still be exactly 1');
});
// ── 18/19: self-paced + instructor rendering, and instructor-note exclusion ─────────────
await test('self-paced ROOT 01 content never defers to a live instructor', async () => {
for (const modId of ROOT01_MODULES) {
const mod = moduleById(structure, modId);
for (const a of mod.activities) {
if (!a.body) continue;
assert.ok(!/ask your instructor/i.test(a.body) && !/your instructor will explain/i.test(a.body), `${a.activity_id} must not defer explanation to a live instructor`);
}
}
});
await test('instructor notes on ROOT 01 content are visible to instructors and never to students', async () => {
const lesson = findActivity(structure, 'root-01-m06-a01-lesson');
assert.ok(lesson.instructor_notes && lesson.instructor_notes.length > 0);
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
const studentLesson = findActivity(body.courses, 'root-01-m06-a01-lesson');
assert.equal(studentLesson.instructor_notes, undefined);
});
// ── 20: enrollment entitlement / RBAC ────────────────────────────────────────────────────
const { body: strayInstructor } = await owner.request('POST', '/api/admin/instructors', { email: `e2e6d-stray-${RUN_ID}@example.com`, display_name: 'Stray Instructor 6D', role: 'instructor', password: 'StrayInstructorPass6D!' });
const stray = makeSession(GW);
await stray.request('POST', '/api/auth/instructor/login', { email: `e2e6d-stray-${RUN_ID}@example.com`, password: 'StrayInstructorPass6D!' });
await test('a plain instructor with no relationship to the student cannot see ROOT 01 progress', async () => {
const { status } = await stray.request('GET', `/api/admin/students/${casey.studentId}/courses?program_id=${PROGRAM}`);
assert.equal(status, 403);
});
await test('owner/admin can see any self-paced student\'s ROOT 01 progress', async () => {
const { status, body } = await owner.request('GET', `/api/admin/students/${casey.studentId}/courses?program_id=${PROGRAM}`);
assert.equal(status, 200);
assert.equal(courseById(body, 'root-01-computing-linux-foundations').complete, true);
});
// ── 21/23: persistence + cross-student isolation ────────────────────────────────────────
await test('ROOT 01 progress survives logout and a fresh login', async () => {
await casey.session.request('POST', '/api/logout');
const returning = makeSession(GW);
await returning.request('POST', '/api/student/login', { identifier: `e2e6d-Casey-${RUN_ID}`, password: 'CaseyPassword123!' });
const { body } = await returning.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').complete, true);
});
await test('a second student sees independent, empty ROOT 01 progress (cross-student isolation)', async () => {
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').complete, false);
});
// ── 25: content version stability ───────────────────────────────────────────────────────
await test('bumping a ROOT 01 module\'s version does not drop its content', async () => {
psql(`UPDATE course_modules SET version = version + 1 WHERE module_id = 'root-01-m06-users-and-permissions-foundations'`);
const { body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
const mod = moduleById(body, 'root-01-m06-users-and-permissions-foundations');
assert.equal(mod.activities.length, 7, 'a version bump must never drop activities');
assert.ok(mod.activities[0].body && mod.activities[0].body.length > 300);
});
// ── content validation as part of this suite ────────────────────────────────────────────
await test('infra/scripts/validate-course-content.js reports zero errors against the current database', () => {
execSync('node infra/scripts/validate-course-content.js', { cwd: `${__dirname}/../..`, stdio: 'pipe' });
});
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);
}
main().catch((e) => { console.error('FATAL', e); process.exit(1); });
+22
View File
@@ -0,0 +1,22 @@
{
"id": "linux-exit-codes-mcq",
"title": "Reading Exit Code Conventions",
"category": "Linux Fundamentals",
"isolation_type": "shared_namespace",
"difficulty": "Easy",
"type": "mcq",
"weight": 3,
"description": "## Reading Exit Code Conventions\n\nYou run `grep pattern file.txt` and it exits with code `1`, printing no error message at all. What actually happened?",
"options": [
{ "id": "a", "text": "The file doesn't exist" },
{ "id": "b", "text": "grep found no lines matching the pattern — this is grep's normal way of reporting \"no match,\" not an error" },
{ "id": "c", "text": "You don't have permission to read the file" },
{ "id": "d", "text": "The command is broken and needs to be reinstalled" }
],
"correct_option": "b",
"explanation": "grep's exit code means \"did I find at least one match,\" not \"did something go wrong.\" Exit 1 with no error message is grep's normal, expected way of reporting zero matches — a very common early surprise, since silence plus a non-zero code doesn't always mean a real failure occurred.",
"hints": [],
"setup_commands": [],
"default_namespace": "default",
"teardown_commands": []
}
+31
View File
@@ -0,0 +1,31 @@
{
"id": "linux-files-and-text",
"title": "Extract the Signal From a Log File",
"category": "Linux Fundamentals",
"isolation_type": "shared_namespace",
"difficulty": "Easy",
"type": "task",
"weight": 4,
"description": "## Extract the Signal From a Log File\n\nA real application log (`/srv/text-lab/app.log`) has been generated for you, mixing `INFO` and `ERROR` lines. Reading raw logs line-by-line doesn't scale — filtering does.\n\n**Your task:**\n\n1. Use `grep` to extract only the `ERROR` lines from `/srv/text-lab/app.log`\n2. Redirect the result into a new file: `/srv/text-lab/errors.txt`\n3. Count how many ERROR lines there were and save the count to `/srv/text-lab/error-count.txt`",
"hints": [
{
"title": "Filtering and redirecting",
"body": "`grep PATTERN file > newfile` writes only matching lines to a new file. Pipe to `wc -l` to count them.",
"command": "grep ERROR /srv/text-lab/app.log > /srv/text-lab/errors.txt\ngrep -c ERROR /srv/text-lab/app.log > /srv/text-lab/error-count.txt"
}
],
"setup_commands": [
{ "command": "mkdir -p /srv/text-lab && printf 'INFO: boot ok\\nERROR: disk full\\nINFO: retry\\nERROR: timeout\\nINFO: done\\n' > /srv/text-lab/app.log" }
],
"validation": {
"commands": [
{ "description": "errors.txt contains only ERROR lines", "command": "grep -vc ERROR /srv/text-lab/errors.txt || true", "expected_output": "0", "match": "exact" },
{ "description": "errors.txt has exactly the 2 real ERROR lines", "command": "wc -l < /srv/text-lab/errors.txt | tr -d ' '", "expected_output": "2", "match": "exact" },
{ "description": "error-count.txt records the count", "command": "cat /srv/text-lab/error-count.txt", "expected_output": "2", "match": "exact" }
]
},
"default_namespace": "default",
"teardown_commands": [
{ "command": "rm -rf /srv/text-lab" }
]
}
@@ -0,0 +1,32 @@
{
"id": "linux-filesystem-navigation",
"title": "Build a Real Project Directory Structure",
"category": "Linux Fundamentals",
"isolation_type": "shared_namespace",
"difficulty": "Easy",
"type": "task",
"weight": 3,
"description": "## Build a Real Project Directory Structure\n\nEvery real codebase has a directory structure that means something. Practice building and navigating one.\n\n**Your task:**\n\nUnder `/srv/fs-lab/`, create:\n\n1. A directory tree: `project/src`, `project/docs`, `project/tests`\n2. A hidden file at `project/.config` containing exactly the text `ready`\n3. Navigate into `project/src` using a RELATIVE path from `/srv/fs-lab` (not an absolute path) to confirm you understand `cd project/src` vs `cd /srv/fs-lab/project/src`",
"hints": [
{
"title": "Creating the tree in one command",
"body": "`mkdir -p` creates every intermediate directory in one call, and brace expansion lets you create three siblings at once.",
"command": "mkdir -p /srv/fs-lab/project/{src,docs,tests}\necho -n 'ready' > /srv/fs-lab/project/.config"
}
],
"setup_commands": [
{ "command": "mkdir -p /srv/fs-lab" }
],
"validation": {
"commands": [
{ "description": "project/src exists", "command": "[ -d /srv/fs-lab/project/src ] && echo yes", "expected_output": "yes", "match": "exact" },
{ "description": "project/docs exists", "command": "[ -d /srv/fs-lab/project/docs ] && echo yes", "expected_output": "yes", "match": "exact" },
{ "description": "project/tests exists", "command": "[ -d /srv/fs-lab/project/tests ] && echo yes", "expected_output": "yes", "match": "exact" },
{ "description": "hidden .config file has the right content", "command": "cat /srv/fs-lab/project/.config", "expected_output": "ready", "match": "exact" }
]
},
"default_namespace": "default",
"teardown_commands": [
{ "command": "rm -rf /srv/fs-lab" }
]
}
@@ -0,0 +1,36 @@
{
"id": "linux-inspect-hardware",
"title": "Inspect What This Machine Actually Has",
"category": "Linux Fundamentals",
"isolation_type": "shared_namespace",
"difficulty": "Easy",
"type": "task",
"weight": 3,
"description": "## Inspect What This Machine Actually Has\n\nThe kernel manages CPU, memory, and storage on behalf of every process running here — before you go further, look at what's actually available.\n\n**Your task:**\n\n1. Find the number of CPUs available (`nproc`)\n2. Find the total memory in MB (`free -m`)\n3. Find the size of the root filesystem in GB (`df -BG /`)\n4. Create a file at `/srv/hardware-lab/report.txt` with exactly three lines in this format (replace `<n>` with the real numbers you found):\n\n```\nCPUS=<n>\nMEM_MB=<n>\nDISK_GB=<n>\n```",
"hints": [
{
"title": "Getting each number",
"body": "`nproc` alone prints the CPU count. `free -m` prints a table — the second column of the `Mem:` row is total memory in MB. `df -BG /` prints a table — the second column of the second row is the root filesystem size in GB (the number, not including the trailing G).",
"command": "nproc; free -m; df -BG /"
},
{
"title": "Writing the report",
"body": "Use a heredoc or several `echo` lines with `>>` to build the file.",
"command": "mkdir -p /srv/hardware-lab && cat <<EOF > /srv/hardware-lab/report.txt\nCPUS=$(nproc)\nMEM_MB=$(free -m | awk '/Mem:/ {print $2}')\nDISK_GB=$(df -BG / | awk 'NR==2{gsub(\"G\",\"\",$2); print $2}')\nEOF"
}
],
"setup_commands": [
{ "command": "mkdir -p /srv/hardware-lab" }
],
"validation": {
"commands": [
{ "description": "report.txt exists and has a CPUS line", "command": "grep -E '^CPUS=[0-9]+$' /srv/hardware-lab/report.txt", "expected_output": "^CPUS=[0-9]+$", "match": "regex" },
{ "description": "report.txt has a MEM_MB line", "command": "grep -E '^MEM_MB=[0-9]+$' /srv/hardware-lab/report.txt", "expected_output": "^MEM_MB=[0-9]+$", "match": "regex" },
{ "description": "report.txt has a DISK_GB line", "command": "grep -E '^DISK_GB=[0-9]+$' /srv/hardware-lab/report.txt", "expected_output": "^DISK_GB=[0-9]+$", "match": "regex" }
]
},
"default_namespace": "default",
"teardown_commands": [
{ "command": "rm -rf /srv/hardware-lab" }
]
}
@@ -0,0 +1,37 @@
{
"id": "linux-permissions-foundations",
"title": "Fix a Real Permission Denial",
"category": "Linux Fundamentals",
"isolation_type": "shared_namespace",
"difficulty": "Easy",
"type": "task",
"weight": 4,
"description": "## Fix a Real Permission Denial\n\nA second user, `labuser`, already exists on this system. A file at `/srv/perm-lab/data.txt` currently has permissions `600` (owner read/write only) — `labuser` cannot read it. Confirm the denial is real, then fix it correctly.\n\n**Your task:**\n\n1. As `labuser`, try to read `/srv/perm-lab/data.txt` and see the permission denial for yourself: `su - labuser -c \"cat /srv/perm-lab/data.txt\"`\n2. Change the file's permissions so it's readable by everyone but still only writable by its owner (this is exactly what `644` means)\n3. Confirm `labuser` can now read it",
"hints": [
{
"title": "Reading permissions as a number",
"body": "Each of the three permission groups (owner/group/other) is one octal digit: read=4, write=2, execute=1, summed. `644` = owner read+write (6), group read-only (4), other read-only (4).",
"command": "stat -c '%a %U' /srv/perm-lab/data.txt"
},
{
"title": "Fixing it",
"body": "`chmod 644 <file>` sets exactly that permission pattern.",
"command": "chmod 644 /srv/perm-lab/data.txt"
}
],
"setup_commands": [
{ "command": "id labuser >/dev/null 2>&1 || useradd -m labuser" },
{ "command": "mkdir -p /srv/perm-lab && chmod 755 /srv/perm-lab" },
{ "command": "echo sensitive > /srv/perm-lab/data.txt && chmod 600 /srv/perm-lab/data.txt && chown root:root /srv/perm-lab/data.txt" }
],
"validation": {
"commands": [
{ "description": "data.txt is now exactly 644", "command": "stat -c '%a' /srv/perm-lab/data.txt", "expected_output": "644", "match": "exact" },
{ "description": "labuser can actually read it now", "command": "su - labuser -c 'cat /srv/perm-lab/data.txt' 2>&1", "expected_output": "sensitive", "match": "exact" }
]
},
"default_namespace": "default",
"teardown_commands": [
{ "command": "rm -rf /srv/perm-lab" }
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"id": "linux-permissions-mcq",
"title": "Choosing the Minimal Permission",
"category": "Linux Fundamentals",
"isolation_type": "shared_namespace",
"difficulty": "Easy",
"type": "mcq",
"weight": 3,
"description": "## Choosing the Minimal Permission\n\nA private SSH key file should be readable and writable by its owner only — nobody else should be able to read, write, or execute it. Which `chmod` octal value is correct?",
"options": [
{ "id": "a", "text": "777" },
{ "id": "b", "text": "644" },
{ "id": "c", "text": "600" },
{ "id": "d", "text": "444" }
],
"correct_option": "c",
"explanation": "600 grants the owner read+write (4+2=6) and nothing at all to group or other (0, 0) — the minimum needed, and the actual permission SSH itself enforces for private key files. 644 incorrectly leaves the file readable by everyone; 777 grants full access to everyone, the opposite of least privilege.",
"hints": [],
"setup_commands": [],
"default_namespace": "default",
"teardown_commands": []
}
@@ -0,0 +1,34 @@
{
"id": "linux-process-management",
"title": "Find and Stop a Runaway Process",
"category": "Linux Fundamentals",
"isolation_type": "shared_namespace",
"difficulty": "Easy",
"type": "task",
"weight": 4,
"description": "## Find and Stop a Runaway Process\n\nA background process has been started for you and its PID recorded at `/srv/proc-lab/target.pid`. Every process in Kubernetes and Docker is, underneath, exactly this: a PID the kernel is tracking.\n\n**Your task:**\n\n1. Confirm the process is currently running (`kill -0 <pid>` succeeds silently if a process exists)\n2. Send it a termination signal: `kill <pid>` (using the PID recorded in the file, not a hardcoded number — it will be different every time)\n3. Confirm it is actually gone afterward",
"hints": [
{
"title": "Reading the PID and checking it's alive",
"body": "`kill -0` sends no real signal — it only checks whether the process exists, which is why it's the standard way to test \"is this PID still running\" without side effects.",
"command": "cat /srv/proc-lab/target.pid\nkill -0 $(cat /srv/proc-lab/target.pid) && echo RUNNING"
},
{
"title": "Terminating it",
"body": "A plain `kill <pid>` sends SIGTERM, the polite \"please shut down\" signal — the correct first choice before ever reaching for `kill -9`.",
"command": "kill $(cat /srv/proc-lab/target.pid)"
}
],
"setup_commands": [
{ "command": "mkdir -p /srv/proc-lab && (sleep 900 & echo $! > /srv/proc-lab/target.pid)" }
],
"validation": {
"commands": [
{ "description": "the target process has actually been terminated", "command": "kill -0 $(cat /srv/proc-lab/target.pid) 2>/dev/null && echo RUNNING || echo GONE", "expected_output": "GONE", "match": "exact" }
]
},
"default_namespace": "default",
"teardown_commands": [
{ "command": "kill -9 $(cat /srv/proc-lab/target.pid) 2>/dev/null; rm -rf /srv/proc-lab" }
]
}
+30
View File
@@ -0,0 +1,30 @@
{
"id": "linux-shell-basics",
"title": "Reading Exit Codes Like an Engineer",
"category": "Linux Fundamentals",
"isolation_type": "shared_namespace",
"difficulty": "Easy",
"type": "task",
"weight": 3,
"description": "## Reading Exit Codes Like an Engineer\n\nEvery command you run returns an exit code — `0` means success, anything else means failure. Scripts, CI pipelines, and Kubernetes probes all rely on this exact convention.\n\n**Your task:**\n\n1. Run a command that succeeds (e.g. `true`, or `ls` on a directory that exists) and save its exit code to `/srv/shell-lab/success-exit-code.txt`\n2. Run a command that fails (e.g. `ls` on a path that does NOT exist) and save its exit code to `/srv/shell-lab/fail-exit-code.txt`\n3. Look up the manual page for `ls` (`man ls`) and find one option you didn't know about",
"hints": [
{
"title": "Capturing an exit code",
"body": "The special variable `$?` holds the exit code of the LAST command that ran — capture it immediately, before running anything else.",
"command": "mkdir -p /srv/shell-lab\ntrue\necho $? > /srv/shell-lab/success-exit-code.txt\nls /this-path-does-not-exist 2>/dev/null\necho $? > /srv/shell-lab/fail-exit-code.txt"
}
],
"setup_commands": [
{ "command": "mkdir -p /srv/shell-lab" }
],
"validation": {
"commands": [
{ "description": "success-exit-code.txt records 0", "command": "cat /srv/shell-lab/success-exit-code.txt", "expected_output": "0", "match": "exact" },
{ "description": "fail-exit-code.txt records a non-zero code", "command": "cat /srv/shell-lab/fail-exit-code.txt", "expected_output": "^[1-9][0-9]*$", "match": "regex" }
]
},
"default_namespace": "default",
"teardown_commands": [
{ "command": "rm -rf /srv/shell-lab" }
]
}
+22
View File
@@ -58,6 +58,28 @@ for i in $(seq 1 60); do
done
OK "k3s API server is up"
# ── 1b. Start a nested Docker daemon (ROOT 04) ───────────────────────────────
# A second, independent runtime for Docker/container-fundamentals labs — see
# docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §3. vfs storage driver deliberately avoids nesting
# overlay-on-overlay under this container's own overlay filesystem. Best-effort and
# non-blocking: a Docker daemon failure must never break the Kubernetes-only Labs that are the
# vast majority of usage, so this never calls `exit 1` — only a log line.
if command -v dockerd >/dev/null 2>&1; then
LOG "Starting nested Docker daemon (ROOT 04 labs)..."
dockerd --storage-driver=vfs --host=unix:///var/run/docker.sock &>/var/log/dockerd.log &
DOCKERD_PID=$!
for i in $(seq 1 30); do
if docker version >/dev/null 2>&1; then
OK "Docker daemon is up"
break
fi
sleep 1
if [ $i -eq 30 ]; then
LOG "⚠ Docker daemon did not become ready in time — ROOT 04 labs will be unavailable this session, everything else is unaffected"
fi
done
fi
# Symlink kubeconfig to the standard location for convenience
mkdir -p /root/.kube
cp "$KUBECONFIG_PATH" /root/.kube/config