@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 +138 -11
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +319 -21
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts +88 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +120 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +30 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/client.ts +2380 -0
- package/src/crypto.ts +153 -0
- package/src/errors.ts +292 -0
- package/src/index.ts +114 -0
- package/src/lockfile.ts +493 -0
- package/src/transports/broker.ts +682 -0
- package/src/transports/native.ts +307 -0
- package/src/transports/stdio.ts +580 -0
- package/src/types.ts +1011 -0
- package/src/zod.ts +69 -0
- package/tsconfig.json +52 -0
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
|
|
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.
|
|
78
|
-
* 2.
|
|
79
|
-
* 3.
|
|
80
|
-
* 4.
|
|
81
|
-
* 5.
|
|
82
|
-
* 6.
|
|
83
|
-
* 7.
|
|
84
|
-
*
|
|
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
|
-
*
|
|
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
|
*/
|
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,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;
|
|
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.
|
|
235
|
-
* 2.
|
|
236
|
-
* 3.
|
|
237
|
-
* 4.
|
|
238
|
-
* 5.
|
|
239
|
-
* 6.
|
|
240
|
-
* 7.
|
|
241
|
-
*
|
|
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
|
-
//
|
|
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(
|
|
422
|
+
buildAuthMessage(nonce) {
|
|
423
|
+
// Sign the nonce using the client's private key
|
|
306
424
|
const privateKey = crypto.createPrivateKey({
|
|
307
|
-
key:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
*/
|