refactor(permissions): preserve pre-PR behavior in three spots
PR #5 review flagged three behavior changes that shouldn't have slipped in. This commit reverts each to match the pre-refactor behavior exactly. 1. User upsert ordering. Split the router hook into two setters: setSenderResolver (runs before agent resolution) and setAccessGate (runs after). Restores the pre-PR sequence where the users row is upserted even if the message is dropped by wiring or trigger rules. 2. dropped_messages audit. Moved src/modules/permissions/db/dropped-messages.ts back to src/db/dropped-messages.ts. The table is core audit infra, not permissions-specific. Router re-writes rows for no_agent_wired and no_trigger_match; the access gate writes rows for policy refusals. 3. Permissionless container fallback. Dropped. poll-loop restores the original deny-all check when NANOCLAW_ADMIN_USER_IDS is empty. Module contract doc updated with the two-hook shape. Validation: host build clean, 137/137 host tests, 17/17 container tests, typecheck clean, service boots to "NanoClaw running" with permissions module registering both hooks and clean SIGTERM shutdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,30 +68,42 @@ export function registerDeliveryAction(action: string, handler: ActionHandler):
|
||||
|
||||
**Current consumers:** scheduling (5 actions — `schedule_task`, `cancel_task`, `pause_task`, `resume_task`, `update_task`), approvals (3 actions — `install_packages`, `request_rebuild`, `add_mcp_server`), agent-to-agent (`create_agent`, and the agent-routing branch keyed as a pseudo-action `agent_route`).
|
||||
|
||||
### 2. Router inbound gate
|
||||
### 2. Router sender resolver + access gate
|
||||
|
||||
Two separate setters, called at different points in `routeInbound`. Preserves the pre-refactor ordering: sender-upsert side effects fire even when the message is ultimately dropped by wiring or trigger rules.
|
||||
|
||||
```typescript
|
||||
// src/router.ts
|
||||
type InboundGateResult =
|
||||
| { allowed: true; userId: string | null }
|
||||
| { allowed: false; userId: string | null; reason: string };
|
||||
type SenderResolverFn = (event: InboundEvent) => string | null;
|
||||
|
||||
type InboundGateFn = (
|
||||
export function setSenderResolver(fn: SenderResolverFn): void;
|
||||
|
||||
type AccessGateResult =
|
||||
| { allowed: true }
|
||||
| { allowed: false; reason: string };
|
||||
|
||||
type AccessGateFn = (
|
||||
event: InboundEvent,
|
||||
userId: string | null,
|
||||
mg: MessagingGroup,
|
||||
agentGroupId: string,
|
||||
) => InboundGateResult;
|
||||
) => AccessGateResult;
|
||||
|
||||
export function setInboundGate(fn: InboundGateFn): void;
|
||||
export function setAccessGate(fn: AccessGateFn): void;
|
||||
```
|
||||
|
||||
**Purpose:** single-setter gate that owns both sender resolution (user upsert) and access decision. Takes the raw event because the permissions module needs the sender fields inside `event.message.content`.
|
||||
**Call order in `routeInbound`:**
|
||||
1. Resolve messaging group.
|
||||
2. **Sender resolver** (if set). Permissions upserts the users row here so the record exists even if agent resolution drops the message.
|
||||
3. Resolve wired agents; `no_agent_wired` → record + drop. (Core writes the dropped_messages row.)
|
||||
4. Pick agent by trigger rules; `no_trigger_match` → record + drop.
|
||||
5. **Access gate** (if set). On refusal it writes its own `dropped_messages` row keyed by policy reason.
|
||||
|
||||
**Default when unset:** `{ allowed: true, userId: null }`. Every message routes through, no users table is needed, downstream must tolerate `userId=null`.
|
||||
**Defaults when unset:** resolver returns null; gate defaults to `{ allowed: true }`. Every message routes through, no users table is needed, downstream tolerates `userId=null`.
|
||||
|
||||
**Current consumer:** permissions module.
|
||||
**Current consumer:** permissions module (registers both).
|
||||
|
||||
**Not a registry, a setter.** There is one decision per inbound message and one module that owns it. Calling `setInboundGate` twice overwrites; core does not iterate.
|
||||
**Not registries, setters.** There is one sender and one access decision per inbound message and one module that owns both. Calling `setSenderResolver` / `setAccessGate` twice overwrites; core does not iterate.
|
||||
|
||||
### 3. Response dispatcher
|
||||
|
||||
|
||||
Reference in New Issue
Block a user