Skip to content

scheduling: combined policy and records invariants are enforced only in the authoring CLI #1249

Description

@jeremi

Found during review of #1092.

What is unchecked

SchedulingPolicy::check_window_records is the canonical combined validation of a
policy against the published window records it governs. Among other rules,
check_window refuses a window whose staffing.pool also backs an exact-time
offering, with PolicyCheckReason::SharedSupplyUnpartitioned. The comment there
states the reason plainly: the two modes count the same staffing differently, so
the mix would double-book it in a ledger that cannot see the conflict.

That validation runs only inside registry-schedulingctl (records::validate
and the project checks), and it runs against the policy file on the operator's
disk, offline, before any transaction opens.

Neither database write re-checks it:

  • PostgresStore::apply_policy locks every supply anchor FOR UPDATE and fences
    offerings that carry active claims, but it never reads scheduling_windows
    and never calls check_window_records.
  • replace_facts swaps the window records and their anchors without consulting
    the deployed policy document.
  • The runtime performs no facts validation at startup; crates/registry-scheduling/src/runtime.rs
    calls apply_policy directly.

Why it matters

Windows are deployed under a policy that has no exact-time offering on pool A, so
the authoring check passes. The policy is later changed to add an exact-time
offering backed by pool A. Restarting the runtime publishes that policy: the
active-claim fence does not object, because the window offering keeps its
service, location, mode, and supply.

The two offerings then sell the same staffing through independent anchor rows. In
lock_and_snapshot, an exact-time admission locks exact.pool and an arrival
admission locks window.id. They are different rows in scheduling_supply, so
the capacity transactions never serialize against each other, and the ledger has
no way to observe that the window's staffing and the pool are the same people.
This is exactly the configuration check_window refuses.

Reaching it requires an operator to deploy a policy their own authoring tool would
refuse, so it is not caller-reachable. It is still a gap in the invariant the
product states it owns absolutely.

Same family as #1242

#1242 covers the supply-identifier collision between a window id and a pool id,
which records::validate refuses in the CLI and the store does not. This issue is
the same shape: a combined policy-by-records invariant enforced only in
CLI-local, file-based validation and never re-checked at either database write.
They probably want one fix.

Direction

Re-enforce the combined invariants inside both transactions, since both already
hold the supply anchors locked: load the deployed window records inside
apply_policy and refuse a candidate policy that conflicts with them, and load
the deployed policy inside replace_facts and refuse conflicting records. A fix
to only one side leaves the other direction open.

Until that lands, consider recording the gap in
products/scheduling/contracts/security-invariant-matrix.yaml as a deferred
entry whose compensating control is the authoring check, so the security contract
is not silent about it. No current row over-claims this, so the matrix is accurate
today, only quiet.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions