agent_sdk_store_file/
content.rs1use std::path::PathBuf;
2
3use agent_sdk_core::{
4 ContentResolutionError, ContentResolutionErrorKind, ContentResolutionPolicy,
5 ContentResolveRequest, ContentResolver, ContentStore, ResolvedContent,
6 content::{ContentRef, ResolvedContent as CoreResolvedContent},
7};
8
9use crate::util::{
10 content_error, read_bytes, read_json, root_join, safe_segment, sha256_hex, write_bytes,
11 write_json,
12};
13
14#[derive(Clone, Debug)]
15pub struct FileContentStore {
17 root: PathBuf,
18}
19
20#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
21struct ContentMetadata {
22 content_ref: ContentRef,
23 byte_len: u64,
24}
25
26impl FileContentStore {
27 pub fn new(root: impl Into<PathBuf>) -> Self {
29 Self { root: root.into() }
30 }
31
32 fn content_dir(&self, content_ref: &ContentRef) -> PathBuf {
33 root_join(
34 &self.root,
35 &[
36 "content".to_string(),
37 safe_segment(content_ref.content_id.as_str()),
38 ],
39 )
40 }
41
42 fn metadata_path(&self, content_ref: &ContentRef) -> PathBuf {
43 self.content_dir(content_ref).join("metadata.json")
44 }
45
46 fn bytes_path(&self, content_ref: &ContentRef) -> PathBuf {
47 self.content_dir(content_ref).join("content.bin")
48 }
49}
50
51impl ContentResolver for FileContentStore {
52 fn resolve(
53 &self,
54 request: ContentResolveRequest,
55 policy: ContentResolutionPolicy,
56 ) -> Result<ResolvedContent, ContentResolutionError> {
57 if request.requested_version != request.content_ref.version {
58 return Err(content_error(
59 ContentResolutionErrorKind::VersionMismatch,
60 request.content_ref,
61 policy.policy_refs,
62 ));
63 }
64 let metadata = read_json::<ContentMetadata>(&self.metadata_path(&request.content_ref))
65 .map_err(|_| {
66 content_error(
67 ContentResolutionErrorKind::StorageUnavailable,
68 request.content_ref.clone(),
69 policy.policy_refs.clone(),
70 )
71 })?
72 .ok_or_else(|| {
73 content_error(
74 ContentResolutionErrorKind::Missing,
75 request.content_ref.clone(),
76 policy.policy_refs.clone(),
77 )
78 })?;
79 if !policy.allow_raw_content {
80 return Ok(CoreResolvedContent::redacted(
81 metadata.content_ref,
82 policy.policy_refs,
83 ));
84 }
85 let bytes = read_bytes(&self.bytes_path(&request.content_ref)).map_err(|_| {
86 content_error(
87 ContentResolutionErrorKind::StorageUnavailable,
88 request.content_ref.clone(),
89 policy.policy_refs.clone(),
90 )
91 })?;
92 if bytes.len() as u64 > policy.max_bytes {
93 return Err(content_error(
94 ContentResolutionErrorKind::MaxBytesExceeded,
95 request.content_ref,
96 policy.policy_refs,
97 ));
98 }
99 if policy.require_hash_match {
100 if let Some(expected) = &metadata.content_ref.content_hash {
101 let actual = format!("sha256:{}", sha256_hex(&bytes));
102 if expected != &actual {
103 return Err(content_error(
104 ContentResolutionErrorKind::HashMismatch,
105 request.content_ref,
106 policy.policy_refs,
107 ));
108 }
109 }
110 }
111 Ok(ResolvedContent {
112 mime: metadata.content_ref.mime.clone(),
113 redacted_summary: metadata.content_ref.redacted_summary.clone(),
114 content_ref: metadata.content_ref,
115 bytes: Some(bytes),
116 policy_refs: policy.policy_refs,
117 raw_content_included: true,
118 })
119 }
120
121 fn store_resolved_content(
122 &self,
123 content_ref: &ContentRef,
124 bytes: Vec<u8>,
125 ) -> Result<(), ContentResolutionError> {
126 self.put_content(content_ref, bytes)
127 }
128}
129
130impl ContentStore for FileContentStore {
131 fn put_content(
132 &self,
133 content_ref: &ContentRef,
134 bytes: Vec<u8>,
135 ) -> Result<(), ContentResolutionError> {
136 let metadata = ContentMetadata {
137 content_ref: content_ref.clone(),
138 byte_len: bytes.len() as u64,
139 };
140 write_bytes(&self.bytes_path(content_ref), &bytes).map_err(|_| {
141 content_error(
142 ContentResolutionErrorKind::StorageUnavailable,
143 content_ref.clone(),
144 Vec::new(),
145 )
146 })?;
147 write_json(&self.metadata_path(content_ref), &metadata).map_err(|_| {
148 content_error(
149 ContentResolutionErrorKind::StorageUnavailable,
150 content_ref.clone(),
151 Vec::new(),
152 )
153 })
154 }
155}