@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.
- package/MIGRATION.md +38 -0
- package/USAGE.md +86 -8
- package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +15 -5
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +28 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
- package/deploy/sema-up/chart/templates/server.yaml +1 -0
- package/deploy/sema-up/chart/values.yaml +16 -6
- package/deploy/sema-up/kube-up.sh +10 -4
- package/deploy/sema-up/lib/node-addrs.sh +42 -0
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +20 -1
- package/dist/audit.js +1 -0
- package/dist/boot/execution-env.js +17 -4
- package/dist/boot/leader.d.ts +2 -1
- package/dist/boot/memory-consolidation.d.ts +37 -3
- package/dist/boot/memory-consolidation.js +18 -2
- package/dist/boot/runner-deps.d.ts +2 -1
- package/dist/boot/session-faces.d.ts +2 -1
- package/dist/boot/stage-06-runners.js +5 -2
- package/dist/config-catalog.js +3 -1
- package/dist/config-types.d.ts +5 -1
- package/dist/config.js +2 -0
- package/dist/execution-lane-caps.d.ts +12 -0
- package/dist/execution-lane-caps.js +16 -2
- package/dist/fleet/subagent-tail-bus.js +15 -5
- package/dist/http/admission.js +5 -1
- package/dist/http/durable-run-leg.js +1 -3
- package/dist/http/resume-legs.js +15 -6
- package/dist/http/route-ctx.d.ts +7 -1
- package/dist/http/routes/runs.js +23 -1
- package/dist/http/routes/tasks.js +17 -8
- package/dist/http/routes/workflows.js +8 -0
- package/dist/http/server.js +1 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lsp/manager.js +1 -1
- package/dist/observability/fail-open.d.ts +16 -0
- package/dist/observability/fail-open.js +16 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal.js +3 -0
- package/dist/plugins/caching-session-store.d.ts +39 -22
- package/dist/plugins/caching-session-store.js +16 -9
- package/dist/plugins/host-platform.d.ts +18 -3
- package/dist/plugins/host-platform.js +23 -10
- package/dist/plugins/k8s-exec-protocol.js +3 -0
- package/dist/plugins/one-time-migrations.d.ts +28 -4
- package/dist/plugins/one-time-migrations.js +22 -4
- package/dist/plugins/remote-env-host.d.ts +23 -6
- package/dist/plugins/remote-env-host.js +21 -12
- package/dist/plugins/remote-env-k8s.d.ts +40 -5
- package/dist/plugins/remote-env-k8s.js +119 -25
- package/dist/plugins/session-store.d.ts +4 -3
- package/dist/plugins/session-store.js +1 -1
- package/dist/plugins/store-backend.d.ts +12 -5
- package/dist/plugins/store-backend.js +12 -3
- package/dist/plugins/store-contracts.d.ts +3 -0
- package/dist/plugins/store-open-wait.js +3 -0
- package/dist/request-key-closure.d.ts +20 -7
- package/dist/request-key-closure.js +22 -6
- package/dist/run-acceptance.d.ts +51 -0
- package/dist/run-acceptance.js +27 -0
- package/dist/runs.d.ts +7 -11
- package/dist/runs.js +5 -16
- package/dist/sandbox-egress-policy.d.ts +61 -0
- package/dist/sandbox-egress-policy.js +235 -0
- package/dist/session-error-wire.d.ts +2 -2
- package/dist/session-titler.d.ts +5 -3
- package/dist/trace/core-keyset-guard.d.ts +3 -2
- package/dist/trace/ledger-events.d.ts +9 -3
- package/dist/trace/ledger-sink.d.ts +7 -0
- package/dist/trace/ledger-sink.js +14 -4
- package/dist/trace/project.d.ts +49 -8
- package/dist/trace/project.js +39 -7
- package/dist/trace/self-minted-wire.d.ts +19 -29
- package/dist/trace/self-minted-wire.js +2 -9
- package/dist/trace/tool-output-media.d.ts +26 -0
- package/dist/trace/tool-output-media.js +57 -0
- package/dist/trace/wire-projection-faces.d.ts +7 -1
- package/dist/trace/wire-projection-faces.js +2 -1
- 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 (
|
|
32
|
-
|
|
33
|
-
|
|
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}
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
419
|
-
*
|
|
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,
|
|
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,
|
|
1322
|
+
mergeEnv(perCommand, sh) {
|
|
1323
|
+
return this.childEnv(perCommand, sh);
|
|
1316
1324
|
}
|
|
1317
|
-
childEnv(perCommand,
|
|
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 (
|
|
1336
|
-
base
|
|
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;
|
|
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
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
* the
|
|
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
|