mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-09 18:57:08 +08:00
4ef7d7367c
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>
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
/**
|
|
* Interactive module — generic ask_user_question flow.
|
|
*
|
|
* Container-side `ask_user_question` writes a chat-sdk card to outbound.db +
|
|
* polls inbound.db for a `question_response` system message. On the host side
|
|
* this module handles the button-click response: look up the pending_questions
|
|
* row, write the response into the session's inbound.db, wake the container.
|
|
*
|
|
* The `createPendingQuestion` call in `deliverMessage` (delivery.ts) stays
|
|
* inline in core — it's 15 lines guarded by `hasTable('pending_questions')`,
|
|
* modularizing it adds more registry surface than it saves.
|
|
*/
|
|
import { getDb, hasTable } from '../../db/connection.js';
|
|
import { deletePendingQuestion, getPendingQuestion, getSession } from '../../db/sessions.js';
|
|
import { wakeContainer } from '../../container-runner.js';
|
|
import { unguarded } from '../../guard/index.js';
|
|
import { registerResponseHandler, type ResponsePayload } from '../../response-registry.js';
|
|
import { log } from '../../log.js';
|
|
import { writeSessionMessage } from '../../session-manager.js';
|
|
|
|
async function handleInteractiveResponse(payload: ResponsePayload): Promise<boolean> {
|
|
if (!hasTable(getDb(), 'pending_questions')) return false;
|
|
|
|
const pq = getPendingQuestion(payload.questionId);
|
|
if (!pq) return false;
|
|
|
|
const session = getSession(pq.session_id);
|
|
if (!session) {
|
|
log.warn('Session not found for pending question', { questionId: payload.questionId, sessionId: pq.session_id });
|
|
deletePendingQuestion(payload.questionId);
|
|
return true; // claimed — we owned this questionId even though the session is gone
|
|
}
|
|
|
|
writeSessionMessage(session.agent_group_id, session.id, {
|
|
id: `qr-${payload.questionId}-${Date.now()}`,
|
|
kind: 'system',
|
|
timestamp: new Date().toISOString(),
|
|
platformId: pq.platform_id,
|
|
channelType: pq.channel_type,
|
|
threadId: pq.thread_id,
|
|
content: JSON.stringify({
|
|
type: 'question_response',
|
|
questionId: payload.questionId,
|
|
selectedOption: payload.value,
|
|
userId: payload.userId ?? '',
|
|
}),
|
|
});
|
|
|
|
deletePendingQuestion(payload.questionId);
|
|
log.info('Question response routed', {
|
|
questionId: payload.questionId,
|
|
selectedOption: payload.value,
|
|
sessionId: session.id,
|
|
});
|
|
|
|
await wakeContainer(session);
|
|
return true;
|
|
}
|
|
|
|
registerResponseHandler(
|
|
handleInteractiveResponse,
|
|
unguarded('not privileged — relays an in-chat answer back into the asking session; only the question row is touched'),
|
|
);
|