Skip to content

Commit f0fb22f

Browse files
authored
Merge branch 'main' into feat/117-dlq-cloudwatch-alarms
2 parents cfc454e + d164e36 commit f0fb22f

3 files changed

Lines changed: 119 additions & 1 deletion

File tree

agent/src/policy.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,43 @@ def _load_shared_constants() -> dict:
110110
_ATS = _SHARED_CONSTANTS["approval_timeout_s"]
111111
FLOOR_TIMEOUT_S: int = int(_ATS["min"]) # §6 decision #6: rejected below this at load
112112
DEFAULT_TASK_TIMEOUT_S: int = int(_ATS["default"]) # §6 decision #6 default
113+
114+
115+
def _validate_constants() -> None:
116+
"""Fail-fast on invariant violations in contracts/constants.json."""
117+
if FLOOR_TIMEOUT_S <= 0:
118+
raise ValueError(
119+
f"contracts/constants.json: approval_timeout_s.min must be > 0, got {FLOOR_TIMEOUT_S}"
120+
)
121+
if DEFAULT_TASK_TIMEOUT_S < FLOOR_TIMEOUT_S:
122+
raise ValueError(
123+
f"contracts/constants.json: approval_timeout_s.default ({DEFAULT_TASK_TIMEOUT_S}) "
124+
f"must be >= min ({FLOOR_TIMEOUT_S})"
125+
)
126+
ats_max = int(_ATS["max"])
127+
if ats_max < DEFAULT_TASK_TIMEOUT_S:
128+
raise ValueError(
129+
f"contracts/constants.json: approval_timeout_s.max ({ats_max}) "
130+
f"must be >= default ({DEFAULT_TASK_TIMEOUT_S})"
131+
)
132+
if APPROVAL_GATE_CAP_MIN <= 0:
133+
raise ValueError(
134+
f"contracts/constants.json: approval_gate_cap.min must be > 0, "
135+
f"got {APPROVAL_GATE_CAP_MIN}"
136+
)
137+
if DEFAULT_APPROVAL_GATE_CAP < APPROVAL_GATE_CAP_MIN:
138+
raise ValueError(
139+
f"contracts/constants.json: approval_gate_cap.default ({DEFAULT_APPROVAL_GATE_CAP}) "
140+
f"must be >= min ({APPROVAL_GATE_CAP_MIN})"
141+
)
142+
if APPROVAL_GATE_CAP_MAX < DEFAULT_APPROVAL_GATE_CAP:
143+
raise ValueError(
144+
f"contracts/constants.json: approval_gate_cap.max ({APPROVAL_GATE_CAP_MAX}) "
145+
f"must be >= default ({DEFAULT_APPROVAL_GATE_CAP})"
146+
)
147+
148+
149+
_validate_constants()
113150
CACHE_MAX_ENTRIES: int = 50 # §12.9: decoupled from approvalGateCap
114151
CACHE_TTL_S: float = 60.0 # §12.8 sliding-window TTL on DENIED/TIMED_OUT
115152
POLICIES_MAX_BYTES: int = 64 * 1024 # finding #12: reject blueprints > 64 KB

agent/tests/test_policy.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Unit tests for policy.py — Cedar policy engine."""
22

3+
from unittest.mock import patch
4+
35
import pytest
46

57
cedarpy = pytest.importorskip("cedarpy")
68

7-
from policy import PolicyDecision, PolicyEngine
9+
import policy
10+
from policy import PolicyDecision, PolicyEngine, _validate_constants
811

912

1013
class TestPolicyDecision:
@@ -231,3 +234,66 @@ def test_task_type_property(self):
231234
def test_task_type_pr_review(self):
232235
engine = PolicyEngine(task_type="pr_review", repo="owner/repo")
233236
assert engine.task_type == "pr_review"
237+
238+
239+
class TestConstantsSemanticValidation:
240+
"""Verify _validate_constants rejects invariant violations."""
241+
242+
def test_rejects_approval_timeout_min_zero(self):
243+
with (
244+
patch.object(policy, "FLOOR_TIMEOUT_S", 0),
245+
pytest.raises(ValueError, match=r"approval_timeout_s\.min must be > 0"),
246+
):
247+
_validate_constants()
248+
249+
def test_rejects_approval_timeout_min_negative(self):
250+
with (
251+
patch.object(policy, "FLOOR_TIMEOUT_S", -1),
252+
pytest.raises(ValueError, match=r"approval_timeout_s\.min must be > 0"),
253+
):
254+
_validate_constants()
255+
256+
def test_rejects_approval_timeout_default_below_min(self):
257+
with (
258+
patch.object(policy, "FLOOR_TIMEOUT_S", 60),
259+
patch.object(policy, "DEFAULT_TASK_TIMEOUT_S", 30),
260+
pytest.raises(ValueError, match=r"approval_timeout_s\.default .* must be >= min"),
261+
):
262+
_validate_constants()
263+
264+
def test_rejects_approval_timeout_max_below_default(self):
265+
with (
266+
patch.object(policy, "FLOOR_TIMEOUT_S", 30),
267+
patch.object(policy, "DEFAULT_TASK_TIMEOUT_S", 300),
268+
patch.object(policy, "_ATS", {"min": 30, "max": 100, "default": 300}),
269+
pytest.raises(ValueError, match=r"approval_timeout_s\.max .* must be >= default"),
270+
):
271+
_validate_constants()
272+
273+
def test_rejects_approval_gate_cap_min_zero(self):
274+
with (
275+
patch.object(policy, "APPROVAL_GATE_CAP_MIN", 0),
276+
pytest.raises(ValueError, match=r"approval_gate_cap\.min must be > 0"),
277+
):
278+
_validate_constants()
279+
280+
def test_rejects_approval_gate_cap_default_below_min(self):
281+
with (
282+
patch.object(policy, "APPROVAL_GATE_CAP_MIN", 10),
283+
patch.object(policy, "DEFAULT_APPROVAL_GATE_CAP", 5),
284+
pytest.raises(ValueError, match=r"approval_gate_cap\.default .* must be >= min"),
285+
):
286+
_validate_constants()
287+
288+
def test_rejects_approval_gate_cap_max_below_default(self):
289+
with (
290+
patch.object(policy, "APPROVAL_GATE_CAP_MIN", 1),
291+
patch.object(policy, "DEFAULT_APPROVAL_GATE_CAP", 100),
292+
patch.object(policy, "APPROVAL_GATE_CAP_MAX", 50),
293+
pytest.raises(ValueError, match=r"approval_gate_cap\.max .* must be >= default"),
294+
):
295+
_validate_constants()
296+
297+
def test_passes_with_valid_constants(self):
298+
"""Sanity: the real constants pass validation."""
299+
_validate_constants()

scripts/check-constants-sync.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ function main(): number {
108108
return 1;
109109
}
110110

111+
// Semantic invariants (belt-and-suspenders — agent also validates at import time)
112+
const invariantErrors: string[] = [];
113+
if (agc.min <= 0) invariantErrors.push('approval_gate_cap.min must be > 0');
114+
if (agc.default < agc.min) invariantErrors.push('approval_gate_cap.default must be >= min');
115+
if (agc.max < agc.default) invariantErrors.push('approval_gate_cap.max must be >= default');
116+
if (ats.min <= 0) invariantErrors.push('approval_timeout_s.min must be > 0');
117+
if (ats.default < ats.min) invariantErrors.push('approval_timeout_s.default must be >= min');
118+
if (ats.max < ats.default) invariantErrors.push('approval_timeout_s.max must be >= default');
119+
120+
if (invariantErrors.length > 0) {
121+
console.error(`Semantic invariant violations in ${CONSTANTS_JSON}:\n`);
122+
for (const e of invariantErrors) console.error(` - ${e}`);
123+
return 1;
124+
}
125+
111126
const drifts = findDriftInPython(POLICY_PY);
112127

113128
if (drifts.length > 0) {

0 commit comments

Comments
 (0)