clawpowers 1.1.4 → 2.2.0
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 +126 -0
- package/COMPATIBILITY.md +13 -0
- package/KNOWN_LIMITATIONS.md +19 -0
- package/LICENSE +44 -0
- package/LICENSING.md +10 -0
- package/README.md +378 -210
- package/SECURITY.md +52 -0
- package/dist/index.d.ts +1477 -0
- package/dist/index.js +3464 -0
- package/dist/index.js.map +1 -0
- package/native/Cargo.lock +4863 -0
- package/native/Cargo.toml +73 -0
- package/native/crates/canonical/Cargo.toml +24 -0
- package/native/crates/canonical/src/lib.rs +673 -0
- package/native/crates/compression/Cargo.toml +20 -0
- package/native/crates/compression/benches/compression_bench.rs +42 -0
- package/native/crates/compression/src/lib.rs +393 -0
- package/native/crates/evm-eth/Cargo.toml +13 -0
- package/native/crates/evm-eth/src/lib.rs +105 -0
- package/native/crates/fee/Cargo.toml +15 -0
- package/native/crates/fee/src/lib.rs +281 -0
- package/native/crates/index/Cargo.toml +16 -0
- package/native/crates/index/src/lib.rs +277 -0
- package/native/crates/policy/Cargo.toml +17 -0
- package/native/crates/policy/src/lib.rs +614 -0
- package/native/crates/security/Cargo.toml +22 -0
- package/native/crates/security/src/lib.rs +478 -0
- package/native/crates/tokens/Cargo.toml +13 -0
- package/native/crates/tokens/src/lib.rs +534 -0
- package/native/crates/verification/Cargo.toml +23 -0
- package/native/crates/verification/src/lib.rs +333 -0
- package/native/crates/wallet/Cargo.toml +20 -0
- package/native/crates/wallet/src/lib.rs +261 -0
- package/native/crates/x402/Cargo.toml +30 -0
- package/native/crates/x402/src/lib.rs +423 -0
- package/native/ffi/Cargo.toml +34 -0
- package/native/ffi/build.rs +4 -0
- package/native/ffi/index.node +0 -0
- package/native/ffi/src/lib.rs +352 -0
- package/native/ffi/tests/integration.rs +354 -0
- package/native/pyo3/Cargo.toml +26 -0
- package/native/pyo3/pyproject.toml +16 -0
- package/native/pyo3/src/lib.rs +407 -0
- package/native/pyo3/tests/test_smoke.py +180 -0
- package/native/wasm/Cargo.toml +44 -0
- package/native/wasm/pkg/.gitignore +6 -0
- package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -0
- package/native/wasm/pkg/clawpowers_wasm.js +872 -0
- package/native/wasm/pkg/clawpowers_wasm_bg.wasm +0 -0
- package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -0
- package/native/wasm/pkg/package.json +17 -0
- package/native/wasm/pkg-node/.gitignore +6 -0
- package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -0
- package/native/wasm/pkg-node/clawpowers_wasm.js +798 -0
- package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm +0 -0
- package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -0
- package/native/wasm/pkg-node/package.json +13 -0
- package/native/wasm/src/lib.rs +433 -0
- package/package.json +71 -44
- package/src/skills/catalog.ts +435 -0
- package/src/skills/executor.ts +56 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/itp/SKILL.md +112 -0
- package/src/skills/loader.ts +193 -0
- package/.claude-plugin/manifest.json +0 -19
- package/.codex/INSTALL.md +0 -36
- package/.cursor-plugin/manifest.json +0 -21
- package/.opencode/INSTALL.md +0 -52
- package/ARCHITECTURE.md +0 -69
- package/bin/clawpowers.js +0 -625
- package/bin/clawpowers.sh +0 -91
- package/docs/demo/clawpowers-demo.cast +0 -197
- package/docs/demo/clawpowers-demo.gif +0 -0
- package/docs/launch-images/25-skills-breakdown.jpg +0 -0
- package/docs/launch-images/clawpowers-vs-superpowers.jpg +0 -0
- package/docs/launch-images/economic-code-optimization.jpg +0 -0
- package/docs/launch-images/native-vs-bridge-2.jpg +0 -0
- package/docs/launch-images/native-vs-bridge.jpg +0 -0
- package/docs/launch-images/post1-hero-lobster.jpg +0 -0
- package/docs/launch-images/post2-dashboard.jpg +0 -0
- package/docs/launch-images/post3-superpowers.jpg +0 -0
- package/docs/launch-images/post4-before-after.jpg +0 -0
- package/docs/launch-images/post5-install-now.jpg +0 -0
- package/docs/launch-images/ultimate-stack.jpg +0 -0
- package/docs/launch-posts.md +0 -76
- package/docs/quickstart-first-transaction.md +0 -204
- package/gemini-extension.json +0 -32
- package/hooks/session-start +0 -205
- package/hooks/session-start.cmd +0 -43
- package/hooks/session-start.js +0 -163
- package/runtime/demo/README.md +0 -78
- package/runtime/demo/x402-mock-server.js +0 -230
- package/runtime/feedback/analyze.js +0 -621
- package/runtime/feedback/analyze.sh +0 -546
- package/runtime/init.js +0 -210
- package/runtime/init.sh +0 -178
- package/runtime/metrics/collector.js +0 -361
- package/runtime/metrics/collector.sh +0 -308
- package/runtime/payments/ledger.js +0 -305
- package/runtime/payments/ledger.sh +0 -262
- package/runtime/payments/pipeline.js +0 -455
- package/runtime/persistence/store.js +0 -433
- package/runtime/persistence/store.sh +0 -303
- package/skill.json +0 -106
- package/skills/agent-bounties/SKILL.md +0 -553
- package/skills/agent-payments/SKILL.md +0 -479
- package/skills/brainstorming/SKILL.md +0 -233
- package/skills/content-pipeline/SKILL.md +0 -282
- package/skills/cross-project-knowledge/SKILL.md +0 -345
- package/skills/dispatching-parallel-agents/SKILL.md +0 -305
- package/skills/economic-code-optimization/SKILL.md +0 -265
- package/skills/executing-plans/SKILL.md +0 -255
- package/skills/finishing-a-development-branch/SKILL.md +0 -260
- package/skills/formal-verification-lite/SKILL.md +0 -441
- package/skills/learn-how-to-learn/SKILL.md +0 -235
- package/skills/market-intelligence/SKILL.md +0 -323
- package/skills/meta-skill-evolution/SKILL.md +0 -325
- package/skills/prospecting/SKILL.md +0 -454
- package/skills/receiving-code-review/SKILL.md +0 -225
- package/skills/requesting-code-review/SKILL.md +0 -206
- package/skills/security-audit/SKILL.md +0 -353
- package/skills/self-healing-code/SKILL.md +0 -369
- package/skills/subagent-driven-development/SKILL.md +0 -244
- package/skills/systematic-debugging/SKILL.md +0 -355
- package/skills/test-driven-development/SKILL.md +0 -416
- package/skills/using-clawpowers/SKILL.md +0 -160
- package/skills/using-git-worktrees/SKILL.md +0 -261
- package/skills/validator/SKILL.md +0 -281
- package/skills/verification-before-completion/SKILL.md +0 -254
- package/skills/writing-plans/SKILL.md +0 -276
- package/skills/writing-skills/SKILL.md +0 -260
|
@@ -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"
|