@sema-agent/server 7.32.0 → 7.33.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http/active-run-conflict.d.ts +50 -33
- package/dist/http/active-run-conflict.js +61 -3
- package/dist/http/routes/runs.d.ts +6 -0
- package/dist/http/routes/runs.js +20 -20
- package/dist/tool-approval.d.ts +7 -0
- package/dist/tool-approval.js +2 -0
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/package.json +3 -3
|
@@ -32,12 +32,58 @@ export declare const ACTIVE_RUN_CONFLICT_BASE_TEXT = "session already has an act
|
|
|
32
32
|
*
|
|
33
33
|
* 🔴 **additive + 只在为真时在场**:与活卡帧 `ToolApprovalFrame.governanceForced` **同一条纪律**——
|
|
34
34
|
* 缺席 ≠「这不是治理门」,而是「没有治理来源的证据」(判据够不着的形也落在缺席里,见装配处的注)。
|
|
35
|
+
*
|
|
36
|
+
* #285 追加的 `checkpointId` = **相关性**(这道门在别的面上叫什么名字),是出路/出身之外的第三个问题。
|
|
37
|
+
* 上文那条「askId 为什么不在这里」的记账(见 {@link governanceOriginOf} 顶注末段)说的是**同一个缺口的
|
|
38
|
+
* 另一半**:park 门此前在 wire 上**没有任何稳定身份**——唯一单射的键是 `token`,而 token 是秘密能力、
|
|
39
|
+
* 永不上 wire,于是消费端只能拿 `(sessionId, kind)` 这种非单射组合去猜「这次的门」和「上次的门」是不是
|
|
40
|
+
* 同一道。core 5.42.0 的 `Checkpoint.checkpointId`(`mintCheckpointId` 铸)正是
|
|
41
|
+
* 为此存在的**非秘密孪生身份**(core 顶注逐字:identification vs capability, separate axes;独立铸而
|
|
42
|
+
* **不是** token 摘要,理由是「最需要它的投影面恰恰是拿不到 token 的那些」——本载体就是其中之一)。
|
|
43
|
+
* ⚠️ 它**不是** askId 的替代:askId 是 `approval_ask` 行的身份,checkpointId 是 checkpoint 行的身份,
|
|
44
|
+
* 两者一对多且今天仍无反查读口(那条记账原样有效,别据本键认为它已销账)。
|
|
45
|
+
* ⚠️ **辖域如实**:本批只把这一位投到**本载体**(409 体 + 其 done 帧)。另外三条 park 读面今天都**没有**
|
|
46
|
+
* 这一格,而且它们的**代价不同**(亲核过读面真码,别按一句「都补上」估工):
|
|
47
|
+
* · `/v1/assistant/inbox` 读 `listByScope` ⇒ core 的 `CheckpointSummary`,**值已经在手**(core
|
|
48
|
+
* `summarizeCheckpoint` 单源投影里就带着),补它 = 那张显式白名单上加一行,零 SQL 面;
|
|
49
|
+
* · `GET /v1/approvals` 与 `/v1/approvals/stream` 读的是**本仓自己的** `PendingCheckpoint`
|
|
50
|
+
* (`checkpoint-store-sql.ts` 的 `listPending`,**按列 SELECT、刻意不拉 blob** —— 那是它避免读放大
|
|
51
|
+
* 的既定姿势),所以这一位**不在手**:要补得给 checkpoint 表反范式一列(与 `gate_kind` /
|
|
52
|
+
* `bound_input_hash` / `risk_descriptor` 同族)⇒ **SQL 面 = 双库集成门**,不是顺手一行。
|
|
53
|
+
* 补它们是另一批的事(各自过三问:谁需要跨面 join、join 不上谁受伤、补偿是什么),别在文档或消费码里
|
|
54
|
+
* 预支「两面能对上」。
|
|
55
|
+
*
|
|
56
|
+
* 🔴 **keyset 门辖域外,故在此成文**:`src/trace/core-keyset-guard.ts` 的七个面是
|
|
57
|
+
* TaskNotificationPayload / BackgroundChildEvent / RosterEntry / AskRequest+ToolApprovalFrame /
|
|
58
|
+
* TaskEvent / MailboxMessage / TraceEvent —— `Checkpoint` / `CheckpointSummary` **不在其中**(本载体
|
|
59
|
+
* 读的是结构形入参,不是 core 类型),所以 core 给 checkpoint 面加 additive 键时本文件**不会** tsc 红。
|
|
60
|
+
* 判据在此:本材料是**显式白名单**(逐键铸,不 spread 整行),新键要进 wire 必须有人在这里写一行。
|
|
35
61
|
*/
|
|
36
62
|
export interface PendingGateMaterial {
|
|
37
63
|
kind: string;
|
|
38
64
|
decidePath: string;
|
|
39
65
|
/** 见 {@link PendingGateMaterial} 顶注:`true` 才在场,恒不写 `false`。 */
|
|
40
66
|
governanceForced?: true;
|
|
67
|
+
/** #285(core 5.42.0):这道 park 门的**非秘密稳定身份**(`Checkpoint.checkpointId`)。逐字过境,
|
|
68
|
+
* server 绝不自铸、绝不派生(信任边界校验见 `safeCheckpointIdOf`)。
|
|
69
|
+
* 🔴 **缺席是三成因合流的一个形**(R3-[low] 验真后改真 —— 此前这里只写了第一条,那是失真):
|
|
70
|
+
* ① **legacy 行**:该键诞生前 park 的(core 明写「absent ⇒ legacy row, read compatibly, no backfill」);
|
|
71
|
+
* ② **服务端校验拒绝**:行上有值但没过 `safeCheckpointIdOf`(空串 / 超长 / 含 token) ⇒ **数据污染**,
|
|
72
|
+
* 而它在运维面上今天**与 ① 不可分辨**(见该函数顶注末段的残留登记);
|
|
73
|
+
* ③ **材料读取失败**:整只 `pendingGate` 都不在(那时本键连位置都没有)。
|
|
74
|
+
* ⇒ 读作「不知道这道门叫什么」,**不是**「这道门没有身份」,更**不是**「这一定是条老行」。
|
|
75
|
+
*
|
|
76
|
+
* 🔴 **它命名的是「本次 409 实际读到的那一行」,不是「activeTaskId 这条 run 的门」**(codex 对抗复审
|
|
77
|
+
* R1-[high] 验真后如实登记):同 session 可以并存两条 pending checkpoint(表上**没有** per-session
|
|
78
|
+
* pending 唯一约束),而 `findPendingTokenBySession` 是**无序 `LIMIT 1`** —— 这个条件是本仓自己成文的
|
|
79
|
+
* 已知事实(见 `src/http/server.ts` mintFreshRun 腿 409 那段的逐字论证:「两条 pending 并存时旧
|
|
80
|
+
* task_done park 完全可能被选中」)。⇒ 本材料的**每一格**(kind / decidePath / governanceForced /
|
|
81
|
+
* checkpointId)共享同一条归属不确定性;本键是**继承**它,不是引入它。
|
|
82
|
+
* **对消费端的直接后果**:这一位**不是**跨调用的稳定去重键 —— 同一情形连续两次 409 可能报出不同的
|
|
83
|
+
* id(选中了另一行),也可能 id 不变而 activeTaskId 已换。可安全用于:落日志/排障关联,以及「id 变了
|
|
84
|
+
* ⇒ 我看到的门确实不是刚才那一道」这个**单向**判断。根治要一条能按 run 绑定选行的 store 读口
|
|
85
|
+
* (SQL 面 = 双库门),已作为提货批未决项上报,不在本批。 */
|
|
86
|
+
checkpointId?: string;
|
|
41
87
|
}
|
|
42
88
|
export interface ActiveRunConflictBody {
|
|
43
89
|
error: string;
|
|
@@ -56,37 +102,6 @@ export declare function resumeEntryForGate(kind: string, ids: {
|
|
|
56
102
|
sessionId: string;
|
|
57
103
|
taskId: string;
|
|
58
104
|
}): string | null;
|
|
59
|
-
/**
|
|
60
|
-
* #220:推「这道门是运维治理层下的吗」。判据是**合取**,两半各管一件事:
|
|
61
|
-
*
|
|
62
|
-
* 行上的取证格 `gate.riskDescriptor.shellGateDoctrine === "always"`
|
|
63
|
-
* ∧ 本部署的治理层本来就要求 always(`governanceMandatesShellGateAlways`,单一判据属主在
|
|
64
|
-
* `runtime-governance.ts`,那里写着为什么单看行不够 —— SUP 路由姿态同样产 `"always"`)
|
|
65
|
-
*
|
|
66
|
-
* 第一半:core 在 mint 这道门时把「当时活着的 doctrine」如实写进 `riskDescriptor.shellGateDoctrine`
|
|
67
|
-
* (2026-08-05 取证裁定;`prepare-task` 只在**被 shell 门铸**的 ask 上写它),所以持久行自己带着证据 ——
|
|
68
|
-
* park 腿天然跨副本、跨重启,活卡腿那张 ALS 标记表在这里结构上够不着。
|
|
69
|
-
* 第二半:把「有效档 always」收窄到「治理层是真成因」,否则一条 SUP 路由出来的门会被谎报成治理强制。
|
|
70
|
-
*
|
|
71
|
-
* ⚠️ 残留(双向失真的窄在场向 + 根治路径)登记在 `governanceMandatesShellGateAlways` 的顶注,别在这里
|
|
72
|
-
* 复述;本位只承担「分诊提示」的分量,不参与任何门/CAS/resume 判定。
|
|
73
|
-
*
|
|
74
|
-
* 🔴 `"classify"` 档**刻意不标**:那一档由 core 的分类器逐调用裁决,一次 shell ask 可能出自分类器(治理)
|
|
75
|
-
* 也可能出自 `APPROVAL_REQUIRE` 这类别的门,行上无从分辨 —— 与活卡腿同一条「宁缺毋假」纪律。
|
|
76
|
-
*
|
|
77
|
-
* 🔴 **导出的理由 = 单一属主**([3684]②):这条推法此前只服务 409 体,于是 `GET /v1/approvals` /
|
|
78
|
-
* `/v1/approvals/stream` 两条 durable 读面的行上**没有**出身格 —— 同一道 park 门,从 409 看得见出身、
|
|
79
|
-
* 从运维队列看不见([3683]-2 实测)。补法**不是**在读面另写一份判据(两份必漂,而这一位恰恰是靠
|
|
80
|
-
* 「行上取证 ∧ 部署姿态」的合取才不谎报的),而是让三条面共用本函数。
|
|
81
|
-
*
|
|
82
|
-
* ⚠️ **askId 为什么不在这里**(#220 的另一半,如实记账):待批 ask 的 `askId` 只活在 `approval_ask` 行上
|
|
83
|
-
* (checkpoint 行没有这一列,core 的 `CheckpointGate`/`PendingAction`/`Checkpoint` 顶层都不带它)。反向
|
|
84
|
-
* (checkpoint → ask)今天**没有读口**:两条 list 读口都硬过滤 `state='STREAM_PENDING'`,而 park 完的行是
|
|
85
|
-
* `PARKED`;唯一精确的连接键是 ask 行上的 `gate_token`,要按它反查得给店加一个新读口(SQL 面 = 双库集成
|
|
86
|
-
* 门)。派生 `deriveAskId(...)` 也不行:它吃 `legKey`(= 上一腿 resume token 的摘要)与 `parentToolCallId`,
|
|
87
|
-
* 这两维从 checkpoint 行推不出来,二腿/子代形上会算出一个**错**的 id —— 在「等的谁」这条通道上,错 id
|
|
88
|
-
* 比缺席坏得多(本文件 `resumeEntryForGate` 的 default 臂是同一条纪律)。故本批诚实缺席。
|
|
89
|
-
*/
|
|
90
105
|
export declare function governanceOriginOf(gate: {
|
|
91
106
|
riskDescriptor?: {
|
|
92
107
|
shellGateDoctrine?: "classify" | "always";
|
|
@@ -122,10 +137,12 @@ interface ConflictProbeDeps<TToken> {
|
|
|
122
137
|
checkpointStore?: {
|
|
123
138
|
peekPendingScope?: (sessionId: string) => Promise<string | null | undefined>;
|
|
124
139
|
findPendingTokenBySession?: (sessionId: string, scope?: string) => Promise<TToken | null | undefined>;
|
|
125
|
-
/** 结构形(不 import core 的 `Checkpoint`)
|
|
140
|
+
/** 结构形(不 import core 的 `Checkpoint`):只声明本模块**真读**的几格。`riskDescriptor` 是
|
|
126
141
|
* core 在 mint 时挂到升级门上的判读元数据(display/triage only),`shellGateDoctrine` 是 2026-08-05
|
|
127
|
-
* 裁定加的取证格 —— 见下方 `governanceOriginOf`
|
|
142
|
+
* 裁定加的取证格 —— 见下方 `governanceOriginOf` 的推导论证。`checkpointId`(#285,core 5.42.0)
|
|
143
|
+
* 是行的非秘密稳定身份,可选 —— legacy 行没有它(见 `PendingGateMaterial.checkpointId`)。 */
|
|
128
144
|
get?: (token: TToken) => Promise<{
|
|
145
|
+
checkpointId?: string;
|
|
129
146
|
gate?: {
|
|
130
147
|
kind?: string;
|
|
131
148
|
riskDescriptor?: {
|
|
@@ -77,6 +77,55 @@ export function resumeEntryForGate(kind, ids) {
|
|
|
77
77
|
* 这两维从 checkpoint 行推不出来,二腿/子代形上会算出一个**错**的 id —— 在「等的谁」这条通道上,错 id
|
|
78
78
|
* 比缺席坏得多(本文件 `resumeEntryForGate` 的 default 臂是同一条纪律)。故本批诚实缺席。
|
|
79
79
|
*/
|
|
80
|
+
/**
|
|
81
|
+
* #285 身份格的**信任边界校验**(codex 对抗复审 R1-[high],验真后修)。
|
|
82
|
+
*
|
|
83
|
+
* 这个值来自**持久层 blob**(`checkpoint-store-sql.ts` 是 `parseJson<Checkpoint>` 整只反序列化,
|
|
84
|
+
* 零运行时字段校验;LOCAL 车道同理),所以它和别的「core 铸给我们的值」不是一个信任等级 ——
|
|
85
|
+
* 坏行 / 错迁移 / 第三方 store 实现都能往这一格里写任何字符串。
|
|
86
|
+
*
|
|
87
|
+
* 🔴 **两条判据都是 fail-closed(判不了就丢键),而不是「尽量带上」**:
|
|
88
|
+
* 1. **绝不**把 token **包含**进去(不是「不等于」——见下方那条返工记账)。这是本文件第一条纪律
|
|
89
|
+
* (`token` 永不上 wire)的**执行点**,不是洁癖:`CheckpointToken` 在运行时就是个字符串,一条把
|
|
90
|
+
* token 抄进 `checkpointId` 的行会让这条投影**原样把 resume 能力写进 409 体**,并经
|
|
91
|
+
* `toDoneFrameResult` 继续流到 SSE done 帧。
|
|
92
|
+
* ⚠️ **返工记账(codex 对抗复审两轮,第二轮才逼出真形)**:第一版判据是「非空串」——对「行上写的
|
|
93
|
+
* 就是 token」这个反例零覆盖(token 也是非空串);第二版收成「不等于 token」——**仍然可绕**:写
|
|
94
|
+
* `"cp_" + token`(35 字符,非空,不等于 token)照样过关,接收方剥掉前缀就拿回完整能力。⇒ 判据必须
|
|
95
|
+
* 是**包含关系**。负控三格(逐字相等 / 前缀包装 / 后缀包装)钉在
|
|
96
|
+
* `test/session-active-conflict-materials.test.ts` 的 #285 安全负控族。
|
|
97
|
+
* 比较**大小写不敏感**(第三轮加固:小写 hex token 的 `toUpperCase()` 形不含原串,零成本堵掉)。
|
|
98
|
+
* 2. **长度有界**。core 的真形是 `cp_`+uuid(39 字符),给到 {@link MAX_CHECKPOINT_ID_CHARS} 已是
|
|
99
|
+
* 数量级余量;无界透传等于让持久层里的垃圾决定响应体大小。边界(恰好 128 / 129)有钉。
|
|
100
|
+
*
|
|
101
|
+
* 🔴 **这道校验买得到什么、买不到什么(威胁模型如实写死,别再让下一个人以为它是闭合的)**:
|
|
102
|
+
* · **买得到**:误抄(迁移脚本 / 店实现把 token 抄进这一格)、朴素包装(`"cp_"+token`、加后缀)、
|
|
103
|
+
* 大小写变形 —— 这几类是**真实可能发生的事故**,而它们的后果是把 resume 能力发上 wire。
|
|
104
|
+
* · **买不到、也不可能买到**:一个**能往 blob 里写任意值的对手**。128 位能力键塞进一个 128 字符的
|
|
105
|
+
* 身份格有无穷多种可逆编码(base64、逐字符置换、……),值校验挡不住任何一种。
|
|
106
|
+
* ⚠️ 第三轮复审建议「严格只放行 `cp_`+UUID 格式」来闭合 —— **实测反证:那样也闭合不了**。把 32 位
|
|
107
|
+
* hex token 按 uuid 分组(`cp_01234567-89ab-cdef-0123-456789abcdef`)**逐字满足**严格正则,接收方
|
|
108
|
+
* 去掉 `cp_` 和连字符就拿回完整 token(2026-08-18 亲跑验过)。⇒ 格式钉只会额外买来「core 换形我方
|
|
109
|
+
* 静默丢键」的代价,不买来任何安全性,故**仍然刻意不做格式正则**。
|
|
110
|
+
* · 真正的控制在别处:能往 checkpoint blob 里写任意值 ⇒ 已有该表的写权限 ⇒ 本来就读得到 token 列。
|
|
111
|
+
* 本校验的定位因此是**纵深防御 + 事故拦截**,不是信任边界的唯一防线。
|
|
112
|
+
* ⚠️ 丢键是**静默**的(本函数无日志席,而本载体按顶注是 best-effort 分诊材料)。⇒ 一条被污染的行在
|
|
113
|
+
* 运维面上**与 legacy 行不可分辨**,这是本批**如实登记的残留**(第三轮复审 [low] 验真):此前这里写
|
|
114
|
+
* 「坏行在 store 读路径另有响亮面」是**假的** —— `checkpoint-store-sql.ts` 的 `get` 只是
|
|
115
|
+
* `parseJson<Checkpoint>` 直返,没有任何字段校验或告警。补一条结构化 warning/metric(**绝不带原值**)
|
|
116
|
+
* 需要给本模块拉观测席,那要过三问,已作为提货批未决项上报,不在本批就地加。
|
|
117
|
+
*/
|
|
118
|
+
const MAX_CHECKPOINT_ID_CHARS = 128;
|
|
119
|
+
function safeCheckpointIdOf(raw, token) {
|
|
120
|
+
if (typeof raw !== "string" || raw === "" || raw.length > MAX_CHECKPOINT_ID_CHARS)
|
|
121
|
+
return undefined;
|
|
122
|
+
// 🔴 `includes` 而不是 `===`:包装形(`"cp_" + token` / `token + "-x"`)剥掉装饰就是完整能力键。
|
|
123
|
+
// 🔴 大小写不敏感:小写 hex token 的 `toUpperCase()` 形不含原串,而它同样是可逆的(零成本堵掉)。
|
|
124
|
+
const secret = token === undefined || token === null ? "" : String(token).toLowerCase();
|
|
125
|
+
if (secret !== "" && raw.toLowerCase().includes(secret))
|
|
126
|
+
return undefined; // 秘密反射(含包装/变形):丢
|
|
127
|
+
return raw;
|
|
128
|
+
}
|
|
80
129
|
export function governanceOriginOf(gate, governance) {
|
|
81
130
|
if (gate?.riskDescriptor?.shellGateDoctrine !== "always")
|
|
82
131
|
return undefined;
|
|
@@ -125,13 +174,22 @@ export async function buildActiveRunConflict(deps, sessionId, activeTaskId) {
|
|
|
125
174
|
if (cs?.findPendingTokenBySession) {
|
|
126
175
|
const scope = (await cs.peekPendingScope?.(sessionId)) ?? undefined;
|
|
127
176
|
const token = scope === null ? null : await cs.findPendingTokenBySession(sessionId, scope);
|
|
128
|
-
const
|
|
177
|
+
const row = token ? await cs.get?.(token) : undefined;
|
|
178
|
+
const gate = row?.gate;
|
|
129
179
|
const kind = gate?.kind;
|
|
130
180
|
const decidePath = kind ? resumeEntryForGate(kind, { sessionId, taskId: activeTaskId }) : null;
|
|
131
181
|
// #220:出身格与出路格**同一次读**里取(不为它多打一次库),`true` 才写键(见 PendingGateMaterial 顶注)。
|
|
132
182
|
const governanceForced = governanceOriginOf(gate, deps.governance);
|
|
133
|
-
|
|
134
|
-
|
|
183
|
+
// #285:身份格同一次读里取,判据见 `safeCheckpointIdOf`(信任边界校验 —— 该值来自持久层 blob)。
|
|
184
|
+
const checkpointId = safeCheckpointIdOf(row?.checkpointId, token);
|
|
185
|
+
if (kind && decidePath) {
|
|
186
|
+
pendingGate = {
|
|
187
|
+
kind,
|
|
188
|
+
decidePath,
|
|
189
|
+
...(governanceForced ? { governanceForced } : {}),
|
|
190
|
+
...(checkpointId !== undefined ? { checkpointId } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
135
193
|
}
|
|
136
194
|
return {
|
|
137
195
|
...base,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* `cancelledViaVerb` first-writer-wins 判别、`steerableRuns` 活流句柄、`wakeParkMints` park 铸造闩)。
|
|
14
14
|
*/
|
|
15
15
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
16
|
+
import { ActorAssertionWire } from "@sema-agent/registry-core";
|
|
16
17
|
import type { RouteCtx, PreparedTaskSubmission } from "../route-ctx.js";
|
|
17
18
|
export declare const RUN_ID_RE: RegExp;
|
|
18
19
|
export declare const RUN_CANCEL_RE: RegExp;
|
|
@@ -26,6 +27,11 @@ export declare const ELICIT_RESPOND_RE: RegExp;
|
|
|
26
27
|
export declare const QUESTION_RESPOND_RE: RegExp;
|
|
27
28
|
export declare const TOOL_APPROVAL_RESPOND_RE: RegExp;
|
|
28
29
|
export declare const TASK_ASK_DECISION_RE: RegExp;
|
|
30
|
+
/** 行上的 `decision_actor`(裸 JSON)投成回显形;形不认 ⇒ undefined(调用点省略该键,不 500)。
|
|
31
|
+
* 导出仅为契约门(`test/ask-decision-actor-contract.test.ts` 钉「键集同源包版 + 严格度刻意宽读」两条)
|
|
32
|
+
* —— 导出的是**行为**而不是 schema 对象:schema 的推断类型会牵出 registry-core 内嵌的 zod 路径
|
|
33
|
+
* (TS2742,不可移植),而这道门要钉的本来就是行为。非 wire 面。 */
|
|
34
|
+
export declare function projectDecisionActor(raw: unknown): ActorAssertionWire | undefined;
|
|
29
35
|
export declare const RUN_SUBAGENT_STEER_RE: RegExp;
|
|
30
36
|
export declare const RUN_SUBAGENT_RESUME_RE: RegExp;
|
|
31
37
|
export declare const RUN_SUBAGENT_OUTPUT_RE: RegExp;
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -21,6 +21,7 @@ import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
|
21
21
|
import { buildActiveRunConflict } from "../active-run-conflict.js";
|
|
22
22
|
import { clearTurnActivity, readTurnActivityMs } from "../../turn-activity.js";
|
|
23
23
|
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
24
|
+
import { ActorAssertionWire } from "@sema-agent/registry-core";
|
|
24
25
|
import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
|
|
25
26
|
// server.ts 侧的 routeLabel / isBillableSubmitPath 仍要用下面这些正则,故本模块导出(方向恒为 server.ts → routes/*)。
|
|
26
27
|
export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
|
|
@@ -51,28 +52,24 @@ const ASK_PARKING_RETRY_AFTER_SEC = 2;
|
|
|
51
52
|
/** `decision_actor` 的展示文本上限(171 ActorAssertion 的 `label` 帽,§2)。 */
|
|
52
53
|
const ASK_ACTOR_LABEL_MAX = 256;
|
|
53
54
|
/**
|
|
54
|
-
* 171 `ActorAssertion` 的 server
|
|
55
|
+
* 171 `ActorAssertion` 的 server 侧**读**面 schema —— **键集同源包版,严格度刻意宽读**。
|
|
55
56
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* 键名/取值域逐字照 171(`{id, label?, verified?}`),**加** `via`(§12-D F7 的审计缺口补丁)。
|
|
60
|
-
* 依赖抬到 ≥0.17.1 的那一车应把本 schema 换成 registry-core 的同源导出(键集已对齐,零 wire 变更)。
|
|
57
|
+
* 属主收敛(#282 件1,DESIGN-282 §1):§12-D 早就裁了「schema 落 `@sema-agent/registry-core`」,车4 当时
|
|
58
|
+
* 因本仓 deps 钉在 `^0.16.0`(`ActorAssertionWire` 要 0.17.1 才有)只能手写一份对齐形,并把欠账写在注里。
|
|
59
|
+
* 本批把 deps 抬到 `^0.18.0` 并兑现:键集/取值域**不再有第二份拼写**,直接取包版 {@link ActorAssertionWire}。
|
|
61
60
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* 🔴 但**不能裸替换**:包版是 `.strict()`(registry-core types.ts `ActorAssertionWire`),而本处是**读**面,
|
|
62
|
+
* 两者的严格度取向刻意相反 ——
|
|
63
|
+
* · 包版严:它同时被**写/铸**侧当规范用,未知键 = 写的人搞错了,当场拒是对的;
|
|
64
|
+
* · 本处宽:行是**别的进程、别的版本**写下的。未来某版给 actor 加一个附加字段,一 `.strict()` 就把
|
|
65
|
+
* **整份** actor 判为不认、连 `id`/`verified` 都一起丢 —— 而「坏形只丢 actor 不 500」正是本 schema
|
|
66
|
+
* 存在的意义(§12-D F12)。出处:本文件 codex 交叉复审 round2 的真 finding(红先验过);本批换装时
|
|
67
|
+
* 再次机械复验 —— 裸用包版 ⇒ D-10(`test/tool-approval-e2e-http.test.ts`)当场红。
|
|
68
|
+
* 故取 `.strip()`(zod 默认档):已知字段照常投影,附加字段静默丢弃(响应键集因此仍是封闭的,不转发陌生字节)。
|
|
69
|
+
* `verified`/`via` 缺席不是错误(缺席 = 写那行的版本没记)。
|
|
64
70
|
*
|
|
65
|
-
* 🔴 **不加 `.strict()`**(codex 交叉复审 round2 抓获,真 finding,红先验过):写面的 body schema 该严
|
|
66
|
-
* (未知键 = 调用方写错了,当场告诉他);**读**面严则是自伤 —— 未来某版给 actor 加一个附加字段,这里
|
|
67
|
-
* 一 `.strict()` 就把**整份** actor 判为不认、连 `id`/`verified` 都一起丢,而那正是本 schema 存在的
|
|
68
|
-
* 意义。zod 默认剥未知键:已知字段照常投影,附加字段静默丢弃(响应键集因此仍是封闭的)。
|
|
69
71
|
*/
|
|
70
|
-
const AskDecisionActorSchema =
|
|
71
|
-
id: z.string().min(1).max(ASK_ACTOR_LABEL_MAX),
|
|
72
|
-
label: z.string().max(ASK_ACTOR_LABEL_MAX).optional(),
|
|
73
|
-
verified: z.boolean().optional(),
|
|
74
|
-
via: z.enum(["owner", "operator"]).optional(),
|
|
75
|
-
});
|
|
72
|
+
const AskDecisionActorSchema = ActorAssertionWire.strip();
|
|
76
73
|
/**
|
|
77
74
|
* 回决 body(稿 §3 + §12-F)。`.strict()` = 未知键当场 400,不静默吞。
|
|
78
75
|
*
|
|
@@ -98,8 +95,11 @@ const AskDecisionBodySchema = z
|
|
|
98
95
|
actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
|
|
99
96
|
})
|
|
100
97
|
.strict();
|
|
101
|
-
/** 行上的 `decision_actor`(裸 JSON)投成回显形;形不认 ⇒ undefined(调用点省略该键,不 500)。
|
|
102
|
-
|
|
98
|
+
/** 行上的 `decision_actor`(裸 JSON)投成回显形;形不认 ⇒ undefined(调用点省略该键,不 500)。
|
|
99
|
+
* 导出仅为契约门(`test/ask-decision-actor-contract.test.ts` 钉「键集同源包版 + 严格度刻意宽读」两条)
|
|
100
|
+
* —— 导出的是**行为**而不是 schema 对象:schema 的推断类型会牵出 registry-core 内嵌的 zod 路径
|
|
101
|
+
* (TS2742,不可移植),而这道门要钉的本来就是行为。非 wire 面。 */
|
|
102
|
+
export function projectDecisionActor(raw) {
|
|
103
103
|
const parsed = AskDecisionActorSchema.safeParse(raw);
|
|
104
104
|
return parsed.success ? parsed.data : undefined;
|
|
105
105
|
}
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -61,6 +61,13 @@ export interface ToolApprovalFrame {
|
|
|
61
61
|
* `governance-ask-marks.ts` 顶注。
|
|
62
62
|
*/
|
|
63
63
|
governanceForced?: true;
|
|
64
|
+
/** #283([4390]③/[4392];**ADDITIVE**,`"tool_approval"` only)——core PreToolUse hook/策略族铸的
|
|
65
|
+
* **安全类 ask 出身位**(`AskRequest.requiresRealApproval`,core 5.37 起可选、真才带)。帧顶层契约与
|
|
66
|
+
* core 侧同构:**真才带,缺席绝不编 false**(governanceForced 同形)——与 `card.requiresRealApproval`
|
|
67
|
+
* (approval-card.ts,恒在布尔)**两侧契约刻意不同、别混**:卡是 durable 店形(askStore 在场)专属,
|
|
68
|
+
* 本键让 live-only 部署(店缺席、帧无卡)也能读到出身位;安全类标记不得依赖店在场([4392] 结构性
|
|
69
|
+
* 缺口成文)。消费语义(cli 挂点已预埋):在场 ⇒ 一切自动放行让位(含记住的规则与 bypass 姿态)。 */
|
|
70
|
+
requiresRealApproval?: true;
|
|
64
71
|
/**
|
|
65
72
|
* #144(core 5.25.0,[3438] 接力契约 / [3443] 主件;**ADDITIVE**,`"tool_approval"` only)——
|
|
66
73
|
* 这只 ask **命中了**调用方的一条持久 allow 规则,而那条规则**没能清掉它**。值 = 被越级的那条规则
|
package/dist/tool-approval.js
CHANGED
|
@@ -889,6 +889,8 @@ export class ToolApprovalCoordinator {
|
|
|
889
889
|
// [2942]/[2943]:治理来源标(additive,只在为真时在场——缺席绝不编 false,见字段顶注)。
|
|
890
890
|
// 值来自上方那一次**非消费式** peek(同一 toolCallId 的重播/failover 再入必须拿到同一份判定)。
|
|
891
891
|
...(governanceForced ? { governanceForced: true } : {}),
|
|
892
|
+
// #283:安全类出身位帧顶层过境(真才带;core 可选契约同构,类型顶注成文与卡内恒在形的分歧)。
|
|
893
|
+
...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
892
894
|
// #144(core 5.25.0):被越级的持久规则原文。**逐字透传 core 的判定**(是不是"命中但清不掉"由引擎
|
|
893
895
|
// 的消音谓词裁,server 不重判);内容族 ⇒ `redactSecrets` 后上帧,空串不铸键(见字段顶注)。
|
|
894
896
|
// 🔴 **脱敏后当场截到卡的上限**(codex 对抗复审 R2-[medium],验真后修):`redactSecrets` 会**变长**
|
|
@@ -26,8 +26,8 @@ type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "descripti
|
|
|
26
26
|
type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, BgNotifProjected | BgNotifExcluded>>;
|
|
27
27
|
type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
|
|
28
28
|
type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
|
|
29
|
-
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleSuggestions" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence";
|
|
30
|
-
type AskExcluded = "preview" | "principal" | "
|
|
29
|
+
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleSuggestions" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval";
|
|
30
|
+
type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason";
|
|
31
31
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
32
32
|
type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest";
|
|
33
33
|
type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.33.0-rc.1",
|
|
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",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^5.
|
|
58
|
-
"@sema-agent/registry-core": "^0.
|
|
57
|
+
"@sema-agent/core": "^5.42.0",
|
|
58
|
+
"@sema-agent/registry-core": "^0.18.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
61
61
|
"mysql2": "^3.22.4",
|