Skip to content

Commit 994725d

Browse files
v0.1.3: approval policies, policy-check, text example, docs
1 parent 067dee7 commit 994725d

52 files changed

Lines changed: 2279 additions & 35 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,14 @@ top level. `just stdlib` proves them all.
152152

153153
## Examples
154154

155-
The repo currently has three worked examples:
155+
The repo currently has four worked examples:
156156

157157
| Example | Contracts | What it demonstrates |
158158
| --- | --- | --- |
159159
| [`examples/email`](examples/email) | `emails.only`, `emails.none`, `emails.no_guarantees`, `emails.unique_recipients`, `emails.content_length_at_most` | allowlists, explicit absence of guarantees, absence of effects, pairwise uniqueness, string length bounds |
160160
| [`examples/bank`](examples/bank) | `bank.max_spend`, `bank.only_account` | numeric aggregation, field equality, and fixed-bound loop unrolling over trusted calls |
161161
| [`examples/email-from-db`](examples/email-from-db) | `emails.addresses_from`, `db.only_table`, `db.only_where` | for-loops over trusted query returns, column-binding constraints |
162+
| [`examples/text`](examples/text) | `text.length_between`, `text.must_not_contain`, `text.no_regex_metacharacters`, `text.only_edit_under`, `text.edit_length_at_most` | string-length bounds, required/banned substrings, regex-metacharacter safety before sending, and file-edit path/size policies |
162163

163164
All examples use generic effect relations inferred from trusted function
164165
signatures. There is no email-specific, bank-specific, or database-specific logic in the core.
@@ -177,11 +178,19 @@ Implemented pieces include:
177178
- `clauz3 install` for copying a trusted `tools/` layer from a local path or a
178179
bundled stdlib tool (`stdlib:filesystem`, `stdlib:grep`), optionally
179180
generating `agents/skills/<domain>/SKILL.md` stubs.
181+
- `clauz3 config` for writing this repo's default Claude Code permissions
182+
(read-only tools plus the `clauz3` CLI) to `.claude/settings.json`. Idempotent
183+
and the configuration counterpart to `install`.
180184
- `clauz3 run` for proving a complete inline program, submitting an approval
181185
request to an externally configured approval service, and executing `main`
182186
only after an approval receipt is returned.
183187
- `clauz3 approval-service` for starting a simple localhost FastAPI approval
184-
service with REST endpoints and a browser UI for user decisions.
188+
service with REST endpoints and a browser UI for user decisions. With
189+
`--policy`, a policy admin's rules can auto-approve or auto-reject a request
190+
by asking the prover whether the program *entails* the rule's contracts,
191+
falling back to a human otherwise. `clauz3 policy-check` dry-runs a policy
192+
against a program. See
193+
[docs/todos/approval-policies.md](docs/todos/approval-policies.md).
185194
- `clauz3 mock-approval-service` for config-driven tests and local demos.
186195
- For-loops over `list[Row]`-returning trusted calls, with column-binding
187196
contracts via `UserRow.email` markers. See
@@ -218,6 +227,7 @@ More detail:
218227
- [ClauZ3 Python subset](docs/reference/python-subset.md)
219228
- [Approval service](docs/how-to/approval-service.md)
220229
- [User approval dialog design](docs/todos/user-approval-dialog.md)
230+
- [Guardians synergies](docs/todos/guardians-synergies.md)
221231
- [Ideas](docs/todos/ideas.md)
222232

223233
## Development

docs/examples/email.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,33 @@ prover.
3333

3434
{{ include_file("examples/email/cases/only_bob_fail.py") }}
3535

36+
## The runtime layer (deal as a backstop)
37+
38+
The static prover is only the first layer. The `@deal.pre` on `send_email`
39+
(`addr` must contain `"@"`) is also a *runtime* contract. The two layers act on
40+
the same artifact at different times.
41+
42+
Statically, `precondition_fail.py` is rejected by the prover before anything
43+
runs, because it cannot discharge that precondition for the call below:
44+
45+
{{ include_file("examples/email/cases/precondition_fail.py") }}
46+
47+
The same precondition is what [deal](https://deal.readthedocs.io/) enforces at
48+
runtime. If the program is run outside a successful proof — a weak or
49+
`no_guarantees()` contract, or runtime-only mode — deal still raises on the bad
50+
call at the trusted boundary:
51+
52+
```pycon
53+
>>> from tools.email.trusted.effects import send_email
54+
>>> send_email("bob@example.com", "hi") # precondition holds: runs
55+
>>> send_email("not-an-email", "hi") # precondition violated
56+
deal.PreContractError: addr must contain "@" (where addr='not-an-email', msg='hi')
57+
```
58+
59+
Same check, different time: the prover discharges it ahead of execution, deal
60+
enforces it at execution. See
61+
[Concepts: static proof vs runtime](../explanation/concepts.md#static-proof-vs-runtime).
62+
3663
## All cases
3764

3865
Every file under `cases/` is a small program plus its declared guarantee.

docs/examples/text-all-cases.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Text — all cases
2+
3+
Every case file under `examples/text/cases/`, inlined. `_pass` cases
4+
should prove; `_fail` cases should be rejected by the prover.
5+
6+
See the [curated walk-through](text.md) for context on the trusted module
7+
and contract vocabulary.
8+
9+
{{ include_dir("examples/text/cases", glob="*.py") }}

docs/examples/text.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Text example
2+
3+
The text example shows the relation algebra's **string** side: length bounds,
4+
substring requirements, and prefix policies. Everything here compiles to Z3's
5+
string theory — `len(...)` becomes `z3.Length`, `x in s` becomes `z3.Contains`,
6+
and `s.startswith(...)` becomes `z3.PrefixOf` — so the prover reasons about the
7+
*shape* of text symbolically, across every reachable branch.
8+
9+
The trusted module exposes two side-effecting functions:
10+
11+
- `send_message(channel, text)` — post text somewhere;
12+
- `edit_file(path, new_text)` — replace a file's contents.
13+
14+
The contract vocabulary then states policies an agent (or a user reviewing the
15+
permission request) cares about:
16+
17+
- **lengths within limits**`length_at_most`, `length_at_least`,
18+
`length_between`;
19+
- **required or banned substrings**`must_contain` (e.g. a mandatory
20+
`[automated]` footer), `must_not_contain` (e.g. a banned token);
21+
- **regex safety before sending**`no_regex_metacharacters`, which forbids
22+
every regular-expression metacharacter so agent- or user-influenced text
23+
cannot inject a pattern or trigger catastrophic backtracking (ReDoS)
24+
downstream;
25+
- **bounded sends**`sends_at_most`;
26+
- **file-edit policies**`only_edit_under` (a path-prefix sandbox),
27+
`edit_length_at_most` (bounded rewrites), and `no_edits`.
28+
29+
## Trusted module
30+
31+
The trusted effects are stubs decorated with `@deal.has(...)` and a
32+
non-empty precondition. The prover lifts each precondition into a proof
33+
obligation at every call site.
34+
35+
{{ include_file("examples/text/tools/text/trusted/effects.py") }}
36+
37+
The contract module builds the string vocabulary on top of the generic
38+
`effect("send_message")` and `effect("edit_file")` relations. Note that the
39+
length and substring checks live inside ordinary lambdas — the same relation
40+
language used elsewhere, just exercising its string operators.
41+
42+
{{ include_file("examples/text/tools/text/trusted/contracts.py") }}
43+
44+
## A passing case
45+
46+
Both messages are within the 20-character bound, so `text.length_at_most(20)`
47+
is discharged.
48+
49+
{{ include_file("examples/text/cases/length_at_most_pass.py") }}
50+
51+
## A failing case
52+
53+
Text that will be fed to a pattern matcher must be metacharacter-free; the
54+
classic ReDoS pattern `(a+)+$` violates `text.no_regex_metacharacters()` and
55+
the proof fails.
56+
57+
{{ include_file("examples/text/cases/no_regex_metacharacters_fail.py") }}
58+
59+
## All cases
60+
61+
Every file under `cases/` is a small program plus its declared guarantee.
62+
Cases with the `_pass` suffix should prove; cases with `_fail` should be
63+
rejected. Browse the full set on the [all-cases page](text-all-cases.md).
64+
65+
## How they run
66+
67+
The example `Justfile` invokes `clauz3 prove` against each case. `_fail`
68+
cases are wrapped so a non-zero exit is the expected outcome.
69+
70+
{{ include_file("examples/text/Justfile") }}

docs/explanation/background.md

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,134 @@ The mechanism differs:
8888
- The deployment is governed by an environment contract: enforcement is only as
8989
sound as the facts the environment promises to publish.
9090

91-
`clauz3` currently proves a single Python program before execution. FORGE
92-
enforces individual decisions during execution. Those designs are not
93-
substitutes. A static contract proof gives the user a concise permission
94-
artifact and avoids partial side effects; runtime enforcement gives defense in
95-
depth when execution drifts from the proved model.
91+
`clauz3` proves a single Python program before execution; that *static* proof
92+
is its distinctive layer. FORGE-style policy enforcement decides individual
93+
actions during execution. Those designs are not substitutes. A static contract
94+
proof gives the user a concise permission artifact and avoids partial side
95+
effects; policy-monitor runtime enforcement gives defense in depth when
96+
execution drifts from the proved model.
97+
98+
Note this comparison is about *policy* enforcement, not about whether clauz3 has
99+
any runtime checks at all — it does. The trusted layer's deal preconditions and
100+
effect markers are enforced at runtime regardless of the proof (see
101+
[Concepts: static proof vs runtime](concepts.md#static-proof-vs-runtime)). What
102+
clauz3 lacks, relative to FORGE, is a reference monitor evaluating a policy over
103+
the agent's actions as they happen.
104+
105+
## Guardians
106+
107+
Meijer, *Guardians of the Agents: Formal verification of AI workflows*
108+
([ACM Queue 23(4), July–August 2025](https://queue.acm.org/detail.cfm?id=3762990);
109+
Communications of the ACM, January 2026;
110+
[doi:10.1145/3777544](https://doi.org/10.1145/3777544)), proposes a
111+
**generate → verify → execute** discipline for agents, with an open-source
112+
implementation at
113+
[`metareflection/guardians`](https://github.com/metareflection/guardians).
114+
Meijer frames it as "a safety paradigm rooted in mathematical proof
115+
verification" and an extension of Java/.NET **bytecode verification** to agentic
116+
computation — the same "complexity in production, verification stays simple"
117+
asymmetry that motivates proof-carrying code above.
118+
119+
The core argument is that prompt injection has the same root cause as SQL
120+
injection: code (instructions) and data (content) are not separated, so the fix
121+
is the same. Instead of letting the model call one tool, read the result, and
122+
then decide the next side-effecting action, the model emits a complete
123+
structured **workflow** *up front* — a JSON AST of steps whose arguments are
124+
**symbolic references** (string result bindings such as `"emails_fetched"`,
125+
rendered `@emails_fetched` in the literate explanation; modeled as `SymRef` in
126+
the implementation) rather than concrete values. The plan is authored from the
127+
goal and tool specs alone, before any concrete (possibly attacker-controlled)
128+
data exists, then verified against a security policy, and only then executed.
129+
Because the code is fixed before the data arrives, malicious content in inbox
130+
data, tool results, *or tool descriptions* cannot introduce a new
131+
side-effecting step; anything unexpected is caught at verification and the
132+
workflow is rejected.
133+
134+
The paper's running example is an email exfiltration: a malicious inbox message
135+
instructs the agent to silently forward a summary to `it@othercorp.com`.
136+
Verification draws on:
137+
138+
- **Source-sink / taint analysis** — a policy forbidding data flow from
139+
`fetch_email`'s result to `send_email`'s `body` when the `to` argument is
140+
outside an allowlist. Meijer suggests a CodeQL path query or SemGrep; the
141+
implementation carries this as its own `TaintRule` with provenance labels and
142+
sanitizers.
143+
- **Security automata** — a finite state machine over the tool-call sequence
144+
that rejects on reaching an error state (the paper's figure 2 forbids sending
145+
to external domains). Meijer introduces this as a *runtime* monitor; the
146+
implementation also evaluates it at verification time.
147+
- **Z3 / Dafny over pre/post/frame conditions** — including a careful treatment
148+
of the **frame problem** (McCarthy & Hayes, 1969): a naive postcondition for
149+
"delete foo.txt and bar.txt" is also satisfied by `delete_file("*.txt")`, so a
150+
**frame condition** ("files not matching the pattern are unchanged") is needed
151+
before the over-broad plan fails to verify.
152+
153+
Meijer gives three reasons to verify first: prevention rather than detection,
154+
**eliminating the need for rollbacks** (only verified workflows run, so there is
155+
no partial side effect to undo), and automation. The middle reason is the same
156+
transactional argument `clauz3` makes for static proof over runtime monitors
157+
(see [static proof vs runtime](concepts.md#static-proof-vs-runtime)).
158+
159+
The shared ground with `clauz3` is substantial: both reject unsafe behavior
160+
*before* side effects run, both make the tool/effect trust boundary explicit,
161+
both compile to Z3, and both replace ad-hoc natural-language promises with a
162+
checkable artifact. The mechanism differs:
163+
164+
- Guardians verifies a structured **workflow AST** against an operator-defined
165+
policy, with the plan fixed before data arrives.
166+
- `clauz3` proves an agent-authored **Python program** satisfies
167+
agent-stated guarantees over trusted effect facts derived by symbolic
168+
execution, and treats the proved contract as the user-facing consent artifact.
169+
170+
Where Guardians is ahead, and `clauz3` is not yet:
171+
172+
- a first-class symbolic workflow representation, with the clean code/data
173+
separation that gives the prompt-injection story;
174+
- built-in taint/provenance tracking for source-to-sink data flow;
175+
- built-in security automata for tool-call *sequences*.
176+
177+
One difference cuts the other way. Meijer needs explicit frame conditions
178+
because his Z3 obligations describe world state (`fileSystem`), and a model will
179+
satisfy an under-specified postcondition the cheapest way it can. `clauz3`
180+
reasons instead over a *closed-world finite trace* of effect facts — an effect
181+
not in the trace provably does not happen — so contracts like
182+
`filesystem.only_write_under` or `emails.only` bound the entire effect set
183+
directly, giving the frame guarantee implicitly for the effects the prover
184+
tracks (see
185+
[effect-IR](../todos/effect-ir.md#what-level-fol-datalog-or-the-z3-lisp-syntax)).
186+
187+
`clauz3` does already reason about data it has not seen — its symbolic
188+
iteration over a trusted query's `list[Row]` return, with `UserRow.email`
189+
column-binding contracts (see
190+
[symbolic iteration](symbolic-iteration.md)), is the closest existing analog to
191+
"plan before the data." What it lacks is the provenance layer needed to say "a
192+
value that came from `fetch_mail` must not reach `send_email.body`," and any
193+
notion of effect *order* on which an automaton could run; today the effect facts
194+
are an unordered set carrying only path conditions.
195+
196+
Where `clauz3` is distinct:
197+
198+
- ordinary Python as the agent-authored surface, rather than a bespoke workflow
199+
AST;
200+
- effect facts inferred from trusted function signatures, rather than
201+
hand-declared `ToolSpec`s;
202+
- a user-facing contract vocabulary (allowlists, absence, uniqueness, counts,
203+
sums, filtered counts, joins) and an approval service built around proved
204+
guarantees and coverage, not pass/fail policy enforcement;
205+
- contract *abduction* — the agent proposes the permission artifact (see
206+
"Operator Policy Versus Agent-Abduced Contracts" below).
207+
208+
These projects look more like complementary halves than alternatives. The
209+
concrete synergy tracks — taint/provenance in the fact layer, security automata
210+
over effect traces, a workflow front-end that lowers into the effect IR, a
211+
combined approval surface, and a shared email-exfiltration benchmark — are
212+
collected in [Guardians synergies](../todos/guardians-synergies.md).
96213

97214
## Operator Policy Versus Agent-Abduced Contracts
98215

99-
FORGE assumes policies are written by humans up front and applied to agents.
100-
That is the right shape for organizational rules the agent should not be able
101-
to weaken.
216+
FORGE and Guardians both assume policies are written by humans up front and
217+
applied to agents. That is the right shape for organizational rules the agent
218+
should not be able to weaken.
102219

103220
`clauz3` explores the opposite surface: the agent proposes a contract for a
104221
specific program, the prover checks that program against that contract, and the
@@ -172,13 +289,18 @@ Provenance facts:
172289
The current prover records trusted calls as independent effect facts. It cannot
173290
yet express policies like "this URL must have come from a literal allowlist or
174291
from a trusted database table." A provenance relation in the fact layer would
175-
unlock that class of policy.
292+
unlock that class of policy. Guardians' source-sink taint model is the sharpest
293+
articulation of this idea; see
294+
[Guardians synergies](../todos/guardians-synergies.md) for how it could attach
295+
to the `FactInfo` layer.
176296

177297
Benchmarks:
178298

179299
The repo has examples by proof shape. It does not yet have an adversarial
180300
benchmark. Even a small corpus of plausible agent-authored programs that try to
181-
violate contracts would make prover coverage more measurable.
301+
violate contracts would make prover coverage more measurable. The Guardians
302+
email-exfiltration scenario is a natural first adversarial case; see
303+
[Guardians synergies](../todos/guardians-synergies.md#track-5-shared-examples-benchmark).
182304

183305
## What Is Distinct Here
184306

docs/explanation/concepts.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,26 @@ proof succeeds, the deal checks at runtime are redundant (but still safe). If
162162
the proof is skipped or the contract is intentionally weak, deal is still on
163163
guard at the trusted boundary.
164164

165+
Concretely, the email trusted layer declares `@deal.pre(lambda addr, msg: "@"
166+
in addr)` on `send_email`. A program that calls `send_email("not-an-email",
167+
...)` is rejected *statically* by the prover (it cannot discharge the
168+
precondition). The exact same precondition fires *at runtime* if such a call is
169+
ever executed:
170+
171+
```pycon
172+
>>> send_email("not-an-email", "hi")
173+
deal.PreContractError: addr must contain "@" (where addr='not-an-email', msg='hi')
174+
```
175+
176+
Same obligation, two enforcement points. See
177+
[the email example](../examples/email.md#the-runtime-layer-deal-as-a-backstop)
178+
for the full worked case.
179+
180+
So the only enforcement that is genuinely *not* implemented yet is binding the
181+
approval **receipt** into this runtime boundary — a trusted effect refusing to
182+
run without a valid receipt. The deal preconditions, postconditions, and effect
183+
markers themselves are enforced today.
184+
165185
## Glossary
166186

167187
- **User** — reviews and consents to contracts; does not read or run code.

0 commit comments

Comments
 (0)