@@ -98,17 +98,21 @@ class CedarPolicyBridge:
9898 behavioral trust on top.
9999 """
100100
101+ MAX_HISTORY = 10000 # Prevent unbounded memory growth
102+
101103 def __init__ (
102104 self ,
103105 trust_floor : int = 0 ,
104106 trust_bonus_per_allow : int = 50 ,
105107 deny_penalty : int = 200 ,
106108 require_receipt : bool = False ,
109+ max_history : int = MAX_HISTORY ,
107110 ):
108111 self .trust_floor = trust_floor
109112 self .trust_bonus = trust_bonus_per_allow
110113 self .deny_penalty = deny_penalty
111114 self .require_receipt = require_receipt
115+ self ._max_history = max_history
112116 self ._history : List [Dict [str , Any ]] = []
113117 self ._history_lock = threading .Lock ()
114118
@@ -141,8 +145,7 @@ def evaluate(
141145 result ["allowed" ] = False
142146 result ["reason" ] = "Decision receipt required but not provided"
143147 result ["adjusted_trust" ] = max (0 , agent_trust_score - self .deny_penalty )
144- with self ._history_lock :
145- self ._history .append (result )
148+ self ._record (result )
146149 return result
147150
148151 # Cedar deny is authoritative — not overridable by trust score
@@ -153,8 +156,7 @@ def evaluate(
153156 f"(policies: { cedar_decision .policy_ids } )"
154157 )
155158 result ["adjusted_trust" ] = max (0 , agent_trust_score - self .deny_penalty )
156- with self ._history_lock :
157- self ._history .append (result )
159+ self ._record (result )
158160 return result
159161
160162 # Cedar allow — layer AGT trust check
@@ -165,8 +167,7 @@ def evaluate(
165167 f"Cedar allowed but trust score { adjusted } below floor { self .trust_floor } "
166168 )
167169 result ["adjusted_trust" ] = adjusted
168- with self ._history_lock :
169- self ._history .append (result )
170+ self ._record (result )
170171 return result
171172
172173 result ["allowed" ] = True
@@ -182,6 +183,13 @@ def evaluate(
182183 self ._history .append (result )
183184 return result
184185
186+ def _record (self , entry : Dict [str , Any ]) -> None :
187+ """Append to history with bounded size to prevent memory leaks."""
188+ with self ._history_lock :
189+ self ._history .append (entry )
190+ if len (self ._history ) > self ._max_history :
191+ self ._history = self ._history [- self ._max_history :]
192+
185193 def get_history (self ) -> List [Dict [str , Any ]]:
186194 with self ._history_lock :
187195 return list (self ._history )
@@ -236,8 +244,11 @@ class ReceiptVerifier:
236244 "acta:artifact" ,
237245 }
238246
239- def __init__ (self , strict : bool = True ):
247+ MAX_LOG = 10000 # Prevent unbounded memory growth
248+
249+ def __init__ (self , strict : bool = True , max_log : int = MAX_LOG ):
240250 self .strict = strict
251+ self ._max_log = max_log
241252 self ._verified : List [Dict [str , Any ]] = []
242253 self ._verified_lock = threading .Lock ()
243254
@@ -277,14 +288,26 @@ def validate_structure_only(self, receipt: Dict[str, Any]) -> Dict[str, Any]:
277288 if not isinstance (payload , dict ):
278289 return {"valid" : False , "reason" : "Payload must be an object" }
279290
291+ # Empty signature/publicKey should not pass as valid
292+ sig = receipt .get ("signature" , "" )
293+ pk = receipt .get ("publicKey" , "" )
294+ if not sig or not pk :
295+ return {
296+ "valid" : False ,
297+ "reason" : "Empty signature or publicKey (cryptographic fields must be non-empty)" ,
298+ "receipt_type" : receipt_type ,
299+ "has_signature" : bool (sig ),
300+ "has_public_key" : bool (pk ),
301+ }
302+
280303 result = {
281304 "valid" : True ,
282305 "receipt_type" : receipt_type ,
283306 "tool" : payload .get ("tool" , payload .get ("resource" , "" )),
284307 "decision" : payload .get ("effect" , payload .get ("decision" , "" )),
285308 "timestamp" : payload .get ("timestamp" ),
286- "has_signature" : bool ( receipt . get ( "signature" )) ,
287- "has_public_key" : bool ( receipt . get ( "publicKey" )) ,
309+ "has_signature" : True ,
310+ "has_public_key" : True ,
288311 }
289312
290313 # Spending authority specific fields
@@ -296,6 +319,8 @@ def validate_structure_only(self, receipt: Dict[str, Any]) -> Dict[str, Any]:
296319
297320 with self ._verified_lock :
298321 self ._verified .append (result )
322+ if len (self ._verified ) > self ._max_log :
323+ self ._verified = self ._verified [- self ._max_log :]
299324 return result
300325
301326 def to_agt_context (self , receipt : Dict [str , Any ]) -> Dict [str , Any ]:
@@ -350,15 +375,19 @@ class SpendingGate:
350375
351376 UTILIZATION_BANDS = {"low" , "medium" , "high" , "exceeded" }
352377
378+ MAX_LOG = 10000 # Prevent unbounded memory growth
379+
353380 def __init__ (
354381 self ,
355382 max_single_amount : float = 10000.0 ,
356383 high_util_trust_floor : int = 500 ,
357384 blocked_categories : Optional [List [str ]] = None ,
385+ max_log : int = MAX_LOG ,
358386 ):
359387 self .max_single_amount = max_single_amount
360388 self .high_util_trust_floor = high_util_trust_floor
361389 self .blocked_categories = set (blocked_categories or [])
390+ self ._max_log = max_log
362391 self ._decisions : List [Dict [str , Any ]] = []
363392 self ._decisions_lock = threading .Lock ()
364393
@@ -398,38 +427,33 @@ def evaluate_spend(
398427 f"Amount { amount } { currency } exceeds single-transaction "
399428 f"limit of { self .max_single_amount } { currency } "
400429 )
401- with self ._decisions_lock :
402- self ._decisions .append (result )
430+ self ._record (result )
403431 return result
404432
405433 if amount <= 0 :
406434 result ["allowed" ] = False
407435 result ["reason" ] = "Amount must be positive"
408- with self ._decisions_lock :
409- self ._decisions .append (result )
436+ self ._record (result )
410437 return result
411438
412439 # 2. Category check
413440 if category in self .blocked_categories :
414441 result ["allowed" ] = False
415442 result ["reason" ] = f"Category '{ category } ' is blocked"
416- with self ._decisions_lock :
417- self ._decisions .append (result )
443+ self ._record (result )
418444 return result
419445
420446 # 3. Utilization + trust
421447 if utilization_band not in self .UTILIZATION_BANDS :
422448 result ["allowed" ] = False
423449 result ["reason" ] = f"Invalid utilization band: { utilization_band } "
424- with self ._decisions_lock :
425- self ._decisions .append (result )
450+ self ._record (result )
426451 return result
427452
428453 if utilization_band == "exceeded" :
429454 result ["allowed" ] = False
430455 result ["reason" ] = "Budget utilization exceeded"
431- with self ._decisions_lock :
432- self ._decisions .append (result )
456+ self ._record (result )
433457 return result
434458
435459 if utilization_band == "high" and agent_trust_score < self .high_util_trust_floor :
@@ -438,8 +462,7 @@ def evaluate_spend(
438462 f"High utilization requires trust score >= { self .high_util_trust_floor } "
439463 f"(current: { agent_trust_score } )"
440464 )
441- with self ._decisions_lock :
442- self ._decisions .append (result )
465+ self ._record (result )
443466 return result
444467
445468 # 4. Receipt check for high-value transactions
@@ -448,8 +471,7 @@ def evaluate_spend(
448471 result ["reason" ] = (
449472 f"Transactions above 1000 { currency } require a spending authority receipt"
450473 )
451- with self ._decisions_lock :
452- self ._decisions .append (result )
474+ self ._record (result )
453475 return result
454476
455477 result ["allowed" ] = True
@@ -463,6 +485,13 @@ def evaluate_spend(
463485 self ._decisions .append (result )
464486 return result
465487
488+ def _record (self , entry : Dict [str , Any ]) -> None :
489+ """Append to decisions with bounded size to prevent memory leaks."""
490+ with self ._decisions_lock :
491+ self ._decisions .append (entry )
492+ if len (self ._decisions ) > self ._max_log :
493+ self ._decisions = self ._decisions [- self ._max_log :]
494+
466495 def get_decisions (self ) -> List [Dict [str , Any ]]:
467496 with self ._decisions_lock :
468497 return list (self ._decisions )
@@ -509,7 +538,7 @@ def scopeblind_context(
509538 """
510539 ctx : Dict [str , Any ] = {
511540 "source" : "scopeblind:protect-mcp" ,
512- "version" : "0.4.6 " ,
541+ "version" : "0.5.2 " ,
513542 }
514543
515544 if cedar_decision is not None :
0 commit comments