From c94ee83c0807f4b0cdb5511192f395e7d23c13d2 Mon Sep 17 00:00:00 2001 From: Trecek Date: Thu, 23 Jul 2026 11:14:28 -0700 Subject: [PATCH 01/62] feat(core): freeze context admission protocol v1 --- docs/decisions/0007-context-admission.md | 366 ++++ docs/decisions/README.md | 1 + src/autoskillit/core/AGENTS.md | 1 + src/autoskillit/core/__init__.pyi | 132 ++ src/autoskillit/core/context_admission.py | 1692 +++++++++++++++++ src/autoskillit/core/types/AGENTS.md | 1 + src/autoskillit/core/types/__init__.py | 3 + .../core/types/_type_context_admission.py | 1384 ++++++++++++++ src/autoskillit/core/types/_type_enums.py | 146 ++ tests/_test_filter.py | 2 + tests/arch/test_subpackage_isolation.py | 27 +- tests/arch/test_subpackage_structure.py | 1 + tests/core/AGENTS.md | 4 + tests/core/test_context_admission_coverage.py | 385 ++++ tests/core/test_context_admission_reducer.py | 1240 ++++++++++++ .../test_context_admission_state_machine.py | 485 +++++ .../types/test_context_admission_contract.py | 608 ++++++ tests/docs/AGENTS.md | 1 + tests/docs/test_context_admission_decision.py | 310 +++ tests/test_test_filter_core_cascade.py | 2 + 20 files changed, 6786 insertions(+), 5 deletions(-) create mode 100644 docs/decisions/0007-context-admission.md create mode 100644 src/autoskillit/core/context_admission.py create mode 100644 src/autoskillit/core/types/_type_context_admission.py create mode 100644 tests/core/test_context_admission_coverage.py create mode 100644 tests/core/test_context_admission_reducer.py create mode 100644 tests/core/test_context_admission_state_machine.py create mode 100644 tests/core/types/test_context_admission_contract.py create mode 100644 tests/docs/test_context_admission_decision.py diff --git a/docs/decisions/0007-context-admission.md b/docs/decisions/0007-context-admission.md new file mode 100644 index 000000000..509ae6cc1 --- /dev/null +++ b/docs/decisions/0007-context-admission.md @@ -0,0 +1,366 @@ +# ADR-0007: Cumulative Context Admission + +**Status:** Accepted + +**Date:** 2026-07-23 + +**Issue:** [#4333](https://github.com/TalonT-Org/AutoSkillit/issues/4333) + +**Work package:** C1 + +## Context + +AutoSkillit has lossless spill controls and raw per-result byte limits, but those controls do not +answer the cumulative question: whether the final request assembled for a model still fits the +active context window. Local history insertion, handler JSON, terminal billing usage, rollout +files, byte counts, and content digests each observe a different representation or a different +time. None proves both provider acceptance and remaining token capacity at admission time. + +This decision freezes C1's implementation-independent contract before C2-C8 add persistence, +adapters, enforcement, or reuse. Its executable form is +`CONTEXT_ADMISSION_PROTOCOL_VERSION`, the pure `reduce_context_admission` reducer, +`replay_context_admission`, and the immutable `CONTEXT_ADMISSION_COVERAGE` registry. + +## Decision + +AutoSkillit adopts protocol version 1 as the shared accounting contract for every model-visible +producer. A debit is authorized only from an authoritative window snapshot and an exact, +representation-bound admission chain. Every transition consumes the complete immutable prior +state and returns the complete next state, a typed decision, and content-free publication effects +as one atomic unit. + +This ADR defines a contract, not enforcement. When the required authority is absent, the only +valid result is a typed unavailable or upstream-gated decision; local estimates never acquire +authority by being conservative. + +## Admission boundary and authority + +The model-visible admission boundary is the exact incremental active-context growth of the final +canonical provider-request representation after all shaping, envelopes, spill markers, tool +arguments and results, hook feedback, retrieval, instructions, schemas, context injection, and +ordered batching. + +Authority requires all of the following to refer to that same immutable representation and +window epoch: + +1. an authoritative remaining-capacity snapshot; +2. an authoritative count for the proposed final input; +3. a binding between that count, the complete representation manifest, and the dispatched + request; and +4. a durable/queryable provider-acceptance or explicit non-admission witness. + +Local history insertion is not request inclusion. Request inclusion is not provider acceptance. +Terminal usage is post-hoc evidence, not admission authority. Bytes are emergency containment, +not token capacity. A digest is integrity metadata, not acceptance, access authority, or +occurrence identity. + +## Protocol version 1 + +Protocol version 1 is identified by `CONTEXT_ADMISSION_PROTOCOL_VERSION = 1`. The static +`CONTEXT_ADMISSION_COVERAGE` registry has exactly one row for every `ProducerSurface`. +`reduce_context_admission(state, event)` is a pure, exhaustive transition function, and +`replay_context_admission(initial_state, events)` deterministically folds a recorded stream. +Neither function performs I/O, persistence, enforcement, response mutation, or estimation. + +The lifecycle is: + +```text +PROPOSED + -> RESERVED + -> PREPARED + -> HISTORY_STAGED + -> REQUEST_DISPATCHED + -> COMMITTED +``` + +Before acceptance, an explicit non-admission or rollback witness may produce `RELEASED` or +`ROLLED_BACK`. A safe epoch rollover may produce `INVALIDATED` for work that was not dispatched. +An ambiguous crash produces `INDETERMINATE`, while inconsistent stored or provider facts produce +`QUARANTINED`; neither state silently releases capacity. + +An uninitialized state has no spendable capacity. Only an authoritative epoch-open snapshot +creates capacity. Commands carry a protocol version, event ID, idempotency namespace, and expected +aggregate revision. Exact event-ID replay and equivalent idempotent retry are resolved before +stale-revision rejection. + +## State, witnesses, and atomic batches + +One occurrence describes one immutable producer contribution and representation revision. An +`AdmissionBatch` owns a request ID, batch ID, ordered unique members, one reserve class, one +complete `CanonicalRepresentationManifest`, and, for protected work, one authorized pool owner. +The batch is reserved, prepared, staged, dispatched, committed, released, rolled back, invalidated, +or quarantined atomically. A provider's exact whole-batch charge is never invented as independent +per-member charges. + +Witnesses are typed and distinct: + +| Stage | Required evidence | +|---|---| +| Epoch open | authoritative snapshot sequence, model/tokenizer identity, and window ID/number | +| Input measured | exact counted representation and complete canonical representation manifest | +| Prepared | representation-binding witness for the counted and dispatch revisions | +| History staged | local history-staging witness | +| Request dispatched | exact request-inclusion witness and receiver fence | +| Committed | durable/queryable provider-acceptance witness for the request, batch, revision, epoch, and snapshot | +| Released/rolled back | explicit non-admission or rollback witness | +| Reconciled | authoritative reconciliation or exact output-usage witness | +| Epoch replaced | truncation/compaction replacement attestation and an authoritative new snapshot | + +Commit reconciles the reservation and exact authoritative charge in the same capacity +transaction. Timeouts, process exit, missing telemetry, and local rendered equality are not +release witnesses. A deadline may request or escalate reconciliation only. + +The reducer's `next_state`, decision, and effects are an atomic publication unit. C2 may persist +that unit with compare-and-swap, but may not split its semantic result. + +## Accounting and identity invariants + +1. An occurrence ID identifies one immutable descriptor. Reuse with changed lineage, surface, + epoch, representation, or batch membership is a conflict or corruption. +2. Admission occurrence identity, storage identity, integrity identity, and access authority are + separate domains. Canonical span IDs are opaque representation-local identities, not hashes. +3. The final manifest owns every model-visible span exactly once. Overlap or omission is rejected; + spans are provenance and duplicate-insertion controls, not additive token truth. +4. Input-context charge and output-generation allowance are separate domains. Generated-output + usage, including invisible tokens, reconciles its maximum; text later added to history is + measured again in the next final input representation. +5. Numeric values enter as authoritative inputs. There are no runtime numeric defaults and no + byte-to-token conversion. +6. Reservations, committed input, unresolved input, and generation reservations have exactly one + canonical accounting owner. They cannot be reconstructed from effects or logs. +7. Retry and resume are idempotent only for the same descriptor, representation revision, key, and + authoritative epoch. The attempt ID is not part of the reservation key. +8. A same-key retry with changed intent returns `CONFLICT` without corrupting valid state. Explicit + witnessed expiry creates a tombstone; wall-clock age neither expires identity nor releases + capacity. +9. Forked or child work has distinct lineage and epoch identity. It affects the parent only when + the parent-visible child delivery is accepted at the parent's admission boundary. +10. Serializable values contain bounded reason codes, counts, versions, timestamps, and opaque + non-secret IDs only. They never contain captured payloads. + +## Protected reserve and epoch isolation + +For one authoritative snapshot, protocol v1 computes global unallocated capacity from snapshot +remaining minus committed input, outstanding input reservations, unresolved input charge, and +outstanding generation reservations. Each protected pool separately subtracts its own committed, +outstanding, unresolved, and generation charge from its injected capacity. Ordinary availability +is global unallocated minus all unused protected capacity; a protected owner can spend only the +non-negative unused capacity of its own class, bounded by global unallocated. + +Protected `SYNTHESIS` and `FINAL_RESPONSE` pools have explicit capability owners, priority, and +release-witness rules. Version 1 forbids borrowing and mixed-reserve-class batches. Every input and +output reservation for one request has the same owner/class, and total injected protected +capacity cannot exceed authoritative remaining capacity at epoch open. This preserves reserve +without subtracting protected use twice. + +A compaction, model, tokenizer, or window change closes the old epoch. Undispatched reservations +are invalidated and cannot transfer. Dispatched or indeterminate work and its conservative charge +remain in immutable closed-epoch audit state. A new identity is not itself a fence: rollover +requires a receiver-validated epoch fence, proof that all old dispatches resolved, or a new +authoritative snapshot that already deducts every unresolved old-epoch charge. + +## Producer coverage matrix + +The states have these meanings: + +- `VERIFIED`: primary source evidence proves the named local observation/control point, but never + implies a global token watermark. +- `PARTIAL`: a useful observation/control point exists, but the final representation, complete + identity, or capacity authority is missing. +- `UPSTREAM_GATED`: no local boundary can observe or control the required fact; Codex/provider + participation is required. + +Every authority state is `UPSTREAM_GATED` for the tested configuration. The following is the +complete, version-pinned projection of `CONTEXT_ADMISSION_COVERAGE`; each row has a stable +`COV-` claim ID and deterministically degrades on version or configuration mismatch. + +| Producer surface | Control-point owner | Observation | Authority | +|---|---|---|---| +| `NATIVE_SHELL` | `shell_capture_hook` | `VERIFIED` | `UPSTREAM_GATED` | +| `UNIFIED_EXEC_AND_WRITE_STDIN` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `APPLY_PATCH` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `AUTOSKILLIT_MCP` | `track_response_size` | `VERIFIED` | `UPSTREAM_GATED` | +| `EXTERNAL_MCP` | `fastmcp_client` | `PARTIAL` | `UPSTREAM_GATED` | +| `AUTOSKILLIT_LOCAL_FUNCTION` | `local_function_dispatch` | `VERIFIED` | `UPSTREAM_GATED` | +| `OTHER_LOCAL_FUNCTION` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `MCP_RESOURCE` | `fastmcp_client` | `PARTIAL` | `UPSTREAM_GATED` | +| `CLIENT_PROVIDER_RETRIEVAL` | `codex_host` | `UPSTREAM_GATED` | `UPSTREAM_GATED` | +| `CODE_MODE_AGGREGATE` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `HOSTED_SPECIALIZED_TOOL` | `codex_host` | `PARTIAL` | `UPSTREAM_GATED` | +| `HOOK_FEEDBACK` | `hook_registry` | `VERIFIED` | `UPSTREAM_GATED` | +| `TOOL_ARGUMENT` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `TOOL_RESULT_ENVELOPE` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `USER_PROMPT` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `ASSISTANT_OUTPUT_HISTORY` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `SKILL_PLUGIN_CONTEXT` | `final_request_assembler` | `PARTIAL` | `UPSTREAM_GATED` | +| `OTHER_CONTEXT_INJECTION` | `final_request_assembler` | `UPSTREAM_GATED` | `UPSTREAM_GATED` | +| `HEADLESS_CHILD_PROMPT` | `headless_prompt_builder` | `VERIFIED` | `UPSTREAM_GATED` | +| `PARENT_VISIBLE_CHILD_DELIVERY` | `child_delivery_receipt` | `VERIFIED` | `UPSTREAM_GATED` | +| `COMPACTION_MODEL_WINDOW_TRANSITION` | `compaction_receiver` | `PARTIAL` | `UPSTREAM_GATED` | + +`VERIFIED` rows use AutoSkillit source at revision `ac8f653a00d2`. Codex-backed rows use +codex-cli `0.145.0` at revision `25af12f7e61572b0bc18ddb1008be543b91519b0`. +`CLIENT_PROVIDER_RETRIEVAL` and `OTHER_CONTEXT_INJECTION` are explicitly inference-backed gap +claims, not verified source claims. Static source pins are documentation provenance, not runtime +lineage. + +## Authority unavailable and byte ceilings + +If the watermark, exact final measurement, receiver fence, or provider witness is absent, +`reduce_context_admission` returns a typed `watermark_unavailable` or `upstream_gated` decision. +Uninitialized and authority-unavailable states are non-spendable. Estimates provide **no numeric** +authorization for cumulative admission or enforcement. + +Existing lossless spill controls and raw-byte emergency ceilings remain independent and +unchanged. They continue to bound transport and result risk even when token authority is +unavailable, but cannot authorize a token debit. Likewise, token authority must not weaken any +existing raw per-producer byte ceiling. + +## Upstream authority request + +Codex/provider support must expose all three parts as one authority contract: + +1. An **atomic snapshot/reservation** operation for interceptable input producers, plus a separate + **generated-output maximum** reservation. +2. A **synchronous blocking** operation on the **final ordered batch**, after every + transformation, that performs exact measurement and admission using the provider's input-token + count operation when available. It must bind the result to an immutable + canonical representation manifest and a receiver fence immediately before provider request + assembly. +3. A **durable/queryable journal** that records distinct facts for **history staging**, exact + **request inclusion**, **provider acceptance**, **output-usage reconciliation**, **rollback**, + **truncation/compaction replacement**, and **authoritative reconciliation**. + +The minimum request and event fields are: `request_id`, `batch_id`, **ordered members**, +**reservation IDs**, **thread/turn/agent lineage**, **admission sequence**, **window ID/number**, +**model/tokenizer identity**, **snapshot sequence**, **measurement kind/source**, +**active/hard-limit/remaining/proposed/max-output counts**, **reserve class**, and +**representation revision**. A compaction, model, window, or tokenizer transition additionally +carries old and new identities and before/after authority status. + +The OpenAI input-token endpoint can measure one resolved representation but does not reserve +capacity, attest that later mutable conversation state is identical, fence a receiver, or prove +provider acceptance. Post-hoc usage likewise cannot substitute for this three-part contract. + +## Privacy and observability + +Runtime IDs are random, opaque, non-secret values and are never derived from personal data. +Runtime/audit records and Aggregate telemetry are governed separately: + +| Field class | Purpose | Maximum length/cardinality | Retention | Access | Deletion | Export | +|---|---|---|---|---|---|---| +| Runtime/audit opaque IDs, counts, versions, reason codes, timestamps, and witness kinds | deterministic replay, reconciliation, and incident audit | IDs/reason codes are schema-bounded; collection cardinality is bounded by one epoch and configured audit retention | only for configured operational/audit retention | authorized operators and the owning runtime | delete with the owning session/audit-retention policy, subject to required security retention | content-free structured records only | +| Runtime/audit lineage and source locator fields | correlate a request, turn, agent, source claim, and authority witness | typed fixed field set; each value is schema-bounded; no free-form labels | same as the owning session or coverage record | authorized operators and maintainers | delete runtime lineage with its session; retain static provenance only with the released contract | export only to access-controlled audit channels; never to Aggregate telemetry | +| Aggregate telemetry state/reason/version counters | measure coverage state, unavailable decisions, and protocol health | fixed enum dimensions and bounded aggregate buckets; no per-session cardinality | configured aggregate-metrics retention | operators with aggregate-metrics access | delete by aggregate retention schedule | aggregates only; no opaque IDs, lineage, source locator, or user-controlled values | +| Forbidden content | prevent model or user data from escaping the accounting plane | zero: no content, payloads, prompts, tool results, absolute paths, bearer tokens, credentials, or content/artifact hashes | never retained | no access because collection is prohibited | reject before publication; remove affected invalid record under incident procedure | never exported | + +Serialization, `repr`, and exception paths must remain content-free. Contract tests use canaries to +prove that content, absolute paths, bearer tokens, and content/artifact hashes cannot escape. +Static commit pins in coverage evidence document provenance only and are not runtime identities. + +## Capability decision for Codex 0.145.0 + +The tested runtime is **codex-cli 0.145.0**, tag `rust-v0.145.0`, peeled commit +`25af12f7e61572b0bc18ddb1008be543b91519b0`. + +Primary source evidence at that exact commit: + +- [context status and window fields](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/core/src/session/context_window.rs) +- [history accounting and estimation](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/core/src/context_manager/history.rs) +- [the disabled experimental `token_budget` feature](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/features/src/lib.rs) +- [`get_context_remaining` tool registration](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/core/src/tools/spec_plan.rs) +- [`get_context_remaining` polling handler](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/core/src/tools/handlers/get_context_remaining.rs) +- [App Server usage fields](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/app-server-protocol/src/protocol/v2/thread.rs) +- [App Server raw-response documentation](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/app-server/README.md) +- [compaction identity](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/protocol/src/protocol.rs) +- [four-byte truncation heuristic](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/utils/string/src/truncate.rs) + +Official provider documentation: [input-token-count API +reference](https://platform.openai.com/docs/api-reference/responses/input-tokens), +[token-counting guide](https://platform.openai.com/docs/guides/token-counting), and +[compaction guide](https://platform.openai.com/docs/guides/compaction). + +The `token_budget` capability is disabled and experimental. `get_context_remaining` is a +model-callable/polling observation, not an atomic reservation available synchronously to every +producer. Internal `base_window_tokens_remaining`, terminal usage, and rollout evidence are +estimated or post-hoc and do not bind the final request to provider acceptance. App Server usage +may be null and is reconciliation evidence only. The pinned hook schemas expose `PreCompact` and +`PostCompact` notification identities, but no atomic hook-visible admission watermark that fences +all producers and the final representation. + +**Inference (clearly labeled):** taken together, these pinned source facts imply that a plugin +cannot close the count-then-mutate interval or prove that the provider accepted the exact +representation it counted. + +**Verdict:** Codex 0.145.0 cannot supply authoritative admission capacity. Its observations may +support diagnostics and reconciliation, but every cumulative admission authority claim remains +`UPSTREAM_GATED`. + +## Protocol evolution + +Version 1 event semantic fields, state meanings, and witness requirements are frozen. Unknown +protocol versions fail closed; they are never silently coerced to version 1. Replay selects +semantics from each stream's recorded protocol version. + +Any semantic change requires a new protocol version and explicit versioned conformance vectors. +C2 (#4334) owns durable encoding, stored-event migration, and upcasting. Upcasting must preserve +the recorded meaning and audit identity; it cannot manufacture an authority witness or reinterpret +an old unavailable decision as admitted. + +## Downstream dependency graph + +This exact graph identifies contract consumers and owners; it does not authorize their +implementation in C1: + +```text +#4333 C1 -> #4334 C2 +#4333 C1 + #4334 C2 + #4335 C3 -> #4336 C4 +#4333 C1 + #4334 C2 + #4335 C3 -> #4337 C5 +#4333 C1 + #4334 C2 -> #4338 C8 + +#4319/#4320/#4321/#4322/#4325/#4326/#4327 -> #4335 C3 artifact authority +#4334 C2 + #4336 C4 + #4337 C5 + #4324 + #4338 C8 -> #4339 C6 +#4334 C2 + #4335 C3 + #4271 + #4338 C8 -> #4340 C7 +``` + +Thus C2 is #4334, C3 is #4335, C4 is #4336, C5 is #4337, C6 is #4339, C7 is #4340, and C8 is +#4338. C2-C8 must use this shared accounting contract rather than invent per-adapter semantics. + +## Non-goals + +- This decision adds no enforcement and chooses no numeric budget defaults. +- Existing raw per-producer ceilings remain in force and are not replaced or relaxed. +- Bytes are not an exact token proxy and are not converted into admission authority. +- A digest is not an access capability or deduplication identity. +- C1 adds no journal, store, producer adapter, provider integration, response mutation, fleet + schema, deduplication store, artifact reuse, or compaction rehydration. +- This decision does not claim that a local observation is provider acceptance or that post-hoc + billing usage can reserve capacity. + +## Traceability + +| ID | Requirement | Contract or verification target | +|---|---|---| +| INV-1 | model-visible admission boundary | Admission boundary and authority | +| INV-2 | stable identities | Accounting and identity invariants | +| INV-3 | atomic reserve/commit/release protocol | State, witnesses, and atomic batches | +| INV-4 | version-pinned coverage matrix | CONTEXT_ADMISSION_COVERAGE | +| INV-5 | token_budget/get_context_remaining | Capability decision for Codex 0.145.0 | +| INV-6 | upstream Codex contract | Upstream authority request | +| INV-7 | privacy-safe observability | Privacy and observability | +| OUT-1 | versioned admission protocol and state machine | CONTEXT_ADMISSION_PROTOCOL_VERSION | +| OUT-2 | producer/control-point coverage matrix | CONTEXT_ADMISSION_COVERAGE | +| OUT-3 | accounting and identity invariants | Accounting and identity invariants | +| OUT-4 | failure and reconciliation semantics | reduce_context_admission | +| OUT-5 | authoritative token accounting | Admission boundary and authority | +| OUT-6 | upstream Codex request | Upstream authority request | +| OUT-7 | implementation dependency graph | Downstream dependency graph | +| NG-1 | no enforcement or numeric budget defaults | Non-goals | +| NG-2 | retain existing raw per-producer ceilings | Non-goals | +| NG-3 | bytes are not an exact token proxy | Non-goals | +| NG-4 | digest is not an access capability or deduplication identity | Non-goals | +| AC-1 | every model-visible producer | Producer coverage matrix | +| AC-2 | idempotent reservation keys and compaction/window reset rules | Accounting and identity invariants | +| AC-3 | outstanding concurrent calls and protected reserve | Protected reserve and epoch isolation | +| AC-4 | Codex claims cite tested version and primary sources | Capability decision for Codex 0.145.0 | +| AC-5 | C2-C8 use the shared accounting contract | Downstream dependency graph | diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 64fc00742..3894e5996 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -6,3 +6,4 @@ - [0004-recipe-redelivery.md](0004-recipe-redelivery.md) — Sanctioned `load_recipe` channel for recipe knowledge re-delivery after Codex context compaction - [0005-output-budget-protocol.md](0005-output-budget-protocol.md) — Bound per-response model-context output with lossless artifacts, pre-spend guards, and derived transport ceilings - [0006-output-containment.md](0006-output-containment.md) — Retire pre-execution command-shape classification in favor of per-backend output-boundary bounding on measured bytes +- [0007-context-admission.md](0007-context-admission.md) — Freeze the versioned cumulative context-admission boundary, authority contract, producer coverage, and privacy rules diff --git a/src/autoskillit/core/AGENTS.md b/src/autoskillit/core/AGENTS.md index 876911047..dff29aa69 100644 --- a/src/autoskillit/core/AGENTS.md +++ b/src/autoskillit/core/AGENTS.md @@ -32,6 +32,7 @@ Sub-packages: types/ (see types/AGENTS.md) and runtime/ (see runtime/AGENTS.md). | `closure_hashing.py` | Canonical hash utilities for closure-mode verdict verification (stdlib-only, IL-0) | | `path_containment.py` | Path containment guards — symlink/hardlink check, TOCTOU guard (stdlib-only, IL-0) | | `closure_verifier.py` | Independent verifier for closure-mode reports (stdlib-only, IL-0) | +| `context_admission.py` | Pure protocol-v1 cumulative context-admission reducer, replay, and coverage resolution | ## Architecture Notes diff --git a/src/autoskillit/core/__init__.pyi b/src/autoskillit/core/__init__.pyi index 7186cc0fc..4a503af68 100644 --- a/src/autoskillit/core/__init__.pyi +++ b/src/autoskillit/core/__init__.pyi @@ -48,6 +48,21 @@ from .claude_conventions import validate_add_dir as validate_add_dir from .claude_conventions import validate_worktree_path as validate_worktree_path from .closure_verifier import VerificationResult as VerificationResult from .closure_verifier import verify_closure_report as verify_closure_report +from .context_admission import ( + ContextAdmissionValidationError as ContextAdmissionValidationError, +) +from .context_admission import ( + UnsupportedContextAdmissionProtocolError as UnsupportedContextAdmissionProtocolError, +) +from .context_admission import ( + reduce_context_admission as reduce_context_admission, +) +from .context_admission import ( + replay_context_admission as replay_context_admission, +) +from .context_admission import ( + resolve_context_admission_coverage as resolve_context_admission_coverage, +) from .feature_flags import _collect_disabled_feature_tags as _collect_disabled_feature_tags from .feature_flags import is_feature_enabled as is_feature_enabled from .git_remote import REMOTE_PRECEDENCE as REMOTE_PRECEDENCE @@ -165,6 +180,10 @@ from .types import CODEX_SCHEMA_VERSION as CODEX_SCHEMA_VERSION from .types import CODEX_SESSIONS_SUBDIR as CODEX_SESSIONS_SUBDIR from .types import CODEX_VALID_MODEL_IDS as CODEX_VALID_MODEL_IDS from .types import CONFIG_AUTHORITY_KEYS as CONFIG_AUTHORITY_KEYS +from .types import CONTEXT_ADMISSION_COVERAGE as CONTEXT_ADMISSION_COVERAGE +from .types import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION as CONTEXT_ADMISSION_PROTOCOL_VERSION, +) from .types import CONTEXT_EXHAUSTION_MARKER as CONTEXT_EXHAUSTION_MARKER from .types import CORE_PACKS as CORE_PACKS from .types import DATA_MANIFEST_SOURCE_TYPES as DATA_MANIFEST_SOURCE_TYPES @@ -271,16 +290,47 @@ from .types import UNGATED_TOOLS as UNGATED_TOOLS from .types import VALID_INPUT_SPEC_TYPES as VALID_INPUT_SPEC_TYPES from .types import VARIADIC_CLAUDE_FLAGS as VARIADIC_CLAUDE_FLAGS from .types import WORKTREE_SKILLS as WORKTREE_SKILLS +from .types import AcceptInputEvent as AcceptInputEvent +from .types import ActiveContextAdmissionState as ActiveContextAdmissionState +from .types import AdmissionAttemptId as AdmissionAttemptId +from .types import AdmissionBatch as AdmissionBatch +from .types import AdmissionBatchId as AdmissionBatchId +from .types import AdmissionBatchRecord as AdmissionBatchRecord +from .types import AdmissionDecision as AdmissionDecision +from .types import AdmissionDecisionKind as AdmissionDecisionKind +from .types import AdmissionEffect as AdmissionEffect +from .types import AdmissionEventId as AdmissionEventId +from .types import AdmissionOccurrence as AdmissionOccurrence +from .types import AdmissionOccurrenceId as AdmissionOccurrenceId +from .types import AdmissionOccurrenceRecord as AdmissionOccurrenceRecord +from .types import AdmissionReplay as AdmissionReplay +from .types import AdmissionRequestId as AdmissionRequestId +from .types import AdmissionReservation as AdmissionReservation +from .types import AdmissionReservationId as AdmissionReservationId +from .types import AdmissionReservationKey as AdmissionReservationKey +from .types import AdmissionSequence as AdmissionSequence +from .types import AdmissionState as AdmissionState +from .types import AdmissionTransition as AdmissionTransition +from .types import AdmissionWitness as AdmissionWitness +from .types import AdmissionWitnessId as AdmissionWitnessId +from .types import AgentInstanceId as AgentInstanceId from .types import AgentPackDef as AgentPackDef from .types import AgentSessionResult as AgentSessionResult +from .types import AggregateRevision as AggregateRevision from .types import ApiRetryOutcome as ApiRetryOutcome from .types import AuditLog as AuditLog +from .types import AuthoritySourceId as AuthoritySourceId +from .types import AuthorityUnavailableEffect as AuthorityUnavailableEffect +from .types import AuthorityUnavailableEvent as AuthorityUnavailableEvent from .types import BackendCapabilities as BackendCapabilities from .types import BackendConventions as BackendConventions from .types import BackendEventKind as BackendEventKind from .types import BackgroundSupervisor as BackgroundSupervisor from .types import BareResume as BareResume from .types import CampaignProtector as CampaignProtector +from .types import CanonicalRepresentationManifest as CanonicalRepresentationManifest +from .types import CanonicalSpanId as CanonicalSpanId +from .types import CanonicalSpanOwner as CanonicalSpanOwner from .types import CanonicalTokenUsage as CanonicalTokenUsage from .types import CapabilityNotSupportedError as CapabilityNotSupportedError from .types import CapabilityResolutionDetail as CapabilityResolutionDetail @@ -290,6 +340,8 @@ from .types import CaptureValueType as CaptureValueType from .types import CaptureValueTypeError as CaptureValueTypeError from .types import ChannelBStatus as ChannelBStatus from .types import ChannelConfirmation as ChannelConfirmation +from .types import ChargeCommittedEffect as ChargeCommittedEffect +from .types import ChargeDomain as ChargeDomain from .types import CIRunScope as CIRunScope from .types import CIWatcher as CIWatcher from .types import ClaudeContentBlockType as ClaudeContentBlockType @@ -302,6 +354,7 @@ from .types import CloneGateUnpublished as CloneGateUnpublished from .types import CloneManager as CloneManager from .types import CloneResult as CloneResult from .types import CloneSuccessResult as CloneSuccessResult +from .types import ClosedEpochAudit as ClosedEpochAudit from .types import ClosureAuthoritySpec as ClosureAuthoritySpec from .types import ClosureReport as ClosureReport from .types import ClosureRow as ClosureRow @@ -312,17 +365,33 @@ from .types import CodexEventType as CodexEventType from .types import CodexItemType as CodexItemType from .types import CodingAgentBackend as CodingAgentBackend from .types import CompletionRequiredResolver as CompletionRequiredResolver +from .types import ConflictRejectedEffect as ConflictRejectedEffect from .types import ContaminationOutcome as ContaminationOutcome +from .types import ContextAdmissionEvent as ContextAdmissionEvent +from .types import ContextAdmissionState as ContextAdmissionState +from .types import ContextLineage as ContextLineage +from .types import ContextSessionId as ContextSessionId +from .types import ContextThreadId as ContextThreadId +from .types import ContextWindowSnapshot as ContextWindowSnapshot +from .types import CoverageEvidence as CoverageEvidence +from .types import CoverageEvidenceKind as CoverageEvidenceKind +from .types import CoverageState as CoverageState from .types import CrossDomainAssessment as CrossDomainAssessment from .types import CrossDomainPrescription as CrossDomainPrescription from .types import DatabaseReader as DatabaseReader +from .types import DeliveryOccurrenceId as DeliveryOccurrenceId from .types import DialingConfig as DialingConfig from .types import DirectInstall as DirectInstall from .types import DispatchGateType as DispatchGateType from .types import DispatchIdentity as DispatchIdentity +from .types import DispatchRequestEvent as DispatchRequestEvent from .types import EffectiveSkillCatalogAuthority as EffectiveSkillCatalogAuthority from .types import EffectiveSkillInvocationAuthority as EffectiveSkillInvocationAuthority from .types import EnvPolicy as EnvPolicy +from .types import EpochClosedEffect as EpochClosedEffect +from .types import EpochFenceProof as EpochFenceProof +from .types import ExpiredIdempotencyTombstone as ExpiredIdempotencyTombstone +from .types import ExpireIdempotencyKeyEvent as ExpireIdempotencyKeyEvent from .types import FailureRecord as FailureRecord from .types import FeatureDef as FeatureDef from .types import FeatureLifecycle as FeatureLifecycle @@ -330,12 +399,23 @@ from .types import FigureSpec as FigureSpec from .types import FleetErrorCode as FleetErrorCode from .types import FleetLock as FleetLock from .types import FleetSessionEnv as FleetSessionEnv +from .types import ForkOccurrenceId as ForkOccurrenceId from .types import GateState as GateState +from .types import GenerationReconciledEffect as GenerationReconciledEffect +from .types import GenerationReservationId as GenerationReservationId +from .types import GenerationReservationRecord as GenerationReservationRecord +from .types import ( + GenerationReservationRecordedEffect as GenerationReservationRecordedEffect, +) +from .types import GenerationState as GenerationState from .types import GitHubApiLog as GitHubApiLog from .types import GitHubFetcher as GitHubFetcher from .types import HardCapabilityMismatch as HardCapabilityMismatch from .types import HeadlessExecutor as HeadlessExecutor from .types import HeadlessSkillDispatchContract as HeadlessSkillDispatchContract +from .types import IdempotencyExpiredEffect as IdempotencyExpiredEffect +from .types import IdempotencyNamespace as IdempotencyNamespace +from .types import IdempotencyRecord as IdempotencyRecord from .types import InfraExitCategory as InfraExitCategory from .types import InfraOutcome as InfraOutcome from .types import InputContractResolver as InputContractResolver @@ -352,27 +432,42 @@ from .types import LensEntry as LensEntry from .types import LoadReport as LoadReport from .types import LoadResult as LoadResult from .types import MarketplaceInstall as MarketplaceInstall +from .types import MarkGenerationIndeterminateEvent as MarkGenerationIndeterminateEvent +from .types import MarkIndeterminateEvent as MarkIndeterminateEvent from .types import McpResponseLog as McpResponseLog +from .types import MeasurementKind as MeasurementKind from .types import MergeFailedStep as MergeFailedStep from .types import MergeQueueWatcher as MergeQueueWatcher from .types import MergeState as MergeState from .types import MigrationService as MigrationService from .types import ModelIdentity as ModelIdentity +from .types import ModelItemId as ModelItemId from .types import ModelTotalEntry as ModelTotalEntry from .types import ModelTranslation as ModelTranslation from .types import NamedResume as NamedResume from .types import NdjsonDriftOutcome as NdjsonDriftOutcome from .types import NoResume as NoResume +from .types import OccurrenceStateChangedEffect as OccurrenceStateChangedEffect +from .types import OpenEpochEvent as OpenEpochEvent from .types import OutputFormat as OutputFormat from .types import OutputPatternResolver as OutputPatternResolver from .types import PackDef as PackDef from .types import PhoropterPhaseSkip as PhoropterPhaseSkip from .types import PhoropterPrescription as PhoropterPrescription from .types import PluginSource as PluginSource +from .types import PrepareBatchEvent as PrepareBatchEvent +from .types import ProcessedEventRecord as ProcessedEventRecord from .types import ProcessStaleError as ProcessStaleError +from .types import ProducerCoverageDef as ProducerCoverageDef +from .types import ProducerInstanceId as ProducerInstanceId +from .types import ProducerSurface as ProducerSurface from .types import PromptContractError as PromptContractError +from .types import ProposeOccurrenceEvent as ProposeOccurrenceEvent +from .types import ProtectedPoolOwnerId as ProtectedPoolOwnerId +from .types import ProtectedPoolSpec as ProtectedPoolSpec from .types import ProviderOutcome as ProviderOutcome from .types import PRState as PRState +from .types import QuarantineRecordedEffect as QuarantineRecordedEffect from .types import QuotaPolicy as QuotaPolicy from .types import QuotaRefreshTask as QuotaRefreshTask from .types import ReadingToken as ReadingToken @@ -393,12 +488,39 @@ from .types import RecipeSectionContentFormatDef as RecipeSectionContentFormatDe from .types import RecipeSectionDef as RecipeSectionDef from .types import RecipeSectionValidationFinding as RecipeSectionValidationFinding from .types import RecipeSource as RecipeSource +from .types import ReconcileGenerationEvent as ReconcileGenerationEvent +from .types import ( + ReconciliationEscalationEffect as ReconciliationEscalationEffect, +) +from .types import ( + ReconciliationQueryRequestedEffect as ReconciliationQueryRequestedEffect, +) +from .types import ReleaseNonAdmissionEvent as ReleaseNonAdmissionEvent +from .types import RepresentationBindingWitness as RepresentationBindingWitness +from .types import RepresentationRevision as RepresentationRevision +from .types import RequestReconciliationEvent as RequestReconciliationEvent +from .types import ReservationInvalidatedEffect as ReservationInvalidatedEffect +from .types import ReservationRecordedEffect as ReservationRecordedEffect +from .types import ReservationReleasedEffect as ReservationReleasedEffect +from .types import ReserveClass as ReserveClass +from .types import ReserveRequestEvent as ReserveRequestEvent +from .types import ( + ResolveIndeterminateAcceptedEvent as ResolveIndeterminateAcceptedEvent, +) +from .types import ( + ResolveIndeterminateNonAdmissionEvent as ResolveIndeterminateNonAdmissionEvent, +) +from .types import ( + ResolveIndeterminateRollbackEvent as ResolveIndeterminateRollbackEvent, +) from .types import ResolvedSkillAuthority as ResolvedSkillAuthority from .types import ResponseBackstopExemptionDef as ResponseBackstopExemptionDef from .types import RestartScope as RestartScope from .types import ResultParser as ResultParser from .types import ResumeSpec as ResumeSpec from .types import RetryReason as RetryReason +from .types import RollbackAdmissionEvent as RollbackAdmissionEvent +from .types import RolloverEpochEvent as RolloverEpochEvent from .types import ServeOverridesSnapshot as ServeOverridesSnapshot from .types import SessionCheckpoint as SessionCheckpoint from .types import SessionEvent as SessionEvent @@ -428,6 +550,8 @@ from .types import SkillSourceRef as SkillSourceRef from .types import SkillVisibilitySpec as SkillVisibilitySpec from .types import SpilledOutput as SpilledOutput from .types import SpillSpec as SpillSpec +from .types import StageHistoryEvent as StageHistoryEvent +from .types import StartGenerationEvent as StartGenerationEvent from .types import StoredSkillSessionContract as StoredSkillSessionContract from .types import StreamParser as StreamParser from .types import SubprocessResult as SubprocessResult @@ -441,10 +565,18 @@ from .types import TestResult as TestResult from .types import TestRunner as TestRunner from .types import TimingLog as TimingLog from .types import TokenFactory as TokenFactory +from .types import TokenizerIdentity as TokenizerIdentity from .types import TokenLog as TokenLog +from .types import ToolCallId as ToolCallId from .types import TraditionManifest as TraditionManifest +from .types import TurnId as TurnId +from .types import ( + UninitializedContextAdmissionState as UninitializedContextAdmissionState, +) from .types import ValidatedAddDir as ValidatedAddDir from .types import ValidatedWorktreePath as ValidatedWorktreePath +from .types import WindowEpochId as WindowEpochId +from .types import WitnessKind as WitnessKind from .types import WorkspaceManager as WorkspaceManager from .types import WriteBehaviorSpec as WriteBehaviorSpec from .types import WriteEvidence as WriteEvidence diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py new file mode 100644 index 000000000..92e10379c --- /dev/null +++ b/src/autoskillit/core/context_admission.py @@ -0,0 +1,1692 @@ +"""Pure reducer and coverage resolver for cumulative context admission.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import assert_never + +from .types._type_context_admission import ( + CONTEXT_ADMISSION_COVERAGE, + AcceptInputEvent, + ActiveContextAdmissionState, + AdmissionBatch, + AdmissionBatchId, + AdmissionBatchRecord, + AdmissionDecision, + AdmissionEffect, + AdmissionOccurrenceRecord, + AdmissionReplay, + AdmissionReservation, + AdmissionReservationId, + AdmissionSequence, + AdmissionTransition, + AdmissionWitness, + AggregateRevision, + AuthorityUnavailableEffect, + AuthorityUnavailableEvent, + ChargeCommittedEffect, + ClosedEpochAudit, + ContextAdmissionEvent, + ContextAdmissionState, + ContextAdmissionValidationError, + DispatchRequestEvent, + EpochClosedEffect, + ExpiredIdempotencyTombstone, + ExpireIdempotencyKeyEvent, + GenerationReconciledEffect, + GenerationReservationId, + GenerationReservationRecord, + GenerationReservationRecordedEffect, + IdempotencyExpiredEffect, + IdempotencyRecord, + MarkGenerationIndeterminateEvent, + MarkIndeterminateEvent, + OccurrenceStateChangedEffect, + OpenEpochEvent, + PrepareBatchEvent, + ProcessedEventRecord, + ProducerCoverageDef, + ProducerSurface, + ProposeOccurrenceEvent, + ProtectedPoolOwnerId, + QuarantineRecordedEffect, + ReconcileGenerationEvent, + ReconciliationEscalationEffect, + ReconciliationQueryRequestedEffect, + ReleaseNonAdmissionEvent, + RequestReconciliationEvent, + ReservationInvalidatedEffect, + ReservationRecordedEffect, + ReservationReleasedEffect, + ReserveRequestEvent, + ResolveIndeterminateAcceptedEvent, + ResolveIndeterminateNonAdmissionEvent, + ResolveIndeterminateRollbackEvent, + RollbackAdmissionEvent, + RolloverEpochEvent, + StageHistoryEvent, + StartGenerationEvent, + UninitializedContextAdmissionState, + UnsupportedContextAdmissionProtocolError, +) +from .types._type_enums import ( + AdmissionDecisionKind, + AdmissionState, + ChargeDomain, + CoverageState, + GenerationState, + ReserveClass, + WitnessKind, +) + +__all__ = [ + "ContextAdmissionValidationError", + "UnsupportedContextAdmissionProtocolError", + "reduce_context_admission", + "replay_context_admission", + "resolve_context_admission_coverage", +] + + +def _effect_coordinates( + state: ContextAdmissionState, + *, + capacity_changed: bool, +) -> tuple[AggregateRevision, AdmissionSequence]: + return ( + AggregateRevision(state.aggregate_revision.value + 1), + AdmissionSequence(state.admission_sequence.value + (1 if capacity_changed else 0)), + ) + + +def _occurrence_effects( + state: ContextAdmissionState, + event: ContextAdmissionEvent, + batch: AdmissionBatch, + previous_state: AdmissionState, + next_state: AdmissionState, + *, + capacity_changed: bool, +) -> tuple[AdmissionEffect, ...]: + revision, sequence = _effect_coordinates( + state, + capacity_changed=capacity_changed, + ) + return tuple( + OccurrenceStateChangedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=occurrence_id, + previous_state=previous_state, + next_state=next_state, + ) + for occurrence_id in batch.occurrence_ids + ) + + +def _accepted_effects( + state: ActiveContextAdmissionState, + event: AcceptInputEvent | ResolveIndeterminateAcceptedEvent, + record: AdmissionBatchRecord, + exact_charge: int, + witness: AdmissionWitness, +) -> tuple[AdmissionEffect, ...]: + reservation = _reservation_for(state, record) + if reservation is None: + return () + quarantined = ( + exact_charge > reservation.reserved_count or exact_charge > state.snapshot.hard_limit + ) + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + ChargeCommittedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=record.batch.batch_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + count=exact_charge, + window_epoch_id=state.snapshot.window_epoch_id, + snapshot_sequence=state.snapshot.snapshot_sequence, + witness_ids=(witness.witness_id,), + ), + *_occurrence_effects( + state, + event, + record.batch, + record.state, + AdmissionState.QUARANTINED if quarantined else AdmissionState.COMMITTED, + capacity_changed=True, + ), + ) + if quarantined: + effects += ( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=record.batch.batch_id, + reason_code="provider_charge_exceeds_reservation", + ), + ) + return effects + + +def _reservation_for( + state: ActiveContextAdmissionState, + record: AdmissionBatchRecord, +) -> AdmissionReservation | None: + if record.reservation_id is None: + return None + return next( + ( + reservation + for reservation in state.reservations + if reservation.reservation_id == record.reservation_id + ), + None, + ) + + +def _capacity( + state: ActiveContextAdmissionState, +) -> tuple[ + int, + int, + dict[tuple[ReserveClass, ProtectedPoolOwnerId], int], +]: + charged_by_pool: dict[tuple[ReserveClass, ProtectedPoolOwnerId], int] = {} + global_charged = 0 + + for record in state.batch_records: + reservation = _reservation_for(state, record) + if record.state in {AdmissionState.COMMITTED, AdmissionState.QUARANTINED}: + charged = record.committed_input_count + elif record.state is AdmissionState.INDETERMINATE: + charged = record.unresolved_input_count + if charged == 0 and reservation is not None: + charged = reservation.reserved_count + elif record.state in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + charged = reservation.reserved_count if reservation is not None else 0 + else: + charged = 0 + global_charged += charged + owner = record.batch.protected_pool_owner_id + if owner is not None: + key = (record.batch.reserve_class, owner) + charged_by_pool[key] = charged_by_pool.get(key, 0) + charged + + for generation in state.generation_reservations: + if generation.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + GenerationState.QUARANTINED, + }: + global_charged += generation.maximum_allowance + owner = generation.protected_pool_owner_id + if owner is not None: + key = (generation.reserve_class, owner) + charged_by_pool[key] = charged_by_pool.get(key, 0) + generation.maximum_allowance + + global_unallocated = max(state.snapshot.remaining_count - global_charged, 0) + pool_available: dict[tuple[ReserveClass, ProtectedPoolOwnerId], int] = {} + for pool in state.protected_pools: + key = (pool.reserve_class, pool.capability_owner_id) + unused = max(pool.injected_count - charged_by_pool.get(key, 0), 0) + pool_available[key] = min(unused, global_unallocated) + ordinary_available = max( + global_unallocated - sum(pool_available.values()), + 0, + ) + return global_unallocated, ordinary_available, pool_available + + +def _decision( + state: ContextAdmissionState, + kind: AdmissionDecisionKind, + reason_code: str, + *, + requested_count: int = 0, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + protected_pool_owner_id: ProtectedPoolOwnerId | None = None, +) -> AdmissionDecision: + if isinstance(state, UninitializedContextAdmissionState): + return AdmissionDecision( + kind=kind, + reason_code=reason_code, + window_epoch_id=None, + snapshot_sequence=None, + requested_count=requested_count, + available_ordinary_count=0, + available_protected_count=0, + ) + _, ordinary_available, pool_available = _capacity(state) + protected_available = 0 + if protected_pool_owner_id is not None: + protected_available = pool_available.get( + (reserve_class, protected_pool_owner_id), + 0, + ) + return AdmissionDecision( + kind=kind, + reason_code=reason_code, + window_epoch_id=state.snapshot.window_epoch_id, + snapshot_sequence=state.snapshot.snapshot_sequence, + requested_count=requested_count, + available_ordinary_count=ordinary_available, + available_protected_count=protected_available, + ) + + +def _reject( + state: ContextAdmissionState, + reason_code: str, + *, + requested_count: int = 0, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + protected_pool_owner_id: ProtectedPoolOwnerId | None = None, +) -> AdmissionTransition: + return AdmissionTransition( + next_state=state, + decision=_decision( + state, + AdmissionDecisionKind.WOULD_REJECT, + reason_code, + requested_count=requested_count, + reserve_class=reserve_class, + protected_pool_owner_id=protected_pool_owner_id, + ), + effects=(), + ) + + +def _publish( + prior_state: ContextAdmissionState, + next_state: ContextAdmissionState, + event: ContextAdmissionEvent, + *, + kind: AdmissionDecisionKind = AdmissionDecisionKind.WOULD_ADMIT, + reason_code: str = "accepted", + requested_count: int = 0, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + protected_pool_owner_id: ProtectedPoolOwnerId | None = None, + capacity_changed: bool = False, + effects: tuple[AdmissionEffect, ...] = (), +) -> AdmissionTransition: + aggregate_revision = type(prior_state.aggregate_revision)( + prior_state.aggregate_revision.value + 1 + ) + admission_sequence = type(prior_state.admission_sequence)( + prior_state.admission_sequence.value + (1 if capacity_changed else 0) + ) + published = replace( + next_state, + aggregate_revision=aggregate_revision, + admission_sequence=admission_sequence, + ) + decision = _decision( + published, + kind, + reason_code, + requested_count=requested_count, + reserve_class=reserve_class, + protected_pool_owner_id=protected_pool_owner_id, + ) + processed = ProcessedEventRecord( + event_id=event.event_id, + event=event, + original_decision=decision, + aggregate_revision=aggregate_revision, + admission_sequence=admission_sequence, + ) + published = replace( + published, + processed_events=published.processed_events + (processed,), + ) + return AdmissionTransition( + next_state=published, + decision=decision, + effects=effects, + ) + + +def _preflight( + state: ContextAdmissionState, + event: ContextAdmissionEvent, +) -> AdmissionTransition | None: + prior = next( + (record for record in state.processed_events if record.event_id == event.event_id), + None, + ) + if prior is not None: + kind = ( + AdmissionDecisionKind.NOOP_IDEMPOTENT + if prior.event == event + else AdmissionDecisionKind.CONFLICT + ) + reason = "event_replay" if prior.event == event else "event_id_conflict" + return AdmissionTransition( + next_state=state, + decision=_decision(state, kind, reason), + effects=(), + ) + if isinstance(event, ReserveRequestEvent) and event.input_reservations: + reservation_key = event.input_reservations[0].key + if any( + tombstone.reservation_key == reservation_key + for tombstone in state.expired_idempotency_tombstones + ): + return AdmissionTransition( + next_state=state, + decision=_decision( + state, + AdmissionDecisionKind.IDEMPOTENCY_EXPIRED, + "idempotency_expired", + ), + effects=(), + ) + idempotency_record = next( + ( + record + for record in state.idempotency_records + if record.reservation_key == reservation_key + ), + None, + ) + if idempotency_record is not None: + original = idempotency_record.original_descriptor + same_intent = ( + original.protocol_version == event.protocol_version + and original.idempotency_namespace == event.idempotency_namespace + and original.batch == event.batch + and original.snapshot_sequence == event.snapshot_sequence + and original.input_reservations == event.input_reservations + and original.generation_reservation == event.generation_reservation + ) + kind = ( + AdmissionDecisionKind.NOOP_IDEMPOTENT + if same_intent + else AdmissionDecisionKind.CONFLICT + ) + return AdmissionTransition( + next_state=state, + decision=_decision( + state, + kind, + ("reservation_key_replay" if same_intent else "reservation_key_conflict"), + ), + effects=(), + ) + if event.expected_aggregate_revision != state.aggregate_revision: + return _reject(state, "stale_revision") + return None + + +def _batch_record( + state: ActiveContextAdmissionState, + batch_id: AdmissionBatchId, +) -> AdmissionBatchRecord | None: + return next( + (record for record in state.batch_records if record.batch.batch_id == batch_id), + None, + ) + + +def _generation_record( + state: ActiveContextAdmissionState, + reservation_id: GenerationReservationId, +) -> GenerationReservationRecord | None: + return next( + ( + record + for record in state.generation_reservations + if record.generation_reservation_id == reservation_id + ), + None, + ) + + +def _replace_batch_record( + state: ActiveContextAdmissionState, + updated: AdmissionBatchRecord, +) -> ActiveContextAdmissionState: + return replace( + state, + batch_records=tuple( + updated if record.batch.batch_id == updated.batch.batch_id else record + for record in state.batch_records + ), + ) + + +def _set_occurrence_state( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + lifecycle_state: AdmissionState, + *, + reservation_id: AdmissionReservationId | None = None, + witness: AdmissionWitness | None = None, + indeterminate_reason_code: str | None = None, + quarantine_reason_code: str | None = None, +) -> ActiveContextAdmissionState: + member_ids = set(batch.occurrence_ids) + records: list[AdmissionOccurrenceRecord] = [] + for record in state.occurrence_records: + if record.occurrence.occurrence_id not in member_ids: + records.append(record) + continue + witness_ids = record.accepted_witness_ids + if witness is not None and witness.witness_id not in witness_ids: + witness_ids += (witness.witness_id,) + records.append( + replace( + record, + state=lifecycle_state, + batch_id=batch.batch_id, + reservation_id=( + reservation_id if reservation_id is not None else record.reservation_id + ), + accepted_witness_ids=witness_ids, + indeterminate_reason_code=indeterminate_reason_code, + quarantine_reason_code=quarantine_reason_code, + ) + ) + return replace(state, occurrence_records=tuple(records)) + + +def _validate_witness( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + witness: AdmissionWitness, + expected_kind: WitnessKind, +) -> bool: + return ( + witness.kind is expected_kind + and witness.window_epoch_id == state.snapshot.window_epoch_id + and witness.window_epoch_number == state.snapshot.window_epoch_number + and witness.snapshot_sequence == state.snapshot.snapshot_sequence + and witness.request_id == batch.request_id + and witness.batch_id == batch.batch_id + and witness.representation_revision == batch.manifest.representation_revision + and witness.occurrence_ids == batch.occurrence_ids + ) + + +def _open_epoch( + state: ContextAdmissionState, + event: OpenEpochEvent, +) -> AdmissionTransition: + if not isinstance(state, UninitializedContextAdmissionState): + return _reject(state, "epoch_already_active") + try: + active = ActiveContextAdmissionState( + protocol_version=state.protocol_version, + aggregate_revision=state.aggregate_revision, + admission_sequence=state.admission_sequence, + snapshot=event.snapshot, + protected_pools=event.protected_pools, + occurrence_records=(), + batch_records=(), + reservations=(), + generation_reservations=(), + processed_events=state.processed_events, + idempotency_records=state.idempotency_records, + expired_idempotency_tombstones=state.expired_idempotency_tombstones, + closed_epochs=state.closed_epochs, + ) + except ContextAdmissionValidationError: + return _reject(state, "invalid_epoch_snapshot") + return _publish(state, active, event) + + +def _propose( + state: ContextAdmissionState, + event: ProposeOccurrenceEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + occurrence = event.occurrence + if ( + occurrence.lineage.window_epoch_id != state.snapshot.window_epoch_id + or occurrence.lineage.window_epoch_number != state.snapshot.window_epoch_number + ): + return _reject(state, "occurrence_epoch_mismatch") + existing = next( + ( + record + for record in state.occurrence_records + if record.occurrence.occurrence_id == occurrence.occurrence_id + ), + None, + ) + if existing is not None: + kind = ( + AdmissionDecisionKind.NOOP_IDEMPOTENT + if existing.occurrence == occurrence + else AdmissionDecisionKind.CONFLICT + ) + reason = ( + "occurrence_replay" if existing.occurrence == occurrence else "occurrence_conflict" + ) + return AdmissionTransition( + next_state=state, + decision=_decision(state, kind, reason), + effects=(), + ) + record = AdmissionOccurrenceRecord( + occurrence=occurrence, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ) + return _publish( + state, + replace(state, occurrence_records=state.occurrence_records + (record,)), + event, + ) + + +def _reserve( + state: ContextAdmissionState, + event: ReserveRequestEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + if event.snapshot_sequence != state.snapshot.snapshot_sequence: + return _reject(state, "snapshot_sequence_mismatch") + if _batch_record(state, event.batch.batch_id) is not None: + return _reject(state, "batch_already_reserved") + member_records = tuple( + record + for record in state.occurrence_records + if record.occurrence.occurrence_id in set(event.batch.occurrence_ids) + ) + if ( + len(member_records) != len(event.batch.occurrence_ids) + or tuple(record.occurrence.occurrence_id for record in member_records) + != event.batch.occurrence_ids + or any(record.state is not AdmissionState.PROPOSED for record in member_records) + or any( + record.occurrence.reserve_class is not event.batch.reserve_class + for record in member_records + ) + ): + return _reject(state, "batch_members_not_all_proposed") + if len(event.input_reservations) != 1: + return _reject(state, "atomic_input_reservation_required") + reservation = event.input_reservations[0] + if ( + reservation.key.batch_id != event.batch.batch_id + or reservation.occurrence_ids != event.batch.occurrence_ids + or reservation.snapshot_sequence != state.snapshot.snapshot_sequence + or reservation.window_epoch_id != state.snapshot.window_epoch_id + or reservation.window_epoch_number != state.snapshot.window_epoch_number + or reservation.reserve_class is not event.batch.reserve_class + or reservation.protected_pool_owner_id != event.batch.protected_pool_owner_id + ): + return _reject(state, "reservation_descriptor_mismatch") + expected_revisions = tuple( + ( + record.occurrence.occurrence_id, + record.occurrence.representation_revision, + ) + for record in member_records + ) + if reservation.key.occurrence_revisions != expected_revisions: + return _reject(state, "reservation_revision_mismatch") + generation = event.generation_reservation + generation_count = generation.maximum_allowance if generation is not None else 0 + if generation is not None and ( + generation.request_id != event.batch.request_id + or generation.window_epoch_id != state.snapshot.window_epoch_id + or generation.window_epoch_number != state.snapshot.window_epoch_number + or generation.snapshot_sequence != state.snapshot.snapshot_sequence + or generation.reserve_class is not event.batch.reserve_class + or generation.protected_pool_owner_id != event.batch.protected_pool_owner_id + ): + return _reject(state, "generation_descriptor_mismatch") + requested = reservation.reserved_count + generation_count + global_available, ordinary_available, pool_available = _capacity(state) + if event.batch.protected_pool_owner_id is None: + available = ordinary_available + else: + available = min( + global_available, + pool_available.get( + ( + event.batch.reserve_class, + event.batch.protected_pool_owner_id, + ), + 0, + ), + ) + if requested > available: + return _reject( + state, + "insufficient_capacity", + requested_count=requested, + reserve_class=event.batch.reserve_class, + protected_pool_owner_id=event.batch.protected_pool_owner_id, + ) + batch_record = AdmissionBatchRecord( + batch=event.batch, + state=AdmissionState.RESERVED, + reservation_id=reservation.reservation_id, + witness_ids=(), + prepared_input_count=None, + committed_input_count=0, + unresolved_input_count=0, + ) + next_state = replace( + state, + batch_records=state.batch_records + (batch_record,), + reservations=state.reservations + event.input_reservations, + generation_reservations=( + state.generation_reservations + + ((generation,) if generation is not None and generation_count > 0 else ()) + ), + ) + reserve_decision = _decision( + next_state, + AdmissionDecisionKind.WOULD_ADMIT, + "accepted", + requested_count=requested, + reserve_class=event.batch.reserve_class, + protected_pool_owner_id=event.batch.protected_pool_owner_id, + ) + next_state = replace( + next_state, + idempotency_records=next_state.idempotency_records + + ( + IdempotencyRecord( + namespace=event.idempotency_namespace, + reservation_key=reservation.key, + original_descriptor=event, + original_reserve_decision=reserve_decision, + owning_event_id=event.event_id, + publication_revision=type(state.aggregate_revision)( + state.aggregate_revision.value + 1 + ), + ), + ), + ) + next_state = _set_occurrence_state( + next_state, + event.batch, + AdmissionState.RESERVED, + reservation_id=reservation.reservation_id, + ) + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + ReservationRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=reservation.reserve_class, + protected_pool_owner_id=reservation.protected_pool_owner_id, + count=reservation.reserved_count, + window_epoch_id=reservation.window_epoch_id, + snapshot_sequence=reservation.snapshot_sequence, + witness_ids=(), + ), + *_occurrence_effects( + state, + event, + event.batch, + AdmissionState.PROPOSED, + AdmissionState.RESERVED, + capacity_changed=True, + ), + ) + if generation is not None and generation_count > 0: + effects += ( + GenerationReservationRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=generation_count, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, + witness_ids=(), + ), + ) + return _publish( + state, + next_state, + event, + requested_count=requested, + reserve_class=event.batch.reserve_class, + protected_pool_owner_id=event.batch.protected_pool_owner_id, + capacity_changed=True, + effects=effects, + ) + + +def _prepare( + state: ContextAdmissionState, + event: PrepareBatchEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + record = _batch_record(state, event.batch_id) + if record is None or record.state is not AdmissionState.RESERVED: + return _reject(state, "illegal_prepare_order") + if event.representation_revision != record.batch.manifest.representation_revision: + return _reject(state, "representation_revision_mismatch") + reservation = _reservation_for(state, record) + if reservation is None or event.proposed_charge != reservation.reserved_count: + return _reject(state, "prepared_charge_mismatch") + updated = replace( + record, + state=AdmissionState.PREPARED, + prepared_input_count=event.proposed_charge, + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + AdmissionState.PREPARED, + ) + return _publish( + state, + next_state, + event, + effects=_occurrence_effects( + state, + event, + record.batch, + AdmissionState.RESERVED, + AdmissionState.PREPARED, + capacity_changed=False, + ), + ) + + +def _stage( + state: ContextAdmissionState, + event: StageHistoryEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + record = _batch_record(state, event.batch_id) + if ( + record is None + or record.state is not AdmissionState.PREPARED + or not _validate_witness( + state, + record.batch, + event.witness, + WitnessKind.HISTORY_STAGED, + ) + ): + return _reject(state, "invalid_history_stage_witness") + updated = replace( + record, + state=AdmissionState.HISTORY_STAGED, + witness_ids=record.witness_ids + (event.witness.witness_id,), + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + AdmissionState.HISTORY_STAGED, + witness=event.witness, + ) + return _publish( + state, + next_state, + event, + effects=_occurrence_effects( + state, + event, + record.batch, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + capacity_changed=False, + ), + ) + + +def _dispatch( + state: ContextAdmissionState, + event: DispatchRequestEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + record = _batch_record(state, event.batch_id) + if ( + record is None + or record.state not in {AdmissionState.PREPARED, AdmissionState.HISTORY_STAGED} + or not _validate_witness( + state, + record.batch, + event.witness, + WitnessKind.REQUEST_INCLUDED, + ) + ): + return _reject(state, "invalid_request_inclusion_witness") + updated = replace( + record, + state=AdmissionState.REQUEST_DISPATCHED, + witness_ids=record.witness_ids + (event.witness.witness_id,), + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + AdmissionState.REQUEST_DISPATCHED, + witness=event.witness, + ) + return _publish( + state, + next_state, + event, + effects=_occurrence_effects( + state, + event, + record.batch, + record.state, + AdmissionState.REQUEST_DISPATCHED, + capacity_changed=False, + ), + ) + + +def _accepted_state( + state: ActiveContextAdmissionState, + record: AdmissionBatchRecord, + witness: AdmissionWitness, + exact_charge: int, +) -> tuple[ActiveContextAdmissionState, AdmissionDecisionKind, str]: + reservation = _reservation_for(state, record) + if reservation is None: + return state, AdmissionDecisionKind.QUARANTINED, "missing_reservation" + quarantined = ( + exact_charge > reservation.reserved_count or exact_charge > state.snapshot.hard_limit + ) + lifecycle = AdmissionState.QUARANTINED if quarantined else AdmissionState.COMMITTED + updated = replace( + record, + state=lifecycle, + witness_ids=record.witness_ids + (witness.witness_id,), + committed_input_count=exact_charge, + unresolved_input_count=0, + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + lifecycle, + witness=witness, + quarantine_reason_code=("provider_charge_exceeds_reservation" if quarantined else None), + ) + return ( + next_state, + (AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT), + "provider_charge_exceeds_reservation" if quarantined else "accepted", + ) + + +def _accept( + state: ContextAdmissionState, + event: AcceptInputEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + record = _batch_record(state, event.batch_id) + if record is None or record.state is not AdmissionState.REQUEST_DISPATCHED: + return _reject(state, "illegal_accept_order") + if not _validate_witness( + state, + record.batch, + event.witness, + WitnessKind.PROVIDER_ACCEPTED, + ): + return _reject(state, "invalid_provider_acceptance_witness") + binding = event.representation_binding_witness + expected_revision = record.batch.manifest.representation_revision + if ( + event.final_manifest_revision != expected_revision + or binding.counted_representation_revision != expected_revision + or binding.dispatched_representation_revision != expected_revision + or binding.final_manifest_revision != expected_revision + or binding.request_id != record.batch.request_id + or binding.batch_id != record.batch.batch_id + ): + return _reject(state, "representation_revision_mismatch") + if event.exact_input_charge < 0: + return _reject(state, "invalid_exact_charge") + next_state, kind, reason = _accepted_state( + state, + record, + event.witness, + event.exact_input_charge, + ) + return _publish( + state, + next_state, + event, + kind=kind, + reason_code=reason, + requested_count=event.exact_input_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=_accepted_effects( + state, + event, + record, + event.exact_input_charge, + event.witness, + ), + ) + + +def _release_or_rollback( + state: ContextAdmissionState, + event: ( + ReleaseNonAdmissionEvent + | RollbackAdmissionEvent + | ResolveIndeterminateNonAdmissionEvent + | ResolveIndeterminateRollbackEvent + ), +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + record = _batch_record(state, event.batch_id) + if record is None: + return _reject(state, "unknown_batch") + is_release = isinstance( + event, + ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, + ) + is_resolution = isinstance( + event, + ResolveIndeterminateNonAdmissionEvent | ResolveIndeterminateRollbackEvent, + ) + expected_witness = WitnessKind.NON_ADMISSION if is_release else WitnessKind.ROLLBACK + allowed_states = ( + {AdmissionState.INDETERMINATE} + if is_resolution + else ( + {AdmissionState.RESERVED, AdmissionState.PREPARED} + if is_release + else { + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + ) + if record.state not in allowed_states or not _validate_witness( + state, + record.batch, + event.witness, + expected_witness, + ): + return _reject(state, "invalid_release_or_rollback_witness") + lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK + updated = replace( + record, + state=lifecycle, + witness_ids=record.witness_ids + (event.witness.witness_id,), + unresolved_input_count=0, + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + lifecycle, + witness=event.witness, + ) + effects: tuple[AdmissionEffect, ...] = _occurrence_effects( + state, + event, + record.batch, + record.state, + lifecycle, + capacity_changed=True, + ) + reservation = _reservation_for(state, record) + if reservation is not None: + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects = ( + ReservationReleasedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=reservation.reserve_class, + protected_pool_owner_id=reservation.protected_pool_owner_id, + count=reservation.reserved_count, + window_epoch_id=reservation.window_epoch_id, + snapshot_sequence=reservation.snapshot_sequence, + witness_ids=(event.witness.witness_id,), + ), + *effects, + ) + return _publish( + state, + next_state, + event, + capacity_changed=True, + effects=effects, + ) + + +def _mark_indeterminate( + state: ContextAdmissionState, + event: MarkIndeterminateEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + record = _batch_record(state, event.batch_id) + if record is None or record.state not in { + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + return _reject(state, "illegal_indeterminate_order") + reservation = _reservation_for(state, record) + unresolved = reservation.reserved_count if reservation is not None else 0 + updated = replace( + record, + state=AdmissionState.INDETERMINATE, + unresolved_input_count=unresolved, + ) + next_state = _replace_batch_record(state, updated) + next_state = _set_occurrence_state( + next_state, + record.batch, + AdmissionState.INDETERMINATE, + indeterminate_reason_code=event.reason_code, + ) + return _publish( + state, + next_state, + event, + effects=_occurrence_effects( + state, + event, + record.batch, + record.state, + AdmissionState.INDETERMINATE, + capacity_changed=False, + ), + ) + + +def _resolve_indeterminate_accepted( + state: ContextAdmissionState, + event: ResolveIndeterminateAcceptedEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + record = _batch_record(state, event.batch_id) + if ( + record is None + or record.state is not AdmissionState.INDETERMINATE + or not _validate_witness( + state, + record.batch, + event.witness, + WitnessKind.PROVIDER_ACCEPTED, + ) + ): + return _reject(state, "invalid_indeterminate_acceptance") + next_state, kind, reason = _accepted_state( + state, + record, + event.witness, + event.exact_charge, + ) + return _publish( + state, + next_state, + event, + kind=kind, + reason_code=reason, + requested_count=event.exact_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=_accepted_effects( + state, + event, + record, + event.exact_charge, + event.witness, + ), + ) + + +def _start_generation( + state: ContextAdmissionState, + event: StartGenerationEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + generation = _generation_record(state, event.generation_reservation_id) + if generation is None or generation.state is not GenerationState.RESERVED: + return _reject(state, "illegal_generation_start") + batch = next( + ( + record.batch + for record in state.batch_records + if record.batch.request_id == generation.request_id + ), + None, + ) + if batch is None or not _validate_witness( + state, + batch, + event.witness, + WitnessKind.REQUEST_INCLUDED, + ): + return _reject(state, "invalid_generation_start_witness") + updated = replace( + generation, + state=GenerationState.STREAMING, + witness_ids=generation.witness_ids + (event.witness.witness_id,), + ) + next_state = replace( + state, + generation_reservations=tuple( + updated + if record.generation_reservation_id == updated.generation_reservation_id + else record + for record in state.generation_reservations + ), + ) + return _publish(state, next_state, event) + + +def _reconcile_generation( + state: ContextAdmissionState, + event: ReconcileGenerationEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + generation = _generation_record(state, event.generation_reservation_id) + if generation is None or generation.state not in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + }: + return _reject(state, "illegal_generation_reconciliation") + if event.output_usage_witness.kind is not WitnessKind.OUTPUT_USAGE: + return _reject(state, "invalid_output_usage_witness") + quarantined = event.exact_output_usage > generation.maximum_allowance + updated = replace( + generation, + state=(GenerationState.QUARANTINED if quarantined else GenerationState.RECONCILED), + exact_terminal_usage=event.exact_output_usage, + witness_ids=generation.witness_ids + (event.output_usage_witness.witness_id,), + ) + next_state = replace( + state, + generation_reservations=tuple( + updated + if record.generation_reservation_id == updated.generation_reservation_id + else record + for record in state.generation_reservations + ), + ) + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + GenerationReconciledEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=event.exact_output_usage, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, + witness_ids=(event.output_usage_witness.witness_id,), + ), + ) + if quarantined: + effects += ( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + reason_code="generation_usage_exceeds_allowance", + ), + ) + return _publish( + state, + next_state, + event, + kind=( + AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT + ), + reason_code=("generation_usage_exceeds_allowance" if quarantined else "accepted"), + capacity_changed=True, + effects=effects, + ) + + +def _mark_generation_indeterminate( + state: ContextAdmissionState, + event: MarkGenerationIndeterminateEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + generation = _generation_record(state, event.generation_reservation_id) + if generation is None or generation.state not in { + GenerationState.RESERVED, + GenerationState.STREAMING, + }: + return _reject(state, "illegal_generation_indeterminate") + updated = replace(generation, state=GenerationState.INDETERMINATE) + next_state = replace( + state, + generation_reservations=tuple( + updated + if record.generation_reservation_id == updated.generation_reservation_id + else record + for record in state.generation_reservations + ), + ) + return _publish(state, next_state, event) + + +def _request_reconciliation( + state: ContextAdmissionState, + event: RequestReconciliationEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + batch = ( + _batch_record(state, event.target_id) + if isinstance(event.target_id, AdmissionBatchId) + else None + ) + generation = ( + _generation_record(state, event.target_id) + if isinstance(event.target_id, GenerationReservationId) + else None + ) + if not ( + ( + batch is not None + and batch.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + or ( + generation is not None + and generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + ): + return _reject(state, "reconciliation_target_not_unresolved") + revision, sequence = _effect_coordinates(state, capacity_changed=False) + effect_type = ( + ReconciliationEscalationEffect + if "deadline" in event.reason_code.casefold() + else ReconciliationQueryRequestedEffect + ) + return _publish( + state, + state, + event, + effects=( + effect_type( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=event.target_id, + reason_code=event.reason_code, + ), + ), + ) + + +def _expire_idempotency( + state: ContextAdmissionState, + event: ExpireIdempotencyKeyEvent, +) -> AdmissionTransition: + record = next( + ( + item + for item in state.idempotency_records + if item.reservation_key == event.reservation_key + ), + None, + ) + if record is None: + return _reject(state, "idempotency_key_not_terminal") + if isinstance(state, ActiveContextAdmissionState): + batch_record = _batch_record( + state, + record.original_descriptor.batch.batch_id, + ) + if batch_record is not None and batch_record.state not in { + AdmissionState.COMMITTED, + AdmissionState.RELEASED, + AdmissionState.ROLLED_BACK, + AdmissionState.INVALIDATED, + AdmissionState.QUARANTINED, + }: + return _reject(state, "idempotency_key_not_terminal") + if event.expiry_witness.kind is not WitnessKind.IDEMPOTENCY_EXPIRY: + return _reject(state, "invalid_expiry_witness") + tombstone = ExpiredIdempotencyTombstone( + namespace=record.namespace, + reservation_key=record.reservation_key, + original_descriptor=record.original_descriptor, + expiry_witness=event.expiry_witness, + original_terminal_decision=record.original_reserve_decision, + ) + next_state = replace( + state, + expired_idempotency_tombstones=(state.expired_idempotency_tombstones + (tombstone,)), + ) + revision, sequence = _effect_coordinates(state, capacity_changed=False) + reservation = record.original_descriptor.input_reservations[0] + return _publish( + state, + next_state, + event, + effects=( + IdempotencyExpiredEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + reservation_key=event.reservation_key, + expiry_witness_id=event.expiry_witness.witness_id, + ), + ), + ) + + +def _rollover( + state: ContextAdmissionState, + event: RolloverEpochEvent, +) -> AdmissionTransition: + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, "epoch_uninitialized") + proof = event.fence_proof + if ( + event.witness.kind is not WitnessKind.EPOCH_ROLLOVER + or proof.old_window_epoch_id != state.snapshot.window_epoch_id + or proof.old_window_epoch_number != state.snapshot.window_epoch_number + or proof.new_window_epoch_id != event.new_snapshot.window_epoch_id + or proof.new_window_epoch_number != event.new_snapshot.window_epoch_number + or proof.new_window_epoch_number <= proof.old_window_epoch_number + or proof.highest_admitted_dispatch_sequence < len(state.reservations) + ): + return _reject(state, "stale_receiver_fence") + terminal_occurrences = tuple( + replace( + record, + state=( + AdmissionState.INVALIDATED + if record.state + in { + AdmissionState.PROPOSED, + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + else record.state + ), + ) + for record in state.occurrence_records + ) + retained_unresolved_count = 0 + for record in state.batch_records: + if record.state in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + }: + reservation = _reservation_for(state, record) + retained_unresolved_count += record.unresolved_input_count or ( + reservation.reserved_count if reservation is not None else 0 + ) + audit = ClosedEpochAudit( + snapshot=state.snapshot, + terminal_occurrence_records=terminal_occurrences, + terminal_reservations=state.reservations, + closure_witness_id=event.witness.witness_id, + fence_proof=proof, + processed_event_tombstones=tuple(record.event_id for record in state.processed_events), + retained_unresolved_count=retained_unresolved_count, + ) + try: + next_state = ActiveContextAdmissionState( + protocol_version=state.protocol_version, + aggregate_revision=state.aggregate_revision, + admission_sequence=state.admission_sequence, + snapshot=event.new_snapshot, + protected_pools=event.protected_pools, + occurrence_records=(), + batch_records=(), + reservations=(), + generation_reservations=(), + processed_events=state.processed_events, + idempotency_records=state.idempotency_records, + expired_idempotency_tombstones=state.expired_idempotency_tombstones, + closed_epochs=state.closed_epochs + (audit,), + ) + except ContextAdmissionValidationError: + return _reject(state, "invalid_rollover_snapshot") + revision, sequence = _effect_coordinates(state, capacity_changed=True) + invalidation_effects = tuple( + ReservationInvalidatedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=reservation.reserve_class, + protected_pool_owner_id=reservation.protected_pool_owner_id, + count=reservation.reserved_count, + window_epoch_id=reservation.window_epoch_id, + snapshot_sequence=reservation.snapshot_sequence, + witness_ids=(event.witness.witness_id, proof.fence_witness_id), + ) + for record in state.batch_records + if record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + for reservation in state.reservations + if reservation.reservation_id == record.reservation_id + ) + occurrence_effects = tuple( + OccurrenceStateChangedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=prior.occurrence.occurrence_id, + previous_state=prior.state, + next_state=terminal.state, + ) + for prior, terminal in zip( + state.occurrence_records, + terminal_occurrences, + strict=True, + ) + if prior.state is not terminal.state + ) + effects: tuple[AdmissionEffect, ...] = ( + *invalidation_effects, + *occurrence_effects, + EpochClosedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=state.snapshot.window_epoch_id, + fence_proof=proof, + ), + ) + return _publish( + state, + next_state, + event, + capacity_changed=True, + effects=effects, + ) + + +def reduce_context_admission( + state: ContextAdmissionState, + event: ContextAdmissionEvent, +) -> AdmissionTransition: + """Apply one protocol event to the complete prior immutable state.""" + preflight = _preflight(state, event) + if preflight is not None: + return preflight + match event: + case OpenEpochEvent(): + return _open_epoch(state, event) + case AuthorityUnavailableEvent(): + kind = ( + AdmissionDecisionKind.UPSTREAM_GATED + if event.authority_state is CoverageState.UPSTREAM_GATED + else AdmissionDecisionKind.WATERMARK_UNAVAILABLE + ) + if isinstance(state, ActiveContextAdmissionState): + revision, sequence = _effect_coordinates( + state, + capacity_changed=False, + ) + return _publish( + state, + state, + event, + kind=kind, + reason_code=event.reason_code, + effects=( + AuthorityUnavailableEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=state.snapshot.window_epoch_id, + reason_code=event.reason_code, + authority_state=event.authority_state, + ), + ), + ) + return AdmissionTransition( + next_state=state, + decision=_decision(state, kind, event.reason_code), + effects=(), + ) + case ProposeOccurrenceEvent(): + return _propose(state, event) + case ReserveRequestEvent(): + return _reserve(state, event) + case PrepareBatchEvent(): + return _prepare(state, event) + case StageHistoryEvent(): + return _stage(state, event) + case DispatchRequestEvent(): + return _dispatch(state, event) + case AcceptInputEvent(): + return _accept(state, event) + case ReleaseNonAdmissionEvent() | RollbackAdmissionEvent(): + return _release_or_rollback(state, event) + case MarkIndeterminateEvent(): + return _mark_indeterminate(state, event) + case ResolveIndeterminateAcceptedEvent(): + return _resolve_indeterminate_accepted(state, event) + case ResolveIndeterminateNonAdmissionEvent() | ResolveIndeterminateRollbackEvent(): + return _release_or_rollback(state, event) + case StartGenerationEvent(): + return _start_generation(state, event) + case ReconcileGenerationEvent(): + return _reconcile_generation(state, event) + case MarkGenerationIndeterminateEvent(): + return _mark_generation_indeterminate(state, event) + case RequestReconciliationEvent(): + return _request_reconciliation(state, event) + case ExpireIdempotencyKeyEvent(): + return _expire_idempotency(state, event) + case RolloverEpochEvent(): + return _rollover(state, event) + case _ as unreachable: + assert_never(unreachable) + + +def replay_context_admission( + initial_state: ContextAdmissionState, + events: tuple[ContextAdmissionEvent, ...], +) -> AdmissionReplay: + """Replay a full stream, feeding each complete next state into the next event.""" + state = initial_state + transitions: list[AdmissionTransition] = [] + for event in events: + transition = reduce_context_admission(state, event) + transitions.append(transition) + state = transition.next_state + return AdmissionReplay(final_state=state, transitions=tuple(transitions)) + + +def resolve_context_admission_coverage( + surface: ProducerSurface, + backend: str, + configuration_mode: str, + source_version: str, + as_of: str, +) -> ProducerCoverageDef: + """Resolve one static coverage row against runtime lineage inputs.""" + row = next( + (item for item in CONTEXT_ADMISSION_COVERAGE if item.surface is surface), + None, + ) + if row is None: + raise ContextAdmissionValidationError("unknown_producer_surface") + evidence = row.evidence[0] + matches = ( + evidence.backend == backend + and evidence.configuration_mode == configuration_mode + and evidence.tested_version == source_version + and evidence.checked_at == as_of + ) + if matches: + return row + return replace( + row, + observation_state=CoverageState.UPSTREAM_GATED, + authority_state=CoverageState.UPSTREAM_GATED, + reason_code="coverage_runtime_mismatch", + ) diff --git a/src/autoskillit/core/types/AGENTS.md b/src/autoskillit/core/types/AGENTS.md index 73e748b61..ae9f5c658 100644 --- a/src/autoskillit/core/types/AGENTS.md +++ b/src/autoskillit/core/types/AGENTS.md @@ -41,6 +41,7 @@ Type re-export hub and all typed building blocks for the autoskillit package (IL | `_type_resume.py` | `ResumeSpec` discriminated union: `NoResume | BareResume | NamedResume` | | `_type_plugin_source.py` | `PluginSource` discriminated union: `DirectInstall | MarketplaceInstall` | | `_type_tradition_manifest.py` | `TraditionManifest`, `LensEntry`, `DialingConfig` frozen dataclasses with `from_dict`/`from_yaml_path` loaders | +| `_type_context_admission.py` | Frozen content-free identities, events, records, effects, states, and coverage definitions for context admission | ## Architecture Notes diff --git a/src/autoskillit/core/types/__init__.py b/src/autoskillit/core/types/__init__.py index 94708e2ed..e97ee409a 100644 --- a/src/autoskillit/core/types/__init__.py +++ b/src/autoskillit/core/types/__init__.py @@ -22,6 +22,8 @@ from ._type_constants_features import __all__ as _constants_features_all from ._type_constants_registries import * # noqa: F401, F403 from ._type_constants_registries import __all__ as _constants_registries_all +from ._type_context_admission import * # noqa: F401, F403 +from ._type_context_admission import __all__ as _context_admission_all from ._type_dispatch_identity import * # noqa: F401, F403 from ._type_dispatch_identity import __all__ as _dispatch_identity_all from ._type_enums import * # noqa: F401, F403 @@ -84,6 +86,7 @@ + _constants_env_all + _constants_features_all + _constants_registries_all + + _context_admission_all + _dispatch_identity_all + _enums_all + _exceptions_all diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py new file mode 100644 index 000000000..4e6f1a602 --- /dev/null +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -0,0 +1,1384 @@ +"""Pure protocol-v1 values for cumulative context admission. + +The contract is intentionally content-free and implementation-independent. It +contains immutable commands, records, decisions, and declarative publication +effects; persistence and producer integration belong to downstream layers. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from enum import StrEnum +from typing import Any, ClassVar, Never, TypeAlias + +from ._type_dispatch_identity import DispatchIdentity +from ._type_enums import ( + AdmissionDecisionKind, + AdmissionState, + ChargeDomain, + CoverageEvidenceKind, + CoverageState, + GenerationState, + MeasurementKind, + ProducerSurface, + ReserveClass, + WitnessKind, +) +from ._type_results import ModelIdentity + +CONTEXT_ADMISSION_PROTOCOL_VERSION = 1 + + +class ContextAdmissionValidationError(ValueError): + """Raised when a protocol value violates a content-free invariant.""" + + +class UnsupportedContextAdmissionProtocolError(ContextAdmissionValidationError): + """Raised when a value uses unsupported protocol semantics.""" + + +_TYPE_REGISTRY: dict[str, type[_ContractValue]] = {} +_ENUM_REGISTRY: dict[str, type[StrEnum]] = { + enum_type.__name__: enum_type + for enum_type in ( + AdmissionDecisionKind, + AdmissionState, + ChargeDomain, + CoverageEvidenceKind, + CoverageState, + GenerationState, + MeasurementKind, + ProducerSurface, + ReserveClass, + WitnessKind, + ) +} + + +def _raise_invalid(reason_code: str) -> Never: + raise ContextAdmissionValidationError(reason_code) + + +def _validate_protocol_version(protocol_version: int) -> None: + if protocol_version != CONTEXT_ADMISSION_PROTOCOL_VERSION: + raise UnsupportedContextAdmissionProtocolError("unsupported_protocol_version") + + +def _validate_non_negative(value: int, reason_code: str) -> None: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + _raise_invalid(reason_code) + + +def _validate_bounded_text( + value: str, + reason_code: str, + *, + maximum: int = 128, + allow_path: bool = False, +) -> None: + if not isinstance(value, str) or not value or len(value) > maximum: + _raise_invalid(reason_code) + lowered = value.casefold() + if ( + lowered.startswith("bearer") + or lowered.startswith("sha256:") + or "\n" in value + or "\r" in value + or (not allow_path and (value.startswith("/") or "\\" in value or "/" in value)) + ): + _raise_invalid(reason_code) + + +def _validate_tuple(value: object, reason_code: str) -> None: + if not isinstance(value, tuple): + _raise_invalid(reason_code) + + +def _encode(value: object) -> object: + if isinstance(value, DispatchIdentity): + return {"dispatch_id": value.dispatch_id} + if isinstance(value, ModelIdentity): + return { + "__type__": "ModelIdentity", + "configured_model": value.configured_model, + "effective_model": value.effective_model, + "profile_name": value.profile_name, + } + if isinstance(value, StrEnum): + return {"__enum__": type(value).__name__, "value": value.value} + if isinstance(value, tuple): + return {"__tuple__": [_encode(item) for item in value]} + if isinstance(value, frozenset): + encoded = [_encode(item) for item in value] + encoded.sort(key=repr) + return {"__frozenset__": encoded} + if isinstance(value, _ContractValue): + result: dict[str, object] = {"__type__": type(value).__name__} + for field_name in value.__dataclass_fields__: + result[field_name] = _encode(getattr(value, field_name)) + return result + if value is None or isinstance(value, bool | int | str): + return value + _raise_invalid("unsupported_serialization_value") + + +def _decode(value: object) -> object: + if isinstance(value, list): + return tuple(_decode(item) for item in value) + if not isinstance(value, Mapping): + return value + if set(value) == {"dispatch_id"}: + dispatch_id = value["dispatch_id"] + if not isinstance(dispatch_id, str): + _raise_invalid("invalid_dispatch_identity") + return DispatchIdentity.from_dispatch_id(dispatch_id) + if "__enum__" in value: + enum_name = value.get("__enum__") + enum_value = value.get("value") + if ( + not isinstance(enum_name, str) + or enum_name not in _ENUM_REGISTRY + or not isinstance(enum_value, str) + ): + _raise_invalid("unknown_serialized_enum") + try: + return _ENUM_REGISTRY[enum_name](enum_value) + except (TypeError, ValueError) as exc: + raise ContextAdmissionValidationError("invalid_serialized_enum") from exc + if "__tuple__" in value: + raw = value["__tuple__"] + if not isinstance(raw, list): + _raise_invalid("invalid_serialized_tuple") + return tuple(_decode(item) for item in raw) + if "__frozenset__" in value: + raw = value["__frozenset__"] + if not isinstance(raw, list): + _raise_invalid("invalid_serialized_frozenset") + return frozenset(_decode(item) for item in raw) + type_name = value.get("__type__") + if type_name == "ModelIdentity": + try: + return ModelIdentity( + configured_model=str(value["configured_model"]), + effective_model=str(value["effective_model"]), + profile_name=str(value["profile_name"]), + ) + except KeyError as exc: + raise ContextAdmissionValidationError("invalid_model_identity") from exc + if not isinstance(type_name, str) or type_name not in _TYPE_REGISTRY: + _raise_invalid("unknown_serialized_contract_type") + contract_type = _TYPE_REGISTRY[type_name] + kwargs = {key: _decode(item) for key, item in value.items() if key != "__type__"} + try: + return contract_type(**kwargs) + except TypeError as exc: + raise ContextAdmissionValidationError("invalid_serialized_contract") from exc + + +class _ContractMeta(type): + def __call__(cls, *args: Any, **kwargs: Any) -> Any: + instance = super().__call__(*args, **kwargs) + _validate_deep_immutability(instance) + return instance + + +class _ContractValue(metaclass=_ContractMeta): + """Canonical content-free serialization shared by all protocol values.""" + + _registry: ClassVar[dict[str, type[_ContractValue]]] = _TYPE_REGISTRY + __dataclass_fields__: ClassVar[dict[str, Any]] + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + _TYPE_REGISTRY[cls.__name__] = cls + + def to_dict(self) -> dict[str, object]: + encoded = _encode(self) + if not isinstance(encoded, dict): + _raise_invalid("invalid_contract_serialization") + encoded.pop("__type__", None) + return encoded + + @classmethod + def from_dict(cls, data: Mapping[str, object]) -> Any: + if not isinstance(data, Mapping): + _raise_invalid("invalid_serialized_contract") + tagged = {"__type__": cls.__name__, **dict(data)} + decoded = _decode(tagged) + if not isinstance(decoded, cls): + _raise_invalid("serialized_contract_type_mismatch") + return decoded + + +def _validate_deep_immutability(value: object) -> None: + if isinstance(value, list | dict | set): + _raise_invalid("mutable_contract_collection") + if isinstance(value, tuple | frozenset): + for item in value: + _validate_deep_immutability(item) + elif isinstance(value, _ContractValue): + for field_name in value.__dataclass_fields__: + _validate_deep_immutability(getattr(value, field_name)) + + +@dataclass(frozen=True, slots=True) +class _OpaqueString(_ContractValue): + value: str + + def __post_init__(self) -> None: + _validate_bounded_text(self.value, "invalid_opaque_identifier") + allowed = "-_.:" + if any( + not (character.isascii() and (character.isalnum() or character in allowed)) + for character in self.value + ): + _raise_invalid("invalid_opaque_identifier") + + +@dataclass(frozen=True, slots=True) +class _NonNegativeInteger(_ContractValue): + value: int + + def __post_init__(self) -> None: + _validate_non_negative(self.value, "invalid_non_negative_integer") + + +@dataclass(frozen=True, slots=True) +class ContextSessionId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AgentInstanceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ContextThreadId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ForkOccurrenceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class TurnId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ProducerInstanceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ToolCallId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ModelItemId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionRequestId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionBatchId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class WindowEpochId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class TokenizerIdentity(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class CanonicalSpanId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionOccurrenceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionAttemptId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class DeliveryOccurrenceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionEventId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionReservationId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionWitnessId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AuthoritySourceId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class GenerationReservationId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class ProtectedPoolOwnerId(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class RepresentationRevision(_OpaqueString): + pass + + +@dataclass(frozen=True, slots=True) +class AggregateRevision(_NonNegativeInteger): + pass + + +@dataclass(frozen=True, slots=True) +class AdmissionSequence(_NonNegativeInteger): + pass + + +@dataclass(frozen=True, slots=True) +class IdempotencyNamespace(_ContractValue): + caller_scope: str + operation_kind: str + + def __post_init__(self) -> None: + _validate_bounded_text(self.caller_scope, "invalid_idempotency_namespace") + _validate_bounded_text(self.operation_kind, "invalid_idempotency_operation") + + +@dataclass(frozen=True, slots=True) +class ContextLineage(_ContractValue): + root_session_id: ContextSessionId + current_session_id: ContextSessionId + root_agent_id: AgentInstanceId + current_agent_id: AgentInstanceId + parent_agent_id: AgentInstanceId | None + root_thread_id: ContextThreadId + current_thread_id: ContextThreadId + parent_thread_id: ContextThreadId | None + fork_occurrence_id: ForkOccurrenceId | None + turn_id: TurnId + producer_surface: ProducerSurface + producer_instance_id: ProducerInstanceId + tool_call_id: ToolCallId | None + model_item_id: ModelItemId | None + dispatch_identity: DispatchIdentity | None = field(repr=False) + attempt_id: AdmissionAttemptId + delivery_occurrence_id: DeliveryOccurrenceId | None + window_epoch_id: WindowEpochId + window_epoch_number: int + + def __post_init__(self) -> None: + _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") + if self.dispatch_identity is not None: + expected = DispatchIdentity.from_dispatch_id(self.dispatch_identity.dispatch_id) + if self.dispatch_identity != expected: + _raise_invalid("invalid_dispatch_identity") + + +@dataclass(frozen=True, slots=True) +class ContextWindowSnapshot(_ContractValue): + protocol_version: int + window_epoch_id: WindowEpochId + window_epoch_number: int + model_identity: ModelIdentity + tokenizer_identity: TokenizerIdentity + snapshot_sequence: int + active_count: int + hard_limit: int + remaining_count: int + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + for value, reason in ( + (self.window_epoch_number, "invalid_window_epoch_number"), + (self.snapshot_sequence, "invalid_snapshot_sequence"), + (self.active_count, "invalid_active_count"), + (self.hard_limit, "invalid_hard_limit"), + (self.remaining_count, "invalid_remaining_count"), + ): + _validate_non_negative(value, reason) + if ( + not self.model_identity.configured_model + or not self.model_identity.effective_model + or self.active_count + self.remaining_count > self.hard_limit + ): + _raise_invalid("invalid_authoritative_snapshot") + + +@dataclass(frozen=True, slots=True) +class CanonicalSpanOwner(_ContractValue): + span_id: CanonicalSpanId + occurrence_id: AdmissionOccurrenceId + + +@dataclass(frozen=True, slots=True) +class CanonicalRepresentationManifest(_ContractValue): + request_id: AdmissionRequestId + representation_revision: RepresentationRevision + span_owners: tuple[CanonicalSpanOwner, ...] + assembler_identity: ProducerInstanceId + assembler_witness_id: AdmissionWitnessId + + def __post_init__(self) -> None: + _validate_tuple(self.span_owners, "invalid_span_owners") + if not self.span_owners: + _raise_invalid("incomplete_representation_manifest") + span_ids = tuple(owner.span_id for owner in self.span_owners) + if len(span_ids) != len(set(span_ids)): + _raise_invalid("overlapping_canonical_span_ownership") + + +@dataclass(frozen=True, slots=True) +class AdmissionOccurrence(_ContractValue): + occurrence_id: AdmissionOccurrenceId + lineage: ContextLineage + reserve_class: ReserveClass + producer_surface: ProducerSurface + predicted_authoritative_maximum: int + representation_revision: RepresentationRevision + owned_span_ids: tuple[CanonicalSpanId, ...] + + def __post_init__(self) -> None: + _validate_non_negative( + self.predicted_authoritative_maximum, + "invalid_predicted_authoritative_maximum", + ) + _validate_tuple(self.owned_span_ids, "invalid_owned_span_ids") + if not self.owned_span_ids or len(self.owned_span_ids) != len(set(self.owned_span_ids)): + _raise_invalid("invalid_owned_span_ids") + if self.producer_surface is not self.lineage.producer_surface: + _raise_invalid("producer_surface_mismatch") + + +@dataclass(frozen=True, slots=True) +class AdmissionBatch(_ContractValue): + batch_id: AdmissionBatchId + request_id: AdmissionRequestId + occurrence_ids: tuple[AdmissionOccurrenceId, ...] + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + manifest: CanonicalRepresentationManifest + + def __post_init__(self) -> None: + _validate_tuple(self.occurrence_ids, "invalid_batch_occurrences") + if not self.occurrence_ids or len(self.occurrence_ids) != len(set(self.occurrence_ids)): + _raise_invalid("invalid_batch_occurrences") + if self.manifest.request_id != self.request_id: + _raise_invalid("batch_manifest_request_mismatch") + manifest_occurrences = tuple(owner.occurrence_id for owner in self.manifest.span_owners) + if set(manifest_occurrences) != set(self.occurrence_ids): + _raise_invalid("incomplete_representation_manifest") + if (self.reserve_class is ReserveClass.ORDINARY) != (self.protected_pool_owner_id is None): + _raise_invalid("invalid_protected_pool_owner") + + +@dataclass(frozen=True, slots=True) +class AdmissionReservationKey(_ContractValue): + idempotency_namespace: IdempotencyNamespace + protocol_version: int + window_epoch_id: WindowEpochId + window_epoch_number: int + batch_id: AdmissionBatchId + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + occurrence_revisions: tuple[tuple[AdmissionOccurrenceId, RepresentationRevision], ...] + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") + _validate_tuple(self.occurrence_revisions, "invalid_occurrence_revisions") + occurrence_ids = tuple(pair[0] for pair in self.occurrence_revisions) + if ( + not occurrence_ids + or len(occurrence_ids) != len(set(occurrence_ids)) + or (self.reserve_class is ReserveClass.ORDINARY) + != (self.protected_pool_owner_id is None) + ): + _raise_invalid("invalid_reservation_key") + + +@dataclass(frozen=True, slots=True) +class AdmissionReservation(_ContractValue): + reservation_id: AdmissionReservationId + key: AdmissionReservationKey + window_epoch_id: WindowEpochId + window_epoch_number: int + snapshot_sequence: int + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + occurrence_ids: tuple[AdmissionOccurrenceId, ...] + reserved_count: int + + def __post_init__(self) -> None: + _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") + _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") + _validate_non_negative(self.reserved_count, "invalid_reserved_count") + _validate_tuple(self.occurrence_ids, "invalid_reservation_occurrences") + if ( + self.window_epoch_id != self.key.window_epoch_id + or self.window_epoch_number != self.key.window_epoch_number + or self.reserve_class is not self.key.reserve_class + or self.protected_pool_owner_id != self.key.protected_pool_owner_id + or self.occurrence_ids + != tuple(occurrence_id for occurrence_id, _ in self.key.occurrence_revisions) + ): + _raise_invalid("reservation_key_mismatch") + + +@dataclass(frozen=True, slots=True) +class AdmissionWitness(_ContractValue): + witness_id: AdmissionWitnessId + kind: WitnessKind + window_epoch_id: WindowEpochId + window_epoch_number: int + snapshot_sequence: int + request_id: AdmissionRequestId + batch_id: AdmissionBatchId + representation_revision: RepresentationRevision + occurrence_ids: tuple[AdmissionOccurrenceId, ...] + authority_source_id: AuthoritySourceId + + def __post_init__(self) -> None: + _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") + _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") + _validate_tuple(self.occurrence_ids, "invalid_witness_occurrences") + + +@dataclass(frozen=True, slots=True) +class RepresentationBindingWitness(_ContractValue): + counted_representation_revision: RepresentationRevision + dispatched_representation_revision: RepresentationRevision + final_manifest_revision: RepresentationRevision + request_id: AdmissionRequestId + batch_id: AdmissionBatchId + authority_source_id: AuthoritySourceId + + +@dataclass(frozen=True, slots=True) +class EpochFenceProof(_ContractValue): + old_window_epoch_id: WindowEpochId + old_window_epoch_number: int + new_window_epoch_id: WindowEpochId + new_window_epoch_number: int + receiver_authority_source_id: AuthoritySourceId + fence_witness_id: AdmissionWitnessId + highest_admitted_dispatch_sequence: int + + def __post_init__(self) -> None: + _validate_non_negative(self.old_window_epoch_number, "invalid_window_epoch_number") + _validate_non_negative(self.new_window_epoch_number, "invalid_window_epoch_number") + _validate_non_negative( + self.highest_admitted_dispatch_sequence, + "invalid_dispatch_sequence", + ) + + +@dataclass(frozen=True, slots=True) +class ProtectedPoolSpec(_ContractValue): + reserve_class: ReserveClass + capability_owner_id: ProtectedPoolOwnerId + injected_count: int + priority: int + required_release_witness_kind: WitnessKind + + def __post_init__(self) -> None: + _validate_non_negative(self.injected_count, "invalid_protected_pool_count") + _validate_non_negative(self.priority, "invalid_protected_pool_priority") + if self.reserve_class is ReserveClass.ORDINARY: + _raise_invalid("ordinary_pool_forbidden") + + +@dataclass(frozen=True, slots=True) +class AdmissionDecision(_ContractValue): + kind: AdmissionDecisionKind + reason_code: str + window_epoch_id: WindowEpochId | None + snapshot_sequence: int | None + requested_count: int + available_ordinary_count: int + available_protected_count: int + + def __post_init__(self) -> None: + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + for value in ( + self.requested_count, + self.available_ordinary_count, + self.available_protected_count, + ): + _validate_non_negative(value, "invalid_decision_count") + if self.snapshot_sequence is not None: + _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") + + +@dataclass(frozen=True, slots=True) +class AdmissionOccurrenceRecord(_ContractValue): + occurrence: AdmissionOccurrence + state: AdmissionState + batch_id: AdmissionBatchId | None + reservation_id: AdmissionReservationId | None + accepted_witness_ids: tuple[AdmissionWitnessId, ...] + indeterminate_reason_code: str | None + quarantine_reason_code: str | None + + +@dataclass(frozen=True, slots=True) +class AdmissionBatchRecord(_ContractValue): + batch: AdmissionBatch + state: AdmissionState + reservation_id: AdmissionReservationId | None + witness_ids: tuple[AdmissionWitnessId, ...] + prepared_input_count: int | None + committed_input_count: int + unresolved_input_count: int + + def __post_init__(self) -> None: + if self.prepared_input_count is not None: + _validate_non_negative(self.prepared_input_count, "invalid_prepared_count") + _validate_non_negative(self.committed_input_count, "invalid_committed_count") + _validate_non_negative(self.unresolved_input_count, "invalid_unresolved_count") + + +@dataclass(frozen=True, slots=True) +class GenerationReservationRecord(_ContractValue): + generation_reservation_id: GenerationReservationId + request_id: AdmissionRequestId + response_id: ModelItemId + window_epoch_id: WindowEpochId + window_epoch_number: int + snapshot_sequence: int + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + maximum_allowance: int + state: GenerationState + exact_terminal_usage: int | None + witness_ids: tuple[AdmissionWitnessId, ...] + + def __post_init__(self) -> None: + for value in ( + self.window_epoch_number, + self.snapshot_sequence, + self.maximum_allowance, + ): + _validate_non_negative(value, "invalid_generation_reservation") + if self.exact_terminal_usage is not None: + _validate_non_negative(self.exact_terminal_usage, "invalid_generation_usage") + if (self.reserve_class is ReserveClass.ORDINARY) != (self.protected_pool_owner_id is None): + _raise_invalid("invalid_generation_owner") + + +@dataclass(frozen=True, slots=True) +class ExpiredIdempotencyTombstone(_ContractValue): + namespace: IdempotencyNamespace + reservation_key: AdmissionReservationKey + original_descriptor: ReserveRequestEvent + expiry_witness: AdmissionWitness + original_terminal_decision: AdmissionDecision + + +@dataclass(frozen=True, slots=True) +class ClosedEpochAudit(_ContractValue): + snapshot: ContextWindowSnapshot + terminal_occurrence_records: tuple[AdmissionOccurrenceRecord, ...] + terminal_reservations: tuple[AdmissionReservation, ...] + closure_witness_id: AdmissionWitnessId + fence_proof: EpochFenceProof | None + processed_event_tombstones: tuple[AdmissionEventId, ...] + retained_unresolved_count: int + + def __post_init__(self) -> None: + _validate_non_negative(self.retained_unresolved_count, "invalid_retained_charge") + + +@dataclass(frozen=True, slots=True) +class CoverageEvidence(_ContractValue): + claim_id: str + kind: CoverageEvidenceKind + backend: str + configuration_mode: str + verifier: str + source_locator: str + tested_version: str + tested_revision: str + checked_at: str + freshness_policy: str + + def __post_init__(self) -> None: + for value, reason, maximum in ( + (self.claim_id, "invalid_claim_id", 96), + (self.backend, "invalid_evidence_backend", 64), + (self.configuration_mode, "invalid_configuration_mode", 64), + (self.verifier, "invalid_evidence_verifier", 64), + (self.source_locator, "invalid_source_locator", 256), + (self.tested_version, "invalid_tested_version", 64), + (self.tested_revision, "invalid_tested_revision", 96), + (self.checked_at, "invalid_checked_at", 32), + (self.freshness_policy, "invalid_freshness_policy", 128), + ): + _validate_bounded_text( + value, + reason, + maximum=maximum, + allow_path=reason == "invalid_source_locator", + ) + + +@dataclass(frozen=True, slots=True) +class ProducerCoverageDef(_ContractValue): + surface: ProducerSurface + control_point_owner: str + observation_state: CoverageState + authority_state: CoverageState + evidence: tuple[CoverageEvidence, ...] + reason_code: str + + def __post_init__(self) -> None: + _validate_bounded_text( + self.control_point_owner, + "invalid_control_point_owner", + maximum=96, + ) + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_tuple(self.evidence, "invalid_coverage_evidence") + if not self.evidence: + _raise_invalid("coverage_evidence_required") + primary = tuple( + item for item in self.evidence if item.kind is not CoverageEvidenceKind.INFERENCE + ) + if ( + self.observation_state is CoverageState.VERIFIED + or self.authority_state is CoverageState.VERIFIED + ) and not primary: + _raise_invalid("verified_coverage_requires_primary_evidence") + + +@dataclass(frozen=True, slots=True) +class _AdmissionEventBase(_ContractValue): + event_id: AdmissionEventId + protocol_version: int + idempotency_namespace: IdempotencyNamespace + expected_aggregate_revision: AggregateRevision + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + + +@dataclass(frozen=True, slots=True) +class OpenEpochEvent(_AdmissionEventBase): + snapshot: ContextWindowSnapshot + protected_pools: tuple[ProtectedPoolSpec, ...] + + +@dataclass(frozen=True, slots=True) +class AuthorityUnavailableEvent(_AdmissionEventBase): + reason_code: str + authority_state: CoverageState + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + if self.authority_state is CoverageState.VERIFIED: + _raise_invalid("invalid_unavailable_authority_state") + + +@dataclass(frozen=True, slots=True) +class ProposeOccurrenceEvent(_AdmissionEventBase): + occurrence: AdmissionOccurrence + + +@dataclass(frozen=True, slots=True) +class ReserveRequestEvent(_AdmissionEventBase): + batch: AdmissionBatch + snapshot_sequence: int + input_reservations: tuple[AdmissionReservation, ...] + generation_reservation: GenerationReservationRecord | None + + +@dataclass(frozen=True, slots=True) +class PrepareBatchEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + representation_revision: RepresentationRevision + proposed_charge: int + measurement_kind: MeasurementKind + authority_source_id: AuthoritySourceId + + +@dataclass(frozen=True, slots=True) +class StageHistoryEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class DispatchRequestEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class AcceptInputEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + final_manifest_revision: RepresentationRevision + exact_input_charge: int + measurement_kind: MeasurementKind + authority_source_id: AuthoritySourceId + representation_binding_witness: RepresentationBindingWitness + + +@dataclass(frozen=True, slots=True) +class ReleaseNonAdmissionEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class RollbackAdmissionEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class MarkIndeterminateEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + reason_code: str + + +@dataclass(frozen=True, slots=True) +class ResolveIndeterminateAcceptedEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + exact_charge: int + measurement_kind: MeasurementKind + authority_source_id: AuthoritySourceId + + +@dataclass(frozen=True, slots=True) +class ResolveIndeterminateNonAdmissionEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class ResolveIndeterminateRollbackEvent(_AdmissionEventBase): + batch_id: AdmissionBatchId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class StartGenerationEvent(_AdmissionEventBase): + generation_reservation_id: GenerationReservationId + witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class ReconcileGenerationEvent(_AdmissionEventBase): + generation_reservation_id: GenerationReservationId + output_usage_witness: AdmissionWitness + exact_output_usage: int + + +@dataclass(frozen=True, slots=True) +class MarkGenerationIndeterminateEvent(_AdmissionEventBase): + generation_reservation_id: GenerationReservationId + reason_code: str + + +@dataclass(frozen=True, slots=True) +class RequestReconciliationEvent(_AdmissionEventBase): + target_id: AdmissionBatchId | GenerationReservationId + reason_code: str + + +@dataclass(frozen=True, slots=True) +class ExpireIdempotencyKeyEvent(_AdmissionEventBase): + reservation_key: AdmissionReservationKey + expiry_witness: AdmissionWitness + + +@dataclass(frozen=True, slots=True) +class RolloverEpochEvent(_AdmissionEventBase): + witness: AdmissionWitness + fence_proof: EpochFenceProof + new_snapshot: ContextWindowSnapshot + protected_pools: tuple[ProtectedPoolSpec, ...] + + +ContextAdmissionEvent: TypeAlias = ( + OpenEpochEvent + | AuthorityUnavailableEvent + | ProposeOccurrenceEvent + | ReserveRequestEvent + | PrepareBatchEvent + | StageHistoryEvent + | DispatchRequestEvent + | AcceptInputEvent + | ReleaseNonAdmissionEvent + | RollbackAdmissionEvent + | MarkIndeterminateEvent + | ResolveIndeterminateAcceptedEvent + | ResolveIndeterminateNonAdmissionEvent + | ResolveIndeterminateRollbackEvent + | StartGenerationEvent + | ReconcileGenerationEvent + | MarkGenerationIndeterminateEvent + | RequestReconciliationEvent + | ExpireIdempotencyKeyEvent + | RolloverEpochEvent +) + + +@dataclass(frozen=True, slots=True) +class _AdmissionEffectBase(_ContractValue): + source_event_id: AdmissionEventId + resulting_aggregate_revision: AggregateRevision + resulting_admission_sequence: AdmissionSequence + target_id: ( + AdmissionOccurrenceId + | AdmissionBatchId + | AdmissionReservationId + | GenerationReservationId + | WindowEpochId + ) + + +@dataclass(frozen=True, slots=True) +class ReservationRecordedEffect(_AdmissionEffectBase): + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + +@dataclass(frozen=True, slots=True) +class ReservationReleasedEffect(_AdmissionEffectBase): + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + +@dataclass(frozen=True, slots=True) +class OccurrenceStateChangedEffect(_AdmissionEffectBase): + previous_state: AdmissionState + next_state: AdmissionState + + +@dataclass(frozen=True, slots=True) +class ChargeCommittedEffect(_AdmissionEffectBase): + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + +@dataclass(frozen=True, slots=True) +class GenerationReservationRecordedEffect(_AdmissionEffectBase): + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + +@dataclass(frozen=True, slots=True) +class GenerationReconciledEffect(_AdmissionEffectBase): + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + +@dataclass(frozen=True, slots=True) +class ReconciliationQueryRequestedEffect(_AdmissionEffectBase): + reason_code: str + + +@dataclass(frozen=True, slots=True) +class ReconciliationEscalationEffect(_AdmissionEffectBase): + reason_code: str + + +@dataclass(frozen=True, slots=True) +class ConflictRejectedEffect(_AdmissionEffectBase): + reason_code: str + + +@dataclass(frozen=True, slots=True) +class IdempotencyExpiredEffect(_AdmissionEffectBase): + reservation_key: AdmissionReservationKey + expiry_witness_id: AdmissionWitnessId + + +@dataclass(frozen=True, slots=True) +class ReservationInvalidatedEffect(_AdmissionEffectBase): + charge_domain: ChargeDomain + reserve_class: ReserveClass + protected_pool_owner_id: ProtectedPoolOwnerId | None + count: int + window_epoch_id: WindowEpochId + snapshot_sequence: int + witness_ids: tuple[AdmissionWitnessId, ...] + + +@dataclass(frozen=True, slots=True) +class EpochClosedEffect(_AdmissionEffectBase): + fence_proof: EpochFenceProof + + +@dataclass(frozen=True, slots=True) +class QuarantineRecordedEffect(_AdmissionEffectBase): + reason_code: str + + +@dataclass(frozen=True, slots=True) +class AuthorityUnavailableEffect(_AdmissionEffectBase): + reason_code: str + authority_state: CoverageState + + +AdmissionEffect: TypeAlias = ( + ReservationRecordedEffect + | ReservationReleasedEffect + | OccurrenceStateChangedEffect + | ChargeCommittedEffect + | GenerationReservationRecordedEffect + | GenerationReconciledEffect + | ReconciliationQueryRequestedEffect + | ReconciliationEscalationEffect + | ConflictRejectedEffect + | IdempotencyExpiredEffect + | ReservationInvalidatedEffect + | EpochClosedEffect + | QuarantineRecordedEffect + | AuthorityUnavailableEffect +) + + +@dataclass(frozen=True, slots=True) +class ProcessedEventRecord(_ContractValue): + event_id: AdmissionEventId + event: ContextAdmissionEvent + original_decision: AdmissionDecision + aggregate_revision: AggregateRevision + admission_sequence: AdmissionSequence + + +@dataclass(frozen=True, slots=True) +class IdempotencyRecord(_ContractValue): + namespace: IdempotencyNamespace + reservation_key: AdmissionReservationKey + original_descriptor: ReserveRequestEvent + original_reserve_decision: AdmissionDecision + owning_event_id: AdmissionEventId + publication_revision: AggregateRevision + + +@dataclass(frozen=True, slots=True) +class UninitializedContextAdmissionState(_ContractValue): + protocol_version: int + aggregate_revision: AggregateRevision + admission_sequence: AdmissionSequence + processed_events: tuple[ProcessedEventRecord, ...] + idempotency_records: tuple[IdempotencyRecord, ...] + expired_idempotency_tombstones: tuple[ExpiredIdempotencyTombstone, ...] + closed_epochs: tuple[ClosedEpochAudit, ...] + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + + +@dataclass(frozen=True, slots=True) +class ActiveContextAdmissionState(_ContractValue): + protocol_version: int + aggregate_revision: AggregateRevision + admission_sequence: AdmissionSequence + snapshot: ContextWindowSnapshot + protected_pools: tuple[ProtectedPoolSpec, ...] + occurrence_records: tuple[AdmissionOccurrenceRecord, ...] + batch_records: tuple[AdmissionBatchRecord, ...] + reservations: tuple[AdmissionReservation, ...] + generation_reservations: tuple[GenerationReservationRecord, ...] + processed_events: tuple[ProcessedEventRecord, ...] + idempotency_records: tuple[IdempotencyRecord, ...] + expired_idempotency_tombstones: tuple[ExpiredIdempotencyTombstone, ...] + closed_epochs: tuple[ClosedEpochAudit, ...] + + def __post_init__(self) -> None: + _validate_protocol_version(self.protocol_version) + if self.snapshot.protocol_version != self.protocol_version: + _raise_invalid("state_snapshot_protocol_mismatch") + pools = tuple( + (pool.reserve_class, pool.capability_owner_id) for pool in self.protected_pools + ) + if len(pools) != len(set(pools)): + _raise_invalid("duplicate_protected_pool") + if sum(pool.injected_count for pool in self.protected_pools) > ( + self.snapshot.remaining_count + ): + _raise_invalid("protected_pool_capacity_exceeded") + + +ContextAdmissionState: TypeAlias = UninitializedContextAdmissionState | ActiveContextAdmissionState + + +@dataclass(frozen=True, slots=True) +class AdmissionTransition(_ContractValue): + next_state: ContextAdmissionState + decision: AdmissionDecision + effects: tuple[AdmissionEffect, ...] + + +@dataclass(frozen=True, slots=True) +class AdmissionReplay(_ContractValue): + final_state: ContextAdmissionState + transitions: tuple[AdmissionTransition, ...] + + +_VERIFIED_SURFACES = frozenset( + { + ProducerSurface.NATIVE_SHELL, + ProducerSurface.AUTOSKILLIT_MCP, + ProducerSurface.AUTOSKILLIT_LOCAL_FUNCTION, + ProducerSurface.HOOK_FEEDBACK, + ProducerSurface.HEADLESS_CHILD_PROMPT, + ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY, + } +) +_UNOBSERVABLE_SURFACES = frozenset( + { + ProducerSurface.CLIENT_PROVIDER_RETRIEVAL, + ProducerSurface.OTHER_CONTEXT_INJECTION, + } +) +_CONTROL_POINT_OWNERS = { + ProducerSurface.NATIVE_SHELL: "shell_capture_hook", + ProducerSurface.AUTOSKILLIT_MCP: "track_response_size", + ProducerSurface.AUTOSKILLIT_LOCAL_FUNCTION: "local_function_dispatch", + ProducerSurface.HOOK_FEEDBACK: "hook_registry", + ProducerSurface.HEADLESS_CHILD_PROMPT: "headless_prompt_builder", + ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY: "child_delivery_receipt", + ProducerSurface.EXTERNAL_MCP: "fastmcp_client", + ProducerSurface.MCP_RESOURCE: "fastmcp_client", + ProducerSurface.COMPACTION_MODEL_WINDOW_TRANSITION: "compaction_receiver", +} +_LOCAL_SOURCE_LOCATORS = { + ProducerSurface.NATIVE_SHELL: "src/autoskillit/hooks/shell_capture_hook.py", + ProducerSurface.AUTOSKILLIT_MCP: "src/autoskillit/server/_notify.py", + ProducerSurface.AUTOSKILLIT_LOCAL_FUNCTION: ( + "src/autoskillit/execution/headless/_headless_helpers.py" + ), + ProducerSurface.HOOK_FEEDBACK: "src/autoskillit/hook_registry.py", + ProducerSurface.HEADLESS_CHILD_PROMPT: ( + "src/autoskillit/execution/headless/_headless_helpers.py" + ), + ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY: ("src/autoskillit/server/_recipe_delivery.py"), +} + + +def _coverage_row(surface: ProducerSurface) -> ProducerCoverageDef: + if surface in _VERIFIED_SURFACES: + observation_state = CoverageState.VERIFIED + evidence_kind = CoverageEvidenceKind.AUTOSKILLIT_SOURCE + backend = "autoskillit" + verifier = "source_inspection" + locator = _LOCAL_SOURCE_LOCATORS[surface] + version = "0.10.890" + revision = "ac8f653a00d2" + elif surface in _UNOBSERVABLE_SURFACES: + observation_state = CoverageState.UPSTREAM_GATED + evidence_kind = CoverageEvidenceKind.INFERENCE + backend = "codex" + verifier = "source_gap_analysis" + locator = "docs/decisions/0007-context-admission.md" + version = "0.145.0" + revision = "25af12f7e61572b0bc18ddb1008be543b91519b0" + else: + observation_state = CoverageState.PARTIAL + evidence_kind = CoverageEvidenceKind.CODEX_SOURCE + backend = "codex" + verifier = "source_inspection" + locator = "codex-rs/core/src/context_manager/history.rs" + version = "0.145.0" + revision = "25af12f7e61572b0bc18ddb1008be543b91519b0" + owner = _CONTROL_POINT_OWNERS.get(surface) + if owner is None: + if surface in { + ProducerSurface.UNIFIED_EXEC_AND_WRITE_STDIN, + ProducerSurface.APPLY_PATCH, + ProducerSurface.OTHER_LOCAL_FUNCTION, + ProducerSurface.CLIENT_PROVIDER_RETRIEVAL, + ProducerSurface.CODE_MODE_AGGREGATE, + ProducerSurface.HOSTED_SPECIALIZED_TOOL, + }: + owner = "codex_host" + else: + owner = "final_request_assembler" + claim_id = f"COV-{surface.name.replace('_', '-')}" + evidence = CoverageEvidence( + claim_id=claim_id, + kind=evidence_kind, + backend=backend, + configuration_mode="default", + verifier=verifier, + source_locator=locator, + tested_version=version, + tested_revision=revision, + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + return ProducerCoverageDef( + surface=surface, + control_point_owner=owner, + observation_state=observation_state, + authority_state=CoverageState.UPSTREAM_GATED, + evidence=(evidence,), + reason_code="authoritative_watermark_unavailable", + ) + + +CONTEXT_ADMISSION_COVERAGE = tuple(_coverage_row(surface) for surface in ProducerSurface) + + +__all__ = [ + "CONTEXT_ADMISSION_PROTOCOL_VERSION", + "CONTEXT_ADMISSION_COVERAGE", + "ContextAdmissionValidationError", + "UnsupportedContextAdmissionProtocolError", + "ContextSessionId", + "AgentInstanceId", + "ContextThreadId", + "ForkOccurrenceId", + "TurnId", + "ProducerInstanceId", + "ToolCallId", + "ModelItemId", + "AdmissionRequestId", + "AdmissionBatchId", + "WindowEpochId", + "TokenizerIdentity", + "CanonicalSpanId", + "AdmissionOccurrenceId", + "AdmissionAttemptId", + "DeliveryOccurrenceId", + "AdmissionEventId", + "AdmissionReservationId", + "AdmissionWitnessId", + "AuthoritySourceId", + "GenerationReservationId", + "ProtectedPoolOwnerId", + "RepresentationRevision", + "AggregateRevision", + "AdmissionSequence", + "IdempotencyNamespace", + "ContextLineage", + "ContextWindowSnapshot", + "CanonicalSpanOwner", + "CanonicalRepresentationManifest", + "AdmissionOccurrence", + "AdmissionBatch", + "AdmissionReservationKey", + "AdmissionReservation", + "AdmissionWitness", + "RepresentationBindingWitness", + "EpochFenceProof", + "ProtectedPoolSpec", + "AdmissionDecision", + "AdmissionOccurrenceRecord", + "AdmissionBatchRecord", + "GenerationReservationRecord", + "ProcessedEventRecord", + "IdempotencyRecord", + "ExpiredIdempotencyTombstone", + "ClosedEpochAudit", + "CoverageEvidence", + "ProducerCoverageDef", + "OpenEpochEvent", + "AuthorityUnavailableEvent", + "ProposeOccurrenceEvent", + "ReserveRequestEvent", + "PrepareBatchEvent", + "StageHistoryEvent", + "DispatchRequestEvent", + "AcceptInputEvent", + "ReleaseNonAdmissionEvent", + "RollbackAdmissionEvent", + "MarkIndeterminateEvent", + "ResolveIndeterminateAcceptedEvent", + "ResolveIndeterminateNonAdmissionEvent", + "ResolveIndeterminateRollbackEvent", + "StartGenerationEvent", + "ReconcileGenerationEvent", + "MarkGenerationIndeterminateEvent", + "RequestReconciliationEvent", + "ExpireIdempotencyKeyEvent", + "RolloverEpochEvent", + "ContextAdmissionEvent", + "ReservationRecordedEffect", + "ReservationReleasedEffect", + "OccurrenceStateChangedEffect", + "ChargeCommittedEffect", + "GenerationReservationRecordedEffect", + "GenerationReconciledEffect", + "ReconciliationQueryRequestedEffect", + "ReconciliationEscalationEffect", + "ConflictRejectedEffect", + "IdempotencyExpiredEffect", + "ReservationInvalidatedEffect", + "EpochClosedEffect", + "QuarantineRecordedEffect", + "AuthorityUnavailableEffect", + "AdmissionEffect", + "UninitializedContextAdmissionState", + "ActiveContextAdmissionState", + "ContextAdmissionState", + "AdmissionTransition", + "AdmissionReplay", +] diff --git a/src/autoskillit/core/types/_type_enums.py b/src/autoskillit/core/types/_type_enums.py index 3f088db8e..fdd9cb248 100644 --- a/src/autoskillit/core/types/_type_enums.py +++ b/src/autoskillit/core/types/_type_enums.py @@ -41,6 +41,16 @@ "CodexEventType", "CodexItemType", "SynthesisStrategy", + "AdmissionState", + "AdmissionDecisionKind", + "ChargeDomain", + "GenerationState", + "MeasurementKind", + "CoverageState", + "CoverageEvidenceKind", + "ReserveClass", + "WitnessKind", + "ProducerSurface", ] @@ -618,3 +628,139 @@ class SynthesisStrategy(StrEnum): ELECTRE_III = "electre_iii" DEX = "dex" CUSTOM = "custom" + + +@unique +class AdmissionState(StrEnum): + """Lifecycle states for one immutable context-admission occurrence.""" + + PROPOSED = "proposed" + RESERVED = "reserved" + PREPARED = "prepared" + HISTORY_STAGED = "history_staged" + REQUEST_DISPATCHED = "request_dispatched" + COMMITTED = "committed" + RELEASED = "released" + ROLLED_BACK = "rolled_back" + INVALIDATED = "invalidated" + INDETERMINATE = "indeterminate" + QUARANTINED = "quarantined" + + +@unique +class AdmissionDecisionKind(StrEnum): + """Closed decision vocabulary returned by the protocol-v1 reducer.""" + + WOULD_ADMIT = "would_admit" + WOULD_REJECT = "would_reject" + WATERMARK_UNAVAILABLE = "watermark_unavailable" + UPSTREAM_GATED = "upstream_gated" + NOOP_IDEMPOTENT = "noop_idempotent" + CONFLICT = "conflict" + IDEMPOTENCY_EXPIRED = "idempotency_expired" + QUARANTINED = "quarantined" + + +@unique +class ChargeDomain(StrEnum): + """Capacity domains kept separate by the admission contract.""" + + INPUT_CONTEXT = "input_context" + OUTPUT_GENERATION = "output_generation" + + +@unique +class GenerationState(StrEnum): + """Lifecycle of a generated-output allowance.""" + + RESERVED = "reserved" + STREAMING = "streaming" + RECONCILED = "reconciled" + INDETERMINATE = "indeterminate" + QUARANTINED = "quarantined" + + +@unique +class MeasurementKind(StrEnum): + """Authority level of a count supplied to the pure reducer.""" + + PROVIDER_EXACT = "provider_exact" + TOKENIZER_EXACT = "tokenizer_exact" + HOST_ESTIMATE = "host_estimate" + BYTE_EMERGENCY = "byte_emergency" + + +@unique +class CoverageState(StrEnum): + """Evidence-backed observation or authority coverage state.""" + + VERIFIED = "verified" + PARTIAL = "partial" + UPSTREAM_GATED = "upstream_gated" + + +@unique +class CoverageEvidenceKind(StrEnum): + """Primary and inference evidence kinds accepted by the coverage registry.""" + + AUTOSKILLIT_SOURCE = "autoskillit_source" + CODEX_SOURCE = "codex_source" + CODEX_OFFICIAL_DOC = "codex_official_doc" + CODEX_RUNTIME_PROBE = "codex_runtime_probe" + INFERENCE = "inference" + + +@unique +class ReserveClass(StrEnum): + """Capability-scoped context reserve classes.""" + + ORDINARY = "ordinary" + SYNTHESIS = "synthesis" + FINAL_RESPONSE = "final_response" + + +@unique +class WitnessKind(StrEnum): + """Closed vocabulary of authoritative admission witnesses.""" + + EPOCH_SNAPSHOT = "epoch_snapshot" + INPUT_COUNTED = "input_counted" + HISTORY_STAGED = "history_staged" + REPRESENTATION_BOUND = "representation_bound" + REQUEST_INCLUDED = "request_included" + PROVIDER_ACCEPTED = "provider_accepted" + OUTPUT_USAGE = "output_usage" + TRUNCATION = "truncation" + NON_ADMISSION = "non_admission" + ROLLBACK = "rollback" + RECONCILIATION = "reconciliation" + IDEMPOTENCY_EXPIRY = "idempotency_expiry" + EPOCH_FENCE = "epoch_fence" + EPOCH_ROLLOVER = "epoch_rollover" + + +@unique +class ProducerSurface(StrEnum): + """Every model-visible producer covered by protocol version 1.""" + + NATIVE_SHELL = "native_shell" + UNIFIED_EXEC_AND_WRITE_STDIN = "unified_exec_and_write_stdin" + APPLY_PATCH = "apply_patch" + AUTOSKILLIT_MCP = "autoskillit_mcp" + EXTERNAL_MCP = "external_mcp" + AUTOSKILLIT_LOCAL_FUNCTION = "autoskillit_local_function" + OTHER_LOCAL_FUNCTION = "other_local_function" + MCP_RESOURCE = "mcp_resource" + CLIENT_PROVIDER_RETRIEVAL = "client_provider_retrieval" + CODE_MODE_AGGREGATE = "code_mode_aggregate" + HOSTED_SPECIALIZED_TOOL = "hosted_specialized_tool" + HOOK_FEEDBACK = "hook_feedback" + TOOL_ARGUMENT = "tool_argument" + TOOL_RESULT_ENVELOPE = "tool_result_envelope" + USER_PROMPT = "user_prompt" + ASSISTANT_OUTPUT_HISTORY = "assistant_output_history" + SKILL_PLUGIN_CONTEXT = "skill_plugin_context" + OTHER_CONTEXT_INJECTION = "other_context_injection" + HEADLESS_CHILD_PROMPT = "headless_child_prompt" + PARENT_VISIBLE_CHILD_DELIVERY = "parent_visible_child_delivery" + COMPACTION_MODEL_WINDOW_TRANSITION = "compaction_model_window_transition" diff --git a/tests/_test_filter.py b/tests/_test_filter.py index 8794d0ed1..6be7a1edf 100644 --- a/tests/_test_filter.py +++ b/tests/_test_filter.py @@ -239,6 +239,7 @@ class ImportContext(enum.StrEnum): "_type_backend": frozenset({"core", "execution", "cli", "recipe", "server", "workspace"}), "_type_recipe_delivery": frozenset({"core", "execution", "server"}), "_type_recipe_sections": frozenset({"core", "execution", "server"}), + "_type_context_admission": frozenset({"core"}), "_type_dispatch_identity": frozenset({"core", "fleet", "execution"}), "_type_figure_spec": frozenset({"core", "report"}), "_type_session_env": frozenset({"core", "cli"}), @@ -276,6 +277,7 @@ class ImportContext(enum.StrEnum): "bash_write_targets": frozenset({"core", "execution", "server"}), "_delivery_bounds": frozenset({"core", "execution", "server"}), "_type_closure_report": frozenset({"core"}), + "context_admission": frozenset({"core"}), "closure_hashing": frozenset({"core"}), "path_containment": frozenset({"core"}), "closure_verifier": frozenset({"core", "execution"}), diff --git a/tests/arch/test_subpackage_isolation.py b/tests/arch/test_subpackage_isolation.py index 107326c66..7acfb256e 100644 --- a/tests/arch/test_subpackage_isolation.py +++ b/tests/arch/test_subpackage_isolation.py @@ -826,9 +826,12 @@ def test_no_subpackage_exceeds_10_files() -> None: _type_invariant_registry.py adds InvariantDef frozen dataclass and INVARIANT_REGISTRY mapping 13 prose prohibitions to runtime gate targets, bringing the core/types count to 31. + _type_recipe_sections.py adds recipe-section schema and digest contracts. _type_skill_contract.py adds the backend-neutral SkillSourceRef identity - consumed by workspace projections, bringing the core/types count to 34. - Exempt at 35 files (core/types: 34). + consumed by workspace projections. + _context_admission.py adds the pure context-admission reducer, and + _type_context_admission.py adds its frozen IL-0 contract records. + Exempt at 26 files (core/types: 36). cli/ — REQ-CNST-003-E5: cli/ retains _terminal_table.py as a re-export shim for backward-compatible cli/ imports; canonical implementation lives in core/_terminal_table.py. Also contains _terminal.py — the terminal state @@ -892,9 +895,8 @@ def test_no_subpackage_exceeds_10_files() -> None: "server": 16, # +_recipe_section_pagination deterministic bounded planner "recipe": 42, # was 33; +9 from CI/graph/dataflow splits "execution": 18, - "core": 25, # +_delivery_bounds (resolve_general_output_token_limit) - "core/types": 35, # +_type_recipe_sections schema and digest contracts - # +_type_skill_contract backend-neutral source identity + "core": 26, # +_context_admission pure reducer + "core/types": 36, # +_type_recipe_sections +_type_skill_contract +context admission "cli": 21, "cli/doctor": 11, # +_doctor_skills capability declaration authenticity checks "workspace": 12, # +skill_capabilities semantic validation +skill_projection @@ -1148,6 +1150,21 @@ def test_data_directories_are_not_python_packages() -> None: "cleanup remain in one manager; semantic validation and projection are already " "isolated in dedicated workspace modules.", ), + "core/context_admission.py": ( + 1750, + "REQ-CNST-010-E13: #4333 freezes one exhaustive protocol-v1 reducer and replay " + "surface. Keeping all closed event transitions together makes atomic batch, " + "idempotency, protected-pool, reconciliation, rollover, and declarative effect " + "semantics reviewable as one state machine; splitting dispatch branches would " + "fragment exhaustiveness.", + ), + "core/types/_type_context_admission.py": ( + 1500, + "REQ-CNST-010-E14: #4333 freezes the complete content-free protocol-v1 schema in " + "one IL-0 shard. Co-locating identities, records, closed event/effect unions, " + "states, canonical serialization, and the static coverage registry prevents " + "downstream layers from defining incompatible wire contracts.", + ), } diff --git a/tests/arch/test_subpackage_structure.py b/tests/arch/test_subpackage_structure.py index c674cfa47..a761ab0e8 100644 --- a/tests/arch/test_subpackage_structure.py +++ b/tests/arch/test_subpackage_structure.py @@ -22,6 +22,7 @@ def test_core_types_has_all_type_modules(self): "_type_constants_env", "_type_constants_features", "_type_constants_registries", + "_type_context_admission", "_type_dispatch_identity", "_type_enums", "_type_exceptions", diff --git a/tests/core/AGENTS.md b/tests/core/AGENTS.md index 6982427f6..0e7c6f33a 100644 --- a/tests/core/AGENTS.md +++ b/tests/core/AGENTS.md @@ -17,6 +17,9 @@ Core layer (IL-0) unit tests — paths, IO, types, feature flags. | `test_cmd_runner.py` | Tests for core/_cmd_runner.py — CmdRunner protocol, default_cmd_runner, run_git, run_gh | | `test_core.py` | Tests for the core/ sub-package foundation layer | | `test_core_terminal_table.py` | Tests for core/_terminal_table.py — the L0 shared table primitive | +| `test_context_admission_coverage.py` | Exact producer/control-point coverage registry and evidence metadata contract | +| `test_context_admission_reducer.py` | Pure context-admission reducer transition and replay tests | +| `test_context_admission_state_machine.py` | Property-based context-admission accounting and lifecycle invariants | | `test_ensure_project_temp_with_config.py` | Tests for ensure_project_temp with configurable override | | `test_feature_flags.py` | Tests for core/feature_flags.py — _collect_disabled_feature_tags helper | | `test_github_url.py` | Unit tests for core.github_url.parse_github_repo | @@ -70,3 +73,4 @@ Core layer (IL-0) unit tests — paths, IO, types, feature flags. | `test_closure_attacks.py` | Adversarial attack tests for closure-mode verification (forged verdicts, containment escape, ref drift, unauthorized rows, metadata stability) | | `test_closure_authority_spec.py` | Tests for ClosureAuthoritySpec and factory validation | | `test_closure_report.py` | Tests for ClosureRow and ClosureReport schema validation | +| `types/test_context_admission_contract.py` | Frozen context-admission enums, records, unions, validation, serialization, and gateway contract | diff --git a/tests/core/test_context_admission_coverage.py b/tests/core/test_context_admission_coverage.py new file mode 100644 index 000000000..eaf372de2 --- /dev/null +++ b/tests/core/test_context_admission_coverage.py @@ -0,0 +1,385 @@ +"""Freeze context-admission producer coverage and its supporting evidence.""" + +from __future__ import annotations + +from typing import NamedTuple + +import pytest + +from autoskillit.core import ( + CONTEXT_ADMISSION_COVERAGE, + CoverageEvidenceKind, + CoverageState, + ProducerSurface, + resolve_context_admission_coverage, +) + +pytestmark = [pytest.mark.layer("core"), pytest.mark.small] + +CHECKED_AT = "2026-07-23" +FRESHNESS_POLICY = "verify_on_version_or_configuration_change" +CODEX_REVISION = "25af12f7e61572b0bc18ddb1008be543b91519b0" +REASON_CODE = "authoritative_watermark_unavailable" + + +class ExpectedEvidence(NamedTuple): + claim_id: str + kind: str + backend: str + configuration_mode: str + verifier: str + source_locator: str + tested_version: str + tested_revision: str + checked_at: str + freshness_policy: str + + +class ExpectedCoverageRow(NamedTuple): + surface: str + control_point_owner: str + observation_state: str + authority_state: str + evidence: tuple[ExpectedEvidence, ...] + reason_code: str + + +def _autoskillit_evidence(claim_id: str, source_locator: str) -> ExpectedEvidence: + return ExpectedEvidence( + claim_id, + "AUTOSKILLIT_SOURCE", + "autoskillit", + "default", + "source_inspection", + source_locator, + "0.10.890", + "ac8f653a00d2", + CHECKED_AT, + FRESHNESS_POLICY, + ) + + +def _codex_evidence(claim_id: str) -> ExpectedEvidence: + return ExpectedEvidence( + claim_id, + "CODEX_SOURCE", + "codex", + "default", + "source_inspection", + "codex-rs/core/src/context_manager/history.rs", + "0.145.0", + CODEX_REVISION, + CHECKED_AT, + FRESHNESS_POLICY, + ) + + +def _gap_evidence(claim_id: str) -> ExpectedEvidence: + return ExpectedEvidence( + claim_id, + "INFERENCE", + "codex", + "default", + "source_gap_analysis", + "docs/decisions/0007-context-admission.md", + "0.145.0", + CODEX_REVISION, + CHECKED_AT, + FRESHNESS_POLICY, + ) + + +def _row( + surface: str, + owner: str, + observation_state: str, + evidence: ExpectedEvidence, +) -> ExpectedCoverageRow: + return ExpectedCoverageRow( + surface, + owner, + observation_state, + "UPSTREAM_GATED", + (evidence,), + REASON_CODE, + ) + + +EXPECTED_COVERAGE = ( + _row( + "NATIVE_SHELL", + "shell_capture_hook", + "VERIFIED", + _autoskillit_evidence( + "COV-NATIVE-SHELL", + "src/autoskillit/hooks/shell_capture_hook.py", + ), + ), + _row( + "UNIFIED_EXEC_AND_WRITE_STDIN", + "codex_host", + "PARTIAL", + _codex_evidence("COV-UNIFIED-EXEC-AND-WRITE-STDIN"), + ), + _row( + "APPLY_PATCH", + "codex_host", + "PARTIAL", + _codex_evidence("COV-APPLY-PATCH"), + ), + _row( + "AUTOSKILLIT_MCP", + "track_response_size", + "VERIFIED", + _autoskillit_evidence( + "COV-AUTOSKILLIT-MCP", + "src/autoskillit/server/_notify.py", + ), + ), + _row( + "EXTERNAL_MCP", + "fastmcp_client", + "PARTIAL", + _codex_evidence("COV-EXTERNAL-MCP"), + ), + _row( + "AUTOSKILLIT_LOCAL_FUNCTION", + "local_function_dispatch", + "VERIFIED", + _autoskillit_evidence( + "COV-AUTOSKILLIT-LOCAL-FUNCTION", + "src/autoskillit/execution/headless/_headless_helpers.py", + ), + ), + _row( + "OTHER_LOCAL_FUNCTION", + "codex_host", + "PARTIAL", + _codex_evidence("COV-OTHER-LOCAL-FUNCTION"), + ), + _row( + "MCP_RESOURCE", + "fastmcp_client", + "PARTIAL", + _codex_evidence("COV-MCP-RESOURCE"), + ), + _row( + "CLIENT_PROVIDER_RETRIEVAL", + "codex_host", + "UPSTREAM_GATED", + _gap_evidence("COV-CLIENT-PROVIDER-RETRIEVAL"), + ), + _row( + "CODE_MODE_AGGREGATE", + "codex_host", + "PARTIAL", + _codex_evidence("COV-CODE-MODE-AGGREGATE"), + ), + _row( + "HOSTED_SPECIALIZED_TOOL", + "codex_host", + "PARTIAL", + _codex_evidence("COV-HOSTED-SPECIALIZED-TOOL"), + ), + _row( + "HOOK_FEEDBACK", + "hook_registry", + "VERIFIED", + _autoskillit_evidence( + "COV-HOOK-FEEDBACK", + "src/autoskillit/hook_registry.py", + ), + ), + _row( + "TOOL_ARGUMENT", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-TOOL-ARGUMENT"), + ), + _row( + "TOOL_RESULT_ENVELOPE", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-TOOL-RESULT-ENVELOPE"), + ), + _row( + "USER_PROMPT", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-USER-PROMPT"), + ), + _row( + "ASSISTANT_OUTPUT_HISTORY", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-ASSISTANT-OUTPUT-HISTORY"), + ), + _row( + "SKILL_PLUGIN_CONTEXT", + "final_request_assembler", + "PARTIAL", + _codex_evidence("COV-SKILL-PLUGIN-CONTEXT"), + ), + _row( + "OTHER_CONTEXT_INJECTION", + "final_request_assembler", + "UPSTREAM_GATED", + _gap_evidence("COV-OTHER-CONTEXT-INJECTION"), + ), + _row( + "HEADLESS_CHILD_PROMPT", + "headless_prompt_builder", + "VERIFIED", + _autoskillit_evidence( + "COV-HEADLESS-CHILD-PROMPT", + "src/autoskillit/execution/headless/_headless_helpers.py", + ), + ), + _row( + "PARENT_VISIBLE_CHILD_DELIVERY", + "child_delivery_receipt", + "VERIFIED", + _autoskillit_evidence( + "COV-PARENT-VISIBLE-CHILD-DELIVERY", + "src/autoskillit/server/_recipe_delivery.py", + ), + ), + _row( + "COMPACTION_MODEL_WINDOW_TRANSITION", + "compaction_receiver", + "PARTIAL", + _codex_evidence("COV-COMPACTION-MODEL-WINDOW-TRANSITION"), + ), +) + +EXPECTED_SURFACES = ( + "NATIVE_SHELL", + "UNIFIED_EXEC_AND_WRITE_STDIN", + "APPLY_PATCH", + "AUTOSKILLIT_MCP", + "EXTERNAL_MCP", + "AUTOSKILLIT_LOCAL_FUNCTION", + "OTHER_LOCAL_FUNCTION", + "MCP_RESOURCE", + "CLIENT_PROVIDER_RETRIEVAL", + "CODE_MODE_AGGREGATE", + "HOSTED_SPECIALIZED_TOOL", + "HOOK_FEEDBACK", + "TOOL_ARGUMENT", + "TOOL_RESULT_ENVELOPE", + "USER_PROMPT", + "ASSISTANT_OUTPUT_HISTORY", + "SKILL_PLUGIN_CONTEXT", + "OTHER_CONTEXT_INJECTION", + "HEADLESS_CHILD_PROMPT", + "PARENT_VISIBLE_CHILD_DELIVERY", + "COMPACTION_MODEL_WINDOW_TRANSITION", +) + + +def _coverage_projection() -> tuple[ExpectedCoverageRow, ...]: + return tuple( + ExpectedCoverageRow( + row.surface.name, + row.control_point_owner, + row.observation_state.name, + row.authority_state.name, + tuple( + ExpectedEvidence( + item.claim_id, + item.kind.name, + item.backend, + item.configuration_mode, + item.verifier, + item.source_locator, + item.tested_version, + item.tested_revision, + str(item.checked_at), + item.freshness_policy, + ) + for item in row.evidence + ), + row.reason_code, + ) + for row in CONTEXT_ADMISSION_COVERAGE + ) + + +def test_producer_surface_and_registry_are_independently_exhaustive() -> None: + assert tuple(member.name for member in ProducerSurface) == EXPECTED_SURFACES + assert tuple(row.surface.name for row in CONTEXT_ADMISSION_COVERAGE) == EXPECTED_SURFACES + + +def test_coverage_registry_freezes_every_row_and_evidence_field() -> None: + assert isinstance(CONTEXT_ADMISSION_COVERAGE, tuple) + assert _coverage_projection() == EXPECTED_COVERAGE + assert all(isinstance(row.evidence, tuple) for row in CONTEXT_ADMISSION_COVERAGE) + + +def test_verified_observations_have_primary_evidence() -> None: + primary_kinds = { + CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + CoverageEvidenceKind.CODEX_SOURCE, + CoverageEvidenceKind.CODEX_OFFICIAL_DOC, + CoverageEvidenceKind.CODEX_RUNTIME_PROBE, + } + for row in CONTEXT_ADMISSION_COVERAGE: + if row.observation_state is CoverageState.VERIFIED: + assert any(item.kind in primary_kinds for item in row.evidence) + assert all(item.kind is not CoverageEvidenceKind.INFERENCE for item in row.evidence) + + +def test_claim_ids_are_stable_and_unique() -> None: + expected = tuple(f"COV-{surface.replace('_', '-')}" for surface in EXPECTED_SURFACES) + actual = tuple(row.evidence[0].claim_id for row in CONTEXT_ADMISSION_COVERAGE) + assert actual == expected + assert len(actual) == len(set(actual)) + + +def test_runtime_version_or_configuration_mismatch_degrades_deterministically() -> None: + for row in CONTEXT_ADMISSION_COVERAGE: + evidence = row.evidence[0] + resolved = resolve_context_admission_coverage( + row.surface, + evidence.backend, + evidence.configuration_mode, + evidence.tested_version, + CHECKED_AT, + ) + assert resolved == row + + version_mismatch = resolve_context_admission_coverage( + row.surface, + evidence.backend, + evidence.configuration_mode, + f"{evidence.tested_version}-mismatch", + CHECKED_AT, + ) + configuration_mismatch = resolve_context_admission_coverage( + row.surface, + evidence.backend, + f"{evidence.configuration_mode}-mismatch", + evidence.tested_version, + CHECKED_AT, + ) + assert version_mismatch.observation_state is CoverageState.UPSTREAM_GATED + assert version_mismatch.authority_state is CoverageState.UPSTREAM_GATED + assert configuration_mismatch.observation_state is CoverageState.UPSTREAM_GATED + assert configuration_mismatch.authority_state is CoverageState.UPSTREAM_GATED + assert version_mismatch == resolve_context_admission_coverage( + row.surface, + evidence.backend, + evidence.configuration_mode, + f"{evidence.tested_version}-mismatch", + CHECKED_AT, + ) + + +def test_compaction_observation_does_not_imply_authority() -> None: + row = next( + item + for item in CONTEXT_ADMISSION_COVERAGE + if item.surface is ProducerSurface.COMPACTION_MODEL_WINDOW_TRANSITION + ) + assert row.observation_state is CoverageState.PARTIAL + assert row.authority_state is CoverageState.UPSTREAM_GATED diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py new file mode 100644 index 000000000..51ddb2118 --- /dev/null +++ b/tests/core/test_context_admission_reducer.py @@ -0,0 +1,1240 @@ +"""Behavioral contract for the pure cumulative context-admission reducer.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import Any + +import pytest + +from autoskillit.core import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION, + AcceptInputEvent, + ActiveContextAdmissionState, + AdmissionAttemptId, + AdmissionBatch, + AdmissionBatchId, + AdmissionDecisionKind, + AdmissionEventId, + AdmissionOccurrence, + AdmissionOccurrenceId, + AdmissionRequestId, + AdmissionReservation, + AdmissionReservationId, + AdmissionReservationKey, + AdmissionSequence, + AdmissionState, + AdmissionWitness, + AdmissionWitnessId, + AgentInstanceId, + AggregateRevision, + AuthoritySourceId, + AuthorityUnavailableEvent, + CanonicalRepresentationManifest, + CanonicalSpanId, + CanonicalSpanOwner, + ContextAdmissionValidationError, + ContextLineage, + ContextSessionId, + ContextThreadId, + ContextWindowSnapshot, + CoverageState, + DeliveryOccurrenceId, + DispatchRequestEvent, + EpochFenceProof, + ExpireIdempotencyKeyEvent, + ForkOccurrenceId, + GenerationReservationId, + GenerationReservationRecord, + GenerationState, + IdempotencyNamespace, + MarkGenerationIndeterminateEvent, + MarkIndeterminateEvent, + MeasurementKind, + ModelIdentity, + ModelItemId, + OpenEpochEvent, + PrepareBatchEvent, + ProducerInstanceId, + ProducerSurface, + ProposeOccurrenceEvent, + ProtectedPoolOwnerId, + ProtectedPoolSpec, + ReconcileGenerationEvent, + ReleaseNonAdmissionEvent, + RepresentationBindingWitness, + RepresentationRevision, + RequestReconciliationEvent, + ReserveClass, + ReserveRequestEvent, + ResolveIndeterminateAcceptedEvent, + ResolveIndeterminateNonAdmissionEvent, + ResolveIndeterminateRollbackEvent, + RollbackAdmissionEvent, + RolloverEpochEvent, + StageHistoryEvent, + StartGenerationEvent, + TokenizerIdentity, + ToolCallId, + TurnId, + UninitializedContextAdmissionState, + WindowEpochId, + WitnessKind, + reduce_context_admission, + replay_context_admission, +) + +pytestmark = [pytest.mark.layer("core"), pytest.mark.small] + + +def _namespace(operation_kind: str) -> IdempotencyNamespace: + return IdempotencyNamespace(caller_scope="test-caller", operation_kind=operation_kind) + + +def _event_fields( + state: UninitializedContextAdmissionState | ActiveContextAdmissionState, + event_id: str, + operation_kind: str, + *, + expected_revision: int | None = None, +) -> dict[str, object]: + return { + "event_id": AdmissionEventId(event_id), + "protocol_version": CONTEXT_ADMISSION_PROTOCOL_VERSION, + "idempotency_namespace": _namespace(operation_kind), + "expected_aggregate_revision": AggregateRevision( + state.aggregate_revision.value if expected_revision is None else expected_revision + ), + } + + +def _snapshot( + *, + epoch: int = 1, + sequence: int = 1, + active_count: int = 60, + hard_limit: int = 100, + remaining_count: int = 40, + model: str = "claude-test", + tokenizer: str = "tokenizer-test", +) -> ContextWindowSnapshot: + return ContextWindowSnapshot( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId(f"epoch-{epoch}"), + window_epoch_number=epoch, + model_identity=ModelIdentity.anthropic(model), + tokenizer_identity=TokenizerIdentity(tokenizer), + snapshot_sequence=sequence, + active_count=active_count, + hard_limit=hard_limit, + remaining_count=remaining_count, + ) + + +def _uninitialized() -> UninitializedContextAdmissionState: + return UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + +def _open_epoch( + *, + remaining_count: int = 40, + protected_pools: tuple[ProtectedPoolSpec, ...] = (), + epoch: int = 1, + state: UninitializedContextAdmissionState | ActiveContextAdmissionState | None = None, +) -> ActiveContextAdmissionState: + prior = state or _uninitialized() + transition = reduce_context_admission( + prior, + OpenEpochEvent( + **_event_fields(prior, f"open-{epoch}", "open-epoch"), + snapshot=_snapshot( + epoch=epoch, + active_count=100 - remaining_count, + remaining_count=remaining_count, + ), + protected_pools=protected_pools, + ), + ) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(transition.next_state, ActiveContextAdmissionState) + return transition.next_state + + +def _lineage( + occurrence: str, + *, + epoch: int = 1, + surface: ProducerSurface = ProducerSurface.TOOL_RESULT_ENVELOPE, + parent_agent: str | None = None, + fork: str | None = None, + delivery: str | None = None, +) -> ContextLineage: + return ContextLineage( + root_session_id=ContextSessionId("session-root"), + current_session_id=ContextSessionId( + "session-child" if parent_agent is not None else "session-root" + ), + root_agent_id=AgentInstanceId("agent-root"), + current_agent_id=AgentInstanceId( + f"agent-{occurrence}" if parent_agent is not None else "agent-root" + ), + parent_agent_id=AgentInstanceId(parent_agent) if parent_agent is not None else None, + root_thread_id=ContextThreadId("thread-root"), + current_thread_id=ContextThreadId( + f"thread-{occurrence}" if fork is not None else "thread-root" + ), + parent_thread_id=ContextThreadId("thread-root") if fork is not None else None, + fork_occurrence_id=ForkOccurrenceId(fork) if fork is not None else None, + turn_id=TurnId(f"turn-{occurrence}"), + producer_surface=surface, + producer_instance_id=ProducerInstanceId(f"producer-{occurrence}"), + tool_call_id=ToolCallId(f"tool-{occurrence}"), + model_item_id=ModelItemId(f"item-{occurrence}"), + dispatch_identity=None, + attempt_id=AdmissionAttemptId(f"attempt-{occurrence}"), + delivery_occurrence_id=(DeliveryOccurrenceId(delivery) if delivery is not None else None), + window_epoch_id=WindowEpochId(f"epoch-{epoch}"), + window_epoch_number=epoch, + ) + + +def _occurrence( + name: str, + *, + maximum: int, + revision: str | None = None, + epoch: int = 1, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + surface: ProducerSurface = ProducerSurface.TOOL_RESULT_ENVELOPE, + lineage: ContextLineage | None = None, +) -> AdmissionOccurrence: + return AdmissionOccurrence( + occurrence_id=AdmissionOccurrenceId(name), + lineage=lineage or _lineage(name, epoch=epoch, surface=surface), + reserve_class=reserve_class, + producer_surface=surface, + predicted_authoritative_maximum=maximum, + representation_revision=RepresentationRevision(revision or f"revision-{name}"), + owned_span_ids=(CanonicalSpanId(f"span-{name}"),), + ) + + +def _manifest( + request: str, + occurrences: tuple[AdmissionOccurrence, ...], + *, + revision: str = "revision-final", +) -> CanonicalRepresentationManifest: + return CanonicalRepresentationManifest( + request_id=AdmissionRequestId(request), + representation_revision=RepresentationRevision(revision), + span_owners=tuple( + CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) + for occurrence in occurrences + for span_id in occurrence.owned_span_ids + ), + assembler_identity=ProducerInstanceId(f"assembler-{request}"), + assembler_witness_id=AdmissionWitnessId(f"assembler-witness-{request}"), + ) + + +def _batch( + name: str, + occurrences: tuple[AdmissionOccurrence, ...], + *, + reserve_class: ReserveClass = ReserveClass.ORDINARY, + protected_owner: str | None = None, + manifest_revision: str = "revision-final", +) -> AdmissionBatch: + request = f"request-{name}" + return AdmissionBatch( + batch_id=AdmissionBatchId(name), + request_id=AdmissionRequestId(request), + occurrence_ids=tuple(occurrence.occurrence_id for occurrence in occurrences), + reserve_class=reserve_class, + protected_pool_owner_id=( + ProtectedPoolOwnerId(protected_owner) if protected_owner is not None else None + ), + manifest=_manifest(request, occurrences, revision=manifest_revision), + ) + + +def _reservation( + batch: AdmissionBatch, + occurrences: tuple[AdmissionOccurrence, ...], + *, + count: int, + snapshot_sequence: int = 1, +) -> AdmissionReservation: + namespace = _namespace("reserve-request") + key = AdmissionReservationKey( + idempotency_namespace=namespace, + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=occurrences[0].lineage.window_epoch_id, + window_epoch_number=occurrences[0].lineage.window_epoch_number, + batch_id=batch.batch_id, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + occurrence_revisions=tuple( + (occurrence.occurrence_id, occurrence.representation_revision) + for occurrence in occurrences + ), + ) + return AdmissionReservation( + reservation_id=AdmissionReservationId(f"reservation-{batch.batch_id.value}"), + key=key, + window_epoch_id=occurrences[0].lineage.window_epoch_id, + window_epoch_number=occurrences[0].lineage.window_epoch_number, + snapshot_sequence=snapshot_sequence, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + occurrence_ids=batch.occurrence_ids, + reserved_count=count, + ) + + +def _generation_reservation( + batch: AdmissionBatch, + *, + maximum: int, + epoch: int = 1, + snapshot_sequence: int = 1, +) -> GenerationReservationRecord: + return GenerationReservationRecord( + generation_reservation_id=GenerationReservationId(f"generation-{batch.batch_id.value}"), + request_id=batch.request_id, + response_id=ModelItemId(f"response-{batch.batch_id.value}"), + window_epoch_id=WindowEpochId(f"epoch-{epoch}"), + window_epoch_number=epoch, + snapshot_sequence=snapshot_sequence, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + maximum_allowance=maximum, + state=GenerationState.RESERVED, + exact_terminal_usage=None, + witness_ids=(), + ) + + +def _witness( + batch: AdmissionBatch, + kind: WitnessKind, + *, + witness: str | None = None, + revision: str | None = None, + epoch: int = 1, +) -> AdmissionWitness: + return AdmissionWitness( + witness_id=AdmissionWitnessId(witness or f"{kind.value}-witness-{batch.batch_id.value}"), + kind=kind, + window_epoch_id=WindowEpochId(f"epoch-{epoch}"), + window_epoch_number=epoch, + snapshot_sequence=1, + request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=RepresentationRevision( + revision or batch.manifest.representation_revision.value + ), + occurrence_ids=batch.occurrence_ids, + authority_source_id=AuthoritySourceId("authority-test"), + ) + + +def _binding( + batch: AdmissionBatch, *, revision: str | None = None +) -> RepresentationBindingWitness: + bound_revision = RepresentationRevision( + revision or batch.manifest.representation_revision.value + ) + return RepresentationBindingWitness( + counted_representation_revision=bound_revision, + dispatched_representation_revision=bound_revision, + final_manifest_revision=bound_revision, + request_id=batch.request_id, + batch_id=batch.batch_id, + authority_source_id=AuthoritySourceId("authority-test"), + ) + + +def _propose( + state: ActiveContextAdmissionState, + occurrence: AdmissionOccurrence, + *, + event_id: str | None = None, +) -> tuple[ActiveContextAdmissionState, ProposeOccurrenceEvent]: + event = ProposeOccurrenceEvent( + **_event_fields( + state, + event_id or f"propose-{occurrence.occurrence_id.value}", + "propose-occurrence", + ), + occurrence=occurrence, + ) + transition = reduce_context_admission(state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(transition.next_state, ActiveContextAdmissionState) + return transition.next_state, event + + +def _reserve( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + occurrences: tuple[AdmissionOccurrence, ...], + *, + input_count: int, + generation_count: int = 0, + event_id: str | None = None, + expected_revision: int | None = None, +) -> tuple[Any, ReserveRequestEvent]: + event = ReserveRequestEvent( + **_event_fields( + state, + event_id or f"reserve-{batch.batch_id.value}", + "reserve-request", + expected_revision=expected_revision, + ), + batch=batch, + snapshot_sequence=state.snapshot.snapshot_sequence, + input_reservations=(_reservation(batch, occurrences, count=input_count),), + generation_reservation=_generation_reservation(batch, maximum=generation_count), + ) + return reduce_context_admission(state, event), event + + +def _records_for(state: ActiveContextAdmissionState, batch: AdmissionBatch) -> tuple[Any, ...]: + wanted = set(batch.occurrence_ids) + return tuple( + record for record in state.occurrence_records if record.occurrence.occurrence_id in wanted + ) + + +def _batch_record(state: ActiveContextAdmissionState, batch: AdmissionBatch) -> Any: + return next( + record for record in state.batch_records if record.batch.batch_id == batch.batch_id + ) + + +def _generation_record( + state: ActiveContextAdmissionState, reservation_id: GenerationReservationId +) -> Any: + return next( + record + for record in state.generation_reservations + if record.generation_reservation_id == reservation_id + ) + + +def _input_reservation( + state: ActiveContextAdmissionState, reservation_id: AdmissionReservationId +) -> AdmissionReservation: + return next( + reservation + for reservation in state.reservations + if reservation.reservation_id == reservation_id + ) + + +@pytest.mark.parametrize( + ("counts", "expected"), + [ + ((20, 20, 1), ("WOULD_ADMIT", "WOULD_ADMIT", "WOULD_REJECT")), + ((39, 1), ("WOULD_ADMIT", "WOULD_ADMIT")), + ((41,), ("WOULD_REJECT",)), + ], +) +def test_sequential_occurrences_exhaust_one_authoritative_window( + counts: tuple[int, ...], expected: tuple[str, ...] +) -> None: + state = _open_epoch(remaining_count=40) + decisions: list[str] = [] + for index, count in enumerate(counts): + occurrence = _occurrence(f"sequential-{index}", maximum=count) + state, _ = _propose(state, occurrence) + batch = _batch(f"batch-sequential-{index}", (occurrence,)) + transition, _ = _reserve(state, batch, (occurrence,), input_count=count) + decisions.append(transition.decision.kind.name) + assert isinstance(transition.next_state, ActiveContextAdmissionState) + state = transition.next_state + assert tuple(decisions) == expected + + +def test_stale_concurrent_proposals_cannot_overcommit_one_snapshot() -> None: + state = _open_epoch(remaining_count=30) + first = _occurrence("concurrent-a", maximum=20) + second = _occurrence("concurrent-b", maximum=20) + state, _ = _propose(state, first) + state, _ = _propose(state, second) + shared_revision = state.aggregate_revision.value + + first_batch = _batch("batch-concurrent-a", (first,)) + second_batch = _batch("batch-concurrent-b", (second,)) + first_transition, _ = _reserve( + state, + first_batch, + (first,), + input_count=20, + expected_revision=shared_revision, + ) + assert first_transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(first_transition.next_state, ActiveContextAdmissionState) + + stale_transition, _ = _reserve( + first_transition.next_state, + second_batch, + (second,), + input_count=20, + expected_revision=shared_revision, + ) + assert stale_transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert stale_transition.next_state == first_transition.next_state + + +@pytest.mark.parametrize( + ("remaining", "input_count", "generation_count", "expected_kind"), + [ + (50, 30, 20, AdmissionDecisionKind.WOULD_ADMIT), + (49, 30, 20, AdmissionDecisionKind.WOULD_REJECT), + ], +) +def test_ordered_batch_and_generation_maximum_are_reserved_atomically( + remaining: int, + input_count: int, + generation_count: int, + expected_kind: AdmissionDecisionKind, +) -> None: + state = _open_epoch(remaining_count=remaining) + occurrences = ( + _occurrence("atomic-a", maximum=10), + _occurrence("atomic-b", maximum=20), + ) + for occurrence in occurrences: + state, _ = _propose(state, occurrence) + batch = _batch("batch-atomic", occurrences) + transition, _ = _reserve( + state, + batch, + occurrences, + input_count=input_count, + generation_count=generation_count, + ) + assert transition.decision.kind is expected_kind + assert isinstance(transition.next_state, ActiveContextAdmissionState) + records = _records_for(transition.next_state, batch) + assert len(records) == 2 + expected_state = ( + AdmissionState.RESERVED + if expected_kind is AdmissionDecisionKind.WOULD_ADMIT + else AdmissionState.PROPOSED + ) + assert {record.state for record in records} == {expected_state} + if expected_kind is AdmissionDecisionKind.WOULD_REJECT: + assert transition.next_state.generation_reservations == () + + +def _reserved_batch( + *, + remaining_count: int = 60, + input_count: int = 25, + generation_count: int = 20, + name: str = "lifecycle", +) -> tuple[ + ActiveContextAdmissionState, + AdmissionBatch, + tuple[AdmissionOccurrence, ...], + GenerationReservationRecord, +]: + state = _open_epoch(remaining_count=remaining_count) + occurrences = (_occurrence(name, maximum=input_count),) + state, _ = _propose(state, occurrences[0]) + batch = _batch(f"batch-{name}", occurrences) + transition, _ = _reserve( + state, + batch, + occurrences, + input_count=input_count, + generation_count=generation_count, + ) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(transition.next_state, ActiveContextAdmissionState) + generation = _generation_reservation(batch, maximum=generation_count) + return transition.next_state, batch, occurrences, generation + + +def _prepare_dispatch( + state: ActiveContextAdmissionState, batch: AdmissionBatch +) -> ActiveContextAdmissionState: + batch_record = _batch_record(state, batch) + assert batch_record.reservation_id is not None + prepare = PrepareBatchEvent( + **_event_fields(state, f"prepare-{batch.batch_id.value}", "prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + proposed_charge=_input_reservation(state, batch_record.reservation_id).reserved_count, + measurement_kind=MeasurementKind.TOKENIZER_EXACT, + authority_source_id=AuthoritySourceId("authority-test"), + ) + prepared = reduce_context_admission(state, prepare) + assert prepared.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(prepared.next_state, ActiveContextAdmissionState) + dispatch = DispatchRequestEvent( + **_event_fields( + prepared.next_state, + f"dispatch-{batch.batch_id.value}", + "dispatch-request", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ) + dispatched = reduce_context_admission(prepared.next_state, dispatch) + assert dispatched.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(dispatched.next_state, ActiveContextAdmissionState) + return dispatched.next_state + + +def test_input_commit_and_output_reconciliation_remain_distinct_domains() -> None: + state, batch, _, generation = _reserved_batch() + state = _prepare_dispatch(state, batch) + accept = AcceptInputEvent( + **_event_fields(state, "accept-lifecycle", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + exact_input_charge=18, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("provider-test"), + representation_binding_witness=_binding(batch), + ) + accepted = reduce_context_admission(state, accept) + assert accepted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(accepted.next_state, ActiveContextAdmissionState) + assert _batch_record(accepted.next_state, batch).committed_input_count == 18 + + start = StartGenerationEvent( + **_event_fields(accepted.next_state, "start-generation-lifecycle", "start-generation"), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ) + started = reduce_context_admission(accepted.next_state, start) + assert isinstance(started.next_state, ActiveContextAdmissionState) + reconcile = ReconcileGenerationEvent( + **_event_fields( + started.next_state, + "reconcile-generation-lifecycle", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=7, + ) + reconciled = reduce_context_admission(started.next_state, reconcile) + assert isinstance(reconciled.next_state, ActiveContextAdmissionState) + generation_record = _generation_record( + reconciled.next_state, generation.generation_reservation_id + ) + assert generation_record.exact_terminal_usage == 7 + assert _batch_record(reconciled.next_state, batch).committed_input_count == 18 + + +def test_overlapping_tool_argument_and_history_span_is_rejected_before_debit() -> None: + tool_argument = _occurrence( + "tool-argument", + maximum=5, + surface=ProducerSurface.TOOL_ARGUMENT, + ) + history = replace( + _occurrence( + "history", + maximum=5, + surface=ProducerSurface.ASSISTANT_OUTPUT_HISTORY, + ), + owned_span_ids=tool_argument.owned_span_ids, + ) + with pytest.raises(ContextAdmissionValidationError): + _batch("overlapping", (tool_argument, history)) + + +def test_serialized_retry_is_idempotent_and_changed_intent_conflicts() -> None: + initial = _open_epoch() + occurrence = _occurrence("idempotent", maximum=7) + next_state, event = _propose(initial, occurrence, event_id="same-event") + restored_state = ActiveContextAdmissionState.from_dict(next_state.to_dict()) + restored_event = ProposeOccurrenceEvent.from_dict(event.to_dict()) + + retry = reduce_context_admission(restored_state, restored_event) + assert retry.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert retry.next_state == restored_state + + changed = replace( + restored_event, + occurrence=replace( + occurrence, + representation_revision=RepresentationRevision("changed-revision"), + ), + ) + conflict = reduce_context_admission(restored_state, changed) + assert conflict.decision.kind is AdmissionDecisionKind.CONFLICT + assert conflict.next_state == restored_state + + +def test_same_reservation_key_under_new_delivery_returns_original_result() -> None: + state = _open_epoch() + occurrence = _occurrence("new-delivery-retry", maximum=7) + state, _ = _propose(state, occurrence) + batch = _batch("batch-new-delivery-retry", (occurrence,)) + reserved, event = _reserve( + state, + batch, + (occurrence,), + input_count=7, + generation_count=3, + ) + assert reserved.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + + redelivered = replace(event, event_id=AdmissionEventId("reserve-new-delivery-retry-2")) + retry = reduce_context_admission(reserved.next_state, redelivered) + assert retry.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert retry.next_state == reserved.next_state + + assert redelivered.generation_reservation is not None + changed_intent = replace( + redelivered, + generation_reservation=replace( + redelivered.generation_reservation, + maximum_allowance=4, + ), + ) + conflict = reduce_context_admission(reserved.next_state, changed_intent) + assert conflict.decision.kind is AdmissionDecisionKind.CONFLICT + assert conflict.next_state == reserved.next_state + + +@pytest.mark.parametrize( + ("event_kind", "witness_kind"), + [ + ("stage", WitnessKind.REQUEST_INCLUDED), + ("dispatch", WitnessKind.HISTORY_STAGED), + ("release", WitnessKind.ROLLBACK), + ("rollback", WitnessKind.NON_ADMISSION), + ], +) +def test_history_request_provider_and_rollback_witnesses_are_not_interchangeable( + event_kind: str, witness_kind: WitnessKind +) -> None: + state, batch, _, _ = _reserved_batch(name=f"witness-{event_kind}") + if event_kind == "stage": + event = StageHistoryEvent( + **_event_fields(state, f"bad-{event_kind}", "stage-history"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + ) + elif event_kind == "dispatch": + event = DispatchRequestEvent( + **_event_fields(state, f"bad-{event_kind}", "dispatch-request"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + ) + elif event_kind == "release": + event = ReleaseNonAdmissionEvent( + **_event_fields(state, f"bad-{event_kind}", "release-non-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + ) + else: + event = RollbackAdmissionEvent( + **_event_fields(state, f"bad-{event_kind}", "rollback-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.next_state == state + + +@pytest.mark.parametrize( + ("mutation", "expected_reason_fragment"), + [ + ("count-then-mutate", "revision"), + ("stale-prepared-revision", "revision"), + ("stale-receiver-fence", "fence"), + ], +) +def test_revision_and_fence_failures_do_not_commit( + mutation: str, expected_reason_fragment: str +) -> None: + state, batch, _, _ = _reserved_batch(name=mutation) + if mutation != "stale-receiver-fence": + event = PrepareBatchEvent( + **_event_fields(state, f"prepare-{mutation}", "prepare-batch"), + batch_id=batch.batch_id, + representation_revision=RepresentationRevision("mutated-revision"), + proposed_charge=25, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("authority-test"), + ) + else: + event = RolloverEpochEvent( + **_event_fields(state, "rollover-stale-fence", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + fence_proof=EpochFenceProof( + old_window_epoch_id=WindowEpochId("epoch-1"), + old_window_epoch_number=1, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=AuthoritySourceId("stale-receiver"), + fence_witness_id=AdmissionWitnessId("stale-fence"), + highest_admitted_dispatch_sequence=0, + ), + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert expected_reason_fragment in rejected.decision.reason_code + assert rejected.next_state == state + + +def test_partial_batch_and_illegal_order_are_typed_non_mutating_rejections() -> None: + state = _open_epoch(remaining_count=40) + occurrences = ( + _occurrence("partial-a", maximum=10), + _occurrence("partial-b", maximum=10), + ) + for occurrence in occurrences: + state, _ = _propose(state, occurrence) + batch = _batch("batch-partial", occurrences) + reserved, _ = _reserve(state, batch, occurrences, input_count=20) + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + state = reserved.next_state + + partial_witness = replace( + _witness(batch, WitnessKind.REQUEST_INCLUDED), + occurrence_ids=(occurrences[0].occurrence_id,), + ) + for index, event in enumerate( + ( + DispatchRequestEvent( + **_event_fields( + state, + "illegal-dispatch-before-prepare", + "dispatch-request", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ), + DispatchRequestEvent( + **_event_fields( + state, + "partial-batch-dispatch", + "dispatch-request", + ), + batch_id=batch.batch_id, + witness=partial_witness, + ), + ) + ): + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT, index + assert rejected.next_state == state + + +@pytest.mark.parametrize( + "mutation", + ( + "final-manifest-revision", + "representation-binding", + "silent-truncation", + ), +) +def test_acceptance_rejects_mutated_or_unattested_final_representation( + mutation: str, +) -> None: + state, batch, _, _ = _reserved_batch(name=f"accept-{mutation}") + state = _prepare_dispatch(state, batch) + witness_kind = ( + WitnessKind.TRUNCATION + if mutation == "silent-truncation" + else WitnessKind.PROVIDER_ACCEPTED + ) + final_revision = ( + RepresentationRevision("mutated-final") + if mutation == "final-manifest-revision" + else batch.manifest.representation_revision + ) + binding_revision = ( + "mutated-binding" + if mutation == "representation-binding" + else batch.manifest.representation_revision.value + ) + event = AcceptInputEvent( + **_event_fields(state, f"reject-{mutation}", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, witness_kind), + final_manifest_revision=final_revision, + exact_input_charge=20, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("provider-test"), + representation_binding_witness=_binding(batch, revision=binding_revision), + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.next_state == state + + +@pytest.mark.parametrize( + ("resolve_kind", "terminal_state"), + [ + ("non-admission", AdmissionState.RELEASED), + ("rollback", AdmissionState.ROLLED_BACK), + ], +) +def test_ambiguous_crash_stays_charged_until_an_authoritative_resolution( + resolve_kind: str, terminal_state: AdmissionState +) -> None: + state, batch, _, _ = _reserved_batch(name=f"crash-{resolve_kind}") + state = _prepare_dispatch(state, batch) + mark = MarkIndeterminateEvent( + **_event_fields(state, f"mark-{resolve_kind}", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="ambiguous-crash", + ) + marked = reduce_context_admission(state, mark) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + assert {record.state for record in _records_for(marked.next_state, batch)} == { + AdmissionState.INDETERMINATE + } + + query = RequestReconciliationEvent( + **_event_fields( + marked.next_state, + f"query-{resolve_kind}", + "request-reconciliation", + ), + target_id=batch.batch_id, + reason_code="deadline-observed", + ) + queried = reduce_context_admission(marked.next_state, query) + assert isinstance(queried.next_state, ActiveContextAdmissionState) + queried_batch = _batch_record(queried.next_state, batch) + assert queried_batch.reservation_id is not None + assert ( + _input_reservation(queried.next_state, queried_batch.reservation_id).reserved_count == 25 + ) + assert not any("Released" in type(effect).__name__ for effect in queried.effects) + + if resolve_kind == "non-admission": + resolution = ResolveIndeterminateNonAdmissionEvent( + **_event_fields( + queried.next_state, + f"resolve-{resolve_kind}", + "resolve-indeterminate-non-admission", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION), + ) + else: + resolution = ResolveIndeterminateRollbackEvent( + **_event_fields( + queried.next_state, + f"resolve-{resolve_kind}", + "resolve-indeterminate-rollback", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.ROLLBACK), + ) + resolved = reduce_context_admission(queried.next_state, resolution) + assert isinstance(resolved.next_state, ActiveContextAdmissionState) + assert {record.state for record in _records_for(resolved.next_state, batch)} == { + terminal_state + } + + +@pytest.mark.parametrize("exact_charge", [26, 101]) +def test_provider_accepted_overage_is_recorded_and_quarantined( + exact_charge: int, +) -> None: + state, batch, _, _ = _reserved_batch( + remaining_count=100, input_count=25, name=f"overage-{exact_charge}" + ) + state = _prepare_dispatch(state, batch) + event = AcceptInputEvent( + **_event_fields(state, f"accept-overage-{exact_charge}", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + exact_input_charge=exact_charge, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("provider-test"), + representation_binding_witness=_binding(batch), + ) + quarantined = reduce_context_admission(state, event) + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + record = _batch_record(quarantined.next_state, batch) + assert record.committed_input_count == exact_charge + assert record.state is AdmissionState.QUARANTINED + + +def test_retry_resume_fork_and_parent_delivery_keep_distinct_occurrences() -> None: + state = _open_epoch(remaining_count=40) + lineages = ( + _lineage("resume"), + _lineage("fork", parent_agent="agent-root", fork="fork-1"), + _lineage( + "delivery", + parent_agent="agent-root", + fork="fork-1", + delivery="delivery-1", + surface=ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY, + ), + ) + occurrences = tuple( + _occurrence( + name, + maximum=5, + lineage=lineage, + surface=lineage.producer_surface, + ) + for name, lineage in zip(("resume", "fork", "delivery"), lineages, strict=True) + ) + for occurrence in occurrences: + state, _ = _propose(state, occurrence) + assert {record.occurrence.occurrence_id for record in state.occurrence_records} == { + occurrence.occurrence_id for occurrence in occurrences + } + assert occurrences[1].lineage.current_thread_id != occurrences[0].lineage.current_thread_id + assert occurrences[2].lineage.delivery_occurrence_id is not None + + +def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> None: + state, batch, _, _ = _reserved_batch(name="rollover") + proof = EpochFenceProof( + old_window_epoch_id=WindowEpochId("epoch-1"), + old_window_epoch_number=1, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=AuthoritySourceId("receiver-2"), + fence_witness_id=AdmissionWitnessId("fence-1-to-2"), + highest_admitted_dispatch_sequence=state.admission_sequence.value, + ) + event = RolloverEpochEvent( + **_event_fields(state, "rollover-1-to-2", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + fence_proof=proof, + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + rolled = reduce_context_admission(state, event) + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + assert rolled.next_state.snapshot.window_epoch_id == WindowEpochId("epoch-2") + assert len(rolled.next_state.closed_epochs) == 1 + assert { + record.state for record in rolled.next_state.closed_epochs[0].terminal_occurrence_records + } == {AdmissionState.INVALIDATED} + + second_batch = _batch("batch-rollover-2", (_occurrence("rollover-2", maximum=1),)) + second_proof = replace( + proof, + old_window_epoch_id=WindowEpochId("epoch-2"), + old_window_epoch_number=2, + new_window_epoch_id=WindowEpochId("epoch-3"), + new_window_epoch_number=3, + fence_witness_id=AdmissionWitnessId("fence-2-to-3"), + ) + second = RolloverEpochEvent( + **_event_fields(rolled.next_state, "rollover-2-to-3", "rollover-epoch"), + witness=_witness(second_batch, WitnessKind.EPOCH_ROLLOVER, epoch=2), + fence_proof=second_proof, + new_snapshot=_snapshot(epoch=3, model="claude-new", tokenizer="tokenizer-new"), + protected_pools=(), + ) + rerolled = reduce_context_admission(rolled.next_state, second) + assert isinstance(rerolled.next_state, ActiveContextAdmissionState) + assert len(rerolled.next_state.closed_epochs) == 2 + assert rerolled.next_state.closed_epochs[0] == rolled.next_state.closed_epochs[0] + + +def _protected_pools() -> tuple[ProtectedPoolSpec, ...]: + return ( + ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=ProtectedPoolOwnerId("synthesis-owner"), + injected_count=20, + priority=10, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ), + ProtectedPoolSpec( + reserve_class=ReserveClass.FINAL_RESPONSE, + capability_owner_id=ProtectedPoolOwnerId("final-owner"), + injected_count=10, + priority=20, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ), + ) + + +def test_protected_pools_are_isolated_without_double_subtracting_usage() -> None: + state = _open_epoch(remaining_count=100, protected_pools=_protected_pools()) + synthesis = _occurrence("synthesis", maximum=15, reserve_class=ReserveClass.SYNTHESIS) + state, _ = _propose(state, synthesis) + synthesis_batch = _batch( + "batch-synthesis", + (synthesis,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner="synthesis-owner", + ) + synthesis_transition, _ = _reserve( + state, + synthesis_batch, + (synthesis,), + input_count=10, + generation_count=5, + ) + assert synthesis_transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(synthesis_transition.next_state, ActiveContextAdmissionState) + + ordinary = _occurrence("ordinary-after-protected", maximum=70) + state, _ = _propose(synthesis_transition.next_state, ordinary) + ordinary_batch = _batch("batch-ordinary-after-protected", (ordinary,)) + exact_fit, _ = _reserve(state, ordinary_batch, (ordinary,), input_count=70) + assert exact_fit.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + + +@pytest.mark.parametrize( + ("authority_state", "expected"), + [ + (CoverageState.PARTIAL, AdmissionDecisionKind.WATERMARK_UNAVAILABLE), + (CoverageState.UPSTREAM_GATED, AdmissionDecisionKind.UPSTREAM_GATED), + ], +) +def test_authority_unavailable_never_creates_spendable_capacity( + authority_state: CoverageState, expected: AdmissionDecisionKind +) -> None: + state = _uninitialized() + event = AuthorityUnavailableEvent( + **_event_fields(state, f"authority-{authority_state.value}", "authority-unavailable"), + reason_code="no-atomic-watermark", + authority_state=authority_state, + ) + transition = reduce_context_admission(state, event) + assert transition.decision.kind is expected + assert isinstance(transition.next_state, UninitializedContextAdmissionState) + assert not hasattr(transition.next_state, "snapshot") + + +def test_full_stream_replay_uses_each_next_state_as_the_only_next_input() -> None: + initial = _uninitialized() + open_event = OpenEpochEvent( + **_event_fields(initial, "replay-open", "open-epoch"), + snapshot=_snapshot(), + protected_pools=(), + ) + opened = reduce_context_admission(initial, open_event) + assert isinstance(opened.next_state, ActiveContextAdmissionState) + occurrence = _occurrence("replay", maximum=10) + propose_event = ProposeOccurrenceEvent( + **_event_fields(opened.next_state, "replay-propose", "propose-occurrence"), + occurrence=occurrence, + ) + proposed = reduce_context_admission(opened.next_state, propose_event) + assert isinstance(proposed.next_state, ActiveContextAdmissionState) + batch = _batch("batch-replay", (occurrence,)) + reserve_event = ReserveRequestEvent( + **_event_fields(proposed.next_state, "replay-reserve", "reserve-request"), + batch=batch, + snapshot_sequence=proposed.next_state.snapshot.snapshot_sequence, + input_reservations=(_reservation(batch, (occurrence,), count=10),), + generation_reservation=_generation_reservation(batch, maximum=0), + ) + sequential = reduce_context_admission(proposed.next_state, reserve_event) + replay = replay_context_admission(initial, (open_event, propose_event, reserve_event)) + assert replay.final_state == sequential.next_state + assert replay.transitions[-1] == sequential + restored = type(replay).from_dict(replay.to_dict()) + assert restored == replay + + +def test_idempotency_expiry_is_explicit_and_does_not_release_capacity() -> None: + state, batch, occurrences, _ = _reserved_batch(name="expiry") + reservation = _reservation(batch, occurrences, count=25) + event = ExpireIdempotencyKeyEvent( + **_event_fields(state, "expire-active", "expire-idempotency-key"), + reservation_key=reservation.key, + expiry_witness=_witness(batch, WitnessKind.IDEMPOTENCY_EXPIRY), + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.next_state == state + + +def test_generation_indeterminate_remains_reserved_across_reconciliation_deadline() -> None: + state, batch, _, generation = _reserved_batch(name="generation-crash") + event = MarkGenerationIndeterminateEvent( + **_event_fields(state, "mark-generation-indeterminate", "mark-generation-indeterminate"), + generation_reservation_id=generation.generation_reservation_id, + reason_code="stream-disconnected", + ) + marked = reduce_context_admission(state, event) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + record = _generation_record(marked.next_state, generation.generation_reservation_id) + assert record.state is GenerationState.INDETERMINATE + assert record.maximum_allowance == 20 + + query = RequestReconciliationEvent( + **_event_fields( + marked.next_state, + "query-generation-indeterminate", + "request-reconciliation", + ), + target_id=generation.generation_reservation_id, + reason_code="deadline-observed", + ) + queried = reduce_context_admission(marked.next_state, query) + assert isinstance(queried.next_state, ActiveContextAdmissionState) + assert ( + _generation_record( + queried.next_state, generation.generation_reservation_id + ).maximum_allowance + == 20 + ) + assert not any("Released" in type(effect).__name__ for effect in queried.effects) + + +def test_resolve_indeterminate_acceptance_reconciles_exact_charge() -> None: + state, batch, _, _ = _reserved_batch(name="resolve-accepted") + state = _prepare_dispatch(state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-resolve-accepted", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + resolution = ResolveIndeterminateAcceptedEvent( + **_event_fields( + marked.next_state, + "resolve-accepted", + "resolve-indeterminate-accepted", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + exact_charge=19, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("provider-test"), + ) + resolved = reduce_context_admission(marked.next_state, resolution) + assert isinstance(resolved.next_state, ActiveContextAdmissionState) + assert _batch_record(resolved.next_state, batch).committed_input_count == 19 + assert {record.state for record in _records_for(resolved.next_state, batch)} == { + AdmissionState.COMMITTED + } diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py new file mode 100644 index 000000000..a87165aff --- /dev/null +++ b/tests/core/test_context_admission_state_machine.py @@ -0,0 +1,485 @@ +"""Bounded model-based checks for cumulative context-admission accounting.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import Any + +import pytest +from hypothesis import settings +from hypothesis import strategies as st +from hypothesis.stateful import RuleBasedStateMachine, invariant, precondition, rule + +from autoskillit.core import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION, + ActiveContextAdmissionState, + AdmissionAttemptId, + AdmissionBatch, + AdmissionBatchId, + AdmissionDecisionKind, + AdmissionEventId, + AdmissionOccurrence, + AdmissionOccurrenceId, + AdmissionRequestId, + AdmissionReservation, + AdmissionReservationId, + AdmissionReservationKey, + AdmissionSequence, + AdmissionState, + AdmissionWitnessId, + AgentInstanceId, + AggregateRevision, + CanonicalRepresentationManifest, + CanonicalSpanId, + CanonicalSpanOwner, + ContextLineage, + ContextSessionId, + ContextThreadId, + ContextWindowSnapshot, + GenerationReservationId, + GenerationReservationRecord, + GenerationState, + IdempotencyNamespace, + ModelIdentity, + ModelItemId, + OpenEpochEvent, + ProducerInstanceId, + ProducerSurface, + ProposeOccurrenceEvent, + ProtectedPoolOwnerId, + ProtectedPoolSpec, + RepresentationRevision, + RequestReconciliationEvent, + ReserveClass, + ReserveRequestEvent, + TokenizerIdentity, + ToolCallId, + TurnId, + UninitializedContextAdmissionState, + WindowEpochId, + WitnessKind, + reduce_context_admission, +) + +pytestmark = [pytest.mark.layer("core"), pytest.mark.small] + + +_SLOT = st.integers(min_value=0, max_value=1) +_RESERVE_CLASS = st.sampled_from( + (ReserveClass.ORDINARY, ReserveClass.SYNTHESIS, ReserveClass.FINAL_RESPONSE) +) + + +def _namespace(operation_kind: str) -> IdempotencyNamespace: + return IdempotencyNamespace(caller_scope="state-machine", operation_kind=operation_kind) + + +def _uninitialized() -> UninitializedContextAdmissionState: + return UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + +def _snapshot() -> ContextWindowSnapshot: + return ContextWindowSnapshot( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + model_identity=ModelIdentity.anthropic("claude-state-machine"), + tokenizer_identity=TokenizerIdentity("tokenizer-state-machine"), + snapshot_sequence=1, + active_count=50, + hard_limit=100, + remaining_count=50, + ) + + +def _pool_specs() -> tuple[ProtectedPoolSpec, ...]: + return ( + ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=ProtectedPoolOwnerId("synthesis-owner"), + injected_count=12, + priority=10, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ), + ProtectedPoolSpec( + reserve_class=ReserveClass.FINAL_RESPONSE, + capability_owner_id=ProtectedPoolOwnerId("final-owner"), + injected_count=12, + priority=20, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ), + ) + + +def _owner(reserve_class: ReserveClass) -> ProtectedPoolOwnerId | None: + if reserve_class is ReserveClass.SYNTHESIS: + return ProtectedPoolOwnerId("synthesis-owner") + if reserve_class is ReserveClass.FINAL_RESPONSE: + return ProtectedPoolOwnerId("final-owner") + return None + + +def _occurrence(slot: int, reserve_class: ReserveClass) -> AdmissionOccurrence: + name = f"occurrence-{slot}" + surface = ( + ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY + if slot == 1 + else ProducerSurface.TOOL_RESULT_ENVELOPE + ) + return AdmissionOccurrence( + occurrence_id=AdmissionOccurrenceId(name), + lineage=ContextLineage( + root_session_id=ContextSessionId("session-root"), + current_session_id=ContextSessionId("session-child" if slot == 1 else "session-root"), + root_agent_id=AgentInstanceId("agent-root"), + current_agent_id=AgentInstanceId("agent-child" if slot == 1 else "agent-root"), + parent_agent_id=AgentInstanceId("agent-root") if slot == 1 else None, + root_thread_id=ContextThreadId("thread-root"), + current_thread_id=ContextThreadId("thread-child" if slot == 1 else "thread-root"), + parent_thread_id=ContextThreadId("thread-root") if slot == 1 else None, + fork_occurrence_id=None, + turn_id=TurnId(f"turn-{slot}"), + producer_surface=surface, + producer_instance_id=ProducerInstanceId(f"producer-{slot}"), + tool_call_id=ToolCallId(f"tool-{slot}"), + model_item_id=ModelItemId(f"item-{slot}"), + dispatch_identity=None, + attempt_id=AdmissionAttemptId(f"attempt-{slot}"), + delivery_occurrence_id=None, + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + ), + reserve_class=reserve_class, + producer_surface=surface, + predicted_authoritative_maximum=15, + representation_revision=RepresentationRevision(f"revision-{slot}"), + owned_span_ids=(CanonicalSpanId(f"span-{slot}"),), + ) + + +def _batch(occurrence: AdmissionOccurrence) -> AdmissionBatch: + slot = occurrence.occurrence_id.value.rsplit("-", 1)[-1] + request_id = AdmissionRequestId(f"request-{slot}") + manifest = CanonicalRepresentationManifest( + request_id=request_id, + representation_revision=occurrence.representation_revision, + span_owners=tuple( + CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) + for span_id in occurrence.owned_span_ids + ), + assembler_identity=ProducerInstanceId(f"assembler-{slot}"), + assembler_witness_id=AdmissionWitnessId(f"assembler-witness-{slot}"), + ) + return AdmissionBatch( + batch_id=AdmissionBatchId(f"batch-{slot}"), + request_id=request_id, + occurrence_ids=(occurrence.occurrence_id,), + reserve_class=occurrence.reserve_class, + protected_pool_owner_id=_owner(occurrence.reserve_class), + manifest=manifest, + ) + + +def _reservation( + occurrence: AdmissionOccurrence, batch: AdmissionBatch, count: int +) -> AdmissionReservation: + key = AdmissionReservationKey( + idempotency_namespace=_namespace("reserve-request"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + batch_id=batch.batch_id, + reserve_class=occurrence.reserve_class, + protected_pool_owner_id=_owner(occurrence.reserve_class), + occurrence_revisions=((occurrence.occurrence_id, occurrence.representation_revision),), + ) + return AdmissionReservation( + reservation_id=AdmissionReservationId(f"reservation-{occurrence.occurrence_id.value}"), + key=key, + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + snapshot_sequence=1, + reserve_class=occurrence.reserve_class, + protected_pool_owner_id=_owner(occurrence.reserve_class), + occurrence_ids=(occurrence.occurrence_id,), + reserved_count=count, + ) + + +def _generation( + occurrence: AdmissionOccurrence, batch: AdmissionBatch, count: int +) -> GenerationReservationRecord: + return GenerationReservationRecord( + generation_reservation_id=GenerationReservationId( + f"generation-{occurrence.occurrence_id.value}" + ), + request_id=batch.request_id, + response_id=ModelItemId(f"response-{occurrence.occurrence_id.value}"), + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + snapshot_sequence=1, + reserve_class=occurrence.reserve_class, + protected_pool_owner_id=_owner(occurrence.reserve_class), + maximum_allowance=count, + state=GenerationState.RESERVED, + exact_terminal_usage=None, + witness_ids=(), + ) + + +class ContextAdmissionStateMachine(RuleBasedStateMachine): + """Compare reducer transitions with a deliberately small accounting oracle.""" + + def __init__(self) -> None: + super().__init__() + initial = _uninitialized() + open_event = OpenEpochEvent( + event_id=AdmissionEventId("open-state-machine"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + idempotency_namespace=_namespace("open-epoch"), + expected_aggregate_revision=initial.aggregate_revision, + snapshot=_snapshot(), + protected_pools=_pool_specs(), + ) + opened = reduce_context_admission(initial, open_event) + assert opened.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(opened.next_state, ActiveContextAdmissionState) + self.state = opened.next_state + self.occurrences: dict[int, AdmissionOccurrence] = {} + self.charges: dict[int, tuple[ReserveClass, int, int]] = {} + self.latest_replayable_event: Any | None = open_event + self.latest_propose_event: ProposeOccurrenceEvent | None = None + self.event_sequence = 0 + self.last_revision = self.state.aggregate_revision.value + self.last_admission_sequence = self.state.admission_sequence.value + self.closed_audit_count = len(self.state.closed_epochs) + + def _fields(self, operation_kind: str) -> dict[str, object]: + self.event_sequence += 1 + return { + "event_id": AdmissionEventId(f"event-{self.event_sequence}"), + "protocol_version": CONTEXT_ADMISSION_PROTOCOL_VERSION, + "idempotency_namespace": _namespace(operation_kind), + "expected_aggregate_revision": self.state.aggregate_revision, + } + + def _accept_publication(self, transition: Any, event: object) -> None: + assert isinstance(transition.next_state, ActiveContextAdmissionState) + assert ( + transition.next_state.aggregate_revision.value >= self.state.aggregate_revision.value + ) + assert ( + transition.next_state.admission_sequence.value >= self.state.admission_sequence.value + ) + self.state = transition.next_state + self.last_revision = self.state.aggregate_revision.value + self.last_admission_sequence = self.state.admission_sequence.value + self.latest_replayable_event = event + + def _availability(self, reserve_class: ReserveClass) -> tuple[int, int, int]: + total_charged = sum( + input_count + output_count for _, input_count, output_count in self.charges.values() + ) + global_unallocated = 50 - total_charged + synthesis_used = sum( + input_count + output_count + for charged_class, input_count, output_count in self.charges.values() + if charged_class is ReserveClass.SYNTHESIS + ) + final_used = sum( + input_count + output_count + for charged_class, input_count, output_count in self.charges.values() + if charged_class is ReserveClass.FINAL_RESPONSE + ) + synthesis_unused = 12 - synthesis_used + final_unused = 12 - final_used + ordinary_available = global_unallocated - synthesis_unused - final_unused + if reserve_class is ReserveClass.SYNTHESIS: + class_available = min(global_unallocated, synthesis_unused) + elif reserve_class is ReserveClass.FINAL_RESPONSE: + class_available = min(global_unallocated, final_unused) + else: + class_available = ordinary_available + return global_unallocated, ordinary_available, class_available + + @rule(slot=_SLOT, reserve_class=_RESERVE_CLASS) + def propose(self, slot: int, reserve_class: ReserveClass) -> None: + if slot in self.occurrences: + return + occurrence = _occurrence(slot, reserve_class) + event = ProposeOccurrenceEvent(**self._fields("propose-occurrence"), occurrence=occurrence) + transition = reduce_context_admission(self.state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + self.occurrences[slot] = occurrence + self.latest_propose_event = event + + @rule( + slot=_SLOT, + input_count=st.integers(min_value=1, max_value=15), + generation_count=st.integers(min_value=0, max_value=8), + ) + def reserve(self, slot: int, input_count: int, generation_count: int) -> None: + if slot not in self.occurrences or slot in self.charges: + return + occurrence = self.occurrences[slot] + batch = _batch(occurrence) + event = ReserveRequestEvent( + **self._fields("reserve-request"), + batch=batch, + snapshot_sequence=1, + input_reservations=(_reservation(occurrence, batch, input_count),), + generation_reservation=_generation(occurrence, batch, generation_count), + ) + prior = self.state + transition = reduce_context_admission(prior, event) + _, _, class_available = self._availability(occurrence.reserve_class) + expected_admit = input_count + generation_count <= class_available + assert transition.decision.kind is ( + AdmissionDecisionKind.WOULD_ADMIT + if expected_admit + else AdmissionDecisionKind.WOULD_REJECT + ) + if expected_admit: + self._accept_publication(transition, event) + self.charges[slot] = ( + occurrence.reserve_class, + input_count, + generation_count, + ) + else: + assert transition.next_state == prior + + @rule(slot=_SLOT) + def stale_fence_is_rejected(self, slot: int) -> None: + if slot not in self.occurrences or slot in self.charges: + return + occurrence = self.occurrences[slot] + batch = _batch(occurrence) + event = ReserveRequestEvent( + **{ + **self._fields("reserve-request"), + "expected_aggregate_revision": AggregateRevision( + max(0, self.state.aggregate_revision.value - 1) + ), + }, + batch=batch, + snapshot_sequence=1, + input_reservations=(_reservation(occurrence, batch, 1),), + generation_reservation=_generation(occurrence, batch, 0), + ) + prior = self.state + transition = reduce_context_admission(prior, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert transition.next_state == prior + + @precondition(lambda self: self.latest_replayable_event is not None) + @rule() + def identical_replay_is_equivalent(self) -> None: + assert self.latest_replayable_event is not None + event_type = type(self.latest_replayable_event) + serialized_event = self.latest_replayable_event.to_dict() + restored_event = event_type.from_dict(serialized_event) + transition = reduce_context_admission(self.state, restored_event) + assert transition.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert transition.next_state == self.state + + @precondition(lambda self: self.latest_propose_event is not None) + @rule() + def changed_intent_conflict_does_not_mutate(self) -> None: + assert self.latest_propose_event is not None + changed = replace( + self.latest_propose_event, + occurrence=replace( + self.latest_propose_event.occurrence, + representation_revision=RepresentationRevision( + f"conflicting-{self.event_sequence}" + ), + ), + ) + transition = reduce_context_admission(self.state, changed) + assert transition.decision.kind is AdmissionDecisionKind.CONFLICT + assert transition.next_state == self.state + + @precondition(lambda self: bool(self.charges)) + @rule() + def reconciliation_deadline_never_releases(self) -> None: + slot = min(self.charges) + occurrence = self.occurrences[slot] + before = dict(self.charges) + event = RequestReconciliationEvent( + **self._fields("request-reconciliation"), + target_id=_batch(occurrence).batch_id, + reason_code="deadline-observed", + ) + transition = reduce_context_admission(self.state, event) + assert not any("Released" in type(effect).__name__ for effect in transition.effects) + self._accept_publication(transition, event) + assert self.charges == before + + @invariant() + def capacity_is_non_negative_and_protected_pools_are_isolated(self) -> None: + global_unallocated, ordinary_available, _ = self._availability(ReserveClass.ORDINARY) + assert global_unallocated >= 0 + assert ordinary_available >= 0 + for reserve_class in ( + ReserveClass.SYNTHESIS, + ReserveClass.FINAL_RESPONSE, + ): + _, _, pool_available = self._availability(reserve_class) + assert pool_available >= 0 + + @invariant() + def state_round_trip_is_canonical(self) -> None: + restored = ActiveContextAdmissionState.from_dict(self.state.to_dict()) + assert restored == self.state + assert restored.to_dict() == self.state.to_dict() + + @invariant() + def revisions_sequences_and_closed_audits_are_monotonic(self) -> None: + assert self.state.aggregate_revision.value >= self.last_revision + assert self.state.admission_sequence.value >= self.last_admission_sequence + assert len(self.state.closed_epochs) >= self.closed_audit_count + self.last_revision = self.state.aggregate_revision.value + self.last_admission_sequence = self.state.admission_sequence.value + self.closed_audit_count = len(self.state.closed_epochs) + + @invariant() + def no_batch_is_partially_reserved_or_double_charged(self) -> None: + charged_occurrences = {self.occurrences[slot].occurrence_id for slot in self.charges} + assert len(charged_occurrences) == len(self.charges) + for record in self.state.batch_records: + member_states = { + occurrence_record.state + for occurrence_record in self.state.occurrence_records + if occurrence_record.occurrence.occurrence_id in record.batch.occurrence_ids + } + assert len(member_states) == 1 + assert member_states <= { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.COMMITTED, + AdmissionState.RELEASED, + AdmissionState.ROLLED_BACK, + AdmissionState.INVALIDATED, + AdmissionState.INDETERMINATE, + AdmissionState.QUARANTINED, + } + + +ContextAdmissionStateMachine.TestCase.settings = settings( + max_examples=24, + stateful_step_count=12, + deadline=None, +) +TestContextAdmissionStateMachine = ContextAdmissionStateMachine.TestCase diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py new file mode 100644 index 000000000..ac9bc213a --- /dev/null +++ b/tests/core/types/test_context_admission_contract.py @@ -0,0 +1,608 @@ +"""Freeze the protocol-v1 cumulative context-admission value contract.""" + +from __future__ import annotations + +import dataclasses +from dataclasses import FrozenInstanceError, fields, replace +from typing import get_args + +import pytest + +from autoskillit.core import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION, + ActiveContextAdmissionState, + AdmissionAttemptId, + AdmissionBatchId, + AdmissionDecision, + AdmissionDecisionKind, + AdmissionEffect, + AdmissionEventId, + AdmissionOccurrence, + AdmissionOccurrenceId, + AdmissionRequestId, + AdmissionReservationId, + AdmissionReservationKey, + AdmissionSequence, + AdmissionState, + AdmissionTransition, + AdmissionWitnessId, + AgentInstanceId, + AggregateRevision, + AuthoritySourceId, + CanonicalRepresentationManifest, + CanonicalSpanId, + CanonicalSpanOwner, + ChargeDomain, + ClosedEpochAudit, + ContextAdmissionEvent, + ContextAdmissionState, + ContextAdmissionValidationError, + ContextLineage, + ContextSessionId, + ContextThreadId, + ContextWindowSnapshot, + CoverageEvidenceKind, + CoverageState, + DeliveryOccurrenceId, + DispatchIdentity, + ForkOccurrenceId, + GenerationReservationId, + GenerationState, + IdempotencyNamespace, + MeasurementKind, + ModelIdentity, + ModelItemId, + ProducerInstanceId, + ProducerSurface, + ProtectedPoolOwnerId, + ProtectedPoolSpec, + RepresentationRevision, + ReserveClass, + TokenizerIdentity, + ToolCallId, + TurnId, + UninitializedContextAdmissionState, + UnsupportedContextAdmissionProtocolError, + WindowEpochId, + WitnessKind, +) + +pytestmark = [pytest.mark.layer("core"), pytest.mark.small] + + +EXPECTED_ENUM_MEMBERS = { + AdmissionState: ( + "PROPOSED", + "RESERVED", + "PREPARED", + "HISTORY_STAGED", + "REQUEST_DISPATCHED", + "COMMITTED", + "RELEASED", + "ROLLED_BACK", + "INVALIDATED", + "INDETERMINATE", + "QUARANTINED", + ), + AdmissionDecisionKind: ( + "WOULD_ADMIT", + "WOULD_REJECT", + "WATERMARK_UNAVAILABLE", + "UPSTREAM_GATED", + "NOOP_IDEMPOTENT", + "CONFLICT", + "IDEMPOTENCY_EXPIRED", + "QUARANTINED", + ), + ChargeDomain: ("INPUT_CONTEXT", "OUTPUT_GENERATION"), + GenerationState: ( + "RESERVED", + "STREAMING", + "RECONCILED", + "INDETERMINATE", + "QUARANTINED", + ), + MeasurementKind: ( + "PROVIDER_EXACT", + "TOKENIZER_EXACT", + "HOST_ESTIMATE", + "BYTE_EMERGENCY", + ), + CoverageState: ("VERIFIED", "PARTIAL", "UPSTREAM_GATED"), + CoverageEvidenceKind: ( + "AUTOSKILLIT_SOURCE", + "CODEX_SOURCE", + "CODEX_OFFICIAL_DOC", + "CODEX_RUNTIME_PROBE", + "INFERENCE", + ), + ReserveClass: ("ORDINARY", "SYNTHESIS", "FINAL_RESPONSE"), + WitnessKind: ( + "EPOCH_SNAPSHOT", + "INPUT_COUNTED", + "HISTORY_STAGED", + "REPRESENTATION_BOUND", + "REQUEST_INCLUDED", + "PROVIDER_ACCEPTED", + "OUTPUT_USAGE", + "TRUNCATION", + "NON_ADMISSION", + "ROLLBACK", + "RECONCILIATION", + "IDEMPOTENCY_EXPIRY", + "EPOCH_FENCE", + "EPOCH_ROLLOVER", + ), +} + +EXPECTED_PRODUCER_SURFACES = ( + "NATIVE_SHELL", + "UNIFIED_EXEC_AND_WRITE_STDIN", + "APPLY_PATCH", + "AUTOSKILLIT_MCP", + "EXTERNAL_MCP", + "AUTOSKILLIT_LOCAL_FUNCTION", + "OTHER_LOCAL_FUNCTION", + "MCP_RESOURCE", + "CLIENT_PROVIDER_RETRIEVAL", + "CODE_MODE_AGGREGATE", + "HOSTED_SPECIALIZED_TOOL", + "HOOK_FEEDBACK", + "TOOL_ARGUMENT", + "TOOL_RESULT_ENVELOPE", + "USER_PROMPT", + "ASSISTANT_OUTPUT_HISTORY", + "SKILL_PLUGIN_CONTEXT", + "OTHER_CONTEXT_INJECTION", + "HEADLESS_CHILD_PROMPT", + "PARENT_VISIBLE_CHILD_DELIVERY", + "COMPACTION_MODEL_WINDOW_TRANSITION", +) + +EXPECTED_EVENT_TYPES = { + "OpenEpochEvent", + "AuthorityUnavailableEvent", + "ProposeOccurrenceEvent", + "ReserveRequestEvent", + "PrepareBatchEvent", + "StageHistoryEvent", + "DispatchRequestEvent", + "AcceptInputEvent", + "ReleaseNonAdmissionEvent", + "RollbackAdmissionEvent", + "MarkIndeterminateEvent", + "ResolveIndeterminateAcceptedEvent", + "ResolveIndeterminateNonAdmissionEvent", + "ResolveIndeterminateRollbackEvent", + "StartGenerationEvent", + "ReconcileGenerationEvent", + "MarkGenerationIndeterminateEvent", + "RequestReconciliationEvent", + "ExpireIdempotencyKeyEvent", + "RolloverEpochEvent", +} + +EXPECTED_EFFECT_TYPES = { + "ReservationRecordedEffect", + "ReservationReleasedEffect", + "OccurrenceStateChangedEffect", + "ChargeCommittedEffect", + "GenerationReservationRecordedEffect", + "GenerationReconciledEffect", + "ReconciliationQueryRequestedEffect", + "ReconciliationEscalationEffect", + "ConflictRejectedEffect", + "IdempotencyExpiredEffect", + "ReservationInvalidatedEffect", + "EpochClosedEffect", + "QuarantineRecordedEffect", + "AuthorityUnavailableEffect", +} + +OPAQUE_STRING_TYPES = ( + ContextSessionId, + AgentInstanceId, + ContextThreadId, + ForkOccurrenceId, + TurnId, + ProducerInstanceId, + ToolCallId, + ModelItemId, + AdmissionRequestId, + AdmissionBatchId, + WindowEpochId, + TokenizerIdentity, + CanonicalSpanId, + AdmissionOccurrenceId, + AdmissionAttemptId, + DeliveryOccurrenceId, + AdmissionEventId, + AdmissionReservationId, + AdmissionWitnessId, + AuthoritySourceId, + GenerationReservationId, + ProtectedPoolOwnerId, + RepresentationRevision, +) + + +def _field_names(value_type: type[object]) -> tuple[str, ...]: + return tuple(field.name for field in fields(value_type)) + + +def _lineage( + *, + surface: ProducerSurface = ProducerSurface.TOOL_RESULT_ENVELOPE, + span_suffix: str = "1", + dispatch_identity: DispatchIdentity | None = None, +) -> ContextLineage: + return ContextLineage( + root_session_id=ContextSessionId("session-root"), + current_session_id=ContextSessionId("session-current"), + root_agent_id=AgentInstanceId("agent-root"), + current_agent_id=AgentInstanceId("agent-current"), + parent_agent_id=None, + root_thread_id=ContextThreadId("thread-root"), + current_thread_id=ContextThreadId("thread-current"), + parent_thread_id=None, + fork_occurrence_id=None, + turn_id=TurnId("turn-1"), + producer_surface=surface, + producer_instance_id=ProducerInstanceId(f"producer-{span_suffix}"), + tool_call_id=ToolCallId(f"tool-{span_suffix}"), + model_item_id=ModelItemId(f"item-{span_suffix}"), + dispatch_identity=dispatch_identity, + attempt_id=AdmissionAttemptId(f"attempt-{span_suffix}"), + delivery_occurrence_id=None, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + ) + + +def _occurrence( + occurrence_id: str = "occurrence-1", + *, + span_id: str = "span-1", + revision: str = "representation-1", + surface: ProducerSurface = ProducerSurface.TOOL_RESULT_ENVELOPE, +) -> AdmissionOccurrence: + return AdmissionOccurrence( + occurrence_id=AdmissionOccurrenceId(occurrence_id), + lineage=_lineage(surface=surface, span_suffix=occurrence_id), + reserve_class=ReserveClass.ORDINARY, + producer_surface=surface, + predicted_authoritative_maximum=7, + representation_revision=RepresentationRevision(revision), + owned_span_ids=(CanonicalSpanId(span_id),), + ) + + +def _manifest(*occurrences: AdmissionOccurrence) -> CanonicalRepresentationManifest: + return CanonicalRepresentationManifest( + request_id=AdmissionRequestId("request-1"), + representation_revision=RepresentationRevision("representation-1"), + span_owners=tuple( + CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) + for occurrence in occurrences + for span_id in occurrence.owned_span_ids + ), + assembler_identity=ProducerInstanceId("assembler-1"), + assembler_witness_id=AdmissionWitnessId("assembler-witness-1"), + ) + + +def _snapshot( + *, + protocol_version: int = CONTEXT_ADMISSION_PROTOCOL_VERSION, + model_identity: ModelIdentity | None = None, +) -> ContextWindowSnapshot: + return ContextWindowSnapshot( + protocol_version=protocol_version, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + model_identity=model_identity or ModelIdentity.anthropic("claude-test"), + tokenizer_identity=TokenizerIdentity("tokenizer-test"), + snapshot_sequence=1, + active_count=60, + hard_limit=100, + remaining_count=40, + ) + + +def _uninitialized() -> UninitializedContextAdmissionState: + return UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + +@pytest.mark.parametrize(("enum_type", "expected"), EXPECTED_ENUM_MEMBERS.items()) +def test_protocol_v1_enum_members_are_exact( + enum_type: type[object], expected: tuple[str, ...] +) -> None: + assert tuple(enum_type.__members__) == expected + assert len(tuple(enum_type)) == len(enum_type.__members__) + + +def test_producer_surface_is_the_independently_frozen_literal_set() -> None: + assert tuple(ProducerSurface.__members__) == EXPECTED_PRODUCER_SURFACES + + +def test_event_and_effect_unions_are_closed() -> None: + assert {event_type.__name__ for event_type in get_args(ContextAdmissionEvent)} == ( + EXPECTED_EVENT_TYPES + ) + assert {effect_type.__name__ for effect_type in get_args(AdmissionEffect)} == ( + EXPECTED_EFFECT_TYPES + ) + assert set(get_args(ContextAdmissionState)) == { + UninitializedContextAdmissionState, + ActiveContextAdmissionState, + } + + +@pytest.mark.parametrize( + ("record_type", "expected_fields"), + [ + ( + UninitializedContextAdmissionState, + ( + "protocol_version", + "aggregate_revision", + "admission_sequence", + "processed_events", + "idempotency_records", + "expired_idempotency_tombstones", + "closed_epochs", + ), + ), + ( + ActiveContextAdmissionState, + ( + "protocol_version", + "aggregate_revision", + "admission_sequence", + "snapshot", + "protected_pools", + "occurrence_records", + "batch_records", + "reservations", + "generation_reservations", + "processed_events", + "idempotency_records", + "expired_idempotency_tombstones", + "closed_epochs", + ), + ), + ( + AdmissionDecision, + ( + "kind", + "reason_code", + "window_epoch_id", + "snapshot_sequence", + "requested_count", + "available_ordinary_count", + "available_protected_count", + ), + ), + (AdmissionTransition, ("next_state", "decision", "effects")), + ], +) +def test_frozen_record_fields_are_exact( + record_type: type[object], expected_fields: tuple[str, ...] +) -> None: + assert _field_names(record_type) == expected_fields + + +def test_lineage_fields_keep_all_identity_domains_separate() -> None: + assert _field_names(ContextLineage) == ( + "root_session_id", + "current_session_id", + "root_agent_id", + "current_agent_id", + "parent_agent_id", + "root_thread_id", + "current_thread_id", + "parent_thread_id", + "fork_occurrence_id", + "turn_id", + "producer_surface", + "producer_instance_id", + "tool_call_id", + "model_item_id", + "dispatch_identity", + "attempt_id", + "delivery_occurrence_id", + "window_epoch_id", + "window_epoch_number", + ) + + +@pytest.mark.parametrize("opaque_type", OPAQUE_STRING_TYPES) +def test_opaque_string_identifiers_are_frozen_content_free_values( + opaque_type: type[object], +) -> None: + value = opaque_type("opaque-123") + assert value.value == "opaque-123" + assert value.from_dict(value.to_dict()) == value + with pytest.raises((FrozenInstanceError, AttributeError)): + value.value = "changed" + + +@pytest.mark.parametrize("revision_type", [AggregateRevision, AdmissionSequence]) +def test_numeric_revisions_are_non_negative_and_have_no_default( + revision_type: type[object], +) -> None: + assert revision_type(0).value == 0 + with pytest.raises(ContextAdmissionValidationError): + revision_type(-1) + assert all(field.default is dataclasses.MISSING for field in fields(revision_type)) + + +def test_dispatch_identity_is_validated_and_projects_only_dispatch_id() -> None: + identity = DispatchIdentity.from_dispatch_id("dispatch-12345678") + lineage = _lineage(dispatch_identity=identity) + serialized = lineage.to_dict() + assert serialized["dispatch_identity"] == {"dispatch_id": identity.dispatch_id} + assert ContextLineage.from_dict(serialized) == lineage + + forged = replace(identity, completion_marker="forged-private-marker") + with pytest.raises(ContextAdmissionValidationError) as exc_info: + replace(lineage, dispatch_identity=forged) + assert "forged-private-marker" not in str(exc_info.value) + + +def test_authoritative_snapshot_reuses_only_known_model_identity() -> None: + snapshot = _snapshot() + assert ContextWindowSnapshot.from_dict(snapshot.to_dict()) == snapshot + assert snapshot.model_identity == ModelIdentity.anthropic("claude-test") + with pytest.raises(ContextAdmissionValidationError): + _snapshot(model_identity=ModelIdentity.unknown()) + + +def test_uninitialized_state_is_explicitly_non_spendable() -> None: + state = _uninitialized() + assert not isinstance(state, ActiveContextAdmissionState) + assert not hasattr(state, "snapshot") + assert not hasattr(state, "reservations") + assert UninitializedContextAdmissionState.from_dict(state.to_dict()) == state + + +def test_reservation_key_is_attempt_independent_and_revision_sensitive() -> None: + key = AdmissionReservationKey( + idempotency_namespace=IdempotencyNamespace( + caller_scope="caller-1", operation_kind="reserve-request" + ), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + batch_id=AdmissionBatchId("batch-1"), + reserve_class=ReserveClass.ORDINARY, + protected_pool_owner_id=None, + occurrence_revisions=( + ( + AdmissionOccurrenceId("occurrence-1"), + RepresentationRevision("representation-1"), + ), + ), + ) + assert "attempt" not in _field_names(AdmissionReservationKey) + assert key.from_dict(key.to_dict()) == key + assert ( + replace( + key, + occurrence_revisions=( + ( + AdmissionOccurrenceId("occurrence-1"), + RepresentationRevision("representation-2"), + ), + ), + ) + != key + ) + + +def test_protected_pool_policy_is_injected_and_has_no_borrowing_or_defaults() -> None: + assert _field_names(ProtectedPoolSpec) == ( + "reserve_class", + "capability_owner_id", + "injected_count", + "priority", + "required_release_witness_kind", + ) + assert "borrow" not in " ".join(_field_names(ProtectedPoolSpec)) + assert all(field.default is dataclasses.MISSING for field in fields(ProtectedPoolSpec)) + with pytest.raises(ContextAdmissionValidationError): + ProtectedPoolSpec( + reserve_class=ReserveClass.ORDINARY, + capability_owner_id=ProtectedPoolOwnerId("ordinary-must-not-own-a-pool"), + injected_count=1, + priority=1, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ) + + +def test_manifest_rejects_overlapping_span_ownership() -> None: + tool_argument = _occurrence( + "tool-argument", + span_id="shared-span", + surface=ProducerSurface.TOOL_ARGUMENT, + ) + assistant_history = _occurrence( + "assistant-history", + span_id="shared-span", + surface=ProducerSurface.ASSISTANT_OUTPUT_HISTORY, + ) + with pytest.raises(ContextAdmissionValidationError): + _manifest(tool_argument, assistant_history) + + +def test_contract_values_are_deeply_immutable() -> None: + occurrence = _occurrence() + assert isinstance(occurrence.owned_span_ids, tuple) + with pytest.raises((FrozenInstanceError, AttributeError)): + occurrence.predicted_authoritative_maximum = 100 + + def assert_no_mutable_collections(value: object) -> None: + if dataclasses.is_dataclass(value): + for field in fields(value): + assert_no_mutable_collections(getattr(value, field.name)) + elif isinstance(value, tuple | frozenset): + for child in value: + assert_no_mutable_collections(child) + else: + assert not isinstance(value, list | dict | set) + + assert_no_mutable_collections(_manifest(occurrence)) + + +@pytest.mark.parametrize( + "canary", + [ + "payload: private user message", + "/home/alice/private/context.txt", + "Bearer secret-token-value", + f"sha256:{'a' * 64}", + ], +) +def test_privacy_canaries_never_escape_validation_repr_or_serialization(canary: str) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId(canary) + assert canary not in str(exc_info.value) + assert canary not in repr(exc_info.value) + + safe = _lineage() + rendered = repr(safe) + serialized = repr(safe.to_dict()) + assert canary not in rendered + assert canary not in serialized + + +def test_unknown_protocol_versions_fail_closed() -> None: + assert CONTEXT_ADMISSION_PROTOCOL_VERSION == 1 + with pytest.raises(UnsupportedContextAdmissionProtocolError): + replace(_uninitialized(), protocol_version=2) + with pytest.raises(UnsupportedContextAdmissionProtocolError): + _snapshot(protocol_version=2) + + +def test_closed_epoch_audits_survive_state_serialization() -> None: + empty_audit = ClosedEpochAudit( + snapshot=_snapshot(), + terminal_occurrence_records=(), + terminal_reservations=(), + closure_witness_id=AdmissionWitnessId("closure-witness-1"), + fence_proof=None, + processed_event_tombstones=(), + retained_unresolved_count=0, + ) + state = replace(_uninitialized(), closed_epochs=(empty_audit,)) + restored = UninitializedContextAdmissionState.from_dict(state.to_dict()) + assert restored == state + assert restored.closed_epochs == (empty_audit,) diff --git a/tests/docs/AGENTS.md b/tests/docs/AGENTS.md index 6f32025d6..6cdb3b84d 100644 --- a/tests/docs/AGENTS.md +++ b/tests/docs/AGENTS.md @@ -25,3 +25,4 @@ Documentation integrity, link validity, and naming convention tests. | `test_output_budget_protocol_decision.py` | Ratchet ADR-0005 limits, accepted gaps, operational signals, corrections, and forward obligations | | `test_recipe_redelivery_decision.py` | ADR-0004 recipe pull pagination identity and reconstruction contracts | | `test_check_sub_claude_md_script.py` | Unit and integration tests for the check_sub_claude_md.py pre-commit hook script | +| `test_context_admission_decision.py` | Ratchet ADR-0007 context-admission authority, evidence, traceability, and downstream ownership | diff --git a/tests/docs/test_context_admission_decision.py b/tests/docs/test_context_admission_decision.py new file mode 100644 index 000000000..e71993bf3 --- /dev/null +++ b/tests/docs/test_context_admission_decision.py @@ -0,0 +1,310 @@ +"""Ratchet the accepted context-admission contract decision.""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[2] +DECISION = REPO_ROOT / "docs/decisions/0007-context-admission.md" +DECISION_INDEX = REPO_ROOT / "docs/decisions/README.md" + +pytestmark = pytest.mark.small + +CODEX_VERSION = "0.145.0" +CODEX_REVISION = "25af12f7e61572b0bc18ddb1008be543b91519b0" + +REQUIRED_HEADINGS = ( + "Context", + "Decision", + "Admission boundary and authority", + "Protocol version 1", + "State, witnesses, and atomic batches", + "Accounting and identity invariants", + "Protected reserve and epoch isolation", + "Producer coverage matrix", + "Authority unavailable and byte ceilings", + "Upstream authority request", + "Privacy and observability", + "Capability decision for Codex 0.145.0", + "Protocol evolution", + "Downstream dependency graph", + "Non-goals", + "Traceability", +) + +TRACEABILITY_TERMS = { + "INV-1": "model-visible admission boundary", + "INV-2": "stable identities", + "INV-3": "atomic reserve/commit/release protocol", + "INV-4": "version-pinned coverage matrix", + "INV-5": "token_budget/get_context_remaining", + "INV-6": "upstream Codex contract", + "INV-7": "privacy-safe observability", + "OUT-1": "versioned admission protocol and state machine", + "OUT-2": "producer/control-point coverage matrix", + "OUT-3": "accounting and identity invariants", + "OUT-4": "failure and reconciliation semantics", + "OUT-5": "authoritative token accounting", + "OUT-6": "upstream Codex request", + "OUT-7": "implementation dependency graph", + "NG-1": "no enforcement or numeric budget defaults", + "NG-2": "retain existing raw per-producer ceilings", + "NG-3": "bytes are not an exact token proxy", + "NG-4": "digest is not an access capability or deduplication identity", + "AC-1": "every model-visible producer", + "AC-2": "idempotent reservation keys and compaction/window reset rules", + "AC-3": "outstanding concurrent calls and protected reserve", + "AC-4": "Codex claims cite tested version and primary sources", + "AC-5": "C2-C8 use the shared accounting contract", +} + +ALLOWED_TRACEABILITY_TARGETS = ( + "CONTEXT_ADMISSION_PROTOCOL_VERSION", + "CONTEXT_ADMISSION_COVERAGE", + "reduce_context_admission", + "replay_context_admission", + "test_context_admission_contract.py", + "test_context_admission_coverage.py", + "test_context_admission_reducer.py", + "test_context_admission_state_machine.py", + "test_context_admission_decision.py", + *REQUIRED_HEADINGS, +) + + +@pytest.fixture(scope="module") +def decision_text() -> str: + assert DECISION.exists(), "ADR-0007 must exist" + return DECISION.read_text(encoding="utf-8") + + +def _section(text: str, heading: str) -> str: + lines = text.splitlines() + start = -1 + level = 0 + for index, line in enumerate(lines): + match = re.fullmatch(r"(#{2,6}) (.+)", line) + if match is not None and match.group(2) == heading: + start = index + 1 + level = len(match.group(1)) + break + assert start >= 0, f"missing ADR heading: {heading}" + end = len(lines) + for index in range(start, len(lines)): + match = re.match(r"(#{2,6}) ", lines[index]) + if match is not None and len(match.group(1)) <= level: + end = index + break + return "\n".join(lines[start:end]) + + +def _traceability_rows(text: str) -> dict[str, tuple[str, str]]: + rows: dict[str, tuple[str, str]] = {} + for row_id, requirement, target in re.findall( + r"^\|\s*((?:INV|OUT|NG|AC)-\d+)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|$", + _section(text, "Traceability"), + flags=re.MULTILINE, + ): + assert row_id not in rows, f"duplicate traceability row: {row_id}" + rows[row_id] = (requirement.strip(), target.strip()) + return rows + + +def test_context_admission_decision_is_indexed(decision_text: str) -> None: + assert "**Status:** Accepted" in decision_text + assert "#4333" in decision_text + assert "C1" in decision_text + assert "0007-context-admission.md" in DECISION_INDEX.read_text(encoding="utf-8") + + +def test_decision_has_the_normative_contract_sections(decision_text: str) -> None: + for heading in REQUIRED_HEADINGS: + assert re.search( + rf"^##+ {re.escape(heading)}\s*$", + decision_text, + flags=re.MULTILINE, + ), f"missing ADR heading: {heading}" + + +def test_decision_references_the_executable_protocol(decision_text: str) -> None: + for required in [ + "CONTEXT_ADMISSION_PROTOCOL_VERSION", + "reduce_context_admission", + "replay_context_admission", + "CONTEXT_ADMISSION_COVERAGE", + ]: + assert required in decision_text + protocol = _section(decision_text, "Protocol version 1") + assert re.search(r"\bprotocol version 1\b", protocol, flags=re.IGNORECASE) + + +def test_traceability_freezes_the_entire_issue_scope(decision_text: str) -> None: + rows = _traceability_rows(decision_text) + assert set(rows) == set(TRACEABILITY_TERMS) + for row_id, required_term in TRACEABILITY_TERMS.items(): + requirement, target = rows[row_id] + assert required_term.casefold() in requirement.casefold(), row_id + assert any(allowed in target for allowed in ALLOWED_TRACEABILITY_TARGETS), row_id + + +def test_codex_capability_claims_are_version_pinned_to_primary_evidence( + decision_text: str, +) -> None: + capability = _section(decision_text, "Capability decision for Codex 0.145.0") + for required in [ + CODEX_VERSION, + CODEX_REVISION, + "token_budget", + "get_context_remaining", + "PreCompact", + "PostCompact", + ]: + assert required in capability + primary_source_prefix = f"https://github.com/openai/codex/blob/{CODEX_REVISION}/" + assert capability.count(primary_source_prefix) >= 3 + + +def test_authority_unavailable_behavior_preserves_byte_boundaries( + decision_text: str, +) -> None: + section = _section(decision_text, "Authority unavailable and byte ceilings").casefold() + for required in [ + "watermark_unavailable", + "upstream_gated", + "raw-byte", + "no numeric", + "independent", + ]: + assert required in section + + +def test_decision_freezes_the_complete_producer_matrix(decision_text: str) -> None: + coverage = _section(decision_text, "Producer coverage matrix") + for surface in [ + "NATIVE_SHELL", + "UNIFIED_EXEC_AND_WRITE_STDIN", + "APPLY_PATCH", + "AUTOSKILLIT_MCP", + "EXTERNAL_MCP", + "AUTOSKILLIT_LOCAL_FUNCTION", + "OTHER_LOCAL_FUNCTION", + "MCP_RESOURCE", + "CLIENT_PROVIDER_RETRIEVAL", + "CODE_MODE_AGGREGATE", + "HOSTED_SPECIALIZED_TOOL", + "HOOK_FEEDBACK", + "TOOL_ARGUMENT", + "TOOL_RESULT_ENVELOPE", + "USER_PROMPT", + "ASSISTANT_OUTPUT_HISTORY", + "SKILL_PLUGIN_CONTEXT", + "OTHER_CONTEXT_INJECTION", + "HEADLESS_CHILD_PROMPT", + "PARENT_VISIBLE_CHILD_DELIVERY", + "COMPACTION_MODEL_WINDOW_TRANSITION", + ]: + assert surface in coverage + assert "PARTIAL" in coverage + assert "UPSTREAM_GATED" in coverage + assert "VERIFIED" in coverage + + +def test_upstream_request_contains_all_three_authority_parts_and_minimum_fields( + decision_text: str, +) -> None: + request = _section(decision_text, "Upstream authority request") + for required in [ + "atomic snapshot/reservation", + "generated-output maximum", + "synchronous blocking", + "final ordered batch", + "canonical representation manifest", + "receiver fence", + "durable/queryable journal", + "history staging", + "request inclusion", + "provider acceptance", + "output-usage reconciliation", + "rollback", + "truncation/compaction replacement", + "authoritative reconciliation", + "request_id", + "batch_id", + "ordered members", + "reservation IDs", + "thread/turn/agent lineage", + "admission sequence", + "window ID/number", + "model/tokenizer identity", + "snapshot sequence", + "measurement kind/source", + "active/hard-limit/remaining/proposed/max-output counts", + "reserve class", + "representation revision", + ]: + assert required in request + + +def test_privacy_table_freezes_field_governance(decision_text: str) -> None: + privacy = _section(decision_text, "Privacy and observability") + for required in [ + "Runtime/audit", + "Aggregate telemetry", + "Purpose", + "Maximum length/cardinality", + "Retention", + "Access", + "Deletion", + "Export", + "opaque", + "lineage", + "source locator", + "content", + "absolute paths", + "bearer tokens", + "content/artifact hashes", + ]: + assert required in privacy + + +def test_decision_keeps_issue_non_goals_explicit(decision_text: str) -> None: + non_goals = _section(decision_text, "Non-goals").casefold() + for required in [ + "enforcement", + "numeric budget defaults", + "existing raw per-producer ceilings", + "bytes", + "exact token proxy", + "digest", + "access capability", + "deduplication identity", + ]: + assert required in non_goals + + +@pytest.mark.parametrize( + ("work_package", "issue"), + [ + ("C2", "#4334"), + ("C3", "#4335"), + ("C4", "#4336"), + ("C5", "#4337"), + ("C6", "#4339"), + ("C7", "#4340"), + ("C8", "#4338"), + ], +) +def test_dependency_graph_maps_existing_child_issues( + decision_text: str, + work_package: str, + issue: str, +) -> None: + graph = _section(decision_text, "Downstream dependency graph") + assert re.search( + rf"\b{re.escape(work_package)}\b[^\n]*{re.escape(issue)}" + rf"|{re.escape(issue)}[^\n]*\b{re.escape(work_package)}\b", + graph, + ) diff --git a/tests/test_test_filter_core_cascade.py b/tests/test_test_filter_core_cascade.py index 5f3ffb052..b7ea1f7e1 100644 --- a/tests/test_test_filter_core_cascade.py +++ b/tests/test_test_filter_core_cascade.py @@ -100,6 +100,7 @@ def test_all_entries_present(self) -> None: "_type_backend", "_type_recipe_delivery", "_type_recipe_sections", + "_type_context_admission", "_type_dispatch_identity", "_type_figure_spec", "_type_session_env", @@ -122,6 +123,7 @@ def test_all_entries_present(self) -> None: "closure_hashing", "path_containment", "closure_verifier", + "context_admission", "_delivery_bounds", } assert set(MODULE_CASCADE_CORE.keys()) == expected_stems From 674a8a6e2f5e909037917467cf2119aa9e871330 Mon Sep 17 00:00:00 2001 From: Trecek Date: Thu, 23 Jul 2026 12:56:02 -0700 Subject: [PATCH 02/62] fix(core): tighten types and mypy annotations in context admission reducer Co-Authored-By: Claude --- src/autoskillit/core/context_admission.py | 278 +++++++++++++++--- .../core/types/_type_context_admission.py | 168 ++++++++++- tests/arch/test_subpackage_isolation.py | 4 +- tests/core/test_context_admission_reducer.py | 17 +- .../types/test_context_admission_contract.py | 183 +++++++++++- 5 files changed, 590 insertions(+), 60 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index 92e10379c..8813dfa72 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -24,6 +24,7 @@ AggregateRevision, AuthorityUnavailableEffect, AuthorityUnavailableEvent, + CanonicalSpanId, ChargeCommittedEffect, ClosedEpochAudit, ContextAdmissionEvent, @@ -75,6 +76,7 @@ ChargeDomain, CoverageState, GenerationState, + MeasurementKind, ReserveClass, WitnessKind, ) @@ -368,15 +370,29 @@ def _preflight( None, ) if prior is not None: - kind = ( - AdmissionDecisionKind.NOOP_IDEMPOTENT - if prior.event == event - else AdmissionDecisionKind.CONFLICT - ) - reason = "event_replay" if prior.event == event else "event_id_conflict" + same_event = prior.event == event + if same_event: + original = prior.original_decision + return AdmissionTransition( + next_state=state, + decision=AdmissionDecision( + kind=AdmissionDecisionKind.NOOP_IDEMPOTENT, + reason_code="event_replay", + window_epoch_id=original.window_epoch_id, + snapshot_sequence=original.snapshot_sequence, + requested_count=original.requested_count, + available_ordinary_count=original.available_ordinary_count, + available_protected_count=original.available_protected_count, + ), + effects=(), + ) return AdmissionTransition( next_state=state, - decision=_decision(state, kind, reason), + decision=_decision( + state, + AdmissionDecisionKind.CONFLICT, + "event_id_conflict", + ), effects=(), ) if isinstance(event, ReserveRequestEvent) and event.input_reservations: @@ -403,26 +419,37 @@ def _preflight( None, ) if idempotency_record is not None: - original = idempotency_record.original_descriptor + stored_descriptor = idempotency_record.original_descriptor same_intent = ( - original.protocol_version == event.protocol_version - and original.idempotency_namespace == event.idempotency_namespace - and original.batch == event.batch - and original.snapshot_sequence == event.snapshot_sequence - and original.input_reservations == event.input_reservations - and original.generation_reservation == event.generation_reservation - ) - kind = ( - AdmissionDecisionKind.NOOP_IDEMPOTENT - if same_intent - else AdmissionDecisionKind.CONFLICT + stored_descriptor.protocol_version == event.protocol_version + and stored_descriptor.idempotency_namespace == event.idempotency_namespace + and stored_descriptor.batch == event.batch + and stored_descriptor.snapshot_sequence == event.snapshot_sequence + and stored_descriptor.input_reservations == event.input_reservations + and stored_descriptor.generation_reservation == event.generation_reservation ) + if same_intent: + original_decision = idempotency_record.original_reserve_decision + replay_decision = AdmissionDecision( + kind=AdmissionDecisionKind.NOOP_IDEMPOTENT, + reason_code="reservation_key_replay", + window_epoch_id=original_decision.window_epoch_id, + snapshot_sequence=original_decision.snapshot_sequence, + requested_count=original_decision.requested_count, + available_ordinary_count=original_decision.available_ordinary_count, + available_protected_count=original_decision.available_protected_count, + ) + return AdmissionTransition( + next_state=state, + decision=replay_decision, + effects=(), + ) return AdmissionTransition( next_state=state, decision=_decision( state, - kind, - ("reservation_key_replay" if same_intent else "reservation_key_conflict"), + AdmissionDecisionKind.CONFLICT, + "reservation_key_conflict", ), effects=(), ) @@ -441,6 +468,12 @@ def _batch_record( ) +def _dispatched_count(state: ActiveContextAdmissionState) -> int: + return sum( + 1 for record in state.batch_records if record.state is AdmissionState.REQUEST_DISPATCHED + ) + + def _generation_record( state: ActiveContextAdmissionState, reservation_id: GenerationReservationId, @@ -468,6 +501,17 @@ def _replace_batch_record( ) +def _replace_batch_record_quarantine( + state: ActiveContextAdmissionState, + record: AdmissionBatchRecord, +) -> tuple[AdmissionBatchRecord, ...]: + quarantined = replace(record, state=AdmissionState.QUARANTINED) + return tuple( + quarantined if other.batch.batch_id == record.batch.batch_id else other + for other in state.batch_records + ) + + def _set_occurrence_state( state: ActiveContextAdmissionState, batch: AdmissionBatch, @@ -608,6 +652,11 @@ def _reserve( return _reject(state, "snapshot_sequence_mismatch") if _batch_record(state, event.batch.batch_id) is not None: return _reject(state, "batch_already_reserved") + reservation = event.input_reservations[0] + if any( + existing.reservation_id == reservation.reservation_id for existing in state.reservations + ): + return _reject(state, "reservation_id_reuse_with_changed_descriptor") member_records = tuple( record for record in state.occurrence_records @@ -626,7 +675,6 @@ def _reserve( return _reject(state, "batch_members_not_all_proposed") if len(event.input_reservations) != 1: return _reject(state, "atomic_input_reservation_required") - reservation = event.input_reservations[0] if ( reservation.key.batch_id != event.batch.batch_id or reservation.occurrence_ids != event.batch.occurrence_ids @@ -794,6 +842,11 @@ def _prepare( reservation = _reservation_for(state, record) if reservation is None or event.proposed_charge != reservation.reserved_count: return _reject(state, "prepared_charge_mismatch") + if event.measurement_kind not in { + MeasurementKind.PROVIDER_EXACT, + MeasurementKind.TOKENIZER_EXACT, + }: + return _reject(state, "non_authoritative_measurement") updated = replace( record, state=AdmissionState.PREPARED, @@ -972,8 +1025,77 @@ def _accept( or binding.batch_id != record.batch.batch_id ): return _reject(state, "representation_revision_mismatch") + if event.measurement_kind not in { + MeasurementKind.PROVIDER_EXACT, + MeasurementKind.TOKENIZER_EXACT, + }: + return _reject(state, "non_authoritative_measurement") if event.exact_input_charge < 0: return _reject(state, "invalid_exact_charge") + expected_owned_spans: set[CanonicalSpanId] = set() + for occurrence in state.occurrence_records: + if occurrence.occurrence.occurrence_id in set(record.batch.occurrence_ids): + expected_owned_spans.update(occurrence.occurrence.owned_span_ids) + manifest_spans = {owner.span_id for owner in record.batch.manifest.span_owners} + if ( + event.authority_source_id != event.witness.authority_source_id + or event.authority_source_id != binding.authority_source_id + ): + coord = _effect_coordinates(state, capacity_changed=True) + quarantined_record = replace( + record, + state=AdmissionState.QUARANTINED, + witness_ids=record.witness_ids + (event.witness.witness_id,), + committed_input_count=event.exact_input_charge, + unresolved_input_count=0, + ) + next_state = _replace_batch_record(state, quarantined_record) + next_state = _set_occurrence_state( + next_state, + record.batch, + AdmissionState.QUARANTINED, + witness=event.witness, + quarantine_reason_code="authority_source_mismatch", + ) + return _publish( + state, + next_state, + event, + kind=AdmissionDecisionKind.QUARANTINED, + reason_code="authority_source_mismatch", + requested_count=event.exact_input_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=coord[0], + resulting_admission_sequence=coord[1], + target_id=record.batch.batch_id, + reason_code="authority_source_mismatch", + ), + ), + ) + if manifest_spans != expected_owned_spans: + coord = _effect_coordinates(state, capacity_changed=True) + return _publish( + state, + replace(state, batch_records=_replace_batch_record_quarantine(state, record)), + event, + kind=AdmissionDecisionKind.QUARANTINED, + reason_code="incomplete_canonical_span_ownership", + capacity_changed=True, + effects=( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=coord[0], + resulting_admission_sequence=coord[1], + target_id=record.batch.batch_id, + reason_code="incomplete_canonical_span_ownership", + ), + ), + ) next_state, kind, reason = _accepted_state( state, record, @@ -1023,19 +1145,21 @@ def _release_or_rollback( ResolveIndeterminateNonAdmissionEvent | ResolveIndeterminateRollbackEvent, ) expected_witness = WitnessKind.NON_ADMISSION if is_release else WitnessKind.ROLLBACK - allowed_states = ( - {AdmissionState.INDETERMINATE} - if is_resolution - else ( - {AdmissionState.RESERVED, AdmissionState.PREPARED} - if is_release - else { - AdmissionState.HISTORY_STAGED, - AdmissionState.REQUEST_DISPATCHED, - AdmissionState.INDETERMINATE, - } - ) - ) + if is_resolution: + allowed_states = {AdmissionState.INDETERMINATE} + elif is_release: + allowed_states = { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + } + else: + allowed_states = { + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } if record.state not in allowed_states or not _validate_witness( state, record.batch, @@ -1043,6 +1167,12 @@ def _release_or_rollback( expected_witness, ): return _reject(state, "invalid_release_or_rollback_witness") + if ( + is_release + and record.state in {AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED} + and event.witness.kind is not WitnessKind.NON_ADMISSION + ): + return _reject(state, "staged_release_requires_non_admission_witness") lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK updated = replace( record, @@ -1188,19 +1318,26 @@ def _start_generation( generation = _generation_record(state, event.generation_reservation_id) if generation is None or generation.state is not GenerationState.RESERVED: return _reject(state, "illegal_generation_start") - batch = next( + batch_record = next( ( - record.batch + record for record in state.batch_records if record.batch.request_id == generation.request_id ), None, ) - if batch is None or not _validate_witness( - state, - batch, - event.witness, - WitnessKind.REQUEST_INCLUDED, + batch = batch_record.batch if batch_record is not None else None + if ( + batch is None + or batch_record is None + or batch_record.state + not in {AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED} + or not _validate_witness( + state, + batch, + event.witness, + WitnessKind.REQUEST_INCLUDED, + ) ): return _reject(state, "invalid_generation_start_witness") updated = replace( @@ -1235,6 +1372,8 @@ def _reconcile_generation( return _reject(state, "illegal_generation_reconciliation") if event.output_usage_witness.kind is not WitnessKind.OUTPUT_USAGE: return _reject(state, "invalid_output_usage_witness") + if event.output_usage_witness.authority_source_id.value == "": + return _reject(state, "missing_witness_authority_source") quarantined = event.exact_output_usage > generation.maximum_allowance updated = replace( generation, @@ -1405,6 +1544,11 @@ def _expire_idempotency( return _reject(state, "idempotency_key_not_terminal") if event.expiry_witness.kind is not WitnessKind.IDEMPOTENCY_EXPIRY: return _reject(state, "invalid_expiry_witness") + if ( + event.expiry_witness.window_epoch_id != event.reservation_key.window_epoch_id + or event.expiry_witness.window_epoch_number != event.reservation_key.window_epoch_number + ): + return _reject(state, "expiry_epoch_mismatch") tombstone = ExpiredIdempotencyTombstone( namespace=record.namespace, reservation_key=record.reservation_key, @@ -1449,6 +1593,7 @@ def _rollover( or proof.new_window_epoch_id != event.new_snapshot.window_epoch_id or proof.new_window_epoch_number != event.new_snapshot.window_epoch_number or proof.new_window_epoch_number <= proof.old_window_epoch_number + or proof.receiver_authority_source_id != event.witness.authority_source_id or proof.highest_admitted_dispatch_sequence < len(state.reservations) ): return _reject(state, "stale_receiver_fence") @@ -1469,7 +1614,42 @@ def _rollover( ) for record in state.occurrence_records ) + retained_batch_records = tuple( + record + for record in state.batch_records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + retained_reservation_ids = { + record.reservation_id for record in retained_batch_records if record.reservation_id + } + retained_reservations = tuple( + reservation + for reservation in state.reservations + if reservation.reservation_id in retained_reservation_ids + ) + retained_generation_ids = {record.batch.batch_id for record in retained_batch_records} + retained_generation_reservations = tuple( + generation + for generation in state.generation_reservations + if generation.request_id in {record.batch.request_id for record in retained_batch_records} + or generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + and any( + record.batch.request_id == generation.request_id + and record.state is AdmissionState.REQUEST_DISPATCHED + for record in retained_batch_records + ) + ) retained_unresolved_count = 0 + retained_generation_count = 0 for record in state.batch_records: if record.state in { AdmissionState.REQUEST_DISPATCHED, @@ -1479,6 +1659,13 @@ def _rollover( retained_unresolved_count += record.unresolved_input_count or ( reservation.reserved_count if reservation is not None else 0 ) + for generation in state.generation_reservations: + if generation.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } and any(request_id == generation.request_id for request_id in retained_generation_ids): + retained_generation_count += generation.maximum_allowance audit = ClosedEpochAudit( snapshot=state.snapshot, terminal_occurrence_records=terminal_occurrences, @@ -1496,9 +1683,9 @@ def _rollover( snapshot=event.new_snapshot, protected_pools=event.protected_pools, occurrence_records=(), - batch_records=(), - reservations=(), - generation_reservations=(), + batch_records=retained_batch_records, + reservations=retained_reservations, + generation_reservations=retained_generation_reservations, processed_events=state.processed_events, idempotency_records=state.idempotency_records, expired_idempotency_tombstones=state.expired_idempotency_tombstones, @@ -1506,6 +1693,7 @@ def _rollover( ) except ContextAdmissionValidationError: return _reject(state, "invalid_rollover_snapshot") + _ = retained_generation_count # retained for future CapacityRecord accounting revision, sequence = _effect_coordinates(state, capacity_changed=True) invalidation_effects = tuple( ReservationInvalidatedEffect( diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 4e6f1a602..e5660caa9 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -75,7 +75,6 @@ def _validate_bounded_text( reason_code: str, *, maximum: int = 128, - allow_path: bool = False, ) -> None: if not isinstance(value, str) or not value or len(value) > maximum: _raise_invalid(reason_code) @@ -83,9 +82,13 @@ def _validate_bounded_text( if ( lowered.startswith("bearer") or lowered.startswith("sha256:") + or lowered.startswith("blake2:") + or lowered.startswith("content:") or "\n" in value or "\r" in value - or (not allow_path and (value.startswith("/") or "\\" in value or "/" in value)) + or value.startswith("/") + or "\\" in value + or value.startswith("~") ): _raise_invalid(reason_code) @@ -407,6 +410,8 @@ def __post_init__(self) -> None: expected = DispatchIdentity.from_dispatch_id(self.dispatch_identity.dispatch_id) if self.dispatch_identity != expected: _raise_invalid("invalid_dispatch_identity") + if self.producer_surface in _NON_DISPATCH_PRODUCER_SURFACES: + _raise_invalid("dispatch_identity_on_non_dispatch_surface") @dataclass(frozen=True, slots=True) @@ -654,6 +659,23 @@ class AdmissionOccurrenceRecord(_ContractValue): indeterminate_reason_code: str | None quarantine_reason_code: str | None + def __post_init__(self) -> None: + _validate_tuple(self.accepted_witness_ids, "invalid_witness_ids") + if len(self.accepted_witness_ids) != len(set(self.accepted_witness_ids)): + _raise_invalid("duplicate_witness_id") + if self.indeterminate_reason_code is not None: + _validate_bounded_text( + self.indeterminate_reason_code, + "invalid_indeterminate_reason", + maximum=64, + ) + if self.quarantine_reason_code is not None: + _validate_bounded_text( + self.quarantine_reason_code, + "invalid_quarantine_reason", + maximum=64, + ) + @dataclass(frozen=True, slots=True) class AdmissionBatchRecord(_ContractValue): @@ -666,10 +688,15 @@ class AdmissionBatchRecord(_ContractValue): unresolved_input_count: int def __post_init__(self) -> None: + _validate_tuple(self.witness_ids, "invalid_witness_ids") + if len(self.witness_ids) != len(set(self.witness_ids)): + _raise_invalid("duplicate_witness_id") if self.prepared_input_count is not None: _validate_non_negative(self.prepared_input_count, "invalid_prepared_count") _validate_non_negative(self.committed_input_count, "invalid_committed_count") _validate_non_negative(self.unresolved_input_count, "invalid_unresolved_count") + if self.committed_input_count > 0 and self.unresolved_input_count > 0: + _raise_invalid("committed_and_unresolved_simultaneously") @dataclass(frozen=True, slots=True) @@ -748,12 +775,7 @@ def __post_init__(self) -> None: (self.checked_at, "invalid_checked_at", 32), (self.freshness_policy, "invalid_freshness_policy", 128), ): - _validate_bounded_text( - value, - reason, - maximum=maximum, - allow_path=reason == "invalid_source_locator", - ) + _validate_bounded_text(value, reason, maximum=maximum) @dataclass(frozen=True, slots=True) @@ -826,6 +848,20 @@ class ReserveRequestEvent(_AdmissionEventBase): input_reservations: tuple[AdmissionReservation, ...] generation_reservation: GenerationReservationRecord | None + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") + _validate_tuple(self.input_reservations, "invalid_input_reservations") + reservation_ids = tuple( + reservation.reservation_id for reservation in self.input_reservations + ) + if len(reservation_ids) != len(set(reservation_ids)): + _raise_invalid("duplicate_reservation_id") + if self.batch.occurrence_ids != self.input_reservations[0].occurrence_ids: + _raise_invalid("reservation_occurrence_mismatch") + if self.idempotency_namespace != self.input_reservations[0].key.idempotency_namespace: + _raise_invalid("reservation_namespace_mismatch") + @dataclass(frozen=True, slots=True) class PrepareBatchEvent(_AdmissionEventBase): @@ -835,6 +871,15 @@ class PrepareBatchEvent(_AdmissionEventBase): measurement_kind: MeasurementKind authority_source_id: AuthoritySourceId + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.proposed_charge, "invalid_proposed_charge") + if self.measurement_kind in { + MeasurementKind.HOST_ESTIMATE, + MeasurementKind.BYTE_EMERGENCY, + }: + _raise_invalid("non_authoritative_measurement") + @dataclass(frozen=True, slots=True) class StageHistoryEvent(_AdmissionEventBase): @@ -858,6 +903,15 @@ class AcceptInputEvent(_AdmissionEventBase): authority_source_id: AuthoritySourceId representation_binding_witness: RepresentationBindingWitness + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.exact_input_charge, "invalid_exact_input_charge") + if self.measurement_kind in { + MeasurementKind.HOST_ESTIMATE, + MeasurementKind.BYTE_EMERGENCY, + }: + _raise_invalid("non_authoritative_measurement") + @dataclass(frozen=True, slots=True) class ReleaseNonAdmissionEvent(_AdmissionEventBase): @@ -876,6 +930,10 @@ class MarkIndeterminateEvent(_AdmissionEventBase): batch_id: AdmissionBatchId reason_code: str + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + @dataclass(frozen=True, slots=True) class ResolveIndeterminateAcceptedEvent(_AdmissionEventBase): @@ -885,6 +943,15 @@ class ResolveIndeterminateAcceptedEvent(_AdmissionEventBase): measurement_kind: MeasurementKind authority_source_id: AuthoritySourceId + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.exact_charge, "invalid_exact_charge") + if self.measurement_kind in { + MeasurementKind.HOST_ESTIMATE, + MeasurementKind.BYTE_EMERGENCY, + }: + _raise_invalid("non_authoritative_measurement") + @dataclass(frozen=True, slots=True) class ResolveIndeterminateNonAdmissionEvent(_AdmissionEventBase): @@ -910,18 +977,30 @@ class ReconcileGenerationEvent(_AdmissionEventBase): output_usage_witness: AdmissionWitness exact_output_usage: int + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_non_negative(self.exact_output_usage, "invalid_exact_output_usage") + @dataclass(frozen=True, slots=True) class MarkGenerationIndeterminateEvent(_AdmissionEventBase): generation_reservation_id: GenerationReservationId reason_code: str + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + @dataclass(frozen=True, slots=True) class RequestReconciliationEvent(_AdmissionEventBase): target_id: AdmissionBatchId | GenerationReservationId reason_code: str + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + @dataclass(frozen=True, slots=True) class ExpireIdempotencyKeyEvent(_AdmissionEventBase): @@ -936,6 +1015,10 @@ class RolloverEpochEvent(_AdmissionEventBase): new_snapshot: ContextWindowSnapshot protected_pools: tuple[ProtectedPoolSpec, ...] + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_tuple(self.protected_pools, "invalid_protected_pools") + ContextAdmissionEvent: TypeAlias = ( OpenEpochEvent @@ -1164,6 +1247,60 @@ def __post_init__(self) -> None: self.snapshot.remaining_count ): _raise_invalid("protected_pool_capacity_exceeded") + _validate_tuple(self.batch_records, "invalid_batch_records") + _validate_tuple(self.reservations, "invalid_reservations") + _validate_tuple(self.generation_reservations, "invalid_generation_reservations") + _validate_tuple(self.occurrence_records, "invalid_occurrence_records") + batch_ids = tuple(record.batch.batch_id for record in self.batch_records) + if len(batch_ids) != len(set(batch_ids)): + _raise_invalid("duplicate_batch_owner") + reservation_ids = tuple(reservation.reservation_id for reservation in self.reservations) + if len(reservation_ids) != len(set(reservation_ids)): + _raise_invalid("duplicate_reservation_owner") + generation_ids = tuple( + record.generation_reservation_id for record in self.generation_reservations + ) + if len(generation_ids) != len(set(generation_ids)): + _raise_invalid("duplicate_generation_owner") + occurrence_ids = tuple( + record.occurrence.occurrence_id for record in self.occurrence_records + ) + if len(occurrence_ids) != len(set(occurrence_ids)): + _raise_invalid("duplicate_occurrence_owner") + seen_pool_owners: set[tuple[ReserveClass, ProtectedPoolOwnerId]] = set() + seen_batch_owners: set[tuple[ReserveClass, ProtectedPoolOwnerId]] = set() + seen_generation_owners: set[tuple[ReserveClass, ProtectedPoolOwnerId]] = set() + for record in self.batch_records: + owner = record.batch.protected_pool_owner_id + if record.batch.reserve_class is not ReserveClass.ORDINARY: + if owner is None: + _raise_invalid("missing_protected_pool_owner") + key = (record.batch.reserve_class, owner) + if key in seen_batch_owners: + _raise_invalid("duplicate_protected_charge_owner") + seen_batch_owners.add(key) + seen_pool_owners.add(key) + if not any( + pool.reserve_class is record.batch.reserve_class + and pool.capability_owner_id == owner + for pool in self.protected_pools + ): + _raise_invalid("orphan_protected_charge_owner") + for generation_record in self.generation_reservations: + owner = generation_record.protected_pool_owner_id + if generation_record.reserve_class is not ReserveClass.ORDINARY: + if owner is None: + _raise_invalid("missing_protected_pool_owner") + key = (generation_record.reserve_class, owner) + if key in seen_generation_owners: + _raise_invalid("duplicate_protected_charge_owner") + seen_generation_owners.add(key) + if not any( + pool.reserve_class is generation_record.reserve_class + and pool.capability_owner_id == owner + for pool in self.protected_pools + ): + _raise_invalid("orphan_protected_charge_owner") ContextAdmissionState: TypeAlias = UninitializedContextAdmissionState | ActiveContextAdmissionState @@ -1192,6 +1329,21 @@ class AdmissionReplay(_ContractValue): ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY, } ) +_NON_DISPATCH_PRODUCER_SURFACES = frozenset( + { + ProducerSurface.TOOL_ARGUMENT, + ProducerSurface.TOOL_RESULT_ENVELOPE, + ProducerSurface.USER_PROMPT, + ProducerSurface.ASSISTANT_OUTPUT_HISTORY, + ProducerSurface.SKILL_PLUGIN_CONTEXT, + ProducerSurface.OTHER_CONTEXT_INJECTION, + ProducerSurface.CLIENT_PROVIDER_RETRIEVAL, + ProducerSurface.CODE_MODE_AGGREGATE, + ProducerSurface.HOSTED_SPECIALIZED_TOOL, + ProducerSurface.HOOK_FEEDBACK, + ProducerSurface.COMPACTION_MODEL_WINDOW_TRANSITION, + } +) _UNOBSERVABLE_SURFACES = frozenset( { ProducerSurface.CLIENT_PROVIDER_RETRIEVAL, diff --git a/tests/arch/test_subpackage_isolation.py b/tests/arch/test_subpackage_isolation.py index 7acfb256e..bbb083ee7 100644 --- a/tests/arch/test_subpackage_isolation.py +++ b/tests/arch/test_subpackage_isolation.py @@ -1151,7 +1151,7 @@ def test_data_directories_are_not_python_packages() -> None: "isolated in dedicated workspace modules.", ), "core/context_admission.py": ( - 1750, + 2000, "REQ-CNST-010-E13: #4333 freezes one exhaustive protocol-v1 reducer and replay " "surface. Keeping all closed event transitions together makes atomic batch, " "idempotency, protected-pool, reconciliation, rollover, and declarative effect " @@ -1159,7 +1159,7 @@ def test_data_directories_are_not_python_packages() -> None: "fragment exhaustiveness.", ), "core/types/_type_context_admission.py": ( - 1500, + 1750, "REQ-CNST-010-E14: #4333 freezes the complete content-free protocol-v1 schema in " "one IL-0 shard. Co-locating identities, records, closed event/effect unions, " "states, canonical serialization, and the static coverage registry prevents " diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 51ddb2118..7df5c1003 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -609,7 +609,7 @@ def test_input_commit_and_output_reconciliation_remain_distinct_domains() -> Non final_manifest_revision=batch.manifest.representation_revision, exact_input_charge=18, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("provider-test"), + authority_source_id=AuthoritySourceId("authority-test"), representation_binding_witness=_binding(batch), ) accepted = reduce_context_admission(state, accept) @@ -1016,18 +1016,23 @@ def test_retry_resume_fork_and_parent_delivery_keep_distinct_occurrences() -> No def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> None: state, batch, _, _ = _reserved_batch(name="rollover") + receiver_authority = AuthoritySourceId("receiver-2") proof = EpochFenceProof( old_window_epoch_id=WindowEpochId("epoch-1"), old_window_epoch_number=1, new_window_epoch_id=WindowEpochId("epoch-2"), new_window_epoch_number=2, - receiver_authority_source_id=AuthoritySourceId("receiver-2"), + receiver_authority_source_id=receiver_authority, fence_witness_id=AdmissionWitnessId("fence-1-to-2"), highest_admitted_dispatch_sequence=state.admission_sequence.value, ) + rollover_witness = replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver_authority, + ) event = RolloverEpochEvent( **_event_fields(state, "rollover-1-to-2", "rollover-epoch"), - witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + witness=rollover_witness, fence_proof=proof, new_snapshot=_snapshot(epoch=2), protected_pools=(), @@ -1049,9 +1054,13 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> new_window_epoch_number=3, fence_witness_id=AdmissionWitnessId("fence-2-to-3"), ) + second_witness = replace( + _witness(second_batch, WitnessKind.EPOCH_ROLLOVER, epoch=2), + authority_source_id=second_proof.receiver_authority_source_id, + ) second = RolloverEpochEvent( **_event_fields(rolled.next_state, "rollover-2-to-3", "rollover-epoch"), - witness=_witness(second_batch, WitnessKind.EPOCH_ROLLOVER, epoch=2), + witness=second_witness, fence_proof=second_proof, new_snapshot=_snapshot(epoch=3, model="claude-new", tokenizer="tokenizer-new"), protected_pools=(), diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index ac9bc213a..f37e0b0ad 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -10,21 +10,26 @@ from autoskillit.core import ( CONTEXT_ADMISSION_PROTOCOL_VERSION, + AcceptInputEvent, ActiveContextAdmissionState, AdmissionAttemptId, + AdmissionBatch, AdmissionBatchId, + AdmissionBatchRecord, AdmissionDecision, AdmissionDecisionKind, AdmissionEffect, AdmissionEventId, AdmissionOccurrence, AdmissionOccurrenceId, + AdmissionOccurrenceRecord, AdmissionRequestId, AdmissionReservationId, AdmissionReservationKey, AdmissionSequence, AdmissionState, AdmissionTransition, + AdmissionWitness, AdmissionWitnessId, AgentInstanceId, AggregateRevision, @@ -41,6 +46,7 @@ ContextSessionId, ContextThreadId, ContextWindowSnapshot, + CoverageEvidence, CoverageEvidenceKind, CoverageState, DeliveryOccurrenceId, @@ -52,10 +58,12 @@ MeasurementKind, ModelIdentity, ModelItemId, + PrepareBatchEvent, ProducerInstanceId, ProducerSurface, ProtectedPoolOwnerId, ProtectedPoolSpec, + RepresentationBindingWitness, RepresentationRevision, ReserveClass, TokenizerIdentity, @@ -291,6 +299,53 @@ def _manifest(*occurrences: AdmissionOccurrence) -> CanonicalRepresentationManif ) +def _batch(batch_id: str, occurrences: tuple[AdmissionOccurrence, ...]) -> AdmissionBatch: + return AdmissionBatch( + batch_id=AdmissionBatchId(batch_id), + request_id=AdmissionRequestId("request-1"), + occurrence_ids=tuple(occurrence.occurrence_id for occurrence in occurrences), + reserve_class=ReserveClass.ORDINARY, + protected_pool_owner_id=None, + manifest=_manifest(*occurrences), + ) + + +def _namespace(operation_kind: str) -> IdempotencyNamespace: + return IdempotencyNamespace(caller_scope="test-caller", operation_kind=operation_kind) + + +def _witness( + batch: AdmissionBatch, + kind: WitnessKind, + *, + witness: str | None = None, +) -> AdmissionWitness: + return AdmissionWitness( + witness_id=AdmissionWitnessId(witness or f"{kind.value}-witness-{batch.batch_id.value}"), + kind=kind, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + snapshot_sequence=1, + request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + occurrence_ids=batch.occurrence_ids, + authority_source_id=AuthoritySourceId("authority-test"), + ) + + +def _binding(batch: AdmissionBatch) -> RepresentationBindingWitness: + bound_revision = batch.manifest.representation_revision + return RepresentationBindingWitness( + counted_representation_revision=bound_revision, + dispatched_representation_revision=bound_revision, + final_manifest_revision=bound_revision, + request_id=batch.request_id, + batch_id=batch.batch_id, + authority_source_id=AuthoritySourceId("authority-test"), + ) + + def _snapshot( *, protocol_version: int = CONTEXT_ADMISSION_PROTOCOL_VERSION, @@ -447,7 +502,7 @@ def test_numeric_revisions_are_non_negative_and_have_no_default( def test_dispatch_identity_is_validated_and_projects_only_dispatch_id() -> None: identity = DispatchIdentity.from_dispatch_id("dispatch-12345678") - lineage = _lineage(dispatch_identity=identity) + lineage = _lineage(surface=ProducerSurface.NATIVE_SHELL, dispatch_identity=identity) serialized = lineage.to_dict() assert serialized["dispatch_identity"] == {"dispatch_id": identity.dispatch_id} assert ContextLineage.from_dict(serialized) == lineage @@ -606,3 +661,129 @@ def test_closed_epoch_audits_survive_state_serialization() -> None: restored = UninitializedContextAdmissionState.from_dict(state.to_dict()) assert restored == state assert restored.closed_epochs == (empty_audit,) + + +@pytest.mark.parametrize( + "non_dispatch_surface", + [ + ProducerSurface.TOOL_ARGUMENT, + ProducerSurface.TOOL_RESULT_ENVELOPE, + ProducerSurface.USER_PROMPT, + ProducerSurface.ASSISTANT_OUTPUT_HISTORY, + ProducerSurface.SKILL_PLUGIN_CONTEXT, + ProducerSurface.OTHER_CONTEXT_INJECTION, + ProducerSurface.CLIENT_PROVIDER_RETRIEVAL, + ProducerSurface.CODE_MODE_AGGREGATE, + ProducerSurface.HOSTED_SPECIALIZED_TOOL, + ProducerSurface.HOOK_FEEDBACK, + ProducerSurface.COMPACTION_MODEL_WINDOW_TRANSITION, + ], +) +def test_dispatch_identity_is_rejected_on_non_dispatch_surfaces( + non_dispatch_surface: ProducerSurface, +) -> None: + identity = DispatchIdentity.from_dispatch_id("dispatch-12345678") + with pytest.raises(ContextAdmissionValidationError) as exc_info: + _lineage(surface=non_dispatch_surface, dispatch_identity=identity) + assert "dispatch_identity_on_non_dispatch_surface" in str(exc_info.value) + + +def test_occurrence_record_rejects_duplicate_witness_ids() -> None: + occurrence = _occurrence() + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionOccurrenceRecord( + occurrence=occurrence, + state=AdmissionState.COMMITTED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=( + AdmissionWitnessId("dup"), + AdmissionWitnessId("dup"), + ), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ) + assert "duplicate_witness_id" in str(exc_info.value) + + +def test_batch_record_rejects_committed_and_unresolved_simultaneously() -> None: + occurrence = _occurrence() + batch = _batch("batch-simul", (occurrence,)) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionBatchRecord( + batch=batch, + state=AdmissionState.INDETERMINATE, + reservation_id=None, + witness_ids=(), + prepared_input_count=None, + committed_input_count=5, + unresolved_input_count=5, + ) + assert "committed_and_unresolved_simultaneously" in str(exc_info.value) + + +def test_privacy_canaries_are_rejected_from_coverage_evidence() -> None: + with pytest.raises(ContextAdmissionValidationError): + CoverageEvidence( + claim_id="COV-test", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="/home/alice/private/source.py", + tested_version="0.10.890", + tested_revision="ac8f653a00d2", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + with pytest.raises(ContextAdmissionValidationError): + CoverageEvidence( + claim_id="COV-test", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="~alice/private", + tested_version="0.10.890", + tested_revision="ac8f653a00d2", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + + +def test_prepare_event_rejects_estimate_measurement() -> None: + occurrence = _occurrence() + batch = _batch("batch-estimate", (occurrence,)) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + PrepareBatchEvent( + event_id=AdmissionEventId("prepare-estimate"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + idempotency_namespace=_namespace("prepare-batch"), + expected_aggregate_revision=AggregateRevision(0), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + proposed_charge=5, + measurement_kind=MeasurementKind.HOST_ESTIMATE, + authority_source_id=AuthoritySourceId("authority-test"), + ) + assert "non_authoritative_measurement" in str(exc_info.value) + + +def test_accept_event_rejects_estimate_measurement() -> None: + occurrence = _occurrence() + batch = _batch("batch-estimate-accept", (occurrence,)) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AcceptInputEvent( + event_id=AdmissionEventId("accept-estimate"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + idempotency_namespace=_namespace("accept-input"), + expected_aggregate_revision=AggregateRevision(0), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + exact_input_charge=5, + measurement_kind=MeasurementKind.BYTE_EMERGENCY, + authority_source_id=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ) + assert "non_authoritative_measurement" in str(exc_info.value) From fbc9a2834fa7439c26b4fae74b4354ced6346967 Mon Sep 17 00:00:00 2001 From: Trecek Date: Thu, 23 Jul 2026 13:23:56 -0700 Subject: [PATCH 03/62] style(core): apply ruff format to state machine test Co-Authored-By: Claude --- docs/decisions/0007-context-admission.md | 57 +++- .../test_context_admission_state_machine.py | 253 ++++++++++++++++++ tests/docs/test_context_admission_decision.py | 145 +++++++++- 3 files changed, 447 insertions(+), 8 deletions(-) diff --git a/docs/decisions/0007-context-admission.md b/docs/decisions/0007-context-admission.md index 509ae6cc1..bc68fa1f7 100644 --- a/docs/decisions/0007-context-admission.md +++ b/docs/decisions/0007-context-admission.md @@ -245,14 +245,59 @@ provider acceptance. Post-hoc usage likewise cannot substitute for this three-pa ## Privacy and observability Runtime IDs are random, opaque, non-secret values and are never derived from personal data. -Runtime/audit records and Aggregate telemetry are governed separately: +Runtime/audit records and Aggregate telemetry are governed separately by field. Each field below +maps to a concrete protocol dataclass attribute (`protocol_version`, `claim_id`, `reason_code`, +etc.) and is governed by an explicit maximum length/cardinality, retention, access, deletion, and +export rule. -| Field class | Purpose | Maximum length/cardinality | Retention | Access | Deletion | Export | +### Runtime/audit fields (per-record, persisted with the session) + +| Field | Purpose | Maximum length/cardinality | Retention | Access | Deletion | Export | +|---|---|---|---|---|---|---| +| `protocol_version` | pinned reducer semantics | 1 non-negative integer | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session; ADR header is the static anchor | content-free structured records only | +| `aggregate_revision` / `admission_sequence` | monotonic per-session counters | 64-bit non-negative | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session | content-free structured records only | +| `event_id`, `reservation_id`, `witness_id`, `batch_id`, `request_id`, `reservation_key`, `occurrence_id`, `attempt_id`, `delivery_occurrence_id`, `generation_reservation_id` | identity | opaque 96 ASCII chars (`[A-Za-z0-9_.:-]`); no `-` leading/trailing segments | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session; tombstone keys preserve identity until retention | content-free structured records only | +| `reason_code` (event/decision) | typed provenance | 64 ASCII chars; kebab-case `^[a-z][a-z0-9-]*$`; no `bearer`, `sha256:`, `blake2:`, `content:` prefix | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session | content-free structured records only | +| `count` / `requested_count` / `available_ordinary_count` / `available_protected_count` / `reserved_count` / `committed_input_count` / `unresolved_input_count` / `retained_unresolved_count` / `maximum_allowance` / `exact_terminal_usage` / `injected_count` / `priority` / `predicted_authoritative_maximum` / `active_count` / `hard_limit` / `remaining_count` / `highest_admitted_dispatch_sequence` | exact accounting counts | 64-bit non-negative integer | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session | content-free structured records only | +| `version` / `representation_revision` / `tested_version` / `tested_revision` / `publication_revision` | binding identity | opaque 96 ASCII chars; tested_revision is a pinned git SHA (40 hex chars) | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session; static ADR pins preserve release provenance | content-free structured records only | +| `checked_at` | evidence freshness date | ISO-8601 date (10 ASCII chars: `YYYY-MM-DD`) | coverage record lifetime | maintainers with release-provenance access | dropped with the coverage record | content-free structured records only | +| `witness_ids` / `span_owners` / `occurrence_ids` / `input_reservations` / `generation_reservations` / `protected_pools` / `idempotency_records` / `expired_idempotency_tombstones` / `closed_epochs` / `processed_events` / `occurrence_records` / `batch_records` / `reservations` / `terminal_occurrence_records` / `terminal_reservations` / `processed_event_tombstones` | ordered collection of typed records | bounded by one epoch (≤ 1 active + ≤ N closed audits, each audit ≤ 10⁴ occurrence records) | session lifetime + audit retention | owning runtime + audit tooling | dropped with the session; closed audits survive until audit retention | content-free structured records only | +| `freshness_policy` | typed degradation policy | 128 ASCII chars; literal enum (`verify_on_version_or_configuration_change`, `verify_on_revision_change`, `infer_only`) | coverage record lifetime | maintainers with release-provenance access | dropped with the coverage record | content-free structured records only | +| `verifier`, `configuration_mode`, `backend`, `control_point_owner` | evidence metadata | 64 ASCII chars (verifier, configuration_mode, backend); 96 ASCII chars (control_point_owner); no secrets or paths | coverage record lifetime | maintainers with release-provenance access | dropped with the coverage record | content-free structured records only | + +### Lineage and source locator fields (per-record) + +| Field | Purpose | Maximum length/cardinality | Retention | Access | Deletion | Export | +|---|---|---|---|---|---|---| +| `root_session_id`, `current_session_id`, `root_agent_id`, `current_agent_id`, `parent_agent_id`, `root_thread_id`, `current_thread_id`, `parent_thread_id`, `fork_occurrence_id`, `turn_id`, `producer_surface`, `producer_instance_id`, `tool_call_id`, `model_item_id`, `dispatch_identity`, `delivery_occurrence_id` | correlate a request, turn, agent, source claim, and authority witness | opaque 96 ASCII chars; `producer_surface` is a closed enum; `dispatch_identity` is a single opaque dispatch ID plus derived sentinels (no per-call secrets) | same as the owning session or coverage record | authorized operators and maintainers | delete runtime lineage with its session; retain static provenance only with the released contract | export only to access-controlled audit channels; never to aggregate telemetry | +| `source_locator` (coverage evidence) | static relative source path under `src/` | 256 ASCII chars; forward-slash relative path only (no `/` or `~` prefix, no `\`); no absolute paths, no home-directory paths, no URLs, or secrets | coverage record lifetime (static) | maintainers with release-provenance access | dropped with the coverage record; release pins remain in the ADR | export only to access-controlled audit channels; never to aggregate telemetry | + +### Aggregate telemetry fields (population-level, never per-session) + +| Field | Purpose | Maximum length/cardinality | Retention | Access | Deletion | Export | |---|---|---|---|---|---|---| -| Runtime/audit opaque IDs, counts, versions, reason codes, timestamps, and witness kinds | deterministic replay, reconciliation, and incident audit | IDs/reason codes are schema-bounded; collection cardinality is bounded by one epoch and configured audit retention | only for configured operational/audit retention | authorized operators and the owning runtime | delete with the owning session/audit-retention policy, subject to required security retention | content-free structured records only | -| Runtime/audit lineage and source locator fields | correlate a request, turn, agent, source claim, and authority witness | typed fixed field set; each value is schema-bounded; no free-form labels | same as the owning session or coverage record | authorized operators and maintainers | delete runtime lineage with its session; retain static provenance only with the released contract | export only to access-controlled audit channels; never to Aggregate telemetry | -| Aggregate telemetry state/reason/version counters | measure coverage state, unavailable decisions, and protocol health | fixed enum dimensions and bounded aggregate buckets; no per-session cardinality | configured aggregate-metrics retention | operators with aggregate-metrics access | delete by aggregate retention schedule | aggregates only; no opaque IDs, lineage, source locator, or user-controlled values | -| Forbidden content | prevent model or user data from escaping the accounting plane | zero: no content, payloads, prompts, tool results, absolute paths, bearer tokens, credentials, or content/artifact hashes | never retained | no access because collection is prohibited | reject before publication; remove affected invalid record under incident procedure | never exported | +| `state`, `reason_code` counters (privacy-only aggregates) | measure coverage state, unavailable decisions, and protocol health | fixed enum dimensions; counter buckets ≤ 10⁴ distinct labels per dimension | configured aggregate-metrics retention (≤ 30 days) | operators with aggregate-metrics access | delete by aggregate retention schedule | aggregates only; no opaque IDs, lineage, source locator, or user-controlled values | +| `version` counters | track pinned protocol version usage | one bucket per released protocol version | configured aggregate-metrics retention (≤ 30 days) | operators with aggregate-metrics access | delete by aggregate retention schedule | aggregates only | + +### Forbidden content (zero cardinality, rejected at the validator) + +The following are forbidden from every serialized contract value, `repr`, and exception message: + +- model content, payloads, prompts, tool results, tool arguments, retrieval bodies, or + `system_message` text; +- absolute paths (any string starting with `/` or `~`, or containing `\\`); +- bearer tokens, credentials, API keys, session cookies, or any string starting with `bearer`, + `sha256:`, `blake2:`, or `content:`; +- content/artifact hashes (any string matching `sha256:[hex]`, `blake2:[hex]`, `content:[hex]`, + or other digest prefixes); +- newlines or carriage returns (`\n`, `\r`) anywhere in a serialized field. + +Validator behavior: each contract type carries a `__post_init__` check that rejects the +construction outright before reduction. Rejection raises `ContextAdmissionValidationError` with a +bounded reason code; the rejected value never reaches storage, telemetry, or the reducer. Tests +in `tests/core/types/test_context_admission_contract.py` and +`tests/core/test_context_admission_coverage.py` freeze this contract by parameterizing against +canary values for every forbidden category. Serialization, `repr`, and exception paths must remain content-free. Contract tests use canaries to prove that content, absolute paths, bearer tokens, and content/artifact hashes cannot escape. diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index a87165aff..e2eb923ec 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -12,6 +12,7 @@ from autoskillit.core import ( CONTEXT_ADMISSION_PROTOCOL_VERSION, + AcceptInputEvent, ActiveContextAdmissionState, AdmissionAttemptId, AdmissionBatch, @@ -26,9 +27,11 @@ AdmissionReservationKey, AdmissionSequence, AdmissionState, + AdmissionWitness, AdmissionWitnessId, AgentInstanceId, AggregateRevision, + AuthoritySourceId, CanonicalRepresentationManifest, CanonicalSpanId, CanonicalSpanOwner, @@ -36,22 +39,29 @@ ContextSessionId, ContextThreadId, ContextWindowSnapshot, + DispatchRequestEvent, + EpochFenceProof, GenerationReservationId, GenerationReservationRecord, GenerationState, IdempotencyNamespace, + MeasurementKind, ModelIdentity, ModelItemId, OpenEpochEvent, + PrepareBatchEvent, ProducerInstanceId, ProducerSurface, ProposeOccurrenceEvent, ProtectedPoolOwnerId, ProtectedPoolSpec, + RepresentationBindingWitness, RepresentationRevision, RequestReconciliationEvent, ReserveClass, ReserveRequestEvent, + RolloverEpochEvent, + StageHistoryEvent, TokenizerIdentity, ToolCallId, TurnId, @@ -188,6 +198,34 @@ def _batch(occurrence: AdmissionOccurrence) -> AdmissionBatch: ) +def _multi_batch(occurrences: tuple[AdmissionOccurrence, ...]) -> AdmissionBatch: + slots = tuple(occurrence.occurrence_id.value.rsplit("-", 1)[-1] for occurrence in occurrences) + request_id = AdmissionRequestId("request-multi-" + "-".join(slots)) + reserve_class = occurrences[0].reserve_class + if any(occurrence.reserve_class is not reserve_class for occurrence in occurrences): + msg = "multi-member batch must share one reserve class" + raise ValueError(msg) + manifest = CanonicalRepresentationManifest( + request_id=request_id, + representation_revision=occurrences[0].representation_revision, + span_owners=tuple( + CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) + for occurrence in occurrences + for span_id in occurrence.owned_span_ids + ), + assembler_identity=ProducerInstanceId("assembler-multi-" + "-".join(slots)), + assembler_witness_id=AdmissionWitnessId("assembler-witness-multi-" + "-".join(slots)), + ) + return AdmissionBatch( + batch_id=AdmissionBatchId("batch-multi-" + "-".join(slots)), + request_id=request_id, + occurrence_ids=tuple(occurrence.occurrence_id for occurrence in occurrences), + reserve_class=reserve_class, + protected_pool_owner_id=_owner(reserve_class), + manifest=manifest, + ) + + def _reservation( occurrence: AdmissionOccurrence, batch: AdmissionBatch, count: int ) -> AdmissionReservation: @@ -214,6 +252,35 @@ def _reservation( ) +def _reservation_for_batch(batch: AdmissionBatch, count: int) -> AdmissionReservation: + occurrence_ids = batch.occurrence_ids + slot = occurrence_ids[0].value.rsplit("-", 1)[-1] + key = AdmissionReservationKey( + idempotency_namespace=_namespace("reserve-multi"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + batch_id=batch.batch_id, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + occurrence_revisions=tuple( + (occurrence_id, batch.manifest.representation_revision) + for occurrence_id in occurrence_ids + ), + ) + return AdmissionReservation( + reservation_id=AdmissionReservationId(f"reservation-multi-{slot}"), + key=key, + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + snapshot_sequence=1, + reserve_class=batch.reserve_class, + protected_pool_owner_id=batch.protected_pool_owner_id, + occurrence_ids=occurrence_ids, + reserved_count=count, + ) + + def _generation( occurrence: AdmissionOccurrence, batch: AdmissionBatch, count: int ) -> GenerationReservationRecord: @@ -235,6 +302,38 @@ def _generation( ) +def _witness( + batch: AdmissionBatch, + kind: WitnessKind, + *, + occurrence_ids: tuple[AdmissionOccurrenceId, ...] | None = None, +) -> AdmissionWitness: + return AdmissionWitness( + witness_id=AdmissionWitnessId(f"{kind.value}-witness-{batch.batch_id.value}"), + kind=kind, + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + snapshot_sequence=1, + request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + occurrence_ids=occurrence_ids or batch.occurrence_ids, + authority_source_id=AuthoritySourceId("authority-state-machine"), + ) + + +def _binding(batch: AdmissionBatch) -> RepresentationBindingWitness: + bound_revision = batch.manifest.representation_revision + return RepresentationBindingWitness( + counted_representation_revision=bound_revision, + dispatched_representation_revision=bound_revision, + final_manifest_revision=bound_revision, + request_id=batch.request_id, + batch_id=batch.batch_id, + authority_source_id=AuthoritySourceId("authority-state-machine"), + ) + + class ContextAdmissionStateMachine(RuleBasedStateMachine): """Compare reducer transitions with a deliberately small accounting oracle.""" @@ -261,6 +360,13 @@ def __init__(self) -> None: self.last_revision = self.state.aggregate_revision.value self.last_admission_sequence = self.state.admission_sequence.value self.closed_audit_count = len(self.state.closed_epochs) + self.last_rollover_retention: int | None = None + + def _find_batch(self, batch_id: AdmissionBatchId) -> Any: + return next( + (record for record in self.state.batch_records if record.batch.batch_id == batch_id), + None, + ) def _fields(self, operation_kind: str) -> dict[str, object]: self.event_sequence += 1 @@ -381,6 +487,153 @@ def stale_fence_is_rejected(self, slot: int) -> None: assert transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert transition.next_state == prior + @rule( + slot=_SLOT, + input_count=st.integers(min_value=1, max_value=10), + ) + def reserve_multi_member_batch(self, slot: int, input_count: int) -> None: + other_slot = 1 if slot == 0 else 0 + if other_slot not in self.occurrences or slot not in self.occurrences: + return + if slot in self.charges or other_slot in self.charges: + return + first = self.occurrences[slot] + second = self.occurrences[other_slot] + if first.reserve_class is not second.reserve_class: + return + batch = _multi_batch((first, second)) + total = input_count * 2 + event = ReserveRequestEvent( + **self._fields("reserve-multi"), + batch=batch, + snapshot_sequence=1, + input_reservations=(_reservation_for_batch(batch, total),), + generation_reservation=None, + ) + prior = self.state + transition = reduce_context_admission(prior, event) + if transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT: + assert transition.next_state == prior + return + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + self.charges[slot] = (first.reserve_class, total, 0) + self.charges[other_slot] = (second.reserve_class, 0, 0) + + @rule() + def prepare_stage_dispatch_and_accept(self) -> None: + if not self.charges: + return + slot = min(self.charges) + reserve_class, input_count, _ = self.charges[slot] + if reserve_class is ReserveClass.ORDINARY: + return + occurrence = self.occurrences[slot] + batch = _batch(occurrence) + reserved = self._find_batch(batch.batch_id) + if reserved is None or reserved.state is not AdmissionState.RESERVED: + return + reserved_input_count = input_count + prepare_event = PrepareBatchEvent( + **self._fields("prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + proposed_charge=reserved_input_count, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("authority-state-machine"), + ) + transition = reduce_context_admission(self.state, prepare_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, prepare_event) + stage_event = StageHistoryEvent( + **self._fields("stage-history"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.HISTORY_STAGED), + ) + transition = reduce_context_admission(self.state, stage_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, stage_event) + dispatch_event = DispatchRequestEvent( + **self._fields("dispatch-request"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ) + transition = reduce_context_admission(self.state, dispatch_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, dispatch_event) + accept_event = AcceptInputEvent( + **self._fields("accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + exact_input_charge=reserved_input_count, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("authority-state-machine"), + representation_binding_witness=_binding(batch), + ) + transition = reduce_context_admission(self.state, accept_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, accept_event) + + @rule() + def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: + before_charges = dict(self.charges) + proof = EpochFenceProof( + old_window_epoch_id=WindowEpochId("epoch-state-machine"), + old_window_epoch_number=1, + new_window_epoch_id=WindowEpochId("epoch-state-machine-2"), + new_window_epoch_number=2, + receiver_authority_source_id=AuthoritySourceId("authority-state-machine"), + fence_witness_id=AdmissionWitnessId("fence-1-to-2"), + highest_admitted_dispatch_sequence=self.state.admission_sequence.value, + ) + rollover_event = RolloverEpochEvent( + **self._fields("rollover-epoch"), + witness=AdmissionWitness( + witness_id=AdmissionWitnessId("rollover-witness-1-to-2"), + kind=WitnessKind.EPOCH_ROLLOVER, + window_epoch_id=WindowEpochId("epoch-state-machine"), + window_epoch_number=1, + snapshot_sequence=1, + request_id=AdmissionRequestId("rollover-request"), + batch_id=AdmissionBatchId("rollover-batch"), + representation_revision=RepresentationRevision("rollover-revision"), + occurrence_ids=(), + authority_source_id=AuthoritySourceId("authority-state-machine"), + ), + fence_proof=proof, + new_snapshot=ContextWindowSnapshot( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId("epoch-state-machine-2"), + window_epoch_number=2, + model_identity=ModelIdentity.anthropic("claude-state-machine"), + tokenizer_identity=TokenizerIdentity("tokenizer-state-machine"), + snapshot_sequence=1, + active_count=50, + hard_limit=100, + remaining_count=50, + ), + protected_pools=_pool_specs(), + ) + transition = reduce_context_admission(self.state, rollover_event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + new_state = transition.next_state + assert isinstance(new_state, ActiveContextAdmissionState) + assert len(new_state.closed_epochs) >= 1 + prior_audit = new_state.closed_epochs[-1] + assert prior_audit.retained_unresolved_count >= 0 + for record in new_state.batch_records: + if record.state in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + }: + owner = record.batch.protected_pool_owner_id + if owner is None: + assert record.batch.reserve_class is ReserveClass.ORDINARY + if before_charges: + self._accept_publication(transition, rollover_event) + self.last_rollover_retention = prior_audit.retained_unresolved_count + @precondition(lambda self: self.latest_replayable_event is not None) @rule() def identical_replay_is_equivalent(self) -> None: diff --git a/tests/docs/test_context_admission_decision.py b/tests/docs/test_context_admission_decision.py index e71993bf3..92f393236 100644 --- a/tests/docs/test_context_admission_decision.py +++ b/tests/docs/test_context_admission_decision.py @@ -251,8 +251,10 @@ def test_upstream_request_contains_all_three_authority_parts_and_minimum_fields( def test_privacy_table_freezes_field_governance(decision_text: str) -> None: privacy = _section(decision_text, "Privacy and observability") for required in [ - "Runtime/audit", - "Aggregate telemetry", + "Runtime/audit fields", + "Lineage and source locator fields", + "Aggregate telemetry fields", + "Forbidden content", "Purpose", "Maximum length/cardinality", "Retention", @@ -270,6 +272,145 @@ def test_privacy_table_freezes_field_governance(decision_text: str) -> None: assert required in privacy +REQUIRED_RUNTIME_AUDIT_FIELDS = ( + "protocol_version", + "aggregate_revision", + "admission_sequence", + "event_id", + "reservation_id", + "witness_id", + "batch_id", + "request_id", + "reservation_key", + "occurrence_id", + "attempt_id", + "delivery_occurrence_id", + "generation_reservation_id", + "reason_code", + "requested_count", + "available_ordinary_count", + "available_protected_count", + "reserved_count", + "committed_input_count", + "unresolved_input_count", + "retained_unresolved_count", + "maximum_allowance", + "exact_terminal_usage", + "injected_count", + "priority", + "predicted_authoritative_maximum", + "active_count", + "hard_limit", + "remaining_count", + "highest_admitted_dispatch_sequence", + "representation_revision", + "tested_version", + "tested_revision", + "publication_revision", + "checked_at", + "freshness_policy", + "verifier", + "configuration_mode", + "backend", + "control_point_owner", +) + +REQUIRED_LINEAGE_FIELDS = ( + "root_session_id", + "current_session_id", + "root_agent_id", + "current_agent_id", + "parent_agent_id", + "root_thread_id", + "current_thread_id", + "parent_thread_id", + "fork_occurrence_id", + "turn_id", + "producer_surface", + "producer_instance_id", + "tool_call_id", + "model_item_id", + "dispatch_identity", + "source_locator", +) + +REQUIRED_AGGREGATE_FIELDS = ( + "state", + "reason_code", + "version", +) + +REQUIRED_FORBIDDEN_CONTENT = ( + "model content", + "payloads", + "prompts", + "tool results", + "absolute paths", + "bearer", + "credentials", + "API keys", + "session cookies", + "content/artifact hashes", + "sha256:", + "blake2:", + "content:", +) + +CONCRETE_MAXIMA_HINTS = ( + "96 ASCII", + "64 ASCII", + "128 ASCII", + "256 ASCII", + "10 ASCII", + "10⁴", + "64-bit non-negative", + "kebab-case", + "ISO-8601", + "30 days", + "no `", + "no absolute", + "no secrets", + "no home-directory", + "no URLs", +) + + +def test_privacy_table_freezes_complete_runtime_audit_field_inventory( + decision_text: str, +) -> None: + privacy = _section(decision_text, "Privacy and observability") + missing = [field for field in REQUIRED_RUNTIME_AUDIT_FIELDS if field not in privacy] + assert not missing, f"missing runtime/audit fields in ADR-0007 privacy table: {missing}" + + +def test_privacy_table_freezes_complete_lineage_field_inventory( + decision_text: str, +) -> None: + privacy = _section(decision_text, "Privacy and observability") + missing = [field for field in REQUIRED_LINEAGE_FIELDS if field not in privacy] + assert not missing, f"missing lineage fields in ADR-0007 privacy table: {missing}" + + +def test_privacy_table_freezes_aggregate_telemetry_field_inventory( + decision_text: str, +) -> None: + privacy = _section(decision_text, "Privacy and observability") + missing = [field for field in REQUIRED_AGGREGATE_FIELDS if field not in privacy] + assert not missing, f"missing aggregate telemetry fields in ADR-0007 privacy table: {missing}" + + +def test_privacy_table_forbids_complete_content_categories(decision_text: str) -> None: + privacy = _section(decision_text, "Privacy and observability").casefold() + missing = [item for item in REQUIRED_FORBIDDEN_CONTENT if item.casefold() not in privacy] + assert not missing, f"missing forbidden-content categories in ADR-0007: {missing}" + + +def test_privacy_table_requires_concrete_field_maxima(decision_text: str) -> None: + privacy = _section(decision_text, "Privacy and observability") + missing = [hint for hint in CONCRETE_MAXIMA_HINTS if hint not in privacy] + assert not missing, f"ADR-0007 privacy table must specify concrete maxima: {missing}" + + def test_decision_keeps_issue_non_goals_explicit(decision_text: str) -> None: non_goals = _section(decision_text, "Non-goals").casefold() for required in [ From f5fcca577fba09f8d45e161a05195044525933e4 Mon Sep 17 00:00:00 2001 From: Trecek Date: Thu, 23 Jul 2026 13:50:26 -0700 Subject: [PATCH 04/62] fix(core): derive rollover source epoch from current state snapshot The state-machine test's rollover rule hardcoded the source epoch identity (epoch 1) and target epoch identity (epoch 2). After the first successful rollover, the active state moves to epoch 2; a second invocation then used a fence proof whose old_window_epoch_id/old_window_epoch_number did not match state.snapshot.window_epoch_id/state.snapshot.window_epoch_number, and the reducer's stale-receiver-fence check at src/autoskillit/core/context_admission.py:1591-1599 rejected the event. Make the rollover rule derive old_window_epoch_id/old_window_epoch_number from self.state.snapshot and bump new_window_epoch_number dynamically. Also make fence/rollover witness IDs unique per invocation by including self.event_sequence to avoid duplicate witness IDs in retained audit chains. --- .../test_context_admission_state_machine.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index e2eb923ec..111507147 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -578,23 +578,31 @@ def prepare_stage_dispatch_and_accept(self) -> None: @rule() def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: before_charges = dict(self.charges) + old_snapshot = self.state.snapshot + new_window_epoch_number = old_snapshot.window_epoch_number + 1 + new_window_epoch_id = WindowEpochId(f"epoch-state-machine-{new_window_epoch_number}") + event_fields = self._fields("rollover-epoch") + witness_suffix = ( + f"{old_snapshot.window_epoch_number}-to-" + f"{new_window_epoch_number}-{self.event_sequence}" + ) proof = EpochFenceProof( - old_window_epoch_id=WindowEpochId("epoch-state-machine"), - old_window_epoch_number=1, - new_window_epoch_id=WindowEpochId("epoch-state-machine-2"), - new_window_epoch_number=2, + old_window_epoch_id=old_snapshot.window_epoch_id, + old_window_epoch_number=old_snapshot.window_epoch_number, + new_window_epoch_id=new_window_epoch_id, + new_window_epoch_number=new_window_epoch_number, receiver_authority_source_id=AuthoritySourceId("authority-state-machine"), - fence_witness_id=AdmissionWitnessId("fence-1-to-2"), + fence_witness_id=AdmissionWitnessId(f"fence-{witness_suffix}"), highest_admitted_dispatch_sequence=self.state.admission_sequence.value, ) rollover_event = RolloverEpochEvent( - **self._fields("rollover-epoch"), + **event_fields, witness=AdmissionWitness( - witness_id=AdmissionWitnessId("rollover-witness-1-to-2"), + witness_id=AdmissionWitnessId(f"rollover-witness-{witness_suffix}"), kind=WitnessKind.EPOCH_ROLLOVER, - window_epoch_id=WindowEpochId("epoch-state-machine"), - window_epoch_number=1, - snapshot_sequence=1, + window_epoch_id=old_snapshot.window_epoch_id, + window_epoch_number=old_snapshot.window_epoch_number, + snapshot_sequence=old_snapshot.snapshot_sequence, request_id=AdmissionRequestId("rollover-request"), batch_id=AdmissionBatchId("rollover-batch"), representation_revision=RepresentationRevision("rollover-revision"), @@ -604,8 +612,8 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: fence_proof=proof, new_snapshot=ContextWindowSnapshot( protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, - window_epoch_id=WindowEpochId("epoch-state-machine-2"), - window_epoch_number=2, + window_epoch_id=new_window_epoch_id, + window_epoch_number=new_window_epoch_number, model_identity=ModelIdentity.anthropic("claude-state-machine"), tokenizer_identity=TokenizerIdentity("tokenizer-state-machine"), snapshot_sequence=1, From 9aaddb6ceb8b7f907e35ded94a2b010cfd209c4c Mon Sep 17 00:00:00 2001 From: Trecek Date: Thu, 23 Jul 2026 18:51:09 -0700 Subject: [PATCH 05/62] fix(core): complete context admission audit remediation --- docs/decisions/0007-context-admission.md | 6 + src/autoskillit/core/context_admission.py | 1487 ++++++++++++++--- .../core/types/_type_context_admission.py | 789 ++++++++- src/autoskillit/core/types/_type_enums.py | 1 + tests/arch/test_subpackage_isolation.py | 4 +- tests/core/test_context_admission_reducer.py | 534 +++++- .../test_context_admission_state_machine.py | 422 ++++- .../types/test_context_admission_contract.py | 494 ++++++ tests/docs/test_context_admission_decision.py | 55 +- 9 files changed, 3418 insertions(+), 374 deletions(-) diff --git a/docs/decisions/0007-context-admission.md b/docs/decisions/0007-context-admission.md index bc68fa1f7..3d0025621 100644 --- a/docs/decisions/0007-context-admission.md +++ b/docs/decisions/0007-context-admission.md @@ -319,12 +319,18 @@ Primary source evidence at that exact commit: - [App Server raw-response documentation](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/app-server/README.md) - [compaction identity](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/protocol/src/protocol.rs) - [four-byte truncation heuristic](https://github.com/openai/codex/blob/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/utils/string/src/truncate.rs) +- [generated hook schemas](https://github.com/openai/codex/tree/25af12f7e61572b0bc18ddb1008be543b91519b0/codex-rs/hooks/schema/generated) Official provider documentation: [input-token-count API reference](https://platform.openai.com/docs/api-reference/responses/input-tokens), [token-counting guide](https://platform.openai.com/docs/guides/token-counting), and [compaction guide](https://platform.openai.com/docs/guides/compaction). +Current hook coverage is documented separately by the +[App Server hook-discovery and `hooks/list` contract](https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md#hooks). +That moving documentation describes current client-visible coverage; it is not substituted for +the immutable 0.145.0 source pins above. + The `token_budget` capability is disabled and experimental. `get_context_remaining` is a model-callable/polling observation, not an atomic reservation available synchronously to every producer. Internal `base_window_tokens_remaining`, terminal usage, and rollout evidence are diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index 8813dfa72..229f6e1e2 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -14,6 +14,7 @@ AdmissionBatchRecord, AdmissionDecision, AdmissionEffect, + AdmissionOccurrenceId, AdmissionOccurrenceRecord, AdmissionReplay, AdmissionReservation, @@ -21,6 +22,7 @@ AdmissionSequence, AdmissionTransition, AdmissionWitness, + AdmissionWitnessId, AggregateRevision, AuthorityUnavailableEffect, AuthorityUnavailableEvent, @@ -30,6 +32,7 @@ ContextAdmissionEvent, ContextAdmissionState, ContextAdmissionValidationError, + ContextWindowSnapshot, DispatchRequestEvent, EpochClosedEffect, ExpiredIdempotencyTombstone, @@ -127,19 +130,18 @@ def _occurrence_effects( ) -def _accepted_effects( +def _acceptance_effects( state: ActiveContextAdmissionState, event: AcceptInputEvent | ResolveIndeterminateAcceptedEvent, record: AdmissionBatchRecord, exact_charge: int, witness: AdmissionWitness, + *, + quarantine_reason_code: str | None, ) -> tuple[AdmissionEffect, ...]: reservation = _reservation_for(state, record) if reservation is None: return () - quarantined = ( - exact_charge > reservation.reserved_count or exact_charge > state.snapshot.hard_limit - ) revision, sequence = _effect_coordinates(state, capacity_changed=True) effects: tuple[AdmissionEffect, ...] = ( ChargeCommittedEffect( @@ -160,23 +162,48 @@ def _accepted_effects( event, record.batch, record.state, - AdmissionState.QUARANTINED if quarantined else AdmissionState.COMMITTED, + ( + AdmissionState.QUARANTINED + if quarantine_reason_code is not None + else AdmissionState.COMMITTED + ), capacity_changed=True, ), ) - if quarantined: + if quarantine_reason_code is not None: effects += ( QuarantineRecordedEffect( source_event_id=event.event_id, resulting_aggregate_revision=revision, resulting_admission_sequence=sequence, target_id=record.batch.batch_id, - reason_code="provider_charge_exceeds_reservation", + reason_code=quarantine_reason_code, ), ) return effects +def _accepted_effects( + state: ActiveContextAdmissionState, + event: AcceptInputEvent | ResolveIndeterminateAcceptedEvent, + record: AdmissionBatchRecord, + exact_charge: int, + witness: AdmissionWitness, +) -> tuple[AdmissionEffect, ...]: + reservation = _reservation_for(state, record) + quarantined = reservation is not None and ( + exact_charge > reservation.reserved_count or exact_charge > state.snapshot.hard_limit + ) + return _acceptance_effects( + state, + event, + record, + exact_charge, + witness, + quarantine_reason_code=("provider_charge_exceeds_reservation" if quarantined else None), + ) + + def _reservation_for( state: ActiveContextAdmissionState, record: AdmissionBatchRecord, @@ -291,22 +318,43 @@ def _decision( def _reject( state: ContextAdmissionState, + event: ContextAdmissionEvent, reason_code: str, *, requested_count: int = 0, reserve_class: ReserveClass = ReserveClass.ORDINARY, protected_pool_owner_id: ProtectedPoolOwnerId | None = None, ) -> AdmissionTransition: - return AdmissionTransition( - next_state=state, - decision=_decision( - state, - AdmissionDecisionKind.WOULD_REJECT, - reason_code, - requested_count=requested_count, - reserve_class=reserve_class, - protected_pool_owner_id=protected_pool_owner_id, + decision = _decision( + state, + AdmissionDecisionKind.WOULD_REJECT, + reason_code, + requested_count=requested_count, + reserve_class=reserve_class, + protected_pool_owner_id=protected_pool_owner_id, + ) + processed = ProcessedEventRecord( + event_id=event.event_id, + event=event, + original_decision=decision, + aggregate_revision=state.aggregate_revision, + admission_sequence=state.admission_sequence, + ) + next_state = replace( + state, + processed_events=tuple( + sorted( + state.processed_events + (processed,), + key=lambda record: ( + record.aggregate_revision.value, + record.event_id.value, + ), + ) ), + ) + return AdmissionTransition( + next_state=next_state, + decision=decision, effects=(), ) @@ -352,7 +400,15 @@ def _publish( ) published = replace( published, - processed_events=published.processed_events + (processed,), + processed_events=tuple( + sorted( + published.processed_events + (processed,), + key=lambda record: ( + record.aggregate_revision.value, + record.event_id.value, + ), + ) + ), ) return AdmissionTransition( next_state=published, @@ -454,7 +510,7 @@ def _preflight( effects=(), ) if event.expected_aggregate_revision != state.aggregate_revision: - return _reject(state, "stale_revision") + return _reject(state, event, "stale_revision") return None @@ -468,9 +524,26 @@ def _batch_record( ) -def _dispatched_count(state: ActiveContextAdmissionState) -> int: - return sum( - 1 for record in state.batch_records if record.state is AdmissionState.REQUEST_DISPATCHED +def _highest_dispatch_sequence(state: ActiveContextAdmissionState) -> int: + return max( + ( + record.dispatch_sequence + for record in state.batch_records + if record.dispatch_sequence is not None + ), + default=0, + ) + + +def _append_witness_ids( + witness_ids: tuple[AdmissionWitnessId, ...], + *new_witness_ids: AdmissionWitnessId, +) -> tuple[AdmissionWitnessId, ...]: + return tuple( + sorted( + {*witness_ids, *new_witness_ids}, + key=lambda witness_id: witness_id.value, + ) ) @@ -492,23 +565,48 @@ def _replace_batch_record( state: ActiveContextAdmissionState, updated: AdmissionBatchRecord, ) -> ActiveContextAdmissionState: + member_ids = set(updated.batch.occurrence_ids) return replace( state, batch_records=tuple( updated if record.batch.batch_id == updated.batch.batch_id else record for record in state.batch_records ), + occurrence_records=tuple( + replace( + record, + state=updated.state, + batch_id=updated.batch.batch_id, + reservation_id=updated.reservation_id, + ) + if record.occurrence.occurrence_id in member_ids + else record + for record in state.occurrence_records + ), ) -def _replace_batch_record_quarantine( +def _quarantined_acceptance_state( state: ActiveContextAdmissionState, record: AdmissionBatchRecord, -) -> tuple[AdmissionBatchRecord, ...]: - quarantined = replace(record, state=AdmissionState.QUARANTINED) - return tuple( - quarantined if other.batch.batch_id == record.batch.batch_id else other - for other in state.batch_records + witness: AdmissionWitness, + exact_charge: int, + reason_code: str, +) -> ActiveContextAdmissionState: + quarantined = replace( + record, + state=AdmissionState.QUARANTINED, + witness_ids=_append_witness_ids(record.witness_ids, witness.witness_id), + committed_input_count=exact_charge, + unresolved_input_count=0, + ) + next_state = _replace_batch_record(state, quarantined) + return _set_occurrence_state( + next_state, + record.batch, + AdmissionState.QUARANTINED, + witness=witness, + quarantine_reason_code=reason_code, ) @@ -530,7 +628,7 @@ def _set_occurrence_state( continue witness_ids = record.accepted_witness_ids if witness is not None and witness.witness_id not in witness_ids: - witness_ids += (witness.witness_id,) + witness_ids = _append_witness_ids(witness_ids, witness.witness_id) records.append( replace( record, @@ -552,12 +650,26 @@ def _validate_witness( batch: AdmissionBatch, witness: AdmissionWitness, expected_kind: WitnessKind, +) -> bool: + return _validate_witness_for_snapshot( + state.snapshot, + batch, + witness, + expected_kind, + ) + + +def _validate_witness_for_snapshot( + snapshot: ContextWindowSnapshot, + batch: AdmissionBatch, + witness: AdmissionWitness, + expected_kind: WitnessKind, ) -> bool: return ( witness.kind is expected_kind - and witness.window_epoch_id == state.snapshot.window_epoch_id - and witness.window_epoch_number == state.snapshot.window_epoch_number - and witness.snapshot_sequence == state.snapshot.snapshot_sequence + and witness.window_epoch_id == snapshot.window_epoch_id + and witness.window_epoch_number == snapshot.window_epoch_number + and witness.snapshot_sequence == snapshot.snapshot_sequence and witness.request_id == batch.request_id and witness.batch_id == batch.batch_id and witness.representation_revision == batch.manifest.representation_revision @@ -565,12 +677,84 @@ def _validate_witness( ) +def _closed_batch_location( + state: ActiveContextAdmissionState, + batch_id: AdmissionBatchId, +) -> tuple[int, ClosedEpochAudit, AdmissionBatchRecord] | None: + for index, audit in enumerate(state.closed_epochs): + for record in audit.terminal_batch_records: + if record.batch.batch_id == batch_id: + return index, audit, record + return None + + +def _closed_generation_location( + state: ActiveContextAdmissionState, + reservation_id: GenerationReservationId, +) -> tuple[int, ClosedEpochAudit, GenerationReservationRecord] | None: + for index, audit in enumerate(state.closed_epochs): + for record in audit.terminal_generation_reservations: + if record.generation_reservation_id == reservation_id: + return index, audit, record + return None + + +def _closed_reservation_for( + audit: ClosedEpochAudit, + record: AdmissionBatchRecord, +) -> AdmissionReservation | None: + if record.reservation_id is None: + return None + return next( + ( + reservation + for reservation in audit.terminal_reservations + if reservation.reservation_id == record.reservation_id + ), + None, + ) + + +def _closed_retained_input_count( + audit: ClosedEpochAudit, + records: tuple[AdmissionBatchRecord, ...], +) -> int: + return sum( + record.unresolved_input_count + or ( + reservation.reserved_count + if (reservation := _closed_reservation_for(audit, record)) is not None + else 0 + ) + for record in records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + + +def _replace_closed_audit( + state: ActiveContextAdmissionState, + index: int, + audit: ClosedEpochAudit, +) -> ActiveContextAdmissionState: + return replace( + state, + closed_epochs=tuple( + audit if item_index == index else item + for item_index, item in enumerate(state.closed_epochs) + ), + ) + + def _open_epoch( state: ContextAdmissionState, event: OpenEpochEvent, ) -> AdmissionTransition: if not isinstance(state, UninitializedContextAdmissionState): - return _reject(state, "epoch_already_active") + return _reject(state, event, "epoch_already_active") try: active = ActiveContextAdmissionState( protocol_version=state.protocol_version, @@ -588,7 +772,7 @@ def _open_epoch( closed_epochs=state.closed_epochs, ) except ContextAdmissionValidationError: - return _reject(state, "invalid_epoch_snapshot") + return _reject(state, event, "invalid_epoch_snapshot") return _publish(state, active, event) @@ -597,13 +781,28 @@ def _propose( event: ProposeOccurrenceEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") occurrence = event.occurrence + lineage = occurrence.lineage + is_fork_work = ( + lineage.current_session_id != lineage.root_session_id + or lineage.current_agent_id != lineage.root_agent_id + or lineage.current_thread_id != lineage.root_thread_id + or lineage.parent_agent_id is not None + or lineage.parent_thread_id is not None + or lineage.fork_occurrence_id is not None + ) + is_parent_delivery = ( + occurrence.producer_surface is ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY + and lineage.delivery_occurrence_id is not None + ) if ( occurrence.lineage.window_epoch_id != state.snapshot.window_epoch_id or occurrence.lineage.window_epoch_number != state.snapshot.window_epoch_number ): - return _reject(state, "occurrence_epoch_mismatch") + return _reject(state, event, "occurrence_epoch_mismatch") + if is_fork_work and not is_parent_delivery: + return _reject(state, event, "fork_requires_distinct_epoch") existing = next( ( record @@ -637,7 +836,15 @@ def _propose( ) return _publish( state, - replace(state, occurrence_records=state.occurrence_records + (record,)), + replace( + state, + occurrence_records=tuple( + sorted( + state.occurrence_records + (record,), + key=lambda item: item.occurrence.occurrence_id.value, + ) + ), + ), event, ) @@ -647,16 +854,16 @@ def _reserve( event: ReserveRequestEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") if event.snapshot_sequence != state.snapshot.snapshot_sequence: - return _reject(state, "snapshot_sequence_mismatch") + return _reject(state, event, "snapshot_sequence_mismatch") if _batch_record(state, event.batch.batch_id) is not None: - return _reject(state, "batch_already_reserved") + return _reject(state, event, "batch_already_reserved") reservation = event.input_reservations[0] if any( existing.reservation_id == reservation.reservation_id for existing in state.reservations ): - return _reject(state, "reservation_id_reuse_with_changed_descriptor") + return _reject(state, event, "reservation_id_reuse_with_changed_descriptor") member_records = tuple( record for record in state.occurrence_records @@ -672,9 +879,24 @@ def _reserve( for record in member_records ) ): - return _reject(state, "batch_members_not_all_proposed") + return _reject(state, event, "batch_members_not_all_proposed") + owned_pairs = tuple( + (span_id, member.occurrence.occurrence_id) + for member in member_records + for span_id in member.occurrence.owned_span_ids + ) + owned_span_ids = tuple(span_id for span_id, _ in owned_pairs) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) for owner in event.batch.manifest.span_owners + ) + if ( + len(owned_span_ids) != len(set(owned_span_ids)) + or set(owned_pairs) != set(manifest_pairs) + or len(owned_pairs) != len(manifest_pairs) + ): + return _reject(state, event, "inconsistent_span_ownership") if len(event.input_reservations) != 1: - return _reject(state, "atomic_input_reservation_required") + return _reject(state, event, "atomic_input_reservation_required") if ( reservation.key.batch_id != event.batch.batch_id or reservation.occurrence_ids != event.batch.occurrence_ids @@ -684,7 +906,7 @@ def _reserve( or reservation.reserve_class is not event.batch.reserve_class or reservation.protected_pool_owner_id != event.batch.protected_pool_owner_id ): - return _reject(state, "reservation_descriptor_mismatch") + return _reject(state, event, "reservation_descriptor_mismatch") expected_revisions = tuple( ( record.occurrence.occurrence_id, @@ -693,18 +915,21 @@ def _reserve( for record in member_records ) if reservation.key.occurrence_revisions != expected_revisions: - return _reject(state, "reservation_revision_mismatch") + return _reject(state, event, "reservation_revision_mismatch") generation = event.generation_reservation generation_count = generation.maximum_allowance if generation is not None else 0 if generation is not None and ( generation.request_id != event.batch.request_id + or generation.batch_id != event.batch.batch_id + or generation.representation_revision != event.batch.manifest.representation_revision + or generation.occurrence_ids != event.batch.occurrence_ids or generation.window_epoch_id != state.snapshot.window_epoch_id or generation.window_epoch_number != state.snapshot.window_epoch_number or generation.snapshot_sequence != state.snapshot.snapshot_sequence or generation.reserve_class is not event.batch.reserve_class or generation.protected_pool_owner_id != event.batch.protected_pool_owner_id ): - return _reject(state, "generation_descriptor_mismatch") + return _reject(state, event, "generation_descriptor_mismatch") requested = reservation.reserved_count + generation_count global_available, ordinary_available, pool_available = _capacity(state) if event.batch.protected_pool_owner_id is None: @@ -723,6 +948,7 @@ def _reserve( if requested > available: return _reject( state, + event, "insufficient_capacity", requested_count=requested, reserve_class=event.batch.reserve_class, @@ -736,14 +962,41 @@ def _reserve( prepared_input_count=None, committed_input_count=0, unresolved_input_count=0, + dispatch_sequence=None, + ) + member_ids = set(event.batch.occurrence_ids) + reserved_occurrence_records = tuple( + replace( + record, + state=AdmissionState.RESERVED, + batch_id=event.batch.batch_id, + reservation_id=reservation.reservation_id, + ) + if record.occurrence.occurrence_id in member_ids + else record + for record in state.occurrence_records ) next_state = replace( state, - batch_records=state.batch_records + (batch_record,), - reservations=state.reservations + event.input_reservations, - generation_reservations=( - state.generation_reservations - + ((generation,) if generation is not None and generation_count > 0 else ()) + occurrence_records=reserved_occurrence_records, + batch_records=tuple( + sorted( + state.batch_records + (batch_record,), + key=lambda item: item.batch.batch_id.value, + ) + ), + reservations=tuple( + sorted( + state.reservations + event.input_reservations, + key=lambda item: item.reservation_id.value, + ) + ), + generation_reservations=tuple( + sorted( + state.generation_reservations + + ((generation,) if generation is not None and generation_count > 0 else ()), + key=lambda item: item.generation_reservation_id.value, + ) ), ) reserve_decision = _decision( @@ -756,26 +1009,28 @@ def _reserve( ) next_state = replace( next_state, - idempotency_records=next_state.idempotency_records - + ( - IdempotencyRecord( - namespace=event.idempotency_namespace, - reservation_key=reservation.key, - original_descriptor=event, - original_reserve_decision=reserve_decision, - owning_event_id=event.event_id, - publication_revision=type(state.aggregate_revision)( - state.aggregate_revision.value + 1 + idempotency_records=tuple( + sorted( + next_state.idempotency_records + + ( + IdempotencyRecord( + namespace=event.idempotency_namespace, + reservation_key=reservation.key, + original_descriptor=event, + original_reserve_decision=reserve_decision, + owning_event_id=event.event_id, + publication_revision=type(state.aggregate_revision)( + state.aggregate_revision.value + 1 + ), + ), + ), + key=lambda item: ( + item.publication_revision.value, + item.owning_event_id.value, ), ), ), ) - next_state = _set_occurrence_state( - next_state, - event.batch, - AdmissionState.RESERVED, - reservation_id=reservation.reservation_id, - ) revision, sequence = _effect_coordinates(state, capacity_changed=True) effects: tuple[AdmissionEffect, ...] = ( ReservationRecordedEffect( @@ -833,20 +1088,20 @@ def _prepare( event: PrepareBatchEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") record = _batch_record(state, event.batch_id) if record is None or record.state is not AdmissionState.RESERVED: - return _reject(state, "illegal_prepare_order") + return _reject(state, event, "illegal_prepare_order") if event.representation_revision != record.batch.manifest.representation_revision: - return _reject(state, "representation_revision_mismatch") + return _reject(state, event, "representation_revision_mismatch") reservation = _reservation_for(state, record) if reservation is None or event.proposed_charge != reservation.reserved_count: - return _reject(state, "prepared_charge_mismatch") + return _reject(state, event, "prepared_charge_mismatch") if event.measurement_kind not in { MeasurementKind.PROVIDER_EXACT, MeasurementKind.TOKENIZER_EXACT, }: - return _reject(state, "non_authoritative_measurement") + return _reject(state, event, "non_authoritative_measurement") updated = replace( record, state=AdmissionState.PREPARED, @@ -878,7 +1133,7 @@ def _stage( event: StageHistoryEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") record = _batch_record(state, event.batch_id) if ( record is None @@ -890,11 +1145,14 @@ def _stage( WitnessKind.HISTORY_STAGED, ) ): - return _reject(state, "invalid_history_stage_witness") + return _reject(state, event, "invalid_history_stage_witness") updated = replace( record, state=AdmissionState.HISTORY_STAGED, - witness_ids=record.witness_ids + (event.witness.witness_id,), + witness_ids=_append_witness_ids( + record.witness_ids, + event.witness.witness_id, + ), ) next_state = _replace_batch_record(state, updated) next_state = _set_occurrence_state( @@ -923,7 +1181,7 @@ def _dispatch( event: DispatchRequestEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") record = _batch_record(state, event.batch_id) if ( record is None @@ -935,11 +1193,15 @@ def _dispatch( WitnessKind.REQUEST_INCLUDED, ) ): - return _reject(state, "invalid_request_inclusion_witness") + return _reject(state, event, "invalid_request_inclusion_witness") updated = replace( record, state=AdmissionState.REQUEST_DISPATCHED, - witness_ids=record.witness_ids + (event.witness.witness_id,), + dispatch_sequence=_highest_dispatch_sequence(state) + 1, + witness_ids=_append_witness_ids( + record.witness_ids, + event.witness.witness_id, + ), ) next_state = _replace_batch_record(state, updated) next_state = _set_occurrence_state( @@ -979,7 +1241,7 @@ def _accepted_state( updated = replace( record, state=lifecycle, - witness_ids=record.witness_ids + (witness.witness_id,), + witness_ids=_append_witness_ids(record.witness_ids, witness.witness_id), committed_input_count=exact_charge, unresolved_input_count=0, ) @@ -998,102 +1260,272 @@ def _accepted_state( ) +def _accept_closed_input( + state: ActiveContextAdmissionState, + event: AcceptInputEvent | ResolveIndeterminateAcceptedEvent, + location: tuple[int, ClosedEpochAudit, AdmissionBatchRecord], +) -> AdmissionTransition: + index, audit, record = location + expected_state = ( + AdmissionState.REQUEST_DISPATCHED + if isinstance(event, AcceptInputEvent) + else AdmissionState.INDETERMINATE + ) + binding = event.representation_binding_witness + expected_revision = record.batch.manifest.representation_revision + exact_charge = ( + event.exact_input_charge if isinstance(event, AcceptInputEvent) else event.exact_charge + ) + if ( + record.state is not expected_state + or not _validate_witness_for_snapshot( + audit.snapshot, + record.batch, + event.witness, + WitnessKind.PROVIDER_ACCEPTED, + ) + or event.measurement_kind is not MeasurementKind.PROVIDER_EXACT + or event.final_manifest_revision != expected_revision + or event.final_manifest.representation_revision != expected_revision + or event.final_manifest.request_id != record.batch.request_id + or binding.counted_representation_revision != expected_revision + or binding.dispatched_representation_revision != expected_revision + or binding.final_manifest_revision != expected_revision + or binding.request_id != record.batch.request_id + or binding.batch_id != record.batch.batch_id + or event.authority_source_id != event.witness.authority_source_id + or event.authority_source_id != binding.authority_source_id + ): + return _reject(state, event, "invalid_closed_epoch_acceptance") + reservation = _closed_reservation_for(audit, record) + if reservation is None: + return _reject(state, event, "missing_closed_epoch_reservation") + member_ids = set(record.batch.occurrence_ids) + expected_owned_pairs = tuple( + (span_id, item.occurrence.occurrence_id) + for item in audit.terminal_occurrence_records + if item.occurrence.occurrence_id in member_ids + for span_id in item.occurrence.owned_span_ids + ) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) for owner in event.final_manifest.span_owners + ) + manifest_invalid = ( + len({span_id for span_id, _ in expected_owned_pairs}) != len(expected_owned_pairs) + or set(manifest_pairs) != set(expected_owned_pairs) + or len(manifest_pairs) != len(expected_owned_pairs) + ) + quarantined = ( + exact_charge > reservation.reserved_count + or exact_charge > audit.snapshot.hard_limit + or manifest_invalid + ) + reason_code = ( + "incomplete_canonical_span_ownership" + if manifest_invalid + else "provider_charge_exceeds_reservation" + if quarantined + else "accepted" + ) + lifecycle = AdmissionState.QUARANTINED if quarantined else AdmissionState.COMMITTED + updated_record = replace( + record, + state=lifecycle, + witness_ids=_append_witness_ids( + record.witness_ids, + event.witness.witness_id, + ), + committed_input_count=exact_charge, + unresolved_input_count=0, + ) + batch_records = tuple( + updated_record if item.batch.batch_id == record.batch.batch_id else item + for item in audit.terminal_batch_records + ) + occurrence_records = tuple( + replace( + item, + state=lifecycle, + accepted_witness_ids=_append_witness_ids( + item.accepted_witness_ids, + event.witness.witness_id, + ), + quarantine_reason_code=(reason_code if quarantined else None), + ) + if item.occurrence.occurrence_id in member_ids + else item + for item in audit.terminal_occurrence_records + ) + updated_audit = replace( + audit, + terminal_occurrence_records=occurrence_records, + terminal_batch_records=batch_records, + retained_unresolved_count=_closed_retained_input_count( + audit, + batch_records, + ), + ) + next_state = _replace_closed_audit(state, index, updated_audit) + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + ChargeCommittedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=record.batch.batch_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + count=exact_charge, + window_epoch_id=audit.snapshot.window_epoch_id, + snapshot_sequence=audit.snapshot.snapshot_sequence, + witness_ids=(event.witness.witness_id,), + ), + *_occurrence_effects( + state, + event, + record.batch, + record.state, + lifecycle, + capacity_changed=True, + ), + ) + if quarantined: + effects += ( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=record.batch.batch_id, + reason_code=reason_code, + ), + ) + return _publish( + state, + next_state, + event, + kind=( + AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT + ), + reason_code=reason_code, + requested_count=exact_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=effects, + ) + + def _accept( state: ContextAdmissionState, event: AcceptInputEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") record = _batch_record(state, event.batch_id) + if record is None: + location = _closed_batch_location(state, event.batch_id) + if location is not None: + return _accept_closed_input(state, event, location) if record is None or record.state is not AdmissionState.REQUEST_DISPATCHED: - return _reject(state, "illegal_accept_order") + return _reject(state, event, "illegal_accept_order") if not _validate_witness( state, record.batch, event.witness, WitnessKind.PROVIDER_ACCEPTED, ): - return _reject(state, "invalid_provider_acceptance_witness") + return _reject(state, event, "invalid_provider_acceptance_witness") binding = event.representation_binding_witness expected_revision = record.batch.manifest.representation_revision if ( event.final_manifest_revision != expected_revision + or event.final_manifest.representation_revision != expected_revision + or event.final_manifest.request_id != record.batch.request_id or binding.counted_representation_revision != expected_revision or binding.dispatched_representation_revision != expected_revision or binding.final_manifest_revision != expected_revision or binding.request_id != record.batch.request_id or binding.batch_id != record.batch.batch_id ): - return _reject(state, "representation_revision_mismatch") - if event.measurement_kind not in { - MeasurementKind.PROVIDER_EXACT, - MeasurementKind.TOKENIZER_EXACT, - }: - return _reject(state, "non_authoritative_measurement") + return _reject(state, event, "representation_revision_mismatch") + if event.measurement_kind is not MeasurementKind.PROVIDER_EXACT: + return _reject(state, event, "non_authoritative_measurement") if event.exact_input_charge < 0: - return _reject(state, "invalid_exact_charge") - expected_owned_spans: set[CanonicalSpanId] = set() + return _reject(state, event, "invalid_exact_charge") + expected_owned_spans: list[CanonicalSpanId] = [] + expected_owned_pairs: list[tuple[CanonicalSpanId, AdmissionOccurrenceId]] = [] for occurrence in state.occurrence_records: if occurrence.occurrence.occurrence_id in set(record.batch.occurrence_ids): - expected_owned_spans.update(occurrence.occurrence.owned_span_ids) - manifest_spans = {owner.span_id for owner in record.batch.manifest.span_owners} + expected_owned_spans.extend(occurrence.occurrence.owned_span_ids) + expected_owned_pairs.extend( + (span_id, occurrence.occurrence.occurrence_id) + for span_id in occurrence.occurrence.owned_span_ids + ) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) for owner in event.final_manifest.span_owners + ) if ( event.authority_source_id != event.witness.authority_source_id or event.authority_source_id != binding.authority_source_id ): - coord = _effect_coordinates(state, capacity_changed=True) - quarantined_record = replace( + reason_code = "authority_source_mismatch" + next_state = _quarantined_acceptance_state( + state, record, - state=AdmissionState.QUARANTINED, - witness_ids=record.witness_ids + (event.witness.witness_id,), - committed_input_count=event.exact_input_charge, - unresolved_input_count=0, + event.witness, + event.exact_input_charge, + reason_code, ) - next_state = _replace_batch_record(state, quarantined_record) - next_state = _set_occurrence_state( - next_state, - record.batch, - AdmissionState.QUARANTINED, - witness=event.witness, - quarantine_reason_code="authority_source_mismatch", - ) - return _publish( - state, + return _publish( + state, next_state, event, kind=AdmissionDecisionKind.QUARANTINED, - reason_code="authority_source_mismatch", + reason_code=reason_code, requested_count=event.exact_input_charge, reserve_class=record.batch.reserve_class, protected_pool_owner_id=record.batch.protected_pool_owner_id, capacity_changed=True, - effects=( - QuarantineRecordedEffect( - source_event_id=event.event_id, - resulting_aggregate_revision=coord[0], - resulting_admission_sequence=coord[1], - target_id=record.batch.batch_id, - reason_code="authority_source_mismatch", - ), + effects=_acceptance_effects( + state, + event, + record, + event.exact_input_charge, + event.witness, + quarantine_reason_code=reason_code, ), ) - if manifest_spans != expected_owned_spans: - coord = _effect_coordinates(state, capacity_changed=True) + if ( + len(expected_owned_spans) != len(set(expected_owned_spans)) + or set(manifest_pairs) != set(expected_owned_pairs) + or len(manifest_pairs) != len(expected_owned_pairs) + ): + reason_code = "incomplete_canonical_span_ownership" + next_state = _quarantined_acceptance_state( + state, + record, + event.witness, + event.exact_input_charge, + reason_code, + ) return _publish( state, - replace(state, batch_records=_replace_batch_record_quarantine(state, record)), + next_state, event, kind=AdmissionDecisionKind.QUARANTINED, - reason_code="incomplete_canonical_span_ownership", + reason_code=reason_code, + requested_count=event.exact_input_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, capacity_changed=True, - effects=( - QuarantineRecordedEffect( - source_event_id=event.event_id, - resulting_aggregate_revision=coord[0], - resulting_admission_sequence=coord[1], - target_id=record.batch.batch_id, - reason_code="incomplete_canonical_span_ownership", - ), + effects=_acceptance_effects( + state, + event, + record, + event.exact_input_charge, + event.witness, + quarantine_reason_code=reason_code, ), ) next_state, kind, reason = _accepted_state( @@ -1122,6 +1554,202 @@ def _accept( ) +def _release_closed_batch( + state: ActiveContextAdmissionState, + event: ( + ReleaseNonAdmissionEvent + | RollbackAdmissionEvent + | ResolveIndeterminateNonAdmissionEvent + | ResolveIndeterminateRollbackEvent + ), + location: tuple[int, ClosedEpochAudit, AdmissionBatchRecord], +) -> AdmissionTransition: + index, audit, record = location + is_release = isinstance( + event, + ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, + ) + is_resolution = isinstance( + event, + ResolveIndeterminateNonAdmissionEvent | ResolveIndeterminateRollbackEvent, + ) + expected_state = ( + AdmissionState.INDETERMINATE if is_resolution else AdmissionState.REQUEST_DISPATCHED + ) + expected_kind = WitnessKind.NON_ADMISSION if is_release else WitnessKind.ROLLBACK + history_removal_witness = ( + event.history_removal_witness + if isinstance( + event, + ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, + ) + else None + ) + if ( + record.state is not expected_state + or not _validate_witness_for_snapshot( + audit.snapshot, + record.batch, + event.witness, + expected_kind, + ) + or ( + is_release + and ( + history_removal_witness is None + or not _validate_witness_for_snapshot( + audit.snapshot, + record.batch, + history_removal_witness, + WitnessKind.ROLLBACK, + ) + or history_removal_witness.witness_id == event.witness.witness_id + ) + ) + ): + return _reject(state, event, "invalid_closed_epoch_resolution") + lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK + witness_ids = _append_witness_ids( + record.witness_ids, + event.witness.witness_id, + *((history_removal_witness.witness_id,) if history_removal_witness is not None else ()), + ) + updated_record = replace( + record, + state=lifecycle, + witness_ids=witness_ids, + unresolved_input_count=0, + ) + batch_records = tuple( + updated_record if item.batch.batch_id == record.batch.batch_id else item + for item in audit.terminal_batch_records + ) + member_ids = set(record.batch.occurrence_ids) + occurrence_records = tuple( + replace( + item, + state=lifecycle, + accepted_witness_ids=_append_witness_ids( + item.accepted_witness_ids, + event.witness.witness_id, + *( + (history_removal_witness.witness_id,) + if history_removal_witness is not None + else () + ), + ), + indeterminate_reason_code=None, + ) + if item.occurrence.occurrence_id in member_ids + else item + for item in audit.terminal_occurrence_records + ) + updated_audit = replace( + audit, + terminal_occurrence_records=occurrence_records, + terminal_batch_records=batch_records, + retained_unresolved_count=_closed_retained_input_count( + audit, + batch_records, + ), + ) + next_state = _replace_closed_audit(state, index, updated_audit) + reservation = _closed_reservation_for(audit, record) + effects: tuple[AdmissionEffect, ...] = _occurrence_effects( + state, + event, + record.batch, + record.state, + lifecycle, + capacity_changed=True, + ) + if reservation is not None: + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects = ( + ReservationReleasedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=reservation.reservation_id, + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=reservation.reserve_class, + protected_pool_owner_id=reservation.protected_pool_owner_id, + count=reservation.reserved_count, + window_epoch_id=reservation.window_epoch_id, + snapshot_sequence=reservation.snapshot_sequence, + witness_ids=_append_witness_ids( + (event.witness.witness_id,), + *( + (history_removal_witness.witness_id,) + if history_removal_witness is not None + else () + ), + ), + ), + *effects, + ) + generation_effects: tuple[AdmissionEffect, ...] = () + generation_records: list[GenerationReservationRecord] = [] + revision, sequence = _effect_coordinates(state, capacity_changed=True) + for generation in audit.terminal_generation_reservations: + if generation.request_id == record.batch.request_id and generation.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + }: + generation_records.append( + replace( + generation, + state=GenerationState.INVALIDATED, + witness_ids=_append_witness_ids( + generation.witness_ids, + event.witness.witness_id, + ), + ) + ) + generation_effects += ( + ReservationInvalidatedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=generation.maximum_allowance, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, + witness_ids=(event.witness.witness_id,), + ), + ) + else: + generation_records.append(generation) + if generation_effects: + updated_audit = replace( + updated_audit, + terminal_generation_reservations=tuple(generation_records), + retained_generation_count=sum( + generation.maximum_allowance + for generation in generation_records + if generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ), + ) + next_state = _replace_closed_audit(state, index, updated_audit) + effects += generation_effects + return _publish( + state, + next_state, + event, + capacity_changed=True, + effects=effects, + ) + + def _release_or_rollback( state: ContextAdmissionState, event: ( @@ -1132,10 +1760,13 @@ def _release_or_rollback( ), ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") record = _batch_record(state, event.batch_id) if record is None: - return _reject(state, "unknown_batch") + location = _closed_batch_location(state, event.batch_id) + if location is not None: + return _release_closed_batch(state, event, location) + return _reject(state, event, "unknown_batch") is_release = isinstance( event, ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, @@ -1158,7 +1789,6 @@ def _release_or_rollback( allowed_states = { AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED, - AdmissionState.INDETERMINATE, } if record.state not in allowed_states or not _validate_witness( state, @@ -1166,18 +1796,48 @@ def _release_or_rollback( event.witness, expected_witness, ): - return _reject(state, "invalid_release_or_rollback_witness") + return _reject(state, event, "invalid_release_or_rollback_witness") + history_removal_witness = ( + event.history_removal_witness + if isinstance( + event, + ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, + ) + else None + ) if ( is_release - and record.state in {AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED} - and event.witness.kind is not WitnessKind.NON_ADMISSION + and record.state + in { + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + and ( + history_removal_witness is None + or not _validate_witness( + state, + record.batch, + history_removal_witness, + WitnessKind.ROLLBACK, + ) + or history_removal_witness.witness_id == event.witness.witness_id + ) ): - return _reject(state, "staged_release_requires_non_admission_witness") + return _reject(state, event, "staged_release_requires_history_removal") lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK updated = replace( record, state=lifecycle, - witness_ids=record.witness_ids + (event.witness.witness_id,), + witness_ids=_append_witness_ids( + record.witness_ids, + event.witness.witness_id, + *( + (history_removal_witness.witness_id,) + if history_removal_witness is not None + else () + ), + ), unresolved_input_count=0, ) next_state = _replace_batch_record(state, updated) @@ -1187,6 +1847,13 @@ def _release_or_rollback( lifecycle, witness=event.witness, ) + if history_removal_witness is not None: + next_state = _set_occurrence_state( + next_state, + record.batch, + lifecycle, + witness=history_removal_witness, + ) effects: tuple[AdmissionEffect, ...] = _occurrence_effects( state, event, @@ -1210,10 +1877,59 @@ def _release_or_rollback( count=reservation.reserved_count, window_epoch_id=reservation.window_epoch_id, snapshot_sequence=reservation.snapshot_sequence, - witness_ids=(event.witness.witness_id,), + witness_ids=_append_witness_ids( + (event.witness.witness_id,), + *( + (history_removal_witness.witness_id,) + if history_removal_witness is not None + else () + ), + ), ), *effects, ) + generation_effects: tuple[AdmissionEffect, ...] = () + generation_records: list[GenerationReservationRecord] = [] + revision, sequence = _effect_coordinates(state, capacity_changed=True) + for generation in next_state.generation_reservations: + if generation.request_id == record.batch.request_id and generation.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + }: + generation_records.append( + replace( + generation, + state=GenerationState.INVALIDATED, + witness_ids=_append_witness_ids( + generation.witness_ids, + event.witness.witness_id, + ), + ) + ) + generation_effects += ( + ReservationInvalidatedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=generation.maximum_allowance, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, + witness_ids=(event.witness.witness_id,), + ), + ) + else: + generation_records.append(generation) + if generation_effects: + next_state = replace( + next_state, + generation_reservations=tuple(generation_records), + ) + effects += generation_effects return _publish( state, next_state, @@ -1228,14 +1944,14 @@ def _mark_indeterminate( event: MarkIndeterminateEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") record = _batch_record(state, event.batch_id) if record is None or record.state not in { AdmissionState.PREPARED, AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED, }: - return _reject(state, "illegal_indeterminate_order") + return _reject(state, event, "illegal_indeterminate_order") reservation = _reservation_for(state, record) unresolved = reservation.reserved_count if reservation is not None else 0 updated = replace( @@ -1270,8 +1986,16 @@ def _resolve_indeterminate_accepted( event: ResolveIndeterminateAcceptedEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") record = _batch_record(state, event.batch_id) + if record is None: + location = _closed_batch_location(state, event.batch_id) + if location is not None: + return _accept_closed_input(state, event, location) + binding = event.representation_binding_witness + expected_revision = ( + record.batch.manifest.representation_revision if record is not None else None + ) if ( record is None or record.state is not AdmissionState.INDETERMINATE @@ -1281,8 +2005,61 @@ def _resolve_indeterminate_accepted( event.witness, WitnessKind.PROVIDER_ACCEPTED, ) + or event.authority_source_id != event.witness.authority_source_id + or event.authority_source_id != binding.authority_source_id + or event.measurement_kind is not MeasurementKind.PROVIDER_EXACT + or event.final_manifest_revision != expected_revision + or event.final_manifest.representation_revision != expected_revision + or event.final_manifest.request_id != record.batch.request_id + or binding.counted_representation_revision != expected_revision + or binding.dispatched_representation_revision != expected_revision + or binding.final_manifest_revision != expected_revision + or binding.request_id != record.batch.request_id + or binding.batch_id != record.batch.batch_id + ): + return _reject(state, event, "invalid_indeterminate_acceptance") + member_ids = set(record.batch.occurrence_ids) + expected_owned_pairs = tuple( + (span_id, item.occurrence.occurrence_id) + for item in state.occurrence_records + if item.occurrence.occurrence_id in member_ids + for span_id in item.occurrence.owned_span_ids + ) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) for owner in event.final_manifest.span_owners + ) + if ( + len({span_id for span_id, _ in expected_owned_pairs}) != len(expected_owned_pairs) + or set(manifest_pairs) != set(expected_owned_pairs) + or len(manifest_pairs) != len(expected_owned_pairs) ): - return _reject(state, "invalid_indeterminate_acceptance") + reason_code = "incomplete_canonical_span_ownership" + next_state = _quarantined_acceptance_state( + state, + record, + event.witness, + event.exact_charge, + reason_code, + ) + return _publish( + state, + next_state, + event, + kind=AdmissionDecisionKind.QUARANTINED, + reason_code=reason_code, + requested_count=event.exact_charge, + reserve_class=record.batch.reserve_class, + protected_pool_owner_id=record.batch.protected_pool_owner_id, + capacity_changed=True, + effects=_acceptance_effects( + state, + event, + record, + event.exact_charge, + event.witness, + quarantine_reason_code=reason_code, + ), + ) next_state, kind, reason = _accepted_state( state, record, @@ -1314,36 +2091,42 @@ def _start_generation( event: StartGenerationEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") generation = _generation_record(state, event.generation_reservation_id) if generation is None or generation.state is not GenerationState.RESERVED: - return _reject(state, "illegal_generation_start") - batch_record = next( - ( - record - for record in state.batch_records - if record.batch.request_id == generation.request_id - ), - None, - ) + return _reject(state, event, "illegal_generation_start") + batch_record = _batch_record(state, generation.batch_id) batch = batch_record.batch if batch_record is not None else None if ( batch is None or batch_record is None or batch_record.state - not in {AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED} + not in { + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.COMMITTED, + AdmissionState.QUARANTINED, + } or not _validate_witness( state, batch, event.witness, WitnessKind.REQUEST_INCLUDED, ) + or ( + generation.authority_source_id is not None + and generation.authority_source_id != event.witness.authority_source_id + ) ): - return _reject(state, "invalid_generation_start_witness") + return _reject(state, event, "invalid_generation_start_witness") updated = replace( generation, state=GenerationState.STREAMING, - witness_ids=generation.witness_ids + (event.witness.witness_id,), + authority_source_id=event.witness.authority_source_id, + witness_ids=_append_witness_ids( + generation.witness_ids, + event.witness.witness_id, + ), ) next_state = replace( state, @@ -1357,29 +2140,155 @@ def _start_generation( return _publish(state, next_state, event) +def _reconcile_closed_generation( + state: ActiveContextAdmissionState, + event: ReconcileGenerationEvent, + location: tuple[int, ClosedEpochAudit, GenerationReservationRecord], +) -> AdmissionTransition: + index, audit, generation = location + batch_record = next( + ( + record + for record in audit.terminal_batch_records + if record.batch.batch_id == generation.batch_id + ), + None, + ) + if ( + generation.state + not in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + or batch_record is None + or not _validate_witness_for_snapshot( + audit.snapshot, + batch_record.batch, + event.output_usage_witness, + WitnessKind.OUTPUT_USAGE, + ) + or ( + generation.authority_source_id is not None + and generation.authority_source_id != event.output_usage_witness.authority_source_id + ) + ): + return _reject(state, event, "invalid_closed_generation_witness") + quarantined = event.exact_output_usage > generation.maximum_allowance + updated = replace( + generation, + state=(GenerationState.QUARANTINED if quarantined else GenerationState.RECONCILED), + exact_terminal_usage=event.exact_output_usage, + witness_ids=_append_witness_ids( + generation.witness_ids, + event.output_usage_witness.witness_id, + ), + authority_source_id=event.output_usage_witness.authority_source_id, + ) + generation_records = tuple( + updated if item.generation_reservation_id == generation.generation_reservation_id else item + for item in audit.terminal_generation_reservations + ) + retained_generation_count = sum( + item.maximum_allowance + for item in generation_records + if item.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + updated_audit = replace( + audit, + terminal_generation_reservations=generation_records, + retained_generation_count=retained_generation_count, + ) + next_state = _replace_closed_audit(state, index, updated_audit) + revision, sequence = _effect_coordinates(state, capacity_changed=True) + effects: tuple[AdmissionEffect, ...] = ( + GenerationReconciledEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=event.exact_output_usage, + window_epoch_id=audit.snapshot.window_epoch_id, + snapshot_sequence=audit.snapshot.snapshot_sequence, + witness_ids=(event.output_usage_witness.witness_id,), + ), + ) + reason_code = "generation_usage_exceeds_allowance" if quarantined else "accepted" + if quarantined: + effects += ( + QuarantineRecordedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=generation.generation_reservation_id, + reason_code=reason_code, + ), + ) + return _publish( + state, + next_state, + event, + kind=( + AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT + ), + reason_code=reason_code, + capacity_changed=True, + effects=effects, + ) + + def _reconcile_generation( state: ContextAdmissionState, event: ReconcileGenerationEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") generation = _generation_record(state, event.generation_reservation_id) + if generation is None: + location = _closed_generation_location( + state, + event.generation_reservation_id, + ) + if location is not None: + return _reconcile_closed_generation(state, event, location) if generation is None or generation.state not in { - GenerationState.RESERVED, GenerationState.STREAMING, GenerationState.INDETERMINATE, }: - return _reject(state, "illegal_generation_reconciliation") - if event.output_usage_witness.kind is not WitnessKind.OUTPUT_USAGE: - return _reject(state, "invalid_output_usage_witness") - if event.output_usage_witness.authority_source_id.value == "": - return _reject(state, "missing_witness_authority_source") + return _reject(state, event, "illegal_generation_reconciliation") + batch_record = _batch_record(state, generation.batch_id) + if ( + batch_record is None + or not _validate_witness( + state, + batch_record.batch, + event.output_usage_witness, + WitnessKind.OUTPUT_USAGE, + ) + or ( + generation.authority_source_id is not None + and generation.authority_source_id != event.output_usage_witness.authority_source_id + ) + ): + return _reject(state, event, "invalid_output_usage_witness") quarantined = event.exact_output_usage > generation.maximum_allowance updated = replace( generation, state=(GenerationState.QUARANTINED if quarantined else GenerationState.RECONCILED), exact_terminal_usage=event.exact_output_usage, - witness_ids=generation.witness_ids + (event.output_usage_witness.witness_id,), + authority_source_id=event.output_usage_witness.authority_source_id, + witness_ids=_append_witness_ids( + generation.witness_ids, + event.output_usage_witness.witness_id, + ), ) next_state = replace( state, @@ -1434,13 +2343,13 @@ def _mark_generation_indeterminate( event: MarkGenerationIndeterminateEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") generation = _generation_record(state, event.generation_reservation_id) if generation is None or generation.state not in { GenerationState.RESERVED, GenerationState.STREAMING, }: - return _reject(state, "illegal_generation_indeterminate") + return _reject(state, event, "illegal_generation_indeterminate") updated = replace(generation, state=GenerationState.INDETERMINATE) next_state = replace( state, @@ -1459,7 +2368,7 @@ def _request_reconciliation( event: RequestReconciliationEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") batch = ( _batch_record(state, event.target_id) if isinstance(event.target_id, AdmissionBatchId) @@ -1470,6 +2379,16 @@ def _request_reconciliation( if isinstance(event.target_id, GenerationReservationId) else None ) + closed_batch = ( + _closed_batch_location(state, event.target_id) + if isinstance(event.target_id, AdmissionBatchId) + else None + ) + closed_generation = ( + _closed_generation_location(state, event.target_id) + if isinstance(event.target_id, GenerationReservationId) + else None + ) if not ( ( batch is not None @@ -1491,8 +2410,25 @@ def _request_reconciliation( GenerationState.INDETERMINATE, } ) + or ( + closed_batch is not None + and closed_batch[2].state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + or ( + closed_generation is not None + and closed_generation[2].state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) ): - return _reject(state, "reconciliation_target_not_unresolved") + return _reject(state, event, "reconciliation_target_not_unresolved") revision, sequence = _effect_coordinates(state, capacity_changed=False) effect_type = ( ReconciliationEscalationEffect @@ -1528,7 +2464,7 @@ def _expire_idempotency( None, ) if record is None: - return _reject(state, "idempotency_key_not_terminal") + return _reject(state, event, "idempotency_key_not_terminal") if isinstance(state, ActiveContextAdmissionState): batch_record = _batch_record( state, @@ -1541,14 +2477,14 @@ def _expire_idempotency( AdmissionState.INVALIDATED, AdmissionState.QUARANTINED, }: - return _reject(state, "idempotency_key_not_terminal") + return _reject(state, event, "idempotency_key_not_terminal") if event.expiry_witness.kind is not WitnessKind.IDEMPOTENCY_EXPIRY: - return _reject(state, "invalid_expiry_witness") + return _reject(state, event, "invalid_expiry_witness") if ( event.expiry_witness.window_epoch_id != event.reservation_key.window_epoch_id or event.expiry_witness.window_epoch_number != event.reservation_key.window_epoch_number ): - return _reject(state, "expiry_epoch_mismatch") + return _reject(state, event, "expiry_epoch_mismatch") tombstone = ExpiredIdempotencyTombstone( namespace=record.namespace, reservation_key=record.reservation_key, @@ -1558,7 +2494,15 @@ def _expire_idempotency( ) next_state = replace( state, - expired_idempotency_tombstones=(state.expired_idempotency_tombstones + (tombstone,)), + expired_idempotency_tombstones=tuple( + sorted( + state.expired_idempotency_tombstones + (tombstone,), + key=lambda item: ( + item.reservation_key.window_epoch_number, + item.reservation_key.batch_id.value, + ), + ) + ), ) revision, sequence = _effect_coordinates(state, capacity_changed=False) reservation = record.original_descriptor.input_reservations[0] @@ -1584,19 +2528,79 @@ def _rollover( event: RolloverEpochEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, "epoch_uninitialized") + return _reject(state, event, "epoch_uninitialized") proof = event.fence_proof if ( event.witness.kind is not WitnessKind.EPOCH_ROLLOVER - or proof.old_window_epoch_id != state.snapshot.window_epoch_id - or proof.old_window_epoch_number != state.snapshot.window_epoch_number - or proof.new_window_epoch_id != event.new_snapshot.window_epoch_id - or proof.new_window_epoch_number != event.new_snapshot.window_epoch_number - or proof.new_window_epoch_number <= proof.old_window_epoch_number - or proof.receiver_authority_source_id != event.witness.authority_source_id - or proof.highest_admitted_dispatch_sequence < len(state.reservations) + or event.witness.window_epoch_id != state.snapshot.window_epoch_id + or event.witness.window_epoch_number != state.snapshot.window_epoch_number + or event.witness.snapshot_sequence != state.snapshot.snapshot_sequence + or event.new_snapshot.window_epoch_number <= state.snapshot.window_epoch_number + or event.new_snapshot.window_epoch_id == state.snapshot.window_epoch_id ): - return _reject(state, "stale_receiver_fence") + return _reject(state, event, "invalid_rollover_witness") + unresolved_batch_records = tuple( + record + for record in state.batch_records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + retained_generation_request_ids = { + record.batch.request_id + for record in state.batch_records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.COMMITTED, + AdmissionState.INDETERMINATE, + AdmissionState.QUARANTINED, + } + } + retained_unresolved_count = sum( + record.unresolved_input_count + or ( + reservation.reserved_count + if (reservation := _reservation_for(state, record)) is not None + else 0 + ) + for record in unresolved_batch_records + ) + retained_generation_count = sum( + generation.maximum_allowance + for generation in state.generation_reservations + if generation.request_id in retained_generation_request_ids + and generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + retained_total = retained_unresolved_count + retained_generation_count + receiver_fence_valid = proof is not None and ( + proof.old_window_epoch_id == state.snapshot.window_epoch_id + and proof.old_window_epoch_number == state.snapshot.window_epoch_number + and proof.new_window_epoch_id == event.new_snapshot.window_epoch_id + and proof.new_window_epoch_number == event.new_snapshot.window_epoch_number + and proof.receiver_authority_source_id == event.witness.authority_source_id + and proof.highest_admitted_dispatch_sequence == _highest_dispatch_sequence(state) + ) + fully_resolved = retained_total == 0 and event.deducted_unresolved_count == 0 + snapshot_deducts_unresolved = ( + retained_total > 0 + and event.deducted_unresolved_count == retained_total + and event.new_snapshot.active_count >= retained_total + ) + authority_alternative_valid = ( + receiver_fence_valid + if proof is not None + else (fully_resolved or snapshot_deducts_unresolved) + ) + if not authority_alternative_valid: + return _reject(state, event, "stale_receiver_fence") terminal_occurrences = tuple( replace( record, @@ -1614,66 +2618,53 @@ def _rollover( ) for record in state.occurrence_records ) - retained_batch_records = tuple( - record + terminal_batch_records = tuple( + replace( + record, + state=( + AdmissionState.INVALIDATED + if record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + else record.state + ), + ) for record in state.batch_records - if record.state - in { - AdmissionState.REQUEST_DISPATCHED, - AdmissionState.INDETERMINATE, - } - ) - retained_reservation_ids = { - record.reservation_id for record in retained_batch_records if record.reservation_id - } - retained_reservations = tuple( - reservation - for reservation in state.reservations - if reservation.reservation_id in retained_reservation_ids ) - retained_generation_ids = {record.batch.batch_id for record in retained_batch_records} - retained_generation_reservations = tuple( - generation - for generation in state.generation_reservations - if generation.request_id in {record.batch.request_id for record in retained_batch_records} - or generation.state + terminal_generation_reservations = tuple( + replace( + generation, + state=GenerationState.INVALIDATED, + ) + if generation.state in { GenerationState.RESERVED, GenerationState.STREAMING, GenerationState.INDETERMINATE, } - and any( - record.batch.request_id == generation.request_id - and record.state is AdmissionState.REQUEST_DISPATCHED - for record in retained_batch_records - ) + and generation.request_id not in retained_generation_request_ids + else generation + for generation in state.generation_reservations ) - retained_unresolved_count = 0 - retained_generation_count = 0 - for record in state.batch_records: - if record.state in { - AdmissionState.REQUEST_DISPATCHED, - AdmissionState.INDETERMINATE, - }: - reservation = _reservation_for(state, record) - retained_unresolved_count += record.unresolved_input_count or ( - reservation.reserved_count if reservation is not None else 0 - ) - for generation in state.generation_reservations: - if generation.state in { - GenerationState.RESERVED, - GenerationState.STREAMING, - GenerationState.INDETERMINATE, - } and any(request_id == generation.request_id for request_id in retained_generation_ids): - retained_generation_count += generation.maximum_allowance audit = ClosedEpochAudit( snapshot=state.snapshot, terminal_occurrence_records=terminal_occurrences, + terminal_batch_records=terminal_batch_records, terminal_reservations=state.reservations, + terminal_generation_reservations=terminal_generation_reservations, closure_witness_id=event.witness.witness_id, fence_proof=proof, - processed_event_tombstones=tuple(record.event_id for record in state.processed_events), + processed_event_tombstones=tuple( + sorted( + (record.event_id for record in state.processed_events), + key=lambda event_id: event_id.value, + ) + ), retained_unresolved_count=retained_unresolved_count, + retained_generation_count=retained_generation_count, ) try: next_state = ActiveContextAdmissionState( @@ -1683,18 +2674,27 @@ def _rollover( snapshot=event.new_snapshot, protected_pools=event.protected_pools, occurrence_records=(), - batch_records=retained_batch_records, - reservations=retained_reservations, - generation_reservations=retained_generation_reservations, + batch_records=(), + reservations=(), + generation_reservations=(), processed_events=state.processed_events, idempotency_records=state.idempotency_records, expired_idempotency_tombstones=state.expired_idempotency_tombstones, - closed_epochs=state.closed_epochs + (audit,), + closed_epochs=tuple( + sorted( + state.closed_epochs + (audit,), + key=lambda item: item.snapshot.window_epoch_number, + ) + ), ) except ContextAdmissionValidationError: - return _reject(state, "invalid_rollover_snapshot") - _ = retained_generation_count # retained for future CapacityRecord accounting + return _reject(state, event, "invalid_rollover_snapshot") revision, sequence = _effect_coordinates(state, capacity_changed=True) + rollover_witness_ids = _append_witness_ids( + (), + event.witness.witness_id, + *((proof.fence_witness_id,) if proof is not None else ()), + ) invalidation_effects = tuple( ReservationInvalidatedEffect( source_event_id=event.event_id, @@ -1707,7 +2707,7 @@ def _rollover( count=reservation.reserved_count, window_epoch_id=reservation.window_epoch_id, snapshot_sequence=reservation.snapshot_sequence, - witness_ids=(event.witness.witness_id, proof.fence_witness_id), + witness_ids=rollover_witness_ids, ) for record in state.batch_records if record.state @@ -1719,6 +2719,27 @@ def _rollover( for reservation in state.reservations if reservation.reservation_id == record.reservation_id ) + generation_invalidation_effects = tuple( + ReservationInvalidatedEffect( + source_event_id=event.event_id, + resulting_aggregate_revision=revision, + resulting_admission_sequence=sequence, + target_id=prior.generation_reservation_id, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + reserve_class=prior.reserve_class, + protected_pool_owner_id=prior.protected_pool_owner_id, + count=prior.maximum_allowance, + window_epoch_id=prior.window_epoch_id, + snapshot_sequence=prior.snapshot_sequence, + witness_ids=rollover_witness_ids, + ) + for prior, terminal in zip( + state.generation_reservations, + terminal_generation_reservations, + strict=True, + ) + if prior.state is not terminal.state + ) occurrence_effects = tuple( OccurrenceStateChangedEffect( source_event_id=event.event_id, @@ -1737,6 +2758,7 @@ def _rollover( ) effects: tuple[AdmissionEffect, ...] = ( *invalidation_effects, + *generation_invalidation_effects, *occurrence_effects, EpochClosedEffect( source_event_id=event.event_id, @@ -1744,6 +2766,7 @@ def _rollover( resulting_admission_sequence=sequence, target_id=state.snapshot.window_epoch_id, fence_proof=proof, + deducted_unresolved_count=event.deducted_unresolved_count, ), ) return _publish( diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index e5660caa9..923d55472 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -7,7 +7,8 @@ from __future__ import annotations -from collections.abc import Mapping +import re +from collections.abc import Callable, Mapping from dataclasses import dataclass, field from enum import StrEnum from typing import Any, ClassVar, Never, TypeAlias @@ -29,6 +30,17 @@ CONTEXT_ADMISSION_PROTOCOL_VERSION = 1 +_CONTENT_FREE_TEXT = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:@+-]*\Z") +_CONTENT_FREE_LOCATOR = re.compile(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]*\Z") +_SENSITIVE_TEXT_MARKERS = ( + "authorization", + "bearer", + "content:", + "password", + "secret", + "token=", +) + class ContextAdmissionValidationError(ValueError): """Raised when a protocol value violates a content-free invariant.""" @@ -75,20 +87,23 @@ def _validate_bounded_text( reason_code: str, *, maximum: int = 128, + locator: bool = False, ) -> None: if not isinstance(value, str) or not value or len(value) > maximum: _raise_invalid(reason_code) lowered = value.casefold() + pattern = _CONTENT_FREE_LOCATOR if locator else _CONTENT_FREE_TEXT if ( - lowered.startswith("bearer") + any(marker in lowered for marker in _SENSITIVE_TEXT_MARKERS) or lowered.startswith("sha256:") or lowered.startswith("blake2:") - or lowered.startswith("content:") or "\n" in value or "\r" in value or value.startswith("/") or "\\" in value or value.startswith("~") + or not pattern.fullmatch(value) + or (locator and ".." in value.split("/")) ): _raise_invalid(reason_code) @@ -98,6 +113,17 @@ def _validate_tuple(value: object, reason_code: str) -> None: _raise_invalid(reason_code) +def _validate_canonical_tuple( + value: tuple[Any, ...], + reason_code: str, + *, + key: Callable[[Any], Any], +) -> None: + _validate_tuple(value, reason_code) + if value != tuple(sorted(value, key=key)): + _raise_invalid(reason_code) + + def _encode(value: object) -> object: if isinstance(value, DispatchIdentity): return {"dispatch_id": value.dispatch_id} @@ -412,6 +438,15 @@ def __post_init__(self) -> None: _raise_invalid("invalid_dispatch_identity") if self.producer_surface in _NON_DISPATCH_PRODUCER_SURFACES: _raise_invalid("dispatch_identity_on_non_dispatch_surface") + is_parent_delivery = self.producer_surface is ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY + if is_parent_delivery != (self.delivery_occurrence_id is not None): + _raise_invalid("invalid_parent_delivery_lineage") + if is_parent_delivery and ( + self.parent_agent_id is None + or self.parent_thread_id is None + or self.fork_occurrence_id is None + ): + _raise_invalid("incomplete_parent_delivery_lineage") @dataclass(frozen=True, slots=True) @@ -442,6 +477,21 @@ def __post_init__(self) -> None: or self.active_count + self.remaining_count > self.hard_limit ): _raise_invalid("invalid_authoritative_snapshot") + for model_value in ( + self.model_identity.configured_model, + self.model_identity.effective_model, + ): + _validate_bounded_text( + model_value, + "invalid_model_identity", + maximum=128, + ) + if self.model_identity.profile_name: + _validate_bounded_text( + self.model_identity.profile_name, + "invalid_model_identity", + maximum=64, + ) @dataclass(frozen=True, slots=True) @@ -459,7 +509,11 @@ class CanonicalRepresentationManifest(_ContractValue): assembler_witness_id: AdmissionWitnessId def __post_init__(self) -> None: - _validate_tuple(self.span_owners, "invalid_span_owners") + _validate_canonical_tuple( + self.span_owners, + "noncanonical_span_owners", + key=lambda owner: (owner.span_id.value, owner.occurrence_id.value), + ) if not self.span_owners: _raise_invalid("incomplete_representation_manifest") span_ids = tuple(owner.span_id for owner in self.span_owners) @@ -482,7 +536,11 @@ def __post_init__(self) -> None: self.predicted_authoritative_maximum, "invalid_predicted_authoritative_maximum", ) - _validate_tuple(self.owned_span_ids, "invalid_owned_span_ids") + _validate_canonical_tuple( + self.owned_span_ids, + "noncanonical_owned_span_ids", + key=lambda span_id: span_id.value, + ) if not self.owned_span_ids or len(self.owned_span_ids) != len(set(self.owned_span_ids)): _raise_invalid("invalid_owned_span_ids") if self.producer_surface is not self.lineage.producer_surface: @@ -499,7 +557,11 @@ class AdmissionBatch(_ContractValue): manifest: CanonicalRepresentationManifest def __post_init__(self) -> None: - _validate_tuple(self.occurrence_ids, "invalid_batch_occurrences") + _validate_canonical_tuple( + self.occurrence_ids, + "noncanonical_batch_occurrences", + key=lambda occurrence_id: occurrence_id.value, + ) if not self.occurrence_ids or len(self.occurrence_ids) != len(set(self.occurrence_ids)): _raise_invalid("invalid_batch_occurrences") if self.manifest.request_id != self.request_id: @@ -525,7 +587,11 @@ class AdmissionReservationKey(_ContractValue): def __post_init__(self) -> None: _validate_protocol_version(self.protocol_version) _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") - _validate_tuple(self.occurrence_revisions, "invalid_occurrence_revisions") + _validate_canonical_tuple( + self.occurrence_revisions, + "noncanonical_occurrence_revisions", + key=lambda pair: (pair[0].value, pair[1].value), + ) occurrence_ids = tuple(pair[0] for pair in self.occurrence_revisions) if ( not occurrence_ids @@ -552,7 +618,11 @@ def __post_init__(self) -> None: _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") _validate_non_negative(self.reserved_count, "invalid_reserved_count") - _validate_tuple(self.occurrence_ids, "invalid_reservation_occurrences") + _validate_canonical_tuple( + self.occurrence_ids, + "noncanonical_reservation_occurrences", + key=lambda occurrence_id: occurrence_id.value, + ) if ( self.window_epoch_id != self.key.window_epoch_id or self.window_epoch_number != self.key.window_epoch_number @@ -580,7 +650,11 @@ class AdmissionWitness(_ContractValue): def __post_init__(self) -> None: _validate_non_negative(self.window_epoch_number, "invalid_window_epoch_number") _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") - _validate_tuple(self.occurrence_ids, "invalid_witness_occurrences") + _validate_canonical_tuple( + self.occurrence_ids, + "noncanonical_witness_occurrences", + key=lambda occurrence_id: occurrence_id.value, + ) @dataclass(frozen=True, slots=True) @@ -660,7 +734,11 @@ class AdmissionOccurrenceRecord(_ContractValue): quarantine_reason_code: str | None def __post_init__(self) -> None: - _validate_tuple(self.accepted_witness_ids, "invalid_witness_ids") + _validate_canonical_tuple( + self.accepted_witness_ids, + "noncanonical_witness_ids", + key=lambda witness_id: witness_id.value, + ) if len(self.accepted_witness_ids) != len(set(self.accepted_witness_ids)): _raise_invalid("duplicate_witness_id") if self.indeterminate_reason_code is not None: @@ -686,15 +764,22 @@ class AdmissionBatchRecord(_ContractValue): prepared_input_count: int | None committed_input_count: int unresolved_input_count: int + dispatch_sequence: int | None def __post_init__(self) -> None: - _validate_tuple(self.witness_ids, "invalid_witness_ids") + _validate_canonical_tuple( + self.witness_ids, + "noncanonical_witness_ids", + key=lambda witness_id: witness_id.value, + ) if len(self.witness_ids) != len(set(self.witness_ids)): _raise_invalid("duplicate_witness_id") if self.prepared_input_count is not None: _validate_non_negative(self.prepared_input_count, "invalid_prepared_count") _validate_non_negative(self.committed_input_count, "invalid_committed_count") _validate_non_negative(self.unresolved_input_count, "invalid_unresolved_count") + if self.dispatch_sequence is not None: + _validate_non_negative(self.dispatch_sequence, "invalid_dispatch_sequence") if self.committed_input_count > 0 and self.unresolved_input_count > 0: _raise_invalid("committed_and_unresolved_simultaneously") @@ -703,6 +788,9 @@ def __post_init__(self) -> None: class GenerationReservationRecord(_ContractValue): generation_reservation_id: GenerationReservationId request_id: AdmissionRequestId + batch_id: AdmissionBatchId + representation_revision: RepresentationRevision + occurrence_ids: tuple[AdmissionOccurrenceId, ...] response_id: ModelItemId window_epoch_id: WindowEpochId window_epoch_number: int @@ -713,6 +801,7 @@ class GenerationReservationRecord(_ContractValue): state: GenerationState exact_terminal_usage: int | None witness_ids: tuple[AdmissionWitnessId, ...] + authority_source_id: AuthoritySourceId | None def __post_init__(self) -> None: for value in ( @@ -725,6 +814,20 @@ def __post_init__(self) -> None: _validate_non_negative(self.exact_terminal_usage, "invalid_generation_usage") if (self.reserve_class is ReserveClass.ORDINARY) != (self.protected_pool_owner_id is None): _raise_invalid("invalid_generation_owner") + _validate_canonical_tuple( + self.occurrence_ids, + "noncanonical_generation_occurrences", + key=lambda occurrence_id: occurrence_id.value, + ) + if not self.occurrence_ids or len(self.occurrence_ids) != len(set(self.occurrence_ids)): + _raise_invalid("invalid_generation_occurrences") + _validate_canonical_tuple( + self.witness_ids, + "noncanonical_witness_ids", + key=lambda witness_id: witness_id.value, + ) + if len(self.witness_ids) != len(set(self.witness_ids)): + _raise_invalid("duplicate_witness_id") @dataclass(frozen=True, slots=True) @@ -740,14 +843,126 @@ class ExpiredIdempotencyTombstone(_ContractValue): class ClosedEpochAudit(_ContractValue): snapshot: ContextWindowSnapshot terminal_occurrence_records: tuple[AdmissionOccurrenceRecord, ...] + terminal_batch_records: tuple[AdmissionBatchRecord, ...] terminal_reservations: tuple[AdmissionReservation, ...] + terminal_generation_reservations: tuple[GenerationReservationRecord, ...] closure_witness_id: AdmissionWitnessId fence_proof: EpochFenceProof | None processed_event_tombstones: tuple[AdmissionEventId, ...] retained_unresolved_count: int + retained_generation_count: int def __post_init__(self) -> None: _validate_non_negative(self.retained_unresolved_count, "invalid_retained_charge") + _validate_non_negative( + self.retained_generation_count, + "invalid_retained_generation_charge", + ) + _validate_canonical_tuple( + self.terminal_occurrence_records, + "noncanonical_terminal_occurrences", + key=lambda record: record.occurrence.occurrence_id.value, + ) + _validate_canonical_tuple( + self.terminal_batch_records, + "noncanonical_terminal_batches", + key=lambda record: record.batch.batch_id.value, + ) + _validate_canonical_tuple( + self.terminal_reservations, + "noncanonical_terminal_reservations", + key=lambda reservation: reservation.reservation_id.value, + ) + _validate_canonical_tuple( + self.terminal_generation_reservations, + "noncanonical_terminal_generation_reservations", + key=lambda record: record.generation_reservation_id.value, + ) + _validate_canonical_tuple( + self.processed_event_tombstones, + "noncanonical_processed_event_tombstones", + key=lambda event_id: event_id.value, + ) + occurrence_by_id = { + record.occurrence.occurrence_id: record for record in self.terminal_occurrence_records + } + batch_by_id = {record.batch.batch_id: record for record in self.terminal_batch_records} + reservation_by_id = { + reservation.reservation_id: reservation for reservation in self.terminal_reservations + } + if ( + len(occurrence_by_id) != len(self.terminal_occurrence_records) + or len(batch_by_id) != len(self.terminal_batch_records) + or len(reservation_by_id) != len(self.terminal_reservations) + or len( + { + record.generation_reservation_id + for record in self.terminal_generation_reservations + } + ) + != len(self.terminal_generation_reservations) + or len(set(self.processed_event_tombstones)) != len(self.processed_event_tombstones) + ): + _raise_invalid("duplicate_closed_epoch_owner") + for batch_record in self.terminal_batch_records: + members = tuple( + occurrence_by_id.get(occurrence_id) + for occurrence_id in batch_record.batch.occurrence_ids + ) + if any(member is None for member in members): + _raise_invalid("missing_closed_epoch_occurrence") + if any( + member is not None + and ( + member.batch_id != batch_record.batch.batch_id + or member.reservation_id != batch_record.reservation_id + or member.state is not batch_record.state + ) + for member in members + ): + _raise_invalid("inconsistent_closed_epoch_link") + if batch_record.reservation_id is not None: + reservation = reservation_by_id.get(batch_record.reservation_id) + if reservation is None or reservation.key.batch_id != batch_record.batch.batch_id: + _raise_invalid("missing_closed_epoch_reservation") + for generation in self.terminal_generation_reservations: + generation_batch = batch_by_id.get(generation.batch_id) + if ( + generation_batch is None + or generation.request_id != generation_batch.batch.request_id + or generation.representation_revision + != generation_batch.batch.manifest.representation_revision + or generation.occurrence_ids != generation_batch.batch.occurrence_ids + ): + _raise_invalid("inconsistent_closed_epoch_generation") + retained_input = 0 + for record in self.terminal_batch_records: + if record.state not in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + }: + continue + charge = record.unresolved_input_count + if charge == 0 and record.reservation_id is not None: + reservation = reservation_by_id.get(record.reservation_id) + if reservation is not None: + charge = reservation.reserved_count + retained_input += charge + retained_generation = sum( + record.maximum_allowance + for record in self.terminal_generation_reservations + if record.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + if ( + retained_input != self.retained_unresolved_count + or retained_generation != self.retained_generation_count + ): + _raise_invalid("closed_epoch_retained_charge_mismatch") @dataclass(frozen=True, slots=True) @@ -769,13 +984,18 @@ def __post_init__(self) -> None: (self.backend, "invalid_evidence_backend", 64), (self.configuration_mode, "invalid_configuration_mode", 64), (self.verifier, "invalid_evidence_verifier", 64), - (self.source_locator, "invalid_source_locator", 256), (self.tested_version, "invalid_tested_version", 64), (self.tested_revision, "invalid_tested_revision", 96), (self.checked_at, "invalid_checked_at", 32), (self.freshness_policy, "invalid_freshness_policy", 128), ): _validate_bounded_text(value, reason, maximum=maximum) + _validate_bounded_text( + self.source_locator, + "invalid_source_locator", + maximum=256, + locator=True, + ) @dataclass(frozen=True, slots=True) @@ -794,7 +1014,15 @@ def __post_init__(self) -> None: maximum=96, ) _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) - _validate_tuple(self.evidence, "invalid_coverage_evidence") + _validate_canonical_tuple( + self.evidence, + "noncanonical_coverage_evidence", + key=lambda evidence: ( + evidence.kind.value, + evidence.source_locator, + evidence.claim_id, + ), + ) if not self.evidence: _raise_invalid("coverage_evidence_required") primary = tuple( @@ -823,6 +1051,18 @@ class OpenEpochEvent(_AdmissionEventBase): snapshot: ContextWindowSnapshot protected_pools: tuple[ProtectedPoolSpec, ...] + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + _validate_canonical_tuple( + self.protected_pools, + "noncanonical_protected_pools", + key=lambda pool: ( + pool.priority, + pool.reserve_class.value, + pool.capability_owner_id.value, + ), + ) + @dataclass(frozen=True, slots=True) class AuthorityUnavailableEvent(_AdmissionEventBase): @@ -851,16 +1091,39 @@ class ReserveRequestEvent(_AdmissionEventBase): def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) _validate_non_negative(self.snapshot_sequence, "invalid_snapshot_sequence") - _validate_tuple(self.input_reservations, "invalid_input_reservations") + _validate_canonical_tuple( + self.input_reservations, + "noncanonical_input_reservations", + key=lambda reservation: reservation.reservation_id.value, + ) + if not self.input_reservations: + _raise_invalid("input_reservation_required") reservation_ids = tuple( reservation.reservation_id for reservation in self.input_reservations ) if len(reservation_ids) != len(set(reservation_ids)): _raise_invalid("duplicate_reservation_id") - if self.batch.occurrence_ids != self.input_reservations[0].occurrence_ids: - _raise_invalid("reservation_occurrence_mismatch") - if self.idempotency_namespace != self.input_reservations[0].key.idempotency_namespace: - _raise_invalid("reservation_namespace_mismatch") + for reservation in self.input_reservations: + if reservation.occurrence_ids != self.batch.occurrence_ids: + _raise_invalid("reservation_occurrence_mismatch") + if self.idempotency_namespace != reservation.key.idempotency_namespace: + _raise_invalid("reservation_namespace_mismatch") + if ( + reservation.key.batch_id != self.batch.batch_id + or reservation.reserve_class is not self.batch.reserve_class + or reservation.protected_pool_owner_id != self.batch.protected_pool_owner_id + ): + _raise_invalid("reservation_batch_policy_mismatch") + generation = self.generation_reservation + if generation is not None and ( + generation.request_id != self.batch.request_id + or generation.batch_id != self.batch.batch_id + or generation.representation_revision != self.batch.manifest.representation_revision + or generation.occurrence_ids != self.batch.occurrence_ids + or generation.reserve_class is not self.batch.reserve_class + or generation.protected_pool_owner_id != self.batch.protected_pool_owner_id + ): + _raise_invalid("generation_batch_policy_mismatch") @dataclass(frozen=True, slots=True) @@ -898,6 +1161,7 @@ class AcceptInputEvent(_AdmissionEventBase): batch_id: AdmissionBatchId witness: AdmissionWitness final_manifest_revision: RepresentationRevision + final_manifest: CanonicalRepresentationManifest exact_input_charge: int measurement_kind: MeasurementKind authority_source_id: AuthoritySourceId @@ -906,10 +1170,7 @@ class AcceptInputEvent(_AdmissionEventBase): def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) _validate_non_negative(self.exact_input_charge, "invalid_exact_input_charge") - if self.measurement_kind in { - MeasurementKind.HOST_ESTIMATE, - MeasurementKind.BYTE_EMERGENCY, - }: + if self.measurement_kind is not MeasurementKind.PROVIDER_EXACT: _raise_invalid("non_authoritative_measurement") @@ -917,6 +1178,15 @@ def __post_init__(self) -> None: class ReleaseNonAdmissionEvent(_AdmissionEventBase): batch_id: AdmissionBatchId witness: AdmissionWitness + history_removal_witness: AdmissionWitness | None + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + if ( + self.history_removal_witness is not None + and self.history_removal_witness.kind is not WitnessKind.ROLLBACK + ): + _raise_invalid("invalid_history_removal_witness") @dataclass(frozen=True, slots=True) @@ -939,17 +1209,17 @@ def __post_init__(self) -> None: class ResolveIndeterminateAcceptedEvent(_AdmissionEventBase): batch_id: AdmissionBatchId witness: AdmissionWitness + final_manifest_revision: RepresentationRevision + final_manifest: CanonicalRepresentationManifest exact_charge: int measurement_kind: MeasurementKind authority_source_id: AuthoritySourceId + representation_binding_witness: RepresentationBindingWitness def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) _validate_non_negative(self.exact_charge, "invalid_exact_charge") - if self.measurement_kind in { - MeasurementKind.HOST_ESTIMATE, - MeasurementKind.BYTE_EMERGENCY, - }: + if self.measurement_kind is not MeasurementKind.PROVIDER_EXACT: _raise_invalid("non_authoritative_measurement") @@ -957,6 +1227,15 @@ def __post_init__(self) -> None: class ResolveIndeterminateNonAdmissionEvent(_AdmissionEventBase): batch_id: AdmissionBatchId witness: AdmissionWitness + history_removal_witness: AdmissionWitness | None + + def __post_init__(self) -> None: + _AdmissionEventBase.__post_init__(self) + if ( + self.history_removal_witness is not None + and self.history_removal_witness.kind is not WitnessKind.ROLLBACK + ): + _raise_invalid("invalid_history_removal_witness") @dataclass(frozen=True, slots=True) @@ -1011,13 +1290,26 @@ class ExpireIdempotencyKeyEvent(_AdmissionEventBase): @dataclass(frozen=True, slots=True) class RolloverEpochEvent(_AdmissionEventBase): witness: AdmissionWitness - fence_proof: EpochFenceProof + fence_proof: EpochFenceProof | None + deducted_unresolved_count: int new_snapshot: ContextWindowSnapshot protected_pools: tuple[ProtectedPoolSpec, ...] def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) - _validate_tuple(self.protected_pools, "invalid_protected_pools") + _validate_non_negative( + self.deducted_unresolved_count, + "invalid_deducted_unresolved_count", + ) + _validate_canonical_tuple( + self.protected_pools, + "noncanonical_protected_pools", + key=lambda pool: ( + pool.priority, + pool.reserve_class.value, + pool.capability_owner_id.value, + ), + ) ContextAdmissionEvent: TypeAlias = ( @@ -1053,13 +1345,53 @@ class _AdmissionEffectBase(_ContractValue): AdmissionOccurrenceId | AdmissionBatchId | AdmissionReservationId + | AdmissionEventId | GenerationReservationId | WindowEpochId ) + def __post_init__(self) -> None: + if not isinstance(self.source_event_id, AdmissionEventId): + _raise_invalid("invalid_effect_source_event") + if not isinstance(self.resulting_aggregate_revision, AggregateRevision): + _raise_invalid("invalid_effect_aggregate_revision") + if not isinstance(self.resulting_admission_sequence, AdmissionSequence): + _raise_invalid("invalid_effect_admission_sequence") + + +def _validate_charge_effect( + effect: _AdmissionEffectBase, + *, + target_type: type[_OpaqueString], + charge_domain: ChargeDomain, +) -> None: + _AdmissionEffectBase.__post_init__(effect) + if not isinstance(effect.target_id, target_type): + _raise_invalid("invalid_effect_target") + if getattr(effect, "charge_domain") is not charge_domain: + _raise_invalid("invalid_effect_charge_domain") + reserve_class = getattr(effect, "reserve_class") + owner = getattr(effect, "protected_pool_owner_id") + if (reserve_class is ReserveClass.ORDINARY) != (owner is None): + _raise_invalid("invalid_effect_protected_pool_owner") + _validate_non_negative(getattr(effect, "count"), "invalid_effect_count") + _validate_non_negative( + getattr(effect, "snapshot_sequence"), + "invalid_effect_snapshot_sequence", + ) + witness_ids = getattr(effect, "witness_ids") + _validate_canonical_tuple( + witness_ids, + "noncanonical_effect_witness_ids", + key=lambda witness_id: witness_id.value, + ) + if len(witness_ids) != len(set(witness_ids)): + _raise_invalid("duplicate_effect_witness_id") + @dataclass(frozen=True, slots=True) class ReservationRecordedEffect(_AdmissionEffectBase): + target_id: AdmissionReservationId charge_domain: ChargeDomain reserve_class: ReserveClass protected_pool_owner_id: ProtectedPoolOwnerId | None @@ -1068,9 +1400,17 @@ class ReservationRecordedEffect(_AdmissionEffectBase): snapshot_sequence: int witness_ids: tuple[AdmissionWitnessId, ...] + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=AdmissionReservationId, + charge_domain=ChargeDomain.INPUT_CONTEXT, + ) + @dataclass(frozen=True, slots=True) class ReservationReleasedEffect(_AdmissionEffectBase): + target_id: AdmissionReservationId charge_domain: ChargeDomain reserve_class: ReserveClass protected_pool_owner_id: ProtectedPoolOwnerId | None @@ -1079,15 +1419,29 @@ class ReservationReleasedEffect(_AdmissionEffectBase): snapshot_sequence: int witness_ids: tuple[AdmissionWitnessId, ...] + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=AdmissionReservationId, + charge_domain=ChargeDomain.INPUT_CONTEXT, + ) + @dataclass(frozen=True, slots=True) class OccurrenceStateChangedEffect(_AdmissionEffectBase): + target_id: AdmissionOccurrenceId previous_state: AdmissionState next_state: AdmissionState + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionOccurrenceId): + _raise_invalid("invalid_effect_target") + @dataclass(frozen=True, slots=True) class ChargeCommittedEffect(_AdmissionEffectBase): + target_id: AdmissionBatchId charge_domain: ChargeDomain reserve_class: ReserveClass protected_pool_owner_id: ProtectedPoolOwnerId | None @@ -1096,9 +1450,17 @@ class ChargeCommittedEffect(_AdmissionEffectBase): snapshot_sequence: int witness_ids: tuple[AdmissionWitnessId, ...] + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=AdmissionBatchId, + charge_domain=ChargeDomain.INPUT_CONTEXT, + ) + @dataclass(frozen=True, slots=True) class GenerationReservationRecordedEffect(_AdmissionEffectBase): + target_id: GenerationReservationId charge_domain: ChargeDomain reserve_class: ReserveClass protected_pool_owner_id: ProtectedPoolOwnerId | None @@ -1107,9 +1469,17 @@ class GenerationReservationRecordedEffect(_AdmissionEffectBase): snapshot_sequence: int witness_ids: tuple[AdmissionWitnessId, ...] + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=GenerationReservationId, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + ) + @dataclass(frozen=True, slots=True) class GenerationReconciledEffect(_AdmissionEffectBase): + target_id: GenerationReservationId charge_domain: ChargeDomain reserve_class: ReserveClass protected_pool_owner_id: ProtectedPoolOwnerId | None @@ -1118,30 +1488,65 @@ class GenerationReconciledEffect(_AdmissionEffectBase): snapshot_sequence: int witness_ids: tuple[AdmissionWitnessId, ...] + def __post_init__(self) -> None: + _validate_charge_effect( + self, + target_type=GenerationReservationId, + charge_domain=ChargeDomain.OUTPUT_GENERATION, + ) + @dataclass(frozen=True, slots=True) class ReconciliationQueryRequestedEffect(_AdmissionEffectBase): + target_id: AdmissionBatchId | GenerationReservationId reason_code: str + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionBatchId | GenerationReservationId): + _raise_invalid("invalid_effect_target") + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + @dataclass(frozen=True, slots=True) class ReconciliationEscalationEffect(_AdmissionEffectBase): + target_id: AdmissionBatchId | GenerationReservationId reason_code: str + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionBatchId | GenerationReservationId): + _raise_invalid("invalid_effect_target") + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + @dataclass(frozen=True, slots=True) class ConflictRejectedEffect(_AdmissionEffectBase): + target_id: AdmissionEventId reason_code: str + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionEventId): + _raise_invalid("invalid_effect_target") + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + @dataclass(frozen=True, slots=True) class IdempotencyExpiredEffect(_AdmissionEffectBase): + target_id: AdmissionReservationId reservation_key: AdmissionReservationKey expiry_witness_id: AdmissionWitnessId + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionReservationId): + _raise_invalid("invalid_effect_target") + @dataclass(frozen=True, slots=True) class ReservationInvalidatedEffect(_AdmissionEffectBase): + target_id: AdmissionReservationId | GenerationReservationId charge_domain: ChargeDomain reserve_class: ReserveClass protected_pool_owner_id: ProtectedPoolOwnerId | None @@ -1150,22 +1555,62 @@ class ReservationInvalidatedEffect(_AdmissionEffectBase): snapshot_sequence: int witness_ids: tuple[AdmissionWitnessId, ...] + def __post_init__(self) -> None: + if isinstance(self.target_id, AdmissionReservationId): + expected_domain = ChargeDomain.INPUT_CONTEXT + elif isinstance(self.target_id, GenerationReservationId): + expected_domain = ChargeDomain.OUTPUT_GENERATION + else: + _raise_invalid("invalid_effect_target") + _validate_charge_effect( + self, target_type=type(self.target_id), charge_domain=expected_domain + ) + @dataclass(frozen=True, slots=True) class EpochClosedEffect(_AdmissionEffectBase): - fence_proof: EpochFenceProof + target_id: WindowEpochId + fence_proof: EpochFenceProof | None + deducted_unresolved_count: int + + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, WindowEpochId): + _raise_invalid("invalid_effect_target") + if self.fence_proof is not None and self.target_id != self.fence_proof.old_window_epoch_id: + _raise_invalid("invalid_effect_target") + _validate_non_negative( + self.deducted_unresolved_count, + "invalid_deducted_unresolved_count", + ) @dataclass(frozen=True, slots=True) class QuarantineRecordedEffect(_AdmissionEffectBase): + target_id: AdmissionBatchId | GenerationReservationId reason_code: str + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, AdmissionBatchId | GenerationReservationId): + _raise_invalid("invalid_effect_target") + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + @dataclass(frozen=True, slots=True) class AuthorityUnavailableEffect(_AdmissionEffectBase): + target_id: WindowEpochId reason_code: str authority_state: CoverageState + def __post_init__(self) -> None: + _AdmissionEffectBase.__post_init__(self) + if not isinstance(self.target_id, WindowEpochId): + _raise_invalid("invalid_effect_target") + _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + if self.authority_state is CoverageState.VERIFIED: + _raise_invalid("invalid_unavailable_authority_state") + AdmissionEffect: TypeAlias = ( ReservationRecordedEffect @@ -1204,6 +1649,32 @@ class IdempotencyRecord(_ContractValue): publication_revision: AggregateRevision +def _validate_state_metadata_uniqueness( + processed_events: tuple[ProcessedEventRecord, ...], + idempotency_records: tuple[IdempotencyRecord, ...], + expired_tombstones: tuple[ExpiredIdempotencyTombstone, ...], + closed_epochs: tuple[ClosedEpochAudit, ...], +) -> None: + if len({record.event_id for record in processed_events}) != len(processed_events): + _raise_invalid("duplicate_processed_event") + idempotency_keys = tuple( + (record.namespace, record.reservation_key) for record in idempotency_records + ) + if len(set(idempotency_keys)) != len(idempotency_keys): + _raise_invalid("duplicate_idempotency_owner") + tombstone_keys = tuple( + (record.namespace, record.reservation_key) for record in expired_tombstones + ) + if len(set(tombstone_keys)) != len(tombstone_keys): + _raise_invalid("duplicate_idempotency_tombstone") + epoch_keys = tuple( + (audit.snapshot.window_epoch_id, audit.snapshot.window_epoch_number) + for audit in closed_epochs + ) + if len(set(epoch_keys)) != len(epoch_keys): + _raise_invalid("duplicate_closed_epoch") + + @dataclass(frozen=True, slots=True) class UninitializedContextAdmissionState(_ContractValue): protocol_version: int @@ -1216,6 +1687,38 @@ class UninitializedContextAdmissionState(_ContractValue): def __post_init__(self) -> None: _validate_protocol_version(self.protocol_version) + _validate_canonical_tuple( + self.processed_events, + "noncanonical_processed_events", + key=lambda record: (record.aggregate_revision.value, record.event_id.value), + ) + _validate_canonical_tuple( + self.idempotency_records, + "noncanonical_idempotency_records", + key=lambda record: ( + record.publication_revision.value, + record.owning_event_id.value, + ), + ) + _validate_canonical_tuple( + self.expired_idempotency_tombstones, + "noncanonical_idempotency_tombstones", + key=lambda tombstone: ( + tombstone.reservation_key.window_epoch_number, + tombstone.reservation_key.batch_id.value, + ), + ) + _validate_canonical_tuple( + self.closed_epochs, + "noncanonical_closed_epochs", + key=lambda audit: audit.snapshot.window_epoch_number, + ) + _validate_state_metadata_uniqueness( + self.processed_events, + self.idempotency_records, + self.expired_idempotency_tombstones, + self.closed_epochs, + ) @dataclass(frozen=True, slots=True) @@ -1238,6 +1741,67 @@ def __post_init__(self) -> None: _validate_protocol_version(self.protocol_version) if self.snapshot.protocol_version != self.protocol_version: _raise_invalid("state_snapshot_protocol_mismatch") + _validate_canonical_tuple( + self.protected_pools, + "noncanonical_protected_pools", + key=lambda pool: ( + pool.priority, + pool.reserve_class.value, + pool.capability_owner_id.value, + ), + ) + _validate_canonical_tuple( + self.occurrence_records, + "noncanonical_occurrence_records", + key=lambda record: record.occurrence.occurrence_id.value, + ) + _validate_canonical_tuple( + self.batch_records, + "noncanonical_batch_records", + key=lambda record: record.batch.batch_id.value, + ) + _validate_canonical_tuple( + self.reservations, + "noncanonical_reservations", + key=lambda reservation: reservation.reservation_id.value, + ) + _validate_canonical_tuple( + self.generation_reservations, + "noncanonical_generation_reservations", + key=lambda record: record.generation_reservation_id.value, + ) + _validate_canonical_tuple( + self.processed_events, + "noncanonical_processed_events", + key=lambda record: (record.aggregate_revision.value, record.event_id.value), + ) + _validate_canonical_tuple( + self.idempotency_records, + "noncanonical_idempotency_records", + key=lambda record: ( + record.publication_revision.value, + record.owning_event_id.value, + ), + ) + _validate_canonical_tuple( + self.expired_idempotency_tombstones, + "noncanonical_idempotency_tombstones", + key=lambda tombstone: ( + tombstone.reservation_key.window_epoch_number, + tombstone.reservation_key.batch_id.value, + ), + ) + _validate_canonical_tuple( + self.closed_epochs, + "noncanonical_closed_epochs", + key=lambda audit: audit.snapshot.window_epoch_number, + ) + _validate_state_metadata_uniqueness( + self.processed_events, + self.idempotency_records, + self.expired_idempotency_tombstones, + self.closed_epochs, + ) pools = tuple( (pool.reserve_class, pool.capability_owner_id) for pool in self.protected_pools ) @@ -1247,10 +1811,6 @@ def __post_init__(self) -> None: self.snapshot.remaining_count ): _raise_invalid("protected_pool_capacity_exceeded") - _validate_tuple(self.batch_records, "invalid_batch_records") - _validate_tuple(self.reservations, "invalid_reservations") - _validate_tuple(self.generation_reservations, "invalid_generation_reservations") - _validate_tuple(self.occurrence_records, "invalid_occurrence_records") batch_ids = tuple(record.batch.batch_id for record in self.batch_records) if len(batch_ids) != len(set(batch_ids)): _raise_invalid("duplicate_batch_owner") @@ -1267,40 +1827,163 @@ def __post_init__(self) -> None: ) if len(occurrence_ids) != len(set(occurrence_ids)): _raise_invalid("duplicate_occurrence_owner") - seen_pool_owners: set[tuple[ReserveClass, ProtectedPoolOwnerId]] = set() - seen_batch_owners: set[tuple[ReserveClass, ProtectedPoolOwnerId]] = set() - seen_generation_owners: set[tuple[ReserveClass, ProtectedPoolOwnerId]] = set() + pools_by_key = { + (pool.reserve_class, pool.capability_owner_id): pool for pool in self.protected_pools + } + reservations_by_id = { + reservation.reservation_id: reservation for reservation in self.reservations + } + batch_records_by_id = {record.batch.batch_id: record for record in self.batch_records} + occurrence_records_by_id = { + record.occurrence.occurrence_id: record for record in self.occurrence_records + } + for occurrence_record in self.occurrence_records: + if occurrence_record.batch_id is None: + if ( + occurrence_record.reservation_id is not None + or occurrence_record.state is not AdmissionState.PROPOSED + ): + _raise_invalid("orphan_occurrence_link") + continue + linked_batch = batch_records_by_id.get(occurrence_record.batch_id) + if ( + linked_batch is None + or occurrence_record.occurrence.occurrence_id + not in linked_batch.batch.occurrence_ids + or occurrence_record.reservation_id != linked_batch.reservation_id + or occurrence_record.state is not linked_batch.state + ): + _raise_invalid("inconsistent_occurrence_link") + for batch_record in self.batch_records: + member_records = tuple( + occurrence_records_by_id.get(occurrence_id) + for occurrence_id in batch_record.batch.occurrence_ids + ) + if any(record is None for record in member_records): + _raise_invalid("missing_batch_occurrence") + concrete_members = tuple(record for record in member_records if record is not None) + if any( + record.batch_id != batch_record.batch.batch_id + or record.reservation_id != batch_record.reservation_id + or record.state is not batch_record.state + for record in concrete_members + ): + _raise_invalid("inconsistent_batch_occurrence_link") + owned_pairs = tuple( + (span_id, record.occurrence.occurrence_id) + for record in concrete_members + for span_id in record.occurrence.owned_span_ids + ) + owned_span_ids = tuple(span_id for span_id, _ in owned_pairs) + manifest_pairs = tuple( + (owner.span_id, owner.occurrence_id) + for owner in batch_record.batch.manifest.span_owners + ) + if ( + len(owned_span_ids) != len(set(owned_span_ids)) + or set(owned_pairs) != set(manifest_pairs) + or len(owned_pairs) != len(manifest_pairs) + ): + _raise_invalid("inconsistent_span_ownership") + for reservation in self.reservations: + matching_batch = batch_records_by_id.get(reservation.key.batch_id) + if matching_batch is None: + _raise_invalid("orphan_reservation") + if ( + matching_batch.reservation_id != reservation.reservation_id + or reservation.occurrence_ids != matching_batch.batch.occurrence_ids + or reservation.reserve_class is not matching_batch.batch.reserve_class + or reservation.protected_pool_owner_id + != matching_batch.batch.protected_pool_owner_id + ): + _raise_invalid("reservation_batch_policy_mismatch") + owner = reservation.protected_pool_owner_id + if ( + owner is not None + and ( + reservation.reserve_class, + owner, + ) + not in pools_by_key + ): + _raise_invalid("orphan_protected_charge_owner") + protected_charges: dict[tuple[ReserveClass, ProtectedPoolOwnerId], int] = {} for record in self.batch_records: owner = record.batch.protected_pool_owner_id + matched_reservation = ( + reservations_by_id.get(record.reservation_id) + if record.reservation_id is not None + else None + ) + if matched_reservation is not None and ( + matched_reservation.key.batch_id != record.batch.batch_id + or matched_reservation.reserve_class is not record.batch.reserve_class + or matched_reservation.protected_pool_owner_id != owner + ): + _raise_invalid("reservation_batch_policy_mismatch") if record.batch.reserve_class is not ReserveClass.ORDINARY: if owner is None: _raise_invalid("missing_protected_pool_owner") key = (record.batch.reserve_class, owner) - if key in seen_batch_owners: - _raise_invalid("duplicate_protected_charge_owner") - seen_batch_owners.add(key) - seen_pool_owners.add(key) - if not any( - pool.reserve_class is record.batch.reserve_class - and pool.capability_owner_id == owner - for pool in self.protected_pools - ): + if key not in pools_by_key: _raise_invalid("orphan_protected_charge_owner") + if record.state is AdmissionState.INDETERMINATE: + charge = record.unresolved_input_count + if charge == 0 and matched_reservation is not None: + charge = matched_reservation.reserved_count + elif record.state in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + charge = ( + matched_reservation.reserved_count + if matched_reservation is not None + else 0 + ) + else: + # Committed/quarantined facts may exceed their reservation after + # an authoritative acceptance. They remain charged by _capacity, + # but are no longer an outstanding allocation against the pool. + charge = 0 + protected_charges[key] = protected_charges.get(key, 0) + charge for generation_record in self.generation_reservations: + matching_batch = batch_records_by_id.get(generation_record.batch_id) + if ( + matching_batch is None + or generation_record.request_id != matching_batch.batch.request_id + or generation_record.representation_revision + != matching_batch.batch.manifest.representation_revision + or generation_record.occurrence_ids != matching_batch.batch.occurrence_ids + or generation_record.reserve_class is not matching_batch.batch.reserve_class + or generation_record.protected_pool_owner_id + != matching_batch.batch.protected_pool_owner_id + or generation_record.window_epoch_id != self.snapshot.window_epoch_id + or generation_record.window_epoch_number != self.snapshot.window_epoch_number + or generation_record.snapshot_sequence != self.snapshot.snapshot_sequence + ): + _raise_invalid("inconsistent_generation_link") owner = generation_record.protected_pool_owner_id if generation_record.reserve_class is not ReserveClass.ORDINARY: if owner is None: _raise_invalid("missing_protected_pool_owner") key = (generation_record.reserve_class, owner) - if key in seen_generation_owners: - _raise_invalid("duplicate_protected_charge_owner") - seen_generation_owners.add(key) - if not any( - pool.reserve_class is generation_record.reserve_class - and pool.capability_owner_id == owner - for pool in self.protected_pools - ): + if key not in pools_by_key: _raise_invalid("orphan_protected_charge_owner") + if generation_record.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + }: + protected_charges[key] = ( + protected_charges.get(key, 0) + generation_record.maximum_allowance + ) + if any( + charged > pools_by_key[key].injected_count + for key, charged in protected_charges.items() + ): + _raise_invalid("protected_pool_overallocated") ContextAdmissionState: TypeAlias = UninitializedContextAdmissionState | ActiveContextAdmissionState diff --git a/src/autoskillit/core/types/_type_enums.py b/src/autoskillit/core/types/_type_enums.py index fdd9cb248..b94124d4a 100644 --- a/src/autoskillit/core/types/_type_enums.py +++ b/src/autoskillit/core/types/_type_enums.py @@ -678,6 +678,7 @@ class GenerationState(StrEnum): RECONCILED = "reconciled" INDETERMINATE = "indeterminate" QUARANTINED = "quarantined" + INVALIDATED = "invalidated" @unique diff --git a/tests/arch/test_subpackage_isolation.py b/tests/arch/test_subpackage_isolation.py index bbb083ee7..77697705e 100644 --- a/tests/arch/test_subpackage_isolation.py +++ b/tests/arch/test_subpackage_isolation.py @@ -1151,7 +1151,7 @@ def test_data_directories_are_not_python_packages() -> None: "isolated in dedicated workspace modules.", ), "core/context_admission.py": ( - 2000, + 2950, "REQ-CNST-010-E13: #4333 freezes one exhaustive protocol-v1 reducer and replay " "surface. Keeping all closed event transitions together makes atomic batch, " "idempotency, protected-pool, reconciliation, rollover, and declarative effect " @@ -1159,7 +1159,7 @@ def test_data_directories_are_not_python_packages() -> None: "fragment exhaustiveness.", ), "core/types/_type_context_admission.py": ( - 1750, + 2300, "REQ-CNST-010-E14: #4333 freezes the complete content-free protocol-v1 schema in " "one IL-0 shard. Co-locating identities, records, closed event/effect unions, " "states, canonical serialization, and the static coverage registry prevents " diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 7df5c1003..fc6756ce8 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -311,6 +311,9 @@ def _generation_reservation( return GenerationReservationRecord( generation_reservation_id=GenerationReservationId(f"generation-{batch.batch_id.value}"), request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + occurrence_ids=batch.occurrence_ids, response_id=ModelItemId(f"response-{batch.batch_id.value}"), window_epoch_id=WindowEpochId(f"epoch-{epoch}"), window_epoch_number=epoch, @@ -321,6 +324,7 @@ def _generation_reservation( state=GenerationState.RESERVED, exact_terminal_usage=None, witness_ids=(), + authority_source_id=None, ) @@ -422,6 +426,14 @@ def _batch_record(state: ActiveContextAdmissionState, batch: AdmissionBatch) -> ) +def _assert_only_rejection_journal_changed( + before: ActiveContextAdmissionState, + after: ActiveContextAdmissionState, +) -> None: + assert replace(after, processed_events=before.processed_events) == before + assert len(after.processed_events) == len(before.processed_events) + 1 + + def _generation_record( state: ActiveContextAdmissionState, reservation_id: GenerationReservationId ) -> Any: @@ -494,7 +506,10 @@ def test_stale_concurrent_proposals_cannot_overcommit_one_snapshot() -> None: expected_revision=shared_revision, ) assert stale_transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert stale_transition.next_state == first_transition.next_state + _assert_only_rejection_journal_changed( + first_transition.next_state, + stale_transition.next_state, + ) @pytest.mark.parametrize( @@ -607,6 +622,7 @@ def test_input_commit_and_output_reconciliation_remain_distinct_domains() -> Non batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, exact_input_charge=18, measurement_kind=MeasurementKind.PROVIDER_EXACT, authority_source_id=AuthoritySourceId("authority-test"), @@ -747,6 +763,7 @@ def test_history_request_provider_and_rollback_witnesses_are_not_interchangeable **_event_fields(state, f"bad-{event_kind}", "release-non-admission"), batch_id=batch.batch_id, witness=_witness(batch, witness_kind), + history_removal_witness=None, ) else: event = RollbackAdmissionEvent( @@ -756,7 +773,7 @@ def test_history_request_provider_and_rollback_witnesses_are_not_interchangeable ) rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected.next_state == state + _assert_only_rejection_journal_changed(state, rejected.next_state) @pytest.mark.parametrize( @@ -793,13 +810,14 @@ def test_revision_and_fence_failures_do_not_commit( fence_witness_id=AdmissionWitnessId("stale-fence"), highest_admitted_dispatch_sequence=0, ), + deducted_unresolved_count=0, new_snapshot=_snapshot(epoch=2), protected_pools=(), ) rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert expected_reason_fragment in rejected.decision.reason_code - assert rejected.next_state == state + _assert_only_rejection_journal_changed(state, rejected.next_state) def test_partial_batch_and_illegal_order_are_typed_non_mutating_rejections() -> None: @@ -843,7 +861,7 @@ def test_partial_batch_and_illegal_order_are_typed_non_mutating_rejections() -> ): rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT, index - assert rejected.next_state == state + _assert_only_rejection_journal_changed(state, rejected.next_state) @pytest.mark.parametrize( @@ -879,6 +897,7 @@ def test_acceptance_rejects_mutated_or_unattested_final_representation( batch_id=batch.batch_id, witness=_witness(batch, witness_kind), final_manifest_revision=final_revision, + final_manifest=batch.manifest, exact_input_charge=20, measurement_kind=MeasurementKind.PROVIDER_EXACT, authority_source_id=AuthoritySourceId("provider-test"), @@ -886,7 +905,7 @@ def test_acceptance_rejects_mutated_or_unattested_final_representation( ) rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected.next_state == state + _assert_only_rejection_journal_changed(state, rejected.next_state) @pytest.mark.parametrize( @@ -939,6 +958,7 @@ def test_ambiguous_crash_stays_charged_until_an_authoritative_resolution( ), batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.NON_ADMISSION), + history_removal_witness=_witness(batch, WitnessKind.ROLLBACK), ) else: resolution = ResolveIndeterminateRollbackEvent( @@ -970,6 +990,7 @@ def test_provider_accepted_overage_is_recorded_and_quarantined( batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, exact_input_charge=exact_charge, measurement_kind=MeasurementKind.PROVIDER_EXACT, authority_source_id=AuthoritySourceId("provider-test"), @@ -983,7 +1004,86 @@ def test_provider_accepted_overage_is_recorded_and_quarantined( assert record.state is AdmissionState.QUARANTINED -def test_retry_resume_fork_and_parent_delivery_keep_distinct_occurrences() -> None: +def test_incomplete_manifest_quarantine_retains_charge_and_publishes_all_effects() -> None: + state = _open_epoch(remaining_count=40) + occurrence = replace( + _occurrence("incomplete-manifest", maximum=20), + owned_span_ids=( + CanonicalSpanId("span-incomplete-manifest-a"), + CanonicalSpanId("span-incomplete-manifest-b"), + ), + ) + state, _ = _propose(state, occurrence) + complete_batch = _batch("batch-incomplete-manifest", (occurrence,)) + incomplete_manifest = replace( + complete_batch.manifest, + span_owners=complete_batch.manifest.span_owners[:1], + ) + batch = complete_batch + reserved, _ = _reserve(state, batch, (occurrence,), input_count=20) + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + state = _prepare_dispatch(reserved.next_state, batch) + event = AcceptInputEvent( + **_event_fields(state, "accept-incomplete-manifest", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=incomplete_manifest, + exact_input_charge=17, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ) + quarantined = reduce_context_admission(state, event) + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + quarantined_record = _batch_record(quarantined.next_state, batch) + assert quarantined_record.committed_input_count == 17 + assert quarantined_record.state is AdmissionState.QUARANTINED + assert {type(effect).__name__ for effect in quarantined.effects} == { + "ChargeCommittedEffect", + "OccurrenceStateChangedEffect", + "QuarantineRecordedEffect", + } + + next_occurrence = _occurrence("after-incomplete-manifest", maximum=24) + next_state, _ = _propose(quarantined.next_state, next_occurrence) + next_batch = _batch("batch-after-incomplete-manifest", (next_occurrence,)) + rejected, _ = _reserve( + next_state, + next_batch, + (next_occurrence,), + input_count=24, + ) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + + +def test_authority_mismatch_quarantine_publishes_charge_state_and_quarantine() -> None: + state, batch, _, _ = _reserved_batch(name="authority-mismatch-effects") + state = _prepare_dispatch(state, batch) + event = AcceptInputEvent( + **_event_fields(state, "accept-authority-mismatch", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=20, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("different-authority"), + representation_binding_witness=_binding(batch), + ) + quarantined = reduce_context_admission(state, event) + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + assert _batch_record(quarantined.next_state, batch).committed_input_count == 20 + assert {type(effect).__name__ for effect in quarantined.effects} == { + "ChargeCommittedEffect", + "OccurrenceStateChangedEffect", + "QuarantineRecordedEffect", + } + + +def test_fork_requires_distinct_epoch_and_parent_accepts_only_delivery() -> None: state = _open_epoch(remaining_count=40) lineages = ( _lineage("resume"), @@ -1005,10 +1105,20 @@ def test_retry_resume_fork_and_parent_delivery_keep_distinct_occurrences() -> No ) for name, lineage in zip(("resume", "fork", "delivery"), lineages, strict=True) ) - for occurrence in occurrences: - state, _ = _propose(state, occurrence) + state, _ = _propose(state, occurrences[0]) + fork_event = ProposeOccurrenceEvent( + **_event_fields(state, "propose-fork", "propose-occurrence"), + occurrence=occurrences[1], + ) + rejected_fork = reduce_context_admission(state, fork_event) + assert rejected_fork.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected_fork.decision.reason_code == "fork_requires_distinct_epoch" + assert isinstance(rejected_fork.next_state, ActiveContextAdmissionState) + state = rejected_fork.next_state + state, _ = _propose(state, occurrences[2]) assert {record.occurrence.occurrence_id for record in state.occurrence_records} == { - occurrence.occurrence_id for occurrence in occurrences + occurrences[0].occurrence_id, + occurrences[2].occurrence_id, } assert occurrences[1].lineage.current_thread_id != occurrences[0].lineage.current_thread_id assert occurrences[2].lineage.delivery_occurrence_id is not None @@ -1024,7 +1134,7 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> new_window_epoch_number=2, receiver_authority_source_id=receiver_authority, fence_witness_id=AdmissionWitnessId("fence-1-to-2"), - highest_admitted_dispatch_sequence=state.admission_sequence.value, + highest_admitted_dispatch_sequence=0, ) rollover_witness = replace( _witness(batch, WitnessKind.EPOCH_ROLLOVER), @@ -1034,6 +1144,7 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> **_event_fields(state, "rollover-1-to-2", "rollover-epoch"), witness=rollover_witness, fence_proof=proof, + deducted_unresolved_count=0, new_snapshot=_snapshot(epoch=2), protected_pools=(), ) @@ -1053,6 +1164,7 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> new_window_epoch_id=WindowEpochId("epoch-3"), new_window_epoch_number=3, fence_witness_id=AdmissionWitnessId("fence-2-to-3"), + highest_admitted_dispatch_sequence=0, ) second_witness = replace( _witness(second_batch, WitnessKind.EPOCH_ROLLOVER, epoch=2), @@ -1062,6 +1174,7 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> **_event_fields(rolled.next_state, "rollover-2-to-3", "rollover-epoch"), witness=second_witness, fence_proof=second_proof, + deducted_unresolved_count=0, new_snapshot=_snapshot(epoch=3, model="claude-new", tokenizer="tokenizer-new"), protected_pools=(), ) @@ -1071,6 +1184,36 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> assert rerolled.next_state.closed_epochs[0] == rolled.next_state.closed_epochs[0] +def test_rollover_rejects_stale_admitted_dispatch_fence() -> None: + state, batch, _, _ = _reserved_batch(name="stale-dispatch-fence") + state = _prepare_dispatch(state, batch) + receiver_authority = AuthoritySourceId("receiver-authority") + proof = EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=receiver_authority, + fence_witness_id=AdmissionWitnessId("stale-dispatch-fence"), + highest_admitted_dispatch_sequence=0, + ) + event = RolloverEpochEvent( + **_event_fields(state, "rollover-stale-dispatch", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver_authority, + ), + fence_proof=proof, + deducted_unresolved_count=0, + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + rejected = reduce_context_admission(state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_only_rejection_journal_changed(state, rejected.next_state) + + def _protected_pools() -> tuple[ProtectedPoolSpec, ...]: return ( ProtectedPoolSpec( @@ -1117,6 +1260,71 @@ def test_protected_pools_are_isolated_without_double_subtracting_usage() -> None assert exact_fit.decision.kind is AdmissionDecisionKind.WOULD_ADMIT +def test_multiple_charges_aggregate_within_one_protected_pool() -> None: + state = _open_epoch(remaining_count=100, protected_pools=_protected_pools()) + for name, count in (("synthesis-a", 8), ("synthesis-b", 7)): + occurrence = _occurrence(name, maximum=count, reserve_class=ReserveClass.SYNTHESIS) + state, _ = _propose(state, occurrence) + batch = _batch( + f"batch-{name}", + (occurrence,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner="synthesis-owner", + ) + admitted, _ = _reserve(state, batch, (occurrence,), input_count=count) + assert admitted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(admitted.next_state, ActiveContextAdmissionState) + state = admitted.next_state + + over_pool = _occurrence( + "synthesis-over-pool", + maximum=6, + reserve_class=ReserveClass.SYNTHESIS, + ) + state, _ = _propose(state, over_pool) + over_pool_batch = _batch( + "batch-synthesis-over-pool", + (over_pool,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner="synthesis-owner", + ) + rejected, _ = _reserve(state, over_pool_batch, (over_pool,), input_count=6) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + + +def test_reserve_event_rejects_batch_reservation_pool_policy_mismatch() -> None: + occurrence = _occurrence( + "synthesis-policy", + maximum=5, + reserve_class=ReserveClass.SYNTHESIS, + ) + batch = _batch( + "batch-synthesis-policy", + (occurrence,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner="synthesis-owner", + ) + reservation = _reservation(batch, (occurrence,), count=5) + wrong_owner = ProtectedPoolOwnerId("different-owner") + wrong_reservation = replace( + reservation, + key=replace( + reservation.key, + protected_pool_owner_id=wrong_owner, + ), + protected_pool_owner_id=wrong_owner, + ) + state = _open_epoch(remaining_count=100, protected_pools=_protected_pools()) + with pytest.raises(ContextAdmissionValidationError): + ReserveRequestEvent( + **_event_fields(state, "reserve-policy-mismatch", "reserve-request"), + batch=batch, + snapshot_sequence=state.snapshot.snapshot_sequence, + input_reservations=(wrong_reservation,), + generation_reservation=None, + ) + + @pytest.mark.parametrize( ("authority_state", "expected"), [ @@ -1171,6 +1379,33 @@ def test_full_stream_replay_uses_each_next_state_as_the_only_next_input() -> Non assert restored == replay +def test_rejected_event_replay_is_idempotent_and_changed_reuse_conflicts() -> None: + state = _open_epoch() + rejected_event = PrepareBatchEvent( + **_event_fields(state, "rejected-event", "prepare-batch"), + batch_id=AdmissionBatchId("unknown-batch"), + representation_revision=RepresentationRevision("revision-1"), + proposed_charge=1, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("authority-test"), + ) + rejected = reduce_context_admission(state, rejected_event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + + replayed = reduce_context_admission(rejected.next_state, rejected_event) + assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert replayed.next_state == rejected.next_state + + changed = replace( + rejected_event, + batch_id=AdmissionBatchId("different-unknown-batch"), + ) + conflicted = reduce_context_admission(rejected.next_state, changed) + assert conflicted.decision.kind is AdmissionDecisionKind.CONFLICT + assert conflicted.next_state == rejected.next_state + + def test_idempotency_expiry_is_explicit_and_does_not_release_capacity() -> None: state, batch, occurrences, _ = _reserved_batch(name="expiry") reservation = _reservation(batch, occurrences, count=25) @@ -1181,7 +1416,7 @@ def test_idempotency_expiry_is_explicit_and_does_not_release_capacity() -> None: ) rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected.next_state == state + _assert_only_rejection_journal_changed(state, rejected.next_state) def test_generation_indeterminate_remains_reserved_across_reconciliation_deadline() -> None: @@ -1237,9 +1472,12 @@ def test_resolve_indeterminate_acceptance_reconciles_exact_charge() -> None: ), batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, exact_charge=19, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("provider-test"), + authority_source_id=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), ) resolved = reduce_context_admission(marked.next_state, resolution) assert isinstance(resolved.next_state, ActiveContextAdmissionState) @@ -1247,3 +1485,275 @@ def test_resolve_indeterminate_acceptance_reconciles_exact_charge() -> None: assert {record.state for record in _records_for(resolved.next_state, batch)} == { AdmissionState.COMMITTED } + + +def test_plain_rollback_cannot_resolve_indeterminate_input() -> None: + state, batch, _, _ = _reserved_batch(name="plain-rollback-indeterminate") + state = _prepare_dispatch(state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-plain-rollback", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + event = RollbackAdmissionEvent( + **_event_fields(marked.next_state, "plain-rollback", "rollback-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.ROLLBACK), + ) + rejected = reduce_context_admission(marked.next_state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_only_rejection_journal_changed(marked.next_state, rejected.next_state) + assert _batch_record(rejected.next_state, batch).state is AdmissionState.INDETERMINATE + + +def test_indeterminate_acceptance_requires_matching_authority_source() -> None: + state, batch, _, _ = _reserved_batch(name="indeterminate-authority") + state = _prepare_dispatch(state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-indeterminate-authority", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + event = ResolveIndeterminateAcceptedEvent( + **_event_fields( + marked.next_state, + "resolve-indeterminate-authority", + "resolve-indeterminate-accepted", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_charge=19, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("different-authority"), + representation_binding_witness=_binding(batch), + ) + rejected = reduce_context_admission(marked.next_state, event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + assert _batch_record(rejected.next_state, batch).state is AdmissionState.INDETERMINATE + + +def test_staged_release_requires_distinct_history_removal_witness() -> None: + state, batch, _, _ = _reserved_batch(name="history-removal") + record = _batch_record(state, batch) + assert record.reservation_id is not None + prepared = reduce_context_admission( + state, + PrepareBatchEvent( + **_event_fields(state, "prepare-history-removal", "prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + proposed_charge=_input_reservation( + state, + record.reservation_id, + ).reserved_count, + measurement_kind=MeasurementKind.TOKENIZER_EXACT, + authority_source_id=AuthoritySourceId("authority-test"), + ), + ) + staged = reduce_context_admission( + prepared.next_state, + StageHistoryEvent( + **_event_fields( + prepared.next_state, + "stage-history-removal", + "stage-history", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.HISTORY_STAGED), + ), + ) + without_removal = ReleaseNonAdmissionEvent( + **_event_fields( + staged.next_state, + "release-without-removal", + "release-non-admission", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION), + history_removal_witness=None, + ) + rejected = reduce_context_admission(staged.next_state, without_removal) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + with_removal = replace( + without_removal, + event_id=AdmissionEventId("release-with-removal"), + expected_aggregate_revision=rejected.next_state.aggregate_revision, + history_removal_witness=_witness(batch, WitnessKind.ROLLBACK), + ) + released = reduce_context_admission(rejected.next_state, with_removal) + assert released.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(released.next_state, ActiveContextAdmissionState) + assert _batch_record(released.next_state, batch).state is AdmissionState.RELEASED + + +def test_output_reconciliation_is_bound_to_exact_generation_request() -> None: + state, batch, _, generation = _reserved_batch( + name="bound-generation", + generation_count=8, + ) + state = _prepare_dispatch(state, batch) + started = reduce_context_admission( + state, + StartGenerationEvent( + **_event_fields(state, "start-bound-generation", "start-generation"), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ), + ) + other_occurrence = _occurrence("other-output-request", maximum=1) + other_batch = _batch("batch-other-output-request", (other_occurrence,)) + mismatched = ReconcileGenerationEvent( + **_event_fields( + started.next_state, + "reconcile-cross-request", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(other_batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=4, + ) + rejected = reduce_context_admission(started.next_state, mismatched) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + unchanged = _generation_record( + rejected.next_state, + generation.generation_reservation_id, + ) + assert unchanged.state is GenerationState.STREAMING + + +def test_rollover_moves_old_work_to_resolvable_closed_epoch_audit() -> None: + state, batch, _, generation = _reserved_batch( + name="closed-resolution", + input_count=18, + generation_count=7, + ) + state = _prepare_dispatch(state, batch) + batch_record = _batch_record(state, batch) + assert batch_record.dispatch_sequence is not None + receiver = AuthoritySourceId("receiver-closed-resolution") + rollover = RolloverEpochEvent( + **_event_fields(state, "rollover-closed-resolution", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=receiver, + fence_witness_id=AdmissionWitnessId("fence-closed-resolution"), + highest_admitted_dispatch_sequence=batch_record.dispatch_sequence, + ), + deducted_unresolved_count=0, + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + rolled = reduce_context_admission(state, rollover) + assert rolled.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + assert rolled.next_state.batch_records == () + assert rolled.next_state.generation_reservations == () + audit = rolled.next_state.closed_epochs[-1] + assert audit.retained_unresolved_count == 18 + assert audit.retained_generation_count == 7 + + accepted = reduce_context_admission( + rolled.next_state, + AcceptInputEvent( + **_event_fields( + rolled.next_state, + "accept-closed-resolution", + "accept-input", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=17, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ), + ) + assert accepted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(accepted.next_state, ActiveContextAdmissionState) + accepted_audit = accepted.next_state.closed_epochs[-1] + assert accepted_audit.retained_unresolved_count == 0 + assert accepted_audit.terminal_batch_records[0].state is AdmissionState.COMMITTED + + reconciled = reduce_context_admission( + accepted.next_state, + ReconcileGenerationEvent( + **_event_fields( + accepted.next_state, + "reconcile-closed-generation", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=5, + ), + ) + assert reconciled.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(reconciled.next_state, ActiveContextAdmissionState) + reconciled_audit = reconciled.next_state.closed_epochs[-1] + assert reconciled_audit.retained_generation_count == 0 + assert reconciled_audit.terminal_generation_reservations[0].state is GenerationState.RECONCILED + + +def test_rollover_accepts_only_one_explicit_authority_alternative() -> None: + empty_state = _open_epoch() + sentinel = _batch( + "batch-rollover-authority", + (_occurrence("rollover-authority", maximum=1),), + ) + fully_resolved = RolloverEpochEvent( + **_event_fields(empty_state, "rollover-fully-resolved", "rollover-epoch"), + witness=_witness(sentinel, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + deducted_unresolved_count=0, + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + resolved_rollover = reduce_context_admission(empty_state, fully_resolved) + assert resolved_rollover.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + + state, batch, _, _ = _reserved_batch( + name="snapshot-deduction", + input_count=10, + generation_count=0, + ) + state = _prepare_dispatch(state, batch) + deducted = RolloverEpochEvent( + **_event_fields(state, "rollover-snapshot-deducted", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + deducted_unresolved_count=10, + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ) + deducted_rollover = reduce_context_admission(state, deducted) + assert deducted_rollover.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + invalid_deduction = replace( + deducted, + event_id=AdmissionEventId("rollover-invalid-deduction"), + deducted_unresolved_count=9, + ) + rejected = reduce_context_admission(state, invalid_deduction) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 111507147..695c2b9fe 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -39,12 +39,16 @@ ContextSessionId, ContextThreadId, ContextWindowSnapshot, + DeliveryOccurrenceId, DispatchRequestEvent, EpochFenceProof, + ExpireIdempotencyKeyEvent, + ForkOccurrenceId, GenerationReservationId, GenerationReservationRecord, GenerationState, IdempotencyNamespace, + MarkIndeterminateEvent, MeasurementKind, ModelIdentity, ModelItemId, @@ -55,13 +59,19 @@ ProposeOccurrenceEvent, ProtectedPoolOwnerId, ProtectedPoolSpec, + ReconcileGenerationEvent, + ReleaseNonAdmissionEvent, RepresentationBindingWitness, RepresentationRevision, RequestReconciliationEvent, ReserveClass, ReserveRequestEvent, + ResolveIndeterminateNonAdmissionEvent, + ResolveIndeterminateRollbackEvent, + RollbackAdmissionEvent, RolloverEpochEvent, StageHistoryEvent, + StartGenerationEvent, TokenizerIdentity, ToolCallId, TurnId, @@ -69,6 +79,7 @@ WindowEpochId, WitnessKind, reduce_context_admission, + replay_context_admission, ) pytestmark = [pytest.mark.layer("core"), pytest.mark.small] @@ -137,7 +148,13 @@ def _owner(reserve_class: ReserveClass) -> ProtectedPoolOwnerId | None: return None -def _occurrence(slot: int, reserve_class: ReserveClass) -> AdmissionOccurrence: +def _occurrence( + slot: int, + reserve_class: ReserveClass, + *, + window_epoch_id: WindowEpochId, + window_epoch_number: int, +) -> AdmissionOccurrence: name = f"occurrence-{slot}" surface = ( ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY @@ -155,7 +172,7 @@ def _occurrence(slot: int, reserve_class: ReserveClass) -> AdmissionOccurrence: root_thread_id=ContextThreadId("thread-root"), current_thread_id=ContextThreadId("thread-child" if slot == 1 else "thread-root"), parent_thread_id=ContextThreadId("thread-root") if slot == 1 else None, - fork_occurrence_id=None, + fork_occurrence_id=(ForkOccurrenceId("fork-state-machine") if slot == 1 else None), turn_id=TurnId(f"turn-{slot}"), producer_surface=surface, producer_instance_id=ProducerInstanceId(f"producer-{slot}"), @@ -163,9 +180,11 @@ def _occurrence(slot: int, reserve_class: ReserveClass) -> AdmissionOccurrence: model_item_id=ModelItemId(f"item-{slot}"), dispatch_identity=None, attempt_id=AdmissionAttemptId(f"attempt-{slot}"), - delivery_occurrence_id=None, - window_epoch_id=WindowEpochId("epoch-state-machine"), - window_epoch_number=1, + delivery_occurrence_id=( + DeliveryOccurrenceId("delivery-state-machine") if slot == 1 else None + ), + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, ), reserve_class=reserve_class, producer_surface=surface, @@ -199,6 +218,7 @@ def _batch(occurrence: AdmissionOccurrence) -> AdmissionBatch: def _multi_batch(occurrences: tuple[AdmissionOccurrence, ...]) -> AdmissionBatch: + occurrences = tuple(sorted(occurrences, key=lambda occurrence: occurrence.occurrence_id.value)) slots = tuple(occurrence.occurrence_id.value.rsplit("-", 1)[-1] for occurrence in occurrences) request_id = AdmissionRequestId("request-multi-" + "-".join(slots)) reserve_class = occurrences[0].reserve_class @@ -229,11 +249,13 @@ def _multi_batch(occurrences: tuple[AdmissionOccurrence, ...]) -> AdmissionBatch def _reservation( occurrence: AdmissionOccurrence, batch: AdmissionBatch, count: int ) -> AdmissionReservation: + window_epoch_id = occurrence.lineage.window_epoch_id + window_epoch_number = occurrence.lineage.window_epoch_number key = AdmissionReservationKey( idempotency_namespace=_namespace("reserve-request"), protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, - window_epoch_id=WindowEpochId("epoch-state-machine"), - window_epoch_number=1, + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, batch_id=batch.batch_id, reserve_class=occurrence.reserve_class, protected_pool_owner_id=_owner(occurrence.reserve_class), @@ -242,8 +264,8 @@ def _reservation( return AdmissionReservation( reservation_id=AdmissionReservationId(f"reservation-{occurrence.occurrence_id.value}"), key=key, - window_epoch_id=WindowEpochId("epoch-state-machine"), - window_epoch_number=1, + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, snapshot_sequence=1, reserve_class=occurrence.reserve_class, protected_pool_owner_id=_owner(occurrence.reserve_class), @@ -252,14 +274,20 @@ def _reservation( ) -def _reservation_for_batch(batch: AdmissionBatch, count: int) -> AdmissionReservation: +def _reservation_for_batch( + batch: AdmissionBatch, + occurrence: AdmissionOccurrence, + count: int, +) -> AdmissionReservation: occurrence_ids = batch.occurrence_ids slot = occurrence_ids[0].value.rsplit("-", 1)[-1] + window_epoch_id = occurrence.lineage.window_epoch_id + window_epoch_number = occurrence.lineage.window_epoch_number key = AdmissionReservationKey( idempotency_namespace=_namespace("reserve-multi"), protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, - window_epoch_id=WindowEpochId("epoch-state-machine"), - window_epoch_number=1, + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, batch_id=batch.batch_id, reserve_class=batch.reserve_class, protected_pool_owner_id=batch.protected_pool_owner_id, @@ -271,8 +299,8 @@ def _reservation_for_batch(batch: AdmissionBatch, count: int) -> AdmissionReserv return AdmissionReservation( reservation_id=AdmissionReservationId(f"reservation-multi-{slot}"), key=key, - window_epoch_id=WindowEpochId("epoch-state-machine"), - window_epoch_number=1, + window_epoch_id=window_epoch_id, + window_epoch_number=window_epoch_number, snapshot_sequence=1, reserve_class=batch.reserve_class, protected_pool_owner_id=batch.protected_pool_owner_id, @@ -289,9 +317,12 @@ def _generation( f"generation-{occurrence.occurrence_id.value}" ), request_id=batch.request_id, + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + occurrence_ids=batch.occurrence_ids, response_id=ModelItemId(f"response-{occurrence.occurrence_id.value}"), - window_epoch_id=WindowEpochId("epoch-state-machine"), - window_epoch_number=1, + window_epoch_id=occurrence.lineage.window_epoch_id, + window_epoch_number=occurrence.lineage.window_epoch_number, snapshot_sequence=1, reserve_class=occurrence.reserve_class, protected_pool_owner_id=_owner(occurrence.reserve_class), @@ -299,11 +330,13 @@ def _generation( state=GenerationState.RESERVED, exact_terminal_usage=None, witness_ids=(), + authority_source_id=None, ) def _witness( batch: AdmissionBatch, + occurrence: AdmissionOccurrence, kind: WitnessKind, *, occurrence_ids: tuple[AdmissionOccurrenceId, ...] | None = None, @@ -311,8 +344,8 @@ def _witness( return AdmissionWitness( witness_id=AdmissionWitnessId(f"{kind.value}-witness-{batch.batch_id.value}"), kind=kind, - window_epoch_id=WindowEpochId("epoch-state-machine"), - window_epoch_number=1, + window_epoch_id=occurrence.lineage.window_epoch_id, + window_epoch_number=occurrence.lineage.window_epoch_number, snapshot_sequence=1, request_id=batch.request_id, batch_id=batch.batch_id, @@ -356,6 +389,7 @@ def __init__(self) -> None: self.charges: dict[int, tuple[ReserveClass, int, int]] = {} self.latest_replayable_event: Any | None = open_event self.latest_propose_event: ProposeOccurrenceEvent | None = None + self.events: list[Any] = [open_event] self.event_sequence = 0 self.last_revision = self.state.aggregate_revision.value self.last_admission_sequence = self.state.admission_sequence.value @@ -389,6 +423,7 @@ def _accept_publication(self, transition: Any, event: object) -> None: self.last_revision = self.state.aggregate_revision.value self.last_admission_sequence = self.state.admission_sequence.value self.latest_replayable_event = event + self.events.append(event) def _availability(self, reserve_class: ReserveClass) -> tuple[int, int, int]: total_charged = sum( @@ -420,7 +455,12 @@ def _availability(self, reserve_class: ReserveClass) -> tuple[int, int, int]: def propose(self, slot: int, reserve_class: ReserveClass) -> None: if slot in self.occurrences: return - occurrence = _occurrence(slot, reserve_class) + occurrence = _occurrence( + slot, + reserve_class, + window_epoch_id=self.state.snapshot.window_epoch_id, + window_epoch_number=self.state.snapshot.window_epoch_number, + ) event = ProposeOccurrenceEvent(**self._fields("propose-occurrence"), occurrence=occurrence) transition = reduce_context_admission(self.state, event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT @@ -462,30 +502,55 @@ def reserve(self, slot: int, input_count: int, generation_count: int) -> None: generation_count, ) else: - assert transition.next_state == prior + self._accept_publication(transition, event) - @rule(slot=_SLOT) - def stale_fence_is_rejected(self, slot: int) -> None: - if slot not in self.occurrences or slot in self.charges: - return - occurrence = self.occurrences[slot] - batch = _batch(occurrence) - event = ReserveRequestEvent( - **{ - **self._fields("reserve-request"), - "expected_aggregate_revision": AggregateRevision( - max(0, self.state.aggregate_revision.value - 1) + @rule() + def stale_fence_is_rejected(self) -> None: + old_snapshot = self.state.snapshot + new_window_epoch_number = old_snapshot.window_epoch_number + 1 + receiver_authority = AuthoritySourceId("authority-state-machine") + event = RolloverEpochEvent( + **self._fields("rollover-epoch"), + witness=AdmissionWitness( + witness_id=AdmissionWitnessId(f"stale-rollover-witness-{self.event_sequence}"), + kind=WitnessKind.EPOCH_ROLLOVER, + window_epoch_id=old_snapshot.window_epoch_id, + window_epoch_number=old_snapshot.window_epoch_number, + snapshot_sequence=old_snapshot.snapshot_sequence, + request_id=AdmissionRequestId("stale-rollover-request"), + batch_id=AdmissionBatchId("stale-rollover-batch"), + representation_revision=RepresentationRevision("stale-rollover-revision"), + occurrence_ids=(), + authority_source_id=receiver_authority, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=WindowEpochId("stale-window-epoch"), + old_window_epoch_number=old_snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId( + f"epoch-state-machine-{new_window_epoch_number}" ), - }, - batch=batch, - snapshot_sequence=1, - input_reservations=(_reservation(occurrence, batch, 1),), - generation_reservation=_generation(occurrence, batch, 0), + new_window_epoch_number=new_window_epoch_number, + receiver_authority_source_id=receiver_authority, + fence_witness_id=AdmissionWitnessId(f"stale-fence-{self.event_sequence}"), + highest_admitted_dispatch_sequence=self.state.admission_sequence.value, + ), + deducted_unresolved_count=0, + new_snapshot=ContextWindowSnapshot( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId(f"epoch-state-machine-{new_window_epoch_number}"), + window_epoch_number=new_window_epoch_number, + model_identity=ModelIdentity.anthropic("claude-state-machine"), + tokenizer_identity=TokenizerIdentity("tokenizer-state-machine"), + snapshot_sequence=1, + active_count=50, + hard_limit=100, + remaining_count=50, + ), + protected_pools=_pool_specs(), ) - prior = self.state - transition = reduce_context_admission(prior, event) + transition = reduce_context_admission(self.state, event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert transition.next_state == prior + self._accept_publication(transition, event) @rule( slot=_SLOT, @@ -507,21 +572,21 @@ def reserve_multi_member_batch(self, slot: int, input_count: int) -> None: **self._fields("reserve-multi"), batch=batch, snapshot_sequence=1, - input_reservations=(_reservation_for_batch(batch, total),), + input_reservations=(_reservation_for_batch(batch, first, total),), generation_reservation=None, ) prior = self.state transition = reduce_context_admission(prior, event) if transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT: - assert transition.next_state == prior + self._accept_publication(transition, event) return assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT self._accept_publication(transition, event) self.charges[slot] = (first.reserve_class, total, 0) self.charges[other_slot] = (second.reserve_class, 0, 0) - @rule() - def prepare_stage_dispatch_and_accept(self) -> None: + @rule(accept_now=st.booleans()) + def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: if not self.charges: return slot = min(self.charges) @@ -548,7 +613,7 @@ def prepare_stage_dispatch_and_accept(self) -> None: stage_event = StageHistoryEvent( **self._fields("stage-history"), batch_id=batch.batch_id, - witness=_witness(batch, WitnessKind.HISTORY_STAGED), + witness=_witness(batch, occurrence, WitnessKind.HISTORY_STAGED), ) transition = reduce_context_admission(self.state, stage_event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT @@ -556,16 +621,19 @@ def prepare_stage_dispatch_and_accept(self) -> None: dispatch_event = DispatchRequestEvent( **self._fields("dispatch-request"), batch_id=batch.batch_id, - witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + witness=_witness(batch, occurrence, WitnessKind.REQUEST_INCLUDED), ) transition = reduce_context_admission(self.state, dispatch_event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT self._accept_publication(transition, dispatch_event) + if not accept_now: + return accept_event = AcceptInputEvent( **self._fields("accept-input"), batch_id=batch.batch_id, - witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + witness=_witness(batch, occurrence, WitnessKind.PROVIDER_ACCEPTED), final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, exact_input_charge=reserved_input_count, measurement_kind=MeasurementKind.PROVIDER_EXACT, authority_source_id=AuthoritySourceId("authority-state-machine"), @@ -575,9 +643,244 @@ def prepare_stage_dispatch_and_accept(self) -> None: assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT self._accept_publication(transition, accept_event) + @rule() + def prepare_and_mark_indeterminate(self) -> None: + if not self.charges: + return + slot = min(self.charges) + _, input_count, _ = self.charges[slot] + occurrence = self.occurrences[slot] + batch = _batch(occurrence) + reserved = self._find_batch(batch.batch_id) + if reserved is None or reserved.state is not AdmissionState.RESERVED: + return + prepare_event = PrepareBatchEvent( + **self._fields("prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + proposed_charge=input_count, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source_id=AuthoritySourceId("authority-state-machine"), + ) + prepared = reduce_context_admission(self.state, prepare_event) + assert prepared.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(prepared, prepare_event) + indeterminate_event = MarkIndeterminateEvent( + **self._fields("mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ) + marked = reduce_context_admission(self.state, indeterminate_event) + assert marked.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(marked, indeterminate_event) + + @rule(rollback=st.booleans()) + def release_or_rollback_preacceptance(self, rollback: bool) -> None: + for slot in sorted(self.charges): + occurrence = self.occurrences.get(slot) + if occurrence is None: + continue + batch = _batch(occurrence) + record = self._find_batch(batch.batch_id) + if record is None: + continue + if rollback: + if record.state not in { + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + continue + event: Any = RollbackAdmissionEvent( + **self._fields("rollback-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, occurrence, WitnessKind.ROLLBACK), + ) + else: + if record.state not in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + continue + needs_removal = record.state in { + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + } + event = ReleaseNonAdmissionEvent( + **self._fields("release-non-admission"), + batch_id=batch.batch_id, + witness=_witness( + batch, + occurrence, + WitnessKind.NON_ADMISSION, + ), + history_removal_witness=( + _witness(batch, occurrence, WitnessKind.ROLLBACK) + if needs_removal + else None + ), + ) + transition = reduce_context_admission(self.state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + reserve_class, _, _ = self.charges[slot] + self.charges[slot] = (reserve_class, 0, 0) + return + + @rule(rollback=st.booleans()) + def resolve_indeterminate_nonacceptance(self, rollback: bool) -> None: + for slot in sorted(self.charges): + occurrence = self.occurrences.get(slot) + if occurrence is None: + continue + batch = _batch(occurrence) + record = self._find_batch(batch.batch_id) + if record is None or record.state is not AdmissionState.INDETERMINATE: + continue + if rollback: + event: Any = ResolveIndeterminateRollbackEvent( + **self._fields("resolve-indeterminate-rollback"), + batch_id=batch.batch_id, + witness=_witness(batch, occurrence, WitnessKind.ROLLBACK), + ) + else: + event = ResolveIndeterminateNonAdmissionEvent( + **self._fields("resolve-indeterminate-non-admission"), + batch_id=batch.batch_id, + witness=_witness( + batch, + occurrence, + WitnessKind.NON_ADMISSION, + ), + history_removal_witness=_witness( + batch, + occurrence, + WitnessKind.ROLLBACK, + ), + ) + transition = reduce_context_admission(self.state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + reserve_class, _, _ = self.charges[slot] + self.charges[slot] = (reserve_class, 0, 0) + return + + @rule(exact_usage=st.integers(min_value=0, max_value=16)) + def start_and_reconcile_generation(self, exact_usage: int) -> None: + for generation in self.state.generation_reservations: + if generation.state is not GenerationState.RESERVED: + continue + slot_and_occurrence = next( + ( + (slot, occurrence) + for slot, occurrence in self.occurrences.items() + if _batch(occurrence).batch_id == generation.batch_id + ), + None, + ) + if slot_and_occurrence is None: + continue + slot, occurrence = slot_and_occurrence + batch = _batch(occurrence) + record = self._find_batch(batch.batch_id) + if record is None or record.state not in { + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.COMMITTED, + AdmissionState.QUARANTINED, + }: + continue + start = StartGenerationEvent( + **self._fields("start-generation"), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness( + batch, + occurrence, + WitnessKind.REQUEST_INCLUDED, + ), + ) + started = reduce_context_admission(self.state, start) + assert started.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(started, start) + reconcile = ReconcileGenerationEvent( + **self._fields("reconcile-generation"), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness( + batch, + occurrence, + WitnessKind.OUTPUT_USAGE, + ), + exact_output_usage=exact_usage, + ) + reconciled = reduce_context_admission(self.state, reconcile) + expected = ( + AdmissionDecisionKind.QUARANTINED + if exact_usage > generation.maximum_allowance + else AdmissionDecisionKind.WOULD_ADMIT + ) + assert reconciled.decision.kind is expected + self._accept_publication(reconciled, reconcile) + reserve_class, input_count, _ = self.charges[slot] + self.charges[slot] = (reserve_class, input_count, 0) + return + + @rule() + def expire_terminal_idempotency_key(self) -> None: + for record in self.state.idempotency_records: + batch = record.original_descriptor.batch + occurrence = next( + ( + occurrence + for occurrence in self.occurrences.values() + if occurrence.occurrence_id in batch.occurrence_ids + ), + None, + ) + batch_record = self._find_batch(batch.batch_id) + if ( + occurrence is None + or batch_record is None + or batch_record.state + not in { + AdmissionState.COMMITTED, + AdmissionState.RELEASED, + AdmissionState.ROLLED_BACK, + AdmissionState.QUARANTINED, + } + ): + continue + event = ExpireIdempotencyKeyEvent( + **self._fields("expire-idempotency-key"), + reservation_key=record.reservation_key, + expiry_witness=_witness( + batch, + occurrence, + WitnessKind.IDEMPOTENCY_EXPIRY, + ), + ) + transition = reduce_context_admission(self.state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + self._accept_publication(transition, event) + return + @rule() def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: before_charges = dict(self.charges) + retained_batch_ids = { + record.batch.batch_id + for record in self.state.batch_records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + } + retained_slots = { + slot + for slot, occurrence in self.occurrences.items() + if _batch(occurrence).batch_id in retained_batch_ids + } old_snapshot = self.state.snapshot new_window_epoch_number = old_snapshot.window_epoch_number + 1 new_window_epoch_id = WindowEpochId(f"epoch-state-machine-{new_window_epoch_number}") @@ -593,7 +896,14 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: new_window_epoch_number=new_window_epoch_number, receiver_authority_source_id=AuthoritySourceId("authority-state-machine"), fence_witness_id=AdmissionWitnessId(f"fence-{witness_suffix}"), - highest_admitted_dispatch_sequence=self.state.admission_sequence.value, + highest_admitted_dispatch_sequence=max( + ( + record.dispatch_sequence + for record in self.state.batch_records + if record.dispatch_sequence is not None + ), + default=0, + ), ) rollover_event = RolloverEpochEvent( **event_fields, @@ -610,6 +920,7 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: authority_source_id=AuthoritySourceId("authority-state-machine"), ), fence_proof=proof, + deducted_unresolved_count=0, new_snapshot=ContextWindowSnapshot( protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, window_epoch_id=new_window_epoch_id, @@ -629,7 +940,9 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: assert isinstance(new_state, ActiveContextAdmissionState) assert len(new_state.closed_epochs) >= 1 prior_audit = new_state.closed_epochs[-1] - assert prior_audit.retained_unresolved_count >= 0 + assert prior_audit.retained_unresolved_count == sum( + before_charges[slot][1] for slot in retained_slots + ) for record in new_state.batch_records: if record.state in { AdmissionState.REQUEST_DISPATCHED, @@ -638,9 +951,10 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: owner = record.batch.protected_pool_owner_id if owner is None: assert record.batch.reserve_class is ReserveClass.ORDINARY - if before_charges: - self._accept_publication(transition, rollover_event) - self.last_rollover_retention = prior_audit.retained_unresolved_count + self._accept_publication(transition, rollover_event) + self.occurrences = {} + self.charges = {} + self.last_rollover_retention = prior_audit.retained_unresolved_count @precondition(lambda self: self.latest_replayable_event is not None) @rule() @@ -653,6 +967,12 @@ def identical_replay_is_equivalent(self) -> None: assert transition.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT assert transition.next_state == self.state + @rule() + def generated_stream_replay_is_deterministic(self) -> None: + replay = replay_context_admission(_uninitialized(), tuple(self.events)) + assert replay.final_state == self.state + assert len(replay.transitions) == len(self.events) + @precondition(lambda self: self.latest_propose_event is not None) @rule() def changed_intent_conflict_does_not_mutate(self) -> None: diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index f37e0b0ad..edd162d2d 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -23,7 +23,9 @@ AdmissionOccurrence, AdmissionOccurrenceId, AdmissionOccurrenceRecord, + AdmissionReplay, AdmissionRequestId, + AdmissionReservation, AdmissionReservationId, AdmissionReservationKey, AdmissionSequence, @@ -37,6 +39,7 @@ CanonicalRepresentationManifest, CanonicalSpanId, CanonicalSpanOwner, + ChargeCommittedEffect, ChargeDomain, ClosedEpochAudit, ContextAdmissionEvent, @@ -51,20 +54,27 @@ CoverageState, DeliveryOccurrenceId, DispatchIdentity, + EpochFenceProof, + ExpiredIdempotencyTombstone, ForkOccurrenceId, GenerationReservationId, + GenerationReservationRecord, GenerationState, IdempotencyNamespace, + IdempotencyRecord, MeasurementKind, ModelIdentity, ModelItemId, PrepareBatchEvent, + ProcessedEventRecord, + ProducerCoverageDef, ProducerInstanceId, ProducerSurface, ProtectedPoolOwnerId, ProtectedPoolSpec, RepresentationBindingWitness, RepresentationRevision, + ReservationRecordedEffect, ReserveClass, TokenizerIdentity, ToolCallId, @@ -109,6 +119,7 @@ "RECONCILED", "INDETERMINATE", "QUARANTINED", + "INVALIDATED", ), MeasurementKind: ( "PROVIDER_EXACT", @@ -207,6 +218,290 @@ "AuthorityUnavailableEffect", } +_EVENT_BASE_FIELDS = ( + "event_id", + "protocol_version", + "idempotency_namespace", + "expected_aggregate_revision", +) + +EXPECTED_EVENT_FIELDS = { + "OpenEpochEvent": _EVENT_BASE_FIELDS + ("snapshot", "protected_pools"), + "AuthorityUnavailableEvent": _EVENT_BASE_FIELDS + ("reason_code", "authority_state"), + "ProposeOccurrenceEvent": _EVENT_BASE_FIELDS + ("occurrence",), + "ReserveRequestEvent": _EVENT_BASE_FIELDS + + ( + "batch", + "snapshot_sequence", + "input_reservations", + "generation_reservation", + ), + "PrepareBatchEvent": _EVENT_BASE_FIELDS + + ( + "batch_id", + "representation_revision", + "proposed_charge", + "measurement_kind", + "authority_source_id", + ), + "StageHistoryEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "DispatchRequestEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "AcceptInputEvent": _EVENT_BASE_FIELDS + + ( + "batch_id", + "witness", + "final_manifest_revision", + "final_manifest", + "exact_input_charge", + "measurement_kind", + "authority_source_id", + "representation_binding_witness", + ), + "ReleaseNonAdmissionEvent": _EVENT_BASE_FIELDS + + ("batch_id", "witness", "history_removal_witness"), + "RollbackAdmissionEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "MarkIndeterminateEvent": _EVENT_BASE_FIELDS + ("batch_id", "reason_code"), + "ResolveIndeterminateAcceptedEvent": _EVENT_BASE_FIELDS + + ( + "batch_id", + "witness", + "final_manifest_revision", + "final_manifest", + "exact_charge", + "measurement_kind", + "authority_source_id", + "representation_binding_witness", + ), + "ResolveIndeterminateNonAdmissionEvent": _EVENT_BASE_FIELDS + + ("batch_id", "witness", "history_removal_witness"), + "ResolveIndeterminateRollbackEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), + "StartGenerationEvent": _EVENT_BASE_FIELDS + ("generation_reservation_id", "witness"), + "ReconcileGenerationEvent": _EVENT_BASE_FIELDS + + ("generation_reservation_id", "output_usage_witness", "exact_output_usage"), + "MarkGenerationIndeterminateEvent": _EVENT_BASE_FIELDS + + ("generation_reservation_id", "reason_code"), + "RequestReconciliationEvent": _EVENT_BASE_FIELDS + ("target_id", "reason_code"), + "ExpireIdempotencyKeyEvent": _EVENT_BASE_FIELDS + ("reservation_key", "expiry_witness"), + "RolloverEpochEvent": _EVENT_BASE_FIELDS + + ( + "witness", + "fence_proof", + "deducted_unresolved_count", + "new_snapshot", + "protected_pools", + ), +} + +_EFFECT_BASE_FIELDS = ( + "source_event_id", + "resulting_aggregate_revision", + "resulting_admission_sequence", + "target_id", +) +_CHARGE_EFFECT_FIELDS = _EFFECT_BASE_FIELDS + ( + "charge_domain", + "reserve_class", + "protected_pool_owner_id", + "count", + "window_epoch_id", + "snapshot_sequence", + "witness_ids", +) + +EXPECTED_EFFECT_FIELDS = { + "ReservationRecordedEffect": _CHARGE_EFFECT_FIELDS, + "ReservationReleasedEffect": _CHARGE_EFFECT_FIELDS, + "OccurrenceStateChangedEffect": _EFFECT_BASE_FIELDS + ("previous_state", "next_state"), + "ChargeCommittedEffect": _CHARGE_EFFECT_FIELDS, + "GenerationReservationRecordedEffect": _CHARGE_EFFECT_FIELDS, + "GenerationReconciledEffect": _CHARGE_EFFECT_FIELDS, + "ReconciliationQueryRequestedEffect": _EFFECT_BASE_FIELDS + ("reason_code",), + "ReconciliationEscalationEffect": _EFFECT_BASE_FIELDS + ("reason_code",), + "ConflictRejectedEffect": _EFFECT_BASE_FIELDS + ("reason_code",), + "IdempotencyExpiredEffect": _EFFECT_BASE_FIELDS + ("reservation_key", "expiry_witness_id"), + "ReservationInvalidatedEffect": _CHARGE_EFFECT_FIELDS, + "EpochClosedEffect": _EFFECT_BASE_FIELDS + ("fence_proof", "deducted_unresolved_count"), + "QuarantineRecordedEffect": _EFFECT_BASE_FIELDS + ("reason_code",), + "AuthorityUnavailableEffect": _EFFECT_BASE_FIELDS + ("reason_code", "authority_state"), +} + +EXPECTED_RECORD_FIELDS = { + CanonicalSpanOwner: ("span_id", "occurrence_id"), + CanonicalRepresentationManifest: ( + "request_id", + "representation_revision", + "span_owners", + "assembler_identity", + "assembler_witness_id", + ), + AdmissionOccurrence: ( + "occurrence_id", + "lineage", + "reserve_class", + "producer_surface", + "predicted_authoritative_maximum", + "representation_revision", + "owned_span_ids", + ), + AdmissionBatch: ( + "batch_id", + "request_id", + "occurrence_ids", + "reserve_class", + "protected_pool_owner_id", + "manifest", + ), + AdmissionReservationKey: ( + "idempotency_namespace", + "protocol_version", + "window_epoch_id", + "window_epoch_number", + "batch_id", + "reserve_class", + "protected_pool_owner_id", + "occurrence_revisions", + ), + AdmissionReservation: ( + "reservation_id", + "key", + "window_epoch_id", + "window_epoch_number", + "snapshot_sequence", + "reserve_class", + "protected_pool_owner_id", + "occurrence_ids", + "reserved_count", + ), + AdmissionWitness: ( + "witness_id", + "kind", + "window_epoch_id", + "window_epoch_number", + "snapshot_sequence", + "request_id", + "batch_id", + "representation_revision", + "occurrence_ids", + "authority_source_id", + ), + RepresentationBindingWitness: ( + "counted_representation_revision", + "dispatched_representation_revision", + "final_manifest_revision", + "request_id", + "batch_id", + "authority_source_id", + ), + EpochFenceProof: ( + "old_window_epoch_id", + "old_window_epoch_number", + "new_window_epoch_id", + "new_window_epoch_number", + "receiver_authority_source_id", + "fence_witness_id", + "highest_admitted_dispatch_sequence", + ), + ProtectedPoolSpec: ( + "reserve_class", + "capability_owner_id", + "injected_count", + "priority", + "required_release_witness_kind", + ), + AdmissionOccurrenceRecord: ( + "occurrence", + "state", + "batch_id", + "reservation_id", + "accepted_witness_ids", + "indeterminate_reason_code", + "quarantine_reason_code", + ), + AdmissionBatchRecord: ( + "batch", + "state", + "reservation_id", + "witness_ids", + "prepared_input_count", + "committed_input_count", + "unresolved_input_count", + "dispatch_sequence", + ), + GenerationReservationRecord: ( + "generation_reservation_id", + "request_id", + "batch_id", + "representation_revision", + "occurrence_ids", + "response_id", + "window_epoch_id", + "window_epoch_number", + "snapshot_sequence", + "reserve_class", + "protected_pool_owner_id", + "maximum_allowance", + "state", + "exact_terminal_usage", + "witness_ids", + "authority_source_id", + ), + ExpiredIdempotencyTombstone: ( + "namespace", + "reservation_key", + "original_descriptor", + "expiry_witness", + "original_terminal_decision", + ), + ClosedEpochAudit: ( + "snapshot", + "terminal_occurrence_records", + "terminal_batch_records", + "terminal_reservations", + "terminal_generation_reservations", + "closure_witness_id", + "fence_proof", + "processed_event_tombstones", + "retained_unresolved_count", + "retained_generation_count", + ), + ProcessedEventRecord: ( + "event_id", + "event", + "original_decision", + "aggregate_revision", + "admission_sequence", + ), + IdempotencyRecord: ( + "namespace", + "reservation_key", + "original_descriptor", + "original_reserve_decision", + "owning_event_id", + "publication_revision", + ), + CoverageEvidence: ( + "claim_id", + "kind", + "backend", + "configuration_mode", + "verifier", + "source_locator", + "tested_version", + "tested_revision", + "checked_at", + "freshness_policy", + ), + ProducerCoverageDef: ( + "surface", + "control_point_owner", + "observation_state", + "authority_state", + "evidence", + "reason_code", + ), + AdmissionReplay: ("final_state", "transitions"), +} + OPAQUE_STRING_TYPES = ( ContextSessionId, AgentInstanceId, @@ -401,6 +696,21 @@ def test_event_and_effect_unions_are_closed() -> None: } +def test_every_event_effect_and_record_field_set_is_exact() -> None: + event_types = { + event_type.__name__: event_type for event_type in get_args(ContextAdmissionEvent) + } + effect_types = {effect_type.__name__: effect_type for effect_type in get_args(AdmissionEffect)} + assert set(event_types) == set(EXPECTED_EVENT_FIELDS) + assert set(effect_types) == set(EXPECTED_EFFECT_FIELDS) + for type_name, expected_fields in EXPECTED_EVENT_FIELDS.items(): + assert _field_names(event_types[type_name]) == expected_fields + for type_name, expected_fields in EXPECTED_EFFECT_FIELDS.items(): + assert _field_names(effect_types[type_name]) == expected_fields + for record_type, expected_fields in EXPECTED_RECORD_FIELDS.items(): + assert _field_names(record_type) == expected_fields + + @pytest.mark.parametrize( ("record_type", "expected_fields"), [ @@ -639,6 +949,185 @@ def test_privacy_canaries_never_escape_validation_repr_or_serialization(canary: assert canary not in serialized +@pytest.mark.parametrize( + "canary", + [ + "private user message", + "secret-value", + "token=private-value", + "/srv/private/model", + ], +) +def test_free_text_and_model_identity_privacy_is_fail_closed(canary: str) -> None: + with pytest.raises(ContextAdmissionValidationError): + AdmissionDecision( + kind=AdmissionDecisionKind.WOULD_REJECT, + reason_code=canary, + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + requested_count=0, + available_ordinary_count=0, + available_protected_count=0, + ) + with pytest.raises(ContextAdmissionValidationError): + _snapshot(model_identity=ModelIdentity.anthropic(canary)) + + +def test_aggregate_tuples_reject_noncanonical_ordering() -> None: + first = _occurrence("occurrence-a", span_id="span-a") + second = _occurrence("occurrence-b", span_id="span-b") + with pytest.raises(ContextAdmissionValidationError): + CanonicalRepresentationManifest( + request_id=AdmissionRequestId("request-1"), + representation_revision=RepresentationRevision("representation-1"), + span_owners=( + CanonicalSpanOwner( + span_id=second.owned_span_ids[0], + occurrence_id=second.occurrence_id, + ), + CanonicalSpanOwner( + span_id=first.owned_span_ids[0], + occurrence_id=first.occurrence_id, + ), + ), + assembler_identity=ProducerInstanceId("assembler-1"), + assembler_witness_id=AdmissionWitnessId("assembler-witness-1"), + ) + + records = ( + AdmissionOccurrenceRecord( + occurrence=first, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ), + AdmissionOccurrenceRecord( + occurrence=second, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ), + ) + state = ActiveContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + snapshot=_snapshot(), + protected_pools=(), + occurrence_records=records, + batch_records=(), + reservations=(), + generation_reservations=(), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + with pytest.raises(ContextAdmissionValidationError): + replace(state, occurrence_records=tuple(reversed(records))) + + +def test_active_state_rejects_inconsistent_bidirectional_ownership_links() -> None: + occurrence = _occurrence("graph-member", span_id="graph-span") + batch = _batch("batch-graph", (occurrence,)) + proposed = AdmissionOccurrenceRecord( + occurrence=occurrence, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ) + state = ActiveContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + snapshot=_snapshot(), + protected_pools=(), + occurrence_records=(proposed,), + batch_records=(), + reservations=(), + generation_reservations=(), + processed_events=(), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + orphan_batch = AdmissionBatchRecord( + batch=batch, + state=AdmissionState.RESERVED, + reservation_id=None, + witness_ids=(), + prepared_input_count=None, + committed_input_count=0, + unresolved_input_count=0, + dispatch_sequence=None, + ) + with pytest.raises(ContextAdmissionValidationError): + replace(state, batch_records=(orphan_batch,)) + with pytest.raises(ContextAdmissionValidationError): + replace( + state, + occurrence_records=( + replace( + proposed, + state=AdmissionState.RESERVED, + batch_id=AdmissionBatchId("missing-batch"), + ), + ), + ) + + +def test_charge_effects_validate_exact_target_domain_owner_and_counts() -> None: + effect = ReservationRecordedEffect( + source_event_id=AdmissionEventId("event-1"), + resulting_aggregate_revision=AggregateRevision(1), + resulting_admission_sequence=AdmissionSequence(1), + target_id=AdmissionReservationId("reservation-1"), + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=ReserveClass.ORDINARY, + protected_pool_owner_id=None, + count=3, + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + witness_ids=(), + ) + with pytest.raises(ContextAdmissionValidationError): + replace(effect, target_id=AdmissionBatchId("batch-1")) + with pytest.raises(ContextAdmissionValidationError): + replace(effect, charge_domain=ChargeDomain.OUTPUT_GENERATION) + with pytest.raises(ContextAdmissionValidationError): + replace(effect, count=-1) + with pytest.raises(ContextAdmissionValidationError): + replace( + effect, + protected_pool_owner_id=ProtectedPoolOwnerId("ordinary-owner"), + ) + + committed = ChargeCommittedEffect( + source_event_id=AdmissionEventId("event-2"), + resulting_aggregate_revision=AggregateRevision(2), + resulting_admission_sequence=AdmissionSequence(2), + target_id=AdmissionBatchId("batch-1"), + charge_domain=ChargeDomain.INPUT_CONTEXT, + reserve_class=ReserveClass.ORDINARY, + protected_pool_owner_id=None, + count=3, + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + witness_ids=(AdmissionWitnessId("witness-1"),), + ) + with pytest.raises(ContextAdmissionValidationError): + replace(committed, target_id=AdmissionReservationId("reservation-1")) + + def test_unknown_protocol_versions_fail_closed() -> None: assert CONTEXT_ADMISSION_PROTOCOL_VERSION == 1 with pytest.raises(UnsupportedContextAdmissionProtocolError): @@ -651,11 +1140,14 @@ def test_closed_epoch_audits_survive_state_serialization() -> None: empty_audit = ClosedEpochAudit( snapshot=_snapshot(), terminal_occurrence_records=(), + terminal_batch_records=(), terminal_reservations=(), + terminal_generation_reservations=(), closure_witness_id=AdmissionWitnessId("closure-witness-1"), fence_proof=None, processed_event_tombstones=(), retained_unresolved_count=0, + retained_generation_count=0, ) state = replace(_uninitialized(), closed_epochs=(empty_audit,)) restored = UninitializedContextAdmissionState.from_dict(state.to_dict()) @@ -718,6 +1210,7 @@ def test_batch_record_rejects_committed_and_unresolved_simultaneously() -> None: prepared_input_count=None, committed_input_count=5, unresolved_input_count=5, + dispatch_sequence=None, ) assert "committed_and_unresolved_simultaneously" in str(exc_info.value) @@ -781,6 +1274,7 @@ def test_accept_event_rejects_estimate_measurement() -> None: batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, exact_input_charge=5, measurement_kind=MeasurementKind.BYTE_EMERGENCY, authority_source_id=AuthoritySourceId("authority-test"), diff --git a/tests/docs/test_context_admission_decision.py b/tests/docs/test_context_admission_decision.py index 92f393236..8c3272b67 100644 --- a/tests/docs/test_context_admission_decision.py +++ b/tests/docs/test_context_admission_decision.py @@ -15,6 +15,30 @@ CODEX_VERSION = "0.145.0" CODEX_REVISION = "25af12f7e61572b0bc18ddb1008be543b91519b0" +REQUIRED_PINNED_CODEX_PATHS = ( + "codex-rs/core/src/session/context_window.rs", + "codex-rs/core/src/context_manager/history.rs", + "codex-rs/features/src/lib.rs", + "codex-rs/core/src/tools/spec_plan.rs", + "codex-rs/core/src/tools/handlers/get_context_remaining.rs", + "codex-rs/app-server-protocol/src/protocol/v2/thread.rs", + "codex-rs/app-server/README.md", + "codex-rs/protocol/src/protocol.rs", + "codex-rs/utils/string/src/truncate.rs", + "codex-rs/hooks/schema/generated", +) +CURRENT_HOOK_COVERAGE_URL = ( + "https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md#hooks" +) +EXPECTED_DEPENDENCY_EDGES = { + "#4333 C1 -> #4334 C2", + "#4333 C1 + #4334 C2 + #4335 C3 -> #4336 C4", + "#4333 C1 + #4334 C2 + #4335 C3 -> #4337 C5", + "#4333 C1 + #4334 C2 -> #4338 C8", + "#4319/#4320/#4321/#4322/#4325/#4326/#4327 -> #4335 C3 artifact authority", + "#4334 C2 + #4336 C4 + #4337 C5 + #4324 + #4338 C8 -> #4339 C6", + "#4334 C2 + #4335 C3 + #4271 + #4338 C8 -> #4340 C7", +} REQUIRED_HEADINGS = ( "Context", @@ -163,8 +187,10 @@ def test_codex_capability_claims_are_version_pinned_to_primary_evidence( "PostCompact", ]: assert required in capability - primary_source_prefix = f"https://github.com/openai/codex/blob/{CODEX_REVISION}/" - assert capability.count(primary_source_prefix) >= 3 + for source_path in REQUIRED_PINNED_CODEX_PATHS: + assert CODEX_REVISION in capability + assert source_path in capability + assert CURRENT_HOOK_COVERAGE_URL in capability def test_authority_unavailable_behavior_preserves_byte_boundaries( @@ -426,26 +452,7 @@ def test_decision_keeps_issue_non_goals_explicit(decision_text: str) -> None: assert required in non_goals -@pytest.mark.parametrize( - ("work_package", "issue"), - [ - ("C2", "#4334"), - ("C3", "#4335"), - ("C4", "#4336"), - ("C5", "#4337"), - ("C6", "#4339"), - ("C7", "#4340"), - ("C8", "#4338"), - ], -) -def test_dependency_graph_maps_existing_child_issues( - decision_text: str, - work_package: str, - issue: str, -) -> None: +def test_dependency_graph_freezes_exact_edges(decision_text: str) -> None: graph = _section(decision_text, "Downstream dependency graph") - assert re.search( - rf"\b{re.escape(work_package)}\b[^\n]*{re.escape(issue)}" - rf"|{re.escape(issue)}[^\n]*\b{re.escape(work_package)}\b", - graph, - ) + actual_edges = {line.strip() for line in graph.splitlines() if "->" in line} + assert actual_edges == EXPECTED_DEPENDENCY_EDGES From bcba2fba1dc2af7d7e4b121882d11aec764e1b4c Mon Sep 17 00:00:00 2001 From: Trecek Date: Thu, 23 Jul 2026 20:28:45 -0700 Subject: [PATCH 06/62] fix(core): close context admission audit gaps --- src/autoskillit/core/__init__.pyi | 1 + src/autoskillit/core/context_admission.py | 77 ++--- .../core/types/_type_context_admission.py | 81 +++++- .../core/types/_type_dispatch_identity.py | 23 +- tests/core/test_context_admission_reducer.py | 272 +++++++++++++++--- .../test_context_admission_state_machine.py | 47 ++- .../types/test_context_admission_contract.py | 92 +++++- 7 files changed, 508 insertions(+), 85 deletions(-) diff --git a/src/autoskillit/core/__init__.pyi b/src/autoskillit/core/__init__.pyi index 4a503af68..e10263705 100644 --- a/src/autoskillit/core/__init__.pyi +++ b/src/autoskillit/core/__init__.pyi @@ -496,6 +496,7 @@ from .types import ( ReconciliationQueryRequestedEffect as ReconciliationQueryRequestedEffect, ) from .types import ReleaseNonAdmissionEvent as ReleaseNonAdmissionEvent +from .types import RepresentationBindingId as RepresentationBindingId from .types import RepresentationBindingWitness as RepresentationBindingWitness from .types import RepresentationRevision as RepresentationRevision from .types import RequestReconciliationEvent as RequestReconciliationEvent diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index 229f6e1e2..a979b27d9 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -333,27 +333,8 @@ def _reject( reserve_class=reserve_class, protected_pool_owner_id=protected_pool_owner_id, ) - processed = ProcessedEventRecord( - event_id=event.event_id, - event=event, - original_decision=decision, - aggregate_revision=state.aggregate_revision, - admission_sequence=state.admission_sequence, - ) - next_state = replace( - state, - processed_events=tuple( - sorted( - state.processed_events + (processed,), - key=lambda record: ( - record.aggregate_revision.value, - record.event_id.value, - ), - ) - ), - ) return AdmissionTransition( - next_state=next_state, + next_state=state, decision=decision, effects=(), ) @@ -673,6 +654,7 @@ def _validate_witness_for_snapshot( and witness.request_id == batch.request_id and witness.batch_id == batch.batch_id and witness.representation_revision == batch.manifest.representation_revision + and witness.representation_binding_id == batch.manifest.representation_binding_id and witness.occurrence_ids == batch.occurrence_ids ) @@ -954,10 +936,25 @@ def _reserve( reserve_class=event.batch.reserve_class, protected_pool_owner_id=event.batch.protected_pool_owner_id, ) + protected_release_witness_kind = None + if event.batch.protected_pool_owner_id is not None: + pool = next( + ( + item + for item in state.protected_pools + if item.reserve_class is event.batch.reserve_class + and item.capability_owner_id == event.batch.protected_pool_owner_id + ), + None, + ) + if pool is None: + return _reject(state, event, "unknown_protected_pool") + protected_release_witness_kind = pool.required_release_witness_kind batch_record = AdmissionBatchRecord( batch=event.batch, state=AdmissionState.RESERVED, reservation_id=reservation.reservation_id, + protected_release_witness_kind=protected_release_witness_kind, witness_ids=(), prepared_input_count=None, committed_input_count=0, @@ -1094,6 +1091,8 @@ def _prepare( return _reject(state, event, "illegal_prepare_order") if event.representation_revision != record.batch.manifest.representation_revision: return _reject(state, event, "representation_revision_mismatch") + if event.representation_binding_id != record.batch.manifest.representation_binding_id: + return _reject(state, event, "representation_binding_mismatch") reservation = _reservation_for(state, record) if reservation is None or event.proposed_charge != reservation.reserved_count: return _reject(state, event, "prepared_charge_mismatch") @@ -1185,7 +1184,7 @@ def _dispatch( record = _batch_record(state, event.batch_id) if ( record is None - or record.state not in {AdmissionState.PREPARED, AdmissionState.HISTORY_STAGED} + or record.state is not AdmissionState.HISTORY_STAGED or not _validate_witness( state, record.batch, @@ -1286,11 +1285,13 @@ def _accept_closed_input( ) or event.measurement_kind is not MeasurementKind.PROVIDER_EXACT or event.final_manifest_revision != expected_revision + or event.final_manifest != record.batch.manifest or event.final_manifest.representation_revision != expected_revision or event.final_manifest.request_id != record.batch.request_id or binding.counted_representation_revision != expected_revision or binding.dispatched_representation_revision != expected_revision or binding.final_manifest_revision != expected_revision + or binding.representation_binding_id != record.batch.manifest.representation_binding_id or binding.request_id != record.batch.request_id or binding.batch_id != record.batch.batch_id or event.authority_source_id != event.witness.authority_source_id @@ -1438,6 +1439,11 @@ def _accept( return _reject(state, event, "invalid_provider_acceptance_witness") binding = event.representation_binding_witness expected_revision = record.batch.manifest.representation_revision + if ( + event.final_manifest != record.batch.manifest + or binding.representation_binding_id != record.batch.manifest.representation_binding_id + ): + return _reject(state, event, "representation_binding_mismatch") if ( event.final_manifest_revision != expected_revision or event.final_manifest.representation_revision != expected_revision @@ -1587,6 +1593,10 @@ def _release_closed_batch( ) if ( record.state is not expected_state + or ( + record.protected_release_witness_kind is not None + and record.protected_release_witness_kind is not expected_kind + ) or not _validate_witness_for_snapshot( audit.snapshot, record.batch, @@ -1790,6 +1800,11 @@ def _release_or_rollback( AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED, } + if ( + record.protected_release_witness_kind is not None + and record.protected_release_witness_kind is not expected_witness + ): + return _reject(state, event, "protected_release_policy_mismatch") if record.state not in allowed_states or not _validate_witness( state, record.batch, @@ -2009,11 +2024,13 @@ def _resolve_indeterminate_accepted( or event.authority_source_id != binding.authority_source_id or event.measurement_kind is not MeasurementKind.PROVIDER_EXACT or event.final_manifest_revision != expected_revision + or event.final_manifest != record.batch.manifest or event.final_manifest.representation_revision != expected_revision or event.final_manifest.request_id != record.batch.request_id or binding.counted_representation_revision != expected_revision or binding.dispatched_representation_revision != expected_revision or binding.final_manifest_revision != expected_revision + or binding.representation_binding_id != record.batch.manifest.representation_binding_id or binding.request_id != record.batch.request_id or binding.batch_id != record.batch.batch_id ): @@ -2100,13 +2117,7 @@ def _start_generation( if ( batch is None or batch_record is None - or batch_record.state - not in { - AdmissionState.HISTORY_STAGED, - AdmissionState.REQUEST_DISPATCHED, - AdmissionState.COMMITTED, - AdmissionState.QUARANTINED, - } + or batch_record.state is not AdmissionState.REQUEST_DISPATCHED or not _validate_witness( state, batch, @@ -2817,10 +2828,12 @@ def reduce_context_admission( ), ), ) - return AdmissionTransition( - next_state=state, - decision=_decision(state, kind, event.reason_code), - effects=(), + return _publish( + state, + state, + event, + kind=kind, + reason_code=event.reason_code, ) case ProposeOccurrenceEvent(): return _propose(state, event) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 923d55472..40ee49661 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -9,9 +9,19 @@ import re from collections.abc import Callable, Mapping -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields from enum import StrEnum -from typing import Any, ClassVar, Never, TypeAlias +from types import UnionType +from typing import ( + Any, + ClassVar, + Never, + TypeAlias, + Union, + get_args, + get_origin, + get_type_hints, +) from ._type_dispatch_identity import DispatchIdentity from ._type_enums import ( @@ -207,7 +217,13 @@ def _decode(value: object) -> object: class _ContractMeta(type): def __call__(cls, *args: Any, **kwargs: Any) -> Any: - instance = super().__call__(*args, **kwargs) + try: + instance = super().__call__(*args, **kwargs) + except ContextAdmissionValidationError: + raise + except (AttributeError, TypeError, ValueError) as exc: + raise ContextAdmissionValidationError("invalid_contract_field_type") from exc + _validate_declared_field_types(instance) _validate_deep_immutability(instance) return instance @@ -251,6 +267,45 @@ def _validate_deep_immutability(value: object) -> None: _validate_deep_immutability(getattr(value, field_name)) +def _matches_declared_type(value: object, declared_type: object) -> bool: + if declared_type is Any: + return True + origin = get_origin(declared_type) + if origin in {Union, UnionType}: + return any(_matches_declared_type(value, member) for member in get_args(declared_type)) + if origin is tuple: + if type(value) is not tuple: + return False + members = get_args(declared_type) + if len(members) == 2 and members[1] is Ellipsis: + return all(_matches_declared_type(item, members[0]) for item in value) + return len(value) == len(members) and all( + _matches_declared_type(item, member) + for item, member in zip(value, members, strict=True) + ) + if origin is frozenset: + if type(value) is not frozenset: + return False + (member_type,) = get_args(declared_type) + return all(_matches_declared_type(item, member_type) for item in value) + if declared_type is None or declared_type is type(None): + return value is None + if isinstance(declared_type, type): + return type(value) is declared_type + return False + + +def _validate_declared_field_types(value: _ContractValue) -> None: + declared_types = get_type_hints(type(value)) + for declared_field in fields(value): + declared_type = declared_types.get(declared_field.name) + if declared_type is None or not _matches_declared_type( + getattr(value, declared_field.name), + declared_type, + ): + _raise_invalid("invalid_contract_field_type") + + @dataclass(frozen=True, slots=True) class _OpaqueString(_ContractValue): value: str @@ -388,6 +443,11 @@ class RepresentationRevision(_OpaqueString): pass +@dataclass(frozen=True, slots=True) +class RepresentationBindingId(_OpaqueString): + pass + + @dataclass(frozen=True, slots=True) class AggregateRevision(_NonNegativeInteger): pass @@ -504,6 +564,7 @@ class CanonicalSpanOwner(_ContractValue): class CanonicalRepresentationManifest(_ContractValue): request_id: AdmissionRequestId representation_revision: RepresentationRevision + representation_binding_id: RepresentationBindingId span_owners: tuple[CanonicalSpanOwner, ...] assembler_identity: ProducerInstanceId assembler_witness_id: AdmissionWitnessId @@ -644,6 +705,7 @@ class AdmissionWitness(_ContractValue): request_id: AdmissionRequestId batch_id: AdmissionBatchId representation_revision: RepresentationRevision + representation_binding_id: RepresentationBindingId occurrence_ids: tuple[AdmissionOccurrenceId, ...] authority_source_id: AuthoritySourceId @@ -662,6 +724,7 @@ class RepresentationBindingWitness(_ContractValue): counted_representation_revision: RepresentationRevision dispatched_representation_revision: RepresentationRevision final_manifest_revision: RepresentationRevision + representation_binding_id: RepresentationBindingId request_id: AdmissionRequestId batch_id: AdmissionBatchId authority_source_id: AuthoritySourceId @@ -760,6 +823,7 @@ class AdmissionBatchRecord(_ContractValue): batch: AdmissionBatch state: AdmissionState reservation_id: AdmissionReservationId | None + protected_release_witness_kind: WitnessKind | None witness_ids: tuple[AdmissionWitnessId, ...] prepared_input_count: int | None committed_input_count: int @@ -767,6 +831,10 @@ class AdmissionBatchRecord(_ContractValue): dispatch_sequence: int | None def __post_init__(self) -> None: + if (self.batch.reserve_class is ReserveClass.ORDINARY) != ( + self.protected_release_witness_kind is None + ): + _raise_invalid("invalid_protected_release_policy") _validate_canonical_tuple( self.witness_ids, "noncanonical_witness_ids", @@ -1130,6 +1198,7 @@ def __post_init__(self) -> None: class PrepareBatchEvent(_AdmissionEventBase): batch_id: AdmissionBatchId representation_revision: RepresentationRevision + representation_binding_id: RepresentationBindingId proposed_charge: int measurement_kind: MeasurementKind authority_source_id: AuthoritySourceId @@ -1927,6 +1996,11 @@ def __post_init__(self) -> None: key = (record.batch.reserve_class, owner) if key not in pools_by_key: _raise_invalid("orphan_protected_charge_owner") + if ( + record.protected_release_witness_kind + is not pools_by_key[key].required_release_witness_kind + ): + _raise_invalid("protected_release_policy_mismatch") if record.state is AdmissionState.INDETERMINATE: charge = record.unresolved_input_count if charge == 0 and matched_reservation is not None: @@ -2150,6 +2224,7 @@ def _coverage_row(surface: ProducerSurface) -> ProducerCoverageDef: "GenerationReservationId", "ProtectedPoolOwnerId", "RepresentationRevision", + "RepresentationBindingId", "AggregateRevision", "AdmissionSequence", "IdempotencyNamespace", diff --git a/src/autoskillit/core/types/_type_dispatch_identity.py b/src/autoskillit/core/types/_type_dispatch_identity.py index edaea263c..e4b0ffb4d 100644 --- a/src/autoskillit/core/types/_type_dispatch_identity.py +++ b/src/autoskillit/core/types/_type_dispatch_identity.py @@ -5,6 +5,7 @@ from __future__ import annotations +import re from dataclasses import dataclass from uuid import uuid4 @@ -15,6 +16,15 @@ class PromptContractError(RuntimeError): """Raised when a prompt violates the sentinel contract.""" +_DISPATCH_ID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\Z") + + +def _validate_dispatch_id(dispatch_id: object) -> str: + if not isinstance(dispatch_id, str) or _DISPATCH_ID.fullmatch(dispatch_id) is None: + raise ValueError("invalid_dispatch_identity") + return dispatch_id + + def _build_sentinel_contract(dispatch_id: str, short: str) -> str: return f"""\ --- SECTION 8: FINAL OUTPUT CONTRACT --- @@ -51,6 +61,17 @@ class DispatchIdentity: sentinel_close: str sentinel_contract: str + def __post_init__(self) -> None: + did = _validate_dispatch_id(self.dispatch_id) + short = did[:8] + if ( + self.completion_marker != f"%%L3_DONE::{short}%%" + or self.sentinel_open != f"---l3-result::{did}---" + or self.sentinel_close != f"---end-l3-result::{did}---" + or self.sentinel_contract != _build_sentinel_contract(did, short) + ): + raise ValueError("invalid_dispatch_identity") + @classmethod def fresh(cls) -> DispatchIdentity: did = str(uuid4()) @@ -58,7 +79,7 @@ def fresh(cls) -> DispatchIdentity: @classmethod def from_dispatch_id(cls, dispatch_id: str) -> DispatchIdentity: - return cls._from_id(dispatch_id) + return cls._from_id(_validate_dispatch_id(dispatch_id)) @classmethod def _from_id(cls, did: str) -> DispatchIdentity: diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index fc6756ce8..34f814900 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -62,6 +62,7 @@ ProtectedPoolSpec, ReconcileGenerationEvent, ReleaseNonAdmissionEvent, + RepresentationBindingId, RepresentationBindingWitness, RepresentationRevision, RequestReconciliationEvent, @@ -236,6 +237,7 @@ def _manifest( return CanonicalRepresentationManifest( request_id=AdmissionRequestId(request), representation_revision=RepresentationRevision(revision), + representation_binding_id=RepresentationBindingId(f"binding-{request}"), span_owners=tuple( CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) for occurrence in occurrences @@ -347,6 +349,7 @@ def _witness( representation_revision=RepresentationRevision( revision or batch.manifest.representation_revision.value ), + representation_binding_id=batch.manifest.representation_binding_id, occurrence_ids=batch.occurrence_ids, authority_source_id=AuthoritySourceId("authority-test"), ) @@ -362,6 +365,7 @@ def _binding( counted_representation_revision=bound_revision, dispatched_representation_revision=bound_revision, final_manifest_revision=bound_revision, + representation_binding_id=batch.manifest.representation_binding_id, request_id=batch.request_id, batch_id=batch.batch_id, authority_source_id=AuthoritySourceId("authority-test"), @@ -426,12 +430,11 @@ def _batch_record(state: ActiveContextAdmissionState, batch: AdmissionBatch) -> ) -def _assert_only_rejection_journal_changed( +def _assert_rejection_unchanged( before: ActiveContextAdmissionState, after: ActiveContextAdmissionState, ) -> None: - assert replace(after, processed_events=before.processed_events) == before - assert len(after.processed_events) == len(before.processed_events) + 1 + assert after == before def _generation_record( @@ -506,7 +509,7 @@ def test_stale_concurrent_proposals_cannot_overcommit_one_snapshot() -> None: expected_revision=shared_revision, ) assert stale_transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT - _assert_only_rejection_journal_changed( + _assert_rejection_unchanged( first_transition.next_state, stale_transition.next_state, ) @@ -592,6 +595,7 @@ def _prepare_dispatch( **_event_fields(state, f"prepare-{batch.batch_id.value}", "prepare-batch"), batch_id=batch.batch_id, representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=_input_reservation(state, batch_record.reservation_id).reserved_count, measurement_kind=MeasurementKind.TOKENIZER_EXACT, authority_source_id=AuthoritySourceId("authority-test"), @@ -599,26 +603,99 @@ def _prepare_dispatch( prepared = reduce_context_admission(state, prepare) assert prepared.decision.kind is AdmissionDecisionKind.WOULD_ADMIT assert isinstance(prepared.next_state, ActiveContextAdmissionState) - dispatch = DispatchRequestEvent( + stage = StageHistoryEvent( **_event_fields( prepared.next_state, + f"stage-{batch.batch_id.value}", + "stage-history", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.HISTORY_STAGED), + ) + staged = reduce_context_admission(prepared.next_state, stage) + assert staged.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(staged.next_state, ActiveContextAdmissionState) + dispatch = DispatchRequestEvent( + **_event_fields( + staged.next_state, f"dispatch-{batch.batch_id.value}", "dispatch-request", ), batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), ) - dispatched = reduce_context_admission(prepared.next_state, dispatch) + dispatched = reduce_context_admission(staged.next_state, dispatch) assert dispatched.decision.kind is AdmissionDecisionKind.WOULD_ADMIT assert isinstance(dispatched.next_state, ActiveContextAdmissionState) return dispatched.next_state +def test_dispatch_and_generation_cannot_skip_required_lifecycle_transitions() -> None: + state, batch, _, generation = _reserved_batch(name="strict-lifecycle") + batch_record = _batch_record(state, batch) + assert batch_record.reservation_id is not None + prepare = PrepareBatchEvent( + **_event_fields(state, "prepare-strict-lifecycle", "prepare-batch"), + batch_id=batch.batch_id, + representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, + proposed_charge=_input_reservation(state, batch_record.reservation_id).reserved_count, + measurement_kind=MeasurementKind.TOKENIZER_EXACT, + authority_source_id=AuthoritySourceId("authority-test"), + ) + prepared = reduce_context_admission(state, prepare) + assert isinstance(prepared.next_state, ActiveContextAdmissionState) + + skipped_stage = DispatchRequestEvent( + **_event_fields( + prepared.next_state, + "dispatch-without-history-stage", + "dispatch-request", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ) + rejected_dispatch = reduce_context_admission(prepared.next_state, skipped_stage) + assert rejected_dispatch.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected_dispatch.next_state == prepared.next_state + + stage = StageHistoryEvent( + **_event_fields(prepared.next_state, "stage-strict-lifecycle", "stage-history"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.HISTORY_STAGED), + ) + staged = reduce_context_admission(prepared.next_state, stage) + assert isinstance(staged.next_state, ActiveContextAdmissionState) + premature_generation = StartGenerationEvent( + **_event_fields( + staged.next_state, + "generation-before-dispatch", + "start-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ) + rejected_generation = reduce_context_admission( + staged.next_state, + premature_generation, + ) + assert rejected_generation.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected_generation.next_state == staged.next_state + + def test_input_commit_and_output_reconciliation_remain_distinct_domains() -> None: state, batch, _, generation = _reserved_batch() state = _prepare_dispatch(state, batch) + start = StartGenerationEvent( + **_event_fields(state, "start-generation-lifecycle", "start-generation"), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + ) + started = reduce_context_admission(state, start) + assert started.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(started.next_state, ActiveContextAdmissionState) accept = AcceptInputEvent( - **_event_fields(state, "accept-lifecycle", "accept-input"), + **_event_fields(started.next_state, "accept-lifecycle", "accept-input"), batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), final_manifest_revision=batch.manifest.representation_revision, @@ -628,21 +705,14 @@ def test_input_commit_and_output_reconciliation_remain_distinct_domains() -> Non authority_source_id=AuthoritySourceId("authority-test"), representation_binding_witness=_binding(batch), ) - accepted = reduce_context_admission(state, accept) + accepted = reduce_context_admission(started.next_state, accept) assert accepted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT assert isinstance(accepted.next_state, ActiveContextAdmissionState) assert _batch_record(accepted.next_state, batch).committed_input_count == 18 - start = StartGenerationEvent( - **_event_fields(accepted.next_state, "start-generation-lifecycle", "start-generation"), - generation_reservation_id=generation.generation_reservation_id, - witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), - ) - started = reduce_context_admission(accepted.next_state, start) - assert isinstance(started.next_state, ActiveContextAdmissionState) reconcile = ReconcileGenerationEvent( **_event_fields( - started.next_state, + accepted.next_state, "reconcile-generation-lifecycle", "reconcile-generation", ), @@ -650,7 +720,7 @@ def test_input_commit_and_output_reconciliation_remain_distinct_domains() -> Non output_usage_witness=_witness(batch, WitnessKind.OUTPUT_USAGE), exact_output_usage=7, ) - reconciled = reduce_context_admission(started.next_state, reconcile) + reconciled = reduce_context_admission(accepted.next_state, reconcile) assert isinstance(reconciled.next_state, ActiveContextAdmissionState) generation_record = _generation_record( reconciled.next_state, generation.generation_reservation_id @@ -773,7 +843,7 @@ def test_history_request_provider_and_rollback_witnesses_are_not_interchangeable ) rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - _assert_only_rejection_journal_changed(state, rejected.next_state) + _assert_rejection_unchanged(state, rejected.next_state) @pytest.mark.parametrize( @@ -793,6 +863,7 @@ def test_revision_and_fence_failures_do_not_commit( **_event_fields(state, f"prepare-{mutation}", "prepare-batch"), batch_id=batch.batch_id, representation_revision=RepresentationRevision("mutated-revision"), + representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=25, measurement_kind=MeasurementKind.PROVIDER_EXACT, authority_source_id=AuthoritySourceId("authority-test"), @@ -817,7 +888,7 @@ def test_revision_and_fence_failures_do_not_commit( rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert expected_reason_fragment in rejected.decision.reason_code - _assert_only_rejection_journal_changed(state, rejected.next_state) + _assert_rejection_unchanged(state, rejected.next_state) def test_partial_batch_and_illegal_order_are_typed_non_mutating_rejections() -> None: @@ -861,14 +932,16 @@ def test_partial_batch_and_illegal_order_are_typed_non_mutating_rejections() -> ): rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT, index - _assert_only_rejection_journal_changed(state, rejected.next_state) + _assert_rejection_unchanged(state, rejected.next_state) @pytest.mark.parametrize( "mutation", ( "final-manifest-revision", - "representation-binding", + "representation-revision-binding", + "same-revision-binding-identity", + "assembler-attestation", "silent-truncation", ), ) @@ -889,23 +962,37 @@ def test_acceptance_rejects_mutated_or_unattested_final_representation( ) binding_revision = ( "mutated-binding" - if mutation == "representation-binding" + if mutation == "representation-revision-binding" else batch.manifest.representation_revision.value ) + final_manifest = ( + replace( + batch.manifest, + assembler_witness_id=AdmissionWitnessId("mutated-assembler-attestation"), + ) + if mutation == "assembler-attestation" + else batch.manifest + ) + binding = _binding(batch, revision=binding_revision) + if mutation == "same-revision-binding-identity": + binding = replace( + binding, + representation_binding_id=RepresentationBindingId("mutated-binding-identity"), + ) event = AcceptInputEvent( **_event_fields(state, f"reject-{mutation}", "accept-input"), batch_id=batch.batch_id, witness=_witness(batch, witness_kind), final_manifest_revision=final_revision, - final_manifest=batch.manifest, + final_manifest=final_manifest, exact_input_charge=20, measurement_kind=MeasurementKind.PROVIDER_EXACT, authority_source_id=AuthoritySourceId("provider-test"), - representation_binding_witness=_binding(batch, revision=binding_revision), + representation_binding_witness=binding, ) rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - _assert_only_rejection_journal_changed(state, rejected.next_state) + _assert_rejection_unchanged(state, rejected.next_state) @pytest.mark.parametrize( @@ -1004,7 +1091,7 @@ def test_provider_accepted_overage_is_recorded_and_quarantined( assert record.state is AdmissionState.QUARANTINED -def test_incomplete_manifest_quarantine_retains_charge_and_publishes_all_effects() -> None: +def test_incomplete_manifest_rejection_is_non_mutating_and_retains_reservation() -> None: state = _open_epoch(remaining_count=40) occurrence = replace( _occurrence("incomplete-manifest", maximum=20), @@ -1034,20 +1121,14 @@ def test_incomplete_manifest_quarantine_retains_charge_and_publishes_all_effects authority_source_id=AuthoritySourceId("authority-test"), representation_binding_witness=_binding(batch), ) - quarantined = reduce_context_admission(state, event) - assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED - assert isinstance(quarantined.next_state, ActiveContextAdmissionState) - quarantined_record = _batch_record(quarantined.next_state, batch) - assert quarantined_record.committed_input_count == 17 - assert quarantined_record.state is AdmissionState.QUARANTINED - assert {type(effect).__name__ for effect in quarantined.effects} == { - "ChargeCommittedEffect", - "OccurrenceStateChangedEffect", - "QuarantineRecordedEffect", - } + rejected_acceptance = reduce_context_admission(state, event) + assert rejected_acceptance.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected_acceptance.decision.reason_code == "representation_binding_mismatch" + assert rejected_acceptance.next_state == state + assert rejected_acceptance.effects == () next_occurrence = _occurrence("after-incomplete-manifest", maximum=24) - next_state, _ = _propose(quarantined.next_state, next_occurrence) + next_state, _ = _propose(state, next_occurrence) next_batch = _batch("batch-after-incomplete-manifest", (next_occurrence,)) rejected, _ = _reserve( next_state, @@ -1211,7 +1292,7 @@ def test_rollover_rejects_stale_admitted_dispatch_fence() -> None: rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert isinstance(rejected.next_state, ActiveContextAdmissionState) - _assert_only_rejection_journal_changed(state, rejected.next_state) + _assert_rejection_unchanged(state, rejected.next_state) def _protected_pools() -> tuple[ProtectedPoolSpec, ...]: @@ -1233,6 +1314,89 @@ def _protected_pools() -> tuple[ProtectedPoolSpec, ...]: ) +@pytest.mark.parametrize( + ("required_kind", "expected_state"), + [ + (WitnessKind.NON_ADMISSION, AdmissionState.RELEASED), + (WitnessKind.ROLLBACK, AdmissionState.ROLLED_BACK), + ], +) +def test_protected_pool_release_policy_accepts_only_its_configured_resolution( + required_kind: WitnessKind, + expected_state: AdmissionState, +) -> None: + owner = ProtectedPoolOwnerId("policy-owner") + pool = ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=owner, + injected_count=30, + priority=1, + required_release_witness_kind=required_kind, + ) + state = _open_epoch(remaining_count=60, protected_pools=(pool,)) + occurrence = _occurrence( + "protected-release-policy", + maximum=10, + reserve_class=ReserveClass.SYNTHESIS, + ) + state, _ = _propose(state, occurrence) + batch = _batch( + "batch-protected-release-policy", + (occurrence,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner=owner.value, + ) + reserved, _ = _reserve( + state, + batch, + (occurrence,), + input_count=10, + generation_count=0, + ) + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + state = _prepare_dispatch(reserved.next_state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-protected-policy", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="ambiguous-provider-result", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + state = marked.next_state + non_admission = ResolveIndeterminateNonAdmissionEvent( + **_event_fields(state, "resolve-protected-non-admission", "resolve-non-admission"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION), + history_removal_witness=_witness( + batch, + WitnessKind.ROLLBACK, + witness="protected-history-removal", + ), + ) + rollback = ResolveIndeterminateRollbackEvent( + **_event_fields(state, "resolve-protected-rollback", "resolve-rollback"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.ROLLBACK), + ) + allowed_event, rejected_event = ( + (non_admission, rollback) + if required_kind is WitnessKind.NON_ADMISSION + else (rollback, non_admission) + ) + + rejected = reduce_context_admission(state, rejected_event) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.decision.reason_code == "protected_release_policy_mismatch" + assert rejected.next_state == state + + allowed = reduce_context_admission(state, allowed_event) + assert allowed.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(allowed.next_state, ActiveContextAdmissionState) + assert _batch_record(allowed.next_state, batch).state is expected_state + + def test_protected_pools_are_isolated_without_double_subtracting_usage() -> None: state = _open_epoch(remaining_count=100, protected_pools=_protected_pools()) synthesis = _occurrence("synthesis", maximum=15, reserve_class=ReserveClass.SYNTHESIS) @@ -1345,6 +1509,24 @@ def test_authority_unavailable_never_creates_spendable_capacity( assert transition.decision.kind is expected assert isinstance(transition.next_state, UninitializedContextAdmissionState) assert not hasattr(transition.next_state, "snapshot") + replayed = reduce_context_admission(transition.next_state, event) + assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert replayed.next_state == transition.next_state + + +def test_authority_unavailable_active_event_replay_is_idempotent() -> None: + state = _open_epoch() + event = AuthorityUnavailableEvent( + **_event_fields(state, "authority-active", "authority-unavailable"), + reason_code="provider-watermark-unavailable", + authority_state=CoverageState.PARTIAL, + ) + transition = reduce_context_admission(state, event) + assert transition.decision.kind is AdmissionDecisionKind.WATERMARK_UNAVAILABLE + assert isinstance(transition.next_state, ActiveContextAdmissionState) + replayed = reduce_context_admission(transition.next_state, event) + assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert replayed.next_state == transition.next_state def test_full_stream_replay_uses_each_next_state_as_the_only_next_input() -> None: @@ -1379,12 +1561,13 @@ def test_full_stream_replay_uses_each_next_state_as_the_only_next_input() -> Non assert restored == replay -def test_rejected_event_replay_is_idempotent_and_changed_reuse_conflicts() -> None: +def test_rejected_event_retries_and_changed_reuse_are_non_mutating() -> None: state = _open_epoch() rejected_event = PrepareBatchEvent( **_event_fields(state, "rejected-event", "prepare-batch"), batch_id=AdmissionBatchId("unknown-batch"), representation_revision=RepresentationRevision("revision-1"), + representation_binding_id=RepresentationBindingId("binding-unknown"), proposed_charge=1, measurement_kind=MeasurementKind.PROVIDER_EXACT, authority_source_id=AuthoritySourceId("authority-test"), @@ -1394,7 +1577,7 @@ def test_rejected_event_replay_is_idempotent_and_changed_reuse_conflicts() -> No assert isinstance(rejected.next_state, ActiveContextAdmissionState) replayed = reduce_context_admission(rejected.next_state, rejected_event) - assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert replayed.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert replayed.next_state == rejected.next_state changed = replace( @@ -1402,7 +1585,7 @@ def test_rejected_event_replay_is_idempotent_and_changed_reuse_conflicts() -> No batch_id=AdmissionBatchId("different-unknown-batch"), ) conflicted = reduce_context_admission(rejected.next_state, changed) - assert conflicted.decision.kind is AdmissionDecisionKind.CONFLICT + assert conflicted.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert conflicted.next_state == rejected.next_state @@ -1416,7 +1599,7 @@ def test_idempotency_expiry_is_explicit_and_does_not_release_capacity() -> None: ) rejected = reduce_context_admission(state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - _assert_only_rejection_journal_changed(state, rejected.next_state) + _assert_rejection_unchanged(state, rejected.next_state) def test_generation_indeterminate_remains_reserved_across_reconciliation_deadline() -> None: @@ -1507,7 +1690,7 @@ def test_plain_rollback_cannot_resolve_indeterminate_input() -> None: rejected = reduce_context_admission(marked.next_state, event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert isinstance(rejected.next_state, ActiveContextAdmissionState) - _assert_only_rejection_journal_changed(marked.next_state, rejected.next_state) + _assert_rejection_unchanged(marked.next_state, rejected.next_state) assert _batch_record(rejected.next_state, batch).state is AdmissionState.INDETERMINATE @@ -1554,6 +1737,7 @@ def test_staged_release_requires_distinct_history_removal_witness() -> None: **_event_fields(state, "prepare-history-removal", "prepare-batch"), batch_id=batch.batch_id, representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=_input_reservation( state, record.reservation_id, diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 695c2b9fe..58cd7f1ff 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -61,6 +61,7 @@ ProtectedPoolSpec, ReconcileGenerationEvent, ReleaseNonAdmissionEvent, + RepresentationBindingId, RepresentationBindingWitness, RepresentationRevision, RequestReconciliationEvent, @@ -200,6 +201,7 @@ def _batch(occurrence: AdmissionOccurrence) -> AdmissionBatch: manifest = CanonicalRepresentationManifest( request_id=request_id, representation_revision=occurrence.representation_revision, + representation_binding_id=RepresentationBindingId(f"binding-{slot}"), span_owners=tuple( CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) for span_id in occurrence.owned_span_ids @@ -228,6 +230,7 @@ def _multi_batch(occurrences: tuple[AdmissionOccurrence, ...]) -> AdmissionBatch manifest = CanonicalRepresentationManifest( request_id=request_id, representation_revision=occurrences[0].representation_revision, + representation_binding_id=RepresentationBindingId("binding-multi-" + "-".join(slots)), span_owners=tuple( CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) for occurrence in occurrences @@ -350,6 +353,7 @@ def _witness( request_id=batch.request_id, batch_id=batch.batch_id, representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, occurrence_ids=occurrence_ids or batch.occurrence_ids, authority_source_id=AuthoritySourceId("authority-state-machine"), ) @@ -361,6 +365,7 @@ def _binding(batch: AdmissionBatch) -> RepresentationBindingWitness: counted_representation_revision=bound_revision, dispatched_representation_revision=bound_revision, final_manifest_revision=bound_revision, + representation_binding_id=batch.manifest.representation_binding_id, request_id=batch.request_id, batch_id=batch.batch_id, authority_source_id=AuthoritySourceId("authority-state-machine"), @@ -422,7 +427,9 @@ def _accept_publication(self, transition: Any, event: object) -> None: self.state = transition.next_state self.last_revision = self.state.aggregate_revision.value self.last_admission_sequence = self.state.admission_sequence.value - self.latest_replayable_event = event + event_id = getattr(event, "event_id") + if any(record.event_id == event_id for record in self.state.processed_events): + self.latest_replayable_event = event self.events.append(event) def _availability(self, reserve_class: ReserveClass) -> tuple[int, int, int]: @@ -502,6 +509,7 @@ def reserve(self, slot: int, input_count: int, generation_count: int) -> None: generation_count, ) else: + assert transition.next_state == prior self._accept_publication(transition, event) @rule() @@ -520,6 +528,7 @@ def stale_fence_is_rejected(self) -> None: request_id=AdmissionRequestId("stale-rollover-request"), batch_id=AdmissionBatchId("stale-rollover-batch"), representation_revision=RepresentationRevision("stale-rollover-revision"), + representation_binding_id=RepresentationBindingId("stale-rollover-binding"), occurrence_ids=(), authority_source_id=receiver_authority, ), @@ -548,8 +557,10 @@ def stale_fence_is_rejected(self) -> None: ), protected_pools=_pool_specs(), ) - transition = reduce_context_admission(self.state, event) + prior = self.state + transition = reduce_context_admission(prior, event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert transition.next_state == prior self._accept_publication(transition, event) @rule( @@ -603,6 +614,7 @@ def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: **self._fields("prepare-batch"), batch_id=batch.batch_id, representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=reserved_input_count, measurement_kind=MeasurementKind.PROVIDER_EXACT, authority_source_id=AuthoritySourceId("authority-state-machine"), @@ -610,6 +622,16 @@ def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: transition = reduce_context_admission(self.state, prepare_event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT self._accept_publication(transition, prepare_event) + skipped_dispatch = DispatchRequestEvent( + **self._fields("dispatch-without-history-stage"), + batch_id=batch.batch_id, + witness=_witness(batch, occurrence, WitnessKind.REQUEST_INCLUDED), + ) + prior = self.state + rejected = reduce_context_admission(prior, skipped_dispatch) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.next_state == prior + self._accept_publication(rejected, skipped_dispatch) stage_event = StageHistoryEvent( **self._fields("stage-history"), batch_id=batch.batch_id, @@ -618,6 +640,25 @@ def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: transition = reduce_context_admission(self.state, stage_event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT self._accept_publication(transition, stage_event) + generation = next( + ( + record + for record in self.state.generation_reservations + if record.batch_id == batch.batch_id + ), + None, + ) + if generation is not None: + premature_generation = StartGenerationEvent( + **self._fields("generation-before-dispatch"), + generation_reservation_id=generation.generation_reservation_id, + witness=_witness(batch, occurrence, WitnessKind.REQUEST_INCLUDED), + ) + prior = self.state + rejected = reduce_context_admission(prior, premature_generation) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.next_state == prior + self._accept_publication(rejected, premature_generation) dispatch_event = DispatchRequestEvent( **self._fields("dispatch-request"), batch_id=batch.batch_id, @@ -658,6 +699,7 @@ def prepare_and_mark_indeterminate(self) -> None: **self._fields("prepare-batch"), batch_id=batch.batch_id, representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=input_count, measurement_kind=MeasurementKind.PROVIDER_EXACT, authority_source_id=AuthoritySourceId("authority-state-machine"), @@ -916,6 +958,7 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: request_id=AdmissionRequestId("rollover-request"), batch_id=AdmissionBatchId("rollover-batch"), representation_revision=RepresentationRevision("rollover-revision"), + representation_binding_id=RepresentationBindingId("rollover-binding"), occurrence_ids=(), authority_source_id=AuthoritySourceId("authority-state-machine"), ), diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index edd162d2d..e2483089b 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -72,6 +72,7 @@ ProducerSurface, ProtectedPoolOwnerId, ProtectedPoolSpec, + RepresentationBindingId, RepresentationBindingWitness, RepresentationRevision, ReservationRecordedEffect, @@ -240,6 +241,7 @@ + ( "batch_id", "representation_revision", + "representation_binding_id", "proposed_charge", "measurement_kind", "authority_source_id", @@ -330,6 +332,7 @@ CanonicalRepresentationManifest: ( "request_id", "representation_revision", + "representation_binding_id", "span_owners", "assembler_identity", "assembler_witness_id", @@ -381,6 +384,7 @@ "request_id", "batch_id", "representation_revision", + "representation_binding_id", "occurrence_ids", "authority_source_id", ), @@ -388,6 +392,7 @@ "counted_representation_revision", "dispatched_representation_revision", "final_manifest_revision", + "representation_binding_id", "request_id", "batch_id", "authority_source_id", @@ -421,6 +426,7 @@ "batch", "state", "reservation_id", + "protected_release_witness_kind", "witness_ids", "prepared_input_count", "committed_input_count", @@ -526,6 +532,7 @@ GenerationReservationId, ProtectedPoolOwnerId, RepresentationRevision, + RepresentationBindingId, ) @@ -584,6 +591,7 @@ def _manifest(*occurrences: AdmissionOccurrence) -> CanonicalRepresentationManif return CanonicalRepresentationManifest( request_id=AdmissionRequestId("request-1"), representation_revision=RepresentationRevision("representation-1"), + representation_binding_id=RepresentationBindingId("binding-1"), span_owners=tuple( CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) for occurrence in occurrences @@ -624,6 +632,7 @@ def _witness( request_id=batch.request_id, batch_id=batch.batch_id, representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, occurrence_ids=batch.occurrence_ids, authority_source_id=AuthoritySourceId("authority-test"), ) @@ -635,6 +644,7 @@ def _binding(batch: AdmissionBatch) -> RepresentationBindingWitness: counted_representation_revision=bound_revision, dispatched_representation_revision=bound_revision, final_manifest_revision=bound_revision, + representation_binding_id=batch.manifest.representation_binding_id, request_id=batch.request_id, batch_id=batch.batch_id, authority_source_id=AuthoritySourceId("authority-test"), @@ -811,18 +821,90 @@ def test_numeric_revisions_are_non_negative_and_have_no_default( def test_dispatch_identity_is_validated_and_projects_only_dispatch_id() -> None: - identity = DispatchIdentity.from_dispatch_id("dispatch-12345678") + identity = DispatchIdentity.from_dispatch_id("12345678-1234-1234-1234-123456789abc") lineage = _lineage(surface=ProducerSurface.NATIVE_SHELL, dispatch_identity=identity) serialized = lineage.to_dict() assert serialized["dispatch_identity"] == {"dispatch_id": identity.dispatch_id} assert ContextLineage.from_dict(serialized) == lineage - forged = replace(identity, completion_marker="forged-private-marker") + forged = object.__new__(DispatchIdentity) + for identity_field in fields(identity): + object.__setattr__(forged, identity_field.name, getattr(identity, identity_field.name)) + object.__setattr__(forged, "completion_marker", "forged-private-marker") with pytest.raises(ContextAdmissionValidationError) as exc_info: replace(lineage, dispatch_identity=forged) assert "forged-private-marker" not in str(exc_info.value) +@pytest.mark.parametrize( + "canary", + [ + "", + "/home/alice/private/context.txt", + "Bearer-secret-token", + "dispatch-private-user-content", + f"sha256:{'a' * 64}", + ], +) +def test_dispatch_identity_rejects_non_uuid_content_without_echoing_it(canary: str) -> None: + with pytest.raises(ValueError) as exc_info: + DispatchIdentity.from_dispatch_id(canary) + if canary: + assert canary not in str(exc_info.value) + assert canary not in repr(exc_info.value) + + +def test_annotated_protocol_fields_reject_raw_values_and_invalid_effect_members() -> None: + occurrence = _occurrence() + batch = _batch("batch-typed-fields", (occurrence,)) + witness = _witness(batch, WitnessKind.PROVIDER_ACCEPTED) + decision = AdmissionDecision( + kind=AdmissionDecisionKind.WOULD_ADMIT, + reason_code="accepted", + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + requested_count=1, + available_ordinary_count=1, + available_protected_count=0, + ) + transition = AdmissionTransition( + next_state=_uninitialized(), + decision=decision, + effects=(), + ) + malformed_fields = ( + (decision, "kind", "would_admit"), + (occurrence, "reserve_class", "ordinary"), + (batch, "batch_id", "batch-raw"), + (witness, "kind", "provider_accepted"), + (witness, "occurrence_ids", ("occurrence-raw",)), + (batch.manifest, "span_owners", ("span-owner-raw",)), + (transition, "effects", ("effect-raw",)), + ) + for value, field_name, malformed in malformed_fields: + with pytest.raises(ContextAdmissionValidationError): + replace(value, **{field_name: malformed}) + + serialized_decision = decision.to_dict() + serialized_decision["kind"] = "unknown-decision" + with pytest.raises(ContextAdmissionValidationError): + AdmissionDecision.from_dict(serialized_decision) + + +@pytest.mark.parametrize( + "canary", + [ + "/srv/private/representation", + "Bearer-private-capability", + f"sha256:{'a' * 64}", + ], +) +def test_representation_binding_identity_is_opaque_and_content_free(canary: str) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + RepresentationBindingId(canary) + assert canary not in str(exc_info.value) + + def test_authoritative_snapshot_reuses_only_known_model_identity() -> None: snapshot = _snapshot() assert ContextWindowSnapshot.from_dict(snapshot.to_dict()) == snapshot @@ -980,6 +1062,7 @@ def test_aggregate_tuples_reject_noncanonical_ordering() -> None: CanonicalRepresentationManifest( request_id=AdmissionRequestId("request-1"), representation_revision=RepresentationRevision("representation-1"), + representation_binding_id=RepresentationBindingId("binding-overlap"), span_owners=( CanonicalSpanOwner( span_id=second.owned_span_ids[0], @@ -1064,6 +1147,7 @@ def test_active_state_rejects_inconsistent_bidirectional_ownership_links() -> No batch=batch, state=AdmissionState.RESERVED, reservation_id=None, + protected_release_witness_kind=None, witness_ids=(), prepared_input_count=None, committed_input_count=0, @@ -1174,7 +1258,7 @@ def test_closed_epoch_audits_survive_state_serialization() -> None: def test_dispatch_identity_is_rejected_on_non_dispatch_surfaces( non_dispatch_surface: ProducerSurface, ) -> None: - identity = DispatchIdentity.from_dispatch_id("dispatch-12345678") + identity = DispatchIdentity.from_dispatch_id("12345678-1234-1234-1234-123456789abc") with pytest.raises(ContextAdmissionValidationError) as exc_info: _lineage(surface=non_dispatch_surface, dispatch_identity=identity) assert "dispatch_identity_on_non_dispatch_surface" in str(exc_info.value) @@ -1206,6 +1290,7 @@ def test_batch_record_rejects_committed_and_unresolved_simultaneously() -> None: batch=batch, state=AdmissionState.INDETERMINATE, reservation_id=None, + protected_release_witness_kind=None, witness_ids=(), prepared_input_count=None, committed_input_count=5, @@ -1255,6 +1340,7 @@ def test_prepare_event_rejects_estimate_measurement() -> None: expected_aggregate_revision=AggregateRevision(0), batch_id=batch.batch_id, representation_revision=batch.manifest.representation_revision, + representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=5, measurement_kind=MeasurementKind.HOST_ESTIMATE, authority_source_id=AuthoritySourceId("authority-test"), From 797230b2078f28b0cda9747507f17b1ad8ac08b4 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 06:01:37 -0700 Subject: [PATCH 07/62] fix(core): satisfy context admission implementation audit --- src/autoskillit/core/context_admission.py | 377 +++++++++--------- .../core/types/_type_context_admission.py | 62 +-- src/autoskillit/core/types/_type_enums.py | 1 - tests/core/test_context_admission_reducer.py | 274 ++++++++++--- .../test_context_admission_state_machine.py | 49 +-- .../types/test_context_admission_contract.py | 43 +- 6 files changed, 475 insertions(+), 331 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index a979b27d9..c048f9453 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -321,20 +321,40 @@ def _reject( event: ContextAdmissionEvent, reason_code: str, *, + kind: AdmissionDecisionKind = AdmissionDecisionKind.WOULD_REJECT, requested_count: int = 0, reserve_class: ReserveClass = ReserveClass.ORDINARY, protected_pool_owner_id: ProtectedPoolOwnerId | None = None, ) -> AdmissionTransition: decision = _decision( state, - AdmissionDecisionKind.WOULD_REJECT, + kind, reason_code, requested_count=requested_count, reserve_class=reserve_class, protected_pool_owner_id=protected_pool_owner_id, ) + processed = ProcessedEventRecord( + event_id=event.event_id, + event=event, + original_decision=decision, + aggregate_revision=state.aggregate_revision, + admission_sequence=state.admission_sequence, + ) + next_state = replace( + state, + processed_events=tuple( + sorted( + state.processed_events + (processed,), + key=lambda record: ( + record.aggregate_revision.value, + record.event_id.value, + ), + ) + ), + ) return AdmissionTransition( - next_state=state, + next_state=next_state, decision=decision, effects=(), ) @@ -506,14 +526,52 @@ def _batch_record( def _highest_dispatch_sequence(state: ActiveContextAdmissionState) -> int: - return max( + return sum( + isinstance(record.event, DispatchRequestEvent) + and record.original_decision.kind is AdmissionDecisionKind.WOULD_ADMIT + and record.event.witness.window_epoch_id == state.snapshot.window_epoch_id + and record.event.witness.window_epoch_number == state.snapshot.window_epoch_number + for record in state.processed_events + ) + + +def _required_release_witness_kind( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + *, + snapshot: ContextWindowSnapshot | None = None, +) -> WitnessKind | None: + if batch.protected_pool_owner_id is None: + return None + policy_snapshot = snapshot or state.snapshot + if ( + policy_snapshot.window_epoch_id == state.snapshot.window_epoch_id + and policy_snapshot.window_epoch_number == state.snapshot.window_epoch_number + ): + protected_pools = state.protected_pools + else: + open_event = next( + ( + record.event + for record in state.processed_events + if isinstance(record.event, OpenEpochEvent) + and record.event.snapshot.window_epoch_id == policy_snapshot.window_epoch_id + and record.event.snapshot.window_epoch_number + == policy_snapshot.window_epoch_number + ), + None, + ) + protected_pools = open_event.protected_pools if open_event is not None else () + pool = next( ( - record.dispatch_sequence - for record in state.batch_records - if record.dispatch_sequence is not None + item + for item in protected_pools + if item.reserve_class is batch.reserve_class + and item.capability_owner_id == batch.protected_pool_owner_id ), - default=0, + None, ) + return pool.required_release_witness_kind if pool is not None else None def _append_witness_ids( @@ -778,13 +836,6 @@ def _propose( occurrence.producer_surface is ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY and lineage.delivery_occurrence_id is not None ) - if ( - occurrence.lineage.window_epoch_id != state.snapshot.window_epoch_id - or occurrence.lineage.window_epoch_number != state.snapshot.window_epoch_number - ): - return _reject(state, event, "occurrence_epoch_mismatch") - if is_fork_work and not is_parent_delivery: - return _reject(state, event, "fork_requires_distinct_epoch") existing = next( ( record @@ -793,20 +844,40 @@ def _propose( ), None, ) - if existing is not None: - kind = ( - AdmissionDecisionKind.NOOP_IDEMPOTENT - if existing.occurrence == occurrence - else AdmissionDecisionKind.CONFLICT - ) - reason = ( - "occurrence_replay" if existing.occurrence == occurrence else "occurrence_conflict" + if existing is None: + existing = next( + ( + record + for audit in state.closed_epochs + for record in audit.terminal_occurrence_records + if record.occurrence.occurrence_id == occurrence.occurrence_id + ), + None, ) - return AdmissionTransition( - next_state=state, - decision=_decision(state, kind, reason), - effects=(), + if existing is not None: + if existing.occurrence == occurrence: + return AdmissionTransition( + next_state=state, + decision=_decision( + state, + AdmissionDecisionKind.NOOP_IDEMPOTENT, + "occurrence_replay", + ), + effects=(), + ) + return _reject( + state, + event, + "occurrence_identity_corruption", + kind=AdmissionDecisionKind.QUARANTINED, ) + if ( + occurrence.lineage.window_epoch_id != state.snapshot.window_epoch_id + or occurrence.lineage.window_epoch_number != state.snapshot.window_epoch_number + ): + return _reject(state, event, "occurrence_epoch_mismatch") + if is_fork_work and not is_parent_delivery: + return _reject(state, event, "fork_requires_distinct_epoch") record = AdmissionOccurrenceRecord( occurrence=occurrence, state=AdmissionState.PROPOSED, @@ -936,7 +1007,6 @@ def _reserve( reserve_class=event.batch.reserve_class, protected_pool_owner_id=event.batch.protected_pool_owner_id, ) - protected_release_witness_kind = None if event.batch.protected_pool_owner_id is not None: pool = next( ( @@ -949,17 +1019,13 @@ def _reserve( ) if pool is None: return _reject(state, event, "unknown_protected_pool") - protected_release_witness_kind = pool.required_release_witness_kind batch_record = AdmissionBatchRecord( batch=event.batch, state=AdmissionState.RESERVED, reservation_id=reservation.reservation_id, - protected_release_witness_kind=protected_release_witness_kind, witness_ids=(), - prepared_input_count=None, committed_input_count=0, unresolved_input_count=0, - dispatch_sequence=None, ) member_ids = set(event.batch.occurrence_ids) reserved_occurrence_records = tuple( @@ -1104,7 +1170,6 @@ def _prepare( updated = replace( record, state=AdmissionState.PREPARED, - prepared_input_count=event.proposed_charge, ) next_state = _replace_batch_record(state, updated) next_state = _set_occurrence_state( @@ -1196,7 +1261,6 @@ def _dispatch( updated = replace( record, state=AdmissionState.REQUEST_DISPATCHED, - dispatch_sequence=_highest_dispatch_sequence(state) + 1, witness_ids=_append_witness_ids( record.witness_ids, event.witness.witness_id, @@ -1294,8 +1358,8 @@ def _accept_closed_input( or binding.representation_binding_id != record.batch.manifest.representation_binding_id or binding.request_id != record.batch.request_id or binding.batch_id != record.batch.batch_id - or event.authority_source_id != event.witness.authority_source_id - or event.authority_source_id != binding.authority_source_id + or event.authority_source != event.witness.authority_source_id + or event.authority_source != binding.authority_source_id ): return _reject(state, event, "invalid_closed_epoch_acceptance") reservation = _closed_reservation_for(audit, record) @@ -1472,8 +1536,8 @@ def _accept( (owner.span_id, owner.occurrence_id) for owner in event.final_manifest.span_owners ) if ( - event.authority_source_id != event.witness.authority_source_id - or event.authority_source_id != binding.authority_source_id + event.authority_source != event.witness.authority_source_id + or event.authority_source != binding.authority_source_id ): reason_code = "authority_source_mismatch" next_state = _quarantined_acceptance_state( @@ -1583,19 +1647,16 @@ def _release_closed_batch( AdmissionState.INDETERMINATE if is_resolution else AdmissionState.REQUEST_DISPATCHED ) expected_kind = WitnessKind.NON_ADMISSION if is_release else WitnessKind.ROLLBACK - history_removal_witness = ( - event.history_removal_witness - if isinstance( - event, - ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, - ) - else None + required_release_kind = _required_release_witness_kind( + state, + record.batch, + snapshot=audit.snapshot, ) if ( record.state is not expected_state or ( - record.protected_release_witness_kind is not None - and record.protected_release_witness_kind is not expected_kind + record.batch.protected_pool_owner_id is not None + and required_release_kind is not expected_kind ) or not _validate_witness_for_snapshot( audit.snapshot, @@ -1603,26 +1664,12 @@ def _release_closed_batch( event.witness, expected_kind, ) - or ( - is_release - and ( - history_removal_witness is None - or not _validate_witness_for_snapshot( - audit.snapshot, - record.batch, - history_removal_witness, - WitnessKind.ROLLBACK, - ) - or history_removal_witness.witness_id == event.witness.witness_id - ) - ) ): return _reject(state, event, "invalid_closed_epoch_resolution") lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK witness_ids = _append_witness_ids( record.witness_ids, event.witness.witness_id, - *((history_removal_witness.witness_id,) if history_removal_witness is not None else ()), ) updated_record = replace( record, @@ -1642,11 +1689,6 @@ def _release_closed_batch( accepted_witness_ids=_append_witness_ids( item.accepted_witness_ids, event.witness.witness_id, - *( - (history_removal_witness.witness_id,) - if history_removal_witness is not None - else () - ), ), indeterminate_reason_code=None, ) @@ -1687,14 +1729,7 @@ def _release_closed_batch( count=reservation.reserved_count, window_epoch_id=reservation.window_epoch_id, snapshot_sequence=reservation.snapshot_sequence, - witness_ids=_append_witness_ids( - (event.witness.witness_id,), - *( - (history_removal_witness.witness_id,) - if history_removal_witness is not None - else () - ), - ), + witness_ids=(event.witness.witness_id,), ), *effects, ) @@ -1707,16 +1742,6 @@ def _release_closed_batch( GenerationState.STREAMING, GenerationState.INDETERMINATE, }: - generation_records.append( - replace( - generation, - state=GenerationState.INVALIDATED, - witness_ids=_append_witness_ids( - generation.witness_ids, - event.witness.witness_id, - ), - ) - ) generation_effects += ( ReservationInvalidatedEffect( source_event_id=event.event_id, @@ -1785,7 +1810,6 @@ def _release_or_rollback( event, ResolveIndeterminateNonAdmissionEvent | ResolveIndeterminateRollbackEvent, ) - expected_witness = WitnessKind.NON_ADMISSION if is_release else WitnessKind.ROLLBACK if is_resolution: allowed_states = {AdmissionState.INDETERMINATE} elif is_release: @@ -1800,9 +1824,11 @@ def _release_or_rollback( AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED, } + expected_witness = WitnessKind.NON_ADMISSION if is_release else WitnessKind.ROLLBACK + required_release_kind = _required_release_witness_kind(state, record.batch) if ( - record.protected_release_witness_kind is not None - and record.protected_release_witness_kind is not expected_witness + record.batch.protected_pool_owner_id is not None + and required_release_kind is not expected_witness ): return _reject(state, event, "protected_release_policy_mismatch") if record.state not in allowed_states or not _validate_witness( @@ -1812,34 +1838,6 @@ def _release_or_rollback( expected_witness, ): return _reject(state, event, "invalid_release_or_rollback_witness") - history_removal_witness = ( - event.history_removal_witness - if isinstance( - event, - ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, - ) - else None - ) - if ( - is_release - and record.state - in { - AdmissionState.HISTORY_STAGED, - AdmissionState.REQUEST_DISPATCHED, - AdmissionState.INDETERMINATE, - } - and ( - history_removal_witness is None - or not _validate_witness( - state, - record.batch, - history_removal_witness, - WitnessKind.ROLLBACK, - ) - or history_removal_witness.witness_id == event.witness.witness_id - ) - ): - return _reject(state, event, "staged_release_requires_history_removal") lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK updated = replace( record, @@ -1847,11 +1845,6 @@ def _release_or_rollback( witness_ids=_append_witness_ids( record.witness_ids, event.witness.witness_id, - *( - (history_removal_witness.witness_id,) - if history_removal_witness is not None - else () - ), ), unresolved_input_count=0, ) @@ -1862,13 +1855,6 @@ def _release_or_rollback( lifecycle, witness=event.witness, ) - if history_removal_witness is not None: - next_state = _set_occurrence_state( - next_state, - record.batch, - lifecycle, - witness=history_removal_witness, - ) effects: tuple[AdmissionEffect, ...] = _occurrence_effects( state, event, @@ -1892,14 +1878,7 @@ def _release_or_rollback( count=reservation.reserved_count, window_epoch_id=reservation.window_epoch_id, snapshot_sequence=reservation.snapshot_sequence, - witness_ids=_append_witness_ids( - (event.witness.witness_id,), - *( - (history_removal_witness.witness_id,) - if history_removal_witness is not None - else () - ), - ), + witness_ids=(event.witness.witness_id,), ), *effects, ) @@ -1912,16 +1891,6 @@ def _release_or_rollback( GenerationState.STREAMING, GenerationState.INDETERMINATE, }: - generation_records.append( - replace( - generation, - state=GenerationState.INVALIDATED, - witness_ids=_append_witness_ids( - generation.witness_ids, - event.witness.witness_id, - ), - ) - ) generation_effects += ( ReservationInvalidatedEffect( source_event_id=event.event_id, @@ -2020,8 +1989,8 @@ def _resolve_indeterminate_accepted( event.witness, WitnessKind.PROVIDER_ACCEPTED, ) - or event.authority_source_id != event.witness.authority_source_id - or event.authority_source_id != binding.authority_source_id + or event.authority_source != event.witness.authority_source_id + or event.authority_source != binding.authority_source_id or event.measurement_kind is not MeasurementKind.PROVIDER_EXACT or event.final_manifest_revision != expected_revision or event.final_manifest != record.batch.manifest @@ -2476,26 +2445,69 @@ def _expire_idempotency( ) if record is None: return _reject(state, event, "idempotency_key_not_terminal") - if isinstance(state, ActiveContextAdmissionState): + if not isinstance(state, ActiveContextAdmissionState): + return _reject(state, event, "idempotency_key_not_terminal") + batch_record: AdmissionBatchRecord | None + snapshot: ContextWindowSnapshot + generation_records: tuple[GenerationReservationRecord, ...] + if ( + event.reservation_key.window_epoch_id == state.snapshot.window_epoch_id + and event.reservation_key.window_epoch_number == state.snapshot.window_epoch_number + ): batch_record = _batch_record( state, record.original_descriptor.batch.batch_id, ) - if batch_record is not None and batch_record.state not in { - AdmissionState.COMMITTED, - AdmissionState.RELEASED, - AdmissionState.ROLLED_BACK, - AdmissionState.INVALIDATED, - AdmissionState.QUARANTINED, - }: + snapshot = state.snapshot + generation_records = state.generation_reservations + else: + audit = next( + ( + item + for item in state.closed_epochs + if item.snapshot.window_epoch_id == event.reservation_key.window_epoch_id + and item.snapshot.window_epoch_number == event.reservation_key.window_epoch_number + ), + None, + ) + if audit is None: return _reject(state, event, "idempotency_key_not_terminal") - if event.expiry_witness.kind is not WitnessKind.IDEMPOTENCY_EXPIRY: - return _reject(state, event, "invalid_expiry_witness") - if ( - event.expiry_witness.window_epoch_id != event.reservation_key.window_epoch_id - or event.expiry_witness.window_epoch_number != event.reservation_key.window_epoch_number + batch_record = next( + ( + item + for item in audit.terminal_batch_records + if item.batch.batch_id == record.original_descriptor.batch.batch_id + ), + None, + ) + snapshot = audit.snapshot + generation_records = audit.terminal_generation_reservations + if batch_record is None or batch_record.state not in { + AdmissionState.COMMITTED, + AdmissionState.RELEASED, + AdmissionState.ROLLED_BACK, + AdmissionState.INVALIDATED, + AdmissionState.QUARANTINED, + }: + return _reject(state, event, "idempotency_key_not_terminal") + if any( + generation.batch_id == batch_record.batch.batch_id + and generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + for generation in generation_records ): - return _reject(state, event, "expiry_epoch_mismatch") + return _reject(state, event, "idempotency_key_not_terminal") + if not _validate_witness_for_snapshot( + snapshot, + batch_record.batch, + event.expiry_witness, + WitnessKind.IDEMPOTENCY_EXPIRY, + ): + return _reject(state, event, "invalid_expiry_witness") tombstone = ExpiredIdempotencyTombstone( namespace=record.namespace, reservation_key=record.reservation_key, @@ -2599,11 +2611,9 @@ def _rollover( and proof.receiver_authority_source_id == event.witness.authority_source_id and proof.highest_admitted_dispatch_sequence == _highest_dispatch_sequence(state) ) - fully_resolved = retained_total == 0 and event.deducted_unresolved_count == 0 + fully_resolved = retained_total == 0 snapshot_deducts_unresolved = ( - retained_total > 0 - and event.deducted_unresolved_count == retained_total - and event.new_snapshot.active_count >= retained_total + retained_total > 0 and event.new_snapshot.active_count >= retained_total ) authority_alternative_valid = ( receiver_fence_valid @@ -2645,11 +2655,9 @@ def _rollover( ) for record in state.batch_records ) - terminal_generation_reservations = tuple( - replace( - generation, - state=GenerationState.INVALIDATED, - ) + invalidated_generation_reservations = tuple( + generation + for generation in state.generation_reservations if generation.state in { GenerationState.RESERVED, @@ -2657,8 +2665,14 @@ def _rollover( GenerationState.INDETERMINATE, } and generation.request_id not in retained_generation_request_ids - else generation + ) + invalidated_generation_ids = { + generation.generation_reservation_id for generation in invalidated_generation_reservations + } + terminal_generation_reservations = tuple( + generation for generation in state.generation_reservations + if generation.generation_reservation_id not in invalidated_generation_ids ) audit = ClosedEpochAudit( snapshot=state.snapshot, @@ -2735,21 +2749,16 @@ def _rollover( source_event_id=event.event_id, resulting_aggregate_revision=revision, resulting_admission_sequence=sequence, - target_id=prior.generation_reservation_id, + target_id=generation.generation_reservation_id, charge_domain=ChargeDomain.OUTPUT_GENERATION, - reserve_class=prior.reserve_class, - protected_pool_owner_id=prior.protected_pool_owner_id, - count=prior.maximum_allowance, - window_epoch_id=prior.window_epoch_id, - snapshot_sequence=prior.snapshot_sequence, + reserve_class=generation.reserve_class, + protected_pool_owner_id=generation.protected_pool_owner_id, + count=generation.maximum_allowance, + window_epoch_id=generation.window_epoch_id, + snapshot_sequence=generation.snapshot_sequence, witness_ids=rollover_witness_ids, ) - for prior, terminal in zip( - state.generation_reservations, - terminal_generation_reservations, - strict=True, - ) - if prior.state is not terminal.state + for generation in invalidated_generation_reservations ) occurrence_effects = tuple( OccurrenceStateChangedEffect( @@ -2777,7 +2786,9 @@ def _rollover( resulting_admission_sequence=sequence, target_id=state.snapshot.window_epoch_id, fence_proof=proof, - deducted_unresolved_count=event.deducted_unresolved_count, + deducted_unresolved_count=( + retained_total if snapshot_deducts_unresolved and proof is None else 0 + ), ), ) return _publish( diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 40ee49661..f1a228143 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -136,7 +136,17 @@ def _validate_canonical_tuple( def _encode(value: object) -> object: if isinstance(value, DispatchIdentity): - return {"dispatch_id": value.dispatch_id} + try: + validated = DispatchIdentity( + dispatch_id=value.dispatch_id, + completion_marker=value.completion_marker, + sentinel_open=value.sentinel_open, + sentinel_close=value.sentinel_close, + sentinel_contract=value.sentinel_contract, + ) + except ValueError: + _raise_invalid("invalid_dispatch_identity") + return {"dispatch_id": validated.dispatch_id} if isinstance(value, ModelIdentity): return { "__type__": "ModelIdentity", @@ -313,7 +323,10 @@ class _OpaqueString(_ContractValue): def __post_init__(self) -> None: _validate_bounded_text(self.value, "invalid_opaque_identifier") allowed = "-_.:" - if any( + if ( + len(self.value) in {40, 64, 128} + and all(character in "0123456789abcdefABCDEF" for character in self.value) + ) or any( not (character.isascii() and (character.isalnum() or character in allowed)) for character in self.value ): @@ -823,18 +836,11 @@ class AdmissionBatchRecord(_ContractValue): batch: AdmissionBatch state: AdmissionState reservation_id: AdmissionReservationId | None - protected_release_witness_kind: WitnessKind | None witness_ids: tuple[AdmissionWitnessId, ...] - prepared_input_count: int | None committed_input_count: int unresolved_input_count: int - dispatch_sequence: int | None def __post_init__(self) -> None: - if (self.batch.reserve_class is ReserveClass.ORDINARY) != ( - self.protected_release_witness_kind is None - ): - _raise_invalid("invalid_protected_release_policy") _validate_canonical_tuple( self.witness_ids, "noncanonical_witness_ids", @@ -842,12 +848,8 @@ def __post_init__(self) -> None: ) if len(self.witness_ids) != len(set(self.witness_ids)): _raise_invalid("duplicate_witness_id") - if self.prepared_input_count is not None: - _validate_non_negative(self.prepared_input_count, "invalid_prepared_count") _validate_non_negative(self.committed_input_count, "invalid_committed_count") _validate_non_negative(self.unresolved_input_count, "invalid_unresolved_count") - if self.dispatch_sequence is not None: - _validate_non_negative(self.dispatch_sequence, "invalid_dispatch_sequence") if self.committed_input_count > 0 and self.unresolved_input_count > 0: _raise_invalid("committed_and_unresolved_simultaneously") @@ -1201,7 +1203,7 @@ class PrepareBatchEvent(_AdmissionEventBase): representation_binding_id: RepresentationBindingId proposed_charge: int measurement_kind: MeasurementKind - authority_source_id: AuthoritySourceId + authority_source: AuthoritySourceId def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) @@ -1233,7 +1235,7 @@ class AcceptInputEvent(_AdmissionEventBase): final_manifest: CanonicalRepresentationManifest exact_input_charge: int measurement_kind: MeasurementKind - authority_source_id: AuthoritySourceId + authority_source: AuthoritySourceId representation_binding_witness: RepresentationBindingWitness def __post_init__(self) -> None: @@ -1247,15 +1249,6 @@ def __post_init__(self) -> None: class ReleaseNonAdmissionEvent(_AdmissionEventBase): batch_id: AdmissionBatchId witness: AdmissionWitness - history_removal_witness: AdmissionWitness | None - - def __post_init__(self) -> None: - _AdmissionEventBase.__post_init__(self) - if ( - self.history_removal_witness is not None - and self.history_removal_witness.kind is not WitnessKind.ROLLBACK - ): - _raise_invalid("invalid_history_removal_witness") @dataclass(frozen=True, slots=True) @@ -1282,7 +1275,7 @@ class ResolveIndeterminateAcceptedEvent(_AdmissionEventBase): final_manifest: CanonicalRepresentationManifest exact_charge: int measurement_kind: MeasurementKind - authority_source_id: AuthoritySourceId + authority_source: AuthoritySourceId representation_binding_witness: RepresentationBindingWitness def __post_init__(self) -> None: @@ -1296,15 +1289,6 @@ def __post_init__(self) -> None: class ResolveIndeterminateNonAdmissionEvent(_AdmissionEventBase): batch_id: AdmissionBatchId witness: AdmissionWitness - history_removal_witness: AdmissionWitness | None - - def __post_init__(self) -> None: - _AdmissionEventBase.__post_init__(self) - if ( - self.history_removal_witness is not None - and self.history_removal_witness.kind is not WitnessKind.ROLLBACK - ): - _raise_invalid("invalid_history_removal_witness") @dataclass(frozen=True, slots=True) @@ -1360,16 +1344,11 @@ class ExpireIdempotencyKeyEvent(_AdmissionEventBase): class RolloverEpochEvent(_AdmissionEventBase): witness: AdmissionWitness fence_proof: EpochFenceProof | None - deducted_unresolved_count: int new_snapshot: ContextWindowSnapshot protected_pools: tuple[ProtectedPoolSpec, ...] def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) - _validate_non_negative( - self.deducted_unresolved_count, - "invalid_deducted_unresolved_count", - ) _validate_canonical_tuple( self.protected_pools, "noncanonical_protected_pools", @@ -1996,11 +1975,6 @@ def __post_init__(self) -> None: key = (record.batch.reserve_class, owner) if key not in pools_by_key: _raise_invalid("orphan_protected_charge_owner") - if ( - record.protected_release_witness_kind - is not pools_by_key[key].required_release_witness_kind - ): - _raise_invalid("protected_release_policy_mismatch") if record.state is AdmissionState.INDETERMINATE: charge = record.unresolved_input_count if charge == 0 and matched_reservation is not None: diff --git a/src/autoskillit/core/types/_type_enums.py b/src/autoskillit/core/types/_type_enums.py index b94124d4a..fdd9cb248 100644 --- a/src/autoskillit/core/types/_type_enums.py +++ b/src/autoskillit/core/types/_type_enums.py @@ -678,7 +678,6 @@ class GenerationState(StrEnum): RECONCILED = "reconciled" INDETERMINATE = "indeterminate" QUARANTINED = "quarantined" - INVALIDATED = "invalidated" @unique diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 34f814900..76fe5a4c4 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -434,7 +434,8 @@ def _assert_rejection_unchanged( before: ActiveContextAdmissionState, after: ActiveContextAdmissionState, ) -> None: - assert after == before + assert len(after.processed_events) == len(before.processed_events) + 1 + assert replace(after, processed_events=before.processed_events) == before def _generation_record( @@ -598,7 +599,7 @@ def _prepare_dispatch( representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=_input_reservation(state, batch_record.reservation_id).reserved_count, measurement_kind=MeasurementKind.TOKENIZER_EXACT, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), ) prepared = reduce_context_admission(state, prepare) assert prepared.decision.kind is AdmissionDecisionKind.WOULD_ADMIT @@ -630,6 +631,46 @@ def _prepare_dispatch( return dispatched.next_state +def _rollover_with_receiver_fence( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + *, + name: str, +) -> ActiveContextAdmissionState: + receiver = AuthoritySourceId(f"receiver-{name}") + next_epoch_number = state.snapshot.window_epoch_number + 1 + next_epoch_id = WindowEpochId(f"epoch-{next_epoch_number}") + dispatch_count = sum( + isinstance(record.event, DispatchRequestEvent) + and record.original_decision.kind is AdmissionDecisionKind.WOULD_ADMIT + and record.event.witness.window_epoch_id == state.snapshot.window_epoch_id + and record.event.witness.window_epoch_number == state.snapshot.window_epoch_number + for record in state.processed_events + ) + event = RolloverEpochEvent( + **_event_fields(state, f"rollover-{name}", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=next_epoch_id, + new_window_epoch_number=next_epoch_number, + receiver_authority_source_id=receiver, + fence_witness_id=AdmissionWitnessId(f"fence-{name}"), + highest_admitted_dispatch_sequence=dispatch_count, + ), + new_snapshot=_snapshot(epoch=next_epoch_number), + protected_pools=(), + ) + transition = reduce_context_admission(state, event) + assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(transition.next_state, ActiveContextAdmissionState) + return transition.next_state + + def test_dispatch_and_generation_cannot_skip_required_lifecycle_transitions() -> None: state, batch, _, generation = _reserved_batch(name="strict-lifecycle") batch_record = _batch_record(state, batch) @@ -641,7 +682,7 @@ def test_dispatch_and_generation_cannot_skip_required_lifecycle_transitions() -> representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=_input_reservation(state, batch_record.reservation_id).reserved_count, measurement_kind=MeasurementKind.TOKENIZER_EXACT, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), ) prepared = reduce_context_admission(state, prepare) assert isinstance(prepared.next_state, ActiveContextAdmissionState) @@ -657,7 +698,7 @@ def test_dispatch_and_generation_cannot_skip_required_lifecycle_transitions() -> ) rejected_dispatch = reduce_context_admission(prepared.next_state, skipped_stage) assert rejected_dispatch.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected_dispatch.next_state == prepared.next_state + _assert_rejection_unchanged(prepared.next_state, rejected_dispatch.next_state) stage = StageHistoryEvent( **_event_fields(prepared.next_state, "stage-strict-lifecycle", "stage-history"), @@ -680,7 +721,7 @@ def test_dispatch_and_generation_cannot_skip_required_lifecycle_transitions() -> premature_generation, ) assert rejected_generation.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected_generation.next_state == staged.next_state + _assert_rejection_unchanged(staged.next_state, rejected_generation.next_state) def test_input_commit_and_output_reconciliation_remain_distinct_domains() -> None: @@ -702,7 +743,7 @@ def test_input_commit_and_output_reconciliation_remain_distinct_domains() -> Non final_manifest=batch.manifest, exact_input_charge=18, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), representation_binding_witness=_binding(batch), ) accepted = reduce_context_admission(started.next_state, accept) @@ -833,7 +874,6 @@ def test_history_request_provider_and_rollback_witnesses_are_not_interchangeable **_event_fields(state, f"bad-{event_kind}", "release-non-admission"), batch_id=batch.batch_id, witness=_witness(batch, witness_kind), - history_removal_witness=None, ) else: event = RollbackAdmissionEvent( @@ -866,7 +906,7 @@ def test_revision_and_fence_failures_do_not_commit( representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=25, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), ) else: event = RolloverEpochEvent( @@ -881,7 +921,6 @@ def test_revision_and_fence_failures_do_not_commit( fence_witness_id=AdmissionWitnessId("stale-fence"), highest_admitted_dispatch_sequence=0, ), - deducted_unresolved_count=0, new_snapshot=_snapshot(epoch=2), protected_pools=(), ) @@ -987,7 +1026,7 @@ def test_acceptance_rejects_mutated_or_unattested_final_representation( final_manifest=final_manifest, exact_input_charge=20, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("provider-test"), + authority_source=AuthoritySourceId("provider-test"), representation_binding_witness=binding, ) rejected = reduce_context_admission(state, event) @@ -1045,7 +1084,6 @@ def test_ambiguous_crash_stays_charged_until_an_authoritative_resolution( ), batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.NON_ADMISSION), - history_removal_witness=_witness(batch, WitnessKind.ROLLBACK), ) else: resolution = ResolveIndeterminateRollbackEvent( @@ -1080,7 +1118,7 @@ def test_provider_accepted_overage_is_recorded_and_quarantined( final_manifest=batch.manifest, exact_input_charge=exact_charge, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("provider-test"), + authority_source=AuthoritySourceId("provider-test"), representation_binding_witness=_binding(batch), ) quarantined = reduce_context_admission(state, event) @@ -1118,13 +1156,13 @@ def test_incomplete_manifest_rejection_is_non_mutating_and_retains_reservation() final_manifest=incomplete_manifest, exact_input_charge=17, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), representation_binding_witness=_binding(batch), ) rejected_acceptance = reduce_context_admission(state, event) assert rejected_acceptance.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert rejected_acceptance.decision.reason_code == "representation_binding_mismatch" - assert rejected_acceptance.next_state == state + _assert_rejection_unchanged(state, rejected_acceptance.next_state) assert rejected_acceptance.effects == () next_occurrence = _occurrence("after-incomplete-manifest", maximum=24) @@ -1150,7 +1188,7 @@ def test_authority_mismatch_quarantine_publishes_charge_state_and_quarantine() - final_manifest=batch.manifest, exact_input_charge=20, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("different-authority"), + authority_source=AuthoritySourceId("different-authority"), representation_binding_witness=_binding(batch), ) quarantined = reduce_context_admission(state, event) @@ -1225,7 +1263,6 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> **_event_fields(state, "rollover-1-to-2", "rollover-epoch"), witness=rollover_witness, fence_proof=proof, - deducted_unresolved_count=0, new_snapshot=_snapshot(epoch=2), protected_pools=(), ) @@ -1255,7 +1292,6 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> **_event_fields(rolled.next_state, "rollover-2-to-3", "rollover-epoch"), witness=second_witness, fence_proof=second_proof, - deducted_unresolved_count=0, new_snapshot=_snapshot(epoch=3, model="claude-new", tokenizer="tokenizer-new"), protected_pools=(), ) @@ -1265,6 +1301,57 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> assert rerolled.next_state.closed_epochs[0] == rolled.next_state.closed_epochs[0] +def test_closed_epoch_occurrence_identity_is_immutable_and_exact_retry_is_idempotent() -> None: + state, batch, occurrences, _ = _reserved_batch(name="closed-occurrence") + rolled = _rollover_with_receiver_fence( + state, + batch, + name="closed-occurrence", + ) + occurrence = occurrences[0] + exact_retry = ProposeOccurrenceEvent( + **_event_fields( + rolled, + "retry-closed-occurrence", + "propose-occurrence", + ), + occurrence=occurrence, + ) + replayed = reduce_context_admission(rolled, exact_retry) + assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT + assert replayed.next_state == rolled + + changed_occurrences = ( + replace( + occurrence, + predicted_authoritative_maximum=occurrence.predicted_authoritative_maximum + 1, + ), + replace( + occurrence, + lineage=replace( + occurrence.lineage, + window_epoch_id=rolled.snapshot.window_epoch_id, + window_epoch_number=rolled.snapshot.window_epoch_number, + ), + ), + ) + current = rolled + for index, changed_occurrence in enumerate(changed_occurrences): + changed = ProposeOccurrenceEvent( + **_event_fields( + current, + f"reuse-closed-occurrence-{index}", + "propose-occurrence", + ), + occurrence=changed_occurrence, + ) + rejected = reduce_context_admission(current, changed) + assert rejected.decision.kind is AdmissionDecisionKind.QUARANTINED + assert rejected.decision.reason_code == "occurrence_identity_corruption" + _assert_rejection_unchanged(current, rejected.next_state) + current = rejected.next_state + + def test_rollover_rejects_stale_admitted_dispatch_fence() -> None: state, batch, _, _ = _reserved_batch(name="stale-dispatch-fence") state = _prepare_dispatch(state, batch) @@ -1285,7 +1372,6 @@ def test_rollover_rejects_stale_admitted_dispatch_fence() -> None: authority_source_id=receiver_authority, ), fence_proof=proof, - deducted_unresolved_count=0, new_snapshot=_snapshot(epoch=2), protected_pools=(), ) @@ -1369,11 +1455,6 @@ def test_protected_pool_release_policy_accepts_only_its_configured_resolution( **_event_fields(state, "resolve-protected-non-admission", "resolve-non-admission"), batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.NON_ADMISSION), - history_removal_witness=_witness( - batch, - WitnessKind.ROLLBACK, - witness="protected-history-removal", - ), ) rollback = ResolveIndeterminateRollbackEvent( **_event_fields(state, "resolve-protected-rollback", "resolve-rollback"), @@ -1389,7 +1470,7 @@ def test_protected_pool_release_policy_accepts_only_its_configured_resolution( rejected = reduce_context_admission(state, rejected_event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert rejected.decision.reason_code == "protected_release_policy_mismatch" - assert rejected.next_state == state + _assert_rejection_unchanged(state, rejected.next_state) allowed = reduce_context_admission(state, allowed_event) assert allowed.decision.kind is AdmissionDecisionKind.WOULD_ADMIT @@ -1561,7 +1642,7 @@ def test_full_stream_replay_uses_each_next_state_as_the_only_next_input() -> Non assert restored == replay -def test_rejected_event_retries_and_changed_reuse_are_non_mutating() -> None: +def test_rejected_event_retries_and_changed_reuse_have_replay_semantics() -> None: state = _open_epoch() rejected_event = PrepareBatchEvent( **_event_fields(state, "rejected-event", "prepare-batch"), @@ -1570,14 +1651,14 @@ def test_rejected_event_retries_and_changed_reuse_are_non_mutating() -> None: representation_binding_id=RepresentationBindingId("binding-unknown"), proposed_charge=1, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), ) rejected = reduce_context_admission(state, rejected_event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert isinstance(rejected.next_state, ActiveContextAdmissionState) replayed = reduce_context_admission(rejected.next_state, rejected_event) - assert replayed.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert replayed.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT assert replayed.next_state == rejected.next_state changed = replace( @@ -1585,7 +1666,7 @@ def test_rejected_event_retries_and_changed_reuse_are_non_mutating() -> None: batch_id=AdmissionBatchId("different-unknown-batch"), ) conflicted = reduce_context_admission(rejected.next_state, changed) - assert conflicted.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert conflicted.decision.kind is AdmissionDecisionKind.CONFLICT assert conflicted.next_state == rejected.next_state @@ -1602,6 +1683,108 @@ def test_idempotency_expiry_is_explicit_and_does_not_release_capacity() -> None: _assert_rejection_unchanged(state, rejected.next_state) +@pytest.mark.parametrize("mark_indeterminate", [False, True]) +def test_post_rollover_expiry_cannot_tombstone_retained_work( + mark_indeterminate: bool, +) -> None: + state, batch, _, _ = _reserved_batch(name=f"retained-expiry-{mark_indeterminate}") + state = _prepare_dispatch(state, batch) + if mark_indeterminate: + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields( + state, + "mark-retained-expiry", + "mark-indeterminate", + ), + batch_id=batch.batch_id, + reason_code="ambiguous-provider-result", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + state = marked.next_state + reservation = state.reservations[0] + rolled = _rollover_with_receiver_fence( + state, + batch, + name=f"retained-expiry-{mark_indeterminate}", + ) + retained_before = rolled.closed_epochs[-1] + expiry = ExpireIdempotencyKeyEvent( + **_event_fields( + rolled, + f"expire-retained-{mark_indeterminate}", + "expire-idempotency-key", + ), + reservation_key=reservation.key, + expiry_witness=_witness(batch, WitnessKind.IDEMPOTENCY_EXPIRY), + ) + rejected = reduce_context_admission(rolled, expiry) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.decision.reason_code == "idempotency_key_not_terminal" + assert rejected.next_state.expired_idempotency_tombstones == () + assert rejected.next_state.closed_epochs[-1] == retained_before + _assert_rejection_unchanged(rolled, rejected.next_state) + + +def test_post_rollover_expiry_requires_the_complete_witness_binding() -> None: + state, batch, _, _ = _reserved_batch(name="expiry-binding") + reservation = state.reservations[0] + rolled = _rollover_with_receiver_fence( + state, + batch, + name="expiry-binding", + ) + valid_witness = _witness(batch, WitnessKind.IDEMPOTENCY_EXPIRY) + mismatched_witnesses = ( + replace(valid_witness, request_id=AdmissionRequestId("wrong-request")), + replace(valid_witness, batch_id=AdmissionBatchId("wrong-batch")), + replace( + valid_witness, + representation_revision=RepresentationRevision("wrong-revision"), + ), + replace( + valid_witness, + representation_binding_id=RepresentationBindingId("wrong-binding"), + ), + replace(valid_witness, occurrence_ids=(AdmissionOccurrenceId("wrong-occurrence"),)), + replace(valid_witness, snapshot_sequence=valid_witness.snapshot_sequence + 1), + ) + current = rolled + for index, mismatched_witness in enumerate(mismatched_witnesses): + expiry = ExpireIdempotencyKeyEvent( + **_event_fields( + current, + f"expire-mismatched-binding-{index}", + "expire-idempotency-key", + ), + reservation_key=reservation.key, + expiry_witness=mismatched_witness, + ) + rejected = reduce_context_admission(current, expiry) + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.decision.reason_code == "invalid_expiry_witness" + assert rejected.next_state.expired_idempotency_tombstones == () + _assert_rejection_unchanged(current, rejected.next_state) + current = rejected.next_state + + accepted = reduce_context_admission( + current, + ExpireIdempotencyKeyEvent( + **_event_fields( + current, + "expire-complete-binding", + "expire-idempotency-key", + ), + reservation_key=reservation.key, + expiry_witness=valid_witness, + ), + ) + assert accepted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert len(accepted.next_state.expired_idempotency_tombstones) == 1 + + def test_generation_indeterminate_remains_reserved_across_reconciliation_deadline() -> None: state, batch, _, generation = _reserved_batch(name="generation-crash") event = MarkGenerationIndeterminateEvent( @@ -1659,7 +1842,7 @@ def test_resolve_indeterminate_acceptance_reconciles_exact_charge() -> None: final_manifest=batch.manifest, exact_charge=19, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), representation_binding_witness=_binding(batch), ) resolved = reduce_context_admission(marked.next_state, resolution) @@ -1718,7 +1901,7 @@ def test_indeterminate_acceptance_requires_matching_authority_source() -> None: final_manifest=batch.manifest, exact_charge=19, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("different-authority"), + authority_source=AuthoritySourceId("different-authority"), representation_binding_witness=_binding(batch), ) rejected = reduce_context_admission(marked.next_state, event) @@ -1727,7 +1910,7 @@ def test_indeterminate_acceptance_requires_matching_authority_source() -> None: assert _batch_record(rejected.next_state, batch).state is AdmissionState.INDETERMINATE -def test_staged_release_requires_distinct_history_removal_witness() -> None: +def test_staged_release_uses_the_exact_single_witness_schema() -> None: state, batch, _, _ = _reserved_batch(name="history-removal") record = _batch_record(state, batch) assert record.reservation_id is not None @@ -1743,7 +1926,7 @@ def test_staged_release_requires_distinct_history_removal_witness() -> None: record.reservation_id, ).reserved_count, measurement_kind=MeasurementKind.TOKENIZER_EXACT, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), ), ) staged = reduce_context_admission( @@ -1758,26 +1941,16 @@ def test_staged_release_requires_distinct_history_removal_witness() -> None: witness=_witness(batch, WitnessKind.HISTORY_STAGED), ), ) - without_removal = ReleaseNonAdmissionEvent( + release = ReleaseNonAdmissionEvent( **_event_fields( staged.next_state, - "release-without-removal", + "release-with-exact-witness", "release-non-admission", ), batch_id=batch.batch_id, witness=_witness(batch, WitnessKind.NON_ADMISSION), - history_removal_witness=None, - ) - rejected = reduce_context_admission(staged.next_state, without_removal) - assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert isinstance(rejected.next_state, ActiveContextAdmissionState) - with_removal = replace( - without_removal, - event_id=AdmissionEventId("release-with-removal"), - expected_aggregate_revision=rejected.next_state.aggregate_revision, - history_removal_witness=_witness(batch, WitnessKind.ROLLBACK), ) - released = reduce_context_admission(rejected.next_state, with_removal) + released = reduce_context_admission(staged.next_state, release) assert released.decision.kind is AdmissionDecisionKind.WOULD_ADMIT assert isinstance(released.next_state, ActiveContextAdmissionState) assert _batch_record(released.next_state, batch).state is AdmissionState.RELEASED @@ -1826,8 +1999,6 @@ def test_rollover_moves_old_work_to_resolvable_closed_epoch_audit() -> None: generation_count=7, ) state = _prepare_dispatch(state, batch) - batch_record = _batch_record(state, batch) - assert batch_record.dispatch_sequence is not None receiver = AuthoritySourceId("receiver-closed-resolution") rollover = RolloverEpochEvent( **_event_fields(state, "rollover-closed-resolution", "rollover-epoch"), @@ -1842,9 +2013,8 @@ def test_rollover_moves_old_work_to_resolvable_closed_epoch_audit() -> None: new_window_epoch_number=2, receiver_authority_source_id=receiver, fence_witness_id=AdmissionWitnessId("fence-closed-resolution"), - highest_admitted_dispatch_sequence=batch_record.dispatch_sequence, + highest_admitted_dispatch_sequence=1, ), - deducted_unresolved_count=0, new_snapshot=_snapshot(epoch=2), protected_pools=(), ) @@ -1871,7 +2041,7 @@ def test_rollover_moves_old_work_to_resolvable_closed_epoch_audit() -> None: final_manifest=batch.manifest, exact_input_charge=17, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), representation_binding_witness=_binding(batch), ), ) @@ -1911,7 +2081,6 @@ def test_rollover_accepts_only_one_explicit_authority_alternative() -> None: **_event_fields(empty_state, "rollover-fully-resolved", "rollover-epoch"), witness=_witness(sentinel, WitnessKind.EPOCH_ROLLOVER), fence_proof=None, - deducted_unresolved_count=0, new_snapshot=_snapshot(epoch=2), protected_pools=(), ) @@ -1928,7 +2097,6 @@ def test_rollover_accepts_only_one_explicit_authority_alternative() -> None: **_event_fields(state, "rollover-snapshot-deducted", "rollover-epoch"), witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), fence_proof=None, - deducted_unresolved_count=10, new_snapshot=_snapshot(epoch=2), protected_pools=(), ) @@ -1937,7 +2105,7 @@ def test_rollover_accepts_only_one_explicit_authority_alternative() -> None: invalid_deduction = replace( deducted, event_id=AdmissionEventId("rollover-invalid-deduction"), - deducted_unresolved_count=9, + new_snapshot=_snapshot(epoch=2, active_count=9), ) rejected = reduce_context_admission(state, invalid_deduction) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 58cd7f1ff..2c5fa9739 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -509,7 +509,13 @@ def reserve(self, slot: int, input_count: int, generation_count: int) -> None: generation_count, ) else: - assert transition.next_state == prior + assert ( + replace( + transition.next_state, + processed_events=prior.processed_events, + ) + == prior + ) self._accept_publication(transition, event) @rule() @@ -543,7 +549,6 @@ def stale_fence_is_rejected(self) -> None: fence_witness_id=AdmissionWitnessId(f"stale-fence-{self.event_sequence}"), highest_admitted_dispatch_sequence=self.state.admission_sequence.value, ), - deducted_unresolved_count=0, new_snapshot=ContextWindowSnapshot( protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, window_epoch_id=WindowEpochId(f"epoch-state-machine-{new_window_epoch_number}"), @@ -560,7 +565,7 @@ def stale_fence_is_rejected(self) -> None: prior = self.state transition = reduce_context_admission(prior, event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert transition.next_state == prior + assert replace(transition.next_state, processed_events=prior.processed_events) == prior self._accept_publication(transition, event) @rule( @@ -617,7 +622,7 @@ def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=reserved_input_count, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("authority-state-machine"), + authority_source=AuthoritySourceId("authority-state-machine"), ) transition = reduce_context_admission(self.state, prepare_event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT @@ -630,7 +635,7 @@ def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: prior = self.state rejected = reduce_context_admission(prior, skipped_dispatch) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected.next_state == prior + assert replace(rejected.next_state, processed_events=prior.processed_events) == prior self._accept_publication(rejected, skipped_dispatch) stage_event = StageHistoryEvent( **self._fields("stage-history"), @@ -657,7 +662,7 @@ def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: prior = self.state rejected = reduce_context_admission(prior, premature_generation) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected.next_state == prior + assert replace(rejected.next_state, processed_events=prior.processed_events) == prior self._accept_publication(rejected, premature_generation) dispatch_event = DispatchRequestEvent( **self._fields("dispatch-request"), @@ -677,7 +682,7 @@ def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: final_manifest=batch.manifest, exact_input_charge=reserved_input_count, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("authority-state-machine"), + authority_source=AuthoritySourceId("authority-state-machine"), representation_binding_witness=_binding(batch), ) transition = reduce_context_admission(self.state, accept_event) @@ -702,7 +707,7 @@ def prepare_and_mark_indeterminate(self) -> None: representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=input_count, measurement_kind=MeasurementKind.PROVIDER_EXACT, - authority_source_id=AuthoritySourceId("authority-state-machine"), + authority_source=AuthoritySourceId("authority-state-machine"), ) prepared = reduce_context_admission(self.state, prepare_event) assert prepared.decision.kind is AdmissionDecisionKind.WOULD_ADMIT @@ -745,10 +750,6 @@ def release_or_rollback_preacceptance(self, rollback: bool) -> None: AdmissionState.REQUEST_DISPATCHED, }: continue - needs_removal = record.state in { - AdmissionState.HISTORY_STAGED, - AdmissionState.REQUEST_DISPATCHED, - } event = ReleaseNonAdmissionEvent( **self._fields("release-non-admission"), batch_id=batch.batch_id, @@ -757,11 +758,6 @@ def release_or_rollback_preacceptance(self, rollback: bool) -> None: occurrence, WitnessKind.NON_ADMISSION, ), - history_removal_witness=( - _witness(batch, occurrence, WitnessKind.ROLLBACK) - if needs_removal - else None - ), ) transition = reduce_context_admission(self.state, event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT @@ -795,11 +791,6 @@ def resolve_indeterminate_nonacceptance(self, rollback: bool) -> None: occurrence, WitnessKind.NON_ADMISSION, ), - history_removal_witness=_witness( - batch, - occurrence, - WitnessKind.ROLLBACK, - ), ) transition = reduce_context_admission(self.state, event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT @@ -938,13 +929,12 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: new_window_epoch_number=new_window_epoch_number, receiver_authority_source_id=AuthoritySourceId("authority-state-machine"), fence_witness_id=AdmissionWitnessId(f"fence-{witness_suffix}"), - highest_admitted_dispatch_sequence=max( - ( - record.dispatch_sequence - for record in self.state.batch_records - if record.dispatch_sequence is not None - ), - default=0, + highest_admitted_dispatch_sequence=sum( + isinstance(record.event, DispatchRequestEvent) + and record.original_decision.kind is AdmissionDecisionKind.WOULD_ADMIT + and record.event.witness.window_epoch_id == old_snapshot.window_epoch_id + and record.event.witness.window_epoch_number == old_snapshot.window_epoch_number + for record in self.state.processed_events ), ) rollover_event = RolloverEpochEvent( @@ -963,7 +953,6 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: authority_source_id=AuthoritySourceId("authority-state-machine"), ), fence_proof=proof, - deducted_unresolved_count=0, new_snapshot=ContextWindowSnapshot( protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, window_epoch_id=new_window_epoch_id, diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index e2483089b..1ab22a743 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -120,7 +120,6 @@ "RECONCILED", "INDETERMINATE", "QUARANTINED", - "INVALIDATED", ), MeasurementKind: ( "PROVIDER_EXACT", @@ -244,7 +243,7 @@ "representation_binding_id", "proposed_charge", "measurement_kind", - "authority_source_id", + "authority_source", ), "StageHistoryEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), "DispatchRequestEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), @@ -256,11 +255,10 @@ "final_manifest", "exact_input_charge", "measurement_kind", - "authority_source_id", + "authority_source", "representation_binding_witness", ), - "ReleaseNonAdmissionEvent": _EVENT_BASE_FIELDS - + ("batch_id", "witness", "history_removal_witness"), + "ReleaseNonAdmissionEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), "RollbackAdmissionEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), "MarkIndeterminateEvent": _EVENT_BASE_FIELDS + ("batch_id", "reason_code"), "ResolveIndeterminateAcceptedEvent": _EVENT_BASE_FIELDS @@ -271,11 +269,10 @@ "final_manifest", "exact_charge", "measurement_kind", - "authority_source_id", + "authority_source", "representation_binding_witness", ), - "ResolveIndeterminateNonAdmissionEvent": _EVENT_BASE_FIELDS - + ("batch_id", "witness", "history_removal_witness"), + "ResolveIndeterminateNonAdmissionEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), "ResolveIndeterminateRollbackEvent": _EVENT_BASE_FIELDS + ("batch_id", "witness"), "StartGenerationEvent": _EVENT_BASE_FIELDS + ("generation_reservation_id", "witness"), "ReconcileGenerationEvent": _EVENT_BASE_FIELDS @@ -288,7 +285,6 @@ + ( "witness", "fence_proof", - "deducted_unresolved_count", "new_snapshot", "protected_pools", ), @@ -426,12 +422,9 @@ "batch", "state", "reservation_id", - "protected_release_witness_kind", "witness_ids", - "prepared_input_count", "committed_input_count", "unresolved_input_count", - "dispatch_sequence", ), GenerationReservationRecord: ( "generation_reservation_id", @@ -835,6 +828,11 @@ def test_dispatch_identity_is_validated_and_projects_only_dispatch_id() -> None: replace(lineage, dispatch_identity=forged) assert "forged-private-marker" not in str(exc_info.value) + object.__setattr__(identity, "completion_marker", "tampered-after-construction") + with pytest.raises(ContextAdmissionValidationError) as serialization_error: + lineage.to_dict() + assert "tampered-after-construction" not in str(serialization_error.value) + @pytest.mark.parametrize( "canary", @@ -1031,6 +1029,17 @@ def test_privacy_canaries_never_escape_validation_repr_or_serialization(canary: assert canary not in serialized +@pytest.mark.parametrize("opaque_type", OPAQUE_STRING_TYPES) +def test_unprefixed_digest_shaped_values_are_rejected_by_every_opaque_wrapper( + opaque_type: type[object], +) -> None: + digest = "a" * 64 + with pytest.raises(ContextAdmissionValidationError) as exc_info: + opaque_type(digest) + assert digest not in str(exc_info.value) + assert digest not in repr(exc_info.value) + + @pytest.mark.parametrize( "canary", [ @@ -1147,12 +1156,9 @@ def test_active_state_rejects_inconsistent_bidirectional_ownership_links() -> No batch=batch, state=AdmissionState.RESERVED, reservation_id=None, - protected_release_witness_kind=None, witness_ids=(), - prepared_input_count=None, committed_input_count=0, unresolved_input_count=0, - dispatch_sequence=None, ) with pytest.raises(ContextAdmissionValidationError): replace(state, batch_records=(orphan_batch,)) @@ -1290,12 +1296,9 @@ def test_batch_record_rejects_committed_and_unresolved_simultaneously() -> None: batch=batch, state=AdmissionState.INDETERMINATE, reservation_id=None, - protected_release_witness_kind=None, witness_ids=(), - prepared_input_count=None, committed_input_count=5, unresolved_input_count=5, - dispatch_sequence=None, ) assert "committed_and_unresolved_simultaneously" in str(exc_info.value) @@ -1343,7 +1346,7 @@ def test_prepare_event_rejects_estimate_measurement() -> None: representation_binding_id=batch.manifest.representation_binding_id, proposed_charge=5, measurement_kind=MeasurementKind.HOST_ESTIMATE, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), ) assert "non_authoritative_measurement" in str(exc_info.value) @@ -1363,7 +1366,7 @@ def test_accept_event_rejects_estimate_measurement() -> None: final_manifest=batch.manifest, exact_input_charge=5, measurement_kind=MeasurementKind.BYTE_EMERGENCY, - authority_source_id=AuthoritySourceId("authority-test"), + authority_source=AuthoritySourceId("authority-test"), representation_binding_witness=_binding(batch), ) assert "non_authoritative_measurement" in str(exc_info.value) From 01ddacdf02ade9b30e4a8d65c80e32d4dbd42ffb Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 10:06:44 -0700 Subject: [PATCH 08/62] fix(tests): use canonical dispatch identities in resume fixtures --- tests/fleet/test_dispatch_state_handle.py | 10 +++++----- tests/fleet/test_resume_preflight.py | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/fleet/test_dispatch_state_handle.py b/tests/fleet/test_dispatch_state_handle.py index 0cf3c14e9..253d06428 100644 --- a/tests/fleet/test_dispatch_state_handle.py +++ b/tests/fleet/test_dispatch_state_handle.py @@ -141,7 +141,7 @@ def test_dispatch_state_handle_open_continued_rejects_missing_file(self, tmp_pat d = _dispatches_dir(tmp_path) with pytest.raises(FileNotFoundError): - DispatchStateHandle.open_continued(d, "nonexistent-id-abc-123") + DispatchStateHandle.open_continued(d, "00000000-0000-0000-0000-000000000001") def test_dispatch_state_handle_open_continued_succeeds_with_existing_file(self, tmp_path): from autoskillit.fleet.state import DispatchStateHandle @@ -161,8 +161,8 @@ class TestAllResumeCombinationsProduceValidHandle: "resume,prior,expect_fresh", [ (None, None, True), - (None, "abc-def-ghi-jkl-mno", True), - ("sess-1", "abc-def-ghi-jkl-mno", False), + (None, "11111111-1111-1111-1111-111111111111", True), + ("sess-1", "11111111-1111-1111-1111-111111111111", False), ("sess-1", None, True), ("sess-1", "", True), ], @@ -235,7 +235,7 @@ async def test_capture_chain_across_resume_boundary(self, tool_ctx, monkeypatch) dispatches_dir.mkdir(parents=True, exist_ok=True) campaign_id = tool_ctx.kitchen_id - prior_id = "prev-dispatch-abc123" + prior_id = "22222222-2222-2222-2222-222222222222" prev_state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( prev_state_path, campaign_id, "camp", "", [DispatchRecord(name="dispatch-a")] @@ -295,7 +295,7 @@ async def test_session_chain_accumulates_across_resume(self, tool_ctx, monkeypat dispatches_dir = tool_ctx.temp_dir / "dispatches" campaign_id = tool_ctx.kitchen_id - prior_id = "prior-dispatch-abc123" + prior_id = "33333333-3333-3333-3333-333333333333" prior_state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( prior_state_path, diff --git a/tests/fleet/test_resume_preflight.py b/tests/fleet/test_resume_preflight.py index 23736930c..5662a691f 100644 --- a/tests/fleet/test_resume_preflight.py +++ b/tests/fleet/test_resume_preflight.py @@ -31,7 +31,7 @@ async def test_missing_jsonl_rejects_dispatch(self, tool_ctx, monkeypatch, tmp_p dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-missing-jsonl" + prior_id = "00000000-0000-0000-0000-000000000101" write_initial_state( dispatches_dir / f"{prior_id}.json", tool_ctx.kitchen_id, @@ -109,7 +109,7 @@ async def test_chain_fallback_proceeds_when_primary_missing( dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-for-chain" + prior_id = "00000000-0000-0000-0000-000000000102" prev_state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( prev_state_path, @@ -229,7 +229,7 @@ async def test_resume_rejected_when_prior_dispatch_succeeded(self, tool_ctx, mon dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-succeeded" + prior_id = "00000000-0000-0000-0000-000000000103" state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( state_path, @@ -291,7 +291,7 @@ async def test_resume_proceeds_when_prior_dispatch_resumable( dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-resumable" + prior_id = "00000000-0000-0000-0000-000000000104" state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( state_path, @@ -345,7 +345,7 @@ async def test_resume_proceeds_when_prior_dispatch_not_in_state( dispatches_dir = tool_ctx.temp_dir / "dispatches" dispatches_dir.mkdir(parents=True, exist_ok=True) - prior_id = "prior-dispatch-other-name" + prior_id = "00000000-0000-0000-0000-000000000105" state_path = dispatches_dir / f"{prior_id}.json" write_initial_state( state_path, @@ -388,7 +388,7 @@ async def test_resume_proceeds_when_prior_state_missing(self, tool_ctx, monkeypa lambda **_: _make_no_sentinel(), ) - prior_id = "prior-dispatch-nonexistent" + prior_id = "00000000-0000-0000-0000-000000000106" await execute_dispatch( tool_ctx=tool_ctx, From dfe66fa173f7d40488701081bee4bb91ca8a13d8 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:01:52 -0700 Subject: [PATCH 09/62] fix(review): preserve rollover epoch release policy --- src/autoskillit/core/context_admission.py | 21 +++-- tests/core/test_context_admission_reducer.py | 92 ++++++++++++++++++++ 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index c048f9453..e59470ead 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -550,18 +550,27 @@ def _required_release_witness_kind( ): protected_pools = state.protected_pools else: - open_event = next( + policy_event = next( ( record.event for record in state.processed_events - if isinstance(record.event, OpenEpochEvent) - and record.event.snapshot.window_epoch_id == policy_snapshot.window_epoch_id - and record.event.snapshot.window_epoch_number - == policy_snapshot.window_epoch_number + if ( + isinstance(record.event, OpenEpochEvent) + and record.event.snapshot.window_epoch_id == policy_snapshot.window_epoch_id + and record.event.snapshot.window_epoch_number + == policy_snapshot.window_epoch_number + ) + or ( + isinstance(record.event, RolloverEpochEvent) + and record.event.new_snapshot.window_epoch_id + == policy_snapshot.window_epoch_id + and record.event.new_snapshot.window_epoch_number + == policy_snapshot.window_epoch_number + ) ), None, ) - protected_pools = open_event.protected_pools if open_event is not None else () + protected_pools = policy_event.protected_pools if policy_event is not None else () pool = next( ( item diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 76fe5a4c4..57b7f2eb9 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -2071,6 +2071,98 @@ def test_rollover_moves_old_work_to_resolvable_closed_epoch_audit() -> None: assert reconciled_audit.terminal_generation_reservations[0].state is GenerationState.RECONCILED +def test_protected_release_uses_policy_from_rollover_created_epoch() -> None: + owner = ProtectedPoolOwnerId("rollover-policy-owner") + pool = ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=owner, + injected_count=20, + priority=1, + required_release_witness_kind=WitnessKind.NON_ADMISSION, + ) + state = _open_epoch(remaining_count=60) + sentinel = _batch( + "batch-rollover-policy-sentinel", + (_occurrence("rollover-policy-sentinel", maximum=1),), + ) + opened_by_rollover = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-policy-open", "rollover-epoch"), + witness=_witness(sentinel, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + new_snapshot=_snapshot(epoch=2), + protected_pools=(pool,), + ), + ) + assert isinstance(opened_by_rollover.next_state, ActiveContextAdmissionState) + state = opened_by_rollover.next_state + + occurrence = _occurrence( + "rollover-policy-work", + maximum=10, + reserve_class=ReserveClass.SYNTHESIS, + epoch=2, + ) + state, _ = _propose(state, occurrence) + batch = _batch( + "batch-rollover-policy-work", + (occurrence,), + reserve_class=ReserveClass.SYNTHESIS, + protected_owner=owner.value, + ) + reserved, _ = _reserve( + state, + batch, + (occurrence,), + input_count=10, + generation_count=0, + ) + assert isinstance(reserved.next_state, ActiveContextAdmissionState) + state = _prepare_dispatch(reserved.next_state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields(state, "mark-rollover-policy", "mark-indeterminate"), + batch_id=batch.batch_id, + reason_code="ambiguous-provider-result", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + state = marked.next_state + + rolled_again = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-policy-close", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER, epoch=2), + fence_proof=None, + new_snapshot=_snapshot(epoch=3), + protected_pools=(), + ), + ) + assert isinstance(rolled_again.next_state, ActiveContextAdmissionState) + resolved = reduce_context_admission( + rolled_again.next_state, + ResolveIndeterminateNonAdmissionEvent( + **_event_fields( + rolled_again.next_state, + "resolve-rollover-policy", + "resolve-non-admission", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION, epoch=2), + ), + ) + + assert resolved.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(resolved.next_state, ActiveContextAdmissionState) + assert ( + resolved.next_state.closed_epochs[-1].terminal_batch_records[0].state + is AdmissionState.RELEASED + ) + + def test_rollover_accepts_only_one_explicit_authority_alternative() -> None: empty_state = _open_epoch() sentinel = _batch( From 788486f4eb8ea2a10f986d417c109d462d278165 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:03:02 -0700 Subject: [PATCH 10/62] fix(review): reject generation reservation ID reuse --- src/autoskillit/core/context_admission.py | 9 +++++ tests/core/test_context_admission_reducer.py | 35 ++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index e59470ead..d011c769a 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -980,6 +980,15 @@ def _reserve( return _reject(state, event, "reservation_revision_mismatch") generation = event.generation_reservation generation_count = generation.maximum_allowance if generation is not None else 0 + if generation is not None and any( + existing.generation_reservation_id == generation.generation_reservation_id + for existing in state.generation_reservations + ): + return _reject( + state, + event, + "generation_reservation_id_reuse_with_changed_descriptor", + ) if generation is not None and ( generation.request_id != event.batch.request_id or generation.batch_id != event.batch.batch_id diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 57b7f2eb9..8658c1b99 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -587,6 +587,41 @@ def _reserved_batch( return transition.next_state, batch, occurrences, generation +def test_reserve_rejects_active_generation_reservation_id_reuse() -> None: + state, _, _, existing_generation = _reserved_batch( + name="generation-id-owner", + input_count=5, + generation_count=5, + ) + occurrence = _occurrence("generation-id-collision", maximum=5) + state, _ = _propose(state, occurrence) + batch = _batch("batch-generation-id-collision", (occurrence,)) + _, event = _reserve( + state, + batch, + (occurrence,), + input_count=5, + generation_count=5, + ) + assert event.generation_reservation is not None + collision = replace( + event, + generation_reservation=replace( + event.generation_reservation, + generation_reservation_id=existing_generation.generation_reservation_id, + ), + ) + + rejected = reduce_context_admission(state, collision) + + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert ( + rejected.decision.reason_code == "generation_reservation_id_reuse_with_changed_descriptor" + ) + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_rejection_unchanged(state, rejected.next_state) + + def _prepare_dispatch( state: ActiveContextAdmissionState, batch: AdmissionBatch ) -> ActiveContextAdmissionState: From d78054bbdb64d62c5306c21f46561cf8af419a10 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:03:55 -0700 Subject: [PATCH 11/62] fix(review): make repeated idempotency expiry typed --- src/autoskillit/core/context_admission.py | 6 ++++++ tests/core/test_context_admission_reducer.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index d011c769a..f4a9bc960 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -2465,6 +2465,12 @@ def _expire_idempotency( return _reject(state, event, "idempotency_key_not_terminal") if not isinstance(state, ActiveContextAdmissionState): return _reject(state, event, "idempotency_key_not_terminal") + if any( + tombstone.namespace == record.namespace + and tombstone.reservation_key == event.reservation_key + for tombstone in state.expired_idempotency_tombstones + ): + return _reject(state, event, "idempotency_key_expired") batch_record: AdmissionBatchRecord | None snapshot: ContextWindowSnapshot generation_records: tuple[GenerationReservationRecord, ...] diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 8658c1b99..efbd95b90 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -1817,8 +1817,26 @@ def test_post_rollover_expiry_requires_the_complete_witness_binding() -> None: ), ) assert accepted.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(accepted.next_state, ActiveContextAdmissionState) assert len(accepted.next_state.expired_idempotency_tombstones) == 1 + repeated = reduce_context_admission( + accepted.next_state, + ExpireIdempotencyKeyEvent( + **_event_fields( + accepted.next_state, + "expire-complete-binding-again", + "expire-idempotency-key", + ), + reservation_key=reservation.key, + expiry_witness=valid_witness, + ), + ) + assert repeated.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert repeated.decision.reason_code == "idempotency_key_expired" + assert len(repeated.next_state.expired_idempotency_tombstones) == 1 + _assert_rejection_unchanged(accepted.next_state, repeated.next_state) + def test_generation_indeterminate_remains_reserved_across_reconciliation_deadline() -> None: state, batch, _, generation = _reserved_batch(name="generation-crash") From 15f886d8b13afce35413a7e5ccaef64382ba8609 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:04:34 -0700 Subject: [PATCH 12/62] fix(review): bound protocol integers to 64 bits --- src/autoskillit/core/types/_type_context_admission.py | 3 ++- tests/core/types/test_context_admission_contract.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index f1a228143..2d4d3a273 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -39,6 +39,7 @@ from ._type_results import ModelIdentity CONTEXT_ADMISSION_PROTOCOL_VERSION = 1 +_MAX_UINT64 = (1 << 64) - 1 _CONTENT_FREE_TEXT = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:@+-]*\Z") _CONTENT_FREE_LOCATOR = re.compile(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]*\Z") @@ -88,7 +89,7 @@ def _validate_protocol_version(protocol_version: int) -> None: def _validate_non_negative(value: int, reason_code: str) -> None: - if isinstance(value, bool) or not isinstance(value, int) or value < 0: + if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > _MAX_UINT64: _raise_invalid(reason_code) diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index 1ab22a743..c8e097ece 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -808,8 +808,11 @@ def test_numeric_revisions_are_non_negative_and_have_no_default( revision_type: type[object], ) -> None: assert revision_type(0).value == 0 + assert revision_type((1 << 64) - 1).value == (1 << 64) - 1 with pytest.raises(ContextAdmissionValidationError): revision_type(-1) + with pytest.raises(ContextAdmissionValidationError): + revision_type(1 << 64) assert all(field.default is dataclasses.MISSING for field in fields(revision_type)) From d5e2b44b4f165338e6d636d7ccbfd7d9794a0313 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:05:32 -0700 Subject: [PATCH 13/62] fix(review): reject noncanonical tagged mappings --- .../core/types/_type_context_admission.py | 6 +++++ .../types/test_context_admission_contract.py | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 2d4d3a273..8f1ead8f2 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -184,6 +184,8 @@ def _decode(value: object) -> object: _raise_invalid("invalid_dispatch_identity") return DispatchIdentity.from_dispatch_id(dispatch_id) if "__enum__" in value: + if set(value) != {"__enum__", "value"}: + _raise_invalid("unknown_serialized_enum") enum_name = value.get("__enum__") enum_value = value.get("value") if ( @@ -197,11 +199,15 @@ def _decode(value: object) -> object: except (TypeError, ValueError) as exc: raise ContextAdmissionValidationError("invalid_serialized_enum") from exc if "__tuple__" in value: + if set(value) != {"__tuple__"}: + _raise_invalid("invalid_serialized_tuple") raw = value["__tuple__"] if not isinstance(raw, list): _raise_invalid("invalid_serialized_tuple") return tuple(_decode(item) for item in raw) if "__frozenset__" in value: + if set(value) != {"__frozenset__"}: + _raise_invalid("invalid_serialized_frozenset") raw = value["__frozenset__"] if not isinstance(raw, list): _raise_invalid("invalid_serialized_frozenset") diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index c8e097ece..4cfeabf0e 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -837,6 +837,33 @@ def test_dispatch_identity_is_validated_and_projects_only_dispatch_id() -> None: assert "tampered-after-construction" not in str(serialization_error.value) +@pytest.mark.parametrize( + ("tagged_value", "reason_code"), + [ + ( + { + "__enum__": "ReserveClass", + "value": "ordinary", + "unexpected": "ignored", + }, + "unknown_serialized_enum", + ), + ({"__tuple__": [], "unexpected": "ignored"}, "invalid_serialized_tuple"), + ( + {"__frozenset__": [], "unexpected": "ignored"}, + "invalid_serialized_frozenset", + ), + ], +) +def test_tagged_serialization_requires_exact_key_sets( + tagged_value: dict[str, object], + reason_code: str, +) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId.from_dict({"value": tagged_value}) + assert reason_code in str(exc_info.value) + + @pytest.mark.parametrize( "canary", [ From 7fbd74a16edfef5f0115d1e55c4bf0fa8ff9d16a Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:05:59 -0700 Subject: [PATCH 14/62] fix(review): suppress invalid enum value causes --- .../core/types/_type_context_admission.py | 4 ++-- .../types/test_context_admission_contract.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 8f1ead8f2..9377932d5 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -196,8 +196,8 @@ def _decode(value: object) -> object: _raise_invalid("unknown_serialized_enum") try: return _ENUM_REGISTRY[enum_name](enum_value) - except (TypeError, ValueError) as exc: - raise ContextAdmissionValidationError("invalid_serialized_enum") from exc + except (TypeError, ValueError): + raise ContextAdmissionValidationError("invalid_serialized_enum") from None if "__tuple__" in value: if set(value) != {"__tuple__"}: _raise_invalid("invalid_serialized_tuple") diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index 4cfeabf0e..4b756f73f 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -864,6 +864,24 @@ def test_tagged_serialization_requires_exact_key_sets( assert reason_code in str(exc_info.value) +def test_invalid_serialized_enum_suppresses_attacker_controlled_cause() -> None: + canary = "private-enum-value" + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId.from_dict( + { + "value": { + "__enum__": "ReserveClass", + "value": canary, + } + } + ) + + assert exc_info.value.__cause__ is None + assert exc_info.value.__suppress_context__ + assert canary not in str(exc_info.value) + assert canary not in repr(exc_info.value) + + @pytest.mark.parametrize( "canary", [ From 76e18fdd12a56a718a3808e40bf30e559d58084e Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:06:39 -0700 Subject: [PATCH 15/62] fix(review): require typed model identity fields --- .../core/types/_type_context_admission.py | 27 +++++++++++++------ .../types/test_context_admission_contract.py | 19 +++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 9377932d5..4c9f4e4ed 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -214,14 +214,25 @@ def _decode(value: object) -> object: return frozenset(_decode(item) for item in raw) type_name = value.get("__type__") if type_name == "ModelIdentity": - try: - return ModelIdentity( - configured_model=str(value["configured_model"]), - effective_model=str(value["effective_model"]), - profile_name=str(value["profile_name"]), - ) - except KeyError as exc: - raise ContextAdmissionValidationError("invalid_model_identity") from exc + if set(value) != { + "__type__", + "configured_model", + "effective_model", + "profile_name", + }: + _raise_invalid("invalid_model_identity") + configured_model = value["configured_model"] + effective_model = value["effective_model"] + profile_name = value["profile_name"] + if not all( + isinstance(item, str) for item in (configured_model, effective_model, profile_name) + ): + _raise_invalid("invalid_model_identity") + return ModelIdentity( + configured_model=configured_model, + effective_model=effective_model, + profile_name=profile_name, + ) if not isinstance(type_name, str) or type_name not in _TYPE_REGISTRY: _raise_invalid("unknown_serialized_contract_type") contract_type = _TYPE_REGISTRY[type_name] diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index 4b756f73f..ad0b26fa9 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -882,6 +882,25 @@ def test_invalid_serialized_enum_suppresses_attacker_controlled_cause() -> None: assert canary not in repr(exc_info.value) +@pytest.mark.parametrize( + "field_name", + ["configured_model", "effective_model", "profile_name"], +) +def test_model_identity_deserialization_requires_string_fields(field_name: str) -> None: + encoded_identity: dict[str, object] = { + "__type__": "ModelIdentity", + "configured_model": "claude-test", + "effective_model": "claude-test", + "profile_name": "default", + } + encoded_identity[field_name] = 42 + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId.from_dict({"value": encoded_identity}) + + assert "invalid_model_identity" in str(exc_info.value) + + @pytest.mark.parametrize( "canary", [ From b904356680cad80b468adc94750e4f7d5f20f8be Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:07:18 -0700 Subject: [PATCH 16/62] fix(review): enforce opaque identifier contract --- .../core/types/_type_context_admission.py | 16 ++++++++++++---- .../types/test_context_admission_contract.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 4c9f4e4ed..449993d7b 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -339,14 +339,22 @@ class _OpaqueString(_ContractValue): value: str def __post_init__(self) -> None: - _validate_bounded_text(self.value, "invalid_opaque_identifier") + _validate_bounded_text( + self.value, + "invalid_opaque_identifier", + maximum=96, + ) allowed = "-_.:" if ( len(self.value) in {40, 64, 128} and all(character in "0123456789abcdefABCDEF" for character in self.value) - ) or any( - not (character.isascii() and (character.isalnum() or character in allowed)) - for character in self.value + ) or ( + self.value.startswith("-") + or self.value.endswith("-") + or any( + not (character.isascii() and (character.isalnum() or character in allowed)) + for character in self.value + ) ): _raise_invalid("invalid_opaque_identifier") diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index ad0b26fa9..8519a7398 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -803,6 +803,22 @@ def test_opaque_string_identifiers_are_frozen_content_free_values( value.value = "changed" +@pytest.mark.parametrize( + "invalid_identifier", + [ + "a" * 97, + "-leading-segment", + "trailing-segment-", + "contains+unsupported", + "contains@unsupported", + ], +) +def test_opaque_identifiers_enforce_protocol_shape(invalid_identifier: str) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionEventId(invalid_identifier) + assert "invalid_opaque_identifier" in str(exc_info.value) + + @pytest.mark.parametrize("revision_type", [AggregateRevision, AdmissionSequence]) def test_numeric_revisions_are_non_negative_and_have_no_default( revision_type: type[object], From 439d6b864fd29cfc6ee67ecf105d28ac031a39ac Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:08:12 -0700 Subject: [PATCH 17/62] fix(review): validate witness occurrence authority --- .../core/types/_type_context_admission.py | 4 ++++ .../types/test_context_admission_contract.py | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 449993d7b..510afd9e1 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -756,6 +756,10 @@ def __post_init__(self) -> None: "noncanonical_witness_occurrences", key=lambda occurrence_id: occurrence_id.value, ) + if len(self.occurrence_ids) != len(set(self.occurrence_ids)) or ( + not self.occurrence_ids and self.kind is not WitnessKind.EPOCH_ROLLOVER + ): + _raise_invalid("invalid_witness_occurrences") @dataclass(frozen=True, slots=True) diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index 8519a7398..b2f02f0e2 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1371,6 +1371,27 @@ def test_occurrence_record_rejects_duplicate_witness_ids() -> None: assert "duplicate_witness_id" in str(exc_info.value) +def test_admission_witness_rejects_missing_or_duplicate_batch_occurrences() -> None: + occurrence = _occurrence() + batch = _batch("batch-witness-occurrences", (occurrence,)) + witness = _witness(batch, WitnessKind.PROVIDER_ACCEPTED) + + for invalid_occurrences in ( + (), + (occurrence.occurrence_id, occurrence.occurrence_id), + ): + with pytest.raises(ContextAdmissionValidationError) as exc_info: + replace(witness, occurrence_ids=invalid_occurrences) + assert "invalid_witness_occurrences" in str(exc_info.value) + + rollover = replace( + witness, + kind=WitnessKind.EPOCH_ROLLOVER, + occurrence_ids=(), + ) + assert rollover.occurrence_ids == () + + def test_batch_record_rejects_committed_and_unresolved_simultaneously() -> None: occurrence = _occurrence() batch = _batch("batch-simul", (occurrence,)) From 686eb2e6bce9b0e319b87f6f1e4b030fd495ab87 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:08:41 -0700 Subject: [PATCH 18/62] fix(review): constrain protected release witnesses --- src/autoskillit/core/types/_type_context_admission.py | 5 +++++ tests/core/types/test_context_admission_contract.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 510afd9e1..e2a836d82 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -805,6 +805,11 @@ def __post_init__(self) -> None: _validate_non_negative(self.priority, "invalid_protected_pool_priority") if self.reserve_class is ReserveClass.ORDINARY: _raise_invalid("ordinary_pool_forbidden") + if self.required_release_witness_kind not in { + WitnessKind.NON_ADMISSION, + WitnessKind.ROLLBACK, + }: + _raise_invalid("invalid_protected_release_witness_kind") @dataclass(frozen=True, slots=True) diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index b2f02f0e2..8daa0c2cb 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1054,6 +1054,15 @@ def test_protected_pool_policy_is_injected_and_has_no_borrowing_or_defaults() -> priority=1, required_release_witness_kind=WitnessKind.NON_ADMISSION, ) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ProtectedPoolSpec( + reserve_class=ReserveClass.SYNTHESIS, + capability_owner_id=ProtectedPoolOwnerId("owner-unsupported-witness"), + injected_count=10, + priority=1, + required_release_witness_kind=WitnessKind.PROVIDER_ACCEPTED, + ) + assert "invalid_protected_release_witness_kind" in str(exc_info.value) def test_manifest_rejects_overlapping_span_ownership() -> None: From 447b71bb632665b489a79387a97b1426b2eef74e Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:12:22 -0700 Subject: [PATCH 19/62] fix(review): enforce kebab-case reason codes --- src/autoskillit/core/context_admission.py | 178 +++++++++--------- .../core/types/_type_context_admission.py | 52 ++--- tests/core/test_context_admission_coverage.py | 2 +- tests/core/test_context_admission_reducer.py | 16 +- .../types/test_context_admission_contract.py | 27 ++- 5 files changed, 153 insertions(+), 122 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index f4a9bc960..e567388f7 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -200,7 +200,7 @@ def _accepted_effects( record, exact_charge, witness, - quarantine_reason_code=("provider_charge_exceeds_reservation" if quarantined else None), + quarantine_reason_code=("provider-charge-exceeds-reservation" if quarantined else None), ) @@ -434,7 +434,7 @@ def _preflight( next_state=state, decision=AdmissionDecision( kind=AdmissionDecisionKind.NOOP_IDEMPOTENT, - reason_code="event_replay", + reason_code="event-replay", window_epoch_id=original.window_epoch_id, snapshot_sequence=original.snapshot_sequence, requested_count=original.requested_count, @@ -448,7 +448,7 @@ def _preflight( decision=_decision( state, AdmissionDecisionKind.CONFLICT, - "event_id_conflict", + "event-id-conflict", ), effects=(), ) @@ -463,7 +463,7 @@ def _preflight( decision=_decision( state, AdmissionDecisionKind.IDEMPOTENCY_EXPIRED, - "idempotency_expired", + "idempotency-expired", ), effects=(), ) @@ -489,7 +489,7 @@ def _preflight( original_decision = idempotency_record.original_reserve_decision replay_decision = AdmissionDecision( kind=AdmissionDecisionKind.NOOP_IDEMPOTENT, - reason_code="reservation_key_replay", + reason_code="reservation-key-replay", window_epoch_id=original_decision.window_epoch_id, snapshot_sequence=original_decision.snapshot_sequence, requested_count=original_decision.requested_count, @@ -506,12 +506,12 @@ def _preflight( decision=_decision( state, AdmissionDecisionKind.CONFLICT, - "reservation_key_conflict", + "reservation-key-conflict", ), effects=(), ) if event.expected_aggregate_revision != state.aggregate_revision: - return _reject(state, event, "stale_revision") + return _reject(state, event, "stale-revision") return None @@ -803,7 +803,7 @@ def _open_epoch( event: OpenEpochEvent, ) -> AdmissionTransition: if not isinstance(state, UninitializedContextAdmissionState): - return _reject(state, event, "epoch_already_active") + return _reject(state, event, "epoch-already-active") try: active = ActiveContextAdmissionState( protocol_version=state.protocol_version, @@ -821,7 +821,7 @@ def _open_epoch( closed_epochs=state.closed_epochs, ) except ContextAdmissionValidationError: - return _reject(state, event, "invalid_epoch_snapshot") + return _reject(state, event, "invalid-epoch-snapshot") return _publish(state, active, event) @@ -830,7 +830,7 @@ def _propose( event: ProposeOccurrenceEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") occurrence = event.occurrence lineage = occurrence.lineage is_fork_work = ( @@ -870,23 +870,23 @@ def _propose( decision=_decision( state, AdmissionDecisionKind.NOOP_IDEMPOTENT, - "occurrence_replay", + "occurrence-replay", ), effects=(), ) return _reject( state, event, - "occurrence_identity_corruption", + "occurrence-identity-corruption", kind=AdmissionDecisionKind.QUARANTINED, ) if ( occurrence.lineage.window_epoch_id != state.snapshot.window_epoch_id or occurrence.lineage.window_epoch_number != state.snapshot.window_epoch_number ): - return _reject(state, event, "occurrence_epoch_mismatch") + return _reject(state, event, "occurrence-epoch-mismatch") if is_fork_work and not is_parent_delivery: - return _reject(state, event, "fork_requires_distinct_epoch") + return _reject(state, event, "fork-requires-distinct-epoch") record = AdmissionOccurrenceRecord( occurrence=occurrence, state=AdmissionState.PROPOSED, @@ -916,16 +916,16 @@ def _reserve( event: ReserveRequestEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") if event.snapshot_sequence != state.snapshot.snapshot_sequence: - return _reject(state, event, "snapshot_sequence_mismatch") + return _reject(state, event, "snapshot-sequence-mismatch") if _batch_record(state, event.batch.batch_id) is not None: - return _reject(state, event, "batch_already_reserved") + return _reject(state, event, "batch-already-reserved") reservation = event.input_reservations[0] if any( existing.reservation_id == reservation.reservation_id for existing in state.reservations ): - return _reject(state, event, "reservation_id_reuse_with_changed_descriptor") + return _reject(state, event, "reservation-id-reuse-with-changed-descriptor") member_records = tuple( record for record in state.occurrence_records @@ -941,7 +941,7 @@ def _reserve( for record in member_records ) ): - return _reject(state, event, "batch_members_not_all_proposed") + return _reject(state, event, "batch-members-not-all-proposed") owned_pairs = tuple( (span_id, member.occurrence.occurrence_id) for member in member_records @@ -956,9 +956,9 @@ def _reserve( or set(owned_pairs) != set(manifest_pairs) or len(owned_pairs) != len(manifest_pairs) ): - return _reject(state, event, "inconsistent_span_ownership") + return _reject(state, event, "inconsistent-span-ownership") if len(event.input_reservations) != 1: - return _reject(state, event, "atomic_input_reservation_required") + return _reject(state, event, "atomic-input-reservation-required") if ( reservation.key.batch_id != event.batch.batch_id or reservation.occurrence_ids != event.batch.occurrence_ids @@ -968,7 +968,7 @@ def _reserve( or reservation.reserve_class is not event.batch.reserve_class or reservation.protected_pool_owner_id != event.batch.protected_pool_owner_id ): - return _reject(state, event, "reservation_descriptor_mismatch") + return _reject(state, event, "reservation-descriptor-mismatch") expected_revisions = tuple( ( record.occurrence.occurrence_id, @@ -977,7 +977,7 @@ def _reserve( for record in member_records ) if reservation.key.occurrence_revisions != expected_revisions: - return _reject(state, event, "reservation_revision_mismatch") + return _reject(state, event, "reservation-revision-mismatch") generation = event.generation_reservation generation_count = generation.maximum_allowance if generation is not None else 0 if generation is not None and any( @@ -987,7 +987,7 @@ def _reserve( return _reject( state, event, - "generation_reservation_id_reuse_with_changed_descriptor", + "generation-reservation-id-reuse-with-changed-descriptor", ) if generation is not None and ( generation.request_id != event.batch.request_id @@ -1000,7 +1000,7 @@ def _reserve( or generation.reserve_class is not event.batch.reserve_class or generation.protected_pool_owner_id != event.batch.protected_pool_owner_id ): - return _reject(state, event, "generation_descriptor_mismatch") + return _reject(state, event, "generation-descriptor-mismatch") requested = reservation.reserved_count + generation_count global_available, ordinary_available, pool_available = _capacity(state) if event.batch.protected_pool_owner_id is None: @@ -1020,7 +1020,7 @@ def _reserve( return _reject( state, event, - "insufficient_capacity", + "insufficient-capacity", requested_count=requested, reserve_class=event.batch.reserve_class, protected_pool_owner_id=event.batch.protected_pool_owner_id, @@ -1036,7 +1036,7 @@ def _reserve( None, ) if pool is None: - return _reject(state, event, "unknown_protected_pool") + return _reject(state, event, "unknown-protected-pool") batch_record = AdmissionBatchRecord( batch=event.batch, state=AdmissionState.RESERVED, @@ -1169,22 +1169,22 @@ def _prepare( event: PrepareBatchEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") record = _batch_record(state, event.batch_id) if record is None or record.state is not AdmissionState.RESERVED: - return _reject(state, event, "illegal_prepare_order") + return _reject(state, event, "illegal-prepare-order") if event.representation_revision != record.batch.manifest.representation_revision: - return _reject(state, event, "representation_revision_mismatch") + return _reject(state, event, "representation-revision-mismatch") if event.representation_binding_id != record.batch.manifest.representation_binding_id: - return _reject(state, event, "representation_binding_mismatch") + return _reject(state, event, "representation-binding-mismatch") reservation = _reservation_for(state, record) if reservation is None or event.proposed_charge != reservation.reserved_count: - return _reject(state, event, "prepared_charge_mismatch") + return _reject(state, event, "prepared-charge-mismatch") if event.measurement_kind not in { MeasurementKind.PROVIDER_EXACT, MeasurementKind.TOKENIZER_EXACT, }: - return _reject(state, event, "non_authoritative_measurement") + return _reject(state, event, "non-authoritative-measurement") updated = replace( record, state=AdmissionState.PREPARED, @@ -1215,7 +1215,7 @@ def _stage( event: StageHistoryEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") record = _batch_record(state, event.batch_id) if ( record is None @@ -1227,7 +1227,7 @@ def _stage( WitnessKind.HISTORY_STAGED, ) ): - return _reject(state, event, "invalid_history_stage_witness") + return _reject(state, event, "invalid-history-stage-witness") updated = replace( record, state=AdmissionState.HISTORY_STAGED, @@ -1263,7 +1263,7 @@ def _dispatch( event: DispatchRequestEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") record = _batch_record(state, event.batch_id) if ( record is None @@ -1275,7 +1275,7 @@ def _dispatch( WitnessKind.REQUEST_INCLUDED, ) ): - return _reject(state, event, "invalid_request_inclusion_witness") + return _reject(state, event, "invalid-request-inclusion-witness") updated = replace( record, state=AdmissionState.REQUEST_DISPATCHED, @@ -1314,7 +1314,7 @@ def _accepted_state( ) -> tuple[ActiveContextAdmissionState, AdmissionDecisionKind, str]: reservation = _reservation_for(state, record) if reservation is None: - return state, AdmissionDecisionKind.QUARANTINED, "missing_reservation" + return state, AdmissionDecisionKind.QUARANTINED, "missing-reservation" quarantined = ( exact_charge > reservation.reserved_count or exact_charge > state.snapshot.hard_limit ) @@ -1332,12 +1332,12 @@ def _accepted_state( record.batch, lifecycle, witness=witness, - quarantine_reason_code=("provider_charge_exceeds_reservation" if quarantined else None), + quarantine_reason_code=("provider-charge-exceeds-reservation" if quarantined else None), ) return ( next_state, (AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT), - "provider_charge_exceeds_reservation" if quarantined else "accepted", + "provider-charge-exceeds-reservation" if quarantined else "accepted", ) @@ -1379,10 +1379,10 @@ def _accept_closed_input( or event.authority_source != event.witness.authority_source_id or event.authority_source != binding.authority_source_id ): - return _reject(state, event, "invalid_closed_epoch_acceptance") + return _reject(state, event, "invalid-closed-epoch-acceptance") reservation = _closed_reservation_for(audit, record) if reservation is None: - return _reject(state, event, "missing_closed_epoch_reservation") + return _reject(state, event, "missing-closed-epoch-reservation") member_ids = set(record.batch.occurrence_ids) expected_owned_pairs = tuple( (span_id, item.occurrence.occurrence_id) @@ -1404,9 +1404,9 @@ def _accept_closed_input( or manifest_invalid ) reason_code = ( - "incomplete_canonical_span_ownership" + "incomplete-canonical-span-ownership" if manifest_invalid - else "provider_charge_exceeds_reservation" + else "provider-charge-exceeds-reservation" if quarantined else "accepted" ) @@ -1504,28 +1504,28 @@ def _accept( event: AcceptInputEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") record = _batch_record(state, event.batch_id) if record is None: location = _closed_batch_location(state, event.batch_id) if location is not None: return _accept_closed_input(state, event, location) if record is None or record.state is not AdmissionState.REQUEST_DISPATCHED: - return _reject(state, event, "illegal_accept_order") + return _reject(state, event, "illegal-accept-order") if not _validate_witness( state, record.batch, event.witness, WitnessKind.PROVIDER_ACCEPTED, ): - return _reject(state, event, "invalid_provider_acceptance_witness") + return _reject(state, event, "invalid-provider-acceptance-witness") binding = event.representation_binding_witness expected_revision = record.batch.manifest.representation_revision if ( event.final_manifest != record.batch.manifest or binding.representation_binding_id != record.batch.manifest.representation_binding_id ): - return _reject(state, event, "representation_binding_mismatch") + return _reject(state, event, "representation-binding-mismatch") if ( event.final_manifest_revision != expected_revision or event.final_manifest.representation_revision != expected_revision @@ -1536,11 +1536,11 @@ def _accept( or binding.request_id != record.batch.request_id or binding.batch_id != record.batch.batch_id ): - return _reject(state, event, "representation_revision_mismatch") + return _reject(state, event, "representation-revision-mismatch") if event.measurement_kind is not MeasurementKind.PROVIDER_EXACT: - return _reject(state, event, "non_authoritative_measurement") + return _reject(state, event, "non-authoritative-measurement") if event.exact_input_charge < 0: - return _reject(state, event, "invalid_exact_charge") + return _reject(state, event, "invalid-exact-charge") expected_owned_spans: list[CanonicalSpanId] = [] expected_owned_pairs: list[tuple[CanonicalSpanId, AdmissionOccurrenceId]] = [] for occurrence in state.occurrence_records: @@ -1557,7 +1557,7 @@ def _accept( event.authority_source != event.witness.authority_source_id or event.authority_source != binding.authority_source_id ): - reason_code = "authority_source_mismatch" + reason_code = "authority-source-mismatch" next_state = _quarantined_acceptance_state( state, record, @@ -1589,7 +1589,7 @@ def _accept( or set(manifest_pairs) != set(expected_owned_pairs) or len(manifest_pairs) != len(expected_owned_pairs) ): - reason_code = "incomplete_canonical_span_ownership" + reason_code = "incomplete-canonical-span-ownership" next_state = _quarantined_acceptance_state( state, record, @@ -1683,7 +1683,7 @@ def _release_closed_batch( expected_kind, ) ): - return _reject(state, event, "invalid_closed_epoch_resolution") + return _reject(state, event, "invalid-closed-epoch-resolution") lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK witness_ids = _append_witness_ids( record.witness_ids, @@ -1813,13 +1813,13 @@ def _release_or_rollback( ), ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") record = _batch_record(state, event.batch_id) if record is None: location = _closed_batch_location(state, event.batch_id) if location is not None: return _release_closed_batch(state, event, location) - return _reject(state, event, "unknown_batch") + return _reject(state, event, "unknown-batch") is_release = isinstance( event, ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, @@ -1848,14 +1848,14 @@ def _release_or_rollback( record.batch.protected_pool_owner_id is not None and required_release_kind is not expected_witness ): - return _reject(state, event, "protected_release_policy_mismatch") + return _reject(state, event, "protected-release-policy-mismatch") if record.state not in allowed_states or not _validate_witness( state, record.batch, event.witness, expected_witness, ): - return _reject(state, event, "invalid_release_or_rollback_witness") + return _reject(state, event, "invalid-release-or-rollback-witness") lifecycle = AdmissionState.RELEASED if is_release else AdmissionState.ROLLED_BACK updated = replace( record, @@ -1946,14 +1946,14 @@ def _mark_indeterminate( event: MarkIndeterminateEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") record = _batch_record(state, event.batch_id) if record is None or record.state not in { AdmissionState.PREPARED, AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED, }: - return _reject(state, event, "illegal_indeterminate_order") + return _reject(state, event, "illegal-indeterminate-order") reservation = _reservation_for(state, record) unresolved = reservation.reserved_count if reservation is not None else 0 updated = replace( @@ -1988,7 +1988,7 @@ def _resolve_indeterminate_accepted( event: ResolveIndeterminateAcceptedEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") record = _batch_record(state, event.batch_id) if record is None: location = _closed_batch_location(state, event.batch_id) @@ -2021,7 +2021,7 @@ def _resolve_indeterminate_accepted( or binding.request_id != record.batch.request_id or binding.batch_id != record.batch.batch_id ): - return _reject(state, event, "invalid_indeterminate_acceptance") + return _reject(state, event, "invalid-indeterminate-acceptance") member_ids = set(record.batch.occurrence_ids) expected_owned_pairs = tuple( (span_id, item.occurrence.occurrence_id) @@ -2037,7 +2037,7 @@ def _resolve_indeterminate_accepted( or set(manifest_pairs) != set(expected_owned_pairs) or len(manifest_pairs) != len(expected_owned_pairs) ): - reason_code = "incomplete_canonical_span_ownership" + reason_code = "incomplete-canonical-span-ownership" next_state = _quarantined_acceptance_state( state, record, @@ -2095,10 +2095,10 @@ def _start_generation( event: StartGenerationEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") generation = _generation_record(state, event.generation_reservation_id) if generation is None or generation.state is not GenerationState.RESERVED: - return _reject(state, event, "illegal_generation_start") + return _reject(state, event, "illegal-generation-start") batch_record = _batch_record(state, generation.batch_id) batch = batch_record.batch if batch_record is not None else None if ( @@ -2116,7 +2116,7 @@ def _start_generation( and generation.authority_source_id != event.witness.authority_source_id ) ): - return _reject(state, event, "invalid_generation_start_witness") + return _reject(state, event, "invalid-generation-start-witness") updated = replace( generation, state=GenerationState.STREAMING, @@ -2171,7 +2171,7 @@ def _reconcile_closed_generation( and generation.authority_source_id != event.output_usage_witness.authority_source_id ) ): - return _reject(state, event, "invalid_closed_generation_witness") + return _reject(state, event, "invalid-closed-generation-witness") quarantined = event.exact_output_usage > generation.maximum_allowance updated = replace( generation, @@ -2219,7 +2219,7 @@ def _reconcile_closed_generation( witness_ids=(event.output_usage_witness.witness_id,), ), ) - reason_code = "generation_usage_exceeds_allowance" if quarantined else "accepted" + reason_code = "generation-usage-exceeds-allowance" if quarantined else "accepted" if quarantined: effects += ( QuarantineRecordedEffect( @@ -2248,7 +2248,7 @@ def _reconcile_generation( event: ReconcileGenerationEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") generation = _generation_record(state, event.generation_reservation_id) if generation is None: location = _closed_generation_location( @@ -2261,7 +2261,7 @@ def _reconcile_generation( GenerationState.STREAMING, GenerationState.INDETERMINATE, }: - return _reject(state, event, "illegal_generation_reconciliation") + return _reject(state, event, "illegal-generation-reconciliation") batch_record = _batch_record(state, generation.batch_id) if ( batch_record is None @@ -2276,7 +2276,7 @@ def _reconcile_generation( and generation.authority_source_id != event.output_usage_witness.authority_source_id ) ): - return _reject(state, event, "invalid_output_usage_witness") + return _reject(state, event, "invalid-output-usage-witness") quarantined = event.exact_output_usage > generation.maximum_allowance updated = replace( generation, @@ -2320,7 +2320,7 @@ def _reconcile_generation( resulting_aggregate_revision=revision, resulting_admission_sequence=sequence, target_id=generation.generation_reservation_id, - reason_code="generation_usage_exceeds_allowance", + reason_code="generation-usage-exceeds-allowance", ), ) return _publish( @@ -2330,7 +2330,7 @@ def _reconcile_generation( kind=( AdmissionDecisionKind.QUARANTINED if quarantined else AdmissionDecisionKind.WOULD_ADMIT ), - reason_code=("generation_usage_exceeds_allowance" if quarantined else "accepted"), + reason_code=("generation-usage-exceeds-allowance" if quarantined else "accepted"), capacity_changed=True, effects=effects, ) @@ -2341,13 +2341,13 @@ def _mark_generation_indeterminate( event: MarkGenerationIndeterminateEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") generation = _generation_record(state, event.generation_reservation_id) if generation is None or generation.state not in { GenerationState.RESERVED, GenerationState.STREAMING, }: - return _reject(state, event, "illegal_generation_indeterminate") + return _reject(state, event, "illegal-generation-indeterminate") updated = replace(generation, state=GenerationState.INDETERMINATE) next_state = replace( state, @@ -2366,7 +2366,7 @@ def _request_reconciliation( event: RequestReconciliationEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") batch = ( _batch_record(state, event.target_id) if isinstance(event.target_id, AdmissionBatchId) @@ -2426,7 +2426,7 @@ def _request_reconciliation( } ) ): - return _reject(state, event, "reconciliation_target_not_unresolved") + return _reject(state, event, "reconciliation-target-not-unresolved") revision, sequence = _effect_coordinates(state, capacity_changed=False) effect_type = ( ReconciliationEscalationEffect @@ -2462,15 +2462,15 @@ def _expire_idempotency( None, ) if record is None: - return _reject(state, event, "idempotency_key_not_terminal") + return _reject(state, event, "idempotency-key-not-terminal") if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "idempotency_key_not_terminal") + return _reject(state, event, "idempotency-key-not-terminal") if any( tombstone.namespace == record.namespace and tombstone.reservation_key == event.reservation_key for tombstone in state.expired_idempotency_tombstones ): - return _reject(state, event, "idempotency_key_expired") + return _reject(state, event, "idempotency-key-expired") batch_record: AdmissionBatchRecord | None snapshot: ContextWindowSnapshot generation_records: tuple[GenerationReservationRecord, ...] @@ -2495,7 +2495,7 @@ def _expire_idempotency( None, ) if audit is None: - return _reject(state, event, "idempotency_key_not_terminal") + return _reject(state, event, "idempotency-key-not-terminal") batch_record = next( ( item @@ -2513,7 +2513,7 @@ def _expire_idempotency( AdmissionState.INVALIDATED, AdmissionState.QUARANTINED, }: - return _reject(state, event, "idempotency_key_not_terminal") + return _reject(state, event, "idempotency-key-not-terminal") if any( generation.batch_id == batch_record.batch.batch_id and generation.state @@ -2524,14 +2524,14 @@ def _expire_idempotency( } for generation in generation_records ): - return _reject(state, event, "idempotency_key_not_terminal") + return _reject(state, event, "idempotency-key-not-terminal") if not _validate_witness_for_snapshot( snapshot, batch_record.batch, event.expiry_witness, WitnessKind.IDEMPOTENCY_EXPIRY, ): - return _reject(state, event, "invalid_expiry_witness") + return _reject(state, event, "invalid-expiry-witness") tombstone = ExpiredIdempotencyTombstone( namespace=record.namespace, reservation_key=record.reservation_key, @@ -2575,7 +2575,7 @@ def _rollover( event: RolloverEpochEvent, ) -> AdmissionTransition: if not isinstance(state, ActiveContextAdmissionState): - return _reject(state, event, "epoch_uninitialized") + return _reject(state, event, "epoch-uninitialized") proof = event.fence_proof if ( event.witness.kind is not WitnessKind.EPOCH_ROLLOVER @@ -2585,7 +2585,7 @@ def _rollover( or event.new_snapshot.window_epoch_number <= state.snapshot.window_epoch_number or event.new_snapshot.window_epoch_id == state.snapshot.window_epoch_id ): - return _reject(state, event, "invalid_rollover_witness") + return _reject(state, event, "invalid-rollover-witness") unresolved_batch_records = tuple( record for record in state.batch_records @@ -2645,7 +2645,7 @@ def _rollover( else (fully_resolved or snapshot_deducts_unresolved) ) if not authority_alternative_valid: - return _reject(state, event, "stale_receiver_fence") + return _reject(state, event, "stale-receiver-fence") terminal_occurrences = tuple( replace( record, @@ -2737,7 +2737,7 @@ def _rollover( ), ) except ContextAdmissionValidationError: - return _reject(state, event, "invalid_rollover_snapshot") + return _reject(state, event, "invalid-rollover-snapshot") revision, sequence = _effect_coordinates(state, capacity_changed=True) rollover_witness_ids = _append_witness_ids( (), @@ -2947,5 +2947,5 @@ def resolve_context_admission_coverage( row, observation_state=CoverageState.UPSTREAM_GATED, authority_state=CoverageState.UPSTREAM_GATED, - reason_code="coverage_runtime_mismatch", + reason_code="coverage-runtime-mismatch", ) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index e2a836d82..b2eb0a127 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -43,6 +43,7 @@ _CONTENT_FREE_TEXT = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:@+-]*\Z") _CONTENT_FREE_LOCATOR = re.compile(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]*\Z") +_REASON_CODE = re.compile(r"[a-z][a-z0-9-]{0,63}\Z") _SENSITIVE_TEXT_MARKERS = ( "authorization", "bearer", @@ -119,6 +120,15 @@ def _validate_bounded_text( _raise_invalid(reason_code) +def _validate_reason_code( + value: str, + validation_error: str = "invalid_reason_code", +) -> None: + _validate_bounded_text(value, validation_error, maximum=64) + if not _REASON_CODE.fullmatch(value): + _raise_invalid(validation_error) + + def _validate_tuple(value: object, reason_code: str) -> None: if not isinstance(value, tuple): _raise_invalid(reason_code) @@ -823,7 +833,7 @@ class AdmissionDecision(_ContractValue): available_protected_count: int def __post_init__(self) -> None: - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) for value in ( self.requested_count, self.available_ordinary_count, @@ -853,16 +863,14 @@ def __post_init__(self) -> None: if len(self.accepted_witness_ids) != len(set(self.accepted_witness_ids)): _raise_invalid("duplicate_witness_id") if self.indeterminate_reason_code is not None: - _validate_bounded_text( + _validate_reason_code( self.indeterminate_reason_code, "invalid_indeterminate_reason", - maximum=64, ) if self.quarantine_reason_code is not None: - _validate_bounded_text( + _validate_reason_code( self.quarantine_reason_code, "invalid_quarantine_reason", - maximum=64, ) @@ -1029,7 +1037,7 @@ def __post_init__(self) -> None: if batch_record.reservation_id is not None: reservation = reservation_by_id.get(batch_record.reservation_id) if reservation is None or reservation.key.batch_id != batch_record.batch.batch_id: - _raise_invalid("missing_closed_epoch_reservation") + _raise_invalid("missing-closed-epoch-reservation") for generation in self.terminal_generation_reservations: generation_batch = batch_by_id.get(generation.batch_id) if ( @@ -1118,7 +1126,7 @@ def __post_init__(self) -> None: "invalid_control_point_owner", maximum=96, ) - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) _validate_canonical_tuple( self.evidence, "noncanonical_coverage_evidence", @@ -1176,7 +1184,7 @@ class AuthorityUnavailableEvent(_AdmissionEventBase): def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) if self.authority_state is CoverageState.VERIFIED: _raise_invalid("invalid_unavailable_authority_state") @@ -1247,7 +1255,7 @@ def __post_init__(self) -> None: MeasurementKind.HOST_ESTIMATE, MeasurementKind.BYTE_EMERGENCY, }: - _raise_invalid("non_authoritative_measurement") + _raise_invalid("non-authoritative-measurement") @dataclass(frozen=True, slots=True) @@ -1277,7 +1285,7 @@ def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) _validate_non_negative(self.exact_input_charge, "invalid_exact_input_charge") if self.measurement_kind is not MeasurementKind.PROVIDER_EXACT: - _raise_invalid("non_authoritative_measurement") + _raise_invalid("non-authoritative-measurement") @dataclass(frozen=True, slots=True) @@ -1299,7 +1307,7 @@ class MarkIndeterminateEvent(_AdmissionEventBase): def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) @dataclass(frozen=True, slots=True) @@ -1315,9 +1323,9 @@ class ResolveIndeterminateAcceptedEvent(_AdmissionEventBase): def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) - _validate_non_negative(self.exact_charge, "invalid_exact_charge") + _validate_non_negative(self.exact_charge, "invalid-exact-charge") if self.measurement_kind is not MeasurementKind.PROVIDER_EXACT: - _raise_invalid("non_authoritative_measurement") + _raise_invalid("non-authoritative-measurement") @dataclass(frozen=True, slots=True) @@ -1356,7 +1364,7 @@ class MarkGenerationIndeterminateEvent(_AdmissionEventBase): def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) @dataclass(frozen=True, slots=True) @@ -1366,7 +1374,7 @@ class RequestReconciliationEvent(_AdmissionEventBase): def __post_init__(self) -> None: _AdmissionEventBase.__post_init__(self) - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) @dataclass(frozen=True, slots=True) @@ -1588,7 +1596,7 @@ def __post_init__(self) -> None: _AdmissionEffectBase.__post_init__(self) if not isinstance(self.target_id, AdmissionBatchId | GenerationReservationId): _raise_invalid("invalid_effect_target") - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) @dataclass(frozen=True, slots=True) @@ -1600,7 +1608,7 @@ def __post_init__(self) -> None: _AdmissionEffectBase.__post_init__(self) if not isinstance(self.target_id, AdmissionBatchId | GenerationReservationId): _raise_invalid("invalid_effect_target") - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) @dataclass(frozen=True, slots=True) @@ -1612,7 +1620,7 @@ def __post_init__(self) -> None: _AdmissionEffectBase.__post_init__(self) if not isinstance(self.target_id, AdmissionEventId): _raise_invalid("invalid_effect_target") - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) @dataclass(frozen=True, slots=True) @@ -1677,7 +1685,7 @@ def __post_init__(self) -> None: _AdmissionEffectBase.__post_init__(self) if not isinstance(self.target_id, AdmissionBatchId | GenerationReservationId): _raise_invalid("invalid_effect_target") - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) @dataclass(frozen=True, slots=True) @@ -1690,7 +1698,7 @@ def __post_init__(self) -> None: _AdmissionEffectBase.__post_init__(self) if not isinstance(self.target_id, WindowEpochId): _raise_invalid("invalid_effect_target") - _validate_bounded_text(self.reason_code, "invalid_reason_code", maximum=64) + _validate_reason_code(self.reason_code) if self.authority_state is CoverageState.VERIFIED: _raise_invalid("invalid_unavailable_authority_state") @@ -1967,7 +1975,7 @@ def __post_init__(self) -> None: or set(owned_pairs) != set(manifest_pairs) or len(owned_pairs) != len(manifest_pairs) ): - _raise_invalid("inconsistent_span_ownership") + _raise_invalid("inconsistent-span-ownership") for reservation in self.reservations: matching_batch = batch_records_by_id.get(reservation.key.batch_id) if matching_batch is None: @@ -2198,7 +2206,7 @@ def _coverage_row(surface: ProducerSurface) -> ProducerCoverageDef: observation_state=observation_state, authority_state=CoverageState.UPSTREAM_GATED, evidence=(evidence,), - reason_code="authoritative_watermark_unavailable", + reason_code="authoritative-watermark-unavailable", ) diff --git a/tests/core/test_context_admission_coverage.py b/tests/core/test_context_admission_coverage.py index eaf372de2..67b98a9db 100644 --- a/tests/core/test_context_admission_coverage.py +++ b/tests/core/test_context_admission_coverage.py @@ -19,7 +19,7 @@ CHECKED_AT = "2026-07-23" FRESHNESS_POLICY = "verify_on_version_or_configuration_change" CODEX_REVISION = "25af12f7e61572b0bc18ddb1008be543b91519b0" -REASON_CODE = "authoritative_watermark_unavailable" +REASON_CODE = "authoritative-watermark-unavailable" class ExpectedEvidence(NamedTuple): diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index efbd95b90..e6c550957 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -616,7 +616,7 @@ def test_reserve_rejects_active_generation_reservation_id_reuse() -> None: assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT assert ( - rejected.decision.reason_code == "generation_reservation_id_reuse_with_changed_descriptor" + rejected.decision.reason_code == "generation-reservation-id-reuse-with-changed-descriptor" ) assert isinstance(rejected.next_state, ActiveContextAdmissionState) _assert_rejection_unchanged(state, rejected.next_state) @@ -1196,7 +1196,7 @@ def test_incomplete_manifest_rejection_is_non_mutating_and_retains_reservation() ) rejected_acceptance = reduce_context_admission(state, event) assert rejected_acceptance.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected_acceptance.decision.reason_code == "representation_binding_mismatch" + assert rejected_acceptance.decision.reason_code == "representation-binding-mismatch" _assert_rejection_unchanged(state, rejected_acceptance.next_state) assert rejected_acceptance.effects == () @@ -1266,7 +1266,7 @@ def test_fork_requires_distinct_epoch_and_parent_accepts_only_delivery() -> None ) rejected_fork = reduce_context_admission(state, fork_event) assert rejected_fork.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected_fork.decision.reason_code == "fork_requires_distinct_epoch" + assert rejected_fork.decision.reason_code == "fork-requires-distinct-epoch" assert isinstance(rejected_fork.next_state, ActiveContextAdmissionState) state = rejected_fork.next_state state, _ = _propose(state, occurrences[2]) @@ -1382,7 +1382,7 @@ def test_closed_epoch_occurrence_identity_is_immutable_and_exact_retry_is_idempo ) rejected = reduce_context_admission(current, changed) assert rejected.decision.kind is AdmissionDecisionKind.QUARANTINED - assert rejected.decision.reason_code == "occurrence_identity_corruption" + assert rejected.decision.reason_code == "occurrence-identity-corruption" _assert_rejection_unchanged(current, rejected.next_state) current = rejected.next_state @@ -1504,7 +1504,7 @@ def test_protected_pool_release_policy_accepts_only_its_configured_resolution( rejected = reduce_context_admission(state, rejected_event) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected.decision.reason_code == "protected_release_policy_mismatch" + assert rejected.decision.reason_code == "protected-release-policy-mismatch" _assert_rejection_unchanged(state, rejected.next_state) allowed = reduce_context_admission(state, allowed_event) @@ -1757,7 +1757,7 @@ def test_post_rollover_expiry_cannot_tombstone_retained_work( ) rejected = reduce_context_admission(rolled, expiry) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected.decision.reason_code == "idempotency_key_not_terminal" + assert rejected.decision.reason_code == "idempotency-key-not-terminal" assert rejected.next_state.expired_idempotency_tombstones == () assert rejected.next_state.closed_epochs[-1] == retained_before _assert_rejection_unchanged(rolled, rejected.next_state) @@ -1799,7 +1799,7 @@ def test_post_rollover_expiry_requires_the_complete_witness_binding() -> None: ) rejected = reduce_context_admission(current, expiry) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert rejected.decision.reason_code == "invalid_expiry_witness" + assert rejected.decision.reason_code == "invalid-expiry-witness" assert rejected.next_state.expired_idempotency_tombstones == () _assert_rejection_unchanged(current, rejected.next_state) current = rejected.next_state @@ -1833,7 +1833,7 @@ def test_post_rollover_expiry_requires_the_complete_witness_binding() -> None: ), ) assert repeated.decision.kind is AdmissionDecisionKind.WOULD_REJECT - assert repeated.decision.reason_code == "idempotency_key_expired" + assert repeated.decision.reason_code == "idempotency-key-expired" assert len(repeated.next_state.expired_idempotency_tombstones) == 1 _assert_rejection_unchanged(accepted.next_state, repeated.next_state) diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index 8daa0c2cb..46ba54d0b 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1156,6 +1156,29 @@ def test_free_text_and_model_identity_privacy_is_fail_closed(canary: str) -> Non _snapshot(model_identity=ModelIdentity.anthropic(canary)) +@pytest.mark.parametrize( + "invalid_reason_code", + [ + "Uppercase", + "under_score", + "dot.value", + "colon:value", + ], +) +def test_reason_codes_require_lowercase_kebab_case(invalid_reason_code: str) -> None: + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionDecision( + kind=AdmissionDecisionKind.WOULD_REJECT, + reason_code=invalid_reason_code, + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + requested_count=0, + available_ordinary_count=0, + available_protected_count=0, + ) + assert "invalid_reason_code" in str(exc_info.value) + + def test_aggregate_tuples_reject_noncanonical_ordering() -> None: first = _occurrence("occurrence-a", span_id="span-a") second = _occurrence("occurrence-b", span_id="span-b") @@ -1461,7 +1484,7 @@ def test_prepare_event_rejects_estimate_measurement() -> None: measurement_kind=MeasurementKind.HOST_ESTIMATE, authority_source=AuthoritySourceId("authority-test"), ) - assert "non_authoritative_measurement" in str(exc_info.value) + assert "non-authoritative-measurement" in str(exc_info.value) def test_accept_event_rejects_estimate_measurement() -> None: @@ -1482,4 +1505,4 @@ def test_accept_event_rejects_estimate_measurement() -> None: authority_source=AuthoritySourceId("authority-test"), representation_binding_witness=_binding(batch), ) - assert "non_authoritative_measurement" in str(exc_info.value) + assert "non-authoritative-measurement" in str(exc_info.value) From faf8ccba95713807a9a61e23cbdb7b43e8c8e15e Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:13:03 -0700 Subject: [PATCH 20/62] fix(review): enforce batch lifecycle count invariants --- .../core/types/_type_context_admission.py | 7 ++++ .../types/test_context_admission_contract.py | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index b2eb0a127..fe5b5f2d3 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -895,6 +895,13 @@ def __post_init__(self) -> None: _validate_non_negative(self.unresolved_input_count, "invalid_unresolved_count") if self.committed_input_count > 0 and self.unresolved_input_count > 0: _raise_invalid("committed_and_unresolved_simultaneously") + if self.committed_input_count > 0 and self.state not in { + AdmissionState.COMMITTED, + AdmissionState.QUARANTINED, + }: + _raise_invalid("committed_count_for_nonterminal_batch") + if self.unresolved_input_count > 0 and self.state is not AdmissionState.INDETERMINATE: + _raise_invalid("unresolved_count_for_resolved_batch") @dataclass(frozen=True, slots=True) diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index 46ba54d0b..1113ba587 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1439,6 +1439,43 @@ def test_batch_record_rejects_committed_and_unresolved_simultaneously() -> None: assert "committed_and_unresolved_simultaneously" in str(exc_info.value) +@pytest.mark.parametrize( + ("state", "committed_count", "unresolved_count", "reason_code"), + [ + ( + AdmissionState.RESERVED, + 1, + 0, + "committed_count_for_nonterminal_batch", + ), + ( + AdmissionState.REQUEST_DISPATCHED, + 0, + 1, + "unresolved_count_for_resolved_batch", + ), + ], +) +def test_batch_record_counts_match_lifecycle_state( + state: AdmissionState, + committed_count: int, + unresolved_count: int, + reason_code: str, +) -> None: + occurrence = _occurrence() + batch = _batch("batch-lifecycle-counts", (occurrence,)) + with pytest.raises(ContextAdmissionValidationError) as exc_info: + AdmissionBatchRecord( + batch=batch, + state=state, + reservation_id=AdmissionReservationId("reservation-lifecycle-counts"), + witness_ids=(), + committed_input_count=committed_count, + unresolved_input_count=unresolved_count, + ) + assert reason_code in str(exc_info.value) + + def test_privacy_canaries_are_rejected_from_coverage_evidence() -> None: with pytest.raises(ContextAdmissionValidationError): CoverageEvidence( From 0fca35c7e09bc10f426fe3ef5fba52d4112b0a29 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:14:39 -0700 Subject: [PATCH 21/62] fix(review): validate coverage evidence provenance --- docs/decisions/0007-context-admission.md | 3 +- .../core/types/_type_context_admission.py | 34 +++++++++++++++-- tests/core/test_context_admission_coverage.py | 2 +- .../types/test_context_admission_contract.py | 38 ++++++++++++++++++- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/docs/decisions/0007-context-admission.md b/docs/decisions/0007-context-admission.md index 3d0025621..7ab69544e 100644 --- a/docs/decisions/0007-context-admission.md +++ b/docs/decisions/0007-context-admission.md @@ -198,7 +198,8 @@ complete, version-pinned projection of `CONTEXT_ADMISSION_COVERAGE`; each row ha | `PARENT_VISIBLE_CHILD_DELIVERY` | `child_delivery_receipt` | `VERIFIED` | `UPSTREAM_GATED` | | `COMPACTION_MODEL_WINDOW_TRANSITION` | `compaction_receiver` | `PARTIAL` | `UPSTREAM_GATED` | -`VERIFIED` rows use AutoSkillit source at revision `ac8f653a00d2`. Codex-backed rows use +`VERIFIED` rows use AutoSkillit source at revision +`ac8f653a00d24b6be50ef285958cfb0e1b7a351b`. Codex-backed rows use codex-cli `0.145.0` at revision `25af12f7e61572b0bc18ddb1008be543b91519b0`. `CLIENT_PROVIDER_RETRIEVAL` and `OTHER_CONTEXT_INJECTION` are explicitly inference-backed gap claims, not verified source claims. Static source pins are documentation provenance, not runtime diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index fe5b5f2d3..4a86cf21e 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -10,6 +10,7 @@ import re from collections.abc import Callable, Mapping from dataclasses import dataclass, field, fields +from datetime import date from enum import StrEnum from types import UnionType from typing import ( @@ -44,6 +45,15 @@ _CONTENT_FREE_TEXT = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:@+-]*\Z") _CONTENT_FREE_LOCATOR = re.compile(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]*\Z") _REASON_CODE = re.compile(r"[a-z][a-z0-9-]{0,63}\Z") +_GIT_REVISION = re.compile(r"[0-9a-fA-F]{40}\Z") +_ISO_DATE = re.compile(r"\d{4}-\d{2}-\d{2}\Z") +_FRESHNESS_POLICIES = frozenset( + { + "verify_on_version_or_configuration_change", + "verify_on_revision_change", + "infer_only", + } +) _SENSITIVE_TEXT_MARKERS = ( "authorization", "bearer", @@ -129,6 +139,15 @@ def _validate_reason_code( _raise_invalid(validation_error) +def _validate_iso_date(value: str) -> None: + if not isinstance(value, str) or not _ISO_DATE.fullmatch(value): + _raise_invalid("invalid_checked_at") + try: + date.fromisoformat(value) + except ValueError: + raise ContextAdmissionValidationError("invalid_checked_at") from None + + def _validate_tuple(value: object, reason_code: str) -> None: if not isinstance(value, tuple): _raise_invalid(reason_code) @@ -1105,11 +1124,18 @@ def __post_init__(self) -> None: (self.configuration_mode, "invalid_configuration_mode", 64), (self.verifier, "invalid_evidence_verifier", 64), (self.tested_version, "invalid_tested_version", 64), - (self.tested_revision, "invalid_tested_revision", 96), - (self.checked_at, "invalid_checked_at", 32), - (self.freshness_policy, "invalid_freshness_policy", 128), ): _validate_bounded_text(value, reason, maximum=maximum) + if not isinstance(self.tested_revision, str) or not _GIT_REVISION.fullmatch( + self.tested_revision + ): + _raise_invalid("invalid_tested_revision") + _validate_iso_date(self.checked_at) + if ( + not isinstance(self.freshness_policy, str) + or self.freshness_policy not in _FRESHNESS_POLICIES + ): + _raise_invalid("invalid_freshness_policy") _validate_bounded_text( self.source_locator, "invalid_source_locator", @@ -2164,7 +2190,7 @@ def _coverage_row(surface: ProducerSurface) -> ProducerCoverageDef: verifier = "source_inspection" locator = _LOCAL_SOURCE_LOCATORS[surface] version = "0.10.890" - revision = "ac8f653a00d2" + revision = "ac8f653a00d24b6be50ef285958cfb0e1b7a351b" elif surface in _UNOBSERVABLE_SURFACES: observation_state = CoverageState.UPSTREAM_GATED evidence_kind = CoverageEvidenceKind.INFERENCE diff --git a/tests/core/test_context_admission_coverage.py b/tests/core/test_context_admission_coverage.py index 67b98a9db..24701fe0c 100644 --- a/tests/core/test_context_admission_coverage.py +++ b/tests/core/test_context_admission_coverage.py @@ -53,7 +53,7 @@ def _autoskillit_evidence(claim_id: str, source_locator: str) -> ExpectedEvidenc "source_inspection", source_locator, "0.10.890", - "ac8f653a00d2", + "ac8f653a00d24b6be50ef285958cfb0e1b7a351b", CHECKED_AT, FRESHNESS_POLICY, ) diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index 1113ba587..f42f1fbc8 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1486,7 +1486,7 @@ def test_privacy_canaries_are_rejected_from_coverage_evidence() -> None: verifier="source_inspection", source_locator="/home/alice/private/source.py", tested_version="0.10.890", - tested_revision="ac8f653a00d2", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", checked_at="2026-07-23", freshness_policy="verify_on_version_or_configuration_change", ) @@ -1499,12 +1499,46 @@ def test_privacy_canaries_are_rejected_from_coverage_evidence() -> None: verifier="source_inspection", source_locator="~alice/private", tested_version="0.10.890", - tested_revision="ac8f653a00d2", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", checked_at="2026-07-23", freshness_policy="verify_on_version_or_configuration_change", ) +@pytest.mark.parametrize( + ("field_name", "invalid_value", "reason_code"), + [ + ("tested_revision", "abc123", "invalid_tested_revision"), + ("tested_revision", "g" * 40, "invalid_tested_revision"), + ("checked_at", "2026-7-3", "invalid_checked_at"), + ("checked_at", "2026-02-30", "invalid_checked_at"), + ("freshness_policy", "always_fresh", "invalid_freshness_policy"), + ], +) +def test_coverage_evidence_enforces_semantic_provenance_fields( + field_name: str, + invalid_value: str, + reason_code: str, +) -> None: + evidence = CoverageEvidence( + claim_id="COV-semantic-fields", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="src/autoskillit/core/context_admission.py", + tested_version="0.10.890", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + replace(evidence, **{field_name: invalid_value}) + + assert reason_code in str(exc_info.value) + + def test_prepare_event_rejects_estimate_measurement() -> None: occurrence = _occurrence() batch = _batch("batch-estimate", (occurrence,)) From 589756aeea34a1667350d79ca26ea72ee38b415b Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:16:19 -0700 Subject: [PATCH 22/62] fix(review): validate journal identity duplication --- .../core/types/_type_context_admission.py | 14 ++++ .../types/test_context_admission_contract.py | 80 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 4a86cf21e..e6e4fa031 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -1762,6 +1762,10 @@ class ProcessedEventRecord(_ContractValue): aggregate_revision: AggregateRevision admission_sequence: AdmissionSequence + def __post_init__(self) -> None: + if self.event_id != self.event.event_id: + _raise_invalid("processed_event_identity_mismatch") + @dataclass(frozen=True, slots=True) class IdempotencyRecord(_ContractValue): @@ -1772,6 +1776,16 @@ class IdempotencyRecord(_ContractValue): owning_event_id: AdmissionEventId publication_revision: AggregateRevision + def __post_init__(self) -> None: + input_reservations = self.original_descriptor.input_reservations + if ( + self.namespace != self.original_descriptor.idempotency_namespace + or len(input_reservations) != 1 + or self.reservation_key != input_reservations[0].key + or self.owning_event_id != self.original_descriptor.event_id + ): + _raise_invalid("idempotency_record_identity_mismatch") + def _validate_state_metadata_uniqueness( processed_events: tuple[ProcessedEventRecord, ...], diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index f42f1fbc8..bd7253f5f 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -77,6 +77,7 @@ RepresentationRevision, ReservationRecordedEffect, ReserveClass, + ReserveRequestEvent, TokenizerIdentity, ToolCallId, TurnId, @@ -1476,6 +1477,85 @@ def test_batch_record_counts_match_lifecycle_state( assert reason_code in str(exc_info.value) +def test_journal_records_cross_check_duplicated_event_identities() -> None: + occurrence = _occurrence() + batch = _batch("batch-journal-identities", (occurrence,)) + namespace = _namespace("reserve-request") + reservation_key = AdmissionReservationKey( + idempotency_namespace=namespace, + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + batch_id=batch.batch_id, + reserve_class=batch.reserve_class, + protected_pool_owner_id=None, + occurrence_revisions=((occurrence.occurrence_id, occurrence.representation_revision),), + ) + reservation = AdmissionReservation( + reservation_id=AdmissionReservationId("reservation-journal-identities"), + key=reservation_key, + window_epoch_id=WindowEpochId("epoch-1"), + window_epoch_number=1, + snapshot_sequence=1, + reserve_class=batch.reserve_class, + protected_pool_owner_id=None, + occurrence_ids=batch.occurrence_ids, + reserved_count=5, + ) + event = ReserveRequestEvent( + event_id=AdmissionEventId("event-journal-identities"), + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + idempotency_namespace=namespace, + expected_aggregate_revision=AggregateRevision(0), + batch=batch, + snapshot_sequence=1, + input_reservations=(reservation,), + generation_reservation=None, + ) + decision = AdmissionDecision( + kind=AdmissionDecisionKind.WOULD_ADMIT, + reason_code="accepted", + window_epoch_id=WindowEpochId("epoch-1"), + snapshot_sequence=1, + requested_count=5, + available_ordinary_count=35, + available_protected_count=0, + ) + processed = ProcessedEventRecord( + event_id=event.event_id, + event=event, + original_decision=decision, + aggregate_revision=AggregateRevision(1), + admission_sequence=AdmissionSequence(1), + ) + with pytest.raises(ContextAdmissionValidationError) as processed_error: + replace(processed, event_id=AdmissionEventId("different-processed-event")) + assert "processed_event_identity_mismatch" in str(processed_error.value) + + idempotency = IdempotencyRecord( + namespace=namespace, + reservation_key=reservation_key, + original_descriptor=event, + original_reserve_decision=decision, + owning_event_id=event.event_id, + publication_revision=AggregateRevision(1), + ) + mismatches = ( + {"namespace": _namespace("different-operation")}, + { + "reservation_key": replace( + reservation_key, + batch_id=AdmissionBatchId("different-reservation-key"), + ) + }, + {"owning_event_id": AdmissionEventId("different-owning-event")}, + ) + for mismatch in mismatches: + with pytest.raises(ContextAdmissionValidationError) as idempotency_error: + replace(idempotency, **mismatch) + assert "idempotency_record_identity_mismatch" in str(idempotency_error.value) + + def test_privacy_canaries_are_rejected_from_coverage_evidence() -> None: with pytest.raises(ContextAdmissionValidationError): CoverageEvidence( From 2f928842dca281e7db1237d9f3c35a730dbec17a Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:17:49 -0700 Subject: [PATCH 23/62] fix(review): reject global context overallocation --- src/autoskillit/core/context_admission.py | 28 +++----------- .../core/types/_type_context_admission.py | 38 +++++++++++++++++++ tests/core/test_context_admission_reducer.py | 19 ++++++++++ 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index e567388f7..fb136b0f2 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -232,21 +232,7 @@ def _capacity( for record in state.batch_records: reservation = _reservation_for(state, record) - if record.state in {AdmissionState.COMMITTED, AdmissionState.QUARANTINED}: - charged = record.committed_input_count - elif record.state is AdmissionState.INDETERMINATE: - charged = record.unresolved_input_count - if charged == 0 and reservation is not None: - charged = reservation.reserved_count - elif record.state in { - AdmissionState.RESERVED, - AdmissionState.PREPARED, - AdmissionState.HISTORY_STAGED, - AdmissionState.REQUEST_DISPATCHED, - }: - charged = reservation.reserved_count if reservation is not None else 0 - else: - charged = 0 + charged = record.charged_input_count(reservation) global_charged += charged owner = record.batch.protected_pool_owner_id if owner is not None: @@ -254,17 +240,13 @@ def _capacity( charged_by_pool[key] = charged_by_pool.get(key, 0) + charged for generation in state.generation_reservations: - if generation.state in { - GenerationState.RESERVED, - GenerationState.STREAMING, - GenerationState.INDETERMINATE, - GenerationState.QUARANTINED, - }: - global_charged += generation.maximum_allowance + generation_charge = generation.charged_output_count() + if generation_charge > 0: + global_charged += generation_charge owner = generation.protected_pool_owner_id if owner is not None: key = (generation.reserve_class, owner) - charged_by_pool[key] = charged_by_pool.get(key, 0) + generation.maximum_allowance + charged_by_pool[key] = charged_by_pool.get(key, 0) + generation_charge global_unallocated = max(state.snapshot.remaining_count - global_charged, 0) pool_available: dict[tuple[ReserveClass, ProtectedPoolOwnerId], int] = {} diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index e6e4fa031..640a6d541 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -902,6 +902,23 @@ class AdmissionBatchRecord(_ContractValue): committed_input_count: int unresolved_input_count: int + def charged_input_count(self, reservation: AdmissionReservation | None) -> int: + """Return the capacity charge represented by this lifecycle record.""" + if self.state in {AdmissionState.COMMITTED, AdmissionState.QUARANTINED}: + return self.committed_input_count + if self.state is AdmissionState.INDETERMINATE: + return self.unresolved_input_count or ( + reservation.reserved_count if reservation is not None else 0 + ) + if self.state in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + }: + return reservation.reserved_count if reservation is not None else 0 + return 0 + def __post_init__(self) -> None: _validate_canonical_tuple( self.witness_ids, @@ -942,6 +959,17 @@ class GenerationReservationRecord(_ContractValue): witness_ids: tuple[AdmissionWitnessId, ...] authority_source_id: AuthoritySourceId | None + def charged_output_count(self) -> int: + """Return the generation capacity charge for this lifecycle record.""" + if self.state in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + GenerationState.QUARANTINED, + }: + return self.maximum_allowance + return 0 + def __post_init__(self) -> None: for value in ( self.window_epoch_number, @@ -2122,6 +2150,16 @@ def __post_init__(self) -> None: for key, charged in protected_charges.items() ): _raise_invalid("protected_pool_overallocated") + global_charged = sum( + record.charged_input_count( + reservations_by_id.get(record.reservation_id) + if record.reservation_id is not None + else None + ) + for record in self.batch_records + ) + sum(generation.charged_output_count() for generation in self.generation_reservations) + if global_charged > self.snapshot.remaining_count: + _raise_invalid("context_capacity_overallocated") ContextAdmissionState: TypeAlias = UninitializedContextAdmissionState | ActiveContextAdmissionState diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index e6c550957..11e021d52 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -622,6 +622,25 @@ def test_reserve_rejects_active_generation_reservation_id_reuse() -> None: _assert_rejection_unchanged(state, rejected.next_state) +def test_active_state_rejects_global_capacity_overallocation() -> None: + state, _, _, _ = _reserved_batch( + remaining_count=60, + input_count=25, + generation_count=20, + name="global-overallocation", + ) + impossible_snapshot = replace( + state.snapshot, + active_count=56, + remaining_count=44, + ) + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + replace(state, snapshot=impossible_snapshot) + + assert "context_capacity_overallocated" in str(exc_info.value) + + def _prepare_dispatch( state: ActiveContextAdmissionState, batch: AdmissionBatch ) -> ActiveContextAdmissionState: From 665d131777cf8ae8d0c2f56b946d2fb8cf29d5fd Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:20:54 -0700 Subject: [PATCH 24/62] test(review): assert publication effect families --- .../test_context_admission_state_machine.py | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 2c5fa9739..4f22d5a2a 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -35,23 +35,29 @@ CanonicalRepresentationManifest, CanonicalSpanId, CanonicalSpanOwner, + ChargeCommittedEffect, ContextLineage, ContextSessionId, ContextThreadId, ContextWindowSnapshot, DeliveryOccurrenceId, DispatchRequestEvent, + EpochClosedEffect, EpochFenceProof, ExpireIdempotencyKeyEvent, ForkOccurrenceId, + GenerationReconciledEffect, GenerationReservationId, GenerationReservationRecord, + GenerationReservationRecordedEffect, GenerationState, + IdempotencyExpiredEffect, IdempotencyNamespace, MarkIndeterminateEvent, MeasurementKind, ModelIdentity, ModelItemId, + OccurrenceStateChangedEffect, OpenEpochEvent, PrepareBatchEvent, ProducerInstanceId, @@ -59,12 +65,18 @@ ProposeOccurrenceEvent, ProtectedPoolOwnerId, ProtectedPoolSpec, + QuarantineRecordedEffect, ReconcileGenerationEvent, + ReconciliationEscalationEffect, + ReconciliationQueryRequestedEffect, ReleaseNonAdmissionEvent, RepresentationBindingId, RepresentationBindingWitness, RepresentationRevision, RequestReconciliationEvent, + ReservationInvalidatedEffect, + ReservationRecordedEffect, + ReservationReleasedEffect, ReserveClass, ReserveRequestEvent, ResolveIndeterminateNonAdmissionEvent, @@ -395,6 +407,7 @@ def __init__(self) -> None: self.latest_replayable_event: Any | None = open_event self.latest_propose_event: ProposeOccurrenceEvent | None = None self.events: list[Any] = [open_event] + self.published_effects: list[tuple[object, ...]] = [opened.effects] self.event_sequence = 0 self.last_revision = self.state.aggregate_revision.value self.last_admission_sequence = self.state.admission_sequence.value @@ -424,6 +437,12 @@ def _accept_publication(self, transition: Any, event: object) -> None: assert ( transition.next_state.admission_sequence.value >= self.state.admission_sequence.value ) + expected_effect_types = self._expected_effect_types(transition, event) + assert tuple(type(effect) for effect in transition.effects) == expected_effect_types + for effect in transition.effects: + assert effect.source_event_id == getattr(event, "event_id") + assert effect.resulting_aggregate_revision == transition.next_state.aggregate_revision + assert effect.resulting_admission_sequence == transition.next_state.admission_sequence self.state = transition.next_state self.last_revision = self.state.aggregate_revision.value self.last_admission_sequence = self.state.admission_sequence.value @@ -431,6 +450,140 @@ def _accept_publication(self, transition: Any, event: object) -> None: if any(record.event_id == event_id for record in self.state.processed_events): self.latest_replayable_event = event self.events.append(event) + self.published_effects.append(transition.effects) + + def _expected_effect_types( + self, + transition: Any, + event: object, + ) -> tuple[type[object], ...]: + if transition.decision.kind not in { + AdmissionDecisionKind.WOULD_ADMIT, + AdmissionDecisionKind.QUARANTINED, + }: + return () + if isinstance(event, ProposeOccurrenceEvent | StartGenerationEvent): + return () + if isinstance(event, ReserveRequestEvent): + effect_types: tuple[type[object], ...] = ( + ReservationRecordedEffect, + *(OccurrenceStateChangedEffect for _ in event.batch.occurrence_ids), + ) + if ( + event.generation_reservation is not None + and event.generation_reservation.maximum_allowance > 0 + ): + effect_types += (GenerationReservationRecordedEffect,) + return effect_types + if isinstance( + event, + PrepareBatchEvent | StageHistoryEvent | DispatchRequestEvent | MarkIndeterminateEvent, + ): + record = self._find_batch(event.batch_id) + assert record is not None + return tuple(OccurrenceStateChangedEffect for _ in record.batch.occurrence_ids) + if isinstance(event, AcceptInputEvent): + record = self._find_batch(event.batch_id) + assert record is not None + effect_types = ( + ChargeCommittedEffect, + *(OccurrenceStateChangedEffect for _ in record.batch.occurrence_ids), + ) + if transition.decision.kind is AdmissionDecisionKind.QUARANTINED: + effect_types += (QuarantineRecordedEffect,) + return effect_types + if isinstance( + event, + ReleaseNonAdmissionEvent + | RollbackAdmissionEvent + | ResolveIndeterminateNonAdmissionEvent + | ResolveIndeterminateRollbackEvent, + ): + record = self._find_batch(event.batch_id) + assert record is not None + generation_count = sum( + generation.batch_id == record.batch.batch_id + and generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + for generation in self.state.generation_reservations + ) + return ( + ReservationReleasedEffect, + *(OccurrenceStateChangedEffect for _ in record.batch.occurrence_ids), + *(ReservationInvalidatedEffect for _ in range(generation_count)), + ) + if isinstance(event, ReconcileGenerationEvent): + effect_types = (GenerationReconciledEffect,) + if transition.decision.kind is AdmissionDecisionKind.QUARANTINED: + effect_types += (QuarantineRecordedEffect,) + return effect_types + if isinstance(event, ExpireIdempotencyKeyEvent): + return (IdempotencyExpiredEffect,) + if isinstance(event, RolloverEpochEvent): + invalidated_batch_ids = { + record.batch.batch_id + for record in self.state.batch_records + if record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + } + input_invalidation_count = sum( + reservation.key.batch_id in invalidated_batch_ids + for reservation in self.state.reservations + ) + retained_request_ids = { + record.batch.request_id + for record in self.state.batch_records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.COMMITTED, + AdmissionState.INDETERMINATE, + AdmissionState.QUARANTINED, + } + } + generation_invalidation_count = sum( + generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + and generation.request_id not in retained_request_ids + for generation in self.state.generation_reservations + ) + occurrence_change_count = sum( + record.state + in { + AdmissionState.PROPOSED, + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + } + for record in self.state.occurrence_records + ) + return ( + *( + ReservationInvalidatedEffect + for _ in range(input_invalidation_count + generation_invalidation_count) + ), + *(OccurrenceStateChangedEffect for _ in range(occurrence_change_count)), + EpochClosedEffect, + ) + if isinstance(event, RequestReconciliationEvent): + return ( + ReconciliationEscalationEffect + if "deadline" in event.reason_code.casefold() + else ReconciliationQueryRequestedEffect, + ) + raise AssertionError(f"missing effect contract for {type(event).__name__}") def _availability(self, reserve_class: ReserveClass) -> tuple[int, int, int]: total_charged = sum( @@ -998,12 +1151,14 @@ def identical_replay_is_equivalent(self) -> None: transition = reduce_context_admission(self.state, restored_event) assert transition.decision.kind is AdmissionDecisionKind.NOOP_IDEMPOTENT assert transition.next_state == self.state + assert transition.effects == () @rule() def generated_stream_replay_is_deterministic(self) -> None: replay = replay_context_admission(_uninitialized(), tuple(self.events)) assert replay.final_state == self.state assert len(replay.transitions) == len(self.events) + assert [transition.effects for transition in replay.transitions] == self.published_effects @precondition(lambda self: self.latest_propose_event is not None) @rule() From d4d0f635749328160b9b4488a8953498d6f0475c Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:21:23 -0700 Subject: [PATCH 25/62] test(review): cover every coverage selector mismatch --- tests/core/test_context_admission_coverage.py | 67 ++++++++++++------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/tests/core/test_context_admission_coverage.py b/tests/core/test_context_admission_coverage.py index 24701fe0c..6ae046a6b 100644 --- a/tests/core/test_context_admission_coverage.py +++ b/tests/core/test_context_admission_coverage.py @@ -348,31 +348,50 @@ def test_runtime_version_or_configuration_mismatch_degrades_deterministically() ) assert resolved == row - version_mismatch = resolve_context_admission_coverage( - row.surface, - evidence.backend, - evidence.configuration_mode, - f"{evidence.tested_version}-mismatch", - CHECKED_AT, - ) - configuration_mismatch = resolve_context_admission_coverage( - row.surface, - evidence.backend, - f"{evidence.configuration_mode}-mismatch", - evidence.tested_version, - CHECKED_AT, - ) - assert version_mismatch.observation_state is CoverageState.UPSTREAM_GATED - assert version_mismatch.authority_state is CoverageState.UPSTREAM_GATED - assert configuration_mismatch.observation_state is CoverageState.UPSTREAM_GATED - assert configuration_mismatch.authority_state is CoverageState.UPSTREAM_GATED - assert version_mismatch == resolve_context_admission_coverage( - row.surface, - evidence.backend, - evidence.configuration_mode, - f"{evidence.tested_version}-mismatch", - CHECKED_AT, + mismatch_inputs = ( + ( + f"{evidence.backend}-mismatch", + evidence.configuration_mode, + evidence.tested_version, + CHECKED_AT, + ), + ( + evidence.backend, + f"{evidence.configuration_mode}-mismatch", + evidence.tested_version, + CHECKED_AT, + ), + ( + evidence.backend, + evidence.configuration_mode, + f"{evidence.tested_version}-mismatch", + CHECKED_AT, + ), + ( + evidence.backend, + evidence.configuration_mode, + evidence.tested_version, + "2026-07-24", + ), ) + for backend, configuration_mode, source_version, as_of in mismatch_inputs: + mismatch = resolve_context_admission_coverage( + row.surface, + backend, + configuration_mode, + source_version, + as_of, + ) + assert mismatch.observation_state is CoverageState.UPSTREAM_GATED + assert mismatch.authority_state is CoverageState.UPSTREAM_GATED + assert mismatch.reason_code == "coverage-runtime-mismatch" + assert mismatch == resolve_context_admission_coverage( + row.surface, + backend, + configuration_mode, + source_version, + as_of, + ) def test_compaction_observation_does_not_imply_authority() -> None: From d9242d0185088fc45bfa79a90ba817c98d6d5719 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:21:57 -0700 Subject: [PATCH 26/62] test(review): compare complete ADR coverage rows --- tests/docs/test_context_admission_decision.py | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/tests/docs/test_context_admission_decision.py b/tests/docs/test_context_admission_decision.py index 8c3272b67..7751bf821 100644 --- a/tests/docs/test_context_admission_decision.py +++ b/tests/docs/test_context_admission_decision.py @@ -7,6 +7,8 @@ import pytest +from autoskillit.core import CONTEXT_ADMISSION_COVERAGE + REPO_ROOT = Path(__file__).resolve().parents[2] DECISION = REPO_ROOT / "docs/decisions/0007-context-admission.md" DECISION_INDEX = REPO_ROOT / "docs/decisions/README.md" @@ -209,33 +211,22 @@ def test_authority_unavailable_behavior_preserves_byte_boundaries( def test_decision_freezes_the_complete_producer_matrix(decision_text: str) -> None: coverage = _section(decision_text, "Producer coverage matrix") - for surface in [ - "NATIVE_SHELL", - "UNIFIED_EXEC_AND_WRITE_STDIN", - "APPLY_PATCH", - "AUTOSKILLIT_MCP", - "EXTERNAL_MCP", - "AUTOSKILLIT_LOCAL_FUNCTION", - "OTHER_LOCAL_FUNCTION", - "MCP_RESOURCE", - "CLIENT_PROVIDER_RETRIEVAL", - "CODE_MODE_AGGREGATE", - "HOSTED_SPECIALIZED_TOOL", - "HOOK_FEEDBACK", - "TOOL_ARGUMENT", - "TOOL_RESULT_ENVELOPE", - "USER_PROMPT", - "ASSISTANT_OUTPUT_HISTORY", - "SKILL_PLUGIN_CONTEXT", - "OTHER_CONTEXT_INJECTION", - "HEADLESS_CHILD_PROMPT", - "PARENT_VISIBLE_CHILD_DELIVERY", - "COMPACTION_MODEL_WINDOW_TRANSITION", - ]: - assert surface in coverage - assert "PARTIAL" in coverage - assert "UPSTREAM_GATED" in coverage - assert "VERIFIED" in coverage + actual_rows = tuple( + tuple(cell.strip().strip("`") for cell in line.strip().strip("|").split("|")) + for line in coverage.splitlines() + if line.startswith("| `") + ) + expected_rows = tuple( + ( + row.surface.name, + row.control_point_owner, + row.observation_state.name, + row.authority_state.name, + ) + for row in CONTEXT_ADMISSION_COVERAGE + ) + + assert actual_rows == expected_rows def test_upstream_request_contains_all_three_authority_parts_and_minimum_fields( From c69e328a1a6f365709770750ea0f0202882f29ef Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:46:07 -0700 Subject: [PATCH 27/62] fix(review): preserve outstanding allocation invariants --- src/autoskillit/core/types/AGENTS.md | 1 + src/autoskillit/core/types/__init__.py | 5 + .../core/types/_type_context_admission.py | 193 ++++-------------- .../_type_context_admission_validation.py | 156 ++++++++++++++ 4 files changed, 200 insertions(+), 155 deletions(-) create mode 100644 src/autoskillit/core/types/_type_context_admission_validation.py diff --git a/src/autoskillit/core/types/AGENTS.md b/src/autoskillit/core/types/AGENTS.md index ae9f5c658..1da749ff5 100644 --- a/src/autoskillit/core/types/AGENTS.md +++ b/src/autoskillit/core/types/AGENTS.md @@ -42,6 +42,7 @@ Type re-export hub and all typed building blocks for the autoskillit package (IL | `_type_plugin_source.py` | `PluginSource` discriminated union: `DirectInstall | MarketplaceInstall` | | `_type_tradition_manifest.py` | `TraditionManifest`, `LensEntry`, `DialingConfig` frozen dataclasses with `from_dict`/`from_yaml_path` loaders | | `_type_context_admission.py` | Frozen content-free identities, events, records, effects, states, and coverage definitions for context admission | +| `_type_context_admission_validation.py` | Internal validation primitives shared by the context-admission protocol values | ## Architecture Notes diff --git a/src/autoskillit/core/types/__init__.py b/src/autoskillit/core/types/__init__.py index e97ee409a..410e84959 100644 --- a/src/autoskillit/core/types/__init__.py +++ b/src/autoskillit/core/types/__init__.py @@ -24,6 +24,10 @@ from ._type_constants_registries import __all__ as _constants_registries_all from ._type_context_admission import * # noqa: F401, F403 from ._type_context_admission import __all__ as _context_admission_all +from ._type_context_admission_validation import * # noqa: F401, F403 +from ._type_context_admission_validation import ( + __all__ as _context_admission_validation_all, +) from ._type_dispatch_identity import * # noqa: F401, F403 from ._type_dispatch_identity import __all__ as _dispatch_identity_all from ._type_enums import * # noqa: F401, F403 @@ -87,6 +91,7 @@ + _constants_features_all + _constants_registries_all + _context_admission_all + + _context_admission_validation_all + _dispatch_identity_all + _enums_all + _exceptions_all diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 640a6d541..2a6fbb0ca 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -7,23 +7,31 @@ from __future__ import annotations -import re -from collections.abc import Callable, Mapping +from collections.abc import Mapping from dataclasses import dataclass, field, fields -from datetime import date from enum import StrEnum -from types import UnionType from typing import ( Any, ClassVar, - Never, TypeAlias, - Union, - get_args, - get_origin, get_type_hints, ) +from ._type_context_admission_validation import ( + CONTEXT_ADMISSION_PROTOCOL_VERSION, + ContextAdmissionValidationError, + UnsupportedContextAdmissionProtocolError, + _matches_declared_type, + _raise_invalid, + _validate_bounded_text, + _validate_canonical_tuple, + _validate_freshness_policy, + _validate_git_revision, + _validate_iso_date, + _validate_non_negative, + _validate_protocol_version, + _validate_reason_code, +) from ._type_dispatch_identity import DispatchIdentity from ._type_enums import ( AdmissionDecisionKind, @@ -39,39 +47,6 @@ ) from ._type_results import ModelIdentity -CONTEXT_ADMISSION_PROTOCOL_VERSION = 1 -_MAX_UINT64 = (1 << 64) - 1 - -_CONTENT_FREE_TEXT = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:@+-]*\Z") -_CONTENT_FREE_LOCATOR = re.compile(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]*\Z") -_REASON_CODE = re.compile(r"[a-z][a-z0-9-]{0,63}\Z") -_GIT_REVISION = re.compile(r"[0-9a-fA-F]{40}\Z") -_ISO_DATE = re.compile(r"\d{4}-\d{2}-\d{2}\Z") -_FRESHNESS_POLICIES = frozenset( - { - "verify_on_version_or_configuration_change", - "verify_on_revision_change", - "infer_only", - } -) -_SENSITIVE_TEXT_MARKERS = ( - "authorization", - "bearer", - "content:", - "password", - "secret", - "token=", -) - - -class ContextAdmissionValidationError(ValueError): - """Raised when a protocol value violates a content-free invariant.""" - - -class UnsupportedContextAdmissionProtocolError(ContextAdmissionValidationError): - """Raised when a value uses unsupported protocol semantics.""" - - _TYPE_REGISTRY: dict[str, type[_ContractValue]] = {} _ENUM_REGISTRY: dict[str, type[StrEnum]] = { enum_type.__name__: enum_type @@ -90,80 +65,6 @@ class UnsupportedContextAdmissionProtocolError(ContextAdmissionValidationError): } -def _raise_invalid(reason_code: str) -> Never: - raise ContextAdmissionValidationError(reason_code) - - -def _validate_protocol_version(protocol_version: int) -> None: - if protocol_version != CONTEXT_ADMISSION_PROTOCOL_VERSION: - raise UnsupportedContextAdmissionProtocolError("unsupported_protocol_version") - - -def _validate_non_negative(value: int, reason_code: str) -> None: - if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > _MAX_UINT64: - _raise_invalid(reason_code) - - -def _validate_bounded_text( - value: str, - reason_code: str, - *, - maximum: int = 128, - locator: bool = False, -) -> None: - if not isinstance(value, str) or not value or len(value) > maximum: - _raise_invalid(reason_code) - lowered = value.casefold() - pattern = _CONTENT_FREE_LOCATOR if locator else _CONTENT_FREE_TEXT - if ( - any(marker in lowered for marker in _SENSITIVE_TEXT_MARKERS) - or lowered.startswith("sha256:") - or lowered.startswith("blake2:") - or "\n" in value - or "\r" in value - or value.startswith("/") - or "\\" in value - or value.startswith("~") - or not pattern.fullmatch(value) - or (locator and ".." in value.split("/")) - ): - _raise_invalid(reason_code) - - -def _validate_reason_code( - value: str, - validation_error: str = "invalid_reason_code", -) -> None: - _validate_bounded_text(value, validation_error, maximum=64) - if not _REASON_CODE.fullmatch(value): - _raise_invalid(validation_error) - - -def _validate_iso_date(value: str) -> None: - if not isinstance(value, str) or not _ISO_DATE.fullmatch(value): - _raise_invalid("invalid_checked_at") - try: - date.fromisoformat(value) - except ValueError: - raise ContextAdmissionValidationError("invalid_checked_at") from None - - -def _validate_tuple(value: object, reason_code: str) -> None: - if not isinstance(value, tuple): - _raise_invalid(reason_code) - - -def _validate_canonical_tuple( - value: tuple[Any, ...], - reason_code: str, - *, - key: Callable[[Any], Any], -) -> None: - _validate_tuple(value, reason_code) - if value != tuple(sorted(value, key=key)): - _raise_invalid(reason_code) - - def _encode(value: object) -> object: if isinstance(value, DispatchIdentity): try: @@ -324,34 +225,6 @@ def _validate_deep_immutability(value: object) -> None: _validate_deep_immutability(getattr(value, field_name)) -def _matches_declared_type(value: object, declared_type: object) -> bool: - if declared_type is Any: - return True - origin = get_origin(declared_type) - if origin in {Union, UnionType}: - return any(_matches_declared_type(value, member) for member in get_args(declared_type)) - if origin is tuple: - if type(value) is not tuple: - return False - members = get_args(declared_type) - if len(members) == 2 and members[1] is Ellipsis: - return all(_matches_declared_type(item, members[0]) for item in value) - return len(value) == len(members) and all( - _matches_declared_type(item, member) - for item, member in zip(value, members, strict=True) - ) - if origin is frozenset: - if type(value) is not frozenset: - return False - (member_type,) = get_args(declared_type) - return all(_matches_declared_type(item, member_type) for item in value) - if declared_type is None or declared_type is type(None): - return value is None - if isinstance(declared_type, type): - return type(value) is declared_type - return False - - def _validate_declared_field_types(value: _ContractValue) -> None: declared_types = get_type_hints(type(value)) for declared_field in fields(value): @@ -1154,16 +1027,9 @@ def __post_init__(self) -> None: (self.tested_version, "invalid_tested_version", 64), ): _validate_bounded_text(value, reason, maximum=maximum) - if not isinstance(self.tested_revision, str) or not _GIT_REVISION.fullmatch( - self.tested_revision - ): - _raise_invalid("invalid_tested_revision") + _validate_git_revision(self.tested_revision) _validate_iso_date(self.checked_at) - if ( - not isinstance(self.freshness_policy, str) - or self.freshness_policy not in _FRESHNESS_POLICIES - ): - _raise_invalid("invalid_freshness_policy") + _validate_freshness_policy(self.freshness_policy) _validate_bounded_text( self.source_locator, "invalid_source_locator", @@ -2150,15 +2016,32 @@ def __post_init__(self) -> None: for key, charged in protected_charges.items() ): _raise_invalid("protected_pool_overallocated") - global_charged = sum( + global_allocated = sum( record.charged_input_count( reservations_by_id.get(record.reservation_id) if record.reservation_id is not None else None ) for record in self.batch_records - ) + sum(generation.charged_output_count() for generation in self.generation_reservations) - if global_charged > self.snapshot.remaining_count: + if record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + sum( + generation.charged_output_count() + for generation in self.generation_reservations + if generation.state + in { + GenerationState.RESERVED, + GenerationState.STREAMING, + GenerationState.INDETERMINATE, + } + ) + if global_allocated > self.snapshot.remaining_count: _raise_invalid("context_capacity_overallocated") diff --git a/src/autoskillit/core/types/_type_context_admission_validation.py b/src/autoskillit/core/types/_type_context_admission_validation.py new file mode 100644 index 000000000..213429737 --- /dev/null +++ b/src/autoskillit/core/types/_type_context_admission_validation.py @@ -0,0 +1,156 @@ +"""Validation primitives for the protocol-v1 context-admission values.""" + +from __future__ import annotations + +import re +from collections.abc import Callable +from datetime import date +from types import UnionType +from typing import Any, Never, Union, get_args, get_origin + +CONTEXT_ADMISSION_PROTOCOL_VERSION = 1 +_MAX_UINT64 = (1 << 64) - 1 + +_CONTENT_FREE_TEXT = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:@+-]*\Z") +_CONTENT_FREE_LOCATOR = re.compile(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]*\Z") +_REASON_CODE = re.compile(r"[a-z][a-z0-9-]{0,63}\Z") +_GIT_REVISION = re.compile(r"[0-9a-fA-F]{40}\Z") +_ISO_DATE = re.compile(r"\d{4}-\d{2}-\d{2}\Z") +_FRESHNESS_POLICIES = frozenset( + { + "verify_on_version_or_configuration_change", + "verify_on_revision_change", + "infer_only", + } +) +_SENSITIVE_TEXT_MARKERS = ( + "authorization", + "bearer", + "content:", + "password", + "secret", + "token=", +) + + +class ContextAdmissionValidationError(ValueError): + """Raised when a protocol value violates a content-free invariant.""" + + +class UnsupportedContextAdmissionProtocolError(ContextAdmissionValidationError): + """Raised when a value uses unsupported protocol semantics.""" + + +def _raise_invalid(reason_code: str) -> Never: + raise ContextAdmissionValidationError(reason_code) + + +def _validate_protocol_version(protocol_version: int) -> None: + if protocol_version != CONTEXT_ADMISSION_PROTOCOL_VERSION: + raise UnsupportedContextAdmissionProtocolError("unsupported_protocol_version") + + +def _validate_non_negative(value: int, reason_code: str) -> None: + if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > _MAX_UINT64: + _raise_invalid(reason_code) + + +def _validate_bounded_text( + value: str, + reason_code: str, + *, + maximum: int = 128, + locator: bool = False, +) -> None: + if not isinstance(value, str) or not value or len(value) > maximum: + _raise_invalid(reason_code) + lowered = value.casefold() + pattern = _CONTENT_FREE_LOCATOR if locator else _CONTENT_FREE_TEXT + if ( + any(marker in lowered for marker in _SENSITIVE_TEXT_MARKERS) + or lowered.startswith("sha256:") + or lowered.startswith("blake2:") + or "\n" in value + or "\r" in value + or value.startswith("/") + or "\\" in value + or value.startswith("~") + or not pattern.fullmatch(value) + or (locator and ".." in value.split("/")) + ): + _raise_invalid(reason_code) + + +def _validate_reason_code( + value: str, + validation_error: str = "invalid_reason_code", +) -> None: + _validate_bounded_text(value, validation_error, maximum=64) + if not _REASON_CODE.fullmatch(value): + _raise_invalid(validation_error) + + +def _validate_iso_date(value: str) -> None: + if not isinstance(value, str) or not _ISO_DATE.fullmatch(value): + _raise_invalid("invalid_checked_at") + try: + date.fromisoformat(value) + except ValueError: + raise ContextAdmissionValidationError("invalid_checked_at") from None + + +def _validate_tuple(value: object, reason_code: str) -> None: + if not isinstance(value, tuple): + _raise_invalid(reason_code) + + +def _validate_canonical_tuple( + value: tuple[Any, ...], + reason_code: str, + *, + key: Callable[[Any], Any], +) -> None: + _validate_tuple(value, reason_code) + if value != tuple(sorted(value, key=key)): + _raise_invalid(reason_code) + + +def _validate_git_revision(value: str) -> None: + if not isinstance(value, str) or not _GIT_REVISION.fullmatch(value): + _raise_invalid("invalid_tested_revision") + + +def _validate_freshness_policy(value: str) -> None: + if not isinstance(value, str) or value not in _FRESHNESS_POLICIES: + _raise_invalid("invalid_freshness_policy") + + +def _matches_declared_type(value: object, declared_type: object) -> bool: + if declared_type is Any: + return True + origin = get_origin(declared_type) + if origin in {Union, UnionType}: + return any(_matches_declared_type(value, member) for member in get_args(declared_type)) + if origin is tuple: + if type(value) is not tuple: + return False + members = get_args(declared_type) + if len(members) == 2 and members[1] is Ellipsis: + return all(_matches_declared_type(item, members[0]) for item in value) + return len(value) == len(members) and all( + _matches_declared_type(item, member) + for item, member in zip(value, members, strict=True) + ) + if origin is frozenset: + if type(value) is not frozenset: + return False + (member_type,) = get_args(declared_type) + return all(_matches_declared_type(item, member_type) for item in value) + if declared_type is None or declared_type is type(None): + return value is None + if isinstance(declared_type, type): + return type(value) is declared_type + return False + + +__all__: list[str] = [] From 2379ed78ff3751e3c758530a9105a86dbc445a75 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:46:14 -0700 Subject: [PATCH 28/62] test(review): bind rollover reservation to current epoch --- tests/core/test_context_admission_reducer.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 11e021d52..8d28c66a3 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -1493,6 +1493,7 @@ def test_protected_pool_release_policy_accepts_only_its_configured_resolution( input_count=10, generation_count=0, ) + assert reserved.decision.kind is AdmissionDecisionKind.WOULD_ADMIT assert isinstance(reserved.next_state, ActiveContextAdmissionState) state = _prepare_dispatch(reserved.next_state, batch) marked = reduce_context_admission( @@ -2183,13 +2184,18 @@ def test_protected_release_uses_policy_from_rollover_created_epoch() -> None: reserve_class=ReserveClass.SYNTHESIS, protected_owner=owner.value, ) - reserved, _ = _reserve( + _, reserve_event = _reserve( state, batch, (occurrence,), input_count=10, generation_count=0, ) + reserved = reduce_context_admission( + state, + replace(reserve_event, generation_reservation=None), + ) + assert reserved.decision.kind is AdmissionDecisionKind.WOULD_ADMIT assert isinstance(reserved.next_state, ActiveContextAdmissionState) state = _prepare_dispatch(reserved.next_state, batch) marked = reduce_context_admission( From e6afb895f6a41126a5287e5d511df55acf2e7719 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:46:19 -0700 Subject: [PATCH 29/62] test(review): scope state-machine identities by epoch --- .../test_context_admission_state_machine.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 4f22d5a2a..1123b60d8 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -168,7 +168,7 @@ def _occurrence( window_epoch_id: WindowEpochId, window_epoch_number: int, ) -> AdmissionOccurrence: - name = f"occurrence-{slot}" + name = f"occurrence-{window_epoch_number}-{slot}" surface = ( ProducerSurface.PARENT_VISIBLE_CHILD_DELIVERY if slot == 1 @@ -208,21 +208,21 @@ def _occurrence( def _batch(occurrence: AdmissionOccurrence) -> AdmissionBatch: - slot = occurrence.occurrence_id.value.rsplit("-", 1)[-1] - request_id = AdmissionRequestId(f"request-{slot}") + identity = occurrence.occurrence_id.value.removeprefix("occurrence-") + request_id = AdmissionRequestId(f"request-{identity}") manifest = CanonicalRepresentationManifest( request_id=request_id, representation_revision=occurrence.representation_revision, - representation_binding_id=RepresentationBindingId(f"binding-{slot}"), + representation_binding_id=RepresentationBindingId(f"binding-{identity}"), span_owners=tuple( CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) for span_id in occurrence.owned_span_ids ), - assembler_identity=ProducerInstanceId(f"assembler-{slot}"), - assembler_witness_id=AdmissionWitnessId(f"assembler-witness-{slot}"), + assembler_identity=ProducerInstanceId(f"assembler-{identity}"), + assembler_witness_id=AdmissionWitnessId(f"assembler-witness-{identity}"), ) return AdmissionBatch( - batch_id=AdmissionBatchId(f"batch-{slot}"), + batch_id=AdmissionBatchId(f"batch-{identity}"), request_id=request_id, occurrence_ids=(occurrence.occurrence_id,), reserve_class=occurrence.reserve_class, @@ -233,8 +233,10 @@ def _batch(occurrence: AdmissionOccurrence) -> AdmissionBatch: def _multi_batch(occurrences: tuple[AdmissionOccurrence, ...]) -> AdmissionBatch: occurrences = tuple(sorted(occurrences, key=lambda occurrence: occurrence.occurrence_id.value)) - slots = tuple(occurrence.occurrence_id.value.rsplit("-", 1)[-1] for occurrence in occurrences) - request_id = AdmissionRequestId("request-multi-" + "-".join(slots)) + identities = tuple( + occurrence.occurrence_id.value.removeprefix("occurrence-") for occurrence in occurrences + ) + request_id = AdmissionRequestId("request-multi-" + "-".join(identities)) reserve_class = occurrences[0].reserve_class if any(occurrence.reserve_class is not reserve_class for occurrence in occurrences): msg = "multi-member batch must share one reserve class" @@ -242,17 +244,17 @@ def _multi_batch(occurrences: tuple[AdmissionOccurrence, ...]) -> AdmissionBatch manifest = CanonicalRepresentationManifest( request_id=request_id, representation_revision=occurrences[0].representation_revision, - representation_binding_id=RepresentationBindingId("binding-multi-" + "-".join(slots)), + representation_binding_id=RepresentationBindingId("binding-multi-" + "-".join(identities)), span_owners=tuple( CanonicalSpanOwner(span_id=span_id, occurrence_id=occurrence.occurrence_id) for occurrence in occurrences for span_id in occurrence.owned_span_ids ), - assembler_identity=ProducerInstanceId("assembler-multi-" + "-".join(slots)), - assembler_witness_id=AdmissionWitnessId("assembler-witness-multi-" + "-".join(slots)), + assembler_identity=ProducerInstanceId("assembler-multi-" + "-".join(identities)), + assembler_witness_id=AdmissionWitnessId("assembler-witness-multi-" + "-".join(identities)), ) return AdmissionBatch( - batch_id=AdmissionBatchId("batch-multi-" + "-".join(slots)), + batch_id=AdmissionBatchId("batch-multi-" + "-".join(identities)), request_id=request_id, occurrence_ids=tuple(occurrence.occurrence_id for occurrence in occurrences), reserve_class=reserve_class, From 18ab64c93fb7818624d9c13e55c42b2a151195e3 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:52:06 -0700 Subject: [PATCH 30/62] refactor(review): honor core type module boundary --- src/autoskillit/core/types/AGENTS.md | 3 +- src/autoskillit/core/types/__init__.py | 5 - .../core/types/_type_context_admission.py | 28 ++-- .../_type_context_admission_validation.py | 156 ------------------ src/autoskillit/core/types/_type_helpers.py | 148 ++++++++++++++++- 5 files changed, 162 insertions(+), 178 deletions(-) delete mode 100644 src/autoskillit/core/types/_type_context_admission_validation.py diff --git a/src/autoskillit/core/types/AGENTS.md b/src/autoskillit/core/types/AGENTS.md index 1da749ff5..793775b66 100644 --- a/src/autoskillit/core/types/AGENTS.md +++ b/src/autoskillit/core/types/AGENTS.md @@ -34,7 +34,7 @@ Type re-export hub and all typed building blocks for the autoskillit package (IL | `_type_recipe_sections.py` | Recipe-section schema validation plus canonical section, element, and plan digest helpers | | `_type_capture.py` | `CaptureEntrySpec` and `CaptureValueTypeError` for typed capture contract enforcement | | `_type_dispatch_identity.py` | `DispatchIdentity` frozen value object, `PromptContractError`, and `assert_prompt_sentinel` for sentinel contract enforcement | -| `_type_helpers.py` | Text processing and skill-name extraction utilities | +| `_type_helpers.py` | Text processing, skill-name extraction, and shared content-free validation utilities | | `_type_inspector.py` | Health Inspector types: `InspectorEvidence`, `InspectorVerdict`, `InspectorCallback` (issue #3533) | | `_type_invariant_registry.py` | Invariant registry: `InvariantDef` dataclass and `INVARIANT_REGISTRY` mapping prose prohibitions to runtime gates | | `_type_phoropter.py` | Phoropter family/phase types: `PhoropterPrescription`, `ReadingToken`, `READING_TOKEN_PATTERN`, `PhoropterPhaseSkip`, `CrossDomainPrescription`, `CrossDomainAssessment` | @@ -42,7 +42,6 @@ Type re-export hub and all typed building blocks for the autoskillit package (IL | `_type_plugin_source.py` | `PluginSource` discriminated union: `DirectInstall | MarketplaceInstall` | | `_type_tradition_manifest.py` | `TraditionManifest`, `LensEntry`, `DialingConfig` frozen dataclasses with `from_dict`/`from_yaml_path` loaders | | `_type_context_admission.py` | Frozen content-free identities, events, records, effects, states, and coverage definitions for context admission | -| `_type_context_admission_validation.py` | Internal validation primitives shared by the context-admission protocol values | ## Architecture Notes diff --git a/src/autoskillit/core/types/__init__.py b/src/autoskillit/core/types/__init__.py index 410e84959..e97ee409a 100644 --- a/src/autoskillit/core/types/__init__.py +++ b/src/autoskillit/core/types/__init__.py @@ -24,10 +24,6 @@ from ._type_constants_registries import __all__ as _constants_registries_all from ._type_context_admission import * # noqa: F401, F403 from ._type_context_admission import __all__ as _context_admission_all -from ._type_context_admission_validation import * # noqa: F401, F403 -from ._type_context_admission_validation import ( - __all__ as _context_admission_validation_all, -) from ._type_dispatch_identity import * # noqa: F401, F403 from ._type_dispatch_identity import __all__ as _dispatch_identity_all from ._type_enums import * # noqa: F401, F403 @@ -91,7 +87,6 @@ + _constants_features_all + _constants_registries_all + _context_admission_all - + _context_admission_validation_all + _dispatch_identity_all + _enums_all + _exceptions_all diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 2a6fbb0ca..373a45870 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -17,7 +17,20 @@ get_type_hints, ) -from ._type_context_admission_validation import ( +from ._type_dispatch_identity import DispatchIdentity +from ._type_enums import ( + AdmissionDecisionKind, + AdmissionState, + ChargeDomain, + CoverageEvidenceKind, + CoverageState, + GenerationState, + MeasurementKind, + ProducerSurface, + ReserveClass, + WitnessKind, +) +from ._type_helpers import ( CONTEXT_ADMISSION_PROTOCOL_VERSION, ContextAdmissionValidationError, UnsupportedContextAdmissionProtocolError, @@ -32,19 +45,6 @@ _validate_protocol_version, _validate_reason_code, ) -from ._type_dispatch_identity import DispatchIdentity -from ._type_enums import ( - AdmissionDecisionKind, - AdmissionState, - ChargeDomain, - CoverageEvidenceKind, - CoverageState, - GenerationState, - MeasurementKind, - ProducerSurface, - ReserveClass, - WitnessKind, -) from ._type_results import ModelIdentity _TYPE_REGISTRY: dict[str, type[_ContractValue]] = {} diff --git a/src/autoskillit/core/types/_type_context_admission_validation.py b/src/autoskillit/core/types/_type_context_admission_validation.py deleted file mode 100644 index 213429737..000000000 --- a/src/autoskillit/core/types/_type_context_admission_validation.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Validation primitives for the protocol-v1 context-admission values.""" - -from __future__ import annotations - -import re -from collections.abc import Callable -from datetime import date -from types import UnionType -from typing import Any, Never, Union, get_args, get_origin - -CONTEXT_ADMISSION_PROTOCOL_VERSION = 1 -_MAX_UINT64 = (1 << 64) - 1 - -_CONTENT_FREE_TEXT = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:@+-]*\Z") -_CONTENT_FREE_LOCATOR = re.compile(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]*\Z") -_REASON_CODE = re.compile(r"[a-z][a-z0-9-]{0,63}\Z") -_GIT_REVISION = re.compile(r"[0-9a-fA-F]{40}\Z") -_ISO_DATE = re.compile(r"\d{4}-\d{2}-\d{2}\Z") -_FRESHNESS_POLICIES = frozenset( - { - "verify_on_version_or_configuration_change", - "verify_on_revision_change", - "infer_only", - } -) -_SENSITIVE_TEXT_MARKERS = ( - "authorization", - "bearer", - "content:", - "password", - "secret", - "token=", -) - - -class ContextAdmissionValidationError(ValueError): - """Raised when a protocol value violates a content-free invariant.""" - - -class UnsupportedContextAdmissionProtocolError(ContextAdmissionValidationError): - """Raised when a value uses unsupported protocol semantics.""" - - -def _raise_invalid(reason_code: str) -> Never: - raise ContextAdmissionValidationError(reason_code) - - -def _validate_protocol_version(protocol_version: int) -> None: - if protocol_version != CONTEXT_ADMISSION_PROTOCOL_VERSION: - raise UnsupportedContextAdmissionProtocolError("unsupported_protocol_version") - - -def _validate_non_negative(value: int, reason_code: str) -> None: - if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > _MAX_UINT64: - _raise_invalid(reason_code) - - -def _validate_bounded_text( - value: str, - reason_code: str, - *, - maximum: int = 128, - locator: bool = False, -) -> None: - if not isinstance(value, str) or not value or len(value) > maximum: - _raise_invalid(reason_code) - lowered = value.casefold() - pattern = _CONTENT_FREE_LOCATOR if locator else _CONTENT_FREE_TEXT - if ( - any(marker in lowered for marker in _SENSITIVE_TEXT_MARKERS) - or lowered.startswith("sha256:") - or lowered.startswith("blake2:") - or "\n" in value - or "\r" in value - or value.startswith("/") - or "\\" in value - or value.startswith("~") - or not pattern.fullmatch(value) - or (locator and ".." in value.split("/")) - ): - _raise_invalid(reason_code) - - -def _validate_reason_code( - value: str, - validation_error: str = "invalid_reason_code", -) -> None: - _validate_bounded_text(value, validation_error, maximum=64) - if not _REASON_CODE.fullmatch(value): - _raise_invalid(validation_error) - - -def _validate_iso_date(value: str) -> None: - if not isinstance(value, str) or not _ISO_DATE.fullmatch(value): - _raise_invalid("invalid_checked_at") - try: - date.fromisoformat(value) - except ValueError: - raise ContextAdmissionValidationError("invalid_checked_at") from None - - -def _validate_tuple(value: object, reason_code: str) -> None: - if not isinstance(value, tuple): - _raise_invalid(reason_code) - - -def _validate_canonical_tuple( - value: tuple[Any, ...], - reason_code: str, - *, - key: Callable[[Any], Any], -) -> None: - _validate_tuple(value, reason_code) - if value != tuple(sorted(value, key=key)): - _raise_invalid(reason_code) - - -def _validate_git_revision(value: str) -> None: - if not isinstance(value, str) or not _GIT_REVISION.fullmatch(value): - _raise_invalid("invalid_tested_revision") - - -def _validate_freshness_policy(value: str) -> None: - if not isinstance(value, str) or value not in _FRESHNESS_POLICIES: - _raise_invalid("invalid_freshness_policy") - - -def _matches_declared_type(value: object, declared_type: object) -> bool: - if declared_type is Any: - return True - origin = get_origin(declared_type) - if origin in {Union, UnionType}: - return any(_matches_declared_type(value, member) for member in get_args(declared_type)) - if origin is tuple: - if type(value) is not tuple: - return False - members = get_args(declared_type) - if len(members) == 2 and members[1] is Ellipsis: - return all(_matches_declared_type(item, members[0]) for item in value) - return len(value) == len(members) and all( - _matches_declared_type(item, member) - for item, member in zip(value, members, strict=True) - ) - if origin is frozenset: - if type(value) is not frozenset: - return False - (member_type,) = get_args(declared_type) - return all(_matches_declared_type(item, member_type) for item in value) - if declared_type is None or declared_type is type(None): - return value is None - if isinstance(declared_type, type): - return type(value) is declared_type - return False - - -__all__: list[str] = [] diff --git a/src/autoskillit/core/types/_type_helpers.py b/src/autoskillit/core/types/_type_helpers.py index 1093ad5aa..aa16ec090 100644 --- a/src/autoskillit/core/types/_type_helpers.py +++ b/src/autoskillit/core/types/_type_helpers.py @@ -11,8 +11,11 @@ import re import shlex import warnings +from collections.abc import Callable +from datetime import date from pathlib import Path -from typing import Any, assert_never +from types import UnionType +from typing import Any, Never, Union, assert_never, get_args, get_origin from ._type_backend import BackendConventions from ._type_constants import SKILL_COMMAND_PREFIX @@ -44,6 +47,149 @@ _PATH_PREFIXES: tuple[str, ...] = ("/", "./", ".autoskillit/") +CONTEXT_ADMISSION_PROTOCOL_VERSION = 1 +_MAX_UINT64 = (1 << 64) - 1 +_CONTENT_FREE_TEXT = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:@+-]*\Z") +_CONTENT_FREE_LOCATOR = re.compile(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]*\Z") +_REASON_CODE = re.compile(r"[a-z][a-z0-9-]{0,63}\Z") +_GIT_REVISION = re.compile(r"[0-9a-fA-F]{40}\Z") +_ISO_DATE = re.compile(r"\d{4}-\d{2}-\d{2}\Z") +_FRESHNESS_POLICIES = frozenset( + { + "verify_on_version_or_configuration_change", + "verify_on_revision_change", + "infer_only", + } +) +_SENSITIVE_TEXT_MARKERS = ( + "authorization", + "bearer", + "content:", + "password", + "secret", + "token=", +) + + +class ContextAdmissionValidationError(ValueError): + """Raised when a protocol value violates a content-free invariant.""" + + +class UnsupportedContextAdmissionProtocolError(ContextAdmissionValidationError): + """Raised when a value uses unsupported protocol semantics.""" + + +def _raise_invalid(reason_code: str) -> Never: + raise ContextAdmissionValidationError(reason_code) + + +def _validate_protocol_version(protocol_version: int) -> None: + if protocol_version != CONTEXT_ADMISSION_PROTOCOL_VERSION: + raise UnsupportedContextAdmissionProtocolError("unsupported_protocol_version") + + +def _validate_non_negative(value: int, reason_code: str) -> None: + if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > _MAX_UINT64: + _raise_invalid(reason_code) + + +def _validate_bounded_text( + value: str, + reason_code: str, + *, + maximum: int = 128, + locator: bool = False, +) -> None: + if not isinstance(value, str) or not value or len(value) > maximum: + _raise_invalid(reason_code) + lowered = value.casefold() + pattern = _CONTENT_FREE_LOCATOR if locator else _CONTENT_FREE_TEXT + if ( + any(marker in lowered for marker in _SENSITIVE_TEXT_MARKERS) + or lowered.startswith("sha256:") + or lowered.startswith("blake2:") + or "\n" in value + or "\r" in value + or value.startswith("/") + or "\\" in value + or value.startswith("~") + or not pattern.fullmatch(value) + or (locator and ".." in value.split("/")) + ): + _raise_invalid(reason_code) + + +def _validate_reason_code( + value: str, + validation_error: str = "invalid_reason_code", +) -> None: + _validate_bounded_text(value, validation_error, maximum=64) + if not _REASON_CODE.fullmatch(value): + _raise_invalid(validation_error) + + +def _validate_iso_date(value: str) -> None: + if not isinstance(value, str) or not _ISO_DATE.fullmatch(value): + _raise_invalid("invalid_checked_at") + try: + date.fromisoformat(value) + except ValueError: + raise ContextAdmissionValidationError("invalid_checked_at") from None + + +def _validate_tuple(value: object, reason_code: str) -> None: + if not isinstance(value, tuple): + _raise_invalid(reason_code) + + +def _validate_canonical_tuple( + value: tuple[Any, ...], + reason_code: str, + *, + key: Callable[[Any], Any], +) -> None: + _validate_tuple(value, reason_code) + if value != tuple(sorted(value, key=key)): + _raise_invalid(reason_code) + + +def _validate_git_revision(value: str) -> None: + if not isinstance(value, str) or not _GIT_REVISION.fullmatch(value): + _raise_invalid("invalid_tested_revision") + + +def _validate_freshness_policy(value: str) -> None: + if not isinstance(value, str) or value not in _FRESHNESS_POLICIES: + _raise_invalid("invalid_freshness_policy") + + +def _matches_declared_type(value: object, declared_type: object) -> bool: + if declared_type is Any: + return True + origin = get_origin(declared_type) + if origin in {Union, UnionType}: + return any(_matches_declared_type(value, member) for member in get_args(declared_type)) + if origin is tuple: + if type(value) is not tuple: + return False + members = get_args(declared_type) + if len(members) == 2 and members[1] is Ellipsis: + return all(_matches_declared_type(item, members[0]) for item in value) + return len(value) == len(members) and all( + _matches_declared_type(item, member) + for item, member in zip(value, members, strict=True) + ) + if origin is frozenset: + if type(value) is not frozenset: + return False + (member_type,) = get_args(declared_type) + return all(_matches_declared_type(item, member_type) for item in value) + if declared_type is None or declared_type is type(None): + return value is None + if isinstance(declared_type, type): + return type(value) is declared_type + return False + def is_path_like_token(token: str) -> bool: return any(token.startswith(p) for p in _PATH_PREFIXES) From a19970cc7c75b3685eadbad2ebf77b89f2419096 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 12:52:11 -0700 Subject: [PATCH 31/62] test(review): derive dispatch witnesses from active epoch --- tests/core/test_context_admission_reducer.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 8d28c66a3..97de6336c 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -665,7 +665,11 @@ def _prepare_dispatch( "stage-history", ), batch_id=batch.batch_id, - witness=_witness(batch, WitnessKind.HISTORY_STAGED), + witness=_witness( + batch, + WitnessKind.HISTORY_STAGED, + epoch=prepared.next_state.snapshot.window_epoch_number, + ), ) staged = reduce_context_admission(prepared.next_state, stage) assert staged.decision.kind is AdmissionDecisionKind.WOULD_ADMIT @@ -677,7 +681,11 @@ def _prepare_dispatch( "dispatch-request", ), batch_id=batch.batch_id, - witness=_witness(batch, WitnessKind.REQUEST_INCLUDED), + witness=_witness( + batch, + WitnessKind.REQUEST_INCLUDED, + epoch=staged.next_state.snapshot.window_epoch_number, + ), ) dispatched = reduce_context_admission(staged.next_state, dispatch) assert dispatched.decision.kind is AdmissionDecisionKind.WOULD_ADMIT From 1a031deb80d3b5610671bc20c1b1ebbeaaebd394 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:09:24 -0700 Subject: [PATCH 32/62] fix(review): require rollover snapshot capacity deduction --- src/autoskillit/core/context_admission.py | 3 ++- tests/core/test_context_admission_reducer.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index fb136b0f2..affe94f9e 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -2619,7 +2619,8 @@ def _rollover( ) fully_resolved = retained_total == 0 snapshot_deducts_unresolved = ( - retained_total > 0 and event.new_snapshot.active_count >= retained_total + retained_total > 0 + and event.new_snapshot.remaining_count <= state.snapshot.remaining_count - retained_total ) authority_alternative_valid = ( receiver_fence_valid diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 97de6336c..b49e27a03 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -2275,7 +2275,7 @@ def test_rollover_accepts_only_one_explicit_authority_alternative() -> None: **_event_fields(state, "rollover-snapshot-deducted", "rollover-epoch"), witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), fence_proof=None, - new_snapshot=_snapshot(epoch=2), + new_snapshot=_snapshot(epoch=2, active_count=70, remaining_count=30), protected_pools=(), ) deducted_rollover = reduce_context_admission(state, deducted) @@ -2283,7 +2283,7 @@ def test_rollover_accepts_only_one_explicit_authority_alternative() -> None: invalid_deduction = replace( deducted, event_id=AdmissionEventId("rollover-invalid-deduction"), - new_snapshot=_snapshot(epoch=2, active_count=9), + new_snapshot=_snapshot(epoch=2), ) rejected = reduce_context_admission(state, invalid_deduction) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT From b6d94b5556e2abaeaee3964c75012e109b9ee9c7 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:10:36 -0700 Subject: [PATCH 33/62] fix(review): scope generation invalidation to batch --- src/autoskillit/core/context_admission.py | 4 +- tests/core/test_context_admission_reducer.py | 59 ++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index affe94f9e..0bb9ae6be 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -1737,7 +1737,7 @@ def _release_closed_batch( generation_records: list[GenerationReservationRecord] = [] revision, sequence = _effect_coordinates(state, capacity_changed=True) for generation in audit.terminal_generation_reservations: - if generation.request_id == record.batch.request_id and generation.state in { + if generation.batch_id == record.batch.batch_id and generation.state in { GenerationState.RESERVED, GenerationState.STREAMING, GenerationState.INDETERMINATE, @@ -1886,7 +1886,7 @@ def _release_or_rollback( generation_records: list[GenerationReservationRecord] = [] revision, sequence = _effect_coordinates(state, capacity_changed=True) for generation in next_state.generation_reservations: - if generation.request_id == record.batch.request_id and generation.state in { + if generation.batch_id == record.batch.batch_id and generation.state in { GenerationState.RESERVED, GenerationState.STREAMING, GenerationState.INDETERMINATE, diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index b49e27a03..9ae1171bb 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -66,6 +66,7 @@ RepresentationBindingWitness, RepresentationRevision, RequestReconciliationEvent, + ReservationInvalidatedEffect, ReserveClass, ReserveRequestEvent, ResolveIndeterminateAcceptedEvent, @@ -2073,6 +2074,64 @@ def test_output_reconciliation_is_bound_to_exact_generation_request() -> None: assert unchanged.state is GenerationState.STREAMING +def test_release_invalidates_generation_for_exact_batch_not_shared_request() -> None: + state = _open_epoch() + first_occurrence = _occurrence("shared-request-first", maximum=10) + second_occurrence = _occurrence("shared-request-second", maximum=10) + state, _ = _propose(state, first_occurrence) + state, _ = _propose(state, second_occurrence) + first_batch = _batch("batch-shared-request-first", (first_occurrence,)) + second_batch_template = _batch("batch-shared-request-second", (second_occurrence,)) + second_batch = replace( + second_batch_template, + request_id=first_batch.request_id, + manifest=replace( + second_batch_template.manifest, + request_id=first_batch.request_id, + ), + ) + first_reserved, _ = _reserve( + state, + first_batch, + (first_occurrence,), + input_count=5, + generation_count=5, + ) + assert isinstance(first_reserved.next_state, ActiveContextAdmissionState) + second_reserved, _ = _reserve( + first_reserved.next_state, + second_batch, + (second_occurrence,), + input_count=5, + generation_count=5, + ) + assert isinstance(second_reserved.next_state, ActiveContextAdmissionState) + before_release = second_reserved.next_state + + released = reduce_context_admission( + before_release, + ReleaseNonAdmissionEvent( + **_event_fields(before_release, "release-shared-request-first", "release"), + batch_id=first_batch.batch_id, + witness=_witness(first_batch, WitnessKind.NON_ADMISSION), + ), + ) + + assert released.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(released.next_state, ActiveContextAdmissionState) + assert tuple(record.batch_id for record in released.next_state.generation_reservations) == ( + second_batch.batch_id, + ) + invalidated_targets = { + effect.target_id + for effect in released.effects + if isinstance(effect, ReservationInvalidatedEffect) + } + assert invalidated_targets == { + GenerationReservationId(f"generation-{first_batch.batch_id.value}") + } + + def test_rollover_moves_old_work_to_resolvable_closed_epoch_audit() -> None: state, batch, _, generation = _reserved_batch( name="closed-resolution", From f222e3e37e04b46a23a5c6343a22785cce88a849 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:11:43 -0700 Subject: [PATCH 34/62] fix(review): validate new generation reservation lifecycle --- .../core/types/_type_context_admission.py | 7 ++++++ tests/core/test_context_admission_reducer.py | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 373a45870..92494a128 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -1155,6 +1155,13 @@ def __post_init__(self) -> None: ): _raise_invalid("reservation_batch_policy_mismatch") generation = self.generation_reservation + if generation is not None and ( + generation.state is not GenerationState.RESERVED + or generation.exact_terminal_usage is not None + or generation.witness_ids + or generation.authority_source_id is not None + ): + _raise_invalid("generation_reservation_not_open") if generation is not None and ( generation.request_id != self.batch.request_id or generation.batch_id != self.batch.batch_id diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 9ae1171bb..a3cf48b76 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -623,6 +623,31 @@ def test_reserve_rejects_active_generation_reservation_id_reuse() -> None: _assert_rejection_unchanged(state, rejected.next_state) +def test_reserve_event_rejects_terminal_generation_record() -> None: + state = _open_epoch() + occurrence = _occurrence("terminal-generation-reserve", maximum=10) + batch = _batch("batch-terminal-generation-reserve", (occurrence,)) + generation = replace( + _generation_reservation(batch, maximum=5), + state=GenerationState.RECONCILED, + exact_terminal_usage=5, + witness_ids=(AdmissionWitnessId("terminal-generation-witness"),), + authority_source_id=AuthoritySourceId("terminal-generation-authority"), + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="generation_reservation_not_open", + ): + ReserveRequestEvent( + **_event_fields(state, "reserve-terminal-generation", "reserve-request"), + batch=batch, + snapshot_sequence=state.snapshot.snapshot_sequence, + input_reservations=(_reservation(batch, (occurrence,), count=5),), + generation_reservation=generation, + ) + + def test_active_state_rejects_global_capacity_overallocation() -> None: state, _, _, _ = _reserved_batch( remaining_count=60, From 6a4a099fb635797df3794dccf83786f63ce8d64c Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:12:54 -0700 Subject: [PATCH 35/62] fix(review): reject closed batch identifier reuse --- src/autoskillit/core/context_admission.py | 5 ++++- tests/core/test_context_admission_reducer.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index 0bb9ae6be..54bc7914d 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -901,7 +901,10 @@ def _reserve( return _reject(state, event, "epoch-uninitialized") if event.snapshot_sequence != state.snapshot.snapshot_sequence: return _reject(state, event, "snapshot-sequence-mismatch") - if _batch_record(state, event.batch.batch_id) is not None: + if ( + _batch_record(state, event.batch.batch_id) is not None + or _closed_batch_location(state, event.batch.batch_id) is not None + ): return _reject(state, event, "batch-already-reserved") reservation = event.input_reservations[0] if any( diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index a3cf48b76..943473862 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -1389,6 +1389,26 @@ def test_rollover_invalidates_undispatched_work_and_preserves_closed_audits() -> assert rerolled.next_state.closed_epochs[0] == rolled.next_state.closed_epochs[0] +def test_reserve_rejects_batch_id_retained_in_closed_epoch() -> None: + state, batch, _, _ = _reserved_batch(name="closed-batch-id") + rolled = _rollover_with_receiver_fence(state, batch, name="closed-batch-id") + occurrence = _occurrence("closed-batch-id-new", maximum=5, epoch=2) + rolled, _ = _propose(rolled, occurrence) + reused_batch = _batch(batch.batch_id.value, (occurrence,)) + + rejected, _ = _reserve( + rolled, + reused_batch, + (occurrence,), + input_count=5, + ) + + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert rejected.decision.reason_code == "batch-already-reserved" + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_rejection_unchanged(rolled, rejected.next_state) + + def test_closed_epoch_occurrence_identity_is_immutable_and_exact_retry_is_idempotent() -> None: state, batch, occurrences, _ = _reserved_batch(name="closed-occurrence") rolled = _rollover_with_receiver_fence( From 7394edbd1e65a78dc514e84d1673d276de499ce8 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:13:47 -0700 Subject: [PATCH 36/62] fix(review): require reservation owner for active batch --- .../core/types/_type_context_admission.py | 11 ++++++++++ tests/core/test_context_admission_reducer.py | 22 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 92494a128..e930b10d0 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -1954,6 +1954,17 @@ def __post_init__(self) -> None: if record.reservation_id is not None else None ) + if ( + record.state + in { + AdmissionState.RESERVED, + AdmissionState.PREPARED, + AdmissionState.HISTORY_STAGED, + AdmissionState.REQUEST_DISPATCHED, + } + and matched_reservation is None + ): + _raise_invalid("missing_active_batch_reservation") if matched_reservation is not None and ( matched_reservation.key.batch_id != record.batch.batch_id or matched_reservation.reserve_class is not record.batch.reserve_class diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 943473862..d7c6ce486 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -667,6 +667,28 @@ def test_active_state_rejects_global_capacity_overallocation() -> None: assert "context_capacity_overallocated" in str(exc_info.value) +def test_active_state_requires_dispatched_batch_reservation_owner() -> None: + state, batch, _, _ = _reserved_batch(name="missing-dispatched-reservation") + state = _prepare_dispatch(state, batch) + batch_record = _batch_record(state, batch) + + with pytest.raises( + ContextAdmissionValidationError, + match="missing_active_batch_reservation", + ): + replace( + state, + occurrence_records=tuple( + replace(record, reservation_id=None) + if record.batch_id == batch.batch_id + else record + for record in state.occurrence_records + ), + batch_records=(replace(batch_record, reservation_id=None),), + reservations=(), + ) + + def _prepare_dispatch( state: ActiveContextAdmissionState, batch: AdmissionBatch ) -> ActiveContextAdmissionState: From d31748eabd53932198358e35474134a2fb7197ba Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:14:42 -0700 Subject: [PATCH 37/62] test(review): cover authority unavailable effects --- .../test_context_admission_state_machine.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 1123b60d8..470ada21b 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -32,6 +32,8 @@ AgentInstanceId, AggregateRevision, AuthoritySourceId, + AuthorityUnavailableEffect, + AuthorityUnavailableEvent, CanonicalRepresentationManifest, CanonicalSpanId, CanonicalSpanOwner, @@ -40,6 +42,7 @@ ContextSessionId, ContextThreadId, ContextWindowSnapshot, + CoverageState, DeliveryOccurrenceId, DispatchRequestEvent, EpochClosedEffect, @@ -459,6 +462,8 @@ def _expected_effect_types( transition: Any, event: object, ) -> tuple[type[object], ...]: + if isinstance(event, AuthorityUnavailableEvent): + return (AuthorityUnavailableEffect,) if transition.decision.kind not in { AdmissionDecisionKind.WOULD_ADMIT, AdmissionDecisionKind.QUARANTINED, @@ -613,6 +618,22 @@ def _availability(self, reserve_class: ReserveClass) -> tuple[int, int, int]: class_available = ordinary_available return global_unallocated, ordinary_available, class_available + @rule(authority_state=st.sampled_from((CoverageState.PARTIAL, CoverageState.UPSTREAM_GATED))) + def report_authority_unavailable(self, authority_state: CoverageState) -> None: + event = AuthorityUnavailableEvent( + **self._fields("authority-unavailable"), + reason_code="authority-watermark-unavailable", + authority_state=authority_state, + ) + transition = reduce_context_admission(self.state, event) + expected = ( + AdmissionDecisionKind.UPSTREAM_GATED + if authority_state is CoverageState.UPSTREAM_GATED + else AdmissionDecisionKind.WATERMARK_UNAVAILABLE + ) + assert transition.decision.kind is expected + self._accept_publication(transition, event) + @rule(slot=_SLOT, reserve_class=_RESERVE_CLASS) def propose(self, slot: int, reserve_class: ReserveClass) -> None: if slot in self.occurrences: From 1214661f5eaffdbd6ee308d678deed09976565d1 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:17:35 -0700 Subject: [PATCH 38/62] test(review): drive multi-member batch lifecycle --- .../test_context_admission_state_machine.py | 107 ++++++++++++++---- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 470ada21b..6574b8791 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -408,6 +408,7 @@ def __init__(self) -> None: assert isinstance(opened.next_state, ActiveContextAdmissionState) self.state = opened.next_state self.occurrences: dict[int, AdmissionOccurrence] = {} + self.batches: dict[int, AdmissionBatch] = {} self.charges: dict[int, tuple[ReserveClass, int, int]] = {} self.latest_replayable_event: Any | None = open_event self.latest_propose_event: ProposeOccurrenceEvent | None = None @@ -425,6 +426,34 @@ def _find_batch(self, batch_id: AdmissionBatchId) -> Any: None, ) + def _reserved_input_count(self, batch: AdmissionBatch) -> int | None: + record = self._find_batch(batch.batch_id) + if record is None or record.reservation_id is None: + return None + reservation = next( + ( + reservation + for reservation in self.state.reservations + if reservation.reservation_id == record.reservation_id + ), + None, + ) + return reservation.reserved_count if reservation is not None else None + + def _clear_batch_charges(self, batch: AdmissionBatch) -> None: + for slot, tracked_batch in self.batches.items(): + if tracked_batch.batch_id != batch.batch_id or slot not in self.charges: + continue + reserve_class, _, _ = self.charges[slot] + self.charges[slot] = (reserve_class, 0, 0) + + def _clear_generation_charges(self, batch: AdmissionBatch) -> None: + for slot, tracked_batch in self.batches.items(): + if tracked_batch.batch_id != batch.batch_id or slot not in self.charges: + continue + reserve_class, input_count, _ = self.charges[slot] + self.charges[slot] = (reserve_class, input_count, 0) + def _fields(self, operation_kind: str) -> dict[str, object]: self.event_sequence += 1 return { @@ -679,6 +708,7 @@ def reserve(self, slot: int, input_count: int, generation_count: int) -> None: ) if expected_admit: self._accept_publication(transition, event) + self.batches[slot] = batch self.charges[slot] = ( occurrence.reserve_class, input_count, @@ -774,6 +804,8 @@ def reserve_multi_member_batch(self, slot: int, input_count: int) -> None: return assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT self._accept_publication(transition, event) + self.batches[slot] = batch + self.batches[other_slot] = batch self.charges[slot] = (first.reserve_class, total, 0) self.charges[other_slot] = (second.reserve_class, 0, 0) @@ -782,15 +814,19 @@ def prepare_stage_dispatch_and_accept(self, accept_now: bool) -> None: if not self.charges: return slot = min(self.charges) - reserve_class, input_count, _ = self.charges[slot] + reserve_class, _, _ = self.charges[slot] if reserve_class is ReserveClass.ORDINARY: return occurrence = self.occurrences[slot] - batch = _batch(occurrence) + batch = self.batches.get(slot) + if batch is None: + return reserved = self._find_batch(batch.batch_id) if reserved is None or reserved.state is not AdmissionState.RESERVED: return - reserved_input_count = input_count + reserved_input_count = self._reserved_input_count(batch) + if reserved_input_count is None: + return prepare_event = PrepareBatchEvent( **self._fields("prepare-batch"), batch_id=batch.batch_id, @@ -870,12 +906,15 @@ def prepare_and_mark_indeterminate(self) -> None: if not self.charges: return slot = min(self.charges) - _, input_count, _ = self.charges[slot] - occurrence = self.occurrences[slot] - batch = _batch(occurrence) + batch = self.batches.get(slot) + if batch is None: + return reserved = self._find_batch(batch.batch_id) if reserved is None or reserved.state is not AdmissionState.RESERVED: return + input_count = self._reserved_input_count(batch) + if input_count is None: + return prepare_event = PrepareBatchEvent( **self._fields("prepare-batch"), batch_id=batch.batch_id, @@ -903,7 +942,9 @@ def release_or_rollback_preacceptance(self, rollback: bool) -> None: occurrence = self.occurrences.get(slot) if occurrence is None: continue - batch = _batch(occurrence) + batch = self.batches.get(slot) + if batch is None: + continue record = self._find_batch(batch.batch_id) if record is None: continue @@ -938,8 +979,7 @@ def release_or_rollback_preacceptance(self, rollback: bool) -> None: transition = reduce_context_admission(self.state, event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT self._accept_publication(transition, event) - reserve_class, _, _ = self.charges[slot] - self.charges[slot] = (reserve_class, 0, 0) + self._clear_batch_charges(batch) return @rule(rollback=st.booleans()) @@ -948,7 +988,9 @@ def resolve_indeterminate_nonacceptance(self, rollback: bool) -> None: occurrence = self.occurrences.get(slot) if occurrence is None: continue - batch = _batch(occurrence) + batch = self.batches.get(slot) + if batch is None: + continue record = self._find_batch(batch.batch_id) if record is None or record.state is not AdmissionState.INDETERMINATE: continue @@ -971,8 +1013,7 @@ def resolve_indeterminate_nonacceptance(self, rollback: bool) -> None: transition = reduce_context_admission(self.state, event) assert transition.decision.kind is AdmissionDecisionKind.WOULD_ADMIT self._accept_publication(transition, event) - reserve_class, _, _ = self.charges[slot] - self.charges[slot] = (reserve_class, 0, 0) + self._clear_batch_charges(batch) return @rule(exact_usage=st.integers(min_value=0, max_value=16)) @@ -984,14 +1025,17 @@ def start_and_reconcile_generation(self, exact_usage: int) -> None: ( (slot, occurrence) for slot, occurrence in self.occurrences.items() - if _batch(occurrence).batch_id == generation.batch_id + if ( + self.batches.get(slot) is not None + and self.batches[slot].batch_id == generation.batch_id + ) ), None, ) if slot_and_occurrence is None: continue slot, occurrence = slot_and_occurrence - batch = _batch(occurrence) + batch = self.batches[slot] record = self._find_batch(batch.batch_id) if record is None or record.state not in { AdmissionState.HISTORY_STAGED, @@ -1030,8 +1074,7 @@ def start_and_reconcile_generation(self, exact_usage: int) -> None: ) assert reconciled.decision.kind is expected self._accept_publication(reconciled, reconcile) - reserve_class, input_count, _ = self.charges[slot] - self.charges[slot] = (reserve_class, input_count, 0) + self._clear_generation_charges(batch) return @rule() @@ -1087,8 +1130,11 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: } retained_slots = { slot - for slot, occurrence in self.occurrences.items() - if _batch(occurrence).batch_id in retained_batch_ids + for slot in self.occurrences + if ( + self.batches.get(slot) is not None + and self.batches[slot].batch_id in retained_batch_ids + ) } old_snapshot = self.state.snapshot new_window_epoch_number = old_snapshot.window_epoch_number + 1 @@ -1161,6 +1207,7 @@ def rollover_preserves_dispatched_and_indeterminate_charge(self) -> None: assert record.batch.reserve_class is ReserveClass.ORDINARY self._accept_publication(transition, rollover_event) self.occurrences = {} + self.batches = {} self.charges = {} self.last_rollover_retention = prior_audit.retained_unresolved_count @@ -1204,11 +1251,11 @@ def changed_intent_conflict_does_not_mutate(self) -> None: @rule() def reconciliation_deadline_never_releases(self) -> None: slot = min(self.charges) - occurrence = self.occurrences[slot] + batch = self.batches[slot] before = dict(self.charges) event = RequestReconciliationEvent( **self._fields("request-reconciliation"), - target_id=_batch(occurrence).batch_id, + target_id=batch.batch_id, reason_code="deadline-observed", ) transition = reduce_context_admission(self.state, event) @@ -1268,6 +1315,26 @@ def no_batch_is_partially_reserved_or_double_charged(self) -> None: } +def test_multi_member_batch_progresses_through_full_lifecycle() -> None: + machine = ContextAdmissionStateMachine() + machine.propose(0, ReserveClass.SYNTHESIS) + machine.propose(1, ReserveClass.SYNTHESIS) + machine.reserve_multi_member_batch(0, 5) + batch = machine.batches[0] + assert machine.batches[1] == batch + + machine.prepare_stage_dispatch_and_accept(accept_now=True) + + record = machine._find_batch(batch.batch_id) + assert record is not None + assert record.state is AdmissionState.COMMITTED + assert { + occurrence_record.state + for occurrence_record in machine.state.occurrence_records + if occurrence_record.occurrence.occurrence_id in batch.occurrence_ids + } == {AdmissionState.COMMITTED} + + ContextAdmissionStateMachine.TestCase.settings = settings( max_examples=24, stateful_step_count=12, From 3dc5477ec68ebc73f4f98200ee9f501246847401 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:23:06 -0700 Subject: [PATCH 39/62] test(review): preserve multi-member reservation revisions --- tests/core/test_context_admission_state_machine.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 6574b8791..453abe7a4 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -296,11 +296,13 @@ def _reservation( def _reservation_for_batch( batch: AdmissionBatch, - occurrence: AdmissionOccurrence, + occurrences: tuple[AdmissionOccurrence, ...], count: int, ) -> AdmissionReservation: occurrence_ids = batch.occurrence_ids + occurrences_by_id = {occurrence.occurrence_id: occurrence for occurrence in occurrences} slot = occurrence_ids[0].value.rsplit("-", 1)[-1] + occurrence = occurrences[0] window_epoch_id = occurrence.lineage.window_epoch_id window_epoch_number = occurrence.lineage.window_epoch_number key = AdmissionReservationKey( @@ -312,7 +314,10 @@ def _reservation_for_batch( reserve_class=batch.reserve_class, protected_pool_owner_id=batch.protected_pool_owner_id, occurrence_revisions=tuple( - (occurrence_id, batch.manifest.representation_revision) + ( + occurrence_id, + occurrences_by_id[occurrence_id].representation_revision, + ) for occurrence_id in occurrence_ids ), ) @@ -794,7 +799,7 @@ def reserve_multi_member_batch(self, slot: int, input_count: int) -> None: **self._fields("reserve-multi"), batch=batch, snapshot_sequence=1, - input_reservations=(_reservation_for_batch(batch, first, total),), + input_reservations=(_reservation_for_batch(batch, (first, second), total),), generation_reservation=None, ) prior = self.state From 8e9418f9f4bc0c95a45af49aba6f1387a4caafa4 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:24:01 -0700 Subject: [PATCH 40/62] test(review): align rollover fixtures with deduction evidence --- tests/core/test_context_admission_reducer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index d7c6ce486..76b8e6ae0 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -2349,10 +2349,11 @@ def test_protected_release_uses_policy_from_rollover_created_epoch() -> None: **_event_fields(state, "rollover-policy-close", "rollover-epoch"), witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER, epoch=2), fence_proof=None, - new_snapshot=_snapshot(epoch=3), + new_snapshot=_snapshot(epoch=3, active_count=70, remaining_count=30), protected_pools=(), ), ) + assert rolled_again.decision.kind is AdmissionDecisionKind.WOULD_ADMIT assert isinstance(rolled_again.next_state, ActiveContextAdmissionState) resolved = reduce_context_admission( rolled_again.next_state, @@ -2409,7 +2410,7 @@ def test_rollover_accepts_only_one_explicit_authority_alternative() -> None: invalid_deduction = replace( deducted, event_id=AdmissionEventId("rollover-invalid-deduction"), - new_snapshot=_snapshot(epoch=2), + new_snapshot=_snapshot(epoch=2, active_count=40, remaining_count=60), ) rejected = reduce_context_admission(state, invalid_deduction) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT From a27ead2f4dc6b06754e519a7b7ba2160b34609ab Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 14:24:29 -0700 Subject: [PATCH 41/62] test(review): avoid closed batch retry event collision --- tests/core/test_context_admission_reducer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 76b8e6ae0..deedc2a28 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -1423,6 +1423,7 @@ def test_reserve_rejects_batch_id_retained_in_closed_epoch() -> None: reused_batch, (occurrence,), input_count=5, + event_id="reserve-closed-batch-id-reused", ) assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT From 740c99dd56de4c9db2112d842a617279872375a8 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:08:04 -0700 Subject: [PATCH 42/62] fix(review): retain rollover generations by batch --- src/autoskillit/core/context_admission.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index 54bc7914d..b18a9deac 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -2580,8 +2580,8 @@ def _rollover( AdmissionState.INDETERMINATE, } ) - retained_generation_request_ids = { - record.batch.request_id + retained_generation_batch_ids = { + record.batch.batch_id for record in state.batch_records if record.state in { @@ -2603,7 +2603,7 @@ def _rollover( retained_generation_count = sum( generation.maximum_allowance for generation in state.generation_reservations - if generation.request_id in retained_generation_request_ids + if generation.batch_id in retained_generation_batch_ids and generation.state in { GenerationState.RESERVED, @@ -2674,7 +2674,7 @@ def _rollover( GenerationState.STREAMING, GenerationState.INDETERMINATE, } - and generation.request_id not in retained_generation_request_ids + and generation.batch_id not in retained_generation_batch_ids ) invalidated_generation_ids = { generation.generation_reservation_id for generation in invalidated_generation_reservations From c37bf47923bcbb9c1edbded7158553930b9a39e9 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:08:56 -0700 Subject: [PATCH 43/62] fix(review): reject closed generation identifier reuse --- src/autoskillit/core/context_admission.py | 13 ++++-- tests/core/test_context_admission_reducer.py | 45 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index b18a9deac..6e9dfdd03 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -965,9 +965,16 @@ def _reserve( return _reject(state, event, "reservation-revision-mismatch") generation = event.generation_reservation generation_count = generation.maximum_allowance if generation is not None else 0 - if generation is not None and any( - existing.generation_reservation_id == generation.generation_reservation_id - for existing in state.generation_reservations + if generation is not None and ( + any( + existing.generation_reservation_id == generation.generation_reservation_id + for existing in state.generation_reservations + ) + or any( + existing.generation_reservation_id == generation.generation_reservation_id + for audit in state.closed_epochs + for existing in audit.terminal_generation_reservations + ) ): return _reject( state, diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index deedc2a28..e66db4a5c 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -623,6 +623,51 @@ def test_reserve_rejects_active_generation_reservation_id_reuse() -> None: _assert_rejection_unchanged(state, rejected.next_state) +def test_reserve_rejects_generation_reservation_id_retained_in_closed_epoch() -> None: + state, batch, _, existing_generation = _reserved_batch( + name="closed-generation-id-owner", + input_count=5, + generation_count=5, + ) + state = _prepare_dispatch(state, batch) + rolled = _rollover_with_receiver_fence( + state, + batch, + name="closed-generation-id-owner", + ) + occurrence = _occurrence( + "closed-generation-id-collision", + maximum=5, + epoch=2, + ) + rolled, _ = _propose(rolled, occurrence) + new_batch = _batch("batch-closed-generation-id-collision", (occurrence,)) + _, event = _reserve( + rolled, + new_batch, + (occurrence,), + input_count=5, + generation_count=5, + ) + assert event.generation_reservation is not None + collision = replace( + event, + generation_reservation=replace( + event.generation_reservation, + generation_reservation_id=existing_generation.generation_reservation_id, + ), + ) + + rejected = reduce_context_admission(rolled, collision) + + assert rejected.decision.kind is AdmissionDecisionKind.WOULD_REJECT + assert ( + rejected.decision.reason_code == "generation-reservation-id-reuse-with-changed-descriptor" + ) + assert isinstance(rejected.next_state, ActiveContextAdmissionState) + _assert_rejection_unchanged(rolled, rejected.next_state) + + def test_reserve_event_rejects_terminal_generation_record() -> None: state = _open_epoch() occurrence = _occurrence("terminal-generation-reserve", maximum=10) From 7f715281ba514b0884d58349c1ff3f7deb506531 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:09:32 -0700 Subject: [PATCH 44/62] fix(review): constrain coverage evidence schema --- .../core/types/_type_context_admission.py | 4 ++ .../types/test_context_admission_contract.py | 60 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index e930b10d0..99da79217 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -1030,6 +1030,8 @@ def __post_init__(self) -> None: _validate_git_revision(self.tested_revision) _validate_iso_date(self.checked_at) _validate_freshness_policy(self.freshness_policy) + if self.freshness_policy != "verify_on_version_or_configuration_change": + _raise_invalid("unsupported_coverage_freshness_policy") _validate_bounded_text( self.source_locator, "invalid_source_locator", @@ -1065,6 +1067,8 @@ def __post_init__(self) -> None: ) if not self.evidence: _raise_invalid("coverage_evidence_required") + if len(self.evidence) != 1: + _raise_invalid("single_coverage_evidence_required") primary = tuple( item for item in self.evidence if item.kind is not CoverageEvidenceKind.INFERENCE ) diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index bd7253f5f..0ae2cc8e8 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1619,6 +1619,66 @@ def test_coverage_evidence_enforces_semantic_provenance_fields( assert reason_code in str(exc_info.value) +@pytest.mark.parametrize( + "freshness_policy", + ["verify_on_revision_change", "infer_only"], +) +def test_coverage_v1_rejects_unimplemented_freshness_policies( + freshness_policy: str, +) -> None: + evidence = CoverageEvidence( + claim_id="COV-v1-policy", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="src/autoskillit/core/context_admission.py", + tested_version="0.10.890", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="unsupported_coverage_freshness_policy", + ): + replace(evidence, freshness_policy=freshness_policy) + + +def test_coverage_v1_requires_exactly_one_evidence_record() -> None: + evidence = CoverageEvidence( + claim_id="COV-v1-cardinality", + kind=CoverageEvidenceKind.AUTOSKILLIT_SOURCE, + backend="autoskillit", + configuration_mode="default", + verifier="source_inspection", + source_locator="src/autoskillit/core/context_admission.py", + tested_version="0.10.890", + tested_revision="ac8f653a00d24b6be50ef285958cfb0e1b7a351b", + checked_at="2026-07-23", + freshness_policy="verify_on_version_or_configuration_change", + ) + additional = replace( + evidence, + claim_id="COV-v1-cardinality-secondary", + source_locator="src/autoskillit/core/context_admission_secondary.py", + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="single_coverage_evidence_required", + ): + ProducerCoverageDef( + surface=ProducerSurface.NATIVE_SHELL, + control_point_owner="shell_capture_hook", + observation_state=CoverageState.VERIFIED, + authority_state=CoverageState.UPSTREAM_GATED, + evidence=(evidence, additional), + reason_code="authoritative-watermark-unavailable", + ) + + def test_prepare_event_rejects_estimate_measurement() -> None: occurrence = _occurrence() batch = _batch("batch-estimate", (occurrence,)) From d0f50f6ab44872be403e76e9eb3d6ff88ae69cd8 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:10:39 -0700 Subject: [PATCH 45/62] fix(review): validate idempotency tombstone bindings --- .../core/types/_type_context_admission.py | 29 ++++++++++++ .../types/test_context_admission_contract.py | 44 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 99da79217..fe8acfeaf 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -878,6 +878,35 @@ class ExpiredIdempotencyTombstone(_ContractValue): expiry_witness: AdmissionWitness original_terminal_decision: AdmissionDecision + def __post_init__(self) -> None: + input_reservations = self.original_descriptor.input_reservations + batch = self.original_descriptor.batch + if ( + self.namespace != self.original_descriptor.idempotency_namespace + or self.reservation_key.idempotency_namespace != self.namespace + or len(input_reservations) != 1 + or self.reservation_key != input_reservations[0].key + or self.reservation_key.batch_id != batch.batch_id + or self.original_terminal_decision.window_epoch_id + != self.reservation_key.window_epoch_id + or self.original_terminal_decision.snapshot_sequence + != self.original_descriptor.snapshot_sequence + ): + _raise_invalid("idempotency_tombstone_identity_mismatch") + witness = self.expiry_witness + if ( + witness.kind is not WitnessKind.IDEMPOTENCY_EXPIRY + or witness.window_epoch_id != self.reservation_key.window_epoch_id + or witness.window_epoch_number != self.reservation_key.window_epoch_number + or witness.snapshot_sequence != self.original_descriptor.snapshot_sequence + or witness.request_id != batch.request_id + or witness.batch_id != batch.batch_id + or witness.representation_revision != batch.manifest.representation_revision + or witness.representation_binding_id != batch.manifest.representation_binding_id + or witness.occurrence_ids != batch.occurrence_ids + ): + _raise_invalid("idempotency_tombstone_witness_mismatch") + @dataclass(frozen=True, slots=True) class ClosedEpochAudit(_ContractValue): diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index 0ae2cc8e8..dca60952c 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1555,6 +1555,50 @@ def test_journal_records_cross_check_duplicated_event_identities() -> None: replace(idempotency, **mismatch) assert "idempotency_record_identity_mismatch" in str(idempotency_error.value) + tombstone = ExpiredIdempotencyTombstone( + namespace=namespace, + reservation_key=reservation_key, + original_descriptor=event, + expiry_witness=_witness(batch, WitnessKind.IDEMPOTENCY_EXPIRY), + original_terminal_decision=decision, + ) + identity_mismatches = ( + {"namespace": _namespace("different-operation")}, + { + "reservation_key": replace( + reservation_key, + batch_id=AdmissionBatchId("different-tombstone-key"), + ) + }, + { + "original_terminal_decision": replace( + decision, + window_epoch_id=WindowEpochId("different-decision-epoch"), + ) + }, + ) + for mismatch in identity_mismatches: + with pytest.raises( + ContextAdmissionValidationError, + match="idempotency_tombstone_identity_mismatch", + ): + replace(tombstone, **mismatch) + + witness_mismatches = ( + {"kind": WitnessKind.PROVIDER_ACCEPTED}, + {"request_id": AdmissionRequestId("different-tombstone-request")}, + {"batch_id": AdmissionBatchId("different-tombstone-batch")}, + ) + for mismatch in witness_mismatches: + with pytest.raises( + ContextAdmissionValidationError, + match="idempotency_tombstone_witness_mismatch", + ): + replace( + tombstone, + expiry_witness=replace(tombstone.expiry_witness, **mismatch), + ) + def test_privacy_canaries_are_rejected_from_coverage_evidence() -> None: with pytest.raises(ContextAdmissionValidationError): From 9ea462e30b7974fca912fc0019eace8e7a788fe8 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:11:42 -0700 Subject: [PATCH 46/62] fix(review): validate state journal coordinates --- .../core/types/_type_context_admission.py | 30 ++++++++++-- .../types/test_context_admission_contract.py | 47 +++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index fe8acfeaf..fbf83827d 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -1721,7 +1721,9 @@ def __post_init__(self) -> None: _raise_invalid("idempotency_record_identity_mismatch") -def _validate_state_metadata_uniqueness( +def _validate_state_metadata( + aggregate_revision: AggregateRevision, + admission_sequence: AdmissionSequence, processed_events: tuple[ProcessedEventRecord, ...], idempotency_records: tuple[IdempotencyRecord, ...], expired_tombstones: tuple[ExpiredIdempotencyTombstone, ...], @@ -1729,11 +1731,29 @@ def _validate_state_metadata_uniqueness( ) -> None: if len({record.event_id for record in processed_events}) != len(processed_events): _raise_invalid("duplicate_processed_event") + processed_revisions = tuple(record.aggregate_revision.value for record in processed_events) + processed_sequences = tuple(record.admission_sequence.value for record in processed_events) + if ( + len(set(processed_revisions)) != len(processed_revisions) + or any(revision > aggregate_revision.value for revision in processed_revisions) + or any(sequence > admission_sequence.value for sequence in processed_sequences) + or any( + later < earlier for earlier, later in zip(processed_sequences, processed_sequences[1:]) + ) + ): + _raise_invalid("invalid_processed_event_coordinates") idempotency_keys = tuple( (record.namespace, record.reservation_key) for record in idempotency_records ) if len(set(idempotency_keys)) != len(idempotency_keys): _raise_invalid("duplicate_idempotency_owner") + processed_by_event_id = {record.event_id: record for record in processed_events} + for record in idempotency_records: + processed = processed_by_event_id.get(record.owning_event_id) + if record.publication_revision.value > aggregate_revision.value or ( + processed is not None and record.publication_revision != processed.aggregate_revision + ): + _raise_invalid("invalid_idempotency_publication_coordinates") tombstone_keys = tuple( (record.namespace, record.reservation_key) for record in expired_tombstones ) @@ -1785,7 +1805,9 @@ def __post_init__(self) -> None: "noncanonical_closed_epochs", key=lambda audit: audit.snapshot.window_epoch_number, ) - _validate_state_metadata_uniqueness( + _validate_state_metadata( + self.aggregate_revision, + self.admission_sequence, self.processed_events, self.idempotency_records, self.expired_idempotency_tombstones, @@ -1868,7 +1890,9 @@ def __post_init__(self) -> None: "noncanonical_closed_epochs", key=lambda audit: audit.snapshot.window_epoch_number, ) - _validate_state_metadata_uniqueness( + _validate_state_metadata( + self.aggregate_revision, + self.admission_sequence, self.processed_events, self.idempotency_records, self.expired_idempotency_tombstones, diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index dca60952c..ac867caf5 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1555,6 +1555,53 @@ def test_journal_records_cross_check_duplicated_event_identities() -> None: replace(idempotency, **mismatch) assert "idempotency_record_identity_mismatch" in str(idempotency_error.value) + with pytest.raises( + ContextAdmissionValidationError, + match="invalid_processed_event_coordinates", + ): + UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(processed,), + idempotency_records=(), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="invalid_idempotency_publication_coordinates", + ): + UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(0), + admission_sequence=AdmissionSequence(0), + processed_events=(), + idempotency_records=(idempotency,), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + + with pytest.raises( + ContextAdmissionValidationError, + match="invalid_idempotency_publication_coordinates", + ): + UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(1), + admission_sequence=AdmissionSequence(1), + processed_events=(processed,), + idempotency_records=( + replace( + idempotency, + publication_revision=AggregateRevision(0), + ), + ), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + tombstone = ExpiredIdempotencyTombstone( namespace=namespace, reservation_key=reservation_key, From 9b40f170351ab603e3b37f6b2f94496b588d56e6 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:12:29 -0700 Subject: [PATCH 47/62] fix(review): require indeterminate reservation owner --- .../core/types/_type_context_admission.py | 1 + tests/core/test_context_admission_reducer.py | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index fbf83827d..0eb701b5f 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -2018,6 +2018,7 @@ def __post_init__(self) -> None: AdmissionState.PREPARED, AdmissionState.HISTORY_STAGED, AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, } and matched_reservation is None ): diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index e66db4a5c..ee1131d88 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -734,6 +734,48 @@ def test_active_state_requires_dispatched_batch_reservation_owner() -> None: ) +def test_active_state_requires_indeterminate_batch_reservation_owner() -> None: + state, batch, _, _ = _reserved_batch(name="missing-indeterminate-reservation") + state = _prepare_dispatch(state, batch) + marked = reduce_context_admission( + state, + MarkIndeterminateEvent( + **_event_fields( + state, + "mark-missing-indeterminate-reservation", + "mark-indeterminate", + ), + batch_id=batch.batch_id, + reason_code="provider-result-lost", + ), + ) + assert isinstance(marked.next_state, ActiveContextAdmissionState) + state = marked.next_state + batch_record = _batch_record(state, batch) + + with pytest.raises( + ContextAdmissionValidationError, + match="missing_active_batch_reservation", + ): + replace( + state, + occurrence_records=tuple( + replace(record, reservation_id=None) + if record.batch_id == batch.batch_id + else record + for record in state.occurrence_records + ), + batch_records=( + replace( + batch_record, + reservation_id=None, + unresolved_input_count=0, + ), + ), + reservations=(), + ) + + def _prepare_dispatch( state: ActiveContextAdmissionState, batch: AdmissionBatch ) -> ActiveContextAdmissionState: From 7549263019e9829a2c7f3072cf27a3d6827d5c54 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:13:24 -0700 Subject: [PATCH 48/62] test(review): scope rollover oracle by batch --- .../test_context_admission_state_machine.py | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 453abe7a4..50492569a 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -579,8 +579,8 @@ def _expected_effect_types( reservation.key.batch_id in invalidated_batch_ids for reservation in self.state.reservations ) - retained_request_ids = { - record.batch.request_id + retained_batch_ids = { + record.batch.batch_id for record in self.state.batch_records if record.state in { @@ -597,7 +597,7 @@ def _expected_effect_types( GenerationState.STREAMING, GenerationState.INDETERMINATE, } - and generation.request_id not in retained_request_ids + and generation.batch_id not in retained_batch_ids for generation in self.state.generation_reservations ) occurrence_change_count = sum( @@ -1340,6 +1340,56 @@ def test_multi_member_batch_progresses_through_full_lifecycle() -> None: } == {AdmissionState.COMMITTED} +def test_rollover_scopes_shared_request_generation_retention_to_batch() -> None: + machine = ContextAdmissionStateMachine() + machine.propose(0, ReserveClass.ORDINARY) + machine.propose(1, ReserveClass.ORDINARY) + machine.reserve(0, input_count=5, generation_count=4) + machine.reserve(1, input_count=5, generation_count=3) + retained_batch = machine.batches[0] + invalidated_batch = machine.batches[1] + invalidated_batch = replace( + invalidated_batch, + request_id=retained_batch.request_id, + manifest=replace( + invalidated_batch.manifest, + request_id=retained_batch.request_id, + ), + ) + machine.batches[1] = invalidated_batch + machine.state = replace( + machine.state, + batch_records=tuple( + replace(record, batch=invalidated_batch) + if record.batch.batch_id == invalidated_batch.batch_id + else record + for record in machine.state.batch_records + ), + generation_reservations=tuple( + replace(generation, request_id=retained_batch.request_id) + if generation.batch_id == invalidated_batch.batch_id + else generation + for generation in machine.state.generation_reservations + ), + ) + + machine.prepare_stage_dispatch_and_accept(accept_now=True) + machine.rollover_preserves_dispatched_and_indeterminate_charge() + + audit = machine.state.closed_epochs[-1] + assert audit.retained_generation_count == 4 + assert tuple(generation.batch_id for generation in audit.terminal_generation_reservations) == ( + retained_batch.batch_id, + ) + assert ( + sum( + isinstance(effect, ReservationInvalidatedEffect) + for effect in machine.published_effects[-1] + ) + == 2 + ) + + ContextAdmissionStateMachine.TestCase.settings = settings( max_examples=24, stateful_step_count=12, From 5848771d4a90f15ec9546e5ea6dc149771fa0919 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:18:45 -0700 Subject: [PATCH 49/62] fix(review): publish journal metadata atomically --- src/autoskillit/core/context_admission.py | 44 +++++++++---------- .../core/types/_type_context_admission.py | 2 +- .../types/test_context_admission_contract.py | 14 ++++++ 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index 6e9dfdd03..da6816620 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -354,6 +354,7 @@ def _publish( protected_pool_owner_id: ProtectedPoolOwnerId | None = None, capacity_changed: bool = False, effects: tuple[AdmissionEffect, ...] = (), + idempotency_record: IdempotencyRecord | None = None, ) -> AdmissionTransition: aggregate_revision = type(prior_state.aggregate_revision)( prior_state.aggregate_revision.value + 1 @@ -381,6 +382,17 @@ def _publish( aggregate_revision=aggregate_revision, admission_sequence=admission_sequence, ) + idempotency_records = published.idempotency_records + if idempotency_record is not None: + idempotency_records = tuple( + sorted( + idempotency_records + (idempotency_record,), + key=lambda item: ( + item.publication_revision.value, + item.owning_event_id.value, + ), + ) + ) published = replace( published, processed_events=tuple( @@ -392,6 +404,7 @@ def _publish( ), ) ), + idempotency_records=idempotency_records, ) return AdmissionTransition( next_state=published, @@ -1080,29 +1093,13 @@ def _reserve( reserve_class=event.batch.reserve_class, protected_pool_owner_id=event.batch.protected_pool_owner_id, ) - next_state = replace( - next_state, - idempotency_records=tuple( - sorted( - next_state.idempotency_records - + ( - IdempotencyRecord( - namespace=event.idempotency_namespace, - reservation_key=reservation.key, - original_descriptor=event, - original_reserve_decision=reserve_decision, - owning_event_id=event.event_id, - publication_revision=type(state.aggregate_revision)( - state.aggregate_revision.value + 1 - ), - ), - ), - key=lambda item: ( - item.publication_revision.value, - item.owning_event_id.value, - ), - ), - ), + idempotency_record = IdempotencyRecord( + namespace=event.idempotency_namespace, + reservation_key=reservation.key, + original_descriptor=event, + original_reserve_decision=reserve_decision, + owning_event_id=event.event_id, + publication_revision=type(state.aggregate_revision)(state.aggregate_revision.value + 1), ) revision, sequence = _effect_coordinates(state, capacity_changed=True) effects: tuple[AdmissionEffect, ...] = ( @@ -1153,6 +1150,7 @@ def _reserve( protected_pool_owner_id=event.batch.protected_pool_owner_id, capacity_changed=True, effects=effects, + idempotency_record=idempotency_record, ) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 0eb701b5f..8fb060a2c 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -1751,7 +1751,7 @@ def _validate_state_metadata( for record in idempotency_records: processed = processed_by_event_id.get(record.owning_event_id) if record.publication_revision.value > aggregate_revision.value or ( - processed is not None and record.publication_revision != processed.aggregate_revision + processed is None or record.publication_revision != processed.aggregate_revision ): _raise_invalid("invalid_idempotency_publication_coordinates") tombstone_keys = tuple( diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index ac867caf5..45b5c4c2b 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1583,6 +1583,20 @@ def test_journal_records_cross_check_duplicated_event_identities() -> None: closed_epochs=(), ) + with pytest.raises( + ContextAdmissionValidationError, + match="invalid_idempotency_publication_coordinates", + ): + UninitializedContextAdmissionState( + protocol_version=CONTEXT_ADMISSION_PROTOCOL_VERSION, + aggregate_revision=AggregateRevision(1), + admission_sequence=AdmissionSequence(1), + processed_events=(), + idempotency_records=(idempotency,), + expired_idempotency_tombstones=(), + closed_epochs=(), + ) + with pytest.raises( ContextAdmissionValidationError, match="invalid_idempotency_publication_coordinates", From abf0a2f326f30803b86464a50b7a141c3187e21f Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:22:19 -0700 Subject: [PATCH 50/62] refactor(review): keep context contract within size guard --- .../core/types/_type_context_admission.py | 82 ++----------------- src/autoskillit/core/types/_type_helpers.py | 81 +++++++++++++++++- 2 files changed, 85 insertions(+), 78 deletions(-) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 8fb060a2c..f33ef4997 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -38,6 +38,8 @@ _raise_invalid, _validate_bounded_text, _validate_canonical_tuple, + _validate_context_admission_state_metadata, + _validate_expired_idempotency_tombstone, _validate_freshness_policy, _validate_git_revision, _validate_iso_date, @@ -879,33 +881,7 @@ class ExpiredIdempotencyTombstone(_ContractValue): original_terminal_decision: AdmissionDecision def __post_init__(self) -> None: - input_reservations = self.original_descriptor.input_reservations - batch = self.original_descriptor.batch - if ( - self.namespace != self.original_descriptor.idempotency_namespace - or self.reservation_key.idempotency_namespace != self.namespace - or len(input_reservations) != 1 - or self.reservation_key != input_reservations[0].key - or self.reservation_key.batch_id != batch.batch_id - or self.original_terminal_decision.window_epoch_id - != self.reservation_key.window_epoch_id - or self.original_terminal_decision.snapshot_sequence - != self.original_descriptor.snapshot_sequence - ): - _raise_invalid("idempotency_tombstone_identity_mismatch") - witness = self.expiry_witness - if ( - witness.kind is not WitnessKind.IDEMPOTENCY_EXPIRY - or witness.window_epoch_id != self.reservation_key.window_epoch_id - or witness.window_epoch_number != self.reservation_key.window_epoch_number - or witness.snapshot_sequence != self.original_descriptor.snapshot_sequence - or witness.request_id != batch.request_id - or witness.batch_id != batch.batch_id - or witness.representation_revision != batch.manifest.representation_revision - or witness.representation_binding_id != batch.manifest.representation_binding_id - or witness.occurrence_ids != batch.occurrence_ids - ): - _raise_invalid("idempotency_tombstone_witness_mismatch") + _validate_expired_idempotency_tombstone(self) @dataclass(frozen=True, slots=True) @@ -1059,8 +1035,6 @@ def __post_init__(self) -> None: _validate_git_revision(self.tested_revision) _validate_iso_date(self.checked_at) _validate_freshness_policy(self.freshness_policy) - if self.freshness_policy != "verify_on_version_or_configuration_change": - _raise_invalid("unsupported_coverage_freshness_policy") _validate_bounded_text( self.source_locator, "invalid_source_locator", @@ -1721,52 +1695,6 @@ def __post_init__(self) -> None: _raise_invalid("idempotency_record_identity_mismatch") -def _validate_state_metadata( - aggregate_revision: AggregateRevision, - admission_sequence: AdmissionSequence, - processed_events: tuple[ProcessedEventRecord, ...], - idempotency_records: tuple[IdempotencyRecord, ...], - expired_tombstones: tuple[ExpiredIdempotencyTombstone, ...], - closed_epochs: tuple[ClosedEpochAudit, ...], -) -> None: - if len({record.event_id for record in processed_events}) != len(processed_events): - _raise_invalid("duplicate_processed_event") - processed_revisions = tuple(record.aggregate_revision.value for record in processed_events) - processed_sequences = tuple(record.admission_sequence.value for record in processed_events) - if ( - len(set(processed_revisions)) != len(processed_revisions) - or any(revision > aggregate_revision.value for revision in processed_revisions) - or any(sequence > admission_sequence.value for sequence in processed_sequences) - or any( - later < earlier for earlier, later in zip(processed_sequences, processed_sequences[1:]) - ) - ): - _raise_invalid("invalid_processed_event_coordinates") - idempotency_keys = tuple( - (record.namespace, record.reservation_key) for record in idempotency_records - ) - if len(set(idempotency_keys)) != len(idempotency_keys): - _raise_invalid("duplicate_idempotency_owner") - processed_by_event_id = {record.event_id: record for record in processed_events} - for record in idempotency_records: - processed = processed_by_event_id.get(record.owning_event_id) - if record.publication_revision.value > aggregate_revision.value or ( - processed is None or record.publication_revision != processed.aggregate_revision - ): - _raise_invalid("invalid_idempotency_publication_coordinates") - tombstone_keys = tuple( - (record.namespace, record.reservation_key) for record in expired_tombstones - ) - if len(set(tombstone_keys)) != len(tombstone_keys): - _raise_invalid("duplicate_idempotency_tombstone") - epoch_keys = tuple( - (audit.snapshot.window_epoch_id, audit.snapshot.window_epoch_number) - for audit in closed_epochs - ) - if len(set(epoch_keys)) != len(epoch_keys): - _raise_invalid("duplicate_closed_epoch") - - @dataclass(frozen=True, slots=True) class UninitializedContextAdmissionState(_ContractValue): protocol_version: int @@ -1805,7 +1733,7 @@ def __post_init__(self) -> None: "noncanonical_closed_epochs", key=lambda audit: audit.snapshot.window_epoch_number, ) - _validate_state_metadata( + _validate_context_admission_state_metadata( self.aggregate_revision, self.admission_sequence, self.processed_events, @@ -1890,7 +1818,7 @@ def __post_init__(self) -> None: "noncanonical_closed_epochs", key=lambda audit: audit.snapshot.window_epoch_number, ) - _validate_state_metadata( + _validate_context_admission_state_metadata( self.aggregate_revision, self.admission_sequence, self.processed_events, diff --git a/src/autoskillit/core/types/_type_helpers.py b/src/autoskillit/core/types/_type_helpers.py index aa16ec090..e46d90166 100644 --- a/src/autoskillit/core/types/_type_helpers.py +++ b/src/autoskillit/core/types/_type_helpers.py @@ -21,7 +21,7 @@ from ._type_constants import SKILL_COMMAND_PREFIX from ._type_constants_env import HEADLESS_ENV_VAR, SESSION_TYPE_ENV_VAR from ._type_constants_registries import FLEET_ERROR_CODES -from ._type_enums import SessionType, SkillSource +from ._type_enums import SessionType, SkillSource, WitnessKind from ._type_protocols_workspace import SkillResolver from ._type_skill_contract import SkillSourceRef @@ -161,6 +161,85 @@ def _validate_git_revision(value: str) -> None: def _validate_freshness_policy(value: str) -> None: if not isinstance(value, str) or value not in _FRESHNESS_POLICIES: _raise_invalid("invalid_freshness_policy") + if value != "verify_on_version_or_configuration_change": + _raise_invalid("unsupported_coverage_freshness_policy") + + +def _validate_expired_idempotency_tombstone(tombstone: Any) -> None: + descriptor = tombstone.original_descriptor + input_reservations = descriptor.input_reservations + batch = descriptor.batch + if ( + tombstone.namespace != descriptor.idempotency_namespace + or tombstone.reservation_key.idempotency_namespace != tombstone.namespace + or len(input_reservations) != 1 + or tombstone.reservation_key != input_reservations[0].key + or tombstone.reservation_key.batch_id != batch.batch_id + or tombstone.original_terminal_decision.window_epoch_id + != tombstone.reservation_key.window_epoch_id + or tombstone.original_terminal_decision.snapshot_sequence != descriptor.snapshot_sequence + ): + _raise_invalid("idempotency_tombstone_identity_mismatch") + witness = tombstone.expiry_witness + if ( + witness.kind is not WitnessKind.IDEMPOTENCY_EXPIRY + or witness.window_epoch_id != tombstone.reservation_key.window_epoch_id + or witness.window_epoch_number != tombstone.reservation_key.window_epoch_number + or witness.snapshot_sequence != descriptor.snapshot_sequence + or witness.request_id != batch.request_id + or witness.batch_id != batch.batch_id + or witness.representation_revision != batch.manifest.representation_revision + or witness.representation_binding_id != batch.manifest.representation_binding_id + or witness.occurrence_ids != batch.occurrence_ids + ): + _raise_invalid("idempotency_tombstone_witness_mismatch") + + +def _validate_context_admission_state_metadata( + aggregate_revision: Any, + admission_sequence: Any, + processed_events: tuple[Any, ...], + idempotency_records: tuple[Any, ...], + expired_tombstones: tuple[Any, ...], + closed_epochs: tuple[Any, ...], +) -> None: + if len({record.event_id for record in processed_events}) != len(processed_events): + _raise_invalid("duplicate_processed_event") + processed_revisions = tuple(record.aggregate_revision.value for record in processed_events) + processed_sequences = tuple(record.admission_sequence.value for record in processed_events) + if ( + len(set(processed_revisions)) != len(processed_revisions) + or any(revision > aggregate_revision.value for revision in processed_revisions) + or any(sequence > admission_sequence.value for sequence in processed_sequences) + or any( + later < earlier for earlier, later in zip(processed_sequences, processed_sequences[1:]) + ) + ): + _raise_invalid("invalid_processed_event_coordinates") + idempotency_keys = tuple( + (record.namespace, record.reservation_key) for record in idempotency_records + ) + if len(set(idempotency_keys)) != len(idempotency_keys): + _raise_invalid("duplicate_idempotency_owner") + processed_by_event_id = {record.event_id: record for record in processed_events} + if any( + record.publication_revision.value > aggregate_revision.value + or (processed := processed_by_event_id.get(record.owning_event_id)) is None + or record.publication_revision != processed.aggregate_revision + for record in idempotency_records + ): + _raise_invalid("invalid_idempotency_publication_coordinates") + tombstone_keys = tuple( + (record.namespace, record.reservation_key) for record in expired_tombstones + ) + if len(set(tombstone_keys)) != len(tombstone_keys): + _raise_invalid("duplicate_idempotency_tombstone") + epoch_keys = tuple( + (audit.snapshot.window_epoch_id, audit.snapshot.window_epoch_number) + for audit in closed_epochs + ) + if len(set(epoch_keys)) != len(epoch_keys): + _raise_invalid("duplicate_closed_epoch") def _matches_declared_type(value: object, declared_type: object) -> bool: From f8b42ee0c6a6c280fcaa1190e1f0a0061d6db633 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 15:26:19 -0700 Subject: [PATCH 51/62] fix(review): allow shared rejection coordinates --- src/autoskillit/core/types/_type_helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/autoskillit/core/types/_type_helpers.py b/src/autoskillit/core/types/_type_helpers.py index e46d90166..784b6c87d 100644 --- a/src/autoskillit/core/types/_type_helpers.py +++ b/src/autoskillit/core/types/_type_helpers.py @@ -208,8 +208,7 @@ def _validate_context_admission_state_metadata( processed_revisions = tuple(record.aggregate_revision.value for record in processed_events) processed_sequences = tuple(record.admission_sequence.value for record in processed_events) if ( - len(set(processed_revisions)) != len(processed_revisions) - or any(revision > aggregate_revision.value for revision in processed_revisions) + any(revision > aggregate_revision.value for revision in processed_revisions) or any(sequence > admission_sequence.value for sequence in processed_sequences) or any( later < earlier for earlier, later in zip(processed_sequences, processed_sequences[1:]) From 36efde338cfb7429a3999afe91738cfe92f48981 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 16:25:01 -0700 Subject: [PATCH 52/62] test(review): drive retained shared request lifecycle --- tests/core/test_context_admission_state_machine.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 50492569a..62a099086 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -1342,10 +1342,10 @@ def test_multi_member_batch_progresses_through_full_lifecycle() -> None: def test_rollover_scopes_shared_request_generation_retention_to_batch() -> None: machine = ContextAdmissionStateMachine() - machine.propose(0, ReserveClass.ORDINARY) - machine.propose(1, ReserveClass.ORDINARY) - machine.reserve(0, input_count=5, generation_count=4) - machine.reserve(1, input_count=5, generation_count=3) + machine.propose(0, ReserveClass.SYNTHESIS) + machine.propose(1, ReserveClass.SYNTHESIS) + machine.reserve(0, input_count=2, generation_count=4) + machine.reserve(1, input_count=2, generation_count=3) retained_batch = machine.batches[0] invalidated_batch = machine.batches[1] invalidated_batch = replace( From 3f97e86c0c1746d56f5744e3ae9737e52bff16a7 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 18:58:20 -0700 Subject: [PATCH 53/62] fix(review): suppress serialized constructor causes --- .../core/types/_type_context_admission.py | 4 ++-- .../core/types/test_context_admission_contract.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index f33ef4997..d5e712624 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -171,8 +171,8 @@ def _decode(value: object) -> object: kwargs = {key: _decode(item) for key, item in value.items() if key != "__type__"} try: return contract_type(**kwargs) - except TypeError as exc: - raise ContextAdmissionValidationError("invalid_serialized_contract") from exc + except TypeError: + raise ContextAdmissionValidationError("invalid_serialized_contract") from None class _ContractMeta(type): diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index 45b5c4c2b..d39d805f5 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -899,6 +899,20 @@ def test_invalid_serialized_enum_suppresses_attacker_controlled_cause() -> None: assert canary not in repr(exc_info.value) +def test_invalid_serialized_contract_suppresses_attacker_controlled_cause() -> None: + canary = "private-attacker-field" + serialized = ContextSessionId("session-1").to_dict() + serialized[canary] = "private-attacker-value" + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ContextSessionId.from_dict(serialized) + + assert exc_info.value.__cause__ is None + assert exc_info.value.__suppress_context__ + assert canary not in str(exc_info.value) + assert canary not in repr(exc_info.value) + + @pytest.mark.parametrize( "field_name", ["configured_model", "effective_model", "profile_name"], From 893988f387122262d1ac28a5755397854bb40e22 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 18:58:54 -0700 Subject: [PATCH 54/62] fix(review): bound closed audit occurrences --- .../core/types/_type_context_admission.py | 3 ++ .../types/test_context_admission_contract.py | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index d5e712624..e03bdf134 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -50,6 +50,7 @@ from ._type_results import ModelIdentity _TYPE_REGISTRY: dict[str, type[_ContractValue]] = {} +_MAX_CLOSED_EPOCH_OCCURRENCES = 10_000 _ENUM_REGISTRY: dict[str, type[StrEnum]] = { enum_type.__name__: enum_type for enum_type in ( @@ -903,6 +904,8 @@ def __post_init__(self) -> None: self.retained_generation_count, "invalid_retained_generation_charge", ) + if len(self.terminal_occurrence_records) > _MAX_CLOSED_EPOCH_OCCURRENCES: + _raise_invalid("closed_epoch_occurrence_limit_exceeded") _validate_canonical_tuple( self.terminal_occurrence_records, "noncanonical_terminal_occurrences", diff --git a/tests/core/types/test_context_admission_contract.py b/tests/core/types/test_context_admission_contract.py index d39d805f5..a9a8a50bb 100644 --- a/tests/core/types/test_context_admission_contract.py +++ b/tests/core/types/test_context_admission_contract.py @@ -1375,6 +1375,35 @@ def test_closed_epoch_audits_survive_state_serialization() -> None: assert restored.closed_epochs == (empty_audit,) +def test_closed_epoch_audit_rejects_excessive_occurrence_cardinality() -> None: + occurrence = _occurrence("occurrence-cardinality", span_id="span-cardinality") + record = AdmissionOccurrenceRecord( + occurrence=occurrence, + state=AdmissionState.PROPOSED, + batch_id=None, + reservation_id=None, + accepted_witness_ids=(), + indeterminate_reason_code=None, + quarantine_reason_code=None, + ) + + with pytest.raises(ContextAdmissionValidationError) as exc_info: + ClosedEpochAudit( + snapshot=_snapshot(), + terminal_occurrence_records=(record,) * 10_001, + terminal_batch_records=(), + terminal_reservations=(), + terminal_generation_reservations=(), + closure_witness_id=AdmissionWitnessId("closure-witness-cardinality"), + fence_proof=None, + processed_event_tombstones=(), + retained_unresolved_count=0, + retained_generation_count=0, + ) + + assert "closed_epoch_occurrence_limit_exceeded" in str(exc_info.value) + + @pytest.mark.parametrize( "non_dispatch_surface", [ From 3f0c2b8abe4c812d81a2eb159a763f3e75b01ace Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 18:59:42 -0700 Subject: [PATCH 55/62] fix(review): quarantine closed authority mismatches --- src/autoskillit/core/context_admission.py | 13 ++-- tests/core/test_context_admission_reducer.py | 64 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index da6816620..f979aa874 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -1366,8 +1366,6 @@ def _accept_closed_input( or binding.representation_binding_id != record.batch.manifest.representation_binding_id or binding.request_id != record.batch.request_id or binding.batch_id != record.batch.batch_id - or event.authority_source != event.witness.authority_source_id - or event.authority_source != binding.authority_source_id ): return _reject(state, event, "invalid-closed-epoch-acceptance") reservation = _closed_reservation_for(audit, record) @@ -1388,13 +1386,20 @@ def _accept_closed_input( or set(manifest_pairs) != set(expected_owned_pairs) or len(manifest_pairs) != len(expected_owned_pairs) ) + authority_mismatch = ( + event.authority_source != event.witness.authority_source_id + or event.authority_source != binding.authority_source_id + ) quarantined = ( - exact_charge > reservation.reserved_count + authority_mismatch + or exact_charge > reservation.reserved_count or exact_charge > audit.snapshot.hard_limit or manifest_invalid ) reason_code = ( - "incomplete-canonical-span-ownership" + "authority-source-mismatch" + if authority_mismatch + else "incomplete-canonical-span-ownership" if manifest_invalid else "provider-charge-exceeds-reservation" if quarantined diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index ee1131d88..56cd278f2 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -2366,6 +2366,70 @@ def test_rollover_moves_old_work_to_resolvable_closed_epoch_audit() -> None: assert reconciled_audit.terminal_generation_reservations[0].state is GenerationState.RECONCILED +def test_closed_epoch_authority_mismatch_quarantines_exact_charge() -> None: + state, batch, _, _ = _reserved_batch( + name="closed-authority-mismatch", + input_count=18, + generation_count=0, + ) + state = _prepare_dispatch(state, batch) + receiver = AuthoritySourceId("receiver-closed-authority-mismatch") + rolled = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-closed-authority-mismatch", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=receiver, + fence_witness_id=AdmissionWitnessId("fence-closed-authority-mismatch"), + highest_admitted_dispatch_sequence=1, + ), + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ), + ) + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + + quarantined = reduce_context_admission( + rolled.next_state, + AcceptInputEvent( + **_event_fields( + rolled.next_state, + "accept-closed-authority-mismatch", + "accept-input", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=20, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("different-authority"), + representation_binding_witness=_binding(batch), + ), + ) + + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert quarantined.decision.reason_code == "authority-source-mismatch" + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + audit = quarantined.next_state.closed_epochs[-1] + assert audit.retained_unresolved_count == 0 + assert audit.terminal_batch_records[0].state is AdmissionState.QUARANTINED + assert audit.terminal_batch_records[0].committed_input_count == 20 + assert {type(effect).__name__ for effect in quarantined.effects} == { + "ChargeCommittedEffect", + "OccurrenceStateChangedEffect", + "QuarantineRecordedEffect", + } + + def test_protected_release_uses_policy_from_rollover_created_epoch() -> None: owner = ProtectedPoolOwnerId("rollover-policy-owner") pool = ProtectedPoolSpec( From 2d9223d25eb1f6a3c7a6e40ee39a6c8042111a59 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 19:00:59 -0700 Subject: [PATCH 56/62] fix(review): restore resolved closed capacity --- src/autoskillit/core/context_admission.py | 58 +++++++++++ tests/core/test_context_admission_reducer.py | 102 +++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index f979aa874..5113a91ea 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -793,6 +793,43 @@ def _replace_closed_audit( ) +def _reconcile_deducted_closed_charge( + state: ActiveContextAdmissionState, + audit: ClosedEpochAudit, + *, + deducted_charge: int, + terminal_charge: int, +) -> ActiveContextAdmissionState: + if audit.fence_proof is not None or deducted_charge == terminal_charge: + return state + snapshot = state.snapshot + charge_delta = deducted_charge - terminal_charge + if charge_delta > 0: + capacity_slack = max( + snapshot.hard_limit - snapshot.active_count - snapshot.remaining_count, + 0, + ) + active_credit = min( + max(charge_delta - capacity_slack, 0), + snapshot.active_count, + ) + restored_count = min(charge_delta, capacity_slack + active_credit) + active_count = snapshot.active_count - active_credit + remaining_count = snapshot.remaining_count + restored_count + else: + additional_charge = min(-charge_delta, snapshot.remaining_count) + active_count = snapshot.active_count + additional_charge + remaining_count = snapshot.remaining_count - additional_charge + return replace( + state, + snapshot=replace( + snapshot, + active_count=active_count, + remaining_count=remaining_count, + ), + ) + + def _open_epoch( state: ContextAdmissionState, event: OpenEpochEvent, @@ -1444,6 +1481,12 @@ def _accept_closed_input( ), ) next_state = _replace_closed_audit(state, index, updated_audit) + next_state = _reconcile_deducted_closed_charge( + next_state, + audit, + deducted_charge=_closed_retained_input_count(audit, (record,)), + terminal_charge=exact_charge, + ) revision, sequence = _effect_coordinates(state, capacity_changed=True) effects: tuple[AdmissionEffect, ...] = ( ChargeCommittedEffect( @@ -1648,6 +1691,7 @@ def _release_closed_batch( location: tuple[int, ClosedEpochAudit, AdmissionBatchRecord], ) -> AdmissionTransition: index, audit, record = location + released_input_count = _closed_retained_input_count(audit, (record,)) is_release = isinstance( event, ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, @@ -1748,6 +1792,7 @@ def _release_closed_batch( ) generation_effects: tuple[AdmissionEffect, ...] = () generation_records: list[GenerationReservationRecord] = [] + invalidated_generation_count = 0 revision, sequence = _effect_coordinates(state, capacity_changed=True) for generation in audit.terminal_generation_reservations: if generation.batch_id == record.batch.batch_id and generation.state in { @@ -1770,6 +1815,7 @@ def _release_closed_batch( witness_ids=(event.witness.witness_id,), ), ) + invalidated_generation_count += generation.maximum_allowance else: generation_records.append(generation) if generation_effects: @@ -1789,6 +1835,12 @@ def _release_closed_batch( ) next_state = _replace_closed_audit(state, index, updated_audit) effects += generation_effects + next_state = _reconcile_deducted_closed_charge( + next_state, + audit, + deducted_charge=released_input_count + invalidated_generation_count, + terminal_charge=0, + ) return _publish( state, next_state, @@ -2198,6 +2250,12 @@ def _reconcile_closed_generation( retained_generation_count=retained_generation_count, ) next_state = _replace_closed_audit(state, index, updated_audit) + next_state = _reconcile_deducted_closed_charge( + next_state, + audit, + deducted_charge=generation.maximum_allowance, + terminal_charge=event.exact_output_usage, + ) revision, sequence = _effect_coordinates(state, capacity_changed=True) effects: tuple[AdmissionEffect, ...] = ( GenerationReconciledEffect( diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 56cd278f2..96b9f48b8 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -2430,6 +2430,108 @@ def test_closed_epoch_authority_mismatch_quarantines_exact_charge() -> None: } +def test_no_fence_closed_reconciliation_restores_deducted_capacity() -> None: + state, batch, _, generation = _reserved_batch( + name="closed-capacity-reconciliation", + input_count=18, + generation_count=7, + ) + state = _prepare_dispatch(state, batch) + rolled = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-closed-capacity-reconciliation", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + new_snapshot=_snapshot(epoch=2, active_count=65, remaining_count=35), + protected_pools=(), + ), + ) + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + + accepted = reduce_context_admission( + rolled.next_state, + AcceptInputEvent( + **_event_fields( + rolled.next_state, + "accept-closed-capacity-reconciliation", + "accept-input", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=17, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ), + ) + assert isinstance(accepted.next_state, ActiveContextAdmissionState) + assert accepted.next_state.snapshot.active_count == 64 + assert accepted.next_state.snapshot.remaining_count == 36 + + reconciled = reduce_context_admission( + accepted.next_state, + ReconcileGenerationEvent( + **_event_fields( + accepted.next_state, + "reconcile-closed-capacity-generation", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=5, + ), + ) + + assert reconciled.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(reconciled.next_state, ActiveContextAdmissionState) + assert reconciled.next_state.snapshot.active_count == 62 + assert reconciled.next_state.snapshot.remaining_count == 38 + + +def test_no_fence_closed_release_restores_all_deducted_capacity() -> None: + state, batch, _, _ = _reserved_batch( + name="closed-capacity-release", + input_count=18, + generation_count=7, + ) + state = _prepare_dispatch(state, batch) + rolled = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, "rollover-closed-capacity-release", "rollover-epoch"), + witness=_witness(batch, WitnessKind.EPOCH_ROLLOVER), + fence_proof=None, + new_snapshot=_snapshot(epoch=2, active_count=65, remaining_count=35), + protected_pools=(), + ), + ) + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + + released = reduce_context_admission( + rolled.next_state, + ReleaseNonAdmissionEvent( + **_event_fields( + rolled.next_state, + "release-closed-capacity", + "release", + ), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.NON_ADMISSION), + ), + ) + + assert released.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(released.next_state, ActiveContextAdmissionState) + assert released.next_state.snapshot.active_count == 40 + assert released.next_state.snapshot.remaining_count == 60 + audit = released.next_state.closed_epochs[-1] + assert audit.retained_unresolved_count == 0 + assert audit.retained_generation_count == 0 + + def test_protected_release_uses_policy_from_rollover_created_epoch() -> None: owner = ProtectedPoolOwnerId("rollover-policy-owner") pool = ProtectedPoolSpec( From 773a85cd57b049a304da0c3f48fb65bf4a67ec9a Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 19:02:30 -0700 Subject: [PATCH 57/62] test(review): cover closed quarantine branches --- tests/core/test_context_admission_reducer.py | 152 +++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/tests/core/test_context_admission_reducer.py b/tests/core/test_context_admission_reducer.py index 96b9f48b8..63b81e8d0 100644 --- a/tests/core/test_context_admission_reducer.py +++ b/tests/core/test_context_admission_reducer.py @@ -2366,6 +2366,158 @@ def test_rollover_moves_old_work_to_resolvable_closed_epoch_audit() -> None: assert reconciled_audit.terminal_generation_reservations[0].state is GenerationState.RECONCILED +def _rollover_closed_work( + state: ActiveContextAdmissionState, + batch: AdmissionBatch, + *, + name: str, +) -> ActiveContextAdmissionState: + receiver = AuthoritySourceId(f"receiver-{name}") + rolled = reduce_context_admission( + state, + RolloverEpochEvent( + **_event_fields(state, f"rollover-{name}", "rollover-epoch"), + witness=replace( + _witness(batch, WitnessKind.EPOCH_ROLLOVER), + authority_source_id=receiver, + ), + fence_proof=EpochFenceProof( + old_window_epoch_id=state.snapshot.window_epoch_id, + old_window_epoch_number=state.snapshot.window_epoch_number, + new_window_epoch_id=WindowEpochId("epoch-2"), + new_window_epoch_number=2, + receiver_authority_source_id=receiver, + fence_witness_id=AdmissionWitnessId(f"fence-{name}"), + highest_admitted_dispatch_sequence=1, + ), + new_snapshot=_snapshot(epoch=2), + protected_pools=(), + ), + ) + assert rolled.decision.kind is AdmissionDecisionKind.WOULD_ADMIT + assert isinstance(rolled.next_state, ActiveContextAdmissionState) + return rolled.next_state + + +def test_closed_epoch_input_overage_quarantines_and_clears_retained_charge() -> None: + state, batch, _, _ = _reserved_batch( + name="closed-input-overage", + input_count=18, + generation_count=0, + ) + state = _prepare_dispatch(state, batch) + state = _rollover_closed_work(state, batch, name="closed-input-overage") + + quarantined = reduce_context_admission( + state, + AcceptInputEvent( + **_event_fields(state, "accept-closed-input-overage", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=19, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ), + ) + + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert quarantined.decision.reason_code == "provider-charge-exceeds-reservation" + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + audit = quarantined.next_state.closed_epochs[-1] + assert audit.retained_unresolved_count == 0 + assert audit.terminal_batch_records[0].state is AdmissionState.QUARANTINED + assert "QuarantineRecordedEffect" in {type(effect).__name__ for effect in quarantined.effects} + + +def test_closed_epoch_incomplete_span_ownership_quarantines() -> None: + state, batch, _, _ = _reserved_batch( + name="closed-incomplete-spans", + input_count=18, + generation_count=0, + ) + state = _prepare_dispatch(state, batch) + state = _rollover_closed_work(state, batch, name="closed-incomplete-spans") + audit = state.closed_epochs[-1] + occurrence_record = audit.terminal_occurrence_records[0] + incomplete_audit = replace( + audit, + terminal_occurrence_records=( + replace( + occurrence_record, + occurrence=replace( + occurrence_record.occurrence, + owned_span_ids=( + *occurrence_record.occurrence.owned_span_ids, + CanonicalSpanId("span-missing-from-closed-manifest"), + ), + ), + ), + ), + ) + state = replace(state, closed_epochs=(incomplete_audit,)) + + quarantined = reduce_context_admission( + state, + AcceptInputEvent( + **_event_fields(state, "accept-closed-incomplete-spans", "accept-input"), + batch_id=batch.batch_id, + witness=_witness(batch, WitnessKind.PROVIDER_ACCEPTED), + final_manifest_revision=batch.manifest.representation_revision, + final_manifest=batch.manifest, + exact_input_charge=17, + measurement_kind=MeasurementKind.PROVIDER_EXACT, + authority_source=AuthoritySourceId("authority-test"), + representation_binding_witness=_binding(batch), + ), + ) + + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert quarantined.decision.reason_code == "incomplete-canonical-span-ownership" + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + closed_audit = quarantined.next_state.closed_epochs[-1] + assert closed_audit.retained_unresolved_count == 0 + assert closed_audit.terminal_batch_records[0].state is AdmissionState.QUARANTINED + assert "QuarantineRecordedEffect" in {type(effect).__name__ for effect in quarantined.effects} + + +def test_closed_epoch_generation_overage_quarantines_and_clears_retained_charge() -> None: + state, batch, _, generation = _reserved_batch( + name="closed-generation-overage", + input_count=18, + generation_count=7, + ) + state = _prepare_dispatch(state, batch) + state = _rollover_closed_work(state, batch, name="closed-generation-overage") + + quarantined = reduce_context_admission( + state, + ReconcileGenerationEvent( + **_event_fields( + state, + "reconcile-closed-generation-overage", + "reconcile-generation", + ), + generation_reservation_id=generation.generation_reservation_id, + output_usage_witness=_witness(batch, WitnessKind.OUTPUT_USAGE), + exact_output_usage=8, + ), + ) + + assert quarantined.decision.kind is AdmissionDecisionKind.QUARANTINED + assert quarantined.decision.reason_code == "generation-usage-exceeds-allowance" + assert isinstance(quarantined.next_state, ActiveContextAdmissionState) + audit = quarantined.next_state.closed_epochs[-1] + assert audit.retained_generation_count == 0 + assert audit.terminal_generation_reservations[0].state is GenerationState.QUARANTINED + assert {type(effect).__name__ for effect in quarantined.effects} == { + "GenerationReconciledEffect", + "QuarantineRecordedEffect", + } + + def test_closed_epoch_authority_mismatch_quarantines_exact_charge() -> None: state, batch, _, _ = _reserved_batch( name="closed-authority-mismatch", From 6c367413fa4c621a3eb3061ca34474a9da239e5d Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 19:02:51 -0700 Subject: [PATCH 58/62] test(review): restrict generation start success state --- tests/core/test_context_admission_state_machine.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/core/test_context_admission_state_machine.py b/tests/core/test_context_admission_state_machine.py index 62a099086..6c247601c 100644 --- a/tests/core/test_context_admission_state_machine.py +++ b/tests/core/test_context_admission_state_machine.py @@ -1042,12 +1042,7 @@ def start_and_reconcile_generation(self, exact_usage: int) -> None: slot, occurrence = slot_and_occurrence batch = self.batches[slot] record = self._find_batch(batch.batch_id) - if record is None or record.state not in { - AdmissionState.HISTORY_STAGED, - AdmissionState.REQUEST_DISPATCHED, - AdmissionState.COMMITTED, - AdmissionState.QUARANTINED, - }: + if record is None or record.state is not AdmissionState.REQUEST_DISPATCHED: continue start = StartGenerationEvent( **self._fields("start-generation"), From 1095e299521590982b569356e675544c3f92df2d Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 19:08:34 -0700 Subject: [PATCH 59/62] fix(review): suppress contract construction causes --- src/autoskillit/core/types/_type_context_admission.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index e03bdf134..45ecdc473 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -182,8 +182,8 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: instance = super().__call__(*args, **kwargs) except ContextAdmissionValidationError: raise - except (AttributeError, TypeError, ValueError) as exc: - raise ContextAdmissionValidationError("invalid_contract_field_type") from exc + except (AttributeError, TypeError, ValueError): + raise ContextAdmissionValidationError("invalid_contract_field_type") from None _validate_declared_field_types(instance) _validate_deep_immutability(instance) return instance From 583e5acbcf26e59ce389fab298685fab3f8c71f5 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 19:09:47 -0700 Subject: [PATCH 60/62] refactor(review): isolate closed epoch accounting --- src/autoskillit/core/AGENTS.md | 1 + .../core/_closed_epoch_accounting.py | 100 ++++++++++++++++++ src/autoskillit/core/context_admission.py | 93 ++-------------- 3 files changed, 107 insertions(+), 87 deletions(-) create mode 100644 src/autoskillit/core/_closed_epoch_accounting.py diff --git a/src/autoskillit/core/AGENTS.md b/src/autoskillit/core/AGENTS.md index dff29aa69..e573918ff 100644 --- a/src/autoskillit/core/AGENTS.md +++ b/src/autoskillit/core/AGENTS.md @@ -32,6 +32,7 @@ Sub-packages: types/ (see types/AGENTS.md) and runtime/ (see runtime/AGENTS.md). | `closure_hashing.py` | Canonical hash utilities for closure-mode verdict verification (stdlib-only, IL-0) | | `path_containment.py` | Path containment guards — symlink/hardlink check, TOCTOU guard (stdlib-only, IL-0) | | `closure_verifier.py` | Independent verifier for closure-mode reports (stdlib-only, IL-0) | +| `_closed_epoch_accounting.py` | Closed-epoch retained-charge and snapshot-capacity accounting helpers | | `context_admission.py` | Pure protocol-v1 cumulative context-admission reducer, replay, and coverage resolution | ## Architecture Notes diff --git a/src/autoskillit/core/_closed_epoch_accounting.py b/src/autoskillit/core/_closed_epoch_accounting.py new file mode 100644 index 000000000..83a168f4d --- /dev/null +++ b/src/autoskillit/core/_closed_epoch_accounting.py @@ -0,0 +1,100 @@ +"""Closed-epoch accounting helpers for cumulative context admission.""" + +from __future__ import annotations + +from dataclasses import replace + +from .types._type_context_admission import ( + ActiveContextAdmissionState, + AdmissionBatchRecord, + AdmissionReservation, + ClosedEpochAudit, +) +from .types._type_enums import AdmissionState + + +def _closed_reservation_for( + audit: ClosedEpochAudit, + record: AdmissionBatchRecord, +) -> AdmissionReservation | None: + if record.reservation_id is None: + return None + return next( + ( + reservation + for reservation in audit.terminal_reservations + if reservation.reservation_id == record.reservation_id + ), + None, + ) + + +def _closed_retained_input_count( + audit: ClosedEpochAudit, + records: tuple[AdmissionBatchRecord, ...], +) -> int: + return sum( + record.unresolved_input_count + or ( + reservation.reserved_count + if (reservation := _closed_reservation_for(audit, record)) is not None + else 0 + ) + for record in records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + + +def _replace_closed_audit( + state: ActiveContextAdmissionState, + index: int, + audit: ClosedEpochAudit, +) -> ActiveContextAdmissionState: + return replace( + state, + closed_epochs=tuple( + audit if item_index == index else item + for item_index, item in enumerate(state.closed_epochs) + ), + ) + + +def _reconcile_deducted_closed_charge( + state: ActiveContextAdmissionState, + audit: ClosedEpochAudit, + *, + deducted_charge: int, + terminal_charge: int, +) -> ActiveContextAdmissionState: + if audit.fence_proof is not None or deducted_charge == terminal_charge: + return state + snapshot = state.snapshot + charge_delta = deducted_charge - terminal_charge + if charge_delta > 0: + capacity_slack = max( + snapshot.hard_limit - snapshot.active_count - snapshot.remaining_count, + 0, + ) + active_credit = min( + max(charge_delta - capacity_slack, 0), + snapshot.active_count, + ) + restored_count = min(charge_delta, capacity_slack + active_credit) + active_count = snapshot.active_count - active_credit + remaining_count = snapshot.remaining_count + restored_count + else: + additional_charge = min(-charge_delta, snapshot.remaining_count) + active_count = snapshot.active_count + additional_charge + remaining_count = snapshot.remaining_count - additional_charge + return replace( + state, + snapshot=replace( + snapshot, + active_count=active_count, + remaining_count=remaining_count, + ), + ) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index 5113a91ea..a3e1e30c0 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -5,6 +5,12 @@ from dataclasses import replace from typing import assert_never +from ._closed_epoch_accounting import ( + _closed_reservation_for, + _closed_retained_input_count, + _reconcile_deducted_closed_charge, + _replace_closed_audit, +) from .types._type_context_admission import ( CONTEXT_ADMISSION_COVERAGE, AcceptInputEvent, @@ -743,93 +749,6 @@ def _closed_generation_location( return None -def _closed_reservation_for( - audit: ClosedEpochAudit, - record: AdmissionBatchRecord, -) -> AdmissionReservation | None: - if record.reservation_id is None: - return None - return next( - ( - reservation - for reservation in audit.terminal_reservations - if reservation.reservation_id == record.reservation_id - ), - None, - ) - - -def _closed_retained_input_count( - audit: ClosedEpochAudit, - records: tuple[AdmissionBatchRecord, ...], -) -> int: - return sum( - record.unresolved_input_count - or ( - reservation.reserved_count - if (reservation := _closed_reservation_for(audit, record)) is not None - else 0 - ) - for record in records - if record.state - in { - AdmissionState.REQUEST_DISPATCHED, - AdmissionState.INDETERMINATE, - } - ) - - -def _replace_closed_audit( - state: ActiveContextAdmissionState, - index: int, - audit: ClosedEpochAudit, -) -> ActiveContextAdmissionState: - return replace( - state, - closed_epochs=tuple( - audit if item_index == index else item - for item_index, item in enumerate(state.closed_epochs) - ), - ) - - -def _reconcile_deducted_closed_charge( - state: ActiveContextAdmissionState, - audit: ClosedEpochAudit, - *, - deducted_charge: int, - terminal_charge: int, -) -> ActiveContextAdmissionState: - if audit.fence_proof is not None or deducted_charge == terminal_charge: - return state - snapshot = state.snapshot - charge_delta = deducted_charge - terminal_charge - if charge_delta > 0: - capacity_slack = max( - snapshot.hard_limit - snapshot.active_count - snapshot.remaining_count, - 0, - ) - active_credit = min( - max(charge_delta - capacity_slack, 0), - snapshot.active_count, - ) - restored_count = min(charge_delta, capacity_slack + active_credit) - active_count = snapshot.active_count - active_credit - remaining_count = snapshot.remaining_count + restored_count - else: - additional_charge = min(-charge_delta, snapshot.remaining_count) - active_count = snapshot.active_count + additional_charge - remaining_count = snapshot.remaining_count - additional_charge - return replace( - state, - snapshot=replace( - snapshot, - active_count=active_count, - remaining_count=remaining_count, - ), - ) - - def _open_epoch( state: ContextAdmissionState, event: OpenEpochEvent, From 54b7dd83a40b21ab554f759a88788827b26a2b9c Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 19:17:34 -0700 Subject: [PATCH 61/62] refactor(review): keep closed accounting within core contracts --- src/autoskillit/core/AGENTS.md | 1 - .../core/_closed_epoch_accounting.py | 100 ------------------ src/autoskillit/core/context_admission.py | 62 +++++++---- .../core/types/_type_context_admission.py | 44 ++++---- src/autoskillit/core/types/_type_helpers.py | 17 +++ 5 files changed, 86 insertions(+), 138 deletions(-) delete mode 100644 src/autoskillit/core/_closed_epoch_accounting.py diff --git a/src/autoskillit/core/AGENTS.md b/src/autoskillit/core/AGENTS.md index e573918ff..dff29aa69 100644 --- a/src/autoskillit/core/AGENTS.md +++ b/src/autoskillit/core/AGENTS.md @@ -32,7 +32,6 @@ Sub-packages: types/ (see types/AGENTS.md) and runtime/ (see runtime/AGENTS.md). | `closure_hashing.py` | Canonical hash utilities for closure-mode verdict verification (stdlib-only, IL-0) | | `path_containment.py` | Path containment guards — symlink/hardlink check, TOCTOU guard (stdlib-only, IL-0) | | `closure_verifier.py` | Independent verifier for closure-mode reports (stdlib-only, IL-0) | -| `_closed_epoch_accounting.py` | Closed-epoch retained-charge and snapshot-capacity accounting helpers | | `context_admission.py` | Pure protocol-v1 cumulative context-admission reducer, replay, and coverage resolution | ## Architecture Notes diff --git a/src/autoskillit/core/_closed_epoch_accounting.py b/src/autoskillit/core/_closed_epoch_accounting.py deleted file mode 100644 index 83a168f4d..000000000 --- a/src/autoskillit/core/_closed_epoch_accounting.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Closed-epoch accounting helpers for cumulative context admission.""" - -from __future__ import annotations - -from dataclasses import replace - -from .types._type_context_admission import ( - ActiveContextAdmissionState, - AdmissionBatchRecord, - AdmissionReservation, - ClosedEpochAudit, -) -from .types._type_enums import AdmissionState - - -def _closed_reservation_for( - audit: ClosedEpochAudit, - record: AdmissionBatchRecord, -) -> AdmissionReservation | None: - if record.reservation_id is None: - return None - return next( - ( - reservation - for reservation in audit.terminal_reservations - if reservation.reservation_id == record.reservation_id - ), - None, - ) - - -def _closed_retained_input_count( - audit: ClosedEpochAudit, - records: tuple[AdmissionBatchRecord, ...], -) -> int: - return sum( - record.unresolved_input_count - or ( - reservation.reserved_count - if (reservation := _closed_reservation_for(audit, record)) is not None - else 0 - ) - for record in records - if record.state - in { - AdmissionState.REQUEST_DISPATCHED, - AdmissionState.INDETERMINATE, - } - ) - - -def _replace_closed_audit( - state: ActiveContextAdmissionState, - index: int, - audit: ClosedEpochAudit, -) -> ActiveContextAdmissionState: - return replace( - state, - closed_epochs=tuple( - audit if item_index == index else item - for item_index, item in enumerate(state.closed_epochs) - ), - ) - - -def _reconcile_deducted_closed_charge( - state: ActiveContextAdmissionState, - audit: ClosedEpochAudit, - *, - deducted_charge: int, - terminal_charge: int, -) -> ActiveContextAdmissionState: - if audit.fence_proof is not None or deducted_charge == terminal_charge: - return state - snapshot = state.snapshot - charge_delta = deducted_charge - terminal_charge - if charge_delta > 0: - capacity_slack = max( - snapshot.hard_limit - snapshot.active_count - snapshot.remaining_count, - 0, - ) - active_credit = min( - max(charge_delta - capacity_slack, 0), - snapshot.active_count, - ) - restored_count = min(charge_delta, capacity_slack + active_credit) - active_count = snapshot.active_count - active_credit - remaining_count = snapshot.remaining_count + restored_count - else: - additional_charge = min(-charge_delta, snapshot.remaining_count) - active_count = snapshot.active_count + additional_charge - remaining_count = snapshot.remaining_count - additional_charge - return replace( - state, - snapshot=replace( - snapshot, - active_count=active_count, - remaining_count=remaining_count, - ), - ) diff --git a/src/autoskillit/core/context_admission.py b/src/autoskillit/core/context_admission.py index a3e1e30c0..fadc91dc3 100644 --- a/src/autoskillit/core/context_admission.py +++ b/src/autoskillit/core/context_admission.py @@ -5,12 +5,6 @@ from dataclasses import replace from typing import assert_never -from ._closed_epoch_accounting import ( - _closed_reservation_for, - _closed_retained_input_count, - _reconcile_deducted_closed_charge, - _replace_closed_audit, -) from .types._type_context_admission import ( CONTEXT_ADMISSION_COVERAGE, AcceptInputEvent, @@ -89,6 +83,7 @@ ReserveClass, WitnessKind, ) +from .types._type_helpers import _reconciled_snapshot_counts __all__ = [ "ContextAdmissionValidationError", @@ -749,6 +744,43 @@ def _closed_generation_location( return None +def _replace_closed_audit( + state: ActiveContextAdmissionState, + index: int, + audit: ClosedEpochAudit, +) -> ActiveContextAdmissionState: + return replace( + state, + closed_epochs=tuple( + audit if item_index == index else item + for item_index, item in enumerate(state.closed_epochs) + ), + ) + + +def _reconcile_deducted_closed_charge( + state: ActiveContextAdmissionState, + audit: ClosedEpochAudit, + *, + deducted_charge: int, + terminal_charge: int, +) -> ActiveContextAdmissionState: + if audit.fence_proof is not None or deducted_charge == terminal_charge: + return state + snapshot = state.snapshot + active_count, remaining_count = _reconciled_snapshot_counts( + snapshot.active_count, + snapshot.remaining_count, + snapshot.hard_limit, + deducted_charge, + terminal_charge, + ) + return replace( + state, + snapshot=replace(snapshot, active_count=active_count, remaining_count=remaining_count), + ) + + def _open_epoch( state: ContextAdmissionState, event: OpenEpochEvent, @@ -1324,7 +1356,7 @@ def _accept_closed_input( or binding.batch_id != record.batch.batch_id ): return _reject(state, event, "invalid-closed-epoch-acceptance") - reservation = _closed_reservation_for(audit, record) + reservation = audit.reservation_for(record) if reservation is None: return _reject(state, event, "missing-closed-epoch-reservation") member_ids = set(record.batch.occurrence_ids) @@ -1394,16 +1426,13 @@ def _accept_closed_input( audit, terminal_occurrence_records=occurrence_records, terminal_batch_records=batch_records, - retained_unresolved_count=_closed_retained_input_count( - audit, - batch_records, - ), + retained_unresolved_count=audit.retained_input_count(batch_records), ) next_state = _replace_closed_audit(state, index, updated_audit) next_state = _reconcile_deducted_closed_charge( next_state, audit, - deducted_charge=_closed_retained_input_count(audit, (record,)), + deducted_charge=audit.retained_input_count((record,)), terminal_charge=exact_charge, ) revision, sequence = _effect_coordinates(state, capacity_changed=True) @@ -1610,7 +1639,7 @@ def _release_closed_batch( location: tuple[int, ClosedEpochAudit, AdmissionBatchRecord], ) -> AdmissionTransition: index, audit, record = location - released_input_count = _closed_retained_input_count(audit, (record,)) + released_input_count = audit.retained_input_count((record,)) is_release = isinstance( event, ReleaseNonAdmissionEvent | ResolveIndeterminateNonAdmissionEvent, @@ -1676,13 +1705,10 @@ def _release_closed_batch( audit, terminal_occurrence_records=occurrence_records, terminal_batch_records=batch_records, - retained_unresolved_count=_closed_retained_input_count( - audit, - batch_records, - ), + retained_unresolved_count=audit.retained_input_count(batch_records), ) next_state = _replace_closed_audit(state, index, updated_audit) - reservation = _closed_reservation_for(audit, record) + reservation = audit.reservation_for(record) effects: tuple[AdmissionEffect, ...] = _occurrence_effects( state, event, diff --git a/src/autoskillit/core/types/_type_context_admission.py b/src/autoskillit/core/types/_type_context_admission.py index 45ecdc473..588469c92 100644 --- a/src/autoskillit/core/types/_type_context_admission.py +++ b/src/autoskillit/core/types/_type_context_admission.py @@ -10,12 +10,7 @@ from collections.abc import Mapping from dataclasses import dataclass, field, fields from enum import StrEnum -from typing import ( - Any, - ClassVar, - TypeAlias, - get_type_hints, -) +from typing import Any, ClassVar, TypeAlias, get_type_hints from ._type_dispatch_identity import DispatchIdentity from ._type_enums import ( @@ -898,6 +893,29 @@ class ClosedEpochAudit(_ContractValue): retained_unresolved_count: int retained_generation_count: int + def reservation_for(self, record: AdmissionBatchRecord) -> AdmissionReservation | None: + if record.reservation_id is None: + return None + return next( + ( + reservation + for reservation in self.terminal_reservations + if reservation.reservation_id == record.reservation_id + ), + None, + ) + + def retained_input_count(self, records: tuple[AdmissionBatchRecord, ...]) -> int: + return sum( + record.charged_input_count(self.reservation_for(record)) + for record in records + if record.state + in { + AdmissionState.REQUEST_DISPATCHED, + AdmissionState.INDETERMINATE, + } + ) + def __post_init__(self) -> None: _validate_non_negative(self.retained_unresolved_count, "invalid_retained_charge") _validate_non_negative( @@ -983,19 +1001,7 @@ def __post_init__(self) -> None: or generation.occurrence_ids != generation_batch.batch.occurrence_ids ): _raise_invalid("inconsistent_closed_epoch_generation") - retained_input = 0 - for record in self.terminal_batch_records: - if record.state not in { - AdmissionState.REQUEST_DISPATCHED, - AdmissionState.INDETERMINATE, - }: - continue - charge = record.unresolved_input_count - if charge == 0 and record.reservation_id is not None: - reservation = reservation_by_id.get(record.reservation_id) - if reservation is not None: - charge = reservation.reserved_count - retained_input += charge + retained_input = self.retained_input_count(self.terminal_batch_records) retained_generation = sum( record.maximum_allowance for record in self.terminal_generation_reservations diff --git a/src/autoskillit/core/types/_type_helpers.py b/src/autoskillit/core/types/_type_helpers.py index 784b6c87d..dd8700b92 100644 --- a/src/autoskillit/core/types/_type_helpers.py +++ b/src/autoskillit/core/types/_type_helpers.py @@ -93,6 +93,23 @@ def _validate_non_negative(value: int, reason_code: str) -> None: _raise_invalid(reason_code) +def _reconciled_snapshot_counts( + active_count: int, + remaining_count: int, + hard_limit: int, + deducted_charge: int, + terminal_charge: int, +) -> tuple[int, int]: + charge_delta = deducted_charge - terminal_charge + if charge_delta > 0: + capacity_slack = max(hard_limit - active_count - remaining_count, 0) + active_credit = min(max(charge_delta - capacity_slack, 0), active_count) + restored_count = min(charge_delta, capacity_slack + active_credit) + return active_count - active_credit, remaining_count + restored_count + additional_charge = min(-charge_delta, remaining_count) + return active_count + additional_charge, remaining_count - additional_charge + + def _validate_bounded_text( value: str, reason_code: str, From 3173e17164a30bfba4b5380b95647d76afa1955e Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 24 Jul 2026 20:36:21 -0700 Subject: [PATCH 62/62] fix(merge): restore core stub import order --- src/autoskillit/core/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autoskillit/core/__init__.pyi b/src/autoskillit/core/__init__.pyi index e10263705..ada25c4a5 100644 --- a/src/autoskillit/core/__init__.pyi +++ b/src/autoskillit/core/__init__.pyi @@ -505,6 +505,7 @@ from .types import ReservationRecordedEffect as ReservationRecordedEffect from .types import ReservationReleasedEffect as ReservationReleasedEffect from .types import ReserveClass as ReserveClass from .types import ReserveRequestEvent as ReserveRequestEvent +from .types import ResolvedSkillAuthority as ResolvedSkillAuthority from .types import ( ResolveIndeterminateAcceptedEvent as ResolveIndeterminateAcceptedEvent, ) @@ -514,7 +515,6 @@ from .types import ( from .types import ( ResolveIndeterminateRollbackEvent as ResolveIndeterminateRollbackEvent, ) -from .types import ResolvedSkillAuthority as ResolvedSkillAuthority from .types import ResponseBackstopExemptionDef as ResponseBackstopExemptionDef from .types import RestartScope as RestartScope from .types import ResultParser as ResultParser