use std::io::{self, Write};
use crate::models::{
EXPORT_FORMAT_VERSION, ExportAgentRecord, ExportArtifactRecord, ExportAuditRecord,
ExportFooter, ExportHeader, ExportLinkRecord, ExportMemoryRecord, ExportRecord, ExportScope,
ExportTagRecord, ExportWorkspaceRecord, RedactionLevel,
};
use crate::policy::import_auth::{RecordsRootBuilder, canonical_record_hash};
const SECRET_SUBSTRINGS: &[&str] = &[
"password",
"secret",
"api_key",
"apikey",
"api-key",
"bearer",
"authorization",
"oauth",
"auth_token",
"auth-token",
"auth.token",
"auth_key",
"auth-key",
"auth.key",
"credential",
"private_key",
"privatekey",
"access_key",
"accesskey",
"secret_key",
"secretkey",
"aws_",
"gcp_",
"azure_",
"database_url",
"connection_string",
"-----begin",
"-----end",
];
const SECRET_CONTEXT_KEYS: &[&str] = &["token"];
pub const REDACTED_PLACEHOLDER: &str = "[REDACTED]";
pub const REDACTED_PATH_PLACEHOLDER: &str = "[REDACTED_PATH]";
pub const REDACTED_ID_PLACEHOLDER: &str = "[REDACTED_ID]";
const MIN_HIGH_ENTROPY_TOKEN_BYTES: usize = 32;
const STANDARD_HIGH_ENTROPY_BITS_PER_BYTE: f64 = 4.0;
const STRICT_HIGH_ENTROPY_BITS_PER_BYTE: f64 = 3.5;
const SENSITIVE_PATH_PREFIXES: &[&str] = &[
"/home/",
"/Users/",
"/Volumes/",
"/data/",
"/private/",
"/var/",
"/tmp/",
"/dp/",
"/workspace/",
"/repo/",
"/etc/",
"C:\\",
"D:\\",
];
#[must_use]
pub fn contains_secret_pattern(content: &str) -> bool {
let lower = content.to_lowercase();
SECRET_SUBSTRINGS.iter().any(|pat| lower.contains(pat))
|| SECRET_CONTEXT_KEYS
.iter()
.any(|key| contains_secret_key_with_value(&lower, key))
}
fn contains_export_secret_pattern(content: &str) -> bool {
if contains_secret_pattern(content) {
return true;
}
crate::policy::redact_secret_like_content(content)
.redacted_reasons
.iter()
.any(|reason| export_redaction_reason_is_secret(reason))
}
fn export_redaction_reason_is_secret(reason: &str) -> bool {
!matches!(
reason,
"email_address" | "ssn" | "phone_number" | "high_entropy_secret"
)
}
fn contains_secret_key_with_value(content: &str, key: &str) -> bool {
content
.match_indices(key)
.any(|(index, _)| secret_key_match_has_value(content, key, index))
}
fn secret_key_match_has_value(content: &str, key: &str, index: usize) -> bool {
let before = content[..index].chars().next_back();
if before.is_some_and(is_secret_key_char) {
return false;
}
let value_start = index + key.len();
let mut chars = content[value_start..].chars();
let Some(after_key) = chars.next() else {
return false;
};
if matches!(after_key, '"' | '\'') && before == Some(after_key) {
return quoted_secret_key_has_value(content, value_start + after_key.len_utf8());
}
if is_secret_key_connector(after_key) {
return secret_key_suffix_has_value(content, value_start + after_key.len_utf8());
}
if is_secret_key_char(after_key) {
return false;
}
separator_starts_secret_value(content, value_start + after_key.len_utf8(), after_key)
}
fn quoted_secret_key_has_value(content: &str, mut offset: usize) -> bool {
while offset < content.len() {
let Some(next) = content[offset..].chars().next() else {
return false;
};
if !next.is_whitespace() {
return matches!(next, ':' | '=');
}
offset += next.len_utf8();
}
false
}
fn separator_starts_secret_value(content: &str, mut offset: usize, mut separator: char) -> bool {
if separator.is_whitespace() {
loop {
let Some(next) = content[offset..].chars().next() else {
return false;
};
offset += next.len_utf8();
if !next.is_whitespace() {
separator = next;
break;
}
}
}
if matches!(separator, ':' | '=') {
return true;
}
if matches!(separator, '"' | '\'') {
return next_non_whitespace(content, offset).is_some_and(|next| matches!(next, ':' | '='));
}
false
}
fn next_non_whitespace(content: &str, mut offset: usize) -> Option<char> {
while offset < content.len() {
let next = content[offset..].chars().next()?;
if !next.is_whitespace() {
return Some(next);
}
offset += next.len_utf8();
}
None
}
fn secret_key_suffix_has_value(content: &str, mut offset: usize) -> bool {
let mut saw_suffix = false;
while offset < content.len() {
let Some(next) = content[offset..].chars().next() else {
break;
};
if is_secret_key_char(next) || is_secret_key_connector(next) {
saw_suffix |= is_secret_key_char(next);
offset += next.len_utf8();
continue;
}
if !saw_suffix {
return false;
}
return separator_starts_secret_value(content, offset + next.len_utf8(), next);
}
false
}
fn is_secret_key_char(ch: char) -> bool {
ch.is_ascii_alphanumeric()
}
fn is_secret_key_connector(ch: char) -> bool {
matches!(ch, '_' | '-' | '.')
}
#[must_use]
pub fn redact_content(content: &str, level: RedactionLevel) -> String {
match level {
RedactionLevel::None => content.to_owned(),
RedactionLevel::Minimal => {
if contains_export_secret_pattern(content) {
REDACTED_PLACEHOLDER.to_owned()
} else {
content.to_owned()
}
}
RedactionLevel::Standard => {
if contains_export_secret_pattern(content) {
REDACTED_PLACEHOLDER.to_owned()
} else {
redact_high_entropy_tokens(
&redact_paths_in_content(content),
STANDARD_HIGH_ENTROPY_BITS_PER_BYTE,
)
}
}
RedactionLevel::Strict => {
if contains_export_secret_pattern(content) {
REDACTED_PLACEHOLDER.to_owned()
} else {
redact_high_entropy_tokens(
&redact_paths_in_content(content),
STRICT_HIGH_ENTROPY_BITS_PER_BYTE,
)
.chars()
.take(200)
.collect()
}
}
RedactionLevel::Paranoid => REDACTED_PLACEHOLDER.to_owned(),
RedactionLevel::Full => REDACTED_PLACEHOLDER.to_owned(),
}
}
fn redact_paths_in_content(content: &str) -> String {
if !SENSITIVE_PATH_PREFIXES
.iter()
.any(|prefix| content.contains(prefix))
{
return content.to_owned();
}
let mut redacted = String::with_capacity(content.len());
for segment in content.split_inclusive('\n') {
let (line, terminator) = segment
.strip_suffix('\n')
.map_or((segment, ""), |line| (line, "\n"));
if SENSITIVE_PATH_PREFIXES
.iter()
.any(|prefix| line.contains(prefix))
{
redacted.push_str(&redact_paths_in_line(line));
redacted.push_str(terminator);
} else {
redacted.push_str(segment);
}
}
redacted
}
fn redact_paths_in_line(line: &str) -> String {
let mut output = String::new();
let mut cursor = 0;
while cursor < line.len() {
let remaining = &line[cursor..];
let matched_prefix = SENSITIVE_PATH_PREFIXES
.iter()
.filter(|prefix| remaining.starts_with(*prefix))
.max_by_key(|prefix| prefix.len());
if let Some(prefix) = matched_prefix {
output.push_str(REDACTED_PATH_PLACEHOLDER);
cursor += prefix.len();
let mut saw_separator_after_prefix = false;
while cursor < line.len() {
let next = line[cursor..].chars().next().unwrap_or('\0');
if is_horizontal_path_space(next) {
if whitespace_starts_path_continuation(line, cursor, saw_separator_after_prefix)
{
cursor += next.len_utf8();
continue;
}
break;
}
if next.is_whitespace() || path_redaction_hard_boundary(next) {
break;
}
if next == '/' || next == '\\' {
saw_separator_after_prefix = true;
}
cursor += next.len_utf8();
}
} else {
let c = remaining.chars().next().unwrap_or('\0');
output.push(c);
cursor += c.len_utf8();
}
}
output
}
fn redact_high_entropy_tokens(content: &str, threshold_bits_per_byte: f64) -> String {
let mut output = String::with_capacity(content.len());
let mut cursor = 0usize;
while cursor < content.len() {
let Some(ch) = content[cursor..].chars().next() else {
break;
};
if !is_high_entropy_token_char(ch) {
output.push(ch);
cursor += ch.len_utf8();
continue;
}
let start = cursor;
cursor += ch.len_utf8();
while cursor < content.len() {
let Some(next) = content[cursor..].chars().next() else {
break;
};
if !is_high_entropy_token_char(next) {
break;
}
cursor += next.len_utf8();
}
let token = &content[start..cursor];
if is_high_entropy_token(token, threshold_bits_per_byte) {
output.push_str(REDACTED_PLACEHOLDER);
} else {
output.push_str(token);
}
}
output
}
fn is_high_entropy_token_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | '+' | '/' | '=')
}
fn is_high_entropy_token(token: &str, threshold_bits_per_byte: f64) -> bool {
if token.len() < MIN_HIGH_ENTROPY_TOKEN_BYTES {
return false;
}
let mut counts = [0usize; 256];
for byte in token.bytes() {
counts[usize::from(byte)] += 1;
}
let len = token.len() as f64;
let entropy = counts
.iter()
.filter(|count| **count > 0)
.fold(0.0, |acc, count| {
let probability = *count as f64 / len;
acc - probability * probability.log2()
});
entropy >= threshold_bits_per_byte
}
fn starts_with_sensitive_path_prefix(value: &str) -> bool {
SENSITIVE_PATH_PREFIXES
.iter()
.any(|prefix| value.starts_with(prefix))
}
fn is_horizontal_path_space(character: char) -> bool {
matches!(character, ' ' | '\t')
}
fn whitespace_starts_path_continuation(
line: &str,
cursor: usize,
saw_separator_after_prefix: bool,
) -> bool {
let mut offset = cursor;
let mut consumed_horizontal_space = false;
while offset < line.len() {
let next = line[offset..].chars().next().unwrap_or('\0');
if !is_horizontal_path_space(next) {
break;
}
consumed_horizontal_space = true;
offset += next.len_utf8();
}
if !consumed_horizontal_space {
return false;
}
let mut saw_component_character = false;
let mut saw_following_separator = false;
while offset < line.len() {
let next = line[offset..].chars().next().unwrap_or('\0');
if next.is_whitespace() || path_redaction_hard_boundary(next) {
break;
}
if next == '=' {
return false;
}
if next == '/' || next == '\\' {
saw_following_separator = true;
}
saw_component_character = true;
offset += next.len_utf8();
}
saw_component_character && (saw_separator_after_prefix || saw_following_separator)
}
fn path_redaction_hard_boundary(c: char) -> bool {
matches!(
c,
'?' | '#' | '"' | '\'' | '`' | '<' | '>' | ')' | ']' | '}' | ',' | ';' | '|'
)
}
#[must_use]
pub fn redact_path(path: &str, level: RedactionLevel) -> String {
match level {
RedactionLevel::None | RedactionLevel::Minimal => path.to_owned(),
RedactionLevel::Standard
| RedactionLevel::Strict
| RedactionLevel::Paranoid
| RedactionLevel::Full => {
if contains_secret_pattern(path) {
REDACTED_PLACEHOLDER.to_owned()
} else if starts_with_sensitive_path_prefix(path) {
REDACTED_PATH_PLACEHOLDER.to_owned()
} else {
redact_paths_in_content(path)
}
}
}
}
pub(crate) fn redact_provenance_uri(uri: &str, level: RedactionLevel) -> String {
let path_redacted = if level.redacts_paths() {
redact_path(uri, level)
} else {
uri.to_owned()
};
if level.redacts_secrets() {
redact_content(&path_redacted, level)
} else {
path_redacted
}
}
#[must_use]
pub fn redact_identifier(id: &str, level: RedactionLevel) -> String {
match level {
RedactionLevel::None | RedactionLevel::Minimal | RedactionLevel::Strict => id.to_owned(),
RedactionLevel::Standard => {
let char_count = id.chars().count();
if char_count > 8 {
let prefix: String = id.chars().take(4).collect();
let suffix: String = id.chars().skip(char_count.saturating_sub(4)).collect();
format!("{prefix}...{suffix}")
} else {
id.to_owned()
}
}
RedactionLevel::Paranoid => format!("id_{}", blake3_prefix(id, 16)),
RedactionLevel::Full => REDACTED_ID_PLACEHOLDER.to_owned(),
}
}
#[must_use]
pub fn redact_memory_record(
mut record: ExportMemoryRecord,
level: RedactionLevel,
) -> ExportMemoryRecord {
if level == RedactionLevel::None {
return record;
}
let original_content = record.content.clone();
if matches!(
level,
RedactionLevel::Strict | RedactionLevel::Paranoid | RedactionLevel::Full
) {
record.content_hash = Some(blake3_digest(&original_content));
}
if original_content != crate::models::MEMORY_SEAL_PLACEHOLDER_CONTENT {
record.content = redact_content(&original_content, level);
}
if let Some(reason) = record.tombstoned_reason.as_ref() {
record.tombstoned_reason = Some(redact_content(reason, level));
}
record.redacted = level != RedactionLevel::None;
record.redaction_reason = Some(format!("redaction_level:{}", level.as_str()));
if let Some(attempt_family) = record.attempt_family.as_mut() {
attempt_family.family_id =
crate::models::public_attempt_family_alias(&attempt_family.family_id);
}
if level.redacts_identifiers() {
record.memory_id = redact_identifier(&record.memory_id, level);
if let Some(logical_id) = record.logical_id.as_mut() {
*logical_id = redact_identifier(logical_id, level);
}
record.workspace_id = redact_identifier(&record.workspace_id, level);
if let Some(agent) = record.source_agent.as_ref() {
record.source_agent = Some(redact_identifier(agent, level));
}
}
if let Some(uri) = record.provenance_uri.as_ref() {
record.provenance_uri = Some(redact_provenance_uri(uri, level));
}
record
}
#[must_use]
pub fn redact_artifact_record(
mut record: ExportArtifactRecord,
level: RedactionLevel,
) -> ExportArtifactRecord {
if level == RedactionLevel::None {
return record;
}
if let Some(snippet) = record.snippet.as_ref() {
record.snippet = Some(redact_content(snippet, level));
}
if level.redacts_paths() {
if let Some(path) = record.original_path.as_ref() {
record.original_path = Some(redact_path(path, level));
}
if let Some(path) = record.canonical_path.as_ref() {
record.canonical_path = Some(redact_path(path, level));
}
}
if let Some(uri) = record.provenance_uri.as_ref() {
record.provenance_uri = Some(redact_provenance_uri(uri, level));
}
if let Some(reference) = record.external_ref.as_ref() {
record.external_ref = Some(redact_content(reference, level));
}
if level.redacts_identifiers() {
record.artifact_id = redact_identifier(&record.artifact_id, level);
record.workspace_id = redact_identifier(&record.workspace_id, level);
}
if level.redacts_content() {
record.snippet = None;
record.metadata = None;
} else if let Some(metadata) = record.metadata.as_mut() {
redact_link_metadata(metadata, level);
}
record
}
#[must_use]
pub fn redact_workspace_record(
mut record: ExportWorkspaceRecord,
level: RedactionLevel,
) -> ExportWorkspaceRecord {
if level == RedactionLevel::None {
return record;
}
if level.redacts_paths() {
record.path = redact_path(&record.path, level);
}
if level.redacts_identifiers() {
record.workspace_id = redact_identifier(&record.workspace_id, level);
if let Some(name) = record.name.as_ref() {
record.name = Some(redact_identifier(name, level));
}
}
record
}
#[must_use]
pub fn redact_agent_record(
mut record: ExportAgentRecord,
level: RedactionLevel,
) -> ExportAgentRecord {
if level == RedactionLevel::None {
return record;
}
if level.redacts_identifiers() {
record.agent_id = redact_identifier(&record.agent_id, level);
}
record
}
#[must_use]
pub fn redact_audit_record(
mut record: ExportAuditRecord,
level: RedactionLevel,
) -> ExportAuditRecord {
if level == RedactionLevel::None {
return record;
}
if level.redacts_identifiers() {
record.audit_id = redact_identifier(&record.audit_id, level);
record.target_id = record
.target_id
.take()
.map(|target_id| redact_identifier(&target_id, level));
if let Some(by) = record.performed_by.as_ref() {
record.performed_by = Some(redact_identifier(by, level));
}
}
match level {
RedactionLevel::Minimal | RedactionLevel::Standard | RedactionLevel::Strict => {
if let Some(details) = record.details.as_ref() {
record.details = Some(serde_json::json!({
"hash": format!("blake3:{}", blake3_prefix(&canonical_json(details), 16)),
}));
}
}
RedactionLevel::Paranoid | RedactionLevel::Full => {
record.details = None;
}
RedactionLevel::None => {}
}
record
}
#[must_use]
pub fn redact_record(record: ExportRecord, level: RedactionLevel) -> ExportRecord {
match record {
ExportRecord::Header(h) => ExportRecord::Header(redact_header(h, level)),
ExportRecord::Memory(m) => ExportRecord::Memory(Box::new(redact_memory_record(*m, level))),
ExportRecord::Artifact(a) => ExportRecord::Artifact(redact_artifact_record(a, level)),
ExportRecord::Link(l) => ExportRecord::Link(redact_link_record(l, level)),
ExportRecord::Tag(t) => ExportRecord::Tag(redact_tag_record(t, level)),
ExportRecord::Agent(a) => ExportRecord::Agent(redact_agent_record(a, level)),
ExportRecord::Workspace(w) => ExportRecord::Workspace(redact_workspace_record(w, level)),
ExportRecord::Audit(a) => ExportRecord::Audit(redact_audit_record(a, level)),
ExportRecord::Footer(f) => ExportRecord::Footer(f),
}
}
fn redact_header(mut header: ExportHeader, level: RedactionLevel) -> ExportHeader {
if level.redacts_paths() {
if let Some(path) = header.workspace_path.as_ref() {
header.workspace_path = Some(redact_path(path, level));
}
}
if level.redacts_identifiers() {
if let Some(id) = header.workspace_id.as_ref() {
header.workspace_id = Some(redact_identifier(id, level));
}
if let Some(host) = header.hostname.as_ref() {
header.hostname = Some(redact_identifier(host, level));
}
}
header
}
fn redact_link_record(mut record: ExportLinkRecord, level: RedactionLevel) -> ExportLinkRecord {
if level.redacts_identifiers() {
record.link_id = redact_identifier(&record.link_id, level);
record.source_memory_id = redact_identifier(&record.source_memory_id, level);
record.target_memory_id = redact_identifier(&record.target_memory_id, level);
}
if level.redacts_content() {
record.metadata = None;
} else if let Some(metadata) = record.metadata.as_mut() {
redact_link_metadata(metadata, level);
}
record
}
fn redact_link_metadata(value: &mut serde_json::Value, level: RedactionLevel) {
match value {
serde_json::Value::Object(object) => {
for (key, child) in object {
redact_link_metadata_field(key, child, level);
}
}
serde_json::Value::Array(values) => {
for child in values {
redact_link_metadata(child, level);
}
}
serde_json::Value::String(text) => {
if level.redacts_paths() || level.redacts_secrets() {
*text = redact_content(text, level);
}
}
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {}
}
}
fn redact_link_metadata_field(key: &str, value: &mut serde_json::Value, level: RedactionLevel) {
match value {
serde_json::Value::String(text) if link_metadata_json_surface_key(key) => {
redact_link_metadata_json_surface(text, level);
}
serde_json::Value::String(text) if link_metadata_identifier_key(key) => {
let original = text.clone();
if level.redacts_paths() {
*text = redact_content(text, level);
}
if level.redacts_identifiers() && *text == original {
*text = redact_identifier(text, level);
}
}
serde_json::Value::String(text) if link_metadata_path_key(key) => {
if level.redacts_paths() || level.redacts_secrets() {
*text = redact_content(text, level);
}
}
child => redact_link_metadata(child, level),
}
}
fn redact_link_metadata_json_surface(text: &mut String, level: RedactionLevel) {
let original = text.clone();
if let Ok(mut parsed) = serde_json::from_str::<serde_json::Value>(&original) {
redact_link_metadata(&mut parsed, level);
if let Ok(redacted) = serde_json::to_string(&parsed) {
*text = redacted;
return;
}
}
if level.redacts_paths() || level.redacts_secrets() {
*text = redact_content(text, level);
}
if level.redacts_identifiers() && *text == original {
*text = redact_identifier(text, level);
}
}
fn link_metadata_json_surface_key(key: &str) -> bool {
matches!(
key,
"policyDecisionJson"
| "policy_decision_json"
| "policyFailureSurfaceJson"
| "policy_failure_surface_json"
)
}
fn link_metadata_identifier_key(key: &str) -> bool {
matches!(
key,
"bodyCacheKey"
| "body_cache_key"
| "cachedMaterialId"
| "cached_material_id"
| "importDecisionId"
| "import_decision_id"
| "importDecisionRef"
| "import_decision_ref"
| "localWorkspaceId"
| "local_workspace_id"
| "originNodeId"
| "origin_node_id"
| "originWorkspaceAlias"
| "origin_workspace_alias"
| "originWorkspaceId"
| "origin_workspace_id"
| "peerId"
| "peer_id"
| "policyId"
| "policy_id"
| "policyRef"
| "policy_ref"
| "producerPeer"
| "producer_peer"
| "producerPeerId"
| "producer_peer_id"
| "workspaceId"
| "workspace_id"
)
}
fn link_metadata_path_key(key: &str) -> bool {
matches!(
key,
"absolutePath"
| "absolute_path"
| "binaryAbsolutePath"
| "binary_absolute_path"
| "canonicalPath"
| "canonical_path"
| "path"
| "provenanceUri"
| "provenance_uri"
| "uri"
)
}
fn redact_tag_record(mut record: ExportTagRecord, level: RedactionLevel) -> ExportTagRecord {
if level.redacts_identifiers() {
record.memory_id = redact_identifier(&record.memory_id, level);
}
match level {
RedactionLevel::Paranoid | RedactionLevel::Full => {
record.tag = format!("tag_{}", blake3_prefix(&record.tag, 16));
}
_ => {}
}
record
}
fn blake3_prefix(input: &str, chars: usize) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let digest = blake3::hash(input.as_bytes());
let mut output = String::with_capacity(chars);
for byte in digest.as_bytes() {
if output.len() >= chars {
break;
}
output.push(HEX[(byte >> 4) as usize] as char);
if output.len() >= chars {
break;
}
output.push(HEX[(byte & 0x0F) as usize] as char);
}
output
}
fn blake3_digest(input: &str) -> String {
format!("blake3:{}", blake3::hash(input.as_bytes()).to_hex())
}
fn canonical_json(value: &serde_json::Value) -> String {
serde_json::to_string(value).unwrap_or_else(|_| value.to_string())
}
pub struct JsonlExporter<W: Write> {
writer: W,
redaction_level: RedactionLevel,
export_scope: ExportScope,
records_written: u64,
memory_count: u64,
artifact_count: u64,
link_count: u64,
tag_count: u64,
audit_count: u64,
records_root: RecordsRootBuilder,
}
impl<W: Write> JsonlExporter<W> {
pub fn new(writer: W, redaction_level: RedactionLevel, export_scope: ExportScope) -> Self {
Self {
writer,
redaction_level,
export_scope,
records_written: 0,
memory_count: 0,
artifact_count: 0,
link_count: 0,
tag_count: 0,
audit_count: 0,
records_root: RecordsRootBuilder::new(),
}
}
#[must_use]
pub const fn redaction_level(&self) -> RedactionLevel {
self.redaction_level
}
#[must_use]
pub const fn export_scope(&self) -> ExportScope {
self.export_scope
}
#[must_use]
pub const fn records_written(&self) -> u64 {
self.records_written
}
pub fn write_header(&mut self, mut header: ExportHeader) -> io::Result<()> {
header.redaction_level = self.redaction_level;
header.export_scope = self.export_scope;
header.format_version = EXPORT_FORMAT_VERSION;
let redacted = redact_header(header, self.redaction_level);
let json = serde_json::to_string(&redacted)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
writeln!(self.writer, "{json}")?;
self.records_written += 1;
Ok(())
}
pub fn write_memory(&mut self, record: ExportMemoryRecord) -> io::Result<()> {
if !self.export_scope.includes_memories() {
return Ok(());
}
let redacted = redact_memory_record(record, self.redaction_level);
let memory_id = redacted.memory_id.clone();
let json = if self.export_scope == ExportScope::MetadataOnly {
let mut meta_only = redacted;
meta_only.content = String::new();
serde_json::to_string(&meta_only)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
} else {
serde_json::to_string(&redacted)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
};
writeln!(self.writer, "{json}")?;
self.records_root
.push(&memory_id, &canonical_record_hash(json.as_bytes()));
self.records_written += 1;
self.memory_count += 1;
Ok(())
}
#[must_use]
pub fn finalize_records_root(&self) -> ([u8; 32], u64) {
(self.records_root.finalize(), self.records_root.count())
}
pub fn write_artifact(&mut self, record: ExportArtifactRecord) -> io::Result<()> {
if !self.export_scope.includes_artifacts() {
return Ok(());
}
let redacted = redact_artifact_record(record, self.redaction_level);
if self.export_scope == ExportScope::MetadataOnly {
let mut meta_only = redacted;
meta_only.snippet = None;
let json = serde_json::to_string(&meta_only)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
writeln!(self.writer, "{json}")?;
} else {
let json = serde_json::to_string(&redacted)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
writeln!(self.writer, "{json}")?;
}
self.records_written += 1;
self.artifact_count += 1;
Ok(())
}
pub fn write_link(&mut self, record: ExportLinkRecord) -> io::Result<()> {
if !self.export_scope.includes_links() {
return Ok(());
}
let redacted = redact_link_record(record, self.redaction_level);
let json = serde_json::to_string(&redacted)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
writeln!(self.writer, "{json}")?;
self.records_root
.push(&redacted.link_id, &canonical_record_hash(json.as_bytes()));
self.records_written += 1;
self.link_count += 1;
Ok(())
}
pub fn write_tag(&mut self, record: ExportTagRecord) -> io::Result<()> {
if !self.export_scope.includes_memories() {
return Ok(());
}
let redacted = redact_tag_record(record, self.redaction_level);
let json = serde_json::to_string(&redacted)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
writeln!(self.writer, "{json}")?;
self.records_root
.push(&redacted.memory_id, &canonical_record_hash(json.as_bytes()));
self.records_written += 1;
self.tag_count += 1;
Ok(())
}
pub fn write_audit(&mut self, record: ExportAuditRecord) -> io::Result<()> {
if !self.export_scope.includes_audit() {
return Ok(());
}
let redacted = redact_audit_record(record, self.redaction_level);
let json = serde_json::to_string(&redacted)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
writeln!(self.writer, "{json}")?;
self.records_written += 1;
self.audit_count += 1;
Ok(())
}
pub fn write_agent(&mut self, record: ExportAgentRecord) -> io::Result<()> {
let redacted = redact_agent_record(record, self.redaction_level);
let json = serde_json::to_string(&redacted)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
writeln!(self.writer, "{json}")?;
self.records_written += 1;
Ok(())
}
pub fn write_workspace(&mut self, record: ExportWorkspaceRecord) -> io::Result<()> {
let redacted = redact_workspace_record(record, self.redaction_level);
let json = serde_json::to_string(&redacted)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
writeln!(self.writer, "{json}")?;
self.records_written += 1;
Ok(())
}
pub fn write_footer(&mut self, mut footer: ExportFooter) -> io::Result<ExportStats> {
let final_record_count = self.records_written.saturating_add(1);
footer.total_records = final_record_count;
footer.memory_count = self.memory_count;
footer.artifact_count = self.artifact_count;
footer.link_count = self.link_count;
footer.tag_count = self.tag_count;
footer.audit_count = self.audit_count;
footer.success = true;
let json = serde_json::to_string(&footer)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
writeln!(self.writer, "{json}")?;
self.records_written = final_record_count;
Ok(ExportStats {
total_records: self.records_written,
memory_count: self.memory_count,
artifact_count: self.artifact_count,
link_count: self.link_count,
tag_count: self.tag_count,
audit_count: self.audit_count,
redaction_level: self.redaction_level,
export_scope: self.export_scope,
})
}
pub fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExportStats {
pub total_records: u64,
pub memory_count: u64,
pub artifact_count: u64,
pub link_count: u64,
pub tag_count: u64,
pub audit_count: u64,
pub redaction_level: RedactionLevel,
pub export_scope: ExportScope,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::models::{
EXPORT_ARTIFACT_SCHEMA_V1, EXPORT_MEMORY_SCHEMA_V1, ExportAttemptFamilyRecord,
ExportHeader, ExportLinkRecord, ExportMemoryRecord,
};
type TestResult = Result<(), String>;
fn ensure<T: std::fmt::Debug + PartialEq>(actual: T, expected: T, ctx: &str) -> TestResult {
if actual == expected {
Ok(())
} else {
Err(format!("{ctx}: expected {expected:?}, got {actual:?}"))
}
}
fn secret_fixture(parts: &[&str]) -> String {
parts.concat()
}
fn secret_assignment(value: &str) -> String {
format!("{}={value}", secret_fixture(&["api", "_", "key"]))
}
#[test]
fn contains_secret_pattern_detects_secrets() {
assert!(contains_secret_pattern(&secret_fixture(&[
"api",
"_key=abc123"
])));
assert!(contains_secret_pattern(&secret_fixture(&[
"PASS",
"WORD: hunter2"
])));
assert!(contains_secret_pattern(&secret_fixture(&[
"Bearer ", "token123"
])));
assert!(contains_secret_pattern(&secret_fixture(&[
"AWS", "_SECRET", "_KEY"
])));
assert!(contains_secret_pattern(&secret_fixture(&[
"-----BEGIN RSA ",
"PRIVATE ",
"KEY-----"
])));
assert!(!contains_secret_pattern("just some normal content"));
assert!(!contains_secret_pattern("public data here"));
}
#[test]
fn contains_secret_pattern_does_not_match_auth_prose() {
assert!(!contains_secret_pattern(
"The author wrote about authority figures in the authentic style."
));
assert!(!contains_secret_pattern(
"Use the authentication middleware to authorize requests."
));
assert!(!contains_secret_pattern(
"JWT sessions are minted by the auth service."
));
assert!(contains_secret_pattern("OAUTH=abc123"));
assert!(contains_secret_pattern("auth_token: 9f3c2"));
assert!(contains_secret_pattern("Authorization: Bearer xyz"));
}
#[test]
fn contains_secret_pattern_does_not_match_token_prose() {
assert!(!contains_secret_pattern(
"Keep the token budget under 4000 for compact context packs."
));
assert!(!contains_secret_pattern(
"Keep the token-budget window under 4000 for compact context packs."
));
assert!(!contains_secret_pattern(
"The token.count metric is only a retrieval budget estimate."
));
assert!(!contains_secret_pattern(
"The tokenizer splits source text into stable token spans."
));
assert!(!contains_secret_pattern(
r#"The field name "token" appears in docs without a value."#
));
assert!(contains_secret_pattern(&secret_fixture(&[
"token", "=abc123"
])));
assert!(contains_secret_pattern(&secret_fixture(&[
"token", ": abc123"
])));
assert!(contains_secret_pattern(&secret_fixture(&[
"token",
"_value=abc123"
])));
assert!(contains_secret_pattern(&secret_fixture(&[
r#""token"#,
r#"":"abc123""#
])));
assert!(contains_secret_pattern(&secret_fixture(&[
"session",
"-token=abc123"
])));
}
#[test]
fn redact_content_minimal_preserves_token_budget_prose() -> TestResult {
let content = "Pack selection compares token budget against estimated tokens.";
ensure(
redact_content(content, RedactionLevel::Minimal),
content.to_owned(),
"minimal redaction preserves token-budget prose",
)
}
#[test]
fn redact_content_none_level_preserves() -> TestResult {
let content = secret_assignment("redaction-fixture");
let result = redact_content(&content, RedactionLevel::None);
ensure(result, content, "none level preserves content")
}
#[test]
fn redact_targetless_audit_preserves_absent_target_pair() -> TestResult {
let audit = ExportAuditRecord::builder()
.audit_id("aud-db-check-redaction")
.operation("db.check_integrity")
.performed_at("2026-04-30T12:00:00Z")
.performed_by("ee db check-integrity")
.details(serde_json::json!({ "passed": true }))
.build()
.map_err(|error| format!("targetless audit fixture must build: {error}"))?;
for level in [RedactionLevel::Minimal, RedactionLevel::Full] {
let redacted = redact_audit_record(audit.clone(), level);
ensure(
redacted.target_type.as_deref(),
None,
&format!("{level} keeps target_type absent"),
)?;
ensure(
redacted.target_id.as_deref(),
None,
&format!("{level} keeps target_id absent"),
)?;
}
Ok(())
}
#[test]
fn redact_content_minimal_level_redacts_secrets() -> TestResult {
let sensitive_input = secret_assignment("redaction-fixture");
let normal = "just normal content";
ensure(
redact_content(&sensitive_input, RedactionLevel::Minimal),
REDACTED_PLACEHOLDER.to_owned(),
"minimal redacts secrets",
)?;
ensure(
redact_content(normal, RedactionLevel::Minimal),
normal.to_owned(),
"minimal preserves normal",
)
}
#[test]
fn redact_content_minimal_uses_policy_secret_shapes_without_pii_or_entropy_drift() -> TestResult
{
let openai_key = "sk-proj-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ==.c2lnbmF0dXJl";
let email = "contact redaction-fixture@example.test for context";
let long_entropy = "abcdEFGH1234abcdEFGH1234abcdEFGH1234abcdEFGH1234abcdEFGH1234";
for secret in [openai_key, jwt] {
ensure(
redact_content(secret, RedactionLevel::Minimal),
REDACTED_PLACEHOLDER.to_owned(),
&format!("minimal redacts policy-recognized credential shape {secret}"),
)?;
}
ensure(
redact_content(email, RedactionLevel::Minimal),
email.to_owned(),
"minimal does not promote policy PII findings into JSONL export content redaction",
)?;
ensure(
redact_content(long_entropy, RedactionLevel::Minimal),
long_entropy.to_owned(),
"minimal preserves high-entropy-only values per redaction-level matrix",
)
}
#[test]
fn redact_content_standard_and_strict_redact_high_entropy_tokens() -> TestResult {
let standard_token = "0123456789abcdef0123456789abcdef";
let strict_only_token = "abcdefghijklabcdefghijklabcdefghijkl";
let standard_text = format!("fingerprint {standard_token} done");
let strict_text = format!("nonce {strict_only_token} done");
ensure(
redact_content(&standard_text, RedactionLevel::Minimal),
standard_text.clone(),
"minimal preserves high-entropy tokens",
)?;
ensure(
redact_content(&standard_text, RedactionLevel::Standard),
format!("fingerprint {REDACTED_PLACEHOLDER} done"),
"standard redacts tokens at or above 4.0 bits per byte",
)?;
ensure(
redact_content(&strict_text, RedactionLevel::Standard),
strict_text,
"standard preserves tokens below 4.0 bits per byte",
)?;
ensure(
redact_content(
&format!("nonce {strict_only_token} done"),
RedactionLevel::Strict,
),
format!("nonce {REDACTED_PLACEHOLDER} done"),
"strict redacts tokens at or above 3.5 bits per byte",
)
}
#[test]
fn redact_content_full_level_redacts_all() -> TestResult {
let content = "just normal content";
ensure(
redact_content(content, RedactionLevel::Full),
REDACTED_PLACEHOLDER.to_owned(),
"full redacts everything",
)
}
#[test]
fn redact_path_standard_level() -> TestResult {
ensure(
redact_path("/home/user/project", RedactionLevel::Standard),
REDACTED_PATH_PLACEHOLDER.to_owned(),
"standard redacts home paths",
)?;
ensure(
redact_path(
"/Volumes/USBNVME16TB/private/model.bin",
RedactionLevel::Standard,
),
REDACTED_PATH_PLACEHOLDER.to_owned(),
"standard redacts mounted-volume paths",
)?;
ensure(
redact_path(
"file:///Users/alice/private/model.bin?download=1",
RedactionLevel::Standard,
),
format!("file://{REDACTED_PATH_PLACEHOLDER}?download=1"),
"standard redacts URI-wrapped home paths without losing query text",
)?;
ensure(
redact_path(
"file:///Users/alice/My Project/model.bin?download=1",
RedactionLevel::Standard,
),
format!("file://{REDACTED_PATH_PLACEHOLDER}?download=1"),
"standard redacts URI-wrapped paths with spaces without leaking suffixes",
)?;
ensure(
redact_path("/usr/local/bin", RedactionLevel::Standard),
"/usr/local/bin".to_owned(),
"standard preserves system paths",
)
}
#[test]
fn redact_content_standard_redacts_paths_with_spaces() -> TestResult {
ensure(
redact_content(
r#"source=/Users/alice/My Project label=/data/private/Release Notes win=C:\Users\alice\Draft Folder note=done"#,
RedactionLevel::Standard,
),
format!(
"source={REDACTED_PATH_PLACEHOLDER} label={REDACTED_PATH_PLACEHOLDER} win={REDACTED_PATH_PLACEHOLDER} note=done"
),
"standard redacts path components with spaces without crossing key fields",
)
}
#[test]
fn redact_content_standard_handles_short_path_before_more_text() -> TestResult {
ensure(
redact_content("open /Users/a now", RedactionLevel::Standard),
format!("open {REDACTED_PATH_PLACEHOLDER} now"),
"standard redacts short paths without losing trailing text",
)
}
#[test]
fn redact_content_standard_preserves_path_boundary_punctuation() -> TestResult {
let cases = [
(
"path `/Users/alice/project` rest",
format!("path `{REDACTED_PATH_PLACEHOLDER}` rest"),
),
(
"path </Users/alice/project> rest",
format!("path <{REDACTED_PATH_PLACEHOLDER}> rest"),
),
(
"path /Users/alice/project|rest",
format!("path {REDACTED_PATH_PLACEHOLDER}|rest"),
),
];
for (input, expected) in cases {
ensure(
redact_content(input, RedactionLevel::Standard),
expected,
input,
)?;
}
Ok(())
}
#[test]
fn redact_content_standard_does_not_cross_line_boundaries_after_paths() -> TestResult {
ensure(
redact_content(
"source=/Users/alice/My Project\nAgent: cod-search",
RedactionLevel::Standard,
),
format!("source={REDACTED_PATH_PLACEHOLDER}\nAgent: cod-search"),
"standard redacts terminal component but keeps the next line",
)
}
#[test]
fn redact_content_standard_preserves_line_terminators_after_path_redaction() -> TestResult {
ensure(
redact_content("source=/Users/alice/My Project\n", RedactionLevel::Standard),
format!("source={REDACTED_PATH_PLACEHOLDER}\n"),
"standard redaction preserves trailing newline",
)?;
ensure(
redact_content(
"source=/Users/alice/My Project\r\nnext=/tmp/private/file\r\n",
RedactionLevel::Standard,
),
format!("source={REDACTED_PATH_PLACEHOLDER}\r\nnext={REDACTED_PATH_PLACEHOLDER}\r\n"),
"standard redaction preserves CRLF line endings",
)
}
#[test]
fn redact_identifier_standard_level() -> TestResult {
ensure(
redact_identifier("mem_abc123xyz456", RedactionLevel::Standard),
"mem_...z456".to_owned(),
"standard truncates long IDs",
)?;
ensure(
redact_identifier("short", RedactionLevel::Standard),
"short".to_owned(),
"standard preserves short IDs",
)?;
ensure(
redact_identifier("anything", RedactionLevel::Full),
REDACTED_ID_PLACEHOLDER.to_owned(),
"full redacts all IDs",
)
}
#[test]
fn sealed_memory_marker_survives_every_redaction_level() {
for level in [
RedactionLevel::None,
RedactionLevel::Minimal,
RedactionLevel::Standard,
RedactionLevel::Strict,
RedactionLevel::Paranoid,
RedactionLevel::Full,
] {
let record = ExportMemoryRecord::builder()
.memory_id("mem-sealed")
.workspace_id("ws-sealed")
.level("procedural")
.kind("rule")
.content(crate::models::MEMORY_SEAL_PLACEHOLDER_CONTENT)
.provenance_uri("https://example.test/?api_key=seal-secret-canary")
.created_at("2026-09-01T00:00:00Z")
.build()
.expect("valid sealed record");
let mut ordinary = record.clone();
ordinary.content = "api_key=ordinary-secret-canary".to_owned();
let sealed = redact_memory_record(record, level);
assert_eq!(
sealed.content,
crate::models::MEMORY_SEAL_PLACEHOLDER_CONTENT
);
if level != RedactionLevel::None {
assert!(
!sealed
.provenance_uri
.expect("provenance retained")
.contains("seal-secret-canary")
);
assert!(
!redact_memory_record(ordinary, level)
.content
.contains("ordinary-secret-canary")
);
}
}
}
#[test]
fn redact_memory_record_minimal() {
let content = secret_assignment("redaction-fixture");
let record = ExportMemoryRecord::builder()
.memory_id("mem-001")
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content(content)
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields");
let redacted = redact_memory_record(record, RedactionLevel::Minimal);
assert_eq!(redacted.content, REDACTED_PLACEHOLDER);
assert!(redacted.redacted);
assert!(redacted.redaction_reason.is_some());
assert_eq!(redacted.memory_id, "mem-001");
}
#[test]
fn minimal_redaction_scrubs_secret_bearing_provenance() {
let secret = secret_assignment("provenance-fixture");
let provenance = format!("https://example.test/report?{secret}");
assert_eq!(
redact_provenance_uri("manual://ordinary-source", RedactionLevel::Minimal),
"manual://ordinary-source",
"minimal redaction must preserve provenance that contains no secret"
);
let memory = ExportMemoryRecord::builder()
.memory_id("mem-provenance-redaction")
.workspace_id("ws-provenance-redaction")
.level("semantic")
.kind("fact")
.content("ordinary content")
.provenance_uri(&provenance)
.created_at("2026-08-30T12:00:00Z")
.build()
.expect("memory has required fields");
let artifact = ExportArtifactRecord::builder()
.artifact_id("artifact-provenance-redaction")
.workspace_id("ws-provenance-redaction")
.source_kind("file")
.artifact_type("report")
.content_hash("blake3:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
.media_type("text/plain")
.size_bytes(42)
.redaction_status("checked")
.provenance_uri(&provenance)
.created_at("2026-08-30T12:00:00Z")
.updated_at("2026-08-30T12:00:00Z")
.build()
.expect("artifact has required fields");
let redacted_memory = redact_memory_record(memory.clone(), RedactionLevel::Minimal);
let redacted_artifact = redact_artifact_record(artifact.clone(), RedactionLevel::Minimal);
assert_eq!(
redacted_memory.provenance_uri.as_deref(),
Some(REDACTED_PLACEHOLDER)
);
assert_eq!(
redacted_artifact.provenance_uri.as_deref(),
Some(REDACTED_PLACEHOLDER)
);
assert!(
!serde_json::to_string(&(redacted_memory, redacted_artifact))
.expect("redacted records serialize")
.contains(&secret)
);
assert_eq!(
redact_memory_record(memory, RedactionLevel::None)
.provenance_uri
.as_deref(),
Some(provenance.as_str())
);
assert_eq!(
redact_artifact_record(artifact, RedactionLevel::None)
.provenance_uri
.as_deref(),
Some(provenance.as_str())
);
}
#[test]
fn redact_memory_record_preserves_revision_reference_aliases() {
let root_id = "mem_00000000000000000000000001";
let record = ExportMemoryRecord::builder()
.memory_id("mem_00000000000000000000000002")
.logical_id(root_id)
.workspace_id("wsp_00000000000000000000000003")
.level("procedural")
.kind("rule")
.content("Keep revision lineage intact.")
.created_at("2026-05-01T00:00:00Z")
.build()
.expect("valid revision record");
for level in [
RedactionLevel::None,
RedactionLevel::Minimal,
RedactionLevel::Standard,
RedactionLevel::Strict,
RedactionLevel::Paranoid,
RedactionLevel::Full,
] {
let redacted = redact_memory_record(record.clone(), level);
assert_eq!(redacted.logical_id, Some(redact_identifier(root_id, level)));
let serialized = serde_json::to_string(&redacted).expect("serialize revision");
let decoded: ExportMemoryRecord =
serde_json::from_str(&serialized).expect("decode revision");
assert_eq!(decoded, redacted);
}
}
#[test]
fn redact_memory_record_never_exports_raw_attempt_family_id() {
let raw_family_id = secret_fixture(&["AKIA", "EXAMPLEFAMILYSECRET"]);
let record = ExportMemoryRecord::builder()
.memory_id("mem-family-redaction")
.workspace_id("ws-family-redaction")
.level("semantic")
.kind("fact")
.content("selected attempt")
.created_at("2026-08-09T12:00:00Z")
.attempt_family(ExportAttemptFamilyRecord {
family_id: raw_family_id.clone(),
declared_size: Some(3),
attempt_index: Some(1),
disposition: Some("selected".to_owned()),
origin: Some("manual".to_owned()),
})
.build()
.expect("memory has required fields");
let redacted = redact_memory_record(record.clone(), RedactionLevel::Minimal);
let redacted_family = redacted
.attempt_family
.expect("attempt family remains represented after redaction");
assert!(redacted_family.family_id.starts_with("afm_"));
assert_ne!(redacted_family.family_id, raw_family_id);
assert!(
!serde_json::to_string(&redacted_family)
.expect("redacted family serializes")
.contains(&raw_family_id)
);
let unredacted = redact_memory_record(record, RedactionLevel::None);
assert_eq!(
unredacted
.attempt_family
.expect("unredacted attempt family remains available")
.family_id,
raw_family_id
);
}
#[test]
fn redact_memory_record_standard() {
let record = ExportMemoryRecord::builder()
.memory_id("mem-abc123xyz456")
.workspace_id("ws-def789uvw012")
.level("procedural")
.kind("rule")
.content("normal content")
.provenance_uri("/home/user/file.txt")
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields");
let redacted = redact_memory_record(record, RedactionLevel::Standard);
assert_eq!(redacted.memory_id, "mem-...z456");
assert_eq!(redacted.workspace_id, "ws-d...w012");
assert_eq!(
redacted.provenance_uri,
Some(REDACTED_PATH_PLACEHOLDER.to_owned())
);
}
#[test]
fn redact_memory_record_standard_redacts_uri_wrapped_provenance_path() {
let record = ExportMemoryRecord::builder()
.memory_id("mem-uri-path")
.workspace_id("ws-uri-path")
.level("procedural")
.kind("rule")
.content("normal content")
.provenance_uri("file:///Users/alice/private/memory.md?download=1")
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields");
let redacted = redact_memory_record(record, RedactionLevel::Standard);
let provenance = redacted.provenance_uri.expect("provenance stays present");
assert!(provenance.contains(REDACTED_PATH_PLACEHOLDER));
assert!(provenance.contains("?download=1"));
assert!(!provenance.contains("/Users/alice/private"));
}
#[test]
fn jsonl_exporter_writes_header() {
let mut output = Vec::new();
let mut exporter = JsonlExporter::new(&mut output, RedactionLevel::None, ExportScope::All);
let header = ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.ee_version("0.1.0")
.export_id("test-export")
.build()
.expect("header has required fields");
exporter.write_header(header).expect("write header");
let written = String::from_utf8(output).expect("valid utf8");
assert!(written.contains("ee.export.header.v1"));
assert!(written.ends_with('\n'));
}
#[test]
fn jsonl_exporter_writes_memory() {
let mut output = Vec::new();
let memory = ExportMemoryRecord::builder()
.memory_id("mem-001")
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content("Test content")
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields");
let memory_count = {
let mut exporter =
JsonlExporter::new(&mut output, RedactionLevel::None, ExportScope::All);
exporter.write_memory(memory).expect("write memory");
exporter.memory_count
};
let written = String::from_utf8(output).expect("valid utf8");
assert!(written.contains("ee.export.memory.v1"));
assert!(written.contains("Test content"));
assert_eq!(memory_count, 1);
}
fn export_memory(memory_id: &str, content: &str) -> ExportMemoryRecord {
ExportMemoryRecord::builder()
.memory_id(memory_id)
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content(content)
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields")
}
fn records_root_over(memories: &[(&str, &str)]) -> ([u8; 32], u64) {
let mut output = Vec::new();
let mut exporter = JsonlExporter::new(&mut output, RedactionLevel::None, ExportScope::All);
for &(id, content) in memories {
exporter
.write_memory(export_memory(id, content))
.expect("write memory");
}
exporter.finalize_records_root()
}
#[test]
fn records_root_counts_and_distinguishes_memories() {
let (root_two, count_two) = records_root_over(&[("mem-a", "one"), ("mem-b", "two")]);
let (root_one, count_one) = records_root_over(&[("mem-a", "one")]);
assert_eq!(count_two, 2);
assert_eq!(count_one, 1);
assert_ne!(root_two, root_one, "dropping a memory must change the root");
}
#[test]
fn records_root_is_deterministic_across_exporters() {
let first = records_root_over(&[("mem-a", "one"), ("mem-b", "two")]);
let second = records_root_over(&[("mem-a", "one"), ("mem-b", "two")]);
assert_eq!(first, second, "identical exports must yield the same root");
}
#[test]
fn records_root_is_order_sensitive() {
let (forward, _) = records_root_over(&[("mem-a", "one"), ("mem-b", "two")]);
let (reversed, _) = records_root_over(&[("mem-b", "two"), ("mem-a", "one")]);
assert_ne!(
forward, reversed,
"reordering memories must change the root"
);
}
#[test]
fn records_root_reflects_content_edits() {
let (original, _) = records_root_over(&[("mem-a", "one")]);
let (edited, _) = records_root_over(&[("mem-a", "one-edited")]);
assert_ne!(
original, edited,
"editing a memory body must change the root"
);
}
#[test]
fn jsonl_exporter_writes_artifact_with_redaction() -> TestResult {
let mut output = Vec::new();
let secret_fixture = format!("api_{}={}", "key", "redaction-fixture");
let artifact = ExportArtifactRecord::builder()
.artifact_id("art_01234567890123456789012345")
.workspace_id("wsp_01234567890123456789012345")
.source_kind("file")
.artifact_type("log")
.canonical_path("/data/projects/example/logs/build.log")
.external_ref(format!("/Users/example/private/{secret_fixture}.log"))
.provenance_uri("file:///Volumes/USBNVME16TB/private/support.json")
.content_hash("blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.media_type("text/plain")
.size_bytes(42)
.redaction_status("checked")
.snippet(secret_fixture.clone())
.created_at("2026-04-30T12:00:00Z")
.updated_at("2026-04-30T12:00:00Z")
.build()
.expect("artifact has required fields");
let artifact_count = {
let mut exporter =
JsonlExporter::new(&mut output, RedactionLevel::Standard, ExportScope::All);
exporter
.write_artifact(artifact)
.map_err(|error| format!("write artifact: {error}"))?;
exporter.artifact_count
};
let written = String::from_utf8(output).map_err(|error| format!("valid utf8: {error}"))?;
assert!(written.contains(EXPORT_ARTIFACT_SCHEMA_V1));
assert!(written.contains(REDACTED_PLACEHOLDER));
assert!(written.contains(REDACTED_PATH_PLACEHOLDER));
assert!(!written.contains(&secret_fixture));
assert!(!written.contains("/Users/example/private"));
assert!(!written.contains("/Volumes/USBNVME16TB/private"));
ensure(artifact_count, 1, "artifact count")
}
#[test]
fn jsonl_exporter_minimal_redacts_secret_artifact_external_ref() -> TestResult {
let mut output = Vec::new();
let external_ref = "https://example.invalid/artifacts?api_key=redaction-fixture";
let artifact = ExportArtifactRecord::builder()
.artifact_id("art_minimal_external_ref")
.workspace_id("wsp_minimal_external_ref")
.source_kind("file")
.artifact_type("log")
.external_ref(external_ref)
.content_hash("blake3:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
.media_type("text/plain")
.size_bytes(42)
.redaction_status("checked")
.created_at("2026-04-30T12:00:00Z")
.updated_at("2026-04-30T12:00:00Z")
.build()
.map_err(|error| format!("build artifact: {error}"))?;
let artifact_count = {
let mut exporter =
JsonlExporter::new(&mut output, RedactionLevel::Minimal, ExportScope::All);
exporter
.write_artifact(artifact)
.map_err(|error| format!("write artifact: {error}"))?;
exporter.artifact_count
};
let written = String::from_utf8(output).map_err(|error| format!("valid utf8: {error}"))?;
ensure(artifact_count, 1, "artifact count")?;
assert!(written.contains(REDACTED_PLACEHOLDER));
assert!(!written.contains("redaction-fixture"));
assert!(!written.contains(external_ref));
Ok(())
}
#[test]
fn jsonl_exporter_standard_redacts_mesh_artifact_metadata_identifiers() -> TestResult {
let mut output = Vec::new();
let origin_workspace = "/Users/example/private/artifact-workspace";
let producer_peer = "nodekey:fedcba9876543210fedcba9876543210";
let import_decision = "mesh_artifact_decision_fedcba9876543210";
let policy_ref = "mesh_artifact_policy_fedcba9876543210";
let artifact = ExportArtifactRecord::builder()
.artifact_id("art_mesh_artifact_redaction")
.workspace_id("wsp_mesh_artifact_redaction")
.source_kind("mesh")
.artifact_type("support")
.canonical_path("/data/projects/example/private/support.json")
.content_hash("blake3:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210")
.media_type("application/json")
.size_bytes(128)
.redaction_status("checked")
.metadata(serde_json::json!({
"mesh": {
"workspaceScopeDecision": "allow",
"originWorkspaceAlias": origin_workspace,
"producerPeer": producer_peer,
"importDecisionId": import_decision,
"policyDecision": {
"schema": "ee.mesh.policy_decision.v1",
"direction": "inbound",
"action": "allow",
"policyRef": policy_ref,
"bodyFetchAllowed": false
}
},
"source": "agent"
}))
.created_at("2026-04-30T12:00:00Z")
.updated_at("2026-04-30T12:00:00Z")
.build()
.map_err(|error| format!("build artifact: {error}"))?;
let artifact_count = {
let mut exporter =
JsonlExporter::new(&mut output, RedactionLevel::Standard, ExportScope::All);
exporter
.write_artifact(artifact)
.map_err(|error| format!("write artifact: {error}"))?;
exporter.artifact_count
};
let written = String::from_utf8(output).map_err(|error| format!("valid utf8: {error}"))?;
ensure(artifact_count, 1, "artifact count")?;
assert!(written.contains(REDACTED_PATH_PLACEHOLDER));
assert!(!written.contains(origin_workspace));
assert!(!written.contains(producer_peer));
assert!(!written.contains(import_decision));
assert!(!written.contains(policy_ref));
assert!(written.contains(r#""source":"agent""#));
assert!(written.contains(r#""workspaceScopeDecision":"allow""#));
Ok(())
}
#[test]
fn jsonl_exporter_standard_redacts_mesh_link_metadata_identifiers() -> TestResult {
let mut output = Vec::new();
let origin_workspace = "/Users/example/private/mesh-workspace";
let producer_peer = "nodekey:0123456789abcdef0123456789abcdef";
let import_decision = "mesh_decision_0123456789abcdef";
let policy_ref = "mesh_policy_0123456789abcdef";
let link = ExportLinkRecord::builder()
.link_id("link_0123456789abcdef")
.source_memory_id("mem_source_0123456789abcdef")
.target_memory_id("mem_target_0123456789abcdef")
.link_type("supports")
.created_at("2026-04-30T12:00:00Z")
.metadata(serde_json::json!({
"mesh": {
"workspaceScopeDecision": "allow",
"originWorkspaceAlias": origin_workspace,
"producerPeer": producer_peer,
"importDecisionId": import_decision,
"policyDecision": {
"schema": "ee.mesh.policy_decision.v1",
"direction": "inbound",
"action": "allow",
"policyRef": policy_ref,
"bodyFetchAllowed": false
}
},
"source": "agent"
}))
.build()
.map_err(|error| format!("build link: {error}"))?;
let link_count = {
let mut exporter =
JsonlExporter::new(&mut output, RedactionLevel::Standard, ExportScope::All);
exporter
.write_link(link)
.map_err(|error| format!("write link: {error}"))?;
exporter.link_count
};
let written = String::from_utf8(output).map_err(|error| format!("valid utf8: {error}"))?;
ensure(link_count, 1, "link count")?;
assert!(written.contains(REDACTED_PATH_PLACEHOLDER));
assert!(!written.contains(origin_workspace));
assert!(!written.contains(producer_peer));
assert!(!written.contains(import_decision));
assert!(!written.contains(policy_ref));
assert!(written.contains(r#""source":"agent""#));
assert!(written.contains(r#""workspaceScopeDecision":"allow""#));
Ok(())
}
#[test]
fn jsonl_exporter_standard_redacts_string_mesh_policy_surfaces() -> TestResult {
let mut output = Vec::new();
let origin_workspace = "/Users/example/private/string-policy-workspace";
let producer_peer = "nodekey:abcdef0123456789abcdef0123456789";
let policy_ref = "mesh_policy_string_abcdef0123456789";
let failure_policy_ref = "mesh_failure_policy_abcdef0123456789";
let policy_decision_json = serde_json::json!({
"schema": "ee.mesh.policy_decision.v1",
"direction": "inbound",
"action": "allow",
"policyRef": policy_ref,
"producerPeer": producer_peer,
"originWorkspaceAlias": origin_workspace
})
.to_string();
let policy_failure_json = serde_json::json!({
"schema": "ee.mesh.policy_failure_surface.v1",
"code": "mesh_peer_policy_denied",
"action": "deny",
"policyRef": failure_policy_ref,
"originWorkspaceAlias": origin_workspace
})
.to_string();
let link = ExportLinkRecord::builder()
.link_id("link_string_policy_redaction")
.source_memory_id("mem_source_string_policy_redaction")
.target_memory_id("mem_target_string_policy_redaction")
.link_type("supports")
.created_at("2026-04-30T12:00:00Z")
.metadata(serde_json::json!({
"mesh": {
"workspaceScopeDecision": "allow",
"policyDecisionJson": policy_decision_json,
"policyFailureSurfaceJson": policy_failure_json
},
"source": "agent"
}))
.build()
.map_err(|error| format!("build link: {error}"))?;
let link_count = {
let mut exporter =
JsonlExporter::new(&mut output, RedactionLevel::Standard, ExportScope::All);
exporter
.write_link(link)
.map_err(|error| format!("write link: {error}"))?;
exporter.link_count
};
let written = String::from_utf8(output).map_err(|error| format!("valid utf8: {error}"))?;
ensure(link_count, 1, "link count")?;
assert!(written.contains(REDACTED_PATH_PLACEHOLDER));
assert!(!written.contains(origin_workspace));
assert!(!written.contains(producer_peer));
assert!(!written.contains(policy_ref));
assert!(!written.contains(failure_policy_ref));
assert!(written.contains(r#""source":"agent""#));
assert!(written.contains(r#""workspaceScopeDecision":"allow""#));
Ok(())
}
#[test]
fn jsonl_exporter_respects_scope() {
let mut output = Vec::new();
let memory = ExportMemoryRecord::builder()
.memory_id("mem-001")
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content("Test content")
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields");
let memory_count = {
let mut exporter =
JsonlExporter::new(&mut output, RedactionLevel::None, ExportScope::Audit);
exporter.write_memory(memory).expect("write memory");
exporter.memory_count
};
let written = String::from_utf8(output).expect("valid utf8");
assert!(written.is_empty());
assert_eq!(memory_count, 0);
}
#[test]
fn jsonl_exporter_metadata_only_strips_content() {
let mut output = Vec::new();
let mut exporter =
JsonlExporter::new(&mut output, RedactionLevel::None, ExportScope::MetadataOnly);
let memory = ExportMemoryRecord::builder()
.memory_id("mem-001")
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content("Sensitive content here")
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields");
exporter.write_memory(memory).expect("write memory");
let written = String::from_utf8(output).expect("valid utf8");
assert!(written.contains(EXPORT_MEMORY_SCHEMA_V1));
assert!(!written.contains("Sensitive content here"));
assert!(written.contains(r#""content":"""#));
}
#[test]
fn jsonl_exporter_applies_redaction() {
let mut output = Vec::new();
let mut exporter =
JsonlExporter::new(&mut output, RedactionLevel::Minimal, ExportScope::All);
let memory = ExportMemoryRecord::builder()
.memory_id("mem-001")
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content(secret_assignment("redaction-fixture"))
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields");
exporter.write_memory(memory).expect("write memory");
let written = String::from_utf8(output).expect("valid utf8");
assert!(written.contains(REDACTED_PLACEHOLDER));
assert!(!written.contains("redaction-fixture"));
}
#[test]
fn jsonl_exporter_footer_includes_counts() {
let mut output = Vec::new();
let mut exporter = JsonlExporter::new(&mut output, RedactionLevel::None, ExportScope::All);
let header = ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.ee_version("0.1.0")
.export_id("test-export")
.build()
.expect("header has required fields");
exporter.write_header(header).expect("write header");
for i in 0..3 {
let memory = ExportMemoryRecord::builder()
.memory_id(format!("mem-{i:03}"))
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content(format!("Content {i}"))
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields");
exporter.write_memory(memory).expect("write memory");
}
let artifact = ExportArtifactRecord::builder()
.artifact_id("art-001")
.workspace_id("ws-123")
.source_kind("file")
.artifact_type("log")
.content_hash("blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.media_type("text/plain")
.size_bytes(42)
.redaction_status("checked")
.created_at("2026-04-30T12:00:00Z")
.updated_at("2026-04-30T12:00:00Z")
.build()
.expect("artifact has required fields");
exporter.write_artifact(artifact).expect("write artifact");
let footer = ExportFooter::builder()
.export_id("test-export")
.completed_at("2026-04-30T12:01:00Z")
.build()
.expect("footer has required fields");
let stats = exporter.write_footer(footer).expect("write footer");
assert_eq!(stats.memory_count, 3);
assert_eq!(stats.artifact_count, 1);
assert_eq!(stats.total_records, 6);
let written = String::from_utf8(output).expect("valid utf8");
let footer_json = written.lines().last().expect("footer line");
let footer: ExportFooter = serde_json::from_str(footer_json).expect("footer parses");
assert_eq!(footer.total_records, stats.total_records);
assert_eq!(footer.memory_count, stats.memory_count);
assert_eq!(footer.artifact_count, stats.artifact_count);
}
#[test]
fn fully_redacted_tags_remain_valid_and_distinct() {
for level in [RedactionLevel::Paranoid, RedactionLevel::Full] {
let mut aliases = std::collections::BTreeSet::new();
for tag in ["private-release", "private-deploy"] {
let record = ExportTagRecord::builder()
.memory_id("mem_00000000000000000000000002")
.tag(tag)
.created_at("2026-09-08T00:00:00Z")
.build()
.expect("valid export tag");
let redacted = redact_tag_record(record.clone(), level);
let parsed = crate::models::Tag::parse(&redacted.tag)
.expect("redacted tag must remain importable");
assert_eq!(parsed.as_str(), redacted.tag);
assert!(!redacted.tag.contains("private"));
assert_eq!(redact_tag_record(record, level), redacted);
assert!(
aliases.insert(redacted.tag),
"distinct tags must stay distinct"
);
}
}
}
#[test]
fn redact_record_union() -> TestResult {
let content = secret_assignment("redaction-fixture");
let memory = ExportRecord::Memory(Box::new(
ExportMemoryRecord::builder()
.memory_id("mem-001")
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content(content)
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields"),
));
let redacted = redact_record(memory, RedactionLevel::Minimal);
if let ExportRecord::Memory(m) = redacted {
let record = *m;
return ensure(
record.content,
REDACTED_PLACEHOLDER.to_owned(),
"memory content redacted",
);
}
Err("expected memory variant".to_owned())
}
}