Skip to content

Observe Mode

Observe mode lets you shadow-test contracts against live traffic without denying any tool calls. Preconditions that would fire emit CALL_WOULD_DENY audit events instead of denying. The tool call proceeds normally.

This gives you real data on what your contracts would do before you enforce them.

The Workflow

1. Deploy contracts in observe mode
        |
2. Review CALL_WOULD_DENY audit events
        |
3. Tune contracts (fix false positives, tighten loose contracts)
        |
4. Switch to enforce mode

Step 1: Deploy in observe mode. Set mode: observe in your contract bundle and deploy to production. Agents run normally -- no tool calls are denied.

Step 2: Review audit events. Every precondition that would have denied a call emits a CALL_WOULD_DENY event. Query your audit sink (stdout, file, OTel) for these events to see which contracts fire and how often.

Step 3: Tune. If a contract fires too often (false positives), narrow its when condition. If it never fires, check that the selectors match your tool arguments. Use edictum check to test specific tool calls against your contracts without running them.

Step 4: Enforce. Change mode: observe to mode: enforce. Contracts now actively deny tool calls.

Enabling Observe Mode

Pipeline-level: all contracts observe

Set the default mode in your contract bundle:

defaults:
  mode: observe

Every contract in the bundle runs in observe mode. No tool calls are denied.

Per-contract: shadow-test one contract

Leave the bundle default as enforce and set mode: observe on specific contracts:

defaults:
  mode: enforce

contracts:
  - id: block-dotenv
    type: pre
    tool: read_file
    when:
      args.path: { contains: ".env" }
    then:
      effect: deny
      message: "Denied: read of sensitive file {args.path}"

  - id: experimental-api-check
    type: pre
    mode: observe
    tool: call_api
    when:
      args.endpoint: { contains: "/v1/expensive" }
    then:
      effect: deny
      message: "Expensive API call detected (observe mode)."

Here, block-dotenv enforces (denies matching calls) while experimental-api-check observes (logs what it would deny but allows the call).

What Changes in Observe Mode

Behavior Enforce Mode Observe Mode
Precondition matches Tool call is denied Tool call proceeds
Audit event action CALL_DENIED CALL_WOULD_DENY
Tool executes No Yes
Postconditions run N/A (tool didn't run) Yes (tool ran)
Audit trail records the match Yes Yes
Session counters Attempt counted, execution not Attempt counted, execution counted

The critical difference: in observe mode, the tool always executes. The audit trail shows you exactly what enforcement would have done, without any impact on the agent.

Postconditions in Observe Mode

Postconditions always produce findings (warnings), never denials. In observe mode, postcondition warnings are prepended with [observe] in the warning string (e.g., "[observe] PII detected in output"). The audit event is still emitted as CALL_EXECUTED or CALL_FAILED -- there is no separate would_warn action. The on_postcondition_warn callback fires in both modes.

When to use this

Read this page when you want to deploy contracts without denying any tool calls yet. Observe mode is for safe rollouts: you deploy contracts in mode: observe, review the CALL_WOULD_DENY audit events they produce, tune false positives, and then switch to mode: enforce once you have confidence. For comparing two contract versions side-by-side in production, see dual-mode evaluation below.

Reviewing Observe-Mode Events

Audit events from observe mode include the same fields as enforce-mode events: tool name, arguments, principal, contract ID, policy version, and session counters. The action field distinguishes them:

  • CALL_DENIED -- enforce mode, call was denied
  • CALL_WOULD_DENY -- observe mode, call would have been denied

Filter your audit sink for CALL_WOULD_DENY to see the shadow denial report. Group by decision_name (the contract id) to see which contracts fire most often.

Dual-Mode Evaluation with observe_alongside

Observe mode applies to individual contracts or to an entire bundle. But sometimes you need to run two versions of the same contract simultaneously -- the current enforced version and a candidate version that only observes. This is dual-mode evaluation.

The Use Case

You have contracts running in production. A new version is ready but you want to compare its behavior against the current version before promoting it. You need both versions evaluating the same tool calls, with the current version making real decisions and the candidate only logging.

How It Works

Create a second YAML file with observe_alongside: true at the top level:

# candidate.yaml
apiVersion: edictum/v1
kind: ContractBundle
observe_alongside: true

metadata:
  name: candidate-contracts

defaults:
  mode: enforce

contracts:
  - id: block-sensitive-reads
    type: pre
    tool: read_file
    when:
      args.path:
        contains_any: [".env", ".secret", "credentials", ".pem", ".key"]
    then:
      effect: deny
      message: "Denied: read of sensitive file {args.path}"

Load both bundles:

guard = Edictum.from_yaml("contracts/base.yaml", "contracts/candidate.yaml")

The pipeline evaluates both versions on every tool call:

  1. Enforced contracts from base.yaml make real allow/deny decisions
  2. Shadow contracts from candidate.yaml evaluate in parallel, producing separate audit events with mode: "observe"

Shadow contract IDs are suffixed with :candidate (e.g., block-sensitive-reads:candidate). Shadow contracts never block tool calls -- they only produce audit events.

Shadow Audit Events

Shadow contracts emit the same audit events as regular observe mode:

  • CALL_WOULD_DENY -- the shadow contract would have denied this call
  • CALL_ALLOWED -- the shadow contract allowed this call

Filter your audit sink for mode: "observe" and decision_name ending in :candidate to see the shadow evaluation results.

When to Use

Contract update rollouts. Deploy the candidate as a shadow. Compare its audit trail with the enforced version. If the candidate would have denied calls that should be allowed (false positives), tune it before promoting.

A/B testing contracts. Run a stricter version of a contract in observe mode to measure the impact of tightening a contract.

Composition Report

Use return_report=True to see which contracts were shadowed:

guard, report = Edictum.from_yaml(
    "contracts/base.yaml",
    "contracts/candidate.yaml",
    return_report=True,
)

for s in report.shadow_contracts:
    print(f"{s.contract_id}: shadow from {s.observed_source}")

See Bundle Composition for full composition reference.

Next Steps

  • Contracts -- writing preconditions, postconditions, and session contracts
  • How it works -- the full pipeline walkthrough
  • Quickstart -- try observe mode in the bonus step
  • YAML reference -- mode field, defaults block, and observe_alongside