openbot 0.4.6 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/runtime-security-reviewer.md +32 -0
- package/.claude/hooks/format.sh +18 -0
- package/.claude/hooks/protect-secrets.sh +23 -0
- package/.claude/settings.json +26 -0
- package/.claude/skills/deploy/SKILL.md +26 -0
- package/.claude/skills/new-plugin/SKILL.md +46 -0
- package/.dockerignore +9 -0
- package/.mcp.json +8 -0
- package/AGENTS.md +2 -2
- package/CLAUDE.md +50 -0
- package/Dockerfile +41 -0
- package/deploy/README.md +61 -0
- package/deploy/entrypoint.sh +6 -0
- package/deploy/fly.toml +36 -0
- package/dist/app/bootstrap.js +29 -0
- package/dist/app/cli.js +3 -1
- package/dist/app/cloud-mode.js +26 -0
- package/dist/app/config.js +61 -16
- package/dist/app/responding-agent.js +32 -0
- package/dist/app/server.js +47 -9
- package/dist/plugins/approval/index.js +1 -1
- package/dist/plugins/bash/index.js +386 -164
- package/dist/plugins/memory/service.js +2 -5
- package/dist/plugins/openbot/context.js +17 -1
- package/dist/plugins/openbot/index.js +25 -9
- package/dist/plugins/openbot/model.js +59 -0
- package/dist/plugins/openbot/runtime.js +39 -56
- package/dist/plugins/openbot/system-prompt.js +44 -9
- package/dist/plugins/preview/index.js +267 -0
- package/dist/plugins/storage/index.js +3 -3
- package/dist/plugins/storage/service.js +58 -12
- package/dist/plugins/todo/index.js +131 -38
- package/dist/plugins/todo/service.js +93 -0
- package/dist/plugins/ui/index.js +8 -1
- package/dist/services/plugins/model-registry.js +101 -0
- package/dist/services/plugins/registry.js +6 -4
- package/dist/services/plugins/service.js +2 -6
- package/docs/agents.md +20 -2
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +13 -3
- package/docs/templates/AGENT.example.md +1 -1
- package/package.json +5 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/app/bootstrap.ts +43 -0
- package/src/app/cli.ts +3 -1
- package/src/app/cloud-mode.ts +41 -0
- package/src/app/config.ts +71 -19
- package/src/app/responding-agent.ts +46 -0
- package/src/app/server.ts +55 -9
- package/src/app/types.ts +167 -31
- package/src/plugins/approval/index.ts +1 -1
- package/src/plugins/bash/index.ts +498 -185
- package/src/plugins/memory/service.ts +2 -5
- package/src/plugins/openbot/context.ts +17 -1
- package/src/plugins/openbot/index.ts +27 -11
- package/src/plugins/openbot/model.ts +76 -0
- package/src/plugins/openbot/runtime.ts +47 -69
- package/src/plugins/openbot/system-prompt.ts +44 -9
- package/src/plugins/preview/index.ts +323 -0
- package/src/plugins/storage/index.ts +5 -8
- package/src/plugins/storage/service.ts +79 -15
- package/src/plugins/todo/index.ts +166 -0
- package/src/plugins/todo/service.ts +123 -0
- package/src/plugins/ui/index.ts +8 -1
- package/src/services/plugins/domain.ts +2 -0
- package/src/services/plugins/model-registry.ts +146 -0
- package/src/services/plugins/registry.ts +6 -4
- package/src/services/plugins/service.ts +2 -6
- package/dist/agents/openbot/index.js +0 -76
- package/dist/agents/openbot/middleware/approval.js +0 -132
- package/dist/agents/openbot/runtime.js +0 -289
- package/dist/agents/openbot/system-prompt.js +0 -32
- package/dist/agents/openbot/tools/delegation.js +0 -78
- package/dist/agents/openbot/tools/mcp.js +0 -99
- package/dist/agents/openbot/tools/shell.js +0 -91
- package/dist/agents/openbot/tools/storage.js +0 -75
- package/dist/agents/openbot/tools/ui.js +0 -176
- package/dist/agents/system.js +0 -33
- package/dist/bus/agent-package.js +0 -1
- package/dist/bus/plugin.js +0 -1
- package/dist/bus/services.js +0 -754
- package/dist/bus/types.js +0 -1
- package/dist/harness/agent-harness.js +0 -45
- package/dist/harness/agent-invoke-run.js +0 -44
- package/dist/harness/agent-turn.js +0 -99
- package/dist/harness/channel-participants.js +0 -40
- package/dist/harness/constants.js +0 -2
- package/dist/harness/context-meter.js +0 -97
- package/dist/harness/context.js +0 -363
- package/dist/harness/dispatch.js +0 -144
- package/dist/harness/dispatcher.js +0 -156
- package/dist/harness/event-normalizer.js +0 -59
- package/dist/harness/history.js +0 -177
- package/dist/harness/mcp.js +0 -61
- package/dist/harness/orchestration.js +0 -88
- package/dist/harness/orchestrator.js +0 -149
- package/dist/harness/participants.js +0 -22
- package/dist/harness/process.js +0 -29
- package/dist/harness/queue-processor.js +0 -187
- package/dist/harness/run-harness.js +0 -154
- package/dist/harness/run.js +0 -98
- package/dist/harness/runtime-factory.js +0 -73
- package/dist/harness/runtime.js +0 -57
- package/dist/harness/todo-advance.js +0 -93
- package/dist/harness/todo-dispatch.js +0 -51
- package/dist/harness/todos.js +0 -5
- package/dist/harness/turn.js +0 -79
- package/dist/harness/types.js +0 -1
- package/dist/plugins/ai-sdk/index.js +0 -34
- package/dist/plugins/ai-sdk/runtime.js +0 -395
- package/dist/plugins/ai-sdk/system-prompt.js +0 -18
- package/dist/plugins/ai-sdk.js +0 -331
- package/dist/plugins/approval.js +0 -163
- package/dist/plugins/delegation.js +0 -108
- package/dist/plugins/mcp/index.js +0 -108
- package/dist/plugins/mcp.js +0 -140
- package/dist/plugins/shell/index.js +0 -100
- package/dist/plugins/shell.js +0 -123
- package/dist/plugins/storage-tools/index.js +0 -76
- package/dist/plugins/storage.js +0 -737
- package/dist/plugins/thread-namer/index.js +0 -72
- package/dist/plugins/threads/index.js +0 -114
- package/dist/plugins/ui.js +0 -211
- package/dist/plugins/workflow/index.js +0 -65
- package/dist/registry/agents.js +0 -138
- package/dist/registry/plugins.js +0 -110
- package/dist/services/agent-packages.js +0 -103
- package/dist/services/memory.js +0 -152
- package/dist/services/plugins.js +0 -98
- package/dist/services/storage.js +0 -1028
- package/dist/workflow/service.js +0 -106
- package/dist/workflow/types.js +0 -3
|
@@ -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,50 @@
|
|
|
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
|
+
- `GET /api/events` — opens an SSE stream for a `channelId`(+`threadId`), or the special `__global__` channel for active-run snapshots across all channels.
|
|
24
|
+
- `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`.
|
|
25
|
+
- `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).
|
|
26
|
+
|
|
27
|
+
`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.
|
|
28
|
+
|
|
29
|
+
### Plugins (`src/plugins/*`, registered in `src/services/plugins/registry.ts`)
|
|
30
|
+
|
|
31
|
+
Built-ins: `openbot` (batteries-included runtime: bash, memory, todo, storage, delegation, approval all bundled in), `bash`, `storage`, `approval` (gates protected tool calls behind UI confirmation), `memory` (`remember`/`recall`/`forget`, append-only JSONL, scoped `global`/`agent`/`channel`), `todo` (`todo_write`/`todo_read`, thread-scoped checklist in `todos.json`), `delegation` (calling other agents), `ui`, `plugin-manager` (marketplace + npm plugin install), `preview`. Community plugins are npm packages installed on demand into `~/.openbot/plugins/<name>/dist/index.js` and loaded dynamically via `resolvePlugin`; they export a `Plugin`-shaped object as `plugin` or `default` (see `parsePluginModule`).
|
|
32
|
+
|
|
33
|
+
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`, and the run's `abortSignal`.
|
|
34
|
+
|
|
35
|
+
### Agents
|
|
36
|
+
|
|
37
|
+
Agents are markdown files with YAML frontmatter (`~/.openbot/agents/<agentId>/AGENT.md`) listing `plugins: [{id, config}]`; the body is the system prompt. `claude-code` and `gemini-cli` runtime plugins own their entire tool loop (attaching tool plugins like `bash` to them is a no-op) — only the `openbot` runtime plugin consumes the merged `tools` map. Two built-in agent ids are hardcoded in `src/app/agent-ids.ts` and can be overridden by an on-disk `AGENT.md` overlay merged on top of code defaults: `system` (`ORCHESTRATOR_AGENT_ID`, the default orchestrator, listed) and `state` (`STATE_AGENT_ID`, hidden, backs `/api/state` and deterministic storage/marketplace actions).
|
|
38
|
+
|
|
39
|
+
### Storage (`src/plugins/storage/service.ts`, `src/services/plugins/domain.ts`)
|
|
40
|
+
|
|
41
|
+
`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.
|
|
42
|
+
|
|
43
|
+
### Cloud mode (`src/app/cloud-mode.ts`)
|
|
44
|
+
|
|
45
|
+
Set via `OPENBOT_CLOUD_MODE=1` for platform-managed (Fly.io) deployments: the `system` agent is forced onto a coordinator model (`openbot/coordinator-1`) and LLM/integration calls proxy through `OPENBOT_INTEGRATIONS_BASE_URL` with an HMAC `OPENBOT_INTEGRATIONS_TOKEN`. `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.
|
|
46
|
+
|
|
47
|
+
## Conventions
|
|
48
|
+
|
|
49
|
+
- ESM throughout (`"type": "module"`, `NodeNext` module resolution) — internal imports must use `.js` extensions even though source is `.ts`.
|
|
50
|
+
- 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,61 @@
|
|
|
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 uses integrations proxy) |
|
|
43
|
+
| `OPENBOT_INTEGRATIONS_BASE_URL` | Yes (cloud) | Integrations gateway base URL, e.g. `https://integrations.openbot.one/abc123` |
|
|
44
|
+
| `OPENBOT_INTEGRATIONS_TOKEN` | Yes (cloud) | Per-workspace HMAC token for `x-openbot-integrations-token` on LLM proxy requests |
|
|
45
|
+
| `PORT` | Auto | Set by Fly (`8080`) |
|
|
46
|
+
|
|
47
|
+
## Health check
|
|
48
|
+
|
|
49
|
+
`GET /api/health` returns `{ status, version, apiVersion }`.
|
|
50
|
+
|
|
51
|
+
## Control plane integration
|
|
52
|
+
|
|
53
|
+
Your control plane should:
|
|
54
|
+
|
|
55
|
+
1. `fly apps create openbot-{tenantId}`
|
|
56
|
+
2. `fly volumes create openbot_data --region {region} --size {gb}`
|
|
57
|
+
3. `fly secrets set OPENBOT_PUBLIC_URL=... OPENBOT_CLOUD_MODE=1 OPENBOT_INTEGRATIONS_BASE_URL=... OPENBOT_INTEGRATIONS_TOKEN=...`
|
|
58
|
+
4. `fly deploy` with a pinned image tag
|
|
59
|
+
5. Poll `/api/health` until `status === 'ok'`
|
|
60
|
+
|
|
61
|
+
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,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
|
const program = new Command();
|
|
5
6
|
const REQUIRED_NODE_VERSION = '20.12.0';
|
|
@@ -16,12 +17,13 @@ function checkNodeVersion() {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
checkNodeVersion();
|
|
19
|
-
program.name('openbot').description('OpenBot CLI').version('0.4
|
|
20
|
+
program.name('openbot').description('OpenBot CLI').version('0.5.4');
|
|
20
21
|
program
|
|
21
22
|
.command('start')
|
|
22
23
|
.description('Start the OpenBot harness')
|
|
23
24
|
.option('-p, --port <number>', 'Port to listen on')
|
|
24
25
|
.action(async (options) => {
|
|
26
|
+
await bootstrap();
|
|
25
27
|
await startServer(options);
|
|
26
28
|
});
|
|
27
29
|
program.parse();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ORCHESTRATOR_AGENT_ID } from './agent-ids.js';
|
|
2
|
+
/** Platform-managed OpenBot (Fly + integrations proxy). Self-hosted when unset. */
|
|
3
|
+
export const isCloudMode = () => process.env.OPENBOT_CLOUD_MODE === '1';
|
|
4
|
+
/** Platform-managed coordinator model for cloud workspaces. */
|
|
5
|
+
export const COORDINATOR_MODEL = 'openbot/coordinator-1';
|
|
6
|
+
/** Default cloud system agent model. */
|
|
7
|
+
export const CLOUD_SYSTEM_MODEL = COORDINATOR_MODEL;
|
|
8
|
+
export function getCloudIntegrationsConfig() {
|
|
9
|
+
if (!isCloudMode())
|
|
10
|
+
return null;
|
|
11
|
+
const baseUrl = process.env.OPENBOT_INTEGRATIONS_BASE_URL?.trim();
|
|
12
|
+
const token = process.env.OPENBOT_INTEGRATIONS_TOKEN?.trim();
|
|
13
|
+
if (!baseUrl || !token)
|
|
14
|
+
return null;
|
|
15
|
+
return { baseUrl: baseUrl.replace(/\/$/, ''), token };
|
|
16
|
+
}
|
|
17
|
+
export function isCloudSystemAgent(agentId) {
|
|
18
|
+
return isCloudMode() && agentId === ORCHESTRATOR_AGENT_ID;
|
|
19
|
+
}
|
|
20
|
+
export function resolveCloudSystemModel(configModel) {
|
|
21
|
+
return configModel?.trim() || COORDINATOR_MODEL;
|
|
22
|
+
}
|
|
23
|
+
export function assertCloudSystemAgentPluginsMutable(agentId, plugins) {
|
|
24
|
+
void agentId;
|
|
25
|
+
void plugins;
|
|
26
|
+
}
|
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
|
}
|
|
@@ -2,6 +2,17 @@ import { ORCHESTRATOR_AGENT_ID } from './agent-ids.js';
|
|
|
2
2
|
import { storageService } from '../plugins/storage/service.js';
|
|
3
3
|
/** Thread `state.json` key for the sticky responding agent id. */
|
|
4
4
|
export const THREAD_RESPONDING_AGENT_ID_KEY = 'respondingAgentId';
|
|
5
|
+
/** Publish events that continue a pending UI interaction rather than a new user turn. */
|
|
6
|
+
export const CONTINUATION_EVENT_TYPES = new Set(['client:ui:widget:response']);
|
|
7
|
+
const readMetaAgentId = (meta) => {
|
|
8
|
+
if (!meta || typeof meta !== 'object')
|
|
9
|
+
return undefined;
|
|
10
|
+
const value = meta.agentId;
|
|
11
|
+
if (typeof value !== 'string')
|
|
12
|
+
return undefined;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed || undefined;
|
|
15
|
+
};
|
|
5
16
|
const readBoundAgentId = (state) => {
|
|
6
17
|
if (!state || typeof state !== 'object')
|
|
7
18
|
return undefined;
|
|
@@ -46,3 +57,24 @@ export async function resolveRespondingAgentId(options) {
|
|
|
46
57
|
});
|
|
47
58
|
return { agentId: fallback, bound: true, overridden: false };
|
|
48
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolves the agent that should handle a publish event.
|
|
62
|
+
* UI continuations route to the widget origin agent; everything else uses sticky thread binding.
|
|
63
|
+
*/
|
|
64
|
+
export async function resolvePublishTargetAgentId(options) {
|
|
65
|
+
const { eventType, channelId, threadId, requestedAgentId, eventMeta, bindIfUnbound } = options;
|
|
66
|
+
if (CONTINUATION_EVENT_TYPES.has(eventType)) {
|
|
67
|
+
const originAgentId = readMetaAgentId(eventMeta) || requestedAgentId?.trim();
|
|
68
|
+
if (!originAgentId) {
|
|
69
|
+
return { error: 'agentId_required' };
|
|
70
|
+
}
|
|
71
|
+
return { agentId: originAgentId };
|
|
72
|
+
}
|
|
73
|
+
const resolved = await resolveRespondingAgentId({
|
|
74
|
+
channelId,
|
|
75
|
+
threadId,
|
|
76
|
+
requestedAgentId,
|
|
77
|
+
bindIfUnbound,
|
|
78
|
+
});
|
|
79
|
+
return { agentId: resolved.agentId };
|
|
80
|
+
}
|