@sema-agent/server 7.15.0 → 7.16.0-rc.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 (61) hide show
  1. package/USAGE.md +36 -2
  2. package/dist/approval-card.d.ts +65 -0
  3. package/dist/approval-card.js +54 -6
  4. package/dist/approval-reconciler.d.ts +17 -1
  5. package/dist/boot/resolve-spec.js +31 -4
  6. package/dist/boot/runner-deps.d.ts +41 -2
  7. package/dist/boot/runner-deps.js +43 -0
  8. package/dist/boot/session-faces.js +26 -4
  9. package/dist/boot/shutdown.js +17 -0
  10. package/dist/boot/stores.js +20 -0
  11. package/dist/config-center/apply-effective.d.ts +14 -0
  12. package/dist/config-center/apply-effective.js +81 -1
  13. package/dist/config-types.d.ts +28 -2
  14. package/dist/config.js +71 -37
  15. package/dist/fleet/fleet-terminal-window.d.ts +12 -0
  16. package/dist/fleet/fleet-terminal-window.js +27 -4
  17. package/dist/http/active-run-conflict.d.ts +41 -1
  18. package/dist/http/active-run-conflict.js +24 -10
  19. package/dist/http/routes/approvals-assistant.d.ts +11 -3
  20. package/dist/http/routes/approvals-assistant.js +97 -20
  21. package/dist/http/routes/capabilities.js +8 -1
  22. package/dist/http/routes/diagnostics.d.ts +18 -0
  23. package/dist/http/routes/diagnostics.js +26 -0
  24. package/dist/http/routes/runs.js +63 -17
  25. package/dist/http/routes/side-query.js +15 -1
  26. package/dist/http/routes/tasks.js +32 -3
  27. package/dist/http/server.js +67 -13
  28. package/dist/leader/fanout.d.ts +18 -0
  29. package/dist/leader/fanout.js +34 -1
  30. package/dist/leader/leader.js +9 -5
  31. package/dist/leader/wire.js +16 -5
  32. package/dist/main.js +1 -0
  33. package/dist/model-select.d.ts +43 -1
  34. package/dist/model-select.js +70 -2
  35. package/dist/observability/fail-open.d.ts +8 -0
  36. package/dist/observability/fail-open.js +8 -0
  37. package/dist/observability/metrics.js +3 -0
  38. package/dist/parent-watch.d.ts +57 -0
  39. package/dist/parent-watch.js +108 -0
  40. package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
  41. package/dist/plugins/checkpoint-store-sql.js +133 -7
  42. package/dist/plugins/local-checkpoint-store.js +11 -1
  43. package/dist/plugins/memory-embedder-fingerprint.d.ts +119 -0
  44. package/dist/plugins/memory-embedder-fingerprint.js +280 -0
  45. package/dist/plugins/permission-rule-store-sql.d.ts +0 -3
  46. package/dist/plugins/permission-rule-store-sql.js +1 -7
  47. package/dist/plugins/pg-pool.js +3 -0
  48. package/dist/plugins/store-backend.d.ts +5 -6
  49. package/dist/plugins/store-backend.js +4 -1
  50. package/dist/plugins/store-contracts.d.ts +17 -0
  51. package/dist/plugins/store-contracts.js +33 -0
  52. package/dist/plugins/tidb-pool.js +7 -0
  53. package/dist/plugins/tool-result-store-sql.d.ts +18 -13
  54. package/dist/plugins/tool-result-store-sql.js +50 -29
  55. package/dist/run-local.js +1 -0
  56. package/dist/tool-approval.d.ts +9 -0
  57. package/dist/tool-approval.js +74 -3
  58. package/dist/trace/project.js +8 -0
  59. package/dist/trace/redact.d.ts +14 -1
  60. package/dist/trace/redact.js +14 -2
  61. package/package.json +3 -3
package/dist/run-local.js CHANGED
@@ -582,6 +582,7 @@ export async function runLocal(argv, deps = {}) {
582
582
  */
583
583
  const sharedRunnerDeps = createSharedRunnerDeps({
584
584
  config,
585
+ logger, // #240②:onNotice 席(共享基座键,本地形同席——结构化通告落同一 logger)
585
586
  // 交接件⑤:commit 尾注署名座进**共享基座** ⇒ 主 runner 与 subRunner 同源。
586
587
  // [931]① clay 拍:run-local = Sema 品牌本地形态,commit 尾注接 Sema 署名(core 1.300 缺省已翻转不署)。
587
588
  // 修前它只写在下面主 runner 的差异键里 ⇒ **委派出去的子代提交不带署名**,而它提交进的是同一个仓、
@@ -419,7 +419,16 @@ export declare class ToolApprovalCoordinator {
419
419
  * `req.args` 是 `unknown` ⇒ 窄读,**禁裸 as-cast**(宪法 [2704]:一个形状漂了的 args 若被 cast,
420
420
  * 会把 `undefined` 当命令送进 `prepareCardApproval`,那是放宽面上的静默垃圾)。
421
421
  */
422
+ /** 两份候选是否**逐条逐键**相等(顺序即展示序 ⇒ 顺序敏感)。两侧都缺席 = 相等;一侧缺席 = 不等。
423
+ * 用在幂等重入的「行 = 真源」对账上(见 `ensureAsk` 那段撤回臂)。 */
424
+ private static ruleSuggestionsEqual;
422
425
  private buildRuleLaneMaterial;
426
+ /** #241([3731]/[3730] 双属主裁定):把某 wire run 的**流内未决 ask** 立即转 durable park——断连支专用。
427
+ * 匹配键=`ctxTaskId`(wire run id)。该字段的顶注说它「不是清扫判据」——那是因为清扫要判**连接**的
428
+ * 生死(同 id 可被两个 ctx 实例复用);本臂语义不同:调用方就是该 wire run 的承载流,断连=这个 id 的
429
+ * 流没了,值相等即目标集;极端复用形下多转的那条也只是从流内卡变 durable 卡,方向保守。
430
+ * 返回触发条数(0 = 无流内未决 ask,调用方照旧 abort)。幂等:已结算条目跳过,重复调用无害。 */
431
+ parkOpenAsksForTask(wireTaskId: string): number;
423
432
  /** 测试/可观测性钩子(X-2):某 (taskId) 维当前占用的准入名额数。 */
424
433
  admittedCount(taskId: string): number;
425
434
  /** #151 车2(D5 store 故障姿势):任何 store 调用 throw ⇒ fail-open 到进程内机械照旧(store 是记账/
@@ -48,7 +48,7 @@ import { uuidv7, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
48
48
  import { redactDeep, redactSecrets } from "./trace/redact.js";
49
49
  import { createLogger } from "./observability/logger.js";
50
50
  import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
51
- import { ApprovalCardEnvelopeSchema, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, } from "./approval-card.js";
51
+ import { ApprovalCardEnvelopeSchema, MAX_RULE_SUGGESTIONS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, } from "./approval-card.js";
52
52
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
53
53
  /** #151 车2:本模块自有的日志出口——同 config-provider.ts/runtime-caps-resolver.ts 先例(协调器不走
54
54
  * DI logger,构造签名是设计定稿钉死的三键 options bag,加第四个 logger 键属于重议已裁事项)。仅用于
@@ -450,6 +450,15 @@ export class ToolApprovalCoordinator {
450
450
  * `req.args` 是 `unknown` ⇒ 窄读,**禁裸 as-cast**(宪法 [2704]:一个形状漂了的 args 若被 cast,
451
451
  * 会把 `undefined` 当命令送进 `prepareCardApproval`,那是放宽面上的静默垃圾)。
452
452
  */
453
+ /** 两份候选是否**逐条逐键**相等(顺序即展示序 ⇒ 顺序敏感)。两侧都缺席 = 相等;一侧缺席 = 不等。
454
+ * 用在幂等重入的「行 = 真源」对账上(见 `ensureAsk` 那段撤回臂)。 */
455
+ static ruleSuggestionsEqual(a, b) {
456
+ if (a === undefined || b === undefined)
457
+ return a === b;
458
+ if (a.length !== b.length)
459
+ return false;
460
+ return a.every((x, i) => x.rule === b[i].rule && x.match === b[i].match && x.command === b[i].command);
461
+ }
453
462
  buildRuleLaneMaterial(req, owner, governanceForced) {
454
463
  if (this.ruleConsent === undefined)
455
464
  return undefined;
@@ -469,9 +478,21 @@ export class ToolApprovalCoordinator {
469
478
  // 「单机 + 无 principal」的部署第一次同时满足其余三项。
470
479
  if (owner === null)
471
480
  return undefined;
472
- const suggestions = req.ruleSuggestions;
473
- if (!Array.isArray(suggestions) || suggestions.length === 0)
481
+ const rawSuggestions = req.ruleSuggestions;
482
+ if (!Array.isArray(rawSuggestions) || rawSuggestions.length === 0)
474
483
  return undefined;
484
+ // 🔴 **基数帽在这里扣,而且只在这里扣**(重扫二轮红先修)。此前同步腿对这一格**零执法**:两族帧与
485
+ // `card_json` 都是逐字透传,真正按 {@link MAX_RULE_SUGGESTIONS} 截的只有耐久腿
486
+ // (`boundedRuleSuggestions`)。于是上游给 5 条时:live 帧发 5 条(契约文 §4a-bis 却把「server 执法帽 4」
487
+ // 写在**活卡帧**这条载体上,消费端按 ≤4 布局)、`card_json` 落 5 条,而一切读面都过
488
+ // `ApprovalCardSchema`(`ruleSuggestions.max(4)`)⇒ 判假 ⇒ 重放腿跳过 + `persisted-row-unusable` 臂返
489
+ // `"unavailable"` ⇒ **整只 ask 走 park**。那条「容忍余量」的设计意图在同步腿上完全没兑现,只是把悬崖
490
+ // 从 2 挪到了 4。
491
+ // 截在**素材铸点**而不是 `buildApprovalCard`:后者只截卡,会让 live 帧(5 条)与卡(4 条)分家,违背
492
+ // `approval-card.ts` 顶注自证的「wire 帧与 card_json 用同一份已洗素材,不是两处各算一遍的巧合」。
493
+ // 单一属主收在这里,于是**两族帧 + `card_json` + 回决口 `rule_not_offered` 的等式左边**拿的是同一份 ——
494
+ // 没渲出来的候选也就恒兑不动(反过来才是真扩权面)。保序取前 N:core 的 `exact` 形恒在 index 0。
495
+ const suggestions = rawSuggestions.length > MAX_RULE_SUGGESTIONS ? rawSuggestions.slice(0, MAX_RULE_SUGGESTIONS) : rawSuggestions;
475
496
  const args = req.args;
476
497
  if (args === null || typeof args !== "object" || Array.isArray(args))
477
498
  return undefined;
@@ -486,6 +507,21 @@ export class ToolApprovalCoordinator {
486
507
  ...(boundInputHash !== null ? { boundInputHash } : {}),
487
508
  };
488
509
  }
510
+ /** #241([3731]/[3730] 双属主裁定):把某 wire run 的**流内未决 ask** 立即转 durable park——断连支专用。
511
+ * 匹配键=`ctxTaskId`(wire run id)。该字段的顶注说它「不是清扫判据」——那是因为清扫要判**连接**的
512
+ * 生死(同 id 可被两个 ctx 实例复用);本臂语义不同:调用方就是该 wire run 的承载流,断连=这个 id 的
513
+ * 流没了,值相等即目标集;极端复用形下多转的那条也只是从流内卡变 durable 卡,方向保守。
514
+ * 返回触发条数(0 = 无流内未决 ask,调用方照旧 abort)。幂等:已结算条目跳过,重复调用无害。 */
515
+ parkOpenAsksForTask(wireTaskId) {
516
+ let n = 0;
517
+ for (const e of this.pending.values()) {
518
+ if (e.ctxTaskId === wireTaskId && e.settledOutcome === undefined) {
519
+ e.forceParkNow();
520
+ n++;
521
+ }
522
+ }
523
+ return n;
524
+ }
489
525
  /** 测试/可观测性钩子(X-2):某 (taskId) 维当前占用的准入名额数。 */
490
526
  admittedCount(taskId) {
491
527
  return this.admitByTask.get(taskId) ?? 0;
@@ -1053,6 +1089,28 @@ export class ToolApprovalCoordinator {
1053
1089
  cardForFrame = rest;
1054
1090
  }
1055
1091
  }
1092
+ // 🔴 **候选是「行 = 真源」F2 家族的第三位**(codex 对抗复审 R1-[medium],验真后修)。上面两条把
1093
+ // `governanceForced` / `persistedRuleShadowed` 收进了这条纪律,候选却漏在外面:幂等命中一条既有行时
1094
+ // 呈卡帧读的是**行上的卡**,而旧帧的 `ruleSuggestions` 与 `ruleLaneMaterial`(= 回决口
1095
+ // `rule_not_offered` 那道等式的**左边**)仍来自**这一腿**的 `req`。两腿的候选可以真的不同(重试 /
1096
+ // 并发 / 混版跑 / 规则店在两腿之间变了),于是同一个 `approvalId` 上会出现:呈卡帧展示 A、旧帧展示 B、
1097
+ // 兑付口按 B 判 —— 人点了卡上看见的 A,拿回一句 `rule_not_offered`。
1098
+ //
1099
+ // 处置 = **不一致就整条撤回车道**,与上面那条治理撤回**逐字同形**(本文件既定的安全方向:少渲一格
1100
+ // 是安全的,渲一格按不动的才是 wire 谎言)。不选「采信行的候选」:兑付还要 `command`/`boundInputHash`
1101
+ // 等素材,而行上的卡不带它们,拼起来就是第三份来源;也不选「采信本地」:人看见的是行上那张卡。
1102
+ // ⚠️ 残余如实记:这里撤的是**本次投递**的两族帧与素材,行本身不动 ⇒ 纯重放腿(`buildReplayFrame`)
1103
+ // 仍会从行上投出候选。那条腿本来就没有自己的兑付通道(射程见 `ApprovalCardSchema.ruleSuggestions`
1104
+ // 顶注:只有同副本 live `approvalId` 兑得动),所以不构成「按得动却兑不动」。
1105
+ if (ruleLaneMaterial !== undefined && !ToolApprovalCoordinator.ruleSuggestionsEqual(cardForFrame.ruleSuggestions, ruleLaneMaterial.suggestions)) {
1106
+ this.noteStoreError(new Error("persisted approval row offers a different ruleSuggestions set than this leg — withdrawing the rule lane for this delivery"), "ensureAsk(rule-suggestions-drift)");
1107
+ ruleLaneMaterial = undefined;
1108
+ delete frame.ruleSuggestions;
1109
+ if (cardForFrame.ruleSuggestions !== undefined) {
1110
+ const { ruleSuggestions: _drifted, ...rest } = cardForFrame;
1111
+ cardForFrame = rest;
1112
+ }
1113
+ }
1056
1114
  }
1057
1115
  catch (err) {
1058
1116
  this.noteStoreError(err, "ensureAsk");
@@ -1291,6 +1349,18 @@ export class ToolApprovalCoordinator {
1291
1349
  }
1292
1350
  })();
1293
1351
  };
1352
+ // #241:断连强制转 park(接口注释见 PendingApproval.forceParkNow)。有店 ⇒ 走窗到期同路(expireAsk
1353
+ // CAS+兄弟撤卡,赢=unavailable/输=向持久真源收敛——人已决就尊重人);无店 ⇒ 进程内直接改判 park 路由。
1354
+ const forceParkNow = () => {
1355
+ if (done)
1356
+ return;
1357
+ if (!this.askStore || askId === undefined || batchId === undefined) {
1358
+ windowRouteUnavailable = true;
1359
+ settle(false, "expired");
1360
+ return;
1361
+ }
1362
+ windowExpired();
1363
+ };
1294
1364
  // F2 修:定时器量的是**行上那个绝对 deadline** 的剩余时长(重入拿到既有行时,剩余时长比一整个窗短
1295
1365
  // —— 旧形会给一条早就该到期的行重新挂一个满窗的表)。store 缺席时逐字沿用 `effectiveWindowMs`(D1)。
1296
1366
  timer = setTimeout(windowExpired, this.askStore ? Math.max(0, persistedExpiresAtMs - Date.now()) : effectiveWindowMs);
@@ -1414,6 +1484,7 @@ export class ToolApprovalCoordinator {
1414
1484
  // 自己的 TTL/abortSignal 才作数;上面的 aliveCtxs abort 倒计时仍照常适用于它自己的连接级 abort)。
1415
1485
  const pendingEntry = {
1416
1486
  settle,
1487
+ forceParkNow,
1417
1488
  owner: primary.owner,
1418
1489
  ctxTaskId: primary.taskId,
1419
1490
  originCtx: primary,
@@ -886,7 +886,15 @@ function mapTraceEventInner(type, seq, data) {
886
886
  return { event: "prompt-assembled", data: { seq, ...data } };
887
887
  case "done":
888
888
  return { event: "done", data: { seq } };
889
+ // 🔴 park 的账本事件名有**两个**(扫描P2,status/kind 同名词混淆病族):`suspended` 与
890
+ // `needs_review` —— 写点是 `trace/ledger-sink.ts` 的 `appendParked(kind)`(kind 逐字取自
891
+ // `result.status`)、`runs.ts` 的 bg 腿、`http/server.ts` 的两条 resume 腿;`needs_review` 就是
892
+ // plan_review / dry_run 两种 gate 的 park 落名。只映射前者 ⇒ 一条 plan_review park 在 trace 流上
893
+ // 落进 `default → null`,**一帧都不发**,消费端只看到流无声中止(既不是 done 也不是 error)。
894
+ // 契约 2.3 的 park 标记只有 `suspended: true` 这一个位 ⇒ 两个词共用同一张帧(gate 细分在
895
+ // `GET /v1/runs/:id`,不在这条流上)。
889
896
  case "suspended":
897
+ case "needs_review":
890
898
  return { event: "done", data: { seq, suspended: true } };
891
899
  case "failed":
892
900
  // BL-13: redact at the read boundary too (defense-in-depth) — covers any errorMessage that reached the
@@ -1,5 +1,18 @@
1
1
  export declare function setRedactionObserver(fn: ((patternLabel: string, count: number) => void) | undefined): void;
2
- export declare function redactSecrets(text: string): string;
2
+ /**
3
+ * `opts.observe: false` = **照常脱敏,但这一遍不计数**(合并码重扫)。
4
+ *
5
+ * 为什么需要它:本函数对自己的输出**值幂等、计数不幂等** —— URL-userinfo 与 JSON 秘密字段两条 pattern
6
+ * 会命中自己已经替换过的输出并再计一次。于是任何**重扫已脱敏文本**的读路径(耐久待批队列的回读腿,
7
+ * `/v1/approvals/stream` 每 3s 一拍)都会持续给 `redactions_applied_total` 加数,把那个用来识别
8
+ * 「过度脱敏 / pattern 从不触发」的指标灌成不可读。
9
+ *
10
+ * 🔴 关的是**计数**,不是脱敏:重扫这一遍仍是纵深防御(手改过的行、未来某条写路径漏了脱敏,读面照样
11
+ * 兜住)—— 少脱一次的代价是把凭证投上跨租户可见的运维队列,那条永远不能省。
12
+ */
13
+ export declare function redactSecrets(text: string, opts?: {
14
+ observe?: boolean;
15
+ }): string;
3
16
  /**
4
17
  * 🔴 环 + 深度预算(2026-07-25 补)。原实现是纯递归、**既不记已访问节点也不限深度**,而它的调用位置是
5
18
  * **最不可信的边界**:`toolStart`/`toolEnd` 把工具的 `args`/`output`/`structured` 直接喂进来。后果两条:
@@ -33,11 +33,23 @@ export function setRedactionObserver(fn) {
33
33
  onRedaction = fn;
34
34
  }
35
35
  const PATTERN_LABELS = SECRET_PATTERNS.map(([, repl]) => repl.replace(/[«»]/g, "").replace(/^redacted:/, ""));
36
- export function redactSecrets(text) {
36
+ /**
37
+ * `opts.observe: false` = **照常脱敏,但这一遍不计数**(合并码重扫)。
38
+ *
39
+ * 为什么需要它:本函数对自己的输出**值幂等、计数不幂等** —— URL-userinfo 与 JSON 秘密字段两条 pattern
40
+ * 会命中自己已经替换过的输出并再计一次。于是任何**重扫已脱敏文本**的读路径(耐久待批队列的回读腿,
41
+ * `/v1/approvals/stream` 每 3s 一拍)都会持续给 `redactions_applied_total` 加数,把那个用来识别
42
+ * 「过度脱敏 / pattern 从不触发」的指标灌成不可读。
43
+ *
44
+ * 🔴 关的是**计数**,不是脱敏:重扫这一遍仍是纵深防御(手改过的行、未来某条写路径漏了脱敏,读面照样
45
+ * 兜住)—— 少脱一次的代价是把凭证投上跨租户可见的运维队列,那条永远不能省。
46
+ */
47
+ export function redactSecrets(text, opts) {
48
+ const observe = opts?.observe ?? true;
37
49
  let out = text;
38
50
  for (let i = 0; i < SECRET_PATTERNS.length; i++) {
39
51
  const [re, repl] = SECRET_PATTERNS[i];
40
- if (!onRedaction) {
52
+ if (!onRedaction || !observe) {
41
53
  out = out.replace(re, repl);
42
54
  continue;
43
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.15.0",
3
+ "version": "7.16.0-rc.2",
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",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "^5.27.0",
57
+ "@sema-agent/core": "^5.28.0",
58
58
  "@sema-agent/registry-core": "^0.16.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@sema-agent/sdk": "^6.16.0",
72
+ "@sema-agent/sdk": "^6.17.2",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",