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:
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 deniedCALL_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:
The pipeline evaluates both versions on every tool call:
- Enforced contracts from
base.yamlmake real allow/deny decisions - Shadow contracts from
candidate.yamlevaluate in parallel, producing separate audit events withmode: "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 callCALL_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 --
modefield,defaultsblock, andobserve_alongside