@sema-agent/server 7.68.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 (100) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +3 -0
  3. package/USAGE.md +122 -9
  4. package/dist/adoption/plan.js +11 -0
  5. package/dist/approval-ask-machine.d.ts +1 -1
  6. package/dist/approval-card.d.ts +27 -0
  7. package/dist/approval-card.js +11 -0
  8. package/dist/approval-reconciler.d.ts +0 -54
  9. package/dist/approval-reconciler.js +2 -2
  10. package/dist/approval.js +5 -4
  11. package/dist/boot/config-center.d.ts +6 -7
  12. package/dist/boot/config-center.js +31 -8
  13. package/dist/boot/leader.js +1 -1
  14. package/dist/boot/parked-revive-gate.js +10 -9
  15. package/dist/boot/runner-deps.d.ts +35 -4
  16. package/dist/boot/runner-deps.js +2 -6
  17. package/dist/boot/workflow-orchestration.d.ts +2 -0
  18. package/dist/boot/workflow-orchestration.js +10 -2
  19. package/dist/budget.js +3 -2
  20. package/dist/config-catalog.d.ts +31 -2
  21. package/dist/config-catalog.js +61 -24
  22. package/dist/config-center/apply-effective.d.ts +8 -1
  23. package/dist/config-center/apply-effective.js +23 -9
  24. package/dist/config-center/facade.d.ts +1 -1
  25. package/dist/config-center/facade.js +1 -1
  26. package/dist/config-center/read-face.d.ts +30 -1
  27. package/dist/config-center/read-face.js +33 -15
  28. package/dist/config-center/restart-signal.js +2 -1
  29. package/dist/config-center/types.d.ts +20 -5
  30. package/dist/config-provider.js +1 -0
  31. package/dist/config-types.d.ts +17 -2
  32. package/dist/config.d.ts +0 -21
  33. package/dist/config.js +19 -6
  34. package/dist/http/route-ctx.d.ts +39 -1
  35. package/dist/http/routes/approvals-assistant.js +4 -12
  36. package/dist/http/routes/capabilities.js +1 -0
  37. package/dist/http/routes/diagnostics.js +4 -0
  38. package/dist/http/routes/runs.js +2 -3
  39. package/dist/http/routes/tasks.js +22 -18
  40. package/dist/http/routes/workflows.js +15 -7
  41. package/dist/http/server.d.ts +10 -2
  42. package/dist/http/server.js +74 -26
  43. package/dist/leader/wire.d.ts +9 -2
  44. package/dist/leader/wire.js +6 -6
  45. package/dist/main.js +8 -7
  46. package/dist/model-compat.d.ts +81 -0
  47. package/dist/model-compat.js +52 -0
  48. package/dist/observability/fail-open.d.ts +4 -0
  49. package/dist/observability/fail-open.js +4 -0
  50. package/dist/observability/metrics.d.ts +0 -6
  51. package/dist/observability/metrics.js +3 -1
  52. package/dist/observability/permission-rule-events.d.ts +20 -0
  53. package/dist/observability/permission-rule-events.js +28 -0
  54. package/dist/observability/run-terminal-log.d.ts +9 -19
  55. package/dist/observability/run-terminal-log.js +183 -32
  56. package/dist/observability/tool-trace.d.ts +12 -0
  57. package/dist/observability/tool-trace.js +5 -1
  58. package/dist/operator-ask.d.ts +46 -0
  59. package/dist/operator-ask.js +4 -0
  60. package/dist/orchestration/workflow-agent-session-index.d.ts +96 -0
  61. package/dist/orchestration/workflow-agent-session-index.js +146 -0
  62. package/dist/orchestration/workflow-journal-entry.d.ts +78 -0
  63. package/dist/orchestration/workflow-journal-entry.js +20 -0
  64. package/dist/orchestration/workflow-notify-journal.d.ts +43 -1
  65. package/dist/orchestration/workflow-notify-journal.js +43 -3
  66. package/dist/parked-decide.d.ts +115 -5
  67. package/dist/parked-decide.js +94 -26
  68. package/dist/plugins/pg-pool.js +8 -0
  69. package/dist/plugins/store-backend.d.ts +7 -1
  70. package/dist/plugins/store-backend.js +3 -1
  71. package/dist/plugins/tidb-pool.js +8 -0
  72. package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
  73. package/dist/plugins/workflow-journal-store-sql.js +5 -5
  74. package/dist/plugins/workflow-run-store-sql.d.ts +30 -0
  75. package/dist/plugins/workflow-run-store-sql.js +40 -0
  76. package/dist/posture-source.d.ts +23 -3
  77. package/dist/posture-source.js +1 -1
  78. package/dist/read-face-posture.d.ts +8 -1
  79. package/dist/read-face-posture.js +1 -0
  80. package/dist/router/route-orchestration.js +2 -1
  81. package/dist/run-cancel-context.d.ts +50 -1
  82. package/dist/run-cancel-context.js +22 -0
  83. package/dist/run-local.d.ts +14 -6
  84. package/dist/run-local.js +2 -2
  85. package/dist/runs.d.ts +0 -51
  86. package/dist/runs.js +40 -23
  87. package/dist/runtime-governance.js +5 -4
  88. package/dist/tool-approval.d.ts +31 -5
  89. package/dist/tool-approval.js +5 -4
  90. package/dist/trace/core-keyset-guard.d.ts +9 -5
  91. package/dist/trace/engine-notice-wire.d.ts +1 -1
  92. package/dist/trace/engine-notice-wire.js +1 -0
  93. package/dist/trace/ledger-events.d.ts +17 -1
  94. package/dist/trace/ledger-events.js +6 -1
  95. package/dist/trace/ledger-sink.d.ts +10 -8
  96. package/dist/trace/ledger-sink.js +2 -1
  97. package/dist/trace/project.d.ts +3 -2
  98. package/dist/trace/redact.d.ts +28 -0
  99. package/dist/trace/redact.js +50 -27
  100. package/package.json +4 -4
@@ -0,0 +1,20 @@
1
+ /** 计量面的 `event` 词表(顺序 = HELP 行里的出现顺序)。 */
2
+ export declare const PERMISSION_RULE_EVENT_TABLE: readonly [{
3
+ readonly event: "persisted_rule_allowed";
4
+ readonly meaning: "a persisted allow rule authorized the call and nobody was asked";
5
+ }, {
6
+ readonly event: "rule_store_unreadable";
7
+ readonly meaning: "the rule store failed to read, so the call was adjudicated with ZERO rules (expect an approval storm whose cause is the store, not the model)";
8
+ }, {
9
+ readonly event: "read_only_allowed";
10
+ readonly meaning: "the engine's read-only shell verdict cleared an ask that was NOT an operator-written rule (core decisionReason \"read_only\"; an operator-declared ask carries matchedAskRule and is never cleared this way)";
11
+ }];
12
+ /** 词表里的一个词(闭集)。 */
13
+ export type PermissionRuleEventWord = (typeof PERMISSION_RULE_EVENT_TABLE)[number]["event"];
14
+ /** 这一帧是否进 `permission_rule_events_total`(判别式与词表同源,不再手抄第二串 `===`)。 */
15
+ export declare function isPermissionRuleEventKind(kind: string): boolean;
16
+ /** 帧名 → `event` 标签值。调用方须先过 {@link isPermissionRuleEventKind}。 */
17
+ export declare function permissionRuleEventWordOf(kind: string): string;
18
+ /** `/metrics` 的 HELP 行文案(从词表派生 —— 加词自动出现,不存在「第二份会漂的散文」)。 */
19
+ export declare function buildPermissionRuleEventsHelp(): string;
20
+ //# sourceMappingURL=permission-rule-events.d.ts.map
@@ -0,0 +1,28 @@
1
+ const PERMISSION_TRACE_PREFIX = "permission.";
2
+ export const PERMISSION_RULE_EVENT_TABLE = [
3
+ {
4
+ event: "persisted_rule_allowed",
5
+ meaning: "a persisted allow rule authorized the call and nobody was asked",
6
+ },
7
+ {
8
+ event: "rule_store_unreadable",
9
+ meaning: "the rule store failed to read, so the call was adjudicated with ZERO rules (expect an approval storm whose cause is the store, not the model)",
10
+ },
11
+ {
12
+ event: "read_only_allowed",
13
+ meaning: "the engine's read-only shell verdict cleared an ask that was NOT an operator-written rule (core decisionReason \"read_only\"; an operator-declared ask carries matchedAskRule and is never cleared this way)",
14
+ },
15
+ ];
16
+ const PERMISSION_RULE_EVENT_KINDS = new Set(PERMISSION_RULE_EVENT_TABLE.map((r) => `${PERMISSION_TRACE_PREFIX}${r.event}`));
17
+ export function isPermissionRuleEventKind(kind) {
18
+ return PERMISSION_RULE_EVENT_KINDS.has(kind);
19
+ }
20
+ export function permissionRuleEventWordOf(kind) {
21
+ return kind.slice(PERMISSION_TRACE_PREFIX.length);
22
+ }
23
+ export function buildPermissionRuleEventsHelp() {
24
+ return ("Permission allow-layer attribution events by event — the ONE answer to \"why was I not asked this time?\" (" +
25
+ PERMISSION_RULE_EVENT_TABLE.map((r) => `${r.event} = ${r.meaning}`).join("; ") +
26
+ ")");
27
+ }
28
+ //# sourceMappingURL=permission-rule-events.js.map
@@ -52,26 +52,15 @@ export declare const REDACT_INPUT_CAP = 65536;
52
52
  * 脱敏**之后**按字节封顶(占位已替换完,切在占位中间不是泄漏)并带标记。 */
53
53
  export declare const REDACT_OUTPUT_MAX_BYTES = 65000;
54
54
  export declare function redactErrorMessage(raw: string | undefined): string | undefined;
55
- /**
56
- * 同口:终态 `result` 整块也持久化(`GET /v1/runs/:id` 直出),写入前把里面的**自由文本座**脱敏,其余键
57
- * 逐字不动;`undefined`/`null` 透传。
58
- *
59
- * 🔴 S-136(core 7.6.0 [ref] D-8):自由文本座**搬家了** —— 失败文案与阻塞理由从顶层平面键
60
- * (`errorMessage` / `blockedReason`)搬进因由 `terminal` 的两条臂(`failed.message` / `blocked.reason`)。
61
- * 本函数**两种形都认**,而这不是「兼容读臂」(codex 对抗复审 r1 [high],亲读验真后**改判**;首版删掉了旧键
62
- * 臂,那是错的):本函数是**读盘面**,而 `task_run.result` / `task_event(done).result` 里躺着 7.63.0 及以前
63
- * 写下的**真字节**,它们本来就是平面形。写口只剩因由一条(没有任何东西会再写出旧形),所以这里不存在
64
- * 「两个写者」;而不认旧形的代价是**可复现的凭据回放**:上线前落盘的原文经 `GET /v1/runs/:id/events` 重新
65
- * 明文外发(读面这一层纵深正是为那批行存在的)。⇒ 规则仍只有一条:**读盘认盘上真有的形,写盘只写一种形**。
66
- *
67
- * 读面也走本函数,所以 `terminal` 形上取 `unknown`:旧持久 blob 与非 core 的 Runner 实现给的形不可信,
68
- * 运行期逐臂判形、不抛(与 `remoteEnvFailures` / `editedFiles` 两座同款不信任)。
69
- */
55
+ /** {@link redactTerminalResult} 会碰的全部顶层键 = 现行形的 `"redacted"` 行 ∪ 旧形平面键;
56
+ * `memoRedactTerminalResult` 的记忆键由它派生(两处同源,见 `RESULT_KEY_TREATMENT` 顶注)。 */
57
+ export declare const REDACTABLE_RESULT_LEAVES: readonly string[];
70
58
  export declare function redactTerminalResult<T extends {
71
59
  terminal?: unknown;
72
- /** 旧形(≤7.63.0)的两个顶层自由文本座;形上取 `unknown` —— 读盘面对盘上字节不作假设。 */
60
+ /** 旧形(≤7.63.0)的三个顶层座;形上取 `unknown` —— 读盘面对盘上字节不作假设。 */
73
61
  errorMessage?: unknown;
74
62
  blockedReason?: unknown;
63
+ checkpointGate?: unknown;
75
64
  remoteEnvFailures?: readonly {
76
65
  message?: string | undefined;
77
66
  }[] | undefined;
@@ -94,9 +83,10 @@ export declare function redactWorkerReportError<T extends {
94
83
  error?: string | undefined;
95
84
  checkpointGate?: unknown;
96
85
  }>(report: T): T;
97
- /** codex r5 [high]:`WorkerReport.checkpointGate`(core `CheckpointGate`)整块随 LeaderResult 落库/直出——`reason` 与
98
- * `riskDescriptor.summary`/`shadowedRule` 是自由文本([ref] 实测 summary 带 PEM 明文;[ref] R3 登记),按类型表脱这三叶;
99
- * `kind`/`toolName`/`axes`/`severity`/`touchedPaths` 闭集或路径,逐字。非对象形原样返回。 */
86
+ export declare function redactRiskDescriptor(rd: unknown): unknown;
87
+ /** `CheckpointGate`(core 类型)整块上 wire —— `reason` 与 `riskDescriptor` 的叶族是自由文本
88
+ * ([ref] 实测 summary 带 PEM 明文;[ref] R3 登记;[ref] 把它扩到结果/账本两面)。逐键**按表**处置,
89
+ * 叶表属主见 {@link redactRiskDescriptor}。非对象形原样返回(同一引用);字节没变也回同一引用。 */
100
90
  export declare function redactCheckpointGate(gate: unknown): unknown;
101
91
  /** 工件字节(patch / *.rej 拒绝块):codex r6 [high]——拒绝块能带 `git clone https://u:TOKEN@host` 或 `_authToken=` 行,
102
92
  * 「工件保真」不能让原凭据上元数据读面。做**原位密钥形替换**(与预览同一张规则表,字节只在密钥处变)、**不截断**;
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { redactSecrets } from "../trace/redact.js";
3
+ import { isParkLedgerEventType } from "../trace/ledger-events.js";
3
4
  import { errorText } from "./err-text.js";
4
5
  import { recordFailOpen } from "./fail-open.js";
5
6
  const MESSAGE_CAP = 1024;
@@ -75,6 +76,37 @@ export function redactErrorMessage(raw) {
75
76
  }
76
77
  }
77
78
  const MAX_EDITED_FILES = 1000;
79
+ const RESULT_KEY_TREATMENT = {
80
+ terminal: "redacted",
81
+ remoteEnvFailures: "redacted",
82
+ editedFiles: "redacted",
83
+ taskId: "verbatim",
84
+ runId: "verbatim",
85
+ sessionId: "verbatim",
86
+ model: "verbatim",
87
+ effectiveReasoning: "verbatim",
88
+ result: "verbatim",
89
+ salvagedOutput: "verbatim",
90
+ apiFailure: "verbatim",
91
+ retryAfterMs: "verbatim",
92
+ degraded: "verbatim",
93
+ structuredOutput: "verbatim",
94
+ rewindNotes: "verbatim",
95
+ haltedOnUserRejection: "verbatim",
96
+ haltedByUser: "verbatim",
97
+ strandedHumanAnswers: "verbatim",
98
+ pendingApproval: "verbatim",
99
+ approvalWaitedMs: "verbatim",
100
+ effectiveReadFace: "verbatim",
101
+ effectiveReadDenyPatterns: "verbatim",
102
+ effectiveMemoryScopes: "verbatim",
103
+ stats: "verbatim",
104
+ };
105
+ const LEGACY_RESULT_LEAVES = ["errorMessage", "blockedReason", "checkpointGate"];
106
+ export const REDACTABLE_RESULT_LEAVES = [
107
+ ...Object.entries(RESULT_KEY_TREATMENT).filter(([, t]) => t === "redacted").map(([k]) => k),
108
+ ...LEGACY_RESULT_LEAVES,
109
+ ];
78
110
  export function redactTerminalResult(result) {
79
111
  if (result === undefined || result === null)
80
112
  return result;
@@ -89,14 +121,41 @@ export function redactTerminalResult(result) {
89
121
  out.blockedReason = redactErrorMessage(r.blockedReason);
90
122
  changed = true;
91
123
  }
124
+ if (r.checkpointGate !== undefined) {
125
+ const legacyGate = redactCheckpointGate(r.checkpointGate);
126
+ if (legacyGate !== r.checkpointGate) {
127
+ out.checkpointGate = legacyGate;
128
+ changed = true;
129
+ }
130
+ }
92
131
  if (r.terminal !== null && typeof r.terminal === "object") {
93
132
  const t = r.terminal;
94
- if (t.kind === "failed" && typeof t.message === "string") {
95
- out.terminal = { ...t, message: redactErrorMessage(t.message) };
96
- changed = true;
133
+ let nt;
134
+ if (t.kind === "failed" && typeof t.message === "string")
135
+ nt = { ...t, message: redactErrorMessage(t.message) };
136
+ else if (t.kind === "blocked" && typeof t.reason === "string")
137
+ nt = { ...t, reason: redactErrorMessage(t.reason) };
138
+ const gateOf = (cause) => {
139
+ if (cause === null || typeof cause !== "object" || !("gate" in cause))
140
+ return undefined;
141
+ const g = cause.gate;
142
+ const ng = redactCheckpointGate(g);
143
+ return { gate: ng, changed: ng !== g };
144
+ };
145
+ const base = nt ?? t;
146
+ if (t.kind === "paused") {
147
+ const g = gateOf(base);
148
+ if (g?.changed === true)
149
+ nt = { ...base, gate: g.gate };
150
+ }
151
+ else if (t.kind === "failed") {
152
+ const np = base.nestedPause;
153
+ const g = gateOf(np);
154
+ if (g?.changed === true)
155
+ nt = { ...base, nestedPause: { ...np, gate: g.gate } };
97
156
  }
98
- else if (t.kind === "blocked" && typeof t.reason === "string") {
99
- out.terminal = { ...t, reason: redactErrorMessage(t.reason) };
157
+ if (nt !== undefined) {
158
+ out.terminal = nt;
100
159
  changed = true;
101
160
  }
102
161
  }
@@ -136,6 +195,11 @@ export function redactLedgerEventData(type, data) {
136
195
  return data;
137
196
  return { ...data, errorMessage: redactErrorMessage(msg) };
138
197
  }
198
+ if (isParkLedgerEventType(type)) {
199
+ const gate = data.gate;
200
+ const next = redactCheckpointGate(gate);
201
+ return next === gate ? data : { ...data, gate: next };
202
+ }
139
203
  return data;
140
204
  }
141
205
  export function redactWorkerReportError(report) {
@@ -145,38 +209,123 @@ export function redactWorkerReportError(report) {
145
209
  return report;
146
210
  return { ...report, ...(error !== report.error ? { error } : {}), ...(gate !== report.checkpointGate ? { checkpointGate: gate } : {}) };
147
211
  }
212
+ const RISK_DESCRIPTOR_TREATMENT = {
213
+ severity: "verbatim",
214
+ axes: "verbatim",
215
+ toolName: "verbatim",
216
+ shellGateDoctrine: "verbatim",
217
+ summary: "text",
218
+ touchedPaths: "text[]",
219
+ shadowedRule: "text",
220
+ probeReason: "text",
221
+ probeCause: "probe-cause",
222
+ };
223
+ const PROBE_CAUSE_TREATMENT = {
224
+ code: "verbatim",
225
+ roots: "operands",
226
+ further: "operands",
227
+ };
228
+ const PROBE_CAUSE_OPERANDS_TREATMENT = {
229
+ shown: "text[]",
230
+ total: "verbatim",
231
+ };
232
+ export function redactRiskDescriptor(rd) {
233
+ if (rd === null || typeof rd !== "object" || Array.isArray(rd))
234
+ return rd;
235
+ const r = rd;
236
+ let changed = false;
237
+ const str = (v) => { const o = redactErrorMessage(v) ?? ""; if (o !== v)
238
+ changed = true; return o; };
239
+ const strs = (v) => (Array.isArray(v) ? v.map((x) => (typeof x === "string" ? str(x) : x)) : v);
240
+ const out = { ...r };
241
+ for (const [key, treatment] of Object.entries(RISK_DESCRIPTOR_TREATMENT)) {
242
+ switch (treatment) {
243
+ case "verbatim":
244
+ break;
245
+ case "text":
246
+ if (typeof r[key] === "string")
247
+ out[key] = str(r[key]);
248
+ break;
249
+ case "text[]":
250
+ if (Array.isArray(r[key]))
251
+ out[key] = strs(r[key]);
252
+ break;
253
+ case "probe-cause": {
254
+ const pc = r[key];
255
+ if (pc === null || typeof pc !== "object" || Array.isArray(pc))
256
+ break;
257
+ const p = pc;
258
+ const np = { ...p };
259
+ for (const [ck, ct] of Object.entries(PROBE_CAUSE_TREATMENT)) {
260
+ if (ct === "verbatim")
261
+ continue;
262
+ const ops = p[ck];
263
+ if (ops === null || typeof ops !== "object" || Array.isArray(ops))
264
+ continue;
265
+ const o = ops;
266
+ const no = { ...o };
267
+ for (const [ok, ot] of Object.entries(PROBE_CAUSE_OPERANDS_TREATMENT)) {
268
+ if (ot === "text[]" && Array.isArray(o[ok]))
269
+ no[ok] = strs(o[ok]);
270
+ }
271
+ np[ck] = no;
272
+ }
273
+ out[key] = np;
274
+ break;
275
+ }
276
+ default: {
277
+ const unhandled = treatment;
278
+ throw new Error(`unhandled leaf treatment: ${String(unhandled)}`);
279
+ }
280
+ }
281
+ }
282
+ return changed ? out : rd;
283
+ }
284
+ const CHECKPOINT_GATE_TREATMENT = {
285
+ kind: "verbatim",
286
+ reason: "text",
287
+ toolName: "verbatim",
288
+ riskDescriptor: "risk-descriptor",
289
+ safetyAxis: "verbatim",
290
+ realApproval: "verbatim",
291
+ resumeAfterMs: "verbatim",
292
+ };
148
293
  export function redactCheckpointGate(gate) {
149
294
  if (gate === null || typeof gate !== "object" || Array.isArray(gate))
150
295
  return gate;
151
296
  const g = gate;
152
- let changed = false;
153
297
  const out = { ...g };
154
- const str = (v) => { const o = redactErrorMessage(v) ?? ""; if (o !== v)
155
- changed = true; return o; };
156
- const strs = (v) => (Array.isArray(v) ? v.map((x) => (typeof x === "string" ? str(x) : x)) : v);
157
- if (typeof g.reason === "string")
158
- out.reason = str(g.reason);
159
- const rd = g.riskDescriptor;
160
- if (rd && typeof rd === "object" && !Array.isArray(rd)) {
161
- const r = rd;
162
- const nr = { ...r };
163
- for (const k of ["summary", "shadowedRule", "probeReason"])
164
- if (typeof r[k] === "string")
165
- nr[k] = str(r[k]);
166
- if (Array.isArray(r.touchedPaths))
167
- nr.touchedPaths = strs(r.touchedPaths);
168
- const pc = r.probeCause;
169
- if (pc && typeof pc === "object" && !Array.isArray(pc)) {
170
- const p = pc;
171
- const np = { ...p };
172
- for (const k of ["roots", "further"]) {
173
- const ops = p[k];
174
- if (ops && typeof ops === "object" && !Array.isArray(ops) && Array.isArray(ops.shown))
175
- np[k] = { ...ops, shown: strs(ops.shown) };
298
+ let changed = false;
299
+ for (const [key, treatment] of Object.entries(CHECKPOINT_GATE_TREATMENT)) {
300
+ switch (treatment) {
301
+ case "verbatim":
302
+ break;
303
+ case "text": {
304
+ const v = g[key];
305
+ if (typeof v !== "string")
306
+ break;
307
+ const next = redactErrorMessage(v) ?? "";
308
+ if (next !== v) {
309
+ out[key] = next;
310
+ changed = true;
311
+ }
312
+ break;
313
+ }
314
+ case "risk-descriptor": {
315
+ if (!(key in g))
316
+ break;
317
+ const next = redactRiskDescriptor(g[key]);
318
+ if (next !== g[key]) {
319
+ out[key] = next;
320
+ changed = true;
321
+ }
322
+ break;
323
+ }
324
+ default: {
325
+ const unhandled = treatment;
326
+ throw new Error(`unhandled gate field treatment: ${String(unhandled)}`);
176
327
  }
177
- nr.probeCause = np;
178
328
  }
179
- out.riskDescriptor = nr;
180
329
  }
181
330
  return changed ? out : gate;
182
331
  }
@@ -366,9 +515,11 @@ export function memoRedactTerminalResult(seat, result) {
366
515
  if (result === undefined || result === null)
367
516
  return result;
368
517
  const r = result;
369
- if (r.terminal === undefined && r.errorMessage === undefined && r.blockedReason === undefined && r.remoteEnvFailures === undefined)
518
+ if (REDACTABLE_RESULT_LEAVES.every((k) => r[k] === undefined))
370
519
  return result;
371
- const leaves = { terminal: r.terminal, errorMessage: r.errorMessage, blockedReason: r.blockedReason, remoteEnvFailures: r.remoteEnvFailures };
520
+ const leaves = {};
521
+ for (const k of REDACTABLE_RESULT_LEAVES)
522
+ leaves[k] = r[k];
372
523
  const redacted = memoRedactedRead(seat, leaves, (l) => redactTerminalResult(l));
373
524
  if (redacted === leaves)
374
525
  return result;
@@ -14,6 +14,18 @@ import type { Metrics } from "./metrics.js";
14
14
  * 🔴 BREAKING(server 7.64.0,core 7.6.0 提货):标签名 `source` → `deniedBy`,词表从七词
15
15
  * (`policy/hook/safety/shellGate/planMode/classifier/org`)换成八词
16
16
  * (`policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution`)。仪表盘/告警规则要改。
17
+ *
18
+ * 🔴 **第二个计数器 `auto_mode_classifier_unavailable_total{cause}`(server 7.70.0,core 7.12.0 [ref])**
19
+ * —— 分类器**停摆**的常开信号。7.12.0 之前这件事有两条可观测路:熔断跳闸时的
20
+ * `auto_mode_breaker_open_total`,以及「`unavailable` 的 ask 会走到人面前」(审批 inbox 里看得见)。
21
+ * 该版把熔断族整族退役、又把 `unavailable` 改成**当场 deny** ⇒ 两条路同时没了,而
22
+ * `permission_denied_total{deniedBy}` 对它是**瞎的**:分类器停摆与分类器正常拦下一次危险调用,
23
+ * 两者都记 `deniedBy:"classifier"`。一次持续停摆于是在默认监控上与「今天安全拦截多了点」不可区分
24
+ * (`TOOL_TRACE` 默认关,拿不到那半边)。core 同批把事实加在了 deny 上
25
+ * (`PermissionDeniedPayload.classifierUnavailable`),本仓就在**这同一只常开观察者**上消费它。
26
+ * 两个计数刻意**不合并成一个标签**:它们答两个问题(「谁拒的」vs「这次拒是不是因为分类器停摆、哪一类」),
27
+ * 而运维对停摆要立刻动手(改配置 / 等重试 / 看后端),对安全拦截不要。
28
+ * ⛔ 纯计量:不改任何裁决、不新开观察面、事实缺席时**不铸**(缺席 ≠ 「分类器好着呢」,只是这次拒不是它)。
17
29
  */
18
30
  export declare function createPermissionDeniedMeter(metrics: Pick<Metrics, "inc">): Hooks;
19
31
  /**
@@ -1,4 +1,4 @@
1
- import { isDeniedBy } from "@sema-agent/core";
1
+ import { isAutoModeUnavailableCause, isDeniedBy } from "@sema-agent/core";
2
2
  import { redactSecrets } from "../trace/redact.js";
3
3
  function deniedByLabel(gate) {
4
4
  const d = gate.disposition;
@@ -8,6 +8,10 @@ export function createPermissionDeniedMeter(metrics) {
8
8
  return {
9
9
  permissionDenied(payload) {
10
10
  metrics.inc("permission_denied_total", { deniedBy: deniedByLabel(payload.gate) });
11
+ const cause = payload.classifierUnavailable?.cause;
12
+ if (cause !== undefined) {
13
+ metrics.inc("auto_mode_classifier_unavailable_total", { cause: isAutoModeUnavailableCause(cause) ? cause : "other" });
14
+ }
11
15
  },
12
16
  };
13
17
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * **运维写下的 ask 一律自报为「显式 ask 规则」** —— 本仓所有 operator/治理面 `ask` 裁决的**唯一**铸造点。
3
+ *
4
+ * ── 为什么存在(成因是一次真红,不是预防性设计)────────────────────────────────────────────────
5
+ * core 7.11.0 [ref] 给 allow 层加了**第二位成员**:一条被 `readOnlyShellVerdict` 证明只读的 shell 命令,
6
+ * 在每一道收紧车道之后**直接放行**(`decisionReason:"read_only"`),没有分类器轮次、没有卡、没有 park。
7
+ * 它的消费谓词(`allowLayerMayClear`,与持久 allow 规则**同一只**)明写不会越过的几件事里,有一件是
8
+ * **显式 ask 规则**(`PermissionResult.matchedAskRule`)。
9
+ *
10
+ * 而本仓的 operator 门此前铸的是**裸** `{action:"ask", message}` —— 一条「someone WROTE 了这条规则」的
11
+ * 事实,从来没有在裁决上说出口。于是 7.11.0 提货那一拍,`APPROVAL_REQUIRE=Bash` 的部署上
12
+ * `echo x` / `cd sub` / `grep -r … ` 这一类**全部静默自动执行**:运维亲手写下的「这只工具必须人批」被
13
+ * 一个自动层清掉了,而且**一声不响**(本仓 7.68.0 的四条 e2e 当场红,那是这条缺口的机器证据)。
14
+ *
15
+ * ── 修法:一条规则,一个铸点 ────────────────────────────────────────────────────────────────
16
+ * core 的 `matchedAskRule` 顶注逐字给了自声明的授权与方向:「A policy self-declaring the member only ever
17
+ * OPTS ITS OWN ask OUT of classifier auto-resolution (**tightening** — the false-claim direction is safe)」。
18
+ * 所以本仓把它做成**不变量**而不是补丁:凡是「运维/部署写下的声明产生的 ask」,一律经本函数铸,自带那条
19
+ * 声明的原文。于是**任何**自动放行层(今天的持久规则与只读判词,明天的第三位成员)都够不着它 —— 规则从
20
+ * 「每来一个新 allow 层就回来补一次名单」变成「运维写的 ask 不可被自动清除」这一句。
21
+ *
22
+ * 🔴 **副作用是有意的、且方向只有一个**:带 `matchedAskRule` 的 ask 同时**退出 auto 模式分类器的自动
23
+ * 裁决**(core:「a person's standing "ask me each time" is not classifier hesitation」)。对
24
+ * `APPROVAL_NEVER_AUTO` 这是把一条**本来就漏**的缝补上(那张表的语义逐字是「always human」,而分类器
25
+ * 今天能替它答);对 `APPROVAL_REQUIRE` / `commandPolicy` 这是收紧一格 —— 与这两个旋钮的字面语义
26
+ * (「必须批准」)一致,且方向是 fail-closed。
27
+ *
28
+ * 🔴 **谁不进这扇门**(逐条,判据在 core 的同一段顶注):
29
+ * · **模式默认**的 ask(`task-settings.ts` 的 plan/default 档折叠)—— core 明写 `matchedAskRule`
30
+ * 「never for a `defaultAction:"ask"` fallback(an unmatched call is default-closed posture, not a
31
+ * per-call instruction)」;把姿态默认伪装成一条写下的规则会让分类器车道的资格判据整条失真。
32
+ * · **钩子**的 ask(`hooks/hook-runner.ts`)—— core 在钩子面**自己盖** `decisionReason:"hook"`
33
+ * (`dist/core/gate-fold.js`),那是 `allowLayerMayClear` 的另一条独立豁免;本仓再盖一次是第二个写者。
34
+ * · **问答工具**(`deployment-governance.ts` 的 `AskUserQuestion`)—— core 的谓词把保留问答工具单列豁免。
35
+ */
36
+ import type { PermissionResult } from "@sema-agent/core";
37
+ /**
38
+ * 铸一条**运维声明产生的** `ask`。
39
+ *
40
+ * @param rule 产生这条 ask 的**声明原文**(进 `matchedAskRule`)。它是 gate 内部消费的机器位 ——
41
+ * core 顶注逐字:「consumed inside the gate and deliberately NOT copied onto the park/approval-card
42
+ * request」,所以它既不上 wire、也不进卡,写的必须是**真实的那条声明**而不是一句好看的话。
43
+ * @param message 给人看的那句话(照旧上卡)。
44
+ */
45
+ export declare function operatorAsk(rule: string, message: string): PermissionResult;
46
+ //# sourceMappingURL=operator-ask.d.ts.map
@@ -0,0 +1,4 @@
1
+ export function operatorAsk(rule, message) {
2
+ return { action: "ask", message, decisionReason: "rule", matchedAskRule: rule };
3
+ }
4
+ //# sourceMappingURL=operator-ask.js.map
@@ -0,0 +1,96 @@
1
+ /** 一条索引行:**子会话 id 是主键**(会话 id 全局唯一 ⇒ 读口天然是单行精确查,无分页假阴性)。 */
2
+ export interface WorkflowAgentSessionRow {
3
+ /** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = checkpoint 的 `sourceTaskId`)。 */
4
+ sessionId: string;
5
+ /** 它所属的 workflow run(`w*`)。 */
6
+ runId: string;
7
+ /** 该 agent 调用的稳定顺位身份(`WorkflowAgentRun.callKey`)—— 行自描述用,取 run 后仍以行内实体为准。 */
8
+ agentKey: string;
9
+ /** run 的租户键(`WorkflowRun.scope`)。 */
10
+ scope: string;
11
+ }
12
+ /**
13
+ * 索引面的两个动词。**写者只有一个**(见文件头),读者是判别器/收敛器/读面。
14
+ *
15
+ * `syncRun` 语义 = 「这条 run 当前的 parked 行集**就是** rows」:多出来的旧行删,rows 里的行 upsert。
16
+ * 幂等、可重放、与 run 的一次成功写同拍。
17
+ */
18
+ export interface WorkflowAgentSessionIndex {
19
+ syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
20
+ findByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow | null>;
21
+ }
22
+ /** 派生的**唯一属主**:一只 `WorkflowRun` 当前的 parked 索引行。
23
+ *
24
+ * 三个键缺任何一个都不入索引 —— 缺 `sessionId` 的 park 行 join 不到(checkpoint 那侧的键就是它),
25
+ * 缺 `parkedCheckpointToken` 的行不是一条可赎回的 park(core 契约:在场 ⟺ `status:"parked"`)。
26
+ * ⚠️ token **不进索引行**:它是赎回能力,家在 run 的 blob 里(与 bg 行同判),索引只回答「去哪条 run 找」。 */
27
+ export declare function buildParkedAgentSessionRows(runId: string, run: {
28
+ scope: string;
29
+ agents?: ReadonlyArray<{
30
+ status?: string;
31
+ sessionId?: string;
32
+ callKey?: string;
33
+ parkedCheckpointToken?: string;
34
+ }>;
35
+ }): WorkflowAgentSessionRow[];
36
+ /** 两处(装饰器的变更检测、local 后端的重放)共用的行集等值判据 —— 顺序无关的集合比较,单一属主。 */
37
+ export declare function sameAgentSessionRows(a: readonly WorkflowAgentSessionRow[], b: readonly WorkflowAgentSessionRow[]): boolean;
38
+ /** 账本一行的**内域形**:一次 `syncRun` 的整份行集快照(`{runId, rows}`)。 */
39
+ export interface WorkflowAgentSessionLedgerLine {
40
+ runId: string;
41
+ rows: WorkflowAgentSessionRow[];
42
+ }
43
+ /**
44
+ * 盘上账本行 → 内域形的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as` 铸形)。
45
+ *
46
+ * 姿态与 SQL 孪生逐字同源:那边 `findByAgentSession` 逐字段收窄(`String(r.session_id)` …),这边逐字段
47
+ * 判形 —— 一张索引面、两个后端、**一种**入域姿态。
48
+ *
49
+ * **整行成败制**(与既有的撕裂行臂同一条规则):一行 = 一次 syncRun 的**整份**快照,任一位读不出来就说明
50
+ * 这份快照重建不出来 ⇒ 整行不 apply(那条 run 保留上一份已知良好的快照)。不做「丢掉坏成员、留下好成员」
51
+ * ——那会静默铸出一份**比真相少几条 park** 的行集,而这条索引的消费面正是 park 的赎回路由。
52
+ * 读不出来 ⇒ 调用方**响亮**记一笔([ref]:安全轴不许静默),不吞整档。
53
+ */
54
+ export declare function readAgentSessionLedgerLine(v: unknown): WorkflowAgentSessionLedgerLine | undefined;
55
+ /**
56
+ * 单机车道的索引(`FileWorkflowRunStore` / `InMemoryWorkflowRunStore` 的同室孪生)。
57
+ *
58
+ * `root` 在场 ⇒ 追加式 JSONL 账本(与姊妹 {@link import("./workflow-notify-journal.js").FileWorkflowNotifyJournalStore}
59
+ * 逐字同形:0o700 目录 + 0o600 账本、按行重放、末行撕裂即丢),重启后 park 仍可 join;缺席 ⇒ 纯内存
60
+ * (`WORKFLOW_RUN_STORE=memory` 的显式易失车道,与它的 run 店同寿命)。
61
+ *
62
+ * 账本的一行 = **一次 `syncRun` 的整份行集**(按 runId 后写覆盖),所以重放即收敛,不需要压缩腿。
63
+ */
64
+ export declare class LocalWorkflowAgentSessionIndex implements WorkflowAgentSessionIndex {
65
+ private readonly fsyncEnabled;
66
+ /** 坏账本行的**响亮**出口(可选;缺席 ⇒ 那一行照样不入索引,只是没人听见)。形与 workflow 店族的
67
+ * `onWarn` 逐字同源(`workflow-completion-inbox.ts` 的那一只),不另立第二种披露通道。 */
68
+ private readonly onWarn?;
69
+ private readonly ledgerPath;
70
+ private readonly byRun;
71
+ private readonly bySession;
72
+ private fd;
73
+ constructor(root?: string, fsyncEnabled?: boolean,
74
+ /** 坏账本行的**响亮**出口(可选;缺席 ⇒ 那一行照样不入索引,只是没人听见)。形与 workflow 店族的
75
+ * `onWarn` 逐字同源(`workflow-completion-inbox.ts` 的那一只),不另立第二种披露通道。 */
76
+ onWarn?: ((msg: string, meta: Record<string, unknown>) => void) | undefined);
77
+ /**
78
+ * 打开账本时把**撕裂尾**(崩在 append 中途、末尾没有换行的那截字节)截回最后一个完整行边界。
79
+ *
80
+ * 🔴 为什么必须在**任何 append 之前**做(codex 修复验证轮 [medium],故障注入复现):残尾留在盘上时,
81
+ * 下一次 `syncRun` 会紧贴着它落字节 ⇒ `<残尾><新记录>` 在 replay 里是**一行**,JSON 解析失败被整行丢掉。
82
+ * 丢的**不是**那截未提交的残尾(丢它是对的),而是紧随其后那次**成功重试**写下的行 —— 于是「写失败了
83
+ * 下一次写会重试救回来」这条本模块与 `syncAgentSessionIndex` 都在依赖的性质当场失效,park 索引静默消失。
84
+ * 截尾之后 append 永远从一个干净的行边界开始,重试才真的救得回来。
85
+ *
86
+ * 截掉的只可能是**未提交**的字节(一条记录的耐久性以它那个换行为界,与 notify journal 同一条崩溃序),
87
+ * 所以这里不是丢数据;但它是一次真实的崩溃留痕,**响亮**一次([ref]:安全轴不许静默)。
88
+ */
89
+ private truncateTornTail;
90
+ private replay;
91
+ /** 内存两张表的收敛(重放与写路径共用一份逻辑)。 */
92
+ private apply;
93
+ syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
94
+ findByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow | null>;
95
+ }
96
+ //# sourceMappingURL=workflow-agent-session-index.d.ts.map