@sema-agent/server 7.126.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.
Files changed (79) hide show
  1. package/MIGRATION.md +38 -0
  2. package/USAGE.md +86 -8
  3. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +15 -5
  4. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +28 -1
  5. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  6. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  7. package/deploy/sema-up/chart/values.yaml +16 -6
  8. package/deploy/sema-up/kube-up.sh +10 -4
  9. package/deploy/sema-up/lib/node-addrs.sh +42 -0
  10. package/deploy/sema-up/stack/sema-stack-cluster.yaml +20 -1
  11. package/dist/audit.js +1 -0
  12. package/dist/boot/execution-env.js +17 -4
  13. package/dist/boot/leader.d.ts +2 -1
  14. package/dist/boot/memory-consolidation.d.ts +37 -3
  15. package/dist/boot/memory-consolidation.js +18 -2
  16. package/dist/boot/runner-deps.d.ts +2 -1
  17. package/dist/boot/session-faces.d.ts +2 -1
  18. package/dist/boot/stage-06-runners.js +5 -2
  19. package/dist/config-catalog.js +3 -1
  20. package/dist/config-types.d.ts +5 -1
  21. package/dist/config.js +2 -0
  22. package/dist/execution-lane-caps.d.ts +12 -0
  23. package/dist/execution-lane-caps.js +16 -2
  24. package/dist/fleet/subagent-tail-bus.js +15 -5
  25. package/dist/http/admission.js +5 -1
  26. package/dist/http/durable-run-leg.js +1 -3
  27. package/dist/http/resume-legs.js +15 -6
  28. package/dist/http/route-ctx.d.ts +7 -1
  29. package/dist/http/routes/runs.js +23 -1
  30. package/dist/http/routes/tasks.js +17 -8
  31. package/dist/http/routes/workflows.js +8 -0
  32. package/dist/http/server.js +1 -3
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +1 -0
  35. package/dist/lsp/manager.js +1 -1
  36. package/dist/observability/fail-open.d.ts +16 -0
  37. package/dist/observability/fail-open.js +16 -0
  38. package/dist/observability/metrics.js +1 -1
  39. package/dist/observability/run-terminal.js +3 -0
  40. package/dist/plugins/caching-session-store.d.ts +39 -22
  41. package/dist/plugins/caching-session-store.js +16 -9
  42. package/dist/plugins/host-platform.d.ts +18 -3
  43. package/dist/plugins/host-platform.js +23 -10
  44. package/dist/plugins/k8s-exec-protocol.js +3 -0
  45. package/dist/plugins/one-time-migrations.d.ts +28 -4
  46. package/dist/plugins/one-time-migrations.js +22 -4
  47. package/dist/plugins/remote-env-host.d.ts +23 -6
  48. package/dist/plugins/remote-env-host.js +21 -12
  49. package/dist/plugins/remote-env-k8s.d.ts +40 -5
  50. package/dist/plugins/remote-env-k8s.js +119 -25
  51. package/dist/plugins/session-store.d.ts +4 -3
  52. package/dist/plugins/session-store.js +1 -1
  53. package/dist/plugins/store-backend.d.ts +12 -5
  54. package/dist/plugins/store-backend.js +12 -3
  55. package/dist/plugins/store-contracts.d.ts +3 -0
  56. package/dist/plugins/store-open-wait.js +3 -0
  57. package/dist/request-key-closure.d.ts +20 -7
  58. package/dist/request-key-closure.js +22 -6
  59. package/dist/run-acceptance.d.ts +51 -0
  60. package/dist/run-acceptance.js +27 -0
  61. package/dist/runs.d.ts +7 -11
  62. package/dist/runs.js +5 -16
  63. package/dist/sandbox-egress-policy.d.ts +61 -0
  64. package/dist/sandbox-egress-policy.js +235 -0
  65. package/dist/session-error-wire.d.ts +2 -2
  66. package/dist/session-titler.d.ts +5 -3
  67. package/dist/trace/core-keyset-guard.d.ts +3 -2
  68. package/dist/trace/ledger-events.d.ts +9 -3
  69. package/dist/trace/ledger-sink.d.ts +7 -0
  70. package/dist/trace/ledger-sink.js +14 -4
  71. package/dist/trace/project.d.ts +49 -8
  72. package/dist/trace/project.js +39 -7
  73. package/dist/trace/self-minted-wire.d.ts +19 -29
  74. package/dist/trace/self-minted-wire.js +2 -9
  75. package/dist/trace/tool-output-media.d.ts +26 -0
  76. package/dist/trace/tool-output-media.js +57 -0
  77. package/dist/trace/wire-projection-faces.d.ts +7 -1
  78. package/dist/trace/wire-projection-faces.js +2 -1
  79. package/package.json +3 -3
@@ -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 };
@@ -1,5 +1,8 @@
1
1
  export function applyPodSpecPatch(pod, patch) {
2
2
  assertHardeningOnly(patch.podSecurityContext, "podSecurityContext");
3
+ if (patch.podSecurityContext !== undefined && "sysctls" in patch.podSecurityContext) {
4
+ throw new Error("podSpecPatch.podSecurityContext rejected — `sysctls` is not a patch key (the pod's sysctls are minted only by the lane's IPv6 posture, K8S_SANDBOX_IPV6)");
5
+ }
3
6
  assertHardeningOnly(patch.containerSecurityContext, "containerSecurityContext");
4
7
  if (patch.labels) {
5
8
  const identity = {};
@@ -12,8 +12,8 @@
12
12
  * **≥** 某条未发布形的 `introducedIn` 而两格仍是 `null` ⇒ 红(发布准备提交就是回填时刻,忘了就当场红;版本更低 = 还在开发,不红)。
13
13
  * 最后一条删掉时,整个文件连同 `StoreBackend.runOneTimeMigrations()`(npm 公面上的接口成员)、`NO_SQL_MIGRATIONS`、
14
14
  * `logOneTimeMigrations`、三只后端与两只测试夹具上的实现、`boot/stores.ts` 的调用点一起删 —— 唯一例外是退役 schema 的拒启臂
15
- * ({@link assertRetiredSchemaAbsent} 与它的错误类型、两件退役 schema 的坐标):它没有窗,跳版升级的库永远可能出现,删最后一条的车要把它搬进启动期对账
16
- * (`schema-consistency.ts` 一族)并保住它先于一切改写的位置,不要随文件删掉。
15
+ * ({@link assertRetiredSchemaAbsent}、探针口 {@link assertRetiredSchemaAbsentOnPool} 与它们的错误类型、两件退役 schema 的坐标):它没有窗,跳版升级的库永远可能出现,
16
+ * 删最后一条的车要把它搬进启动期对账(`schema-consistency.ts` 一族)并保住它先于一切建表与改写的位置(现位:`store-backend.ts` 里 SQL 两后端的 `ensureSchema()` 自身,探针先于它的第一条 CREATE),不要随文件删掉。
17
17
  * 其余文件一律只写「窗口见本文件登记表」,不各写一个日期或版本 —— S-590 之前本文件头写死了一个删除版本号(7.75.0),
18
18
  * 连同 11 个文件里的复述共 23 行;那一版过去了 28 个 minor 也没删,7.93.5 反倒往本文件里又加了 M4。
19
19
  *
@@ -33,6 +33,10 @@
33
33
  * 7.64.0–7.71.0 恒带两者),所以**旧拼法不另扫**:一条对账本 `task_event` 的无索引整表扫正是当年被判不许成为每次启动固定开销的东西
34
34
  * (账本是全站最大的表,PG 池级 `statement_timeout` 会把它切成拒启、每次启动重演)。两件任一在场 ⇒ 这只库没有经过带迁移的版本 ⇒ 旧拼法的改写同样没跑过 ⇒ 拒句一并点名。
35
35
  * 出路:先在 7.125.x(最后一个带这三条迁移的版)上起服一次让它们跑完,再升。
36
+ * **判据一只、读一处**:判据只有 {@link assertRetiredSchemaAbsent} 一只,唯一的读处是**探针口**({@link assertRetiredSchemaAbsentOnPool}):库一连上就探,先于 `ensureSchema()`
37
+ * 发出的任何一条 CREATE —— 一次会拒的起服不许先在库里建出几十张本版新表(`task_run` 还带着新列),否则拒句里「没动过库」就是假话,而按拒句指路去 7.125.x 起服的人会撞上
38
+ * 「库比 7.125.x 新」的第二道拒。探针与建表的先后只写在 `store-backend.ts` 里 SQL 两后端的 `ensureSchema()` 自身(装配处包一次)。迁移段入口({@link runSqlOneTimeMigrations})**不再问**这一问:
39
+ * 它只在 `ensureSchema()` 之后被起服序列调用,一个事实不在两处各读一遍;嵌入方走公开序列 `createStoreBackend()` → `ensureSchema()` → `runOneTimeMigrations()` 即得到这一问,零动作。
36
40
  * 残余(如实):运维在第一次起带迁移的版本之前就把两件退役 schema 都手工删掉的库,残留的旧拼法由读腿既有的 `server.trace.gate-outcome-defective`
37
41
  * (整条 withhold + 计数 + 一次 warn)接住;local 文件车道没有表与列,没有这一问的探针 —— 引擎 File 店里已决 checkpoint 的记录按契约物理不可变,本仓没有合法缝改写它们
38
42
  * (下游不改上游盘上格式),同由那条读腿接住。
@@ -125,6 +129,7 @@
125
129
  */
126
130
  import type { Pool as MysqlPool } from "mysql2/promise";
127
131
  import type { Pool as PgPool } from "pg";
132
+ import { StoreBootRefusalError } from "./store-contracts.js";
128
133
  import { type UndurableScopeRuleRow, type UnreadableRuleBucket } from "./permission-rule-store-sql.js";
129
134
  /** 迁移读数(启动日志一行写全;测试按它断言,不靠日志文本)。 */
130
135
  export interface OneTimeMigrationReport {
@@ -200,13 +205,31 @@ export interface MigrationLogSink {
200
205
  /**
201
206
  * 跳版升级的库的拒启:退役 schema(快照索引表 / 单座列)任一仍在。**具名类型**:锚类型不锚文案(日志消费端与测试按 `code` / 类型分派);
202
207
  * 码取列册探针同一个 `config.schema_mismatch`,运维面一个拒启码一个读法。拒句点名在场的坐标、说明第三条(旧拼法改写)同样没跑过、给出路。
208
+ * 继承 {@link StoreBootRefusalError}:这是一句**确定**的拒启表态(再等也等不来另一个答案)—— `ensureSchema()` 里它不被 SQL 开库的等待窗重试、不被改写成「库打不开」的拒句。
203
209
  */
204
- export declare class RetiredSchemaPresentError extends Error {
210
+ export declare class RetiredSchemaPresentError extends StoreBootRefusalError {
205
211
  readonly found: readonly string[];
206
212
  /** 机读码(与列册探针的拒启同一个)。 */
207
213
  readonly code = "config.schema_mismatch";
208
214
  constructor(found: readonly string[]);
209
215
  }
216
+ /** 探针口的池参数:方言与它的池一起走(判别联合 ⇒ 不靠调用方断言「这个池是哪种」)。 */
217
+ export type RetiredSchemaProbeTarget = {
218
+ readonly dialect: "tidb";
219
+ readonly pool: MysqlPool;
220
+ } | {
221
+ readonly dialect: "pg";
222
+ readonly pool: PgPool;
223
+ };
224
+ /**
225
+ * **探针口**(退役 schema 唯一的读处):库一连上、**任何 DDL 之前**探一遍,在场 ⇒ 抛 {@link RetiredSchemaPresentError}。本仓唯一调用点 =
226
+ * `store-backend.ts` 的 `withRetiredSchemaProbe`(SQL 两后端的 `ensureSchema()` 自身:探针先于建表的先后只写在那一处,不在两方言各写一遍)。**不在包根上**:
227
+ * 嵌入方经公开序列的 `ensureSchema()` 得到这一问,不需要自己排序列。
228
+ *
229
+ * 不取 `ensureSchema` 那把 advisory 锁:本口纯读(两条目录探针),不改任何东西,与别的副本正在跑的建表 / 迁移互不相干 —— 它们之间的先后不影响本口的判据
230
+ * (退役 schema 只会被 7.125.x 的迁移删去、不会被谁凭空建回)。用完即还连接,拒启时连接也已归还。
231
+ */
232
+ export declare function assertRetiredSchemaAbsentOnPool(target: RetiredSchemaProbeTarget): Promise<void>;
210
233
  /**
211
234
  * SQL 两后端的一次性迁移。**调用点只有一个**(boot/stores.ts,在 `ensureSchema` 之后、任何路由装配
212
235
  * 之前)—— 拒启在那一刻仍然是拒启(还没开始服务),而 `ensureSchema` 那条通道的契约是「只发 CREATE」
@@ -223,7 +246,8 @@ export declare class RetiredSchemaPresentError extends Error {
223
246
  * 非破坏改写(M4–M9:改值不丢信息 / 加法 DDL / 复制 + 换名)在前,破坏性步骤(M11 的删行)排在最后;
224
247
  * M11 自己半途失败时,拒因自带已删的行(`purgeUndurableScopeRows`)—— 否则重试看不见已删的行,证据永久消失。
225
248
  *
226
- * @throws {RetiredSchemaPresentError} 退役 schema 仍在(跳版升级的库;唯一的拒启臂,先于一切改写)。
249
+ * 本函数**不问**退役 schema:带着它的库在更早处就被 `ensureSchema()` 自身拒掉了({@link assertRetiredSchemaAbsentOnPool},先于建表);走公开序列
250
+ * `createStoreBackend()` → `ensureSchema()` → `runOneTimeMigrations()` 的嵌入方与本服务的起服序列一样,有这一问、零动作。
227
251
  */
228
252
  export declare function runSqlOneTimeMigrations(dialect: "tidb" | "pg", pool: MysqlPool | PgPool): Promise<OneTimeMigrationReport>;
229
253
  /** M8 一行改写的结论:改写后的文本 / 这一行不用动 / 读不懂(解析错误原样交出,调用方计数进读数)。 */
@@ -4,7 +4,7 @@ import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
4
4
  import { ensureIndexes } from "./ensure-index.js";
5
5
  import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn } from "./sql-json-column.js";
6
6
  import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
7
- import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
7
+ import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError } from "./store-contracts.js";
8
8
  import { PERMISSION_RULE_TABLE, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
9
9
  import { SCHEMA_MISMATCH_ERROR_CODE } from "./schema-consistency.js";
10
10
  export const ONE_TIME_MIGRATIONS = [
@@ -198,7 +198,7 @@ const AUDIT_TABLE = "device_audit";
198
198
  const AUDIT_DETAIL_COLUMN = "detail";
199
199
  const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
200
200
  const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
201
- export class RetiredSchemaPresentError extends Error {
201
+ export class RetiredSchemaPresentError extends StoreBootRefusalError {
202
202
  found;
203
203
  code = SCHEMA_MISMATCH_ERROR_CODE;
204
204
  constructor(found) {
@@ -209,7 +209,7 @@ export class RetiredSchemaPresentError extends Error {
209
209
  `This build carries none of the three and will not guess. ` +
210
210
  `Way out: start a server 7.125.x build once against this database and let its one-time migrations finish (it drops the table and the column and rewrites the spelling; ` +
211
211
  `while the column still holds a steering instruction that can be delivered it refuses and names the rows and its own ways out), then upgrade to this build. ` +
212
- `The check behind this refusal is read-only and runs before any rewrite: it changed nothing in the database.`);
212
+ `The check behind this refusal is read-only and runs before this build creates any table or rewrites anything: it changed nothing in the database.`);
213
213
  this.found = found;
214
214
  this.name = "RetiredSchemaPresentError";
215
215
  }
@@ -267,6 +267,25 @@ async function assertRetiredSchemaAbsent(db, dialect) {
267
267
  if (found.length > 0)
268
268
  throw new RetiredSchemaPresentError(found);
269
269
  }
270
+ export async function assertRetiredSchemaAbsentOnPool(target) {
271
+ if (target.dialect === "tidb") {
272
+ const conn = await target.pool.getConnection();
273
+ try {
274
+ await assertRetiredSchemaAbsent(mysqlDb(conn), "tidb");
275
+ }
276
+ finally {
277
+ conn.release();
278
+ }
279
+ return;
280
+ }
281
+ const client = await target.pool.connect();
282
+ try {
283
+ await assertRetiredSchemaAbsent(pgDb(client), "pg");
284
+ }
285
+ finally {
286
+ client.release();
287
+ }
288
+ }
270
289
  export async function runSqlOneTimeMigrations(dialect, pool) {
271
290
  return dialect === "tidb" ? withMysqlLock(pool, (db) => migrateOn(db, "tidb")) : withPgLock(pool, (db) => migrateOn(db, "pg"));
272
291
  }
@@ -299,7 +318,6 @@ async function withPgLock(pool, run) {
299
318
  }
300
319
  }
301
320
  async function migrateOn(db, dialect) {
302
- await assertRetiredSchemaAbsent(db, dialect);
303
321
  const auditTablePresent = await tableExists(db, dialect, AUDIT_TABLE);
304
322
  const auditDetailJsonNullRows = !auditTablePresent
305
323
  ? 0
@@ -1,4 +1,4 @@
1
- import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
1
+ import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type BoundFileObject, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
2
2
  /** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
3
3
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
4
4
  /** SIGTERM → grace → SIGKILL (no host isolation). 🔴 宽限的**唯一属主**:本 driver 的 `kill` 梯子用它排升级定时器;会话级收敛
@@ -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;
@@ -290,6 +292,16 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
290
292
  maxLines?: number;
291
293
  abortSignal?: AbortSignal;
292
294
  }): Promise<Result<string[], FileError>>;
295
+ /** core `NodeExecutionEnv` 的绑定读器 —— 首次 {@link openBound} 时建(构造无副作用,只记 cwd)。 */
296
+ private boundReader;
297
+ /**
298
+ * core 7.50.0 `FileSystem.openBound`:开着并**持有**一只对象(叶链接不跟,开后身份对拍),调用方判的与读的是同一只对象 ——
299
+ * LSP 会话的读口与路径附件只经它读,不实现 ⇒ 两者在本车道一律拒。
300
+ * 🔴 **不重写**:委托 core `NodeExecutionEnv.openBound`(O_NOFOLLOW 开句柄 + fstat / 路径身份对拍的唯一属主;本车道的命令就跑在
301
+ * 这台机器、这个用户下,与它判的是同一个文件系统)。本适配器只给它两道与其余读口同一的入口纪律:销毁后拒(`fsReady`)与
302
+ * 工作区相对解析(`resolve`)。持有的对象由调用方关;本适配器的 destroy 不追它(只读句柄,活到被关)。
303
+ */
304
+ openBound(p: string, abortSignal?: AbortSignal): Promise<Result<BoundFileObject, FileError>>;
293
305
  readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
294
306
  /** S-539:整只方法体(**含** `fsReady` 那道门)走 `withInflightFs` —— 登记必须与调用方同一同步段,
295
307
  * 否则 destroy 的快照可能刚好在 `await this.fsReady(...)` 的让出点上取完。见那处头注。 */
@@ -412,11 +424,16 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
412
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`
413
425
  * maps `true`→`"all"` / `false`→`[]`. */
414
426
  private inheritMode;
415
- /** 每次 spawn 交给子进程的 env(组合 + 剥除披露)。 */
427
+ /** 每次 spawn 交给子进程的 env(组合 + 剥除披露 + win32 的 SHELL 钉)。`sh` = 调用点刚从 {@link hostShell} 读到的那只(spawn 路径
428
+ * 都在 `shellReady()` 之后):没有已落定的 shell 就到不了这条腿 —— 类型即次序守卫。 */
416
429
  private mergeEnv;
417
430
  /**
418
- * 子进程 env 的**唯一组合**。`disclose` = 这一次是不是真要交给子进程(是 ⇒ scrub 腿经 `hostSubprocessEnv` 记剥除;否 ⇒ 只读组合,
419
- * 不计数 —— {@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` 与钉各自一个布尔。
420
437
  */
421
438
  private childEnv;
422
439
  private fileError;
@@ -7,9 +7,9 @@ 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
- import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
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";
14
14
  import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
15
15
  const PROVIDER = "host";
@@ -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 = () => {
@@ -958,6 +958,14 @@ export class RemoteHostExecutionEnv {
958
958
  lines = lines.slice(0, options.maxLines);
959
959
  return ok(lines);
960
960
  }
961
+ boundReader;
962
+ async openBound(p, abortSignal) {
963
+ const ready = await this.fsReady(p);
964
+ if (!ready.ok)
965
+ return ready;
966
+ this.boundReader ??= new NodeExecutionEnv({ cwd: this.workspaceDir });
967
+ return this.boundReader.openBound(this.resolve(p), abortSignal);
968
+ }
961
969
  async readBinaryFile(p, abortSignal) {
962
970
  const ready = await this.fsReady(p);
963
971
  if (!ready.ok)
@@ -1311,10 +1319,11 @@ export class RemoteHostExecutionEnv {
1311
1319
  return "all";
1312
1320
  return "scrub";
1313
1321
  }
1314
- mergeEnv(perCommand) {
1315
- return this.childEnv(perCommand, true);
1322
+ mergeEnv(perCommand, sh) {
1323
+ return this.childEnv(perCommand, sh);
1316
1324
  }
1317
- childEnv(perCommand, disclose) {
1325
+ childEnv(perCommand, spawnShell) {
1326
+ const disclose = spawnShell !== undefined;
1318
1327
  const mode = this.inheritMode();
1319
1328
  let base;
1320
1329
  if (mode === "all")
@@ -1332,8 +1341,8 @@ export class RemoteHostExecutionEnv {
1332
1341
  Object.assign(base, this.cfg.env);
1333
1342
  if (perCommand)
1334
1343
  Object.assign(base, perCommand);
1335
- if (process.platform === "win32")
1336
- base.SHELL = hostShell().shell;
1344
+ if (spawnShell)
1345
+ pinHostShell(base, spawnShell);
1337
1346
  return collapseWin32EnvKeys(base);
1338
1347
  }
1339
1348
  fileError(e, p, abortSignal) {
@@ -1,6 +1,7 @@
1
1
  import { type SpaceReclaimer, type WorkspaceSpacePolicy, type CacheReclaimReason } from "../workspace-space.js";
2
2
  import { type SpaceCommitFence } from "../workspace-space-commit.js";
3
3
  import { type WorkspaceCacheCapability } from "../workspace-cache.js";
4
+ import { type SandboxEgressGap, type SandboxIpv6 } from "../sandbox-egress-policy.js";
4
5
  import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
5
6
  import type { PodSpecPatch } from "./k8s-exec-protocol.js";
6
7
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
@@ -50,6 +51,9 @@ export interface K8sEnvConfig {
50
51
  snapshotExclude?: readonly string[];
51
52
  cpu?: string;
52
53
  memory?: string;
54
+ /** The sandbox pod's IPv6 posture (`K8S_SANDBOX_IPV6`; closed set + default owned by `sandbox-egress-policy.ts`): `disabled` ⇒ the
55
+ * pod-level sysctls of {@link sandboxPodSysctls}; `enabled` (default) ⇒ no `securityContext` key, the pod byte-identical to before. */
56
+ ipv6?: SandboxIpv6;
53
57
  retry?: {
54
58
  maxAttempts?: number;
55
59
  backoffMs?: number;
@@ -209,6 +213,8 @@ export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
209
213
  * String() is "[object ErrorEvent]" — that masked the real TLS failure for a whole drill. */
210
214
  export declare function errorMessageOf(e: unknown): string;
211
215
  export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
216
+ /** The sandbox namespace when `K8S_NAMESPACE` is unset (the one spelling: the adapter and the boot's egress line read it). */
217
+ export declare const K8S_SANDBOX_NAMESPACE_DEFAULT = "sandboxes";
212
218
  export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, BackgroundShellCapability {
213
219
  /**
214
220
  * 🔴 **S-354 / core 7.20.0([ref])`canonicalPathAuthoritative: true`** —— 本适配器的 `canonicalPath`
@@ -237,6 +243,10 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
237
243
  private inFlightExecs;
238
244
  /** Memoized in-flight connect (lazy-connect; prevents concurrent double pod-create — the E2B race lesson). */
239
245
  private connectPromise?;
246
+ /** The space restore's refusal: `latest` names an archive that is there and did not come back. A fact of this env until `destroy` —
247
+ * written once, by {@link restoreSnapshot} at the restore's end; read by {@link connect} alone (the lazy and the explicit connect alike),
248
+ * which answers it to every later connect: the same refusal, zero pods, zero store requests, no second line. */
249
+ private restoreRefusal?;
240
250
  /** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
241
251
  readonly homeDir?: string | undefined;
242
252
  /** core 7.49.0 #1554:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在 pod 里(镜像与 pod spec 的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
@@ -282,6 +292,8 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
282
292
  * Persistence off / no store ⇒ a fresh pod, exactly as before. Persistence on but no such session (the occupant absent) ⇒ a
283
293
  * fresh pod and one `workspace_space_unavailable` line (no restore by any other key). The occupant read FAULTED ⇒ the same
284
294
  * loud `connect_failed` as a failed `latest` read, no pod ({@link provision} refuses it — every entry that would create one).
295
+ * One restore per env: a restore that ended refused ({@link restoreRefusal}) is answered as it stands to every later connect — the
296
+ * previous commit is not waited on again, `latest` is not read again, no pod is created.
285
297
  */
286
298
  connect(config?: RemoteConnectConfig): Promise<ConnectResult>;
287
299
  /** This env holds a workspace its `destroy()` will commit ⇒ arm its seat on the space's commit fence (from here the
@@ -320,7 +332,9 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
320
332
  * Lazy-connect on first use — 🔴 core NEVER calls `connect()` (verified: the Runner only calls `resumeVM` on
321
333
  * the resume path), so an adapter that demands an explicit connect can never work through the production
322
334
  * hand-tool path (lazy-connect belongs in the adapter). Memoized so concurrent first ops share ONE
323
- * pod create; a failed connect clears the memo (next op retries); `destroyed` is terminal.
335
+ * pod create; `destroyed` is terminal. A failed connect clears the memo — how long a failure stands is {@link connect}'s, not
336
+ * this memo's: a failure of that attempt (a pod that could not be created, a space read that failed) ⇒ the next op connects
337
+ * afresh; the space restore's refusal is the env's ({@link restoreRefusal}) ⇒ the next op's connect answers that same refusal at once.
324
338
  */
325
339
  private ensureConnected;
326
340
  /**
@@ -348,6 +362,12 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
348
362
  } | ({
349
363
  ok: false;
350
364
  } & CacheReclaimFailure)>;
365
+ /**
366
+ * The egress self-check's two reads (`GET` nodes, `GET` the sandbox namespace's NetworkPolicies) judged by the one rule
367
+ * ({@link sandboxEgressGaps}) against the labels this env mints on its pods (base identity + the pod-spec patch's additions, the
368
+ * same merge `createPod` applies). A read that is refused / fails / times out is a `unreadable` gap, never a guess. Never throws.
369
+ */
370
+ egressPolicyGaps(): Promise<SandboxEgressGap[]>;
351
371
  /** The one spelling of "the reclaim call's deadline passed" — both halves of the reclaim (volume and objects) say it with this. */
352
372
  private reclaimDeadlinePassed;
353
373
  /** The cache volume could not be had: counted (`recordFailOpen`) and one operator line — once per env. */
@@ -473,12 +493,21 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
473
493
  * by the bucket's lifecycle rule or a hand): the data is gone by an operator rule ⇒ loud and continuable —
474
494
  * {@link dropDanglingLatest} (one `workspace_latest_dangling` line, the pointer deleted); the run's own run-end
475
495
  * snapshot then becomes the new `latest`.
476
- * A restore that does not complete releases the pod it provisioned: no tool and no run-end snapshot may ever see a
477
- * torn workspace. On the space lane that failure is the archive `latest` names being THERE but not coming back
478
- * ({@link RestoreFailureClass}): the run is refused all the same — never an empty workspace (the user's changes are still in
479
- * the bucket) — with exactly one `workspace_restore_failed` line and a refusal naming the two ways out.
496
+ * An attempt that does not complete releases the pod it provisioned: no tool and no run-end snapshot may ever see a torn
497
+ * workspace. A transient class ({@link restoreFailureIsTransient}) is tried again on a fresh pod after the `retry.backoffMs`
498
+ * pause, up to the lane's {@link RESTORE_MAX_ATTEMPTS}; a non-transient one ends the restore at once. The lane then says what
499
+ * the end means:
500
+ * - park: a plain `connect_failed` — its caller retries; nothing said, nothing held;
501
+ * - space: the archive `latest` names is THERE and did not come back ({@link RestoreFailureClass}): the run is refused all the
502
+ * same — never an empty workspace (the user's changes are still in the bucket) — with exactly one `workspace_restore_failed`
503
+ * line (its `attempts`; an attempt that failed before one that succeeded is no line) and a refusal naming the two ways out,
504
+ * held for the env ({@link restoreRefusal}).
505
+ * A pod that could not be created is that attempt's failure, not the archive's: returned as it is — no line, nothing held, the
506
+ * next connect tries again. An abort during the pause ends the restore `aborted` (not the archive's either).
480
507
  */
481
508
  private restoreSnapshot;
509
+ /** The pause before a restore's next attempt (`retry.backoffMs`, the adapter's one retry pause). `false` ⇒ aborted (now or during it). */
510
+ private restorePause;
482
511
  /**
483
512
  * `latest` names an archive that is not there (S-1438): one `workspace_latest_dangling{space, key, deleted}` line and the
484
513
  * pointer is deleted (the next run must not trip on it again); the run continues on the empty workspace. A pointer
@@ -645,6 +674,12 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
645
674
  * bucket's lifecycle rule). A caller that must not wait for the seat (the session-delete answer) starts it and does not await it.
646
675
  */
647
676
  export declare function createSpaceReclaimer(config: K8sEnvConfig, deps?: K8sEnvDeps): SpaceReclaimer;
677
+ /**
678
+ * The k8s lane's boot-time egress self-check (once per boot, never holds the boot): the gaps between the sandbox pods' egress
679
+ * policy and what the cluster API shows (`sandbox-egress-policy.ts` — the closed set and the rule). A reading, not the boundary:
680
+ * nodes that join after boot are read at the next boot. Never throws (a read failure is an `unreadable` gap).
681
+ */
682
+ export declare function checkSandboxEgressPolicy(config: K8sEnvConfig, deps?: K8sEnvDeps): Promise<SandboxEgressGap[]>;
648
683
  /**
649
684
  * `ExecutionEnvFactory` for the k8s pod-sandbox backend — one fresh (unconnected) env per task; the pod is
650
685
  * created lazily on connect and deleted by `destroy()` (Runner-owned lifetime; `activeDeadlineSeconds` is the