clawpowers 2.0.0 → 2.2.1

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 (63) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/COMPATIBILITY.md +13 -0
  3. package/KNOWN_LIMITATIONS.md +19 -0
  4. package/LICENSING.md +10 -0
  5. package/README.md +201 -9
  6. package/SECURITY.md +33 -53
  7. package/dist/index.d.ts +638 -5
  8. package/dist/index.js +986 -58
  9. package/dist/index.js.map +1 -1
  10. package/native/Cargo.lock +4863 -0
  11. package/native/Cargo.toml +73 -0
  12. package/native/crates/canonical/Cargo.toml +24 -0
  13. package/native/crates/canonical/src/lib.rs +673 -0
  14. package/native/crates/compression/Cargo.toml +20 -0
  15. package/native/crates/compression/benches/compression_bench.rs +42 -0
  16. package/native/crates/compression/src/lib.rs +393 -0
  17. package/native/crates/evm-eth/Cargo.toml +13 -0
  18. package/native/crates/evm-eth/src/lib.rs +105 -0
  19. package/native/crates/fee/Cargo.toml +15 -0
  20. package/native/crates/fee/src/lib.rs +281 -0
  21. package/native/crates/index/Cargo.toml +16 -0
  22. package/native/crates/index/src/lib.rs +277 -0
  23. package/native/crates/policy/Cargo.toml +17 -0
  24. package/native/crates/policy/src/lib.rs +614 -0
  25. package/native/crates/security/Cargo.toml +22 -0
  26. package/native/crates/security/src/lib.rs +478 -0
  27. package/native/crates/tokens/Cargo.toml +13 -0
  28. package/native/crates/tokens/src/lib.rs +534 -0
  29. package/native/crates/verification/Cargo.toml +23 -0
  30. package/native/crates/verification/src/lib.rs +333 -0
  31. package/native/crates/wallet/Cargo.toml +20 -0
  32. package/native/crates/wallet/src/lib.rs +261 -0
  33. package/native/crates/x402/Cargo.toml +30 -0
  34. package/native/crates/x402/src/lib.rs +423 -0
  35. package/native/ffi/Cargo.toml +34 -0
  36. package/native/ffi/build.rs +4 -0
  37. package/native/ffi/index.node +0 -0
  38. package/native/ffi/src/lib.rs +352 -0
  39. package/native/ffi/tests/integration.rs +354 -0
  40. package/native/pyo3/Cargo.toml +26 -0
  41. package/native/pyo3/pyproject.toml +16 -0
  42. package/native/pyo3/src/lib.rs +407 -0
  43. package/native/pyo3/tests/test_smoke.py +180 -0
  44. package/native/wasm/Cargo.toml +44 -0
  45. package/native/wasm/pkg/.gitignore +6 -0
  46. package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -0
  47. package/native/wasm/pkg/clawpowers_wasm.js +872 -0
  48. package/native/wasm/pkg/clawpowers_wasm_bg.wasm +0 -0
  49. package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -0
  50. package/native/wasm/pkg/package.json +17 -0
  51. package/native/wasm/pkg-node/.gitignore +6 -0
  52. package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -0
  53. package/native/wasm/pkg-node/clawpowers_wasm.js +798 -0
  54. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm +0 -0
  55. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -0
  56. package/native/wasm/pkg-node/package.json +13 -0
  57. package/native/wasm/src/lib.rs +433 -0
  58. package/package.json +24 -3
  59. package/src/skills/catalog.ts +435 -0
  60. package/src/skills/executor.ts +56 -0
  61. package/src/skills/index.ts +3 -0
  62. package/src/skills/itp/SKILL.md +112 -0
  63. package/src/skills/loader.ts +193 -0
@@ -0,0 +1,407 @@
1
+ //! clawpowers-pyo3 — Python bindings for clawpowers-core.
2
+
3
+ use pyo3::prelude::*;
4
+ use pyo3::exceptions::PyValueError;
5
+
6
+ // ═══════════════════════════════════════════════════════════════════════════════
7
+ // WALLET
8
+ // ═══════════════════════════════════════════════════════════════════════════════
9
+
10
+ /// EVM agent wallet — key management and message signing.
11
+ #[pyclass]
12
+ pub struct AgentWallet {
13
+ inner: clawpowers_wallet::AgentWallet,
14
+ }
15
+
16
+ #[pymethods]
17
+ impl AgentWallet {
18
+ /// Generate a fresh random wallet.
19
+ #[staticmethod]
20
+ fn generate() -> Self {
21
+ Self {
22
+ inner: clawpowers_wallet::AgentWallet::generate(),
23
+ }
24
+ }
25
+
26
+ /// Import a wallet from a hex private key string.
27
+ #[staticmethod]
28
+ fn from_private_key(hex: &str) -> PyResult<Self> {
29
+ let inner = clawpowers_wallet::AgentWallet::from_private_key(hex)
30
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
31
+ Ok(Self { inner })
32
+ }
33
+
34
+ /// Return the checksummed EVM address.
35
+ fn address(&self) -> String {
36
+ format!("{:#x}", self.inner.address())
37
+ }
38
+
39
+ /// Return the wallet UUID.
40
+ fn wallet_id(&self) -> String {
41
+ self.inner.wallet_id.to_string()
42
+ }
43
+
44
+ /// Sign a raw byte buffer. Returns the signature as a string.
45
+ fn sign_message(&self, msg: &[u8]) -> PyResult<String> {
46
+ let sig = self.inner.sign_message(msg)
47
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
48
+ Ok(format!("{sig:?}"))
49
+ }
50
+ }
51
+
52
+ // ═══════════════════════════════════════════════════════════════════════════════
53
+ // TOKENS
54
+ // ═══════════════════════════════════════════════════════════════════════════════
55
+
56
+ /// Fixed-point token amounts with decimal precision.
57
+ #[pyclass]
58
+ pub struct TokenAmount {
59
+ inner: clawpowers_tokens::TokenAmount,
60
+ }
61
+
62
+ #[pymethods]
63
+ impl TokenAmount {
64
+ /// Create a token amount from a human-readable float value.
65
+ #[staticmethod]
66
+ fn from_human(amount: f64, decimals: u8) -> Self {
67
+ Self {
68
+ inner: clawpowers_tokens::TokenAmount::from_human(amount, decimals),
69
+ }
70
+ }
71
+
72
+ /// Convert to a human-readable float.
73
+ fn to_human(&self) -> f64 {
74
+ self.inner.to_human()
75
+ }
76
+
77
+ /// Return true if the amount is zero.
78
+ fn is_zero(&self) -> bool {
79
+ self.inner.is_zero()
80
+ }
81
+
82
+ /// Serialize to JSON.
83
+ fn to_json(&self) -> PyResult<String> {
84
+ serde_json::to_string(&self.inner)
85
+ .map_err(|e| PyValueError::new_err(e.to_string()))
86
+ }
87
+
88
+ fn __repr__(&self) -> String {
89
+ format!("TokenAmount({})", self.inner.to_human())
90
+ }
91
+ }
92
+
93
+ /// Get the default token registry as JSON.
94
+ #[pyfunction]
95
+ fn default_token_registry() -> PyResult<String> {
96
+ let reg = clawpowers_tokens::TokenRegistry::default();
97
+ let tokens: Vec<serde_json::Value> = reg.iter().map(|t| {
98
+ serde_json::json!({
99
+ "symbol": t.symbol,
100
+ "decimals": t.decimals,
101
+ "chain_id": t.chain_id,
102
+ })
103
+ }).collect();
104
+ serde_json::to_string(&tokens)
105
+ .map_err(|e| PyValueError::new_err(e.to_string()))
106
+ }
107
+
108
+ // ═══════════════════════════════════════════════════════════════════════════════
109
+ // FEE
110
+ // ═══════════════════════════════════════════════════════════════════════════════
111
+
112
+ /// Fee schedule calculation.
113
+ #[pyclass]
114
+ pub struct FeeSchedule {
115
+ inner: clawpowers_fee::FeeSchedule,
116
+ }
117
+
118
+ #[pymethods]
119
+ impl FeeSchedule {
120
+ /// Create a fee schedule with default rates (77 bps tx, 30 bps swap).
121
+ #[staticmethod]
122
+ fn with_defaults() -> Self {
123
+ Self {
124
+ inner: clawpowers_fee::FeeSchedule::default(),
125
+ }
126
+ }
127
+
128
+ /// Create a fee schedule with custom rates and recipient address.
129
+ #[new]
130
+ fn new(tx_bps: u64, swap_bps: u64, recipient_hex: &str) -> PyResult<Self> {
131
+ let recipient: alloy_primitives::Address = recipient_hex.parse()
132
+ .map_err(|e: alloy_primitives::hex::FromHexError| PyValueError::new_err(e.to_string()))?;
133
+ Ok(Self {
134
+ inner: clawpowers_fee::FeeSchedule::new(tx_bps, swap_bps, recipient),
135
+ })
136
+ }
137
+
138
+ /// Calculate fee. fee_type: "transaction", "swap", or "custom:<bps>".
139
+ fn calculate(&self, amount: f64, decimals: u8, fee_type: &str) -> PyResult<String> {
140
+ let amt = clawpowers_tokens::TokenAmount::from_human(amount, decimals);
141
+ let ft = match fee_type {
142
+ "transaction" => clawpowers_fee::FeeType::Transaction,
143
+ "swap" => clawpowers_fee::FeeType::Swap,
144
+ s if s.starts_with("custom:") => {
145
+ let bps: u64 = s[7..].parse()
146
+ .map_err(|e: std::num::ParseIntError| PyValueError::new_err(e.to_string()))?;
147
+ clawpowers_fee::FeeType::Custom(bps)
148
+ }
149
+ _ => return Err(PyValueError::new_err(format!("unknown fee type: {fee_type}"))),
150
+ };
151
+ let calc = self.inner.calculate(amt, ft)
152
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
153
+ let result = serde_json::json!({
154
+ "gross": calc.gross_amount.to_human(),
155
+ "fee": calc.fee_amount.to_human(),
156
+ "net": calc.net_amount.to_human(),
157
+ "fee_recipient": format!("{:#x}", calc.fee_recipient),
158
+ });
159
+ Ok(result.to_string())
160
+ }
161
+ }
162
+
163
+ // ═══════════════════════════════════════════════════════════════════════════════
164
+ // X402
165
+ // ═══════════════════════════════════════════════════════════════════════════════
166
+
167
+ /// HTTP 402 Payment Required protocol client.
168
+ #[pyclass]
169
+ pub struct X402Client {
170
+ #[allow(dead_code)]
171
+ inner: clawpowers_x402::X402Client,
172
+ }
173
+
174
+ #[pymethods]
175
+ impl X402Client {
176
+ /// Create a new x402 client.
177
+ #[new]
178
+ fn new() -> Self {
179
+ Self {
180
+ inner: clawpowers_x402::X402Client::new(),
181
+ }
182
+ }
183
+
184
+ /// Build an X-Payment header value from payment JSON and signature.
185
+ fn create_payment_header(&self, payment_json: &str, signature: &str) -> PyResult<String> {
186
+ let payment: clawpowers_x402::X402PaymentRequired =
187
+ serde_json::from_str(payment_json)
188
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
189
+ Ok(clawpowers_x402::X402Client::create_payment_header(&payment, signature))
190
+ }
191
+ }
192
+
193
+ // ═══════════════════════════════════════════════════════════════════════════════
194
+ // CANONICAL STORE
195
+ // ═══════════════════════════════════════════════════════════════════════════════
196
+
197
+ /// Append-only canonical record store backed by SQLite.
198
+ #[pyclass(unsendable)]
199
+ pub struct CanonicalStore {
200
+ inner: clawpowers_canonical::CanonicalStore,
201
+ }
202
+
203
+ #[pymethods]
204
+ impl CanonicalStore {
205
+ /// Open or create a persistent store at path.
206
+ #[staticmethod]
207
+ fn open(path: &str) -> PyResult<Self> {
208
+ let inner = clawpowers_canonical::CanonicalStore::new(path)
209
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
210
+ Ok(Self { inner })
211
+ }
212
+
213
+ /// Create an in-memory store (non-persistent).
214
+ #[staticmethod]
215
+ fn in_memory() -> PyResult<Self> {
216
+ let inner = clawpowers_canonical::CanonicalStore::in_memory()
217
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
218
+ Ok(Self { inner })
219
+ }
220
+
221
+ /// Insert a record (JSON). Returns assigned UUID.
222
+ fn insert(&self, record_json: &str) -> PyResult<String> {
223
+ let record: clawpowers_canonical::CanonicalRecord =
224
+ serde_json::from_str(record_json)
225
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
226
+ let id = self.inner.insert(&record)
227
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
228
+ Ok(id.to_string())
229
+ }
230
+
231
+ /// Get a record by UUID. Returns JSON or None.
232
+ fn get(&self, id: &str) -> PyResult<Option<String>> {
233
+ let uuid: uuid::Uuid = id.parse()
234
+ .map_err(|e: uuid::Error| PyValueError::new_err(e.to_string()))?;
235
+ let record = self.inner.get(&uuid)
236
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
237
+ match record {
238
+ Some(r) => Ok(Some(serde_json::to_string(&r)
239
+ .map_err(|e| PyValueError::new_err(e.to_string()))?)),
240
+ None => Ok(None),
241
+ }
242
+ }
243
+
244
+ /// Verify record integrity by re-hashing.
245
+ fn verify_integrity(&self, id: &str) -> PyResult<bool> {
246
+ let uuid: uuid::Uuid = id.parse()
247
+ .map_err(|e: uuid::Error| PyValueError::new_err(e.to_string()))?;
248
+ self.inner.verify_integrity(&uuid)
249
+ .map_err(|e| PyValueError::new_err(e.to_string()))
250
+ }
251
+ }
252
+
253
+ // ═══════════════════════════════════════════════════════════════════════════════
254
+ // TURBO COMPRESSOR
255
+ // ═══════════════════════════════════════════════════════════════════════════════
256
+
257
+ /// TurboQuant vector compressor for embeddings.
258
+ #[pyclass]
259
+ pub struct TurboCompressor {
260
+ inner: clawpowers_compression::TurboCompressor,
261
+ }
262
+
263
+ #[pymethods]
264
+ impl TurboCompressor {
265
+ /// Create a new compressor for the given dimensions and quantization bits.
266
+ #[new]
267
+ fn new(dimensions: usize, bits: u8) -> Self {
268
+ Self {
269
+ inner: clawpowers_compression::TurboCompressor::new(
270
+ clawpowers_compression::CompressionConfig {
271
+ dimensions,
272
+ quantization_bits: bits,
273
+ rotation_seed: 0xDEAD_BEEF_CAFE_1234,
274
+ },
275
+ ),
276
+ }
277
+ }
278
+
279
+ /// Compress a list of f32 values. Returns JSON.
280
+ fn compress(&self, vector: Vec<f32>) -> PyResult<String> {
281
+ let compressed = self.inner.compress(&vector)
282
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
283
+ serde_json::to_string(&compressed)
284
+ .map_err(|e| PyValueError::new_err(e.to_string()))
285
+ }
286
+
287
+ /// Decompress a JSON compressed vector back to a list of f32.
288
+ fn decompress(&self, compressed_json: &str) -> PyResult<Vec<f32>> {
289
+ let compressed: clawpowers_compression::CompressedVector =
290
+ serde_json::from_str(compressed_json)
291
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
292
+ self.inner.decompress(&compressed)
293
+ .map_err(|e| PyValueError::new_err(e.to_string()))
294
+ }
295
+ }
296
+
297
+ // ═══════════════════════════════════════════════════════════════════════════════
298
+ // WRITE FIREWALL
299
+ // ═══════════════════════════════════════════════════════════════════════════════
300
+
301
+ /// Write access control firewall.
302
+ #[pyclass]
303
+ pub struct WriteFirewall {
304
+ inner: clawpowers_security::WriteFirewall,
305
+ }
306
+
307
+ #[pymethods]
308
+ impl WriteFirewall {
309
+ /// Create a firewall from a JSON config with "allowed_namespaces" array.
310
+ #[new]
311
+ fn new(config_json: &str) -> PyResult<Self> {
312
+ #[derive(serde::Deserialize)]
313
+ struct FirewallConfig {
314
+ allowed_namespaces: Vec<String>,
315
+ }
316
+ let config: FirewallConfig = serde_json::from_str(config_json)
317
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
318
+ Ok(Self {
319
+ inner: clawpowers_security::WriteFirewall::new(config.allowed_namespaces),
320
+ })
321
+ }
322
+
323
+ /// Evaluate a write request (JSON). Returns JSON decision.
324
+ fn evaluate(&self, request_json: &str) -> PyResult<String> {
325
+ let req: clawpowers_security::WriteRequest =
326
+ serde_json::from_str(request_json)
327
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
328
+ let decision = self.inner.evaluate(&req);
329
+ serde_json::to_string(&decision)
330
+ .map_err(|e| PyValueError::new_err(e.to_string()))
331
+ }
332
+ }
333
+
334
+ // ═══════════════════════════════════════════════════════════════════════════════
335
+ // POLICY
336
+ // ═══════════════════════════════════════════════════════════════════════════════
337
+
338
+ /// Evaluate a spending policy on a proposed transaction.
339
+ #[pyfunction]
340
+ fn evaluate_spending_policy(
341
+ max_per_tx: f64,
342
+ decimals: u8,
343
+ fail_closed: bool,
344
+ tx_amount: f64,
345
+ tx_recipient: &str,
346
+ ) -> PyResult<String> {
347
+ let policy = clawpowers_policy::SpendingPolicy::builder()
348
+ .max_per_tx(clawpowers_tokens::TokenAmount::from_human(max_per_tx, decimals))
349
+ .fail_closed(fail_closed)
350
+ .build();
351
+ let recipient: alloy_primitives::Address = tx_recipient.parse()
352
+ .map_err(|e: alloy_primitives::hex::FromHexError| PyValueError::new_err(e.to_string()))?;
353
+ let tx = clawpowers_policy::ProposedTx {
354
+ recipient,
355
+ amount: clawpowers_tokens::TokenAmount::from_human(tx_amount, decimals),
356
+ merchant_allowlist_check: false,
357
+ };
358
+ let decision = policy.evaluate(&tx);
359
+ let result = match decision {
360
+ clawpowers_policy::PolicyDecision::Approve => "approve".to_string(),
361
+ clawpowers_policy::PolicyDecision::Deny(reason) => format!("deny: {reason}"),
362
+ clawpowers_policy::PolicyDecision::RequireHumanApproval(reason) => format!("escalate: {reason}"),
363
+ };
364
+ Ok(result)
365
+ }
366
+
367
+ /// Compute SHA-256 hash of content (from canonical crate).
368
+ #[pyfunction]
369
+ fn compute_sha256(content: &str) -> String {
370
+ clawpowers_canonical::compute_sha256(content)
371
+ }
372
+
373
+ /// Compute cosine similarity between two f32 vectors.
374
+ #[pyfunction]
375
+ fn cosine_similarity(a: Vec<f32>, b: Vec<f32>) -> f32 {
376
+ clawpowers_compression::cosine_similarity(&a, &b)
377
+ }
378
+
379
+ /// Compute L2 distance between two f32 vectors.
380
+ #[pyfunction]
381
+ fn l2_distance(a: Vec<f32>, b: Vec<f32>) -> f32 {
382
+ clawpowers_compression::l2_distance(&a, &b)
383
+ }
384
+
385
+ // ═══════════════════════════════════════════════════════════════════════════════
386
+ // MODULE
387
+ // ═══════════════════════════════════════════════════════════════════════════════
388
+
389
+ /// ClawPowers Core — Rust-powered Python bindings for the agent economy.
390
+ #[pymodule]
391
+ fn clawpowers_core(m: &Bound<'_, PyModule>) -> PyResult<()> {
392
+ // Classes
393
+ m.add_class::<AgentWallet>()?;
394
+ m.add_class::<TokenAmount>()?;
395
+ m.add_class::<FeeSchedule>()?;
396
+ m.add_class::<X402Client>()?;
397
+ m.add_class::<CanonicalStore>()?;
398
+ m.add_class::<TurboCompressor>()?;
399
+ m.add_class::<WriteFirewall>()?;
400
+ // Functions
401
+ m.add_function(wrap_pyfunction!(default_token_registry, m)?)?;
402
+ m.add_function(wrap_pyfunction!(evaluate_spending_policy, m)?)?;
403
+ m.add_function(wrap_pyfunction!(compute_sha256, m)?)?;
404
+ m.add_function(wrap_pyfunction!(cosine_similarity, m)?)?;
405
+ m.add_function(wrap_pyfunction!(l2_distance, m)?)?;
406
+ Ok(())
407
+ }
@@ -0,0 +1,180 @@
1
+ """Smoke test for clawpowers_core PyO3 bindings.
2
+
3
+ Exercises at least one function from: wallet, tokens, fee, x402, canonical,
4
+ compression, security, and policy (8 crates).
5
+ """
6
+
7
+ import json
8
+ import sys
9
+
10
+
11
+ def main():
12
+ import clawpowers_core as cc
13
+
14
+ passed = 0
15
+ total = 0
16
+
17
+ # ── 1. Wallet ──────────────────────────────────────────────────────────
18
+ total += 1
19
+ w = cc.AgentWallet.generate()
20
+ addr = w.address()
21
+ assert addr.startswith("0x"), f"address should start with 0x, got {addr}"
22
+ assert len(addr) == 42, f"address should be 42 chars, got {len(addr)}"
23
+ wid = w.wallet_id()
24
+ assert len(wid) == 36, f"wallet_id should be UUID, got {wid}"
25
+ sig = w.sign_message(b"hello clawpowers")
26
+ assert len(sig) > 0, "signature should not be empty"
27
+ passed += 1
28
+ print(f" ✅ wallet: address={addr[:10]}…")
29
+
30
+ # ── 2. Wallet from private key ─────────────────────────────────────────
31
+ total += 1
32
+ w2 = cc.AgentWallet.from_private_key(
33
+ "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
34
+ )
35
+ assert w2.address() == "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
36
+ passed += 1
37
+ print(f" ✅ wallet from_private_key: {w2.address()[:10]}…")
38
+
39
+ # ── 3. Tokens ──────────────────────────────────────────────────────────
40
+ total += 1
41
+ t = cc.TokenAmount.from_human(123.456, 6)
42
+ assert abs(t.to_human() - 123.456) < 0.001
43
+ assert not t.is_zero()
44
+ j = t.to_json()
45
+ assert "raw" in j
46
+ passed += 1
47
+ print(f" ✅ tokens: {t}")
48
+
49
+ # ── 4. Token registry ──────────────────────────────────────────────────
50
+ total += 1
51
+ reg = json.loads(cc.default_token_registry())
52
+ symbols = {t["symbol"] for t in reg}
53
+ assert "USDC" in symbols
54
+ assert "ETH" in symbols
55
+ passed += 1
56
+ print(f" ✅ token registry: {len(reg)} tokens")
57
+
58
+ # ── 5. Fee ─────────────────────────────────────────────────────────────
59
+ total += 1
60
+ fs = cc.FeeSchedule.with_defaults()
61
+ calc = json.loads(fs.calculate(1000.0, 6, "transaction"))
62
+ assert abs(calc["fee"] - 7.7) < 0.001, f"fee should be 7.7, got {calc['fee']}"
63
+ assert abs(calc["net"] - 992.3) < 0.001
64
+ passed += 1
65
+ print(f" ✅ fee: gross={calc['gross']} fee={calc['fee']} net={calc['net']}")
66
+
67
+ # ── 6. X402 ────────────────────────────────────────────────────────────
68
+ total += 1
69
+ x = cc.X402Client()
70
+ payment_json = json.dumps({
71
+ "payment_url": "https://pay.example.com/pay",
72
+ "amount": "1.00",
73
+ "token": "USDC",
74
+ "chain_id": 8453,
75
+ "recipient": "0xrecipient",
76
+ "memo": None,
77
+ })
78
+ header = x.create_payment_header(payment_json, "0xsig")
79
+ assert "0xrecipient" in header
80
+ assert "0xsig" in header
81
+ passed += 1
82
+ print(f" ✅ x402: header={header[:40]}…")
83
+
84
+ # ── 7. Canonical store ─────────────────────────────────────────────────
85
+ total += 1
86
+ store = cc.CanonicalStore.in_memory()
87
+ record = json.dumps({
88
+ "id": "00000000-0000-0000-0000-000000000001",
89
+ "namespace": "test",
90
+ "content": "hello world",
91
+ "content_hash": cc.compute_sha256("hello world"),
92
+ "embedding": None,
93
+ "metadata": {},
94
+ "created_at": "2026-03-31T00:00:00Z",
95
+ "provenance": "smoke-test",
96
+ })
97
+ rid = store.insert(record)
98
+ assert len(rid) == 36, f"insert should return UUID, got {rid}"
99
+ fetched = store.get(rid)
100
+ assert fetched is not None
101
+ assert json.loads(fetched)["content"] == "hello world"
102
+ assert store.verify_integrity(rid) is True
103
+ passed += 1
104
+ print(f" ✅ canonical: inserted and verified id={rid[:8]}…")
105
+
106
+ # ── 8. Compression ─────────────────────────────────────────────────────
107
+ total += 1
108
+ comp = cc.TurboCompressor(64, 8)
109
+ vec = [float(i) / 64.0 for i in range(64)]
110
+ compressed = comp.compress(vec)
111
+ cdata = json.loads(compressed)
112
+ assert len(cdata["quantized"]) == 64
113
+ decompressed = comp.decompress(compressed)
114
+ assert len(decompressed) == 64
115
+ # Check roundtrip fidelity
116
+ err = sum((a - b) ** 2 for a, b in zip(vec, decompressed)) ** 0.5
117
+ assert err < 1.0, f"roundtrip error too high: {err}"
118
+ passed += 1
119
+ print(f" ✅ compression: {len(vec)} dims → {len(cdata['quantized'])} quantized, err={err:.4f}")
120
+
121
+ # ── 9. Security (WriteFirewall) ────────────────────────────────────────
122
+ total += 1
123
+ fw = cc.WriteFirewall('{"allowed_namespaces": ["agents", "test"]}')
124
+ decision = json.loads(fw.evaluate(json.dumps({
125
+ "namespace": "agents",
126
+ "content": "normal content",
127
+ "trust_level": "Agent",
128
+ "source": "test-agent",
129
+ })))
130
+ assert decision == "Allow", f"expected Allow, got {decision}"
131
+ # Test deny for unlisted namespace
132
+ deny = json.loads(fw.evaluate(json.dumps({
133
+ "namespace": "forbidden",
134
+ "content": "bad",
135
+ "trust_level": "Agent",
136
+ "source": "test",
137
+ })))
138
+ assert "Deny" in str(deny), f"expected Deny, got {deny}"
139
+ passed += 1
140
+ print(f" ✅ security: allow={decision}, deny works")
141
+
142
+ # ── 10. Policy ─────────────────────────────────────────────────────────
143
+ total += 1
144
+ result = cc.evaluate_spending_policy(
145
+ 100.0, 6, True, 50.0,
146
+ "0x0000000000000000000000000000000000000000"
147
+ )
148
+ assert result == "approve", f"expected approve, got {result}"
149
+ # Test deny
150
+ result2 = cc.evaluate_spending_policy(
151
+ 100.0, 6, True, 200.0,
152
+ "0x0000000000000000000000000000000000000000"
153
+ )
154
+ assert result2.startswith("deny"), f"expected deny, got {result2}"
155
+ passed += 1
156
+ print(f" ✅ policy: approve ok, deny ok")
157
+
158
+ # ── 11. Utility functions ──────────────────────────────────────────────
159
+ total += 1
160
+ h = cc.compute_sha256("test")
161
+ assert len(h) == 64, f"sha256 should be 64 hex chars, got {len(h)}"
162
+ sim = cc.cosine_similarity([1.0, 0.0], [1.0, 0.0])
163
+ assert abs(sim - 1.0) < 0.001
164
+ dist = cc.l2_distance([0.0, 0.0], [3.0, 4.0])
165
+ assert abs(dist - 5.0) < 0.001
166
+ passed += 1
167
+ print(f" ✅ utilities: sha256, cosine_similarity, l2_distance")
168
+
169
+ print(f"\n{'='*60}")
170
+ print(f" SMOKE TEST: {passed}/{total} passed")
171
+ print(f" Crates exercised: wallet, tokens, fee, x402, canonical,")
172
+ print(f" compression, security, policy (8 crates)")
173
+ print(f"{'='*60}")
174
+
175
+ if passed < total:
176
+ sys.exit(1)
177
+
178
+
179
+ if __name__ == "__main__":
180
+ main()
@@ -0,0 +1,44 @@
1
+ [package]
2
+ name = "clawpowers-wasm"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+ description = "WASM bindings for ClawPowers core — tokens, fees, policy, compression, index, canonical, verification, security"
7
+
8
+ [lib]
9
+ crate-type = ["cdylib", "rlib"]
10
+
11
+ [dependencies]
12
+ wasm-bindgen = { workspace = true }
13
+ serde = { workspace = true }
14
+ serde_json = { workspace = true }
15
+ serde-wasm-bindgen = "0.6"
16
+
17
+ # Crates that compile cleanly to WASM
18
+ clawpowers-tokens = { path = "../crates/tokens" }
19
+ clawpowers-fee = { path = "../crates/fee" }
20
+ clawpowers-policy = { path = "../crates/policy" }
21
+ clawpowers-compression = { path = "../crates/compression" }
22
+ clawpowers-index = { path = "../crates/index" }
23
+
24
+ # Crates that need WASM feature flag
25
+ clawpowers-canonical = { path = "../crates/canonical", default-features = false, features = ["wasm"] }
26
+ clawpowers-verification = { path = "../crates/verification", default-features = false, features = ["wasm"] }
27
+ clawpowers-security = { path = "../crates/security", default-features = false, features = ["wasm"] }
28
+
29
+ # alloy-primitives needed for Address type
30
+ alloy-primitives = { workspace = true }
31
+ k256 = { version = "0.13", features = ["ecdsa", "sha2"] }
32
+ clawpowers-evm-eth = { path = "../crates/evm-eth" }
33
+ uuid = { version = "1", features = ["v4", "serde", "js"] }
34
+ chrono = { workspace = true }
35
+
36
+ # WASM-compatible random number generation
37
+ getrandom = { version = "0.3", features = ["wasm_js"] }
38
+
39
+ # k256 → rand_core pulls getrandom 0.2; enable `js` for wasm32-unknown-unknown
40
+ [target.'cfg(target_arch = "wasm32")'.dependencies]
41
+ getrandom = { version = "0.2", features = ["js"] }
42
+
43
+ [dev-dependencies]
44
+ wasm-bindgen-test = "0.3"
@@ -0,0 +1,6 @@
1
+ *
2
+ !.gitignore
3
+ !*.js
4
+ !*.d.ts
5
+ !*.wasm
6
+ !package.json