aws-runtime-bridge 1.2.1 → 1.3.0
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/README.md +77 -77
- package/dist/adapter/ClaudeSdkAdapter.d.ts +1 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +7 -3
- package/dist/adapter/ClaudeSdkAdapter.test.js +2 -2
- package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/CodexSdkAdapter.js +7 -4
- package/dist/adapter/CodexSdkAdapter.test.js +4 -2
- package/dist/adapter/OpencodeSdkAdapter.d.ts +2 -0
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +15 -1
- package/dist/adapter/OpencodeSdkAdapter.test.js +5 -0
- package/dist/index.js +0 -0
- package/dist/routes/properties.test.js +4 -4
- package/dist/routes/runtime-binding.d.ts.map +1 -1
- package/dist/routes/runtime-binding.js +8 -13
- package/dist/routes/runtime-mcp-proxy.d.ts +3 -0
- package/dist/routes/runtime-mcp-proxy.d.ts.map +1 -0
- package/dist/routes/runtime-mcp-proxy.js +102 -0
- package/dist/routes/runtime-mcp-proxy.test.d.ts +2 -0
- package/dist/routes/runtime-mcp-proxy.test.d.ts.map +1 -0
- package/dist/routes/runtime-mcp-proxy.test.js +111 -0
- package/dist/routes/terminal.js +2 -5
- package/dist/routes/terminal.test.js +3 -4
- package/dist/services/auto-register.d.ts +6 -0
- package/dist/services/auto-register.d.ts.map +1 -1
- package/dist/services/auto-register.js +63 -1
- package/dist/services/aws-client-agent-mcp.d.ts.map +1 -1
- package/dist/services/aws-client-agent-mcp.js +4 -4
- package/dist/services/aws-client-agent-mcp.test.js +14 -0
- package/dist/services/mcp-launch-binding-queue.d.ts +0 -2
- package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
- package/dist/services/mcp-launch-binding-queue.js +44 -16
- package/dist/services/mcp-launch-binding-queue.test.js +42 -37
- package/dist/services/runtime-binding.d.ts +1 -0
- package/dist/services/runtime-binding.d.ts.map +1 -1
- package/dist/services/runtime-binding.js +39 -5
- package/dist/services/runtime-binding.test.d.ts +2 -0
- package/dist/services/runtime-binding.test.d.ts.map +1 -0
- package/dist/services/runtime-binding.test.js +11 -0
- package/dist/utils/yaml-utils.test.js +129 -129
- package/node_modules/@cc-switch/sdk/README.md +540 -540
- package/node_modules/@cc-switch/sdk/dist/sdk-import.test.d.ts +2 -0
- package/node_modules/@cc-switch/sdk/dist/sdk-import.test.d.ts.map +1 -0
- package/node_modules/@cc-switch/sdk/dist/sdk-import.test.js +119 -0
- package/node_modules/@cc-switch/sdk/package.json +31 -31
- package/package/aws-client-agent-mcp/README.md +288 -288
- package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.js +96 -13
- package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.test.js +26 -8
- package/package/aws-client-agent-mcp/dist/config.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/constants.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/constants.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/constants.js +0 -1
- package/package/aws-client-agent-mcp/dist/constants.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +49 -13
- package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.test.js +40 -13
- package/package/aws-client-agent-mcp/dist/http-client.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/index.js +11 -6
- package/package/aws-client-agent-mcp/dist/index.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/logger.d.ts +11 -1
- package/package/aws-client-agent-mcp/dist/logger.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/logger.js +91 -6
- package/package/aws-client-agent-mcp/dist/logger.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/logger.test.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/logger.test.d.ts.map +1 -0
- package/package/aws-client-agent-mcp/dist/logger.test.js +27 -0
- package/package/aws-client-agent-mcp/dist/logger.test.js.map +1 -0
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +18 -14
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js +51 -21
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +3 -2
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +1 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +18 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js +53 -2
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js.map +1 -1
- package/package/aws-client-agent-mcp/package.json +52 -52
- package/package/cc-switch-sdk/README.md +540 -540
- package/package/cc-switch-sdk/dist/sdk-import.test.d.ts +2 -0
- package/package/cc-switch-sdk/dist/sdk-import.test.d.ts.map +1 -0
- package/package/cc-switch-sdk/dist/sdk-import.test.js +119 -0
- package/package/cc-switch-sdk/package.json +31 -31
- package/package.json +78 -78
package/dist/routes/terminal.js
CHANGED
|
@@ -80,7 +80,7 @@ terminalRouter.post('/start', validateToken, async (req, res) => {
|
|
|
80
80
|
});
|
|
81
81
|
// SDK 模式
|
|
82
82
|
if (mode === 'sdk') {
|
|
83
|
-
await startSdkSession(req, res
|
|
83
|
+
await startSdkSession(req, res);
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
// PTY 模式(原有逻辑)
|
|
@@ -110,7 +110,6 @@ terminalRouter.post('/start', validateToken, async (req, res) => {
|
|
|
110
110
|
env: {
|
|
111
111
|
...process.env,
|
|
112
112
|
AWS_AGENT_ID: String(agentId),
|
|
113
|
-
AWS_MCP_LAUNCH_BINDING_ID: queuedBinding?.id || '',
|
|
114
113
|
AWS_MCP_CLAIM_LAUNCH_BINDING: 'true',
|
|
115
114
|
},
|
|
116
115
|
});
|
|
@@ -176,14 +175,13 @@ terminalRouter.post('/start', validateToken, async (req, res) => {
|
|
|
176
175
|
workspacePath,
|
|
177
176
|
command: actualCommand,
|
|
178
177
|
mode: 'pty',
|
|
179
|
-
mcpLaunchBindingId: queuedBinding?.id,
|
|
180
178
|
});
|
|
181
179
|
});
|
|
182
180
|
/**
|
|
183
181
|
* 启动 SDK 会话
|
|
184
182
|
* 支持 MCP 配置和空闲命令
|
|
185
183
|
*/
|
|
186
|
-
async function startSdkSession(req, res
|
|
184
|
+
async function startSdkSession(req, res) {
|
|
187
185
|
const { agentId, workspacePath, command, autoAccept = true, initialPrompt,
|
|
188
186
|
// MCP 配置 - 加载 aws-client-agent-mcp
|
|
189
187
|
mcpConfigPath, extraMcpServers,
|
|
@@ -213,7 +211,6 @@ async function startSdkSession(req, res, mcpLaunchBindingId) {
|
|
|
213
211
|
envOverrides: {
|
|
214
212
|
AWS_AGENT_ID: String(agentId),
|
|
215
213
|
AWS_WORKSPACE_PATH: String(workspacePath),
|
|
216
|
-
AWS_MCP_LAUNCH_BINDING_ID: mcpLaunchBindingId || '',
|
|
217
214
|
AWS_MCP_CLAIM_LAUNCH_BINDING: 'true',
|
|
218
215
|
},
|
|
219
216
|
};
|
|
@@ -56,15 +56,14 @@ describe('terminal configuration', () => {
|
|
|
56
56
|
expect(getShellConfig('linux').shell).toBe('bash');
|
|
57
57
|
});
|
|
58
58
|
it('builds correct terminal environment', () => {
|
|
59
|
-
const buildTerminalEnv = (agentId,
|
|
59
|
+
const buildTerminalEnv = (agentId, baseEnv) => ({
|
|
60
60
|
...baseEnv,
|
|
61
61
|
AWS_AGENT_ID: agentId,
|
|
62
|
-
AWS_MCP_LAUNCH_BINDING_ID: bindingId,
|
|
63
62
|
AWS_MCP_CLAIM_LAUNCH_BINDING: 'true',
|
|
64
63
|
});
|
|
65
|
-
const env = buildTerminalEnv('agent-123',
|
|
64
|
+
const env = buildTerminalEnv('agent-123', { PATH: '/usr/bin' });
|
|
66
65
|
expect(env.AWS_AGENT_ID).toBe('agent-123');
|
|
67
|
-
expect(env.AWS_MCP_LAUNCH_BINDING_ID).
|
|
66
|
+
expect(env.AWS_MCP_LAUNCH_BINDING_ID).toBeUndefined();
|
|
68
67
|
expect(env.AWS_MCP_CLAIM_LAUNCH_BINDING).toBe('true');
|
|
69
68
|
});
|
|
70
69
|
});
|
|
@@ -75,6 +75,12 @@ export declare function requestRuntimeAccessTokenRefresh(): Promise<{
|
|
|
75
75
|
updated?: boolean;
|
|
76
76
|
error?: string;
|
|
77
77
|
}>;
|
|
78
|
+
export declare function requestRuntimeAccessTokenRefreshForServer(serverBaseUrl: string): Promise<{
|
|
79
|
+
success: boolean;
|
|
80
|
+
runtimeAccessToken?: string;
|
|
81
|
+
updated?: boolean;
|
|
82
|
+
error?: string;
|
|
83
|
+
}>;
|
|
78
84
|
export declare function unregister(): Promise<boolean>;
|
|
79
85
|
/**
|
|
80
86
|
* 获取注册状态
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-register.d.ts","sourceRoot":"","sources":["../../src/services/auto-register.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"auto-register.d.ts","sourceRoot":"","sources":["../../src/services/auto-register.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiDH;;GAEG;AACH,UAAU,kBAAkB;IAC1B,eAAe;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,oBAAoB;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAwBD,wBAAgB,2BAA2B,IAAI,MAAM,EAAE,CAEtD;AAiED;;GAEG;AACH,wBAAgB,6BAA6B,IAAI,MAAM,CAGtD;AAyJD;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,kBAAkB,CA2C/C;AAED,wBAAgB,WAAW,IAAI,kBAAkB,EAAE,CA0ClD;AA4OD;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,YAAY,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GACzC,OAAO,CAAC,OAAO,CAAC,CAalB;AAyHD;;GAEG;AACH,wBAAsB,gCAAgC,IAAI,OAAO,CAAC;IAChE,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAqED;AAED,wBAAsB,yCAAyC,CAC7D,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CA2ED;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAuBnD;AAED;;GAEG;AACH,wBAAgB,oBAAoB;gBA14BtB,OAAO;iBACN,MAAM;mBACJ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;kBACvB,IAAI;YACV,MAAM;EAw4Bf;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAElD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC;IACpD,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CA2ED"}
|
|
@@ -13,7 +13,7 @@ import path from "node:path";
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import { logger } from "../utils/logger.js";
|
|
15
15
|
import { schedulerBaseUrl, runtimeToken } from "../config.js";
|
|
16
|
-
import { getRuntimeAccessToken, getRuntimeBindingPublicState, loadRuntimeBinding, saveRuntimeBinding, } from "./runtime-binding.js";
|
|
16
|
+
import { getRuntimeAccessToken, getRuntimeBindingPublicState, loadRuntimeBinding, normalizeSchedulerBaseUrl, saveRuntimeBinding, saveScopedRuntimeAccessToken, } from "./runtime-binding.js";
|
|
17
17
|
// 默认配置
|
|
18
18
|
const DEFAULT_CONFIG = {
|
|
19
19
|
enabled: false,
|
|
@@ -37,6 +37,9 @@ function normalizeOptionalString(value) {
|
|
|
37
37
|
const normalized = String(value).trim();
|
|
38
38
|
return normalized || undefined;
|
|
39
39
|
}
|
|
40
|
+
function normalizeSchedulerHttpBaseUrl(value) {
|
|
41
|
+
return normalizeSchedulerBaseUrl(value);
|
|
42
|
+
}
|
|
40
43
|
function normalizeOptionalBoolean(value) {
|
|
41
44
|
if (typeof value === "boolean") {
|
|
42
45
|
return value;
|
|
@@ -649,6 +652,65 @@ export async function requestRuntimeAccessTokenRefresh() {
|
|
|
649
652
|
return { success: false, error: message };
|
|
650
653
|
}
|
|
651
654
|
}
|
|
655
|
+
export async function requestRuntimeAccessTokenRefreshForServer(serverBaseUrl) {
|
|
656
|
+
const targetServerUrl = normalizeSchedulerHttpBaseUrl(serverBaseUrl);
|
|
657
|
+
if (!targetServerUrl) {
|
|
658
|
+
return { success: false, error: "serverUrl is required for runtime token refresh" };
|
|
659
|
+
}
|
|
660
|
+
const config = loadConfigs().find((item) => normalizeSchedulerHttpBaseUrl(item.serverUrl) === targetServerUrl) || loadPrimaryLifecycleConfig();
|
|
661
|
+
if (!config.userKey) {
|
|
662
|
+
return {
|
|
663
|
+
success: false,
|
|
664
|
+
error: "userKey is required for runtime token refresh",
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const state = getRuntimeBindingPublicState();
|
|
668
|
+
const localIp = getLocalIpAddress(targetServerUrl);
|
|
669
|
+
const runtimeBridgePort = process.env.AWS_RUNTIME_BRIDGE_PORT || 18081;
|
|
670
|
+
const runtimeBridgeBaseUrl = `http://${config.registerIp || config.virtualIp || localIp}:${runtimeBridgePort}`;
|
|
671
|
+
try {
|
|
672
|
+
const response = await axios.post(`${targetServerUrl}/api/instances/runtime-tokens/refresh`, {
|
|
673
|
+
tenantId: config.tenantId,
|
|
674
|
+
userKey: config.userKey,
|
|
675
|
+
instanceId: state.instanceId,
|
|
676
|
+
instanceName: config.instanceName,
|
|
677
|
+
runtimeBridgeBaseUrl,
|
|
678
|
+
}, {
|
|
679
|
+
headers: {
|
|
680
|
+
"Content-Type": "application/json",
|
|
681
|
+
"X-Runtime-Token": runtimeToken,
|
|
682
|
+
},
|
|
683
|
+
timeout: 10000,
|
|
684
|
+
});
|
|
685
|
+
if (!response.data.success || !response.data.runtimeAccessToken) {
|
|
686
|
+
return {
|
|
687
|
+
success: false,
|
|
688
|
+
error: response.data.message ||
|
|
689
|
+
"scheduler did not return runtimeAccessToken",
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const userId = response.data.userId || config.userKey;
|
|
693
|
+
const previousToken = getRuntimeAccessToken(userId, targetServerUrl);
|
|
694
|
+
const updated = previousToken !== response.data.runtimeAccessToken;
|
|
695
|
+
saveScopedRuntimeAccessToken({
|
|
696
|
+
userId,
|
|
697
|
+
serverBaseUrl: targetServerUrl,
|
|
698
|
+
accessToken: response.data.runtimeAccessToken,
|
|
699
|
+
});
|
|
700
|
+
return {
|
|
701
|
+
success: true,
|
|
702
|
+
runtimeAccessToken: response.data.runtimeAccessToken,
|
|
703
|
+
updated,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
const err = error;
|
|
708
|
+
const message = err.response?.data
|
|
709
|
+
? JSON.stringify(err.response.data)
|
|
710
|
+
: err.message;
|
|
711
|
+
return { success: false, error: message };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
652
714
|
export async function unregister() {
|
|
653
715
|
if (!registrationState.registered || !registrationState.instanceId) {
|
|
654
716
|
logger.info("[AutoRegister] 实例未注册,无需注销");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aws-client-agent-mcp.d.ts","sourceRoot":"","sources":["../../src/services/aws-client-agent-mcp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"aws-client-agent-mcp.d.ts","sourceRoot":"","sources":["../../src/services/aws-client-agent-mcp.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAsB7C,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,sCAAsC;IACrD,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,sCAAsC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAaD,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;CAC3C;AAuBD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAgLD,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,MAAM,GAAG,IAAI,CAoDf;AA2ED,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CAEpB;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CAsBpB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,uBAAuB,GAC7B,kBAAkB,CA0BpB;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAgBtD"}
|
|
@@ -4,10 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { getRuntimeHomeDir, port, schedulerBaseUrl } from "../config.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
|
-
import { getRuntimeAccessToken, loadRuntimeBinding, } from "./runtime-binding.js";
|
|
7
|
+
import { getRuntimeAccessToken, loadRuntimeBinding, normalizeSchedulerBaseUrl, } from "./runtime-binding.js";
|
|
8
8
|
export const AWS_MCP_SERVER_NAME = "aws-mcp";
|
|
9
9
|
const AWS_MCP_ALLOWED_ENV_KEYS = [
|
|
10
|
-
"AWS_INTERNAL_API_KEY",
|
|
11
10
|
"AWS_PROJECT_NAME",
|
|
12
11
|
"AWS_PROMPT",
|
|
13
12
|
"AWS_ROLE_NAME",
|
|
@@ -236,7 +235,7 @@ function parseAwsClientAgentMcpArgs(raw) {
|
|
|
236
235
|
return [];
|
|
237
236
|
}
|
|
238
237
|
function toWebSocketUrl(baseUrl) {
|
|
239
|
-
const url = new URL("/ws/agent", baseUrl);
|
|
238
|
+
const url = new URL("/ws/agent", normalizeSchedulerBaseUrl(baseUrl) || baseUrl);
|
|
240
239
|
if (url.protocol === "https:") {
|
|
241
240
|
url.protocol = "wss:";
|
|
242
241
|
}
|
|
@@ -246,7 +245,7 @@ function toWebSocketUrl(baseUrl) {
|
|
|
246
245
|
return url.toString();
|
|
247
246
|
}
|
|
248
247
|
function toMcpHttpUrl(baseUrl) {
|
|
249
|
-
return new URL("/mcp/call", baseUrl).toString();
|
|
248
|
+
return new URL("/mcp/call", normalizeSchedulerBaseUrl(baseUrl) || baseUrl).toString();
|
|
250
249
|
}
|
|
251
250
|
function resolveSchedulerBaseUrlForMcp() {
|
|
252
251
|
const envSchedulerBaseUrl = String(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL || "").trim();
|
|
@@ -297,6 +296,7 @@ export function buildAwsMcpServerConfig(input) {
|
|
|
297
296
|
AWS_SERVER_URL: env.AWS_SERVER_URL || toWebSocketUrl(effectiveSchedulerBaseUrl),
|
|
298
297
|
AWS_MCP_HTTP_URL: env.AWS_MCP_HTTP_URL || toMcpHttpUrl(effectiveSchedulerBaseUrl),
|
|
299
298
|
AWS_RUNTIME_BRIDGE_BASE_URL: env.AWS_RUNTIME_BRIDGE_BASE_URL || `http://127.0.0.1:${port}`,
|
|
299
|
+
AWS_MCP_CLAIM_LAUNCH_BINDING: env.AWS_MCP_CLAIM_LAUNCH_BINDING || "true",
|
|
300
300
|
...(issuedRuntimeAccessToken
|
|
301
301
|
? { AWS_RUNTIME_ACCESS_TOKEN: issuedRuntimeAccessToken }
|
|
302
302
|
: {}),
|
|
@@ -216,6 +216,20 @@ describe('aws-client-agent-mcp service', () => {
|
|
|
216
216
|
expect(config.env.AWS_RUNTIME_CALLBACK_TOKEN).toBeUndefined();
|
|
217
217
|
expect(config.env.CUSTOM_SECRET).toBeUndefined();
|
|
218
218
|
});
|
|
219
|
+
it('repairs duplicated scheduler ports when building MCP URLs', async () => {
|
|
220
|
+
process.env.AWS_CLIENT_AGENT_MCP_COMMAND = 'aws-client-agent-mcp';
|
|
221
|
+
process.env.AWS_RUNTIME_HOME_DIR = mkdtempSync(path.join(os.tmpdir(), 'aws-mcp-config-'));
|
|
222
|
+
process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = 'http://127.0.0.1:8080:8080';
|
|
223
|
+
process.env.AWS_SERVER_URL = '';
|
|
224
|
+
process.env.AWS_MCP_HTTP_URL = '';
|
|
225
|
+
const mod = await import('./aws-client-agent-mcp.js');
|
|
226
|
+
const config = mod.buildAwsMcpServerConfig({
|
|
227
|
+
agentId: 'agent-1',
|
|
228
|
+
workspacePath: '/workspace/demo',
|
|
229
|
+
});
|
|
230
|
+
expect(config.env.AWS_SERVER_URL).toBe('ws://127.0.0.1:8080/ws/agent');
|
|
231
|
+
expect(config.env.AWS_MCP_HTTP_URL).toBe('http://127.0.0.1:8080/mcp/call');
|
|
232
|
+
});
|
|
219
233
|
it('ignores malformed AWS_CLIENT_AGENT_MCP_ARGS JSON', async () => {
|
|
220
234
|
process.env.AWS_CLIENT_AGENT_MCP_COMMAND = 'custom-aws-client-agent-mcp';
|
|
221
235
|
process.env.AWS_CLIENT_AGENT_MCP_ARGS = '{bad json';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-launch-binding-queue.d.ts","sourceRoot":"","sources":["../../src/services/mcp-launch-binding-queue.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mcp-launch-binding-queue.d.ts","sourceRoot":"","sources":["../../src/services/mcp-launch-binding-queue.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAwB,SAAQ,gBAAgB;IAC/D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAmDD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,qBAAqB,GAAG,gBAAgB,GAAG,IAAI,CA+B7F;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,uBAAuB,GAAG,IAAI,CA0C3G;AAED,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,OAAO,GAAG,MAAM,CAOpE;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAElD"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { schedulerBaseUrl as defaultSchedulerBaseUrl } from "../config.js";
|
|
3
|
+
import { getRuntimeAccessToken, loadRuntimeBinding, normalizeSchedulerBaseUrl } from "./runtime-binding.js";
|
|
4
|
+
const LAUNCH_BINDING_TTL_MS = 5 * 60 * 1000;
|
|
3
5
|
const launchBindings = [];
|
|
4
6
|
function normalizeWorkspacePath(workspacePath) {
|
|
5
7
|
const raw = String(workspacePath || "").trim();
|
|
@@ -11,57 +13,82 @@ function normalizeWorkspacePath(workspacePath) {
|
|
|
11
13
|
function normalizeOptionalUrl(value) {
|
|
12
14
|
return String(value || "").trim();
|
|
13
15
|
}
|
|
14
|
-
function
|
|
15
|
-
const raw = String(serverUrl || "").trim();
|
|
16
|
+
function normalizeSchedulerOrigin(serverUrl) {
|
|
17
|
+
const raw = normalizeSchedulerBaseUrl(serverUrl) || String(serverUrl || "").trim();
|
|
16
18
|
if (!raw) {
|
|
17
19
|
return "";
|
|
18
20
|
}
|
|
19
21
|
try {
|
|
20
22
|
const url = new URL(raw);
|
|
21
|
-
url.protocol
|
|
23
|
+
if (url.protocol === "ws:") {
|
|
24
|
+
url.protocol = "http:";
|
|
25
|
+
}
|
|
26
|
+
else if (url.protocol === "wss:") {
|
|
27
|
+
url.protocol = "https:";
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
url.protocol = url.protocol.toLowerCase();
|
|
31
|
+
}
|
|
22
32
|
url.hostname = url.hostname.toLowerCase();
|
|
23
|
-
|
|
24
|
-
return url.toString().replace(/\/+$/, "");
|
|
33
|
+
return url.origin;
|
|
25
34
|
}
|
|
26
35
|
catch {
|
|
27
36
|
return raw.replace(/\/+$/, "").toLowerCase();
|
|
28
37
|
}
|
|
29
38
|
}
|
|
39
|
+
function pruneExpiredBindings(now = Date.now()) {
|
|
40
|
+
for (let index = launchBindings.length - 1; index >= 0; index -= 1) {
|
|
41
|
+
const enqueuedAt = Date.parse(launchBindings[index]?.enqueuedAt || "");
|
|
42
|
+
if (!Number.isFinite(enqueuedAt) || now - enqueuedAt > LAUNCH_BINDING_TTL_MS) {
|
|
43
|
+
launchBindings.splice(index, 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
30
47
|
function createBindingId(agentId) {
|
|
31
48
|
return `${agentId}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
32
49
|
}
|
|
33
50
|
export function enqueueMcpLaunchBinding(input) {
|
|
51
|
+
pruneExpiredBindings();
|
|
34
52
|
const agentId = String(input.agentId || "").trim();
|
|
35
53
|
const workspaceKey = normalizeWorkspacePath(input.workspacePath);
|
|
36
54
|
if (!agentId || !workspaceKey) {
|
|
37
55
|
return null;
|
|
38
56
|
}
|
|
57
|
+
const runtimeState = loadRuntimeBinding();
|
|
58
|
+
const schedulerUrl = normalizeSchedulerBaseUrl(input.serverUrl || input.schedulerBaseUrl || runtimeState.schedulerBaseUrl || defaultSchedulerBaseUrl) || normalizeOptionalUrl(input.serverUrl || input.schedulerBaseUrl || runtimeState.schedulerBaseUrl || defaultSchedulerBaseUrl);
|
|
59
|
+
const schedulerBindingBaseUrl = normalizeSchedulerBaseUrl(input.schedulerBaseUrl || runtimeState.schedulerBaseUrl || defaultSchedulerBaseUrl) || normalizeOptionalUrl(input.schedulerBaseUrl || runtimeState.schedulerBaseUrl || defaultSchedulerBaseUrl);
|
|
39
60
|
const binding = {
|
|
40
61
|
id: createBindingId(agentId),
|
|
41
62
|
agentId,
|
|
42
63
|
workspacePath: path.resolve(String(input.workspacePath)),
|
|
43
|
-
serverUrl:
|
|
64
|
+
serverUrl: schedulerUrl,
|
|
44
65
|
mcpHttpUrl: normalizeOptionalUrl(input.mcpHttpUrl),
|
|
45
66
|
runtimeAccessToken: normalizeOptionalUrl(input.runtimeAccessToken),
|
|
46
67
|
userId: normalizeOptionalUrl(input.userId),
|
|
47
|
-
schedulerBaseUrl:
|
|
68
|
+
schedulerBaseUrl: schedulerBindingBaseUrl,
|
|
48
69
|
enqueuedAt: new Date().toISOString(),
|
|
49
70
|
};
|
|
50
71
|
launchBindings.push(binding);
|
|
51
72
|
return binding;
|
|
52
73
|
}
|
|
53
74
|
export function claimMcpLaunchBinding(request) {
|
|
54
|
-
|
|
55
|
-
const bindingId = String(request.bindingId || "").trim();
|
|
75
|
+
pruneExpiredBindings();
|
|
56
76
|
const workspaceKey = normalizeWorkspacePath(request.workspacePath);
|
|
57
|
-
const serverKey =
|
|
58
|
-
if (!
|
|
77
|
+
const serverKey = normalizeSchedulerOrigin(request.serverUrl);
|
|
78
|
+
if (!workspaceKey) {
|
|
59
79
|
return null;
|
|
60
80
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
let bindingIndex = launchBindings.findIndex((binding) => normalizeWorkspacePath(binding.workspacePath) === workspaceKey &&
|
|
82
|
+
serverKey &&
|
|
83
|
+
normalizeSchedulerOrigin(binding.serverUrl || binding.schedulerBaseUrl) === serverKey);
|
|
84
|
+
if (bindingIndex < 0) {
|
|
85
|
+
const workspaceMatches = launchBindings
|
|
86
|
+
.map((binding, index) => ({ binding, index }))
|
|
87
|
+
.filter(({ binding }) => normalizeWorkspacePath(binding.workspacePath) === workspaceKey);
|
|
88
|
+
if (workspaceMatches.length === 1) {
|
|
89
|
+
bindingIndex = workspaceMatches[0]?.index ?? -1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
65
92
|
if (bindingIndex < 0) {
|
|
66
93
|
return null;
|
|
67
94
|
}
|
|
@@ -81,6 +108,7 @@ export function claimMcpLaunchBinding(request) {
|
|
|
81
108
|
};
|
|
82
109
|
}
|
|
83
110
|
export function getMcpLaunchQueueSize(workspacePath) {
|
|
111
|
+
pruneExpiredBindings();
|
|
84
112
|
const workspaceKey = normalizeWorkspacePath(workspacePath);
|
|
85
113
|
if (!workspaceKey) {
|
|
86
114
|
return 0;
|
|
@@ -4,7 +4,7 @@ describe('mcp launch binding queue', () => {
|
|
|
4
4
|
afterEach(() => {
|
|
5
5
|
clearMcpLaunchBindingQueues();
|
|
6
6
|
});
|
|
7
|
-
it('claims same-workspace bindings
|
|
7
|
+
it('claims same-workspace same-server bindings from the front of the queue', () => {
|
|
8
8
|
const firstBinding = enqueueMcpLaunchBinding({
|
|
9
9
|
agentId: 'agent-a',
|
|
10
10
|
workspacePath: 'C:/repo/demo',
|
|
@@ -22,15 +22,11 @@ describe('mcp launch binding queue', () => {
|
|
|
22
22
|
schedulerBaseUrl: 'http://server-a:7380',
|
|
23
23
|
});
|
|
24
24
|
expect(getMcpLaunchQueueSize('C:/repo/demo')).toBe(2);
|
|
25
|
-
const
|
|
26
|
-
agentId: 'agent-b',
|
|
27
|
-
bindingId: secondBinding?.id,
|
|
25
|
+
const first = claimMcpLaunchBinding({
|
|
28
26
|
workspacePath: 'C:/repo/demo',
|
|
29
27
|
serverUrl: 'ws://server-a:7380/ws/agent',
|
|
30
28
|
});
|
|
31
|
-
const
|
|
32
|
-
agentId: 'agent-a',
|
|
33
|
-
bindingId: firstBinding?.id,
|
|
29
|
+
const second = claimMcpLaunchBinding({
|
|
34
30
|
workspacePath: 'C:/repo/demo',
|
|
35
31
|
serverUrl: 'ws://server-a:7380/ws/agent',
|
|
36
32
|
});
|
|
@@ -40,68 +36,77 @@ describe('mcp launch binding queue', () => {
|
|
|
40
36
|
expect(second?.runtimeAccessToken).toBe('token-b');
|
|
41
37
|
expect(getMcpLaunchQueueSize('C:/repo/demo')).toBe(0);
|
|
42
38
|
});
|
|
43
|
-
it('
|
|
39
|
+
it('claims by workspace path and server URL regardless of agent id', () => {
|
|
44
40
|
const binding = enqueueMcpLaunchBinding({
|
|
45
41
|
agentId: 'agent-a',
|
|
46
42
|
workspacePath: 'C:/repo/demo',
|
|
47
43
|
serverUrl: 'ws://server:7380/ws/agent',
|
|
48
44
|
});
|
|
49
45
|
expect(claimMcpLaunchBinding({
|
|
50
|
-
agentId: 'agent-b',
|
|
51
|
-
bindingId: binding?.id,
|
|
52
|
-
workspacePath: 'C:/repo/demo',
|
|
53
|
-
serverUrl: 'ws://server:7380/ws/agent',
|
|
54
|
-
})).toBeNull();
|
|
55
|
-
expect(getMcpLaunchQueueSize('C:/repo/demo')).toBe(1);
|
|
56
|
-
expect(claimMcpLaunchBinding({
|
|
57
|
-
agentId: 'agent-a',
|
|
58
|
-
bindingId: binding?.id,
|
|
59
46
|
workspacePath: 'C:/repo/demo',
|
|
60
47
|
serverUrl: 'ws://server:7380/ws/agent',
|
|
61
48
|
})?.agentId).toBe('agent-a');
|
|
49
|
+
expect(binding?.agentId).toBe('agent-a');
|
|
62
50
|
});
|
|
63
|
-
it('
|
|
51
|
+
it('ignores explicit binding id and claims the first matching queue entry', () => {
|
|
64
52
|
const binding = enqueueMcpLaunchBinding({
|
|
65
53
|
agentId: 'agent-a',
|
|
66
54
|
workspacePath: 'C:/repo/demo',
|
|
67
55
|
serverUrl: 'ws://server:7380/ws/agent',
|
|
68
56
|
});
|
|
69
57
|
expect(claimMcpLaunchBinding({
|
|
70
|
-
agentId: 'agent-a',
|
|
71
58
|
workspacePath: 'C:/repo/demo',
|
|
72
59
|
serverUrl: 'ws://server:7380/ws/agent',
|
|
73
|
-
})).
|
|
60
|
+
})?.agentId).toBe('agent-a');
|
|
61
|
+
expect(binding?.id).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
it('matches by server URL as well as workspace path', () => {
|
|
64
|
+
const firstBinding = enqueueMcpLaunchBinding({ agentId: 'agent-a', workspacePath: 'C:/repo/demo', serverUrl: 'ws://server-a:7380/ws/agent' });
|
|
65
|
+
const secondBinding = enqueueMcpLaunchBinding({ agentId: 'agent-b', workspacePath: 'C:/repo/demo', serverUrl: 'ws://server-b:7380/ws/agent' });
|
|
66
|
+
expect(claimMcpLaunchBinding({ workspacePath: 'C:/repo/demo', serverUrl: 'ws://server-b:7380/ws/agent' })?.agentId).toBe('agent-b');
|
|
67
|
+
expect(claimMcpLaunchBinding({ workspacePath: 'C:/repo/demo', serverUrl: 'ws://server-a:7380/ws/agent' })?.agentId).toBe('agent-a');
|
|
68
|
+
});
|
|
69
|
+
it('matches http scheduler URLs and websocket agent URLs by scheduler origin', () => {
|
|
70
|
+
enqueueMcpLaunchBinding({ agentId: 'agent-a', workspacePath: 'C:/repo/demo', schedulerBaseUrl: 'http://server-a:7380' });
|
|
74
71
|
expect(claimMcpLaunchBinding({
|
|
72
|
+
workspacePath: 'C:/repo/demo',
|
|
73
|
+
serverUrl: 'ws://server-a:7380/ws/agent',
|
|
74
|
+
})?.agentId).toBe('agent-a');
|
|
75
|
+
});
|
|
76
|
+
it('repairs duplicated scheduler ports when enqueuing launch bindings', () => {
|
|
77
|
+
const binding = enqueueMcpLaunchBinding({
|
|
75
78
|
agentId: 'agent-a',
|
|
76
|
-
bindingId: 'wrong-binding',
|
|
77
79
|
workspacePath: 'C:/repo/demo',
|
|
78
|
-
|
|
79
|
-
})
|
|
80
|
-
expect(
|
|
80
|
+
schedulerBaseUrl: 'http://127.0.0.1:8080:8080',
|
|
81
|
+
});
|
|
82
|
+
expect(binding?.serverUrl).toBe('http://127.0.0.1:8080');
|
|
83
|
+
expect(binding?.schedulerBaseUrl).toBe('http://127.0.0.1:8080');
|
|
81
84
|
expect(claimMcpLaunchBinding({
|
|
85
|
+
workspacePath: 'C:/repo/demo',
|
|
86
|
+
serverUrl: 'ws://127.0.0.1:8080/ws/agent',
|
|
87
|
+
})?.schedulerBaseUrl).toBe('http://127.0.0.1:8080');
|
|
88
|
+
});
|
|
89
|
+
it('falls back to the only same-workspace binding when scheduler URLs differ', () => {
|
|
90
|
+
enqueueMcpLaunchBinding({
|
|
82
91
|
agentId: 'agent-a',
|
|
83
|
-
bindingId: binding?.id,
|
|
84
92
|
workspacePath: 'C:/repo/demo',
|
|
85
|
-
serverUrl: 'ws://
|
|
93
|
+
serverUrl: 'ws://127.0.0.1:7380/ws/agent',
|
|
94
|
+
});
|
|
95
|
+
expect(claimMcpLaunchBinding({
|
|
96
|
+
workspacePath: 'C:/repo/demo',
|
|
97
|
+
serverUrl: 'ws://101.42.200.84:7380/ws/agent',
|
|
86
98
|
})?.agentId).toBe('agent-a');
|
|
87
99
|
});
|
|
88
|
-
it('matches by server URL as well as workspace path', () => {
|
|
89
|
-
const firstBinding = enqueueMcpLaunchBinding({ agentId: 'agent-a', workspacePath: 'C:/repo/demo', serverUrl: 'ws://server-a:7380/ws/agent' });
|
|
90
|
-
const secondBinding = enqueueMcpLaunchBinding({ agentId: 'agent-b', workspacePath: 'C:/repo/demo', serverUrl: 'ws://server-b:7380/ws/agent' });
|
|
91
|
-
expect(claimMcpLaunchBinding({ agentId: 'agent-b', bindingId: secondBinding?.id, workspacePath: 'C:/repo/demo', serverUrl: 'ws://server-b:7380/ws/agent' })?.agentId).toBe('agent-b');
|
|
92
|
-
expect(claimMcpLaunchBinding({ agentId: 'agent-a', bindingId: firstBinding?.id, workspacePath: 'C:/repo/demo', serverUrl: 'ws://server-a:7380/ws/agent' })?.agentId).toBe('agent-a');
|
|
93
|
-
});
|
|
94
100
|
it('does not mix different workspace paths', () => {
|
|
95
101
|
const firstBinding = enqueueMcpLaunchBinding({ agentId: 'agent-a', workspacePath: 'C:/repo/a', serverUrl: 'ws://server:7380/ws/agent' });
|
|
96
102
|
const secondBinding = enqueueMcpLaunchBinding({ agentId: 'agent-b', workspacePath: 'C:/repo/b', serverUrl: 'ws://server:7380/ws/agent' });
|
|
97
|
-
expect(claimMcpLaunchBinding({
|
|
98
|
-
expect(claimMcpLaunchBinding({
|
|
103
|
+
expect(claimMcpLaunchBinding({ workspacePath: 'C:/repo/b', serverUrl: 'ws://server:7380/ws/agent' })?.agentId).toBe('agent-b');
|
|
104
|
+
expect(claimMcpLaunchBinding({ workspacePath: 'C:/repo/a', serverUrl: 'ws://server:7380/ws/agent' })?.agentId).toBe('agent-a');
|
|
99
105
|
});
|
|
100
106
|
it('returns null for missing or invalid workspaces', () => {
|
|
101
107
|
expect(enqueueMcpLaunchBinding({ agentId: '', workspacePath: 'C:/repo/demo' })).toBeNull();
|
|
102
108
|
expect(enqueueMcpLaunchBinding({ agentId: 'agent-a', workspacePath: '' })).toBeNull();
|
|
103
|
-
expect(claimMcpLaunchBinding({
|
|
104
|
-
expect(claimMcpLaunchBinding({
|
|
105
|
-
expect(claimMcpLaunchBinding({ agentId: 'agent-a', bindingId: 'binding-a', workspacePath: 'C:/repo/demo' })).toBeNull();
|
|
109
|
+
expect(claimMcpLaunchBinding({ workspacePath: 'C:/repo/missing', serverUrl: 'ws://server:7380/ws/agent' })).toBeNull();
|
|
110
|
+
expect(claimMcpLaunchBinding({ workspacePath: 'C:/repo/demo' })).toBeNull();
|
|
106
111
|
});
|
|
107
112
|
});
|
|
@@ -14,6 +14,7 @@ export type RuntimeBindingState = {
|
|
|
14
14
|
updatedAt?: string;
|
|
15
15
|
revokedAt?: string;
|
|
16
16
|
};
|
|
17
|
+
export declare function normalizeSchedulerBaseUrl(value: unknown): string | undefined;
|
|
17
18
|
export declare function getRuntimePairingCode(): string;
|
|
18
19
|
export declare function getRuntimeBindingFilePath(): string;
|
|
19
20
|
export declare function getRuntimeTokenStoreFilePath(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-binding.d.ts","sourceRoot":"","sources":["../../src/services/runtime-binding.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime-binding.d.ts","sourceRoot":"","sources":["../../src/services/runtime-binding.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AA2DF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAqB5E;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAoBD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,MAAM,CAYR;AAsCD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,MAAM,CAWT;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,MAAM,GAAG,SAAS,CAUpB;AAcD,wBAAgB,kBAAkB,IAAI,mBAAmB,CAgBxD;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAClD,mBAAmB,EACnB,WAAW,GAAG,aAAa,CAC5B,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAWtB;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAG3C;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAcnE;AAED,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,OAAO,EAChB,aAAa,CAAC,EAAE,OAAO,GACtB,MAAM,GAAG,SAAS,CAepB;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAEjE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,mBAAmB,CAwCtB;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAkB1C"}
|
|
@@ -46,6 +46,33 @@ function safeTextEqual(a, b) {
|
|
|
46
46
|
function normalizeToken(token) {
|
|
47
47
|
return String(token || "").trim();
|
|
48
48
|
}
|
|
49
|
+
function repairDuplicatePort(raw) {
|
|
50
|
+
return raw.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i, "$1");
|
|
51
|
+
}
|
|
52
|
+
export function normalizeSchedulerBaseUrl(value) {
|
|
53
|
+
const raw = normalizeToken(value);
|
|
54
|
+
if (!raw) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const repaired = repairDuplicatePort(raw);
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(repaired);
|
|
60
|
+
if (url.protocol === "ws:") {
|
|
61
|
+
url.protocol = "http:";
|
|
62
|
+
}
|
|
63
|
+
else if (url.protocol === "wss:") {
|
|
64
|
+
url.protocol = "https:";
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
url.protocol = url.protocol.toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
url.hostname = url.hostname.toLowerCase();
|
|
70
|
+
return url.origin;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return repaired.replace(/\/+$/, "");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
49
76
|
export function getRuntimePairingCode() {
|
|
50
77
|
return pairingCode;
|
|
51
78
|
}
|
|
@@ -56,7 +83,7 @@ export function getRuntimeTokenStoreFilePath() {
|
|
|
56
83
|
return tokenStoreFile();
|
|
57
84
|
}
|
|
58
85
|
function normalizeServerIp(serverBaseUrl) {
|
|
59
|
-
const raw = String(serverBaseUrl || "").trim();
|
|
86
|
+
const raw = normalizeSchedulerBaseUrl(serverBaseUrl) || String(serverBaseUrl || "").trim();
|
|
60
87
|
if (!raw) {
|
|
61
88
|
return "";
|
|
62
89
|
}
|
|
@@ -157,7 +184,11 @@ export function loadRuntimeBinding() {
|
|
|
157
184
|
const raw = fs.readFileSync(bindingFile(), "utf8");
|
|
158
185
|
const parsed = JSON.parse(raw);
|
|
159
186
|
if (parsed?.status === "paired" && parsed.tokenHash) {
|
|
160
|
-
return
|
|
187
|
+
return {
|
|
188
|
+
...parsed,
|
|
189
|
+
schedulerBaseUrl: normalizeSchedulerBaseUrl(parsed.schedulerBaseUrl) ||
|
|
190
|
+
parsed.schedulerBaseUrl,
|
|
191
|
+
};
|
|
161
192
|
}
|
|
162
193
|
}
|
|
163
194
|
catch {
|
|
@@ -195,6 +226,9 @@ export function getRuntimeAccessToken(userId, serverBaseUrl) {
|
|
|
195
226
|
if (scopedToken) {
|
|
196
227
|
return scopedToken;
|
|
197
228
|
}
|
|
229
|
+
if (serverBaseUrl) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
198
232
|
const state = loadRuntimeBinding();
|
|
199
233
|
if (state.status !== "paired") {
|
|
200
234
|
return undefined;
|
|
@@ -210,6 +244,8 @@ export function saveRuntimeBinding(input) {
|
|
|
210
244
|
throw new Error("accessToken must be at least 16 characters");
|
|
211
245
|
}
|
|
212
246
|
const previous = loadRuntimeBinding();
|
|
247
|
+
const schedulerBaseUrl = normalizeSchedulerBaseUrl(input.schedulerBaseUrl) ||
|
|
248
|
+
normalizeSchedulerBaseUrl(previous.schedulerBaseUrl);
|
|
213
249
|
const now = new Date().toISOString();
|
|
214
250
|
const next = {
|
|
215
251
|
status: "paired",
|
|
@@ -217,9 +253,7 @@ export function saveRuntimeBinding(input) {
|
|
|
217
253
|
? String(input.instanceId)
|
|
218
254
|
: previous.instanceId,
|
|
219
255
|
userId: input.userId ? String(input.userId) : previous.userId,
|
|
220
|
-
schedulerBaseUrl
|
|
221
|
-
? String(input.schedulerBaseUrl)
|
|
222
|
-
: previous.schedulerBaseUrl,
|
|
256
|
+
schedulerBaseUrl,
|
|
223
257
|
accessToken,
|
|
224
258
|
tokenHash: hashToken(accessToken),
|
|
225
259
|
createdAt: previous.createdAt || now,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-binding.test.d.ts","sourceRoot":"","sources":["../../src/services/runtime-binding.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeSchedulerBaseUrl } from './runtime-binding.js';
|
|
3
|
+
describe('runtime binding scheduler URL normalization', () => {
|
|
4
|
+
it('repairs duplicated scheduler ports', () => {
|
|
5
|
+
expect(normalizeSchedulerBaseUrl('http://127.0.0.1:8080:8080')).toBe('http://127.0.0.1:8080');
|
|
6
|
+
});
|
|
7
|
+
it('normalizes websocket agent URLs to scheduler HTTP origins', () => {
|
|
8
|
+
expect(normalizeSchedulerBaseUrl('ws://127.0.0.1:8080/ws/agent')).toBe('http://127.0.0.1:8080');
|
|
9
|
+
expect(normalizeSchedulerBaseUrl('wss://example.com/ws/agent')).toBe('https://example.com');
|
|
10
|
+
});
|
|
11
|
+
});
|