@sema-agent/server 7.70.0 → 7.72.0
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +8 -4
- package/dist/approval-card.d.ts +16 -17
- package/dist/approval-card.js +0 -9
- package/dist/approval-content-kind.d.ts +13 -7
- package/dist/approval-content-kind.js +3 -1
- package/dist/approval.js +3 -2
- package/dist/bench/s1/live-deps.js +2 -0
- package/dist/boot/execution-env.js +12 -2
- package/dist/boot/memory-consolidation.d.ts +2 -1
- package/dist/boot/parked-revive-gate.d.ts +2 -0
- package/dist/boot/parked-revive-gate.js +5 -1
- package/dist/boot/resolve-spec.js +8 -3
- package/dist/boot/runner-deps.d.ts +48 -59
- package/dist/boot/runner-deps.js +10 -4
- package/dist/brain.d.ts +3 -1
- package/dist/brain.js +2 -8
- package/dist/budget.d.ts +12 -1
- package/dist/budget.js +1 -1
- package/dist/config-catalog.js +13 -11
- package/dist/config-center/apply-effective.js +2 -4
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/restart-signal.js +2 -2
- package/dist/config-center/skills-mcp.js +15 -0
- package/dist/config-types.d.ts +86 -4
- package/dist/config-types.js +19 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +71 -22
- package/dist/deployment-governance.d.ts +6 -3
- package/dist/deployment-governance.js +2 -1
- package/dist/execution-lane-caps.d.ts +51 -37
- package/dist/execution-lane-caps.js +23 -0
- package/dist/fleet/fleet-bus.d.ts +22 -1
- package/dist/fleet/fleet-bus.js +2 -1
- package/dist/http/durable-run-leg.d.ts +64 -0
- package/dist/http/durable-run-leg.js +63 -0
- package/dist/http/route-ctx.d.ts +14 -0
- package/dist/http/route-ctx.js +3 -0
- package/dist/http/routes/approvals-assistant.js +3 -3
- package/dist/http/routes/capabilities.js +2 -2
- package/dist/http/routes/runs.js +10 -58
- package/dist/http/routes/workflows.js +1 -0
- package/dist/http/server.js +72 -22
- package/dist/index.d.ts +1 -1
- package/dist/leader/wire.js +4 -3
- package/dist/main.js +8 -3
- package/dist/memory-operator-faces.d.ts +1 -1
- package/dist/memory-scope.d.ts +24 -0
- package/dist/memory-scope.js +12 -3
- package/dist/observability/fail-open.d.ts +1 -1
- package/dist/observability/fail-open.js +1 -1
- package/dist/observability/metrics.d.ts +4 -0
- package/dist/observability/metrics.js +3 -1
- package/dist/observability/run-terminal-log.d.ts +4 -3
- package/dist/observability/run-terminal-log.js +4 -15
- package/dist/observability/tool-trace.d.ts +18 -1
- package/dist/observability/tool-trace.js +4 -3
- package/dist/orchestration/workflow-agent-session-index.d.ts +85 -2
- package/dist/orchestration/workflow-agent-session-index.js +38 -5
- package/dist/parked-decide.d.ts +40 -1
- package/dist/parked-decide.js +22 -4
- package/dist/plugins/checkpoint-store-sql.js +5 -5
- package/dist/plugins/e2b-orphan-reclaim.d.ts +4 -4
- package/dist/plugins/e2b-orphan-reclaim.js +18 -20
- package/dist/plugins/local-checkpoint-store.js +3 -3
- package/dist/plugins/remote-env-adb.d.ts +11 -0
- package/dist/plugins/remote-env-adb.js +2 -0
- package/dist/plugins/remote-env-device.d.ts +9 -0
- package/dist/plugins/remote-env-device.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +11 -0
- package/dist/plugins/remote-env-e2b.js +2 -0
- package/dist/plugins/remote-env-host.d.ts +11 -0
- package/dist/plugins/remote-env-host.js +2 -0
- package/dist/plugins/remote-env-k8s.d.ts +11 -0
- package/dist/plugins/remote-env-k8s.js +2 -0
- package/dist/plugins/remote-env-local-docker.d.ts +11 -0
- package/dist/plugins/remote-env-local-docker.js +2 -0
- package/dist/plugins/remote-env-ssh.d.ts +11 -0
- package/dist/plugins/remote-env-ssh.js +2 -0
- package/dist/plugins/store-backend.d.ts +34 -12
- package/dist/plugins/store-backend.js +12 -3
- package/dist/plugins/store-contracts.d.ts +81 -18
- package/dist/plugins/store-contracts.js +18 -30
- package/dist/plugins/workflow-run-store-sql.d.ts +31 -9
- package/dist/plugins/workflow-run-store-sql.js +28 -1
- package/dist/run-cancel-context.js +1 -1
- package/dist/run-local.js +7 -1
- package/dist/runs.d.ts +16 -2
- package/dist/runs.js +2 -1
- package/dist/runtime-caps-resolver.js +5 -3
- package/dist/task-a2a.js +2 -1
- package/dist/task-mcp.js +2 -1
- package/dist/task-settings.d.ts +21 -8
- package/dist/task-settings.js +2 -2
- package/dist/tool-approval.d.ts +26 -21
- package/dist/tool-approval.js +2 -2
- package/dist/trace/core-keyset-guard.d.ts +136 -10
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +1 -0
- package/dist/trace/ledger-events.d.ts +5 -1
- package/dist/trace/ledger-sink.js +8 -1
- package/dist/trace/project.d.ts +33 -4
- package/dist/trace/project.js +24 -6
- package/dist/trace/redact.js +10 -5
- package/dist/usage-analytics.d.ts +9 -1
- package/dist/usage-analytics.js +8 -0
- package/dist/wire-name-bound.d.ts +14 -0
- package/dist/wire-name-bound.js +13 -0
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isClassifierDenyCause, isDeniedBy } from "@sema-agent/core";
|
|
2
2
|
import { redactSecrets } from "../trace/redact.js";
|
|
3
3
|
function deniedByLabel(gate) {
|
|
4
4
|
const d = gate.disposition;
|
|
@@ -8,9 +8,10 @@ export function createPermissionDeniedMeter(metrics) {
|
|
|
8
8
|
return {
|
|
9
9
|
permissionDenied(payload) {
|
|
10
10
|
metrics.inc("permission_denied_total", { deniedBy: deniedByLabel(payload.gate) });
|
|
11
|
-
const
|
|
11
|
+
const disposition = payload.gate?.disposition;
|
|
12
|
+
const cause = disposition?.kind === "denied" ? disposition.cause : undefined;
|
|
12
13
|
if (cause !== undefined) {
|
|
13
|
-
metrics.inc("
|
|
14
|
+
metrics.inc("auto_mode_classifier_deny_cause_total", { cause: isClassifierDenyCause(cause) ? cause : "other" });
|
|
14
15
|
}
|
|
15
16
|
},
|
|
16
17
|
};
|
|
@@ -19,10 +19,93 @@ export interface WorkflowAgentSessionIndex {
|
|
|
19
19
|
syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
|
|
20
20
|
findByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow | null>;
|
|
21
21
|
}
|
|
22
|
+
/** 一条 wa* 行在**恢复面**上的读形(本模块两个派生点共用的最小入参 —— 只声明恢复判据用得到的位)。 */
|
|
23
|
+
export interface ParkedAgentReadFace {
|
|
24
|
+
label?: unknown;
|
|
25
|
+
status?: unknown;
|
|
26
|
+
sessionId?: unknown;
|
|
27
|
+
callKey?: unknown;
|
|
28
|
+
queuedAt?: unknown;
|
|
29
|
+
startedAt?: unknown;
|
|
30
|
+
parkedCheckpointToken?: unknown;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 🔴 **「这一行是不是一条可赎回的 park」的唯一判据**(S-192 提取;此前只内联在
|
|
34
|
+
* {@link buildParkedAgentSessionRows} 里,而店层的瘦身腿需要**同一条**判据 —— 两处各写一遍就是两份会漂的
|
|
35
|
+
* 定义,而它们的消费面是同一件事:park 的赎回路由)。
|
|
36
|
+
*
|
|
37
|
+
* 三位缺任何一个都不算:缺 `sessionId` 的 park 行 join 不到(checkpoint 那侧的键就是它),缺
|
|
38
|
+
* `parkedCheckpointToken` 的行不是一条可赎回的 park(core 契约:在场 ⟺ `status:"parked"`)。
|
|
39
|
+
*/
|
|
40
|
+
export declare function isRecoverableParkedAgent<T extends ParkedAgentReadFace>(a: T): a is T & {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
parkedCheckpointToken: string;
|
|
43
|
+
};
|
|
44
|
+
/** LLM 撰写的 `label` 在**恢复身份行**上的字符上界。core 的铸点对它零钳零脱(脚本经 `opts.label` 供,
|
|
45
|
+
* 缺席才落 auto 名;`parked-decide.ts` 拒把它内插进可信帧时亲核过这一点),而这条行存在的理由是**装得下**
|
|
46
|
+
* —— 把一段无界展示文本原样带进来会让「留身份」这件事本身把行撑爆。 */
|
|
47
|
+
export declare const PARKED_IDENTITY_LABEL_MAX = 200;
|
|
48
|
+
/**
|
|
49
|
+
* 🔴 **一条 parked 行的「恢复身份」形**(S-192)—— 店层瘦身时**最后才放弃**的那一份。
|
|
50
|
+
*
|
|
51
|
+
* 它是一条**合法的** `WorkflowAgentRun`(core 的必填四位 `label`/`callKey`/`status`/`queuedAt` 齐全),
|
|
52
|
+
* 加上赎回路由要的两位(`sessionId` = checkpoint 的 `sourceTaskId`,`parkedCheckpointToken` = 赎回键)。
|
|
53
|
+
* 展示载荷(`prompt`/`output`/`activity`/`errorMessage`/`phase`/`model`…)一律不带 —— 那正是被裁掉的东西。
|
|
54
|
+
*
|
|
55
|
+
* 返回的是**普通数据**(build*),消费方是 `plugins/workflow-run-store-sql.ts` 的 `slimOversizeRun`。
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildParkedRecoveryIdentity(a: ParkedAgentReadFace & {
|
|
58
|
+
sessionId: string;
|
|
59
|
+
parkedCheckpointToken: string;
|
|
60
|
+
}, // 准入判据 {@link isRecoverableParkedAgent} 的守卫输出:类型上绕不开
|
|
61
|
+
labelMax?: number): Record<string, unknown>;
|
|
62
|
+
/**
|
|
63
|
+
* 一只 run 的 parked 行的恢复身份集(顺序 = 原行序;非 park / 无身份的行不产出)。
|
|
64
|
+
*
|
|
65
|
+
* `labelMax` 是**展示名的预算**,不是第二种形:身份四位(`callKey` / `sessionId` /
|
|
66
|
+
* `parkedCheckpointToken` / `status`)恒在,`label` 是这条行上唯一可弃的展示物。店的瘦身阶梯据此把
|
|
67
|
+
* 「放弃展示名」排在「放弃身份」**之前** —— 传 0 即得只剩身份的最小行(`redactHead(x, 0)` 恒得空串)。
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildParkedRecoveryIdentities(agents: ReadonlyArray<ParkedAgentReadFace> | undefined, labelMax?: number): Array<Record<string, unknown>>;
|
|
70
|
+
/**
|
|
71
|
+
* 🔴 **park 恢复身份「保不住」的显式标记**(S-192)。
|
|
72
|
+
*
|
|
73
|
+
* ## 为什么它住在 run 的 blob 上、而且键名带 `server` 前缀
|
|
74
|
+
* core 的 `WorkflowRun`(`orchestration/workflow-types.d.ts`)**没有合法座** —— 亲读全部可选成员
|
|
75
|
+
* (`agentFailures` / `budgetOvershoot` / `timeoutInterruption` / `resume` / `journalSkips` …)全是 core 自己的
|
|
76
|
+
* 观测席,没有一个说得了「本仓的店把这条 run 的 park 身份裁掉了」。而这条事实的**唯一知情点**是
|
|
77
|
+
* `plugins/workflow-run-store-sql.ts` 的瘦身腿(店内),它的**唯一消费点**是 `/decide` 的 workflow 车道
|
|
78
|
+
* (读 `WorkflowRunStore.get`)—— 两点之间已经有的、且只有的那条通路就是 run 的 blob。
|
|
79
|
+
* (本仓的 park join 索引 {@link WorkflowAgentSessionIndex} 承载不了它:索引的**单写者**是
|
|
80
|
+
* `JournalingWorkflowRunStore` 的 put/update 观察点,它拿到的是**内存里的整只 run**,结构上看不见店内那次
|
|
81
|
+
* 瘦身;把这条事实塞过去要么加第二个写者、要么把店的字节上界漏进装饰器,两者都比现在这条更坏。)
|
|
82
|
+
* 键名前缀 `server` 是**诚实标注**:它是本仓铸的位,不是 core 契约的一部分,也因此永远不会与 core 后来加的
|
|
83
|
+
* 成员撞名。
|
|
84
|
+
*
|
|
85
|
+
* ## 闭集
|
|
86
|
+
* `reason` 只有一个词(单一成因:店的字节上界)。`parks` 是被丢掉的 parked 行数 —— **只有计数,不带任何
|
|
87
|
+
* 身份位**(带了就等于没裁)。
|
|
88
|
+
*/
|
|
89
|
+
export declare const PARK_IDENTITY_LOSS_KEY = "serverParkIdentityLoss";
|
|
90
|
+
export interface ParkIdentityLoss {
|
|
91
|
+
/** 闭集单值:行被店的字节上界裁掉。 */
|
|
92
|
+
reason: "oversize_slim";
|
|
93
|
+
/** 被裁掉的 parked 行数(诊断口径,不含身份位)。 */
|
|
94
|
+
parks: number;
|
|
95
|
+
}
|
|
96
|
+
/** 铸点(普通数据)。 */
|
|
97
|
+
export declare function buildParkIdentityLoss(parks: number): ParkIdentityLoss;
|
|
98
|
+
/**
|
|
99
|
+
* 盘上 run blob → {@link ParkIdentityLoss} 的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as` 铸形;
|
|
100
|
+
* 与本文件 {@link readAgentSessionLedgerLine} 同一条姿态)。
|
|
101
|
+
*
|
|
102
|
+
* 形不合(缺键 / 词出闭集 / 计数不是有限数)⇒ **缺席**,而不是「铸一个默认标记出来」:标记在场的后果是
|
|
103
|
+
* `/decide` 响亮拒,一个凭空铸出来的标记会把一条**可赎回**的 park 拒死。
|
|
104
|
+
*/
|
|
105
|
+
export declare function readParkIdentityLoss(run: unknown): ParkIdentityLoss | undefined;
|
|
22
106
|
/** 派生的**唯一属主**:一只 `WorkflowRun` 当前的 parked 索引行。
|
|
23
107
|
*
|
|
24
|
-
*
|
|
25
|
-
* 缺 `parkedCheckpointToken` 的行不是一条可赎回的 park(core 契约:在场 ⟺ `status:"parked"`)。
|
|
108
|
+
* 行的准入判据 = {@link isRecoverableParkedAgent}(与店层瘦身腿**同一条**)。
|
|
26
109
|
* ⚠️ token **不进索引行**:它是赎回能力,家在 run 的 blob 里(与 bg 行同判),索引只回答「去哪条 run 找」。 */
|
|
27
110
|
export declare function buildParkedAgentSessionRows(runId: string, run: {
|
|
28
111
|
scope: string;
|
|
@@ -1,13 +1,46 @@
|
|
|
1
1
|
import { existsSync, fsyncSync, mkdirSync, openSync, readFileSync, truncateSync, writeSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { redactHead } from "../observability/run-terminal-log.js";
|
|
4
|
+
const nonEmptyString = (v) => typeof v === "string" && v !== "";
|
|
5
|
+
export function isRecoverableParkedAgent(a) {
|
|
6
|
+
return a.status === "parked" && nonEmptyString(a.sessionId) && nonEmptyString(a.parkedCheckpointToken);
|
|
7
|
+
}
|
|
8
|
+
export const PARKED_IDENTITY_LABEL_MAX = 200;
|
|
9
|
+
export function buildParkedRecoveryIdentity(a, labelMax = PARKED_IDENTITY_LABEL_MAX) {
|
|
10
|
+
return {
|
|
11
|
+
label: typeof a.label === "string" ? redactHead(a.label, labelMax) : "",
|
|
12
|
+
callKey: typeof a.callKey === "string" ? a.callKey : "",
|
|
13
|
+
status: "parked",
|
|
14
|
+
queuedAt: typeof a.queuedAt === "number" && Number.isFinite(a.queuedAt) ? a.queuedAt : 0,
|
|
15
|
+
...(typeof a.startedAt === "number" && Number.isFinite(a.startedAt) ? { startedAt: a.startedAt } : {}),
|
|
16
|
+
sessionId: a.sessionId,
|
|
17
|
+
parkedCheckpointToken: a.parkedCheckpointToken,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function buildParkedRecoveryIdentities(agents, labelMax = PARKED_IDENTITY_LABEL_MAX) {
|
|
21
|
+
return (agents ?? []).filter(isRecoverableParkedAgent).map((a) => buildParkedRecoveryIdentity(a, labelMax));
|
|
22
|
+
}
|
|
23
|
+
export const PARK_IDENTITY_LOSS_KEY = "serverParkIdentityLoss";
|
|
24
|
+
export function buildParkIdentityLoss(parks) {
|
|
25
|
+
return { reason: "oversize_slim", parks };
|
|
26
|
+
}
|
|
27
|
+
export function readParkIdentityLoss(run) {
|
|
28
|
+
if (typeof run !== "object" || run === null)
|
|
29
|
+
return undefined;
|
|
30
|
+
const v = run[PARK_IDENTITY_LOSS_KEY];
|
|
31
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
32
|
+
return undefined;
|
|
33
|
+
const { reason, parks } = v;
|
|
34
|
+
if (reason !== "oversize_slim")
|
|
35
|
+
return undefined;
|
|
36
|
+
if (typeof parks !== "number" || !Number.isFinite(parks) || parks <= 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
return { reason, parks };
|
|
39
|
+
}
|
|
3
40
|
export function buildParkedAgentSessionRows(runId, run) {
|
|
4
41
|
const rows = [];
|
|
5
42
|
for (const a of run.agents ?? []) {
|
|
6
|
-
if (a
|
|
7
|
-
continue;
|
|
8
|
-
if (typeof a.sessionId !== "string" || a.sessionId === "")
|
|
9
|
-
continue;
|
|
10
|
-
if (typeof a.parkedCheckpointToken !== "string" || a.parkedCheckpointToken === "")
|
|
43
|
+
if (!isRecoverableParkedAgent(a))
|
|
11
44
|
continue;
|
|
12
45
|
rows.push({ sessionId: a.sessionId, runId, agentKey: typeof a.callKey === "string" ? a.callKey : "", scope: run.scope });
|
|
13
46
|
}
|
package/dist/parked-decide.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { HostDecision } from "./host-decision.js";
|
|
12
12
|
import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, ResumeOutcome, TaskRegistry, ToolSpec, WorkflowAgentRun, WorkflowRun, WorkflowRunStore } from "@sema-agent/core";
|
|
13
|
-
import type
|
|
13
|
+
import { type ParkIdentityLoss, type WorkflowAgentSessionIndex } from "./orchestration/workflow-agent-session-index.js";
|
|
14
14
|
export interface ParkedAgentMatch {
|
|
15
15
|
handle: string;
|
|
16
16
|
row: BackgroundAgentRecord;
|
|
@@ -47,6 +47,23 @@ export type ParkOrigin = {
|
|
|
47
47
|
runId: string;
|
|
48
48
|
run: WorkflowRun;
|
|
49
49
|
agent: WorkflowAgentRun;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* S-192 —— **恢复身份被店的字节上界裁掉**的那条 park(第四种出身,不是第四条车道)。
|
|
53
|
+
*
|
|
54
|
+
* 索引行还在(它由**内存里的整只 run** 派生,店内的瘦身它看不见),run 行也还在,但 blob 里已经没有
|
|
55
|
+
* 那一行 —— 而 blob 里的 `parkedCheckpointToken` 是 server 侧**复核**「这条 park 仍在此 token 下」的唯一
|
|
56
|
+
* 依据(引擎自己的那份赎回键住在 resume journal 的 parked 条目里,resume 时从那里取,见
|
|
57
|
+
* {@link denyWorkflowParkIdentityLost} 顶注亲读的 core 坐标)。此前这一格与「索引落后一拍 / 行已在新
|
|
58
|
+
* token 下 re-park」两种**正常** miss 完全同形,于是落 legacy 腿拿一个错因 409。
|
|
59
|
+
* 现在店在裁掉它的同时落一个闭集标记({@link readParkIdentityLoss}),判别式据它把这一格分出来,
|
|
60
|
+
* 三个消费点各自响亮处置(decide 拒 + 不驱动宿主续跑;SLA sweep 照旧静默跳过——它同样复核不了)。
|
|
61
|
+
*/
|
|
62
|
+
| {
|
|
63
|
+
kind: "workflow_identity_lost";
|
|
64
|
+
runId: string;
|
|
65
|
+
run: WorkflowRun;
|
|
66
|
+
loss: ParkIdentityLoss;
|
|
50
67
|
};
|
|
51
68
|
/** 判别式的两个源。各自可缺席(部署没接就是没接)—— 缺席 ⇒ 那条臂不判,不是失败。 */
|
|
52
69
|
export interface ParkOriginSources {
|
|
@@ -208,6 +225,28 @@ export type WorkflowParkedDecidePlan = {
|
|
|
208
225
|
ok: false;
|
|
209
226
|
deny: ParkedDecideResult;
|
|
210
227
|
};
|
|
228
|
+
/**
|
|
229
|
+
* S-192 —— **恢复身份已被裁掉的 workflow park 的响亮拒**(与上面那族拒臂同一种返回形)。
|
|
230
|
+
*
|
|
231
|
+
* 为什么是拒而不是「尽力而为地投递一次」(机理按 core 7.13.0 `dist/orchestration/workflow.js` 亲读,合并复审
|
|
232
|
+
* R9/R23 推翻本注此前「引擎手上没有赎回键 ⇒ 重跑」的推断):
|
|
233
|
+
* · 投递用的 token 是调用方 checkpoint 的 `cp.token`(路由拿到的那枚),**不是** blob 里那一行;blob 行在本
|
|
234
|
+
* 车道的用途只有一个 —— 复核「这条 park 此刻仍在此 token 下」(`agent.parkedCheckpointToken === cp.token`,
|
|
235
|
+
* 行已在新 token 下 re-park ⇒ miss 不错配)。行被裁掉 = 这道复核**失明**,200「决定已交给宿主的下一跑」
|
|
236
|
+
* 就证不出它兑付的是哪一条 park。
|
|
237
|
+
* · 引擎 resume 时的赎回键取自 **resume journal** 的 parked 条目(`parkedResumeFor(cached.parked.terminal.token)`,
|
|
238
|
+
* :1327/:1727),不取自 run blob;不带决定的 resume 用同一 token **重新 park**(`parkAgentLeg(reparkRec, …)`,
|
|
239
|
+
* :1345/:1738),不是重跑。blob 的 parked 行在引擎侧唯一的读点是 `prior.agents` 那道「journal 缺条目」防线
|
|
240
|
+
* (:2118-2130)—— 身份被裁掉后失明的正是它:**只有 journal 也缺该条目时**,resume 才会把那次调用活跑。
|
|
241
|
+
* ⇒ 拒,并把真话与真恢复路径说出来:先不带决定 resume(引擎按 journal re-park 进一只新的、小的 run 行),
|
|
242
|
+
* 再对同一 checkpoint decide;re-park 后能否再赎回取决于新 blob 是否装得下身份行(不下 ⇒ 再落本臂)。
|
|
243
|
+
* 码是新的,与既有 `decide.workflow_*` 同族。
|
|
244
|
+
*
|
|
245
|
+
* 体上带 `runId` 与被裁掉的 park 数(`parks`)—— 后者是店给的计数,**不含任何身份位**(带了就等于没裁)。
|
|
246
|
+
*/
|
|
247
|
+
export declare function denyWorkflowParkIdentityLost(origin: Extract<ParkOrigin, {
|
|
248
|
+
kind: "workflow_identity_lost";
|
|
249
|
+
}>): ParkedDecideResult;
|
|
211
250
|
export declare function planWorkflowParkedDecide(origin: Extract<ParkOrigin, {
|
|
212
251
|
kind: "workflow";
|
|
213
252
|
}>, req: ParkedDecideRequest): WorkflowParkedDecidePlan;
|
package/dist/parked-decide.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { readParkIdentityLoss } from "./orchestration/workflow-agent-session-index.js";
|
|
1
2
|
import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
|
|
2
3
|
import { backgroundScopesForCheckpointScope, decodeCheckpointScope } from "./security.js";
|
|
4
|
+
import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
|
|
3
5
|
export async function findParkOrigin(sources, cp) {
|
|
4
6
|
if (sources.agentStore !== undefined) {
|
|
5
7
|
let match;
|
|
@@ -24,7 +26,11 @@ export async function findParkOrigin(sources, cp) {
|
|
|
24
26
|
if (run === null)
|
|
25
27
|
return undefined;
|
|
26
28
|
const agent = (run.agents ?? []).find((a) => a.sessionId === cp.sessionId && a.status === "parked");
|
|
27
|
-
if (agent === undefined
|
|
29
|
+
if (agent === undefined) {
|
|
30
|
+
const loss = readParkIdentityLoss(run);
|
|
31
|
+
return loss === undefined ? undefined : { kind: "workflow_identity_lost", runId: row.runId, run, loss };
|
|
32
|
+
}
|
|
33
|
+
if (agent.parkedCheckpointToken !== cp.token)
|
|
28
34
|
return undefined;
|
|
29
35
|
return { kind: "workflow", runId: row.runId, run, agent };
|
|
30
36
|
}
|
|
@@ -80,6 +86,17 @@ export function buildParkedResumeOutcome(req) {
|
|
|
80
86
|
hostDecision: req.hostDecision,
|
|
81
87
|
};
|
|
82
88
|
}
|
|
89
|
+
export function denyWorkflowParkIdentityLost(origin) {
|
|
90
|
+
return {
|
|
91
|
+
status: 409,
|
|
92
|
+
body: {
|
|
93
|
+
error: "this parked workflow agent's recovery identity was dropped when the run store slimmed an oversize run blob — the server can no longer verify that the row is still parked under this checkpoint token, so the decision is not delivered. The park itself is kept by the engine's resume journal, not by this blob: resume the run without a decision (the engine re-parks the child under the same token into a fresh run row), then decide again; only if that journal entry is also missing would a resume run the call live",
|
|
94
|
+
errorCode: "decide.workflow_park_identity_lost",
|
|
95
|
+
runId: origin.runId,
|
|
96
|
+
parksDropped: origin.loss.parks,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
83
100
|
export function planWorkflowParkedDecide(origin, req) {
|
|
84
101
|
if (req.grantRemember !== undefined) {
|
|
85
102
|
return {
|
|
@@ -115,8 +132,9 @@ export function planWorkflowParkedDecide(origin, req) {
|
|
|
115
132
|
runId: origin.runId,
|
|
116
133
|
outcome: buildParkedResumeOutcome(req),
|
|
117
134
|
message: `The human approval that workflow run "${origin.runId}" parked on (agent callKey "${origin.agent.callKey}") has been ${verdict}. ` +
|
|
118
|
-
`Resume that run now
|
|
119
|
-
`
|
|
135
|
+
`Resume that run now: re-invoke Workflow with the SAME script source you launched it from (script / name / scriptPath) ` +
|
|
136
|
+
`AND resumeFromRunId: "${origin.runId}" — the decision is already held by the deployment and will be applied to the parked ` +
|
|
137
|
+
`agent; completed steps replay from the journal. Do not re-issue the original call.`,
|
|
120
138
|
};
|
|
121
139
|
}
|
|
122
140
|
export async function decideParkedAgent(deps, req, match) {
|
|
@@ -126,7 +144,7 @@ export async function decideParkedAgent(deps, req, match) {
|
|
|
126
144
|
return { status: 400, body: { error: "remember cannot be applied: this parked agent row carries no root/parent session anchor for the exemption", errorCode: "decide.parked_remember_unsupported", taskId: match.handle } };
|
|
127
145
|
}
|
|
128
146
|
const pendingTool = req.pendingAction?.toolName;
|
|
129
|
-
if (pendingTool ===
|
|
147
|
+
if (pendingTool === ASK_USER_QUESTION_TOOL_NAME && req.decision === "approve" && req.answer === undefined) {
|
|
130
148
|
return { status: 400, body: { error: "pending action is AskUserQuestion — approve requires body.answer (the operator's answers[]); deny needs none", errorCode: "decide.parked_answer_required", taskId: match.handle } };
|
|
131
149
|
}
|
|
132
150
|
if (row.name === undefined) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { contentKindOf } from "../approval-content-kind.js";
|
|
1
|
+
import { ASK_USER_QUESTION_SQL_LITERAL, contentKindOf } from "../approval-content-kind.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { APPROVAL_GATE_KINDS_SQL_IN } from "../tool-approval.js";
|
|
4
4
|
import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, ExecutionOutcomeConflictError, executionRecordDisposition, executionVerdict, } from "@sema-agent/core";
|
|
@@ -487,14 +487,14 @@ export class SqlCheckpointStore {
|
|
|
487
487
|
}
|
|
488
488
|
async reapExpired(cutoff) {
|
|
489
489
|
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at_ms = ? " +
|
|
490
|
-
`WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= ? AND (gate_kind IS NULL OR gate_kind NOT IN ${APPROVAL_GATE_KINDS_SQL_IN} OR COALESCE(tool_name,'') =
|
|
491
|
-
`WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= $2 AND (gate_kind IS NULL OR gate_kind NOT IN ${APPROVAL_GATE_KINDS_SQL_IN} OR COALESCE(tool_name,'') =
|
|
490
|
+
`WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= ? AND (gate_kind IS NULL OR gate_kind NOT IN ${APPROVAL_GATE_KINDS_SQL_IN} OR COALESCE(tool_name,'') = ${ASK_USER_QUESTION_SQL_LITERAL})) OR (terminal_at_ms IS NOT NULL AND terminal_at_ms <= ?))`, "UPDATE checkpoint SET status = 'expired', decided_at_ms = $1 " +
|
|
491
|
+
`WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= $2 AND (gate_kind IS NULL OR gate_kind NOT IN ${APPROVAL_GATE_KINDS_SQL_IN} OR COALESCE(tool_name,'') = ${ASK_USER_QUESTION_SQL_LITERAL})) OR (terminal_at_ms IS NOT NULL AND terminal_at_ms <= $3))`), [Date.now(), cutoff, cutoff]);
|
|
492
492
|
return res.affected;
|
|
493
493
|
}
|
|
494
494
|
async listExpiredApprovalGates(cutoff, limit = 100) {
|
|
495
495
|
const { rows } = await this.db.query(this.q(`SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ${APPROVAL_GATE_KINDS_SQL_IN} ` +
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
`AND COALESCE(tool_name,'') <> ${ASK_USER_QUESTION_SQL_LITERAL} AND deadline IS NOT NULL AND deadline <= ? ORDER BY deadline ASC LIMIT ?`, `SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ${APPROVAL_GATE_KINDS_SQL_IN} ` +
|
|
497
|
+
`AND COALESCE(tool_name,'') <> ${ASK_USER_QUESTION_SQL_LITERAL} AND deadline IS NOT NULL AND deadline <= $1 ORDER BY deadline ASC LIMIT $2`), [cutoff, limit]);
|
|
498
498
|
return rows.map((r) => ({ sessionId: String(r.session_id), scope: String(r.scope) }));
|
|
499
499
|
}
|
|
500
500
|
async listPending(scope) {
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* memory `capability-probe-fail-direction`;误杀的代价是杀死一个活任务的手,漏杀的代价只是钱):
|
|
11
11
|
* a. `metadata.semaManaged !== "1"` → 非我方铸造,provider 侧 query 过滤已排除;防御性重验。
|
|
12
12
|
* b. `metadata.taskId` 缺席 → 病态形(factory 恒打标),不杀 + failOpen 留痕。
|
|
13
|
-
* c. run
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* c. run 行在场(词 → 类问 `RUN_STATUS_CLASS` 属主,S-221):
|
|
14
|
+
* 类 live / parked(行还能再动) → 活,跳过;
|
|
15
|
+
* 类 succeeded / unsuccessful(终局) → 孤儿,kill;
|
|
16
|
+
* 属主表外的词(类 undefined) → 词表纪律([ref]):不杀 + failOpen(新词先安全后收编)。
|
|
17
17
|
* d. 行不存在:
|
|
18
18
|
* startedAt 距今 < GRACE → create→行落库窗口,跳过(防误杀新沙盒);
|
|
19
19
|
* ≥ GRACE → 真孤儿,kill(单库 fleet 方向下行跨副本可见)。
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
2
|
+
import { runStatusClassOf } from "./store-contracts.js";
|
|
2
3
|
export const E2B_ORPHAN_GRACE_MS = 10 * 60 * 1000;
|
|
3
4
|
export const SEMA_MANAGED_METADATA_KEY = "semaManaged";
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const CLASS_VERDICT = {
|
|
6
|
+
live: "live",
|
|
7
|
+
parked: "live",
|
|
8
|
+
succeeded: "kill",
|
|
9
|
+
unsuccessful: "kill",
|
|
10
|
+
};
|
|
8
11
|
export async function reclaimOrphanE2bSandboxes(deps) {
|
|
9
12
|
const now = deps.now ?? Date.now;
|
|
10
13
|
const graceMs = deps.graceMs ?? E2B_ORPHAN_GRACE_MS;
|
|
@@ -29,28 +32,15 @@ export async function reclaimOrphanE2bSandboxes(deps) {
|
|
|
29
32
|
if (run === undefined) {
|
|
30
33
|
verdict = now() - sbx.startedAt.getTime() < graceMs ? "grace" : "kill";
|
|
31
34
|
}
|
|
32
|
-
else if (LIVE_STATUSES.has(run.status)) {
|
|
33
|
-
verdict = "live";
|
|
34
|
-
}
|
|
35
|
-
else if (TERMINAL_STATUSES.has(run.status)) {
|
|
36
|
-
verdict = "kill";
|
|
37
|
-
}
|
|
38
35
|
else {
|
|
39
|
-
|
|
36
|
+
const cls = runStatusClassOf(run.status);
|
|
37
|
+
verdict = cls === undefined ? "unknown" : CLASS_VERDICT[cls];
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
40
|
catch {
|
|
43
41
|
verdict = "unknown";
|
|
44
42
|
}
|
|
45
|
-
if (verdict === "
|
|
46
|
-
summary.skippedLive++;
|
|
47
|
-
else if (verdict === "grace")
|
|
48
|
-
summary.skippedGrace++;
|
|
49
|
-
else if (verdict === "unknown") {
|
|
50
|
-
summary.skippedUnknown++;
|
|
51
|
-
recordFailOpen("server.e2b.orphan-skip-unknown");
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
43
|
+
if (verdict === "kill") {
|
|
54
44
|
try {
|
|
55
45
|
await deps.provider.kill(sbx.sandboxId);
|
|
56
46
|
summary.reclaimed++;
|
|
@@ -61,6 +51,14 @@ export async function reclaimOrphanE2bSandboxes(deps) {
|
|
|
61
51
|
deps.logger?.warn("e2b_orphan_kill_failed", { sandboxId: sbx.sandboxId, taskId, error: e instanceof Error ? e.message : String(e) });
|
|
62
52
|
}
|
|
63
53
|
}
|
|
54
|
+
else if (verdict === "live")
|
|
55
|
+
summary.skippedLive++;
|
|
56
|
+
else if (verdict === "grace")
|
|
57
|
+
summary.skippedGrace++;
|
|
58
|
+
else {
|
|
59
|
+
summary.skippedUnknown++;
|
|
60
|
+
recordFailOpen("server.e2b.orphan-skip-unknown");
|
|
61
|
+
}
|
|
64
62
|
}
|
|
65
63
|
if (summary.reclaimed > 0 || summary.killFailed > 0) {
|
|
66
64
|
deps.logger?.info("e2b_orphan_reclaim_round", { ...summary });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { contentKindOf } from "../approval-content-kind.js";
|
|
1
|
+
import { ASK_USER_QUESTION_TOOL_NAME, contentKindOf } from "../approval-content-kind.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -249,7 +249,7 @@ export class LocalCheckpointStore {
|
|
|
249
249
|
const toolName = cp.pendingAction?.toolName ?? "";
|
|
250
250
|
const deadlineBranch = cp.deadline !== undefined &&
|
|
251
251
|
cp.deadline <= cutoff &&
|
|
252
|
-
(gateKind === undefined || !isApprovalGateKind(gateKind) || toolName ===
|
|
252
|
+
(gateKind === undefined || !isApprovalGateKind(gateKind) || toolName === ASK_USER_QUESTION_TOOL_NAME);
|
|
253
253
|
const terminalBranch = terminalAtOf(cp) <= cutoff;
|
|
254
254
|
if ((deadlineBranch || terminalBranch) && (await this.inner.expire(token, cp.scope)))
|
|
255
255
|
n++;
|
|
@@ -263,7 +263,7 @@ export class LocalCheckpointStore {
|
|
|
263
263
|
const toolName = cp.pendingAction?.toolName ?? "";
|
|
264
264
|
if (gateKind !== undefined &&
|
|
265
265
|
isApprovalGateKind(gateKind) &&
|
|
266
|
-
toolName !==
|
|
266
|
+
toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
267
267
|
cp.deadline !== undefined &&
|
|
268
268
|
cp.deadline <= cutoff)
|
|
269
269
|
hits.push({ sessionId: cp.sessionId, scope: cp.scope, deadline: cp.deadline });
|
|
@@ -6,6 +6,15 @@ export interface AdbEnvConfig {
|
|
|
6
6
|
serial: string;
|
|
7
7
|
/** Path to the adb binary. Default "adb" (must be on PATH in the runner image). */
|
|
8
8
|
adbPath?: string;
|
|
9
|
+
/**
|
|
10
|
+
* S-213④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的
|
|
11
|
+
* 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。
|
|
12
|
+
* 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask),
|
|
13
|
+
* 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。
|
|
14
|
+
* 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来;
|
|
15
|
+
* adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。
|
|
16
|
+
*/
|
|
17
|
+
homeDir?: string;
|
|
9
18
|
/** On-device workspace root; relative paths resolve against it. Default "/data/local/tmp". */
|
|
10
19
|
mountPath?: string;
|
|
11
20
|
commandTimeoutMs?: number;
|
|
@@ -48,6 +57,8 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
48
57
|
* 同姿态的 ssh 腿早有先例(`remote-env-ssh.ts`「destroy 后一律不复活,显式 connect/reconnect 同禁」),
|
|
49
58
|
* 五条兄弟腿全部 fail-closed,只有 adb 例外 —— 补齐它。 */
|
|
50
59
|
private destroyed;
|
|
60
|
+
/** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
|
|
61
|
+
readonly homeDir?: string | undefined;
|
|
51
62
|
private readonly cfg;
|
|
52
63
|
private readonly spawnFn;
|
|
53
64
|
private readonly injectedConnectOnce?;
|
|
@@ -22,6 +22,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
22
22
|
cwd;
|
|
23
23
|
connected = false;
|
|
24
24
|
destroyed = false;
|
|
25
|
+
homeDir;
|
|
25
26
|
cfg;
|
|
26
27
|
spawnFn;
|
|
27
28
|
injectedConnectOnce;
|
|
@@ -35,6 +36,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
35
36
|
retry: { maxAttempts: config.retry?.maxAttempts ?? 3, backoffMs: config.retry?.backoffMs ?? 800 },
|
|
36
37
|
};
|
|
37
38
|
this.cwd = this.cfg.mountPath;
|
|
39
|
+
this.homeDir = config.homeDir;
|
|
38
40
|
this.spawnFn = deps?.spawn ?? spawn;
|
|
39
41
|
this.injectedConnectOnce = deps?.connectOnce;
|
|
40
42
|
}
|
|
@@ -54,6 +54,9 @@ export interface DeviceEnvLogger {
|
|
|
54
54
|
info?(msg: string, fields?: Record<string, unknown>): void;
|
|
55
55
|
}
|
|
56
56
|
export interface DeviceEnvFactoryDeps {
|
|
57
|
+
/** S-213④ / core [ref]:设备上执行用户的 home(`ExecutionEnv.homeDir` 座)。由 `boot/execution-env.ts`
|
|
58
|
+
* 从唯一属主 `executionLaneHomeDir` 递进;缺席 ⇒ 本 env 声明「没有 home」,`~/` 规则判 unreadable。 */
|
|
59
|
+
homeDir?: string;
|
|
57
60
|
hub: DeviceWsHub;
|
|
58
61
|
store: DeviceStore;
|
|
59
62
|
enrollment: DeviceEnrollment;
|
|
@@ -71,6 +74,10 @@ interface DeviceEnvConfig {
|
|
|
71
74
|
deviceId: string;
|
|
72
75
|
owner: DeviceOwner;
|
|
73
76
|
cwd: string;
|
|
77
|
+
/** S-213④ / core [ref] —— 员工设备上执行用户的 home(`ExecutionEnv.homeDir` 座)。缺席 ⇒ 本 env 声明
|
|
78
|
+
* 「我没有 home」,`~/` 形权限规则对本腿判 unreadable(fail-closed)。值经 `boot/execution-env.ts`
|
|
79
|
+
* 从唯一属主 `executionLaneHomeDir` 递进;设备是运行期连进来的,boot 期只有运维显式声明才有值。 */
|
|
80
|
+
homeDir?: string;
|
|
74
81
|
shellEnv?: Record<string, string>;
|
|
75
82
|
execTimeoutMs: number;
|
|
76
83
|
/** 设备当前连接声明的指令面(`undefined` = 铸 env 时设备不在线 ⇒ 可选面一律缺席)。 */
|
|
@@ -79,6 +86,8 @@ interface DeviceEnvConfig {
|
|
|
79
86
|
}
|
|
80
87
|
export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
|
|
81
88
|
private readonly cfg;
|
|
89
|
+
/** core `ExecutionEnv.homeDir`([ref]):设备上执行用户的 home。见 `DeviceEnvConfig.homeDir`。 */
|
|
90
|
+
readonly homeDir?: string | undefined;
|
|
82
91
|
/**
|
|
83
92
|
* 🔴 `suspendable: false` 在 core 契约里**同时断言**「工作区在目标上外部持久」(remote-env.ts 的
|
|
84
93
|
* load-bearing 不变量)—— 设备工作区 = 员工机磁盘,真持久 ✅,park-only 降级腿因此合法可用。
|
|
@@ -109,6 +109,7 @@ function assertPosixDevicePlatform(os, deviceId) {
|
|
|
109
109
|
}
|
|
110
110
|
export class RemoteDeviceExecutionEnv {
|
|
111
111
|
cfg;
|
|
112
|
+
homeDir;
|
|
112
113
|
capabilities = { isolation: false, suspendable: false };
|
|
113
114
|
externalContentTarget = true;
|
|
114
115
|
hostLocalPaths = false;
|
|
@@ -121,6 +122,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
121
122
|
constructor(cfg) {
|
|
122
123
|
this.cfg = cfg;
|
|
123
124
|
this.cwd = cfg.cwd;
|
|
125
|
+
this.homeDir = cfg.homeDir;
|
|
124
126
|
if (cfg.supports?.has("readLink") === true)
|
|
125
127
|
this.readLink = (p, s) => this.doReadLink(p, s);
|
|
126
128
|
if (cfg.supports?.has("writeFileExclusive") === true)
|
|
@@ -724,6 +726,7 @@ export function deviceExecutionEnvFactory(deps) {
|
|
|
724
726
|
deviceId: binding.deviceId,
|
|
725
727
|
owner: binding.owner,
|
|
726
728
|
cwd: effectiveDeviceCwd(deps.perSessionCwd?.get(ctx.sessionId), ctx.parentCwd, view.row),
|
|
729
|
+
...(deps.homeDir === undefined ? {} : { homeDir: deps.homeDir }),
|
|
727
730
|
...(deps.perSessionShellEnv?.get(ctx.sessionId) === undefined ? {} : { shellEnv: deps.perSessionShellEnv.get(ctx.sessionId) }),
|
|
728
731
|
execTimeoutMs,
|
|
729
732
|
supports: deps.hub.declaredSupports(binding.deviceId),
|
|
@@ -41,6 +41,15 @@ export interface RemoteContainerEnvConfig extends RemoteEnvObservers {
|
|
|
41
41
|
template?: string;
|
|
42
42
|
/** Sandbox lifetime in ms (E2B billing window). Default {@link DEFAULT_TIMEOUT_MS}. */
|
|
43
43
|
timeoutMs?: number;
|
|
44
|
+
/**
|
|
45
|
+
* S-213④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的
|
|
46
|
+
* 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。
|
|
47
|
+
* 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask),
|
|
48
|
+
* 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。
|
|
49
|
+
* 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来;
|
|
50
|
+
* adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。
|
|
51
|
+
*/
|
|
52
|
+
homeDir?: string;
|
|
44
53
|
/** Workspace root inside the VM; remote paths normalize against it. Default {@link DEFAULT_MOUNT_PATH}. */
|
|
45
54
|
mountPath?: string;
|
|
46
55
|
/** Outbound network for the sandbox (clay: service needs external-network capability). Default `true`. */
|
|
@@ -89,6 +98,8 @@ export interface RemoteContainerEnvDeps {
|
|
|
89
98
|
export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv, BackgroundShellCapability {
|
|
90
99
|
/** Current working directory for relative paths (FileSystem contract; mutable, core may reassign). */
|
|
91
100
|
cwd: string;
|
|
101
|
+
/** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
|
|
102
|
+
readonly homeDir?: string | undefined;
|
|
92
103
|
private readonly cfg;
|
|
93
104
|
private readonly sandboxApi;
|
|
94
105
|
private sbx?;
|
|
@@ -18,6 +18,7 @@ const E2B_BG_PROVIDER_BACKSTOP_PAD_SEC = 30;
|
|
|
18
18
|
const ok = (value) => ({ ok: true, value });
|
|
19
19
|
export class RemoteContainerExecutionEnv {
|
|
20
20
|
cwd;
|
|
21
|
+
homeDir;
|
|
21
22
|
cfg;
|
|
22
23
|
sandboxApi;
|
|
23
24
|
sbx;
|
|
@@ -38,6 +39,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
38
39
|
dataTimeoutMs: config.dataTimeoutMs ?? DEFAULT_DATA_TIMEOUT_MS,
|
|
39
40
|
};
|
|
40
41
|
this.cwd = this.cfg.mountPath;
|
|
42
|
+
this.homeDir = config.homeDir;
|
|
41
43
|
this.sandboxApi = deps.sandboxApi ?? Sandbox;
|
|
42
44
|
}
|
|
43
45
|
capabilities = { isolation: true, suspendable: true };
|
|
@@ -20,6 +20,15 @@ export interface HostEnvConfig {
|
|
|
20
20
|
* own `process.env` is the default base (a local coding agent inherits the user's environment).
|
|
21
21
|
*/
|
|
22
22
|
env?: Record<string, string>;
|
|
23
|
+
/**
|
|
24
|
+
* S-213④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的
|
|
25
|
+
* 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。
|
|
26
|
+
* 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask),
|
|
27
|
+
* 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。
|
|
28
|
+
* 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来;
|
|
29
|
+
* adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。
|
|
30
|
+
*/
|
|
31
|
+
homeDir?: string;
|
|
23
32
|
/** Per-command wall-clock bound (ms) when the caller gives none. Default {@link DEFAULT_COMMAND_TIMEOUT_MS}. */
|
|
24
33
|
commandTimeoutMs?: number;
|
|
25
34
|
/** Shell-child env policy (D1; mirrors core's `NodeExecutionEnv`). Default `"scrub"`: inherit the worker's
|
|
@@ -64,6 +73,8 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
64
73
|
list(ctx: SchedulerContext): Promise<Result<ScheduledTaskSummary[], SchedulerError>>;
|
|
65
74
|
/** Working directory; relative paths resolve against it (ExecutionEnv contract). Starts at the workspace root. */
|
|
66
75
|
cwd: string;
|
|
76
|
+
/** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
|
|
77
|
+
readonly homeDir?: string | undefined;
|
|
67
78
|
private readonly workspaceDir;
|
|
68
79
|
private readonly cfg;
|
|
69
80
|
/** Workspace created on disk (lazily, on first use). */
|
|
@@ -53,6 +53,7 @@ export class RemoteHostExecutionEnv {
|
|
|
53
53
|
: Promise.resolve({ ok: false, error: new SchedulerError("unsupported", "scheduler not enabled on this worker") });
|
|
54
54
|
}
|
|
55
55
|
cwd;
|
|
56
|
+
homeDir;
|
|
56
57
|
workspaceDir;
|
|
57
58
|
cfg;
|
|
58
59
|
prepared = false;
|
|
@@ -84,6 +85,7 @@ export class RemoteHostExecutionEnv {
|
|
|
84
85
|
commandTimeoutMs: config.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
|
|
85
86
|
};
|
|
86
87
|
this.cwd = this.workspaceDir;
|
|
88
|
+
this.homeDir = config.homeDir;
|
|
87
89
|
this.handle = { sandboxId: this.workspaceDir, provider: PROVIDER, mountPath: this.workspaceDir };
|
|
88
90
|
}
|
|
89
91
|
workspaceHandle() {
|
|
@@ -16,6 +16,15 @@ export interface K8sEnvConfig {
|
|
|
16
16
|
image: string;
|
|
17
17
|
/** RuntimeClass for the VM boundary. Default "kata-qemu" (set "" to run plain runc — tests/dev only). */
|
|
18
18
|
runtimeClass?: string;
|
|
19
|
+
/**
|
|
20
|
+
* S-213④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的
|
|
21
|
+
* 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。
|
|
22
|
+
* 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask),
|
|
23
|
+
* 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。
|
|
24
|
+
* 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来;
|
|
25
|
+
* adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。
|
|
26
|
+
*/
|
|
27
|
+
homeDir?: string;
|
|
19
28
|
/** Workspace root inside the pod; created on connect. Default "/workspace". */
|
|
20
29
|
mountPath?: string;
|
|
21
30
|
/** Pod lifetime (activeDeadlineSeconds) — the leak guard. Default 30min. */
|
|
@@ -126,6 +135,8 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
126
135
|
private inFlightExecs;
|
|
127
136
|
/** Memoized in-flight connect (lazy-connect; prevents concurrent double pod-create — the E2B race lesson). */
|
|
128
137
|
private connectPromise?;
|
|
138
|
+
/** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
|
|
139
|
+
readonly homeDir?: string | undefined;
|
|
129
140
|
private readonly cfg;
|
|
130
141
|
private readonly deps;
|
|
131
142
|
constructor(config: K8sEnvConfig, deps?: K8sEnvDeps);
|