Declarative Boundary Policy¶
Up to v0.56 the BoundaryEngine evaluated a ContextRecord through three
built-in gates (capability scope, validity window, freshness). Any other rule
was a Python callable appended with BoundaryEngine.add_gate(). That works for
a library user, but not for a Network Authority operator: a new rule such as
“transfers above 1 000 need MFA” meant shipping code. The rule was not signed,
versioned or audited, and the signed BoundaryDecision did not record which
rules produced it.
v0.58 makes gate configuration a signed, versioned artifact. A
BoundaryPolicy selects requests by generic context facts and configures gates
from a trusted, code-defined gate registry. The Network Authority
publishes, validates, activates, deactivates and rolls back policies through
operator-authenticated routes. A new policy-aware evaluation route returns a
BoundaryDecision whose signed body carries a PolicyBinding: the exact
policy versions and digests, the evaluation order, the outcome of every
configured gate, and a digest of the request context. The key design decision
is that activation state is not signed. A rollback restores the exact bytes
that were signed earlier, and each activation change is an audit event.
This is not a policy language. A policy cannot contain code, regular expressions or references to external systems.
gate_typeis a lookup key into gate implementations the operator already installed. A new kind of rule is added by implementing and registering a gate type, then referencing it from policy.
This is not execution evidence. An authorized decision means the requested action is authorized under the evaluated policy. It does not mean the action ran. External systems perform the action and report execution results separately, for example with an execution evidence chain.
identity + capability + context + signed policy + gates
│
BoundaryEngine.evaluate_with_policies()
│
signed BoundaryDecision (+ PolicyBinding) + signed JustificationProof
What a policy-bound decision proves¶
Anyone holding the NA public key and the signed policy versions can check, offline:
Which rules applied.
policy_binding.policieslists(policy_id, version, policy_digest)in resolution order.verify_boundary_decision(..., expected_policies=[...])fails withpolicy_binding_mismatchif a different version is presented.How each rule came out.
policy_binding.gate_evaluationsrecords every configured gate with its order, mode (enforceorobserve) and outcome (pass,fail,missing_context,invalid_context,gate_error).That the record is the one that was signed. The binding sits inside the decision’s signed body. Removing or editing it fails signature verification.
Which request it was for.
context_digestis the SHA-256 of the canonicalContextRecord.
Decisions produced before v0.58, or by the unchanged /admin/boundary/decide
route, have no policy_binding key at all. Their canonical bytes and
signatures are identical to v0.56.
Rules the framework enforces¶
Constraints only add. Every matching active policy applies. There is no precedence and no “allow wins”. Any failing
enforcegate denies.Deterministic order. Built-in gates run first, keeping their short-circuit behaviour. Applied policies are then ordered by
(policy_id, version)and gates byorder. Every configured gate is evaluated, so a denial lists every failing rule.Fail closed. A signed DENY is returned when an active policy has a bad signature (
policy_signature_invalid), a stored row fails its integrity check (policy_store_integrity_failed), a referenced gate type is not installed (gate_type_unavailable), a policy is structurally invalid (policy_invalid), two versions of one policy are active (ambiguous_resolution), or a matching active policy has expired (policy_expired). Missing facts and gate exceptions fail the gate. Malformed or unauthenticated HTTP requests stay ordinary HTTP errors.One broken policy denies everything. Signatures and structure of all active policies are checked before any selector is matched: a policy that cannot be trusted cannot be trusted to say which requests it does not cover.
/health,GET /admin/boundary-policies/activeand the dashboard report the set as unhealthy.Private by default. Proofs, bindings, audit events and logs record the fact path, whether it was present, its type, the configured condition and the outcome. The raw value is recorded only when the signed policy sets
disclose_input: truefor that gate.
Never put credentials, tokens or secret values in request_parameters or
attributes. Supply metadata about them instead, such as mfa_verified,
token_present or credential_age_days.
Built-in gate types¶
gate_type |
config |
passes when |
|---|---|---|
|
|
fact present and not null |
|
|
finite number <= max (< when not inclusive) |
|
|
finite number >= min |
|
|
scalar equals one of values (type-strict) |
|
|
scalar equals none of values |
|
|
fact is a boolean equal to expected |
|
|
fact is a list of strings, all in allowed |
|
|
|
|
|
fact is one of the values in |
Fact paths address requested_capability, requester_sovereign_id,
provider_sovereign_id, agreement_id, parent_kind, requested_at,
context_freshness_seq, request_parameters.<key>… and
attributes.<key>… (at most 8 segments). Since v0.58.1 an attestation basis
also exposes the read-only attestation.subject_id, attestation.roles and
attestation.claims.<key>… facts; they are missing for any other basis. Any
other root is rejected.
A selector can target attestation-backed requests with
"parent_kinds": ["attestation"]. See
Example: Attestation-Backed Boundary Evaluation for evaluating a request against a
membership attestation instead of an agreement.
Walkthrough¶
1. Write the policy intent¶
The operator declares intent only. The NA assigns version, issued_at,
issued_by and issuer_sovereign_id and signs the policy. A request that
supplies any of those fields is refused with unexpected_field.
{
"policy_id": "transfer-limits",
"description": "Cap transfers and require MFA",
"valid_from": "2026-10-01T00:00:00Z",
"valid_until": "2027-10-01T00:00:00Z",
"selector": {"capabilities": ["payments.*"]},
"gates": [
{"gate_id": "amount-cap", "gate_type": "max_value.v1", "order": 0,
"config": {"path": "request_parameters.amount", "max": 1000}},
{"gate_id": "mfa", "gate_type": "boolean_required.v1", "order": 1,
"config": {"path": "attributes.mfa_verified"}},
{"gate_id": "business-hours", "gate_type": "time_window.v1", "order": 2,
"mode": "observe",
"config": {"weekdays": [1, 2, 3, 4, 5], "utc_hour_start": 7, "utc_hour_end": 19}}
]
}
An empty selector makes a global policy. observe mode records a failure
without denying, which lets you introduce a rule before it enforces.
2. Validate, publish, activate¶
POST /admin/boundary-policies/validate (standard tier) -> {"valid": true, "issues": []}
POST /admin/boundary-policies (privileged) -> signed policy, version 1, inactive
POST /admin/boundary-policies/transfer-limits/activate {"version": 1} (privileged)
GET /admin/boundary-policies/active -> active set, policy_set_healthy, enforcement
Activation re-verifies the signature, the stored digest and the registry
validation right before the policy goes live. Publishing version 2 and
activating it deactivates version 1 in the same transaction. Activating
version 1 again is the rollback, and its audit event records
previous_version and rollback: true.
3. Evaluate¶
POST /admin/boundary/evaluate
{
"agreement": {...},
"requested_capability": "payments.transfer",
"context": {
"request_parameters": {"amount": 2500},
"attributes": {"mfa_verified": true}
}
}
The response carries a denied decision and its proof:
{
"decision": {
"authorized": false,
"denial_reason": "policy gate 'transfer-limits/amount-cap' failed",
"policy_binding": {
"policies": [{"policy_id": "transfer-limits", "version": 1, "policy_digest": "9c1e…", "signed_by": "na-2026"}],
"gate_evaluations": [
{"policy_id": "transfer-limits", "gate_id": "amount-cap", "order": 0, "mode": "enforce", "passed": false, "outcome": "fail", "...": "..."},
{"policy_id": "transfer-limits", "gate_id": "mfa", "order": 1, "mode": "enforce", "passed": true, "outcome": "pass", "...": "..."}
],
"resolution_status": "resolved",
"...": "..."
},
"signature": {"key_id": "na-2026", "sig": "…"}
},
"justification_proof": {"...": "..."}
}
The proof entry for transfer-limits/amount-cap has
inputs = {"path": "request_parameters.amount", "present": true, "value_type": "number", "condition": {"operator": "<=", "max": 1000.0}}.
The amount itself is not recorded.
4. Verify offline¶
from genesis_mesh.models import BoundaryPolicy
from genesis_mesh.models.context import BoundaryDecision
from genesis_mesh.models.justification import JustificationProof
from genesis_mesh.trust.context import verify_boundary_decision, verify_boundary_policy
from genesis_mesh.trust.justification import verify_justification_proof
policy = BoundaryPolicy.model_validate_json(policy_json)
decision = BoundaryDecision.model_validate(body["decision"])
proof = JustificationProof.model_validate(body["justification_proof"])
assert verify_boundary_policy(policy, [na_pub_b64]).valid
result = verify_boundary_decision(decision, [na_pub_b64], expected_policies=[policy])
print(result.reason) # "unauthorized_policy_gate_failure"
assert verify_justification_proof(proof, [na_pub_b64], decision=decision).valid
Python API¶
from genesis_mesh.trust.context import BoundaryEngine, GateRegistry
registry = GateRegistry.default() # built-in gate types, frozen
engine = BoundaryEngine("bank-a")
decision, proof = engine.evaluate_with_policies(
context, agreement, signing_key, issued_by="na-2026",
policies=active_policies, # the full active set
registry=registry,
policy_public_keys=[na_pub_b64],
)
resolve_policies(), validate_boundary_policy(), sign_boundary_policy()
and verify_boundary_policy() are available separately for tooling.
Adding a gate type¶
from pydantic import BaseModel, ConfigDict
from genesis_mesh.trust.context import ConfiguredGateOutcome, GateRegistry, fact_inputs, resolve_fact
class MaxLengthConfig(BaseModel):
model_config = ConfigDict(extra="forbid") # required by the registry
path: str
max_length: int
class MaxLengthGate:
gate_type = "string_max_length.v1"
config_model = MaxLengthConfig
def evaluate(self, context, config, *, disclose_input):
value = resolve_fact(context, config.path)
passed = isinstance(value, str) and len(value) <= config.max_length
return ConfiguredGateOutcome(
passed=passed, outcome="pass" if passed else "fail",
detail="within length" if passed else "too long",
inputs=fact_inputs(config.path, value, disclose_input),
condition={"max_length": config.max_length},
)
registry = GateRegistry.builtin()
registry.register(MaxLengthGate())
registry.freeze() # the NA refuses an unfrozen registry
Pass the registry to NetworkAuthorityService(gate_registry=registry).
Nothing else changes: resolution, routes, signing, proofs and audit handle the
new gate type as they handle the built-in ones. A policy that references a
gate type the running NA does not have (for example after a downgrade) fails
closed with gate_type_unavailable.
CLI usage¶
# Offline validation of intent or a signed policy against the built-in registry
genesis-mesh trust boundary-policy validate --file transfer-limits.json
# Verify a signed policy
genesis-mesh trust boundary-policy verify \
--file transfer-limits.signed.json --public-key keys/na.pub
# Explain a policy-bound decision (the /admin/boundary/evaluate response works too)
genesis-mesh trust boundary-policy explain --decision evaluate-response.json
# List installed gate types and their config fields
genesis-mesh trust boundary-policy gate-types
Integration with BoundaryEngine¶
BoundaryEngine.evaluate()andevaluate_with_proof()are unchanged.BoundaryEngine.add_gate()gates still run, in their usual position, inevaluate_with_policies()too./admin/boundary/decideis unchanged by default. Start the NA with--boundary-policy-enforcement required(orBOUNDARY_POLICY_ENFORCEMENT=required) to refuse that route with HTTP 409boundary_policy_required. Only then does every decision go through policy, and only then can an operator claim that policies are enforced.
Limits of this release¶
A policy-bound decision proves which signed policy versions and gate outcomes produced it. It does not independently prove the NA’s activation history; that rests on the NA audit store.
Selectors that match on request parameters are only as strong as the facts the caller supplies: omitting the parameter means the selector does not match. Put mandatory rules in a policy selected by capability or identity, and use
required_parameter.v1there.The TypeScript, Go and .NET SDKs do not yet verify policy-bound decisions offline; that is planned with the cross-language interoperability release.
Test command¶
python -m pytest genesis_mesh/tests/test_boundary_policy.py \
genesis_mesh/tests/test_na_boundary_policy.py \
genesis_mesh/tests/test_cli_boundary_policy.py \
genesis_mesh/tests/integration/test_boundary_policy_lifecycle.py -q