clawpowers 2.2.5 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +186 -160
  2. package/COMPATIBILITY.md +48 -13
  3. package/KNOWN_LIMITATIONS.md +20 -19
  4. package/LICENSE +44 -44
  5. package/LICENSING.md +10 -10
  6. package/README.md +486 -462
  7. package/SECURITY.md +52 -52
  8. package/dist/index.d.ts +17 -5
  9. package/dist/index.js +187 -92
  10. package/dist/index.js.map +1 -1
  11. package/native/Cargo.lock +4927 -4927
  12. package/native/Cargo.toml +73 -73
  13. package/native/crates/canonical/Cargo.toml +24 -24
  14. package/native/crates/canonical/src/lib.rs +677 -673
  15. package/native/crates/compression/Cargo.toml +20 -20
  16. package/native/crates/compression/benches/compression_bench.rs +42 -42
  17. package/native/crates/compression/src/lib.rs +393 -393
  18. package/native/crates/evm-eth/Cargo.toml +13 -13
  19. package/native/crates/evm-eth/src/lib.rs +105 -105
  20. package/native/crates/fee/Cargo.toml +15 -15
  21. package/native/crates/fee/src/lib.rs +281 -281
  22. package/native/crates/index/Cargo.toml +16 -16
  23. package/native/crates/index/src/lib.rs +277 -277
  24. package/native/crates/policy/Cargo.toml +17 -17
  25. package/native/crates/policy/src/lib.rs +614 -614
  26. package/native/crates/security/Cargo.toml +22 -22
  27. package/native/crates/security/src/lib.rs +478 -478
  28. package/native/crates/tokens/Cargo.toml +13 -13
  29. package/native/crates/tokens/src/lib.rs +534 -534
  30. package/native/crates/verification/Cargo.toml +23 -23
  31. package/native/crates/verification/src/lib.rs +333 -333
  32. package/native/crates/wallet/Cargo.toml +20 -20
  33. package/native/crates/wallet/src/lib.rs +261 -261
  34. package/native/crates/x402/Cargo.toml +30 -30
  35. package/native/crates/x402/src/lib.rs +423 -423
  36. package/native/ffi/Cargo.toml +34 -34
  37. package/native/ffi/build.rs +4 -4
  38. package/native/ffi/src/lib.rs +352 -352
  39. package/native/ffi/tests/integration.rs +354 -354
  40. package/native/pyo3/Cargo.toml +26 -26
  41. package/native/pyo3/pyproject.toml +16 -16
  42. package/native/pyo3/src/lib.rs +407 -407
  43. package/native/pyo3/tests/test_smoke.py +180 -180
  44. package/native/wasm/Cargo.toml +47 -44
  45. package/native/wasm/pkg/.gitignore +6 -6
  46. package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -208
  47. package/native/wasm/pkg/clawpowers_wasm.js +872 -872
  48. package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -40
  49. package/native/wasm/pkg/package.json +16 -16
  50. package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -143
  51. package/native/wasm/pkg-node/clawpowers_wasm.js +798 -798
  52. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -40
  53. package/native/wasm/pkg-node/package.json +12 -12
  54. package/native/wasm/src/lib.rs +433 -433
  55. package/package.json +13 -8
  56. package/scripts/build-wasm.mjs +59 -0
  57. package/scripts/generate_hermes_wrappers.py +211 -0
  58. package/scripts/hermes_wrapper_overrides.json +184 -0
  59. package/scripts/run-python-script.mjs +48 -0
  60. package/scripts/verify-consumer-install.mjs +109 -0
  61. package/scripts/verify-wasm-artifacts.mjs +26 -3
  62. package/scripts/verify_hermes_wrappers.py +154 -0
  63. package/skill.json +20 -0
  64. package/skills/1password/SKILL.md +34 -0
  65. package/skills/README.md +44 -0
  66. package/skills/agent-nexus-2/SKILL.md +34 -0
  67. package/skills/apple-notes/SKILL.md +34 -0
  68. package/skills/apple-reminders/SKILL.md +34 -0
  69. package/skills/autoresearch/SKILL.md +43 -0
  70. package/skills/bear-notes/SKILL.md +34 -0
  71. package/skills/blogwatcher/SKILL.md +34 -0
  72. package/skills/blucli/SKILL.md +34 -0
  73. package/skills/bluebubbles/SKILL.md +34 -0
  74. package/skills/business-strategy/SKILL.md +41 -0
  75. package/skills/camsnap/SKILL.md +34 -0
  76. package/skills/canvas/SKILL.md +34 -0
  77. package/skills/clawhub/SKILL.md +34 -0
  78. package/skills/coding-agent/SKILL.md +34 -0
  79. package/skills/coding-discipline.skill/SKILL.md +34 -0
  80. package/skills/content-writer/SKILL.md +41 -0
  81. package/skills/discord/SKILL.md +34 -0
  82. package/skills/eightctl/SKILL.md +34 -0
  83. package/skills/execution-validation.skill/SKILL.md +34 -0
  84. package/skills/gemini/SKILL.md +34 -0
  85. package/skills/gh-issues/SKILL.md +34 -0
  86. package/skills/gifgrep/SKILL.md +34 -0
  87. package/skills/github/SKILL.md +41 -0
  88. package/skills/gog/SKILL.md +34 -0
  89. package/skills/goplaces/SKILL.md +34 -0
  90. package/skills/healthcheck/SKILL.md +34 -0
  91. package/skills/himalaya/SKILL.md +34 -0
  92. package/skills/humanize/SKILL.md +41 -0
  93. package/skills/imsg/SKILL.md +34 -0
  94. package/skills/itp/SKILL.md +112 -0
  95. package/skills/mcporter/SKILL.md +34 -0
  96. package/skills/model-usage/SKILL.md +34 -0
  97. package/skills/nano-pdf/SKILL.md +34 -0
  98. package/skills/node-connect/SKILL.md +34 -0
  99. package/skills/notion/SKILL.md +34 -0
  100. package/skills/obsidian/SKILL.md +34 -0
  101. package/skills/openai-whisper/SKILL.md +34 -0
  102. package/skills/openai-whisper-api/SKILL.md +34 -0
  103. package/skills/openhue/SKILL.md +34 -0
  104. package/skills/oracle/SKILL.md +34 -0
  105. package/skills/ordercli/SKILL.md +34 -0
  106. package/skills/peekaboo/SKILL.md +34 -0
  107. package/skills/polyclaw/SKILL.md +34 -0
  108. package/skills/prospector/SKILL.md +41 -0
  109. package/skills/rsi.skill/SKILL.md +34 -0
  110. package/skills/sag/SKILL.md +34 -0
  111. package/skills/security/SKILL.md +41 -0
  112. package/skills/session-logs/SKILL.md +34 -0
  113. package/skills/sherpa-onnx-tts/SKILL.md +34 -0
  114. package/skills/skill-creator/SKILL.md +34 -0
  115. package/skills/slack/SKILL.md +34 -0
  116. package/skills/songsee/SKILL.md +34 -0
  117. package/skills/sonoscli/SKILL.md +34 -0
  118. package/skills/spotify-player/SKILL.md +34 -0
  119. package/skills/strykr-prism/SKILL.md +41 -0
  120. package/skills/summarize/SKILL.md +34 -0
  121. package/skills/taskbridge/SKILL.md +34 -0
  122. package/skills/things-mac/SKILL.md +34 -0
  123. package/skills/tmux/SKILL.md +34 -0
  124. package/skills/trello/SKILL.md +34 -0
  125. package/skills/validator-agent/SKILL.md +41 -0
  126. package/skills/video-frames/SKILL.md +34 -0
  127. package/skills/voice-call/SKILL.md +34 -0
  128. package/skills/wacli/SKILL.md +34 -0
  129. package/skills/weather/SKILL.md +34 -0
  130. package/skills/webmcp-payments/SKILL.md +41 -0
  131. package/skills/xurl/SKILL.md +34 -0
  132. package/src/skills/catalog.ts +435 -435
  133. package/src/skills/executor.ts +56 -56
  134. package/src/skills/index.ts +3 -3
  135. package/src/skills/itp/SKILL.md +112 -112
  136. package/src/skills/loader.ts +262 -193
  137. package/native/ffi/index.node +0 -0
  138. package/native/wasm/pkg-node/.gitignore +0 -6
@@ -1,333 +1,333 @@
1
- //! Exact verification pipeline for TurboMemory.
2
- //!
3
- //! The [`VerificationPipeline`] fetches records from a [`CanonicalStore`],
4
- //! recomputes content hashes, and checks TTL / quarantine metadata before
5
- //! returning a typed [`VerificationResult`].
6
-
7
- use chrono::{Duration, Utc};
8
- use clawpowers_canonical::{CanonicalRecord, CanonicalStore, compute_sha256};
9
- use thiserror::Error;
10
- use uuid::Uuid;
11
-
12
- /// Errors returned by the verification pipeline.
13
- #[derive(Debug, Error)]
14
- pub enum VerificationError {
15
- /// Underlying canonical store error.
16
- #[error("store error: {0}")]
17
- Store(#[from] clawpowers_canonical::CanonicalError),
18
- }
19
-
20
- /// A shorthand result type for [`VerificationError`].
21
- pub type Result<T> = std::result::Result<T, VerificationError>;
22
-
23
- // ─── Verification Result ─────────────────────────────────────────────────────
24
-
25
- /// The outcome of verifying a single record.
26
- #[derive(Debug)]
27
- pub enum VerificationResult {
28
- /// Record is present, hash is valid, not expired, and not quarantined.
29
- Verified(CanonicalRecord),
30
-
31
- /// The stored hash does not match the recomputed hash.
32
- IntegrityFailed {
33
- /// Record identifier.
34
- id: Uuid,
35
- /// Hash value stored in the database.
36
- expected_hash: String,
37
- /// Hash value computed from the stored content.
38
- actual_hash: String,
39
- },
40
-
41
- /// The record has a `ttl_seconds` metadata field and the record has
42
- /// outlived it.
43
- Expired {
44
- /// Record identifier.
45
- id: Uuid,
46
- /// How far past the TTL deadline the record is.
47
- ttl_exceeded_by: Duration,
48
- },
49
-
50
- /// The record has `"quarantined": true` in its metadata.
51
- Quarantined {
52
- /// Record identifier.
53
- id: Uuid,
54
- /// Reason, extracted from `metadata["quarantine_reason"]` if present.
55
- reason: String,
56
- },
57
-
58
- /// No record with the given id exists in the store.
59
- NotFound(Uuid),
60
- }
61
-
62
- // ─── Pipeline ────────────────────────────────────────────────────────────────
63
-
64
- /// Verification pipeline wrapping a [`CanonicalStore`].
65
- pub struct VerificationPipeline {
66
- store: CanonicalStore,
67
- }
68
-
69
- impl VerificationPipeline {
70
- /// Create a new pipeline backed by `store`.
71
- pub fn new(store: CanonicalStore) -> Self {
72
- Self { store }
73
- }
74
-
75
- /// Access the underlying canonical store.
76
- pub fn store(&self) -> &CanonicalStore {
77
- &self.store
78
- }
79
-
80
- // ── Single Verification ───────────────────────────────────────────────
81
-
82
- /// Verify a single record identified by `id`.
83
- ///
84
- /// Steps:
85
- /// 1. Fetch — [`VerificationResult::NotFound`] if absent.
86
- /// 2. Hash check — [`VerificationResult::IntegrityFailed`] on mismatch.
87
- /// 3. TTL check — [`VerificationResult::Expired`] if past `metadata["ttl_seconds"]`.
88
- /// 4. Quarantine check — [`VerificationResult::Quarantined`] if
89
- /// `metadata["quarantined"] == true`.
90
- /// 5. Otherwise [`VerificationResult::Verified`].
91
- pub fn verify(&self, id: &Uuid) -> Result<VerificationResult> {
92
- // Step 1: Fetch.
93
- let record = match self.store.get(id)? {
94
- None => return Ok(VerificationResult::NotFound(*id)),
95
- Some(r) => r,
96
- };
97
-
98
- // Step 2: Hash check.
99
- let actual_hash = compute_sha256(&record.content);
100
- if actual_hash != record.content_hash {
101
- return Ok(VerificationResult::IntegrityFailed {
102
- id: *id,
103
- expected_hash: record.content_hash.clone(),
104
- actual_hash,
105
- });
106
- }
107
-
108
- // Step 3: TTL check.
109
- if let Some(ttl_val) = record.metadata.get("ttl_seconds")
110
- && let Some(ttl_secs) = ttl_val.as_i64()
111
- {
112
- let deadline = record.created_at + Duration::seconds(ttl_secs);
113
- let now = Utc::now();
114
- if now > deadline {
115
- let exceeded_by = now - deadline;
116
- return Ok(VerificationResult::Expired {
117
- id: *id,
118
- ttl_exceeded_by: exceeded_by,
119
- });
120
- }
121
- }
122
-
123
- // Step 4: Quarantine check.
124
- if record
125
- .metadata
126
- .get("quarantined")
127
- .and_then(|v| v.as_bool())
128
- .unwrap_or(false)
129
- {
130
- let reason = record
131
- .metadata
132
- .get("quarantine_reason")
133
- .and_then(|v| v.as_str())
134
- .unwrap_or("unknown")
135
- .to_string();
136
- return Ok(VerificationResult::Quarantined { id: *id, reason });
137
- }
138
-
139
- Ok(VerificationResult::Verified(record))
140
- }
141
-
142
- // ── Batch Verification ────────────────────────────────────────────────
143
-
144
- /// Verify a batch of records; each id is verified independently.
145
- pub fn verify_batch(&self, ids: &[Uuid]) -> Result<Vec<VerificationResult>> {
146
- ids.iter().map(|id| self.verify(id)).collect()
147
- }
148
- }
149
-
150
- // ─── Tests ───────────────────────────────────────────────────────────────────
151
-
152
- #[cfg(test)]
153
- mod tests {
154
- use super::*;
155
- use clawpowers_canonical::{CanonicalRecord, CanonicalStore};
156
- use serde_json::json;
157
-
158
- fn make_pipeline() -> VerificationPipeline {
159
- let store = CanonicalStore::in_memory().expect("in-memory store");
160
- VerificationPipeline::new(store)
161
- }
162
-
163
- fn insert(
164
- pipeline: &VerificationPipeline,
165
- namespace: &str,
166
- content: &str,
167
- metadata: serde_json::Value,
168
- ) -> Uuid {
169
- let rec = CanonicalRecord::new(namespace, content, None, metadata, "test");
170
- let id = rec.id;
171
- pipeline.store().insert(&rec).expect("insert");
172
- id
173
- }
174
-
175
- // ── Verified ─────────────────────────────────────────────────────────
176
-
177
- #[test]
178
- fn test_verify_valid_record() {
179
- let p = make_pipeline();
180
- let id = insert(&p, "ns", "hello world", json!({}));
181
- match p.verify(&id).expect("verify") {
182
- VerificationResult::Verified(r) => assert_eq!(r.id, id),
183
- other => panic!("expected Verified, got {other:?}"),
184
- }
185
- }
186
-
187
- #[test]
188
- fn test_verify_correct_hash_is_verified() {
189
- let p = make_pipeline();
190
- let id = insert(&p, "ns", "content for integrity check", json!({}));
191
- assert!(matches!(
192
- p.verify(&id).expect("verify"),
193
- VerificationResult::Verified(_)
194
- ));
195
- }
196
-
197
- // ── Not Found ─────────────────────────────────────────────────────────
198
-
199
- #[test]
200
- fn test_verify_not_found() {
201
- let p = make_pipeline();
202
- let missing = Uuid::new_v4();
203
- match p.verify(&missing).expect("verify") {
204
- VerificationResult::NotFound(id) => assert_eq!(id, missing),
205
- other => panic!("expected NotFound, got {other:?}"),
206
- }
207
- }
208
-
209
- // ── Integrity (via hash comparison) ───────────────────────────────────
210
-
211
- #[test]
212
- fn test_verify_integrity_passes_for_fresh_record() {
213
- let p = make_pipeline();
214
- let id = insert(&p, "ns", "fresh record content", json!({}));
215
- // Freshly inserted record should always pass hash check.
216
- assert!(matches!(
217
- p.verify(&id).expect("v"),
218
- VerificationResult::Verified(_)
219
- ));
220
- }
221
-
222
- #[test]
223
- fn test_integrity_check_uses_sha256() {
224
- let content = "check sha256 content";
225
- let expected_hash = compute_sha256(content);
226
- let p = make_pipeline();
227
- let rec = CanonicalRecord::new("ns", content, None, json!({}), "test");
228
- assert_eq!(rec.content_hash, expected_hash);
229
- let id = p.store().insert(&rec).expect("insert");
230
- assert!(matches!(
231
- p.verify(&id).expect("v"),
232
- VerificationResult::Verified(_)
233
- ));
234
- }
235
-
236
- // ── TTL ───────────────────────────────────────────────────────────────
237
-
238
- #[test]
239
- fn test_verify_expired_ttl() {
240
- let p = make_pipeline();
241
- let id = insert(&p, "ns", "old content", json!({"ttl_seconds": -1}));
242
- match p.verify(&id).expect("verify") {
243
- VerificationResult::Expired { id: eid, .. } => assert_eq!(eid, id),
244
- other => panic!("expected Expired, got {other:?}"),
245
- }
246
- }
247
-
248
- #[test]
249
- fn test_verify_future_ttl_is_valid() {
250
- let p = make_pipeline();
251
- let id = insert(&p, "ns", "fresh content", json!({"ttl_seconds": 3600}));
252
- assert!(matches!(
253
- p.verify(&id).expect("v"),
254
- VerificationResult::Verified(_)
255
- ));
256
- }
257
-
258
- #[test]
259
- fn test_no_ttl_field_is_valid() {
260
- let p = make_pipeline();
261
- let id = insert(&p, "ns", "no ttl here", json!({"source": "test"}));
262
- assert!(matches!(
263
- p.verify(&id).expect("v"),
264
- VerificationResult::Verified(_)
265
- ));
266
- }
267
-
268
- // ── Quarantine ────────────────────────────────────────────────────────
269
-
270
- #[test]
271
- fn test_verify_quarantined_with_reason() {
272
- let p = make_pipeline();
273
- let id = insert(
274
- &p,
275
- "ns",
276
- "suspicious content",
277
- json!({"quarantined": true, "quarantine_reason": "policy violation"}),
278
- );
279
- match p.verify(&id).expect("verify") {
280
- VerificationResult::Quarantined { id: qid, reason } => {
281
- assert_eq!(qid, id);
282
- assert_eq!(reason, "policy violation");
283
- }
284
- other => panic!("expected Quarantined, got {other:?}"),
285
- }
286
- }
287
-
288
- #[test]
289
- fn test_verify_quarantined_default_reason() {
290
- let p = make_pipeline();
291
- let id = insert(&p, "ns", "another bad record", json!({"quarantined": true}));
292
- match p.verify(&id).expect("verify") {
293
- VerificationResult::Quarantined { reason, .. } => assert_eq!(reason, "unknown"),
294
- other => panic!("expected Quarantined, got {other:?}"),
295
- }
296
- }
297
-
298
- // ── Batch ─────────────────────────────────────────────────────────────
299
-
300
- #[test]
301
- fn test_verify_batch_mixed_results() {
302
- let p = make_pipeline();
303
- let valid_id = insert(&p, "ns", "valid batch record", json!({}));
304
- let expired_id = insert(&p, "ns", "expired batch", json!({"ttl_seconds": -1}));
305
- let missing_id = Uuid::new_v4();
306
-
307
- let results = p
308
- .verify_batch(&[valid_id, expired_id, missing_id])
309
- .expect("batch");
310
- assert_eq!(results.len(), 3);
311
- assert!(matches!(results[0], VerificationResult::Verified(_)));
312
- assert!(matches!(results[1], VerificationResult::Expired { .. }));
313
- assert!(matches!(results[2], VerificationResult::NotFound(_)));
314
- }
315
-
316
- #[test]
317
- fn test_verify_batch_empty() {
318
- let p = make_pipeline();
319
- let results = p.verify_batch(&[]).expect("batch");
320
- assert!(results.is_empty());
321
- }
322
-
323
- #[test]
324
- fn test_verify_batch_all_not_found() {
325
- let p = make_pipeline();
326
- let ids = vec![Uuid::new_v4(), Uuid::new_v4()];
327
- let results = p.verify_batch(&ids).expect("batch");
328
- assert_eq!(results.len(), 2);
329
- for r in results {
330
- assert!(matches!(r, VerificationResult::NotFound(_)));
331
- }
332
- }
333
- }
1
+ //! Exact verification pipeline for TurboMemory.
2
+ //!
3
+ //! The [`VerificationPipeline`] fetches records from a [`CanonicalStore`],
4
+ //! recomputes content hashes, and checks TTL / quarantine metadata before
5
+ //! returning a typed [`VerificationResult`].
6
+
7
+ use chrono::{Duration, Utc};
8
+ use clawpowers_canonical::{CanonicalRecord, CanonicalStore, compute_sha256};
9
+ use thiserror::Error;
10
+ use uuid::Uuid;
11
+
12
+ /// Errors returned by the verification pipeline.
13
+ #[derive(Debug, Error)]
14
+ pub enum VerificationError {
15
+ /// Underlying canonical store error.
16
+ #[error("store error: {0}")]
17
+ Store(#[from] clawpowers_canonical::CanonicalError),
18
+ }
19
+
20
+ /// A shorthand result type for [`VerificationError`].
21
+ pub type Result<T> = std::result::Result<T, VerificationError>;
22
+
23
+ // ─── Verification Result ─────────────────────────────────────────────────────
24
+
25
+ /// The outcome of verifying a single record.
26
+ #[derive(Debug)]
27
+ pub enum VerificationResult {
28
+ /// Record is present, hash is valid, not expired, and not quarantined.
29
+ Verified(CanonicalRecord),
30
+
31
+ /// The stored hash does not match the recomputed hash.
32
+ IntegrityFailed {
33
+ /// Record identifier.
34
+ id: Uuid,
35
+ /// Hash value stored in the database.
36
+ expected_hash: String,
37
+ /// Hash value computed from the stored content.
38
+ actual_hash: String,
39
+ },
40
+
41
+ /// The record has a `ttl_seconds` metadata field and the record has
42
+ /// outlived it.
43
+ Expired {
44
+ /// Record identifier.
45
+ id: Uuid,
46
+ /// How far past the TTL deadline the record is.
47
+ ttl_exceeded_by: Duration,
48
+ },
49
+
50
+ /// The record has `"quarantined": true` in its metadata.
51
+ Quarantined {
52
+ /// Record identifier.
53
+ id: Uuid,
54
+ /// Reason, extracted from `metadata["quarantine_reason"]` if present.
55
+ reason: String,
56
+ },
57
+
58
+ /// No record with the given id exists in the store.
59
+ NotFound(Uuid),
60
+ }
61
+
62
+ // ─── Pipeline ────────────────────────────────────────────────────────────────
63
+
64
+ /// Verification pipeline wrapping a [`CanonicalStore`].
65
+ pub struct VerificationPipeline {
66
+ store: CanonicalStore,
67
+ }
68
+
69
+ impl VerificationPipeline {
70
+ /// Create a new pipeline backed by `store`.
71
+ pub fn new(store: CanonicalStore) -> Self {
72
+ Self { store }
73
+ }
74
+
75
+ /// Access the underlying canonical store.
76
+ pub fn store(&self) -> &CanonicalStore {
77
+ &self.store
78
+ }
79
+
80
+ // ── Single Verification ───────────────────────────────────────────────
81
+
82
+ /// Verify a single record identified by `id`.
83
+ ///
84
+ /// Steps:
85
+ /// 1. Fetch — [`VerificationResult::NotFound`] if absent.
86
+ /// 2. Hash check — [`VerificationResult::IntegrityFailed`] on mismatch.
87
+ /// 3. TTL check — [`VerificationResult::Expired`] if past `metadata["ttl_seconds"]`.
88
+ /// 4. Quarantine check — [`VerificationResult::Quarantined`] if
89
+ /// `metadata["quarantined"] == true`.
90
+ /// 5. Otherwise [`VerificationResult::Verified`].
91
+ pub fn verify(&self, id: &Uuid) -> Result<VerificationResult> {
92
+ // Step 1: Fetch.
93
+ let record = match self.store.get(id)? {
94
+ None => return Ok(VerificationResult::NotFound(*id)),
95
+ Some(r) => r,
96
+ };
97
+
98
+ // Step 2: Hash check.
99
+ let actual_hash = compute_sha256(&record.content);
100
+ if actual_hash != record.content_hash {
101
+ return Ok(VerificationResult::IntegrityFailed {
102
+ id: *id,
103
+ expected_hash: record.content_hash.clone(),
104
+ actual_hash,
105
+ });
106
+ }
107
+
108
+ // Step 3: TTL check.
109
+ if let Some(ttl_val) = record.metadata.get("ttl_seconds")
110
+ && let Some(ttl_secs) = ttl_val.as_i64()
111
+ {
112
+ let deadline = record.created_at + Duration::seconds(ttl_secs);
113
+ let now = Utc::now();
114
+ if now > deadline {
115
+ let exceeded_by = now - deadline;
116
+ return Ok(VerificationResult::Expired {
117
+ id: *id,
118
+ ttl_exceeded_by: exceeded_by,
119
+ });
120
+ }
121
+ }
122
+
123
+ // Step 4: Quarantine check.
124
+ if record
125
+ .metadata
126
+ .get("quarantined")
127
+ .and_then(|v| v.as_bool())
128
+ .unwrap_or(false)
129
+ {
130
+ let reason = record
131
+ .metadata
132
+ .get("quarantine_reason")
133
+ .and_then(|v| v.as_str())
134
+ .unwrap_or("unknown")
135
+ .to_string();
136
+ return Ok(VerificationResult::Quarantined { id: *id, reason });
137
+ }
138
+
139
+ Ok(VerificationResult::Verified(record))
140
+ }
141
+
142
+ // ── Batch Verification ────────────────────────────────────────────────
143
+
144
+ /// Verify a batch of records; each id is verified independently.
145
+ pub fn verify_batch(&self, ids: &[Uuid]) -> Result<Vec<VerificationResult>> {
146
+ ids.iter().map(|id| self.verify(id)).collect()
147
+ }
148
+ }
149
+
150
+ // ─── Tests ───────────────────────────────────────────────────────────────────
151
+
152
+ #[cfg(test)]
153
+ mod tests {
154
+ use super::*;
155
+ use clawpowers_canonical::{CanonicalRecord, CanonicalStore};
156
+ use serde_json::json;
157
+
158
+ fn make_pipeline() -> VerificationPipeline {
159
+ let store = CanonicalStore::in_memory().expect("in-memory store");
160
+ VerificationPipeline::new(store)
161
+ }
162
+
163
+ fn insert(
164
+ pipeline: &VerificationPipeline,
165
+ namespace: &str,
166
+ content: &str,
167
+ metadata: serde_json::Value,
168
+ ) -> Uuid {
169
+ let rec = CanonicalRecord::new(namespace, content, None, metadata, "test");
170
+ let id = rec.id;
171
+ pipeline.store().insert(&rec).expect("insert");
172
+ id
173
+ }
174
+
175
+ // ── Verified ─────────────────────────────────────────────────────────
176
+
177
+ #[test]
178
+ fn test_verify_valid_record() {
179
+ let p = make_pipeline();
180
+ let id = insert(&p, "ns", "hello world", json!({}));
181
+ match p.verify(&id).expect("verify") {
182
+ VerificationResult::Verified(r) => assert_eq!(r.id, id),
183
+ other => panic!("expected Verified, got {other:?}"),
184
+ }
185
+ }
186
+
187
+ #[test]
188
+ fn test_verify_correct_hash_is_verified() {
189
+ let p = make_pipeline();
190
+ let id = insert(&p, "ns", "content for integrity check", json!({}));
191
+ assert!(matches!(
192
+ p.verify(&id).expect("verify"),
193
+ VerificationResult::Verified(_)
194
+ ));
195
+ }
196
+
197
+ // ── Not Found ─────────────────────────────────────────────────────────
198
+
199
+ #[test]
200
+ fn test_verify_not_found() {
201
+ let p = make_pipeline();
202
+ let missing = Uuid::new_v4();
203
+ match p.verify(&missing).expect("verify") {
204
+ VerificationResult::NotFound(id) => assert_eq!(id, missing),
205
+ other => panic!("expected NotFound, got {other:?}"),
206
+ }
207
+ }
208
+
209
+ // ── Integrity (via hash comparison) ───────────────────────────────────
210
+
211
+ #[test]
212
+ fn test_verify_integrity_passes_for_fresh_record() {
213
+ let p = make_pipeline();
214
+ let id = insert(&p, "ns", "fresh record content", json!({}));
215
+ // Freshly inserted record should always pass hash check.
216
+ assert!(matches!(
217
+ p.verify(&id).expect("v"),
218
+ VerificationResult::Verified(_)
219
+ ));
220
+ }
221
+
222
+ #[test]
223
+ fn test_integrity_check_uses_sha256() {
224
+ let content = "check sha256 content";
225
+ let expected_hash = compute_sha256(content);
226
+ let p = make_pipeline();
227
+ let rec = CanonicalRecord::new("ns", content, None, json!({}), "test");
228
+ assert_eq!(rec.content_hash, expected_hash);
229
+ let id = p.store().insert(&rec).expect("insert");
230
+ assert!(matches!(
231
+ p.verify(&id).expect("v"),
232
+ VerificationResult::Verified(_)
233
+ ));
234
+ }
235
+
236
+ // ── TTL ───────────────────────────────────────────────────────────────
237
+
238
+ #[test]
239
+ fn test_verify_expired_ttl() {
240
+ let p = make_pipeline();
241
+ let id = insert(&p, "ns", "old content", json!({"ttl_seconds": -1}));
242
+ match p.verify(&id).expect("verify") {
243
+ VerificationResult::Expired { id: eid, .. } => assert_eq!(eid, id),
244
+ other => panic!("expected Expired, got {other:?}"),
245
+ }
246
+ }
247
+
248
+ #[test]
249
+ fn test_verify_future_ttl_is_valid() {
250
+ let p = make_pipeline();
251
+ let id = insert(&p, "ns", "fresh content", json!({"ttl_seconds": 3600}));
252
+ assert!(matches!(
253
+ p.verify(&id).expect("v"),
254
+ VerificationResult::Verified(_)
255
+ ));
256
+ }
257
+
258
+ #[test]
259
+ fn test_no_ttl_field_is_valid() {
260
+ let p = make_pipeline();
261
+ let id = insert(&p, "ns", "no ttl here", json!({"source": "test"}));
262
+ assert!(matches!(
263
+ p.verify(&id).expect("v"),
264
+ VerificationResult::Verified(_)
265
+ ));
266
+ }
267
+
268
+ // ── Quarantine ────────────────────────────────────────────────────────
269
+
270
+ #[test]
271
+ fn test_verify_quarantined_with_reason() {
272
+ let p = make_pipeline();
273
+ let id = insert(
274
+ &p,
275
+ "ns",
276
+ "suspicious content",
277
+ json!({"quarantined": true, "quarantine_reason": "policy violation"}),
278
+ );
279
+ match p.verify(&id).expect("verify") {
280
+ VerificationResult::Quarantined { id: qid, reason } => {
281
+ assert_eq!(qid, id);
282
+ assert_eq!(reason, "policy violation");
283
+ }
284
+ other => panic!("expected Quarantined, got {other:?}"),
285
+ }
286
+ }
287
+
288
+ #[test]
289
+ fn test_verify_quarantined_default_reason() {
290
+ let p = make_pipeline();
291
+ let id = insert(&p, "ns", "another bad record", json!({"quarantined": true}));
292
+ match p.verify(&id).expect("verify") {
293
+ VerificationResult::Quarantined { reason, .. } => assert_eq!(reason, "unknown"),
294
+ other => panic!("expected Quarantined, got {other:?}"),
295
+ }
296
+ }
297
+
298
+ // ── Batch ─────────────────────────────────────────────────────────────
299
+
300
+ #[test]
301
+ fn test_verify_batch_mixed_results() {
302
+ let p = make_pipeline();
303
+ let valid_id = insert(&p, "ns", "valid batch record", json!({}));
304
+ let expired_id = insert(&p, "ns", "expired batch", json!({"ttl_seconds": -1}));
305
+ let missing_id = Uuid::new_v4();
306
+
307
+ let results = p
308
+ .verify_batch(&[valid_id, expired_id, missing_id])
309
+ .expect("batch");
310
+ assert_eq!(results.len(), 3);
311
+ assert!(matches!(results[0], VerificationResult::Verified(_)));
312
+ assert!(matches!(results[1], VerificationResult::Expired { .. }));
313
+ assert!(matches!(results[2], VerificationResult::NotFound(_)));
314
+ }
315
+
316
+ #[test]
317
+ fn test_verify_batch_empty() {
318
+ let p = make_pipeline();
319
+ let results = p.verify_batch(&[]).expect("batch");
320
+ assert!(results.is_empty());
321
+ }
322
+
323
+ #[test]
324
+ fn test_verify_batch_all_not_found() {
325
+ let p = make_pipeline();
326
+ let ids = vec![Uuid::new_v4(), Uuid::new_v4()];
327
+ let results = p.verify_batch(&ids).expect("batch");
328
+ assert_eq!(results.len(), 2);
329
+ for r in results {
330
+ assert!(matches!(r, VerificationResult::NotFound(_)));
331
+ }
332
+ }
333
+ }