# LabSCH — Lab School Manager [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/) [![Cloudflare Workers](https://img.shields.io/badge/server-Cloudflare%20Workers%20%2B%20D1-orange.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Platform: Windows](https://img.shields.io/badge/platform-Windows%2010%2F11%20LTSC-blue.svg)]() [![Status: v0.4.0-test10](https://img.shields.io/badge/status-v0.4.0--test10-orange.svg)]() [![Web UI](https://img.shields.io/badge/admin-Cloudflare%20Pages-blue.svg)](https://labsch-manager.pages.dev) Centralized Windows lab management for 20+ PCs — **100% serverless**. Push block/allow rules, app-block policies, camera/audio controls, remote commands, and secure download tasks to lightweight Python agents on each client. Manage the fleet through `labschctl` or the Cloudflare Pages Web UI. > **Current status:** `v0.4.0-test10` is an acceptance/test build, not a fleet-wide stable release. Windows behavior must be physically verified before broad rollout. > **Live deployment**: Managing SMK + SMP lab PCs in Medan, Indonesia. Server runs on **Cloudflare Workers + D1** (free tier, $0/month) — no homeserver required, no tunnel to maintain, no VM to babysit. > > **This is a self-hosted project.** Deploy your own Workers URL (see [Server setup](#server-setup-cloudflare-workers)) — the URLs in this README are examples from the author's own deployment. **Never ship someone else's Workers URL with your token.** ## Features - 🛡️ **Triple-layer blocking** — hosts file, browser policy registry, Windows IFEO. DoH-proof (disables Secure DNS via policy). Incognito-proof (disables Private mode). - 📷🔊 **Camera & audio control** — kill switches per-PC or globally, reversible, delivered via heartbeat. - 🆔 **MAC-based client ID** — one device = one record, even after agent reinstall or PC rename. No duplicates. - 📦 **1-click installers** — `install.bat` (agent) + `labsch_startup.bat` (4-layer auto-start that survives reboots) + `labsch_full_uninstall.bat` (clean removal of everything). - 🔁 **Self-protection** — Scheduled tasks + Run key + (optional) Task Manager lockdown. The student cannot kill or uninstall the agent through normal means. - 💾 **Named rule profiles** — Save config as "Rules Lab", "Ujian", "Bebas Akses", activate with one command. - 🪟 **Windows 10/11 LTSC** — Tested on MSI Thin 15 (LTSC), should work on Pro/Home/Enterprise. - ⚡ **Serverless edge** — Cloudflare Workers global anycast, <50ms latency from Indonesia, 100k requests/day free. - 🛠️ **Hermes skill** — Bundled admin CLI integrates with the Hermes Agent skill system. Just `labschctl` from any terminal. - 🌐 **Cloudflare Pages Web UI** — Dashboard, clients, config, profiles, commands, downloads, device flags, and events from a responsive browser interface. - ⬇️ **Durable download tasks** — Explicit per-client HTTPS downloads with SHA-256 verification, size/TTL limits, separate download/execution status, cancellation, and optional user-context autorun. - ✅ **Confirmed remote commands** — Commands remain queued until the matching agent reports success; failed executions are retained for retry. - 🧑‍💻 **Session refresh API** — Idempotent session refresh using stable device IDs and daily retention cleanup. ## Web UI The static admin interface lives in [`web/`](web/) and talks directly to the Workers REST API. Deploy it to Cloudflare Pages: ```bash node --check web/app.js npx wrangler pages deploy web --project-name labsch-manager --branch main ``` Open the Pages URL, enter your own Worker URL and admin token, then manage the fleet. The token is never committed or baked into the static files; it is supplied at runtime and stored in that browser's `localStorage`. Use only a trusted admin device and log out afterward. The UI considers a PC **online only when `last_seen` is no more than 60 seconds old**, rather than trusting a potentially stale database status field. ## Secure download tasks (v0.4.0 test) ```bash # Autorun is the default and requires the expected SHA-256. labschctl download https://example.org/worksheet.pdf \ --name worksheet.pdf \ --sha256 <64-lowercase-hex> \ --clients PC-LAB-01 # Download without opening the file. labschctl download https://example.org/worksheet.pdf \ --no-run --clients PC-LAB-01 labschctl downloads labschctl download-cancel ``` Only explicit clients are supported. There is no implicit broadcast. Sources must use public HTTPS; script/installer autorun, SYSTEM autorun, custom arguments, and group targeting are intentionally unsupported in this test line. See [`docs/DOWNLOAD_TEST_RELEASE.md`](docs/DOWNLOAD_TEST_RELEASE.md). ## 🔐 API token management (v0.3.2+) Rotate your `X-Agent-Token` without redeploying secrets manually: ```bash # Generate a new token (returned ONCE, save it!) labschctl token generate # Check current token fingerprint (12-char SHA-256 prefix) labschctl token info # Revoke in-process / clear KV metadata labschctl token revoke ``` **Under the hood:** `POST /api/admin/token/generate`, `GET /api/admin/token/info`, `DELETE /api/admin/token` — Workers KV namespace `TOKEN_META` (provision once: `wrangler kv:namespace create "TOKEN_META"`). For local FastAPI, tokens auto-persist to `~/.hermes/.env`. **The full token is never stored server-side** — only its SHA-256 fingerprint, so a KV / `.env` leak does not leak usable credentials. ## 🛠 API token CLI `labschctl token ` supports `generate`, `info`, `revoke`. The CLI prompts for confirmation unless you pass `--yes`. See `docs/API.md` for full HTTP contract. ## Architecture ``` ┌──────────────────────────────────────────┐ │ Cloudflare Edge (serverless) │ │ │ │ Workers: labsch-api (Hono, TS) │ │ ├─ 29 REST endpoints │ │ ├─ X-Agent-Token auth middleware │ │ └─ Cron: mark-stale (every 5 min) │ │ │ │ D1 Database: labsch-db (SQLite) │ │ ├─ clients / config / events │ │ └─ profiles / client_overrides │ └────────────┬─────────────────────────────┘ │ HTTPS polling │ 30s heartbeat (configurable) │ Auth: X-Agent-Token (UUID) ▼ ┌──────────────────────────────────────────┐ │ Each Windows PC │ │ (agent/labsch_agent.py) │ │ Python → PyInstaller .exe │ │ 4-layer auto-start, self-protect │ │ Pulls config → applies policy │ │ Hosts + browser policy + IFEO + devices │ └──────────────────────────────────────────┘ ▲ │ HTTPS (X-Agent-Token) │ ┌────────────┴─────────────────────────────┐ │ Admin (anywhere) │ │ labschctl CLI or Hermes Agent │ └──────────────────────────────────────────┘ ``` The agent is **pull-based**: each client polls the server. No firewall ports to open on the school network, no NAT-traversal, no WebSocket. Plain HTTPS polling — simple and reliable at 20-PC scale. **Why serverless?** The old design (FastAPI on a homeserver + Cloudflare Tunnel) worked, but meant babysitting a VM: systemd restarts, tunnel URL rotations, disk space. Workers + D1 removes all of that — $0/month, scales automatically, and the deployment URL never changes. ## Quick start (admin) First, deploy your own server (see [Server setup](#server-setup-cloudflare-workers) below), then point the CLI at it: ```bash # Set environment (or in ~/.hermes/.env): export SCHOOL_SERVER_URL="https://labsch-api..workers.dev" export SCHOOL_API_TOKEN="" # List all clients labschctl clients # Block / allow labschctl config block-site tiktok.com labschctl config block-app RobloxPlayerLauncher.exe labschctl config allow-site wikipedia.org # Camera / audio control (v0.3.1+) # (via API: POST /api/admin/device {"disable_camera": true}) # Bulk clear labschctl unblock-all # Named profiles labschctl profile save "Rules Lab" labschctl profile activate "Rules Lab" # Rename a PC, view events labschctl rename "PC-LAB-01" labschctl events --hours 1 ``` ## Deploy to a new PC 1. **Download** `labsch-agent-v0.3.2.zip` from the [Releases](../../releases) page. 2. **Copy** to a USB drive or directly to the target PC. 3. **Extract** the zip. 4. **Right-click `install.bat` → "Run as administrator"**. 5. **Answer two prompts**: - Display name (e.g. `PC-LAB-01`) - Is this a test PC? (Y/N — test PCs are excluded from profile rules) 6. **Run `labsch_startup.bat`** (also as admin) — installs 4-layer auto-start so the agent survives reboots reliably. 7. **Done.** The agent registers and starts applying the active config within 60 seconds. ### Auto-start layers (v0.3.1+) `labsch_startup.bat` installs all of these: | Layer | Trigger | Purpose | |---|---|---| | Startup folder (user) | User login | First chance | | Startup folder (all users) | Any user login | Coverage for shared PCs | | Run key HKLM | Boot | Classic auto-start | | Scheduled task ONSTART (SYSTEM) | **Before login** | Runs even at the login screen | Plus a **starter script with retry**: waits until Python is available on PATH (up to 5 min), then launches the agent, then respawns it every 10 seconds if it dies. This fixes the "sometimes doesn't load after reboot" issue where Windows fires the Run key before Python/PATH is ready. ## Per-PC overrides The global config applies to every client by default. A per-PC override replaces it for one client only. ```bash # Block YouTube + Roblox only on PC-LAB-01; allow Google there labschctl client-config set desktop-abc123 \ --blocked-site youtube.com \ --allowed-site google.com \ --blocked-app RobloxPlayerBeta.exe # Inspect the override labschctl client-config show desktop-abc123 # Remove the override; it inherits global rules again labschctl client-config clear desktop-abc123 ``` To keep one PC completely free while the global profile remains active, save an explicit empty override: ```bash labschctl client-config set desktop-abc123 ``` `set` with no lists means "free access for this PC". `clear` means "inherit global rules". ## Camera & audio control (v0.3.1+) Two new kill switches, set globally or per-PC, delivered through the same heartbeat as blocking rules: | Flag | Effect | Mechanism | |---|---|---| | `disable_camera` | No app can access the webcam | Registry policy `AllowCamera=0` + device-install restriction on the camera class GUID | | `disable_audio` | System-wide silence | Stop + disable `audiosrv` and `AudioEndpointBuilder` services | Both are **reversible** — flipping the flag off immediately restores the device. Set via the admin API: ```bash # Global: kill both curl -X POST -H "X-Agent-Token: $TOKEN" -H "Content-Type: application/json" \ -d '{"disable_camera": true, "disable_audio": true}' \ $SCHOOL_SERVER_URL/api/admin/device # Per-PC only curl -X POST ... $SCHOOL_SERVER_URL/api/admin/device/ # Clear per-PC override (inherit global) curl -X DELETE ... $SCHOOL_SERVER_URL/api/admin/device/ ``` ## Uninstall (per PC) ```powershell labsch_full_uninstall.bat # Right-click → Run as administrator ``` Removes **everything**: agent processes, all 4 auto-start layers, hosts-file entries, browser policies, IFEO keys, re-enables Task Manager, and deletes `C:\ProgramData\LabSCHAgent`. The PC returns to a clean state. For the agent files only (keep auto-start), use `uninstall.bat` instead. ## How blocking works (defense in depth) The agent applies three layers on every config change: ### 1. Hosts file Appends `127.0.0.1 ` lines with marker comments. Catches all DNS-aware software. ### 2. Browser policy (registry) Sets `URLBlocklist` and `URLAllowlist` for Edge/Chrome/Brave. Disables DoH and Incognito so the browser can't bypass. ### 3. IFEO (Image File Execution Options) Sets `Debugger=cmd.exe /c exit` for each blocked `.exe`. The OS itself blocks execution *before* the process starts — renaming or moving the binary doesn't help. If config is cleared, the agent **deletes** the registry keys (not sets to empty) to avoid stale policy caching in Chromium browsers. ## Rule profiles A profile is a named snapshot of `(blocked_apps, blocked_websites, allowed_websites)`. Workflow: 1. Set up the live config: ```bash labschctl config block-site tiktok.com labschctl config allow-site wikipedia.org labschctl config block-app RobloxPlayerLauncher.exe ``` 2. Save it: `labschctl profile save "Rules Lab"` 3. Restore it later: `labschctl profile activate "Rules Lab"` Multiple profiles coexist. Activation bumps `config_version`, which agents pull on their next cycle. ## Requirements ### Server (Cloudflare) - Cloudflare free account - Node.js 18+ with `wrangler` (deploy-time only, not runtime) - D1 database (free tier: 5 GB storage, 5M reads/day) ### Agent (per PC) - Windows 10 or 11 (LTSC, Pro, Home all tested) - Python 3.10+ (or pre-built `.exe` via `build.bat`) - Administrator privileges for install + first run - Internet access to `*.workers.dev` (or your custom domain) ### Quota math (free tier) | Item | Usage at 20 PCs | Free limit | |---|---|---| | Workers requests | ~86,500/day | 100,000/day ✅ | | D1 reads | ~50,000/day | 5M/day ✅ | | D1 writes | ~5,000/day | 100k/day ✅ | Heartbeat interval is configurable (`heartbeat_interval` in config.ini, default 30s) if you need to trade latency for quota headroom. ## Server setup (Cloudflare Workers) From scratch (replacing the old homeserver + tunnel setup): ```bash # 1. Install wrangler + login npm install -g wrangler wrangler login # browser OAuth (or --device flow on headless) # 2. Create the D1 database wrangler d1 create labsch-db # Note the database_id → put in workers/wrangler.toml # 3. Apply schema cd workers wrangler d1 execute labsch-db --remote --file=schema.sql -y # 4. Set the auth token secret wrangler secret put SCHOOL_API_TOKEN # paste a UUID # 5. Deploy wrangler deploy # → https://labsch-api..workers.dev ``` Set the URL as `SCHOOL_SERVER_URL` for `labschctl`, and bake it into the agent's `install.bat` before distribution. Cron trigger (mark-stale every 5 min) is already in `wrangler.toml`. ## ⚠️ Configure your own URL (REQUIRED before deploying agents) After deploying your own Worker, you must replace the server URL in **every place the agents and CLI read it**. There is no central config — each location is baked at install/build time: | # | File | What to change | |---|---|---| | 1 | **`agent/install.bat`** (line ~34) | `set "SERVER_URL=https://labsch-api..workers.dev"` | | 2 | **`agent/build.bat`** args | `build.bat --server https://labsch-api..workers.dev --token ` | | 3 | **`skill/labschctl`** (or `~/.hermes/.env`) | `SCHOOL_SERVER_URL` env var | | 4 | **Existing PCs** | Edit `C:\ProgramData\LabSCHAgent\config.ini` → `"server_url"` key, then restart the agent | Also replace `API_TOKEN` in `install.bat` with **your own** generated UUID: ```bash python3 -c "import uuid; print(uuid.uuid4())" ``` > **Security note**: the API token grants full control of every registered PC (including remote shutdown). Never commit your real token to a public repo, and never point your agents at a Workers URL you don't control — the operator of that URL could shut down your lab. > > **Already-installed PCs without a reinstall?** Fastest path: edit `config.ini` on each PC (option 4 above). The MAC-based client ID means no re-registration is needed — the same PC keeps its record. ## Build (Windows .exe) To distribute as a single `.exe` instead of Python scripts: ```cmd cd agent build.bat --server https://labsch-api..workers.dev --token ``` Output: `dist\LabSCHAgent.exe` (~15 MB, self-contained). For 1-click install across 20 PCs, use the Python version + `install.bat` — it's smaller and updates the agent in one place. ## API All admin endpoints require `X-Agent-Token: ` header. See [`docs/API.md`](docs/API.md) for the full list. Quick reference: | Method | Path | Purpose | |--------|------|---------| | `GET` | `/api/health` | Health check (open) | | `POST` | `/api/heartbeat` | Agent heartbeat — returns config + device flags + pending command | | `GET` | `/api/clients` | List registered clients | | `GET` | `/api/admin/config` | Live blocklist config | | `POST` | `/api/admin/profiles` | Create or update named profile | | `POST` | `/api/admin/profiles/{name}/activate` | Apply profile to all clients | | `POST` | `/api/admin/device` | Set camera/audio flags (global) | | `POST` | `/api/admin/device/{client_id}` | Set camera/audio flags (per-PC) | | `POST` | `/api/admin/command/{client_id}` | Queue shutdown/restart/lock/notify | ## File layout ``` labsch/ ├── workers/ Cloudflare Workers server (TypeScript, Hono) │ ├── src/index.ts Entry + routing │ ├── src/auth.ts Token middleware │ ├── src/handlers/ API handlers + Vitest coverage │ ├── migrations/ Additive D1 migrations │ ├── schema.sql Complete D1 schema │ ├── wrangler.toml Deploy config (D1/KV bindings, crons) │ └── package.json ├── web/ Static Cloudflare Pages admin UI │ ├── index.html │ ├── app.js │ └── style.css ├── agent/ Windows client │ ├── labsch_agent.py Main loop │ ├── config_sync.py HTTP client (custom UA, CF bot-fight safe) │ ├── device_blocker.py Camera/audio kill switches (v0.3.1) │ ├── browser_policy.py Edge/Chrome/Brave registry policy │ ├── ifeo_blocker.py App execution block │ ├── website_blocker.py Hosts file editor │ ├── self_protect.py Scheduled task + Run key │ ├── device_id.py MAC-based stable ID │ ├── install.bat 1-click agent installer │ ├── labsch_startup.bat 4-layer auto-start installer │ ├── labsch_full_uninstall.bat Full clean uninstall │ ├── uninstall.bat Minimal uninstall │ └── build.bat PyInstaller .exe builder ├── skill/ Hermes skill │ ├── SKILL.md │ └── labschctl Admin CLI └── docs/ ├── ARCHITECTURE.md ├── API.md ├── ALLOWLIST_BAT.md └── plans/ Migration plans ``` ## Roadmap - [x] Per-client config overrides (block site X only on PC-LAB-01) - [x] Serverless migration (Cloudflare Workers + D1) - [x] Camera & audio control - [x] Reliable auto-start across reboots (4-layer) - [ ] Group profiles (apply Rules Lab to group "lab", not test PCs) - [x] Cloudflare Pages Web UI (dashboard, clients, config, profiles, commands, downloads, devices, events) - [ ] Real-time events stream (WebSocket/SSE for admin UI) - [ ] Per-PC schedule (different rules for class time vs break time) - [ ] Native Windows installer (MSI) for Group Policy deployment ## License [MIT](LICENSE) — use freely for any school / lab / office environment. ## Credits - Inspired by the [FULL ALLOWLIST WEB v4](docs/ALLOWLIST_BAT.md) BAT script (registry-based browser policy) - Built for SMK/SMP schools in Medan, Indonesia - Runs on [Cloudflare Workers](https://workers.cloudflare.com/) + D1 (free tier)