@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.
- package/USAGE.md +4 -1
- package/dist/approval.d.ts +28 -0
- package/dist/approval.js +24 -0
- package/dist/boot/budget-tracing.d.ts +1 -0
- package/dist/boot/budget-tracing.js +3 -2
- package/dist/boot/config-center.js +2 -5
- package/dist/boot/coordinators.js +2 -2
- package/dist/boot/deferred-sandbox-path-env.d.ts +5 -2
- package/dist/boot/deferred-sandbox-path-env.js +3 -1
- package/dist/boot/memory-boundary.d.ts +17 -2
- package/dist/boot/memory-boundary.js +11 -0
- package/dist/boot/reapers.d.ts +3 -1
- package/dist/boot/reapers.js +2 -2
- package/dist/boot/resolve-spec.js +6 -6
- package/dist/boot/runner-deps.d.ts +2 -1
- package/dist/boot/session-faces.d.ts +4 -1
- package/dist/boot/session-faces.js +5 -5
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-04-stores.js +2 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -0
- package/dist/boot/stage-06-runners.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +10 -14
- package/dist/boot/stage-08-reapers.d.ts +2 -3
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stores.js +5 -11
- package/dist/capabilities/execution-lane.d.ts +5 -7
- package/dist/capabilities/execution-lane.js +2 -1
- package/dist/capabilities/memory-notice.d.ts +30 -12
- package/dist/capabilities/memory-notice.js +8 -1
- package/dist/capabilities/skills.d.ts +14 -0
- package/dist/capabilities/skills.js +4 -0
- package/dist/config-catalog.js +1 -0
- package/dist/config-types.d.ts +8 -1
- package/dist/config.js +4 -3
- package/dist/env-facts.js +2 -4
- package/dist/execution-lane-caps.d.ts +36 -6
- package/dist/execution-lane-caps.js +4 -0
- package/dist/file-history-capture.d.ts +65 -0
- package/dist/file-history-capture.js +21 -0
- package/dist/hosted-posture.d.ts +1 -1
- package/dist/http/resume-legs.d.ts +14 -4
- package/dist/http/resume-legs.js +26 -17
- package/dist/http/route-ctx.d.ts +35 -9
- package/dist/http/routes/approvals-assistant.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +16 -6
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/routes/runs.js +2 -2
- package/dist/http/routes/session-sync.js +6 -1
- package/dist/http/server.d.ts +4 -1
- package/dist/http/server.js +1 -1
- package/dist/http/verify-rounds.d.ts +1 -1
- package/dist/memory-scope.d.ts +6 -2
- package/dist/memory-scope.js +3 -2
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/run-local.js +4 -5
- package/dist/session-sync.d.ts +4 -1
- package/dist/session-sync.js +2 -2
- package/dist/task-cwd.d.ts +9 -3
- package/dist/task-cwd.js +3 -1
- package/package.json +1 -1
package/dist/http/resume-legs.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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("
|
|
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,
|
|
1281
|
+
legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, preparePlanReview, drivePlanReview },
|
|
1273
1282
|
};
|
|
1274
|
-
return { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake,
|
|
1283
|
+
return { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, preparePlanReview, drivePlanReview };
|
|
1275
1284
|
}
|
|
1276
1285
|
//# sourceMappingURL=resume-legs.js.map
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -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
|
-
* 静默放宽):写失败 ⇒ 耐久体保持**上一次**的值,而这四条腿里只有 `
|
|
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
|
-
/**
|
|
399
|
-
|
|
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]
|
|
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
|
-
* · **上送域**(`
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
525
|
-
if (storedCtx
|
|
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
|
|
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),
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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))
|
package/dist/http/server.d.ts
CHANGED
|
@@ -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 面同姿)。绑定/物化半场在
|
package/dist/http/server.js
CHANGED
|
@@ -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
|
|
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
|
package/dist/memory-scope.d.ts
CHANGED
|
@@ -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
|
-
* (
|
|
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:
|
|
50
|
+
lane: ExecutionLane;
|
|
47
51
|
posture: "dark" | "forced";
|
|
48
52
|
} | undefined;
|
|
49
53
|
/**
|
package/dist/memory-scope.js
CHANGED
|
@@ -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 ===
|
|
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 {
|
|
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 =
|
|
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
|
|
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)
|
package/dist/session-sync.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/session-sync.js
CHANGED
|
@@ -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 =
|
|
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"
|
package/dist/task-cwd.d.ts
CHANGED
|
@@ -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
|
-
* · `
|
|
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
|
-
|
|
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.
|
|
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",
|