@sema-agent/server 7.90.0 → 7.91.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/adoption/plan.js +11 -0
  2. package/dist/boot/runner-deps.d.ts +6 -0
  3. package/dist/boot/runner-deps.js +11 -0
  4. package/dist/boot/session-faces.d.ts +6 -0
  5. package/dist/boot/session-faces.js +10 -2
  6. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  7. package/dist/boot/stage-03-store-backend.js +2 -2
  8. package/dist/boot/stage-04-stores.d.ts +1 -0
  9. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  10. package/dist/boot/stage-06-runners.d.ts +1 -0
  11. package/dist/boot/stage-06-runners.js +2 -2
  12. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  13. package/dist/boot/stage-08-reapers.d.ts +1 -0
  14. package/dist/boot/stage-09-leader.d.ts +1 -0
  15. package/dist/boot/stage-09-leader.js +2 -2
  16. package/dist/boot/stage-10-http-server.d.ts +1 -0
  17. package/dist/boot/stage-10-http-server.js +1 -0
  18. package/dist/boot/stores.d.ts +2 -0
  19. package/dist/boot/stores.js +11 -3
  20. package/dist/cross-session-settings.d.ts +6 -2
  21. package/dist/http/routes/capabilities.js +4 -1
  22. package/dist/http/routes/rules.d.ts +11 -0
  23. package/dist/http/routes/rules.js +54 -5
  24. package/dist/http/routes/sessions-list.js +6 -1
  25. package/dist/http/server.d.ts +12 -0
  26. package/dist/peer-directory.d.ts +114 -0
  27. package/dist/peer-directory.js +136 -0
  28. package/dist/permission-rule-vocab.d.ts +27 -1
  29. package/dist/permission-rule-vocab.js +8 -0
  30. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +86 -0
  31. package/dist/plugins/mailbox-recipient-lifecycle.js +10 -0
  32. package/dist/plugins/mailbox-store-sql.d.ts +18 -2
  33. package/dist/plugins/mailbox-store-sql.js +77 -1
  34. package/dist/plugins/store-contracts.d.ts +36 -1
  35. package/dist/plugins/store-contracts.js +10 -0
  36. package/dist/rules-consent.d.ts +74 -1
  37. package/dist/rules-consent.js +81 -1
  38. package/dist/trace/redact.js +95 -6
  39. package/dist/trace/sema-provenance.d.ts +7 -6
  40. package/package.json +2 -2
@@ -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
@@ -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 = "[_./-]";
@@ -1736,12 +1740,90 @@ export function isHierarchicalUrl(raw) {
1736
1740
  const u = new URL(raw);
1737
1741
  return u.href.startsWith(`${u.protocol}//`);
1738
1742
  }
1743
+ function decodeToFixedPoint(s, maxRounds = 3) {
1744
+ let cur = s;
1745
+ for (let round = 0; round < maxRounds; round += 1) {
1746
+ if (!cur.includes("%"))
1747
+ return { text: cur, residue: false };
1748
+ const next = cur.replace(/%([0-9A-Fa-f]{2})/g, (_whole, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
1749
+ if (next === cur)
1750
+ break;
1751
+ cur = next;
1752
+ }
1753
+ return { text: cur, residue: cur.includes("%") };
1754
+ }
1755
+ const VIEW_FILLER_PREFERENCE = ["a", "z", "_", "-", "0"];
1756
+ let armValueClassViewsCache;
1757
+ function armValueClassViews() {
1758
+ armValueClassViewsCache ??= [...new Set(ARM_VALUE_CLASSES)].map((cls) => {
1759
+ const inClass = new RegExp(`^(?:${cls})$`, "u");
1760
+ const filler = VIEW_FILLER_PREFERENCE.find((c) => inClass.test(c));
1761
+ if (filler === undefined)
1762
+ throw new Error(`redact: arm value class ${cls} admits none of the view fillers — add one that it accepts`);
1763
+ return { inClass, filler };
1764
+ });
1765
+ return armValueClassViewsCache;
1766
+ }
1767
+ function boundedUnitView(unit, inClass, filler) {
1768
+ let out = "";
1769
+ for (const ch of unit)
1770
+ out += inClass.test(ch) ? ch : filler;
1771
+ return out;
1772
+ }
1773
+ function unitCarriesCredential(unit) {
1774
+ const decoded = decodeToFixedPoint(unit);
1775
+ const views = new Set();
1776
+ for (const spelling of [unit, decoded.text])
1777
+ for (const v of armValueClassViews())
1778
+ views.add(boundedUnitView(spelling, v.inClass, v.filler));
1779
+ views.delete("");
1780
+ const hit = [...views].some((view) => redactSecrets(view) !== view);
1781
+ return { hit, residue: decoded.residue };
1782
+ }
1783
+ function displaySafePathname(pathname) {
1784
+ const parts = pathname.split("/");
1785
+ let withheld = false;
1786
+ for (let i = 0; i < parts.length; i += 1) {
1787
+ const seg = parts[i];
1788
+ if (seg === "")
1789
+ continue;
1790
+ const verdict = unitCarriesCredential(seg);
1791
+ if (!verdict.hit && !verdict.residue)
1792
+ continue;
1793
+ if (isAllRedactionTokens(redactSecrets(seg)))
1794
+ continue;
1795
+ parts[i] = WITHHELD_PATH_SEGMENT_TOKEN;
1796
+ withheld = true;
1797
+ }
1798
+ return { text: withheld ? parts.join("/") : pathname, withheld };
1799
+ }
1800
+ const ALL_REDACTION_TOKENS_RE = new RegExp(`^(?:${REDACTION_TOKEN_RE_SRC})+$`);
1801
+ const isAllRedactionTokens = (text) => ALL_REDACTION_TOKENS_RE.test(text);
1802
+ const WITHHELD_PATH_SEGMENT_TOKEN = "«redacted:key»";
1803
+ if (!REDACTION_TOKENS.has(WITHHELD_PATH_SEGMENT_TOKEN)) {
1804
+ 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`);
1805
+ }
1739
1806
  function shapeLawCredentialInQueryOrFragment(canonical) {
1740
1807
  const normalized = normalizeForRedaction(canonical);
1741
1808
  const boundary = normalized.indexOf("?");
1742
1809
  if (boundary < 0)
1743
1810
  return false;
1744
- return runRedaction(canonical, false, false, false).spans.some((sp) => sp.end > boundary);
1811
+ if (runRedaction(canonical, false, false, false).spans.some((sp) => sp.end > boundary))
1812
+ return true;
1813
+ for (const unit of normalized.slice(boundary + 1).split("&").flatMap((sg) => {
1814
+ const eq = sg.indexOf("=");
1815
+ return eq === -1 ? [sg] : [sg.slice(0, eq), sg.slice(eq + 1)];
1816
+ })) {
1817
+ if (unit === "")
1818
+ continue;
1819
+ const verdict = unitCarriesCredential(unit);
1820
+ if (verdict.hit || verdict.residue)
1821
+ return true;
1822
+ const decoded = decodeToFixedPoint(unit).text;
1823
+ if (decoded.includes("://") || decoded.includes("@"))
1824
+ return true;
1825
+ }
1826
+ return false;
1745
1827
  }
1746
1828
  let displaySafeFailClosedWarned = false;
1747
1829
  export function resetDisplaySafeFailClosedWarnForTest() {
@@ -1756,12 +1838,19 @@ export function buildDisplaySafeUrlChecked(raw, positionalLaw = displayUrlOf) {
1756
1838
  if (!isHierarchicalUrl(raw))
1757
1839
  return { text: DISPLAY_SAFE_URL_TOKEN, failedClosed: false };
1758
1840
  const parsed = new URL(raw);
1841
+ const safePath = displaySafePathname(parsed.pathname);
1759
1842
  if (shapeLawCredentialInQueryOrFragment(parsed.href)) {
1760
1843
  noteDisplaySafeFailClosed("query-and-fragment-withheld");
1761
- const root = redactSecrets(`${parsed.protocol}//${parsed.host}${parsed.pathname}`).replace(/\/+$/, "");
1844
+ const root = redactSecrets(`${parsed.protocol}//${parsed.host}${safePath.text}`);
1762
1845
  return { text: `${root}?${WITHHELD_VALUE_TOKEN}`, failedClosed: true };
1763
1846
  }
1764
- return { text: redactSecrets(positionalLaw(raw)), failedClosed: false };
1847
+ if (!safePath.withheld)
1848
+ return { text: redactSecrets(positionalLaw(raw)), failedClosed: false };
1849
+ noteDisplaySafeFailClosed("path-segment-withheld");
1850
+ const positional = positionalLaw(raw);
1851
+ const cut = positional.indexOf("?");
1852
+ const root = redactSecrets(`${parsed.protocol}//${parsed.host}${safePath.text}`);
1853
+ return { text: cut < 0 ? root.replace(/\/+$/, "") : `${root}${positional.slice(cut)}`, failedClosed: true };
1765
1854
  }
1766
1855
  export function buildDisplaySafeRefusal(text, sourceUrls) {
1767
1856
  if (text === "")
@@ -1778,7 +1867,7 @@ function noteDisplaySafeFailClosed(level) {
1778
1867
  if (displaySafeFailClosedWarned)
1779
1868
  return;
1780
1869
  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");
1870
+ 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. ` +
1871
+ `The product is safe, but this deployment has an input hitting the composition seam — report it upstream. No original bytes are logged.`, "SemaRedactionFailClosed");
1783
1872
  }
1784
1873
  //# sourceMappingURL=redact.js.map
@@ -17,14 +17,15 @@ export declare const SEMA_PROVENANCE_KINDS: readonly ["agent_message"];
17
17
  * 有意**不**放行的 lane 词(EXCLUDED,逐词记账;运行期落到「丢整键」臂 —— 与闭集外的未知词同臂,但这里的词是
18
18
  * core 词表**内**的、经人裁定不上 wire 的):
19
19
  * · `cross_session_message` —— core fc52b4d4 [ref] §4.1(session-box drain 铸;`from` 是对端**地址**、`peerMeta`
20
- * 携 typed peer 记录)。[ref] 候:本仓不可达(`RunnerDeps.peerDirectory` 零声明点)+ 注入帧渲染形候 [ref] @server③
21
- * 表态;与 core-keyset-guard.ts ① 的 `crossSessionMessage` / ①-nested 的 `peerMeta` 同批同判,接线时三处同翻。
20
+ * 携 typed peer 记录)。🔴 **7.91.0 订正**:`RunnerDeps.peerDirectory` 座已接线([ref])⇒ 「本仓不可达」那半句作废,
21
+ * 有席的部署上这个 lane 词真的会被铸出来;仍不放行的理由只剩**注入帧渲染形候 [ref] @server③ 表态**。
22
+ * 与 core-keyset-guard.ts ① 的 `crossSessionMessage` / ①-nested 的 `peerMeta` 同批同判,表态之后三处同翻。
22
23
  * · `cross_session_notice` —— core 7.4.0 [ref] §4.4/§5.2(片4;本闭集 2026-09-05 真红出它)。**同一条车道的
23
24
  * 第二个载体**,不是新判据:同一个 session-box drain 铸(`agents/peer-session-drain.js` 的 notice 臂),同一个
24
- * `RunnerDeps.peerDirectory` 座前提 ⇒ 本仓同样不可达;渲染形与它的载体 `crossSessionNotice`(core-keyset-guard.ts
25
- * ① 的同名排除键)一同候 [ref] 表态。放进 EXCLUDED 而不是 PROJECTED,是**沿用**上一行的裁定而不是新增特判:
26
- * 车道没接线时放行一个 lane 词,等于替 [ref] 预先声明一种 cli 渲染形。[ref] 接线那天三处(本簿、载体键、
27
- * ①-nested 的 `peerMeta`)同批翻。
25
+ * `RunnerDeps.peerDirectory` 座前提(同样自 7.91.0 起在场);渲染形与它的载体 `crossSessionNotice`
26
+ * (core-keyset-guard.ts ① 的同名排除键)一同候 [ref] 表态。放进 EXCLUDED 而不是 PROJECTED,是**沿用**上一行的
27
+ * 裁定而不是新增特判:表态未出前放行一个 lane 词,等于替它预先声明一种 cli 渲染形。表态落定那天三处(本簿、
28
+ * 载体键、①-nested 的 `peerMeta`)同批翻。
28
29
  */
29
30
  export declare const SEMA_PROVENANCE_KINDS_EXCLUDED: readonly ["cross_session_message", "cross_session_notice"];
30
31
  /** 本仓两条腿入参的**形**(不是 core 类型:入参是运行期载荷,`kind` 先当 string 收、经闭集才成 typed)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.90.0",
3
+ "version": "7.91.1",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -74,7 +74,7 @@
74
74
  "sharp": "^0.35.3"
75
75
  },
76
76
  "devDependencies": {
77
- "@sema-agent/sdk": "9.6.0",
77
+ "@sema-agent/sdk": "9.10.0",
78
78
  "@types/libsodium-wrappers": "^0.7.14",
79
79
  "@types/node": "22.10.2",
80
80
  "@types/pg": "^8.20.0",