Shared Crates Reference

On this page

Canopy’s crates/ directory holds the cross-cutting libraries every service depends on. This page is the public-API orientation for each; the crate source + rustdoc are the authoritative detail.

canopy-common

Cross-cutting utilities shared by all services. Modules: crypto, date, error, http, id, pagination, policy_target (the #1467 composite policy identity — validated CorpusHashHex/ParamsDigest 64-hex newtypes, the half-open EffectivePeriod, PolicyTarget, the snapshot’s ParamsProvenance projection, and the #1472 PolicyTargetRef request-side pair a dry-run caller names a target policy with; ADR-028 Amendment 6), settings, telemetry, trigger (the ADR-002 A1 D9 DeterminationTrigger provenance classifier, #1468).

  • ApiError — enum (BadRequest, Unauthorized, Forbidden, NotFound, Conflict, Internal). Use as the handler return type; implements IntoResponse with RFC 9457 Problem Details JSON.

  • define_id! macro — generates typed newtype UUID wrappers (PersonId, HouseholdId, ApplicationId, …) that prevent compile-time ID mixing. All use UUID v7 (Uuid::now_v7()).

  • PageRequest / PageResponse — pagination query/response structs. Max 500 per page.

  • ServiceSettings — loads env vars with the CANOPY_{SERVICE}__ prefix via the config crate. ServiceSettings::load(prefix).

  • age_years(dob, as_of) — calendar-aware age (not num_days() / 365).

  • encrypt_field() / decrypt_field() — AES-256-GCM field-level encryption. Key from CANOPY_ENCRYPTION_KEY (base64, 32 bytes); 12-byte random nonce prepended to the ciphertext.

  • http::data_path_client() — the single blessed reqwest::Client builder with explicit connect (5s) + total (30s) timeouts, so a black-holed upstream fails fast instead of hanging a lock / MQ handler / SSR handler (#1200/#1243). build_data_path_client(connect, total) is the timeout-parameterized core (fault-injection tests). Bare reqwest::Client::new() is banned in production by the cargo xtask http-clients audit-client-new ratchet gate.

canopy-auth

Keycloak OIDC JWT validation and RBAC (ADR-019).

  • Claims — extracted from the JWT realm_access.roles. Role gates: require_caseworker_or_above(), require_eligibility_specialist_or_above(), require_supervisor_or_above(), require_admin() (return ApiError::Forbidden on failure). Carries primary_programs (a bearer-JWT claim populated by the realm’s user-attribute mapper — ADR-044); parsed_primary_programs() is fail-closed (401 on an unknown slug). The field is #[serde(default)], so absent and [] are indistinguishable on the wire — consumers must treat both as "no scope stated", never as "all programs". canopy-web does exactly that: since #1515 it refuses admission for either (ADR-044).

  • AuthLayer — tower middleware validating the RS256 JWT on every request; extracts Claims into request extensions. Rejects (401) any request carrying the retired X-Canopy-Actor header (#1443); verifies X-Canopy-Applicant ownership claims when an ApplicantClaimVerifier is wired (#1442).

  • JwksProvider — fetches + caches JWKS from Keycloak; auto-refreshes hourly; forced refresh on an unknown kid with a 30-second debounce. Single-flighted (concurrent refreshers coalesce; a slow fetch can’t clobber a newer key set). ensure_fresh(max_age) force-refreshes within a bound; for_self_validation(aud) yields a sibling sharing the warmed key cache but scoped to a single audience; validate_current() validates against the current cache with typed errors (no internal refresh) — the ADR-037 primitives.

  • ServiceTokenSource — caches this service’s own client_credentials JWT (ADR-019); current() returns it, minting + refreshing 5 min before expiry. with_self_validation(provider, max_age) (ADR-037) makes current() revalidate the cached token against a bounded-fresh JWKS and re-mint when its signing key has been deleted at the IdP (fail-closed on a fresh JWKS, fail-open when the JWKS is unreachable). Off by default (TTL-only); wired on in canopy-api bootstrap (every service that goes through it). with_scopes (#1440) adds RFC 6749 scope values to the mint — how the portal’s per-target narrow sources activate their aud-canopy-<target> client scopes; canopy-portal builds EIGHT per-target sources (one per backend target, each self-validating against its own target audience) via PortalTokenSources (#1039 wired the original single source; #1440 split it).

  • EffectiveUser — the single resolution of "who is the human behind this request" (ADR-043, OIDC F1b; two shapes since #1443): Direct (any non-service bearer, including exchanged tokens) or System (background service traffic). require_user() → 403 for system traffic; attribution_sub() is the attribution shape. Adopted fleet-wide per the F1a inventory.

  • SubjectBearer — request extension carrying the validated user bearer token for RFC 8693 exchange (OIDC F2, #1420). Deliberately implements neither Debug, Display, nor Serialize — logging it is a compile error; the only accessor is expose_for_exchange(). Populated by `AuthLayer’s middleware only for validated non-service bearers.

  • policy — receiver-contract primitives (OIDC F2): require_exact_audience (the token’s aud must be exactly the target — kills any-match for opted-in routes), require_azp_allowlisted, require_realm_role (any-of; empty inputs fail closed). All return PolicyDenial, which converts only to a coded 403 Forbidden (aud_not_exact / azp_not_allowlisted / required_role_missing) — the ADR-043 frozen 401/403 contract encoded in the type.

  • ChainAuditSink — the production ExchangeAuditSink (OIDC A1, #1424): every exchange outcome becomes an auth.token_exchange row in canopy-security’s ADR-014 chain via the commit-before-return HTTP ingest, under the service’s own ADR-019 identity. One bounded retry on availability failures only (safe: canopy-security dedups grants by exchange jti server-side), per-attempt budget inside the broker’s 5s AUDIT_TIMEOUT, deterministic 4xx rejections never retried. Constructed at boot by canopy-web and canopy-eligibility when the dedicated exchanger credentials (oidc_exchanger_client_id/_secret, ServiceSettings) are configured — absent, the exchange path stays inert.

  • TokenExchanger — the RFC 8693 exchange broker (OIDC F3, #1421; live behind config since A1 #1424 — R1 wired the realm, A1 the audit sink + boot construction). Validates every exchanged token before use (ADR-043 A2: sub preserved, azp = the exchanger client, exactly the requested audience, worker roles intact — a stripped-roles exchange fails loudly at the broker — scope ⊆ requested, Bearer, no refresh token, exp ≤ min(subject.exp, now+300s+5s skew tolerance on the TTL arm — #1565; the validation clock is sampled post-roundtrip, and the subject arm stays exact)); audits before release via the fail-closed ExchangeAuditSink boundary (a token whose audit write didn’t commit is dropped, and denials audit too); caches per request only (ExchangeCache, ruling R4 — key = audience + canonical scope set + purpose, roles re-checked on every hit); bounded timeout/body/retry with an availability breaker, and no fallback: an IdP outage fails the request, never hands back a broad service token. ExchangedToken is non-Debug/Display/Serialize like SubjectBearer. canopy-test-lib gains acquire_exchanged_token (returns None pre-R1, same as devstack-down).

  • CanopyRequestBuilderExt — outbound reqwest::RequestBuilder decorators: with_service_identity(token) (the ADR-019 service bearer) and with_finalize_step(Option<FinalizeStepHeaders>) (the three x-canopy-finalize-* headers). The finalize header namesFINALIZE_OPERATION_HEADER / FINALIZE_GENERATION_HEADER / FINALIZE_STEP_HEADER — live here as the single source shared by the writer (canopy-persons-client) and the reader (canopy-persons' FinalizeStep::from_headers, which re-exports them), so the literals are never duplicated (ADR-038).

Roles, highest to lowest: admin, supervisor, quality_control, eligibility_specialist, caseworker, applicant.

canopy-chain

The chain-v2 byte-level protocol substrate (#1246 MR-1, ADR-014 Amendment 6). Pure by construction: no DB, no signing, no clock — every byte-level rule in one place, consumed by the SQL append functions' Rust callers (#1207), the verifiers (#1205/#1206), and the xtask genesis installer.

  • canonical_bytes(&Value) — RFC 8785-conforming canonicalization (serde_json_canonicalizer) behind a recursive I-JSON validation layer that REFUSES integers beyond ±(2^53−1) (conforming serializers silently round them through f64 — a semantic collision an audit chain must not accept; floats pass).

  • Validated newtypes (ShardCount 1..=32767, EventSeq/HeadSeq, UUIDv7-checked ChainInstanceId, const-2 HashFormulaVersion, EventHash([u8; 32]), ChainFamily/ChainSource closed enums).

  • ChainEnvelope + event_hash — the pinned formula-bearing event-hash preimage; AuditPayloadBuilder / FtiPayloadBuilder — the CLOSED per-family payload constructors (excluded columns structurally unplaceable; the MR-2 SQL re-validates the exact key set).

  • shard_for(routing_id, count) — deterministic shard routing; empty_head_hash / GenesisPlan / verify_genesis_state — the pure genesis rules the installer executes; AnchorManifest — the pinned C5 anchor encoding (five kinds incl. genesis, ordered complete shard array, zero-sentinel linkage).

  • Frozen, INDEPENDENTLY-seeded KAT corpus under tests/vectors/ — changing a vector means bumping that surface’s protocol version + an ADR-014 amendment (see Testing for the pattern).

canopy-db

PostgreSQL connection-pool wrapper.

  • DbPool — wraps sqlx::PgPool. DbPool::connect_with(url, opts); db.inner() returns &PgPool for queries.

  • DbPoolOptsmax_connections (default 10), idle_timeout (default 600s).

  • validate_database_name(url, service_name) — warns if the DATABASE_URL database name doesn’t match the service name (ADR-001 program isolation).

  • advisory::run_with_advisory_lock(pool, lock_name, f) — tick-level leader election on pg_try_advisory_xact_lock (lock id = first 8 bytes of SHA-256 of the name); returns SchedulerOutcome::{Ran, Skipped}. Dedups CONCURRENT invocations only — backs the manual scheduler triggers; background daily loops use the window fence below (#1211, scale audit H2).

  • window_fence::run_daily_fenced(pool, job_name, f) (#1211) — wall-clock window fence for daily schedulers: INSERT .. ON CONFLICT DO NOTHING election on the service-local scheduler_runs table keyed (job_name, UTC-day); the single winner per window runs, every other probe — any replica, any boot time, any restart — skips. The #428 advisory lock (same name) is composed inside, so a fenced tick never overlaps a manual trigger that competes for the same lock name, and a lost advisory race doesn’t consume the window. A failed tick releases its window (retried by the next hourly probe); a crash mid-tick consumes it (documented trade vs the old pattern’s once-per-replica-per-day over-running). Canonical DDL: crates/canopy-db/scheduler-migrations/, copied per-service and parity-gated by cargo xtask outbox-migrations. window_fence::consume_window(pool, job_name) (#1218) lets a manual trigger that just performed the window’s work under the job’s advisory lock stamp the day consumed, so the next fenced probe skips instead of repeating it. Consumers: renewals scheduler + caseload-rollup refresh (#1218), enrollment expungement, applications draft-reaper + recovery-pruner, appeals decision-clock + reconciliation.

  • ensure_idempotency_schema(pool) (#1463) — the fleet idempotency_keys DDL (create + the ADR-016 expand migration), advisory-lock-serialised and idempotent. Owned by the migration path — called from canopy_api::bootstrap’s `apply_migrations_on (in-process MigrationMode::Run) and from cargo xtask migrate apply (JobOwned/SKIP_MIGRATIONS) — never by the runtime: IdempotencyCache::with_pool only verifies the table exists, so a least-privilege runtime role needs no CREATE (ADR-004 A8b). Deliberately raw DDL, not sqlx::migrate! — a second migrator would fight each service’s own _sqlx_migrations tracker.

  • lease::spawn_heartbeat(period, renew) (ADR-038, epic &71 MR4) — the shared holder-side row-lease renewal loop: calls the caller’s lease-fenced extend UPDATE every period, stops + flags on a fence miss (lease stolen), retries on transient errors (failing toward losing the lease, never falsely keeping it). Returns an abort-on-drop HeartbeatGuard with an advisory lease_lost(); the authoritative fence stays the caller’s own fenced terminal UPDATE. lease::new_claim_id() mints the v7 holder id. Per-table claim/steal SQL stays with its store (finalize saga, idempotency single-flight, outbox drainer) — the renewal loop is the shared piece.

canopy-mq

RabbitMQ event publishing + subscribing.

  • EventEnvelope — standard wrapper: id (UUID v7), source, event_type (routing key), payload (JSON), timestamp, optional W3C trace_context.

  • Publisher — publishes to the canopy.events topic exchange. Validates the payload before publish — rejects 35 restricted field names (SSN, FTI, IEVS, HIPAA) per ADR-004. Uses the persistent event_outbox (ADR-018): publish_tx(tx, envelope) stages the row in the caller’s transaction; the background OutboxDrainer flushes it later.

  • Event-hold (ADR-039): publish_tx_held(tx, envelope, EventHold { operation_id, generation }) stages an event held (atomic with the domain write, like publish_tx, but carrying a hold key the drainer skips). release_held(exec, hold) clears the key so it drains; drop_held(exec, hold) deletes still-held unpublished rows (compensation). Both are idempotent and run on any executor. Used by the epic &71 finalize saga so downstream never sees a partial/aborted cross-service operation.

  • Subscriber — subscribes to routing-key patterns (e.g. determination.completed.*). Callback: async fn(EventEnvelope) → Result<(), anyhow::Error>. Parked-state inbox (#1089): a handler that cannot INTERPRET a delivery returns Err(ParkEvent::…) — the delivery is durably parked in event_inbox (broker acked; handler tx rolled back) and the per-subscriber unpark scanner (CANOPY_MQ_UNPARK_INTERVAL_SECS, default 60s; on-demand via run_unpark_pass) re-offers it until a capable binary processes it. The inbox classify step is row-locked (FOR UPDATE), so concurrent replicas can never double-run one event; envelopes carry schema_version (additive-within-a-version rule) and the inbox schema is single-sourced with the outbox (ADR-039). Full protocol: Event-Delivery Protocol. Queues are durable by default (#1088): rights-bearing events must survive a broker restart, and a transient queue would let the outbox drainer publish into nothing after one. A pre-existing mismatched queue of the same name is self-healed on attach (one-time if-empty delete + durable redeclare, PRECONDITION_FAILED-scoped — a mismatched queue still holding messages fails loudly for operator action rather than losing a backlog). In the devstack provisioning (devstack/rabbitmq/definitions.json), the broker keeps its state in a compose volume and unroutable publishes (no bound queue yet) land in the durable canopy.unrouted capture queue via an alternate-exchange policy instead of being discarded; a production broker must be provisioned equivalently (tracked in the epic &72 production-gap register).

  • OutboxDrainer::spawn(pool, manager) — background lease-based three-phase drainer (ADR-018 + #478): claim with a FOR UPDATE SKIP LOCKED CTE (skipping held rows, AND hold_operation_id IS NULL), publish outside any DB tx with channel-per-batch publisher confirms, mark in two short txes guarded by claimed_by. Crashed-drainer recovery via CANOPY_MQ_DRAINER_LEASE_TTL_SECS.

  • Outbox schema is single-sourced (ADR-039): the canonical migrations live in crates/canopy-mq/outbox-migrations/ and are generated into every service (cargo xtask outbox-migrations --write) + parity-gated (--check, in the pre-push battery) — no more hand-copied per-service outbox migrations.

  • connect(url)Arc<lapin::Connection>. Constant EVENTS_EXCHANGE = "canopy.events".

Drainer env vars (defaults work for production):

Env var Default Purpose

CANOPY_MQ_DRAINER_TICK_MS

250

Sleep between drain ticks.

CANOPY_MQ_DRAINER_BATCH_SIZE

100

Max rows claimed per tick.

CANOPY_MQ_DRAINER_LEASE_TTL_SECS

60

How long a claim survives before reclaim. Boot assert: >= 3 × pipeline_depth × 100ms.

CANOPY_MQ_DRAINER_PIPELINE_DEPTH

32

Max in-flight publishes before awaiting confirms within a batch.

CANOPY_MQ_DRAINER_CONFIRM_TIMEOUT_SECS

30

Deadline for one batch’s whole Phase-2 publish+confirm exchange (#1061). Boot asserts: >= pipeline_depth × 100ms, < lease TTL.

CANOPY_MQ_OUTBOX_RETENTION_DAYS

7

Janitor sweep threshold for published rows.

CANOPY_MQ_HELD_AGE_WARN_SECS

7200

Held-row watch warn threshold for the oldest ADR-039 held row (#1061).

CANOPY_MQ_REPLICA_ID

drainer-{hostname}-{pid}-{uuid-v7}

Source for claimed_by.

CANOPY_MQ_PREFETCH_COUNT

32

Per-consumer AMQP prefetch (basic_qos, global: false) set before basic_consume on every attach path — bounds in-flight+buffered deliveries per consumer so a backlog fans across competing consumers instead of dumping into one replica’s unbounded buffer (#1199, scale audit H3). 0 (unlimited) is rejected → default; recommended band 16–64.

canopy-api

Shared Axum server infrastructure.

  • AppState { db: DbPool, auth: AuthLayer } — standard service state.

  • ApiServer::router(state, routes, options, openapi) — builds the Axum router with CORS, rate limiting, body limit, idempotency middleware, and OpenAPI/Swagger UI.

  • bootstrap(prefix, service_name, migrator) → (ServiceSettings, BootstrapResult) — standard startup: load settings, connect DB, validate database name, apply the supplied sqlx::migrate::Migrator, connect RabbitMQ, fetch JWKS, create publisher/subscriber. Callers pass sqlx::migrate!("./migrations") at their site so the path resolves relative to their crate; bootstrap runs the migrator before the OutboxDrainer spawns (centralising this prevents the #471 drift — #473).

  • shutdown_signal() — graceful SIGTERM/SIGINT (Unix) or Ctrl+C (Windows).

  • ServerOptionscors_origins, body_limit, rate_limit_rpm.

  • pagination::{DEFAULT_LIMIT, MAX_LIMIT, clamp_limit} — the single home for the keyset page-size bounds (ADR-001 Amendment 1 §B2, #1251): the interactive keyset lists clamp their limit param through clamp_limit (absent → 50, forced into 1..=200), and the canopy-reporting universe drains request pages at MAX_LIMIT. A per-endpoint deviation needs a justification comment at the deviating site — today the three /v1/overpayments handlers (default 200 / max 500, the §B2 larger-page-for-throughput override), each carrying a const assert that its cap stays ≥ MAX_LIMIT so the drain’s full-page cadence can’t silently break.

canopy-api retry

canopy_api::retry is a bounded exponential-backoff retry layer for outbound reqwest calls (#462), coordinating with the server-side single-flight idempotency middleware (epic #1003). Because the retry client resends the same Idempotency-Key and body, a request that already completed replays its stored response; one still in flight on another worker returns a retryable 503 (with Retry-After), and a reused key with a changed body returns 409. The handler runs once per key across concurrent requests and replicas — a retry never double-executes the side effect.

  • RetryPolicy::default_http() — 3 attempts, 100ms→30s backoff, ±25% jitter, no overall timeout. Tunable via with_max_attempts / with_overall_timeout / with_per_attempt_timeout / with_initial_backoff / with_max_backoff / with_jitter (all assert invariants at construction). with_overall_timeout is a hard wall-clock cap (#572): the whole retry body is wrapped in one tokio::time::timeout, so a per-attempt timer that overruns under tokio timer-wheel starvation cannot inflate the total past the SLO — the outer timeout preempts regardless. with_per_attempt_timeout sets an explicit per-attempt cap independent of the overall budget; when unset, each attempt’s budget is the remaining overall split across the remaining attempts (or 30s when there is no overall cap).

  • RetryRequest — descriptor with private fields. Constructors RetryRequest::{get, head, delete, post_with_idempotency_key} + try_new(method, has_key). POST without an idempotency key is unconstructable; PATCH/PUT unsupported in this iteration.

  • retry_request(&policy, &request, make_req) — runs make_req (builds a fresh RequestBuilder per attempt). Ok(Response) for terminal HTTP (2xx, or 4xx not-retryable — body preserved); Err(RetryError) only for exhausted / timeout / non-retryable network error.

  • Emits tracing::info!(target: "retry", attempt, backoff_ms, status, error, …) before each retry sleep — operators grep this target.

For POST retries the caller MUST send the same Idempotency-Key on every attempt (TestClient::with_retry auto-injects a Uuid::now_v7() once). The eligibility orchestrator’s dispatch was wrapped by #462 but reverted to a single-attempt reqwest call after the #572 semantic flaw inflated slow_program_does_not_block_combined_result wall-clock under workspace-parallel timer-wheel starvation; #572 has since made overall_timeout a hard tokio::time::timeout wrap, but the orchestrator deliberately keeps single-attempt dispatch (a ~30-call synchronous fan-out per action should fail fast, not amplify latency/load), keeping the Idempotency-Key: det_id header so the program service replays the first response if the call is ever resent.

canopy-store

S3-compatible object storage.

  • Store — wraps the object_store crate. put(path, bytes), get(path), delete(path), list(prefix).

  • ObjectStoreConfig — from CANOPY_STORE_* env vars; supports local filesystem, S3, and Garage.

  • validate_upload(bytes, claimed_content_type, filename, &UploadValidation) — PURE content validation (size, magic-byte, allowlist, SHA-256, filename; sync since #1006 — scanning is an explicit write-site step, not part of validation).

  • scan_admitting(scanner, bytes) — the inline write-site scan gate (put_validated and other inline-scanning callers); the applications quarantine lifecycle (ADR-042) scans asynchronously instead.

  • Scanner trait + ScanReport/ScanResult/ScanError — the pluggable AV seam; NoopScanner ships here, real backends live outside the crate.

  • sanitize_filename(name) — prevents path-traversal.

canopy-scanner-clamd

ClamAV clamd INSTREAM backend for the Scanner trait (ADR-042/#1006). Transport via clamav-client (tokio, pure Rust); response parsing is strict and OURS (single NUL/newline-terminated line, 512-byte cap, UTF-8). Verdict mapping quarantines what the engine could not inspect (Heuristics.Encrypted., Heuristics.Limits.Exceeded, the INSTREAM size-limit class → Skipped); definition freshness is fail-closed (a clamav verdict without a fresh parseable VERSION line never settles).

canopy-reference

Shared enum types and reference data (all Serialize/Deserialize with string representation).

  • DeterminationStatus — 10 variants (Approved, Denied, PendingVerification, PendingAppeal, Withdrawn, Terminated, Sanctioned, TimeLimitExceeded, AbawdExceeded, Disqualified).

  • IncomeType — 16 variants (Employment, Wages, SelfEmployment, SSI, SocialSecurity, Veterans, Tanf, ChildSupport, …).

  • AssetType — 10 variants (Checking, Savings, Vehicle, RealEstate, IdaAccount, …).

  • VerificationSource — 10 variants (GeorgiaDolSwr, GeorgiaDolUi, SsaSdx, SsaBendex, CollateralContact, …).

  • NoticeType — 15 variants (approval, denial, termination, change, pending, ABAWD, expedited, expungement, sanction, continued-benefits, …).

  • FederalProgram — 7 variants (Snap, Tanf, Medicaid, Chip, Caps, WicPc, CcdfAcf801).

  • Types: VerificationItem, VerificationRequirement, Determination (used by canopy-eligibility combined results), DenialReasonCode (typed, with Other(String) preserving the ADR-011 source of truth for unknown codes).

canopy-signing

ECDSA P-256 JWS determination signing (ADR-002).

  • SigningKey::from_pem(pem, key_id) — loads a PKCS#8 private key; sign_detached(payload) → String returns base64url JWS.

  • VerifyingKey::from_pem(pem, key_id) — loads an SPKI public key; verify(payload, signature) → bool.

  • VerifyingKeyRegistry — multi-key registry for zero-downtime rotation (add_key(key, RotationState::Current/Previous); tries current first, falls back to previous).

  • DeterminationSigner trait — fn sign(&self, payload: &[u8]) → Result<String>, implemented by each program service’s EcdsaSigner.

Generate keys: cargo xtask gen-signing-keys --program snap.

canopy-typst

PDF rendering via Typst (ADR-010).

  • RenderEngine::new(notices_root, fonts_dir) — spawns a dedicated OS thread for Typst compilation (synchronous on that thread; async wrapper for callers).

  • RenderEngine::render(program, template_key, context) → RenderedNotice — the NOA path: resolves the template via the manifest, compiles the .typ source, returns PDF bytes + metadata.

  • RenderEngine::render_document(relative_path, &serde_json::Value) → RenderedDocument — the general document path (ADR-029): renders any template file under the notices root from free-form JSON inputs, bypassing the NOA manifest + NoticeContext (path is ../absolute-validated). Used by canopy-notices' general signed-document endpoint (audit citations etc.).

  • NoticeManifest::load(path) — loads manifest.toml, mapping template keys to versioned .typ files with form numbers.

  • NoticeContext — converts serde_json::Value to a Typst Dict for template binding.

Templates live in rulesets/{jurisdiction}/notices/ (shared components in components/; non-NOA templates such as audit/citation.typ live alongside the program folders).

canopy-rules-client

HTTP client for the canopy-rules zen-engine API.

  • RulesClient::new(base_url).

  • evaluate(ruleset_name, context_type, context_id, input) → serde_json::Value — evaluates a JDM ruleset (ruleset_name follows {jurisdiction}-{program}-{name}); returns the decision-table output JSON.

See Rulesets for the JDM format + authoring guide.

canopy-persons-client

HTTP client for canopy-persons — one shared definition of the read/write surface every service caller needs (ADR-001 / ADR-019). Every call carries the ADR-019 service token (fetched per call); a 404 on a read resolves to Ok(None).

  • PersonsClient::new(http, base_url, service_token).

  • Reads: get_person(id, as_of), household_for_person(id, as_of), get_household_full(id, as_of).

  • Writes (create/claim): create_person / create_household / claim_member / claim_income / claim_asset / claim_expense — each takes an Option<&FinalizeStep> that tags the write for idempotent finalization (ADR-038); None is an ordinary write.

  • Finalize control surface (applications-only, ADR-038): register_finalize(op, gen), release_finalize(op, gen), cancel_finalize(op, gen), get_finalize_operation(op) — drive an operation’s lifecycle around the tagged writes.

  • finalize::StepKey — the per-step receipt-key grammar (person(i) / household() / member(i) / income(j) / asset(k) / expense(m), Display + FromStr); it is both the on-wire step key and the applications-side local step-cache key. finalize::FinalizeStep { operation_id, generation, step_key } is the tag threaded into the writes above.

canopy-test-lib

Test utilities for integration + E2E tests.

  • TestClient::new(base_url) — no auth (for 401 tests); TestClient::authenticated(base_url) — auto-acquires a Keycloak JWT for jane.doe (caseworker); TestClient::with_token(token).

  • acquire_token_for(username, password); infrastructure_available() — returns false gracefully in local dev, panics in CI (CANOPY_CI=true) so CI never silently skips.

  • TestResponseassert_status(code), json::<T>(), text().

chaos helpers (canopy_test_lib::chaos)

Cross-process chaos observability harness — in-process production fixtures under EvilLayer (#480 + ADR-020). Solves the thread-local-subscriber constraint (SpanCapture::install_scoped uses a thread-local default subscriber, so events in devstack containers are invisible) by spawning the production component IN the test process.

  • spawn_jwks_provider_for_chaos(EvilLayer) → ChaosJwksHandle — static-document JWKS mock wrapped by the supplied EvilLayer; tests drive handle.provider.refresh().await for deterministic timing.

  • spawn_outbox_drainer_for_chaos(pool, broker_url) → Result<ChaosOutboxHandle, anyhow::Error> — spawns the drainer against a REAL devstack broker URL (invalid URLs return Err before the drainer exists).

Thread-local constraint: chaos tests using these helpers MUST use #[tokio::test(flavor = "current_thread")]. Adding a new contract: see the chaos runbook.

Devstack test users (Keycloak): jane.doe / password (caseworker), bob.smith / password (caseworker), admin / password (admin). Per-program intake fixtures jane.snap-worker / jane.tanf-worker carry primary_programs claims.

Edit this page · default