@sema-agent/server 7.10.0 → 7.12.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 (83) hide show
  1. package/README.md +1 -1
  2. package/USAGE.md +56 -0
  3. package/dist/adoption/plan.d.ts +152 -0
  4. package/dist/adoption/plan.js +513 -0
  5. package/dist/adoption/runner.d.ts +54 -0
  6. package/dist/adoption/runner.js +505 -0
  7. package/dist/adoption/sql.d.ts +76 -0
  8. package/dist/adoption/sql.js +106 -0
  9. package/dist/adoption/wire.d.ts +250 -0
  10. package/dist/adoption/wire.js +153 -0
  11. package/dist/approval-card.d.ts +24 -0
  12. package/dist/approval-card.js +32 -0
  13. package/dist/auth-keys.d.ts +28 -4
  14. package/dist/auth-keys.js +60 -15
  15. package/dist/boot/adoption.d.ts +30 -0
  16. package/dist/boot/adoption.js +57 -0
  17. package/dist/boot/coordinators.d.ts +4 -0
  18. package/dist/boot/coordinators.js +3 -1
  19. package/dist/boot/parked-revive-gate.d.ts +56 -7
  20. package/dist/boot/parked-revive-gate.js +177 -8
  21. package/dist/boot/permission-rules-audit.d.ts +49 -0
  22. package/dist/boot/permission-rules-audit.js +57 -0
  23. package/dist/boot/resolve-spec.js +43 -12
  24. package/dist/boot/runner-deps.d.ts +10 -2
  25. package/dist/boot/runner-deps.js +12 -1
  26. package/dist/budget.js +22 -0
  27. package/dist/config-types.d.ts +24 -1
  28. package/dist/config.d.ts +28 -2
  29. package/dist/config.js +348 -75
  30. package/dist/governance-ask-marks.js +8 -2
  31. package/dist/http/route-ctx.d.ts +6 -3
  32. package/dist/http/routes/adoption.d.ts +26 -0
  33. package/dist/http/routes/adoption.js +120 -0
  34. package/dist/http/routes/approvals-assistant.js +2 -1
  35. package/dist/http/routes/capabilities.js +49 -2
  36. package/dist/http/routes/rules.d.ts +35 -0
  37. package/dist/http/routes/rules.js +293 -0
  38. package/dist/http/routes/shared-memory.d.ts +31 -0
  39. package/dist/http/routes/shared-memory.js +181 -0
  40. package/dist/http/routes/trace-usage.js +139 -3
  41. package/dist/http/server.d.ts +23 -3
  42. package/dist/http/server.js +123 -3
  43. package/dist/http/wire-types.d.ts +48 -0
  44. package/dist/main.js +70 -3
  45. package/dist/observability/fail-open.d.ts +12 -0
  46. package/dist/observability/fail-open.js +12 -0
  47. package/dist/observability/metrics.js +2 -1
  48. package/dist/observability/tool-trace.d.ts +5 -1
  49. package/dist/observability/tool-trace.js +33 -6
  50. package/dist/parked-decide.d.ts +13 -3
  51. package/dist/parked-decide.js +10 -1
  52. package/dist/plugins/adoption-log-sql.d.ts +191 -0
  53. package/dist/plugins/adoption-log-sql.js +273 -0
  54. package/dist/plugins/checkpoint-store-sql.d.ts +13 -0
  55. package/dist/plugins/checkpoint-store-sql.js +11 -0
  56. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  57. package/dist/plugins/local-checkpoint-store.js +8 -0
  58. package/dist/plugins/permission-rule-store-file.d.ts +83 -0
  59. package/dist/plugins/permission-rule-store-file.js +371 -0
  60. package/dist/plugins/permission-rule-store-sql.d.ts +249 -0
  61. package/dist/plugins/permission-rule-store-sql.js +828 -0
  62. package/dist/plugins/pg-pool.js +37 -0
  63. package/dist/plugins/session-policy-store-sql.d.ts +6 -0
  64. package/dist/plugins/session-policy-store-sql.js +7 -1
  65. package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
  66. package/dist/plugins/shared-memory-store-sql.js +516 -0
  67. package/dist/plugins/store-backend.d.ts +38 -2
  68. package/dist/plugins/store-backend.js +69 -6
  69. package/dist/plugins/tidb-pool.js +47 -0
  70. package/dist/rules-consent.d.ts +194 -0
  71. package/dist/rules-consent.js +240 -0
  72. package/dist/run-local.js +120 -13
  73. package/dist/runtime-governance.js +9 -3
  74. package/dist/shared-memory-scope-authorizer.d.ts +29 -0
  75. package/dist/shared-memory-scope-authorizer.js +17 -0
  76. package/dist/task-settings.d.ts +44 -0
  77. package/dist/task-settings.js +57 -1
  78. package/dist/tool-approval.d.ts +60 -0
  79. package/dist/tool-approval.js +223 -25
  80. package/dist/trace/core-keyset-guard.d.ts +15 -4
  81. package/dist/trace/project.d.ts +20 -2
  82. package/dist/trace/project.js +25 -4
  83. package/package.json +3 -3
package/dist/auth-keys.js CHANGED
@@ -8,37 +8,82 @@
8
8
  * (verifyApprovalHmac / verifyPrincipalJwt) stay in ./approval-hmac.ts and ./principal-jwt.ts; consumers import these four names from HERE ([2354] 兼容面全清).
9
9
  */
10
10
  /** Parse APPROVAL_HMAC_KEYS — a sema-registry-managed, rotating key-SET, as a JSON array of {kid,key,status}.
11
- * Empty / unset / unparseable ⇒ `[]` ⇒ HMAC verification is OFF (D-G inactive; back-compat, BFF-only door). */
12
- export function parseApprovalHmacKeys(raw) {
11
+ * Empty / unset / unparseable ⇒ `[]` ⇒ HMAC verification is OFF (D-G inactive; back-compat, BFF-only door).
12
+ * fail-closed 语义与改前**逐字相同**;新增的只有 `onReject`(#210:每一条被丢弃的条目都要说出来) */
13
+ export function parseApprovalHmacKeys(raw, onReject) {
13
14
  if (!raw)
14
15
  return [];
16
+ let v;
15
17
  try {
16
- const v = JSON.parse(raw);
17
- if (!Array.isArray(v))
18
- return [];
19
- return v.filter((k) => !!k && typeof k.kid === "string" && typeof k.key === "string");
18
+ v = JSON.parse(raw);
20
19
  }
21
20
  catch {
21
+ // ⚠️ 解析器自带的 message **不能**转述:V8 的 `JSON.parse` 报错里嵌了输入片段
22
+ // (`Unexpected token 'T', "TOP_SECRET"... is not valid JSON`),而这条 env 的内容就是对称密钥 ——
23
+ // 把它写进 boot 日志等于把密钥前缀印出来。只说「不是合法 JSON」,定位靠 env 名本身。
24
+ onReject({ index: "-", reason: "not valid JSON (value withheld: this env holds secrets)" });
22
25
  return [];
23
26
  }
27
+ if (!Array.isArray(v)) {
28
+ onReject({ index: "-", reason: "not a JSON array of {kid,key,status} entries" });
29
+ return [];
30
+ }
31
+ const out = [];
32
+ v.forEach((raw, i) => {
33
+ const e = raw;
34
+ if (!e || typeof e !== "object") {
35
+ onReject({ index: i, reason: "entry is not an object" });
36
+ return;
37
+ }
38
+ const bad = ["kid", "key"].filter((f) => typeof e[f] !== "string");
39
+ if (bad.length > 0)
40
+ onReject({ index: i, reason: `missing/non-string field(s): ${bad.join(", ")}` });
41
+ else
42
+ out.push(e);
43
+ });
44
+ return out;
24
45
  }
25
46
  /** Parse PRINCIPAL_JWT_PUBKEYS — a JSON array of {kid,alg,key,status}. Empty/unset/malformed ⇒ [] ⇒ no trust
26
- * anchor ⇒ the direct door cannot open (D-G inactive). Each entry must have kid + a pinned alg + a PEM key. */
27
- export function parsePrincipalJwks(raw) {
47
+ * anchor ⇒ the direct door cannot open (D-G inactive). Each entry must have kid + a pinned alg + a PEM key.
48
+ * #210: fail-closed 语义逐字不变,但每条被丢弃的条目经 `onReject` 报出(序号 + 缺陷字段名,不带值)——
49
+ * 这里的 key 是**公**钥,但 reason 仍只说字段名,与 HMAC 那条同形(两个出口的口径不该分岔)。 */
50
+ export function parsePrincipalJwks(raw, onReject) {
28
51
  if (!raw)
29
52
  return [];
30
53
  const ALGS = new Set(["EdDSA", "RS256", "ES256"]);
54
+ let v;
31
55
  try {
32
- const v = JSON.parse(raw);
33
- if (!Array.isArray(v))
34
- return [];
35
- return v.filter((k) => {
36
- const e = k;
37
- return !!e && typeof e.kid === "string" && typeof e.key === "string" && typeof e.alg === "string" && ALGS.has(e.alg);
38
- });
56
+ v = JSON.parse(raw);
39
57
  }
40
58
  catch {
59
+ // 同 parseApprovalHmacKeys:不转述 V8 的 message(它嵌输入片段)。这一口装的是**公**钥,但两个出口
60
+ // 的口径不该分岔 —— 一个「视情况才脱敏」的规矩,迟早会在错的那一侧被抄走。
61
+ onReject({ index: "-", reason: "not valid JSON (value withheld)" });
62
+ return [];
63
+ }
64
+ if (!Array.isArray(v)) {
65
+ onReject({ index: "-", reason: "not a JSON array of {kid,alg,key,status} entries" });
41
66
  return [];
42
67
  }
68
+ const out = [];
69
+ v.forEach((row, i) => {
70
+ const e = row;
71
+ if (!e || typeof e !== "object") {
72
+ onReject({ index: i, reason: "entry is not an object" });
73
+ return;
74
+ }
75
+ const bad = ["kid", "key"].filter((f) => typeof e[f] !== "string");
76
+ // alg 单列:缺席与「不在闭集」是两种手滑,但都必须点名 `alg` —— 运维看到字段名才知道去改哪一格
77
+ // (alg-confusion 防线就挂在这条钉上,静默滤掉等于防线自己消失了没人知道)。
78
+ if (typeof e.alg !== "string")
79
+ bad.push("alg");
80
+ else if (!ALGS.has(e.alg))
81
+ bad.push(`alg (must be one of: ${[...ALGS].join(", ")})`);
82
+ if (bad.length > 0)
83
+ onReject({ index: i, reason: `missing/invalid field(s): ${bad.join(", ")}` });
84
+ else
85
+ out.push(e);
86
+ });
87
+ return out;
43
88
  }
44
89
  //# sourceMappingURL=auth-keys.js.map
@@ -0,0 +1,30 @@
1
+ import type { StoreBackend } from "../plugins/store-backend.js";
2
+ export interface AdoptionBootCtx {
3
+ backend: StoreBackend | undefined;
4
+ logger: {
5
+ info?(msg: string, meta?: unknown): void;
6
+ warn?(msg: string, meta?: unknown): void;
7
+ error?(msg: string, meta?: unknown): void;
8
+ };
9
+ now?: () => number;
10
+ }
11
+ export interface AdoptionBootReading {
12
+ /** 本次扫到的在飞行数(0 = 常态)。 */
13
+ scanned: number;
14
+ /** 推到终态的弧。 */
15
+ resumed: string[];
16
+ /** 推不动的弧(锁被别的副本持着,或还差一步)—— 留在库里,下一个副本/下一次读面继续推。 */
17
+ stalled: string[];
18
+ /** 续跑时撞上目的地冲突而落 rejected 终态的弧。 */
19
+ rejected: string[];
20
+ /** 推不动**且抛了异常**的弧(逐弧兜住 —— 一条坏行不许绑架其它弧的恢复)。 */
21
+ failed: string[];
22
+ /** 扫描本身失败(库不可达等)。**不吞** —— 消息进启动日志。 */
23
+ error?: string;
24
+ }
25
+ /**
26
+ * boot 期的在飞收编扫描。无 SQL 后端 ⇒ 零动作(收编面在那种部署上本来就 501)。
27
+ * 绝不抛:一次库抖动不该拒掉整台 worker 的启动,但**必须**留下响亮读数(`error` 位 + error 级日志)。
28
+ */
29
+ export declare function runAdoptionBootScan(ctx: AdoptionBootCtx): Promise<AdoptionBootReading>;
30
+ //# sourceMappingURL=adoption.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * design/183 I6(server 同族)—— **每副本 boot 必查收编日志**。
3
+ *
4
+ * 🔴 为什么这道门必须存在:phase 真源住 SQL 正是为了「发起 host 在两条腿之间被摧毁 ⇒ 接棒副本按日志
5
+ * 续跑」。可是**没有人会替它按下重试** —— 运维那次 POST 的响应早就丢了。所以每个副本起来时都扫一遍
6
+ * 在飞行:能推就推到终态,推不动就**响亮**留痕(禁静默跳过 —— 静默跳过等于把半迁移状态带进服务期,
7
+ * 那正是 I6 存在的理由)。
8
+ *
9
+ * 🔴 为什么是「续跑或响亮拒」而不是「拒启」:半迁移的收编**是可恢复的**(每条腿幂等,phase 单调),
10
+ * 拒启只会让一个能自愈的状态变成一次停机。拒启留给不可恢复的装配谎言(boot/org-memory.ts 那类)。
11
+ * 与之对称:续跑失败**不吞异常** —— 它落一条 error 级留痕并把 outcome 记进返回值,调用方(main)
12
+ * 把读数打进启动日志,运维在启动行里就能看见「这台副本上还有 N 条收编没走完」。
13
+ *
14
+ * 阻塞性:扫描在**监听之前**跑完(main 的调用点)。一条弧的推进是几条 UPDATE,毫秒级;库里正常状态下
15
+ * 在飞行数为 0,一次 `WHERE state='in_flight'` 的索引点查而已。
16
+ */
17
+ import { createAdoptionRunner } from "../adoption/runner.js";
18
+ /**
19
+ * boot 期的在飞收编扫描。无 SQL 后端 ⇒ 零动作(收编面在那种部署上本来就 501)。
20
+ * 绝不抛:一次库抖动不该拒掉整台 worker 的启动,但**必须**留下响亮读数(`error` 位 + error 级日志)。
21
+ */
22
+ export async function runAdoptionBootScan(ctx) {
23
+ const store = ctx.backend?.adoptionLog?.();
24
+ if (!store)
25
+ return { scanned: 0, resumed: [], stalled: [], rejected: [], failed: [] };
26
+ const runner = createAdoptionRunner({
27
+ store,
28
+ dialect: ctx.backend?.kind === "pg" ? "pg" : "tidb",
29
+ now: ctx.now ?? Date.now,
30
+ logger: ctx.logger,
31
+ });
32
+ try {
33
+ const inFlight = await store.listInFlight();
34
+ if (inFlight.length === 0)
35
+ return { scanned: 0, resumed: [], stalled: [], rejected: [], failed: [] };
36
+ ctx.logger.warn?.("adoption_in_flight_on_boot", {
37
+ count: inFlight.length,
38
+ ids: inFlight.map((r) => r.adoptionId),
39
+ note: "design/183 I6: resuming in-flight identity adoptions before this replica starts serving",
40
+ });
41
+ const out = await runner.resumeInFlight();
42
+ const reading = { scanned: inFlight.length, ...out };
43
+ if (reading.stalled.length > 0 || reading.rejected.length > 0 || reading.failed.length > 0) {
44
+ ctx.logger.error?.("adoption_boot_scan_incomplete", reading);
45
+ }
46
+ else {
47
+ ctx.logger.info?.("adoption_boot_scan_resumed", reading);
48
+ }
49
+ return reading;
50
+ }
51
+ catch (err) {
52
+ const error = err instanceof Error ? err.message : String(err);
53
+ ctx.logger.error?.("adoption_boot_scan_failed", { error });
54
+ return { scanned: 0, resumed: [], stalled: [], rejected: [], failed: [], error };
55
+ }
56
+ }
57
+ //# sourceMappingURL=adoption.js.map
@@ -15,11 +15,15 @@ import { ToolApprovalCoordinator } from "../tool-approval.js";
15
15
  import { SendUserFileEmitter } from "../capabilities/send-user-file-tool.js";
16
16
  import { TaskEnvRegistry } from "../capabilities/sandbox-file-send.js";
17
17
  import type { StoreBackend } from "../plugins/store-backend.js";
18
+ import type { RuleConsentLane } from "../rules-consent.js";
18
19
  export interface LiveCoordinatorsCtx {
19
20
  config: ServiceConfig;
20
21
  logger: Logger;
21
22
  backend: StoreBackend | undefined;
22
23
  sendUserFileTaskEnvs: TaskEnvRegistry | undefined;
24
+ /** #154 车二:持久化权限规则的同意车道(main.ts 与 `RunnerDeps.permissionRuleStore` **同源**于
25
+ * `backend.permissionRule()` 的同一个返回值)。在场 ⇒ ask 帧投 `ruleSuggestions` + 回决可兑付。 */
26
+ ruleConsent: RuleConsentLane | undefined;
23
27
  }
24
28
  export declare function createLiveCoordinators(ctx: LiveCoordinatorsCtx): {
25
29
  elicitation: ElicitationCoordinator | undefined;
@@ -8,7 +8,7 @@ import { withLedgerRecording } from "../plugins/send-file-ledger.js";
8
8
  import { basename, resolve } from "node:path";
9
9
  import { stat as fsStat, readFile as fsReadFile } from "node:fs/promises";
10
10
  export function createLiveCoordinators(ctx) {
11
- const { config, logger, backend, sendUserFileTaskEnvs } = ctx;
11
+ const { config, logger, backend, sendUserFileTaskEnvs, ruleConsent } = ctx;
12
12
  // E23 (shell-host contract): inbound MCP elicitation coordinator (live-only HITL). Present ONLY when MCP_ELICITATION_ENABLED
13
13
  // — absent ⇒ onElicit is not wired ⇒ core advertises no elicitation capability to any server (fail-closed). Shared
14
14
  // by the runner (the onElicit seam) and the HTTP layer (the respond route + the per-run ALS context wraps).
@@ -71,6 +71,8 @@ export function createLiveCoordinators(ctx) {
71
71
  // X-2 写侧准入门两帽(协议未上场时门本身不参与,见 ToolApprovalCoordinator.admit)。
72
72
  admitMaxPerTask: config.streamApproval.admitMaxPerTask,
73
73
  admitMaxPerOwner: config.streamApproval.admitMaxPerOwner,
74
+ // #154 车二:同意车道(缺席 = 规则店没装配 ⇒ 帧上零候选、回决的 persistRule 如实拒)。
75
+ ...(ruleConsent !== undefined ? { ruleConsent } : {}),
74
76
  })
75
77
  : undefined;
76
78
  // SendUserFile(真 CC 契约,clay 2026-07-14)两种 lane 形态,其余缺席=诚实(工具不进 roster):
@@ -1,5 +1,6 @@
1
- import { type BackgroundAgentRecord, type ToolExecuteContext } from "@sema-agent/core";
1
+ import { type BackgroundAgentRecord, type RunnerDeps, type ToolExecuteContext } from "@sema-agent/core";
2
2
  import type { ServiceConfig } from "../config.js";
3
+ import type { RunnerDepsOnAsk } from "./runner-deps.js";
3
4
  import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView, type LiveQuestionFace } from "../deployment-governance.js";
4
5
  import { type SandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
5
6
  /** 本腿读的配置切面(`ServiceConfig` 结构满足)。两个构造口各自的切面 + lane 判别式读的 `remoteExec`。 */
@@ -15,9 +16,50 @@ export interface ApprovalExemptionProbe {
15
16
  export interface ParkedReviveGateDeps {
16
17
  /** ⚠️ **活引用**:热改字段(autonomy/commandPolicy/守卫集/审批四旋钮)每次赎回现读,见下方「禁 memoize」。 */
17
18
  readonly config: ParkedReviveGateConfigView;
18
- /** #152:活体 AskUserQuestion 面。赎回腿今天无 ALS ctx ⇒ 判 ask,与原形同判。 */
19
+ /** #152:活体 AskUserQuestion 面。赎回腿今天无 ALS ctx ⇒ 判 ask,与原形同判。
20
+ * ⚠️ 它同时是**决议链元数据** `contentMandate` 的唯一判据(见 {@link mandatePostureOf})。 */
19
21
  readonly question: LiveQuestionFace | undefined;
22
+ /**
23
+ * 部署级活体审批席 —— `RunnerDeps.onAsk` 的**同源铸法**(main.ts 递 `createRunnerDepsOnAsk(toolApproval)`,
24
+ * 单一属主同 boot/runner-deps.ts)。
25
+ *
26
+ * ⚠️ **本仓确有 per-task `spec.onAsk` 装配点**(codex 对抗复审 R1-中1 验真纠正 —— 别照抄
27
+ * `createRunnerDepsOnAsk` 头注那句「bg/resume 两条腿没有 per-task 装配点」,那句话的射程只是那两条腿):
28
+ * `http/routes/tasks.ts` 的宿主 sync/stream 腿在装配点写 `spec.onAsk = toolApproval.boundAsk({owner,
29
+ * taskId, sessionId, legKey…})`(windowZero 形则写一个恒返 `"unavailable"` 的闭包)。core 冻的是
30
+ * `spec.onAsk ?? deps.onAsk`,所以那条腿冻下去的是**带身份的** bound 席,不是这一格。
31
+ *
32
+ * 这对本腿的两个用途分别意味着什么:
33
+ * · **`durableMandate` 判据(承重,见 {@link mandatePostureOf})—— 不受影响**:两个装配点是**同一个
34
+ * 条件**的两种表达(`spec.onAsk` 的两支都在 `deps.toolApproval` 在场时才写,windowZero 支写的也是
35
+ * 函数),所以「席位在不在场」这个**布尔**在 spec 位与 deps 位上恒等。core 的
36
+ * `isLiveApproverSeat` 只判 `typeof === "function"` ⇒ 本腿算出的位与 park 时冻的位逐字相同,摘要对得上。
37
+ * · **重建条目的 `onAsk` 席位本身 —— 是一个诚实的降级,不是等价替换**:bound 席携的身份
38
+ * (owner/taskId/sessionId/legKey/腿时限)是**宿主那条腿**的,park 时未持久化 ⇒ 跨副本重建不出来。
39
+ * 交这一格的效果:赎回腿没有 ALS 上下文,`ToolApprovalCoordinator.ask` 第一行 `if (!ctx) return
40
+ * "unavailable"`(tool-approval.ts)⇒ 继承 ask 恒走 core 的 `approverUnavailable` 回路 = **再 park
41
+ * 一次给人**。它**到不了**宿主原来那张卡(那是残余,与「客户端折叠层重建不出」同族);它也**不会**
42
+ * 错投到别处(无 ctx 就直接返回,`askBroadcast` 根本不执行 —— codex 提出的「按赎回腿归因/投递」
43
+ * 这条机制经亲读证伪)。
44
+ * 为什么仍然要交:席位缺席 + `durableMandate` 为 false(装了活体席的部署就是这一形)⇒ 继承 ask 只剩
45
+ * headless auto-deny,比「再问人一次」严且哑。交了之后最坏也是 park,方向 fail-safe。
46
+ */
47
+ readonly approverSeat: RunnerDepsOnAsk | undefined;
20
48
  readonly approvalExemptionStore: ApprovalExemptionProbe | undefined;
49
+ /**
50
+ * A-010.1 —— core `RunnerDeps.runtimeCapsResolver` 的**同一只**(main.ts 的 `runtimeCapsResolver`,
51
+ * 单一属主同 `boot/runtime-caps.ts`)。重建条目的第三位决议链元数据 `autoModeArmed` 的唯一判据是
52
+ * per-principal 的 `RuntimeCaps.autoMode`,而它只有 center 解得出 —— 见 {@link autoModePostureOf}。
53
+ * 缺席(无 center 的部署 / dry-run)⇒ core 侧 `runtimeCaps` 也恒 undefined ⇒ 该位在 park 时也从不置位,
54
+ * 两侧同为「不供」,逐字零行为差。
55
+ *
56
+ * 类型从 **core 的载体位**推导(同 `boot/runner-deps.ts:94` 的先例,理由同 {@link RebuiltInheritedGate}:
57
+ * 手抄一个等价签名会让 core 改这条 seam 的那天变成静默漂移而不是编译期事件)。
58
+ */
59
+ readonly resolveRuntimeCaps?: RunnerDeps["runtimeCapsResolver"];
60
+ /** `RunnerDeps.autoMode` 分类器面(信任门半场)在不在场 —— core 的武装条件是
61
+ * `runtimeCaps.autoMode === true` **∧** 这一面在场,两个半场缺一不武装(prepare-task 原式)。 */
62
+ readonly autoModeSeatMounted?: boolean;
21
63
  readonly logger: {
22
64
  info(event: string, fields?: Record<string, unknown>): void;
23
65
  warn(event: string, fields?: Record<string, unknown>): void;
@@ -36,6 +78,9 @@ export interface ParkedReviveGateDeps {
36
78
  * 事件(载体键集钉见 test/parked-revive-gate-wiring.test.ts)。
37
79
  */
38
80
  export type RebuiltInheritedGate = NonNullable<NonNullable<NonNullable<ToolExecuteContext["reviveClaim"]>["parkedResume"]>["inheritedGate"]>;
81
+ /** 重建链的**单层条目**形 —— 同样从载体推导(理由同 {@link RebuiltInheritedGate}:core 又加一个
82
+ * 能收紧的槽时,本仓的编译面要看得见)。 */
83
+ export type RebuiltParentConstraint = NonNullable<RebuiltInheritedGate["parentConstraints"]>[number];
39
84
  /**
40
85
  * 赎回腿的父约束链重建(design/181 件二)。
41
86
  *
@@ -66,13 +111,17 @@ export type RebuiltInheritedGate = NonNullable<NonNullable<NonNullable<ToolExecu
66
111
  * 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
67
112
  * · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
68
113
  * · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
69
- * · `onAsk` 不供,但**这不等于 auto-deny**(codex R3-中1 验真纠正):带 `durableMandate` 的继承层
70
- * `ask` 时,core 只要还看得见 checkpoint + durable 审批配置就把它**重新浮回 durable 门**
71
- * (再 park 一次,人再批一次);deny 只是「durable 设施不在场」的兜底臂。⛔ 但**不是无条件**
72
- * (codex R4-中1):core 对挂起链计 `suspendCount`,到 `maxSuspends`(默认 5)即终止任务而不再 park。
114
+ * · `autoModeArmed` 轴(A-010.1)= 按**当前** per-principal entitlement 现解( {@link autoModePostureOf});
115
+ * toolPolicy 轴同族:挂起期间权益被撤/被授 摘要对不上 core pre-CAS 响亮拒,不静默换姿势跑。
116
+ * · `onAsk` = 部署的活体审批席(`deps.approverSeat`),**在场即供**:park 时那一层冻的就是它。
117
+ * 席位缺席的部署上不供,而**这不等于 auto-deny**(codex R3-中1 验真纠正):此形恒带
118
+ * `durableMandate`(见 {@link mandatePostureOf}),继承层判 `ask` 时 core 只要还看得见 checkpoint 店
119
+ * + durable 审批配置就把它**重新浮回 durable 门**(再 park 一次,人再批一次);deny 只是「durable
120
+ * 设施不在场」的兜底臂。⛔ 但**不是无条件**(codex R4-中1):core 对挂起链计 `suspendCount`,到
121
+ * `maxSuspends`(默认 5)即终止任务而不再 park。
73
122
  * · 客户端每请求 settings 折出来的层(`settings.permissions` 的 allow/deny/ask、模式派生的 fs-write
74
123
  * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
75
124
  * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
76
125
  */
77
- export declare function createParkedReviveInheritedGate(deps: ParkedReviveGateDeps): (row: BackgroundAgentRecord) => RebuiltInheritedGate;
126
+ export declare function createParkedReviveInheritedGate(deps: ParkedReviveGateDeps): (row: BackgroundAgentRecord) => Promise<RebuiltInheritedGate>;
78
127
  //# sourceMappingURL=parked-revive-gate.d.ts.map
@@ -7,7 +7,9 @@
7
7
  * 治理矩阵的第三维消费腿)。装配次序契约不变:main.ts 仍在同一位置构造,构造条件逐字保持。
8
8
  */
9
9
  import { join } from "node:path";
10
- import { NodeExecutionEnv } from "@sema-agent/core";
10
+ import { createAutoModeDecider, NodeExecutionEnv } from "@sema-agent/core";
11
+ import { recordFailOpen } from "../observability/fail-open.js";
12
+ import { decodeCheckpointScope } from "../security.js";
11
13
  import { createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "../deployment-governance.js";
12
14
  import { applyRuntimeGovernance } from "../runtime-governance.js";
13
15
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
@@ -57,18 +59,26 @@ function hostGateCwd(config, localRoot) {
57
59
  * 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
58
60
  * · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
59
61
  * · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
60
- * · `onAsk` 不供,但**这不等于 auto-deny**(codex R3-中1 验真纠正):带 `durableMandate` 的继承层
61
- * `ask` 时,core 只要还看得见 checkpoint + durable 审批配置就把它**重新浮回 durable 门**
62
- * (再 park 一次,人再批一次);deny 只是「durable 设施不在场」的兜底臂。⛔ 但**不是无条件**
63
- * (codex R4-中1):core 对挂起链计 `suspendCount`,到 `maxSuspends`(默认 5)即终止任务而不再 park。
62
+ * · `autoModeArmed` 轴(A-010.1)= 按**当前** per-principal entitlement 现解( {@link autoModePostureOf});
63
+ * toolPolicy 轴同族:挂起期间权益被撤/被授 摘要对不上 core pre-CAS 响亮拒,不静默换姿势跑。
64
+ * · `onAsk` = 部署的活体审批席(`deps.approverSeat`),**在场即供**:park 时那一层冻的就是它。
65
+ * 席位缺席的部署上不供,而**这不等于 auto-deny**(codex R3-中1 验真纠正):此形恒带
66
+ * `durableMandate`(见 {@link mandatePostureOf}),继承层判 `ask` 时 core 只要还看得见 checkpoint 店
67
+ * + durable 审批配置就把它**重新浮回 durable 门**(再 park 一次,人再批一次);deny 只是「durable
68
+ * 设施不在场」的兜底臂。⛔ 但**不是无条件**(codex R4-中1):core 对挂起链计 `suspendCount`,到
69
+ * `maxSuspends`(默认 5)即终止任务而不再 park。
64
70
  * · 客户端每请求 settings 折出来的层(`settings.permissions` 的 allow/deny/ask、模式派生的 fs-write
65
71
  * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
66
72
  * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
67
73
  */
68
74
  export function createParkedReviveInheritedGate(deps) {
69
- const { config, question, approvalExemptionStore, logger, localRoot } = deps;
75
+ const { config, question, approvalExemptionStore, logger, localRoot, approverSeat } = deps;
70
76
  const slots = deps.slots ?? sandboxPathEnvSlots;
71
- return (row) => {
77
+ return async (row) => {
78
+ // A-010.1:三位决议链元数据。**async 的唯一理由**就是其中两位要按 principal 现解 entitlement
79
+ // (core 的 resolver 座本身也是 sync-or-async),而这条腿的消费点(parked-decide 的 `decideParkedAgent`)
80
+ // 从来就在 async 里,「席位必须同步」是本文件此前自设的约束,不是结构事实。
81
+ const metadata = await chainMetadataOf(deps, row);
72
82
  // lane 分形与 resolve-spec 同一判别式(单一属主,取值处只此一个)。
73
83
  // 🔑 沙箱腿的 slot 键 = **row.sessionId**(子代自己的会话,design/181 §2.2 裁定):revive 后 core 以
74
84
  // 该 sessionId 铸 env 并注册 slot(subagent → prepare-task → 工厂装饰器),这个键真能绑上。
@@ -101,11 +111,170 @@ export function createParkedReviveInheritedGate(deps) {
101
111
  // 治理段无可施加时 applyRuntimeGovernance 原样返回 base ⇒ 基线本身即为链(恒非 undefined,无需断言)。
102
112
  // shellGate 只在治理段真产出档位时出现在载体上(缺席 = 本次没有 live 值可交,core 用 seed 兜底)。
103
113
  return {
104
- parentConstraints: [{ policy: governed.toolPolicy ?? baseline, durableMandate: true }],
114
+ parentConstraints: [
115
+ {
116
+ policy: governed.toolPolicy ?? baseline,
117
+ ...(approverSeat !== undefined ? { onAsk: approverSeat } : {}),
118
+ ...metadata,
119
+ },
120
+ ],
105
121
  ...(governed.shellGate !== undefined ? { shellGate: governed.shellGate } : {}),
106
122
  };
107
123
  };
108
124
  }
125
+ /**
126
+ * 重建条目的**决议链元数据**(core 5.22.0 F-012)—— 三位的逐字重算,**一次** entitlement 现解喂全三位
127
+ * (第三位与两位 mandate 的 `forceDurableGate` 项读的是同一次 caps,分两次解会开一个「同一条链里两位读到
128
+ * 不同代 entitlement」的窗)。
129
+ *
130
+ * 为什么它承重:5.22.0 把 re-supply 契约从 count 换成 `cpv1:` 内容摘要
131
+ * (`constraintChainDigest`),而摘要 **把这两位卷了进去**。park 时那一层记的是什么,赎回腿就必须供
132
+ * 什么——供多了、供少了都是 `resume.parent_constraint_mismatch` 响亮拒(pre-CAS,checkpoint 留
133
+ * pending)。**不许**为了让摘要对上而回读 checkpoint 里记的那两位再原样回声:那等于把「重算出同一条链」
134
+ * 换成「抄一份自证」,摘要的防篡改语义当场归零。
135
+ *
136
+ * core 的原式(`prepare-task.js` 的 `inheritedGateForChildren`):
137
+ * ```
138
+ * durableMandate = runtimeCaps.forceDurableGate || (spec.durableApproval !== undefined && !isLiveApproverSeat(spec.onAsk ?? deps.onAsk))
139
+ * contentMandate = runtimeCaps.forceDurableGate || (spec.durableApproval !== undefined && !isLiveQuestionFace(spec.onQuestion ?? deps.onQuestion))
140
+ * ```
141
+ * 本仓三项对位:
142
+ * · `spec.durableApproval !== undefined` —— 本工厂**只在** `config.durableApproval` 为真时构造
143
+ * (main.ts 的构造条件逐字),故这一项在本函数射程内恒成立;
144
+ * · 审批席 = `spec.onAsk ?? deps.onAsk` 的**在场性**。本仓两个装配点(deps 级
145
+ * `createRunnerDepsOnAsk`、per-task 级 `routes/tasks.ts` 的 `boundAsk`)受**同一个**条件闸
146
+ * ——`toolApproval` 协调器在不在场——所以这个布尔在两位上恒等,`approverSeat` 在不在场即判据。
147
+ * ⚠️ 判据是**在场性**不是同一只闭包:身份不同不影响摘要(全文见 {@link ParkedReviveGateDeps.approverSeat});
148
+ * · 问答席 = resolve-spec 的 `liveQuestionFace`:在场 ⇒ spec 不 stamp sentinel、`deps.onQuestion` 的
149
+ * 协调器生效(`isLiveQuestionFace` 判 true);缺席 ⇒ spec stamp `QUESTION_AWAITS_RESUME`
150
+ * (该 sentinel 被 `isLiveQuestionFace` 判 false)。两支都归结为 `question` 在不在场。
151
+ *
152
+ * ✅ **原「已披露残余」销账(A-010.1 同车)**:`runtimeCaps.forceDurableGate`(center 逐 principal 的
153
+ * 授权位)此前写着「在本装配点解不出来——resolver 是 async 而重供席位的形是同步 `(row) => gate`」。那条
154
+ * 阻塞是**本文件自设**的,不是结构事实:唯一的消费点(`parked-decide.ts` 的 `decideParkedAgent`)本来
155
+ * 就在 async 里。席位随本件改成 async 之后,该位与 `autoModeArmed` 走同一条 caps 现解腿一并补齐 —— 否则
156
+ * 「该位为真 + 部署装了活体席」这个组合上 park 记 true 而本腿供 false,摘要恒不匹配、这批 principal 的
157
+ * parked 卡同样永不可赎(与 A-010.1 同族同因的第二个总失效面)。
158
+ *
159
+ * ⚠️ 时点语义与 toolPolicy 轴同族:两位都按**当前** entitlement 现解,挂起期间被撤/被授 ⇒ 摘要对不上
160
+ * ⇒ core pre-CAS 响亮拒(带 `parked_resume.startup_failed` 码可观测),绝不静默按新姿势跑。
161
+ */
162
+ async function chainMetadataOf(deps, row) {
163
+ const caps = await resolveCapsFor(deps, row);
164
+ const forced = caps?.forceDurableGate === true;
165
+ return {
166
+ ...(forced || deps.approverSeat === undefined ? { durableMandate: true } : {}),
167
+ ...(forced || deps.question === undefined ? { contentMandate: true } : {}),
168
+ ...autoModePostureOf(deps, row, caps),
169
+ };
170
+ }
171
+ /**
172
+ * 本行 principal 的 entitlement 现解(三位元数据共用**一次**)。
173
+ *
174
+ * principal 从哪来:`row.scope`。承重的不是「两个写入方碰巧写了同一个字符串」,而是**取数结构**:
175
+ * `findParkedAgentForCheckpoint` 走的是 `agentStore.listByScope(cp.scope, …)` 这条**按 scope 分区**的
176
+ * 查询(parked-decide.ts),所以任何能走到本函数的行都满足 `row.scope === cp.scope` —— 这是构造式的,
177
+ * 不是巧合。而 `cp.scope` 由 `boot/resolve-spec.ts` 写成 `encodeCheckpointScope(auth.principal)`
178
+ * (legacy resume 腿反向读回 `principal: decodeCheckpointScope(cp.scope)` 是同一等式的另一半),
179
+ * 故 `decodeCheckpointScope(row.scope)` 就是那条 run 的 `spec.principal`,与 core 喂给
180
+ * `runtimeCapsResolver(spec.principal)` 的**同一个**值 —— 摘要两侧因此解到同一份 caps。
181
+ *
182
+ * ⚠️ 别把 `row.scope` 当成「和 `cp.scope` 同一个写入方铸的」:**不是**。写行那一侧(core `subagent.js`
183
+ * 的 `ctx.principal ?? opts.background.scope`,本仓 `capabilities/scenarios.ts` 填字面 `"default"`)用的是
184
+ * `principal ?? "default"` 这套约定,与 `encodeCheckpointScope` 的 `"_"` 哨兵**不同源**。有 principal 时
185
+ * 两套折出同一个字符串(encode 只在 null/undefined 时才替换),所以上面那条等式成立;**匿名** run 上两者
186
+ * 分别是 `"default"` 与 `"_"`,分区查询直接落空 ⇒ 本函数根本到不了(decide 退回 legacy resume 腿,
187
+ * parked-decide.ts 自述的「未命中=零回归」形)。谁哪天去弥合那个匿名未命中,必须**连本函数一起**重看:
188
+ * 那时 `decodeCheckpointScope("default")` 会解出字符串 `"default"` 并把它当租户名递进 resolver。
189
+ *
190
+ * resolver 缺席(无 center / dry-run)⇒ core 侧 `runtimeCaps` 也恒 undefined ⇒ 这两位在 park 时也从不
191
+ * 由 caps 置位 ⇒ 逐字零行为差。resolver 抛错(契约上它自己 fail-closed 且不抛)⇒ 当作没解出来:元数据
192
+ * 位按「无 caps」算 ⇒ 与 park 记的对不上就响亮拒,**绝不**静默按更松的姿势赎回。
193
+ */
194
+ async function resolveCapsFor(deps, row) {
195
+ if (deps.resolveRuntimeCaps === undefined)
196
+ return undefined;
197
+ try {
198
+ return await deps.resolveRuntimeCaps(decodeCheckpointScope(row.scope));
199
+ }
200
+ catch (err) {
201
+ // 留痕是必需的:否则「这批卡为什么突然赎不动」查无痕迹。
202
+ deps.logger.warn("parked_revive_caps_unresolved", { handle: row.handle, err: err instanceof Error ? err.message : String(err) });
203
+ return undefined;
204
+ }
205
+ }
206
+ /**
207
+ * A-010.1 —— 重建条目的**第三位**决议链元数据 `autoModeArmed`(core 5.22.0 F-012 与两位 mandate 同车)。
208
+ *
209
+ * ## 病(本函数存在的唯一理由)
210
+ * core 的摘要口(`tool-policy.js` `constraintChainEntryOf`)把**三**位卷进 `cpv1:` 摘要,而判据是
211
+ * 「条目上有没有 `autoMode` 这个键」:park 时 `prepare-task.js` 的 `inheritedGateForChildren` 写
212
+ * `...(autoModeDecider !== undefined ? { autoMode: { decider } } : {})`,赎回时 `runtask.js` 的 pre-CAS
213
+ * 门按 `pc.autoMode !== undefined` 逐字重算。本腿此前只供 policy/onAsk/两位 mandate,**从不供** autoMode
214
+ * ⇒ auto-mode 权益 principal 的每一条跨副本 parked 赎回摘要恒不匹配、`resume.parent_constraint_mismatch`
215
+ * 响亮拒、卡永不可赎(方向 fail-safe,但功能面对这批 principal 是整条失效)。
216
+ *
217
+ * ## core 的原式(逐字对位,不绕过)
218
+ * ```
219
+ * if (runtimeCaps?.autoMode === true && deps.autoMode !== undefined) autoModeDecider = createAutoModeDecider(…)
220
+ * ```
221
+ * 两个半场本仓分别对位:
222
+ * · `deps.autoMode !== undefined` ⇒ {@link ParkedReviveGateDeps.autoModeSeatMounted}(装配面恒挂,见
223
+ * `boot/runner-deps.ts` 的 `autoMode:{onBreakerOpen}`——**信任门**,单挂它武装不了任何东西);
224
+ * · `runtimeCaps.autoMode === true` ⇒ {@link resolveCapsFor} 按**本行的 principal** 现解(时点语义、
225
+ * principal 从哪来、resolver 缺席/抛错怎么判,全在那只函数的头注)。
226
+ *
227
+ * ## ⛔ 已披露残余:decider 的**身份**跨不了进程(P-DEBT,不是等价替换)
228
+ * 摘要只看键在不在,但 core 在赎回后的每一次继承 ask 上会**真的调** `pc.autoMode.decider`
229
+ * (`prepare-task.js` 的继承包装器,顺序:分类器 → 冻结审批席)。而那只 decider 是祖先任务上的活闭包
230
+ * (绑着它自己的 session 转写窗 + brain),跨副本重建不出来 —— 与本文件 `approverSeat` 那格「诚实的降级」
231
+ * 同族。本腿交的是 **core 自己的构造器**铸的一只 decider,其 classify 腿如实拒答 ⇒ core 收到
232
+ * `unavailable`(它自己 `.catch(() => ({kind:"unavailable"}))` 的同一形)⇒ **不产生任何自动裁决**,原样
233
+ * 落到祖先冻结审批席那条链上(本腿的席位又是无 ALS 的降级形 ⇒ `approverUnavailable` ⇒ 再 park 给人)。
234
+ * 方向:分类器本会 `allow` 的调用改成问人(**更严**);本会 `block` 的也改成问人(**不是自动放行**,但
235
+ * 确实比自动拒松一档)。所以它按 P-DEBT 记债而不是当合法兜底 —— tag
236
+ * `server.parked-revive.ancestor-classifier-unreachable`,逐次计数 + 一次性 warn,收口件二选一:core 把
237
+ * 分类器判据持久进链条目,或让祖先 decider 有可跨进程重建的形。
238
+ */
239
+ function autoModePostureOf(deps, row, caps) {
240
+ if (deps.autoModeSeatMounted !== true || caps?.autoMode !== true)
241
+ return {};
242
+ return { autoMode: { decider: createUnreachableAncestorClassifier(deps, row) } };
243
+ }
244
+ /**
245
+ * 祖先分类器的**跨进程降级形**(全部理由在 {@link autoModePostureOf} 的残余段)。
246
+ *
247
+ * 内核用 core 自己的 `createAutoModeDecider` 铸 —— 超时/熔断/verdict 词表(`unavailable` 这个词本身)
248
+ * 都归 core 单一属主,本仓只提供一条**如实拒答**的 classify 腿,绝不在此手抄 `{kind:"unavailable"}`。
249
+ * `failureThreshold: 1` 让熔断一拍即开:第一次被咨询就把一次性告警喊出来,之后每次 `decide` 在 core 内
250
+ * 早返 `unavailable`(不再进 classify、不再起 15s 定时器)。
251
+ *
252
+ * 🔴 计数**必须**打在外层 `decide` 上、不能打在 `classify` 里:熔断开了以后 core 早返、classify 再也不
253
+ * 被调用,计数打在里面就永远只记 1 —— 而这个 tag 的语义(和普查表里那一行的文字)是「**丢了祖先分类器
254
+ * 判决的继承 ask 次数**」。打在里面等于让遥测读数与它自称的含义不是一回事,债就在账上消失了。
255
+ */
256
+ function createUnreachableAncestorClassifier(deps, row) {
257
+ const inner = createAutoModeDecider({
258
+ failureThreshold: 1,
259
+ classify: async () => {
260
+ throw new Error("the ancestor task's auto-mode classifier is a live closure over that task's own transcript/brain — it cannot be " +
261
+ "reconstructed on a fresh replica, so this inherited layer has no classifier verdict to give (the call falls " +
262
+ "through to the ancestor's frozen approver chain)");
263
+ },
264
+ onBreakerOpen: () => deps.logger.warn("parked_revive_ancestor_classifier_unreachable", {
265
+ handle: row.handle,
266
+ sessionId: row.sessionId,
267
+ note: "an inherited ancestor layer was auto-mode armed at park time; its classifier does not survive the process boundary — inherited asks resolve on the approver chain (durable park) instead of a classifier verdict",
268
+ }),
269
+ });
270
+ return {
271
+ breakerOpen: () => inner.breakerOpen(),
272
+ decide: async (input, signal) => {
273
+ recordFailOpen("server.parked-revive.ancestor-classifier-unreachable", row.handle);
274
+ return inner.decide(input, signal);
275
+ },
276
+ };
277
+ }
109
278
  /** host 腿的裁决 env(真 fs;构造是纯字段赋值,不 spawn)。**每次赎回现铸**——与「禁 memoize」同因:
110
279
  * cwd 占位读的是活 config 的 `localDataRoot`。 */
111
280
  function createHostAdjudicationEnv(config, localRoot) {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * #203 §3(design/203 v2 §6 F4 的**残余采纳**)—— 默认 ON 的**休眠行审计**。
3
+ *
4
+ * 🔴 这条审计存在的唯一理由:`PERMISSION_RULES_ENABLED` 的默认值在本车从 OFF 翻成 ON,而**旋钮翻转
5
+ * 与规则行的寿命是两条独立的时间线**。三张表自 #154 车二起就随中央 `ensureSchema` 建、并且从来没有被
6
+ * 清过,所以下面这两类部署在升级的那一刻会突然带着**既有**规则行开始放行:
7
+ * · 曾经把旋钮开过一段时间又关掉的部署(行留在库里,只是没人读);
8
+ * · 从旧库恢复/克隆出来的部署(行跟着数据一起来的)。
9
+ * 对运维来说,这是「我什么都没改,worker 却开始少问我了」——一次**放宽面**上的静默变化。审计做的事
10
+ * 很小但是承重:在启动日志里把它说出来,让「少问」是一条能被读到的因果,而不是一次玄学。
11
+ *
12
+ * ── 三条判据,逐条有理由 ────────────────────────────────────────────────────────────────────────
13
+ * ① **店缺席 ⇒ 整条不上场**(零 I/O、零行)。店缺席包括:显式关旋钮、`local` 车道没接 File 店的旧
14
+ * 部署、以及压根没有 backend 的 env-only worker。没有店就没有行,谈不上唤醒。
15
+ * ② **零桶 ⇒ 不打行**。一条「0 条既有规则随默认 ON 激活」的行是纯噪音:它对读日志的人不传递任何
16
+ * 可行动的信息,却让真正有东西被唤醒的那一行变得不显眼。
17
+ * ③ **运维**显式**表过态(`PERMISSION_RULES_ENABLED` 被设成 true 或 false)⇒ 不打行**。那些行本来
18
+ * 就是活的(或者旋钮被显式关着、店根本没装配),默认值翻不翻转对它们零影响 —— 说「随默认 ON
19
+ * 激活」是**假话**,而在启动日志里说假话比不说更坏。判据用 `permissionRulesEnabledExplicit`
20
+ * (config.ts 与 `dbBackendExplicit` 同姿势),不是猜。
21
+ *
22
+ * ── 失败方向 ────────────────────────────────────────────────────────────────────────────────────
23
+ * 数不出来(库抖动 / 文件读不了)⇒ **不拒启、也不编一个 0 出来**:返回 `undefined` + 一条 warn。
24
+ * 这是一条纯诊断面(它不参与任何放行/拒绝判决),所以它的降级方向是「安静地不知道」而不是「谎报
25
+ * 零」——`{ buckets: 0 }` 会被下游读成「确认没有休眠行」,那是把一次读失败伪装成一个结论。
26
+ * 因为它零判决,这条 fail-open 不属于 `docs/FAIL-OPEN-CENSUS.md` 的安全轴 P 类,故不占 `recordFailOpen`
27
+ * 的标签;它的可见性由那条 warn 承担。
28
+ */
29
+ /** 审计只需要一只桶计数口——**刻意**不吃整个 `PermissionRuleStoreBundle`:诊断面对店的其余能力
30
+ * 一无所知也不该知道,窄口让测试能用四行 fixture 驱动真实现同一段代码。 */
31
+ export interface DormantRuleCounter {
32
+ /** 库里已存在的规则**桶**(一只 owner 一只桶)数量。 */
33
+ countBuckets(): Promise<number>;
34
+ }
35
+ export interface DormantRuleAuditCtx {
36
+ /** 已装配的规则店束;`undefined` = 本部署没有规则店(判据 ①)。 */
37
+ stores: DormantRuleCounter | undefined;
38
+ logger: {
39
+ info(msg: string, meta?: unknown): void;
40
+ warn?(msg: string, meta?: unknown): void;
41
+ };
42
+ /** 运维是否**显式**设过 `PERMISSION_RULES_ENABLED`(判据 ③)。 */
43
+ explicit: boolean;
44
+ }
45
+ /** boot 期一次性调用。返回读数(`undefined` = 没店 / 数不出来),绝不抛。 */
46
+ export declare function auditDormantPermissionRules(ctx: DormantRuleAuditCtx): Promise<{
47
+ buckets: number;
48
+ } | undefined>;
49
+ //# sourceMappingURL=permission-rules-audit.d.ts.map