openbot 0.4.7 → 0.5.4

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 (130) hide show
  1. package/.claude/agents/runtime-security-reviewer.md +32 -0
  2. package/.claude/hooks/format.sh +18 -0
  3. package/.claude/hooks/protect-secrets.sh +23 -0
  4. package/.claude/settings.json +26 -0
  5. package/.claude/skills/deploy/SKILL.md +26 -0
  6. package/.claude/skills/new-plugin/SKILL.md +46 -0
  7. package/.dockerignore +9 -0
  8. package/.mcp.json +8 -0
  9. package/AGENTS.md +2 -2
  10. package/CLAUDE.md +50 -0
  11. package/Dockerfile +41 -0
  12. package/deploy/README.md +61 -0
  13. package/deploy/entrypoint.sh +6 -0
  14. package/deploy/fly.toml +36 -0
  15. package/dist/app/bootstrap.js +29 -0
  16. package/dist/app/cli.js +3 -1
  17. package/dist/app/cloud-mode.js +26 -0
  18. package/dist/app/config.js +61 -16
  19. package/dist/app/server.js +38 -6
  20. package/dist/plugins/approval/index.js +1 -1
  21. package/dist/plugins/bash/index.js +386 -164
  22. package/dist/plugins/memory/service.js +2 -5
  23. package/dist/plugins/openbot/context.js +17 -1
  24. package/dist/plugins/openbot/index.js +23 -8
  25. package/dist/plugins/openbot/model.js +59 -0
  26. package/dist/plugins/openbot/runtime.js +39 -56
  27. package/dist/plugins/openbot/system-prompt.js +43 -8
  28. package/dist/plugins/preview/index.js +267 -0
  29. package/dist/plugins/storage/index.js +3 -3
  30. package/dist/plugins/storage/service.js +58 -12
  31. package/dist/plugins/todo/index.js +131 -38
  32. package/dist/plugins/todo/service.js +93 -0
  33. package/dist/plugins/ui/index.js +8 -1
  34. package/dist/services/plugins/model-registry.js +101 -0
  35. package/dist/services/plugins/registry.js +6 -4
  36. package/dist/services/plugins/service.js +2 -6
  37. package/docs/agents.md +20 -2
  38. package/docs/architecture.md +1 -1
  39. package/docs/plugins.md +13 -3
  40. package/docs/templates/AGENT.example.md +1 -1
  41. package/package.json +5 -2
  42. package/pnpm-workspace.yaml +2 -0
  43. package/src/app/bootstrap.ts +43 -0
  44. package/src/app/cli.ts +3 -1
  45. package/src/app/cloud-mode.ts +41 -0
  46. package/src/app/config.ts +71 -19
  47. package/src/app/server.ts +45 -6
  48. package/src/app/types.ts +162 -31
  49. package/src/plugins/approval/index.ts +1 -1
  50. package/src/plugins/bash/index.ts +498 -185
  51. package/src/plugins/memory/service.ts +2 -5
  52. package/src/plugins/openbot/context.ts +17 -1
  53. package/src/plugins/openbot/index.ts +25 -10
  54. package/src/plugins/openbot/model.ts +76 -0
  55. package/src/plugins/openbot/runtime.ts +47 -69
  56. package/src/plugins/openbot/system-prompt.ts +43 -8
  57. package/src/plugins/preview/index.ts +323 -0
  58. package/src/plugins/storage/index.ts +5 -8
  59. package/src/plugins/storage/service.ts +79 -15
  60. package/src/plugins/todo/index.ts +166 -0
  61. package/src/plugins/todo/service.ts +123 -0
  62. package/src/plugins/ui/index.ts +8 -1
  63. package/src/services/plugins/domain.ts +2 -0
  64. package/src/services/plugins/model-registry.ts +146 -0
  65. package/src/services/plugins/registry.ts +6 -4
  66. package/src/services/plugins/service.ts +2 -6
  67. package/dist/agents/openbot/index.js +0 -76
  68. package/dist/agents/openbot/middleware/approval.js +0 -132
  69. package/dist/agents/openbot/runtime.js +0 -289
  70. package/dist/agents/openbot/system-prompt.js +0 -32
  71. package/dist/agents/openbot/tools/delegation.js +0 -78
  72. package/dist/agents/openbot/tools/mcp.js +0 -99
  73. package/dist/agents/openbot/tools/shell.js +0 -91
  74. package/dist/agents/openbot/tools/storage.js +0 -75
  75. package/dist/agents/openbot/tools/ui.js +0 -176
  76. package/dist/agents/system.js +0 -33
  77. package/dist/bus/agent-package.js +0 -1
  78. package/dist/bus/plugin.js +0 -1
  79. package/dist/bus/services.js +0 -754
  80. package/dist/bus/types.js +0 -1
  81. package/dist/harness/agent-harness.js +0 -45
  82. package/dist/harness/agent-invoke-run.js +0 -44
  83. package/dist/harness/agent-turn.js +0 -99
  84. package/dist/harness/channel-participants.js +0 -40
  85. package/dist/harness/constants.js +0 -2
  86. package/dist/harness/context-meter.js +0 -97
  87. package/dist/harness/context.js +0 -363
  88. package/dist/harness/dispatch.js +0 -144
  89. package/dist/harness/dispatcher.js +0 -156
  90. package/dist/harness/event-normalizer.js +0 -59
  91. package/dist/harness/history.js +0 -177
  92. package/dist/harness/mcp.js +0 -61
  93. package/dist/harness/orchestration.js +0 -88
  94. package/dist/harness/orchestrator.js +0 -149
  95. package/dist/harness/participants.js +0 -22
  96. package/dist/harness/process.js +0 -29
  97. package/dist/harness/queue-processor.js +0 -187
  98. package/dist/harness/run-harness.js +0 -154
  99. package/dist/harness/run.js +0 -98
  100. package/dist/harness/runtime-factory.js +0 -73
  101. package/dist/harness/runtime.js +0 -57
  102. package/dist/harness/todo-advance.js +0 -93
  103. package/dist/harness/todo-dispatch.js +0 -51
  104. package/dist/harness/todos.js +0 -5
  105. package/dist/harness/turn.js +0 -79
  106. package/dist/harness/types.js +0 -1
  107. package/dist/plugins/ai-sdk/index.js +0 -34
  108. package/dist/plugins/ai-sdk/runtime.js +0 -395
  109. package/dist/plugins/ai-sdk/system-prompt.js +0 -18
  110. package/dist/plugins/ai-sdk.js +0 -331
  111. package/dist/plugins/approval.js +0 -163
  112. package/dist/plugins/delegation.js +0 -108
  113. package/dist/plugins/mcp/index.js +0 -108
  114. package/dist/plugins/mcp.js +0 -140
  115. package/dist/plugins/shell/index.js +0 -100
  116. package/dist/plugins/shell.js +0 -123
  117. package/dist/plugins/storage-tools/index.js +0 -76
  118. package/dist/plugins/storage.js +0 -737
  119. package/dist/plugins/thread-namer/index.js +0 -72
  120. package/dist/plugins/threads/index.js +0 -114
  121. package/dist/plugins/ui.js +0 -211
  122. package/dist/plugins/workflow/index.js +0 -65
  123. package/dist/registry/agents.js +0 -138
  124. package/dist/registry/plugins.js +0 -110
  125. package/dist/services/agent-packages.js +0 -103
  126. package/dist/services/memory.js +0 -152
  127. package/dist/services/plugins.js +0 -98
  128. package/dist/services/storage.js +0 -1028
  129. package/dist/workflow/service.js +0 -106
  130. package/dist/workflow/types.js +0 -3
@@ -0,0 +1,123 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { DEFAULT_CHANNELS_DIR, getBaseDir, resolvePath } from '../../app/config.js';
4
+
5
+ export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
6
+
7
+ export type TodoItem = {
8
+ id: string;
9
+ content: string;
10
+ status: TodoStatus;
11
+ };
12
+
13
+ export type TodoList = {
14
+ items: TodoItem[];
15
+ updatedAt: string;
16
+ };
17
+
18
+ export const MAX_TODO_ITEMS = 20;
19
+
20
+ const TODO_STATUSES: ReadonlySet<string> = new Set([
21
+ 'pending',
22
+ 'in_progress',
23
+ 'completed',
24
+ 'cancelled',
25
+ ]);
26
+
27
+ const getThreadDir = (channelId: string, threadId: string): string => {
28
+ const base = resolvePath(path.join(getBaseDir(), DEFAULT_CHANNELS_DIR, channelId));
29
+ return path.join(base, 'threads', threadId);
30
+ };
31
+
32
+ const getTodosPath = (channelId: string, threadId: string): string =>
33
+ path.join(getThreadDir(channelId, threadId), 'todos.json');
34
+
35
+ const emptyList = (): TodoList => ({
36
+ items: [],
37
+ updatedAt: new Date().toISOString(),
38
+ });
39
+
40
+ /**
41
+ * Validate and normalize a todo list write. Throws on invalid input.
42
+ * Enforces: non-empty content, known statuses, unique ids, max size,
43
+ * and at most one `in_progress` item.
44
+ */
45
+ export function validateTodoItems(items: TodoItem[]): TodoItem[] {
46
+ if (!Array.isArray(items)) {
47
+ throw new Error('todos must be an array');
48
+ }
49
+ if (items.length > MAX_TODO_ITEMS) {
50
+ throw new Error(`At most ${MAX_TODO_ITEMS} todos allowed`);
51
+ }
52
+
53
+ const seen = new Set<string>();
54
+ let inProgressCount = 0;
55
+ const normalized: TodoItem[] = [];
56
+
57
+ for (const raw of items) {
58
+ if (!raw || typeof raw !== 'object') {
59
+ throw new Error('Each todo must be an object');
60
+ }
61
+ const id = typeof raw.id === 'string' ? raw.id.trim() : '';
62
+ const content = typeof raw.content === 'string' ? raw.content.trim() : '';
63
+ const status = raw.status;
64
+
65
+ if (!id) throw new Error('Each todo requires a non-empty id');
66
+ if (!content) throw new Error(`Todo "${id}" requires non-empty content`);
67
+ if (!TODO_STATUSES.has(status)) {
68
+ throw new Error(
69
+ `Todo "${id}" has invalid status "${String(status)}"; expected pending|in_progress|completed|cancelled`,
70
+ );
71
+ }
72
+ if (seen.has(id)) throw new Error(`Duplicate todo id "${id}"`);
73
+ seen.add(id);
74
+
75
+ if (status === 'in_progress') inProgressCount += 1;
76
+
77
+ normalized.push({ id, content, status: status as TodoStatus });
78
+ }
79
+
80
+ if (inProgressCount > 1) {
81
+ throw new Error('At most one todo may be in_progress');
82
+ }
83
+
84
+ return normalized;
85
+ }
86
+
87
+ export const todoService = {
88
+ getTodos: async (args: { channelId: string; threadId: string }): Promise<TodoList> => {
89
+ const filePath = getTodosPath(args.channelId, args.threadId);
90
+ try {
91
+ const raw = await fs.readFile(filePath, 'utf-8');
92
+ const parsed = JSON.parse(raw) as Partial<TodoList>;
93
+ const items = Array.isArray(parsed.items) ? (parsed.items as TodoItem[]) : [];
94
+ return {
95
+ items,
96
+ updatedAt:
97
+ typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
98
+ };
99
+ } catch (e: unknown) {
100
+ if ((e as { code?: string })?.code === 'ENOENT') return emptyList();
101
+ throw e;
102
+ }
103
+ },
104
+
105
+ writeTodos: async (args: {
106
+ channelId: string;
107
+ threadId: string;
108
+ items: TodoItem[];
109
+ }): Promise<TodoList> => {
110
+ const items = validateTodoItems(args.items);
111
+ const list: TodoList = {
112
+ items,
113
+ updatedAt: new Date().toISOString(),
114
+ };
115
+ const threadDir = getThreadDir(args.channelId, args.threadId);
116
+ await fs.mkdir(threadDir, { recursive: true });
117
+ const filePath = getTodosPath(args.channelId, args.threadId);
118
+ const tmp = `${filePath}.tmp`;
119
+ await fs.writeFile(tmp, `${JSON.stringify(list, null, 2)}\n`, 'utf-8');
120
+ await fs.rename(tmp, filePath);
121
+ return list;
122
+ },
123
+ };
@@ -40,7 +40,14 @@ export const uiPlugin: Plugin = {
40
40
  id: z.string(),
41
41
  label: z.string(),
42
42
  description: z.string().optional(),
43
- status: z.enum(['pending', 'in_progress', 'done', 'error', 'cancelled']).optional(),
43
+ status: z
44
+ .string()
45
+ .optional()
46
+ .describe('Status label shown on the item (e.g. "Pending", "Shipped").'),
47
+ statusVariant: z
48
+ .enum(['default', 'success', 'warning', 'danger', 'info'])
49
+ .optional()
50
+ .describe('Semantic hint for status badge coloring in the client.'),
44
51
  metadata: z.record(z.string(), z.any()).optional()
45
52
  })).optional().describe('Required for kind="list". List of items to display.'),
46
53
  submitLabel: z.string().optional().describe('Label for the primary action button (e.g. "Submit", "Save").')
@@ -49,6 +49,8 @@ export type ConfigSchema = {
49
49
  description?: string;
50
50
  default?: unknown;
51
51
  enum?: unknown[];
52
+ /** Labeled select options (preferred over bare `enum` when present). */
53
+ options?: Array<{ label: string; value: string; description?: string }>;
52
54
  minimum?: number;
53
55
  maximum?: number;
54
56
  format?: 'password' | 'url' | 'email';
@@ -0,0 +1,146 @@
1
+ import {
2
+ DEFAULT_MARKETPLACE_REGISTRY_URL,
3
+ loadConfig,
4
+ } from '../../app/config.js';
5
+ import { isCloudMode } from '../../app/cloud-mode.js';
6
+ import type { ConfigSchema, PluginDescriptor } from './domain.js';
7
+
8
+ export type RegistryProviderCatalog = Record<
9
+ string,
10
+ {
11
+ label: string;
12
+ models: Array<{ id: string; label: string; description: string }>;
13
+ }
14
+ >;
15
+
16
+ export type ModelRegistry = {
17
+ providers?: RegistryProviderCatalog;
18
+ };
19
+
20
+ export type RegistryModelOption = {
21
+ value: string;
22
+ label: string;
23
+ description?: string;
24
+ provider: string;
25
+ };
26
+
27
+ let cachedRegistry: ModelRegistry | null = null;
28
+ let cacheUrl: string | null = null;
29
+
30
+ function getRegistryUrl(): string {
31
+ const { marketplaceRegistryUrl } = loadConfig();
32
+ return marketplaceRegistryUrl?.trim() || DEFAULT_MARKETPLACE_REGISTRY_URL;
33
+ }
34
+
35
+ export async function resolveModelRegistry(): Promise<ModelRegistry> {
36
+ const url = getRegistryUrl();
37
+ if (cachedRegistry && cacheUrl === url) return cachedRegistry;
38
+
39
+ try {
40
+ const res = await fetch(url, {
41
+ headers: { Accept: 'application/json' },
42
+ signal: AbortSignal.timeout(15_000),
43
+ });
44
+ if (!res.ok) {
45
+ throw new Error(`Registry HTTP ${res.status} ${res.statusText}`);
46
+ }
47
+ cachedRegistry = (await res.json()) as ModelRegistry;
48
+ cacheUrl = url;
49
+ return cachedRegistry;
50
+ } catch (err) {
51
+ console.warn(
52
+ '[model-registry] fetch failed:',
53
+ err instanceof Error ? err.message : err,
54
+ );
55
+ return { providers: {} };
56
+ }
57
+ }
58
+
59
+ export function listModelOptionsFromRegistry(registry: ModelRegistry): RegistryModelOption[] {
60
+ const providers = registry.providers ?? {};
61
+ const out: RegistryModelOption[] = [];
62
+
63
+ for (const [providerId, provider] of Object.entries(providers)) {
64
+ for (const model of provider.models ?? []) {
65
+ out.push({
66
+ value: `${providerId}/${model.id}`,
67
+ label: `${provider.label} — ${model.label}`,
68
+ description: model.description,
69
+ provider: providerId,
70
+ });
71
+ }
72
+ }
73
+
74
+ return out;
75
+ }
76
+
77
+ export async function listRegistryModelOptions(): Promise<RegistryModelOption[]> {
78
+ const registry = await resolveModelRegistry();
79
+ return listModelOptionsFromRegistry(registry);
80
+ }
81
+
82
+ export function listApiKeyProvidersFromRegistry(
83
+ registry: ModelRegistry,
84
+ ): Array<{ id: string; label: string }> {
85
+ const providers = registry.providers ?? {};
86
+ return Object.entries(providers).map(([id, provider]) => ({
87
+ id,
88
+ label: provider.label,
89
+ }));
90
+ }
91
+
92
+ function pickDefaultModelValue(options: RegistryModelOption[]): string | undefined {
93
+ if (options.length === 0) return undefined;
94
+ const values = options.map((option) => option.value);
95
+ const preferred = isCloudMode()
96
+ ? values.find((value) => value.startsWith('openbot/'))
97
+ : values.find((value) => value.startsWith('openai/'));
98
+ return preferred ?? values[0];
99
+ }
100
+
101
+ export function enrichOpenbotPluginDescriptor(
102
+ descriptor: PluginDescriptor,
103
+ modelOptions: RegistryModelOption[],
104
+ ): PluginDescriptor {
105
+ if (descriptor.id !== 'openbot' || !descriptor.configSchema) return descriptor;
106
+
107
+ const modelProperty = descriptor.configSchema.properties.model;
108
+ if (!modelProperty) return descriptor;
109
+
110
+ const values = modelOptions.map((option) => option.value);
111
+ const staticDefault =
112
+ typeof modelProperty.default === 'string' ? modelProperty.default : undefined;
113
+ const defaultModel =
114
+ staticDefault && values.includes(staticDefault)
115
+ ? staticDefault
116
+ : pickDefaultModelValue(modelOptions) ?? staticDefault;
117
+
118
+ const nextModelProperty: ConfigSchema['properties'][string] = {
119
+ ...modelProperty,
120
+ description: 'Model from the hosted marketplace registry.',
121
+ };
122
+
123
+ if (values.length > 0) {
124
+ nextModelProperty.enum = values;
125
+ nextModelProperty.options = modelOptions.map((option) => ({
126
+ label: option.label,
127
+ value: option.value,
128
+ description: option.description,
129
+ }));
130
+ }
131
+
132
+ if (defaultModel) {
133
+ nextModelProperty.default = defaultModel;
134
+ }
135
+
136
+ return {
137
+ ...descriptor,
138
+ configSchema: {
139
+ ...descriptor.configSchema,
140
+ properties: {
141
+ ...descriptor.configSchema.properties,
142
+ model: nextModelProperty,
143
+ },
144
+ },
145
+ };
146
+ }
@@ -7,10 +7,12 @@ import { bashPlugin } from '../../plugins/bash/index.js';
7
7
  import { storagePlugin } from '../../plugins/storage/index.js';
8
8
  import { approvalPlugin } from '../../plugins/approval/index.js';
9
9
  import { memoryPlugin } from '../../plugins/memory/index.js';
10
+ import { todoPlugin } from '../../plugins/todo/index.js';
10
11
  import { delegationPlugin } from '../../plugins/delegation/index.js';
11
12
  import { uiPlugin } from '../../plugins/ui/index.js';
12
13
  import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
13
- import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
14
+ import { previewPlugin } from '../../plugins/preview/index.js';
15
+ import { DEFAULT_PLUGINS_DIR, getBaseDir } from '../../app/config.js';
14
16
  import {
15
17
  invalidatePlugin as clearResolvedPluginEntry,
16
18
  loadedCommunityPlugins,
@@ -25,9 +27,11 @@ const BUILT_IN: Record<string, Plugin> = {
25
27
  [storagePlugin.id]: storagePlugin,
26
28
  [approvalPlugin.id]: approvalPlugin,
27
29
  [memoryPlugin.id]: memoryPlugin,
30
+ [todoPlugin.id]: todoPlugin,
28
31
  [delegationPlugin.id]: delegationPlugin,
29
32
  [uiPlugin.id]: uiPlugin,
30
33
  [pluginManagerPlugin.id]: pluginManagerPlugin,
34
+ [previewPlugin.id]: previewPlugin,
31
35
  };
32
36
 
33
37
  /**
@@ -71,9 +75,7 @@ export function initPlugins(dir?: string) {
71
75
  if (dir) {
72
76
  pluginsDir = dir;
73
77
  } else {
74
- const config = loadConfig();
75
- const baseDir = config.baseDir || DEFAULT_BASE_DIR;
76
- pluginsDir = path.join(resolvePath(baseDir), DEFAULT_PLUGINS_DIR);
78
+ pluginsDir = path.join(getBaseDir(), DEFAULT_PLUGINS_DIR);
77
79
  }
78
80
  }
79
81
 
@@ -5,8 +5,8 @@ import { exec } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
6
  import {
7
7
  DEFAULT_PLUGINS_DIR,
8
- DEFAULT_BASE_DIR,
9
8
  DEFAULT_MARKETPLACE_REGISTRY_URL,
9
+ getBaseDir,
10
10
  loadConfig,
11
11
  resolvePath,
12
12
  } from '../../app/config.js';
@@ -202,11 +202,7 @@ export async function resolveMarketplaceAgentList(): Promise<MarketplaceAgentLis
202
202
  return registry.agents;
203
203
  }
204
204
 
205
- const getPluginsDir = (): string => {
206
- const config = loadConfig();
207
- const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
208
- return path.join(baseDir, DEFAULT_PLUGINS_DIR);
209
- };
205
+ const getPluginsDir = (): string => path.join(getBaseDir(), DEFAULT_PLUGINS_DIR);
210
206
 
211
207
  /**
212
208
  * Lifecycle for community-built plugins distributed via npm.
@@ -1,76 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { openBotRuntime } from './runtime.js';
3
- import { shellPlugin, shellToolDefinitions } from './tools/shell.js';
4
- import { mcpPlugin, mcpToolDefinitions } from './tools/mcp.js';
5
- import { uiPlugin, uiToolDefinitions } from './tools/ui.js';
6
- import { delegationPlugin, delegationToolDefinitions } from './tools/delegation.js';
7
- import { storageToolDefinitions } from './tools/storage.js';
8
- import { approvalPlugin } from './middleware/approval.js';
9
- import { DEFAULT_OPENBOT_APPROVAL_RULES, OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
10
- const OPENBOT_ICON_DATA_URL = (() => {
11
- try {
12
- const svg = readFileSync(new URL('../../assets/icon.svg', import.meta.url), 'utf-8').trim();
13
- if (!svg.startsWith('<svg'))
14
- return undefined;
15
- return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`;
16
- }
17
- catch {
18
- return undefined;
19
- }
20
- })();
21
- const composeMelonyPlugin = (...plugins) => {
22
- return (builder) => {
23
- for (const plugin of plugins) {
24
- plugin(builder);
25
- }
26
- };
27
- };
28
- /**
29
- * OpenBot — the first-party orchestrator agent package.
30
- *
31
- * Treats the bus as a peer environment: registers a runtime that owns
32
- * `agent:invoke`, exposes a curated tool set (delegation, storage, MCP, shell,
33
- * UI widgets), and wires approval middleware for protected actions.
34
- *
35
- * Other agents (Codex, Claude Code, Gemini, custom Coder/Researcher) are
36
- * separate AgentPackages with their own runtime + tool composition.
37
- */
38
- export const openBotAgentPackage = {
39
- id: 'openbot',
40
- name: 'OpenBot',
41
- description: 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff and delegation.',
42
- image: OPENBOT_ICON_DATA_URL,
43
- defaultInstructions: OPENBOT_SYSTEM_PROMPT,
44
- configSchema: {
45
- type: 'object',
46
- properties: {
47
- model: {
48
- type: 'string',
49
- description: 'Provider model string, e.g. openai/gpt-4o-mini, anthropic/claude-3-5-sonnet-20240620',
50
- default: 'openai/gpt-4o-mini',
51
- },
52
- },
53
- },
54
- factory: ({ agentDetails, config, storage }) => {
55
- const model = typeof config.model === 'string' && config.model
56
- ? config.model
57
- : 'openai/gpt-4o-mini';
58
- const toolDefinitions = {
59
- ...delegationToolDefinitions,
60
- ...storageToolDefinitions,
61
- ...mcpToolDefinitions,
62
- ...shellToolDefinitions,
63
- // Re-enable when the dashboard renders widgets:
64
- // ...uiToolDefinitions,
65
- };
66
- const systemPrompt = agentDetails.instructions || OPENBOT_SYSTEM_PROMPT;
67
- return composeMelonyPlugin(openBotRuntime({
68
- model,
69
- system: systemPrompt,
70
- storage,
71
- toolDefinitions,
72
- }), approvalPlugin({ rules: DEFAULT_OPENBOT_APPROVAL_RULES }), delegationPlugin(), shellPlugin(), mcpPlugin(), uiPlugin());
73
- },
74
- };
75
- // Suppress unused warning while UI widget tools are not wired into the LLM tool list.
76
- void uiToolDefinitions;
@@ -1,132 +0,0 @@
1
- import { storageService } from '../../../services/storage.js';
2
- const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
3
- ? value
4
- : {};
5
- const getApprovalsFromState = (state) => {
6
- const source = state.threadDetails?.state ?? state.channelDetails?.state;
7
- const stateRecord = asRecord(source);
8
- return asRecord(stateRecord.approvals);
9
- };
10
- const persistApprovals = async (state, approvals) => {
11
- if (state.threadId) {
12
- await storageService.patchThreadState({
13
- channelId: state.channelId,
14
- threadId: state.threadId,
15
- state: { approvals },
16
- });
17
- return;
18
- }
19
- await storageService.patchChannelState({
20
- channelId: state.channelId,
21
- state: { approvals },
22
- });
23
- };
24
- export const approvalPlugin = (options = {}) => (builder) => {
25
- const rules = options.rules || [];
26
- for (const rule of rules) {
27
- builder.on(rule.action, async function* (event, context) {
28
- const meta = asRecord(event.meta);
29
- if (meta.approvalStatus === 'approved')
30
- return;
31
- const eventData = asRecord(event.data);
32
- const eventMeta = meta;
33
- const approvalId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
34
- const widgetId = `widget_${approvalId}`;
35
- const executeEvent = rule.executeEvent || rule.action;
36
- const denyEvent = rule.denyEvent || `${rule.action}:result`;
37
- const denyData = rule.denyData || {};
38
- const hiddenKeys = new Set(rule.hiddenKeys || []);
39
- const detailKeys = rule.detailKeys || Object.keys(eventData);
40
- const details = detailKeys
41
- .filter((key) => !hiddenKeys.has(key))
42
- .map((key) => `- ${key}: ${String(eventData[key] ?? '')}`)
43
- .join('\n');
44
- const pendingApprovals = getApprovalsFromState(context.state);
45
- pendingApprovals[approvalId] = {
46
- id: approvalId,
47
- action: rule.action,
48
- executeEvent,
49
- denyEvent,
50
- denyData,
51
- payload: eventData,
52
- meta: eventMeta,
53
- message: rule.message || `Approval required for ${rule.action}.`,
54
- createdAt: new Date().toISOString(),
55
- status: 'pending',
56
- };
57
- await persistApprovals(context.state, pendingApprovals);
58
- yield {
59
- type: 'client:ui:widget',
60
- data: {
61
- kind: 'choice',
62
- widgetId,
63
- title: 'Approval Required',
64
- body: `${rule.message || 'A protected action requires approval.'}${details ? `\n\n${details}` : ''}`,
65
- metadata: { type: 'approval:request', approvalId, action: rule.action },
66
- actions: [
67
- { id: 'approve', label: 'Approve', variant: 'primary' },
68
- { id: 'deny', label: 'Deny', variant: 'danger' },
69
- ],
70
- },
71
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
72
- };
73
- yield {
74
- type: 'agent:output',
75
- data: { content: `Waiting for approval before running \`${rule.action}\`.` },
76
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
77
- };
78
- context.suspend();
79
- });
80
- }
81
- builder.on('client:ui:widget:response', async function* (event, context) {
82
- const metadata = asRecord(event.data?.metadata);
83
- if (metadata.type !== 'approval:request')
84
- return;
85
- const approvalId = String(metadata.approvalId || '');
86
- if (!approvalId)
87
- return;
88
- const approvals = getApprovalsFromState(context.state);
89
- const approval = approvals[approvalId];
90
- if (!approval || approval.status !== 'pending') {
91
- yield {
92
- type: 'agent:output',
93
- data: { content: 'Approval request not found or already resolved.' },
94
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
95
- };
96
- return;
97
- }
98
- const approved = event.data.actionId === 'approve';
99
- approvals[approvalId] = {
100
- ...approval,
101
- status: approved ? 'approved' : 'denied',
102
- };
103
- await persistApprovals(context.state, approvals);
104
- if (approved) {
105
- yield {
106
- type: approval.executeEvent,
107
- data: approval.payload,
108
- meta: {
109
- ...(approval.meta || {}),
110
- approvalId,
111
- approvalStatus: 'approved',
112
- },
113
- };
114
- return;
115
- }
116
- yield {
117
- type: approval.denyEvent,
118
- data: {
119
- success: false,
120
- approved: false,
121
- error: 'Action denied by user approval.',
122
- ...approval.denyData,
123
- },
124
- meta: { ...(approval.meta || {}), approvalId },
125
- };
126
- yield {
127
- type: 'agent:output',
128
- data: { content: 'Action denied by user approval.' },
129
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
130
- };
131
- });
132
- };