@sema-agent/server 7.12.0 → 7.14.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 (74) hide show
  1. package/USAGE.md +51 -2
  2. package/dist/adoption/plan.d.ts +38 -4
  3. package/dist/adoption/plan.js +72 -0
  4. package/dist/adoption/quiesce.d.ts +70 -0
  5. package/dist/adoption/quiesce.js +148 -0
  6. package/dist/adoption/runner.js +63 -5
  7. package/dist/adoption/sql.d.ts +15 -0
  8. package/dist/adoption/sql.js +18 -0
  9. package/dist/adoption/wire.d.ts +7 -1
  10. package/dist/adoption/wire.js +6 -0
  11. package/dist/approval-card.d.ts +5 -0
  12. package/dist/approval-card.js +22 -0
  13. package/dist/boot/coordinators.js +2 -1
  14. package/dist/boot/memory-boundary.d.ts +84 -0
  15. package/dist/boot/memory-boundary.js +110 -0
  16. package/dist/boot/permission-rules-audit.js +29 -1
  17. package/dist/boot/reapers.d.ts +15 -0
  18. package/dist/boot/reapers.js +101 -44
  19. package/dist/boot/resolve-spec.js +31 -0
  20. package/dist/boot/runner-deps.d.ts +16 -2
  21. package/dist/boot/runner-deps.js +24 -4
  22. package/dist/boot/stores.js +93 -3
  23. package/dist/capabilities/memory-notice.d.ts +83 -0
  24. package/dist/capabilities/memory-notice.js +90 -0
  25. package/dist/config-types.d.ts +100 -11
  26. package/dist/config.d.ts +1 -1
  27. package/dist/config.js +111 -1
  28. package/dist/governance-ask-marks.js +2 -1
  29. package/dist/http/active-run-conflict.d.ts +33 -8
  30. package/dist/http/active-run-conflict.js +37 -2
  31. package/dist/http/routes/adoption.js +25 -2
  32. package/dist/http/routes/approvals-assistant.js +33 -3
  33. package/dist/http/routes/capabilities.js +35 -5
  34. package/dist/http/routes/images.js +18 -0
  35. package/dist/http/routes/runs.js +21 -5
  36. package/dist/http/routes/tasks.js +18 -6
  37. package/dist/http/routes/trace-usage.js +43 -14
  38. package/dist/http/server.d.ts +35 -9
  39. package/dist/http/server.js +111 -17
  40. package/dist/http/wire-types.d.ts +6 -1
  41. package/dist/main.js +46 -6
  42. package/dist/observability/fail-open.d.ts +4 -0
  43. package/dist/observability/fail-open.js +4 -0
  44. package/dist/plugins/adoption-log-sql.d.ts +40 -0
  45. package/dist/plugins/adoption-log-sql.js +69 -2
  46. package/dist/plugins/approval-ask-store-sql.d.ts +2 -1
  47. package/dist/plugins/approval-ask-store-sql.js +2 -1
  48. package/dist/plugins/file-run-store.d.ts +85 -1
  49. package/dist/plugins/file-run-store.js +450 -17
  50. package/dist/plugins/memory-embedder.d.ts +44 -0
  51. package/dist/plugins/memory-embedder.js +173 -0
  52. package/dist/plugins/permission-rule-store-sql.d.ts +45 -0
  53. package/dist/plugins/permission-rule-store-sql.js +60 -2
  54. package/dist/plugins/shared-memory-store-sql.d.ts +23 -9
  55. package/dist/plugins/shared-memory-store-sql.js +55 -18
  56. package/dist/plugins/sql-driver.d.ts +19 -0
  57. package/dist/plugins/sql-driver.js +12 -0
  58. package/dist/plugins/store-backend.d.ts +3 -1
  59. package/dist/plugins/store-backend.js +24 -1
  60. package/dist/plugins/tidb-pool.js +11 -4
  61. package/dist/plugins/tool-result-store-sql.d.ts +35 -2
  62. package/dist/plugins/tool-result-store-sql.js +127 -11
  63. package/dist/plugins/web-search.d.ts +3 -1
  64. package/dist/plugins/web-search.js +3 -1
  65. package/dist/rules-consent.d.ts +33 -4
  66. package/dist/rules-consent.js +43 -2
  67. package/dist/run-local.js +6 -2
  68. package/dist/runtime-governance.d.ts +33 -0
  69. package/dist/runtime-governance.js +32 -0
  70. package/dist/security.js +3 -1
  71. package/dist/tool-approval.d.ts +32 -0
  72. package/dist/tool-approval.js +39 -0
  73. package/dist/trace/core-keyset-guard.d.ts +1 -1
  74. package/package.json +3 -3
@@ -13,6 +13,7 @@
13
13
  * (`gate_not_tool_approval`/`gate_not_resumable`/`wake.gate_pending` 的指路句)。三处文案与本表若漂移,
14
14
  * test/session-active-conflict-materials.test.ts 的分门用例会红。认不出的 kind ⇒ null(诚实缺席,不铸假门)。
15
15
  */
16
+ import { governanceMandatesShellGateAlways } from "../runtime-governance.js";
16
17
  /** 与 runs.ts/tasks.ts 三个 409 位共享的旧文案(byte-frozen:api-error-text-freeze 门认这句)。 */
17
18
  export const ACTIVE_RUN_CONFLICT_BASE_TEXT = "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)";
18
19
  /** gate.kind → 它的那一个 resume 入口(sessionId/taskId 寻址,无秘密)。 */
@@ -39,6 +40,37 @@ export function resumeEntryForGate(kind, ids) {
39
40
  return null; // 未知门型:宁缺毋假 —— 客户端仍有 activeTaskStatus + cancel 这条保底真路
40
41
  }
41
42
  }
43
+ /**
44
+ * #220:推「这道门是运维治理层下的吗」。判据是**合取**,两半各管一件事:
45
+ *
46
+ * 行上的取证格 `gate.riskDescriptor.shellGateDoctrine === "always"`
47
+ * ∧ 本部署的治理层本来就要求 always(`governanceMandatesShellGateAlways`,单一判据属主在
48
+ * `runtime-governance.ts`,那里写着为什么单看行不够 —— SUP 路由姿态同样产 `"always"`)
49
+ *
50
+ * 第一半:core 在 mint 这道门时把「当时活着的 doctrine」如实写进 `riskDescriptor.shellGateDoctrine`
51
+ * (2026-08-05 取证裁定;`prepare-task` 只在**被 shell 门铸**的 ask 上写它),所以持久行自己带着证据 ——
52
+ * park 腿天然跨副本、跨重启,活卡腿那张 ALS 标记表在这里结构上够不着。
53
+ * 第二半:把「有效档 always」收窄到「治理层是真成因」,否则一条 SUP 路由出来的门会被谎报成治理强制。
54
+ *
55
+ * ⚠️ 残留(双向失真的窄在场向 + 根治路径)登记在 `governanceMandatesShellGateAlways` 的顶注,别在这里
56
+ * 复述;本位只承担「分诊提示」的分量,不参与任何门/CAS/resume 判定。
57
+ *
58
+ * 🔴 `"classify"` 档**刻意不标**:那一档由 core 的分类器逐调用裁决,一次 shell ask 可能出自分类器(治理)
59
+ * 也可能出自 `APPROVAL_REQUIRE` 这类别的门,行上无从分辨 —— 与活卡腿同一条「宁缺毋假」纪律。
60
+ *
61
+ * ⚠️ **askId 为什么不在这里**(#220 的另一半,如实记账):待批 ask 的 `askId` 只活在 `approval_ask` 行上
62
+ * (checkpoint 行没有这一列,core 的 `CheckpointGate`/`PendingAction`/`Checkpoint` 顶层都不带它)。反向
63
+ * (checkpoint → ask)今天**没有读口**:两条 list 读口都硬过滤 `state='STREAM_PENDING'`,而 park 完的行是
64
+ * `PARKED`;唯一精确的连接键是 ask 行上的 `gate_token`,要按它反查得给店加一个新读口(SQL 面 = 双库集成
65
+ * 门)。派生 `deriveAskId(...)` 也不行:它吃 `legKey`(= 上一腿 resume token 的摘要)与 `parentToolCallId`,
66
+ * 这两维从 checkpoint 行推不出来,二腿/子代形上会算出一个**错**的 id —— 在「等的谁」这条通道上,错 id
67
+ * 比缺席坏得多(本文件 `resumeEntryForGate` 的 default 臂是同一条纪律)。故本批诚实缺席。
68
+ */
69
+ function governanceOriginOf(gate, governance) {
70
+ if (gate?.riskDescriptor?.shellGateDoctrine !== "always")
71
+ return undefined;
72
+ return governanceMandatesShellGateAlways(governance ?? {}) ? true : undefined;
73
+ }
42
74
  export function toDoneFrameResult(body) {
43
75
  return {
44
76
  status: "failed",
@@ -75,10 +107,13 @@ export async function buildActiveRunConflict(deps, sessionId, activeTaskId) {
75
107
  if (cs?.findPendingTokenBySession) {
76
108
  const scope = (await cs.peekPendingScope?.(sessionId)) ?? undefined;
77
109
  const token = scope === null ? null : await cs.findPendingTokenBySession(sessionId, scope);
78
- const kind = token ? (await cs.get?.(token))?.gate?.kind : undefined;
110
+ const gate = token ? (await cs.get?.(token))?.gate : undefined;
111
+ const kind = gate?.kind;
79
112
  const decidePath = kind ? resumeEntryForGate(kind, { sessionId, taskId: activeTaskId }) : null;
113
+ // #220:出身格与出路格**同一次读**里取(不为它多打一次库),`true` 才写键(见 PendingGateMaterial 顶注)。
114
+ const governanceForced = governanceOriginOf(gate, deps.governance);
80
115
  if (kind && decidePath)
81
- pendingGate = { kind, decidePath };
116
+ pendingGate = { kind, decidePath, ...(governanceForced ? { governanceForced } : {}) };
82
117
  }
83
118
  return {
84
119
  ...base,
@@ -1,6 +1,6 @@
1
1
  import { createAdoptionRunner } from "../../adoption/runner.js";
2
2
  import { AdoptionRequestSchema } from "../../adoption/wire.js";
3
- import { checkAdoptionWidths } from "../../adoption/plan.js";
3
+ import { checkAdoptionPrincipalShape, checkAdoptionWidths } from "../../adoption/plan.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
5
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
6
6
  const ADOPTION_ID_RE = /^\/v1\/adoption\/([^/]+)$/;
@@ -21,6 +21,11 @@ function sendOutcome(res, out) {
21
21
  return;
22
22
  // 闭集穷举:`AdoptionRejectCode` 加一个成员而这里没加臂 ⇒ **编译红**(拒绝码是 wire 契约,
23
23
  // 不许有一个走到默认臂的静默码)。
24
+ //
25
+ // 🔴 A-010.19(验真后修):上面这句话此前是**一句自我声明,不是一道门**。内层 switch 只有两条 case
26
+ // 而结尾是一个裸 `return;` —— 加第三个拒绝码时 TS 一个字都不会说,请求会走到那个裸 return、
27
+ // **一个字节都不写**就把响应扔在那里:调用方看到的是一条挂住的连接直到超时,而不是任何错误。
28
+ // 现在由下面的 `never` 断言真正兑现它:少一条臂 ⇒ `out` 收窄不到 `never` ⇒ 赋值失败 ⇒ 编译红。
24
29
  case "rejected":
25
30
  switch (out.code) {
26
31
  case "adoption.destination_conflict":
@@ -37,9 +42,19 @@ function sendOutcome(res, out) {
37
42
  });
38
43
  return;
39
44
  }
40
- return;
45
+ // 穷举证明:走到这里 `out.code` 必须已经收窄成 `never`。加一个 `AdoptionRejectCode` 成员而不加臂
46
+ // ⇒ 这一行编译红,红在**加码的那一刻**,而不是在生产上表现为一条永不应答的请求。
47
+ return assertNoRejectCodeLeft(out.code);
41
48
  }
42
49
  }
50
+ /** 闭集穷举的落点。**入参类型是 `never`** —— 这就是那道门本身;函数体永远跑不到,写一句响亮抛是为了
51
+ * 「万一有人用 `as` 把一个未知码塞进来」时仍然 fail-loud(不是静默挂住连接)。
52
+ * ⚠️ 形参**刻意不叫 `code`**:`error-code-key-gate` 按 `code:` 的字面形扫全树错误体(3.0.0 起
53
+ * `errorCode` 是唯一机器判别键),一个叫 `code` 的形参会被它当成一处 legacy 键站点。名字换掉比放宽
54
+ * 那道门便宜得多 —— 门宽一分,真站点就可能溜过去。 */
55
+ function assertNoRejectCodeLeft(unhandled) {
56
+ throw new Error(`adoption: unhandled reject code ${JSON.stringify(unhandled)} — the wire contract's closed set and this switch have drifted apart`);
57
+ }
43
58
  export async function handleAdoption(req, res, url, ctx) {
44
59
  const miss = { fell: false };
45
60
  await handleAdoptionBody(req, res, url, ctx, miss);
@@ -101,6 +116,14 @@ async function handleAdoptionBody(req, res, url, ctx, miss) {
101
116
  return;
102
117
  }
103
118
  const { fromPrincipal, toPrincipal } = parsed.data;
119
+ // 🔴 键空间卫生在**列宽之前**(A-010.15):带首尾空白的身份在两方言上的唯一键语义不等价
120
+ // (MySQL utf8mb4_bin 是 PAD SPACE),而它在本仓根本不是一个可能存在的真身份 —— 详见该函数头注。
121
+ // 排在列宽前面的理由与列宽自己那条次序判据同族:两条都能命中时,报**更根本**的那一个。
122
+ const badShape = checkAdoptionPrincipalShape(fromPrincipal, toPrincipal);
123
+ if (badShape !== undefined) {
124
+ sendError(res, 400, "request.field_invalid", "the adoption principals are not in a portable identity shape", { detail: badShape });
125
+ return;
126
+ }
104
127
  // 🔴 列宽校验在**铸行之前**(codex R1-F3):合法长度的 principal 也可能装不进最窄的那根被写列,
105
128
  // 而那种失败若发生在腿的中途,phase 会永远停在 INTENT —— 每次重发和每次 boot 扫描都再撞一次同样的墙。
106
129
  const tooWide = checkAdoptionWidths(fromPrincipal, toPrincipal);
@@ -1,7 +1,7 @@
1
1
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
2
2
  import { verifyDirectDoorProof } from "../../principal-jwt.js";
3
3
  import { MAX_APPROVAL_REASON_CHARS } from "../../approval-hmac.js";
4
- import { redactedPreview } from "../../trace/redact.js";
4
+ import { redactedPreview, redactSecrets } from "../../trace/redact.js";
5
5
  import { fleetRunLabels } from "../../fleet/fleet-bus.js"; // [2069]④ §3 行展示名与 fleet 行同源(见用处的注)
6
6
  import { sleep } from "../sse-log.js";
7
7
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
@@ -68,7 +68,9 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
68
68
  const scope = operator
69
69
  ? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined) // operator: all (or ?owner)
70
70
  : (principal ?? "__none__"); // non-operator: only its own scope (never others' pending)
71
- sendJson(res, 200, { pending: await cs.listPending(scope) });
71
+ // #209 件4:行整只上 wire(键集契约不变),但 `riskDescriptor.shadowedRule` 这一格先脱敏 ——
72
+ // 理由与 `/v1/approvals/stream` 共用同一个投影函数,见 `redactPendingDisclosures` 顶注。
73
+ sendJson(res, 200, { pending: (await cs.listPending(scope)).map(redactPendingDisclosures) });
72
74
  return;
73
75
  }
74
76
  // exemptions surface — the UI's "本会话不再询问" state (list) + revoke. Same authz shape as
@@ -659,6 +661,32 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
659
661
  * connection runs its OWN poll loop — fine for a handful of operators; a shared fan-out poll is a future
660
662
  * optimization if the operator count grows.) */
661
663
  const APPROVALS_STREAM_POLL_MS = 3000;
664
+ /**
665
+ * 🔴 #209 件4(codex 对抗复审 R3-[high],验真后修)—— **durable 读面的脱敏投影**。
666
+ *
667
+ * core 5.25.0 的 `RiskDescriptor.shadowedRule` = 被越级的那条持久规则的**原文**。core 侧只过
668
+ * `inlineUntrusted`(中和 + 限长),**不**做秘密脱敏 —— 它的 JSDoc 也只承诺中和,不承诺无秘密
669
+ * (与同结构里的 `summary` 不同:那一格 core 明写「NEVER raw secrets / full args / env」)。
670
+ * 而规则原文是**人手写的文本**,一条 `Bash(curl -H "Authorization: Bearer …")` 形的规则完全拼得出来。
671
+ *
672
+ * 同步路(`tool_approval` 帧 / `card_json`)在铸点就 `redactSecrets` 了;durable 路两条读面
673
+ * (`GET /v1/approvals` 与 `/v1/approvals/stream`)此前把 `listPending()` 的行**整只**上 wire ⇒
674
+ * 同一份内容在两条腿上一条脱敏、一条不脱敏,而 operator 的队列是**跨租户**可见的那一条。
675
+ *
676
+ * 修在**读面**而不是落库点,理由是本仓成文的分工(同文件 inbox 段那条 `redactedPreview(s.toolInput)` 的
677
+ * 逐字理由):「core bounds but does NOT redact it — redaction is the consumer's job」。落库点改写会与
678
+ * core 的属主面打架,而且救不了**别的副本**已经写下的行。
679
+ *
680
+ * 🔴 只动这一格,不顺手洗整只 descriptor:`summary` 有 core 的无秘密承诺、`touchedPaths` 是路径,
681
+ * 两者的现行为不在本批辖域内(要改得走各自的三问)。键集**不变** —— 「整只透传、不按键投影」那条契约
682
+ * (`test/approval-shadowed-rule-wire.test.ts` §3)照旧成立,变的只是这一格的**内容**。
683
+ */
684
+ function redactPendingDisclosures(row) {
685
+ const rd = row.riskDescriptor;
686
+ if (rd === null || rd === undefined || typeof rd.shadowedRule !== "string")
687
+ return row;
688
+ return { ...row, riskDescriptor: { ...rd, shadowedRule: redactSecrets(rd.shadowedRule) } };
689
+ }
662
690
  /**
663
691
  * GET /v1/approvals/stream (design/80 native push): SSE — pushes pending-approval deltas so the portal
664
692
  * SUBSCRIBES ONCE instead of polling GET /v1/approvals every ~10s (better UX: near-real-time + no client poll
@@ -696,7 +724,9 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
696
724
  while (!closed) {
697
725
  let pending;
698
726
  try {
699
- pending = await cs.listPending(scope);
727
+ // #209 件4:与 `GET /v1/approvals` **同一个**投影函数 —— 两条 durable 读面各洗各的就会漂
728
+ // (SSE 那条恰恰是 operator 常驻订阅的那条,漏掉它等于没修)。
729
+ pending = (await cs.listPending(scope)).map(redactPendingDisclosures);
700
730
  }
701
731
  catch {
702
732
  // a transient TiDB blip must NOT kill the subscription — heartbeat + retry next tick (fail-soft)
@@ -3,6 +3,7 @@ import { cwdHonored } from "../../task-cwd.js";
3
3
  import { mcpInjectionHonored } from "../../task-mcp.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
5
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
6
+ import { gatedPrincipal } from "../principal-gate.js";
6
7
  export async function handleCapabilities(req, res, url, ctx) {
7
8
  const miss = { fell: false };
8
9
  await handleCapabilitiesBody(req, res, url, ctx, miss);
@@ -75,6 +76,14 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
75
76
  : false;
76
77
  })(),
77
78
  leader: Boolean(deps.leaderEndpoint),
79
+ // #F1([3397]-5 同族):sandbox-image-pool 的 P1 面(`GET /v1/images`、`/v1/images/:profile`、
80
+ // `/v1/images/digests/:digest`、`POST /v1/images/select`)。谓词**逐字**是 routes/images.ts 里那条
81
+ // 挂载合取式的 `deps.imageIndex` —— 缺席时该域整个不挂载,同文件的能力臂回 501
82
+ // `capability.image_index_required`(此前是与拼错路由同码的 404,消费端只能 trial-by-404)。
83
+ // local 后端刻意不实现 `imageIndex()`(池是 cloud/fleet-only),所以单机形上这一位诚实为 false。
84
+ // ⚠️ 辖域**只到 P1**:`/v1/images/bakes*`(P2 烤制控制面)由独立的 `imageBakes` 依赖挂载,
85
+ // 且是 operator/runner 内部面,不在本位的承诺里。
86
+ images: Boolean(deps.imageIndex),
78
87
  // design/138 S1 (clay 2026-07-08): the legacy MemoryStore surface (GET/DELETE /v1/memory + the MF-30
79
88
  // session write verbs) was DEPRECATED with the store itself — the new memory engine is model file skills
80
89
  // over the injected memory dir, with NO service HTTP verbs. Hard-coded false so a shell that still knows
@@ -103,7 +112,22 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
103
112
  // #154 车二 + #203 §2:规则车道(cc-import 两口 + 撤销面两口 + respond 的 persistRule 兑付口)。
104
113
  // 谓词与 `/v1/rules/*` 全族的 501 同源:ruleConsent lane 在场 ⟺ 总开关未被显式关 ∧ 规则店 wired
105
114
  // (SQL 双方言,或 #203 起 local 的 File 三面束)。说 yes ⟺ **四口都活**,不是「有一口活」。
106
- permissionRules: Boolean(deps.ruleConsent),
115
+ //
116
+ // 🔴 A-010.20(验真后修,补第二条合取项):店在场**不等于**这四口活。规则车道是
117
+ // **principal 寻址**的 —— `routes/rules.ts` 的域头逐字是 `if (principal === undefined) 401`
118
+ // (无条件,与 `requirePrincipal` 无关),而铸卡侧 `buildRuleLaneMaterial` 与回决侧
119
+ // `persistRuleAfterDecision` 都有同源的 `owner === null ⇒ 不进车道 / rule_lane_unavailable` 一门。
120
+ // 于是「装了店、但调用方没有可验证身份」的部署上,旧谓词报 true 而**四口全废**:两口恒 401、
121
+ // 卡上恒无候选、`persistRule` 恒被拒。#203 给 local 车道接上 File 店之后,「单机 + 无 principal」
122
+ // 第一次同时满足其余条件 —— 这不再是纸面形。
123
+ //
124
+ // 🔴 为什么这一位是**按调用方**算而不是按部署算:`requirePrincipal=false` 的部署上,带 principal
125
+ // 的调用方与不带的调用方**答案真的不同**(前者四口全活,后者全废),没有任何一个部署级布尔能同时
126
+ // 对两者不说谎。`/v1/capabilities` 是每请求读面且与 `/v1/rules` 走同一道全局凭证门,所以
127
+ // 「这一位说 yes」⟺「**你**拿同一份头去打那四口真的能用」——这正是本文件顶注承诺的那条等价。
128
+ // ⚠️ 与下面的 `permissionRulesRevoke` **刻意不同源**:那一位是**存在性/版本**信号(见其注),
129
+ // 必须与调用方身份无关,否则老版本探测会把「我没带 principal」误读成「这个 worker 太老」。
130
+ permissionRules: Boolean(deps.ruleConsent) && gatedPrincipal(req, deps.config) !== undefined,
107
131
  // #205 SDK 车上游请托二连(2026-08-10):
108
132
  // · `permissionRulesRevoke` —— 撤销面两口(GET/DELETE /v1/rules)**存在性**的独立信号。谓词与
109
133
  // `permissionRules` 同源,但**在场性**本身就是版本信号:≤7.11.0 的 worker `permissionRules` 已为
@@ -135,8 +159,11 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
135
159
  // served this task — the RESOLVED `Model.id`, not the requested `TaskSpec.model` ref … Lets a UI echo
136
160
  // 'served by X'". A grep for the identifier `effectiveModel` returns zero hits in core and that was mistaken
137
161
  // for "core doesn't have it" (search by SEMANTICS, not by identifier name). It already reaches the wire:
138
- // `POST /v1/tasks` returns the TaskResult verbatim (~L1852), the SDK declares `TaskResult.model`, and this
139
- // server already consumes it internally for cache-family accounting (~L1843). Pinned by
162
+ // `POST /v1/tasks` returns the TaskResult verbatim (`routes/tasks.ts` `sendSubmitResult` the sync submit
163
+ // leg's one response mouth; design/158 A9 moved the whole sync-submit surface OUT of `http/server.ts`, so the
164
+ // old `~L1852` anchor into that file is dead), the SDK declares `TaskResult.model`, and this server already
165
+ // consumes the resolved id internally for cache-family accounting (`budget.ts` `cacheFamilyOfMirror` + its
166
+ // caller in `routes/side-query.ts`, NOT `http/server.ts`). Pinned by
140
167
  // `test/effective-model-echo-wire.test.ts` (real HTTP: resolved id echoed, absent key stays absent) so a
141
168
  // future projection/whitelist on this leg can't silently drop it — the `toolEndEventData`-label bug class.
142
169
  // Nothing is advertised as a capability BIT here because an additive result field needs no gate
@@ -330,9 +357,12 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
330
357
  // `workflowsCapability(deps)` (hardened script runner ∧ governance), boot-computed in main.ts. Falls back
331
358
  // to the `Boolean(workflowRunStore)` proxy when the boot flag isn't wired (older callers / tests).
332
359
  // • `workflowsList` = can durably LIST workflow runs = `Boolean(workflowRunStore)` (GET /v1/workflows* 501s
333
- // without it — list AND detail/stream, see the store gate ~L731).
360
+ // without it — list AND detail/stream, see the store gate in `routes/workflows.ts`, the
361
+ // `if (!deps.workflowRunStore)` → 501 `capability.self_orchestration_required` arm).
334
362
  // ⚠️ In the CURRENT service wiring these two COINCIDE: `workflowRunStore` is built IFF self-orchestration is on
335
- // (InMemoryWorkflowRunStore when WORKFLOW_RUN_STORE=memory, else File — main.ts ~L527), the SAME gate that
363
+ // (InMemoryWorkflowRunStore when WORKFLOW_RUN_STORE=memory, else File — built in
364
+ // `boot/workflow-orchestration.ts`, which design/158 A10 split out of main.ts; main.ts itself no longer
365
+ // mentions InMemoryWorkflowRunStore at all, so the old `main.ts ~L527` anchor is dead), the SAME gate that
336
366
  // wires the safe script runner `workflowsCapability` checks → `Boolean(workflowRunStore)` ≡ `workflowsCapability`
337
367
  // always today. So this split is core's FORWARD-LOOKING orthogonal-axes CONTRACT, NOT a fix for a live
338
368
  // divergence (the earlier "memory backend → no store → divergence" framing was WRONG — InMemory IS a store;
@@ -490,6 +490,24 @@ async function handleImagesBody(req, res, url, ctx, miss) {
490
490
  return;
491
491
  }
492
492
  }
493
+ else if (url === "/v1/images" || url.startsWith("/v1/images/")) {
494
+ // 🔴 #F1([3397]-5 同族缺口):索引缺席时**整个域不挂载**,请求此前一路落到全局路由表尾的
495
+ // `404 not_found.route` —— 与「路由拼错了」同一个码,消费端结构上分不出「这个部署没接这个面」
496
+ // 与「我 URL 写错了」(trial-by-404 的最坏形:两种成因的处置完全相反)。改成诚实的能力拒。
497
+ //
498
+ // 谓词与上面那条挂载合取式**同一个符号**(`deps.imageIndex`),所以 `/v1/capabilities` 的 `images`
499
+ // 位(同一谓词)说 yes ⟺ 这条臂不触发 ⟺ 目录读面真能用。
500
+ //
501
+ // ⚠️ 判据是**按段**的(`=== "/v1/images"` 或 `"/v1/images/"` 前缀),不是上面挂载式那个裸前缀:
502
+ // `/v1/imagesfoo` 是**拼错的路由**,不是本域的路径,它该继续拿 404(索引在场的部署上它本来就落 404
503
+ // —— 两种部署形对同一个手滑给出不同的码,那正是本臂要消灭的那种含糊)。
504
+ //
505
+ // ⚠️ bake 子面**不误伤**:`deps.imageBakes` 在场时 `/v1/images/bakes*` 已在本函数最上面的域里应答
506
+ // 并 `return`,走不到这里;两者都缺席时(local 后端的真实形态——两个工厂它都不实现)本臂如实
507
+ // 回答「这个部署没有 sandbox-image-pool」,那对 bakes 路径也是真话。
508
+ sendError(res, 501, "capability.image_index_required", "the sandbox-image-pool requires a SQL store backend (DB_BACKEND=mysql|pg) — the local backend wires no image index");
509
+ return;
510
+ }
493
511
  miss.fell = true;
494
512
  }
495
513
  /** The image_bake SSE (P2.2/P2.8) — the SAME pump, with the run-trace-SSE-shaped bake frame
@@ -161,11 +161,27 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
161
161
  // 「开关关 = 逐字零变化」(§6.5/§7.2)当场破。谓词同时省掉了关闭态每次开流的一次 store 往返
162
162
  // 与那份 store 故障暴露面。钉:场景9(零帧 **且** 零 store 读 —— 只钉零帧会放过「读了但没投」)。
163
163
  preamble: async (signal) => {
164
- if (!deps.config.streamApproval.enabled)
164
+ // 🔴 A-010.7(验真后修):谓词是**上场门**,不是裸开关。
165
+ // 原式是 `if (!config.streamApproval.enabled) return []` + 一个 `backend?.approvalAsk()` 兜底,
166
+ // 那是本仓早就收编掉的「局部合取式」形(见本文件 askDecision 口的头注:车4 落地时的临时判据,
167
+ // 交接条件写死了「换成消费**那一个**谓词」)。两处不同源的后果在 `STREAM_APPROVAL` 默认翻 ON
168
+ // 之后才显形:上场门是**五**项合取(还要 toolApproval 在场、账本非易失 `kind !== "local"`、
169
+ // park 设施在场),而这里只看了两项 ⇒ 一个协议**没有上场**的 SQL 部署
170
+ // (典型:`TOOL_APPROVAL_ENABLED=false`,或没开 `DURABLE_APPROVAL` ⇒ 无 park 设施)
171
+ // **每一次开流都照打一次库** —— 一次纯白打的 `listPendingByTask`,外加那份 store 故障暴露面,
172
+ // 而它按契约永远只能投出零帧(协议没上场 ⇒ 没有任何一条 `STREAM_PENDING` 行是本协议铸的)。
173
+ // 换成单一谓词后,「能力面说 false」⟺「本腿零 store 往返」是结构成立的。
174
+ const gate = resolveStreamApprovalGate({
175
+ toolApprovalEnabled: Boolean(deps.toolApproval),
176
+ streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
177
+ backend: deps.backend,
178
+ parkFacility: deps.checkpointStore !== undefined,
179
+ });
180
+ // 协议未上场 ⇒ 零帧**且零 store 读**(场景9 的钉打的就是这两件事,不只是零帧)。
181
+ // env-only worker(无 durable backend)在 `no_backend` 那条臂上一并收掉:无 ask 账 ⇒ 无对账基准。
182
+ if (!gate.active)
165
183
  return [];
166
- const store = deps.backend?.approvalAsk();
167
- if (!store)
168
- return []; // env-only worker(无 durable backend)= 无 ask 账 ⇒ 无对账基准,诚实缺席
184
+ const store = gate.askStore;
169
185
  const rows = await store.listPendingByTask(taskId, signal);
170
186
  const { frames, skipped, dropped } = buildApprovalPreamble(rows, Date.now(), deps.config.streamApproval.replayMax);
171
187
  if (skipped > 0)
@@ -284,7 +300,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
284
300
  return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
285
301
  }
286
302
  // [2255]① 真出路材料:activeTaskStatus + parked 时 pendingGate{kind,decidePath}(best-effort,失败退旧形)
287
- return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore }, sessionId, created.activeTaskId) };
303
+ return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore, governance: deps.config }, sessionId, created.activeTaskId) };
288
304
  }
289
305
  // Durable F4: persist the resume rebuild inputs (sessionId-keyed) so an operator can resume from any
290
306
  // replica even after this worker is gone — core's checkpoint blob can't carry service scenario context.
@@ -219,14 +219,26 @@ async function handleTasksBody(req, res, url, ctx, miss) {
219
219
  // 有界(2s)+ fail-open:store 抛或挂起都只记一次 warn、零卡继续开流(§5.3)。
220
220
  // 🔴 开关谓词与 runs.ts 同源同理由(codex 复审第二轮 high:回滚下表里可能有残留 STREAM_PENDING
221
221
  // 行,无谓词会破「开关关 = 逐字零变化」;顺带省掉关闭态每次开流的一次 store 往返)。钉:场景9。
222
- const replaySessionId = deps.config.streamApproval.enabled ? prepared.spec.sessionId : undefined;
222
+ //
223
+ // 🔴 A-010.7 随修(**兄弟扫查**:台账只点名了 runs.ts 那一处,这里是同形的第二处):谓词是
224
+ // **上场门**不是裸开关。五项合取(协调器在场 / 开关真 / backend 在场 / 账本非易失 / park 设施在场)
225
+ // 里少查三项 ⇒ 一个协议根本没上场的 SQL 部署,每次开流仍要白打一次库;回滚时更坏 —— 别的副本
226
+ // 落下的残留 `STREAM_PENDING` 行会被无门的重放腿照常投上 wire。换成与协调器注入 / 能力面 /
227
+ // 回决 501 口**同一个**谓词,「说没上场」⟺「零帧且零 store 读」才是结构成立的。
228
+ const replayGate = resolveStreamApprovalGate({
229
+ toolApprovalEnabled: Boolean(deps.toolApproval),
230
+ streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
231
+ backend: deps.backend,
232
+ parkFacility: deps.checkpointStore !== undefined,
233
+ });
234
+ const replaySessionId = replayGate.active ? prepared.spec.sessionId : undefined;
223
235
  if (replaySessionId) {
224
236
  const replayOwner = gatedPrincipal(req, deps.config) ?? null;
237
+ const askStore = replayGate.active ? replayGate.askStore : undefined;
225
238
  const preambleFrames = await collectSsePreamble(async (signal) => {
226
- const store = deps.backend?.approvalAsk();
227
- if (!store)
239
+ if (!askStore)
228
240
  return [];
229
- const rows = await store.listPendingBySession(replaySessionId, replayOwner, signal);
241
+ const rows = await askStore.listPendingBySession(replaySessionId, replayOwner, signal);
230
242
  const { frames, skipped, dropped } = buildApprovalPreamble(rows, Date.now(), deps.config.streamApproval.replayMax);
231
243
  if (skipped > 0)
232
244
  deps.logger?.warn?.("approval_preamble_row_skipped", { sessionId: replaySessionId, skipped, face: "tasks.stream" });
@@ -307,7 +319,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
307
319
  if (created.ok)
308
320
  deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget; in-titler dedupe;model 跟 turn([1992]②)
309
321
  if (!created.ok) {
310
- const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore }, prepared.spec.sessionId, created.activeTaskId); // [2255]① 真出路材料
322
+ const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config }, prepared.spec.sessionId, created.activeTaskId); // [2255]① 真出路材料
311
323
  // Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
312
324
  // the returned resp is for a deduplicated concurrent caller (and is never idem-cached).
313
325
  // done 帧 additive 携带同一份材料 —— 形状铸在 toDoneFrameResult(有名字有类型),不在这行内联挑键。
@@ -1225,7 +1237,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1225
1237
  const tid = uuidv7();
1226
1238
  const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
1227
1239
  if (!created.ok)
1228
- return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore }, prepared.spec.sessionId, created.activeTaskId) }; // [2255]① 真出路材料
1240
+ return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config }, prepared.spec.sessionId, created.activeTaskId) }; // [2255]① 真出路材料
1229
1241
  deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget;model 跟 turn([1992]②)
1230
1242
  durableTaskId = tid;
1231
1243
  }
@@ -255,6 +255,11 @@ async function handleTaskArtifacts(res, runStore, taskId) {
255
255
  * trace 的 tool-result 块与本判据读的是同一份日志,所以「trace 上看得见 ⇒ 这里读得到」成立;
256
256
  * 同 session 里**别的 task** 的 ref(或日志已过保留窗被逐出的)按 404 同形拒,请到那条 task 上读。
257
257
  *
258
+ * 🟢 **两条残余已随 core 5.26.0 #119 销账**(下面原文保留作病历)。core 按当年这里写的方向动了刀:
259
+ * `put` 带出处落库 + `ownerOf` 读回,读面改用出处判定(实现见 ②-a),于是 (a) 合成 id 铸的 ref 不再
260
+ * 读不到、(b) 拼接非单射的越权面结构上消失(ref 铸法本身也换成 `~` 四段单射)。日志重铸只剩**存量
261
+ * 兜底**一条腿(无出处的旧行),新写入一律经出处。
262
+ *
258
263
  * ⚠️ 两处**已知残余**,都要 core 侧动刀才能消,写在明处而不是让下一个人自己撞(codex 复审 R3):
259
264
  *
260
265
  * (a) **覆盖面**:core 除了「按工具调用 id 卸载」这条主路,还从**合成 id** 铸 ref ——
@@ -315,22 +320,46 @@ ref, query) {
315
320
  const run = ownedRun ?? (await runStore.getRun(taskId));
316
321
  if (!run)
317
322
  return notFound();
318
- // 精确成员判定(见头注):本 task 耐久日志作证过的 toolCallId,经 core 同一个铸造点变成 ref 集合。
319
- // 成本口径:整份日志一读一扫,与同域的 `/turns`、`/artifacts` 同款(不是本面引入的新量级);分页读会
320
- // 逐页重扫 —— 真消要一张 (task, ref) 出处索引,与上面 (a) 是同一件 core/store 侧的活。
321
- const events = await runStore.getEvents(taskId, 0);
322
- let attested = false;
323
- for (const ev of events) {
324
- // 取键姿势与同域的 `trace/artifacts.ts` 逐字同款(同一份日志、同一个字段,读法不该有第二种)。
325
- const { toolCallId: id } = (ev.data ?? {});
326
- // 逐条比对而不是先建全集:命中即停,长日志上不必把整份集合物化。
327
- if (typeof id === "string" && id.length > 0 && buildToolResultRef(run.sessionId, id) === ref) {
328
- attested = true;
329
- break;
323
+ // ②-a 出处判定(#119,core 5.26.0 —— 上面 (a)/(b) 两条残余的**正解到货**)。
324
+ // 店里现在自己记着「这枚 ref 属于谁」(`put` 的第三参落库,`ownerOf` 读回),读面拿**自己已知的字段**
325
+ // 比对即可:零解析、零段转义规则复制,合成 id 铸的 ref 与拼接歧义两条残余同时消失。
326
+ //
327
+ // 🔴 为什么必须改而不是"锦上添花":5.26.0 起 offload ref 是四段单射形(内容坐标进 ref),服务端手里
328
+ // 没有内容 下面那条重铸判据**重铸不出来** 每一枚真 offload ref 都会 404。读面整死。
329
+ //
330
+ // ⚠️ 辖域(有意的口径变化,写在明处):出处的粒度由 core —— offload/budget/projection 三个写点只声明
331
+ // session,把 taskId 钉在那里会把「一个 session 的两条 task 共用一枚 ref」这个**设计内**的共享形变成拒绝
332
+ // (core d.ts 逐字裁过)。所以有出处时的判据是「同 session,且出处**若点名了** task 则必须是本 task」——
333
+ // 相对旧的「本 task 日志作证过」是**放宽到同 session**。放宽的边界仍在 owner 之内(run 归属门在本函数
334
+ // 之前已过,session 的属主就是这个 caller),换来的是这条读面在 5.26.0 上还活着。
335
+ const owner = store?.ownerOf ? await store.ownerOf(ref) : undefined;
336
+ const ownedByThisCaller = owner !== undefined && owner.sessionId === run.sessionId && (owner.taskId === undefined || owner.taskId === taskId);
337
+ // ②-b 无出处兜底:**没声明属主**的行按老判据走 —— 本 task 耐久日志作证过的 toolCallId,经 core 同一个
338
+ // 铸造点变成 ref 再精确相等。unowned 本身不是授权(core:读面对 unowned 一律 fail-closed),日志作证才是。
339
+ //
340
+ // ⚠️ 这条**不是**「读得到升级前的存量行」(codex 复审 medium,已核真并改口):5.26.0 的
341
+ // `buildToolResultRef` 铸的是 `~` 分隔形,重铸不出旧引擎的 `tr_<sid>_<call>`,而按旧规则手写一个 legacy
342
+ // 编码器 = 复制上游规则(本文件头注自己禁掉的那件事)。真正的处置在别处 —— 本批要求**删表重建**
343
+ // `tool_result`(见 CHANGELOG),所以升级后库里根本没有旧行。这一支覆盖的是「5.26.0 引擎写下、但写点
344
+ // 没声明出处」的行(core 自家的 offload 写点都带出处,所以现实里几乎为空),留着是纵深不是承诺。
345
+ // 成本口径:整份日志一读一扫,与同域的 `/turns`、`/artifacts` 同款;有出处时**根本不读日志**。
346
+ if (!ownedByThisCaller) {
347
+ if (owner !== undefined)
348
+ return notFound(); // 出处在,但不是这位 —— 不给第二次机会(日志重铸绕不过出处)
349
+ const events = await runStore.getEvents(taskId, 0);
350
+ let attested = false;
351
+ for (const ev of events) {
352
+ // 取键姿势与同域的 `trace/artifacts.ts` 逐字同款(同一份日志、同一个字段,读法不该有第二种)。
353
+ const { toolCallId: id } = (ev.data ?? {});
354
+ // 逐条比对而不是先建全集:命中即停,长日志上不必把整份集合物化。
355
+ if (typeof id === "string" && id.length > 0 && buildToolResultRef(run.sessionId, id) === ref) {
356
+ attested = true;
357
+ break;
358
+ }
330
359
  }
360
+ if (!attested)
361
+ return notFound();
331
362
  }
332
- if (!attested)
333
- return notFound();
334
363
  if (!store)
335
364
  return notFound(); // durable store 缺席:404 同形,不 501(不泄露部署形)
336
365
  const slice = await store.get(ref, { ...(offset !== undefined ? { offset } : {}), ...(limit !== undefined ? { limit } : {}) });
@@ -511,23 +511,49 @@ export declare function validateUserSkills(skills: unknown): string | null;
511
511
  * POST /v1/runs → async: 202 {taskId, sessionId, status}, runs in background (S1)
512
512
  * GET /v1/runs/:id → run status + result (poll)
513
513
  * GET /v1/runs/:id/events → SSE replay from Last-Event-ID, then tail to terminal (S2)
514
- * POST /v1/sessions/:id/wake501 (resume is implicit: re-issue a task with the same sessionId)
514
+ * POST /v1/sessions/:id/notify inject an out-of-session event (design/144 §2): live stream core
515
+ * `TaskStream.notify()`; idle ⇒ parked in the session inbox, drained
516
+ * as a `task_notification` on the next stream open
517
+ * POST /v1/sessions/:id/wake → wake a `task_done` pure park with a message and resume it
518
+ * (design/144 §3). NOT a gate decision — a pending gate is refused
519
+ * (`wake.gate_pending`). Goes through `resumeWake`, which reuses the
520
+ * resume family's lease/CAS/run-log leg, so it BURNS MODEL TOKENS and
521
+ * is subject to the billable-submit gates (`SESSION_WAKE_RE`).
522
+ * (both live in `http/routes/notify-wake.ts` — design/158 A9 domain split)
515
523
  */
516
524
  export declare function createHttpServer(rawDeps: ServiceDeps): http.Server & {
517
525
  denyExpiredApprovals: (now: number) => Promise<void>;
518
526
  };
519
- /** SSE: replay durable events after Last-Event-ID, then tail until the run is terminal (or stale). */
520
- /**
521
- * The differences a concrete log (task_run | image_bake) feeds the ONE resumable SSE reader (P2.8). Everything
522
- * the reader does — Last-Event-ID/`?from=` resume, the 416 retention boundary, the per-poll concurrent
523
- * status+events read, the terminal re-fetch (the terminal event lands in the gap before the status flips), the
524
- * stale fallback, the 15-min cap, the 15s idle heartbeat — is provider-agnostic and lives in `streamSseLog`.
525
- * 🔴 The task_run provider MUST keep the existing wire bytes EXACTLY (center's relay + 730+ tests depend on it).
526
- */
527
527
  /** POST endpoints that trigger BILLABLE work — the fail-closed auth guard must cover ALL of them (council: the
528
528
  * guard's inline list had drifted from the handlers and missed `/v1/approvals/:id/decide`, which resumes a run
529
529
  * via resumeCheckpoint/store.decide → paid tokens). Keep this in sync when adding a billable POST route. */
530
530
  export declare function isBillableSubmitPath(url: string): boolean;
531
+ /**
532
+ * A-010.23(#209 件5)—— **改写门**:不烧模型、但在**没有任何 service credential** 的部署形下必须与
533
+ * billable / bake 两族一样 fail-closed 的写门。谓词与 `isBillableSubmitPath` **刻意分开**:这几扇门
534
+ * `billable=false`(零模型工作),塞进那张名单会让它们连带吃 drain 与 model-roster-pending 两道 503 ——
535
+ * 而收编与规则读写在排空期/roster 未落时做完全无害。
536
+ *
537
+ * 判据(两个合取项,缺一不进本表)= 「授权的唯一输入是 principal 头」∧「动作是**持久改写**」:
538
+ * · `POST /v1/adoption` —— operator-only 的一次性部署级动作,把一个 principal 名下**每一行**重写到另一个
539
+ * 身份;授权判据 `explicitOperatorOk(gatedPrincipal(...))`。
540
+ * · `POST /v1/rules/cc-import/*` —— principal lane 的**持久 allow 规则**铸造口;一条持久 allow 规则对后续
541
+ * 同命令的 classify 档 ask 常驻消音(core #144 之后仍然如此,只是不再消 mandated 的那些)。
542
+ * · `DELETE /v1/rules` —— 同一个规则店的**撤销**口。方向相反(收紧)但同样是持久改写,而且它多一条
543
+ * operator 越权域(`?principal=` / body 的 `principal` 让 operator 收回**任一**租户的规则)⇒ 伪造一个
544
+ * 列在 `OPERATOR_PRINCIPALS` 里的头,就能把每一位租户的规则一次清空。
545
+ * 无 service token 的部署上 `verifiedPrincipal` 走的是「BFF/gated:头已由上游验过」那一支,而这一形恰恰
546
+ * 是**没有**那个上游 —— 于是伪造一个头就能收编别人、替任意租户种下常驻放行、或替他们全删。bake 门当年
547
+ * 补的就是同一句话,`isDestructiveSessionWrite` 从逐条路由白名单改成族判定也是同一句话(那次的措辞逐字是
548
+ * 「a forged principal header alone could **tighten/DoS** a session's tools」——撤销口落的正是 DoS 那一半)。
549
+ *
550
+ * 🔴 **方法感知**(codex 对抗复审 R2-[high],验真后修):撤销走的是 `DELETE`,而本门最初只挂在 `POST` 的
551
+ * 合取里 —— 于是一个自称「持久改写」的谓词把爆炸半径最大的那条动词漏在门外,正是 `isDestructiveSessionWrite`
552
+ * 当年被抓到的同一形。签名因此收方法,与那只谓词逐字同形。
553
+ * 🔴 读面不进本表:`GET /v1/adoption/:id` 与 `GET /v1/rules` 各有自己的属主/operator 门,本门只拦写。
554
+ * 行为面钉:`test/rewrite-door-service-token-gate.test.ts`(四扇门各一正一反 + 逃生口格 + 读面负控)。
555
+ */
556
+ export declare function isCredentialGatedRewrite(method: string, url: string): boolean;
531
557
  /** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`).
532
558
  * [#104] 字面量**先于**模式:此前模式先查,五条精确路由被形状桶吞掉(`/v1/approvals/stream`
533
559
  * 落 ":id"、`/v1/images/{bakes,select,register}` 落 ":profile"、`/v1/images/bakes/claim` 落