@sema-agent/server 7.84.0 → 7.86.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 +24 -2
- package/dist/approval-card.d.ts +7 -0
- package/dist/approval-card.js +2 -2
- package/dist/approval-hmac.d.ts +28 -1
- package/dist/approval-hmac.js +2 -1
- package/dist/boot/stage-10-http-server.js +2 -1
- package/dist/capabilities/execution-lane.d.ts +22 -0
- package/dist/capabilities/execution-lane.js +8 -0
- package/dist/capabilities/memory-notice.d.ts +4 -1
- package/dist/config-catalog.js +3 -2
- package/dist/config-types.d.ts +5 -1
- package/dist/execution-lane-caps.d.ts +13 -0
- package/dist/execution-lane-caps.js +1 -0
- package/dist/fleet/fleet-reconciler.js +2 -2
- package/dist/http/resume-legs.d.ts +2 -1
- package/dist/http/resume-legs.js +21 -6
- package/dist/http/route-ctx.d.ts +51 -1
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/approvals-assistant.d.ts +11 -0
- package/dist/http/routes/approvals-assistant.js +61 -16
- package/dist/http/routes/capabilities.js +3 -0
- package/dist/http/routes/images.js +38 -8
- package/dist/http/routes/mcp-reconnect.d.ts +56 -0
- package/dist/http/routes/mcp-reconnect.js +139 -0
- package/dist/http/routes/memory-policy.js +3 -6
- package/dist/http/routes/notify-wake.js +11 -3
- package/dist/http/routes/run-memory-capture.js +3 -5
- package/dist/http/routes/runs.js +21 -23
- package/dist/http/routes/sessions.js +11 -2
- package/dist/http/routes/shared-memory.js +5 -1
- package/dist/http/server.d.ts +14 -0
- package/dist/http/wire-types.d.ts +5 -1
- package/dist/memory-scope.d.ts +16 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +8 -2
- package/dist/plugins/store-contracts.d.ts +23 -0
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/principal-jwt.d.ts +15 -7
- package/dist/principal-jwt.js +1 -1
- package/dist/read-face-posture.d.ts +6 -2
- package/dist/task-settings.d.ts +35 -0
- package/dist/task-settings.js +7 -0
- package/dist/trace/core-keyset-guard.d.ts +49 -3
- package/dist/trace/project.d.ts +12 -0
- package/dist/trace/project.js +14 -0
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -408,6 +408,12 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
|
|
|
408
408
|
车道会尊重它(那条车道的定义就是"运维自己这台机、无容器边界")。所以「服务跑在 A 机、代码在 B 机」不成立
|
|
409
409
|
—— 没有任何东西把 B 的路径映到 A。要么把服务(或 `run-local`)跑在放着文件的那台机器上,要么走容器车道
|
|
410
410
|
(`e2b`/`k8s`/`local-docker`),那里的工作区是沙箱自己的,`cwd` 被忽略。
|
|
411
|
+
- **读面(S-426 起)**:`GET /v1/capabilities` 的 `executionLane` 把本部署的车道广告成
|
|
412
|
+
`{provider:<REMOTE_EXEC 闭集词>, toolsOnThisHost:boolean}`(`REMOTE_EXEC` 未设 ⇒ `provider:"host"`)。
|
|
413
|
+
壳/客户端判「工具跑不跑在本机」**读这一位**,不要再从自己这一侧的 `REMOTE_EXEC` 猜 —— 连远端引擎时
|
|
414
|
+
那个 env 说的是**壳自己这台**的事。⚠️ 它是**文件系统同一性**位,**不是**权限位:`cwd` 兑不兑现仍由
|
|
415
|
+
`projectContext` / `callerCwd` 回答(那两位的判据多一条单用户约束,多租户 host 部署上两者会分叉)。
|
|
416
|
+
端点/凭据/镜像/`serial` 一个字都不上这条 wire,那些仍只在 operator 面 `GET /v1/config/catalog`。
|
|
411
417
|
|
|
412
418
|
**workspace 浏览面 —— 已随整树快照纪元退役**
|
|
413
419
|
- 旧四端点(list/tree/file/archive)恒 `501 capability.workspace_retired`,`capabilities.workspace`
|
|
@@ -669,8 +675,12 @@ READ_DENY_PATTERNS='[{"pattern":"**/secrets/**"},{"pattern":".ssh","caseSensitiv
|
|
|
669
675
|
「读工作区外的文件」从被拒变成放行。**要钉回收敛姿态:显式 `READ_FACE=roots`。**
|
|
670
676
|
· **多租户**(`REQUIRE_PRINCIPAL=true`):缺席仍**不铸**、不传给引擎(引擎默认 `roots`)——**一个字节不变**。
|
|
671
677
|
· `READ_DENY_PATTERNS` / `READ_DENY_BUILTIN_TIERS` / `READ_DENY_BUILTIN_EXCLUDE` 三键的「缺席不铸」
|
|
672
|
-
|
|
673
|
-
|
|
678
|
+
**不变**。⚠️ **但「不铸」在引擎里的读法自 core 7.22.0(内置读 deny 表出厂 OFF 的裁定)起翻了面**:`READ_DENY_BUILTIN_TIERS`
|
|
679
|
+
未设 = **OFF**(内建表一行不生效),此前是「除 shell-history 外四档全开」。⇒ 一台没配过这根 env 的机器
|
|
680
|
+
升级后**读得到**根内的 `.npmrc` / `.ssh` / `.aws` / `.claude/settings.json` 这类名字,且整个递归读族
|
|
681
|
+
零卡执行。**要老姿态就显式写** `READ_DENY_BUILTIN_TIERS=credentials,browser,wallet,agent-config`
|
|
682
|
+
(运维面全文见 `docs/DEPLOY-PREREQS.md` 的 core 7.22.0 段)。`open` 与这张表仍是**两个并列机制**:
|
|
683
|
+
`open` 放开的是「工作区外」那道围栏,内建表判的是名字 —— 只是后者现在默认空着。
|
|
674
684
|
· 「这一档到底是谁定的」由 operator 面 `GET /v1/diagnostics/wiring` 的 `readFace` 段答:
|
|
675
685
|
`{face, source, note}`,`source` ∈ `env`(本机 env 显式)/ `center`(组织下发)/ `posture`(单机派生)/
|
|
676
686
|
`engine-default`(没钉,引擎当家),`note` 是指路句。
|
|
@@ -1520,6 +1530,18 @@ GET /v1/capabilities?permissionMode=default
|
|
|
1520
1530
|
**per-run 面**:本批不在 run 记录上加 per-run `autoMode` 座——引擎侧的武装结果 / 未武装原因(含只有引擎知道的那些;core 7.12.0 起熔断族已退役)
|
|
1521
1531
|
将随 core 的状态座到货,届时 server 只投影不自算;此前 `armed` 是 server 侧三项可知判据的裁决。
|
|
1522
1532
|
|
|
1533
|
+
### 9.4b plan 模式(`permissionMode:"plan"`):批准一份计划时,顺带说「解到哪一档」(7.86.0 起)
|
|
1534
|
+
|
|
1535
|
+
只读起步的任务把计划交给人之后停在 `plan_review` 门上。`POST /v1/assistant/tasks/:id/plan_review` 的
|
|
1536
|
+
`decision:"approve"` 可携 **`permissionModeAfter`**(闭集 `"default" | "acceptEdits"`,**缺席 ⇒ `"default"`**):
|
|
1537
|
+
`acceptEdits` = 继续跑时对自己工作目录内的写不再逐次征询,`default` = 有手但每次写照常征询;`edit` / `reject`
|
|
1538
|
+
的语义是重做计划,只读**原样保留**,在它们上送这一位是 `400`(词表外的值、以及本来就不是只读起步的任务送它,
|
|
1539
|
+
同样是 `400`)。解除会写回这条会话的续跑输入 —— 此后每一次挂起→续跑都沿用解除后的档,不回落只读。
|
|
1540
|
+
🔴 **这个体是闭集(7.86.0,S-438)**:受理集 = `decision` / `editedPlan` / `reason` / `permissionModeAfter` /
|
|
1541
|
+
`boundCallId` 五键,集外键(含定谳前的旧拼法 `nextPermissionMode`)**响亮拒** `400 request.body_shape` +
|
|
1542
|
+
机读两桶逐字点名,**不再静默丢** —— 与 `/v1/approvals/:id/decide` 同一只机器、同码同串。
|
|
1543
|
+
逐字契约见 `docs/ASSISTANT-WIRE-CONTRACT.md` §4c。
|
|
1544
|
+
|
|
1523
1545
|
### 9.5 Web search:两车道优先级、per-request `settings.webSearch`、错误形
|
|
1524
1546
|
|
|
1525
1547
|
**两车道,整段整取(不是逐键 merge)**:部署 env(`WEB_SEARCH_*`,§0)与 per-request `body.settings.webSearch`
|
package/dist/approval-card.d.ts
CHANGED
|
@@ -290,6 +290,13 @@ export type ReadRootCandidateProjection = ReadRootGrantCandidate;
|
|
|
290
290
|
* 🔴 形不合 ⇒ **按缺席处置**;超限 ⇒ **整只丢**(见 {@link MAX_READ_ROOT_CANDIDATE_DIR})。
|
|
291
291
|
* 🔴 **缺席不是断言**:它同时覆盖「这一族 ask 没有任何目录能清掉」(敏感路径 deny 行、读边界压根读不懂
|
|
292
292
|
* 的命令、递归遍历、非读边界 ask……)与「老引擎」两形 —— 消费端只读在场,永远不读缺席。
|
|
293
|
+
*
|
|
294
|
+
* 🔴 **逐成员挑键的代价,与它的解药**(core 7.22.0 #862b 真发生):下面这条 `return` 是**逐键重建**的,
|
|
295
|
+
* 而返回型是 core 的 `ReadRootGrantCandidate` ⇒ core 给这只座加可选成员时,新成员被**静默剥掉**且
|
|
296
|
+
* tsc 一声不响(7.22.0 的 `covers` 就是这样被吞过一次:消费端拿到一条看起来正常的「目录出路」,按目录
|
|
297
|
+
* 回授,卡清不掉 —— 而那正是这个成员存在的唯一理由)。解药**不是**改成整只透传(那会让未经校验的远端
|
|
298
|
+
* 形上 wire),而是 `src/trace/core-keyset-guard.ts` ⑭ 的**成员级编译门** `_GuardReadRootCandidate`:
|
|
299
|
+
* core 加成员 ⇒ 那里当场红,由人判该不该投。**改本函数时必须同步那张表。**
|
|
293
300
|
*/
|
|
294
301
|
export declare function readReadRootCandidate(req: unknown): ReadRootCandidateProjection | undefined;
|
|
295
302
|
/**
|
package/dist/approval-card.js
CHANGED
|
@@ -162,7 +162,7 @@ export function readProbeCause(req) {
|
|
|
162
162
|
}
|
|
163
163
|
export const MAX_READ_ROOT_CANDIDATE_DIR = 1024;
|
|
164
164
|
const ReadRootCandidateEnvelopeSchema = z.object({
|
|
165
|
-
readRootCandidate: z.object({ dir: z.string().min(1), clearsThisAsk: z.literal(true) }).optional(),
|
|
165
|
+
readRootCandidate: z.object({ dir: z.string().min(1), clearsThisAsk: z.literal(true), covers: z.literal("exact").optional() }).optional(),
|
|
166
166
|
});
|
|
167
167
|
export function readReadRootCandidate(req) {
|
|
168
168
|
const parsed = ReadRootCandidateEnvelopeSchema.safeParse(req);
|
|
@@ -171,7 +171,7 @@ export function readReadRootCandidate(req) {
|
|
|
171
171
|
return undefined;
|
|
172
172
|
if (raw.dir.length > MAX_READ_ROOT_CANDIDATE_DIR)
|
|
173
173
|
return undefined;
|
|
174
|
-
return { dir: raw.dir, clearsThisAsk: true };
|
|
174
|
+
return { dir: raw.dir, clearsThisAsk: true, ...(raw.covers !== undefined ? { covers: raw.covers } : {}) };
|
|
175
175
|
}
|
|
176
176
|
const MAX_EVIDENCE_DOT_ACTOR = 256;
|
|
177
177
|
const MAX_EVIDENCE_DOTS = 16;
|
package/dist/approval-hmac.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ApprovalHmacKey } from "./auth-keys.js";
|
|
|
3
3
|
* The canonical message an approval MAC signs — a fixed-order, JSON-escaped tuple so no field value (an id, a
|
|
4
4
|
* hash, a free-text note) can ambiguate the boundary with the next (same discipline as the approvals-stream diff
|
|
5
5
|
* key). 🔴 Bound to CLIENT-VISIBLE values only: `sessionId` (in the /decide URL) + `boundCallId`/`boundInputHash`
|
|
6
|
-
* (surfaced by listPending) + `reason` (the caller's own body). It must NOT include the checkpointToken — that is
|
|
6
|
+
* (surfaced by listPending) + `reason` and `permissionModeAfter` (the caller's own body). It must NOT include the checkpointToken — that is
|
|
7
7
|
* the unexposed resume credential the client can never see (it resumes by sessionId), so a checkpointToken-bound
|
|
8
8
|
* message would be uncomputable by the signer.
|
|
9
9
|
*
|
|
@@ -12,11 +12,24 @@ import type { ApprovalHmacKey } from "./auth-keys.js";
|
|
|
12
12
|
* 被接管的客户端 —— 正是 HMAC 要防的风险模型)可以在操作员批准同一个动作的那次决定里,把落账理由换成
|
|
13
13
|
* 别的:动作没变,**审计记录变了**。三方裁定纳入签名载荷,不留 `reasonUnsigned` 宽限代际(未上生产)。
|
|
14
14
|
*
|
|
15
|
+
* ── 🔴 **两把绑定的分工**(S-433 F1 立此,别再把它们当成一回事)─────────────────────────────────
|
|
16
|
+
* `principal-jwt.ts` 的 `approvalBnd`(JWT `cnf.bnd`)只哈希**前三位** `[sessionId, boundCallId, boundInputHash]`
|
|
17
|
+
* —— 它绑的是**「哪一张卡」**(这份身份凭据被签发去决哪一次待决动作),所以决定内容改了它**不该**变。
|
|
18
|
+
* 本文件的 HMAC 信封绑的是**「决了什么」**—— 决定本身的每一个会改变后果的字段都在里面(`decision`、
|
|
19
|
+
* `reason`,以及 S-433 起的 `permissionModeAfter`)。⇒ 「同一张卡上把 approve 的档偷偷调宽」这件事,
|
|
20
|
+
* 结构上只有 HMAC 这一把拦得住;往 `cnf.bnd` 里加位既不解决它,还会把一份合法 JWT 绑死在一个它签发时
|
|
21
|
+
* 根本不知道的值上。**本次(S-433 F1)只动 HMAC 这一把,`approvalBnd` 一个字节不动。**
|
|
22
|
+
*
|
|
15
23
|
* ── 🔴 字节构造规则(权威定义;跨仓签名器照此实现,docs/ASSISTANT-WIRE-CONTRACT.md §6.2 同源)──────────
|
|
16
24
|
* 被 HMAC 的是下面这个字符串的 **UTF-8 字节**:
|
|
17
25
|
* 1. **恒 5 元素、定序**的紧凑 JSON 数组(元素间**无空格**):
|
|
18
26
|
* `[sessionId, boundCallId|null, boundInputHash|null, decision, reason|null]`
|
|
19
27
|
* —— 恒定长度是刻意的:签名器里少一个"有 reason 才追加"的条件分支,就少一类跨实现漂移。
|
|
28
|
+
* 🔴 **唯一的例外是尾位**(S-433,server 7.86.0):`permissionModeAfter` **在场时**追加为第 6 位
|
|
29
|
+
* `[…, permissionModeAfter]`,**缺席时一个字节都不追加**。之所以在这一位上接受那个条件分支:
|
|
30
|
+
* 它换来的是「不签这一位的既有签名器对不带本键的体照常验过」—— `reason` 入签那次是**全体签名器
|
|
31
|
+
* 同拍 BREAKING**(1.284.0),本次刻意不再来一次。带了本键而签名方没签 ⇒ MAC 不匹配 ⇒ 既有
|
|
32
|
+
* `401 approval_mac_invalid`,不铸第二种拒绝形。
|
|
20
33
|
* 2. 缺席 ⇒ `null`;**`""`(空串)是与 `null` 不同的被签值**(否则"把理由抹成空"不改变 MAC)。
|
|
21
34
|
* 3. 字符串转义 = RFC 8259 最小集:`"`→`\"`、`\`→`\\`、U+0000–U+001F → `\n`/`\t`/`\uXXXX`。
|
|
22
35
|
* 4. **非 ASCII 原样输出 UTF-8,不转 `\uXXXX`**。⚠️ Python 的 `json.dumps` 缺省 `ensure_ascii=True` 会踩
|
|
@@ -41,6 +54,20 @@ export interface ApprovalEnvelope {
|
|
|
41
54
|
boundInputHash?: string | null;
|
|
42
55
|
decision: string;
|
|
43
56
|
reason?: string | null;
|
|
57
|
+
/**
|
|
58
|
+
* 🔴 **S-433 合并复审 F1 [high] —— 「凡改变续跑能做什么的决裁字段都在签名信封里」**(7.86.0,验真后修)。
|
|
59
|
+
*
|
|
60
|
+
* `plan_review` 的 `permissionModeAfter` 选的是**批准之后这条 run 有没有手**(`acceptEdits` = 工作目录内
|
|
61
|
+
* 的写不再逐次征询)。它铸下来时**没有**进签名载荷 ⇒ 一个能改写线上字节的中间人(反代 / 被接管的
|
|
62
|
+
* 客户端 —— 正是 HMAC 的风险模型)可以把操作员签的那次 `default` 批准原地改成 `acceptEdits`:动作没变、
|
|
63
|
+
* MAC 照过,**权限变了**。这与 `reason` 入签是同一条判据的更贵一格(那条改的是审计记录,这条改的是手)。
|
|
64
|
+
*
|
|
65
|
+
* 🔴 **尾位、按在场序列化**(编码规则见 {@link approvalHmacMessage}):键缺席 ⇒ 消息字节与 7.85.0 **逐字
|
|
66
|
+
* 节相同**,所以不签这一位的既有签名器(cli / BFF / deploy 夹具)对**不带本键的体**照常验过 —— 这一位
|
|
67
|
+
* 不是第二次 `reason` 式的 BREAKING。带了键而签名方没签 ⇒ MAC 不匹配 ⇒ 落既有的
|
|
68
|
+
* `401 approval_mac_invalid`,**不铸第二种拒绝形**(不加 400/401 串,不加「直连门直接拒收本键」的特判)。
|
|
69
|
+
*/
|
|
70
|
+
permissionModeAfter?: string | null;
|
|
44
71
|
}
|
|
45
72
|
export declare function approvalHmacMessage(env: ApprovalEnvelope): string;
|
|
46
73
|
/** 签名载荷里 `reason` 的字符上限。**拒**而不是截 —— 截断在这条路径上**结构上不可行**:服务端截过的字节
|
package/dist/approval-hmac.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
2
|
export function approvalHmacMessage(env) {
|
|
3
|
-
|
|
3
|
+
const head = [env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null, env.decision, env.reason ?? null];
|
|
4
|
+
return JSON.stringify(env.permissionModeAfter === undefined ? head : [...head, env.permissionModeAfter]);
|
|
4
5
|
}
|
|
5
6
|
export const MAX_APPROVAL_REASON_CHARS = 4_096;
|
|
6
7
|
export function verifyApprovalHmac(env, mac, kid, keys) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defaultTaskRegistry } from "@sema-agent/core";
|
|
1
|
+
import { autoRunOnRecommendationEffective, defaultTaskRegistry } from "@sema-agent/core";
|
|
2
2
|
import { posIntEnv } from "../session-watch.js";
|
|
3
3
|
import { brainSummary } from "../brain.js";
|
|
4
4
|
import { resolveBindHost } from "../config.js";
|
|
@@ -82,6 +82,7 @@ export async function runStage10(ctx) {
|
|
|
82
82
|
memoryCompliance: memoryComplianceFaces,
|
|
83
83
|
memoryOriginFace: memoryOriginFaces,
|
|
84
84
|
sessionMemoryStatus: sessionMemoryStatusFace,
|
|
85
|
+
memoryAutoConsolidationArmed: autoRunOnRecommendationEffective(runnerDeps),
|
|
85
86
|
memoryConsolidation: memoryConsolidationFaces,
|
|
86
87
|
memoryOptOutGrant: memoryOptOutGrantStore,
|
|
87
88
|
orgMemoryDirectory: orgMemoryAdmission.directory,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RemoteExecProvider } from "../execution-lane-caps.js";
|
|
2
|
+
export interface ExecutionLaneCapability {
|
|
3
|
+
/** 本部署的执行车道词(闭集 = {@link RemoteExecProvider};`REMOTE_EXEC` 未设 ⇒ `"host"`,见头注)。 */
|
|
4
|
+
readonly provider: RemoteExecProvider;
|
|
5
|
+
/** 工具跑的那台机器就是本进程这台吗(= server 盘上的绝对路径对模型的工具有没有意义)。
|
|
6
|
+
* 属主 = `task-cwd.ts` 的 {@link toolsRunOnThisHost},与 `skills[].baseDir` 的铸点同一只函数。 */
|
|
7
|
+
readonly toolsOnThisHost: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 部署席 → 消费端窄投影。形照 `projectWebSearchCapability` / `projectSqlEngineCapability`:
|
|
11
|
+
* **只 import 型**,不碰 config.ts,于是本模块是叶。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 **唯一**写缺席折词的地方。`"host"` 这个字面量不入 `api-notice-text-freeze`:它是
|
|
14
|
+
* {@link RemoteExecProvider} 的成员,配置联合里那条臂改名 ⇒ 本行当场 tsc 红(S-382 对三个 provider 词
|
|
15
|
+
* 免入表的同一条理由 —— 会静默漂的只有**本仓自铸**的词,这里一个都没有)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function projectExecutionLaneCapability(config: {
|
|
18
|
+
readonly remoteExec?: {
|
|
19
|
+
readonly provider: RemoteExecProvider;
|
|
20
|
+
} | undefined;
|
|
21
|
+
}): ExecutionLaneCapability;
|
|
22
|
+
//# sourceMappingURL=execution-lane.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { toolsRunOnThisHost } from "../task-cwd.js";
|
|
2
|
+
export function projectExecutionLaneCapability(config) {
|
|
3
|
+
return {
|
|
4
|
+
provider: config.remoteExec?.provider ?? "host",
|
|
5
|
+
toolsOnThisHost: toolsRunOnThisHost(config),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=execution-lane.js.map
|
|
@@ -38,7 +38,10 @@ export interface MemoryFaceFacts {
|
|
|
38
38
|
/** 这一腿挂不挂记忆面 —— 由唯一属主 `memory-scope.ts` `memoryFaceMounted(spec.memory)` 算出(与 core `prepareMemory`
|
|
39
39
|
* 同判:`normalizeMemorySpec(...)?.enabled === true`)。`false` = 引擎未接 / 多租户 dark / remote lane fail-closed /
|
|
40
40
|
* S-405 `memory:"off"`(键在场但 `enabled:false`)—— 殊途同归:core 不会 prepare 记忆,也就不会挂任何披露。
|
|
41
|
-
* 🔴 不再收 `TaskSpec.memory` 本体:按「键在不在场」判是 7.84.0 前的病(合并复审 车HF F1)。
|
|
41
|
+
* 🔴 不再收 `TaskSpec.memory` 本体:按「键在不在场」判是 7.84.0 前的病(合并复审 车HF F1)。
|
|
42
|
+
* 🔴 属主会**抛**(值闭包门没过的手搓 spec —— 单数 `scope` / 非 `"off"` 的 `capture`),理由与两个属主
|
|
43
|
+
* 的坐标逐字写在 `memory-scope.ts` 的 `memoryFaceMounted` 头注里;本判别式不接这条臂(接了就是在
|
|
44
|
+
* 披露轴上静默 fail-open)。 */
|
|
42
45
|
memoryFaceMounted: boolean;
|
|
43
46
|
/** 部署声明 `MEMORY_PERSISTENCE_CAPABLE`(三态)。`true` = operator 自证有推断看不见的持久通道
|
|
44
47
|
* (自定义 writer / MCP 记忆服务)⇒ 披露会变成假话,撤回;`false` = 强制披露;缺席 ⇒ 走推断。 */
|
package/dist/config-catalog.js
CHANGED
|
@@ -2,6 +2,7 @@ import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } fr
|
|
|
2
2
|
import { THINKING_FORMATS } from "./model-compat.js";
|
|
3
3
|
import { RESILIENCE_DEFAULTS } from "./config-types.js";
|
|
4
4
|
import { webSearchConfigFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
|
|
5
|
+
import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
|
|
5
6
|
import { CENTER_READ_FACE_LIVE_KEYS } from "./config-center/read-face.js";
|
|
6
7
|
export const CATALOG_VALUE_TYPES = ["boolean", "number", "string", "enum", "csv", "json", "path", "url", "secret", "opaque"];
|
|
7
8
|
export const CATALOG_TIMINGS = ["hot", "restart", "deploy", "inert"];
|
|
@@ -342,12 +343,12 @@ export const CONFIG_CATALOG = [
|
|
|
342
343
|
r("QUESTION_TTL_MS", "approval", "number", "AskUserQuestion 无人应答释放窗(ms)", { derivedDefaultNote: "缺席 ⇒ 出厂窗 300000" }),
|
|
343
344
|
r("RATE_LIMIT_RPM", "limitsHttp", "number", "计费提交每分钟限流(0=关)", { staticDefault: "0", danger: BILL, configKey: "rateLimitPerMin", center: cLimits("rateLimitPerMin") }),
|
|
344
345
|
r("READ_DENY_BUILTIN_EXCLUDE", "orchestration", "csv", "按行名删内建 deny 条目(档位之后生效;绝不削任务显式 deny)", { danger: SEC, configKey: "readDenyBuiltinExclude", center: cRead("denyBuiltinExclude", "readDenyBuiltinExclude") }),
|
|
345
|
-
r("READ_DENY_BUILTIN_TIERS", "orchestration", "csv", "内建 deny 表档位选择(闭集五档 credentials/shell-history/browser/wallet/agent-config;none=显式空姿态)", { danger: SEC, configKey: "readDenyBuiltinTiers", center: cRead("denyBuiltinTiers", "readDenyBuiltinTiers") }),
|
|
346
|
+
r("READ_DENY_BUILTIN_TIERS", "orchestration", "csv", "内建 deny 表档位选择(闭集五档 credentials/shell-history/browser/wallet/agent-config;none=显式空姿态)", { danger: SEC, derivedDefaultNote: "🔴 缺席 ⇒ **OFF**(core 7.22.0 / C-R60 起内建表出厂全关:根内凭据名可读、整个递归读族零卡)。要 7.21.x 姿态就显式写 credentials,browser,wallet,agent-config;空值拒启(手滑与显式意图不许同形)", configKey: "readDenyBuiltinTiers", center: cRead("denyBuiltinTiers", "readDenyBuiltinTiers") }),
|
|
346
347
|
r("READ_DENY_PATTERNS", "orchestration", "csv", "READ 面额外 deny pattern 表", { danger: SEC, configKey: "readDenyPatterns", center: cRead("denyPatterns", "readDenyPatterns") }),
|
|
347
348
|
r("READ_FACE", "orchestration", "enum", "READ 容纳面档位(env 逐键恒赢 center —— 部署主权表达)", { enumValues: ["open", "roots"], danger: SEC, derivedDefaultNote: "缺席 ⇒ 按 posture 派生(S-167):单用户 turnkey(REQUIRE_PRINCIPAL 未设)铸 open;多租户不铸键、core 默认 roots。生效来源见 GET /v1/diagnostics/wiring 的 readFace.source 四词", configKey: "readFace", center: cRead("face", "readFace") }),
|
|
348
349
|
r("REAP_INTERVAL_SEC", "limitsHttp", "number", "reaper 周期(秒)", { staticDefault: "60", configKey: "reapIntervalSec" }),
|
|
349
350
|
r("REAP_RUN_STALE_SEC", "limitsHttp", "number", "run 活性判陈旧阈(秒)", { staticDefault: "120", configKey: "runStaleSec", center: cLimits("runStaleSec") }),
|
|
350
|
-
r("REMOTE_EXEC", "orchestration", "enum", "执行车道选择(点名而配不齐/认不出 ⇒ 拒启,绝不静默降级到 host/stub)", { enumValues:
|
|
351
|
+
r("REMOTE_EXEC", "orchestration", "enum", "执行车道选择(点名而配不齐/认不出 ⇒ 拒启,绝不静默降级到 host/stub)", { enumValues: REMOTE_EXEC_PROVIDER_WORDS, danger: SEC, derivedDefaultNote: "缺席:CONFIG_PROVIDER=local ⇒ host;其余 ⇒ 进程内 stub", configKey: "remoteExec", resolve: (c) => c.remoteExec?.provider ?? null }),
|
|
351
352
|
r("REMOTE_EXEC_HOME_DIR", "orchestration", "path", "执行环境的 home 目录(`~/` 形权限规则的基;core ExecutionEnv.homeDir 座)", { danger: SEC, derivedDefaultNote: "缺席:host/in-process ⇒ os.homedir();e2b ⇒ /home/user;k8s/local-docker/ssh/adb/device ⇒ 无(`~/` 规则 422 响亮拒)", configKey: "remoteExecHomeDir", resolve: (c) => c.remoteExecHomeDir ?? null }),
|
|
352
353
|
r("REQUIRE_PRINCIPAL", "auth", "boolean", "多租户开关(posture 判别位:true ⇒ CC 能力族默认收紧)", { staticDefault: "false", danger: SEC, configKey: "requirePrincipal" }),
|
|
353
354
|
r("RESOURCE_SUSPEND", "approval", "boolean", "durable 路径 opt-in core resourceSuspend(预算/preempt ⇒ 可恢复挂起)", { staticDefault: "false", configKey: "resourceSuspend" }),
|
package/dist/config-types.d.ts
CHANGED
|
@@ -1740,7 +1740,11 @@ export interface ServiceConfigFlat {
|
|
|
1740
1740
|
* 行名的是**文件**不是配置**目录**(`.sema`/`.claude` 底下还住着 agents/skills/hooks 与记忆库 —— 模型
|
|
1741
1741
|
* 当作日常材料读写的东西);同族工具的 token **存储**(`.credentials.json`/`.codex/auth.json` …)归
|
|
1742
1742
|
* `credentials` 档,不归本档 ⇒ 为排查 agent 配置而关掉本档的部署,凭证行照样拦着。
|
|
1743
|
-
*
|
|
1743
|
+
* 🔴 **缺席 = 不铸 ⇒ OFF**(core 7.22.0 / [ref] 翻面;7.21.x 及以前是「除 shell-history 外全开」)。
|
|
1744
|
+
* 未配置的部署从内建表里**什么都不拒**,而且 shell 读边界的 deny 判官席 present-iff 编译集有条目 ⇒
|
|
1745
|
+
* 递归读降级臂也随之不接线(`grep -r` 一族零卡)。要老姿态**必须显式写下**
|
|
1746
|
+
* `credentials,browser,wallet,agent-config`。本仓**不复制上游缺省**:这里只记「不铸」,解析归引擎。
|
|
1747
|
+
* **仅 deployment 席**:TaskSpec 上没有同名键,
|
|
1744
1748
|
* 任务/受治脚本无法据此放宽自己的部署(core 侧的设计,与本仓「部署级旋钮无条件施加」同向)。 */
|
|
1745
1749
|
readDenyBuiltinTiers?: readonly string[];
|
|
1746
1750
|
/** [ref] 件 G5:按**行名**(内建表的 canonical pattern)删内建条目,在档位选择**之后**生效。
|
|
@@ -89,6 +89,19 @@ export declare function executionLaneCaps(lane: ExecutionLane): ExecutionLaneCap
|
|
|
89
89
|
export declare function isExecutionLane(word: string): word is ExecutionLane;
|
|
90
90
|
/** 车道普查全集(顺序 = 词表声明序;对账测试按它逐 lane 跑真判别式)。 */
|
|
91
91
|
export declare const ALL_EXECUTION_LANES: readonly ExecutionLane[];
|
|
92
|
+
/**
|
|
93
|
+
* `REMOTE_EXEC` **真能写下去的**那几个词(= {@link ALL_EXECUTION_LANES} 去掉 `in-process`:后者是
|
|
94
|
+
* 「没有 remoteExec」这个状态的名字,env 里写它没有意义)。
|
|
95
|
+
*
|
|
96
|
+
* 🔴 S-426 以点改面:此前 `config-catalog.ts` 的 `REMOTE_EXEC` 行把这张表**手抄**成一个
|
|
97
|
+
* `enumValues: ["e2b","k8s",…]` 字面量(字段静态型是 `readonly string[]`,与 {@link RemoteExecProvider}
|
|
98
|
+
* 零类型联系)。病形与 S-382 逮到的 `WEB_SEARCH_PROVIDER` 逐字同族:给
|
|
99
|
+
* `ServiceConfigFlat["remoteExec"]` 加一条 provider 臂(config-types.ts 里写着的下一条就是
|
|
100
|
+
* `remote-docker`),本文件的穷举 switch 与词表**会**编译红,而目录行不会 —— 于是运维在
|
|
101
|
+
* `GET /v1/config/catalog` 上读到的可写词集静默少一个,新车道成了「配了但目录说不认识」。
|
|
102
|
+
* 收成一只派生数组之后,目录行**就用它本人**(身份相等有钉),全仓只剩这一张表。
|
|
103
|
+
*/
|
|
104
|
+
export declare const REMOTE_EXEC_PROVIDER_WORDS: readonly RemoteExecProvider[];
|
|
92
105
|
/**
|
|
93
106
|
* `config.remoteExec?.provider` → 车道词。
|
|
94
107
|
*
|
|
@@ -156,6 +156,7 @@ export function isExecutionLane(word) {
|
|
|
156
156
|
return Object.hasOwn(EXECUTION_LANE_WORDS, word);
|
|
157
157
|
}
|
|
158
158
|
export const ALL_EXECUTION_LANES = Object.freeze(Object.keys(EXECUTION_LANE_WORDS).filter(isExecutionLane));
|
|
159
|
+
export const REMOTE_EXEC_PROVIDER_WORDS = Object.freeze(ALL_EXECUTION_LANES.filter((lane) => lane !== "in-process"));
|
|
159
160
|
export function executionLaneOf(provider) {
|
|
160
161
|
if (provider === undefined)
|
|
161
162
|
return "in-process";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isTerminalWorkflowStatus } from "@sema-agent/core";
|
|
2
2
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
3
|
-
import { isParkedRunStatus, isTerminalRunStatus } from "../plugins/store-contracts.js";
|
|
3
|
+
import { isParkedRunStatus, isTerminalRunStatus, runProvenElsewhere } from "../plugins/store-contracts.js";
|
|
4
4
|
import { buildFleetWorkflowRow, fleetRunResiduals, runStatusToFleet } from "./fleet-bus.js";
|
|
5
5
|
import { isTerminalWorkflowRowStatus } from "./fleet-terminal-window.js";
|
|
6
6
|
export const FLEET_RECONCILE_INTERVAL_MS = 60_000;
|
|
@@ -169,7 +169,7 @@ export function createFleetReconciler(deps) {
|
|
|
169
169
|
tally.skippedParked++;
|
|
170
170
|
continue;
|
|
171
171
|
}
|
|
172
|
-
orphanExit(row, rec
|
|
172
|
+
orphanExit(row, runProvenElsewhere(rec, deps.instanceId) ? "durable_running_other_replica" : "durable_running_no_local_leg");
|
|
173
173
|
}
|
|
174
174
|
for (const row of snap.tasks) {
|
|
175
175
|
if (row.parentId === undefined || present.has(row.parentId))
|
|
@@ -16,6 +16,7 @@ import { type QuestionAnswer, type CheckpointToken } from "@sema-agent/core";
|
|
|
16
16
|
import type { DecideBinding } from "./wire-types.js";
|
|
17
17
|
import type { CheckpointStoreFull } from "../plugins/store-backend.js";
|
|
18
18
|
import { type HostDecision } from "../host-decision.js";
|
|
19
|
+
import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
|
|
19
20
|
import type { RouteCtxBase, DriveResumeArgs } from "./route-ctx.js";
|
|
20
21
|
import { type TrustedRunInternals } from "./durable-run-leg.js";
|
|
21
22
|
import type { RunRegistry, RouteHelpers, RouteLegs } from "./route-ctx.js";
|
|
@@ -72,7 +73,7 @@ export declare function createResumeLegs(ctx: {
|
|
|
72
73
|
}, acceptEarly?: boolean, decider?: {
|
|
73
74
|
principal?: string;
|
|
74
75
|
explicitOperator: boolean;
|
|
75
|
-
}) => Promise<{
|
|
76
|
+
}, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
|
|
76
77
|
status: number;
|
|
77
78
|
body: object;
|
|
78
79
|
}>;
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -24,6 +24,7 @@ import { reasoningDeltaBytes, reasoningSegmentEventId } from "../trace/model-seg
|
|
|
24
24
|
import { errorText } from "../observability/err-text.js";
|
|
25
25
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
26
26
|
import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
27
|
+
import { storedBodyPermissionMode } from "../task-settings.js";
|
|
27
28
|
import { acceptedResumingReceipt } from "./route-ctx.js";
|
|
28
29
|
import { mintAndDriveDurableRun } from "./durable-run-leg.js";
|
|
29
30
|
import { createSessionsLocal } from "./routes/sessions.js";
|
|
@@ -320,7 +321,7 @@ export function createResumeLegs(ctx) {
|
|
|
320
321
|
hostDecision,
|
|
321
322
|
};
|
|
322
323
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
323
|
-
const driven = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}), ...(ownerLeaseAdmittedHere ? { ownerLeaseAdmitted: true } : {}) });
|
|
324
|
+
const driven = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}), ...(ownerLeaseAdmittedHere ? { ownerLeaseAdmitted: true } : {}) });
|
|
324
325
|
noteLateSettleIfAccepted(driven);
|
|
325
326
|
return driven;
|
|
326
327
|
}
|
|
@@ -353,7 +354,11 @@ export function createResumeLegs(ctx) {
|
|
|
353
354
|
}
|
|
354
355
|
async function driveResumeLeg(args, onAccepted) {
|
|
355
356
|
const resumeLegStartedAt = Date.now();
|
|
356
|
-
const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds } = args;
|
|
357
|
+
const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx } = args;
|
|
358
|
+
const commitResumeCtx = () => deps.checkpointStore.putCtx(resumeCtx.sessionId, resumeCtx.ctx).catch((e) => {
|
|
359
|
+
deps.metrics?.inc("resume_ctx_not_persisted_total");
|
|
360
|
+
deps.logger?.warn?.("resume_ctx_not_persisted", { sessionId: resumeCtx.sessionId, gate: outcome.gate, error: errorText(e), note: "this continuation runs with the body it was given; the durable rebuild input still holds the PREVIOUS body, so the NEXT continuation of this session rebuilds from that one (never wider than this leg's)" });
|
|
361
|
+
});
|
|
357
362
|
const legKey = createHash("sha256").update(String(token)).digest("hex");
|
|
358
363
|
const streamApprovalOn = resolveStreamApprovalGate({
|
|
359
364
|
toolApprovalEnabled: Boolean(deps.toolApproval),
|
|
@@ -539,6 +544,7 @@ export function createResumeLegs(ctx) {
|
|
|
539
544
|
};
|
|
540
545
|
resumeLegLive = true;
|
|
541
546
|
const vr = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
|
|
547
|
+
await commitResumeCtx();
|
|
542
548
|
let safe = stripResumeToken(vr);
|
|
543
549
|
const vrNested = nestedPauseOf(vr.terminal);
|
|
544
550
|
const reParkStatus = nestedParkStatusOf(vr);
|
|
@@ -663,6 +669,7 @@ export function createResumeLegs(ctx) {
|
|
|
663
669
|
}
|
|
664
670
|
: {}),
|
|
665
671
|
});
|
|
672
|
+
await commitResumeCtx();
|
|
666
673
|
resumeStreamRef = stream;
|
|
667
674
|
if (taskId)
|
|
668
675
|
steerableRuns.set(taskId, stream);
|
|
@@ -1026,7 +1033,7 @@ export function createResumeLegs(ctx) {
|
|
|
1026
1033
|
const { spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth);
|
|
1027
1034
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1028
1035
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1029
|
-
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1036
|
+
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1030
1037
|
}
|
|
1031
1038
|
async function startFreshHostTurn(sessionId, objective, principal, req, trustedInternals, runId) {
|
|
1032
1039
|
const cs = deps.checkpointStore;
|
|
@@ -1139,6 +1146,7 @@ export function createResumeLegs(ctx) {
|
|
|
1139
1146
|
resumeObjective,
|
|
1140
1147
|
outcome: { gate: "wake", ...(message !== undefined ? { message: { text: message, trusted } } : {}) },
|
|
1141
1148
|
verifyRounds: undefined,
|
|
1149
|
+
resumeCtx: { sessionId, ctx },
|
|
1142
1150
|
mintFreshRun: { source: "wake" },
|
|
1143
1151
|
...(engineNotices.length > 0 ? { engineNotices } : {}),
|
|
1144
1152
|
...(acceptEarly === true ? { acceptEarly: true } : {}),
|
|
@@ -1147,7 +1155,7 @@ export function createResumeLegs(ctx) {
|
|
|
1147
1155
|
...(ownerLeaseAdmittedFor !== undefined && auth.principal === ownerLeaseAdmittedFor ? { ownerLeaseAdmitted: true } : {}),
|
|
1148
1156
|
});
|
|
1149
1157
|
}
|
|
1150
|
-
async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
|
|
1158
|
+
async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider, permissionModeAfter) {
|
|
1151
1159
|
const cs = deps.checkpointStore;
|
|
1152
1160
|
const log404 = (r) => deps.logger?.info?.("decide_404", {
|
|
1153
1161
|
reason: r,
|
|
@@ -1208,7 +1216,13 @@ export function createResumeLegs(ctx) {
|
|
|
1208
1216
|
if (!ctx)
|
|
1209
1217
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1210
1218
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1211
|
-
const
|
|
1219
|
+
const effMode = storedBodyPermissionMode(ctx.body);
|
|
1220
|
+
if (permissionModeAfter !== undefined && effMode !== "plan") {
|
|
1221
|
+
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)");
|
|
1222
|
+
}
|
|
1223
|
+
const approvedMode = decision === "approve" && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
|
|
1224
|
+
const resumeBody = approvedMode === undefined ? ctx.body : { ...ctx.body, permissionMode: approvedMode };
|
|
1225
|
+
const { spec, engineNotices } = await resolveResumeSpec(resumeBody, req, auth);
|
|
1212
1226
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1213
1227
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1214
1228
|
const outcome = {
|
|
@@ -1217,7 +1231,8 @@ export function createResumeLegs(ctx) {
|
|
|
1217
1231
|
...(editedPlan !== undefined ? { editedPlan } : {}),
|
|
1218
1232
|
...(reason ? { reason } : {}),
|
|
1219
1233
|
};
|
|
1220
|
-
|
|
1234
|
+
const out = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx: { sessionId, ctx: { ...ctx, body: resumeBody } }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1235
|
+
return out;
|
|
1221
1236
|
}
|
|
1222
1237
|
const routeCtxBase = {
|
|
1223
1238
|
deps,
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { IdempotencyCache } from "./idempotency.js";
|
|
|
23
23
|
import type { ImagesLocal } from "./routes/images.js";
|
|
24
24
|
import type { SessionsLocal } from "./routes/sessions.js";
|
|
25
25
|
import type { SessionSyncLocal } from "./routes/session-sync.js";
|
|
26
|
+
import type { PlanApprovePermissionModeAfter } from "../task-settings.js";
|
|
26
27
|
import type { VerifyRoundsSpec } from "./verify-rounds.js";
|
|
27
28
|
/** 跨域可变状态 = lens1 §C2 实测的那 8 条「运行期登记簿」,一个对象。 */
|
|
28
29
|
export interface RunRegistry {
|
|
@@ -127,9 +128,55 @@ export interface PreparedTaskSubmission {
|
|
|
127
128
|
* 经 `pending` 一起投(投递单点仍是那一只注册助手,不新开第二条机制)。缺席 = 本次装配没铸下任何通告。 */
|
|
128
129
|
engineNotices?: readonly EngineNotice[];
|
|
129
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* 一条会话的**续跑重建输入**(`checkpoint_ctx` 的行体)—— 提交腿写一次,续跑腿读它重建 spec。
|
|
133
|
+
*
|
|
134
|
+
* S-433 F2 起命名化(此前是五处结构等价的内联字面 `{ body: TaskRequestBody; memoryScope?: string }`):
|
|
135
|
+
* 它同时是 {@link DriveResumeArgs.resumeCtx} 的形,而那一位是**写回**口 —— 读形与写形是同一件东西这句话
|
|
136
|
+
* 得由类型说,不能靠五处手抄碰巧一致。
|
|
137
|
+
*/
|
|
138
|
+
export interface CheckpointCtx {
|
|
139
|
+
body: TaskRequestBody;
|
|
140
|
+
/** 提交时这条会话绑定的记忆域(重建 `RequestAuth` 用);缺席 = 没有记忆面。 */
|
|
141
|
+
memoryScope?: string;
|
|
142
|
+
}
|
|
130
143
|
/** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
|
|
131
144
|
export interface DriveResumeArgs {
|
|
132
145
|
token: CheckpointToken;
|
|
146
|
+
/**
|
|
147
|
+
* 🔴 **S-433 合并复审 F2 [high] —— 「本次续跑真正用的那个体」,由本腿在赢下 CAS 之后写回耐久层**
|
|
148
|
+
* (验真后修;必填 —— `driveResumeIntoRunLog` 今天恰有**四个**调用点:legacy `/decide`(`resumeCheckpoint`)、
|
|
149
|
+
* `resumePreempted`、`resumeWake`、`resumePlanReview`,四条一视同仁)。
|
|
150
|
+
*
|
|
151
|
+
* 病(修前):`resumePlanReview` 在 `driveResumeIntoRunLog` **受理(2xx)之后**才 `putCtx(sessionId,
|
|
152
|
+
* {...ctx, body})`,而 `ctx` 是请求**开头** `getCtx` 读到的那一份,两个店的 `putCtx` 又都是按
|
|
153
|
+
* `session_id` **无条件 upsert**(`checkpoint-store-sql.ts` / `local-checkpoint-store.ts`)。受理点之后
|
|
154
|
+
* 本腿已经可以跑完并 `setTerminal` 释放 `task_active` —— 别的副本随即 `createRun` + 写一份**新的** ctx,
|
|
155
|
+
* 然后这条迟到的写把它整只盖掉。那是一条**跨副本的丢写**,而被盖掉的恰恰是权限模式。
|
|
156
|
+
*
|
|
157
|
+
* 修法(单一写者,栅栏 = CAS 赢点,不引版本号/不加特判):写点移进 `driveResumeLeg`,落在
|
|
158
|
+
* `resumeStream` / `resumeWithVerification` **解析之后**(core 的 pre-CAS 守卫 + 原子 CAS 已经赢了)、
|
|
159
|
+
* 本腿驱终局之前。此时 park 行仍攥着 `task_active`,在本腿 `setTerminal` 之前**谁也建不了新 run** ⇒
|
|
160
|
+
* 写与释放天然串行;输掉 CAS 的决裁在 `resumeStream` 处就 reject,**永远到不了写点**(「先写回则输家
|
|
161
|
+
* 放宽」这个顾虑同时成立)。
|
|
162
|
+
*
|
|
163
|
+
* 不变量(规则变小,四条驱动腿一视同仁):**`ctx.body` 恒等于最近一次赢得 CAS 的续跑实际用的体**。
|
|
164
|
+
* 体没被改写的腿传**原 ctx** —— 同内容 upsert 幂等,不需要「只有 plan_review 才写」这条特判。
|
|
165
|
+
*
|
|
166
|
+
* 🔴 `sessionId` 自带一位:写回的键**恒是读进来的那一个**(各腿的 `sessionId` 形参,**不是**
|
|
167
|
+
* `cp.sessionId`)—— 读写不同键会写出一条谁也读不回来的行。
|
|
168
|
+
*
|
|
169
|
+
* 🔴 写失败**响亮但不致命**(codex r1 两条 [high] 验真后定):core 的 `resumeStream` 是 **EAGER** 的 ——
|
|
170
|
+
* 写点这一拍 run 已经在执行,裸抛会把一条活腿判成 failed + 释放 claim 而执行还在继续。⇒ 一个臂、
|
|
171
|
+
* 四条腿同守:计数 `resume_ctx_not_persisted_total` + 一行 warn,本次续跑照常。方向论证(不是 [ref] 的
|
|
172
|
+
* 静默放宽):写失败 ⇒ 耐久体保持**上一次**的值,而这四条腿里只有 `resumePlanReview` 改写这个体、
|
|
173
|
+
* 且只会**放宽** ⇒ 落盘的恒不比本次续跑更宽。旧的 `plan_approve_mode_not_persisted`(plan 专用)随本批删除,
|
|
174
|
+
* 由上面这只家族级的取代。全文见 `resume-legs.ts` 的 `commitResumeCtx` 顶注。
|
|
175
|
+
*/
|
|
176
|
+
resumeCtx: {
|
|
177
|
+
sessionId: string;
|
|
178
|
+
ctx: CheckpointCtx;
|
|
179
|
+
};
|
|
133
180
|
/** [ref]:续跑腿的执行 Runner。resume 会用同一条 resolveSpec 重解析场景,故与首跑选同一只 —— 否则
|
|
134
181
|
* 一次续跑就把手带 band 重新 mount 回来(收窄只在首跑成立 = 没成立)。 */
|
|
135
182
|
runner: Runner;
|
|
@@ -354,7 +401,10 @@ export interface RouteLegs {
|
|
|
354
401
|
decider?: {
|
|
355
402
|
principal?: string;
|
|
356
403
|
explicitOperator: boolean;
|
|
357
|
-
}
|
|
404
|
+
},
|
|
405
|
+
/** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER`;只在 `approve`
|
|
406
|
+
* 上有意义,缺席 ⇒ `"default"`)。语义与判据的唯一属主 = `resume-legs.ts` 的同名形参顶注。 */
|
|
407
|
+
permissionModeAfter?: PlanApprovePermissionModeAfter): Promise<{
|
|
358
408
|
status: number;
|
|
359
409
|
body: object;
|
|
360
410
|
}>;
|
package/dist/http/route-table.js
CHANGED
|
@@ -28,6 +28,7 @@ import { handleNotifyWake, NOTIFY_WAKE_ROUTES } from "./routes/notify-wake.js";
|
|
|
28
28
|
import { handleApprovalsAssistant, APPROVALS_ASSISTANT_ROUTES } from "./routes/approvals-assistant.js";
|
|
29
29
|
import { handleRuns, handleRunVerbs, RUNS_ROUTES, RUN_VERBS_ROUTES } from "./routes/runs.js";
|
|
30
30
|
import { handleRunMemoryCapture, RUN_MEMORY_CAPTURE_ROUTES } from "./routes/run-memory-capture.js";
|
|
31
|
+
import { handleMcpReconnect, MCP_RECONNECT_ROUTES } from "./routes/mcp-reconnect.js";
|
|
31
32
|
import { handleSideQuery, SIDE_QUERY_ROUTES } from "./routes/side-query.js";
|
|
32
33
|
import { handleTasks, TASKS_ROUTES } from "./routes/tasks.js";
|
|
33
34
|
import { handleLeader, LEADER_ROUTES } from "./routes/leader.js";
|
|
@@ -48,6 +49,7 @@ export const ROUTES = [
|
|
|
48
49
|
row({ handler: handleWorkflowAgentSteer, decls: WORKFLOW_AGENT_STEER_ROUTES, position: "post-credential", label: "workflow-agent-steer" }),
|
|
49
50
|
row({ handler: handleRunVerbs, decls: RUN_VERBS_ROUTES, position: "post-credential", label: "run-verbs" }),
|
|
50
51
|
row({ handler: handleRunMemoryCapture, decls: RUN_MEMORY_CAPTURE_ROUTES, position: "post-credential", label: "run-memory-capture" }),
|
|
52
|
+
row({ handler: handleMcpReconnect, decls: MCP_RECONNECT_ROUTES, position: "post-credential", label: "mcp-reconnect" }),
|
|
51
53
|
row({ handler: handleLeader, decls: LEADER_ROUTES, position: "post-credential", label: "leader", enabled: (deps) => Boolean(deps.leaderEndpoint) }),
|
|
52
54
|
row({ handler: handleImages, decls: IMAGES_ROUTES, position: "post-credential", label: "images" }),
|
|
53
55
|
row({ handler: handleApprovalsAssistant, decls: APPROVALS_ASSISTANT_ROUTES, position: "post-credential", label: "approvals-assistant", enabled: (deps) => Boolean(deps.checkpointStore) }),
|
|
@@ -16,6 +16,17 @@ import { type KeyClosureIssue } from "../../request-key-closure.js";
|
|
|
16
16
|
* 的两种兑现方式:**门只站在人**(或客户端)**现写的那份字节**上。
|
|
17
17
|
*/
|
|
18
18
|
export declare function decideBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
|
|
19
|
+
/**
|
|
20
|
+
* FRESH 决裁面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
|
|
21
|
+
* 全部受理 ⇒ `null`。判据边界与三个孪生门(`taskBodyKeyIssue` / `taskSettingsKeyIssue` /
|
|
22
|
+
* {@link decideBodyKeyIssue})逐字同一条:**只判键名**(值形校验归各自既有的门,本门零抢话);体本身
|
|
23
|
+
* 非对象 ⇒ 本门不判(留给「body must be { decision … }」那道根形门,免得同一个体被两句话抢答)。
|
|
24
|
+
*
|
|
25
|
+
* 🔴 **重放不额外过门**,也不需要 —— 逐字同 {@link decideBodyKeyIssue} 的同名段:本腿的反重放是
|
|
26
|
+
* 一次性 nonce(S-280),它守的是**同一份现写的字节**;而续跑重建读的是盘上的体、走 `resolveSpec` 的
|
|
27
|
+
* 宽容层 ⇒ 4xx 砖不到任何存量挂起的任务。**门只站在人(或客户端)现写的那份字节上**。
|
|
28
|
+
*/
|
|
29
|
+
export declare function planReviewBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
|
|
19
30
|
/**
|
|
20
31
|
* S-342 —— 「空串的理由 = 没有理由 = 这是一次**裸拒**」。这条语义只适用于**本仓自铸**的那一个域。
|
|
21
32
|
*
|