@kaiba-cloud/orbital 0.0.1 → 0.0.2
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/README.md +155 -0
- package/dist/index.js +10580 -6562
- package/dist/mod.json +1 -1
- package/dist/orbital.js +14928 -26392
- package/dist/runner.js +15068 -17088
- package/package.json +2 -8
- package/schemas/telemetry-snapshot.schema.json +17 -1
- package/skills/orbital-plugin-builder/SKILL.md +16 -14
- package/skills/orbital-plugin-builder/references/host-api.md +35 -1
- package/skills/orbital-plugin-builder/references/manifest.md +14 -14
- package/skills/orbital-plugin-builder/references/ports.md +137 -31
- package/skills/orbital-plugin-builder/references/recipes.md +133 -11
package/package.json
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaiba-cloud/orbital",
|
|
3
3
|
"description": "orbital: the control plane lite for agents on any harness: the node, its runner, its terminal UI, its protocol and its Claude Code mod, in one command line",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.2",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/ai-kaiba/atom.git",
|
|
10
|
-
"directory": "packages/orbital"
|
|
11
|
-
},
|
|
12
7
|
"engines": {
|
|
13
8
|
"node": ">=22.18"
|
|
14
9
|
},
|
|
@@ -22,8 +17,7 @@
|
|
|
22
17
|
"dependencies": {
|
|
23
18
|
"@anthropic-ai/claude-agent-sdk": "^0.3.295",
|
|
24
19
|
"@anthropic-ai/sandbox-runtime": "0.0.76",
|
|
25
|
-
"@mariozechner/pi-ai": "0.73.1"
|
|
26
|
-
"@mariozechner/pi-coding-agent": "0.73.1"
|
|
20
|
+
"@mariozechner/pi-ai": "0.73.1"
|
|
27
21
|
},
|
|
28
22
|
"devDependencies": {
|
|
29
23
|
"@a2a-js/sdk": "1.3.0",
|
|
@@ -214,6 +214,20 @@
|
|
|
214
214
|
],
|
|
215
215
|
"additionalProperties": false
|
|
216
216
|
}
|
|
217
|
+
},
|
|
218
|
+
"problems": {
|
|
219
|
+
"default": [],
|
|
220
|
+
"type": "array",
|
|
221
|
+
"items": {
|
|
222
|
+
"type": "string"
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"retiring": {
|
|
226
|
+
"default": null,
|
|
227
|
+
"type": [
|
|
228
|
+
"string",
|
|
229
|
+
"null"
|
|
230
|
+
]
|
|
217
231
|
}
|
|
218
232
|
},
|
|
219
233
|
"required": [
|
|
@@ -221,7 +235,9 @@
|
|
|
221
235
|
"node",
|
|
222
236
|
"purpose",
|
|
223
237
|
"lead",
|
|
224
|
-
"members"
|
|
238
|
+
"members",
|
|
239
|
+
"problems",
|
|
240
|
+
"retiring"
|
|
225
241
|
],
|
|
226
242
|
"additionalProperties": false
|
|
227
243
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: orbital-plugin-builder
|
|
3
|
-
description: Use when building, changing or debugging any orbital plugin or port for the `orbital` CLI
|
|
3
|
+
description: Use when building, changing or debugging any orbital plugin or port for the `orbital` CLI — a channel (Slack, Discord, email), a store or workspace store, a harness, a panel, a policy engine, a telemetry sink or live dashboard (snapshot and events), a directory of agents, placements on other machines, session hooks, hosted tools, actions, workflows, a reasoner, a missions relay for cross-machine or LAN work, a plugin command or a background service — or when a plugin uses the host API, writes orbital-plugin.json or permissions, fails `orbital plugin check`, is refused at start, or must be built, added, tested or published to npm or a marketplace.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Build an orbital plugin
|
|
@@ -15,14 +15,14 @@ plain object for each port.
|
|
|
15
15
|
|---|---|
|
|
16
16
|
| [references/ports.md](references/ports.md) | Each port: what it is for, when the node calls it, the config, the interface, the remote methods, an example. |
|
|
17
17
|
| [references/host-api.md](references/host-api.md) | Each method of the host API: `submit`, `deliver`, `completeModel`, `missions`, `snapshot` and the others. |
|
|
18
|
-
| [references/manifest.md](references/manifest.md) | `orbital-plugin.json`, the `
|
|
19
|
-
| [references/recipes.md](references/recipes.md) | Whole plugins: a cross-machine missions relay with a directory and placements, a telemetry dashboard, a Slack channel, a policy engine. |
|
|
18
|
+
| [references/manifest.md](references/manifest.md) | `orbital-plugin.json`, the `orbital.yaml` entry, the manifest, permissions, secrets, `context.init`. |
|
|
19
|
+
| [references/recipes.md](references/recipes.md) | Whole plugins: a cross-machine missions relay with a directory and placements, a telemetry dashboard, a Slack channel, a policy engine, a gatekeeper for messages from the network. |
|
|
20
20
|
|
|
21
21
|
## Ports
|
|
22
22
|
|
|
23
23
|
| Port | Key | Use | Section |
|
|
24
24
|
|---|---|---|---|
|
|
25
|
-
| directory | `ports.directory` | agents, skills and blueprints that are not
|
|
25
|
+
| directory | `ports.directory` | agents, skills and blueprints that are not files of a harness folder | [directory](references/ports.md#directory) |
|
|
26
26
|
| placements | `ports.placements: { <kind>: Placement }` | run a session on another machine or service | [placements](references/ports.md#placements) |
|
|
27
27
|
| policy | `ports.policy` | allow, deny or ask before an action | [policy](references/ports.md#policy) |
|
|
28
28
|
| telemetry | `ports.telemetry` | usage, spans and node events, for a live dashboard | [telemetry](references/ports.md#telemetry) |
|
|
@@ -31,7 +31,7 @@ plain object for each port.
|
|
|
31
31
|
| channels | `ports.channels: { <id>: Channel }` | send messages and listen for them | [channels](references/ports.md#channels) |
|
|
32
32
|
| store | `ports.store` | session history and scratchpads off this machine | [store](references/ports.md#store) |
|
|
33
33
|
| workspace | `ports.workspace` | the workspace of a team or an agent off this machine | [workspace](references/ports.md#workspace) |
|
|
34
|
-
| harnesses | `ports.harnesses` |
|
|
34
|
+
| harnesses | `ports.harnesses` | the adapter of a harness: its folder, agent files and install, and the turns on its agent program | [harnesses](references/ports.md#harnesses) |
|
|
35
35
|
| panels | `ports.panels: { <id>: Panel }` | rows a surface shows, and the `missions` and `peers` panels | [panels](references/ports.md#panels) |
|
|
36
36
|
| actions | `ports.actions` | run the action types of the ontology | [actions](references/ports.md#actions) |
|
|
37
37
|
| workflows | `ports.workflows` | run workflow files on an engine | [workflows](references/ports.md#workflows) |
|
|
@@ -88,7 +88,7 @@ export function plugin(context: PluginContext): PluginImplementation {
|
|
|
88
88
|
}
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
- `name` must match `orbital-plugin.json` and the key in `
|
|
91
|
+
- `name` must match `orbital-plugin.json` and the key in `orbital.yaml`.
|
|
92
92
|
- The SDK builds the manifest from this object. `provides` comes from the ports
|
|
93
93
|
you give.
|
|
94
94
|
- Validate `context.init.config` at the start. Throw an error that names the
|
|
@@ -118,23 +118,22 @@ npm test # build, then the conformance checks and your own checks
|
|
|
118
118
|
|
|
119
119
|
### 4. Check through the real protocol
|
|
120
120
|
|
|
121
|
-
In a project with a
|
|
121
|
+
In a project with a harness folder (`orbital install claude`):
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
124
|
orbital plugin add ./<name>
|
|
125
125
|
orbital plugin check <name> --fixtures <name>/fixtures.json
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
`plugin add` prints the permissions, and names a folder in `
|
|
128
|
+
`plugin add` prints the permissions, and names a folder in `orbital.yaml` by its
|
|
129
129
|
path. It does not copy it. Rebuild, and the next start of the node uses the new
|
|
130
130
|
build. `plugin check` starts the built plugin with the config of its
|
|
131
|
-
`
|
|
131
|
+
`orbital.yaml` entry and the secrets of `orbital plugin secret set`.
|
|
132
132
|
|
|
133
133
|
### 5. Add to the config
|
|
134
134
|
|
|
135
|
-
The node's file is `
|
|
136
|
-
`.claude/orbital.yaml
|
|
137
|
-
folder. This skill says `config.yaml` for both.
|
|
135
|
+
The node's file is `orbital.yaml` in a harness folder, such as
|
|
136
|
+
`.claude/orbital.yaml`.
|
|
138
137
|
|
|
139
138
|
```yaml
|
|
140
139
|
plugins:
|
|
@@ -201,12 +200,12 @@ start stops the start with "A plugin did not start: …".
|
|
|
201
200
|
|
|
202
201
|
| Symptom | Cause |
|
|
203
202
|
|---|---|
|
|
204
|
-
| "is X, and
|
|
203
|
+
| "is X, and orbital.yaml names it Y" | The manifest `name` differs from the `orbital.yaml` key. |
|
|
205
204
|
| "exited (code N) before it answered" | It threw at start, or it wrote to stdout. Read the node's log for its stderr. |
|
|
206
205
|
| "did not declare the secret" | Add the name to `permissions.secrets`. |
|
|
207
206
|
| "declares the variables [...] as secrets, and its orbital-plugin.json declares env: [...]" | Make the secrets in variable form and `env` the same. |
|
|
208
207
|
| Refused with `offline: true` | It declares a network host. |
|
|
209
|
-
| "chooses the plugin X, and this host has no plugin of that name" | A port choice in `
|
|
208
|
+
| "chooses the plugin X, and this host has no plugin of that name" | A port choice in `orbital.yaml` names a plugin that `plugins:` does not have. |
|
|
210
209
|
| "chooses X for the Y port, and that plugin provides none" | The plugin does not give that port, or the manifest leaves it out. |
|
|
211
210
|
| "No plugin of this node serves the placement" | No plugin has the placement kind in `ports.placements`. |
|
|
212
211
|
| A send check fails with another ref | A second send of a message id must answer the first ref and send nothing. |
|
|
@@ -220,8 +219,11 @@ In the source repository of orbital:
|
|
|
220
219
|
|
|
221
220
|
- `plugins/channel-slack`: a channel with a Socket Mode service, `host.deliver`, idempotent send, secrets, a panel and a command.
|
|
222
221
|
- `plugins/policy-judge`: a policy that calls a model through `host.completeModel`.
|
|
222
|
+
- `plugins/gatekeeper`: a policy that asks an agent of the receiving team through `host.askAgent` if a message from the network may reach the team.
|
|
223
223
|
- `plugins/store-object`: a store in an object store.
|
|
224
224
|
- `plugins/reasoner-open-ontologies`: a reasoner.
|
|
225
225
|
- `packages/orbital/src/core/node/static-directory.ts`: a directory, a `sessions.create` hook and a placement that send `<agent>@<node>` to another node.
|
|
226
226
|
- `packages/orbital/__tests__/core/fixtures/echo-plugin`: session hooks, a directory, a hosted tool, a panel, a service that submits a turn, and a command.
|
|
227
227
|
- `packages/orbital/__tests__/core/fixtures/echo-harness`: a harness and its turn program.
|
|
228
|
+
- `packages/orbital-harness-pi`: the adapter of the harness `pi`: its folder `.pi`, its install, and a turn program on the Pi coding agent.
|
|
229
|
+
- `packages/orbital-harness-codex`: the adapter of the harness `codex`: its folder `.codex`, its install in `config.toml`, and a turn program on the OpenAI Codex CLI.
|
|
@@ -35,6 +35,7 @@ message. Catch it where a failure must not stop the plugin.
|
|
|
35
35
|
| `a2a(body)` | the JSON-RPC answer | Call the node's A2A server. |
|
|
36
36
|
| `deliver(inbound)` | `DeliveryAnswer` | Give the node a message that a channel received. |
|
|
37
37
|
| `completeModel(request, signal?)` | `ModelCompletion` | Make one model call on the person's keys. |
|
|
38
|
+
| `askAgent(request)` | `{ answer: string }` | Ask one agent of a team one question, and wait for its answer. |
|
|
38
39
|
| `startMission(start)` | `MissionStarted` | Start a mission from an event. |
|
|
39
40
|
| `missions()` | `MissionRecord[]` | Read the missions of this node. |
|
|
40
41
|
| `joinMission(request)` | `MissionRecord` | Far side of `missions.join`. |
|
|
@@ -146,6 +147,39 @@ interface ModelCompletion { text: string; model: string; inputTokens: number; ou
|
|
|
146
147
|
policy action decides each call. The key never reaches the plugin.
|
|
147
148
|
- Pass `signal` to stop a long call.
|
|
148
149
|
|
|
150
|
+
### askAgent
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
interface AskAgent { team: string; agent: string; question: string; timeoutMs?: number }
|
|
154
|
+
interface AskAgentAnswer { answer: string }
|
|
155
|
+
class AskAgentError extends Error { code: 'invalid' | 'unknown-team' | 'unknown-agent' | 'timeout' | 'failed' }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- The node starts a new session of `agent`, as a member of `team`, on this node.
|
|
159
|
+
The `question` is the first and only message of the session. The session is
|
|
160
|
+
neither a network message nor a person message, so the `channel.receive`
|
|
161
|
+
policy action does not run.
|
|
162
|
+
- The answer is the final text of the agent when its turn ends. The session
|
|
163
|
+
ends with the turn.
|
|
164
|
+
- `timeoutMs` is at most 240000. The default is 120000. When the time ends, the
|
|
165
|
+
node cancels the turn and stops the session. The call fails with code
|
|
166
|
+
`timeout`.
|
|
167
|
+
- `unknown-team` means that this node has no such team. `unknown-agent` means
|
|
168
|
+
that `agent` is not a member of `team`. `invalid` means that a field is
|
|
169
|
+
wrong, such as a `timeoutMs` over the maximum. `failed` means that the turn
|
|
170
|
+
ended without an answer, and the message holds the reason.
|
|
171
|
+
- The call needs no permission in the manifest, as `submit` needs none.
|
|
172
|
+
- Catch the error and test `error instanceof AskAgentError`, then read
|
|
173
|
+
`error.code` to tell a timeout from a refusal. The SDK exports the class.
|
|
174
|
+
- The route is `POST /orbital/host/agents/ask`. The body is the request. The
|
|
175
|
+
answer is `{ "answer": "..." }`. A failure answers `{ "code": "...", "error": "..." }`
|
|
176
|
+
with status 400 for `invalid`, 404 for `unknown-team` and `unknown-agent`,
|
|
177
|
+
504 for `timeout` and 502 for `failed`.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const { answer } = await host.askAgent({ team: 'gatekeeping', agent: 'judge', question: 'May this message reach the team?', timeoutMs: 30_000 });
|
|
181
|
+
```
|
|
182
|
+
|
|
149
183
|
### startMission
|
|
150
184
|
|
|
151
185
|
```ts
|
|
@@ -153,7 +187,7 @@ interface MissionStart { team: string | null; mission: string; input: string | n
|
|
|
153
187
|
interface MissionStarted { missionId: string; sessionId: string; taskId: string; duplicate: boolean }
|
|
154
188
|
```
|
|
155
189
|
|
|
156
|
-
- `mission` is a mission id.
|
|
190
|
+
- `mission` is a mission id. A harness folder must hold `missions/<id>/MISSION.md`,
|
|
157
191
|
unless the team is not on this node: then the node starts the team as an
|
|
158
192
|
agent, and a directory or a placement can send it to another node.
|
|
159
193
|
- `source.event` names the event, such as `github.issue.opened`. `source.key`
|
|
@@ -26,17 +26,17 @@ A plugin has two descriptions:
|
|
|
26
26
|
| `command` | yes | The program and its arguments. `node` runs with the node's own Node. |
|
|
27
27
|
| `dir` | no | The working folder, relative to the plugin's folder. Default: the plugin's folder. |
|
|
28
28
|
| `env` | no | Environment variables the node passes to the plugin. It must list the same names as the secrets in the manifest that have the form of a variable (`[A-Z][A-Z0-9_]*`). |
|
|
29
|
-
| `config` | no | Default config. The `config` of the `
|
|
29
|
+
| `config` | no | Default config. The `config` of the `orbital.yaml` entry overrides it key by key. |
|
|
30
30
|
|
|
31
|
-
No other field is allowed. A `
|
|
31
|
+
No other field is allowed. A `orbital.yaml` source `exec:<command> <args>`
|
|
32
32
|
starts a program on PATH with no `orbital-plugin.json`. Only such an entry can
|
|
33
|
-
name `env:` in `
|
|
33
|
+
name `env:` in `orbital.yaml`.
|
|
34
34
|
|
|
35
35
|
The node starts the plugin with a small environment: `PATH`, `HOME`, the
|
|
36
36
|
locale, the temp folders, the proxy and certificate variables, and the
|
|
37
37
|
variables in `env`. Any other variable of the node does not reach the plugin.
|
|
38
38
|
|
|
39
|
-
## The
|
|
39
|
+
## The orbital.yaml entry
|
|
40
40
|
|
|
41
41
|
```yaml
|
|
42
42
|
plugins:
|
|
@@ -67,7 +67,7 @@ interface PluginManifest {
|
|
|
67
67
|
channels: Array<{ id: string; capabilities: ChannelCapabilities }>;
|
|
68
68
|
store: { kinds: StateKind[]; lease: boolean } | null;
|
|
69
69
|
workspace: boolean;
|
|
70
|
-
harnesses: Array<{ id: string; description: string; callsModel: boolean }>;
|
|
70
|
+
harnesses: Array<{ id: string; description: string; callsModel: boolean; browser: boolean }>;
|
|
71
71
|
service: { done: boolean } | null;
|
|
72
72
|
actions: boolean;
|
|
73
73
|
workflows: { engines: string[] } | null;
|
|
@@ -80,15 +80,15 @@ interface PluginManifest {
|
|
|
80
80
|
}
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
- `name` must be the key of the plugin in `
|
|
84
|
-
it: "is X, and
|
|
83
|
+
- `name` must be the key of the plugin in `orbital.yaml`. Else the node refuses
|
|
84
|
+
it: "is X, and orbital.yaml names it Y".
|
|
85
85
|
- `ports` must be the version of the node. Build with the SDK of the node's
|
|
86
86
|
version.
|
|
87
87
|
- The node makes the remote ports only from `provides`. A method that you give
|
|
88
88
|
and the manifest does not name is never called.
|
|
89
|
-
- A plugin cannot have the name of a
|
|
90
|
-
`
|
|
91
|
-
`
|
|
89
|
+
- A plugin cannot have the name of a folder of orbital in the harness folder:
|
|
90
|
+
`agents`, `memory`, `missions`, `orbital`, `plans`, `policies`, `sessions`,
|
|
91
|
+
`skills`, `teams`.
|
|
92
92
|
|
|
93
93
|
## Permissions
|
|
94
94
|
|
|
@@ -102,7 +102,7 @@ permissions: {
|
|
|
102
102
|
|
|
103
103
|
| Field | Meaning | What the node does |
|
|
104
104
|
|---|---|---|
|
|
105
|
-
| `network` | Every host the plugin reaches, such as `*.example.net`. | `offline: true` in `
|
|
105
|
+
| `network` | Every host the plugin reaches, such as `*.example.net`. | `offline: true` in `orbital.yaml` refuses a plugin with any host. The node does not sandbox the plugin process: the list is a promise that a person and a policy read. |
|
|
106
106
|
| `secrets` | Every secret the plugin reads. | `context.secret(name)` fails for a name not listed. A name in the form of a variable also comes in the environment. |
|
|
107
107
|
| `models` | Every model the plugin calls through `host.completeModel`, as `<provider>:<model>`. `*` matches any part. | A call to a model that no pattern matches fails. The `model.use` policy decides each call. |
|
|
108
108
|
|
|
@@ -131,7 +131,7 @@ if (!token) throw new Error('No token: run `orbital plugin secret set example ex
|
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
The node keeps secrets in the system's secret store, or in a file of the
|
|
134
|
-
orbital home. A secret never goes in `
|
|
134
|
+
orbital home. A secret never goes in `orbital.yaml` or in `orbital-plugin.json`.
|
|
135
135
|
|
|
136
136
|
## The plugin's folder and the init data
|
|
137
137
|
|
|
@@ -147,8 +147,8 @@ interface PluginInit {
|
|
|
147
147
|
|
|
148
148
|
- `config`: the merged config. Validate it at the start, and throw an error that
|
|
149
149
|
names the bad key. The node shows it.
|
|
150
|
-
- `folder`: `<
|
|
150
|
+
- `folder`: `<harness folder>/<plugin name>`, such as `.claude/lan/`. Put the
|
|
151
151
|
files that a person writes for the plugin there. The folder can be absent, so
|
|
152
|
-
give a default. It is null when the node has no
|
|
152
|
+
give a default. It is null when the node has no harness folder.
|
|
153
153
|
- `node.name`: the name of this node. A cross-machine plugin uses it to
|
|
154
154
|
introduce itself to peers.
|
|
@@ -4,7 +4,7 @@ Each section has the same parts:
|
|
|
4
4
|
|
|
5
5
|
- **For**: what the port does for the node.
|
|
6
6
|
- **When**: when the node calls each method.
|
|
7
|
-
- **Config**: how `
|
|
7
|
+
- **Config**: how `orbital.yaml` chooses the plugin for the port.
|
|
8
8
|
- **Interface**: the TypeScript interface, as `ports.ts` in `@kaiba-cloud/orbital` declares it.
|
|
9
9
|
- **Remote methods**: the JSON-RPC methods for a plugin out of process, with their named params. The SDK maps them to your object. You need them only when you write a plugin without the SDK.
|
|
10
10
|
- **Example**: a minimal port that passes `orbital plugin check`.
|
|
@@ -13,29 +13,27 @@ Every type comes from `@kaiba-cloud/orbital-sdk`. Each argument and each answer
|
|
|
13
13
|
is plain JSON. A method that the interface marks with `?` is optional. Leave it
|
|
14
14
|
out, and the manifest says that the plugin does not provide it.
|
|
15
15
|
|
|
16
|
-
The order of plugins: first the plugins of `
|
|
16
|
+
The order of plugins: first the plugins of `orbital.yaml`, in the order of
|
|
17
17
|
`plugins:`, then the built-in plugins `orbital`, `local`, `static`, `git` and
|
|
18
18
|
`folder`. "In plugin order" below means this order. So your plugin answers a
|
|
19
19
|
hook such as `sessions.create` before a built-in plugin does.
|
|
20
20
|
|
|
21
21
|
## `directory`
|
|
22
22
|
|
|
23
|
-
**For.** Give the node agents
|
|
24
|
-
|
|
23
|
+
**For.** Give the node agents and blueprints that are not files of a harness
|
|
24
|
+
folder. Examples: a registry of agents, or the agents of other machines.
|
|
25
25
|
|
|
26
26
|
**When.**
|
|
27
27
|
|
|
28
28
|
- `agents(workspaceDir)`: the node lists its agents, for `agent list`, for the
|
|
29
|
-
catalog and for `agentPeers`.
|
|
29
|
+
catalog and for `agentPeers`. An agent file with the same name hides a
|
|
30
30
|
directory agent.
|
|
31
|
-
- `lookup(ref)`: a run names an agent that
|
|
31
|
+
- `lookup(ref)`: a run names an agent that no harness folder has. The
|
|
32
32
|
node asks each directory in order. The first answer that is not null wins.
|
|
33
33
|
- `agent(id, session)`: a session of an agent from this directory starts. The
|
|
34
34
|
answer gives its harness, egress and bootstrap. The node reads these
|
|
35
35
|
`bootstrap` keys: `systemPrompt`, `agentModel`, `provider`, `harness` and
|
|
36
36
|
`skills` (a list of `ResolvedSkill`).
|
|
37
|
-
- `skill(slug, session)`: an agent names a skill that the definition does not
|
|
38
|
-
have. The first answer that is not null wins.
|
|
39
37
|
- `blueprint(id, session)` and `blueprintFile(id, path, session)`: a session of
|
|
40
38
|
an agent from this directory calls `GET /orbital/blueprints/<id>` or
|
|
41
39
|
`GET /orbital/blueprints/<id>/file?path=<path>` on the node.
|
|
@@ -54,7 +52,6 @@ interface Directory {
|
|
|
54
52
|
agents(workspaceDir: string): Promise<DirectoryAgentSummary[]>;
|
|
55
53
|
lookup(ref: string): Promise<DirectoryAgentRef | null>;
|
|
56
54
|
agent(id: string, session: SessionInfo): Promise<ResolvedAgent | null>;
|
|
57
|
-
skill(slug: string, session: SessionInfo): Promise<ResolvedSkill | null>;
|
|
58
55
|
blueprint?(id: string, session: SessionInfo): Promise<BlueprintContent | null>;
|
|
59
56
|
blueprintFile?(id: string, path: string, session: SessionInfo): Promise<BlueprintFile | null>;
|
|
60
57
|
announce?(catalog: MachineCatalog): Promise<void>;
|
|
@@ -68,7 +65,7 @@ interface BlueprintFile { path: string; mimeType: string | null; size: number; c
|
|
|
68
65
|
```
|
|
69
66
|
|
|
70
67
|
**Remote methods.** `directory.agents {workspaceDir}`, `directory.lookup {ref}`,
|
|
71
|
-
`directory.agent {id, session}`,
|
|
68
|
+
`directory.agent {id, session}`,
|
|
72
69
|
`directory.blueprint {id, session}`, `directory.blueprintFile {id, path, session}`,
|
|
73
70
|
`directory.announce {catalog}`.
|
|
74
71
|
|
|
@@ -85,13 +82,12 @@ ports: {
|
|
|
85
82
|
const found = agents.find((one) => one.id === id);
|
|
86
83
|
return found ? { id: found.id, name: found.name, harness: null, egress: [], bootstrap: { systemPrompt: found.prompt } } : null;
|
|
87
84
|
},
|
|
88
|
-
skill: async () => null,
|
|
89
85
|
},
|
|
90
86
|
},
|
|
91
87
|
```
|
|
92
88
|
|
|
93
89
|
The check: `agents()` answers `{ name, id, description }` items, and `lookup()`
|
|
94
|
-
|
|
90
|
+
of an unknown name answers null.
|
|
95
91
|
|
|
96
92
|
## `placements`
|
|
97
93
|
|
|
@@ -173,13 +169,73 @@ it, the check skips.
|
|
|
173
169
|
| `message.send` | One agent sends a message to another | `fromSessionId`, `toSessionId`, `rootSessionId`, `hops`, `at` |
|
|
174
170
|
| `model.use` | A model call, also `host.completeModel` | `sessionId`, `model`, `plugin` |
|
|
175
171
|
| `plugin.load` | A plugin starts (see the note below) | `plugin`, `network`, `secrets`, `models` |
|
|
176
|
-
| `channel.receive` | A
|
|
172
|
+
| `channel.receive` | A message reaches the node from a channel or over the network (see the section below) | `channel`, `from`, `to`, `human`, `text`, `origin` |
|
|
177
173
|
| `action.submit` | An agent submits an action type | `sessionId`, `actionType`, `actor` |
|
|
178
174
|
| `mission.start` | A mission starts | `team`, `mission`, `plugin`, `event`, `key` |
|
|
179
175
|
|
|
180
176
|
Note: the node checks `plugin.load` while it starts the plugins, before any
|
|
181
177
|
policy engine runs. In this version no engine sees that action from a node.
|
|
182
178
|
|
|
179
|
+
### `channel.receive`
|
|
180
|
+
|
|
181
|
+
The action gives an engine the text of a message and where it came from, so an
|
|
182
|
+
engine can govern each message that reaches the node over the network.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
type ChannelTrust = 'verified' | 'unverified';
|
|
186
|
+
type ChannelOrigin =
|
|
187
|
+
| { kind: 'local' }
|
|
188
|
+
| { kind: 'network'; peer: string; address: Address; trust: ChannelTrust };
|
|
189
|
+
type ChannelReceive = {
|
|
190
|
+
kind: 'channel.receive';
|
|
191
|
+
channel: string;
|
|
192
|
+
from: Address;
|
|
193
|
+
to: Address;
|
|
194
|
+
human: boolean;
|
|
195
|
+
text: string;
|
|
196
|
+
origin: ChannelOrigin;
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `text` is the text of the message. A call with no text, such as a cancel or a
|
|
201
|
+
pause, gives a short phrase that names the operation, for example
|
|
202
|
+
`cancel the task t1`.
|
|
203
|
+
- `human` is true when the sender is a person.
|
|
204
|
+
- `origin.peer` is the name of the sending node when the sender address has an
|
|
205
|
+
`ade`. Else it is the name of the sender, or the name that the carrying
|
|
206
|
+
plugin gives.
|
|
207
|
+
- `origin.address` is the address of the sender.
|
|
208
|
+
- `origin.trust` is `verified` only when the channel declares
|
|
209
|
+
`authenticated: true`. Every other path gives `unverified`.
|
|
210
|
+
- `origin.kind` is `local` only for a message on a channel that declares
|
|
211
|
+
`authenticated: true` when the sender address names this node in `ade`. The
|
|
212
|
+
address of a message that an unauthenticated channel delivers is the word of
|
|
213
|
+
the sender, so the node never trusts it as local.
|
|
214
|
+
- A session of the node, a holder of the node secret and the CLI on the same
|
|
215
|
+
machine never reach this action. A request over HTTP reaches it when its
|
|
216
|
+
socket address is not a loopback address.
|
|
217
|
+
|
|
218
|
+
The node checks `channel.receive` on these paths, and each refusal gives the
|
|
219
|
+
reason to the sender:
|
|
220
|
+
|
|
221
|
+
| Path | The sender gets |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `host.deliver`: work, a cancel, the answer of a person to a question | `DeliveryAnswer` with `accepted: false` and the `reason`. For refused work, the node also sends a reply over the same channel (see below) |
|
|
224
|
+
| `host.joinMission`, `replyMission`, `sayMission`, `pauseMission`, `resumeMission`, `askGiver` | an error with the reason |
|
|
225
|
+
| `host.a2a`, and a request to `/a2a` from a non-loopback address | a JSON-RPC error with the reason |
|
|
226
|
+
|
|
227
|
+
The answer of a remote node to a request of this node is not checked: this
|
|
228
|
+
node made the request.
|
|
229
|
+
|
|
230
|
+
When the node refuses work that a channel delivered, it also sends the sender a
|
|
231
|
+
reply envelope over the same channel. The reply is in the same thread, from the
|
|
232
|
+
receiver to the sender. Its message answers the request (`orbital/inReplyTo`)
|
|
233
|
+
with a status update of the state `TASK_STATE_REJECTED`. The text of the status
|
|
234
|
+
message is `refused: <reason>`. The node of the sender turns the reply into the
|
|
235
|
+
status of the task that its session reads, so the sender sees the request end
|
|
236
|
+
as rejected with the reason as its result. A refused cancel and a refused
|
|
237
|
+
answer of a person get no reply.
|
|
238
|
+
|
|
183
239
|
The node asks each engine in order. One `deny` refuses the action. An `ask`
|
|
184
240
|
waits for a person on the team's channel. An engine with
|
|
185
241
|
`deterministic: false`, such as a model, can only deny or ask: the node reads
|
|
@@ -336,7 +392,7 @@ interface SessionContribution { env?: Record<string, string>; shims?: Record<str
|
|
|
336
392
|
interface SessionInfo {
|
|
337
393
|
sessionId: string; node: string; agent: string; agentId: string | null; agentSource: string | null;
|
|
338
394
|
declaration: Record<string, unknown> | null; parentSessionId: string | null; workspaceDir: string;
|
|
339
|
-
|
|
395
|
+
harness: Harness; team: string | null; mission: string | null;
|
|
340
396
|
scratchpad: string | null; memory: string | null; missionScratchpad: string | null;
|
|
341
397
|
}
|
|
342
398
|
```
|
|
@@ -419,7 +475,7 @@ email, a file. The key of the record is the channel id.
|
|
|
419
475
|
**When.**
|
|
420
476
|
|
|
421
477
|
- `listen(bindings)`: when the node starts serving, with the bindings of the
|
|
422
|
-
started teams and of `channels:` in `
|
|
478
|
+
started teams and of `channels:` in `orbital.yaml`. A later call replaces the
|
|
423
479
|
bindings. After a restart of the plugin, the node calls it again.
|
|
424
480
|
- `send(envelope, binding)`: the node sends a message on the binding, such as an
|
|
425
481
|
answer or a question that waits for a person.
|
|
@@ -428,7 +484,7 @@ A message that arrives on the service goes to `host.deliver(inbound)`.
|
|
|
428
484
|
|
|
429
485
|
**Config.** A team names the channel in `teams/<team>/channels.yaml` as
|
|
430
486
|
`type: <id>`. The other keys of the entry are the binding's `config`. A node
|
|
431
|
-
names its own channels under `channels:` in `
|
|
487
|
+
names its own channels under `channels:` in `orbital.yaml`. The node uses the
|
|
432
488
|
first plugin that provides the id.
|
|
433
489
|
|
|
434
490
|
**Interface.**
|
|
@@ -439,7 +495,7 @@ interface Channel {
|
|
|
439
495
|
send(envelope: ChannelEnvelope, binding: ChannelBinding): Promise<{ ref: string; thread: string | null }>;
|
|
440
496
|
listen(bindings: readonly ChannelBinding[]): Promise<void>;
|
|
441
497
|
}
|
|
442
|
-
interface ChannelCapabilities { live: boolean; human: boolean; threads: boolean; durable: boolean; offline: boolean }
|
|
498
|
+
interface ChannelCapabilities { live: boolean; human: boolean; threads: boolean; durable: boolean; offline: boolean; authenticated: boolean }
|
|
443
499
|
interface ChannelBinding { id: string; channel: string; config: Record<string, unknown>; team: string | null; ade: string }
|
|
444
500
|
interface ChannelEnvelope { message: Message; from: Address; to: Address; thread: string | null }
|
|
445
501
|
interface Address { kind: 'agent' | 'team' | 'session' | 'person' | 'node'; name: string; ade?: string }
|
|
@@ -458,7 +514,7 @@ let bindings: ChannelBinding[] = [];
|
|
|
458
514
|
ports: {
|
|
459
515
|
channels: {
|
|
460
516
|
discord: {
|
|
461
|
-
capabilities: { live: true, human: true, threads: true, durable: true, offline: false },
|
|
517
|
+
capabilities: { live: true, human: true, threads: true, durable: true, offline: false, authenticated: true },
|
|
462
518
|
send: async (envelope, binding) => {
|
|
463
519
|
const known = sent.get(envelope.message.messageId);
|
|
464
520
|
if (known) return known;
|
|
@@ -474,7 +530,10 @@ ports: {
|
|
|
474
530
|
```
|
|
475
531
|
|
|
476
532
|
- `capabilities`: `live` (it pushes), `human` (a person reads and writes),
|
|
477
|
-
`threads`, `durable`, `offline` (it works with no network outside the LAN)
|
|
533
|
+
`threads`, `durable`, `offline` (it works with no network outside the LAN),
|
|
534
|
+
`authenticated` (the channel proves who sent each message, so the node marks
|
|
535
|
+
its messages `verified`). Set `authenticated: false` when the channel only
|
|
536
|
+
carries the word of the sender, such as a shared folder.
|
|
478
537
|
- A second send of the same message id must answer the first ref and send
|
|
479
538
|
nothing.
|
|
480
539
|
- A message id must be stable: the node drops a second delivery of the same id.
|
|
@@ -498,8 +557,8 @@ at a time holds a unit.
|
|
|
498
557
|
never goes there.
|
|
499
558
|
- `temporary`: short-lived files.
|
|
500
559
|
|
|
501
|
-
**Config.** Only an explicit choice: `store: { history: [{ plugin: r2, bucket:
|
|
502
|
-
`settings` is the choice without `plugin`: here `{ bucket: '
|
|
560
|
+
**Config.** Only an explicit choice: `store: { history: [{ plugin: r2, bucket: orbital }] }`.
|
|
561
|
+
`settings` is the choice without `plugin`: here `{ bucket: 'orbital' }`. A store
|
|
503
562
|
whose `kinds` do not hold the kind refuses the start.
|
|
504
563
|
|
|
505
564
|
**Interface.**
|
|
@@ -552,7 +611,7 @@ session's own (`ephemeral`) workspace is never saved.
|
|
|
552
611
|
**When.** `load(ref, dir)` when the node makes the workspace on a machine that
|
|
553
612
|
has none of it. `save(ref, dir, ignore)` after each turn.
|
|
554
613
|
|
|
555
|
-
**Config.** Only an explicit choice: `store: { workspace: [{ plugin: r2, bucket:
|
|
614
|
+
**Config.** Only an explicit choice: `store: { workspace: [{ plugin: r2, bucket: orbital }] }`.
|
|
556
615
|
|
|
557
616
|
**Interface.**
|
|
558
617
|
|
|
@@ -588,45 +647,65 @@ ports: {
|
|
|
588
647
|
|
|
589
648
|
## `harnesses`
|
|
590
649
|
|
|
591
|
-
**For.**
|
|
650
|
+
**For.** Bring a harness to orbital: run turns on another agent program, such
|
|
651
|
+
as a CLI agent, and own the folder of that harness in a project (its agent
|
|
652
|
+
files, its `orbital.yaml`, and how it connects to orbital). One port does both:
|
|
653
|
+
it is the adapter of the harness. `claude` is the only harness built in;
|
|
654
|
+
every other harness, `pi` among them, is a plugin on this port.
|
|
592
655
|
|
|
593
656
|
**When.** A session's harness is one of the ids. For each turn, the node calls
|
|
594
657
|
`command(spec)`, writes the spec to a file, sets `ORBITAL_HARNESS_TURN` to its
|
|
595
658
|
path, and runs the command in the sandbox.
|
|
596
659
|
|
|
597
660
|
**Config.** None. An agent or a team member names the harness id. The node uses
|
|
598
|
-
the first plugin that brings the id.
|
|
661
|
+
the first plugin that brings the id. A harness with a `folder` is set up in a
|
|
662
|
+
project with `orbital install <id> --plugin <npm package|path>`.
|
|
599
663
|
|
|
600
664
|
**Interface.**
|
|
601
665
|
|
|
602
666
|
```ts
|
|
603
667
|
interface HarnessProvider {
|
|
604
|
-
readonly harnesses: ReadonlyArray<{ id: string; description: string; callsModel?: boolean }>;
|
|
668
|
+
readonly harnesses: ReadonlyArray<{ id: string; description: string; callsModel?: boolean; browser?: boolean; folder?: HarnessFolder | null }>;
|
|
605
669
|
command(spec: HarnessTurnSpec): Promise<HarnessCommand>;
|
|
670
|
+
install?(request: HarnessInstallRequest): Promise<{ written: string[]; notes: string[] }>;
|
|
671
|
+
agents?(request: HarnessAgentsRequest): Promise<HarnessAgentFile[]>;
|
|
672
|
+
}
|
|
673
|
+
interface HarnessFolder {
|
|
674
|
+
folder: string; // '.foo': a dot and the name, at the root of the project
|
|
675
|
+
instructions?: string | null; // the harness's instructions file, such as FOO.md
|
|
676
|
+
agents?: string; // the folder of the agent files, default 'agents'
|
|
677
|
+
skills?: string | null; // the folder of the skills
|
|
678
|
+
agentFiles?: 'markdown' | 'plugin'; // markdown with frontmatter (default), or agents() reads them
|
|
679
|
+
agentHarness?: 'adapter' | 'node'; // an agent file that names no harness runs on this one (default), or on the node's default
|
|
606
680
|
}
|
|
681
|
+
interface HarnessInstallRequest { harness: string; workspaceDir: string; folder: string; program: string[]; force: boolean }
|
|
682
|
+
interface HarnessAgentsRequest { harness: string; workspaceDir: string; folder: string; dir: string }
|
|
683
|
+
interface HarnessAgentFile { name: string; description?: string | null; systemPrompt: string; model?: string | null; harness?: string | null }
|
|
607
684
|
interface HarnessCommand { command: string[]; env?: Record<string, string>; egress?: string[] }
|
|
608
685
|
interface HarnessTurnSpec {
|
|
609
686
|
harness: string; sessionId: string; prompt: string; systemPrompt: string; model: string | null;
|
|
610
|
-
|
|
687
|
+
workspaceDir: string; sessionDir: string; resume: boolean; env: Record<string, string>;
|
|
611
688
|
credentials: Array<{ provider: string; authType: string; payload: string }>;
|
|
612
689
|
tools: { command: string[]; env: Record<string, string> };
|
|
690
|
+
browser: { url: string; headers: Record<string, string> } | null;
|
|
613
691
|
scratchpad: string | null; memory: string | null; missionScratchpad: string | null;
|
|
614
692
|
}
|
|
615
693
|
```
|
|
616
694
|
|
|
617
|
-
**Remote methods.** `harness.command {spec}`.
|
|
695
|
+
**Remote methods.** `harness.command {spec}`, `harness.install {request}`, `harness.agents {request}`.
|
|
618
696
|
|
|
619
697
|
**Example.**
|
|
620
698
|
|
|
621
699
|
```ts
|
|
622
700
|
ports: {
|
|
623
701
|
harnesses: {
|
|
624
|
-
harnesses: [{ id: 'my-agent', description: 'My agent CLI.', callsModel: false }],
|
|
702
|
+
harnesses: [{ id: 'my-agent', description: 'My agent CLI.', callsModel: false, folder: { folder: '.my-agent', instructions: 'MY-AGENT.md' } }],
|
|
625
703
|
command: async () => ({
|
|
626
704
|
command: [process.execPath, fileURLToPath(new URL('turn.js', import.meta.url))],
|
|
627
705
|
env: {},
|
|
628
706
|
egress: ['api.openai.com'],
|
|
629
707
|
}),
|
|
708
|
+
install: async (request) => registerMcpServer(request),
|
|
630
709
|
},
|
|
631
710
|
},
|
|
632
711
|
```
|
|
@@ -636,8 +715,35 @@ the node then needs a provider key for the agent's model and gives it in
|
|
|
636
715
|
`credentials`. Set `callsModel: false` for a program that uses its own login or
|
|
637
716
|
calls no model.
|
|
638
717
|
|
|
639
|
-
The
|
|
640
|
-
|
|
718
|
+
**The shared browser.** `browser` is false by default. A harness that sets
|
|
719
|
+
`browser: true` gets `spec.browser` in each turn: the URL of the shared browser
|
|
720
|
+
of the node, an MCP server over streamable HTTP, and the headers that name the
|
|
721
|
+
node token and the session. The harness then sends the browser calls of the
|
|
722
|
+
agent there, in place of the chrome-devtools server of its own config. A
|
|
723
|
+
harness that sets no `browser` gets `spec.browser: null` and has no browser
|
|
724
|
+
tools.
|
|
725
|
+
|
|
726
|
+
**The folder.** A harness with a `folder` owns that folder in a project, as
|
|
727
|
+
`claude` owns `.claude`: `orbital.yaml`, `teams/`, `missions/`, `policies/`,
|
|
728
|
+
`orbital/plugins/` and its agent files. `orbital-plugin.json` declares the same
|
|
729
|
+
folder under `harnesses`, so a project finds the adapter before the plugin
|
|
730
|
+
runs; the node refuses a plugin whose two declarations differ:
|
|
731
|
+
|
|
732
|
+
```json
|
|
733
|
+
{ "name": "my-agent", "command": ["node", "dist/plugin.js"],
|
|
734
|
+
"harnesses": [{ "id": "my-agent", "folder": ".my-agent", "instructions": "MY-AGENT.md" }] }
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
`orbital install my-agent --plugin <npm package|path>` loads the adapter,
|
|
738
|
+
writes `.my-agent/orbital.yaml` with the plugin under `plugins:` (so a clone
|
|
739
|
+
finds it), and calls `install`. `install` connects the harness to orbital, for
|
|
740
|
+
example it registers the orbital MCP server in the harness's own config:
|
|
741
|
+
`request.program` is the command that runs orbital, and the server is
|
|
742
|
+
`[...program, 'mcp', '--attach', '--harness', <id>, '--agent', <id>]`. A second
|
|
743
|
+
`install` writes nothing. The orbital files of every harness folder that
|
|
744
|
+
an adapter knows load together; a team runs where its folder is.
|
|
745
|
+
|
|
746
|
+
The harnesses a node runs are `claude` and the ids of its harness plugins. A node refuses to start when a charter member or an agent names
|
|
641
747
|
a harness outside them, and names the ones it runs. A member runs on the
|
|
642
748
|
harness its charter names, also when its lead dispatches it.
|
|
643
749
|
|
|
@@ -785,7 +891,7 @@ port.
|
|
|
785
891
|
starts a run, sends it a signal, reads its state and cancels it. The node uses
|
|
786
892
|
the first plugin whose `engines` holds the engine.
|
|
787
893
|
|
|
788
|
-
**Config.** `workflowBindings:` in `
|
|
894
|
+
**Config.** `workflowBindings:` in `orbital.yaml` binds a file suffix to an
|
|
789
895
|
engine name: `- { engine: <name>, suffix: .bpmn }`. The defaults bind
|
|
790
896
|
`.sw.yaml`, `.sw.json`, `.bpmn` and `.arazzo.yaml`.
|
|
791
897
|
|