Add learner dev environment, testing infrastructure, and skills
Learner account management: - learner-add.sh: create accounts with SSH, plugin skeleton - learner-remove.sh: remove accounts with optional archive - plugin-skeleton template: starter maubot plugin Testing: - flake.nix: add checks output for pre-deploy validation - smoke-test.sh: post-deploy service verification Documentation: - learner-onboarding.md: VS Code Remote-SSH setup guide - learner-admin.md: account management procedures Skills: - code-review.md: multi-lens code review skill - orch, worklog: symlinks to shared skills
This commit is contained in:
parent
abe2adfead
commit
3d33a45cc9
|
|
@ -1,35 +1,37 @@
|
|||
{"id":"ops-jrz1-00e","title":"Upgrade NixOS from 24.05 to 24.11","description":"Running NixOS 24.05.20241230 (Uakari). Current stable is 24.11. May be missing security patches. Low priority as no known critical CVEs, but should plan upgrade.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-04T21:03:22.760228514-08:00","updated_at":"2025-12-04T21:04:35.805980055-08:00","comments":[{"id":1,"issue_id":"ops-jrz1-00e","author":"dan","text":"Analysis Findings:\n1. Version Mismatch: Local flake.nix is pinned to 'nixos-24.05', but the dev environment reports '25.11' (Unstable), indicating state divergence.\n2. Upstream Bugs: Blocking issues in mautrix-slack (ops-jrz1-blh) and maubot (sync failure) are present in the current unstable revision (2025-12-02).\n3. Recommendation: Upgrade platform to NixOS 24.11 (Stable) to align environment, ensure stability, and pull fresh upstream fixes.","created_at":"2025-12-08T23:54:57Z"}]}
|
||||
{"id":"ops-jrz1-03o","title":"Upgrade mautrix-slack to v25.11","description":"Upgrade is just flake update + deploy. Current deployed: v0.2.3+dev.unknown (Oct 13). Flake lock: v25.10 (Oct 22). Latest nixpkgs-unstable: v25.11. Run: nix flake update nixpkgs-unstable \u0026\u0026 deploy. May fix edit panic (ops-jrz1-qxr).","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T18:24:18.332067067-08:00","updated_at":"2025-12-05T19:07:09.156981447-08:00","closed_at":"2025-12-05T19:07:09.156981447-08:00"}
|
||||
{"id":"ops-jrz1-3ca","title":"Persist opencode state/cache across restarts","description":"opencode may store index/cache in ~/.cache or other dirs not covered by current bind mounts. AI context could be lost on container restart. Verify and add mounts.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:30.90315778-08:00","updated_at":"2025-12-05T15:32:30.90315778-08:00","dependencies":[{"issue_id":"ops-jrz1-3ca","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.247361009-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-2pm","title":"Remote dev environment for learners","description":"Set up dev environments for learners to build maubot plugins.\n\n**Approach**: VS Code Remote-SSH + shared maubot + per-user Unix accounts\n\n**Plan**: ~/.claude/plans/snazzy-snacking-llama.md\n\n**Key deliverables**:\n1. learner-add.sh / learner-remove.sh scripts\n2. Maubot config for per-user plugin directories \n3. Hello-world plugin template with Makefile\n4. Onboarding docs\n\n**Decisions**: \n- Shared #learners-sandbox test room\n- Auto-create starter plugin on user add\n- Direct SSH editing (not git-based deploy)","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-28T10:13:21.90764918-05:00","created_by":"dan","updated_at":"2025-12-28T20:14:18.249188974-05:00"}
|
||||
{"id":"ops-jrz1-3ca","title":"Persist opencode state/cache across restarts","description":"opencode may store index/cache in ~/.cache or other dirs not covered by current bind mounts. AI context could be lost on container restart. Verify and add mounts.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:30.90315778-08:00","updated_at":"2025-12-28T00:05:44.753074955-05:00","closed_at":"2025-12-28T00:05:44.753074955-05:00","close_reason":"Parent epic cancelled - browser-based dev approach abandoned","dependencies":[{"issue_id":"ops-jrz1-3ca","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.247361009-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-3fd","title":"Deploy and test single-user instance (Phase 1)","description":"Deploy one container for testing. Validate: WebSocket, extensions, terminal, opencode, memory usage. Access via SSH tunnel initially.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T17:16:54.783260036-08:00","updated_at":"2025-12-05T17:16:54.783260036-08:00","dependencies":[{"issue_id":"ops-jrz1-3fd","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:17:36.400677984-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-3fd","depends_on_id":"ops-jrz1-5oe","type":"blocks","created_at":"2025-12-05T17:17:38.708397909-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-3fd","depends_on_id":"ops-jrz1-av0","type":"blocks","created_at":"2025-12-05T17:17:38.721665448-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-3fd","depends_on_id":"ops-jrz1-9gd","type":"blocks","created_at":"2025-12-05T17:17:38.737824478-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-3so","title":"Browser-based dev environment with opencode","description":"Epic: Provide VS Code in browser via code-server with opencode AI integration.\n\nKey decisions:\n- code-server in Podman containers (rootless)\n- opencode CLI + VS Code extension pre-installed\n- Subdomain routing (dan.code.clarun.xyz)\n- Custom container image\n- Target users: non-programmers, testers, learners\n\nDesign doc: specs/004-browser-dev-environment/design.md\n\nMigrated from ops-jrz1-ndl","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-05T17:04:36.709352529-08:00","updated_at":"2025-12-05T17:04:36.709352529-08:00"}
|
||||
{"id":"ops-jrz1-3x4","title":"Add maubot SDK and deploy script to container image","description":"Container image needs:\n- Python 3.11 + maubot SDK\n- deploy.sh script (zip → .mbp → curl to maubot API)\n- maubot API reachable from container (host network or port forward)\n\nPart of learner onboarding for bot development.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-06T12:18:06.841708662-08:00","updated_at":"2025-12-06T12:18:06.841708662-08:00","dependencies":[{"issue_id":"ops-jrz1-3x4","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-06T12:18:16.085519885-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-3x4","depends_on_id":"ops-jrz1-d58","type":"blocks","created_at":"2025-12-06T12:18:16.110944935-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-3so","title":"Browser-based dev environment with opencode","description":"Epic: Provide VS Code in browser via code-server with opencode AI integration.\n\nKey decisions:\n- code-server in Podman containers (rootless)\n- opencode CLI + VS Code extension pre-installed\n- Subdomain routing (dan.code.clarun.xyz)\n- Custom container image\n- Target users: non-programmers, testers, learners\n\nDesign doc: specs/004-browser-dev-environment/design.md\n\nMigrated from ops-jrz1-ndl","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-05T17:04:36.709352529-08:00","updated_at":"2025-12-28T00:01:08.825440506-05:00","closed_at":"2025-12-28T00:01:08.825440506-05:00","close_reason":"Won't fix - non-MS browser-based VSCode solutions have reliability/compatibility issues"}
|
||||
{"id":"ops-jrz1-3x4","title":"Add maubot SDK and deploy script to container image","description":"Container image needs:\n- Python 3.11 + maubot SDK\n- deploy.sh script (zip → .mbp → curl to maubot API)\n- maubot API reachable from container (host network or port forward)\n\nPart of learner onboarding for bot development.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-06T12:18:06.841708662-08:00","updated_at":"2025-12-28T00:05:44.748474842-05:00","closed_at":"2025-12-28T00:05:44.748474842-05:00","close_reason":"Parent epic cancelled - browser-based dev approach abandoned","dependencies":[{"issue_id":"ops-jrz1-3x4","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-06T12:18:16.085519885-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-3x4","depends_on_id":"ops-jrz1-d58","type":"blocks","created_at":"2025-12-06T12:18:16.110944935-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-45v","title":"Matrix/Slack identity mismatch: dan vs vlad","description":"Matrix user @dan:clarun.xyz is linked to Slack user 'vlad'. Messages appear as vlad in Slack but dan in Element. Cosmetic confusion. Options: rename Matrix display name, or re-login bridge with different Slack account.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T19:38:19.899555475-08:00","updated_at":"2025-12-05T19:38:19.899555475-08:00"}
|
||||
{"id":"ops-jrz1-46y","title":"Write onboarding documentation","description":"Critical for non-programmers. Cover: login, opencode usage, Git setup (PAT workflow), resource limits, security hygiene. Keep concise.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T15:32:41.586544583-08:00","updated_at":"2025-12-05T15:32:41.586544583-08:00","dependencies":[{"issue_id":"ops-jrz1-46y","depends_on_id":"ops-jrz1-7j4","type":"blocks","created_at":"2025-12-05T15:33:25.328712413-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-46y","depends_on_id":"ops-jrz1-wj2","type":"blocks","created_at":"2025-12-05T15:33:25.351559821-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-46y","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.401868669-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-46y","title":"Write onboarding documentation","description":"Critical for non-programmers. Cover: login, opencode usage, Git setup (PAT workflow), resource limits, security hygiene. Keep concise.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T15:32:41.586544583-08:00","updated_at":"2025-12-28T00:08:06.743739166-05:00","closed_at":"2025-12-28T00:08:06.743739166-05:00","close_reason":"Browser-based dev environment cancelled","dependencies":[{"issue_id":"ops-jrz1-46y","depends_on_id":"ops-jrz1-7j4","type":"blocks","created_at":"2025-12-05T15:33:25.328712413-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-46y","depends_on_id":"ops-jrz1-wj2","type":"blocks","created_at":"2025-12-05T15:33:25.351559821-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-46y","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.401868669-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-4jm","title":"Smoke test Matrix server (conduwuit)","description":"Verify Matrix homeserver is healthy: check /_matrix/client/versions endpoint, test registration, verify federation status (disabled). Quick health check after deployments.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T18:09:47.220765063-08:00","updated_at":"2025-12-05T18:19:33.059734881-08:00","closed_at":"2025-12-05T18:19:33.059734881-08:00"}
|
||||
{"id":"ops-jrz1-5fk","title":"Smoke test Maubot service","description":"Verify Maubot is healthy: check management UI accessible via SSH tunnel, verify bot instances running, test plugin functionality. Quick health check after deployments.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T18:09:47.33773092-08:00","updated_at":"2025-12-05T18:19:33.061388913-08:00","closed_at":"2025-12-05T18:19:33.061388913-08:00"}
|
||||
{"id":"ops-jrz1-5ki","title":"Set up programmatic QA test user for bridge testing","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T20:17:04.312571398-08:00","updated_at":"2025-12-05T20:17:04.312571398-08:00"}
|
||||
{"id":"ops-jrz1-5oe","title":"Create NixOS module for code-server containers","description":"Module to manage per-user Podman containers, nginx routing, secrets. Use virtualisation.oci-containers. Generate systemd units.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T17:16:54.656121092-08:00","updated_at":"2025-12-05T17:16:54.656121092-08:00","dependencies":[{"issue_id":"ops-jrz1-5oe","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:17:36.386278268-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-5oe","depends_on_id":"ops-jrz1-d58","type":"blocks","created_at":"2025-12-05T17:17:38.694752468-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-5oe","title":"Create NixOS module for code-server containers","description":"Module to manage per-user Podman containers, nginx routing, secrets. Use virtualisation.oci-containers. Generate systemd units.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T17:16:54.656121092-08:00","updated_at":"2025-12-28T00:05:44.743524099-05:00","closed_at":"2025-12-28T00:05:44.743524099-05:00","close_reason":"Parent epic cancelled - browser-based dev approach abandoned","dependencies":[{"issue_id":"ops-jrz1-5oe","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:17:36.386278268-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-5oe","depends_on_id":"ops-jrz1-d58","type":"blocks","created_at":"2025-12-05T17:17:38.694752468-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-6of","title":"AI cost/rate limiting per user","description":"One user could drain API credits with runaway script. Need rate limiting per user, either via proxy middleware or opencode config. Track usage.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T15:32:30.772304538-08:00","updated_at":"2025-12-05T17:42:42.773613559-08:00","closed_at":"2025-12-05T17:42:42.773613559-08:00","dependencies":[{"issue_id":"ops-jrz1-6of","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.206816868-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-6of","depends_on_id":"ops-jrz1-wj2","type":"blocks","created_at":"2025-12-05T17:17:38.658742196-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-7j4","title":"Git credential strategy for non-programmers","description":"Non-programmers can't manage SSH keys. Pre-configure git-credential-store or provide simple PAT workflow with docs. Store in persistent home with 600 perms.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T15:32:19.673999683-08:00","updated_at":"2025-12-05T17:38:54.788694408-08:00","closed_at":"2025-12-05T17:38:54.788694408-08:00","dependencies":[{"issue_id":"ops-jrz1-7j4","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.139749437-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-88o","title":"Implement backup strategy for VPS","description":"No backups configured. Critical data: Matrix DB (622M), PostgreSQL (161M), Forgejo (2.5M), maubot (320K). No recovery path if disk fails. Need automated backups with off-site storage.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-04T22:55:25.546850172-08:00","updated_at":"2025-12-05T00:56:27.720623612-08:00","closed_at":"2025-12-05T00:56:27.720623612-08:00"}
|
||||
{"id":"ops-jrz1-9gd","title":"Upgrade VPS RAM for dev environments","description":"Current: 2GB. Need 4-8GB for multiple code-server containers. Coordinate with Vultr, plan maintenance window.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T17:16:54.267689439-08:00","updated_at":"2025-12-05T17:16:54.267689439-08:00","dependencies":[{"issue_id":"ops-jrz1-9gd","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:17:36.331146543-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-9gd","title":"Upgrade VPS RAM for dev environments","description":"Current: 2GB. Need 4-8GB for multiple code-server containers. Coordinate with Vultr, plan maintenance window.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T17:16:54.267689439-08:00","updated_at":"2025-12-28T00:08:06.748175273-05:00","closed_at":"2025-12-28T00:08:06.748175273-05:00","close_reason":"Browser-based dev environment cancelled","dependencies":[{"issue_id":"ops-jrz1-9gd","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:17:36.331146543-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-av0","title":"Configure wildcard DNS and ACME cert","description":"Set up *.code.clarun.xyz DNS record and wildcard SSL cert via ACME. Depends on subdomain routing decision (kg0).","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T17:16:54.387356964-08:00","updated_at":"2025-12-05T17:16:54.387356964-08:00","dependencies":[{"issue_id":"ops-jrz1-av0","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:17:36.34918436-08:00","created_by":"daemon","metadata":"{}"},{"issue_id":"ops-jrz1-av0","depends_on_id":"ops-jrz1-kg0","type":"blocks","created_at":"2025-12-05T17:17:38.676800677-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"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-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":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:41.199417226-08:00","updated_at":"2025-12-05T15:32:41.199417226-08:00","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":"open","priority":2,"issue_type":"bug","created_at":"2025-12-05T19:40:33.255395189-08:00","updated_at":"2025-12-05T23:05:05.344825241-08:00","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-d58","title":"Build custom code-server container image","description":"Dockerfile with: code-server, opencode CLI, opencode VS Code extension (Open VSX), Python, Node, Git. Push to registry or build locally.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T17:16:54.507577308-08:00","updated_at":"2025-12-05T17:16:54.507577308-08:00","dependencies":[{"issue_id":"ops-jrz1-d58","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:17:36.369590207-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-d58","title":"Build custom code-server container image","description":"Dockerfile with: code-server, opencode CLI, opencode VS Code extension (Open VSX), Python, Node, Git. Push to registry or build locally.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T17:16:54.507577308-08:00","updated_at":"2025-12-28T00:05:44.736614157-05:00","closed_at":"2025-12-28T00:05:44.736614157-05:00","close_reason":"Parent epic cancelled - browser-based dev approach abandoned","dependencies":[{"issue_id":"ops-jrz1-d58","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:17:36.369590207-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-dhj","title":"Port forwarding strategy for user apps","description":"When user runs app on localhost:3000, how do they view it? code-server has /proxy/\u003cport\u003e but URL is confusing for learners. Need clear UX or docs.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T15:32:30.649292743-08:00","updated_at":"2025-12-05T17:41:01.486505687-08:00","closed_at":"2025-12-05T17:41:01.486505687-08:00","dependencies":[{"issue_id":"ops-jrz1-dhj","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.175857247-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-dt9","title":"Increase container RAM limits (2GB too tight)","description":"2GB hard limit will OOM with code-server + opencode + LSP + user app. Gemini/GPT recommend 3-4GB per container or add swap. Need to size server appropriately.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-05T15:32:19.400171408-08:00","updated_at":"2025-12-05T17:38:54.770433169-08:00","closed_at":"2025-12-05T17:38:54.770433169-08:00","dependencies":[{"issue_id":"ops-jrz1-dt9","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.066130377-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-dux","title":"Container isolation: maubot API access only","description":"Security design for learner containers:\n\n**Container CAN access**:\n- maubot API (:29316) for plugin deploy\n- Matrix rooms via bot (through maubot)\n- Slack via bridge (through Matrix)\n\n**Container CANNOT access**:\n- Host filesystem\n- Other containers\n- PostgreSQL directly\n- Matrix homeserver directly\n- sops secrets\n\nImplementation: Podman network config, no --privileged, limited port exposure.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-06T12:18:16.212646624-08:00","updated_at":"2025-12-06T12:18:16.212646624-08:00","dependencies":[{"issue_id":"ops-jrz1-dux","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-06T12:18:21.627621772-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-ezf","title":"Maubot plugin dev workflow for learners","description":"Design frictionless dev workflow for Python/Go learners building maubot plugins.\n\n**Requirements**:\n- No SSH tunnel setup for learners\n- Fast feedback loop (edit → see bot respond)\n- Circuit breakers (allowed_rooms, rate limits)\n- Test channel: #vlads-pad (Slack) ↔ Matrix\n\n**Options being considered**:\n1. Git-push deploy: push to repo → CI builds .mbp → deploys to maubot\n2. Code-server containers: browser IDE on VPS, deploy script talks to maubot locally\n3. Hybrid: code-server + git workflow\n\n**Related**: ops-jrz1-3so (browser-dev-environment epic)","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-06T01:36:26.529372206-08:00","updated_at":"2025-12-06T01:36:26.529372206-08:00","dependencies":[{"issue_id":"ops-jrz1-ezf","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-06T12:18:06.743837766-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-dux","title":"Container isolation: maubot API access only","description":"Security design for learner containers:\n\n**Container CAN access**:\n- maubot API (:29316) for plugin deploy\n- Matrix rooms via bot (through maubot)\n- Slack via bridge (through Matrix)\n\n**Container CANNOT access**:\n- Host filesystem\n- Other containers\n- PostgreSQL directly\n- Matrix homeserver directly\n- sops secrets\n\nImplementation: Podman network config, no --privileged, limited port exposure.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-06T12:18:16.212646624-08:00","updated_at":"2025-12-28T10:12:31.107338781-05:00","closed_at":"2025-12-28T10:12:31.107338781-05:00","close_reason":"Depends on cancelled browser-dev epic (ops-jrz1-3so)","dependencies":[{"issue_id":"ops-jrz1-dux","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-06T12:18:21.627621772-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-ezf","title":"Maubot plugin dev workflow for learners","description":"Design frictionless dev workflow for Python/Go learners building maubot plugins.\n\n**Requirements**:\n- No SSH tunnel setup for learners\n- Fast feedback loop (edit → see bot respond)\n- Circuit breakers (allowed_rooms, rate limits)\n- Test channel: #vlads-pad (Slack) ↔ Matrix\n\n**Options being considered**:\n1. Git-push deploy: push to repo → CI builds .mbp → deploys to maubot\n2. Code-server containers: browser IDE on VPS, deploy script talks to maubot locally\n3. Hybrid: code-server + git workflow\n\n**Related**: ops-jrz1-3so (browser-dev-environment epic)","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-06T01:36:26.529372206-08:00","updated_at":"2025-12-28T10:12:31.096280407-05:00","closed_at":"2025-12-28T10:12:31.096280407-05:00","close_reason":"Depends on cancelled browser-dev epic (ops-jrz1-3so)","dependencies":[{"issue_id":"ops-jrz1-ezf","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-06T12:18:06.743837766-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-f15","title":"mautrix-slack ConvertEdit panic on message edits","description":"mautrix-slack v25.11 panics with nil pointer dereference at handleslack.go:575 when processing Slack message_changed events. \n\nFIX READY at /tmp/mautrix-slack-fix (branch: fix-convert-edit-nil-panic)\n\nThree changes needed in pkg/connector/handleslack.go:\n1. Line 181: Add 'evt.SubMessage != nil \u0026\u0026' before evt.SubMessage.SubType\n2. Lines 573-578: Add nil checks for SubMessage and SubMessage.Edited at start of ConvertEdit()\n3. Line 609: Add 's.Data.SubMessage != nil \u0026\u0026' before accessing Timestamp\n\nTo submit PR: fork mautrix/slack, push branch, open PR against main.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-27T10:03:28.857940916-05:00","created_by":"dan","updated_at":"2025-12-28T10:02:58.660206297-05:00","closed_at":"2025-12-28T10:02:58.660206297-05:00","close_reason":"Won't fix for now - fix ready in /tmp/mautrix-slack-fix if needed later"}
|
||||
{"id":"ops-jrz1-gci","title":"Enable fail2ban for SSH brute force protection","description":"SSH brute force attempts generate log noise but don't pose security risk (key-only auth). fail2ban would help but is low priority. Deferred pending RFC on SSH log management strategy.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-04T21:03:22.651495544-08:00","updated_at":"2025-12-04T22:55:13.805471391-08:00","dependencies":[{"issue_id":"ops-jrz1-gci","depends_on_id":"ops-jrz1-nir","type":"blocks","created_at":"2025-12-04T22:56:14.777377818-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-glk","title":"VS Code extension policy (security)","description":"Extensions can run arbitrary code. Decide: allow arbitrary installs, or curate/restrict? For non-programmers, pre-install safe set and optionally disable marketplace.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:41.463030936-08:00","updated_at":"2025-12-05T15:32:41.463030936-08:00","dependencies":[{"issue_id":"ops-jrz1-glk","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.372120465-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-glk","title":"VS Code extension policy (security)","description":"Extensions can run arbitrary code. Decide: allow arbitrary installs, or curate/restrict? For non-programmers, pre-install safe set and optionally disable marketplace.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:41.463030936-08:00","updated_at":"2025-12-28T00:08:06.752037252-05:00","closed_at":"2025-12-28T00:08:06.752037252-05:00","close_reason":"Browser-based dev environment cancelled","dependencies":[{"issue_id":"ops-jrz1-glk","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.372120465-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-i8i","title":"Enable mautrix-slack relay mode for bot bridging","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-06T19:09:42.087506995-08:00","updated_at":"2025-12-06T19:09:47.612545472-08:00","closed_at":"2025-12-06T19:09:47.612545472-08:00"}
|
||||
{"id":"ops-jrz1-iok","title":"Instagram bot missing base-config.yaml","description":"Plugin was missing base-config.yaml required by maubot Config class. Fixed in commit 4b9481d.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-06T13:02:10.103730128-08:00","updated_at":"2025-12-06T13:02:15.055396318-08:00","closed_at":"2025-12-06T13:02:15.055396318-08:00"}
|
||||
{"id":"ops-jrz1-jit","title":"Logging and monitoring for dev environments","description":"No observability plan. Need: container CPU/mem metrics, nginx logs, disk usage monitoring, alert on repeated 401s or resource exhaustion.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:41.318448038-08:00","updated_at":"2025-12-05T15:32:41.318448038-08:00","dependencies":[{"issue_id":"ops-jrz1-jit","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.343610481-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"ops-jrz1-jit","title":"Logging and monitoring for dev environments","description":"No observability plan. Need: container CPU/mem metrics, nginx logs, disk usage monitoring, alert on repeated 401s or resource exhaustion.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:41.318448038-08:00","updated_at":"2025-12-28T00:08:06.750234292-05:00","closed_at":"2025-12-28T00:08:06.750234292-05:00","close_reason":"Browser-based dev environment cancelled","dependencies":[{"issue_id":"ops-jrz1-jit","depends_on_id":"ops-jrz1-3so","type":"parent-child","created_at":"2025-12-05T17:05:47.343610481-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":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T15:32:31.045592689-08:00","updated_at":"2025-12-05T15:32:31.045592689-08:00","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-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-nvx","title":"Slack bot architecture: Matrix-first approach","description":"**Decision**: Use Matrix as primary platform for Slack bot development.\n\n**Architecture**: Bots run as maubot plugins (or Matrix bots), communicate to Slack via mautrix-slack bridge.\n\n**Rationale**:\n- Existing infrastructure (maubot deployed, bridge working)\n- Single platform to manage\n- Bots work with Matrix users too\n- Avoid Socket Mode contention (only one xapp- connection allowed)\n\n**Trade-offs accepted**:\n- Bridge dependency (edit panic bug exists)\n- Extra latency through bridge hop\n- Limited to bridged channels\n\n**Alternative considered (Option B - direct Slack API)**:\n- Could use xoxb- token for outbound-only (REST)\n- Would need new Slack app for full Socket Mode independence\n- Deferred for now\n\n**Credentials available**:\n- slack-oauth-token (xoxb-) - shareable for REST calls if needed\n- slack-app-token (xapp-) - reserved for bridge Socket Mode\n\n**Status**: DECIDED - staying with Matrix-first","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T23:12:22.011872713-08:00","updated_at":"2025-12-05T23:12:28.329467732-08:00","closed_at":"2025-12-05T23:12:28.329467732-08:00"}
|
||||
|
|
|
|||
177
.claude/commands/code-review.md
Normal file
177
.claude/commands/code-review.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
---
|
||||
name: code-review
|
||||
description: Run multi-lens code review on target files. Analyzes for bloat, smells, dead-code, and redundancy. Interactive - asks before filing issues.
|
||||
---
|
||||
|
||||
# Code Review Skill
|
||||
|
||||
Run focused code analysis using multiple review lenses. Findings are synthesized and presented for your approval before any issues are filed.
|
||||
|
||||
## When to Use
|
||||
|
||||
Invoke this skill when:
|
||||
- "Review this code"
|
||||
- "Run code review on src/"
|
||||
- "Check this file for issues"
|
||||
- "Analyze the codebase"
|
||||
- `/code-review`
|
||||
|
||||
## Arguments
|
||||
|
||||
The skill accepts an optional target:
|
||||
- `/code-review` - Reviews recently changed files (git diff)
|
||||
- `/code-review src/` - Reviews specific directory
|
||||
- `/code-review src/main.py` - Reviews specific file
|
||||
- `/code-review --diff HEAD~5` - Reviews changes in last 5 commits
|
||||
|
||||
## Available Lenses
|
||||
|
||||
Lenses are focused review prompts located in `~/.config/lenses/`:
|
||||
|
||||
| Lens | Focus |
|
||||
|------|-------|
|
||||
| `bloat.md` | File size, function length, complexity, SRP violations |
|
||||
| `smells.md` | Code smells, naming, control flow, readability |
|
||||
| `dead-code.md` | Unused exports, zombie code, unreachable paths |
|
||||
| `redundancy.md` | Duplication, parallel systems, YAGNI violations |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Target Selection
|
||||
1. Parse the target argument (default: git diff of uncommitted changes)
|
||||
2. Identify files to review
|
||||
3. Show file list to user for confirmation
|
||||
|
||||
### Phase 2: Lens Execution
|
||||
For each lens, analyze the target files:
|
||||
|
||||
1. Read the lens prompt from `~/.config/lenses/{lens}.md`
|
||||
2. Apply the lens to the target code
|
||||
3. Collect findings in structured format
|
||||
|
||||
**Finding Format:**
|
||||
```
|
||||
[TAG] <severity:HIGH|MED|LOW> <file:line>
|
||||
Issue: <one-line description>
|
||||
Suggest: <actionable fix>
|
||||
Evidence: <why this matters>
|
||||
```
|
||||
|
||||
### Phase 3: Synthesis
|
||||
After all lenses complete:
|
||||
1. Deduplicate overlapping findings
|
||||
2. Group related issues
|
||||
3. Rank by severity and confidence
|
||||
4. Generate summary report
|
||||
|
||||
**Optional:** If user requests consensus (`--orch` or asks for it):
|
||||
```bash
|
||||
orch consensus "<findings summary>" gpt gemini
|
||||
```
|
||||
Use this to filter false positives and prioritize.
|
||||
|
||||
### Phase 4: Interactive Review
|
||||
Present findings to user:
|
||||
1. Show executive summary (counts by severity)
|
||||
2. List top issues with details
|
||||
3. Ask: "Which findings should I file as issues?"
|
||||
|
||||
**User can respond:**
|
||||
- "File all" - creates beads issues for everything
|
||||
- "File HIGH only" - filters by severity
|
||||
- "File 1, 3, 5" - specific findings
|
||||
- "None" - just keep the report
|
||||
- "Let me review first" - show full details
|
||||
|
||||
### Phase 5: Issue Filing (if requested)
|
||||
For approved findings:
|
||||
1. Create beads issues with `bd create`
|
||||
2. Include lens tag, severity, file location
|
||||
3. Link related issues if applicable
|
||||
|
||||
## Output
|
||||
|
||||
The skill produces:
|
||||
1. **Console summary** - immediate feedback
|
||||
2. **Beads issues** - if user approves filing
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
User: /code-review src/cli.py
|
||||
|
||||
Agent: I'll review src/cli.py with 4 lenses (bloat, smells, dead-code, redundancy).
|
||||
|
||||
[Running bloat lens...]
|
||||
[Running smells lens...]
|
||||
[Running dead-code lens...]
|
||||
[Running redundancy lens...]
|
||||
|
||||
## Review Summary: src/cli.py
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| HIGH | 1 |
|
||||
| MED | 3 |
|
||||
| LOW | 2 |
|
||||
|
||||
### Top Issues
|
||||
|
||||
1. [BLOAT] HIGH src/cli.py:145-280
|
||||
Issue: Function `handle_request` is 135 lines
|
||||
Suggest: Extract into smaller functions by responsibility
|
||||
|
||||
2. [SMELL] MED src/cli.py:89
|
||||
Issue: Magic number 3600 without explanation
|
||||
Suggest: Extract to named constant SECONDS_PER_HOUR
|
||||
|
||||
3. [DEAD] MED src/cli.py:12
|
||||
Issue: Import `unused_module` has no references
|
||||
Suggest: Remove unused import
|
||||
|
||||
Would you like me to file any of these as beads issues?
|
||||
Options: all, HIGH only, specific numbers (1,2,3), or none
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The skill respects `.code-review.yml` in the repo root if present:
|
||||
|
||||
```yaml
|
||||
# Optional configuration
|
||||
ignore_paths:
|
||||
- vendor/
|
||||
- node_modules/
|
||||
- "*.generated.*"
|
||||
|
||||
severity_defaults:
|
||||
bloat: MED
|
||||
dead-code: LOW
|
||||
|
||||
max_file_size_kb: 500 # Skip files larger than this
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
1. **Be Thorough But Focused** - Each lens checks one concern deeply
|
||||
2. **Evidence Over Opinion** - Cite specific lines and patterns
|
||||
3. **Actionable Suggestions** - Every finding needs a clear fix
|
||||
4. **Respect User Time** - Summarize first, details on request
|
||||
5. **No Spam** - Don't file issues without explicit approval
|
||||
|
||||
## Process Checklist
|
||||
|
||||
1. [ ] Parse target (files/directory/diff)
|
||||
2. [ ] Confirm scope with user if large (>10 files)
|
||||
3. [ ] Run each lens, collecting findings
|
||||
4. [ ] Deduplicate and rank findings
|
||||
5. [ ] Present summary to user
|
||||
6. [ ] Ask which findings to file
|
||||
7. [ ] Create beads issues for approved findings
|
||||
8. [ ] Report issue IDs created
|
||||
|
||||
## Integration
|
||||
|
||||
- **Lenses**: Read from `~/.config/lenses/*.md`
|
||||
- **Issue Tracking**: Uses `bd create` for beads issues
|
||||
- **Orch**: Optional consensus filtering via `orch consensus`
|
||||
1
.claude/skills/orch
Symbolic link
1
.claude/skills/orch
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/nix/store/c1qr5g1z0n91x5qw9cpc2wyk66hsavkh-ai-skill-orch
|
||||
1
.claude/skills/worklog
Symbolic link
1
.claude/skills/worklog
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/nix/store/pnw3i2sn988jv6dwjps7ilvlniz76pbn-ai-skill-worklog
|
||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
# Use bd merge for beads JSONL files
|
||||
.beads/issues.jsonl merge=beads
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -57,7 +57,7 @@ venv/
|
|||
docs/worklogs/
|
||||
|
||||
# Local dev config
|
||||
.claude/
|
||||
.claude/settings.local.json
|
||||
.opencode/
|
||||
.envrc
|
||||
.skills
|
||||
|
|
|
|||
101
docs/learner-admin.md
Normal file
101
docs/learner-admin.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Learner Account Administration
|
||||
|
||||
Guide for managing learner accounts on the maubot development server.
|
||||
|
||||
## Adding a Learner
|
||||
|
||||
1. Get the learner's SSH public key (they run `cat ~/.ssh/id_ed25519.pub`)
|
||||
|
||||
2. SSH to the server and run:
|
||||
```bash
|
||||
sudo /path/to/scripts/learner-add.sh <username> '<ssh-public-key>'
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
sudo ./scripts/learner-add.sh alice 'ssh-ed25519 AAAAC3... alice@laptop'
|
||||
```
|
||||
|
||||
3. The script will output onboarding instructions - send these to the learner.
|
||||
|
||||
### What Gets Created
|
||||
|
||||
- Unix user account with SSH access
|
||||
- `~/plugins/hello_<username>/` - starter maubot plugin
|
||||
- The plugin includes a working hello/ping bot
|
||||
|
||||
## Removing a Learner
|
||||
|
||||
```bash
|
||||
sudo ./scripts/learner-remove.sh <username>
|
||||
```
|
||||
|
||||
With archive (saves home directory before deleting):
|
||||
```bash
|
||||
sudo ./scripts/learner-remove.sh <username> --archive
|
||||
```
|
||||
|
||||
Archives are saved to `/var/backups/learners/`.
|
||||
|
||||
## Maubot Setup (One-Time)
|
||||
|
||||
After adding the first learner, set up the shared test environment:
|
||||
|
||||
1. Create a Matrix bot user for learners (via Element or API)
|
||||
|
||||
2. In maubot admin (http://localhost:29316):
|
||||
- Add the bot user as a client
|
||||
- Learners will create instances using this client
|
||||
|
||||
3. Create `#learners-sandbox` room:
|
||||
- Create the room in Matrix
|
||||
- Invite the bot user
|
||||
- Give learners the room ID/alias
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Check Learner Plugin Status
|
||||
|
||||
```bash
|
||||
# See what plugins are loaded
|
||||
curl -s http://localhost:29316/_matrix/maubot/v1/plugins \
|
||||
-H "Authorization: Bearer <token>" | jq
|
||||
```
|
||||
|
||||
### View Learner Directories
|
||||
|
||||
```bash
|
||||
ls -la /home/*/plugins/
|
||||
```
|
||||
|
||||
### Check Maubot Logs
|
||||
|
||||
```bash
|
||||
journalctl -u maubot -f
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Learner Can't Connect
|
||||
|
||||
1. Verify user exists: `id <username>`
|
||||
2. Check SSH key: `cat /home/<username>/.ssh/authorized_keys`
|
||||
3. Check SSH logs: `journalctl -u sshd | grep <username>`
|
||||
|
||||
### Plugin Won't Load
|
||||
|
||||
1. Check file permissions:
|
||||
```bash
|
||||
ls -la /home/<username>/plugins/
|
||||
```
|
||||
2. Verify .mbp file structure:
|
||||
```bash
|
||||
unzip -l /home/<username>/plugins/*/dist/*.mbp
|
||||
```
|
||||
|
||||
### Disk Space
|
||||
|
||||
Monitor learner disk usage:
|
||||
```bash
|
||||
du -sh /home/*/
|
||||
```
|
||||
177
docs/learner-onboarding.md
Normal file
177
docs/learner-onboarding.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# Maubot Plugin Development - Learner Onboarding
|
||||
|
||||
This guide walks you through setting up your development environment for building maubot plugins.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **VS Code** with the [Remote-SSH extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh)
|
||||
2. **SSH key pair** - if you don't have one:
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "your-email@example.com"
|
||||
```
|
||||
3. **Your public key** - send this to the admin:
|
||||
```bash
|
||||
cat ~/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
## Getting Connected
|
||||
|
||||
Once the admin has set up your account, you'll receive connection details.
|
||||
|
||||
### 1. Configure SSH
|
||||
|
||||
Add this to your SSH config file (`~/.ssh/config` on Mac/Linux, `C:\Users\YOU\.ssh\config` on Windows):
|
||||
|
||||
```
|
||||
Host maubot-dev
|
||||
HostName <server-ip>
|
||||
User <your-username>
|
||||
LocalForward 29316 127.0.0.1:29316
|
||||
```
|
||||
|
||||
Replace `<server-ip>` and `<your-username>` with the values provided by your admin.
|
||||
|
||||
### 2. Connect with VS Code
|
||||
|
||||
1. Open VS Code
|
||||
2. Press `F1` (or `Cmd+Shift+P` / `Ctrl+Shift+P`)
|
||||
3. Type "Remote-SSH: Connect to Host"
|
||||
4. Select `maubot-dev`
|
||||
5. Wait for VS Code to connect
|
||||
|
||||
### 3. Open Your Plugin Folder
|
||||
|
||||
Once connected:
|
||||
1. Click "Open Folder"
|
||||
2. Navigate to `/home/<your-username>/plugins/hello_<your-username>`
|
||||
3. Click "OK"
|
||||
|
||||
You should see your starter plugin files.
|
||||
|
||||
## Your First Plugin
|
||||
|
||||
Your starter plugin responds to two commands:
|
||||
- `!hello` - Says hello
|
||||
- `!ping` - Responds with pong
|
||||
|
||||
### Build and Deploy
|
||||
|
||||
1. Open the integrated terminal in VS Code (`Ctrl+`` ` or `View > Terminal`)
|
||||
|
||||
2. Build your plugin:
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
This creates a `.mbp` file in the `dist/` folder.
|
||||
|
||||
3. Open the maubot admin UI:
|
||||
- In your browser, go to: http://localhost:29316
|
||||
- Log in (ask admin for credentials)
|
||||
|
||||
4. Upload your plugin:
|
||||
- Go to "Plugins" tab
|
||||
- Click "Upload new plugin"
|
||||
- Select your `.mbp` file from `dist/`
|
||||
|
||||
5. Create an instance:
|
||||
- Go to "Instances" tab
|
||||
- Click "Create instance"
|
||||
- Select your plugin and a bot user
|
||||
- Add `#learners-sandbox` to allowed rooms
|
||||
- Click "Create"
|
||||
|
||||
6. Test in Matrix:
|
||||
- Join the `#learners-sandbox` room
|
||||
- Type `!hello` - your bot should respond!
|
||||
|
||||
## Plugin Development
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
hello_yourname/
|
||||
├── maubot.yaml # Plugin manifest (name, version, etc.)
|
||||
├── hello_yourname/ # Python module
|
||||
│ ├── __init__.py # Module init
|
||||
│ └── bot.py # Your bot code
|
||||
├── Makefile # Build commands
|
||||
└── dist/ # Built .mbp files
|
||||
```
|
||||
|
||||
### Adding Commands
|
||||
|
||||
Edit `hello_yourname/bot.py`:
|
||||
|
||||
```python
|
||||
from maubot import Plugin, MessageEvent
|
||||
from maubot.handlers import command
|
||||
|
||||
class Bot(Plugin):
|
||||
@command.new("hello")
|
||||
async def hello_command(self, evt: MessageEvent) -> None:
|
||||
await evt.reply("Hello!")
|
||||
|
||||
# Add your new command here:
|
||||
@command.new("dice")
|
||||
async def dice_command(self, evt: MessageEvent) -> None:
|
||||
import random
|
||||
result = random.randint(1, 6)
|
||||
await evt.reply(f"🎲 You rolled a {result}!")
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Edit your code in VS Code
|
||||
2. Run `make build` in the terminal
|
||||
3. In maubot admin, go to your instance and click "Reload"
|
||||
4. Test your changes in Matrix
|
||||
|
||||
### Makefile Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `make build` | Build the .mbp plugin file |
|
||||
| `make check` | Check Python syntax |
|
||||
| `make clean` | Remove built files |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Connection refused" when opening localhost:29316
|
||||
|
||||
The SSH tunnel isn't working. Make sure:
|
||||
- You're connected via VS Code Remote-SSH
|
||||
- Your SSH config has the `LocalForward` line
|
||||
- Try disconnecting and reconnecting
|
||||
|
||||
### Plugin doesn't appear in maubot
|
||||
|
||||
- Check that `make build` completed without errors
|
||||
- Make sure you uploaded the `.mbp` file from the `dist/` folder
|
||||
- Check the maubot logs for errors
|
||||
|
||||
### Bot doesn't respond to commands
|
||||
|
||||
- Make sure the bot is in the room (check room members)
|
||||
- Check that the room is in "allowed_rooms" in instance config
|
||||
- Check maubot logs for errors
|
||||
- Commands are case-sensitive: `!hello` not `!Hello`
|
||||
|
||||
### Syntax errors
|
||||
|
||||
Run `make check` to validate Python syntax before building.
|
||||
|
||||
### Need to see logs
|
||||
|
||||
In the maubot admin UI, go to "Logs" to see recent activity and errors.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Maubot Documentation](https://docs.mau.fi/maubot/)
|
||||
- [Maubot Plugin Examples](https://github.com/maubot)
|
||||
- [Matrix Python SDK (mautrix)](https://docs.mau.fi/python/latest/)
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Ask in `#learners-sandbox` - other learners and admins can help
|
||||
- Check the maubot logs for error messages
|
||||
- Review the maubot documentation
|
||||
15
flake.nix
15
flake.nix
|
|
@ -11,7 +11,20 @@
|
|||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, nixpkgs-unstable, sops-nix, ... }@inputs: {
|
||||
outputs = { self, nixpkgs, nixpkgs-unstable, sops-nix, ... }@inputs:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
# Pre-deploy checks: nix flake check
|
||||
checks.${system} = {
|
||||
# Verify production config evaluates and builds
|
||||
ops-jrz1-config = self.nixosConfigurations.ops-jrz1.config.system.build.toplevel;
|
||||
|
||||
# Verify VM config evaluates (lighter weight)
|
||||
ops-jrz1-vm-config = self.nixosConfigurations.ops-jrz1-vm.config.system.build.toplevel;
|
||||
};
|
||||
|
||||
nixosConfigurations = {
|
||||
# Production configuration (for actual VPS deployment)
|
||||
ops-jrz1 = nixpkgs.lib.nixosSystem {
|
||||
|
|
|
|||
304
scripts/learner-add.sh
Executable file
304
scripts/learner-add.sh
Executable file
|
|
@ -0,0 +1,304 @@
|
|||
#!/usr/bin/env bash
|
||||
# learner-add.sh - Add a new learner account for maubot plugin development
|
||||
# Usage: learner-add.sh <username> <ssh-pubkey>
|
||||
#
|
||||
# Creates:
|
||||
# - Unix user account with SSH key
|
||||
# - ~/plugins directory with hello-world starter plugin
|
||||
# - Symlink in maubot plugins directory
|
||||
# - Outputs onboarding instructions
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MAUBOT_PLUGINS_DIR="/var/lib/maubot/plugins"
|
||||
TEMPLATE_DIR="${SCRIPT_DIR}/../templates/plugin-skeleton"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <username> <ssh-pubkey>"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " username - Learner's username (alphanumeric, 3-16 chars)"
|
||||
echo " ssh-pubkey - SSH public key (starts with ssh-ed25519, ssh-rsa, etc.)"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 alice 'ssh-ed25519 AAAA... alice@laptop'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
validate_username() {
|
||||
local username="$1"
|
||||
if [[ ! "$username" =~ ^[a-z][a-z0-9_-]{2,15}$ ]]; then
|
||||
log_error "Invalid username: must be 3-16 chars, start with letter, alphanumeric/underscore/dash only"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if user already exists
|
||||
if id "$username" &>/dev/null; then
|
||||
log_error "User '$username' already exists"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_ssh_key() {
|
||||
local key="$1"
|
||||
if [[ ! "$key" =~ ^ssh-(ed25519|rsa|ecdsa) ]]; then
|
||||
log_error "Invalid SSH key: must start with ssh-ed25519, ssh-rsa, or ssh-ecdsa"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
create_user() {
|
||||
local username="$1"
|
||||
local ssh_key="$2"
|
||||
|
||||
log_info "Creating user '$username'..."
|
||||
|
||||
# Create user with home directory
|
||||
useradd -m -s /bin/bash "$username"
|
||||
|
||||
# Set up SSH key
|
||||
local ssh_dir="/home/$username/.ssh"
|
||||
mkdir -p "$ssh_dir"
|
||||
echo "$ssh_key" > "$ssh_dir/authorized_keys"
|
||||
chmod 700 "$ssh_dir"
|
||||
chmod 600 "$ssh_dir/authorized_keys"
|
||||
chown -R "$username:$username" "$ssh_dir"
|
||||
|
||||
log_info "User created with SSH access"
|
||||
}
|
||||
|
||||
setup_plugin_directory() {
|
||||
local username="$1"
|
||||
local plugin_name="hello-${username}"
|
||||
local user_plugins_dir="/home/$username/plugins"
|
||||
local plugin_dir="$user_plugins_dir/$plugin_name"
|
||||
|
||||
log_info "Setting up plugin directory..."
|
||||
|
||||
# Create plugins directory
|
||||
mkdir -p "$plugin_dir"
|
||||
mkdir -p "$plugin_dir/dist"
|
||||
|
||||
# Python module names must use underscores, not hyphens
|
||||
local module_name="${plugin_name//-/_}"
|
||||
|
||||
# Copy template if exists, otherwise create from scratch
|
||||
if [[ -d "$TEMPLATE_DIR" ]]; then
|
||||
cp -r "$TEMPLATE_DIR/"* "$plugin_dir/"
|
||||
# Rename MODULE_NAME directory to actual module name
|
||||
if [[ -d "$plugin_dir/MODULE_NAME" ]]; then
|
||||
mv "$plugin_dir/MODULE_NAME" "$plugin_dir/$module_name"
|
||||
fi
|
||||
# Replace placeholders in all files
|
||||
find "$plugin_dir" -type f -exec sed -i "s/MODULE_NAME/$module_name/g" {} \;
|
||||
find "$plugin_dir" -type f -exec sed -i "s/USERNAME/$username/g" {} \;
|
||||
else
|
||||
# Create minimal starter plugin
|
||||
create_starter_plugin "$plugin_dir" "$module_name" "$username"
|
||||
fi
|
||||
|
||||
# Set ownership
|
||||
chown -R "$username:$username" "$user_plugins_dir"
|
||||
|
||||
# Make plugin dir readable by maubot group
|
||||
chmod 755 "/home/$username"
|
||||
chmod 755 "$user_plugins_dir"
|
||||
chmod -R 755 "$plugin_dir"
|
||||
|
||||
log_info "Plugin directory created at $plugin_dir"
|
||||
}
|
||||
|
||||
create_starter_plugin() {
|
||||
local plugin_dir="$1"
|
||||
local module_name="$2"
|
||||
local username="$3"
|
||||
|
||||
# Create module directory
|
||||
mkdir -p "$plugin_dir/$module_name"
|
||||
|
||||
# maubot.yaml
|
||||
cat > "$plugin_dir/maubot.yaml" << EOF
|
||||
maubot: 0.4.0
|
||||
id: xyz.clarun.${module_name}
|
||||
version: 0.1.0
|
||||
license: MIT
|
||||
modules:
|
||||
- ${module_name}
|
||||
main_class: ${module_name}.Bot
|
||||
EOF
|
||||
|
||||
# Python module __init__.py
|
||||
cat > "$plugin_dir/$module_name/__init__.py" << EOF
|
||||
from .bot import Bot
|
||||
|
||||
__all__ = ["Bot"]
|
||||
EOF
|
||||
|
||||
# Main bot.py
|
||||
cat > "$plugin_dir/$module_name/bot.py" << EOF
|
||||
from maubot import Plugin, MessageEvent
|
||||
from maubot.handlers import command
|
||||
|
||||
|
||||
class Bot(Plugin):
|
||||
"""Hello world bot for $username"""
|
||||
|
||||
@command.new("hello")
|
||||
async def hello_command(self, evt: MessageEvent) -> None:
|
||||
"""Respond to !hello command"""
|
||||
await evt.reply(f"Hello from ${username}'s bot!")
|
||||
|
||||
@command.new("ping")
|
||||
async def ping_command(self, evt: MessageEvent) -> None:
|
||||
"""Respond to !ping command"""
|
||||
await evt.reply("Pong!")
|
||||
EOF
|
||||
|
||||
# Makefile
|
||||
cat > "$plugin_dir/Makefile" << 'EOF'
|
||||
PLUGIN_NAME := $(shell grep '^id:' maubot.yaml | cut -d: -f2 | tr -d ' ')
|
||||
PLUGIN_VERSION := $(shell grep '^version:' maubot.yaml | cut -d: -f2 | tr -d ' ')
|
||||
MBP_FILE := dist/$(PLUGIN_NAME)-$(PLUGIN_VERSION).mbp
|
||||
|
||||
.PHONY: build reload clean help
|
||||
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " build - Build the .mbp plugin file"
|
||||
@echo " reload - Reload plugin in maubot (requires instance configured)"
|
||||
@echo " clean - Remove built files"
|
||||
@echo " dev - Build and reload"
|
||||
|
||||
build:
|
||||
@echo "Building $(PLUGIN_NAME)..."
|
||||
@mkdir -p dist
|
||||
@rm -f $(MBP_FILE)
|
||||
@zip -r $(MBP_FILE) maubot.yaml $(shell grep -E '^ - ' maubot.yaml | sed 's/ - //')
|
||||
@echo "Built: $(MBP_FILE)"
|
||||
|
||||
reload:
|
||||
@echo "Reloading plugin..."
|
||||
@echo "TODO: Add maubot API reload command"
|
||||
|
||||
clean:
|
||||
rm -rf dist/*.mbp
|
||||
|
||||
dev: build reload
|
||||
EOF
|
||||
|
||||
# README
|
||||
cat > "$plugin_dir/README.md" << EOF
|
||||
# $module_name
|
||||
|
||||
Hello world maubot plugin for $username.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Edit \`$module_name/bot.py\` to add your bot logic
|
||||
2. Run \`make build\` to create the .mbp file
|
||||
3. Upload to maubot via admin UI (http://localhost:29316)
|
||||
|
||||
## Commands
|
||||
|
||||
- \`!hello\` - Says hello
|
||||
- \`!ping\` - Responds with pong
|
||||
|
||||
## Files
|
||||
|
||||
- \`maubot.yaml\` - Plugin manifest
|
||||
- \`$module_name/bot.py\` - Main bot code
|
||||
- \`Makefile\` - Build commands
|
||||
EOF
|
||||
}
|
||||
|
||||
create_maubot_symlink() {
|
||||
local username="$1"
|
||||
local plugin_name="hello-${username}"
|
||||
local plugin_dir="/home/$username/plugins/$plugin_name"
|
||||
local symlink_path="$MAUBOT_PLUGINS_DIR/${username}-${plugin_name}.mbp"
|
||||
|
||||
log_info "Creating maubot plugin symlink..."
|
||||
|
||||
# Note: Symlink will point to the built .mbp file
|
||||
# For now, we'll link to the dist directory's expected output
|
||||
# The learner will need to run 'make build' first
|
||||
|
||||
# We could also configure maubot to load from source directories
|
||||
# but .mbp symlinks are simpler for now
|
||||
|
||||
log_warn "Learner must run 'make build' before plugin appears in maubot"
|
||||
}
|
||||
|
||||
print_onboarding() {
|
||||
local username="$1"
|
||||
local server_ip
|
||||
server_ip=$(hostname -I | awk '{print $1}')
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Onboarding Instructions for $username"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "1. Install VS Code with Remote-SSH extension"
|
||||
echo ""
|
||||
echo "2. Add this to your SSH config (~/.ssh/config):"
|
||||
echo ""
|
||||
echo " Host maubot-dev"
|
||||
echo " HostName ${server_ip:-<server-ip>}"
|
||||
echo " User $username"
|
||||
echo " LocalForward 29316 127.0.0.1:29316"
|
||||
echo ""
|
||||
echo "3. Connect via VS Code:"
|
||||
echo " - Press F1 -> 'Remote-SSH: Connect to Host'"
|
||||
echo " - Select 'maubot-dev'"
|
||||
echo ""
|
||||
echo "4. Open your plugin folder:"
|
||||
echo " /home/$username/plugins/hello-$username"
|
||||
echo ""
|
||||
echo "5. Build and test:"
|
||||
echo " make build # Build the plugin"
|
||||
echo " make reload # Reload in maubot"
|
||||
echo ""
|
||||
echo "6. Test in Matrix:"
|
||||
echo " Join #learners-sandbox and try !hello"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local username="$1"
|
||||
local ssh_key="$2"
|
||||
|
||||
# Must run as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
validate_username "$username"
|
||||
validate_ssh_key "$ssh_key"
|
||||
|
||||
create_user "$username" "$ssh_key"
|
||||
setup_plugin_directory "$username"
|
||||
create_maubot_symlink "$username"
|
||||
print_onboarding "$username"
|
||||
|
||||
log_info "Learner '$username' setup complete!"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
194
scripts/learner-remove.sh
Executable file
194
scripts/learner-remove.sh
Executable file
|
|
@ -0,0 +1,194 @@
|
|||
#!/usr/bin/env bash
|
||||
# learner-remove.sh - Remove a learner account
|
||||
# Usage: learner-remove.sh <username> [--archive]
|
||||
#
|
||||
# Removes:
|
||||
# - Unix user account
|
||||
# - Home directory (or archives if --archive flag)
|
||||
# - Maubot plugin symlinks
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MAUBOT_PLUGINS_DIR="/var/lib/maubot/plugins"
|
||||
ARCHIVE_DIR="/var/backups/learners"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <username> [--archive]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " username - Learner's username to remove"
|
||||
echo " --archive - Archive home directory instead of deleting"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 alice # Delete user and home directory"
|
||||
echo " $0 alice --archive # Archive home directory before deleting"
|
||||
exit 1
|
||||
}
|
||||
|
||||
validate_username() {
|
||||
local username="$1"
|
||||
|
||||
# Check if user exists
|
||||
if ! id "$username" &>/dev/null; then
|
||||
log_error "User '$username' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Safety check - don't delete system users
|
||||
local uid
|
||||
uid=$(id -u "$username")
|
||||
if [[ $uid -lt 1000 ]]; then
|
||||
log_error "Refusing to delete system user '$username' (UID $uid < 1000)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Safety check - don't delete important users
|
||||
case "$username" in
|
||||
root|dan|admin|maubot|postgres|nginx)
|
||||
log_error "Refusing to delete protected user '$username'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
remove_maubot_symlinks() {
|
||||
local username="$1"
|
||||
|
||||
log_info "Removing maubot plugin symlinks for '$username'..."
|
||||
|
||||
# Remove any symlinks that point to user's home directory
|
||||
local count=0
|
||||
for symlink in "$MAUBOT_PLUGINS_DIR"/*; do
|
||||
if [[ -L "$symlink" ]]; then
|
||||
local target
|
||||
target=$(readlink "$symlink")
|
||||
if [[ "$target" == "/home/$username/"* ]]; then
|
||||
rm "$symlink"
|
||||
log_info "Removed symlink: $symlink"
|
||||
((count++)) || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Also remove any plugins named after user
|
||||
for symlink in "$MAUBOT_PLUGINS_DIR/${username}-"*; do
|
||||
if [[ -e "$symlink" || -L "$symlink" ]]; then
|
||||
rm -f "$symlink"
|
||||
log_info "Removed: $symlink"
|
||||
((count++)) || true
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $count -eq 0 ]]; then
|
||||
log_info "No maubot symlinks found for '$username'"
|
||||
fi
|
||||
}
|
||||
|
||||
archive_home() {
|
||||
local username="$1"
|
||||
local home_dir="/home/$username"
|
||||
|
||||
if [[ ! -d "$home_dir" ]]; then
|
||||
log_warn "Home directory does not exist: $home_dir"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Archiving home directory..."
|
||||
|
||||
mkdir -p "$ARCHIVE_DIR"
|
||||
local archive_name="${username}_$(date +%Y%m%d_%H%M%S).tar.gz"
|
||||
local archive_path="$ARCHIVE_DIR/$archive_name"
|
||||
|
||||
tar -czf "$archive_path" -C /home "$username"
|
||||
chmod 600 "$archive_path"
|
||||
|
||||
log_info "Archived to: $archive_path"
|
||||
}
|
||||
|
||||
remove_user() {
|
||||
local username="$1"
|
||||
|
||||
log_info "Removing user '$username'..."
|
||||
|
||||
# Kill any running processes
|
||||
pkill -u "$username" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Remove user and home directory
|
||||
userdel -r "$username" 2>/dev/null || {
|
||||
# If userdel -r fails, try without -r and manually remove home
|
||||
userdel "$username" 2>/dev/null || true
|
||||
rm -rf "/home/$username"
|
||||
}
|
||||
|
||||
log_info "User removed"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local username="$1"
|
||||
local archive=false
|
||||
|
||||
# Parse flags
|
||||
shift
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--archive)
|
||||
archive=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Must run as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
validate_username "$username"
|
||||
|
||||
# Confirm deletion
|
||||
echo ""
|
||||
log_warn "This will remove user '$username' and all their data!"
|
||||
if [[ "$archive" == true ]]; then
|
||||
echo "Home directory will be archived to $ARCHIVE_DIR"
|
||||
else
|
||||
echo "Home directory will be PERMANENTLY DELETED"
|
||||
fi
|
||||
echo ""
|
||||
read -p "Are you sure? (yes/no): " confirm
|
||||
if [[ "$confirm" != "yes" ]]; then
|
||||
log_info "Aborted"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
remove_maubot_symlinks "$username"
|
||||
|
||||
if [[ "$archive" == true ]]; then
|
||||
archive_home "$username"
|
||||
fi
|
||||
|
||||
remove_user "$username"
|
||||
|
||||
log_info "Learner '$username' removed successfully"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
199
scripts/smoke-test.sh
Executable file
199
scripts/smoke-test.sh
Executable file
|
|
@ -0,0 +1,199 @@
|
|||
#!/usr/bin/env bash
|
||||
# smoke-test.sh - Post-deploy smoke tests for ops-jrz1
|
||||
#
|
||||
# Usage:
|
||||
# ./smoke-test.sh # Run against production VPS
|
||||
# ./smoke-test.sh --local # Run locally (for VM testing)
|
||||
# SSH_HOST=user@host ./smoke-test.sh # Custom SSH target
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 - All checks passed
|
||||
# 1 - One or more checks failed
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
SSH_HOST="${SSH_HOST:-root@45.77.205.49}"
|
||||
LOCAL_MODE=false
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Counters
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
WARNINGS=0
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--local)
|
||||
LOCAL_MODE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Run command locally or via SSH
|
||||
run() {
|
||||
if [[ "$LOCAL_MODE" == true ]]; then
|
||||
bash -c "$1"
|
||||
else
|
||||
ssh -o ConnectTimeout=10 "$SSH_HOST" "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test helpers
|
||||
pass() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
((PASSED++)) || true
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
((FAILED++)) || true
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
((WARNINGS++)) || true
|
||||
}
|
||||
|
||||
# Check if systemd service is active
|
||||
check_service() {
|
||||
local service="$1"
|
||||
local desc="${2:-$service}"
|
||||
|
||||
if run "systemctl is-active --quiet $service" 2>/dev/null; then
|
||||
pass "$desc is running"
|
||||
return 0
|
||||
else
|
||||
fail "$desc is not running"
|
||||
return 0 # Don't exit script on failure
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if port responds to HTTP
|
||||
check_http() {
|
||||
local url="$1"
|
||||
local desc="$2"
|
||||
local expected_code="${3:-200}"
|
||||
|
||||
local code
|
||||
code=$(run "curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 '$url'" 2>/dev/null) || code="000"
|
||||
|
||||
if [[ "$code" == "$expected_code" ]]; then
|
||||
pass "$desc (HTTP $code)"
|
||||
elif [[ "$code" == "401" && "$expected_code" == "401" ]]; then
|
||||
pass "$desc (HTTP 401 - auth required, expected)"
|
||||
elif [[ "$code" == "000" ]]; then
|
||||
fail "$desc (connection failed)"
|
||||
else
|
||||
fail "$desc (HTTP $code, expected $expected_code)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if port is listening
|
||||
check_port() {
|
||||
local port="$1"
|
||||
local desc="$2"
|
||||
|
||||
if run "ss -tlnp | grep -q ':$port '" 2>/dev/null; then
|
||||
pass "$desc listening on port $port"
|
||||
else
|
||||
fail "$desc not listening on port $port"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main tests
|
||||
echo "================================"
|
||||
echo " Smoke Tests for ops-jrz1"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
if [[ "$LOCAL_MODE" == true ]]; then
|
||||
echo "Mode: Local"
|
||||
else
|
||||
echo "Target: $SSH_HOST"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test SSH connectivity first
|
||||
echo "── Connectivity ──"
|
||||
if [[ "$LOCAL_MODE" == true ]]; then
|
||||
pass "Local mode - no SSH needed"
|
||||
else
|
||||
if ssh -o ConnectTimeout=5 "$SSH_HOST" "echo ok" &>/dev/null; then
|
||||
pass "SSH connection"
|
||||
else
|
||||
fail "SSH connection failed"
|
||||
echo ""
|
||||
echo "Cannot connect to $SSH_HOST - aborting"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Systemd services
|
||||
echo "── Core Services ──"
|
||||
check_service "nginx" "nginx (reverse proxy)"
|
||||
check_service "postgresql" "PostgreSQL"
|
||||
check_service "matrix-continuwuity" "Matrix homeserver (continuwuity)"
|
||||
check_service "mautrix-slack" "Slack bridge"
|
||||
check_service "maubot" "Maubot"
|
||||
echo ""
|
||||
|
||||
# Ports
|
||||
echo "── Ports ──"
|
||||
check_port 443 "HTTPS"
|
||||
check_port 8008 "Matrix homeserver"
|
||||
check_port 29316 "Maubot admin"
|
||||
check_port 29319 "Slack bridge"
|
||||
echo ""
|
||||
|
||||
# HTTP endpoints
|
||||
echo "── HTTP Endpoints ──"
|
||||
check_http "https://clarun.xyz/.well-known/matrix/server" "Matrix server discovery" "200"
|
||||
check_http "https://clarun.xyz/.well-known/matrix/client" "Matrix client discovery" "200"
|
||||
check_http "http://127.0.0.1:8008/_matrix/client/versions" "Matrix API" "200"
|
||||
check_http "http://127.0.0.1:29316/_matrix/maubot/v1/login" "Maubot API" "401"
|
||||
echo ""
|
||||
|
||||
# Database connectivity
|
||||
echo "── Database ──"
|
||||
if run "sudo -u postgres psql -c 'SELECT 1' >/dev/null 2>&1"; then
|
||||
pass "PostgreSQL responds to queries"
|
||||
else
|
||||
fail "PostgreSQL not responding"
|
||||
fi
|
||||
|
||||
if run "sudo -u postgres psql -d mautrix_slack -c 'SELECT 1' >/dev/null 2>&1"; then
|
||||
pass "mautrix_slack database exists"
|
||||
else
|
||||
warn "mautrix_slack database not accessible"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "================================"
|
||||
echo " Results"
|
||||
echo "================================"
|
||||
echo -e " ${GREEN}Passed:${NC} $PASSED"
|
||||
echo -e " ${RED}Failed:${NC} $FAILED"
|
||||
echo -e " ${YELLOW}Warnings:${NC} $WARNINGS"
|
||||
echo ""
|
||||
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
echo -e "${RED}SMOKE TEST FAILED${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}SMOKE TEST PASSED${NC}"
|
||||
exit 0
|
||||
fi
|
||||
3
templates/plugin-skeleton/MODULE_NAME/__init__.py
Normal file
3
templates/plugin-skeleton/MODULE_NAME/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .bot import Bot
|
||||
|
||||
__all__ = ["Bot"]
|
||||
21
templates/plugin-skeleton/MODULE_NAME/bot.py
Normal file
21
templates/plugin-skeleton/MODULE_NAME/bot.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from maubot import Plugin, MessageEvent
|
||||
from maubot.handlers import command
|
||||
|
||||
|
||||
class Bot(Plugin):
|
||||
"""Hello world bot for USERNAME"""
|
||||
|
||||
@command.new("hello")
|
||||
async def hello_command(self, evt: MessageEvent) -> None:
|
||||
"""Respond to !hello command"""
|
||||
await evt.reply(f"Hello from USERNAME's bot!")
|
||||
|
||||
@command.new("ping")
|
||||
async def ping_command(self, evt: MessageEvent) -> None:
|
||||
"""Respond to !ping command"""
|
||||
await evt.reply("Pong!")
|
||||
|
||||
@command.new("whoami")
|
||||
async def whoami_command(self, evt: MessageEvent) -> None:
|
||||
"""Tell the user who they are"""
|
||||
await evt.reply(f"You are {evt.sender}")
|
||||
46
templates/plugin-skeleton/Makefile
Normal file
46
templates/plugin-skeleton/Makefile
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Maubot Plugin Makefile
|
||||
# Run 'make help' for available commands
|
||||
|
||||
PLUGIN_ID := $(shell grep '^id:' maubot.yaml | cut -d: -f2 | tr -d ' ')
|
||||
VERSION := $(shell grep '^version:' maubot.yaml | cut -d: -f2 | tr -d ' ')
|
||||
MODULES := $(shell grep -E '^ - ' maubot.yaml | sed 's/ - //')
|
||||
MBP_FILE := dist/$(PLUGIN_ID)-$(VERSION).mbp
|
||||
|
||||
# Maubot API (accessible via SSH tunnel or localhost)
|
||||
MAUBOT_URL := http://127.0.0.1:29316
|
||||
|
||||
.PHONY: build reload clean dev help check
|
||||
|
||||
help:
|
||||
@echo "Maubot Plugin Development"
|
||||
@echo ""
|
||||
@echo "Commands:"
|
||||
@echo " make build - Build the .mbp plugin file"
|
||||
@echo " make check - Check Python syntax"
|
||||
@echo " make clean - Remove built files"
|
||||
@echo ""
|
||||
@echo "Plugin: $(PLUGIN_ID) v$(VERSION)"
|
||||
@echo "Output: $(MBP_FILE)"
|
||||
|
||||
build:
|
||||
@echo "Building $(PLUGIN_ID) v$(VERSION)..."
|
||||
@mkdir -p dist
|
||||
@rm -f $(MBP_FILE)
|
||||
@zip -r $(MBP_FILE) maubot.yaml $(MODULES)
|
||||
@echo "Built: $(MBP_FILE)"
|
||||
@ls -lh $(MBP_FILE)
|
||||
|
||||
check:
|
||||
@echo "Checking Python syntax..."
|
||||
@python3 -m py_compile $(MODULES)/*.py
|
||||
@echo "Syntax OK"
|
||||
|
||||
clean:
|
||||
rm -rf dist/*.mbp __pycache__ $(MODULES)/__pycache__
|
||||
|
||||
dev: check build
|
||||
@echo ""
|
||||
@echo "Plugin built! To deploy:"
|
||||
@echo "1. Open maubot admin: http://localhost:29316"
|
||||
@echo "2. Upload $(MBP_FILE)"
|
||||
@echo "3. Create/update instance"
|
||||
47
templates/plugin-skeleton/README.md
Normal file
47
templates/plugin-skeleton/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# MODULE_NAME
|
||||
|
||||
A maubot plugin by USERNAME.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Edit `MODULE_NAME/bot.py` to customize your bot
|
||||
2. Run `make build` to create the plugin package
|
||||
3. Upload to maubot via the admin UI (http://localhost:29316)
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `!hello` | Says hello |
|
||||
| `!ping` | Responds with pong |
|
||||
| `!whoami` | Tells you your Matrix ID |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
make check # Verify Python syntax
|
||||
make build # Build .mbp file
|
||||
make clean # Remove built files
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `maubot.yaml` - Plugin manifest (ID, version, modules)
|
||||
- `MODULE_NAME/bot.py` - Your bot code
|
||||
- `Makefile` - Build automation
|
||||
|
||||
## Adding Commands
|
||||
|
||||
Edit `MODULE_NAME/bot.py` and add a new method:
|
||||
|
||||
```python
|
||||
@command.new("mycommand")
|
||||
async def my_command(self, evt: MessageEvent) -> None:
|
||||
"""Description of what it does"""
|
||||
await evt.reply("Response!")
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Maubot Documentation](https://docs.mau.fi/maubot/)
|
||||
- [Matrix Python SDK](https://matrix-nio.readthedocs.io/)
|
||||
7
templates/plugin-skeleton/maubot.yaml
Normal file
7
templates/plugin-skeleton/maubot.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
maubot: 0.4.0
|
||||
id: xyz.clarun.MODULE_NAME
|
||||
version: 0.1.0
|
||||
license: MIT
|
||||
modules:
|
||||
- MODULE_NAME
|
||||
main_class: MODULE_NAME.Bot
|
||||
Loading…
Reference in a new issue