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:
gavrielc
2026-04-18 18:00:10 +03:00
parent 7cc4ecc3be
commit 32bcc2c5ae
5 changed files with 142 additions and 75 deletions

View File

@@ -80,10 +80,6 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
// Handle commands: categorize chat messages
const adminUserIds = config.adminUserIds ?? new Set<string>();
// Permissionless mode: when the permissions module isn't installed on
// the host, NANOCLAW_ADMIN_USER_IDS arrives empty. Treat every sender
// with an identifiable senderId as admin so admin commands still work.
const permissionless = adminUserIds.size === 0;
const normalMessages = [];
const commandIds: string[] = [];
@@ -103,8 +99,7 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
}
if (cmdInfo.category === 'admin') {
const authorized = permissionless ? !!cmdInfo.senderId : !!cmdInfo.senderId && adminUserIds.has(cmdInfo.senderId);
if (!authorized) {
if (!cmdInfo.senderId || !adminUserIds.has(cmdInfo.senderId)) {
log(`Admin command denied: ${cmdInfo.command} from ${cmdInfo.senderId} (msg: ${msg.id})`);
writeMessageOut({
id: generateId(),