@agents-at-scale/ark 0.1.63 → 0.1.66

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.
Files changed (38) hide show
  1. package/dist/commands/agents/index.js +13 -1
  2. package/dist/commands/install/index.js +33 -15
  3. package/dist/commands/mcp/authClient.d.ts +37 -0
  4. package/dist/commands/mcp/authClient.js +47 -0
  5. package/dist/commands/mcp/index.d.ts +3 -0
  6. package/dist/commands/mcp/index.js +43 -0
  7. package/dist/commands/mcp/login.d.ts +21 -0
  8. package/dist/commands/mcp/login.js +130 -0
  9. package/dist/commands/mcp/logout.d.ts +16 -0
  10. package/dist/commands/mcp/logout.js +65 -0
  11. package/dist/commands/mcp/namespace.d.ts +1 -0
  12. package/dist/commands/mcp/namespace.js +17 -0
  13. package/dist/commands/mcp/timeout.d.ts +1 -0
  14. package/dist/commands/mcp/timeout.js +23 -0
  15. package/dist/commands/models/kubernetes/manifest-builder.js +30 -18
  16. package/dist/commands/models/kubernetes/secret-manager.js +18 -8
  17. package/dist/commands/models/providers/bedrock.d.ts +7 -2
  18. package/dist/commands/models/providers/bedrock.js +91 -42
  19. package/dist/commands/query/index.js +11 -1
  20. package/dist/commands/status/index.js +39 -5
  21. package/dist/index.js +2 -0
  22. package/dist/lib/arkApiClient.d.ts +4 -0
  23. package/dist/lib/arkApiClient.js +3 -0
  24. package/dist/lib/chatClient.d.ts +2 -0
  25. package/dist/lib/chatClient.js +25 -1
  26. package/dist/lib/executeQuery.d.ts +4 -1
  27. package/dist/lib/executeQuery.js +19 -0
  28. package/dist/lib/readinessChecks.d.ts +10 -2
  29. package/dist/lib/readinessChecks.js +93 -4
  30. package/dist/lib/types.d.ts +5 -0
  31. package/package.json +9 -5
  32. package/templates/mcp-server/Dockerfile +1 -1
  33. package/templates/models/azure.yaml +7 -6
  34. package/templates/models/claude.yaml +1 -1
  35. package/templates/models/gemini.yaml +1 -1
  36. package/templates/models/openai.yaml +1 -1
  37. package/templates/query/query.template.yaml +3 -3
  38. package/templates/tool/uv.lock +60 -60
@@ -23,10 +23,15 @@ export class KubernetesSecretManager {
23
23
  async createNewSecret(config) {
24
24
  const secretArgs = ['create', 'secret', 'generic', config.secretName];
25
25
  if (config.type === 'bedrock') {
26
- secretArgs.push(`--from-literal=access-key-id=${config.accessKeyId}`);
27
- secretArgs.push(`--from-literal=secret-access-key=${config.secretAccessKey}`);
28
- if (config.sessionToken) {
29
- secretArgs.push(`--from-literal=session-token=${config.sessionToken}`);
26
+ if (config.authMethod === 'api-key') {
27
+ secretArgs.push(`--from-literal=bedrock-api-key=${config.apiKey}`);
28
+ }
29
+ else {
30
+ secretArgs.push(`--from-literal=access-key-id=${config.accessKeyId}`);
31
+ secretArgs.push(`--from-literal=secret-access-key=${config.secretAccessKey}`);
32
+ if (config.sessionToken) {
33
+ secretArgs.push(`--from-literal=session-token=${config.sessionToken}`);
34
+ }
30
35
  }
31
36
  }
32
37
  else {
@@ -39,10 +44,15 @@ export class KubernetesSecretManager {
39
44
  async updateSecret(config) {
40
45
  const secretArgs = ['create', 'secret', 'generic', config.secretName];
41
46
  if (config.type === 'bedrock') {
42
- secretArgs.push(`--from-literal=access-key-id=${config.accessKeyId}`);
43
- secretArgs.push(`--from-literal=secret-access-key=${config.secretAccessKey}`);
44
- if (config.sessionToken) {
45
- secretArgs.push(`--from-literal=session-token=${config.sessionToken}`);
47
+ if (config.authMethod === 'api-key') {
48
+ secretArgs.push(`--from-literal=bedrock-api-key=${config.apiKey}`);
49
+ }
50
+ else {
51
+ secretArgs.push(`--from-literal=access-key-id=${config.accessKeyId}`);
52
+ secretArgs.push(`--from-literal=secret-access-key=${config.secretAccessKey}`);
53
+ if (config.sessionToken) {
54
+ secretArgs.push(`--from-literal=session-token=${config.sessionToken}`);
55
+ }
46
56
  }
47
57
  }
48
58
  else {
@@ -2,11 +2,14 @@ import { BaseProviderConfig, BaseCollectorOptions, ProviderConfigCollector } fro
2
2
  /**
3
3
  * Configuration for AWS Bedrock models.
4
4
  */
5
+ export type BedrockAuthMethod = 'api-key' | 'iam';
5
6
  export interface BedrockConfig extends BaseProviderConfig {
6
7
  type: 'bedrock';
7
8
  region: string;
8
- accessKeyId: string;
9
- secretAccessKey: string;
9
+ authMethod: BedrockAuthMethod;
10
+ apiKey?: string;
11
+ accessKeyId?: string;
12
+ secretAccessKey?: string;
10
13
  sessionToken?: string;
11
14
  modelArn?: string;
12
15
  }
@@ -15,6 +18,8 @@ export interface BedrockConfig extends BaseProviderConfig {
15
18
  */
16
19
  export interface BedrockCollectorOptions extends BaseCollectorOptions {
17
20
  region?: string;
21
+ authMethod?: BedrockAuthMethod;
22
+ apiKey?: string;
18
23
  accessKeyId?: string;
19
24
  secretAccessKey?: string;
20
25
  sessionToken?: string;
@@ -29,56 +29,103 @@ export class BedrockConfigCollector {
29
29
  if (!region) {
30
30
  throw new Error('region is required');
31
31
  }
32
- let accessKeyId = bedrockOptions.accessKeyId;
33
- if (!accessKeyId) {
32
+ let authMethod = bedrockOptions.authMethod;
33
+ if (!authMethod) {
34
34
  const answer = await inquirer.prompt([
35
35
  {
36
- type: 'input',
37
- name: 'accessKeyId',
38
- message: 'AWS access key ID:',
39
- validate: (input) => {
40
- if (!input)
41
- return 'access key ID is required';
42
- return true;
43
- },
36
+ type: 'list',
37
+ name: 'authMethod',
38
+ message: 'Authentication method:',
39
+ choices: [
40
+ { name: 'API key (bearer token)', value: 'api-key' },
41
+ { name: 'IAM credentials', value: 'iam' },
42
+ ],
43
+ default: 'iam',
44
44
  },
45
45
  ]);
46
- accessKeyId = answer.accessKeyId;
46
+ authMethod = answer.authMethod;
47
47
  }
48
- if (!accessKeyId) {
49
- throw new Error('access key ID is required');
48
+ if (!authMethod) {
49
+ throw new Error('authentication method is required');
50
50
  }
51
- let secretAccessKey = bedrockOptions.secretAccessKey;
52
- if (!secretAccessKey) {
53
- const answer = await inquirer.prompt([
54
- {
55
- type: 'password',
56
- name: 'secretAccessKey',
57
- message: 'AWS secret access key:',
58
- mask: '*',
59
- validate: (input) => {
60
- if (!input)
61
- return 'secret access key is required';
62
- return true;
51
+ let apiKey;
52
+ let accessKeyId;
53
+ let secretAccessKey;
54
+ let sessionToken;
55
+ if (authMethod === 'api-key') {
56
+ apiKey = bedrockOptions.apiKey;
57
+ if (!apiKey) {
58
+ const answer = await inquirer.prompt([
59
+ {
60
+ type: 'password',
61
+ name: 'apiKey',
62
+ message: 'Bedrock API key:',
63
+ mask: '*',
64
+ validate: (input) => {
65
+ if (!input)
66
+ return 'API key is required';
67
+ return true;
68
+ },
63
69
  },
64
- },
65
- ]);
66
- secretAccessKey = answer.secretAccessKey;
67
- }
68
- if (!secretAccessKey) {
69
- throw new Error('secret access key is required');
70
+ ]);
71
+ apiKey = answer.apiKey;
72
+ }
73
+ if (!apiKey) {
74
+ throw new Error('API key is required');
75
+ }
70
76
  }
71
- let sessionToken = bedrockOptions.sessionToken;
72
- if (!sessionToken) {
73
- const answer = await inquirer.prompt([
74
- {
75
- type: 'password',
76
- name: 'sessionToken',
77
- message: 'AWS session token (optional, press enter to skip):',
78
- mask: '*',
79
- },
80
- ]);
81
- sessionToken = answer.sessionToken;
77
+ else {
78
+ accessKeyId = bedrockOptions.accessKeyId;
79
+ if (!accessKeyId) {
80
+ const answer = await inquirer.prompt([
81
+ {
82
+ type: 'input',
83
+ name: 'accessKeyId',
84
+ message: 'AWS access key ID:',
85
+ validate: (input) => {
86
+ if (!input)
87
+ return 'access key ID is required';
88
+ return true;
89
+ },
90
+ },
91
+ ]);
92
+ accessKeyId = answer.accessKeyId;
93
+ }
94
+ if (!accessKeyId) {
95
+ throw new Error('access key ID is required');
96
+ }
97
+ secretAccessKey = bedrockOptions.secretAccessKey;
98
+ if (!secretAccessKey) {
99
+ const answer = await inquirer.prompt([
100
+ {
101
+ type: 'password',
102
+ name: 'secretAccessKey',
103
+ message: 'AWS secret access key:',
104
+ mask: '*',
105
+ validate: (input) => {
106
+ if (!input)
107
+ return 'secret access key is required';
108
+ return true;
109
+ },
110
+ },
111
+ ]);
112
+ secretAccessKey = answer.secretAccessKey;
113
+ }
114
+ if (!secretAccessKey) {
115
+ throw new Error('secret access key is required');
116
+ }
117
+ sessionToken = bedrockOptions.sessionToken;
118
+ if (!sessionToken) {
119
+ const answer = await inquirer.prompt([
120
+ {
121
+ type: 'password',
122
+ name: 'sessionToken',
123
+ message: 'AWS session token (optional, press enter to skip):',
124
+ mask: '*',
125
+ },
126
+ ]);
127
+ sessionToken = answer.sessionToken;
128
+ }
82
129
  }
83
130
  let modelArn = bedrockOptions.modelArn;
84
131
  if (!modelArn) {
@@ -96,6 +143,8 @@ export class BedrockConfigCollector {
96
143
  modelValue: options.model,
97
144
  secretName: '',
98
145
  region,
146
+ authMethod,
147
+ apiKey,
99
148
  accessKeyId,
100
149
  secretAccessKey,
101
150
  sessionToken: sessionToken || undefined,
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
- import { executeQuery, parseTarget } from '../../lib/executeQuery.js';
3
+ import { executeQuery, parseTarget, parseParameters, } from '../../lib/executeQuery.js';
4
4
  import { ExitCodes } from '../../lib/errors.js';
5
5
  export function createQueryCommand(config) {
6
6
  const queryCommand = new Command('query');
@@ -10,6 +10,7 @@ export function createQueryCommand(config) {
10
10
  .argument('<message>', 'Message to send')
11
11
  .option('-o, --output <format>', 'Output format: yaml, json, name or events (shows structured event data)')
12
12
  .option('--timeout <timeout>', 'Query timeout (e.g., 30s, 5m, 1h)')
13
+ .option('-p, --parameter <name=value>', 'Template parameter in name=value format (can be used multiple times)', (val, acc) => [...acc, val], [])
13
14
  .option('--session-id <sessionId>', 'Session ID to associate with the query for conversation continuity')
14
15
  .option('--conversation-id <conversationId>', 'Conversation ID to associate with the query for memory continuity')
15
16
  .action(async (target, message, options) => {
@@ -18,12 +19,21 @@ export function createQueryCommand(config) {
18
19
  console.error(chalk.red('Invalid target format. Use: model/name or agent/name etc'));
19
20
  process.exit(ExitCodes.CliError);
20
21
  }
22
+ let parameters;
23
+ try {
24
+ parameters = parseParameters(options.parameter || []);
25
+ }
26
+ catch (error) {
27
+ console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
28
+ process.exit(ExitCodes.CliError);
29
+ }
21
30
  await executeQuery({
22
31
  targetType: parsed.type,
23
32
  targetName: parsed.name,
24
33
  message,
25
34
  outputFormat: options.output,
26
35
  timeout: options.timeout || config.queryTimeout,
36
+ parameters,
27
37
  sessionId: options.sessionId,
28
38
  conversationId: options.conversationId,
29
39
  });
@@ -5,7 +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
+ import { runReadinessChecks, describeStorageBackend, } from '../../lib/readinessChecks.js';
9
9
  import { arkServices } from '../../arkServices.js';
10
10
  import output from '../../lib/output.js';
11
11
  import { parseTimeoutToSeconds } from '../../lib/timeout.js';
@@ -49,7 +49,26 @@ function enrichServiceDetails(service) {
49
49
  details: details.join(', '),
50
50
  };
51
51
  }
52
- function buildStatusSections(data, versionInfo) {
52
+ function backendStatusLine(detection) {
53
+ const display = {
54
+ etcd: { color: 'green', details: 'etcd' },
55
+ postgresql: { color: 'green', details: 'postgresql' },
56
+ 'not-installed': { color: 'yellow', details: 'not installed' },
57
+ unreachable: { color: 'yellow', details: 'cluster unreachable' },
58
+ forbidden: { color: 'yellow', details: 'access denied' },
59
+ undetermined: { color: 'yellow', details: 'undetermined' },
60
+ };
61
+ const { color, details } = display[detection.status];
62
+ return {
63
+ icon: '●',
64
+ iconColor: color,
65
+ status: 'storage backend',
66
+ statusColor: color,
67
+ name: '',
68
+ details,
69
+ };
70
+ }
71
+ function buildStatusSections(data, versionInfo, backend) {
53
72
  const sections = [];
54
73
  // Dependencies section
55
74
  sections.push({
@@ -251,6 +270,9 @@ function buildStatusSections(data, versionInfo) {
251
270
  }
252
271
  }
253
272
  }
273
+ if (backend) {
274
+ arkStatusLines.push(backendStatusLine(backend));
275
+ }
254
276
  sections.push({ title: 'ark status:', lines: arkStatusLines });
255
277
  return sections;
256
278
  }
@@ -266,12 +288,24 @@ export async function checkStatus(serviceNames, options) {
266
288
  statusChecker.checkAll(),
267
289
  fetchVersionInfo(),
268
290
  ]);
291
+ // Only probe for the storage backend if the cluster is reachable; probing an
292
+ // unreachable cluster would just retry to its timeout.
293
+ const detection = statusData.clusterAccess
294
+ ? await describeStorageBackend()
295
+ : {
296
+ backend: 'unknown',
297
+ status: 'unreachable',
298
+ message: 'Cluster is not reachable — cannot determine the storage backend.',
299
+ };
269
300
  spinner.stop();
270
- const sections = buildStatusSections(statusData, versionInfo);
301
+ const sections = buildStatusSections(statusData, versionInfo, detection);
271
302
  StatusFormatter.printSections(sections);
272
303
  if (options?.waitForReady) {
273
304
  const timeoutSeconds = parseTimeoutToSeconds(options.waitForReady);
274
- const backend = await detectStorageBackend();
305
+ const backend = detection.backend;
306
+ if (backend === 'unknown') {
307
+ output.warning(`${detection.message} Skipping backend-specific readiness checks.`);
308
+ }
275
309
  let servicesToWait = [];
276
310
  if (serviceNames && serviceNames.length > 0) {
277
311
  servicesToWait = serviceNames
@@ -316,7 +350,7 @@ export async function checkStatus(serviceNames, options) {
316
350
  waitSpinner.succeed('All services are ready');
317
351
  const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
318
352
  const remainingSeconds = Math.max(1, timeoutSeconds - elapsedSeconds);
319
- const deepResults = await runReadinessChecks(remainingSeconds, (r) => {
353
+ const deepResults = await runReadinessChecks(remainingSeconds, backend, (r) => {
320
354
  const icon = r.passed ? chalk.green('✓') : chalk.red('✗');
321
355
  const dur = `${(r.durationMs / 1000).toFixed(1)}s`;
322
356
  const suffix = r.message ? ` — ${r.message}` : '';
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { createGenerateCommand } from './commands/generate/index.js';
18
18
  import { createImportCommand } from './commands/import/index.js';
19
19
  import { createInstallCommand } from './commands/install/index.js';
20
20
  import { createMarketplaceCommand } from './commands/marketplace/index.js';
21
+ import { createMcpCommand } from './commands/mcp/index.js';
21
22
  import { createMemoryCommand } from './commands/memory/index.js';
22
23
  import { createModelsCommand } from './commands/models/index.js';
23
24
  import { createQueryCommand } from './commands/query/index.js';
@@ -53,6 +54,7 @@ async function main() {
53
54
  program.addCommand(createImportCommand(config));
54
55
  program.addCommand(createInstallCommand(config));
55
56
  program.addCommand(createMarketplaceCommand(config));
57
+ program.addCommand(createMcpCommand(config));
56
58
  program.addCommand(createMemoryCommand(config));
57
59
  program.addCommand(createModelsCommand(config));
58
60
  program.addCommand(createQueryCommand(config));
@@ -58,6 +58,10 @@ export declare class ArkApiClient {
58
58
  sessionId?: string;
59
59
  conversationId?: string;
60
60
  timeout?: string;
61
+ parameters?: Array<{
62
+ name: string;
63
+ value?: string;
64
+ }>;
61
65
  metadata?: {
62
66
  annotations?: Record<string, string>;
63
67
  };
@@ -159,6 +159,9 @@ export class ArkApiClient {
159
159
  sessionId: params.sessionId,
160
160
  conversationId: params.conversationId,
161
161
  timeout: params.timeout,
162
+ ...(params.parameters && params.parameters.length > 0
163
+ ? { parameters: params.parameters }
164
+ : {}),
162
165
  ...(params.metadata ? { metadata: params.metadata } : {}),
163
166
  }),
164
167
  });
@@ -1,4 +1,5 @@
1
1
  import { ArkApiClient, QueryTarget } from './arkApiClient.js';
2
+ import type { QueryParameter } from './types.js';
2
3
  export { QueryTarget };
3
4
  export interface ChatConfig {
4
5
  streamingEnabled: boolean;
@@ -7,6 +8,7 @@ export interface ChatConfig {
7
8
  conversationId?: string;
8
9
  queryTimeout?: string;
9
10
  a2aContextId?: string;
11
+ parameters?: QueryParameter[];
10
12
  }
11
13
  export interface ToolCall {
12
14
  id: string;
@@ -1,4 +1,19 @@
1
1
  import { QUERY_ANNOTATIONS } from './constants.js';
2
+ /** Best-effort human-readable error from a failed query's status. */
3
+ function extractQueryError(status) {
4
+ if (!status)
5
+ return 'Query failed';
6
+ if (status.error)
7
+ return status.error;
8
+ if (status.message)
9
+ return status.message;
10
+ const condition = status.conditions?.find((c) => c.message);
11
+ if (condition?.message)
12
+ return condition.message.trim();
13
+ if (status.response?.content)
14
+ return status.response.content;
15
+ return 'Query failed';
16
+ }
2
17
  export class ChatClient {
3
18
  arkApiClient;
4
19
  constructor(arkApiClient) {
@@ -26,6 +41,7 @@ export class ChatClient {
26
41
  sessionId: config.sessionId,
27
42
  conversationId: config.conversationId,
28
43
  timeout: config.queryTimeout,
44
+ parameters: config.parameters,
29
45
  ...(Object.keys(annotations).length > 0
30
46
  ? { metadata: { annotations } }
31
47
  : {}),
@@ -72,7 +88,7 @@ export class ChatClient {
72
88
  onChunk(content, undefined, undefined);
73
89
  }
74
90
  if (phase === 'error') {
75
- throw new Error(content || 'Query failed');
91
+ throw new Error(extractQueryError(query.status));
76
92
  }
77
93
  return content;
78
94
  }
@@ -155,6 +171,14 @@ export class ChatClient {
155
171
  finally {
156
172
  reader.releaseLock();
157
173
  }
174
+ // Empty stream (no content, no tool calls) can mean the query errored — surface it.
175
+ if (!fullResponse && toolCallsById.size === 0 && !signal?.aborted) {
176
+ const query = (await this.arkApiClient.getQuery(queryName));
177
+ const phase = query?.status?.phase;
178
+ if (phase === 'error' || phase === 'canceled') {
179
+ throw new Error(extractQueryError(query?.status));
180
+ }
181
+ }
158
182
  return fullResponse;
159
183
  }
160
184
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared query execution logic for both universal and resource-specific query commands
3
3
  */
4
- import type { QueryTarget } from './types.js';
4
+ import type { QueryTarget, QueryParameter } from './types.js';
5
5
  export interface QueryOptions {
6
6
  targetType: string;
7
7
  targetName: string;
@@ -12,6 +12,7 @@ export interface QueryOptions {
12
12
  outputFormat?: string;
13
13
  sessionId?: string;
14
14
  conversationId?: string;
15
+ parameters?: QueryParameter[];
15
16
  }
16
17
  export declare function executeQuery(options: QueryOptions): Promise<void>;
17
18
  /**
@@ -19,3 +20,5 @@ export declare function executeQuery(options: QueryOptions): Promise<void>;
19
20
  * Returns QueryTarget or null if invalid
20
21
  */
21
22
  export declare function parseTarget(target: string): QueryTarget | null;
23
+ /** Parse repeated `name=value` flags into QueryParameters (mirrors the fark CLI). */
24
+ export declare function parseParameters(parameters: string[]): QueryParameter[];
@@ -37,6 +37,7 @@ export async function executeQuery(options) {
37
37
  sessionId,
38
38
  conversationId,
39
39
  queryTimeout: options.timeout,
40
+ parameters: options.parameters,
40
41
  }, (chunk, toolCalls, arkMetadata) => {
41
42
  if (firstOutput) {
42
43
  spinner.stop();
@@ -120,6 +121,9 @@ async function executeQueryWithFormat(options) {
120
121
  type: options.targetType,
121
122
  name: options.targetName,
122
123
  },
124
+ ...(options.parameters && options.parameters.length > 0
125
+ ? { parameters: options.parameters }
126
+ : {}),
123
127
  },
124
128
  };
125
129
  try {
@@ -172,3 +176,18 @@ export function parseTarget(target) {
172
176
  name: parts[1],
173
177
  };
174
178
  }
179
+ /** Parse repeated `name=value` flags into QueryParameters (mirrors the fark CLI). */
180
+ export function parseParameters(parameters) {
181
+ return parameters.map((param) => {
182
+ const idx = param.indexOf('=');
183
+ if (idx === -1) {
184
+ throw new Error(`parameter must be in name=value format, got: ${param}`);
185
+ }
186
+ const name = param.slice(0, idx).trim();
187
+ const value = param.slice(idx + 1).trim();
188
+ if (name === '') {
189
+ throw new Error(`parameter name cannot be empty in: ${param}`);
190
+ }
191
+ return { name, value };
192
+ });
193
+ }
@@ -1,4 +1,11 @@
1
1
  export type StorageBackend = 'etcd' | 'postgresql';
2
+ export type DetectedBackend = StorageBackend | 'unknown';
3
+ export type BackendStatus = 'etcd' | 'postgresql' | 'not-installed' | 'unreachable' | 'forbidden' | 'undetermined';
4
+ export interface BackendDetection {
5
+ backend: DetectedBackend;
6
+ status: BackendStatus;
7
+ message: string;
8
+ }
2
9
  export interface ReadinessCheckResult {
3
10
  name: string;
4
11
  passed: boolean;
@@ -6,5 +13,6 @@ export interface ReadinessCheckResult {
6
13
  message?: string;
7
14
  }
8
15
  export type ReadinessProgress = (result: ReadinessCheckResult) => void;
9
- export declare function detectStorageBackend(): Promise<StorageBackend>;
10
- export declare function runReadinessChecks(timeoutSeconds: number, onProgress?: ReadinessProgress): Promise<ReadinessCheckResult[]>;
16
+ export declare function describeStorageBackend(): Promise<BackendDetection>;
17
+ export declare function detectStorageBackend(): Promise<DetectedBackend>;
18
+ export declare function runReadinessChecks(timeoutSeconds: number, backend: DetectedBackend, onProgress?: ReadinessProgress): Promise<ReadinessCheckResult[]>;
@@ -14,9 +14,89 @@ async function runKubectl(args, timeoutMs) {
14
14
  stderr: result.stderr ?? '',
15
15
  };
16
16
  }
17
+ function classifyFailure(stderr) {
18
+ if (/not\s*found/i.test(stderr)) {
19
+ return 'not-found';
20
+ }
21
+ if (/forbidden|unauthorized/i.test(stderr)) {
22
+ return 'forbidden';
23
+ }
24
+ if (/connection refused|was refused|no such host|i\/o timeout|timed out|dial tcp|unable to connect|did you specify the right host/i.test(stderr)) {
25
+ return 'unreachable';
26
+ }
27
+ return 'undetermined';
28
+ }
29
+ const DETECT_RETRY_DELAY_MS = 250;
30
+ const DETECT_MAX_RETRIES = 2;
31
+ function isAuthoritativeResult(result) {
32
+ if (result.exitCode === 0) {
33
+ return true;
34
+ }
35
+ return /not\s*found|forbidden/i.test(result.stderr);
36
+ }
37
+ async function probeKubectl(args, timeoutMs, maxRetries = DETECT_MAX_RETRIES) {
38
+ let result = await runKubectl(args, timeoutMs);
39
+ for (let attempt = 0; attempt < maxRetries && !isAuthoritativeResult(result); attempt++) {
40
+ await sleep(DETECT_RETRY_DELAY_MS);
41
+ result = await runKubectl(args, timeoutMs);
42
+ }
43
+ return result;
44
+ }
45
+ function unknownFrom(reason) {
46
+ switch (reason) {
47
+ case 'forbidden':
48
+ return {
49
+ backend: 'unknown',
50
+ status: 'forbidden',
51
+ message: 'Access denied reading cluster resources (RBAC) — cannot determine the storage backend.',
52
+ };
53
+ case 'unreachable':
54
+ return {
55
+ backend: 'unknown',
56
+ status: 'unreachable',
57
+ message: 'Cluster is not reachable (connection failed or timed out) — cannot determine the storage backend.',
58
+ };
59
+ default:
60
+ return {
61
+ backend: 'unknown',
62
+ status: 'undetermined',
63
+ message: 'Could not determine the storage backend (unrecognized kubectl error).',
64
+ };
65
+ }
66
+ }
67
+ export async function describeStorageBackend() {
68
+ const crd = await probeKubectl(['get', 'crd', 'agents.ark.mckinsey.com'], 10000);
69
+ if (crd.exitCode === 0) {
70
+ return {
71
+ backend: 'etcd',
72
+ status: 'etcd',
73
+ message: 'ARK is running the etcd (Kubernetes-native) backend; agents are stored as CRDs.',
74
+ };
75
+ }
76
+ const crdReason = classifyFailure(crd.stderr);
77
+ if (crdReason !== 'not-found') {
78
+ return unknownFrom(crdReason);
79
+ }
80
+ const api = await probeKubectl(['get', 'apiservice', 'v1alpha1.ark.mckinsey.com', '-o', 'name'], 10000);
81
+ if (api.exitCode === 0) {
82
+ return {
83
+ backend: 'postgresql',
84
+ status: 'postgresql',
85
+ message: 'ARK is running the PostgreSQL backend; agents are served by the aggregated API server.',
86
+ };
87
+ }
88
+ const apiReason = classifyFailure(api.stderr);
89
+ if (apiReason === 'not-found') {
90
+ return {
91
+ backend: 'unknown',
92
+ status: 'not-installed',
93
+ message: 'ARK is not installed on this cluster (no agents CRD and no aggregated APIService).',
94
+ };
95
+ }
96
+ return unknownFrom(apiReason);
97
+ }
17
98
  export async function detectStorageBackend() {
18
- const { exitCode } = await runKubectl(['get', 'crd', 'agents.ark.mckinsey.com'], 10000);
19
- return exitCode === 0 ? 'etcd' : 'postgresql';
99
+ return (await describeStorageBackend()).backend;
20
100
  }
21
101
  async function waitForApiServices(timeoutSeconds) {
22
102
  const start = Date.now();
@@ -64,11 +144,20 @@ async function waitForApiGroup(timeoutSeconds) {
64
144
  message: 'timed out waiting for ark.mckinsey.com API group',
65
145
  };
66
146
  }
67
- export async function runReadinessChecks(timeoutSeconds, onProgress) {
68
- const backend = await detectStorageBackend();
147
+ export async function runReadinessChecks(timeoutSeconds, backend, onProgress) {
69
148
  if (backend === 'etcd') {
70
149
  return [];
71
150
  }
151
+ if (backend === 'unknown') {
152
+ const result = {
153
+ name: 'Storage backend',
154
+ passed: false,
155
+ durationMs: 0,
156
+ message: 'could not determine storage backend (ARK not installed, cluster unreachable, or access denied)',
157
+ };
158
+ onProgress?.(result);
159
+ return [result];
160
+ }
72
161
  const overallStart = Date.now();
73
162
  const remaining = () => Math.max(1, timeoutSeconds - Math.floor((Date.now() - overallStart) / 1000));
74
163
  const checks = [
@@ -92,6 +92,10 @@ export interface QueryTarget {
92
92
  type: string;
93
93
  name: string;
94
94
  }
95
+ export interface QueryParameter {
96
+ name: string;
97
+ value?: string;
98
+ }
95
99
  export interface QueryResponse {
96
100
  content?: string;
97
101
  a2a?: {
@@ -124,6 +128,7 @@ export interface Query {
124
128
  sessionId?: string;
125
129
  conversationId?: string;
126
130
  timeout?: string;
131
+ parameters?: QueryParameter[];
127
132
  };
128
133
  status?: QueryStatus;
129
134
  }