diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index d457c80..a33faef 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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"} diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md new file mode 100644 index 0000000..af57574 --- /dev/null +++ b/.claude/commands/code-review.md @@ -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] +Issue: +Suggest: +Evidence: +``` + +### 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 "" 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` diff --git a/.claude/skills/orch b/.claude/skills/orch new file mode 120000 index 0000000..12b0e3b --- /dev/null +++ b/.claude/skills/orch @@ -0,0 +1 @@ +/nix/store/c1qr5g1z0n91x5qw9cpc2wyk66hsavkh-ai-skill-orch \ No newline at end of file diff --git a/.claude/skills/worklog b/.claude/skills/worklog new file mode 120000 index 0000000..9e01618 --- /dev/null +++ b/.claude/skills/worklog @@ -0,0 +1 @@ +/nix/store/pnw3i2sn988jv6dwjps7ilvlniz76pbn-ai-skill-worklog \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..807d598 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ + +# Use bd merge for beads JSONL files +.beads/issues.jsonl merge=beads diff --git a/.gitignore b/.gitignore index af43a96..1ae2482 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,7 @@ venv/ docs/worklogs/ # Local dev config -.claude/ +.claude/settings.local.json .opencode/ .envrc .skills diff --git a/docs/learner-admin.md b/docs/learner-admin.md new file mode 100644 index 0000000..ee82e29 --- /dev/null +++ b/docs/learner-admin.md @@ -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 '' + ``` + + 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_/` - starter maubot plugin +- The plugin includes a working hello/ping bot + +## Removing a Learner + +```bash +sudo ./scripts/learner-remove.sh +``` + +With archive (saves home directory before deleting): +```bash +sudo ./scripts/learner-remove.sh --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 " | 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 ` +2. Check SSH key: `cat /home//.ssh/authorized_keys` +3. Check SSH logs: `journalctl -u sshd | grep ` + +### Plugin Won't Load + +1. Check file permissions: + ```bash + ls -la /home//plugins/ + ``` +2. Verify .mbp file structure: + ```bash + unzip -l /home//plugins/*/dist/*.mbp + ``` + +### Disk Space + +Monitor learner disk usage: +```bash +du -sh /home/*/ +``` diff --git a/docs/learner-onboarding.md b/docs/learner-onboarding.md new file mode 100644 index 0000000..be2d728 --- /dev/null +++ b/docs/learner-onboarding.md @@ -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 + User + LocalForward 29316 127.0.0.1:29316 +``` + +Replace `` and `` 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//plugins/hello_` +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 diff --git a/flake.nix b/flake.nix index ec9eab8..ce1d5fc 100644 --- a/flake.nix +++ b/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 { diff --git a/scripts/learner-add.sh b/scripts/learner-add.sh new file mode 100755 index 0000000..030d6ea --- /dev/null +++ b/scripts/learner-add.sh @@ -0,0 +1,304 @@ +#!/usr/bin/env bash +# learner-add.sh - Add a new learner account for maubot plugin development +# Usage: learner-add.sh +# +# 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 " + 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:-}" + 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 "$@" diff --git a/scripts/learner-remove.sh b/scripts/learner-remove.sh new file mode 100755 index 0000000..be74c56 --- /dev/null +++ b/scripts/learner-remove.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# learner-remove.sh - Remove a learner account +# Usage: learner-remove.sh [--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 [--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 "$@" diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..5d8a373 --- /dev/null +++ b/scripts/smoke-test.sh @@ -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 diff --git a/templates/plugin-skeleton/MODULE_NAME/__init__.py b/templates/plugin-skeleton/MODULE_NAME/__init__.py new file mode 100644 index 0000000..c35b0c9 --- /dev/null +++ b/templates/plugin-skeleton/MODULE_NAME/__init__.py @@ -0,0 +1,3 @@ +from .bot import Bot + +__all__ = ["Bot"] diff --git a/templates/plugin-skeleton/MODULE_NAME/bot.py b/templates/plugin-skeleton/MODULE_NAME/bot.py new file mode 100644 index 0000000..22fbe9c --- /dev/null +++ b/templates/plugin-skeleton/MODULE_NAME/bot.py @@ -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}") diff --git a/templates/plugin-skeleton/Makefile b/templates/plugin-skeleton/Makefile new file mode 100644 index 0000000..59b335e --- /dev/null +++ b/templates/plugin-skeleton/Makefile @@ -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" diff --git a/templates/plugin-skeleton/README.md b/templates/plugin-skeleton/README.md new file mode 100644 index 0000000..de37fd1 --- /dev/null +++ b/templates/plugin-skeleton/README.md @@ -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/) diff --git a/templates/plugin-skeleton/maubot.yaml b/templates/plugin-skeleton/maubot.yaml new file mode 100644 index 0000000..ecd0af5 --- /dev/null +++ b/templates/plugin-skeleton/maubot.yaml @@ -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