Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cora.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ ignore:
- "yarn.lock"
- "vendor/"
- "node_modules/"
# Test vectors of the privacy filter (#1464): inputs are secret-shaped on
# purpose (split into fragments, never a real credential).
- "docs/privacy-filter-cases.json"
rules:
- "Hardcoded password or secret"
- "Command injection via exec/system with dynamic input"
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### Changed

- Internal: `docs/privacy-filter-cases.json`, the shared test vectors of the privacy filter (35 cases: every kind and 23 inputs that must stay untouched; inputs are split into fragments so no complete token appears, and the file is excluded from Cora review in `.cora.yaml` because its secret-shaped assignments are the point of the test). `uteke-core` pins every case, and uteke-cloud pins parity against the same file (uteke-cloud#218). Part 2a of #1464.
- Internal: `memories.author`, part 1 of #1427 phase 2 (schema v22): an optional, free-text author on the memory itself, usable without a room. The first open by a build with this change migrates the store in place (additive `ALTER TABLE`; a memory that belongs to exactly one room takes that room link's author, the rest stay unset). `Memory` gains `author` (omitted from JSON when unset) and `Uteke::set_author` validates it (trimmed, 1 to 128 characters, no control characters). No HTTP, MCP or CLI input accepts it yet: that follows once uteke-cloud#219 is answered.
- Internal: privacy filter detection module, part 1 of #1464 (`uteke_core::privacy`): `redact` masks AWS access keys, GitHub tokens, `sk-` API keys, bearer headers, JWTs, PEM private key blocks and long secret-named assignments (`API_KEY=...`, with an entropy check and exceptions for hashes, UUIDs, paths and placeholders), keeping the shape (`ghp_[REDACTED:github-token]`), with an allowlist for false positives. Nothing calls it yet: it is wired into the write paths, behind `[privacy] mode`, in part 2, after the cloud contract answer (uteke-cloud#218).
- Internal: scope-isolation regression gate, parts 1 and 2 (#1467): the HTTP suite runs 20 scoped read requests through `route` (every recall strategy, search, list, recent, export, graph, room, orphans, tags, stats, doc) and a test fails when a read route is added to the registry without an isolation check or a documented exemption. Part 1: a two-namespace fixture with overlapping vocabulary and a core suite asserting that `recall` (every strategy), unified recall, `search`, `list`, tags, export, room recall and graph views never return a record from another namespace. The fixture ships behind the `test-support` feature of `uteke-core` for the server, MCP and CLI suites that follow.
Expand Down
48 changes: 48 additions & 0 deletions crates/uteke-core/src/privacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,54 @@ mod tests {
}
}

/// `docs/privacy-filter-cases.json` is the shared test-vector file uteke-cloud
/// pins parity against (uteke-cloud#218): every case must hold here too.
#[test]
fn the_shared_cases_file_matches_the_implementation() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../docs/privacy-filter-cases.json"
);
let doc: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(path).expect("cases file")).unwrap();
assert_eq!(doc["version"], 1);
let cases = doc["cases"].as_array().unwrap();
assert!(cases.len() >= 30, "only {} cases", cases.len());
let mut masked = 0;
for case in cases {
let name = case["name"].as_str().unwrap();
let input: String = case["input"]
.as_array()
.unwrap()
.iter()
.map(|f| f.as_str().unwrap())
.collect();
let r = redact(&input, &[]);
assert_eq!(r.text, case["expected"].as_str().unwrap(), "{name}");
let kinds: Vec<(String, u64)> = r
.redactions
.iter()
.map(|x| (x.kind.to_string(), x.count as u64))
.collect();
let expected: Vec<(String, u64)> = case["kinds"]
.as_array()
.unwrap()
.iter()
.map(|k| (k[0].as_str().unwrap().to_string(), k[1].as_u64().unwrap()))
.collect();
assert_eq!(kinds, expected, "{name}");
if !expected.is_empty() {
masked += 1;
}
// The expected text is a fixed point of the filter.
assert!(redact(&r.text, &[]).is_clean(), "{name}: not idempotent");
}
assert!(
masked >= 10,
"the file must exercise every kind, not only clean text"
);
}

#[test]
fn entropy_matches_known_values() {
assert_eq!(entropy("aaaa"), 0.0);
Expand Down
Loading
Loading