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
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
  ```
@@ -8,27 +8,21 @@ name: Example Agent
8
8
  description: One-line description shown in agent pickers and lists.
9
9
 
10
10
  # Plugins compose the agent. Order matters for tool collisions (first wins).
11
- # At least one plugin must handle `agent:invoke` (a "runtime" plugin like
12
- # `openbot`, `claude-code`, or `gemini-cli`). Tool plugins like `bash`,
13
- # `delegation`, and `storage-tools` contribute tools to whichever runtime
14
- # plugin can consume them.
11
+ # At least one plugin must handle `agent:invoke`. Use the monolithic runtime:
12
+ # `@meetopenbot/openbot` (batteries-included: shell, memory, todo, storage tools,
13
+ # delegation, approval, preview).
15
14
  #
16
- # Built-in plugin ids: openbot, claude-code, gemini-cli, bash, delegation,
17
- # storage-tools, approval.
15
+ # Core built-in plugin ids: storage, plugin-manager.
18
16
  #
19
17
  # Community plugins are referenced by their npm package name (e.g.
20
- # `openbot-plugin-search` or `@scope/openbot-plugin-foo`) and are auto-installed
18
+ # `openbot-plugin-search` or `@meetopenbot/openbot`) and are auto-installed
21
19
  # on first use into ~/.openbot/plugins/<id>/.
22
20
  plugins:
23
- - id: openbot
21
+ - id: '@meetopenbot/openbot'
24
22
  config:
25
23
  model: openai/gpt-4o-mini
26
- - id: bash
27
- - id: delegation
28
- - id: storage
29
- - id: approval
30
- config:
31
- actions: [action:bash]
24
+ approval:
25
+ actions: [action:shell_exec]
32
26
  ---
33
27
 
34
28
  <!--
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.4.7",
3
+ "version": "0.5.5",
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.5"
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,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
+ import { bootstrap } from './bootstrap.js';
4
+ import { ensureDefaultStack } from './ensure-default-stack.js';
3
5
  import { startServer } from './server.js';
4
6
 
5
7
  const program = new Command();
@@ -25,13 +27,15 @@ function checkNodeVersion() {
25
27
 
26
28
  checkNodeVersion();
27
29
 
28
- program.name('openbot').description('OpenBot CLI').version('0.4.7');
30
+ program.name('openbot').description('OpenBot CLI').version('0.5.5');
29
31
 
30
32
  program
31
33
  .command('start')
32
34
  .description('Start the OpenBot harness')
33
35
  .option('-p, --port <number>', 'Port to listen on')
34
36
  .action(async (options) => {
37
+ await bootstrap();
38
+ await ensureDefaultStack();
35
39
  await startServer(options);
36
40
  });
37
41
 
@@ -0,0 +1,26 @@
1
+ import { ORCHESTRATOR_AGENT_ID } from './agent-ids.js';
2
+
3
+ /** Platform-managed OpenBot (Fly). Self-hosted when unset. */
4
+ export const isCloudMode = (): boolean => process.env.OPENBOT_CLOUD_MODE === '1';
5
+
6
+ /** How the cloud system agent authenticates to LLM providers. */
7
+ export type OpenbotAuthMode = 'credits' | 'byok';
8
+
9
+ /** Default cloud openbot plugin auth mode. */
10
+ export const DEFAULT_CLOUD_OPENBOT_AUTH_MODE: OpenbotAuthMode = 'credits';
11
+
12
+ export function isCloudSystemAgent(agentId: string): boolean {
13
+ return isCloudMode() && agentId === ORCHESTRATOR_AGENT_ID;
14
+ }
15
+
16
+ export function parseOpenbotAuthMode(value: unknown): OpenbotAuthMode {
17
+ return value === 'byok' ? 'byok' : 'credits';
18
+ }
19
+
20
+ export function assertCloudSystemAgentPluginsMutable(
21
+ agentId: string,
22
+ plugins: unknown,
23
+ ): void {
24
+ void agentId;
25
+ void plugins;
26
+ }
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
+ }
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import matter from 'gray-matter';
5
+ import { ORCHESTRATOR_AGENT_ID } from './agent-ids.js';
6
+ import { DEFAULT_SYSTEM_AGENT_MODEL, OPENBOT_PLUGIN_ID } from './openbot-plugin.js';
7
+ import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode } from './cloud-mode.js';
8
+ import { DEFAULT_AGENTS_DIR, getBaseDir } from './config.js';
9
+ import { pluginService } from '../services/plugins/service.js';
10
+
11
+ function systemAgentMdPath(): string {
12
+ return path.join(getBaseDir(), DEFAULT_AGENTS_DIR, ORCHESTRATOR_AGENT_ID, 'AGENT.md');
13
+ }
14
+
15
+ function defaultSystemPluginConfig(): Record<string, unknown> {
16
+ const config: Record<string, unknown> = {
17
+ model: DEFAULT_SYSTEM_AGENT_MODEL,
18
+ };
19
+ if (isCloudMode()) {
20
+ config.authMode = DEFAULT_CLOUD_OPENBOT_AUTH_MODE;
21
+ }
22
+ return config;
23
+ }
24
+
25
+ async function ensureOpenbotPluginInstalled(): Promise<void> {
26
+ if (await pluginService.isInstalled(OPENBOT_PLUGIN_ID)) {
27
+ return;
28
+ }
29
+
30
+ console.log(`[bootstrap] Installing ${OPENBOT_PLUGIN_ID} from npm`);
31
+ await pluginService.install({ packageName: OPENBOT_PLUGIN_ID });
32
+ }
33
+
34
+ async function ensureSystemAgentMaterialized(): Promise<void> {
35
+ const agentMdPath = systemAgentMdPath();
36
+ if (existsSync(agentMdPath)) {
37
+ return;
38
+ }
39
+
40
+ const agentDir = path.dirname(agentMdPath);
41
+ await fs.mkdir(agentDir, { recursive: true });
42
+
43
+ const frontmatter = {
44
+ name: 'OpenBot',
45
+ description: 'First-party orchestration agent for OpenBot.',
46
+ plugins: [
47
+ {
48
+ id: OPENBOT_PLUGIN_ID,
49
+ config: defaultSystemPluginConfig(),
50
+ },
51
+ ],
52
+ };
53
+
54
+ const content = matter.stringify('', frontmatter);
55
+ await fs.writeFile(agentMdPath, content, 'utf-8');
56
+ console.log(`[bootstrap] Materialized system agent at ${agentMdPath}`);
57
+ }
58
+
59
+ /** Install the default agent plugin from npm and materialize the system agent on first boot. */
60
+ export async function ensureDefaultStack(): Promise<void> {
61
+ await ensureOpenbotPluginInstalled();
62
+ await ensureSystemAgentMaterialized();
63
+ }
@@ -0,0 +1,5 @@
1
+ /** npm package id for the monolithic OpenBot agent runtime plugin. */
2
+ export const OPENBOT_PLUGIN_ID = '@meetopenbot/openbot';
3
+
4
+ /** Default model for the materialized system agent. */
5
+ export const DEFAULT_SYSTEM_AGENT_MODEL = 'openai/gpt-5.4-mini';
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 } 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';
@@ -46,8 +47,11 @@ 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
+ console.log('[server] Cloud mode enabled');
52
+ }
53
+
54
+ const openBotDir = getBaseDir();
51
55
  const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4132);
52
56
  const app = express();
53
57
  const clients: Map<string, express.Response[]> = new Map();
@@ -247,6 +251,23 @@ export async function startServer(options: ServerOptions = {}) {
247
251
 
248
252
  app.use(cors());
249
253
 
254
+ const gatewayToken = process.env.OPENBOT_GATEWAY_TOKEN?.trim();
255
+ if (gatewayToken) {
256
+ app.use((req, res, next) => {
257
+ if (req.path === '/api/health' || req.method === 'OPTIONS') {
258
+ next();
259
+ return;
260
+ }
261
+
262
+ const got = req.get('x-openbot-gateway-token');
263
+ if (got !== gatewayToken) {
264
+ res.status(401).json({ error: 'unauthorized' });
265
+ return;
266
+ }
267
+ next();
268
+ });
269
+ }
270
+
250
271
  const resolvePublicBaseUrl = () => getPublicBaseUrl(PORT, config.publicUrl);
251
272
 
252
273
  app.use((req, res, next) => {
@@ -264,7 +285,7 @@ export async function startServer(options: ServerOptions = {}) {
264
285
  });
265
286
 
266
287
  app.get('/api/health', (req, res) => {
267
- res.json({ status: 'ok', version: pkg.version });
288
+ res.json({ status: 'ok', version: pkg.version, apiVersion: 1 });
268
289
  });
269
290
 
270
291
  app.get('/api/events', (req, res) => {
@@ -683,10 +704,25 @@ export async function startServer(options: ServerOptions = {}) {
683
704
  }
684
705
  });
685
706
 
686
- app.listen(PORT, () => {
687
- console.log(`\x1b[32mOpenBot server listening at http://localhost:${PORT}\x1b[0m`);
707
+ const HOST = process.env.HOST || '0.0.0.0';
708
+ const server = app.listen(PORT, HOST, () => {
709
+ console.log(`\x1b[32mOpenBot server listening at http://${HOST}:${PORT}\x1b[0m`);
688
710
  console.log(
689
711
  `🌐 Visit \x1b[96m\x1b[1mhttps://openbot.one\x1b[0m to connect to this runtime and manage everything from there. ✨`,
690
712
  );
691
713
  });
714
+
715
+ const shutdown = (signal: string) => {
716
+ console.log(`\n[server] Received ${signal}, shutting down...`);
717
+ server.close(() => {
718
+ process.exit(0);
719
+ });
720
+ setTimeout(() => {
721
+ console.error('[server] Forced shutdown after timeout');
722
+ process.exit(1);
723
+ }, 10_000).unref();
724
+ };
725
+
726
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
727
+ process.on('SIGINT', () => shutdown('SIGINT'));
692
728
  }