@agents-at-scale/ark 0.1.60 → 0.1.62

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.
@@ -13,6 +13,7 @@ export declare const arkDependencies: DependencyCollection;
13
13
  */
14
14
  export declare const arkServices: ServiceCollection;
15
15
  /**
16
- * Get services that can be installed via Helm charts (only enabled services)
16
+ * Get services that can be installed via Helm charts (only enabled services).
17
+ * When a backend is specified, services with a non-matching requiresBackend are excluded.
17
18
  */
18
- export declare function getInstallableServices(): ServiceCollection;
19
+ export declare function getInstallableServices(backend?: 'etcd' | 'postgresql'): ServiceCollection;
@@ -74,6 +74,19 @@ const defaultArkServices = {
74
74
  k8sDeploymentName: 'ark-controller',
75
75
  k8sDevDeploymentName: 'ark-controller-devspace',
76
76
  },
77
+ 'ark-apiserver': {
78
+ name: 'ark-apiserver',
79
+ helmReleaseName: 'ark-apiserver',
80
+ description: 'Aggregated API server serving ark.mckinsey.com APIs from PostgreSQL',
81
+ enabled: true,
82
+ mandatory: true,
83
+ category: 'core',
84
+ namespace: 'ark-system',
85
+ chartPath: `${REGISTRY_BASE}/ark-apiserver:${CHART_VERSION}`,
86
+ installArgs: ['--create-namespace'],
87
+ k8sDeploymentName: 'ark-apiserver',
88
+ requiresBackend: 'postgresql',
89
+ },
77
90
  'ark-completions': {
78
91
  name: 'ark-completions',
79
92
  helmReleaseName: 'ark-completions',
@@ -208,14 +221,17 @@ function applyConfigOverrides(defaults) {
208
221
  */
209
222
  export const arkServices = applyConfigOverrides(defaultArkServices);
210
223
  /**
211
- * Get services that can be installed via Helm charts (only enabled services)
224
+ * Get services that can be installed via Helm charts (only enabled services).
225
+ * When a backend is specified, services with a non-matching requiresBackend are excluded.
212
226
  */
213
- export function getInstallableServices() {
227
+ export function getInstallableServices(backend = 'etcd') {
214
228
  const installable = {};
215
229
  for (const [key, service] of Object.entries(arkServices)) {
216
- if (service.enabled && service.chartPath) {
217
- installable[key] = service;
218
- }
230
+ if (!service.enabled || !service.chartPath)
231
+ continue;
232
+ if (service.requiresBackend && service.requiresBackend !== backend)
233
+ continue;
234
+ installable[key] = service;
219
235
  }
220
236
  return installable;
221
237
  }
@@ -10,6 +10,7 @@ import { printNextSteps } from '../../lib/nextSteps.js';
10
10
  import ora from 'ora';
11
11
  import { waitForServicesReady, } from '../../lib/waitForReady.js';
12
12
  import { parseTimeoutToSeconds } from '../../lib/timeout.js';
13
+ import { detectStorageBackend } from '../../lib/readinessChecks.js';
13
14
  function isValidVersion(version) {
14
15
  return /^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/.test(version);
15
16
  }
@@ -75,7 +76,7 @@ async function checkAndCleanFailedRelease(releaseName, namespace, verbose = fals
75
76
  // Ignore errors - prerequisite may not exist
76
77
  }
77
78
  }
78
- async function installService(service, verbose = false, arkVersionOverride, marketplaceVersionOverride) {
79
+ async function installService(service, verbose = false, arkVersionOverride, marketplaceVersionOverride, backend) {
79
80
  await uninstallPrerequisites(service, verbose);
80
81
  await checkAndCleanFailedRelease(service.helmReleaseName, service.namespace, verbose);
81
82
  let chartPath = service.chartPath;
@@ -140,6 +141,7 @@ export async function installArk(config, serviceNames = [], options = {}) {
140
141
  // Show cluster info
141
142
  output.success(`connected to cluster: ${chalk.bold(clusterInfo.context)}`);
142
143
  console.log(); // Add blank line after cluster info
144
+ const backend = await detectStorageBackend();
143
145
  // If specific services are requested, install only those services
144
146
  if (serviceNames.length > 0) {
145
147
  for (const serviceName of serviceNames) {
@@ -185,7 +187,7 @@ export async function installArk(config, serviceNames = [], options = {}) {
185
187
  continue;
186
188
  }
187
189
  // Core ARK service
188
- const services = getInstallableServices();
190
+ const services = getInstallableServices(backend);
189
191
  const service = Object.values(services).find((s) => s.name === serviceName);
190
192
  if (!service) {
191
193
  output.error(`service '${serviceName}' not found`);
@@ -210,11 +212,19 @@ export async function installArk(config, serviceNames = [], options = {}) {
210
212
  }
211
213
  // If not using -y flag, show checklist interface
212
214
  if (!options.yes) {
215
+ const backendMatch = (s) => !s.requiresBackend || s.requiresBackend === backend;
213
216
  const coreServices = Object.values(arkServices)
214
- .filter((s) => s.category === 'core')
215
- .sort((a, b) => a.name.localeCompare(b.name));
217
+ .filter((s) => s.category === 'core' && backendMatch(s))
218
+ .sort((a, b) => {
219
+ // Ensure ark-controller is always first
220
+ if (a.name === 'ark-controller')
221
+ return -1;
222
+ if (b.name === 'ark-controller')
223
+ return 1;
224
+ return a.name.localeCompare(b.name);
225
+ });
216
226
  const otherServices = Object.values(arkServices)
217
- .filter((s) => s.category === 'service')
227
+ .filter((s) => s.category === 'service' && backendMatch(s))
218
228
  .sort((a, b) => a.name.localeCompare(b.name));
219
229
  const mandatoryServiceNames = [...coreServices, ...otherServices]
220
230
  .filter((s) => s.mandatory)
@@ -361,8 +371,16 @@ export async function installArk(config, serviceNames = [], options = {}) {
361
371
  }
362
372
  }
363
373
  // Install all services
364
- const services = getInstallableServices();
365
- for (const service of Object.values(services)) {
374
+ const services = getInstallableServices(backend);
375
+ const sortedServices = Object.values(services).sort((a, b) => {
376
+ // Ensure ark-controller is always first
377
+ if (a.name === 'ark-controller')
378
+ return -1;
379
+ if (b.name === 'ark-controller')
380
+ return 1;
381
+ return a.name.localeCompare(b.name);
382
+ });
383
+ for (const service of sortedServices) {
366
384
  output.info(`installing ${service.name}...`);
367
385
  try {
368
386
  await installService(service, options.verbose, options.arkVersion, options.marketplaceVersion);
@@ -388,7 +406,8 @@ export async function installArk(config, serviceNames = [], options = {}) {
388
406
  const servicesToWait = Object.values(arkServices).filter((s) => s.enabled &&
389
407
  s.category === 'core' &&
390
408
  s.k8sDeploymentName &&
391
- s.namespace);
409
+ s.namespace &&
410
+ (!s.requiresBackend || s.requiresBackend === backend));
392
411
  const spinner = ora(`Waiting for Ark to be ready (timeout: ${timeoutSeconds}s)...`).start();
393
412
  const statusMap = new Map();
394
413
  servicesToWait.forEach((s) => statusMap.set(s.name, false));
@@ -5,6 +5,7 @@ import { StatusChecker } from '../../components/statusChecker.js';
5
5
  import { StatusFormatter, } from '../../ui/statusFormatter.js';
6
6
  import { fetchVersionInfo } from '../../lib/versions.js';
7
7
  import { waitForServicesReady, } from '../../lib/waitForReady.js';
8
+ import { runReadinessChecks, detectStorageBackend, } from '../../lib/readinessChecks.js';
8
9
  import { arkServices } from '../../arkServices.js';
9
10
  import output from '../../lib/output.js';
10
11
  import { parseTimeoutToSeconds } from '../../lib/timeout.js';
@@ -270,13 +271,15 @@ export async function checkStatus(serviceNames, options) {
270
271
  StatusFormatter.printSections(sections);
271
272
  if (options?.waitForReady) {
272
273
  const timeoutSeconds = parseTimeoutToSeconds(options.waitForReady);
274
+ const backend = await detectStorageBackend();
273
275
  let servicesToWait = [];
274
276
  if (serviceNames && serviceNames.length > 0) {
275
277
  servicesToWait = serviceNames
276
278
  .map((name) => Object.values(arkServices).find((s) => s.name === name))
277
279
  .filter((s) => s !== undefined &&
278
280
  s.k8sDeploymentName !== undefined &&
279
- s.namespace !== undefined);
281
+ s.namespace !== undefined &&
282
+ (!s.requiresBackend || s.requiresBackend === backend));
280
283
  if (servicesToWait.length === 0) {
281
284
  output.error(`No valid services found matching: ${serviceNames.join(', ')}`);
282
285
  process.exit(1);
@@ -286,7 +289,8 @@ export async function checkStatus(serviceNames, options) {
286
289
  servicesToWait = Object.values(arkServices).filter((s) => s.enabled &&
287
290
  s.category === 'core' &&
288
291
  s.k8sDeploymentName &&
289
- s.namespace);
292
+ s.namespace &&
293
+ (!s.requiresBackend || s.requiresBackend === backend));
290
294
  }
291
295
  console.log();
292
296
  const waitSpinner = ora(`Waiting for services to be ready (timeout: ${timeoutSeconds}s)...`).start();
@@ -305,14 +309,23 @@ export async function checkStatus(serviceNames, options) {
305
309
  });
306
310
  waitSpinner.text = `Waiting for services to be ready (${elapsed}/${timeoutSeconds}s)...\n${lines.join('\n')}`;
307
311
  });
308
- if (result) {
309
- waitSpinner.succeed('All services are ready');
310
- process.exit(0);
311
- }
312
- else {
312
+ if (!result) {
313
313
  waitSpinner.fail(`Services did not become ready within ${timeoutSeconds} seconds`);
314
314
  process.exit(1);
315
315
  }
316
+ waitSpinner.succeed('All services are ready');
317
+ const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
318
+ const remainingSeconds = Math.max(1, timeoutSeconds - elapsedSeconds);
319
+ const deepResults = await runReadinessChecks(remainingSeconds, (r) => {
320
+ const icon = r.passed ? chalk.green('✓') : chalk.red('✗');
321
+ const dur = `${(r.durationMs / 1000).toFixed(1)}s`;
322
+ const suffix = r.message ? ` — ${r.message}` : '';
323
+ console.log(` ${icon} ${r.name} (${dur})${suffix}`);
324
+ });
325
+ if (deepResults.some((r) => !r.passed)) {
326
+ process.exit(1);
327
+ }
328
+ process.exit(0);
316
329
  }
317
330
  process.exit(0);
318
331
  }
@@ -0,0 +1,10 @@
1
+ export type StorageBackend = 'etcd' | 'postgresql';
2
+ export interface ReadinessCheckResult {
3
+ name: string;
4
+ passed: boolean;
5
+ durationMs: number;
6
+ message?: string;
7
+ }
8
+ export type ReadinessProgress = (result: ReadinessCheckResult) => void;
9
+ export declare function detectStorageBackend(): Promise<StorageBackend>;
10
+ export declare function runReadinessChecks(timeoutSeconds: number, onProgress?: ReadinessProgress): Promise<ReadinessCheckResult[]>;
@@ -0,0 +1,88 @@
1
+ import { execa } from 'execa';
2
+ const API_GROUP_POLL_INTERVAL_MS = 10000;
3
+ function sleep(ms) {
4
+ return new Promise((resolve) => setTimeout(resolve, ms));
5
+ }
6
+ async function runKubectl(args, timeoutMs) {
7
+ const result = await execa('kubectl', args, {
8
+ timeout: timeoutMs,
9
+ reject: false,
10
+ });
11
+ return {
12
+ exitCode: result.exitCode ?? 1,
13
+ stdout: result.stdout ?? '',
14
+ stderr: result.stderr ?? '',
15
+ };
16
+ }
17
+ export async function detectStorageBackend() {
18
+ const { exitCode } = await runKubectl(['get', 'crd', 'agents.ark.mckinsey.com'], 10000);
19
+ return exitCode === 0 ? 'etcd' : 'postgresql';
20
+ }
21
+ async function waitForApiServices(timeoutSeconds) {
22
+ const start = Date.now();
23
+ const primary = await runKubectl([
24
+ 'wait',
25
+ '--for=condition=Available',
26
+ 'apiservice',
27
+ 'v1alpha1.ark.mckinsey.com',
28
+ `--timeout=${timeoutSeconds}s`,
29
+ ], timeoutSeconds * 1000 + 5000);
30
+ await runKubectl([
31
+ 'wait',
32
+ '--for=condition=Available',
33
+ 'apiservice',
34
+ 'v1prealpha1.ark.mckinsey.com',
35
+ '--timeout=30s',
36
+ ], 35000);
37
+ return {
38
+ name: 'APIServices available',
39
+ passed: primary.exitCode === 0,
40
+ durationMs: Date.now() - start,
41
+ message: primary.exitCode === 0
42
+ ? undefined
43
+ : (primary.stderr || primary.stdout).trim(),
44
+ };
45
+ }
46
+ async function waitForApiGroup(timeoutSeconds) {
47
+ const start = Date.now();
48
+ const deadline = start + timeoutSeconds * 1000;
49
+ while (Date.now() < deadline) {
50
+ const { stdout, exitCode } = await runKubectl(['api-resources', '--api-group=ark.mckinsey.com', '-o', 'name'], 10000);
51
+ if (exitCode === 0 && /agents\./.test(stdout)) {
52
+ return {
53
+ name: 'API group registered',
54
+ passed: true,
55
+ durationMs: Date.now() - start,
56
+ };
57
+ }
58
+ await sleep(API_GROUP_POLL_INTERVAL_MS);
59
+ }
60
+ return {
61
+ name: 'API group registered',
62
+ passed: false,
63
+ durationMs: Date.now() - start,
64
+ message: 'timed out waiting for ark.mckinsey.com API group',
65
+ };
66
+ }
67
+ export async function runReadinessChecks(timeoutSeconds, onProgress) {
68
+ const backend = await detectStorageBackend();
69
+ if (backend === 'etcd') {
70
+ return [];
71
+ }
72
+ const overallStart = Date.now();
73
+ const remaining = () => Math.max(1, timeoutSeconds - Math.floor((Date.now() - overallStart) / 1000));
74
+ const checks = [
75
+ () => waitForApiServices(Math.min(remaining(), 120)),
76
+ () => waitForApiGroup(Math.min(remaining(), 300)),
77
+ ];
78
+ const results = [];
79
+ for (const check of checks) {
80
+ const result = await check();
81
+ results.push(result);
82
+ onProgress?.(result);
83
+ if (!result.passed) {
84
+ break;
85
+ }
86
+ }
87
+ return results;
88
+ }
@@ -18,6 +18,7 @@ export interface ArkService {
18
18
  k8sPortForwardLocalPort?: number;
19
19
  k8sDeploymentName?: string;
20
20
  k8sDevDeploymentName?: string;
21
+ requiresBackend?: 'etcd' | 'postgresql';
21
22
  }
22
23
  export interface ServiceCollection {
23
24
  [key: string]: ArkService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-at-scale/ark",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "Ark CLI - Interactive terminal interface for ARK agents",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@kubernetes/client-node": "^1.3.0",
46
46
  "@modelcontextprotocol/sdk": "^1.27.1",
47
- "axios": "^1.15.0",
47
+ "axios": "^1.16.0",
48
48
  "chalk": "^4.1.2",
49
49
  "commander": "^12.1.0",
50
50
  "debug": "^4.4.1",
@@ -91,6 +91,7 @@
91
91
  "@hono/node-server": "^1.19.13",
92
92
  "flatted": "^3.4.2",
93
93
  "tar": "^7.5.11",
94
- "express-rate-limit": "^8.3.0"
94
+ "express-rate-limit": "^8.3.0",
95
+ "fast-uri": "^3.1.1"
95
96
  }
96
97
  }
@@ -763,11 +763,11 @@ wheels = [
763
763
 
764
764
  [[package]]
765
765
  name = "python-dotenv"
766
- version = "1.1.1"
766
+ version = "1.2.2"
767
767
  source = { registry = "https://pypi.org/simple" }
768
- sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
768
+ sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
769
769
  wheels = [
770
- { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
770
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
771
771
  ]
772
772
 
773
773
  [[package]]
@@ -1007,15 +1007,15 @@ wheels = [
1007
1007
 
1008
1008
  [[package]]
1009
1009
  name = "starlette"
1010
- version = "0.47.1"
1010
+ version = "0.49.1"
1011
1011
  source = { registry = "https://pypi.org/simple" }
1012
1012
  dependencies = [
1013
1013
  { name = "anyio" },
1014
1014
  { name = "typing-extensions", marker = "python_full_version < '3.13'" },
1015
1015
  ]
1016
- sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" }
1016
+ sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" }
1017
1017
  wheels = [
1018
- { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" },
1018
+ { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" },
1019
1019
  ]
1020
1020
 
1021
1021
  [[package]]