Fail-Closed Guarantees
Every ambiguous failure in Edictum leads to block. False positives are recoverable. False negatives may not be.
Right page if: you need to understand what happens when something goes wrong in the Edictum pipeline -- broken rulesets, server outages, type mismatches, or session limit overflows. Wrong page if: you need to understand what Edictum defends against at the threat-model level -- see https://docs.edictum.ai/docs/security/defense-scope. For the pipeline architecture, see https://docs.edictum.ai/docs/architecture. Gotcha: "no rulesets match" is ALLOW, not block -- Edictum does not assume block-all-by-default. Add a catch-all rule if you want that behavior. Rule evaluation errors are always block (fail-closed), and audit events include policy_error: true to make broken rulesets visible.
A tool call that should have been blocked but was allowed cannot be undone. A tool call that should have been allowed but was blocked can be retried. Edictum treats every ambiguous failure as a block.
This is the fail-closed principle: false positives (blocking safe calls) are recoverable. False negatives (allowing dangerous calls) may not be.
How It Works in Code
The fail-closed behavior is enforced at two levels in the pipeline.
Evaluation errors become blocks. When a rule condition throws an exception during evaluation, the pipeline catches it and returns a Decision.fail() with policy_error=True:
# yaml_engine/compiler.py — precondition evaluation
try:
result = evaluate_expression(when_expr, envelope, ...)
except Exception as exc:
# Fail-closed: evaluation error triggers the rule
return Decision.fail(
msg, tags=tags, policy_error=True,
error_detail=str(exc), **then_metadata,
)Type mismatches become blocks. The _PolicyError sentinel in the evaluator has __bool__ returning True, so any type mismatch or comparison error triggers the rule rather than silently passing:
# yaml_engine/evaluator.py
class _PolicyError:
def __bool__(self) -> bool:
return True # Errors trigger the rule (fail-closed)Both paths produce audit events with policy_error: true, making broken rulesets visible in monitoring even when the system falls back to a safe default.
Scenario Table
Every failure mode in the pipeline has a defined outcome. Seven scenarios, one principle.
| Scenario | Outcome | Rationale |
|---|---|---|
| Rule has syntax error | block | A broken rule cannot evaluate correctly. Blocking is safer than guessing intent. |
| No rulesets match tool call | allow (default) | No rule applies -- the tool call is outside rule scope. This is intentional: rulesets are opt-in, not a global block-all. |
| Rule evaluation timeout | block (planned) | A rule that cannot complete evaluation in time is treated as failed. |
| Control Plane server unreachable | local cache, then block | The graceful degradation chain applies (see below). |
| Malformed rule YAML | reject load, keep previous | Bad YAML never replaces a working rule set. The previous rulesets remain in effect. |
| Unknown rule type | reject load | The ruleset schema only allows pre, post, session, and sandbox. Unknown types fail validation and do not load. |
| Session limit exceeded | block | Session limits are hard caps. Exceeding them is a block, not a warning. |
"No rulesets match" is allow, not block. This is deliberate. If you want block-all-by-default, add a catch-all rule. Edictum does not assume you want to block everything -- it assumes you want to block what your rulesets specify.
Unknown rule types are rejected at load time by schema validation. A typo in the type field (for example type: prre) is a validation error, not a silent skip.
Graceful Degradation
When an agent connects to the optional server surface via the server SDK, ruleset updates arrive over SSE. If connectivity is lost, the system degrades through a defined chain:
SSE connection (live updates)
|
v connection lost
In-memory rulesets (last successful load)
|
v no rulesets loaded
Embedded YAML (bundled with agent, if configured)
|
v no rulesets
Block allAt every step, the system prefers the most recent known-good rulesets. When the SSE connection drops, the agent continues enforcing whatever rulesets it last successfully loaded into memory -- no external cache is involved. If no rulesets are available at all, every tool call is blocked. The agent cannot silently operate without rule enforcement.
Edictum.reload() atomically swaps rulesets from new YAML. If the new ruleset fails to load (parse error, schema validation error), the swap is aborted and the previous rulesets remain in effect. This is the same fail-closed principle applied to hot-reload.
Input Validation: First Line of Defense
Before any rule evaluation begins, create_envelope() validates the tool call itself. Tool names containing null bytes, newlines, or path separators are rejected immediately. This prevents injection attacks that could corrupt session keys or audit records.
# envelope.py — create_envelope()
# Rejects: empty strings, null bytes (\x00),
# newlines (\n, \r), path separators (/, \)This validation runs before the pipeline, before rulesets, before session checks. A malformed tool name never reaches the evaluation layer.
Backend Errors
The StorageBackend protocol defines how session state is read and written. When using the server SDK (ServerBackend), HTTP errors are handled with fail-closed semantics:
- HTTP 404 (key not found): returns
None-- this is normal "no value yet" behavior - Connection refused, timeout, HTTP 500: propagates as an exception to the pipeline, which converts it to a block decision
The pipeline does not distinguish between "the server said no" and "the server is unreachable." Both result in a block. This prevents a network partition from silently disabling rule enforcement.
Monitoring Rule Failures
When a rule fails to load or evaluate, the audit event includes policy_error: true. This field is the signal to alert on in your monitoring system.
{
"action": "CALL_DENIED",
"tool_name": "bash",
"policy_error": true,
"error_detail": "unsupported operand type(s) for >: 'str' and 'int'",
"decision_name": "rate-limit-deploys",
"timestamp": "2026-03-08T14:22:01Z"
}Filter your audit sink for policy_error: true to catch broken rulesets before they affect agent operations. A high rate of policy errors means rulesets need attention -- not that the system is failing, but that rulesets are misconfigured and falling back to block.
Design Rationale
The fail-closed default exists because of an asymmetry in consequences:
| Failure type | Impact | Recovery |
|---|---|---|
| False positive (safe call blocked) | Agent retries or asks for help. Workflow is slowed. | Retry, fix the rule, redeploy. |
| False negative (dangerous call allowed) | Data deleted. Secrets leaked. Unauthorized action completed. | May not be recoverable. |
Every design decision in Edictum favors false positives over false negatives:
- Unregistered tools default to
SideEffect.IRREVERSIBLE(most restrictive classification) - Rule evaluation errors block the tool call rather than silently allowing it
- Observe mode is opt-in per-rule or per-pipeline, never the default
- Postconditions default to
warn;redactandblockeffects are enforced for READ/PURE tools but fall back towarnfor WRITE/IRREVERSIBLE tools (because the action already happened -- hiding the result removes context the agent needs)
Next Steps
- Pipeline architecture -- the full pipeline evaluation order and error handling
- Rulesets -- all four rule types at a glance
- Sandbox rulesets -- allowlist boundaries with
outside: block - Defense scope -- what Edictum defends against and what it does not
- Testing rulesets -- validating rulesets before deployment
Last updated on