use serde_json::{Value, json};
use crate::mcp::param_names;
use crate::offload::{ContextOffloader, OffloadConfig};
fn resolve_namespace(params: &Value) -> String {
params
.get(param_names::NAMESPACE)
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map_or_else(|| "auto".to_string(), str::to_string)
}
pub fn handle_offload(
conn: &rusqlite::Connection,
params: &Value,
agent_id: &str,
) -> Result<Value, String> {
let content = params
.get(param_names::CONTENT)
.and_then(Value::as_str)
.ok_or(crate::errors::msg::CONTENT_REQUIRED)?;
let namespace = resolve_namespace(params);
let ttl_seconds = params.get(param_names::TTL_SECONDS).and_then(Value::as_u64);
let off = ContextOffloader::new(conn, None, OffloadConfig::default());
let result = off
.offload(content, &namespace, ttl_seconds, agent_id)
.map_err(|e| e.to_string())?;
Ok(json!({
"ref_id": result.ref_id,
(crate::models::field_names::CONTENT_SHA256): result.content_sha256,
"stored_at": result.stored_at,
"namespace": namespace,
}))
}
pub fn handle_deref(
conn: &rusqlite::Connection,
params: &Value,
agent_id: &str,
) -> Result<Value, String> {
let ref_id = params
.get("ref_id")
.and_then(Value::as_str)
.ok_or("ref_id is required")?;
let off = ContextOffloader::new(conn, None, OffloadConfig::default());
let result = off
.deref(ref_id, Some(agent_id))
.map_err(|e| e.to_string())?;
Ok(json!({
"ref_id": ref_id,
"content": result.content,
"stored_at": result.stored_at,
"sha256": result.sha256,
}))
}
use crate::mcp::registry::McpTool;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct OffloadRequest {
pub content: String,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub ttl_seconds: Option<i64>,
}
#[allow(dead_code)]
pub struct OffloadTool;
impl McpTool for OffloadTool {
fn name() -> &'static str {
crate::mcp::registry::tool_names::MEMORY_OFFLOAD
}
fn description() -> &'static str {
"Offload verbatim content; returns ref_id (Family::Power)."
}
fn docs() -> &'static str {
"QW-3 follow-up: store verbatim in offloaded_blobs. Returns {ref_id, content_sha256, stored_at}. Dereference via memory_deref. Semantic+ tier."
}
fn input_schema() -> Value {
crate::mcp::registry::input_schema_for::<OffloadRequest>()
}
fn family() -> &'static str {
crate::profile::Family::Power.name()
}
}
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct DerefRequest {
pub ref_id: String,
}
#[allow(dead_code)]
pub struct DerefTool;
impl McpTool for DerefTool {
fn name() -> &'static str {
crate::mcp::registry::tool_names::MEMORY_DEREF
}
fn description() -> &'static str {
"Dereference a memory_offload ref_id (Family::Power)."
}
fn docs() -> &'static str {
"QW-3 follow-up: sha256-verified lookup. Returns {ref_id, content, stored_at, sha256}. Refuses tampered rows. Semantic+ tier."
}
fn input_schema() -> Value {
crate::mcp::registry::input_schema_for::<DerefRequest>()
}
fn family() -> &'static str {
crate::profile::Family::Power.name()
}
}
#[cfg(test)]
mod d1_5_986_tests {
use super::*;
use crate::mcp::parity_test_helpers::{
assert_descriptions_match, assert_property_set_parity, derived_props_for,
};
#[test]
fn offload_parity_986() {
let derived = derived_props_for::<OffloadRequest>();
assert_property_set_parity("memory_offload", &derived);
assert_descriptions_match("memory_offload", &derived);
}
#[test]
fn offload_tool_metadata_986() {
assert_eq!(OffloadTool::name(), "memory_offload");
assert_eq!(OffloadTool::family(), "power");
}
#[test]
fn deref_parity_986() {
let derived = derived_props_for::<DerefRequest>();
assert_property_set_parity("memory_deref", &derived);
assert_descriptions_match("memory_deref", &derived);
}
#[test]
fn deref_tool_metadata_986() {
assert_eq!(DerefTool::name(), "memory_deref");
assert_eq!(DerefTool::family(), "power");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage as db;
use std::path::Path;
fn fresh_conn() -> rusqlite::Connection {
db::open(Path::new(":memory:")).expect("open in-memory db")
}
#[test]
fn handle_offload_requires_content() {
let conn = fresh_conn();
let err = handle_offload(&conn, &json!({}), "ai:alice").unwrap_err();
assert!(err.contains("content"));
}
#[test]
fn handle_deref_requires_ref_id() {
let conn = fresh_conn();
let err = handle_deref(&conn, &json!({}), "ai:alice").unwrap_err();
assert!(err.contains("ref_id"));
}
#[test]
fn handle_offload_then_deref_round_trip() {
let conn = fresh_conn();
let off = handle_offload(
&conn,
&json!({"content": "hello mcp", "namespace": "mcp/test"}),
"ai:alice",
)
.expect("offload");
let ref_id = off["ref_id"].as_str().expect("ref_id").to_string();
let back = handle_deref(&conn, &json!({"ref_id": ref_id}), "ai:alice").expect("deref");
assert_eq!(back["content"].as_str(), Some("hello mcp"));
}
#[test]
fn handle_deref_refuses_cross_agent_caller_mcp_layer() {
let conn = fresh_conn();
let off = handle_offload(
&conn,
&json!({"content": "alice secret", "namespace": "mcp/test"}),
"ai:alice",
)
.expect("offload");
let ref_id = off["ref_id"].as_str().expect("ref_id").to_string();
let err = handle_deref(&conn, &json!({"ref_id": ref_id}), "ai:bob")
.expect_err("cross-agent deref must reject");
assert!(
err.contains("not found"),
"leak-resistant deref error must look like not-found, got: {err}"
);
}
#[test]
fn handle_offload_defaults_namespace_when_omitted() {
let conn = fresh_conn();
let resp = handle_offload(&conn, &json!({"content": "x"}), "ai:alice").expect("ok");
assert_eq!(resp["namespace"].as_str(), Some("auto"));
}
#[test]
fn handle_offload_passes_through_ttl() {
let conn = fresh_conn();
let resp = handle_offload(
&conn,
&json!({"content": "ttl-payload", "ttl_seconds": crate::SECS_PER_HOUR}),
"ai:alice",
)
.expect("ok");
let ref_id = resp["ref_id"].as_str().unwrap();
let ttl: Option<i64> = conn
.query_row(
"SELECT ttl_seconds FROM offloaded_blobs WHERE ref_id = ?1",
rusqlite::params![ref_id],
|r| r.get(0),
)
.unwrap();
assert_eq!(ttl, Some(crate::SECS_PER_HOUR));
}
}