feat(govern): Policy Store REST client behind the PolicyLookup seam

What

The behavioral half of the split requested in the review (comment 3736928080): stacked on !177 (closed) (glaz-policy-store-client crate + lookup error taxonomy, inert). This MR switches everything onto the client, as one story — no implicit policy source anywhere:

  • glaz-module constructs explicitly: zero-config new()/Default deleted (an implicit source would evaluate fixture policies against a real tenant, or fail open as an empty "nothing to enforce" batch); with_lookup / with_limits(lookup, …) / with_store(config: StoreConfig) with the client types re-exported, and the client as a hard link-time dependency.
  • The canned fixture retires from every shipped surface: glaz-govern exposes the PolicyLookup seam and no implementation — the list lookup is a #[cfg(test)] ListLookup, and every test states its own policies (including the scope-filtered one from the scope-evaluation work).
  • ABI v3, one constructor: glaz_govern_engine_new/_with_limits deleted; glaz_govern_engine_new_with_store(…, max_time_ms, check_interval) is the only way to build an engine (name kept so a stale host resolving v2's zero-argument engine_new fails at symbol lookup instead of calling a wrong signature).
  • Self-contained conformance: cases.json is a {policies, cases} document (Go-client re-sync required); every harness serves the entities from a local stub speaking the store's list contract and drives the shipped constructor. smoke.c fails closed without a store URL (GLAZ_SMOKE_NO_STORE opts out for the packaging link proofs); ffi-link runs the ephemeral-port stub plus the nm export gate.

The client's own contract, hardening, and stub-server suite are reviewed in !177 (closed) — this MR contains no HTTP code.

Decisions

  • HTTP client: blocking ureq 3 with platform trust roots (self-managed behind an internal CA works), chosen for simplicity — one call per lookup needs no async runtime. 500 ms connect / 2 s total defaults, pinned by test — including behaviorally, against a server that accepts and never responds. deny.toml carries per-crate CDLA-Permissive-2.0 exceptions for the webpki root-store data crates only.
  • No caching, no retries in this milestone — the hot-path strategy (cache / GVL release / in-process fetch for the Rails host) is a tracked decision: #17, to be recorded on gitlab-org/gitlab#604407.
  • CI: ffi-link runs the C smoke test against a local stub on an ephemeral port (fail-closed when the stub URL is missing) and asserts the released staticlib exports glaz_govern_engine_new_with_store. No --all-features anywhere — no workspace crate declares features anymore.

Testing

  • cargo test --workspace — 249 tests, everything in the default build: 19 stub-server contract/hardening tests (single-request local TcpListener, no new dev-dependencies), module + FFI end-to-end backend-fault tests, explicit-construction coverage
  • cargo clippy --workspace --all-targets -- -D warnings, cargo fmt --all --check, C smoke test against the real staticlib (three states: stub URL set, missing = failure, explicit opt-out)

Out of scope / draft until

Deliberately not here: caching/invalidation (#17, #604407), enforcement_mode evaluation semantics (#607657 — the client already carries the field's gate), gem configuration plumbing (gem-repo follow-up: ext/glaz must move to explicit construction since zero-config new() no longer exists).

Draft until:

  1. The store serves executable policy content (transpiled rego on the entity, or the GOVERN-006 evaluation/bundle read) — the DTO/mapping will likely reshape with it
  2. A bounded, engine-facing read route exists (Govern::Policy.evaluation_candidates is only a model method today; the current route is the unbounded CRUD list behind admin-scoped read_govern_policy) with a lesser-scoped token provisioned for GLAZ
  3. One smoke test against a real GDK store (experiment enabled, Ultimate)
  4. The hot-path decision in #17

Author checklist

  • Title follows Conventional Commits and breaking changes are flagged (pre-release surface; the gem binding is adapted in its own repo)
  • Docs / comments updated where behaviour changed
  • No unrelated changes swept in
Edited by Artur Fedorov

Merge request reports

Loading
Loading