@agents-at-scale/ark 0.1.50 → 0.1.52

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.
@@ -134,17 +134,20 @@ const defaultArkServices = {
134
134
  k8sDeploymentName: 'ark-broker',
135
135
  k8sDevDeploymentName: 'ark-broker-devspace',
136
136
  },
137
- 'mcp-filesystem': {
138
- name: 'mcp-filesystem',
139
- helmReleaseName: 'mcp-filesystem',
140
- description: 'Stateful filesystem MCP server with workspace isolation',
141
- enabled: false,
137
+ 'file-gateway': {
138
+ name: 'file-gateway',
139
+ helmReleaseName: 'file-gateway',
140
+ description: 'S3-compatible file storage gateway with REST API for shared storage access',
141
+ enabled: true,
142
142
  category: 'service',
143
- // namespace: undefined - uses current context namespace
144
- chartPath: `${REGISTRY_BASE}/mcp-filesystem`,
145
- installArgs: [],
146
- k8sDeploymentName: 'mcp-filesystem',
147
- k8sDevDeploymentName: 'mcp-filesystem-devspace',
143
+ namespace: 'default',
144
+ chartPath: `${getMarketplaceRegistry()}/file-gateway`,
145
+ installArgs: ['--create-namespace'],
146
+ k8sServiceName: 'file-gateway-file-api',
147
+ k8sServicePort: 8080,
148
+ k8sPortForwardLocalPort: undefined,
149
+ k8sDeploymentName: 'file-gateway-file-api',
150
+ k8sDevDeploymentName: undefined,
148
151
  },
149
152
  'localhost-gateway': {
150
153
  name: 'localhost-gateway',
@@ -156,7 +159,7 @@ const defaultArkServices = {
156
159
  chartPath: `${REGISTRY_BASE}/localhost-gateway`,
157
160
  installArgs: [],
158
161
  },
159
- 'noah': {
162
+ noah: {
160
163
  name: 'noah',
161
164
  helmReleaseName: 'noah',
162
165
  description: 'Runtime administration agent with cluster privileges',
@@ -176,7 +179,9 @@ function applyConfigOverrides(defaults) {
176
179
  for (const [key, service] of Object.entries(defaults)) {
177
180
  const override = overrides[key];
178
181
  result[key] =
179
- override && typeof override === 'object' ? { ...service, ...override } : service;
182
+ override && typeof override === 'object'
183
+ ? { ...service, ...override }
184
+ : service;
180
185
  }
181
186
  return result;
182
187
  }
@@ -727,7 +727,7 @@ Generated with ARK CLI generator`;
727
727
  },
728
728
  ];
729
729
  if (config.projectType === 'empty') {
730
- steps.push({ desc: 'Add YAML files to agents/, teams/, queries/ directories' }, { desc: 'Copy model configurations from samples/models/' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'devspace dev' });
730
+ steps.push({ desc: 'Add YAML files to agents/, teams/, queries/ directories' }, { desc: 'Use either the default model already in models/ or a configuration template from samples/models/ of ARK repository' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'devspace dev' });
731
731
  }
732
732
  else if (config.selectedModels && config.selectedModels !== 'none') {
733
733
  steps.push({ desc: 'Edit .env file to set your API keys' }, { desc: 'Load environment variables', cmd: 'source .env' }, { desc: 'Deploy your project', cmd: 'devspace dev' }, {
@@ -736,7 +736,7 @@ Generated with ARK CLI generator`;
736
736
  });
737
737
  }
738
738
  else {
739
- steps.push({ desc: 'Copy model configurations from samples/models/' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'devspace dev' });
739
+ steps.push({ desc: 'Use either the default model already in models/ or a configuration template from samples/models/ of ARK repository' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'devspace dev' });
740
740
  }
741
741
  console.log(chalk.magenta.bold('🚀 NEXT STEPS:\n'));
742
742
  let stepNumber = 1;
@@ -21,8 +21,30 @@ async function uninstallPrerequisites(service, verbose = false) {
21
21
  await execute('helm', helmArgs, { stdio: 'inherit' }, { verbose });
22
22
  }
23
23
  }
24
+ async function checkAndCleanFailedRelease(releaseName, namespace, verbose = false) {
25
+ const statusArgs = ['status', releaseName];
26
+ if (namespace) {
27
+ statusArgs.push('--namespace', namespace);
28
+ }
29
+ try {
30
+ const result = await execute('helm', statusArgs, {}, { verbose: false });
31
+ const stdout = String(result.stdout || '');
32
+ if (stdout.includes('STATUS: pending-install') ||
33
+ stdout.includes('STATUS: failed') ||
34
+ stdout.includes('STATUS: uninstalling')) {
35
+ const uninstallArgs = ['uninstall', releaseName];
36
+ if (namespace) {
37
+ uninstallArgs.push('--namespace', namespace);
38
+ }
39
+ await execute('helm', uninstallArgs, { stdio: 'inherit' }, { verbose });
40
+ }
41
+ }
42
+ catch {
43
+ }
44
+ }
24
45
  async function installService(service, verbose = false) {
25
46
  await uninstallPrerequisites(service, verbose);
47
+ await checkAndCleanFailedRelease(service.helmReleaseName, service.namespace, verbose);
26
48
  const helmArgs = [
27
49
  'upgrade',
28
50
  '--install',
@@ -140,4 +140,147 @@ describe('install command', () => {
140
140
  await expect(command.parseAsync(['node', 'test', 'ark-api'])).rejects.toThrow('process.exit called');
141
141
  expect(mockExit).toHaveBeenCalledWith(1);
142
142
  });
143
+ describe('checkAndCleanFailedRelease', () => {
144
+ it('uninstalls release in pending-install state', async () => {
145
+ const mockService = {
146
+ name: 'ark-api',
147
+ helmReleaseName: 'ark-api',
148
+ chartPath: './charts/ark-api',
149
+ namespace: 'ark-system',
150
+ };
151
+ mockGetInstallableServices.mockReturnValue({
152
+ 'ark-api': mockService,
153
+ });
154
+ mockExeca
155
+ .mockResolvedValueOnce({
156
+ stdout: 'NAME: ark-api\nSTATUS: pending-install\n',
157
+ })
158
+ .mockResolvedValueOnce({})
159
+ .mockResolvedValueOnce({});
160
+ const command = createInstallCommand(mockConfig);
161
+ await command.parseAsync(['node', 'test', 'ark-api']);
162
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['status', 'ark-api', '--namespace', 'ark-system'], {});
163
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-api', '--namespace', 'ark-system'], { stdio: 'inherit' });
164
+ expect(mockExeca).toHaveBeenCalledWith('helm', [
165
+ 'upgrade',
166
+ '--install',
167
+ 'ark-api',
168
+ './charts/ark-api',
169
+ '--namespace',
170
+ 'ark-system',
171
+ ], { stdio: 'inherit' });
172
+ });
173
+ it('uninstalls release in failed state', async () => {
174
+ const mockService = {
175
+ name: 'ark-api',
176
+ helmReleaseName: 'ark-api',
177
+ chartPath: './charts/ark-api',
178
+ namespace: 'ark-system',
179
+ };
180
+ mockGetInstallableServices.mockReturnValue({
181
+ 'ark-api': mockService,
182
+ });
183
+ mockExeca
184
+ .mockResolvedValueOnce({
185
+ stdout: 'NAME: ark-api\nSTATUS: failed\n',
186
+ })
187
+ .mockResolvedValueOnce({})
188
+ .mockResolvedValueOnce({});
189
+ const command = createInstallCommand(mockConfig);
190
+ await command.parseAsync(['node', 'test', 'ark-api']);
191
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-api', '--namespace', 'ark-system'], { stdio: 'inherit' });
192
+ });
193
+ it('uninstalls release in uninstalling state', async () => {
194
+ const mockService = {
195
+ name: 'ark-dashboard',
196
+ helmReleaseName: 'ark-dashboard',
197
+ chartPath: './charts/ark-dashboard',
198
+ namespace: 'default',
199
+ };
200
+ mockGetInstallableServices.mockReturnValue({
201
+ 'ark-dashboard': mockService,
202
+ });
203
+ mockExeca
204
+ .mockResolvedValueOnce({
205
+ stdout: 'NAME: ark-dashboard\nSTATUS: uninstalling\nREVISION: 2\n',
206
+ })
207
+ .mockResolvedValueOnce({})
208
+ .mockResolvedValueOnce({});
209
+ const command = createInstallCommand(mockConfig);
210
+ await command.parseAsync(['node', 'test', 'ark-dashboard']);
211
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-dashboard', '--namespace', 'default'], { stdio: 'inherit' });
212
+ });
213
+ it('does not uninstall release in deployed state', async () => {
214
+ const mockService = {
215
+ name: 'ark-api',
216
+ helmReleaseName: 'ark-api',
217
+ chartPath: './charts/ark-api',
218
+ namespace: 'ark-system',
219
+ };
220
+ mockGetInstallableServices.mockReturnValue({
221
+ 'ark-api': mockService,
222
+ });
223
+ mockExeca
224
+ .mockResolvedValueOnce({
225
+ stdout: 'NAME: ark-api\nSTATUS: deployed\n',
226
+ })
227
+ .mockResolvedValueOnce({});
228
+ const command = createInstallCommand(mockConfig);
229
+ await command.parseAsync(['node', 'test', 'ark-api']);
230
+ const uninstallCalls = mockExeca.mock.calls.filter((call) => call[0] === 'helm' && call[1][0] === 'uninstall');
231
+ expect(uninstallCalls).toHaveLength(0);
232
+ expect(mockExeca).toHaveBeenCalledWith('helm', [
233
+ 'upgrade',
234
+ '--install',
235
+ 'ark-api',
236
+ './charts/ark-api',
237
+ '--namespace',
238
+ 'ark-system',
239
+ ], { stdio: 'inherit' });
240
+ });
241
+ it('handles helm status errors gracefully', async () => {
242
+ const mockService = {
243
+ name: 'ark-api',
244
+ helmReleaseName: 'ark-api',
245
+ chartPath: './charts/ark-api',
246
+ namespace: 'ark-system',
247
+ };
248
+ mockGetInstallableServices.mockReturnValue({
249
+ 'ark-api': mockService,
250
+ });
251
+ mockExeca
252
+ .mockRejectedValueOnce(new Error('release not found'))
253
+ .mockResolvedValueOnce({});
254
+ const command = createInstallCommand(mockConfig);
255
+ await command.parseAsync(['node', 'test', 'ark-api']);
256
+ expect(mockExeca).toHaveBeenCalledWith('helm', [
257
+ 'upgrade',
258
+ '--install',
259
+ 'ark-api',
260
+ './charts/ark-api',
261
+ '--namespace',
262
+ 'ark-system',
263
+ ], { stdio: 'inherit' });
264
+ });
265
+ it('handles service without namespace', async () => {
266
+ const mockService = {
267
+ name: 'ark-dashboard',
268
+ helmReleaseName: 'ark-dashboard',
269
+ chartPath: './charts/ark-dashboard',
270
+ };
271
+ mockGetInstallableServices.mockReturnValue({
272
+ 'ark-dashboard': mockService,
273
+ });
274
+ mockExeca
275
+ .mockResolvedValueOnce({
276
+ stdout: 'NAME: ark-dashboard\nSTATUS: failed\n',
277
+ })
278
+ .mockResolvedValueOnce({})
279
+ .mockResolvedValueOnce({});
280
+ const command = createInstallCommand(mockConfig);
281
+ await command.parseAsync(['node', 'test', 'ark-dashboard']);
282
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['status', 'ark-dashboard'], {});
283
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-dashboard'], { stdio: 'inherit' });
284
+ });
285
+ });
143
286
  });
@@ -134,15 +134,34 @@ describe('createModel', () => {
134
134
  expect(mockOutput.info).toHaveBeenCalledWith('model creation cancelled');
135
135
  });
136
136
  it('handles secret creation failure', async () => {
137
- mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
137
+ mockExeca
138
+ .mockRejectedValueOnce(new Error('not found')); // model doesn't exist
138
139
  mockInquirer.prompt
139
140
  .mockResolvedValueOnce({ modelType: 'openai' })
140
141
  .mockResolvedValueOnce({ model: 'gpt-4' })
141
142
  .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com' })
142
143
  .mockResolvedValueOnce({ apiKey: 'secret' });
144
+ mockExeca
145
+ .mockRejectedValueOnce(new Error('not found')); // secret doesn't exist check
143
146
  mockExeca.mockRejectedValueOnce(new Error('secret creation failed')); // create secret fails
144
147
  const result = await createModel('test-model');
145
148
  expect(result).toBe(false);
146
149
  expect(mockOutput.error).toHaveBeenCalledWith('failed to create secret');
147
150
  });
151
+ it('updates existing secret when secret already exists', async () => {
152
+ mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
153
+ mockInquirer.prompt
154
+ .mockResolvedValueOnce({ modelType: 'openai' })
155
+ .mockResolvedValueOnce({ model: 'gpt-4' })
156
+ .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com' })
157
+ .mockResolvedValueOnce({ apiKey: 'new-secret-key' });
158
+ mockExeca.mockResolvedValueOnce({}); // secret exists check
159
+ mockExeca.mockResolvedValueOnce({ stdout: 'secret yaml' }); // dry-run output
160
+ mockExeca.mockResolvedValueOnce({}); // kubectl apply
161
+ mockExeca.mockResolvedValueOnce({}); // apply model
162
+ const result = await createModel('test-model');
163
+ expect(result).toBe(true);
164
+ expect(mockOutput.success).toHaveBeenCalledWith('updated secret test-model-model-secret');
165
+ expect(mockOutput.success).toHaveBeenCalledWith('model test-model created');
166
+ });
148
167
  });
@@ -4,4 +4,7 @@ export interface SecretManager {
4
4
  }
5
5
  export declare class KubernetesSecretManager implements SecretManager {
6
6
  createSecret(config: ProviderConfig): Promise<void>;
7
+ private secretExists;
8
+ private createNewSecret;
9
+ private updateSecret;
7
10
  }
@@ -3,6 +3,24 @@ import output from '../../../lib/output.js';
3
3
  // Kubernetes secret manager implementation
4
4
  export class KubernetesSecretManager {
5
5
  async createSecret(config) {
6
+ const secretExists = await this.secretExists(config.secretName);
7
+ if (secretExists) {
8
+ await this.updateSecret(config);
9
+ }
10
+ else {
11
+ await this.createNewSecret(config);
12
+ }
13
+ }
14
+ async secretExists(secretName) {
15
+ try {
16
+ await execa('kubectl', ['get', 'secret', secretName], { stdio: 'pipe' });
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ async createNewSecret(config) {
6
24
  const secretArgs = ['create', 'secret', 'generic', config.secretName];
7
25
  if (config.type === 'bedrock') {
8
26
  secretArgs.push(`--from-literal=access-key-id=${config.accessKeyId}`);
@@ -13,8 +31,30 @@ export class KubernetesSecretManager {
13
31
  }
14
32
  else {
15
33
  secretArgs.push(`--from-literal=api-key=${config.apiKey}`);
34
+ secretArgs.push(`--from-literal=token=${config.apiKey}`);
16
35
  }
17
36
  await execa('kubectl', secretArgs, { stdio: 'pipe' });
18
37
  output.success(`created secret ${config.secretName}`);
19
38
  }
39
+ async updateSecret(config) {
40
+ const secretArgs = ['create', 'secret', 'generic', config.secretName];
41
+ if (config.type === 'bedrock') {
42
+ secretArgs.push(`--from-literal=access-key-id=${config.accessKeyId}`);
43
+ secretArgs.push(`--from-literal=secret-access-key=${config.secretAccessKey}`);
44
+ if (config.sessionToken) {
45
+ secretArgs.push(`--from-literal=session-token=${config.sessionToken}`);
46
+ }
47
+ }
48
+ else {
49
+ secretArgs.push(`--from-literal=api-key=${config.apiKey}`);
50
+ secretArgs.push(`--from-literal=token=${config.apiKey}`);
51
+ }
52
+ secretArgs.push('--dry-run=client', '-o', 'yaml');
53
+ const { stdout } = await execa('kubectl', secretArgs, { stdio: 'pipe' });
54
+ await execa('kubectl', ['apply', '-f', '-'], {
55
+ input: stdout,
56
+ stdio: ['pipe', 'pipe', 'pipe'],
57
+ });
58
+ output.success(`updated secret ${config.secretName}`);
59
+ }
20
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-at-scale/ark",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "Ark CLI - Interactive terminal interface for ARK agents",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -60,7 +60,7 @@
60
60
  "open": "^10.2.0",
61
61
  "openai": "^5.19.1",
62
62
  "ora": "^8.2.0",
63
- "react": "^19.1.0",
63
+ "react": "^19.1.5",
64
64
  "yaml": "^2.6.1"
65
65
  },
66
66
  "devDependencies": {
@@ -1,5 +1,5 @@
1
1
  {{- if eq .Values.technology "node" }}
2
- FROM node:22
2
+ FROM node:22.22.0-alpine
3
3
 
4
4
  # Install the MCP server package
5
5
  {{- if .Values.packageSource }}
@@ -78,7 +78,7 @@ WORKDIR /app
78
78
  RUN go build -o {{ .Values.mcpServerName }} .
79
79
  {{- end }}
80
80
 
81
- FROM node:22
81
+ FROM node:22.22.0-alpine
82
82
  {{- if eq .Values.packageSource "go-install" }}
83
83
  COPY --from=go-builder /go/bin/{{ .Values.packageName }} /usr/local/bin/
84
84
  {{- else }}
@@ -27,7 +27,7 @@ securityContext:
27
27
  - ALL
28
28
  readOnlyRootFilesystem: true
29
29
  runAsNonRoot: true
30
- runAsUser: 1000
30
+ runAsUser: 1001
31
31
 
32
32
  service:
33
33
  type: ClusterIP
@@ -49,7 +49,7 @@ security:
49
49
  # Pod security context
50
50
  podSecurityContext:
51
51
  runAsNonRoot: true
52
- runAsUser: 1000
52
+ runAsUser: 1001
53
53
  fsGroup: 2000
54
54
 
55
55
  # Container security context
@@ -57,7 +57,7 @@ security:
57
57
  allowPrivilegeEscalation: false
58
58
  readOnlyRootFilesystem: true
59
59
  runAsNonRoot: true
60
- runAsUser: 1000
60
+ runAsUser: 1001
61
61
  capabilities:
62
62
  drop:
63
63
  - ALL