Edictum
Security & Compliance

Fail-Closed Guarantees

Every ambiguous failure in Edictum leads to block. False positives are recoverable. False negatives may not be.

AI Assistance

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.

ScenarioOutcomeRationale
Rule has syntax errorblockA broken rule cannot evaluate correctly. Blocking is safer than guessing intent.
No rulesets match tool callallow (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 timeoutblock (planned)A rule that cannot complete evaluation in time is treated as failed.
Control Plane server unreachablelocal cache, then blockThe graceful degradation chain applies (see below).
Malformed rule YAMLreject load, keep previousBad YAML never replaces a working rule set. The previous rulesets remain in effect.
Unknown rule typereject loadThe ruleset schema only allows pre, post, session, and sandbox. Unknown types fail validation and do not load.
Session limit exceededblockSession 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 all

At 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 typeImpactRecovery
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; redact and block effects are enforced for READ/PURE tools but fall back to warn for WRITE/IRREVERSIBLE tools (because the action already happened -- hiding the result removes context the agent needs)

Next Steps

Last updated on

On this page