Skip to content

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 just a stub function decorated with @deal.has(...) and a precondition.

examples/email/tools/email/trusted/effects.py

import deal


@deal.pre(lambda addr, msg: "@" in addr, message='addr must contain "@"')
@deal.has("trusted")
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 generic effect("send_email") relation.

examples/email/tools/email/trusted/contracts.py

"""Email-specific predicates built from generic trusted effect facts."""

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)

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")

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 (deal as a backstop)

The static prover is only the first layer. The @deal.pre on send_email (addr must contain "@") is also a runtime contract. 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")

The same precondition is what deal enforces at runtime. If the program is run outside a successful proof — a weak or no_guarantees() contract, or runtime-only mode — deal still raises on the bad call at the trusted boundary:

>>> 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
deal.PreContractError: addr must contain "@" (where addr='not-an-email', msg='hi')

Same check, different time: the prover discharges it ahead of execution, deal 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"
effects := trusted + "/effects.py"
prove := "uv run clauz3 prove --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 python -m deal lint {{effects}}

cases: only-bob-pass only-bob-fail none-pass none-fail no-guarantees-pass helper-pass helper-fail precondition-fail dead-branch-pass unique-recipients-pass unique-recipients-fail unique-recipients-branch-pass nobody-emailed-twice-fail at-most-two-pass at-most-two-fail bob-at-most-two-pass bob-at-most-two-fail bob-at-most-two-branch-pass content-length-pass content-length-fail content-length-branch-pass same-content-pass same-content-fail same-content-branch-pass

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

only-bob-pass:
    {{prove}} cases/only_bob_pass.py

only-bob-fail:
    if {{prove}} cases/only_bob_fail.py; then exit 1; fi

none-pass:
    {{prove}} cases/none_pass.py

none-fail:
    if {{prove}} cases/none_fail.py; then exit 1; fi

no-guarantees-pass:
    {{prove}} cases/no_guarantees_pass.py

helper-pass:
    {{prove}} cases/helper_pass.py

helper-fail:
    if {{prove}} cases/helper_fail.py; then exit 1; fi

precondition-fail:
    if {{prove}} cases/precondition_fail.py; then exit 1; fi

dead-branch-pass:
    {{prove}} cases/dead_branch_pass.py

unique-recipients-pass:
    {{prove}} cases/unique_recipients_pass.py

unique-recipients-fail:
    if {{prove}} cases/unique_recipients_fail.py; then exit 1; fi

unique-recipients-branch-pass:
    {{prove}} cases/unique_recipients_branch_pass.py

nobody-emailed-twice-fail:
    if {{prove}} cases/nobody_emailed_twice_fail.py; then exit 1; fi

at-most-two-pass:
    {{prove}} cases/at_most_two_pass.py

at-most-two-fail:
    if {{prove}} cases/at_most_two_fail.py; then exit 1; fi

bob-at-most-two-pass:
    {{prove}} cases/bob_at_most_two_pass.py

bob-at-most-two-fail:
    if {{prove}} cases/bob_at_most_two_fail.py; then exit 1; fi

bob-at-most-two-branch-pass:
    {{prove}} cases/bob_at_most_two_branch_pass.py

content-length-pass:
    {{prove}} cases/content_length_pass.py

content-length-fail:
    if {{prove}} cases/content_length_fail.py; then exit 1; fi

content-length-branch-pass:
    {{prove}} cases/content_length_branch_pass.py

same-content-pass:
    {{prove}} cases/same_content_pass.py

same-content-fail:
    if {{prove}} cases/same_content_fail.py; then exit 1; fi

same-content-branch-pass:
    {{prove}} cases/same_content_branch_pass.py

case1: only-bob-pass

case1-fail: only-bob-fail