@sema-agent/client-core 0.30.1 → 0.30.2
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 +34 -0
- package/README.md +2 -1
- package/dist/hitl/localAllowRule.d.ts +80 -0
- package/dist/hitl/localAllowRule.js +96 -0
- package/dist/hitl/persistedRulesWire.d.ts +169 -0
- package/dist/hitl/persistedRulesWire.js +236 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/docs/INTEGRATION-CLIENTS.md +7 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,40 @@
|
|
|
16
16
|
> 🔴 **互链**(web [C166]⑦):各版「已知局限」段只记**该版新增**;接入面已知局限的完整台账在
|
|
17
17
|
> `docs/INTEGRATION-CLIENTS.md` §6e/§7 —— **只读其一会漏**,两处都过。
|
|
18
18
|
|
|
19
|
+
## 0.30.2
|
|
20
|
+
|
|
21
|
+
- **#244 F2 HITL 规则侧上收 · 包半场(A-028.14 + parseLocalAllowRule 硬排期件;hitl/ 新两件,
|
|
22
|
+
+7 公面导出)**(2026-08-14;源形 = cli `src/sema/persistedRulesWire.ts` /
|
|
23
|
+
`src/sema/localAllowRuleWrite.ts` 的纯判定面;常驻门 `scripts/run-rules-side-test.mjs`,收官形 86 checks):
|
|
24
|
+
- **`hitl/persistedRulesWire.ts`** —— 持久规则车道的通用判定半场:
|
|
25
|
+
`persistedRulesLaneAvailable` / `persistedRulesGovernanceAvailable`(能力位双段 gate,
|
|
26
|
+
`permissionRulesRevoke` 存在性 × `permissionRules` 真值合取;baseUrl 由宿主显式传入,env
|
|
27
|
+
缺省读法留壳)· `RulesFailure` + `classifyRulesFailure`(处置分类,404 两支绝不共用一格)·
|
|
28
|
+
`listAllPersistedRules` + `RulesFacade`(keyset 翻页收口:显式 limit 200 / 游标失效丢游标
|
|
29
|
+
重列一次 / 页数硬帽,绝不交半份清单)· `classifySkippedReason`(散文前缀分类)。
|
|
30
|
+
⚠️ 相对 cli 源形收紧两处(codex F2 两轮,各有门回归钉):
|
|
31
|
+
① `listAllPersistedRules` 的 params **不收调用方 cursor**(类型 `Omit<…,'cursor'>` + 运行期
|
|
32
|
+
剥除留痕)—— 源形的 `...params` 会把外来 cursor 原样送出,「列全」从中途起步却报完整清单;
|
|
33
|
+
② 页体 **fail-closed 窄化** —— 源形把坏形 2xx 页(`{rev:9}` 无 rules 数组 / rev 非有限数 /
|
|
34
|
+
nextCursor 空串或非串)静默认证成「完整(空)清单」,治理面据此宣称零规则而活规则不可见;
|
|
35
|
+
现坏形页一律 `{ok:false, failure:{kind:'error'}}`,nextCursor 只认「缺席=终页/非空串=续页」;
|
|
36
|
+
轮三追钉:跨页 rev 漂移(2xx 换 rev = 混合快照)与坏规则行(撤销承重两键 `rule`/`scope`
|
|
37
|
+
非空串,坏行=坏页;展示键不过度收紧,未知附加键照收)同臂 fail-closed。
|
|
38
|
+
- **persist-ack 读口合成一处(A-028.14 收口本体,⚠️ 行为收紧)**:`readRulePersistOutcome`
|
|
39
|
+
的三态判决改从 `readToolApprovalRespondAck` 的结构窄化产物导出 —— `rulePersisted` /
|
|
40
|
+
`ruleRefusal` 从此单一台账。**半形 ack(如 `{rulePersisted:true}` 无 `delivery` /
|
|
41
|
+
`approvalId` / 闭集 `decision`)从「读成 persisted」收紧为 `unknown`**(一张连回执身份都
|
|
42
|
+
不成形的 ack 不驱动「已保存」告知);真 server ack 恒合形,真车道零行为差。门有反向钉。
|
|
43
|
+
- **`hitl/localAllowRule.ts`** —— durable 腿「不再询问」本地落规则的判定骨架
|
|
44
|
+
`parseLocalAllowRule(rule, expectedToolName, deps)`:窄化①(整工具拒)②(工具名对齐)
|
|
45
|
+
③(字面锚,转义星是字面量)⑤(裸解释器/包装器前缀,只对 Bash 族咨询)⑤b(canonical
|
|
46
|
+
危险规则谓词叠加,[3925] P0 跟修的包位)。规则语法(parse/format)与危险谓词经
|
|
47
|
+
`LocalAllowRuleDeps` 注入(parkOwnership deps 同形)—— 谓词表本体(BARE_SHELL_PREFIXES /
|
|
48
|
+
dangerousPatterns)留宿主,包内绝不自建第二份名单。🔴 拒绝集文案是三端可观察行为
|
|
49
|
+
(cli 128 断言套逐字锚),门内逐字节钉。
|
|
50
|
+
- 消费提示(cli 1.0.77 起换装;desktop/web 接 durable 行第三态时直接消费本骨架):壳侧
|
|
51
|
+
`persistedRulesWire` 的 CC settings 三层 fs 读、SDK facade 装配、通知呈现均留壳原位。
|
|
52
|
+
|
|
19
53
|
## 0.30.1
|
|
20
54
|
|
|
21
55
|
- **#244 F1 决断卡链换装批 · 包半场(hitl/armedGateRegistry + hitl/planReviewWire;A-028.3/.4 二段,
|
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.30.
|
|
38
|
+
**Version:** 0.30.2
|
|
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
|
|
@@ -251,6 +251,7 @@ public-surface guard checks that last one).
|
|
|
251
251
|
| `scripts/run-durable-card-display-keys-test.mjs` | The durable approval row's two display keys survive the row→card recast in `surfaceFsApprovalAndDecide`: `governanceForced` stamps on strict `true` only (absence is "no evidence", never `false`), `ruleSuggestions` passes through the same shape-narrowing reader as the live-frame leg and lands on the **read-only** card key — plus a standing pin that the durable leg never stamps the redeemable `ruleSuggestions` card position (the `/decide` body has no rule slot; offering a "don't ask again" option there would be an affordance nothing can honour) |
|
|
252
252
|
| `scripts/run-integration-doc-freshness-test.mjs` | The **integration contract** (`docs/INTEGRATION-CLIENTS.md`) and the **changelog** (`CHANGELOG.md`) checked against the code, because a document with no guard rots — this one had a whole nest of drift found on it within a day of being written. Five directions, each a claim a machine can actually evaluate. (1) *Counting discipline*: the version-anchor row for the guard count may no longer carry a hand-copied number at all — it changes every time a guard is added, and writing it down is planting a timer; the export counts that are still hand-copied (the surface total, the test-hook count, the sentence describing the surface's internal composition, the sum of the sixteen domain rows, and the three sub-counts) are each compared against a value **derived** from `public-export-baseline.json`, which is the drift a human reviewer caught last time. (2) *Coordinates alive*: every `src/` `scripts/` `docs/` path the doc quotes must be on disk **and tracked by git** — on disk is not in the repo, and a doc that points readers at a file living only in its author's working tree sends every clone to nothing. A file landing in the same commit takes a named carve-out that **stops applying** the moment the file is really tracked (it can no longer let anything through, and the guard prints a line asking for it to be deleted) — deliberately not a red, since turning red on the very commit that lands the file would just manufacture a break that only a follow-up commit could clear. (3) *Arm tables*: the `hitl_out_of_slice` row and the `not_in_slice` fenced list must equal, name for name and in **both** directions, the case labels that really fall into those two buckets — read through the **TypeScript AST**, since which bucket an arm lands in is decided by the argument to `nothing(...)` and by nothing a comment says. The extractor is anchored to the one production projector: exactly one function named `eventToSdkMessage`, exactly one `switch (ev.type)` inside it, and no repeated case label — anything else is a broken anchor rather than a verdict, because a second same-shaped switch elsewhere in the file would otherwise overwrite the real one's conclusions and leave the doc agreeing with a switch nobody runs. The list is delimited by a machine-readable fence rather than by section headings, because the same section also names the terminal arms as a counter-example and prose boundaries cannot tell a member from a foil. (4) *Released sections are frozen*: an **append-only ledger** carries every version ever published — its number, the commit it was published from, and the sha256 of its section — and each one is checked, not just the current release, since pinning only the latest would set every earlier version free the moment the next one ships. The ledger cannot vouch for itself either: each recorded hash is **re-derived from that release commit** through git, so editing an old section and its constant together no longer passes — and the commit the row names is in turn checked against the `gitHead` npm recorded at publish time, which is the one value this repository cannot rewrite, so pointing an old version at a freshly written commit does not pass either. The *set* of versions that must be frozen comes from the registry too, so deleting an old row together with its section — which would otherwise remove that version from every set the guard looks at — is red rather than invisible. A failed registry call is classified rather than swallowed, and the classification consults the registry's own status code *before* it considers connection-level symptoms, so an auth refusal whose body happens to mention the network is still red rather than a skip. The version set is compared as full SemVer including prereleases — matching only `x.y.z` would silently drop a published `0.30.0-beta.1` and reopen the very hole this direction closes — and section headings are matched on a whole-version boundary so a stable release cannot bind itself to the release-candidate section sitting above it. Publishing itself is a two-phase protocol rather than a paradox: before a release, exactly one row may be marked pending and must name the current `package.json` version, exempt from the checks whose inputs do not exist yet; once the registry has that version the row must be promoted, so the temporary state cannot survive its own release. And because the pending exemption rests entirely on "this version is not out yet," it is refused outright when the registry cannot be reached to confirm that — an unverifiable premise is not a licence. Three reverse directions close the rest: a section claiming to be released but absent from the ledger, a ledger entry whose section has vanished, and a `package.json` version that was never frozen. Publishing appends a row; it never rewrites one. (6) *Sentinels*: the readers §5a hands hosts for "is this port installed" are checked against what the source actually declares it returns — `hasXxx()` is a `boolean`, the card port / HITL surface / wire target return `T | null`, the `installHost` family returns `T | undefined`. Testing a `null`-returning reader for `!== undefined` is *always true*, and a self-check that passes whether or not the port is installed is worse than none, because hosts retire their own fallback on the strength of it. Both directions are red: an implementation that changes its sentinel without the doc following, and a doc that names the wrong one. The roster covers the zero-argument readers and their `*For` variants alike — a multi-session host reads the variants, so leaving them off would let exactly the surface desktop depends on drift unwatched — and the §5a table and the §8-B checklist line are each checked against the source, because hosts tick the checklist, and a guard that only watches the prose table misses the line people actually follow. (5) *Packaging*: the README ships with the package and opens by pointing hosts at the integration doc, and the checklist names two more files as required reading before an upgrade — all three must really appear in the `npm pack` manifest, or an npm consumer follows a relative link that npmjs rewrites onto a private repository. Missing tooling never takes the whole verdict down with it: when git, npm or the registry is unreachable those legs print the `SKIPPED-SECTION` marker and the rest still judges, while a release commit the ledger names but git cannot resolve is red rather than skipped. The guard says in its own header what it does **not** do: it judges counts, coordinates, arm sets, released bytes and the packing list — whether a sentence is *right* is still for review and for the hosts to report |
|
|
253
253
|
| `scripts/run-type-superset-ledger-test.mjs` | The type/wire **superset ledger** (`docs/type-superset.json`): positions this package adds on top of a CC-shaped contract, each carrying the evidence for what CC's own type surface does or does not have there. Completeness is deliberately uneven and the ledger says so. The `_sema_*` private-key class is checked in **both** directions (a key in the source that never entered the ledger is red, naming key and file; a ledger row whose key left the source is red) — but only for keys written as literals, which is the convention the ledger mandates. A key assembled by string arithmetic is beyond what any static rule can enumerate, so the guard fails closed on every shape it *can* decide (a bare `_sema_` prefix is red wherever it appears, save one pinned guard site) and leaves the rest as a convention violation for review to catch, rather than claiming a completeness it does not have. The two hand-surveyed classes are only checked for coordinate and evidence integrity, never discovered. Both directions read the source through the **TypeScript AST**, not a text scan, and they read two different sets out of it. A *key site* is an identifier, or a string whose whole value is the key — so `'_sema_decision-v2'` is carried whole rather than truncated at the first non-identifier character into some *other* key that happens to be registered. A *mention* is the key appearing inside a longer string, which is prose, not usage. The staleness direction counts key sites only: a comment or a doc sentence left behind after the last real mint site is deleted must not keep the row alive (mutation-proven — with both the comment and the prose string untouched, removing the one real site turns the guard red). And because a prefix can be concatenated or interpolated into a key no static set will ever see, the bare `_sema_` literal is refused outright rather than traced: every occurrence is red except the single inline `startsWith` guard the sanitizer needs, because the set of expressions a bare prefix can travel through on its way to a concatenation is open-ended and enumerating it is always one form behind. Every row's `host` must still resolve, with the key being a real **member of that declaration** rather than a string occurring somewhere in the same file — `governanceForced`/`delegation` each live on two different shapes in one file, and a member commented out is a member deleted, which a text-shaped check happily reads as still present. And the direction worth the most: each machine-form `ccAbsenceEvidence` is re-derived from the row's own `key` — the ledger's recorded string must match that derivation verbatim, since a row quietly witnessing `\bnever_present\b` is green forever while watching nothing (mutation-proven: the same edit passes the unbound form and is caught by the bound one) — and the check runs against the names the installed `@sema-agent/agent-types` `.d.ts` set actually declares, parsed with the TypeScript AST rather than grepped, so a name CC merely mentions in a comment cannot force the row into the manual escape hatch and thereby retire the very witness that was supposed to fire the day CC declares that name for real. That escape hatch is gated by an allowlist living **in the guard**, not the ledger, so claiming it costs a reviewed diff. Missing material never reads as a pass, and the verdict splits by *why* it is missing: no TypeScript parser skips the suite before it starts; a missing `agent-types` still runs and prints the first three directions, then exits **1** when `package.json` declares the mirror but it is not installed — a broken install must not retire the repository's only "the day CC declares this name" alarm, and reporting it as a skip would leave "never evaluated" and "evaluated, no drift" indistinguishable to the runner — and exits 3 only when nothing declares the mirror at all, which is the one case where the direction genuinely does not apply. Either way a run that evaluated no witness is never counted as one that did. When the mirror *is* present its **installed version** is witnessed too (the two declared floors must agree with each other and the installed copy must meet them), since four preflight probes are satisfied by an arbitrarily stale mirror — they prove the extractor speaks, not that it is current. Every direction carries a positive control — known-present CC symbols, a comment-only sample proving the extractor distinguishes declaration from mention, and synthetic corpora fed through the **same** discriminator function the real verdict uses, so a verdict quietly rewritten to return nothing takes its own control down with it |
|
|
254
|
+
| `scripts/run-rules-side-test.mjs` | The persisted-permission-rules lane's shared decision half. The two capability bits are checked as **two independent gates** — a worker can honestly advertise the rules lane while predating the revoke routes, and that shape must *hide* the governance surface rather than render a dead entry. Failure classification is by **disposition, not cause**: the two 404s (route missing vs. dead ticket) never share a bucket, a 503 `rule_import_retry` means *the ticket is still alive* (the opposite handling of a dead one), and a stale-cursor 400 drops the cursor and re-lists from the top exactly once — never resuming a stale keyset, never surfacing a partial governance list, and never paging past the hard cap. The persist-ack reader is **merged into** `readToolApprovalRespondAck`: the three-state verdict (`persisted` / `refused` / `unknown`) is derived only from an ack that passed the package's structural narrowing, and a half-shaped object such as `{rulePersisted: true}` with no `delivery` reads as `unknown` — the pre-merge shell read would have said `persisted`, which is precisely the double-ledger drift this file closes, so that case is pinned in reverse. The local-allow-rule skeleton pins all five narrowings (whole-tool, tool-name match, literal anchor with the escaped-star counter-example, bare interpreter prefix consulted only for Bash, and the canonical dangerous-pattern overlay) **with their refusal strings byte-for-byte** — the cli's 128-assertion suite anchors the same strings, so a one-character edit here changes observable behaviour on three clients — and asserts the parse is a pure function of its input, because the same call backs both "render the option" and "resolve the selected value" |
|
|
254
255
|
| `scripts/run-park-decision-layer-test.mjs` | The decision layer behind the "stuck behind a card" family, shared by every client. A pending row that is **not in the queue** is three states, not one: a bounded, interruptible re-probe loop distinguishes *a decidable row*, *not born yet* (no positive evidence that anything settled — an empty queue proves nothing) and *settled elsewhere*, always probes at least once so a zero budget keeps the pre-fix semantics verbatim, cuts a hung read face off at the window rather than only noticing afterwards, and reports the honest failure when the window is spent instead of inventing a decision. The decision-note reader is likewise three-state: an explicit `noteRecorded: false` outranks an echoed note body, absence renders **no line at all**, and untrusted note text is flattened and bounded before it ever reaches a renderer. Row routing anchors on the deciding quantity — a row carrying `gateKind: "human"` with `toolName: "Write"` is a tool gate, because `human` is the engine's *generic* "someone must decide", not a synonym for a question — and the queue scan refuses to surface a row it cannot positively prove belongs to this session. A chain that fails after the row vanished is split by whether a card was ever presented: decided-elsewhere, or not-its-turn-yet. A row-level single-flight makes "at most one card per pending item" structural rather than incidental. The resume three-way card pins the option **order** (the zero-effect choice sits at index 0, because the frame carries no default-focus field and a stray Enter must not attach or cancel), renders only options the wired verbs can honour, collapses every ambiguous answer to zero action, omits the liveness line entirely when the engine gave no evidence, and — when there is no card lane at all — prints three real routes and exits on a dedicated code rather than reporting success |
|
|
255
256
|
| `scripts/run-selfheal-reopen-test.mjs` | The 409 active-run self-heal decision chain: `governanceForced` narrows on strict `true` only; triage prefers the wire's `pendingGate.kind` and falls back to the status table (an off-table kind is never guessed into a card arm — hands-off plus the honest wording); a first-sight card makes zero closed/reopened claims and a host presentation receipt of `presented: false` demotes the outcome to reopen-failed; park-row ownership is a fail-closed positive proof (own-run ledger or session id — unprovable is not owned); the three gate-identity key literals live in exactly one mint (`hitl/gateIdentity.ts`, AST string-token scan); the armed-gate presentation ledger is per-session; and the `plan_review` reopen arm shares the arm arm's card body, three-state verdict and delivery pipe, consuming the presentation history once a decision is delivered. The same chain also carries the `running` three-way card: both plan-family gate kinds route to the plan arm and all four ask-family kinds to the ask arm (an off-table kind still never gets guessed into either); the card is offered only for verbs that can actually be honoured and a missing presenter means zero action rather than a silent cancel; a steer is sent **exactly once** with its three delivery outcomes worded apart (a `queued` receipt is the wire correcting the triage input, so the named park word decides which card gets reopened, and an unrecognised park word drives neither arm), and a steer failure is split into *provably not delivered* (4xx) and *delivery unknown*, because telling a user to resend a non-idempotent instruction that may already have landed is how duplicates get made. After a user-chosen cancel, "the session is free" is asserted only from a whitelist of terminal states — park states hold the claim, an unrecognised state word is not a release, a failed read is *unknown* rather than a release, and only a 404 counts as one — and the honest timeout line quotes how long it really waited |
|
|
256
257
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hitl/localAllowRule.ts — durable park 腿「不再询问」第三态的**本地落规则判定骨架**
|
|
3
|
+
* (#244 F2 上收件,clay 硬排期;源形 = cli `src/sema/localAllowRuleWrite.ts` 的
|
|
4
|
+
* `parseLocalAllowRule` + 窄化五步。settings 写入、内存权限态、托管策略读口全部留宿主)。
|
|
5
|
+
*
|
|
6
|
+
* ── 为什么这半场是三端公共的 ────────────────────────────────────────────────────────────────
|
|
7
|
+
* durable park 腿的候选走行上的 `PendingCheckpoint.ruleSuggestions` → 卡入参**只读键**
|
|
8
|
+
* `ruleSuggestionsReadOnly`,而 `/decide` 的体(SDK `AskDecisionBody`)是**闭集、无规则位**——
|
|
9
|
+
* 兑付口只能是**客户端本地** settings。哪些候选可本地兑付、写下去的 canonical 字节是什么,
|
|
10
|
+
* 是纯**形**判定:desktop/web 接到同一份行供给时要的正是同一把窄化(各写各的必漂,而漏一步
|
|
11
|
+
* 就是跨会话任意代码放行 —— [3925] P0 解释器族绕过案的教训)。
|
|
12
|
+
*
|
|
13
|
+
* ── 🔴 五道窄化(本地写入独有;wire 兑付腿没有,那条只搬字节给 server)────────────────────────
|
|
14
|
+
* 1. **整工具规则不写**。`Bash` / `Bash(*)` 解析后退化成整工具 allow,比这只 ask 的动词宽得多
|
|
15
|
+
* (CC `suppressAlwaysAllowRule` 同理;durable 行上没有这一位过境,按**收紧方向**自守)。
|
|
16
|
+
* 2. **规则得是给这只 ask 的那个工具的**(`expectedToolName`)。规则文本是**引擎**铸的,可能用
|
|
17
|
+
* 引擎侧 wire 工具名(`file_write(/tmp/x)` 解析得干干净净,写进 settings 后**永远匹配不上**)
|
|
18
|
+
* —— 落盘不报错、也不生效的死规则,比拒绝更坏。
|
|
19
|
+
* 3. **没有字面锚的规则不写**。`Bash( * )` / `Bash(**)` 等拼法 ruleContent **非空**却与被禁的
|
|
20
|
+
* `Bash(*)` 完全等效(宿主匹配器先 trim 再把未转义 `*` 展成 `.*`)。判据锚在「这条规则还有
|
|
21
|
+
* 没有约束力」:去掉全部**未转义**通配符后必须还剩字面字符(转义星 `\*` 是字面星号,照留)。
|
|
22
|
+
* 5. **解释器/包装器裸前缀不写**(`Bash(bash:*)` / `Bash(sudo:*)` …):过得了窄化③(有字面锚)
|
|
23
|
+
* 却等于放行任意命令(`bash -c "任何东西"`)。谓词经 {@link LocalAllowRuleDeps} 注入 ——
|
|
24
|
+
* 策略表本体(BARE_SHELL_PREFIXES / dangerousPatterns)留宿主,绝不在包内自建第二份名单。
|
|
25
|
+
* 5b. **canonical 危险规则谓词叠加**([3925] P0 跟修):⑤ 只覆盖 shell/wrapper,而
|
|
26
|
+
* python/node/npx/eval/exec/ssh 等**解释器族**的规则形同样等于任意代码放行(`python -c`)。
|
|
27
|
+
* 同经 deps 注入宿主的 `isDangerous{Bash,PowerShell}Permission`(auto-mode 入口同一把谓词)。
|
|
28
|
+
* ⑤ 不撤:它做 basename/.exe 路径归一(`/bin/bash`),canonical 谓词是纯字面形不做归一 ——
|
|
29
|
+
* 两把互补,各管一族拼法。
|
|
30
|
+
* (④ 托管策略是**可变量**,刻意不在本骨架里 —— 解析必须纯:同一串输入永远同一个结论,否则
|
|
31
|
+
* 已上屏的 option value 会反查不到,用户的选择被丢掉。策略由宿主在渲染面与写入面各自查。)
|
|
32
|
+
*
|
|
33
|
+
* 🔴 **纯函数 / 只看形**:不读 settings、不读宿主状态、不读策略;deps 注入的谓词也必须是
|
|
34
|
+
* 纯函数(pattern 表 + 启动期常量),否则「渲档位」与「按 value 反查候选」两处调用会漂。
|
|
35
|
+
* 🔴 **错误文案是契约面**:cli 常驻套(localRuleSuggestionsCard,128+ 断言)对拒绝集逐字锚 ——
|
|
36
|
+
* 上收前后拒绝集必须逐字相等,改一处文案就是改三端的可观察行为。
|
|
37
|
+
* 🔴 UNTRUSTED:`rule` 是 wire 串(core `inlineUntrusted` + server `redactSecrets` 之后)。它只被
|
|
38
|
+
* 喂给宿主自己的规则解析器与 settings 写口,不回喂模型、不进工具入参、不当代码执行。
|
|
39
|
+
*/
|
|
40
|
+
import type { PermissionRuleValue } from '@sema-agent/agent-types';
|
|
41
|
+
export type { PermissionRuleValue };
|
|
42
|
+
/**
|
|
43
|
+
* 宿主注入面(parkOwnership deps 同形:缺一不可的**纯函数**切片,策略表本体留宿主)。
|
|
44
|
+
* · `parseRule` / `formatRule`:宿主的规则语法(CC `Tool(content)` 形 + 转义 + legacy 工具名
|
|
45
|
+
* 归一)。解析失败**抛**(骨架把它折成 `ok:false`);`formatRule` 的产物 = canonical 字节
|
|
46
|
+
* (真正会被写进 settings 的那串,渲染面必须渲它而不是 wire 原文)。
|
|
47
|
+
* · `bashRuleContentHasDangerousBarePrefix`:窄化⑤ 的谓词(宿主 BARE_SHELL_PREFIXES 表,
|
|
48
|
+
* 含 basename/.exe 路径归一)。
|
|
49
|
+
* · `isDangerousBashPermission` / `isDangerousPowerShellPermission`:窄化⑤b 的 canonical 危险
|
|
50
|
+
* 规则谓词(宿主 dangerousPatterns 表;谓词自己按 toolName 分派,非 Bash/PowerShell 恒 false)。
|
|
51
|
+
*/
|
|
52
|
+
export interface LocalAllowRuleDeps {
|
|
53
|
+
parseRule(rule: string): PermissionRuleValue;
|
|
54
|
+
formatRule(value: PermissionRuleValue): string;
|
|
55
|
+
bashRuleContentHasDangerousBarePrefix(ruleContent: string): boolean;
|
|
56
|
+
isDangerousBashPermission(toolName: string, ruleContent: string): boolean;
|
|
57
|
+
isDangerousPowerShellPermission(toolName: string, ruleContent: string): boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 解析结局。`ok:false` = 这条候选**不可本地兑付**(渲染面据此不渲该档)。
|
|
61
|
+
* `canonical` = 真正会被写进 settings 的那串字节(`deps.formatRule` 的产物)——
|
|
62
|
+
* 🔴 渲染面**必须渲它**而不是 wire 原文:两者在带括号的命令上会差一层转义,渲原文就等于
|
|
63
|
+
* 「展示的规则和落地的规则不是同一串」。
|
|
64
|
+
*/
|
|
65
|
+
export type LocalAllowRuleParse = {
|
|
66
|
+
ok: true;
|
|
67
|
+
value: PermissionRuleValue;
|
|
68
|
+
canonical: string;
|
|
69
|
+
} | {
|
|
70
|
+
ok: false;
|
|
71
|
+
error: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* 规则原文 → 宿主规则空间的 `PermissionRuleValue`,带文件头注的**形**类窄化(①②③⑤⑤b)。
|
|
75
|
+
*
|
|
76
|
+
* @param expectedToolName 这只 ask 自己的工具名(卡的 `tool.name`)。给了就必须相等 —— 见头注
|
|
77
|
+
* 窄化②:引擎侧 wire 工具名(`file_write` 之类)解析得干净但在宿主的规则空间里是死规则。
|
|
78
|
+
* **缺省不校**:纵深第二道(写口)不知道也不该猜「这只 ask 是哪个工具」,那是卡面的知识。
|
|
79
|
+
*/
|
|
80
|
+
export declare function parseLocalAllowRule(rule: string, expectedToolName: string | undefined, deps: LocalAllowRuleDeps): LocalAllowRuleParse;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** 规则空间里 Bash 工具的名字(窄化⑤ 只对这一族适用;⑤b canonical 谓词管 PS 族)。 */
|
|
2
|
+
const BASH_TOOL_NAME_FOR_RULES = 'Bash';
|
|
3
|
+
/**
|
|
4
|
+
* 规则原文 → 宿主规则空间的 `PermissionRuleValue`,带文件头注的**形**类窄化(①②③⑤⑤b)。
|
|
5
|
+
*
|
|
6
|
+
* @param expectedToolName 这只 ask 自己的工具名(卡的 `tool.name`)。给了就必须相等 —— 见头注
|
|
7
|
+
* 窄化②:引擎侧 wire 工具名(`file_write` 之类)解析得干净但在宿主的规则空间里是死规则。
|
|
8
|
+
* **缺省不校**:纵深第二道(写口)不知道也不该猜「这只 ask 是哪个工具」,那是卡面的知识。
|
|
9
|
+
*/
|
|
10
|
+
export function parseLocalAllowRule(rule, expectedToolName, deps) {
|
|
11
|
+
if (typeof rule !== 'string' || rule === '') {
|
|
12
|
+
return { ok: false, error: 'rule text is empty' };
|
|
13
|
+
}
|
|
14
|
+
let value;
|
|
15
|
+
try {
|
|
16
|
+
value = deps.parseRule(rule);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return { ok: false, error: `rule text did not parse (${String(e)})` };
|
|
20
|
+
}
|
|
21
|
+
if (typeof value.toolName !== 'string' || value.toolName === '') {
|
|
22
|
+
return { ok: false, error: 'rule text carries no tool name' };
|
|
23
|
+
}
|
|
24
|
+
// 窄化①:整工具 allow 比这只 ask 的动词宽得多(CC suppressAlwaysAllowRule 同理)。
|
|
25
|
+
// `Bash(*)` 也走这里 —— 解析器把它归一成整工具形,所以判据只有一条「有没有 ruleContent」。
|
|
26
|
+
if (value.ruleContent === undefined || value.ruleContent === '') {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
error: 'candidate is a whole-tool allow rule (broader than this ask) — not offered locally',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// 窄化②:规则得是给**这只 ask 的那个工具**的(否则是一条落盘不报错、也永远不生效的死规则)。
|
|
33
|
+
if (expectedToolName !== undefined && value.toolName !== expectedToolName) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: `candidate names tool "${value.toolName}" but this ask is for "${expectedToolName}" — a rule for another tool would never match`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// 窄化③:去掉未转义通配符后必须还剩字面字符,否则这条规则与整工具放行等效(见头注 3)。
|
|
40
|
+
if (!hasLiteralAnchor(value.ruleContent)) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: 'candidate is all wildcard (matches every command for this tool) — equivalent to a whole-tool allow, not offered locally',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// 窄化⑤:解释器/包装器裸前缀(`bash:*` / `env:*` / `sudo:*` …)——有字面锚但等于任意命令放行。
|
|
47
|
+
// 谓词表属主在宿主(bashPermissions 的 BARE_SHELL_PREFIXES),经 deps 注入,绝不自建第二份名单。
|
|
48
|
+
if (value.toolName === BASH_TOOL_NAME_FOR_RULES &&
|
|
49
|
+
deps.bashRuleContentHasDangerousBarePrefix(value.ruleContent)) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: 'candidate is a bare interpreter/wrapper prefix (bash/sh/env/sudo/…) — persisting it would authorize arbitrary commands, not offered locally',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// 窄化⑤b([3925] P0 跟修):canonical 危险规则谓词叠加 —— python/node/npx/eval/exec/ssh 等
|
|
56
|
+
// 解释器族的五种规则形同样等于任意代码放行(`python -c`)。谓词自己按 toolName 分派
|
|
57
|
+
// (非 Bash/PowerShell 恒 false),纯函数,不破坏本函数「同输入恒同结论」的承重性质。
|
|
58
|
+
if (deps.isDangerousBashPermission(value.toolName, value.ruleContent) ||
|
|
59
|
+
deps.isDangerousPowerShellPermission(value.toolName, value.ruleContent)) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: 'candidate allow-rule matches a code-execution interpreter/wrapper pattern (python/node/eval/ssh/…) — persisting it would authorize arbitrary code, not offered locally',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, value, canonical: deps.formatRule(value) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 规则内容里还有没有**字面**约束(窄化③ 的判据)。
|
|
69
|
+
*
|
|
70
|
+
* 只剥**未转义**的 `*`:转义星 `\*` 是字面星号(与宿主匹配器 `hasWildcards` 的判据同源),
|
|
71
|
+
* 剥掉它会把「命令就叫 `*`」这条有约束力的规则误判成通配。剥完 **trim**,空 ⇒ 这条规则对该工具的
|
|
72
|
+
* 任何命令都成立。
|
|
73
|
+
*
|
|
74
|
+
* 末尾的 `trim()` 是承重的,不是顺手:宿主匹配器自己入口也先 `pattern.trim()`,所以「只剩空白」
|
|
75
|
+
* 与「什么都不剩」在匹配语义上是同一件事 —— `Bash(* *)` / `Bash( ** )` 这类「看起来有字符」的
|
|
76
|
+
* 拼法因此同样被拦下(cli 常驻套 ①-17 五种拼法全红)。
|
|
77
|
+
* ⚠️ 仍是**收紧方向**的近似:带一个字面非空白字符的极宽规则(如 `Bash(*a*)`)有字面锚、会放行。
|
|
78
|
+
* 真正按匹配器语义算「这条规则有多宽」要把 per-form 的判定搬过来,记为 follow-up;当前判据不漏
|
|
79
|
+
* 「等效于 `Bash(*)`」的那一族,而那一族才是题面。
|
|
80
|
+
*/
|
|
81
|
+
function hasLiteralAnchor(ruleContent) {
|
|
82
|
+
let out = '';
|
|
83
|
+
for (let i = 0; i < ruleContent.length; i++) {
|
|
84
|
+
const ch = ruleContent[i];
|
|
85
|
+
if (ch === '\\' && i + 1 < ruleContent.length) {
|
|
86
|
+
// 转义序列整段保留(`\*` 的星号是字面量,`\\` 是字面反斜杠)。
|
|
87
|
+
out += ruleContent[i + 1];
|
|
88
|
+
i++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ch === '*')
|
|
92
|
+
continue; // 未转义通配符:剥掉
|
|
93
|
+
out += ch;
|
|
94
|
+
}
|
|
95
|
+
return out.trim() !== '';
|
|
96
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hitl/persistedRulesWire.ts — 持久权限规则(design/179 / design/203)车道的**通用判定半场**
|
|
3
|
+
* (A-028.14 上收件,#244 F2,2026-08-14;源形 = cli `src/sema/persistedRulesWire.ts` 的
|
|
4
|
+
* 纯判定/wire 面;CC settings 三层 fs 读、facade 装配(SDK 短命 client)、通知呈现全部留宿主)。
|
|
5
|
+
*
|
|
6
|
+
* ── 病(census top-08 #1 分脑实证)───────────────────────────────────────────────────────────
|
|
7
|
+
* 包 `hitl/toolApprovalWire.ts` 的 `readToolApprovalRespondAck` 已把 `rulePersisted` /
|
|
8
|
+
* `ruleRefusal` 归一进 `ToolApprovalRespondAck`(delivery/decision 闭集门之后),而 cli 对同两键
|
|
9
|
+
* **另持一份裸读**(入参 `unknown`,无上述两道门)—— 同两键两处读法在跑 = 双份台账,server 增键
|
|
10
|
+
* /改形时两处各漂各的。web 消费同一帧族([3536] 记账)时还会长出第三份。
|
|
11
|
+
* 收口:persist-ack 读口({@link readRulePersistOutcome})与 `readToolApprovalRespondAck`
|
|
12
|
+
* **合成一处** —— 三态判决只从**已过包内结构窄化**的 ack 上导出,不再对 wire 原料二次开读。
|
|
13
|
+
*
|
|
14
|
+
* ── 🔴 能力位 gate,不 trial-by-501 ──────────────────────────────────────────────────────────
|
|
15
|
+
* SDK `resources/rules.d.ts` 头注逐字:`capabilities.permissionRules` 与这台 server **所有**规则口
|
|
16
|
+
* 的 501 是同一条谓词(`PERMISSION_RULES_ENABLED ∧ 规则店 wired`),**按它 gate**,别按版本号猜、
|
|
17
|
+
* 也别拿 501 当探针。撤销面两口(GET/DELETE /v1/rules)自 server 7.12.0 才有路由,而
|
|
18
|
+
* `permissionRules` 比撤销面**早出生** ⇒ 一台 7.11.0 worker 会报 `permissionRules:true` 而这两口
|
|
19
|
+
* 404 `not_found.route`。所以撤销面**另按 `capabilities.permissionRulesRevoke`**:
|
|
20
|
+
* · 在场 ⇒ 路由已铸(答不答仍看 `permissionRules`);
|
|
21
|
+
* · 缺席 ⇒ 这台 worker 比撤销面老 ⇒ **藏起治理入口**(不渲死按钮)。
|
|
22
|
+
* 读法用包内同步口 `engineCapTrue`(engineCapsCache;未判/缺键 = false = 藏,version-safe)。
|
|
23
|
+
* baseUrl 由宿主显式传入(cli 的 `SEMA_LIVE_BASEURL` 缺省读法留壳 —— env 是宿主资产)。
|
|
24
|
+
*
|
|
25
|
+
* ── 🔴 404 禁按成因分支 ──────────────────────────────────────────────────────────────────────
|
|
26
|
+
* redeem 的 404 `not_found.rule_ticket` 是同形信封(伪造/别人的/过期/已用过 + 记录/载荷/确认失败
|
|
27
|
+
* 七类共用一格,wire 上分不出来是设计)。唯一可分的是 503 `state.rule_import_retry`:那一支意味着
|
|
28
|
+
* **票还在**,原样重试即可(与「这张票没了」是相反的处置)。
|
|
29
|
+
*
|
|
30
|
+
* ── UNTRUSTED ───────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
* 规则文本与 `skipped[].reason` 都是引擎/用户 settings 侧的内容,只渲染绝不当代码用;`reason` 是
|
|
32
|
+
* **给人看的散文不是机读码**(SDK 头注),分类只许按第一个 `:` 前缀,并且要容得下**没有前缀**的形。
|
|
33
|
+
*/
|
|
34
|
+
import type { CcImportLayer, CcImportPrepareResult, CcImportRedeemResult, PersistedRule, RuleListParams, RuleListResult, RuleRevokeRequest, RuleRevokeResult } from '@sema-agent/sdk';
|
|
35
|
+
/** 规则车道消费的 `client.rules` 切片(注入缝:宿主给真 SDK facade,测试给假件)。 */
|
|
36
|
+
export interface RulesFacade {
|
|
37
|
+
list(params?: RuleListParams, opts?: {
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}): Promise<RuleListResult>;
|
|
40
|
+
revoke(input: RuleRevokeRequest, opts?: {
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}): Promise<RuleRevokeResult>;
|
|
43
|
+
ccImportPrepare(layers: CcImportLayer[], opts?: {
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
}): Promise<CcImportPrepareResult>;
|
|
46
|
+
ccImportRedeem(ticket: string, opts?: {
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}): Promise<CcImportRedeemResult>;
|
|
49
|
+
}
|
|
50
|
+
/** 规则车道在不在(四口 501 同源谓词)。未判/缺键/base 缺席 = false = 整面藏。 */
|
|
51
|
+
export declare function persistedRulesLaneAvailable(baseUrl: string | undefined): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* 治理面(list + revoke)在不在。**两段合取**:
|
|
54
|
+
* · `permissionRulesRevoke` 在场 = 撤销面路由已铸(缺席 = worker 比撤销面老 ⇒ 藏);
|
|
55
|
+
* · `permissionRules` 为真 = 车道真答话(店 + 旋钮)。
|
|
56
|
+
* 只查前者会在「7.11.0 且规则店在」的 worker 上渲出一个恒 404 的治理入口。
|
|
57
|
+
*/
|
|
58
|
+
export declare function persistedRulesGovernanceAvailable(baseUrl: string | undefined): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* 规则口失败的**处置分类**(不是成因分类)。SDK 的 typed 错误族按 `status` + `errorCode` 读,
|
|
61
|
+
* 结构读法(不 instanceof)—— 注入面可能是假件,bundling 下 instanceof 也不该是承重判据。
|
|
62
|
+
*/
|
|
63
|
+
export type RulesFailure =
|
|
64
|
+
/** 501 `capability.rule_store_required` —— 这台部署没接规则店/旋钮关着 ⇒ 整面藏起来。 */
|
|
65
|
+
{
|
|
66
|
+
kind: 'lane-unavailable';
|
|
67
|
+
message: string;
|
|
68
|
+
}
|
|
69
|
+
/** 404 `not_found.route` —— worker 比撤销面老(≤7.11.0)⇒ 治理入口藏起来。 */
|
|
70
|
+
| {
|
|
71
|
+
kind: 'route-missing';
|
|
72
|
+
message: string;
|
|
73
|
+
}
|
|
74
|
+
/** 404 `not_found.rule_ticket` —— 这张票在这里不可用,**禁按成因分支**:丢票,重来一遍。 */
|
|
75
|
+
| {
|
|
76
|
+
kind: 'ticket-dead';
|
|
77
|
+
message: string;
|
|
78
|
+
}
|
|
79
|
+
/** 503 `state.rule_import_retry` —— **票还在**,原样重试(与 ticket-dead 相反的处置)。 */
|
|
80
|
+
| {
|
|
81
|
+
kind: 'retry-same-ticket';
|
|
82
|
+
message: string;
|
|
83
|
+
retryAfterSec?: number;
|
|
84
|
+
}
|
|
85
|
+
/** 503 `state.rule_remove_failed` —— 什么都没写,或结局不定;自己重试后用 list 对账。 */
|
|
86
|
+
| {
|
|
87
|
+
kind: 'retryable';
|
|
88
|
+
message: string;
|
|
89
|
+
}
|
|
90
|
+
/** 400 `request.query_invalid` —— 游标铸在别的 rev/principal/scope 上 ⇒ 丢游标从头列。 */
|
|
91
|
+
| {
|
|
92
|
+
kind: 'cursor-stale';
|
|
93
|
+
message: string;
|
|
94
|
+
}
|
|
95
|
+
/** 413 —— 候选总数超上限(200):缩短后重发,重试同一份没有意义。 */
|
|
96
|
+
| {
|
|
97
|
+
kind: 'too-many-candidates';
|
|
98
|
+
message: string;
|
|
99
|
+
}
|
|
100
|
+
/** 403 `auth.operator_only` —— 越权读/撤销(替别人)被拒。 */
|
|
101
|
+
| {
|
|
102
|
+
kind: 'forbidden';
|
|
103
|
+
message: string;
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'error';
|
|
106
|
+
message: string;
|
|
107
|
+
};
|
|
108
|
+
export declare function classifyRulesFailure(e: unknown): RulesFailure;
|
|
109
|
+
/** 一次「列全」的结果。`rules` 恒是**完整**清单(翻不完 ⇒ 走 failure,绝不交半份清单)。 */
|
|
110
|
+
export type ListAllPersistedRulesOutcome = {
|
|
111
|
+
ok: true;
|
|
112
|
+
rules: PersistedRule[];
|
|
113
|
+
rev: number;
|
|
114
|
+
} | {
|
|
115
|
+
ok: false;
|
|
116
|
+
failure: RulesFailure;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* 列全一位 principal 名下活着的规则。
|
|
120
|
+
*
|
|
121
|
+
* 🔴 **翻页要翻完**:`nextCursor` 缺席才是终点 —— 半途停下拿到的是一份不全的清单,而治理视图
|
|
122
|
+
* 恰恰最不能拿不全的清单当全量。所以这里 drain 到底,任何一页失败都不交部分结果。
|
|
123
|
+
* 🔴 **游标绑 `(rev, principal, scope)`**:两页之间有人加/删了规则 ⇒ 第二页 400
|
|
124
|
+
* `request.query_invalid`。处置 = **丢游标从头列一次**(静默重置成「接着上一页」会得到一份既漏行
|
|
125
|
+
* 又重行的清单);从头再撞一次 ⇒ 如实报 cursor-stale,由调用方(人按 r 刷新)决定。
|
|
126
|
+
* 🔴 **调用方 cursor 不收**(codex F2 对抗复审 [medium]):类型上剔掉 `cursor` 还不够 —— JS
|
|
127
|
+
* 调用方仍能塞进来,而首页的 `...params` 会把它原样送出 ⇒ 「列全」从**中途**开始却报 `ok:true`
|
|
128
|
+
* 完整清单(治理面据此藏掉仍然生效的规则)。运行期显式剥除 + 留痕:drain 恒从第一页起,
|
|
129
|
+
* 「接着别人的 keyset」证明不了完整性,与本函数的契约(rules 恒完整)结构性冲突。
|
|
130
|
+
*/
|
|
131
|
+
export declare function listAllPersistedRules(facade: RulesFacade, params?: Omit<RuleListParams, 'cursor'>, opts?: {
|
|
132
|
+
signal?: AbortSignal;
|
|
133
|
+
}): Promise<ListAllPersistedRulesOutcome>;
|
|
134
|
+
/**
|
|
135
|
+
* `skipped[].reason` 的分类。**只按第一个 `:` 前缀**,且必须容得下**没有前缀**的两种真值形
|
|
136
|
+
* (整层 JSON 解不开 / 条目不是串)——SDK 头注逐字:reason 是给人看的散文,不是机读码,
|
|
137
|
+
* 全串等值匹配与「裸码」两种读法都会漂。
|
|
138
|
+
* 前缀形判据刻意保守(`RuleRejectCode` 形:小写 + 点/下划线),防把散文里的第一个冒号误读成码。
|
|
139
|
+
*/
|
|
140
|
+
export declare function classifySkippedReason(reason: string): {
|
|
141
|
+
code?: string;
|
|
142
|
+
text: string;
|
|
143
|
+
};
|
|
144
|
+
/** 「不再询问到底存上了没」的三态判决(呈现半场按它渲一行,绝不据它翻转裁决)。 */
|
|
145
|
+
export type RulePersistOutcome = {
|
|
146
|
+
state: 'unknown';
|
|
147
|
+
} | {
|
|
148
|
+
state: 'persisted';
|
|
149
|
+
} | {
|
|
150
|
+
state: 'refused';
|
|
151
|
+
reason?: string;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* respond 回执上「不再询问到底存上了没」的**结构化读口**。
|
|
155
|
+
*
|
|
156
|
+
* 🔴 **单一台账**(A-028.14 收口本体):`rulePersisted` / `ruleRefusal` 的结构窄化只有
|
|
157
|
+
* {@link readToolApprovalRespondAck} 这一份 —— 本函数不对 wire 原料二次开读,ack 过不了包内
|
|
158
|
+
* 结构门(缺 `delivery:'applied'` / `approvalId` / 三词闭集 `decision`)⇒ **`unknown`**:
|
|
159
|
+
* 一张连回执身份都不成形的 ack,不配驱动一行「已保存」的用户告知(诚实缺席优先)。
|
|
160
|
+
* 收口前 cli 的裸读会把 `{rulePersisted:true}` 这类半形对象读成 `persisted` —— 那正是双份台账
|
|
161
|
+
* 各漂各的形,常驻套对这一格有反向钉。
|
|
162
|
+
*
|
|
163
|
+
* 🔴 `rulePersisted` 缺席 ≠ `false`(未带 persistRule 的回决 / 旧 server ⇒ 字段省略);
|
|
164
|
+
* 🔴 规则没存上**从不翻转裁决** —— 200 + `rulePersisted:false` + `ruleRefusal` 是诚实形,
|
|
165
|
+
* 宿主只说「这次放行了,但『不再询问』没存上」,绝不渲成整次审批失败。
|
|
166
|
+
* 🔴 `rule_lane_unavailable` **四种成因共用一格、两种是本卡局限** ⇒ 禁据一帧判断整台部署的车道
|
|
167
|
+
* 在不在(那要看能力位)。
|
|
168
|
+
*/
|
|
169
|
+
export declare function readRulePersistOutcome(ack: unknown): RulePersistOutcome;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { engineCapTrue } from '../engineCapsCache.js';
|
|
2
|
+
import { hostLog } from '../host.js';
|
|
3
|
+
import { readToolApprovalRespondAck } from './toolApprovalWire.js';
|
|
4
|
+
// ── 能力位 gate ──────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
/** 规则车道在不在(四口 501 同源谓词)。未判/缺键/base 缺席 = false = 整面藏。 */
|
|
6
|
+
export function persistedRulesLaneAvailable(baseUrl) {
|
|
7
|
+
return engineCapTrue(baseUrl, 'permissionRules');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 治理面(list + revoke)在不在。**两段合取**:
|
|
11
|
+
* · `permissionRulesRevoke` 在场 = 撤销面路由已铸(缺席 = worker 比撤销面老 ⇒ 藏);
|
|
12
|
+
* · `permissionRules` 为真 = 车道真答话(店 + 旋钮)。
|
|
13
|
+
* 只查前者会在「7.11.0 且规则店在」的 worker 上渲出一个恒 404 的治理入口。
|
|
14
|
+
*/
|
|
15
|
+
export function persistedRulesGovernanceAvailable(baseUrl) {
|
|
16
|
+
return engineCapTrue(baseUrl, 'permissionRulesRevoke') && engineCapTrue(baseUrl, 'permissionRules');
|
|
17
|
+
}
|
|
18
|
+
function errShape(e) {
|
|
19
|
+
const o = (e ?? {});
|
|
20
|
+
return {
|
|
21
|
+
...(typeof o.status === 'number' ? { status: o.status } : {}),
|
|
22
|
+
...(typeof o.errorCode === 'string' ? { errorCode: o.errorCode } : {}),
|
|
23
|
+
message: typeof o.message === 'string' && o.message !== '' ? o.message : String(e),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** `Retry-After` 秒数(SDK 把它折进 `retryAfterMs`)。非数/负数 ⇒ 缺席。 */
|
|
27
|
+
function retryAfterSecOf(e) {
|
|
28
|
+
const ms = e.retryAfterMs;
|
|
29
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
return Math.ceil(ms / 1000);
|
|
32
|
+
}
|
|
33
|
+
export function classifyRulesFailure(e) {
|
|
34
|
+
const { status, errorCode, message } = errShape(e);
|
|
35
|
+
if (status === 501 || errorCode === 'capability.rule_store_required') {
|
|
36
|
+
return { kind: 'lane-unavailable', message };
|
|
37
|
+
}
|
|
38
|
+
if (status === 404) {
|
|
39
|
+
// 🔴 两个 404 是**不同处置**(SDK 头注):route 缺席 = 版本太老(藏面);rule_ticket = 票没了
|
|
40
|
+
// (丢票重来)。其余 404 一律按通用错误 —— 绝不按成因猜。
|
|
41
|
+
if (errorCode === 'not_found.route')
|
|
42
|
+
return { kind: 'route-missing', message };
|
|
43
|
+
if (errorCode === 'not_found.rule_ticket')
|
|
44
|
+
return { kind: 'ticket-dead', message };
|
|
45
|
+
return { kind: 'error', message };
|
|
46
|
+
}
|
|
47
|
+
if (errorCode === 'state.rule_import_retry') {
|
|
48
|
+
const sec = retryAfterSecOf(e);
|
|
49
|
+
return { kind: 'retry-same-ticket', message, ...(sec !== undefined ? { retryAfterSec: sec } : {}) };
|
|
50
|
+
}
|
|
51
|
+
if (errorCode === 'state.rule_remove_failed')
|
|
52
|
+
return { kind: 'retryable', message };
|
|
53
|
+
if (status === 400 && errorCode === 'request.query_invalid')
|
|
54
|
+
return { kind: 'cursor-stale', message };
|
|
55
|
+
if (status === 413)
|
|
56
|
+
return { kind: 'too-many-candidates', message };
|
|
57
|
+
if (status === 403 && errorCode === 'auth.operator_only')
|
|
58
|
+
return { kind: 'forbidden', message };
|
|
59
|
+
return { kind: 'error', message };
|
|
60
|
+
}
|
|
61
|
+
/** 一页要多少条。**必须显式给**(codex 对抗复审 [medium] 实撞):server 缺省是 **50**,而页帽
|
|
62
|
+
* 按「200/页」算 ⇒ 真实上界只有 1250 条,一位规则多于 1250 的 principal 会恒拿到「翻不完」的
|
|
63
|
+
* 失败、整个治理面打不开,而注释还写着 5000。夹取语义在 server(非数/越界夹进 1..200),所以给
|
|
64
|
+
* 上限最省往返、也让页帽的算术与现实一致。 */
|
|
65
|
+
const PAGE_LIMIT = 200;
|
|
66
|
+
/** 页数硬帽:server 恒给 nextCursor 的坏形不该让治理面无限翻(PAGE_LIMIT × 25 = 5000 条)。 */
|
|
67
|
+
const MAX_PAGES = 25;
|
|
68
|
+
/**
|
|
69
|
+
* 列全一位 principal 名下活着的规则。
|
|
70
|
+
*
|
|
71
|
+
* 🔴 **翻页要翻完**:`nextCursor` 缺席才是终点 —— 半途停下拿到的是一份不全的清单,而治理视图
|
|
72
|
+
* 恰恰最不能拿不全的清单当全量。所以这里 drain 到底,任何一页失败都不交部分结果。
|
|
73
|
+
* 🔴 **游标绑 `(rev, principal, scope)`**:两页之间有人加/删了规则 ⇒ 第二页 400
|
|
74
|
+
* `request.query_invalid`。处置 = **丢游标从头列一次**(静默重置成「接着上一页」会得到一份既漏行
|
|
75
|
+
* 又重行的清单);从头再撞一次 ⇒ 如实报 cursor-stale,由调用方(人按 r 刷新)决定。
|
|
76
|
+
* 🔴 **调用方 cursor 不收**(codex F2 对抗复审 [medium]):类型上剔掉 `cursor` 还不够 —— JS
|
|
77
|
+
* 调用方仍能塞进来,而首页的 `...params` 会把它原样送出 ⇒ 「列全」从**中途**开始却报 `ok:true`
|
|
78
|
+
* 完整清单(治理面据此藏掉仍然生效的规则)。运行期显式剥除 + 留痕:drain 恒从第一页起,
|
|
79
|
+
* 「接着别人的 keyset」证明不了完整性,与本函数的契约(rules 恒完整)结构性冲突。
|
|
80
|
+
*/
|
|
81
|
+
export async function listAllPersistedRules(facade, params = {}, opts) {
|
|
82
|
+
const { cursor: callerCursor, ...cleanParams } = params;
|
|
83
|
+
if (callerCursor !== undefined) {
|
|
84
|
+
hostLog('debug', 'persistedRulesWire: caller-supplied cursor ignored by listAllPersistedRules — a drain that starts mid-keyset cannot prove completeness, so it always starts from the top');
|
|
85
|
+
}
|
|
86
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
87
|
+
const rules = [];
|
|
88
|
+
let cursor;
|
|
89
|
+
let rev = null;
|
|
90
|
+
let restart = false;
|
|
91
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
92
|
+
let res;
|
|
93
|
+
try {
|
|
94
|
+
// limit 显式给:①页帽的算术要与真实页大小一致(见 PAGE_LIMIT);②keyset 游标绑的是
|
|
95
|
+
// 一份确定的翻页参数,页大小在两页之间变会让「接着上一页」失去意义。调用方可覆盖。
|
|
96
|
+
res = await facade.list({ limit: PAGE_LIMIT, ...cleanParams, ...(cursor !== undefined ? { cursor } : {}) }, opts);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
const failure = classifyRulesFailure(e);
|
|
100
|
+
if (failure.kind === 'cursor-stale' && cursor !== undefined && attempt === 0) {
|
|
101
|
+
hostLog('debug', 'persistedRulesWire: rule list cursor was minted on another rev/principal/scope — dropping it and re-listing from the top (never resuming a stale keyset)');
|
|
102
|
+
restart = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return { ok: false, failure };
|
|
106
|
+
}
|
|
107
|
+
// 🔴 页体 fail-closed 窄化(codex F2 轮二 [high]):SDK 传输层只 JSON.parse,不做运行期
|
|
108
|
+
// schema 校验 —— 一个 2xx 的 `{rev:9}`(无 rules 数组)在旧读法下会被认证成「完整的空清单」
|
|
109
|
+
// (`ok:true, rules:[]`),治理面据此宣称「没有持久规则」而活规则不可见、无法撤销
|
|
110
|
+
// (版本偏斜/后端降级下静默发生)。坏形页 = 判不出,绝不当「读到了空的」:
|
|
111
|
+
// · `rules` 必须是数组、`rev` 必须是有限数(SDK `RuleListResult` 两键皆必填);
|
|
112
|
+
// · `nextCursor` 只有两种合法形:**缺席**(= 终页)或**非空字符串**(= 还有下一页);
|
|
113
|
+
// `''`/null/数字等坏形不许被折成「到头了」—— 猜终点与猜续点同罪。
|
|
114
|
+
if (typeof res !== 'object' ||
|
|
115
|
+
res === null ||
|
|
116
|
+
!Array.isArray(res.rules) ||
|
|
117
|
+
typeof res.rev !== 'number' ||
|
|
118
|
+
!Number.isFinite(res.rev)) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
failure: {
|
|
122
|
+
kind: 'error',
|
|
123
|
+
message: 'rule list page was malformed (missing/ill-typed rules array or rev on a 2xx) — refusing to certify it as a complete governance list',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// 🔴 跨页 rev 钉(codex F2 轮三 [high]):游标契约上绑 rev —— 清单变了,续页**该** 400
|
|
128
|
+
// cursor-stale。一个 2xx 却换了 rev 的续页 = server 违约或降级形,拼起来是**混合快照**;
|
|
129
|
+
// 认证它为完整清单,比 400 那条腿(丢游标重列)更坏 —— 这里不猜不修补,如实 failure。
|
|
130
|
+
if (rev !== null && res.rev !== rev) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
failure: {
|
|
134
|
+
kind: 'error',
|
|
135
|
+
message: `rule list rev changed mid-drain (page rev ${res.rev} ≠ first page rev ${rev}) on a 2xx — refusing to stitch a mixed snapshot into one governance list`,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// 🔴 行级窄化(同轮):撤销承重的两键(`rule` / `scope`,revoke 的按内容身份对)必须是
|
|
140
|
+
// 非空串 —— `rules:[null]` / 缺 scope 的行被展进 `PersistedRule[]`,下游要么渲空白治理项、
|
|
141
|
+
// 要么按 undefined 撤销(什么都对不上)。坏行 = 坏页(丢行会把活规则藏起来,正是本函数
|
|
142
|
+
// 拒绝的病);其余展示键(tool/match/command/adds)不在此过度收紧 —— server additive 演进
|
|
143
|
+
// 不该把整面打红,消费端对展示键自有坏形容忍。
|
|
144
|
+
for (const r of res.rules) {
|
|
145
|
+
const rr = r;
|
|
146
|
+
if (rr === null ||
|
|
147
|
+
typeof rr !== 'object' ||
|
|
148
|
+
typeof rr.rule !== 'string' ||
|
|
149
|
+
rr.rule === '' ||
|
|
150
|
+
typeof rr.scope !== 'string' ||
|
|
151
|
+
rr.scope === '') {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
failure: {
|
|
155
|
+
kind: 'error',
|
|
156
|
+
message: 'rule list page carried a malformed rule row (missing/ill-typed rule or scope) — refusing to present a list that hides or garbles live rules',
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
rev = res.rev;
|
|
162
|
+
rules.push(...res.rules);
|
|
163
|
+
const next = res.nextCursor;
|
|
164
|
+
if (next === undefined)
|
|
165
|
+
return { ok: true, rules, rev };
|
|
166
|
+
if (typeof next !== 'string' || next === '') {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
failure: {
|
|
170
|
+
kind: 'error',
|
|
171
|
+
message: 'rule list page carried a malformed nextCursor (neither absent nor a non-empty string) — refusing to guess where the list ends',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
cursor = next;
|
|
176
|
+
}
|
|
177
|
+
if (!restart) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
failure: {
|
|
181
|
+
kind: 'error',
|
|
182
|
+
message: `rule list did not terminate within ${MAX_PAGES} pages — refusing to present a partial governance list`,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { ok: false, failure: { kind: 'cursor-stale', message: 'rule list kept changing under the cursor — try again' } };
|
|
188
|
+
}
|
|
189
|
+
// ── skipped.reason 分类 ─────────────────────────────────────────────────────────────────────
|
|
190
|
+
/**
|
|
191
|
+
* `skipped[].reason` 的分类。**只按第一个 `:` 前缀**,且必须容得下**没有前缀**的两种真值形
|
|
192
|
+
* (整层 JSON 解不开 / 条目不是串)——SDK 头注逐字:reason 是给人看的散文,不是机读码,
|
|
193
|
+
* 全串等值匹配与「裸码」两种读法都会漂。
|
|
194
|
+
* 前缀形判据刻意保守(`RuleRejectCode` 形:小写 + 点/下划线),防把散文里的第一个冒号误读成码。
|
|
195
|
+
*/
|
|
196
|
+
export function classifySkippedReason(reason) {
|
|
197
|
+
const text = typeof reason === 'string' ? reason : String(reason);
|
|
198
|
+
const idx = text.indexOf(':');
|
|
199
|
+
if (idx <= 0)
|
|
200
|
+
return { text };
|
|
201
|
+
const head = text.slice(0, idx);
|
|
202
|
+
if (!/^[a-z][a-z0-9_]*(\.[a-z0-9_]+)*$/.test(head))
|
|
203
|
+
return { text };
|
|
204
|
+
return { code: head, text };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* respond 回执上「不再询问到底存上了没」的**结构化读口**。
|
|
208
|
+
*
|
|
209
|
+
* 🔴 **单一台账**(A-028.14 收口本体):`rulePersisted` / `ruleRefusal` 的结构窄化只有
|
|
210
|
+
* {@link readToolApprovalRespondAck} 这一份 —— 本函数不对 wire 原料二次开读,ack 过不了包内
|
|
211
|
+
* 结构门(缺 `delivery:'applied'` / `approvalId` / 三词闭集 `decision`)⇒ **`unknown`**:
|
|
212
|
+
* 一张连回执身份都不成形的 ack,不配驱动一行「已保存」的用户告知(诚实缺席优先)。
|
|
213
|
+
* 收口前 cli 的裸读会把 `{rulePersisted:true}` 这类半形对象读成 `persisted` —— 那正是双份台账
|
|
214
|
+
* 各漂各的形,常驻套对这一格有反向钉。
|
|
215
|
+
*
|
|
216
|
+
* 🔴 `rulePersisted` 缺席 ≠ `false`(未带 persistRule 的回决 / 旧 server ⇒ 字段省略);
|
|
217
|
+
* 🔴 规则没存上**从不翻转裁决** —— 200 + `rulePersisted:false` + `ruleRefusal` 是诚实形,
|
|
218
|
+
* 宿主只说「这次放行了,但『不再询问』没存上」,绝不渲成整次审批失败。
|
|
219
|
+
* 🔴 `rule_lane_unavailable` **四种成因共用一格、两种是本卡局限** ⇒ 禁据一帧判断整台部署的车道
|
|
220
|
+
* 在不在(那要看能力位)。
|
|
221
|
+
*/
|
|
222
|
+
export function readRulePersistOutcome(ack) {
|
|
223
|
+
const parsed = readToolApprovalRespondAck(ack);
|
|
224
|
+
if (parsed === undefined)
|
|
225
|
+
return { state: 'unknown' };
|
|
226
|
+
if (parsed.rulePersisted === true)
|
|
227
|
+
return { state: 'persisted' };
|
|
228
|
+
if (parsed.rulePersisted === false) {
|
|
229
|
+
// 空串/非串已在 readToolApprovalRespondAck 降缺席(单一台账),这里只剩「在场即带」。
|
|
230
|
+
return {
|
|
231
|
+
state: 'refused',
|
|
232
|
+
...(typeof parsed.ruleRefusal === 'string' ? { reason: parsed.ruleRefusal } : {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return { state: 'unknown' };
|
|
236
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -228,6 +228,8 @@ export * from './hitl/parkRowBirthWait.js';
|
|
|
228
228
|
export * from './hitl/approvalDecisionNoteAudit.js';
|
|
229
229
|
export * from './hitl/askParkRowRouting.js';
|
|
230
230
|
export * from './hitl/resumeRunningCard.js';
|
|
231
|
+
export * from './hitl/persistedRulesWire.js';
|
|
232
|
+
export * from './hitl/localAllowRule.js';
|
|
231
233
|
export * from './hitl/approvalsFeed.js';
|
|
232
234
|
export * from './compensations.js';
|
|
233
235
|
export * from './request/printNotification.js';
|
package/dist/index.js
CHANGED
|
@@ -340,6 +340,19 @@ export * from './hitl/parkRowBirthWait.js';
|
|
|
340
340
|
export * from './hitl/approvalDecisionNoteAudit.js';
|
|
341
341
|
export * from './hitl/askParkRowRouting.js';
|
|
342
342
|
export * from './hitl/resumeRunningCard.js';
|
|
343
|
+
// ── A-028.14 / #244 F2(2026-08-14):HITL 规则侧上收 —— 持久规则车道通用判定半场 + durable 腿
|
|
344
|
+
// 本地落规则判定骨架(源形 = cli persistedRulesWire / localAllowRuleWrite 的纯判定面)。
|
|
345
|
+
// · persistedRulesWire:能力位双段 gate(permissionRules / permissionRulesRevoke 合取)、
|
|
346
|
+
// RulesFailure 处置分类(404 两支绝不共用一格)、keyset 翻页收口(游标失效重列 + 页数硬帽)、
|
|
347
|
+
// skipped.reason 前缀分类、persist-ack 三态读口 —— 读口与 `readToolApprovalRespondAck`
|
|
348
|
+
// **合成一处**(census top-08 #1 的双份台账收口本体);CC settings 三层 fs 读、SDK facade
|
|
349
|
+
// 装配、通知呈现留宿主。
|
|
350
|
+
// · localAllowRule:parseLocalAllowRule 窄化五步(①整工具拒/②工具名对齐/③字面锚/⑤裸解释器
|
|
351
|
+
// 前缀/⑤b canonical 危险谓词)骨架;危险谓词与规则语法经 `LocalAllowRuleDeps` 注入
|
|
352
|
+
// (parkOwnership deps 同形)—— 谓词表本体(BARE_SHELL_PREFIXES / dangerousPatterns)留宿主,
|
|
353
|
+
// 包内绝不自建第二份名单。🔴 安全面等值契约:拒绝集文案是三端可观察行为,cli 常驻套逐字锚。
|
|
354
|
+
export * from './hitl/persistedRulesWire.js';
|
|
355
|
+
export * from './hitl/localAllowRule.js';
|
|
343
356
|
// B7 ③(census G20,**行为改动**不是搬迁):pending-approvals 推送 feed(stream 优先 / 断流回落
|
|
344
357
|
// 轮询 / 定期再试)。🔴 它**不替换** D-1 的取件 —— 那三处必须继续走权威 `list()`(见文件头)。
|
|
345
358
|
export * from './hitl/approvalsFeed.js';
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
| peer:wire 契约 | `@sema-agent/sdk` **>=6.17.2**(value-level,非 type-only) | `package.json` `peerDependencies` |
|
|
24
24
|
| peer:会话词汇表 | `@sema-agent/agent-types` **>=0.2.0**(type-only,零运行时) | 同上 |
|
|
25
25
|
| runtime dep | `diff` ^9.0.0(**唯一**一条;portability 门按**等值**钉死) | `package.json` `dependencies` |
|
|
26
|
-
| 公开导出面 | **
|
|
26
|
+
| 公开导出面 | **692** 个运行期符号(+ 33 个测试钩) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
|
|
27
27
|
| 常驻门 | 以 `scripts/gates-manifest.json` 的 `suites` 长度为准(**本档不抄这个数**) | `scripts/gates-manifest.json`;`npm test` 的名单等值门与它逐名对账 |
|
|
28
28
|
| 沿革档 | 0.29.0 起建 `CHANGELOG.md`;更早批次记账在 `src/index.ts` 文件头 + `docs/REFACTOR-LEDGER.md` | — |
|
|
29
29
|
|
|
@@ -99,7 +99,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
99
99
|
|
|
100
100
|
## §2 公共导出面地图(按域)
|
|
101
101
|
|
|
102
|
-
> 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**
|
|
102
|
+
> 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**692** 项)。
|
|
103
103
|
> 本节**不逐名抄**,只给「域 → 承重导出 → 用途 → 实现锚」。承重导出 = 一个端为了让这个域干活
|
|
104
104
|
> **必须**直接调到的那几个符号;其余是它们的类型、变体与辅助位。
|
|
105
105
|
> 单一入口:`import { … } from '@sema-agent/client-core'`(`exports` 只有 `.` 一个;
|
|
@@ -109,27 +109,27 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
109
109
|
|
|
110
110
|
`public-export-baseline.json` 由 **`dist/index.js` 的运行期导出**生成(生成口径自述见
|
|
111
111
|
`scripts/run-client-core-typeshape-test.mjs`,双向精确集合门在 `scripts/run-public-surface-test.mjs`)。
|
|
112
|
-
实测:
|
|
112
|
+
实测:692 项 **100% 是运行期导出,零 type-only**。
|
|
113
113
|
|
|
114
114
|
**推论(端必须知道)**:
|
|
115
|
-
- barrel 导出的**类型**面比
|
|
115
|
+
- barrel 导出的**类型**面比 692 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
|
|
116
116
|
`ChromeEvent` / `HostPorts` / `ApprovalCardPort` / `HitlHostSurface` / `ClientSliceLike` /
|
|
117
117
|
`LocalSessionEvent` / `SeatMethodName` / `ModelCatalog` 全在公面上、全**不在**基线里。
|
|
118
118
|
端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
|
|
119
119
|
- `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
692 项的内部构成(帮助端估读表大小):**206** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
|
|
122
122
|
(矩阵、键集、env 名、锚串)而非可调用物;**4** 项是 PascalCase 运行期值
|
|
123
123
|
(`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError`);
|
|
124
124
|
**38** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6)。
|
|
125
125
|
|
|
126
|
-
### 2b. 域图(16 域,逐域计数之和 =
|
|
126
|
+
### 2b. 域图(16 域,逐域计数之和 = 692)
|
|
127
127
|
|
|
128
128
|
| # | 域 | 名数 | 承重导出 | 用途 | 实现锚 |
|
|
129
129
|
|---|---|---|---|---|---|
|
|
130
130
|
| 1 | **适配内核(下行主链)** | 29 | `adapt` · `createWireToCcAdapter` · `runStream` · `eventToSdkMessage` · `terminalToSdkResult` · `turnUsageToModelUsage` · `isRunStreamActive` · `ADAPTER_DIVERGENCES` | 引擎 SSE `AgentEvent` → 端要渲的**双面输出**:transcript(`SDKMessage`)+ chrome(瞬态 `ChromeEvent`)。**本包存在的理由** | `src/adapt.ts`、`src/adapt/{arms,wireShapes,panelTasks}.ts`(经 `adapt.ts` 再导出)、`src/adapter/runStream.ts`、`src/adapter/downstream/*`、`src/adapter/types.ts` |
|
|
131
131
|
| 2 | **seam 公共契约** | 2(其余为 type-only) | `CHROME_ARMS` · `deriveTranscriptId` | 公共词汇 + **id 确定性不变量**(同一条流重放 ⇒ 同一串 id)。`CHROME_ARMS` = 端「我要消费哪些 chrome 臂」的对照清单 | `src/seam.ts` |
|
|
132
|
-
| 3 | **HITL 决断卡链**(§4/§5 主战场) |
|
|
132
|
+
| 3 | **HITL 决断卡链**(§4/§5 主战场) | 119 | `makeHitlCanUseTool` · `HitlBridge` · `findPendingForTask` · `HitlSafetyError` · `bridgeAskUserQuestionGates` · `surfaceToolApprovalFrameAndRespond` / `surfaceFsApprovalAndDecide` · `readToolApprovalRespondAck` · `installApprovalCardPort(For)` · `installHitlHostSurface(For)` · `armPlanReviewApproval` · `reopenPlanReviewCard` · `decidePlanReview` · `startApprovalsFeed` · `pendingRowIsOwnedByThisSession` · `approvalCallKey`/`liveFrameCallKey`/`planReviewQuestionId` · `registerArmedGateFor`/`wasGateArmedFor`/`clearArmedGateFor` · `waitForGateArmed(For)`/`onGateArmed(For)`/`gateArmedWaitMs`(#244 F1 呈现回执事件源) · `planReviewArmedKey(For)`/`notePlanReviewAnswered(For)`/`notePlanReviewAnsweredIfDecisive(For)`(A-024.4 plan 呈现分代) · `toolEndOutputText` · `isAskTool` · `waitForParkRowBirth` · `classifyAskParkRows` / `askParkRowArm` / `classifyAskParkChainFailure` · `readDecisionNoteAudit` / `decisionNoteAuditLine` · `resumeRunningOptions` / `resumeChoiceFromLabels` · `persistedRulesLaneAvailable`/`persistedRulesGovernanceAvailable` · `classifyRulesFailure` · `listAllPersistedRules` · `classifySkippedReason` · `readRulePersistOutcome`(#244 F2:persist-ack 读口与 `readToolApprovalRespondAck` 合成一处) · `parseLocalAllowRule`(durable 腿本地落规则窄化骨架,谓词经 `LocalAllowRuleDeps` 注入) | suspended→decide→resume 环。🔴 **D-1 两元组 verbatim 回显**是字节级断言的安全不变量,端**不许重实现它的任何一段**。🔴 键空间边界(web [C1] d3 拦截):`gateIdentity` 四常量两函数只覆盖 HITL questionId/callKey 空间;seat 的 `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`plan:` 等)是另一键空间,**两者绝不合并**(合并=座位校验器静默拒全部 plan-review 卡) | `src/hitl/hitlBridge.ts`、`toolApprovalWire.ts`、`askGateWire.ts`、`planReviewWire.ts`、`hitlHostSurface.ts`、`gateIdentity.ts`、`armedGateRegistry.ts`、`parkOwnership.ts`、`parkResolver.ts`、`approvalsFeed.ts`、`frameRouter.ts`(**只挑名导出** `toolEndOutputText`/`ENGINE_ABORT_TOOL_RESULT`/`isAskTool`/`HITL_REJECT_MESSAGE`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧) |
|
|
133
133
|
| 4 | **子代 wire + 面板侧信道台账** | 68 | `tailEngineSubagent` · `installSubagentActivitySink` · `stopEngineTask` + `classifyTaskStopConflict` · `fetchEngineSubagentReport` · `steerEngineSubagent` · `subscribeSubagentContent` · `subscribeEngineAgentPanel` · `publishQuestionFrame` / `respondToQuestion` | 驱动与观测委派子代;经 module 级台账喂活体 agent/task 面板。全部**能力位 gate**(§5b) | `src/subagent/*.ts`、`src/subagentContentStore.ts`、`src/engineAgentPanelStore.ts`、`src/engineInlineTaskStats.ts`、`src/engineToolLabelStore.ts`、`src/liveQuestionStore.ts` |
|
|
134
134
|
| 5 | **fleet 投影** | 43 | `createFleetLedger` · `projectTasks` · `projectWorkflows` · `projectFleetAgentRows` · `readEngineActiveBgTasks` · `FLEET_TASK_VIEW_KEYS` | 老 `fleetClient` 那一刀的成品:**帧体归库、连接归端** —— 端持 SSE 连接,库做行投影 + 保留台账 | `src/fleet/fleetProjection.ts`、`src/fleet/fleetLedger.ts`、`src/fleetAgentPanelProjection.ts`、`src/fleetTaskDesc.ts` |
|
|
135
135
|
| 6 | **请求装配(上行唯一构造口)** | 8 | `buildTaskRequest` · `REQUEST_FIELD_MATRIX` · `unregisteredRequestKeys` · `applyLiveRequestDefaults` · `taskNotificationToPrintFrame` | 两条车道(`interactive`/`print`)出站请求的**唯一**构造器;`unregisteredRequestKeys` 是可执行门 —— 端偷带一个未登记键上 wire 就红 | `src/request/taskRequest.ts`、`src/request/printNotification.ts` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.2",
|
|
4
4
|
"description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|