Skip to content

Commit a12e39f

Browse files
author
Kevin Hopper
committed
Phase 5 v0.1 — Maker Lab Advanced bundle (JupyterHub for 9+)
Ships the Phase 5 foundation per the plan's 'future track' language: the bundle is installable and the skill teaches the AI how to pair- program at tween/teen reading level, but full multi-host / sandboxed-per-user deploys are deferred. - bundles/maker-lab-advanced/manifest.json: age_gate.min_age=9, sibling_of: [maker-lab], hard dep on the maker-lab bundle so the pair-programmer skill can reuse maker-lab's hint pipeline + persona prompts. Admin user + password required via env. - bundles/maker-lab-advanced/docker-compose.yml: jupyterhub/jupyterhub:5 image (official), loopback-bound on :8088, volume-mounted config, starts by pip-installing notebook + jupyterlab + jupyterhub-nativeauthenticator then execing jupyterhub. 4 GB mem_limit, 120s healthcheck start_period for cold-install. - bundles/maker-lab-advanced/config/jupyterhub_config.py: single- machine classroom shape — NativeAuthenticator (password auth with admin signup approval), SimpleLocalProcessSpawner (user kernels run inside the hub container, no per-user OS accounts), default URL /lab (JupyterLab, not the classic notebook UI). Kid-safe kernel hardening writes an IPython startup hook that strips shell-escape magics (%%bash, !shell, %sx) so a learner can't pivot the kernel into an arbitrary shell. - bundles/maker-lab-advanced/panel/maker-lab-advanced.js: thin Nest panel with liveness probe (/hub/health), first-boot admin signup checklist, kid-safe-defaults caveat (defaults, not a sandbox), pair-programmer posture explanation, and the age-ladder table (Blockly 5-9 / Scratch 8+ / Jupyter 9+). - bundles/maker-lab-advanced/skills/maker-lab-advanced.md: AI behavior — default persona tween-tutor (80-word hints, middle- grade vocab), switches to adult-tutor for 14+ learners (drops the hint-ladder for direct Q&A), explains tracebacks bottom-up, stays inside whatever library the learner imported, routes Blockly questions back to the maker-lab skill, treats the kid-safe config as a default not a security sandbox and says so plainly. - registry/add-ons.json: entry next to vllm. - skills/superpowers.md: trigger row. - CLAUDE.md: bundle directory map + Skills Reference entry. Deferred to v2+: bootstrap admin via NativeAuthenticator CLI (the API shape varied enough across NA releases that I pulled the speculative code and documented the manual first-boot flow in the panel instead); DockerSpawner or KubeSpawner for per-user isolation; GitHub OAuth; persistent per-user images with classroom-specific libraries pre-installed.
1 parent a4b3e41 commit a12e39f

8 files changed

Lines changed: 404 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ bundles/maker-lab/ → STEM education companion for kids (MCP server
160160
bundles/kolibri/ → Kolibri learning platform (Docker + panel + skill; offline-first STEM/literacy, Pi-friendly, sibling of maker-lab)
161161
bundles/scratch-offline/ → Scratch block-based coding (Dockerfile builds scratch-gui from source + nginx; age 8+, no cloud save)
162162
bundles/vllm/ → vLLM GPU inference server (OpenAI-compatible endpoint; Linux x86_64 + NVIDIA only; recommended classroom engine for Maker Lab)
163+
bundles/maker-lab-advanced/ → Maker Lab Advanced (Phase 5): JupyterHub for 9+ learners, kid-safe kernel defaults, AI pair-programmer at tween/teen reading level
163164
android/ → Android WebView shell app (Crow's Nest mobile client)
164165
servers/gateway/public/ → PWA assets (manifest.json, service worker, icons)
165166
servers/gateway/push/ → Web Push notification infrastructure (VAPID)
@@ -468,6 +469,7 @@ Add-on skills (activated when corresponding add-on is installed):
468469
- `kolibri.md` — Kolibri offline-first learning platform: channel recommendation by age/subject, classroom setup, LAN-sync, pairs with maker-lab as content spine
469470
- `scratch-offline.md` — Self-hosted Scratch (age 8+ block coding): first-project coaching, vocabulary-stays-in-Scratch-terms dialogue, pair with Maker Lab when the learner is stuck
470471
- `vllm.md` — Operational skill for the vLLM classroom inference engine: GPU sizing, model selection, Maker Lab wiring, common-issue diagnostics
472+
- `maker-lab-advanced.md` — Pair-programmer for JupyterHub classroom (ages 9+): traceback explanation, next-cell suggestions at tween/teen reading level, routes back to maker-lab for Blockly, flags kid-safe kernel as a default (not a security sandbox)
471473
- `calibre-server.md` — Calibre content server: search, browse, download ebooks via OPDS
472474
- `calibre-web.md` — Calibre-Web reader: search, shelves, reading status, download
473475
- `miniflux.md` — Miniflux RSS reader: subscribe feeds, read articles, star, mark read
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# JupyterHub config for Maker Lab Advanced (Phase 5 v1)
2+
#
3+
# Single-machine classroom shape: NativeAuthenticator for password auth
4+
# with admin-approved signups, SimpleLocalProcessSpawner so user
5+
# kernels run inside this container without per-user OS accounts.
6+
# Kid-safe default kernel config: write scope chrooted to the user's
7+
# home, shell-escape magics disabled.
8+
#
9+
# For production multi-host or sandboxed-per-user deployments, swap the
10+
# spawner for DockerSpawner or KubeSpawner and the authenticator for
11+
# GitHubOAuthenticator or LDAP.
12+
13+
import os
14+
15+
c = get_config() # noqa: F821 (provided by JupyterHub at import time)
16+
17+
admin_user = os.environ["MLA_ADMIN_USER"]
18+
19+
# --- Authenticator ---------------------------------------------------
20+
# NativeAuthenticator: local password auth with admin signup approval.
21+
c.JupyterHub.authenticator_class = "nativeauthenticator.NativeAuthenticator"
22+
c.Authenticator.admin_users = {admin_user}
23+
c.NativeAuthenticator.open_signup = False # admin approves new users
24+
c.NativeAuthenticator.minimum_password_length = 8
25+
c.NativeAuthenticator.check_common_password = True
26+
27+
# --- Spawner ---------------------------------------------------------
28+
# SimpleLocalProcessSpawner: runs each user's kernel in this container
29+
# without needing a host OS account per user. Good enough for a
30+
# single-classroom setup where the admin trusts the kids not to pivot
31+
# out of their home dir; the kernel config below adds hardening.
32+
c.JupyterHub.spawner_class = "simple"
33+
c.Spawner.notebook_dir = "~/"
34+
c.Spawner.default_url = "/lab"
35+
c.Spawner.args = [
36+
"--ServerApp.allow_origin=*",
37+
"--ServerApp.trust_xheaders=True",
38+
]
39+
40+
# Environment passed to every spawned kernel. Used by the
41+
# maker-lab-advanced pair-programmer skill to prefix prompts with a
42+
# learner-id marker so Maker Lab's hint pipeline can scope memory.
43+
c.Spawner.environment = {
44+
"CROW_MLA_ENABLED": "1",
45+
}
46+
47+
# --- Kid-safe kernel defaults ----------------------------------------
48+
# - Disable cell-magic shell escape (%%bash, !shell) via a startup hook
49+
# that the notebook server reads from ~/.ipython/profile_default/
50+
# startup/ at every spawn. Written once at hub startup.
51+
import pathlib
52+
startup_dir = pathlib.Path("/srv/jupyterhub/ipython-startup")
53+
startup_dir.mkdir(parents=True, exist_ok=True)
54+
(startup_dir / "00-crow-kid-safe.py").write_text(
55+
"# Crow kid-safe kernel hardening (Maker Lab Advanced).\n"
56+
"# Disables shell-escape cell magics so '!rm -rf ~' and '%%bash' do\n"
57+
"# not give a learner a path out of the kernel. Removing this file\n"
58+
"# re-enables them — admin decision, documented per classroom.\n"
59+
"try:\n"
60+
" ip = get_ipython()\n"
61+
" if ip is not None:\n"
62+
" for magic in ('system', 'sx', 'bash', 'sh'):\n"
63+
" try:\n"
64+
" ip.magics_manager.magics.get('line', {}).pop(magic, None)\n"
65+
" ip.magics_manager.magics.get('cell', {}).pop(magic, None)\n"
66+
" except Exception:\n"
67+
" pass\n"
68+
"except Exception:\n"
69+
" pass\n"
70+
)
71+
c.Spawner.environment["IPYTHONDIR"] = "/srv/jupyterhub/ipython-startup"
72+
73+
# --- Hub settings ----------------------------------------------------
74+
c.JupyterHub.ip = "0.0.0.0"
75+
c.JupyterHub.port = 8000
76+
c.JupyterHub.hub_ip = "0.0.0.0"
77+
c.JupyterHub.cleanup_servers = False
78+
c.JupyterHub.allow_named_servers = False
79+
80+
# Template path for the NativeAuthenticator signup/login pages.
81+
import nativeauthenticator # noqa: F401 (ensures the module is importable)
82+
c.JupyterHub.template_paths = [
83+
f"{os.path.dirname(nativeauthenticator.__file__)}/templates/",
84+
]
85+
86+
# Persist Hub DB (users, tokens) on the mounted volume so a container
87+
# restart doesn't forget everyone.
88+
c.JupyterHub.db_url = "sqlite:////srv/jupyterhub/jupyterhub.sqlite"
89+
c.JupyterHub.cookie_secret_file = "/srv/jupyterhub/jupyterhub_cookie_secret"
90+
91+
# --- Admin bootstrap (manual first-run) ------------------------------
92+
# NativeAuthenticator's ORM API varies across releases, so we skip
93+
# programmatic seeding and rely on the UI signup flow:
94+
#
95+
# 1. On first launch, the admin navigates to /hub/signup, enters
96+
# MLA_ADMIN_USER and MLA_ADMIN_PASSWORD, clicks Sign Up.
97+
# 2. Because MLA_ADMIN_USER is in admin_users above, the account is
98+
# auto-authorized and promoted without requiring self-approval.
99+
# 3. The admin then uses /hub/authorize to approve additional
100+
# learner signups.
101+
#
102+
# This is a Phase 5 v1 compromise. A future version can plug in a
103+
# one-shot bootstrap container that runs `jupyterhub-nativeauthenticator`
104+
# CLI commands after the hub DB is ready.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
services:
2+
maker-lab-advanced:
3+
image: jupyterhub/jupyterhub:5
4+
container_name: crow-maker-lab-advanced
5+
ports:
6+
- "127.0.0.1:${MLA_HTTP_PORT:-8088}:8000"
7+
volumes:
8+
- ${MLA_DATA_PATH:-mla-data}:/srv/jupyterhub
9+
- ./config/jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro
10+
environment:
11+
- MLA_ADMIN_USER=${MLA_ADMIN_USER:?Set MLA_ADMIN_USER}
12+
- MLA_ADMIN_PASSWORD=${MLA_ADMIN_PASSWORD:?Set MLA_ADMIN_PASSWORD}
13+
command:
14+
- "sh"
15+
- "-c"
16+
- "pip install --quiet notebook jupyterlab jupyterhub-nativeauthenticator && jupyterhub -f /srv/jupyterhub/jupyterhub_config.py"
17+
init: true
18+
mem_limit: 4g
19+
restart: unless-stopped
20+
healthcheck:
21+
test: ["CMD", "curl", "-fsS", "http://localhost:8000/hub/health"]
22+
interval: 30s
23+
timeout: 10s
24+
retries: 5
25+
start_period: 120s
26+
27+
volumes:
28+
mla-data:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"id": "maker-lab-advanced",
3+
"name": "Maker Lab Advanced",
4+
"version": "0.1.0",
5+
"description": "Self-hosted JupyterHub for older learners (ages 9+) — single-machine multi-user Python notebooks with kid-safe kernel defaults and an AI pair-programmer. Sibling bundle to maker-lab: Blockly is the 5-9 on-ramp, Scratch is the 8+ step-up, this is the 9+ landing zone with real Python.",
6+
"type": "bundle",
7+
"author": "Crow",
8+
"category": "education",
9+
"tags": ["education", "jupyter", "jupyterhub", "python", "notebook", "classroom", "k12", "teen", "maker-lab"],
10+
"icon": "graduation-cap",
11+
"docker": { "composefile": "docker-compose.yml" },
12+
"panel": "panel/maker-lab-advanced.js",
13+
"skills": ["skills/maker-lab-advanced.md"],
14+
"requires": {
15+
"env": ["MLA_ADMIN_USER", "MLA_ADMIN_PASSWORD"],
16+
"min_ram_mb": 2048,
17+
"recommended_ram_mb": 8000,
18+
"min_disk_mb": 4000,
19+
"recommended_disk_mb": 40000,
20+
"bundles": ["maker-lab"]
21+
},
22+
"env_vars": [
23+
{ "name": "MLA_ADMIN_USER", "description": "Initial JupyterHub admin username. Gets first login; add more admins from the UI after setup.", "required": true },
24+
{ "name": "MLA_ADMIN_PASSWORD", "description": "Initial admin password. Set via NativeAuthenticator on first boot; change via the JupyterHub UI afterward.", "required": true, "secret": true },
25+
{ "name": "MLA_HTTP_PORT", "description": "Host port for the JupyterHub UI. Default 8088.", "default": "8088", "required": false },
26+
{ "name": "MLA_DATA_PATH", "description": "Host path for user home dirs + JupyterHub state (defaults to a named volume).", "required": false }
27+
],
28+
"ports": [8088],
29+
"webUI": { "port": 8088, "path": "/", "label": "JupyterHub" },
30+
"sibling_of": ["maker-lab"],
31+
"age_gate": { "min_age": 9 },
32+
"notes": "v1 single-admin classroom shape: NativeAuthenticator + SimpleLocalProcessSpawner, shared container runtime, loopback-bound on :8088. Kid-safe kernel config blocks shell-escape patterns and chroots file writes to the user's home. True multi-host / GitHub-OAuth / sandboxed-per-user deploys are future work. Hard dep on the maker-lab bundle: the pair-programmer skill reuses maker-lab's hint pipeline and persona prompts at a higher reading level."
33+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Crow's Nest Panel — Maker Lab Advanced: JupyterHub launcher + admin
3+
* bootstrap checklist + pair-programmer posture.
4+
*
5+
* Thin panel, same shape as kolibri/scratch-offline. The real UX lives
6+
* in JupyterHub's own web UI; this panel is setup hints + the deep link.
7+
*/
8+
9+
export default {
10+
id: "maker-lab-advanced",
11+
name: "Maker Lab Advanced",
12+
icon: "graduation-cap",
13+
route: "/dashboard/maker-lab-advanced",
14+
navOrder: 57,
15+
category: "education",
16+
17+
async handler(req, res, { layout, appRoot }) {
18+
const { pathToFileURL } = await import("node:url");
19+
const { join } = await import("node:path");
20+
const componentsPath = join(appRoot, "servers/gateway/dashboard/shared/components.js");
21+
const { escapeHtml } = await import(pathToFileURL(componentsPath).href);
22+
23+
const port = process.env.MLA_HTTP_PORT || "8088";
24+
const hubUrl = `http://${req.hostname || "localhost"}:${port}`;
25+
const adminUser = process.env.MLA_ADMIN_USER || "(MLA_ADMIN_USER unset — set in .env before first launch)";
26+
27+
// Short liveness probe so the operator sees status at a glance.
28+
async function probe(url, timeoutMs) {
29+
try {
30+
const ctrl = new AbortController();
31+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
32+
const resp = await fetch(url, { signal: ctrl.signal });
33+
clearTimeout(t);
34+
return resp.ok;
35+
} catch { return false; }
36+
}
37+
const live = await probe(`${hubUrl}/hub/health`, 1500);
38+
const statusBadge = live
39+
? `<span class="ma-badge ma-ok">● live</span>`
40+
: `<span class="ma-badge ma-off">○ offline</span>`;
41+
42+
const content = `
43+
<style>${styles()}</style>
44+
<div class="ma-panel">
45+
<h1>Maker Lab Advanced</h1>
46+
<p class="ma-sub">JupyterHub for older learners · ages 9+ · Python notebooks with kid-safe kernel defaults</p>
47+
48+
<section class="ma-card">
49+
<h2>Status</h2>
50+
<p>${statusBadge}</p>
51+
<dl class="ma-dl">
52+
<dt>Hub URL</dt><dd><code>${escapeHtml(hubUrl)}</code></dd>
53+
<dt>Configured admin</dt><dd><code>${escapeHtml(adminUser)}</code></dd>
54+
</dl>
55+
<p><a class="ma-btn" href="${escapeHtml(hubUrl)}" target="_blank" rel="noopener">Open Hub ↗</a></p>
56+
</section>
57+
58+
<section class="ma-card">
59+
<h2>First-boot checklist</h2>
60+
<ol>
61+
<li>Set <code>MLA_ADMIN_USER</code> and <code>MLA_ADMIN_PASSWORD</code> in <code>.env</code>. Restart the bundle if you changed them.</li>
62+
<li>Navigate to <code>${escapeHtml(hubUrl)}/hub/signup</code>, sign up with the admin username + password from step 1. Because that username is in the hub's <code>admin_users</code> list, the account auto-authorizes — no self-approval required.</li>
63+
<li>Admin view at <code>${escapeHtml(hubUrl)}/hub/admin</code> lists users + server status.</li>
64+
<li>Learner signups go to <code>${escapeHtml(hubUrl)}/hub/authorize</code> for admin approval before they can spawn a server.</li>
65+
</ol>
66+
</section>
67+
68+
<section class="ma-card">
69+
<h2>Kid-safe kernel defaults</h2>
70+
<ul>
71+
<li>Shell-escape magics (<code>%%bash</code>, <code>!rm</code>, <code>%sx</code>) are disabled at kernel startup. A learner can't shell out of the notebook into the container.</li>
72+
<li>Per-user home dirs live inside the container at <code>/home/&lt;user&gt;</code>; writes outside that path fail with permission errors from the kernel perspective.</li>
73+
<li><strong>These are defaults for learners, not a security sandbox.</strong> A determined attacker with Python access can still explore the container filesystem. For untrusted users, swap the spawner for DockerSpawner + per-user images.</li>
74+
</ul>
75+
</section>
76+
77+
<section class="ma-card">
78+
<h2>Pair-programmer (v1)</h2>
79+
<p>The <code>maker-lab-advanced</code> skill teaches the AI to act as a pair-programmer at tween/teen reading level. It reuses Maker Lab's hint pipeline and hint-ladder prompts, but:</p>
80+
<ul>
81+
<li>Default persona: <code>tween-tutor</code> (80-word hints, middle-grade vocabulary).</li>
82+
<li>For older learners (14+), the caregiver can switch the session persona to <code>adult-tutor</code> (200-word explanations, plain-language technical terminology, direct Q&A).</li>
83+
<li>Hints reference <strong>Python + notebook idioms</strong>, not Blockly blocks — the skill explicitly flips off the "never say the answer" constraint at <code>adult-tutor</code> level.</li>
84+
</ul>
85+
</section>
86+
87+
<section class="ma-card">
88+
<h2>Where this sits in the ladder</h2>
89+
<table class="ma-tbl">
90+
<tr><th>Ages</th><th>Surface</th><th>Tutor persona</th></tr>
91+
<tr><td>5-9</td><td>Blockly (maker-lab)</td><td>kid-tutor</td></tr>
92+
<tr><td>8+</td><td>Scratch (scratch-offline)</td><td>kid-tutor → tween-tutor</td></tr>
93+
<tr><td>9+</td><td>JupyterLab (this bundle)</td><td>tween-tutor → adult-tutor</td></tr>
94+
</table>
95+
</section>
96+
</div>
97+
`;
98+
return layout({ title: "Maker Lab Advanced", content });
99+
},
100+
};
101+
102+
function styles() {
103+
return `
104+
.ma-panel { max-width: 900px; margin: 0 auto; padding: 1.5rem; }
105+
.ma-sub { color: var(--fg-muted, #888); margin: 0 0 1.5rem; }
106+
.ma-card { background: var(--card-bg, rgba(255,255,255,0.04)); border: 1px solid var(--border, #333); border-radius: 10px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; }
107+
.ma-card h2 { margin: 0 0 0.75rem; font-size: 1.05rem; color: #84cc16; }
108+
.ma-card ol, .ma-card ul { margin: 0; padding-left: 1.25rem; line-height: 1.6; }
109+
.ma-badge { display: inline-block; padding: 0.25rem 0.7rem; border-radius: 999px; font-size: 0.85rem; font-weight: 600; }
110+
.ma-ok { background: rgba(34,197,94,0.15); color: #22c55e; }
111+
.ma-off { background: rgba(239,68,68,0.15); color: #ef4444; }
112+
.ma-btn { display: inline-block; padding: 0.6rem 1.2rem; background: #84cc16; color: #111; text-decoration: none; border-radius: 6px; font-weight: 600; margin-top: 0.5rem; }
113+
.ma-btn:hover { background: #a3e635; }
114+
.ma-dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1rem; margin: 0.5rem 0 0; }
115+
.ma-dl dt { color: var(--fg-muted, #888); }
116+
.ma-dl dd { margin: 0; }
117+
.ma-tbl { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
118+
.ma-tbl th, .ma-tbl td { padding: 0.5rem 0.75rem; text-align: left; border-bottom: 1px solid var(--border, #333); }
119+
.ma-tbl th { color: var(--fg-muted, #888); font-weight: 600; }
120+
`;
121+
}

0 commit comments

Comments
 (0)