@rebon/cli-win32-x64 1.2.0 → 1.4.0
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/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +203 -144
- package/THIRD_PARTY_NOTICES.txt +1232 -128
- package/bin/rebon.js +22 -22
- package/lib/windows-managed.js +390 -390
- package/package.json +9 -2
- package/payload/compose-runtime/package.json +17 -17
- package/payload/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -10
- package/payload/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -13
- package/payload/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -21
- package/payload/compose-runtime/payload/compose/shims/dsh-llm.js +313 -313
- package/payload/compose-runtime/payload/compose/shims/dsh-settings.js +43 -43
- package/payload/compose-runtime/payload/compose/shims/dsh-tools.js +20 -20
- package/payload/compose-runtime/payload/compose/shims/dsh-web.js +10 -10
- package/payload/compose-runtime/payload/compose/shims/llm-adapter.js +32 -32
- package/payload/compose-runtime/payload/compose/shims/zod-lite.js +49 -49
- package/payload/compose-runtime/payload/vendor/cordis/LICENSE +21 -0
- package/payload/compose-runtime/payload/vendor/cordis/index.js +1530 -1530
- package/payload/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -21
- package/payload/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -357
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown +21 -0
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown-plugin-gfm +21 -0
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -1193
- package/payload/compose-runtime/payload/vendor/dsh/agent.js +714 -714
- package/payload/compose-runtime/payload/vendor/dsh/llm-core.js +269 -269
- package/payload/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -672
- package/payload/compose-runtime/payload/vendor/dsh/logger-console.js +83 -83
- package/payload/compose-runtime/payload/vendor/dsh/scope.js +287 -287
- package/payload/compose-runtime/payload/vendor/dsh/session.js +1668 -1668
- package/payload/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -309
- package/payload/compose-runtime/payload/vendor/dsh/timeout.js +100 -100
- package/payload/compose-runtime/payload/vendor/dsh/timer.js +128 -128
- package/payload/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -143
- package/payload/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -1527
- package/payload/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -849
- package/payload/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -122
- package/payload/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -21
- package/payload/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -177
- package/payload/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -48
- package/payload/compose-runtime/payload/vendor/schemastery/LICENSE +21 -0
- package/payload/compose-runtime/payload/vendor/schemastery/index.mjs +656 -656
- package/payload/compose-runtime/payload-manifests.json +48 -48
- package/payload/compose-runtime/src/bridge.mjs +193 -195
- package/payload/compose-runtime/src/credentials-runtime.mjs +50 -50
- package/payload/compose-runtime/src/eventsource-stream.mjs +48 -48
- package/payload/compose-runtime/src/index.mjs +7 -7
- package/payload/compose-runtime/src/llm-runtime.mjs +326 -326
- package/payload/compose-runtime/src/loader.mjs +78 -78
- package/payload/compose-runtime/src/loop-assembly.mjs +237 -237
- package/payload/compose-runtime/src/payload.mjs +49 -49
- package/payload/compose-runtime/src/plugin.mjs +59 -59
- package/payload/compose-runtime/src/realm.mjs +223 -223
- package/payload/compose-runtime/src/registry.mjs +191 -191
- package/payload/compose-runtime/src/resolve.mjs +115 -115
- package/payload/compose-runtime/src/systemprompt-runtime.mjs +67 -67
- package/payload/compose-runtime/src/tools-runtime.mjs +239 -239
- package/payload/compose-runtime/src/web-runtime.mjs +209 -209
- package/payload/plugin-host/package.json +11 -11
- package/payload/plugin-host/src/bridge.mjs +64 -64
- package/payload/plugin-host/src/cli.mjs +147 -147
- package/payload/plugin-host/src/framing.mjs +113 -113
- package/payload/plugin-host/src/host.mjs +691 -691
- package/payload/plugin-host/src/json.mjs +208 -208
- package/payload/plugin-host/src/lifecycle.mjs +114 -114
- package/payload/plugin-host/src/loader.mjs +183 -183
- package/payload/plugin-host/src/methods.mjs +377 -377
- package/payload/plugin-host/src/ownership.mjs +57 -57
- package/payload/plugin-host/src/protocol.mjs +129 -129
- package/payload/plugin-host/src/sdk.mjs +7 -7
- package/payload/rebon-boa-helper.exe +0 -0
- package/payload/rebon-browser-mcp.exe +0 -0
- package/payload/rebon-computer-use.exe +0 -0
- package/payload/rebon-lsp-mcp.exe +0 -0
- package/payload/rebon.exe +2 -2
- package/payload/sandbox-win.exe +0 -0
- package/scripts/postinstall.js +112 -112
- package/payload/compose-runtime/payload/compose/credentials-runtime.js +0 -38
- package/payload/compose-runtime/payload/compose/llm-runtime.js +0 -330
- package/payload/compose-runtime/payload/compose/loop-assembly.js +0 -149
- package/payload/compose-runtime/payload/compose/serve-dispatch.js +0 -82
- package/payload/compose-runtime/payload/compose/systemprompt-runtime.js +0 -74
- package/payload/compose-runtime/payload/compose/tools-runtime.js +0 -228
- package/payload/compose-runtime/payload/compose/web-runtime.js +0 -204
|
@@ -1,377 +1,377 @@
|
|
|
1
|
-
// Payload schemas for the methods that carry one. Mirrors
|
|
2
|
-
// `crates/rebon-plugin-protocol/src/methods.rs`; the shared corpus at
|
|
3
|
-
// `crates/rebon-plugin-protocol/tests/fixtures/v1/methods.json` is what keeps
|
|
4
|
-
// the two honest. Codes are the bracketed tokens that corpus pins, which is a
|
|
5
|
-
// different vocabulary from `protocol.mjs`'s framing codes on purpose: they
|
|
6
|
-
// answer different questions and are pinned by different corpora.
|
|
7
|
-
//
|
|
8
|
-
// Validation order is part of the contract. A payload with two problems reports
|
|
9
|
-
// the first one in declaration order, and both languages must pick the same one.
|
|
10
|
-
import { ProtocolError, PLATFORM_PLUGIN_ID, plain } from './protocol.mjs';
|
|
11
|
-
|
|
12
|
-
export const PLUGIN_LOAD_METHOD = 'plugin/load';
|
|
13
|
-
export const PLUGIN_UNLOAD_METHOD = 'plugin/unload';
|
|
14
|
-
export const SERVICE_CALL_METHOD = 'service/call';
|
|
15
|
-
export const EVENT_DELIVER_METHOD = 'event/deliver';
|
|
16
|
-
export const EVENT_SUBSCRIBE_METHOD = 'event/subscribe';
|
|
17
|
-
export const EVENT_UNSUBSCRIBE_METHOD = 'event/unsubscribe';
|
|
18
|
-
export const EVENT_EMIT_METHOD = 'event/emit';
|
|
19
|
-
export const TOOL_INVOKE_METHOD = 'tool/invoke';
|
|
20
|
-
export const LLM_STREAM_METHOD = 'llm/stream';
|
|
21
|
-
export const LLM_CONTROL_METHOD = 'llm/control';
|
|
22
|
-
export const COMMAND_INVOKE_METHOD = 'command/invoke';
|
|
23
|
-
// The surfaces a command can say it works on, and the two groupings a menu
|
|
24
|
-
// knows. Closed sets, mirroring `rebon-slash-commands`.
|
|
25
|
-
export const COMMAND_SURFACES = Object.freeze(['tui', 'desktop', 'acp', 'web', 'mobile']);
|
|
26
|
-
export const COMMAND_CATEGORIES = Object.freeze(['command', 'agent']);
|
|
27
|
-
// The conversation-level signals an adapter can be told about. A closed set:
|
|
28
|
-
// an open string would make every unknown signal look like a typo.
|
|
29
|
-
export const LLM_CONTROL_SIGNALS = Object.freeze(['reset', 'endTurn', 'invalidate']);
|
|
30
|
-
export const TOOL_CALL_METHOD = 'tool/call';
|
|
31
|
-
export const SEAT_CALL_METHOD = 'seat/call';
|
|
32
|
-
|
|
33
|
-
export const MAX_NAME_BYTES = 128;
|
|
34
|
-
export const MAX_DECLARATIONS = 256;
|
|
35
|
-
// A tool description is read by a model, so it is spent out of every turn's
|
|
36
|
-
// context budget for as long as the tool is offered. The bound is part of the
|
|
37
|
-
// contract, not a defensive habit.
|
|
38
|
-
export const MAX_DESCRIPTION_BYTES = 4096;
|
|
39
|
-
|
|
40
|
-
// Rust bounds a name by its UTF-8 byte length. Measuring UTF-16 units here
|
|
41
|
-
// would accept names Rust rejects, which is exactly the kind of drift a plugin
|
|
42
|
-
// author discovers only on the other host.
|
|
43
|
-
const byteLength = (value) => Buffer.byteLength(value, 'utf8');
|
|
44
|
-
// `\p{Cc}` is the Unicode category Rust's `char::is_control` tests.
|
|
45
|
-
const CONTROL = /\p{Cc}/u;
|
|
46
|
-
|
|
47
|
-
function shape(value, required, optional, where) {
|
|
48
|
-
if (!plain(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be a plain object`);
|
|
49
|
-
const known = new Set([...required, ...optional]);
|
|
50
|
-
for (const key of Object.keys(value)) {
|
|
51
|
-
if (!known.has(key)) throw new ProtocolError('[UNKNOWN_FIELD]', `${where} has unknown field ${JSON.stringify(key)}`);
|
|
52
|
-
}
|
|
53
|
-
for (const key of required) {
|
|
54
|
-
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
55
|
-
throw new ProtocolError('[MISSING_FIELD]', `${where} is missing field ${JSON.stringify(key)}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function string(value, where) {
|
|
61
|
-
if (typeof value !== 'string') throw new ProtocolError('[WRONG_SHAPE]', `${where} must be a string`);
|
|
62
|
-
return value;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function validateName(kind, name) {
|
|
66
|
-
if (name.length === 0) throw new ProtocolError('[EMPTY_NAME]', `${kind} name must not be empty`);
|
|
67
|
-
if (byteLength(name) > MAX_NAME_BYTES) throw new ProtocolError('[NAME_TOO_LONG]', `${kind} name is longer than ${MAX_NAME_BYTES} bytes`);
|
|
68
|
-
if (CONTROL.test(name)) throw new ProtocolError('[CONTROL_CHARACTER]', `${kind} name contains a control character`);
|
|
69
|
-
return name;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const MAX_PATH_BYTES = 4096;
|
|
73
|
-
|
|
74
|
-
// `path.isAbsolute` answers for the platform it runs on, which is right for
|
|
75
|
-
// using a path and wrong for agreeing about one: this rule is read by both
|
|
76
|
-
// languages on whichever machine runs CI. A supervisor and its host always run
|
|
77
|
-
// on the same machine, so both spellings count.
|
|
78
|
-
const looksAbsolute = (value) => /^[/\\]/.test(value) || /^[A-Za-z]:[/\\]/.test(value);
|
|
79
|
-
|
|
80
|
-
function pathShape(kind, value) {
|
|
81
|
-
if (value.length === 0) throw new ProtocolError('[EMPTY_NAME]', `${kind} path must not be empty`);
|
|
82
|
-
if (byteLength(value) > MAX_PATH_BYTES) throw new ProtocolError('[PATH_TOO_LONG]', `${kind} path is longer than ${MAX_PATH_BYTES} bytes`);
|
|
83
|
-
if (CONTROL.test(value)) throw new ProtocolError('[CONTROL_CHARACTER]', `${kind} path contains a control character`);
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function validateAbsolutePath(kind, value) {
|
|
88
|
-
pathShape(kind, value);
|
|
89
|
-
if (!looksAbsolute(value)) throw new ProtocolError('[PATH_NOT_ABSOLUTE]', `${kind} path must be absolute`);
|
|
90
|
-
return value;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function validateRelativePath(kind, value) {
|
|
94
|
-
pathShape(kind, value);
|
|
95
|
-
if (looksAbsolute(value)) throw new ProtocolError('[PATH_NOT_RELATIVE]', `${kind} path must be relative to the package root`);
|
|
96
|
-
if (value.includes('\\') || value.split('/').some((part) => part === '..' || part === '')) {
|
|
97
|
-
throw new ProtocolError('[PATH_ESCAPES]', `${kind} path leaves the package root`);
|
|
98
|
-
}
|
|
99
|
-
return value;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function validatePluginId(pluginId) {
|
|
103
|
-
validateName('plugin', pluginId);
|
|
104
|
-
if (pluginId === PLATFORM_PLUGIN_ID) throw new ProtocolError('[RESERVED_PLUGIN_ID]', `plugin id ${PLATFORM_PLUGIN_ID} is reserved for platform control traffic`);
|
|
105
|
-
return pluginId;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Absent lists default to empty, matching serde's `#[serde(default)]`. The
|
|
109
|
-
// length bound is checked before the entries, and each entry's own validity
|
|
110
|
-
// before the duplicate check — same order as Rust.
|
|
111
|
-
function declarations(value, kind, where) {
|
|
112
|
-
if (value === undefined) return [];
|
|
113
|
-
if (!Array.isArray(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be an array`);
|
|
114
|
-
if (value.length > MAX_DECLARATIONS) throw new ProtocolError('[TOO_MANY_DECLARATIONS]', `${where} declares ${value.length} entries, over the ${MAX_DECLARATIONS} limit`);
|
|
115
|
-
const seen = new Set();
|
|
116
|
-
for (const entry of value) {
|
|
117
|
-
validateName(kind, string(entry, `${where} entry`));
|
|
118
|
-
if (seen.has(entry)) throw new ProtocolError('[DUPLICATE_DECLARATION]', `${where} declares ${JSON.stringify(entry)} twice`);
|
|
119
|
-
seen.add(entry);
|
|
120
|
-
}
|
|
121
|
-
return Object.freeze([...value]);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// A command as a plugin describes it: one-to-one with `CommandSpec`, minus
|
|
125
|
-
// the two kinds a plugin may not claim (`native` has no front-end function to
|
|
126
|
-
// map an id to, `session` is rebon's own engine state).
|
|
127
|
-
export function pluginCommandDefinition(input) {
|
|
128
|
-
shape(input, ['name', 'description', 'kind'], ['aliases', 'zhAliases', 'hint', 'category', 'surfaces'], 'command definition');
|
|
129
|
-
const name = validateName('command', string(input.name, 'name'));
|
|
130
|
-
for (const alias of input.aliases ?? []) validateName('command', string(alias, 'alias'));
|
|
131
|
-
for (const alias of input.zhAliases ?? []) validateName('command', string(alias, 'zh alias'));
|
|
132
|
-
const description = string(input.description, 'description');
|
|
133
|
-
if (description.length === 0) throw new ProtocolError('[EMPTY_DESCRIPTION]', `command ${JSON.stringify(name)} has no description`);
|
|
134
|
-
if (byteLength(description) > MAX_DESCRIPTION_BYTES) throw new ProtocolError('[DESCRIPTION_TOO_LONG]', `command ${JSON.stringify(name)} has a description over the ${MAX_DESCRIPTION_BYTES} byte limit`);
|
|
135
|
-
if (input.category !== undefined && !COMMAND_CATEGORIES.includes(input.category)) {
|
|
136
|
-
throw new ProtocolError('[WRONG_SHAPE]', `command category must be one of ${COMMAND_CATEGORIES.join(', ')}`);
|
|
137
|
-
}
|
|
138
|
-
for (const surface of input.surfaces ?? []) {
|
|
139
|
-
if (!COMMAND_SURFACES.includes(surface)) {
|
|
140
|
-
throw new ProtocolError('[WRONG_SHAPE]', `command surface must be one of ${COMMAND_SURFACES.join(', ')}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const kind = input.kind;
|
|
144
|
-
if (!plain(kind)) throw new ProtocolError('[WRONG_SHAPE]', 'command kind must be a plain object');
|
|
145
|
-
switch (kind.type) {
|
|
146
|
-
case 'prompt':
|
|
147
|
-
shape(kind, ['type'], [], 'command kind');
|
|
148
|
-
break;
|
|
149
|
-
case 'explain': {
|
|
150
|
-
shape(kind, ['type', 'text'], [], 'command kind');
|
|
151
|
-
const text = string(kind.text, 'kind text');
|
|
152
|
-
if (text.length === 0) throw new ProtocolError('[EMPTY_DESCRIPTION]', `command ${JSON.stringify(name)} explains nothing`);
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
case 'panel':
|
|
156
|
-
shape(kind, ['type', 'dialog'], [], 'command kind');
|
|
157
|
-
validateName('dialog', string(kind.dialog, 'kind dialog'));
|
|
158
|
-
break;
|
|
159
|
-
default:
|
|
160
|
-
throw new ProtocolError('[WRONG_SHAPE]', 'command kind must be prompt, explain or panel');
|
|
161
|
-
}
|
|
162
|
-
return Object.freeze({ ...input });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function commandDefinitions(value, where) {
|
|
166
|
-
if (value === undefined) return Object.freeze([]);
|
|
167
|
-
if (!Array.isArray(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be an array`);
|
|
168
|
-
if (value.length > MAX_DECLARATIONS) throw new ProtocolError('[TOO_MANY_DECLARATIONS]', `${where} declares ${value.length} entries, over the ${MAX_DECLARATIONS} limit`);
|
|
169
|
-
const seen = new Set();
|
|
170
|
-
const out = [];
|
|
171
|
-
for (const entry of value) {
|
|
172
|
-
const command = pluginCommandDefinition(entry);
|
|
173
|
-
if (seen.has(command.name)) throw new ProtocolError('[DUPLICATE_DECLARATION]', `${where} declares ${JSON.stringify(command.name)} twice`);
|
|
174
|
-
seen.add(command.name);
|
|
175
|
-
out.push(command);
|
|
176
|
-
}
|
|
177
|
-
return Object.freeze(out);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function pluginLoadRequest(input) {
|
|
181
|
-
shape(input, ['pluginId', 'root', 'entry'], ['services', 'eventTopics', 'publishedTopics', 'llmProviders', 'tools', 'commands', 'invokableTools', 'seats', 'config'], 'plugin/load payload');
|
|
182
|
-
return Object.freeze({
|
|
183
|
-
pluginId: validatePluginId(string(input.pluginId, 'pluginId')),
|
|
184
|
-
root: validateAbsolutePath('root', string(input.root, 'root')),
|
|
185
|
-
entry: validateRelativePath('entry', string(input.entry, 'entry')),
|
|
186
|
-
services: declarations(input.services, 'service', 'services'),
|
|
187
|
-
eventTopics: declarations(input.eventTopics, 'topic', 'eventTopics'),
|
|
188
|
-
// What it may publish, as opposed to what it listens to. Two powers, two
|
|
189
|
-
// lists — the same separation `tools` and `invokableTools` keep.
|
|
190
|
-
publishedTopics: declarations(input.publishedTopics, 'topic', 'publishedTopics'),
|
|
191
|
-
llmProviders: declarations(input.llmProviders, 'provider', 'llmProviders'),
|
|
192
|
-
tools: declarations(input.tools, 'tool', 'tools'),
|
|
193
|
-
// Command names only. What a command looks like in a menu comes with the
|
|
194
|
-
// registration; the name is the reviewable part, and the part that can
|
|
195
|
-
// collide with a built-in.
|
|
196
|
-
commands: declarations(input.commands, 'command', 'commands'),
|
|
197
|
-
// The one declared list with no registered counterpart: a plugin calls
|
|
198
|
-
// these rather than providing them, which is why its name says so.
|
|
199
|
-
invokableTools: declarations(input.invokableTools, 'tool', 'invokableTools'),
|
|
200
|
-
seats: declarations(input.seats, 'seat', 'seats'),
|
|
201
|
-
// Opaque: what a plugin's configuration means is the plugin's contract, and
|
|
202
|
-
// an absent one is `null` rather than a missing field — the same rule every
|
|
203
|
-
// other payload slot follows.
|
|
204
|
-
config: input.config === undefined ? null : input.config,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function pluginToolDefinition(input) {
|
|
209
|
-
shape(input, ['name', 'description', 'inputSchema'], [], 'tool definition');
|
|
210
|
-
const name = validateName('tool', string(input.name, 'name'));
|
|
211
|
-
const description = string(input.description, 'description');
|
|
212
|
-
if (description.length === 0) throw new ProtocolError('[EMPTY_DESCRIPTION]', `tool ${JSON.stringify(name)} has no description, so a model cannot know when to use it`);
|
|
213
|
-
if (byteLength(description) > MAX_DESCRIPTION_BYTES) throw new ProtocolError('[DESCRIPTION_TOO_LONG]', `tool ${JSON.stringify(name)} has a description over the ${MAX_DESCRIPTION_BYTES} byte limit`);
|
|
214
|
-
return Object.freeze({ name, description, inputSchema: input.inputSchema });
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function toolDefinitions(value, where) {
|
|
218
|
-
if (value === undefined) return [];
|
|
219
|
-
if (!Array.isArray(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be an array`);
|
|
220
|
-
if (value.length > MAX_DECLARATIONS) throw new ProtocolError('[TOO_MANY_DECLARATIONS]', `${where} declares ${value.length} entries, over the ${MAX_DECLARATIONS} limit`);
|
|
221
|
-
const seen = new Set();
|
|
222
|
-
const out = [];
|
|
223
|
-
for (const entry of value) {
|
|
224
|
-
const tool = pluginToolDefinition(entry);
|
|
225
|
-
if (seen.has(tool.name)) throw new ProtocolError('[DUPLICATE_DECLARATION]', `${where} declares ${JSON.stringify(tool.name)} twice`);
|
|
226
|
-
seen.add(tool.name);
|
|
227
|
-
out.push(tool);
|
|
228
|
-
}
|
|
229
|
-
return Object.freeze(out);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// What each adapter says about itself, keyed by the provider it serves. The
|
|
233
|
-
// values stay opaque — the wire layer decides only whether a route may be
|
|
234
|
-
// served, and `llmProviders` answers that — so the only rule here is that a
|
|
235
|
-
// description must belong to a route the same report claims.
|
|
236
|
-
function llmAdapters(value, providers, where) {
|
|
237
|
-
if (value === undefined) return Object.freeze({});
|
|
238
|
-
if (!plain(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be a plain object`);
|
|
239
|
-
const declared = new Set(providers);
|
|
240
|
-
for (const provider of Object.keys(value)) {
|
|
241
|
-
validateName('provider', provider);
|
|
242
|
-
if (!declared.has(provider)) {
|
|
243
|
-
throw new ProtocolError('[UNDECLARED_ADAPTER]', `adapter info describes provider ${JSON.stringify(provider)}, which this report does not serve`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return Object.freeze({ ...value });
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function pluginReadyReport(input) {
|
|
250
|
-
shape(input, ['pluginId'], ['services', 'eventTopics', 'llmProviders', 'llmAdapters', 'tools', 'commands'], 'plugin/load terminal payload');
|
|
251
|
-
// Field order, not convenience order: a payload with two problems has to
|
|
252
|
-
// report the same one here and in Rust, and `llmAdapters` is checked against
|
|
253
|
-
// `llmProviders` so the providers have to be read first — but not before the
|
|
254
|
-
// fields Rust reads before them.
|
|
255
|
-
const pluginId = validatePluginId(string(input.pluginId, 'pluginId'));
|
|
256
|
-
const services = declarations(input.services, 'service', 'services');
|
|
257
|
-
const eventTopics = declarations(input.eventTopics, 'topic', 'eventTopics');
|
|
258
|
-
const providers = declarations(input.llmProviders, 'provider', 'llmProviders');
|
|
259
|
-
return Object.freeze({
|
|
260
|
-
pluginId,
|
|
261
|
-
services,
|
|
262
|
-
eventTopics,
|
|
263
|
-
llmProviders: providers,
|
|
264
|
-
llmAdapters: llmAdapters(input.llmAdapters, providers, 'llmAdapters'),
|
|
265
|
-
tools: toolDefinitions(input.tools, 'tools'),
|
|
266
|
-
commands: commandDefinitions(input.commands, 'commands'),
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function pluginUnloadRequest(input) {
|
|
271
|
-
shape(input, ['pluginId'], [], 'plugin/unload payload');
|
|
272
|
-
return Object.freeze({ pluginId: validatePluginId(string(input.pluginId, 'pluginId')) });
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function pluginDrainReport(input) {
|
|
276
|
-
shape(input, ['pluginId'], ['outstandingCalls', 'revokedSubscriptions'], 'plugin/unload terminal payload');
|
|
277
|
-
return Object.freeze({
|
|
278
|
-
pluginId: validatePluginId(string(input.pluginId, 'pluginId')),
|
|
279
|
-
outstandingCalls: declarations(input.outstandingCalls, 'call', 'outstandingCalls'),
|
|
280
|
-
revokedSubscriptions: declarations(input.revokedSubscriptions, 'subscription', 'revokedSubscriptions'),
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export function serviceCallRequest(input) {
|
|
285
|
-
shape(input, ['service', 'request'], [], 'service/call payload');
|
|
286
|
-
return Object.freeze({ service: validateName('service', string(input.service, 'service')), request: input.request });
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function eventSubscribeRequest(input) {
|
|
290
|
-
shape(input, ['subscription', 'topic'], [], 'event/subscribe payload');
|
|
291
|
-
return Object.freeze({
|
|
292
|
-
subscription: validateName('subscription', string(input.subscription, 'subscription')),
|
|
293
|
-
topic: validateName('topic', string(input.topic, 'topic')),
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export function eventUnsubscribeRequest(input) {
|
|
298
|
-
shape(input, ['subscription'], [], 'event/unsubscribe payload');
|
|
299
|
-
return Object.freeze({ subscription: validateName('subscription', string(input.subscription, 'subscription')) });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function eventDelivery(input) {
|
|
303
|
-
shape(input, ['subscription', 'topic', 'event'], [], 'event/deliver payload');
|
|
304
|
-
return Object.freeze({
|
|
305
|
-
subscription: validateName('subscription', string(input.subscription, 'subscription')),
|
|
306
|
-
topic: validateName('topic', string(input.topic, 'topic')),
|
|
307
|
-
event: input.event,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function eventEmitRequest(input) {
|
|
312
|
-
shape(input, ['topic', 'event'], [], 'event/emit payload');
|
|
313
|
-
return Object.freeze({
|
|
314
|
-
topic: validateName('topic', string(input.topic, 'topic')),
|
|
315
|
-
event: input.event,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export function toolInvokeRequest(input) {
|
|
320
|
-
shape(input, ['tool', 'input'], [], 'tool/invoke payload');
|
|
321
|
-
return Object.freeze({ tool: validateName('tool', string(input.tool, 'tool')), input: input.input });
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export function llmStreamRequest(input) {
|
|
325
|
-
shape(input, ['provider', 'request'], [], 'llm/stream payload');
|
|
326
|
-
return Object.freeze({ provider: validateName('provider', string(input.provider, 'provider')), request: input.request });
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export function llmControlRequest(input) {
|
|
330
|
-
shape(input, ['provider', 'signal'], [], 'llm/control payload');
|
|
331
|
-
// Provider first, matching the Rust validator's field order.
|
|
332
|
-
const provider = validateName('provider', string(input.provider, 'provider'));
|
|
333
|
-
const signal = string(input.signal, 'signal');
|
|
334
|
-
if (!LLM_CONTROL_SIGNALS.includes(signal)) {
|
|
335
|
-
throw new ProtocolError('[UNKNOWN_LLM_SIGNAL]', `llm control signal ${JSON.stringify(signal)} is not one of ${LLM_CONTROL_SIGNALS.join(', ')}`);
|
|
336
|
-
}
|
|
337
|
-
return Object.freeze({ provider, signal });
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export function commandInvokeRequest(input) {
|
|
341
|
-
shape(input, ['name', 'raw', 'rest', 'surface'], [], 'command/invoke payload');
|
|
342
|
-
const name = validateName('command', string(input.name, 'name'));
|
|
343
|
-
const raw = string(input.raw, 'raw');
|
|
344
|
-
const rest = string(input.rest, 'rest');
|
|
345
|
-
const surface = string(input.surface, 'surface');
|
|
346
|
-
if (!COMMAND_SURFACES.includes(surface)) {
|
|
347
|
-
throw new ProtocolError('[WRONG_SHAPE]', `surface must be one of ${COMMAND_SURFACES.join(', ')}`);
|
|
348
|
-
}
|
|
349
|
-
return Object.freeze({ name, raw, rest, surface });
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export function seatCallRequest(input) {
|
|
353
|
-
shape(input, ['seat', 'method', 'params'], [], 'seat/call payload');
|
|
354
|
-
return Object.freeze({
|
|
355
|
-
seat: validateName('seat', string(input.seat, 'seat')),
|
|
356
|
-
method: validateName('method', string(input.method, 'method')),
|
|
357
|
-
params: input.params,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/// The corpus's `kind` names, so a shared case table can dispatch by name.
|
|
362
|
-
export const VALIDATORS = Object.freeze({
|
|
363
|
-
plugin_load: pluginLoadRequest,
|
|
364
|
-
plugin_ready: pluginReadyReport,
|
|
365
|
-
plugin_unload: pluginUnloadRequest,
|
|
366
|
-
plugin_drain: pluginDrainReport,
|
|
367
|
-
service_call: serviceCallRequest,
|
|
368
|
-
event_subscribe: eventSubscribeRequest,
|
|
369
|
-
event_unsubscribe: eventUnsubscribeRequest,
|
|
370
|
-
event_deliver: eventDelivery,
|
|
371
|
-
event_emit: eventEmitRequest,
|
|
372
|
-
tool_invoke: toolInvokeRequest,
|
|
373
|
-
llm_stream: llmStreamRequest,
|
|
374
|
-
llm_control: llmControlRequest,
|
|
375
|
-
command_invoke: commandInvokeRequest,
|
|
376
|
-
seat_call: seatCallRequest,
|
|
377
|
-
});
|
|
1
|
+
// Payload schemas for the methods that carry one. Mirrors
|
|
2
|
+
// `crates/rebon-plugin-protocol/src/methods.rs`; the shared corpus at
|
|
3
|
+
// `crates/rebon-plugin-protocol/tests/fixtures/v1/methods.json` is what keeps
|
|
4
|
+
// the two honest. Codes are the bracketed tokens that corpus pins, which is a
|
|
5
|
+
// different vocabulary from `protocol.mjs`'s framing codes on purpose: they
|
|
6
|
+
// answer different questions and are pinned by different corpora.
|
|
7
|
+
//
|
|
8
|
+
// Validation order is part of the contract. A payload with two problems reports
|
|
9
|
+
// the first one in declaration order, and both languages must pick the same one.
|
|
10
|
+
import { ProtocolError, PLATFORM_PLUGIN_ID, plain } from './protocol.mjs';
|
|
11
|
+
|
|
12
|
+
export const PLUGIN_LOAD_METHOD = 'plugin/load';
|
|
13
|
+
export const PLUGIN_UNLOAD_METHOD = 'plugin/unload';
|
|
14
|
+
export const SERVICE_CALL_METHOD = 'service/call';
|
|
15
|
+
export const EVENT_DELIVER_METHOD = 'event/deliver';
|
|
16
|
+
export const EVENT_SUBSCRIBE_METHOD = 'event/subscribe';
|
|
17
|
+
export const EVENT_UNSUBSCRIBE_METHOD = 'event/unsubscribe';
|
|
18
|
+
export const EVENT_EMIT_METHOD = 'event/emit';
|
|
19
|
+
export const TOOL_INVOKE_METHOD = 'tool/invoke';
|
|
20
|
+
export const LLM_STREAM_METHOD = 'llm/stream';
|
|
21
|
+
export const LLM_CONTROL_METHOD = 'llm/control';
|
|
22
|
+
export const COMMAND_INVOKE_METHOD = 'command/invoke';
|
|
23
|
+
// The surfaces a command can say it works on, and the two groupings a menu
|
|
24
|
+
// knows. Closed sets, mirroring `rebon-slash-commands`.
|
|
25
|
+
export const COMMAND_SURFACES = Object.freeze(['tui', 'desktop', 'acp', 'web', 'mobile']);
|
|
26
|
+
export const COMMAND_CATEGORIES = Object.freeze(['command', 'agent']);
|
|
27
|
+
// The conversation-level signals an adapter can be told about. A closed set:
|
|
28
|
+
// an open string would make every unknown signal look like a typo.
|
|
29
|
+
export const LLM_CONTROL_SIGNALS = Object.freeze(['reset', 'endTurn', 'invalidate']);
|
|
30
|
+
export const TOOL_CALL_METHOD = 'tool/call';
|
|
31
|
+
export const SEAT_CALL_METHOD = 'seat/call';
|
|
32
|
+
|
|
33
|
+
export const MAX_NAME_BYTES = 128;
|
|
34
|
+
export const MAX_DECLARATIONS = 256;
|
|
35
|
+
// A tool description is read by a model, so it is spent out of every turn's
|
|
36
|
+
// context budget for as long as the tool is offered. The bound is part of the
|
|
37
|
+
// contract, not a defensive habit.
|
|
38
|
+
export const MAX_DESCRIPTION_BYTES = 4096;
|
|
39
|
+
|
|
40
|
+
// Rust bounds a name by its UTF-8 byte length. Measuring UTF-16 units here
|
|
41
|
+
// would accept names Rust rejects, which is exactly the kind of drift a plugin
|
|
42
|
+
// author discovers only on the other host.
|
|
43
|
+
const byteLength = (value) => Buffer.byteLength(value, 'utf8');
|
|
44
|
+
// `\p{Cc}` is the Unicode category Rust's `char::is_control` tests.
|
|
45
|
+
const CONTROL = /\p{Cc}/u;
|
|
46
|
+
|
|
47
|
+
function shape(value, required, optional, where) {
|
|
48
|
+
if (!plain(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be a plain object`);
|
|
49
|
+
const known = new Set([...required, ...optional]);
|
|
50
|
+
for (const key of Object.keys(value)) {
|
|
51
|
+
if (!known.has(key)) throw new ProtocolError('[UNKNOWN_FIELD]', `${where} has unknown field ${JSON.stringify(key)}`);
|
|
52
|
+
}
|
|
53
|
+
for (const key of required) {
|
|
54
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
55
|
+
throw new ProtocolError('[MISSING_FIELD]', `${where} is missing field ${JSON.stringify(key)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function string(value, where) {
|
|
61
|
+
if (typeof value !== 'string') throw new ProtocolError('[WRONG_SHAPE]', `${where} must be a string`);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateName(kind, name) {
|
|
66
|
+
if (name.length === 0) throw new ProtocolError('[EMPTY_NAME]', `${kind} name must not be empty`);
|
|
67
|
+
if (byteLength(name) > MAX_NAME_BYTES) throw new ProtocolError('[NAME_TOO_LONG]', `${kind} name is longer than ${MAX_NAME_BYTES} bytes`);
|
|
68
|
+
if (CONTROL.test(name)) throw new ProtocolError('[CONTROL_CHARACTER]', `${kind} name contains a control character`);
|
|
69
|
+
return name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const MAX_PATH_BYTES = 4096;
|
|
73
|
+
|
|
74
|
+
// `path.isAbsolute` answers for the platform it runs on, which is right for
|
|
75
|
+
// using a path and wrong for agreeing about one: this rule is read by both
|
|
76
|
+
// languages on whichever machine runs CI. A supervisor and its host always run
|
|
77
|
+
// on the same machine, so both spellings count.
|
|
78
|
+
const looksAbsolute = (value) => /^[/\\]/.test(value) || /^[A-Za-z]:[/\\]/.test(value);
|
|
79
|
+
|
|
80
|
+
function pathShape(kind, value) {
|
|
81
|
+
if (value.length === 0) throw new ProtocolError('[EMPTY_NAME]', `${kind} path must not be empty`);
|
|
82
|
+
if (byteLength(value) > MAX_PATH_BYTES) throw new ProtocolError('[PATH_TOO_LONG]', `${kind} path is longer than ${MAX_PATH_BYTES} bytes`);
|
|
83
|
+
if (CONTROL.test(value)) throw new ProtocolError('[CONTROL_CHARACTER]', `${kind} path contains a control character`);
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validateAbsolutePath(kind, value) {
|
|
88
|
+
pathShape(kind, value);
|
|
89
|
+
if (!looksAbsolute(value)) throw new ProtocolError('[PATH_NOT_ABSOLUTE]', `${kind} path must be absolute`);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateRelativePath(kind, value) {
|
|
94
|
+
pathShape(kind, value);
|
|
95
|
+
if (looksAbsolute(value)) throw new ProtocolError('[PATH_NOT_RELATIVE]', `${kind} path must be relative to the package root`);
|
|
96
|
+
if (value.includes('\\') || value.split('/').some((part) => part === '..' || part === '')) {
|
|
97
|
+
throw new ProtocolError('[PATH_ESCAPES]', `${kind} path leaves the package root`);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function validatePluginId(pluginId) {
|
|
103
|
+
validateName('plugin', pluginId);
|
|
104
|
+
if (pluginId === PLATFORM_PLUGIN_ID) throw new ProtocolError('[RESERVED_PLUGIN_ID]', `plugin id ${PLATFORM_PLUGIN_ID} is reserved for platform control traffic`);
|
|
105
|
+
return pluginId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Absent lists default to empty, matching serde's `#[serde(default)]`. The
|
|
109
|
+
// length bound is checked before the entries, and each entry's own validity
|
|
110
|
+
// before the duplicate check — same order as Rust.
|
|
111
|
+
function declarations(value, kind, where) {
|
|
112
|
+
if (value === undefined) return [];
|
|
113
|
+
if (!Array.isArray(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be an array`);
|
|
114
|
+
if (value.length > MAX_DECLARATIONS) throw new ProtocolError('[TOO_MANY_DECLARATIONS]', `${where} declares ${value.length} entries, over the ${MAX_DECLARATIONS} limit`);
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (const entry of value) {
|
|
117
|
+
validateName(kind, string(entry, `${where} entry`));
|
|
118
|
+
if (seen.has(entry)) throw new ProtocolError('[DUPLICATE_DECLARATION]', `${where} declares ${JSON.stringify(entry)} twice`);
|
|
119
|
+
seen.add(entry);
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze([...value]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A command as a plugin describes it: one-to-one with `CommandSpec`, minus
|
|
125
|
+
// the two kinds a plugin may not claim (`native` has no front-end function to
|
|
126
|
+
// map an id to, `session` is rebon's own engine state).
|
|
127
|
+
export function pluginCommandDefinition(input) {
|
|
128
|
+
shape(input, ['name', 'description', 'kind'], ['aliases', 'zhAliases', 'hint', 'category', 'surfaces'], 'command definition');
|
|
129
|
+
const name = validateName('command', string(input.name, 'name'));
|
|
130
|
+
for (const alias of input.aliases ?? []) validateName('command', string(alias, 'alias'));
|
|
131
|
+
for (const alias of input.zhAliases ?? []) validateName('command', string(alias, 'zh alias'));
|
|
132
|
+
const description = string(input.description, 'description');
|
|
133
|
+
if (description.length === 0) throw new ProtocolError('[EMPTY_DESCRIPTION]', `command ${JSON.stringify(name)} has no description`);
|
|
134
|
+
if (byteLength(description) > MAX_DESCRIPTION_BYTES) throw new ProtocolError('[DESCRIPTION_TOO_LONG]', `command ${JSON.stringify(name)} has a description over the ${MAX_DESCRIPTION_BYTES} byte limit`);
|
|
135
|
+
if (input.category !== undefined && !COMMAND_CATEGORIES.includes(input.category)) {
|
|
136
|
+
throw new ProtocolError('[WRONG_SHAPE]', `command category must be one of ${COMMAND_CATEGORIES.join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
for (const surface of input.surfaces ?? []) {
|
|
139
|
+
if (!COMMAND_SURFACES.includes(surface)) {
|
|
140
|
+
throw new ProtocolError('[WRONG_SHAPE]', `command surface must be one of ${COMMAND_SURFACES.join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const kind = input.kind;
|
|
144
|
+
if (!plain(kind)) throw new ProtocolError('[WRONG_SHAPE]', 'command kind must be a plain object');
|
|
145
|
+
switch (kind.type) {
|
|
146
|
+
case 'prompt':
|
|
147
|
+
shape(kind, ['type'], [], 'command kind');
|
|
148
|
+
break;
|
|
149
|
+
case 'explain': {
|
|
150
|
+
shape(kind, ['type', 'text'], [], 'command kind');
|
|
151
|
+
const text = string(kind.text, 'kind text');
|
|
152
|
+
if (text.length === 0) throw new ProtocolError('[EMPTY_DESCRIPTION]', `command ${JSON.stringify(name)} explains nothing`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'panel':
|
|
156
|
+
shape(kind, ['type', 'dialog'], [], 'command kind');
|
|
157
|
+
validateName('dialog', string(kind.dialog, 'kind dialog'));
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
throw new ProtocolError('[WRONG_SHAPE]', 'command kind must be prompt, explain or panel');
|
|
161
|
+
}
|
|
162
|
+
return Object.freeze({ ...input });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function commandDefinitions(value, where) {
|
|
166
|
+
if (value === undefined) return Object.freeze([]);
|
|
167
|
+
if (!Array.isArray(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be an array`);
|
|
168
|
+
if (value.length > MAX_DECLARATIONS) throw new ProtocolError('[TOO_MANY_DECLARATIONS]', `${where} declares ${value.length} entries, over the ${MAX_DECLARATIONS} limit`);
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
const out = [];
|
|
171
|
+
for (const entry of value) {
|
|
172
|
+
const command = pluginCommandDefinition(entry);
|
|
173
|
+
if (seen.has(command.name)) throw new ProtocolError('[DUPLICATE_DECLARATION]', `${where} declares ${JSON.stringify(command.name)} twice`);
|
|
174
|
+
seen.add(command.name);
|
|
175
|
+
out.push(command);
|
|
176
|
+
}
|
|
177
|
+
return Object.freeze(out);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function pluginLoadRequest(input) {
|
|
181
|
+
shape(input, ['pluginId', 'root', 'entry'], ['services', 'eventTopics', 'publishedTopics', 'llmProviders', 'tools', 'commands', 'invokableTools', 'seats', 'config'], 'plugin/load payload');
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
pluginId: validatePluginId(string(input.pluginId, 'pluginId')),
|
|
184
|
+
root: validateAbsolutePath('root', string(input.root, 'root')),
|
|
185
|
+
entry: validateRelativePath('entry', string(input.entry, 'entry')),
|
|
186
|
+
services: declarations(input.services, 'service', 'services'),
|
|
187
|
+
eventTopics: declarations(input.eventTopics, 'topic', 'eventTopics'),
|
|
188
|
+
// What it may publish, as opposed to what it listens to. Two powers, two
|
|
189
|
+
// lists — the same separation `tools` and `invokableTools` keep.
|
|
190
|
+
publishedTopics: declarations(input.publishedTopics, 'topic', 'publishedTopics'),
|
|
191
|
+
llmProviders: declarations(input.llmProviders, 'provider', 'llmProviders'),
|
|
192
|
+
tools: declarations(input.tools, 'tool', 'tools'),
|
|
193
|
+
// Command names only. What a command looks like in a menu comes with the
|
|
194
|
+
// registration; the name is the reviewable part, and the part that can
|
|
195
|
+
// collide with a built-in.
|
|
196
|
+
commands: declarations(input.commands, 'command', 'commands'),
|
|
197
|
+
// The one declared list with no registered counterpart: a plugin calls
|
|
198
|
+
// these rather than providing them, which is why its name says so.
|
|
199
|
+
invokableTools: declarations(input.invokableTools, 'tool', 'invokableTools'),
|
|
200
|
+
seats: declarations(input.seats, 'seat', 'seats'),
|
|
201
|
+
// Opaque: what a plugin's configuration means is the plugin's contract, and
|
|
202
|
+
// an absent one is `null` rather than a missing field — the same rule every
|
|
203
|
+
// other payload slot follows.
|
|
204
|
+
config: input.config === undefined ? null : input.config,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function pluginToolDefinition(input) {
|
|
209
|
+
shape(input, ['name', 'description', 'inputSchema'], [], 'tool definition');
|
|
210
|
+
const name = validateName('tool', string(input.name, 'name'));
|
|
211
|
+
const description = string(input.description, 'description');
|
|
212
|
+
if (description.length === 0) throw new ProtocolError('[EMPTY_DESCRIPTION]', `tool ${JSON.stringify(name)} has no description, so a model cannot know when to use it`);
|
|
213
|
+
if (byteLength(description) > MAX_DESCRIPTION_BYTES) throw new ProtocolError('[DESCRIPTION_TOO_LONG]', `tool ${JSON.stringify(name)} has a description over the ${MAX_DESCRIPTION_BYTES} byte limit`);
|
|
214
|
+
return Object.freeze({ name, description, inputSchema: input.inputSchema });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function toolDefinitions(value, where) {
|
|
218
|
+
if (value === undefined) return [];
|
|
219
|
+
if (!Array.isArray(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be an array`);
|
|
220
|
+
if (value.length > MAX_DECLARATIONS) throw new ProtocolError('[TOO_MANY_DECLARATIONS]', `${where} declares ${value.length} entries, over the ${MAX_DECLARATIONS} limit`);
|
|
221
|
+
const seen = new Set();
|
|
222
|
+
const out = [];
|
|
223
|
+
for (const entry of value) {
|
|
224
|
+
const tool = pluginToolDefinition(entry);
|
|
225
|
+
if (seen.has(tool.name)) throw new ProtocolError('[DUPLICATE_DECLARATION]', `${where} declares ${JSON.stringify(tool.name)} twice`);
|
|
226
|
+
seen.add(tool.name);
|
|
227
|
+
out.push(tool);
|
|
228
|
+
}
|
|
229
|
+
return Object.freeze(out);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// What each adapter says about itself, keyed by the provider it serves. The
|
|
233
|
+
// values stay opaque — the wire layer decides only whether a route may be
|
|
234
|
+
// served, and `llmProviders` answers that — so the only rule here is that a
|
|
235
|
+
// description must belong to a route the same report claims.
|
|
236
|
+
function llmAdapters(value, providers, where) {
|
|
237
|
+
if (value === undefined) return Object.freeze({});
|
|
238
|
+
if (!plain(value)) throw new ProtocolError('[WRONG_SHAPE]', `${where} must be a plain object`);
|
|
239
|
+
const declared = new Set(providers);
|
|
240
|
+
for (const provider of Object.keys(value)) {
|
|
241
|
+
validateName('provider', provider);
|
|
242
|
+
if (!declared.has(provider)) {
|
|
243
|
+
throw new ProtocolError('[UNDECLARED_ADAPTER]', `adapter info describes provider ${JSON.stringify(provider)}, which this report does not serve`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return Object.freeze({ ...value });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function pluginReadyReport(input) {
|
|
250
|
+
shape(input, ['pluginId'], ['services', 'eventTopics', 'llmProviders', 'llmAdapters', 'tools', 'commands'], 'plugin/load terminal payload');
|
|
251
|
+
// Field order, not convenience order: a payload with two problems has to
|
|
252
|
+
// report the same one here and in Rust, and `llmAdapters` is checked against
|
|
253
|
+
// `llmProviders` so the providers have to be read first — but not before the
|
|
254
|
+
// fields Rust reads before them.
|
|
255
|
+
const pluginId = validatePluginId(string(input.pluginId, 'pluginId'));
|
|
256
|
+
const services = declarations(input.services, 'service', 'services');
|
|
257
|
+
const eventTopics = declarations(input.eventTopics, 'topic', 'eventTopics');
|
|
258
|
+
const providers = declarations(input.llmProviders, 'provider', 'llmProviders');
|
|
259
|
+
return Object.freeze({
|
|
260
|
+
pluginId,
|
|
261
|
+
services,
|
|
262
|
+
eventTopics,
|
|
263
|
+
llmProviders: providers,
|
|
264
|
+
llmAdapters: llmAdapters(input.llmAdapters, providers, 'llmAdapters'),
|
|
265
|
+
tools: toolDefinitions(input.tools, 'tools'),
|
|
266
|
+
commands: commandDefinitions(input.commands, 'commands'),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function pluginUnloadRequest(input) {
|
|
271
|
+
shape(input, ['pluginId'], [], 'plugin/unload payload');
|
|
272
|
+
return Object.freeze({ pluginId: validatePluginId(string(input.pluginId, 'pluginId')) });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function pluginDrainReport(input) {
|
|
276
|
+
shape(input, ['pluginId'], ['outstandingCalls', 'revokedSubscriptions'], 'plugin/unload terminal payload');
|
|
277
|
+
return Object.freeze({
|
|
278
|
+
pluginId: validatePluginId(string(input.pluginId, 'pluginId')),
|
|
279
|
+
outstandingCalls: declarations(input.outstandingCalls, 'call', 'outstandingCalls'),
|
|
280
|
+
revokedSubscriptions: declarations(input.revokedSubscriptions, 'subscription', 'revokedSubscriptions'),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function serviceCallRequest(input) {
|
|
285
|
+
shape(input, ['service', 'request'], [], 'service/call payload');
|
|
286
|
+
return Object.freeze({ service: validateName('service', string(input.service, 'service')), request: input.request });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function eventSubscribeRequest(input) {
|
|
290
|
+
shape(input, ['subscription', 'topic'], [], 'event/subscribe payload');
|
|
291
|
+
return Object.freeze({
|
|
292
|
+
subscription: validateName('subscription', string(input.subscription, 'subscription')),
|
|
293
|
+
topic: validateName('topic', string(input.topic, 'topic')),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function eventUnsubscribeRequest(input) {
|
|
298
|
+
shape(input, ['subscription'], [], 'event/unsubscribe payload');
|
|
299
|
+
return Object.freeze({ subscription: validateName('subscription', string(input.subscription, 'subscription')) });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function eventDelivery(input) {
|
|
303
|
+
shape(input, ['subscription', 'topic', 'event'], [], 'event/deliver payload');
|
|
304
|
+
return Object.freeze({
|
|
305
|
+
subscription: validateName('subscription', string(input.subscription, 'subscription')),
|
|
306
|
+
topic: validateName('topic', string(input.topic, 'topic')),
|
|
307
|
+
event: input.event,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function eventEmitRequest(input) {
|
|
312
|
+
shape(input, ['topic', 'event'], [], 'event/emit payload');
|
|
313
|
+
return Object.freeze({
|
|
314
|
+
topic: validateName('topic', string(input.topic, 'topic')),
|
|
315
|
+
event: input.event,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function toolInvokeRequest(input) {
|
|
320
|
+
shape(input, ['tool', 'input'], [], 'tool/invoke payload');
|
|
321
|
+
return Object.freeze({ tool: validateName('tool', string(input.tool, 'tool')), input: input.input });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function llmStreamRequest(input) {
|
|
325
|
+
shape(input, ['provider', 'request'], [], 'llm/stream payload');
|
|
326
|
+
return Object.freeze({ provider: validateName('provider', string(input.provider, 'provider')), request: input.request });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function llmControlRequest(input) {
|
|
330
|
+
shape(input, ['provider', 'signal'], [], 'llm/control payload');
|
|
331
|
+
// Provider first, matching the Rust validator's field order.
|
|
332
|
+
const provider = validateName('provider', string(input.provider, 'provider'));
|
|
333
|
+
const signal = string(input.signal, 'signal');
|
|
334
|
+
if (!LLM_CONTROL_SIGNALS.includes(signal)) {
|
|
335
|
+
throw new ProtocolError('[UNKNOWN_LLM_SIGNAL]', `llm control signal ${JSON.stringify(signal)} is not one of ${LLM_CONTROL_SIGNALS.join(', ')}`);
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze({ provider, signal });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function commandInvokeRequest(input) {
|
|
341
|
+
shape(input, ['name', 'raw', 'rest', 'surface'], [], 'command/invoke payload');
|
|
342
|
+
const name = validateName('command', string(input.name, 'name'));
|
|
343
|
+
const raw = string(input.raw, 'raw');
|
|
344
|
+
const rest = string(input.rest, 'rest');
|
|
345
|
+
const surface = string(input.surface, 'surface');
|
|
346
|
+
if (!COMMAND_SURFACES.includes(surface)) {
|
|
347
|
+
throw new ProtocolError('[WRONG_SHAPE]', `surface must be one of ${COMMAND_SURFACES.join(', ')}`);
|
|
348
|
+
}
|
|
349
|
+
return Object.freeze({ name, raw, rest, surface });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function seatCallRequest(input) {
|
|
353
|
+
shape(input, ['seat', 'method', 'params'], [], 'seat/call payload');
|
|
354
|
+
return Object.freeze({
|
|
355
|
+
seat: validateName('seat', string(input.seat, 'seat')),
|
|
356
|
+
method: validateName('method', string(input.method, 'method')),
|
|
357
|
+
params: input.params,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// The corpus's `kind` names, so a shared case table can dispatch by name.
|
|
362
|
+
export const VALIDATORS = Object.freeze({
|
|
363
|
+
plugin_load: pluginLoadRequest,
|
|
364
|
+
plugin_ready: pluginReadyReport,
|
|
365
|
+
plugin_unload: pluginUnloadRequest,
|
|
366
|
+
plugin_drain: pluginDrainReport,
|
|
367
|
+
service_call: serviceCallRequest,
|
|
368
|
+
event_subscribe: eventSubscribeRequest,
|
|
369
|
+
event_unsubscribe: eventUnsubscribeRequest,
|
|
370
|
+
event_deliver: eventDelivery,
|
|
371
|
+
event_emit: eventEmitRequest,
|
|
372
|
+
tool_invoke: toolInvokeRequest,
|
|
373
|
+
llm_stream: llmStreamRequest,
|
|
374
|
+
llm_control: llmControlRequest,
|
|
375
|
+
command_invoke: commandInvokeRequest,
|
|
376
|
+
seat_call: seatCallRequest,
|
|
377
|
+
});
|