M01: Resource registry, state, and collectors for executable resources (issue #86) #99

Merged
adrian.tkacz merged 3 commits from m01/resource-state into develop 2026-09-19 21:11:43 +00:00
Member

Summary

Implements M01 — Resource registry, state, and collectors for executable resources (issue #86) under the A0 execution-gateway architecture (#85; D-040–D-045). M01 owns observation and normalized state only: it does not rank, choose, admit or execute anything.

One new module, scarcity_router/resource_state.py, defines the versioned resource-state snapshot contract (schema_version = 1) sibling to capacity v3, plus the in-memory registry that resolves the deferred U-003 staleness policy for the server's state store. A bounded review correction (commit 2) made the administrator registration the single authoritative owner of freshness/polling policy and configured facts.

What is introduced

  • Resource identity (ResourceIdentity): the D-042 target dimensions kept strictly separate — opaque resource_id, execution channel (server_direct_http | worker_bridged | local_app_adapter), backend provider/model/optional variant, entitlement class (5 D-042 classes + explicit unknown), and confirmed-only quota_pool_ids. Client-visible routing profiles are absent by design (M02's concept). The same subscription seen through Desktop/CLI/Windows/WSL does not become multiple pools, and pool sharing is never assumed in either direction: pools exist only as explicit confirmed ids.
  • Observation documents (ResourceStateSnapshot, v1): a pure observation record — identity, health, observed_at, quota facts with observation classes, promotion observations. It deliberately carries no freshness TTL, polling interval, capabilities or cost fields: those are administrator registration state, so a worker-reported observation is structurally unable to enlarge its own freshness window, change its polling cadence or override configured facts (serialized documents containing such fields are rejected as malformed).
  • Administrator policy is authoritative (ResourceRegistration → ResourceRegistryEntry): the registration owns freshness_ttl_seconds, poll_interval_seconds, ExecutionCapabilities (three-valued configured facts) and ResourceCost (integer micro-USD per million tokens + observation class). The registry read model composes them with the latest observation — one canonical server-side value per resource, never duplicated competing truth.
  • Health (ResourceHealth): the six capacity v3 status values unchanged (identity mapping from CapacitySnapshot, zero information loss), status-level diagnostics required for non-ok, zero/exhausted quota never a health state. stale is deliberately not a stored status — it is evaluated freshness.
  • Quota facts: QuotaFact pairs an unchanged CapacityWindow with its D-042 observation class (direct_observation, provider_telemetry, estimate, local_limit, unknown). Adapter tokens are never equated with subscription quota percentages.
  • Promotions (PromotionObservation): source, observation time, execution-channel/provider/model/plan scopes, validity period, timezone. A promotion record never asserts execution qualification (D-039 remains the proof path; eligibility is deliberately not embedded).
  • Freshness / U-003 resolution: the registration's TTL is authoritative; classify_freshness and registry reads expose exactly fresh / stale / never_observed against an injectable instant. A future-dated observation is never treated as fresh: it is rejected at application time (worker reports remain atomic) and classify_freshness fails closed on observed_at after the evaluation instant; producing server-comparable observation times is documented as the reporting side's (M05) responsibility — no skew-tolerance protocol is invented here. Polling is bounded and pull-driven (refresh_due); no background threads.
  • Registry (ResourceRegistry, RegistrySnapshot v1): in-memory per D-041 (the durable SQLite-class store stays M03/M09 scope; nothing persisted here). Fail-closed mutations: observations apply only to registered resources with exactly matching identity and non-future timestamps; registry read state is cross-resource consistent — ResourceRegistryEntry requires observation.identity == entry.identity exactly whenever an observation is present, enforced in the constructor and therefore in RegistrySnapshot.from_dict() round-trips. Monotonic revision + generated_at identify reads for audit provenance.
  • Worker state-report boundary (WorkerStateReport v1): the M01 contract for M05-reported state — opaque per-device worker_id, observation documents produced by the one shared normalization (resource_snapshot_from_capacity), only worker-reachable channels accepted, unique ids, no future-dated observations, canonically ordered, applied atomically. Transport/pairing is M05 and is not implemented here.

Preserved contracts (verified unchanged)

  • Capacity contract v3: capacity.py, status.py and providers/* are untouched; the existing Codex and Z.ai collectors remain the single telemetry authority (no second collector/parser; normalization consumes their snapshots).
  • ExecutionEligibility v1 / D-039: untouched and not embedded in resource state.
  • Machine-interface v1: server.py, cli.py, mcp.py, machine_api.py untouched — recommendation-only behavior byte-identical.

Security

Snapshots carry only safe-opaque identifiers (same restricted alphabet as capacity v3); no secrets, account identifiers, free-text fields or raw provider payloads are representable (tests assert credential-shaped ids are rejected and the capacity plan label is not carried). No network access, no credentials, no live accounts — all tests are deterministic synthetic fixtures.

Tests

80 deterministic tests in tests/test_resource_state.py: identity/vocabulary validation, all six status mappings, failure isolation between resources, zero-vs-unknown-vs-exhausted distinctions, fresh/stale/never-observed boundaries, registration-policy authority (worker cannot enlarge freshness or change polling/configured facts — structurally and behaviorally), cross-resource ResourceRegistryEntry.from_dict / RegistrySnapshot.from_dict rejection, future-timestamp rejection at classify_freshness and at apply time, refresh-due cadence, confirmed-pool grouping and non-assumption of sharing, worker-report atomicity, wrong-version/malformed documents failing closed, and canonical deterministic serialization.

Validation

uv sync --only-dev           OK
make check                   OK (1125 tests + typecheck)
uv run basedpyright          0 errors, 0 warnings, 0 notes
git diff --check             clean
make package-check           all checks passed

Forgejo CI workflow ci / job check runs on this PR (pull_request event) and is green on the head commit.

Deliberate deferrals

  • Routing/ranking, requirement binding, routing profiles → M02 (#87).
  • Gateway, admission, execution, audit writing → M03 (#88).
  • Generic HTTP/Ollama adapters, their live probes and any observed (vs configured) capability/cost facts → M04 (#89) and an explicit future contract extension.
  • Worker transport, pairing, heartbeat, clock-skew/tolerance protocol → M05 (#90) (only the state-report content contract lands here).
  • Codex/ZCode execution adapters → M06/M07 (#91/#92).
  • Administrator configuration surface, TTL/polling defaults, durable store schema → M09 (#94) / M03 (#88).

Closes #86 upon merge (the Forgejo issue's acceptance criteria are covered above).

## Summary Implements **M01 — Resource registry, state, and collectors for executable resources** (issue #86) under the A0 execution-gateway architecture (#85; D-040–D-045). M01 owns **observation and normalized state only**: it does not rank, choose, admit or execute anything. One new module, `scarcity_router/resource_state.py`, defines the versioned resource-state snapshot contract (`schema_version = 1`) sibling to capacity v3, plus the in-memory registry that resolves the deferred U-003 staleness policy for the server's state store. A bounded review correction (commit 2) made the administrator registration the single authoritative owner of freshness/polling policy and configured facts. ## What is introduced - **Resource identity** (`ResourceIdentity`): the D-042 target dimensions kept strictly separate — opaque `resource_id`, execution channel (`server_direct_http` | `worker_bridged` | `local_app_adapter`), backend `provider`/`model`/optional `variant`, entitlement class (5 D-042 classes + explicit `unknown`), and **confirmed-only** `quota_pool_ids`. Client-visible routing profiles are absent by design (M02's concept). The same subscription seen through Desktop/CLI/Windows/WSL does not become multiple pools, and pool sharing is never assumed in either direction: pools exist only as explicit confirmed ids. - **Observation documents** (`ResourceStateSnapshot`, v1): a pure observation record — identity, health, `observed_at`, quota facts with observation classes, promotion observations. It deliberately carries **no** freshness TTL, polling interval, capabilities or cost fields: those are administrator registration state, so a worker-reported observation is structurally unable to enlarge its own freshness window, change its polling cadence or override configured facts (serialized documents containing such fields are rejected as malformed). - **Administrator policy is authoritative** (`ResourceRegistration` → `ResourceRegistryEntry`): the registration owns `freshness_ttl_seconds`, `poll_interval_seconds`, `ExecutionCapabilities` (three-valued configured facts) and `ResourceCost` (integer micro-USD per million tokens + observation class). The registry read model composes them with the latest observation — one canonical server-side value per resource, never duplicated competing truth. - **Health** (`ResourceHealth`): the six capacity v3 status values unchanged (identity mapping from `CapacitySnapshot`, zero information loss), status-level diagnostics required for non-`ok`, zero/exhausted quota never a health state. `stale` is deliberately not a stored status — it is evaluated freshness. - **Quota facts**: `QuotaFact` pairs an unchanged `CapacityWindow` with its D-042 observation class (`direct_observation`, `provider_telemetry`, `estimate`, `local_limit`, `unknown`). Adapter tokens are never equated with subscription quota percentages. - **Promotions** (`PromotionObservation`): source, observation time, execution-channel/provider/model/plan scopes, validity period, timezone. A promotion record never asserts execution qualification (D-039 remains the proof path; eligibility is deliberately not embedded). - **Freshness / U-003 resolution**: the registration's TTL is authoritative; `classify_freshness` and registry reads expose exactly `fresh` / `stale` / `never_observed` against an injectable instant. **A future-dated observation is never treated as fresh**: it is rejected at application time (worker reports remain atomic) and `classify_freshness` fails closed on `observed_at` after the evaluation instant; producing server-comparable observation times is documented as the reporting side's (M05) responsibility — no skew-tolerance protocol is invented here. Polling is bounded and pull-driven (`refresh_due`); no background threads. - **Registry** (`ResourceRegistry`, `RegistrySnapshot` v1): in-memory per D-041 (the durable SQLite-class store stays M03/M09 scope; nothing persisted here). Fail-closed mutations: observations apply only to registered resources with exactly matching identity and non-future timestamps; **registry read state is cross-resource consistent** — `ResourceRegistryEntry` requires `observation.identity == entry.identity` exactly whenever an observation is present, enforced in the constructor and therefore in `RegistrySnapshot.from_dict()` round-trips. Monotonic `revision` + `generated_at` identify reads for audit provenance. - **Worker state-report boundary** (`WorkerStateReport` v1): the M01 contract for M05-reported state — opaque per-device `worker_id`, observation documents produced by the **one shared normalization** (`resource_snapshot_from_capacity`), only worker-reachable channels accepted, unique ids, no future-dated observations, canonically ordered, applied atomically. Transport/pairing is M05 and is not implemented here. ## Preserved contracts (verified unchanged) - Capacity contract **v3**: `capacity.py`, `status.py` and `providers/*` are untouched; the existing Codex and Z.ai collectors remain the single telemetry authority (no second collector/parser; normalization *consumes* their snapshots). - `ExecutionEligibility` v1 / D-039: untouched and not embedded in resource state. - Machine-interface v1: `server.py`, `cli.py`, `mcp.py`, `machine_api.py` untouched — recommendation-only behavior byte-identical. ## Security Snapshots carry only safe-opaque identifiers (same restricted alphabet as capacity v3); no secrets, account identifiers, free-text fields or raw provider payloads are representable (tests assert credential-shaped ids are rejected and the capacity `plan` label is not carried). No network access, no credentials, no live accounts — all tests are deterministic synthetic fixtures. ## Tests 80 deterministic tests in `tests/test_resource_state.py`: identity/vocabulary validation, all six status mappings, failure isolation between resources, zero-vs-unknown-vs-exhausted distinctions, fresh/stale/never-observed boundaries, registration-policy authority (worker cannot enlarge freshness or change polling/configured facts — structurally and behaviorally), cross-resource `ResourceRegistryEntry.from_dict` / `RegistrySnapshot.from_dict` rejection, future-timestamp rejection at `classify_freshness` and at apply time, refresh-due cadence, confirmed-pool grouping and non-assumption of sharing, worker-report atomicity, wrong-version/malformed documents failing closed, and canonical deterministic serialization. ## Validation ``` uv sync --only-dev OK make check OK (1125 tests + typecheck) uv run basedpyright 0 errors, 0 warnings, 0 notes git diff --check clean make package-check all checks passed ``` Forgejo CI workflow `ci` / job `check` runs on this PR (pull_request event) and is green on the head commit. ## Deliberate deferrals - Routing/ranking, requirement binding, routing profiles → M02 (#87). - Gateway, admission, execution, audit writing → M03 (#88). - Generic HTTP/Ollama adapters, their live probes and any observed (vs configured) capability/cost facts → M04 (#89) and an explicit future contract extension. - Worker transport, pairing, heartbeat, clock-skew/tolerance protocol → M05 (#90) (only the state-report content contract lands here). - Codex/ZCode execution adapters → M06/M07 (#91/#92). - Administrator configuration surface, TTL/polling defaults, durable store schema → M09 (#94) / M03 (#88). Closes #86 upon merge (the Forgejo issue's acceptance criteria are covered above).
feat(m01): versioned resource-state contract, registry and worker report boundary (issue #86)
All checks were successful
ci / check (pull_request) Successful in 51s
b5b573643f
Add scarcity_router/resource_state.py as the new resource-state snapshot
contract (schema_version 1) sibling to capacity v3: resource identity
separating physical model/variant, execution channel, entitlement and
confirmed quota pools (D-042); health in the capacity v3 status vocabulary;
execution capabilities as observed/configured facts; quota and cost facts
with observation classes; promotions as separate observations; and the
U-003 freshness/staleness resolution with bounded polling metadata and an
in-memory ResourceRegistry (D-041).

WorkerStateReport defines the M01 contract boundary for worker-reported
state through the one shared normalization (resource_snapshot_from_capacity);
the M05 transport is not implemented here. Capacity contract v3,
ExecutionEligibility v1 and all recommendation-only interfaces are
unchanged; 72 new deterministic contract tests use synthetic fixtures only.

Resolve U-003 in docs/decisions.md and point docs/capacity-model.md and
docs/architecture.md at the implemented contract.
Bounded contract corrections to PR #99; M01 architecture and scope are
unchanged.

1. Administrator-owned resource policy is authoritative. ResourceStateSnapshot
   is now a pure observation record (schema_version 1 shape adjusted before
   release): it no longer carries freshness TTL, polling interval,
   capabilities or cost. Those live only on ResourceRegistration and are
   composed into ResourceRegistryEntry, so an accepted observation —
   including a worker report — structurally cannot enlarge its own freshness
   window, change its polling cadence or override configured facts.
   resource_snapshot_from_capacity drops the policy parameters accordingly.
2. Registry read state fails closed on cross-resource state:
   ResourceRegistryEntry now requires observation.identity ==
   entry.identity exactly whenever an observation is present, enforced in
   the constructor and therefore in RegistrySnapshot.from_dict round-trips.
   Other cross-field invariants of the read model audited (pool derivation
   equality, never_observed iff no observation, entry/pool ordering) —
   already enforced; no other gaps found.
3. Future timestamps fail closed: classify_freshness rejects
   observed_at > evaluation instant instead of computing a negative age as
   fresh; the registry also rejects future-dated observations at
   application time (worker reports remain atomic). Producing
   server-comparable observation times is documented as the reporting
   side's (M05) responsibility; no skew-tolerance protocol is invented.

docs/decisions.md U-003 and docs/capacity-model.md updated to the
authoritative-registration wording. 80 deterministic tests (8 new), all
synthetic; capacity v3, ExecutionEligibility v1 and recommendation-only
interfaces untouched.
Final consistency corrections to PR #99; M01 design unchanged.

docs/architecture.md: the resource-state contract paragraph now describes
the implemented three-record split — ResourceStateSnapshot (pure
observation: identity, observed_at, health, quota facts + observation
classes, promotions), ResourceRegistration (administrator-owned canonical
policy: freshness TTL, polling cadence, configured capability facts,
configured cost facts) and ResourceRegistryEntry (evaluated self-contained
read model combining registration state with the latest observation and
derived freshness/refresh-due state). The module-map ownership statement
that M01 as a whole owns identity/health/freshness/cost/pool state stays
true; no other doc wording carried the stale assumption.

resource_state.py: refresh_due and the registry-read refresh-due
evaluation now reuse the same age/time validation seam as
classify_freshness (_age_or_reject), so an observation later than the
evaluation instant fails closed with CapacityValidationError instead of
being silently treated as not due via a negative age. The apply-time
future check uses the same seam. No clock-skew tolerance; that remains
M05 territory.

Two new deterministic tests: refresh_due rejects a backwards evaluation
instant; boundary behavior unchanged (age 0 valid, polling threshold and
never-observed semantics intact).
adrian.tkacz deleted branch m01/resource-state 2026-09-19 21:11:53 +00:00
Sign in to join this conversation.
No description provided.