@sema-agent/server 7.96.0 → 7.97.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.
Files changed (65) hide show
  1. package/USAGE.md +4 -1
  2. package/dist/approval.d.ts +28 -0
  3. package/dist/approval.js +24 -0
  4. package/dist/boot/budget-tracing.d.ts +1 -0
  5. package/dist/boot/budget-tracing.js +3 -2
  6. package/dist/boot/config-center.js +2 -5
  7. package/dist/boot/coordinators.js +2 -2
  8. package/dist/boot/deferred-sandbox-path-env.d.ts +5 -2
  9. package/dist/boot/deferred-sandbox-path-env.js +3 -1
  10. package/dist/boot/memory-boundary.d.ts +17 -2
  11. package/dist/boot/memory-boundary.js +11 -0
  12. package/dist/boot/reapers.d.ts +3 -1
  13. package/dist/boot/reapers.js +2 -2
  14. package/dist/boot/resolve-spec.js +6 -6
  15. package/dist/boot/runner-deps.d.ts +2 -1
  16. package/dist/boot/session-faces.d.ts +4 -1
  17. package/dist/boot/session-faces.js +5 -5
  18. package/dist/boot/stage-04-stores.d.ts +1 -0
  19. package/dist/boot/stage-04-stores.js +2 -2
  20. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  21. package/dist/boot/stage-06-runners.d.ts +1 -0
  22. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  23. package/dist/boot/stage-07-capability-layer.js +10 -14
  24. package/dist/boot/stage-08-reapers.d.ts +2 -3
  25. package/dist/boot/stage-08-reapers.js +2 -2
  26. package/dist/boot/stage-09-leader.d.ts +1 -0
  27. package/dist/boot/stage-09-leader.js +2 -2
  28. package/dist/boot/stage-10-http-server.d.ts +1 -0
  29. package/dist/boot/stores.js +5 -11
  30. package/dist/capabilities/execution-lane.d.ts +5 -7
  31. package/dist/capabilities/execution-lane.js +2 -1
  32. package/dist/capabilities/memory-notice.d.ts +30 -12
  33. package/dist/capabilities/memory-notice.js +8 -1
  34. package/dist/capabilities/skills.d.ts +14 -0
  35. package/dist/capabilities/skills.js +4 -0
  36. package/dist/config-catalog.js +1 -0
  37. package/dist/config-types.d.ts +8 -1
  38. package/dist/config.js +4 -3
  39. package/dist/env-facts.js +2 -4
  40. package/dist/execution-lane-caps.d.ts +36 -6
  41. package/dist/execution-lane-caps.js +4 -0
  42. package/dist/file-history-capture.d.ts +65 -0
  43. package/dist/file-history-capture.js +21 -0
  44. package/dist/hosted-posture.d.ts +1 -1
  45. package/dist/http/resume-legs.d.ts +14 -4
  46. package/dist/http/resume-legs.js +26 -17
  47. package/dist/http/route-ctx.d.ts +35 -9
  48. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  49. package/dist/http/routes/approvals-assistant.js +16 -6
  50. package/dist/http/routes/capabilities.js +2 -0
  51. package/dist/http/routes/runs.js +2 -2
  52. package/dist/http/routes/session-sync.js +6 -1
  53. package/dist/http/server.d.ts +4 -1
  54. package/dist/http/server.js +1 -1
  55. package/dist/http/verify-rounds.d.ts +1 -1
  56. package/dist/memory-scope.d.ts +6 -2
  57. package/dist/memory-scope.js +3 -2
  58. package/dist/observability/fail-open.d.ts +4 -0
  59. package/dist/observability/fail-open.js +4 -0
  60. package/dist/run-local.js +4 -5
  61. package/dist/session-sync.d.ts +4 -1
  62. package/dist/session-sync.js +2 -2
  63. package/dist/task-cwd.d.ts +9 -3
  64. package/dist/task-cwd.js +3 -1
  65. package/package.json +1 -1
@@ -64,6 +64,9 @@ function checkpointRowRedeemableElsewhere(reason) {
64
64
  return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
65
65
  }
66
66
  const NON_BINDING_INVALID_OUTCOME_FIELDS = new Set(["hostDecision", "approver"]);
67
+ function notResumableAnswer() {
68
+ return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
69
+ }
67
70
  export function createResumeLegs(ctx) {
68
71
  const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
69
72
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
@@ -458,7 +461,7 @@ export function createResumeLegs(ctx) {
458
461
  if (taskId && deps.runStore) {
459
462
  claimedRow = freshRunClaimed ? true : await deps.runStore.markResuming(taskId, deps.instanceId ?? "default");
460
463
  if (!claimedRow)
461
- return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
464
+ return notResumableAnswer();
462
465
  markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
463
466
  fleetPub?.onStart();
464
467
  const rs = deps.runStore;
@@ -1179,8 +1182,9 @@ export function createResumeLegs(ctx) {
1179
1182
  ...(ownerLeaseAdmittedFor !== undefined && auth.principal === ownerLeaseAdmittedFor ? { ownerLeaseAdmitted: true } : {}),
1180
1183
  });
1181
1184
  }
1182
- async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider, permissionModeAfter) {
1185
+ async function preparePlanReview(sessionId, log, decider) {
1183
1186
  const cs = deps.checkpointStore;
1187
+ const refuse = (answer) => ({ ok: false, answer });
1184
1188
  const log404 = (r) => deps.logger?.info?.("decide_404", {
1185
1189
  reason: r,
1186
1190
  ...(log?.taskId !== undefined ? { taskId: log.taskId } : { sessionId }),
@@ -1192,7 +1196,7 @@ export function createResumeLegs(ctx) {
1192
1196
  const noPending = { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
1193
1197
  if (!token) {
1194
1198
  log404("no-pending-checkpoint");
1195
- return noPending;
1199
+ return refuse(noPending);
1196
1200
  }
1197
1201
  if (decider !== undefined && !decider.explicitOperator) {
1198
1202
  const peeked = await cs
@@ -1202,7 +1206,7 @@ export function createResumeLegs(ctx) {
1202
1206
  const peekedOwner = peeked != null ? decodeCheckpointScope(peeked) : undefined;
1203
1207
  if (peekedOwner !== undefined && peekedOwner !== decider.principal) {
1204
1208
  deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "pending row is foreign to the verified caller — owner gate ahead of the row load" });
1205
- return noPending;
1209
+ return refuse(noPending);
1206
1210
  }
1207
1211
  }
1208
1212
  let cp;
@@ -1212,40 +1216,45 @@ export function createResumeLegs(ctx) {
1212
1216
  catch (e) {
1213
1217
  if (!(await mayDiscloseRowError(cs, token, decider))) {
1214
1218
  deps.logger?.warn?.("plan_review_unreadable_row_withheld", { sessionId, note: "row unreadable and the caller's ownership of THAT token is unproven — folded into the uniform 404" });
1215
- return noPending;
1219
+ return refuse(noPending);
1216
1220
  }
1217
- return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1221
+ return refuse({ status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } });
1218
1222
  }
1219
1223
  if (!cp) {
1220
1224
  log404("checkpoint-missing");
1221
1225
  if (!(await mayDiscloseRowError(cs, token, decider)))
1222
- return noPending;
1223
- return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1226
+ return refuse(noPending);
1227
+ return refuse({ status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } });
1224
1228
  }
1225
1229
  if (decider !== undefined && !decider.explicitOperator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== decider.principal) {
1226
1230
  deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
1227
- return noPending;
1231
+ return refuse(noPending);
1228
1232
  }
1229
1233
  const gateKind = cp.gate?.kind;
1230
1234
  if (gateKind !== PLAN_REVIEW_GATE_KIND) {
1231
- return {
1235
+ return refuse({
1232
1236
  status: 409,
1233
1237
  body: {
1234
1238
  error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a plan_review — a tool approval must be decided via POST /v1/approvals/:id/decide`,
1235
1239
  errorCode: "gate_not_plan_review",
1236
1240
  },
1237
- };
1241
+ });
1238
1242
  }
1239
1243
  const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
1240
1244
  if (ctxRead.kind === "corrupt")
1241
- return resumeCtxCorruptAnswer(ctxRead.where);
1245
+ return refuse(resumeCtxCorruptAnswer(ctxRead.where));
1242
1246
  if (ctxRead.kind === "absent")
1243
- return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
1244
- const ctx = ctxRead.ctx;
1247
+ return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
1248
+ return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
1249
+ }
1250
+ async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
1251
+ const { sessionId, token, cp, ctx } = prepared;
1252
+ if ((await deps.checkpointStore.findPendingTokenBySession(sessionId, undefined, { gateKinds: ["plan_review"] })) !== token)
1253
+ return notResumableAnswer();
1245
1254
  const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
1246
1255
  const effMode = storedBodyPermissionMode(ctx.body);
1247
1256
  if (permissionModeAfter !== undefined && effMode !== "plan") {
1248
- throw new Error("resumePlanReview: permissionModeAfter was passed for a task that was not submitted with permissionMode \"plan\" — the caller must refuse this before any state changes (see the plan_review route's pre-nonce gate)");
1257
+ throw new Error("drivePlanReview: permissionModeAfter was passed for a task that was not submitted with permissionMode \"plan\" — the caller must refuse this before any state changes (see the plan_review route's pre-nonce gate)");
1249
1258
  }
1250
1259
  const approvedMode = decision === "approve" && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
1251
1260
  const resumeBody = approvedMode === undefined ? ctx.body : { ...ctx.body, permissionMode: approvedMode };
@@ -1269,8 +1278,8 @@ export function createResumeLegs(ctx) {
1269
1278
  registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
1270
1279
  helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
1271
1280
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
1272
- legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, resumePlanReview },
1281
+ legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, preparePlanReview, drivePlanReview },
1273
1282
  };
1274
- return { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, resumePlanReview };
1283
+ return { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, preparePlanReview, drivePlanReview };
1275
1284
  }
1276
1285
  //# sourceMappingURL=resume-legs.js.map
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
18
  import type { HostDecision } from "../host-decision.js";
19
- import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, RunInternals, Runner, EngineNotice } from "@sema-agent/core";
19
+ import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, Checkpoint, CheckpointToken, ResumeOutcome, RunInternals, Runner, EngineNotice } from "@sema-agent/core";
20
20
  import type { FlatServiceDeps, RequestAuth } from "./server.js";
21
21
  import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
22
22
  import type { IdempotencyCache, SubmitOutcome } from "./idempotency.js";
@@ -154,7 +154,7 @@ export interface DriveResumeArgs {
154
154
  /**
155
155
  * 🔴 **S-433 合并复审 F2 [high] —— 「本次续跑真正用的那个体」,由本腿在赢下 CAS 之后写回耐久层**
156
156
  * (验真后修;必填 —— `driveResumeIntoRunLog` 今天恰有**四个**调用点:legacy `/decide`(`resumeCheckpoint`)、
157
- * `resumePreempted`、`resumeWake`、`resumePlanReview`,四条一视同仁)。
157
+ * `resumePreempted`、`resumeWake`、`drivePlanReview`(S-609 前名 `resumePlanReview`),四条一视同仁)。
158
158
  *
159
159
  * 病(修前):`resumePlanReview` 在 `driveResumeIntoRunLog` **受理(2xx)之后**才 `putCtx(sessionId,
160
160
  * {...ctx, body})`,而 `ctx` 是请求**开头** `getCtx` 读到的那一份,两个店的 `putCtx` 又都是按
@@ -177,7 +177,7 @@ export interface DriveResumeArgs {
177
177
  * 🔴 写失败**响亮但不致命**(codex r1 两条 [high] 验真后定):core 的 `resumeStream` 是 **EAGER** 的 ——
178
178
  * 写点这一拍 run 已经在执行,裸抛会把一条活腿判成 failed + 释放 claim 而执行还在继续。⇒ 一个臂、
179
179
  * 四条腿同守:计数 `resume_ctx_not_persisted_total` + 一行 warn,本次续跑照常。方向论证(不是 [ref] 的
180
- * 静默放宽):写失败 ⇒ 耐久体保持**上一次**的值,而这四条腿里只有 `resumePlanReview` 改写这个体、
180
+ * 静默放宽):写失败 ⇒ 耐久体保持**上一次**的值,而这四条腿里只有 `drivePlanReview` 改写这个体、
181
181
  * 且只会**放宽** ⇒ 落盘的恒不比本次续跑更宽。旧的 `plan_approve_mode_not_persisted`(plan 专用)随本批删除,
182
182
  * 由上面这只家族级的取代。全文见 `resume-legs.ts` 的 `commitResumeCtx` 顶注。
183
183
  */
@@ -311,6 +311,17 @@ export interface DriveResumeArgs {
311
311
  */
312
312
  ownerLeaseAdmitted?: boolean;
313
313
  }
314
+ /**
315
+ * S-609(7.97.0):plan_review 纯读段全部过关后交给驱动段的那一份 —— **选中的那张卡**(token + 载入的行)与**它那一刻**
316
+ * 的续跑上下文。三者一起走:驱动段只认这张卡,ctx 读绑在它上面(读在选卡之后;之后任何 ctx 写都得先赢这张卡的
317
+ * CAS,于是一份过期的读只会让驱动段在 CAS 上败,不会被配给别的卡)。
318
+ */
319
+ export interface PreparedPlanReview {
320
+ readonly sessionId: string;
321
+ readonly token: CheckpointToken;
322
+ readonly cp: Checkpoint;
323
+ readonly ctx: CheckpointCtx;
324
+ }
314
325
  /** 提交/续跑「腿」= 跨域共享的**有状态**长流程(不像 {@link RouteHelpers} 那样只依赖 deps/config:它们要写
315
326
  * run log、发 fleet 帧、走 markResuming CAS、驱动模型)。A9 上批的停点就在这里——tasks/runs 两域共用
316
327
  * `prepareSpec`,approvals/assistant/notify-wake 三域共用 resume 家族,把它们跟着任一域搬都会造出
@@ -395,21 +406,36 @@ export interface RouteLegs {
395
406
  status: number;
396
407
  body: object;
397
408
  }>;
398
- /** assistant plan_review 三态腿。 */
399
- resumePlanReview(sessionId: string, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined,
409
+ /**
410
+ * assistant plan_review 三态腿的**纯读段**(S-609,7.97.0):选卡 → 属主轻读 → 载入 → 行级 scope → 门类 → 这条决断
411
+ * 唯一的一次 ctx 读。任何一步拒 ⇒ `{ok:false, answer}`(不改任何状态;端点在消费一次性 nonce **之前**把它答出去);
412
+ * 全部过关 ⇒ `{ok:true, prepared}`,交给 {@link RouteLegs.drivePlanReview}。语义属主 = `resume-legs.ts` 同名函数顶注。
413
+ */
414
+ preparePlanReview(sessionId: string,
400
415
  /** [ref]/[ref] 件5:`decide_404` 判别日志的附加位(只进日志,不上 wire;身份只记有无不记值)。 */
401
416
  log?: {
402
417
  taskId?: string;
403
418
  principalPresent?: boolean;
404
419
  },
405
- /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
406
- acceptEarly?: boolean,
407
- /** [ref]②([ref]):调用方已验身份的裁定(直连门 = HMAC 验出的 principal)—— 腿内行级属主复核
420
+ /** [ref]②([ref]):调用方已验身份的裁定(直连门 = HMAC 验出的 principal)—— 属主轻读与行级属主复核
408
421
  * ([ref] R3 同形;缺席 = 匿名 dev 部署,不咬)。 */
409
422
  decider?: {
410
423
  principal?: string;
411
424
  explicitOperator: boolean;
412
- },
425
+ }): Promise<{
426
+ readonly ok: false;
427
+ readonly answer: {
428
+ status: number;
429
+ body: object;
430
+ };
431
+ } | {
432
+ readonly ok: true;
433
+ readonly prepared: PreparedPlanReview;
434
+ }>;
435
+ /** plan_review 三态腿的**驱动段**(S-609):只吃 prepare 的结果,用它选定的 token,不再选卡、不再读 ctx。 */
436
+ drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined,
437
+ /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
438
+ acceptEarly?: boolean,
413
439
  /** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER`;只在 `approve`
414
440
  * 上有意义,缺席 ⇒ `"default"`)。语义与判据的唯一属主 = `resume-legs.ts` 的同名形参顶注。 */
415
441
  permissionModeAfter?: PlanApprovePermissionModeAfter): Promise<{
@@ -36,7 +36,7 @@ export declare function planReviewBodyKeyIssue(raw: unknown): KeyClosureIssue |
36
36
  * · **签名域**(直连门的 HMAC 信封第五位)—— 属主是**跨仓字节契约**(§6.2 / `approvalHmacMessage`,
37
37
  * 签名器在 cli / SDK)。那份契约明写空串与 `null` 是两个不同的被签值。⇒ **逐字线值,永不折叠**;
38
38
  * 在这里折一刀,一份合法签发的空理由证明就永远 401(合并复审 R1 [high] 真复现过)。
39
- * · **上送域**(`resumePlanReview` / `resumeCheckpoint` 的 `reason` 尾参)—— 属主是**引擎的单铸点**
39
+ * · **上送域**(`drivePlanReview` / `resumeCheckpoint` 的 `reason` 尾参)—— 属主是**引擎的单铸点**
40
40
  * (7.19.0 / [ref] 把「人的结算」收进一个铸点,空文本在那里折成缺席,live 与 durable decide 两条路径同形)。
41
41
  * ⇒ **原样透传**;跨仓宪法「源头修复禁下游旁路」:在下游再折一次就是同一语义的第二个写者,
42
42
  * 今天两者答案相同,上游哪天改法就变成一条谁都查不出来的分歧。
@@ -19,6 +19,8 @@ import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal }
19
19
  import { governanceOriginOf } from "../active-run-conflict.js";
20
20
  import { sendResumeOutcome } from "../route-ctx.js";
21
21
  import { assertNever } from "../route-ctx.js";
22
+ import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
23
+ import { recordFailOpen } from "../../observability/fail-open.js";
22
24
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
23
25
  const DECIDE_BODY_DECLARED_KEYS = [
24
26
  "answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "settledBy", "updatedInput",
@@ -199,7 +201,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
199
201
  const { deps } = ctx;
200
202
  const { preemptableRuns } = ctx.registry;
201
203
  const { readJson, rateLimited, quotaExceeded, safeDecode } = ctx.helpers;
202
- const { resumeCheckpoint, resumePreempted, resumePlanReview } = ctx.legs;
204
+ const { resumeCheckpoint, resumePreempted, preparePlanReview, drivePlanReview } = ctx.legs;
203
205
  {
204
206
  const cs = deps.checkpointStore;
205
207
  const principal = gatedPrincipal(req, deps.config);
@@ -271,7 +273,10 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
271
273
  const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
272
274
  const summaries = await cs.listByScope(scope);
273
275
  const inbox = (await Promise.all(summaries.map(async (s) => {
274
- const ctx = await cs.getCtx(s.sessionId).catch(() => null);
276
+ const ctxRead = await readResumeCtx(cs.getCtx(s.sessionId));
277
+ if (ctxRead.kind === "corrupt")
278
+ recordFailOpen("server.assistant-inbox.objective-ctx-corrupt", ctxRead.where);
279
+ const objective = ctxRead.kind === "ok" ? (ctxRead.ctx.body?.objective ?? null) : null;
275
280
  const input = s.toolInput != null ? redactedPreview(s.toolInput) : null;
276
281
  return {
277
282
  sessionId: s.sessionId,
@@ -286,7 +291,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
286
291
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
287
292
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
288
293
  ...(s.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: s.ruleStoreUnreadable } : {}),
289
- objective: ctx?.body?.objective ?? null,
294
+ objective,
290
295
  input,
291
296
  };
292
297
  }))).sort((a, b) => (b.severity ?? 0) - (a.severity ?? 0) || (a.createdAt ?? 0) - (b.createdAt ?? 0));
@@ -520,9 +525,14 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
520
525
  sendError(res, 404, "not_found.run", "task not found");
521
526
  return;
522
527
  }
528
+ const plan = await preparePlanReview(run.sessionId, { taskId, principalPresent: principal !== undefined }, { principal: deciderPrincipal, explicitOperator });
529
+ if (!plan.ok) {
530
+ sendResumeOutcome(res, plan.answer, deps.logger);
531
+ return;
532
+ }
523
533
  if (body.permissionModeAfter !== undefined) {
524
- const storedCtx = await deps.checkpointStore?.getCtx(run.sessionId).catch(() => null);
525
- if (storedCtx?.body !== undefined && storedBodyPermissionMode(storedCtx.body) !== "plan") {
534
+ const storedCtx = plan.prepared.ctx;
535
+ if (storedCtx.body !== undefined && storedBodyPermissionMode(storedCtx.body) !== "plan") {
526
536
  sendError(res, 400, "request.field_conflict", "permissionModeAfter does not apply to this task: it was not submitted with permissionMode \"plan\", so approving the plan lifts no read-only restriction");
527
537
  return;
528
538
  }
@@ -539,7 +549,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
539
549
  return;
540
550
  }
541
551
  }
542
- const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined }, true, { principal: deciderPrincipal, explicitOperator }, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
552
+ const out = await drivePlanReview(plan.prepared, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, true, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
543
553
  sendResumeOutcome(res, out, deps.logger);
544
554
  return;
545
555
  }
@@ -12,6 +12,7 @@ import { buildEmptyMcpProbeFace, buildMcpInjectionRefusal, buildMcpProbeCacheKey
12
12
  import { mcpForScenario } from "../../config-center/facade.js";
13
13
  import { sendJson, sendError } from "../send.js";
14
14
  import { assertNever } from "../route-ctx.js";
15
+ import { fileHistoryCaptureOf } from "../../file-history-capture.js";
15
16
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
16
17
  import { projectMemoryEngineCapability } from "../../memory-posture.js";
17
18
  import { projectWriteProtectionCapability } from "../../write-protection.js";
@@ -173,6 +174,7 @@ export async function handleCapabilities(req, res, match, ctx) {
173
174
  rewindFiles: Boolean(deps.fileHistoryStore),
174
175
  restoreFiles: Boolean(deps.fileHistoryStore),
175
176
  rewindFilesTo: Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
177
+ fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore),
176
178
  scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
177
179
  sessionSync: Boolean(deps.backend && deps.sessionStorage?.exportEntries && deps.sessionStorage?.beginImportStaging && deps.fileHistoryStore),
178
180
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
@@ -589,7 +589,7 @@ export async function handleRuns(req, res, match, ctx) {
589
589
  const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
590
590
  if (leafId === undefined || leafId === null)
591
591
  return;
592
- const resumeRead = await readResumeCtx(Promise.resolve(cs.getCtx(run.sessionId)));
592
+ const resumeRead = await readResumeCtx(cs.getCtx(run.sessionId));
593
593
  if (resumeRead.kind === "corrupt") {
594
594
  mintResult.corruptCtxWhere = resumeRead.where;
595
595
  return;
@@ -612,7 +612,7 @@ export async function handleRuns(req, res, match, ctx) {
612
612
  createdAt: Date.now(),
613
613
  sourceTaskId: taskId,
614
614
  });
615
- const stillRead = await readResumeCtx(Promise.resolve(cs.getCtx(run.sessionId)));
615
+ const stillRead = await readResumeCtx(cs.getCtx(run.sessionId));
616
616
  if (stillRead.kind === "corrupt") {
617
617
  await cs.expire(wakeToken, cpScope).catch(() => undefined);
618
618
  mintResult.corruptCtxWhere = stillRead.where;
@@ -41,6 +41,11 @@ export async function handleSessionSync(req, res, match, ctx) {
41
41
  sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
42
42
  return;
43
43
  }
44
+ const historyStore = deps.fileHistoryStore;
45
+ if (historyStore === undefined) {
46
+ sendError(res, 501, "capability.snapshot_store_required", "session sync is not available on this deployment: file-history capture is turned off (FILE_HISTORY_CAPTURE=false), so there is no file history to export or import");
47
+ return;
48
+ }
44
49
  const scope = sessionOwnerScopeForWrite(req);
45
50
  if (!scope.fleetWide && scope.gateOwner === null) {
46
51
  sendError(res, 401, "auth.unauthorized", "unauthorized");
@@ -76,7 +81,7 @@ export async function handleSessionSync(req, res, match, ctx) {
76
81
  return;
77
82
  let out;
78
83
  try {
79
- out = await exportSessionManifest(sessionId, deps.backend);
84
+ out = await exportSessionManifest(sessionId, deps.backend, historyStore);
80
85
  }
81
86
  catch (e) {
82
87
  if (sendSessionPolicyError(res, e))
@@ -146,7 +146,10 @@ export interface ServiceStoreDeps {
146
146
  toolResultStore?: ToolResultStoreFull;
147
147
  /** [ref] per-edited-file rewind history store. Present ⇒ the rewind capabilities are advertised; core uses it
148
148
  * via RunnerDeps.fileHistoryStore (first-touch tracking + per-turn boundaries + tracked-set restore, ANY
149
- * env). Also the 2c history-transfer store (exportHistory/importHistory + blob negotiation faces). */
149
+ * env). Also the 2c history-transfer store (exportHistory/importHistory + blob negotiation faces).
150
+ * 🔴 S-589:这是装配单点 `assembleFileHistory` 的**捕获脸**(后端 ∧ `FILE_HISTORY_CAPTURE`),不是「后端有没有这只店」
151
+ * —— 旋钮关着时它缺席,rewind 族四位、能力词 `fileHistoryCapture` 与 2c sync 路由族一起按「店不在」走。
152
+ * 擦除(E21 / 孤儿 blob GC)不读本键,读 boot 侧的擦除脸。 */
150
153
  fileHistoryStore?: ServiceFileHistoryStore;
151
154
  /** D-1 通用文件上传(clay 拍 2026-07-27:独立附件 store,不骑 snapshot blob 面):enables
152
155
  * POST/GET/DELETE /v1/attachments*。缺席 ⇒ 501(与其他可选 store 面同姿)。绑定/物化半场在
@@ -94,7 +94,7 @@ export function createHttpServer(rawDeps) {
94
94
  }
95
95
  const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
96
96
  const { prepareSpec } = createAdmission({ deps, readJson, runnerFor });
97
- const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, resumePlanReview } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
97
+ const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
98
98
  const { denyExpiredApprovals, redeemParkedAsk } = createApprovalSweeps({ deps, resumeCheckpoint, mayDiscloseRowError });
99
99
  const { handle } = createDispatch({ deps, noteActivity, routeCtxBase, counters });
100
100
  const server = http.createServer((req, res) => {
@@ -19,6 +19,6 @@ export declare function clampVerifyRounds(v: unknown): number;
19
19
  /** `undefined` when `body.verify` is not `true` (the caller didn't opt into the gate); otherwise the clamped
20
20
  * rounds + (if `maxCostUsd` is a positive number) the same USD→micro-USD cost ceiling the cascade lane uses.
21
21
  * Same expression that lived inline at all four verify/resume call sites in http/server.ts (fresh-submit,
22
- * resumeCheckpoint, resumePreempted, resumePlanReview) — collected here VERBATIM ([ref] R9). */
22
+ * resumeCheckpoint, resumePreempted, drivePlanReview — named resumePlanReview before S-609) — collected here VERBATIM ([ref] R9). */
23
23
  export declare function verifyRoundsFromBody(body: Pick<TaskRequestBody, "verify" | "verifyRounds" | "maxCostUsd">): VerifyRoundsSpec | undefined;
24
24
  //# sourceMappingURL=verify-rounds.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import { FileMemoryEngineBackend, type TaskSpec } from "@sema-agent/core";
2
2
  import type { ServiceConfig } from "./config-types.js";
3
+ import { type ExecutionLane } from "./execution-lane-caps.js";
3
4
  /** 前缀词表本体(供门/诊断读;判定一律走 {@link isV2ScopeKey},别在别处重写 startsWith 链)。 */
4
5
  export declare const V2_SCOPE_PREFIXES: readonly string[];
5
6
  /**
@@ -40,10 +41,13 @@ export declare function memoryScopeFor(config: ServiceConfig, principal?: string
40
41
  * (`MEMORY_ENGINE_REMOTE_LANE=allow`, e.g. ssh to a host on the same NFS mount).
41
42
  *
42
43
  * Pure posture probe (memoryEngineBackendFor consumes it; main.ts logs it): `undefined` = no plane split
43
- * (no remote lane, or the `host` lane), `dark` = split + fail-closed, `forced` = split + operator override.
44
+ * (the lane's file plane IS this worker's fs — `in-process` / `host`), `dark` = split + fail-closed, `forced` =
45
+ * split + operator override.
46
+ * S-602: the plane question reads the lane-caps table (`hostFilePlane`) — it used to be the hand-written synonym
47
+ * `lane === undefined || lane === "host"` (fold-ban gate D, R3); readings are unchanged lane by lane.
44
48
  */
45
49
  export declare function memoryEngineRemoteLanePosture(config: ServiceConfig): {
46
- lane: string;
50
+ lane: ExecutionLane;
47
51
  posture: "dark" | "forced";
48
52
  } | undefined;
49
53
  /**
@@ -1,5 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { FileMemoryEngineBackend, resolveMemoryEngineRoot, deriveControlPlaneDir, formatUserScope, formatProjScope, normalizeMemorySpec } from "@sema-agent/core";
3
+ import { executionLaneCaps, executionLaneOf } from "./execution-lane-caps.js";
3
4
  import { assertMemoryScopeWidth } from "./plugins/memory-key-guards.js";
4
5
  const V2_SCOPE_PREFIX_BY_KIND = {
5
6
  user: "user:",
@@ -26,8 +27,8 @@ export function memoryScopeFor(config, principal, projectId) {
26
27
  return config.memoryScope;
27
28
  }
28
29
  export function memoryEngineRemoteLanePosture(config) {
29
- const lane = config.remoteExec?.provider;
30
- if (lane === undefined || lane === "host")
30
+ const lane = executionLaneOf(config.remoteExec?.provider);
31
+ if (executionLaneCaps(lane).hostFilePlane === "supported")
31
32
  return undefined;
32
33
  return { lane, posture: config.memoryEngineRemoteLaneAllowed === true ? "forced" : "dark" };
33
34
  }
@@ -281,6 +281,10 @@ export declare const FAIL_OPEN_TAGS: {
281
281
  readonly cls: "F";
282
282
  readonly note: "S-287(`plugins/ensure-index.ts` 的 PG 臂):这口 PG **协议**面不认 `CREATE INDEX CONCURRENTLY`。今天唯一的真消费者是**仓内离线镜像的手搓 `PgQueryFn` 假件**(`pg-mem` 不是本仓依赖,`test/memory-embedder-fingerprint.test.ts` 头注已为此记过一次档,这里不重复那个错)。判据是**能力面**不是名字,且**只认阳性信号**:SQLSTATE ∈ {`0A000` feature_not_supported、`42601` syntax_error},或错误**完全不带 `code`**(= 不是驱动也不是服务端给的错误)⇒ 判「不认」,回退成普通 `CREATE INDEX IF NOT EXISTS`;**带任何其它 `code` 的一律原样抛** —— SQLSTATE 的 `55P03`/`57014`/`23505`/`42501`/`25001` 与 libuv 的 `ECONNRESET`/`EPIPE`/`ETIMEDOUT` 都在这一侧(⚠️ 不要按「5 位大写」的形状去分这两族:`EPIPE` 恰好也是 5 位大写)。放行的最坏后果 = 这一次建索引持表级 `SHARE` 锁阻塞该表的写(纯**性能/可用性**面,索引本身照样建成、语义零差别),而能走到这条臂的面**按定义**不是生产 PG。计数非零 = 有一口本以为是真 PG 的部署其实不认 CONCURRENTLY,运维要去看它到底是什么。detail = `<表>.<索引名>`。";
283
283
  };
284
+ readonly "server.assistant-inbox.objective-ctx-corrupt": {
285
+ readonly cls: "F";
286
+ readonly note: "S-609(`routes/approvals-assistant.ts` 的 `GET /v1/assistant/inbox`):某一行的续跑上下文(checkpoint_ctx)读出来是**坏体**(店的拒绝契约 `StoreCorruptError`;local = 档不是 JSON / 信封不合形,SQL = 列是合法 JSON 但不是一只对象)⇒ **这一行**的 `objective` 落 `null`,列表其余照常。放行的最坏后果 = inbox 上这一张卡不显示它属于哪个任务(纯展示位;这一格不参与任何决裁、续跑或门判 —— 续跑腿与 plan_review 门对同一行各自走读口、坏体一律响亮 500)。不放行的代价是整只 `summaries.map` 抛出 ⇒ 一行坏体打掉整个用户的 inbox。故 F 类;必须留痕,因为修前那句 `.catch(() => null)` 让「坏体」与「本来就没有」同形无痕(detail = 读口转述的 `where`,人拿它去修那一行)。缺席(行不在 / 店抖动)**不**记本 tag:那是读口的刻意保守,不是失败开放。";
287
+ };
284
288
  };
285
289
  /** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
286
290
  export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
@@ -254,6 +254,10 @@ export const FAIL_OPEN_TAGS = {
254
254
  cls: "F",
255
255
  note: "S-287(`plugins/ensure-index.ts` 的 PG 臂):这口 PG **协议**面不认 `CREATE INDEX CONCURRENTLY`。今天唯一的真消费者是**仓内离线镜像的手搓 `PgQueryFn` 假件**(`pg-mem` 不是本仓依赖,`test/memory-embedder-fingerprint.test.ts` 头注已为此记过一次档,这里不重复那个错)。判据是**能力面**不是名字,且**只认阳性信号**:SQLSTATE ∈ {`0A000` feature_not_supported、`42601` syntax_error},或错误**完全不带 `code`**(= 不是驱动也不是服务端给的错误)⇒ 判「不认」,回退成普通 `CREATE INDEX IF NOT EXISTS`;**带任何其它 `code` 的一律原样抛** —— SQLSTATE 的 `55P03`/`57014`/`23505`/`42501`/`25001` 与 libuv 的 `ECONNRESET`/`EPIPE`/`ETIMEDOUT` 都在这一侧(⚠️ 不要按「5 位大写」的形状去分这两族:`EPIPE` 恰好也是 5 位大写)。放行的最坏后果 = 这一次建索引持表级 `SHARE` 锁阻塞该表的写(纯**性能/可用性**面,索引本身照样建成、语义零差别),而能走到这条臂的面**按定义**不是生产 PG。计数非零 = 有一口本以为是真 PG 的部署其实不认 CONCURRENTLY,运维要去看它到底是什么。detail = `<表>.<索引名>`。"
256
256
  },
257
+ "server.assistant-inbox.objective-ctx-corrupt": {
258
+ cls: "F",
259
+ note: "S-609(`routes/approvals-assistant.ts` 的 `GET /v1/assistant/inbox`):某一行的续跑上下文(checkpoint_ctx)读出来是**坏体**(店的拒绝契约 `StoreCorruptError`;local = 档不是 JSON / 信封不合形,SQL = 列是合法 JSON 但不是一只对象)⇒ **这一行**的 `objective` 落 `null`,列表其余照常。放行的最坏后果 = inbox 上这一张卡不显示它属于哪个任务(纯展示位;这一格不参与任何决裁、续跑或门判 —— 续跑腿与 plan_review 门对同一行各自走读口、坏体一律响亮 500)。不放行的代价是整只 `summaries.map` 抛出 ⇒ 一行坏体打掉整个用户的 inbox。故 F 类;必须留痕,因为修前那句 `.catch(() => null)` 让「坏体」与「本来就没有」同形无痕(detail = 读口转述的 `where`,人拿它去修那一行)。缺席(行不在 / 店抖动)**不**记本 tag:那是读口的刻意保守,不是失败开放。",
260
+ },
257
261
  };
258
262
  export function failOpenTagForDroppedFrame(frameType) {
259
263
  switch (frameType) {
package/dist/run-local.js CHANGED
@@ -24,10 +24,9 @@ import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
24
24
  import { createConfigProvider } from "./config-provider.js";
25
25
  import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
26
26
  import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
27
- import { executionLaneHomeDir } from "./execution-lane-caps.js";
27
+ import { executionLaneHomeDir, executionLaneOf } from "./execution-lane-caps.js";
28
28
  import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
29
- import { loadSkills } from "./capabilities/skills.js";
30
- import { toolsRunOnThisHost } from "./task-cwd.js";
29
+ import { loadDeploymentSkills } from "./capabilities/skills.js";
31
30
  import { createRepoClient } from "./capabilities/repo-tools.js";
32
31
  import { webSearchConfigFromEnv, createWebSearchBackend } from "./plugins/web-search.js";
33
32
  import { buildScenarios, selectScenario, centerScenarios } from "./capabilities/scenarios.js";
@@ -424,7 +423,7 @@ export async function runLocal(argv, deps = {}) {
424
423
  const subRunner = new Runner(subRunnerDeps);
425
424
  const handslessRunner = new Runner(withoutExecutionEnv(runnerDeps));
426
425
  const handslessSubRunner = new Runner(withoutExecutionEnv(subRunnerDeps));
427
- const skills = loadSkills(config.skillsDir, { toolsShareThisFilesystem: toolsRunOnThisHost(config) });
426
+ const skills = loadDeploymentSkills(config);
428
427
  const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
429
428
  const webSearchCfg = webSearchConfigFromEnv();
430
429
  const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
@@ -489,7 +488,7 @@ export async function runLocal(argv, deps = {}) {
489
488
  ...(a2a ? { a2a } : {}),
490
489
  ...(Object.keys(limits).length > 0 ? { limits } : {}),
491
490
  }, config, workspaceDir, memoryEngine?.root);
492
- logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: config.remoteExec?.provider ?? "in-process" });
491
+ logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: executionLaneOf(config.remoteExec?.provider) });
493
492
  const settleSessionBackground = async () => {
494
493
  const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, scope);
495
494
  if (reaped === 0)
@@ -140,7 +140,10 @@ export interface SessionManifest {
140
140
  * `null` ONLY for "session does not exist". (Named exportSessionManifest, NOT exportManifest, to avoid the collision
141
141
  * with the history store's own export verb.)
142
142
  */
143
- export declare function exportSessionManifest(sessionId: string, srcBackend: StoreBackend): Promise<{
143
+ export declare function exportSessionManifest(sessionId: string, srcBackend: StoreBackend,
144
+ /** S-589:文件历史读**装配结果**(捕获脸),不直取 `srcBackend.fileHistory()` —— 「这台部署有没有文件历史店」
145
+ * 只在 `assembleFileHistory` 判一次。同一个后端铸的同一只店,四店同源的不变式不变。 */
146
+ historyStore: ServiceFileHistoryStore): Promise<{
144
147
  manifest: SessionManifest;
145
148
  getBlob: BlobGetter;
146
149
  } | null>;
@@ -21,7 +21,7 @@ export async function replayFileHistory(store, scope, envelope, srcGetBlob, opts
21
21
  function ownerAware(backend) {
22
22
  return backend.session();
23
23
  }
24
- export async function exportSessionManifest(sessionId, srcBackend) {
24
+ export async function exportSessionManifest(sessionId, srcBackend, historyStore) {
25
25
  const session = ownerAware(srcBackend);
26
26
  const listEntryIds = session.listEntryIds?.bind(session);
27
27
  if (!listEntryIds)
@@ -31,7 +31,7 @@ export async function exportSessionManifest(sessionId, srcBackend) {
31
31
  return null;
32
32
  const getLeafId = session.getLeafId?.bind(session);
33
33
  const leafId = getLeafId ? await getLeafId(sessionId) : null;
34
- const fs = srcBackend.fileHistory();
34
+ const fs = historyStore;
35
35
  const fileHistory = typeof fs.exportHistory === "function" ? await fs.exportHistory(sessionId) : null;
36
36
  const policyStore = srcBackend.sessionPolicy();
37
37
  const policy = typeof policyStore.listBySession === "function"
@@ -56,12 +56,18 @@ export declare function cwdHonored(config: {
56
56
  * S-377 —— 「**工具跑的那台机器**就是本进程这台吗」。回答的是一个**文件系统同一性**问题,不是权限问题:
57
57
  * server 盘上的一条绝对路径(出厂 `SKILLS_DIR` 下某个技能自己的目录)对模型的工具**有没有意义**。
58
58
  *
59
+ * 判据 = 能力表两位的合取 {@link handsOnWorkerFilePlane}(`coreRemoteExecutionEnv ∧ hostFilePlane`,都读表,
60
+ * 本函数不手写任何车道词):
59
61
  * · `host`(run-on-this-machine,无容器)⇒ 真;
60
- * · `undefined`(in-process 单用户 lane)⇒ 真 —— 该 lane 根本没挂手(core 落 `StubExecutionEnv`),
61
- * 路径对谁都不可达,但它在**文件系统同一性**这条轴上确实是同一台机器,判真让规则只剩一条;
62
+ * · `in-process`(`REMOTE_EXEC` 未设)⇒ **假**(S-602,clay 裁 [ref] A 翻面;改前判真)—— 该 lane 根本
63
+ * 没挂手(core 落 `StubExecutionEnv`),「工具的盘」这句话在这里没有指称对象。改前的理由「文件系统同一性
64
+ * 这条轴上确实是同一台机器,判真让规则只剩一条」把**缺席折成了邻居**:wire 上的 `toolsOnThisHost` 因此
65
+ * 对无手部署说真,消费端的 doctor 据此说「工具在本机」。代价如实记:出厂目录形技能在 in-process 上不再带
66
+ * `baseDir` —— core 律:缺席 ⇒ 结果逐字节同 baseDir 到货之前,而那条 lane 上本来也没有能读它的工具;
62
67
  * · 其余每一条(`k8s` / `e2b` / `ssh` / `adb` / `device` / `local-docker`)⇒ 假:工具的盘在别处,
63
68
  * server 自己的路径在那边**不存在**,声明它等于给模型一条指向不存在文件的线索,还顺带把本机目录
64
- * 布局说了出去。
69
+ * 布局说了出去;
70
+ * · 认不出的词 ⇒ 假(fail-closed:没认出来的车道不许被当成「本机」)。
65
71
  *
66
72
  * 🔴 **为什么不复用 {@link cwdHonored}**:那一只是**权限**判据(单用户 ∧ host),它的 `requirePrincipal`
67
73
  * 那一半防的是「一个租户的输入把 agent 指向另一个租户的文件」。本谓词判的路径不是调用方给的,是**运维
package/dist/task-cwd.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { executionLaneOf, handsOnWorkerFilePlane } from "./execution-lane-caps.js";
2
3
  export const MAX_CWD_CHARS = 4_096;
3
4
  export function isValidCwd(cwd) {
4
5
  return (typeof cwd === "string" &&
@@ -21,7 +22,8 @@ export function cwdHonored(config) {
21
22
  return config.remoteExec?.provider === "host" && config.requirePrincipal !== true;
22
23
  }
23
24
  export function toolsRunOnThisHost(config) {
24
- return config.remoteExec === undefined || config.remoteExec.provider === undefined || config.remoteExec.provider === "host";
25
+ const lane = executionLaneOf(config.remoteExec?.provider);
26
+ return lane !== undefined && handsOnWorkerFilePlane(lane);
25
27
  }
26
28
  export function deviceCwdHonored(config) {
27
29
  return config.remoteExec?.provider === "device";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.96.0",
3
+ "version": "7.97.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",