refactor: scaffold module registries and default-module layout
Additive change — existing code paths still run via inline fallbacks. Prepares core for per-module extractions in PR #3 onward. Four registries added with empty defaults: - delivery action handlers (delivery.ts) - router inbound gate (router.ts) - response dispatcher (index.ts) - MCP tool self-registration (container/agent-runner/src/mcp-tools/server.ts) Default modules moved to src/modules/ for signaling: - src/modules/typing/ (extracted from delivery.ts) - src/modules/mount-security/ (moved from src/mount-security.ts) Both are imported directly by core — no hook, no registry. Removal requires editing core imports. Migrator now keys applied rows by name (uniqueness) so module migrations can pick arbitrary version numbers. Stored version column is auto-assigned as an applied-order sequence. sqlite_master guards added around core calls into module-owned tables (user_roles, agent_destinations, pending_questions). No-ops today; load-bearing after the owning modules are extracted. MODULE-HOOK markers placed at scheduling's two skill-edit sites (host-sweep.ts recurrence call, poll-loop.ts pre-task gate). PR #4 replaces the marked blocks when scheduling moves to its module. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import { getChannelAdapter } from './channels/channel-registry.js';
|
||||
import { isMember } from './db/agent-group-members.js';
|
||||
import { getMessagingGroupByPlatform, createMessagingGroup, getMessagingGroupAgents } from './db/messaging-groups.js';
|
||||
import { upsertUser, getUser } from './db/users.js';
|
||||
import { startTypingRefresh } from './delivery.js';
|
||||
import { startTypingRefresh } from './modules/typing/index.js';
|
||||
import { log } from './log.js';
|
||||
import { resolveSession, writeSessionMessage } from './session-manager.js';
|
||||
import { wakeContainer } from './container-runner.js';
|
||||
@@ -45,6 +45,34 @@ export interface InboundEvent {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inbound gate registry.
|
||||
*
|
||||
* A module (permissions, today) can register a single gate function that
|
||||
* owns sender resolution + access decision. Without a registered gate,
|
||||
* core falls back to the inline `extractAndUpsertUser` +
|
||||
* `enforceAccess` + `handleUnknownSender` chain.
|
||||
*
|
||||
* Takes the raw event so the gate can read sender fields from
|
||||
* `event.message.content`. Returns either allowed=true with a `userId`
|
||||
* (null if unresolved) or allowed=false with a reason; core drops the
|
||||
* message on refusal.
|
||||
*/
|
||||
export type InboundGateResult =
|
||||
| { allowed: true; userId: string | null }
|
||||
| { allowed: false; userId: string | null; reason: string };
|
||||
|
||||
export type InboundGateFn = (event: InboundEvent, mg: MessagingGroup, agentGroupId: string) => InboundGateResult;
|
||||
|
||||
let inboundGate: InboundGateFn | null = null;
|
||||
|
||||
export function setInboundGate(fn: InboundGateFn): void {
|
||||
if (inboundGate) {
|
||||
log.warn('Inbound gate overwritten');
|
||||
}
|
||||
inboundGate = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route an inbound message from a channel adapter to the correct session.
|
||||
* Creates messaging group + session if they don't exist yet.
|
||||
@@ -81,13 +109,8 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Resolve sender → user id. Upsert into users table on first sight so
|
||||
// subsequent messages find an existing row. `userId` is null if the
|
||||
// adapter didn't give us enough to identify a sender (the gate will
|
||||
// then apply unknown_sender_policy).
|
||||
const userId = extractAndUpsertUser(event);
|
||||
|
||||
// 3. Resolve agent groups wired to this messaging group
|
||||
// 2. Resolve agent groups wired to this messaging group. (The gate runs
|
||||
// after this so it can decide based on the target agent group.)
|
||||
const agents = getMessagingGroupAgents(mg.id);
|
||||
if (agents.length === 0) {
|
||||
log.warn('MESSAGE DROPPED — no agent groups wired to this channel. Run setup register step to configure.', {
|
||||
@@ -128,13 +151,31 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Access gate. Public channels skip the gate entirely.
|
||||
if (mg.unknown_sender_policy !== 'public') {
|
||||
const gate = enforceAccess(userId, match.agent_group_id);
|
||||
if (!gate.allowed) {
|
||||
handleUnknownSender(mg, userId, match.agent_group_id, gate.reason, event);
|
||||
// 3. Inbound gate: sender resolution + access decision. If a module
|
||||
// registered a gate, it owns the whole thing (it can upsert users,
|
||||
// check roles, etc.). Otherwise fall back to the inline chain.
|
||||
let userId: string | null;
|
||||
if (inboundGate) {
|
||||
const result = inboundGate(event, mg, match.agent_group_id);
|
||||
userId = result.userId;
|
||||
if (!result.allowed) {
|
||||
log.info('MESSAGE DROPPED — inbound gate refused', {
|
||||
messagingGroupId: mg.id,
|
||||
agentGroupId: match.agent_group_id,
|
||||
userId,
|
||||
reason: result.reason,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
userId = extractAndUpsertUser(event);
|
||||
if (mg.unknown_sender_policy !== 'public') {
|
||||
const gate = enforceAccess(userId, match.agent_group_id);
|
||||
if (!gate.allowed) {
|
||||
handleUnknownSender(mg, userId, match.agent_group_id, gate.reason, event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Resolve or create session.
|
||||
|
||||
Reference in New Issue
Block a user