New docs/guide/ tree — canonical home for operator-facing procedural docs. Starlight frontmatter added to all files. 0.12 alignment fixes applied: - v0.11.0 → v0.12.0 throughout - PI_TUI_PROVIDER/MODEL → DEEPSEEK_API_KEY - Headless Codex login → Agent runtime setup (zot + RPC mode) - /login and auth.json references removed - pi → zot in provider-fallback spawn reference - colibri-provider-verify (was pi-provider-smoke) - Language cleanup: smoke test → verification, fake → test, can't self-fix → requires operator intervention, broken → unresponsive, Fix anything broken → Verify all checks pass Two-tree model: docs/wiki/ (decisions) + docs/guide/ (procedural). Single source of truth in colibri. clawdie-ai docs/public/ to be retired.
9.8 KiB
| title |
|---|
| Channels — Implementation Plan |
Status: Plan only — no implementation started Last updated: 7.apr.2026 Strategy: see channels-roadmap.md
Current state
v0.9.0 is in development. Telegram is the only channel and is working. Nothing in this plan should be implemented until v0.9.0 is validated in production.
Conclusion
Two channels for v0.9.0: Telegram (done) + Web UI (new). Signal and WhatsApp deferred until real user demand exists. This covers phone dependency, privacy, and global reach without Java or Meta.
Exact steps to v0.9.0
Pre-condition: production validate v0.9.0
Before writing a single line of channels code, confirm these work on a real install:
- Telegram bot receives and responds to messages
npm run backupproduces a valid tarballnpm run doctorreports healthy- Metrics endpoint responds on port 9100
- Grafana dashboard shows real data from VictoriaMetrics
- ZFS snapshots are being taken by Sanoid
- Management jail starts cleanly on boot
Verify all checks pass before proceeding.
v0.9.0 implementation sequence
Step 1 — Onboarding refactor
Make setup channel-agnostic. Currently assumes Telegram unconditionally.
setup/onboarding.ts— no change needed (doesn't touch Telegram)setup/telegram-auth.ts— wrap in a channel selection step so Telegram is one option, not the only optionsrc/index.ts— guardTELEGRAM_BOT_TOKENcheck behindTELEGRAM_ENABLED=truerather than failing hard if token missing.env— addTELEGRAM_ENABLED=true(default true, no breaking change)
Step 2 — Invite code system
New users register by sending a code. Operator generates codes via CLI. Full implementation was prototyped on 16.mar.2026 — see git history or re-derive from channels-plan.md decisions section above.
Files to create:
src/invite.ts—createInviteCode,tryConsumeInviteCode,jidToFolderscripts/invite.ts—npm run invite [--ttl Nd]→ prints new codescripts/users.ts—npm run users→ lists registered usersscripts/codes.ts—npm run codes→ lists all active/used invite codesscripts/revoke.ts—npm run revoke <jid>→ removes a registered userscripts/revoke-code.ts—npm run revoke-code <code>→ deletes unused codescripts/suspend.ts—npm run suspend <jid>/npm run unsuspend <jid>
Files to modify:
src/db.ts—invite_codestable (withexpires_at,revoked_at),invited_bycolumn onregistered_groups,suspended_atonregistered_groupssrc/types.ts—invitedBy?: string,suspendedAt?: DateonRegisteredGroupsrc/channels/telegram.ts—onInviteCode?callback in opts, check suspensionsrc/index.ts— wire callback, auto-register on valid code, reject suspended JIDspackage.json— addinvite,users,codes,revoke,revoke-code,suspend,unsuspendscripts
Key decisions:
- Single-use codes, optional TTL (default: no expiry)
- Server stores invite_code → JID mapping permanently (enables re-link on browser clear)
- Store
invited_byfrom day one for future delegated invites - Operator-only invite generation to start (Model A)
- Audit log:
logs/invite-audit.log
Step 3 — Web UI channel
Files to create:
src/channels/web.ts— implementsChannel, WebSocket server, JID prefixweb:- Frontend: single HTML/CSS/JS chat page
setup/web-ui.ts— deploy frontend to CMS jail nginx
Files to modify:
src/index.ts— start web channel onWEB_UI_PORTifWEB_UI_ENABLED=true.env—WEB_UI_ENABLED=false,WEB_UI_PORT=3001setup/index.ts— addweb-uistepsetup/pf.ts— allow/restrictWEB_UI_PORTas appropriate
User identity on Web UI: invite code entered on first visit, stored in localStorage. Same invite system as Step 2 — no new concepts for the user.
Step 4 — Version bump and docs
- Bump to v0.9.0
- Update
docs/internal/sessions/with a session log for this work - Update install docs to mention Web UI and invite codes
Part A — Decisions
Decision 1 — End user registration model ✓ DECIDED: Invite code
Operator runs npm run invite → gets a short code (e.g. CLAWDIE-A3X7),
shares it out of band. User sends it as their first message. Bot registers them automatically.
Decision 2 — Invite code behaviour ✓ DECIDED: Single-use, optional TTL
One code per person, works until used.
Store invited_by: jid | 'operator' on every registered user from day one —
enables upgrade to delegated invites later without a migration.
Security requirements:
- Optional TTL:
npm run invite --ttl 7d(default: no expiry). TTL stored ininvite_codes.expires_at(nullable). Expired codes rejected silently. - Audit log: every code creation and consumption appended to
logs/invite-audit.logwith timestamp, code, JID (on consume), and source. - Revoke unused codes:
npm run revoke-code <code>hard-deletes an unused code. Used codes cannot be revoked (JID is already registered — usenpm run revoke <jid>instead). - List codes:
npm run codesshows all active codes with status:
CODE CREATED EXPIRES STATUS JID
CLAWDIE-A3X7 2026-03-15 — used web:a3x7
CLAWDIE-B9K2 2026-03-17 2026-03-24 active —
Schema change: add expires_at TIMESTAMP NULL and revoked_at TIMESTAMP NULL
to invite_codes table. Rollback migration: DROP COLUMN expires_at; DROP COLUMN revoked_at;
Decision 3 — Web UI user identity ✓ DECIDED: Invite code login
User enters their invite code on first Web UI visit. Code becomes their persistent identity — survives browser clears, works on any device. Reuses the same invite system as Decision 1. No OAuth, no Google dependency.
Identity recovery: localStorage is used for convenience (avoids re-entry on
repeat visits) but is NOT the authoritative identity store. The server maintains
a permanent invite_code → jid mapping in the invite_codes table. If a user
clears browser data, entering the same invite code re-links them to their
existing JID — no lockout. tryConsumeInviteCode must check for an existing
mapping before creating a new JID:
async function tryConsumeInviteCode(code: string): Promise<{ jid: string; isNew: boolean }> {
const existing = await db.getJidByInviteCode(code);
if (existing) return { jid: existing, isNew: false }; // re-link, not new
// ... create new JID, store mapping
}
Decision 4 — Signal ✓ DEFERRED
signal-cli requires Java. Java in a jail adds attack surface. No mature non-Java Signal client exists. Revisit only when real users ask for it.
Decision 5 — WhatsApp ✓ DEFERRED
Meta-owned, metadata collected, unofficial library only. Revisit only when real users ask for it. If added: use Baileys (pure Node.js, no browser/Puppeteer dependency).
Part B — Implementation steps
Step 1 — Onboarding refactor (prerequisite for everything)
What changes:
setup/onboarding.ts— replace hardcoded Telegram token prompt with channel selectionsetup/telegram.ts— Telegram becomes one option, not the default assumptionsrc/db.ts— addinvite_codestable, addinvited_byfield to registered userssrc/index.ts— handle first-message invite code registration logicnpm run invite— new CLI command to generate invite codesnpm run users— list registered users with channel and invited_bynpm run revoke <jid>— remove a registered user
What does not change:
Channelinterface — already correct- Message pipeline — already channel-agnostic
- JID storage — already channel-prefixed
Step 2 — Web UI (Tier 1)
What changes:
src/channels/web.ts— implementsChannel, WebSocket server, JID prefixweb:src/index.ts— start web channel onWEB_UI_PORT- Frontend: single HTML/CSS/JS chat page served from CMS jail via nginx
setup/web-ui.ts— deploy frontend, configure nginx route.env—WEB_UI_ENABLED,WEB_UI_PORT- Invite code entered on first visit → stored in localStorage for convenience
- localStorage is not authoritative — server re-links on re-entry (see Decision 3)
Rate limiting (WebSocket):
- Per-IP: max 10 messages/minute; excess connections dropped with
429 - Per-JID: suspended users rejected at WebSocket handshake (no session created)
- Implementation: in-memory
Map<ip, { count, resetAt }>insrc/channels/web.ts
Operator user management:
npm run suspend <jid>— setssuspended_at, blocks all channels for that JIDnpm run unsuspend <jid>— clearssuspended_at, restores access- Suspension does not delete data — full history preserved
Access options:
- Private: Tailscale only (no public exposure)
- Public:
chat.yourdomain.comvia nginx, invite code is the only gate
Step 3 — Signal (deferred)
Add only when real users request it. Java/JRE in a jail is the accepted tradeoff at that point.
Step 4 — WhatsApp (deferred)
Add only when real users request it. Tier 2, opt-in, informed-consent warning in setup.
Step 5 — WeChat (future / community)
Requires Chinese business registration. Not part of main Clawdie install.
JID prefix reserved: wc:
Summary
| Step | Target version | Priority |
|---|---|---|
| 1. Onboarding refactor | v0.9.0 | Now — blocks everything else |
| 2. Web UI | v0.9.0 | Now — solves phone dependency |
| 3. Signal | Future | Only on user demand |
| 4. WhatsApp | Future | Only on user demand |
| 5. WeChat | Community | Not in scope |