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,354 +1,354 @@
1
- //! Integration tests for the clawpowers-core cross-crate pipeline.
2
- //!
3
- //! These tests exercise the full workflows that the napi-rs FFI bindings expose
4
- //! to JavaScript, but at the Rust level using the public crate APIs directly.
5
- //!
6
- //! Pipelines tested:
7
- //! 1. Wallet → Policy → Fee → x402 payment format
8
- //! 2. Canonical store → Compression → Index → Verification
9
- //! 3. Security firewall → Evaluate write request → Audit log
10
-
11
- // ─── Pipeline 1: wallet → policy → fee → x402 ───────────────────────────────
12
-
13
- #[cfg(test)]
14
- mod wallet_payment_pipeline {
15
- use clawpowers_fee::{FeeSchedule, FeeType};
16
- use clawpowers_policy::{PolicyDecision, ProposedTx, SpendingPolicy};
17
- use clawpowers_tokens::TokenAmount;
18
- use clawpowers_wallet::AgentWallet;
19
- use clawpowers_x402::{X402Client, X402PaymentRequired};
20
-
21
- /// Hard-coded Hardhat account #0 private key used for deterministic tests.
22
- const TEST_KEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
23
-
24
- fn usdc(amount: f64) -> TokenAmount {
25
- TokenAmount::from_human(amount, 6)
26
- }
27
-
28
- /// Build a complete x402 header from wallet signing.
29
- #[test]
30
- fn test_wallet_sign_then_x402_header() {
31
- // 1. Import wallet from known key.
32
- let wallet = AgentWallet::from_private_key(TEST_KEY).expect("valid test key");
33
- assert!(wallet.address().to_string().to_lowercase().contains("f39f"));
34
-
35
- // 2. Sign a payment descriptor.
36
- let descriptor = b"pay:USDC:1.00:8453:0xrecipient";
37
- let sig = wallet.sign_message(descriptor).expect("sign");
38
- assert_eq!(sig.as_bytes().len(), 65, "signature must be 65 bytes");
39
- let sig_hex = format!("0x{}", hex_encode(&sig.as_bytes()));
40
-
41
- // 3. Build x402 payment required struct.
42
- let requirement = X402PaymentRequired {
43
- payment_url: "https://pay.example.com/pay".to_string(),
44
- amount: "1.00".to_string(),
45
- token: "USDC".to_string(),
46
- chain_id: 8453,
47
- recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
48
- memo: None,
49
- };
50
-
51
- // 4. Format the X-Payment header.
52
- let header = X402Client::create_payment_header(&requirement, &sig_hex);
53
- assert!(header.contains("8453"), "chain_id must be in header");
54
- assert!(header.contains(&sig_hex), "signature must be in header");
55
- }
56
-
57
- /// Policy → fee → verify the fee calculation is consistent.
58
- #[test]
59
- fn test_policy_permits_then_fee_calculated() {
60
- // 1. Build a spending policy: max 500 USDC per tx.
61
- let policy = SpendingPolicy::builder()
62
- .max_per_tx(usdc(500.0))
63
- .fail_closed(true)
64
- .build();
65
-
66
- // 2. Define a proposed 100 USDC transaction.
67
- let tx = ProposedTx {
68
- recipient: "0x0000000000000000000000000000000000000001"
69
- .parse()
70
- .expect("valid address"),
71
- amount: usdc(100.0),
72
- merchant_allowlist_check: false,
73
- };
74
-
75
- // 3. Policy evaluation must approve.
76
- assert_eq!(policy.evaluate(&tx), PolicyDecision::Approve);
77
-
78
- // 4. Calculate 30 bps fee on the approved amount.
79
- let schedule = FeeSchedule::new(
80
- 30, // tx_bps
81
- 50, // swap_bps
82
- "0x0000000000000000000000000000000000000000"
83
- .parse()
84
- .expect("zero address"),
85
- );
86
- let calc = schedule
87
- .calculate(tx.amount, FeeType::Transaction)
88
- .expect("fee calculation");
89
-
90
- // 5. Invariant: gross = fee + net.
91
- let gross: f64 = calc.gross_amount.to_human();
92
- let fee: f64 = calc.fee_amount.to_human();
93
- let net: f64 = calc.net_amount.to_human();
94
- assert!((gross - fee - net).abs() < 1e-9, "gross != fee + net");
95
-
96
- // 6. 30 bps of 100 USDC = 0.3 USDC.
97
- assert!(
98
- (fee - 0.3).abs() < 0.000_001,
99
- "fee should be 0.3 USDC, got {fee}"
100
- );
101
- }
102
-
103
- /// Policy denies oversized transactions.
104
- #[test]
105
- fn test_policy_denies_over_limit() {
106
- let policy = SpendingPolicy::builder()
107
- .max_per_tx(usdc(100.0))
108
- .fail_closed(true)
109
- .build();
110
- let big_tx = ProposedTx {
111
- recipient: "0x0000000000000000000000000000000000000001"
112
- .parse()
113
- .expect("valid"),
114
- amount: usdc(200.0),
115
- merchant_allowlist_check: false,
116
- };
117
- assert!(matches!(policy.evaluate(&big_tx), PolicyDecision::Deny(_)));
118
- }
119
-
120
- fn hex_encode(bytes: &[u8]) -> String {
121
- bytes.iter().map(|b| format!("{b:02x}")).collect()
122
- }
123
- }
124
-
125
- // ─── Pipeline 2: canonical store → compression → index → verification ────────
126
-
127
- #[cfg(test)]
128
- mod memory_pipeline {
129
- use clawpowers_canonical::{CanonicalRecord, CanonicalStore};
130
- use clawpowers_compression::{CompressionConfig, TurboCompressor};
131
- use clawpowers_index::{InMemoryIndex, VectorIndex};
132
- use clawpowers_verification::VerificationPipeline;
133
-
134
- const DIM: usize = 32;
135
-
136
- fn make_compressor() -> TurboCompressor {
137
- TurboCompressor::new(CompressionConfig {
138
- dimensions: DIM,
139
- quantization_bits: 8,
140
- rotation_seed: 42,
141
- })
142
- }
143
-
144
- fn simple_vector(seed: f32) -> Vec<f32> {
145
- (0..DIM).map(|i| seed + i as f32 * 0.01).collect()
146
- }
147
-
148
- /// Full memory pipeline: insert → compress → index → search → verify.
149
- #[test]
150
- fn test_full_memory_pipeline() {
151
- // 1. Create in-memory canonical store.
152
- let store = CanonicalStore::in_memory().expect("in-memory store");
153
-
154
- // 2. Insert three records.
155
- let rec1 = CanonicalRecord::new(
156
- "embeddings",
157
- "first document text",
158
- Some(simple_vector(0.1)),
159
- serde_json::json!({"doc_id": 1}),
160
- "test-pipeline",
161
- );
162
- let rec2 = CanonicalRecord::new(
163
- "embeddings",
164
- "second document text",
165
- Some(simple_vector(0.5)),
166
- serde_json::json!({"doc_id": 2}),
167
- "test-pipeline",
168
- );
169
- let rec3 = CanonicalRecord::new(
170
- "embeddings",
171
- "third document text",
172
- Some(simple_vector(0.9)),
173
- serde_json::json!({"doc_id": 3}),
174
- "test-pipeline",
175
- );
176
-
177
- let id1 = store.insert(&rec1).expect("insert rec1");
178
- let id2 = store.insert(&rec2).expect("insert rec2");
179
- let id3 = store.insert(&rec3).expect("insert rec3");
180
-
181
- // 3. Compress the embeddings and index them.
182
- let compressor = make_compressor();
183
- let mut index = InMemoryIndex::with_dimensions(DIM);
184
-
185
- for &id in &[id1, id2, id3] {
186
- let record = store.get(&id).expect("get").expect("present");
187
- let embedding = record.embedding.expect("has embedding");
188
- // Compress to verify the pipeline works (store compressed stats).
189
- let _ = compressor.compress(&embedding).expect("compress");
190
- // Index the original vector for ANN search.
191
- index.insert(id, embedding).expect("index");
192
- }
193
-
194
- assert_eq!(index.len(), 3);
195
-
196
- // 4. Search: query close to rec1's vector (seed 0.1).
197
- let query = simple_vector(0.11); // very close to rec1
198
- let results = index.search(&query, 1).expect("search");
199
- assert_eq!(results.len(), 1);
200
- assert_eq!(results[0].id, id1, "nearest should be rec1");
201
-
202
- // 5. Verify integrity of all records.
203
- let pipeline = VerificationPipeline::new(store);
204
- for &id in &[id1, id2, id3] {
205
- let result = pipeline.verify(&id).expect("verify");
206
- assert!(
207
- matches!(
208
- result,
209
- clawpowers_verification::VerificationResult::Verified(_)
210
- ),
211
- "all records should be verified"
212
- );
213
- }
214
- }
215
-
216
- /// Compression produces smaller byte footprint than original f32 slice.
217
- #[test]
218
- fn test_compression_reduces_size() {
219
- let compressor = make_compressor();
220
- let original = simple_vector(0.5);
221
- let original_bytes = original.len() * 4; // f32 = 4 bytes each
222
- let compressed = compressor.compress(&original).expect("compress");
223
- assert!(
224
- compressed.byte_size() < original_bytes,
225
- "compressed ({}) should be smaller than original ({original_bytes})",
226
- compressed.byte_size()
227
- );
228
- }
229
-
230
- /// Decompressed vector is within 5% L2 error of the original.
231
- #[test]
232
- fn test_compression_roundtrip_accuracy() {
233
- let compressor = make_compressor();
234
- let original = simple_vector(0.3);
235
- let compressed = compressor.compress(&original).expect("compress");
236
- let restored = compressor.decompress(&compressed).expect("decompress");
237
-
238
- let l2: f32 = original
239
- .iter()
240
- .zip(restored.iter())
241
- .map(|(a, b)| (a - b) * (a - b))
242
- .sum::<f32>()
243
- .sqrt();
244
- let norm: f32 = original.iter().map(|x| x * x).sum::<f32>().sqrt();
245
- let relative_err = if norm > f32::EPSILON { l2 / norm } else { l2 };
246
- assert!(
247
- relative_err < 0.05,
248
- "relative L2 error {relative_err:.4} should be < 5%"
249
- );
250
- }
251
- }
252
-
253
- // ─── Pipeline 3: security firewall → evaluate → audit log ────────────────────
254
-
255
- #[cfg(test)]
256
- mod security_pipeline {
257
- use clawpowers_security::{
258
- AuditLog, FirewallDecision, TrustLevel, WriteFirewall, WriteRequest,
259
- };
260
-
261
- fn make_firewall() -> WriteFirewall {
262
- WriteFirewall::new(vec!["agents".to_string(), "memory".to_string()])
263
- }
264
-
265
- fn req(namespace: &str, content: &str, trust: TrustLevel, source: &str) -> WriteRequest {
266
- WriteRequest {
267
- namespace: namespace.to_string(),
268
- content: content.to_string(),
269
- trust_level: trust,
270
- source: source.to_string(),
271
- }
272
- }
273
-
274
- /// Full security pipeline: evaluate request, record in audit log.
275
- #[test]
276
- fn test_full_security_pipeline() {
277
- let firewall = make_firewall();
278
- let mut audit = AuditLog::new();
279
-
280
- // 1. Approved agent write.
281
- let r1 = req("agents", "store memory fact", TrustLevel::Agent, "agent-1");
282
- let d1 = firewall.evaluate(&r1);
283
- assert_eq!(d1, FirewallDecision::Allow);
284
- audit.record(&r1, d1, "write");
285
-
286
- // 2. Denied: unauthorized namespace.
287
- let r2 = req("secrets", "leak private key", TrustLevel::Agent, "agent-1");
288
- let d2 = firewall.evaluate(&r2);
289
- assert!(matches!(d2, FirewallDecision::Deny(_)));
290
- audit.record(&r2, d2, "write");
291
-
292
- // 3. External write with injection attempt.
293
- let r3 = req(
294
- "memory",
295
- "hello; DROP users;--",
296
- TrustLevel::External,
297
- "api-gateway",
298
- );
299
- let d3 = firewall.evaluate(&r3);
300
- // Should be sanitized or allowed; not panicking.
301
- audit.record(&r3, d3, "write");
302
-
303
- // 4. Verify audit log has all 3 entries.
304
- assert_eq!(audit.len(), 3);
305
-
306
- // 5. Query by namespace.
307
- let agent_entries = audit.query("agents", 10);
308
- assert_eq!(agent_entries.len(), 1);
309
- assert_eq!(agent_entries[0].source, "agent-1");
310
-
311
- let secret_entries = audit.query("secrets", 10);
312
- assert_eq!(secret_entries.len(), 1);
313
- assert!(matches!(
314
- secret_entries[0].decision,
315
- FirewallDecision::Deny(_)
316
- ));
317
- }
318
-
319
- /// System trust level always allowed in permitted namespaces.
320
- #[test]
321
- fn test_system_trust_always_allowed() {
322
- let firewall = make_firewall();
323
- let r = req("agents", "system operation", TrustLevel::System, "sys");
324
- assert_eq!(firewall.evaluate(&r), FirewallDecision::Allow);
325
- }
326
-
327
- /// Content length enforcement.
328
- #[test]
329
- fn test_content_length_limit() {
330
- let mut firewall = make_firewall();
331
- firewall.max_content_length = 10;
332
- let r = req(
333
- "agents",
334
- "this content is way too long",
335
- TrustLevel::Agent,
336
- "a",
337
- );
338
- assert!(matches!(firewall.evaluate(&r), FirewallDecision::Deny(_)));
339
- }
340
-
341
- /// Audit log query respects limit.
342
- #[test]
343
- fn test_audit_log_limit() {
344
- let firewall = make_firewall();
345
- let mut audit = AuditLog::new();
346
- for i in 0..5 {
347
- let r = req("agents", &format!("content {i}"), TrustLevel::Agent, "a");
348
- let d = firewall.evaluate(&r);
349
- audit.record(&r, d, "write");
350
- }
351
- let limited = audit.query("agents", 3);
352
- assert_eq!(limited.len(), 3);
353
- }
354
- }
1
+ //! Integration tests for the clawpowers-core cross-crate pipeline.
2
+ //!
3
+ //! These tests exercise the full workflows that the napi-rs FFI bindings expose
4
+ //! to JavaScript, but at the Rust level using the public crate APIs directly.
5
+ //!
6
+ //! Pipelines tested:
7
+ //! 1. Wallet → Policy → Fee → x402 payment format
8
+ //! 2. Canonical store → Compression → Index → Verification
9
+ //! 3. Security firewall → Evaluate write request → Audit log
10
+
11
+ // ─── Pipeline 1: wallet → policy → fee → x402 ───────────────────────────────
12
+
13
+ #[cfg(test)]
14
+ mod wallet_payment_pipeline {
15
+ use clawpowers_fee::{FeeSchedule, FeeType};
16
+ use clawpowers_policy::{PolicyDecision, ProposedTx, SpendingPolicy};
17
+ use clawpowers_tokens::TokenAmount;
18
+ use clawpowers_wallet::AgentWallet;
19
+ use clawpowers_x402::{X402Client, X402PaymentRequired};
20
+
21
+ /// Hard-coded Hardhat account #0 private key used for deterministic tests.
22
+ const TEST_KEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
23
+
24
+ fn usdc(amount: f64) -> TokenAmount {
25
+ TokenAmount::from_human(amount, 6)
26
+ }
27
+
28
+ /// Build a complete x402 header from wallet signing.
29
+ #[test]
30
+ fn test_wallet_sign_then_x402_header() {
31
+ // 1. Import wallet from known key.
32
+ let wallet = AgentWallet::from_private_key(TEST_KEY).expect("valid test key");
33
+ assert!(wallet.address().to_string().to_lowercase().contains("f39f"));
34
+
35
+ // 2. Sign a payment descriptor.
36
+ let descriptor = b"pay:USDC:1.00:8453:0xrecipient";
37
+ let sig = wallet.sign_message(descriptor).expect("sign");
38
+ assert_eq!(sig.as_bytes().len(), 65, "signature must be 65 bytes");
39
+ let sig_hex = format!("0x{}", hex_encode(&sig.as_bytes()));
40
+
41
+ // 3. Build x402 payment required struct.
42
+ let requirement = X402PaymentRequired {
43
+ payment_url: "https://pay.example.com/pay".to_string(),
44
+ amount: "1.00".to_string(),
45
+ token: "USDC".to_string(),
46
+ chain_id: 8453,
47
+ recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
48
+ memo: None,
49
+ };
50
+
51
+ // 4. Format the X-Payment header.
52
+ let header = X402Client::create_payment_header(&requirement, &sig_hex);
53
+ assert!(header.contains("8453"), "chain_id must be in header");
54
+ assert!(header.contains(&sig_hex), "signature must be in header");
55
+ }
56
+
57
+ /// Policy → fee → verify the fee calculation is consistent.
58
+ #[test]
59
+ fn test_policy_permits_then_fee_calculated() {
60
+ // 1. Build a spending policy: max 500 USDC per tx.
61
+ let policy = SpendingPolicy::builder()
62
+ .max_per_tx(usdc(500.0))
63
+ .fail_closed(true)
64
+ .build();
65
+
66
+ // 2. Define a proposed 100 USDC transaction.
67
+ let tx = ProposedTx {
68
+ recipient: "0x0000000000000000000000000000000000000001"
69
+ .parse()
70
+ .expect("valid address"),
71
+ amount: usdc(100.0),
72
+ merchant_allowlist_check: false,
73
+ };
74
+
75
+ // 3. Policy evaluation must approve.
76
+ assert_eq!(policy.evaluate(&tx), PolicyDecision::Approve);
77
+
78
+ // 4. Calculate 30 bps fee on the approved amount.
79
+ let schedule = FeeSchedule::new(
80
+ 30, // tx_bps
81
+ 50, // swap_bps
82
+ "0x0000000000000000000000000000000000000000"
83
+ .parse()
84
+ .expect("zero address"),
85
+ );
86
+ let calc = schedule
87
+ .calculate(tx.amount, FeeType::Transaction)
88
+ .expect("fee calculation");
89
+
90
+ // 5. Invariant: gross = fee + net.
91
+ let gross: f64 = calc.gross_amount.to_human();
92
+ let fee: f64 = calc.fee_amount.to_human();
93
+ let net: f64 = calc.net_amount.to_human();
94
+ assert!((gross - fee - net).abs() < 1e-9, "gross != fee + net");
95
+
96
+ // 6. 30 bps of 100 USDC = 0.3 USDC.
97
+ assert!(
98
+ (fee - 0.3).abs() < 0.000_001,
99
+ "fee should be 0.3 USDC, got {fee}"
100
+ );
101
+ }
102
+
103
+ /// Policy denies oversized transactions.
104
+ #[test]
105
+ fn test_policy_denies_over_limit() {
106
+ let policy = SpendingPolicy::builder()
107
+ .max_per_tx(usdc(100.0))
108
+ .fail_closed(true)
109
+ .build();
110
+ let big_tx = ProposedTx {
111
+ recipient: "0x0000000000000000000000000000000000000001"
112
+ .parse()
113
+ .expect("valid"),
114
+ amount: usdc(200.0),
115
+ merchant_allowlist_check: false,
116
+ };
117
+ assert!(matches!(policy.evaluate(&big_tx), PolicyDecision::Deny(_)));
118
+ }
119
+
120
+ fn hex_encode(bytes: &[u8]) -> String {
121
+ bytes.iter().map(|b| format!("{b:02x}")).collect()
122
+ }
123
+ }
124
+
125
+ // ─── Pipeline 2: canonical store → compression → index → verification ────────
126
+
127
+ #[cfg(test)]
128
+ mod memory_pipeline {
129
+ use clawpowers_canonical::{CanonicalRecord, CanonicalStore};
130
+ use clawpowers_compression::{CompressionConfig, TurboCompressor};
131
+ use clawpowers_index::{InMemoryIndex, VectorIndex};
132
+ use clawpowers_verification::VerificationPipeline;
133
+
134
+ const DIM: usize = 32;
135
+
136
+ fn make_compressor() -> TurboCompressor {
137
+ TurboCompressor::new(CompressionConfig {
138
+ dimensions: DIM,
139
+ quantization_bits: 8,
140
+ rotation_seed: 42,
141
+ })
142
+ }
143
+
144
+ fn simple_vector(seed: f32) -> Vec<f32> {
145
+ (0..DIM).map(|i| seed + i as f32 * 0.01).collect()
146
+ }
147
+
148
+ /// Full memory pipeline: insert → compress → index → search → verify.
149
+ #[test]
150
+ fn test_full_memory_pipeline() {
151
+ // 1. Create in-memory canonical store.
152
+ let store = CanonicalStore::in_memory().expect("in-memory store");
153
+
154
+ // 2. Insert three records.
155
+ let rec1 = CanonicalRecord::new(
156
+ "embeddings",
157
+ "first document text",
158
+ Some(simple_vector(0.1)),
159
+ serde_json::json!({"doc_id": 1}),
160
+ "test-pipeline",
161
+ );
162
+ let rec2 = CanonicalRecord::new(
163
+ "embeddings",
164
+ "second document text",
165
+ Some(simple_vector(0.5)),
166
+ serde_json::json!({"doc_id": 2}),
167
+ "test-pipeline",
168
+ );
169
+ let rec3 = CanonicalRecord::new(
170
+ "embeddings",
171
+ "third document text",
172
+ Some(simple_vector(0.9)),
173
+ serde_json::json!({"doc_id": 3}),
174
+ "test-pipeline",
175
+ );
176
+
177
+ let id1 = store.insert(&rec1).expect("insert rec1");
178
+ let id2 = store.insert(&rec2).expect("insert rec2");
179
+ let id3 = store.insert(&rec3).expect("insert rec3");
180
+
181
+ // 3. Compress the embeddings and index them.
182
+ let compressor = make_compressor();
183
+ let mut index = InMemoryIndex::with_dimensions(DIM);
184
+
185
+ for &id in &[id1, id2, id3] {
186
+ let record = store.get(&id).expect("get").expect("present");
187
+ let embedding = record.embedding.expect("has embedding");
188
+ // Compress to verify the pipeline works (store compressed stats).
189
+ let _ = compressor.compress(&embedding).expect("compress");
190
+ // Index the original vector for ANN search.
191
+ index.insert(id, embedding).expect("index");
192
+ }
193
+
194
+ assert_eq!(index.len(), 3);
195
+
196
+ // 4. Search: query close to rec1's vector (seed 0.1).
197
+ let query = simple_vector(0.11); // very close to rec1
198
+ let results = index.search(&query, 1).expect("search");
199
+ assert_eq!(results.len(), 1);
200
+ assert_eq!(results[0].id, id1, "nearest should be rec1");
201
+
202
+ // 5. Verify integrity of all records.
203
+ let pipeline = VerificationPipeline::new(store);
204
+ for &id in &[id1, id2, id3] {
205
+ let result = pipeline.verify(&id).expect("verify");
206
+ assert!(
207
+ matches!(
208
+ result,
209
+ clawpowers_verification::VerificationResult::Verified(_)
210
+ ),
211
+ "all records should be verified"
212
+ );
213
+ }
214
+ }
215
+
216
+ /// Compression produces smaller byte footprint than original f32 slice.
217
+ #[test]
218
+ fn test_compression_reduces_size() {
219
+ let compressor = make_compressor();
220
+ let original = simple_vector(0.5);
221
+ let original_bytes = original.len() * 4; // f32 = 4 bytes each
222
+ let compressed = compressor.compress(&original).expect("compress");
223
+ assert!(
224
+ compressed.byte_size() < original_bytes,
225
+ "compressed ({}) should be smaller than original ({original_bytes})",
226
+ compressed.byte_size()
227
+ );
228
+ }
229
+
230
+ /// Decompressed vector is within 5% L2 error of the original.
231
+ #[test]
232
+ fn test_compression_roundtrip_accuracy() {
233
+ let compressor = make_compressor();
234
+ let original = simple_vector(0.3);
235
+ let compressed = compressor.compress(&original).expect("compress");
236
+ let restored = compressor.decompress(&compressed).expect("decompress");
237
+
238
+ let l2: f32 = original
239
+ .iter()
240
+ .zip(restored.iter())
241
+ .map(|(a, b)| (a - b) * (a - b))
242
+ .sum::<f32>()
243
+ .sqrt();
244
+ let norm: f32 = original.iter().map(|x| x * x).sum::<f32>().sqrt();
245
+ let relative_err = if norm > f32::EPSILON { l2 / norm } else { l2 };
246
+ assert!(
247
+ relative_err < 0.05,
248
+ "relative L2 error {relative_err:.4} should be < 5%"
249
+ );
250
+ }
251
+ }
252
+
253
+ // ─── Pipeline 3: security firewall → evaluate → audit log ────────────────────
254
+
255
+ #[cfg(test)]
256
+ mod security_pipeline {
257
+ use clawpowers_security::{
258
+ AuditLog, FirewallDecision, TrustLevel, WriteFirewall, WriteRequest,
259
+ };
260
+
261
+ fn make_firewall() -> WriteFirewall {
262
+ WriteFirewall::new(vec!["agents".to_string(), "memory".to_string()])
263
+ }
264
+
265
+ fn req(namespace: &str, content: &str, trust: TrustLevel, source: &str) -> WriteRequest {
266
+ WriteRequest {
267
+ namespace: namespace.to_string(),
268
+ content: content.to_string(),
269
+ trust_level: trust,
270
+ source: source.to_string(),
271
+ }
272
+ }
273
+
274
+ /// Full security pipeline: evaluate request, record in audit log.
275
+ #[test]
276
+ fn test_full_security_pipeline() {
277
+ let firewall = make_firewall();
278
+ let mut audit = AuditLog::new();
279
+
280
+ // 1. Approved agent write.
281
+ let r1 = req("agents", "store memory fact", TrustLevel::Agent, "agent-1");
282
+ let d1 = firewall.evaluate(&r1);
283
+ assert_eq!(d1, FirewallDecision::Allow);
284
+ audit.record(&r1, d1, "write");
285
+
286
+ // 2. Denied: unauthorized namespace.
287
+ let r2 = req("secrets", "leak private key", TrustLevel::Agent, "agent-1");
288
+ let d2 = firewall.evaluate(&r2);
289
+ assert!(matches!(d2, FirewallDecision::Deny(_)));
290
+ audit.record(&r2, d2, "write");
291
+
292
+ // 3. External write with injection attempt.
293
+ let r3 = req(
294
+ "memory",
295
+ "hello; DROP users;--",
296
+ TrustLevel::External,
297
+ "api-gateway",
298
+ );
299
+ let d3 = firewall.evaluate(&r3);
300
+ // Should be sanitized or allowed; not panicking.
301
+ audit.record(&r3, d3, "write");
302
+
303
+ // 4. Verify audit log has all 3 entries.
304
+ assert_eq!(audit.len(), 3);
305
+
306
+ // 5. Query by namespace.
307
+ let agent_entries = audit.query("agents", 10);
308
+ assert_eq!(agent_entries.len(), 1);
309
+ assert_eq!(agent_entries[0].source, "agent-1");
310
+
311
+ let secret_entries = audit.query("secrets", 10);
312
+ assert_eq!(secret_entries.len(), 1);
313
+ assert!(matches!(
314
+ secret_entries[0].decision,
315
+ FirewallDecision::Deny(_)
316
+ ));
317
+ }
318
+
319
+ /// System trust level always allowed in permitted namespaces.
320
+ #[test]
321
+ fn test_system_trust_always_allowed() {
322
+ let firewall = make_firewall();
323
+ let r = req("agents", "system operation", TrustLevel::System, "sys");
324
+ assert_eq!(firewall.evaluate(&r), FirewallDecision::Allow);
325
+ }
326
+
327
+ /// Content length enforcement.
328
+ #[test]
329
+ fn test_content_length_limit() {
330
+ let mut firewall = make_firewall();
331
+ firewall.max_content_length = 10;
332
+ let r = req(
333
+ "agents",
334
+ "this content is way too long",
335
+ TrustLevel::Agent,
336
+ "a",
337
+ );
338
+ assert!(matches!(firewall.evaluate(&r), FirewallDecision::Deny(_)));
339
+ }
340
+
341
+ /// Audit log query respects limit.
342
+ #[test]
343
+ fn test_audit_log_limit() {
344
+ let firewall = make_firewall();
345
+ let mut audit = AuditLog::new();
346
+ for i in 0..5 {
347
+ let r = req("agents", &format!("content {i}"), TrustLevel::Agent, "a");
348
+ let d = firewall.evaluate(&r);
349
+ audit.record(&r, d, "write");
350
+ }
351
+ let limited = audit.query("agents", 3);
352
+ assert_eq!(limited.len(), 3);
353
+ }
354
+ }