@rebon/cli-darwin-arm64 0.17.2 → 0.18.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/bin/compose-runtime/package.json +17 -0
- package/bin/compose-runtime/payload/compose/credentials-runtime.js +38 -0
- package/bin/compose-runtime/payload/compose/llm-runtime.js +330 -0
- package/bin/compose-runtime/payload/compose/loop-assembly.js +149 -0
- package/bin/compose-runtime/payload/compose/serve-dispatch.js +82 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-llm.js +313 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-settings.js +43 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-tools.js +20 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-web.js +10 -0
- package/bin/compose-runtime/payload/compose/shims/llm-adapter.js +32 -0
- package/bin/compose-runtime/payload/compose/shims/zod-lite.js +49 -0
- package/bin/compose-runtime/payload/compose/systemprompt-runtime.js +74 -0
- package/bin/compose-runtime/payload/compose/tools-runtime.js +228 -0
- package/bin/compose-runtime/payload/compose/web-runtime.js +204 -0
- package/bin/compose-runtime/payload/vendor/cordis/index.js +1530 -0
- package/bin/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -0
- package/bin/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -0
- package/bin/compose-runtime/payload/vendor/dsh/agent.js +714 -0
- package/bin/compose-runtime/payload/vendor/dsh/llm-core.js +269 -0
- package/bin/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -0
- package/bin/compose-runtime/payload/vendor/dsh/logger-console.js +83 -0
- package/bin/compose-runtime/payload/vendor/dsh/scope.js +287 -0
- package/bin/compose-runtime/payload/vendor/dsh/session.js +1668 -0
- package/bin/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -0
- package/bin/compose-runtime/payload/vendor/dsh/timeout.js +100 -0
- package/bin/compose-runtime/payload/vendor/dsh/timer.js +128 -0
- package/bin/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -0
- package/bin/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -0
- package/bin/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -0
- package/bin/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -0
- package/bin/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -0
- package/bin/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -0
- package/bin/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -0
- package/bin/compose-runtime/payload/vendor/schemastery/index.mjs +656 -0
- package/bin/compose-runtime/payload-manifests.json +48 -0
- package/bin/compose-runtime/src/bridge.mjs +142 -0
- package/bin/compose-runtime/src/credentials-runtime.mjs +50 -0
- package/bin/compose-runtime/src/eventsource-stream.mjs +48 -0
- package/bin/compose-runtime/src/index.mjs +7 -0
- package/bin/compose-runtime/src/llm-runtime.mjs +326 -0
- package/bin/compose-runtime/src/loader.mjs +78 -0
- package/bin/compose-runtime/src/loop-assembly.mjs +237 -0
- package/bin/compose-runtime/src/payload.mjs +49 -0
- package/bin/compose-runtime/src/plugin.mjs +59 -0
- package/bin/compose-runtime/src/realm.mjs +223 -0
- package/bin/compose-runtime/src/registry.mjs +191 -0
- package/bin/compose-runtime/src/resolve.mjs +115 -0
- package/bin/compose-runtime/src/systemprompt-runtime.mjs +67 -0
- package/bin/compose-runtime/src/tools-runtime.mjs +239 -0
- package/bin/compose-runtime/src/web-runtime.mjs +209 -0
- package/bin/plugin-host/package.json +11 -0
- package/bin/plugin-host/src/bridge.mjs +64 -0
- package/bin/plugin-host/src/cli.mjs +105 -0
- package/bin/plugin-host/src/framing.mjs +113 -0
- package/bin/plugin-host/src/host.mjs +624 -0
- package/bin/plugin-host/src/json.mjs +196 -0
- package/bin/plugin-host/src/lifecycle.mjs +114 -0
- package/bin/plugin-host/src/loader.mjs +142 -0
- package/bin/plugin-host/src/methods.mjs +256 -0
- package/bin/plugin-host/src/protocol.mjs +129 -0
- package/bin/plugin-host/src/sdk.mjs +7 -0
- package/bin/rebon +0 -0
- package/bin/rebon-boa-helper +0 -0
- package/package.json +4 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { ProtocolError, RawJsonNumber, validateEnvelope } from './protocol.mjs';
|
|
2
|
+
|
|
3
|
+
class RawNumber {
|
|
4
|
+
constructor(raw) { this.raw = raw; Object.freeze(this); }
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
class JsonParser {
|
|
8
|
+
constructor(text) { this.text = text; this.index = 0; }
|
|
9
|
+
parse() {
|
|
10
|
+
this.#space();
|
|
11
|
+
const value = this.#value('envelope');
|
|
12
|
+
this.#space();
|
|
13
|
+
if (this.index !== this.text.length) this.#error('trailing_json', 'trailing data after JSON value');
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
#value(role = 'value') {
|
|
17
|
+
const c = this.text[this.index];
|
|
18
|
+
if (c === '{') return this.#object(role);
|
|
19
|
+
if (c === '[') return this.#array();
|
|
20
|
+
if (c === '"') return this.#string();
|
|
21
|
+
if (c === 't') return this.#literal('true', true);
|
|
22
|
+
if (c === 'f') return this.#literal('false', false);
|
|
23
|
+
if (c === 'n') return this.#literal('null', null);
|
|
24
|
+
if (c === '-' || (c >= '0' && c <= '9')) return this.#number();
|
|
25
|
+
this.#error('malformed_json', 'expected a JSON value');
|
|
26
|
+
}
|
|
27
|
+
#object(role) {
|
|
28
|
+
this.index++;
|
|
29
|
+
const object = Object.create(null);
|
|
30
|
+
const keys = new Set();
|
|
31
|
+
this.#space();
|
|
32
|
+
if (this.text[this.index] === '}') { this.index++; return object; }
|
|
33
|
+
for (;;) {
|
|
34
|
+
if (this.text[this.index] !== '"') this.#error('malformed_json', 'object key must be a string');
|
|
35
|
+
const key = this.#string();
|
|
36
|
+
this.#space();
|
|
37
|
+
if (this.text[this.index++] !== ':') this.#error('malformed_json', 'expected colon after object key');
|
|
38
|
+
this.#space();
|
|
39
|
+
const childRole = role === 'envelope' && key === 'message' ? 'message' : 'value';
|
|
40
|
+
const value = this.#value(childRole);
|
|
41
|
+
if ((role === 'envelope' || role === 'message') && keys.has(key)) {
|
|
42
|
+
this.#error('duplicate_field', `duplicate field in ${role}`);
|
|
43
|
+
}
|
|
44
|
+
keys.add(key);
|
|
45
|
+
object[key] = value; // serde_json::Value uses the last value for opaque object duplicates.
|
|
46
|
+
this.#space();
|
|
47
|
+
const delimiter = this.text[this.index++];
|
|
48
|
+
if (delimiter === '}') return object;
|
|
49
|
+
if (delimiter !== ',') this.#error('malformed_json', 'expected comma or object end');
|
|
50
|
+
this.#space();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
#array() {
|
|
54
|
+
this.index++;
|
|
55
|
+
const array = [];
|
|
56
|
+
this.#space();
|
|
57
|
+
if (this.text[this.index] === ']') { this.index++; return array; }
|
|
58
|
+
for (;;) {
|
|
59
|
+
array.push(this.#value());
|
|
60
|
+
this.#space();
|
|
61
|
+
const delimiter = this.text[this.index++];
|
|
62
|
+
if (delimiter === ']') return array;
|
|
63
|
+
if (delimiter !== ',') this.#error('malformed_json', 'expected comma or array end');
|
|
64
|
+
this.#space();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
#string() {
|
|
68
|
+
this.index++;
|
|
69
|
+
let result = '';
|
|
70
|
+
for (;;) {
|
|
71
|
+
if (this.index >= this.text.length) this.#error('malformed_json', 'unterminated JSON string');
|
|
72
|
+
const c = this.text[this.index++];
|
|
73
|
+
if (c === '"') return result;
|
|
74
|
+
if (c === '\\') {
|
|
75
|
+
if (this.index >= this.text.length) this.#error('malformed_json', 'unterminated JSON escape');
|
|
76
|
+
const escape = this.text[this.index++];
|
|
77
|
+
const simple = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' }[escape];
|
|
78
|
+
if (simple !== undefined) { result += simple; continue; }
|
|
79
|
+
if (escape !== 'u') this.#error('malformed_json', 'invalid JSON escape');
|
|
80
|
+
const first = this.#hexUnit();
|
|
81
|
+
if (first >= 0xd800 && first <= 0xdbff) {
|
|
82
|
+
if (this.text.slice(this.index, this.index + 2) !== '\\u') this.#error('invalid_surrogate', 'lone high surrogate in JSON string');
|
|
83
|
+
this.index += 2;
|
|
84
|
+
const second = this.#hexUnit();
|
|
85
|
+
if (second < 0xdc00 || second > 0xdfff) this.#error('invalid_surrogate', 'invalid surrogate pair in JSON string');
|
|
86
|
+
result += String.fromCodePoint(0x10000 + ((first - 0xd800) << 10) + second - 0xdc00);
|
|
87
|
+
} else if (first >= 0xdc00 && first <= 0xdfff) {
|
|
88
|
+
this.#error('invalid_surrogate', 'lone low surrogate in JSON string');
|
|
89
|
+
} else result += String.fromCharCode(first);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (c.charCodeAt(0) < 0x20) this.#error('malformed_json', 'unescaped control character in JSON string');
|
|
93
|
+
if (c.charCodeAt(0) >= 0xd800 && c.charCodeAt(0) <= 0xdfff) this.#error('invalid_surrogate', 'lone surrogate in JSON string');
|
|
94
|
+
result += c;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
#hexUnit() {
|
|
98
|
+
const token = this.text.slice(this.index, this.index + 4);
|
|
99
|
+
if (!/^[0-9a-fA-F]{4}$/.test(token)) this.#error('malformed_json', 'invalid unicode escape');
|
|
100
|
+
this.index += 4;
|
|
101
|
+
return Number.parseInt(token, 16);
|
|
102
|
+
}
|
|
103
|
+
#number() {
|
|
104
|
+
const start = this.index;
|
|
105
|
+
if (this.text[this.index] === '-') this.index++;
|
|
106
|
+
if (this.text[this.index] === '0') this.index++;
|
|
107
|
+
else {
|
|
108
|
+
if (this.text[this.index] < '1' || this.text[this.index] > '9') this.#error('malformed_json', 'invalid JSON number');
|
|
109
|
+
while (this.text[this.index] >= '0' && this.text[this.index] <= '9') this.index++;
|
|
110
|
+
}
|
|
111
|
+
if (this.text[this.index] === '.') {
|
|
112
|
+
this.index++;
|
|
113
|
+
const fraction = this.index;
|
|
114
|
+
while (this.text[this.index] >= '0' && this.text[this.index] <= '9') this.index++;
|
|
115
|
+
if (fraction === this.index) this.#error('malformed_json', 'fraction requires digits');
|
|
116
|
+
}
|
|
117
|
+
if (this.text[this.index] === 'e' || this.text[this.index] === 'E') {
|
|
118
|
+
this.index++;
|
|
119
|
+
if (this.text[this.index] === '+' || this.text[this.index] === '-') this.index++;
|
|
120
|
+
const exponent = this.index;
|
|
121
|
+
while (this.text[this.index] >= '0' && this.text[this.index] <= '9') this.index++;
|
|
122
|
+
if (exponent === this.index) this.#error('malformed_json', 'exponent requires digits');
|
|
123
|
+
}
|
|
124
|
+
return new RawNumber(this.text.slice(start, this.index));
|
|
125
|
+
}
|
|
126
|
+
#literal(token, value) {
|
|
127
|
+
if (this.text.slice(this.index, this.index + token.length) !== token) this.#error('malformed_json', 'invalid JSON literal');
|
|
128
|
+
this.index += token.length;
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
#space() { while (' \t\r\n'.includes(this.text[this.index] ?? '\0')) this.index++; }
|
|
132
|
+
#error(code, message) { throw new ProtocolError(code, message); }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function exponentValue(raw) {
|
|
136
|
+
const marker = Math.max(raw.indexOf('e'), raw.indexOf('E'));
|
|
137
|
+
if (marker < 0) return 0;
|
|
138
|
+
let value = raw.slice(marker + 1);
|
|
139
|
+
let sign = 1;
|
|
140
|
+
if (value[0] === '+' || value[0] === '-') { if (value[0] === '-') sign = -1; value = value.slice(1); }
|
|
141
|
+
value = value.replace(/^0+/, '') || '0';
|
|
142
|
+
if (value.length > 9) return sign * Infinity;
|
|
143
|
+
return sign * Number(value);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function parseSafeIntegerToken(raw, field) {
|
|
147
|
+
const negative = raw[0] === '-';
|
|
148
|
+
const unsigned = negative ? raw.slice(1) : raw;
|
|
149
|
+
const marker = Math.max(unsigned.indexOf('e'), unsigned.indexOf('E'));
|
|
150
|
+
const mantissa = marker < 0 ? unsigned : unsigned.slice(0, marker);
|
|
151
|
+
const dot = mantissa.indexOf('.');
|
|
152
|
+
const fractionLength = dot < 0 ? 0 : mantissa.length - dot - 1;
|
|
153
|
+
let digits = (dot < 0 ? mantissa : mantissa.slice(0, dot) + mantissa.slice(dot + 1)).replace(/^0+/, '');
|
|
154
|
+
if (!digits) return 0;
|
|
155
|
+
if (negative) throw new ProtocolError('unsafe_integer', `${field} must be a safe non-negative integer`);
|
|
156
|
+
const scale = exponentValue(unsigned) - fractionLength;
|
|
157
|
+
if (!Number.isFinite(scale)) throw new ProtocolError('unsafe_integer', `${field} must be a safe non-negative integer`);
|
|
158
|
+
if (scale < 0) {
|
|
159
|
+
const remove = -scale;
|
|
160
|
+
if (remove > digits.length || !digits.endsWith('0'.repeat(remove))) throw new ProtocolError('unsafe_integer', `${field} must be an exact integer`);
|
|
161
|
+
digits = digits.slice(0, digits.length - remove).replace(/^0+/, '') || '0';
|
|
162
|
+
} else {
|
|
163
|
+
if (digits.length + scale > 16) throw new ProtocolError('unsafe_integer', `${field} must be a safe non-negative integer`);
|
|
164
|
+
digits += '0'.repeat(scale);
|
|
165
|
+
}
|
|
166
|
+
if (digits.length > 16 || (digits.length === 16 && digits > '9007199254740991')) {
|
|
167
|
+
throw new ProtocolError('unsafe_integer', `${field} must be a safe non-negative integer`);
|
|
168
|
+
}
|
|
169
|
+
return Number(digits);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function materialize(value) {
|
|
173
|
+
if (value instanceof RawNumber) {
|
|
174
|
+
const number = Number(value.raw);
|
|
175
|
+
if (/^-?(?:0|[1-9][0-9]*)$/.test(value.raw) && Number.isSafeInteger(number)) return Object.is(number, -0) ? 0 : number;
|
|
176
|
+
return new RawJsonNumber(value.raw);
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(value)) return value.map(materialize);
|
|
179
|
+
if (value && typeof value === 'object') for (const key of Object.keys(value)) value[key] = materialize(value[key]);
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function parseEnvelopeJson(text) {
|
|
184
|
+
const value = new JsonParser(text).parse();
|
|
185
|
+
if (!value || value instanceof RawNumber || Array.isArray(value) || typeof value !== 'object') throw new ProtocolError('wrong_shape', 'envelope must be an object');
|
|
186
|
+
if (!(value.protocol_version instanceof RawNumber) || value.protocol_version.raw !== '1') {
|
|
187
|
+
throw new ProtocolError('bad_version', 'protocol_version must be integer token 1');
|
|
188
|
+
}
|
|
189
|
+
value.protocol_version = 1;
|
|
190
|
+
for (const field of ['host_epoch', 'scope_generation']) {
|
|
191
|
+
if (!(value[field] instanceof RawNumber)) throw new ProtocolError('unsafe_integer', `${field} must be a JSON number`);
|
|
192
|
+
value[field] = parseSafeIntegerToken(value[field].raw, field);
|
|
193
|
+
}
|
|
194
|
+
value.message = materialize(value.message);
|
|
195
|
+
return validateEnvelope(value);
|
|
196
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ProtocolError, identityOf } from './protocol.mjs';
|
|
2
|
+
|
|
3
|
+
const scopeKey = (pluginId, scopeId) => JSON.stringify([pluginId, scopeId]);
|
|
4
|
+
const key = (identity) => scopeKey(identity.plugin_id, identity.scope_id);
|
|
5
|
+
const same = (a, b) => ['host_epoch','plugin_id','scope_id','scope_generation','call_id'].every((field) => a[field] === b[field]);
|
|
6
|
+
|
|
7
|
+
export class CallLedger {
|
|
8
|
+
#scopes = new Map(); #calls = new Map();
|
|
9
|
+
constructor(hostEpoch) {
|
|
10
|
+
if (!Number.isSafeInteger(hostEpoch) || hostEpoch < 0) throw new ProtocolError('unsafe_integer', 'host_epoch is invalid');
|
|
11
|
+
this.hostEpoch = Object.is(hostEpoch, -0) ? 0 : hostEpoch;
|
|
12
|
+
}
|
|
13
|
+
scopeGeneration(pluginId, scopeId) { return this.#scopes.get(scopeKey(pluginId, scopeId)); }
|
|
14
|
+
advanceScope(pluginId, scopeId, generation, open = true) {
|
|
15
|
+
if (!Number.isSafeInteger(generation) || generation < 0) throw new ProtocolError('unsafe_integer', 'scope_generation is invalid');
|
|
16
|
+
const key = scopeKey(pluginId, scopeId);
|
|
17
|
+
const current = this.#scopes.get(key);
|
|
18
|
+
if (current && generation < current.generation) throw new ProtocolError('scope_generation_regression', 'scope generation regressed');
|
|
19
|
+
if (current && generation === current.generation) {
|
|
20
|
+
if (open && current.open) return 'idempotent';
|
|
21
|
+
if (!open && !current.open) return 'idempotent';
|
|
22
|
+
current.open = open;
|
|
23
|
+
return 'updated';
|
|
24
|
+
}
|
|
25
|
+
this.#scopes.set(key, { generation, open });
|
|
26
|
+
return current ? 'advanced' : 'established';
|
|
27
|
+
}
|
|
28
|
+
closeScope(pluginId, scopeId, generation) {
|
|
29
|
+
const current = this.#scopes.get(scopeKey(pluginId, scopeId));
|
|
30
|
+
if (!current) throw new ProtocolError('unknown_scope', 'scope is not registered');
|
|
31
|
+
if (generation < current.generation) throw new ProtocolError('scope_generation_regression', 'scope generation regressed');
|
|
32
|
+
if (generation === current.generation && current.open) throw new ProtocolError('scope_close_not_advanced', 'scope close must carry an advanced generation');
|
|
33
|
+
return this.advanceScope(pluginId, scopeId, generation, false);
|
|
34
|
+
}
|
|
35
|
+
register(identity, direction, owner) {
|
|
36
|
+
if (direction !== 'inbound' && direction !== 'outbound') throw new ProtocolError('bad_direction', 'call direction is invalid');
|
|
37
|
+
if (direction === 'inbound') this.#classifyEpoch(identity, true);
|
|
38
|
+
else this.#classify(identity, true);
|
|
39
|
+
if (this.#calls.has(identity.call_id)) throw new ProtocolError('call_id_reused', 'call_id is globally unique within an epoch');
|
|
40
|
+
this.#calls.set(identity.call_id, Object.freeze({ identity: identityOf(identity), direction, owner, terminal: null, registered: true }));
|
|
41
|
+
}
|
|
42
|
+
retire(identity) {
|
|
43
|
+
const current = this.#calls.get(identity.call_id);
|
|
44
|
+
if (!current || !current.registered) throw new ProtocolError('unknown_call', 'call is not registered');
|
|
45
|
+
if (!same(current.identity, identity)) throw new ProtocolError('identity_mismatch', 'full call identity does not match');
|
|
46
|
+
if (current.terminal) throw new ProtocolError('already_terminal', 'terminal call cannot be aborted');
|
|
47
|
+
this.#calls.set(identity.call_id, Object.freeze({ ...current, registered: false }));
|
|
48
|
+
}
|
|
49
|
+
terminal(envelope, direction) {
|
|
50
|
+
if (direction !== 'inbound' && direction !== 'outbound') throw new ProtocolError('bad_direction', 'terminal direction is invalid');
|
|
51
|
+
const epochStale = this.#classifyEpoch(envelope, false); if (epochStale) return { stale: epochStale };
|
|
52
|
+
const current = this.#calls.get(envelope.call_id);
|
|
53
|
+
if (current && (current.identity.plugin_id !== envelope.plugin_id || current.identity.scope_id !== envelope.scope_id))
|
|
54
|
+
throw new ProtocolError('identity_mismatch', 'plugin_id or scope_id does not match');
|
|
55
|
+
const scopeStale = direction === 'outbound' ? this.#classifyScope(envelope, false) : null; if (scopeStale) return { stale: scopeStale };
|
|
56
|
+
if (!current || !current.registered) throw new ProtocolError('unknown_call', 'call is not registered');
|
|
57
|
+
if (current.direction !== direction) throw new ProtocolError('direction_mismatch', `call belongs to ${current.direction} direction`);
|
|
58
|
+
if (!same(current.identity, envelope)) throw new ProtocolError('identity_mismatch', 'full call identity does not match');
|
|
59
|
+
if (envelope.message.type !== 'terminal') throw new ProtocolError('not_terminal', 'message is not terminal');
|
|
60
|
+
if (current.terminal) throw new ProtocolError('duplicate_terminal', `terminal already committed as ${current.terminal}`);
|
|
61
|
+
this.#calls.set(envelope.call_id, Object.freeze({ ...current, terminal: envelope.message.status }));
|
|
62
|
+
return { committed: envelope.message.status, owner: current.owner };
|
|
63
|
+
}
|
|
64
|
+
/// Validates one chunk of a call's answer. Chunks are checked, not recorded:
|
|
65
|
+
/// a stream of a million of them must not grow the ledger.
|
|
66
|
+
///
|
|
67
|
+
/// The boundary is what the ledger owns — **a chunk after the terminal is an
|
|
68
|
+
/// error**, not a late arrival to drop. A duplicate terminal is a race two
|
|
69
|
+
/// peers can legitimately lose; a chunk after the end means the producer kept
|
|
70
|
+
/// emitting after saying it was done.
|
|
71
|
+
chunk(envelope, direction) {
|
|
72
|
+
if (direction !== 'inbound' && direction !== 'outbound') throw new ProtocolError('bad_direction', 'chunk direction is invalid');
|
|
73
|
+
const epochStale = this.#classifyEpoch(envelope, false); if (epochStale) return { stale: epochStale };
|
|
74
|
+
const current = this.#calls.get(envelope.call_id);
|
|
75
|
+
if (current && (current.identity.plugin_id !== envelope.plugin_id || current.identity.scope_id !== envelope.scope_id))
|
|
76
|
+
throw new ProtocolError('identity_mismatch', 'plugin_id or scope_id does not match');
|
|
77
|
+
const scopeStale = this.#classifyScope(envelope, false); if (scopeStale) return { stale: scopeStale };
|
|
78
|
+
if (!current || !current.registered) throw new ProtocolError('unknown_call', 'call is not registered');
|
|
79
|
+
if (current.direction !== direction) throw new ProtocolError('direction_mismatch', `call belongs to ${current.direction} direction`);
|
|
80
|
+
if (!same(current.identity, envelope)) throw new ProtocolError('identity_mismatch', 'full call identity does not match');
|
|
81
|
+
if (envelope.message.type !== 'chunk') throw new ProtocolError('not_chunk', 'message is not a chunk');
|
|
82
|
+
if (current.terminal) throw new ProtocolError('chunk_after_terminal', `call already ended as ${current.terminal}`);
|
|
83
|
+
return { accepted: true };
|
|
84
|
+
}
|
|
85
|
+
cancel(envelope) {
|
|
86
|
+
const epochStale = this.#classifyEpoch(envelope, false); if (epochStale) return { stale: epochStale };
|
|
87
|
+
const current = this.#calls.get(envelope.call_id);
|
|
88
|
+
if (current && (current.identity.plugin_id !== envelope.plugin_id || current.identity.scope_id !== envelope.scope_id))
|
|
89
|
+
throw new ProtocolError('identity_mismatch', 'plugin_id or scope_id does not match');
|
|
90
|
+
const scopeStale = this.#classifyScope(envelope, false); if (scopeStale) return { stale: scopeStale };
|
|
91
|
+
if (!current || !current.registered) throw new ProtocolError('unknown_call', 'call is not registered');
|
|
92
|
+
if (!same(current.identity, envelope)) throw new ProtocolError('identity_mismatch', 'full call identity does not match');
|
|
93
|
+
if (envelope.message.type !== 'notification' || envelope.message.method !== 'call/cancel' || envelope.message.payload !== null)
|
|
94
|
+
throw new ProtocolError('not_cancel', 'message is not an exact cancel intent');
|
|
95
|
+
return { accepted: true };
|
|
96
|
+
}
|
|
97
|
+
#classify(identity, registration) {
|
|
98
|
+
const epochStale = this.#classifyEpoch(identity, registration); if (epochStale) return epochStale;
|
|
99
|
+
return this.#classifyScope(identity, registration);
|
|
100
|
+
}
|
|
101
|
+
#classifyEpoch(identity, registration) {
|
|
102
|
+
if (identity.host_epoch < this.hostEpoch) return registration ? (() => { throw new ProtocolError('stale_identity', 'old host epoch'); })() : 'host_epoch';
|
|
103
|
+
if (identity.host_epoch > this.hostEpoch) throw new ProtocolError('future_host_epoch', 'future host epoch');
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
#classifyScope(identity, registration) {
|
|
107
|
+
const scope = this.#scopes.get(key(identity));
|
|
108
|
+
if (!scope) throw new ProtocolError('unknown_scope', 'scope is not registered');
|
|
109
|
+
if (identity.scope_generation < scope.generation) return registration ? (() => { throw new ProtocolError('stale_identity', 'old scope generation'); })() : 'scope_generation';
|
|
110
|
+
if (identity.scope_generation > scope.generation) throw new ProtocolError('future_scope_generation', 'future scope generation');
|
|
111
|
+
if (registration && !scope.open) throw new ProtocolError('scope_closed', 'scope is closed to admission');
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Turning a `plugin/load` request into a loaded plugin.
|
|
2
|
+
//
|
|
3
|
+
// The registrar is the only thing a plugin's code is handed, and it enforces
|
|
4
|
+
// the same rule the Rust registry does from the other side: a plugin may
|
|
5
|
+
// register a service or a topic only if its manifest declared one. Checking it
|
|
6
|
+
// here as well is not redundancy for its own sake — it means the refusal names
|
|
7
|
+
// the plugin's own line rather than arriving as a rejected ready report after
|
|
8
|
+
// the module has already run and had its side effects.
|
|
9
|
+
//
|
|
10
|
+
// Registration closes when the entry module's `activate` returns. A plugin that
|
|
11
|
+
// registers later would be adding capabilities the host already reported as the
|
|
12
|
+
// complete set, and nothing downstream would know.
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import { ProtocolError } from './protocol.mjs';
|
|
16
|
+
import { pluginToolDefinition, validateName } from './methods.mjs';
|
|
17
|
+
|
|
18
|
+
/// Resolves an entry against its package root and refuses anything outside it.
|
|
19
|
+
///
|
|
20
|
+
/// The payload validator already rejects `..`, absolute entries, and
|
|
21
|
+
/// backslashes; this is the check that survives a symlink or a root that is
|
|
22
|
+
/// itself a relative-looking string, because it asks the filesystem's own
|
|
23
|
+
/// resolver rather than the text.
|
|
24
|
+
export function resolveEntry(root, entry) {
|
|
25
|
+
const base = path.resolve(root);
|
|
26
|
+
const resolved = path.resolve(base, entry);
|
|
27
|
+
const inside = resolved === base || resolved.startsWith(base + path.sep);
|
|
28
|
+
if (!inside) throw new ProtocolError('[PATH_ESCAPES]', `entry ${entry} resolves outside the package root`);
|
|
29
|
+
return pathToFileURL(resolved).href;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class Registrar {
|
|
33
|
+
#declaredServices; #declaredTopics; #declaredProviders; #declaredTools;
|
|
34
|
+
#services = new Map(); #topics = new Map(); #adapters = new Map();
|
|
35
|
+
#tools = new Map(); #toolHandlers = new Map(); #scopes = []; #sealed = false;
|
|
36
|
+
constructor(declaredServices, declaredTopics, declaredProviders, declaredTools) {
|
|
37
|
+
this.#declaredServices = new Set(declaredServices);
|
|
38
|
+
this.#declaredTopics = new Set(declaredTopics);
|
|
39
|
+
this.#declaredProviders = new Set(declaredProviders);
|
|
40
|
+
this.#declaredTools = new Set(declaredTools);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// A tool differs from a service in one way that matters: it has to be
|
|
44
|
+
/// *offered* to a model, so it cannot be registered by name alone. The
|
|
45
|
+
/// manifest still declares only the name — description and schema are how a
|
|
46
|
+
/// tool describes itself, not what it is permitted to be.
|
|
47
|
+
#admitTool(definition, handler) {
|
|
48
|
+
if (this.#sealed) throw new ProtocolError('[REGISTRATION_CLOSED]', 'a tool was registered after activation finished');
|
|
49
|
+
const tool = pluginToolDefinition(definition);
|
|
50
|
+
if (typeof handler !== 'function') throw new ProtocolError('[WRONG_SHAPE]', `tool ${JSON.stringify(tool.name)} needs a function`);
|
|
51
|
+
if (!this.#declaredTools.has(tool.name)) throw new ProtocolError('[UNAUTHORIZED_REGISTER]', `tool ${JSON.stringify(tool.name)} is not declared by this plugin's manifest`);
|
|
52
|
+
if (this.#tools.has(tool.name)) throw new ProtocolError('[DUPLICATE_DECLARATION]', `tool ${JSON.stringify(tool.name)} is registered twice`);
|
|
53
|
+
this.#tools.set(tool.name, tool);
|
|
54
|
+
this.#toolHandlers.set(tool.name, handler);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#admit(kind, name, handler, declared, into) {
|
|
58
|
+
if (this.#sealed) throw new ProtocolError('[REGISTRATION_CLOSED]', `${kind} ${JSON.stringify(name)} was registered after activation finished`);
|
|
59
|
+
validateName(kind, name);
|
|
60
|
+
if (typeof handler !== 'function') throw new ProtocolError('[WRONG_SHAPE]', `${kind} ${JSON.stringify(name)} needs a function`);
|
|
61
|
+
if (!declared.has(name)) throw new ProtocolError('[UNAUTHORIZED_REGISTER]', `${kind} ${JSON.stringify(name)} is not declared by this plugin's manifest`);
|
|
62
|
+
if (into.has(name)) throw new ProtocolError('[DUPLICATE_DECLARATION]', `${kind} ${JSON.stringify(name)} is registered twice`);
|
|
63
|
+
into.set(name, handler);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// The surface a plugin's `activate` receives. Frozen, and holding no
|
|
67
|
+
/// reference back to the host: at activation a plugin registers, it does not
|
|
68
|
+
/// reach.
|
|
69
|
+
///
|
|
70
|
+
/// Reaching happens later and only from inside a handler, which is called as
|
|
71
|
+
/// `handler(request, ctx)`. That `ctx` is bound to the scope incarnation the
|
|
72
|
+
/// call arrived on and carries `invoke(tool, input)` — so a plugin can act on
|
|
73
|
+
/// the session it was called for and has no way to name another.
|
|
74
|
+
api() {
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
service: (name, handler) => this.#admit('service', name, handler, this.#declaredServices, this.#services),
|
|
77
|
+
topic: (name, handler) => this.#admit('topic', name, handler, this.#declaredTopics, this.#topics),
|
|
78
|
+
// An llm adapter is a service under a different routing key: declared in
|
|
79
|
+
// the manifest, registered here, and answered with chunks instead of one
|
|
80
|
+
// value.
|
|
81
|
+
llm: (provider, adapter) => this.#admit('provider', provider, adapter, this.#declaredProviders, this.#adapters),
|
|
82
|
+
tool: (definition, handler) => this.#admitTool(definition, handler),
|
|
83
|
+
// Not a capability, so nothing to declare: everything a scope handle can
|
|
84
|
+
// do is gated by the declarations above. What it is, is a lifetime — the
|
|
85
|
+
// session, rather than one call inside it. A plugin that produces facts
|
|
86
|
+
// on its own schedule (an agent loop, a watcher) has no inbound call to
|
|
87
|
+
// hang them on, and requiring one would mean it could only speak when
|
|
88
|
+
// spoken to.
|
|
89
|
+
scope: (handler) => {
|
|
90
|
+
if (this.#sealed) throw new ProtocolError('[REGISTRATION_CLOSED]', 'a scope handler was registered after activation finished');
|
|
91
|
+
if (typeof handler !== 'function') throw new ProtocolError('[WRONG_SHAPE]', 'a scope handler must be a function');
|
|
92
|
+
this.#scopes.push(handler);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
seal() {
|
|
98
|
+
this.#sealed = true;
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
services: Object.freeze([...this.#services.keys()]),
|
|
101
|
+
eventTopics: Object.freeze([...this.#topics.keys()]),
|
|
102
|
+
llmProviders: Object.freeze([...this.#adapters.keys()]),
|
|
103
|
+
tools: Object.freeze([...this.#tools.values()]),
|
|
104
|
+
serviceHandlers: this.#services,
|
|
105
|
+
topicHandlers: this.#topics,
|
|
106
|
+
llmAdapters: this.#adapters,
|
|
107
|
+
toolHandlers: this.#toolHandlers,
|
|
108
|
+
scopeHandlers: Object.freeze([...this.#scopes]),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Imports a plugin and collects what it registered.
|
|
114
|
+
///
|
|
115
|
+
/// `activate` is called as `activate(api, config)`: the package says what the
|
|
116
|
+
/// plugin can do, the load request's `config` says what this installation wants
|
|
117
|
+
/// it to do. The second argument is opaque here — reading it is the plugin's
|
|
118
|
+
/// business, and validating it is the plugin's job rather than the wire's.
|
|
119
|
+
///
|
|
120
|
+
/// `importModule` is injectable so the loader's rules can be tested without a
|
|
121
|
+
/// file on disk for every case.
|
|
122
|
+
export async function loadPlugin(request, importModule = (url) => import(url)) {
|
|
123
|
+
const url = resolveEntry(request.root, request.entry);
|
|
124
|
+
let module;
|
|
125
|
+
try {
|
|
126
|
+
module = await importModule(url);
|
|
127
|
+
} catch (cause) {
|
|
128
|
+
throw new ProtocolError('[ENTRY_FAILED]', `plugin entry ${request.entry} failed to load: ${cause?.message ?? cause}`);
|
|
129
|
+
}
|
|
130
|
+
const activate = module?.activate ?? module?.default;
|
|
131
|
+
if (typeof activate !== 'function') {
|
|
132
|
+
throw new ProtocolError('[NO_ACTIVATE]', `plugin entry ${request.entry} exports no activate function`);
|
|
133
|
+
}
|
|
134
|
+
const registrar = new Registrar(request.services, request.eventTopics, request.llmProviders, request.tools);
|
|
135
|
+
try {
|
|
136
|
+
await activate(registrar.api(), request.config);
|
|
137
|
+
} catch (cause) {
|
|
138
|
+
if (cause instanceof ProtocolError) throw cause;
|
|
139
|
+
throw new ProtocolError('[ACTIVATE_FAILED]', `plugin activation failed: ${cause?.message ?? cause}`);
|
|
140
|
+
}
|
|
141
|
+
return registrar.seal();
|
|
142
|
+
}
|