@sema-agent/server 3.20.0 → 3.22.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/dist/approval-hmac.d.ts +9 -9
- package/dist/approval-hmac.js +0 -31
- package/dist/approval.js +8 -1
- package/dist/boot/config-center.d.ts +3 -2
- package/dist/boot/resolve-spec.js +4 -4
- package/dist/boot/session-faces.js +3 -2
- package/dist/boot/workflow-orchestration.js +1 -1
- package/dist/budget.d.ts +2 -2
- package/dist/budget.js +11 -6
- package/dist/fleet/fleet-bus.d.ts +2 -0
- package/dist/fleet/fleet-bus.js +34 -7
- package/dist/hooks/hook-llm.d.ts +2 -2
- package/dist/hooks/hook-llm.js +1 -1
- package/dist/http/active-run-conflict.d.ts +68 -0
- package/dist/http/active-run-conflict.js +89 -0
- package/dist/http/principal-gate.d.ts +7 -3
- package/dist/http/principal-gate.js +7 -5
- package/dist/http/route-ctx.d.ts +34 -25
- package/dist/http/routes/approvals-assistant.js +5 -5
- package/dist/http/routes/images.js +8 -8
- package/dist/http/routes/runs.js +3 -1
- package/dist/http/routes/tasks.js +5 -3
- package/dist/http/server.d.ts +2 -2
- package/dist/http/server.js +2 -2
- package/dist/key-resolver.d.ts +7 -1
- package/dist/key-resolver.js +0 -23
- package/dist/leader/wire.d.ts +19 -1
- package/dist/leader/wire.js +48 -18
- package/dist/main.js +2 -2
- package/dist/parked-decide.js +4 -1
- package/dist/plugins/file-run-store.js +21 -8
- package/dist/plugins/host-platform.d.ts +11 -24
- package/dist/plugins/host-platform.js +14 -0
- package/dist/plugins/memory-engine-tidb.js +16 -0
- package/dist/plugins/memory-run-store.js +19 -8
- package/dist/plugins/remote-env-adb.js +12 -5
- package/dist/plugins/remote-env-local-docker.js +3 -7
- package/dist/plugins/remote-env-ssh.js +17 -2
- package/dist/plugins/run-store-sql.js +18 -12
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +2 -2
- package/dist/plugins/tool-result-store-sql.d.ts +7 -1
- package/dist/plugins/tool-result-store-sql.js +9 -8
- package/dist/plugins/workflow-run-store-sql.d.ts +11 -6
- package/dist/plugins/workflow-run-store-sql.js +18 -8
- package/dist/session-sync.js +6 -3
- package/package.json +2 -2
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -75,6 +75,38 @@ export interface RouteRequestState {
|
|
|
75
75
|
/** 凭证派生的调用方系统身份(`source`)。handle() 的鉴权门是**唯一**赋值点,域模块只读。 */
|
|
76
76
|
source: string | null;
|
|
77
77
|
}
|
|
78
|
+
/** B4①(命名化):`prepareSpec` 的返回形(tasks.ts/runs.ts 两域共同消费的属性面)——原为 6 成员内联匿名对象。
|
|
79
|
+
* 命名后好处不只是过 B4 门:两个消费域现在都能对同一个符号做 `import type` 标注,而不是各自重推断结构。 */
|
|
80
|
+
export interface PreparedTaskSubmission {
|
|
81
|
+
spec: TaskSpec;
|
|
82
|
+
auth?: RequestAuth;
|
|
83
|
+
verify?: {
|
|
84
|
+
maxRounds: number;
|
|
85
|
+
costCeilingMicroUsd?: number;
|
|
86
|
+
};
|
|
87
|
+
cascade?: boolean;
|
|
88
|
+
jobId?: string;
|
|
89
|
+
body: TaskRequestBody;
|
|
90
|
+
}
|
|
91
|
+
/** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
|
|
92
|
+
export interface DriveResumeArgs {
|
|
93
|
+
token: CheckpointToken;
|
|
94
|
+
sessionId: string;
|
|
95
|
+
principal: string | undefined;
|
|
96
|
+
fleetScope: string;
|
|
97
|
+
taskConfig: Omit<TaskSpec, "objective" | "sessionId">;
|
|
98
|
+
resumeObjective: string;
|
|
99
|
+
outcome: ResumeOutcome;
|
|
100
|
+
verifyRounds: {
|
|
101
|
+
maxRounds: number;
|
|
102
|
+
costCeilingMicroUsd?: number;
|
|
103
|
+
} | undefined;
|
|
104
|
+
/** codex M2: side-effects that must land AFTER the markResuming CAS is WON (decide accepted, sibling races
|
|
105
|
+
* lost — never fires on the 409/404 paths) and BEFORE the model leg drives (so the resumed leg's own next
|
|
106
|
+
* ask sees them — the decide leg's exemption grant). Contract: must not throw (callers swallow internally);
|
|
107
|
+
* awaited so the ordering guarantee is real, and a store write here is ms-scale vs the model leg. */
|
|
108
|
+
onResumeCommitted?: () => Promise<void>;
|
|
109
|
+
}
|
|
78
110
|
/** 提交/续跑「腿」= 跨域共享的**有状态**长流程(不像 {@link RouteHelpers} 那样只依赖 deps/config:它们要写
|
|
79
111
|
* run log、发 fleet 帧、走 markResuming CAS、驱动模型)。A9 上批的停点就在这里——tasks/runs 两域共用
|
|
80
112
|
* `prepareSpec`,approvals/assistant/notify-wake 三域共用 resume 家族,把它们跟着任一域搬都会造出
|
|
@@ -82,17 +114,7 @@ export interface RouteRequestState {
|
|
|
82
114
|
* 装进这一格,域模块经 `ctx.legs.*` 调用。类型化 ⇒ 腿的形漂了是编译红,不是运行时静默 404。 */
|
|
83
115
|
export interface RouteLegs {
|
|
84
116
|
/** POST /v1/tasks · /v1/tasks/stream · /v1/runs 的共同前段:读体 → 校验 → resolveSpec。null = 已应答(400/…)。 */
|
|
85
|
-
prepareSpec(req: IncomingMessage, res: ServerResponse): Promise<
|
|
86
|
-
spec: TaskSpec;
|
|
87
|
-
auth?: RequestAuth;
|
|
88
|
-
verify?: {
|
|
89
|
-
maxRounds: number;
|
|
90
|
-
costCeilingMicroUsd?: number;
|
|
91
|
-
};
|
|
92
|
-
cascade?: boolean;
|
|
93
|
-
jobId?: string;
|
|
94
|
-
body: TaskRequestBody;
|
|
95
|
-
} | null>;
|
|
117
|
+
prepareSpec(req: IncomingMessage, res: ServerResponse): Promise<PreparedTaskSubmission | null>;
|
|
96
118
|
/** 同步腿的终局记账(计费/配额/指标),tasks 域用。 */
|
|
97
119
|
finalizeTaskResult(result: TaskResult, principal: string | undefined, objective: string, sessionId: string | undefined): void;
|
|
98
120
|
/** approvals 决策腿:session → pending checkpoint → markResuming CAS → 驱动续跑。 */
|
|
@@ -101,20 +123,7 @@ export interface RouteLegs {
|
|
|
101
123
|
body: object;
|
|
102
124
|
}>;
|
|
103
125
|
/** 全 resume 家族的共同下半场(lease admission + 写 run log + fleet 发布)。 */
|
|
104
|
-
driveResumeIntoRunLog(args: {
|
|
105
|
-
token: CheckpointToken;
|
|
106
|
-
sessionId: string;
|
|
107
|
-
principal: string | undefined;
|
|
108
|
-
fleetScope: string;
|
|
109
|
-
taskConfig: Omit<TaskSpec, "objective" | "sessionId">;
|
|
110
|
-
resumeObjective: string;
|
|
111
|
-
outcome: ResumeOutcome;
|
|
112
|
-
verifyRounds: {
|
|
113
|
-
maxRounds: number;
|
|
114
|
-
costCeilingMicroUsd?: number;
|
|
115
|
-
} | undefined;
|
|
116
|
-
onResumeCommitted?: () => Promise<void>;
|
|
117
|
-
}): Promise<{
|
|
126
|
+
driveResumeIntoRunLog(args: DriveResumeArgs): Promise<{
|
|
118
127
|
status: number;
|
|
119
128
|
body: object;
|
|
120
129
|
}>;
|
|
@@ -92,7 +92,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
const operators = deps.config.operatorPrincipals;
|
|
95
|
-
const isExplicitOperator = operators
|
|
95
|
+
const isExplicitOperator = explicitOperatorOk(principal, operators); // E1: single mint point (principal-gate)
|
|
96
96
|
if (!isExplicitOperator) {
|
|
97
97
|
// 🔴 fail-CLOSED owner gate (both reviewers, 2026-07-13): mirror the sibling owner-gated routes
|
|
98
98
|
// (session policy/delete) — ownerOf REQUIRED (absent ⇒ 501, never open), a store error PROPAGATES
|
|
@@ -292,7 +292,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
292
292
|
// + the cancel/decide precedent). NOT the bare isOperator (its empty-list "true-for-all" would let any
|
|
293
293
|
// caller preempt anyone — same trap the /decide owner-gate avoids).
|
|
294
294
|
const operators = deps.config.operatorPrincipals;
|
|
295
|
-
const explicitOperator = operators
|
|
295
|
+
const explicitOperator = explicitOperatorOk(principal, operators); // E1: single mint point (principal-gate)
|
|
296
296
|
if (!explicitOperator && run.owner !== null && run.owner !== principal) {
|
|
297
297
|
sendError(res, 404, "not_found.run", "task not found");
|
|
298
298
|
return;
|
|
@@ -349,7 +349,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
349
349
|
return;
|
|
350
350
|
}
|
|
351
351
|
const operators = deps.config.operatorPrincipals;
|
|
352
|
-
const explicitOperator = operators
|
|
352
|
+
const explicitOperator = explicitOperatorOk(principal, operators); // E1: single mint point (principal-gate)
|
|
353
353
|
if (!explicitOperator && run.owner !== null && run.owner !== principal) {
|
|
354
354
|
sendError(res, 404, "not_found.run", "task not found");
|
|
355
355
|
return;
|
|
@@ -436,7 +436,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
436
436
|
deciderPrincipal = proof.principal;
|
|
437
437
|
}
|
|
438
438
|
const operators = deps.config.operatorPrincipals;
|
|
439
|
-
const explicitOperator = operators
|
|
439
|
+
const explicitOperator = explicitOperatorOk(deciderPrincipal, operators); // E1: single mint point (principal-gate)
|
|
440
440
|
if (!explicitOperator && run.owner !== null && run.owner !== deciderPrincipal) {
|
|
441
441
|
sendError(res, 404, "not_found.run", "task not found");
|
|
442
442
|
return;
|
|
@@ -579,7 +579,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
579
579
|
// credential sessionId+boundInputHash (from a log/screenshot/ticket). A non-owner-non-operator gets 404
|
|
580
580
|
// (no existence oracle — parity with runOwnerOk + the cancel P0 precedent), never 403.
|
|
581
581
|
const operators = deps.config.operatorPrincipals;
|
|
582
|
-
const explicitOperator = operators
|
|
582
|
+
const explicitOperator = explicitOperatorOk(deciderPrincipal, operators); // E1: single mint point (principal-gate)
|
|
583
583
|
if (!explicitOperator) {
|
|
584
584
|
const cpScope = await cs.peekPendingScope(sessionId);
|
|
585
585
|
// "_" = the anonymous-submit scope SENTINEL (main.ts writes auth?.principal ?? "_" at suspend) — treat it
|
|
@@ -5,7 +5,7 @@ import { manifestToIndexEntry } from "../../images/manifest.js";
|
|
|
5
5
|
import { validateBake, buildBakeArgv } from "../../images/bake-validate.js";
|
|
6
6
|
import { streamSseLog } from "../sse-log.js";
|
|
7
7
|
import { sendJson, sendError } from "../send.js";
|
|
8
|
-
import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk
|
|
8
|
+
import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
9
9
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
10
10
|
export function createImagesLocal(deps) {
|
|
11
11
|
// Per-principal bake-submission rate guard (IMAGE-API-DESIGN.md §P2.4c — fills the register DoS gap the images
|
|
@@ -86,7 +86,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
86
86
|
// Identity resolved EXACTLY as P1 (direct-door JWT-without-cnf.bnd, else principalFrom).
|
|
87
87
|
const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
|
|
88
88
|
const operators = deps.config.operatorPrincipals;
|
|
89
|
-
const
|
|
89
|
+
const isExplicitOperator = explicitOperatorOk(principal, operators);
|
|
90
90
|
const cfg = deps.config.imageBakes;
|
|
91
91
|
// 🔴 H1: the bake-runner identity is DERIVED from its bearer CREDENTIAL (the token-derived `source`,
|
|
92
92
|
// resolved above from SERVICE_AUTH_TOKENS), NOT a self-asserted x-agent-principal/JWT the runner never
|
|
@@ -96,7 +96,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
96
96
|
const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
|
|
97
97
|
// POST /v1/images/bakes — operator-only submit. EXPLICIT operator gate (NOT the bare isOperator; §P2.4a).
|
|
98
98
|
if (req.method === "POST" && url === "/v1/images/bakes") {
|
|
99
|
-
if (!
|
|
99
|
+
if (!isExplicitOperator) {
|
|
100
100
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
@@ -198,7 +198,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
198
198
|
// GET /v1/images/bakes/:bakeId/events — resumable SSE (the live "baking…" view), run-trace-SSE-shaped (§P2.2).
|
|
199
199
|
const evMatch = req.method === "GET" ? BAKE_EVENTS_RE.exec(url) : null;
|
|
200
200
|
if (evMatch) {
|
|
201
|
-
if (!
|
|
201
|
+
if (!isExplicitOperator) {
|
|
202
202
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
@@ -214,7 +214,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
214
214
|
// POST /v1/images/bakes/:bakeId/cancel — cooperative cancel (durable flag; the runner kills the pgid, §P2.11).
|
|
215
215
|
const cancelMatch = req.method === "POST" ? BAKE_CANCEL_RE.exec(url) : null;
|
|
216
216
|
if (cancelMatch) {
|
|
217
|
-
if (!
|
|
217
|
+
if (!isExplicitOperator) {
|
|
218
218
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
@@ -337,7 +337,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
337
337
|
// the `/events|/cancel|/claim|/ingest` sub-actions (two-segment) win first.
|
|
338
338
|
const idMatch = req.method === "GET" ? BAKE_ID_RE.exec(url) : null;
|
|
339
339
|
if (idMatch) {
|
|
340
|
-
if (!
|
|
340
|
+
if (!isExplicitOperator) {
|
|
341
341
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
342
342
|
return;
|
|
343
343
|
}
|
|
@@ -362,10 +362,10 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
362
362
|
if (deps.imageIndex && url.startsWith("/v1/images")) {
|
|
363
363
|
const idx = deps.imageIndex;
|
|
364
364
|
const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
|
|
365
|
-
//
|
|
365
|
+
// explicitOperatorOk (NOT isOperator): for image VISIBILITY, an empty OPERATOR_PRINCIPALS must NOT make every
|
|
366
366
|
// caller an operator (that would leak tenant-scoped images). Adversarial-review HIGH — same fix as the P0.5
|
|
367
367
|
// per-task resolve in main.ts. (Approval operator gates below keep isOperator's legacy empty=all boundary.)
|
|
368
|
-
const operator =
|
|
368
|
+
const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
369
369
|
const viewer = { operator, tenantId: principal ?? null };
|
|
370
370
|
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
371
371
|
// GET /v1/images — the paginated catalog. ?profile=&status=&capability=&latest=true&limit=&cursor=
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -10,6 +10,7 @@ import { scopedIdempotencyKey } from "../idempotency.js";
|
|
|
10
10
|
import { streamSseLog } from "../sse-log.js";
|
|
11
11
|
import { normalizeRunEventType } from "../../trace/project.js";
|
|
12
12
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
13
|
+
import { buildActiveRunConflict } from "../active-run-conflict.js";
|
|
13
14
|
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
14
15
|
// server.ts 侧的 routeLabel / isBillableSubmitPath 仍要用下面这些正则,故本模块导出(方向恒为 server.ts → routes/*)。
|
|
15
16
|
export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
|
|
@@ -161,7 +162,8 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
161
162
|
if (clientTaskId && created.activeTaskId === clientTaskId) {
|
|
162
163
|
return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
|
|
163
164
|
}
|
|
164
|
-
|
|
165
|
+
// [2255]① 真出路材料:activeTaskStatus + parked 时 pendingGate{kind,decidePath}(best-effort,失败退旧形)
|
|
166
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore }, sessionId, created.activeTaskId) };
|
|
165
167
|
}
|
|
166
168
|
// Durable F4: persist the resume rebuild inputs (sessionId-keyed) so an operator can resume from any
|
|
167
169
|
// replica even after this worker is gone — core's checkpoint blob can't carry service scenario context.
|
|
@@ -10,6 +10,7 @@ import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEve
|
|
|
10
10
|
import { cascadeConfig, runMeta } from "../run-meta.js";
|
|
11
11
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
12
12
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
13
|
+
import { buildActiveRunConflict, toDoneFrameResult } from "../active-run-conflict.js";
|
|
13
14
|
import { headerStr, gatedPrincipal } from "../principal-gate.js";
|
|
14
15
|
export async function handleTasks(req, res, url, ctx) {
|
|
15
16
|
const miss = { fell: false };
|
|
@@ -216,10 +217,11 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
216
217
|
if (created.ok)
|
|
217
218
|
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget; in-titler dedupe;model 跟 turn([1992]②)
|
|
218
219
|
if (!created.ok) {
|
|
219
|
-
const conflict = {
|
|
220
|
+
const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore }, prepared.spec.sessionId, created.activeTaskId); // [2255]① 真出路材料
|
|
220
221
|
// Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
|
|
221
222
|
// the returned resp is for a deduplicated concurrent caller (and is never idem-cached).
|
|
222
|
-
|
|
223
|
+
// done 帧 additive 携带同一份材料 —— 形状铸在 toDoneFrameResult(有名字有类型),不在这行内联挑键。
|
|
224
|
+
res.write(`data: ${JSON.stringify({ type: "done", result: toDoneFrameResult(conflict) })}\n\n`);
|
|
223
225
|
return { status: 409, body: conflict };
|
|
224
226
|
}
|
|
225
227
|
durableTaskId = tid;
|
|
@@ -971,7 +973,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
971
973
|
const tid = uuidv7();
|
|
972
974
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
973
975
|
if (!created.ok)
|
|
974
|
-
return { status: 409, body: {
|
|
976
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore }, prepared.spec.sessionId, created.activeTaskId) }; // [2255]① 真出路材料
|
|
975
977
|
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget;model 跟 turn([1992]②)
|
|
976
978
|
durableTaskId = tid;
|
|
977
979
|
}
|
package/dist/http/server.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ import { cascadeConfig } from "./run-meta.js";
|
|
|
31
31
|
export { cascadeConfig };
|
|
32
32
|
import { coarseStatusForState, errorCodeForExit } from "./routes/images.js";
|
|
33
33
|
export { coarseStatusForState, errorCodeForExit };
|
|
34
|
-
import { explicitOperatorOk, isOperator
|
|
35
|
-
export { explicitOperatorOk, isOperator
|
|
34
|
+
import { explicitOperatorOk, isOperator } from "./principal-gate.js";
|
|
35
|
+
export { explicitOperatorOk, isOperator };
|
|
36
36
|
/** Per-request authorization context resolved before building the spec (identity, owned session). */
|
|
37
37
|
export interface RequestAuth {
|
|
38
38
|
principal?: string;
|
package/dist/http/server.js
CHANGED
|
@@ -48,8 +48,8 @@ export { coarseStatusForState, errorCodeForExit };
|
|
|
48
48
|
// design/158 A9:装配器缝——发送器/身份门下沉到 http/ 叶子模块,routes/* 与 server.ts 共用同一实现
|
|
49
49
|
// (routes/* 绝不可值 import server.ts:那条边会闭合运行时环,见 test/module-cycle-gate.test.ts)。
|
|
50
50
|
import { sendJson, sendError, httpErrorCode, msg } from "./send.js";
|
|
51
|
-
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator
|
|
52
|
-
export { explicitOperatorOk, isOperator
|
|
51
|
+
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator } from "./principal-gate.js";
|
|
52
|
+
export { explicitOperatorOk, isOperator };
|
|
53
53
|
/** 分组入参 → 平铺视图(createHttpServer 的第一件事)。一组都没给 ⇒ 原样返回(存量调用零开销、零形变)。 */
|
|
54
54
|
export function flattenServiceDeps(deps) {
|
|
55
55
|
const { stores, coordinators, seams, observability, governance, deployment, knobs, ...flat } = deps;
|
package/dist/key-resolver.d.ts
CHANGED
|
@@ -23,7 +23,13 @@ import { type SealedKeyPoison } from "./sealed-key.js";
|
|
|
23
23
|
* construction in applyEffective (a sealed model never lands in modelApiKeyEnv); the ordering here is
|
|
24
24
|
* belt-and-suspenders. 🔴 Plaintext values are live secrets: memory-only, never log them.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
/** The ONE field the per-model key plane keys on. The resolver's parameter is deliberately this minimal
|
|
27
|
+
* named ref (not the full core `Model`): full models are structural supersets and pass unchanged, while
|
|
28
|
+
* callers that never materialize a full Model (hook-llm resolves keys for roster entries by catalog name)
|
|
29
|
+
* can mint `{ name }` legally instead of forcing it through an `as never` escape hatch (B1). Contravariance
|
|
30
|
+
* keeps the resolver assignable wherever `(model: Model) => …` is expected. */
|
|
31
|
+
export type ModelKeyRef = Pick<Model, "name">;
|
|
32
|
+
export declare function createKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): ((model: ModelKeyRef) => Promise<{
|
|
27
33
|
apiKey: string;
|
|
28
34
|
} | undefined>) | undefined;
|
|
29
35
|
/**
|
package/dist/key-resolver.js
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
import { isSealedKeyPoison, SealedKeyPoisonedError } from "./sealed-key.js";
|
|
2
|
-
/**
|
|
3
|
-
* Build a per-model API-key resolver for `TaskSpec.getApiKeyAndHeaders` from the catalog's
|
|
4
|
-
* name → env-var-NAME map (sema-registry `apiKeyEnv`). core threads this per brain call — and per cascade
|
|
5
|
-
* rung — so each model/rung authenticates with its OWN upstream account/key.
|
|
6
|
-
*
|
|
7
|
-
* Additive by construction: a model absent from the map — or whose env var is unset — returns
|
|
8
|
-
* `undefined`, so core falls back to the brain's construction-time gateway key (today's behavior, no
|
|
9
|
-
* regression). When no per-model keys are configured at all this returns `undefined`, leaving the spec
|
|
10
|
-
* field unset so core's default path is byte-for-byte unchanged.
|
|
11
|
-
*
|
|
12
|
-
* Only the api KEY is per-model; the base URL stays brain-owned (design/15) — core's seam carries
|
|
13
|
-
* `{ apiKey, headers? }`, not a baseUrl. Same-account-rotation could round-robin inside this function.
|
|
14
|
-
*
|
|
15
|
-
* `modelApiKeys` (sealed-box custody) carries per-model entries unsealed from `ModelEntry.sealedApiKey` —
|
|
16
|
-
* resolved FIRST (sealed outranks the env-NAME reference, the registry-core mutual-exclusion ruling). Each
|
|
17
|
-
* entry is either the PLAINTEXT key (successful unseal) or a `SealedKeyPoison` marker (unseal FAILED at
|
|
18
|
-
* apply time): a poisoned model THROWS `SealedKeyPoisonedError` on every resolve — task-level fail-loud —
|
|
19
|
-
* because returning `undefined` here means "use the gateway key", and a broken sealed key must NEVER
|
|
20
|
-
* silently burn the shared gateway account (the core invariant of the poison-pill fix; a poisoned model
|
|
21
|
-
* skips its env reference too, same mutual-exclusion as a healthy sealed key). The maps are disjoint by
|
|
22
|
-
* construction in applyEffective (a sealed model never lands in modelApiKeyEnv); the ordering here is
|
|
23
|
-
* belt-and-suspenders. 🔴 Plaintext values are live secrets: memory-only, never log them.
|
|
24
|
-
*/
|
|
25
2
|
export function createKeyResolver(modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
|
|
26
3
|
if (Object.keys(modelApiKeyEnv).length === 0 && Object.keys(modelApiKeys).length === 0)
|
|
27
4
|
return undefined;
|
package/dist/leader/wire.d.ts
CHANGED
|
@@ -115,7 +115,6 @@ export declare function stageWorkerEnv(env: WireEnv, opts: {
|
|
|
115
115
|
uploadScriptPath: string;
|
|
116
116
|
uploadScript: string;
|
|
117
117
|
}): Promise<void>;
|
|
118
|
-
/** Build the `runLeader(body)` the HTTP endpoint invokes. Constructs real per-task deps + runs the leader. */
|
|
119
118
|
/**
|
|
120
119
|
* Per-leader-run resource bounds derived from env (2026-06-14). Exported for unit tests. The worker sandbox
|
|
121
120
|
* lifetime (BUG2), the worker's diff-upload presigned-URL TTL (BUG1), and the worker spec's triple bound
|
|
@@ -140,6 +139,25 @@ export declare function leaderResourceConfig(env?: NodeJS.ProcessEnv): {
|
|
|
140
139
|
maxSuspends: number;
|
|
141
140
|
};
|
|
142
141
|
};
|
|
142
|
+
/** Repair/conflict/replan-loop knobs derived from env + optional `LeaderWireConfig` fallbacks (`repairRounds`/
|
|
143
|
+
* `conflictRounds` land in cfg when the caller wires them programmatically instead of via env). Split out of
|
|
144
|
+
* `createLeaderRunner`'s closure so it's a plain, unit-testable data constructor. */
|
|
145
|
+
export interface LeaderLoopConfig {
|
|
146
|
+
repairRounds: number;
|
|
147
|
+
repairBudgetUsd: number;
|
|
148
|
+
conflictRounds: number;
|
|
149
|
+
repairLoopOn: boolean;
|
|
150
|
+
measureGatesOn: boolean;
|
|
151
|
+
repairLoopAttempts: number;
|
|
152
|
+
oracleFlakyK: number;
|
|
153
|
+
/** replan-lite (design/68 §6) fan-out spend cap from LEADER_BUDGET_USD. Absent = replan-lite's own default
|
|
154
|
+
* (unset env stays absent — existing contract untouched; see leaderLoopConfig doc below). */
|
|
155
|
+
replanBudgetUsd?: number;
|
|
156
|
+
}
|
|
157
|
+
export declare function leaderLoopConfig(cfg: {
|
|
158
|
+
repairRounds?: number;
|
|
159
|
+
conflictRounds?: number;
|
|
160
|
+
}, env?: NodeJS.ProcessEnv): LeaderLoopConfig;
|
|
143
161
|
export declare function createLeaderRunner(cfg: LeaderWireConfig): (body: LeaderRequestBody) => Promise<LeaderResult>;
|
|
144
162
|
export {};
|
|
145
163
|
//# sourceMappingURL=wire.d.ts.map
|
package/dist/leader/wire.js
CHANGED
|
@@ -98,6 +98,18 @@ export async function stageWorkerEnv(env, opts) {
|
|
|
98
98
|
throw new Error(`upload-script staging failed: ${wr.error.message}`);
|
|
99
99
|
}
|
|
100
100
|
/** Build the `runLeader(body)` the HTTP endpoint invokes. Constructs real per-task deps + runs the leader. */
|
|
101
|
+
// 🔴 review-batch(重构池②,2026-08-01):`parseNumOrFail` alone still lets a NEGATIVE value through — a negative
|
|
102
|
+
// number is JS-truthy, so every `parseNumOrFail(...) || default` chain in this file passed it straight through
|
|
103
|
+
// instead of falling back (LEADER_WORKER_MAX_TURNS=-5 ⇒ workerMaxTurns=-5, unchanged by any later `|| default`).
|
|
104
|
+
// This is the one extra domain bound (A2: illegal input INCLUDING negatives must fail loud) layered on the same
|
|
105
|
+
// shared parse primitive — not a second parser. `undefined` (unset) stays NaN (`NaN < 0` is false) so the
|
|
106
|
+
// existing `|| default` fallback chains are untouched; only "non-numeric" and "negative" newly throw.
|
|
107
|
+
function parseNumOrFailNonNegative(name, raw) {
|
|
108
|
+
const n = parseNumOrFail(name, raw);
|
|
109
|
+
if (n < 0)
|
|
110
|
+
throw new Error(`env ${name}=${n} must not be negative`);
|
|
111
|
+
return n;
|
|
112
|
+
}
|
|
101
113
|
/**
|
|
102
114
|
* Per-leader-run resource bounds derived from env (2026-06-14). Exported for unit tests. The worker sandbox
|
|
103
115
|
* lifetime (BUG2), the worker's diff-upload presigned-URL TTL (BUG1), and the worker spec's triple bound
|
|
@@ -111,13 +123,13 @@ export async function stageWorkerEnv(env, opts) {
|
|
|
111
123
|
// ⇒ diff 上传预签名 URL 的暴露窗口一起放大。改走与 numEnv 同源的 parseNumOrFail:非法值当场报错。
|
|
112
124
|
// 刻意**零行为变更**:`|| default` 保留,所以未设与显式 0 的既有语义一个没动。
|
|
113
125
|
export function leaderResourceConfig(env = process.env) {
|
|
114
|
-
const leaderTimeoutMs = Math.max(600_000, Math.floor(
|
|
115
|
-
const workerMaxTurns = Math.floor(
|
|
116
|
-
const workerBudgetUsd = (
|
|
126
|
+
const leaderTimeoutMs = Math.max(600_000, Math.floor(parseNumOrFailNonNegative("LEADER_TIMEOUT_MS", env.LEADER_TIMEOUT_MS) || 0) || 86_400_000); // default 24h (was 10min)
|
|
127
|
+
const workerMaxTurns = Math.floor(parseNumOrFailNonNegative("LEADER_WORKER_MAX_TURNS", env.LEADER_WORKER_MAX_TURNS) || 0) || 10_000;
|
|
128
|
+
const workerBudgetUsd = (parseNumOrFailNonNegative("LEADER_WORKER_BUDGET_USD", env.LEADER_WORKER_BUDGET_USD) || 0) || 100;
|
|
117
129
|
const resourceSuspendOn = String(env.LEADER_RESOURCE_SUSPEND ?? "").toLowerCase() === "true";
|
|
118
130
|
// Per-slice window: a fraction of the total so a worker suspends+resumes several times across its budget
|
|
119
131
|
// (default = total/4, min $1). Smaller window ⇒ more, shorter slices ⇒ progress saved more often.
|
|
120
|
-
const sliceMaxCostUsd = (
|
|
132
|
+
const sliceMaxCostUsd = (parseNumOrFailNonNegative("LEADER_WORKER_SLICE_BUDGET_USD", env.LEADER_WORKER_SLICE_BUDGET_USD) || 0) || Math.max(1, workerBudgetUsd / 4);
|
|
121
133
|
return {
|
|
122
134
|
leaderTimeoutMs,
|
|
123
135
|
workerMaxTurns,
|
|
@@ -135,24 +147,41 @@ export function leaderResourceConfig(env = process.env) {
|
|
|
135
147
|
: {}),
|
|
136
148
|
};
|
|
137
149
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
150
|
+
// 🔴 review-batch(重构池②,2026-08-01): every knob below used to be a bare `Number(process.env.X ?? default)`
|
|
151
|
+
// with ZERO fail-loud guard — worst case was `LEADER_BUDGET_USD="20usd"` silently becoming `budgetUsd: NaN`,
|
|
152
|
+
// which makes the replan-lite budget gate's `spent > budgetUsd` comparison permanently `false` (the whole
|
|
153
|
+
// gate goes dark, not "loose" — `NaN` compares false in every direction). Converged onto the same
|
|
154
|
+
// `parseNumOrFailNonNegative` primitive leaderResourceConfig uses: non-numeric and negative now throw at
|
|
155
|
+
// startup; `undefined`/explicit-`0` fallback semantics are byte-identical to before.
|
|
156
|
+
export function leaderLoopConfig(cfg, env = process.env) {
|
|
157
|
+
const repairRounds = Math.max(0, Math.floor(env.LEADER_REPAIR_ROUNDS !== undefined
|
|
158
|
+
? parseNumOrFailNonNegative("LEADER_REPAIR_ROUNDS", env.LEADER_REPAIR_ROUNDS)
|
|
159
|
+
: (cfg.repairRounds ?? 0)) || 0);
|
|
160
|
+
const repairBudgetUsd = parseNumOrFailNonNegative("LEADER_REPAIR_BUDGET_USD", env.LEADER_REPAIR_BUDGET_USD ?? "4") || 4;
|
|
161
|
+
const conflictRounds = Math.max(0, Math.floor(env.LEADER_CONFLICT_ROUNDS !== undefined
|
|
162
|
+
? parseNumOrFailNonNegative("LEADER_CONFLICT_ROUNDS", env.LEADER_CONFLICT_ROUNDS)
|
|
163
|
+
: (cfg.conflictRounds ?? 0)) || 0);
|
|
146
164
|
// ── LEADER-REPAIRLOOP-INTEGRATION §10 env knobs (FRESH hunk, §10.8 — mirrors LEADER_REPAIR_ROUNDS above) ──
|
|
147
165
|
// Every flag default-OFF so the whole feature is inert until flipped on a canary (the default path is
|
|
148
166
|
// byte-identical to today). `LEADER_REPAIR_LOOP` is the MASTER (gates the single-agent `runRepairLoop` dep);
|
|
149
167
|
// `LEADER_MEASURE_GATES` gates the merge push-hold-on-signal + measure-drives-repair (§10.3/§10.4);
|
|
150
168
|
// `LEADER_REPAIR_LOOP_ATTEMPTS` is the in-loop attempt ceiling (validated 2-3, default 2 — design/78 §5);
|
|
151
169
|
// `LEADER_ORACLE_FLAKY_K` is the flaky-settle re-isolation count (default 2 — §10.1 / repair-oracle.ts).
|
|
152
|
-
const repairLoopOn = String(
|
|
153
|
-
const measureGatesOn = String(
|
|
154
|
-
const repairLoopAttempts = Math.min(3, Math.max(2, Math.floor(
|
|
155
|
-
const oracleFlakyK = Math.max(1, Math.floor(
|
|
170
|
+
const repairLoopOn = String(env.LEADER_REPAIR_LOOP ?? "").toLowerCase() === "true";
|
|
171
|
+
const measureGatesOn = String(env.LEADER_MEASURE_GATES ?? "").toLowerCase() === "true";
|
|
172
|
+
const repairLoopAttempts = Math.min(3, Math.max(2, Math.floor(parseNumOrFailNonNegative("LEADER_REPAIR_LOOP_ATTEMPTS", env.LEADER_REPAIR_LOOP_ATTEMPTS ?? "2")) || 2));
|
|
173
|
+
const oracleFlakyK = Math.max(1, Math.floor(parseNumOrFailNonNegative("LEADER_ORACLE_FLAKY_K", env.LEADER_ORACLE_FLAKY_K ?? "2")) || 2);
|
|
174
|
+
return {
|
|
175
|
+
repairRounds, repairBudgetUsd, conflictRounds, repairLoopOn, measureGatesOn, repairLoopAttempts, oracleFlakyK,
|
|
176
|
+
...(env.LEADER_BUDGET_USD ? { replanBudgetUsd: parseNumOrFailNonNegative("LEADER_BUDGET_USD", env.LEADER_BUDGET_USD) } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function createLeaderRunner(cfg) {
|
|
180
|
+
const W = cfg.workspace ?? "/home/user";
|
|
181
|
+
const repo = `${W}/repo`;
|
|
182
|
+
const ident = cfg.git ?? { name: "leader", email: "leader@local" };
|
|
183
|
+
const { leaderTimeoutMs, workerLimits, presignTtlSec, resourceSuspend: resourceCfg } = leaderResourceConfig();
|
|
184
|
+
const { repairRounds, repairBudgetUsd, conflictRounds, repairLoopOn, measureGatesOn, repairLoopAttempts, oracleFlakyK, replanBudgetUsd, } = leaderLoopConfig({ repairRounds: cfg.repairRounds, conflictRounds: cfg.conflictRounds });
|
|
156
185
|
// worker resource bounds (limits/maxCostUsd) + presign ttl are derived in leaderResourceConfig(), above.
|
|
157
186
|
// Bounded integration-repair (search 2026-06-14): when the merged tree compiles-clean-but-fails, run a strong
|
|
158
187
|
// agent WITH HANDS in the live merged sandbox to fix the cross-worker integration, then mergeBranches re-runs
|
|
@@ -590,8 +619,9 @@ export function createLeaderRunner(cfg) {
|
|
|
590
619
|
// found live: task02b run5, porting workers + collapse-solo all 'run aborted' at the 600s default while
|
|
591
620
|
// mid-gradle; the per-worker presign ttl below tracks the same knob).
|
|
592
621
|
timeoutMs: leaderTimeoutMs, maxConcurrency: 4, // Semaphore cap; the planner emits ≤6 sub-tasks
|
|
593
|
-
// replan-lite (design/68 §6) always on in the wired leader; LEADER_BUDGET_USD bounds the fan-out spend
|
|
594
|
-
|
|
622
|
+
// replan-lite (design/68 §6) always on in the wired leader; LEADER_BUDGET_USD bounds the fan-out spend
|
|
623
|
+
// (parsed + fail-loud-guarded in leaderLoopConfig(), above — see LEADER_BUDGET_USD review-batch note).
|
|
624
|
+
replan: { ...(replanBudgetUsd !== undefined ? { budgetUsd: replanBudgetUsd } : {}) },
|
|
595
625
|
// LEADER-REPAIRLOOP-INTEGRATION §10.3 — the merge push-hold-on-signal (OFF unless LEADER_MEASURE_GATES). It
|
|
596
626
|
// needs NO extra sandbox: `strongOracleSeeded` keys on whether a hidden held-out oracle was injected into the
|
|
597
627
|
// integration sandbox (`injectOracles` runs `body.oracleFiles`), and the §10.3 hold combines it with the
|
package/dist/main.js
CHANGED
|
@@ -24,7 +24,7 @@ import { createRegistryJwtVerifier } from "./auth-bridge.js";
|
|
|
24
24
|
import { createMetrics } from "./observability/metrics.js";
|
|
25
25
|
import { setRedactionObserver, redactSecrets } from "./trace/redact.js";
|
|
26
26
|
import { RateLimiter } from "./observability/rate-limit.js";
|
|
27
|
-
import { createHttpServer,
|
|
27
|
+
import { createHttpServer, explicitOperatorOk } from "./http/server.js";
|
|
28
28
|
import { exportMemoryScope } from "./memory-export.js";
|
|
29
29
|
import { performMemorySync } from "./memory-sync.js";
|
|
30
30
|
import { startOtlpExporter } from "./observability/otel-exporter.js";
|
|
@@ -421,7 +421,7 @@ async function main() {
|
|
|
421
421
|
? selectEnvironmentTool({
|
|
422
422
|
catalog: imageIndex,
|
|
423
423
|
selection: sessionEnvSelection,
|
|
424
|
-
viewerFor: (principal) => ({ operator:
|
|
424
|
+
viewerFor: (principal) => ({ operator: explicitOperatorOk(principal, config.operatorPrincipals), tenantId: principal ?? null }),
|
|
425
425
|
})
|
|
426
426
|
: undefined;
|
|
427
427
|
// Sandbox-image-pool BAKE control plane (IMAGE-API-DESIGN.md §P2): enables /v1/images/bakes* when a pool exists
|
package/dist/parked-decide.js
CHANGED
|
@@ -135,7 +135,10 @@ export async function decideParkedAgent(deps, req) {
|
|
|
135
135
|
boundInputHash: req.binding?.boundInputHash ?? persistedHash,
|
|
136
136
|
decision: req.decision === "approve" ? "allow" : "deny",
|
|
137
137
|
...(req.decision === "approve" && req.binding?.updatedInput !== undefined ? { updatedInput: req.binding.updatedInput } : {}),
|
|
138
|
-
|
|
138
|
+
// B10:存在性判定,不是真值判定 —— reason 可以是空串("运维显式选择不写理由"),这与"没传 reason"
|
|
139
|
+
// 是两件不同的事(姊妹 approval-hmac.ts `env.reason ?? null` 同判据:`??` 只在 null/undefined 时落
|
|
140
|
+
// null,空串照样入签名载荷)。真值判定会把显式 "" 与缺席折成同一个结果,审计/签名面丢了这个区分。
|
|
141
|
+
...(req.reason !== undefined ? { reason: req.reason } : {}),
|
|
139
142
|
};
|
|
140
143
|
const ctx = {
|
|
141
144
|
toolCallId: `drv-${ticket.claimId}`,
|
|
@@ -293,8 +293,17 @@ export class FileRunStore {
|
|
|
293
293
|
* [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
|
|
294
294
|
async setTerminal(taskId, status, result, error) {
|
|
295
295
|
const r = this.runs.get(taskId);
|
|
296
|
-
if (!r)
|
|
296
|
+
if (!r) {
|
|
297
|
+
// [2255]③ claim 孤儿修:行不在(跨进程窗 —— claim 落盘、行未落/已失;R9 只兜同进程写失败)时,
|
|
298
|
+
// 终局动词仍必须能放锁:cancel 是文案承诺的用户自救路,它对孤儿失效=会话钉死到 reaper/boot。
|
|
299
|
+
// 反查 active 索引找持有该 taskId 的 session(O(claims),claims 数=活跃会话数,小)。
|
|
300
|
+
for (const [sessionId, tid] of this.active)
|
|
301
|
+
if (tid === taskId) {
|
|
302
|
+
this.releaseClaim(sessionId);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
297
305
|
return;
|
|
306
|
+
}
|
|
298
307
|
if (r.status === "running" || r.status === "suspended" || r.status === "needs_review") {
|
|
299
308
|
r.status = status;
|
|
300
309
|
r.result = result;
|
|
@@ -354,9 +363,9 @@ export class FileRunStore {
|
|
|
354
363
|
const cursorAt = opts.cursor ? Date.parse(opts.cursor.createdAt) : undefined;
|
|
355
364
|
const rows = [...this.runs.values()]
|
|
356
365
|
.filter((r) => (opts.status ? r.status === opts.status : true))
|
|
357
|
-
.filter((r) => (opts.jobId ? r.jobId === opts.jobId : true))
|
|
358
|
-
.filter((r) => (opts.source ? r.source === opts.source : true))
|
|
359
|
-
.filter((r) => (opts.owner ? r.owner === opts.owner : true)) // exact (SQL `owner = ?`): a
|
|
366
|
+
.filter((r) => (opts.jobId !== undefined ? r.jobId === opts.jobId : true)) // "" is a valid exact jobId, not "no filter" (B10)
|
|
367
|
+
.filter((r) => (opts.source !== undefined ? r.source === opts.source : true)) // "" is a valid exact source, not "no filter" (B10)
|
|
368
|
+
.filter((r) => (opts.owner !== undefined ? r.owner === opts.owner : true)) // exact (SQL `owner = ?`): "" is a valid exact owner, not "no filter" (B10)
|
|
360
369
|
.filter((r) => {
|
|
361
370
|
if (cursorAt === undefined)
|
|
362
371
|
return true;
|
|
@@ -370,8 +379,8 @@ export class FileRunStore {
|
|
|
370
379
|
async listSessions(opts) {
|
|
371
380
|
const bySession = new Map();
|
|
372
381
|
for (const r of this.runs.values()) {
|
|
373
|
-
if (opts.owner && r.owner !== opts.owner)
|
|
374
|
-
continue; // exact owner filter (SQL `owner = ?`)
|
|
382
|
+
if (opts.owner !== undefined && r.owner !== opts.owner)
|
|
383
|
+
continue; // exact owner filter (SQL `owner = ?`); "" is a valid exact owner (B10)
|
|
375
384
|
const arr = bySession.get(r.sessionId) ?? [];
|
|
376
385
|
arr.push(r);
|
|
377
386
|
bySession.set(r.sessionId, arr);
|
|
@@ -535,8 +544,10 @@ export class FileRunStore {
|
|
|
535
544
|
*/
|
|
536
545
|
async reapSuspended(olderThanMs) {
|
|
537
546
|
const probe = this.checkpointProbe;
|
|
538
|
-
if (!probe)
|
|
547
|
+
if (!probe) {
|
|
548
|
+
this.sweepClaims();
|
|
539
549
|
return 0;
|
|
550
|
+
} // [2255]③:时间腿无 probe=诚实 NO-OP,但 claim 不变式维护不跟 probe 走
|
|
540
551
|
const cutoff = Date.now() - olderThanMs;
|
|
541
552
|
let reaped = 0;
|
|
542
553
|
for (const r of this.runs.values()) {
|
|
@@ -566,8 +577,10 @@ export class FileRunStore {
|
|
|
566
577
|
*/
|
|
567
578
|
async failSuspendedWithExpiredCheckpoint() {
|
|
568
579
|
const probe = this.checkpointProbe;
|
|
569
|
-
if (!probe)
|
|
580
|
+
if (!probe) {
|
|
581
|
+
this.sweepClaims();
|
|
570
582
|
return 0;
|
|
583
|
+
} // [2255]③ 同上
|
|
571
584
|
let reaped = 0;
|
|
572
585
|
for (const r of this.runs.values()) {
|
|
573
586
|
if (r.status !== "suspended" && r.status !== "needs_review")
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host-lane PLATFORM seam (DESIGN-windows-native.md, FINAL r3) — the ONE place the host exec adapter's
|
|
3
|
-
* POSIX/win32 differences live, so `remote-env-host.ts` stays a single code path with platform-gated leaves.
|
|
4
|
-
*
|
|
5
|
-
* 🔴 Iron invariant (design §4.1): the POSIX path is BYTE-IDENTICAL to the pre-Windows code — every helper is
|
|
6
|
-
* `win32 ? new : exactly-what-the-inline-code-did` (same syscall, same throw behavior). Never "improve" POSIX
|
|
7
|
-
* here; the existing full test suite is the regression net.
|
|
8
|
-
*
|
|
9
|
-
* win32 semantics (design D1-D4):
|
|
10
|
-
* - shell = Git Bash via core 1.224 `getShellConfig` (WSL-launcher-filtered — the `System32\bash.exe`
|
|
11
|
-
* trap). Fail-LOUD when absent; never silently degrade to cmd (D1).
|
|
12
|
-
* - kill = core 1.224 `signalProcessTree` (taskkill /T, /F for hard). Soft is a no-op for console trees
|
|
13
|
-
* (taskkill errors "can only be terminated forcefully") — the SIGTERM→grace→SIGKILL ladder is
|
|
14
|
-
* effectively delay→hard-kill on win32; accepted, CC-identical (D2).
|
|
15
|
-
* - spawn = `detached:false` + `windowsHide:true` (no console window; no POSIX process group — the kill
|
|
16
|
-
* side uses the tree, not the group) (D3).
|
|
17
|
-
* - env = case-insensitive key collapse before spawn (win32 env keys are case-insensitive; a `Path`+`PATH`
|
|
18
|
-
* pair from case-sensitive Object.assign reaches CreateProcess as ONE undefined-which
|
|
19
|
-
* entry). Canonical casing = the first-seen key (process.env's native casing wins since the
|
|
20
|
-
* inherit base is spread first).
|
|
21
|
-
*
|
|
22
|
-
* ⚠️ The win32 branches are UNVERIFIED on a real machine until S5 (Windows CI runner) — design D5 discipline:
|
|
23
|
-
* structural tests only on mac/Linux; behavior-level bite happens on the first Windows-runner green.
|
|
24
|
-
*/
|
|
25
1
|
import { killProcessTree } from "@sema-agent/core";
|
|
26
2
|
export declare const IS_WIN32: boolean;
|
|
27
3
|
export interface HostShell {
|
|
@@ -82,6 +58,17 @@ export { killProcessTree };
|
|
|
82
58
|
* POSIX: returns the input UNTOUCHED (case-sensitive env is real there — `Path` and `PATH` are distinct).
|
|
83
59
|
*/
|
|
84
60
|
export declare function collapseWin32EnvKeys(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
61
|
+
/**
|
|
62
|
+
* Conventional exit-code mapping for a process killed by a signal (128 + signal number). Single pattern-home
|
|
63
|
+
* construction point for every exec adapter that must turn a `close` event's external-kill case
|
|
64
|
+
* (`code===null`, `signal` set) into an exit code instead of silently reporting a fake success 0 — the host
|
|
65
|
+
* lane, ssh, adb, and local-docker all consume THIS function; none of them may hand-write their own signal
|
|
66
|
+
* table. Uses Node's authoritative platform table (`os.constants.signals`) rather than a hand-written list —
|
|
67
|
+
* a hand-written table has previously missed `SIGABRT`/`SIGPIPE` (`kill -ABRT` reported 137, impersonating
|
|
68
|
+
* `SIGKILL`). Callers keep a `?? 9` fallback for a name the platform table doesn't define, matching core's
|
|
69
|
+
* `SIGNUM[signal] ?? 9`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function signalNumber(signal: NodeJS.Signals): number | undefined;
|
|
85
72
|
/** The platform-free collapse algorithm (exported so the mac/Linux suite can pin the win32 behavior — design
|
|
86
73
|
* D5: structural verification everywhere, behavioral bite on the Windows runner). */
|
|
87
74
|
export declare function collapseEnvKeysCaseInsensitive(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|