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 +9 -4
- package/dist/config.d.ts +14 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -4
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +95 -0
- package/dist/routes/instance.d.ts +15 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +25 -2
- package/dist/routes/instance.test.js +19 -0
- package/dist/services/aws-client-agent-mcp.test.js +1 -0
- package/dist/services/runtime-binding.d.ts.map +1 -1
- package/dist/services/runtime-binding.js +10 -3
- package/dist/services/runtime-binding.test.js +13 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
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
|
|
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
|
-
/**
|
|
11
|
-
export const
|
|
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() ||
|
|
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 @@
|
|
|
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"}
|
package/dist/routes/instance.js
CHANGED
|
@@ -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 {
|
|
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)
|
|
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,
|
|
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({
|