openbot 0.3.6 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +10 -19
  5. package/dist/app/server.js +208 -17
  6. package/dist/bus/services.js +34 -124
  7. package/dist/harness/agent-invoke-run.js +44 -0
  8. package/dist/harness/agent-turn.js +99 -0
  9. package/dist/harness/channel-participants.js +40 -0
  10. package/dist/harness/constants.js +2 -0
  11. package/dist/harness/context-meter.js +97 -0
  12. package/dist/harness/context.js +95 -47
  13. package/dist/harness/dispatch.js +144 -0
  14. package/dist/harness/dispatcher.js +45 -156
  15. package/dist/harness/history.js +177 -0
  16. package/dist/harness/index.js +109 -0
  17. package/dist/harness/orchestration.js +88 -0
  18. package/dist/harness/participants.js +22 -0
  19. package/dist/harness/run-harness.js +154 -0
  20. package/dist/harness/run.js +98 -0
  21. package/dist/harness/runtime-factory.js +0 -34
  22. package/dist/harness/runtime.js +57 -0
  23. package/dist/harness/todo-dispatch.js +51 -0
  24. package/dist/harness/todos.js +5 -0
  25. package/dist/harness/turn.js +79 -0
  26. package/dist/plugins/approval/index.js +120 -149
  27. package/dist/plugins/bash/index.js +195 -0
  28. package/dist/plugins/delegation/index.js +121 -32
  29. package/dist/plugins/memory/index.js +103 -14
  30. package/dist/plugins/memory/service.js +152 -0
  31. package/dist/plugins/openbot/context.js +125 -0
  32. package/dist/plugins/openbot/history.js +144 -0
  33. package/dist/plugins/openbot/index.js +71 -0
  34. package/dist/plugins/openbot/runtime.js +381 -0
  35. package/dist/plugins/openbot/system-prompt.js +25 -0
  36. package/dist/plugins/plugin-manager/index.js +189 -0
  37. package/dist/plugins/shell/index.js +2 -1
  38. package/dist/plugins/storage/files.js +67 -0
  39. package/dist/plugins/storage/index.js +750 -0
  40. package/dist/plugins/storage/service.js +1316 -0
  41. package/dist/plugins/storage-tools/index.js +2 -2
  42. package/dist/plugins/thread-namer/index.js +72 -0
  43. package/dist/plugins/thread-naming/generate-title.js +44 -0
  44. package/dist/plugins/thread-naming/index.js +103 -0
  45. package/dist/plugins/threads/index.js +114 -0
  46. package/dist/plugins/todo/index.js +24 -25
  47. package/dist/plugins/ui/index.js +109 -180
  48. package/dist/registry/plugins.js +3 -9
  49. package/dist/services/abort.js +43 -0
  50. package/dist/services/plugins/domain.js +1 -0
  51. package/dist/services/plugins/plugin-cache.js +9 -0
  52. package/dist/services/plugins/registry.js +112 -0
  53. package/dist/services/plugins/service.js +232 -0
  54. package/dist/services/plugins/types.js +1 -0
  55. package/dist/services/process.js +29 -0
  56. package/dist/services/storage.js +11 -10
  57. package/dist/services/thread-naming.js +81 -0
  58. package/docs/agents.md +15 -12
  59. package/docs/architecture.md +2 -2
  60. package/docs/plugins.md +29 -17
  61. package/docs/templates/AGENT.example.md +8 -14
  62. package/package.json +1 -2
  63. package/src/app/agent-ids.ts +5 -0
  64. package/src/app/cli.ts +1 -1
  65. package/src/app/config.ts +14 -31
  66. package/src/app/server.ts +243 -19
  67. package/src/app/types.ts +331 -187
  68. package/src/harness/index.ts +166 -0
  69. package/src/plugins/approval/index.ts +107 -188
  70. package/src/plugins/bash/index.ts +232 -0
  71. package/src/plugins/delegation/index.ts +139 -39
  72. package/src/plugins/memory/index.ts +112 -15
  73. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  74. package/src/plugins/openbot/context.ts +140 -0
  75. package/src/plugins/openbot/history.ts +158 -0
  76. package/src/plugins/openbot/index.ts +79 -0
  77. package/src/plugins/openbot/runtime.ts +478 -0
  78. package/src/plugins/openbot/system-prompt.ts +27 -0
  79. package/src/plugins/plugin-manager/index.ts +224 -0
  80. package/src/plugins/storage/files.ts +81 -0
  81. package/src/plugins/storage/index.ts +823 -0
  82. package/src/{services/storage.ts → plugins/storage/service.ts} +485 -105
  83. package/src/plugins/ui/index.ts +117 -221
  84. package/src/services/abort.ts +46 -0
  85. package/src/{bus/types.ts → services/plugins/domain.ts} +50 -8
  86. package/src/services/plugins/plugin-cache.ts +13 -0
  87. package/src/{registry/plugins.ts → services/plugins/registry.ts} +28 -28
  88. package/src/services/plugins/service.ts +318 -0
  89. package/src/{bus/plugin.ts → services/plugins/types.ts} +7 -3
  90. package/src/bus/services.ts +0 -954
  91. package/src/harness/context.ts +0 -365
  92. package/src/harness/dispatcher.ts +0 -379
  93. package/src/harness/mcp.ts +0 -78
  94. package/src/harness/runtime-factory.ts +0 -129
  95. package/src/harness/todo-advance.ts +0 -128
  96. package/src/plugins/ai-sdk/index.ts +0 -41
  97. package/src/plugins/ai-sdk/runtime.ts +0 -468
  98. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  99. package/src/plugins/mcp/index.ts +0 -128
  100. package/src/plugins/shell/index.ts +0 -123
  101. package/src/plugins/storage-tools/index.ts +0 -90
  102. package/src/plugins/todo/index.ts +0 -64
  103. package/src/services/plugins.ts +0 -133
  104. /package/src/{harness → services}/process.ts +0 -0
@@ -0,0 +1,166 @@
1
+ import { melony } from 'melony';
2
+ import { OpenBotEvent, OpenBotState } from '../app/types.js';
3
+ import { ensureEventId } from '../app/utils.js';
4
+ import { storageService } from '../plugins/storage/service.js';
5
+ import { STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID } from '../app/agent-ids.js';
6
+ import { resolvePlugin } from '../services/plugins/registry.js';
7
+ import { ToolDefinition } from '../services/plugins/types.js';
8
+ import { abortRegistry, abortKey } from '../services/abort.js';
9
+ import { loadConfig } from '../app/config.js';
10
+ import { getPublicBaseUrl } from '../plugins/storage/files.js';
11
+
12
+ export { STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID };
13
+
14
+ export interface RunAgentOptions {
15
+ runId: string;
16
+ agentId: string;
17
+ event: OpenBotEvent;
18
+ channelId: string;
19
+ threadId?: string;
20
+ persistEvents?: boolean;
21
+ /** Resolved public base URL for the server. */
22
+ publicBaseUrl?: string;
23
+ onEvent: (event: OpenBotEvent, state?: OpenBotState) => Promise<void>;
24
+ }
25
+
26
+ async function emitEvent(
27
+ chunk: OpenBotEvent,
28
+ state: OpenBotState | undefined,
29
+ {
30
+ persistEvents,
31
+ channelId,
32
+ threadId,
33
+ onEvent,
34
+ parentAgentId,
35
+ parentToolCallId,
36
+ }: {
37
+ persistEvents: boolean;
38
+ channelId: string;
39
+ threadId?: string;
40
+ onEvent: RunAgentOptions['onEvent'];
41
+ parentAgentId?: string;
42
+ parentToolCallId?: string;
43
+ },
44
+ ): Promise<void> {
45
+ ensureEventId(chunk);
46
+
47
+ // Enrich event with parent metadata if not already present
48
+ if (parentAgentId || parentToolCallId) {
49
+ chunk.meta = {
50
+ ...chunk.meta,
51
+ parentAgentId: chunk.meta?.parentAgentId || parentAgentId,
52
+ parentToolCallId: chunk.meta?.parentToolCallId || parentToolCallId,
53
+ };
54
+ }
55
+
56
+ if (persistEvents) {
57
+ await storageService.storeEvent({
58
+ channelId: state?.channelId || channelId,
59
+ threadId: state?.threadId || threadId,
60
+ event: chunk,
61
+ });
62
+ }
63
+
64
+ await onEvent(chunk, state);
65
+ }
66
+
67
+ /**
68
+ * Runs a single agent turn.
69
+ * Fire and forget.
70
+ */
71
+ export async function runAgent(options: RunAgentOptions): Promise<void> {
72
+ const { runId, agentId, event, channelId, threadId, onEvent } = options;
73
+ const persistEvents = options.persistEvents !== false;
74
+
75
+ let publicBaseUrl = options.publicBaseUrl;
76
+ if (!publicBaseUrl) {
77
+ const config = loadConfig();
78
+ const port = Number(config.port ?? process.env.PORT ?? 4132);
79
+ publicBaseUrl = getPublicBaseUrl(port, config.publicUrl);
80
+ }
81
+
82
+ const parentAgentId = event.meta?.parentAgentId;
83
+ const parentToolCallId = event.meta?.parentToolCallId;
84
+
85
+ const agentDetails = await storageService.getAgentDetails({ agentId });
86
+
87
+ const state = await storageService.getOpenBotState({
88
+ runId,
89
+ agentId,
90
+ channelId,
91
+ threadId,
92
+ event,
93
+ });
94
+
95
+ // Shared per-thread abort signal so a stop request cancels this run and any
96
+ // delegated sub-agent runs (which execute in the same channel/thread).
97
+ const runKey = abortKey(channelId, threadId);
98
+ const abortSignal = abortRegistry.acquire(runKey);
99
+
100
+ await emitEvent(
101
+ {
102
+ type: 'agent:run:start',
103
+ data: { runId, agentId, channelId, threadId },
104
+ } as OpenBotEvent,
105
+ state,
106
+ { persistEvents, channelId, threadId, onEvent, parentAgentId, parentToolCallId },
107
+ );
108
+
109
+ try {
110
+ const pluginRefs = agentDetails.pluginRefs ?? [];
111
+ const tools: Record<string, ToolDefinition> = {};
112
+
113
+ for (const ref of pluginRefs) {
114
+ const plugin = await resolvePlugin(ref.id);
115
+ if (plugin?.toolDefinitions) {
116
+ Object.assign(tools, plugin.toolDefinitions);
117
+ }
118
+ }
119
+
120
+ const builder = melony<OpenBotState, OpenBotEvent>().initialState(state);
121
+
122
+ for (const ref of pluginRefs) {
123
+ const plugin = await resolvePlugin(ref.id);
124
+ if (!plugin) continue;
125
+
126
+ builder.use(
127
+ plugin.factory({
128
+ agentId,
129
+ agentDetails,
130
+ config: ref.config ?? {},
131
+ storage: storageService,
132
+ tools,
133
+ publicBaseUrl,
134
+ abortSignal,
135
+ }),
136
+ );
137
+ }
138
+
139
+ const runtime = builder.build();
140
+ const generator = runtime.run(event, { runId, state });
141
+
142
+ for await (const outputEvent of generator) {
143
+ if (abortSignal.aborted) break;
144
+ await emitEvent(outputEvent, state, {
145
+ persistEvents,
146
+ channelId,
147
+ threadId,
148
+ onEvent,
149
+ parentAgentId,
150
+ parentToolCallId,
151
+ });
152
+ }
153
+ } catch (error) {
154
+ console.error(`[harness] Error running agent ${agentId}:`, error);
155
+ } finally {
156
+ abortRegistry.release(runKey);
157
+ await emitEvent(
158
+ {
159
+ type: 'agent:run:end',
160
+ data: { runId, agentId, channelId, threadId },
161
+ } as OpenBotEvent,
162
+ state,
163
+ { persistEvents, channelId, threadId, onEvent, parentAgentId, parentToolCallId },
164
+ );
165
+ }
166
+ }
@@ -1,228 +1,147 @@
1
- import { MelonyPlugin } from 'melony';
2
- import type { Plugin } from '../../bus/plugin.js';
3
- import { OpenBotEvent, OpenBotState } from '../../app/types.js';
4
- import { storageService } from '../../services/storage.js';
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { Plugin } from '../../services/plugins/types.js';
3
+ import { OpenBotEvent } from '../../app/types.js';
5
4
 
6
5
  /**
7
6
  * `approval` — gates protected tool calls behind a UI confirmation widget.
8
- *
9
- * Configuration is read from the per-agent plugin config in AGENT.md:
10
- * ```yaml
11
- * plugins:
12
- * - id: approval
13
- * config:
14
- * rules:
15
- * - action: action:shell_exec
16
- * message: The agent wants to run a terminal command.
17
- * detailKeys: [command, cwd, shell, timeoutMs]
18
- * ```
7
+ *
8
+ * This is a simplified version that intercepts specified actions (default: bash)
9
+ * and requires user approval before they are allowed to proceed.
19
10
  */
20
11
 
21
- export type ApprovalRule = {
22
- action: string;
23
- message?: string;
24
- detailKeys?: string[];
25
- hiddenKeys?: string[];
26
- executeEvent?: string;
27
- denyEvent?: string;
28
- denyData?: Record<string, unknown>;
29
- };
12
+ // In-memory tracking for pending approval IDs with TTL (shared across plugin instances)
13
+ const pendingApprovals = new Map<string, number>();
14
+ const TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
30
15
 
31
- export const DEFAULT_APPROVAL_RULES: ApprovalRule[] = [
32
- {
33
- action: 'action:shell_exec',
34
- denyEvent: 'action:shell_exec:result',
35
- message: 'The agent wants to run a terminal command.',
36
- detailKeys: ['command', 'cwd', 'shell', 'timeoutMs'],
37
- hiddenKeys: ['env'],
38
- denyData: {
39
- exitCode: null,
40
- stdout: '',
41
- stderr: 'Command execution was denied by the user.',
42
- timedOut: false,
43
- },
44
- },
45
- ];
46
-
47
- type PendingApproval = {
48
- id: string;
49
- action: string;
50
- executeEvent: string;
51
- denyEvent: string;
52
- denyData: Record<string, unknown>;
53
- payload: Record<string, unknown>;
54
- meta?: Record<string, unknown>;
55
- message: string;
56
- createdAt: string;
57
- status: 'pending' | 'approved' | 'denied';
58
- };
16
+ export const approvalPlugin: Plugin = {
17
+ id: 'approval',
18
+ name: 'Approval',
19
+ description: 'Gate protected tool calls behind a UI confirmation widget.',
20
+ factory: ({ config, storage }) => (builder) => {
21
+ // Actions that require approval. Defaults to bash.
22
+ const actionsToApprove = (config.actions as string[]) || ['action:bash'];
59
23
 
60
- const asRecord = (value: unknown): Record<string, unknown> =>
61
- value && typeof value === 'object' && !Array.isArray(value)
62
- ? (value as Record<string, unknown>)
63
- : {};
24
+ for (const action of actionsToApprove) {
25
+ builder.intercept(action as OpenBotEvent['type'], (event, context) => {
26
+ // If already approved in this flow, let it pass to the actual handler
27
+ if (event.meta?.approvalStatus === 'approved') return event;
64
28
 
65
- const getApprovalsFromState = (state: OpenBotState): Record<string, PendingApproval> => {
66
- const source = state.threadDetails?.state ?? state.channelDetails?.state;
67
- const stateRecord = asRecord(source);
68
- return asRecord(stateRecord.approvals) as Record<string, PendingApproval>;
69
- };
29
+ // Otherwise, intercept and ask for approval via a UI widget
30
+ const displayData = JSON.stringify((event as any)?.data) || '';
70
31
 
71
- const persistApprovals = async (
72
- state: OpenBotState,
73
- approvals: Record<string, PendingApproval>,
74
- ): Promise<void> => {
75
- if (state.threadId) {
76
- await storageService.patchThreadState({
77
- channelId: state.channelId,
78
- threadId: state.threadId,
79
- state: { approvals },
80
- });
81
- return;
82
- }
83
- await storageService.patchChannelState({
84
- channelId: state.channelId,
85
- state: { approvals },
86
- });
87
- };
88
-
89
- const buildApprovalPlugin =
90
- (rules: ApprovalRule[]): MelonyPlugin<OpenBotState, OpenBotEvent> =>
91
- (builder) => {
92
- for (const rule of rules) {
93
- builder.on(rule.action as OpenBotEvent['type'], async function* (event, context) {
94
- const meta = asRecord(event.meta);
95
- if (meta.approvalStatus === 'approved') return;
96
-
97
- const eventData = asRecord((event as { data?: unknown }).data);
98
- const eventMeta = meta;
99
-
100
- const approvalId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
101
- const widgetId = `widget_${approvalId}`;
102
- const executeEvent = rule.executeEvent || rule.action;
103
- const denyEvent = rule.denyEvent || `${rule.action}:result`;
104
- const denyData = rule.denyData || {};
105
- const hiddenKeys = new Set(rule.hiddenKeys || []);
106
- const detailKeys = rule.detailKeys || Object.keys(eventData);
107
- const details = detailKeys
108
- .filter((key) => !hiddenKeys.has(key))
109
- .map((key) => `- ${key}: ${String(eventData[key] ?? '')}`)
110
- .join('\n');
111
-
112
- const pendingApprovals = getApprovalsFromState(context.state);
113
- pendingApprovals[approvalId] = {
114
- id: approvalId,
115
- action: rule.action,
116
- executeEvent,
117
- denyEvent,
118
- denyData,
119
- payload: eventData,
120
- meta: eventMeta,
121
- message: rule.message || `Approval required for ${rule.action}.`,
122
- createdAt: new Date().toISOString(),
123
- status: 'pending',
124
- };
125
- await persistApprovals(context.state, pendingApprovals);
32
+ const widgetId = randomUUID();
33
+ pendingApprovals.set(widgetId, Date.now());
126
34
 
127
- yield {
35
+ return {
128
36
  type: 'client:ui:widget',
129
37
  data: {
130
- kind: 'choice',
131
38
  widgetId,
132
- title: 'Approval Required',
133
- body: `${rule.message || 'A protected action requires approval.'}${
134
- details ? `\n\n${details}` : ''
135
- }`,
136
- metadata: { type: 'approval:request', approvalId, action: rule.action },
39
+ kind: 'message',
40
+ title: `The agent wants to perform \`${action}\``,
41
+ body: displayData,
42
+ metadata: {
43
+ type: 'approval:request',
44
+ originalEvent: event,
45
+ },
137
46
  actions: [
138
47
  { id: 'approve', label: 'Approve', variant: 'primary' },
139
48
  { id: 'deny', label: 'Deny', variant: 'danger' },
140
49
  ],
141
50
  },
142
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
143
- } as OpenBotEvent;
144
-
145
- yield {
146
- type: 'agent:output',
147
- data: { content: `Waiting for approval before running \`${rule.action}\`.` },
148
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
51
+ meta: { agentId: context.state.agentId, threadId: context.state.threadId },
149
52
  } as OpenBotEvent;
150
-
151
- context.suspend();
152
53
  });
153
54
  }
154
55
 
56
+ // Handle the user's response from the UI widget
155
57
  builder.on('client:ui:widget:response', async function* (event, context) {
156
- const metadata = asRecord(event.data?.metadata);
157
- if (metadata.type !== 'approval:request') return;
58
+ const { widgetId, actionId } = event.data;
59
+ const metadata = event.data?.metadata;
60
+ if (metadata?.type !== 'approval:request') return;
158
61
 
159
- const approvalId = String(metadata.approvalId || '');
160
- if (!approvalId) return;
62
+ // Verify the widget is still pending and hasn't expired
63
+ if (!widgetId || !pendingApprovals.has(widgetId)) {
64
+ console.warn(`[approval] Received response for unknown or already handled widget: ${widgetId}`);
65
+ return;
66
+ }
161
67
 
162
- const approvals = getApprovalsFromState(context.state);
163
- const approval = approvals[approvalId];
164
- if (!approval || approval.status !== 'pending') {
165
- yield {
166
- type: 'agent:output',
167
- data: { content: 'Approval request not found or already resolved.' },
168
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
169
- } as OpenBotEvent;
68
+ const timestamp = pendingApprovals.get(widgetId)!;
69
+ if (Date.now() - timestamp > TTL_MS) {
70
+ pendingApprovals.delete(widgetId);
71
+ console.warn(`[approval] Received response for expired widget: ${widgetId}`);
170
72
  return;
171
73
  }
172
74
 
173
- const approved = event.data.actionId === 'approve';
174
- approvals[approvalId] = {
175
- ...approval,
176
- status: approved ? 'approved' : 'denied',
177
- };
178
- await persistApprovals(context.state, approvals);
75
+ // Mark as handled
76
+ pendingApprovals.delete(widgetId);
77
+
78
+ const originalEvent = metadata.originalEvent as OpenBotEvent;
79
+ const approved = actionId === 'approve';
80
+
81
+ const displayData = JSON.stringify((event as any)?.data) || '';
82
+
83
+ // Yield a "responded" widget update to the UI
84
+ yield {
85
+ type: 'client:ui:widget',
86
+ data: {
87
+ widgetId,
88
+ kind: 'message',
89
+ title: `Action ${approved ? 'Approved' : 'Denied'}`,
90
+ body: displayData,
91
+ state: approved ? 'submitted' : 'cancelled',
92
+ display: 'collapsed',
93
+ disabled: true,
94
+ actions: [], // Clear actions to disable buttons in UI
95
+ },
96
+ meta: { agentId: context.state.agentId, threadId: context.state.threadId },
97
+ } as OpenBotEvent;
179
98
 
180
99
  if (approved) {
100
+ // Re-emit the original event with approved status so the actual handler can run
181
101
  yield {
182
- type: approval.executeEvent as OpenBotEvent['type'],
183
- data: approval.payload,
102
+ ...originalEvent,
184
103
  meta: {
185
- ...(approval.meta || {}),
186
- approvalId,
104
+ ...(originalEvent.meta || {}),
187
105
  approvalStatus: 'approved',
188
106
  },
107
+ };
108
+ } else {
109
+ // Manually store the original event with denied status so it's recorded in history
110
+ // but NOT re-emitted to the pipeline (to avoid actual execution).
111
+ if (storage) {
112
+ await storage.storeEvent({
113
+ channelId: context.state.channelId,
114
+ threadId: context.state.threadId,
115
+ event: {
116
+ ...originalEvent,
117
+ meta: {
118
+ ...(originalEvent.meta || {}),
119
+ approvalStatus: 'denied',
120
+ },
121
+ },
122
+ });
123
+ }
124
+
125
+ // Emit a failure result event for the denied action to clear the pending tool batch
126
+ yield {
127
+ type: `${originalEvent.type}:result` as OpenBotEvent['type'],
128
+ data: {
129
+ success: false,
130
+ error: 'Action denied by user.',
131
+ stderr: 'Action denied by user.',
132
+ output: 'Action denied by user.',
133
+ },
134
+ meta: originalEvent.meta,
189
135
  } as OpenBotEvent;
190
- return;
191
- }
192
-
193
- yield {
194
- type: approval.denyEvent as OpenBotEvent['type'],
195
- data: {
196
- success: false,
197
- approved: false,
198
- error: 'Action denied by user approval.',
199
- ...approval.denyData,
200
- },
201
- meta: { ...(approval.meta || {}), approvalId },
202
- } as OpenBotEvent;
203
136
 
204
- yield {
205
- type: 'agent:output',
206
- data: { content: 'Action denied by user approval.' },
207
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
208
- } as OpenBotEvent;
137
+ yield {
138
+ type: 'agent:output',
139
+ data: { content: `Action \`${originalEvent.type}\` was denied.` },
140
+ meta: { agentId: context.state.agentId },
141
+ } as OpenBotEvent;
142
+ }
209
143
  });
210
- };
211
-
212
- const readRules = (config: Record<string, unknown>): ApprovalRule[] => {
213
- const raw = config.rules;
214
- if (!Array.isArray(raw)) return DEFAULT_APPROVAL_RULES;
215
- return raw.filter(
216
- (entry): entry is ApprovalRule =>
217
- !!entry && typeof entry === 'object' && typeof (entry as { action?: unknown }).action === 'string',
218
- );
219
- };
220
-
221
- export const approvalPlugin: Plugin = {
222
- id: 'approval',
223
- name: 'Approval',
224
- description: 'Gate protected tool calls (e.g. shell_exec) behind a UI confirmation prompt.',
225
- factory: ({ config }) => buildApprovalPlugin(readRules(config)),
144
+ },
226
145
  };
227
146
 
228
147
  export default approvalPlugin;