Policies & Risk
WCP’s policy layer answers a single question before every dispatch: given this worker, this data, this environment, and this tenant — should execution proceed, be held for human review, or be denied outright?
Risk Tiers
Every worker declares a risk_tier in its registry record. Risk tier is the primary signal the policy gate uses when evaluating a dispatch request.
| Tier | Typical blast score | Behavior in prof.prod.strict |
|---|---|---|
low | 0–3 | Auto-dispatched |
medium | 4–6 | Dispatched with full controls verified |
high | 7–9 | STEWARD_HOLD — human review required |
critical | 10+ | DENY unless explicit override |
Risk tier is set by the worker’s author at enrollment time. A worker that writes to external databases is high. A worker that reads local files is low. The tier is not dynamic — it is a property of the worker species.
Blast Radius
Blast radius is a five-dimension score that quantifies potential damage if a worker fails or behaves unexpectedly. It is computed per-dispatch from the RouteInput and the worker’s registry record.
Each dimension is scored 0–5:
| Dimension | 0 | 1–2 | 3–4 | 5 |
|---|---|---|---|---|
| Data | No data touched | Read-only access | Writes local | Writes external/shared |
| Network | No egress | Allowlisted egress | Broad egress | Unrestricted |
| Financial | None | Negligible | Noticeable | Significant |
| Time | Instant | <1 min | <1 hour | Long-running |
| Reversibility | 0 | Partially reversible | Difficult | Irreversible |
The five dimension scores sum to a blast_score (0–25 dimensional, scaled to 0–100 in practice). Routing rules declare a maximum blast score per environment. Requests exceeding the threshold are denied or escalated.
Blast radius is additive in worker chains. If Worker A (score 3) calls Worker B (score 4), the total chain blast is 7. The router accounts for chain propagation when evaluating multi-worker pipelines.
The PyHall heuristic scorer adds to a baseline of 10:
data_label INTERNAL: +20data_label RESTRICTED: +40env prodoredge: +15qos_class P0: +10- request includes
egressorexternal_call: +15 - request includes
writesormutates_state: +15
Policy Profiles
A profile is a named, pre-bundled governance posture. Swap a profile to change the entire fleet’s behavior — no code changes required.
WCP defines four built-in profiles:
prof.dev.permissive — Development. Low-risk workers auto-dispatch. High-risk workers produce STEWARD_HOLD. Controls are enforced but blast gate thresholds are relaxed. Use for local development and CI.
prof.prod.strict — Production. All controls enforced. Blast scoring gate active for every dispatch. High-risk workers denied unless explicitly allowed by routing rule. All decisions logged.
prof.edge.isolated — Edge / air-gapped. Strict data isolation. network_egress: none enforced. Workers may only access local resources. No external calls permitted.
prof.mem.rag-strict — Strict RAG. Memory access is scoped to the requesting tenant’s data only. ctrl.mem.provenance-required enforced. Cross-tenant memory access is denied.
The Policy Gate
The policy gate is evaluated after rule matching, controls verification, and blast scoring — immediately before worker selection.
Evaluation sequence:
- Risk tier check — compare worker’s
risk_tieragainst the active profile’s tier thresholds - Tenant risk check — combine
tenant_riskfrom RouteInput with the worker’s tier - Data label check — verify the worker’s
privilege_envelopeallows access to the requesteddata_label - Blast gate — compare the computed
blast_scoreagainst the rule’s declared threshold
If any check fails, dispatch is denied or escalated. The policy gate produces one of three outcomes.
Controls
Controls are governance invariants that must be satisfied before a worker can be dispatched. They are predicate statements describing required system state:
ctrl.obs.audit-log-append-only — audit log is append-onlyctrl.net.egress-denied — egress is denied by defaultctrl.identity.secrets-deny-default — secrets access is deny-by-defaultctrl.mem.provenance-required — all memory access trackedctrl.blast-radius-scoring — blast scoring is activectrl.privilege-envelopes-required — privilege envelopes are enforcedWorkers declare controls in two lists:
required_controls— controls the worker requires to be present in the environmentcurrently_implements— controls the worker has actually implemented
The Hall verifies currently_implements satisfies required_controls at enrollment and again at dispatch. A required control that is declared but not implemented means dispatch is denied with DENY_CONTROL_MISSING.
Required controls block dispatch if absent. Suggested controls (from recommended_profiles_effective) are logged in the RouteDecision but do not block dispatch.
Dispatch Outcomes
Every routing decision produces one of three outcomes:
DISPATCH — all checks passed. Worker is cleared for execution. denied: false, selected_worker_species_id is set.
DENY — a check failed. Worker is not dispatched. denied: true, deny_reason_if_denied carries the deny code and message.
STEWARD_HOLD — the policy gate returned REQUIRE_HUMAN. The Hall returns denied: true with deny_reason_if_denied["supervisor_required"] = true and deny_reason_if_denied["code"] = "DENY_REQUIRES_HUMAN_APPROVAL". Callers must check deny_reason_if_denied["code"] == "DENY_REQUIRES_HUMAN_APPROVAL" to detect STEWARD_HOLD specifically. Execution waits for human approval via whatever H2A channel is configured (Discord, webhook, email, etc.).
import uuidfrom pyhall import make_decision, RouteInput, Registry, load_rulesfrom pyhall.models import HallConfig
rules = load_rules("rules.json")registry = Registry(registry_dir="enrolled/")
# High-risk request in production — will hit STEWARD_HOLDinp = RouteInput( capability_id="cap.db.write", env="prod", data_label="RESTRICTED", tenant_risk="high", qos_class="P0", tenant_id="org.acme", correlation_id=str(uuid.uuid4()),)
decision = make_decision( inp=inp, rules=rules, registry_controls_present=registry.controls_present(), registry_worker_available=registry.worker_available,)
# In prof.prod.strict with high-risk worker:print(decision.denied) # Trueprint(decision.deny_reason_if_denied)# {"code": "DENY_REQUIRES_HUMAN_APPROVAL", "supervisor_required": True}# dispatch is held for human review — check deny_reason_if_denied["code"]