@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.
@@ -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 createErrorServiceStatus(name, url, error, defaultStatus = 'unhealthy', defaultDetails) {
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
  /**
@@ -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
- switch (clusterInfo.type) {
60
- case 'minikube':
61
- try {
62
- const { stdout } = await execa('minikube', ['ip']);
63
- ip = stdout.trim();
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
  }
@@ -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.71",
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.6-alpine AS go-builder
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