openbot 0.5.4 → 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.
- package/CLAUDE.md +5 -4
- package/deploy/README.md +2 -4
- package/dist/app/cli.js +3 -1
- package/dist/app/cloud-mode.js +5 -16
- package/dist/app/ensure-default-stack.js +54 -0
- package/dist/app/openbot-plugin.js +4 -0
- package/dist/app/server.js +2 -3
- package/dist/harness/index.js +3 -0
- package/dist/plugins/openbot/index.js +6 -5
- package/dist/plugins/openbot/model.js +2 -41
- package/dist/plugins/openbot/runtime.js +9 -4
- package/dist/plugins/storage/index.js +3 -325
- package/dist/plugins/storage/service.js +18 -21
- package/dist/services/plugins/host.js +21 -0
- package/dist/services/plugins/model-registry.js +34 -9
- package/dist/services/plugins/registry.js +26 -42
- package/dist/services/todo/types.js +1 -0
- package/docs/templates/AGENT.example.md +8 -14
- package/package.json +2 -2
- package/src/app/cli.ts +3 -1
- package/src/app/cloud-mode.ts +7 -22
- package/src/app/ensure-default-stack.ts +63 -0
- package/src/app/openbot-plugin.ts +5 -0
- package/src/app/server.ts +2 -5
- package/src/app/types.ts +3 -8
- package/src/harness/index.ts +4 -0
- package/src/plugins/storage/index.ts +3 -368
- package/src/plugins/storage/service.ts +17 -22
- package/src/services/plugins/host.ts +36 -0
- package/src/services/plugins/model-registry.ts +41 -9
- package/src/services/plugins/registry.ts +28 -43
- package/src/services/plugins/service.ts +6 -11
- package/src/services/plugins/types.ts +36 -2
- package/src/services/todo/types.ts +12 -0
- package/src/plugins/approval/index.ts +0 -147
- package/src/plugins/bash/index.ts +0 -545
- package/src/plugins/delegation/index.ts +0 -153
- package/src/plugins/memory/index.ts +0 -182
- package/src/plugins/openbot/context.ts +0 -137
- package/src/plugins/openbot/history.ts +0 -158
- package/src/plugins/openbot/index.ts +0 -95
- package/src/plugins/openbot/model.ts +0 -76
- package/src/plugins/openbot/runtime.ts +0 -504
- package/src/plugins/openbot/system-prompt.ts +0 -57
- package/src/plugins/preview/index.ts +0 -323
- package/src/plugins/todo/index.ts +0 -166
- package/src/plugins/todo/service.ts +0 -123
- package/src/plugins/ui/index.ts +0 -130
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ORCHESTRATOR_AGENT_ID, STATE_AGENT_ID } from '../../app/agent-ids.js';
|
|
2
2
|
import {
|
|
3
3
|
assertCloudSystemAgentPluginsMutable,
|
|
4
|
-
|
|
4
|
+
DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
5
5
|
isCloudMode,
|
|
6
6
|
} from '../../app/cloud-mode.js';
|
|
7
7
|
import {
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
ThreadState,
|
|
32
32
|
} from '../../services/plugins/domain.js';
|
|
33
33
|
import type { PluginRef } from '../../services/plugins/types.js';
|
|
34
|
-
import {
|
|
34
|
+
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
35
35
|
import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
|
|
36
36
|
import {
|
|
37
37
|
enrichOpenbotPluginDescriptor,
|
|
@@ -122,34 +122,32 @@ const SYSTEM_AGENT_ID = ORCHESTRATOR_AGENT_ID;
|
|
|
122
122
|
|
|
123
123
|
const SYSTEM_DEFAULT_PLUGINS: PluginRef[] = [
|
|
124
124
|
{
|
|
125
|
-
id:
|
|
125
|
+
id: OPENBOT_PLUGIN_ID,
|
|
126
126
|
config: {
|
|
127
127
|
model: 'openai/gpt-5.4-mini',
|
|
128
|
-
approval: {
|
|
129
|
-
actions: ['action:shell_exec', 'action:create_channel', 'action:delete_channel'],
|
|
130
|
-
},
|
|
131
128
|
},
|
|
132
129
|
},
|
|
133
130
|
];
|
|
134
131
|
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
id
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
function getCloudSystemDefaultPlugins(): PluginRef[] {
|
|
133
|
+
return SYSTEM_DEFAULT_PLUGINS.map((ref) => {
|
|
134
|
+
if (ref.id !== OPENBOT_PLUGIN_ID) return ref;
|
|
135
|
+
return {
|
|
136
|
+
...ref,
|
|
137
|
+
config: {
|
|
138
|
+
...ref.config,
|
|
139
|
+
authMode: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
142
140
|
},
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
146
144
|
|
|
147
145
|
function mergeCloudSystemPluginRefs(
|
|
148
146
|
defaults: PluginRef[],
|
|
149
147
|
diskRefs: PluginRef[],
|
|
150
148
|
): PluginRef[] {
|
|
151
|
-
const defaultOpenbot = defaults.find((ref) => ref.id ===
|
|
152
|
-
const diskOpenbot = diskRefs.find((ref) => ref.id ===
|
|
149
|
+
const defaultOpenbot = defaults.find((ref) => ref.id === OPENBOT_PLUGIN_ID);
|
|
150
|
+
const diskOpenbot = diskRefs.find((ref) => ref.id === OPENBOT_PLUGIN_ID);
|
|
153
151
|
|
|
154
152
|
if (!defaultOpenbot) return defaults;
|
|
155
153
|
|
|
@@ -174,7 +172,7 @@ const STATE_AGENT_INSTRUCTIONS =
|
|
|
174
172
|
'Built-in infra agent for deterministic state reads. No conversational model is attached; handle storage, approvals, memory, and plugin marketplace events.';
|
|
175
173
|
|
|
176
174
|
function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails {
|
|
177
|
-
const defaultPlugins = isCloudMode() ?
|
|
175
|
+
const defaultPlugins = isCloudMode() ? getCloudSystemDefaultPlugins() : SYSTEM_DEFAULT_PLUGINS;
|
|
178
176
|
const defaults: AgentDetails = {
|
|
179
177
|
id: SYSTEM_AGENT_ID,
|
|
180
178
|
name: 'OpenBot',
|
|
@@ -277,9 +275,6 @@ const agentSummaryFromDetails = (details: AgentDetails): Agent => ({
|
|
|
277
275
|
updatedAt: details.updatedAt,
|
|
278
276
|
});
|
|
279
277
|
|
|
280
|
-
// Suppress unused warning until system agent customization re-uses openbotPlugin metadata.
|
|
281
|
-
void openbotPlugin;
|
|
282
|
-
|
|
283
278
|
/** Built-in agents may persist optional `agents/<id>/AGENT.md` overlays; read path merges them with defaults. */
|
|
284
279
|
const isBuiltinOverlayAgentId = (agentId: string): boolean =>
|
|
285
280
|
agentId === SYSTEM_AGENT_ID || agentId === STATE_AGENT_ID;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
3
|
+
isCloudMode,
|
|
4
|
+
isCloudSystemAgent,
|
|
5
|
+
parseOpenbotAuthMode,
|
|
6
|
+
type OpenbotAuthMode,
|
|
7
|
+
} from '../../app/cloud-mode.js';
|
|
8
|
+
import { ORCHESTRATOR_AGENT_ID } from '../../app/agent-ids.js';
|
|
9
|
+
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
10
|
+
import { getBaseDir, resolvePath, saveConfig } from '../../app/config.js';
|
|
11
|
+
import {
|
|
12
|
+
listApiKeyProvidersFromRegistry,
|
|
13
|
+
resolveModelRegistry,
|
|
14
|
+
} from './model-registry.js';
|
|
15
|
+
import type { PluginHost, RunAgentHostOptions } from './types.js';
|
|
16
|
+
|
|
17
|
+
export function createPluginHost(
|
|
18
|
+
runAgent: (options: RunAgentHostOptions) => Promise<void>,
|
|
19
|
+
): PluginHost {
|
|
20
|
+
return {
|
|
21
|
+
runAgent,
|
|
22
|
+
isCloudSystemAgent,
|
|
23
|
+
isCloudMode,
|
|
24
|
+
parseOpenbotAuthMode,
|
|
25
|
+
resolveModelRegistry,
|
|
26
|
+
listApiKeyProvidersFromRegistry,
|
|
27
|
+
saveConfig,
|
|
28
|
+
getBaseDir,
|
|
29
|
+
resolvePath,
|
|
30
|
+
orchestratorAgentId: ORCHESTRATOR_AGENT_ID,
|
|
31
|
+
openbotPluginId: OPENBOT_PLUGIN_ID,
|
|
32
|
+
defaultCloudAuthMode: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type { OpenbotAuthMode };
|
|
@@ -2,7 +2,11 @@ import {
|
|
|
2
2
|
DEFAULT_MARKETPLACE_REGISTRY_URL,
|
|
3
3
|
loadConfig,
|
|
4
4
|
} from '../../app/config.js';
|
|
5
|
-
import {
|
|
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';
|
|
6
10
|
import type { ConfigSchema, PluginDescriptor } from './domain.js';
|
|
7
11
|
|
|
8
12
|
export type RegistryProviderCatalog = Record<
|
|
@@ -92,9 +96,7 @@ export function listApiKeyProvidersFromRegistry(
|
|
|
92
96
|
function pickDefaultModelValue(options: RegistryModelOption[]): string | undefined {
|
|
93
97
|
if (options.length === 0) return undefined;
|
|
94
98
|
const values = options.map((option) => option.value);
|
|
95
|
-
const preferred =
|
|
96
|
-
? values.find((value) => value.startsWith('openbot/'))
|
|
97
|
-
: values.find((value) => value.startsWith('openai/'));
|
|
99
|
+
const preferred = values.find((value) => value.startsWith('openai/'));
|
|
98
100
|
return preferred ?? values[0];
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -102,7 +104,7 @@ export function enrichOpenbotPluginDescriptor(
|
|
|
102
104
|
descriptor: PluginDescriptor,
|
|
103
105
|
modelOptions: RegistryModelOption[],
|
|
104
106
|
): PluginDescriptor {
|
|
105
|
-
if (descriptor.id !==
|
|
107
|
+
if (descriptor.id !== OPENBOT_PLUGIN_ID || !descriptor.configSchema) return descriptor;
|
|
106
108
|
|
|
107
109
|
const modelProperty = descriptor.configSchema.properties.model;
|
|
108
110
|
if (!modelProperty) return descriptor;
|
|
@@ -133,14 +135,44 @@ export function enrichOpenbotPluginDescriptor(
|
|
|
133
135
|
nextModelProperty.default = defaultModel;
|
|
134
136
|
}
|
|
135
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
|
+
|
|
136
171
|
return {
|
|
137
172
|
...descriptor,
|
|
138
173
|
configSchema: {
|
|
139
174
|
...descriptor.configSchema,
|
|
140
|
-
properties:
|
|
141
|
-
...descriptor.configSchema.properties,
|
|
142
|
-
model: nextModelProperty,
|
|
143
|
-
},
|
|
175
|
+
properties: nextProperties,
|
|
144
176
|
},
|
|
145
177
|
};
|
|
146
178
|
}
|
|
@@ -1,17 +1,10 @@
|
|
|
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 { todoPlugin } from '../../plugins/todo/index.js';
|
|
11
|
-
import { delegationPlugin } from '../../plugins/delegation/index.js';
|
|
12
|
-
import { uiPlugin } from '../../plugins/ui/index.js';
|
|
13
7
|
import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
|
|
14
|
-
import { previewPlugin } from '../../plugins/preview/index.js';
|
|
15
8
|
import { DEFAULT_PLUGINS_DIR, getBaseDir } from '../../app/config.js';
|
|
16
9
|
import {
|
|
17
10
|
invalidatePlugin as clearResolvedPluginEntry,
|
|
@@ -22,16 +15,8 @@ import {
|
|
|
22
15
|
let pluginsDir: string | null = null;
|
|
23
16
|
|
|
24
17
|
const BUILT_IN: Record<string, Plugin> = {
|
|
25
|
-
[openbotPlugin.id]: openbotPlugin,
|
|
26
|
-
[bashPlugin.id]: bashPlugin,
|
|
27
18
|
[storagePlugin.id]: storagePlugin,
|
|
28
|
-
[approvalPlugin.id]: approvalPlugin,
|
|
29
|
-
[memoryPlugin.id]: memoryPlugin,
|
|
30
|
-
[todoPlugin.id]: todoPlugin,
|
|
31
|
-
[delegationPlugin.id]: delegationPlugin,
|
|
32
|
-
[uiPlugin.id]: uiPlugin,
|
|
33
19
|
[pluginManagerPlugin.id]: pluginManagerPlugin,
|
|
34
|
-
[previewPlugin.id]: previewPlugin,
|
|
35
20
|
};
|
|
36
21
|
|
|
37
22
|
/**
|
|
@@ -81,16 +66,12 @@ export function initPlugins(dir?: string) {
|
|
|
81
66
|
|
|
82
67
|
/**
|
|
83
68
|
* Resolve a Plugin by id. The id is either:
|
|
84
|
-
* - a built-in id (e.g. "
|
|
85
|
-
* - an npm package name (e.g. "
|
|
69
|
+
* - a built-in id (e.g. "storage", "plugin-manager"), or
|
|
70
|
+
* - an npm package name (e.g. "@meetopenbot/openbot"),
|
|
86
71
|
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
87
72
|
*/
|
|
88
73
|
export async function resolvePlugin(id: string): Promise<Plugin | null> {
|
|
89
74
|
if (resolvedPluginCache.has(id)) return resolvedPluginCache.get(id)!;
|
|
90
|
-
if (BUILT_IN[id]) {
|
|
91
|
-
resolvedPluginCache.set(id, BUILT_IN[id]);
|
|
92
|
-
return BUILT_IN[id];
|
|
93
|
-
}
|
|
94
75
|
|
|
95
76
|
if (!pluginsDir) {
|
|
96
77
|
initPlugins();
|
|
@@ -99,29 +80,33 @@ export async function resolvePlugin(id: string): Promise<Plugin | null> {
|
|
|
99
80
|
|
|
100
81
|
const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
|
|
101
82
|
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (!parsed) {
|
|
111
|
-
console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
const plugin: Plugin = { id, ...parsed, name: parsed.name || id };
|
|
115
|
-
resolvedPluginCache.set(id, plugin);
|
|
116
|
-
if (!loadedCommunityPlugins.has(id)) {
|
|
117
|
-
console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
|
|
118
|
-
loadedCommunityPlugins.add(id);
|
|
119
|
-
}
|
|
120
|
-
return plugin;
|
|
121
|
-
} catch (e) {
|
|
122
|
-
console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
|
|
123
|
-
return null;
|
|
103
|
+
if (BUILT_IN[id]) {
|
|
104
|
+
resolvedPluginCache.set(id, BUILT_IN[id]);
|
|
105
|
+
return BUILT_IN[id];
|
|
124
106
|
}
|
|
107
|
+
|
|
108
|
+
console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
|
|
109
|
+
return null;
|
|
125
110
|
}
|
|
126
111
|
|
|
127
112
|
/** Drop a single id from the in-memory cache (e.g. after fresh install). */
|
|
@@ -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(
|
|
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) {
|
|
@@ -263,17 +265,13 @@ export const pluginService = {
|
|
|
263
265
|
console.warn(`[plugins] Failed to run npm install in ${finalPath}:`, e);
|
|
264
266
|
}
|
|
265
267
|
|
|
266
|
-
const pkgJson = JSON.parse(
|
|
267
|
-
await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'),
|
|
268
|
-
);
|
|
268
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'));
|
|
269
269
|
|
|
270
270
|
invalidatePlugin(packageName);
|
|
271
271
|
return { name: pkgJson.name, version: pkgJson.version };
|
|
272
272
|
} catch (error) {
|
|
273
273
|
console.error(`[plugins] Failed to install ${packageName}:`, error);
|
|
274
|
-
throw new Error(
|
|
275
|
-
`Failed to install plugin ${packageName}: ${(error as Error).message}`,
|
|
276
|
-
);
|
|
274
|
+
throw new Error(`Failed to install plugin ${packageName}: ${(error as Error).message}`);
|
|
277
275
|
} finally {
|
|
278
276
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
279
277
|
}
|
|
@@ -299,10 +297,7 @@ export const pluginService = {
|
|
|
299
297
|
}
|
|
300
298
|
} catch (error) {
|
|
301
299
|
console.error(`[plugins] Failed to uninstall ${packageName}:`, error);
|
|
302
|
-
throw new Error(
|
|
303
|
-
`Failed to uninstall plugin ${packageName}: ${(error as Error).message}`,
|
|
304
|
-
);
|
|
300
|
+
throw new Error(`Failed to uninstall plugin ${packageName}: ${(error as Error).message}`);
|
|
305
301
|
}
|
|
306
302
|
},
|
|
307
303
|
};
|
|
308
|
-
|
|
@@ -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. `
|
|
9
|
-
* package name (e.g.
|
|
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
|
/**
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import type { Plugin } from '../../services/plugins/types.js';
|
|
3
|
-
import { OpenBotEvent } from '../../app/types.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* `approval` — gates protected tool calls behind a UI confirmation widget.
|
|
7
|
-
*
|
|
8
|
-
* This is a simplified version that intercepts specified actions (default: bash)
|
|
9
|
-
* and requires user approval before they are allowed to proceed.
|
|
10
|
-
*/
|
|
11
|
-
|
|
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
|
|
15
|
-
|
|
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:shell_exec'];
|
|
23
|
-
|
|
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;
|
|
28
|
-
|
|
29
|
-
// Otherwise, intercept and ask for approval via a UI widget
|
|
30
|
-
const displayData = JSON.stringify((event as any)?.data) || '';
|
|
31
|
-
|
|
32
|
-
const widgetId = randomUUID();
|
|
33
|
-
pendingApprovals.set(widgetId, Date.now());
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
type: 'client:ui:widget',
|
|
37
|
-
data: {
|
|
38
|
-
widgetId,
|
|
39
|
-
kind: 'message',
|
|
40
|
-
title: `The agent wants to perform \`${action}\``,
|
|
41
|
-
body: displayData,
|
|
42
|
-
metadata: {
|
|
43
|
-
type: 'approval:request',
|
|
44
|
-
originalEvent: event,
|
|
45
|
-
},
|
|
46
|
-
actions: [
|
|
47
|
-
{ id: 'approve', label: 'Approve', variant: 'primary' },
|
|
48
|
-
{ id: 'deny', label: 'Deny', variant: 'danger' },
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
meta: { agentId: context.state.agentId, threadId: context.state.threadId },
|
|
52
|
-
} as OpenBotEvent;
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Handle the user's response from the UI widget
|
|
57
|
-
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
58
|
-
const { widgetId, actionId } = event.data;
|
|
59
|
-
const metadata = event.data?.metadata;
|
|
60
|
-
if (metadata?.type !== 'approval:request') return;
|
|
61
|
-
|
|
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
|
-
}
|
|
67
|
-
|
|
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}`);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
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;
|
|
98
|
-
|
|
99
|
-
if (approved) {
|
|
100
|
-
// Re-emit the original event with approved status so the actual handler can run
|
|
101
|
-
yield {
|
|
102
|
-
...originalEvent,
|
|
103
|
-
meta: {
|
|
104
|
-
...(originalEvent.meta || {}),
|
|
105
|
-
approvalStatus: 'approved',
|
|
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,
|
|
135
|
-
} as OpenBotEvent;
|
|
136
|
-
|
|
137
|
-
yield {
|
|
138
|
-
type: 'agent:output',
|
|
139
|
-
data: { content: `Action \`${originalEvent.type}\` was denied.` },
|
|
140
|
-
meta: { agentId: context.state.agentId },
|
|
141
|
-
} as OpenBotEvent;
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
export default approvalPlugin;
|