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; implementsIntoResponsewith 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 theCANOPY_{SERVICE}__prefix via theconfigcrate.ServiceSettings::load(prefix). -
age_years(dob, as_of)— calendar-aware age (notnum_days() / 365). -
encrypt_field()/decrypt_field()— AES-256-GCM field-level encryption. Key fromCANOPY_ENCRYPTION_KEY(base64, 32 bytes); 12-byte random nonce prepended to the ciphertext. -
http::data_path_client()— the single blessedreqwest::Clientbuilder 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). Barereqwest::Client::new()is banned in production by thecargo xtask http-clients audit-client-newratchet gate.
canopy-auth
Keycloak OIDC JWT validation and RBAC (ADR-019).
-
Claims— extracted from the JWTrealm_access.roles. Role gates:require_caseworker_or_above(),require_eligibility_specialist_or_above(),require_supervisor_or_above(),require_admin()(returnApiError::Forbiddenon failure). Carriesprimary_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; extractsClaimsinto request extensions. Rejects (401) any request carrying the retiredX-Canopy-Actorheader (#1443); verifiesX-Canopy-Applicantownership claims when anApplicantClaimVerifieris wired (#1442). -
JwksProvider— fetches + caches JWKS from Keycloak; auto-refreshes hourly; forced refresh on an unknownkidwith 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 ownclient_credentialsJWT (ADR-019);current()returns it, minting + refreshing 5 min before expiry.with_self_validation(provider, max_age)(ADR-037) makescurrent()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 incanopy-apibootstrap (every service that goes through it).with_scopes(#1440) adds RFC 6749scopevalues to the mint — how the portal’s per-target narrow sources activate theiraud-canopy-<target>client scopes; canopy-portal builds EIGHT per-target sources (one per backend target, each self-validating against its own target audience) viaPortalTokenSources(#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) orSystem(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 neitherDebug,Display, norSerialize— logging it is a compile error; the only accessor isexpose_for_exchange(). Populated by `AuthLayer’s middleware only for validated non-service bearers. -
policy— receiver-contract primitives (OIDC F2):require_exact_audience(the token’saudmust be exactly the target — kills any-match for opted-in routes),require_azp_allowlisted,require_realm_role(any-of; empty inputs fail closed). All returnPolicyDenial, which converts only to a coded403 Forbidden(aud_not_exact/azp_not_allowlisted/required_role_missing) — the ADR-043 frozen 401/403 contract encoded in the type. -
ChainAuditSink— the productionExchangeAuditSink(OIDC A1, #1424): every exchange outcome becomes anauth.token_exchangerow 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 exchangejtiserver-side), per-attempt budget inside the broker’s 5sAUDIT_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:subpreserved,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-closedExchangeAuditSinkboundary (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.ExchangedTokenis non-Debug/Display/SerializelikeSubjectBearer. canopy-test-lib gainsacquire_exchanged_token(returnsNonepre-R1, same as devstack-down). -
CanopyRequestBuilderExt— outboundreqwest::RequestBuilderdecorators:with_service_identity(token)(the ADR-019 service bearer) andwith_finalize_step(Option<FinalizeStepHeaders>)(the threex-canopy-finalize-*headers). The finalize header names —FINALIZE_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 (
ShardCount1..=32767,EventSeq/HeadSeq, UUIDv7-checkedChainInstanceId, const-2HashFormulaVersion,EventHash([u8; 32]),ChainFamily/ChainSourceclosed 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— wrapssqlx::PgPool.DbPool::connect_with(url, opts);db.inner()returns&PgPoolfor queries. -
DbPoolOpts—max_connections(default 10),idle_timeout(default 600s). -
validate_database_name(url, service_name)— warns if theDATABASE_URLdatabase name doesn’t match the service name (ADR-001 program isolation). -
advisory::run_with_advisory_lock(pool, lock_name, f)— tick-level leader election onpg_try_advisory_xact_lock(lock id = first 8 bytes of SHA-256 of the name); returnsSchedulerOutcome::{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 NOTHINGelection on the service-localscheduler_runstable 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 bycargo 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 fleetidempotency_keysDDL (create + the ADR-016 expand migration), advisory-lock-serialised and idempotent. Owned by the migration path — called fromcanopy_api::bootstrap’s `apply_migrations_on(in-processMigrationMode::Run) and fromcargo xtask migrate apply(JobOwned/SKIP_MIGRATIONS) — never by the runtime:IdempotencyCache::with_poolonly verifies the table exists, so a least-privilege runtime role needs noCREATE(ADR-004 A8b). Deliberately raw DDL, notsqlx::migrate!— a second migrator would fight each service’s own_sqlx_migrationstracker. -
lease::spawn_heartbeat(period, renew)(ADR-038, epic &71 MR4) — the shared holder-side row-lease renewal loop: calls the caller’s lease-fenced extendUPDATEevery 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-dropHeartbeatGuardwith an advisorylease_lost(); the authoritative fence stays the caller’s own fenced terminalUPDATE.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 W3Ctrace_context. -
Publisher— publishes to thecanopy.eventstopic exchange. Validates the payload before publish — rejects 35 restricted field names (SSN, FTI, IEVS, HIPAA) per ADR-004. Uses the persistentevent_outbox(ADR-018):publish_tx(tx, envelope)stages the row in the caller’s transaction; the backgroundOutboxDrainerflushes it later. -
Event-hold (ADR-039):
publish_tx_held(tx, envelope, EventHold { operation_id, generation })stages an event held (atomic with the domain write, likepublish_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 returnsErr(ParkEvent::…)— the delivery is durably parked inevent_inbox(broker acked; handler tx rolled back) and the per-subscriber unpark scanner (CANOPY_MQ_UNPARK_INTERVAL_SECS, default 60s; on-demand viarun_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 carryschema_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 durablecanopy.unroutedcapture 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 aFOR UPDATE SKIP LOCKEDCTE (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 byclaimed_by. Crashed-drainer recovery viaCANOPY_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>. ConstantEVENTS_EXCHANGE = "canopy.events".
Drainer env vars (defaults work for production):
| Env var | Default | Purpose |
|---|---|---|
|
250 |
Sleep between drain ticks. |
|
100 |
Max rows claimed per tick. |
|
60 |
How long a claim survives before reclaim. Boot assert: |
|
32 |
Max in-flight publishes before awaiting confirms within a batch. |
|
30 |
Deadline for one batch’s whole Phase-2 publish+confirm exchange (#1061). Boot asserts: |
|
7 |
Janitor sweep threshold for published rows. |
|
7200 |
Held-row watch warn threshold for the oldest ADR-039 held row (#1061). |
|
|
Source for |
|
32 |
Per-consumer AMQP prefetch ( |
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 suppliedsqlx::migrate::Migrator, connect RabbitMQ, fetch JWKS, create publisher/subscriber. Callers passsqlx::migrate!("./migrations")at their site so the path resolves relative to their crate; bootstrap runs the migrator before theOutboxDrainerspawns (centralising this prevents the #471 drift — #473). -
shutdown_signal()— graceful SIGTERM/SIGINT (Unix) or Ctrl+C (Windows). -
ServerOptions—cors_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 theirlimitparam throughclamp_limit(absent → 50, forced into1..=200), and the canopy-reporting universe drains request pages atMAX_LIMIT. A per-endpoint deviation needs a justification comment at the deviating site — today the three/v1/overpaymentshandlers (default 200 / max 500, the §B2 larger-page-for-throughput override), each carrying a const assert that its cap stays ≥MAX_LIMITso 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 viawith_max_attempts/with_overall_timeout/with_per_attempt_timeout/with_initial_backoff/with_max_backoff/with_jitter(all assert invariants at construction).with_overall_timeoutis a hard wall-clock cap (#572): the whole retry body is wrapped in onetokio::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_timeoutsets 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. ConstructorsRetryRequest::{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)— runsmake_req(builds a freshRequestBuilderper 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 theobject_storecrate.put(path, bytes),get(path),delete(path),list(prefix). -
ObjectStoreConfig— fromCANOPY_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_validatedand other inline-scanning callers); the applications quarantine lifecycle (ADR-042) scans asynchronously instead. -
Scannertrait +ScanReport/ScanResult/ScanError— the pluggable AV seam;NoopScannerships 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, withOther(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) → Stringreturns 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). -
DeterminationSignertrait —fn sign(&self, payload: &[u8]) → Result<String>, implemented by each program service’sEcdsaSigner.
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.typsource, 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)— loadsmanifest.toml, mapping template keys to versioned.typfiles with form numbers. -
NoticeContext— convertsserde_json::Valueto a TypstDictfor 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_namefollows{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 anOption<&FinalizeStep>that tags the write for idempotent finalization (ADR-038);Noneis 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 forjane.doe(caseworker);TestClient::with_token(token). -
acquire_token_for(username, password);infrastructure_available()— returnsfalsegracefully in local dev, panics in CI (CANOPY_CI=true) so CI never silently skips. -
TestResponse—assert_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 suppliedEvilLayer; tests drivehandle.provider.refresh().awaitfor 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 returnErrbefore 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.