Policy DSL¶
YAML-based rule engine for declarative supervisor behaviour.
Status¶
Implemented in supervisor/policy_rules.py (PolicyEngine + load_policy_rules).
The hardcoded SupervisorPolicy provides default regime-driven actions;
PolicyEngine extends it with domainpack-specific YAML rules evaluated
per step. Domainpacks can ship declarative policy.yaml files or embed
equivalent protocol policy in their binding/runtime configuration.
Syntax (v0.1 — single condition, single action)¶
rules:
- name: suppress_retry_storm
regime: [NOMINAL, DEGRADED]
condition:
metric: R_bad
layer: 1
op: ">"
threshold: 0.7
action:
knob: alpha
scope: layer_1
value: 0.3
ttl_s: 5.0
Syntax (v0.2 — compound conditions, action chains)¶
rules:
- name: emergency_boost
regime: [DEGRADED, CRITICAL]
logic: AND
conditions:
- metric: R
layer: 0
op: "<"
threshold: 0.3
- metric: stability_proxy
op: "<"
threshold: 0.5
actions:
- knob: K
scope: global
value: 0.5
ttl_s: 5.0
- knob: alpha
scope: layer_0
value: 0.1
ttl_s: 3.0
cooldown_s: 10.0
max_fires: 5
- name: either_layer_low
regime: [NOMINAL]
logic: OR
conditions:
- metric: R
layer: 0
op: "<"
threshold: 0.3
- metric: R
layer: 1
op: "<"
threshold: 0.3
action:
knob: K
scope: global
value: 0.2
ttl_s: 5.0
Both v0.1 and v0.2 syntax are supported in the same file. The parser auto-detects:
condition(singular) → singlePolicyConditionconditions(plural) +logic→CompoundConditionaction(singular) → single-element action listactions(plural) → multi-action chain
Fields¶
| Field | Type | Description |
|---|---|---|
name |
str | Rule identifier |
regime |
list[str] | Active only in listed regimes (NOMINAL, DEGRADED, CRITICAL, RECOVERY) |
condition.metric |
str | R, R_good, R_bad, stability_proxy, pac_max, mean_amplitude, subcritical_fraction, amplitude_spread, or mean_amplitude_layer |
condition.layer |
int | null | Layer index (null for global metrics like stability_proxy) |
condition.op |
str | ">", "<", ">=", "<=", "==" |
condition.threshold |
float | Comparison value |
logic |
str | "AND" (default) or "OR" — combinator for conditions list |
action.knob |
str | Control knob: K, alpha, zeta, or Psi |
action.scope |
str | global or layer_N |
action.value |
float | Adjustment magnitude |
action.ttl_s |
float | Time-to-live in seconds |
cooldown_s |
float | Minimum seconds between consecutive firings of this rule (0 = no cooldown) |
max_fires |
int | Maximum total firings of this rule (0 = unlimited) |
Evaluation¶
Rules are evaluated in order. All matching rules fire (not first-match-only).
For compound conditions: - AND: all sub-conditions must pass - OR: at least one sub-condition must pass
Each matching rule produces one ControlAction per entry in its actions
list. The justification field of each action references the rule name.
Rate-limiting applies per rule:
- cooldown_s prevents re-firing within the specified interval
- max_fires caps the total number of times the rule can fire
Regime Scoping¶
A rule only fires if the current regime (from RegimeManager) is in its
regime list. This prevents conflicting actions across regimes.
Integration¶
The binding spec references a policy file:
Resolved relative to the binding spec directory. In run.py:
from scpn_phase_orchestrator.supervisor.policy_rules import PolicyEngine, load_policy_rules
rules = load_policy_rules(policy_path)
policy_engine = PolicyEngine(rules)
actions = policy_engine.evaluate(regime, upde_state, good_layers, bad_layers)
Protocol Net (v0.3)¶
The binding spec supports an optional protocol_net: key for Petri net
regime sequencing. When present, SupervisorPolicy.decide() delegates
regime evaluation to PetriNetAdapter instead of RegimeManager.evaluate().
protocol_net:
places: [warmup, nominal, cooldown, done]
initial: {warmup: 1}
place_regime: {warmup: NOMINAL, nominal: NOMINAL, cooldown: RECOVERY, done: NOMINAL}
transitions:
- name: start
inputs: [{place: warmup, weight: 1}]
outputs: [{place: nominal, weight: 1}]
guard: "stability_proxy > 0.6"
- name: wind_down
inputs: [{place: nominal, weight: 1}]
outputs: [{place: cooldown, weight: 1}]
guard: "R_0 < 0.3"
- name: finish
inputs: [{place: cooldown, weight: 1}]
outputs: [{place: done, weight: 1}]
Fields¶
| Field | Type | Description |
|---|---|---|
places |
list[str] | Place names in the net |
initial |
dict[str, int] | Initial token marking |
place_regime |
dict[str, str] | Maps each place to a Regime |
transitions[].name |
str | Transition identifier |
transitions[].inputs |
list[{place, weight}] | Input arcs |
transitions[].outputs |
list[{place, weight}] | Output arcs |
transitions[].guard |
str | null | Guard expression: "metric op threshold" |
Guard syntax uses the same operators as policy conditions: >, >=, <,
<=, ==. Metrics are the same flat dict keys passed to policy rules
(R, R_0..R_n, stability_proxy).
When multiple places are marked, the highest-severity regime wins (CRITICAL > RECOVERY > DEGRADED > NOMINAL).
References¶
Implementation: src/scpn_phase_orchestrator/supervisor/policy_rules.py.
The hardcoded default policy: src/scpn_phase_orchestrator/supervisor/policy.py.
Petri net: src/scpn_phase_orchestrator/supervisor/petri_net.py.
Petri adapter: src/scpn_phase_orchestrator/supervisor/petri_adapter.py.
Threshold values used in example rules are empirical — see ASSUMPTIONS.md.