refactor(modules): extract permissions as optional module
Moves user-roles / users / agent-group-members / user-dms / dropped-messages / user-dm / canAccessAgentGroup into src/modules/permissions/. Module registers a single inbound-gate that owns sender resolution, access decision, unknown-sender policy, and drop-audit recording. Router slimmed from 357 → 179 lines; the inline fallback chain (extractAndUpsertUser / enforceAccess / handleUnknownSender / recordDroppedMessage) is gone — without the permissions module core defaults to allow-all with userId=null. container-runner's admin-ID query is now inline SQL guarded by sqlite_master on user_roles, keeping core free of any import from the permissions module. The container-side formatter falls back to permissionless mode when NANOCLAW_ADMIN_USER_IDS is empty: every sender with an identifiable senderId is treated as admin. Module contract doc formalizes the tier model and the dependency rule (core ← default modules ← optional modules). One transitional violation flagged: src/access.ts (core) imports from the permissions module for its remaining approver-picking helpers; resolves in the planned PR #7 re-tier. Validation: host build clean, 137/137 host tests, 17/17 container tests, typecheck clean, service boots to "NanoClaw running" with permissions module registering its gate and clean SIGTERM shutdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,10 @@ 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[] = [];
|
||||
|
||||
@@ -99,7 +103,8 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
|
||||
}
|
||||
|
||||
if (cmdInfo.category === 'admin') {
|
||||
if (!cmdInfo.senderId || !adminUserIds.has(cmdInfo.senderId)) {
|
||||
const authorized = permissionless ? !!cmdInfo.senderId : !!cmdInfo.senderId && adminUserIds.has(cmdInfo.senderId);
|
||||
if (!authorized) {
|
||||
log(`Admin command denied: ${cmdInfo.command} from ${cmdInfo.senderId} (msg: ${msg.id})`);
|
||||
writeMessageOut({
|
||||
id: generateId(),
|
||||
|
||||
Reference in New Issue
Block a user