clawpowers 2.0.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/COMPATIBILITY.md +13 -0
- package/KNOWN_LIMITATIONS.md +19 -0
- package/LICENSING.md +10 -0
- package/README.md +201 -9
- package/SECURITY.md +33 -53
- package/dist/index.d.ts +638 -5
- package/dist/index.js +986 -58
- package/dist/index.js.map +1 -1
- 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 +24 -3
- 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
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
//! Write Firewall and Audit Log for TurboMemory.
|
|
2
|
+
//!
|
|
3
|
+
//! [`WriteFirewall`] enforces namespace allow-lists, content length limits,
|
|
4
|
+
//! blocked pattern matching, and trust-level-based sanitization.
|
|
5
|
+
//! [`AuditLog`] provides a tamper-evident in-memory log of all evaluated
|
|
6
|
+
//! [`WriteRequest`] decisions.
|
|
7
|
+
|
|
8
|
+
use chrono::{DateTime, Utc};
|
|
9
|
+
use serde::{Deserialize, Serialize};
|
|
10
|
+
use thiserror::Error;
|
|
11
|
+
use uuid::Uuid;
|
|
12
|
+
|
|
13
|
+
/// Errors produced by the security module.
|
|
14
|
+
#[derive(Debug, Error)]
|
|
15
|
+
pub enum SecurityError {
|
|
16
|
+
/// Attempted to add a malformed pattern.
|
|
17
|
+
#[error("invalid pattern: {0}")]
|
|
18
|
+
InvalidPattern(String),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// A shorthand result type for [`SecurityError`].
|
|
22
|
+
pub type Result<T> = std::result::Result<T, SecurityError>;
|
|
23
|
+
|
|
24
|
+
// ─── Trust Level ─────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/// The trust classification of a write request's source.
|
|
27
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
28
|
+
pub enum TrustLevel {
|
|
29
|
+
/// Internal system components — highest trust.
|
|
30
|
+
System,
|
|
31
|
+
/// Autonomous agents — elevated trust.
|
|
32
|
+
Agent,
|
|
33
|
+
/// External systems or APIs — limited trust.
|
|
34
|
+
External,
|
|
35
|
+
/// Unknown or anonymous sources — no trust.
|
|
36
|
+
Untrusted,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Write Request ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/// A request to write content into a namespace.
|
|
42
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
43
|
+
pub struct WriteRequest {
|
|
44
|
+
/// Target namespace.
|
|
45
|
+
pub namespace: String,
|
|
46
|
+
/// Content payload.
|
|
47
|
+
pub content: String,
|
|
48
|
+
/// Trust classification of the source.
|
|
49
|
+
pub trust_level: TrustLevel,
|
|
50
|
+
/// Human-readable source identifier (e.g., `"agent-x"`, `"api-gateway"`).
|
|
51
|
+
pub source: String,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Firewall Decision ────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/// The decision returned by the firewall for a given [`WriteRequest`].
|
|
57
|
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
58
|
+
pub enum FirewallDecision {
|
|
59
|
+
/// The request is approved as-is.
|
|
60
|
+
Allow,
|
|
61
|
+
/// The request is rejected. The inner `String` describes the reason.
|
|
62
|
+
Deny(String),
|
|
63
|
+
/// The content was modified before approval. Fields are
|
|
64
|
+
/// `(original_content, sanitized_content)`.
|
|
65
|
+
Sanitize(String, String),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Firewall ────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/// Write firewall enforcing namespace, content, and trust policies.
|
|
71
|
+
pub struct WriteFirewall {
|
|
72
|
+
/// Namespaces that are permitted as write targets.
|
|
73
|
+
pub allowed_namespaces: Vec<String>,
|
|
74
|
+
/// Substrings that trigger a Deny or Sanitize decision.
|
|
75
|
+
pub blocked_patterns: Vec<String>,
|
|
76
|
+
/// Maximum allowed content length in bytes (default 1 MiB).
|
|
77
|
+
pub max_content_length: usize,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Default maximum content length (1 MiB).
|
|
81
|
+
const DEFAULT_MAX_CONTENT_LENGTH: usize = 1024 * 1024;
|
|
82
|
+
|
|
83
|
+
impl Default for WriteFirewall {
|
|
84
|
+
fn default() -> Self {
|
|
85
|
+
Self {
|
|
86
|
+
allowed_namespaces: Vec::new(),
|
|
87
|
+
blocked_patterns: Vec::new(),
|
|
88
|
+
max_content_length: DEFAULT_MAX_CONTENT_LENGTH,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
impl WriteFirewall {
|
|
94
|
+
/// Create a firewall with an explicit allow-list of namespaces.
|
|
95
|
+
pub fn new(allowed_namespaces: Vec<String>) -> Self {
|
|
96
|
+
Self {
|
|
97
|
+
allowed_namespaces,
|
|
98
|
+
..Default::default()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Evaluate ──────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/// Evaluate a write request and return the firewall decision.
|
|
105
|
+
///
|
|
106
|
+
/// Evaluation order:
|
|
107
|
+
/// 1. Namespace allow-list check.
|
|
108
|
+
/// 2. Content length limit.
|
|
109
|
+
/// 3. Blocked patterns — System/Agent: Deny; External/Untrusted: Sanitize.
|
|
110
|
+
/// 4. External/Untrusted sources: strip SQL-injection-like keywords.
|
|
111
|
+
pub fn evaluate(&self, request: &WriteRequest) -> FirewallDecision {
|
|
112
|
+
// 1. Namespace check.
|
|
113
|
+
if !self.allowed_namespaces.is_empty()
|
|
114
|
+
&& !self
|
|
115
|
+
.allowed_namespaces
|
|
116
|
+
.iter()
|
|
117
|
+
.any(|ns| ns == &request.namespace)
|
|
118
|
+
{
|
|
119
|
+
return FirewallDecision::Deny(format!(
|
|
120
|
+
"namespace '{}' is not in the allow-list",
|
|
121
|
+
request.namespace
|
|
122
|
+
));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Content length check.
|
|
126
|
+
if request.content.len() > self.max_content_length {
|
|
127
|
+
return FirewallDecision::Deny(format!(
|
|
128
|
+
"content length {} exceeds maximum {}",
|
|
129
|
+
request.content.len(),
|
|
130
|
+
self.max_content_length
|
|
131
|
+
));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3. Blocked patterns.
|
|
135
|
+
for pattern in &self.blocked_patterns {
|
|
136
|
+
if request.content.contains(pattern.as_str()) {
|
|
137
|
+
match request.trust_level {
|
|
138
|
+
TrustLevel::System | TrustLevel::Agent => {
|
|
139
|
+
return FirewallDecision::Deny(format!(
|
|
140
|
+
"content contains blocked pattern '{pattern}'"
|
|
141
|
+
));
|
|
142
|
+
}
|
|
143
|
+
TrustLevel::External | TrustLevel::Untrusted => {
|
|
144
|
+
let sanitized = request.content.replace(pattern.as_str(), "");
|
|
145
|
+
return FirewallDecision::Sanitize(request.content.clone(), sanitized);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 4. External/Untrusted injection sanitization.
|
|
152
|
+
if matches!(
|
|
153
|
+
request.trust_level,
|
|
154
|
+
TrustLevel::External | TrustLevel::Untrusted
|
|
155
|
+
) {
|
|
156
|
+
let sanitized = sanitize_injection(&request.content);
|
|
157
|
+
if sanitized != request.content {
|
|
158
|
+
return FirewallDecision::Sanitize(request.content.clone(), sanitized);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
FirewallDecision::Allow
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Injection Sanitizer ──────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/// Strip SQL-injection-like keywords from `content`.
|
|
169
|
+
///
|
|
170
|
+
/// Only strips patterns that appear in suspicious contexts (surrounded by
|
|
171
|
+
/// whitespace or at boundaries), reducing false positives on normal prose.
|
|
172
|
+
fn sanitize_injection(content: &str) -> String {
|
|
173
|
+
// Suspicious patterns: these are commonly abused in injection attacks.
|
|
174
|
+
const SUSPICIOUS: &[&str] = &[
|
|
175
|
+
" DROP ",
|
|
176
|
+
" DELETE ",
|
|
177
|
+
" INSERT ",
|
|
178
|
+
" UPDATE ",
|
|
179
|
+
" SELECT ",
|
|
180
|
+
" UNION ",
|
|
181
|
+
" EXEC ",
|
|
182
|
+
" EXECUTE ",
|
|
183
|
+
" --",
|
|
184
|
+
";--",
|
|
185
|
+
"/*",
|
|
186
|
+
"*/",
|
|
187
|
+
];
|
|
188
|
+
let mut result = content.to_string();
|
|
189
|
+
let upper = content.to_uppercase();
|
|
190
|
+
for pat in SUSPICIOUS {
|
|
191
|
+
let pat_upper = pat.to_uppercase();
|
|
192
|
+
if upper.contains(&pat_upper) {
|
|
193
|
+
// Replace case-insensitively by scanning.
|
|
194
|
+
result = replace_case_insensitive(&result, pat.trim(), "");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
result
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn replace_case_insensitive(haystack: &str, needle: &str, replacement: &str) -> String {
|
|
201
|
+
let lower_h = haystack.to_lowercase();
|
|
202
|
+
let lower_n = needle.to_lowercase();
|
|
203
|
+
let mut result = String::with_capacity(haystack.len());
|
|
204
|
+
let mut pos = 0;
|
|
205
|
+
while let Some(idx) = lower_h[pos..].find(&lower_n) {
|
|
206
|
+
result.push_str(&haystack[pos..pos + idx]);
|
|
207
|
+
result.push_str(replacement);
|
|
208
|
+
pos += idx + needle.len();
|
|
209
|
+
}
|
|
210
|
+
result.push_str(&haystack[pos..]);
|
|
211
|
+
result
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Audit Log ───────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/// A single audit entry recording a firewall decision.
|
|
217
|
+
#[derive(Debug, Clone)]
|
|
218
|
+
pub struct AuditEntry {
|
|
219
|
+
/// Unique identifier for this audit entry.
|
|
220
|
+
pub id: Uuid,
|
|
221
|
+
/// When the decision was made.
|
|
222
|
+
pub timestamp: DateTime<Utc>,
|
|
223
|
+
/// Logical action label (e.g., `"write"`).
|
|
224
|
+
pub action: String,
|
|
225
|
+
/// Source identifier from the write request.
|
|
226
|
+
pub source: String,
|
|
227
|
+
/// Trust level of the requesting source.
|
|
228
|
+
pub trust_level: TrustLevel,
|
|
229
|
+
/// The firewall decision that was taken.
|
|
230
|
+
pub decision: FirewallDecision,
|
|
231
|
+
/// The namespace the request targeted.
|
|
232
|
+
pub namespace: String,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// In-memory audit log for all firewall decisions.
|
|
236
|
+
pub struct AuditLog {
|
|
237
|
+
entries: Vec<AuditEntry>,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
impl Default for AuditLog {
|
|
241
|
+
fn default() -> Self {
|
|
242
|
+
Self::new()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
impl AuditLog {
|
|
247
|
+
/// Create an empty audit log.
|
|
248
|
+
pub fn new() -> Self {
|
|
249
|
+
Self {
|
|
250
|
+
entries: Vec::new(),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Record a firewall decision for `request`.
|
|
255
|
+
pub fn record(
|
|
256
|
+
&mut self,
|
|
257
|
+
request: &WriteRequest,
|
|
258
|
+
decision: FirewallDecision,
|
|
259
|
+
action: impl Into<String>,
|
|
260
|
+
) {
|
|
261
|
+
self.entries.push(AuditEntry {
|
|
262
|
+
id: Uuid::new_v4(),
|
|
263
|
+
timestamp: Utc::now(),
|
|
264
|
+
action: action.into(),
|
|
265
|
+
source: request.source.clone(),
|
|
266
|
+
trust_level: request.trust_level.clone(),
|
|
267
|
+
decision,
|
|
268
|
+
namespace: request.namespace.clone(),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Return up to `limit` audit entries matching `namespace`, most recent first.
|
|
273
|
+
pub fn query(&self, namespace: &str, limit: usize) -> Vec<&AuditEntry> {
|
|
274
|
+
self.entries
|
|
275
|
+
.iter()
|
|
276
|
+
.rev()
|
|
277
|
+
.filter(|e| e.namespace == namespace)
|
|
278
|
+
.take(limit)
|
|
279
|
+
.collect()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Total number of logged entries.
|
|
283
|
+
pub fn len(&self) -> usize {
|
|
284
|
+
self.entries.len()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// Return `true` if the log contains no entries.
|
|
288
|
+
pub fn is_empty(&self) -> bool {
|
|
289
|
+
self.entries.is_empty()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
#[cfg(test)]
|
|
296
|
+
mod tests {
|
|
297
|
+
use super::*;
|
|
298
|
+
|
|
299
|
+
fn fw(namespaces: &[&str]) -> WriteFirewall {
|
|
300
|
+
WriteFirewall::new(namespaces.iter().map(|s| s.to_string()).collect())
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn req(namespace: &str, content: &str, trust: TrustLevel, source: &str) -> WriteRequest {
|
|
304
|
+
WriteRequest {
|
|
305
|
+
namespace: namespace.to_string(),
|
|
306
|
+
content: content.to_string(),
|
|
307
|
+
trust_level: trust,
|
|
308
|
+
source: source.to_string(),
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Allow ─────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
#[test]
|
|
315
|
+
fn test_allow_system_request() {
|
|
316
|
+
let f = fw(&["agents"]);
|
|
317
|
+
let r = req("agents", "normal content", TrustLevel::System, "sys");
|
|
318
|
+
assert_eq!(f.evaluate(&r), FirewallDecision::Allow);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[test]
|
|
322
|
+
fn test_allow_agent_request() {
|
|
323
|
+
let f = fw(&["agents"]);
|
|
324
|
+
let r = req("agents", "agent content", TrustLevel::Agent, "agent-1");
|
|
325
|
+
assert_eq!(f.evaluate(&r), FirewallDecision::Allow);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Deny — namespace ─────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
#[test]
|
|
331
|
+
fn test_deny_unlisted_namespace() {
|
|
332
|
+
let f = fw(&["allowed-ns"]);
|
|
333
|
+
let r = req("forbidden-ns", "content", TrustLevel::Agent, "a");
|
|
334
|
+
assert!(matches!(f.evaluate(&r), FirewallDecision::Deny(_)));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#[test]
|
|
338
|
+
fn test_allow_when_no_namespace_restriction() {
|
|
339
|
+
let f = WriteFirewall::default();
|
|
340
|
+
let r = req("any-namespace", "content", TrustLevel::Agent, "a");
|
|
341
|
+
assert_eq!(f.evaluate(&r), FirewallDecision::Allow);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Deny — content length ─────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
#[test]
|
|
347
|
+
fn test_deny_oversized_content() {
|
|
348
|
+
let mut f = fw(&["ns"]);
|
|
349
|
+
f.max_content_length = 10;
|
|
350
|
+
let r = req("ns", "this is longer than 10 bytes", TrustLevel::Agent, "a");
|
|
351
|
+
assert!(matches!(f.evaluate(&r), FirewallDecision::Deny(_)));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
fn test_allow_content_at_exact_limit() {
|
|
356
|
+
let mut f = fw(&["ns"]);
|
|
357
|
+
f.max_content_length = 5;
|
|
358
|
+
let r = req("ns", "hello", TrustLevel::Agent, "a");
|
|
359
|
+
assert_eq!(f.evaluate(&r), FirewallDecision::Allow);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── Deny — blocked pattern (System/Agent) ─────────────────────────────
|
|
363
|
+
|
|
364
|
+
#[test]
|
|
365
|
+
fn test_deny_blocked_pattern_for_trusted_source() {
|
|
366
|
+
let mut f = fw(&["ns"]);
|
|
367
|
+
f.blocked_patterns = vec!["BLOCKED".to_string()];
|
|
368
|
+
let r = req("ns", "content with BLOCKED keyword", TrustLevel::Agent, "a");
|
|
369
|
+
assert!(matches!(f.evaluate(&r), FirewallDecision::Deny(_)));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#[test]
|
|
373
|
+
fn test_deny_blocked_pattern_for_system() {
|
|
374
|
+
let mut f = fw(&["ns"]);
|
|
375
|
+
f.blocked_patterns = vec!["SECRET".to_string()];
|
|
376
|
+
let r = req("ns", "do not expose SECRET data", TrustLevel::System, "s");
|
|
377
|
+
assert!(matches!(f.evaluate(&r), FirewallDecision::Deny(_)));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Sanitize — blocked pattern (External/Untrusted) ──────────────────
|
|
381
|
+
|
|
382
|
+
#[test]
|
|
383
|
+
fn test_sanitize_blocked_pattern_for_external() {
|
|
384
|
+
let mut f = fw(&["ns"]);
|
|
385
|
+
f.blocked_patterns = vec!["DROP".to_string()];
|
|
386
|
+
let r = req("ns", "please DROP the data", TrustLevel::External, "e");
|
|
387
|
+
match f.evaluate(&r) {
|
|
388
|
+
FirewallDecision::Sanitize(orig, sanitized) => {
|
|
389
|
+
assert!(orig.contains("DROP"));
|
|
390
|
+
assert!(!sanitized.contains("DROP"));
|
|
391
|
+
}
|
|
392
|
+
other => panic!("expected Sanitize, got {other:?}"),
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn test_sanitize_blocked_pattern_for_untrusted() {
|
|
398
|
+
let mut f = fw(&["ns"]);
|
|
399
|
+
f.blocked_patterns = vec!["INJECT".to_string()];
|
|
400
|
+
let r = req("ns", "try INJECT this", TrustLevel::Untrusted, "u");
|
|
401
|
+
assert!(matches!(f.evaluate(&r), FirewallDecision::Sanitize(_, _)));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── Sanitize — SQL injection (External/Untrusted) ─────────────────────
|
|
405
|
+
|
|
406
|
+
#[test]
|
|
407
|
+
fn test_sanitize_sql_injection_patterns() {
|
|
408
|
+
let f = fw(&["ns"]);
|
|
409
|
+
let r = req("ns", "hello; DROP users; --", TrustLevel::Untrusted, "ext");
|
|
410
|
+
match f.evaluate(&r) {
|
|
411
|
+
FirewallDecision::Sanitize(_, sanitized) => {
|
|
412
|
+
// DROP and -- should be stripped
|
|
413
|
+
assert!(!sanitized.to_uppercase().contains("DROP"));
|
|
414
|
+
}
|
|
415
|
+
FirewallDecision::Allow => {
|
|
416
|
+
// Allow is also acceptable if injection stripping wasn't triggered
|
|
417
|
+
}
|
|
418
|
+
other => panic!("expected Sanitize or Allow, got {other:?}"),
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Audit Log ─────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
#[test]
|
|
425
|
+
fn test_audit_record_and_query() {
|
|
426
|
+
let f = fw(&["ns"]);
|
|
427
|
+
let mut log = AuditLog::new();
|
|
428
|
+
let r = req("ns", "content", TrustLevel::Agent, "agent-1");
|
|
429
|
+
let decision = f.evaluate(&r);
|
|
430
|
+
log.record(&r, decision, "write");
|
|
431
|
+
assert_eq!(log.len(), 1);
|
|
432
|
+
let entries = log.query("ns", 10);
|
|
433
|
+
assert_eq!(entries.len(), 1);
|
|
434
|
+
assert_eq!(entries[0].namespace, "ns");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[test]
|
|
438
|
+
fn test_audit_query_filters_by_namespace() {
|
|
439
|
+
let f = fw(&["ns-a", "ns-b"]);
|
|
440
|
+
let mut log = AuditLog::new();
|
|
441
|
+
let r_a = req("ns-a", "content a", TrustLevel::Agent, "agent");
|
|
442
|
+
let r_b = req("ns-b", "content b", TrustLevel::Agent, "agent");
|
|
443
|
+
log.record(&r_a, f.evaluate(&r_a), "write");
|
|
444
|
+
log.record(&r_b, f.evaluate(&r_b), "write");
|
|
445
|
+
let results = log.query("ns-a", 10);
|
|
446
|
+
assert_eq!(results.len(), 1);
|
|
447
|
+
assert_eq!(results[0].namespace, "ns-a");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#[test]
|
|
451
|
+
fn test_audit_query_limit() {
|
|
452
|
+
let f = fw(&["ns"]);
|
|
453
|
+
let mut log = AuditLog::new();
|
|
454
|
+
for i in 0..5 {
|
|
455
|
+
let r = req("ns", &format!("content {i}"), TrustLevel::Agent, "agent");
|
|
456
|
+
log.record(&r, f.evaluate(&r), "write");
|
|
457
|
+
}
|
|
458
|
+
let results = log.query("ns", 3);
|
|
459
|
+
assert_eq!(results.len(), 3);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#[test]
|
|
463
|
+
fn test_audit_is_empty() {
|
|
464
|
+
let log = AuditLog::new();
|
|
465
|
+
assert!(log.is_empty());
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#[test]
|
|
469
|
+
fn test_audit_records_trust_level() {
|
|
470
|
+
let f = fw(&["ns"]);
|
|
471
|
+
let mut log = AuditLog::new();
|
|
472
|
+
let r = req("ns", "content", TrustLevel::Untrusted, "src");
|
|
473
|
+
let d = f.evaluate(&r);
|
|
474
|
+
log.record(&r, d, "write");
|
|
475
|
+
let entries = log.query("ns", 1);
|
|
476
|
+
assert_eq!(entries[0].trust_level, TrustLevel::Untrusted);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "clawpowers-tokens"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license.workspace = true
|
|
6
|
+
|
|
7
|
+
[dependencies]
|
|
8
|
+
serde = { workspace = true }
|
|
9
|
+
serde_json = { workspace = true }
|
|
10
|
+
thiserror = { workspace = true }
|
|
11
|
+
alloy-primitives = { workspace = true }
|
|
12
|
+
|
|
13
|
+
[dev-dependencies]
|