@kadi.build/core 0.4.0 → 0.6.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/src/crypto.ts ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Cryptographic utilities for KADI
3
+ *
4
+ * This module provides utilities for converting between Ed25519 signing keys
5
+ * and X25519 encryption keys.
6
+ *
7
+ * ## Background
8
+ *
9
+ * KADI uses Ed25519 keys for identity and authentication (signing).
10
+ * However, Ed25519 is a signing algorithm - it cannot encrypt data.
11
+ *
12
+ * X25519 is an encryption algorithm that allows:
13
+ * - Encrypting data so only the recipient can read it
14
+ * - Sealed boxes: sender encrypts with recipient's public key,
15
+ * only recipient's private key can decrypt
16
+ *
17
+ * Both algorithms use Curve25519 underneath, so an Ed25519 key
18
+ * can be mathematically converted to an X25519 key.
19
+ *
20
+ * @module crypto
21
+ */
22
+
23
+ // @ts-expect-error - ed2curve doesn't have type definitions
24
+ import ed2curve from 'ed2curve';
25
+
26
+ /**
27
+ * X25519 encryption key pair.
28
+ *
29
+ * Used for public-key encryption (e.g., sealed boxes).
30
+ * Derived from Ed25519 signing keys.
31
+ */
32
+ export interface EncryptionKeyPair {
33
+ /** X25519 public key (32 bytes) - share with others to receive encrypted messages */
34
+ publicKey: Uint8Array;
35
+ /** X25519 secret key (32 bytes) - keep private, used to decrypt messages */
36
+ secretKey: Uint8Array;
37
+ }
38
+
39
+ /**
40
+ * Convert an Ed25519 signing public key to an X25519 encryption public key.
41
+ *
42
+ * KADI uses Ed25519 keys for identity and authentication (signing).
43
+ * However, Ed25519 is a signing algorithm - it cannot encrypt data.
44
+ *
45
+ * X25519 is an encryption algorithm that allows:
46
+ * - Encrypting data so only the recipient can read it
47
+ * - Sealed boxes: sender encrypts with recipient's public key,
48
+ * only recipient's private key can decrypt
49
+ *
50
+ * Both algorithms use Curve25519 underneath, so an Ed25519 key
51
+ * can be mathematically converted to an X25519 key. This function
52
+ * performs that conversion.
53
+ *
54
+ * @param publicKey - Base64-encoded Ed25519 public key (SPKI DER format)
55
+ * @returns X25519 public key as Uint8Array (32 bytes)
56
+ * @throws Error if the public key is invalid
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * import { convertToEncryptionKey } from '@kadi.build/core';
61
+ * import nacl from 'tweetnacl';
62
+ *
63
+ * // Convert agent's Ed25519 public key to X25519
64
+ * const encryptionKey = convertToEncryptionKey(agentPublicKey);
65
+ *
66
+ * // Now you can encrypt data for that agent using sealed box
67
+ * const encrypted = nacl.sealedbox.seal(message, encryptionKey);
68
+ * ```
69
+ */
70
+ export function convertToEncryptionKey(publicKey: string): Uint8Array {
71
+ // Decode base64 SPKI DER format to raw bytes
72
+ const derBytes = Buffer.from(publicKey, 'base64');
73
+
74
+ // SPKI DER format for Ed25519 has a 12-byte header, raw key starts at offset 12
75
+ // Format: 30 2a 30 05 06 03 2b 65 70 03 21 00 [32-byte key]
76
+ const rawEd25519Key = derBytes.slice(12);
77
+
78
+ if (rawEd25519Key.length !== 32) {
79
+ throw new Error(
80
+ `Invalid Ed25519 public key: expected 32 bytes after SPKI header, got ${rawEd25519Key.length}`
81
+ );
82
+ }
83
+
84
+ // Convert Ed25519 public key to X25519 public key
85
+ const x25519Key = ed2curve.convertPublicKey(new Uint8Array(rawEd25519Key));
86
+
87
+ if (!x25519Key) {
88
+ throw new Error('Failed to convert Ed25519 public key to X25519');
89
+ }
90
+
91
+ return x25519Key;
92
+ }
93
+
94
+ /**
95
+ * Convert an Ed25519 signing key pair to an X25519 encryption key pair.
96
+ *
97
+ * This is used internally by KadiClient to derive encryption keys from
98
+ * its Ed25519 identity. The resulting key pair can be used with
99
+ * tweetnacl's box or sealedbox functions.
100
+ *
101
+ * @param privateKey - Ed25519 private key in PKCS8 DER format (Buffer)
102
+ * @param publicKey - Base64-encoded Ed25519 public key (SPKI DER format)
103
+ * @returns X25519 key pair for encryption/decryption
104
+ * @throws Error if the keys are invalid
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * // Inside KadiClient
109
+ * const keyPair = convertToEncryptionKeyPair(this._privateKey, this._publicKeyBase64);
110
+ *
111
+ * // Decrypt a sealed box message
112
+ * const decrypted = nacl.sealedbox.open(encrypted, keyPair.publicKey, keyPair.secretKey);
113
+ * ```
114
+ */
115
+ export function convertToEncryptionKeyPair(
116
+ privateKey: Buffer,
117
+ publicKey: string
118
+ ): EncryptionKeyPair {
119
+ // Get X25519 public key
120
+ const x25519PublicKey = convertToEncryptionKey(publicKey);
121
+
122
+ // PKCS8 DER format for Ed25519 has a header, raw key is at offset 16
123
+ // Format: 30 2e 02 01 00 30 05 06 03 2b 65 70 04 22 04 20 [32-byte seed]
124
+ // The seed is the first 32 bytes of the 64-byte Ed25519 secret key
125
+ const rawSeed = privateKey.slice(16, 48);
126
+
127
+ if (rawSeed.length !== 32) {
128
+ throw new Error(
129
+ `Invalid Ed25519 private key: expected 32-byte seed after PKCS8 header, got ${rawSeed.length}`
130
+ );
131
+ }
132
+
133
+ // Convert Ed25519 secret key (seed) to X25519 secret key
134
+ // ed2curve.convertSecretKey expects the 64-byte secret key, but we only have the seed
135
+ // We need to expand the seed first by creating the full 64-byte key
136
+ // The full Ed25519 secret key is: seed (32 bytes) + public key (32 bytes)
137
+ const derBytes = Buffer.from(publicKey, 'base64');
138
+ const rawPublicKey = derBytes.slice(12);
139
+ const fullSecretKey = new Uint8Array(64);
140
+ fullSecretKey.set(rawSeed, 0);
141
+ fullSecretKey.set(rawPublicKey, 32);
142
+
143
+ const x25519SecretKey = ed2curve.convertSecretKey(fullSecretKey);
144
+
145
+ if (!x25519SecretKey) {
146
+ throw new Error('Failed to convert Ed25519 secret key to X25519');
147
+ }
148
+
149
+ return {
150
+ publicKey: x25519PublicKey,
151
+ secretKey: x25519SecretKey,
152
+ };
153
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Error handling for kadi-core v0.1.0
3
+ *
4
+ * All errors in kadi-core use KadiError, which includes:
5
+ * - A human-readable message
6
+ * - An error code for programmatic handling
7
+ * - Context with additional details
8
+ * - Automatic formatting of hints and suggestions
9
+ *
10
+ * The goal is helpful errors that guide users to solutions.
11
+ */
12
+
13
+ // ═══════════════════════════════════════════════════════════════════════
14
+ // ERROR CODES
15
+ // ═══════════════════════════════════════════════════════════════════════
16
+
17
+ /**
18
+ * All possible error codes.
19
+ * Use these for programmatic error handling.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * try {
24
+ * await client.connect();
25
+ * } catch (error) {
26
+ * if (error instanceof KadiError && error.code === 'CONNECTION_FAILED') {
27
+ * // Handle connection failure specifically
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ export type ErrorCode =
33
+ // Connection errors
34
+ | 'CONNECTION_FAILED'
35
+ | 'AUTHENTICATION_FAILED'
36
+ | 'REGISTRATION_FAILED'
37
+ | 'BROKER_NOT_CONNECTED'
38
+ | 'BROKER_CONNECTION_FAILED'
39
+ | 'CONNECTION_CLOSED'
40
+
41
+ // Broker errors
42
+ | 'BROKER_ERROR'
43
+ | 'BROKER_TIMEOUT'
44
+ | 'BROKER_RESPONSE_ERROR'
45
+ | 'UNKNOWN_BROKER'
46
+
47
+ // Tool errors
48
+ | 'TOOL_NOT_FOUND'
49
+ | 'TOOL_ALREADY_REGISTERED'
50
+ | 'TOOL_INVOCATION_FAILED'
51
+ | 'TOOL_TIMEOUT'
52
+ | 'VALIDATION_FAILED'
53
+
54
+ // Ability errors
55
+ | 'ABILITY_NOT_FOUND'
56
+ | 'ABILITY_LOAD_FAILED'
57
+ | 'ABILITY_NOT_CONNECTED'
58
+
59
+ // Lock file errors
60
+ | 'LOCKFILE_NOT_FOUND'
61
+ | 'LOCKFILE_PARSE_ERROR'
62
+
63
+ // Config errors
64
+ | 'INVALID_CONFIG'
65
+ | 'INVALID_INPUT'
66
+ | 'PROJECT_ROOT_NOT_FOUND'
67
+
68
+ // Transport errors
69
+ | 'STDIO_ERROR'
70
+ | 'STDIO_PROCESS_FAILED'
71
+ | 'NATIVE_IMPORT_ERROR'
72
+ | 'TRANSPORT_SEND_FAILED'
73
+
74
+ // Secrets errors (Trusted Introducer pattern)
75
+ | 'SECRETS_NOT_CONNECTED'
76
+ | 'SECRETS_DECRYPTION_FAILED'
77
+ | 'SECRETS_REQUEST_FAILED'
78
+ | 'SECRETS_REJECTED'
79
+ | 'SECRETS_TIMEOUT'
80
+
81
+ // Generic
82
+ | 'TIMEOUT'
83
+ | 'INTERNAL_ERROR'
84
+ | 'UNKNOWN_ERROR'
85
+ | 'NOT_IMPLEMENTED';
86
+
87
+ // ═══════════════════════════════════════════════════════════════════════
88
+ // ERROR CONTEXT
89
+ // ═══════════════════════════════════════════════════════════════════════
90
+
91
+ /**
92
+ * Additional context that can be attached to errors.
93
+ * All fields are optional - include what's helpful.
94
+ */
95
+ export interface ErrorContext {
96
+ /** Hint for how to fix the error */
97
+ hint?: string;
98
+
99
+ /** Alternative approaches */
100
+ alternative?: string;
101
+
102
+ /** Path that was searched */
103
+ searched?: string;
104
+
105
+ /** List of available options */
106
+ available?: string[];
107
+
108
+ /** List of connected brokers */
109
+ connectedBrokers?: string[];
110
+
111
+ /** The broker name involved */
112
+ broker?: string;
113
+
114
+ /** The broker URL involved */
115
+ url?: string;
116
+
117
+ /** The tool name involved */
118
+ toolName?: string;
119
+
120
+ /** The ability name involved */
121
+ abilityName?: string;
122
+
123
+ /** The path involved */
124
+ path?: string;
125
+
126
+ /** The reason for the error */
127
+ reason?: string;
128
+
129
+ /** Any other context */
130
+ [key: string]: unknown;
131
+ }
132
+
133
+ // ═══════════════════════════════════════════════════════════════════════
134
+ // KADI ERROR CLASS
135
+ // ═══════════════════════════════════════════════════════════════════════
136
+
137
+ /**
138
+ * Custom error class for kadi-core.
139
+ *
140
+ * Features:
141
+ * - Error code for programmatic handling
142
+ * - Context object with additional details
143
+ * - Automatic formatting of helpful messages
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * throw new KadiError(
148
+ * 'Ability "calculator" not found in agent-lock.json',
149
+ * 'ABILITY_NOT_FOUND',
150
+ * {
151
+ * searched: '/path/to/agent-lock.json',
152
+ * hint: 'Run `kadi install calculator` to install it',
153
+ * alternative: 'Or specify explicit path: { path: "./path" }',
154
+ * }
155
+ * );
156
+ * ```
157
+ */
158
+ export class KadiError extends Error {
159
+ /**
160
+ * Error code for programmatic handling.
161
+ */
162
+ public readonly code: ErrorCode;
163
+
164
+ /**
165
+ * Additional context about the error.
166
+ */
167
+ public readonly context: ErrorContext;
168
+
169
+ constructor(message: string, code: ErrorCode, context: ErrorContext = {}) {
170
+ // Format the message with context appended
171
+ super(formatErrorMessage(message, context));
172
+
173
+ this.name = 'KadiError';
174
+ this.code = code;
175
+ this.context = context;
176
+
177
+ // Maintain proper stack trace in V8 environments
178
+ if (Error.captureStackTrace) {
179
+ Error.captureStackTrace(this, KadiError);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Create a KadiError from an unknown error.
185
+ * Useful for wrapping errors from external sources.
186
+ *
187
+ * @param error - The error to wrap
188
+ * @param code - Error code to use (default: 'UNKNOWN_ERROR')
189
+ * @param context - Additional context to add
190
+ */
191
+ static from(
192
+ error: unknown,
193
+ code: ErrorCode = 'UNKNOWN_ERROR',
194
+ context: ErrorContext = {}
195
+ ): KadiError {
196
+ // If it's already a KadiError, return it (don't double-wrap)
197
+ if (error instanceof KadiError) {
198
+ return error;
199
+ }
200
+
201
+ // Extract message from various error types
202
+ let message: string;
203
+ if (error instanceof Error) {
204
+ message = error.message;
205
+ } else if (typeof error === 'string') {
206
+ message = error;
207
+ } else {
208
+ message = 'An unknown error occurred';
209
+ }
210
+
211
+ return new KadiError(message, code, context);
212
+ }
213
+
214
+ /**
215
+ * Check if an error is a KadiError.
216
+ */
217
+ static isKadiError(error: unknown): error is KadiError {
218
+ return error instanceof KadiError;
219
+ }
220
+ }
221
+
222
+ // ═══════════════════════════════════════════════════════════════════════
223
+ // MESSAGE FORMATTING
224
+ // ═══════════════════════════════════════════════════════════════════════
225
+
226
+ /**
227
+ * Format an error message with context appended.
228
+ * Creates a multi-line message that's easy to read in console output.
229
+ *
230
+ * @example
231
+ * Input:
232
+ * message: 'Ability "foo" not found'
233
+ * context: { searched: '/path', hint: 'Run kadi install foo' }
234
+ *
235
+ * Output:
236
+ * 'Ability "foo" not found
237
+ * Searched: /path
238
+ * Hint: Run kadi install foo'
239
+ */
240
+ function formatErrorMessage(message: string, context: ErrorContext): string {
241
+ const lines: string[] = [message];
242
+
243
+ // Add context fields in a consistent order
244
+ if (context.searched) {
245
+ lines.push(` Searched: ${context.searched}`);
246
+ }
247
+
248
+ if (context.path) {
249
+ lines.push(` Path: ${context.path}`);
250
+ }
251
+
252
+ if (context.broker) {
253
+ lines.push(` Broker: ${context.broker}`);
254
+ }
255
+
256
+ if (context.url) {
257
+ lines.push(` URL: ${context.url}`);
258
+ }
259
+
260
+ if (context.reason) {
261
+ lines.push(` Reason: ${context.reason}`);
262
+ }
263
+
264
+ if (context.connectedBrokers && context.connectedBrokers.length > 0) {
265
+ lines.push(` Connected brokers: ${context.connectedBrokers.join(', ')}`);
266
+ }
267
+
268
+ if (context.available && context.available.length > 0) {
269
+ lines.push(` Available: ${context.available.join(', ')}`);
270
+ }
271
+
272
+ if (context.hint) {
273
+ lines.push(` Hint: ${context.hint}`);
274
+ }
275
+
276
+ if (context.alternative) {
277
+ lines.push(` Or: ${context.alternative}`);
278
+ }
279
+
280
+ return lines.join('\n');
281
+ }
282
+
283
+ // ═══════════════════════════════════════════════════════════════════════
284
+ // TYPE GUARD
285
+ // ═══════════════════════════════════════════════════════════════════════
286
+
287
+ /**
288
+ * Check if a value is an Error object.
289
+ */
290
+ export function isError(value: unknown): value is Error {
291
+ return value instanceof Error;
292
+ }
package/src/index.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * kadi-core v0.1.0
3
+ *
4
+ * A lean, readable SDK for building KADI agents.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { KadiClient, z } from 'kadi-core';
9
+ *
10
+ * const client = new KadiClient({
11
+ * name: 'my-agent',
12
+ * brokers: {
13
+ * production: 'ws://broker:8080',
14
+ * },
15
+ * defaultBroker: 'production',
16
+ * });
17
+ *
18
+ * client.registerTool({
19
+ * name: 'add',
20
+ * description: 'Add two numbers',
21
+ * input: z.object({ a: z.number(), b: z.number() }),
22
+ * }, async ({ a, b }) => ({ result: a + b }));
23
+ *
24
+ * await client.connect();
25
+ * ```
26
+ */
27
+
28
+ // Re-export Zod for schema definitions
29
+ export { z } from 'zod';
30
+
31
+ // Main client
32
+ export { KadiClient } from './client.js';
33
+
34
+ // Errors
35
+ export { KadiError } from './errors.js';
36
+ export type { ErrorCode, ErrorContext } from './errors.js';
37
+
38
+ // Types
39
+ export type {
40
+ // Configuration
41
+ ClientConfig,
42
+ ResolvedConfig,
43
+
44
+ // Tools
45
+ ToolDefinition,
46
+ ZodToolDefinition,
47
+ ToolHandler,
48
+ RegisteredTool,
49
+ JSONSchema,
50
+ RequestContext,
51
+
52
+ // Abilities
53
+ LoadedAbility,
54
+ InvokeOptions,
55
+ TransportType,
56
+ EventHandler,
57
+
58
+ // Options
59
+ RegisterToolOptions,
60
+ LoadOptions,
61
+ LoadNativeOptions,
62
+ LoadStdioOptions,
63
+ LoadBrokerOptions,
64
+ InvokeRemoteOptions,
65
+
66
+ // Broker
67
+ BrokerState,
68
+ BrokerStatus,
69
+ PendingRequest,
70
+
71
+ // Broker Events (Pub/Sub)
72
+ BrokerEvent,
73
+ BrokerEventHandler,
74
+ PublishOptions,
75
+ SubscribeOptions,
76
+ EmitOptions,
77
+
78
+ // JSON-RPC
79
+ JsonRpcRequest,
80
+ JsonRpcResponse,
81
+ JsonRpcNotification,
82
+ JsonRpcError,
83
+ JsonRpcMessage,
84
+
85
+ // Lock file
86
+ AgentLockFile,
87
+ AbilityLockEntry,
88
+
89
+ // Script resolution
90
+ ResolvedScript,
91
+ } from './types.js';
92
+
93
+ // Zod utilities
94
+ export { zodToJsonSchema, isZodSchema } from './zod.js';
95
+
96
+ // Lock file utilities
97
+ export {
98
+ findProjectRoot,
99
+ readLockFile,
100
+ findAbilityEntry,
101
+ resolveAbilityPath,
102
+ resolveAbilityEntry,
103
+ getInstalledAbilityNames,
104
+ } from './lockfile.js';
105
+
106
+ // Transports (for advanced use cases)
107
+ export { loadNativeTransport } from './transports/native.js';
108
+ export { loadStdioTransport } from './transports/stdio.js';
109
+ export { loadBrokerTransport } from './transports/broker.js';
110
+ export type { BrokerTransportOptions } from './transports/broker.js';
111
+
112
+ // Crypto utilities (Ed25519 to X25519 conversion)
113
+ export { convertToEncryptionKey, convertToEncryptionKeyPair } from './crypto.js';
114
+ export type { EncryptionKeyPair } from './crypto.js';