Skip to content

feat(policy)!: gate maintenance behind the maintain and reclaim actions - #1175

Draft
ragnorc wants to merge 3 commits into
ModernRelay:mainfrom
ragnorc:feat/maintenance-policy-actions
Draft

ragnorc wants to merge 3 commits into
ModernRelay:mainfrom
ragnorc:feat/maintenance-policy-actions

Conversation

@ragnorc

@ragnorc ragnorc commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Phase A, step 1 ("Policy") of docs/rfcs/2026-10-26-served-graph-maintenance.md (#1168). No maintenance entry point checks policy today: optimize, cleanup, ensure_indices, ensure_indices_on and repair take no actor, and repair with confirm publishes under hard-coded actor strings, so an embedded program with a policy checker installed can compact, rebuild indexes and delete history around the gate that invariant 10 requires of every mutating entry point (fact F7 in the RFC). Served maintenance, the later Phase A steps, needs a grant to check before it can have a route.

Two Cedar actions join the vocabulary, following decisions O1 and O6. maintain covers optimize, index builds, the full-text rebuild and the repair report, and is judged on the branch the operation maintains (main for optimize and repair), so it takes branch_scope like change. reclaim covers cleanup, which spans every branch's history, so it is graph scoped and refuses branch scopes. Admin stays reserved. The engine's maintenance surface is now only _as forms, each enforcing its action before any effect: optimize_as, ensure_indices_as, ensure_indices_on_as, rebuild_full_text_indices_on_as and the read-only repair_report_as check maintain, and cleanup_as checks reclaim. The actor-less forms are removed rather than made crate-private, since nothing inside the crate needs them; optimize and the index writer now record the actor on the commit they publish. For O5 the full-text rebuild admits maintain or change through a new provided method, PolicyChecker::check_any, behind Omnigraph::enforce_any, of which enforce is now the one-action case: the first allowed action admits, an evaluation failure refuses at once rather than falling through to the next action, and a refusal names every action. Repair is report-only: RepairOptions, the healed and forced actions, RepairStats::graph_manifest_version and the publish with omnigraph:repair and omnigraph:repair:force are gone. No internal path used the publishing mode, so nothing is kept crate-private for it; the coordinator's PublishPrecondition::Any publisher it called now compiles only for tests and failpoint fixtures. The guard is forbidden_apis.rs::maintenance_entry_points_take_an_actor: every surface registered under the index, optimize and cleanup protocols, plus the repair report, must be an _as method with an actor: Option<&str> parameter, and the set is pinned, so a new actor-less maintenance method fails the registry. The CLI, DST, GQT and bench callers move to the _as forms; the direct CLI passes resolve_actor, as the full-text rebuild and direct writes already do.

policy_engine_chassis.rs gains maintenance_entry_points_enforce_maintain_on_their_branch_before_effects, which refuses every maintenance entry point to an actor holding only change, reclaim or maintain on unprotected branches, and to no actor, with the storage tree byte-for-byte unchanged, then admits each under maintain and checks that optimize's commit carries the actor; cleanup_as_enforces_reclaim_before_effects does the same for cleanup; and the full-text test now admits the rebuild under either grant, requires the refusal to name both, and checks each commit's actor. The policy crate's tests round-trip every action through its name and serde, compile a rule for every action against the Cedar schema, pin the scope rules of the two new actions, and cover check_any, including that an evaluation failure on the first action is not passed over. The ignored changes_cost.rs test that built its multi-version interval with a forced repair is deleted, since no builder for that fixture remains, and the repair confirm and force tests in integration/maintenance.rs fold into the two report tests they duplicated.

For review: the decision to challenge is the scope of maintain. Judging it per branch lets a policy keep automation off protected main, and lets the O5 overlap compare change and maintain on the same scope; a graph-wide maintain would be simpler for the future routes but could not express that. The access report (GET /graphs/{graph_id}/access, the MCP access tool and omnigraph access) does not name the new actions, because no served operation performs maintenance and a new value in that enum is a wire change under the 0.14 contract; the routes step should add them. cleanup_plan, the read-only preview, keeps no actor here; the RFC's reclaim on the preview lands with its route. The CLI still refuses --as on optimize, repair and cleanup, so they act as operator.actor; widening --as belongs to the CLI step. repair --json drops its always-null graph_manifest_version. This is a source-breaking change for Rust embedders, with a breaking note. The engine was not built locally: the machine's free disk fell below the 20 GB floor during the first engine build, so the engine, CLI and workspace results are CI's. On the first CI run csv_source::tests::cancelled_csv_producer_settles_and_releases_its_admission failed its pause.is_paused() assertion; it touches nothing here and passed on the re-run, so it looks like a race in that test.

Add the `maintain` and `reclaim` Cedar actions. `maintain` covers
optimize, index builds, the full-text rebuild and the repair report and
is judged on the branch maintained; `reclaim` covers cleanup and takes
no branch scope.

Maintenance has no actor-less public engine entry point any more:
`optimize_as`, `cleanup_as`, `ensure_indices_as`, `ensure_indices_on_as`,
`rebuild_full_text_indices_on_as` and the read-only `repair_report_as`
each enforce their action before any effect. The full-text rebuild
admits `maintain` or `change` through `PolicyChecker::check_any`.
Repair loses its publishing mode, its options and its hard-coded actor
strings. The CLI, DST, GQT and bench callers move to the `_as` forms;
the direct CLI passes the actor it resolves for direct writes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant