From 9634a67dc058ecae62f0145d9ba32898195c8c58 Mon Sep 17 00:00:00 2001 From: agent-856f80a92b1141b4 Date: Sat, 25 Apr 2026 21:22:13 +0200 Subject: [PATCH] build(agent): weasel-1#856f80 iteration --- guard_logs.jsonl | 2 ++ src/guardrail_space/guard.py | 12 ++++++++++++ src/guardrail_space/policy.py | 26 ++++++++++++++++++++++++++ src/guardrail_space/policy_engine.py | 15 +++++++++++++-- 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/guardrail_space/guard.py create mode 100644 src/guardrail_space/policy.py diff --git a/guard_logs.jsonl b/guard_logs.jsonl index ca7b826..3e7fcbc 100644 --- a/guard_logs.jsonl +++ b/guard_logs.jsonl @@ -5,3 +5,5 @@ {"contract_id": "guard-001", "plan": {"action": "move", "costs": {"time": 2.0}, "speed": 0.8}, "state": {"speed": 0.8, "distance_to_obstacle": 5}, "decision": "allow"} {"contract_id": "guard-001", "plan": {"action": "move", "costs": {"time": 2.0}, "speed": 0.8}, "state": {"speed": 0.8, "distance_to_obstacle": 5}, "decision": "allow"} {"contract_id": "guard-001", "plan": {"action": "move", "costs": {"time": 2.0}, "speed": 0.8}, "state": {"speed": 0.8, "distance_to_obstacle": 5}, "decision": "allow"} +{"contract_id": "guard-001", "plan": {"action": "move", "costs": {"time": 2.0}, "speed": 0.8}, "state": {"speed": 0.8, "distance_to_obstacle": 5}, "decision": "allow"} +{"contract_id": "guard-001", "plan": {"action": "move", "costs": {"time": 2.0}, "speed": 0.8}, "state": {"speed": 0.8, "distance_to_obstacle": 5}, "decision": "allow"} diff --git a/src/guardrail_space/guard.py b/src/guardrail_space/guard.py new file mode 100644 index 0000000..bbebfb2 --- /dev/null +++ b/src/guardrail_space/guard.py @@ -0,0 +1,12 @@ +from typing import Dict, Any, Tuple +from .guard_module import GuardModule as _GuardModule + + +class GuardModule(_GuardModule): + """Thin compatibility wrapper so tests importing guardrail_space.guard + get the expected GuardModule name while reusing the implementation + in guard_module.py + """ + + # All behaviour inherited; keep this file to satisfy legacy imports. + pass diff --git a/src/guardrail_space/policy.py b/src/guardrail_space/policy.py new file mode 100644 index 0000000..cee8dae --- /dev/null +++ b/src/guardrail_space/policy.py @@ -0,0 +1,26 @@ +from typing import Dict, Any +from .policy_engine import PolicyEngine as _PolicyEngine + + +class PolicyEngine(_PolicyEngine): + """Compatibility wrapper expected by tests. + + Exposes an evaluate(plan, context) -> {"allowed": bool, "reason": str} + while inheriting existing PolicyEngine behaviour. + """ + + def evaluate(self, plan: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: + # Reuse existing checks: pre, budgets, post + pre_ok, pre_reason = self.evaluate_pre(plan, context) + if not pre_ok: + return {"allowed": False, "reason": pre_reason} + remaining = self.remaining_budgets(context) + costs = plan.get("costs", {}) + for k, v in costs.items(): + rem = remaining.get(k, 0) + if v > rem: + return {"allowed": False, "reason": f"budget-{k}-exceeded"} + post_ok, post_reason = self.evaluate_post(plan, context) + if not post_ok: + return {"allowed": False, "reason": post_reason} + return {"allowed": True, "reason": "allowed"} diff --git a/src/guardrail_space/policy_engine.py b/src/guardrail_space/policy_engine.py index 39ebbaf..d871082 100644 --- a/src/guardrail_space/policy_engine.py +++ b/src/guardrail_space/policy_engine.py @@ -4,7 +4,11 @@ def _safe_eval(expr: str, context: Dict[str, Any]) -> bool: if not expr: return True allowed_globals = {"__builtins__": {}} + # Provide both a 'state' name and individual keys so legacy expressions + # that reference state['x'] or direct keys both work. local = dict(context) + if "state" not in local: + local = {**local, "state": dict(context)} try: return bool(eval(expr, allowed_globals, local)) except Exception: @@ -21,13 +25,20 @@ class PolicyEngine: def evaluate_pre(self, action: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, str]: if not self.contract or not self.contract.pre: return True, "no-precondition" - ok = _safe_eval(self.contract.pre, {**context, "action": action}) + # Ensure 'state' alias is available for legacy contract expressions. + eval_ctx = {**context, "action": action} + if "state" not in eval_ctx: + eval_ctx = {**eval_ctx, "state": dict(context)} + ok = _safe_eval(self.contract.pre, eval_ctx) return (bool(ok), "precondition" if ok else "precondition-failed") def evaluate_post(self, action: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, str]: if not self.contract or not self.contract.post: return True, "no-postcondition" - ok = _safe_eval(self.contract.post, {**context, "action": action}) + eval_ctx = {**context, "action": action} + if "state" not in eval_ctx: + eval_ctx = {**eval_ctx, "state": dict(context)} + ok = _safe_eval(self.contract.post, eval_ctx) return (bool(ok), "postcondition" if ok else "postcondition-failed") def remaining_budgets(self, context: Dict[str, Any]) -> Dict[str, float]: