@kadi.build/core 0.7.2 → 0.8.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/README.md +53 -0
- package/dist/client.d.ts +14 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +89 -127
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +49 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +109 -0
- package/dist/protocol.js.map +1 -0
- package/dist/transports/stdio.d.ts.map +1 -1
- package/dist/transports/stdio.js +2 -6
- package/dist/transports/stdio.js.map +1 -1
- package/dist/types.d.ts +57 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +112 -132
- package/src/index.ts +7 -1
- package/src/protocol.ts +161 -0
- package/src/transports/stdio.ts +2 -6
- package/src/types.ts +60 -18
package/README.md
CHANGED
|
@@ -107,6 +107,59 @@ client.registerTool({
|
|
|
107
107
|
client.registerTool(definition, handler, { brokers: ['internal'] });
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Targeting Specific Networks
|
|
111
|
+
|
|
112
|
+
By default, a tool is visible on **all networks** the agent has joined. You can restrict a tool to specific networks using the `networks` option — the tool will only be discoverable and invocable by agents on those networks.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const client = new KadiClient({
|
|
116
|
+
name: 'multi-network-agent',
|
|
117
|
+
brokers: { default: 'ws://localhost:8080/kadi' },
|
|
118
|
+
networks: ['global', 'internal', 'gpu-cluster'],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Visible on ALL agent networks (global, internal, gpu-cluster)
|
|
122
|
+
client.registerTool({
|
|
123
|
+
name: 'echo',
|
|
124
|
+
description: 'Echo back the input',
|
|
125
|
+
input: z.object({ message: z.string() }),
|
|
126
|
+
}, async ({ message }) => ({ echo: message }));
|
|
127
|
+
|
|
128
|
+
// Visible ONLY on gpu-cluster
|
|
129
|
+
client.registerTool({
|
|
130
|
+
name: 'gpu-inference',
|
|
131
|
+
description: 'Run ML inference on GPU',
|
|
132
|
+
input: z.object({ model: z.string(), prompt: z.string() }),
|
|
133
|
+
}, async ({ model, prompt }) => {
|
|
134
|
+
return { result: await runInference(model, prompt) };
|
|
135
|
+
}, { networks: ['gpu-cluster'] });
|
|
136
|
+
|
|
137
|
+
// Visible on internal AND global only (not gpu-cluster)
|
|
138
|
+
client.registerTool({
|
|
139
|
+
name: 'audit-log',
|
|
140
|
+
description: 'Retrieve audit logs',
|
|
141
|
+
input: z.object({ limit: z.number() }),
|
|
142
|
+
}, async ({ limit }) => {
|
|
143
|
+
return { logs: await getAuditLogs(limit) };
|
|
144
|
+
}, { networks: ['internal', 'global'] });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Per-tool networks must be a **subset** of the client's `networks`. Attempting to scope a tool to a network the agent hasn't joined throws an `INVALID_CONFIG` error:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Throws KadiError — 'finance' is not in client networks
|
|
151
|
+
client.registerTool(definition, handler, { networks: ['finance'] });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
You can combine `brokers` and `networks` to control both which brokers a tool registers with and which networks it's visible on:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
client.registerTool(definition, handler, {
|
|
158
|
+
brokers: ['production'],
|
|
159
|
+
networks: ['internal'],
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
110
163
|
---
|
|
111
164
|
|
|
112
165
|
## Connecting to Brokers
|
package/dist/client.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - Readable, traceable code flow
|
|
16
16
|
*/
|
|
17
17
|
import { type EncryptionKeyPair } from './crypto.js';
|
|
18
|
-
import type { ClientConfig, ClientIdentity, BrokerState,
|
|
18
|
+
import type { ClientConfig, ClientIdentity, BrokerState, BrokerToolDefinition, ZodToolDefinition, ToolHandler, LoadedAbility, ToolExecutionBridge, RegisterToolOptions, LoadNativeOptions, LoadStdioOptions, LoadBrokerOptions, InvokeRemoteOptions, BrokerEventHandler, PublishOptions, SubscribeOptions, EmitOptions } from './types.js';
|
|
19
19
|
/**
|
|
20
20
|
* The main client for building KADI agents.
|
|
21
21
|
*
|
|
@@ -25,8 +25,8 @@ import type { ClientConfig, ClientIdentity, BrokerState, ToolDefinition, ZodTool
|
|
|
25
25
|
* const client = new KadiClient({
|
|
26
26
|
* name: 'my-agent',
|
|
27
27
|
* brokers: {
|
|
28
|
-
* production: 'ws://broker-prod:8080',
|
|
29
|
-
* internal: 'ws://broker-internal:8080',
|
|
28
|
+
* production: { url: 'ws://broker-prod:8080/kadi' },
|
|
29
|
+
* internal: { url: 'ws://broker-internal:8080/kadi', networks: ['private'] },
|
|
30
30
|
* },
|
|
31
31
|
* defaultBroker: 'production',
|
|
32
32
|
* });
|
|
@@ -404,6 +404,11 @@ export declare class KadiClient {
|
|
|
404
404
|
* Handle response to a pending request.
|
|
405
405
|
*/
|
|
406
406
|
private handleBrokerResponse;
|
|
407
|
+
/**
|
|
408
|
+
* Finalize a broker connection: start heartbeat and mark as connected.
|
|
409
|
+
* Shared by initial connect and reconnect paths.
|
|
410
|
+
*/
|
|
411
|
+
private finalizeConnection;
|
|
407
412
|
/**
|
|
408
413
|
* Send heartbeat to keep connection alive.
|
|
409
414
|
*/
|
|
@@ -526,6 +531,11 @@ export declare class KadiClient {
|
|
|
526
531
|
* ```
|
|
527
532
|
*/
|
|
528
533
|
registerTool<TInput, TOutput>(definition: ZodToolDefinition<TInput, TOutput>, handler: ToolHandler<TInput, TOutput>, options?: RegisterToolOptions): void;
|
|
534
|
+
/**
|
|
535
|
+
* Get the union of all networks configured across all brokers.
|
|
536
|
+
* Used by registerTool() to validate per-tool network targeting.
|
|
537
|
+
*/
|
|
538
|
+
private getAllConfiguredNetworks;
|
|
529
539
|
/**
|
|
530
540
|
* Get tool definitions, optionally filtered for a specific broker.
|
|
531
541
|
*
|
|
@@ -697,7 +707,7 @@ export declare class KadiClient {
|
|
|
697
707
|
readAgentJson(): {
|
|
698
708
|
name: string;
|
|
699
709
|
version: string;
|
|
700
|
-
tools:
|
|
710
|
+
tools: BrokerToolDefinition[];
|
|
701
711
|
};
|
|
702
712
|
/**
|
|
703
713
|
* Get broker connection state (for broker transport).
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EACV,YAAY,EAEZ,cAAc,EACd,WAAW,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EACV,YAAY,EAEZ,cAAc,EACd,WAAW,EAGX,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EAOnB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,WAAW,EACZ,MAAM,YAAY,CAAC;AAkGpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,UAAU;IACrB,mDAAmD;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IAExC,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAEhE,iCAAiC;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAE/D,gDAAgD;IAChD,OAAO,CAAC,aAAa,CAAK;IAE1B,uDAAuD;IACvD,OAAO,CAAC,YAAY,CAAyD;IAE7E,8DAA8D;IAC9D,OAAO,CAAC,cAAc,CAAS;IAE/B,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyC;IAEzE,kCAAkC;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkC;IAMlE,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,uEAAuE;IACvE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAMtB,MAAM,EAAE,YAAY;IAgEhC;;;;;;;;;;;;OAYG;IACH,IAAI,QAAQ,IAAI,cAAc,CAK7B;IAED;;;;OAIG;IACH,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED;;;;;OAKG;IACH,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAI,OAAO,IAAI;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAKvD;IAMD;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,mBAAmB,IAAI,UAAU,CAEpC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,IAAI,iBAAiB,IAAI,iBAAiB,CAEzC;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,MAAM,CAEpB;IAMD;;;;;;;;;OASG;IACG,OAAO,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8CjD;;;;;;;;;;;;;;OAcG;YACW,eAAe;IAyD7B,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,gBAAgB;IAiBxB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,qBAAqB;IAQ7B;;OAEG;IACH,OAAO,CAAC,aAAa;IAsDrB;;;;;;;;OAQG;YACW,gBAAgB;IAwC9B;;;;;;OAMG;YACW,kBAAkB;IAoBhC;;;;OAIG;IACH,OAAO,CAAC,WAAW;IA4CnB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAmC3B;;;;;;;OAOG;IACH,OAAO,CAAC,qBAAqB;IAsB7B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAoB9B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAsB/B;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IA0C3B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,qBAAqB;IAM7B;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAiC7B;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,SAAS,CACb,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC;IAkBhB;;;;;;;;;;;;;;;;;OAiBG;IACG,WAAW,CACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC;IAsChB;;;;;;;;;;;;;;;;;;OAkBG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAc1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACG,cAAc,CAAC,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IA2MhG;;OAEG;YACW,mBAAmB;IA+BjC;;;;;;;;;;;OAWG;YACW,mBAAmB;IAuCjC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAsB5B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;OAEG;IACH,OAAO,CAAC,aAAa;IAOrB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAyCrB;;;;;;;OAOG;IACH,OAAO,CAAC,oBAAoB;IAuC5B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,iBAAiB;IAezB;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB;IAgBzB;;;;;;;OAOG;YACW,gBAAgB;IAuC9B;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAI7C;;;;;OAKG;IACG,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BpD;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;;;OAIG;IACH,eAAe,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAItE;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI;IA4B/D;;;;;;;;;;;;;;;;;;;OAmBG;IACH,YAAY,CAAC,MAAM,EAAE,OAAO,EAC1B,UAAU,EAAE,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9C,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,OAAO,GAAE,mBAAwB,GAChC,IAAI;IA+CP;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAQhC;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAkB1B;;;;;;;;;;;;;;;;;OAiBG;YACW,kBAAkB;IAkBhC;;;;;;;OAOG;IACH,gBAAgB,IAAI,mBAAmB;IAavC;;;;;;;;;;;;;;OAcG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC;IAoBvF;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC;IA8BrF;;;;;;;;;;;;;;OAcG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC;IAmBvF;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,YAAY,CAAC,CAAC,GAAG,OAAO,EAC5B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,CAAC,CAAC;IA8Eb;;;;;;;;;;;;;OAaG;IACG,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAQpD;;;;;;;;OAQG;YACW,UAAU;IAiFxB;;OAEG;YACW,kBAAkB;IAgChC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAKzB;;OAEG;IACH,OAAO,CAAC,cAAc;IAKtB;;;;;OAKG;YACW,WAAW;IAsBzB;;OAEG;IACH,aAAa,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,oBAAoB,EAAE,CAAA;KAAE;IAQjF;;OAEG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAI3D;;;;OAIG;IACH,WAAW,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO;IAazC;;OAEG;IACH,mBAAmB,IAAI,MAAM,EAAE;CAKhC"}
|
package/dist/client.js
CHANGED
|
@@ -20,6 +20,7 @@ import { convertToEncryptionKey, convertToEncryptionKeyPair, } from './crypto.js
|
|
|
20
20
|
// @ts-expect-error - tweetnacl-sealedbox-js doesn't have type definitions
|
|
21
21
|
import sealedbox from 'tweetnacl-sealedbox-js';
|
|
22
22
|
import { KadiError } from './errors.js';
|
|
23
|
+
import * as protocol from './protocol.js';
|
|
23
24
|
import { zodToJsonSchema, isZodSchema } from './zod.js';
|
|
24
25
|
import { resolveAbilityEntry, resolveAbilityScript } from './lockfile.js';
|
|
25
26
|
import { loadNativeTransport } from './transports/native.js';
|
|
@@ -112,8 +113,8 @@ function isMcpCallToolResult(result) {
|
|
|
112
113
|
* const client = new KadiClient({
|
|
113
114
|
* name: 'my-agent',
|
|
114
115
|
* brokers: {
|
|
115
|
-
* production: 'ws://broker-prod:8080',
|
|
116
|
-
* internal: 'ws://broker-internal:8080',
|
|
116
|
+
* production: { url: 'ws://broker-prod:8080/kadi' },
|
|
117
|
+
* internal: { url: 'ws://broker-internal:8080/kadi', networks: ['private'] },
|
|
117
118
|
* },
|
|
118
119
|
* defaultBroker: 'production',
|
|
119
120
|
* });
|
|
@@ -182,16 +183,21 @@ export class KadiClient {
|
|
|
182
183
|
// Derive agentId using same algorithm as broker: SHA256(publicKey).hex().substring(0, 16)
|
|
183
184
|
this._agentId = crypto.createHash('sha256').update(this._publicKeyBase64).digest('hex').substring(0, 16);
|
|
184
185
|
// Resolve configuration with defaults
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
186
|
+
const rawBrokers = config.brokers ?? {};
|
|
187
|
+
const normalizedBrokers = {};
|
|
188
|
+
for (const [name, entry] of Object.entries(rawBrokers)) {
|
|
189
|
+
normalizedBrokers[name] = {
|
|
190
|
+
url: entry.url,
|
|
191
|
+
networks: entry.networks ?? ['global'],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const firstBrokerName = Object.keys(normalizedBrokers)[0];
|
|
188
195
|
this.config = {
|
|
189
196
|
name: config.name,
|
|
190
197
|
version: config.version ?? '1.0.0',
|
|
191
198
|
description: config.description ?? '',
|
|
192
|
-
brokers,
|
|
199
|
+
brokers: normalizedBrokers,
|
|
193
200
|
defaultBroker: config.defaultBroker ?? firstBrokerName,
|
|
194
|
-
networks: config.networks ?? ['global'],
|
|
195
201
|
heartbeatInterval: config.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
|
|
196
202
|
requestTimeout: config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT,
|
|
197
203
|
autoReconnect: config.autoReconnect ?? DEFAULT_AUTO_RECONNECT,
|
|
@@ -364,8 +370,6 @@ export class KadiClient {
|
|
|
364
370
|
for (const [i, result] of results.entries()) {
|
|
365
371
|
if (result.status === 'rejected') {
|
|
366
372
|
const failedBroker = brokerNames[i];
|
|
367
|
-
if (failedBroker === undefined)
|
|
368
|
-
continue;
|
|
369
373
|
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
370
374
|
console.error(`[KADI] Failed to connect to broker "${failedBroker}": ${reason}`);
|
|
371
375
|
failures.push(failedBroker);
|
|
@@ -397,14 +401,15 @@ export class KadiClient {
|
|
|
397
401
|
* across all broker connections, ensuring consistent identity.
|
|
398
402
|
*/
|
|
399
403
|
async connectToBroker(brokerName) {
|
|
400
|
-
const
|
|
401
|
-
if (!
|
|
404
|
+
const brokerConfig = this.config.brokers[brokerName];
|
|
405
|
+
if (!brokerConfig) {
|
|
402
406
|
throw new KadiError(`Broker "${brokerName}" not found in configuration`, 'UNKNOWN_BROKER', {
|
|
403
407
|
broker: brokerName,
|
|
404
408
|
available: Object.keys(this.config.brokers),
|
|
405
409
|
hint: 'Check your brokers configuration',
|
|
406
410
|
});
|
|
407
411
|
}
|
|
412
|
+
const url = brokerConfig.url;
|
|
408
413
|
// Check if already connected
|
|
409
414
|
if (this.brokers.has(brokerName)) {
|
|
410
415
|
const existing = this.brokers.get(brokerName);
|
|
@@ -416,6 +421,7 @@ export class KadiClient {
|
|
|
416
421
|
const state = {
|
|
417
422
|
name: brokerName,
|
|
418
423
|
url,
|
|
424
|
+
networks: brokerConfig.networks,
|
|
419
425
|
ws: null,
|
|
420
426
|
heartbeatTimer: null,
|
|
421
427
|
pendingRequests: new Map(),
|
|
@@ -436,22 +442,14 @@ export class KadiClient {
|
|
|
436
442
|
await this.performHandshake(state);
|
|
437
443
|
// Register with broker (transitions to "ready" state)
|
|
438
444
|
await this.registerWithBroker(state);
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
this.sendHeartbeat(state);
|
|
442
|
-
}, this.config.heartbeatInterval);
|
|
443
|
-
state.status = 'connected';
|
|
445
|
+
// Finalize connection (heartbeat + status)
|
|
446
|
+
this.finalizeConnection(state);
|
|
444
447
|
}
|
|
445
448
|
// ─────────────────────────────────────────────────────────────
|
|
446
449
|
// MESSAGE BUILDERS
|
|
447
450
|
// ─────────────────────────────────────────────────────────────
|
|
448
451
|
buildHelloMessage() {
|
|
449
|
-
return
|
|
450
|
-
jsonrpc: '2.0',
|
|
451
|
-
id: this.nextRequestId++,
|
|
452
|
-
method: 'kadi.session.hello',
|
|
453
|
-
params: { role: 'agent' },
|
|
454
|
-
};
|
|
452
|
+
return protocol.hello(this.nextRequestId++);
|
|
455
453
|
}
|
|
456
454
|
buildAuthMessage(nonce) {
|
|
457
455
|
// Sign the nonce using the client's private key
|
|
@@ -461,36 +459,13 @@ export class KadiClient {
|
|
|
461
459
|
type: 'pkcs8',
|
|
462
460
|
});
|
|
463
461
|
const signature = crypto.sign(null, Buffer.from(nonce, 'utf8'), privateKey);
|
|
464
|
-
return
|
|
465
|
-
jsonrpc: '2.0',
|
|
466
|
-
id: this.nextRequestId++,
|
|
467
|
-
method: 'kadi.session.authenticate',
|
|
468
|
-
params: {
|
|
469
|
-
publicKey: this._publicKeyBase64,
|
|
470
|
-
signature: signature.toString('base64'),
|
|
471
|
-
nonce,
|
|
472
|
-
},
|
|
473
|
-
};
|
|
462
|
+
return protocol.authenticate(this.nextRequestId++, this._publicKeyBase64, signature.toString('base64'), nonce);
|
|
474
463
|
}
|
|
475
464
|
buildRegisterMessage(tools, networks) {
|
|
476
|
-
return
|
|
477
|
-
jsonrpc: '2.0',
|
|
478
|
-
id: this.nextRequestId++,
|
|
479
|
-
method: 'kadi.agent.register',
|
|
480
|
-
params: {
|
|
481
|
-
tools,
|
|
482
|
-
networks,
|
|
483
|
-
displayName: this.config.name,
|
|
484
|
-
},
|
|
485
|
-
};
|
|
465
|
+
return protocol.register(this.nextRequestId++, tools, networks, this.config.name);
|
|
486
466
|
}
|
|
487
467
|
buildHeartbeatMessage() {
|
|
488
|
-
return
|
|
489
|
-
jsonrpc: '2.0',
|
|
490
|
-
id: this.nextRequestId++,
|
|
491
|
-
method: 'kadi.session.heartbeat',
|
|
492
|
-
params: { timestamp: Date.now() },
|
|
493
|
-
};
|
|
468
|
+
return protocol.heartbeat(this.nextRequestId++);
|
|
494
469
|
}
|
|
495
470
|
// ─────────────────────────────────────────────────────────────
|
|
496
471
|
// CONNECTION HELPERS
|
|
@@ -593,7 +568,11 @@ export class KadiClient {
|
|
|
593
568
|
// Note: If registration fails, sendRequest will reject with the error.
|
|
594
569
|
// We don't need to check response.error here - errors are already
|
|
595
570
|
// handled via Promise rejection in handleBrokerResponse.
|
|
596
|
-
await this.sendRequest(state, this.buildRegisterMessage(tools,
|
|
571
|
+
const { droppedNetworks } = await this.sendRequest(state, this.buildRegisterMessage(tools, state.networks));
|
|
572
|
+
if (droppedNetworks && droppedNetworks.length > 0) {
|
|
573
|
+
console.warn(`[KADI] Broker "${state.name}" dropped networks ${JSON.stringify(droppedNetworks)} ` +
|
|
574
|
+
`during registration. Tools scoped to those networks will not be discoverable.`);
|
|
575
|
+
}
|
|
597
576
|
}
|
|
598
577
|
/**
|
|
599
578
|
* Send a JSON-RPC request and wait for the response.
|
|
@@ -862,12 +841,7 @@ export class KadiClient {
|
|
|
862
841
|
handlers.add(handler);
|
|
863
842
|
// If this is the first handler for this pattern, subscribe on broker
|
|
864
843
|
if (!state.subscribedPatterns.has(pattern)) {
|
|
865
|
-
await this.sendRequest(state,
|
|
866
|
-
jsonrpc: '2.0',
|
|
867
|
-
id: this.nextRequestId++,
|
|
868
|
-
method: 'kadi.event.subscribe',
|
|
869
|
-
params: { pattern },
|
|
870
|
-
});
|
|
844
|
+
await this.sendRequest(state, protocol.eventSubscribe(this.nextRequestId++, pattern));
|
|
871
845
|
state.subscribedPatterns.add(pattern);
|
|
872
846
|
}
|
|
873
847
|
}
|
|
@@ -913,12 +887,7 @@ export class KadiClient {
|
|
|
913
887
|
// Unsubscribe from broker if we were subscribed
|
|
914
888
|
if (state.subscribedPatterns.has(pattern) && state.status === 'connected') {
|
|
915
889
|
try {
|
|
916
|
-
await this.sendRequest(state,
|
|
917
|
-
jsonrpc: '2.0',
|
|
918
|
-
id: this.nextRequestId++,
|
|
919
|
-
method: 'kadi.event.unsubscribe',
|
|
920
|
-
params: { pattern },
|
|
921
|
-
});
|
|
890
|
+
await this.sendRequest(state, protocol.eventUnsubscribe(this.nextRequestId++, pattern));
|
|
922
891
|
}
|
|
923
892
|
catch {
|
|
924
893
|
// Ignore errors during unsubscribe (broker might be disconnecting)
|
|
@@ -949,19 +918,10 @@ export class KadiClient {
|
|
|
949
918
|
*/
|
|
950
919
|
async publish(channel, data, options = {}) {
|
|
951
920
|
const { state } = this.getConnectedBrokerState(options.broker);
|
|
952
|
-
// Resolve which network to publish to
|
|
953
|
-
const networkId = options.network ??
|
|
921
|
+
// Resolve which network to publish to (prefer broker-specific, fallback to global)
|
|
922
|
+
const networkId = options.network ?? state.networks[0] ?? '';
|
|
954
923
|
// Send publish request to broker
|
|
955
|
-
await this.sendRequest(state,
|
|
956
|
-
jsonrpc: '2.0',
|
|
957
|
-
id: this.nextRequestId++,
|
|
958
|
-
method: 'kadi.event.publish',
|
|
959
|
-
params: {
|
|
960
|
-
channel,
|
|
961
|
-
data,
|
|
962
|
-
networkId,
|
|
963
|
-
},
|
|
964
|
-
});
|
|
924
|
+
await this.sendRequest(state, protocol.eventPublish(this.nextRequestId++, channel, data, networkId));
|
|
965
925
|
}
|
|
966
926
|
// ─────────────────────────────────────────────────────────────
|
|
967
927
|
// DEPLOYMENT SECRETS (Trusted Introducer Pattern)
|
|
@@ -1191,35 +1151,18 @@ export class KadiClient {
|
|
|
1191
1151
|
// KADI clients receive raw structured data
|
|
1192
1152
|
responseResult = result;
|
|
1193
1153
|
}
|
|
1194
|
-
|
|
1195
|
-
jsonrpc: '2.0',
|
|
1196
|
-
id: requestId,
|
|
1197
|
-
result: responseResult,
|
|
1198
|
-
};
|
|
1199
|
-
state.ws?.send(JSON.stringify(response));
|
|
1154
|
+
state.ws?.send(JSON.stringify(protocol.resultResponse(requestId, responseResult)));
|
|
1200
1155
|
}
|
|
1201
1156
|
catch (error) {
|
|
1202
1157
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1203
1158
|
// For MCP clients, wrap errors in CallToolResult format too
|
|
1204
1159
|
if (context.callerProtocol === 'mcp') {
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
id: requestId,
|
|
1208
|
-
result: { content: [{ type: 'text', text: errorMessage }], isError: true },
|
|
1209
|
-
};
|
|
1210
|
-
state.ws?.send(JSON.stringify(response));
|
|
1160
|
+
const mcpResult = { content: [{ type: 'text', text: errorMessage }], isError: true };
|
|
1161
|
+
state.ws?.send(JSON.stringify(protocol.resultResponse(requestId, mcpResult)));
|
|
1211
1162
|
}
|
|
1212
1163
|
else {
|
|
1213
1164
|
// KADI clients receive JSON-RPC error
|
|
1214
|
-
|
|
1215
|
-
jsonrpc: '2.0',
|
|
1216
|
-
id: requestId,
|
|
1217
|
-
error: {
|
|
1218
|
-
code: -32000,
|
|
1219
|
-
message: errorMessage,
|
|
1220
|
-
},
|
|
1221
|
-
};
|
|
1222
|
-
state.ws?.send(JSON.stringify(response));
|
|
1165
|
+
state.ws?.send(JSON.stringify(protocol.errorResponse(requestId, -32000, errorMessage)));
|
|
1223
1166
|
}
|
|
1224
1167
|
}
|
|
1225
1168
|
}
|
|
@@ -1244,6 +1187,16 @@ export class KadiClient {
|
|
|
1244
1187
|
pending.resolve(response.result);
|
|
1245
1188
|
}
|
|
1246
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Finalize a broker connection: start heartbeat and mark as connected.
|
|
1192
|
+
* Shared by initial connect and reconnect paths.
|
|
1193
|
+
*/
|
|
1194
|
+
finalizeConnection(state) {
|
|
1195
|
+
state.heartbeatTimer = setInterval(() => {
|
|
1196
|
+
this.sendHeartbeat(state);
|
|
1197
|
+
}, this.config.heartbeatInterval);
|
|
1198
|
+
state.status = 'connected';
|
|
1199
|
+
}
|
|
1247
1200
|
/**
|
|
1248
1201
|
* Send heartbeat to keep connection alive.
|
|
1249
1202
|
*/
|
|
@@ -1283,6 +1236,9 @@ export class KadiClient {
|
|
|
1283
1236
|
}));
|
|
1284
1237
|
}
|
|
1285
1238
|
state.pendingInvocations.clear();
|
|
1239
|
+
// Clear broker-side subscription tracking — subscriptions are lost when
|
|
1240
|
+
// the connection drops and must be re-established after reconnection.
|
|
1241
|
+
state.subscribedPatterns.clear();
|
|
1286
1242
|
}
|
|
1287
1243
|
// ─────────────────────────────────────────────────────────────
|
|
1288
1244
|
// RECONNECTION LOGIC
|
|
@@ -1395,14 +1351,11 @@ export class KadiClient {
|
|
|
1395
1351
|
await this.performHandshake(state);
|
|
1396
1352
|
// Re-register tools
|
|
1397
1353
|
await this.registerWithBroker(state);
|
|
1398
|
-
//
|
|
1399
|
-
|
|
1400
|
-
this.sendHeartbeat(state);
|
|
1401
|
-
}, this.config.heartbeatInterval);
|
|
1354
|
+
// Finalize connection (heartbeat + status)
|
|
1355
|
+
this.finalizeConnection(state);
|
|
1402
1356
|
// Success!
|
|
1403
1357
|
console.error(`[KADI] Reconnected to broker "${state.name}" after ${state.reconnectAttempts} attempts`);
|
|
1404
1358
|
state.reconnectAttempts = 0;
|
|
1405
|
-
state.status = 'connected';
|
|
1406
1359
|
}
|
|
1407
1360
|
catch (error) {
|
|
1408
1361
|
// Log the error and try again
|
|
@@ -1509,12 +1462,7 @@ export class KadiClient {
|
|
|
1509
1462
|
}
|
|
1510
1463
|
else if (this.isServingStdio) {
|
|
1511
1464
|
// Stdio transport: write notification to stdout
|
|
1512
|
-
const
|
|
1513
|
-
jsonrpc: '2.0',
|
|
1514
|
-
method: 'event',
|
|
1515
|
-
params: { name: event, data },
|
|
1516
|
-
};
|
|
1517
|
-
const json = JSON.stringify(notification);
|
|
1465
|
+
const json = JSON.stringify(protocol.eventNotification(event, data));
|
|
1518
1466
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
1519
1467
|
}
|
|
1520
1468
|
// Broadcast: also publish to broker (if specified and connected)
|
|
@@ -1569,15 +1517,40 @@ export class KadiClient {
|
|
|
1569
1517
|
? zodToJsonSchema(definition.output)
|
|
1570
1518
|
: undefined,
|
|
1571
1519
|
};
|
|
1520
|
+
// Validate per-tool networks against broker networks (skip when no brokers)
|
|
1521
|
+
const targetNetworks = options.networks ?? [];
|
|
1522
|
+
if (targetNetworks.length > 0 && Object.keys(this.config.brokers).length > 0) {
|
|
1523
|
+
const allNetworks = this.getAllConfiguredNetworks();
|
|
1524
|
+
const invalid = targetNetworks.filter((n) => !allNetworks.includes(n));
|
|
1525
|
+
if (invalid.length > 0) {
|
|
1526
|
+
throw new KadiError(`Tool "${definition.name}" has networks ${JSON.stringify(invalid)} not present in client networks ${JSON.stringify(allNetworks)}. Per-tool networks must be a subset of the client's networks — a tool can only be visible on networks the agent has joined.`, 'INVALID_CONFIG', {
|
|
1527
|
+
toolName: definition.name,
|
|
1528
|
+
invalidNetworks: invalid,
|
|
1529
|
+
clientNetworks: allNetworks,
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1572
1533
|
// Store registration
|
|
1573
1534
|
const registered = {
|
|
1574
1535
|
definition: jsonDefinition,
|
|
1575
1536
|
handler: handler,
|
|
1576
|
-
registeredAt: new Date(),
|
|
1577
1537
|
targetBrokers: options.brokers ?? [],
|
|
1538
|
+
targetNetworks,
|
|
1578
1539
|
};
|
|
1579
1540
|
this.tools.set(definition.name, registered);
|
|
1580
1541
|
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Get the union of all networks configured across all brokers.
|
|
1544
|
+
* Used by registerTool() to validate per-tool network targeting.
|
|
1545
|
+
*/
|
|
1546
|
+
getAllConfiguredNetworks() {
|
|
1547
|
+
const all = new Set();
|
|
1548
|
+
for (const broker of Object.values(this.config.brokers)) {
|
|
1549
|
+
for (const n of broker.networks)
|
|
1550
|
+
all.add(n);
|
|
1551
|
+
}
|
|
1552
|
+
return [...all];
|
|
1553
|
+
}
|
|
1581
1554
|
/**
|
|
1582
1555
|
* Get tool definitions, optionally filtered for a specific broker.
|
|
1583
1556
|
*
|
|
@@ -1596,7 +1569,10 @@ export class KadiClient {
|
|
|
1596
1569
|
// Otherwise, only include if this broker is in the target list
|
|
1597
1570
|
return t.targetBrokers.includes(forBroker);
|
|
1598
1571
|
})
|
|
1599
|
-
.map((t) =>
|
|
1572
|
+
.map((t) => ({
|
|
1573
|
+
...t.definition,
|
|
1574
|
+
...(t.targetNetworks.length > 0 && { networks: t.targetNetworks }),
|
|
1575
|
+
}));
|
|
1600
1576
|
}
|
|
1601
1577
|
/**
|
|
1602
1578
|
* Execute a tool registered on this client.
|
|
@@ -1752,7 +1728,7 @@ export class KadiClient {
|
|
|
1752
1728
|
const ability = await loadBrokerTransport(name, {
|
|
1753
1729
|
broker: state,
|
|
1754
1730
|
requestTimeout: options.timeout ?? this.config.requestTimeout,
|
|
1755
|
-
networks: options.networks,
|
|
1731
|
+
networks: options.networks ?? state.networks,
|
|
1756
1732
|
// Provide subscribe/unsubscribe for ability.on()/off() support
|
|
1757
1733
|
subscribe: (pattern, handler) => this.subscribe(pattern, handler, { broker: brokerName }),
|
|
1758
1734
|
unsubscribe: (pattern, handler) => this.unsubscribe(pattern, handler, { broker: brokerName }),
|
|
@@ -1831,13 +1807,9 @@ export class KadiClient {
|
|
|
1831
1807
|
};
|
|
1832
1808
|
// NOW send the request (listener already exists, no race possible)
|
|
1833
1809
|
try {
|
|
1834
|
-
const pendingResult = await this.sendRequest(state,
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
method: 'kadi.ability.request',
|
|
1838
|
-
// timeout sent so the broker can enforce a matching server-side deadline
|
|
1839
|
-
params: { toolName, toolInput: params, requestId, timeout },
|
|
1840
|
-
});
|
|
1810
|
+
const pendingResult = await this.sendRequest(state,
|
|
1811
|
+
// timeout sent so the broker can enforce a matching server-side deadline
|
|
1812
|
+
protocol.abilityRequest(++this.nextRequestId, toolName, params, requestId, timeout));
|
|
1841
1813
|
// Validate the broker accepted the request
|
|
1842
1814
|
if (pendingResult.status !== 'pending') {
|
|
1843
1815
|
cleanupPendingInvocation();
|
|
@@ -1990,24 +1962,14 @@ export class KadiClient {
|
|
|
1990
1962
|
* Send a response via stdio.
|
|
1991
1963
|
*/
|
|
1992
1964
|
sendStdioResponse(id, result) {
|
|
1993
|
-
const
|
|
1994
|
-
jsonrpc: '2.0',
|
|
1995
|
-
id,
|
|
1996
|
-
result,
|
|
1997
|
-
};
|
|
1998
|
-
const json = JSON.stringify(response);
|
|
1965
|
+
const json = JSON.stringify(protocol.resultResponse(id, result));
|
|
1999
1966
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
2000
1967
|
}
|
|
2001
1968
|
/**
|
|
2002
1969
|
* Send an error response via stdio.
|
|
2003
1970
|
*/
|
|
2004
1971
|
sendStdioError(id, error) {
|
|
2005
|
-
const
|
|
2006
|
-
jsonrpc: '2.0',
|
|
2007
|
-
id,
|
|
2008
|
-
error,
|
|
2009
|
-
};
|
|
2010
|
-
const json = JSON.stringify(response);
|
|
1972
|
+
const json = JSON.stringify(protocol.errorResponse(id, error.code, error.message));
|
|
2011
1973
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
2012
1974
|
}
|
|
2013
1975
|
/**
|