@sema-agent/server 7.108.0 → 7.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/MIGRATION.md +43 -1
  2. package/USAGE.md +18 -28
  3. package/dist/approval-ask-machine.d.ts +9 -25
  4. package/dist/approval-ask-machine.js +5 -2
  5. package/dist/approval-card.d.ts +32 -3
  6. package/dist/approval-card.js +29 -2
  7. package/dist/bench/s1/live-deps.d.ts +4 -5
  8. package/dist/bench/s1/live-deps.js +9 -10
  9. package/dist/boot/leader.js +0 -2
  10. package/dist/boot/resolve-spec.d.ts +1 -1
  11. package/dist/boot/resolve-spec.js +3 -8
  12. package/dist/capabilities/scenarios.d.ts +3 -2
  13. package/dist/config-catalog.js +1 -3
  14. package/dist/config-provider.js +1 -0
  15. package/dist/config-types.d.ts +2 -9
  16. package/dist/config.js +2 -5
  17. package/dist/http/admission.js +8 -2
  18. package/dist/http/route-ctx.d.ts +3 -3
  19. package/dist/http/routes/capabilities.js +1 -0
  20. package/dist/http/server.d.ts +2 -2
  21. package/dist/http/wire-types.d.ts +8 -0
  22. package/dist/leader/diffup.d.ts +53 -0
  23. package/dist/leader/diffup.js +95 -1
  24. package/dist/leader/leader.d.ts +0 -6
  25. package/dist/leader/leader.js +1 -1
  26. package/dist/leader/repair-wire.js +1 -1
  27. package/dist/leader/wire.d.ts +3 -10
  28. package/dist/leader/wire.js +39 -13
  29. package/dist/observability/fail-open.d.ts +1 -5
  30. package/dist/observability/fail-open.js +1 -5
  31. package/dist/plugins/approval-ask-store-file.d.ts +2 -1
  32. package/dist/plugins/approval-ask-store-file.js +3 -0
  33. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  34. package/dist/plugins/approval-ask-store-memory.js +7 -1
  35. package/dist/plugins/approval-ask-store-sql.d.ts +18 -0
  36. package/dist/plugins/approval-ask-store-sql.js +7 -0
  37. package/dist/request-key-closure.js +1 -1
  38. package/dist/run-local.js +2 -3
  39. package/dist/runtime-governance.d.ts +3 -3
  40. package/dist/tool-approval.d.ts +37 -15
  41. package/dist/tool-approval.js +8 -7
  42. package/dist/trace/core-keyset-guard.d.ts +1 -1
  43. package/dist/trace/project.js +2 -3
  44. package/dist/unattended-permission-mode.d.ts +54 -93
  45. package/dist/unattended-permission-mode.js +8 -61
  46. package/package.json +3 -3
package/MIGRATION.md CHANGED
@@ -7,6 +7,47 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.109.0(车SK,S-934)—— `@sema-agent/core` 7.37.1(7.37.0 的全部变更 + #1224 工具回合律请求修复,运维通告 `context.tool_turn_repaired` 不上会话流):无人值守腿按类作答(`UNATTENDED_PERMISSION_MODE` 退役,设了即拒启)· ask 类对 `askClass` / `classes` 上 wire · 策略决定对象的普通对象律 · 迁移窗口
11
+
12
+ - **🔴 env:`UNATTENDED_PERMISSION_MODE` 设了即拒启**(任何值,含 7.106.0 的缺省 `bypassPermissions`;拒因带 `config.env_key_retired` 并点名键)。
13
+ **迁移**:从 `.env` / 编排清单里删掉它。leader worker / leader 修复腿 / `sema` 一次性 CLI(run-local)及 leader worker park 的 `/decide` 续跑,
14
+ 自本版起恒按 `default` 跑,审批席**按类作答**:机器只答引擎盖了 `ordinary` 类的问(普通写、非只读 shell 的形状层问 —— 照常落地)。
15
+ **写面与 `ordinary` 的边界**:`roots` 只约束 Write / Edit 这类结构化文件工具;shell 的根外写与 Read 工具的根外读属 core `ordinary`,无人值守腿上由机器答(与 ≤7.108.0 的 bypass 放行同一落地结果,记法不同)。
16
+ **审计读者**:机器按类放行在 core 门记录里写成 `settlement.kind:"human_allowed"` / `who.party:"person"`(core 没有「机器放行」归属词,S-953 转 core)—— 无人值守腿上读作机器放行;无席且无 durable 块的 leader 腿被拒的问(run-local 有本地席,不走这一词)结算词 `no_approver` → `approver_unavailable`,按词告警的运维改比码。
17
+ - **🔴 行为面(安全轴收紧)**:这些腿上此前被缺省 `bypassPermissions` 放掉的六类问 —— shell 读会话目录外(`probe`)、MCP / A2A 工具标记、
18
+ transcript 写、规则店读不出(`rule_unreadable`)、共享制品读(`artifact_shared_read`)—— 改为**不答**:run-local 有终端时问你 y/N、无终端当场拒;
19
+ leader 各腿配了 durable 审批块 ⇒ park 到 `/v1/approvals/:id/decide`,没配 ⇒ 当场拒。写保护表目标从「策略层路径拒行拒(人在场也答不了)」改成一问
20
+ (有席问人 / 无席 park 或拒);`cp <写保护目标> <普通目标>` 这类只读写保护源的命令不再被过拒。**迁移**:靠无人值守 worker 读工作区外文件 /
21
+ 调带标记 MCP 工具的流程,给 leader 配 durable 审批块让人答,或把要读的东西放进工作区。
22
+ - **wire(additive)**:live `tool_approval` 帧、`approval_request` 呈卡帧的 `card`(= `card_json` = 断连重放帧)、`GET /v1/approvals` 队列行与 `/stream`
23
+ 的 `pending` 帧新增两只可选键 `askClass?` / `classes?`(core 盖的类对,两键同在同缺;缺席 = 老引擎 / 老行 / 不盖类的站,**不是**「普通」)。
24
+ 零动作;按类渲卡 / 按类答的壳读**整只** `classes`。回滚窗:`card_json` 是 `.strict()` 形 ⇒ 回滚到 ≤7.108.0 的副本读不动带这两键的新卡行
25
+ (重放跳过 + 一次 warn,幂等重入走 park —— 与既往 additive 卡键同一代价),整队滚。
26
+ - **🔴 行为面(core 7.37.0,策略 / hook 作者)**:引擎对策略 / hook / 祖先返回的决定对象**只读一次**,只收**普通对象**:`action` 必须是
27
+ `allow` / `deny` / `ask` 之一;契约成员必须是对象**自有可枚举**成员(不放原型上、不靠 getter、不靠 Proxy);改写(`updatedInput` 等)只放可克隆纯数据。
28
+ 不合 ⇒ 接手即拒(工具不执行)。本服务自身的策略 / hook 全是字面对象或展开(亲核),部署自写的 `ToolPolicy`(嵌入方)按此自查。
29
+ **委派**:祖先层一次「是」只覆盖它答过的类 —— 子代门查到更严的类(写保护、根外读)会再出一张卡;带出网 / 不可逆标记的工具在委派下首卡即
30
+ `operator_forced`(`bypassPermissions` 不放它)。持久 ask 店车道(流内审批协议上场的部署)上第二张卡同样出卡(S-944,见下一条)。
31
+ - **🔴 安全轴(S-944):持久 ask 店车道上的 askId 身份含这一问的类集**。委派下同一调用的第二张卡(类集不同)自本版是**新问**:新行、出自己的卡、
32
+ 等人答这一问 —— 此前它与首卡同 askId,被首卡的「是」回放。同一调用同类集的重试照旧幂等。**行为面(收紧)**:会话授权(`allow_session` /
33
+ 耐久 `remember:"session"`)不再代答「这次调用在这条腿上已出过卡」的后续问(同一调用的另一类集问)—— 出卡,人答。**wire 值变更**:不透明 `askId`(呈卡帧、
34
+ `POST /v1/tasks/:taskId/asks/:askId/decision`)对同一调用的取值与 ≤7.108.0 不同(形不变,64 位 hex);持久 `approval_ask.ask_id` 主键的派生同变,
35
+ **零 DDL、零迁移、零兼容读**。**升级影响(亲核)**:升级前写的行按旧派生,行上存的 askId 原样可答 / 可重放 / 可对账(本服务没有任何一处从行或
36
+ 停车行反算 askId);跨 park 续跑的腿本来就换 `legKey` ⇒ 新派生,与旧行无交集 ⇒ 无影响。**同一条腿跨版本重入**(同 run、同 `legKey`:重试 /
37
+ 闭包再入 / 跨副本 failover / 同一 resume token 重投)同一调用时,新版派生的新 id 不命中旧行 ⇒ 这一问出卡再问(人再答一次,fail-closed);
38
+ 会话授权(live `allow_session` / 耐久 `remember:"session"`)**不会**替它代答 —— 一揽子放行的复核按**调用**读(这次调用在这条腿上有任何行,
39
+ 含旧派生写的人拒 / 挂着的卡 ⇒ 不短路),与 ask 身份无关。**迁移**:自建 `StoreBackend`(不走 `createStoreBackend`)的嵌入方须实现 `ApprovalAskStore.listByCall`(类型面硬破坏,tsc 红即通知);部署零 DDL、零数据迁移。**滚动升级**:混版窗内旧版副本接管本版写过的腿读不到新派生行(反向),
40
+ 与 7.107.0 段「持久面反向不兼容边界」同一处置:同一存储上不要让旧版实例接管本版在跑的腿。
41
+ - **leader worker 的增量上传循环随 env 收尾(S-949,P1;嵌入方 / 夹具可见)**:worker `executionEnvFactory` 交出的 env 若**没有 `destroy()`**,工厂当场拒收(与集成沙箱 / grader 同一道闸;生产三车道 e2b / k8s / host 都有);stage 末步起循环、finalize 首步停循环(先于 belt 与真 destroy,等每一代退出 ≤30 s),循环的每次上传带 `curl --max-time 25`;工作区多出 `.leader-upload-loop.sh` / `.gen` / `.seq` / `.pid.<代>`(无凭据)。**迁移**:自建 worker env 的嵌入方补 `destroy()`;依赖「循环与 belt 并跑」时序的无;收尾每 worker 常态多 ≤1–2 s(等循环下一拍退出),上界 = stop 等待 30 s(循环正卡在一次上传上时)。已知边界:S-950(e2b 车道循环是否真活着未核)/ S-951(stop 超界残余窗)。
42
+ - **目录条目 `headers` 两条读腿打通(S-935,settings-schema 6.4.0)**:`config.d/models.json` 与配置中心写的 `models[].headers` 自本版起到达目录(7.108.0 被读腿剥掉)。
43
+ **行为面**:秘密形的头值(形似令牌 / 密钥)在 schema 自由形安全闸即让条目不可用(拒因字段整键 `headers`)。**迁移**:零动作。
44
+ - **🔴 迁移窗口(core #1213;core 7.38.0 发车帖把它改期到 **7.39.0**)**:core 7.39.0 删掉旧检查点行的一次性迁移、并**拒续跑**仍带旧模式键(`autoModeRequested` / `approverPosture` /
45
+ doctrine `shellGate`)的行。这类行只可能是 **server ≤7.105.0(core ≤7.33.8)时期停放、至今仍 pending** 的行 —— server 7.106.0(core 7.35.2)起写的行
46
+ 与在 7.106.0+ 上续跑过一次的行都已是新形。**在升到提货 core 7.39.0 的 server 版本之前**(7.110.0 提货 core 7.38.0 仍迁移、不拒),把这些行答完或放弃:
47
+ 列表 = `GET /v1/approvals`(运维队列;`createdAt` 早于本部署升到 server 7.106.0 那一刻的行)与 `GET /v1/assistant/inbox`;
48
+ 决定口 = 工具审批 `POST /v1/approvals/:id/decide`(`:id` = sessionId,体 `{ "decision": "approve" | "deny" }`)、计划审阅
49
+ `POST /v1/assistant/tasks/:id/plan_review`、资源停车续跑 `POST /v1/assistant/tasks/:id/resume`。本版(core 7.37.x)上续跑这类行仍会迁移一次再继续。
50
+ 本服务不读、也不判这几个旧键(那是 core 的私有迁移面),只按停放时间给出区间。
10
51
  ## 7.108.0(车SI,S-919 / S-929 / S-930 / S-920;车SJ,S-931 / S-925 / S-922 / S-921)—— `@sema-agent/core` 7.36.0 不变:按名策略臂读 core 名字全集 · 有序关停 settle 活卡 · 自助抹除口码表补齐 · 目录条目 `headers` · 出站建连失败通告 · side-query `errorMessage` 脱敏
11
52
 
12
53
  - **🔴 安全轴(S-919):本仓收紧方向的按名策略臂(`APPROVAL_DENY` / `APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` / commandPolicy ask 表 / 治理 shell 标 / durable 问题门 /
@@ -36,6 +77,7 @@
36
77
  - **wire 值(S-922):`POST /v1/side-query` 200 体 `errorMessage` 过 `redactErrorMessage`**(键不变;服务商错误体里的密钥形换 `«redacted»`)。**迁移**:按原样错误体做字符串匹配的调用方改比错误码。
37
78
  - **零动作(S-921 / S-920)**:披露改真与明拒行保鲜钉,无行为变化。
38
79
 
80
+
39
81
  ## 7.107.0(车SE,S-907 / S-890;车SF)—— `@sema-agent/core` 7.36.0:post 钩子收 wire 名 · 双引擎名声明拒 · 工具调用 id 越界折叠(服务商拿回原串)· 两只恒真能力位删
40
82
 
41
83
  - **🔴 hooks:PostToolUse / PostToolUseFailure 的 matcher 与 payload `tool_name` 改为调用派发到的工具的 wire 名**(此前是模型拼写;PreToolUse 本来就是 wire 名)。
@@ -74,7 +116,7 @@
74
116
  也被拒(core 路径拒座把 `cp` 的源也按写目标判,与 CC 同形),升级前照跑 —— 补偿 = 旋钮 `acceptEdits|default` + 审批席,或用读工具取内容。
75
117
  🔴 **宽于升级前的六类**:根外读探针(`probe`)、MCP / A2A 工具标记、transcript 写、`rule_unreadable`(**规则店读不出时照放** —— 安全轴 fail-open 方向)、
76
118
  `artifact_shared_read` —— 升级前这些腿上照问(无人答 ⇒ 拒),缺省旋钮下被释放。**补偿**:`UNATTENDED_PERMISSION_MODE=acceptEdits|default`,或给腿配审批席。
77
- 想更严设 `acceptEdits` / `default` / `dontAsk`,拼错即拒启。**过渡形**:`UNATTENDED_PERMISSION_MODE` 与写保护派生 deny 行是 7.106.0 过渡形;core 7.37.0 到货后(S-906,`AskRequest.askClass?` / `classes?`)退成 `default` + onAsk 按类作答,届时硬删旋钮(拒启点名)。客户端 `bypassPermissions` 不再放掉部署治理层(commandPolicy / 敏感写守卫)的 ask。`config.tool_mount_denied` 的 HTTP 档仍 400。
119
+ 想更严设 `acceptEdits` / `default` / `dontAsk`,拼错即拒启。⚰️ **7.109.0 起旋钮退役(设了即拒启),这些腿改按类作答 —— 见 7.109.0 段。** **过渡形**:`UNATTENDED_PERMISSION_MODE` 与写保护派生 deny 行是 7.106.0 过渡形;core 7.37.0 到货后(S-906,`AskRequest.askClass?` / `classes?`)退成 `default` + onAsk 按类作答,并硬删旋钮(拒启点名)—— 已于 7.109.0 兑现。客户端 `bypassPermissions` 不再放掉部署治理层(commandPolicy / 敏感写守卫)的 ask。`config.tool_mount_denied` 的 HTTP 档仍 400。
78
120
  - **会话授权(`remember:"session"` / `allow_session`)对引擎的问也生效**:同会话下一次同工具不再出卡(Write / 非只读 Bash 在 `default` 下同此;
79
121
  需要审批席 —— `TOOL_APPROVAL_ENABLED` 开,不要活卡再配 `STREAM_ASK_WINDOW_MS=0`)。`APPROVAL_NEVER_AUTO` / 耐久预算耗尽 / **治理层**
80
122
  (`runtime.commandPolicy` 的 ask、`AUTONOMY` 折出的门、敏感写守卫集)的卡改成强制卡(`mandated:true`、`ruleOffersAbsence:"mandated"`,此前 `"shadowed"`),
package/USAGE.md CHANGED
@@ -332,7 +332,7 @@ MODEL_EXTRA_BODY='{"top_k":40}' # JSON 逃生口(top_k / logit_bias 等;penalt
332
332
  🔴 **调用方读法(core 官方配方,别拿 `terminal.kind` 三元代替)**:`const out = r.result || r.salvagedOutput || "";`
333
333
  —— 先读 `result`,空了才回落。理由:`salvagedOutput` 只在**一张七员闭集**的失败终局上在场(逐字取自 core 的 `SALVAGE_ELIGIBLE_TERMINALS`:`output.degenerate`、`limits.max_tokens_exceeded`、`limits.max_cost_exceeded`、`limits.max_turns_exceeded`、`limits.max_walltime_exceeded`、`env.lifetime_expired`、`usage.window_exhausted`),**闭集之外**的失败终局(模型/供应商错 `provider.error`、`conflict`、未铸码的失败……)它**整键缺席**,而最终助手文本仍在 `result` 里;闭集**之内**它又与 `result` 取自**同一条** final message 文本。两头都指向同一条读法:先 `result`。只读 `salvagedOutput` 会在闭集外的每一条失败路径上把真产出当成"没有输出"丢掉。penalty 是预防、这是兜底。
334
334
 
335
- **可选 — 目录条目的供应商附加请求头(`headers`,S-931,7.108.0)**
335
+ **可选 — 目录条目的供应商附加请求头(`headers`,S-931,7.108.0;两条读腿打通 = S-935,7.109.0)**
336
336
  ```jsonc
337
337
  // config.d/models.json(或配置中心 models 域)的一条条目:
338
338
  { "name": "claude-1m", "id": "<上游模型 id>", "provider": "anthropic", "api": "anthropic-messages",
@@ -349,8 +349,9 @@ MODEL_EXTRA_BODY='{"top_k":40}' # JSON 逃生口(top_k / logit_bias 等;penalt
349
349
  `degrade-route` 重启片走)。
350
350
  - **请求面没有 `betas`**:头是部署 ↔ 供应商的契约(计费 / 配额 / 兼容性由运维承担),不是租户可表态的位 —— `POST /v1/tasks` 带 `betas` ⇒
351
351
  400 `request.body_shape`(键闭集)。
352
- - ⚠️ **7.108.0 的已知边界**:`@sema-agent/settings-schema` 6.3.0 的 `ModelEntry` 还没有 `headers` 键,`config.d` 与配置中心两条读腿会
353
- **剥掉**它(读告警点名未知键)—— server 这一半已就位,schema 补键并提货之后才真正可用(候裁;届时本段删掉这一条)。
352
+ - **读腿**(7.109.0 起,S-935):`config.d/models.json` 与配置中心两条读腿逐字交出 `headers`。`@sema-agent/settings-schema` 6.4.0 的 `ModelEntry.headers`
353
+ 是解析透明键 —— **契约包只做自由文本安全判**(与 `compat` / `extraBody` 同一只闸:**秘密形的值**(形似令牌 / 密钥)在那一步就让条目不可用,拒因字段是
354
+ 整键 `headers`、不回显值),**形(头名白名单 × 线路 × 值形)由本服务判**(`src/model-entry-headers.ts`,上面那几条)。
354
355
 
355
356
  **可选 — OTLP/HTTP 指标导出(core 1.37 可观测)**
356
357
  ```bash
@@ -863,31 +864,20 @@ UNATTENDED_APPROVAL_POLICY=park
863
864
  - 代答不到的:`APPROVAL_DENY` / 敏感路径守卫(是拒,不是问);`APPROVAL_NEVER_AUTO` 与耐久预算耗尽的问(「恒问人」,回执 `rememberApplied:false`);
864
865
  委派子代经父链继承到祖先席上的引擎问(引擎盖强制位,席依约不代答)。
865
866
 
866
- **无人值守腿的权限模式(`UNATTENDED_PERMISSION_MODE`,7.106.0 起)**
867
-
868
- ```bash
869
- # 词表 = 引擎的六个权限模式:default | acceptEdits | plan | auto | bypassPermissions | dontAsk。缺省 bypassPermissions。
870
- # 拼错词**启动期响亮拒**(拒因列出六个合法词;不折大小写)
871
- UNATTENDED_PERMISSION_MODE=bypassPermissions
872
- ```
873
- - 管的是本服务**自己铸** spec、**没有调用方**能在请求上说模式、也**没有人**能答问句的三类腿:leader 的 worker、leader 的
874
- 修复腿(集成沙箱修复 / 模块门修复 / 单代理修复环)、`sema` 一次性 CLI(run-local)。`POST /v1/tasks|runs` **不读**本旋钮 ——
875
- 那里模式在请求上(`permissionMode` / `settings.permissions.defaultMode`),不说 = 引擎的 `default`。
876
- - 为什么要有它:引擎(core 7.34.0)起「不说模式」= `default` —— 每一次写、每一条它证不了只读的 shell 命令先问人。上面三类腿
877
- 没有人可问 ⇒ 那一问当场拒,leader worker 会「跑完了,一个字都没写」。缺省 `bypassPermissions` 让写门 / 形状层回到 7.105.0 的样子
878
- (写保护表在这些腿上从问改成拒,不随 bypass 放开;🔴 **根外读探针、MCP / A2A 工具标记、transcript 写、`rule_unreadable`、`artifact_shared_read` 六类会被放掉 —— 7.105.0 上它们照问**;
879
- 过渡形:core 7.37.0 到货后本旋钮退成 `default` + 按类作答的审批席并硬删):
880
- 普通问句不问。本旋钮**不增不减**这些腿上已经接着的运维门:运维写下的问(审批名单、治理层的问、hook 的问)在
881
- `bypassPermissions` 下照问(没人答照旧 park 或拒),拒照拒 —— 它只决定引擎自己的「普通」问句由谁答。
882
- - 🔴 写面不随它打开:这三类腿的写面恒是工作区根内(`roots`,与 7.105.0 同)—— `bypassPermissions` 在引擎模式表里自带的「根外也能写」
883
- **不**随本旋钮进来。
884
- - 想让这些腿更严:设 `acceptEdits`(根内写不问、非只读命令仍问 ⇒ 没人答即拒)、`default`(写与非只读命令都问)或 `dontAsk`
885
- (凡是会问的一律拒)。run-local 有 TTY 时 `default` 会在终端里问你。
886
- - `auto` 需要引擎的 auto 分类器席;leader / run-local 的 Runner 不挂它 ⇒ 引擎按「auto 不可用」折回 `default` 并发一条
887
- `config.permission_mode_unavailable` 通告。中心下发的 `RuntimeCaps.bypassPermissions === false` 同样会把 `bypassPermissions`
888
- 折回 `default`(组织级拒优先)。
889
- - 纯部署级:不看任何客户端表态,也不参与托管姿态判定(这些腿不是请求)。启动日志一行 `unattended_permission_mode`
890
- (`value` + `source: env|default`)。
867
+ **无人值守腿:`default` 模式 + 按类作答的审批席(7.109.0 起;S-934 / S-906)—— 没有旋钮**
868
+
869
+ - 管的是本服务**自己铸** spec、**没有调用方**能在请求上说模式的腿:leader 的 worker、leader 的修复腿(集成沙箱修复 / 模块门修复 /
870
+ 单代理修复环)、`sema` 一次性 CLI(run-local),以及 leader worker 的 park 经 `/decide` 续跑时重挂的同一条腿。`POST /v1/tasks|runs`
871
+ 不走这一条 —— 那里模式在请求上(`permissionMode` / `settings.permissions.defaultMode`),不说 = 引擎的 `default`。
872
+ - 这些腿恒按 `default` 跑,审批席是一只**按类作答**的席:引擎给每一问盖上它属于哪几类(`AskRequest.classes`),机器只答**全部**属于
873
+ `ordinary` 的问 —— 模式写门对普通写的问、形状层对非只读 shell 的问(写与命令照常落地,与 7.105.0 同)。其余每一类都不答:
874
+ 写保护表目标(`write_protection`)、shell 读会话目录外(`probe`)、MCP / A2A 工具标记、transcript 写、规则店读不出、共享制品读、
875
+ plan 的写问(`mode`),以及运维 / hook / 规则 / 组织为人提的问(审批名单、治理层、`APPROVAL_NEVER_AUTO` …)。不答的问:
876
+ - **有席的腿交给席**:run-local 在终端里跑 ⇒ 问你 y/N;没有 TTY ⇒ 当场拒(stderr 一句)。
877
+ - **无席的腿**(leader 各腿):配了 durable 审批块的部署 ⇒ park,等人经 `/decide` 答(与 7.105.0 同形);没配 ⇒ 当场拒(fail-closed)。
878
+ - 答集是常量(只 `ordinary`,core 推荐形),**不是旋钮**。7.106.0–7.108.0 的 `UNATTENDED_PERMISSION_MODE` 已退役:**设了即拒启**
879
+ (`config.env_key_retired`,任何值都拒);从 `.env` 里删掉它。要让机器多答某一类问,先立案,不开旋钮。
880
+ - 写面:`default` 模式不带写面 ⇒ 工作区根内(`roots`,与 7.105.0 同)。 `roots` 只约束 Write / Edit 这类结构化文件工具;shell 的根外写、Read 工具的根外读属 core `ordinary`,这些腿上由机器答(落地结果与 ≤7.108.0 的 bypass 放行同,门记录归属词见 MIGRATION 7.109.0「审计读者」)。
891
881
 
892
882
  **可选 — 写保护名表(S-138;缺席=引擎缺省表在岗)**
893
883
 
@@ -1,3 +1,4 @@
1
+ import type { AskRequest } from "@sema-agent/core";
1
2
  export type AskState = "STREAM_PENDING" | "DECIDED" | "PARKING" | "PARKED" | "DENIED" | "VOID";
2
3
  export type BatchState = "OPEN" | "ROUTING_UNBOUND" | "ROUTING_BOUND" | "ABORTED";
3
4
  /** ask 侧终态集合(无出边的 4 态)——store 层 CAS 谓词与 reap/对账扫描复用这份闭集,不许各自重列。 */
@@ -18,31 +19,14 @@ export declare const MAX_DECISION_NOTE_CHARS = 2048;
18
19
  export declare function canAskTransition(from: AskState, to: AskState): boolean;
19
20
  /** 批转移合法性,同形。 */
20
21
  export declare function canBatchTransition(from: BatchState, to: BatchState): boolean;
21
- /**
22
- * 🔴 身份元组的两条轴(车3 设计稿 §3.2′,2026-08-06 换轴,原轴 `(sourceTaskId, toolCallId, leg:number)`
23
- * 被推翻)—— 这段注释是「为什么是这五个部件」的唯一书面记录:
24
- *
25
- * ── `runId`(新增轴,封死跨 run 复用)────────────────────────────────────────────────────────────────
26
- * core 给 `AskRequest.sourceTaskId` 填的是 **sessionId**(`prepare-task.js` 的 `askSourceIdentity`),
27
- * 不是 wire run id。而原轴给每条新 run 的首腿都重置 `leg=0` ⇒ 同一会话的**下一次全新提交**若撞上同一
28
- * `toolCallId`([ref] 定稿 §3.1 记录了部分网关按**请求编号**而非全局编号铸它)会整组重现,
29
- * `ensureAsk` 的幂等 upsert 于是命中上一 run 已 `DECIDED(approve)` 的旧行并把它回放成 `true` ——
30
- * 静默复用一次早已过去的人类同意。这不是低概率碰撞,是可确定复现的路径(§10 钉 C-1)。
31
- * `runId` = 本 run 的 wire id(durable 腿 = 真 run uuid;adhoc 腿 = 该腿一次性 uuid):每次全新提交必是
32
- * 新值,同一腿内恒定(重试/failover/闭包再入不变)⇒ 幂等仍成立。
33
- *
34
- * ── `legKey`(取代数值 `leg`,一腿一凭据)─────────────────────────────────────────────────────────────
35
- * = `sha256(resume checkpoint token)` 的 hex,**首腿 = 空串**。一次 park→resume 的 token 就是这条腿的
36
- * 天然身份:同 token 重投 = 同一腿(幂等,正确);新 park ⇒ 新 token ⇒ 新腿 ⇒ 新 askId。
37
- * 取摘要而非原始 token 是因为 token 是能力凭据(本仓有 `sealResultForExit` 专门把它从可重放账本里
38
- * 剥掉),而 `askId` 是 wire 可见值 ⇒ **只存/只喂摘要**。用它当轴顺带消掉了原裁需要的 `task_run` 加列
39
- * 与 `markResuming` 返回加宽(后者有第二个调用点 `http/routes/runs.ts:463`,加宽会误杀已成功 resume 的
40
- * running run)。残留边界:同一 run 内**不经 checkpoint token** 的重入形若未来出现会得到同一 legKey ——
41
- * 今天不存在这种腿(run 内每次重入都必须经 checkpoint resolve),故不设兜底。
42
- */
43
- /** ask_id = sha256('ask' ⊕ sourceTaskId ⊕ runId ⊕ toolCallId ⊕ legKey ⊕ parentToolCallId).slice(0,64)。
44
- * 确定性:同参数元组(含重试/failover/闭包再入)永远派生同一个 id——这是 ensureAsk 幂等 upsert 成立的前提。 */
45
- export declare function deriveAskId(sourceTaskId: string, runId: string, toolCallId: string, legKey: string, parentToolCallId?: string): string;
22
+ /** 一只 ask 的请求侧身份素材 = core `AskRequest` 上的三格(型从 core 派生,不手抄;整只 `AskRequest` 可直接交)。 */
23
+ export type AskIdentityRequest = Pick<AskRequest, "toolCallId" | "classes"> & {
24
+ readonly delegation?: Pick<NonNullable<AskRequest["delegation"]>, "parentToolCallId">;
25
+ };
26
+ /** ask_id = sha256('ask' ⊕ sourceTaskId ⊕ runId ⊕ toolCallId ⊕ legKey ⊕ parentToolCallId ⊕ 类集规范形).slice(0,64)。
27
+ * 确定性:同参数元组(含重试/failover/闭包再入)永远派生同一个 id——这是 ensureAsk 幂等 upsert 成立的前提;
28
+ * 同调用不同类集 ⇒ 不同 id(S-944)。 */
29
+ export declare function deriveAskId(sourceTaskId: string, runId: string, legKey: string, request: AskIdentityRequest): string;
46
30
  /** batch_id = sha256('batch' ⊕ sourceTaskId ⊕ runId ⊕ legKey).slice(0,64)。同一 (sourceTaskId, runId,
47
31
  * legKey) 恒派生同一批——批 = 同一决策点的兄弟 ask 集合,故不掺 toolCallId。 */
48
32
  export declare function deriveBatchId(sourceTaskId: string, runId: string, legKey: string): string;
@@ -27,8 +27,11 @@ function deterministicId(prefix, parts) {
27
27
  const joined = [prefix, ...parts.map((p) => p ?? "")].join(ID_SEPARATOR);
28
28
  return createHash("sha256").update(joined, "utf8").digest("hex").slice(0, 64);
29
29
  }
30
- export function deriveAskId(sourceTaskId, runId, toolCallId, legKey, parentToolCallId) {
31
- return deterministicId("ask", [sourceTaskId, runId, toolCallId, legKey, parentToolCallId]);
30
+ function askClassSetKey(classes) {
31
+ return classes === undefined ? "" : [...new Set(classes)].sort().join(ID_SEPARATOR);
32
+ }
33
+ export function deriveAskId(sourceTaskId, runId, legKey, request) {
34
+ return deterministicId("ask", [sourceTaskId, runId, request.toolCallId, legKey, request.delegation?.parentToolCallId, askClassSetKey(request.classes)]);
32
35
  }
33
36
  export function deriveBatchId(sourceTaskId, runId, legKey) {
34
37
  return deterministicId("batch", [sourceTaskId, runId, legKey]);
@@ -21,7 +21,7 @@
21
21
  * 没有方法、没有捕获的行为。
22
22
  */
23
23
  import { z } from "zod";
24
- import { READ_ROOT_CANDIDATE_DIR_MAX, type AskEvidenceAbsence, type AskRequest as CoreAskRequest, type PendingAction as CorePendingAction, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
24
+ import { READ_ROOT_CANDIDATE_DIR_MAX, type AskClass, type AskEvidenceAbsence, type AskRequest as CoreAskRequest, type PendingAction as CorePendingAction, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
25
25
  import type { AskRow } from "./plugins/approval-ask-store-sql.js";
26
26
  /** 模型自由文本(`sourceAgentName` / `delegation.agentName`)的限长(设计稿 §6.2)。设计 §3.1 把
27
27
  * 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
@@ -429,6 +429,25 @@ export declare function readClosedWordSeat<S extends ClosedWordSeat>(carrier: ob
429
429
  type ClosedWordSeat = "mandate" | "origin";
430
430
  /** 一座的值型 = core 寄存行上那一座的型(去掉可选位的 `undefined`)。 */
431
431
  type ClosedWordOf<S extends ClosedWordSeat> = NonNullable<ToolApprovalPark[S]>;
432
+ /**
433
+ * 🔴 **S-934 ②(core 7.37.0 #1214)—— ask 类对的唯一窄读**:`askClass`(一问的类 = `classes` 的首员)与 `classes`(这一问属于的全部类,core
434
+ * `ASK_CLASSES` 序)。三个载体上同名:`AskRequest`(活卡帧 / 呈卡帧的卡读它)、寄存行 `PendingAction.tool_approval`(运维队列行经
435
+ * {@link parkRowFacts} 读它);`CheckpointSummary` 不回显(core 原句「read the row」)⇒ inbox 行不带。
436
+ *
437
+ * 判据(display,不是裁决输入 —— 裁决在 core 的仲裁点,本仓无人值守腿的按类作答读的是 core 交来的 `AskRequest.classes` 本身,不经本函数):
438
+ * · **两键都缺席 ⇒ 缺席**(不计数):老引擎(core 7.37.0 之前)、老行(续跑不补,KL-1214-b)、不盖 `origin` 的站(KL-1214-a)。缺席从不编默认类;
439
+ * · **在场 ⇒ 必须是一整只好对**:`askClass` 过 core `isAskClass`;`classes` 是非空数组、每员过 `isAskClass`、首员 === `askClass`(core d.ts
440
+ * 「`classes[0] === askClass`」「Present exactly when askClass is」)。半对 / 集外词 / 首员不符 ⇒ **两键都不投**(卡上少一行类 ≪ 卡上出现生词
441
+ * 或半只对)+ 计一次 `server.approval-card.closed-word-out-of-set`(S-249 族,detail 前缀 `askClass=` / `classes=` 判别是哪一格读不出)。
442
+ * · 不自造:本仓不重算类、不排序、不去重(core 的屏 `screenAskClassMembers` 在写停车行前已经筛过;它不在包根导出面上 —— 转 core 请求导出,
443
+ * 收车档 ⑤)。投出去的 `classes` 是新数组(帧不与载体共享可变引用)。
444
+ */
445
+ export declare function readAskClassPair(carrier: object): AskClassPair | undefined;
446
+ /** 类对的投影形(帧 / 卡 / 队列行同一形):两键同在同缺。值型取 core 寄存行上的座(core 改型 ⇒ 下面的围栏编译红)。 */
447
+ export interface AskClassPair {
448
+ askClass: NonNullable<ToolApprovalPark["askClass"]>;
449
+ classes: Array<NonNullable<ToolApprovalPark["classes"]>[number]>;
450
+ }
432
451
  /**
433
452
  * 🔴 S-700 P1(client-core CC-180 / cli L-652 的根子;板 [ref] 订正 → [ref] 立案)—— **park 行事实**:
434
453
  * 运维队列行(`GET /v1/approvals` + `/stream`)顶层展开的那几位。型**从 core 寄存行派生,不手抄**(S-249 同一条
@@ -446,8 +465,10 @@ type ClosedWordOf<S extends ClosedWordSeat> = NonNullable<ToolApprovalPark[S]>;
446
465
  * S-755(cli L-695 / [ref],client-core [ref])第五位 `origin`:谁提的这一问(core `ASK_ORIGINS` 闭集一词,经 {@link readClosedWordSeat};
447
466
  * 与活卡帧 `origin` / inbox 行 `origin` 同名同义同值)。此前只有活卡帧与 inbox 行带它 ⇒ 耐久重开 / 重连时壳从队列行读不到出身词,
448
467
  * bypass 姿态下按 `ask_rule` / `hook` 照问的那条壳规则无从落地。缺席 = 老行(core 7.5.0 之前 park 的)/ 集外词(计 S-249 tag)。
468
+ * S-934 ②(core 7.37.0 #1214)第六 / 七位 `askClass` / `classes`:这一问的类对(经 {@link readAskClassPair};两位同在同缺,与活卡帧 / 呈卡帧的卡
469
+ * 同名同义同值)—— durable 审批席按类答的依据。缺席 = 老行(core 7.37.0 之前 park 的,续跑不补)/ 读不出(计 S-249 tag)。
449
470
  */
450
- export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "ruleOffersAbsence" | "mandated" | "mandate" | "origin">;
471
+ export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "ruleOffersAbsence" | "mandated" | "mandate" | "origin" | "askClass" | "classes">;
451
472
  /**
452
473
  * S-700 —— park 行事实的**唯一投影**(`pendingAction` 是寄存行上 core 铸的那一只,读自持久层 ⇒ `unknown`)。
453
474
  *
@@ -629,6 +650,8 @@ export declare const ApprovalCardSchema: z.ZodObject<{
629
650
  }, z.core.$strict>>>;
630
651
  personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
631
652
  }, z.core.$strict>>;
653
+ askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>;
654
+ classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>>;
632
655
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
633
656
  sourceTaskId: z.ZodOptional<z.ZodString>;
634
657
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -742,6 +765,8 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
742
765
  }, z.core.$strict>>>;
743
766
  personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
744
767
  }, z.core.$strict>>;
768
+ askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>;
769
+ classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>>;
745
770
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
746
771
  sourceTaskId: z.ZodOptional<z.ZodString>;
747
772
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -787,6 +812,10 @@ export interface ApprovalCardSource {
787
812
  * `ApprovalCardSchema.ruleOffers`)。与 wire 帧**同一份素材** ⇒ live 帧 / `card_json` / 重放帧
788
813
  * 三面同源。 */
789
814
  ruleOffers?: readonly RuleOffer[];
815
+ /** S-934 ②:类对 —— 与 wire 帧**同一份素材**(发帧点经 `readAskClassPair` 窄读定;`ToolApprovalFrame` 结构上满足本接口)⇒ live 帧 /
816
+ * `approval_request` / `card_json` / 重放帧四面同源。两键同在同缺。 */
817
+ askClass?: AskClass;
818
+ classes?: readonly AskClass[];
790
819
  fromSubagent?: true;
791
820
  sourceTaskId?: string;
792
821
  /** 已 redactSecrets。 */
@@ -838,7 +867,7 @@ export interface ApprovalRequestFrame {
838
867
  /** = SSE 具名事件名(与 `tool_approval` 同约定)。 */
839
868
  type: "approval_request";
840
869
  schemaVersion: 1;
841
- /** `deriveAskId` 派生(持久层主键;五元组见 approval-ask-machine.ts)。 */
870
+ /** `deriveAskId` 派生(持久层主键;六元身份元组 —— S-944 / 7.109.0 起含这一问的类集 —— 见 approval-ask-machine.ts)。 */
842
871
  askId: string;
843
872
  /** wire run id(= 协调器 ctx 的 taskId)。 */
844
873
  taskId: string;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, isAskOrigin, isPersistedRuleMandate } from "@sema-agent/core";
2
+ import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, isAskClass, isAskOrigin, isPersistedRuleMandate } from "@sema-agent/core";
3
3
  import { redactSecrets } from "./trace/redact.js";
4
4
  import { recordFailOpen } from "./observability/fail-open.js";
5
5
  import { PERSISTED_RULE_MATCHES, UNCOVERED_SEGMENT_REASONS } from "./permission-rule-vocab.js";
@@ -284,18 +284,31 @@ const CLOSED_WORD_SEAT_GUARDS = {
284
284
  mandate: isPersistedRuleMandate,
285
285
  origin: isAskOrigin,
286
286
  };
287
+ export function readAskClassPair(carrier) {
288
+ const askClass = "askClass" in carrier ? carrier.askClass : undefined;
289
+ const classes = "classes" in carrier ? carrier.classes : undefined;
290
+ if (askClass === undefined && classes === undefined)
291
+ return undefined;
292
+ if (isAskClass(askClass) && Array.isArray(classes) && classes.length > 0 && classes.every(isAskClass) && classes[0] === askClass) {
293
+ return { askClass, classes: [...classes] };
294
+ }
295
+ recordFailOpen("server.approval-card.closed-word-out-of-set", isAskClass(askClass) ? `classes=${outOfSetDetail(classes)}` : `askClass=${outOfSetDetail(askClass)}`);
296
+ return undefined;
297
+ }
287
298
  export function parkRowFacts(pendingAction) {
288
299
  if (typeof pendingAction !== "object" || pendingAction === null || !("kind" in pendingAction) || pendingAction.kind !== "tool_approval")
289
300
  return {};
290
301
  const ruleOffersAbsence = readRuleOffersAbsence(pendingAction);
291
302
  const mandate = readClosedWordSeat(pendingAction, "mandate");
292
303
  const origin = readClosedWordSeat(pendingAction, "origin");
304
+ const askClassPair = readAskClassPair(pendingAction);
293
305
  return {
294
306
  ...(readParkRowTrueSeat(pendingAction, "requiresRealApproval") ? { requiresRealApproval: true } : {}),
295
307
  ...(readParkRowTrueSeat(pendingAction, "mandated") ? { mandated: true } : {}),
296
308
  ...(mandate !== undefined ? { mandate } : {}),
297
309
  ...(origin !== undefined ? { origin } : {}),
298
310
  ...(ruleOffersAbsence !== undefined ? { ruleOffersAbsence } : {}),
311
+ ...(askClassPair ?? {}),
299
312
  };
300
313
  }
301
314
  const DenialLimitFallbackSchema = z
@@ -337,6 +350,8 @@ export const ApprovalCardSchema = z
337
350
  classifierUnavailable: ClassifierUnavailableSchema.optional(),
338
351
  probeCause: ProbeCauseSchema.optional(),
339
352
  ruleEvidence: RuleEvidenceSchema.optional(),
353
+ askClass: z.custom((v) => isAskClass(v)).optional(),
354
+ classes: z.array(z.custom((v) => isAskClass(v))).min(1).optional(),
340
355
  fromSubagent: z.literal(true).optional(),
341
356
  sourceTaskId: z.string().max(MAX_IDENT).optional(),
342
357
  sourceAgentName: z.string().max(MAX_AGENT_NAME).optional(),
@@ -348,7 +363,17 @@ export const ApprovalCardSchema = z
348
363
  })
349
364
  .optional(),
350
365
  })
351
- .strict();
366
+ .strict()
367
+ .superRefine((card, ctx) => {
368
+ const hasClass = card.askClass !== undefined;
369
+ const hasClasses = card.classes !== undefined;
370
+ if (hasClass !== hasClasses) {
371
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [hasClass ? "classes" : "askClass"], message: "askClass and classes must be present together" });
372
+ }
373
+ else if (hasClass && card.classes[0] !== card.askClass) {
374
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["classes"], message: "classes[0] must equal askClass" });
375
+ }
376
+ });
352
377
  export const ApprovalCardEnvelopeSchema = z
353
378
  .object({
354
379
  schemaVersion: z.literal(1),
@@ -377,6 +402,7 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
377
402
  const ruleEvidence = readRuleEvidence(req);
378
403
  const ruleOffersAbsence = readRuleOffersAbsence(req);
379
404
  const denialLimitFallback = readDenialLimitFallback(req);
405
+ const askClassPair = readAskClassPair(source);
380
406
  const toolCallId = clip(source.toolCallId, MAX_IDENT);
381
407
  const sourceTaskId = clip(source.sourceTaskId, MAX_IDENT);
382
408
  const sourceAgentName = clip(source.sourceAgentName, MAX_AGENT_NAME);
@@ -399,6 +425,7 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
399
425
  ...(source.ruleOffers !== undefined && source.ruleOffers.length > 0 ? { ruleOffers: source.ruleOffers.map(copyRuleOffer) } : {}),
400
426
  ...(ruleOffersAbsence !== undefined ? { ruleOffersAbsence } : {}),
401
427
  ...(denialLimitFallback !== undefined ? { denialLimitFallback } : {}),
428
+ ...(askClassPair !== undefined ? { askClass: askClassPair.askClass, classes: askClassPair.classes } : {}),
402
429
  ...(source.fromSubagent === true ? { fromSubagent: true } : {}),
403
430
  ...(sourceTaskId !== undefined ? { sourceTaskId } : {}),
404
431
  ...(sourceAgentName !== undefined ? { sourceAgentName } : {}),
@@ -35,7 +35,6 @@
35
35
  * `infraFailed` (EXCLUDED) — NEVER a fabricated uniform delivered=false loss for the whole TEAM column.
36
36
  */
37
37
  import { type TaskSpec } from "@sema-agent/core";
38
- import { type UnattendedLeg } from "../../unattended-permission-mode.js";
39
38
  import type { ProfileDeps, SoloImplSpec, SupImplSpec } from "./arms.js";
40
39
  import type { TrapSpec } from "./tasks.js";
41
40
  /** Live runtime config — the SAME base model + budget every arm runs on (the §3.2 fairness root). */
@@ -96,11 +95,11 @@ export declare function benchBaseSpec(rt: LiveRuntimeConfig, cellId: string): (o
96
95
  /**
97
96
  * Project a `SoloImplSpec` (budget already stamped by the harness, all keys in `limits`) onto the bench base TaskSpec.
98
97
  * 🔴 7.106.0 合并复审 X3:SOLO 是**无人值守腿**(verify→fix 跑在一只不接 `onAsk` 的 Runner 上,没有审批席)⇒ 经 `withUnattendedLeg` 接
99
- * 与 TEAM 的 leader worker 同一条腿(模式 = `UNATTENDED_PERMISSION_MODE`、写面 roots、写保护拒层)。修前裸 spec ⇒ core 7.34.0「缺席 =
100
- * default」让写与非只读命令 headless 拒、终态照旧 completed —— SOLO 与 TEAM 的比较混进了权限差异。SUP 不走这里:它有耐久审批席
101
- * (checkpoint suspend),它的问要人答,bypass 会把 SUP 的门放掉。
98
+ * 与 TEAM 的 leader worker 同一只合并口(S-934 ①:`default` + 按类作答的 onAsk,无席)。修前裸 spec ⇒ core 7.34.0「缺席 =
99
+ * default」且无席 ⇒ 写与非只读命令 headless 拒、终态照旧 completed —— SOLO 与 TEAM 的比较混进了权限差异。SUP 不走这里:它有耐久审批席
100
+ * (checkpoint suspend),它的问要人答。
102
101
  */
103
- export declare function toTaskSpec(base: (objective: string) => TaskSpec, impl: SoloImplSpec, leg: UnattendedLeg): TaskSpec;
102
+ export declare function toTaskSpec(base: (objective: string) => TaskSpec, impl: SoloImplSpec): TaskSpec;
104
103
  /** Project a `SupImplSpec` onto the bench base TaskSpec PLUS the durable wiring (checkpointStore + ask policy). */
105
104
  export declare function toSupTaskSpec(base: (objective: string) => TaskSpec, impl: SupImplSpec, trap: TrapSpec): TaskSpec;
106
105
  //# sourceMappingURL=live-deps.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { outwardPlane } from "../../terminal.js";
2
2
  import { Runner, runWithVerification, resumeWithVerification, runRepairLoop, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, } from "@sema-agent/core";
3
3
  import { loadConfig } from "../../config.js";
4
- import { createUnattendedLeg, unattendedPermissionMode, withUnattendedLeg } from "../../unattended-permission-mode.js";
4
+ import { withUnattendedLeg } from "../../unattended-permission-mode.js";
5
5
  import { createBrain } from "../../brain.js";
6
6
  import { buildPricing } from "../../budget.js";
7
7
  import { RemoteContainerExecutionEnv } from "../../plugins/remote-env-e2b.js";
@@ -58,7 +58,7 @@ function buildBrainIngredients(rt) {
58
58
  if (!p || ((p.inputPer1M ?? 0) === 0 && (p.outputPer1M ?? 0) === 0)) {
59
59
  throw new Error(`s1 live: zero pricing for ${rt.modelId} — C1 cost axis would be a fabricated $0 (set MODEL_COST_INPUT/OUTPUT)`);
60
60
  }
61
- return { brain, models: config.models, roles: config.roles, pricing, unattendedPermissionMode: unattendedPermissionMode(config) };
61
+ return { brain, models: config.models, roles: config.roles, pricing };
62
62
  }
63
63
  const sh = (env) => async (cmd) => {
64
64
  const r = await env.exec(`bash -lc ${JSON.stringify(cmd)}`);
@@ -168,9 +168,8 @@ function graderTransport(grader) {
168
168
  };
169
169
  }
170
170
  export function buildLiveDeps(rt, trap, seed, cellId) {
171
- const { brain, models, roles, pricing, unattendedPermissionMode: leaderMode } = buildBrainIngredients(rt);
171
+ const { brain, models, roles, pricing } = buildBrainIngredients(rt);
172
172
  const baseSpec = benchBaseSpec(rt, cellId);
173
- const soloLeg = createUnattendedLeg(leaderMode, {});
174
173
  const state = {};
175
174
  const ensureWorker = async () => {
176
175
  if (!state.worker) {
@@ -208,7 +207,7 @@ export function buildLiveDeps(rt, trap, seed, cellId) {
208
207
  const deps = {
209
208
  runWithVerification: async (implSpec) => {
210
209
  const { runner } = await ensureWorker();
211
- return runWithVerification(runner, toTaskSpec(baseSpec, implSpec, soloLeg), { maxRounds: rt.maxRounds ?? 2 });
210
+ return runWithVerification(runner, toTaskSpec(baseSpec, implSpec), { maxRounds: rt.maxRounds ?? 2 });
212
211
  },
213
212
  runWithVerificationSup: async (implSpec) => {
214
213
  const { runner } = await ensureWorker();
@@ -284,7 +283,7 @@ export function buildLiveDeps(rt, trap, seed, cellId) {
284
283
  rmSync(tmp, { recursive: true, force: true });
285
284
  }
286
285
  };
287
- return runLeaderForTrap(rt, brain, models, roles, pricing, leaderMode, trap, cellId, captureIntegratedTreeToBundle);
286
+ return runLeaderForTrap(rt, brain, models, roles, pricing, trap, cellId, captureIntegratedTreeToBundle);
288
287
  },
289
288
  runOracle: () => runOracle(false),
290
289
  };
@@ -303,8 +302,8 @@ export function benchBaseSpec(rt, cellId) {
303
302
  function projectImplBudget(base, impl) {
304
303
  return { ...base(impl.objective), limits: { ...impl.limits } };
305
304
  }
306
- export function toTaskSpec(base, impl, leg) {
307
- return withUnattendedLeg(projectImplBudget(base, impl), leg);
305
+ export function toTaskSpec(base, impl) {
306
+ return withUnattendedLeg(projectImplBudget(base, impl));
308
307
  }
309
308
  export function toSupTaskSpec(base, impl, trap) {
310
309
  const requireApproval = trap.gatedTools ?? ["Bash"];
@@ -317,7 +316,7 @@ export function toSupTaskSpec(base, impl, trap) {
317
316
  onQuestion: QUESTION_AWAITS_RESUME,
318
317
  };
319
318
  }
320
- async function runLeaderForTrap(rt, brain, models, roles, pricing, leaderMode, trap, cellId, importIntegratedTreeToWorker) {
319
+ async function runLeaderForTrap(rt, brain, models, roles, pricing, trap, cellId, importIntegratedTreeToWorker) {
321
320
  const { mkdtempSync, rmSync, writeFileSync, mkdirSync } = await import("node:fs");
322
321
  const { execFileSync, spawn } = await import("node:child_process");
323
322
  const { tmpdir } = await import("node:os");
@@ -370,7 +369,7 @@ async function runLeaderForTrap(rt, brain, models, roles, pricing, leaderMode, t
370
369
  process.env.LEADER_ENABLED = "true";
371
370
  process.env.LEADER_WORKER_BUDGET_USD = String((rt.maxCostUsd ?? 1.0) / 2);
372
371
  const run = createLeaderRunner({
373
- brain, models, roles, pricing, unattendedPermissionMode: leaderMode, e2bApiKey: rt.e2bApiKey, workspace: W,
372
+ brain, models, roles, pricing, e2bApiKey: rt.e2bApiKey, workspace: W,
374
373
  ...(s1DebugGit() ? { logger: { warn: (m, x) => console.error(`[S1_DEBUG_LEADER warn] ${m}`, x ? JSON.stringify(x).slice(0, 300) : ""), info: (m, x) => console.error(`[S1_DEBUG_LEADER info] ${m}`, x ? JSON.stringify(x).slice(0, 200) : "") } } : {}),
375
374
  });
376
375
  const teamSeedWrite = (trap.seedFiles ?? [])
@@ -2,7 +2,6 @@ import { createLeaderEndpoint } from "../leader/endpoint.js";
2
2
  import { createLeaderRunner } from "../leader/wire.js";
3
3
  import { buildDeploymentPostureSeats } from "./runner-deps.js";
4
4
  import { hasApprovalListIntent } from "../approval.js";
5
- import { unattendedPermissionMode } from "../unattended-permission-mode.js";
6
5
  export function leaderK8sMinioGap(leaderEnabled, leaderProvider, hasS3) {
7
6
  if (!(leaderEnabled && leaderProvider === "k8s" && !hasS3))
8
7
  return null;
@@ -59,7 +58,6 @@ export function createLeaderFace(ctx) {
59
58
  : {}),
60
59
  ...(ctx.promptSource ? { promptSource: ctx.promptSource } : {}),
61
60
  ...(ctx.onError ? { onError: ctx.onError } : {}),
62
- unattendedPermissionMode: unattendedPermissionMode(config),
63
61
  fanoutEnabled: config.leaderFanoutEnabled,
64
62
  ...((process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID)
65
63
  ? { routerModel: (process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID) }
@@ -17,7 +17,7 @@ import type { StoreBackend } from "../plugins/store-backend.js";
17
17
  import { type TaskAttachmentStore } from "../plugins/task-attachment-store.js";
18
18
  import { type createPrincipalEntitlementsClient } from "../runtime-caps-resolver.js";
19
19
  import { type OwnerAwareSessionStore } from "../security.js";
20
- import { type RunnerDepsOnAsk } from "./runner-deps.js";
20
+ import type { RunnerDepsOnAsk } from "./runner-deps.js";
21
21
  type PrincipalCaps = ReturnType<typeof createPrincipalEntitlementsClient>;
22
22
  /** `resolveSpec` 原先从 `main()` 闭包里拿到的全部 boot 局部量。 */
23
23
  export interface ResolveSpecCtx {
@@ -40,8 +40,7 @@ import { redactSecrets } from "../trace/redact.js";
40
40
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
41
41
  import { executionLaneCaps, executionLaneHomeDir, executionLaneOf } from "../execution-lane-caps.js";
42
42
  import { effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
43
- import { buildDeploymentPostureSeats } from "./runner-deps.js";
44
- import { createUnattendedLeg, unattendedPermissionMode, withUnattendedLeg } from "../unattended-permission-mode.js";
43
+ import { withUnattendedLeg } from "../unattended-permission-mode.js";
45
44
  import { memoryFaceMounted } from "../memory-scope.js";
46
45
  import { createAssemblyIncarnationRead, sessionFaceReaderOf } from "./session-incarnation-seat.js";
47
46
  export function createResolveSpec(ctx) {
@@ -440,7 +439,7 @@ export function createResolveSpec(ctx) {
440
439
  const merged = extra.length > 0 ? [...(base ?? []), ...extra] : base;
441
440
  return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
442
441
  })(cap.tools),
443
- skills: mergeUserSkills(cap.skills, body.skills, logger),
442
+ skills: body.disableSkills === true ? [] : mergeUserSkills(cap.skills, body.skills, logger),
444
443
  ...(parsedSettings.settings?.skillListingBudgetFraction !== undefined ? { skillListingBudgetFraction: parsedSettings.settings.skillListingBudgetFraction } : {}),
445
444
  ...(parsedSettings.settings?.skillListingMaxDescChars !== undefined ? { skillListingMaxDescChars: parsedSettings.settings.skillListingMaxDescChars } : {}),
446
445
  mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined),
@@ -670,15 +669,11 @@ export function createResolveSpec(ctx) {
670
669
  }
671
670
  return governed;
672
671
  };
673
- const resumeRunnerSeats = () => ({
674
- ...buildDeploymentPostureSeats(config),
675
- ...(memoryEngine !== undefined ? { memoryEngineDir: memoryEngine.root } : {}),
676
- });
677
672
  const resolveSpecForLeg = async (body, req, auth, opts) => {
678
673
  const spec = await resolveSpec(body, req, auth, opts);
679
674
  if (opts?.unattendedLeg !== true)
680
675
  return spec;
681
- return withUnattendedLeg(spec, createUnattendedLeg(unattendedPermissionMode(config), resumeRunnerSeats()));
676
+ return withUnattendedLeg(spec);
682
677
  };
683
678
  return resolveSpecForLeg;
684
679
  }
@@ -246,8 +246,9 @@ export declare function centerScenarios(deps: ScenarioDeps, specs: CenterScenari
246
246
  };
247
247
  /**
248
248
  * Personalization seam: merge the caller's own skills AFTER the scenario's, with
249
- * SCENARIO-WINS on a name clash — the security baseline (scenario/center skills) can never be shadowed
250
- * by user content. Duplicate names within the user list keep the first. Clashes are logged for audit
249
+ * SCENARIO-WINS on a name clash — the scenario / center skills own the name (they are the deployment's baseline
250
+ * wording, not an enforcement face: permissions / write gates / hooks do not ride on skills, and a caller may
251
+ * unload the whole set with `disableSkills`, S-946); user content never shadows them. Duplicate names within the user list keep the first. Clashes are logged for audit
251
252
  * (silent drops would make "my skill didn't fire" undebuggable). Input is already shape/size-validated
252
253
  * by the HTTP layer (validateUserSkills); this stays pure merge policy.
253
254
  */
@@ -1,10 +1,9 @@
1
- import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, PERMISSION_MODES } from "@sema-agent/core";
1
+ import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } from "@sema-agent/core";
2
2
  import { CENTER_CREDENTIAL_ENV, DOMAIN_SCHEMAS } from "@sema-agent/settings-schema";
3
3
  import { CENTER_CREDENTIAL_RENEW_LEAD_DEFAULT_MS, CENTER_CREDENTIAL_RENEW_LEAD_MAX_MS, CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS } from "./center-credential-renewer.js";
4
4
  import { THINKING_FORMATS } from "./model-compat.js";
5
5
  import { AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, CONFIG_PROVIDER_WORDS, DB_BACKEND_WORDS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
6
6
  import { webSearchProviderFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
7
- import { UNATTENDED_PERMISSION_MODE_DEFAULT } from "./unattended-permission-mode.js";
8
7
  import { AUTONOMY_MODES } from "./runtime-governance.js";
9
8
  import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
10
9
  import { MODEL_PROVIDER_WORDS } from "./model-provider.js";
@@ -454,7 +453,6 @@ export const CONFIG_CATALOG = [
454
453
  r("TRACE_THINKING", "observability", "boolean", "思考轨迹入 durable 事件日志(敏感部署可关;落盘前脱敏)", { staticDefault: "true", danger: DATA, configKey: "traceThinking" }),
455
454
  r("TRACE_TOKEN", "observability", "secret", "Task Trace API 只读令牌(fleet 拉轨迹,不授任务/配置权)", { danger: SEC }),
456
455
  r("UNATTENDED_APPROVAL_POLICY", "approval", "enum", "无人能答的治理 ask 终局(park=durable 等人;deny=当场拒 —— 自声明无人值守)", { enumValues: ["park", "deny"], staticDefault: "park", danger: SEC, configKey: "unattendedApprovalPolicy" }),
457
- r("UNATTENDED_PERMISSION_MODE", "approval", "enum", "无人值守腿(leader worker / leader 修复腿 / run-local:本仓自铸 spec、无调用方可表态、无审批席)的权限模式;写面恒 roots(不随 bypassPermissions 开放);词表外拒启", { enumValues: [...PERMISSION_MODES], staticDefault: UNATTENDED_PERMISSION_MODE_DEFAULT, danger: SEC, configKey: "unattendedPermissionMode" }),
458
456
  r("UNREACHED_ASK_TTL_MS", "approval", "number", "零活流铸造的 ask 悬挂窗(ms;[60s,24h] 拒启带界)", { staticDefault: "3600000" }),
459
457
  r("USAGE_WINDOWS", "orchestration", "opaque", "部署级治理窗(JSON 数组;滑动窗内跨任务 token/$ 预算,key=principal)", { danger: BILL }),
460
458
  r("WEB_SEARCH_API_KEY", "orchestration", "secret", "web search 后端 API 凭据(brave/tavily)", { danger: [...SEC, ...BILL] }),
@@ -125,6 +125,7 @@ export function mapToServiceEffective(eff, version) {
125
125
  ...(m.cost !== undefined ? { cost: m.cost } : {}),
126
126
  ...(m.extraBody !== undefined ? { extraBody: m.extraBody } : {}),
127
127
  ...(m.compat !== undefined ? { compat: m.compat } : {}),
128
+ ...(m.headers !== undefined ? { headers: m.headers } : {}),
128
129
  ...(m.promptGuidance !== undefined ? { promptGuidance: m.promptGuidance } : {}),
129
130
  enabled: m.enabled,
130
131
  })),