@kadi.build/core 0.0.1-alpha.1 → 0.0.1-alpha.3

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.
Files changed (39) hide show
  1. package/README.md +1145 -216
  2. package/examples/example-abilities/echo-js/README.md +131 -0
  3. package/examples/example-abilities/echo-js/agent.json +63 -0
  4. package/examples/example-abilities/echo-js/package.json +24 -0
  5. package/examples/example-abilities/echo-js/service.js +43 -0
  6. package/examples/example-abilities/hash-go/agent.json +53 -0
  7. package/examples/example-abilities/hash-go/cmd/hash_ability/main.go +340 -0
  8. package/examples/example-abilities/hash-go/go.mod +3 -0
  9. package/examples/example-agent/abilities/echo-js/0.0.1/README.md +131 -0
  10. package/examples/example-agent/abilities/echo-js/0.0.1/agent.json +63 -0
  11. package/examples/example-agent/abilities/echo-js/0.0.1/package-lock.json +93 -0
  12. package/examples/example-agent/abilities/echo-js/0.0.1/package.json +24 -0
  13. package/examples/example-agent/abilities/echo-js/0.0.1/service.js +41 -0
  14. package/examples/example-agent/abilities/hash-go/0.0.1/agent.json +53 -0
  15. package/examples/example-agent/abilities/hash-go/0.0.1/bin/hash_ability +0 -0
  16. package/examples/example-agent/abilities/hash-go/0.0.1/cmd/hash_ability/main.go +340 -0
  17. package/examples/example-agent/abilities/hash-go/0.0.1/go.mod +3 -0
  18. package/examples/example-agent/agent.json +39 -0
  19. package/examples/example-agent/index.js +102 -0
  20. package/examples/example-agent/package-lock.json +93 -0
  21. package/examples/example-agent/package.json +17 -0
  22. package/package.json +4 -2
  23. package/src/KadiAbility.js +478 -0
  24. package/src/index.js +65 -0
  25. package/src/loadAbility.js +1086 -0
  26. package/src/servers/BaseRpcServer.js +404 -0
  27. package/src/servers/BrokerRpcServer.js +776 -0
  28. package/src/servers/StdioRpcServer.js +360 -0
  29. package/src/transport/BrokerMessageBuilder.js +377 -0
  30. package/src/transport/IpcMessageBuilder.js +1229 -0
  31. package/src/utils/agentUtils.js +137 -0
  32. package/src/utils/commandUtils.js +64 -0
  33. package/src/utils/configUtils.js +72 -0
  34. package/src/utils/logger.js +161 -0
  35. package/src/utils/pathUtils.js +86 -0
  36. package/broker.js +0 -214
  37. package/index.js +0 -370
  38. package/ipc.js +0 -220
  39. package/ipcInterfaces/pythonAbilityIPC.py +0 -177
@@ -0,0 +1,377 @@
1
+ // BrokerMessageBuilder.js
2
+ // ESM module
3
+ // A tiny, zero‑dependency helper to build JSON‑RPC messages that "speak"
4
+ // to the broker. Method names default to the current broker handlers but
5
+ // can be reconfigured if/when you rename them.
6
+ //
7
+ // Public API:
8
+ // - IdFactory, newRequestId
9
+ // - toBase64Der (SPKI) + internal signing helper for authenticate
10
+ // - Broker (hello/auth/register/ping/list/callAbility/abilityResult/agentAbility)
11
+ // - MCP (subset; optional for the WS <-> stdio bridge)
12
+ // - configureMethods(overrides) to swap JSON‑RPC method strings
13
+ //
14
+ // Example:
15
+ // import BrokerMessageBuilder, { Broker, IdFactory } from './BrokerMessageBuilder.js';
16
+ // const ids = new IdFactory();
17
+ // ws.send(Broker.hello({ role: 'agent' }).id(ids.next()).toString());
18
+ // ws.send(Broker.authenticate({...}).id(ids.next()).toString());
19
+ // ws.send(Broker.registerCapabilities({...}).id(ids.next()).toString());
20
+ // ws.send(Broker.callAbility({ toolName: 'echoC', args: { message: 'hi' } }).id(ids.next()).toString());
21
+ // ws.send(Broker.abilityResult({ requestId, toSessionId, result: {...} }).toString());
22
+
23
+ import crypto from 'node:crypto';
24
+ import { randomUUID } from 'node:crypto';
25
+
26
+ /** @typedef {import('node:crypto').KeyObject} KeyObject */
27
+
28
+ /** Central method-name map (override via configureMethods). */
29
+ const methodNames = {
30
+ hello: 'kadi.hello',
31
+ authenticate: 'kadi.authenticate',
32
+ registerCapabilities: 'kadi.register_capabilities',
33
+ ping: 'kadi.ping',
34
+ listTools: 'kadi.list_tools',
35
+ callAbility: 'agent.callAbility',
36
+ abilityResult: 'ability.result'
37
+ };
38
+
39
+ /** Allow consumers to override one or more JSON‑RPC method strings. */
40
+ export function configureMethods(overrides = {}) {
41
+ for (const [k, v] of Object.entries(overrides)) {
42
+ if (!(k in methodNames)) throw new Error(`Unknown method key: ${k}`);
43
+ if (typeof v !== 'string' || !v.length)
44
+ throw new Error(`Bad method string for ${k}`);
45
+ methodNames[k] = v;
46
+ }
47
+ return { ...methodNames };
48
+ }
49
+
50
+ /** Minimal JSON-RPC 2.0 message wrapper with a chainable API. */
51
+ class JsonRpcBuilder {
52
+ /**
53
+ * @param {string} method
54
+ * @param {object|undefined} params
55
+ * @param {boolean} isNotification - when true, no id will be set unless forced via .id()
56
+ */
57
+ constructor(method, params = undefined, isNotification = false) {
58
+ this._jsonrpc = '2.0';
59
+ this._method = method;
60
+ this._params = params;
61
+ this._id = isNotification ? undefined : 0; // placeholder until .id()
62
+ this._forceNoId = isNotification;
63
+ }
64
+
65
+ /** Attach/override params. */
66
+ params(obj) {
67
+ this._params = obj ?? undefined;
68
+ return this;
69
+ }
70
+
71
+ /** Set the JSON-RPC id (number|string). If null/undefined, remove id (notification). */
72
+ id(id) {
73
+ this._id = id ?? undefined;
74
+ this._forceNoId = id == null;
75
+ return this;
76
+ }
77
+
78
+ /** Build a plain object suitable for JSON.stringify and sending. */
79
+ build() {
80
+ /** @type {any} */
81
+ const msg = { jsonrpc: this._jsonrpc, method: this._method };
82
+ if (this._params !== undefined) msg.params = this._params;
83
+ if (!this._forceNoId) {
84
+ msg.id = this._id === 0 ? IdFactory.default().next() : this._id;
85
+ }
86
+ return msg;
87
+ }
88
+
89
+ /** Convenience: JSON string. */
90
+ toString() {
91
+ return JSON.stringify(this.build());
92
+ }
93
+ }
94
+
95
+ /** Monotonic id generator; stable across builders if you reuse the instance. */
96
+ export class IdFactory {
97
+ constructor(prefix = '') {
98
+ this.prefix = prefix;
99
+ this.counter = 1;
100
+ }
101
+ next() {
102
+ const n = this.counter++;
103
+ return this.prefix ? `${this.prefix}${n}` : n;
104
+ }
105
+ static default() {
106
+ if (!globalThis.__brokerIdFactory)
107
+ globalThis.__brokerIdFactory = new IdFactory();
108
+ return globalThis.__brokerIdFactory;
109
+ }
110
+ }
111
+
112
+ /** Generate a correlation id suitable for broker requestId fields. */
113
+ export function newRequestId() {
114
+ return randomUUID();
115
+ }
116
+
117
+ /**
118
+ * Convert a Node KeyObject public key (Ed25519) to base64 DER (SPKI) string.
119
+ * @param {KeyObject} pubKey
120
+ */
121
+ export function toBase64Der(pubKey) {
122
+ return pubKey.export({ format: 'der', type: 'spki' }).toString('base64');
123
+ }
124
+
125
+ /**
126
+ * Build signature for authenticate. If signatureBase64 is given, returns it as-is.
127
+ * Otherwise, signs the nonce with the provided privateKey using Ed25519.
128
+ *
129
+ * @param {object} opts
130
+ * @param {string} [opts.signatureBase64]
131
+ * @param {KeyObject} [opts.privateKey]
132
+ * @param {string} opts.nonce
133
+ */
134
+ function ensureSignature({ signatureBase64, privateKey, nonce }) {
135
+ if (signatureBase64) return signatureBase64;
136
+ if (!privateKey)
137
+ throw new Error('Either signatureBase64 or privateKey must be provided');
138
+ return crypto
139
+ .sign(null, Buffer.from(nonce, 'utf8'), privateKey)
140
+ .toString('base64');
141
+ }
142
+
143
+ /** Namespace: Broker protocol message builders (outbound). */
144
+ export const Broker = {
145
+ /**
146
+ * hello — specify the role explicitly.
147
+ * @param {object} opts
148
+ * @param {'agent'|'ability'|'observer'} opts.role
149
+ * @param {string} [opts.version='0.1']
150
+ */
151
+ hello({ role, version = '0.1' } = {}) {
152
+ if (!role)
153
+ throw new Error('role is required ("agent" | "ability" | "observer")');
154
+ return new JsonRpcBuilder(methodNames.hello, { role, version });
155
+ },
156
+
157
+ /**
158
+ * authenticate
159
+ * @param {object} opts
160
+ * @param {string} opts.publicKeyBase64Der - base64 DER (SPKI) public key
161
+ * @param {string} opts.nonce
162
+ * @param {boolean} [opts.wantNewId=true]
163
+ * @param {KeyObject} [opts.privateKey] - used to compute signature if signatureBase64 not given
164
+ * @param {string} [opts.signatureBase64] - if you prefer to sign externally
165
+ */
166
+ authenticate({
167
+ publicKeyBase64Der,
168
+ nonce,
169
+ wantNewId = true,
170
+ privateKey,
171
+ signatureBase64
172
+ }) {
173
+ const signature = ensureSignature({ signatureBase64, privateKey, nonce });
174
+ const params = {
175
+ publicKey: publicKeyBase64Der,
176
+ signature,
177
+ nonce,
178
+ wantNewId
179
+ };
180
+ return new JsonRpcBuilder(methodNames.authenticate, params);
181
+ },
182
+
183
+ /**
184
+ * registerCapabilities
185
+ * @param {object} opts
186
+ * @param {string} opts.displayName
187
+ * @param {('volatile'|'persistent')} [opts.mailboxMode='persistent']
188
+ * @param {Array<object>} [opts.tools=[]] - [{ name, description, inputSchema, outputSchema?, scopes? }]
189
+ * @param {string|string[]} [opts.scopes=['global']]
190
+ * @param {string} [opts.parentAgentId]
191
+ */
192
+ registerCapabilities({
193
+ displayName,
194
+ mailboxMode = 'persistent',
195
+ tools = [],
196
+ scopes = ['global'],
197
+ parentAgentId
198
+ } = {}) {
199
+ if (!displayName) throw new Error('displayName is required');
200
+ /** @type {any} */
201
+ const params = { displayName, mailboxMode, tools, scopes };
202
+ if (parentAgentId !== undefined) params.parentAgentId = parentAgentId;
203
+ return new JsonRpcBuilder(methodNames.registerCapabilities, params);
204
+ },
205
+
206
+ /** ping (notification) */
207
+ ping() {
208
+ return new JsonRpcBuilder(
209
+ methodNames.ping,
210
+ undefined,
211
+ /* notification */ true
212
+ );
213
+ },
214
+
215
+ /**
216
+ * listTools
217
+ * @param {object} [opts]
218
+ * @param {string|string[]} [opts.scopes=['global']]
219
+ */
220
+ listTools({ scopes = ['global'] } = {}) {
221
+ return new JsonRpcBuilder(methodNames.listTools, { scopes });
222
+ },
223
+
224
+ /**
225
+ * callAbility
226
+ * @param {object} opts
227
+ * @param {string} opts.toolName
228
+ * @param {object} [opts.args]
229
+ * @param {string} [opts.requestId] - correlation ID; generated if omitted
230
+ */
231
+ callAbility({ toolName, args = {}, requestId = undefined }) {
232
+ if (!toolName) throw new Error('toolName is required');
233
+ const rid = requestId ?? newRequestId();
234
+ return new JsonRpcBuilder(methodNames.callAbility, {
235
+ toolName,
236
+ args,
237
+ requestId: rid
238
+ });
239
+ },
240
+
241
+ /**
242
+ * abilityResult (notification)
243
+ * @param {object} opts
244
+ * @param {string} opts.requestId
245
+ * @param {string} opts.toSessionId
246
+ * @param {any} [opts.result]
247
+ * @param {string|null} [opts.error]
248
+ */
249
+ abilityResult({ requestId, toSessionId, result = undefined, error = null }) {
250
+ if (!requestId) throw new Error('requestId is required');
251
+ if (!toSessionId) throw new Error('toSessionId is required');
252
+ const params = { requestId, toSessionId };
253
+ if (result !== undefined) params.result = result;
254
+ if (error !== null) params.error = error;
255
+ return new JsonRpcBuilder(
256
+ methodNames.abilityResult,
257
+ params,
258
+ /* notification */ true
259
+ );
260
+ },
261
+
262
+ /**
263
+ * Build an agent ability definition object.
264
+ * Kept shape compatible with current broker "tools" array.
265
+ * @param {object} opts
266
+ * @param {string} opts.name
267
+ * @param {string} opts.description
268
+ * @param {object} opts.inputSchema
269
+ * @param {object} [opts.outputSchema]
270
+ * @param {string[]} [opts.scopes]
271
+ */
272
+ agentAbility({ name, description, inputSchema, outputSchema, scopes }) {
273
+ if (!name) throw new Error('ability.name is required');
274
+ if (!description) throw new Error('ability.description is required');
275
+ if (!inputSchema) throw new Error('ability.inputSchema is required');
276
+ /** @type {any} */
277
+ const def = { name, description, inputSchema };
278
+ if (outputSchema) def.outputSchema = outputSchema;
279
+ if (scopes) def.scopes = scopes;
280
+ return def;
281
+ },
282
+
283
+ /** Short alias. */
284
+ ability(opts) {
285
+ return this.agentAbility(opts);
286
+ }
287
+ };
288
+
289
+ /** Namespace: MCP bridge message builders (subset as per README). */
290
+ export const MCP = {
291
+ initialize({
292
+ protocolVersion = '2025-06-18',
293
+ capabilities = {},
294
+ clientInfo = { name: 'mcp-client', version: '1.0.0' }
295
+ } = {}) {
296
+ const params = {
297
+ protocolVersion,
298
+ capabilities: {
299
+ resources: { subscribe: false, ...capabilities.resources },
300
+ tools: true,
301
+ prompts: { subscribe: false, ...capabilities.prompts },
302
+ sampling: false,
303
+ ...capabilities
304
+ },
305
+ clientInfo
306
+ };
307
+ return new JsonRpcBuilder('initialize', params);
308
+ },
309
+ initialized() {
310
+ return new JsonRpcBuilder('initialized', undefined, true);
311
+ },
312
+ toolsList() {
313
+ return new JsonRpcBuilder('tools/list', {});
314
+ },
315
+ toolsCall({ name, arguments: args = {} }) {
316
+ if (!name) throw new Error('name is required');
317
+ return new JsonRpcBuilder('tools/call', { name, arguments: args });
318
+ }
319
+ };
320
+
321
+ /** Helpers for common agent handshake flows */
322
+ export const Flows = {
323
+ /**
324
+ * Build all handshake messages for an agent given the broker's hello result and your keys.
325
+ * @param {object} opts
326
+ * @param {KeyObject} opts.publicKey
327
+ * @param {KeyObject} opts.privateKey
328
+ * @param {string} opts.nonce - From hello result
329
+ * @param {string} opts.displayName
330
+ * @param {Array<object>} opts.tools
331
+ * @param {('volatile'|'persistent')} [opts.mailboxMode]
332
+ * @param {string|string[]} [opts.scopes=['global']]
333
+ */
334
+ completeAgentHandshake({
335
+ publicKey,
336
+ privateKey,
337
+ nonce,
338
+ displayName,
339
+ tools,
340
+ mailboxMode = 'volatile',
341
+ scopes = ['global']
342
+ }) {
343
+ const ids = new IdFactory(); // fresh id sequence for this flow
344
+ const hello = Broker.hello({ role: 'agent' }).id(ids.next()).build();
345
+ const auth = Broker.authenticate({
346
+ publicKeyBase64Der: toBase64Der(publicKey),
347
+ privateKey,
348
+ nonce,
349
+ wantNewId: true
350
+ })
351
+ .id(ids.next())
352
+ .build();
353
+ const reg = Broker.registerCapabilities({
354
+ displayName,
355
+ tools,
356
+ mailboxMode,
357
+ scopes
358
+ })
359
+ .id(ids.next())
360
+ .build();
361
+ return { hello, authenticate: auth, register: reg };
362
+ }
363
+ };
364
+
365
+ const BrokerMessageBuilder = {
366
+ JsonRpcBuilder,
367
+ IdFactory,
368
+ newRequestId,
369
+ toBase64Der,
370
+ Broker,
371
+ MCP,
372
+ Flows,
373
+ configureMethods,
374
+ _methodNames: methodNames // exposed for debugging
375
+ };
376
+
377
+ export default BrokerMessageBuilder;