@sema-agent/server 7.102.0 → 7.103.1
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/MIGRATION.md +91 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/USAGE.md +107 -6
- package/dist/background-session-reap.d.ts +4 -1
- package/dist/bake-runner/main.d.ts +21 -3
- package/dist/bake-runner/main.js +18 -11
- package/dist/boot/budget-tracing.d.ts +1 -0
- package/dist/boot/budget-tracing.js +3 -2
- package/dist/boot/config-center.d.ts +12 -2
- package/dist/boot/config-center.js +33 -21
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-04-stores.js +2 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -0
- package/dist/boot/stage-06-runners.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +6 -1
- package/dist/brain.d.ts +60 -9
- package/dist/brain.js +44 -8
- package/dist/capabilities/center-plugins.d.ts +9 -3
- package/dist/capabilities/center-plugins.js +5 -10
- package/dist/config-catalog.js +9 -5
- package/dist/config-center/apply-effective.d.ts +40 -2
- package/dist/config-center/apply-effective.js +32 -12
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/mcp-revocation.d.ts +2 -2
- package/dist/config-center/mcp-revocation.js +1 -1
- package/dist/config-center/restart-signal.d.ts +39 -12
- package/dist/config-center/restart-signal.js +13 -24
- package/dist/config-types.d.ts +35 -17
- package/dist/config-types.js +3 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +43 -17
- package/dist/env-name-allowlist-knobs.d.ts +21 -7
- package/dist/env-name-allowlist-knobs.js +7 -2
- package/dist/fleet-client.d.ts +9 -3
- package/dist/fleet-client.js +2 -0
- package/dist/host-lsp-manager.d.ts +4 -3
- package/dist/host-lsp-manager.js +2 -6
- package/dist/http/dispatch.js +4 -0
- package/dist/http/server.d.ts +4 -0
- package/dist/leader/wire.d.ts +8 -1
- package/dist/leader/wire.js +4 -3
- package/dist/model-entry-refusal.d.ts +32 -9
- package/dist/model-entry-refusal.js +22 -10
- package/dist/model-route-endpoint.d.ts +10 -0
- package/dist/model-route-endpoint.js +6 -2
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +15 -2
- package/dist/outbound-dispatcher.d.ts +125 -0
- package/dist/outbound-dispatcher.js +211 -0
- package/dist/plugins/host-platform.d.ts +6 -1
- package/dist/plugins/remote-env-adb.d.ts +10 -3
- package/dist/plugins/remote-env-adb.js +24 -20
- package/dist/plugins/remote-env-host.js +3 -11
- package/dist/plugins/remote-env-k8s.d.ts +8 -1
- package/dist/plugins/remote-env-k8s.js +31 -24
- package/dist/plugins/remote-env-ssh.d.ts +8 -1
- package/dist/plugins/remote-env-ssh.js +24 -18
- package/dist/plugins/remote-shell.d.ts +109 -9
- package/dist/plugins/remote-shell.js +83 -0
- package/dist/project-memory.js +3 -6
- package/dist/run-local.js +2 -0
- package/dist/server-secret-env.d.ts +89 -24
- package/dist/server-secret-env.js +21 -1
- package/dist/trace/core-keyset-guard.d.ts +6 -1
- package/dist/trace/project.js +13 -0
- package/dist/trace/redact.d.ts +11 -0
- package/dist/trace/redact.js +1 -0
- package/package.json +4 -3
- package/skills/update-config.md +23 -5
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-776 —— server 进程出网的**唯一**一只 dispatcher:回环目标恒直连,其余按 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`
|
|
3
|
+
* 交给 undici `EnvHttpProxyAgent`。**一只 dispatcher、一处安装、零旋钮** —— 代理变量本身就是配置通道。
|
|
4
|
+
*
|
|
5
|
+
* ── 病(test 黑盒追出,core 直讯 09-26)────────────────────────────────────────────────────────────
|
|
6
|
+
* Node 的内建 `fetch` 不读这三只变量(Node 24 起要另开 `NODE_USE_ENV_PROXY`)。于是只能经代理出网的部署上,引擎 → 网关这一跳、
|
|
7
|
+
* 以及 server 自己所有出网跳(中心拉取 / 公告 / 凭证续签 / WebSearch / 对象存储 …)一律直连 ⇒ 网关主机名只有代理解析得了时
|
|
8
|
+
* `ENOTFOUND`、代理零命中。
|
|
9
|
+
*
|
|
10
|
+
* ── 为什么一处安装就覆盖全部(读实现得出,不是从名字推断)─────────────────────────────────────────
|
|
11
|
+
* · 本仓的出网 fetch 座默认都是**全局** `fetch`(`fetchImpl ?? fetch` / `fetchImpl: typeof fetch = fetch`),全局 fetch 每次请求
|
|
12
|
+
* 读 undici 的全局 dispatcher 槽;
|
|
13
|
+
* · core 的引擎传输(`@sema-agent/core` 7.33.1 `dist/brain/transport.js` 的 `hostDispatcher` / `resolveFetch`)给每次请求带 `ENGINE_DISPATCHER`,它是宿主
|
|
14
|
+
* 全局 dispatcher 的透明替身 —— **每次请求**按 handler API 版本读 `globalThis[Symbol.for("undici.globalDispatcher.2")]` / `…1`,
|
|
15
|
+
* 除逐请求的头 / 体钟外一切转发 ⇒ 本仓装一只全局 dispatcher,引擎跳同样生效,**core 零改**;
|
|
16
|
+
* · 槽位(车OY 实测,不臆断):npm undici 7.30.0 的 `setGlobalDispatcher` **两只槽同写**(`lib/global.js` 的 `setGlobalDispatcher` 两次 `defineProperty`),所以
|
|
17
|
+
* **一次调用**就同时覆盖 Node 20 / 22(内嵌 undici 6.24.1,读槽 1)与 Node 24(内嵌 7.10.0 / 7.29.1,读槽 1)的内建 `fetch`,
|
|
18
|
+
* 以及 core 按 handler API 选槽的那一读 —— 不需要第二只槽的安装代码。实测表(内建 `fetch()` 是否命中):Node 20.20.2 / 22.22.3 /
|
|
19
|
+
* 24.2.0 / 24.21.0 全命中,内建 fetch 交来的 handler 在四个版本上都是 v1 形(`onConnect`/`onHeaders`…),undici 7 的
|
|
20
|
+
* `DispatcherBase.dispatch` 自己 `UnwrapHandler.unwrap` 两形都收。
|
|
21
|
+
* · 🔴 **Bun 例外**(镜像 `CMD ["bun", "run", "src/main.ts"]` 与 `bun build --compile` 两形):`import "undici"` 在 Bun 下解析到
|
|
22
|
+
* Bun 自带的 undici 垫片 —— `setGlobalDispatcher` 只赋一个模块变量、两只槽都不写,Bun 的原生 `fetch` 也不读 dispatcher。
|
|
23
|
+
* Bun 的原生 fetch **自己**读 `HTTP(S)_PROXY` / `NO_PROXY`(Bun 1.3.14 实测:代理、`NO_PROXY` 精确 / 后缀 / `*` 三形都认),
|
|
24
|
+
* 但**回环不豁免**(`127.0.0.1` / `localhost` 照送代理)。本模块在那里是惰性的:安装后探槽,槽里不是我们 ⇒ 如实报
|
|
25
|
+
* `loopbackDirect:false`,配了代理时升 warn 并指路(见 {@link installOutboundDispatcher})。
|
|
26
|
+
*
|
|
27
|
+
* ── 回环恒直连(无条件规则;对 undici 缺省的**收窄**)─────────────────────────────────────────────
|
|
28
|
+
* `EnvHttpProxyAgent` 只按 `NO_PROXY` 判豁免,**没有回环隐式豁免**(undici 7.30.0 `lib/dispatcher/env-http-proxy-agent.js` 的 `#shouldProxy`:`NO_PROXY` 空 ⇒ 恒代理)
|
|
29
|
+
* ⇒ `HTTPS_PROXY` 设了而 `NO_PROXY` 没写 localhost 时,`127.0.0.1` 上的本机网关 / 本机中心会被送进代理 —— 代理够不到本进程的回环,
|
|
30
|
+
* 那是静默全断。所以回环目标在这里**先于** `NO_PROXY` 判、恒走直连 Agent;不看拓扑、不看客户端表态、没有旋钮(旋钮 = 让运维
|
|
31
|
+
* 能把本机流量送进公司代理,没有任何部署形需要它)。判据集与 sema-cli 1.0.136 `src/utils/proxy.ts:104` `isLoopbackHost`
|
|
32
|
+
* (壳 → 本机引擎那一跳的同形修 `LoopbackDirectDispatcher`,L-587)同一集,两段一致。
|
|
33
|
+
* **unix socket**:undici 的 `socketPath` 是连接器(`Agent` / `Client` 构造)选项,不是逐请求选项 ⇒ 打 unix socket 的调用必然自带
|
|
34
|
+
* dispatcher,**结构上**不经过全局槽 —— 本模块没有、也不需要一条 unix 臂。
|
|
35
|
+
*
|
|
36
|
+
* ── boot 冻结 ────────────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
* 三只变量在构造时读**一次**(与本仓其余 env 同律)。`EnvHttpProxyAgent` 自己会在每次请求比对 `process.env.NO_PROXY` 并重读 ——
|
|
38
|
+
* 显式把 `noProxy`(以及两只代理)交给它就关掉那条运行期重读(`#noProxyChanged` 在 `opts.noProxy !== undefined` 时恒假)。
|
|
39
|
+
* 读法与 undici 读 `process.env` **逐字同序**:小写优先、`??`(空串算「设了」)、`HTTPS_PROXY` 缺席 / 空 ⇒ https 目标走 `HTTP_PROXY`
|
|
40
|
+
* (`EnvHttpProxyAgent` 构造器与 `#noProxyEnv`)—— 本模块读一次、再原样交给它,undici 不再读 env,读者只有这一处。
|
|
41
|
+
*
|
|
42
|
+
* ── 代理那一跳:失败可辨认 + 不许进重连风暴(test 复测义务 srv-s776-engine-proxy-recheck ④,板 [ref];opus 修复验证轮 F1 / F2)──
|
|
43
|
+
* 接缝 = `EnvHttpProxyAgent` 公开的 `clientFactory` 选项(类型面 `ProxyAgent.Options.clientFactory`,原样交给两只 `ProxyAgent`;交给
|
|
44
|
+
* 直连 `Agent` 的那份被它的 `deepClone`(JSON 往返)丢掉,不生效)—— 它造的是**连代理的那只客户端**,每一次代理拨号(TCP / TLS 到代理 +
|
|
45
|
+
* `CONNECT` 交换)都经它的 `connect`,不多不少。{@link ProxyDialPool} 在这里把两类结局收成一只错误 {@link OutboundProxyError}:
|
|
46
|
+
* · 拨号失败(拒连 / 解析不了 / 超时 / 代理 TLS / 收下就关 …)⇒ 包成 `OutboundProxyError`(`cause` = 原错误);
|
|
47
|
+
* · `CONNECT` 回非 200 ⇒ 关掉那条 socket(undici 自己也是这么做的),同样包成 `OutboundProxyError`(`cause` 说出状态码)。
|
|
48
|
+
* 🔴 **包装层不铸 `code`,这是承重的两件事**:① undici 客户端在连接阶段把 `UND_ERR_SOCKET` 当可恢复错误重连(`client.js` 的 `onError`),
|
|
49
|
+
* 而 `CONNECT` 恰在连接阶段 —— 代理收下 TCP 就关(Linux 上是 FIN ⇒ `UND_ERR_SOCKET`)时,裸 `ProxyAgent` / Node 24 `NODE_USE_ENV_PROXY`
|
|
50
|
+
* 都会重连死循环(修前实测 10 s 四五万条连接、请求挂到调用方超时、abort 之后仍在涨);无 code 的错误让排队的请求当场失败。
|
|
51
|
+
* ② core 的连接失败分类(`classifyConnectFailure`)只看链上的 `code`,无 code 的一节对它透明 —— 重试 / 切备 / 断路器的判据不变。
|
|
52
|
+
* 修前读数(装了 dispatcher、未加归因;引擎终局句 = core `describeNetworkError` 原样):代理拒连 `[network] fetch failed (connect ECONNREFUSED
|
|
53
|
+
* 127.0.0.1:1)`;代理主机解析不了 `… (getaddrinfo ENOTFOUND proxy.s776.invalid)`;CONNECT 回 403 `… (Request was cancelled. ← UND_ERR_ABORTED:
|
|
54
|
+
* Proxy response (403) !== 200 when HTTP Tunneling)` —— 都不说「经代理」。修后:`… (via outbound proxy (from HTTPS_PROXY) ←
|
|
55
|
+
* connect ECONNREFUSED 127.0.0.1:1)`(message 不带代理地址 —— 主会话 F3 裁定,见 {@link OutboundProxyError})。core 的句子一字不改(它逐层展开 cause 链,包装层就是链上多一节)。隧道建好之后的失败(目标的 TLS、
|
|
56
|
+
* 目标的响应)不经这个接缝,不冠代理 —— 那是目标的事。
|
|
57
|
+
*
|
|
58
|
+
* ── 安全轴 ───────────────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
* 代理 URL 的 userinfo 是凭据(undici 把它译成 `Proxy-Authorization: Basic …`)。它只进 dispatcher,不进任何观测面:姿态
|
|
60
|
+
* ({@link buildOutboundProxyPosture})、拒启句与归因句同一把尺 {@link displayProxyUrl}(形干净只出 `scheme//host[:port]`、过全仓那一只
|
|
61
|
+
* display-safe 铸点;形不干净整只占位);姿态只上启动日志一行
|
|
62
|
+
* `outbound_proxy`,不进 `/health`、不进公告、不进租户 wire(代理那一跳失败的终局句只说「经代理 + 哪只变量」,F3 裁定)。代理变量不可用(undici 构造即拒:无 scheme、`ftp:`、带路径 / 查询)⇒
|
|
63
|
+
* **拒启**(与 Node 24 `NODE_USE_ENV_PROXY=1` 遇坏值的行为同向)—— 静默直连 = 运维以为出网走了代理而实际没有,那是出网策略面的
|
|
64
|
+
* fail-open;拒因只回显 display-safe 值。
|
|
65
|
+
*/
|
|
66
|
+
import { Dispatcher } from "undici";
|
|
67
|
+
import type { Logger } from "./observability/logger.js";
|
|
68
|
+
/**
|
|
69
|
+
* 出网代理姿态(`build*`:纯数据)—— 启动日志 `outbound_proxy` 行与配置目录共读的**同一份**投影。
|
|
70
|
+
* · `httpProxy` / `httpsProxy`:http: / https: 目标**实际**走的代理(display-safe;`null` = 直连)。`httpsProxy` 在 `HTTPS_PROXY`
|
|
71
|
+
* 缺席 / 空时回落 `HTTP_PROXY` —— undici 的读法,不是本仓的发明;
|
|
72
|
+
* · `noProxy`:条目原样(按 undici 的切分:逗号或空白;`null` = 未设 / 空)。
|
|
73
|
+
*/
|
|
74
|
+
export interface OutboundProxyPosture {
|
|
75
|
+
readonly httpProxy: string | null;
|
|
76
|
+
readonly httpsProxy: string | null;
|
|
77
|
+
readonly noProxy: readonly string[] | null;
|
|
78
|
+
}
|
|
79
|
+
export declare function buildOutboundProxyPosture(env?: NodeJS.ProcessEnv): OutboundProxyPosture;
|
|
80
|
+
/**
|
|
81
|
+
* 目标 origin 会不会落在**本机**(回环 / 未指定地址)。入参是 dispatch 的 `origin`(`string | URL`);解析不动 ⇒ `false`
|
|
82
|
+
* (按远端处置 = 交给代理那一半,与修前方向一致)。认哪几形(与 sema-cli 1.0.136 `isLoopbackHost` 同集):
|
|
83
|
+
* · `localhost`(含 FQDN 尾点 `localhost.`);刻意**不**认 `*.localhost` 子域(能否落到本机取决于系统解析器,不是地址本身的事实);
|
|
84
|
+
* · IPv4 `127.0.0.0/8` 整段(WHATWG URL 先把 `127.1` / `0x7f.1` 这类写法规范成点分四段);
|
|
85
|
+
* · IPv6 `::1`、IPv4 映射段 `::ffff:127.x.x.x`(URL 规范成 `::ffff:7fxx:xxxx` 形);
|
|
86
|
+
* · 未指定地址 `0.0.0.0` / `::`(及映射形):作为目的地址它在本机就是「本机」,送进代理只会让代理去连它自己。
|
|
87
|
+
*/
|
|
88
|
+
export declare function isLoopbackOrigin(origin: string | URL | undefined): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* 代理那一跳失败的**唯一**错误形(`cause` = 原错误;**不铸 `code`**,理由见顶注)。`proxy` = display-safe 代理地址,`from` = 哪只变量给的。
|
|
91
|
+
* 🔴 **message 不带地址**(主会话 F3 裁定,09-26):这句经 core 的终局句进租户可见的失败原因;地址是基础设施细节,只点明「经代理 + 哪只变量」,
|
|
92
|
+
* 一条规则、不按姿态分叉。地址留在 `proxy` 属性上供进程内日志腿取;单用户运维在启动日志 `outbound_proxy` 行里有同一串可对照。根因那一节是
|
|
93
|
+
* undici 自己的错误原文(拒连 / 解析不了两形会点名代理的 host:port,与直连失败点名目标同形),本仓不改写上游错误。
|
|
94
|
+
* 字符串化(`String(err)`)带出根因:只打第一层 cause 的日志腿(如 hook 的 `String(cause)`)也读得到 `ECONNREFUSED` 这类根因;
|
|
95
|
+
* core 的 `describeNetworkError` 读的是 `.message`,不受影响、不重复。
|
|
96
|
+
*/
|
|
97
|
+
export declare class OutboundProxyError extends Error {
|
|
98
|
+
readonly proxy: string;
|
|
99
|
+
readonly from: string;
|
|
100
|
+
readonly name = "OutboundProxyError";
|
|
101
|
+
constructor(proxy: string, from: string, cause: unknown);
|
|
102
|
+
toString(): string;
|
|
103
|
+
}
|
|
104
|
+
/** 代理变量不可用的拒启码(拼法随 `config.*` 家族,出现在拒句方括号里)。 */
|
|
105
|
+
export declare const OUTBOUND_PROXY_INVALID_CODE = "config.outbound_proxy_invalid";
|
|
106
|
+
/**
|
|
107
|
+
* 构造(`create*`:活对象)。代理变量不可用(undici 构造即拒)⇒ **抛**,拒因点名变量、只回显 {@link displayProxyUrl} 的形
|
|
108
|
+
* (undici 自己的错误文案不转述:`ERR_INVALID_URL` 把原值挂在 `input` 上,转述即泄漏的一条路)。
|
|
109
|
+
*/
|
|
110
|
+
export declare function createOutboundDispatcher(env?: NodeJS.ProcessEnv): {
|
|
111
|
+
dispatcher: Dispatcher;
|
|
112
|
+
posture: OutboundProxyPosture;
|
|
113
|
+
};
|
|
114
|
+
/** 安装结果 = 姿态 + 这条规则在本进程**是否生效**(运行时的 fetch 读不读 undici 全局槽 —— Node 读、Bun 不读)。 */
|
|
115
|
+
export interface OutboundDispatcherInstall extends OutboundProxyPosture {
|
|
116
|
+
readonly loopbackDirect: boolean;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 进程级安装(**幂等**:第二次调用原样返回第一次的结果,不再装)。唯二调用点:HTTP 服务的 `boot/stage-01-config.ts` 末尾
|
|
120
|
+
* (config 已载、任何出网之前)与一次性 CLI `run-local.ts` 的同一位置(它不经 stage-01)。
|
|
121
|
+
*/
|
|
122
|
+
export declare function installOutboundDispatcher(logger: Logger, env?: NodeJS.ProcessEnv): OutboundDispatcherInstall;
|
|
123
|
+
/** 测试专用:撤回安装(槽还给装之前的那只,本只关掉),下一次 {@link installOutboundDispatcher} 重新读 env。 */
|
|
124
|
+
export declare function resetOutboundDispatcherForTest(): void;
|
|
125
|
+
//# sourceMappingURL=outbound-dispatcher.d.ts.map
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { Agent, Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, Pool, setGlobalDispatcher } from "undici";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { buildDisplaySafeUrl, DISPLAY_SAFE_URL_TOKEN } from "./trace/redact.js";
|
|
4
|
+
function readOutboundProxyEnv(env = process.env) {
|
|
5
|
+
return {
|
|
6
|
+
httpProxy: env.http_proxy ?? env.HTTP_PROXY ?? "",
|
|
7
|
+
httpsProxy: env.https_proxy ?? env.HTTPS_PROXY ?? "",
|
|
8
|
+
noProxy: env.no_proxy ?? env.NO_PROXY ?? "",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function isCleanProxyUrl(raw) {
|
|
12
|
+
return /^[a-z][a-z0-9+.-]*:\/\/[^/?#\\]+\/?$/i.test(raw) && URL.canParse(raw) && new URL(raw).host !== "";
|
|
13
|
+
}
|
|
14
|
+
function displayProxyUrl(raw) {
|
|
15
|
+
if (!isCleanProxyUrl(raw))
|
|
16
|
+
return DISPLAY_SAFE_URL_TOKEN;
|
|
17
|
+
const u = new URL(raw);
|
|
18
|
+
return buildDisplaySafeUrl(`${u.protocol}//${u.host}`);
|
|
19
|
+
}
|
|
20
|
+
function postureOf(v) {
|
|
21
|
+
const shown = (raw) => (raw === "" ? null : displayProxyUrl(raw));
|
|
22
|
+
const entries = v.noProxy.split(/[,\s]/).filter((e) => e !== "");
|
|
23
|
+
return { httpProxy: shown(v.httpProxy), httpsProxy: shown(v.httpsProxy || v.httpProxy), noProxy: entries.length > 0 ? entries : null };
|
|
24
|
+
}
|
|
25
|
+
export function buildOutboundProxyPosture(env = process.env) {
|
|
26
|
+
return postureOf(readOutboundProxyEnv(env));
|
|
27
|
+
}
|
|
28
|
+
export function isLoopbackOrigin(origin) {
|
|
29
|
+
if (origin === undefined)
|
|
30
|
+
return false;
|
|
31
|
+
const text = String(origin);
|
|
32
|
+
if (!URL.canParse(text))
|
|
33
|
+
return false;
|
|
34
|
+
let host = new URL(text).hostname.toLowerCase();
|
|
35
|
+
if (host.startsWith("[") && host.endsWith("]"))
|
|
36
|
+
host = host.slice(1, -1);
|
|
37
|
+
host = host.replace(/\.+$/, "");
|
|
38
|
+
if (host === "localhost")
|
|
39
|
+
return true;
|
|
40
|
+
const family = isIP(host);
|
|
41
|
+
if (family === 4)
|
|
42
|
+
return host.startsWith("127.") || host === "0.0.0.0";
|
|
43
|
+
if (family !== 6)
|
|
44
|
+
return false;
|
|
45
|
+
if (host === "::1" || host === "::")
|
|
46
|
+
return true;
|
|
47
|
+
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
48
|
+
if (mapped === null)
|
|
49
|
+
return false;
|
|
50
|
+
const hi = Number.parseInt(mapped[1], 16);
|
|
51
|
+
const lo = Number.parseInt(mapped[2], 16);
|
|
52
|
+
return hi >> 8 === 127 || (hi === 0 && lo === 0);
|
|
53
|
+
}
|
|
54
|
+
class OutboundDispatcher extends Dispatcher {
|
|
55
|
+
direct;
|
|
56
|
+
proxied;
|
|
57
|
+
constructor(direct, proxied) {
|
|
58
|
+
super();
|
|
59
|
+
this.direct = direct;
|
|
60
|
+
this.proxied = proxied;
|
|
61
|
+
}
|
|
62
|
+
dispatch(options, handler) {
|
|
63
|
+
return (isLoopbackOrigin(options.origin) ? this.direct : this.proxied).dispatch(options, handler);
|
|
64
|
+
}
|
|
65
|
+
close(callback) {
|
|
66
|
+
const done = Promise.all([this.direct.close(), this.proxied.close()]).then(() => undefined);
|
|
67
|
+
if (callback === undefined)
|
|
68
|
+
return done;
|
|
69
|
+
void done.then(callback, callback);
|
|
70
|
+
}
|
|
71
|
+
destroy(errOrCallback, callback) {
|
|
72
|
+
const cb = typeof errOrCallback === "function" ? errOrCallback : callback;
|
|
73
|
+
const err = typeof errOrCallback === "function" ? null : (errOrCallback ?? null);
|
|
74
|
+
const done = Promise.all([this.direct.destroy(err), this.proxied.destroy(err)]).then(() => undefined);
|
|
75
|
+
if (cb === undefined)
|
|
76
|
+
return done;
|
|
77
|
+
void done.then(cb, cb);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class OutboundProxyError extends Error {
|
|
81
|
+
proxy;
|
|
82
|
+
from;
|
|
83
|
+
name = "OutboundProxyError";
|
|
84
|
+
constructor(proxy, from, cause) {
|
|
85
|
+
super(`via outbound proxy (from ${from})`, { cause });
|
|
86
|
+
this.proxy = proxy;
|
|
87
|
+
this.from = from;
|
|
88
|
+
}
|
|
89
|
+
toString() {
|
|
90
|
+
let root = this.cause;
|
|
91
|
+
for (let depth = 0; root instanceof Error && root.cause instanceof Error && depth < 4; depth++)
|
|
92
|
+
root = root.cause;
|
|
93
|
+
return `${this.name}: ${this.message} ← ${root instanceof Error ? root.message : String(root)}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const PROXY_CONNECT_DEADLINE_MS = 10_000;
|
|
97
|
+
const dialDeadlines = new WeakMap();
|
|
98
|
+
function clearDialDeadline(socket) {
|
|
99
|
+
clearTimeout(dialDeadlines.get(socket));
|
|
100
|
+
dialDeadlines.delete(socket);
|
|
101
|
+
}
|
|
102
|
+
class ProxyDialPool extends Pool {
|
|
103
|
+
shown;
|
|
104
|
+
from;
|
|
105
|
+
constructor(origin, options, shown, from) {
|
|
106
|
+
super(origin, options);
|
|
107
|
+
this.shown = shown;
|
|
108
|
+
this.from = from;
|
|
109
|
+
}
|
|
110
|
+
connect(options, callback) {
|
|
111
|
+
if (callback === undefined) {
|
|
112
|
+
return super.connect(options).then((data) => {
|
|
113
|
+
const refused = this.refusal(data);
|
|
114
|
+
if (refused !== undefined)
|
|
115
|
+
throw refused;
|
|
116
|
+
return data;
|
|
117
|
+
}, (err) => {
|
|
118
|
+
throw new OutboundProxyError(this.shown, this.from, err);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
super.connect(options, (err, data) => callback(err !== null ? new OutboundProxyError(this.shown, this.from, err) : (this.refusal(data) ?? null), data));
|
|
122
|
+
}
|
|
123
|
+
refusal(data) {
|
|
124
|
+
clearDialDeadline(data.socket);
|
|
125
|
+
if (data.statusCode === 200)
|
|
126
|
+
return undefined;
|
|
127
|
+
data.socket.on("error", () => undefined).destroy();
|
|
128
|
+
return new OutboundProxyError(this.shown, this.from, new Error(`proxy answered CONNECT with HTTP ${String(data.statusCode)}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function withDialDeadline(options) {
|
|
132
|
+
const inner = Reflect.get(options, "connect");
|
|
133
|
+
if (typeof inner !== "function")
|
|
134
|
+
return { ...options };
|
|
135
|
+
const connect = (opts, callback) => {
|
|
136
|
+
const expiresAt = performance.now() + PROXY_CONNECT_DEADLINE_MS;
|
|
137
|
+
Reflect.apply(inner, undefined, [
|
|
138
|
+
opts,
|
|
139
|
+
(err, socket) => {
|
|
140
|
+
if (err === null && socket !== null) {
|
|
141
|
+
const expire = () => void socket.destroy(new Error(`proxy did not answer CONNECT within ${String(PROXY_CONNECT_DEADLINE_MS)} ms`));
|
|
142
|
+
dialDeadlines.set(socket, setTimeout(expire, Math.max(0, expiresAt - performance.now())));
|
|
143
|
+
socket.once("close", () => clearDialDeadline(socket));
|
|
144
|
+
callback(null, socket);
|
|
145
|
+
}
|
|
146
|
+
else
|
|
147
|
+
callback(err ?? new Error("proxy connector returned neither a socket nor an error"), null);
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
};
|
|
151
|
+
return { ...options, connect };
|
|
152
|
+
}
|
|
153
|
+
function createProxyClientFactory(v) {
|
|
154
|
+
const vars = [["HTTP_PROXY", v.httpProxy], ["HTTPS_PROXY", v.httpsProxy]].filter(([, raw]) => raw !== "" && URL.canParse(raw));
|
|
155
|
+
return (origin, options) => {
|
|
156
|
+
const same = vars.filter(([, raw]) => new URL(raw).href === origin.href);
|
|
157
|
+
const raw = same[0]?.[1] ?? origin.href;
|
|
158
|
+
return new ProxyDialPool(origin, withDialDeadline(options), displayProxyUrl(raw), same.length > 0 ? same.map(([name]) => name).join("/") : "proxy variable");
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export const OUTBOUND_PROXY_INVALID_CODE = "config.outbound_proxy_invalid";
|
|
162
|
+
export function createOutboundDispatcher(env = process.env) {
|
|
163
|
+
const v = readOutboundProxyEnv(env);
|
|
164
|
+
const shown = (name, raw) => `${name}=${raw === "" ? "(unset)" : `"${displayProxyUrl(raw)}"`}`;
|
|
165
|
+
const refuse = (code) => new Error(`[${OUTBOUND_PROXY_INVALID_CODE}] the outbound proxy variables are not usable by the HTTP transport (${code}): ${shown("HTTP_PROXY/http_proxy", v.httpProxy)}, ${shown("HTTPS_PROXY/https_proxy", v.httpsProxy)} — ` +
|
|
166
|
+
`write the proxy as a full URL (http://host:port or https://host:port; userinfo allowed, no path or query), or unset it for this process`);
|
|
167
|
+
if ([v.httpProxy, v.httpsProxy].some((raw) => raw.includes("://") && !isCleanProxyUrl(raw)))
|
|
168
|
+
throw refuse("ambiguous-url");
|
|
169
|
+
let proxied;
|
|
170
|
+
try {
|
|
171
|
+
proxied = new EnvHttpProxyAgent({ httpProxy: v.httpProxy, httpsProxy: v.httpsProxy, noProxy: v.noProxy, clientFactory: createProxyClientFactory(v) });
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
throw refuse(err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : "invalid");
|
|
175
|
+
}
|
|
176
|
+
return { dispatcher: new OutboundDispatcher(new Agent(), proxied), posture: postureOf(v) };
|
|
177
|
+
}
|
|
178
|
+
const GLOBAL_DISPATCHER_SLOTS = [Symbol.for("undici.globalDispatcher.1"), Symbol.for("undici.globalDispatcher.2")];
|
|
179
|
+
let installed;
|
|
180
|
+
export function installOutboundDispatcher(logger, env = process.env) {
|
|
181
|
+
if (installed !== undefined)
|
|
182
|
+
return installed.result;
|
|
183
|
+
const prior = getGlobalDispatcher();
|
|
184
|
+
const { dispatcher, posture } = createOutboundDispatcher(env);
|
|
185
|
+
setGlobalDispatcher(dispatcher);
|
|
186
|
+
const loopbackDirect = GLOBAL_DISPATCHER_SLOTS.every((slot) => Reflect.get(globalThis, slot) === dispatcher);
|
|
187
|
+
const result = { ...posture, loopbackDirect };
|
|
188
|
+
const runtime = process.versions.bun !== undefined ? `bun ${process.versions.bun}` : `node ${process.versions.node}`;
|
|
189
|
+
if (!loopbackDirect && (posture.httpProxy !== null || posture.httpsProxy !== null)) {
|
|
190
|
+
logger.warn("outbound_proxy", {
|
|
191
|
+
...result,
|
|
192
|
+
runtime,
|
|
193
|
+
note: "this runtime's fetch does not read the undici global dispatcher: it applies HTTP(S)_PROXY / NO_PROXY itself and does NOT exempt loopback targets — list localhost,127.0.0.1,::1 in NO_PROXY so local gateways / centers are not sent to the proxy",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
logger.info("outbound_proxy", { ...result, runtime });
|
|
198
|
+
}
|
|
199
|
+
installed = { dispatcher, prior, result };
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
export function resetOutboundDispatcherForTest() {
|
|
203
|
+
if (installed === undefined)
|
|
204
|
+
return;
|
|
205
|
+
const { dispatcher, prior } = installed;
|
|
206
|
+
installed = undefined;
|
|
207
|
+
if (prior !== undefined)
|
|
208
|
+
setGlobalDispatcher(prior);
|
|
209
|
+
void dispatcher.close().catch(() => undefined);
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=outbound-dispatcher.js.map
|
|
@@ -59,13 +59,18 @@ export declare function spawnGroupOptions(): {
|
|
|
59
59
|
* INCLUDING the throw on a dead group (ESRCH): every call site already wraps in its own try/catch with its own
|
|
60
60
|
* fallback (e.g. `child.kill("SIGKILL")`), and that structure must not change.
|
|
61
61
|
* win32 = core `signalProcessTree(pid,"SIGKILL")` → `taskkill /F /T` (never throws — fire-and-forget spawn).
|
|
62
|
+
* 不返回「落地」证据(S-769 车PA):POSIX 组信号「任一成员收到即成功」,返回成功证明不了组里没有打不着的活成员 —— 这一枪之后
|
|
63
|
+
* 「这个组还在不在」只由观察回答(同步硬收座 {@link hardKillGroupNow} 的 ESRCH、node 收割组长),不由这里推断。
|
|
62
64
|
*/
|
|
63
65
|
export declare function killTreeHard(pid: number): void;
|
|
64
66
|
/**
|
|
65
67
|
* S-745(core 7.32.0 #1107)—— **同步组硬收的一枪**:后台 driver 的 `killBackgroundNow` 座位用它(梯子的「组 SIGKILL」那一级,
|
|
66
68
|
* 不经 TERM、不挂定时器)。读数是 core 座位闭集里的三词,判据只看 `kill` 系统调用自己的结局:
|
|
67
69
|
* · POSIX:`kill(-pid, SIGKILL)` 落地 ⇒ `killed`;`ESRCH`(这个组已经没有进程)⇒ `already_gone`;其余拒因(`EPERM` 一族)⇒
|
|
68
|
-
* `no_pgid`(没有落地,调用方可重试;core 随即派普通 kill)
|
|
70
|
+
* `no_pgid`(没有落地,调用方可重试;core 随即派普通 kill)。⚠️ S-769 读数:darwin 对「组里只剩还没被 node 收割的僵尸」也答 `EPERM`
|
|
71
|
+
* (linux 答成功)。本函数**不**替它改判:EPERM 同样是「组里还有无权发信号的活成员」的答复,只看组长分不开这两形(codex r1 C1 [high]
|
|
72
|
+
* 验真:组长已死未收割 + 活后代被拒 ⇒ 改判就把真被拒读成已不在)。落地也**不**等于「组已空」(组信号任一成员收到即成功,车PA)——
|
|
73
|
+
* 所以 `already_gone` 只有 ESRCH 这一个来源;梯子那一枪先落地、组长还没收割的那一拍,darwin 上如实答 `no_pgid`(未确认),linux 答 `killed`;
|
|
69
74
|
* · win32 ⇒ 恒 `no_pgid`(core 契约:win32 无同步硬收 —— 没有进程组,树杀要一个延后的发现步,同步 `taskkill` 会杀掉启动器而
|
|
70
75
|
* 留下 Git Bash 的作业组、却自称 `killed`)。
|
|
71
76
|
* 「这个 pid 还是不是我的」由**调用方**先判(观察到退出 ⇒ 不许再发,pid 可能已经是别人的):本函数只管打出去的那一枪。永不抛。
|
|
@@ -149,6 +149,11 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
149
149
|
stderr: string;
|
|
150
150
|
exitCode: number;
|
|
151
151
|
}, ExecutionError>>;
|
|
152
|
+
/** 文件面探针的 exec(车PL 验修 ③):只用绝对路径,**不进工作目录**、没有 cwd 守卫 —— 工作目录进不去时探针照样答真话(`permission_denied` /
|
|
153
|
+
* `not_found` 由命令自己的 stderr 归),core 的 `isDeadCwd` 不会把「进不去」读成「没了」。 */
|
|
154
|
+
private fsExec;
|
|
155
|
+
/** buffered exec 的唯一体:`call` 定了整串、stderr 的 started 行剥法与收口(`remote-shell.ts` `createCwdGuardedCall`)。 */
|
|
156
|
+
private execCall;
|
|
152
157
|
execStream(command: string, options?: ExecStreamOptions): AsyncIterable<OutputChunk>;
|
|
153
158
|
absolutePath(p: string): Promise<Result<string, FileError>>;
|
|
154
159
|
joinPath(parts: string[]): Promise<Result<string, FileError>>;
|
|
@@ -230,11 +235,13 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
230
235
|
* 远端退出码与分流的那根旗标,即「一条以 `-x` 开头的命令能静默把本修关回去」。真机验证 `--` 之后的
|
|
231
236
|
* 参数原样落到设备 shell(`adb shell -T -- '-x'` ⇒ 设备答 `/system/bin/sh: -c: requires an argument`)。
|
|
232
237
|
*
|
|
233
|
-
* 命令永远是**最后一个**参数,整条包在一个 argv 元素里
|
|
234
|
-
*
|
|
238
|
+
* 命令永远是**最后一个**参数,整条包在一个 argv 元素里 —— 设备侧 `sh -c` 收全串,即 {@link buildCwdEnvCommand} /
|
|
239
|
+
* {@link createCwdGuardedCall} 组出的那一串(带工作目录时含 cwd 守卫前缀:cd 失败 ⇒ 整条不跑)。
|
|
235
240
|
*/
|
|
236
241
|
private deviceShellArgs;
|
|
237
|
-
|
|
242
|
+
/** 本腿只定工作目录(缺省 cwd 规则 + 绝对化);串的形状归 {@link buildCwdEnvCommand} / {@link createCwdGuardedCall}(S-410:cwd 不可用 ⇒ 命令不跑;
|
|
243
|
+
* 车PL:buffered `exec` 上守卫触发 ⇒ `spawn_error`)。 */
|
|
244
|
+
private workingDirOf;
|
|
238
245
|
}
|
|
239
246
|
/**
|
|
240
247
|
* `ExecutionEnvFactory` for the ADB backend — one fresh (unconnected) env per task against the same device
|
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
5
|
+
import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
6
6
|
import { signalNumber } from "./host-platform.js";
|
|
7
7
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
8
8
|
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
@@ -147,15 +147,21 @@ export class RemoteAdbExecutionEnv {
|
|
|
147
147
|
return this.destroy();
|
|
148
148
|
}
|
|
149
149
|
async exec(command, options) {
|
|
150
|
+
return this.execCall(createCwdGuardedCall("adb", command, this.workingDirOf(options?.cwd), options?.env), options);
|
|
151
|
+
}
|
|
152
|
+
fsExec(command, abortSignal) {
|
|
153
|
+
return this.execCall(createCwdGuardedCall("adb", command, undefined, undefined), { abortSignal });
|
|
154
|
+
}
|
|
155
|
+
async execCall(call, options) {
|
|
150
156
|
const ready = await this.ensureConnected();
|
|
151
157
|
if (!ready.ok)
|
|
152
158
|
return { ok: false, error: lazyConnectExecError(ready.error) };
|
|
153
|
-
const
|
|
154
|
-
const r = await this.adb(this.deviceShellArgs(wrapped), {
|
|
159
|
+
const r = await this.adb(this.deviceShellArgs(call.command), {
|
|
155
160
|
signal: options?.abortSignal,
|
|
156
161
|
timeoutMs: options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs,
|
|
157
162
|
onStdout: options?.onStdout,
|
|
158
163
|
onStderr: options?.onStderr,
|
|
164
|
+
stderrFilter: call,
|
|
159
165
|
});
|
|
160
166
|
if (!r.ok)
|
|
161
167
|
return { ok: false, error: r.error };
|
|
@@ -163,7 +169,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
163
169
|
this.connected = false;
|
|
164
170
|
return { ok: false, error: new ExecutionError("transport_lost", `adb transport lost mid-command: ${r.value.stderr.trim().slice(0, 200)} — reconnect + retry per idempotency`) };
|
|
165
171
|
}
|
|
166
|
-
return ok(r.value);
|
|
172
|
+
return call.settle(ok(r.value));
|
|
167
173
|
}
|
|
168
174
|
async *execStream(command, options) {
|
|
169
175
|
const ready = await this.ensureConnected();
|
|
@@ -171,7 +177,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
171
177
|
throw ready.error;
|
|
172
178
|
if (this.destroyed)
|
|
173
179
|
throw new RemoteExecutionError("connect_failed", "execution env already destroyed");
|
|
174
|
-
const wrapped =
|
|
180
|
+
const wrapped = buildCwdEnvCommand(command, this.workingDirOf(options?.cwd), options?.env, "raw").command;
|
|
175
181
|
const child = this.spawnFn(this.cfg.adbPath, this.deviceShellArgs(wrapped), { stdio: ["ignore", "pipe", "pipe"] });
|
|
176
182
|
const queue = [];
|
|
177
183
|
let finished = false;
|
|
@@ -327,7 +333,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
327
333
|
const abs = this.resolve(p);
|
|
328
334
|
const dir = path.posix.dirname(abs);
|
|
329
335
|
if (dir && dir !== "." && dir !== "/") {
|
|
330
|
-
const mk = await this.
|
|
336
|
+
const mk = await this.fsExec(`mkdir -p ${shellQuote(dir)}`, abortSignal);
|
|
331
337
|
if (!mk.ok)
|
|
332
338
|
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
333
339
|
if (mk.value.exitCode !== 0)
|
|
@@ -374,7 +380,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
374
380
|
posixFsInst;
|
|
375
381
|
get posixFs() {
|
|
376
382
|
return (this.posixFsInst ??= createPosixShellFs({
|
|
377
|
-
run: (cmd, abortSignal) => this.
|
|
383
|
+
run: (cmd, abortSignal) => this.fsExec(cmd, abortSignal),
|
|
378
384
|
resolve: (p) => this.resolve(p),
|
|
379
385
|
tempDir: () => this.cwd,
|
|
380
386
|
}));
|
|
@@ -456,14 +462,19 @@ export class RemoteAdbExecutionEnv {
|
|
|
456
462
|
outTail.push(d);
|
|
457
463
|
opts?.onStdout?.(d.toString());
|
|
458
464
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
465
|
+
const takeStderr = (bytes) => {
|
|
466
|
+
if (bytes.length === 0)
|
|
467
|
+
return;
|
|
468
|
+
errBuf.push(bytes);
|
|
469
|
+
opts?.onStderr?.(bytes.toString());
|
|
470
|
+
};
|
|
471
|
+
child.stderr.on("data", (d) => takeStderr(opts?.stderrFilter ? opts.stderrFilter.filterStderr(d) : d));
|
|
463
472
|
child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `adb spawn failed (is '${this.cfg.adbPath}' installed?): ${e.message}`, e) }));
|
|
464
473
|
child.on("close", (code, signal) => {
|
|
465
474
|
if (forceSettleTimer)
|
|
466
475
|
clearTimeout(forceSettleTimer);
|
|
476
|
+
if (opts?.stderrFilter)
|
|
477
|
+
takeStderr(opts.stderrFilter.endStderr());
|
|
467
478
|
const err = errBuf.result();
|
|
468
479
|
const stderr = markTruncated(err.text, err.droppedBytes);
|
|
469
480
|
const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0);
|
|
@@ -481,16 +492,9 @@ export class RemoteAdbExecutionEnv {
|
|
|
481
492
|
deviceShellArgs(command) {
|
|
482
493
|
return ["-s", this.cfg.serial, "shell", "-T", "--", command];
|
|
483
494
|
}
|
|
484
|
-
|
|
485
|
-
const parts = [];
|
|
495
|
+
workingDirOf(cwd) {
|
|
486
496
|
const dir = cwd ?? this.cwd;
|
|
487
|
-
|
|
488
|
-
parts.push(`cd ${shellQuote(this.resolve(dir))} &&`);
|
|
489
|
-
if (env)
|
|
490
|
-
for (const [k, v] of Object.entries(env))
|
|
491
|
-
parts.push(`export ${k}=${shellQuote(v)};`);
|
|
492
|
-
parts.push(command);
|
|
493
|
-
return parts.join(" ");
|
|
497
|
+
return dir ? this.resolve(dir) : undefined;
|
|
494
498
|
}
|
|
495
499
|
}
|
|
496
500
|
export function adbExecutionEnvFactory(config) {
|
|
@@ -10,8 +10,7 @@ import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
|
|
|
10
10
|
import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
|
|
11
11
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
12
12
|
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
|
|
13
|
-
import {
|
|
14
|
-
import { allowlistedEnv, scrubServerSecrets } from "../server-secret-env.js";
|
|
13
|
+
import { allowlistedEnv, hostSubprocessEnv } from "../server-secret-env.js";
|
|
15
14
|
import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
|
|
16
15
|
const PROVIDER = "host";
|
|
17
16
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -335,14 +334,9 @@ export class RemoteHostExecutionEnv {
|
|
|
335
334
|
await fs.rm(state.dir, { recursive: true, force: true }).catch(() => { });
|
|
336
335
|
},
|
|
337
336
|
killNow(state) {
|
|
338
|
-
if (state.hardKilled === true)
|
|
339
|
-
return "already_gone";
|
|
340
337
|
if (state.child.exitCode !== null || state.child.signalCode !== null)
|
|
341
338
|
return "already_gone";
|
|
342
|
-
|
|
343
|
-
if (outcome === "killed")
|
|
344
|
-
state.hardKilled = true;
|
|
345
|
-
return outcome;
|
|
339
|
+
return hardKillGroupNow(state.pid);
|
|
346
340
|
},
|
|
347
341
|
};
|
|
348
342
|
}
|
|
@@ -1322,9 +1316,7 @@ export class RemoteHostExecutionEnv {
|
|
|
1322
1316
|
base = allowlistedEnv(mode, process.env);
|
|
1323
1317
|
}
|
|
1324
1318
|
else {
|
|
1325
|
-
|
|
1326
|
-
base = scrubServerSecrets(process.env, findings);
|
|
1327
|
-
recordSecretEnvScrub("server.remote-env-host.inherit-scrub", findings, []);
|
|
1319
|
+
base = hostSubprocessEnv({ site: "server.remote-env-host.inherit-scrub", allow: [] });
|
|
1328
1320
|
}
|
|
1329
1321
|
if (this.cfg.env)
|
|
1330
1322
|
Object.assign(base, this.cfg.env);
|
|
@@ -245,6 +245,11 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
245
245
|
shellId: string;
|
|
246
246
|
};
|
|
247
247
|
}, ExecutionError>>;
|
|
248
|
+
/** 文件面探针的 exec(车PL 验修 ③):只用绝对路径,**不进工作目录**、没有 cwd 守卫 —— 工作目录进不去时探针照样答真话(`permission_denied` /
|
|
249
|
+
* `not_found` 由命令自己的 stderr 归),core 的 `isDeadCwd` 不会把「进不去」读成「没了」。 */
|
|
250
|
+
private fsExec;
|
|
251
|
+
/** buffered exec 的唯一体:`call` 定了整串、stderr 的 started 行剥法与收口(`remote-shell.ts` `createCwdGuardedCall`)。 */
|
|
252
|
+
private execCall;
|
|
248
253
|
/**
|
|
249
254
|
* LSP surface (lsp/manager.ts `isLspCapable` duck-type, [ref] §13.1): cluster-internal address for a
|
|
250
255
|
* port on the sandbox pod. The E2B counterpart returns a public proxy host; a pod has none — the WORKER
|
|
@@ -333,7 +338,9 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
333
338
|
}): Promise<Result<string, FileError>>;
|
|
334
339
|
/** exec with the data-transfer bound (large reads/writes legitimately run longer than a control op). */
|
|
335
340
|
private execData;
|
|
336
|
-
|
|
341
|
+
/** 本腿只定工作目录(缺省 cwd 规则 + 绝对化);串的形状归 {@link buildCwdEnvCommand} / {@link createCwdGuardedCall}(S-410:cwd 不可用 ⇒ 命令不跑;
|
|
342
|
+
* 车PL:buffered `exec` 上守卫触发 ⇒ `spawn_error`)。 */
|
|
343
|
+
private workingDirOf;
|
|
337
344
|
}
|
|
338
345
|
/**
|
|
339
346
|
* `ExecutionEnvFactory` for the k8s pod-sandbox backend — one fresh (unconnected) env per task; the pod is
|