@kadi.build/core 0.3.4 → 0.5.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/dist/client.d.ts CHANGED
@@ -14,7 +14,8 @@
14
14
  * - Explicit over implicit (no proxy magic)
15
15
  * - Readable, traceable code flow
16
16
  */
17
- import type { ClientConfig, BrokerState, ToolDefinition, ZodToolDefinition, ToolHandler, LoadedAbility, ToolExecutionBridge, RegisterToolOptions, LoadNativeOptions, LoadStdioOptions, LoadBrokerOptions, InvokeRemoteOptions, BrokerEventHandler, PublishOptions, SubscribeOptions, EmitOptions } from './types.js';
17
+ import { type EncryptionKeyPair } from './crypto.js';
18
+ import type { ClientConfig, ClientIdentity, BrokerState, ToolDefinition, ZodToolDefinition, ToolHandler, LoadedAbility, ToolExecutionBridge, RegisterToolOptions, LoadNativeOptions, LoadStdioOptions, LoadBrokerOptions, InvokeRemoteOptions, BrokerEventHandler, PublishOptions, SubscribeOptions, EmitOptions } from './types.js';
18
19
  /**
19
20
  * The main client for building KADI agents.
20
21
  *
@@ -58,7 +59,92 @@ export declare class KadiClient {
58
59
  private eventHandler;
59
60
  /** Whether we're serving via stdio (set by serve('stdio')) */
60
61
  private isServingStdio;
62
+ /** Ed25519 private key (DER format) - used for signing */
63
+ private readonly _privateKey;
64
+ /** Base64-encoded Ed25519 public key (SPKI DER format) */
65
+ private readonly _publicKeyBase64;
66
+ /** Deterministic agent ID: SHA256(publicKey).hex().substring(0, 16) */
67
+ private readonly _agentId;
61
68
  constructor(config: ClientConfig);
69
+ /**
70
+ * Get the client's cryptographic identity.
71
+ *
72
+ * Available immediately after construction (before connect).
73
+ * The same identity is used for all broker connections.
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const client = new KadiClient({ name: 'my-agent' });
78
+ * console.log(client.identity);
79
+ * // { publicKey: 'MIIBIjAN...', agentId: 'a1b2c3d4e5f67890' }
80
+ * ```
81
+ */
82
+ get identity(): ClientIdentity;
83
+ /**
84
+ * Get the client's public key (base64-encoded SPKI DER format).
85
+ *
86
+ * This is the key used for Ed25519 authentication with brokers.
87
+ */
88
+ get publicKey(): string;
89
+ /**
90
+ * Get the client's agent ID.
91
+ *
92
+ * Derived deterministically from publicKey: SHA256(publicKey).hex().substring(0, 16)
93
+ * This is the same ID that brokers will assign during authentication.
94
+ */
95
+ get agentId(): string;
96
+ /**
97
+ * Get the client's X25519 encryption public key.
98
+ *
99
+ * This key is derived from the client's Ed25519 signing key and can be
100
+ * shared with others so they can encrypt messages that only this client
101
+ * can decrypt.
102
+ *
103
+ * Use this when you want others to send encrypted data to your agent.
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * // Agent shares its encryption public key
108
+ * await client.publish('secrets.request', {
109
+ * agentId: client.agentId,
110
+ * publicKey: client.publicKey, // Ed25519 for identity
111
+ * // The sender will use convertToEncryptionKey(publicKey) to encrypt
112
+ * });
113
+ * ```
114
+ */
115
+ get encryptionPublicKey(): Uint8Array;
116
+ /**
117
+ * Get the client's X25519 encryption key pair.
118
+ *
119
+ * This key pair is derived from the client's Ed25519 signing keys and can
120
+ * be used to decrypt messages that were encrypted with the client's
121
+ * encryption public key.
122
+ *
123
+ * Use this when you need to decrypt sealed box messages sent to your agent.
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * import nacl from 'tweetnacl';
128
+ * nacl.sealedbox = require('tweetnacl-sealedbox-js');
129
+ *
130
+ * // Decrypt a sealed box message
131
+ * const keyPair = client.encryptionKeyPair;
132
+ * const decrypted = nacl.sealedbox.open(
133
+ * encrypted,
134
+ * keyPair.publicKey,
135
+ * keyPair.secretKey
136
+ * );
137
+ * ```
138
+ */
139
+ get encryptionKeyPair(): EncryptionKeyPair;
140
+ /**
141
+ * Get the agent name (from config).
142
+ */
143
+ get name(): string;
144
+ /**
145
+ * Get the agent version (from config).
146
+ */
147
+ get version(): string;
62
148
  /**
63
149
  * Connect to all configured brokers.
64
150
  * Call this after registering tools.
@@ -74,14 +160,16 @@ export declare class KadiClient {
74
160
  * Connect to a specific broker by name.
75
161
  *
76
162
  * Protocol:
77
- * 1. Generate Ed25519 keypair
78
- * 2. Open WebSocket connection
79
- * 3. Send kadi.session.hello
80
- * 4. Receive nonce from broker
81
- * 5. Send kadi.session.authenticate with signed nonce
82
- * 6. Receive agentId from broker
83
- * 7. Register tools with broker
84
- * 8. Start heartbeat
163
+ * 1. Open WebSocket connection
164
+ * 2. Send kadi.session.hello
165
+ * 3. Receive nonce from broker
166
+ * 4. Send kadi.session.authenticate with signed nonce (using client's identity)
167
+ * 5. Receive agentId from broker (should match client.agentId)
168
+ * 6. Register tools with broker
169
+ * 7. Start heartbeat
170
+ *
171
+ * Note: The keypair is generated once at client construction and shared
172
+ * across all broker connections, ensuring consistent identity.
85
173
  */
86
174
  private connectToBroker;
87
175
  private buildHelloMessage;
@@ -230,6 +318,45 @@ export declare class KadiClient {
230
318
  * ```
231
319
  */
232
320
  publish(channel: string, data: unknown, options?: PublishOptions): Promise<void>;
321
+ /**
322
+ * Request secrets from the deployer during agent startup.
323
+ *
324
+ * This implements the agent side of the "Trusted Introducer" pattern:
325
+ * 1. Check for KADI_DEPLOY_NONCE env var (set by deployer)
326
+ * 2. Subscribe to secrets.response.{agentId} channel
327
+ * 3. Publish request to secrets.request with nonce and public key
328
+ * 4. Wait for response from deployer (approved with encrypted secrets, or rejected)
329
+ * 5. If approved, decrypt using agent's encryption key pair
330
+ * 6. Set secrets in process.env
331
+ *
332
+ * The deployer (kadi deploy) stays connected after deployment and waits
333
+ * for this request. It verifies the nonce, prompts for approval, and
334
+ * either shares encrypted secrets or sends a rejection.
335
+ *
336
+ * @param options - Configuration options
337
+ * @returns Decrypted secrets as key-value pairs, or null if no secrets needed
338
+ * @throws KadiError if timeout waiting for secrets or decryption fails
339
+ *
340
+ * @example
341
+ * ```typescript
342
+ * const client = new KadiClient({ name: 'my-agent', ... });
343
+ * await client.connect();
344
+ *
345
+ * // Request secrets from deployer (throws if timeout)
346
+ * const secrets = await client.requestSecrets();
347
+ *
348
+ * if (secrets) {
349
+ * console.log('Received secrets:', Object.keys(secrets));
350
+ * // Secrets are also set in process.env
351
+ * }
352
+ *
353
+ * // With custom timeout
354
+ * const secrets = await client.requestSecrets({ timeout: 120000 });
355
+ * ```
356
+ */
357
+ requestSecrets(options?: {
358
+ timeout?: number;
359
+ }): Promise<Record<string, string> | null>;
233
360
  /**
234
361
  * Handle incoming request from broker (tool invocation).
235
362
  */
@@ -299,8 +426,8 @@ export declare class KadiClient {
299
426
  /**
300
427
  * Attempt to reconnect to the broker.
301
428
  *
302
- * This reuses the existing keypair (agentId stays the same) and
303
- * re-registers tools with the broker.
429
+ * Uses the client's identity (same keypair/agentId for all connections)
430
+ * and re-registers tools with the broker.
304
431
  *
305
432
  * On failure, schedules another attempt with increased delay.
306
433
  */
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EACV,YAAY,EAEZ,WAAW,EAEX,cAAc,EACd,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;AAiGpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;gBAMnB,MAAM,EAAE,YAAY;IA4ChC;;;;;;;;;OASG;IACG,OAAO,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+CjD;;;;;;;;;;;;OAYG;YACW,eAAe;IAgE7B,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,gBAAgB;IAoBxB,OAAO,CAAC,oBAAoB;IAa5B,OAAO,CAAC,qBAAqB;IAa7B;;OAEG;IACH,OAAO,CAAC,aAAa;IAwCrB;;;;;;;;OAQG;YACW,gBAAgB;IAiC9B;;;;;;OAMG;YACW,kBAAkB;IAahC;;;;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;IAuBhB;;;;;;;;;;;;;;;;;OAiBG;IACG,WAAW,CACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC;IA2ChB;;;;;;;;;;;;;;;;;;OAkBG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB1F;;OAEG;YACW,mBAAmB;IA+BjC;;;;;;;;;;;OAWG;YACW,mBAAmB;IAwDjC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAsB5B;;OAEG;IACH,OAAO,CAAC,aAAa;IAOrB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAqCrB;;;;;;;OAOG;IACH,OAAO,CAAC,oBAAoB;IAuC5B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,iBAAiB;IAezB;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB;IAgBzB;;;;;;;OAOG;YACW,gBAAgB;IAwC9B;;;;;OAKG;IACG,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBpD;;;;;;;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;IAiC/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;IA6BP;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAe1B;;;;;;;;;;;;;;;;;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;IAkBvF;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC;IA0BrF;;;;;;;;;;;;;;OAcG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC;IAiBvF;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,YAAY,CAAC,CAAC,GAAG,OAAO,EAC5B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,CAAC,CAAC;IA6Eb;;;;;;;;;;;;;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;IAUzB;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;;;;OAKG;YACW,WAAW;IAsBzB;;OAEG;IACH,aAAa,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,cAAc,EAAE,CAAA;KAAE;IAQ3E;;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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -16,6 +16,9 @@
16
16
  */
17
17
  import { WebSocket } from 'ws';
18
18
  import * as crypto from 'crypto';
19
+ import { convertToEncryptionKey, convertToEncryptionKeyPair, } from './crypto.js';
20
+ // @ts-expect-error - tweetnacl-sealedbox-js doesn't have type definitions
21
+ import sealedbox from 'tweetnacl-sealedbox-js';
19
22
  import { KadiError } from './errors.js';
20
23
  import { zodToJsonSchema, isZodSchema } from './zod.js';
21
24
  import { resolveAbilityEntry, resolveAbilityScript } from './lockfile.js';
@@ -144,6 +147,15 @@ export class KadiClient {
144
147
  /** Whether we're serving via stdio (set by serve('stdio')) */
145
148
  isServingStdio = false;
146
149
  // ─────────────────────────────────────────────────────────────
150
+ // IDENTITY (single keypair shared across all broker connections)
151
+ // ─────────────────────────────────────────────────────────────
152
+ /** Ed25519 private key (DER format) - used for signing */
153
+ _privateKey;
154
+ /** Base64-encoded Ed25519 public key (SPKI DER format) */
155
+ _publicKeyBase64;
156
+ /** Deterministic agent ID: SHA256(publicKey).hex().substring(0, 16) */
157
+ _agentId;
158
+ // ─────────────────────────────────────────────────────────────
147
159
  // CONSTRUCTOR
148
160
  // ─────────────────────────────────────────────────────────────
149
161
  constructor(config) {
@@ -153,6 +165,12 @@ export class KadiClient {
153
165
  hint: 'Provide a name for your agent: new KadiClient({ name: "my-agent" })',
154
166
  });
155
167
  }
168
+ // Generate Ed25519 keypair once for consistent identity across all brokers
169
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
170
+ this._privateKey = privateKey.export({ type: 'pkcs8', format: 'der' });
171
+ this._publicKeyBase64 = publicKey.export({ type: 'spki', format: 'der' }).toString('base64');
172
+ // Derive agentId using same algorithm as broker: SHA256(publicKey).hex().substring(0, 16)
173
+ this._agentId = crypto.createHash('sha256').update(this._publicKeyBase64).digest('hex').substring(0, 16);
156
174
  // Resolve configuration with defaults
157
175
  // Auto-select first broker as default if not specified (matches Python behavior)
158
176
  const brokers = config.brokers ?? {};
@@ -179,6 +197,108 @@ export class KadiClient {
179
197
  }
180
198
  }
181
199
  // ─────────────────────────────────────────────────────────────
200
+ // IDENTITY GETTERS
201
+ // ─────────────────────────────────────────────────────────────
202
+ /**
203
+ * Get the client's cryptographic identity.
204
+ *
205
+ * Available immediately after construction (before connect).
206
+ * The same identity is used for all broker connections.
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const client = new KadiClient({ name: 'my-agent' });
211
+ * console.log(client.identity);
212
+ * // { publicKey: 'MIIBIjAN...', agentId: 'a1b2c3d4e5f67890' }
213
+ * ```
214
+ */
215
+ get identity() {
216
+ return {
217
+ publicKey: this._publicKeyBase64,
218
+ agentId: this._agentId,
219
+ };
220
+ }
221
+ /**
222
+ * Get the client's public key (base64-encoded SPKI DER format).
223
+ *
224
+ * This is the key used for Ed25519 authentication with brokers.
225
+ */
226
+ get publicKey() {
227
+ return this._publicKeyBase64;
228
+ }
229
+ /**
230
+ * Get the client's agent ID.
231
+ *
232
+ * Derived deterministically from publicKey: SHA256(publicKey).hex().substring(0, 16)
233
+ * This is the same ID that brokers will assign during authentication.
234
+ */
235
+ get agentId() {
236
+ return this._agentId;
237
+ }
238
+ // ─────────────────────────────────────────────────────────────
239
+ // ENCRYPTION KEY GETTERS
240
+ // ─────────────────────────────────────────────────────────────
241
+ /**
242
+ * Get the client's X25519 encryption public key.
243
+ *
244
+ * This key is derived from the client's Ed25519 signing key and can be
245
+ * shared with others so they can encrypt messages that only this client
246
+ * can decrypt.
247
+ *
248
+ * Use this when you want others to send encrypted data to your agent.
249
+ *
250
+ * @example
251
+ * ```typescript
252
+ * // Agent shares its encryption public key
253
+ * await client.publish('secrets.request', {
254
+ * agentId: client.agentId,
255
+ * publicKey: client.publicKey, // Ed25519 for identity
256
+ * // The sender will use convertToEncryptionKey(publicKey) to encrypt
257
+ * });
258
+ * ```
259
+ */
260
+ get encryptionPublicKey() {
261
+ return convertToEncryptionKey(this._publicKeyBase64);
262
+ }
263
+ /**
264
+ * Get the client's X25519 encryption key pair.
265
+ *
266
+ * This key pair is derived from the client's Ed25519 signing keys and can
267
+ * be used to decrypt messages that were encrypted with the client's
268
+ * encryption public key.
269
+ *
270
+ * Use this when you need to decrypt sealed box messages sent to your agent.
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * import nacl from 'tweetnacl';
275
+ * nacl.sealedbox = require('tweetnacl-sealedbox-js');
276
+ *
277
+ * // Decrypt a sealed box message
278
+ * const keyPair = client.encryptionKeyPair;
279
+ * const decrypted = nacl.sealedbox.open(
280
+ * encrypted,
281
+ * keyPair.publicKey,
282
+ * keyPair.secretKey
283
+ * );
284
+ * ```
285
+ */
286
+ get encryptionKeyPair() {
287
+ return convertToEncryptionKeyPair(this._privateKey, this._publicKeyBase64);
288
+ }
289
+ /**
290
+ * Get the agent name (from config).
291
+ */
292
+ get name() {
293
+ return this.config.name;
294
+ }
295
+ /**
296
+ * Get the agent version (from config).
297
+ */
298
+ get version() {
299
+ return this.config.version;
300
+ }
301
+ // ─────────────────────────────────────────────────────────────
182
302
  // CONNECTION MANAGEMENT
183
303
  // ─────────────────────────────────────────────────────────────
184
304
  /**
@@ -231,14 +351,16 @@ export class KadiClient {
231
351
  * Connect to a specific broker by name.
232
352
  *
233
353
  * Protocol:
234
- * 1. Generate Ed25519 keypair
235
- * 2. Open WebSocket connection
236
- * 3. Send kadi.session.hello
237
- * 4. Receive nonce from broker
238
- * 5. Send kadi.session.authenticate with signed nonce
239
- * 6. Receive agentId from broker
240
- * 7. Register tools with broker
241
- * 8. Start heartbeat
354
+ * 1. Open WebSocket connection
355
+ * 2. Send kadi.session.hello
356
+ * 3. Receive nonce from broker
357
+ * 4. Send kadi.session.authenticate with signed nonce (using client's identity)
358
+ * 5. Receive agentId from broker (should match client.agentId)
359
+ * 6. Register tools with broker
360
+ * 7. Start heartbeat
361
+ *
362
+ * Note: The keypair is generated once at client construction and shared
363
+ * across all broker connections, ensuring consistent identity.
242
364
  */
243
365
  async connectToBroker(brokerName) {
244
366
  const url = this.config.brokers[brokerName];
@@ -256,16 +378,11 @@ export class KadiClient {
256
378
  return;
257
379
  }
258
380
  }
259
- // Generate Ed25519 keypair for authentication
260
- const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
261
- // Create broker state
381
+ // Create broker state (identity is at client level, not per-broker)
262
382
  const state = {
263
383
  name: brokerName,
264
384
  url,
265
385
  ws: null,
266
- agentId: '',
267
- publicKey: publicKey.export({ type: 'spki', format: 'der' }),
268
- privateKey: privateKey.export({ type: 'pkcs8', format: 'der' }),
269
386
  heartbeatTimer: null,
270
387
  pendingRequests: new Map(),
271
388
  pendingInvocations: new Map(),
@@ -302,9 +419,10 @@ export class KadiClient {
302
419
  params: { role: 'agent' },
303
420
  };
304
421
  }
305
- buildAuthMessage(state, nonce) {
422
+ buildAuthMessage(nonce) {
423
+ // Sign the nonce using the client's private key
306
424
  const privateKey = crypto.createPrivateKey({
307
- key: state.privateKey,
425
+ key: this._privateKey,
308
426
  format: 'der',
309
427
  type: 'pkcs8',
310
428
  });
@@ -314,7 +432,7 @@ export class KadiClient {
314
432
  id: this.nextRequestId++,
315
433
  method: 'kadi.session.authenticate',
316
434
  params: {
317
- publicKey: state.publicKey.toString('base64'),
435
+ publicKey: this._publicKeyBase64,
318
436
  signature: signature.toString('base64'),
319
437
  nonce,
320
438
  },
@@ -399,13 +517,18 @@ export class KadiClient {
399
517
  }
400
518
  // Step 2: Sign the nonce and authenticate
401
519
  // This proves we own the private key without revealing it
402
- const authResult = await this.sendRequest(state, this.buildAuthMessage(state, helloResult.nonce));
520
+ const authResult = await this.sendRequest(state, this.buildAuthMessage(helloResult.nonce));
403
521
  if (!authResult.agentId) {
404
522
  throw new KadiError('Auth response missing agentId', 'AUTHENTICATION_FAILED', {
405
523
  broker: state.name,
406
524
  });
407
525
  }
408
- state.agentId = authResult.agentId;
526
+ // Verify broker assigned the expected agentId (sanity check)
527
+ if (authResult.agentId !== this._agentId) {
528
+ console.warn(`[KADI] Broker "${state.name}" returned different agentId: ` +
529
+ `expected ${this._agentId}, got ${authResult.agentId}. ` +
530
+ `This may indicate the broker uses a different ID derivation algorithm.`);
531
+ }
409
532
  }
410
533
  /**
411
534
  * Register with broker after authentication.
@@ -790,6 +913,181 @@ export class KadiClient {
790
913
  },
791
914
  });
792
915
  }
916
+ // ─────────────────────────────────────────────────────────────
917
+ // DEPLOYMENT SECRETS (Trusted Introducer Pattern)
918
+ // ─────────────────────────────────────────────────────────────
919
+ /**
920
+ * Request secrets from the deployer during agent startup.
921
+ *
922
+ * This implements the agent side of the "Trusted Introducer" pattern:
923
+ * 1. Check for KADI_DEPLOY_NONCE env var (set by deployer)
924
+ * 2. Subscribe to secrets.response.{agentId} channel
925
+ * 3. Publish request to secrets.request with nonce and public key
926
+ * 4. Wait for response from deployer (approved with encrypted secrets, or rejected)
927
+ * 5. If approved, decrypt using agent's encryption key pair
928
+ * 6. Set secrets in process.env
929
+ *
930
+ * The deployer (kadi deploy) stays connected after deployment and waits
931
+ * for this request. It verifies the nonce, prompts for approval, and
932
+ * either shares encrypted secrets or sends a rejection.
933
+ *
934
+ * @param options - Configuration options
935
+ * @returns Decrypted secrets as key-value pairs, or null if no secrets needed
936
+ * @throws KadiError if timeout waiting for secrets or decryption fails
937
+ *
938
+ * @example
939
+ * ```typescript
940
+ * const client = new KadiClient({ name: 'my-agent', ... });
941
+ * await client.connect();
942
+ *
943
+ * // Request secrets from deployer (throws if timeout)
944
+ * const secrets = await client.requestSecrets();
945
+ *
946
+ * if (secrets) {
947
+ * console.log('Received secrets:', Object.keys(secrets));
948
+ * // Secrets are also set in process.env
949
+ * }
950
+ *
951
+ * // With custom timeout
952
+ * const secrets = await client.requestSecrets({ timeout: 120000 });
953
+ * ```
954
+ */
955
+ async requestSecrets(options = {}) {
956
+ const { timeout = 60000 } = options; // Default 60 seconds
957
+ // Check for deployment nonce - if not set, no secrets needed
958
+ const nonce = process.env.KADI_DEPLOY_NONCE;
959
+ if (!nonce) {
960
+ return null;
961
+ }
962
+ // Parse required/optional secrets from env
963
+ const requiredSecrets = process.env.KADI_REQUIRED_SECRETS?.split(',').filter(Boolean) || [];
964
+ const optionalSecrets = process.env.KADI_OPTIONAL_SECRETS?.split(',').filter(Boolean) || [];
965
+ if (requiredSecrets.length === 0 && optionalSecrets.length === 0) {
966
+ return null;
967
+ }
968
+ // Ensure we're connected to at least one broker
969
+ const connectedBrokers = [];
970
+ for (const [name, state] of this.brokers.entries()) {
971
+ if (state.status === 'connected') {
972
+ connectedBrokers.push([name, state]);
973
+ }
974
+ }
975
+ if (connectedBrokers.length === 0) {
976
+ throw new KadiError('Cannot request secrets: not connected to any broker', 'SECRETS_NOT_CONNECTED', { hint: 'Call client.connect() before requestSecrets()' });
977
+ }
978
+ // Determine which broker to use for the secret handshake
979
+ let brokerName;
980
+ const rendezvousBrokerUrl = process.env.KADI_RENDEZVOUS_BROKER;
981
+ if (rendezvousBrokerUrl) {
982
+ // Find the broker matching the rendezvous URL
983
+ const match = connectedBrokers.find(([_, state]) => state.url === rendezvousBrokerUrl);
984
+ if (!match) {
985
+ const connectedUrlsFormatted = connectedBrokers.map(([name, state]) => ` - ${name}: ${state.url}`).join('\n');
986
+ throw new KadiError(`This agent needs secrets, but it's not connected to the right broker.`, 'SECRETS_NOT_CONNECTED', {
987
+ hint: `Secrets will be delivered on:\n ${rendezvousBrokerUrl}\n\nYour client is connected to:\n${connectedUrlsFormatted}\n\nTo fix, add the delivery broker URL to your KadiClient config,\nor update agent.json brokers to include this URL.`,
988
+ });
989
+ }
990
+ brokerName = match[0];
991
+ }
992
+ else {
993
+ // No rendezvous broker specified - use first connected (for dev/testing)
994
+ brokerName = connectedBrokers[0][0];
995
+ }
996
+ return new Promise((resolve, reject) => {
997
+ let resolved = false;
998
+ let timeoutHandle;
999
+ // Cleanup function
1000
+ const cleanup = () => {
1001
+ if (timeoutHandle) {
1002
+ clearTimeout(timeoutHandle);
1003
+ }
1004
+ // Unsubscribe from the channel
1005
+ this.unsubscribe(`secrets.response.${this._agentId}`, handleSecrets, { broker: brokerName })
1006
+ .catch(() => { }); // Ignore unsubscribe errors
1007
+ };
1008
+ // Handler for receiving secrets response
1009
+ const handleSecrets = async (event) => {
1010
+ if (resolved)
1011
+ return;
1012
+ // Extract response data from event
1013
+ const data = event.data;
1014
+ if (!data?.status) {
1015
+ // Malformed response - ignore
1016
+ return;
1017
+ }
1018
+ // Validate status is expected value
1019
+ if (data.status !== 'approved' && data.status !== 'rejected') {
1020
+ // Unknown status - ignore malformed response
1021
+ return;
1022
+ }
1023
+ resolved = true;
1024
+ cleanup();
1025
+ // Handle rejection
1026
+ if (data.status === 'rejected') {
1027
+ reject(new KadiError(`Secrets request rejected: ${data.reason || 'No reason provided'}`, 'SECRETS_REJECTED', { reason: data.reason }));
1028
+ return;
1029
+ }
1030
+ // Handle approval - must have encrypted data
1031
+ if (!data.encrypted) {
1032
+ reject(new KadiError('Secrets approved but no encrypted data received', 'SECRETS_DECRYPTION_FAILED', { hint: 'The deployer may have encountered an error' }));
1033
+ return;
1034
+ }
1035
+ try {
1036
+ // Decode base64 encrypted blob
1037
+ const encryptedBytes = Buffer.from(data.encrypted, 'base64');
1038
+ // Get our encryption key pair
1039
+ const keyPair = this.encryptionKeyPair;
1040
+ // Decrypt the sealed box
1041
+ const decrypted = sealedbox.open(new Uint8Array(encryptedBytes), keyPair.publicKey, keyPair.secretKey);
1042
+ if (!decrypted) {
1043
+ reject(new KadiError('Failed to decrypt secrets: invalid sealed box or wrong key', 'SECRETS_DECRYPTION_FAILED', { hint: 'The secrets may have been encrypted for a different agent' }));
1044
+ return;
1045
+ }
1046
+ // Parse JSON
1047
+ const secretsJson = new TextDecoder().decode(decrypted);
1048
+ const secrets = JSON.parse(secretsJson);
1049
+ // Set secrets in process.env
1050
+ for (const [key, value] of Object.entries(secrets)) {
1051
+ process.env[key] = value;
1052
+ }
1053
+ resolve(secrets);
1054
+ }
1055
+ catch (err) {
1056
+ reject(new KadiError(`Failed to decrypt secrets: ${err.message}`, 'SECRETS_DECRYPTION_FAILED', { cause: err, hint: 'The secrets may have been encrypted for a different agent' }));
1057
+ }
1058
+ };
1059
+ // Set timeout
1060
+ timeoutHandle = setTimeout(() => {
1061
+ if (!resolved) {
1062
+ resolved = true;
1063
+ cleanup();
1064
+ reject(new KadiError(`Timeout waiting for secrets (${timeout / 1000}s). Deployer may have disconnected or denied the request.`, 'SECRETS_TIMEOUT', {
1065
+ timeout,
1066
+ hint: 'Ensure the deployer is still running and approves the secret request',
1067
+ }));
1068
+ }
1069
+ }, timeout);
1070
+ // Subscribe to our response channel
1071
+ this.subscribe(`secrets.response.${this._agentId}`, handleSecrets, { broker: brokerName })
1072
+ .then(() => {
1073
+ // Publish secret request
1074
+ return this.publish('secrets.request', {
1075
+ nonce,
1076
+ agentId: this._agentId,
1077
+ publicKey: this._publicKeyBase64,
1078
+ required: requiredSecrets,
1079
+ optional: optionalSecrets,
1080
+ }, { broker: brokerName });
1081
+ })
1082
+ .catch((err) => {
1083
+ if (!resolved) {
1084
+ resolved = true;
1085
+ cleanup();
1086
+ reject(new KadiError(`Failed to request secrets: ${err.message}`, 'SECRETS_REQUEST_FAILED', { cause: err }));
1087
+ }
1088
+ });
1089
+ });
1090
+ }
793
1091
  /**
794
1092
  * Handle incoming request from broker (tool invocation).
795
1093
  */
@@ -1028,8 +1326,8 @@ export class KadiClient {
1028
1326
  /**
1029
1327
  * Attempt to reconnect to the broker.
1030
1328
  *
1031
- * This reuses the existing keypair (agentId stays the same) and
1032
- * re-registers tools with the broker.
1329
+ * Uses the client's identity (same keypair/agentId for all connections)
1330
+ * and re-registers tools with the broker.
1033
1331
  *
1034
1332
  * On failure, schedules another attempt with increased delay.
1035
1333
  */