- Remove TOCTOU exists() check in _load(); rely on FileNotFoundError
- Define PAIRING_CODE_META_KEY and PAIRING_COMMAND_META_KEY constants
in nanobot.pairing, replacing magic strings across base.py, slack.py,
and builtin.py
- Flatten nested revoke logic in handle_pairing_command()
- Trim redundant docstring/comment noise in is_allowed() and generate_code()
CLI pairing commands (list/approve/deny/revoke) are fully replaceable by
`nanobot agent -m "/pairing ..."`, which routes through the same
CommandRouter and handle_pairing_command() backend. Removing them
cuts 86 lines of duplicate surface area without losing any functionality.
- Remove pairing_app and its 4 subcommands from cli/commands.py
- Update format_pairing_reply() to drop the "Via CLI" line
- Clear pending_user_turn after shortcut command persistence
- Guard is_allowed against None allow_from values
- Update pairing help text for two-arg revoke
- Reuse format_expiry in CLI pairing list
Update format_pairing_reply() to be more conversational and explicitly
mention both ways an owner can approve:
- In-chat: /pairing approve <code>
- CLI: nanobot pairing approve <code>
/pairing is now a first-class built-in command dispatched through
CommandRouter, just like /status, /model, /dream, etc.
Benefits:
- WebUI automatically shows /pairing in the slash command palette
(because builtin_command_palette() feeds /api/commands).
- All channels (Telegram, Discord, WebSocket, etc.) use the same
dispatch path for /pairing; no more channel-level interception.
- The command still only works for already-authorised users because
is_allowed() gates message ingestion before the bus.
Changes:
- Add handle_pairing_command() to nanobot.pairing.store — pure
function callable from CLI, CommandRouter, and tests.
- Add cmd_pairing to nanobot.command.builtin and register in
BUILTIN_COMMAND_SPECS + register_builtin_commands().
- Remove BaseChannel._handle_pairing_command() and the /pairing
interception logic from _handle_message().
- Clean up unused pairing imports from base.py.
- Add unit tests for handle_pairing_command and cmd_pairing dispatch.
- Extract format_pairing_reply() and format_expiry() to eliminate
duplication between BaseChannel and SlackChannel.
- Use _write_text_atomic() from helpers.py instead of hand-rolled
fsync logic in pairing store.
- Convert approved lists to in-memory sets for O(1) lookup.
- Remove collision retry loop (8-char entropy is sufficient).
- Fix /pairing command parsing to split prefix exactly.
- Remove unused import time from base.py.
- Fix tests to pass subcommand_text, not full /pairing string.
- Add os.fsync with Windows-compatible directory flush in pairing store
- Increase pairing code length from 6 -> 8 characters for higher entropy
- Remove SystemExit on empty allowFrom; empty list now defers to pairing
- Update is_allowed docstring to document pairing fallback semantics
- Propagate is_dm to Matrix (direct rooms) and Slack (im channels)
- Slack _is_allowed now checks pairing store for DM allowlist mode
- Fix /pairing revoke to accept optional channel argument
- Move inline import time to module top-level
- Add WebSocket comment explaining is_dm=True assumption
- Add comprehensive tests for store and BaseChannel pairing integration
- Fix existing tests that expected empty allowFrom to hard-exit
Refs #3774
Replace the file-editing onboarding workflow with a chat-native pairing flow:
- New pairing store (nanobot/pairing/store.py) persists approved senders
and pending codes in ~/.nanobot/pairing.json.
- DM messages from unknown senders receive a short pairing code instead of
silent denial. Group chats remain silently ignored.
- Existing allowFrom semantics are fully preserved; approved pairing users
are merged at runtime so no config migration is needed.
- nanobot pairing list/approve/deny/revoke CLI commands for bootstrap and
emergency management.
- /pairing slash commands intercepted in-channel so owners can approve
senders without leaving the chat.
- is_dm flag added to BaseChannel._handle_message; Telegram, Discord and
WebSocket updated to pass it.
Closes#3768