/** * [ref] 件③ —— **出处 / 抹除合规面的 operator 两口**(core 5.57.0 `MemoryEngine.provenanceOf` / * `eraseMemoryEntries`,[ref] v2-a / v2-b)。 * * · `GET /v1/memory/entries/:entryId/provenance` —— 回 `EntryProvenanceAccount` 原样; * · `POST /v1/memory/erase` —— 请求 `{requestId, select, allowUnevidenced?}`,回 * `MemoryErasureAttestation` 原样。 * * ── 为什么是 operator-only ───────────────────────────────────────────────────────────────────── * 与 [ref] 的 bundle 两口同族(memory-bundle.ts 的同名段): * · erase 是**治理写面**,而且是本族里爆炸半径最大的一口 —— 整 scope / 整会话删除是**合法**选择子, * 而 core 的大规模删除熔丝对它**显式不适用**(engine.d.ts 逐字:那道熔丝守的是「文件在 harvest * 时悄悄不见了」的事故形,一次显式授权的抹除靠在收执里逐 id 列举来自证); * · provenance 是**跨租户治理读**:一条 entry 的绑定 / 贡献会话 / 污染标记 / 托管链事件全在里面, * 按定义超出任何单个 principal 的自助边界。 * 缺 principal 的形照 sibling operator 端点(adoption / retention-ops / memory-bundle)401。 * * ── 门序(逐字同 routes/memory-bundle.ts)───────────────────────────────────────────────────── * 身份(401)→ 授权(403)→ 能力(501)→ 验型(400)。**授权在能力之前**:一个够不着任何东西的调用方 * 不该从「这个部署有没有记忆引擎」上读出部署形态。 * * ── 能力面的诚实形:为什么这两口的 501 判据与 bundle **不同** ───────────────────────────────── * 两族同码 `capability.memory_engine_required`(消费端分支相同:换部署形态),但**判据不是同一条**: * bundle 要的是后端的 v2-c 复合面(`exportSnapshotOf` / `importBundleCommit`),本族要的是后端**自带 * 控制面归属**(`controlPlaneRoot`)—— 这两口真的读引擎控制面(lineage / challenges / 托管链),而缺 * `controlPlaneRoot` 时 core 会把控制面落到**副本本地盘**上,与本仓 stateless replicas 正面冲突。 * 判在**挂载期**(`createMemoryComplianceFaces` 返回 undefined ⇒ 两口整个不挂),全文见 * `src/memory-operator-faces.ts` 头注。⚠️ 因此本族 501 的**文案**与 bundle 那句刻意不同字:两句指的是 * 不同的旋钮,复用一句会把运维指到一个根本没问题的地方去(bundle 那条自己的头注就吃过这个亏)。 * * ── 透传的理由(照 memory-bundle.ts 的「披露的传导」段同源)─────────────────────────────────── * 两口的 200 体都**原样下发**,不投影、不复述、不补键。理由是单一属主 + 账形是**开放判别式**: * · `EntryProvenanceAccount.binding` 是 `CommittedBinding | {state:"absent"} | {state:"unknown",…}` * 的判别式,`custody.events` 是 core 的 `TransferEvidence` OPEN form(通道词表由写它的那个二进制定), * `MemoryErasureAttestation.erased[].binding` 同族。在本层写一张白名单就是**复述一份会随 core 漂移 * 的判别式**:core 收一格我方不会跟着动,而两边都"绿"。 * · 与 import 报告那边**刻意相反**(那里是显式白名单):`MemoryImportReport` 是一张**处置清单**—— * core 新长一个处置座时,静默上 wire 才是危险的,所以那边要编译期差集门。本族两个账是**事实陈述** * (这条 entry 是什么、这次删了什么),新长一个事实键的正确行为就是让它到达消费端 —— 少给一个字段 * 才是这里的危险。两种姿势的判据是「新键静默上 wire 危险吗」,不是「透传省事吗」。 * `bundle.doc` 那条披露传导的先例在这里同样适用:转发即真话,复述会随版本漂移。 * * ── 幂等:**两条腿的契约不同**,消费端必须分开读(F-5;core 明写)───────────────────────────── * · **证据腿**(后端有 `eraseWithEvidence` —— core 自带的 File 后端有):`requestId` 是幂等身份。 * core 在托管链上落一条 erasure anchor,重发同一个 requestId **读回**那条 anchor 的 pinned id 集, * 绝不重新解析;换了选择子还用同一个 id ⇒ 响亮拒(`memory.erasure_selector_mismatch`)。 * · **降级腿**(`allowUnevidenced: true`,后端没有证据面):core 的原话逐字是 "same requestId = a NEW * request (re-resolved — replay convergence is not promised)" —— **重发就是第二次真删**。这条腿上 * `erasedPreviously` / `evidenceEv` 永不出现,每一行 notFound 都带 `historyUnknown`(「从没存在过」与 * 「无证据地被删过」不可判别),而 `evidenceCapability: "none"` + `custodyState: "capability-absent"` * 是它的自证标记(静默降级是被禁止的形)。 * 🔴 消费端(SDK / 壳 / 运维脚本)**不许**把这一口当成「重发安全」的口:先读收执的 `evidenceCapability`, * `"none"` 时任何重试都必须是人做的决定,不是自动重试策略。 * ⚠️ **部署形态的诚实话**(照 memory-bundle.ts 的「能力面的诚实形」):今天在本仓,凡是这两口**挂得上** * 的部署(= 后端带 `controlPlaneRoot`,即 core 的 File 记忆后端),`eraseWithEvidence` 都在场 * ⇒ 走的是**证据腿**;两只 SQL 记忆孪生既没有 `controlPlaneRoot` 也没有 `eraseWithEvidence`, * 它们的形态是「整口不挂 + 501」,不是「降级腿」。因此降级腿今天是一条**够得着但用不上**的路 * (调用方传了 `allowUnevidenced: true` 也会被证据腿先接走 —— core 的判序如此)。它成为唯一可达腿的 * 前提是「某个后端有控制面归属却没有证据面」,那种后端今天不存在;真出现的那天,上面那段幂等契约 * 就是它的合同,而不是到时候再补一句。 * * `allowUnevidenced` **恒不注入**:server 一个字都不替调用方选 —— 递交的就是调用方发的那个体。降级腿是 * 一次**人的**授权表态(「我接受没有证据的删除」),中间层替他勾上就是替他签字。 * * ── 验型的分权:server 验**形**,core 验**义**(同 memory-bundle.ts)──────────────────────────── * 本层只用 zod 校 JSON **结构**(宪法 [ref]:边界必 schema、禁裸 as-cast)。三选一选择子的判决 * (恰好一个 ids/scope/sessionId、ids 非空去重、allowUnevidenced 是布尔…)**全部**归 core 的 * `erasureRequestInvalid` —— 在本层抄一遍等于第二真源。于是 `{ids:[…], scope:…}` 这种**形对义错**的请求 * 会走到引擎并带回 `config.memory_erasure_request`。 * * 计费/lane:两口都是**部署级治理动作**,零模型工作 ⇒ `billable=false`(与 adoption / retention-ops / * memory-bundle 同族,申明在 test/billable-route-declaration.test.ts)。 * * ── 改写门(`isCredentialGatedRewrite`):**两口都进** ────────────────────────────────────────── * 该门自 [ref] 起的判据是「持久改写 **或** 授权唯一输入是 principal 头、且**没有属主门兜底**的治理面」: * · `POST /v1/memory/erase` 落在**前**一项上,而且是最深的一种:它把条目从本店**删掉**。自造一个在册 * operator 头即可对任一租户执行一次不可撤销的删除 —— 无凭证部署上缺的正是「上游会验这个头」这个前提。 * · `GET …/:entryId/provenance` 落在**后**一项上。🔴 这一条**推翻了 [ref] §3 的原始裁定** * (稿子把它豁免在门外,理由是「爆炸半径=一条」);codex 交叉复审 [high] 指出该理由站不住,亲验后采信: * ⑴ 稿子援引来类比的两条豁免读(`GET /v1/adoption/:id` / `GET /v1/rules`)在门外的**真正**原因是 * 它们各有**属主门**兜底,不是「读得少」;本口的授权判据就只有 `explicitOperatorOk`,过了就直接答; * ⑵ 无凭证部署上那个头**可自造**,于是「一条」这个上界实际退化成「知道 id 就能读」——拿一个不可猜的 * id 当唯一的授权,是本仓在别处明令不接受的形; * ⑶ 一条账里有:绑定的 scope+slug、贡献会话 id 与污染理由、仓内 ingest 路径与内容哈希、托管链事件。 * 先例逐字同源:`POST /v1/memory/export` 当初也「按原措辞本不该进」,同一条论证把它收了进来 * ——「把它留在门外只为守住一句措辞,是把措辞看得比它要保护的东西更重」。 * 🔴 判据里**刻意不写**「本口 side-effect-free」:那不是这道门的判据。该门拦的是「授权的唯一输入是 * 一个可自造的头」这件事 —— 用「没有副作用」当豁免理由,正是 [ref] 那次把 `POST /v1/memory/export` * (纯读、跨租户全库)漏在门外的那句话。 * ⚠️ 代价与补偿如实成文:无凭证部署上两口都答 503 `auth.service_token_required`。补偿是**既有旋钮、 * 零新增** —— 配 `SERVICE_AUTH_TOKEN`(正路),或本地开发显式 `ALLOW_UNAUTHED_WRITES=true` * (与 billable / bake / 破坏性会话写 / 其余七扇门共用同一个逃生口)。 * * 分层:本模块不值 import `server.ts`(那条边闭合运行时装载环),只 `import type`。 */ import type { IncomingMessage, ServerResponse } from "node:http"; import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js"; export declare const MEMORY_ERASE_PATH = "/v1/memory/erase"; export declare function handleMemoryCompliance(req: IncomingMessage, res: ServerResponse, match: RouteMatch, ctx: RouteCtx): Promise; export declare const MEMORY_COMPLIANCE_ROUTES: readonly [{ readonly id: "memory-erase"; readonly path: "/v1/memory/erase"; readonly credentialGated: readonly ["POST"]; readonly methods: readonly ["POST"]; }, { readonly id: "memory-entry-provenance"; readonly pattern: RegExp; readonly label: "/v1/memory/entries/:entryId/provenance"; readonly credentialGated: readonly ["GET"]; readonly methods: readonly ["GET"]; }]; /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */ export type MemoryComplianceRouteId = RouteIdsOf; //# sourceMappingURL=memory-compliance.d.ts.map