@sema-agent/server 7.69.0 → 7.70.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.
Files changed (51) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +3 -0
  3. package/USAGE.md +103 -7
  4. package/dist/approval-ask-machine.d.ts +1 -1
  5. package/dist/approval-card.d.ts +3 -2
  6. package/dist/approval-reconciler.d.ts +0 -54
  7. package/dist/approval-reconciler.js +2 -2
  8. package/dist/boot/parked-revive-gate.js +10 -9
  9. package/dist/boot/runner-deps.d.ts +35 -4
  10. package/dist/boot/runner-deps.js +2 -6
  11. package/dist/budget.js +3 -2
  12. package/dist/config-catalog.d.ts +31 -2
  13. package/dist/config-catalog.js +41 -23
  14. package/dist/http/routes/approvals-assistant.js +3 -12
  15. package/dist/http/routes/diagnostics.js +4 -0
  16. package/dist/http/routes/runs.js +2 -3
  17. package/dist/http/routes/tasks.js +22 -18
  18. package/dist/http/server.js +4 -2
  19. package/dist/main.js +2 -2
  20. package/dist/observability/metrics.d.ts +0 -6
  21. package/dist/observability/metrics.js +3 -1
  22. package/dist/observability/permission-rule-events.d.ts +20 -0
  23. package/dist/observability/permission-rule-events.js +28 -0
  24. package/dist/observability/run-terminal-log.d.ts +9 -19
  25. package/dist/observability/run-terminal-log.js +183 -32
  26. package/dist/observability/tool-trace.d.ts +12 -0
  27. package/dist/observability/tool-trace.js +5 -1
  28. package/dist/plugins/store-backend.d.ts +2 -1
  29. package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
  30. package/dist/plugins/workflow-journal-store-sql.js +3 -10
  31. package/dist/posture-source.d.ts +23 -3
  32. package/dist/posture-source.js +1 -1
  33. package/dist/read-face-posture.d.ts +8 -1
  34. package/dist/read-face-posture.js +1 -0
  35. package/dist/run-cancel-context.d.ts +50 -1
  36. package/dist/run-cancel-context.js +22 -0
  37. package/dist/run-local.d.ts +14 -6
  38. package/dist/run-local.js +1 -1
  39. package/dist/runs.d.ts +0 -51
  40. package/dist/runs.js +40 -25
  41. package/dist/tool-approval.d.ts +14 -8
  42. package/dist/tool-approval.js +3 -3
  43. package/dist/trace/core-keyset-guard.d.ts +7 -3
  44. package/dist/trace/ledger-events.d.ts +17 -1
  45. package/dist/trace/ledger-events.js +6 -1
  46. package/dist/trace/ledger-sink.d.ts +10 -8
  47. package/dist/trace/ledger-sink.js +2 -1
  48. package/dist/trace/project.d.ts +3 -2
  49. package/dist/trace/redact.d.ts +28 -0
  50. package/dist/trace/redact.js +50 -27
  51. package/package.json +3 -3
package/dist/runs.js CHANGED
@@ -7,7 +7,7 @@ import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest,
7
7
  import { createLedgerSink } from "./trace/ledger-sink.js";
8
8
  import { registerEngineNoticeLeg } from "./trace/engine-notice-wire.js";
9
9
  import { recordTurnActivity } from "./turn-activity.js";
10
- import { buildCancelContext } from "./run-cancel-context.js";
10
+ import { CANCELLED_CODE, mintCancelledResult } from "./run-cancel-context.js";
11
11
  import { createApprovalCardEmitter, resolveApprovalLeg } from "./tool-approval.js";
12
12
  import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
13
13
  import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
@@ -66,30 +66,35 @@ export function isSessionConflictResult(result) {
66
66
  return result.terminal.code === "conflict";
67
67
  return /stale write|concurrent write|conflict/i.test(result.terminal.message ?? "");
68
68
  }
69
+ const GOVERNANCE_HTTP_STATUS = {
70
+ "memory.admission_denied": 403,
71
+ "memory.admission_required": 503,
72
+ "memory.capture_optout_denied": 403,
73
+ "config.tool_mount_denied": 400,
74
+ "config.locked_key": 400,
75
+ "usage.window_exhausted": null,
76
+ "config.compliance_required": null,
77
+ "config.compliance_denied": null,
78
+ };
69
79
  export function resumeAtHttpStatus(result) {
70
80
  const code = failureCodeOf(result.terminal);
71
81
  if (code === undefined)
72
82
  return undefined;
83
+ if (Object.hasOwn(GOVERNANCE_HTTP_STATUS, code)) {
84
+ return GOVERNANCE_HTTP_STATUS[code] ?? undefined;
85
+ }
73
86
  if (code === "resume.session_not_found")
74
87
  return 404;
75
88
  if (code === "rewind_snapshot.unresolvable")
76
89
  return 422;
77
90
  if (code === "rewind.child_scope_unsupported")
78
91
  return 422;
79
- if (code === "memory.admission_denied")
80
- return 403;
81
- if (code === "memory.admission_required")
82
- return 503;
83
- if (code === "memory.capture_optout_denied")
84
- return 403;
85
92
  if (code === "memory.capture_optout_unpersisted")
86
93
  return 503;
87
94
  if (code === "config.memory_capture_spelling")
88
95
  return 400;
89
96
  if (code === "config.invalid_checkpoint_store")
90
97
  return 400;
91
- if (code === "config.tool_mount_denied")
92
- return 400;
93
98
  if (code === "config.memory_capture_unsupported")
94
99
  return 409;
95
100
  if (!code.startsWith("resume_at."))
@@ -166,10 +171,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
166
171
  const preemptCtrl = new AbortController();
167
172
  if (preemptEligible)
168
173
  preemptable?.set(taskId, preemptCtrl);
169
- const cancelledResult = () => {
170
- const ctx = buildCancelContext(taskId, startedAt);
171
- return { taskId, sessionId: spec.sessionId, terminal: { kind: "failed", code: "cancelled", message: "cancelled by user" }, stats: { turns: 0, tokens: 0 }, ...(ctx ? { cancelContext: ctx } : {}) };
172
- };
174
+ const cancelledResult = (engineResult) => mintCancelledResult({ taskId, sessionId: spec.sessionId ?? "", startedAtMs: startedAt, message: "cancelled by user", ...(engineResult !== undefined ? { engineResult } : {}) });
173
175
  const recordDone = (result, status) => {
174
176
  metrics?.addGauge("runs_active", -1);
175
177
  metrics?.inc("tasks_total", { status });
@@ -216,6 +218,9 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
216
218
  heartbeat.unref?.();
217
219
  const anchor = new TurnAnchorCapture(captureTurnAnchor, () => metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
218
220
  let reached;
221
+ let observedDone;
222
+ let cancelledAtDone = false;
223
+ let settledAsCancel = false;
219
224
  let steerableStreamRef;
220
225
  let legLive = true;
221
226
  const notifiedKeys = new NotifiedKeys();
@@ -343,6 +348,10 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
343
348
  steerableStreamRef = stream;
344
349
  steerable?.set(taskId, stream);
345
350
  for await (const ev of stream) {
351
+ if (ev.type === "done") {
352
+ observedDone = ev.result;
353
+ cancelledAtDone = cancelCtrl.signal.aborted;
354
+ }
346
355
  fleetPublisher?.onEvent(ev);
347
356
  await flushPromptManifest();
348
357
  switch (ev.type) {
@@ -377,12 +386,14 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
377
386
  }
378
387
  evictIfConflict(runner, spec.sessionId, ev.result);
379
388
  {
380
- const safe = await withModelUsage(stripResumeToken(ev.result));
389
+ const settled = cancelledAtDone ? cancelledResult(ev.result) : ev.result;
390
+ settledAsCancel = failureCodeOf(settled.terminal) === CANCELLED_CODE;
391
+ const safe = await withModelUsage(stripResumeToken(settled));
381
392
  await sink.appendDone(safe);
382
393
  reached = { kind: "done", result: safe };
383
394
  const plane = terminalProjection(safe.terminal);
384
395
  await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
385
- recordDone(safe, plane.status);
396
+ recordDone(safe, failureCodeOf(safe.terminal) === CANCELLED_CODE ? CANCELLED_CODE : plane.status);
386
397
  }
387
398
  continue;
388
399
  }
@@ -429,15 +440,16 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
429
440
  recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=flush-clean-cancel ${errText(e)}`);
430
441
  }
431
442
  const c = cancelledResult();
443
+ settledAsCancel = failureCodeOf(c.terminal) === CANCELLED_CODE;
432
444
  try {
433
- await append("failed", { errorMessage: "cancelled by user", errorCode: "cancelled" });
445
+ await append("failed", { errorMessage: "cancelled by user", errorCode: CANCELLED_CODE });
434
446
  }
435
447
  catch (e) {
436
448
  recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-clean-cancel ${errText(e)}`);
437
449
  }
438
450
  reached = { kind: "done", result: c };
439
451
  await runStore.setTerminal(taskId, "failed", c, terminalProjection(c.terminal).errorMessage ?? null);
440
- recordDone(c, "cancelled");
452
+ recordDone(c, CANCELLED_CODE);
441
453
  }
442
454
  }
443
455
  catch (err) {
@@ -451,6 +463,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
451
463
  }
452
464
  if (reached?.kind === "done") {
453
465
  const plane = terminalProjection(reached.result.terminal);
466
+ settledAsCancel = failureCodeOf(reached.result.terminal) === CANCELLED_CODE;
454
467
  await runStore.setTerminal(taskId, plane.status, reached.result, plane.errorMessage ?? null);
455
468
  }
456
469
  else if (reached?.kind === "suspended") {
@@ -459,10 +472,11 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
459
472
  else if (reached?.kind === "needs_review") {
460
473
  await runStore.setNeedsReview(taskId);
461
474
  }
462
- else if (cancelCtrl.signal.aborted) {
475
+ else if (cancelCtrl.signal.aborted && observedDone === undefined) {
463
476
  const c = cancelledResult();
477
+ settledAsCancel = failureCodeOf(c.terminal) === CANCELLED_CODE;
464
478
  try {
465
- await append("failed", { errorMessage: "cancelled by user", errorCode: "cancelled" });
479
+ await append("failed", { errorMessage: "cancelled by user", errorCode: CANCELLED_CODE });
466
480
  }
467
481
  catch (e) {
468
482
  recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-thrown-cancel ${errText(e)}`);
@@ -470,6 +484,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
470
484
  await runStore.setTerminal(taskId, "failed", c, terminalProjection(c.terminal).errorMessage ?? null);
471
485
  }
472
486
  else {
487
+ settledAsCancel = false;
473
488
  const safeMsg = redactErrorMessage(message) ?? message;
474
489
  try {
475
490
  await append("failed", { errorMessage: safeMsg });
@@ -483,13 +498,13 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
483
498
  catch {
484
499
  }
485
500
  if (reached?.kind === "done")
486
- recordDone(reached.result, failureCodeOf(reached.result.terminal) === "cancelled" ? "cancelled" : terminalProjection(reached.result.terminal).status);
501
+ recordDone(reached.result, failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : terminalProjection(reached.result.terminal).status);
487
502
  else if (reached?.kind === "suspended") {
488
503
  metrics?.addGauge("runs_active", -1);
489
504
  metrics?.inc("tasks_total", { status: "suspended" });
490
505
  }
491
- else if (cancelCtrl.signal.aborted) {
492
- recordDone(null, "cancelled");
506
+ else if (settledAsCancel) {
507
+ recordDone(null, CANCELLED_CODE);
493
508
  }
494
509
  else {
495
510
  recordDone(null, "failed");
@@ -500,14 +515,14 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
500
515
  unregisterEngineNotice();
501
516
  if (fleetPublisher) {
502
517
  if (reached?.kind === "done")
503
- fleetPublisher.onTerminal(failureCodeOf(reached.result.terminal) === "cancelled" ? "cancelled" : terminalProjection(reached.result.terminal).status, fleetRunResiduals(reached.result));
518
+ fleetPublisher.onTerminal(failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : terminalProjection(reached.result.terminal).status, fleetRunResiduals(reached.result));
504
519
  else if (reached?.kind === "suspended")
505
520
  fleetPublisher.onTerminal("suspended");
506
521
  else if (reached?.kind === "needs_review")
507
522
  fleetPublisher.onTerminal("needs_review");
508
523
  else
509
- fleetPublisher.onTerminal(cancelCtrl.signal.aborted ? "cancelled" : "failed", {
510
- ...(cancelCtrl.signal.aborted ? { stoppedBy: "user" } : {}),
524
+ fleetPublisher.onTerminal(settledAsCancel ? CANCELLED_CODE : "failed", {
525
+ ...(settledAsCancel ? { stoppedBy: "user" } : {}),
511
526
  ...(spec.sessionId ? { transcriptId: spec.sessionId } : {}),
512
527
  });
513
528
  }
@@ -145,9 +145,10 @@ export interface ToolApprovalFrame {
145
145
  origin?: string;
146
146
  /**
147
147
  * **S-185 / [ref]**(core 7.10.0;**ADDITIVE**,`"tool_approval"` only)—— 这一问**为什么**到了人手上:
148
- * auto 档的分类器被咨询过、而它**跑不了**。`cause` = core 的闭三词
149
- * (`AUTO_MODE_UNAVAILABLE_CAUSES` = `error` / `timeout` / `breaker_open`),`AskRequest.classifierUnavailable`
150
- * 逐字透传。
148
+ * 分类器被咨询过、而它**跑不了**。`cause` = core 的闭集词
149
+ * (`AUTO_MODE_UNAVAILABLE_CAUSES`;core 7.12.0 起 = `error` / `timeout`),`AskRequest.classifierUnavailable`
150
+ * 逐字透传。core 7.12.0 / [ref] 起 ask 上这一位只由**策略层自声明**铸(引擎自己的那份改骑 deny —— 全文
151
+ * 在 `approval-card.ts` 同名键的顶注)。
151
152
  *
152
153
  * 🔴 **缺席 ≠「分类器好着呢」**:缺席同时覆盖三形 —— 分类器答上了 / 这只 ask 没资格走分类器
153
154
  * (`classifierMayAnswer` 的闭表)/ 本部署压根没接分类器。消费端**禁**把缺席读成对分类器健康的断言。
@@ -176,13 +177,18 @@ export interface ToolApprovalFrame {
176
177
  * 执行的那串字节(卡上显示的与真跑的不是同一个东西),**比不披露更坏**;所以 server 只报「有」,显形
177
178
  * 归壳的渲染面(转义/高亮/加标记,壳自己定)。
178
179
  *
179
- * **判据素材** = `boundArgs` 那**一次**序列化的结果(脱敏后的 args),且在**字节帽判定之前**算:
180
+ * **判据素材** = `boundArgs` 里对**原字节**(`redactDeep` 之前)的那一次序列化,且在**字节帽判定之前**算:
180
181
  * · 超帽时 args 以 `argsOmitted:true` 缺席上帧(卡上根本没有 args)—— 那正是人最看不见输入的一格,
181
182
  * 更需要知道「你看不到的那份里有隐形字符」,所以帽不影响本键;
182
- * · 序列化失败(循环引用 / 抛错的 `toJSON` / BigInt)⇒ 两件事一起交白卷:args 不上帧,本键**不铸**;
183
- * · 用脱敏后那份而不是原文,是因为它与卡面/`card_json` 是**同一份**素材(一次算定,三面同源),
184
- * 且人眼看到的正是它。理论残余如实记:一段**被遮蔽掉的密钥**里的 bidi 会随遮蔽一起消失 —— 已知、
185
- * 可忽略(密钥形不含 bidi),且方向安全(少报一次,不会误报干净为脏)。
183
+ * · 序列化失败(循环引用 / 抛错的 `toJSON` / BigInt)⇒ 两件事一起交白卷:args 不上帧,本键**不铸**
184
+ * (两次 `JSON.stringify` 共用同一个 try/catch —— 顶注反对的是第二个**静默降级点**,不是第二次序列化);
185
+ * · 🔴 **7.70.0(S-103)改素材**:此前用的是脱敏后那份(理由=与卡面同源),而本版 `redactSecrets` 入口
186
+ * 先剥全部 `\p{Cf}` —— bidi 控制符**整族都是** `\p{Cf}` ⇒ 在脱敏后的文本上扫,本键恒缺席、E-14 整条
187
+ * 失效(六格红先复现)。所以判据素材改回原字节;同源那条理由让位于「信号必须真」。
188
+ * · ⚠️ **上面「字节零改」那一条自 7.70.0 起对 `\p{Cf}` 族不再成立**:帧/卡上的 args 是**剥后**字节
189
+ * (与 core 显示面 `renderUntrustedCommandText` 同律,显示的是真正执行的逻辑顺序),壳因此**拿不到
190
+ * 「在哪一位」**、只能按整只 args 打标。这是有意的取舍(夹层密钥的根治要求单一剥点),不是遗漏;
191
+ * 要恢复定位面需要给脱敏器一条 span/偏移表回填路(S-103 定案明确不做),不是在这条腿上加特判。
186
192
  *
187
193
  * 判据**不看 `message`**(引擎/策略写的说明文本,不是待执行输入):本键被壳读作「**待执行的输入**可能
188
194
  * 在骗你的眼睛」,把两个来源折进一个布尔会让壳无法判断该给哪一段加显形标记。
@@ -2,7 +2,7 @@ import { contentKindOf } from "./approval-content-kind.js";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { uuidv7, MAX_RULE_TEXT_CHARS, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
4
4
  import { precheckCardRuleText } from "./rules-consent.js";
5
- import { redactDeep, redactSecrets } from "./trace/redact.js";
5
+ import { normalizeForRedaction, redactDeep, redactSecrets } from "./trace/redact.js";
6
6
  import { hasBidiControls } from "./text-bidi.js";
7
7
  import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerprint.js";
8
8
  import { createLogger } from "./observability/logger.js";
@@ -34,7 +34,7 @@ const PARKING_RETRY_AFTER_MS = 250;
34
34
  function durableRuleMaterial(material) {
35
35
  if (material === undefined)
36
36
  return { ruleCommand: null, ruleScopeRoot: null };
37
- if (redactSecrets(material.command, { observe: false }) !== material.command) {
37
+ if (redactSecrets(material.command, { observe: false }) !== normalizeForRedaction(material.command)) {
38
38
  defaultLogger.warn("approval rule material withheld: the adjudicated command carries credential-shaped bytes", {
39
39
  commandChars: material.command.length,
40
40
  });
@@ -141,8 +141,8 @@ function boundArgs(args) {
141
141
  const redacted = redactDeep(args);
142
142
  let hasBidi = false;
143
143
  try {
144
+ hasBidi = hasBidiControls(JSON.stringify(args));
144
145
  const text = JSON.stringify(redacted);
145
- hasBidi = hasBidiControls(text);
146
146
  if (Buffer.byteLength(text, "utf8") > MAX_APPROVAL_ARGS_BYTES)
147
147
  return { omitted: true, hasBidi };
148
148
  }
@@ -18,8 +18,12 @@
18
18
  * TaskResult 按「转发/摘除」记 Forwarded/Stripped —— 谁改 resolve-spec.ts / runs.ts 的发射点谁同步本文件。
19
19
  */
20
20
  import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, TaskEvent, TraceEvent, MailboxMessage, MailboxAppendMessage, TaskSpec, TaskResult, GateOutcome, Settlement, PausedCause } from "@sema-agent/core";
21
- /** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。 */
22
- type AssertAllKeysHandled<T extends never> = T;
21
+ /** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。
22
+ * 🔴 **导出**([ref] U1/U2):本仓「词表闭集,未处置成员=编译红」的钉法只许有**一个**形 ——
23
+ * `observability/run-terminal-log.ts` 的三张叶表 + 结果顶层键表用同一只断言,而不是各自再抄一份
24
+ * `type X<T extends never> = T`(抄一份就是第二个写者,两处哪天分岔谁都发现不了)。
25
+ * 本文件只有类型、不产 JS,所以被 `import type` 引用不新增任何运行期边。 */
26
+ export type AssertAllKeysHandled<T extends never> = T;
23
27
  type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode" | "apiFailure" | "agentMessage" | "_sema_provenance";
24
28
  type NotificationExcluded = "peer" | "crossSessionMessage" | "crossSessionNotice";
25
29
  type _GuardNotification = AssertAllKeysHandled<Exclude<keyof TaskNotificationPayload, NotificationProjected | NotificationExcluded>>;
@@ -38,7 +42,7 @@ type _GuardMailbox = AssertAllKeysHandled<Exclude<keyof MailboxMessage, MailboxP
38
42
  type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
39
43
  type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
40
44
  type PermissionTraceKind = Extract<TraceEvent["kind"], `permission.${string}`>;
41
- type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable" | "permission.read_only_allowed";
45
+ export type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable" | "permission.read_only_allowed";
42
46
  type PermissionTraceDropped = "permission.sandbox_admitted" | "permission.rule_sync_resurrected" | "permission.rule_sync_dropped" | "permission.org_snapshot_unavailable";
43
47
  type _GuardPermissionTrace = AssertAllKeysHandled<Exclude<PermissionTraceKind, PermissionTraceProjected | PermissionTraceDropped>>;
44
48
  type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTraceProjected | PermissionTraceDropped, PermissionTraceKind>>;
@@ -32,7 +32,8 @@
32
32
  * · **腿自己的带外行**:park/终局/失败/建议/用量/装配自证(不来自流事件)。
33
33
  */
34
34
  export type LedgerEventType = "reasoning" | "text" | "tool_start" | "tool_end" | "status" | "context_usage" | "compacted" | "diagnostics" | "steering_injected" | "compaction_outcome" | "workspace_changed" | "wiring_manifest" | "human_input" | "task_progress" | "task_notification" | "turn_end"
35
- /** park 行名**两个**(`appendParked(kind)` 逐字取自 `result.status`)。 */
35
+ /** park 行名**两个**(`appendParked(kind)` 逐字取自 `result.status`)—— 运行期闭集见
36
+ * {@link PARK_LEDGER_EVENT_TYPES}(本联合是它的类型面,两者由下方的双向断言锁步)。 */
36
37
  | "suspended" | "needs_review" | "done" | "failed" | "suggestions" | "model_usage" | "prompt_assembled" | "config_assembled" | "file_link"
37
38
  /** 人在环三族的 open/close 帧对(投递面 = 本腿的 durable events tail)。 */
38
39
  | "elicitation" | "elicitation_complete" | "question" | "question_complete" | "tool_approval" | "tool_approval_complete"
@@ -59,4 +60,19 @@ export type LedgerEventType = "reasoning" | "text" | "tool_start" | "tool_end" |
59
60
  * 由 `test/engine-notice-wire.test.ts` 的腿覆盖门看守,那才是它的对位门。
60
61
  */
61
62
  | "engine_notice";
63
+ /**
64
+ * 🔴 **park 行名的运行时闭集**(合并重扫 [4])。
65
+ *
66
+ * 此前这两个词在仓里有**三处**并列的字面量:`ledger-sink.ts` 的 `appendParked` 形参与 `onDone` 的分诊、
67
+ * 以及 `observability/run-terminal-log.ts` 的脱敏臂。`LedgerEventType` 只是类型联合、运行期取不到,
68
+ * 所以脱敏臂只能自己再抄一份 —— 而抄的那一份**加词不红**:仓里/引擎加第三个 park 词并登记进
69
+ * `LedgerEventType` 之后,`appendParked` 与 resume 腿的动态首参照写照落,回放时那一行的
70
+ * `gate.riskDescriptor.summary` 明文,脱敏臂一声不吭。
71
+ *
72
+ * ⇒ 词表落成**运行时常量**由消费点派生;下方两条断言把它与类型联合双向锁死(多一词 / 少一词都 tsc 红)。
73
+ */
74
+ export declare const PARK_LEDGER_EVENT_TYPES: readonly ["suspended", "needs_review"];
75
+ export type ParkLedgerEventType = (typeof PARK_LEDGER_EVENT_TYPES)[number];
76
+ /** park 行名的**收窄谓词**(消费点要的是「这个词是不是 park 词」+ 类型收窄,不是一次裸 includes)。 */
77
+ export declare function isParkLedgerEventType(v: string): v is ParkLedgerEventType;
62
78
  //# sourceMappingURL=ledger-events.d.ts.map
@@ -1,2 +1,7 @@
1
- export {};
1
+ export const PARK_LEDGER_EVENT_TYPES = ["suspended", "needs_review"];
2
+ const _parkWordsAreLedgerTypes = true;
3
+ void _parkWordsAreLedgerTypes;
4
+ export function isParkLedgerEventType(v) {
5
+ return PARK_LEDGER_EVENT_TYPES.includes(v);
6
+ }
2
7
  //# sourceMappingURL=ledger-events.js.map
@@ -1,7 +1,7 @@
1
1
  import { type TerminalCause } from "@sema-agent/core";
2
2
  import { brainStatusEventData } from "./project.js";
3
3
  import { NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
4
- import type { LedgerEventType } from "./ledger-events.js";
4
+ import { type LedgerEventType, type ParkLedgerEventType } from "./ledger-events.js";
5
5
  /**
6
6
  * LedgerSink — the SINGLE "stream event → durable runs-ledger" projection, shared by the background leg
7
7
  * (runs.ts `runInBackground`) and the sync `/v1/tasks/stream` detach lane (server.ts, `x-detach-on-disconnect`).
@@ -61,13 +61,15 @@ export interface LedgerSink {
61
61
  }): void;
62
62
  /** Park event (durable F4 suspend / [ref] D-B review pause): ONLY the `checkpointGate` is persisted —
63
63
  * never the capability `checkpointToken` (search [18] Q1b).
64
- * ⚠️ **"non-secret" was the old wording and it is not true** ([ref] R3, 2026-09-03 — wording only, behaviour
65
- * unchanged; [ref] decides the arm): the gate carries NO capability token, but its `riskDescriptor.summary` /
66
- * `shadowedRule` are model-/human-controlled display text that [ref] measured carrying a PEM private-key
67
- * body verbatim. This durable row replays on every `GET /v1/runs/:id/events` reconnect and is NOT backfilled
68
- * by core [ref]'s mint-point fix — it is the third face of the `checkpointGate` sister item recorded in
69
- * `http/routes/approvals-assistant.ts` (`redactPendingDisclosures` header). */
70
- appendParked(kind: "suspended" | "needs_review", result: unknown): Promise<void>;
64
+ * 🔴 **The gate is redacted, not "non-secret"** ([ref], 2026-09-10 — the arm [ref] R3 left open): it carries no
65
+ * capability token, but its `reason` and the `riskDescriptor` leaf family are model-/human-controlled display
66
+ * text that [ref] measured carrying a PEM private-key body verbatim, and this durable row replays on EVERY
67
+ * `GET /v1/runs/:id/events` reconnect (core [ref]'s mint-point fix does not backfill rows already written).
68
+ * The redaction owner is NOT this writer: it is `redactLedgerEventData` — the one confluence every ledger row
69
+ * crosses at the three store twins' `appendEvent` AND again on the read face (idempotent). Redacting here too
70
+ * would put two writers on one semantic surface; the other park writers (`http/server.ts`'s two resume legs)
71
+ * would still need it, and a per-writer arm is exactly the shape that drifts. */
72
+ appendParked(kind: ParkLedgerEventType, result: unknown): Promise<void>;
71
73
  /** Terminal `done` event. `result` must already be checkpoint-token-stripped (and, on the bg leg,
72
74
  * model-usage-attached) by the caller. */
73
75
  appendDone(result: unknown): Promise<void>;
@@ -2,6 +2,7 @@ import { terminalProjection } from "@sema-agent/core";
2
2
  import { redactSecrets } from "./redact.js";
3
3
  import { toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, humanInputEventData, turnEndEventData, contextUsageEventData } from "./project.js";
4
4
  import { NotifiedKeys, taskNotificationStreamKey } from "../orchestration/workflow-completion-inbox.js";
5
+ import { isParkLedgerEventType } from "./ledger-events.js";
5
6
  export function createSerialLedgerAppend(appendEvent, startSeq) {
6
7
  let seq = startSeq;
7
8
  let chain = Promise.resolve();
@@ -156,7 +157,7 @@ export function createLedgerSink(opts) {
156
157
  await flush();
157
158
  if (result.terminal.kind === "paused") {
158
159
  const parkKind = terminalProjection(result.terminal).status;
159
- if (parkKind === "suspended" || parkKind === "needs_review") {
160
+ if (isParkLedgerEventType(parkKind)) {
160
161
  await appendParked(parkKind, result);
161
162
  return;
162
163
  }
@@ -357,11 +357,12 @@ export declare function workspaceChangedEventData(ev: {
357
357
  * ([ref],7.57.0 已发)答的是「**若此刻提交**会不会武装」,本段答的是「**这条真跑起来的腿**武装了吗」;
358
358
  * 同一个主体(调用方自己的 run)、同一条轴,只差事前/事后。剥掉它,壳就只能拿事前预测冒充事后事实;
359
359
  * ② **两个值都是引擎派生的闭形**:`armed` 布尔,`reason` 是 core 的闭集词(`AUTO_MODE_ARM_REASONS`:
360
- * armed / no_intent / no_face / denied / resolver_fault / latch_open)—— 非用户内容,不脱敏、逐字;
360
+ * armed / no_intent / no_face / denied / resolver_fault;core 7.12.0 起 `latch_open` 随熔断/单向闩
361
+ * 整族退役)—— 非用户内容,不脱敏、逐字;
361
362
  * ③ **不是 governance 侧信道**:它描述的是**本腿**的武装结论,与 lockedConfig/compliance/memoryAdmission/
362
363
  * retention 四位无函数关系;而 `denied` 这一词所披露的「组织把这一位关了」在 [ref] 读面上**本就已经**
363
364
  * 对同一租户可见(`org_denied`),本段没有扩大任何受众。
364
- * 🔴 **逐字透传,不映射**:core 的六词与本仓 [ref] 读面的六词(`AUTO_MODE_UNARMED_REASONS`)**刻意不归一**
365
+ * 🔴 **逐字透传,不映射**:core 的五词与本仓 [ref] 读面的六词(`AUTO_MODE_UNARMED_REASONS`)**刻意不归一**
365
366
  * —— 两张表回答的是两个问题、粒度也不同(core 的 `denied` 一词对应本仓的 `org_denied` / `local_denied`
366
367
  * 两词:core 只看合成后的 caps deny 位,本仓分得出这个 false 是 center 下发的还是本地 env 写的)。
367
368
  * ⚠️ **`settings_denied` 不折进 `denied`,它折进 `no_intent`**(codex 车BO r1 [medium] 验真后纠):
@@ -1,3 +1,31 @@
1
+ /**
2
+ * Trace thinking-block redaction (service-side). Per core [R57]: the agent's reasoning/CoT is emitted on the
3
+ * live TaskEvent stream but DELIBERATELY kept out of the metadata Tracer because CoT may contain reasoned-about
4
+ * secrets (and is large / ToS-sensitive for some providers). Persisting it for the trace UI is the deployment's
5
+ * call — so when we DO persist it, redact unambiguous secret formats first. Conservative on purpose: only clear
6
+ * key/token shapes are redacted. ⚠️ It is NOT exhaustive: unrecognised secret formats (a free-text password,
7
+ * a JWT) pass through, and the broad `Bearer <20+ chars>` rule can over-match legitimate CoT (false positives
8
+ * tolerated — over-redacting trace text is safe). For sensitive deployments, disable thinking capture entirely
9
+ * via TRACE_THINKING=false. This is a seam — deployments can extend the patterns.
10
+ *
11
+ * 两组臂,顺序即优先序:{@link FULL_SECRET_PATTERNS}(完整形)在前,{@link FRAGMENT_SECRET_PATTERNS}
12
+ * ([ref] 前缀残片,上游先截后脱的产物 ⇒ 带 `:partial` 后缀的 token)在后。
13
+ */
14
+ /** 本模块替换 token 的**闭形**正则源(`«redacted»` / `«redacted:aws-key»` / `«redacted:gh-token:partial»` …):
15
+ * URL-userinfo 臂用它把「用户名位上已经是本模块 token」整个当一个用户名单元吃掉。模块加载时对
16
+ * {@link REDACTION_TOKENS} 逐个校验闭形(表加了新 token 形而这里没跟 ⇒ 加载即抛,fail-closed)。 */
17
+ import { stripFormatCharacters } from "@sema-agent/core";
18
+ /**
19
+ * `redactSecrets` 的**归一化前置**(S-103 ①):把全部 `\p{Cf}`(零宽空格 / 词连接符 / BOM / 软连字符 / ZWJ / ZWNJ …)
20
+ * 剥掉。实现单一属主是 core 的 `stripFormatCharacters`(`@sema-agent/core`,7.3.1 起导出,正是为「先扫描后渲染」这一序
21
+ * 开的口),本模块只**转出**它,不抄第二份 —— 两份实现漂了就等于两个「什么是格式字符」的定义。
22
+ *
23
+ * 🔴 **需要判断「这次脱敏到底有没有认出凭据形」的调用方,比较基准是它、不是原文**:
24
+ * `redactSecrets(x) !== x` 在本版里对**任何**含 Cf 的文本都为真(输出是剥后文本),拿它当谓词会把一条带 emoji ZWJ
25
+ * 的普通 `git commit` 判成「含凭据形」;正确的谓词是 `redactSecrets(x) !== normalizeForRedaction(x)`。
26
+ * 现役消费方:`src/tool-approval.ts` 的 `durableRuleMaterial`(规则车道素材闸)与语料的 FP=0 格。
27
+ */
28
+ export declare const normalizeForRedaction: typeof stripFormatCharacters;
1
29
  /** 测试可见:扫描器 indexOf 实际扫过的字节数(摊还线性 ⇒ ≤ 常数 × 输入长)。 */
2
30
  export declare function ppkScanBytesForTest(): number;
3
31
  export declare function resetPpkScanBytesForTest(): void;
@@ -1,22 +1,34 @@
1
+ import { stripFormatCharacters } from "@sema-agent/core";
1
2
  import { recordFailOpen } from "../observability/fail-open.js";
3
+ export const normalizeForRedaction = stripFormatCharacters;
2
4
  const REDACTION_TOKEN_RE_SRC = String.raw `«redacted(?::[a-z-]{1,16}){0,2}»`;
3
- const FULL_SECRET_PATTERNS = [
4
- [/-----BEGIN[^-](?:[^-](?:[^-]|-(?!-))*)?PRIVATE KEY(?: BLOCK)?-----[\s\S]{0,8192}?-----END[^-](?:[^-](?:[^-]|-(?!-))*)?PRIVATE KEY(?: BLOCK)?-----/g, "«redacted:private-key»"],
5
+ const ENTROPY_SCAN_MAX = 96;
6
+ const twoStrongAhead = (valueClass) => String.raw `(?=${valueClass}{0,${ENTROPY_SCAN_MAX}}[0-9A-Z]${valueClass}{0,${ENTROPY_SCAN_MAX}}[0-9A-Z])`;
7
+ const valueArm = (prefixSrc, valueClass, min) => new RegExp(`${prefixSrc}${twoStrongAhead(valueClass)}${valueClass}{${min},}`, "g");
8
+ const keywordArm = (keywordSrc, valueClass, min, tailSrc = "") => new RegExp(`(?:${keywordSrc}\\s+${valueClass}{${min},}${tailSrc}` +
9
+ `|(?<!${valueClass})${keywordSrc}${twoStrongAhead(valueClass)}${valueClass}{${min},}${tailSrc})`, "g");
10
+ const STRUCTURAL_SECRET_PATTERNS = [
11
+ [/-----BEGIN(?:[^-](?:[^-](?:[^-]|-(?!-))*)?)?PRIVATE KEY(?: BLOCK)?-----[\s\S]{0,8192}?-----END(?:[^-](?:[^-](?:[^-]|-(?!-))*)?)?PRIVATE KEY(?: BLOCK)?-----/g, "«redacted:private-key»"],
5
12
  [/---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----[\s\S]{0,8192}?---- END SSH2 ENCRYPTED PRIVATE KEY ----/g, "«redacted:private-key»"],
6
- [/\bsk-[A-Za-z0-9_-]{16,}\b/g, "«redacted:key»"],
7
- [/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/g, "«redacted:gh-token»"],
8
- [/\bgithub_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}\b/g, "«redacted:gh-token»"],
9
- [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "«redacted:slack-token»"],
10
- [/\bAKIA[0-9A-Z]{16}\b/g, "«redacted:aws-key»"],
11
- [/\bASIA[0-9A-Z]{16}\b/g, "«redacted:aws-key»"],
12
- [/\b(?:gl(?:pat|dt|rt|rtr|cbt|ptt|oas|ft|imt|agent|wt|soat|ffct)-[A-Za-z0-9_-]{20,}|(?:sk|rk)_(?:live|test|org)_[A-Za-z0-9]{24,}|pypi-[A-Za-z0-9_-]{85,})\b/g, "«redacted:key»"],
13
- [/\bxapp-\d-[A-Za-z0-9-]{20,}\b/g, "«redacted:slack-token»"],
14
- [/\b[Bb]earer\s+[A-Za-z0-9._~+/-]{20,}=*/g, "Bearer «redacted»"],
15
- [/\bBasic\s+[A-Za-z0-9+/]{16,}={0,2}/g, "Basic «redacted»"],
16
- [/:(?<=\b[A-Za-z][A-Za-z0-9+.-]*:)\/\//g, "$1:«redacted»@"],
17
- [/\b[Tt]oken\s+[A-Za-z0-9._~+/-]{20,}/g, "token «redacted»"],
13
+ [/:(?<=[A-Za-z][A-Za-z0-9+.-]*:)\/\//g, "$1:«redacted»@"],
18
14
  [/("(?:private_key|client_secret|password|api[_-]?key|secret|token)"\s*:\s*)"[^"\\]*(?:\\.[^"\\]*)*"/gi, '$1"«redacted»"'],
19
15
  ];
16
+ const KEYWORD_SECRET_PATTERNS = [
17
+ [keywordArm("[Bb]earer", "[A-Za-z0-9._~+/-]", 20, "=*"), "Bearer «redacted»"],
18
+ [keywordArm("Basic", "[A-Za-z0-9+/]", 16, "={0,2}"), "Basic «redacted»"],
19
+ [keywordArm("[Tt]oken", "[A-Za-z0-9._~+/-]", 20), "token «redacted»"],
20
+ ];
21
+ const SHAPE_SECRET_PATTERNS = [
22
+ [valueArm("sk-", "[A-Za-z0-9_-]", 16), "«redacted:key»"],
23
+ [/(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g, "«redacted:gh-token»"],
24
+ [/github_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59,}/g, "«redacted:gh-token»"],
25
+ [/xox[baprs]-[A-Za-z0-9-]{10,}/g, "«redacted:slack-token»"],
26
+ [/AKIA[0-9A-Z]{16,}/g, "«redacted:aws-key»"],
27
+ [/ASIA[0-9A-Z]{16,}/g, "«redacted:aws-key»"],
28
+ [/(?:gl(?:pat|dt|rt|rtr|cbt|ptt|oas|ft|imt|agent|wt|soat|ffct)-[A-Za-z0-9_-]{20,}|(?:sk|rk)_(?:live|test|org)_[A-Za-z0-9]{24,}|pypi-[A-Za-z0-9_-]{85,})/g, "«redacted:key»"],
29
+ [/xapp-\d-[A-Za-z0-9-]{20,}/g, "«redacted:slack-token»"],
30
+ ];
31
+ const FULL_SECRET_PATTERNS = [...STRUCTURAL_SECRET_PATTERNS, ...KEYWORD_SECRET_PATTERNS, ...SHAPE_SECRET_PATTERNS];
20
32
  const PEM_FRAGMENT_TOKEN = "«redacted:private-key:partial»";
21
33
  const PEM_HEADER_IDENT_MAX = 31;
22
34
  const PEM_BODY_RUN_MIN = 8;
@@ -216,7 +228,7 @@ function isPemFenceLabel(s, from, to) {
216
228
  let end = to;
217
229
  if (end - from > PEM_BLOCK_WORD.length && s.startsWith(PEM_BLOCK_WORD, end - PEM_BLOCK_WORD.length))
218
230
  end -= PEM_BLOCK_WORD.length;
219
- if (end - from <= PEM_KEY_WORD.length)
231
+ if (end - from < PEM_KEY_WORD.length)
220
232
  return false;
221
233
  if (!s.startsWith(PEM_KEY_WORD, end - PEM_KEY_WORD.length))
222
234
  return false;
@@ -399,14 +411,14 @@ function redactHeadlessPpkBodies(text) {
399
411
  return count === 0 ? { out: text, count: 0 } : { out: out + text.slice(last), count };
400
412
  }
401
413
  const FRAGMENT_SECRET_PATTERNS = [
402
- [/\bsk-(?=[A-Za-z0-9_-]{6,})(?=[A-Za-z0-9_-]{0,64}[0-9A-Z][A-Za-z0-9_-]{0,64}[0-9A-Z])[A-Za-z0-9_-]{6,}/g, "«redacted:key:partial»"],
403
- [/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{8,}/g, "«redacted:gh-token:partial»"],
404
- [/\bgithub_pat_[A-Za-z0-9_]{8,}/g, "«redacted:gh-token:partial»"],
405
- [/\bxox[baprs]-[A-Za-z0-9-]{4,}/g, "«redacted:slack-token:partial»"],
406
- [/\bAKIA[0-9A-Z]{6,}/g, "«redacted:aws-key:partial»"],
407
- [/\bASIA[0-9A-Z]{8,}/g, "«redacted:aws-key:partial»"],
408
- [/\b(?:gl(?:pat|dt|rt|rtr|cbt|ptt|oas|ft|imt|agent|wt|soat|ffct)-|(?:sk|rk)_(?:live|test|org)_|pypi-)(?=[A-Za-z0-9_-]{8,})(?=[A-Za-z0-9_-]{0,90}[0-9A-Z][A-Za-z0-9_-]{0,90}[0-9A-Z])[A-Za-z0-9_-]{8,}/g, "«redacted:key:partial»"],
409
- [/\bxapp-\d-[A-Za-z0-9-]{4,}/g, "«redacted:slack-token:partial»"],
414
+ [valueArm("sk-", "[A-Za-z0-9_-]", 6), "«redacted:key:partial»"],
415
+ [valueArm("(?:ghp|gho|ghu|ghs|ghr)_", "[A-Za-z0-9]", 8), "«redacted:gh-token:partial»"],
416
+ [/github_pat_[A-Za-z0-9_]{8,}/g, "«redacted:gh-token:partial»"],
417
+ [valueArm("xox[baprs]-", "[A-Za-z0-9-]", 4), "«redacted:slack-token:partial»"],
418
+ [valueArm("AKIA", "[0-9A-Z]", 6), "«redacted:aws-key:partial»"],
419
+ [valueArm("ASIA", "[0-9A-Z]", 8), "«redacted:aws-key:partial»"],
420
+ [valueArm("(?:gl(?:pat|dt|rt|rtr|cbt|ptt|oas|ft|imt|agent|wt|soat|ffct)-|(?:sk|rk)_(?:live|test|org)_|pypi-)", "[A-Za-z0-9_-]", 8), "«redacted:key:partial»"],
421
+ [valueArm("xapp-\\d-", "[A-Za-z0-9-]", 4), "«redacted:slack-token:partial»"],
410
422
  ];
411
423
  const REDACTION_TOKENS = new Set([...FULL_SECRET_PATTERNS.map(([, r]) => r), PEM_FRAGMENT_TOKEN, ...FRAGMENT_SECRET_PATTERNS.map(([, r]) => r)]
412
424
  .flatMap((r) => r.match(/«[^»]*»/g) ?? []));
@@ -490,21 +502,33 @@ function scanUserinfoRun(text, i, kind, json) {
490
502
  return i;
491
503
  }
492
504
  const REDACTION_TOKEN_Y = new RegExp(REDACTION_TOKEN_RE_SRC, "y");
505
+ const SHAPE_STICKY = SHAPE_SECRET_PATTERNS.map(([re]) => new RegExp(re.source, "y"));
506
+ function keyShapedUsernameEnd(text, p) {
507
+ for (const re of SHAPE_STICKY) {
508
+ re.lastIndex = p;
509
+ if (re.test(text))
510
+ return re.lastIndex;
511
+ }
512
+ return -1;
513
+ }
493
514
  function matchUserinfoAt(text, p, json) {
494
515
  REDACTION_TOKEN_Y.lastIndex = p;
495
516
  const tokEnd = REDACTION_TOKEN_Y.test(text) ? REDACTION_TOKEN_Y.lastIndex : -1;
496
517
  const afterTok = tokEnd >= 0 ? text.charCodeAt(tokEnd) : -1;
497
518
  if (afterTok === 64)
498
519
  return null;
499
- let userEnd;
520
+ let userEnd = -1;
500
521
  let kind;
501
522
  if (afterTok === 58) {
502
523
  userEnd = tokEnd;
503
524
  kind = "token-pass";
504
525
  }
526
+ else if ((() => { const e = keyShapedUsernameEnd(text, p); return e >= 0 && text.charCodeAt(e) === 58 ? ((userEnd = e), true) : false; })()) {
527
+ kind = "token-pass";
528
+ }
505
529
  else {
506
530
  userEnd = scanUserinfoRun(text, p, "plain-user", json);
507
- if (userEnd === p || text.charCodeAt(userEnd) !== 58)
531
+ if (text.charCodeAt(userEnd) !== 58)
508
532
  return null;
509
533
  kind = "plain-pass";
510
534
  }
@@ -670,8 +694,7 @@ function reportHits(hits) {
670
694
  export function redactSecrets(text, opts) {
671
695
  const wantCount = onRedaction !== undefined && (opts?.observe ?? true);
672
696
  const json = opts?.json === true;
673
- let r;
674
- r = applyRules(text, wantCount, json, opts?.observe ?? true);
697
+ const r = applyRules(normalizeForRedaction(text), wantCount, json, opts?.observe ?? true);
675
698
  reportHits(r.hits);
676
699
  return r.out;
677
700
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.69.0",
3
+ "version": "7.70.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",
@@ -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": "7.11.2",
57
+ "@sema-agent/core": "7.12.0",
58
58
  "@sema-agent/settings-schema": "1.10.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": "8.8.0",
72
+ "@sema-agent/sdk": "9.0.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",