@sema-agent/client-core 0.76.1 → 0.77.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.
- package/CHANGELOG.md +33 -0
- package/README.md +5 -1
- package/dist/approvalsStreamLiveCapability.js +5 -1
- package/dist/deviceExecutorManagementCapability.js +2 -2
- package/dist/executionLaneCapability.js +5 -1
- package/dist/hitl/persistedRulesWire.d.ts +221 -1
- package/dist/hitl/persistedRulesWire.js +324 -18
- package/dist/hitl/sessionPolicyWire.d.ts +275 -0
- package/dist/hitl/sessionPolicyWire.js +464 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/memoryEntriesWire.js +7 -0
- package/dist/peerLaneCapability.js +5 -1
- package/dist/permissionRulesWriteCapability.js +5 -1
- package/dist/request/taskRequest.d.ts +101 -1
- package/dist/request/taskRequest.js +457 -80
- package/dist/sessionPolicyCapability.d.ts +68 -0
- package/dist/sessionPolicyCapability.js +103 -0
- package/dist/sqlEngineCapability.js +5 -1
- package/dist/webSearchBackendCapability.js +5 -1
- package/dist/writeProtectionCapability.js +5 -1
- package/docs/INTEGRATION-CLIENTS.md +86 -8
- package/package.json +1 -1
- package/dist/mcpProbeCapability.d.ts +0 -84
- package/dist/mcpProbeCapability.js +0 -124
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,39 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.77.0(2026-09-20)
|
|
53
|
+
|
|
54
|
+
> 主题:**请求装配逐键去向 + 两条写面归包 + 读器族口径整族改齐**(🔴 **minor**:四条**行为面 BREAKING**(型面零变)+ 三处闭集读数改口;公面值导出 1122 → **1143**;peer 地板不动 `>=9.8.1`)—— 请求装配 print 车道补座 + 逐键去向回执 + 表外键响亮拒(CC-101)· 会话策略写端口(CC-105)· 持久规则单步写面(CC-103)· workflow 子代面板行代际三向恢复真三向(CC-104,零改码)· 能力位读器族对非对象 caps 与原型链键的口径整族改齐 · 两处订正(F-A / F-B)。**成文改口段见 §79 79y,按表态制点名三端。**
|
|
55
|
+
|
|
56
|
+
### 🔴 BREAKING(行为面;型面零变)
|
|
57
|
+
|
|
58
|
+
- **入参在场的表外键 / 表外车道词 / 非对象 `settings` / `rewind` 表外子键 ⇒ 构造期 `TypeError`,点名键名不回显值**(CC-101)。修前静默丢:请求装配的 stamp 门把四种「不带」折成同一个 `false`,表外键连这个 `false` 都轮不到 —— 无人值守车道传 `systemPrompt` / `outputSchema` / `maxCostUsd`,产出只剩 `{objective, sessionId}`,调用方拿不到任何位能答「少了哪个键、为什么少」。为什么抛:不认识 = 分不开「打错的在座键名 / 真请求位而车道表没登记 / 多余键」,前两种可以是**收紧方向**的声明(排除工具 / 关记忆 / 花费上限)—— 安静丢掉它,这一次运行就比声明人以为的更宽;与本包对值轴的同一病(记忆声明坏拼写抛)同一处置。编译期只挡新鲜字面量直写,spread 片段 / 落变量都过,而三端真实调用点全是 spread ⇒ 运行期判官不多余。`undefined` / `null` 视为缺席不拒。
|
|
59
|
+
- **print 车道从此带 `systemPrompt` / `reasoningEffort` / `excludeTools`**(修前传了也丢;后者是收紧方向的声明,传了只进回执 = 端不读回执就把用户排除的工具放回)。
|
|
60
|
+
- **`maxCostUsd` 坏值(非数 / 非有限 / 非正)构造器 `TypeError`**:上游对坏值不是 4xx,是安静忽略并回落部署上限 —— 无上限部署 = 这一次运行没有花费闸。数字串请在端侧先解析成数。
|
|
61
|
+
- **`rewind` 改四键按名投影**(`resumeAt` / `resumeAtMode` / `rewindFiles` / `rewindFilesTo`):修前整只展开,任意子键都能上 wire 并覆盖同名顶层键、绕过该键的坏值门。
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- **请求装配逐键去向回执(CC-101)**:`assembleTaskRequest(input, lane) → { request, omitted }`,`request` 与 `buildTaskRequest` 逐字节同一份(后者改一行转调),`omitted` 冻结、逐键带成因;`[]` 是肯定断言(看过了,在场的键全带了)。成因词表 `TASK_REQUEST_OMISSION_CAUSES = ['upstream_absent', 'other_channel', 'off_lane', 'not_live']`(冻结;词序 = 判定序;判官表按词表型 mapped,加词不加判官编译红);座位读数 `taskRequestSeatOf(key, lane)` 三态 `seated{liveGated}` / `none{cause}` / `unknown` —— 不装配就能问「这个旗有没有座」,`unknown` 在装配口是抛不借词。加座三层核过(装着的 sdk 具名位 ∧ 上游提交体真读 ∧ 引擎 spec 真消费):`systemPrompt` / `reasoningEffort` 两车道、`outputSchema` / `maxCostUsd` print(live 门后;⚠️ `outputSchema` 只是上行座位,结构化产出的下行今天不落 CC 形 result 帧);`taskBudget` / `fallbackModel` / `maxThinkingTokens` 三层零命中 = **上游无对位**(`UPSTREAM_ABSENT_REQUEST_INTENTS`;门对装着的 sdk 型面直读,上游铸位那天当场红;索引签名不算证据)。读序:两个记忆声明先读走、已解析权限面紧随其后物化、`maxCostUsd`、然后才读其余键(带副作用访问器的入参从此改不动已读走的声明与已物化的权限面;每个已知键恰读一次)。入参型上删 `agents`(它归 live 兜底层,构造器从不盖;运行期入参带它 ⇒ 回执 `other_channel`,不抛)。
|
|
66
|
+
- **会话策略写端口(CC-105)**:`capabilities.sessionPolicy` 四态读口(读器第十一只;「渲不渲写入口」判据单源 `sessionPolicyFaceAvailable`,键缺席 = 二进制比这一位老 ⇒ 藏 —— 该路由与能力位在服务端同一提交首入,键缺席的 worker 必无此路由;读口刻意不受 gate)+ `readSessionPolicy` 三向窄读(真空 = `rev:0` + 零桶 / 读不懂 / 调用失败;**半坏就是坏**,丢掉坏桶再整份回写 = 一次放宽;版本号绝不补 0;不可信数组只读一遍不截断)+ `tightenSessionPolicy({ sessionId, add, capability })` 收紧编排:🔴 写口是**整份替换**,「追加一条限制」= 读 → 「已有 ∪ 新增」(桶名集合由引擎型面派生,加桶即编译红;在场空桶原样保留)→ 带版本号整份回写;结局五臂判别联合 `written{rev}` / `conflict{rev}`(**只重读重写一次**,结构上无循环)/ `loosen_forbidden`(收紧判据只有引擎一个判官,放宽被拒原样上报不吞不重试)/ `capability_absent{why}` / `unknown{why}`(十二种成因;🔴 写腿的分界是「引擎的裁决到没到手」:**带机器码的** 4xx 才能说没写 —— 引擎的错误信封恒带机器码,没码的 405 / 409 证明不了出自引擎(反代 / 网关拦下这次写回一页 HTML 就是这个形,而那时引擎可能已经落盘);裸抛 / 断连 / 5xx / 无码 4xx / 回执读不懂 / **回执对不上**(版本号没推进、送出去的桶或条目不在回执里)一律「可能已经生效,先重读」)。🔴 `written` 之前先对账:回执只验形就认已写是谎报(上游契约:成功回执 = 规范化后的提交体 + 严格大于并发键的版本号;目录桶由引擎做词法规范化,只核在场)。🔴 并集两侧都过读腿那同一只桶级窄化器(`hasOwn` 桶名 + `hasOwn` 下标 + 逐条字符串):稀疏数组的洞会沿原型链读出别人放进去的值,而并集是整份写回去的;坏的 `add` 是调用方入参 ⇒ 编排入口 `TypeError` 点名侧与桶、零往返(不借引擎的 `request_rejected`)。🔴 `written` 只说「记录现在等于原有的加上送来的」,措辞里没有 tightened / stricter 任何一个词 —— 上游的方向门是**身份门**,运维身份上不跑。措辞十七句单源。规则店 typed 码是三个:并发不符 / 放宽被拒 / 存活标记读不出(后者 wire 上 403 与放宽同码,独立一臂,措辞明说送出去的条目不是问题)。
|
|
67
|
+
- **持久规则单步写面(CC-103;server ≥7.91.1)**:`writePersistedRule(facade, input, opts?)` 四臂 `persisted` / `no_op` / `refused{cause}`(十词闭集,共同承诺 = 一个字都没写进店)/ `unknown{why}`(五词闭集,读不出结局;两组键集不相交)。🔴 `persisted` 说的是「这次调用往店里写了」不是「一条新规则诞生了」(等价规则已在店仍铸一枚新因果点;要判逻辑规则是不是新的读回包行的批准台账);收得下的两个态从三态词表**减去放宽那一个派生**(`PERSISTED_RULE_WRITE_BEHAVIORS`;放宽态类型上拼不出);回包行与列举面**同一只窄化器**,之后再核身份三元组(缺态 / 兄弟态 / 另一条文本或作用域 ⇒ `identity_mismatch`,读作不知道);405 分两格 —— 带引擎裸码 = worker 比动词老(`lane_too_old`,确知没写),给不出出处 = `unknown/unattributed_method_refusal`(中间层可先转发再回 405,不承诺没写);本包不自动重试(重试在批准台账上安全:引擎落地前先问「已经站着吗」)。🔴 装着的 sdk 型面还没有这个动词 ⇒ `RulesFacade.write` 是**可选口**、两只请求 / 回包型按上游结构自铸;宿主漏接 ⇒ `refused/client_too_old`(确知没写)。
|
|
68
|
+
- **workflow 子代面板行的代际三向恢复真三向(CC-104;core ≥7.24.4 起生效)**:引擎给 `wa*` 行帧补上 `seq`(= 该行重试循环的 attempt 号),服务端通用透传口原样生效 —— **本包零改码**(三向判据从不按行 id 前缀分支);面板周期门加 S10 十六格用真实 `wa+16hex` 行形锁定(同代陈旧快照 / 可证更新含 `cycleSeq` 压过相等 `startedAt` / 老 server 常态形靠 `startedAt` 判同代不丢用量 / 真判不出窄档维持既有处置 / 多候选一条都不放不因 `seq` 放宽)。KL-26 按版本收窄并澄清:老 server 的真实暴露面比原文窄,同一次调用内重试多半被 `startedAt` 护住。
|
|
69
|
+
|
|
70
|
+
### Changed(闭集读数改口;详见 §79 79y)
|
|
71
|
+
|
|
72
|
+
- **能力位读器族整族改齐**(外部验收 F-B 后续,11 只):七只此前用 `in` 判在场的投影改 `hasOwn`(原型链上的同名键永远不会被序列化上 wire,读成在场 = 凭空造一格;老件用 `in` 没有成文理由);非对象 / 数组 caps ⇒ 投影答 `undefined` ⇒ 读口 `unobserved`(数组不是能力表,答 `not_reported` 是把「取不到」冒充「报了但没提」)。只对非 JSON 输入可见;三端存量:cli 对 `not_reported` 的引用全是注释与四态穷举,其余端零。
|
|
73
|
+
- **`classifyRulesFailure` 多五格**(CC-103):`body-shape` / `field-refused` / `method-unsupported` / `method-refused` / `write-indeterminate`。其中 `body-shape`(撤销 / 导入两条腿今天可达)与 `method-refused` 此前落通用 `error` 桶 —— 按 `kind === 'error'` 认「一般错误」的端要把这两格挪进「请求有问题」/「认不出是谁拒的,去对账」。**同批改口一格:`lane-unavailable`(店缺席)只认机器码 `capability.rule_store_required`,不再认无码 / 他码的 501 状态本身**(引擎的错误信封恒带机器码;没码的 501 谁都能发 —— 反代对陌生动词就回它;凭它藏面、或在写腿说「确知没写」都是把出处让给了状态码)⇒ 无码 501 落通用 `error`,写面 `writePersistedRule` 对它答 `unknown/unclassified` 而不是 `refused/store_absent`。与会话策略写面的「无码 4xx ⇒ 裁决没到手」同一条律(异源对抗复审合并树轮首发于会话策略面,同形存量扫到这一格)。
|
|
74
|
+
- **`scopeExternalOriginVerdict` 第二形参缺席 ⇒ 入口即抛具名可解释错误**(外部验收 F-A):此前只在「干净答案」那条路上抛裸 TypeError,JS 宿主一路顺跑、第一次遇到干净的店才炸;防御方向不变(缺席绝不折成「干净」)。
|
|
75
|
+
- **§77 ⑫ / KL-29 二次订正**(外部验收 F-B):能力位读器族没有畸形臂,差别是两种缺席语义;面名与入参形写明。
|
|
76
|
+
|
|
77
|
+
### Gates
|
|
78
|
+
|
|
79
|
+
- 新门四只:`run-task-request-omission-receipt-test.mjs`(147:在座 / 认识而没带 / 不认识三格分立,成因词表源码级双向咬,上游位对 sdk 型面直读,拒绝面,已知键恰读一次,表漂响亮)/ `run-session-policy-wire-test.mjs`(236:窄读三向,并集只加不减且两侧入参过窄化器,编排三轴反证含往返计数与三枚「已落盘」判别力实证,回执对账六形,无码 4xx 出处判据,措辞互异,上游字节见证)/ `run-persisted-rule-write-test.mjs`(263:词表派生双向咬,闭集逐词可达,persisted ≠ 新规则,两腿同一只窄化器,不共形,本地四闸零往返,身份对账,接收者绑定,台账安全,无出处 405)/ 面板周期门 +S10。读器工厂门 F11 改「全族 hasOwn」+ F13 / F13b;记忆条目门 +V-A 三格。
|
|
80
|
+
|
|
81
|
+
## 0.76.2(2026-09-20)
|
|
82
|
+
|
|
83
|
+
> 主题:**制品卫生**(patch;零源码改动、零型面改动、零行为改动)—— 0.76.1 的 tarball 里带着两只已撤出源码的编译产物 `dist/mcpProbeCapability.js` / `.d.ts`(编译器只写不删,源码撤出后上一次 build 的产物随 `files` 白名单出门;不在 barrel 导出面,只有深路径 import 才碰得到)。本版清 `dist/` 重 build,并加门 `run-dist-orphan-test.mjs`(dist 里每只 `.js` / `.d.ts` 必须有 `src/` 同名源,反向也对账;在 0.76.1 的 dist 上实跑见红)。除此之外与 0.76.1 逐字节同源(同一 gitHead 的源码);排了 0.76.1 的下游不必为本版重跑功能面,只需核 tarball 里那两只文件不在。
|
|
84
|
+
|
|
52
85
|
## 0.76.1(2026-09-20)
|
|
53
86
|
|
|
54
87
|
> 主题:**能力位读器工厂 + 五只新读口 + 一处别名收口**(patch;零型面 BREAKING;公面值导出 1081 → **1120**)—— 读器工厂(CC-75)· MCP 活性观察读口(CC-36)· `capabilities.peerLane` / `capabilities.permissionRulesWrite` 四态读口(CC-102)· 记忆治理面两只四态读口 + 三只条目面回体读口(CC-97 a)· 主车道证明逐次新建(CC-99)。🔴 **已从本版撤出**:`capabilities.mcpProbe` 读口(CC-74)—— 上游三仓源码里尚无这一格与它的 501 码,上游那张票仍是设计稿;公面一旦发出就是承诺,无一字可验的键名不上公面,候上游真码落地后按字节重做。
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.
|
|
38
|
+
**Version:** 0.77.0
|
|
39
39
|
|
|
40
40
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
41
41
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -398,6 +398,10 @@ public-surface guard checks that last one).
|
|
|
398
398
|
| `scripts/run-peer-lane-rules-write-capability-test.mjs` | Two more engine self-descriptions read the same four-state way as their seven sibling capability readers (`capabilities.peerLane`, `capabilities.permissionRulesWrite`): an absent key is not reported (an older engine that predates the position, never folded into `false`), `true` is present, `false` is a positive absent (the cross-session lane not being mounted on this deployment, or this particular call not being able to reach the tightening-direction write entry), and anything else is unreadable and drops the cell. Each carries its own single-source verdict (`peerLaneAvailable` returns `yes`/`no`/`unknown`; `permissionRulesWriteAvailable` collapses to a plain boolean, present being the only `true`). The write-entry position pairs with a boolean convenience port in the persisted-rules module, and this guard pins that port to derive from nothing but this one reader's own reading — never a conjunction with the lane-reachable position, and never a second read of the deployment-level existence signal the revoke surface uses (the two are documented as reading differently on purpose): a deployment where the lane answers true but the write entry's key is simply absent (an older binary) must still come back `false`, a deployment where the write entry answers true while the lane key is entirely unseen must still come back `true` (proving no silent conjunction crept in), seeding only the general capabilities cache — never this reader's own feed — must still come back `false` (proving the convenience port cannot be satisfied by the wrong table), and passing an explicit `undefined` base URL must still come back `false` even while a different, already-installed engine target answers `true` for the same position (an adversarial pass found the naive forward of that parameter falls through to the reader's own convenience default, silently answering for whichever engine happens to be installed rather than the caller's absent target — the fix routes an explicit absence through the same empty-string path the reader treats as unobserved). |
|
|
399
399
|
| `scripts/run-lane-proof-identity-test.mjs` | The **instance identity of a lane proof**: the main-lane proof is minted fresh on every emission. Previously a single module-level constant object was handed both to `laneOf(an unregistered task id)` and to some fifty main-lane emission points, so two unrelated consumers — across adapter instances, across streams, across turns — held the same object: writing a card id onto one of them was readable on the other, and the four opening main-lane events changed together. Nothing in this package writes to a lane proof and the known consumers only read it, so this is an **aliasing hazard on a published output surface** rather than an observed corruption — a consumer that uses the proof as an identity key, for dedup, or as a view-layer identity would conflate two unrelated rows without writing a single byte, which is precisely the half that freezing the object would not solve. The gate therefore anchors on instance identity: two independent adapter instances, two rows inside one instance, the same id read twice, and two arms in one beat are each distinct references; mutating one leaves the others byte-identical; and the subagent lane, which already minted fresh, is the control that proves the criterion discriminates. The main-lane **value** is unchanged — an unregistered id still answers `{lane:"main"}` with exactly one own key and still emits its events, so absence is not turned into a second kind of absence — with ordering pinned three ways (registered-then-read, read-then-registered with no retroactive edit of an already delivered proof, the same id twice) and the id failure classes pinned four ways (unregistered, empty string, absent, non-string, the last two emitting no panel event at all rather than an ownerless proof). Where one row emits **two** events — the terminal-tick and card-close legs, which each yield a lifecycle stop and a panel end — the attribution is decided once (a consumer binding a card between the two yields must not split one row across two lanes) while each event still gets its own proof, so a host consuming them one at a time cannot poison the second before it is even yielded. The run stream leg is covered as the same shape, and a syntax-tree check forbids reintroducing a module-level lane-proof object literal or a module-level `LaneProof`-annotated binding (judged on the type node, not on text, so a compile-time pin tuple that merely mentions the type is not miscaught), backed by a type-checker pass that also catches an un-annotated module-level cache such as `const x = mainLane()` while letting the callable factory itself through, while the module-private three-state sentinels of the untrusted read are frozen instead — only `Object.freeze` counts, never `Object.seal`, which still permits writes to existing keys — their exposure being confined to one module |
|
|
400
400
|
| `scripts/run-memory-entries-wire-test.mjs` | The two memory-governance capability bits and the three memory-entry response readers. Each bit (`capabilities.memoryCompliance`, for the entry-provenance and erasure endpoints; `capabilities.memoryOrigin`, for the external-origin listing and clearance endpoints) is read the same four-state way as its sibling capability readers: an absent key is reported as not reported (never folded into `false` — an older engine simply does not answer, and the right next step is to try the endpoint and read its 501), `true` is the face being mounted, `false` is a positive "not on this deployment" (the wire does not distinguish a backend without control-plane ownership from an empty operator roster, so the wording never guesses which), any non-boolean value is unreadable and drops the cell instead of being folded into "absent", and a capabilities body that is not an object at all is unreadable rather than "not reported". The two bits deliberately stay **two** readers with two separate per-engine tables, because the engine deliberately keeps them two separate product faces even while they happen to carry the same value today: feeding one an unreadable body, or invalidating one, leaves the other's reading untouched, and a body where one is on and the other off is answered one bit at a time. The entry-export reader narrows each row on its own (an empty array really is zero rows, a non-empty array with nothing readable in it is reported as unreadable rather than as "no rows", and partly bad rows are kept with a dropped count), reads the external-origin marker as three states rather than a boolean (the two structural carriers mark a row; a row whose frontmatter cannot be read, or which carries the third, suspended-form carrier, is undecidable, because the judge for that carrier lives in the engine and this package refuses to mint a second copy of it), and treats an unreadable "is this the whole scope" flag as "not the whole scope". Its verdict port implements — in code, not in a comment — the rule that an empty answer is never a clean store: the caller must state whether the request declared origin-awareness, because this endpoint withholds marked entries by default and the two bodies are shaped identically, so without that statement an empty answer is only ever "unknown"; the affirmative answer is scoped to the one named scope and carries that scope with it, and the type has no store-wide arm at all. The erasure receipt reader keeps three things apart that are easy to collapse: "this call erased nothing" (a real receipt whose erased list is empty and whose not-found list explains why, per id), "a 200 with an empty body", and "a body that could not be read" — at the reading, the counting and the verdict layer alike; it refuses a version envelope it does not recognise instead of reinterpreting it, treats the three closed vocabularies as closed (an unknown word is unreadable, never folded into a known arm), keeps an unreadable binding as unknown instead of claiming "unbound", passes the "history cannot be judged" flag through as four states (set, explicitly unset, absent, and present-but-unreadable — an unreadable flag is kept distinct from an absent one, and the history verdict then answers "unknown" rather than the stronger claim), and answers the replay question as three states so that the degraded lane is never retried automatically. The clearance receipt reader carries the cleared marker through verbatim and says separately whether it was reported at all. Every array in every response is snapshotted once — the length is read exactly once and each index exactly once, rather than iterating the caller's own iterator — because an array that reports one length while being walked and another afterwards could otherwise have a marked row quietly dropped while the "was anything unreadable" check saw nothing, which ends in calling the scope clean; an array that reports an absurd length is reported as unreadable rather than silently truncated to its first rows. All three readers never throw. |
|
|
401
|
+
| `scripts/run-dist-orphan-test.mjs` | Every `.js` / `.d.ts` under `dist/` must have a same-named source under `src/`, and every source must have its build output — because the compiler only writes and never deletes, so a module removed from the sources keeps shipping from the previous build (the whole `dist/` directory is on the publish whitelist) while the public-surface gate only looks at what the barrel exports and the hygiene gate only looks at forbidden words. Orphans are named one by one; the pre-publish posture is a clean rebuild, and this gate is the check that the posture was actually followed. |
|
|
402
|
+
| `scripts/run-task-request-omission-receipt-test.mjs` | Where every key a client hands to the request constructor ends up. The constructor used to answer "not stamped" the same way for four different reasons — value absent, no such row, wrong lane, live gate closed — and a key it had never heard of did not even get that: an unattended run could pass a system prompt, an output schema and a spend cap and receive a body holding the objective and the session id, with nothing anywhere saying what was left out or why. The guard pins the three answers apart. **Seated** keys reach the body verbatim on the unattended lane. Keys the package **knows but did not carry** never throw, never reach the body, and each gets a receipt row with one word from a frozen cause list — every present key is on the body or on the receipt, never both and never neither, checked across both lanes with the live gate open and closed against a key-by-key table written independently of the package's own routing. Keys the package **does not know** are refused loudly and are a separate cell, not a fourth cause: the cause list has no word that could hold them, and the seat reader answers `unknown`, not `none`. The cause list is bitten from both sides (exact, every word producible, nothing produced outside it, the judge table's keys read from source through the TypeScript parser) and no second hand-copied list may exist in `src/`. Upstream claims are read straight off the installed SDK typings: a key seated in this release must be a named request field, a key registered as having no upstream counterpart must not be — the day it appears the guard turns red — and the index signature counts as evidence for nothing. |
|
|
403
|
+
| `scripts/run-session-policy-wire-test.mjs` | The per-session tool-rule face: the capability bit that says whether an engine keeps such rules at all, and the narrow read plus tightening orchestration built on it. The bit is read the same four-state way as its sibling capability readers — an absent key is not reported (this binary predates the position itself, which says nothing about whether the face exists), `true` is present, `false` is a positive absent (this deployment keeps no per-session rules), any non-boolean value is unreadable and drops the cell rather than being folded into "absent", and a capabilities body that is not an object at all (an array included) is unreadable rather than "not reported". Its single-source verdict answers whether to show the tightening entry: only an engine that says yes is `yes`, both a positive no and a binary too old to answer are `no`, and never having observed a capabilities body is `unknown`. Whether to put a request on the wire is deliberately a **different** question with a different answer for that last state, and lives with the orchestration. The read narrows three ways that must not collapse into each other: a record that really is empty (present, version zero — what an engine answers for a session no rules were ever written for), a record that cannot be read, and a call that failed with a typed disposition. A half-bad record — one rule bucket well-formed and another the wrong shape — counts as unreadable in full, because the write verb replaces the whole record: dropping the bad bucket and writing the rest back would empty it, which relaxes the rules while the caller sees a 200. An unreadable version stamp is never filled in with a zero, a bucket that reports an implausible number of entries is unreadable rather than walked or truncated, each array's length and each of its indices are read exactly once, and a throwing accessor is unreadable rather than propagated. Every load-bearing key is read as an own property — the envelope, the version stamp, each of the five buckets and each array index — because a prototype lookup would let a polluted prototype put a bucket into the reading that the wire never carried, and since the write replaces the whole record the union would then write that invented restriction back as a real one; a guard pollutes the object and array prototypes in place and proves all four shapes stay out. Because the write replaces the whole record, adding a restriction means writing "what is already there, plus the new entries": the union only ever adds, de-duplicates verbatim, keeps a bucket that is present but empty (present-and-empty and absent are opposite meanings, and dropping it would relax the rules), mints no bucket neither side had, copies entry bytes as they came (no trimming, sorting or path rewriting — those judgements belong to the engine), and takes its bucket names from the engine's own type surface rather than a hand-copied list, so a new bucket upstream is a compile error instead of a silently dropped one. Which differences count as relaxing is the engine's judgement and is never re-implemented here: a refusal on those grounds is reported verbatim, never swallowed and never retried. The orchestration is guarded on three axes. Timing: when the record moves between the read and the write, it re-reads and re-writes **exactly once** — two reads and two writes, no more — and the second attempt's union carries the other writer's entries, which is the entire point of re-reading; a second collision is reported rather than retried a third time, and an uncontended write makes exactly one round trip. Concurrency: an explicit barrier holds both orchestrations first reads at the same version before either may write, and the criterion is how many times the store actually rejected a stale version rather than how many writes it saw — the latter is equally true of two serial successes, so it would stop detecting contention the day the interleaving changed. Under real contention the store rejects exactly once, both writers land on strictly different versions, both writers entries survive in the final record, and the round trips are exactly three reads and three writes; the same two orchestrations run serially are asserted to reject zero times in two reads and two writes, which is what proves those numbers are discriminating. On a store where every write loses the race both report a collision having written exactly twice each. Failure classification: a relaxation refusal, a refusal to stamp a version the store cannot establish (the same status code as the relaxation refusal but a different machine code, and folding it into that arm would send the caller off to edit entries that are not the problem), a missing session, a deployment without the face, an ownerless session, a collision code, a bare conflict with no machine code, a rejected body and an unauthorized call each land on their own arm — the collision arm is matched on the machine code verbatim rather than on the status, because two different situations share that status and only one of them is worth retrying. The remaining split is not "which code is this" but "did the engine answer at all": an answered client-side refusal is allowed to say nothing was written, because every such refusal on this endpoint is emitted before the record is touched, while a throw with no answer at all — a dropped connection, a timeout, a response body the transport itself could not decode, a server fault — can only say "unknown", since that throw may well have happened after the record was already saved. Two guards prove that is not theoretical: a write whose receipt cannot be read, and a write that throws after the fixture store has committed, both leave the record changed. Neither is success nor failure: the only honest answer is "unknown", it carries no version, and it is never retried. The direction of the change is likewise never claimed. The engine’s tighten-only rule is an identity gate, not a field gate — for a principal the deployment treats as an operator it does not run at all, so a union that adds a name to an existing allowlist is accepted and really does widen it. This package does not mint a second copy of that rule, so what it reports is the fact it can stand behind: the record now holds what it already had plus the entries sent here. The sentence for a saved write is pinned to contain no claim of tightening, narrowing or restriction, and a guard reproduces the operator case to prove the widening is real while the wording stays honest. Every sentence the module mints is checked pairwise distinct, with the receipt-unreadable one required to keep its "may already be in effect" and the record-unreadable one required to say nothing was written. |
|
|
404
|
+
| `scripts/run-persisted-rule-write-test.mjs` | The **single-step tightening write** for persisted permission rules — the dual of the revoke surface, and the half where a hopeful reading is expensive. The two behaviours this entry accepts are **derived** from the three-state vocabulary by subtracting the widening one, never hand-copied: the guard bites in both directions (every word in the derived table is really accepted; every constructed outsider — casing variants, trailing whitespace, the widening word itself — is refused before a single round trip), keeps a word-count canary against the parent table, and pins that the source file contains **exactly one** array literal carrying two or more behaviour words, so a second hand-written table shows up as a boundary failure rather than as drift nobody reads. A standing approval is minted by answering a permission question or by importing settings; this entry is not a third route, and the widening word is unspellable in the type. The outcome is a discriminated union whose two failure arms are **not** interchangeable: ten refusal causes each promise the same single thing — not one byte reached the store — while three separate words say the opposite, that the outcome could not be read at all. The service's own "I cannot tell" (a write that could not be confirmed as standing: store wobble, a redemption leg with no decidable ending, or a write that landed and was revoked concurrently before the read-back) stays in the second group, because announcing "nothing was written" invites a clean retry that is not clean, and announcing success misreports a tightening that may already be gone. Anything the shared failure classifier does not recognise defaults to the same place — this is a non-idempotent verb, so "unclassified" must mean "unknown", never "no write": a 500 can happen after the store commits. A 2xx whose body cannot be read is pinned in the same direction and from both sides: it reads as unknown, and the unknown arm carries **neither** the revision nor the written row, so a consumer cannot even spell the shape that would let "unreadable" pass for "written". `persisted` is guarded against the reading everyone reaches for first: it says *this call wrote*, not *a new rule now exists* — an equivalent rule already in the store still mints a fresh causal point, so the lane honestly reports `persisted`, and the material for judging whether the **logical** rule is new (the approval ledger on the returned row) is handed to the caller rather than folded into the discriminant, since the package does not have the one fact that judgement needs. The returned row goes through the **same single narrower** the listing surface uses — proven by running one row corpus through both legs and asserting the two verdicts agree entry for entry (an adversarial pass that forks the listing leg back into an inline copy reds here immediately), plus a source pin that the predicate is defined once and called from exactly the two legs. That sharing is what keeps a row whose behaviour cell is unreadable **visible in both places** rather than hidden by one of them — and the shared narrower is deliberately followed by a second, *different* question only the write leg can ask: is the row that came back **the rule that was just sent**? A rule's identity is a triple, so a receipt missing its behaviour cell, carrying the sibling state, carrying the widening one, or naming another text or another scope is not evidence that the requested tightening is standing; it reads as unknown with its own word, kept distinct from "unreadable" so the two stay tellable apart, while display and derived cells may vary freely. An adversarial pass found both of the gaps this pins: the receipt check that only looked at whether the row was renderable, and a subtler one — pulling the verb off the injected port and calling it bare drops the receiver, so a host that hands over a real resource object (a class instance whose verbs reach the transport through `this`) would see every write throw and be reported as "could not tell", retry after retry, while the package's three other ports call their verbs as methods and work fine. Both are pinned from the failing side: a shorthand-method facade and a class-instance facade must reach the transport and return a real outcome, with the bare-call throw proven to be a real failure mode first. A 405 is split in two, because only the engine's own bare code is evidence about **the engine**: with it, the path exists and this verb does not, so this worker predates the verb; without it — an absent, empty, or foreign code, which is what a proxy or gateway blocking the method typically returns as HTML or an empty body — what was seen is that the verb was refused, while **who** refused it and **at which hop** is unknown, so it lands in the unreadable-outcome arm with its own word rather than sending someone to upgrade a worker that is fine, hiding an entry that is live, or — the part a second adversarial pass insisted on — promising that nothing was written. That promise is what the refusal group means, and a middlebox is free to forward the request and only then answer 405 on its own policy, so a caller who skipped reconciliation on that word would leave behind a standing refusal the user believes never took effect; the guard pins exactly that shape, with a double that writes the rule and *then* answers 405, and with the engine's own bare code still landing in the refusal group beside it. (The same passes caught the naive status-only reading and the asymmetry where an empty-string code fell through to a different bucket.) The documented recovery for an unreadable outcome — retry, then reconcile — is pinned to be **ledger-safe** rather than merely asserted: against a double that models the engine's own "is this identity already standing?" question, re-sending the same identity comes back as a no-op with the approval ledger and the bucket revision both unmoved, however many times it is repeated, while two concurrent writers each landing a causal point are both honestly reported as having written. Finally the four local gates are pinned to be free: a missing write verb on the injected port, an unwritable direction, an unreadable identity pair, and a principal key that is present but cannot name anyone all refuse **without sending anything** — the last of those because silently degrading a blank target into absence would land a tightening aimed at someone else in the caller's own bucket and return a 200 |
|
|
401
405
|
|
|
402
406
|
Each suite carries a floor that only moves up — a refactor that stops executing a group of
|
|
403
407
|
assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
|
|
@@ -18,7 +18,11 @@ import { createEngineCapReader } from './engineCapReader.js';
|
|
|
18
18
|
export function projectApprovalsStreamLiveCapability(caps) {
|
|
19
19
|
if (caps === null || typeof caps !== 'object')
|
|
20
20
|
return undefined;
|
|
21
|
-
|
|
21
|
+
// 0.77.0 整族改齐:① 非对象 / 数组 caps 不是能力表 ⇒ 畸形(投影答 undefined ⇒ 删格 ⇒ 读口答 unobserved),不再答 not_reported
|
|
22
|
+
// (那是把「取不到」冒充「报了但没提」);② 在场判据改 hasOwn —— 原型链上的同名键永远不会被序列化上 wire,读成在场 = 凭空造一格。
|
|
23
|
+
if (caps === null || typeof caps !== 'object' || Array.isArray(caps))
|
|
24
|
+
return undefined;
|
|
25
|
+
if (!Object.hasOwn(caps, 'approvalsStreamLive'))
|
|
22
26
|
return { kind: 'not_reported' };
|
|
23
27
|
const v = caps.approvalsStreamLive;
|
|
24
28
|
if (v === undefined)
|
|
@@ -20,8 +20,8 @@ import { engineWireTarget } from './engineWireTarget.js';
|
|
|
20
20
|
import { createEngineCapReader } from './engineCapReader.js';
|
|
21
21
|
/** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
|
|
22
22
|
export function projectDeviceExecutorManagementCapability(caps) {
|
|
23
|
-
if (caps === null || typeof caps !== 'object')
|
|
24
|
-
return undefined;
|
|
23
|
+
if (caps === null || typeof caps !== 'object' || Array.isArray(caps))
|
|
24
|
+
return undefined; // 0.77.0 整族同律:数组不是能力表
|
|
25
25
|
const c = caps;
|
|
26
26
|
if (!Object.hasOwn(c, 'deviceExecutor'))
|
|
27
27
|
return { kind: 'not_reported', why: 'capability_absent' };
|
|
@@ -33,7 +33,11 @@ import { createEngineCapReader } from './engineCapReader.js';
|
|
|
33
33
|
export function projectExecutionLaneCapability(caps) {
|
|
34
34
|
if (caps === null || typeof caps !== 'object')
|
|
35
35
|
return undefined;
|
|
36
|
-
|
|
36
|
+
// 0.77.0 整族改齐:① 非对象 / 数组 caps 不是能力表 ⇒ 畸形(投影答 undefined ⇒ 删格 ⇒ 读口答 unobserved),不再答 not_reported
|
|
37
|
+
// (那是把「取不到」冒充「报了但没提」);② 在场判据改 hasOwn —— 原型链上的同名键永远不会被序列化上 wire,读成在场 = 凭空造一格。
|
|
38
|
+
if (caps === null || typeof caps !== 'object' || Array.isArray(caps))
|
|
39
|
+
return undefined;
|
|
40
|
+
if (!Object.hasOwn(caps, 'executionLane'))
|
|
37
41
|
return { kind: 'not_reported' };
|
|
38
42
|
const lane = caps.executionLane;
|
|
39
43
|
if (lane === undefined)
|
|
@@ -51,6 +51,21 @@ export interface RulesFacade {
|
|
|
51
51
|
revoke(input: RuleRevokeRequest, opts?: {
|
|
52
52
|
signal?: AbortSignal;
|
|
53
53
|
}): Promise<RuleRevokeResult>;
|
|
54
|
+
/**
|
|
55
|
+
* 收紧方向的**单步写**口(`POST /v1/rules`,engine ≥7.91.0)——**可选口**。
|
|
56
|
+
*
|
|
57
|
+
* 🔴 **为什么是可选的,而不是必填**:本包今天装的 SDK 型面上**还没有**这个动词(它的 `rules` 资源
|
|
58
|
+
* 只有导入两口 + 列举 + 撤销),所以宿主从真 client 装配 facade 时**拼不出**这一格。做成必填 =
|
|
59
|
+
* 三端当天全部编译不过,而它们一行新代码都没写错。⇒ 口可缺席,而**缺席是一句有内容的话**:
|
|
60
|
+
* {@link writePersistedRule} 据此答 `refused` / `client_too_old`(这次调用连发都没发出去,
|
|
61
|
+
* 所以「什么都没写」是**确知**的,不是「不知道」)。
|
|
62
|
+
* ⚠️ 这一格的型也因此**由本包按上游的结构声明**({@link PersistedRuleWriteRequest} /
|
|
63
|
+
* {@link PersistedRuleWriteWireResult}),而不是从 SDK 取型 —— SDK 地板抬到带这个动词的那一版时,
|
|
64
|
+
* 这两只型改为取自 SDK、本口转必填、`client_too_old` 一格退役(见接入文档的包侧缺口段)。
|
|
65
|
+
*/
|
|
66
|
+
write?(input: PersistedRuleWriteRequest, opts?: {
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
}): Promise<PersistedRuleWriteWireResult>;
|
|
54
69
|
ccImportPrepare(layers: CcImportLayer[], opts?: {
|
|
55
70
|
signal?: AbortSignal;
|
|
56
71
|
}): Promise<CcImportPrepareResult>;
|
|
@@ -143,10 +158,71 @@ export type RulesFailure =
|
|
|
143
158
|
kind: 'too-many-candidates';
|
|
144
159
|
message: string;
|
|
145
160
|
}
|
|
146
|
-
/** 403 `auth.operator_only` ——
|
|
161
|
+
/** 403 `auth.operator_only` —— 越权读/撤销/写(替别人)被拒。 */
|
|
147
162
|
| {
|
|
148
163
|
kind: 'forbidden';
|
|
149
164
|
message: string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 400 `request.body_shape` —— **体读不出形**(身份三键缺/坏,或 `scope` 不是 `global` /
|
|
168
|
+
* `project:<非空 root>`)。🆕 0.77.0 从通用 `error` 里分出来:同一份体重发永远同一个结果 ⇒ 处置是
|
|
169
|
+
* **改请求**,不是重试、也不是「服务端出问题了」。引擎对这一码在写与撤两条腿上同句同码,所以它
|
|
170
|
+
* 在本表里也只占一格。
|
|
171
|
+
* ⚠️ **成文改口**:0.76.x 及以前这一码落 `error`;按 `kind === 'error'` 认「一般错误」的消费端要把
|
|
172
|
+
* 这一格挪到自己的「请求有问题」分支(渲染面可不变——本臂同样带 `message`)。
|
|
173
|
+
*/
|
|
174
|
+
| {
|
|
175
|
+
kind: 'body-shape';
|
|
176
|
+
message: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 400 `request.field_invalid` —— **某个字段的值被拒**(单步写口专有)。两类成因共用这一码:
|
|
180
|
+
* 方向(写口只收 deny/ask)与车道的逐行判据(规则文本非规范拼写 / scope 的字节 / 行不自洽)。
|
|
181
|
+
* 🔴 **两类在这条面上分不出来**:引擎把机读的 `reason` 与人读的 `detail` 放在响应体里,而本包消费的
|
|
182
|
+
* 错误型面只保留 `status` / `errorCode` / `message` 三格 ⇒ 那两格**停在 wire 上**。所以本臂只说
|
|
183
|
+
* 「这条写没落地,是字段值被拒」,**不猜**是哪一格、更不猜该改成什么(猜一个规范拼法再试,等于
|
|
184
|
+
* 替引擎的解析器发明一份第二真源)。
|
|
185
|
+
*/
|
|
186
|
+
| {
|
|
187
|
+
kind: 'field-refused';
|
|
188
|
+
message: string;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 405 **且码是引擎自己的裸 `method_not_allowed`** —— **这条路径在,这个动词不在**:这台 worker 比这条
|
|
192
|
+
* 动词老(路径上只路由了它认得的那几个)。
|
|
193
|
+
* ⚠️ 别读成「这台不支持规则面」(那看车道能力位),也别读成「路径不存在」(那是 404 `not_found.route`)。
|
|
194
|
+
* 🔴 **要码在场才给这个结论**(异源复审 R2 [medium],真病):引擎那一支发的是没有前缀族的裸码,
|
|
195
|
+
* 而「裸码」与「**没有**码」在这条面上长得很像 —— 一个中间层/网关拦下 POST 时回的常是 HTML 或空体
|
|
196
|
+
* (读不出任何码)。把无码 405 也判成这一格,等于凭一个谁都能发的状态码断言**对端的版本**,
|
|
197
|
+
* 而它导出的动作(藏起入口 / 要求升级 worker)恰恰是错的。无码那一支走 {@link RulesFailure}
|
|
198
|
+
* 的 `method-refused`。
|
|
199
|
+
*/
|
|
200
|
+
| {
|
|
201
|
+
kind: 'method-unsupported';
|
|
202
|
+
message: string;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 405 **而码给不出出处**(缺席 / 空串 / 不是引擎那个裸码)—— 看见的是「这个动词被拒了」,而**谁拒的、
|
|
206
|
+
* 拒在哪一段不知道**:引擎那一侧的 405 发在路由分派期,但一个中间层 / 网关也能发同一个状态码,而它
|
|
207
|
+
* **可能已经把请求转给引擎**、事后才按自己的策略回 405。
|
|
208
|
+
* 处置两条:① 在有副作用的动词上当作「**结局说不清**」,去对账,别宣布「没做」(异源复审 R3 [medium]);
|
|
209
|
+
* ② **别**据此要求升级 worker、也别据此藏掉入口 —— 那两个动作要的是能力位或版本证据,不是一个谁都能发
|
|
210
|
+
* 的状态码。要说「这台 worker 比这个动词老」,只有 `method-unsupported`(引擎自己的裸码)才有资格。
|
|
211
|
+
*/
|
|
212
|
+
| {
|
|
213
|
+
kind: 'method-refused';
|
|
214
|
+
message: string;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 503 `state.rule_write_failed` —— 单步写的**结局不定**:店抖动 / 兑付面没交回可判的终局 / 写确实
|
|
218
|
+
* 落了而回读它时一条并发撤销已先到,三者在这条口上**不可分辨**。
|
|
219
|
+
* 🔴 与 {@link RulesFailure} 里其余任何一臂的分别是承重的:它**既不是**「没写」也**不是**「写了」。
|
|
220
|
+
* 宣布没写会让人以为可以重来,宣布写了会谎报一次可能已被撤掉的收紧。处置 = 重试,然后用
|
|
221
|
+
* {@link listAllPersistedRules} 对账。
|
|
222
|
+
*/
|
|
223
|
+
| {
|
|
224
|
+
kind: 'write-indeterminate';
|
|
225
|
+
message: string;
|
|
150
226
|
} | {
|
|
151
227
|
kind: 'error';
|
|
152
228
|
message: string;
|
|
@@ -228,6 +304,149 @@ export type ListAllPersistedRulesOutcome = {
|
|
|
228
304
|
export declare function listAllPersistedRules(facade: RulesFacade, params?: Omit<RuleListParams, 'cursor'>, opts?: {
|
|
229
305
|
signal?: AbortSignal;
|
|
230
306
|
}): Promise<ListAllPersistedRulesOutcome>;
|
|
307
|
+
/**
|
|
308
|
+
* 单步写口**不收**的那一态 —— 放宽方向的单一铸点。
|
|
309
|
+
*
|
|
310
|
+
* 🔴 `satisfies RuleBehavior` 不是装饰:它把这个字面量钉在**引擎的态词表**上。哪天词表里没有这个词了,
|
|
311
|
+
* 本行当场编译不过 —— 而一个手写的排除词在那天会安静地排除一个不存在的东西。
|
|
312
|
+
*/
|
|
313
|
+
declare const DIRECT_WRITE_EXCLUDED_BEHAVIOR = "allow";
|
|
314
|
+
/**
|
|
315
|
+
* 单步写口收得下的**态**,运行期表。**由 {@link PERSISTED_RULE_BEHAVIORS} 减去放宽那一词派生**,
|
|
316
|
+
* 源码里**没有第二个 behavior 字面量数组**(门对这一条有边界钉)。
|
|
317
|
+
*
|
|
318
|
+
* 🔴 **为什么必须是派生而不是一张两词表**:态的词表属主只有一个(引擎)。一张手抄的两词表会在词表
|
|
319
|
+
* 加员的那天变成一句没人复核过的旧话 —— 新词既进不来,也没有任何一道门会响。派生式让这句话跟着
|
|
320
|
+
* 词表走:这里表达的不是「有哪两个词」,而是**「三态里哪些进得了这条口」**。
|
|
321
|
+
* 🔴 **放宽方向在类型上就拼不出来**:常驻放行只有两条路(审批卡上的人点头 / 一次显式的 settings 导入),
|
|
322
|
+
* 这条口**不是**第三条。一个能拼出来的放宽入参会让调用方以为它是。
|
|
323
|
+
* 🔴 形制同兄弟表:`Object.freeze` 的数组,不是只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
|
|
324
|
+
* 就能改,而公面消费者拿到的正是这个实例。
|
|
325
|
+
*/
|
|
326
|
+
export declare const PERSISTED_RULE_WRITE_BEHAVIORS: readonly Exclude<RuleBehavior, typeof DIRECT_WRITE_EXCLUDED_BEHAVIOR>[];
|
|
327
|
+
/** 单步写口收得下的态(型,**由上面那张表派生**——表与型不会各走各的)。 */
|
|
328
|
+
export type PersistedRuleWriteBehavior = (typeof PERSISTED_RULE_WRITE_BEHAVIORS)[number];
|
|
329
|
+
/**
|
|
330
|
+
* 单步写口的请求体 —— 与撤销体**同一份身份三元组**,差别只有方向那一格收窄成
|
|
331
|
+
* {@link PersistedRuleWriteBehavior}。
|
|
332
|
+
*
|
|
333
|
+
* ⚠️ **型由本包按上游的结构声明**(本包今天装的 SDK 型面上还没有这个动词),所以它是一份**对位形**
|
|
334
|
+
* 而不是取来的型;地板抬到带这个动词的那一版时改为取型、本形退役(见接入文档的包侧缺口段)。
|
|
335
|
+
*/
|
|
336
|
+
export interface PersistedRuleWriteRequest {
|
|
337
|
+
/** 要写的是哪一态。**只收收紧方向**;放宽那一词在类型上拼不出来。 */
|
|
338
|
+
behavior: PersistedRuleWriteBehavior;
|
|
339
|
+
/**
|
|
340
|
+
* 规则文本。🔴 **必须已经是引擎的规范拼写**:解析器会归一化多余空白,而店里落的是规范形 ——
|
|
341
|
+
* 若这条口照收,调用方将**叫不出自己刚写的那条规则的名字**(拿同一份字节去撤,撤不掉一条正在
|
|
342
|
+
* 生效的规则)。非规范拼写由引擎当场拒(见 {@link PersistedRuleWriteOutcome} 的 `field_refused`)。
|
|
343
|
+
* 规范形是不动点:列举面的行与本口回包里的 `rule.rule` 都已经是规范形,**原样回传**即可。
|
|
344
|
+
*/
|
|
345
|
+
rule: string;
|
|
346
|
+
/** 作用域判别式串:`global` 或 `project:<非空 root>`,与撤销体**同一份表示**。 */
|
|
347
|
+
scope: string;
|
|
348
|
+
/** 越权域:替**别人**写。缺席(或自己的名字)= 写自己的。判据与撤销体逐字相同。 */
|
|
349
|
+
principal?: string;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* 单步写口 200 体的**对位形**(同上:本包声明,不是取来的型)。
|
|
353
|
+
*
|
|
354
|
+
* 🔴 `status` 的判别位是**「这次调用有没有往店里写」**,不是「这条规则现在在不在」。
|
|
355
|
+
* 🔴 `rule` 是**从店里读回的那一行**,与列举面的行**逐格相同**(含派生的 `source` / `status`)——
|
|
356
|
+
* 所以「写完把它插进我本地那份清单」不需要任何翻译,撤销体想要的三串也逐字都在这一行里。
|
|
357
|
+
*/
|
|
358
|
+
export interface PersistedRuleWriteWireResult {
|
|
359
|
+
status: 'persisted' | 'no-op';
|
|
360
|
+
rev: number;
|
|
361
|
+
rule: PersistedRule;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* 写**没落地**的成因闭集。每一词都对着一条真码 / 一条本地判据,**核不到的不铸**。
|
|
365
|
+
*
|
|
366
|
+
* 前四词是**发出去之前**就定局的(本地拒铸,连一次往返都没有);后六词是引擎的答话。
|
|
367
|
+
* 🔴 十词共同的、也是唯一的承诺:**这次调用一个字都没写进店**。所以一个拿不出这句话的证据的成因
|
|
368
|
+
* **不许**进这张表 —— 哪怕它看上去像一次拒绝(例:一个来源不明的 405,见
|
|
369
|
+
* {@link PERSISTED_RULE_WRITE_UNKNOWN_REASONS} 的 `unattributed_method_refusal`)。「说不清」不在这张表里,它是
|
|
370
|
+
* {@link PersistedRuleWriteOutcome} 的另一臂({@link PERSISTED_RULE_WRITE_UNKNOWN_REASONS})。
|
|
371
|
+
*/
|
|
372
|
+
export declare const PERSISTED_RULE_WRITE_REFUSAL_CAUSES: readonly ["unwritable_behavior", "unreadable_identity", "unusable_principal", "client_too_old", "field_refused", "body_shape", "operator_only", "store_absent", "lane_too_old", "route_absent"];
|
|
373
|
+
/** {@link PERSISTED_RULE_WRITE_REFUSAL_CAUSES} 的成员型(**由表派生**,不留第二份手抄)。 */
|
|
374
|
+
export type PersistedRuleWriteRefusalCause = (typeof PERSISTED_RULE_WRITE_REFUSAL_CAUSES)[number];
|
|
375
|
+
/**
|
|
376
|
+
* 「**不知道落没落**」的成因闭集。
|
|
377
|
+
*
|
|
378
|
+
* 🔴 这五词与上面那十词**不可互折**:那些说的是「确知没写」,这五词说的是「**读不出结局**」。
|
|
379
|
+
* 把后者渲成前者会让人以为可以干净地重来一遍;渲成 `persisted` 则是谎报一次可能根本没发生的
|
|
380
|
+
* 收紧。四词的处置一律是:重试,然后用 {@link listAllPersistedRules} 对账。
|
|
381
|
+
*
|
|
382
|
+
* 🔴 **「重试」在批准台账上是安全的,而这条安全性在引擎里、不在这里**(异源复审 R2 [high] 提出的
|
|
383
|
+
* 「重放会让台账无界增长」,亲读引擎的写腿验真后**改成文、不改代码**):引擎在落任何东西之前**先问
|
|
384
|
+
* 「这条身份已经站着吗」**,站着就**一个字都不写**并答 `no-op`。所以一次「说不清」之后**顺序**重发
|
|
385
|
+
* 同一份身份三元组,最坏情况是收到 `no_op` —— 批准台账不增长、桶的修订号不推进。
|
|
386
|
+
* ⚠️ 真正会让同一条逻辑规则带上两枚因果点的是**并发**:两个写者同时看见「不在」而各落一枚,
|
|
387
|
+
* 那是 add-wins 的正解(两枚点、一条规则),两边都会如实收到 `persisted`,没有人被告知「没写」。
|
|
388
|
+
* ⚠️ 也正因为这道前问在**引擎**那一侧,本包**不**在重试前自己先列举一遍 —— 那会把引擎已经做了的
|
|
389
|
+
* 读再做一次,并且给同一个问题立第二个说话人。「先对账再决定要不要重发」是调用方的策略自由,
|
|
390
|
+
* 不是本口的前置条件。
|
|
391
|
+
*/
|
|
392
|
+
export declare const PERSISTED_RULE_WRITE_UNKNOWN_REASONS: readonly ["indeterminate", "malformed_result", "identity_mismatch", "unattributed_method_refusal", "unclassified"];
|
|
393
|
+
/** {@link PERSISTED_RULE_WRITE_UNKNOWN_REASONS} 的成员型(**由表派生**,不留第二份手抄)。 */
|
|
394
|
+
export type PersistedRuleWriteUnknownReason = (typeof PERSISTED_RULE_WRITE_UNKNOWN_REASONS)[number];
|
|
395
|
+
/**
|
|
396
|
+
* 单步写一条收紧规则的**结局**(判别联合)。
|
|
397
|
+
*
|
|
398
|
+
* 🔴 **`persisted` 说的是「这次调用往店里写了」,不是「一条新规则诞生了」**(上游头注逐字):等价
|
|
399
|
+
* 规则已在店的那一支**仍然铸一枚新 dot** ⇒ 车道如实报 `persisted`。要判**逻辑规则**是不是新的,
|
|
400
|
+
* 看回包那一行的 `adds`(批准台账)—— 别看这一格。本包**不**替调用方做那个判断:它需要「写之前
|
|
401
|
+
* 它在不在」这个本函数手上没有的事实,而编一个出来正是诚实缺席要挡的东西。
|
|
402
|
+
* 🔴 **`no_op` 是一句关于「这次调用」的话**:这条身份此前就站着,本次**一个字都没写** —— `adds`
|
|
403
|
+
* 不增长、桶的 `rev` 不推进(它是列举游标的锚)。所以重试安全,但它**不是**第二次批准。
|
|
404
|
+
* ⇒ 一次「说不清」之后顺序重发同一份身份,最坏结局就是这一臂:批准台账不会因为重试而增长。
|
|
405
|
+
* 🔴 **`unknown` 与 `persisted` 不共形**:两臂的键集刻意不相交(`why` vs `rev` / `rule`),一个把
|
|
406
|
+
* 「读不出」当成「写上了」的消费端**连拼都拼不出来**。
|
|
407
|
+
*/
|
|
408
|
+
export type PersistedRuleWriteOutcome = {
|
|
409
|
+
status: 'persisted';
|
|
410
|
+
rev: number;
|
|
411
|
+
rule: PersistedRule;
|
|
412
|
+
} | {
|
|
413
|
+
status: 'no_op';
|
|
414
|
+
rev: number;
|
|
415
|
+
rule: PersistedRule;
|
|
416
|
+
} | {
|
|
417
|
+
status: 'refused';
|
|
418
|
+
cause: PersistedRuleWriteRefusalCause;
|
|
419
|
+
message: string;
|
|
420
|
+
} | {
|
|
421
|
+
status: 'unknown';
|
|
422
|
+
why: PersistedRuleWriteUnknownReason;
|
|
423
|
+
message: string;
|
|
424
|
+
};
|
|
425
|
+
/**
|
|
426
|
+
* 单步写一条**收紧**规则(deny/ask)进持久店 —— 撤销面的对偶,无同意仪式往返。
|
|
427
|
+
*
|
|
428
|
+
* 判据次序照引擎那一侧逐条对位,**只多一道在最前面**:
|
|
429
|
+
* ⓪ **口在不在**(注入的 facade 有没有这个动词)—— 与「按能力位 gate、别 trial-by-501」同一条纪律:
|
|
430
|
+
* 一次连动词都拼不出来的调用不该先去撞引擎的限流桶;
|
|
431
|
+
* ① 体形(身份三键读得出);② 方向(只收收紧);③ scope 读得出形;④ 越权域那一格。
|
|
432
|
+
* ②③④ 的**本地**判据刻意比引擎宽:本地只判「这串是不是一个 X」,「这个 X 写得进去吗」永远由引擎答
|
|
433
|
+
* (规范拼写、scope 的字节、行自洽都是引擎的判据)—— 壳替它判一遍就是同一件事的第二份真源。
|
|
434
|
+
*
|
|
435
|
+
* 🔴 **本函数不重试**:一次「说不清」被自动重放,重试预算与节奏就成了本包替调用方做的决定;而对账
|
|
436
|
+
* 那一步(用 {@link listAllPersistedRules} 读回来看)只有调用方做得了。
|
|
437
|
+
* ⚠️ 手工重试**在批准台账上是安全的**(引擎落地前先问「已经站着吗」,站着就一个字都不写)——
|
|
438
|
+
* 逐条理由见 {@link PERSISTED_RULE_WRITE_UNKNOWN_REASONS} 的头注。
|
|
439
|
+
* 🔴 **回包那一行走列举面同一只窄化器**,不给同一份判据立第二个法官;读不出 ⇒ `unknown` /
|
|
440
|
+
* `malformed_result`,**绝不**当成「写上了」。
|
|
441
|
+
* 🔴 **窄化之后还有一道身份对账**(`identity_mismatch`):回包那一行的身份三元组必须与发出去的那份逐字
|
|
442
|
+
* 相同。这不是同一份判据的第二个法官 —— 窄化器答「读得出来吗」,对账答「是不是我要的那条」,而后者的
|
|
443
|
+
* 材料只有写腿手上有。列举面「态读不出的行照样交出去」的纪律在这里不适用:看得见 ≠ 证明得了。
|
|
444
|
+
* ⚠️ 回包行的 `adds[].origin` **不是**「谁写的这条规则」的判据(引擎今天从审批记录的种类派生出处词,
|
|
445
|
+
* 而本口落店走的是同一条兑付腿)—— 原样透传,不进本包任何一条判据。
|
|
446
|
+
*/
|
|
447
|
+
export declare function writePersistedRule(facade: RulesFacade, input: PersistedRuleWriteRequest, opts?: {
|
|
448
|
+
signal?: AbortSignal;
|
|
449
|
+
}): Promise<PersistedRuleWriteOutcome>;
|
|
231
450
|
/**
|
|
232
451
|
* `skipped[].reason` 的分类。**只按第一个 `:` 前缀**,且必须容得下**没有前缀**的两种真值形
|
|
233
452
|
* (整层 JSON 解不开 / 条目不是串)——SDK 头注逐字:reason 是给人看的散文,不是机读码,
|
|
@@ -264,3 +483,4 @@ export type RulePersistOutcome = {
|
|
|
264
483
|
* 在不在(那要看能力位)。
|
|
265
484
|
*/
|
|
266
485
|
export declare function readRulePersistOutcome(ack: unknown): RulePersistOutcome;
|
|
486
|
+
export {};
|