Every approved pairing expires 30 days after approval (pairedDeviceTTL),
and nothing lets an operator change that: owners of a personal setup have
to re-pair their own Telegram account and browser every month. The schema
already treats a NULL paired_devices.expires_at as "never expires" (IsPaired
and the prune both check for it), only no code path ever writes NULL.
- store: SetPairingPermanent(senderID, channel, permanent) on PairingStore,
PG and SQLite. permanent=true clears expires_at, false restarts the
default TTL from now. An already expired pairing is not revived.
PairedDeviceData gains expires_at (Unix ms, null = never), returned by
ApprovePairing and ListPaired.
- gateway: device.pair.approve accepts `permanent`; new admin RPC
device.pair.update {senderId, channel, permanent} for existing pairings,
classified in permissions/policy.go next to the other pairing methods.
- mcp: pairing approve tool accepts `permanent`.
- web UI (Nodes): "Never expires" switch in the approve dialog, an
Expires column, and a Make permanent / Set expiry action per device.
ConfirmDialog takes optional children for the switch.
The default stays 30 days; permanence is an explicit operator choice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When an agent's DM/Group policy is "pairing (require code)", an unpaired
Bitrix24 sender got silence: the channel only had a Phase 03 stub
(logPairingNeeded) that logged "pairing required (Phase 07 will send pairing
reply)" and dropped the message — no code was ever generated or sent, so an
admin had nothing to approve. Bitrix24 was the only channel missing this;
Discord/Zalo/WhatsApp/Slack/Feishu already send a pairing reply.
Replace the stub with sendPairingReply, mirroring the other channels:
- RequestPairing() to generate the code (keyed by senderID for DMs,
"group:<chatID>" for groups so it matches CheckGroupPolicy.IsPaired).
- Render the shared systemmessages.KeyPairingAccountRequired message
(code + how to approve).
- Send it back into the chat via the v2 API (imbot.v2.Chat.Message.send),
consistent with the welcome / OAuth-invite messages — not the legacy
imbot.message.add v1.
- Keep the per-sender debounce so a spammy sender can't flood the chat.
Tests: send fires on the v2 endpoint with the code, no-op without a pairing
service, and the second call within the debounce window is suppressed.
Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>