@sema-agent/server 7.90.0 → 7.91.2

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 (44) hide show
  1. package/USAGE.md +1 -1
  2. package/dist/adoption/plan.js +11 -0
  3. package/dist/boot/runner-deps.d.ts +6 -0
  4. package/dist/boot/runner-deps.js +11 -0
  5. package/dist/boot/session-faces.d.ts +6 -0
  6. package/dist/boot/session-faces.js +10 -2
  7. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  8. package/dist/boot/stage-03-store-backend.js +2 -2
  9. package/dist/boot/stage-04-stores.d.ts +1 -0
  10. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  11. package/dist/boot/stage-06-runners.d.ts +1 -0
  12. package/dist/boot/stage-06-runners.js +2 -2
  13. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  14. package/dist/boot/stage-08-reapers.d.ts +1 -0
  15. package/dist/boot/stage-09-leader.d.ts +1 -0
  16. package/dist/boot/stage-09-leader.js +2 -2
  17. package/dist/boot/stage-10-http-server.d.ts +1 -0
  18. package/dist/boot/stage-10-http-server.js +1 -0
  19. package/dist/boot/stores.d.ts +2 -0
  20. package/dist/boot/stores.js +11 -3
  21. package/dist/cross-session-settings.d.ts +6 -2
  22. package/dist/http/routes/capabilities.js +4 -1
  23. package/dist/http/routes/rules.d.ts +11 -0
  24. package/dist/http/routes/rules.js +54 -5
  25. package/dist/http/routes/sessions-list.js +6 -1
  26. package/dist/http/server.d.ts +12 -0
  27. package/dist/peer-directory.d.ts +114 -0
  28. package/dist/peer-directory.js +136 -0
  29. package/dist/permission-rule-vocab.d.ts +27 -1
  30. package/dist/permission-rule-vocab.js +8 -0
  31. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +86 -0
  32. package/dist/plugins/mailbox-recipient-lifecycle.js +10 -0
  33. package/dist/plugins/mailbox-store-sql.d.ts +18 -2
  34. package/dist/plugins/mailbox-store-sql.js +77 -1
  35. package/dist/plugins/store-contracts.d.ts +36 -1
  36. package/dist/plugins/store-contracts.js +10 -0
  37. package/dist/rules-consent.d.ts +74 -1
  38. package/dist/rules-consent.js +81 -1
  39. package/dist/trace/core-keyset-guard.d.ts +10 -4
  40. package/dist/trace/project.d.ts +16 -0
  41. package/dist/trace/project.js +13 -1
  42. package/dist/trace/redact.js +129 -18
  43. package/dist/trace/sema-provenance.d.ts +7 -6
  44. package/package.json +3 -3
@@ -14,7 +14,7 @@
14
14
  * - The TiDB adapters re-export these names for back-compat (the historical import surface), but new
15
15
  * code should import from this file.
16
16
  */
17
- import type { TaskResult, TaskStatus } from "@sema-agent/core";
17
+ import type { PeerSessionTempo, TaskResult, TaskStatus } from "@sema-agent/core";
18
18
  /**
19
19
  * **File 店族的 boot 期拒启基类**(S-384;把 `RunStoreStrictHydrateError` 的那条论证升成一条族规)。
20
20
  *
@@ -240,6 +240,41 @@ export declare function isUnsuccessfulRunStatus(status: RunRecord["status"]): bo
240
240
  * 动作(cancel 的恢复把手会 CAS-expire 一张 checkpoint);对一个本进程读不懂的状态,少动一次远好过错杀。
241
241
  */
242
242
  export declare function isParkedRunStatus(status: RunRecord["status"]): boolean;
243
+ /**
244
+ * 会话的**活动词**(core `PeerSessionTempo`)——「这条会话现在在忙还是闲」的**唯一**一张表([ref])。
245
+ *
246
+ * 🔴 为什么键是 {@link RunStatusClass} 而不是状态词本身:那张状态词表(`RUN_STATUS_CLASS`)已经是这条
247
+ * 车道上唯一的词表属主,它的双向编译钉(core 加词 / 删词当场红)是本表白拿的。按状态词再写一张表
248
+ * 等于给同一族词开第二个写者 —— 正是那张表顶注里 `blocked` 长出两个互斥读法的成因。
249
+ *
250
+ * 🔴 两处读、一处定义:core 的 peer 目录席(`src/peer-directory.ts` 的 `PeerSessionRecord.tempo`)与
251
+ * `GET /v1/sessions` 列表行的 `tempo` 列读的是**同一次调用**的结果 —— 同一条会话在两个面上不可能给出
252
+ * 两个不同的忙闲。
253
+ *
254
+ * 🔴 **未知词 / 无读数 ⇒ 词缺席(`undefined`),不是 `idle`**(诚实缺席,不是静默兜底):
255
+ * · `lastStatus === ""` 有两个真来源 ——(a)SQL 双生:这条会话**还没有任何 run**;(b)local 文件车道:
256
+ * 该 lister **结构上说不出** run 状态(`local-session-store.ts:465` 自陈的诚实降级 `lastStatus: ""`)。
257
+ * 两者都不是「我知道它闲着」,所以都不许铸出 `idle` 这个断言;
258
+ * · 读不懂的词(滚动升级里新副本写入的新状态词经裸 cast 读到本进程)同判 —— 与
259
+ * {@link isTerminalRunStatus} 族的保守方向一致。
260
+ * · **缺席怎么渲染是 core 的辖域,本仓一个字都不复述**(S-514-JU:此处原写着「core 对缺席渲染成 `idle`」
261
+ * 并抄了当时 `list-agents-tool.js:58` 的旧三元 —— core 7.24.0 [ref] 把那一臂改成了一张 own-key 表 +
262
+ * `unknown` 兜底,于是那句复述当天就成了假话,而它在本仓被抄了六处)。本表只负责**铸不铸键**;
263
+ * 渲染词由**跑真 core 的黑盒**钉住(`test/peer-lane-e2e.test.ts` 的 G1 格),core 哪天再改那张表,
264
+ * 红的是那一格而不是一句注释。承重的不变量只有一条,且它与渲染词无关:**缺席绝不铸 `dead`** ——
265
+ * `liveness` 才是 core 会拿来**整行隐藏**的那一位(7.24.2 dist 的 `list-agents-tool.js:60/62`),而任何 tempo 词
266
+ * (含缺席)都照样列出。
267
+ */
268
+ export declare const SESSION_TEMPO_BY_RUN_CLASS: {
269
+ readonly live: "active";
270
+ readonly parked: "blocked";
271
+ readonly succeeded: "idle";
272
+ readonly unsuccessful: "idle";
273
+ };
274
+ /** {@link SESSION_TEMPO_BY_RUN_CLASS} 的查表面 —— 入参是**最近一条 run 的状态词**({@link SessionSummary.lastStatus},
275
+ * 开集 `string`:它来自无约束的 SQL 文本列 / 诚实降级的空串,收成闭集等于在签名上撒谎)。
276
+ * 返回词缺席 ⇒ 调用方**不铸键**(不是铸 `idle`),理由逐字见上表头注。 */
277
+ export declare function sessionTempoOfRunStatus(lastStatus: string): PeerSessionTempo | undefined;
243
278
  /**
244
279
  * 「**证得出**这条 run 活在别的副本上」——「他副本」那一句话的**唯一判据**(S-381 收编;S-430 迁居)。
245
280
  *
@@ -23,6 +23,16 @@ export function isUnsuccessfulRunStatus(status) {
23
23
  export function isParkedRunStatus(status) {
24
24
  return runStatusClassOf(status) === "parked";
25
25
  }
26
+ export const SESSION_TEMPO_BY_RUN_CLASS = {
27
+ live: "active",
28
+ parked: "blocked",
29
+ succeeded: "idle",
30
+ unsuccessful: "idle",
31
+ };
32
+ export function sessionTempoOfRunStatus(lastStatus) {
33
+ const cls = runStatusClassOf(lastStatus);
34
+ return cls === undefined ? undefined : SESSION_TEMPO_BY_RUN_CLASS[cls];
35
+ }
26
36
  export function runProvenElsewhere(run, thisInstanceId) {
27
37
  return typeof run.instanceId === "string" && run.instanceId.length > 0 && thisInstanceId !== undefined && run.instanceId !== thisInstanceId;
28
38
  }
@@ -1,3 +1,4 @@
1
+ import { type DirectWritableRuleBehavior } from "./permission-rule-vocab.js";
1
2
  import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedRule, type RuleBehavior, type RuleCandidate, type EffectivePermissionRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
2
3
  import type { DurableRulePartitionProvider, PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
3
4
  import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js";
@@ -155,7 +156,12 @@ export interface LocalImportRuleKey {
155
156
  *
156
157
  * 🔴 闭集而不是自由文本:客户端要按拒因分流(「这条是你本机的数据坏了」vs「这条规则本身不合法」是
157
158
  * 两句不同的话),而自由文本只能被展示、不能被分支。`detail` 才是给人看的那一半。
158
- */
159
+ *
160
+ * 🔴 S-511 起**两条腿共用这一张表**(导入面的逐行预览 + 单步写口的拒):它们判的是同一件事,而判据
161
+ * 本身就是同一个函数({@link screenLocalImportRow})。给写口另起一张同形的表,就是给同一个问题立第二个
162
+ * 说话人 —— 漂的表现会是「同一条规则在一条腿上收、在另一条腿上拒」。写口的**方向**判据不在这张表里:
163
+ * 它在端点上先判、有自己的整句文案,而且判完就把类型收窄掉(`permission-rule-vocab.ts` 的
164
+ * `DIRECT_RULE_WRITE_ADMITS`)⇒ 放宽态根本到不了本表所服务的那段代码。 */
159
165
  export type LocalImportRefusalCode =
160
166
  /** scope 串读不出形(既不是 `global` 也不是 `project:<非空 root>`)。 */
161
167
  "scope_unreadable"
@@ -501,6 +507,22 @@ export declare function parseRuleScope(text: string): DurableRuleScope | undefin
501
507
  /** 一条规则在 wire 上的行 = 一个逻辑 (rule, scope) 对([ref] v2 §6 F5)。
502
508
  * `adds` **如实上 wire**、不折叠成单值:同一条规则可以被批准过多次(不同 dot、不同来源、不同时刻),
503
509
  * 折成「一个 addedAt」会让「这条规则是我导入的还是我点过卡的」在治理面上不可分。 */
510
+ /**
511
+ * 一条 wire 行与一份身份三元组是不是**同一条规则**。
512
+ *
513
+ * 🔴 **三元组,不是 `(rule, scope)` 二元组**(异源复核 [medium],验真后修):core 的 `sameRuleIdentity`
514
+ * 头注逐字 ——「a comparison that forgot the behavior member would let a delete aimed at an allow take the
515
+ * same-text deny」。漏掉 behavior 的比较在 wire 上的表现不是「删错行」,是**报错行的死活**:撤一条从来
516
+ * 没有过的 `allow` 会因为看见同文本的兄弟 `deny` 而答 `stillLive: true`,那句话说的是另一条规则。
517
+ *
518
+ * 凡「这份清单里有没有这条规则」一律走这一只(撤销口的存活回读 / 单步写口的写前问与写后读回)——
519
+ * 同一个问题有两个说话人,迟早会给两个答案。
520
+ */
521
+ export declare function sameWireRuleIdentity(row: PersistedRuleWireRow, identity: {
522
+ behavior: RuleBehavior;
523
+ rule: string;
524
+ scope: string;
525
+ }): boolean;
504
526
  export interface PersistedRuleWireRow {
505
527
  /** core 7.9.0([ref])—— 这一行是哪一态的规则(`deny` / `ask` / `allow`)。**身份的一格**:
506
528
  * 规则的身份是 (behavior, text, scope) 三元组,同文本不同态是两条不同的行(core `sameRuleIdentity`),
@@ -557,6 +579,37 @@ export interface PersistedRuleListing {
557
579
  * 不可分。判别式联合是唯一诚实的形。
558
580
  */
559
581
  export type RuleBucketRef = string | RuleOwner;
582
+ /**
583
+ * S-511 —— 单步写的结果(闭集三臂)。
584
+ *
585
+ * 🔴 三臂**刻意不塌成布尔**:「落了」「早就有了」「不知道落没落」对调用方是三种不同的处置,而把第三种
586
+ * 折进前两种里的任一边都是编一个结论(猜"成了"会谎报一次没发生的写,猜"没成"会让人以为可以重来)。
587
+ * 与导入面的成员级四态({@link LocalImportMemberOutcome})同一条纪律,词更少是因为本口只有一行。
588
+ */
589
+ export type RuleWritten =
590
+ /** `persisted` = 这一次**真的往店里写了**;`no-op` = 这条身份**此前就站在店里**,本次调用**一个字都没写**
591
+ * (幂等重写)。两支都带 `row` —— 它是**从店里读出来的**那一行,不是我方按入参拼出来的回声。
592
+ *
593
+ * 🔴 两个词的判别位是「**这次调用有没有写**」,不是 core 的 `deduped`(codex 对抗复审 F1,验真后修)。
594
+ * core 的 `deduped` 只说「等价规则此前已站」,它**仍然落了一枚新的 dot**(契约逐字:`deduped` counts
595
+ * as LANDED;亲读 dist 的成员循环:`res.dot` 无条件进 members)。把它当 `no-op` 上 wire 是一句假话:
596
+ * 一个照常重试的客户端会让 `adds` 台账无界增长、桶的 rev 逐次推进(在翻页的治理面全体重列)。 */
597
+ {
598
+ status: "persisted" | "no-op";
599
+ rev: number;
600
+ row: PersistedRuleWireRow;
601
+ }
602
+ /** 裁定性不收(内容判据 / 方向判据)。`detail` 是给人看的那一半,`reason` 是给客户端分流的那一半。 */
603
+ | {
604
+ status: "rejected";
605
+ reason: LocalImportRefusalCode;
606
+ detail: string;
607
+ }
608
+ /** **不知道落没落**:店在这一条上出了状况。如实编,禁猜(端点侧 503 + 指路 `GET /v1/rules` 对账)。 */
609
+ | {
610
+ status: "indeterminate";
611
+ detail: string;
612
+ };
560
613
  export interface RuleConsentLane {
561
614
  /** 卡道兑付口:一次 ask 决议携规则确认 ⇒ prepare→confirm→redeem 一气呵成。 */
562
615
  persistCardRule(input: {
@@ -608,6 +661,26 @@ export interface RuleConsentLane {
608
661
  rule: string;
609
662
  scope: RuleScope;
610
663
  }): Promise<RemoveResult>;
664
+ /**
665
+ * S-511 撤销面写半场的**对偶** —— 一条**收紧**规则(deny/ask)的单步写入,无同意仪式往返。
666
+ *
667
+ * 它为什么可以没有仪式,与 `removeRule` 为什么可以没有仪式是**同一条理由**(core
668
+ * `removePersistedRule` 头注逐字:「a host may narrow on a user's behalf, it may not widen」)。放宽
669
+ * 方向在类型上就到不了这里:`behavior` 收窄成 {@link DirectWritableRuleBehavior}。
670
+ *
671
+ * 🔴 **一行都不自造**:行的 tool/match/command 三格是规则文本的**派生事实**,由 core 的
672
+ * `parseRuleText` 铸;落店走的是导入面那条**同一条**兑付腿(逐行判据 → core 的记录铸造口 → confirm
673
+ * → 批兑付),于是本口与导入口对「一条规则能不能进店」永远给同一个答案。
674
+ *
675
+ * `principal` 是裸串(与 prepare/redeem 两腿同形);local-owner 桶**不在本口**上 —— 那只桶没有
676
+ * 「谁在写」这个主体,而本口的授权基础就是已验明的 principal。
677
+ */
678
+ writeRule(input: {
679
+ principal: string;
680
+ behavior: DirectWritableRuleBehavior;
681
+ rule: string;
682
+ scope: DurableRuleScope;
683
+ }): Promise<RuleWritten>;
611
684
  }
612
685
  export declare function createRuleConsentLane(stores: RuleConsentStores, opts?: {
613
686
  ticketTtlMs?: number;
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { recordFailOpen } from "./observability/fail-open.js";
4
4
  import { createLogger } from "./observability/logger.js";
5
5
  import { MAX_CWD_CHARS } from "./task-cwd.js";
6
+ import { isDirectWritableRuleBehavior } from "./permission-rule-vocab.js";
6
7
  import { CONTROL_AND_BIDI_CHARS, hasBidiControls } from "./text-bidi.js";
7
8
  import { confirmRuleApproval, RULE_BEHAVIORS, parseRuleText, ruleReachesProgramRun, ruleToolGrammarOf, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, effectiveOrThrow, readRowBehavior, } from "@sema-agent/core";
8
9
  import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
@@ -59,6 +60,9 @@ export function parseRuleScope(text) {
59
60
  const root = text.slice("project:".length);
60
61
  return root === "" ? undefined : { kind: "project", root };
61
62
  }
63
+ export function sameWireRuleIdentity(row, identity) {
64
+ return row.behavior === identity.behavior && row.rule === identity.rule && row.scope === identity.scope;
65
+ }
62
66
  function storeForBucket(provider, ref) {
63
67
  if (typeof ref === "string")
64
68
  return provider.forPrincipal(ref);
@@ -256,7 +260,7 @@ export function createRuleConsentLane(stores, opts) {
256
260
  const monotonic = opts?.monotonic ?? (() => performance.now());
257
261
  const sleep = opts?.sleep ?? (async (ms) => new Promise((resolve) => setTimeout(resolve, ms)));
258
262
  const localImportLogger = createLogger();
259
- return {
263
+ const lane = {
260
264
  async persistCardRule(input) {
261
265
  let prepared;
262
266
  try {
@@ -710,6 +714,82 @@ export function createRuleConsentLane(stores, opts) {
710
714
  async removeRule(input) {
711
715
  return await removePersistedRule({ behavior: input.behavior, rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
712
716
  },
717
+ async writeRule(input) {
718
+ const written = parseRuleText(input.rule, input.behavior);
719
+ if ("reject" in written)
720
+ return { status: "rejected", reason: "rule_rejected", detail: `${written.reject.code}: ${written.reject.message}` };
721
+ if (input.rule !== written.rule.rule) {
722
+ return {
723
+ status: "rejected",
724
+ reason: "rule_rejected",
725
+ detail: `this rule text is not the spelling the store would hold: send ${JSON.stringify(written.rule.rule)} instead ` +
726
+ `— a rule written under one spelling and held under another cannot be listed or revoked by the bytes you sent`,
727
+ };
728
+ }
729
+ const row = {
730
+ behavior: input.behavior,
731
+ rule: input.rule,
732
+ scope: serializeRuleScope(input.scope),
733
+ tool: written.rule.tool,
734
+ match: written.rule.match,
735
+ command: written.rule.command,
736
+ adds: [],
737
+ };
738
+ const rowOf = (rows) => rows.find((r) => sameWireRuleIdentity(r, { behavior: input.behavior, rule: written.rule.rule, scope: row.scope }));
739
+ const before = await lane.listRules(input.principal);
740
+ const standing = rowOf(before.rows);
741
+ if (standing !== undefined)
742
+ return { status: "no-op", rev: before.rev, row: standing };
743
+ const prepared = await lane.prepareLocalImport(input.principal, [row]);
744
+ if (!prepared.ok) {
745
+ throw new Error(`a single-rule write hit the batch ceiling (${prepared.reason}) — the write door and the batch caps are out of step`);
746
+ }
747
+ if (prepared.ticket === undefined) {
748
+ const member = prepared.preview.members[0];
749
+ if (member === undefined || member.admitted) {
750
+ throw new Error("the rule screening reported neither an admitted row nor a refusal for this write — refusing to guess which of the two happened");
751
+ }
752
+ return { status: "rejected", reason: member.reason, detail: member.detail };
753
+ }
754
+ const redeemed = await lane.redeemLocalImport(input.principal, prepared.ticket);
755
+ if (!redeemed.ok) {
756
+ localImportLogger.warn("direct_rule_write_ticket_refused", {
757
+ reason: redeemed.reason,
758
+ detail: redeemed.detail,
759
+ note: "a self-minted, immediately redeemed ticket was refused — the outcome of this write is not known",
760
+ });
761
+ return { status: "indeterminate", detail: `${redeemed.reason}: ${redeemed.detail}` };
762
+ }
763
+ const member = redeemed.outcome.members[0];
764
+ if (member === undefined) {
765
+ throw new Error("the redemption of a single-rule write came back with no member — refusing to report an outcome nobody computed");
766
+ }
767
+ switch (member.outcome) {
768
+ case "persisted":
769
+ break;
770
+ case "no-op":
771
+ break;
772
+ case "indeterminate":
773
+ return { status: "indeterminate", detail: member.detail ?? member.reason ?? "the store did not say whether this rule landed" };
774
+ case "refused":
775
+ return { status: "indeterminate", detail: member.detail ?? "the store refused this rule without a machine-readable reason" };
776
+ default: {
777
+ const unknown = member.outcome;
778
+ throw new Error(`unknown single-rule write outcome: ${JSON.stringify(unknown)}`);
779
+ }
780
+ }
781
+ const landed = rowOf((await lane.listRules(input.principal)).rows);
782
+ if (landed === undefined) {
783
+ localImportLogger.warn("direct_rule_write_vanished_before_read_back", {
784
+ behavior: input.behavior,
785
+ scope: row.scope,
786
+ note: "the store reported this rule as landed, and it was already gone when the row was read back — a revocation landed in between",
787
+ });
788
+ return { status: "indeterminate", detail: "this rule landed and was already gone when it was read back — a revocation landed in between" };
789
+ }
790
+ return { status: "persisted", rev: redeemed.outcome.rev, row: landed };
791
+ },
713
792
  };
793
+ return lane;
714
794
  }
715
795
  //# sourceMappingURL=rules-consent.js.map
@@ -94,9 +94,13 @@ type _GuardWiringFleet = AssertAllKeysHandled<Exclude<keyof WiringSection<"fleet
94
94
  type _GuardWiringFleetReverse = AssertAllKeysHandled<Exclude<WiringFleetHandled, keyof WiringSection<"fleet">>>;
95
95
  type WiringMcpEntry = WiringSection<"mcp">[number];
96
96
  type WiringMcpExcluded = "error";
97
- type WiringMcpHandled = "name" | "source" | "status" | "errorCode" | "delivered" | "httpStatus" | "toolCount" | WiringMcpExcluded;
97
+ type WiringMcpHandled = "name" | "source" | "status" | "errorCode" | "delivered" | "httpStatus" | "toolCount" | "liveness" | WiringMcpExcluded;
98
98
  type _GuardWiringMcp = AssertAllKeysHandled<Exclude<keyof WiringMcpEntry, WiringMcpHandled>>;
99
99
  type _GuardWiringMcpReverse = AssertAllKeysHandled<Exclude<WiringMcpHandled, keyof WiringMcpEntry>>;
100
+ type WiringMcpLiveness = NonNullable<WiringMcpEntry["liveness"]>;
101
+ type WiringMcpLivenessHandled = "state" | "observedAt" | "errorCode";
102
+ type _GuardWiringMcpLiveness = AssertAllKeysHandled<Exclude<keyof WiringMcpLiveness, WiringMcpLivenessHandled>>;
103
+ type _GuardWiringMcpLivenessReverse = AssertAllKeysHandled<Exclude<WiringMcpLivenessHandled, keyof WiringMcpLiveness>>;
100
104
  type WiringRulesHandled = "storeWired" | "syncWired" | "orgGoverned";
101
105
  type _GuardWiringRules = AssertAllKeysHandled<Exclude<keyof WiringSection<"permissionRules">, WiringRulesHandled>>;
102
106
  type _GuardWiringRulesReverse = AssertAllKeysHandled<Exclude<WiringRulesHandled, keyof WiringSection<"permissionRules">>>;
@@ -180,12 +184,12 @@ type _GuardMcpReconnect = AssertAllKeysHandled<Exclude<keyof McpReconnectResult,
180
184
  type _NoDoubleBookMcpReconnect = AssertAllKeysHandled<Extract<McpReconnectProjected, McpReconnectWithheld>>;
181
185
  type _GuardMcpReconnectReverse = AssertAllKeysHandled<Exclude<McpReconnectProjected | McpReconnectWithheld, keyof McpReconnectResult>>;
182
186
  type McpRedialStatusProjected = "name" | "status" | "errorCode" | "httpStatus" | "delivered" | "serverInfo" | "error" | "transportClosed";
183
- type McpRedialStatusWithheld = "toolNames";
187
+ type McpRedialStatusWithheld = "toolNames" | "liveness";
184
188
  type _GuardMcpRedialStatus = AssertAllKeysHandled<Exclude<keyof McpServerStatus, McpRedialStatusProjected | McpRedialStatusWithheld>>;
185
189
  type _NoDoubleBookMcpRedialStatus = AssertAllKeysHandled<Extract<McpRedialStatusProjected, McpRedialStatusWithheld>>;
186
190
  type _GuardMcpRedialStatusReverse = AssertAllKeysHandled<Exclude<McpRedialStatusProjected | McpRedialStatusWithheld, keyof McpServerStatus>>;
187
191
  type McpPanelStatusProjected = "name" | "status" | "serverInfo" | "toolNames" | "error";
188
- type McpPanelStatusWithheld = "errorCode" | "httpStatus" | "delivered" | "transportClosed";
192
+ type McpPanelStatusWithheld = "errorCode" | "httpStatus" | "delivered" | "transportClosed" | "liveness";
189
193
  type _GuardMcpPanelStatus = AssertAllKeysHandled<Exclude<keyof McpServerStatus, McpPanelStatusProjected | McpPanelStatusWithheld>>;
190
194
  type _NoDoubleBookMcpPanelStatus = AssertAllKeysHandled<Extract<McpPanelStatusProjected, McpPanelStatusWithheld>>;
191
195
  type _GuardMcpPanelStatusReverse = AssertAllKeysHandled<Exclude<McpPanelStatusProjected | McpPanelStatusWithheld, keyof McpServerStatus>>;
@@ -288,7 +292,9 @@ export type CoreKeysetGuards = [
288
292
  _NoDoubleBookMcpPanelStatus,
289
293
  _GuardMcpPanelStatusReverse,
290
294
  _GuardMcpListingIncomplete,
291
- _GuardMcpListingIncompleteReverse
295
+ _GuardMcpListingIncompleteReverse,
296
+ _GuardWiringMcpLiveness,
297
+ _GuardWiringMcpLivenessReverse
292
298
  ];
293
299
  export {};
294
300
  //# sourceMappingURL=core-keyset-guard.d.ts.map
@@ -585,6 +585,22 @@ export declare function workspaceChangedEventData(ev: {
585
585
  * (「connection state is a fact about this leg's attempt, not about the assembly」)。
586
586
  * ⚠️ 空数组 ≠ 缺席:core 顶注逐字「an empty array is "this leg declared no servers", absence is an older
587
587
  * mint or an external derivation」⇒ 空数组照铸,别折成缺席。
588
+ * 🔴 **⑤ `liveness`(core 7.24.3 [ref] / S-402 提货件)—— 裁:租户可见,整条记录投,逐键挑**。
589
+ * `{state, observedAt, errorCode?}` = 引擎**最后一次观察**到的「这台还够得着吗」。受众判据与同行
590
+ * 其余键**同一条**(同主体同受众、引擎派生的闭形值、非 governance 侧信道 —— 它同样不进
591
+ * `configFingerprint`),所以这一格不需要新的裁定,只需要新的**读法**。
592
+ * 🔴 **契约一行(client-core CC-36 的那句话):活性位独立于名册申报与 re-dial 结果。**
593
+ * `status` 是拨号那一刻的判决且 core **有意冻结**(服务器中途死了仍读 `connected`);re-dial 回执的
594
+ * `outcome` 是一次动作的判词;这一格是连接层最后学到的事,自带时戳。三者**合法地可以不一致** ——
595
+ * `status:"failed"` 与 `liveness.state:"reachable"` 同行是真行(服务器答了握手、答的是协议错:
596
+ * 活着,且配错了)。⇒ 本仓**不合并、不互相校验、不据一个推另一个**(与 ① 里「两条 MCP 读面同名
597
+ * 不同源、合并只会造出『哪个才算数』」逐字同源)。
598
+ * ⚠️ **缺席是第四读数,不是一个词**:core 顶注逐字「a consumer must not fold absence into
599
+ * `unknown`」——`unknown` 是**更强**的断言(引擎看了、看不出来)。
600
+ * 🔴 **但本仓出口上的「缺席」比 core 源端的更宽**(codex r1 [medium] 采纳):core 那边是
601
+ * 压根没有观察(`skipped` 的行没走到 transport / `invalid_config` 的声明从没被拨过 / 老 mint),
602
+ * 而经过本投影后**判形没过也落缺席**(词出闭集 / `observedAt` 非有限数 / 整只不是对象)。
603
+ * ⇒ 对消费端,缺席只能读成「**没有可用的观察记录**」,**不许**据它反推这条腿拨没拨过号。
588
604
  * · `writeProtection`(`{ targetView: "spelling-only" | "target" }`;core 7.20.1 [ref] / clay [ref] 丙,
589
605
  * S-361 提货件)—— **裁:租户可见,逐字透传**。它是 per-leg 的读面:「这条腿的写保护判官,拿到的是
590
606
  * 哪一种目标读法」——`"target"` = 判官握着执行环境,拼法没命中时会在**解析后的真目标**上再判一次;
@@ -1,5 +1,5 @@
1
1
  import { redactErrorMessage, redactModelText } from "../observability/run-terminal.js";
2
- import { MCP_DELIVERY_VERDICTS, screenGateOutcome, ToolRoster, ToolRosterDelta, WRITE_PROTECTION_TARGET_VIEWS } from "@sema-agent/core";
2
+ import { MCP_DELIVERY_VERDICTS, MCP_LIVENESS_STATES, screenGateOutcome, ToolRoster, ToolRosterDelta, WRITE_PROTECTION_TARGET_VIEWS } from "@sema-agent/core";
3
3
  import { Value } from "typebox/value";
4
4
  import { recordFailOpen } from "../observability/fail-open.js";
5
5
  import { redactSecrets, redactDeep } from "./redact.js";
@@ -507,6 +507,17 @@ export function wiringManifestEventData(ev) {
507
507
  const status = str(e.status);
508
508
  if (name === undefined || status === undefined)
509
509
  continue;
510
+ const liveness = (() => {
511
+ const rec = sec(e.liveness);
512
+ if (rec === undefined)
513
+ return undefined;
514
+ const state = str(rec.state);
515
+ const observedAt = num(rec.observedAt);
516
+ if (state === undefined || !MCP_LIVENESS_STATES.includes(state) || observedAt === undefined)
517
+ return undefined;
518
+ const errorCode = str(rec.errorCode);
519
+ return { state, observedAt, ...(errorCode === undefined ? {} : { errorCode }) };
520
+ })();
510
521
  rows.push({
511
522
  name,
512
523
  status,
@@ -515,6 +526,7 @@ export function wiringManifestEventData(ev) {
515
526
  ...(str(e.errorCode) !== undefined ? { errorCode: e.errorCode } : {}),
516
527
  ...(typeof e.delivered === "string" && MCP_DELIVERY_VERDICTS.includes(e.delivered) ? { delivered: e.delivered } : {}),
517
528
  ...(num(e.httpStatus) !== undefined ? { httpStatus: e.httpStatus } : {}),
529
+ ...(liveness !== undefined ? { liveness } : {}),
518
530
  });
519
531
  }
520
532
  return rows;
@@ -2,7 +2,11 @@ import { displayUrlOf, stripFormatCharacters } from "@sema-agent/core";
2
2
  import { recordFailOpen } from "../observability/fail-open.js";
3
3
  export const normalizeForRedaction = stripFormatCharacters;
4
4
  const REDACTION_TOKEN_RE_SRC = String.raw `«redacted(?::[a-z-]{1,16}){0,2}»`;
5
- const atLeast = (cls, min) => `${cls}{${min}}${cls}*`;
5
+ const ARM_VALUE_CLASSES = [];
6
+ const atLeast = (cls, min) => {
7
+ ARM_VALUE_CLASSES.push(cls);
8
+ return `${cls}{${min}}${cls}*`;
9
+ };
6
10
  const ENTROPY_SCAN_MAX = 96;
7
11
  const twoStrongAhead = (valueClass) => String.raw `(?=${valueClass}{0,${ENTROPY_SCAN_MAX}}[0-9A-Z]${valueClass}{0,${ENTROPY_SCAN_MAX}}[0-9A-Z])`;
8
12
  const IDENT_CONNECTORS = "[_./-]";
@@ -586,9 +590,11 @@ function collectStickySpans(a, text, at, out) {
586
590
  }
587
591
  }
588
592
  const WORD_SEGMENT_CLASS = "[A-Za-z0-9._~+/-]";
593
+ const LEXICAL_KEYWORD_ARMS = new Set();
589
594
  const keywordArm = (keywordSrc, valueClass, min, tailSrc = "") => {
590
595
  const re = new RegExp(`(?:${keywordSrc}\\s+${atLeast(valueClass, min)}${tailSrc}` +
591
596
  `|(?<!${WORD_SEGMENT_CLASS})${keywordSrc}(?:${twoStrongAhead(valueClass)}|${noConnectorAhead(valueClass)})${atLeast(valueClass, min)}${tailSrc})`, "g");
597
+ LEXICAL_KEYWORD_ARMS.add(re);
592
598
  return re;
593
599
  };
594
600
  const CREDENTIAL_NAME_WORDS = [
@@ -1511,6 +1517,7 @@ const armPool = (() => {
1511
1517
  rank,
1512
1518
  order,
1513
1519
  phase: 1,
1520
+ lexical: LEXICAL_KEYWORD_ARMS.has(re),
1514
1521
  collect: boundary === undefined
1515
1522
  ? (text, _json, _observe, out) => collectRegexSpans(arm, text, out)
1516
1523
  : (text, _json, _observe, out) => collectValueArmSpans(boundary, text, out),
@@ -1526,6 +1533,7 @@ const armPool = (() => {
1526
1533
  rank: ARM_RANK.structural,
1527
1534
  order,
1528
1535
  phase,
1536
+ lexical: false,
1529
1537
  collect: (text, json, observe, out) => scan(text, json, observe, (start, end) => out.push({ start, end, repl, rank: ARM_RANK.structural, order })),
1530
1538
  retry: undefined,
1531
1539
  headSrc: undefined,
@@ -1550,14 +1558,32 @@ const armPool = (() => {
1550
1558
  members.push(regexMember(row, ARM_RANK.shape, members.length, false));
1551
1559
  for (const row of FRAGMENT_SECRET_PATTERNS)
1552
1560
  members.push(regexMember(row, ARM_RANK.fragment, members.length, false));
1561
+ for (const [re] of KEYWORD_SECRET_PATTERNS) {
1562
+ if (!LEXICAL_KEYWORD_ARMS.has(re))
1563
+ throw new Error(`redact: keyword arm /${re.source}/ is not registered as lexical — build it with keywordArm() so the bounded-unit scope can exclude it`);
1564
+ }
1553
1565
  const retryMembers = members.filter((m) => m.retry !== undefined);
1554
1566
  const phase1 = members.filter((m) => m.phase === 1);
1555
1567
  const phase2 = members.filter((m) => m.phase === 2);
1568
+ const laneOf = (scope) => {
1569
+ const inScope = (m) => {
1570
+ switch (scope) {
1571
+ case "free-text": return true;
1572
+ case "bounded-unit": return !m.lexical;
1573
+ }
1574
+ };
1575
+ return { phase1: phase1.filter(inScope), phase2: phase2.filter(inScope), retry: retryMembers.filter(inScope) };
1576
+ };
1577
+ const LANES = {
1578
+ "free-text": laneOf("free-text"),
1579
+ "bounded-unit": laneOf("bounded-unit"),
1580
+ };
1556
1581
  const headScreen = new RegExp(`(?:${retryMembers.map((m) => m.headSrc).join("|")})`, "y");
1557
1582
  const headFirstUnit = unionMembers(retryMembers.map((m) => m.headFirst)) ?? new Uint32Array(0x10000 >>> 5).fill(0xffffffff);
1558
1583
  poolArmCount = members.length;
1559
1584
  poolBoundaryArmCount = retryMembers.length;
1560
- return (text, wantCount, json, observe) => {
1585
+ return (text, wantCount, json, observe, scope) => {
1586
+ const lane = LANES[scope];
1561
1587
  const spans = [];
1562
1588
  let tried;
1563
1589
  const freshCovered = (from) => {
@@ -1582,25 +1608,25 @@ const armPool = (() => {
1582
1608
  return at.sort((x, y) => x - y);
1583
1609
  };
1584
1610
  const closeCoveragePositions = (from) => {
1585
- if (retryMembers.length === 0 || spans.length === from)
1611
+ if (lane.retry.length === 0 || spans.length === from)
1586
1612
  return;
1587
1613
  for (let at = freshCovered(from); at.length > 0;) {
1588
1614
  const before = spans.length;
1589
- for (const m of retryMembers)
1615
+ for (const m of lane.retry)
1590
1616
  m.retry(text, at, spans);
1591
1617
  at = freshCovered(before);
1592
1618
  }
1593
1619
  };
1594
1620
  setScanEvidence(NO_CREDENTIAL_SPANS);
1595
- for (const m of phase1)
1621
+ for (const m of lane.phase1)
1596
1622
  m.collect(text, json, observe, spans);
1597
1623
  closeCoveragePositions(0);
1598
- if (phase2.length > 0) {
1624
+ if (lane.phase2.length > 0) {
1599
1625
  let evidence = credentialSpans(text, spans, false);
1600
1626
  for (let level = 0;; level += 1) {
1601
1627
  const before = spans.length;
1602
1628
  setScanEvidence(evidence);
1603
- for (const m of phase2)
1629
+ for (const m of lane.phase2)
1604
1630
  m.collect(text, json, observe && level === 0, spans);
1605
1631
  closeCoveragePositions(before);
1606
1632
  forensicLevels += 1;
@@ -1642,8 +1668,8 @@ let onRedaction;
1642
1668
  export function setRedactionObserver(fn) {
1643
1669
  onRedaction = fn;
1644
1670
  }
1645
- function applyRules(text, wantCount, json, observe) {
1646
- return armPool(text, wantCount, json, observe);
1671
+ function applyRules(text, wantCount, json, observe, scope) {
1672
+ return armPool(text, wantCount, json, observe, scope);
1647
1673
  }
1648
1674
  function reportHits(hits) {
1649
1675
  if (onRedaction === undefined)
@@ -1656,20 +1682,20 @@ function reportHits(hits) {
1656
1682
  }
1657
1683
  }
1658
1684
  }
1659
- function runRedaction(text, observe, json, wantHits) {
1685
+ function runRedaction(text, observe, json, wantHits, scope) {
1660
1686
  const wantCount = wantHits || (onRedaction !== undefined && observe);
1661
1687
  const normalized = normalizeForRedaction(text);
1662
- const r = applyRules(normalized, wantCount, json, observe);
1688
+ const r = applyRules(normalized, wantCount, json, observe, scope);
1663
1689
  forgetEscapedSeparatorTails();
1664
1690
  if (observe)
1665
1691
  reportHits(r.hits);
1666
1692
  return { text: r.out, hits: r.hits, spans: r.kept };
1667
1693
  }
1668
1694
  export function redactSecrets(text, opts) {
1669
- return runRedaction(text, opts?.observe ?? true, opts?.json === true, false).text;
1695
+ return runRedaction(text, opts?.observe ?? true, opts?.json === true, false, "free-text").text;
1670
1696
  }
1671
1697
  export function redactSecretsWithArms(text, opts) {
1672
- const r = runRedaction(text, opts?.observe ?? true, opts?.json === true, true);
1698
+ const r = runRedaction(text, opts?.observe ?? true, opts?.json === true, true, "free-text");
1673
1699
  return { text: r.text, arms: new Set(r.hits.map((h) => h.arm)) };
1674
1700
  }
1675
1701
  const REDACT_MAX_DEPTH = 64;
@@ -1736,12 +1762,90 @@ export function isHierarchicalUrl(raw) {
1736
1762
  const u = new URL(raw);
1737
1763
  return u.href.startsWith(`${u.protocol}//`);
1738
1764
  }
1765
+ function decodeToFixedPoint(s, maxRounds = 3) {
1766
+ let cur = s;
1767
+ for (let round = 0; round < maxRounds; round += 1) {
1768
+ if (!cur.includes("%"))
1769
+ return { text: cur, residue: false };
1770
+ const next = cur.replace(/%([0-9A-Fa-f]{2})/g, (_whole, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
1771
+ if (next === cur)
1772
+ break;
1773
+ cur = next;
1774
+ }
1775
+ return { text: cur, residue: cur.includes("%") };
1776
+ }
1777
+ const VIEW_FILLER_PREFERENCE = ["a", "z", "_", "-", "0"];
1778
+ let armValueClassViewsCache;
1779
+ function armValueClassViews() {
1780
+ armValueClassViewsCache ??= [...new Set(ARM_VALUE_CLASSES)].map((cls) => {
1781
+ const inClass = new RegExp(`^(?:${cls})$`, "u");
1782
+ const filler = VIEW_FILLER_PREFERENCE.find((c) => inClass.test(c));
1783
+ if (filler === undefined)
1784
+ throw new Error(`redact: arm value class ${cls} admits none of the view fillers — add one that it accepts`);
1785
+ return { inClass, filler };
1786
+ });
1787
+ return armValueClassViewsCache;
1788
+ }
1789
+ function boundedUnitView(unit, inClass, filler) {
1790
+ let out = "";
1791
+ for (const ch of unit)
1792
+ out += inClass.test(ch) ? ch : filler;
1793
+ return out;
1794
+ }
1795
+ function unitCarriesCredential(unit) {
1796
+ const decoded = decodeToFixedPoint(unit);
1797
+ const views = new Set();
1798
+ for (const spelling of [unit, decoded.text])
1799
+ for (const v of armValueClassViews())
1800
+ views.add(boundedUnitView(spelling, v.inClass, v.filler));
1801
+ views.delete("");
1802
+ const hit = [...views].some((view) => runRedaction(view, true, false, false, "bounded-unit").text !== view);
1803
+ return { hit, residue: decoded.residue };
1804
+ }
1805
+ function displaySafePathname(pathname) {
1806
+ const parts = pathname.split("/");
1807
+ let withheld = false;
1808
+ for (let i = 0; i < parts.length; i += 1) {
1809
+ const seg = parts[i];
1810
+ if (seg === "")
1811
+ continue;
1812
+ const verdict = unitCarriesCredential(seg);
1813
+ if (!verdict.hit && !verdict.residue)
1814
+ continue;
1815
+ if (isAllRedactionTokens(redactSecrets(seg)))
1816
+ continue;
1817
+ parts[i] = WITHHELD_PATH_SEGMENT_TOKEN;
1818
+ withheld = true;
1819
+ }
1820
+ return { text: withheld ? parts.join("/") : pathname, withheld };
1821
+ }
1822
+ const ALL_REDACTION_TOKENS_RE = new RegExp(`^(?:${REDACTION_TOKEN_RE_SRC})+$`);
1823
+ const isAllRedactionTokens = (text) => ALL_REDACTION_TOKENS_RE.test(text);
1824
+ const WITHHELD_PATH_SEGMENT_TOKEN = "«redacted:key»";
1825
+ if (!REDACTION_TOKENS.has(WITHHELD_PATH_SEGMENT_TOKEN)) {
1826
+ throw new Error(`redact: ${WITHHELD_PATH_SEGMENT_TOKEN} is no longer produced by the arm tables — the path-segment filler must stay a closed-set member`);
1827
+ }
1739
1828
  function shapeLawCredentialInQueryOrFragment(canonical) {
1740
1829
  const normalized = normalizeForRedaction(canonical);
1741
1830
  const boundary = normalized.indexOf("?");
1742
1831
  if (boundary < 0)
1743
1832
  return false;
1744
- return runRedaction(canonical, false, false, false).spans.some((sp) => sp.end > boundary);
1833
+ if (runRedaction(canonical, false, false, false, "free-text").spans.some((sp) => sp.end > boundary))
1834
+ return true;
1835
+ for (const unit of normalized.slice(boundary + 1).split("&").flatMap((sg) => {
1836
+ const eq = sg.indexOf("=");
1837
+ return eq === -1 ? [sg] : [sg.slice(0, eq), sg.slice(eq + 1)];
1838
+ })) {
1839
+ if (unit === "")
1840
+ continue;
1841
+ const verdict = unitCarriesCredential(unit);
1842
+ if (verdict.hit || verdict.residue)
1843
+ return true;
1844
+ const decoded = decodeToFixedPoint(unit).text;
1845
+ if (decoded.includes("://") || decoded.includes("@"))
1846
+ return true;
1847
+ }
1848
+ return false;
1745
1849
  }
1746
1850
  let displaySafeFailClosedWarned = false;
1747
1851
  export function resetDisplaySafeFailClosedWarnForTest() {
@@ -1756,12 +1860,19 @@ export function buildDisplaySafeUrlChecked(raw, positionalLaw = displayUrlOf) {
1756
1860
  if (!isHierarchicalUrl(raw))
1757
1861
  return { text: DISPLAY_SAFE_URL_TOKEN, failedClosed: false };
1758
1862
  const parsed = new URL(raw);
1863
+ const safePath = displaySafePathname(parsed.pathname);
1759
1864
  if (shapeLawCredentialInQueryOrFragment(parsed.href)) {
1760
1865
  noteDisplaySafeFailClosed("query-and-fragment-withheld");
1761
- const root = redactSecrets(`${parsed.protocol}//${parsed.host}${parsed.pathname}`).replace(/\/+$/, "");
1866
+ const root = redactSecrets(`${parsed.protocol}//${parsed.host}${safePath.text}`);
1762
1867
  return { text: `${root}?${WITHHELD_VALUE_TOKEN}`, failedClosed: true };
1763
1868
  }
1764
- return { text: redactSecrets(positionalLaw(raw)), failedClosed: false };
1869
+ if (!safePath.withheld)
1870
+ return { text: redactSecrets(positionalLaw(raw)), failedClosed: false };
1871
+ noteDisplaySafeFailClosed("path-segment-withheld");
1872
+ const positional = positionalLaw(raw);
1873
+ const cut = positional.indexOf("?");
1874
+ const root = redactSecrets(`${parsed.protocol}//${parsed.host}${safePath.text}`);
1875
+ return { text: cut < 0 ? root.replace(/\/+$/, "") : `${root}${positional.slice(cut)}`, failedClosed: true };
1765
1876
  }
1766
1877
  export function buildDisplaySafeRefusal(text, sourceUrls) {
1767
1878
  if (text === "")
@@ -1778,7 +1889,7 @@ function noteDisplaySafeFailClosed(level) {
1778
1889
  if (displaySafeFailClosedWarned)
1779
1890
  return;
1780
1891
  displaySafeFailClosedWarned = true;
1781
- process.emitWarning(`display-safe URL composition fell back to fail-closed (${level}): the positional law reshaped the input so that a fragment the shape law had judged a credential survived into the product. ` +
1782
- `The product is safe (the offending part is withheld), but this deployment has an input hitting the composition seam — report it upstream. No original bytes are logged.`, "SemaRedactionFailClosed");
1892
+ process.emitWarning(`display-safe URL composition fell back to fail-closed (${level}): a part of the configured URL was judged to carry a credential that the composition cannot redact at byte granularity, so that whole part is withheld. ` +
1893
+ `The product is safe, but this deployment has an input hitting the composition seam — report it upstream. No original bytes are logged.`, "SemaRedactionFailClosed");
1783
1894
  }
1784
1895
  //# sourceMappingURL=redact.js.map