aws-runtime-bridge 1.3.8 → 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` 或后续多调度中心身份路由明确目标。
@@ -92,6 +92,18 @@ export declare class ClaudeSdkAdapter extends EventEmitter implements BaseProvid
92
92
  * 支持 Windows / macOS / Linux
93
93
  */
94
94
  private findClaudeExecutable;
95
+ /**
96
+ * 从面板配置的命令行中提取实际可执行文件,避免把参数一起传给 PATH 查找。
97
+ */
98
+ private extractExecutableCommand;
99
+ /**
100
+ * 按当前 Bridge 进程的 PATH/PATHEXT 直接查找可执行文件,避免依赖 which/where 和 shell 解析。
101
+ */
102
+ private resolveExecutableFromPath;
103
+ private getWindowsPathExtensions;
104
+ private shouldUseDefaultClaudeFallbacks;
105
+ private isWindowsWrapperPath;
106
+ private resolveWindowsWrapper;
95
107
  /**
96
108
  * 从 wrapper 脚本解析 cli.js 路径
97
109
  */
@@ -1 +1 @@
1
- {"version":3,"file":"ClaudeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/ClaudeSdkAdapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ClaudeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/ClaudeSdkAdapter.ts"],"names":[],"mappings":"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"}
@@ -9,11 +9,11 @@
9
9
  * - 支持 AskUserQuestion 拦截并转发到聊天面板
10
10
  * - 支持 turn_complete 事件精确判断 AI 就绪状态
11
11
  */
12
+ import { execSync } from 'child_process';
12
13
  import { EventEmitter } from 'events';
13
- import { v4 as uuidv4 } from 'uuid';
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
- import { execSync } from 'child_process';
16
+ import { v4 as uuidv4 } from 'uuid';
17
17
  import { getToolActionInfo } from './types.js';
18
18
  // ============ AsyncIterableQueue ============
19
19
  /**
@@ -574,13 +574,25 @@ export class ClaudeSdkAdapter extends EventEmitter {
574
574
  * 支持 Windows / macOS / Linux
575
575
  */
576
576
  findClaudeExecutable(command) {
577
- const normalizedCommand = (command || 'claude').trim() || 'claude';
577
+ const normalizedCommand = this.extractExecutableCommand(command);
578
578
  // 1. 如果是绝对路径,直接返回
579
579
  if (path.isAbsolute(normalizedCommand) && fs.existsSync(normalizedCommand)) {
580
+ const resolvedWrapper = this.resolveWindowsWrapper(normalizedCommand);
581
+ if (resolvedWrapper) {
582
+ return resolvedWrapper;
583
+ }
584
+ if (this.isWindowsWrapperPath(normalizedCommand)) {
585
+ console.warn(`[ClaudeSdkAdapter] Skipping wrapper script for Claude Code executable: ${normalizedCommand}`);
586
+ return undefined;
587
+ }
580
588
  return normalizedCommand;
581
589
  }
582
- // 2. Windows 特定路径(优先级高)
583
- if (process.platform === 'win32') {
590
+ const pathExecutable = this.resolveExecutableFromPath(normalizedCommand);
591
+ if (pathExecutable) {
592
+ return pathExecutable;
593
+ }
594
+ // 2. Windows 特定路径(作为 claude 默认命令的兜底)
595
+ if (process.platform === 'win32' && this.shouldUseDefaultClaudeFallbacks(normalizedCommand)) {
584
596
  const userProfile = process.env.USERPROFILE || '';
585
597
  const localAppData = process.env.LOCALAPPDATA || '';
586
598
  const appData = process.env.APPDATA || '';
@@ -607,55 +619,111 @@ export class ClaudeSdkAdapter extends EventEmitter {
607
619
  }
608
620
  }
609
621
  }
610
- // 3. 使用 which/where 查找
611
- try {
612
- const findCmd = process.platform === 'win32' ? `where ${normalizedCommand}` : `which ${normalizedCommand}`;
613
- const result = execSync(findCmd, { encoding: 'utf8', timeout: 5000 }).trim();
614
- if (result) {
615
- const foundPath = result.split(/\r?\n/)[0].trim();
616
- if (fs.existsSync(foundPath)) {
617
- // 如果是 wrapper 脚本,尝试找到实际的 cli.js
618
- if (foundPath.endsWith('.cmd') || foundPath.endsWith('.ps1')) {
619
- const cliJs = this.resolveCliJsFromWrapper(foundPath);
620
- if (cliJs)
621
- return cliJs;
622
- }
623
- return foundPath;
624
- }
625
- }
626
- }
627
- catch {
628
- // 忽略查找失败
629
- }
630
- // 4. macOS/Linux 特定路径
622
+ // 3. macOS/Linux 特定路径(作为 claude 默认命令的兜底)
631
623
  const home = process.env.HOME || '';
632
624
  const candidates = [
625
+ path.join(home, '.local', 'bin', 'claude'),
626
+ path.join(home, '.npm-global', 'bin', 'claude'),
627
+ path.join(home, '.local', 'share', 'mise', 'shims', 'claude'),
628
+ path.join(home, '.asdf', 'shims', 'claude'),
629
+ `/opt/homebrew/bin/claude`,
630
+ `/usr/local/bin/claude`,
631
+ `/usr/bin/claude`,
633
632
  `/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js`,
634
633
  `/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js`,
635
634
  path.join(home, '.local', 'share', 'volta', 'tools', 'shared', '@anthropic-ai', 'claude-code', 'cli.js'),
636
635
  path.join(home, '.volta', 'tools', 'shared', '@anthropic-ai', 'claude-code', 'cli.js'),
637
636
  ];
638
- for (const p of candidates) {
639
- if (p && fs.existsSync(p)) {
640
- console.log(`[ClaudeSdkAdapter] Found Claude Code CLI: ${p}`);
641
- return p;
637
+ if (this.shouldUseDefaultClaudeFallbacks(normalizedCommand)) {
638
+ for (const p of candidates) {
639
+ if (p && fs.existsSync(p)) {
640
+ console.log(`[ClaudeSdkAdapter] Found Claude Code CLI: ${p}`);
641
+ return p;
642
+ }
642
643
  }
643
644
  }
644
- // 5. 尝试 npm root -g
645
- try {
646
- const npmRoot = execSync('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
647
- const cliJs = path.join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js');
648
- if (fs.existsSync(cliJs)) {
649
- console.log(`[ClaudeSdkAdapter] Found Claude Code CLI via npm root: ${cliJs}`);
650
- return cliJs;
645
+ // 4. 尝试 npm root -g
646
+ if (this.shouldUseDefaultClaudeFallbacks(normalizedCommand)) {
647
+ try {
648
+ const npmRoot = execSync('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
649
+ const cliJs = path.join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js');
650
+ if (fs.existsSync(cliJs)) {
651
+ console.log(`[ClaudeSdkAdapter] Found Claude Code CLI via npm root: ${cliJs}`);
652
+ return cliJs;
653
+ }
654
+ }
655
+ catch {
656
+ // 忽略
651
657
  }
652
- }
653
- catch {
654
- // 忽略
655
658
  }
656
659
  console.warn(`[ClaudeSdkAdapter] Claude Code executable not found for command: ${normalizedCommand}`);
657
660
  return undefined;
658
661
  }
662
+ /**
663
+ * 从面板配置的命令行中提取实际可执行文件,避免把参数一起传给 PATH 查找。
664
+ */
665
+ extractExecutableCommand(command) {
666
+ const input = (command || 'claude').trim() || 'claude';
667
+ const firstToken = input.match(/^\s*(?:"([^"]+)"|'([^']+)'|(\S+))/);
668
+ return firstToken?.[1] || firstToken?.[2] || firstToken?.[3] || 'claude';
669
+ }
670
+ /**
671
+ * 按当前 Bridge 进程的 PATH/PATHEXT 直接查找可执行文件,避免依赖 which/where 和 shell 解析。
672
+ */
673
+ resolveExecutableFromPath(command) {
674
+ const pathValue = process.env.PATH || '';
675
+ if (!pathValue) {
676
+ return undefined;
677
+ }
678
+ const commandHasExtension = path.extname(command).length > 0;
679
+ const extensions = process.platform === 'win32'
680
+ ? this.getWindowsPathExtensions(commandHasExtension)
681
+ : [''];
682
+ for (const directory of pathValue.split(path.delimiter)) {
683
+ if (!directory) {
684
+ continue;
685
+ }
686
+ for (const extension of extensions) {
687
+ const candidate = path.join(directory, `${command}${extension}`);
688
+ if (!fs.existsSync(candidate)) {
689
+ continue;
690
+ }
691
+ if (this.isWindowsWrapperPath(candidate)) {
692
+ const resolvedWrapper = this.resolveWindowsWrapper(candidate);
693
+ if (resolvedWrapper) {
694
+ return resolvedWrapper;
695
+ }
696
+ continue;
697
+ }
698
+ console.log(`[ClaudeSdkAdapter] Found Claude Code CLI via PATH: ${candidate}`);
699
+ return candidate;
700
+ }
701
+ }
702
+ return undefined;
703
+ }
704
+ getWindowsPathExtensions(commandHasExtension) {
705
+ if (commandHasExtension) {
706
+ return [''];
707
+ }
708
+ const pathExt = process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM;.PS1';
709
+ return pathExt
710
+ .split(';')
711
+ .map((extension) => extension.trim())
712
+ .filter((extension) => extension.length > 0);
713
+ }
714
+ shouldUseDefaultClaudeFallbacks(command) {
715
+ return ['claude', 'claude-code', 'claudecode'].includes(command.toLowerCase());
716
+ }
717
+ isWindowsWrapperPath(filePath) {
718
+ const extension = path.extname(filePath).toLowerCase();
719
+ return ['.cmd', '.bat', '.ps1'].includes(extension);
720
+ }
721
+ resolveWindowsWrapper(wrapperPath) {
722
+ if (!this.isWindowsWrapperPath(wrapperPath)) {
723
+ return undefined;
724
+ }
725
+ return this.resolveCliJsFromWrapper(wrapperPath);
726
+ }
659
727
  /**
660
728
  * 从 wrapper 脚本解析 cli.js 路径
661
729
  */
@@ -669,6 +737,11 @@ export class ClaudeSdkAdapter extends EventEmitter {
669
737
  return voltaShared;
670
738
  }
671
739
  }
740
+ const wrapperDirectory = path.dirname(wrapperPath);
741
+ const npmCliJs = path.join(wrapperDirectory, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
742
+ if (fs.existsSync(npmCliJs)) {
743
+ return npmCliJs;
744
+ }
672
745
  }
673
746
  catch {
674
747
  // 忽略
@@ -1063,7 +1136,7 @@ export class ClaudeSdkAdapter extends EventEmitter {
1063
1136
  toolInput = JSON.parse(jsonStr);
1064
1137
  console.log(`[ClaudeSdkAdapter] Parsed tool input:`, JSON.stringify(toolInput).slice(0, 200));
1065
1138
  }
1066
- catch (e) {
1139
+ catch {
1067
1140
  console.warn(`[ClaudeSdkAdapter] Failed to parse tool input JSON:`, jsonStr.slice(0, 100));
1068
1141
  }
1069
1142
  }
@@ -1,3 +1,6 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
1
4
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
5
  import { ClaudeSdkAdapter } from './ClaudeSdkAdapter.js';
3
6
  describe('ClaudeSdkAdapter', () => {
@@ -5,6 +8,149 @@ describe('ClaudeSdkAdapter', () => {
5
8
  vi.useRealTimers();
6
9
  vi.restoreAllMocks();
7
10
  });
11
+ it('resolves claude from PATH without shelling out to which or where', () => {
12
+ const adapter = new ClaudeSdkAdapter();
13
+ const originalPath = process.env.PATH;
14
+ const originalPathExt = process.env.PATHEXT;
15
+ const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-path-'));
16
+ const commandPath = path.join(commandDirectory, process.platform === 'win32' ? 'claude.EXE' : 'claude');
17
+ fs.writeFileSync(commandPath, '#!/usr/bin/env node\n');
18
+ if (process.platform !== 'win32') {
19
+ fs.chmodSync(commandPath, 0o755);
20
+ }
21
+ process.env.PATH = commandDirectory;
22
+ process.env.PATHEXT = '.EXE;.CMD';
23
+ try {
24
+ const result = adapter.findClaudeExecutable('claude');
25
+ expect(result).toBe(commandPath);
26
+ }
27
+ finally {
28
+ process.env.PATH = originalPath;
29
+ process.env.PATHEXT = originalPathExt;
30
+ fs.rmSync(commandDirectory, { recursive: true, force: true });
31
+ }
32
+ });
33
+ it('extracts the executable before resolving commands that include flags', () => {
34
+ const adapter = new ClaudeSdkAdapter();
35
+ const originalPath = process.env.PATH;
36
+ const originalPathExt = process.env.PATHEXT;
37
+ const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-flags-'));
38
+ const commandPath = path.join(commandDirectory, process.platform === 'win32' ? 'claude.EXE' : 'claude');
39
+ const unexpectedCommandWithFlags = path.join(commandDirectory, process.platform === 'win32' ? 'claude --model sonnet.EXE' : 'claude --model sonnet');
40
+ fs.writeFileSync(commandPath, '#!/usr/bin/env node\n');
41
+ if (process.platform !== 'win32') {
42
+ fs.chmodSync(commandPath, 0o755);
43
+ }
44
+ process.env.PATH = commandDirectory;
45
+ process.env.PATHEXT = '.EXE;.CMD';
46
+ try {
47
+ const result = adapter.findClaudeExecutable('claude --model sonnet');
48
+ expect(result).toBe(commandPath);
49
+ expect(fs.existsSync(unexpectedCommandWithFlags)).toBe(false);
50
+ }
51
+ finally {
52
+ process.env.PATH = originalPath;
53
+ process.env.PATHEXT = originalPathExt;
54
+ fs.rmSync(commandDirectory, { recursive: true, force: true });
55
+ }
56
+ });
57
+ it.skipIf(process.platform !== 'win32')('resolves uppercase Windows wrapper extensions before returning PATH candidate', () => {
58
+ const adapter = new ClaudeSdkAdapter();
59
+ const originalPath = process.env.PATH;
60
+ const originalPathExt = process.env.PATHEXT;
61
+ const originalLocalAppData = process.env.LOCALAPPDATA;
62
+ const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-wrapper-'));
63
+ const localAppDataDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-localappdata-'));
64
+ const wrapperPath = path.join(commandDirectory, 'claude.CMD');
65
+ const cliPath = path.join(localAppDataDirectory, 'Volta', 'tools', 'shared', '@anthropic-ai', 'claude-code', 'cli.js');
66
+ fs.mkdirSync(path.dirname(cliPath), { recursive: true });
67
+ fs.writeFileSync(wrapperPath, 'volta run %~n0 %*\n');
68
+ fs.writeFileSync(cliPath, '#!/usr/bin/env node\n');
69
+ process.env.PATH = commandDirectory;
70
+ process.env.PATHEXT = '.EXE;.CMD;.PS1';
71
+ process.env.LOCALAPPDATA = localAppDataDirectory;
72
+ try {
73
+ const result = adapter.findClaudeExecutable('claude');
74
+ expect(result).toBe(cliPath);
75
+ }
76
+ finally {
77
+ process.env.PATH = originalPath;
78
+ process.env.PATHEXT = originalPathExt;
79
+ process.env.LOCALAPPDATA = originalLocalAppData;
80
+ fs.rmSync(commandDirectory, { recursive: true, force: true });
81
+ fs.rmSync(localAppDataDirectory, { recursive: true, force: true });
82
+ }
83
+ });
84
+ it.skipIf(process.platform !== 'win32')('resolves npm command wrappers to the Claude Code cli.js file', () => {
85
+ const adapter = new ClaudeSdkAdapter();
86
+ const originalPath = process.env.PATH;
87
+ const originalPathExt = process.env.PATHEXT;
88
+ const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-npm-wrapper-'));
89
+ const wrapperPath = path.join(commandDirectory, process.platform === 'win32' ? 'claude.CMD' : 'claude.ps1');
90
+ const cliPath = path.join(commandDirectory, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
91
+ fs.mkdirSync(path.dirname(cliPath), { recursive: true });
92
+ fs.writeFileSync(wrapperPath, '@ECHO off\nnode "%~dp0\\node_modules\\@anthropic-ai\\claude-code\\cli.js" %*\n');
93
+ fs.writeFileSync(cliPath, '#!/usr/bin/env node\n');
94
+ process.env.PATH = commandDirectory;
95
+ process.env.PATHEXT = '.EXE;.CMD;.PS1';
96
+ try {
97
+ const result = adapter.findClaudeExecutable('claude');
98
+ expect(result).toBe(cliPath);
99
+ }
100
+ finally {
101
+ process.env.PATH = originalPath;
102
+ process.env.PATHEXT = originalPathExt;
103
+ fs.rmSync(commandDirectory, { recursive: true, force: true });
104
+ }
105
+ });
106
+ it('skips unresolved command wrappers instead of returning them to the SDK', () => {
107
+ const adapter = new ClaudeSdkAdapter();
108
+ const originalPath = process.env.PATH;
109
+ const originalPathExt = process.env.PATHEXT;
110
+ const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-unresolved-wrapper-'));
111
+ const wrapperPath = path.join(commandDirectory, 'claude.CMD');
112
+ fs.writeFileSync(wrapperPath, '@ECHO off\nnode missing-cli.js %*\n');
113
+ process.env.PATH = commandDirectory;
114
+ process.env.PATHEXT = '.EXE;.CMD;.PS1';
115
+ try {
116
+ const result = adapter.findClaudeExecutable('claude');
117
+ expect(result).not.toBe(wrapperPath);
118
+ }
119
+ finally {
120
+ process.env.PATH = originalPath;
121
+ process.env.PATHEXT = originalPathExt;
122
+ fs.rmSync(commandDirectory, { recursive: true, force: true });
123
+ }
124
+ });
125
+ it('resolves absolute npm command wrappers before passing them to the SDK', () => {
126
+ const adapter = new ClaudeSdkAdapter();
127
+ const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-absolute-wrapper-'));
128
+ const wrapperPath = path.join(commandDirectory, 'claude.cmd');
129
+ const cliPath = path.join(commandDirectory, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
130
+ fs.mkdirSync(path.dirname(cliPath), { recursive: true });
131
+ fs.writeFileSync(wrapperPath, '@ECHO off\nnode "%~dp0\\node_modules\\@anthropic-ai\\claude-code\\cli.js" %*\n');
132
+ fs.writeFileSync(cliPath, '#!/usr/bin/env node\n');
133
+ try {
134
+ const result = adapter.findClaudeExecutable(wrapperPath);
135
+ expect(result).toBe(cliPath);
136
+ }
137
+ finally {
138
+ fs.rmSync(commandDirectory, { recursive: true, force: true });
139
+ }
140
+ });
141
+ it('does not pass unresolved absolute wrapper scripts to the SDK', () => {
142
+ const adapter = new ClaudeSdkAdapter();
143
+ const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-absolute-unresolved-wrapper-'));
144
+ const wrapperPath = path.join(commandDirectory, 'claude.bat');
145
+ fs.writeFileSync(wrapperPath, '@ECHO off\nnode missing-cli.js %*\n');
146
+ try {
147
+ const result = adapter.findClaudeExecutable(wrapperPath);
148
+ expect(result).toBeUndefined();
149
+ }
150
+ finally {
151
+ fs.rmSync(commandDirectory, { recursive: true, force: true });
152
+ }
153
+ });
8
154
  it('does not auto-wake Claude for my_task-style idle commands', async () => {
9
155
  vi.useFakeTimers();
10
156
  const adapter = new ClaudeSdkAdapter();
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.8",
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",