@sema-agent/server 7.127.0 → 7.127.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 CHANGED
@@ -7,6 +7,10 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.127.1 —— Windows 宿主车道:策略链读 `cdPath` 不再因 shell 未落定而拒每条 Bash(行为修复,升级零动作)
11
+
12
+ 升级零动作:无 DDL、无配置变化、无 wire 变化。**谁受影响**:Windows 宿主部署里 server 进程环境找不到 Git Bash 的(PATH 无 Git、`ProgramFiles` 不对、经 ssh 起服白名单环境等)—— 7.127.0 及之前每条 Bash 都被策略链拒成「policy check threw / aborted」且真因被吞;本版起策略链读 `cdPath` 不再碰 shell,命令真要跑时才以真因拒(装 Git for Windows 或设 `HOST_SHELL_PATH`)。装了 Git Bash 的普通部署行为不变。回滚到 7.127.0 安全。
13
+
10
14
  ## 7.127.0 —— 引擎 7.50.2(精确钉)· 🔴 `tool_end.output` 带图:SSE 整块、账本只存描述符 · 新帧 `task_started`(SSE 与账本同在)· 🔴 e2b / k8s 车道不再挂 LSP · 记忆整理按人的路径规则判写(自动腿)· 附件请求键 400 · `~/…` 规则在无家目录车道上转问
11
15
 
12
16
  1. **`tool_end.output` 两形(BREAKING,消费方)**:`output` 可以是串,也可以是块数组;块里可有图像 / 文档 `{ type, data, mimeType }`。
@@ -18,6 +18,9 @@
18
18
  * pair from case-sensitive Object.assign reaches CreateProcess as ONE undefined-which
19
19
  * entry). Canonical casing = the first-seen key (process.env's native casing wins since the
20
20
  * inherit base is spread first).
21
+ * - layering = env COMPOSITION is a pure function of config + process.env and never reads the resolved shell;
22
+ * the shell belongs to the SPAWN layer only ({@link pinHostShell}, applied by the spawn sites after
23
+ * `shellReady()`). A read surface (`cdPath`) therefore never depends on resolution having settled.
21
24
  *
22
25
  * ⚠️ The win32 branches are UNVERIFIED on a real machine until S5 (Windows CI runner) — design D5 discipline:
23
26
  * structural tests only on mac/Linux; behavior-level bite happens on the first Windows-runner green.
@@ -39,10 +42,22 @@ export interface HostShell {
39
42
  * read the cached value synchronously via {@link hostShell}.
40
43
  */
41
44
  export declare function resolveHostShell(): Promise<HostShell>;
42
- /** Synchronous read of the resolved shell — valid only AFTER {@link resolveHostShell} settled (the spawn sites
43
- * are all behind `shellReady()`, which awaits it). POSIX never needs the async step. Throws on a mis-ordered
44
- * call (a programming bug, not an operator condition — fail loud, not /bin/sh-on-windows). */
45
+ /** Synchronous read of the resolved shell — a SPAWN-layer read, valid only AFTER {@link resolveHostShell} settled
46
+ * (the spawn sites are all behind `shellReady()`, which awaits it). It is never an env-composition or read-surface
47
+ * input (see {@link pinHostShell}). POSIX never needs the async step.
48
+ * win32, not settled: a resolve that FAILED throws that failure (the real cause + the way out, memoised above);
49
+ * only a resolve that never ran / is still in flight reaches the ordering sentence — on the spawn paths that arm
50
+ * is unreachable (they await the resolve first), so it stays as the loud guard against a future mis-ordered caller
51
+ * (a programming bug, not an operator condition — fail loud, not /bin/sh-on-windows). */
45
52
  export declare function hostShell(): HostShell;
53
+ /**
54
+ * The SPAWN layer's one env edit: win32 pins `SHELL` to the resolved Git Bash (subshell scripts read `$SHELL`).
55
+ * Takes the {@link HostShell} the caller already read via {@link hostShell} — so a spawn site cannot reach the pin
56
+ * without a resolved shell (the type is the ordering guard), and env COMPOSITION never needs one. Mutates and
57
+ * returns `env` (call it BEFORE {@link collapseWin32EnvKeys} so an inherited case-variant `Shell` folds into one
58
+ * entry). POSIX: identity — `SHELL` is whatever the composed env already carried (byte-identical to pre-Windows).
59
+ */
60
+ export declare function pinHostShell(env: NodeJS.ProcessEnv, sh: HostShell): NodeJS.ProcessEnv;
46
61
  /** test-only: reset the shell caches (each test can re-drive resolution). */
47
62
  export declare function resetHostShellCacheForTest(): void;
48
63
  /** Per-platform spawn options for a group-killable child (design D3).
@@ -4,6 +4,7 @@ export const IS_WIN32 = process.platform === "win32";
4
4
  const POSIX_SH_FALLBACK = { shell: "/bin/sh", args: ["-c"] };
5
5
  let cachedWinShell;
6
6
  let cachedPosixShell;
7
+ let winShellFailure;
7
8
  export async function resolveHostShell() {
8
9
  if (!IS_WIN32) {
9
10
  if (cachedPosixShell)
@@ -15,26 +16,38 @@ export async function resolveHostShell() {
15
16
  if (cachedWinShell)
16
17
  return cachedWinShell;
17
18
  const custom = process.env.HOST_SHELL_PATH;
18
- const cfg = await getShellConfig(custom || undefined);
19
- if (!cfg.ok) {
20
- throw new Error(`host lane needs a POSIX shell on Windows and none was found${custom ? ` (HOST_SHELL_PATH=${custom})` : ""}: ` +
21
- `${cfg.error.message}. Install Git for Windows (https://gitforwindows.org) or set HOST_SHELL_PATH to a bash.exe. ` +
22
- `Note: System32\\bash.exe is the WSL launcher, not a Windows bash — it is deliberately rejected.`);
19
+ try {
20
+ const cfg = await getShellConfig(custom || undefined);
21
+ if (!cfg.ok) {
22
+ throw new Error(`host lane needs a POSIX shell on Windows and none was found${custom ? ` (HOST_SHELL_PATH=${custom})` : ""}: ` +
23
+ `${cfg.error.message}. Install Git for Windows (https://gitforwindows.org) or set HOST_SHELL_PATH to a bash.exe. ` +
24
+ `Note: System32\\bash.exe is the WSL launcher, not a Windows bash — it is deliberately rejected.`);
25
+ }
26
+ cachedWinShell = { shell: cfg.value.shell, args: cfg.value.args };
27
+ return cachedWinShell;
28
+ }
29
+ catch (e) {
30
+ winShellFailure = e instanceof Error ? e : new Error(String(e));
31
+ throw winShellFailure;
23
32
  }
24
- cachedWinShell = { shell: cfg.value.shell, args: cfg.value.args };
25
- return cachedWinShell;
26
33
  }
27
34
  export function hostShell() {
28
35
  if (!IS_WIN32) {
29
36
  return cachedPosixShell ?? POSIX_SH_FALLBACK;
30
37
  }
31
- if (!cachedWinShell)
32
- throw new Error("host shell not resolved yet — resolveHostShell() must settle before any spawn (shellReady ordering bug)");
33
- return cachedWinShell;
38
+ if (cachedWinShell)
39
+ return cachedWinShell;
40
+ throw winShellFailure ?? new Error("host shell not resolved yet — resolveHostShell() must settle before any spawn (shellReady ordering bug)");
41
+ }
42
+ export function pinHostShell(env, sh) {
43
+ if (IS_WIN32)
44
+ env.SHELL = sh.shell;
45
+ return env;
34
46
  }
35
47
  export function resetHostShellCacheForTest() {
36
48
  cachedWinShell = undefined;
37
49
  cachedPosixShell = undefined;
50
+ winShellFailure = undefined;
38
51
  }
39
52
  export function spawnGroupOptions() {
40
53
  return IS_WIN32 ? { detached: false, windowsHide: true } : { detached: true };
@@ -96,8 +96,10 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
96
96
  readonly homeDir?: string | undefined;
97
97
  /**
98
98
  * core 7.49.0 #1554:shell 子进程**真拿到的** `CDPATH` 条目(`[]` = 没有)—— 删除守卫按它解裸 `cd <dir>`。本车道的命令就是本机子进程,
99
- * env 是本类自己组合的(继承姿态 + `cfg.env`),所以说得出:读同一只组合({@link childEnv},不披露),切法与 core `NodeExecutionEnv` 同
100
- * (缺席 / 空串 ⇒ `[]`,否则按 `:` 切)。逐命令的 `options.env` 由引擎自己交,它知道。
99
+ * env 是本类自己组合的(继承姿态 + `cfg.env`),所以说得出:读同一只组合({@link childEnv} 的**读面**,不披露、不钉 shell),切法与 core
100
+ * `NodeExecutionEnv` 同(缺席 / 空串 ⇒ `[]`,否则按 `:` 切)。逐命令的 `options.env` 由引擎自己交,它知道。
101
+ * 🔴 读面**不依赖 shell 落定**:core 判 Bash 的策略链在任何 exec 之前就读它(shell 解析在 exec 路径上才发生),所以这里不能碰
102
+ * {@link hostShell}(win32 上未落定同步抛 —— 修前每条 Bash 的策略链都因此被拒)。
101
103
  */
102
104
  get cdPath(): readonly string[];
103
105
  private readonly workspaceDir;
@@ -422,11 +424,16 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
422
424
  * source of the shape rule) minus the config catalog's `secret`-typed names (`scrubServerSecrets`, 7.99.0 F7). `"all"` = full inherit (opt-out); a `string[]` = strict allowlist. Legacy `inheritProcessEnv`
423
425
  * maps `true`→`"all"` / `false`→`[]`. */
424
426
  private inheritMode;
425
- /** 每次 spawn 交给子进程的 env(组合 + 剥除披露)。 */
427
+ /** 每次 spawn 交给子进程的 env(组合 + 剥除披露 + win32 的 SHELL 钉)。`sh` = 调用点刚从 {@link hostShell} 读到的那只(spawn 路径
428
+ * 都在 `shellReady()` 之后):没有已落定的 shell 就到不了这条腿 —— 类型即次序守卫。 */
426
429
  private mergeEnv;
427
430
  /**
428
- * 子进程 env 的**唯一组合**。`disclose` = 这一次是不是真要交给子进程(是 ⇒ scrub 腿经 `hostSubprocessEnv` 记剥除;否 ⇒ 只读组合,
429
- * 不计数 —— {@link cdPath} 这类「子进程会拿到什么」的读者走这一臂,一次没发生的 spawn 不进剥除账)。
431
+ * 子进程 env 的**唯一组合**,分两层(一条律:**env 组合是纯函数,shell 只属于 spawn**):
432
+ * · **组合层**(恒有)—— 继承姿态 + `cfg.env` + perCommand + win32 大小写折叠。**不碰 shell**,所以读面(如 {@link cdPath})任何时候
433
+ * 都能读,不依赖 shell 是否已落定。
434
+ * · **spawn 层**(`spawnShell` 在场)—— 这一次是真要交给子进程:scrub 腿经 `hostSubprocessEnv` 记剥除(`spawnShell` 缺席 ⇒ 只读组合,
435
+ * 不计数:一次没发生的 spawn 不进剥除账),win32 上把 `SHELL` 钉到已解析的 Git Bash({@link pinHostShell},在大小写折叠**之前**)。
436
+ * 披露与钉同一个开关(`spawnShell`)—— 「真 spawn」只有一种说法,不再有 `disclose` 与钉各自一个布尔。
430
437
  */
431
438
  private childEnv;
432
439
  private fileError;
@@ -7,7 +7,7 @@ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdi
7
7
  import { StringDecoder } from "node:string_decoder";
8
8
  import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-shell.js";
9
9
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
10
- import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
10
+ import { resolveHostShell, hostShell, pinHostShell, 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, NodeExecutionEnv, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
13
  import { allowlistedEnv, composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
@@ -58,7 +58,7 @@ export class RemoteHostExecutionEnv {
58
58
  cwd;
59
59
  homeDir;
60
60
  get cdPath() {
61
- const value = this.childEnv(undefined, false).CDPATH;
61
+ const value = this.childEnv(undefined, undefined).CDPATH;
62
62
  return value === undefined || value === "" ? [] : value.split(":");
63
63
  }
64
64
  workspaceDir;
@@ -233,7 +233,7 @@ export class RemoteHostExecutionEnv {
233
233
  const sh = hostShell();
234
234
  child = spawn(sh.shell, [...sh.args, command], {
235
235
  cwd: self.resolve(options?.cwd ?? self.cwd),
236
- env: self.mergeEnv(options?.env),
236
+ env: self.mergeEnv(options?.env, sh),
237
237
  ...spawnGroupOptions(),
238
238
  stdio: ["ignore", outFh.fd, errFh.fd],
239
239
  });
@@ -412,7 +412,7 @@ export class RemoteHostExecutionEnv {
412
412
  const sh = hostShell();
413
413
  const child = spawn(sh.shell, [...sh.args, command], {
414
414
  cwd,
415
- env: this.mergeEnv(options?.env),
415
+ env: this.mergeEnv(options?.env, sh),
416
416
  ...spawnGroupOptions(),
417
417
  });
418
418
  const ticket = adoptHostForegroundChild(child);
@@ -672,7 +672,7 @@ export class RemoteHostExecutionEnv {
672
672
  try {
673
673
  child = spawn(sh.shell, [...sh.args, command], {
674
674
  cwd,
675
- env: this.mergeEnv(options?.env),
675
+ env: this.mergeEnv(options?.env, sh),
676
676
  ...spawnGroupOptions(),
677
677
  stdio: ["ignore", spool.fds[0], spool.fds[1]],
678
678
  });
@@ -814,7 +814,7 @@ export class RemoteHostExecutionEnv {
814
814
  let wake;
815
815
  const signalReady = () => { const w = wake; wake = undefined; w?.(); };
816
816
  const sh = hostShell();
817
- const child = spawn(sh.shell, [...sh.args, command], { cwd, env: this.mergeEnv(options?.env), ...spawnGroupOptions() });
817
+ const child = spawn(sh.shell, [...sh.args, command], { cwd, env: this.mergeEnv(options?.env, sh), ...spawnGroupOptions() });
818
818
  const ticket = adoptHostForegroundChild(child);
819
819
  let pipeDestroyTimer;
820
820
  const kill = () => {
@@ -1319,10 +1319,11 @@ export class RemoteHostExecutionEnv {
1319
1319
  return "all";
1320
1320
  return "scrub";
1321
1321
  }
1322
- mergeEnv(perCommand) {
1323
- return this.childEnv(perCommand, true);
1322
+ mergeEnv(perCommand, sh) {
1323
+ return this.childEnv(perCommand, sh);
1324
1324
  }
1325
- childEnv(perCommand, disclose) {
1325
+ childEnv(perCommand, spawnShell) {
1326
+ const disclose = spawnShell !== undefined;
1326
1327
  const mode = this.inheritMode();
1327
1328
  let base;
1328
1329
  if (mode === "all")
@@ -1340,8 +1341,8 @@ export class RemoteHostExecutionEnv {
1340
1341
  Object.assign(base, this.cfg.env);
1341
1342
  if (perCommand)
1342
1343
  Object.assign(base, perCommand);
1343
- if (process.platform === "win32")
1344
- base.SHELL = hostShell().shell;
1344
+ if (spawnShell)
1345
+ pinHostShell(base, spawnShell);
1345
1346
  return collapseWin32EnvKeys(base);
1346
1347
  }
1347
1348
  fileError(e, p, abortSignal) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.127.0",
3
+ "version": "7.127.1",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",