Conversation
Extract the persistence contracts from praxis-ai-apis into a new publish=false praxis-ai-store crate with no sqlx/TLS in its dependency graph: the StateOwner identity (from_trusted_parts stays the only constructor, so the crate cannot mint a request-forged owner), the record types and StoreError, the ResponseStore and ConversationItemStore traits, and a PersistedStateBackend supertrait with a blanket impl over any type implementing both halves. Part of praxis-proxy#1258 (epic praxis-proxy#1257). The SQL backends adopt these in a follow-up. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…t suite StoreRegistry holds Arc<dyn PersistedStateBackend> so a resolved backend provably implements both trait halves; a Conversations-requiring caller can never receive a response-only backend. OwnerScopedStore binds every request-driven access to one validated owner. The registry stays transport-free (no praxis_filter); the pipeline binding lives in the transport layer. InMemoryStore is a deterministic double for service unit tests: a single mutex makes the multi-step operations (all-or-nothing approval consumption, compare-and-swap, create/delete-items-and-sync) atomic, as the SQL backends achieve with a transaction. The test-support contract_tests suite is the shared conformance harness the in-memory and SQL backends run against. Part of praxis-proxy#1258 (epic praxis-proxy#1257). Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…fter the response The transport layer (apis) validates an individual owner component as it parses trusted headers, before it has all three parts for from_trusted_parts, so validate_component is now public. The contract suite writes the owning response before recording its pending approvals, so a backend with a foreign key from approvals to responses accepts them. Part of praxis-proxy#1258 (epic praxis-proxy#1257). Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Depend on praxis-ai-store and re-export its traits, records, owner, and errors from crate::store and crate root, so existing paths keep compiling. Remove the moved trait_def and types modules; split StateOwner out of state_owner.rs (the transport filter and projection stay). The SQL backends implement the moved traits and the ResponseStoreRegistry becomes a transport-bound wrapper over the SQL-free StoreRegistry, holding a combined Arc<dyn PersistedStateBackend> so a resolved backend always has both halves; the PipelineExtension binding lives here, keeping the store crate transport-free. Prove adoption by running the shared contract suite against the SQLite backend. The conversations-filter store path is unchanged (deferred to praxis-proxy#1262). Part of praxis-proxy#1258 (epic praxis-proxy#1257). Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…d test-support Nothing in non-test src uses tokio (InMemoryStore is std::sync::Mutex; the tokio::test macros are covered by the dev-dependency), so remove it from the normal dependencies to keep tokio out of every downstream consumer's non-dev closure. Gate pub mod memory behind test-support like contract_tests, so the test double is not built or exported in the default build. Part of praxis-proxy#1258 (epic praxis-proxy#1257). Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…i-store The registry that used DashMap moved into praxis-ai-store, so apis no longer uses dashmap. Remove it from the dependencies and the store feature; cargo machete flagged it as unused. Part of praxis-proxy#1258 (epic praxis-proxy#1257). Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
The test target ran cargo test --workspace with default features (store-postgres only), so sqlite-backed tests in praxis-test-utils constructed a sqlite ResponseStore against a backend that was never compiled and failed at runtime with backend 'sqlite' is unavailable. Add STORE_ALL_WORKSPACE_FEATURES (already used by coverage-check) so the store crates build with store-all across the workspace run; every crate still runs, none is dropped. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Add StoreBackendFactory (backend_id, effective_key, build), BackendError with a distinct backend-unavailable variant (and a transient variant for bounded retry), a StoreCapability descriptor, an EffectiveConfigKey dedup key that redacts its Debug, and a RetireBackend hook plus ProvisionedBackend. SQL-free and crypto-free: concrete factories own their config type and any SQL/TLS. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…e crate New publish=false praxis-ai-store-lifecycle crate depending only on the praxis-ai-store interface (crypto-free, no store-backends). It routes inline config to injected StoreBackendFactory implementations, builds and validates backends eagerly at pipeline construction (SQLite permanent-init fails the build; a transient failure retries within a bounded budget then fails as unavailable), deduplicates identical effective configs onto one pooled backend, and reuses or retires backends across a reload via a process-wide refcounted cache with explicit retirement. Nine unit tests cover the six behaviors: initial load, reload reuse, retire on last release and on changed config, unavailable-at-build, transient-then-build, and single-pool dedup. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Wrap the existing SQLite backend as a StoreBackendFactory the lifecycle layer provisions: it parses the inline config, computes the effective dedup key, classifies a SQLite init failure as permanent (BackendError::Unavailable so the build fails closed), and closes the pool on retirement via a new close() on the SQL stores. Add a redaction helper that scrubs the connection string and any embedded credentials from an error before it surfaces. Five end-to-end tests bind the SQLite-applicable behaviors against a real file-backed SQLite backend (initial load, reload reuse, changed-config second backend, unavailable-at-build fails closed distinct from unknown-backend, single-pool dedup). Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Mirror the SQLite factory for Postgres: parse inline config (incl. ssl_mode, ssl_root_cert, allow_private_database_url), re-run revalidate_postgres_host on every build attempt (guards DNS rebinding), build over PgTlsConfig, and close the pool on retirement. A connect failure is classified BackendError::Transient so the lifecycle cache retries it within budget, preserving the tested Postgres-transient vs SQLite-permanent semantics. Redaction and the pool fingerprint are shared across both factories. No runnable Postgres here, so the transient-then-build behavior remains machinery-covered (9 lifecycle tests). Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…esolution Add resolve_pipelines_with_stores, which attaches a caller-provided ResponseStoreRegistry per listener. resolve_pipelines stays a thin wrapper delegating with empty registries, so the validation, CLI, and test paths are unchanged. The serve and reload paths provision backends and pass populated registries here. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Add StoreBackendFactory::validate_config (default over effective_key, no I/O) and BackendCache::validate, a synchronous pass that fails a malformed or unknown-backend store config at pipeline construction rather than at first traffic. Pool creation stays lazy on the serving runtime: sqlx pools bind to the runtime that opens them and the server owns that runtime, unreachable at sync construction, so eager here is eager config validation. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…e factory injection The Postgres backend factory now models ssl_client_cert, ssl_client_key, and require_certificate_authentication, matching the response-store filter config, so factory validation and build cover client-cert mutual TLS and no longer reject a config the filter accepts. Add store_backend_factories() so a binary can inject the compiled factories into the lifecycle cache. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…ifecycle state Run the filter's TLS/auth validation from the factory (connect and validate_config), so a client cert under a non-verifying ssl_mode and the certificate-authentication compliance profile fail closed on this path as they already do in the filter. Key the Postgres dedup on the cert-path values rather than their presence, so distinct trust anchors or client identities at different paths do not collide onto one pool. Drop the unused StoreCapability descriptor and StoreRef.capability field (no runtime reads them), privatize redact_connection_error, correct the build-retry pacing comment, and fix the cert-field docs to say path, not PEM material. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…ime background service Install an empty per-listener ResponseStoreRegistry into each pipeline via a From<StoreRegistry> bridge, and provision the configured backends into that same shared map from a Pingora background service that runs on the serving runtime. sqlx pools bind to the runtime that opens them, so provisioning must run there rather than on the config-watcher runtime a reload uses. BackendCache gains provision_into so a caller can populate a shared registry handed to it before the serving runtime exists. Store configuration is validated eagerly at startup (fatal), so a malformed or unknown-backend config fails before serving; connectivity surfaces when the pools open. Reload reuses the serving-runtime-provisioned registries: the reloaded pipeline keeps the live backends, and a changed store config is restart-required rather than re-provisioned on the watcher runtime. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…ution The response-store filter no longer builds or caches its own backend. It resolves an owner-scoped handle from the per-request ResponseStoreRegistry via get_scoped(DEFAULT_STORE_NAME, owner), mirroring the rehydrate filter, so the tenant-isolation seam has one public request-path read. The OnceCell, the build/init/registration helpers, and the StorageBackend match are removed; the filter is a stateless unit struct that validates config at construction and rejects with 500 when a request that needs the store finds none provisioned. OwnerScopedStore gains an owner-checked persist_response_with_pending_approvals so the write path stays inside the facade. Permanent-vs-transient init policy now lives in the factory's BackendError classification and the lifecycle cache, not the filter. Connection-error redaction is threaded into the Postgres backend's runtime StoreError::Database sites: the store carries its URL and redacts it (and any embedded credentials) through a shared helper, matching the connect-time redaction the provisioning factories apply. SQLite URLs carry no credentials. The filter test suite is rewritten to the provisioning model: tests install a store into the context registry (or seed one) rather than the filter's OnceCell, and the obsolete lazy-init / self-registration / permanent-failure tests are dropped since that behavior moved to the factory and cache. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
The integration/recording harness (praxis-test-utils build_pingora_server) built pipelines with an empty ResponseStoreRegistry and never ran the store-provision background service, so after the filter migration a store-filter request through the harness found no provisioned backend and rejected with 500. Reuse the production wiring: build_store_wiring and StoreProvisionService are now public on the praxis_ai crate, and build_pingora_server installs the per-listener registries into the pipelines and registers the background service on the harness's own Pingora server, exactly as boot_server does. No second provisioning path is introduced. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
The provisioning module doc links StoreBackendFactory unqualified, but the trait moved to praxis-ai-store in praxis-proxy#1258 and the module imports only BackendError (cfg-gated). The always-compiled module doc left the link unresolved, failing cargo doc under RUSTDOCFLAGS="-D warnings". Qualify it to praxis_ai_store::StoreBackendFactory. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…ignal The store-provision background service opens backend pools asynchronously after the HTTP listener already accepts, so a store-filter request that arrives during the provisioning window finds no provisioned backend and returns 500. The integration harness only waited for HTTP readiness, so the two Postgres store tests failed deterministically (500 vs 200): a Postgres pool open, and its TLS handshake for the mTLS variant, reliably lose the race against HTTP readiness. Add a StoreReadiness watch owned by StoreProvisionService and a cloneable StoreReadinessHandle observers read or await. Signal Ready only once every configured listener holds a lease. On a provisioning failure, keep the state non-Ready and retry with bounded backoff so a transient database or TLS failure self-heals rather than being logged and abandoned. The harness awaits Ready before the first request. The handle is reusable: the readiness endpoint and sibling workstreams read the same one. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Expose the store readiness signal as an HTTP endpoint so an orchestrator probe gates traffic on store provisioning. It runs on its own listener because the protocol admin service owns its route set, and it composes store readiness with cluster health: ready (200) only when store provisioning is Ready and cluster health is ready, 503 otherwise, so a provisioning failure stops reporting healthy. It reads the same StoreReadinessHandle the provisioning service drives, so one state serves both the endpoint and the harness wait. The listen address comes from PRAXIS_STORE_READINESS_ADDR; enabling it is opt-in via that variable (default value 0.0.0.0:9200, path /ready) so no new port binds unless a deployment asks for it. praxis_core is untouched. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Relocate url_security (cloud-metadata, private-range, NAT64 and special-use classification) from praxis-ai-apis into the SQL-free praxis-ai-store crate so the Postgres URL validator can keep using it after it moves out of apis in the store-backends split. Both the outbound file fetch and callout paths now share the one canonical policy from the leaf. normalize_mapped_ipv4 is inlined rather than pulled from praxis-proxy-core: praxis-proxy-core drags rustls/ring/aws-lc into its dependency graph, and the store leaf must stay crypto-free. Its unit tests move with the module. Part of the store-backends crate split (praxis-proxy#1260). Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Move the concrete SQL backends out of praxis-ai-apis into a new praxis-ai-store-backends crate: the Postgres and Sqlite ResponseStore implementations, the connection pool, TLS and URL validation, schema management, and the StoreBackendFactory provisioning. The new crate depends only on the praxis-ai-store contracts leaf, so the SQL client crypto (sqlx TLS) is compiled in one place and a consumer that omits the crate ships none of it. praxis-ai-apis keeps a transitional dependency on the new crate and re-exports the backends at their original crate::store::* paths, so the conversations and responses filters compile unchanged. That transitional dependency is removed once #1259b and praxis-proxy#1262 take those filters off concrete-store construction, which is also when apis stops pulling sqlx transitively. Part of the store-backends crate split (praxis-proxy#1260). Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Move the Responses persistence business logic out of the request-path filter into an in-crate service layer (crate::service::responses). ResponsesService is built from an owner-scoped store handle resolved from the registry; it owns record assembly, input-item listing, CRUD, and pending-approval coordination, constructs no backend, and holds no pool. The store, compact, and mcp_dispatch filters route persistence through it, leaving the transport free of business logic. The service runs against the in-memory backend with no pipeline and no database. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
The OwnerScopedStore facade exposed only the response surface plus a read-only get_conversation. Add the owner-scoped conversation-item operations the conversations service needs: upsert_conversation, update_conversation_metadata, delete_conversation, create_items_and_sync_messages, get_existing_conversation_item_ids, list_conversation_items, get_conversation_item, and delete_item_and_sync_messages. Every read binds the handle's owner; the two record-taking writes reject a record built under a different owner through require_matching_owner, so a request cannot broaden its owner scope. The owner stays server-set via get_scoped; no public owner-scoped constructor is added. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
…tion The conversations filter constructed its own SQL store through an OnceCell, keeping durable state on the request path and pinning the concrete backend into apis. Move it onto the same per-listener registry the response-store filter uses: the filter resolves an owner-scoped handle via get_scoped and holds nothing. - OpenaiConversationsFilter becomes a unit struct. The OnceCell, build_store, get_or_init_store, require_store, with_store_for_test, and the StorageBackend construction path are removed; from_config still validates fatally at pipeline construction. - Handlers take an OwnerScopedStore instead of a raw ConversationItemStore plus an owner; the owner is server-set into the handle and read back via owner(), so a handler cannot pass a mismatched owner and the record-taking writes are re-checked by require_matching_owner. - The serving-runtime provisioner scans openai_conversations too and registers its backend under a distinct conversations store name; the lifecycle cache shares one backend with the response store when their effective configs match. - reload warns on a conversations store config change as it already does for the response store. - Tests drive the stateless filter against a shared in-memory backend installed through the registry; the foreign-owner append-back test keeps the SQL backend to assert its fail-closed semantics. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
Mirror the responses service split: move the conversation and item business
logic out of the transport into a transport-neutral apis/src/service/conversations
module that operates through an owner-scoped store handle.
- ConversationsService { store: OwnerScopedStore } + new(store), with thin async
delegation for conversation and item CRUD, listing, and metadata updates. The
service holds only the owner-scoped handle, never a raw backend; record-taking
writes stay guarded by the facade's owner check.
- The item-record assembly (build_item_records, normalize_item and family,
duplicate/count checks) moves into the service as transport-neutral functions
returning StoreError; id generation and created_at are threaded in so they run
with no pipeline. The filter and handlers become transport-only, resolving the
service via get_scoped(...).map(ConversationsService::new) and mapping
StoreError to the existing HTTP responses.
- item_schema::validate_output_item and contracts::MAX_ITEMS_PER_REQUEST widen to
pub(crate) so the service can reuse them; net surface stays internal.
- Service unit tests exercise assembly and CRUD against the in-memory backend
with no pipeline or database, including owner-forgery rejection and cross-owner
isolation.
Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
built on #1287
Summary
Extract the Conversations persistence service out of the filter and migrate the conversations filter to registry-only store resolution, mirroring the Responses service split. The conversations filter resolves its store through the owner-scoped registry handle (ConversationItemStore via get_scoped) instead of constructing a backend, and the conversation CRUD/business logic moves into an independently testable service layer. This also removes the transitional apis/src/store re-export bridge that #1260 added (its own PR notes it is removed when #1262 lands): the conversations filter was the last consumer of the concrete-store construction path, so the bridge goes with the migration. Part of the store-layer split (#1257); builds on the contracts crate (#1258), the lifecycle/registry (#1259), the SQL-backends crate (#1260), and the Responses service (#1261).
Related issue
Closes #1262
Part of #1257.
Validation
make doc and make lint are green. All test suites pass (unit, store-features, integration). make coverage-check reports below the 95% line threshold, but the entire gap is the inherited exclusion-regex issue from #1260: the coverage exclusion still names store/postgres.rs and does not match the moved store-backends/src/postgres.rs, so the moved Postgres backend (which needs a live Postgres to cover) is now scored. With that file excluded (the pre-#1260 intent) coverage is 95.65% and passes. No conversations test fails. The regex is a one-line Makefile fix owned by #1260.
Checklist
Breaking changes
No public API or runtime behavior change to the Conversations API. The conversations filter no longer constructs its own SQL store; the transitional apis/src/store re-export bridge added in #1260 is removed now that the last concrete-store consumer is migrated. The conversation service logic is now testable against an in-memory backend with no pipeline or database.