/** * [ref] 件② —— **外源标记人面的 operator 三口**(core 5.59.0 `MemoryEngine.listExternalOriginEntries` / * `listOriginClearances` / `clearEntryOrigin`,[ref] §13-4①②)。 * * · `GET /v1/memory/origin/external?scopes=a,b` —— 点名 scope 里每一条带外源标记的条目 + 出处账; * · `GET /v1/memory/origin/clearances` —— 清标审计账(**白名单投影**,见下); * · `POST /v1/memory/origin/entries/:entryId/clear` `{requestId, reason}` —— 审计化的 UN-MARK 阀门。 * * ── 为什么是 operator-only ───────────────────────────────────────────────────────────────────── * 与件③(memory-compliance.ts)、[ref] 的 bundle 两口同族: * · 前两口是**跨租户治理读**:一次调用取回任意 scope 的条目 + 每条的完整出处账(贡献会话 id、污染 * 理由、托管链事件),按定义超出任何单个 principal 的自助边界; * · clear 是**治理写面**,而且是记忆域里语义最重的一次表态 —— 它把「这条记忆来自外部、用前先核」 * 这句披露**替宿主担保掉**。core 的原话是 the host's explicit vouching starts the entry's unmarked * life:签字的人必须是部署的操作员,不是任何一个租户。 * 缺 principal 的形照 sibling operator 端点(adoption / retention-ops / memory-bundle / memory-compliance)401。 * * ── 门序(逐字同 routes/memory-compliance.ts)───────────────────────────────────────────────── * 身份(401)→ 授权(403)→ 能力(501)→ 验型(400)。**授权在能力之前**:一个够不着任何东西的调用方 * 不该从「这个部署有没有记忆引擎」上读出部署形态。 * * ── 能力面:501 的判据与件③ **同一条**(同码同文案族,不同族名)──────────────────────────────── * 三口都真的读引擎控制面,而且 clear 的写前托管行是崩溃窗内条目字节的**唯一一份** —— 判在挂载期 * (`createMemoryOriginFaces` 返 undefined ⇒ 三口整个不挂),全文见 `src/memory-operator-faces.ts` 头注。 * 码复用 `capability.memory_engine_required`(消费端分支相同:换部署形态),文案自成一句 —— 与 bundle / * compliance 两族刻意不同字,因为要查的旋钮不是同一个(那两族自己的头注就吃过复用文案的亏)。 * * ── `scopes` 为什么必须由调用方给(F-13 的诚实形)────────────────────────────────────────────── * 🔴 本仓**今天没有任何 scope 枚举读面**。core 5.59.0 长出了 `listMemoryScopes`([ref]③:File 腿在场、 * SQL 腿是 core 候升件,且带「backend 是否支持枚举」判别位——不支持**永不读作零**),但本仓尚未接线。 * 于是 v1 的形是:scope 名**显式提供**,缺参 400 并**指路来源**。 * 为什么不给一个默认名单(比如「本部署配过的那些」):污染审计的价值全在**发现链闭合**,而不完备是 * **静默**的 —— 空数组与「这些 scope 干净」在 wire 上同形。一个猜出来的名单会让 operator 读到一份 * 自称干净、其实只查了一半的账。所以本口的答案永远只对**它被点名的那些 scope**成立,这句话写进 400 * 文案、写进附录 A、也写进端点文档:**never read an empty answer as "the store is clean"**。 * * ── `GET …/clearances` 为什么是**白名单**而不是透传(F-7)─────────────────────────────────────── * 与件③ 的两个账(透传)**刻意相反**,判据仍是那一条「新键静默上 wire 危险吗」: * · `OriginClearanceRow.entryText` = 被清标条目的**完整正文托管字节**。core 逐字:a crash between the * tombstone batch and the re-record batch leaves this as the only copy —— 它是**崩溃恢复席**,而恢复 * 的手段是**再调一次 `clearEntryOrigin`**(引擎从行里自己重放),不是让人把字节读出来贴回去。 * 把整条记忆的正文放上一个审计列表面,等于给每一次「看看清标记录」都附赠一份全文导出; * · 而这份账**新长一个键**时,静默上 wire 恰恰是危险的那一侧(它是托管面,不是事实陈述面)。 * ⇒ 显式白名单 + **编译期差集门**(`test/wire-whitelist-exhaustiveness.test.ts` 的 `KnownExcluded` * = `entryText`,逐字写理由):core 加一个键 ⇒ TS2344 ⇒ 由人处置,而不是悄悄上 wire 或悄悄漏掉。 * 🔴 白名单是**深**的,不是只做一层(codex 交叉复审 [high] 抓的第一版病):嵌套的 `origin` 也逐键投影 * —— 一层白名单 + 一个整只透传的嵌套对象,等于给这条排除留了一个旁路,全文见 {@link buildOriginWire}。 * 🔴 **剥了不等于不说**:投影补一个 `custodyBytes`(托管字节数)。这是 [ref] absence-reports-not-silent-green * 在本口的落法 —— operator 判「这个 pending 行还揣着一条记忆吗」只需要这一位;悄悄把整个托管概念从 * 审计面上抹掉,会让一个**必须由人处置**的崩溃恢复席看上去不存在。 * * ── 拒因族:**闭集逐码 switch**([ref] 码族)──────────────────────────────────────────────────── * 见 {@link sentClearCode} 的头注(逐码状态与理由、以及 core 真形与 [ref] 稿面的**一处偏差**)。 * * 计费/lane:三口都是**部署级治理动作**,零模型工作 ⇒ `billable=false`(与 adoption / retention-ops / * memory-bundle / memory-compliance 同族,申明在 test/billable-route-declaration.test.ts)。 * * ── 改写门(`isCredentialGatedRewrite`):**三口全进** ────────────────────────────────────────── * 该门自 [ref] 起的判据是「持久改写 **或** 授权唯一输入是 principal 头的跨租户治理读」: * · `POST …/clear` 落在**前**一项:它把一条条目的外源披露**永久摘掉**(墓碑 + 同 id 重录),自造一个 * 在册 operator 头即可让任意租户的一条外来记忆看上去是本店自产的 —— 而记忆是模型每次挂载都读的面; * · `GET …/external` 落在**后**一项,而且是这一族里爆炸半径最大的读:一次调用取回**任意 scope**的 * 条目 + 每条的完整出处账(贡献会话 id、污染理由、仓内 ingest 路径与内容哈希、托管链事件)。判据与 * `POST /v1/memory/export`([ref] 收进门的那条纯读)逐字同构:它没有属主门兜底,operator 头过了就直接答。 * · `GET …/clearances` —— 🔴 **本车第一版把它留在门外,codex 交叉复审 [high] 推翻,亲验后采信**。 * 那一版的理由是「投影后不含条目正文,爆炸半径差一个量级」,而这个理由**用错了基准**:本门的判据 * 从来不是「含不含正文」,而是「授权的**唯一**输入是 principal 头」∧「没有属主门兜底」。按那条判据, * 一口**整部署**的清标账(每个租户的 scope+slug、entryId、rev、operator 手写的自由文本 `reason`、 * requestId、时间线)比已经在门里的**单条** `GET …/:entryId/provenance` 爆炸半径更大,不是更小;而 * `reason` 是自由文本 —— 它完全可能把条目内容摘一句进去,连「不含正文」这个前提本身都不牢。 * 与 `POST /v1/memory/export` / `GET …/provenance` 两次改判是同一条论证:把它留在门外只为守住一句 * 自己写的措辞,是把措辞看得比它要保护的东西更重。 * ⚠️ 三条都是**逐条式**入门,不是 `/v1/memory/origin/` 前缀吃(写口那一臂才是前缀,方向宁紧):这个 * 前缀下将来长出的读口未必都够判据,逐口论证过才入门 —— 这正是本门「闭」的那一半。代价与补偿如实 * 成文:无凭证部署上三口都答 503 `auth.service_token_required`,补偿是**既有旋钮零新增**(配 * `SERVICE_AUTH_TOKEN` 走正路,或本地开发显式 `ALLOW_UNAUTHED_WRITES=true`)。 * * 分层:本模块不值 import `server.ts`(那条边闭合运行时装载环),只 `import type`。 */ import type { IncomingMessage, ServerResponse } from "node:http"; import { type OriginClearanceRow } from "@sema-agent/core"; import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js"; export declare const MEMORY_ORIGIN_EXTERNAL_PATH = "/v1/memory/origin/external"; export declare const MEMORY_ORIGIN_CLEARANCES_PATH = "/v1/memory/origin/clearances"; /** * 一条清标行 → wire 的**显式白名单**(F-7)。全文理由见文件头「为什么是白名单」。 * * 被剥的 core 键**只有一个**:`entryText`(完整正文托管字节),而且**剥了要说** —— `custodyBytes` 是它 * 留在审计面上的那句披露(0 = 这行没揣着字节;>0 = 崩溃恢复席里还有一条记忆的正文,pending 行要人处置)。 * 嵌套的 `origin` 不是「被剥」而是**被深投影**(三键之外的成员剥值留名,见 {@link buildOriginWire})。 * 🔴 **不要**给这个函数标 `: Record` 返回型:那会让 `keyof ReturnType<…>` 塌成 `string`, * 编译期差集门当场变成恒真(vacuous)—— `buildImportReportWire` 的头注里踩过这个坑,而本车在 `origin` * 那只投影上**又踩了一次**(标了具体返回型,门读标注不读字面量),所以这条钉现在有两处案底。 */ export declare function buildClearanceRowWire(r: OriginClearanceRow): { events: { detail?: string | undefined; eventId: string; at: number; to: "failed" | "done"; requestId: string; }[]; custodyBytes: number; tombstonedAt?: number | undefined; requestId: string; reason: string; at: number; status: "pending" | "failed" | "done"; originUnknownKeys?: string[] | undefined; clearanceId: string; entryId: string; scope: string; slug: string; baseRev: string; origin: { at: number; cause?: "static" | "unattributed" | "observed" | "derived" | undefined; taint: "external"; }; }; export declare function handleMemoryOrigin(req: IncomingMessage, res: ServerResponse, match: RouteMatch, ctx: RouteCtx): Promise; export declare const MEMORY_ORIGIN_ROUTES: readonly [{ readonly id: "memory-origin-external"; readonly path: "/v1/memory/origin/external"; readonly credentialGated: readonly ["GET"]; readonly methods: readonly ["GET"]; }, { readonly id: "memory-origin-clearances"; readonly path: "/v1/memory/origin/clearances"; readonly credentialGated: readonly ["GET"]; readonly methods: readonly ["GET"]; }, { readonly id: "memory-origin-entry-clear"; readonly pattern: RegExp; readonly label: "/v1/memory/origin/entries/:entryId/clear"; readonly methods: readonly ["POST"]; }, { readonly prefix: "/v1/memory/origin/entries/"; readonly credentialGated: readonly ["POST"]; }]; /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */ export type MemoryOriginRouteId = RouteIdsOf; //# sourceMappingURL=memory-origin.d.ts.map