@sema-agent/core 7.4.0 → 7.5.1
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 +69 -0
- package/dist/agents/cascade.d.ts +2 -1
- package/dist/agents/peer-notices.d.ts +11 -1
- package/dist/agents/peer-session-drain.js +2 -0
- package/dist/agents/verify.d.ts +2 -1
- package/dist/core/ask-origin.d.ts +130 -0
- package/dist/core/ask-origin.js +35 -0
- package/dist/core/auto-mode-arming.d.ts +40 -1
- package/dist/core/auto-mode-arming.js +51 -3
- package/dist/core/auto-mode.d.ts +87 -10
- package/dist/core/auto-mode.js +34 -6
- package/dist/core/checkpoint-store.d.ts +41 -6
- package/dist/core/checkpoint-store.js +8 -0
- package/dist/core/hooks.d.ts +63 -19
- package/dist/core/hooks.js +37 -16
- package/dist/core/mcp.d.ts +47 -55
- package/dist/core/mcp.js +89 -31
- package/dist/core/park-selfcheck.js +3 -0
- package/dist/core/permission-rule-consent.d.ts +2 -11
- package/dist/core/permission-rule-consent.js +13 -62
- package/dist/core/permission-rule-org.d.ts +70 -54
- package/dist/core/permission-rule-org.js +47 -61
- package/dist/core/permission-rule-provider.d.ts +162 -0
- package/dist/core/permission-rule-provider.js +148 -0
- package/dist/core/permission-rule-session.d.ts +23 -19
- package/dist/core/permission-rule-session.js +5 -5
- package/dist/core/permission-rule-store.d.ts +46 -21
- package/dist/core/permission-rule-store.js +13 -6
- package/dist/core/permission-rule-sync.d.ts +2 -1
- package/dist/core/permission-rule-sync.js +11 -0
- package/dist/core/runner/assemble-result.d.ts +3 -2
- package/dist/core/runner/checkpoint-scope.d.ts +32 -0
- package/dist/core/runner/checkpoint-scope.js +4 -0
- package/dist/core/runner/contracts.d.ts +1878 -0
- package/dist/core/runner/contracts.js +1 -0
- package/dist/core/runner/denial-limit-arms.d.ts +57 -31
- package/dist/core/runner/denial-limit-arms.js +42 -17
- package/dist/core/runner/derived-route-fallback.d.ts +34 -0
- package/dist/core/runner/derived-route-fallback.js +16 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
- package/dist/core/runner/prepare-announce-once.d.ts +83 -0
- package/dist/core/runner/prepare-announce-once.js +105 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
- package/dist/core/runner/prepare-config-doors.d.ts +2 -10
- package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
- package/dist/core/runner/prepare-defer-classify.js +107 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
- package/dist/core/runner/prepare-delegation-surface.js +144 -0
- package/dist/core/runner/prepare-execution-env.d.ts +54 -0
- package/dist/core/runner/prepare-execution-env.js +86 -0
- package/dist/core/runner/prepare-file-history.d.ts +95 -0
- package/dist/core/runner/prepare-file-history.js +383 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
- package/dist/core/runner/prepare-hands-readface.js +3 -3
- package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
- package/dist/core/runner/prepare-inherited-gate.js +266 -0
- package/dist/core/runner/prepare-listings.d.ts +77 -0
- package/dist/core/runner/prepare-listings.js +76 -0
- package/dist/core/runner/prepare-lsp.d.ts +55 -0
- package/dist/core/runner/prepare-lsp.js +27 -0
- package/dist/core/runner/prepare-memory.d.ts +1 -1
- package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
- package/dist/core/runner/prepare-offload-wrappers.js +45 -0
- package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
- package/dist/core/runner/prepare-permission-rules.js +140 -0
- package/dist/core/runner/prepare-project-context.d.ts +131 -0
- package/dist/core/runner/prepare-project-context.js +150 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
- package/dist/core/runner/prepare-prompt-inputs.js +141 -0
- package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
- package/dist/core/runner/prepare-protocol-tools.js +182 -0
- package/dist/core/runner/prepare-question-face.d.ts +119 -0
- package/dist/core/runner/prepare-question-face.js +83 -0
- package/dist/core/runner/prepare-run-refs.d.ts +89 -0
- package/dist/core/runner/prepare-run-refs.js +39 -0
- package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
- package/dist/core/runner/prepare-task.d.ts +11 -1815
- package/dist/core/runner/prepare-task.js +138 -2542
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
- package/dist/core/runner/prepare-wiring-manifest.js +240 -0
- package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
- package/dist/core/runner/prepare-workspace-restore.js +1 -22
- package/dist/core/runner/rollback-stack.d.ts +32 -0
- package/dist/core/runner/rollback-stack.js +30 -0
- package/dist/core/runner/runtask.d.ts +11 -2
- package/dist/core/runner/runtask.js +27 -9
- package/dist/core/runner/workspace-path.d.ts +33 -0
- package/dist/core/runner/workspace-path.js +22 -0
- package/dist/core/sensitive-path-policy.d.ts +16 -0
- package/dist/core/sensitive-path-policy.js +1 -1
- package/dist/core/tool-policy.d.ts +57 -9
- package/dist/core/tool-policy.js +11 -0
- package/dist/core/types.d.ts +63 -51
- package/dist/core/wiring-manifest.d.ts +40 -3
- package/dist/core/wiring-manifest.js +4 -3
- package/dist/core/write-protect.d.ts +13 -2
- package/dist/core/write-protect.js +58 -29
- package/dist/engine/harness/types.d.ts +38 -16
- package/dist/engine/harness/types.js +25 -1
- package/dist/engine/session/session.d.ts +3 -11
- package/dist/index.d.ts +10 -6
- package/dist/index.js +9 -5
- package/dist/internal/harness.d.ts +1 -0
- package/dist/stores/file/adoption/adopt.d.ts +1 -1
- package/dist/stores/file/adoption/marker.d.ts +1 -1
- package/dist/stores/file/permission-rule-adopt.js +4 -3
- package/dist/stores/file/permission-rule-store.d.ts +65 -25
- package/dist/stores/file/permission-rule-store.js +215 -37
- package/dist/stores/file/task-list-store.d.ts +1 -1
- package/dist/tools/fs/read-face.d.ts +1 -1
- package/package.json +8 -2
- package/test/export-surface.snapshot.json +76 -28
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.5.1 — 2026-09-06
|
|
4
|
+
|
|
5
|
+
Maintenance release: the design/390 runner decomposition (slices S0–S2), the post-7.5.0 gate repairs, and one summary hardening (#566). **No BREAKING change**: the export surface is byte-identical to 7.5.0 (export snapshot unchanged); the only observable delta is the label text of a wedged dispose on the prepare-throw path (below).
|
|
6
|
+
|
|
7
|
+
### Changed — design/390 S0–S2: `prepareTask` decomposed into phase modules (14 229 → 7 723 lines), one contract each
|
|
8
|
+
- 24 phase modules under `src/core/runner/prepare-*.ts`, each `prepareX(input: Input): Result` with every Input seat classified (owned / borrowed-readonly / borrowed-mutable + writer table / ownership-ref) and the driver reduced to one destructuring call per phase. Extracted verbatim: offload wrappers, execution-env (`OwnedEnvSeat`), file-history, run-refs, protocol-tools (MCP/A2A), question-face, LSP, project-context, prompt-inputs, defer-classify, listings, inherited-gate, caps-and-workflow, delegation-surface, tool-disclosure-mount, wiring-manifest; helpers `contracts.ts`, `workspace-path.ts`, `derived-route-fallback.ts`, `checkpoint-scope.ts`, `denial-limit-arms.ts`.
|
|
9
|
+
- **RollbackStack** (`rollback-stack.ts`): every resource acquired during prepare registers its disposer the moment it is acquired (env, MCP clients, A2A peers); a prepare throw unwinds them in reverse order, each bounded and reported, replacing the driver's explicit `if (handle) dispose` catch legs. The A2A registration keys on the acquisition decision read ONCE (a caller replacing `spec.a2a` during materialization can no longer unregister an acquired handle).
|
|
10
|
+
- **Disclosed delta**: a dispose that hangs past its budget on the prepare-throw path now reports leg `"<disposer> (rollback leg N of M, prepare-throw)"` (e.g. `mcpDispose (rollback leg 2 of 3, prepare-throw)`) instead of `"mcp.dispose (prepare-throw leg)"` / `"a2a.dispose (prepare-throw leg)"` / `"ownedEnv.destroy (prepare-throw leg)"`. Error class and budget unchanged; no test or consumer referenced the old text.
|
|
11
|
+
- Contract sentence `@contract prepare.deps-read-stable`: the host keeps `deps` / `internals` / `executionEnv` read-stable for the duration of `prepareTask`; an async phase returning a microtask later than the inlined code is within contract.
|
|
12
|
+
- Roster mutation owner table: which phase pushes/splices which tool under which condition is written once on `Prepared.tools` (contracts.ts), not scattered in prose.
|
|
13
|
+
- `isLiveApproverSeat` consumers (3) now read the one predicate in `tool-policy.ts`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- #566 `CheckpointSummary.denialLimitFallback` is produced by `readDenialLimitFallback` (the same reader the park row and `AskRequest` use): exactly the four typed members when the row's value is well-formed, no key otherwise — a hand-edited or partial value is dropped, never echoed.
|
|
17
|
+
- Wire-change generator: inherited keys are keys the type HAS — `readTreeModel` folds an interface's `extends` / intersection bases, so moving members onto a base interface no longer reports a false "lost keys" BREAKING item (the two false items in `wire-changes/core@7.5.0.json` are left as published and noted there).
|
|
18
|
+
- `gate:message-branching` (the mcp.ts `transport.kind` fold's five sites are the ONE reader, exemption count corrected) and `gate:domain-lexicon` (UNC anchor test written with `isUncPath`) — two CI-only gates that had been red since the A-089 merge without the local chain running them.
|
|
19
|
+
- Two load-sensitive pins (#583) measure by best-of-three / a generous spawn timeout instead of a single wall-clock reading.
|
|
20
|
+
|
|
21
|
+
### Tooling / gates
|
|
22
|
+
- `node scripts/gate-all.mjs [--with-dist] [--rc]` runs the whole `gate:*` roster in one command; a `gate:*` script without a roster seat fails `test/gate-all-roster.test.ts` (the release chain's L3 line now points here).
|
|
23
|
+
- New gates: **G1** module line-count ratchet (`test/module-size-ratchet.test.ts`, banked per extraction), **G2** dependency-direction gate (`scripts/verify-layering.mjs` + `docs/LAYERING.json`, four layers: contracts ← prepare-path machinery ← phases ← orchestrator ← runtask; exemptions 13 → 3), **G5** phase-contract gate (`scripts/verify-phase-api.mjs`: every `prepare-*.ts` exports one Input/Result pair, every Input field classified; `KNOWN_INPUT_FIELDS` anti-blindness floor 371), `gate:agent-returns` (every archived car receipt's unresolved line carries a ticket / DEBTS id / DV / slice home).
|
|
24
|
+
- Release criteria: `retiredIn` annotation lets a historic criteria file name a face that a later version removed without rewriting history (`criteria-lint` accepts an absent face carrying the annotation).
|
|
25
|
+
- 16 runtime exports added in 7.5.0 registered in the consumer smoke probe; `test/prepare-lsp-ignore-filter.test.ts` pins the LSP tool's git-ignore wiring (a receiving-side mutation had left every LSP test green).
|
|
26
|
+
|
|
27
|
+
## 7.5.0 — 2026-09-05
|
|
28
|
+
|
|
29
|
+
**BREAKING release** (the 7.4.1 bug batch merged into it — one publish, one pickup). 13 exports removed, 5 public types lose keys, one store contract replaced whole; every item is named below with its replacement. Nothing here is a wire FRAME change: `TaskEvent`, `PendingAction`, `CheckpointSummary`, `WiringManifest` only GAIN keys.
|
|
30
|
+
|
|
31
|
+
### BREAKING — design/389 one permission-rule store, partitioned by source (the three stores become one)
|
|
32
|
+
- `RunnerDeps.permissionRuleStore` is now the ONE seam: a `PermissionRuleStoreProvider` built by `createPermissionRuleStoreProvider({ durable, org?, session? })`. Its `partitions { durable, org, session }` is a mandatory DECLARATION (a manifest reads it, nothing probes for it); `forPrincipal(p).effective({ sessionId? })` is the single read, answering every partition with each row's `source ∈ org|user|project|session` and `status ∈ live|shadowed-by-org` plus the org resolution beside it.
|
|
33
|
+
- **Removed** (13): `RunnerDeps.permissionRuleOrg` and `RunnerDeps.sessionPermissionRules` (both folded into the provider's `org` / `session` partitions); `RuleConsentDeps.sessionRules`; `PermissionRuleStore.list()` (→ `effective()`); `OrgRuleOverlay` / `createOrgRuleOverlay` / `OrgOverlayResolution` / `OrgOverlayStatus` / `orgRuleStatePersistenceOf` (→ `OrgRulePartitionConfig` / `OrgRuleResolution` / `OrgRuleStatus`, `createPermissionRuleStoreProvider({ org })`); `SessionRuleOverlay` / `SessionRuleOverlayAdd` / `SessionRuleOverlayApplyResult` / `InMemorySessionRuleOverlay` (→ `SessionRulePartition` / `SessionRuleAdd` / `SessionRuleApplyResult` / `InMemorySessionRulePartition`); `FilePermissionRuleStoreProvider` / `InMemoryPermissionRuleStore` / `WritablePermissionRuleStore` / `EMPTY_RULE_STORE` (→ `FileDurableRulePartitionProvider` / `InMemoryDurableRulePartition` / `WritableDurableRulePartition` / `EMPTY_DURABLE_RULE_PARTITION`; the durable backend a provider is built over is a `DurableRulePartitionProvider`).
|
|
34
|
+
- **Reshaped, same name**: `PermissionRuleStoreProvider` (tier advanced → stable; `+partitions`); `PermissionRuleStore` (`-list`, `+effective`); `EffectivePermissionRule` now `extends PersistedAllowRule` and adds `source`/`status` (its `rule`/`scope` keys are INHERITED, not lost — a syntactic key diff reports them removed).
|
|
35
|
+
- **File backend schema 3**: bucket files drop the `org` block (→ `org.json`, `ORG_STATE_FILE`); a one-time migration runs at construction under the writer lock and publishes `.rule-schema-3` when complete. A legacy (schema 1/2) bucket after the cutover is REFUSED WHOLE by the reader (`list()` rejects; `effective()` reports the durable partition `unreadable`) — previously a bad bucket read as empty. A live writer (an older engine) or an unwritable directory DEFERS the migration (disclosed via `onError`, no marker, construction succeeds as a read face); the next construction that can take the lock performs it.
|
|
36
|
+
- Org: `resolve()` is bounded whole by `ORG_ADJUDICATION_TIMEOUT_MS` (15 s) — a persistence that never answers reports `unavailable` instead of hanging the gate; an equal-revision snapshot with the same content keeps the fresher `fetchedAtMs`; a lower/conflicting revision among several legacy buckets is a typed `OrgRevisionRefusal` (disclosed, the highest wins).
|
|
37
|
+
- Session-scope grants route through the same writer (`redemption-add` with a `session` scope lands in the session partition; the durable `expectedRev` is not compared there). Known coupling (#579, design candidate): a session grant is refused while the durable partition is unreadable, because the dot is minted by the durable replica.
|
|
38
|
+
|
|
39
|
+
### BREAKING — #559 ② the auto-deny window is minted at the ROUTE, by one writer (car D + car H)
|
|
40
|
+
- `PermissionResult`'s ask arm `denialLimitFallback` is now an `UnarmedDenialLimitFallback` (counts only: `consecutive`, `total`, `limit`; `autoDenyAfterMs?: never`). A policy can no longer declare a window — `{ …, autoDenyAfterMs: 120000 }` on a decision is a compile error in every shape (fresh literal, predeclared const, `as const`, async `check`). The window is written by exactly two route stations: `createAutoModeDenialTracker().armTimedWindow(fallback)` (the hand-out to a LIVE function approver, the only writer of a non-zero window) and `unarmedWindow(fallback)` (every route that cannot count down: durable park, headless, blanket `onAsk` — the one place a `0` is spelled).
|
|
41
|
+
- `DenialLimitFallback` (what `AskRequest`, the durable park row, `CheckpointSummary` and `tool_end` carry) keeps its FOUR members; it now `extends DenialLimitCounts` (a syntactic key diff reports `consecutive`/`limit`/`total` removed — they are inherited; server's structural conformance door still holds both ways). Readers that only render take `DenialLimitFallbackFace = Unarmed | Armed` (`denialLimitSentence`, `denialLimitFallbackMessage`).
|
|
42
|
+
- **Tripwire (loud, not silent)**: a DECISION whose `denialLimitFallback` carries `autoDenyAfterMs` (a structural widening through `DenialLimitCounts`, a cast, a JS host) is refused at the policy fold — deny with the retired member named — instead of having its number silently replaced by the route station. The route stations construct the armed member from the three counts by name (a prototype-backed member keeps its counts; a foreign key does not ride onto the request).
|
|
43
|
+
- **Behaviour**: the durable park row's `autoDenyAfterMs` is 0 (a park is never timed — the timed card is not burned; the streak's next live hand-out arms it); the TOTAL-bound card is never timed; headless arms nothing (the fail-closed deny stands); a tracker `reset` starts a new minted generation (an old member cannot burn the new streak's card). `AskRequest.origin` (see #564) rides the wire; `suspendAsk` takes one structural seat `AskCarry { signal?, denialLimitFallback?, origin? }` instead of growing positional seats.
|
|
44
|
+
- `AutoModeArmingRecipe` / `AutoModeArmingFace` gain `denialLimit` (#556: the persisted arming recipe carries the bounds; a rebuilt tracker runs the configured limit, not the default).
|
|
45
|
+
|
|
46
|
+
### Changed (narrowing, named) — A-089 / #562 remote MCP error text is redacted where it is minted
|
|
47
|
+
- Every remote-authored MCP error text that becomes an engine-thrown `Error` message, an operator warning `cause`, a first-turn attachment, a continuation-page record, a tool-result fence or a manifest `error` is passed through `redactSecrets` at ONE mint (`remoteErrorText`); a server-signalled `isError` body joins the same rule. Successful tool CONTENT is not touched (tool-output egress is a separate matter). Consumers that pinned raw upstream error strings will see redacted markers.
|
|
48
|
+
- `WiringManifest.mcp[]` / `WiringFacts.mcp` (#562): per-server connection-state read face `{ name, status, errorCode?, … }` with the closed set `MCP_FAILURE_CODES` / `mcpFailureCodeOf` (`McpServerStatus.errorCode`, `McpServerSpec.source`). Not part of the fingerprint.
|
|
49
|
+
|
|
50
|
+
### Changed (narrowing, named) — #555 / #549 the `.sema` rows and ONE data root
|
|
51
|
+
- `WRITE_PROTECTED_DEFAULT_TABLE` gains `.sema.json` (basename) and `.sema` (segment) — the brand twins of `.claude.json` / `.claude`; a Write/Edit onto `~/.sema/settings.json` is demoted to `ask` (headless: deny). The data-root passthrough (`dataRootSpellings`, one resolver for both judges) keeps the engine's own library writable, and the engine's mount now hands the judge `RunnerDeps.memoryEngineDir` — the same seat the memory library is materialized under — so a deployment that sites its data root under its config root by that seat alone (env unset) is not demoted (A-090.1). Bare deployments (no `memoryEngineDir`, no `$AGENT_DATA_DIR`) still resolve `~/.ai-agent`. UNC `..` cannot hop shares (#555 r2).
|
|
52
|
+
|
|
53
|
+
### Changed — #564 / #570 the ask's ORIGIN is a closed set and one predicate
|
|
54
|
+
- `ASK_ORIGINS` (8 words: `policy | hook | ask_rule | org_rule | org_unavailable | content_question | denial_limit_fallback | unresolvable`), `askOriginOf`, `classifierMayAnswer` (the table: only `policy` is classifier-answerable); the auto-mode classifier's eligibility gate is this one predicate instead of an eight-conjunct exclusion chain (the inherited lane reads the same table). `PermissionResult.origin` / `AskRequest.origin` / `PendingAction.tool_approval.origin` / `CheckpointSummary.origin` (all additive).
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
- MCP dial reads `transport.kind` ONCE per server and threads it to the connect, the idle-timeout bucket and the failure classifier (#562 had added a second reader before the dial; a mutating `kind` getter could connect over one transport and be judged under another).
|
|
58
|
+
- #569 a mailbox `ack` that faults keeps its watermark, so the next boundary retries the SAME prefix; a dropped watermark re-judged the whole leased batch and fired every per-record side effect twice inside one run (the admission gate's seq+body replay arm ADMITS a re-presentation; only the model injection was masked by the injection queue's dedup key). Pin D4c watches a side effect with no downstream dedup.
|
|
59
|
+
- #560 MCP ghost replies: prepare-time one-shot announcements (failed MCP servers, memory-assembly advisories, agent/skills listings) are per-SESSION facts on the `announcedListings` ledger, re-issued only on delta (CC 2.1.250 form); a halt/stop-after-turn boundary does not collect the attachment bundle.
|
|
60
|
+
- #557 `tool_end.autoDenied` + `settledBy:"timeout"` + `resolution:"window_expired"` on the frame; the durable park row and `CheckpointSummary` carry `denialLimitFallback` and `requiresRealApproval` (`PendingAction.tool_approval.*`, additive).
|
|
61
|
+
- #536 `PEER_DROP_REASONS.queue-full` STAYS (CC 2.1.250 mints it on the recipient's ingress; this engine judges the same fact at the send station with tool error `queue_full` — divergence DV-536-1 registered; the receipt word is kept for renderer alignment).
|
|
62
|
+
- A-089/A-090 JSDoc and INTEGRATION-CORE coordinates brought to the verified truth (the store-contract table, `tool_end.autoDenied`'s counterpart, `mcpFailureCodeOf`'s two message-derived words, read-face's org-governance seat).
|
|
63
|
+
|
|
64
|
+
### Tooling
|
|
65
|
+
- `docs/release-criteria` observations accept `retiredIn: "<version>"`: a historic document keeps citing the face its release shipped, and the linter requires that face to be ABSENT (an annotation on a face that still exists reds). 7.0.0.json carries seven (the design/389 removals).
|
|
66
|
+
- File rule store: the three on-disk shapes are read as `unknown` and admitted by type guards (no boundary cast); one `jsonForm` round-trip helper.
|
|
67
|
+
- `scripts/wire-changes.mjs` (`npm run gate:wire`): the machine-readable wire delta of a release (exports / closed sets / type keys / narrowing notes / criteria coverage / consumer hints) → `wire-changes/core@<ver>.json`; `ship-post-lint` grows a WIRE leg; `scripts/capability-rows.mjs` prints CAPABILITY-CONSUMPTION rows. `scripts/repro/B-002.mjs`, `B-014..B-017` dist-level repros + `gate:repro`. `prepareTask`'s #548 inherited arms live in `runner/denial-limit-arms.ts`.
|
|
68
|
+
|
|
69
|
+
### Export surface
|
|
70
|
+
- +37 / −13 (named above), 1 tier change; `test/export-surface.snapshot.json` 2048 → 2072.
|
|
71
|
+
|
|
3
72
|
## 7.4.0 — 2026-09-04
|
|
4
73
|
|
|
5
74
|
Bug-fix release. No exports removed, no wire key removed; every narrowing is named below.
|
package/dist/agents/cascade.d.ts
CHANGED
|
@@ -157,7 +157,8 @@ export interface CascadeRunResult extends TaskResult {
|
|
|
157
157
|
export declare function runCascade(runner: Runner, spec: TaskSpec, config: CascadeConfig,
|
|
158
158
|
/**
|
|
159
159
|
* The TRUSTED, run-scoped internals channel — the same object `Runner.runTask` takes as its second
|
|
160
|
-
* argument — forwarded
|
|
160
|
+
* argument — forwarded to EVERY rung under the composition forward contract (identity, with the one
|
|
161
|
+
* placement-axis exception stated where the shape is named in `src/index.ts`).
|
|
161
162
|
*
|
|
162
163
|
* WHY THE SEAT EXISTS: `runCascade` returns a `TaskResult` and DRAINS each rung's event stream
|
|
163
164
|
* internally, so the host never holds a `TaskStream` for any rung. Without this parameter a
|
|
@@ -46,7 +46,17 @@ export declare const PEER_HELD_REVIEW_CAUSES: readonly CrossSessionHoldCause[];
|
|
|
46
46
|
export declare const PEER_DELIVERY_RECEIPT_STATES: readonly ["held", "denied", "expired", "delivered", "refused", "dropped"];
|
|
47
47
|
export type PeerDeliveryReceiptState = (typeof PEER_DELIVERY_RECEIPT_STATES)[number];
|
|
48
48
|
export declare function isPeerDeliveryReceiptState(v: unknown): v is PeerDeliveryReceiptState;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The drop reasons a `dropped` receipt may carry (CC vocabulary, kept verbatim — the recipient's ingress guard).
|
|
51
|
+
*
|
|
52
|
+
* `queue-full` is a member of the set but is NOT minted by this engine today: CC judges "recipient's queue
|
|
53
|
+
* is at its limit" on the recipient's ingress and reports it back as a dropped receipt, whereas here the
|
|
54
|
+
* SendMessage tool reads the recipient box's observable count at the send station and refuses synchronously
|
|
55
|
+
* (tool error code `queue_full`, nothing queued). The word stays so that renderers and ledgers keyed on this
|
|
56
|
+
* set remain aligned with CC and an ingress-side mint (two senders racing past the count) would need no
|
|
57
|
+
* vocabulary change. The approval-hold buffer overflowing is a different arm: the oldest held message is
|
|
58
|
+
* evicted with an `expired` receipt (see peer-held-queue).
|
|
59
|
+
*/
|
|
50
60
|
export declare const PEER_DROP_REASONS: readonly ["rate-limited", "duplicate", "hop-loop", "hop-runaway", "queue-full"];
|
|
51
61
|
export type PeerDropReason = (typeof PEER_DROP_REASONS)[number];
|
|
52
62
|
/** CC `g$e` — the short label of a receipt state (UI face). */
|
|
@@ -159,6 +159,8 @@ export function createPeerSessionDrain(opts) {
|
|
|
159
159
|
await mailbox.ack(scope, boxHandle, drainOwner, upTo);
|
|
160
160
|
}
|
|
161
161
|
catch (e) {
|
|
162
|
+
if (settledUpTo === undefined || settledUpTo < upTo)
|
|
163
|
+
settledUpTo = upTo;
|
|
162
164
|
opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-ack" });
|
|
163
165
|
}
|
|
164
166
|
};
|
package/dist/agents/verify.d.ts
CHANGED
|
@@ -245,7 +245,8 @@ export interface VerificationResult extends TaskResult {
|
|
|
245
245
|
export declare function verifyCompleted(runner: Runner, result: TaskResult, specBase: ResumeTaskConfig, objective: string, config: VerifyConfig,
|
|
246
246
|
/**
|
|
247
247
|
* The TRUSTED, run-scoped internals channel — the same object `Runner.runTask` takes as its second
|
|
248
|
-
* argument — forwarded
|
|
248
|
+
* argument — forwarded to EVERY leg this gate drives under the composition forward contract (identity,
|
|
249
|
+
* with the one placement-axis exception stated where the shape is named in `src/index.ts`): the verifier run of each round and
|
|
249
250
|
* each fix continuation (and, from {@link runWithVerification} / {@link resumeWithVerification} /
|
|
250
251
|
* {@link runDeveloperTask}, the implementation leg as well). This doc is the contract for the whole
|
|
251
252
|
* family; the sibling entries carry the same parameter and point here.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ORIGIN of a surviving `ask` — WHICH AUTHORITY raised it, or took it over — as one word from a
|
|
3
|
+
* closed set, and the per-lane tables that say what an automatic decider may do with an ask of that
|
|
4
|
+
* origin.
|
|
5
|
+
*
|
|
6
|
+
* Why an axis of its own. The tool gate has several automatic ask-resolution lanes (the auto-mode
|
|
7
|
+
* classifier today; the persisted-rule lane and the sandbox-admission leg are the same shape), and each
|
|
8
|
+
* one's eligibility used to be written as a growing conjunction of NEGATIVES — "not an org ask, not a
|
|
9
|
+
* hook ask, not a matched ask rule, not the classifier's own fallback, not the question tool, not a
|
|
10
|
+
* marked call…" — one more conjunct per kind of ask the engine learned to mint. Every conjunct was
|
|
11
|
+
* right; the FORM was the debt: a new kind of ask had to be remembered at every lane, and forgetting
|
|
12
|
+
* one was a silent loosening (the new ask fell into whichever lane nobody had excluded it from).
|
|
13
|
+
*
|
|
14
|
+
* The replacement: the gate derives the origin ONCE from the facts its layers established
|
|
15
|
+
* ({@link askOriginOf}), stamps it on the surviving ask, and each lane asks one question — "may this
|
|
16
|
+
* lane answer an ask of this origin?" — against a table that MUST name every member of the set
|
|
17
|
+
* ({@link AssertAllKeysHandled}). Adding a kind of ask is then: one member here, one arm in the
|
|
18
|
+
* derivation, one row per table — and the compiler refuses the build until the row exists, so the
|
|
19
|
+
* default for an unconsidered origin is a red build, never a silent allow.
|
|
20
|
+
*
|
|
21
|
+
* What is and is not an origin. An origin is WHO raised the question. Two facts that ride beside it
|
|
22
|
+
* are deliberately not origins: `requiresRealApproval` says what may CLEAR the question (a governance
|
|
23
|
+
* bit any origin can carry — a safety policy's own ask carries it with origin `policy`), and the
|
|
24
|
+
* gate's `autoMode` seat says whether a classifier is WIRED at all (a property of the session, not of
|
|
25
|
+
* the ask). Both stay where they are and are consumed beside the origin, not folded into it.
|
|
26
|
+
*/
|
|
27
|
+
import type { PermissionResult } from "./tool-policy.js";
|
|
28
|
+
/**
|
|
29
|
+
* The closed set, in AUTHORITY order — the order {@link askOriginOf} resolves a fold in when several
|
|
30
|
+
* facts hold on one call (a hook ask folded over a policy ask, an org stamp over either). The order
|
|
31
|
+
* decides only the WORD; every lane table below is total over the set, so it can never turn an ask
|
|
32
|
+
* one member would have excluded into one another member admits (the invariant is pinned).
|
|
33
|
+
*
|
|
34
|
+
* - `content_question` — the reserved question tool: a content-ROUTING signal, not a permission
|
|
35
|
+
* question; it leaves the gate through the content channel, never through a permission decider.
|
|
36
|
+
* - `unresolvable` — the call is MARKED by an ancestor (an inherited approver-unavailable float, or
|
|
37
|
+
* a durable mandate floated down because this task can park it): it must reach the park with no
|
|
38
|
+
* synchronous decision-maker in between.
|
|
39
|
+
* - `org_unavailable` — the deployment is org-governed and could not read its snapshot: the fail-closed
|
|
40
|
+
* tighten, cleared by a person only ("does the org forbid this?" is unanswered, and no automatic
|
|
41
|
+
* lane may answer it).
|
|
42
|
+
* - `org_rule` — an organization policy rule asked (or stamped an existing ask): "no configuration
|
|
43
|
+
* clears this, a person nods".
|
|
44
|
+
* - `hook` — a deployment's PreToolUse screening face raised or co-raised the ask; the word is
|
|
45
|
+
* engine-stamped at the fold and is the deployment's own per-call adjudication.
|
|
46
|
+
* - `ask_rule` — an EXPLICIT `ask` permission rule matched: a person's standing "ask me each time",
|
|
47
|
+
* an instruction rather than hesitation.
|
|
48
|
+
* - `denial_limit_fallback` — the auto-mode classifier reached its denial bound and handed the call
|
|
49
|
+
* to a person; the classifier stands BEFORE this ask, never after it.
|
|
50
|
+
* - `policy` — everything else: a deployment `ToolPolicy` ask, a mode-default ask, or one of the
|
|
51
|
+
* engine's own post-fold tightens (egress / irreversibility / peer referral / write protection).
|
|
52
|
+
* This is the hesitation family — the one an armed classifier exists to resolve.
|
|
53
|
+
*/
|
|
54
|
+
export declare const ASK_ORIGINS: readonly ["content_question", "unresolvable", "org_unavailable", "org_rule", "hook", "ask_rule", "denial_limit_fallback", "policy"];
|
|
55
|
+
export type AskOrigin = (typeof ASK_ORIGINS)[number];
|
|
56
|
+
/** Whether a value is a member of the closed origin set. Exported because the word crosses process
|
|
57
|
+
* boundaries: a host validating one it received must not hand-roll the set. */
|
|
58
|
+
export declare function isAskOrigin(v: unknown): v is AskOrigin;
|
|
59
|
+
/**
|
|
60
|
+
* A compile-time fence: `T` must be `never`. Instantiate it with `Exclude<<closed set>, keyof <table>>`
|
|
61
|
+
* beside a table that is meant to be TOTAL over the set — while every member has a row the argument is
|
|
62
|
+
* `never` and the build is green; add a member without its row and the argument is that member's
|
|
63
|
+
* literal, which does not satisfy the constraint, so the build is red AT THE FENCE with the missing
|
|
64
|
+
* member spelled out in the error. The table's own `satisfies Partial<Record<…>>` clause guards the
|
|
65
|
+
* other direction (a row for a word that is not in the set).
|
|
66
|
+
*/
|
|
67
|
+
export type AssertAllKeysHandled<T extends never> = T;
|
|
68
|
+
/** The facts about one call that only the gate holds — the derivation's inputs beyond the decision
|
|
69
|
+
* itself. Every member is a plain boolean/word read ONCE by the gate, so the derivation is a pure
|
|
70
|
+
* function that a test can drive arm by arm. */
|
|
71
|
+
export interface AskOriginFacts {
|
|
72
|
+
/** The call is on the reserved question tool. */
|
|
73
|
+
readonly contentQuestion: boolean;
|
|
74
|
+
/** The call is marked unresolvable by an ancestor (see the gate's `isMarkedUnresolvable` seat). */
|
|
75
|
+
readonly markedUnresolvable: boolean;
|
|
76
|
+
/** Which org state, if any, spoke on this call — the gate's own record of its org layer, never a
|
|
77
|
+
* read-back off the decision (a policy can put `decisionReason: "org_rule"` on its own ask; the
|
|
78
|
+
* gate's cell cannot be written by a policy). */
|
|
79
|
+
readonly org: "rule" | "unavailable" | undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The ONE derivation of an ask's origin, from the surviving decision plus the gate's own facts. The
|
|
83
|
+
* arms are in {@link ASK_ORIGINS} order (strongest authority first); the members it reads off the
|
|
84
|
+
* decision are the three the engine already treats as tighten-only provenance (`decisionReason:
|
|
85
|
+
* "hook"` is engine-stamped at both fold arms; `matchedAskRule` and `denialLimitFallback` are stamped
|
|
86
|
+
* by their mint sites and, when self-declared by a policy, can only opt that policy's own ask OUT of
|
|
87
|
+
* automatic resolution — the safe direction). Nothing here reads a member that could loosen.
|
|
88
|
+
*/
|
|
89
|
+
export declare function askOriginOf(decision: Extract<PermissionResult, {
|
|
90
|
+
action: "ask";
|
|
91
|
+
}>, facts: AskOriginFacts): AskOrigin;
|
|
92
|
+
/**
|
|
93
|
+
* The auto-mode CLASSIFIER's table: may a live classifier verdict resolve an ask of this origin?
|
|
94
|
+
*
|
|
95
|
+
* The classifier is the deployment's delegate for CLASSIFY-doctrine hesitation — the `policy` family,
|
|
96
|
+
* and ONLY it. Every other origin is a question the classifier is structurally the wrong party to
|
|
97
|
+
* answer, each for its own reason (kept beside its row so the reason travels with the fact):
|
|
98
|
+
* - `content_question`: a permission decider's verdict is neither an answer nor a park — letting it
|
|
99
|
+
* speak would skip the durable mandate, the park and the content route in one step.
|
|
100
|
+
* - `unresolvable`: a marked call must reach the park with no synchronous decision-maker between.
|
|
101
|
+
* - `org_unavailable` / `org_rule`: judgment may clear a `requiresRealApproval` bit a POLICY minted,
|
|
102
|
+
* but not the org's — while adjudication is unavailable the org's answer is unknown, and an org
|
|
103
|
+
* ask carries the org's own "a person nods" semantic.
|
|
104
|
+
* - `hook`: the deployment's own per-call adjudication; the classifier is its delegate for
|
|
105
|
+
* hesitation, not an override for its screening face.
|
|
106
|
+
* - `ask_rule`: a person's standing "ask me each time" — the classifier answering it would decide
|
|
107
|
+
* whether the person is asked at all, which is the rule's whole content.
|
|
108
|
+
* - `denial_limit_fallback`: the classifier already spoke (its bound is what minted this ask); an
|
|
109
|
+
* allow here would un-say the fallback, a block would count it twice.
|
|
110
|
+
* - `policy`: YES — including the engine's own safety tightens and a safety policy's
|
|
111
|
+
* `requiresRealApproval` ask (a live verdict is judgment, and arming auto mode is the deployment's
|
|
112
|
+
* explicit choice to let the classifier be the explicit ask-resolution for those classes).
|
|
113
|
+
* Exclusion is skip-not-deny: the ask flows the original chain (park / approver / headless deny) —
|
|
114
|
+
* narrower autonomy, never a lost ask.
|
|
115
|
+
*/
|
|
116
|
+
declare const CLASSIFIER_MAY_ANSWER: {
|
|
117
|
+
readonly content_question: false;
|
|
118
|
+
readonly unresolvable: false;
|
|
119
|
+
readonly org_unavailable: false;
|
|
120
|
+
readonly org_rule: false;
|
|
121
|
+
readonly hook: false;
|
|
122
|
+
readonly ask_rule: false;
|
|
123
|
+
readonly denial_limit_fallback: false;
|
|
124
|
+
readonly policy: true;
|
|
125
|
+
};
|
|
126
|
+
/** The fence over the classifier table: `never` while every origin has a row (pinned by a type test). */
|
|
127
|
+
export type ClassifierTableCoversEveryOrigin = AssertAllKeysHandled<Exclude<AskOrigin, keyof typeof CLASSIFIER_MAY_ANSWER>>;
|
|
128
|
+
/** Whether the auto-mode classifier may resolve an ask of this origin (see the table's contract). */
|
|
129
|
+
export declare function classifierMayAnswer(origin: AskOrigin): boolean;
|
|
130
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const ASK_ORIGINS = ["content_question", "unresolvable", "org_unavailable", "org_rule", "hook", "ask_rule", "denial_limit_fallback", "policy"];
|
|
2
|
+
const ASK_ORIGIN_SET = new Set(ASK_ORIGINS);
|
|
3
|
+
export function isAskOrigin(v) {
|
|
4
|
+
return ASK_ORIGIN_SET.has(v);
|
|
5
|
+
}
|
|
6
|
+
export function askOriginOf(decision, facts) {
|
|
7
|
+
if (facts.contentQuestion)
|
|
8
|
+
return "content_question";
|
|
9
|
+
if (facts.markedUnresolvable)
|
|
10
|
+
return "unresolvable";
|
|
11
|
+
if (facts.org === "unavailable")
|
|
12
|
+
return "org_unavailable";
|
|
13
|
+
if (facts.org === "rule")
|
|
14
|
+
return "org_rule";
|
|
15
|
+
if (decision.decisionReason === "hook")
|
|
16
|
+
return "hook";
|
|
17
|
+
if (decision.matchedAskRule !== undefined)
|
|
18
|
+
return "ask_rule";
|
|
19
|
+
if (decision.denialLimitFallback !== undefined)
|
|
20
|
+
return "denial_limit_fallback";
|
|
21
|
+
return "policy";
|
|
22
|
+
}
|
|
23
|
+
const CLASSIFIER_MAY_ANSWER = {
|
|
24
|
+
content_question: false,
|
|
25
|
+
unresolvable: false,
|
|
26
|
+
org_unavailable: false,
|
|
27
|
+
org_rule: false,
|
|
28
|
+
hook: false,
|
|
29
|
+
ask_rule: false,
|
|
30
|
+
denial_limit_fallback: false,
|
|
31
|
+
policy: true,
|
|
32
|
+
};
|
|
33
|
+
export function classifierMayAnswer(origin) {
|
|
34
|
+
return CLASSIFIER_MAY_ANSWER[origin];
|
|
35
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AutoModeRules, AutoModeWindowOptions } from "./auto-mode-prompt.js";
|
|
2
|
+
import type { AutoModeDenialLimitOptions } from "./auto-mode.js";
|
|
2
3
|
/** The recipe format's version. A reader that does not know a version REFUSES it (never guesses): the
|
|
3
4
|
* recipe names the criteria a classifier enforces, and a partially-understood criteria set is the one
|
|
4
5
|
* thing a permission gate may not improvise. */
|
|
@@ -29,6 +30,24 @@ export interface AutoModeArmingRecipe {
|
|
|
29
30
|
timeoutMs?: number;
|
|
30
31
|
/** Consecutive-failure threshold opening the one-way breaker (floored, as the decider itself floors it). */
|
|
31
32
|
failureThreshold?: number;
|
|
33
|
+
/**
|
|
34
|
+
* #556 — the classifier DENIAL-LIMIT bounds (`RunnerDeps.autoMode.denialLimit`, CC 2.1.250 `FO`/`AKe`):
|
|
35
|
+
* how many blocks the ancestor's classifier was allowed before a person had to look, and the window the
|
|
36
|
+
* fallback ask ran under. A KNOB triple, not prompt body — every member is orderable, so the fold takes
|
|
37
|
+
* the strictest of the two sides rather than refusing on a difference.
|
|
38
|
+
*
|
|
39
|
+
* It belongs on the recipe because the tracker CANNOT travel (it is live state, like the breaker) while
|
|
40
|
+
* its BOUNDS are exactly the kind of criteria the recipe exists to carry: without them a cross-process
|
|
41
|
+
* redemption rebuilt the ancestor's classifier and counted its blocks against the REDEEMING
|
|
42
|
+
* deployment's own bounds, which may be looser — the ancestor's "three strikes and a person looks"
|
|
43
|
+
* silently became the redeemer's five. (A `requiresRealApproval` fallback ask still has a person as its
|
|
44
|
+
* only exit either way, which is why this is a widening of the BUDGET and not of the gate.)
|
|
45
|
+
*
|
|
46
|
+
* Members are canonicalized under the SAME screen `createAutoModeDenialTracker` applies to the live
|
|
47
|
+
* face — a recorded value that tracker would refuse makes the whole recipe malformed here rather than
|
|
48
|
+
* being floored into one it accepts, because the rebuild hands these very numbers back to it.
|
|
49
|
+
*/
|
|
50
|
+
denialLimit?: AutoModeDenialLimitOptions;
|
|
32
51
|
/**
|
|
33
52
|
* The cross-session lane's classifier rule was spliced into the assembled prompt (the lane was
|
|
34
53
|
* mounted on the arming leg). Part of the PROMPT BODY: the rebuild re-splices the same engine
|
|
@@ -64,11 +83,29 @@ export interface AutoModeArmingFace {
|
|
|
64
83
|
window?: AutoModeWindowOptions;
|
|
65
84
|
timeoutMs?: number;
|
|
66
85
|
failureThreshold?: number;
|
|
86
|
+
denialLimit?: AutoModeDenialLimitOptions;
|
|
67
87
|
settingsEpoch?: string;
|
|
68
88
|
/** `true` when the cross-session lane's classifier rule is spliced into this deployment's classifier
|
|
69
89
|
* prompt (the Runner sets it from its own lane mount; a redeeming host declares it from its). */
|
|
70
90
|
crossSessionMessagesRule?: boolean;
|
|
71
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* #556 — the ONE tighten rule for the denial-limit bounds, shared by {@link foldAutoModeArming} and by
|
|
94
|
+
* the runner's rebuilt-tracker mint (a second computation of "the strictest of these two" is how the
|
|
95
|
+
* recipe's account and the tracker's actual bounds would come to disagree). Both sides' EFFECTIVE values
|
|
96
|
+
* are materialized first (an omitted bound is the default the tracker applies, never "unbounded" — the
|
|
97
|
+
* same correction the knob fold below already carries), and the result is explicit for the same reason.
|
|
98
|
+
*
|
|
99
|
+
* · `maxConsecutive` / `maxTotal` — the MINIMUM. A lower bound falls back to a person SOONER, which is
|
|
100
|
+
* the fail-closed direction on both: the classifier gets less rope, never more.
|
|
101
|
+
* · `autoDenyAfterMs` — the SHORTER WINDOW wins, and `0` is not a short window: it is the special value
|
|
102
|
+
* for "no window at all" (the fallback ask blocks on the approver, which is the shape a total-bound
|
|
103
|
+
* card and every park already run under, #559). Read numerically, `0` would read as the strictest
|
|
104
|
+
* value and would silently DISARM an ancestor's window; read as "no window", a declared absence of one
|
|
105
|
+
* never removes the other side's. Both surviving directions are deny-ward — an auto-deny can only ever
|
|
106
|
+
* refuse — so neither can widen the gate.
|
|
107
|
+
*/
|
|
108
|
+
export declare function tightenDenialLimit(a: AutoModeDenialLimitOptions | undefined, b: AutoModeDenialLimitOptions | undefined): Required<AutoModeDenialLimitOptions>;
|
|
72
109
|
/**
|
|
73
110
|
* Canonicalize + VALIDATE an arming recipe: the plain-data form that persists, or `undefined` when the
|
|
74
111
|
* input is not one (unknown version, a non-string in a rule list, a NaN knob, a non-object).
|
|
@@ -140,7 +177,9 @@ export type AutoModeArmingFold = {
|
|
|
140
177
|
* · bodies EQUAL ⇒ rebuild, with `timeoutMs`/`failureThreshold` taking the MINIMUM of the two. Both
|
|
141
178
|
* directions of that minimum are fail-closed: a shorter timeout turns a slow classifier into
|
|
142
179
|
* `unavailable` (the original chain, i.e. a human), and a lower threshold opens the one-way breaker
|
|
143
|
-
* sooner (the session falls back to non-auto). Neither can widen a verdict.
|
|
180
|
+
* sooner (the session falls back to non-auto). Neither can widen a verdict. The denial-limit triple
|
|
181
|
+
* (#556) folds the same way through {@link tightenDenialLimit} — fewer blocks before a person looks,
|
|
182
|
+
* and no window ever lengthened or removed.
|
|
144
183
|
* · bodies DIFFER ⇒ refuse. `settings_moved` normally; `epoch_inconsistent` when the two sides
|
|
145
184
|
* nevertheless declare the same epoch — that is a deployment telling us its settings did not move
|
|
146
185
|
* while its own data says otherwise, and picking either side would be a guess about which one is the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AUTO_MODE_DEFAULT_FAILURE_THRESHOLD, AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL, } from "./auto-mode-defaults.js";
|
|
1
|
+
import { AUTO_MODE_DEFAULT_FAILURE_THRESHOLD, AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS, } from "./auto-mode-defaults.js";
|
|
2
2
|
export const AUTO_MODE_ARMING_RECIPE_VERSION = 1;
|
|
3
3
|
const isPlainRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4
4
|
function canonicalStrings(v) {
|
|
@@ -22,6 +22,44 @@ function canonicalRuleSection(v) {
|
|
|
22
22
|
return undefined;
|
|
23
23
|
return deduped.length === 1 && deduped[0] === AUTO_MODE_DEFAULTS_SENTINEL ? undefined : deduped;
|
|
24
24
|
}
|
|
25
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
26
|
+
function canonicalDenialBound(v, min, max) {
|
|
27
|
+
if (v === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < min || v > max)
|
|
30
|
+
return null;
|
|
31
|
+
return v;
|
|
32
|
+
}
|
|
33
|
+
const NO_BOUND_CEILING = Number.POSITIVE_INFINITY;
|
|
34
|
+
function canonicalDenialLimit(v) {
|
|
35
|
+
if (v === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (!isPlainRecord(v))
|
|
38
|
+
return null;
|
|
39
|
+
const maxConsecutive = canonicalDenialBound(v.maxConsecutive, 1, NO_BOUND_CEILING);
|
|
40
|
+
const maxTotal = canonicalDenialBound(v.maxTotal, 1, NO_BOUND_CEILING);
|
|
41
|
+
const autoDenyAfterMs = canonicalDenialBound(v.autoDenyAfterMs, 0, MAX_TIMER_DELAY_MS);
|
|
42
|
+
if (maxConsecutive === null || maxTotal === null || autoDenyAfterMs === null)
|
|
43
|
+
return null;
|
|
44
|
+
const merged = {
|
|
45
|
+
...(maxConsecutive !== undefined ? { maxConsecutive } : {}),
|
|
46
|
+
...(maxTotal !== undefined ? { maxTotal } : {}),
|
|
47
|
+
...(autoDenyAfterMs !== undefined ? { autoDenyAfterMs } : {}),
|
|
48
|
+
};
|
|
49
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
50
|
+
}
|
|
51
|
+
export function tightenDenialLimit(a, b) {
|
|
52
|
+
const window = (v) => {
|
|
53
|
+
const eff = v ?? AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS;
|
|
54
|
+
return eff === 0 ? Number.POSITIVE_INFINITY : eff;
|
|
55
|
+
};
|
|
56
|
+
const shortest = Math.min(window(a?.autoDenyAfterMs), window(b?.autoDenyAfterMs));
|
|
57
|
+
return {
|
|
58
|
+
maxConsecutive: Math.min(a?.maxConsecutive ?? AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxConsecutive, b?.maxConsecutive ?? AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxConsecutive),
|
|
59
|
+
maxTotal: Math.min(a?.maxTotal ?? AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxTotal, b?.maxTotal ?? AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxTotal),
|
|
60
|
+
autoDenyAfterMs: Number.isFinite(shortest) ? shortest : 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
25
63
|
function canonicalNumber(v, min, floor) {
|
|
26
64
|
if (v === undefined)
|
|
27
65
|
return undefined;
|
|
@@ -39,7 +77,8 @@ export function sanitizeAutoModeArmingRecipe(value) {
|
|
|
39
77
|
const sessionContext = canonicalStrings(value.sessionContext);
|
|
40
78
|
const timeoutMs = canonicalNumber(value.timeoutMs, 1, false);
|
|
41
79
|
const failureThreshold = canonicalNumber(value.failureThreshold, 1, true);
|
|
42
|
-
|
|
80
|
+
const denialLimit = canonicalDenialLimit(value.denialLimit);
|
|
81
|
+
if (settingsDenyRules === null || sessionContext === null || timeoutMs === null || failureThreshold === null || denialLimit === null)
|
|
43
82
|
return undefined;
|
|
44
83
|
let rules;
|
|
45
84
|
if (value.rules !== undefined) {
|
|
@@ -94,6 +133,7 @@ export function sanitizeAutoModeArmingRecipe(value) {
|
|
|
94
133
|
...(window !== undefined ? { window } : {}),
|
|
95
134
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
96
135
|
...(failureThreshold !== undefined ? { failureThreshold } : {}),
|
|
136
|
+
...(denialLimit !== undefined ? { denialLimit } : {}),
|
|
97
137
|
...(settingsEpoch !== undefined && settingsEpoch !== "" ? { settingsEpoch } : {}),
|
|
98
138
|
};
|
|
99
139
|
}
|
|
@@ -107,6 +147,7 @@ export function autoModeArmingRecipeOf(face, bind) {
|
|
|
107
147
|
...(face.window !== undefined ? { window: face.window } : {}),
|
|
108
148
|
...(face.timeoutMs !== undefined ? { timeoutMs: face.timeoutMs } : {}),
|
|
109
149
|
...(face.failureThreshold !== undefined ? { failureThreshold: face.failureThreshold } : {}),
|
|
150
|
+
...(face.denialLimit !== undefined ? { denialLimit: face.denialLimit } : {}),
|
|
110
151
|
...(face.settingsEpoch !== undefined ? { settingsEpoch: face.settingsEpoch } : {}),
|
|
111
152
|
...(face.crossSessionMessagesRule !== undefined ? { crossSessionMessagesRule: face.crossSessionMessagesRule } : {}),
|
|
112
153
|
});
|
|
@@ -173,6 +214,8 @@ export function foldAutoModeArming(recorded, current) {
|
|
|
173
214
|
const minKnob = (a, b, fallback) => Math.min(a ?? fallback, b ?? fallback);
|
|
174
215
|
const timeoutMs = minKnob(rec.timeoutMs, cur.timeoutMs, AUTO_MODE_DEFAULT_TIMEOUT_MS);
|
|
175
216
|
const failureThreshold = minKnob(rec.failureThreshold, cur.failureThreshold, AUTO_MODE_DEFAULT_FAILURE_THRESHOLD);
|
|
217
|
+
const denialLimit = tightenDenialLimit(rec.denialLimit, cur.denialLimit);
|
|
218
|
+
const recordedDenial = tightenDenialLimit(rec.denialLimit, rec.denialLimit);
|
|
176
219
|
return {
|
|
177
220
|
ok: true,
|
|
178
221
|
recorded: rec,
|
|
@@ -180,8 +223,13 @@ export function foldAutoModeArming(recorded, current) {
|
|
|
180
223
|
...rec,
|
|
181
224
|
timeoutMs,
|
|
182
225
|
failureThreshold,
|
|
226
|
+
denialLimit,
|
|
183
227
|
...(cur.settingsEpoch !== undefined ? { settingsEpoch: cur.settingsEpoch } : {}),
|
|
184
228
|
},
|
|
185
|
-
tightened: timeoutMs < (rec.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS) ||
|
|
229
|
+
tightened: timeoutMs < (rec.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS) ||
|
|
230
|
+
failureThreshold < (rec.failureThreshold ?? AUTO_MODE_DEFAULT_FAILURE_THRESHOLD) ||
|
|
231
|
+
denialLimit.maxConsecutive < recordedDenial.maxConsecutive ||
|
|
232
|
+
denialLimit.maxTotal < recordedDenial.maxTotal ||
|
|
233
|
+
denialLimit.autoDenyAfterMs !== recordedDenial.autoDenyAfterMs,
|
|
186
234
|
};
|
|
187
235
|
}
|
package/dist/core/auto-mode.d.ts
CHANGED
|
@@ -113,38 +113,115 @@ export interface AutoModeDenialLimitOptions {
|
|
|
113
113
|
* delays and fires at once — the fail-closed direction, and invisible). */
|
|
114
114
|
autoDenyAfterMs?: number;
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
|
|
116
|
+
/** What BOTH shapes below say, and the whole of what a reader that only renders the card needs: the
|
|
117
|
+
* counts that tripped the bound and which bound it was. Declared once so the two faces cannot drift.
|
|
118
|
+
* A reader that only renders takes {@link DenialLimitFallbackFace} rather than this, so that an ARMED
|
|
119
|
+
* object LITERAL is accepted too (a parameter of this type would meet the excess-property check). */
|
|
120
|
+
export interface DenialLimitCounts {
|
|
119
121
|
/** Consecutive blocks INCLUDING the one that tripped the bound. */
|
|
120
122
|
readonly consecutive: number;
|
|
121
123
|
/** Total blocks this run INCLUDING the one that tripped the bound (read before the total-bound reset). */
|
|
122
124
|
readonly total: number;
|
|
123
125
|
/** Which bound tripped. */
|
|
124
126
|
readonly limit: "consecutive" | "total";
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The DECISION-layer member of a denial-limit fallback ask (`PermissionResult` ask arm): the counts
|
|
130
|
+
* that tripped the bound, and NOTHING about a window. A window is a fact of the ask's ROUTE — a live
|
|
131
|
+
* function approver can count down, a durable park row and a headless deny cannot — and the route is
|
|
132
|
+
* chosen downstream of the count, so the decision that carries these counts is by construction not in
|
|
133
|
+
* a position to state one. The type says so: there is no window member to write here, and the only
|
|
134
|
+
* ways to reach a {@link DenialLimitFallback} (the member an {@link
|
|
135
|
+
* import("./tool-policy.js").AskRequest} carries) are the two route stations,
|
|
136
|
+
* {@link AutoModeDenialTracker.armTimedWindow} and {@link unarmedWindow}. A host that builds its own
|
|
137
|
+
* gate adapter therefore cannot forward a decision's member straight into an approval request: the
|
|
138
|
+
* compiler stops it and names the station it skipped.
|
|
139
|
+
*/
|
|
140
|
+
export interface UnarmedDenialLimitFallback extends DenialLimitCounts {
|
|
141
|
+
/** FORBIDDEN on a decision, and stated rather than merely omitted. Leaving the member out would only
|
|
142
|
+
* stop a FRESH object literal (TypeScript's excess-property check); a policy that writes its window
|
|
143
|
+
* through a `const`, an `as const`, or an annotated return type would still compile — and would then
|
|
144
|
+
* have its number silently replaced at whichever route station the ask reached, turning a bounded
|
|
145
|
+
* wait into an unbounded one with no diagnostic. `never` makes the old shape a compile error at the
|
|
146
|
+
* policy boundary instead, so the retirement of the self-declared window is a migration a host is
|
|
147
|
+
* TOLD about rather than one it discovers when nobody is auto-denied. (A host that casts past the
|
|
148
|
+
* type still meets the route stations' runtime belt: they write the route's number, never the
|
|
149
|
+
* decision's.) */
|
|
150
|
+
readonly autoDenyAfterMs?: never;
|
|
151
|
+
}
|
|
152
|
+
/** The REQUEST-layer member (`AskRequest`, a durable park row, a checkpoint summary's echo): the same
|
|
153
|
+
* counts with the route's window already decided. Reachable only through the two route stations —
|
|
154
|
+
* {@link AutoModeDenialTracker.armTimedWindow} for a hand-out to a live function approver, and
|
|
155
|
+
* {@link unarmedWindow} for every route that cannot count down (a durable park, a headless deny, a
|
|
156
|
+
* non-function `onAsk` seat, a fallback no tracker of this run minted). Neither station accepts an
|
|
157
|
+
* ALREADY-armed member: a window is decided once, at the route, and re-routing an armed one would be
|
|
158
|
+
* a second decision with the first one silently discarded. */
|
|
159
|
+
export interface DenialLimitFallback extends DenialLimitCounts {
|
|
160
|
+
/** The auto-deny window for THIS ask, ms; `0` = none. Non-zero only where a live function approver
|
|
161
|
+
* is about to be called and the streak's timed card is still due — {@link
|
|
162
|
+
* AutoModeDenialTracker.armTimedWindow} is its one writer. The ask resolver reads this member to
|
|
163
|
+
* arm its deadline. */
|
|
127
164
|
readonly autoDenyAfterMs: number;
|
|
128
165
|
}
|
|
166
|
+
/** Either face of the member — what every READ-ONLY consumer accepts. A reader that renders the card
|
|
167
|
+
* has no interest in the route, so it must take both, and as a UNION rather than as the shared base:
|
|
168
|
+
* the base alone would reject a caller's four-member object literal on the excess-property check,
|
|
169
|
+
* which is a break with nothing behind it (the routing invariant constrains where a window is WRITTEN,
|
|
170
|
+
* never who may read one). */
|
|
171
|
+
export type DenialLimitFallbackFace = UnarmedDenialLimitFallback | DenialLimitFallback;
|
|
172
|
+
/**
|
|
173
|
+
* A stored or foreign value read back AS a fallback member, or `undefined` when it is not one: exactly
|
|
174
|
+
* the four members, each of the right kind (finite non-negative counts, the closed `limit` word, a
|
|
175
|
+
* finite non-negative window), nothing else carried. A deployment-written row (a checkpoint store is a
|
|
176
|
+
* host's) can hold any shape; the faces that echo it (the inbox summary) read through here, so a bad
|
|
177
|
+
* shape reads as ABSENT rather than reaching a consumer as a malformed card. Not a parser of intent —
|
|
178
|
+
* `"3"` is not 3.
|
|
179
|
+
*/
|
|
180
|
+
export declare function readDenialLimitFallback(v: unknown): DenialLimitFallback | undefined;
|
|
181
|
+
/** The window-less route's member, spelled in ONE place: every station that hands a fallback somewhere
|
|
182
|
+
* nothing can count down (a durable park row, a headless deny, a string `onAsk` seat, a member this
|
|
183
|
+
* run's tracker did not mint) passes through here rather than writing a `0` of its own. */
|
|
184
|
+
export declare function unarmedWindow(fallback: UnarmedDenialLimitFallback): DenialLimitFallback;
|
|
129
185
|
export type DenialLimitVerdict = {
|
|
130
186
|
limitReached: false;
|
|
131
187
|
} | {
|
|
132
188
|
limitReached: true;
|
|
133
|
-
fallback:
|
|
189
|
+
fallback: UnarmedDenialLimitFallback;
|
|
134
190
|
};
|
|
135
191
|
/** The per-run denial tracker. Lives beside {@link AutoModeDecider} (same owner, same lifetime). */
|
|
136
192
|
export interface AutoModeDenialTracker {
|
|
137
193
|
/** A classifier `block`: count first, judge second (CC `eme` → `tme`). Returns whether THIS block is
|
|
138
|
-
* the one that falls back to a person, with the fallback's
|
|
194
|
+
* the one that falls back to a person, with the fallback's counts. There is no window here — the
|
|
195
|
+
* route is not known yet, and the minted member has no member to hold one; see {@link armTimedWindow}. */
|
|
139
196
|
recordBlock(): DenialLimitVerdict;
|
|
197
|
+
/** The ONE writer of the timed card (CC `u8t`: the timed shape exists only when a dialog is present,
|
|
198
|
+
* the CONSECUTIVE bound tripped, and no timed card was shown this streak). Called by the station
|
|
199
|
+
* that is about to hand `fallback` to a live function approver; returns the member that ask carries:
|
|
200
|
+
* the configured window when the card is due, else the same counts with `0` ({@link unarmedWindow}).
|
|
201
|
+
* Sets the once-per-streak mark and `windowArmedAt` exactly when it hands out a non-zero window. A
|
|
202
|
+
* fallback this tracker did not mint (a deeper ancestor's, or a policy's self-declared member) comes
|
|
203
|
+
* back with no window — the tracker arms only the streak it counted, and consults each minted member
|
|
204
|
+
* ONCE (a second call with the same member arms nothing), so the hand-out is a recorded fact, not a
|
|
205
|
+
* convention. */
|
|
206
|
+
armTimedWindow(fallback: UnarmedDenialLimitFallback): DenialLimitFallback;
|
|
140
207
|
/** A classifier `allow`, or a person's allow of a fallback ask (CC `yR`): consecutive → 0 and the
|
|
141
208
|
* timed-card mark cleared. A rule/fast-path allow must NOT call this. */
|
|
142
209
|
recordAllow(): void;
|
|
143
|
-
/** The current counts (diagnostics / pins).
|
|
210
|
+
/** The current counts (diagnostics / pins). `windowArmedAt` = the epoch-ms instant
|
|
211
|
+
* {@link armTimedWindow} handed the CURRENT streak's fallback a non-zero window to a live approver
|
|
212
|
+
* (the instant CC's `timedFallbackShown` mark was set); present ⇔ that mark stands, absent otherwise
|
|
213
|
+
* — a fallback parked or denied headless WITHOUT reaching a function approver never arms it, a
|
|
214
|
+
* total-bound card is never timed, a disarmed window (`autoDenyAfterMs: 0`) arms nothing, and every
|
|
215
|
+
* reset (`recordAllow`, the total-bound budget reset) clears it. Present ⇒ the ask was handed to a
|
|
216
|
+
* function approver carrying a window; the resolver arms its deadline from it unless it exits before
|
|
217
|
+
* the wait (an already-aborted signal, unclonable args, or an approver that answers "unavailable" and
|
|
218
|
+
* sends the ask to the park) — the card counts as shown in those cases too, which errs toward a
|
|
219
|
+
* person being asked without a countdown, never toward an auto-deny nobody was shown. */
|
|
144
220
|
snapshot(): {
|
|
145
221
|
consecutive: number;
|
|
146
222
|
total: number;
|
|
147
223
|
timedFallbackShown: boolean;
|
|
224
|
+
windowArmedAt?: number;
|
|
148
225
|
};
|
|
149
226
|
}
|
|
150
227
|
/** Build the per-run tracker. Bad knob values throw (the deployment face is read at prepare; a refusal
|
|
@@ -154,6 +231,6 @@ export declare function createAutoModeDenialTracker(opts?: AutoModeDenialLimitOp
|
|
|
154
231
|
* and the inherited-lane arms): `Classifier denial limit exceeded, falling back to prompting: <limit
|
|
155
232
|
* sentence>` + a blank line + `Latest blocked action: <the classifier's own reason, or the tool name>`.
|
|
156
233
|
* The reason is the classifier MODEL's text — the caller neutralizes it before it gets here. */
|
|
157
|
-
export declare function denialLimitFallbackMessage(fallback:
|
|
234
|
+
export declare function denialLimitFallbackMessage(fallback: DenialLimitFallbackFace, latestBlockedAction: string): string;
|
|
158
235
|
/** The limit sentence alone (CC's two forms) — shared by the fallback ask and the headless terminal. */
|
|
159
|
-
export declare function denialLimitSentence(fallback:
|
|
236
|
+
export declare function denialLimitSentence(fallback: DenialLimitFallbackFace): string;
|