openbot 0.4.7 → 0.5.5

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 (139) 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 +51 -0
  11. package/Dockerfile +41 -0
  12. package/deploy/README.md +59 -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 +5 -1
  17. package/dist/app/cloud-mode.js +15 -0
  18. package/dist/app/config.js +61 -16
  19. package/dist/app/ensure-default-stack.js +54 -0
  20. package/dist/app/openbot-plugin.js +4 -0
  21. package/dist/app/server.js +37 -6
  22. package/dist/harness/index.js +3 -0
  23. package/dist/plugins/approval/index.js +1 -1
  24. package/dist/plugins/bash/index.js +386 -164
  25. package/dist/plugins/memory/service.js +2 -5
  26. package/dist/plugins/openbot/context.js +17 -1
  27. package/dist/plugins/openbot/index.js +23 -7
  28. package/dist/plugins/openbot/model.js +20 -0
  29. package/dist/plugins/openbot/runtime.js +45 -57
  30. package/dist/plugins/openbot/system-prompt.js +43 -8
  31. package/dist/plugins/preview/index.js +267 -0
  32. package/dist/plugins/storage/index.js +3 -325
  33. package/dist/plugins/storage/service.js +61 -18
  34. package/dist/plugins/todo/index.js +131 -38
  35. package/dist/plugins/todo/service.js +93 -0
  36. package/dist/plugins/ui/index.js +8 -1
  37. package/dist/services/plugins/host.js +21 -0
  38. package/dist/services/plugins/model-registry.js +126 -0
  39. package/dist/services/plugins/registry.js +28 -42
  40. package/dist/services/plugins/service.js +2 -6
  41. package/docs/agents.md +20 -2
  42. package/docs/architecture.md +1 -1
  43. package/docs/plugins.md +13 -3
  44. package/docs/templates/AGENT.example.md +8 -14
  45. package/package.json +5 -2
  46. package/pnpm-workspace.yaml +2 -0
  47. package/src/app/bootstrap.ts +43 -0
  48. package/src/app/cli.ts +5 -1
  49. package/src/app/cloud-mode.ts +26 -0
  50. package/src/app/config.ts +71 -19
  51. package/src/app/ensure-default-stack.ts +63 -0
  52. package/src/app/openbot-plugin.ts +5 -0
  53. package/src/app/server.ts +42 -6
  54. package/src/app/types.ts +158 -32
  55. package/src/harness/index.ts +4 -0
  56. package/src/plugins/memory/service.ts +2 -5
  57. package/src/plugins/storage/index.ts +3 -371
  58. package/src/plugins/storage/service.ts +81 -22
  59. package/src/services/plugins/domain.ts +2 -0
  60. package/src/services/plugins/host.ts +36 -0
  61. package/src/services/plugins/model-registry.ts +178 -0
  62. package/src/services/plugins/registry.ts +30 -43
  63. package/src/services/plugins/service.ts +8 -17
  64. package/src/services/plugins/types.ts +36 -2
  65. package/src/services/todo/types.ts +12 -0
  66. package/dist/agents/openbot/index.js +0 -76
  67. package/dist/agents/openbot/middleware/approval.js +0 -132
  68. package/dist/agents/openbot/runtime.js +0 -289
  69. package/dist/agents/openbot/system-prompt.js +0 -32
  70. package/dist/agents/openbot/tools/delegation.js +0 -78
  71. package/dist/agents/openbot/tools/mcp.js +0 -99
  72. package/dist/agents/openbot/tools/shell.js +0 -91
  73. package/dist/agents/openbot/tools/storage.js +0 -75
  74. package/dist/agents/openbot/tools/ui.js +0 -176
  75. package/dist/agents/system.js +0 -33
  76. package/dist/bus/agent-package.js +0 -1
  77. package/dist/bus/plugin.js +0 -1
  78. package/dist/bus/services.js +0 -754
  79. package/dist/harness/agent-harness.js +0 -45
  80. package/dist/harness/agent-invoke-run.js +0 -44
  81. package/dist/harness/agent-turn.js +0 -99
  82. package/dist/harness/channel-participants.js +0 -40
  83. package/dist/harness/constants.js +0 -2
  84. package/dist/harness/context-meter.js +0 -97
  85. package/dist/harness/context.js +0 -363
  86. package/dist/harness/dispatch.js +0 -144
  87. package/dist/harness/dispatcher.js +0 -156
  88. package/dist/harness/event-normalizer.js +0 -59
  89. package/dist/harness/history.js +0 -177
  90. package/dist/harness/mcp.js +0 -61
  91. package/dist/harness/orchestration.js +0 -88
  92. package/dist/harness/orchestrator.js +0 -149
  93. package/dist/harness/participants.js +0 -22
  94. package/dist/harness/process.js +0 -29
  95. package/dist/harness/queue-processor.js +0 -187
  96. package/dist/harness/run-harness.js +0 -154
  97. package/dist/harness/run.js +0 -98
  98. package/dist/harness/runtime-factory.js +0 -73
  99. package/dist/harness/runtime.js +0 -57
  100. package/dist/harness/todo-advance.js +0 -93
  101. package/dist/harness/todo-dispatch.js +0 -51
  102. package/dist/harness/todos.js +0 -5
  103. package/dist/harness/turn.js +0 -79
  104. package/dist/harness/types.js +0 -1
  105. package/dist/plugins/ai-sdk/index.js +0 -34
  106. package/dist/plugins/ai-sdk/runtime.js +0 -395
  107. package/dist/plugins/ai-sdk/system-prompt.js +0 -18
  108. package/dist/plugins/ai-sdk.js +0 -331
  109. package/dist/plugins/approval.js +0 -163
  110. package/dist/plugins/delegation.js +0 -108
  111. package/dist/plugins/mcp/index.js +0 -108
  112. package/dist/plugins/mcp.js +0 -140
  113. package/dist/plugins/shell/index.js +0 -100
  114. package/dist/plugins/shell.js +0 -123
  115. package/dist/plugins/storage-tools/index.js +0 -76
  116. package/dist/plugins/storage.js +0 -737
  117. package/dist/plugins/thread-namer/index.js +0 -72
  118. package/dist/plugins/threads/index.js +0 -114
  119. package/dist/plugins/ui.js +0 -211
  120. package/dist/plugins/workflow/index.js +0 -65
  121. package/dist/registry/agents.js +0 -138
  122. package/dist/registry/plugins.js +0 -110
  123. package/dist/services/agent-packages.js +0 -103
  124. package/dist/services/memory.js +0 -152
  125. package/dist/services/plugins.js +0 -98
  126. package/dist/services/storage.js +0 -1028
  127. package/dist/workflow/service.js +0 -106
  128. package/dist/workflow/types.js +0 -3
  129. package/src/plugins/approval/index.ts +0 -147
  130. package/src/plugins/bash/index.ts +0 -232
  131. package/src/plugins/delegation/index.ts +0 -153
  132. package/src/plugins/memory/index.ts +0 -182
  133. package/src/plugins/openbot/context.ts +0 -121
  134. package/src/plugins/openbot/history.ts +0 -158
  135. package/src/plugins/openbot/index.ts +0 -80
  136. package/src/plugins/openbot/runtime.ts +0 -526
  137. package/src/plugins/openbot/system-prompt.ts +0 -22
  138. package/src/plugins/ui/index.ts +0 -123
  139. /package/dist/{bus → services/todo}/types.js +0 -0
@@ -0,0 +1,178 @@
1
+ import {
2
+ DEFAULT_MARKETPLACE_REGISTRY_URL,
3
+ loadConfig,
4
+ } from '../../app/config.js';
5
+ import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
6
+ import {
7
+ DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
8
+ isCloudMode,
9
+ } from '../../app/cloud-mode.js';
10
+ import type { ConfigSchema, PluginDescriptor } from './domain.js';
11
+
12
+ export type RegistryProviderCatalog = Record<
13
+ string,
14
+ {
15
+ label: string;
16
+ models: Array<{ id: string; label: string; description: string }>;
17
+ }
18
+ >;
19
+
20
+ export type ModelRegistry = {
21
+ providers?: RegistryProviderCatalog;
22
+ };
23
+
24
+ export type RegistryModelOption = {
25
+ value: string;
26
+ label: string;
27
+ description?: string;
28
+ provider: string;
29
+ };
30
+
31
+ let cachedRegistry: ModelRegistry | null = null;
32
+ let cacheUrl: string | null = null;
33
+
34
+ function getRegistryUrl(): string {
35
+ const { marketplaceRegistryUrl } = loadConfig();
36
+ return marketplaceRegistryUrl?.trim() || DEFAULT_MARKETPLACE_REGISTRY_URL;
37
+ }
38
+
39
+ export async function resolveModelRegistry(): Promise<ModelRegistry> {
40
+ const url = getRegistryUrl();
41
+ if (cachedRegistry && cacheUrl === url) return cachedRegistry;
42
+
43
+ try {
44
+ const res = await fetch(url, {
45
+ headers: { Accept: 'application/json' },
46
+ signal: AbortSignal.timeout(15_000),
47
+ });
48
+ if (!res.ok) {
49
+ throw new Error(`Registry HTTP ${res.status} ${res.statusText}`);
50
+ }
51
+ cachedRegistry = (await res.json()) as ModelRegistry;
52
+ cacheUrl = url;
53
+ return cachedRegistry;
54
+ } catch (err) {
55
+ console.warn(
56
+ '[model-registry] fetch failed:',
57
+ err instanceof Error ? err.message : err,
58
+ );
59
+ return { providers: {} };
60
+ }
61
+ }
62
+
63
+ export function listModelOptionsFromRegistry(registry: ModelRegistry): RegistryModelOption[] {
64
+ const providers = registry.providers ?? {};
65
+ const out: RegistryModelOption[] = [];
66
+
67
+ for (const [providerId, provider] of Object.entries(providers)) {
68
+ for (const model of provider.models ?? []) {
69
+ out.push({
70
+ value: `${providerId}/${model.id}`,
71
+ label: `${provider.label} — ${model.label}`,
72
+ description: model.description,
73
+ provider: providerId,
74
+ });
75
+ }
76
+ }
77
+
78
+ return out;
79
+ }
80
+
81
+ export async function listRegistryModelOptions(): Promise<RegistryModelOption[]> {
82
+ const registry = await resolveModelRegistry();
83
+ return listModelOptionsFromRegistry(registry);
84
+ }
85
+
86
+ export function listApiKeyProvidersFromRegistry(
87
+ registry: ModelRegistry,
88
+ ): Array<{ id: string; label: string }> {
89
+ const providers = registry.providers ?? {};
90
+ return Object.entries(providers).map(([id, provider]) => ({
91
+ id,
92
+ label: provider.label,
93
+ }));
94
+ }
95
+
96
+ function pickDefaultModelValue(options: RegistryModelOption[]): string | undefined {
97
+ if (options.length === 0) return undefined;
98
+ const values = options.map((option) => option.value);
99
+ const preferred = values.find((value) => value.startsWith('openai/'));
100
+ return preferred ?? values[0];
101
+ }
102
+
103
+ export function enrichOpenbotPluginDescriptor(
104
+ descriptor: PluginDescriptor,
105
+ modelOptions: RegistryModelOption[],
106
+ ): PluginDescriptor {
107
+ if (descriptor.id !== OPENBOT_PLUGIN_ID || !descriptor.configSchema) return descriptor;
108
+
109
+ const modelProperty = descriptor.configSchema.properties.model;
110
+ if (!modelProperty) return descriptor;
111
+
112
+ const values = modelOptions.map((option) => option.value);
113
+ const staticDefault =
114
+ typeof modelProperty.default === 'string' ? modelProperty.default : undefined;
115
+ const defaultModel =
116
+ staticDefault && values.includes(staticDefault)
117
+ ? staticDefault
118
+ : pickDefaultModelValue(modelOptions) ?? staticDefault;
119
+
120
+ const nextModelProperty: ConfigSchema['properties'][string] = {
121
+ ...modelProperty,
122
+ description: 'Model from the hosted marketplace registry.',
123
+ };
124
+
125
+ if (values.length > 0) {
126
+ nextModelProperty.enum = values;
127
+ nextModelProperty.options = modelOptions.map((option) => ({
128
+ label: option.label,
129
+ value: option.value,
130
+ description: option.description,
131
+ }));
132
+ }
133
+
134
+ if (defaultModel) {
135
+ nextModelProperty.default = defaultModel;
136
+ }
137
+
138
+ const { model: _model, authMode: _authMode, ...otherProperties } =
139
+ descriptor.configSchema.properties;
140
+
141
+ const cloudAuthModeProperty: ConfigSchema['properties'][string] = {
142
+ type: 'string',
143
+ description: 'Credits uses OpenBot platform billing. BYOK uses your own API keys.',
144
+ enum: ['credits', 'byok'],
145
+ default: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
146
+ options: [
147
+ {
148
+ label: 'Credits',
149
+ value: 'credits',
150
+ description: 'Use OpenBot platform credits.',
151
+ },
152
+ {
153
+ label: 'Bring your own key',
154
+ value: 'byok',
155
+ description: 'Use your own provider API keys.',
156
+ },
157
+ ],
158
+ };
159
+
160
+ const nextProperties: ConfigSchema['properties'] = isCloudMode()
161
+ ? {
162
+ authMode: cloudAuthModeProperty,
163
+ model: nextModelProperty,
164
+ ...otherProperties,
165
+ }
166
+ : {
167
+ model: nextModelProperty,
168
+ ...otherProperties,
169
+ };
170
+
171
+ return {
172
+ ...descriptor,
173
+ configSchema: {
174
+ ...descriptor.configSchema,
175
+ properties: nextProperties,
176
+ },
177
+ };
178
+ }
@@ -1,16 +1,11 @@
1
- import fs from 'node:fs';
1
+ import fs from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
  import type { Plugin } from './types.js';
5
- import { openbotPlugin } from '../../plugins/openbot/index.js';
6
- import { bashPlugin } from '../../plugins/bash/index.js';
7
6
  import { storagePlugin } from '../../plugins/storage/index.js';
8
- import { approvalPlugin } from '../../plugins/approval/index.js';
9
- import { memoryPlugin } from '../../plugins/memory/index.js';
10
- import { delegationPlugin } from '../../plugins/delegation/index.js';
11
- import { uiPlugin } from '../../plugins/ui/index.js';
12
7
  import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
13
- import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
8
+ import { DEFAULT_PLUGINS_DIR, getBaseDir } from '../../app/config.js';
14
9
  import {
15
10
  invalidatePlugin as clearResolvedPluginEntry,
16
11
  loadedCommunityPlugins,
@@ -20,13 +15,7 @@ import {
20
15
  let pluginsDir: string | null = null;
21
16
 
22
17
  const BUILT_IN: Record<string, Plugin> = {
23
- [openbotPlugin.id]: openbotPlugin,
24
- [bashPlugin.id]: bashPlugin,
25
18
  [storagePlugin.id]: storagePlugin,
26
- [approvalPlugin.id]: approvalPlugin,
27
- [memoryPlugin.id]: memoryPlugin,
28
- [delegationPlugin.id]: delegationPlugin,
29
- [uiPlugin.id]: uiPlugin,
30
19
  [pluginManagerPlugin.id]: pluginManagerPlugin,
31
20
  };
32
21
 
@@ -71,24 +60,18 @@ export function initPlugins(dir?: string) {
71
60
  if (dir) {
72
61
  pluginsDir = dir;
73
62
  } else {
74
- const config = loadConfig();
75
- const baseDir = config.baseDir || DEFAULT_BASE_DIR;
76
- pluginsDir = path.join(resolvePath(baseDir), DEFAULT_PLUGINS_DIR);
63
+ pluginsDir = path.join(getBaseDir(), DEFAULT_PLUGINS_DIR);
77
64
  }
78
65
  }
79
66
 
80
67
  /**
81
68
  * Resolve a Plugin by id. The id is either:
82
- * - a built-in id (e.g. "openbot", "bash"), or
83
- * - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
69
+ * - a built-in id (e.g. "storage", "plugin-manager"), or
70
+ * - an npm package name (e.g. "@meetopenbot/openbot"),
84
71
  * in which case the folder layout is `plugins/<id>/dist/index.js`.
85
72
  */
86
73
  export async function resolvePlugin(id: string): Promise<Plugin | null> {
87
74
  if (resolvedPluginCache.has(id)) return resolvedPluginCache.get(id)!;
88
- if (BUILT_IN[id]) {
89
- resolvedPluginCache.set(id, BUILT_IN[id]);
90
- return BUILT_IN[id];
91
- }
92
75
 
93
76
  if (!pluginsDir) {
94
77
  initPlugins();
@@ -97,29 +80,33 @@ export async function resolvePlugin(id: string): Promise<Plugin | null> {
97
80
 
98
81
  const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
99
82
 
100
- if (!fs.existsSync(distPath)) {
101
- console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
102
- return null;
83
+ if (existsSync(distPath)) {
84
+ try {
85
+ const module = await import(pathToFileURL(distPath).href);
86
+ const parsed = parsePluginModule(module as Record<string, unknown>);
87
+ if (!parsed) {
88
+ console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
89
+ } else {
90
+ const plugin: Plugin = { id, ...parsed, name: parsed.name || id };
91
+ resolvedPluginCache.set(id, plugin);
92
+ if (!loadedCommunityPlugins.has(id)) {
93
+ console.log(`[plugins] Loaded plugin "${id}" from ${distPath}`);
94
+ loadedCommunityPlugins.add(id);
95
+ }
96
+ return plugin;
97
+ }
98
+ } catch (e) {
99
+ console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
100
+ }
103
101
  }
104
102
 
105
- try {
106
- const module = await import(pathToFileURL(distPath).href);
107
- const parsed = parsePluginModule(module as Record<string, unknown>);
108
- if (!parsed) {
109
- console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
110
- return null;
111
- }
112
- const plugin: Plugin = { id, ...parsed, name: parsed.name || id };
113
- resolvedPluginCache.set(id, plugin);
114
- if (!loadedCommunityPlugins.has(id)) {
115
- console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
116
- loadedCommunityPlugins.add(id);
117
- }
118
- return plugin;
119
- } catch (e) {
120
- console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
121
- return null;
103
+ if (BUILT_IN[id]) {
104
+ resolvedPluginCache.set(id, BUILT_IN[id]);
105
+ return BUILT_IN[id];
122
106
  }
107
+
108
+ console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
109
+ return null;
123
110
  }
124
111
 
125
112
  /** Drop a single id from the in-memory cache (e.g. after fresh install). */
@@ -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';
@@ -104,7 +104,9 @@ export function parseMarketplaceRegistryJson(data: unknown): MarketplaceRegistry
104
104
  if (!Array.isArray(pluginsRaw)) throw new Error(`agents[${i}].plugins must be an array`);
105
105
  const plugins: PluginRef[] = pluginsRaw.map((p, j) => {
106
106
  if (!isRecord(p) || typeof p.id !== 'string' || !p.id) {
107
- throw new Error(`agents[${i}].plugins[${j}]: expected { "id": string, "config"?: object }`);
107
+ throw new Error(
108
+ `agents[${i}].plugins[${j}]: expected { "id": string, "config"?: object }`,
109
+ );
108
110
  }
109
111
  const ref: PluginRef = { id: p.id };
110
112
  if (p.config !== undefined) {
@@ -202,11 +204,7 @@ export async function resolveMarketplaceAgentList(): Promise<MarketplaceAgentLis
202
204
  return registry.agents;
203
205
  }
204
206
 
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
- };
207
+ const getPluginsDir = (): string => path.join(getBaseDir(), DEFAULT_PLUGINS_DIR);
210
208
 
211
209
  /**
212
210
  * Lifecycle for community-built plugins distributed via npm.
@@ -267,17 +265,13 @@ export const pluginService = {
267
265
  console.warn(`[plugins] Failed to run npm install in ${finalPath}:`, e);
268
266
  }
269
267
 
270
- const pkgJson = JSON.parse(
271
- await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'),
272
- );
268
+ const pkgJson = JSON.parse(await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'));
273
269
 
274
270
  invalidatePlugin(packageName);
275
271
  return { name: pkgJson.name, version: pkgJson.version };
276
272
  } catch (error) {
277
273
  console.error(`[plugins] Failed to install ${packageName}:`, error);
278
- throw new Error(
279
- `Failed to install plugin ${packageName}: ${(error as Error).message}`,
280
- );
274
+ throw new Error(`Failed to install plugin ${packageName}: ${(error as Error).message}`);
281
275
  } finally {
282
276
  await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
283
277
  }
@@ -303,10 +297,7 @@ export const pluginService = {
303
297
  }
304
298
  } catch (error) {
305
299
  console.error(`[plugins] Failed to uninstall ${packageName}:`, error);
306
- throw new Error(
307
- `Failed to uninstall plugin ${packageName}: ${(error as Error).message}`,
308
- );
300
+ throw new Error(`Failed to uninstall plugin ${packageName}: ${(error as Error).message}`);
309
301
  }
310
302
  },
311
303
  };
312
-
@@ -5,8 +5,8 @@ import { AgentDetails, ConfigSchema, Storage } from './domain.js';
5
5
  /**
6
6
  * Reference to a plugin from an agent's AGENT.md frontmatter.
7
7
  *
8
- * The `id` is either a built-in plugin id (e.g. `openbot`, `bash`) or an npm
9
- * package name (e.g. `openbot-plugin-codex`, `@scope/openbot-plugin-foo`).
8
+ * The `id` is either a built-in plugin id (e.g. `storage`, `plugin-manager`) or
9
+ * an npm package name (e.g. `@meetopenbot/openbot`, `openbot-plugin-foo`).
10
10
  * Each entry may carry plugin-specific `config`.
11
11
  */
12
12
  export interface PluginRef {
@@ -20,6 +20,38 @@ export interface ToolDefinition {
20
20
  inputSchema: unknown;
21
21
  }
22
22
 
23
+ /** Options for delegated / nested agent runs via PluginHost.runAgent. */
24
+ export interface RunAgentHostOptions {
25
+ runId: string;
26
+ agentId: string;
27
+ event: OpenBotEvent;
28
+ channelId: string;
29
+ threadId?: string;
30
+ persistEvents?: boolean;
31
+ publicBaseUrl?: string;
32
+ onEvent: (event: OpenBotEvent, state?: OpenBotState) => Promise<void>;
33
+ }
34
+
35
+ /** Host capabilities exposed to community / external agent runtime plugins. */
36
+ export interface PluginHost {
37
+ runAgent: (options: RunAgentHostOptions) => Promise<void>;
38
+ isCloudSystemAgent: (agentId: string) => boolean;
39
+ isCloudMode: () => boolean;
40
+ parseOpenbotAuthMode: (value: unknown) => 'credits' | 'byok';
41
+ resolveModelRegistry: () => ReturnType<
42
+ typeof import('./model-registry.js').resolveModelRegistry
43
+ >;
44
+ listApiKeyProvidersFromRegistry: (
45
+ registry: Awaited<ReturnType<PluginHost['resolveModelRegistry']>>,
46
+ ) => Array<{ id: string; label: string }>;
47
+ saveConfig: (patch: Record<string, unknown>) => void;
48
+ getBaseDir: () => string;
49
+ resolvePath: (p: string) => string;
50
+ orchestratorAgentId: string;
51
+ openbotPluginId: string;
52
+ defaultCloudAuthMode: 'credits' | 'byok';
53
+ }
54
+
23
55
  /**
24
56
  * Context passed to a Plugin factory at runtime.
25
57
  *
@@ -37,6 +69,8 @@ export interface PluginContext {
37
69
  publicBaseUrl: string;
38
70
  /** Signal that fires when this run is stopped; runtimes should pass it to long-running calls. */
39
71
  abortSignal?: AbortSignal;
72
+ /** Platform hooks for external runtime plugins (delegation, cloud auth, config). */
73
+ host: PluginHost;
40
74
  }
41
75
 
42
76
  /**
@@ -0,0 +1,12 @@
1
+ export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
2
+
3
+ export type TodoItem = {
4
+ id: string;
5
+ content: string;
6
+ status: TodoStatus;
7
+ };
8
+
9
+ export type TodoList = {
10
+ items: TodoItem[];
11
+ updatedAt: string;
12
+ };
@@ -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
- };