@sema-agent/server 7.106.0 → 7.107.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/MIGRATION.md CHANGED
@@ -7,6 +7,29 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.107.0(车SE,S-907 / S-890;车SF)—— `@sema-agent/core` 7.36.0:post 钩子收 wire 名 · 双引擎名声明拒 · 工具调用 id 越界折叠(服务商拿回原串)· 两只恒真能力位删
11
+
12
+ - **🔴 hooks:PostToolUse / PostToolUseFailure 的 matcher 与 payload `tool_name` 改为调用派发到的工具的 wire 名**(此前是模型拼写;PreToolUse 本来就是 wire 名)。
13
+ 按工具**别名**写的 post matcher(及条目 `if` 条件的名部)不再命中(只影响执行后面,不放宽任何决定)。**迁移**:matcher 写 wire 名;滚动升级期两名都写(本服务不做双读)。
14
+ PostToolBatch 不变(条目 `tool_name` 仍是拼写,matcher 本就被忽略)。hook 故障通告的 `toolName` 在 post 两事件上同步变为 wire 名。
15
+ - **🔴 声明:一只工具的名字(正名 + 别名,按解析后计)落到两只引擎工具上 ⇒ run 在准备阶段被拒 `config.tool_face_invalid`**(零轮;同步口 200 + failed 体)。
16
+ 只影响经 `resolveSpec` 席自带工具声明的嵌入方。**迁移**:一只工具只答一只引擎工具的名。
17
+ - **行为面(收紧)**:调用方工具声明低不可逆档又挂 `Bash` / `Monitor` 别名 ⇒ 座取最严的名(`default` 多一张卡、`dontAsk` 从跑变拒)。**迁移**:去掉那个别名,或接受 shell 座。
18
+ - **wire 值:服务商给的工具调用 id 越界(控制字节 / 空格 / 非 ASCII / `~` / 129 字符以上)时,本栈各面的 `toolCallId` 是引擎的折叠形** `<前 32 码元>~<32 hex>`
19
+ (恒在形、确定、单射;在形 id 逐字不变)。服务商自己的那串在转录 `ToolCall.wireId` / side-query `toolCalls[].wireId`,引擎回给服务商的调用 / 结果恒用它
20
+ (⚠️ PostgreSQL 会话后端:原串含 NUL / 孤立代理码元时经落店重载会被清洗成 U+FFFD —— 既有限制,7.106.0 同病落在调用 id 本身)。
21
+ **零动作**(join 照旧成立);与服务商日志对账的运维读 `wireId`,或服务端结构化日志 `engine_notice` `route.tool_call_id_folded`(每 run 一行,operator 受众,不上会话流)。
22
+ 升级前已 park 的行以原串为键,新版 resume 不再折叠(askId 不变、卡照旧可答)。
23
+ - **耐久停车行 `pendingAction.names` 的序改「拼写先」**(门判定那张脸)。本服务任何 wire 面都不投 `names`;只影响直读店行(SQL `checkpoint` 列 / local 检查点文件)的工具 —— 按 `names[0]` 猜 wire 名的改读 `toolName`。
24
+ - **🔴 wire(能力面,车SF S-899):`GET /v1/capabilities` 的 `modeShellGateTranslation` 与 `permissionModeAuto.intentArming` 删**(两位 7.106.0 起恒 `true`,零信息)。
25
+ **迁移**:壳不再按 `modeShellGateTranslation` 决定是否注 `MANUAL_MODE_SHELL_GATE`(缺席不是「老 server」;直接不注);`intentArming` 的读者改读 `armed` / `reason`。**真实后果**:已发布 cli ≤1.0.139 的引擎附着门把缺席读成旧引擎 ⇒ 同机多安装下 headless `-p` 拒复用 ≥7.107.0 引擎、交互车道误报一行(fail-closed 有出路:停那台引擎或升级较旧的 cli 安装 ≥1.0.140);sdk 14.0.0 仍把 `intentArming` 写成必填(门 6 红在围栏头校准 7.106.0),随 sdk 14.1.0 删两位 + 抬围栏头。
26
+ - **请求面模式词表(S-902)**:五词受理集不变、`dontAsk` 仍 400 `request.field_invalid`;三处 400 拒句里的词序改随 core `PERMISSION_MODES` 序(文案值变、码不变)—— 按字节比对拒句的测试改比码。
27
+
28
+ ### 升级须知补三条(S-923 + 合并复审)
29
+ - **工具名帧的拼写**:fresh 腿 `tool_start` / `tool_end.toolName` 是模型拼写(与 7.106.0 同);升级前 park 的行续跑仍给 wire 名(`names` wire 名在先);**只有 7.107.0 起按别名 park 的调用**续跑帧改给拼写。壳按 `toolCallId` join,不按帧的 `toolName` 归一;要 wire 名读 PreToolUse / post 钩子上下文的 `toolName`、`wiring_manifest.tools` 名册或停车行 `pendingAction.toolName`(帧不能反推)。
30
+ - **把引擎手工具名挂成别名的嵌入方**(`aliases:["Read"]` 一类):中断调用按较不安全效应判、效应面按较不安全效应记(只在该名挂载不重登效应时)、结果预算不再豁免、微压缩清理不再达它(上下文可能变大)—— 升级前排空这类声明下的 park,或接受更保守判定;一只名字属于两只引擎工具的声明在 prepare 被拒(`config.tool_face_invalid`),已 park 的调用续跑不动,改声明后重试。
31
+ - **持久面反向不兼容边界(回滚 / 共享存储混版)**:7.107.0(core 7.36.0 #1207)起,服务商给的**越界** tool-call id(字符集外或 >128)在转录 / 检查点里落**折叠形**,原串只在同行 `wireId`;≤7.106.0 的实例(core 7.35.2)不读 `wireId`,续跑这类会话时会把折叠 id 发回网关 ⇒ 要求回传原 id 的网关拒绝后续请求(在形 id 零差异,不受影响;7.35.2 的 pre-CAS 复核按 `names` 先读拼写、取座可能不同)。**约束**:同一存储上不要让 ≤7.106.0 实例接管 7.107.0 已写过折叠 id 的会话;回滚前排空这类会话(会话 `messages[]` 里出现 `wireId` 的行即受影响),或接受重开。
32
+
10
33
  ## 7.106.0(车QY,S-877 / S-881)—— `@sema-agent/core` 7.35.2(7.35.0 的全部变更 + #1206 新会话锁 + 看门狗 30s 等待披露):一只权限模式 · `approverPosture` / `taskApproverPosture` 删 · `MANUAL_MODE_SHELL_GATE=classify` 拒启 · 宿主子进程 env 按 CC 形剥(git 配置族与代理族原样到达)
11
34
 
12
35
  - **🔴 wire:提交体 `approverPosture` 删,零别名。** 仍带它 ⇒ 400 `request.body_shape`(`unknownKeys:["approverPosture"]`)。**迁移**:删掉这个键;要「宿主自动答卡」的效果用
package/README.md CHANGED
@@ -186,8 +186,8 @@ The server is configured entirely through environment variables. The most import
186
186
  | `SENSITIVE_WRITE_PATTERNS` | core's recommended set | Sensitive-path write deny list; comma-separated value replaces the set, `off` **or an empty value** disables. Applied unconditionally at the governance layer (independent of client permission mode, lane or settings presence) — including on the `run-local` leg. A value that cannot compile into a guard set (e.g. `/`, a pattern with no path segment) refuses to start |
187
187
  | `WRITE_PROTECTED_EXTRA` | unset (engine default table) | **Adds** rows to core's write-protection table (the literal-name table whose hit demotes a surviving `allow` to `ask` on Write/Edit/NotebookEdit). Comma-separated bare names (a bare name matches ANY path segment; a name containing `/` matches a consecutive segment run) or a JSON array of `"name"` strings / `{name, kind}` rows (`kind`: `basename` \| `segment` \| `segment-run`). The value is composed as `[...WRITE_PROTECTED_DEFAULT_TABLE, …]`, so **no default row can be lost**. Unset = no seat is wired = the engine's default table is in force (this server never copies that table). Form discrimination is by content, not by first character: a value containing any JSON structural character (`[ ] { } "`) is parsed as JSON and **must** be a top-level array (a missing pair of brackets refuses to start instead of being split into junk bare names). An empty value, a malformed entry (glob metacharacter, unknown kind, kind/name mismatch) or setting this together with `WRITE_PROTECTED_TABLE_REPLACE` **refuses to start**. Read faces: `GET /v1/capabilities` → `writeProtection.{armed,rows,replaced}`; `GET /v1/diagnostics/wiring` → `writeProtection.{rows,source,droppedDefaultRows}` (operator-only) |
188
188
  | `WRITE_PROTECTED_TABLE_REPLACE` | unset (engine default table) | **Replaces the whole** write-protection table (core's seat is whole-table by contract). JSON array only — deliberately no comma shorthand, because a slipped bare string would swap 51 default rows for one. `[]` = the explicit "no write-protection table at all" posture. Replacing logs one **loud** boot line naming every default row you dropped (`write_protection_table_replaced`; the empty posture logs `write_protection_table_disabled`) — use `WRITE_PROTECTED_EXTRA` when you meant to ADD. Same refuse-to-start conditions as its sibling, plus: both knobs set = two writers on one surface = refuses to start |
189
- | `MANUAL_MODE_SHELL_GATE` | unset | `always`\|`classify` — tighten `Bash` into the approval chain, applied unconditionally at the governance layer (≥7.1.0: independent of client permission mode, lane, or settings presence). **Unset is not "off"**: since 7.12.0 the caller's explicit `permissionMode` supplies the baseline this knob tightens from (`bypassPermissions` → `off`, `auto`/`default`/`acceptEdits`/`plan` → `classify`; **no** mode stated → core's `off` default). Since 7.73.0 (core 7.15.0) `off` no longer means "no gate at all": the READ BOUNDARY (built-in read-deny tiers + workspace containment) is judged under **every** doctrine, and `shellGate` governs only the RESIDUAL shell risk. Since 7.92.0 (core 7.25.0) that boundary is ONE read station judged before approval, and its three outcomes differ: a **deny-listed read is REFUSED** on the `off` lane as on every other face — no card, no stored rule and no approver's yes can release it (`tool_end.errorCode`/`structured` = `read_path_denied` with the matched `target`/`pattern`, `gate.disposition.deniedBy = "read_boundary"`); a **recursive read form is ENUMERATED** — it runs with zero asks when no deny row sits under the tree, is refused naming that row when one does, and still raises ONE mandated approval when the tree is too large to enumerate; an **out-of-root read** is unchanged and still raises exactly ONE mandated approval (no stored rule and no auto-mode classifier can clear it). Non-readers (`rm`/`curl`/`git`/`npm`) and ordinary in-workspace reads stay unasked on that lane. This knob only ever raises that baseline — it has no relax half, so `off` is accepted as an explicit **no-op** (a boot line says so; not symmetric with `SENSITIVE_WRITE_PATTERNS=off`, which really does clear a set). **Any other value refuses to start** (7.12.0, BREAKING for a deployment that had a typo: it was previously treated as unset, i.e. silently no gate at all) |
190
- | `PERMISSIONS_DISABLE_AUTO_MODE` | `false` | Local mirror of CC `permissions.disableAutoMode` — the **org deny** bit for `permissionMode:"auto"` (paired with core's intent-arming rule). **Tighten-only**: `true` folds every principal's `runtimeCaps.autoMode` to `false` (a center grant cannot flip it back); unset leaves caps untouched, so on a center-less box a shell asking for `auto` **arms** the classifier once the paired core (intent-arming rule "requested ∧ classifier seat ∧ `autoMode !== false`" — absence is not a deny) is installed; on core 7.2.0 the engine still uses the old "org grant" rule (`permissionModeAuto.intentArming:false`), so the self-check answers `armed:true` only for a center-granted principal and `deployment_incapable` on a center-less box. Boolean word table; any other value refuses to start. Self-check: `GET /v1/capabilities?permissionMode=auto` → `permissionModeAuto.{armed, reason, model}` (USAGE §9.4) |
189
+ | `MANUAL_MODE_SHELL_GATE` | unset | `always` — the operator floor: keep every `Bash` call under the engine's shape-tier approval question regardless of the client's permission mode, lane or settings (applied at the governance layer). **Unset is not "off"** and `off` is a no-op (the knob only tightens); `classify` was retired in 7.106.0 (core 7.34.0 folds the classifier into the engine's own mode arbitration) and now **refuses to boot** (the boot error names the retirement and the two remaining words; no error code — it is a config parse refusal, not a wire code). The catalog row (`GET /v1/config/catalog`) lists `enumValues: ["always","off"]`, `echoWords: ["always"]`. |
190
+ | `PERMISSIONS_DISABLE_AUTO_MODE` | `false` | Local mirror of CC `permissions.disableAutoMode` — the **org deny** bit for `permissionMode:"auto"` (paired with core's intent-arming rule). **Tighten-only**: `true` folds every principal's `runtimeCaps.autoMode` to `false` (a center grant cannot flip it back); unset leaves caps untouched, so on a center-less box a shell asking for `auto` **arms** the classifier once the paired core (intent-arming rule "requested ∧ classifier seat ∧ `autoMode !== false`" — absence is not a deny) is installed; the old "org grant" rule is gone since server 7.106.0 (core 7.34.0: one arming polarity; the `permissionModeAuto.intentArming` member that used to report it was removed in 7.107.0). Boolean word table; any other value refuses to start. Self-check: `GET /v1/capabilities?permissionMode=auto` → `permissionModeAuto.{armed, reason, model}` (USAGE §9.4) |
191
191
  | `SCRATCHPAD_SWEEP_TTL_MS` | 7 days | Idle-reap window for per-session scratchpad dirs (by dir mtime; `0` disables). The scratchpad is **ephemeral by contract**: replica-local disk, NOT part of the durable-suspend persistence set — a resume on a different replica, or after a sweep, starts with an empty dir (same two-track posture as the Agent SDK hosting doc: conversation persists, working-directory artifacts don't). Raise/disable only on single-replica deployments that park approvals for longer than the window |
192
192
  | `MODEL_FIRST_BYTE_TIMEOUT_MS` | unset (core's own default) | The wait from `fetch()` to the response headers, ms (7.103.0; replaces the retired `MODEL_CONNECT_TIMEOUT_MS`, which now **refuses to start** — the TCP + TLS connect is the transport's own clock since core 7.33.0). Unset = no key is passed, core's default applies (plus an upload allowance by request size, and one widened retry of its own). `0` = no engine clock on that segment and the transport's header / body clocks off too (the TCP + TLS connect keeps the transport's own clock). A whole number in `[0, 2147483647]`, anything else refuses to start. With `MODEL_GATEWAY_FALLBACK_URLS` set every gateway route likewise gets the first window plus one widened retry before failover (the server does not pass core's `firstByteRetries`: a caller can turn failover off per request, and that call has no backup behind it). A failover topology must set this knob explicitly: unset, a primary that accepts the connection but never answers is cut over only after core's first-byte lane (default window plus the widened retry) runs out, and a call with a shorter time budget (code-review council lenses, 180 s) is cancelled before the backup is tried; at boot such a deployment logs one `failover_first_byte_lane` warn |
193
193
  | `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `600000` | First-token timeout (the only watchdog for a stream that opens and never emits; `0` = no engine clock on that segment, the transport's body clock off too. Raised from `120000` in 7.71.0 — a self-hosted backend's long prefill legitimately takes minutes to the first byte; set `120000` to restore the old posture) |
package/USAGE.md CHANGED
@@ -1933,11 +1933,11 @@ armed ⟺ 本次请求 permissionMode 是 "auto" ∧ 本部署装配了分类
1933
1933
  | `CROSS_SESSION_DIALOG_EXPIRY` | 未设(⇒ 引擎缺省 `5m`) | 同族:CC settings 键名 `dialogExpiry` 的部署层 —— 被 hold 的跨会话消息等待人审多久后按**安全缺省**结算(过期丢弃,**带回执**告知发送方,不静默吞)。词表 `60s`/`5m`/`10m`/`never`(`never` = 不设期限);未设 ⇒ 本仓**不铸键**,由引擎落它自己的缺省(不复制上游缺省值)。词表外拒启。⚠️ 生效前提同上。另注:CC 的同名键还有第二个消费面(转发到远端客户端的审批对话框停靠时长),本旋钮**今天不驱动**那一面 |
1934
1934
 
1935
1935
  **自查读面**(`GET /v1/capabilities.permissionModeAuto`,壳的 `sema doctor permissions` 消费;server 7.106.0 起引擎只剩一种武装极性,
1936
- `intentArming` 恒 `true`,见该条):
1936
+ 7.107.0 起成员 `intentArming` 删除,见该条):
1937
1937
 
1938
1938
  ```
1939
1939
  GET /v1/capabilities?permissionMode=auto
1940
- → permissionModeAuto: { accepted:true, classifierSeat:true, entitlementSource:false, intentArming:true, armed:true, model:"default" }
1940
+ → permissionModeAuto: { accepted:true, classifierSeat:true, entitlementSource:false, armed:true, model:"default" }
1941
1941
  GET /v1/capabilities?permissionMode=default
1942
1942
  → permissionModeAuto: { …, armed:false, reason:"mode_not_auto" }
1943
1943
  ```
@@ -1949,8 +1949,8 @@ GET /v1/capabilities?permissionMode=default
1949
1949
  center 硬失败 fail-closed 拒项)/ `local_denied`(本地旋钮 `PERMISSIONS_DISABLE_AUTO_MODE=true`,改本机 env)/ `settings_denied`
1950
1950
  (settings 层 `permissions.disableAutoMode:"disable"`——本面不产,归 run 级面)/ `resolver_fault`(per-principal resolver 抛错,
1951
1951
  引擎同形全拒)。
1952
- - `intentArming`:7.106.0 起恒 `true`(引擎只剩意图武装式一种极性:无 center 也能 auto);≤7.105.0 由安装包探针答,`false` = 旧式
1953
- 「组织授予」极性。键名与值形不动,整键删除见 7.107.0。
1952
+ - `intentArming`:**7.107.0 删除**(S-899)。7.57.0–7.105.0 由安装包探针答(`false` = 旧式「组织授予」极性),7.106.0 恒 `true`(引擎只剩意图武装式
1953
+ 一种极性:无 center 也能 auto)—— 不携带信息;「会不会武装」读 `armed` / `reason`。
1954
1954
  - `model`:分类器**配置**路由(role 回落序 `classifier → summarize → default` 的 catalog 名),不是本次真用的模型。
1955
1955
  - `entitlementSource` 仍只说「center 源在不在」;新武装式下它为 `false` **不再**意味着 auto 不武装。
1956
1956
  - 读面按调用方身份解 per-principal caps(与引擎同一只 resolver,per-principal 缓存 60s;center 硬失败时受 8s
@@ -46,7 +46,7 @@ export declare const COMPOSITE_CAPABILITY_MEMBERS: {
46
46
  readonly webSearch: readonly ["backend"];
47
47
  readonly background: readonly ["listFace", "exitFaces"];
48
48
  readonly taskSettings: readonly ["permissions", "permissionMode", "model", "outputStyle", "env", "hooks"];
49
- readonly permissionModeAuto: readonly ["accepted", "classifierSeat", "entitlementSource", "intentArming", "armed", "reason", "model"];
49
+ readonly permissionModeAuto: readonly ["accepted", "classifierSeat", "entitlementSource", "armed", "reason", "model"];
50
50
  readonly writeProtection: readonly ["armed", "rows", "replaced"];
51
51
  readonly executionLane: readonly ["provider", "toolsOnThisHost", "isolated"];
52
52
  readonly retention: readonly ["mode", "maxAgeDays"];
@@ -7,7 +7,7 @@ export const COMPOSITE_CAPABILITY_MEMBERS = {
7
7
  webSearch: ["backend"],
8
8
  background: ["listFace", "exitFaces"],
9
9
  taskSettings: ["permissions", "permissionMode", "model", "outputStyle", "env", "hooks"],
10
- permissionModeAuto: ["accepted", "classifierSeat", "entitlementSource", "intentArming", "armed", "reason", "model"],
10
+ permissionModeAuto: ["accepted", "classifierSeat", "entitlementSource", "armed", "reason", "model"],
11
11
  writeProtection: ["armed", "rows", "replaced"],
12
12
  executionLane: ["provider", "toolsOnThisHost", "isolated"],
13
13
  retention: ["mode", "maxAgeDays"],
@@ -181,8 +181,13 @@ export declare const CONFIG_CATALOG: readonly ConfigCatalogSpec[];
181
181
  * 进程起不来、每个 import 本模块的测试当场红)—— 不是 warn、不是标一个 `false` 位上 wire(7.105.0 及以前那只 `domainExists`
182
182
  * 位就是后一种:它只看域不看路径,而 `models` 是真域,于是 `models.degrade` 这个不存在的成员被报成「中心可写」)。
183
183
  *
184
- * 解析只认 zod 的结构外衣(optional / nullable / default / effects / lazy)与对象 / 数组两种容器;认不出的节点 = 解析不出
184
+ * 解析只认 zod 的结构外衣(optional / nullable / default / catch / readonly / effects·管道 / lazy)与对象 / 数组两种容器;认不出的节点 = 解析不出
185
185
  * (fail-closed:宁可多拒一个座位,也不放过一个假座位)。
186
+ *
187
+ * 🔴 S-909(7.106.0 合并复审 opus 腿 L-1):**只走 zod 公开面**。契约包今天是 zod 3.25、本仓是 zod 4 —— 读 zod 私有定义字段(类型判别名 /
188
+ * 内层 schema / lazy getter)的旧写法在契约包升 zod 4 那天会让 `models[].…` 整族座位解析不出,模块 import 期整只抛。两大版没有一个能同时
189
+ * 公开读到的「这是什么节点」判别位(v4 的 `.def.type` 在 v3 上不存在),`instanceof` 也跨不了两份 zod 的类身份 ⇒ 按两大版**同名的公开访问器**
190
+ * 认形,规则三条(见 {@link zodObjectShapeOf} / {@link isZodArrayFace} / {@link ZOD_WRAPPER_FACES})。钉:`test/config-catalog-zod-public-face.test.ts`。
186
191
  */
187
192
  export declare function assertCenterSeatsResolvable(specs: readonly {
188
193
  readonly name: string;
@@ -498,24 +498,42 @@ export function assertCenterSeatsResolvable(specs) {
498
498
  throw new Error(`config catalog claims center seats the settings-schema contract does not have — the center cannot publish them, so the catalog must not advertise a center write lane for them (delete the seat, or add the member to the contract first): ${bad.join("; ")}`);
499
499
  }
500
500
  }
501
- function zodMember(node, key) {
501
+ function zodPublicMember(node, key) {
502
502
  return typeof node === "object" && node !== null && key in node ? Reflect.get(node, key) : undefined;
503
503
  }
504
+ function callZodPublic(node, key) {
505
+ const f = zodPublicMember(node, key);
506
+ return typeof f === "function" ? Reflect.apply(f, node, []) : undefined;
507
+ }
508
+ function zodObjectShapeOf(node) {
509
+ const shape = zodPublicMember(node, "shape");
510
+ return typeof shape === "object" && shape !== null ? shape : undefined;
511
+ }
512
+ function isZodArrayFace(node) {
513
+ return zodPublicMember(node, "element") !== undefined && typeof zodPublicMember(node, "length") === "function";
514
+ }
515
+ const ZOD_WRAPPER_FACES = [
516
+ (n) => callZodPublic(n, "unwrap"),
517
+ (n) => callZodPublic(n, "removeDefault"),
518
+ (n) => callZodPublic(n, "removeCatch"),
519
+ (n) => callZodPublic(n, "innerType"),
520
+ (n) => zodPublicMember(n, "schema"),
521
+ (n) => zodPublicMember(n, "in"),
522
+ ];
504
523
  function unwrapZodNode(node) {
505
524
  let cur = node;
506
525
  for (let depth = 0; depth < 32; depth++) {
507
- const def = zodMember(cur, "_def");
508
- const inner = zodMember(def, "innerType") ?? zodMember(def, "schema");
509
- if (inner !== undefined) {
510
- cur = inner;
511
- continue;
512
- }
513
- const getter = zodMember(def, "getter");
514
- if (typeof getter === "function") {
515
- cur = Reflect.apply(getter, def, []);
516
- continue;
526
+ if (zodObjectShapeOf(cur) !== undefined || isZodArrayFace(cur))
527
+ return cur;
528
+ let inner;
529
+ for (const face of ZOD_WRAPPER_FACES) {
530
+ inner = face(cur);
531
+ if (inner !== undefined)
532
+ break;
517
533
  }
518
- return cur;
534
+ if (inner === undefined)
535
+ return cur;
536
+ cur = inner;
519
537
  }
520
538
  return cur;
521
539
  }
@@ -524,15 +542,14 @@ function zodPathResolves(schema, path) {
524
542
  for (const segment of path.split(".")) {
525
543
  const intoArray = segment.endsWith("[]");
526
544
  const name = intoArray ? segment.slice(0, -2) : segment;
527
- const shape = zodMember(cur, "shape");
528
- if (typeof shape !== "object" || shape === null || !Object.hasOwn(shape, name))
545
+ const shape = zodObjectShapeOf(cur);
546
+ if (shape === undefined || !Object.hasOwn(shape, name))
529
547
  return false;
530
548
  cur = unwrapZodNode(Reflect.get(shape, name));
531
549
  if (intoArray) {
532
- const def = zodMember(cur, "_def");
533
- if (zodMember(def, "typeName") !== "ZodArray")
550
+ if (!isZodArrayFace(cur))
534
551
  return false;
535
- cur = unwrapZodNode(zodMember(def, "type"));
552
+ cur = unwrapZodNode(zodPublicMember(cur, "element"));
536
553
  }
537
554
  }
538
555
  return true;
@@ -122,12 +122,10 @@ export async function handleCapabilities(req, res, match, ctx) {
122
122
  subagentSteer: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
123
123
  subagentResume: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
124
124
  taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: deps.config.requirePrincipal !== true },
125
- modeShellGateTranslation: true,
126
125
  permissionModeAuto: {
127
126
  accepted: true,
128
127
  classifierSeat: deps.autoModeFace?.seatMounted === true,
129
128
  entitlementSource,
130
- intentArming: true,
131
129
  ...autoMode,
132
130
  },
133
131
  readFace: deps.config.readFace ?? null,
@@ -130,14 +130,16 @@ export declare function redactModelText(raw: string): string;
130
130
  *
131
131
  * **回声字段是什么**:模型服务商(上游模型 API / 中间网关)在响应里给、引擎**逐字转交**的不透明标识 ——
132
132
  * `apiFailure.requestId`(响应头 `request-id` / `x-request-id`,core `brain/errors.js` 的 `readProviderRequestId`)、
133
- * `responseModel`(服务商自报的模型名,与账本 `turn_end.responseModel` 是孪生)、工具调用 id(`strandedHumanAnswers[].toolCallId` /
134
- * `stats.humanReview.gates[].toolCallId`;core `brain/openai.js` / `brain/anthropic.js` 取 `tc.id` / `cb.id`,只在缺席时
135
- * `mintFallbackToolCallId` 自铸兜底)。字节由**部署方接的那个端点**决定,本栈一个都没铸 ⇒ **不可信**。
133
+ * `responseModel`(服务商自报的模型名,与账本 `turn_end.responseModel` 是孪生)。字节由**部署方接的那个端点**决定,本栈一个都没铸 ⇒ **不可信**。
134
+ * 工具调用 id(`strandedHumanAnswers[].toolCallId` / `stats.humanReview.gates[].toolCallId`)**出身**同样是服务商,但自 core 7.36.0(#1207)起
135
+ * 它进引擎之前已**准入一次**:三只 brain 在建调用的那一行(`brain/tool-call-id.js` 的 `admitToolCallId`)—— 缺席 ⇒ 车道兜底铸;
136
+ * 在形(可见 ASCII 1–128 且不含 `~`)⇒ 原样;越界 ⇒ 单射折叠成引擎 id(`<前 32 码元,越界字符记 `-`>~<sha256 前 32 hex>`),原字节只挂在
137
+ * 转录的 `ToolCall.wireId` 上、只回给 provider。⇒ 到达本仓任何一张脸的 `toolCallId` 恒在形。
136
138
  *
137
139
  * 🔴 **本闸只施加在「这个值出现的每一张脸都被覆盖到」的回声上**(codex r1 F1,实测成立的回归后改判):工具调用 id 是
138
140
  * **跨面 join 键** —— 同一枚 id 同时逐字出现在 `tool_start` / `tool_end` / 审批卡等逐帧面上。只在结果面动它 ⇒ 结果面与逐帧面
139
- * join 不上。所以它的处置词是 `"provider-join-key"`:信任类仍是 provider 回声,但形状闸只能在**全部面的共同入口**一次施加
140
- * (独立一车,已登记为债),本面逐字。`requestId` / `responseModel` 的每一张脸(结果面四处 + 账本孪生 + 子代通告 + `POST /v1/side-query` 200 体的 `responseModel`,
141
+ * join 不上。所以它的处置词是 `"provider-join-key"`:出身是 provider,形状在**全部面的共同入口一次施加** —— 那个入口就是上面那条
142
+ * core brain 准入(S-890 v1 / v2 小稿提给 core、#1207 落地;本仓零闸,不加第二道门),本面逐字 = 每张脸同一枚引擎 id,join 成立。`requestId` / `responseModel` 的每一张脸(结果面四处 + 账本孪生 + 子代通告 + `POST /v1/side-query` 200 体的 `responseModel`,
141
143
  * 后者 7.105.0 合并复审 X1 补上 —— `http/routes/side-query.ts` 的 `shapeSideQueryEcho`)本闸都覆盖到了。
142
144
  *
143
145
  * **只做形状校验,不过脱敏器**(本车裁,与 S-270 / clay [ref] ② 的 `structuredOutput` 逐字回声面同一方向):回声值
@@ -35,25 +35,38 @@
35
35
  * - (`hooks` NO LONGER deferred either — 轴B #2 注释保鲜:applied 形已接线(本文件 hooks 分片 +
36
36
  * main.ts 单用户闸),仅 malformed 才报 deferred;旧「需要 remote hook-runner」是接线前拍照。)
37
37
  */
38
- import { type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider } from "@sema-agent/core";
38
+ import { type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider, type PermissionMode } from "@sema-agent/core";
39
39
  import type { HooksConfig } from "@sema-agent/settings-schema/hooks";
40
40
  import { type KeyClosureIssue } from "./request-key-closure.js";
41
- /** The permission MODE a client may request (SDK `SettingsPermissions.defaultMode`), the [ref]/[ref] five-word
42
- * table. ⚠️ This is a DELIBERATE deployment set, NOT a copy of any CC set ([ref] P2-1 corpus check): CC's own
43
- * full enum is SIX words (= ours + `dontAsk`), CC's managed/remote form accepts FOUR (= ours − `bypassPermissions`,
44
- * which it warns-and-ignores). Ours = {CC-remote four} ∪ {`bypassPermissions`} — registered divergence, not parity.
45
- * `bypassPermissions` and `auto` joined the set with the [ref] fs-write gate: since the gate exists, every
46
- * mode is a choice of HOW MUCH mode-derived gating the interpretation layer ADDS on top of the deployment baseline —
47
- * `bypassPermissions` adds none (= the pre-[ref] behavior of every mode: "不加门" not "开门", [ref] table row 4; the
48
- * deployment approval/governance baseline is composed OUTSIDE this derive and is untouchable from a settings stamp),
49
- * and `auto` adds the same ask gate as `default` with core's auto-mode classifier screening the asks upstream
50
- * (entitlement-gated in core, [ref]②). */
51
- export declare const SETTINGS_PERMISSION_MODES: readonly ["default", "acceptEdits", "plan", "bypassPermissions", "auto"];
52
- /** 五模式闭集的型形 —— **从值表派生**(S-496 / [ref]):改前本仓有 **4 份**同一张五词表(本处的手写联合 /
53
- * 下面 `coercePermissionModeStrict` 的运行期字面量链 / `http/admission.ts` 顶层 `permissionMode` 的比较链 /
54
- * 同文件 `settings.permissions.defaultMode` 的比较链),外加两句错误文案里各抄一遍词。现在值表一份、
55
- * 型与文案都从它派生,比较链改调 {@link isSettingsPermissionMode}。 */
56
- export type SettingsPermissionMode = (typeof SETTINGS_PERMISSION_MODES)[number];
41
+ /**
42
+ * S-902(7.107.0,车QY 候裁 11 裁 A;[ref])—— core 的**每一个**权限模式词,本仓请求面收不收:`"request"` = 受理(顶层 `permissionMode` /
43
+ * `settings.permissions.defaultMode` / `?permissionMode=` 查询串三处同读),`"excluded"` = 显式排除(不上请求面)。
44
+ *
45
+ * 🔴 这张表是**表态**,不是词表的第二份:词与词序只从 core `PERMISSION_MODES` 来(下面 {@link SETTINGS_PERMISSION_MODES} 按它过滤);
46
+ * 表只对每个词说一句「收 / 不收」。`satisfies Record<PermissionMode, …>` 对 core 的词型**穷举**:core 加一个词 ⇒ 本表缺键 **编译红**
47
+ * (新词必须被表态一次,不会被静默收进请求面、也不会被静默漏掉);core 删 / 改名一个词 ⇒ 本表多键编译红(残条目不留)。
48
+ *
49
+ * 今天的表态(与 7.106.0 逐字同行为):五词受理;`dontAsk`(CC 的「自动拒」档,core 7.34.0 起第六词)**排除** —— 它上不上请求面是一件
50
+ * 新能力(未裁,本表不替它裁);不收它时提交面 400 `request.field_invalid`,续跑重放腿按 {@link coercePermissionMode} 的既有宽读折 `default`。
51
+ * 语义沿革(为什么是这五个、`bypassPermissions` / `auto` 何时进来):[ref] / [ref] / [ref] —— CC 自己的全集是六词,CC 托管 / 远端形收四词
52
+ * (= 本仓 − `bypassPermissions`),本仓 = {CC 远端四词} ∪ {`bypassPermissions`},登记过的偏离;生效模式原样过境 `TaskSpec.permissionMode`(S-877)。
53
+ */
54
+ export declare const PERMISSION_MODE_REQUEST_FACE: {
55
+ readonly default: "request";
56
+ readonly acceptEdits: "request";
57
+ readonly plan: "request";
58
+ readonly auto: "request";
59
+ readonly bypassPermissions: "request";
60
+ readonly dontAsk: "excluded";
61
+ };
62
+ /** 请求面受理的模式词的型形 —— 从表态表派生(`"request"` 那几行的键)。 */
63
+ export type SettingsPermissionMode = {
64
+ [M in PermissionMode]: (typeof PERMISSION_MODE_REQUEST_FACE)[M] extends "request" ? M : never;
65
+ }[PermissionMode];
66
+ /** 请求面受理的模式词(core 序)= core `PERMISSION_MODES` 过滤表态 `"request"`。wire 门、coerce 两族、两句 400 文案同读这一份。 */
67
+ export declare const SETTINGS_PERMISSION_MODES: readonly SettingsPermissionMode[];
68
+ /** 显式排除集(core 序)= core `PERMISSION_MODES` 过滤表态 `"excluded"`;今天恰是 `["dontAsk"]`。只作披露 / 钉读,不参与判定。 */
69
+ export declare const SETTINGS_PERMISSION_MODES_EXCLUDED: readonly Exclude<PermissionMode, SettingsPermissionMode>[];
57
70
  /** 五词表的 wire 文案形(`"a" | "b" | …`)—— 两条 400 的句子从这里取词,不再各抄一份。 */
58
71
  export declare const SETTINGS_PERMISSION_MODE_WORDS: string;
59
72
  /** 闭集成员判定(`unknown` 入口用)—— 词表的**唯一**运行期读者:wire 门与 coerce 两族同调。 */
@@ -1,9 +1,18 @@
1
- import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, createPermissionRulePolicy, parseRuleText, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, ruleToolGrammarOf, } from "@sema-agent/core";
1
+ import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, createPermissionRulePolicy, parseRuleText, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, PERMISSION_MODES, ruleToolGrammarOf, } from "@sema-agent/core";
2
2
  import { findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
3
3
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
4
4
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
5
5
  import { parseHooksConfig } from "./hooks/hook-runner.js";
6
- export const SETTINGS_PERMISSION_MODES = ["default", "acceptEdits", "plan", "bypassPermissions", "auto"];
6
+ export const PERMISSION_MODE_REQUEST_FACE = {
7
+ default: "request",
8
+ acceptEdits: "request",
9
+ plan: "request",
10
+ auto: "request",
11
+ bypassPermissions: "request",
12
+ dontAsk: "excluded",
13
+ };
14
+ export const SETTINGS_PERMISSION_MODES = PERMISSION_MODES.filter((m) => PERMISSION_MODE_REQUEST_FACE[m] === "request");
15
+ export const SETTINGS_PERMISSION_MODES_EXCLUDED = PERMISSION_MODES.filter((m) => PERMISSION_MODE_REQUEST_FACE[m] === "excluded");
7
16
  export const SETTINGS_PERMISSION_MODE_WORDS = SETTINGS_PERMISSION_MODES.map((m) => `"${m}"`).join(" | ");
8
17
  export function isSettingsPermissionMode(raw) {
9
18
  return typeof raw === "string" && SETTINGS_PERMISSION_MODES.includes(raw);
@@ -296,9 +296,10 @@ export declare function toolEndEventData(ev: {
296
296
  * **不等于** `tool_end` 的 duration)。
297
297
  *
298
298
  * ## 信任划线
299
- * · `toolCallId` / `toolName` —— 调用 id(🔴 S-283 订正:**服务商**在 tool_use / tool_calls 里给的,引擎只在缺席时自铸兜底 ——
300
- * 此前写「引擎铸」不实)与名册里的工具名(与 `tool_start` 逐字同源)⇒ verbatim。调用 id 这一族在**逐帧面**上的形状闸
301
- * 不在 S-283 射程(它是全 wire 的 join 键,要闸得所有面一起闸,见 run-terminal `admitProviderEcho` 顶注与车QO 收车档)。
299
+ * · `toolCallId` / `toolName` —— 调用 id(🔴 S-283 订正:出身是**服务商**在 tool_use / tool_calls 里给的,引擎缺席时自铸兜底、越界时折叠 ——
300
+ * 此前写「引擎铸」不实)与名册里的工具名(与 `tool_start` 逐字同源)⇒ verbatim。调用 id 这一族的形状在**全部面的共同入口**一次施加 ——
301
+ * core 7.36.0(#1207)brain 建调用那一行的准入(在形原样 / 越界单射折叠,原字节只在转录 `ToolCall.wireId`),逐帧面收到的恒是在形的引擎 id;
302
+ * 本仓不设第二道闸(见 run-terminal `admitProviderEcho` 顶注;S-890 关)。
302
303
  * · `elapsedTimeSeconds` —— 引擎铸的整秒,单调、一 call 一个原点 ⇒ 有限数才铸键。
303
304
  * · `output` —— 🔴 **UNTRUSTED RAW**,core 逐字「exactly as `tool_end.output`: process output that may
304
305
  * carry secrets, paths or PII; core bounds only its SIZE, and a consumer MUST redact before persisting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.106.0",
3
+ "version": "7.107.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -61,7 +61,7 @@
61
61
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
62
62
  },
63
63
  "dependencies": {
64
- "@sema-agent/core": "7.35.2",
64
+ "@sema-agent/core": "7.36.0",
65
65
  "@sema-agent/settings-schema": "6.3.0",
66
66
  "e2b": "^2.28.0",
67
67
  "libsodium-wrappers": "^0.8.4",