/** * device lane **首绑写协议**(design `device-executor-lane-v2` §4.3.2)—— 两阶段 placement 的 * server 半场:`resolveDevicePlacementIntent`(**只校验、不写库**)+ {@link commitPlacement} * (**唯一**写入属主)。 * * ── 两阶段的**分界在哪**(codex R1 两条 finding 判真后的正形)──────────────────────────────────── * 设计同段给了两条等价路(「同一 DB 事务原子提交,**或** commit 失败时以 taskId/rev CAS 终结并释放已建的 * run」)。本仓取**后者**,理由是结构性的:run 行住在 `RunStore`(tidb / pg / **file**),绑定行住在 * `DeviceStore`(tidb / pg)—— 要它们共享一个事务席,就等于要求「凡 device 车道,两只店必须是同一个 * SQL 连接池」。那会把 file / memory 形态的 run 店整个排除在外,而「本地与云无缝、TOC 面 backend 可插」 * 是本仓的铁律。 * * 🔴 **分界的判据只有一句话:凡需要「这条会话已经归我」才成立的事,排在 claim 之后;其余全排在建任何 * 东西之前。** claim 之后只剩两件,而且是同一条不变量的两条臂 —— **离开 commit 的那一刻,绑定就是准入 * 批准的那一台**:首绑那一次 CAS(写),或对已有绑定的**再断言**(零写;codex 轮 2 [high] 的类修)。 * ⚠️ 「那一刻」是字面义,**不是「此后不变」** —— 论证与残余见 {@link commitPlacement} 顶注。 首绑那一次 CAS 必须在 claim 之后 —— 否则两条并发提交里输掉 claim 的那个会先把绑定写成**它**要的设备, * 赢家的 run 于是落到别人挑的机器上。而准入链的三步(绑定解出 / devices active / presence)**全是读**, * 它们一个字节都不写,所以整段挪到 `resolveDevicePlacementIntent` 里、在 `createRun` **之前**判 *(意图把批准的那台设备**带上**,commit 才有东西可断言 —— 见 `DevicePlacementIntent.admittedDeviceId`)。 * 这条收法同时关掉 codex 轮 1 的两条(都判真): * · **[medium] 幂等键被常规拒绝烧掉**:`POST /v1/runs` 的自带 `body.taskId` 重放腿读的是 durable run 行 * (`routes/runs.ts` 的 `getRun(clientTaskId)` 臂),行在**哪怕是 failed** 也直接回 `202 {status:"failed"}` * ⇒ 「设备离线」这种**常规**拒绝若先建了行,网关按同一个 taskId 的正常重试从此永远拿不到一次真准入。 * 读排在前面 ⇒ 常规拒绝的读面代价为零(零 run 行、零 claim、零补偿)。 * · **[high] claim 与 placement 写之间的无界窗**:claim 之后的**每一次**店往返都带 * {@link placementWriteDeadlineMs} 派生的**硬时限**(远小于 `runStaleSec`)⇒ 「挂得比 stale 回收还久、 * 回来以后仍然放行」这条竞态被时限从结构上关掉;超时 = 拒(fail-closed)+ 释放,绝不放行。 * 残余(如实):`createRun` 之后到心跳起来之前**本来**就有若干 await(`putCtx`、SSE 前导),那个窗是 * 存量形,本文件只负责**不加宽它**(本层的写有界),不假装把它关掉了。 * * ── 为什么 commit 只许有一个属主函数 ─────────────────────────────────────────────────────────────── * 真码有**四个** `runStore.createRun(` 成功点(流式提交 / 同步提交 / durable 提交腿 / wake 续跑腿), * 而「建了 run 行之后要不要绑设备」这件事对它们是同一句话。四份手抄 = 第五个入口出现时漏接一份, * 而漏接的症状是「run 跑起来了但没有绑定」⇒ 执行期才 `device.not_bound`,账面上是一条已收费的失败 run。 * 类级机器门钉住这条:`test/device-first-bind.test.ts` 按 AST 找 `src/http/**` 里每一处 `createRun(`, * 要求同一函数体内可见地走到 {@link commitPlacement}(漏接 = 当场红)。 * * ── 准入链(§4.3.2 提交期三步,全 fail-closed)───────────────────────────────────────────────────── * 1. 绑定解出 deviceId —— 解不出 = `device.not_bound`; * 2. devices 行 `active` —— 行不在 / 不是你的 = `not_found.device`(反枚举同形),吊销 = `device.revoked`; * 3. presence 在线 —— 否 = `device.offline`(**只在提交期**校验;执行期离线走 §5.4/§8)。 * 三步的属主是 `device-enrollment.ts` 的 `admitSession`,本文件一个判据都不自建,而且**整段在建 run 之前**跑 * (见上一段的分界判据)。任何查表故障 = 拒([ref]:安全轴上没有静默 fail-open 臂)。 * * ── 便利度不变(clay [ref] 硬约束)──────────────────────────────────────────────────────────────── * 本文件**不产生任何审批**:它只做「这条 session 该落在哪台设备上」的查表与一次 CAS。device 车道上 * `permissionMode: "bypassPermissions"` 的 run 与 host 车道同构造同读数(判据钉在 device-first-bind * 测试的「零额外审批」格)——「因为是 device 车道所以多问一次」在本层结构上不存在。 */ import type { IncomingMessage } from "node:http"; import { type DeviceOwner } from "../device-store.js"; import { type DeviceEnrollment, type DeviceRejection } from "../device-enrollment.js"; import type { ServiceConfig } from "../config.js"; import type { RunStore } from "../plugins/store-backend.js"; /** * 调用方 device 域的**唯一铸造点**。`undefined` = 判不出域(无 principal,或凭据不带 tenant claim)—— * 消费点必须响亮拒,禁静默空视图。 * * 🔴 位置即契约:`{owner_tenant, owner_subject}` 是 O11 的 device-lane-local 复合身份(全局 principal * 一个字节不动)。subject = `gatedPrincipal`(verified 链),tenant = `ssoVerifiedScope`(registry * auth-bridge JWT 的 `scope` claim)—— **两者都是签名真源,不是自报头**。管理面三读三写与首绑写协议 * 全部从这一只函数取域;派生第二份就是把 O11 的判据本体复制成一份会漂的孪生。 */ export declare function deviceOwnerDomainOf(req: IncomingMessage, config: ServiceConfig): DeviceOwner | undefined; /** * **已校验、未写库**的 placement 意图(§4.3.2 ①)。 * * 它的存在本身就是那条设计裁定:`resolveSpec` / `prepareSpec` 那一刻**还不知道**哪个并发请求会赢下 * session 的 active-run claim,所以它不能写绑定行 —— 只能把「如果我赢了,该写什么」算好交出去。 * 而**除了那一次写**,别的都已经在这一刻判完了(见文件头注的分界判据)。 */ export interface DevicePlacementIntent { /** 绑定键 = **根**会话(后代无条件继承;core 为 subagent/cascade/verify 另铸 sessionId,不另绑)。 */ readonly rootSessionId: string; /** 验证过的复合身份(§4.3.2:恒取 verified 链,不取自可伪造头)。 */ readonly owner: DeviceOwner; /** * **准入链批准的那台设备** —— 意图阶段判完的那一台,也是 {@link commitPlacement} 的**断言对象**。 * * 🔴 这一位是 codex 轮 2 [high] 的类修(病:已绑路径的意图**丢掉了设备身份**,于是一条明确写了 * `deviceId: A` 的提交在「准入过后、claim 之前」被换绑到 B 时会**静默地在 B 上跑完** —— 执行环境 * 工厂读的是**当下**的绑定,它根本不知道调用方点过名,`plugins/remote-env-device.ts` 的工厂段亲读坐实)。 * 身份留在意图里,commit 才有东西可断言。 */ readonly admittedDeviceId: string; /** * `true` = 这条根会话在意图阶段**还没有绑定** ⇒ commit 要做那一次首绑 CAS(写); * `false` = 已有绑定 ⇒ commit **不写**,只把 {@link admittedDeviceId} 对着活绑定**再断言一次**。 * 两条臂的共同不变量只有一句:**离开 commitPlacement 时,这条会话的绑定就是准入批准的那一台。** */ readonly firstBind: boolean; } /** * §4.3.2 ① —— 产出**已校验、不写库**的意图,或响亮拒。**准入链整段在这里跑**(它全是读),所以 * 常规拒绝(未绑定 / 离线 / 已吊销 / 不是你的设备)的读面代价是零:没有 run 行、没有会话占用、 * 没有补偿要做,调用方自带的幂等 taskId 一个都不会被烧掉(codex R1-[medium] 的类修)。 * * 判据(逐条,fail-closed): * · 车道不是 device 而体携 `deviceId` ⇒ **400**(不静默忽略 —— 安全轴禁 quiet fallback:调用方以为 * 自己指定了执行设备,而那条声明从未存在); * · 车道是 device 而装配缺件 ⇒ **501**(boot 的 `device-lane-unwired` 不变量本应先拒启,这里是消费点自卫); * · 判不出调用方域 ⇒ **403 响亮**(O11:准入等值比对没有真源时放行 = 把绑定写成任何人的); * · 没有根会话 ⇒ **400**(绑定键不存在,绑无可绑); * · 其余全部交给准入链属主 `admitSession`,**唯一的分叉**是它答 `device.not_bound` 那一支 —— * 那正是「还没有绑定」,于是本函数看提交体有没有携首绑目标:没携 ⇒ 把 `device.not_bound` 原样交回; * 携了 ⇒ 形门 + devices 行(active ∧ 是本域的 ∧ **在线**)三条读判完,产出首绑意图。 * * 返回 `undefined` = 本部署不是 device 车道 ⇒ 整条 placement 面不存在(既有行为逐字节不变)。 */ export declare function resolveDevicePlacementIntent(args: { config: ServiceConfig; req: IncomingMessage | undefined; rootSessionId: string | undefined; deviceId: unknown; store: import("../device-store.js").DeviceStore | undefined; enrollment: DeviceEnrollment | undefined; }): Promise; /** {@link commitPlacement} 的依赖 —— 逐字取自 `ServiceDeps` 的同名键(同一只 deps 对象喂进来,所以 * 「意图在 ⟹ 准入链在」是**同对象不变量**,不是一句祈愿)。 */ export interface PlacementCommitCtx { deviceEnrollment?: DeviceEnrollment | undefined; config?: { runStaleSec?: number; } | undefined; logger?: { warn(msg: string, fields?: Record): void; } | undefined; metrics?: { inc(name: string, labels?: Record): void; } | undefined; } /** * claim 之后那一次写的**硬时限**(codex R1-[high] 的类修)。 * * 界从 `runStaleSec` **派生**而不是新铸一个旋钮(operator-knob 纪律:没人要求过的旋钮不铸):它要回答的 * 问题只有一个 —— 「这次写会不会挂到 stale 回收把我的会话占用收走」。取四分之一并夹在 `[1s, 15s]`: * 上夹是因为一次 CAS 写挂过 15 秒已经不是慢而是坏,下夹是因为运维把 `runStaleSec` 配得极小时,时限 * 不该短到把正常的库往返判成故障。 */ export declare function placementWriteDeadlineMs(runStaleSec: number | undefined): number; /** * §4.3.2 ②③ —— placement 的**唯一**提交点(写 + 断言都只在这里发生)。四个 `createRun` 成功点全部经它。 * * 到这一刻为止,该判的读已经判完(见文件头注的分界判据),而本函数拿到了一样意图阶段拿不到的东西: * **这条会话的 active-run claim 已经归我们了**。两件事因此才成立: * · **首绑臂**(`firstBind`)—— 做那一次 INSERT-if-absent CAS(赢 = 成立;输且同设备 = 幂等成功; * 输且异设备 = `device.claim_conflict`); * · **已绑臂** —— **不写**,但拿同一只 `admitSession` 对着活绑定把 {@link DevicePlacementIntent.admittedDeviceId} * **再断言一次**(codex 轮 2 [high] 的类修)。判据属主仍是准入链那一只函数,本文件一个新判据都没造。 * * 不变量(一句话):**离开本函数的那一刻,这条会话的绑定就是准入批准的那一台;否则本请求刚建的 run * 已被终结并释放,调用方拿到的是一个 typed 拒绝。** * * 🔴 **这条不变量到此为止,它不说「此后不变」——一次如实改口(codex 轮 3 驳回了我方轮 2 的论证)。** * 我方轮 2 写过「claim 一归我们,换绑动词的『无活跃 run』门就把并发换绑拒掉 ⇒ 断言之后绑定不可能再动」。 * **那句话是错的**,codex 轮 3 给的交错成立、亲读坐标复核无误:换绑腿的那道门是 check→act * (`http/routes/devices.ts` 的「🔴 成文残留(设计裁定,非疏漏)」那一段逐字写着这个窗),它可以在本次 * `createRun` **之前**读到「无活跃 run」而在本函数**之后**才提交 CAS —— run claim 不推进绑定 `rev`, * 那条 CAS 照样赢。于是: * · 本断言**关掉**的是「换绑在本函数读之前已可见」那一半(较大的一半); * · **剩下**的一半是那条**存量**的 check→act 窗,它的既有处置逐字在那段注里:①赢窗的 run 在换绑提交 * **后**铸 env ⇒ 读到新绑定;②在提交前铸(或已 connect)⇒ 投递门 `expectedDeviceId` 围栏按铸造时 * 设备恒拒,零指令落到任何一台设备。**对本批新增的「体里点了名」这一形,臂 ① 的『合法』口径不再成立** * (调用方点了 A,却在 B 上跑),这条**登记为未闭环**:要真正关掉它,得把 `admittedDeviceId` 一路钉进 * 执行环境工厂与投递围栏(那是一条跨 core seam 的**新能力**,按本仓复审纪律不在复审环里加),或按 * 稿 §6 的候门段做跨店互斥 claim。两条都是独立的车与设计,不是本层的一个补丁。 * * 返回 `null` = 放行(含「本部署不是 device 车道」)。 */ export declare function commitPlacement(ctx: PlacementCommitCtx, runStore: RunStore, taskId: string, intent: DevicePlacementIntent | undefined): Promise; /** 拒绝的**唯一** wire 体铸点(四个消费点各自用自己协议的发送姿势,但体的键集只有这一份)。 */ export declare function devicePlacementErrorBody(rej: DeviceRejection): { error: string; errorCode: string; }; //# sourceMappingURL=device-placement.d.ts.map