Rename learner to dev across codebase

- scripts/learner-*.sh → scripts/dev-*.sh
- docs/learner-*.md → docs/dev-*.md
- tests/test-learner-env.sh → tests/test-dev-env.sh
- Update all internal references

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dan 2026-01-03 10:42:34 -08:00
parent 26be2b1548
commit bc81b4ec15
13 changed files with 95 additions and 95 deletions

View file

@ -34,7 +34,7 @@
{"id":"ops-jrz1-ayl","title":"Rename sna-instagram-bot to something memorable","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-08T16:54:31.223265094-08:00","updated_at":"2025-12-08T16:54:31.223265094-08:00"} {"id":"ops-jrz1-ayl","title":"Rename sna-instagram-bot to something memorable","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-08T16:54:31.223265094-08:00","updated_at":"2025-12-08T16:54:31.223265094-08:00"}
{"id":"ops-jrz1-b09","title":"Research: Forgejo backup strategy","description":"Consider backup strategy for the Forgejo instance.\n\n## Questions\n- What data needs backing up? (repos, issues, users, config)\n- Where to back up to? (off-site, object storage, etc.)\n- Frequency?\n- Restore procedure?\n\n## Context\nForgejo runs on ops-jrz1 at :3000, stores git repos and metadata.","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-02T19:01:55.854723242-08:00","created_by":"dan","updated_at":"2026-01-02T19:01:55.854723242-08:00"} {"id":"ops-jrz1-b09","title":"Research: Forgejo backup strategy","description":"Consider backup strategy for the Forgejo instance.\n\n## Questions\n- What data needs backing up? (repos, issues, users, config)\n- Where to back up to? (off-site, object storage, etc.)\n- Frequency?\n- Restore procedure?\n\n## Context\nForgejo runs on ops-jrz1 at :3000, stores git repos and metadata.","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-02T19:01:55.854723242-08:00","created_by":"dan","updated_at":"2026-01-02T19:01:55.854723242-08:00"}
{"id":"ops-jrz1-b8v","title":"Enable nix-ld for VS Code Remote SSH","description":"VS Code Remote-SSH requires nix-ld to run the VS Code Server on NixOS.\n\n## Status\n- [x] Config added to hosts/ops-jrz1.nix\n- [x] nix flake check passed\n- [ ] Deploy to server\n\n## Config Added\n```nix\nprograms.nix-ld.enable = true;\nprograms.nix-ld.libraries = with pkgs; [\n stdenv.cc.cc.lib\n zlib\n openssl\n];\n```\n\n## Blocks\n- VS Code Remote-SSH (won't work without this)\n- Testing Claude Code extension over Remote-SSH\n- JetBrains Gateway (same issue)\n\n## Deploy Command\n```bash\nnixos-rebuild switch --flake .#ops-jrz1 --target-host root@ops-jrz1 --build-host localhost\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-30T22:24:15.465531482-05:00","created_by":"dan","updated_at":"2026-01-02T10:22:07.3354851-08:00","closed_at":"2026-01-02T10:22:07.3354851-08:00","close_reason":"Deployed - nix-ld active at /lib64/ld-linux-x86-64.so.2"} {"id":"ops-jrz1-b8v","title":"Enable nix-ld for VS Code Remote SSH","description":"VS Code Remote-SSH requires nix-ld to run the VS Code Server on NixOS.\n\n## Status\n- [x] Config added to hosts/ops-jrz1.nix\n- [x] nix flake check passed\n- [ ] Deploy to server\n\n## Config Added\n```nix\nprograms.nix-ld.enable = true;\nprograms.nix-ld.libraries = with pkgs; [\n stdenv.cc.cc.lib\n zlib\n openssl\n];\n```\n\n## Blocks\n- VS Code Remote-SSH (won't work without this)\n- Testing Claude Code extension over Remote-SSH\n- JetBrains Gateway (same issue)\n\n## Deploy Command\n```bash\nnixos-rebuild switch --flake .#ops-jrz1 --target-host root@ops-jrz1 --build-host localhost\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-30T22:24:15.465531482-05:00","created_by":"dan","updated_at":"2026-01-02T10:22:07.3354851-08:00","closed_at":"2026-01-02T10:22:07.3354851-08:00","close_reason":"Deployed - nix-ld active at /lib64/ld-linux-x86-64.so.2"}
{"id":"ops-jrz1-bbn","title":"Research: Resource limits and quotas","description":"Should we limit CPU/memory/disk per learner?\n\n## Current state\n- No limits configured\n- Single VPS shared by all users\n- 2GB RAM, 1 vCPU (Vultr $6 tier?)\n\n## Options\n1. **No limits** - Trust learners, monitor manually\n2. **Systemd slices** - cgroups for user sessions\n3. **Disk quotas** - Limit ~/\n4. **ulimits** - Process limits\n\n## Questions\n- What resources does a typical dev session use?\n- What about `go build` or `npm install`?\n- Is this premature optimization?","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-02T12:27:34.884865507-08:00","created_by":"dan","updated_at":"2026-01-02T12:27:34.884865507-08:00"} {"id":"ops-jrz1-bbn","title":"Research: Resource limits and quotas","description":"Should we limit CPU/memory/disk per learner?\n\n## Current state\n- No limits configured\n- Single VPS shared by all users\n- 2GB RAM, 1 vCPU (Vultr $6 tier?)\n\n## Options\n1. **No limits** - Trust learners, monitor manually\n2. **Systemd slices** - cgroups for user sessions\n3. **Disk quotas** - Limit ~/\n4. **ulimits** - Process limits\n\n## Questions\n- What resources does a typical dev session use?\n- What about `go build` or `npm install`?\n- Is this premature optimization?","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-02T12:27:34.884865507-08:00","created_by":"dan","updated_at":"2026-01-03T10:11:09.79120117-08:00","closed_at":"2026-01-03T10:11:09.79120117-08:00","close_reason":"Implemented: systemd slices (user.slice MemoryMax=80%, TasksMax=500) + per-user limits (MemoryMax=50%, TasksMax=200, CPUQuota=200%). Disk quotas tracked separately in ops-jrz1-oxx."}
{"id":"ops-jrz1-bhk","title":"Add disk quotas for user workspaces","description":"User could fill host disk via /var/lib/vscode/\u003cuser\u003e/. Add per-directory quotas or monitoring/alerting on disk usage.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:41.199417226-08:00","updated_at":"2025-12-28T00:05:44.7635372-05:00","closed_at":"2025-12-28T00:05:44.7635372-05:00","close_reason":"Parent epic cancelled - browser-based dev approach abandoned","dependencies":[{"issue_id":"ops-jrz1-bhk","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.309592029-08:00","created_by":"daemon","metadata":"{}"}]} {"id":"ops-jrz1-bhk","title":"Add disk quotas for user workspaces","description":"User could fill host disk via /var/lib/vscode/\u003cuser\u003e/. Add per-directory quotas or monitoring/alerting on disk usage.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:41.199417226-08:00","updated_at":"2025-12-28T00:05:44.7635372-05:00","closed_at":"2025-12-28T00:05:44.7635372-05:00","close_reason":"Parent epic cancelled - browser-based dev approach abandoned","dependencies":[{"issue_id":"ops-jrz1-bhk","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.309592029-08:00","created_by":"daemon","metadata":"{}"}]}
{"id":"ops-jrz1-blh","title":"mautrix-slack edit panic persists in v25.11","description":"mautrix-slack panic on rapid message edits (race condition)\n\n**Root cause**: Edit event arrives before original message is stored in DB. ConvertEdit accesses nil metadata.\n\n**Location**: handleslack.go:575 - has TODO comment: 'this can panic?'\n\n**Reproduction**: Edit a Slack message within ~1 second of sending\n\n**Upstream status**: \n- v25.11 is latest (we're on it)\n- Known to devs (TODO in code)\n- No open issue filed yet\n\n**Stack trace**:\ngo.mau.fi/mautrix-slack/pkg/connector.(*SlackMessage).ConvertEdit\n handleslack.go:575\nmaunium.net/go/mautrix/bridgev2.(*Portal).handleRemoteEdit\n portal.go:2838","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-05T19:40:33.255395189-08:00","updated_at":"2025-12-28T00:06:14.637057055-05:00","closed_at":"2025-12-28T00:06:14.637057055-05:00","close_reason":"Duplicate of ops-jrz1-f15 which has fix ready","comments":[{"id":2,"issue_id":"ops-jrz1-blh","author":"dan","text":"Confirmed panic exists in nixpkgs-unstable from 2025-12-02. Fix will be addressed via platform upgrade (see ops-jrz1-00e).","created_at":"2025-12-08T23:54:57Z"}]} {"id":"ops-jrz1-blh","title":"mautrix-slack edit panic persists in v25.11","description":"mautrix-slack panic on rapid message edits (race condition)\n\n**Root cause**: Edit event arrives before original message is stored in DB. ConvertEdit accesses nil metadata.\n\n**Location**: handleslack.go:575 - has TODO comment: 'this can panic?'\n\n**Reproduction**: Edit a Slack message within ~1 second of sending\n\n**Upstream status**: \n- v25.11 is latest (we're on it)\n- Known to devs (TODO in code)\n- No open issue filed yet\n\n**Stack trace**:\ngo.mau.fi/mautrix-slack/pkg/connector.(*SlackMessage).ConvertEdit\n handleslack.go:575\nmaunium.net/go/mautrix/bridgev2.(*Portal).handleRemoteEdit\n portal.go:2838","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-05T19:40:33.255395189-08:00","updated_at":"2025-12-28T00:06:14.637057055-05:00","closed_at":"2025-12-28T00:06:14.637057055-05:00","close_reason":"Duplicate of ops-jrz1-f15 which has fix ready","comments":[{"id":2,"issue_id":"ops-jrz1-blh","author":"dan","text":"Confirmed panic exists in nixpkgs-unstable from 2025-12-02. Fix will be addressed via platform upgrade (see ops-jrz1-00e).","created_at":"2025-12-08T23:54:57Z"}]}
{"id":"ops-jrz1-cmv","title":"Add egress rate limiting (iptables)","description":"Hard limit outbound connections per user to prevent mass exfil/scanning.\n\n## Config\n```nix\nnetworking.firewall.extraCommands = ''\n # Rate limit new outbound connections for regular users (uid 1000+)\n iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \\\n -m limit --limit 30/min --limit-burst 60 -j ACCEPT\n iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \\\n -j LOG --log-prefix \"EGRESS-LIMIT: \"\n iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \\\n -j REJECT\n'';\n```\n\n## Behavior\n- 30 new connections/min sustained, burst of 60\n- Over limit: logged and rejected\n- Doesn't affect established connections\n\n## Testing\n- `for i in {1..100}; do curl -s ifconfig.me \u0026 done`\n- Should see EGRESS-LIMIT in journal after ~60","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T20:16:32.276607792-08:00","created_by":"dan","updated_at":"2026-01-02T21:12:35.5888406-08:00","closed_at":"2026-01-02T21:12:35.5888406-08:00","close_reason":"Closed"} {"id":"ops-jrz1-cmv","title":"Add egress rate limiting (iptables)","description":"Hard limit outbound connections per user to prevent mass exfil/scanning.\n\n## Config\n```nix\nnetworking.firewall.extraCommands = ''\n # Rate limit new outbound connections for regular users (uid 1000+)\n iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \\\n -m limit --limit 30/min --limit-burst 60 -j ACCEPT\n iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \\\n -j LOG --log-prefix \"EGRESS-LIMIT: \"\n iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \\\n -j REJECT\n'';\n```\n\n## Behavior\n- 30 new connections/min sustained, burst of 60\n- Over limit: logged and rejected\n- Doesn't affect established connections\n\n## Testing\n- `for i in {1..100}; do curl -s ifconfig.me \u0026 done`\n- Should see EGRESS-LIMIT in journal after ~60","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T20:16:32.276607792-08:00","created_by":"dan","updated_at":"2026-01-02T21:12:35.5888406-08:00","closed_at":"2026-01-02T21:12:35.5888406-08:00","close_reason":"Closed"}
@ -62,7 +62,7 @@
{"id":"ops-jrz1-kg0","title":"Switch to subdomain routing (dan.code.clarun.xyz)","description":"Path-based routing (/code/dan/) is fragile. Extensions assume root path, cookies scope incorrectly, PWA breaks. Switch to wildcard subdomains for cleaner isolation.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-05T15:32:19.283887085-08:00","updated_at":"2025-12-05T17:23:11.983564455-08:00","closed_at":"2025-12-05T17:23:11.983564455-08:00","dependencies":[{"issue_id":"ops-jrz1-kg0","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.043217984-08:00","created_by":"daemon","metadata":"{}"}]} {"id":"ops-jrz1-kg0","title":"Switch to subdomain routing (dan.code.clarun.xyz)","description":"Path-based routing (/code/dan/) is fragile. Extensions assume root path, cookies scope incorrectly, PWA breaks. Switch to wildcard subdomains for cleaner isolation.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-05T15:32:19.283887085-08:00","updated_at":"2025-12-05T17:23:11.983564455-08:00","closed_at":"2025-12-05T17:23:11.983564455-08:00","dependencies":[{"issue_id":"ops-jrz1-kg0","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.043217984-08:00","created_by":"daemon","metadata":"{}"}]}
{"id":"ops-jrz1-kia","title":"Container reset mechanism (keep workspace)","description":"If user breaks their environment, need simple way to wipe container and restore default image while preserving /workspace. Script or admin command.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:31.045592689-08:00","updated_at":"2025-12-28T00:05:44.757842852-05:00","closed_at":"2025-12-28T00:05:44.757842852-05:00","close_reason":"Parent epic cancelled - browser-based dev approach abandoned","dependencies":[{"issue_id":"ops-jrz1-kia","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.275530016-08:00","created_by":"daemon","metadata":"{}"}]} {"id":"ops-jrz1-kia","title":"Container reset mechanism (keep workspace)","description":"If user breaks their environment, need simple way to wipe container and restore default image while preserving /workspace. Script or admin command.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:31.045592689-08:00","updated_at":"2025-12-28T00:05:44.757842852-05:00","closed_at":"2025-12-28T00:05:44.757842852-05:00","close_reason":"Parent epic cancelled - browser-based dev approach abandoned","dependencies":[{"issue_id":"ops-jrz1-kia","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.275530016-08:00","created_by":"daemon","metadata":"{}"}]}
{"id":"ops-jrz1-lae","title":"egress-watchdog: Fix subshell gotcha in while-read pipeline","description":"while-read in pipeline runs in subshell - variables don't persist outside loop. Use process substitution: while read ...; done \u003c \u003c(echo \"$hits\" | grep ...). scripts/egress-watchdog:25","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-03T08:17:35.401495377-08:00","created_by":"dan","updated_at":"2026-01-03T09:30:50.535018144-08:00","closed_at":"2026-01-03T09:30:50.535018144-08:00","close_reason":"Fixed: using process substitution instead of pipeline subshell"} {"id":"ops-jrz1-lae","title":"egress-watchdog: Fix subshell gotcha in while-read pipeline","description":"while-read in pipeline runs in subshell - variables don't persist outside loop. Use process substitution: while read ...; done \u003c \u003c(echo \"$hits\" | grep ...). scripts/egress-watchdog:25","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-03T08:17:35.401495377-08:00","created_by":"dan","updated_at":"2026-01-03T09:30:50.535018144-08:00","closed_at":"2026-01-03T09:30:50.535018144-08:00","close_reason":"Fixed: using process substitution instead of pipeline subshell"}
{"id":"ops-jrz1-meh","title":"cpu-watchdog: Add flock for atomic strike counter updates","description":"Read-modify-write of strike counter not atomic. Systemd timer serializes runs so low risk now, but add flock if parallelism added later. scripts/cpu-watchdog:29-31","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-03T08:17:35.759126212-08:00","created_by":"dan","updated_at":"2026-01-03T08:26:28.66076716-08:00"} {"id":"ops-jrz1-meh","title":"cpu-watchdog: Add flock for atomic strike counter updates","description":"Read-modify-write of strike counter not atomic. Systemd timer serializes runs so low risk now, but add flock if parallelism added later. scripts/cpu-watchdog:29-31","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-03T08:17:35.759126212-08:00","created_by":"dan","updated_at":"2026-01-03T10:08:38.50903714-08:00","closed_at":"2026-01-03T10:08:38.50903714-08:00","close_reason":"Wontfix: systemd timer serializes runs, race condition is theoretical only"}
{"id":"ops-jrz1-mh2","title":"Research: Forgejo integration for shared projects","description":"How does beads/bd integrate with our Forgejo git server (git.clarun.xyz)?\n\n## Questions\n- Can bd sync to Forgejo repos?\n- How do dev users on the server collaborate on shared projects?\n- Is there a git workflow that makes sense (forks? shared repo? branches?)\n- Does bd need any special config for Forgejo vs GitHub?\n\n## Context\n- Forgejo running at git.clarun.xyz\n- Dev users have SSH access to server\n- May want shared project tracking via beads","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-02T16:24:01.771168961-08:00","created_by":"dan","updated_at":"2026-01-02T16:24:01.771168961-08:00"} {"id":"ops-jrz1-mh2","title":"Research: Forgejo integration for shared projects","description":"How does beads/bd integrate with our Forgejo git server (git.clarun.xyz)?\n\n## Questions\n- Can bd sync to Forgejo repos?\n- How do dev users on the server collaborate on shared projects?\n- Is there a git workflow that makes sense (forks? shared repo? branches?)\n- Does bd need any special config for Forgejo vs GitHub?\n\n## Context\n- Forgejo running at git.clarun.xyz\n- Dev users have SSH access to server\n- May want shared project tracking via beads","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-02T16:24:01.771168961-08:00","created_by":"dan","updated_at":"2026-01-02T16:24:01.771168961-08:00"}
{"id":"ops-jrz1-ndl","title":"Browser-based dev environment (code-server)","description":"Explore setting up browser-based development:\n\nOptions:\n- code-server / openvscode-server - VS Code in browser\n- ttyd / wetty - terminal in browser \n- PWA install to home screen for native app feel\n\nCould combine with Tailscale for secure access without exposing ports.\n\nRef: ops-dev thin client brainstorm session","notes":"Design doc created: specs/004-browser-dev-environment/design.md - covers architecture, tech choices, resource planning, security model, rollout phases","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-04T15:08:02.406274744-08:00","updated_at":"2025-12-05T17:05:52.872944892-08:00","closed_at":"2025-12-05T17:05:52.872944892-08:00"} {"id":"ops-jrz1-ndl","title":"Browser-based dev environment (code-server)","description":"Explore setting up browser-based development:\n\nOptions:\n- code-server / openvscode-server - VS Code in browser\n- ttyd / wetty - terminal in browser \n- PWA install to home screen for native app feel\n\nCould combine with Tailscale for secure access without exposing ports.\n\nRef: ops-dev thin client brainstorm session","notes":"Design doc created: specs/004-browser-dev-environment/design.md - covers architecture, tech choices, resource planning, security model, rollout phases","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-04T15:08:02.406274744-08:00","updated_at":"2025-12-05T17:05:52.872944892-08:00","closed_at":"2025-12-05T17:05:52.872944892-08:00"}
{"id":"ops-jrz1-nir","title":"RFC: SSH log noise reduction strategy","description":"Research showed 99.8% of SSH logs are scanner noise (9000 failed attempts/day). Options: (1) Change SSH port - simple, ~99% reduction (2) journald filter - surgical but complex (3) LogLevel ERROR - loses successful login audit trail (4) fail2ban - bans IPs, partial reduction. Orch consensus: Gemini opposed LogLevel ERROR due to losing audit trail, GPT supported. Need RFC to decide approach. See posture review from Dec 2025 session.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-04T22:55:13.990334935-08:00","updated_at":"2025-12-04T22:55:13.990334935-08:00"} {"id":"ops-jrz1-nir","title":"RFC: SSH log noise reduction strategy","description":"Research showed 99.8% of SSH logs are scanner noise (9000 failed attempts/day). Options: (1) Change SSH port - simple, ~99% reduction (2) journald filter - surgical but complex (3) LogLevel ERROR - loses successful login audit trail (4) fail2ban - bans IPs, partial reduction. Orch consensus: Gemini opposed LogLevel ERROR due to losing audit trail, GPT supported. Need RFC to decide approach. See posture review from Dec 2025 session.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-04T22:55:13.990334935-08:00","updated_at":"2025-12-04T22:55:13.990334935-08:00"}
@ -74,7 +74,7 @@
{"id":"ops-jrz1-qxr","title":"mautrix-slack message edit panic (upstream bug)","description":"Bridge upgraded to v25.11. Need to verify if edit panic is fixed by testing a Slack message edit. Watch logs: journalctl -u mautrix-slack -f | grep -E 'ERR|panic|edit'","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-05T18:22:38.18203834-08:00","updated_at":"2025-12-05T19:36:00.556011621-08:00","closed_at":"2025-12-05T19:36:00.556011621-08:00","dependencies":[{"issue_id":"ops-jrz1-qxr","depends_on_id":"ops-jrz1-03o","type":"blocks","created_at":"2025-12-05T18:24:23.259399275-08:00","created_by":"daemon","metadata":"{}"}]} {"id":"ops-jrz1-qxr","title":"mautrix-slack message edit panic (upstream bug)","description":"Bridge upgraded to v25.11. Need to verify if edit panic is fixed by testing a Slack message edit. Watch logs: journalctl -u mautrix-slack -f | grep -E 'ERR|panic|edit'","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-05T18:22:38.18203834-08:00","updated_at":"2025-12-05T19:36:00.556011621-08:00","closed_at":"2025-12-05T19:36:00.556011621-08:00","dependencies":[{"issue_id":"ops-jrz1-qxr","depends_on_id":"ops-jrz1-03o","type":"blocks","created_at":"2025-12-05T18:24:23.259399275-08:00","created_by":"daemon","metadata":"{}"}]}
{"id":"ops-jrz1-rkp","title":"Add egress abuse watchdog","description":"Monitor for users hitting egress rate limits, kill if sustained.\n\n## Script: /usr/local/bin/egress-watchdog\n```bash\n#\\!/usr/bin/env bash\n# Kill users who keep hitting egress limits\nTHRESHOLD=10 # EGRESS-LIMIT hits per minute\nCOUNTFILE=\"/var/lib/egress-watchdog\"\nmkdir -p \"$COUNTFILE\"\n\n# Count recent limit hits per UID\njournalctl -k --since \"1 minute ago\" 2\u003e/dev/null | grep \"EGRESS-LIMIT\" | \\\n grep -oP 'UID=\\K[0-9]+' | sort | uniq -c | while read count uid; do\n \n user=$(getent passwd \"$uid\" | cut -d: -f1)\n [ -z \"$user\" ] \u0026\u0026 continue\n \n if [ \"$count\" -gt \"$THRESHOLD\" ]; then\n strikes=$(cat \"$COUNTFILE/$user\" 2\u003e/dev/null || echo 0)\n strikes=$((strikes + 1))\n echo \"$strikes\" \u003e \"$COUNTFILE/$user\"\n logger -t egress-watchdog \"User $user hit egress limit $count times (strike $strikes/3)\"\n \n if [ \"$strikes\" -ge 3 ]; then\n /usr/local/bin/killswitch \"$user\" \"egress abuse ($count hits)\"\n rm -f \"$COUNTFILE/$user\"\n fi\n else\n rm -f \"$COUNTFILE/$user\"\n fi\ndone\n```\n\n## Behavior\n- Runs every minute (same timer as CPU watchdog, or separate)\n- 3 consecutive minutes of \u003e10 blocked connections = kill\n- Works with egress rate limiting (ops-jrz1-cmv)\n\n## Dependencies\n- Requires ops-jrz1-cmv (egress rate limiting)\n- Requires ops-jrz1-396 (killswitch script)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T20:21:09.516724064-08:00","created_by":"dan","updated_at":"2026-01-03T06:02:02.132992356-08:00","closed_at":"2026-01-03T06:02:02.132992356-08:00","close_reason":"Egress watchdog deployed and tested. Script monitors EGRESS-LIMIT kernel log entries, tracks strikes per user, kills after 3 strikes.","dependencies":[{"issue_id":"ops-jrz1-rkp","depends_on_id":"ops-jrz1-396","type":"blocks","created_at":"2026-01-02T20:21:14.314011866-08:00","created_by":"dan"},{"issue_id":"ops-jrz1-rkp","depends_on_id":"ops-jrz1-cmv","type":"blocks","created_at":"2026-01-02T20:21:14.352411765-08:00","created_by":"dan"}]} {"id":"ops-jrz1-rkp","title":"Add egress abuse watchdog","description":"Monitor for users hitting egress rate limits, kill if sustained.\n\n## Script: /usr/local/bin/egress-watchdog\n```bash\n#\\!/usr/bin/env bash\n# Kill users who keep hitting egress limits\nTHRESHOLD=10 # EGRESS-LIMIT hits per minute\nCOUNTFILE=\"/var/lib/egress-watchdog\"\nmkdir -p \"$COUNTFILE\"\n\n# Count recent limit hits per UID\njournalctl -k --since \"1 minute ago\" 2\u003e/dev/null | grep \"EGRESS-LIMIT\" | \\\n grep -oP 'UID=\\K[0-9]+' | sort | uniq -c | while read count uid; do\n \n user=$(getent passwd \"$uid\" | cut -d: -f1)\n [ -z \"$user\" ] \u0026\u0026 continue\n \n if [ \"$count\" -gt \"$THRESHOLD\" ]; then\n strikes=$(cat \"$COUNTFILE/$user\" 2\u003e/dev/null || echo 0)\n strikes=$((strikes + 1))\n echo \"$strikes\" \u003e \"$COUNTFILE/$user\"\n logger -t egress-watchdog \"User $user hit egress limit $count times (strike $strikes/3)\"\n \n if [ \"$strikes\" -ge 3 ]; then\n /usr/local/bin/killswitch \"$user\" \"egress abuse ($count hits)\"\n rm -f \"$COUNTFILE/$user\"\n fi\n else\n rm -f \"$COUNTFILE/$user\"\n fi\ndone\n```\n\n## Behavior\n- Runs every minute (same timer as CPU watchdog, or separate)\n- 3 consecutive minutes of \u003e10 blocked connections = kill\n- Works with egress rate limiting (ops-jrz1-cmv)\n\n## Dependencies\n- Requires ops-jrz1-cmv (egress rate limiting)\n- Requires ops-jrz1-396 (killswitch script)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T20:21:09.516724064-08:00","created_by":"dan","updated_at":"2026-01-03T06:02:02.132992356-08:00","closed_at":"2026-01-03T06:02:02.132992356-08:00","close_reason":"Egress watchdog deployed and tested. Script monitors EGRESS-LIMIT kernel log entries, tracks strikes per user, kills after 3 strikes.","dependencies":[{"issue_id":"ops-jrz1-rkp","depends_on_id":"ops-jrz1-396","type":"blocks","created_at":"2026-01-02T20:21:14.314011866-08:00","created_by":"dan"},{"issue_id":"ops-jrz1-rkp","depends_on_id":"ops-jrz1-cmv","type":"blocks","created_at":"2026-01-02T20:21:14.352411765-08:00","created_by":"dan"}]}
{"id":"ops-jrz1-sdz","title":"Remove /usr/local/bin scripts from server","description":"After declarative deployment works, clean up manually deployed scripts from /usr/local/bin on the server.","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-03T08:39:54.483032394-08:00","created_by":"dan","updated_at":"2026-01-03T09:20:34.591216526-08:00","closed_at":"2026-01-03T09:20:34.591216526-08:00","close_reason":"Removed all manual scripts from /usr/local/bin/","dependencies":[{"issue_id":"ops-jrz1-sdz","depends_on_id":"ops-jrz1-ujw","type":"blocks","created_at":"2026-01-03T08:40:02.851476398-08:00","created_by":"dan"},{"issue_id":"ops-jrz1-sdz","depends_on_id":"ops-jrz1-o9c","type":"blocks","created_at":"2026-01-03T08:45:48.023849189-08:00","created_by":"dan"}]} {"id":"ops-jrz1-sdz","title":"Remove /usr/local/bin scripts from server","description":"After declarative deployment works, clean up manually deployed scripts from /usr/local/bin on the server.","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-03T08:39:54.483032394-08:00","created_by":"dan","updated_at":"2026-01-03T09:20:34.591216526-08:00","closed_at":"2026-01-03T09:20:34.591216526-08:00","close_reason":"Removed all manual scripts from /usr/local/bin/","dependencies":[{"issue_id":"ops-jrz1-sdz","depends_on_id":"ops-jrz1-ujw","type":"blocks","created_at":"2026-01-03T08:40:02.851476398-08:00","created_by":"dan"},{"issue_id":"ops-jrz1-sdz","depends_on_id":"ops-jrz1-o9c","type":"blocks","created_at":"2026-01-03T08:45:48.023849189-08:00","created_by":"dan"}]}
{"id":"ops-jrz1-t73","title":"Rename learner to dev in scripts and docs","description":"Rename terminology from \"learner\" to \"dev\" or \"user\" across:\n\n- scripts/learner-add.sh → dev-add.sh\n- scripts/learner-remove.sh → dev-remove.sh\n- /etc/slack-learner.env → /etc/slack-dev.env\n- learners group → devs group\n- docs/learner-*.md\n- tests/test-learner-env.sh\n\nLow priority cleanup.","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-02T12:32:40.340984626-08:00","created_by":"dan","updated_at":"2026-01-02T12:32:40.340984626-08:00"} {"id":"ops-jrz1-t73","title":"Rename learner to dev in scripts and docs","description":"Rename terminology from \"learner\" to \"dev\" or \"user\" across:\n\n- scripts/learner-add.sh → dev-add.sh\n- scripts/learner-remove.sh → dev-remove.sh\n- /etc/slack-learner.env → /etc/slack-dev.env\n- learners group → devs group\n- docs/learner-*.md\n- tests/test-learner-env.sh\n\nLow priority cleanup.","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-02T12:32:40.340984626-08:00","created_by":"dan","updated_at":"2026-01-03T10:37:34.321661169-08:00","closed_at":"2026-01-03T10:37:34.321661169-08:00","close_reason":"Renamed learner to dev across scripts, docs, tests, and configuration"}
{"id":"ops-jrz1-u0w","title":"Security review of running server","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-04T21:03:22.420507724-08:00","updated_at":"2025-12-04T21:04:31.989886731-08:00","closed_at":"2025-12-04T21:04:31.989886731-08:00"} {"id":"ops-jrz1-u0w","title":"Security review of running server","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-04T21:03:22.420507724-08:00","updated_at":"2025-12-04T21:04:31.989886731-08:00","closed_at":"2025-12-04T21:04:31.989886731-08:00"}
{"id":"ops-jrz1-ujw","title":"Update systemd services to use nix store paths","description":"Change ExecStart from /usr/local/bin/cpu-watchdog to use the derivation path. Either reference package directly or use pkgs.writeShellApplication.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-03T08:39:54.227335183-08:00","created_by":"dan","updated_at":"2026-01-03T09:20:08.685831615-08:00","closed_at":"2026-01-03T09:20:08.685831615-08:00","close_reason":"Systemd services now reference Nix store paths via ${pkg}/bin/script","dependencies":[{"issue_id":"ops-jrz1-ujw","depends_on_id":"ops-jrz1-5ef","type":"blocks","created_at":"2026-01-03T08:40:02.815677839-08:00","created_by":"dan"}]} {"id":"ops-jrz1-ujw","title":"Update systemd services to use nix store paths","description":"Change ExecStart from /usr/local/bin/cpu-watchdog to use the derivation path. Either reference package directly or use pkgs.writeShellApplication.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-03T08:39:54.227335183-08:00","created_by":"dan","updated_at":"2026-01-03T09:20:08.685831615-08:00","closed_at":"2026-01-03T09:20:08.685831615-08:00","close_reason":"Systemd services now reference Nix store paths via ${pkg}/bin/script","dependencies":[{"issue_id":"ops-jrz1-ujw","depends_on_id":"ops-jrz1-5ef","type":"blocks","created_at":"2026-01-03T08:40:02.815677839-08:00","created_by":"dan"}]}
{"id":"ops-jrz1-vix","title":"Evaluate home-manager for per-user config","description":"Evaluate whether home-manager adds value for our setup.\n\n## What home-manager could manage\n- Shell config (.bashrc, .zshrc)\n- Git config (.gitconfig)\n- Tool configs (~/.config/*)\n- direnv integration\n\n## Questions\n- Do we need declarative per-user dotfiles?\n- Is the complexity worth it for a small team?\n- Can we start without it and add later?\n\n## Recommendation from consensus\n\"Optional but recommended\" - good for pushing default configs to all devs.\nStart without it, add if pain point emerges.","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-02T16:36:04.849881753-08:00","created_by":"dan","updated_at":"2026-01-02T16:36:04.849881753-08:00"} {"id":"ops-jrz1-vix","title":"Evaluate home-manager for per-user config","description":"Evaluate whether home-manager adds value for our setup.\n\n## What home-manager could manage\n- Shell config (.bashrc, .zshrc)\n- Git config (.gitconfig)\n- Tool configs (~/.config/*)\n- direnv integration\n\n## Questions\n- Do we need declarative per-user dotfiles?\n- Is the complexity worth it for a small team?\n- Can we start without it and add later?\n\n## Recommendation from consensus\n\"Optional but recommended\" - good for pushing default configs to all devs.\nStart without it, add if pain point emerges.","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-02T16:36:04.849881753-08:00","created_by":"dan","updated_at":"2026-01-02T16:36:04.849881753-08:00"}
@ -83,5 +83,5 @@
{"id":"ops-jrz1-wj2","title":"Design API key provisioning strategy","description":"opencode needs API keys (OpenAI, Anthropic). Options: 1) Shared key with proxy + rate limiting, 2) Per-user keys in sops-nix. Need to prevent key exposure and enable usage tracking.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-05T15:32:19.526073243-08:00","updated_at":"2025-12-05T17:25:10.534718515-08:00","closed_at":"2025-12-05T17:25:10.534718515-08:00","dependencies":[{"issue_id":"ops-jrz1-wj2","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.103332379-08:00","created_by":"daemon","metadata":"{}"}]} {"id":"ops-jrz1-wj2","title":"Design API key provisioning strategy","description":"opencode needs API keys (OpenAI, Anthropic). Options: 1) Shared key with proxy + rate limiting, 2) Per-user keys in sops-nix. Need to prevent key exposure and enable usage tracking.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-05T15:32:19.526073243-08:00","updated_at":"2025-12-05T17:25:10.534718515-08:00","closed_at":"2025-12-05T17:25:10.534718515-08:00","dependencies":[{"issue_id":"ops-jrz1-wj2","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.103332379-08:00","created_by":"daemon","metadata":"{}"}]}
{"id":"ops-jrz1-xz1","title":"Fix maubot admin UI exposed to internet (port 29316)","description":"Maubot admin UI on port 29316 is publicly accessible (returns 401 but API surface exposed). Firewall explicitly allows this port. Risk: brute force on admin password, direct exploit of any maubot vulnerabilities. Fix: bind to 127.0.0.1 only, remove from firewall, access via SSH tunnel.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-04T21:03:22.531676543-08:00","updated_at":"2025-12-04T22:35:24.162735368-08:00","closed_at":"2025-12-04T22:35:24.162735368-08:00"} {"id":"ops-jrz1-xz1","title":"Fix maubot admin UI exposed to internet (port 29316)","description":"Maubot admin UI on port 29316 is publicly accessible (returns 401 but API surface exposed). Firewall explicitly allows this port. Risk: brute force on admin password, direct exploit of any maubot vulnerabilities. Fix: bind to 127.0.0.1 only, remove from firewall, access via SSH tunnel.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-04T21:03:22.531676543-08:00","updated_at":"2025-12-04T22:35:24.162735368-08:00","closed_at":"2025-12-04T22:35:24.162735368-08:00"}
{"id":"ops-jrz1-xz7","title":"Research: Multi-user auth storage for agentic coders","description":"Investigate where auth credentials are stored for each agentic coder when multiple users authenticate:\n\n## Questions\n- Claude Code: Where is OAuth token stored? ~/.claude? Conflicts between users?\n- opencode: Auth storage location?\n- gemini-cli: Auth storage?\n- codex: Auth storage?\n\n## Goal\nUnderstand if there are isolation issues when multiple users auth on same server.","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-02T17:30:15.028994987-08:00","created_by":"dan","updated_at":"2026-01-02T17:30:15.028994987-08:00"} {"id":"ops-jrz1-xz7","title":"Research: Multi-user auth storage for agentic coders","description":"Investigate where auth credentials are stored for each agentic coder when multiple users authenticate:\n\n## Questions\n- Claude Code: Where is OAuth token stored? ~/.claude? Conflicts between users?\n- opencode: Auth storage location?\n- gemini-cli: Auth storage?\n- codex: Auth storage?\n\n## Goal\nUnderstand if there are isolation issues when multiple users auth on same server.","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-02T17:30:15.028994987-08:00","created_by":"dan","updated_at":"2026-01-02T17:30:15.028994987-08:00"}
{"id":"ops-jrz1-yhu","title":"configuration.nix: Consider custom iptables chain for egress rules","description":"Same iptables match pattern repeated 8 times. Could create custom chain for cleaner rule management. Optional - readability tradeoff. configuration.nix:68-79","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-03T08:17:35.532609792-08:00","created_by":"dan","updated_at":"2026-01-03T08:26:28.411759428-08:00"} {"id":"ops-jrz1-yhu","title":"configuration.nix: Consider custom iptables chain for egress rules","description":"Same iptables match pattern repeated 8 times. Could create custom chain for cleaner rule management. Optional - readability tradeoff. configuration.nix:68-79","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-03T08:17:35.532609792-08:00","created_by":"dan","updated_at":"2026-01-03T10:07:28.725278889-08:00","closed_at":"2026-01-03T10:07:28.725278889-08:00","close_reason":"Wontfix: current inline rules work fine, custom chain is marginal improvement"}
{"id":"ops-jrz1-zvh","title":"Fix maubot health check (failing every 5 min)","description":"Health check at /_matrix/maubot/v1/version returns 401 (auth required). Check script doesn't provide auth token. Spamming error logs every 5 minutes.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-04T22:55:25.755541054-08:00","updated_at":"2025-12-05T02:00:19.284410671-08:00","closed_at":"2025-12-05T02:00:19.284410671-08:00"} {"id":"ops-jrz1-zvh","title":"Fix maubot health check (failing every 5 min)","description":"Health check at /_matrix/maubot/v1/version returns 401 (auth required). Check script doesn't provide auth token. Spamming error logs every 5 minutes.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-04T22:55:25.755541054-08:00","updated_at":"2025-12-05T02:00:19.284410671-08:00","closed_at":"2025-12-05T02:00:19.284410671-08:00"}

View file

@ -44,8 +44,8 @@ NixOS-based Matrix homeserver (conduwuit) with mautrix-slack bridge for Slack
├── killswitch # Emergency user termination ├── killswitch # Emergency user termination
├── cpu-watchdog # CPU abuse detection ├── cpu-watchdog # CPU abuse detection
├── egress-watchdog # Egress rate limit abuse detection ├── egress-watchdog # Egress rate limit abuse detection
├── learner-add.sh # Add dev user account ├── dev-add.sh # Add dev user account
└── learner-remove.sh # Remove dev user account └── dev-remove.sh # Remove dev user account
``` ```
## Commands ## Commands

View file

@ -35,16 +35,16 @@ let
# Admin Scripts - Added to systemPackages for interactive use # Admin Scripts - Added to systemPackages for interactive use
# ========================================================================== # ==========================================================================
learner-add = pkgs.writeShellApplication { dev-add = pkgs.writeShellApplication {
name = "learner-add"; name = "dev-add";
runtimeInputs = with pkgs; [ shadow coreutils iproute2 gnugrep gawk ]; runtimeInputs = with pkgs; [ shadow coreutils iproute2 gnugrep gawk ];
text = builtins.readFile ./scripts/learner-add.sh; text = builtins.readFile ./scripts/dev-add.sh;
}; };
learner-remove = pkgs.writeShellApplication { dev-remove = pkgs.writeShellApplication {
name = "learner-remove"; name = "dev-remove";
runtimeInputs = with pkgs; [ shadow coreutils gnutar procps findutils ]; runtimeInputs = with pkgs; [ shadow coreutils gnutar procps findutils ];
text = builtins.readFile ./scripts/learner-remove.sh; text = builtins.readFile ./scripts/dev-remove.sh;
}; };
in in
@ -83,8 +83,8 @@ in
# For npm-based AI tools (gemini-cli, codex): users run npm install # For npm-based AI tools (gemini-cli, codex): users run npm install
nodejs_22 nodejs_22
# Admin scripts (declarative deployment) # Admin scripts (declarative deployment)
learner-add dev-add
learner-remove dev-remove
]; ];
# Add ~/.local/bin and /usr/local/bin to PATH for manually installed tools # Add ~/.local/bin and /usr/local/bin to PATH for manually installed tools
@ -117,7 +117,7 @@ in
# Egress controls for regular users # Egress controls for regular users
# UID range: 1000 (first regular user) to 65534 (nobody - excluded from controls) # UID range: 1000 (first regular user) to 65534 (nobody - excluded from controls)
# This covers all learner accounts while excluding system services # This covers all dev accounts while excluding system services
extraCommands = '' extraCommands = ''
# Log all new outbound connections from regular users # Log all new outbound connections from regular users
iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \ iptables -A OUTPUT -m state --state NEW -m owner --uid-owner 1000:65534 \

View file

@ -1,22 +1,22 @@
# Learner Account Administration # Dev Account Administration
Guide for managing learner accounts on the maubot development server. Guide for managing dev accounts on the maubot development server.
## Adding a Learner ## Adding a Dev
1. Get the learner's SSH public key (they run `cat ~/.ssh/id_ed25519.pub`) 1. Get the dev's SSH public key (they run `cat ~/.ssh/id_ed25519.pub`)
2. SSH to the server and run: 2. SSH to the server and run:
```bash ```bash
sudo /path/to/scripts/learner-add.sh <username> '<ssh-public-key>' sudo /path/to/scripts/dev-add.sh <username> '<ssh-public-key>'
``` ```
Example: Example:
```bash ```bash
sudo ./scripts/learner-add.sh alice 'ssh-ed25519 AAAAC3... alice@laptop' sudo ./scripts/dev-add.sh alice 'ssh-ed25519 AAAAC3... alice@laptop'
``` ```
3. The script will output onboarding instructions - send these to the learner. 3. The script will output onboarding instructions - send these to the dev.
### What Gets Created ### What Gets Created
@ -24,37 +24,37 @@ Guide for managing learner accounts on the maubot development server.
- `~/plugins/hello_<username>/` - starter maubot plugin - `~/plugins/hello_<username>/` - starter maubot plugin
- The plugin includes a working hello/ping bot - The plugin includes a working hello/ping bot
## Removing a Learner ## Removing a Dev
```bash ```bash
sudo ./scripts/learner-remove.sh <username> sudo ./scripts/dev-remove.sh <username>
``` ```
With archive (saves home directory before deleting): With archive (saves home directory before deleting):
```bash ```bash
sudo ./scripts/learner-remove.sh <username> --archive sudo ./scripts/dev-remove.sh <username> --archive
``` ```
Archives are saved to `/var/backups/learners/`. Archives are saved to `/var/backups/devs/`.
## Maubot Setup (One-Time) ## Maubot Setup (One-Time)
After adding the first learner, set up the shared test environment: After adding the first dev, set up the shared test environment:
1. Create a Matrix bot user for learners (via Element or API) 1. Create a Matrix bot user for devs (via Element or API)
2. In maubot admin (http://localhost:29316): 2. In maubot admin (http://localhost:29316):
- Add the bot user as a client - Add the bot user as a client
- Learners will create instances using this client - Devs will create instances using this client
3. Create `#learners-sandbox` room: 3. Create `#devs-sandbox` room:
- Create the room in Matrix - Create the room in Matrix
- Invite the bot user - Invite the bot user
- Give learners the room ID/alias - Give devs the room ID/alias
## Monitoring ## Monitoring
### Check Learner Plugin Status ### Check Dev Plugin Status
```bash ```bash
# See what plugins are loaded # See what plugins are loaded
@ -62,7 +62,7 @@ curl -s http://localhost:29316/_matrix/maubot/v1/plugins \
-H "Authorization: Bearer <token>" | jq -H "Authorization: Bearer <token>" | jq
``` ```
### View Learner Directories ### View Dev Directories
```bash ```bash
ls -la /home/*/plugins/ ls -la /home/*/plugins/
@ -76,7 +76,7 @@ journalctl -u maubot -f
## Troubleshooting ## Troubleshooting
### Learner Can't Connect ### Dev Can't Connect
1. Verify user exists: `id <username>` 1. Verify user exists: `id <username>`
2. Check SSH key: `cat /home/<username>/.ssh/authorized_keys` 2. Check SSH key: `cat /home/<username>/.ssh/authorized_keys`
@ -95,7 +95,7 @@ journalctl -u maubot -f
### Disk Space ### Disk Space
Monitor learner disk usage: Monitor dev disk usage:
```bash ```bash
du -sh /home/*/ du -sh /home/*/
``` ```

View file

@ -1,4 +1,4 @@
# Maubot Plugin Development - Learner Onboarding # Maubot Plugin Development - Dev Onboarding
This guide walks you through setting up your development environment for building maubot plugins. This guide walks you through setting up your development environment for building maubot plugins.
@ -77,11 +77,11 @@ Your starter plugin responds to two commands:
- Go to "Instances" tab - Go to "Instances" tab
- Click "Create instance" - Click "Create instance"
- Select your plugin and a bot user - Select your plugin and a bot user
- Add `#learners-sandbox` to allowed rooms - Add `#devs-sandbox` to allowed rooms
- Click "Create" - Click "Create"
6. Test in Matrix: 6. Test in Matrix:
- Join the `#learners-sandbox` room - Join the `#devs-sandbox` room
- Type `!hello` - your bot should respond! - Type `!hello` - your bot should respond!
## Plugin Development ## Plugin Development
@ -172,6 +172,6 @@ In the maubot admin UI, go to "Logs" to see recent activity and errors.
## Getting Help ## Getting Help
- Ask in `#learners-sandbox` - other learners and admins can help - Ask in `#devs-sandbox` - other devs and admins can help
- Check the maubot logs for error messages - Check the maubot logs for error messages
- Review the maubot documentation - Review the maubot documentation

View file

@ -1,10 +1,10 @@
# Direct Slack Bot Development for Learners # Direct Slack Bot Development for Devs
Design doc for the "direct to Slack" learner path, bypassing Matrix/maubot. Design doc for the "direct to Slack" dev path, bypassing Matrix/maubot.
## Overview ## Overview
Learners write Python bots using `slack-bolt` that connect directly to Slack via Socket Mode. No Matrix, no bridge, no maubot. Devs write Python bots using `slack-bolt` that connect directly to Slack via Socket Mode. No Matrix, no bridge, no maubot.
``` ```
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
@ -21,7 +21,7 @@ Learners write Python bots using `slack-bolt` that connect directly to Slack via
### Credentials ### Credentials
Shared Slack App tokens stored in `/etc/slack-learner.env`: Shared Slack App tokens stored in `/etc/slack-dev.env`:
| Variable | Purpose | | Variable | Purpose |
|----------|---------| |----------|---------|
@ -32,24 +32,24 @@ These come from the existing mautrix-slack bridge login (Chochacho workspace, vl
### Access Control ### Access Control
- File owned by `root:learners`, mode `640` - File owned by `root:devs`, mode `640`
- Learner users added to `learners` group on creation - Dev users added to `devs` group on creation
- `.bashrc` sources the env file on login - `.bashrc` sources the env file on login
### Scripts ### Scripts
``` ```
/usr/local/bin/learner-add.sh <username> '<ssh-pubkey>' /usr/local/bin/dev-add.sh <username> '<ssh-pubkey>'
/usr/local/bin/learner-remove.sh <username> [--archive] /usr/local/bin/dev-remove.sh <username> [--archive]
``` ```
## Learner Workflow ## Dev Workflow
### 1. Get Access ### 1. Get Access
Send SSH pubkey to admin. Admin runs: Send SSH pubkey to admin. Admin runs:
```bash ```bash
ssh root@ops-jrz1 'learner-add.sh alice "ssh-ed25519 AAAA..."' ssh root@ops-jrz1 'dev-add.sh alice "ssh-ed25519 AAAA..."'
``` ```
### 2. Connect ### 2. Connect
@ -166,10 +166,10 @@ say(blocks=[
### Risks ### Risks
1. **Shared tokens** - All learners use same bot identity 1. **Shared tokens** - All devs use same bot identity
2. **Token exposure** - If leaked, affects everyone 2. **Token exposure** - If leaked, affects everyone
3. **Rate limits** - Shared across all bots 3. **Rate limits** - Shared across all bots
4. **Process sprawl** - N learners = N processes to manage 4. **Process sprawl** - N devs = N processes to manage
5. **No guardrails** - Bad code can spam/crash easily 5. **No guardrails** - Bad code can spam/crash easily
### Mitigations ### Mitigations
@ -184,10 +184,10 @@ say(blocks=[
### Process Management ### Process Management
Options: Options:
1. **systemd user services** - Each learner manages their own 1. **systemd user services** - Each dev manages their own
2. **supervisor** - Central process manager 2. **supervisor** - Central process manager
3. **tmux/screen** - Manual but simple 3. **tmux/screen** - Manual but simple
4. **Container per learner** - Isolation but complex 4. **Container per dev** - Isolation but complex
### Starter Template ### Starter Template
@ -197,17 +197,17 @@ Create `~/slack-bot-template/` with:
- `Makefile` - run, install targets - `Makefile` - run, install targets
- `README.md` - Quick start - `README.md` - Quick start
### Per-Learner Bot Identity ### Per-Dev Bot Identity
Create separate Slack App per learner: Create separate Slack App per dev:
- More setup friction - More setup friction
- Full isolation - Full isolation
- Each bot has own name/avatar - Each bot has own name/avatar
### Test Channel ### Test Channel
Create `#learner-sandbox` in Slack: Create `#dev-sandbox` in Slack:
- All learner bots invited - All dev bots invited
- Safe place to spam - Safe place to spam
- Doesn't pollute real channels - Doesn't pollute real channels

View file

@ -7,11 +7,11 @@ ops-jrz1 is a shared development server for learning agentic coding. Current sec
## Threat Model ## Threat Model
### Assumed Users ### Assumed Users
- **Learners**: Trusted individuals learning agentic coding - **Devs**: Trusted individuals learning agentic coding
- **Agentic Coders**: AI tools (Claude, opencode, etc.) running with user privileges - **Agentic Coders**: AI tools (Claude, opencode, etc.) running with user privileges
### Threat Actors ### Threat Actors
1. **Curious learner** - Explores beyond their sandbox (low intent, low skill) 1. **Curious dev** - Explores beyond their sandbox (low intent, low skill)
2. **Compromised AI agent** - Prompt injection or malicious code execution 2. **Compromised AI agent** - Prompt injection or malicious code execution
3. **Malicious insider** - Intentional abuse (not in scope for learning environment) 3. **Malicious insider** - Intentional abuse (not in scope for learning environment)
@ -26,7 +26,7 @@ ops-jrz1 is a shared development server for learning agentic coding. Current sec
|---------|--------| |---------|--------|
| Firewall | Only 22, 80, 443 open | | Firewall | Only 22, 80, 443 open |
| Internal services | Bound to 127.0.0.1 only | | Internal services | Bound to 127.0.0.1 only |
| sudo access | Denied for learner users | | sudo access | Denied for dev users |
| sops secrets | Root-only (/run/secrets) | | sops secrets | Root-only (/run/secrets) |
| SSH auth | Key-only, no passwords | | SSH auth | Key-only, no passwords |
@ -44,8 +44,8 @@ ops-jrz1 is a shared development server for learning agentic coding. Current sec
### Secrets at Risk ### Secrets at Risk
| Secret | Location | Access | Risk | | Secret | Location | Access | Risk |
|--------|----------|--------|------| |--------|----------|--------|------|
| Slack bot token | /etc/slack-learner.env | learners group | Low - shared intentionally | | Slack bot token | /etc/slack-dev.env | devs group | Low - shared intentionally |
| Slack app token | /etc/slack-learner.env | learners group | Low - shared intentionally | | Slack app token | /etc/slack-dev.env | devs group | Low - shared intentionally |
| Matrix registration | /run/secrets | root only | Protected | | Matrix registration | /run/secrets | root only | Protected |
| Maubot credentials | /run/secrets | root only | Protected | | Maubot credentials | /run/secrets | root only | Protected |
| User SSH keys | ~/.ssh | user only | Protected | | User SSH keys | ~/.ssh | user only | Protected |

View file

@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# learner-add.sh - Add a new dev user account # dev-add.sh - Add a new dev user account
# Usage: learner-add.sh <username> <ssh-pubkey> # Usage: dev-add.sh <username> <ssh-pubkey>
# #
# Creates: # Creates:
# - Unix user account with SSH key # - Unix user account with SSH key
# - Adds to learners group (Slack token access) # - Adds to devs group (Slack token access)
# - Outputs onboarding instructions # - Outputs onboarding instructions
set -euo pipefail set -euo pipefail
@ -23,7 +23,7 @@ usage() {
echo "Usage: $0 <username> <ssh-pubkey>" echo "Usage: $0 <username> <ssh-pubkey>"
echo "" echo ""
echo "Arguments:" echo "Arguments:"
echo " username - Learner's username (alphanumeric, 3-16 chars)" echo " username - Dev's username (alphanumeric, 3-16 chars)"
echo " ssh-pubkey - SSH public key (starts with ssh-ed25519, ssh-rsa, etc.)" echo " ssh-pubkey - SSH public key (starts with ssh-ed25519, ssh-rsa, etc.)"
echo "" echo ""
echo "Example:" echo "Example:"
@ -66,8 +66,8 @@ create_user() {
# Make home directory private (not world-readable) # Make home directory private (not world-readable)
chmod 700 "/home/$username" chmod 700 "/home/$username"
# Add to learners group for Slack token access # Add to devs group for Slack token access
usermod -aG learners "$username" usermod -aG devs "$username"
# Set up SSH key # Set up SSH key
local ssh_dir="/home/$username/.ssh" local ssh_dir="/home/$username/.ssh"
@ -81,7 +81,7 @@ create_user() {
{ {
echo '' echo ''
echo '# Slack bot development tokens' echo '# Slack bot development tokens'
echo 'source /etc/slack-learner.env' echo 'source /etc/slack-dev.env'
} >> "/home/$username/.bashrc" } >> "/home/$username/.bashrc"
log_info "User created with SSH access" log_info "User created with SSH access"

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# learner-remove.sh - Remove a learner account # dev-remove.sh - Remove a dev account
# Usage: learner-remove.sh <username> [--archive] # Usage: dev-remove.sh <username> [--archive]
# #
# Removes: # Removes:
# - Unix user account # - Unix user account
@ -10,7 +10,7 @@
set -euo pipefail set -euo pipefail
MAUBOT_PLUGINS_DIR="/var/lib/maubot/plugins" MAUBOT_PLUGINS_DIR="/var/lib/maubot/plugins"
ARCHIVE_DIR="/var/backups/learners" ARCHIVE_DIR="/var/backups/devs"
# Colors for output # Colors for output
RED='\033[0;31m' RED='\033[0;31m'
@ -26,7 +26,7 @@ usage() {
echo "Usage: $0 <username> [--archive]" echo "Usage: $0 <username> [--archive]"
echo "" echo ""
echo "Arguments:" echo "Arguments:"
echo " username - Learner's username to remove" echo " username - Dev's username to remove"
echo " --archive - Archive home directory instead of deleting" echo " --archive - Archive home directory instead of deleting"
echo "" echo ""
echo "Example:" echo "Example:"
@ -189,7 +189,7 @@ main() {
remove_user "$username" remove_user "$username"
log_info "Learner '$username' removed successfully" log_info "Dev '$username' removed successfully"
} }
main "$@" main "$@"

View file

@ -13,7 +13,7 @@ Provide VS Code in the browser via code-server, with:
|---------|-------------|-------| |---------|-------------|-------|
| **Non-programmer** | Learning to code with AI assistance | GUI-first, minimal friction, no terminal knowledge required | | **Non-programmer** | Learning to code with AI assistance | GUI-first, minimal friction, no terminal knowledge required |
| **Programmer (testing)** | Evaluating AI coding tools | Fast setup, full terminal access, multiple language support | | **Programmer (testing)** | Evaluating AI coding tools | Fast setup, full terminal access, multiple language support |
| **Learner** | Learning AI-assisted dev or new languages | Gentle on-ramp, room to grow, pre-configured tools | | **Dev** | Learning AI-assisted dev or new languages | Gentle on-ramp, room to grow, pre-configured tools |
## Requirements ## Requirements
@ -529,4 +529,4 @@ Enterprise platform for provisioning dev environments.
| Minimal resources | High learning curve | | Minimal resources | High learning curve |
| Power user friendly | Non-programmers excluded | | Power user friendly | Non-programmers excluded |
**Why not chosen**: Must support non-programmer learners with GUI. **Why not chosen**: Must support non-programmer devs with GUI.

View file

@ -1,4 +1,4 @@
# Integration tests for learner dev environment # Integration tests for dev dev environment
# Run from repo root: make -C tests <target> # Run from repo root: make -C tests <target>
SERVER := 45.77.205.49 SERVER := 45.77.205.49
@ -7,12 +7,12 @@ USER := dantest
.PHONY: all env slack-bolt vscode help .PHONY: all env slack-bolt vscode help
help: help:
@echo "Learner Environment Integration Tests" @echo "Dev Environment Integration Tests"
@echo "" @echo ""
@echo "Usage: make -C tests <target> [USER=username]" @echo "Usage: make -C tests <target> [USER=username]"
@echo "" @echo ""
@echo "Targets:" @echo "Targets:"
@echo " env - Test learner environment (SSH, tokens, Python)" @echo " env - Test dev environment (SSH, tokens, Python)"
@echo " slack-bolt - Test slack-bolt Socket Mode connection" @echo " slack-bolt - Test slack-bolt Socket Mode connection"
@echo " vscode - Test VS Code Remote-SSH compatibility" @echo " vscode - Test VS Code Remote-SSH compatibility"
@echo " all - Run all tests" @echo " all - Run all tests"
@ -25,11 +25,11 @@ help:
all: env slack-bolt all: env slack-bolt
env: env:
@./test-learner-env.sh $(USER) @./test-dev-env.sh $(USER)
slack-bolt: slack-bolt:
@echo "Testing slack-bolt on server as $(USER)..." @echo "Testing slack-bolt on server as $(USER)..."
@ssh $(USER)@$(SERVER) 'source /etc/slack-learner.env && \ @ssh $(USER)@$(SERVER) 'source /etc/slack-dev.env && \
uv pip install --quiet slack-bolt 2>/dev/null && \ uv pip install --quiet slack-bolt 2>/dev/null && \
python3 -' < test-slack-bolt.py python3 -' < test-slack-bolt.py

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# test-learner-env.sh - Integration tests for learner dev environment # test-dev-env.sh - Integration tests for dev dev environment
# Run from local machine: ./tests/test-learner-env.sh [username] [ssh-key] # Run from local machine: ./tests/test-dev-env.sh [username] [ssh-key]
# Default username: dantest # Default username: dantest
set -euo pipefail set -euo pipefail
@ -30,7 +30,7 @@ fail() { FAIL=$((FAIL + 1)); red " FAIL: $1"; }
skip() { yellow " SKIP: $1"; } skip() { yellow " SKIP: $1"; }
echo "==========================================" echo "=========================================="
echo "Learner Environment Tests" echo "Dev Environment Tests"
echo "Server: $SERVER" echo "Server: $SERVER"
echo "User: $USER" echo "User: $USER"
echo "==========================================" echo "=========================================="
@ -67,31 +67,31 @@ fi
# --------------------------------------------- # ---------------------------------------------
echo "" echo ""
echo "## 3. Learners group membership" echo "## 3. Devs group membership"
# --------------------------------------------- # ---------------------------------------------
if ssh_cmd 'groups' | grep -q learners; then if ssh_cmd 'groups' | grep -q devs; then
pass "User in learners group" pass "User in devs group"
else else
fail "User NOT in learners group" fail "User NOT in devs group"
fi fi
# --------------------------------------------- # ---------------------------------------------
echo "" echo ""
echo "## 4. Slack tokens accessible" echo "## 4. Slack tokens accessible"
# --------------------------------------------- # ---------------------------------------------
if ssh_cmd 'test -r /etc/slack-learner.env'; then if ssh_cmd 'test -r /etc/slack-dev.env'; then
pass "Slack env file readable" pass "Slack env file readable"
else else
fail "Slack env file NOT readable (check group permissions)" fail "Slack env file NOT readable (check group permissions)"
fi fi
if ssh_cmd 'source /etc/slack-learner.env && test -n "$SLACK_BOT_TOKEN"' &>/dev/null; then if ssh_cmd 'source /etc/slack-dev.env && test -n "$SLACK_BOT_TOKEN"' &>/dev/null; then
pass "SLACK_BOT_TOKEN set" pass "SLACK_BOT_TOKEN set"
else else
fail "SLACK_BOT_TOKEN not set" fail "SLACK_BOT_TOKEN not set"
fi fi
if ssh_cmd 'source /etc/slack-learner.env && test -n "$SLACK_APP_TOKEN"' &>/dev/null; then if ssh_cmd 'source /etc/slack-dev.env && test -n "$SLACK_APP_TOKEN"' &>/dev/null; then
pass "SLACK_APP_TOKEN set" pass "SLACK_APP_TOKEN set"
else else
fail "SLACK_APP_TOKEN not set" fail "SLACK_APP_TOKEN not set"
@ -140,7 +140,7 @@ echo "## 7. Slack API connectivity"
# --------------------------------------------- # ---------------------------------------------
echo " Testing Slack API auth (this may take a moment)..." echo " Testing Slack API auth (this may take a moment)..."
SLACK_TEST=$(ssh_cmd 'source /etc/slack-learner.env && python3 -c " SLACK_TEST=$(ssh_cmd 'source /etc/slack-dev.env && python3 -c "
import urllib.request import urllib.request
import urllib.error import urllib.error
import json import json

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Test slack-bolt can connect via Socket Mode. """Test slack-bolt can connect via Socket Mode.
Run on server as learner user: Run on server as dev user:
python3 tests/test-slack-bolt.py python3 tests/test-slack-bolt.py
Requires: pip install slack-bolt Requires: pip install slack-bolt
@ -20,12 +20,12 @@ def main():
if not bot_token: if not bot_token:
print("FAIL: SLACK_BOT_TOKEN not set") print("FAIL: SLACK_BOT_TOKEN not set")
print(" Hint: source /etc/slack-learner.env") print(" Hint: source /etc/slack-dev.env")
sys.exit(1) sys.exit(1)
if not app_token: if not app_token:
print("FAIL: SLACK_APP_TOKEN not set") print("FAIL: SLACK_APP_TOKEN not set")
print(" Hint: source /etc/slack-learner.env") print(" Hint: source /etc/slack-dev.env")
sys.exit(1) sys.exit(1)
print(f"Bot token: {bot_token[:10]}...{bot_token[-4:]}") print(f"Bot token: {bot_token[:10]}...{bot_token[-4:]}")