import { type DirectWritableRuleBehavior, type RemovalLiveness } from "./permission-rule-vocab.js"; import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedRule, type RuleBehavior, type RuleCandidate, type EffectivePermissionRule, type RemoveResult, type RuleWriteCommitted, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core"; import type { DurableRulePartitionProvider, PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core"; import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js"; /** * 规则分区**之外**的两只记录面 —— 它们是 server 自铸的(审批记录的 SQL/File 形、CC 导入票), * 与 [ref] 的分区族不是同一件东西,所以在这里单列一次:一个 backend 交出来的束 * ({@link PermissionRuleStoreBundle})与同意车道消费的束({@link RuleConsentStores})都含这两面, * 差的只是规则那一格躺的是 **durable 分区后端**还是**合成好的统一店**。 */ export interface RuleConsentRecordStores { /** core 的记录店面 + 一个**可选**的加项:`discardPendingRecord`(超帽拒绝时收掉 `prepareCcImport` * 已经落盘的那一条 pending 记录)。没有这个加项的后端只是留一条孤儿行,不影响任何判决 * (语义与「已确认的记录永不被收」的硬条款见 SQL 侧同名方法)。 */ approvals: RuleApprovalRecordStore & { discardPendingRecord?(recordId: string): Promise; }; tickets: { mint(input: { ticketId: string; principal: string; approvalId: string; purpose: RuleTicketPurpose; candidates: ReadonlyArray; ttlMs: number; }): Promise; /** 一次**认领**(不是「已完成」)。语义与「认领之后怎么办」见 {@link RuleConsentStores.tickets.release}。 * `purpose` = 车道绑定(A7 R1-F2):一条车道的口只认它自己铸的票,不符与「票不存在」同形。 * 🔴 **不收任何时刻量**([ref]):过期判定是店自己的事,由店那口钟(SQL=数据库的钟 / local=本进程的 * 墙钟)在**语句执行的那一刻**答。车道递一个时刻进来,就等于让副本的钟参与授权判决。 */ consume(ticketId: string, principal: string, purpose: RuleTicketPurpose): Promise; /** 把认领**放回去**(认领之后遇到不确定/瞬时失败时)。`claimId` = 本次认领的代次(A7 R1-F1 围栏: * 被顶替的旧持有者动不了现持有者的认领);`mustRemainValidMs` = 放回之后这张票至少还要能用多久 * —— 撑不过就**不算放回成功**(理由逐字见 store 侧 `release` 的头注)。 */ release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number): Promise; /** * [ref] A7 三态协议的三条口(CLAIMED → SETTLED / 三态快照 / 崩溃窗抢认领)。 * * 🔴 **必填,不给 optional**:它们是 local-import 车道正确性的承重件 —— 一个"没有 settle 的店"上, * 一次已经落地的导入会永远答不出终局(属主重放恒 409 到票过期)。把它们写成可选 = 让一台缺实现的 * 部署**静默**退化成那个形,而本仓对安全/权限轴的静默降级是明令禁止的。两个真实现(SQL / File)与 * 测试孪生各自实现同一份语义,缺一个当场编译红。 */ settle(ticketId: string, principal: string, claimId: string, outcome: unknown): Promise; peek(ticketId: string, principal: string, purpose: RuleTicketPurpose): Promise; reclaim(ticketId: string, principal: string, purpose: RuleTicketPurpose, minClaimAgeMs: number): Promise; }; } /** * 本车道**真正**依赖的三面(**结构**接口,不是 SQL 类)。 * * 🔴 为什么不直接吃 `PermissionRuleStores`(SQL 束):同意车道是**纯协议逻辑**,它对「行躺在 MySQL 还是 * PG 还是别处」一无所知也不该知道。吃结构接口有两个真收益:①单元测试能用 core 自己的 * `InMemoryDurableRulePartition` / `InMemoryRuleApprovalRecordStore` 驱动**同一段**代码(而不是为了测协议 * 去拉一个数据库);②将来若真出了 File 形规则店,本文件一个字不用改。SQL 束结构上满足本接口。 * * 🔴 `provider` 是 [ref] **合成后**的统一店(`createPermissionRuleStoreProvider` 的产物),不是 * backend 交出来的 durable 分区 —— core 的 `removePersistedRule` / `RuleConsentDeps.provider` 两口收的 * 都是它,而合成点只有 `main.ts` 一处:引擎接缝(`RunnerDeps.permissionRuleStore`)与本车道读的必须是 * **同一只**对象,否则「卡上给的候选」与「引擎放行时看到的规则」会分别站在两只店上。 */ export interface RuleConsentStores extends RuleConsentRecordStores { provider: PermissionRuleStoreProvider; } /** * 一个 backend 交出来的**完整**规则店束 = 同意车道要的三面 + boot 期审计要的窄读口。 * * 🔴 为什么它住在这里而不是某个 `plugins/*-sql.ts`:`StoreBackend.permissionRule()` 现在有**两个** * 实现(SQL 双方言 + local File),把返回类型钉成其中任何一个的具体类型都会让另一个只能靠断言挤进去。 * 结构接口是这条 seam 唯一站得住的形——两个实现各自满足它,消费点(main 装配 / 同意车道 / 撤销面 * 路由)一个字都不用知道行躺在哪里。 */ export interface PermissionRuleStoreBundle extends RuleConsentRecordStores { /** [ref] 的 **durable 分区后端**(`user` + `project` 两源)。**不是**引擎接缝本身: * `RunnerDeps.permissionRuleStore` 与 {@link RuleConsentStores.provider} 是 * `createPermissionRuleStoreProvider({ durable })` 合成出来的统一店,合成在 `main.ts` 一处发生一次。 * 分区构成(接不接 org / session)是**部署**决定,不是 backend 决定 —— 让 backend 各自合成,等于 * 日后接 org 分区时要挨个改后端,而两个后端合成得不一样时没有任何人看得出来。 */ durable: DurableRulePartitionProvider; /** [ref] §3:库里已有几只规则桶(一 owner 一桶)。boot 期休眠行审计的**唯一**依赖; * 为什么数桶不数规则、以及读失败必须响亮,见 `boot/permission-rules-audit.ts` 与两个实现处的注。 */ countBuckets(): Promise; /** * [ref] 保留期腿:清掉**可证已死**的导入票与孤儿 pending 审批记录,返回删掉的总行数。 * 谓词与「为什么不需要旋钮」逐字见 `plugins/permission-rule-store-sql.ts` 的同名成员。 * * 🔴 **可选**,而缺席是**如实登记**过的:File 车道(`permission-rule-store-file.ts`)的审批/票两面是 * **追加日志**,删一行等于重写整个日志 —— 那是一次压实(compaction)设计,与 SQL 的一条 DELETE 不是 * 同一件事,不在本条射程内。缺席的实际后果有界:File 车道 = 单用户本地形,两只日志的增长由**一个人** * 的导入次数封顶,不是多租户共享库那种无界增长。要做压实时在这里补实现,消费点(reaper)零改动。 */ reapExpired?(nowMs: number): Promise; } /** 导入票的存活窗。人从「看到预览」到「按下确认」是一次交互,不是一段会话——十分钟宽到不会误伤, * 窄到一张外泄的票不会常驻。运维旋钮暂不开(没有部署形要求它可调;要开时走 config.ts 的既有姿势)。 */ export declare const RULE_IMPORT_TICKET_TTL_MS: number; /** 可重试那一支通告给客户端的等待秒数(`state.rule_import_retry` 的 `retryAfterSec` / `Retry-After`)。 * 短 —— 它对应的是一次瞬时 store 抖动,不是排队。**单一真源在本文件**:放回认领时要用它判「这张票 * 还能不能撑过这段窗」,HTTP 层要用它填响应,两处用同一个数才谈得上「承诺兑得出来」。 */ export declare const RULE_IMPORT_RETRY_AFTER_SEC = 2; /** * **全部层合计**的候选上限(codex 交叉复审 round7 [high] 二,验真后修)。 * * 单层 16 KiB 只管住了**请求体**,管不住**下游工作量**:紧凑的合法规则(`Bash(ls)` 十个字符)能让三层 * 塞进上千条候选,而 core 的 `redeemRuleBatch` 是**逐候选**跑一遍「读店 + CAS 写」的串行循环 —— 一个 * 已鉴权的请求就能把连接池占满、把响应撑到兆字节。全局限流器默认是关的,拦不住它(一次准入就够了)。 * * 帽数的是 **core 已经解析好的候选**(不是我们自己再数一遍 JSON):零重复解析,而且数的正好是那条串行 * 循环的**真实**长度。位置在**铸票之前** —— 超限就没有票,那条昂贵的循环一次都跑不起来(代价只剩一条 * 已铸的 pending 记录,与任何一次没去兑付的 prepare 留下的残余同形)。 * 200 的取值:一份人手写的 CC settings 极少超过几十条;200 给了一个数量级的余量,又把最坏工作量钉在 * 「几百次 SQL」而不是「几万次」。 */ export declare const MAX_IMPORT_CANDIDATES = 200; /** 一批 local-import 的行数上限。**与 cc-import 同值同拒形**(413):帽住的是同一条东西 —— * `redeemRuleBatch` 那条**逐成员**「读店 + CAS 写」的串行循环(见 {@link MAX_IMPORT_CANDIDATES} 的 * 头注,理由一个字不变)。两处用同一个数不是巧合,是同一条论证的两个入口,所以直接派生而不是另挑一个。 */ export declare const MAX_LOCAL_IMPORT_ROWS = 200; /** * 一批 rows 的**总字节**上限(D5「防巨包」)。 * * 为什么行数帽不够:行数管住的是**下游工作量**(那条串行循环的长度),管不住**单次请求的体积** —— * 200 行 × (512 字规则 + 1088 字 scope + 512 字命令 + 一串出处回声)在纯算术上能到兆字节级,而这条口 * 是**已鉴权即可打**的写面。256 KiB 宽到装得下任何真实的规则店(一份人手养出来的规则集是几十条到几百 * 条一行的短串),窄到一次请求不能拿它当上传通道。超帽与行数超帽**同拒形**(413):对客户端来说处置 * 相同(把这批拆小再来),分成两个码只会让它多一条分支。 */ export declare const MAX_LOCAL_IMPORT_BYTES: number; /** * **认领租约**:一次认领持续超过这么久还没落定,就当持有它的那个进程**已经死了**(A7 崩溃窗恢复臂)。 * * 三问答(CLAUDE.md 行为面条款): * · **谁要**:无状态多副本部署。`consume`(认领)与 `settle`(落定)之间隔着整条兑付走;副本在这中 * 间被杀/被驱逐/OOM 是常规运维事件,不是异常。没有这条租约,那张票就停在 CLAIMED 直到 TTL 到期 * ——**人的确认凭空丢掉**,而客户端拿到的是一个永远重试不出结果的 409。 * · **谁受伤**:一次**还活着**但走得特别慢的兑付,可能在租约到期后被第二条腿抢过去重驱 ⇒ 同一批被 * 两个走者同时走。这不是猜测出来的安全边界:core 的批走契约**逐字**写着「resuming a partial walk * IS re-walking … two concurrent walkers converge on one dot set, zero double mints」——重叠的代价 * 是多跑一遍同样的重放,不是重复落规则。 * · **拿什么补**:取值远宽于一次真实的走(最坏 {@link MAX_LOCAL_IMPORT_ROWS} 次店往返),又远窄于 * 票的 TTL({@link RULE_IMPORT_TICKET_TTL_MS} = 10 分钟)—— 于是"崩了的票在它自己这一生里就能被 * 救回来",而不是等下一次 prepare。运维旋钮暂不开(没有部署形要求它可调)。 */ export declare const LOCAL_IMPORT_CLAIM_LEASE_MS = 60000; /** 一条规则的 wire 身份键(**态** + 规则文本 + {@link serializeRuleScope} 的判别式串)。两个面 * (预览 / 兑付回执)用**同一把键**,客户端拿它把两张表 join 起来。 * 🔴 `behavior` 自 7.67.0 起是这把键的一格(core 7.9.0 [ref]:规则的身份是三元组)—— 少了它,同文本 * 同作用域的 deny 与 allow 两行会被本口判成 `duplicate_row`,而它们是两条不同的规则。 */ export interface LocalImportRuleKey { behavior: RuleBehavior; rule: string; scope: string; } /** * prepare 面的**成员级拒因**(闭集,机读)。 * * 🔴 闭集而不是自由文本:客户端要按拒因分流(「这条是你本机的数据坏了」vs「这条规则本身不合法」是 * 两句不同的话),而自由文本只能被展示、不能被分支。`detail` 才是给人看的那一半。 * * 🔴 S-511 起**两条腿共用这一张表**(导入面的逐行预览 + 单步写口的拒):它们判的是同一件事,而判据 * 本身就是同一个函数({@link screenLocalImportRow})。给写口另起一张同形的表,就是给同一个问题立第二个 * 说话人 —— 漂的表现会是「同一条规则在一条腿上收、在另一条腿上拒」。写口的**方向**判据不在这张表里: * 它在端点上先判、有自己的整句文案,而且判完就把类型收窄掉(`permission-rule-vocab.ts` 的 * `DIRECT_RULE_WRITE_ADMITS`)⇒ 放宽态根本到不了本表所服务的那段代码。 */ export type LocalImportRefusalCode = /** scope 串读不出形(既不是 `global` 也不是 `project:<非空 root>`)。 */ "scope_unreadable" /** scope 的 project root 里有 bidi 控制符([ref]② Trojan Source 族):人看到的射程与机器判据用的 * 射程不是同一串 —— 一条规则的**作用范围**因此可以被伪装。 */ | "scope_bidi_controls" /** scope 的 project root 里有 **C0/DEL 控制字节**(codex 交叉轮 R9,验真后补)。与 bidi 分家成两个词: * bidi 说的是"你看到的射程是假的",这个说的是"这串根本不是一条路径"。挡它有三条各自独立成立的理由: * ①**跨后端行为分岔** —— PG 的协议编码器直接拒 NUL(整条 prepare 变 500),而 TiDB/File 收得下同一行; * ②它把本层比较键的分隔符假设(见 {@link LOCAL_IMPORT_KEY_SEP} 的注:"路径里不可能出现它")变成假的 * —— 去重与对账的键就此可被撞;③治理面的分页游标按 (scope, rule) 的串序做 keyset,控制字节会让那个 * 序不再是人以为的那个。 */ | "scope_control_chars" /** 规则文本过不了引擎的共享校验器,或它**不容纳它自称的那条命令**(覆盖门)。 */ | "rule_rejected" /** 文本门**答不了**这一行(引擎对调用方 bug 是抛,不是拒:命令缺席/非串)——那是行本身坏了, * 不是"你写的规则被拒了",故单独成词。 */ | "rule_uncheckable" /** 行**自相矛盾**:规则文本解析出来的 tool/match/command 与行上自称的对不上。一行 File 店行的这三 * 格本该是从 `rule` 派生的事实,对不上 = 这一行是拼出来的,不是店里出来的。 */ | "row_inconsistent" /** 同一个 (rule, scope) 在这一批里出现了两次。引擎会静默去重 —— 静默去重会让"我交了 20 条,只回了 * 19 条"变成一次无解的差异,所以在这里如实说出来。 */ | "duplicate_row" /** 过了本层全部判据,却没能成为引擎的候选(= 本仓与 core 的判据分了家)。**响亮**而不是静默丢: * 它意味着两侧的规则语法判据漂了,那是要人去看的事。 */ | "not_importable"; /** 兑付面的**成员级终态**(闭集,机读;`detail` 是给人看的那一半)。 * * 🔴 **`refused` 在本版的兑付面结构上到不了,这是如实登记而不是遗漏**:走到兑付这一步的成员,已经 * 在 prepare 过了全部内容判据 —— 于是 core 交回的 `refused` 行只剩店侧成因(读不出店 / 写面抛 / OCC * 用尽),而那些**都是"不知道落没落"**,不是"这条规则不行"。core 的 `RedeemedBatchMember` 拒绝臂 * 只带一个**自由文本** `reason`,把它按串去猜「这是裁定还是瞬时」正是本仓明令禁止的自造判读, * 所以一律落到 `indeterminate`(安全方向)。词仍留在表里是因为它进了**落盘回执的边界 schema** * (`LocalImportOutcomeSchema` 的 z.enum——`redeemed_outcome` 列回读同一张词表,旧行 / 别版本进程写下的 * `refused` 必须读得回),不是留一个死枝;⚠️ 预览面**不用**这个词:那一面的拒绝形是 * `admitted:false + LocalImportRefusalCode`({@link LocalImportPreviewMember}),两面词表**不同**。 * 收窄需要 core 给拒绝臂一个**带类型的** * 瞬时/裁定判别位 —— 已作为上游请托登记(server 不在下游造第二份判据,[ref])。 */ export type LocalImportMemberOutcome = /** 这一次真的写进店里了。 */ "persisted" /** 等价规则**已经在店**(引擎的 `deduped`)—— 同意早就生效,这次什么都不用做。 */ | "no-op" /** 这条被拒了(落盘回执 schema 的历史词位——现行两面都不铸它:预览面用 admitted:false + * LocalImportRefusalCode,兑付面见上方登记恒折 indeterminate;留词只为旧行回读)。 */ | "refused" /** **不知道落没落**:店在这一条上出了状况。如实编,禁猜(猜"成了"会谎报一次没发生的授权,猜"没成" * 会让人以为可以重来一次,而重来会撞上一条其实已经存在的规则)。 */ | "indeterminate"; /** `indeterminate` 的成因(闭集)。 */ export type LocalImportIndeterminateCode = /** 引擎在这一条上答了 refused,而拒因只有自由文本 ⇒ 落不了裁定,如实说"不知道"。 */ "store_refused" /** 兑付走本身没走完(本层抛出/中断)⇒ 这一条连走都没走到。 */ | "walk_failed"; /** prepare 面的一行。判别式联合:被收下的行**没有**拒因字段可读,被拒的行**必然**带拒因 —— * 「admitted 却带 reason」这种矛盾态在类型上不可表达。 */ export type LocalImportPreviewMember = { key: LocalImportRuleKey; admitted: true; } | { key: LocalImportRuleKey; admitted: false; reason: LocalImportRefusalCode; detail: string; }; /** 兑付回执的一行。 */ export interface LocalImportOutcomeMember { key: LocalImportRuleKey; outcome: LocalImportMemberOutcome; reason?: LocalImportIndeterminateCode; detail?: string; } /** 兑付回执整只 = **写进票行 `redeemed_outcome` 的逐字体**(回放同源:回放答的就是它)。 */ export interface LocalImportOutcome { members: readonly LocalImportOutcomeMember[]; /** 这只桶在兑付结束时的 OCC 版本(与 cc-import 的 `result.rev` 同义)。 */ rev: number; } /** A7 prepare 的产物。第二臂(`ticket` 整键缺席)= **零可导入行** —— 与 cc-import 的「预览无票」臂 * 同形同理由(没有可确认的对象,也就没有票;键缺席即答案,不编一个 null)。 */ export type LocalImportPrepared = { ok: true; preview: { members: readonly LocalImportPreviewMember[]; }; ticket: string; expiresAtMs: number; } | { ok: true; preview: { members: readonly LocalImportPreviewMember[]; }; ticket?: undefined; expiresAtMs?: undefined; } | { ok: false; reason: "too-many-rows"; rows: number; } | { ok: false; reason: "batch-too-large"; bytes: number; }; /** A7 redeem 的结果。 */ export type LocalImportRedeemed = /** `replayed` = 这份终局不是本次走出来的,是从票行里逐字读回来的(⑥ 丢响应重试那条路)。 */ { ok: true; outcome: LocalImportOutcome; replayed: boolean; } /** 五类票拒(不存在 / 别人的 / 错车道 / 过期 / 已用过而无从回放)—— wire 面同形 404。 */ | { ok: false; reason: "ticket-unusable"; detail: string; } /** 裁定性拒绝(记录不存在 / 形太旧 / 载荷被改写)。`retryable` 与 cc-import 同义。 */ | { ok: false; reason: "record-unusable" | "payload-mismatch"; detail: string; retryable?: true; } /** 另一条腿**正持着**这张票在走,而它在有界等待窗里没走完 ⇒ 稍后再来(409)。**不驱不猜**: * 抢一个还活着的持有者是拿"两个走者"去换"少等两秒",方向不对。 */ | { ok: false; reason: "in-flight"; detail: string; }; /** 一次导入 prepare 的产物(HTTP 200 体的素材),或**超帽**的拒绝(见 {@link MAX_IMPORT_CANDIDATES})。 * 5.58([ref]):零 importable 候选 ⇒ core **不铸** `approvalId`(「absence says so」)⇒ 无票可铸, * 第三臂把 preview(全 skipped 的理由就在里面)原样交给人看——不是错误,是「没有什么可确认」。 */ export type RuleImportPrepared = { ok: true; preview: ImportPreview; ticket: string; expiresAtMs: number; } | { ok: true; preview: ImportPreview; ticket?: undefined; expiresAtMs?: undefined; } | { ok: false; reason: "too-many-candidates"; candidates: number; }; /** 导入 redeem 的结果。wire 口径写死(端点=http/routes/rules.ts):票拒五类(自造 / 别人的 / 错车道 / * 过期 / 已用过)与裁定性拒绝(record-unusable / payload-mismatch)同形 404(零存在性 oracle,顶注 ①); * `retryable === true` ⇒ 503 `state.rule_import_retry`;`state-unreadable` ⇒ 503 * `state.rule_ticket_unreadable`。分类保留只为服务端日志/诊断与上述三分支的判据。 */ export type RuleImportRedeemed = { ok: true; result: { members: readonly RedeemedBatchMember[]; rev: number; }; } /** `retryable` = 这次失败**没有裁定任何事**(store 抛错 / CAS 冲突),认领已放回,属主原样重试即可。 * 它与裁定性拒绝(载荷被改写、记录不存在)分家是承重的:把两者塌成一格,一次数据库抖动就会被 * 客户端读成「这张票永远不能用了」。⚠️ wire 面**不再**折 404:`retryable === true` ⇒ 503 * `state.rule_import_retry` + Retry-After(http/routes/rules.ts;走到这一步的调用方已赢下 principal * 绑定的认领,对他说「稍后再试」零存在性泄露——五类票拒仍同形 404,一个字没变)。 */ | { ok: false; reason: "ticket-unusable" | "record-unusable" | "payload-mismatch"; detail: string; retryable?: true; } /** [ref] 独立复审 件2(codex 轮二、轮三改形):首跳 `consume` 抛错后**结局歧义**的那一支 —— 认领 * 可能已提交而代次丢了,可能根本没提交,甚至可能**还在途、稍后才提交**(轮三 R3-F2:一次事后 * peek 读到 MINTED 证明不了在途的写不会迟到落地,同一场店故障也常让 peek 一起挂)。终局宣布不了 * (可能把活票说死),「票还能用」承诺不了(可能撒谎),断言「在途」也不行(那是冒充知道)—— * 唯一诚实的形是**独立的歧义响应**(503 `state.rule_ticket_unreadable`):只说「现在答不了,稍后 * 拿同一张票来读个诚实答案;届时若报 not found 就重新 prepare」。`retryable?: never`:这一臂 * 永远不许携重试承诺(它存在的全部意义就是不承诺),声明出来只为联合型上的读点可编译。 */ | { ok: false; reason: "state-unreadable"; detail: string; retryable?: never; }; export declare function precheckCardRuleText(text: string, command: string): EditedRuleTextPrecheck | undefined; export type CardRuleRedemption = /** 人**点了卡上的某条候选**:文本用来在引擎铸的候选表里定位下标。 */ { kind: "offered"; ruleText: string; } /** 人在卡上**手改了规则文本**(core 的 card-edit 面:同一条已鉴权确认通道,`editedCandidate`)。 */ | { kind: "edited"; text: string; } /** [ref]:人勾了**合取批**——传的是**行素材上人看到的那只 batch 整只**(不是裸 index), * 等式两侧(看到的 / 重铸可兑的)在本车道一个函数里对齐(防漂等式,见 batch 分支行注)。 */ | { kind: "batch"; offer: Extract; }; /** * P-39 —— 一条已落地批成员的**归属锚**(offer 空间)。 * * `memberIndex` = 这条规则在**人看到的那只 batch offer** 的 `rules[]` 里的下标。它与请求体的 * `persistRule.batchOfferIndex`(= 帧上 `ruleOffers` 的下标)是**同一个空间**的两级坐标: * 「第几只 offer / 那只 offer 的第几个成员」。core 的 `RedeemedBatchMember.candidateIndex` * (candidate 空间)**刻意不透**:wire 面只见一个下标空间是 cli [ref] 的定形理由(三端零映射)。 */ export interface RedeemedBatchAnchor { memberIndex: number; rule: string; } /** 卡道兑付的结果。`rule`(旧键名 canonical 已改;single 臂顶层键,批臂在 `members[].rule`)= * **真正落盘**的那条规则文本(编辑臂上它可能与人敲的原字节不同 —— * core 会把 `Bash(adb *)` 规范成 `Bash(adb:*)`;界面要回显的是这一份,不是输入框里的那一份)。 */ export type CardRulePersisted = { ok: true; kind: "single"; rule: string; rev: number; alreadyRedeemed: boolean; } /** [ref] 批臂:全体成员落地(persisted/deduped 都计——等价规则已在店=同意已生效)。 * `rules` = 规范文本,**展示序**(= 兑付时递进来的那只 batch offer 的成员序,壳逐条回显)。 * * 🔴 **P-39(cli [ref] 请托 / [ref] 定形):展示序自本版起是承诺,不再是碰巧。** * 修前 `rules` 直接是 `redeemRuleBatch` 的 members 走序 = **重铸批**(下面 `prepareCardApproval` * 在兑付这一刻重铸的那只)的成员序;而人看到的是**呈卡那一刻**的批,两侧的等式是**集合**等式 * (防漂等式逐字「序无关」,见 batch 分支行注)⇒ 序从来不在等式里,「回执序=展示序」只是当下 * 两次 `suggestRulesForCommand` 恰好同序的副产物。现在按 {@link CardRuleRedemption} 批臂递进来的 * 那只 offer 的成员序**显式重排**,于是它与壳渲的那张卡逐条对得上,不依赖引擎两次铸形同序。 * * `members` = 逐条**归属锚**(index 与 `rules` 对齐):`memberIndex` = 这条规则在那只展示批 * `rules[]` 里的位置 —— **offer 空间**,candidate 空间刻意不出现在本类型上(cli [ref] 裁词: * 三端零映射,candidate 空间留在 core 内部)。 * * ⚠️ **可选 = 诚实缺席**:落地成员与展示批成员对不上号(集合等式过了却出现 core 契约级的成员 * 漂移 —— dist/core 版本不同步那一形)时本键**整键缺席**,`rules` 退回 members 走序(= 修前字节)。 * 编不出锚就别编一个,消费方读到缺席即知「这次归属证不出来」;这条缺席**响亮**(warn 一条)。 */ | { ok: true; kind: "batch"; rules: readonly string[]; members?: readonly RedeemedBatchAnchor[]; rev: number; } | { ok: false; reason: "no-candidates" | "unknown-candidate" | "confirm-refused" | "redeem-refused" /** [ref]:编辑面**在这只卡上用不了**——卡没有绑定锚(`boundInputHash` 缺席 ⇒ 无从证明「我编辑的 * 是显示了这条命令的那张卡」),或这台部署没有打开编辑面。与下面那个词刻意分家:这个说的是 * 「别再渲那个输入框」,那个说的是「你写的这条不行,改一下再来」——壳要做的事完全相反。 */ | "edit-unsupported" /** * [ref]:人写的文本**被引擎的门拒了**——共享校验器不认这个拼写,或它不覆盖本次被裁决的命令 * (「编辑可以更宽,但不许换成另一条授权」)。 * * 🔴 **射程自 [ref] 起收窄**(core 5.57.0 `precheckEditedRuleText` 到货 —— 下面那条上游请托兑现了)。 * 原话保留作史:「兑付发生在裁决**落定之后**,所以拿到这个词时这张卡已经消费掉了;想做到『同一张 * 卡上改了再来』需要一次**裁决之前**的预检,而那要求 core 把编辑面的校验(含它自己的拼写规范化) * 导出成一只纯函数 —— 在本层照抄一个更严的校验器会把 `Bash(adb *)` 这条**成因形**当场误拒。」 * * 现况:那只纯函数到货了,于是**文本能独立判定的那一半**({@link precheckEditedRuleText} 的语法门 * + 覆盖门)搬到了**裁决之前**——`POST /v1/tool-approvals/:id/respond` 在结算之前就 400,卡**还在** * (与 `persistRule.rule` 的形/上限 400 同一姿势,那条 400 早就是这个形)。所以壳今天可以渲 * 「改一下再来」,而且真能来。 * * 这个词因此只剩**纵深**的那一半:预检被跳过的路径(治理档 / 无车道素材 / 无属主 / ctrl+g 编辑放行 * 各自有自己的词,不到这里)与「预检过了但记录级仍拒」的残余竞态。到这个词时那句老话仍然成立 * ——卡已消费、同一把 approvalId 再回决是 404。 */ | "edit-rejected" /** * core 7.0.0([ref]② BREAKING B2,[ref] §4.4):silent-global 死——`prepareCardApproval` * 在**既无显式 scope 又无 cwd**时响亮拒(`config.missing_scope`),不再默落 global。本仓的 * scope 素材=铸卡时 `ruleScopeRootFor` 咨询一次的落点(行列 `rule_scope_root`);resolver 答不出 * root 的车道(host lane 未注册 launch dir)在 v7 下就是这一形。**刻意不在兑付时刻补一个 * `process.cwd()`**:core 契约明写 cwd=被裁决调用的工作目录、never inferred from the process—— * server 进程的 cwd 不是那个坐标,编一个等于把规则锚到错误的射程上(比修前的 global 更糟)。 * 出路=部署接 `ruleScopeRootFor`(host lane 注册 launch dir),或调用方在回决体带显式 scope。 * 裁决本身照常生效(与全族同律:规则没存上不改判这次放行)。 */ | "scope-unresolved"; detail: string; /** [ref]:core 的**共享校验器**refusal code(`RuleRejectCode`,开集透传)。覆盖门拒时**缺席** —— * 这条在场规则与 core `confirmRuleApproval` 的 `edit_rejected` detail 逐字同源(同一个函数体)。 * 🔴 不 switch、不翻译、不折叠:词表属主是 core,本仓照转。 */ code?: string; }; /** 一层用户交上来的 settings(HTTP 载荷已 zod 校验过的形)。 */ export interface RuleImportLayerInput { layer: ImportedSettingsLayer; path: string; root: string; content: string; } /** * scope 的 **wire 形**:`global` | `project:` 的判别式串([ref] v2 §6 F5「显式成文」)。 * * 🔴 为什么是一个串而不是把 core 的 `{kind, root}` 对象直接上 wire:这一份表示要同时当**列举结果里的 * 一列**、**查询参数**、**删除请求体的一个字段**和**分页游标的一部分**。四处若各用各的形,「删掉我刚才 * 列出来的那一行」就变成一次跨形状翻译 —— 而那正是最容易漂的地方。判别式串在四处逐字相同,而且 * `project:` 前缀让「这条规则是项目内的」在一眼扫日志时就成立。 * * `root` 里可以含冒号(路径合法),所以只切**第一个**冒号 —— 用 `split(":")` 取 [1] 会把 * `project:/a:b` 悄悄截成 `/a`,那是一条**放宽面**上的静默改写(更短的 root 覆盖更多 cwd)。 */ export declare function serializeRuleScope(scope: RuleScope): string; /** * 🔴 判别式串的**上限,由铸侧的尺派生**([ref],2026-08-19 合并重扫 confirmed —— 修的是「两个数字 * 各挑各的」这件事本身,不是把某个数字调大一点)。 * * 病:铸侧 `cardRuleScopeRoot` 对 root 长度零把关(入口尺 = `isValidCwd` 的 {@link MAX_CWD_CHARS}), * core 与两个店都只校验 `root.min(1)` ⇒ 写面全程无阻;而 `DELETE /v1/rules` 的体 schema 此前独立写死 * `scope: max(1088)`,且 safeParse 排在 `parseRuleScope`/权限判据**之前** ⇒ 超限 root 的规则 * `GET /v1/rules` **列得出**、按同一串 DELETE **恒 400**。全仓只有一个 `lane.removeRule` 调用点(直删 * 店行是无墓碑硬删、会被 sync 的 join 复活),所以那是**一条已列出的常驻放行规则在受支持接口上不可撤**; * 唯一的钝器补偿是 `PERMISSION_RULES_ENABLED=false`(关掉全部规则,两口自身随之 501)。 * * ⚠️ 可达性在 [ref].F-A 之后**变严重**:那批把铸侧出口从 `realpath` 改成 `path.resolve`,而 realpath * 要求目录真实存在(macOS PATH_MAX=1024 ⇒ 宿主上 root 恒 ≤1023,结构性免疫);`path.resolve` 纯词法、 * 不碰盘 ⇒ **任何宿主上** 1081..4096 字符的注册 cwd 都能铸出这样一条规则。 * * 取值 = `"project:"` 前缀 + 铸侧 root 的入口尺。`path.resolve` 对「已绝对、无 `..` 段」的路径不加长 * (`isValidCwd` 两条都要求),故 {@link MAX_CWD_CHARS} 是 root 的真上界。**两侧从此同源**:谁改铸侧 * 的尺,撤侧自动跟。 */ export declare const MAX_RULE_SCOPE_CHARS: number; /** durable 面的**二员**窄型(core 7.0.0 [ref] §4.3:consent 面三员、durable 面二员分治)。 * wire 判别式串只拼得出这两员(session 刻意无 wire 拼法——它的家在引擎的会话 overlay,不在持久店), * 所以 parse 产物与本仓筛选行都钉在这个窄型上:`.root` 访问零猜测,session 员漏进来是编译错不是运行病。 */ export type DurableRuleScope = Exclude; /** {@link serializeRuleScope} 的逆。读不出形 ⇒ `undefined`(调用方 400,绝不猜一个 global 出来 —— * 猜 global 会把一次「删项目内规则」的请求变成一次删不掉任何东西的 no-op,或者更糟)。 */ export declare function parseRuleScope(text: string): DurableRuleScope | undefined; /** 一条规则在 wire 上的行 = 一个逻辑 (rule, scope) 对([ref] v2 §6 F5)。 * `adds` **如实上 wire**、不折叠成单值:同一条规则可以被批准过多次(不同 dot、不同来源、不同时刻), * 折成「一个 addedAt」会让「这条规则是我导入的还是我点过卡的」在治理面上不可分。 */ /** * 一条 wire 行与一份身份三元组是不是**同一条规则**。 * * 🔴 **三元组,不是 `(rule, scope)` 二元组**(异源复核 [medium],验真后修):core 的 `sameRuleIdentity` * 头注逐字 ——「a comparison that forgot the behavior member would let a delete aimed at an allow take the * same-text deny」。漏掉 behavior 的比较在 wire 上的表现不是「删错行」,是**报错行的死活**:撤一条从来 * 没有过的 `allow` 会因为看见同文本的兄弟 `deny` 而答 `stillLive: "yes"`,那句话说的是另一条规则。 * * 凡「这份清单里有没有这条规则」一律走这一只(撤销口的存活回读 / 单步写口的写前问与写后读回)—— * 同一个问题有两个说话人,迟早会给两个答案。 */ export declare function sameWireRuleIdentity(row: PersistedRuleWireRow, identity: { behavior: RuleBehavior; rule: string; scope: string; }): boolean; export interface PersistedRuleWireRow { /** core 7.9.0([ref])—— 这一行是哪一态的规则(`deny` / `ask` / `allow`)。**身份的一格**: * 规则的身份是 (behavior, text, scope) 三元组,同文本不同态是两条不同的行(core `sameRuleIdentity`), * 所以它既在列举面上,也在**导入/撤销的输入面**上必填 —— 少了它,一次撤销会瞄准同文本的另一态。 */ behavior: PersistedRule["behavior"]; rule: string; /** {@link serializeRuleScope} 的判别式串。 */ scope: string; tool: PersistedRule["tool"]; match: PersistedRule["match"]; /** 去规范化的命令(core 的匹配器信的就是它 + `match`,不是重新解析 `rule`)。 */ command: string; adds: PersistedRule["adds"]; /** [ref](core 7.5.0):这一行来自哪个源。**由 `scope` 派生**(`global`⟺`user`、`project`⟺ * `project`、`session`⟺`session`),不是行上另存的第二个字节 —— core 的 `ruleSourceOf` 是整条映射。 * 今天本部署只接 durable 一格分区 ⇒ 值域实为 `user | project`。 * * 🔴 为什么上 wire 而不是丢掉:统一读把这一格**交到手里了**,治理列举知道而不说 = 一次 wire 谎言的 * 反面(客户端只能自己去 decode `scope` 猜),而 org 分区接线的那天它会立刻变得承重。 */ source: EffectivePermissionRule["source"]; /** [ref]:这一行的**站位** —— `live` 或 `shadowed-by-org`(org deny 盖到它的命令模式上)。 * 今天无 org 分区 ⇒ 恒 `live`;这一格正是「接了 org 分区之后治理面必须看得见的那件事」。 */ status: EffectivePermissionRule["status"]; } /** * 一条**可导入**的规则行 = 列举行**减去两格派生键**。 * * 🔴 为什么导入输入面不收 `source`/`status`:两格都是**派生**的 —— `source` 由 `scope` 派生 * (core 的 `ruleSourceOf` 是整条映射,d.ts 逐字「A row never carries a second source byte that could * drift from its scope」),`status` 由 org 分区的遮蔽判据派生。一个调用方递进来的 `source` 只会与它 * 自己的 `scope` 漂,而漂的落点是治理面上「这行到底归谁管」。同族纪律 core 自己在卡面投影上也写过 * (「derived HERE, at projection time … never accepted from a caller」)。 * * 这不是两份平行形状:`Omit` 让「导入面 = 列举面 − 派生键」是一条**机器判据**,列举行加员时导入面 * 自动跟随、`LOCAL_IMPORT_ROW_SHAPE_PIN`(路由侧)当场编译红。 */ export type ImportableRuleRow = Omit; /** 列举结果。`rev` = 这只桶的 OCC 版本,游标绑它(见路由侧的游标注)。 */ export interface PersistedRuleListing { rows: PersistedRuleWireRow[]; rev: number; } /** * 撤销面两口寻址的**桶**([ref] 收官件,core 5.24.0 提货批 [ref])。 * * 裸串 = principal 简写(既有调用方逐字不变);结构形 {@link RuleOwner} 多出 `{kind:"local-owner"}` * —— 身份缺席的单机形自己那只桶。这不是 server 发明的第二种寻址:core 的 * `PermissionRuleStoreProvider.forLocalOwner()`([ref] §4.5)一直就有这只桶,缺的是**撤销入口 * 叫不出它的名字** —— 我方 [ref]① 请托、core 5.24.0 把 `removePersistedRule` 的 `principal` 入参扩成 * `string | RuleOwner` 兑现(d.ts 那段注逐字写着「downstream request, 2026-08-10」)。 * * 🔴 **绝不用哨兵串**表示身份缺席(core 的 `RuleOwner` 头注就是这条纪律的出处):`forPrincipal("")` / * `forPrincipal("local-owner")` 都会让一个**编出来的名字**流进身份面,而身份面上编的名字与真名在下游 * 不可分。判别式联合是唯一诚实的形。 */ export type RuleBucketRef = string | RuleOwner; /** * S-511 —— 单步写的结果(闭集三臂)。 * * 🔴 三臂**刻意不塌成布尔**:「落了」「早就有了」「不知道落没落」对调用方是三种不同的处置,而把第三种 * 折进前两种里的任一边都是编一个结论(猜"成了"会谎报一次没发生的写,猜"没成"会让人以为可以重来)。 * 与导入面的成员级四态({@link LocalImportMemberOutcome})同一条纪律,词更少是因为本口只有一行。 */ export type RuleWritten = /** `persisted` = 这一次**真的往店里写了**;`no-op` = 这条身份**此前就站在店里**,本次调用**一个字都没写** * (幂等重写)。两支都带 `row` —— 它是**从店里读出来的**那一行,不是我方按入参拼出来的回声。 * * 🔴 两个词的判别位是「**这次调用有没有写**」(codex 对抗复审 F1,验真后修)。 * S-511 时这条口借的是导入面的兑付腿,而那侧的 `deduped` 只说「等价规则此前已站」、**仍然落了一枚新的 dot** * ⇒ 那一格必须如实报 `persisted`,否则一个照常重试的客户端会让 `adds` 台账无界增长、桶的 rev 逐次推进 * (在翻页的治理面全体重列)。**S-525 换成 core 的直写接缝之后这条歧义没了**:`addPersistedRule` 的 `no-op` * 契约逐字是「等价规则已经在这只桶里活着,**没有铸第二枚 dot**」——与本口 `no-op` 承诺的那句话逐字同一件事, * 所以现在两个词与 core 的两个词一一对应,不再需要翻译。 */ { status: "persisted" | "no-op"; rev: number; stillLive: Extract; row: PersistedRuleWireRow; } /** 同上两个词,但**写后读回时它已经不在了** —— 一次合法并发(自己的 / operator 的撤销挤在了「引擎落了 * dot」与「读回那一行」之间)。 * * 🔴 B3(core fable 复审 2026-09-20):这一形**不是** `indeterminate`。core 已经答了 `added` 并把 `rev` * 交到手里 —— 「写没写」这一问有答案;没答案的是「它现在还站着吗」,而那是**另一个问题**,撤销面早就有 * 一只键在回答它({@link RemovalLiveness} 三词)。折成 `indeterminate` 的代价有名字:那条腿的处置是 * 「retry」,而照着重试会把**刚刚被人撤掉的收紧重新立起来**(core `AddResult.failed` 头注逐字警告过 * 这条路)。 * * 🔴 `row` 在这一支**缺席**,而缺席由 `stillLive` 判别(不是「未知」):没有活行可端的时候编一行出来 * 才是谎 —— 亲读 core 的 `AddResult`,`added` 臂只有 `{status, rev, dot}`,**刻意**不带行(头注:之后 * 落地的撤销是「`list()` 答的普通后续事件」),而本仓的行形带着 `status: "live"` 这一格,给一行已经 * 不在的规则铸它就是第二个谎。 */ /** 🔴 `"unknown"`(7.93.0 合并复审 codex 验真后加):引擎已确认 `added`(`rev` 在手),而**写后读回本身抛了** * (店读不出来)。「写没写」有答案、「现在站不站着」没人观察 —— 与撤销口 `no-op` + `unknown` 同词同义: * **不要重试**(重发只会 `no-op`),用 `GET /v1/rules` 对账。此前这一形落域外通用 500 `internal.error`, * 与「写前读库失败」(零写,重发安全)走同一个异常出口 —— 两种强度一句话,正是 `committed` 要拆开的那对。 */ | { status: "persisted" | "no-op"; rev: number; stillLive: Exclude; row?: undefined; } /** 裁定性不收(内容判据 / 方向判据)。`detail` 是给人看的那一半,`reason` 是给客户端分流的那一半。 */ | { status: "rejected"; reason: LocalImportRefusalCode; detail: string; } /** 终态写失败。`detail` 是给人看的那一半;`committed`({@link RuleWriteCommitted},core 的两词闭集)是 * 给机器分流的那一半 —— 它回答的是**调用方下一步该做什么**。 * * 🔴 **词从 core 的 `AddResult.failed` 原样接过来,不从报文里猜**(core 7.26.0 [ref])。此前本臂叫 * `indeterminate` 且只带 `detail`:core 把「店自己答了、确知零写」与「apply 抛了、落没落不知道」两种强度 * **只写在散文里**,本仓于是把两者一律折成「不确定」+ 端点 503「retry」。代价是那句 retry 对两种强度**一句 * 都不对**:`"no"` 的那半确知零写、重发同一份体是安全的(而旧文案让客户端以为要先对账);`"unknown"` 的那半 * 可能已经落了,而盲重试会**再铸一枚 dot**、把刚被人撤掉的收紧重新立起来(core `AddResult.failed` 头注逐字 * 警告过这条路)——那正是本臂最需要说清的一句话,旧形却把它和安全的那半说成了同一句。 * * 🔴 臂名随词改成 core 的 `failed`(不再叫 `indeterminate`):拿到判别位之后,「不确定」对 `"no"` 这半是 * **假的**,而一个名字与它所含的一半相反的臂,下一个读它的人一定读错。两个词表合成一个,规则少一条。 * * **「写落了随后被撤」不在本臂** —— 那一形有答案,见上面 `stillLive:"no"`。 */ | { status: "failed"; detail: string; committed: RuleWriteCommitted; }; export interface RuleConsentLane { /** 卡道兑付口:一次 ask 决议携规则确认 ⇒ prepare→confirm→redeem 一气呵成。 */ persistCardRule(input: { principal: string; toolName: string; /** 被裁决的命令**原字节**(与 ask 上的那份同源;候选臂的规则文本由引擎从它铸,编辑臂的覆盖门也拿它判)。 */ command: string; /** 这次要兑的是候选表里的哪一条,还是人手改的自由文本(两个显式臂,见 {@link CardRuleRedemption})。 */ redemption: CardRuleRedemption; toolCallId?: string; /** 卡上的实参摘要。编辑臂的**必要条件**:core 要求编辑回显它(「我编辑的是显示了这条命令的那张卡」), * 缺席 ⇒ 编辑臂 fail-closed 拒(候选臂不受影响,那条路上它只是记账用)。 */ boundInputHash?: string; scope?: RuleScope; }): Promise; /** 导入道:读层 → 预览 + pending 记录 + 票。 */ prepareImport(principal: string, layers: readonly RuleImportLayerInput[]): Promise; /** 导入道:原子消费票 → confirm → redeemRuleBatch。 */ redeemImport(principal: string, ticket: string): Promise; /** [ref] A7 直通道:逐行校验 → 预览 + pending 记录 + 票(零可导入行 ⇒ 无票)。 */ prepareLocalImport(principal: string, rows: readonly ImportableRuleRow[]): Promise; /** [ref] A7 直通道:三态兑付(首兑 / 逐字回放 / 崩溃窗重驱),终局落进票行。 */ redeemLocalImport(principal: string, ticket: string): Promise; /** * 撤销面读半场:列出一只桶名下**活着**的规则(墓碑已折算)。 * * 排序 = (scope, rule) 字典序,**确定性**:分页游标是 keyset 形,而 keyset 的全部前提就是「同一份 * 数据每次以同一个顺序出现」。core 的 `list()` 不承诺顺序(SQL 形按写入顺序、File 形按文件内顺序), * 所以序在这里定,不在店里。 * * 入参裸串 = principal 简写(既有调用方逐字不变);{@link RuleBucketRef} 的结构形另开 local-owner 桶。 */ listRules(bucket: RuleBucketRef): Promise; /** * 撤销面写半场。**恒经 core `removePersistedRule`**([ref] §2 裁定)——它产墓碑,而墓碑是 * `screenRuleSyncState` 的筛子保证「别的副本不把这条规则回灌回来」的唯一凭据。直接删 SQL 行/文件行 * 是**无墓碑硬删**:sync 的 join 会让它复活,而运维以为自己收回了权限。 * * 返回值逐字是 core 的 `RemoveResult` 三态(removed / no-op / failed),**不塌**: * `removed.stillLive` 自 core 7.25.0 起是**三词**(`"yes"` / `"no"` / `"unknown"`)而不是布尔 —— * `"yes"` 意思是「墓碑落了,但本次调用期间又落了一次新的批准,规则按 add-wins 仍然活着」(一个**有名字的 * 真结果**,不是异常,更不是「撤销完成」);`"unknown"` 意思是**回答这一问的那次读回没做成**,它与 `"no"` * 的区别正是这个域唯一不肯说的那句话:「我们看不见规则」永远不等于「没有规则」。本车道**原样**交出去、 * 不折叠(折 `unknown` 进 `no` 就是把「没看见」渲染成「不在了」)。 * * `principal` 裸串 = principal 简写(既有调用方逐字不变);{@link RuleBucketRef} 的结构形让 * local-owner 桶**真可撤**(core 5.24.0 起 `removePersistedRule` 的入参 union 扩,[ref] 收官件)。 */ removeRule(input: { principal: RuleBucketRef; behavior: RuleBehavior; rule: string; scope: RuleScope; }): Promise; /** * S-511 撤销面写半场的**对偶** —— 一条**收紧**规则(deny/ask)的单步写入,无同意仪式往返。 * * 它为什么可以没有仪式,与 `removeRule` 为什么可以没有仪式是**同一条理由**(core * `removePersistedRule` 头注逐字:「a host may narrow on a user's behalf, it may not widen」)。放宽 * 方向在类型上就到不了这里:`behavior` 收窄成 {@link DirectWritableRuleBehavior}。 * * 🔴 **一行都不自造**:行的 tool/match/command 三格是规则文本的**派生事实**,由 core 的 * `parseRuleText` 铸;**内容判据**与导入面走**同一个函数**({@link screenLocalImportRow}),于是本口与 * 导入口对「一条规则能不能进店」永远给同一个答案。**落店**(S-525 / core 7.25.0)走 core 的**记录级 * 直写接缝** `addPersistedRule` —— 一步、无票、core 自己铸 `origin:"user"`、`allow` 在类型上不可拼; * 此前借的是导入面的两跳票仪式,代价是出处词恒 `imported-cc` + 每次成功的写白落两行台账。 * * `principal` 是裸串(与 prepare/redeem 两腿同形);local-owner 桶**不在本口**上 —— 那只桶没有 * 「谁在写」这个主体,而本口的授权基础就是已验明的 principal。 */ writeRule(input: { principal: string; behavior: DirectWritableRuleBehavior; rule: string; scope: DurableRuleScope; }): Promise; } export declare function createRuleConsentLane(stores: RuleConsentStores, opts?: { ticketTtlMs?: number; newTicketId?: () => string; /** [ref]③ 的单调钟 seam。缺省 `performance.now()` —— 本仓既定单调域。 */ monotonic?: () => number; /** A7 有界等待的睡眠 seam(缺省真定时器)。等待窗与轮询步长都是**实现常数**,不是旋钮 —— * 开成 opts 只是为了让"输者到底等没等"这件事可被确定性地钉住。 */ sleep?: (ms: number) => Promise; }): RuleConsentLane; //# sourceMappingURL=rules-consent.d.ts.map