@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.
- package/dist/arkServices.js +17 -12
- package/dist/commands/generate/generators/project.js +2 -2
- package/dist/commands/install/index.js +22 -0
- package/dist/commands/install/index.spec.js +143 -0
- package/dist/commands/models/create.spec.js +20 -1
- package/dist/commands/models/kubernetes/secret-manager.d.ts +3 -0
- package/dist/commands/models/kubernetes/secret-manager.js +40 -0
- package/package.json +2 -2
- package/templates/mcp-server/Dockerfile +2 -2
- package/templates/mcp-server/chart/values.yaml +1 -1
- package/templates/project/values.yaml +2 -2
package/dist/arkServices.js
CHANGED
|
@@ -134,17 +134,20 @@ const defaultArkServices = {
|
|
|
134
134
|
k8sDeploymentName: 'ark-broker',
|
|
135
135
|
k8sDevDeploymentName: 'ark-broker-devspace',
|
|
136
136
|
},
|
|
137
|
-
'
|
|
138
|
-
name: '
|
|
139
|
-
helmReleaseName: '
|
|
140
|
-
description: '
|
|
141
|
-
enabled:
|
|
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
|
-
|
|
144
|
-
chartPath: `${
|
|
145
|
-
installArgs: [],
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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'
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
});
|
|
@@ -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.
|
|
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.
|
|
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 }}
|
|
@@ -49,7 +49,7 @@ security:
|
|
|
49
49
|
# Pod security context
|
|
50
50
|
podSecurityContext:
|
|
51
51
|
runAsNonRoot: true
|
|
52
|
-
runAsUser:
|
|
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:
|
|
60
|
+
runAsUser: 1001
|
|
61
61
|
capabilities:
|
|
62
62
|
drop:
|
|
63
63
|
- ALL
|