kadi-deploy 0.19.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/.env.example +6 -0
- package/.prettierrc +6 -0
- package/README.md +589 -0
- package/agent.json +23 -0
- package/index.js +11 -0
- package/package.json +42 -0
- package/quick-command.txt +92 -0
- package/scripts/preflight.js +458 -0
- package/scripts/preflight.sh +300 -0
- package/src/cli/bid-selector.ts +222 -0
- package/src/cli/colors.ts +216 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/prompts.ts +190 -0
- package/src/cli/spinners.ts +165 -0
- package/src/commands/deploy-local.ts +475 -0
- package/src/commands/deploy.ts +1342 -0
- package/src/commands/down.ts +679 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/lock.ts +571 -0
- package/src/config/agent-loader.ts +177 -0
- package/src/config/index.ts +9 -0
- package/src/display/deployment-info.ts +220 -0
- package/src/display/pricing.ts +137 -0
- package/src/display/resources.ts +234 -0
- package/src/enhanced-registry-manager.ts +892 -0
- package/src/index.ts +307 -0
- package/src/infrastructure/registry.ts +269 -0
- package/src/schemas/profiles.ts +529 -0
- package/src/secrets/broker-urls.ts +109 -0
- package/src/secrets/handshake.ts +407 -0
- package/src/secrets/index.ts +69 -0
- package/src/secrets/inject-env.ts +171 -0
- package/src/secrets/nonce.ts +31 -0
- package/src/secrets/normalize.ts +204 -0
- package/src/secrets/prepare.ts +152 -0
- package/src/secrets/validate.ts +243 -0
- package/src/secrets/vault.ts +80 -0
- package/src/types/akash.ts +116 -0
- package/src/types/container-registry-ability.d.ts +158 -0
- package/src/types/external.ts +49 -0
- package/src/types.ts +211 -0
- package/src/utils/akt-price.ts +74 -0
- package/tests/agent-loader.test.ts +239 -0
- package/tests/autonomous.test.ts +244 -0
- package/tests/down.test.ts +1143 -0
- package/tests/lock.test.ts +1148 -0
- package/tests/nonce.test.ts +34 -0
- package/tests/normalize.test.ts +270 -0
- package/tests/secrets-schema.test.ts +301 -0
- package/tests/types.test.ts +198 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod Schemas for Profile Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates agent.json deploy profiles at runtime to catch configuration errors early.
|
|
5
|
+
* Provides clear error messages when profiles are malformed or missing required fields.
|
|
6
|
+
*
|
|
7
|
+
* @module schemas/profiles
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import type {
|
|
12
|
+
AkashRegion,
|
|
13
|
+
AkashTier,
|
|
14
|
+
} from '@kadi.build/deploy-ability';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Port exposure configuration for services
|
|
18
|
+
*
|
|
19
|
+
* Supports both simple string format and Akash SDL object format for 'to' field
|
|
20
|
+
*/
|
|
21
|
+
const PortExposeSchema = z.object({
|
|
22
|
+
/** Port number to expose */
|
|
23
|
+
port: z.number().int().min(1).max(65535),
|
|
24
|
+
|
|
25
|
+
/** Port number to expose as (required - be explicit about external port) */
|
|
26
|
+
as: z.number().int().min(1).max(65535),
|
|
27
|
+
|
|
28
|
+
/** Where to expose the port - defaults to local only */
|
|
29
|
+
to: z.array(
|
|
30
|
+
z.union([
|
|
31
|
+
z.string(),
|
|
32
|
+
z.object({ global: z.boolean() }).passthrough(),
|
|
33
|
+
z.object({ service: z.string() }).passthrough()
|
|
34
|
+
])
|
|
35
|
+
).default([{ global: false }]),
|
|
36
|
+
|
|
37
|
+
/** Protocol (tcp or udp) */
|
|
38
|
+
proto: z.enum(['tcp', 'udp']).optional(),
|
|
39
|
+
|
|
40
|
+
/** Enable global exposure (alternative to using 'to' field) */
|
|
41
|
+
global: z.boolean().optional(),
|
|
42
|
+
|
|
43
|
+
/** Accept list for global exposure */
|
|
44
|
+
accept: z.array(z.string()).optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Registry credentials for private images
|
|
49
|
+
*/
|
|
50
|
+
const RegistryCredentialsSchema = z.object({
|
|
51
|
+
/** Registry host (e.g., "ghcr.io") */
|
|
52
|
+
host: z.string(),
|
|
53
|
+
|
|
54
|
+
/** Registry username */
|
|
55
|
+
username: z.string(),
|
|
56
|
+
|
|
57
|
+
/** Registry password or token */
|
|
58
|
+
password: z.string(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Persistent volume specification for Akash Network
|
|
63
|
+
*/
|
|
64
|
+
const PersistentVolumeSchema = z.object({
|
|
65
|
+
/** Volume name (must be unique within service) */
|
|
66
|
+
name: z.string(),
|
|
67
|
+
|
|
68
|
+
/** Volume size (e.g., "10Gi") */
|
|
69
|
+
size: z.string(),
|
|
70
|
+
|
|
71
|
+
/** Container mount path (e.g., "/data") */
|
|
72
|
+
mount: z.string(),
|
|
73
|
+
|
|
74
|
+
/** Storage class: beta1 (HDD), beta2 (SSD), beta3 (NVMe), ram */
|
|
75
|
+
class: z.enum(['beta1', 'beta2', 'beta3', 'ram']).optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* GPU configuration schema (shared between Akash and Local)
|
|
80
|
+
*
|
|
81
|
+
* If requesting GPU, you must specify vendor and model details.
|
|
82
|
+
* Example: { units: 1, attributes: { vendor: { nvidia: [{ model: "rtx4090" }] } } }
|
|
83
|
+
*/
|
|
84
|
+
const GpuConfigSchema = z.object({
|
|
85
|
+
/** Number of GPU units */
|
|
86
|
+
units: z.number().int().positive(),
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* GPU attributes - REQUIRED when requesting GPU
|
|
90
|
+
* Specifies vendor and model requirements
|
|
91
|
+
*/
|
|
92
|
+
attributes: z.object({
|
|
93
|
+
vendor: z.record(
|
|
94
|
+
z.string(),
|
|
95
|
+
z.array(
|
|
96
|
+
z.object({
|
|
97
|
+
/** GPU model - required so you know what you're getting */
|
|
98
|
+
model: z.string(),
|
|
99
|
+
ram: z.string().optional(),
|
|
100
|
+
interface: z.enum(['pcie', 'sxm']).optional(),
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resource configuration for Akash deployments
|
|
109
|
+
*
|
|
110
|
+
* cpu and memory are REQUIRED for Akash since the network needs to
|
|
111
|
+
* know what resources to allocate.
|
|
112
|
+
*/
|
|
113
|
+
const AkashResourceConfigSchema = z.object({
|
|
114
|
+
/** CPU units (e.g., 0.5 for half a CPU) - REQUIRED */
|
|
115
|
+
cpu: z.number().positive(),
|
|
116
|
+
|
|
117
|
+
/** Memory (RAM) with units (e.g., "512Mi", "2Gi") - REQUIRED */
|
|
118
|
+
memory: z.string(),
|
|
119
|
+
|
|
120
|
+
/** Ephemeral storage for container root filesystem (e.g., "512Mi", "1Gi") */
|
|
121
|
+
ephemeralStorage: z.string().optional(),
|
|
122
|
+
|
|
123
|
+
/** Persistent volumes that survive container restarts */
|
|
124
|
+
persistentVolumes: z.array(PersistentVolumeSchema).optional(),
|
|
125
|
+
|
|
126
|
+
/** GPU configuration */
|
|
127
|
+
gpu: GpuConfigSchema.optional(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resource configuration for Local deployments
|
|
132
|
+
*
|
|
133
|
+
* All fields optional since Docker/Podman handle defaults.
|
|
134
|
+
*/
|
|
135
|
+
const LocalResourceConfigSchema = z.object({
|
|
136
|
+
/** CPU units (e.g., 0.5 for half a CPU) */
|
|
137
|
+
cpu: z.number().positive().optional(),
|
|
138
|
+
|
|
139
|
+
/** Memory (RAM) with units (e.g., "512Mi", "2Gi") */
|
|
140
|
+
memory: z.string().optional(),
|
|
141
|
+
|
|
142
|
+
/** Ephemeral storage for container root filesystem (e.g., "512Mi", "1Gi") */
|
|
143
|
+
ephemeralStorage: z.string().optional(),
|
|
144
|
+
|
|
145
|
+
/** Persistent volumes that survive container restarts */
|
|
146
|
+
persistentVolumes: z.array(PersistentVolumeSchema).optional(),
|
|
147
|
+
|
|
148
|
+
/** GPU configuration */
|
|
149
|
+
gpu: GpuConfigSchema.optional(),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Akash pricing configuration
|
|
154
|
+
*/
|
|
155
|
+
const AkashPricingSchema = z.object({
|
|
156
|
+
/** Denomination (e.g., "uakt") */
|
|
157
|
+
denom: z.string(),
|
|
158
|
+
|
|
159
|
+
/** Amount (number or string) */
|
|
160
|
+
amount: z.union([z.number(), z.string()]),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Valid Akash region values (typed for validation)
|
|
165
|
+
*
|
|
166
|
+
* These are the **actual values used by providers on Akash mainnet**,
|
|
167
|
+
* not the official schema values. Based on provider query results.
|
|
168
|
+
*
|
|
169
|
+
* Provider counts (as of query):
|
|
170
|
+
* - us-west: 5, us-central: 4, us-east: 3
|
|
171
|
+
* - eu-central: 2, ca-central: 2, ca-east: 2
|
|
172
|
+
* - Others: 1-2 each
|
|
173
|
+
*/
|
|
174
|
+
const VALID_REGIONS: readonly AkashRegion[] = [
|
|
175
|
+
// United States (most common)
|
|
176
|
+
'us-west', 'us-central', 'us-east', 'us-west-1', 'us',
|
|
177
|
+
// Canada
|
|
178
|
+
'ca-central', 'ca-east', 'ca',
|
|
179
|
+
// Europe
|
|
180
|
+
'eu-central', 'eu-west', 'eu-east', 'europe', 'eu',
|
|
181
|
+
// Asia
|
|
182
|
+
'asia-east', 'singapore',
|
|
183
|
+
// Other regions (less common)
|
|
184
|
+
'westmidlands', 'westeurope', 'westcoast',
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Valid Akash tier values (typed for validation)
|
|
189
|
+
*
|
|
190
|
+
* Provider tiers are one of the most commonly used attributes (37+ providers).
|
|
191
|
+
* - community: 35 providers (standard pricing)
|
|
192
|
+
* - premium: 2 providers (higher SLA)
|
|
193
|
+
*/
|
|
194
|
+
const VALID_TIERS: readonly AkashTier[] = [
|
|
195
|
+
'community', 'premium',
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Akash placement attributes for geographic targeting
|
|
200
|
+
*
|
|
201
|
+
* Based on **actual provider usage on mainnet**, not the official schema.
|
|
202
|
+
* Only includes attributes that providers actually use.
|
|
203
|
+
*
|
|
204
|
+
* **Available Attributes:**
|
|
205
|
+
* - region: 33 providers use this
|
|
206
|
+
* - tier: 37 providers use this
|
|
207
|
+
*/
|
|
208
|
+
const AkashPlacementAttributesSchema = z.object({
|
|
209
|
+
/** Geographic region */
|
|
210
|
+
region: z.string()
|
|
211
|
+
.refine((val): val is AkashRegion => VALID_REGIONS.includes(val as AkashRegion), {
|
|
212
|
+
message: `Invalid region. Valid regions: ${VALID_REGIONS.slice(0, 8).join(', ')}... (${VALID_REGIONS.length} total). Examples: 'us-west' (5 providers), 'us-central' (4 providers), 'eu-central' (2 providers)`,
|
|
213
|
+
})
|
|
214
|
+
.optional(),
|
|
215
|
+
|
|
216
|
+
/** Provider tier (community or premium) */
|
|
217
|
+
tier: z.string()
|
|
218
|
+
.refine((val): val is AkashTier => VALID_TIERS.includes(val as AkashTier), {
|
|
219
|
+
message: `Invalid tier. Valid tiers: ${VALID_TIERS.join(', ')}. Note: 'community' (35 providers), 'premium' (2 providers)`,
|
|
220
|
+
})
|
|
221
|
+
.optional(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Auditor signature requirements for provider attributes
|
|
226
|
+
*/
|
|
227
|
+
const SignedBySchema = z.object({
|
|
228
|
+
/** All of these auditors must have signed */
|
|
229
|
+
allOf: z.array(z.string()).optional(),
|
|
230
|
+
|
|
231
|
+
/** Any of these auditors must have signed */
|
|
232
|
+
anyOf: z.array(z.string()).optional(),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Vault name validation (reused across schemas)
|
|
237
|
+
*/
|
|
238
|
+
const VaultNameSchema = z.string()
|
|
239
|
+
.min(1, 'Vault name cannot be empty')
|
|
240
|
+
.regex(/^[a-zA-Z0-9_-]+$/, 'Vault name must contain only alphanumeric characters, hyphens, and underscores');
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Individual vault source for multi-vault secrets configuration.
|
|
244
|
+
* Each entry specifies a vault and its associated secret keys.
|
|
245
|
+
*/
|
|
246
|
+
const VaultSourceSchema = z.object({
|
|
247
|
+
/** Vault name to read secrets from (must exist in kadi-secret) */
|
|
248
|
+
vault: VaultNameSchema,
|
|
249
|
+
|
|
250
|
+
/** Secret names that must exist before deployment (deploy fails if missing) */
|
|
251
|
+
required: z.array(z.string()).optional(),
|
|
252
|
+
|
|
253
|
+
/** Secret names that will be shared if available (no error if missing) */
|
|
254
|
+
optional: z.array(z.string()).optional(),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Secrets configuration for deployment
|
|
259
|
+
*
|
|
260
|
+
* Declares which secrets the deployed agent needs. These are validated
|
|
261
|
+
* before deployment to ensure they exist in the specified vault(s) (via kadi-secret).
|
|
262
|
+
*
|
|
263
|
+
* Supports two formats:
|
|
264
|
+
*
|
|
265
|
+
* **Legacy (single vault):**
|
|
266
|
+
* ```json
|
|
267
|
+
* { "vault": "my-app", "required": ["API_KEY"], "delivery": "broker" }
|
|
268
|
+
* ```
|
|
269
|
+
*
|
|
270
|
+
* **Multi-vault:**
|
|
271
|
+
* ```json
|
|
272
|
+
* {
|
|
273
|
+
* "vaults": [
|
|
274
|
+
* { "vault": "my-app", "required": ["API_KEY"] },
|
|
275
|
+
* { "vault": "infra", "required": ["TUNNEL_TOKEN"] }
|
|
276
|
+
* ],
|
|
277
|
+
* "delivery": "broker"
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
280
|
+
*
|
|
281
|
+
* Delivery modes:
|
|
282
|
+
* - "env" (default): Secrets injected as plain environment variables
|
|
283
|
+
* - "broker": E2E encrypted handshake via broker (requires KADI CLI/SDK in container)
|
|
284
|
+
*/
|
|
285
|
+
const SecretsSchema = z.union([
|
|
286
|
+
// Legacy single-vault format (backwards compatible)
|
|
287
|
+
z.object({
|
|
288
|
+
/** Vault name to read secrets from (must exist in kadi-secret) */
|
|
289
|
+
vault: VaultNameSchema,
|
|
290
|
+
|
|
291
|
+
/** Secret names that must exist before deployment (deploy fails if missing) */
|
|
292
|
+
required: z.array(z.string()).optional(),
|
|
293
|
+
|
|
294
|
+
/** Secret names that will be shared if available (no error if missing) */
|
|
295
|
+
optional: z.array(z.string()).optional(),
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Secret delivery mode:
|
|
299
|
+
* - "env": Inject secrets as plain environment variables (works with any container)
|
|
300
|
+
* - "broker": E2E encrypted handshake via broker (requires KADI CLI or SDK)
|
|
301
|
+
* Default: "env"
|
|
302
|
+
*/
|
|
303
|
+
delivery: z.enum(['env', 'broker']).optional(),
|
|
304
|
+
}),
|
|
305
|
+
|
|
306
|
+
// New multi-vault format
|
|
307
|
+
z.object({
|
|
308
|
+
/** Array of vault sources, each with their own required/optional keys */
|
|
309
|
+
vaults: z.array(VaultSourceSchema).min(1, 'At least one vault source is required'),
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Secret delivery mode (applies to all vaults):
|
|
313
|
+
* - "env": Inject secrets as plain environment variables (works with any container)
|
|
314
|
+
* - "broker": E2E encrypted handshake via broker (requires KADI CLI or SDK)
|
|
315
|
+
* Default: "env"
|
|
316
|
+
*/
|
|
317
|
+
delivery: z.enum(['env', 'broker']).optional(),
|
|
318
|
+
}),
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Akash service definition
|
|
323
|
+
*/
|
|
324
|
+
const AkashServiceSchema = z.object({
|
|
325
|
+
/** Container image (e.g., "nginx:latest") */
|
|
326
|
+
image: z.string(),
|
|
327
|
+
|
|
328
|
+
/** Optional command override */
|
|
329
|
+
command: z.array(z.string()).optional(),
|
|
330
|
+
|
|
331
|
+
/** Environment variables (array or object format) */
|
|
332
|
+
env: z.union([
|
|
333
|
+
z.array(z.string()),
|
|
334
|
+
z.record(z.string(), z.string())
|
|
335
|
+
]).optional(),
|
|
336
|
+
|
|
337
|
+
/** Port exposures */
|
|
338
|
+
expose: z.array(PortExposeSchema).optional(),
|
|
339
|
+
|
|
340
|
+
/** Registry credentials for private images */
|
|
341
|
+
credentials: RegistryCredentialsSchema.optional(),
|
|
342
|
+
|
|
343
|
+
/** Resource requirements */
|
|
344
|
+
resources: AkashResourceConfigSchema.optional(),
|
|
345
|
+
|
|
346
|
+
/** Pricing configuration */
|
|
347
|
+
pricing: AkashPricingSchema.optional(),
|
|
348
|
+
|
|
349
|
+
/** Number of instances */
|
|
350
|
+
count: z.number().int().positive().optional(),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Local service definition (for Docker/Podman)
|
|
355
|
+
*/
|
|
356
|
+
const LocalServiceSchema = z.object({
|
|
357
|
+
/** Container image (e.g., "nginx:latest") */
|
|
358
|
+
image: z.string(),
|
|
359
|
+
|
|
360
|
+
/** Optional command override */
|
|
361
|
+
command: z.array(z.string()).optional(),
|
|
362
|
+
|
|
363
|
+
/** Environment variables (array format) */
|
|
364
|
+
env: z.array(z.string()).optional(),
|
|
365
|
+
|
|
366
|
+
/** Port exposures */
|
|
367
|
+
expose: z.array(PortExposeSchema).optional(),
|
|
368
|
+
|
|
369
|
+
/** Resource requirements */
|
|
370
|
+
resources: LocalResourceConfigSchema.optional(),
|
|
371
|
+
|
|
372
|
+
/** Docker/Podman volume mounts (e.g., "/host/path:/container/path:ro") */
|
|
373
|
+
volumes: z.array(z.string()).optional(),
|
|
374
|
+
|
|
375
|
+
/** Working directory inside the container */
|
|
376
|
+
workingDir: z.string().optional(),
|
|
377
|
+
|
|
378
|
+
/** Services this service depends on */
|
|
379
|
+
dependsOn: z.array(z.string()).optional(),
|
|
380
|
+
|
|
381
|
+
/** Restart policy (e.g., "unless-stopped", "always", "on-failure") */
|
|
382
|
+
restart: z.string().optional(),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Akash deployment profile schema
|
|
387
|
+
*/
|
|
388
|
+
export const AkashProfileSchema = z.object({
|
|
389
|
+
/** Deployment target (must be 'akash') */
|
|
390
|
+
target: z.literal('akash'),
|
|
391
|
+
|
|
392
|
+
/** Network to deploy to */
|
|
393
|
+
network: z.enum(['mainnet', 'testnet']),
|
|
394
|
+
|
|
395
|
+
/** Container engine for local operations */
|
|
396
|
+
engine: z.enum(['docker', 'podman']).optional(),
|
|
397
|
+
|
|
398
|
+
/** Service definitions */
|
|
399
|
+
services: z.record(z.string(), AkashServiceSchema),
|
|
400
|
+
|
|
401
|
+
/** Geographic and facility placement constraints for deployment targeting */
|
|
402
|
+
placement: AkashPlacementAttributesSchema.optional(),
|
|
403
|
+
|
|
404
|
+
/** Custom provider attributes (advanced - prefer using 'placement' for geographic targeting) */
|
|
405
|
+
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
406
|
+
|
|
407
|
+
/** Auditor signature requirements for provider attributes */
|
|
408
|
+
signedBy: SignedBySchema.optional(),
|
|
409
|
+
|
|
410
|
+
/** WalletConnect project ID */
|
|
411
|
+
walletConnectProjectId: z.string().optional(),
|
|
412
|
+
|
|
413
|
+
/** Path to certificate file */
|
|
414
|
+
cert: z.string().optional(),
|
|
415
|
+
|
|
416
|
+
/** Provider blacklist */
|
|
417
|
+
blacklist: z.array(z.string()).optional(),
|
|
418
|
+
|
|
419
|
+
/** Deployment deposit in AKT (default: 5 AKT) */
|
|
420
|
+
deposit: z.coerce.number().positive().optional(),
|
|
421
|
+
|
|
422
|
+
/** CLI options */
|
|
423
|
+
verbose: z.boolean().optional(),
|
|
424
|
+
showCommands: z.boolean().optional(),
|
|
425
|
+
yes: z.boolean().optional(),
|
|
426
|
+
dryRun: z.boolean().optional(),
|
|
427
|
+
|
|
428
|
+
/** Secrets to validate and share with the deployed agent */
|
|
429
|
+
secrets: SecretsSchema.optional(),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Local deployment profile schema
|
|
434
|
+
*/
|
|
435
|
+
export const LocalProfileSchema = z.object({
|
|
436
|
+
/** Deployment target (must be 'local') */
|
|
437
|
+
target: z.literal('local'),
|
|
438
|
+
|
|
439
|
+
/** Container engine (required) */
|
|
440
|
+
engine: z.enum(['docker', 'podman']),
|
|
441
|
+
|
|
442
|
+
/** Docker network name */
|
|
443
|
+
network: z.string().optional(),
|
|
444
|
+
|
|
445
|
+
/** Service definitions */
|
|
446
|
+
services: z.record(z.string(), LocalServiceSchema),
|
|
447
|
+
|
|
448
|
+
/** CLI options */
|
|
449
|
+
verbose: z.boolean().optional(),
|
|
450
|
+
showCommands: z.boolean().optional(),
|
|
451
|
+
yes: z.boolean().optional(),
|
|
452
|
+
dryRun: z.boolean().optional(),
|
|
453
|
+
|
|
454
|
+
/** Secrets to validate and share with the deployed agent */
|
|
455
|
+
secrets: SecretsSchema.optional(),
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Union of all profile types
|
|
460
|
+
*/
|
|
461
|
+
export const ProfileSchema = z.union([
|
|
462
|
+
AkashProfileSchema,
|
|
463
|
+
LocalProfileSchema,
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Inferred TypeScript types from Zod schemas
|
|
468
|
+
*/
|
|
469
|
+
export type AkashProfile = z.infer<typeof AkashProfileSchema>;
|
|
470
|
+
export type LocalProfile = z.infer<typeof LocalProfileSchema>;
|
|
471
|
+
export type Profile = z.infer<typeof ProfileSchema>;
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Validate and parse a profile from agent.json
|
|
475
|
+
*
|
|
476
|
+
* @param profileData - Raw profile data from agent.json
|
|
477
|
+
* @param profileName - Name of the profile (for error messages)
|
|
478
|
+
* @returns Validated profile
|
|
479
|
+
* @throws ZodError with detailed validation errors
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```typescript
|
|
483
|
+
* try {
|
|
484
|
+
* const profile = validateProfile(rawData, 'production');
|
|
485
|
+
* // profile is now fully typed and validated
|
|
486
|
+
* } catch (error) {
|
|
487
|
+
* if (error instanceof z.ZodError) {
|
|
488
|
+
* console.error('Profile validation failed:', error.format());
|
|
489
|
+
* }
|
|
490
|
+
* }
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
export function validateProfile(profileData: unknown, profileName: string): Profile {
|
|
494
|
+
try {
|
|
495
|
+
return ProfileSchema.parse(profileData);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (error instanceof z.ZodError) {
|
|
498
|
+
// Add context to error message with detailed issue information
|
|
499
|
+
const formattedErrors = error.issues.map((issue: z.ZodIssue) => {
|
|
500
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : 'root';
|
|
501
|
+
const expected = 'expected' in issue ? ` (expected: ${JSON.stringify(issue.expected)})` : '';
|
|
502
|
+
const received = 'received' in issue ? ` (received: ${JSON.stringify(issue.received)})` : '';
|
|
503
|
+
return ` - ${path}: ${issue.message}${expected}${received}`;
|
|
504
|
+
}).join('\n');
|
|
505
|
+
|
|
506
|
+
// Also log the raw profile data for debugging
|
|
507
|
+
console.error('[DEBUG] Raw profile data:', JSON.stringify(profileData, null, 2));
|
|
508
|
+
|
|
509
|
+
throw new Error(
|
|
510
|
+
`Profile "${profileName}" validation failed:\n${formattedErrors}`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Type guard to check if profile is an Akash profile
|
|
519
|
+
*/
|
|
520
|
+
export function isAkashProfile(profile: Profile): profile is AkashProfile {
|
|
521
|
+
return profile.target === 'akash';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Type guard to check if profile is a Local profile
|
|
526
|
+
*/
|
|
527
|
+
export function isLocalProfile(profile: Profile): profile is LocalProfile {
|
|
528
|
+
return profile.target === 'local';
|
|
529
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broker URL Resolution for Secrets Injection
|
|
3
|
+
*
|
|
4
|
+
* Resolves broker URLs from environment or agent.json configuration.
|
|
5
|
+
* The deployed agent needs broker URLs to request secrets from the deployer.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order:
|
|
8
|
+
* 1. KADI_BROKER_URLS environment variable (comma-separated)
|
|
9
|
+
* 2. agent.json brokers field (object mapping names to URLs)
|
|
10
|
+
* 3. Fail fast with helpful error if neither found
|
|
11
|
+
*
|
|
12
|
+
* @module secrets/broker-urls
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { AgentConfig } from '../types.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Result of broker URL resolution
|
|
19
|
+
*/
|
|
20
|
+
export interface BrokerUrlsResult {
|
|
21
|
+
/** Whether broker URLs were found */
|
|
22
|
+
found: boolean;
|
|
23
|
+
/** Comma-separated broker URLs (for KADI_BROKER_URLS env var) */
|
|
24
|
+
urls: string;
|
|
25
|
+
/** Source of the URLs for logging */
|
|
26
|
+
source: 'env' | 'agent.json' | 'none';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve broker URLs from environment or agent.json
|
|
31
|
+
*
|
|
32
|
+
* @param agentConfig - Loaded agent.json configuration
|
|
33
|
+
* @returns Resolution result with URLs and source
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const result = resolveBrokerUrls(agentConfig);
|
|
38
|
+
*
|
|
39
|
+
* if (!result.found) {
|
|
40
|
+
* throw new Error('No broker URLs configured');
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* // Inject into container env
|
|
44
|
+
* env.push(`KADI_BROKER_URLS=${result.urls}`);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function resolveBrokerUrls(agentConfig: AgentConfig): BrokerUrlsResult {
|
|
48
|
+
// 1. Check KADI_BROKER_URLS env var first (takes precedence)
|
|
49
|
+
const envUrls = process.env.KADI_BROKER_URLS;
|
|
50
|
+
if (envUrls) {
|
|
51
|
+
const urls = envUrls
|
|
52
|
+
.split(',')
|
|
53
|
+
.map(u => u.trim())
|
|
54
|
+
.filter(u => u.length > 0);
|
|
55
|
+
|
|
56
|
+
if (urls.length > 0) {
|
|
57
|
+
return {
|
|
58
|
+
found: true,
|
|
59
|
+
urls: urls.join(','),
|
|
60
|
+
source: 'env',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Fall back to agent.json brokers field
|
|
66
|
+
const brokers = agentConfig.brokers;
|
|
67
|
+
if (brokers && typeof brokers === 'object') {
|
|
68
|
+
const urls = Object.values(brokers).filter(
|
|
69
|
+
(url): url is string => typeof url === 'string' && url.length > 0
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (urls.length > 0) {
|
|
73
|
+
return {
|
|
74
|
+
found: true,
|
|
75
|
+
urls: urls.join(','),
|
|
76
|
+
source: 'agent.json',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Not found
|
|
82
|
+
return {
|
|
83
|
+
found: false,
|
|
84
|
+
urls: '',
|
|
85
|
+
source: 'none',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format error message for missing broker URLs
|
|
91
|
+
*
|
|
92
|
+
* @returns Formatted error message with instructions
|
|
93
|
+
*/
|
|
94
|
+
export function formatMissingBrokerUrlsError(): string {
|
|
95
|
+
return `No broker URLs configured.
|
|
96
|
+
|
|
97
|
+
The deployed agent needs broker URLs to request secrets.
|
|
98
|
+
Configure them in one of these ways:
|
|
99
|
+
|
|
100
|
+
1. Set KADI_BROKER_URLS environment variable:
|
|
101
|
+
export KADI_BROKER_URLS=ws://broker.example.com:8080/kadi
|
|
102
|
+
|
|
103
|
+
2. Add brokers to agent.json:
|
|
104
|
+
{
|
|
105
|
+
"brokers": {
|
|
106
|
+
"default": "ws://broker.example.com:8080/kadi"
|
|
107
|
+
}
|
|
108
|
+
}`;
|
|
109
|
+
}
|