@steerable/agent-shell 0.6.15

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.
Files changed (204) hide show
  1. package/LICENSE +91 -0
  2. package/contracts/tool-contract.json +326 -0
  3. package/dist/attachments.d.ts +41 -0
  4. package/dist/attachments.js +147 -0
  5. package/dist/brand.d.ts +24 -0
  6. package/dist/brand.js +92 -0
  7. package/dist/host/http-routes.d.ts +21 -0
  8. package/dist/host/http-routes.js +55 -0
  9. package/dist/host/ipc.d.ts +11 -0
  10. package/dist/host/ipc.js +20 -0
  11. package/dist/host/pack-assembly.d.ts +86 -0
  12. package/dist/host/pack-assembly.js +32 -0
  13. package/dist/host/runtime.d.ts +69 -0
  14. package/dist/host/runtime.js +207 -0
  15. package/dist/host/visible-terminal-exec.d.ts +21 -0
  16. package/dist/host/visible-terminal-exec.js +151 -0
  17. package/dist/hosted-web-search.d.ts +13 -0
  18. package/dist/hosted-web-search.js +82 -0
  19. package/dist/image-attachment.d.ts +39 -0
  20. package/dist/image-attachment.js +133 -0
  21. package/dist/insights/flush.d.ts +10 -0
  22. package/dist/insights/flush.js +139 -0
  23. package/dist/insights/record.d.ts +17 -0
  24. package/dist/insights/record.js +44 -0
  25. package/dist/json-store.d.ts +9 -0
  26. package/dist/json-store.js +22 -0
  27. package/dist/llm/index.d.ts +23 -0
  28. package/dist/llm/index.js +105 -0
  29. package/dist/llm/ollama.d.ts +23 -0
  30. package/dist/llm/ollama.js +242 -0
  31. package/dist/llm/openai-compat.d.ts +20 -0
  32. package/dist/llm/openai-compat.js +199 -0
  33. package/dist/llm/sidecar-provider.d.ts +37 -0
  34. package/dist/llm/sidecar-provider.js +163 -0
  35. package/dist/llm/tool-choice.d.ts +35 -0
  36. package/dist/llm/tool-choice.js +85 -0
  37. package/dist/llm/types.d.ts +122 -0
  38. package/dist/llm/types.js +1 -0
  39. package/dist/local-backend/agent-capability.d.ts +101 -0
  40. package/dist/local-backend/agent-capability.js +174 -0
  41. package/dist/local-backend/ai-title.d.ts +43 -0
  42. package/dist/local-backend/ai-title.js +173 -0
  43. package/dist/local-backend/auto-continue-helper.d.ts +80 -0
  44. package/dist/local-backend/auto-continue-helper.js +83 -0
  45. package/dist/local-backend/branch-helper.d.ts +24 -0
  46. package/dist/local-backend/branch-helper.js +27 -0
  47. package/dist/local-backend/context-compactor.d.ts +81 -0
  48. package/dist/local-backend/context-compactor.js +213 -0
  49. package/dist/local-backend/coreloop-stream.d.ts +245 -0
  50. package/dist/local-backend/coreloop-stream.js +277 -0
  51. package/dist/local-backend/deferred-detector.d.ts +15 -0
  52. package/dist/local-backend/deferred-detector.js +124 -0
  53. package/dist/local-backend/history-helper.d.ts +30 -0
  54. package/dist/local-backend/history-helper.js +34 -0
  55. package/dist/local-backend/interrupted-helper.d.ts +29 -0
  56. package/dist/local-backend/interrupted-helper.js +25 -0
  57. package/dist/local-backend/live-stream.d.ts +36 -0
  58. package/dist/local-backend/live-stream.js +23 -0
  59. package/dist/local-backend/llm-diagnose.d.ts +37 -0
  60. package/dist/local-backend/llm-diagnose.js +284 -0
  61. package/dist/local-backend/message-triggers.d.ts +27 -0
  62. package/dist/local-backend/message-triggers.js +67 -0
  63. package/dist/local-backend/pack-backend-routes.d.ts +31 -0
  64. package/dist/local-backend/pack-backend-routes.js +62 -0
  65. package/dist/local-backend/pack-turn-hooks.d.ts +37 -0
  66. package/dist/local-backend/pack-turn-hooks.js +67 -0
  67. package/dist/local-backend/prompt-builder.d.ts +101 -0
  68. package/dist/local-backend/prompt-builder.js +246 -0
  69. package/dist/local-backend/regenerate-helper.d.ts +62 -0
  70. package/dist/local-backend/regenerate-helper.js +75 -0
  71. package/dist/local-backend/router.d.ts +175 -0
  72. package/dist/local-backend/router.js +3139 -0
  73. package/dist/local-backend/skill-install.d.ts +19 -0
  74. package/dist/local-backend/skill-install.js +71 -0
  75. package/dist/local-backend/skill-loader.d.ts +92 -0
  76. package/dist/local-backend/skill-loader.js +146 -0
  77. package/dist/local-backend/skills/00-identity/SKILL.md +32 -0
  78. package/dist/local-backend/skills/10-goal/SKILL.md +59 -0
  79. package/dist/local-backend/skills/11-loop/SKILL.md +71 -0
  80. package/dist/local-backend/skills/12-create-skill/SKILL.md +88 -0
  81. package/dist/local-backend/skills/70-plan-mode/SKILL.md +58 -0
  82. package/dist/local-backend/skills/80-tool-usage/SKILL.md +70 -0
  83. package/dist/local-backend/skills/81-anti-deferred/SKILL.md +53 -0
  84. package/dist/local-backend/skills/82-data-grounding/SKILL.md +56 -0
  85. package/dist/local-backend/skills/85-local-exec/SKILL.md +86 -0
  86. package/dist/local-backend/skills/86-proactive-coding/SKILL.md +51 -0
  87. package/dist/local-backend/subagent-profiles.d.ts +30 -0
  88. package/dist/local-backend/subagent-profiles.js +74 -0
  89. package/dist/local-backend/task-process.d.ts +12 -0
  90. package/dist/local-backend/task-process.js +176 -0
  91. package/dist/local-backend/task-service.d.ts +135 -0
  92. package/dist/local-backend/task-service.js +565 -0
  93. package/dist/local-backend/turn-duration.d.ts +2 -0
  94. package/dist/local-backend/turn-duration.js +9 -0
  95. package/dist/local-backend/turn-timeline.d.ts +16 -0
  96. package/dist/local-backend/turn-timeline.js +42 -0
  97. package/dist/local-backend/worktree-service.d.ts +84 -0
  98. package/dist/local-backend/worktree-service.js +243 -0
  99. package/dist/local-edit.d.ts +48 -0
  100. package/dist/local-edit.js +44 -0
  101. package/dist/local-executor.d.ts +255 -0
  102. package/dist/local-executor.js +881 -0
  103. package/dist/local-script-registry.d.ts +28 -0
  104. package/dist/local-script-registry.js +63 -0
  105. package/dist/log.d.ts +13 -0
  106. package/dist/log.js +12 -0
  107. package/dist/main.d.ts +1 -0
  108. package/dist/main.js +855 -0
  109. package/dist/mcp-executor.d.ts +45 -0
  110. package/dist/mcp-executor.js +241 -0
  111. package/dist/mcp-server-registry.d.ts +104 -0
  112. package/dist/mcp-server-registry.js +234 -0
  113. package/dist/preload-default.d.ts +1 -0
  114. package/dist/preload-default.js +9 -0
  115. package/dist/preload.cjs +395 -0
  116. package/dist/preload.d.ts +20 -0
  117. package/dist/preload.js +411 -0
  118. package/dist/product-config.d.ts +43 -0
  119. package/dist/product-config.js +26 -0
  120. package/dist/project-registry.d.ts +55 -0
  121. package/dist/project-registry.js +106 -0
  122. package/dist/project-rules.d.ts +15 -0
  123. package/dist/project-rules.js +102 -0
  124. package/dist/runtime.d.ts +62 -0
  125. package/dist/runtime.js +217 -0
  126. package/dist/scenario/pack.d.ts +8 -0
  127. package/dist/scenario/pack.js +1 -0
  128. package/dist/scenario/registry.d.ts +24 -0
  129. package/dist/scenario/registry.js +31 -0
  130. package/dist/server/http-server.d.ts +39 -0
  131. package/dist/server/http-server.js +361 -0
  132. package/dist/server/index.d.ts +1 -0
  133. package/dist/server/index.js +107 -0
  134. package/dist/server/sse-bus.d.ts +14 -0
  135. package/dist/server/sse-bus.js +31 -0
  136. package/dist/shell-adapt.d.ts +21 -0
  137. package/dist/shell-adapt.js +104 -0
  138. package/dist/sidecar/boot.d.ts +36 -0
  139. package/dist/sidecar/boot.js +343 -0
  140. package/dist/sidecar/egress-hint.d.ts +15 -0
  141. package/dist/sidecar/egress-hint.js +46 -0
  142. package/dist/sidecar/egress-proxy.d.ts +183 -0
  143. package/dist/sidecar/egress-proxy.js +419 -0
  144. package/dist/sidecar/errors.d.ts +22 -0
  145. package/dist/sidecar/errors.js +38 -0
  146. package/dist/sidecar/exec-sandbox.d.ts +48 -0
  147. package/dist/sidecar/exec-sandbox.js +94 -0
  148. package/dist/sidecar/handle.d.ts +32 -0
  149. package/dist/sidecar/handle.js +53 -0
  150. package/dist/sidecar/index.d.ts +14 -0
  151. package/dist/sidecar/index.js +13 -0
  152. package/dist/sidecar/proxy-detect.d.ts +50 -0
  153. package/dist/sidecar/proxy-detect.js +182 -0
  154. package/dist/sidecar/reverse-approval.d.ts +55 -0
  155. package/dist/sidecar/reverse-approval.js +86 -0
  156. package/dist/sidecar/reverse-ask-user.d.ts +34 -0
  157. package/dist/sidecar/reverse-ask-user.js +59 -0
  158. package/dist/sidecar/reverse-spawn.d.ts +19 -0
  159. package/dist/sidecar/reverse-spawn.js +161 -0
  160. package/dist/sidecar/reverse-tools.d.ts +29 -0
  161. package/dist/sidecar/reverse-tools.js +106 -0
  162. package/dist/sidecar/safety-patterns.d.ts +41 -0
  163. package/dist/sidecar/safety-patterns.js +157 -0
  164. package/dist/sidecar/storage-path.d.ts +14 -0
  165. package/dist/sidecar/storage-path.js +35 -0
  166. package/dist/sidecar/supervisor.d.ts +218 -0
  167. package/dist/sidecar/supervisor.js +932 -0
  168. package/dist/sidecar/types.d.ts +601 -0
  169. package/dist/sidecar/types.js +1 -0
  170. package/dist/single-instance.d.ts +11 -0
  171. package/dist/single-instance.js +21 -0
  172. package/dist/storage/empty-chats.d.ts +9 -0
  173. package/dist/storage/empty-chats.js +16 -0
  174. package/dist/storage/index.d.ts +373 -0
  175. package/dist/storage/index.js +1158 -0
  176. package/dist/storage/insights-redact.d.ts +2 -0
  177. package/dist/storage/insights-redact.js +30 -0
  178. package/dist/storage/insights-settings.d.ts +53 -0
  179. package/dist/storage/insights-settings.js +92 -0
  180. package/dist/storage/llm-settings.d.ts +120 -0
  181. package/dist/storage/llm-settings.js +233 -0
  182. package/dist/storage/local-store-singleton.d.ts +28 -0
  183. package/dist/storage/local-store-singleton.js +38 -0
  184. package/dist/storage/message-order.d.ts +25 -0
  185. package/dist/storage/message-order.js +27 -0
  186. package/dist/storage/pack-migrations.d.ts +22 -0
  187. package/dist/storage/pack-migrations.js +24 -0
  188. package/dist/storage/pack-seeds.d.ts +36 -0
  189. package/dist/storage/pack-seeds.js +42 -0
  190. package/dist/storage/telemetry-settings.d.ts +38 -0
  191. package/dist/storage/telemetry-settings.js +59 -0
  192. package/dist/storage/usage-summary.d.ts +55 -0
  193. package/dist/storage/usage-summary.js +38 -0
  194. package/dist/storage/web-search-settings.d.ts +38 -0
  195. package/dist/storage/web-search-settings.js +74 -0
  196. package/dist/storage/write-lease.d.ts +26 -0
  197. package/dist/storage/write-lease.js +74 -0
  198. package/dist/terminal-manager.d.ts +83 -0
  199. package/dist/terminal-manager.js +506 -0
  200. package/dist/tool-router.d.ts +228 -0
  201. package/dist/tool-router.js +930 -0
  202. package/dist/tool-search-rank.d.ts +42 -0
  203. package/dist/tool-search-rank.js +96 -0
  204. package/package.json +67 -0
@@ -0,0 +1,36 @@
1
+ import { type ToolRouter } from '../tool-router.js';
2
+ import type { createApprovalBridge } from './reverse-approval.js';
3
+ import type { SidecarReverseHandler } from './types.js';
4
+ export interface HostSidecarDeps {
5
+ toolRouter: ToolRouter;
6
+ /** W4-2 项目模式围栏:chatId → 项目根(无项目对话返回 null)。 */
7
+ resolveProjectRoot: (chatId: string) => string | null;
8
+ /**
9
+ * 项目模式之外额外放行的只读根(会话附件目录等)。文件写入仍只受
10
+ * projectRoot 围栏约束;这些根只放宽 local_read_file 的读取范围。
11
+ */
12
+ resolveAdditionalReadRoots?: (chatId: string) => string[];
13
+ /** W4-1 审批反向通道处理器(宿主审批弹窗的应答入口)。 */
14
+ approvalHandler: ReturnType<typeof createApprovalBridge>['handler'];
15
+ /**
16
+ * W8:sidecar ask_user 工具的 `ask_user.request` 反向调用处理器
17
+ * (renderer 问题卡片应答)。缺省时 ask_user 对宿主 fail-open 为空答。
18
+ */
19
+ askUserHandler?: SidecarReverseHandler;
20
+ /**
21
+ * P2b:sidecar 会话恢复时把记录里的 read-before-write 证据经
22
+ * `read_state.seed` 推给宿主(桌面 LocalExecutor 的 readFileState)。
23
+ * 缺省时宿主无读证据,自动 CAS 退化为仅依赖显式 expectedVersion。
24
+ */
25
+ readStateSeedHandler?: SidecarReverseHandler;
26
+ onLogLine?: (line: string) => void;
27
+ }
28
+ /**
29
+ * Default-on (2026-08-26): the sidecar hosts the CoreLoop, which is the
30
+ * default chat path. Explicit STEERABLE_USE_SIDECAR=0 opts out (in-process
31
+ * TS loop). Boot is registered synchronously so early RPC thin clients
32
+ * (skill-loader et al.) can await it via whenSidecarSupervisor().
33
+ */
34
+ export declare function startHostSidecar(deps: HostSidecarDeps): Promise<void>;
35
+ /** 宿主退出钩子:停 supervisor + egress proxy。幂等。 */
36
+ export declare function shutdownHostSidecar(): Promise<void>;
@@ -0,0 +1,343 @@
1
+ /**
2
+ * sidecar 启动装配(W2/W4/W5 各反向通道 + egress 加固),Electron 主进程
3
+ * (main.ts)与 BS server(server/index.ts)共用一份,避免两条宿主路径
4
+ * 的启动行为漂移。
5
+ *
6
+ * 模块级持有 supervisor / egress proxy 引用,配对的 `shutdownHostSidecar`
7
+ * 由宿主的退出钩子调用(Electron before-quit / BS SIGTERM)。
8
+ */
9
+ import path from 'node:path';
10
+ import log from 'electron-log';
11
+ import { SidecarSupervisor, resolveSidecarPython } from './supervisor.js';
12
+ import { setSidecarSupervisor, setSidecarSupervisorPending, llmService } from '../llm/index.js';
13
+ import { localStore } from '../storage/index.js';
14
+ import { resolveSidecarStoragePath } from './storage-path.js';
15
+ import { deriveEgressAllowListFromBaseUrl, probeExecSandboxCapability, } from './exec-sandbox.js';
16
+ import { createToolInvokeHandler } from './reverse-tools.js';
17
+ import { createProcessSpawnHandler } from './reverse-spawn.js';
18
+ import { collectAmbientProxyEndpoints } from './proxy-detect.js';
19
+ import { buildEgressProxyPlan, decideEgressProxy, deriveWebEgressHosts, pickFreePort, recordEgressPosture, startEgressProxy, } from './egress-proxy.js';
20
+ import { WEB_TOOL_RPC_TIMEOUT_MS, webToolsEnabled } from '../tool-router.js';
21
+ import { sidecarWebSearchEnv } from '../storage/web-search-settings.js';
22
+ import { executeHostedWebSearch } from '../hosted-web-search.js';
23
+ let egressProxyRef = null;
24
+ let supervisorRef = null;
25
+ async function deriveSidecarEgressAllowList() {
26
+ const envList = (process.env.STEERABLE_SIDECAR_SANDBOX_ALLOWED_HOSTS ?? '')
27
+ .split(',')
28
+ .map((h) => h.trim())
29
+ .filter(Boolean);
30
+ if (envList.length > 0)
31
+ return envList;
32
+ const derived = deriveEgressAllowListFromBaseUrl(llmService.getSettings().baseUrl);
33
+ // W4-8: the sidecar's httpx stack honors ambient proxies (env vars, and
34
+ // the macOS system proxy via getproxies). A configured proxy is an
35
+ // effective egress point — without it in the allow-list, the sandbox
36
+ // denies every LLM call for proxy users (found by dogfooding 2026-08-29:
37
+ // system proxy 127.0.0.1:7890 hijacked even 127.0.0.1:11434 because
38
+ // getproxies() does not surface the system bypass list).
39
+ const proxies = await collectAmbientProxyEndpoints();
40
+ const merged = [...new Set([...(derived ?? []), ...proxies])];
41
+ if (merged.length === 0) {
42
+ log.warn('[sidecar] cannot derive egress allow-list from provider baseUrl; outbound stays open');
43
+ return undefined;
44
+ }
45
+ if (proxies.length > 0) {
46
+ log.info(`[sidecar] egress allow-list includes ambient proxy: ${proxies.join(', ')}`);
47
+ }
48
+ return merged;
49
+ }
50
+ /**
51
+ * W1.3.3(3.1a 起默认开):把 sidecar 出网收敛到本机
52
+ * steerable_egress_proxy。显式 `STEERABLE_EGRESS_PROXY=0` 退出。
53
+ *
54
+ * 两类自动回退到旧的 Seatbelt 端口级派生路径(加固永远不能弄断 LLM
55
+ * 通路):
56
+ * - 检测到 ambient/系统代理:代理解析 CONNECT 后直连目标、无上游链,
57
+ * 系统代理劫持类网络开了反而断网(deriveSidecarEgressAllowList 会把
58
+ * ambient 端点并进允许列表,见 W4-8)。
59
+ * - 启动失败 / 派生不出任何代理条目。
60
+ */
61
+ async function startEgressProxyIfEnabled() {
62
+ try {
63
+ const ambient = process.env.STEERABLE_EGRESS_PROXY === '0' ? [] : await collectAmbientProxyEndpoints();
64
+ const decision = decideEgressProxy({ env: process.env, ambientProxies: ambient });
65
+ if (!decision.start) {
66
+ if (ambient.length > 0) {
67
+ log.warn(`[egress-proxy] ambient proxy detected (${ambient.join(', ')}); ` +
68
+ 'the per-host proxy dials targets directly and cannot chain upstream — ' +
69
+ 'staying on port-level enforcement');
70
+ }
71
+ recordEgressPosture(decision.posture);
72
+ return null;
73
+ }
74
+ const settings = llmService.getSettings();
75
+ const searchSettings = localStore.getWebSearchSettings();
76
+ const searchEnv = sidecarWebSearchEnv({
77
+ processEnv: process.env,
78
+ storedApiKey: searchSettings?.apiKey,
79
+ storedProvider: searchSettings?.provider,
80
+ llmBaseUrl: settings.baseUrl,
81
+ });
82
+ const plan = buildEgressProxyPlan({
83
+ pythonExecutable: resolveSidecarPython(),
84
+ port: await pickFreePort(),
85
+ providerBaseUrl: settings.baseUrl,
86
+ providerApiKey: settings.apiKey || undefined,
87
+ // 3.1b/3.1d:web 工具的允许域名与 sidecar 应用层同源,并入代理
88
+ // CONNECT 白名单——web_fetch/web_search 经 HTTPS_PROXY 走代理,
89
+ // 不再是 confined 下的互斥禁用。
90
+ webAllowedHosts: deriveWebEgressHosts({
91
+ webTools: webToolsEnabled(),
92
+ searchEnv,
93
+ env: process.env,
94
+ }),
95
+ });
96
+ if (!plan) {
97
+ log.warn('[egress-proxy] no https endpoint derivable from provider baseUrl; staying on port-level enforcement');
98
+ recordEgressPosture({
99
+ mode: 'port-only-fallback',
100
+ reason: '无法从 provider baseUrl 派生出 https 端点,按主机管控未启用',
101
+ });
102
+ return null;
103
+ }
104
+ egressProxyRef = await startEgressProxy(plan, (line) => log.info('[egress-proxy]', line));
105
+ log.info(`[egress-proxy] per-host egress active via ${plan.proxyEndpoint} (allow: ${plan.proxiedHosts.join(', ')}${plan.broker ? `; credential broker for ${plan.broker.host}` : ''})`);
106
+ recordEgressPosture({ mode: 'per-host-proxy', reason: null });
107
+ // 出网拓宽 ask 链路:控制端点端口 + token 注入 sidecar 环境。sidecar
108
+ // 进程在沙箱外;其沙箱子进程的环境经白名单过滤(run_code 的
109
+ // _child_environ),拿不到 token,无法自我拓宽。
110
+ const controlEnv = plan.control && egressProxyRef.controlPort
111
+ ? {
112
+ STEERABLE_EGRESS_CONTROL_PORT: String(egressProxyRef.controlPort),
113
+ [plan.control.tokenEnv]: plan.control.tokenValue,
114
+ }
115
+ : {};
116
+ return {
117
+ sandboxAllowedHosts: plan.sandboxAllowedHosts,
118
+ // 小写 https_proxy 是 httpx/getproxies 的惯例键;两个都写以兼容
119
+ // 只认大写的栈。broker 模式下 provider baseUrl 被改写成 http,
120
+ // 所以 HTTP_PROXY 也要指到代理(凭证注入在代理侧完成)。
121
+ // STEERABLE_EGRESS_CONFINED=1 标记"出网已收敛到代理"(W5-2)。只在
122
+ // 这条成功路径上设置——启动失败的回退分支不写,sidecar 不会误以为
123
+ // 自己被收敛。
124
+ env: plan.broker
125
+ ? {
126
+ HTTPS_PROXY: plan.proxyUrl,
127
+ https_proxy: plan.proxyUrl,
128
+ HTTP_PROXY: plan.proxyUrl,
129
+ http_proxy: plan.proxyUrl,
130
+ STEERABLE_EGRESS_CONFINED: '1',
131
+ ...controlEnv,
132
+ }
133
+ : {
134
+ HTTPS_PROXY: plan.proxyUrl,
135
+ https_proxy: plan.proxyUrl,
136
+ STEERABLE_EGRESS_CONFINED: '1',
137
+ ...controlEnv,
138
+ },
139
+ };
140
+ }
141
+ catch (err) {
142
+ log.warn('[egress-proxy] start failed; falling back to port-level enforcement', err);
143
+ egressProxyRef = null;
144
+ recordEgressPosture({
145
+ mode: 'port-only-fallback',
146
+ reason: `按主机代理启动失败(${err instanceof Error ? err.message : String(err)}),已退回端口级`,
147
+ });
148
+ return null;
149
+ }
150
+ }
151
+ /**
152
+ * Default-on (2026-08-26): the sidecar hosts the CoreLoop, which is the
153
+ * default chat path. Explicit STEERABLE_USE_SIDECAR=0 opts out (in-process
154
+ * TS loop). Boot is registered synchronously so early RPC thin clients
155
+ * (skill-loader et al.) can await it via whenSidecarSupervisor().
156
+ */
157
+ export async function startHostSidecar(deps) {
158
+ if (process.env.STEERABLE_USE_SIDECAR === '0')
159
+ return;
160
+ // ready 后的完整接线:注册全局 handle + reverse channels + web 工具握手。
161
+ // 正常 boot 路径与「boot 失败后后台 restart 迟到就绪」路径共用。
162
+ const wireSupervisor = async (supervisor) => {
163
+ supervisorRef = supervisor;
164
+ setSidecarSupervisor(supervisor);
165
+ // A4 reverse channel: the sidecar-hosted CoreLoop asks this process to
166
+ // execute tools (shell / files / MCP live here, not in Python).
167
+ supervisor.onReverseRequest('tool.invoke', createToolInvokeHandler({
168
+ toolRouter: deps.toolRouter,
169
+ onBlocked: (info) => log.warn('[sidecar tool] blocked critical shell command', info),
170
+ // W4-2: project-mode fence on the CoreLoop path — the sidecar's
171
+ // tool.invoke carries context.chatId; resolve it to the project
172
+ // root so ToolRouter's cwd/path confinement applies.
173
+ resolveProjectRoot: deps.resolveProjectRoot,
174
+ resolveAdditionalReadRoots: deps.resolveAdditionalReadRoots,
175
+ }));
176
+ // W4-1: approval algebra reverse channel — the sidecar's
177
+ // ApprovalExecutor(HostApprover) asks here; the renderer modal answers
178
+ // via the host's decide endpoint (IPC in Electron, HTTP in BS).
179
+ supervisor.onReverseRequest('approval.request', deps.approvalHandler);
180
+ // W8: structured user questions — the sidecar-hosted ask_user tool asks
181
+ // here; the renderer question card answers via the host's answer
182
+ // endpoint (IPC in Electron, HTTP in BS).
183
+ if (deps.askUserHandler) {
184
+ supervisor.onReverseRequest('ask_user.request', deps.askUserHandler);
185
+ }
186
+ // P2b: on resume the sidecar rebuilds read-before-write evidence from
187
+ // the durable record and pushes it here so the host's readFileState
188
+ // survives process restarts (CC seed_read_state parity).
189
+ if (deps.readStateSeedHandler) {
190
+ supervisor.onReverseRequest('read_state.seed', deps.readStateSeedHandler);
191
+ }
192
+ // W4.1.1: confined-spawn reverse channel (Windows). The sidecar routes
193
+ // here only when no local rewriter backend exists; the handler confines
194
+ // via win-spawn-helper (restricted token + Job Object) and fails closed
195
+ // on other platforms or when the helper is missing.
196
+ supervisor.onReverseRequest('host.process.spawn', createProcessSpawnHandler());
197
+ // Layer-3 态势:问出这台机器的逐 exec 后端真能达到哪一档强制,
198
+ // `buildExecSandbox` 据此决定要不要 requireFull(见 exec-sandbox.ts)。
199
+ await probeExecSandboxCapability(supervisor);
200
+ // W5-2: web_search/web_fetch handshake. The single implementation lives
201
+ // in the sidecar (web_tools.py); the host router carries schemas only
202
+ // and forwards over `tool.invoke`. tool.list is the availability source
203
+ // of truth — web_search registers only when a search backend is
204
+ // configured, so an unconfigured deployment never advertises a broken
205
+ // tool. Handshake failure degrades to "web tools absent", not a boot
206
+ // failure.
207
+ try {
208
+ const advertised = webToolsEnabled() ? await supervisor.listToolNames() : [];
209
+ const webNames = advertised.filter((n) => n === 'web_fetch' || n === 'web_search');
210
+ deps.toolRouter.setWebTools((name, args) => supervisor.invokeTool(name, args, {
211
+ // Approval already happened in the sidecar's CoreLoop
212
+ // (ApprovalExecutor → approval.request reverse channel) before
213
+ // the call was delegated here.
214
+ consentGranted: true,
215
+ timeoutMs: WEB_TOOL_RPC_TIMEOUT_MS,
216
+ }), webNames);
217
+ const searchSettings = localStore.getWebSearchSettings();
218
+ const searchEnv = sidecarWebSearchEnv({
219
+ processEnv: process.env,
220
+ storedApiKey: searchSettings?.apiKey,
221
+ storedProvider: searchSettings?.provider,
222
+ llmBaseUrl: llmService.getSettings().baseUrl,
223
+ });
224
+ if (webNames.includes('web_search')
225
+ && searchEnv.STEERABLE_WEB_SEARCH_PROVIDER === 'host'
226
+ && !searchEnv.STEERABLE_WEB_SEARCH_API_KEY) {
227
+ deps.toolRouter.setHostedWebSearch(async (args) => {
228
+ const query = typeof args.query === 'string' ? args.query : '';
229
+ const max = typeof args.max_results === 'number' ? args.max_results : 8;
230
+ return executeHostedWebSearch(query, max, llmService.getSettings());
231
+ });
232
+ }
233
+ else {
234
+ deps.toolRouter.setHostedWebSearch(null);
235
+ }
236
+ log.info(`[sidecar] web tools available: ${webNames.join(', ') || '(none)'}`);
237
+ }
238
+ catch (err) {
239
+ log.warn('[sidecar] web tool handshake failed; web tools unavailable this session', err);
240
+ }
241
+ // 插件生命周期:sidecar 的 plugin.* RPC 由 PluginRegistry 背书(__main__
242
+ // 缺省接线)。模型面是 tool-router 的 plugin_* 工具(deferred 层,经
243
+ // tool_search 发现);这里注入直调缝。握手失败降级为工具缺席,不阻塞 boot。
244
+ try {
245
+ await supervisor.call('plugin.list');
246
+ deps.toolRouter.setPluginRpc((method, params) => supervisor.call(method, params));
247
+ log.info('[sidecar] plugin registry wired; plugin_* tools available');
248
+ }
249
+ catch (err) {
250
+ deps.toolRouter.setPluginRpc(null);
251
+ log.warn('[sidecar] plugin registry unavailable; plugin_* tools hidden this session', err);
252
+ }
253
+ log.info('[sidecar] ready', supervisor.getBootSnapshot());
254
+ };
255
+ const boot = (async () => {
256
+ try {
257
+ const egressProxy = await startEgressProxyIfEnabled();
258
+ // W2.6.1: durable sessions/traces/history in a zero-dependency
259
+ // sqlite database under ~/.steerable — the one root the Seatbelt
260
+ // profile already allows writes to; userData would be denied.
261
+ // Path is per-flavor / env-overridable — see resolveSidecarStoragePath.
262
+ const storagePath = resolveSidecarStoragePath();
263
+ const supervisor = await SidecarSupervisor.start({
264
+ args: [
265
+ '--storage-path',
266
+ storagePath,
267
+ ],
268
+ // 当 storage path 落在 ~/.steerable 之外(BS 模式 DEEPPATH_USER_DATA_DIR、
269
+ // 测试 tmpdir、显式 STEERABLE_SIDECAR_STORAGE_PATH)时,沙箱必须显式
270
+ // 授予该目录写权限,否则 sidecar 打不开 sessions.db/sessions.lock。
271
+ sandboxWritableRoots: [path.dirname(storagePath)],
272
+ // W4-3: sandbox default-on. This list is fixed at spawn (the sandbox
273
+ // profile cannot be edited afterwards), so a provider change reaches it
274
+ // only on restart — plain-http endpoints therefore still need one. The
275
+ // proxy's own CONNECT list does track the setting at runtime, via
276
+ // allowEgressForBaseUrl.
277
+ sandboxAllowedHosts: egressProxy?.sandboxAllowedHosts ?? (await deriveSidecarEgressAllowList()),
278
+ // egress-proxy 模式下不开 web-egress(*:80/443 会架空代理的按主机
279
+ // 名单):web 工具改走代理(HTTPS_PROXY),代理白名单与 sidecar
280
+ // 应用层域名名单同源(3.1b/3.1d)。
281
+ sandboxWebEgress: webToolsEnabled() && !egressProxy,
282
+ // 3.1b:代理模式下 web_fetch 的 SSRF 预检仍在沙箱内做 DNS(抓取
283
+ // 本身走代理隧道),所以只放行解析器 socket——无 IP 可达性。
284
+ sandboxAllowResolver: webToolsEnabled() && Boolean(egressProxy),
285
+ env: {
286
+ ...egressProxy?.env,
287
+ ...sidecarWebSearchEnv({
288
+ processEnv: process.env,
289
+ storedApiKey: localStore.getWebSearchSettings()?.apiKey,
290
+ storedProvider: localStore.getWebSearchSettings()?.provider,
291
+ llmBaseUrl: llmService.getSettings().baseUrl,
292
+ }),
293
+ // P1: offer the sidecar's run_code (programmatic tool calls) to the
294
+ // model. The sidecar registers + advertises it; the desktop's
295
+ // tool-router forwards the call back over the reverse channel.
296
+ STEERABLE_RUN_CODE: '1',
297
+ // P3: conversational JS PTC (run_js/wait_js). The sidecar spawns a
298
+ // long-lived Node worker; in the desktop that worker is the bundled
299
+ // runtime — process.execPath is the Electron binary, which runs as
300
+ // plain Node under ELECTRON_RUN_AS_NODE (and already *is* plain
301
+ // Node in headless/BS mode, where the variable is ignored). The
302
+ // sidecar's env scrub passes both through to the worker child.
303
+ STEERABLE_PTC_JS: '1',
304
+ STEERABLE_PTC_NODE: process.execPath,
305
+ ELECTRON_RUN_AS_NODE: '1',
306
+ },
307
+ onLogLine: (line) => (deps.onLogLine ?? ((l) => log.info('[sidecar]', l)))(line),
308
+ });
309
+ await wireSupervisor(supervisor);
310
+ return supervisor;
311
+ }
312
+ catch (err) {
313
+ log.error('[sidecar] failed to start, falling back to in-process providers', err);
314
+ setSidecarSupervisor(null);
315
+ // boot 失败后 supervisor 的 exit→restart 循环仍在后台重试(另一个
316
+ // 宿主暂时持有 sessions.lock 这类瞬态冲突会自愈)。迟到就绪时补
317
+ // 接线,否则 sidecar 进程活着但 router 永远 503。
318
+ const late = err.supervisor;
319
+ if (late) {
320
+ late.once('ready', () => {
321
+ log.info('[sidecar] late ready after initial boot failure');
322
+ void wireSupervisor(late);
323
+ });
324
+ }
325
+ return null;
326
+ }
327
+ })();
328
+ setSidecarSupervisorPending(boot);
329
+ await boot;
330
+ }
331
+ /** 宿主退出钩子:停 supervisor + egress proxy。幂等。 */
332
+ export async function shutdownHostSidecar() {
333
+ if (supervisorRef) {
334
+ const supervisor = supervisorRef;
335
+ supervisorRef = null;
336
+ setSidecarSupervisor(null);
337
+ await supervisor.shutdown();
338
+ }
339
+ if (egressProxyRef) {
340
+ egressProxyRef.stop();
341
+ egressProxyRef = null;
342
+ }
343
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * W-egress-hint: translate cryptic egress failures into user-facing hints.
3
+ *
4
+ * The sidecar's LLM clients (httpx, `trust_env=True`) honor ambient proxy
5
+ * configuration. When the sandbox or egress proxy denies a connection, the
6
+ * raw error is often a generic "All connection attempts failed" or a 403
7
+ * from the proxy — useless to the user. This module maps known failure
8
+ * patterns to actionable Chinese hints.
9
+ */
10
+ /**
11
+ * Translate a raw egress/LLM error into a user-facing hint. Returns null
12
+ * when the error does not match a known egress failure pattern (caller
13
+ * should fall back to the raw message).
14
+ */
15
+ export declare function egressFailureHint(raw: string): string | null;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * W-egress-hint: translate cryptic egress failures into user-facing hints.
3
+ *
4
+ * The sidecar's LLM clients (httpx, `trust_env=True`) honor ambient proxy
5
+ * configuration. When the sandbox or egress proxy denies a connection, the
6
+ * raw error is often a generic "All connection attempts failed" or a 403
7
+ * from the proxy — useless to the user. This module maps known failure
8
+ * patterns to actionable Chinese hints.
9
+ */
10
+ /**
11
+ * Translate a raw egress/LLM error into a user-facing hint. Returns null
12
+ * when the error does not match a known egress failure pattern (caller
13
+ * should fall back to the raw message).
14
+ */
15
+ export function egressFailureHint(raw) {
16
+ const text = raw.toLowerCase();
17
+ // httpx "All connection attempts failed" — the sandbox denied the
18
+ // connection (EPERM) or the proxy refused it. Most common cause: the
19
+ // provider endpoint is not in the allow-list.
20
+ if (text.includes('all connection attempts failed')) {
21
+ return '无法连接模型服务:沙箱或代理拒绝了连接。请检查 baseUrl 是否已加入允许列表,或尝试关闭系统代理后重试。';
22
+ }
23
+ // 403 from the egress proxy — the host is not in the per-host allow-list.
24
+ if (text.includes('403') && (text.includes('forbidden') || text.includes('denied'))) {
25
+ return '模型服务被代理拒绝(403)。请确认 baseUrl 已加入允许列表,或联系管理员放行该域名。';
26
+ }
27
+ // 502 Bad Gateway — the proxy itself failed to reach the upstream.
28
+ if (text.includes('502') && text.includes('bad gateway')) {
29
+ return '代理无法连接到模型服务(502)。请检查网络连接或代理配置,或尝试直连(关闭代理)后重试。';
30
+ }
31
+ // 504 Gateway Timeout — upstream timed out through the proxy.
32
+ if (text.includes('504') && text.includes('gateway timeout')) {
33
+ return '代理连接模型服务超时(504)。请检查网络连接或代理配置,或尝试直连(关闭代理)后重试。';
34
+ }
35
+ // ECONNREFUSED / ENOTFOUND / ETIMEDOUT — direct connection failures.
36
+ if (text.includes('econnrefused')) {
37
+ return '无法连接模型服务:连接被拒绝。请检查 baseUrl 是否正确,或确认服务已启动。';
38
+ }
39
+ if (text.includes('enotfound')) {
40
+ return '无法连接模型服务:域名解析失败。请检查 baseUrl 域名是否正确,或检查 DNS 配置。';
41
+ }
42
+ if (text.includes('etimedout')) {
43
+ return '无法连接模型服务:连接超时。请检查网络连接,或确认防火墙/代理未拦截该域名。';
44
+ }
45
+ return null;
46
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * W1.3.3:桌面宿主内嵌 `steerable_egress_proxy` 的启动器。
3
+ *
4
+ * 默认态势(3.1a,2026-09-08 起默认开):sidecar 出网收敛到本机代理,
5
+ * Seatbelt 列表只放行代理端口,真正的主机名单由代理进程持有(代理跑在
6
+ * 沙箱外)。显式 `STEERABLE_EGRESS_PROXY=0` 退出,回退到 Seatbelt 允许
7
+ * 列表从 provider baseUrl 派生、主机名退化为端口级的旧态势(sbpl 限制,
8
+ * 见 docs/spec/safety.md)。
9
+ *
10
+ * 自动回退:检测到 ambient/系统代理(collectAmbientProxyEndpoints 非空)
11
+ * 时不启动——代理解析 CONNECT 后直连目标,没有上游代理链,系统代理劫持
12
+ * 类网络开了反而断网。回退路径就是旧的 Seatbelt 端口级派生(其允许列表
13
+ * 会并入 ambient proxy 端点,见 boot.ts 的 W4-8 注释)。
14
+ *
15
+ * v1 限制(与框架 egress-proxy 的 v1 范围一致):
16
+ * - 只服务 CONNECT(HTTPS 隧道)。plain-http 端点(如本机 Ollama)不进
17
+ * 代理,直接留在 Seatbelt 列表里。
18
+ * - 代理直连目标主机,无上游代理链。
19
+ */
20
+ export interface EgressProxyPlan {
21
+ command: string;
22
+ args: string[];
23
+ port: number;
24
+ /** `127.0.0.1:<port>` — 同时是 Seatbelt 列表里的代理条目。 */
25
+ proxyEndpoint: string;
26
+ /** 给 sidecar 子进程环境的 HTTPS_PROXY 值。 */
27
+ proxyUrl: string;
28
+ /** 代理开启后的 Seatbelt 允许列表:代理端口 + plain-http 直连端点。 */
29
+ sandboxAllowedHosts: string[];
30
+ /** 交给代理 --allow 的主机名单(https 端点)。 */
31
+ proxiedHosts: string[];
32
+ /**
33
+ * W2.2.2 凭证代理:存在时代理以注入模式运行,sidecar 的 provider
34
+ * baseUrl 改写为 http://<host> 且不再下发 apiKey——真实密钥只存在于
35
+ * 代理进程(经 env 传入,绝不出现在 argv / sidecar / 沙箱子进程)。
36
+ */
37
+ broker?: {
38
+ host: string;
39
+ secretEnv: string;
40
+ /** 完整 Authorization 头值("Bearer sk-...");仅驻留主进程内存。 */
41
+ secretValue: string;
42
+ };
43
+ /**
44
+ * 出网拓宽控制面(ask 链路):代理暴露 loopback-only `POST /allow`
45
+ * 端点(Bearer 认证),用户批准放行后 sidecar 把目标加进会话级白名单。
46
+ * token 经 env 传入代理与 sidecar(argv 在 ps 可见);沙箱子进程环境
47
+ * 被白名单过滤,拿不到 token,无法自我拓宽。
48
+ */
49
+ control?: {
50
+ tokenEnv: string;
51
+ /** 仅驻留主进程内存;随 sidecar env 下发。 */
52
+ tokenValue: string;
53
+ };
54
+ }
55
+ /**
56
+ * `baseUrl` 对应的允许条目形态:裸主机(框架侧隐含 443/80)或 `host:port`。
57
+ *
58
+ * `proxied` 为 false 表示 plain-http 端点——它们不走代理,留在沙箱 profile
59
+ * 的直连列表里。解析不出主机名时返回 null。boot 时的名单派生与运行期的会话
60
+ * 放行共用这一处,两者的条目语义不会分叉。
61
+ */
62
+ export declare function egressAllowEntry(baseUrl: string | undefined): {
63
+ entry: string;
64
+ host: string;
65
+ port: string;
66
+ proxied: boolean;
67
+ } | null;
68
+ /**
69
+ * 从 provider baseUrl 构建代理计划。https 端点进代理名单;http 端点留在
70
+ * 直连列表。派生不出任何代理条目(无 https 端点且无 web 域名)时返回
71
+ * null(调用方回退到旧的 Seatbelt 派生路径——空名单的代理没有意义,
72
+ * 框架侧也会 fail loud)。
73
+ */
74
+ export declare function buildEgressProxyPlan(options: {
75
+ pythonExecutable: string;
76
+ port: number;
77
+ providerBaseUrl?: string;
78
+ /** 存在且 provider 为 https 时启用凭证代理模式(W2.2.2)。 */
79
+ providerApiKey?: string;
80
+ /**
81
+ * 3.1b/3.1d:并入代理 CONNECT 白名单的 web 出网主机(由
82
+ * deriveWebEgressHosts 推导,与 sidecar 应用层
83
+ * STEERABLE_WEB_ALLOWED_DOMAINS 同源)。注意框架代理是精确主机匹配
84
+ * ——应用层域名条目覆盖的子域不在其列,抓取子域会被代理 403(错误
85
+ * 信息已带提示)。
86
+ */
87
+ webAllowedHosts?: string[];
88
+ }): EgressProxyPlan | null;
89
+ /**
90
+ * 3.1b/3.1d:推导应并入代理 CONNECT 白名单的 web 出网主机。
91
+ *
92
+ * 数据源与 sidecar 应用层完全一致(同源):`STEERABLE_WEB_ALLOWED_DOMAINS`
93
+ * 进程环境(sidecar 继承同一 env 做 domain_policy_error),规范化也与
94
+ * WebToolsConfig 相同(小写、去前导点)。web_search 在 sidecar 进程内
95
+ * 执行时(搜索 key 存在,或 provider=ddg),其固定 API 端点一并加入——否则
96
+ * CONNECT api.tavily.com:443 / html.duckduckgo.com:443 会被代理 403。
97
+ * provider=host 的托管搜索在 Electron 主进程执行,不占 sidecar 出网。
98
+ *
99
+ * 应用层空名单的语义是"任意公网",代理白名单不能是开放的,所以空名单
100
+ * 不并入任何条目——此时 web_fetch 在 confined 下对名单外目标被代理
101
+ * 403,sidecar 侧错误信息已带指向本名单的提示。
102
+ */
103
+ export declare function deriveWebEgressHosts(options: {
104
+ /** webToolsEnabled() 的结果。 */
105
+ webTools: boolean;
106
+ /** sidecarWebSearchEnv(...) 的结果(判断 sidecar 是否进程内执行搜索)。 */
107
+ searchEnv: Record<string, string>;
108
+ /** 读取 STEERABLE_WEB_* 的环境(通常 process.env)。 */
109
+ env: NodeJS.ProcessEnv;
110
+ }): string[];
111
+ export declare function getActiveEgressBroker(): {
112
+ host: string;
113
+ proxyUrl: string;
114
+ } | null;
115
+ /**
116
+ * 出网管控态势的持久披露(W-egress-posture)。boot 的每次决策都记录:
117
+ * per-host 代理生效 / 退回端口级(含原因)/ 用户显式关闭。设置页
118
+ * 「安全」区经 sandbox-posture 端点读取——退回不再是只有主进程日志
119
+ * 可见的静默事件。
120
+ */
121
+ export interface EgressPosture {
122
+ mode: 'per-host-proxy' | 'port-only-fallback' | 'disabled';
123
+ /** 退回/关闭的原因(人可读,一行)。per-host-proxy 时为 null。 */
124
+ reason: string | null;
125
+ }
126
+ export declare function recordEgressPosture(posture: EgressPosture): void;
127
+ /**
128
+ * 起不起按主机代理——在派生名单、起进程之前就能定的那部分判断。
129
+ *
130
+ * 两种不起:显式关闭,以及检测到 ambient/系统代理(本代理解析 CONNECT 后
131
+ * 直连目标,没有上游代理链,在系统代理劫持的网络里开了反而断网)。两者的
132
+ * `reason` 是设置页「安全」区直接渲染的文案,所以在这里定形并被测试锁住。
133
+ *
134
+ * 派生不出代理条目、或进程启动失败,要等到那两步才知道,仍由调用方处理。
135
+ */
136
+ export declare function decideEgressProxy(options: {
137
+ env: NodeJS.ProcessEnv;
138
+ ambientProxies: string[];
139
+ }): {
140
+ start: true;
141
+ } | {
142
+ start: false;
143
+ posture: EgressPosture;
144
+ };
145
+ export declare function getEgressPosture(): EgressPosture | null;
146
+ export declare function getActiveEgressProxyEndpoint(): string | null;
147
+ /**
148
+ * 把 `baseUrl` 的主机加进运行中代理的会话白名单。
149
+ *
150
+ * LLM 端点不是一条策略判断:白名单本来就是从用户正在编辑的那一项设置派生
151
+ * 出来的,所以它必须跟着那项设置走,而不是停在 boot 时的取值上。否则用户
152
+ * 换网关后整个会话(连聊天)都被自己的代理 403,只有重启才好——测试按钮
153
+ * 更是必然被拒,因为它验的正是尚未保存的地址。
154
+ *
155
+ * 只有人能到达这里:调用点是设置页的 models.list 与保存路由,模型的工具面
156
+ * 没有这两者,`web_fetch` 的 SSRF 预检也拒绝回环地址。web 那半边的名单不受
157
+ * 影响,被注入的模型照样连不上名单外的主机。
158
+ *
159
+ * 放行是会话域的(代理进程退出即失效),条目形态与 `buildEgressProxyPlan`
160
+ * 的派生一致,两处语义不会分叉。
161
+ *
162
+ * @returns 已放行为 true;无可放行之处为 false——代理没在跑、URL 解析不了,
163
+ * 或端点是 plain-http(那类端点不进代理,由 spawn 时的沙箱 profile 钉死,
164
+ * 运行期改不了,仍需重启)。
165
+ */
166
+ export declare function allowEgressForBaseUrl(baseUrl: string | undefined): Promise<boolean>;
167
+ export interface EgressProxyHandle {
168
+ plan: EgressProxyPlan;
169
+ /** 控制端点实际端口(代理 stdout 的 EGRESS_CONTROL_PORT= 行读回)。 */
170
+ controlPort: number | null;
171
+ stop: () => void;
172
+ }
173
+ /**
174
+ * 启动代理并等它就绪后再放行 sidecar 启动——sidecar 的 LLM 路径要经过它,
175
+ * 代理未就绪就启动 sidecar 等于把首轮对话送进必失败的黑洞。
176
+ *
177
+ * 就绪判定用 TCP 连接探针而不是解析 stdout:框架 v1 的就绪日志走
178
+ * logging(默认不输出),把宿主耦合到日志文案上太脆。端口是我们选的,
179
+ * connect 成功 = 已在监听。
180
+ */
181
+ export declare function startEgressProxy(plan: EgressProxyPlan, onLog: (line: string) => void, readyTimeoutMs?: number): Promise<EgressProxyHandle>;
182
+ /** 让系统分配一个空闲回环端口(listen(0) 后立即释放,供代理绑定)。 */
183
+ export declare function pickFreePort(): Promise<number>;