diff --git a/Cargo.lock b/Cargo.lock index a74ad26..adba413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,12 +4,14 @@ version = 4 [[package]] name = "agent-store" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "blake3", "rusqlite", + "serde", "thiserror", + "toml", ] [[package]] @@ -93,6 +95,12 @@ dependencies = [ "libc", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -120,13 +128,29 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "hashlink" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", ] [[package]] @@ -146,6 +170,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + [[package]] name = "once_cell" version = "1.21.4" @@ -190,6 +220,45 @@ dependencies = [ "smallvec", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "2.0.1" @@ -233,6 +302,47 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -251,6 +361,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "zerocopy" version = "0.8.52" diff --git a/Cargo.toml b/Cargo.toml index 3354ff7..07273e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,10 @@ resolver = "2" members = ["crates/agent-store"] # 0.1.x semver line (Shawn, 2026-06-13): 0.1.0 published; 0.1.1 adds the -# incremental-adoption seam (SqliteBackend::from_connection) for consumers. +# incremental-adoption seam (SqliteBackend::from_connection); 0.1.2 adds the +# declarative StorePolicy (config-selected backend). [workspace.package] -version = "0.1.1" +version = "0.1.2" edition = "2021" rust-version = "1.85" license = "MIT OR Apache-2.0" @@ -13,9 +14,11 @@ authors = ["Shawn Hartsock "] repository = "https://github.com/Gilamonster-Foundation/agent-store" [workspace.dependencies] -agent-store = { path = "crates/agent-store", version = "=0.1.1" } +agent-store = { path = "crates/agent-store", version = "=0.1.2" } anyhow = "1.0" blake3 = "1.8" rusqlite = { version = "0.32", features = ["bundled"] } +serde = { version = "1.0", features = ["derive"] } thiserror = "2.0" +toml = "0.8" diff --git a/crates/agent-store/Cargo.toml b/crates/agent-store/Cargo.toml index d940ef2..4c1763e 100644 --- a/crates/agent-store/Cargo.toml +++ b/crates/agent-store/Cargo.toml @@ -21,4 +21,9 @@ pg = [] anyhow.workspace = true blake3.workspace = true rusqlite.workspace = true +serde.workspace = true thiserror.workspace = true + +[dev-dependencies] +# Exercises StorePolicy deserialization in the real config format. +toml.workspace = true diff --git a/crates/agent-store/src/lib.rs b/crates/agent-store/src/lib.rs index 7da5fa8..2f65700 100644 --- a/crates/agent-store/src/lib.rs +++ b/crates/agent-store/src/lib.rs @@ -60,6 +60,7 @@ mod doorbell; mod error; mod fingerprint; mod generation; +mod policy; mod writer_log; pub use backend::{Backend, Dialect, Row, SqliteBackend, Value}; @@ -67,4 +68,5 @@ pub use doorbell::{CommitEvent, Doorbell}; pub use error::{Result, StoreError}; pub use fingerprint::Fingerprint; pub use generation::Generation; +pub use policy::{BackendKind, StorePolicy}; pub use writer_log::{Entry, WriterLog}; diff --git a/crates/agent-store/src/policy.rs b/crates/agent-store/src/policy.rs new file mode 100644 index 0000000..4a914a2 --- /dev/null +++ b/crates/agent-store/src/policy.rs @@ -0,0 +1,72 @@ +//! Declarative storage policy. +//! +//! The backend a consumer opens is a **reviewed configuration choice**, never a +//! hardcoded commitment in code. [`StorePolicy`] is that seam: its [`Default`] +//! is today's behavior — a local SQLite file, no daemon — so adopting a policy +//! changes nothing until a value is deliberately set, and changing direction +//! (to Postgres, later) is a config edit rather than a rewrite. +//! +//! The policy is **flat by design**: one reviewable knob per line, embedded in +//! a consumer's existing config (`[store] backend = "sqlite"`). This crate owns +//! only the vocabulary — *resolution* lives in each consumer, because how a +//! backend opens (pragmas, connection ownership, domain-specific SQL) is +//! consumer-specific. + +use serde::{Deserialize, Serialize}; + +/// Which storage backend a [`StorePolicy`] selects. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum BackendKind { + /// Bundled SQLite — zero system deps, no daemon. The fleet default. + #[default] + Sqlite, + /// PostgreSQL — opt-in, only where an operator already runs a server. + /// Reserved: consumers reject it until their Postgres path is wired. + Postgres, +} + +/// A declarative storage policy. +/// +/// Flat and minimal today (one knob); grows new fields — a Postgres URL +/// reference, a coordination doorbell mode — as those features land. The +/// `#[serde(default)]` makes every field optional, so a bare `[store]` section +/// (or none at all) resolves to the safe, daemonless SQLite default. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[serde(default)] +pub struct StorePolicy { + /// Which backend to open. + pub backend: BackendKind, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_is_safe_sqlite() { + // Regression: the default must stay SQLite, so adopting a policy is a + // no-op until a value is deliberately set (the low-risk guarantee). + assert_eq!(StorePolicy::default().backend, BackendKind::Sqlite); + } + + #[test] + fn deserializes_flat_lowercase_knob() { + let p: StorePolicy = toml::from_str(r#"backend = "postgres""#).unwrap(); + assert_eq!(p.backend, BackendKind::Postgres); + + let p: StorePolicy = toml::from_str(r#"backend = "sqlite""#).unwrap(); + assert_eq!(p.backend, BackendKind::Sqlite); + } + + #[test] + fn empty_config_is_the_default() { + let p: StorePolicy = toml::from_str("").unwrap(); + assert_eq!(p.backend, BackendKind::Sqlite); + } + + #[test] + fn unknown_backend_is_an_error() { + assert!(toml::from_str::(r#"backend = "mongo""#).is_err()); + } +}