@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/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.1",
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 (also `atom orbital`) — 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.
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 `config.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. |
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 definition files | [directory](references/ports.md#directory) |
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` | run turns on another agent program | [harnesses](references/ports.md#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 `config.yaml`.
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 definition (`orbital init`):
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 `config.yaml` by its
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
- `config.yaml` entry and the secrets of `orbital plugin secret set`.
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 `config.yaml` in a definition folder, or
136
- `.claude/orbital.yaml` in a project where `orbital init` found a `.claude`
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 config.yaml names it Y" | The manifest `name` differs from the `config.yaml` key. |
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 `config.yaml` names a plugin that `plugins:` does not have. |
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. The definition must hold `missions/<id>/MISSION.md`,
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 `config.yaml` entry overrides it key by key. |
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 `config.yaml` source `exec:<command> <args>`
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 `config.yaml`.
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 config.yaml entry
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 `config.yaml`. Else the node refuses
84
- it: "is X, and config.yaml names it Y".
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 definition folder: `agents`,
90
- `blueprints`, `local`, `policies`, `skills`, `system`, `teams`, `vendor`,
91
- `workflows`.
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 `config.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. |
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 `config.yaml` or in `orbital-plugin.json`.
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`: `<definition folder>/<plugin name>`, such as `.atom/lan/`. Put the
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 definition folder.
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 `config.yaml` chooses the plugin for the port.
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 `config.yaml`, in the order 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, skills and blueprints that are not files of the
24
- definition. Examples: a registry of agents, or the agents of other machines.
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`. A definition agent with the same name hides a
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 the definition does not have. The
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}`, `directory.skill {slug, 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
- and `skill()` of an unknown name answer null.
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 channel delivers a message | `channel`, `from`, `to`, `human` |
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
- build: string | null; harness: Harness; team: string | null; mission: string | null;
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 `config.yaml`. A later call replaces the
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 `config.yaml`. The node uses the
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: atom }] }`.
502
- `settings` is the choice without `plugin`: here `{ bucket: 'atom' }`. A store
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: atom }] }`.
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.** Run turns on another agent program, such as a CLI agent.
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
- skillsDir: string; workspaceDir: string; sessionDir: string; resume: boolean; env: Record<string, string>;
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 harnesses a node runs are its runners (`pi`, `claude`) and the ids of its
640
- harness plugins. A node refuses to start when a charter member or an agent names
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 `config.yaml` binds a file suffix to an
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