Skip to content

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.

TierTypical blast scoreBehavior in prof.prod.strict
low0–3Auto-dispatched
medium4–6Dispatched with full controls verified
high7–9STEWARD_HOLD — human review required
critical10+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:

Dimension01–23–45
DataNo data touchedRead-only accessWrites localWrites external/shared
NetworkNo egressAllowlisted egressBroad egressUnrestricted
FinancialNoneNegligibleNoticeableSignificant
TimeInstant<1 min<1 hourLong-running
Reversibility0Partially reversibleDifficultIrreversible

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: +20
  • data_label RESTRICTED: +40
  • env prod or edge: +15
  • qos_class P0: +10
  • request includes egress or external_call: +15
  • request includes writes or mutates_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:

  1. Risk tier check — compare worker’s risk_tier against the active profile’s tier thresholds
  2. Tenant risk check — combine tenant_risk from RouteInput with the worker’s tier
  3. Data label check — verify the worker’s privilege_envelope allows access to the requested data_label
  4. Blast gate — compare the computed blast_score against 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-only
ctrl.net.egress-denied — egress is denied by default
ctrl.identity.secrets-deny-default — secrets access is deny-by-default
ctrl.mem.provenance-required — all memory access tracked
ctrl.blast-radius-scoring — blast scoring is active
ctrl.privilege-envelopes-required — privilege envelopes are enforced

Workers declare controls in two lists:

  • required_controls — controls the worker requires to be present in the environment
  • currently_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 uuid
from pyhall import make_decision, RouteInput, Registry, load_rules
from pyhall.models import HallConfig
rules = load_rules("rules.json")
registry = Registry(registry_dir="enrolled/")
# High-risk request in production — will hit STEWARD_HOLD
inp = 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) # True
print(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"]