@agents-at-scale/ark 0.1.71 → 0.1.72
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/commands/status/index.js +1 -0
- package/dist/components/statusChecker.js +17 -3
- package/dist/lib/cluster.js +64 -86
- package/dist/lib/readinessChecks.d.ts +3 -0
- package/dist/lib/readinessChecks.js +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/package.json +3 -2
- package/templates/mcp-server/Dockerfile +1 -1
|
@@ -19,6 +19,7 @@ function enrichServiceDetails(service) {
|
|
|
19
19
|
warning: { icon: '⚠', text: 'warning', color: 'yellow' },
|
|
20
20
|
'not ready': { icon: '○', text: 'not ready', color: 'yellow' },
|
|
21
21
|
'not installed': { icon: '?', text: 'not installed', color: 'yellow' },
|
|
22
|
+
'no access': { icon: '⊘', text: 'no access', color: 'yellow' },
|
|
22
23
|
};
|
|
23
24
|
const statusInfo = statusMap[service.status] || {
|
|
24
25
|
icon: '?',
|
|
@@ -52,8 +52,22 @@ export const getKindVersion = () => ({
|
|
|
52
52
|
return match ? match[1] : output.trim();
|
|
53
53
|
},
|
|
54
54
|
});
|
|
55
|
-
function
|
|
55
|
+
function isForbiddenError(error) {
|
|
56
|
+
return error instanceof Error && /forbidden/i.test(error.message);
|
|
57
|
+
}
|
|
58
|
+
function createErrorServiceStatus(name, url, error, defaultStatus = 'unhealthy', defaultDetails, namespace) {
|
|
56
59
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
60
|
+
if (isForbiddenError(error)) {
|
|
61
|
+
return {
|
|
62
|
+
name,
|
|
63
|
+
status: 'no access',
|
|
64
|
+
url,
|
|
65
|
+
details: namespace
|
|
66
|
+
? `No permission to read namespace '${namespace}'`
|
|
67
|
+
: 'No permission to read this resource',
|
|
68
|
+
namespace,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
57
71
|
return {
|
|
58
72
|
name,
|
|
59
73
|
status: defaultStatus,
|
|
@@ -246,7 +260,7 @@ export class StatusChecker {
|
|
|
246
260
|
namespace,
|
|
247
261
|
};
|
|
248
262
|
}
|
|
249
|
-
return createErrorServiceStatus(serviceName, '', error, 'unhealthy', `Failed to check deployment: ${errorMessage}
|
|
263
|
+
return createErrorServiceStatus(serviceName, '', error, 'unhealthy', `Failed to check deployment: ${errorMessage}`, namespace);
|
|
250
264
|
}
|
|
251
265
|
}
|
|
252
266
|
/**
|
|
@@ -288,7 +302,7 @@ export class StatusChecker {
|
|
|
288
302
|
}
|
|
289
303
|
catch (error) {
|
|
290
304
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
291
|
-
return createErrorServiceStatus(serviceName, '', error, 'unhealthy', `Failed to check helm status: ${errorMessage}
|
|
305
|
+
return createErrorServiceStatus(serviceName, '', error, 'unhealthy', `Failed to check helm status: ${errorMessage}`, namespace);
|
|
292
306
|
}
|
|
293
307
|
}
|
|
294
308
|
/**
|
package/dist/lib/cluster.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
|
+
import { classifyFailure } from './readinessChecks.js';
|
|
2
3
|
export async function detectClusterType() {
|
|
3
4
|
try {
|
|
4
5
|
const { stdout } = await execa('kubectl', ['config', 'current-context']);
|
|
@@ -31,7 +32,59 @@ export async function detectClusterType() {
|
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
}
|
|
35
|
+
const NODE_INTERNAL_IP_JSONPATH = 'jsonpath={.items[0].status.addresses[?(@.type=="InternalIP")].address}';
|
|
36
|
+
const NODE_EXTERNAL_IP_JSONPATH = 'jsonpath={.items[0].status.addresses[?(@.type=="ExternalIP")].address}';
|
|
37
|
+
async function getNodeIp(jsonpath) {
|
|
38
|
+
const { stdout } = await execa('kubectl', ['get', 'nodes', '-o', jsonpath]);
|
|
39
|
+
return stdout.trim();
|
|
40
|
+
}
|
|
41
|
+
async function getIstioGatewayAddress(field) {
|
|
42
|
+
const { stdout } = await execa('kubectl', [
|
|
43
|
+
'get',
|
|
44
|
+
'svc',
|
|
45
|
+
'-n',
|
|
46
|
+
'istio-system',
|
|
47
|
+
'istio-ingressgateway',
|
|
48
|
+
'-o',
|
|
49
|
+
`jsonpath={.status.loadBalancer.ingress[0].${field}}`,
|
|
50
|
+
]);
|
|
51
|
+
return stdout.trim();
|
|
52
|
+
}
|
|
53
|
+
async function resolveClusterIp(type) {
|
|
54
|
+
switch (type) {
|
|
55
|
+
case 'minikube':
|
|
56
|
+
try {
|
|
57
|
+
const { stdout } = await execa('minikube', ['ip']);
|
|
58
|
+
return stdout.trim();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return getNodeIp(NODE_INTERNAL_IP_JSONPATH);
|
|
62
|
+
}
|
|
63
|
+
case 'docker-desktop':
|
|
64
|
+
return 'localhost';
|
|
65
|
+
case 'cloud':
|
|
66
|
+
try {
|
|
67
|
+
const ip = await getIstioGatewayAddress('ip');
|
|
68
|
+
return ip || (await getIstioGatewayAddress('hostname'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return getNodeIp(NODE_EXTERNAL_IP_JSONPATH);
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return getNodeIp(NODE_INTERNAL_IP_JSONPATH);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isForbiddenError(error) {
|
|
78
|
+
if (!(error instanceof Error)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const stderr = error.stderr;
|
|
82
|
+
const text = typeof stderr === 'string' && stderr ? stderr : error.message;
|
|
83
|
+
return classifyFailure(text) === 'forbidden';
|
|
84
|
+
}
|
|
34
85
|
export async function getClusterInfo(context) {
|
|
86
|
+
let resolvedContext;
|
|
87
|
+
let resolvedNamespace;
|
|
35
88
|
try {
|
|
36
89
|
// If context is provided, use it
|
|
37
90
|
const contextArgs = context ? ['--context', context] : [];
|
|
@@ -48,6 +101,8 @@ export async function getClusterInfo(context) {
|
|
|
48
101
|
const currentContext = config['current-context'] || '';
|
|
49
102
|
const contextData = config.contexts?.find((c) => c.name === currentContext);
|
|
50
103
|
const namespace = contextData?.context?.namespace || 'default';
|
|
104
|
+
resolvedContext = currentContext;
|
|
105
|
+
resolvedNamespace = namespace;
|
|
51
106
|
// Detect cluster type from context name
|
|
52
107
|
const clusterInfo = await detectClusterType();
|
|
53
108
|
clusterInfo.context = currentContext;
|
|
@@ -56,99 +111,22 @@ export async function getClusterInfo(context) {
|
|
|
56
111
|
return clusterInfo;
|
|
57
112
|
}
|
|
58
113
|
let ip;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
catch {
|
|
66
|
-
// Fallback to kubectl if minikube command fails
|
|
67
|
-
const { stdout } = await execa('kubectl', [
|
|
68
|
-
'get',
|
|
69
|
-
'nodes',
|
|
70
|
-
'-o',
|
|
71
|
-
'jsonpath={.items[0].status.addresses[?(@.type=="InternalIP")].address}',
|
|
72
|
-
]);
|
|
73
|
-
ip = stdout.trim();
|
|
74
|
-
}
|
|
75
|
-
break;
|
|
76
|
-
case 'kind': {
|
|
77
|
-
const { stdout: kindOutput } = await execa('kubectl', [
|
|
78
|
-
'get',
|
|
79
|
-
'nodes',
|
|
80
|
-
'-o',
|
|
81
|
-
'jsonpath={.items[0].status.addresses[?(@.type=="InternalIP")].address}',
|
|
82
|
-
]);
|
|
83
|
-
ip = kindOutput.trim();
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
case 'docker-desktop':
|
|
87
|
-
ip = 'localhost';
|
|
88
|
-
break;
|
|
89
|
-
case 'k3s': {
|
|
90
|
-
const { stdout: k3sOutput } = await execa('kubectl', [
|
|
91
|
-
'get',
|
|
92
|
-
'nodes',
|
|
93
|
-
'-o',
|
|
94
|
-
'jsonpath={.items[0].status.addresses[?(@.type=="InternalIP")].address}',
|
|
95
|
-
]);
|
|
96
|
-
ip = k3sOutput.trim();
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
case 'cloud':
|
|
100
|
-
// For cloud clusters, try to get the external IP or load balancer IP
|
|
101
|
-
try {
|
|
102
|
-
const { stdout: lbOutput } = await execa('kubectl', [
|
|
103
|
-
'get',
|
|
104
|
-
'svc',
|
|
105
|
-
'-n',
|
|
106
|
-
'istio-system',
|
|
107
|
-
'istio-ingressgateway',
|
|
108
|
-
'-o',
|
|
109
|
-
'jsonpath={.status.loadBalancer.ingress[0].ip}',
|
|
110
|
-
]);
|
|
111
|
-
ip = lbOutput.trim();
|
|
112
|
-
if (!ip) {
|
|
113
|
-
const { stdout: hostnameOutput } = await execa('kubectl', [
|
|
114
|
-
'get',
|
|
115
|
-
'svc',
|
|
116
|
-
'-n',
|
|
117
|
-
'istio-system',
|
|
118
|
-
'istio-ingressgateway',
|
|
119
|
-
'-o',
|
|
120
|
-
'jsonpath={.status.loadBalancer.ingress[0].hostname}',
|
|
121
|
-
]);
|
|
122
|
-
ip = hostnameOutput.trim();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
// Fallback to node IP
|
|
127
|
-
const { stdout: nodeOutput } = await execa('kubectl', [
|
|
128
|
-
'get',
|
|
129
|
-
'nodes',
|
|
130
|
-
'-o',
|
|
131
|
-
'jsonpath={.items[0].status.addresses[?(@.type=="ExternalIP")].address}',
|
|
132
|
-
]);
|
|
133
|
-
ip = nodeOutput.trim();
|
|
134
|
-
}
|
|
135
|
-
break;
|
|
136
|
-
default: {
|
|
137
|
-
const { stdout: defaultOutput } = await execa('kubectl', [
|
|
138
|
-
'get',
|
|
139
|
-
'nodes',
|
|
140
|
-
'-o',
|
|
141
|
-
'jsonpath={.items[0].status.addresses[?(@.type=="InternalIP")].address}',
|
|
142
|
-
]);
|
|
143
|
-
ip = defaultOutput.trim();
|
|
144
|
-
break;
|
|
114
|
+
try {
|
|
115
|
+
ip = await resolveClusterIp(clusterInfo.type);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (!isForbiddenError(error)) {
|
|
119
|
+
throw error;
|
|
145
120
|
}
|
|
121
|
+
ip = undefined;
|
|
146
122
|
}
|
|
147
123
|
return { ...clusterInfo, ip };
|
|
148
124
|
}
|
|
149
125
|
catch (error) {
|
|
150
126
|
return {
|
|
151
127
|
type: 'unknown',
|
|
128
|
+
...(resolvedContext !== undefined && { context: resolvedContext }),
|
|
129
|
+
...(resolvedNamespace !== undefined && { namespace: resolvedNamespace }),
|
|
152
130
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
153
131
|
};
|
|
154
132
|
}
|
|
@@ -20,6 +20,8 @@ export interface NamespaceMemoryCheck {
|
|
|
20
20
|
message: string;
|
|
21
21
|
}
|
|
22
22
|
export type ReadinessProgress = (result: ReadinessCheckResult) => void;
|
|
23
|
+
type FailureReason = 'not-found' | 'forbidden' | 'unreachable' | 'undetermined';
|
|
24
|
+
export declare function classifyFailure(stderr: string): FailureReason;
|
|
23
25
|
export declare function describeStorageBackend(): Promise<BackendDetection>;
|
|
24
26
|
/**
|
|
25
27
|
* Report whether the namespace ends up with a Memory the executor can use.
|
|
@@ -39,3 +41,4 @@ export declare function describeStorageBackend(): Promise<BackendDetection>;
|
|
|
39
41
|
export declare function describeNamespaceMemory(namespace: string): Promise<NamespaceMemoryCheck>;
|
|
40
42
|
export declare function detectStorageBackend(): Promise<DetectedBackend>;
|
|
41
43
|
export declare function runReadinessChecks(timeoutSeconds: number, backend: DetectedBackend, onProgress?: ReadinessProgress): Promise<ReadinessCheckResult[]>;
|
|
44
|
+
export {};
|
|
@@ -19,7 +19,7 @@ async function runKubectl(args, timeoutMs) {
|
|
|
19
19
|
// aggregated API server serves no arkconfigs, so this is the normal reply
|
|
20
20
|
// there rather than something to retry.
|
|
21
21
|
const UNSERVED_RESOURCE_PATTERN = /doesn't have a resource type|no matches for kind|the server could not find the requested resource/i;
|
|
22
|
-
function classifyFailure(stderr) {
|
|
22
|
+
export function classifyFailure(stderr) {
|
|
23
23
|
if (/not\s*found/i.test(stderr) || UNSERVED_RESOURCE_PATTERN.test(stderr)) {
|
|
24
24
|
return 'not-found';
|
|
25
25
|
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface KubernetesConfig {
|
|
|
16
16
|
export type DeploymentStatus = 'available' | 'progressing' | 'replicafailure' | 'failed' | 'not found' | 'unknown';
|
|
17
17
|
export type ServiceStatus = {
|
|
18
18
|
name: string;
|
|
19
|
-
status: 'healthy' | 'warning' | 'unhealthy' | 'not installed' | 'not ready';
|
|
19
|
+
status: 'healthy' | 'warning' | 'unhealthy' | 'not installed' | 'not ready' | 'no access';
|
|
20
20
|
deploymentStatus?: DeploymentStatus;
|
|
21
21
|
url?: string;
|
|
22
22
|
version?: string;
|
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.72",
|
|
4
4
|
"description": "Ark CLI - Interactive terminal interface for ARK agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"form-data": "^4.0.6",
|
|
103
103
|
"postcss": "^8.5.18",
|
|
104
104
|
"proxy-addr": "^2.0.8",
|
|
105
|
-
"source-map-js": "^1.2.2"
|
|
105
|
+
"source-map-js": "^1.2.2",
|
|
106
|
+
"follow-redirects": "^1.16.1"
|
|
106
107
|
}
|
|
107
108
|
}
|
|
@@ -63,7 +63,7 @@ ENTRYPOINT [ "/bin/bash", "-c", "mcp-proxy --debug --port 8080 --host 0.0.0.0 --
|
|
|
63
63
|
{{- end }}
|
|
64
64
|
|
|
65
65
|
{{- else if eq .Values.technology "go" }}
|
|
66
|
-
FROM golang:1.26.
|
|
66
|
+
FROM golang:1.26.9-alpine AS go-builder
|
|
67
67
|
{{- if .Values.packageSource }}
|
|
68
68
|
{{- if eq .Values.packageSource "go-install" }}
|
|
69
69
|
RUN go install {{ .Values.packageName }}@latest
|