Email example¶
The email example is the canonical worked case. The trusted module exposes
one side-effecting function (send_email) and a small vocabulary of
domain contracts (only, none, unique_recipients, content_length_at_most,
same_content, no_guarantees). Agent code in cases/ uses
@clauz3.guarantee(...) to express what should be true about the program's
effects.
Trusted module¶
The trusted effect is a stub function decorated with @clauz3.trusted.
Its @clauz3.requires(...) precondition is an obligation at every call site.
examples/email/tools/email/trusted/effects.py
import clauz3
@clauz3.requires(lambda addr, msg: "@" in addr, message='addr must contain "@"')
@clauz3.trusted
@clauz3.mock
def send_email(addr: str, msg: str) -> None:
"""trusted email sender.
Note: this is a mock function for testing, it won't *actually* send an email
Arguments:
addr: recipient. Must be a valid email address
msg: body of email
"""
pass
The contract module defines the domain vocabulary on top of the
effect(send_email) relation, passing the imported trusted function directly.
The string form effect("send_email") also names this effect.
examples/email/tools/email/trusted/contracts.py
"""Email-specific predicates built from generic trusted effect facts."""
from tools.email.trusted.effects import send_email
from clauz3.spec import ContractSpec, contract, effect
from clauz3.spec import no_guarantees as core_no_guarantees
Email = effect(send_email)
@contract
def only(addresses: list[str]) -> ContractSpec:
"""Guarantee that every sent email targets one of these addresses."""
return Email.all(lambda e: e.addr in addresses)
@contract
def none() -> ContractSpec:
"""Guarantee that no email is sent."""
return Email.empty()
@contract
def no_guarantees() -> ContractSpec:
"""Make no guarantees about email effects."""
return core_no_guarantees()
@contract
def unique_recipients() -> ContractSpec:
"""Guarantee that no reachable execution emails the same address twice."""
return Email.distinct(lambda e: e.addr)
@contract
def at_most(count: int) -> ContractSpec:
"""Guarantee that at most count emails are sent."""
return Email.count() <= count
@contract
def recipient_at_most(addr: str, count: int) -> ContractSpec:
"""Guarantee that addr is emailed at most count times."""
return Email.where(lambda e: e.addr == addr).count() <= count
@contract
def content_length_at_most(max_chars: int) -> ContractSpec:
"""Guarantee that no sent email content is longer than max_chars."""
return Email.all(lambda e: len(e.msg) <= max_chars)
@contract
def same_content(left_addr: str, right_addr: str) -> ContractSpec:
"""Guarantee that both addresses receive at least one identical message."""
left_emails = Email.where(lambda e: e.addr == left_addr)
right_emails = Email.where(lambda e: e.addr == right_addr)
return left_emails.shares_value(right_emails, lambda e: e.msg)
@contract
def bob_gets_x() -> ContractSpec:
"""Bob only ever receives the message "x".
Deliberately written on one line: two relation lambdas sharing a physical
line used to compile to the same body (gh-125), so this is the fixture for
the end-to-end regression in ``tests/test_email_prover.py``.
"""
# fmt: off
return Email.where(lambda e: e.addr == "bob@example.com").all(lambda e: e.msg == "x") # noqa: E501
# fmt: on
A passing case¶
Sending one email to an allow-listed address discharges
emails.only([...]).
examples/email/cases/only_bob_pass.py
# ruff: noqa: F821
from tools.email.trusted import contracts as emails
from tools.email.trusted.effects import send_email
import clauz3
@clauz3.guarantee(emails.only(["bob@example.com"]))
def main() -> None:
send_email("bob@example.com", "hi")
Of course, this case is trivial. The contract is somewhat superfluous here
as it's easy for a person to see the one-line main() function and infer it
only emails the one person. But it serves to illustrate the simplest case
where a user need only read the contract and not the program.
In a real world scenario, the agent may author a more complex program that is harder for a person to quickly reason over.
A failing case¶
The same guarantee with a non-allow-listed recipient is rejected by the prover.
examples/email/cases/only_bob_fail.py
# ruff: noqa: F821
from tools.email.trusted import contracts as emails
from tools.email.trusted.effects import send_email
import clauz3
@clauz3.guarantee(emails.only(["bob@example.com"]))
def main() -> None:
send_email("ann@example.com", "hi")
The runtime layer¶
The static prover is only the first layer. The @clauz3.requires on
send_email (addr must contain "@") is also a runtime check. The two
layers act on the same artifact at different times.
Statically, precondition_fail.py is rejected by the prover before anything
runs, because it cannot discharge that precondition for the call below:
examples/email/cases/precondition_fail.py
# ruff: noqa: F821
from tools.email.trusted import contracts as emails
from tools.email.trusted.effects import send_email
import clauz3
@clauz3.guarantee(emails.only(["bob@example.com"]))
def main() -> None:
send_email("not-an-email", "hi")
Choosing no_guarantees() removes the email-effect guarantee; it does not
remove preconditions. The prover still rejects a call whose address lacks
"@". The same precondition is also enforced at runtime, including when the
trusted function is called directly without a proof:
>>> from tools.email.trusted.effects import send_email
>>> send_email("bob@example.com", "hi") # precondition holds: runs
>>> send_email("not-an-email", "hi") # precondition violated
PreContractError: addr must contain "@" (where addr='not-an-email', msg='hi')
Same check, different time: the prover discharges it ahead of execution, the boundary enforces it at execution. See Concepts: static proof vs runtime.
All cases¶
Every file under cases/ is a small program plus its declared guarantee.
Cases with _pass suffix should prove; cases with _fail should be
rejected. Browse the full set on the all-cases page.
How they run¶
The example Justfile invokes clauz3 prove against each case. _fail
cases are wrapped so a non-zero exit is the expected outcome.
examples/email/Justfile
trusted := "tools/email/trusted"
prove := "uv run clauz3 prove --trusted-root " + trusted
# Proves each case in one process: *_pass.py must prove, *_fail.py must not.
prove-cases := "uv run python -m clauz3._prove_cases --trusted-root " + trusted
policy-check := "uv run clauz3 policy-check --policy approval-policy.json --trusted-root " + trusted + " --import-root ."
test: lint cases policy
lint:
uv run clauz3 lint {{trusted}}/effects.py
# alias_import_fail.py:
# Issue #129: the guarantee is spelled through an import alias. Before the fix
# the decorator was dropped and this printed "proved!".
cases: while-loop-skip aug-assign-fail aug-assign-rewritten-fail
{{prove-cases}} cases/*_pass.py cases/*_fail.py
policy: policy-auto-pass policy-auto-reject policy-ask
policy-auto-pass:
{{policy-check}} --expect auto_approved cases/only_bob_pass.py
policy-auto-reject:
{{policy-check}} --expect auto_rejected cases/policy_reject.py
policy-ask:
{{policy-check}} --expect ask cases/policy_ask.py
case1:
{{prove}} cases/only_bob_pass.py
case1-fail:
if {{prove}} cases/only_bob_fail.py; then exit 1; fi
# Distinguish unsupported syntax from a refuted guarantee.
while-loop-skip:
out=$({{prove}} cases/while_loop_skip.py 2>&1); status=$?; echo "$out"; test $status -ne 0 && echo "$out" | grep -qF "main: skipped unsupported program body"
# `+=` on a str is proved like its desugaring: a refuted guarantee, not a skip.
aug-assign-fail:
out=$({{prove}} cases/aug_assign_fail.py 2>&1); status=$?; echo "$out"; test $status -ne 0 && echo "$out" | grep -qF "main: failed guarantee"
aug-assign-rewritten-fail:
out=$({{prove}} cases/aug_assign_rewritten_fail.py 2>&1); status=$?; echo "$out"; test $status -ne 0 && echo "$out" | grep -qF "main: failed guarantee"