openbot 0.4.6 → 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.
- package/.claude/agents/runtime-security-reviewer.md +32 -0
- package/.claude/hooks/format.sh +18 -0
- package/.claude/hooks/protect-secrets.sh +23 -0
- package/.claude/settings.json +26 -0
- package/.claude/skills/deploy/SKILL.md +26 -0
- package/.claude/skills/new-plugin/SKILL.md +46 -0
- package/.dockerignore +9 -0
- package/.mcp.json +8 -0
- package/AGENTS.md +2 -2
- package/CLAUDE.md +50 -0
- package/Dockerfile +41 -0
- package/deploy/README.md +61 -0
- package/deploy/entrypoint.sh +6 -0
- package/deploy/fly.toml +36 -0
- package/dist/app/bootstrap.js +29 -0
- package/dist/app/cli.js +3 -1
- package/dist/app/cloud-mode.js +26 -0
- package/dist/app/config.js +61 -16
- package/dist/app/responding-agent.js +32 -0
- package/dist/app/server.js +47 -9
- package/dist/plugins/approval/index.js +1 -1
- package/dist/plugins/bash/index.js +386 -164
- package/dist/plugins/memory/service.js +2 -5
- package/dist/plugins/openbot/context.js +17 -1
- package/dist/plugins/openbot/index.js +25 -9
- package/dist/plugins/openbot/model.js +59 -0
- package/dist/plugins/openbot/runtime.js +39 -56
- package/dist/plugins/openbot/system-prompt.js +44 -9
- package/dist/plugins/preview/index.js +267 -0
- package/dist/plugins/storage/index.js +3 -3
- package/dist/plugins/storage/service.js +58 -12
- package/dist/plugins/todo/index.js +131 -38
- package/dist/plugins/todo/service.js +93 -0
- package/dist/plugins/ui/index.js +8 -1
- package/dist/services/plugins/model-registry.js +101 -0
- package/dist/services/plugins/registry.js +6 -4
- package/dist/services/plugins/service.js +2 -6
- package/docs/agents.md +20 -2
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +13 -3
- package/docs/templates/AGENT.example.md +1 -1
- package/package.json +5 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/app/bootstrap.ts +43 -0
- package/src/app/cli.ts +3 -1
- package/src/app/cloud-mode.ts +41 -0
- package/src/app/config.ts +71 -19
- package/src/app/responding-agent.ts +46 -0
- package/src/app/server.ts +55 -9
- package/src/app/types.ts +167 -31
- package/src/plugins/approval/index.ts +1 -1
- package/src/plugins/bash/index.ts +498 -185
- package/src/plugins/memory/service.ts +2 -5
- package/src/plugins/openbot/context.ts +17 -1
- package/src/plugins/openbot/index.ts +27 -11
- package/src/plugins/openbot/model.ts +76 -0
- package/src/plugins/openbot/runtime.ts +47 -69
- package/src/plugins/openbot/system-prompt.ts +44 -9
- package/src/plugins/preview/index.ts +323 -0
- package/src/plugins/storage/index.ts +5 -8
- package/src/plugins/storage/service.ts +79 -15
- package/src/plugins/todo/index.ts +166 -0
- package/src/plugins/todo/service.ts +123 -0
- package/src/plugins/ui/index.ts +8 -1
- package/src/services/plugins/domain.ts +2 -0
- package/src/services/plugins/model-registry.ts +146 -0
- package/src/services/plugins/registry.ts +6 -4
- package/src/services/plugins/service.ts +2 -6
- package/dist/agents/openbot/index.js +0 -76
- package/dist/agents/openbot/middleware/approval.js +0 -132
- package/dist/agents/openbot/runtime.js +0 -289
- package/dist/agents/openbot/system-prompt.js +0 -32
- package/dist/agents/openbot/tools/delegation.js +0 -78
- package/dist/agents/openbot/tools/mcp.js +0 -99
- package/dist/agents/openbot/tools/shell.js +0 -91
- package/dist/agents/openbot/tools/storage.js +0 -75
- package/dist/agents/openbot/tools/ui.js +0 -176
- package/dist/agents/system.js +0 -33
- package/dist/bus/agent-package.js +0 -1
- package/dist/bus/plugin.js +0 -1
- package/dist/bus/services.js +0 -754
- package/dist/bus/types.js +0 -1
- package/dist/harness/agent-harness.js +0 -45
- package/dist/harness/agent-invoke-run.js +0 -44
- package/dist/harness/agent-turn.js +0 -99
- package/dist/harness/channel-participants.js +0 -40
- package/dist/harness/constants.js +0 -2
- package/dist/harness/context-meter.js +0 -97
- package/dist/harness/context.js +0 -363
- package/dist/harness/dispatch.js +0 -144
- package/dist/harness/dispatcher.js +0 -156
- package/dist/harness/event-normalizer.js +0 -59
- package/dist/harness/history.js +0 -177
- package/dist/harness/mcp.js +0 -61
- package/dist/harness/orchestration.js +0 -88
- package/dist/harness/orchestrator.js +0 -149
- package/dist/harness/participants.js +0 -22
- package/dist/harness/process.js +0 -29
- package/dist/harness/queue-processor.js +0 -187
- package/dist/harness/run-harness.js +0 -154
- package/dist/harness/run.js +0 -98
- package/dist/harness/runtime-factory.js +0 -73
- package/dist/harness/runtime.js +0 -57
- package/dist/harness/todo-advance.js +0 -93
- package/dist/harness/todo-dispatch.js +0 -51
- package/dist/harness/todos.js +0 -5
- package/dist/harness/turn.js +0 -79
- package/dist/harness/types.js +0 -1
- package/dist/plugins/ai-sdk/index.js +0 -34
- package/dist/plugins/ai-sdk/runtime.js +0 -395
- package/dist/plugins/ai-sdk/system-prompt.js +0 -18
- package/dist/plugins/ai-sdk.js +0 -331
- package/dist/plugins/approval.js +0 -163
- package/dist/plugins/delegation.js +0 -108
- package/dist/plugins/mcp/index.js +0 -108
- package/dist/plugins/mcp.js +0 -140
- package/dist/plugins/shell/index.js +0 -100
- package/dist/plugins/shell.js +0 -123
- package/dist/plugins/storage-tools/index.js +0 -76
- package/dist/plugins/storage.js +0 -737
- package/dist/plugins/thread-namer/index.js +0 -72
- package/dist/plugins/threads/index.js +0 -114
- package/dist/plugins/ui.js +0 -211
- package/dist/plugins/workflow/index.js +0 -65
- package/dist/registry/agents.js +0 -138
- package/dist/registry/plugins.js +0 -110
- package/dist/services/agent-packages.js +0 -103
- package/dist/services/memory.js +0 -152
- package/dist/services/plugins.js +0 -98
- package/dist/services/storage.js +0 -1028
- package/dist/workflow/service.js +0 -106
- package/dist/workflow/types.js +0 -3
package/docs/plugins.md
CHANGED
|
@@ -39,6 +39,14 @@ same agent into a single map and passes it to every plugin via `context.tools`.
|
|
|
39
39
|
Runtime plugins read it; tool plugins ignore it. First plugin wins on tool
|
|
40
40
|
name collisions.
|
|
41
41
|
|
|
42
|
+
### Tool results
|
|
43
|
+
|
|
44
|
+
When a tool handler finishes, yield `action:<tool>:result` with
|
|
45
|
+
`data.output: string`. The OpenBot runtime feeds that string back into the
|
|
46
|
+
model as the tool result (`history.ts`). Structured fields (e.g. `list`,
|
|
47
|
+
`success`) are fine alongside it, but without `output` the model only sees
|
|
48
|
+
`"No output"`.
|
|
49
|
+
|
|
42
50
|
## Built-in plugins
|
|
43
51
|
|
|
44
52
|
| Id | Role | Notes |
|
|
@@ -49,6 +57,7 @@ name collisions.
|
|
|
49
57
|
| `bash` | Tool | `bash` (inbuilt in `openbot`) |
|
|
50
58
|
| `storage` | Tool | `create_channel`, `patch_*`, ... (inbuilt in `openbot`) |
|
|
51
59
|
| `memory` | Tool | `remember`, `recall`, `forget` (inbuilt in `openbot`) |
|
|
60
|
+
| `todo` | Tool | `todo_write`, `todo_read` (inbuilt in `openbot`) |
|
|
52
61
|
| `plugin-manager`| Infra | Marketplace list, npm plugin install/uninstall, agent install |
|
|
53
62
|
|
|
54
63
|
## Batteries-included: `openbot` runtime
|
|
@@ -58,6 +67,7 @@ to be isolated and self-contained, providing a core ecosystem of inbuilt tools:
|
|
|
58
67
|
|
|
59
68
|
- **Bash**: Stateful system tasks and file operations.
|
|
60
69
|
- **Memory**: Long-term durable fact storage.
|
|
70
|
+
- **Todo**: Thread-scoped multi-step task checklist.
|
|
61
71
|
- **Storage**: Channel and thread management.
|
|
62
72
|
- **Delegation**: Calling upon other specialized agents.
|
|
63
73
|
- **Approval**: Gating protected actions behind UI confirmation.
|
|
@@ -71,7 +81,7 @@ plugins:
|
|
|
71
81
|
config:
|
|
72
82
|
model: openai/gpt-4o-mini
|
|
73
83
|
approval:
|
|
74
|
-
actions: [action:
|
|
84
|
+
actions: [action:shell_exec, action:create_channel]
|
|
75
85
|
```
|
|
76
86
|
|
|
77
87
|
A community plugin is just an npm package whose default export matches the
|
|
@@ -90,11 +100,11 @@ On first use OpenBot installs the package into
|
|
|
90
100
|
|
|
91
101
|
## Approval plugin
|
|
92
102
|
|
|
93
|
-
The `approval` plugin gates protected tool calls behind a UI confirmation widget. By default, it gates `action:
|
|
103
|
+
The `approval` plugin gates protected tool calls behind a UI confirmation widget. By default, it gates `action:shell_exec`.
|
|
94
104
|
|
|
95
105
|
```yaml
|
|
96
106
|
plugins:
|
|
97
107
|
- id: approval
|
|
98
108
|
config:
|
|
99
|
-
actions: [action:
|
|
109
|
+
actions: [action:shell_exec]
|
|
100
110
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openbot",
|
|
3
|
-
"version": "0.4
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "tsx watch src/app/cli.ts start",
|
|
11
11
|
"build": "tsc && mkdir -p dist/assets && cp src/assets/icon.svg dist/assets/icon.svg",
|
|
12
|
-
"start": "node dist/app/cli.js start"
|
|
12
|
+
"start": "node dist/app/cli.js start",
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"push": "fly deploy --build-only --push --config deploy/fly.toml -a openbot-runtime --image-label v0.5.4"
|
|
13
15
|
},
|
|
14
16
|
"bin": {
|
|
15
17
|
"openbot": "./dist/app/cli.js"
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"@types/express": "^4.17.21",
|
|
35
37
|
"@types/js-yaml": "^4.0.9",
|
|
36
38
|
"@types/node": "^20.10.1",
|
|
39
|
+
"prettier": "^3.4.2",
|
|
37
40
|
"tsx": "^4.21.0",
|
|
38
41
|
"typescript": "^5.9.3"
|
|
39
42
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
CONFIG_FILE,
|
|
5
|
+
DEFAULT_AGENTS_DIR,
|
|
6
|
+
DEFAULT_CHANNELS_DIR,
|
|
7
|
+
DEFAULT_PLUGINS_DIR,
|
|
8
|
+
getBaseDir,
|
|
9
|
+
getChannelsWorkspaceRoot,
|
|
10
|
+
getConfigDir,
|
|
11
|
+
type OpenBotconfig,
|
|
12
|
+
} from './config.js';
|
|
13
|
+
|
|
14
|
+
/** Ensure data directories and a minimal config exist (first boot on a Fly volume). */
|
|
15
|
+
export async function bootstrap(): Promise<void> {
|
|
16
|
+
const baseDir = getBaseDir();
|
|
17
|
+
const configDir = getConfigDir();
|
|
18
|
+
|
|
19
|
+
await fs.mkdir(path.join(baseDir, DEFAULT_AGENTS_DIR), { recursive: true });
|
|
20
|
+
await fs.mkdir(path.join(baseDir, DEFAULT_PLUGINS_DIR), { recursive: true });
|
|
21
|
+
await fs.mkdir(path.join(baseDir, DEFAULT_CHANNELS_DIR), { recursive: true });
|
|
22
|
+
await fs.mkdir(getChannelsWorkspaceRoot(), { recursive: true });
|
|
23
|
+
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
24
|
+
|
|
25
|
+
const configPath = path.join(configDir, CONFIG_FILE);
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(configPath);
|
|
28
|
+
} catch {
|
|
29
|
+
const initial: OpenBotconfig = { baseDir };
|
|
30
|
+
|
|
31
|
+
const publicUrl = process.env.OPENBOT_PUBLIC_URL?.trim();
|
|
32
|
+
if (publicUrl) {
|
|
33
|
+
initial.publicUrl = publicUrl.replace(/\/$/, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const workspaceDir = process.env.OPENBOT_CHANNELS_WORKSPACE_DIR?.trim();
|
|
37
|
+
if (workspaceDir) {
|
|
38
|
+
initial.channelsWorkspaceDir = getChannelsWorkspaceRoot();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await fs.writeFile(configPath, JSON.stringify(initial, null, 2), { mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/app/cli.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import { bootstrap } from './bootstrap.js';
|
|
3
4
|
import { startServer } from './server.js';
|
|
4
5
|
|
|
5
6
|
const program = new Command();
|
|
@@ -25,13 +26,14 @@ function checkNodeVersion() {
|
|
|
25
26
|
|
|
26
27
|
checkNodeVersion();
|
|
27
28
|
|
|
28
|
-
program.name('openbot').description('OpenBot CLI').version('0.4
|
|
29
|
+
program.name('openbot').description('OpenBot CLI').version('0.5.4');
|
|
29
30
|
|
|
30
31
|
program
|
|
31
32
|
.command('start')
|
|
32
33
|
.description('Start the OpenBot harness')
|
|
33
34
|
.option('-p, --port <number>', 'Port to listen on')
|
|
34
35
|
.action(async (options) => {
|
|
36
|
+
await bootstrap();
|
|
35
37
|
await startServer(options);
|
|
36
38
|
});
|
|
37
39
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ORCHESTRATOR_AGENT_ID } from './agent-ids.js';
|
|
2
|
+
|
|
3
|
+
/** Platform-managed OpenBot (Fly + integrations proxy). Self-hosted when unset. */
|
|
4
|
+
export const isCloudMode = (): boolean => process.env.OPENBOT_CLOUD_MODE === '1';
|
|
5
|
+
|
|
6
|
+
/** Platform-managed coordinator model for cloud workspaces. */
|
|
7
|
+
export const COORDINATOR_MODEL = 'openbot/coordinator-1';
|
|
8
|
+
|
|
9
|
+
/** Default cloud system agent model. */
|
|
10
|
+
export const CLOUD_SYSTEM_MODEL = COORDINATOR_MODEL;
|
|
11
|
+
|
|
12
|
+
export interface CloudIntegrationsConfig {
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
token: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getCloudIntegrationsConfig(): CloudIntegrationsConfig | null {
|
|
18
|
+
if (!isCloudMode()) return null;
|
|
19
|
+
|
|
20
|
+
const baseUrl = process.env.OPENBOT_INTEGRATIONS_BASE_URL?.trim();
|
|
21
|
+
const token = process.env.OPENBOT_INTEGRATIONS_TOKEN?.trim();
|
|
22
|
+
if (!baseUrl || !token) return null;
|
|
23
|
+
|
|
24
|
+
return { baseUrl: baseUrl.replace(/\/$/, ''), token };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isCloudSystemAgent(agentId: string): boolean {
|
|
28
|
+
return isCloudMode() && agentId === ORCHESTRATOR_AGENT_ID;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveCloudSystemModel(configModel: string | undefined): string {
|
|
32
|
+
return configModel?.trim() || COORDINATOR_MODEL;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function assertCloudSystemAgentPluginsMutable(
|
|
36
|
+
agentId: string,
|
|
37
|
+
plugins: unknown,
|
|
38
|
+
): void {
|
|
39
|
+
void agentId;
|
|
40
|
+
void plugins;
|
|
41
|
+
}
|
package/src/app/config.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface OpenBotconfig {
|
|
|
8
8
|
model?: string;
|
|
9
9
|
image?: string;
|
|
10
10
|
baseDir?: string;
|
|
11
|
+
/** Parent directory for per-channel working directories (user-facing workspace). */
|
|
12
|
+
channelsWorkspaceDir?: string;
|
|
11
13
|
port?: number;
|
|
12
14
|
/**
|
|
13
15
|
* Overrides the default public marketplace registry URL. If omitted or blank,
|
|
@@ -41,52 +43,102 @@ export function resolvePath(p: string) {
|
|
|
41
43
|
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : path.resolve(p);
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
function readConfigFile(configDir: string): OpenBotconfig {
|
|
47
|
+
const configPath = path.join(configDir, CONFIG_FILE);
|
|
48
|
+
if (fs.existsSync(configPath)) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8')) as OpenBotconfig;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`Warning: Failed to parse config at ${configPath}`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Directory that holds `config.json` (env `OPENBOT_BASE_DIR` or `~/.openbot`). */
|
|
59
|
+
export function getConfigDir(): string {
|
|
60
|
+
const envBase = process.env.OPENBOT_BASE_DIR?.trim();
|
|
61
|
+
if (envBase) {
|
|
62
|
+
return resolvePath(envBase);
|
|
63
|
+
}
|
|
64
|
+
return path.join(os.homedir(), '.openbot');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Resolved OpenBot data root (agents, channels, plugins, variables). */
|
|
68
|
+
export function getBaseDir(): string {
|
|
69
|
+
const envBase = process.env.OPENBOT_BASE_DIR?.trim();
|
|
70
|
+
if (envBase) {
|
|
71
|
+
return resolvePath(envBase);
|
|
72
|
+
}
|
|
73
|
+
const fileConfig = readConfigFile(getConfigDir());
|
|
74
|
+
return resolvePath(fileConfig.baseDir || DEFAULT_BASE_DIR);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Resolved parent directory for per-channel workspace folders. */
|
|
78
|
+
export function getChannelsWorkspaceRoot(): string {
|
|
79
|
+
const env = process.env.OPENBOT_CHANNELS_WORKSPACE_DIR?.trim();
|
|
80
|
+
if (env) {
|
|
81
|
+
return resolvePath(env);
|
|
82
|
+
}
|
|
83
|
+
const fileConfig = readConfigFile(getConfigDir());
|
|
84
|
+
if (fileConfig.channelsWorkspaceDir) {
|
|
85
|
+
return resolvePath(fileConfig.channelsWorkspaceDir);
|
|
86
|
+
}
|
|
87
|
+
return resolvePath(DEFAULT_CHANNELS_WORKSPACE_DIR);
|
|
88
|
+
}
|
|
89
|
+
|
|
44
90
|
/** Default absolute cwd for a channel when none is provided at creation time. */
|
|
45
91
|
export function getDefaultChannelCwd(channelId: string): string {
|
|
46
92
|
const id = channelId.trim();
|
|
47
93
|
if (!id) {
|
|
48
94
|
throw new Error('channelId is required');
|
|
49
95
|
}
|
|
50
|
-
return
|
|
96
|
+
return path.join(getChannelsWorkspaceRoot(), id);
|
|
51
97
|
}
|
|
52
98
|
|
|
53
99
|
export function loadConfig(): OpenBotconfig {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
const fileConfig = readConfigFile(getConfigDir());
|
|
101
|
+
const merged: OpenBotconfig = { ...fileConfig };
|
|
102
|
+
|
|
103
|
+
merged.baseDir = getBaseDir();
|
|
104
|
+
|
|
105
|
+
const envPublicUrl = process.env.OPENBOT_PUBLIC_URL?.trim();
|
|
106
|
+
if (envPublicUrl) {
|
|
107
|
+
merged.publicUrl = envPublicUrl.replace(/\/$/, '');
|
|
108
|
+
} else if (merged.publicUrl) {
|
|
109
|
+
merged.publicUrl = merged.publicUrl.replace(/\/$/, '');
|
|
61
110
|
}
|
|
62
|
-
|
|
111
|
+
|
|
112
|
+
merged.channelsWorkspaceDir = getChannelsWorkspaceRoot();
|
|
113
|
+
|
|
114
|
+
const envPort = process.env.PORT?.trim();
|
|
115
|
+
if (envPort && !Number.isNaN(Number(envPort))) {
|
|
116
|
+
merged.port = Number(envPort);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return merged;
|
|
63
120
|
}
|
|
64
121
|
|
|
65
122
|
export function saveConfig(config: Partial<OpenBotconfig>) {
|
|
66
|
-
const configDir =
|
|
123
|
+
const configDir = getConfigDir();
|
|
67
124
|
const configPath = path.join(configDir, CONFIG_FILE);
|
|
68
125
|
|
|
69
126
|
if (!fs.existsSync(configDir)) {
|
|
70
127
|
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
71
128
|
}
|
|
72
129
|
|
|
73
|
-
const currentConfig =
|
|
130
|
+
const currentConfig = readConfigFile(configDir);
|
|
74
131
|
const newConfig = { ...currentConfig, ...config };
|
|
75
132
|
|
|
76
133
|
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
|
|
77
134
|
}
|
|
78
135
|
|
|
79
136
|
export function isConfigured(): boolean {
|
|
80
|
-
|
|
81
|
-
return fs.existsSync(configPath);
|
|
137
|
+
return fs.existsSync(path.join(getConfigDir(), CONFIG_FILE));
|
|
82
138
|
}
|
|
83
139
|
|
|
84
140
|
export function loadVariables(): { version: number; variables: StoredVariable[] } {
|
|
85
|
-
const
|
|
86
|
-
const variablesPath = path.join(
|
|
87
|
-
resolvePath(config.baseDir || DEFAULT_BASE_DIR),
|
|
88
|
-
VARIABLES_FILE,
|
|
89
|
-
);
|
|
141
|
+
const variablesPath = path.join(getBaseDir(), VARIABLES_FILE);
|
|
90
142
|
if (fs.existsSync(variablesPath)) {
|
|
91
143
|
return JSON.parse(fs.readFileSync(variablesPath, 'utf-8')) as {
|
|
92
144
|
version: number;
|
|
@@ -94,4 +146,4 @@ export function loadVariables(): { version: number; variables: StoredVariable[]
|
|
|
94
146
|
};
|
|
95
147
|
}
|
|
96
148
|
return { version: 1, variables: [] };
|
|
97
|
-
}
|
|
149
|
+
}
|
|
@@ -4,6 +4,9 @@ import { storageService } from '../plugins/storage/service.js';
|
|
|
4
4
|
/** Thread `state.json` key for the sticky responding agent id. */
|
|
5
5
|
export const THREAD_RESPONDING_AGENT_ID_KEY = 'respondingAgentId';
|
|
6
6
|
|
|
7
|
+
/** Publish events that continue a pending UI interaction rather than a new user turn. */
|
|
8
|
+
export const CONTINUATION_EVENT_TYPES = new Set(['client:ui:widget:response']);
|
|
9
|
+
|
|
7
10
|
export type ResolveRespondingAgentOptions = {
|
|
8
11
|
channelId: string;
|
|
9
12
|
threadId?: string;
|
|
@@ -20,6 +23,14 @@ export type ResolveRespondingAgentResult = {
|
|
|
20
23
|
overridden: boolean;
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
const readMetaAgentId = (meta: unknown): string | undefined => {
|
|
27
|
+
if (!meta || typeof meta !== 'object') return undefined;
|
|
28
|
+
const value = (meta as Record<string, unknown>).agentId;
|
|
29
|
+
if (typeof value !== 'string') return undefined;
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
return trimmed || undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
23
34
|
const readBoundAgentId = (state: unknown): string | undefined => {
|
|
24
35
|
if (!state || typeof state !== 'object') return undefined;
|
|
25
36
|
const value = (state as Record<string, unknown>)[THREAD_RESPONDING_AGENT_ID_KEY];
|
|
@@ -72,3 +83,38 @@ export async function resolveRespondingAgentId(
|
|
|
72
83
|
|
|
73
84
|
return { agentId: fallback, bound: true, overridden: false };
|
|
74
85
|
}
|
|
86
|
+
|
|
87
|
+
export type ResolvePublishTargetAgentOptions = {
|
|
88
|
+
eventType: string;
|
|
89
|
+
channelId: string;
|
|
90
|
+
threadId?: string;
|
|
91
|
+
requestedAgentId?: string;
|
|
92
|
+
eventMeta?: unknown;
|
|
93
|
+
bindIfUnbound?: boolean;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolves the agent that should handle a publish event.
|
|
98
|
+
* UI continuations route to the widget origin agent; everything else uses sticky thread binding.
|
|
99
|
+
*/
|
|
100
|
+
export async function resolvePublishTargetAgentId(
|
|
101
|
+
options: ResolvePublishTargetAgentOptions,
|
|
102
|
+
): Promise<{ agentId: string } | { error: 'agentId_required' }> {
|
|
103
|
+
const { eventType, channelId, threadId, requestedAgentId, eventMeta, bindIfUnbound } = options;
|
|
104
|
+
|
|
105
|
+
if (CONTINUATION_EVENT_TYPES.has(eventType)) {
|
|
106
|
+
const originAgentId = readMetaAgentId(eventMeta) || requestedAgentId?.trim();
|
|
107
|
+
if (!originAgentId) {
|
|
108
|
+
return { error: 'agentId_required' };
|
|
109
|
+
}
|
|
110
|
+
return { agentId: originAgentId };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const resolved = await resolveRespondingAgentId({
|
|
114
|
+
channelId,
|
|
115
|
+
threadId,
|
|
116
|
+
requestedAgentId,
|
|
117
|
+
bindIfUnbound,
|
|
118
|
+
});
|
|
119
|
+
return { agentId: resolved.agentId };
|
|
120
|
+
}
|
package/src/app/server.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { createRequire } from 'module';
|
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
const pkg = require('../../package.json');
|
|
10
10
|
import { generateId } from 'melony';
|
|
11
|
-
import {
|
|
11
|
+
import { getBaseDir, loadConfig } from '../app/config.js';
|
|
12
|
+
import { isCloudMode, getCloudIntegrationsConfig } from './cloud-mode.js';
|
|
12
13
|
import { ActiveRunsSnapshotEvent, OpenBotEvent, OpenBotState } from './types.js';
|
|
13
14
|
import { processService } from '../services/process.js';
|
|
14
15
|
import { runAgent, STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID } from '../harness/index.js';
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
} from '../plugins/storage/files.js';
|
|
22
23
|
import { ensureEventId, openBotEventFromQuery } from './utils.js';
|
|
23
24
|
import { abortRegistry, abortKey } from '../services/abort.js';
|
|
24
|
-
import { resolveRespondingAgentId } from './responding-agent.js';
|
|
25
|
+
import { resolvePublishTargetAgentId, resolveRespondingAgentId } from './responding-agent.js';
|
|
25
26
|
import {
|
|
26
27
|
DEFAULT_UNCATEGORIZED_SPEC,
|
|
27
28
|
UNCATEGORIZED_CHANNEL_ID,
|
|
@@ -46,8 +47,14 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
46
47
|
const config = loadConfig();
|
|
47
48
|
processService.syncWorkspaceVariablesToProcessEnv();
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if (isCloudMode()) {
|
|
51
|
+
const integrations = getCloudIntegrationsConfig();
|
|
52
|
+
console.log(
|
|
53
|
+
`[server] Cloud mode enabled${integrations ? '' : ' (integrations proxy env not set)'}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const openBotDir = getBaseDir();
|
|
51
58
|
const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4132);
|
|
52
59
|
const app = express();
|
|
53
60
|
const clients: Map<string, express.Response[]> = new Map();
|
|
@@ -247,6 +254,23 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
247
254
|
|
|
248
255
|
app.use(cors());
|
|
249
256
|
|
|
257
|
+
const gatewayToken = process.env.OPENBOT_GATEWAY_TOKEN?.trim();
|
|
258
|
+
if (gatewayToken) {
|
|
259
|
+
app.use((req, res, next) => {
|
|
260
|
+
if (req.path === '/api/health' || req.method === 'OPTIONS') {
|
|
261
|
+
next();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const got = req.get('x-openbot-gateway-token');
|
|
266
|
+
if (got !== gatewayToken) {
|
|
267
|
+
res.status(401).json({ error: 'unauthorized' });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
next();
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
250
274
|
const resolvePublicBaseUrl = () => getPublicBaseUrl(PORT, config.publicUrl);
|
|
251
275
|
|
|
252
276
|
app.use((req, res, next) => {
|
|
@@ -264,7 +288,7 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
264
288
|
});
|
|
265
289
|
|
|
266
290
|
app.get('/api/health', (req, res) => {
|
|
267
|
-
res.json({ status: 'ok', version: pkg.version });
|
|
291
|
+
res.json({ status: 'ok', version: pkg.version, apiVersion: 1 });
|
|
268
292
|
});
|
|
269
293
|
|
|
270
294
|
app.get('/api/events', (req, res) => {
|
|
@@ -561,16 +585,23 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
561
585
|
}
|
|
562
586
|
|
|
563
587
|
const bindIfUnbound = event.type === 'agent:invoke';
|
|
564
|
-
const
|
|
588
|
+
const target = await resolvePublishTargetAgentId({
|
|
589
|
+
eventType: event.type,
|
|
565
590
|
channelId,
|
|
566
591
|
threadId,
|
|
567
592
|
requestedAgentId: agentId,
|
|
593
|
+
eventMeta: event.meta,
|
|
568
594
|
bindIfUnbound,
|
|
569
595
|
});
|
|
570
596
|
|
|
597
|
+
if ('error' in target) {
|
|
598
|
+
res.status(400).json({ error: 'agentId is required for widget response' });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
571
602
|
await runAgent({
|
|
572
603
|
runId,
|
|
573
|
-
agentId:
|
|
604
|
+
agentId: target.agentId,
|
|
574
605
|
event,
|
|
575
606
|
channelId,
|
|
576
607
|
threadId,
|
|
@@ -676,10 +707,25 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
676
707
|
}
|
|
677
708
|
});
|
|
678
709
|
|
|
679
|
-
|
|
680
|
-
|
|
710
|
+
const HOST = process.env.HOST || '0.0.0.0';
|
|
711
|
+
const server = app.listen(PORT, HOST, () => {
|
|
712
|
+
console.log(`\x1b[32mOpenBot server listening at http://${HOST}:${PORT}\x1b[0m`);
|
|
681
713
|
console.log(
|
|
682
714
|
`🌐 Visit \x1b[96m\x1b[1mhttps://openbot.one\x1b[0m to connect to this runtime and manage everything from there. ✨`,
|
|
683
715
|
);
|
|
684
716
|
});
|
|
717
|
+
|
|
718
|
+
const shutdown = (signal: string) => {
|
|
719
|
+
console.log(`\n[server] Received ${signal}, shutting down...`);
|
|
720
|
+
server.close(() => {
|
|
721
|
+
process.exit(0);
|
|
722
|
+
});
|
|
723
|
+
setTimeout(() => {
|
|
724
|
+
console.error('[server] Forced shutdown after timeout');
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}, 10_000).unref();
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
730
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
685
731
|
}
|