@sema-agent/client-core 0.18.0 → 0.20.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.
@@ -98,6 +98,26 @@ const isObject = (v) => typeof v === "object" && v !== null;
98
98
  const isAbsent = (v) => typeof v === "undefined";
99
99
  const optionalString = (v) => isAbsent(v) || typeof v === "string";
100
100
  const inTuple = (tuple, v) => tuple.includes(v);
101
+ /**
102
+ * `ToolPermissionRequest.requestId` 的**域词表**(FIX⑥,2026-08-07)—— 三端唯一真源。
103
+ *
104
+ * 🔴 前缀不是装饰:它**就是**决断的路由依据(session-host 按它挑 wire 动词)。一个域外的 requestId
105
+ * 意味着「这条决断无处可去」,所以校验器对它 fail-loud 而不是放行(此前门只校「是不是串」,
106
+ * 于是 desktop 早就在用的 `plan:` 既不在枚举里、也不会被任何一处说出来)。
107
+ */
108
+ export const TOOL_PERMISSION_REQUEST_ID_DOMAINS = [
109
+ "live:",
110
+ "durable:",
111
+ "question:",
112
+ "elicit:",
113
+ "plan:",
114
+ ];
115
+ /** 读口:requestId → 它的域。未登记前缀 / 非串 ⇒ `undefined`(诚实缺席,绝不猜一个域)。 */
116
+ export function toolPermissionRequestIdDomain(requestId) {
117
+ if (typeof requestId !== "string")
118
+ return undefined;
119
+ return TOOL_PERMISSION_REQUEST_ID_DOMAINS.find((d) => requestId.startsWith(d));
120
+ }
101
121
  export const TOCTOU_BINDING_KEYS = ["callId", "inputHash"];
102
122
  /**
103
123
  * 座位事件名的**闭集**(REF-CC-065):这是三端唯一的事件名真源。
@@ -227,6 +247,9 @@ const checkLocalSessionRecord = (s) => {
227
247
  if (!okQueue)
228
248
  return bad("queuedMessages must be an array of {uuid,text}", "queuedMessages");
229
249
  }
250
+ if (!isAbsent(s.parked) && typeof s.parked !== "boolean") {
251
+ return bad("parked must be a boolean when present", "parked");
252
+ }
230
253
  return true;
231
254
  };
232
255
  export function isLocalSessionRecord(s) {
@@ -247,6 +270,10 @@ const checkToolPermissionRequest = (r) => {
247
270
  return bad("tool permission request is not an object");
248
271
  if (typeof r.requestId !== "string")
249
272
  return bad("requestId must be a string", "requestId");
273
+ // FIX⑥:域(前缀)就是决断的路由依据 —— 认不出域 = 这条决断无处可去,fail-closed 而不是放行。
274
+ if (toolPermissionRequestIdDomain(r.requestId) === undefined) {
275
+ return bad(`requestId must carry a registered domain prefix (${TOOL_PERMISSION_REQUEST_ID_DOMAINS.join("|")})`, "requestId");
276
+ }
250
277
  if (typeof r.sessionId !== "string")
251
278
  return bad("sessionId must be a string", "sessionId");
252
279
  if (!isAbsent(r.toolName) && r.toolName !== null && typeof r.toolName !== "string") {
@@ -414,6 +441,22 @@ const checkSeatModelInfo = (m, index) => {
414
441
  }
415
442
  return true;
416
443
  };
444
+ /** applyModelConfig ack 校验:布尔形 + reason 条件在场(ok:false ⇒ 必带非空 reason)。
445
+ * 🔴 错误消息零回显 req/ack 值——本 verb 载荷带 apiKey,校验层是回显面之一。 */
446
+ const checkApplyModelConfigResult = (v) => {
447
+ if (!isObject(v))
448
+ return bad("applyModelConfig result is not an object");
449
+ if (typeof v.ok !== "boolean")
450
+ return bad("ok must be a boolean", "ok");
451
+ if (typeof v.requiresRestart !== "boolean")
452
+ return bad("requiresRestart must be a boolean", "requiresRestart");
453
+ if (v.ok === false && (typeof v.reason !== "string" || v.reason.length === 0)) {
454
+ return bad("reason must be a non-empty string when ok is false", "reason");
455
+ }
456
+ if (v.reason !== undefined && typeof v.reason !== "string")
457
+ return bad("reason must be a string when present", "reason");
458
+ return true;
459
+ };
417
460
  const checkSeatModelCatalog = (v) => {
418
461
  if (!isObject(v))
419
462
  return bad("model catalog is not an object");
@@ -545,6 +588,7 @@ export const LOCAL_SESSIONS_SPEC = {
545
588
  searchSessions: { kind: "invoke", check: checkSessionSearchEnvelope },
546
589
  uploadAttachment: { kind: "invoke", check: checkAttachmentUploadReceipt },
547
590
  listModels: { kind: "invoke", check: checkSeatModelCatalog },
591
+ applyModelConfig: { kind: "invoke", check: checkApplyModelConfigResult },
548
592
  onEvent: { kind: "event", check: checkLocalSessionEvent },
549
593
  onToolPermissionRequest: { kind: "event", check: checkToolPermissionRequest },
550
594
  },
@@ -570,6 +614,7 @@ export const SEAT_METHOD_NAMES = [
570
614
  "searchSessions",
571
615
  "uploadAttachment",
572
616
  "listModels",
617
+ "applyModelConfig",
573
618
  "onEvent",
574
619
  "onToolPermissionRequest",
575
620
  ];
@@ -622,6 +667,7 @@ export const LOCAL_SESSION_RECORD_KEYS = [
622
667
  "createdAt",
623
668
  "lastActivityAt",
624
669
  "queuedMessages",
670
+ "parked",
625
671
  ];
626
672
  export const SEND_MESSAGE_REQUEST_KEYS = [
627
673
  "sessionId",
@@ -672,7 +718,7 @@ export const SEAT_VALIDATOR_KEY_COVERAGE = {
672
718
  LocalSessionRecord: {
673
719
  guard: "isLocalSessionRecord",
674
720
  check: checkLocalSessionRecord,
675
- validated: ["sessionId", "engineSessionId", "model", "title", "isRunning", "createdAt", "lastActivityAt", "queuedMessages"],
721
+ validated: ["sessionId", "engineSessionId", "model", "title", "isRunning", "createdAt", "lastActivityAt", "queuedMessages", "parked"],
676
722
  unchecked: [],
677
723
  },
678
724
  ToolPermissionRequest: {
@@ -726,6 +772,11 @@ export const SEAT_VALIDATOR_KEY_COVERAGE = {
726
772
  validated: ["models", "defaultId"],
727
773
  unchecked: [],
728
774
  },
775
+ ApplyModelConfigResult: {
776
+ check: checkApplyModelConfigResult,
777
+ validated: ["ok", "requiresRestart", "reason"],
778
+ unchecked: [],
779
+ },
729
780
  SendMessageAck: {
730
781
  guard: "isSendMessageAck",
731
782
  check: checkSendMessageAck,
@@ -811,7 +862,11 @@ export const SEAT_VALIDATOR_COVERAGE_EXEMPT = [
811
862
  {
812
863
  label: "LaneProof",
813
864
  check: (v) => checkLaneProof(v, "laneProof"),
814
- why: "判别式联合:必填键面随 `lane` 臂而变(subagent 要 parentToolCallId、workflow 要 via+key),而覆盖表的方法是「一个最小合法体 + 逐键投毒」—— 那套方法在它身上没有判别力(main 臂对另外两臂的伴随位本来就不该拒,投毒过了不代表漏校)。判别力由 `LANE_PROOF_LANE_TUPLE` ⇄ seam.ts 车道联合的双向 `Covers` + F 段逐臂投毒(空对象 / 非对象 / 未登记车道词)承担。",
865
+ why: "判别式联合:必填键面随 `lane` 臂而变(subagent 要 parentToolCallId、workflow 要 via+key),而覆盖表的方法是「一个最小合法体 + 逐键投毒」—— 那套方法在它身上没有判别力(main 臂对另外两臂的伴随位本来就不该拒,投毒过了不代表漏校)。" +
866
+ "🔴 FIX④(2026-08-07 订正):本条此前自称判别力由「F 段**逐臂**投毒(空对象 / 非对象 / **未登记车道词**)」承担 —— 全树实扫,那三样里只有**空对象**与**非对象**真的存在(run-seat-contract-keys-test.mjs F 段)," +
867
+ "「未登记车道词」实际只钉了 `lane: 7`(非串,证明不了闭集成员判定在场),「逐臂」(subagent 缺 parentToolCallId / workflow 缺 via+key)则是**零覆盖**。也就是说这条豁免曾经拿三件不存在的判据给自己背书。" +
868
+ "现势(判据锚在真存在的东西上):① `LANE_PROOF_LANE_TUPLE` ⇄ seam.ts 车道联合的双向 `Covers`(本文件 `_seatContractPins`,加车道不跟 ⇒ 编译红);" +
869
+ "② run-client-core-pure-test.mjs **FIX7 段 ④** —— 未登记车道词 / subagent 缺伴随位 / workflow 缺 via 或 key 三条投毒 + 两条逐臂正控 + 一条「main 臂不被过紧误拒」负控,全部直接喂本条的 `check`。",
815
870
  },
816
871
  ];
817
872
  /** 双向覆盖钉:`Exclude` 两个方向都必须塌成 `never`,少一个键红、多一个键也红。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",