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:
Dan 2025-12-28 22:23:06 -05:00
parent abe2adfead
commit 3d33a45cc9
17 changed files with 1312 additions and 16 deletions

View file

@ -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"}

View 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
View file

@ -0,0 +1 @@
/nix/store/c1qr5g1z0n91x5qw9cpc2wyk66hsavkh-ai-skill-orch

1
.claude/skills/worklog Symbolic link
View file

@ -0,0 +1 @@
/nix/store/pnw3i2sn988jv6dwjps7ilvlniz76pbn-ai-skill-worklog

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
# Use bd merge for beads JSONL files
.beads/issues.jsonl merge=beads

2
.gitignore vendored
View file

@ -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
View 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
View 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

View file

@ -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
View 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
View 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
View 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

View file

@ -0,0 +1,3 @@
from .bot import Bot
__all__ = ["Bot"]

View 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}")

View 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"

View 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/)

View 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