|
| 1 | +# SourceOS Policy Decision Normalizer |
| 2 | + |
| 3 | +Status: v0.1 implementation contract |
| 4 | +Owner surface: sourceos-syncd / PolicyFabric / AgentPlane / SocioSphere |
| 5 | + |
| 6 | +## Purpose |
| 7 | + |
| 8 | +The policy decision normalizer converts raw policy, sandbox, IPC, filesystem, network, identity, and sync authorization observations into canonical `policy.decision` events. |
| 9 | + |
| 10 | +Its primary job is to prevent secure expected behavior from becoming operator-facing garbage. A denied operation is not automatically an error. It must be classified by policy intent, actor profile, target class, and security semantics. |
| 11 | + |
| 12 | +## Input classes |
| 13 | + |
| 14 | +The normalizer accepts observations from: |
| 15 | + |
| 16 | +- kernel sandbox or capability decisions; |
| 17 | +- PolicyFabric authorization decisions; |
| 18 | +- local IPC/mach/dbus/service lookup gates; |
| 19 | +- filesystem read/write/metadata gates; |
| 20 | +- network egress, trust lookup, and endpoint gates; |
| 21 | +- identity, entitlement, DID, credential, and role gates; |
| 22 | +- sync, replication, retention, purge, and repair gates; |
| 23 | +- agent runtime admission and action gates. |
| 24 | + |
| 25 | +## Canonical output |
| 26 | + |
| 27 | +Every normalized policy observation emits or attaches a canonical `policy.decision` event with: |
| 28 | + |
| 29 | +- `decision.policy_bundle` |
| 30 | +- `decision.policy_rule` |
| 31 | +- `decision.operation` |
| 32 | +- `decision.target_class` |
| 33 | +- `decision.result` |
| 34 | +- `decision.semantic_outcome` |
| 35 | +- `decision.explanation_code` |
| 36 | +- `severity` |
| 37 | +- `outcome` |
| 38 | +- `operator_narrative` |
| 39 | + |
| 40 | +## Result vs semantic outcome |
| 41 | + |
| 42 | +`decision.result` records the mechanical policy decision: |
| 43 | + |
| 44 | +- `allow` |
| 45 | +- `deny` |
| 46 | +- `defer` |
| 47 | +- `degrade` |
| 48 | + |
| 49 | +`decision.semantic_outcome` records SourceOS interpretation: |
| 50 | + |
| 51 | +- `allowed` |
| 52 | +- `blocked_expected` |
| 53 | +- `blocked_unexpected` |
| 54 | +- `blocked_attack_like` |
| 55 | +- `degraded` |
| 56 | +- `failed` |
| 57 | +- `observed` |
| 58 | + |
| 59 | +The two must not be conflated. |
| 60 | + |
| 61 | +Examples: |
| 62 | + |
| 63 | +- `result=deny`, `semantic_outcome=blocked_expected`: sandbox blocked a telemetry component from reading raw executable data outside its profile. |
| 64 | +- `result=deny`, `semantic_outcome=blocked_attack_like`: a user process probed privileged identity, kernel, or agent control-plane boundaries. |
| 65 | +- `result=degrade`, `semantic_outcome=degraded`: trust lookup could not complete, but local-first fallback preserved safety. |
| 66 | +- `result=allow`, `semantic_outcome=allowed`: policy permitted an expected action. |
| 67 | + |
| 68 | +## Severity mapping |
| 69 | + |
| 70 | +The normalizer must assign severity from semantic outcome and registry defaults, not raw denial status. |
| 71 | + |
| 72 | +Default mapping: |
| 73 | + |
| 74 | +- `allowed` -> `info` |
| 75 | +- `blocked_expected` -> `notice` |
| 76 | +- `blocked_unexpected` -> `warning` |
| 77 | +- `blocked_attack_like` -> `critical` |
| 78 | +- `degraded` -> `warning` |
| 79 | +- `failed` -> `error` |
| 80 | +- `observed` -> `info` |
| 81 | + |
| 82 | +Registry entries may narrow severity but must not inflate expected blocks to `error` unless the policy rule explicitly says the block indicates user-visible failure or integrity loss. |
| 83 | + |
| 84 | +## Explanation-code registry |
| 85 | + |
| 86 | +Every `policy.decision` must reference a known explanation code. |
| 87 | + |
| 88 | +Minimum required codes: |
| 89 | + |
| 90 | +- `POLICY_EXPECTED_METADATA_BOUNDARY` |
| 91 | +- `POLICY_EXPECTED_NETWORK_DISABLED` |
| 92 | +- `POLICY_UNEXPECTED_FILE_READ` |
| 93 | +- `POLICY_ATTACK_LIKE_PRIVILEGE_BOUNDARY_PROBE` |
| 94 | +- `POLICY_DEGRADED_TRUST_LOCAL_ONLY` |
| 95 | + |
| 96 | +The registry records: |
| 97 | + |
| 98 | +- code |
| 99 | +- title |
| 100 | +- domain |
| 101 | +- result |
| 102 | +- semantic outcome |
| 103 | +- severity |
| 104 | +- risk |
| 105 | +- default summary |
| 106 | +- default why |
| 107 | +- default next action |
| 108 | +- allowed operation classes |
| 109 | +- allowed target classes |
| 110 | + |
| 111 | +## Normalization rules |
| 112 | + |
| 113 | +1. Look up `explanation_code` in the registry. |
| 114 | +2. Validate that the observed result matches registry result unless an override is explicitly allowed. |
| 115 | +3. Validate that operation class and target class match the registry entry. |
| 116 | +4. Set event `outcome` from registry semantic outcome unless caller provides a stricter compatible outcome. |
| 117 | +5. Set event `severity` from registry severity unless caller provides a stricter compatible severity. |
| 118 | +6. Fill operator narrative from registry defaults when caller does not provide specific narrative text. |
| 119 | +7. Preserve raw evidence as evidence references, not primary product surface. |
| 120 | +8. Attach to an existing root trace when parent/root IDs are provided. |
| 121 | + |
| 122 | +## Compatibility rules |
| 123 | + |
| 124 | +A caller may provide more specific summary/why/next-action text, but may not use a known explanation code with contradictory semantics. |
| 125 | + |
| 126 | +Invalid examples: |
| 127 | + |
| 128 | +- `POLICY_EXPECTED_METADATA_BOUNDARY` with `outcome=blocked_attack_like`. |
| 129 | +- `POLICY_ATTACK_LIKE_PRIVILEGE_BOUNDARY_PROBE` with `severity=notice`. |
| 130 | +- `POLICY_EXPECTED_NETWORK_DISABLED` with `result=allow`. |
| 131 | + |
| 132 | +## Non-goals |
| 133 | + |
| 134 | +The normalizer is not a replacement for the policy engine. |
| 135 | +It does not decide access by itself. |
| 136 | +It records and explains policy decisions already made by the responsible control surface. |
| 137 | +It must not hide attack-like events under benign explanation codes. |
| 138 | +It must not generate false errors for expected blocks. |
| 139 | + |
| 140 | +## Acceptance criteria |
| 141 | + |
| 142 | +- Known explanation codes validate against registry semantics. |
| 143 | +- Expected sandbox/capability denials render as `notice` + `blocked_expected`. |
| 144 | +- Network-disabled policy outcomes render as successful local-first safety, not failure. |
| 145 | +- Unexpected file reads render as `warning` unless explicitly attack-like. |
| 146 | +- Privilege-boundary probes render as `critical` + `blocked_attack_like`. |
| 147 | +- Generated policy events validate against the canonical event schema. |
| 148 | +- Invalid contradictory combinations fail validation. |
0 commit comments