From a597b42648c3a50792652fade46b7f0d4576fcdc Mon Sep 17 00:00:00 2001 From: gavrielc Date: Wed, 6 May 2026 00:40:15 +0300 Subject: [PATCH] feat(cli): add remaining resources, fix descriptions from code review New read-only resources: - destinations (agent-to-agent ACL + routing map) - user-dms (DM channel cache) - dropped-messages (audit trail for dropped messages) - approvals (in-flight approval cards) Description fixes from reading source: - messaging-groups: add denied_at column (router checks it) - sessions: fix container_status (idle is unused, stopped is auto-restarted by sweep) - wirings: add note that threaded adapters force per-thread Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli/resources/approvals.ts | 36 +++++++++++++ src/cli/resources/destinations.ts | 77 +++++++++++++++++++++++++++ src/cli/resources/dropped-messages.ts | 28 ++++++++++ src/cli/resources/index.ts | 4 ++ src/cli/resources/messaging-groups.ts | 10 +++- src/cli/resources/sessions.ts | 3 +- src/cli/resources/user-dms.ts | 21 ++++++++ src/cli/resources/wirings.ts | 5 +- 8 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 src/cli/resources/approvals.ts create mode 100644 src/cli/resources/destinations.ts create mode 100644 src/cli/resources/dropped-messages.ts create mode 100644 src/cli/resources/user-dms.ts diff --git a/src/cli/resources/approvals.ts b/src/cli/resources/approvals.ts new file mode 100644 index 0000000..a5310a4 --- /dev/null +++ b/src/cli/resources/approvals.ts @@ -0,0 +1,36 @@ +import { registerResource } from '../crud.js'; + +registerResource({ + name: 'approval', + plural: 'approvals', + table: 'pending_approvals', + description: + 'Pending approval — in-flight approval cards waiting for an admin response. Created by requestApproval() (self-mod install_packages/add_mcp_server) and OneCLI credential approval flow. Rows are deleted after the admin approves/rejects or the request expires.', + idColumn: 'approval_id', + columns: [ + { name: 'approval_id', type: 'string', description: 'Unique approval identifier (also used as the card questionId).' }, + { name: 'session_id', type: 'string', description: 'Session that requested the approval. Null for OneCLI credential approvals.' }, + { name: 'request_id', type: 'string', description: 'Original request identifier (OneCLI request UUID or same as approval_id).' }, + { + name: 'action', + type: 'string', + description: 'Action type — matches the registered approval handler (e.g. install_packages, add_mcp_server, onecli_credential).', + }, + { name: 'payload', type: 'json', description: 'JSON payload carried through to the approval handler.' }, + { name: 'created_at', type: 'string', description: 'Auto-set.' }, + { name: 'agent_group_id', type: 'string', description: 'Originating agent group.' }, + { name: 'channel_type', type: 'string', description: 'Channel the approval card was delivered on.' }, + { name: 'platform_id', type: 'string', description: 'Platform chat ID the card was delivered to.' }, + { name: 'platform_message_id', type: 'string', description: 'Platform message ID of the delivered card (for editing on expiry).' }, + { name: 'expires_at', type: 'string', description: 'When this approval expires (OneCLI gateway TTL).' }, + { + name: 'status', + type: 'string', + description: 'Current status.', + enum: ['pending', 'approved', 'rejected', 'expired'], + }, + { name: 'title', type: 'string', description: 'Card title shown to the admin.' }, + { name: 'options_json', type: 'json', description: 'Card button options as JSON array.' }, + ], + operations: { list: 'open', get: 'open' }, +}); diff --git a/src/cli/resources/destinations.ts b/src/cli/resources/destinations.ts new file mode 100644 index 0000000..ea67035 --- /dev/null +++ b/src/cli/resources/destinations.ts @@ -0,0 +1,77 @@ +import { getDb } from '../../db/connection.js'; +import { registerResource } from '../crud.js'; + +registerResource({ + name: 'destination', + plural: 'destinations', + table: 'agent_destinations', + description: + 'Agent destination — per-agent routing entry and ACL. Each row authorizes an agent to send messages to a target (channel or another agent) and assigns a local name the agent uses to address it. Names are scoped to the source agent — two agents can have different local names for the same target. Created automatically when wiring channels or when agents create child agents.', + idColumn: 'agent_group_id', + columns: [ + { + name: 'agent_group_id', + type: 'string', + description: 'The agent that owns this destination. References agent_groups.id.', + }, + { + name: 'local_name', + type: 'string', + description: + 'Name the agent uses to address this target (e.g. ). Unique per agent. Lowercase, dash-separated.', + }, + { + name: 'target_type', + type: 'string', + description: '"channel" for messaging group targets, "agent" for agent-to-agent targets.', + enum: ['channel', 'agent'], + }, + { + name: 'target_id', + type: 'string', + description: 'The target\'s ID — messaging_groups.id for channels, agent_groups.id for agents.', + }, + { name: 'created_at', type: 'string', description: 'Auto-set.' }, + ], + operations: { list: 'open' }, + customOperations: { + add: { + access: 'approval', + description: 'Add a destination for an agent. Use --agent-group-id, --local-name, --target-type, --target-id.', + handler: async (args) => { + const agentGroupId = args.agent_group_id as string; + const localName = args.local_name as string; + const targetType = args.target_type as string; + const targetId = args.target_id as string; + if (!agentGroupId) throw new Error('--agent-group-id is required'); + if (!localName) throw new Error('--local-name is required'); + if (!targetType || !['channel', 'agent'].includes(targetType)) { + throw new Error('--target-type must be channel or agent'); + } + if (!targetId) throw new Error('--target-id is required'); + getDb() + .prepare( + `INSERT INTO agent_destinations (agent_group_id, local_name, target_type, target_id, created_at) + VALUES (?, ?, ?, ?, datetime('now'))`, + ) + .run(agentGroupId, localName, targetType, targetId); + return { agent_group_id: agentGroupId, local_name: localName, target_type: targetType, target_id: targetId }; + }, + }, + remove: { + access: 'approval', + description: 'Remove a destination from an agent. Use --agent-group-id and --local-name.', + handler: async (args) => { + const agentGroupId = args.agent_group_id as string; + const localName = args.local_name as string; + if (!agentGroupId) throw new Error('--agent-group-id is required'); + if (!localName) throw new Error('--local-name is required'); + const result = getDb() + .prepare('DELETE FROM agent_destinations WHERE agent_group_id = ? AND local_name = ?') + .run(agentGroupId, localName); + if (result.changes === 0) throw new Error('destination not found'); + return { removed: { agent_group_id: agentGroupId, local_name: localName } }; + }, + }, + }, +}); diff --git a/src/cli/resources/dropped-messages.ts b/src/cli/resources/dropped-messages.ts new file mode 100644 index 0000000..3404fc2 --- /dev/null +++ b/src/cli/resources/dropped-messages.ts @@ -0,0 +1,28 @@ +import { registerResource } from '../crud.js'; + +registerResource({ + name: 'dropped-message', + plural: 'dropped-messages', + table: 'unregistered_senders', + description: + 'Dropped message log — tracks messages that were dropped by the router or access gate. Aggregates by (channel_type, platform_id) with a running count. Reasons include: no_agent_wired (no wiring exists), no_agent_engaged (wiring exists but engage rules didn\'t fire), unknown_sender_strict (sender not recognized, strict policy), unknown_sender_request_approval (sender not recognized, approval requested).', + idColumn: 'channel_type', + columns: [ + { name: 'channel_type', type: 'string', description: 'Channel adapter type of the dropped message.' }, + { name: 'platform_id', type: 'string', description: 'Platform chat ID where the message was dropped.' }, + { name: 'user_id', type: 'string', description: 'Sender user ID if resolved, null otherwise.' }, + { name: 'sender_name', type: 'string', description: 'Sender display name if available.' }, + { + name: 'reason', + type: 'string', + description: 'Why the message was dropped.', + enum: ['no_agent_wired', 'no_agent_engaged', 'unknown_sender_strict', 'unknown_sender_request_approval'], + }, + { name: 'messaging_group_id', type: 'string', description: 'Messaging group ID if resolved.' }, + { name: 'agent_group_id', type: 'string', description: 'Target agent group ID if resolved.' }, + { name: 'message_count', type: 'number', description: 'Number of dropped messages from this sender on this chat.' }, + { name: 'first_seen', type: 'string', description: 'First drop timestamp.' }, + { name: 'last_seen', type: 'string', description: 'Most recent drop timestamp.' }, + ], + operations: { list: 'open' }, +}); diff --git a/src/cli/resources/index.ts b/src/cli/resources/index.ts index 42155e7..816b32f 100644 --- a/src/cli/resources/index.ts +++ b/src/cli/resources/index.ts @@ -8,4 +8,8 @@ import './wirings.js'; import './users.js'; import './roles.js'; import './members.js'; +import './destinations.js'; +import './user-dms.js'; +import './dropped-messages.js'; +import './approvals.js'; import './sessions.js'; diff --git a/src/cli/resources/messaging-groups.ts b/src/cli/resources/messaging-groups.ts index edccfc0..0cda1c8 100644 --- a/src/cli/resources/messaging-groups.ts +++ b/src/cli/resources/messaging-groups.ts @@ -12,7 +12,8 @@ registerResource({ { name: 'channel_type', type: 'string', - description: 'Channel adapter type — matches the adapter registered by /add- (e.g. telegram, discord, slack, whatsapp).', + description: + 'Channel adapter type — matches the adapter registered by /add- (e.g. telegram, discord, slack, whatsapp).', required: true, }, { @@ -44,6 +45,13 @@ registerResource({ default: 'strict', updatable: true, }, + { + name: 'denied_at', + type: 'string', + description: + 'Set when the owner explicitly denies registering this channel. While set, the router drops all messages silently without re-escalating. Cleared by any explicit wiring mutation.', + updatable: true, + }, { name: 'created_at', type: 'string', description: 'Auto-set.', generated: true }, ], operations: { list: 'open', get: 'open', create: 'approval', update: 'approval', delete: 'approval' }, diff --git a/src/cli/resources/sessions.ts b/src/cli/resources/sessions.ts index 1a3bd24..f60fccc 100644 --- a/src/cli/resources/sessions.ts +++ b/src/cli/resources/sessions.ts @@ -34,7 +34,8 @@ registerResource({ { name: 'container_status', type: 'string', - description: '"running" — container alive. "idle" — exited, restarts on next message. "stopped" — needs explicit wake.', + description: + '"running" — container alive and polling. "stopped" — container exited; the sweep will restart it automatically when due messages arrive. "idle" — reserved, currently unused.', enum: ['running', 'idle', 'stopped'], }, { name: 'last_active', type: 'string', description: 'Last message or heartbeat. Used for stale detection.' }, diff --git a/src/cli/resources/user-dms.ts b/src/cli/resources/user-dms.ts new file mode 100644 index 0000000..8b7b1cd --- /dev/null +++ b/src/cli/resources/user-dms.ts @@ -0,0 +1,21 @@ +import { registerResource } from '../crud.js'; + +registerResource({ + name: 'user-dm', + plural: 'user-dms', + table: 'user_dms', + description: + 'User DM cache — maps (user, channel_type) to the messaging group used for DM delivery. Populated lazily by ensureUserDm() when the host needs to cold-DM a user (approvals, pairing). For direct-addressable channels (Telegram, WhatsApp) the handle IS the DM chat ID. For resolution-required channels (Discord, Slack) the adapter\'s openDM resolves it.', + idColumn: 'user_id', + columns: [ + { name: 'user_id', type: 'string', description: 'User this DM route is for.' }, + { name: 'channel_type', type: 'string', description: 'Channel adapter type.' }, + { + name: 'messaging_group_id', + type: 'string', + description: 'The messaging group used to deliver DMs to this user on this channel.', + }, + { name: 'resolved_at', type: 'string', description: 'When this DM route was last resolved.' }, + ], + operations: { list: 'open' }, +}); diff --git a/src/cli/resources/wirings.ts b/src/cli/resources/wirings.ts index f04102f..d52f8b1 100644 --- a/src/cli/resources/wirings.ts +++ b/src/cli/resources/wirings.ts @@ -40,7 +40,8 @@ registerResource({ { name: 'sender_scope', type: 'string', - description: '"all" — any sender (subject to unknown_sender_policy). "known" — only users with a role or membership in this agent group.', + description: + '"all" — any sender (subject to unknown_sender_policy). "known" — only users with a role or membership in this agent group.', enum: ['all', 'known'], default: 'all', updatable: true, @@ -58,7 +59,7 @@ registerResource({ name: 'session_mode', type: 'string', description: - '"shared" — one session per (agent, messaging group). "per-thread" — separate session per thread/topic. "agent-shared" — one session across all messaging groups wired to this agent.', + '"shared" — one session per (agent, messaging group). "per-thread" — separate session per thread/topic. "agent-shared" — one session across all messaging groups wired to this agent. Note: threaded adapters in group chats force per-thread regardless of this setting.', enum: ['shared', 'per-thread', 'agent-shared'], default: 'shared', updatable: true,