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.
Files changed (131) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/COMPATIBILITY.md +13 -0
  3. package/KNOWN_LIMITATIONS.md +19 -0
  4. package/LICENSE +44 -0
  5. package/LICENSING.md +10 -0
  6. package/README.md +378 -210
  7. package/SECURITY.md +52 -0
  8. package/dist/index.d.ts +1477 -0
  9. package/dist/index.js +3464 -0
  10. package/dist/index.js.map +1 -0
  11. package/native/Cargo.lock +4863 -0
  12. package/native/Cargo.toml +73 -0
  13. package/native/crates/canonical/Cargo.toml +24 -0
  14. package/native/crates/canonical/src/lib.rs +673 -0
  15. package/native/crates/compression/Cargo.toml +20 -0
  16. package/native/crates/compression/benches/compression_bench.rs +42 -0
  17. package/native/crates/compression/src/lib.rs +393 -0
  18. package/native/crates/evm-eth/Cargo.toml +13 -0
  19. package/native/crates/evm-eth/src/lib.rs +105 -0
  20. package/native/crates/fee/Cargo.toml +15 -0
  21. package/native/crates/fee/src/lib.rs +281 -0
  22. package/native/crates/index/Cargo.toml +16 -0
  23. package/native/crates/index/src/lib.rs +277 -0
  24. package/native/crates/policy/Cargo.toml +17 -0
  25. package/native/crates/policy/src/lib.rs +614 -0
  26. package/native/crates/security/Cargo.toml +22 -0
  27. package/native/crates/security/src/lib.rs +478 -0
  28. package/native/crates/tokens/Cargo.toml +13 -0
  29. package/native/crates/tokens/src/lib.rs +534 -0
  30. package/native/crates/verification/Cargo.toml +23 -0
  31. package/native/crates/verification/src/lib.rs +333 -0
  32. package/native/crates/wallet/Cargo.toml +20 -0
  33. package/native/crates/wallet/src/lib.rs +261 -0
  34. package/native/crates/x402/Cargo.toml +30 -0
  35. package/native/crates/x402/src/lib.rs +423 -0
  36. package/native/ffi/Cargo.toml +34 -0
  37. package/native/ffi/build.rs +4 -0
  38. package/native/ffi/index.node +0 -0
  39. package/native/ffi/src/lib.rs +352 -0
  40. package/native/ffi/tests/integration.rs +354 -0
  41. package/native/pyo3/Cargo.toml +26 -0
  42. package/native/pyo3/pyproject.toml +16 -0
  43. package/native/pyo3/src/lib.rs +407 -0
  44. package/native/pyo3/tests/test_smoke.py +180 -0
  45. package/native/wasm/Cargo.toml +44 -0
  46. package/native/wasm/pkg/.gitignore +6 -0
  47. package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -0
  48. package/native/wasm/pkg/clawpowers_wasm.js +872 -0
  49. package/native/wasm/pkg/clawpowers_wasm_bg.wasm +0 -0
  50. package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -0
  51. package/native/wasm/pkg/package.json +17 -0
  52. package/native/wasm/pkg-node/.gitignore +6 -0
  53. package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -0
  54. package/native/wasm/pkg-node/clawpowers_wasm.js +798 -0
  55. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm +0 -0
  56. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -0
  57. package/native/wasm/pkg-node/package.json +13 -0
  58. package/native/wasm/src/lib.rs +433 -0
  59. package/package.json +71 -44
  60. package/src/skills/catalog.ts +435 -0
  61. package/src/skills/executor.ts +56 -0
  62. package/src/skills/index.ts +3 -0
  63. package/src/skills/itp/SKILL.md +112 -0
  64. package/src/skills/loader.ts +193 -0
  65. package/.claude-plugin/manifest.json +0 -19
  66. package/.codex/INSTALL.md +0 -36
  67. package/.cursor-plugin/manifest.json +0 -21
  68. package/.opencode/INSTALL.md +0 -52
  69. package/ARCHITECTURE.md +0 -69
  70. package/bin/clawpowers.js +0 -625
  71. package/bin/clawpowers.sh +0 -91
  72. package/docs/demo/clawpowers-demo.cast +0 -197
  73. package/docs/demo/clawpowers-demo.gif +0 -0
  74. package/docs/launch-images/25-skills-breakdown.jpg +0 -0
  75. package/docs/launch-images/clawpowers-vs-superpowers.jpg +0 -0
  76. package/docs/launch-images/economic-code-optimization.jpg +0 -0
  77. package/docs/launch-images/native-vs-bridge-2.jpg +0 -0
  78. package/docs/launch-images/native-vs-bridge.jpg +0 -0
  79. package/docs/launch-images/post1-hero-lobster.jpg +0 -0
  80. package/docs/launch-images/post2-dashboard.jpg +0 -0
  81. package/docs/launch-images/post3-superpowers.jpg +0 -0
  82. package/docs/launch-images/post4-before-after.jpg +0 -0
  83. package/docs/launch-images/post5-install-now.jpg +0 -0
  84. package/docs/launch-images/ultimate-stack.jpg +0 -0
  85. package/docs/launch-posts.md +0 -76
  86. package/docs/quickstart-first-transaction.md +0 -204
  87. package/gemini-extension.json +0 -32
  88. package/hooks/session-start +0 -205
  89. package/hooks/session-start.cmd +0 -43
  90. package/hooks/session-start.js +0 -163
  91. package/runtime/demo/README.md +0 -78
  92. package/runtime/demo/x402-mock-server.js +0 -230
  93. package/runtime/feedback/analyze.js +0 -621
  94. package/runtime/feedback/analyze.sh +0 -546
  95. package/runtime/init.js +0 -210
  96. package/runtime/init.sh +0 -178
  97. package/runtime/metrics/collector.js +0 -361
  98. package/runtime/metrics/collector.sh +0 -308
  99. package/runtime/payments/ledger.js +0 -305
  100. package/runtime/payments/ledger.sh +0 -262
  101. package/runtime/payments/pipeline.js +0 -455
  102. package/runtime/persistence/store.js +0 -433
  103. package/runtime/persistence/store.sh +0 -303
  104. package/skill.json +0 -106
  105. package/skills/agent-bounties/SKILL.md +0 -553
  106. package/skills/agent-payments/SKILL.md +0 -479
  107. package/skills/brainstorming/SKILL.md +0 -233
  108. package/skills/content-pipeline/SKILL.md +0 -282
  109. package/skills/cross-project-knowledge/SKILL.md +0 -345
  110. package/skills/dispatching-parallel-agents/SKILL.md +0 -305
  111. package/skills/economic-code-optimization/SKILL.md +0 -265
  112. package/skills/executing-plans/SKILL.md +0 -255
  113. package/skills/finishing-a-development-branch/SKILL.md +0 -260
  114. package/skills/formal-verification-lite/SKILL.md +0 -441
  115. package/skills/learn-how-to-learn/SKILL.md +0 -235
  116. package/skills/market-intelligence/SKILL.md +0 -323
  117. package/skills/meta-skill-evolution/SKILL.md +0 -325
  118. package/skills/prospecting/SKILL.md +0 -454
  119. package/skills/receiving-code-review/SKILL.md +0 -225
  120. package/skills/requesting-code-review/SKILL.md +0 -206
  121. package/skills/security-audit/SKILL.md +0 -353
  122. package/skills/self-healing-code/SKILL.md +0 -369
  123. package/skills/subagent-driven-development/SKILL.md +0 -244
  124. package/skills/systematic-debugging/SKILL.md +0 -355
  125. package/skills/test-driven-development/SKILL.md +0 -416
  126. package/skills/using-clawpowers/SKILL.md +0 -160
  127. package/skills/using-git-worktrees/SKILL.md +0 -261
  128. package/skills/validator/SKILL.md +0 -281
  129. package/skills/verification-before-completion/SKILL.md +0 -254
  130. package/skills/writing-plans/SKILL.md +0 -276
  131. package/skills/writing-skills/SKILL.md +0 -260
@@ -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]