clawpowers 2.2.6 → 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.
- package/CHANGELOG.md +186 -175
- package/COMPATIBILITY.md +48 -13
- package/KNOWN_LIMITATIONS.md +20 -19
- package/LICENSE +44 -44
- package/LICENSING.md +10 -10
- package/README.md +486 -462
- package/SECURITY.md +52 -52
- package/dist/index.d.ts +17 -5
- package/dist/index.js +186 -91
- package/dist/index.js.map +1 -1
- package/native/Cargo.lock +4927 -4927
- package/native/Cargo.toml +73 -73
- package/native/crates/canonical/Cargo.toml +24 -24
- package/native/crates/canonical/src/lib.rs +677 -677
- package/native/crates/compression/Cargo.toml +20 -20
- package/native/crates/compression/benches/compression_bench.rs +42 -42
- package/native/crates/compression/src/lib.rs +393 -393
- package/native/crates/evm-eth/Cargo.toml +13 -13
- package/native/crates/evm-eth/src/lib.rs +105 -105
- package/native/crates/fee/Cargo.toml +15 -15
- package/native/crates/fee/src/lib.rs +281 -281
- package/native/crates/index/Cargo.toml +16 -16
- package/native/crates/index/src/lib.rs +277 -277
- package/native/crates/policy/Cargo.toml +17 -17
- package/native/crates/policy/src/lib.rs +614 -614
- package/native/crates/security/Cargo.toml +22 -22
- package/native/crates/security/src/lib.rs +478 -478
- package/native/crates/tokens/Cargo.toml +13 -13
- package/native/crates/tokens/src/lib.rs +534 -534
- package/native/crates/verification/Cargo.toml +23 -23
- package/native/crates/verification/src/lib.rs +333 -333
- package/native/crates/wallet/Cargo.toml +20 -20
- package/native/crates/wallet/src/lib.rs +261 -261
- package/native/crates/x402/Cargo.toml +30 -30
- package/native/crates/x402/src/lib.rs +423 -423
- package/native/ffi/Cargo.toml +34 -34
- package/native/ffi/build.rs +4 -4
- package/native/ffi/src/lib.rs +352 -352
- package/native/ffi/tests/integration.rs +354 -354
- package/native/pyo3/Cargo.toml +26 -26
- package/native/pyo3/pyproject.toml +16 -16
- package/native/pyo3/src/lib.rs +407 -407
- package/native/pyo3/tests/test_smoke.py +180 -180
- package/native/wasm/Cargo.toml +47 -47
- package/native/wasm/pkg/.gitignore +6 -6
- package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -208
- package/native/wasm/pkg/clawpowers_wasm.js +872 -872
- package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -40
- package/native/wasm/pkg/package.json +16 -16
- package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -143
- package/native/wasm/pkg-node/clawpowers_wasm.js +798 -798
- package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -40
- package/native/wasm/pkg-node/package.json +12 -12
- package/native/wasm/src/lib.rs +433 -433
- package/package.json +12 -8
- package/scripts/build-wasm.mjs +59 -0
- package/scripts/generate_hermes_wrappers.py +211 -0
- package/scripts/hermes_wrapper_overrides.json +184 -0
- package/scripts/run-python-script.mjs +48 -0
- package/scripts/verify-consumer-install.mjs +109 -0
- package/scripts/verify-wasm-artifacts.mjs +25 -2
- package/scripts/verify_hermes_wrappers.py +154 -0
- package/skill.json +1 -1
- package/skills/1password/SKILL.md +34 -0
- package/skills/README.md +44 -0
- package/skills/agent-nexus-2/SKILL.md +34 -0
- package/skills/apple-notes/SKILL.md +34 -0
- package/skills/apple-reminders/SKILL.md +34 -0
- package/skills/autoresearch/SKILL.md +43 -0
- package/skills/bear-notes/SKILL.md +34 -0
- package/skills/blogwatcher/SKILL.md +34 -0
- package/skills/blucli/SKILL.md +34 -0
- package/skills/bluebubbles/SKILL.md +34 -0
- package/skills/business-strategy/SKILL.md +41 -0
- package/skills/camsnap/SKILL.md +34 -0
- package/skills/canvas/SKILL.md +34 -0
- package/skills/clawhub/SKILL.md +34 -0
- package/skills/coding-agent/SKILL.md +34 -0
- package/skills/coding-discipline.skill/SKILL.md +34 -0
- package/skills/content-writer/SKILL.md +41 -0
- package/skills/discord/SKILL.md +34 -0
- package/skills/eightctl/SKILL.md +34 -0
- package/skills/execution-validation.skill/SKILL.md +34 -0
- package/skills/gemini/SKILL.md +34 -0
- package/skills/gh-issues/SKILL.md +34 -0
- package/skills/gifgrep/SKILL.md +34 -0
- package/skills/github/SKILL.md +41 -0
- package/skills/gog/SKILL.md +34 -0
- package/skills/goplaces/SKILL.md +34 -0
- package/skills/healthcheck/SKILL.md +34 -0
- package/skills/himalaya/SKILL.md +34 -0
- package/skills/humanize/SKILL.md +41 -0
- package/skills/imsg/SKILL.md +34 -0
- package/skills/itp/SKILL.md +112 -0
- package/skills/mcporter/SKILL.md +34 -0
- package/skills/model-usage/SKILL.md +34 -0
- package/skills/nano-pdf/SKILL.md +34 -0
- package/skills/node-connect/SKILL.md +34 -0
- package/skills/notion/SKILL.md +34 -0
- package/skills/obsidian/SKILL.md +34 -0
- package/skills/openai-whisper/SKILL.md +34 -0
- package/skills/openai-whisper-api/SKILL.md +34 -0
- package/skills/openhue/SKILL.md +34 -0
- package/skills/oracle/SKILL.md +34 -0
- package/skills/ordercli/SKILL.md +34 -0
- package/skills/peekaboo/SKILL.md +34 -0
- package/skills/polyclaw/SKILL.md +34 -0
- package/skills/prospector/SKILL.md +41 -0
- package/skills/rsi.skill/SKILL.md +34 -0
- package/skills/sag/SKILL.md +34 -0
- package/skills/security/SKILL.md +41 -0
- package/skills/session-logs/SKILL.md +34 -0
- package/skills/sherpa-onnx-tts/SKILL.md +34 -0
- package/skills/skill-creator/SKILL.md +34 -0
- package/skills/slack/SKILL.md +34 -0
- package/skills/songsee/SKILL.md +34 -0
- package/skills/sonoscli/SKILL.md +34 -0
- package/skills/spotify-player/SKILL.md +34 -0
- package/skills/strykr-prism/SKILL.md +41 -0
- package/skills/summarize/SKILL.md +34 -0
- package/skills/taskbridge/SKILL.md +34 -0
- package/skills/things-mac/SKILL.md +34 -0
- package/skills/tmux/SKILL.md +34 -0
- package/skills/trello/SKILL.md +34 -0
- package/skills/validator-agent/SKILL.md +41 -0
- package/skills/video-frames/SKILL.md +34 -0
- package/skills/voice-call/SKILL.md +34 -0
- package/skills/wacli/SKILL.md +34 -0
- package/skills/weather/SKILL.md +34 -0
- package/skills/webmcp-payments/SKILL.md +41 -0
- package/skills/xurl/SKILL.md +34 -0
- package/src/skills/catalog.ts +435 -435
- package/src/skills/executor.ts +56 -56
- package/src/skills/index.ts +3 -3
- package/src/skills/itp/SKILL.md +112 -112
- package/src/skills/loader.ts +262 -193
package/native/pyo3/src/lib.rs
CHANGED
|
@@ -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
|
+
}
|