Skip to main content

handshake_core/memories/
shared.rs

1//! Experimental folder-bound shared memory. Policy and history stay outside the agent inbox.
2//! This is a local serving implementation, not an AMIS wire format or conformance claim.
3mod exchange;
4mod storage;
5
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use thiserror::Error;
11
12#[derive(Debug, Error)]
13pub enum SharedMemoryError {
14    #[error("Enable cross-agent memory consolidation in Settings > Feature flags first.")]
15    FeatureDisabled,
16    #[error("Choose an existing absolute folder path.")]
17    InvalidFolder,
18    #[error("Enable automatic sharing for this folder first.")]
19    Disabled,
20    #[error("Use nonempty memory text without unsupported control characters.")]
21    InvalidText,
22    #[error("Only a Codex or Claude Code durable memory belonging to this folder can be imported.")]
23    InvalidSource,
24    #[error("This memory is no longer available. Refresh the shared library.")]
25    Missing,
26    #[error(transparent)]
27    Io(#[from] std::io::Error),
28    #[error(transparent)]
29    Database(#[from] rusqlite::Error),
30    #[error(transparent)]
31    Memory(#[from] super::MemoryError),
32}
33
34#[derive(Clone, Debug, Deserialize)]
35#[serde(tag = "operation", rename_all = "camelCase", deny_unknown_fields)]
36pub enum SharedOperation {
37    Refresh,
38    SetEnabled { enabled: bool },
39    Add { text: String },
40    Import { source_id: String },
41    Withdraw { id: String },
42    Read { id: String },
43}
44
45#[derive(Debug, Deserialize)]
46#[serde(rename_all = "camelCase", deny_unknown_fields)]
47pub struct SharedRequest {
48    pub data_directory: PathBuf,
49    pub folder: PathBuf,
50    pub action: SharedOperation,
51}
52
53#[derive(Debug, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct SharedSnapshot {
56    pub enabled: bool,
57    pub delivery_ready: bool,
58    pub folder: PathBuf,
59    pub inbox: PathBuf,
60    pub catalog_path: PathBuf,
61    pub notes: Vec<SharedNote>,
62    pub detail: Option<SharedNoteDetail>,
63    pub diagnostics: Vec<String>,
64}
65
66#[derive(Debug, Serialize)]
67#[serde(rename_all = "camelCase")]
68pub struct SharedNote {
69    pub id: String,
70    pub title: String,
71    pub source: String,
72    pub digest: String,
73    pub status: String,
74}
75
76#[derive(Debug, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct SharedNoteDetail {
79    pub id: String,
80    pub digest: String,
81    pub text: String,
82}
83
84/// Applies receiver policy, accepts new inbox files, and rebuilds the serving view.
85///
86/// # Errors
87/// Rejects invalid paths, unsupported sources, disabled writes and unavailable storage.
88pub fn execute(request: &SharedRequest) -> Result<SharedSnapshot, SharedMemoryError> {
89    if !crate::config::Config::load_user()
90        .feature_flags
91        .cross_agent_memory_consolidation
92    {
93        return Err(SharedMemoryError::FeatureDisabled);
94    }
95    if !request.folder.is_absolute() || !request.folder.is_dir() {
96        return Err(SharedMemoryError::InvalidFolder);
97    }
98    let folder = request.folder.canonicalize()?;
99    let root = library_root(&request.data_directory, &folder)?;
100    storage::execute(&root, &folder, &request.action)
101}
102
103fn library_root(data: &Path, folder: &Path) -> Result<PathBuf, SharedMemoryError> {
104    if !data.is_absolute() {
105        return Err(SharedMemoryError::InvalidFolder);
106    }
107    Ok(data
108        .join("shared-memory")
109        .join(digest(folder.as_os_str().as_encoded_bytes())))
110}
111
112fn digest(bytes: &[u8]) -> String {
113    use std::fmt::Write as _;
114    Sha256::digest(bytes)
115        .iter()
116        .fold(String::with_capacity(64), |mut hex, byte| {
117            let _ = write!(hex, "{byte:02x}");
118            hex
119        })
120}
121
122fn import_text(folder: &Path, source_id: &str) -> Result<(String, String), SharedMemoryError> {
123    let memory = super::read(&super::MemoryRequest {
124        folder: Some(folder.into()),
125        source_id: Some(source_id.into()),
126        examples_root: None,
127    })?;
128    let default_claude_group = format!(
129        "Claude folder: {}",
130        super::locations::claude_key(&super::locations::repository_root(folder))
131    );
132    if memory.source.group.starts_with("Claude folder: ")
133        && memory.source.group != default_claude_group
134    {
135        return Err(SharedMemoryError::InvalidSource);
136    }
137    if memory.source.scope != super::MemoryScope::Folder
138        || memory.source.group == "Claude configured memory directory"
139        || !matches!(
140            memory.source.harness,
141            super::MemoryHarness::Codex | super::MemoryHarness::ClaudeCode
142        )
143        || !matches!(
144            memory.source.kind,
145            super::MemoryKind::Durable | super::MemoryKind::AgentMemory
146        )
147        || memory.source.example
148    {
149        return Err(SharedMemoryError::InvalidSource);
150    }
151    Ok((
152        memory.text.ok_or(SharedMemoryError::InvalidSource)?,
153        memory.source.location,
154    ))
155}