mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-09 18:57:08 +08:00
refactor(guard): consults carry the defined action value — delete the registry walk
The guard's weak point was the wiring: catalog entries registered by
side-effect import, consult sites naming them by string, a map miss
resolving to ALLOW, and a boot walk that could only see 2 of the 4
registries (response handlers and interceptors erased their specs into
closures). Dropping one unreferenced import silently disarmed
channel-registration click-auth.
Make the broken wiring unconstructible instead of detected:
- defineGuardedAction returns a branded GuardedAction value; guard(action,
input) takes the value, not a name. A dropped module-edge import or a
typo'd action is now a compile error; there is no lookup and no
fail-open branch. A forged value (outside TS) is denied at runtime.
- Every registry (delivery actions, response handlers, interceptors)
requires a guard spec or an explicit unguarded(<reason>) declaration at
the registration site — unguarded-by-omission is not representable, and
the justification travels with the registration (grep "unguarded(" for
the complete inventory). CLI commands derive their guard inside
register(), so a command cannot exist without one.
- guardConformanceViolations() and EXEMPT_DELIVERY_ACTIONS are gone. The
boot check shrinks to the one cross-registry invariant the compiler
can't see: every holding action has a registered approve continuation
(grantContinuationGaps), same fail-closed refuse-to-start posture.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+42
-37
@@ -8,52 +8,57 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
|
||||
import { registerDeliveryAction } from '../delivery.js';
|
||||
import { unguarded } from '../guard/index.js';
|
||||
import { insertMessage } from '../db/session-db.js';
|
||||
import { log } from '../log.js';
|
||||
import { dispatch } from './dispatch.js';
|
||||
import type { RequestFrame } from './frame.js';
|
||||
import type { Session } from '../types.js';
|
||||
|
||||
registerDeliveryAction('cli_request', async (content, session, inDb) => {
|
||||
const requestId = content.requestId as string;
|
||||
const command = content.command as string;
|
||||
const args = (content.args as Record<string, unknown>) ?? {};
|
||||
registerDeliveryAction(
|
||||
'cli_request',
|
||||
async (content, session, inDb) => {
|
||||
const requestId = content.requestId as string;
|
||||
const command = content.command as string;
|
||||
const args = (content.args as Record<string, unknown>) ?? {};
|
||||
|
||||
if (!requestId || !command) {
|
||||
log.warn('cli_request missing requestId or command', { sessionId: session.id });
|
||||
return;
|
||||
}
|
||||
if (!requestId || !command) {
|
||||
log.warn('cli_request missing requestId or command', { sessionId: session.id });
|
||||
return;
|
||||
}
|
||||
|
||||
const req: RequestFrame = { id: requestId, command, args };
|
||||
const ctx = {
|
||||
caller: 'agent' as const,
|
||||
sessionId: session.id,
|
||||
agentGroupId: session.agent_group_id,
|
||||
messagingGroupId: session.messaging_group_id ?? '',
|
||||
};
|
||||
const req: RequestFrame = { id: requestId, command, args };
|
||||
const ctx = {
|
||||
caller: 'agent' as const,
|
||||
sessionId: session.id,
|
||||
agentGroupId: session.agent_group_id,
|
||||
messagingGroupId: session.messaging_group_id ?? '',
|
||||
};
|
||||
|
||||
log.info('CLI request from agent', { requestId, command, sessionId: session.id });
|
||||
log.info('CLI request from agent', { requestId, command, sessionId: session.id });
|
||||
|
||||
const response = await dispatch(req, ctx);
|
||||
const response = await dispatch(req, ctx);
|
||||
|
||||
// Write response to inbound.db so the container can read it.
|
||||
// trigger=0: don't wake the agent — this is an inline response to a tool call.
|
||||
insertMessage(inDb, {
|
||||
id: `cli-resp-${requestId}`,
|
||||
kind: 'system',
|
||||
timestamp: new Date().toISOString(),
|
||||
platformId: null,
|
||||
channelType: null,
|
||||
threadId: null,
|
||||
content: JSON.stringify({
|
||||
type: 'cli_response',
|
||||
requestId,
|
||||
frame: response,
|
||||
}),
|
||||
processAfter: null,
|
||||
recurrence: null,
|
||||
trigger: 0,
|
||||
});
|
||||
// Write response to inbound.db so the container can read it.
|
||||
// trigger=0: don't wake the agent — this is an inline response to a tool call.
|
||||
insertMessage(inDb, {
|
||||
id: `cli-resp-${requestId}`,
|
||||
kind: 'system',
|
||||
timestamp: new Date().toISOString(),
|
||||
platformId: null,
|
||||
channelType: null,
|
||||
threadId: null,
|
||||
content: JSON.stringify({
|
||||
type: 'cli_response',
|
||||
requestId,
|
||||
frame: response,
|
||||
}),
|
||||
processAfter: null,
|
||||
recurrence: null,
|
||||
trigger: 0,
|
||||
});
|
||||
|
||||
log.info('CLI response written', { requestId, ok: response.ok, sessionId: session.id });
|
||||
});
|
||||
log.info('CLI response written', { requestId, ok: response.ok, sessionId: session.id });
|
||||
},
|
||||
unguarded('transport envelope — every inner command is guarded at dispatch'),
|
||||
);
|
||||
|
||||
+2
-4
@@ -20,9 +20,8 @@ import { registerApprovalHandler, requestApproval } from '../modules/approvals/i
|
||||
import type { PendingApproval } from '../types.js';
|
||||
import type { CallerContext, ErrorCode, RequestFrame, ResponseFrame } from './frame.js';
|
||||
import { getResource } from './crud.js';
|
||||
import { commandGuardAction } from './guard.js';
|
||||
import { listVerbs, renderVerbHelp } from './help-render.js';
|
||||
import { listCommands, lookup } from './registry.js';
|
||||
import { commandGuard, listCommands, lookup } from './registry.js';
|
||||
|
||||
type DispatchOptions = {
|
||||
/** Verified approval row when a command is replayed after approval. */
|
||||
@@ -101,8 +100,7 @@ export async function dispatch(
|
||||
}
|
||||
}
|
||||
|
||||
const decision = guard({
|
||||
action: commandGuardAction(cmd),
|
||||
const decision = guard(commandGuard(cmd.name), {
|
||||
actor: actorFor(ctx),
|
||||
payload: req.args,
|
||||
grant: opts.grant ?? null,
|
||||
|
||||
+14
-4
@@ -8,7 +8,7 @@
|
||||
* registers the help commands, so the registry is populated before the host's
|
||||
* CLI server accepts connections.
|
||||
*/
|
||||
import { registerGuardedAction } from '../guard/index.js';
|
||||
import { defineGuardedAction, type GuardedAction } from '../guard/index.js';
|
||||
import { commandGuardSpec } from './guard.js';
|
||||
import type { CallerContext } from './frame.js';
|
||||
|
||||
@@ -61,6 +61,7 @@ export type CommandDef<TArgs = unknown, TData = unknown> = {
|
||||
};
|
||||
|
||||
const registry = new Map<string, CommandDef>();
|
||||
const commandGuards = new Map<string, GuardedAction>();
|
||||
|
||||
export function register<TArgs, TData>(def: CommandDef<TArgs, TData>): void {
|
||||
if (registry.has(def.name)) {
|
||||
@@ -68,9 +69,18 @@ export function register<TArgs, TData>(def: CommandDef<TArgs, TData>): void {
|
||||
}
|
||||
registry.set(def.name, def as CommandDef);
|
||||
// Declaration is registration: every command gets a guard-catalog entry
|
||||
// derived from its own definition — dispatch consults the guard, and the
|
||||
// conformance test walks the registry against the catalog.
|
||||
registerGuardedAction(commandGuardSpec(def as CommandDef));
|
||||
// derived from its own definition, in the same call that registers it — a
|
||||
// command cannot exist without a guard, and dispatch consults it by value.
|
||||
commandGuards.set(def.name, defineGuardedAction(commandGuardSpec(def as CommandDef)));
|
||||
}
|
||||
|
||||
/** The guard defined for a registered command — total for anything register() accepted. */
|
||||
export function commandGuard(name: string): GuardedAction {
|
||||
const g = commandGuards.get(name);
|
||||
if (!g) {
|
||||
throw new Error(`CLI command "${name}" has no guard — was it registered through register()?`);
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
export function lookup(name: string): CommandDef | undefined {
|
||||
|
||||
Reference in New Issue
Block a user