Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 121 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ 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"
authors = ["Shawn Hartsock <hartsock@users.noreply.github.com>"]
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"
5 changes: 5 additions & 0 deletions crates/agent-store/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions crates/agent-store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ mod doorbell;
mod error;
mod fingerprint;
mod generation;
mod policy;
mod writer_log;

pub use backend::{Backend, Dialect, Row, SqliteBackend, Value};
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};
72 changes: 72 additions & 0 deletions crates/agent-store/src/policy.rs
Original file line number Diff line number Diff line change
@@ -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::<StorePolicy>(r#"backend = "mongo""#).is_err());
}
}
Loading