Files
nanoclaw/docs/v1-vs-v2/sender-allowlist.md
gavrielc 47950671fa docs: add v1→v2 action-items analysis + SDK signal probe tool
- docs/v1-vs-v2/: full v1→v2 regression analysis (SUMMARY + 21 per-module
  docs + ACTION-ITEMS rollup with decisions + timezone recreation spec).
- container/agent-runner/scripts/sdk-signal-probe.ts: empirical harness
  used to characterise Claude Agent SDK event/hook/stderr timing for the
  stuck-detection design in item 9.
- src/channels/chat-sdk-bridge.ts: document the conversations Map staleness
  in a code comment; fix deferred to when dynamic group registration lands
  (ACTION-ITEMS item 17).

No runtime behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 01:00:04 +03:00

47 lines
3.6 KiB
Markdown

# sender-allowlist: v1 vs v2
## Scope
- v1: `src/v1/sender-allowlist.ts` (97 LOC), `sender-allowlist.test.ts` (217 LOC) — flat JSON config at `~/.config/nanoclaw/sender-allowlist.json`
- v2 counterparts: `src/access.ts` (116 LOC), `src/router.ts` (317 LOC), `src/db/schema.ts` (user_roles, agent_group_members, messaging_groups.unknown_sender_policy), `src/container-runner.ts:291-295` (admin injection), `src/types.ts` (MessagingGroupAgent.response_scope)
## Capability map
| v1 behavior | v2 location | Status | Notes |
|---|---|---|---|
| Per-chat entry (`chats[chatJid]`) | `messaging_groups.unknown_sender_policy` | replaced | Policy per channel, not allowlist entries |
| Default entry | Default `unknown_sender_policy = 'strict'` | **reversed** | v1 default-allow → v2 default-deny |
| `allow: '*'` wildcard | Not present | removed | |
| `allow: string[]` (exact-match list) | `agent_group_members` rows + `user_roles` | replaced | Role-based / membership-based |
| `mode: 'trigger'` (allow for processing) | Implicit (access granted → routed) | kept | |
| `mode: 'drop'` (silent drop) | `recordDroppedMessage()` (logs only) | **partially lost** | No silent-drop mode; denied = logged |
| Admin override | owner / global_admin / scoped_admin | **new in v2** | Richer privilege hierarchy |
| Static JSON file | Central DB (`users`, `user_roles`, `agent_group_members`) | changed | Runtime-mutable, queryable |
| Exact-string sender | Namespaced `channel_type:handle` user IDs | enhanced | Explicit channel scoping |
| `logDenied` flag | implicit (log at decision point) | kept | |
## Access-model diff
**v1**: flat allowlist per chat → default-allow → binary allowed/denied.
**v2**: entity model (`users` + roles + memberships) + per-messaging-group policy (`strict | request_approval | public`) → default-deny for unknowns.
**Strictly more expressive:** role hierarchy, per-agent-group scope, three-way unknown handling, user metadata (display_name/kind), runtime reconfig.
**Lost:** per-message `drop` mode, default-allow posture, simple JSON editing.
## Missing from v2
1. **`request_approval` flow** — marked TODO in `router.ts:295`. Approval-on-first-contact for unknown senders is scaffolded but not wired
2. **`response_scope` enforcement** — field exists (`'all' | 'triggered' | 'allowlisted'`) but is not checked in `router.ts` or `delivery.ts`
3. **Trigger-rule matching on `messaging_group_agents`**`router.ts:198` TODO ("Future: trigger rule matching"); currently only priority-based agent selection
4. **Silent-drop option for known-noisy senders** — v1's `mode: 'drop'` allowed "I see you but I ignore you"; v2 can only log and drop
## Behavioral discrepancies
1. **Default posture flipped**: v1 open-by-default vs v2 closed-by-default — **breaking for migrations that relied on default-allow**
2. **Drop semantics**: v1 silent drop; v2 `recordDroppedMessage()` always logs
3. **Admin bypass**: v1 had no implicit bypass; v2 grants owners/admins access regardless of membership — more permissive for privileged users
4. **Scope resolution**: v1 per-chat; v2 per-agent-group via `user_roles.agent_group_id` — misalignment if one chat routes to multiple agent groups with different admins
## Worth preserving?
The v2 role-based model is architecturally superior. The gaps worth closing:
- **Finish `request_approval`** flow — half-implemented scaffolding
- **Finish `response_scope` enforcement** — exists in schema but unused
- **Finish trigger-rule matching** in `pickAgent` — without it, every wired agent fires on every message
- **Consider silent-drop via a dedicated table** (`(agent_group_id, sender_pattern)` with action=drop) — orthogonal to privilege