33The real PolicyFabric service will eventually own policy evaluation. This module
44provides a local, deterministic contract-compatible evaluator so State Integrity
55Reports can already carry policy counts and explanation codes.
6+
7+ Boundary invariant: this module emits policy decisions only. It does not perform
8+ runtime effects, mutate agent grants, repair state, write ledgers, or replicate
9+ payloads. Downstream systems must consume explicit decision refs before taking
10+ any action.
611"""
712
813from __future__ import annotations
1520
1621POLICY_DECISION_SCHEMA = "sourceos.policy-decision/v1alpha1"
1722POLICY_ENGINE = "policy-fabric-local-stub"
23+ DECISION_SCOPE = "policy-only"
1824
1925ACTIONS = {
2026 "index" ,
@@ -44,6 +50,31 @@ class PolicyRequest:
4450 context : dict [str , Any ] = field (default_factory = dict )
4551
4652
53+ @dataclass (frozen = True )
54+ class DecisionBoundary :
55+ """Hard boundary carried by local policy decisions."""
56+
57+ decision_scope : str = DECISION_SCOPE
58+ runtime_effect_performed : bool = False
59+ authority_mutation_performed : bool = False
60+ state_repair_performed : bool = False
61+ ledger_write_performed : bool = False
62+ downstream_refs : tuple [str , ...] = (
63+ "SourceOS-Linux/sourceos-spec#113" ,
64+ "SourceOS-Linux/sourceos-syncd#30" ,
65+ )
66+
67+ def as_dict (self ) -> dict [str , Any ]:
68+ return {
69+ "decision_scope" : self .decision_scope ,
70+ "runtime_effect_performed" : self .runtime_effect_performed ,
71+ "authority_mutation_performed" : self .authority_mutation_performed ,
72+ "state_repair_performed" : self .state_repair_performed ,
73+ "ledger_write_performed" : self .ledger_write_performed ,
74+ "downstream_refs" : list (self .downstream_refs ),
75+ }
76+
77+
4778@dataclass (frozen = True )
4879class PolicyDecision :
4980 decision_id : str
@@ -57,6 +88,7 @@ class PolicyDecision:
5788 engine : str = POLICY_ENGINE
5889 schema : str = POLICY_DECISION_SCHEMA
5990 generated_at : str = field (default_factory = utc_now )
91+ boundary : DecisionBoundary = field (default_factory = DecisionBoundary )
6092
6193 def as_dict (self ) -> dict [str , Any ]:
6294 return {
@@ -71,9 +103,28 @@ def as_dict(self) -> dict[str, Any]:
71103 "subject" : self .subject ,
72104 "object_id" : self .object_id ,
73105 "data_class" : self .data_class ,
106+ "decision_boundary" : self .boundary .as_dict (),
74107 }
75108
76109
110+ def validate_decision_boundary (decision : dict [str , Any ]) -> None :
111+ """Reject collapsed policy→runtime/authority/state records."""
112+
113+ boundary = decision .get ("decision_boundary" )
114+ if not isinstance (boundary , dict ):
115+ raise ValueError ("policy decision missing decision_boundary" )
116+ if boundary .get ("decision_scope" ) != DECISION_SCOPE :
117+ raise ValueError ("policy decision scope must be policy-only" )
118+ for key in (
119+ "runtime_effect_performed" ,
120+ "authority_mutation_performed" ,
121+ "state_repair_performed" ,
122+ "ledger_write_performed" ,
123+ ):
124+ if boundary .get (key ) is not False :
125+ raise ValueError (f"policy decision must not perform { key } " )
126+
127+
77128def _decision_id (request : PolicyRequest , status : str , reason : str ) -> str :
78129 payload = {
79130 "action" : request .action ,
@@ -131,13 +182,16 @@ def evaluate_report_policy(lanes: list[dict[str, Any]], subject: str = "sourceos
131182 lane_name = str (lane .get ("name" , "normal" ))
132183 for action in ("index" , "retain" , "replicate" , "agent_access" ):
133184 decision = evaluate_policy (PolicyRequest (action = action , lane = lane_name , subject = subject ))
134- decisions .append (decision .as_dict ())
185+ payload = decision .as_dict ()
186+ validate_decision_boundary (payload )
187+ decisions .append (payload )
135188 return decisions
136189
137190
138191def decision_counts (decisions : list [dict [str , Any ]]) -> dict [str , int ]:
139192 counts = {status : 0 for status in sorted (STATUSES )}
140193 for decision in decisions :
194+ validate_decision_boundary (decision )
141195 status = str (decision .get ("status" , "deferred" ))
142196 if status not in counts :
143197 status = "deferred"
@@ -146,6 +200,8 @@ def decision_counts(decisions: list[dict[str, Any]]) -> dict[str, int]:
146200
147201
148202def policy_summary (decisions : list [dict [str , Any ]], sample_limit : int = 12 ) -> dict [str , Any ]:
203+ for decision in decisions :
204+ validate_decision_boundary (decision )
149205 return {
150206 "engine" : POLICY_ENGINE ,
151207 "counts" : decision_counts (decisions ),
0 commit comments