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.
Files changed (132) 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/responding-agent.js +32 -0
  20. package/dist/app/server.js +47 -9
  21. package/dist/plugins/approval/index.js +1 -1
  22. package/dist/plugins/bash/index.js +386 -164
  23. package/dist/plugins/memory/service.js +2 -5
  24. package/dist/plugins/openbot/context.js +17 -1
  25. package/dist/plugins/openbot/index.js +25 -9
  26. package/dist/plugins/openbot/model.js +59 -0
  27. package/dist/plugins/openbot/runtime.js +39 -56
  28. package/dist/plugins/openbot/system-prompt.js +44 -9
  29. package/dist/plugins/preview/index.js +267 -0
  30. package/dist/plugins/storage/index.js +3 -3
  31. package/dist/plugins/storage/service.js +58 -12
  32. package/dist/plugins/todo/index.js +131 -38
  33. package/dist/plugins/todo/service.js +93 -0
  34. package/dist/plugins/ui/index.js +8 -1
  35. package/dist/services/plugins/model-registry.js +101 -0
  36. package/dist/services/plugins/registry.js +6 -4
  37. package/dist/services/plugins/service.js +2 -6
  38. package/docs/agents.md +20 -2
  39. package/docs/architecture.md +1 -1
  40. package/docs/plugins.md +13 -3
  41. package/docs/templates/AGENT.example.md +1 -1
  42. package/package.json +5 -2
  43. package/pnpm-workspace.yaml +2 -0
  44. package/src/app/bootstrap.ts +43 -0
  45. package/src/app/cli.ts +3 -1
  46. package/src/app/cloud-mode.ts +41 -0
  47. package/src/app/config.ts +71 -19
  48. package/src/app/responding-agent.ts +46 -0
  49. package/src/app/server.ts +55 -9
  50. package/src/app/types.ts +167 -31
  51. package/src/plugins/approval/index.ts +1 -1
  52. package/src/plugins/bash/index.ts +498 -185
  53. package/src/plugins/memory/service.ts +2 -5
  54. package/src/plugins/openbot/context.ts +17 -1
  55. package/src/plugins/openbot/index.ts +27 -11
  56. package/src/plugins/openbot/model.ts +76 -0
  57. package/src/plugins/openbot/runtime.ts +47 -69
  58. package/src/plugins/openbot/system-prompt.ts +44 -9
  59. package/src/plugins/preview/index.ts +323 -0
  60. package/src/plugins/storage/index.ts +5 -8
  61. package/src/plugins/storage/service.ts +79 -15
  62. package/src/plugins/todo/index.ts +166 -0
  63. package/src/plugins/todo/service.ts +123 -0
  64. package/src/plugins/ui/index.ts +8 -1
  65. package/src/services/plugins/domain.ts +2 -0
  66. package/src/services/plugins/model-registry.ts +146 -0
  67. package/src/services/plugins/registry.ts +6 -4
  68. package/src/services/plugins/service.ts +2 -6
  69. package/dist/agents/openbot/index.js +0 -76
  70. package/dist/agents/openbot/middleware/approval.js +0 -132
  71. package/dist/agents/openbot/runtime.js +0 -289
  72. package/dist/agents/openbot/system-prompt.js +0 -32
  73. package/dist/agents/openbot/tools/delegation.js +0 -78
  74. package/dist/agents/openbot/tools/mcp.js +0 -99
  75. package/dist/agents/openbot/tools/shell.js +0 -91
  76. package/dist/agents/openbot/tools/storage.js +0 -75
  77. package/dist/agents/openbot/tools/ui.js +0 -176
  78. package/dist/agents/system.js +0 -33
  79. package/dist/bus/agent-package.js +0 -1
  80. package/dist/bus/plugin.js +0 -1
  81. package/dist/bus/services.js +0 -754
  82. package/dist/bus/types.js +0 -1
  83. package/dist/harness/agent-harness.js +0 -45
  84. package/dist/harness/agent-invoke-run.js +0 -44
  85. package/dist/harness/agent-turn.js +0 -99
  86. package/dist/harness/channel-participants.js +0 -40
  87. package/dist/harness/constants.js +0 -2
  88. package/dist/harness/context-meter.js +0 -97
  89. package/dist/harness/context.js +0 -363
  90. package/dist/harness/dispatch.js +0 -144
  91. package/dist/harness/dispatcher.js +0 -156
  92. package/dist/harness/event-normalizer.js +0 -59
  93. package/dist/harness/history.js +0 -177
  94. package/dist/harness/mcp.js +0 -61
  95. package/dist/harness/orchestration.js +0 -88
  96. package/dist/harness/orchestrator.js +0 -149
  97. package/dist/harness/participants.js +0 -22
  98. package/dist/harness/process.js +0 -29
  99. package/dist/harness/queue-processor.js +0 -187
  100. package/dist/harness/run-harness.js +0 -154
  101. package/dist/harness/run.js +0 -98
  102. package/dist/harness/runtime-factory.js +0 -73
  103. package/dist/harness/runtime.js +0 -57
  104. package/dist/harness/todo-advance.js +0 -93
  105. package/dist/harness/todo-dispatch.js +0 -51
  106. package/dist/harness/todos.js +0 -5
  107. package/dist/harness/turn.js +0 -79
  108. package/dist/harness/types.js +0 -1
  109. package/dist/plugins/ai-sdk/index.js +0 -34
  110. package/dist/plugins/ai-sdk/runtime.js +0 -395
  111. package/dist/plugins/ai-sdk/system-prompt.js +0 -18
  112. package/dist/plugins/ai-sdk.js +0 -331
  113. package/dist/plugins/approval.js +0 -163
  114. package/dist/plugins/delegation.js +0 -108
  115. package/dist/plugins/mcp/index.js +0 -108
  116. package/dist/plugins/mcp.js +0 -140
  117. package/dist/plugins/shell/index.js +0 -100
  118. package/dist/plugins/shell.js +0 -123
  119. package/dist/plugins/storage-tools/index.js +0 -76
  120. package/dist/plugins/storage.js +0 -737
  121. package/dist/plugins/thread-namer/index.js +0 -72
  122. package/dist/plugins/threads/index.js +0 -114
  123. package/dist/plugins/ui.js +0 -211
  124. package/dist/plugins/workflow/index.js +0 -65
  125. package/dist/registry/agents.js +0 -138
  126. package/dist/registry/plugins.js +0 -110
  127. package/dist/services/agent-packages.js +0 -103
  128. package/dist/services/memory.js +0 -152
  129. package/dist/services/plugins.js +0 -98
  130. package/dist/services/storage.js +0 -1028
  131. package/dist/workflow/service.js +0 -106
  132. 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:bash, action:create_channel]
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:bash`.
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:bash]
109
+ actions: [action:shell_exec]
100
110
  ```
@@ -28,7 +28,7 @@ plugins:
28
28
  - id: storage
29
29
  - id: approval
30
30
  config:
31
- actions: [action:bash]
31
+ actions: [action:shell_exec]
32
32
  ---
33
33
 
34
34
  <!--
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.4.6",
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,2 @@
1
+ allowBuilds:
2
+ esbuild: true
@@ -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.6');
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 resolvePath(`${DEFAULT_CHANNELS_WORKSPACE_DIR}/${id}`);
96
+ return path.join(getChannelsWorkspaceRoot(), id);
51
97
  }
52
98
 
53
99
  export function loadConfig(): OpenBotconfig {
54
- const configPath = path.join(os.homedir(), '.openbot', CONFIG_FILE);
55
- if (fs.existsSync(configPath)) {
56
- try {
57
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
58
- } catch (error) {
59
- console.error(`Warning: Failed to parse config at ${configPath}`, error);
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
- return {};
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 = resolvePath(DEFAULT_BASE_DIR);
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 = loadConfig();
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
- const configPath = path.join(resolvePath(DEFAULT_BASE_DIR), CONFIG_FILE);
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 config = loadConfig();
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 { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
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
- const baseDir = config.baseDir || DEFAULT_BASE_DIR;
50
- const openBotDir = resolvePath(baseDir);
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 resolved = await resolveRespondingAgentId({
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: resolved.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
- app.listen(PORT, () => {
680
- console.log(`\x1b[32mOpenBot server listening at http://localhost:${PORT}\x1b[0m`);
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
  }