@agents-at-scale/ark 0.1.69 → 0.1.71

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.
@@ -108,6 +108,9 @@ const defaultArkServices = {
108
108
  category: 'core',
109
109
  chartPath: `${REGISTRY_BASE}/ark-tenant:${CHART_VERSION}`,
110
110
  installArgs: [],
111
+ // Without a broker, a tenant loses conversation history (issue #2642).
112
+ requires: ['ark-broker'],
113
+ dependencyOverrideArgs: ['--set', 'memory.requireBroker=false'],
111
114
  },
112
115
  'ark-api': {
113
116
  name: 'ark-api',
@@ -7,5 +7,6 @@ export declare function installArk(config: ArkConfig, serviceNames?: string[], o
7
7
  arkVersion?: string;
8
8
  marketplaceVersion?: string;
9
9
  backend?: string;
10
+ deps?: boolean;
10
11
  }): Promise<void>;
11
12
  export declare function createInstallCommand(config: ArkConfig): Command;
@@ -85,6 +85,43 @@ function exitIfServicesSkipped(skipped) {
85
85
  output.error(`installation incomplete: ${skipped.length} service(s) skipped because the requested version was not found: ${detail}`);
86
86
  process.exit(1);
87
87
  }
88
+ function findService(services, name) {
89
+ return Object.values(services).find((s) => s.name === name || s.helmReleaseName === name);
90
+ }
91
+ /**
92
+ * Expand requested service names to include their transitive `requires`,
93
+ * dependencies first. A visited set both de-duplicates and guards against
94
+ * a cycle in `requires` (a node already being visited is never re-entered).
95
+ * A dependency that does not resolve to an installable service (disabled,
96
+ * or excluded for the current backend) is skipped with a warning rather
97
+ * than pushed through to a confusing "service not found" error for a name
98
+ * the caller never typed.
99
+ */
100
+ function resolveServiceOrder(requestedNames, services, skipDeps) {
101
+ const resolved = [];
102
+ const visited = new Set();
103
+ function visit(name) {
104
+ if (visited.has(name))
105
+ return;
106
+ visited.add(name);
107
+ if (!skipDeps) {
108
+ const service = findService(services, name);
109
+ for (const dep of service?.requires || []) {
110
+ if (findService(services, dep)) {
111
+ visit(dep);
112
+ }
113
+ else {
114
+ output.warning(`${name} requires ${dep}, but it is not installable in this configuration — skipping it`);
115
+ }
116
+ }
117
+ }
118
+ resolved.push(name);
119
+ }
120
+ for (const name of requestedNames) {
121
+ visit(name);
122
+ }
123
+ return resolved;
124
+ }
88
125
  async function uninstallPrerequisites(service, verbose = false) {
89
126
  if (!service.prerequisiteUninstalls?.length)
90
127
  return;
@@ -204,7 +241,23 @@ export async function installArk(config, serviceNames = [], options = {}) {
204
241
  }
205
242
  // If specific services are requested, install only those services
206
243
  if (serviceNames.length > 0) {
207
- for (const serviceName of serviceNames) {
244
+ const installableServices = getInstallableServices(backend);
245
+ const skipDeps = options.deps === false;
246
+ const resolvedServiceNames = resolveServiceOrder(serviceNames, installableServices, skipDeps);
247
+ if (skipDeps) {
248
+ const skippedDeps = resolveServiceOrder(serviceNames, installableServices, false).filter((name) => !serviceNames.includes(name));
249
+ if (skippedDeps.length > 0) {
250
+ output.warning(`--no-deps: not installing ${skippedDeps.join(', ')} automatically; the requested service(s) may not behave as expected without them`);
251
+ }
252
+ }
253
+ else {
254
+ for (const name of resolvedServiceNames) {
255
+ if (!serviceNames.includes(name)) {
256
+ output.info(`${name} is required by a requested service, installing it first...`);
257
+ }
258
+ }
259
+ }
260
+ for (const serviceName of resolvedServiceNames) {
208
261
  // Check if it's a marketplace item
209
262
  if (isMarketplaceService(serviceName)) {
210
263
  const service = await getMarketplaceItem(serviceName);
@@ -249,19 +302,29 @@ export async function installArk(config, serviceNames = [], options = {}) {
249
302
  continue;
250
303
  }
251
304
  // Core ARK service
252
- const services = getInstallableServices(backend);
253
- const service = Object.values(services).find((s) => s.name === serviceName);
305
+ const service = findService(installableServices, serviceName);
254
306
  if (!service) {
255
307
  output.error(`service '${serviceName}' not found`);
256
308
  output.info('available services:');
257
- for (const s of Object.values(services)) {
309
+ for (const s of Object.values(installableServices)) {
258
310
  output.info(` ${s.name}`);
259
311
  }
260
312
  process.exit(1);
261
313
  }
314
+ // Apply the override only for a service actually missing one of its
315
+ // `requires` from this install — whether because --no-deps skipped it
316
+ // globally, or because a specific dependency wasn't installable (e.g.
317
+ // disabled in .arkrc.yaml) — not just because --no-deps was passed at
318
+ // all, which would wrongly override a service whose dependency IS
319
+ // being installed alongside it (e.g. `ark install ark-broker
320
+ // ark-tenant --no-deps`).
321
+ const missingRequires = (service.requires || []).some((dep) => !resolvedServiceNames.includes(dep));
262
322
  output.info(`installing ${service.name}...`);
263
323
  try {
264
- await installService(service, options.verbose, options.arkVersion, options.marketplaceVersion, backendInstallArgs(service, backend, postgresValues));
324
+ await installService(service, options.verbose, options.arkVersion, options.marketplaceVersion, [
325
+ ...backendInstallArgs(service, backend, postgresValues),
326
+ ...(missingRequires ? service.dependencyOverrideArgs || [] : []),
327
+ ]);
265
328
  output.success(`${service.name} installed successfully`);
266
329
  // Wait for ark-apiserver to be ready before continuing to other services
267
330
  if (service.helmReleaseName === 'ark-apiserver' && backend === 'postgresql') {
@@ -416,9 +479,13 @@ export async function installArk(config, serviceNames = [], options = {}) {
416
479
  }
417
480
  }
418
481
  }
419
- // Install selected services
420
- for (const serviceName of selectedComponents) {
421
- const service = Object.values(arkServices).find((s) => s.helmReleaseName === serviceName);
482
+ // Install selected services, dependencies first: `mandatory` only forces
483
+ // a service into `selectedComponents`, it says nothing about install
484
+ // order, so a `requires` dependency (e.g. ark-broker for ark-tenant) must
485
+ // still be resolved here even though every mandatory name is present.
486
+ const orderedComponents = resolveServiceOrder(selectedComponents, arkServices, false);
487
+ for (const serviceName of orderedComponents) {
488
+ const service = findService(arkServices, serviceName);
422
489
  if (!service || !service.chartPath) {
423
490
  continue;
424
491
  }
@@ -473,7 +540,9 @@ export async function installArk(config, serviceNames = [], options = {}) {
473
540
  process.exit(1);
474
541
  }
475
542
  }
476
- // Install all services
543
+ // Install all services. Every service is selected here, so alphabetical
544
+ // order happens to put ark-broker before ark-tenant today — resolve
545
+ // `requires` explicitly anyway rather than depend on that coincidence.
477
546
  const services = getInstallableServices(backend);
478
547
  const sortedServices = Object.values(services).sort((a, b) => {
479
548
  // Ensure ark-controller is always first
@@ -483,10 +552,22 @@ export async function installArk(config, serviceNames = [], options = {}) {
483
552
  return 1;
484
553
  return a.name.localeCompare(b.name);
485
554
  });
486
- for (const service of sortedServices) {
555
+ const orderedNames = resolveServiceOrder(sortedServices.map((s) => s.name), services, false);
556
+ const orderedServices = orderedNames
557
+ .map((name) => findService(services, name))
558
+ .filter((s) => Boolean(s));
559
+ for (const service of orderedServices) {
487
560
  output.info(`installing ${service.name}...`);
561
+ // A `requires` dependency disabled in .arkrc.yaml gets dropped (with a
562
+ // warning) by resolveServiceOrder above rather than installed — apply
563
+ // the same override as the named-service path so the dependent isn't
564
+ // then refused by its own chart-level guard.
565
+ const missingRequires = (service.requires || []).some((dep) => !orderedNames.includes(dep));
488
566
  try {
489
- await installService(service, options.verbose, options.arkVersion, options.marketplaceVersion, backendInstallArgs(service, backend, postgresValues));
567
+ await installService(service, options.verbose, options.arkVersion, options.marketplaceVersion, [
568
+ ...backendInstallArgs(service, backend, postgresValues),
569
+ ...(missingRequires ? service.dependencyOverrideArgs || [] : []),
570
+ ]);
490
571
  console.log(); // Add blank line after command output
491
572
  }
492
573
  catch (error) {
@@ -555,6 +636,7 @@ export function createInstallCommand(config) {
555
636
  .option('--marketplace-version <version>', 'Marketplace item version to install (e.g., 0.1.5)')
556
637
  .option('--wait-for-ready <timeout>', 'wait for Ark to be ready after installation (e.g., 30s, 2m)')
557
638
  .option('--backend <type>', "storage backend: 'etcd' (default) or 'postgresql' (overrides storage.backend in .arkrc.yaml)")
639
+ .option('--no-deps', "skip a requested service's dependencies (e.g. ark-broker for ark-tenant); the resulting install may lose the guarantees those dependencies provide")
558
640
  .option('-v, --verbose', 'show commands being executed')
559
641
  .action(async (services, options) => {
560
642
  await installArk(config, services, options);
@@ -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, describeStorageBackend, } from '../../lib/readinessChecks.js';
8
+ import { runReadinessChecks, describeStorageBackend, describeNamespaceMemory, } 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';
@@ -68,7 +68,26 @@ function backendStatusLine(detection) {
68
68
  details,
69
69
  };
70
70
  }
71
- function buildStatusSections(data, versionInfo, backend) {
71
+ function namespaceMemoryStatusLine(check) {
72
+ const display = {
73
+ ready: { icon: '●', color: 'green', details: check.message },
74
+ unresolved: { icon: '✗', color: 'red', details: 'not resolving' },
75
+ missing: { icon: '✗', color: 'red', details: 'missing' },
76
+ 'no-broker': { icon: '○', color: 'yellow', details: 'not configured' },
77
+ undetermined: { icon: '?', color: 'yellow', details: 'undetermined' },
78
+ };
79
+ const { icon, color, details } = display[check.status];
80
+ return {
81
+ icon,
82
+ iconColor: color,
83
+ status: 'default memory',
84
+ statusColor: color,
85
+ name: '',
86
+ details,
87
+ subtext: check.status === 'ready' ? undefined : check.message,
88
+ };
89
+ }
90
+ function buildStatusSections(data, versionInfo, backend, namespaceMemory) {
72
91
  const sections = [];
73
92
  // Dependencies section
74
93
  sections.push({
@@ -270,6 +289,9 @@ function buildStatusSections(data, versionInfo, backend) {
270
289
  }
271
290
  }
272
291
  }
292
+ if (namespaceMemory) {
293
+ arkStatusLines.push(namespaceMemoryStatusLine(namespaceMemory));
294
+ }
273
295
  if (backend) {
274
296
  arkStatusLines.push(backendStatusLine(backend));
275
297
  }
@@ -289,16 +311,24 @@ export async function checkStatus(serviceNames, options) {
289
311
  fetchVersionInfo(),
290
312
  ]);
291
313
  // 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
- };
314
+ // unreachable cluster would just retry to its timeout. The memory check is
315
+ // scoped to the kubeconfig's current namespace: the invariant it reports on
316
+ // is per-namespace, and that is the namespace the user's queries land in.
317
+ // Both probes are independent, so they run together rather than in series.
318
+ const [detection, namespaceMemory] = await Promise.all([
319
+ statusData.clusterAccess
320
+ ? describeStorageBackend()
321
+ : Promise.resolve({
322
+ backend: 'unknown',
323
+ status: 'unreachable',
324
+ message: 'Cluster is not reachable — cannot determine the storage backend.',
325
+ }),
326
+ statusData.clusterAccess && statusData.arkReady
327
+ ? describeNamespaceMemory(statusData.clusterInfo?.namespace || 'default')
328
+ : Promise.resolve(undefined),
329
+ ]);
300
330
  spinner.stop();
301
- const sections = buildStatusSections(statusData, versionInfo, detection);
331
+ const sections = buildStatusSections(statusData, versionInfo, detection, namespaceMemory);
302
332
  StatusFormatter.printSections(sections);
303
333
  if (options?.waitForReady) {
304
334
  const timeoutSeconds = parseTimeoutToSeconds(options.waitForReady);
@@ -32,9 +32,6 @@ export class ChatClient {
32
32
  if (config.a2aContextId) {
33
33
  annotations[QUERY_ANNOTATIONS.A2A_CONTEXT_ID] = config.a2aContextId;
34
34
  }
35
- if (config.streamingEnabled && onChunk) {
36
- annotations['ark.mckinsey.com/streaming-enabled'] = 'true';
37
- }
38
35
  const queryResult = await this.arkApiClient.createQuery({
39
36
  input,
40
37
  target: { type: targetType, name: targetName },
@@ -12,7 +12,30 @@ export interface ReadinessCheckResult {
12
12
  durationMs: number;
13
13
  message?: string;
14
14
  }
15
+ export type NamespaceMemoryStatus = 'ready' | 'unresolved' | 'missing' | 'no-broker' | 'undetermined';
16
+ export interface NamespaceMemoryCheck {
17
+ status: NamespaceMemoryStatus;
18
+ namespace: string;
19
+ memoryName: string;
20
+ message: string;
21
+ }
15
22
  export type ReadinessProgress = (result: ReadinessCheckResult) => void;
16
23
  export declare function describeStorageBackend(): Promise<BackendDetection>;
24
+ /**
25
+ * Report whether the namespace ends up with a Memory the executor can use.
26
+ *
27
+ * Existence is not the question: NewHTTPMemory errors when
28
+ * status.lastResolvedAddress is empty, so a Memory left unresolved by a
29
+ * missing broker Service fails every query in the namespace rather than
30
+ * degrading. A namespace with no broker is expected to have no Memory, so that
31
+ * is reported as unconfigured rather than broken.
32
+ *
33
+ * ArkConfig.defaultMemory only changes the answer when the Memory it names
34
+ * both exists and resolves: resolveInjectableMemory declines to inject
35
+ * otherwise, and NewMemoryForQuery then falls back to the hardcoded name
36
+ * `default`. So the override is a candidate, and `default` decides the verdict
37
+ * whenever that candidate is not usable.
38
+ */
39
+ export declare function describeNamespaceMemory(namespace: string): Promise<NamespaceMemoryCheck>;
17
40
  export declare function detectStorageBackend(): Promise<DetectedBackend>;
18
41
  export declare function runReadinessChecks(timeoutSeconds: number, backend: DetectedBackend, onProgress?: ReadinessProgress): Promise<ReadinessCheckResult[]>;
@@ -14,8 +14,13 @@ async function runKubectl(args, timeoutMs) {
14
14
  stderr: result.stderr ?? '',
15
15
  };
16
16
  }
17
+ // A resource type the cluster does not serve at all. kubectl reports this
18
+ // differently from a missing object, and it is just as final an answer — the
19
+ // aggregated API server serves no arkconfigs, so this is the normal reply
20
+ // there rather than something to retry.
21
+ const UNSERVED_RESOURCE_PATTERN = /doesn't have a resource type|no matches for kind|the server could not find the requested resource/i;
17
22
  function classifyFailure(stderr) {
18
- if (/not\s*found/i.test(stderr)) {
23
+ if (/not\s*found/i.test(stderr) || UNSERVED_RESOURCE_PATTERN.test(stderr)) {
19
24
  return 'not-found';
20
25
  }
21
26
  if (/forbidden|unauthorized/i.test(stderr)) {
@@ -32,7 +37,8 @@ function isAuthoritativeResult(result) {
32
37
  if (result.exitCode === 0) {
33
38
  return true;
34
39
  }
35
- return /not\s*found|forbidden/i.test(result.stderr);
40
+ return (/not\s*found|forbidden/i.test(result.stderr) ||
41
+ UNSERVED_RESOURCE_PATTERN.test(result.stderr));
36
42
  }
37
43
  async function probeKubectl(args, timeoutMs, maxRetries = DETECT_MAX_RETRIES) {
38
44
  let result = await runKubectl(args, timeoutMs);
@@ -95,6 +101,236 @@ export async function describeStorageBackend() {
95
101
  }
96
102
  return unknownFrom(apiReason);
97
103
  }
104
+ // The Memory name the broker chart creates, and the one NewMemoryForQuery
105
+ // hardcodes when a Query carries no spec.memory. ArkConfig can name a
106
+ // different one, but only as a candidate — see describeNamespaceMemory.
107
+ const FALLBACK_MEMORY_NAME = 'default';
108
+ // Either ConfigMap is a broker presence signal, and only when its data carries
109
+ // enabled: "true" — the same test BrokerServiceRefFor applies before the
110
+ // controller's default-Memory backstop will act on it.
111
+ const BROKER_CONFIGMAP_NAMES = ['ark-config-broker', 'ark-config-streaming'];
112
+ // The ConfigMaps are not the whole story: they are rendered only when
113
+ // otelEndpoint.enabled / streaming.enabled are set, both of which default true
114
+ // but are independently disablable. A broker installed purely for message and
115
+ // session storage has neither, and reporting "no broker in this namespace"
116
+ // there is a confident claim that happens to be false. The label is the
117
+ // chart's rather than the release's, so a renamed release still matches. This
118
+ // mirrors DefaultMemoryReconciler's own two-signal lookup.
119
+ const BROKER_SERVICE_SELECTOR = 'app.kubernetes.io/name=ark-broker';
120
+ const BROKER_ENABLED_JSONPATH = '{range .items[*]}{.data.enabled}{"\\n"}{end}';
121
+ // kubectl reports a missing namespace and a missing object with the same
122
+ // NotFound status, so the two are told apart by the message. Reporting on a
123
+ // namespace that does not exist as "no broker here" would be a confident
124
+ // answer drawn from nothing.
125
+ function isMissingNamespace(stderr) {
126
+ return /namespaces?\s+"[^"]*"\s+not\s+found/i.test(stderr);
127
+ }
128
+ async function probeMemory(namespace, name) {
129
+ const result = await probeKubectl(['get', 'memory', name, '--namespace', namespace, '-o', 'json'], 10000);
130
+ if (result.exitCode !== 0) {
131
+ if (isMissingNamespace(result.stderr)) {
132
+ return {
133
+ outcome: 'undetermined',
134
+ detail: `namespace ${namespace} does not exist`,
135
+ };
136
+ }
137
+ const reason = classifyFailure(result.stderr);
138
+ if (reason === 'not-found') {
139
+ return { outcome: 'absent' };
140
+ }
141
+ return { outcome: 'undetermined', detail: reason };
142
+ }
143
+ let address = '';
144
+ let phase = '';
145
+ let statusMessage = '';
146
+ try {
147
+ const parsed = JSON.parse(result.stdout);
148
+ address = parsed?.status?.lastResolvedAddress || '';
149
+ phase = parsed?.status?.phase || '';
150
+ statusMessage = parsed?.status?.message || '';
151
+ }
152
+ catch {
153
+ return { outcome: 'undetermined', detail: 'unreadable kubectl output' };
154
+ }
155
+ if (address) {
156
+ return { outcome: 'resolved', address };
157
+ }
158
+ return {
159
+ outcome: 'unresolved',
160
+ detail: statusMessage || phase || 'no resolved address',
161
+ };
162
+ }
163
+ async function probeBrokerConfigMaps(namespace) {
164
+ const result = await probeKubectl([
165
+ 'get',
166
+ 'configmap',
167
+ ...BROKER_CONFIGMAP_NAMES,
168
+ '--namespace',
169
+ namespace,
170
+ '--ignore-not-found',
171
+ '-o',
172
+ `jsonpath=${BROKER_ENABLED_JSONPATH}`,
173
+ ], 10000);
174
+ if (result.exitCode !== 0) {
175
+ return 'undetermined';
176
+ }
177
+ const enabled = result.stdout
178
+ .split('\n')
179
+ .some((line) => line.trim() === 'true');
180
+ return enabled ? 'present' : 'absent';
181
+ }
182
+ async function probeBrokerService(namespace) {
183
+ const result = await probeKubectl([
184
+ 'get',
185
+ 'service',
186
+ '--namespace',
187
+ namespace,
188
+ '--selector',
189
+ BROKER_SERVICE_SELECTOR,
190
+ '-o',
191
+ 'name',
192
+ ], 10000);
193
+ if (result.exitCode !== 0) {
194
+ return 'undetermined';
195
+ }
196
+ return result.stdout.trim() ? 'present' : 'absent';
197
+ }
198
+ async function probeBroker(namespace) {
199
+ const fromConfigMaps = await probeBrokerConfigMaps(namespace);
200
+ if (fromConfigMaps === 'present') {
201
+ return fromConfigMaps;
202
+ }
203
+ const fromService = await probeBrokerService(namespace);
204
+ if (fromService === 'present') {
205
+ return fromService;
206
+ }
207
+ // Neither signal found it. Say so only if both probes actually answered.
208
+ if (fromConfigMaps === 'undetermined' || fromService === 'undetermined') {
209
+ return 'undetermined';
210
+ }
211
+ return 'absent';
212
+ }
213
+ // ArkConfig is cluster-scoped and optional, and the aggregated API server does
214
+ // not serve it at all — both of which classifyFailure reports as not-found, a
215
+ // legitimate "there is no override". "Could not tell" is different: a
216
+ // namespace-scoped user is forbidden from reading a cluster-scoped resource,
217
+ // and would otherwise be judged against the wrong name.
218
+ async function readDefaultMemoryOverride() {
219
+ const result = await probeKubectl([
220
+ 'get',
221
+ 'arkconfig',
222
+ 'default',
223
+ '-o',
224
+ 'jsonpath={.spec.defaultMemory.name}',
225
+ ], 10000);
226
+ if (result.exitCode === 0) {
227
+ const name = result.stdout.trim();
228
+ return name ? { kind: 'name', name } : { kind: 'none' };
229
+ }
230
+ if (classifyFailure(result.stderr) === 'not-found') {
231
+ return { kind: 'none' };
232
+ }
233
+ return { kind: 'undetermined' };
234
+ }
235
+ /**
236
+ * Report whether the namespace ends up with a Memory the executor can use.
237
+ *
238
+ * Existence is not the question: NewHTTPMemory errors when
239
+ * status.lastResolvedAddress is empty, so a Memory left unresolved by a
240
+ * missing broker Service fails every query in the namespace rather than
241
+ * degrading. A namespace with no broker is expected to have no Memory, so that
242
+ * is reported as unconfigured rather than broken.
243
+ *
244
+ * ArkConfig.defaultMemory only changes the answer when the Memory it names
245
+ * both exists and resolves: resolveInjectableMemory declines to inject
246
+ * otherwise, and NewMemoryForQuery then falls back to the hardcoded name
247
+ * `default`. So the override is a candidate, and `default` decides the verdict
248
+ * whenever that candidate is not usable.
249
+ */
250
+ export async function describeNamespaceMemory(namespace) {
251
+ const override = await readDefaultMemoryOverride();
252
+ if (override.kind === 'name' && override.name !== FALLBACK_MEMORY_NAME) {
253
+ const probe = await probeMemory(namespace, override.name);
254
+ if (probe.outcome === 'resolved') {
255
+ return {
256
+ namespace,
257
+ memoryName: override.name,
258
+ status: 'ready',
259
+ message: probe.address,
260
+ };
261
+ }
262
+ if (probe.outcome === 'undetermined') {
263
+ return {
264
+ namespace,
265
+ memoryName: override.name,
266
+ status: 'undetermined',
267
+ message: `Could not read Memory "${override.name}" in namespace ${namespace} (${probe.detail}).`,
268
+ };
269
+ }
270
+ }
271
+ const unusedOverride = override.kind === 'name' && override.name !== FALLBACK_MEMORY_NAME
272
+ ? override.name
273
+ : undefined;
274
+ const check = await describeFallbackMemory(namespace, unusedOverride);
275
+ // The override read failed, so this verdict may be judging the wrong name.
276
+ // Only downgrade one that accuses the namespace of being broken.
277
+ if (override.kind === 'undetermined' &&
278
+ (check.status === 'missing' || check.status === 'unresolved')) {
279
+ return {
280
+ ...check,
281
+ status: 'undetermined',
282
+ message: `${check.message} ArkConfig could not be read, so a cluster-wide default memory name may apply instead.`,
283
+ };
284
+ }
285
+ return check;
286
+ }
287
+ async function describeFallbackMemory(namespace, unusedOverride) {
288
+ const base = { namespace, memoryName: FALLBACK_MEMORY_NAME };
289
+ // The operator asked for a different name and is not getting it. Without
290
+ // saying so, the remediation below reads as "create Memory/default", which
291
+ // is right for the executor and confusing next to their own ArkConfig.
292
+ const overrideNote = unusedOverride
293
+ ? ` ArkConfig names "${unusedOverride}", which is not usable here, so resolution falls back to "${FALLBACK_MEMORY_NAME}".`
294
+ : '';
295
+ const probe = await probeMemory(namespace, FALLBACK_MEMORY_NAME);
296
+ if (probe.outcome === 'resolved') {
297
+ return { ...base, status: 'ready', message: probe.address };
298
+ }
299
+ if (probe.outcome === 'undetermined') {
300
+ return {
301
+ ...base,
302
+ status: 'undetermined',
303
+ message: `Could not read Memory "${FALLBACK_MEMORY_NAME}" in namespace ${namespace} (${probe.detail}).`,
304
+ };
305
+ }
306
+ if (probe.outcome === 'unresolved') {
307
+ return {
308
+ ...base,
309
+ status: 'unresolved',
310
+ message: `Memory "${FALLBACK_MEMORY_NAME}" in namespace ${namespace} has not resolved an address (${probe.detail}); queries using it will fail.${overrideNote}`,
311
+ };
312
+ }
313
+ const broker = await probeBroker(namespace);
314
+ if (broker === 'undetermined') {
315
+ return {
316
+ ...base,
317
+ status: 'undetermined',
318
+ message: `Could not determine whether namespace ${namespace} has a broker, so a missing Memory cannot be judged.`,
319
+ };
320
+ }
321
+ if (broker === 'present') {
322
+ return {
323
+ ...base,
324
+ status: 'missing',
325
+ message: `Namespace ${namespace} has a broker but no Memory "${FALLBACK_MEMORY_NAME}"; chats there will silently lose conversation history.${overrideNote}`,
326
+ };
327
+ }
328
+ return {
329
+ ...base,
330
+ status: 'no-broker',
331
+ message: `No broker in namespace ${namespace}, so no conversation history is kept there.`,
332
+ };
333
+ }
98
334
  export async function detectStorageBackend() {
99
335
  return (await describeStorageBackend()).backend;
100
336
  }
@@ -13,6 +13,8 @@ export interface ArkService {
13
13
  chartPath?: string;
14
14
  installArgs?: string[];
15
15
  prerequisiteUninstalls?: PrerequisiteUninstall[];
16
+ requires?: string[];
17
+ dependencyOverrideArgs?: string[];
16
18
  k8sServiceName?: string;
17
19
  k8sServicePort?: number;
18
20
  k8sPortForwardLocalPort?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-at-scale/ark",
3
- "version": "0.1.69",
3
+ "version": "0.1.71",
4
4
  "description": "Ark CLI - Interactive terminal interface for ARK agents",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -43,8 +43,8 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@kubernetes/client-node": "^1.3.0",
46
- "@modelcontextprotocol/sdk": "^1.27.1",
47
- "axios": "^1.18.0",
46
+ "@modelcontextprotocol/sdk": "^1.32.0",
47
+ "axios": "^1.20.0",
48
48
  "chalk": "^4.1.2",
49
49
  "commander": "^12.1.0",
50
50
  "debug": "^4.4.1",
@@ -85,7 +85,8 @@
85
85
  "node": ">=18.0.0"
86
86
  },
87
87
  "overrides": {
88
- "brace-expansion": ">=5.0.9",
88
+ "vite": "^8.3.3",
89
+ "brace-expansion": "^5.0.12",
89
90
  "ip-address": "^10.3.1",
90
91
  "js-yaml": "^4.3.1",
91
92
  "minimatch": "^10.2.3",
@@ -99,6 +100,8 @@
99
100
  "ws": "^8.21.0",
100
101
  "esbuild": "^0.28.1",
101
102
  "form-data": "^4.0.6",
102
- "postcss": "^8.5.18"
103
+ "postcss": "^8.5.18",
104
+ "proxy-addr": "^2.0.8",
105
+ "source-map-js": "^1.2.2"
103
106
  }
104
107
  }
@@ -96,9 +96,9 @@ metadata:
96
96
  name: sample-query
97
97
  spec:
98
98
  input: 'Hello! Can you help me understand what you can do?'
99
- targets:
100
- - type: agent
101
- name: sample-agent
99
+ target:
100
+ type: agent
101
+ name: sample-agent
102
102
  ```
103
103
 
104
104
  ## Prerequisites
@@ -13,7 +13,7 @@ kind: Query
13
13
  metadata:
14
14
  name: sample-query
15
15
  spec:
16
- targets:
17
- - name: sample-agent
18
- type: agent
16
+ target:
17
+ name: sample-agent
18
+ type: agent
19
19
  input: Calculate 3+2.
@@ -7,3 +7,8 @@ requires-python = ">=3.12"
7
7
  dependencies = [
8
8
  "fastmcp>=2.9.0",
9
9
  ]
10
+
11
+ [tool.uv]
12
+ # Security floor: authlib <=1.7.2 is affected by CVE-2026-104056 (XRAY-1104189).
13
+ # authlib is pulled transitively via fastmcp; pin the minimum to the patched release.
14
+ constraint-dependencies = ["authlib>=1.8.0"]
@@ -8,6 +8,9 @@ resolution-markers = [
8
8
  "python_full_version < '3.14' and sys_platform != 'win32'",
9
9
  ]
10
10
 
11
+ [manifest]
12
+ constraints = [{ name = "authlib", specifier = ">=1.8.0" }]
13
+
11
14
  [[package]]
12
15
  name = "aiofile"
13
16
  version = "3.9.0"
@@ -31,16 +34,15 @@ wheels = [
31
34
 
32
35
  [[package]]
33
36
  name = "anyio"
34
- version = "4.9.0"
37
+ version = "4.14.2"
35
38
  source = { registry = "https://pypi.org/simple" }
36
39
  dependencies = [
37
40
  { name = "idna" },
38
- { name = "sniffio" },
39
41
  { name = "typing-extensions", marker = "python_full_version < '3.13'" },
40
42
  ]
41
- sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
43
+ sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
42
44
  wheels = [
43
- { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
45
+ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
44
46
  ]
45
47
 
46
48
  [[package]]
@@ -54,14 +56,15 @@ wheels = [
54
56
 
55
57
  [[package]]
56
58
  name = "authlib"
57
- version = "1.6.12"
59
+ version = "1.8.0"
58
60
  source = { registry = "https://pypi.org/simple" }
59
61
  dependencies = [
60
62
  { name = "cryptography" },
63
+ { name = "joserfc" },
61
64
  ]
62
- sdist = { url = "https://files.pythonhosted.org/packages/d3/30/6691fdc63b35f54a5a65e04fa1e59d827f4d4e8f4a39678ba7d3088ce0c8/authlib-1.6.12.tar.gz", hash = "sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd", size = 165368, upload-time = "2026-05-04T08:11:31.826Z" }
65
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/51/bc1729d3cfdc214b4935f4e886e4dd443c3065fd8e1e66423fe84b490f81/authlib-1.8.0.tar.gz", hash = "sha256:f3ecd5f1da737262fb53bf1a4d95c4ea1ad9dd509316587a255c99ab1838a4f0", size = 177759, upload-time = "2026-08-30T12:12:34.833Z" }
63
66
  wheels = [
64
- { url = "https://files.pythonhosted.org/packages/cd/51/9b0b5cd4cf683a02db937a6f9bbebcdc9c56558a7bb3763ce7d3512103c3/authlib-1.6.12-py2.py3-none-any.whl", hash = "sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab", size = 244473, upload-time = "2026-05-04T08:11:30.354Z" },
67
+ { url = "https://files.pythonhosted.org/packages/b8/c6/6f124bcfbbfb20fba22c939b4e43a06dccfc0e1ca20e5634ca573cb1e271/authlib-1.8.0-py2.py3-none-any.whl", hash = "sha256:88aebbd9af6757e14e912d5dc007ae1dc1f3e27e3b2152ce7c552ee2c3b3c121", size = 260804, upload-time = "2026-08-30T12:12:33.162Z" },
65
68
  ]
66
69
 
67
70
  [[package]]
@@ -449,6 +452,18 @@ wheels = [
449
452
  { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
450
453
  ]
451
454
 
455
+ [[package]]
456
+ name = "joserfc"
457
+ version = "1.7.5"
458
+ source = { registry = "https://pypi.org/simple" }
459
+ dependencies = [
460
+ { name = "cryptography" },
461
+ ]
462
+ sdist = { url = "https://files.pythonhosted.org/packages/19/94/80fea1514b7c6d7d37804d3fe9ca81455f633347fc98731bd71ffe1faa17/joserfc-1.7.5.tar.gz", hash = "sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea", size = 234990, upload-time = "2026-08-29T13:05:42.057Z" }
463
+ wheels = [
464
+ { url = "https://files.pythonhosted.org/packages/67/c5/82addfd375e5ee6520644e0553e4aadde92d668c4fc99cc716d337fe7bb3/joserfc-1.7.5-py3-none-any.whl", hash = "sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159", size = 71269, upload-time = "2026-08-29T13:05:40.718Z" },
465
+ ]
466
+
452
467
  [[package]]
453
468
  name = "jsonref"
454
469
  version = "1.1.0"
@@ -669,10 +684,10 @@ resolution-markers = [
669
684
  "python_full_version < '3.14' and sys_platform != 'win32'",
670
685
  ]
671
686
  dependencies = [
672
- { name = "annotated-types" },
673
- { name = "pydantic-core", version = "2.33.2", source = { registry = "https://pypi.org/simple" } },
674
- { name = "typing-extensions" },
675
- { name = "typing-inspection", version = "0.4.1", source = { registry = "https://pypi.org/simple" } },
687
+ { name = "annotated-types", marker = "python_full_version < '3.14'" },
688
+ { name = "pydantic-core", version = "2.33.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" },
689
+ { name = "typing-extensions", marker = "python_full_version < '3.14'" },
690
+ { name = "typing-inspection", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" },
676
691
  ]
677
692
  sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
678
693
  wheels = [
@@ -681,7 +696,7 @@ wheels = [
681
696
 
682
697
  [package.optional-dependencies]
683
698
  email = [
684
- { name = "email-validator" },
699
+ { name = "email-validator", marker = "python_full_version < '3.14'" },
685
700
  ]
686
701
 
687
702
  [[package]]
@@ -693,10 +708,10 @@ resolution-markers = [
693
708
  "python_full_version >= '3.14' and sys_platform != 'win32'",
694
709
  ]
695
710
  dependencies = [
696
- { name = "annotated-types" },
697
- { name = "pydantic-core", version = "2.46.4", source = { registry = "https://pypi.org/simple" } },
698
- { name = "typing-extensions" },
699
- { name = "typing-inspection", version = "0.4.2", source = { registry = "https://pypi.org/simple" } },
711
+ { name = "annotated-types", marker = "python_full_version >= '3.14'" },
712
+ { name = "pydantic-core", version = "2.46.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
713
+ { name = "typing-extensions", marker = "python_full_version >= '3.14'" },
714
+ { name = "typing-inspection", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
700
715
  ]
701
716
  sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
702
717
  wheels = [
@@ -705,7 +720,7 @@ wheels = [
705
720
 
706
721
  [package.optional-dependencies]
707
722
  email = [
708
- { name = "email-validator" },
723
+ { name = "email-validator", marker = "python_full_version >= '3.14'" },
709
724
  ]
710
725
 
711
726
  [[package]]
@@ -717,7 +732,7 @@ resolution-markers = [
717
732
  "python_full_version < '3.14' and sys_platform != 'win32'",
718
733
  ]
719
734
  dependencies = [
720
- { name = "typing-extensions" },
735
+ { name = "typing-extensions", marker = "python_full_version < '3.14'" },
721
736
  ]
722
737
  sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
723
738
  wheels = [
@@ -763,7 +778,7 @@ resolution-markers = [
763
778
  "python_full_version >= '3.14' and sys_platform != 'win32'",
764
779
  ]
765
780
  dependencies = [
766
- { name = "typing-extensions" },
781
+ { name = "typing-extensions", marker = "python_full_version >= '3.14'" },
767
782
  ]
768
783
  sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
769
784
  wheels = [
@@ -1096,23 +1111,14 @@ name = "secretstorage"
1096
1111
  version = "3.5.0"
1097
1112
  source = { registry = "https://pypi.org/simple" }
1098
1113
  dependencies = [
1099
- { name = "cryptography" },
1100
- { name = "jeepney" },
1114
+ { name = "cryptography", marker = "sys_platform != 'win32'" },
1115
+ { name = "jeepney", marker = "sys_platform != 'win32'" },
1101
1116
  ]
1102
1117
  sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
1103
1118
  wheels = [
1104
1119
  { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
1105
1120
  ]
1106
1121
 
1107
- [[package]]
1108
- name = "sniffio"
1109
- version = "1.3.1"
1110
- source = { registry = "https://pypi.org/simple" }
1111
- sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
1112
- wheels = [
1113
- { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
1114
- ]
1115
-
1116
1122
  [[package]]
1117
1123
  name = "sse-starlette"
1118
1124
  version = "2.3.6"
@@ -1167,7 +1173,7 @@ resolution-markers = [
1167
1173
  "python_full_version < '3.14' and sys_platform != 'win32'",
1168
1174
  ]
1169
1175
  dependencies = [
1170
- { name = "typing-extensions" },
1176
+ { name = "typing-extensions", marker = "python_full_version < '3.14'" },
1171
1177
  ]
1172
1178
  sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
1173
1179
  wheels = [
@@ -1183,7 +1189,7 @@ resolution-markers = [
1183
1189
  "python_full_version >= '3.14' and sys_platform != 'win32'",
1184
1190
  ]
1185
1191
  dependencies = [
1186
- { name = "typing-extensions" },
1192
+ { name = "typing-extensions", marker = "python_full_version >= '3.14'" },
1187
1193
  ]
1188
1194
  sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
1189
1195
  wheels = [