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,407 +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
- }
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
+ }