1use std::path::{Path, PathBuf};
2
3use agent_sdk_core::{
4 ContentResolutionError, ContentResolutionErrorKind, ContentResolutionPolicy,
5 ContentResolveRequest, ContentResolver, ContentStore, ResolvedContent,
6 content::{ContentRef, ResolvedContent as CoreResolvedContent},
7};
8use rusqlite::params;
9
10use crate::util::{content_error, decode, encode, open, sha256_hex};
11
12const SCHEMA: &str = "
13CREATE TABLE IF NOT EXISTS content (
14 content_ref TEXT PRIMARY KEY,
15 content_json TEXT NOT NULL,
16 bytes BLOB NOT NULL,
17 byte_len INTEGER NOT NULL
18);
19";
20
21#[derive(Clone, Debug)]
22pub struct SqliteContentStore {
24 path: PathBuf,
25}
26
27impl SqliteContentStore {
28 pub fn open(path: impl AsRef<Path>) -> Result<Self, agent_sdk_core::AgentError> {
30 crate::util::init(path.as_ref(), SCHEMA)?;
31 Ok(Self {
32 path: path.as_ref().to_path_buf(),
33 })
34 }
35}
36
37impl ContentResolver for SqliteContentStore {
38 fn resolve(
39 &self,
40 request: ContentResolveRequest,
41 policy: ContentResolutionPolicy,
42 ) -> Result<ResolvedContent, ContentResolutionError> {
43 if request.requested_version != request.content_ref.version {
44 return Err(content_error(
45 ContentResolutionErrorKind::VersionMismatch,
46 request.content_ref,
47 policy.policy_refs,
48 ));
49 }
50 let connection = open(&self.path).map_err(|_| {
51 content_error(
52 ContentResolutionErrorKind::StorageUnavailable,
53 request.content_ref.clone(),
54 policy.policy_refs.clone(),
55 )
56 })?;
57 let mut statement = connection
58 .prepare("SELECT content_json, bytes FROM content WHERE content_ref = ?1")
59 .map_err(|_| {
60 content_error(
61 ContentResolutionErrorKind::StorageUnavailable,
62 request.content_ref.clone(),
63 policy.policy_refs.clone(),
64 )
65 })?;
66 let row = statement
67 .query_row(params![request.content_ref.content_id.as_str()], |row| {
68 Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
69 })
70 .optional()
71 .map_err(|_| {
72 content_error(
73 ContentResolutionErrorKind::StorageUnavailable,
74 request.content_ref.clone(),
75 policy.policy_refs.clone(),
76 )
77 })?
78 .ok_or_else(|| {
79 content_error(
80 ContentResolutionErrorKind::Missing,
81 request.content_ref.clone(),
82 policy.policy_refs.clone(),
83 )
84 })?;
85 let content_ref: ContentRef = decode(&row.0).map_err(|_| {
86 content_error(
87 ContentResolutionErrorKind::StorageUnavailable,
88 request.content_ref.clone(),
89 policy.policy_refs.clone(),
90 )
91 })?;
92 if !policy.allow_raw_content {
93 return Ok(CoreResolvedContent::redacted(
94 content_ref,
95 policy.policy_refs,
96 ));
97 }
98 if row.1.len() as u64 > policy.max_bytes {
99 return Err(content_error(
100 ContentResolutionErrorKind::MaxBytesExceeded,
101 request.content_ref,
102 policy.policy_refs,
103 ));
104 }
105 if policy.require_hash_match {
106 if let Some(expected) = &content_ref.content_hash {
107 let actual = format!("sha256:{}", sha256_hex(&row.1));
108 if expected != &actual {
109 return Err(content_error(
110 ContentResolutionErrorKind::HashMismatch,
111 request.content_ref,
112 policy.policy_refs,
113 ));
114 }
115 }
116 }
117 Ok(ResolvedContent {
118 mime: content_ref.mime.clone(),
119 redacted_summary: content_ref.redacted_summary.clone(),
120 content_ref,
121 bytes: Some(row.1),
122 policy_refs: policy.policy_refs,
123 raw_content_included: true,
124 })
125 }
126
127 fn store_resolved_content(
128 &self,
129 content_ref: &ContentRef,
130 bytes: Vec<u8>,
131 ) -> Result<(), ContentResolutionError> {
132 self.put_content(content_ref, bytes)
133 }
134}
135
136impl ContentStore for SqliteContentStore {
137 fn put_content(
138 &self,
139 content_ref: &ContentRef,
140 bytes: Vec<u8>,
141 ) -> Result<(), ContentResolutionError> {
142 let connection = open(&self.path).map_err(|_| {
143 content_error(
144 ContentResolutionErrorKind::StorageUnavailable,
145 content_ref.clone(),
146 Vec::new(),
147 )
148 })?;
149 connection
150 .execute(
151 "INSERT OR REPLACE INTO content
152 (content_ref, content_json, bytes, byte_len)
153 VALUES (?1, ?2, ?3, ?4)",
154 params![
155 content_ref.content_id.as_str(),
156 encode(content_ref).map_err(|_| {
157 content_error(
158 ContentResolutionErrorKind::StorageUnavailable,
159 content_ref.clone(),
160 Vec::new(),
161 )
162 })?,
163 bytes,
164 bytes.len() as i64,
165 ],
166 )
167 .map_err(|_| {
168 content_error(
169 ContentResolutionErrorKind::StorageUnavailable,
170 content_ref.clone(),
171 Vec::new(),
172 )
173 })?;
174 Ok(())
175 }
176}
177
178use rusqlite::OptionalExtension;