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.
- 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 +51 -0
- package/Dockerfile +41 -0
- package/deploy/README.md +59 -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 +5 -1
- package/dist/app/cloud-mode.js +15 -0
- package/dist/app/config.js +61 -16
- package/dist/app/ensure-default-stack.js +54 -0
- package/dist/app/openbot-plugin.js +4 -0
- package/dist/app/server.js +37 -6
- package/dist/harness/index.js +3 -0
- 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 +23 -7
- package/dist/plugins/openbot/model.js +20 -0
- package/dist/plugins/openbot/runtime.js +45 -57
- package/dist/plugins/openbot/system-prompt.js +43 -8
- package/dist/plugins/preview/index.js +267 -0
- package/dist/plugins/storage/index.js +3 -325
- package/dist/plugins/storage/service.js +61 -18
- 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/host.js +21 -0
- package/dist/services/plugins/model-registry.js +126 -0
- package/dist/services/plugins/registry.js +28 -42
- 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 +8 -14
- package/package.json +5 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/app/bootstrap.ts +43 -0
- package/src/app/cli.ts +5 -1
- package/src/app/cloud-mode.ts +26 -0
- package/src/app/config.ts +71 -19
- package/src/app/ensure-default-stack.ts +63 -0
- package/src/app/openbot-plugin.ts +5 -0
- package/src/app/server.ts +42 -6
- package/src/app/types.ts +158 -32
- package/src/harness/index.ts +4 -0
- package/src/plugins/memory/service.ts +2 -5
- package/src/plugins/storage/index.ts +3 -371
- package/src/plugins/storage/service.ts +81 -22
- package/src/services/plugins/domain.ts +2 -0
- package/src/services/plugins/host.ts +36 -0
- package/src/services/plugins/model-registry.ts +178 -0
- package/src/services/plugins/registry.ts +30 -43
- package/src/services/plugins/service.ts +8 -17
- package/src/services/plugins/types.ts +36 -2
- package/src/services/todo/types.ts +12 -0
- 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/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/src/plugins/approval/index.ts +0 -147
- package/src/plugins/bash/index.ts +0 -232
- package/src/plugins/delegation/index.ts +0 -153
- package/src/plugins/memory/index.ts +0 -182
- package/src/plugins/openbot/context.ts +0 -121
- package/src/plugins/openbot/history.ts +0 -158
- package/src/plugins/openbot/index.ts +0 -80
- package/src/plugins/openbot/runtime.ts +0 -526
- package/src/plugins/openbot/system-prompt.ts +0 -22
- package/src/plugins/ui/index.ts +0 -123
- /package/dist/{bus → services/todo}/types.js +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: runtime-security-reviewer
|
|
3
|
+
description: Use proactively after changes touching server.ts, harness/index.ts, storage/service.ts, storage/files.ts, cloud-mode.ts, or plugin loading (services/plugins/registry.ts). Reviews for auth-boundary regressions, path traversal in channel/workspace file access, abort-signal correctness, and risks from dynamically loaded community plugins. Not a substitute for the general PR reviewer (pr-review-toolkit) — this agent is scoped to this repo's specific trust boundaries.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
model: inherit
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are reviewing changes to OpenBot's runtime — a local-first, event-driven agent harness (Express + `melony`). Your job is to catch regressions in this codebase's specific trust boundaries, not general code quality (that's covered elsewhere).
|
|
9
|
+
|
|
10
|
+
## What to check
|
|
11
|
+
|
|
12
|
+
1. **Gateway/integrations auth** (`src/app/server.ts`, `src/app/cloud-mode.ts`)
|
|
13
|
+
- Every route except `/api/health` must stay behind the `x-openbot-gateway-token` check when `OPENBOT_GATEWAY_TOKEN` is set. Flag any new route added before that middleware, or any code path that could bypass it.
|
|
14
|
+
- `OPENBOT_INTEGRATIONS_TOKEN` / `OPENBOT_GATEWAY_TOKEN` must never be logged, echoed in error responses, or forwarded to a client.
|
|
15
|
+
|
|
16
|
+
2. **Path traversal in file/workspace access** (`src/plugins/storage/files.ts`, `src/plugins/storage/service.ts`)
|
|
17
|
+
- Any `channelId` or `path` value that originates from a request (query, body, header) and is joined into a filesystem path must be validated to stay within `getChannelsWorkspaceRoot()` / the channel's directory. Look for `path.join`/`path.resolve` calls on user-controlled input without a containment check (e.g. resolved path still starting with the expected root).
|
|
18
|
+
- This applies to upload, write, serve-file, and stat code paths in `server.ts` and the storage service.
|
|
19
|
+
|
|
20
|
+
3. **Abort-signal correctness** (`src/harness/index.ts`, `src/services/abort.ts`)
|
|
21
|
+
- A run's `abortSignal` must be acquired and released symmetrically (`abortRegistry.acquire`/`release`) even on thrown errors.
|
|
22
|
+
- Delegated sub-agent runs sharing a `channelId:threadId` key must observe the same abort signal — check that new delegation paths don't spin up runs outside `runAgent` (which would miss abort wiring).
|
|
23
|
+
|
|
24
|
+
4. **Dynamic plugin loading** (`src/services/plugins/registry.ts`)
|
|
25
|
+
- `resolvePlugin` imports and executes arbitrary code from `~/.openbot/plugins/<id>/dist/index.js`. Flag any change that widens what ids/paths can be resolved (e.g. accepting an absolute path or `..` in `id`) without equivalent validation to the existing built-in/npm-name assumption.
|
|
26
|
+
|
|
27
|
+
5. **Event persistence & injection**
|
|
28
|
+
- Events written via `storageService.storeEvent` / echoed over SSE should not allow arbitrary control characters or unbounded payloads to break the `data: ...\n\n` SSE framing (check any code constructing SSE payloads outside the existing `sendToClientKey` helper).
|
|
29
|
+
|
|
30
|
+
## Output
|
|
31
|
+
|
|
32
|
+
For each finding: file:line, what invariant it breaks, and a concrete input/request that would exploit it. If nothing in the diff touches these boundaries, say so briefly — don't invent findings.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PostToolUse: format the edited file with Prettier if it's a type Prettier handles.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
file_path=$(jq -r '.tool_input.file_path // empty')
|
|
6
|
+
[ -z "$file_path" ] && exit 0
|
|
7
|
+
[ -f "$file_path" ] || exit 0
|
|
8
|
+
|
|
9
|
+
case "$file_path" in
|
|
10
|
+
*.ts|*.tsx|*.js|*.jsx|*.json|*.md|*.yml|*.yaml)
|
|
11
|
+
;;
|
|
12
|
+
*)
|
|
13
|
+
exit 0
|
|
14
|
+
;;
|
|
15
|
+
esac
|
|
16
|
+
|
|
17
|
+
npx --no-install prettier --write "$file_path" >/dev/null 2>&1 || true
|
|
18
|
+
exit 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse: block edits to env files and local git config (which can hold credentials).
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
file_path=$(jq -r '.tool_input.file_path // empty')
|
|
6
|
+
[ -z "$file_path" ] && exit 0
|
|
7
|
+
|
|
8
|
+
base=$(basename "$file_path")
|
|
9
|
+
case "$base" in
|
|
10
|
+
.env|.env.*)
|
|
11
|
+
echo "Blocked: editing $file_path is not allowed (env file may contain secrets)." >&2
|
|
12
|
+
exit 2
|
|
13
|
+
;;
|
|
14
|
+
esac
|
|
15
|
+
|
|
16
|
+
case "$file_path" in
|
|
17
|
+
*/.git/config|.git/config)
|
|
18
|
+
echo "Blocked: editing .git/config is not allowed (may contain credentials in the remote URL)." >&2
|
|
19
|
+
exit 2
|
|
20
|
+
;;
|
|
21
|
+
esac
|
|
22
|
+
|
|
23
|
+
exit 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Edit|Write",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "bash .claude/hooks/protect-secrets.sh"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Edit|Write",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "bash .claude/hooks/format.sh"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy
|
|
3
|
+
description: Build and push a new OpenBot runtime image to Fly.io, following this repo's release process.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Deploy
|
|
8
|
+
|
|
9
|
+
Releases a new OpenBot runtime image for the `openbot-runtime` Fly.io app.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. Confirm the working tree is clean and on the intended branch (`git status`).
|
|
14
|
+
2. Bump the version in [package.json](../../../package.json) if this is a real release (the CLI reports this version via `--version` and `/api/health`).
|
|
15
|
+
3. Update the `--image-label` in the `push` script in [package.json](../../../package.json) to match, and update the version string in [src/app/cli.ts](../../../src/app/cli.ts).
|
|
16
|
+
4. Run the build + push:
|
|
17
|
+
```bash
|
|
18
|
+
npm run push
|
|
19
|
+
```
|
|
20
|
+
This runs `fly deploy --build-only --push --config deploy/fly.toml -a openbot-runtime --image-label v<version>` — it builds and pushes the image but does **not** deploy it to a running app (`--build-only`). Actually rolling a tenant onto the new image is a control-plane action outside this repo (see [deploy/README.md](../../../deploy/README.md) for the tenant deploy sequence).
|
|
21
|
+
5. Confirm the push succeeded and note the image label for whoever triggers the control-plane rollout.
|
|
22
|
+
|
|
23
|
+
## Notes
|
|
24
|
+
|
|
25
|
+
- This is a shared image used across tenant Fly apps (one app per tenant, one persistent volume each) — pushing here does not affect any running instance until a control-plane deploy references the new label.
|
|
26
|
+
- Never run `fly deploy` without `--build-only` from this repo; a bare deploy would need an `-a <app>` targeting a specific tenant app, which isn't this repo's job.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: new-plugin
|
|
3
|
+
description: Scaffold a new built-in OpenBot plugin (a Plugin-shaped module under src/plugins/<name>) and register it in the built-in plugin registry.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# New plugin
|
|
8
|
+
|
|
9
|
+
Scaffolds a new **built-in** plugin — for a community (npm-distributed) plugin, this skill doesn't apply; see [docs/plugins.md](../../../docs/plugins.md) instead.
|
|
10
|
+
|
|
11
|
+
A plugin is the single extension unit on the OpenBot bus (see [docs/plugins.md](../../../docs/plugins.md)). It's just an object matching `Plugin` from [src/services/plugins/types.ts](../../../src/services/plugins/types.ts): `id`, `name`, `description`, optional `toolDefinitions`, and a `factory(context)` that returns a `MelonyPlugin`. Whether it acts as a "runtime" (handles `agent:invoke`), a "tool" (handles `action:<tool>` events, contributes `toolDefinitions`), or "middleware" (intercepts/re-emits events, like `approval`) is defined entirely by which events it subscribes to.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
1. Ask the user for: plugin id (kebab-case, becomes the folder name and registry key), a one-line description, and its role (tool / middleware / runtime).
|
|
16
|
+
2. Create `src/plugins/<id>/index.ts`. Minimal tool-plugin shape (adapt from [src/plugins/approval/index.ts](../../../src/plugins/approval/index.ts) for middleware, or [src/plugins/bash/index.ts](../../../src/plugins/bash/index.ts) for a fuller tool example with widgets):
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import type { Plugin } from '../../services/plugins/types.js';
|
|
20
|
+
import { OpenBotEvent } from '../../app/types.js';
|
|
21
|
+
|
|
22
|
+
export const myPlugin: Plugin = {
|
|
23
|
+
id: 'my-plugin',
|
|
24
|
+
name: 'My Plugin',
|
|
25
|
+
description: 'One-line description of what this plugin does.',
|
|
26
|
+
toolDefinitions: {
|
|
27
|
+
my_tool: {
|
|
28
|
+
description: 'What this tool does, written for the LLM.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
/* zod schema, e.g. z.object({ foo: z.string() }) */
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
factory: ({ config, storage }) => (builder) => {
|
|
35
|
+
builder.on('action:my_tool', async function* (event, context) {
|
|
36
|
+
// handle the tool call, yield OpenBotEvent(s)
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default myPlugin;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
3. Register it in [src/services/plugins/registry.ts](../../../src/services/plugins/registry.ts): import it and add `[myPlugin.id]: myPlugin` to the `BUILT_IN` map.
|
|
45
|
+
4. If the plugin exposes tools, note in the description whether it's meant to be attached alongside the `openbot` runtime plugin (which merges `toolDefinitions` from every attached plugin) or is self-contained.
|
|
46
|
+
5. Build (`npm run build`) to catch type errors, since there's no test suite to lean on.
|
package/.dockerignore
ADDED
package/.mcp.json
ADDED
package/AGENTS.md
CHANGED
|
@@ -15,7 +15,7 @@ OpenBot is a local-first platform for multi-agent orchestration and coordination
|
|
|
15
15
|
|
|
16
16
|
- `src/`: event-driven orchestration engine built on Melony.
|
|
17
17
|
- Persistence is local-file based (no mandatory cloud datastore by default).
|
|
18
|
-
- API surface is intentionally minimal:
|
|
18
|
+
- API surface is intentionally minimal: `GET /api/events`, `POST /api/publish`, `GET /api/state`.
|
|
19
19
|
|
|
20
20
|
## Product Mental Model
|
|
21
21
|
|
|
@@ -34,7 +34,7 @@ OpenBot is a local-first platform for multi-agent orchestration and coordination
|
|
|
34
34
|
|
|
35
35
|
- The server is event-first: treat HTTP routes as entry points into the event pipeline.
|
|
36
36
|
- Follow Melony's event flow: `Event -> Handler -> Events`.
|
|
37
|
-
- Keep
|
|
37
|
+
- Keep orchestration on `GET /api/events`, `POST /api/publish`, and `GET /api/state`.
|
|
38
38
|
- Plugin changes belong in `src/plugins/*`.
|
|
39
39
|
- Agent behavior and orchestration belong in agent/runtime layers, not UI code.
|
|
40
40
|
- For tool-calling flows, ensure completion/result events are emitted consistently.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
@AGENTS.md
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
- `npm run dev` — run the CLI (`start` command) via `tsx watch`, hot-reloading on change.
|
|
10
|
+
- `npm run build` — `tsc` compile to `dist/` + copy `src/assets/icon.svg` to `dist/assets`.
|
|
11
|
+
- `npm run start` — run the built server (`node dist/app/cli.js start`).
|
|
12
|
+
- No test suite and no lint script exist in this repo. Prettier config is `.prettierrc` (single quotes, semicolons, trailing commas, 100 print width) but there's no `format`/`lint` script — format via editor integration.
|
|
13
|
+
- Package manager is pnpm (`pnpm-lock.yaml`, `pnpm-workspace.yaml`), even though `README`/`CONTRIBUTING` reference `npm`. Use `pnpm install`.
|
|
14
|
+
- `npm run push` builds and pushes a Fly.io image (`deploy/fly.toml`, app `openbot-runtime`) — this is a release action, not a dev command.
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
OpenBot is a local-first, event-driven harness for running AI agents, built on the `melony` event-bus library. Almost everything flows through one abstraction: an **agent** is a named stack of **plugins**, and the bus doesn't distinguish "runtime" plugins from "tool" plugins — that role is defined purely by which events a plugin subscribes to / emits (see `docs/plugins.md`, `docs/agents.md`).
|
|
19
|
+
|
|
20
|
+
### Request flow
|
|
21
|
+
|
|
22
|
+
`src/app/server.ts` is the only HTTP surface, with three routes:
|
|
23
|
+
|
|
24
|
+
- `GET /api/events` — opens an SSE stream for a `channelId`(+`threadId`), or the special `__global__` channel for active-run snapshots across all channels.
|
|
25
|
+
- `POST /api/publish` — publishes an event; the common case is `agent:invoke`, which resolves a target agent and calls `runAgent` (fire-and-forget, streamed back over SSE + persisted). Also special-cased inline for file upload/write and `action:agent_run_stop`.
|
|
26
|
+
- `GET /api/state` — same dispatch as `/api/publish` but synchronous (collects emitted events into a JSON response instead of streaming) and defaults to the hidden `state` agent; used for deterministic, non-LLM reads (storage plugin actions).
|
|
27
|
+
|
|
28
|
+
`src/harness/index.ts` (`runAgent`) is the core execution loop for **one agent turn**: it loads `AgentDetails` and `OpenBotState` from storage, resolves every plugin referenced by the agent (`resolvePlugin`), merges their `toolDefinitions` into a shared `tools` map, builds a `melony<OpenBotState, OpenBotEvent>()` runtime by calling each plugin's `factory(context)`, and drains the resulting async generator, emitting `agent:run:start`/`agent:run:end` around it. Each run acquires a per-`channelId:threadId` `AbortSignal` from `abortRegistry` so a stop request (`action:agent_run_stop`) can cancel a parent run and any of its delegated sub-agent runs together.
|
|
29
|
+
|
|
30
|
+
### Plugins (`src/plugins/*`, registered in `src/services/plugins/registry.ts`)
|
|
31
|
+
|
|
32
|
+
Core built-ins: `storage` (infra handlers for `/api/state`), `plugin-manager` (marketplace + npm plugin install). The default agent runtime is **`@meetopenbot/openbot`**, a community npm package installed to `~/.openbot/plugins/@meetopenbot/openbot/` on first boot (`ensureDefaultStack`). Disk-installed plugins take precedence over built-ins in `resolvePlugin`.
|
|
33
|
+
|
|
34
|
+
A `Plugin`'s `factory(context)` returns a `MelonyPlugin`; `PluginContext` carries `agentId`, `agentDetails`, per-agent `config` (from `AGENT.md` frontmatter), the shared `storage` service, the merged `tools` map, `publicBaseUrl`, the run's `abortSignal`, and `host` (platform hooks: `runAgent`, cloud auth, model registry, config).
|
|
35
|
+
|
|
36
|
+
### Agents
|
|
37
|
+
|
|
38
|
+
Agents are markdown files with YAML frontmatter (`~/.openbot/agents/<agentId>/AGENT.md`) listing `plugins: [{id, config}]`; the body is the system prompt. The default **`system`** orchestrator references `@meetopenbot/openbot` (materialized on first boot). Built-in agent ids in `src/app/agent-ids.ts`: `system` (orchestrator, listed) and `state` (hidden, backs `/api/state`). `system` may be customized via `agents/system/AGENT.md`; deleting that file reverts to bootstrap defaults on next start.
|
|
39
|
+
|
|
40
|
+
### Storage (`src/plugins/storage/service.ts`, `src/services/plugins/domain.ts`)
|
|
41
|
+
|
|
42
|
+
`storageService` is the single source of truth for agents, channels, threads, events, and files — all local-file-based under `getBaseDir()` (`~/.openbot`, or `OPENBOT_BASE_DIR`). Per-channel working directories for bash/file tools live separately under `getChannelsWorkspaceRoot()` (`~/openbot`, or `OPENBOT_CHANNELS_WORKSPACE_DIR`) — don't conflate the two roots.
|
|
43
|
+
|
|
44
|
+
### Cloud mode (`src/app/cloud-mode.ts`)
|
|
45
|
+
|
|
46
|
+
Set via `OPENBOT_CLOUD_MODE=1` for platform-managed (Fly.io) deployments. The `system` agent openbot plugin defaults to **credits** mode and can be switched to **BYOK** in agent config. `OPENBOT_GATEWAY_TOKEN`, when set, gates every route except `/api/health` behind an `x-openbot-gateway-token` header check (see `server.ts`) — this is how the control plane's proxy authenticates to a per-tenant instance. See `deploy/README.md` for the full env var contract and control-plane integration sequence.
|
|
47
|
+
|
|
48
|
+
## Conventions
|
|
49
|
+
|
|
50
|
+
- ESM throughout (`"type": "module"`, `NodeNext` module resolution) — internal imports must use `.js` extensions even though source is `.ts`.
|
|
51
|
+
- Node >=20.12.0 required (enforced at CLI startup with a warning, not a hard fail).
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
FROM node:22-alpine AS builder
|
|
4
|
+
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
|
|
8
|
+
|
|
9
|
+
COPY package.json pnpm-lock.yaml ./
|
|
10
|
+
RUN pnpm install --frozen-lockfile
|
|
11
|
+
|
|
12
|
+
COPY tsconfig.json ./
|
|
13
|
+
COPY src ./src
|
|
14
|
+
RUN pnpm run build
|
|
15
|
+
|
|
16
|
+
FROM node:22-alpine AS runner
|
|
17
|
+
|
|
18
|
+
WORKDIR /app
|
|
19
|
+
|
|
20
|
+
ENV OPENBOT_BASE_DIR=/data/.openbot
|
|
21
|
+
ENV OPENBOT_CHANNELS_WORKSPACE_DIR=/data/workspace
|
|
22
|
+
|
|
23
|
+
RUN apk add --no-cache bash git su-exec curl \
|
|
24
|
+
&& ARCH="$(uname -m)" \
|
|
25
|
+
&& case "$ARCH" in x86_64) CF_ARCH=amd64 ;; aarch64) CF_ARCH=arm64 ;; *) CF_ARCH=amd64 ;; esac \
|
|
26
|
+
&& curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${CF_ARCH}" -o /usr/local/bin/cloudflared \
|
|
27
|
+
&& chmod +x /usr/local/bin/cloudflared \
|
|
28
|
+
&& corepack enable && corepack prepare pnpm@9.15.9 --activate
|
|
29
|
+
|
|
30
|
+
COPY package.json pnpm-lock.yaml ./
|
|
31
|
+
RUN pnpm install --frozen-lockfile --prod
|
|
32
|
+
|
|
33
|
+
COPY --from=builder /app/dist ./dist
|
|
34
|
+
COPY deploy/entrypoint.sh /entrypoint.sh
|
|
35
|
+
RUN chmod +x /entrypoint.sh
|
|
36
|
+
|
|
37
|
+
EXPOSE 8080
|
|
38
|
+
|
|
39
|
+
USER root
|
|
40
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
41
|
+
CMD ["node", "dist/app/cli.js", "start"]
|
package/deploy/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Fly.io deployment
|
|
2
|
+
|
|
3
|
+
One OpenBot instance per Fly app, with a persistent volume at `/data`.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/data/
|
|
9
|
+
.openbot/ ← OPENBOT_BASE_DIR (config, channels, agents, plugins)
|
|
10
|
+
workspace/ ← OPENBOT_CHANNELS_WORKSPACE_DIR (per-channel cwd)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
On boot, `deploy/entrypoint.sh` runs as root to `chown -R node:node /data`, then starts OpenBot as the `node` user. The image also includes `git`, `bash`, and `cloudflared` for the bash and preview plugins.
|
|
14
|
+
|
|
15
|
+
## First deploy (manual)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# From repo root
|
|
19
|
+
fly apps create openbot-my-tenant
|
|
20
|
+
fly volumes create openbot_data --region ord --size 1 -a openbot-my-tenant
|
|
21
|
+
|
|
22
|
+
fly secrets set \
|
|
23
|
+
OPENBOT_PUBLIC_URL=https://openbot-my-tenant.fly.dev \
|
|
24
|
+
-a openbot-my-tenant
|
|
25
|
+
|
|
26
|
+
# Optional: LLM API keys via Fly secrets or set later in the UI
|
|
27
|
+
# fly secrets set OPENAI_API_KEY=sk-... -a openbot-my-tenant
|
|
28
|
+
|
|
29
|
+
fly deploy --config deploy/fly.toml -a openbot-my-tenant
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Copy `deploy/fly.toml`, set `app` to your tenant id, and ensure the volume name matches `openbot_data`.
|
|
33
|
+
|
|
34
|
+
## Environment variables
|
|
35
|
+
|
|
36
|
+
| Variable | Required | Description |
|
|
37
|
+
|----------|----------|-------------|
|
|
38
|
+
| `OPENBOT_BASE_DIR` | Yes (cloud) | Data root on the volume, e.g. `/data/.openbot` |
|
|
39
|
+
| `OPENBOT_CHANNELS_WORKSPACE_DIR` | Yes (cloud) | Channel workspace parent, e.g. `/data/workspace` |
|
|
40
|
+
| `OPENBOT_PUBLIC_URL` | Yes (cloud) | Public HTTPS URL for file links |
|
|
41
|
+
| `OPENBOT_GATEWAY_TOKEN` | Yes (cloud) | Per-workspace HMAC token; gateway sends `x-openbot-gateway-token` on proxied requests |
|
|
42
|
+
| `OPENBOT_CLOUD_MODE` | Yes (cloud) | Set to `1` for platform-managed OpenBot (system agent defaults to credits mode) |
|
|
43
|
+
| `PORT` | Auto | Set by Fly (`8080`) |
|
|
44
|
+
|
|
45
|
+
## Health check
|
|
46
|
+
|
|
47
|
+
`GET /api/health` returns `{ status, version, apiVersion }`.
|
|
48
|
+
|
|
49
|
+
## Control plane integration
|
|
50
|
+
|
|
51
|
+
Your control plane should:
|
|
52
|
+
|
|
53
|
+
1. `fly apps create openbot-{tenantId}`
|
|
54
|
+
2. `fly volumes create openbot_data --region {region} --size {gb}`
|
|
55
|
+
3. `fly secrets set OPENBOT_PUBLIC_URL=... OPENBOT_CLOUD_MODE=1`
|
|
56
|
+
4. `fly deploy` with a pinned image tag
|
|
57
|
+
5. Poll `/api/health` until `status === 'ok'`
|
|
58
|
+
|
|
59
|
+
Set `OPENBOT_GATEWAY_TOKEN` via Fly secrets (derived per workspace). The gateway appends `x-openbot-gateway-token` on proxied requests; when the env var is unset (local dev), the check is skipped. `GET /api/health` is exempt for Fly health checks.
|
package/deploy/fly.toml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Fly.io template — copy per tenant and set `app` + secrets from your control plane.
|
|
2
|
+
# Provision: fly volumes create openbot_data --region ord --size 1
|
|
3
|
+
|
|
4
|
+
app = "openbot-tenant-example"
|
|
5
|
+
primary_region = "ord"
|
|
6
|
+
|
|
7
|
+
[build]
|
|
8
|
+
dockerfile = "../Dockerfile"
|
|
9
|
+
|
|
10
|
+
[env]
|
|
11
|
+
OPENBOT_BASE_DIR = "/data/.openbot"
|
|
12
|
+
OPENBOT_CHANNELS_WORKSPACE_DIR = "/data/workspace"
|
|
13
|
+
PORT = "8080"
|
|
14
|
+
|
|
15
|
+
[mounts]
|
|
16
|
+
source = "openbot_data"
|
|
17
|
+
destination = "/data"
|
|
18
|
+
|
|
19
|
+
[http_service]
|
|
20
|
+
internal_port = 8080
|
|
21
|
+
force_https = true
|
|
22
|
+
auto_stop_machines = "off"
|
|
23
|
+
auto_start_machines = true
|
|
24
|
+
min_machines_running = 1
|
|
25
|
+
|
|
26
|
+
[[http_service.checks]]
|
|
27
|
+
grace_period = "15s"
|
|
28
|
+
interval = "15s"
|
|
29
|
+
method = "GET"
|
|
30
|
+
path = "/api/health"
|
|
31
|
+
timeout = "5s"
|
|
32
|
+
|
|
33
|
+
[[vm]]
|
|
34
|
+
memory = "1gb"
|
|
35
|
+
cpu_kind = "shared"
|
|
36
|
+
cpus = 1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CONFIG_FILE, DEFAULT_AGENTS_DIR, DEFAULT_CHANNELS_DIR, DEFAULT_PLUGINS_DIR, getBaseDir, getChannelsWorkspaceRoot, getConfigDir, } from './config.js';
|
|
4
|
+
/** Ensure data directories and a minimal config exist (first boot on a Fly volume). */
|
|
5
|
+
export async function bootstrap() {
|
|
6
|
+
const baseDir = getBaseDir();
|
|
7
|
+
const configDir = getConfigDir();
|
|
8
|
+
await fs.mkdir(path.join(baseDir, DEFAULT_AGENTS_DIR), { recursive: true });
|
|
9
|
+
await fs.mkdir(path.join(baseDir, DEFAULT_PLUGINS_DIR), { recursive: true });
|
|
10
|
+
await fs.mkdir(path.join(baseDir, DEFAULT_CHANNELS_DIR), { recursive: true });
|
|
11
|
+
await fs.mkdir(getChannelsWorkspaceRoot(), { recursive: true });
|
|
12
|
+
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
13
|
+
const configPath = path.join(configDir, CONFIG_FILE);
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(configPath);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
const initial = { baseDir };
|
|
19
|
+
const publicUrl = process.env.OPENBOT_PUBLIC_URL?.trim();
|
|
20
|
+
if (publicUrl) {
|
|
21
|
+
initial.publicUrl = publicUrl.replace(/\/$/, '');
|
|
22
|
+
}
|
|
23
|
+
const workspaceDir = process.env.OPENBOT_CHANNELS_WORKSPACE_DIR?.trim();
|
|
24
|
+
if (workspaceDir) {
|
|
25
|
+
initial.channelsWorkspaceDir = getChannelsWorkspaceRoot();
|
|
26
|
+
}
|
|
27
|
+
await fs.writeFile(configPath, JSON.stringify(initial, null, 2), { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/app/cli.js
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
|
const program = new Command();
|
|
5
7
|
const REQUIRED_NODE_VERSION = '20.12.0';
|
|
@@ -16,12 +18,14 @@ function checkNodeVersion() {
|
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
checkNodeVersion();
|
|
19
|
-
program.name('openbot').description('OpenBot CLI').version('0.
|
|
21
|
+
program.name('openbot').description('OpenBot CLI').version('0.5.5');
|
|
20
22
|
program
|
|
21
23
|
.command('start')
|
|
22
24
|
.description('Start the OpenBot harness')
|
|
23
25
|
.option('-p, --port <number>', 'Port to listen on')
|
|
24
26
|
.action(async (options) => {
|
|
27
|
+
await bootstrap();
|
|
28
|
+
await ensureDefaultStack();
|
|
25
29
|
await startServer(options);
|
|
26
30
|
});
|
|
27
31
|
program.parse();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ORCHESTRATOR_AGENT_ID } from './agent-ids.js';
|
|
2
|
+
/** Platform-managed OpenBot (Fly). Self-hosted when unset. */
|
|
3
|
+
export const isCloudMode = () => process.env.OPENBOT_CLOUD_MODE === '1';
|
|
4
|
+
/** Default cloud openbot plugin auth mode. */
|
|
5
|
+
export const DEFAULT_CLOUD_OPENBOT_AUTH_MODE = 'credits';
|
|
6
|
+
export function isCloudSystemAgent(agentId) {
|
|
7
|
+
return isCloudMode() && agentId === ORCHESTRATOR_AGENT_ID;
|
|
8
|
+
}
|
|
9
|
+
export function parseOpenbotAuthMode(value) {
|
|
10
|
+
return value === 'byok' ? 'byok' : 'credits';
|
|
11
|
+
}
|
|
12
|
+
export function assertCloudSystemAgentPluginsMutable(agentId, plugins) {
|
|
13
|
+
void agentId;
|
|
14
|
+
void plugins;
|
|
15
|
+
}
|
package/dist/app/config.js
CHANGED
|
@@ -14,16 +14,8 @@ export const DEFAULT_MARKETPLACE_REGISTRY_URL = 'https://raw.githubusercontent.c
|
|
|
14
14
|
export function resolvePath(p) {
|
|
15
15
|
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : path.resolve(p);
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const id = channelId.trim();
|
|
20
|
-
if (!id) {
|
|
21
|
-
throw new Error('channelId is required');
|
|
22
|
-
}
|
|
23
|
-
return resolvePath(`${DEFAULT_CHANNELS_WORKSPACE_DIR}/${id}`);
|
|
24
|
-
}
|
|
25
|
-
export function loadConfig() {
|
|
26
|
-
const configPath = path.join(os.homedir(), '.openbot', CONFIG_FILE);
|
|
17
|
+
function readConfigFile(configDir) {
|
|
18
|
+
const configPath = path.join(configDir, CONFIG_FILE);
|
|
27
19
|
if (fs.existsSync(configPath)) {
|
|
28
20
|
try {
|
|
29
21
|
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
@@ -34,23 +26,76 @@ export function loadConfig() {
|
|
|
34
26
|
}
|
|
35
27
|
return {};
|
|
36
28
|
}
|
|
29
|
+
/** Directory that holds `config.json` (env `OPENBOT_BASE_DIR` or `~/.openbot`). */
|
|
30
|
+
export function getConfigDir() {
|
|
31
|
+
const envBase = process.env.OPENBOT_BASE_DIR?.trim();
|
|
32
|
+
if (envBase) {
|
|
33
|
+
return resolvePath(envBase);
|
|
34
|
+
}
|
|
35
|
+
return path.join(os.homedir(), '.openbot');
|
|
36
|
+
}
|
|
37
|
+
/** Resolved OpenBot data root (agents, channels, plugins, variables). */
|
|
38
|
+
export function getBaseDir() {
|
|
39
|
+
const envBase = process.env.OPENBOT_BASE_DIR?.trim();
|
|
40
|
+
if (envBase) {
|
|
41
|
+
return resolvePath(envBase);
|
|
42
|
+
}
|
|
43
|
+
const fileConfig = readConfigFile(getConfigDir());
|
|
44
|
+
return resolvePath(fileConfig.baseDir || DEFAULT_BASE_DIR);
|
|
45
|
+
}
|
|
46
|
+
/** Resolved parent directory for per-channel workspace folders. */
|
|
47
|
+
export function getChannelsWorkspaceRoot() {
|
|
48
|
+
const env = process.env.OPENBOT_CHANNELS_WORKSPACE_DIR?.trim();
|
|
49
|
+
if (env) {
|
|
50
|
+
return resolvePath(env);
|
|
51
|
+
}
|
|
52
|
+
const fileConfig = readConfigFile(getConfigDir());
|
|
53
|
+
if (fileConfig.channelsWorkspaceDir) {
|
|
54
|
+
return resolvePath(fileConfig.channelsWorkspaceDir);
|
|
55
|
+
}
|
|
56
|
+
return resolvePath(DEFAULT_CHANNELS_WORKSPACE_DIR);
|
|
57
|
+
}
|
|
58
|
+
/** Default absolute cwd for a channel when none is provided at creation time. */
|
|
59
|
+
export function getDefaultChannelCwd(channelId) {
|
|
60
|
+
const id = channelId.trim();
|
|
61
|
+
if (!id) {
|
|
62
|
+
throw new Error('channelId is required');
|
|
63
|
+
}
|
|
64
|
+
return path.join(getChannelsWorkspaceRoot(), id);
|
|
65
|
+
}
|
|
66
|
+
export function loadConfig() {
|
|
67
|
+
const fileConfig = readConfigFile(getConfigDir());
|
|
68
|
+
const merged = { ...fileConfig };
|
|
69
|
+
merged.baseDir = getBaseDir();
|
|
70
|
+
const envPublicUrl = process.env.OPENBOT_PUBLIC_URL?.trim();
|
|
71
|
+
if (envPublicUrl) {
|
|
72
|
+
merged.publicUrl = envPublicUrl.replace(/\/$/, '');
|
|
73
|
+
}
|
|
74
|
+
else if (merged.publicUrl) {
|
|
75
|
+
merged.publicUrl = merged.publicUrl.replace(/\/$/, '');
|
|
76
|
+
}
|
|
77
|
+
merged.channelsWorkspaceDir = getChannelsWorkspaceRoot();
|
|
78
|
+
const envPort = process.env.PORT?.trim();
|
|
79
|
+
if (envPort && !Number.isNaN(Number(envPort))) {
|
|
80
|
+
merged.port = Number(envPort);
|
|
81
|
+
}
|
|
82
|
+
return merged;
|
|
83
|
+
}
|
|
37
84
|
export function saveConfig(config) {
|
|
38
|
-
const configDir =
|
|
85
|
+
const configDir = getConfigDir();
|
|
39
86
|
const configPath = path.join(configDir, CONFIG_FILE);
|
|
40
87
|
if (!fs.existsSync(configDir)) {
|
|
41
88
|
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
42
89
|
}
|
|
43
|
-
const currentConfig =
|
|
90
|
+
const currentConfig = readConfigFile(configDir);
|
|
44
91
|
const newConfig = { ...currentConfig, ...config };
|
|
45
92
|
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
|
|
46
93
|
}
|
|
47
94
|
export function isConfigured() {
|
|
48
|
-
|
|
49
|
-
return fs.existsSync(configPath);
|
|
95
|
+
return fs.existsSync(path.join(getConfigDir(), CONFIG_FILE));
|
|
50
96
|
}
|
|
51
97
|
export function loadVariables() {
|
|
52
|
-
const
|
|
53
|
-
const variablesPath = path.join(resolvePath(config.baseDir || DEFAULT_BASE_DIR), VARIABLES_FILE);
|
|
98
|
+
const variablesPath = path.join(getBaseDir(), VARIABLES_FILE);
|
|
54
99
|
if (fs.existsSync(variablesPath)) {
|
|
55
100
|
return JSON.parse(fs.readFileSync(variablesPath, 'utf-8'));
|
|
56
101
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
function systemAgentMdPath() {
|
|
11
|
+
return path.join(getBaseDir(), DEFAULT_AGENTS_DIR, ORCHESTRATOR_AGENT_ID, 'AGENT.md');
|
|
12
|
+
}
|
|
13
|
+
function defaultSystemPluginConfig() {
|
|
14
|
+
const config = {
|
|
15
|
+
model: DEFAULT_SYSTEM_AGENT_MODEL,
|
|
16
|
+
};
|
|
17
|
+
if (isCloudMode()) {
|
|
18
|
+
config.authMode = DEFAULT_CLOUD_OPENBOT_AUTH_MODE;
|
|
19
|
+
}
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
22
|
+
async function ensureOpenbotPluginInstalled() {
|
|
23
|
+
if (await pluginService.isInstalled(OPENBOT_PLUGIN_ID)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.log(`[bootstrap] Installing ${OPENBOT_PLUGIN_ID} from npm`);
|
|
27
|
+
await pluginService.install({ packageName: OPENBOT_PLUGIN_ID });
|
|
28
|
+
}
|
|
29
|
+
async function ensureSystemAgentMaterialized() {
|
|
30
|
+
const agentMdPath = systemAgentMdPath();
|
|
31
|
+
if (existsSync(agentMdPath)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const agentDir = path.dirname(agentMdPath);
|
|
35
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
36
|
+
const frontmatter = {
|
|
37
|
+
name: 'OpenBot',
|
|
38
|
+
description: 'First-party orchestration agent for OpenBot.',
|
|
39
|
+
plugins: [
|
|
40
|
+
{
|
|
41
|
+
id: OPENBOT_PLUGIN_ID,
|
|
42
|
+
config: defaultSystemPluginConfig(),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
const content = matter.stringify('', frontmatter);
|
|
47
|
+
await fs.writeFile(agentMdPath, content, 'utf-8');
|
|
48
|
+
console.log(`[bootstrap] Materialized system agent at ${agentMdPath}`);
|
|
49
|
+
}
|
|
50
|
+
/** Install the default agent plugin from npm and materialize the system agent on first boot. */
|
|
51
|
+
export async function ensureDefaultStack() {
|
|
52
|
+
await ensureOpenbotPluginInstalled();
|
|
53
|
+
await ensureSystemAgentMaterialized();
|
|
54
|
+
}
|