aws-runtime-bridge 1.3.9 → 1.4.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  AgentsWorkStudio 机器实例运行时桥接服务,用于在实例上管理 Agent 运行时、终端、配置与回调通信。
4
4
 
5
+ > `aws-runtime-bridge` 面向机器实例宿主机运行,需要访问本机工作区、终端、运行时配置和文件系统;不要将它作为 Docker/Compose 服务部署。Docker Compose 仅用于仓库根目录的 `aws-dashboard` 与 `aws-mcp-server` Web 服务。
6
+
5
7
  ## 全局安装
6
8
 
7
9
  从 npm 包安装时:
@@ -23,7 +25,7 @@ npm install -g .
23
25
 
24
26
  ## 启动
25
27
 
26
- 首次运行 `awsb` / `aws-bridge` 时,如果不存在 `~/.aws-bridge/config.json`,CLI 会进入交互式配置引导;也可以在提示中选择跳过。跳过时仍会创建配置文件并自动生成随机 `connectionKey`,终端会输出该密钥,请保存后在 server/面板连接此 Bridge 时使用。非交互环境(如 systemd、CI、Docker 后台启动)不会阻塞等待输入,也会自动生成随机 `connectionKey` 并跳过引导。
28
+ 首次运行 `awsb` / `aws-bridge` 时,如果不存在 `~/.aws-bridge/config.json`,CLI 会进入交互式配置引导;也可以在提示中选择跳过。跳过时仍会创建配置文件并自动生成随机 `connectionKey`,终端会输出该密钥,请保存后在 server/面板连接此 Bridge 时使用。非交互环境(如 systemd、CI 后台启动)不会阻塞等待输入,也会自动生成随机 `connectionKey` 并跳过引导。
27
29
 
28
30
  引导会生成类似下面的配置:
29
31
 
@@ -45,11 +47,14 @@ npm install -g .
45
47
 
46
48
  ```bash
47
49
  AWS_RUNTIME_BRIDGE_PORT=18081 \
48
- AWS_RUNTIME_SCHEDULER_BASE_URL=http://your-server-host:7380 \
49
50
  AWS_RUNTIME_HOME_DIR=/opt/agentswork/runtime-home \
50
51
  aws-bridge
51
52
  ```
52
53
 
54
+ 当 `~/.aws-bridge/config.json` 只配置了一个 `autoRegisterTargets[].serverUrl` 时,bridge 会自动将该地址作为 `/runtime/ping` 回连调度中心的地址,无需重复配置 `AWS_RUNTIME_SCHEDULER_BASE_URL`。如需显式覆盖,或同一个 bridge 配置了多个自动注册目标,请设置 `AWS_RUNTIME_SCHEDULER_BASE_URL` 指定当前实例测试连接时应回连的调度中心。
55
+
56
+ If an existing runtime binding still stores an old scheduler URL such as `http://127.0.0.1:8080`, re-run auto-register, refresh the runtime token, or re-pair after changing `serverUrl` / `AWS_RUNTIME_SCHEDULER_BASE_URL`; bridge will not reuse a token issued for the old scheduler URL against the new scheduler URL.
57
+
53
58
  `aws-runtime-bridge` 命令仍作为兼容别名保留。安装 `aws-runtime-bridge` 后,包内会随附
54
59
  `aws-client-agent-mcp` 的编译产物;bridge 启动时只负责准备该 MCP 产物,不再在 Agent 启动时默认动态注入 `aws-mcp`。
55
60
 
@@ -84,8 +89,8 @@ sudo awsb service uninstall
84
89
  | 变量名 | 说明 | 默认值 |
85
90
  | --- | --- | --- |
86
91
  | `AWS_RUNTIME_BRIDGE_PORT` | Bridge HTTP 端口 | `18081` |
87
- | `AWS_RUNTIME_SCHEDULER_BASE_URL` | aws-mcp-server 地址 | `http://localhost:8080` |
92
+ | `AWS_RUNTIME_SCHEDULER_BASE_URL` | aws-mcp-server 地址;显式配置优先级最高,未配置且只有一个 `autoRegisterTargets[].serverUrl` 时自动使用该地址 | 单目标自动注册地址;否则 `http://localhost:8080` |
88
93
  | `AWS_RUNTIME_HOME_DIR` | Bridge 管理配置与状态的主目录 | 当前用户 Home |
89
94
  | `AWS_RUNTIME_CORS_ORIGINS` | 允许访问 bridge 的来源,逗号分隔 | 本地开发地址 |
90
95
 
91
- 生产环境中,`AWS_RUNTIME_SCHEDULER_BASE_URL` 必须填写机器实例可访问的 `aws-mcp-server` 地址,不能使用容器内的 `localhost`。
96
+ 生产环境中,bridge 最终解析出的调度中心地址必须是机器实例可访问的 `aws-mcp-server` 地址,不能使用不可达的容器内 `localhost`。如果配置了多个 `autoRegisterTargets`,bridge 不会静默选择第一个目标,需通过 `AWS_RUNTIME_SCHEDULER_BASE_URL` 或后续多调度中心身份路由明确目标。
package/dist/config.d.ts CHANGED
@@ -5,7 +5,20 @@
5
5
  */
6
6
  /** 服务端口 */
7
7
  export declare const port: number;
8
- /** 调度器基础 URL */
8
+ /** 默认调度器基础 URL */
9
+ export declare const DEFAULT_SCHEDULER_BASE_URL = "http://localhost:8080";
10
+ export type SchedulerBaseUrlSource = "env" | "single-auto-register-target" | "default" | "ambiguous-auto-register-targets";
11
+ export interface SchedulerBaseUrlResolution {
12
+ url: string;
13
+ source: SchedulerBaseUrlSource;
14
+ ambiguousTargetUrls: string[];
15
+ }
16
+ /**
17
+ * 解析调度中心基础 URL。
18
+ * 主流程:显式环境变量优先;未配置时,单个自动注册目标可作为宿主机部署的零额外配置回退;多目标不静默取第一个。
19
+ */
20
+ export declare function resolveSchedulerBaseUrl(): SchedulerBaseUrlResolution;
21
+ /** 调度器基础 URL(兼容旧导入;请求处理应优先调用 resolveSchedulerBaseUrl 获取最新值) */
9
22
  export declare const schedulerBaseUrl: string;
10
23
  /** Node 环境 */
11
24
  export declare const nodeEnv: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,WAAW;AACX,eAAO,MAAM,IAAI,EAAE,MAElB,CAAC;AAEF,gBAAgB;AAChB,eAAO,MAAM,gBAAgB,EAAE,MACwC,CAAC;AAExE,cAAc;AACd,eAAO,MAAM,OAAO,EAAE,MAA8C,CAAC;AAErE,oCAAoC;AACpC,eAAO,MAAM,oBAAoB,EAAE,OACiB,CAAC;AAErD,8BAA8B;AAC9B,eAAO,MAAM,kBAAkB,EAAE,MAAM,EAMC,CAAC;AAEzC,gEAAgE;AAChE,wBAAgB,uBAAuB,IAAI,IAAI,CAAG;AAElD,eAAe;AACf,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,8CAA8C;AAC9C,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,2BAA2B;AAC3B,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKlE;AAED,0BAA0B;AAC1B,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,yBAAyB;AACzB,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED,sBAAsB;AACtB,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,yBAAyB;AACzB,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,4BAA4B;AAC5B,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED;;GAEG;AAEH,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,sBAAsB,EACrB,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1C,eAAe;AACf,eAAO,MAAM,mBAAmB,SACiB,CAAC;AAElD,iCAAiC;AACjC,eAAO,MAAM,2BAA2B,QAEvC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AAEF,qCAAqC;AACrC,eAAO,MAAM,uBAAuB,QAEnC,CAAC;AAEF;;GAEG;AAEH,eAAe;AACf,eAAO,MAAM,qBAAqB,SAA2C,CAAC;AAE9E,YAAY;AACZ,eAAO,MAAM,uBAAuB,QAAkC,CAAC;AAEvE,iBAAiB;AACjB,eAAO,MAAM,sBAAsB,QAAiC,CAAC;AAErE,oBAAoB;AACpB,eAAO,MAAM,2BAA2B,QAAsC,CAAC;AAE/E,WAAW;AACX,eAAO,MAAM,0BAA0B,QAAqC,CAAC;AAE7E,WAAW;AACX,eAAO,MAAM,uBAAuB,QAAkC,CAAC;AAEvE,aAAa;AACb,eAAO,MAAM,4BAA4B,QACH,CAAC;AAEvC,kCAAkC;AAClC,eAAO,MAAM,wBAAwB,QAC4B,CAAC;AAElE,aAAa;AACb,eAAO,MAAM,yBAAyB,QAErC,CAAC;AAEF,iBAAiB;AACjB,eAAO,MAAM,4BAA4B,QAExC,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,WAAW;AACX,eAAO,MAAM,IAAI,EAAE,MAElB,CAAC;AAEF,kBAAkB;AAClB,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AAElE,MAAM,MAAM,sBAAsB,GAC9B,KAAK,GACL,6BAA6B,GAC7B,SAAS,GACT,iCAAiC,CAAC;AAEtC,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,sBAAsB,CAAC;IAC/B,mBAAmB,EAAE,MAAM,EAAE,CAAC;CAC/B;AA+ED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,0BAA0B,CAiCpE;AAED,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,EAAE,MAEH,CAAC;AAE7B,cAAc;AACd,eAAO,MAAM,OAAO,EAAE,MAA8C,CAAC;AAErE,oCAAoC;AACpC,eAAO,MAAM,oBAAoB,EAAE,OACiB,CAAC;AAErD,8BAA8B;AAC9B,eAAO,MAAM,kBAAkB,EAAE,MAAM,EAMC,CAAC;AAEzC,gEAAgE;AAChE,wBAAgB,uBAAuB,IAAI,IAAI,CAAG;AAElD,eAAe;AACf,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,8CAA8C;AAC9C,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,2BAA2B;AAC3B,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKlE;AAED,0BAA0B;AAC1B,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,yBAAyB;AACzB,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED,sBAAsB;AACtB,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,yBAAyB;AACzB,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,4BAA4B;AAC5B,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED;;GAEG;AAEH,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,sBAAsB,EACrB,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1C,eAAe;AACf,eAAO,MAAM,mBAAmB,SACiB,CAAC;AAElD,iCAAiC;AACjC,eAAO,MAAM,2BAA2B,QAEvC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AAEF,qCAAqC;AACrC,eAAO,MAAM,uBAAuB,QAEnC,CAAC;AAEF;;GAEG;AAEH,eAAe;AACf,eAAO,MAAM,qBAAqB,SAA2C,CAAC;AAE9E,YAAY;AACZ,eAAO,MAAM,uBAAuB,QAAkC,CAAC;AAEvE,iBAAiB;AACjB,eAAO,MAAM,sBAAsB,QAAiC,CAAC;AAErE,oBAAoB;AACpB,eAAO,MAAM,2BAA2B,QAAsC,CAAC;AAE/E,WAAW;AACX,eAAO,MAAM,0BAA0B,QAAqC,CAAC;AAE7E,WAAW;AACX,eAAO,MAAM,uBAAuB,QAAkC,CAAC;AAEvE,aAAa;AACb,eAAO,MAAM,4BAA4B,QACH,CAAC;AAEvC,kCAAkC;AAClC,eAAO,MAAM,wBAAwB,QAC4B,CAAC;AAElE,aAAa;AACb,eAAO,MAAM,yBAAyB,QAErC,CAAC;AAEF,iBAAiB;AACjB,eAAO,MAAM,4BAA4B,QAExC,CAAC"}
package/dist/config.js CHANGED
@@ -3,12 +3,111 @@
3
3
  *
4
4
  * 集中管理所有环境变量和配置常量
5
5
  */
6
- import os from "node:os";
6
+ import { homedir } from "node:os";
7
7
  import path from "node:path";
8
+ import fs from "node:fs";
8
9
  /** 服务端口 */
9
10
  export const port = Number(process.env.AWS_RUNTIME_BRIDGE_PORT || 18081);
10
- /** 调度器基础 URL */
11
- export const schedulerBaseUrl = process.env.AWS_RUNTIME_SCHEDULER_BASE_URL || "http://localhost:8080";
11
+ /** 默认调度器基础 URL */
12
+ export const DEFAULT_SCHEDULER_BASE_URL = "http://localhost:8080";
13
+ function normalizeOptionalString(value) {
14
+ if (value == null) {
15
+ return undefined;
16
+ }
17
+ const normalized = String(value).trim();
18
+ return normalized || undefined;
19
+ }
20
+ function normalizeSchedulerHttpBaseUrl(value) {
21
+ const raw = normalizeOptionalString(value);
22
+ if (!raw) {
23
+ return undefined;
24
+ }
25
+ const repaired = raw.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i, "$1");
26
+ try {
27
+ const url = new URL(repaired);
28
+ if (url.protocol === "ws:") {
29
+ url.protocol = "http:";
30
+ }
31
+ else if (url.protocol === "wss:") {
32
+ url.protocol = "https:";
33
+ }
34
+ else {
35
+ url.protocol = url.protocol.toLowerCase();
36
+ }
37
+ url.hostname = url.hostname.toLowerCase();
38
+ return url.origin;
39
+ }
40
+ catch {
41
+ return repaired.replace(/\/+$/, "");
42
+ }
43
+ }
44
+ function getAutoRegisterConfigFilePath() {
45
+ const testHomeDir = String(process.env.AWS_TEST_HOME || "").trim();
46
+ return path.join(testHomeDir || homedir(), ".aws-bridge", "config.json");
47
+ }
48
+ function readAutoRegisterTargetUrls() {
49
+ const configPath = getAutoRegisterConfigFilePath();
50
+ try {
51
+ if (!fs.existsSync(configPath)) {
52
+ return [];
53
+ }
54
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
55
+ const rawTargets = Array.isArray(parsed.autoRegisterTargets)
56
+ ? parsed.autoRegisterTargets
57
+ : [];
58
+ const targetUrls = rawTargets
59
+ .filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item)))
60
+ .map((target) => normalizeSchedulerHttpBaseUrl(target.serverUrl) ||
61
+ normalizeSchedulerHttpBaseUrl(target.schedulerBaseUrl))
62
+ .filter((url) => Boolean(url));
63
+ if (targetUrls.length > 0) {
64
+ return Array.from(new Set(targetUrls));
65
+ }
66
+ const topLevelUrl = normalizeSchedulerHttpBaseUrl(parsed.serverUrl) ||
67
+ normalizeSchedulerHttpBaseUrl(parsed.schedulerBaseUrl);
68
+ return topLevelUrl ? [topLevelUrl] : [];
69
+ }
70
+ catch {
71
+ return [];
72
+ }
73
+ }
74
+ /**
75
+ * 解析调度中心基础 URL。
76
+ * 主流程:显式环境变量优先;未配置时,单个自动注册目标可作为宿主机部署的零额外配置回退;多目标不静默取第一个。
77
+ */
78
+ export function resolveSchedulerBaseUrl() {
79
+ const envSchedulerBaseUrl = normalizeSchedulerHttpBaseUrl(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL);
80
+ if (envSchedulerBaseUrl) {
81
+ return {
82
+ url: envSchedulerBaseUrl,
83
+ source: "env",
84
+ ambiguousTargetUrls: [],
85
+ };
86
+ }
87
+ const autoRegisterTargetUrls = readAutoRegisterTargetUrls();
88
+ if (autoRegisterTargetUrls.length === 1) {
89
+ return {
90
+ url: autoRegisterTargetUrls[0] || DEFAULT_SCHEDULER_BASE_URL,
91
+ source: "single-auto-register-target",
92
+ ambiguousTargetUrls: [],
93
+ };
94
+ }
95
+ if (autoRegisterTargetUrls.length > 1) {
96
+ return {
97
+ url: DEFAULT_SCHEDULER_BASE_URL,
98
+ source: "ambiguous-auto-register-targets",
99
+ ambiguousTargetUrls: autoRegisterTargetUrls,
100
+ };
101
+ }
102
+ return {
103
+ url: DEFAULT_SCHEDULER_BASE_URL,
104
+ source: "default",
105
+ ambiguousTargetUrls: [],
106
+ };
107
+ }
108
+ /** 调度器基础 URL(兼容旧导入;请求处理应优先调用 resolveSchedulerBaseUrl 获取最新值) */
109
+ export const schedulerBaseUrl = normalizeSchedulerHttpBaseUrl(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL) ||
110
+ DEFAULT_SCHEDULER_BASE_URL;
12
111
  /** Node 环境 */
13
112
  export const nodeEnv = process.env.NODE_ENV || "development";
14
113
  /** 是否允许浏览宿主机任意目录(默认关闭,仅建议本地排障启用) */
@@ -23,7 +122,7 @@ export const allowedCorsOrigins = String(process.env.AWS_RUNTIME_CORS_ORIGINS ||
23
122
  export function validateProductionToken() { }
24
123
  /** 获取运行时主目录 */
25
124
  export function getRuntimeHomeDir() {
26
- return String(process.env.AWS_RUNTIME_HOME_DIR || "").trim() || os.homedir();
125
+ return String(process.env.AWS_RUNTIME_HOME_DIR || "").trim() || homedir();
27
126
  }
28
127
  /** 获取 Claude 配置文件路径(AI 配置写入 settings.json) */
29
128
  export function getClaudeConfigFile(runtimeHome) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../src/config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,95 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ vi.mock("node:os", async () => {
6
+ const actual = await vi.importActual("node:os");
7
+ const mocked = {
8
+ ...actual,
9
+ homedir: () => process.env.AWS_TEST_HOME || actual.homedir(),
10
+ };
11
+ return {
12
+ ...mocked,
13
+ default: mocked,
14
+ };
15
+ });
16
+ const originalEnv = { ...process.env };
17
+ const tempRoots = [];
18
+ function createRuntimeHome() {
19
+ const root = mkdtempSync(path.join(os.tmpdir(), "aws-config-"));
20
+ tempRoots.push(root);
21
+ return root;
22
+ }
23
+ function useRuntimeHome(runtimeHome) {
24
+ process.env.AWS_TEST_HOME = runtimeHome;
25
+ process.env.HOME = runtimeHome;
26
+ process.env.USERPROFILE = runtimeHome;
27
+ process.env.AWS_RUNTIME_HOME_DIR = runtimeHome;
28
+ delete process.env.AWS_RUNTIME_SCHEDULER_BASE_URL;
29
+ }
30
+ function writeBridgeConfig(runtimeHome, autoRegisterTargets) {
31
+ const configPath = path.join(runtimeHome, ".aws-bridge", "config.json");
32
+ mkdirSync(path.dirname(configPath), { recursive: true });
33
+ writeFileSync(configPath, `${JSON.stringify({ connectionKey: "bridge-key", autoRegisterTargets }, null, 2)}\n`, "utf-8");
34
+ }
35
+ afterEach(() => {
36
+ process.env = { ...originalEnv };
37
+ vi.resetModules();
38
+ vi.clearAllMocks();
39
+ vi.restoreAllMocks();
40
+ for (const root of tempRoots.splice(0)) {
41
+ rmSync(root, { recursive: true, force: true });
42
+ }
43
+ });
44
+ describe("scheduler base URL resolution", () => {
45
+ it("keeps explicit AWS_RUNTIME_SCHEDULER_BASE_URL as the highest priority", async () => {
46
+ const runtimeHome = createRuntimeHome();
47
+ useRuntimeHome(runtimeHome);
48
+ process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = "http://explicit.local:7380/path";
49
+ writeBridgeConfig(runtimeHome, [{ serverUrl: "http://target.local:7380" }]);
50
+ const { resolveSchedulerBaseUrl } = await import("./config.js");
51
+ expect(resolveSchedulerBaseUrl()).toEqual({
52
+ url: "http://explicit.local:7380",
53
+ source: "env",
54
+ ambiguousTargetUrls: [],
55
+ });
56
+ });
57
+ it("uses a single auto-register target serverUrl when scheduler env is absent", async () => {
58
+ const runtimeHome = createRuntimeHome();
59
+ useRuntimeHome(runtimeHome);
60
+ writeBridgeConfig(runtimeHome, [{ serverUrl: "http://127.0.0.1:7380" }]);
61
+ const { resolveSchedulerBaseUrl } = await import("./config.js");
62
+ expect(resolveSchedulerBaseUrl()).toEqual({
63
+ url: "http://127.0.0.1:7380",
64
+ source: "single-auto-register-target",
65
+ ambiguousTargetUrls: [],
66
+ });
67
+ });
68
+ it("does not silently select the first auto-register target when multiple URLs exist", async () => {
69
+ const runtimeHome = createRuntimeHome();
70
+ useRuntimeHome(runtimeHome);
71
+ writeBridgeConfig(runtimeHome, [
72
+ { serverUrl: "http://scheduler-a.local:7380" },
73
+ { serverUrl: "http://scheduler-b.local:7380" },
74
+ ]);
75
+ const { DEFAULT_SCHEDULER_BASE_URL, resolveSchedulerBaseUrl } = await import("./config.js");
76
+ expect(resolveSchedulerBaseUrl()).toEqual({
77
+ url: DEFAULT_SCHEDULER_BASE_URL,
78
+ source: "ambiguous-auto-register-targets",
79
+ ambiguousTargetUrls: [
80
+ "http://scheduler-a.local:7380",
81
+ "http://scheduler-b.local:7380",
82
+ ],
83
+ });
84
+ });
85
+ it("falls back to the legacy localhost default when no scheduler source exists", async () => {
86
+ const runtimeHome = createRuntimeHome();
87
+ useRuntimeHome(runtimeHome);
88
+ const { DEFAULT_SCHEDULER_BASE_URL, resolveSchedulerBaseUrl } = await import("./config.js");
89
+ expect(resolveSchedulerBaseUrl()).toEqual({
90
+ url: DEFAULT_SCHEDULER_BASE_URL,
91
+ source: "default",
92
+ ambiguousTargetUrls: [],
93
+ });
94
+ });
95
+ });
@@ -18,9 +18,24 @@ export interface SchedulerPingFailureResponse {
18
18
  schedulerBaseUrl: string;
19
19
  hint: string;
20
20
  }
21
+ interface AmbiguousSchedulerBaseUrlResponse {
22
+ ok: false;
23
+ error: "ambiguous_scheduler_base_url";
24
+ failureStage: "scheduler_ping";
25
+ runtimeBridge: "healthy";
26
+ schedulerBaseUrl: string;
27
+ autoRegisterTargetUrls: string[];
28
+ hint: string;
29
+ }
21
30
  /**
22
31
  * 构建调度中心 ping 失败响应。
23
32
  * 主流程:保留底层错误与 scheduler 地址,同时明确 bridge 本身已连通、失败点在 bridge 回连调度中心。
24
33
  */
25
34
  export declare function buildSchedulerPingFailureResponse(error: Error, configuredSchedulerBaseUrl: string): SchedulerPingFailureResponse;
35
+ /**
36
+ * Build the response for ambiguous scheduler targets.
37
+ * Main flow: never silently pick the first target; require an explicit scheduler URL or future scheduler identity routing.
38
+ */
39
+ export declare function buildAmbiguousSchedulerBaseUrlResponse(targetUrls: string[]): AmbiguousSchedulerBaseUrlResponse;
40
+ export {};
26
41
  //# sourceMappingURL=instance.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"AA6BA,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,IAAI,CAEN;AAED,eAAO,MAAM,cAAc,4CAAW,CAAC;AAyBvC,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,MAAM,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B,oBAAoB,EAAE,OAAO,CAAC;QAC9B,qBAAqB,EAAE,OAAO,CAAC;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAqCA;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,KAAK,EACZ,0BAA0B,EAAE,MAAM,GACjC,4BAA4B,CAiB9B"}
1
+ {"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"AA6BA,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,IAAI,CAEN;AAED,eAAO,MAAM,cAAc,4CAAW,CAAC;AAyBvC,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,MAAM,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B,oBAAoB,EAAE,OAAO,CAAC;QAC9B,qBAAqB,EAAE,OAAO,CAAC;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAqCA;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,iCAAiC;IACzC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,8BAA8B,CAAC;IACtC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,KAAK,EACZ,0BAA0B,EAAE,MAAM,GACjC,4BAA4B,CAiB9B;AAED;;;GAGG;AACH,wBAAgB,sCAAsC,CACpD,UAAU,EAAE,MAAM,EAAE,GACnB,iCAAiC,CAUnC"}
@@ -2,7 +2,7 @@ import { Router } from "express";
2
2
  import axios from "axios";
3
3
  import { createHash, timingSafeEqual } from "node:crypto";
4
4
  import { validateToken } from "../middleware/auth.js";
5
- import { schedulerBaseUrl } from "../config.js";
5
+ import { resolveSchedulerBaseUrl } from "../config.js";
6
6
  import { getRuntimeAccessToken } from "../services/runtime-binding.js";
7
7
  import { loadInstanceState, saveInstanceState, } from "../services/instance-state.js";
8
8
  import { initInstance } from "../services/instance-init-service.js";
@@ -91,6 +91,21 @@ export function buildSchedulerPingFailureResponse(error, configuredSchedulerBase
91
91
  : "aws-runtime-bridge 已连通,但它回连调度中心失败。请确认 AWS_RUNTIME_SCHEDULER_BASE_URL 指向的调度中心地址可从 bridge 机器访问,并且运行时访问令牌仍有效。",
92
92
  };
93
93
  }
94
+ /**
95
+ * Build the response for ambiguous scheduler targets.
96
+ * Main flow: never silently pick the first target; require an explicit scheduler URL or future scheduler identity routing.
97
+ */
98
+ export function buildAmbiguousSchedulerBaseUrlResponse(targetUrls) {
99
+ return {
100
+ ok: false,
101
+ error: "ambiguous_scheduler_base_url",
102
+ failureStage: "scheduler_ping",
103
+ runtimeBridge: "healthy",
104
+ schedulerBaseUrl: "",
105
+ autoRegisterTargetUrls: targetUrls,
106
+ hint: "Multiple autoRegisterTargets.serverUrl values were found, so bridge cannot safely infer which scheduler /runtime/ping should call back. Set AWS_RUNTIME_SCHEDULER_BASE_URL explicitly, or use future schedulerId/token-bound multi-scheduler routing.",
107
+ };
108
+ }
94
109
  instanceRouter.get("/healthz", (_req, res) => {
95
110
  res.json({
96
111
  ok: true,
@@ -103,8 +118,16 @@ instanceRouter.get("/connection-check", (req, res) => {
103
118
  res.status(result.status).json(result.body);
104
119
  });
105
120
  instanceRouter.get("/ping", validateToken, async (_req, res) => {
121
+ const schedulerBaseUrlResolution = resolveSchedulerBaseUrl();
122
+ if (schedulerBaseUrlResolution.source === "ambiguous-auto-register-targets") {
123
+ res
124
+ .status(400)
125
+ .json(buildAmbiguousSchedulerBaseUrlResponse(schedulerBaseUrlResolution.ambiguousTargetUrls));
126
+ return;
127
+ }
128
+ const schedulerBaseUrl = schedulerBaseUrlResolution.url;
106
129
  try {
107
- const runtimeAccessToken = getRuntimeAccessToken(undefined, schedulerBaseUrl) || getRuntimeAccessToken();
130
+ const runtimeAccessToken = getRuntimeAccessToken(undefined, schedulerBaseUrl);
108
131
  if (!runtimeAccessToken) {
109
132
  res.status(401).json({ ok: false, error: "runtime_access_token_required" });
110
133
  return;
@@ -119,4 +119,23 @@ describe('instance route validation', () => {
119
119
  expect(response.runtimeBridge).toBe('healthy');
120
120
  expect(response.hint).toContain('运行时访问令牌仍有效');
121
121
  });
122
+ it('explains ambiguous auto-register scheduler targets without picking the first one', async () => {
123
+ const { buildAmbiguousSchedulerBaseUrlResponse } = await import('./instance.js');
124
+ const response = buildAmbiguousSchedulerBaseUrlResponse([
125
+ 'http://scheduler-a.local:7380',
126
+ 'http://scheduler-b.local:7380',
127
+ ]);
128
+ expect(response).toEqual({
129
+ ok: false,
130
+ error: 'ambiguous_scheduler_base_url',
131
+ failureStage: 'scheduler_ping',
132
+ runtimeBridge: 'healthy',
133
+ schedulerBaseUrl: '',
134
+ autoRegisterTargetUrls: [
135
+ 'http://scheduler-a.local:7380',
136
+ 'http://scheduler-b.local:7380',
137
+ ],
138
+ hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
139
+ });
140
+ });
122
141
  });
@@ -199,6 +199,7 @@ describe('aws-client-agent-mcp service', () => {
199
199
  process.env.AWS_CLIENT_AGENT_MCP_COMMAND = 'aws-client-agent-mcp';
200
200
  process.env.CUSTOM_SECRET = 'should-not-leak';
201
201
  process.env.AWS_RUNTIME_HOME_DIR = mkdtempSync(path.join(os.tmpdir(), 'aws-mcp-config-'));
202
+ process.env.AWS_TEST_HOME = process.env.AWS_RUNTIME_HOME_DIR;
202
203
  process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = '';
203
204
  process.env.AWS_SERVER_URL = '';
204
205
  process.env.AWS_MCP_HTTP_URL = '';
@@ -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;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;AAiCD,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;AAED,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,OAAO,CAeT;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"}
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;AAiCD,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;AAED,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,OAAO,CAeT;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,CAyBpB;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"}
@@ -254,13 +254,20 @@ export function getRuntimeAccessToken(userId, serverBaseUrl) {
254
254
  if (scopedToken) {
255
255
  return scopedToken;
256
256
  }
257
- if (serverBaseUrl) {
258
- return undefined;
259
- }
260
257
  const state = loadRuntimeBinding();
261
258
  if (state.status !== "paired") {
262
259
  return undefined;
263
260
  }
261
+ if (serverBaseUrl) {
262
+ const requestedSchedulerBaseUrl = normalizeSchedulerBaseUrl(serverBaseUrl);
263
+ const boundSchedulerBaseUrl = normalizeSchedulerBaseUrl(state.schedulerBaseUrl);
264
+ if (requestedSchedulerBaseUrl &&
265
+ boundSchedulerBaseUrl &&
266
+ requestedSchedulerBaseUrl === boundSchedulerBaseUrl) {
267
+ return normalizeToken(state.accessToken) || undefined;
268
+ }
269
+ return undefined;
270
+ }
264
271
  return normalizeToken(state.accessToken) || undefined;
265
272
  }
266
273
  export function validateRuntimePairingCode(code) {
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { afterEach, describe, expect, it } from "vitest";
5
- import { buildRuntimeTokenKey, clearScopedRuntimeAccessToken, getScopedRuntimeAccessToken, normalizeSchedulerBaseUrl, saveScopedRuntimeAccessToken, } from "./runtime-binding.js";
5
+ import { buildRuntimeTokenKey, clearScopedRuntimeAccessToken, getRuntimeAccessToken, getScopedRuntimeAccessToken, normalizeSchedulerBaseUrl, saveScopedRuntimeAccessToken, } from "./runtime-binding.js";
6
6
  const originalEnv = { ...process.env };
7
7
  const tempRoots = [];
8
8
  function useRuntimeHome() {
@@ -28,6 +28,18 @@ describe('runtime binding scheduler URL normalization', () => {
28
28
  expect(normalizeSchedulerBaseUrl('ws://127.0.0.1:8080/ws/agent')).toBe('http://127.0.0.1:8080');
29
29
  expect(normalizeSchedulerBaseUrl('wss://example.com/ws/agent')).toBe('https://example.com');
30
30
  });
31
+ it("returns paired binding token only when requested scheduler URL matches", async () => {
32
+ useRuntimeHome();
33
+ const { saveRuntimeBinding } = await import("./runtime-binding.js");
34
+ saveRuntimeBinding({
35
+ accessToken: "paired-runtime-token-123456",
36
+ instanceId: "bridge-1",
37
+ userId: "user-a",
38
+ schedulerBaseUrl: "http://scheduler.local:7380",
39
+ });
40
+ expect(getRuntimeAccessToken(undefined, "http://scheduler.local:7380/api")).toBe("paired-runtime-token-123456");
41
+ expect(getRuntimeAccessToken(undefined, "http://scheduler.local:8080")).toBeUndefined();
42
+ });
31
43
  it("scopes runtime tokens by user and full scheduler origin", () => {
32
44
  useRuntimeHome();
33
45
  const key7380 = saveScopedRuntimeAccessToken({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-runtime-bridge",
3
- "version": "1.3.9",
3
+ "version": "1.4.0",
4
4
  "description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",