@agents-at-scale/ark 0.1.63 → 0.1.66
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/agents/index.js +13 -1
- package/dist/commands/install/index.js +33 -15
- package/dist/commands/mcp/authClient.d.ts +37 -0
- package/dist/commands/mcp/authClient.js +47 -0
- package/dist/commands/mcp/index.d.ts +3 -0
- package/dist/commands/mcp/index.js +43 -0
- package/dist/commands/mcp/login.d.ts +21 -0
- package/dist/commands/mcp/login.js +130 -0
- package/dist/commands/mcp/logout.d.ts +16 -0
- package/dist/commands/mcp/logout.js +65 -0
- package/dist/commands/mcp/namespace.d.ts +1 -0
- package/dist/commands/mcp/namespace.js +17 -0
- package/dist/commands/mcp/timeout.d.ts +1 -0
- package/dist/commands/mcp/timeout.js +23 -0
- package/dist/commands/models/kubernetes/manifest-builder.js +30 -18
- package/dist/commands/models/kubernetes/secret-manager.js +18 -8
- package/dist/commands/models/providers/bedrock.d.ts +7 -2
- package/dist/commands/models/providers/bedrock.js +91 -42
- package/dist/commands/query/index.js +11 -1
- package/dist/commands/status/index.js +39 -5
- package/dist/index.js +2 -0
- package/dist/lib/arkApiClient.d.ts +4 -0
- package/dist/lib/arkApiClient.js +3 -0
- package/dist/lib/chatClient.d.ts +2 -0
- package/dist/lib/chatClient.js +25 -1
- package/dist/lib/executeQuery.d.ts +4 -1
- package/dist/lib/executeQuery.js +19 -0
- package/dist/lib/readinessChecks.d.ts +10 -2
- package/dist/lib/readinessChecks.js +93 -4
- package/dist/lib/types.d.ts +5 -0
- package/package.json +9 -5
- package/templates/mcp-server/Dockerfile +1 -1
- package/templates/models/azure.yaml +7 -6
- package/templates/models/claude.yaml +1 -1
- package/templates/models/gemini.yaml +1 -1
- package/templates/models/openai.yaml +1 -1
- package/templates/query/query.template.yaml +3 -3
- package/templates/tool/uv.lock +60 -60
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { execa } from 'execa';
|
|
3
|
+
import chalk from 'chalk';
|
|
3
4
|
import output from '../../lib/output.js';
|
|
4
|
-
import { executeQuery } from '../../lib/executeQuery.js';
|
|
5
|
+
import { executeQuery, parseParameters } from '../../lib/executeQuery.js';
|
|
6
|
+
import { ExitCodes } from '../../lib/errors.js';
|
|
5
7
|
async function listAgents(options) {
|
|
6
8
|
try {
|
|
7
9
|
// Use kubectl to get agents
|
|
@@ -55,12 +57,22 @@ export function createAgentsCommand(config) {
|
|
|
55
57
|
.argument('<name>', 'Agent name')
|
|
56
58
|
.argument('<message>', 'Message to send')
|
|
57
59
|
.option('--timeout <timeout>', 'Query timeout (e.g., 30s, 5m, 1h)')
|
|
60
|
+
.option('-p, --parameter <name=value>', 'Template parameter in name=value format (can be used multiple times)', (val, acc) => [...acc, val], [])
|
|
58
61
|
.action(async (name, message, options) => {
|
|
62
|
+
let parameters;
|
|
63
|
+
try {
|
|
64
|
+
parameters = parseParameters(options.parameter || []);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
68
|
+
process.exit(ExitCodes.CliError);
|
|
69
|
+
}
|
|
59
70
|
await executeQuery({
|
|
60
71
|
targetType: 'agent',
|
|
61
72
|
targetName: name,
|
|
62
73
|
message,
|
|
63
74
|
timeout: options.timeout || config.queryTimeout,
|
|
75
|
+
parameters,
|
|
64
76
|
});
|
|
65
77
|
});
|
|
66
78
|
return agentsCommand;
|
|
@@ -50,7 +50,7 @@ function backendInstallArgs(service, backend, values) {
|
|
|
50
50
|
function isValidVersion(version) {
|
|
51
51
|
return /^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/.test(version);
|
|
52
52
|
}
|
|
53
|
-
function
|
|
53
|
+
function notFoundVersion(error, options) {
|
|
54
54
|
let errorMsg = '';
|
|
55
55
|
if (error && typeof error === 'object') {
|
|
56
56
|
const err = error;
|
|
@@ -61,24 +61,30 @@ function isVersionNotFoundError(error, options) {
|
|
|
61
61
|
errorMsg = String(error);
|
|
62
62
|
}
|
|
63
63
|
if (options.arkVersion && errorMsg.includes(`:${options.arkVersion}: not found`)) {
|
|
64
|
-
return
|
|
64
|
+
return options.arkVersion;
|
|
65
65
|
}
|
|
66
66
|
if (options.marketplaceVersion && errorMsg.includes(`:${options.marketplaceVersion}: not found`)) {
|
|
67
|
-
return
|
|
67
|
+
return options.marketplaceVersion;
|
|
68
68
|
}
|
|
69
|
-
return
|
|
69
|
+
return null;
|
|
70
70
|
}
|
|
71
71
|
function handleInstallError(error, service, options) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return true; // should continue to next service
|
|
72
|
+
const version = notFoundVersion(error, options);
|
|
73
|
+
if (version !== null) {
|
|
74
|
+
return version; // should continue to next service
|
|
76
75
|
}
|
|
77
76
|
// Other errors still fail
|
|
78
77
|
output.error(`failed to install ${service.name}`);
|
|
79
78
|
console.error(error);
|
|
80
79
|
process.exit(1);
|
|
81
80
|
}
|
|
81
|
+
function exitIfServicesSkipped(skipped) {
|
|
82
|
+
if (skipped.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
const detail = skipped.map((s) => `${s.name}@${s.version}`).join(', ');
|
|
85
|
+
output.error(`installation incomplete: ${skipped.length} service(s) skipped because the requested version was not found: ${detail}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
82
88
|
async function uninstallPrerequisites(service, verbose = false) {
|
|
83
89
|
if (!service.prerequisiteUninstalls?.length)
|
|
84
90
|
return;
|
|
@@ -185,6 +191,7 @@ export async function installArk(config, serviceNames = [], options = {}) {
|
|
|
185
191
|
process.exit(1);
|
|
186
192
|
}
|
|
187
193
|
const backend = requestedBackend;
|
|
194
|
+
const skippedServices = [];
|
|
188
195
|
let postgresValues;
|
|
189
196
|
if (backend === 'postgresql') {
|
|
190
197
|
try {
|
|
@@ -233,7 +240,9 @@ export async function installArk(config, serviceNames = [], options = {}) {
|
|
|
233
240
|
output.success(`${service.name} installed successfully`);
|
|
234
241
|
}
|
|
235
242
|
catch (error) {
|
|
236
|
-
|
|
243
|
+
const skippedVersion = handleInstallError(error, service, options);
|
|
244
|
+
if (skippedVersion) {
|
|
245
|
+
skippedServices.push({ name: service.name, version: skippedVersion });
|
|
237
246
|
continue;
|
|
238
247
|
}
|
|
239
248
|
}
|
|
@@ -258,7 +267,7 @@ export async function installArk(config, serviceNames = [], options = {}) {
|
|
|
258
267
|
if (service.helmReleaseName === 'ark-apiserver' && backend === 'postgresql') {
|
|
259
268
|
const spinner = ora('Waiting for ark-apiserver to be ready...').start();
|
|
260
269
|
try {
|
|
261
|
-
const results = await runReadinessChecks(120); // 2 minute timeout
|
|
270
|
+
const results = await runReadinessChecks(120, backend); // 2 minute timeout
|
|
262
271
|
const failed = results.find((r) => !r.passed);
|
|
263
272
|
if (failed) {
|
|
264
273
|
spinner.fail(`ark-apiserver readiness check failed: ${failed.message || 'unknown error'}`);
|
|
@@ -274,11 +283,14 @@ export async function installArk(config, serviceNames = [], options = {}) {
|
|
|
274
283
|
}
|
|
275
284
|
}
|
|
276
285
|
catch (error) {
|
|
277
|
-
|
|
286
|
+
const skippedVersion = handleInstallError(error, service, options);
|
|
287
|
+
if (skippedVersion) {
|
|
288
|
+
skippedServices.push({ name: service.name, version: skippedVersion });
|
|
278
289
|
continue;
|
|
279
290
|
}
|
|
280
291
|
}
|
|
281
292
|
}
|
|
293
|
+
exitIfServicesSkipped(skippedServices);
|
|
282
294
|
return;
|
|
283
295
|
}
|
|
284
296
|
// If not using -y flag, show checklist interface
|
|
@@ -417,7 +429,7 @@ export async function installArk(config, serviceNames = [], options = {}) {
|
|
|
417
429
|
if (service.helmReleaseName === 'ark-apiserver' && backend === 'postgresql') {
|
|
418
430
|
const spinner = ora('Waiting for ark-apiserver to be ready...').start();
|
|
419
431
|
try {
|
|
420
|
-
const results = await runReadinessChecks(120); // 2 minute timeout
|
|
432
|
+
const results = await runReadinessChecks(120, backend); // 2 minute timeout
|
|
421
433
|
const failed = results.find((r) => !r.passed);
|
|
422
434
|
if (failed) {
|
|
423
435
|
spinner.fail(`ark-apiserver readiness check failed: ${failed.message || 'unknown error'}`);
|
|
@@ -434,7 +446,9 @@ export async function installArk(config, serviceNames = [], options = {}) {
|
|
|
434
446
|
console.log(); // Add blank line after command output
|
|
435
447
|
}
|
|
436
448
|
catch (error) {
|
|
437
|
-
|
|
449
|
+
const skippedVersion = handleInstallError(error, service, options);
|
|
450
|
+
if (skippedVersion) {
|
|
451
|
+
skippedServices.push({ name: service.name, version: skippedVersion });
|
|
438
452
|
console.log(); // Add blank line after warning
|
|
439
453
|
continue;
|
|
440
454
|
}
|
|
@@ -476,7 +490,9 @@ export async function installArk(config, serviceNames = [], options = {}) {
|
|
|
476
490
|
console.log(); // Add blank line after command output
|
|
477
491
|
}
|
|
478
492
|
catch (error) {
|
|
479
|
-
|
|
493
|
+
const skippedVersion = handleInstallError(error, service, options);
|
|
494
|
+
if (skippedVersion) {
|
|
495
|
+
skippedServices.push({ name: service.name, version: skippedVersion });
|
|
480
496
|
console.log(); // Add blank line after warning
|
|
481
497
|
continue;
|
|
482
498
|
}
|
|
@@ -496,7 +512,8 @@ export async function installArk(config, serviceNames = [], options = {}) {
|
|
|
496
512
|
s.category === 'core' &&
|
|
497
513
|
s.k8sDeploymentName &&
|
|
498
514
|
s.namespace &&
|
|
499
|
-
(!s.requiresBackend || s.requiresBackend === backend)
|
|
515
|
+
(!s.requiresBackend || s.requiresBackend === backend) &&
|
|
516
|
+
!skippedServices.some((sk) => sk.name === s.name));
|
|
500
517
|
const spinner = ora(`Waiting for Ark to be ready (timeout: ${timeoutSeconds}s)...`).start();
|
|
501
518
|
const statusMap = new Map();
|
|
502
519
|
servicesToWait.forEach((s) => statusMap.set(s.name, false));
|
|
@@ -526,6 +543,7 @@ export async function installArk(config, serviceNames = [], options = {}) {
|
|
|
526
543
|
process.exit(1);
|
|
527
544
|
}
|
|
528
545
|
}
|
|
546
|
+
exitIfServicesSkipped(skippedServices);
|
|
529
547
|
}
|
|
530
548
|
export function createInstallCommand(config) {
|
|
531
549
|
const command = new Command('install');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface AuthStartResponse {
|
|
2
|
+
auth_id: string;
|
|
3
|
+
authorization_url: string;
|
|
4
|
+
flow_expires_at: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AuthStatusResponse {
|
|
7
|
+
state: 'pending' | 'authorized' | 'failed' | 'expired';
|
|
8
|
+
expires_at?: string | null;
|
|
9
|
+
message?: string | null;
|
|
10
|
+
controller_state?: string | null;
|
|
11
|
+
controller_message?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface AuthLogoutResponse {
|
|
14
|
+
cleared_keys?: string[];
|
|
15
|
+
deleted?: boolean;
|
|
16
|
+
noop?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface AuthStartBody {
|
|
19
|
+
force?: boolean;
|
|
20
|
+
scope?: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface AuthLogoutBody {
|
|
23
|
+
keep_client?: boolean;
|
|
24
|
+
delete_secret?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare class AuthHttpError extends Error {
|
|
27
|
+
status: number;
|
|
28
|
+
body: string;
|
|
29
|
+
constructor(status: number, body: string);
|
|
30
|
+
}
|
|
31
|
+
export declare class McpAuthClient {
|
|
32
|
+
private baseUrl;
|
|
33
|
+
constructor(baseUrl: string);
|
|
34
|
+
start(name: string, namespace: string, body: AuthStartBody): Promise<AuthStartResponse>;
|
|
35
|
+
status(name: string, namespace: string, authId: string): Promise<AuthStatusResponse>;
|
|
36
|
+
logout(name: string, namespace: string, body: AuthLogoutBody): Promise<AuthLogoutResponse>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export class AuthHttpError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
body;
|
|
4
|
+
constructor(status, body) {
|
|
5
|
+
super(`HTTP ${status}: ${body}`);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.body = body;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class McpAuthClient {
|
|
11
|
+
baseUrl;
|
|
12
|
+
constructor(baseUrl) {
|
|
13
|
+
this.baseUrl = baseUrl;
|
|
14
|
+
}
|
|
15
|
+
async start(name, namespace, body) {
|
|
16
|
+
const url = `${this.baseUrl}/v1/mcp-servers/${encodeURIComponent(name)}/auth/start?namespace=${encodeURIComponent(namespace)}`;
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify(body),
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new AuthHttpError(response.status, await response.text());
|
|
24
|
+
}
|
|
25
|
+
return (await response.json());
|
|
26
|
+
}
|
|
27
|
+
async status(name, namespace, authId) {
|
|
28
|
+
const url = `${this.baseUrl}/v1/mcp-servers/${encodeURIComponent(name)}/auth/status?namespace=${encodeURIComponent(namespace)}&auth_id=${encodeURIComponent(authId)}`;
|
|
29
|
+
const response = await fetch(url);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new AuthHttpError(response.status, await response.text());
|
|
32
|
+
}
|
|
33
|
+
return (await response.json());
|
|
34
|
+
}
|
|
35
|
+
async logout(name, namespace, body) {
|
|
36
|
+
const url = `${this.baseUrl}/v1/mcp-servers/${encodeURIComponent(name)}/auth/logout?namespace=${encodeURIComponent(namespace)}`;
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new AuthHttpError(response.status, await response.text());
|
|
44
|
+
}
|
|
45
|
+
return (await response.json());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { runLogin } from './login.js';
|
|
3
|
+
import { runLogout } from './logout.js';
|
|
4
|
+
export function createMcpCommand(_config) {
|
|
5
|
+
const mcp = new Command('mcp');
|
|
6
|
+
mcp.description('manage MCP servers');
|
|
7
|
+
const auth = new Command('auth');
|
|
8
|
+
auth.description('manage MCP server authorization');
|
|
9
|
+
auth
|
|
10
|
+
.command('login <server-name>')
|
|
11
|
+
.description('start an OAuth flow for an MCPServer via ark-api')
|
|
12
|
+
.option('-n, --namespace <namespace>', 'namespace of the MCPServer')
|
|
13
|
+
.option('--force', 'bypass the Authorized preflight and force fresh client registration')
|
|
14
|
+
.option('--no-open', 'do not open a browser; print the URL only')
|
|
15
|
+
.option('--timeout <duration>', 'how long to wait for authorization to complete (e.g. 60s, 5m, 1h)')
|
|
16
|
+
.option('--scope <scope>', 'space- or comma-separated list of OAuth scopes to request')
|
|
17
|
+
.action(async (serverName, options) => {
|
|
18
|
+
const exitCode = await runLogin(serverName, {
|
|
19
|
+
namespace: options.namespace,
|
|
20
|
+
force: options.force,
|
|
21
|
+
open: options.open,
|
|
22
|
+
timeout: options.timeout,
|
|
23
|
+
scope: options.scope,
|
|
24
|
+
});
|
|
25
|
+
process.exit(exitCode);
|
|
26
|
+
});
|
|
27
|
+
auth
|
|
28
|
+
.command('logout <server-name>')
|
|
29
|
+
.description('clear OAuth state for an MCPServer via ark-api')
|
|
30
|
+
.option('-n, --namespace <namespace>', 'namespace of the MCPServer')
|
|
31
|
+
.option('--keep-client', 'preserve cached client_id/client_secret in the Secret')
|
|
32
|
+
.option('--delete-secret', 'delete the entire token Secret')
|
|
33
|
+
.action(async (serverName, options) => {
|
|
34
|
+
const exitCode = await runLogout(serverName, {
|
|
35
|
+
namespace: options.namespace,
|
|
36
|
+
keepClient: options.keepClient,
|
|
37
|
+
deleteSecret: options.deleteSecret,
|
|
38
|
+
});
|
|
39
|
+
process.exit(exitCode);
|
|
40
|
+
});
|
|
41
|
+
mcp.addCommand(auth);
|
|
42
|
+
return mcp;
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { McpAuthClient } from './authClient.js';
|
|
2
|
+
export interface LoginOptions {
|
|
3
|
+
namespace?: string;
|
|
4
|
+
force?: boolean;
|
|
5
|
+
open?: boolean;
|
|
6
|
+
timeout?: string;
|
|
7
|
+
scope?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LoginDeps {
|
|
10
|
+
buildClient: (baseUrl: string) => McpAuthClient;
|
|
11
|
+
openBrowser: (url: string) => Promise<unknown>;
|
|
12
|
+
resolveNs: (explicit?: string) => string;
|
|
13
|
+
startProxy: () => Promise<{
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
stop: () => void;
|
|
16
|
+
}>;
|
|
17
|
+
sleep: (ms: number) => Promise<void>;
|
|
18
|
+
now: () => number;
|
|
19
|
+
}
|
|
20
|
+
export declare const defaultDeps: LoginDeps;
|
|
21
|
+
export declare function runLogin(serverName: string, options: LoginOptions, deps?: LoginDeps): Promise<number>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
import output from '../../lib/output.js';
|
|
3
|
+
import { ArkApiProxy } from '../../lib/arkApiProxy.js';
|
|
4
|
+
import { loadConfig } from '../../lib/config.js';
|
|
5
|
+
import { resolveNamespace } from './namespace.js';
|
|
6
|
+
import { parseTimeoutDuration } from './timeout.js';
|
|
7
|
+
import { AuthHttpError, McpAuthClient } from './authClient.js';
|
|
8
|
+
export const defaultDeps = {
|
|
9
|
+
buildClient: (baseUrl) => new McpAuthClient(baseUrl),
|
|
10
|
+
openBrowser: (url) => open(url),
|
|
11
|
+
resolveNs: (explicit) => resolveNamespace(explicit),
|
|
12
|
+
startProxy: async () => {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const proxy = new ArkApiProxy(undefined, config.services?.reusePortForwards ?? false);
|
|
15
|
+
const client = await proxy.start();
|
|
16
|
+
return {
|
|
17
|
+
baseUrl: client.getBaseUrl(),
|
|
18
|
+
stop: () => proxy.stop(),
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
22
|
+
now: () => Date.now(),
|
|
23
|
+
};
|
|
24
|
+
const POLL_INTERVAL_MS = 2000;
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
26
|
+
const LOOPBACK_LITERALS = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
27
|
+
function warnIfCallbackUnreachable(authorizationUrl, proxyBaseUrl) {
|
|
28
|
+
let redirect;
|
|
29
|
+
try {
|
|
30
|
+
const raw = new URL(authorizationUrl).searchParams.get('redirect_uri');
|
|
31
|
+
if (!raw)
|
|
32
|
+
return;
|
|
33
|
+
redirect = new URL(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const host = redirect.hostname.replace(/^\[|\]$/g, '');
|
|
39
|
+
if (!LOOPBACK_LITERALS.has(host))
|
|
40
|
+
return;
|
|
41
|
+
const proxyPort = new URL(proxyBaseUrl).port;
|
|
42
|
+
if (redirect.port && proxyPort && redirect.port !== proxyPort) {
|
|
43
|
+
output.warning(`callback is configured for ${redirect.host} but the CLI port-forward is on ` +
|
|
44
|
+
`port ${proxyPort}; the browser redirect will not reach ark-api. Set ` +
|
|
45
|
+
`ARK_API_PUBLIC_CALLBACK_URL to use port ${proxyPort}, or port-forward ` +
|
|
46
|
+
`ark-api on port ${redirect.port}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function runLogin(serverName, options, deps = defaultDeps) {
|
|
50
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
51
|
+
if (options.timeout !== undefined) {
|
|
52
|
+
try {
|
|
53
|
+
timeoutMs = parseTimeoutDuration(options.timeout);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
output.error('mcp auth failed:', err.message);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const namespace = deps.resolveNs(options.namespace);
|
|
61
|
+
const body = {};
|
|
62
|
+
if (options.force)
|
|
63
|
+
body.force = true;
|
|
64
|
+
if (options.scope) {
|
|
65
|
+
body.scope = options.scope.split(/[\s,]+/).filter((s) => s.length > 0);
|
|
66
|
+
}
|
|
67
|
+
const proxy = await deps.startProxy();
|
|
68
|
+
try {
|
|
69
|
+
const client = deps.buildClient(proxy.baseUrl);
|
|
70
|
+
let startResponse;
|
|
71
|
+
try {
|
|
72
|
+
startResponse = await client.start(serverName, namespace, body);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof AuthHttpError) {
|
|
76
|
+
output.error('mcp auth failed:', err.body || `HTTP ${err.status}`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
output.error('mcp auth failed:', err.message);
|
|
80
|
+
}
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
warnIfCallbackUnreachable(startResponse.authorization_url, proxy.baseUrl);
|
|
84
|
+
console.log(`Authorization URL: ${startResponse.authorization_url}`);
|
|
85
|
+
if (options.open !== false) {
|
|
86
|
+
try {
|
|
87
|
+
await deps.openBrowser(startResponse.authorization_url);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// ignore — URL is already printed
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const deadline = deps.now() + timeoutMs;
|
|
94
|
+
while (deps.now() < deadline) {
|
|
95
|
+
let status;
|
|
96
|
+
try {
|
|
97
|
+
status = await client.status(serverName, namespace, startResponse.auth_id);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (err instanceof AuthHttpError) {
|
|
101
|
+
output.error('mcp auth failed:', err.body || `HTTP ${err.status}`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
output.error('mcp auth failed:', err.message);
|
|
105
|
+
}
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
if (status.state === 'authorized') {
|
|
109
|
+
if (status.expires_at) {
|
|
110
|
+
output.success(`authorized (token expires at ${status.expires_at})`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
output.success('authorized');
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
if (status.state === 'failed' || status.state === 'expired') {
|
|
118
|
+
const reason = status.message || status.state;
|
|
119
|
+
output.error('mcp auth failed:', reason);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
await deps.sleep(POLL_INTERVAL_MS);
|
|
123
|
+
}
|
|
124
|
+
output.error('mcp auth failed:', 'timed out waiting for authorization');
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
proxy.stop();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { McpAuthClient } from './authClient.js';
|
|
2
|
+
export interface LogoutOptions {
|
|
3
|
+
namespace?: string;
|
|
4
|
+
keepClient?: boolean;
|
|
5
|
+
deleteSecret?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface LogoutDeps {
|
|
8
|
+
buildClient: (baseUrl: string) => McpAuthClient;
|
|
9
|
+
resolveNs: (explicit?: string) => string;
|
|
10
|
+
startProxy: () => Promise<{
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
stop: () => void;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
export declare const defaultLogoutDeps: LogoutDeps;
|
|
16
|
+
export declare function runLogout(serverName: string, options: LogoutOptions, deps?: LogoutDeps): Promise<number>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import output from '../../lib/output.js';
|
|
2
|
+
import { ArkApiProxy } from '../../lib/arkApiProxy.js';
|
|
3
|
+
import { loadConfig } from '../../lib/config.js';
|
|
4
|
+
import { resolveNamespace } from './namespace.js';
|
|
5
|
+
import { AuthHttpError, McpAuthClient } from './authClient.js';
|
|
6
|
+
export const defaultLogoutDeps = {
|
|
7
|
+
buildClient: (baseUrl) => new McpAuthClient(baseUrl),
|
|
8
|
+
resolveNs: (explicit) => resolveNamespace(explicit),
|
|
9
|
+
startProxy: async () => {
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
const proxy = new ArkApiProxy(undefined, config.services?.reusePortForwards ?? false);
|
|
12
|
+
const client = await proxy.start();
|
|
13
|
+
return {
|
|
14
|
+
baseUrl: client.getBaseUrl(),
|
|
15
|
+
stop: () => proxy.stop(),
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
export async function runLogout(serverName, options, deps = defaultLogoutDeps) {
|
|
20
|
+
if (options.keepClient && options.deleteSecret) {
|
|
21
|
+
output.error('mcp auth failed:', '--keep-client and --delete-secret are mutually exclusive');
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
const namespace = deps.resolveNs(options.namespace);
|
|
25
|
+
const body = {};
|
|
26
|
+
if (options.keepClient)
|
|
27
|
+
body.keep_client = true;
|
|
28
|
+
if (options.deleteSecret)
|
|
29
|
+
body.delete_secret = true;
|
|
30
|
+
const proxy = await deps.startProxy();
|
|
31
|
+
try {
|
|
32
|
+
const client = deps.buildClient(proxy.baseUrl);
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
response = await client.logout(serverName, namespace, body);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err instanceof AuthHttpError) {
|
|
39
|
+
if (err.status === 404) {
|
|
40
|
+
output.error('mcp auth failed:', 'MCPServer not found');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
output.error('mcp auth failed:', err.body || `HTTP ${err.status}`);
|
|
44
|
+
}
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
output.error('mcp auth failed:', err.message);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
if (response.noop) {
|
|
51
|
+
output.success('logout: nothing to clear');
|
|
52
|
+
}
|
|
53
|
+
else if (response.deleted) {
|
|
54
|
+
output.success('logout: secret deleted');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const keys = response.cleared_keys ?? [];
|
|
58
|
+
output.success(`logout: cleared ${keys.length} key(s)`);
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
proxy.stop();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveNamespace(explicit?: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { KubeConfig } from '@kubernetes/client-node';
|
|
2
|
+
export function resolveNamespace(explicit) {
|
|
3
|
+
if (explicit && explicit.length > 0) {
|
|
4
|
+
return explicit;
|
|
5
|
+
}
|
|
6
|
+
const kc = new KubeConfig();
|
|
7
|
+
kc.loadFromDefault();
|
|
8
|
+
const contextName = kc.getCurrentContext();
|
|
9
|
+
if (!contextName) {
|
|
10
|
+
return 'default';
|
|
11
|
+
}
|
|
12
|
+
const ctx = kc.getContextObject(contextName);
|
|
13
|
+
if (ctx && ctx.namespace && ctx.namespace.length > 0) {
|
|
14
|
+
return ctx.namespace;
|
|
15
|
+
}
|
|
16
|
+
return 'default';
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseTimeoutDuration(input: string): number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function parseTimeoutDuration(input) {
|
|
2
|
+
const match = input.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/);
|
|
3
|
+
if (!match) {
|
|
4
|
+
throw new Error(`invalid --timeout value: ${input} (expected Go duration such as 60s, 5m, 1h)`);
|
|
5
|
+
}
|
|
6
|
+
const value = parseFloat(match[1]);
|
|
7
|
+
if (!isFinite(value) || value <= 0) {
|
|
8
|
+
throw new Error(`invalid --timeout value: ${input} (must be a positive duration)`);
|
|
9
|
+
}
|
|
10
|
+
const unit = match[2];
|
|
11
|
+
switch (unit) {
|
|
12
|
+
case 'ms':
|
|
13
|
+
return value;
|
|
14
|
+
case 's':
|
|
15
|
+
return value * 1000;
|
|
16
|
+
case 'm':
|
|
17
|
+
return value * 60 * 1000;
|
|
18
|
+
case 'h':
|
|
19
|
+
return value * 60 * 60 * 1000;
|
|
20
|
+
default:
|
|
21
|
+
throw new Error(`invalid --timeout unit: ${unit}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -109,34 +109,46 @@ export class KubernetesModelManifestBuilder {
|
|
|
109
109
|
region: {
|
|
110
110
|
value: config.region,
|
|
111
111
|
},
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const bedrock = bedrockConfig.bedrock;
|
|
115
|
+
if (config.authMethod === 'api-key') {
|
|
116
|
+
bedrock.apiKey = {
|
|
117
|
+
valueFrom: {
|
|
118
|
+
secretKeyRef: {
|
|
119
|
+
name: config.secretName,
|
|
120
|
+
key: 'bedrock-api-key',
|
|
118
121
|
},
|
|
119
122
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
bedrock.accessKeyId = {
|
|
127
|
+
valueFrom: {
|
|
128
|
+
secretKeyRef: {
|
|
129
|
+
name: config.secretName,
|
|
130
|
+
key: 'access-key-id',
|
|
126
131
|
},
|
|
127
132
|
},
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const bedrock = bedrockConfig.bedrock;
|
|
131
|
-
if (config.sessionToken) {
|
|
132
|
-
bedrock.sessionToken = {
|
|
133
|
+
};
|
|
134
|
+
bedrock.secretAccessKey = {
|
|
133
135
|
valueFrom: {
|
|
134
136
|
secretKeyRef: {
|
|
135
137
|
name: config.secretName,
|
|
136
|
-
key: '
|
|
138
|
+
key: 'secret-access-key',
|
|
137
139
|
},
|
|
138
140
|
},
|
|
139
141
|
};
|
|
142
|
+
if (config.sessionToken) {
|
|
143
|
+
bedrock.sessionToken = {
|
|
144
|
+
valueFrom: {
|
|
145
|
+
secretKeyRef: {
|
|
146
|
+
name: config.secretName,
|
|
147
|
+
key: 'session-token',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
140
152
|
}
|
|
141
153
|
if (config.modelArn) {
|
|
142
154
|
bedrock.modelArn = {
|