Skip to content

Policy cookbook

ArhaanDev24 edited this page Sep 20, 2026 · 1 revision

Policy cookbook

Rules for servers that do not ship with a policy. Copy the nearest shape, then correct it against your own server — a rule that names a tool the server does not have is refused at load, so a mistake here fails loudly rather than quietly.

The grammar is deliberately tiny. Three namespaces and dotted paths, nothing else: $. reads the call's own arguments, $snapshot. reads what the pre-read captured, $result. reads what the server answered. [] applies the rest of the path to every element. There are no expressions, and that is on purpose — the moment a manifest becomes a language it stops being writable in fifteen minutes by somebody who has never seen one.

Start every server with:

synartesis init <name> -- <command to start it>

That drafts every tool as irreversible and guarded, except ones the server marks read-only. You are opening it up one tool at a time from there.


The four shapes

1. A read — readonly

Nothing to capture, nothing to undo.

- match: "db.query"
  class: readonly

Match with a wildcard where the server is consistent about naming, which saves writing twenty of these:

- match: "db.get_*"
  class: readonly
- match: "db.list_*"
  class: readonly

2. An overwrite of something that already exists — reversible

The common case, and the only one that restores content exactly. Read the old value first, write it back on undo.

- match: "db.update_record"
  class: reversible
  snapshot:
    tool: "db.get_record"
    args:
      id: "$.id"
  inverse:
    tool: "db.update_record"
    args:
      id: "$.id"
      fields: "$snapshot.fields"

The test that matters: can something be read back out of the snapshot by path? If the read answers in prose written for a person — "Record 12 was last updated on Tuesday" rather than {"id": 12, ...} — then nothing can be interpolated out of it and the tool is not reversible through this server, however reversible the underlying operation is. This is exactly why the shipped git policy has almost nothing in it.

3. Making something that did not exist — compensable

There is no prior state to capture, because the thing did not exist. A different call offsets it instead.

- match: "db.create_record"
  class: compensable
  inverse:
    tool: "db.delete_record"
    args:
      id: "$result.id"

Use $result, not $., for anything the server assigns. An id you did not send is only knowable from the answer. And there is a sharper reason: if the server treats a duplicate create as a no-op, an inverse built from the arguments deletes the record that was already there — an agent's no-op costing somebody data they had before it ran.

A compensation has no pre-read, so undo has nothing to compare against and will compensate over somebody else's later edit. Declare a verify read and it stops doing that:

  verify:
    tool: "db.get_record"
    args:
      id: "$result.id"

verify is resolved after the call, so $result is available and it can name a resource the call itself created.

4. Neither — irreversible

Say so, and let it be held.

- match: "db.drop_table"
  class: irreversible
  gate: always

gate: never on an irreversible tool is occasionally right — for something that destroys nothing and cannot be reversed only because no read reports the state as data. The shipped git policy uses it for git_checkout, on the reasoning that a gate on every branch switch is one people learn to approve without reading, which costs more than it buys on the commit next to it.


Recipes for awkward cases

Moving or renaming, where the destination may be occupied

Reversible when the path is free, and beyond undo when it is not — one inverse cannot both move your file back and restore what it landed on. expect: absent inverts the pre-read so that finding nothing is the reversible case:

- match: "store.move_object"
  class: reversible
  snapshot:
    tool: "store.get_object"
    args:
      key: "$.destination"
    absent_when: ["NoSuchKey", "not found"]
    expect: absent
  inverse:
    tool: "store.move_object"
    args:
      source: "$.destination"
      destination: "$.source"

Finding something at the destination is then held for a person and recorded with no inverse, so undo says it cannot be undone rather than putting half of it back and reporting success.

A delete you can restore

If the server has a restore or undelete, it is compensable rather than irreversible:

- match: "crm.delete_customer"
  class: reversible
  snapshot:
    tool: "crm.get_customer"
    args:
      id: "$.id"
  inverse:
    tool: "crm.restore_customer"
    args:
      customer: "$snapshot"

$snapshot bare is the whole captured value, which is what you want when the restore takes the record rather than a field of it.

A tool that does two things at once

A call that writes a file and sends a notification is irreversible, and the reason is worth being exact about: the file can be put back, the notification cannot, and a class is a property of the whole call. Classify it irreversible, and if you control the server, split the tool.

A server that answers in prose

Most of what stops a tool being reversible is not the operation — it is that the read gives you a sentence rather than a field. If you control the server, have the read return structured data. If you do not, the tool is irreversible here and honest about it.


Before you rely on it

synartesis check

Starts every server the manifest names, confirms each tool exists, and lists the tools your policy does not cover — those are the ones that will stop your agent mid-task.

Then pin the shapes you wrote it against, so a server that changes a tool's arguments under you stops the proxy instead of quietly serving the old policy:

synartesis pin

And the step none of the above replaces: make the change, undo it, and look at the result with something other than Synartesis. A policy can name every tool correctly, take exactly the arguments each one wants, and still resolve an inverse that restores nothing — and that failure reports success. The three adapter tests in tests/ are the shape of that check if you want one to copy.


Contributed policies

Add yours here with whether you round-tripped it. If it is good enough to rely on, send it to manifests/ as a pull request too.

Server Author Round-tripped? Link
(yours)