@agents-at-scale/ark 0.1.69 → 0.1.70
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/dist/arkServices.js +3 -0
- package/dist/commands/install/index.d.ts +1 -0
- package/dist/commands/install/index.js +93 -11
- package/dist/commands/status/index.js +41 -11
- package/dist/lib/chatClient.js +0 -3
- package/dist/lib/readinessChecks.d.ts +23 -0
- package/dist/lib/readinessChecks.js +238 -2
- package/dist/types/arkService.d.ts +2 -0
- package/package.json +7 -5
- package/templates/tool/uv.lock +3 -13
package/dist/arkServices.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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);
|
package/dist/lib/chatClient.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.70",
|
|
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.
|
|
47
|
-
"axios": "^1.
|
|
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,7 @@
|
|
|
85
85
|
"node": ">=18.0.0"
|
|
86
86
|
},
|
|
87
87
|
"overrides": {
|
|
88
|
-
"brace-expansion": "
|
|
88
|
+
"brace-expansion": "^5.0.12",
|
|
89
89
|
"ip-address": "^10.3.1",
|
|
90
90
|
"js-yaml": "^4.3.1",
|
|
91
91
|
"minimatch": "^10.2.3",
|
|
@@ -99,6 +99,8 @@
|
|
|
99
99
|
"ws": "^8.21.0",
|
|
100
100
|
"esbuild": "^0.28.1",
|
|
101
101
|
"form-data": "^4.0.6",
|
|
102
|
-
"postcss": "^8.5.18"
|
|
102
|
+
"postcss": "^8.5.18",
|
|
103
|
+
"proxy-addr": "^2.0.8",
|
|
104
|
+
"source-map-js": "^1.2.2"
|
|
103
105
|
}
|
|
104
106
|
}
|
package/templates/tool/uv.lock
CHANGED
|
@@ -31,16 +31,15 @@ wheels = [
|
|
|
31
31
|
|
|
32
32
|
[[package]]
|
|
33
33
|
name = "anyio"
|
|
34
|
-
version = "4.
|
|
34
|
+
version = "4.14.2"
|
|
35
35
|
source = { registry = "https://pypi.org/simple" }
|
|
36
36
|
dependencies = [
|
|
37
37
|
{ name = "idna" },
|
|
38
|
-
{ name = "sniffio" },
|
|
39
38
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
40
39
|
]
|
|
41
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
40
|
+
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
41
|
wheels = [
|
|
43
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
42
|
+
{ 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
43
|
]
|
|
45
44
|
|
|
46
45
|
[[package]]
|
|
@@ -1104,15 +1103,6 @@ wheels = [
|
|
|
1104
1103
|
{ 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
1104
|
]
|
|
1106
1105
|
|
|
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
1106
|
[[package]]
|
|
1117
1107
|
name = "sse-starlette"
|
|
1118
1108
|
version = "2.3.6"
|