-
-
Notifications
You must be signed in to change notification settings - Fork 0
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.
Nothing to capture, nothing to undo.
- match: "db.query"
class: readonlyMatch 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: readonlyThe 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.
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.
Say so, and let it be held.
- match: "db.drop_table"
class: irreversible
gate: alwaysgate: 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.
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.
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 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.
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.
synartesis checkStarts 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 pinAnd 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.
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) |