revoco 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {revoco-0.2.2 → revoco-0.3.0}/PKG-INFO +47 -3
  2. {revoco-0.2.2 → revoco-0.3.0}/README.md +46 -2
  3. {revoco-0.2.2 → revoco-0.3.0}/pyproject.toml +1 -1
  4. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/__init__.py +3 -1
  5. revoco-0.3.0/src/revoco/adapters/ras_eval.py +187 -0
  6. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/__init__.py +7 -0
  7. revoco-0.3.0/src/revoco/bench/external.py +325 -0
  8. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/cli.py +12 -0
  9. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/controlplane.py +40 -6
  10. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/drills.py +23 -1
  11. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/ledger.py +34 -19
  12. revoco-0.3.0/src/revoco/store/__init__.py +15 -0
  13. revoco-0.3.0/src/revoco/store/sqlite.py +448 -0
  14. revoco-0.3.0/tests/test_external_corpus.py +178 -0
  15. revoco-0.3.0/tests/test_store.py +361 -0
  16. {revoco-0.2.2 → revoco-0.3.0}/.github/workflows/ci.yml +0 -0
  17. {revoco-0.2.2 → revoco-0.3.0}/.github/workflows/release.yml +0 -0
  18. {revoco-0.2.2 → revoco-0.3.0}/.gitignore +0 -0
  19. {revoco-0.2.2 → revoco-0.3.0}/LICENSE +0 -0
  20. {revoco-0.2.2 → revoco-0.3.0}/docs/ADAPTERS.md +0 -0
  21. {revoco-0.2.2 → revoco-0.3.0}/docs/RELEASING.md +0 -0
  22. {revoco-0.2.2 → revoco-0.3.0}/examples/inverses.yaml +0 -0
  23. {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_cloud.json +0 -0
  24. {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_database.json +0 -0
  25. {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_devops.json +0 -0
  26. {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_identity.json +0 -0
  27. {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_saas.json +0 -0
  28. {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_sap.json +0 -0
  29. {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_workday.json +0 -0
  30. {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_workstation.json +0 -0
  31. {revoco-0.2.2 → revoco-0.3.0}/examples/policy.yaml +0 -0
  32. {revoco-0.2.2 → revoco-0.3.0}/scripts/bump_version.py +0 -0
  33. {revoco-0.2.2 → revoco-0.3.0}/scripts/validate_workstation.py +0 -0
  34. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/__init__.py +0 -0
  35. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/cloud.py +0 -0
  36. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/database.py +0 -0
  37. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/devops.py +0 -0
  38. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/identity.py +0 -0
  39. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/saas.py +0 -0
  40. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/sap.py +0 -0
  41. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/workday.py +0 -0
  42. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/workstation.py +0 -0
  43. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/__init__.py +0 -0
  44. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/action.py +0 -0
  45. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/delegation.py +0 -0
  46. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/engine.py +0 -0
  47. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/principals.py +0 -0
  48. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/revocation.py +0 -0
  49. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/scope.py +0 -0
  50. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/corpus.py +0 -0
  51. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/harness.py +0 -0
  52. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/report.py +0 -0
  53. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/scenario.py +0 -0
  54. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/world.py +0 -0
  55. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/core/__init__.py +0 -0
  56. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/core/crypto.py +0 -0
  57. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/core/errors.py +0 -0
  58. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/core/ids.py +0 -0
  59. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/demo.py +0 -0
  60. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/detect.py +0 -0
  61. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/evidence.py +0 -0
  62. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/__init__.py +0 -0
  63. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/conditions.py +0 -0
  64. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/decision.py +0 -0
  65. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/engine.py +0 -0
  66. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/policy.py +0 -0
  67. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/session.py +0 -0
  68. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/threats.py +0 -0
  69. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/__init__.py +0 -0
  70. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/budget.py +0 -0
  71. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/engine.py +0 -0
  72. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/horizon.py +0 -0
  73. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/model.py +0 -0
  74. {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/registry.py +0 -0
  75. {revoco-0.2.2 → revoco-0.3.0}/tests/test_adapter_catalog.py +0 -0
  76. {revoco-0.2.2 → revoco-0.3.0}/tests/test_adapters.py +0 -0
  77. {revoco-0.2.2 → revoco-0.3.0}/tests/test_authority.py +0 -0
  78. {revoco-0.2.2 → revoco-0.3.0}/tests/test_bench.py +0 -0
  79. {revoco-0.2.2 → revoco-0.3.0}/tests/test_budget_and_drills.py +0 -0
  80. {revoco-0.2.2 → revoco-0.3.0}/tests/test_controlplane.py +0 -0
  81. {revoco-0.2.2 → revoco-0.3.0}/tests/test_core.py +0 -0
  82. {revoco-0.2.2 → revoco-0.3.0}/tests/test_demo_and_cli.py +0 -0
  83. {revoco-0.2.2 → revoco-0.3.0}/tests/test_gate.py +0 -0
  84. {revoco-0.2.2 → revoco-0.3.0}/tests/test_horizon_and_scheduling.py +0 -0
  85. {revoco-0.2.2 → revoco-0.3.0}/tests/test_ledger.py +0 -0
  86. {revoco-0.2.2 → revoco-0.3.0}/tests/test_reversal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: revoco
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Undo for AI agent actions. Plans the rollback before the action runs, proves it still works, and rolls back a compromised grant's whole blast radius in one call.
5
5
  Project-URL: Homepage, https://github.com/rsh1k/revoco
6
6
  Project-URL: Source, https://github.com/rsh1k/revoco
@@ -212,6 +212,33 @@ It keeps five states apart, and the distinctions carry the value: a window that
212
212
 
213
213
  ---
214
214
 
215
+ ## Persistence
216
+
217
+ ```python
218
+ from revoco import ControlPlane
219
+ from revoco.store import SqliteStore
220
+
221
+ store = SqliteStore("/var/lib/revoco/revoco.db")
222
+ cp = ControlPlane(store=store, ...)
223
+
224
+ report = store.startup_report() # what this restart means
225
+ print(report.notes)
226
+ ```
227
+
228
+ Four decisions, each with a reason:
229
+
230
+ **WAL with `synchronous=FULL`.** SQLite's defaults are ambiguous enough that you can't rely on them, and `NORMAL` doesn't survive power loss. For an evidence store that isn't a trade-off — a ledger that silently drops its last entries is precisely the truncation case a self-contained hash chain **cannot** detect, because the remaining prefix still verifies.
231
+
232
+ **Append-only by triggers, not permissions.** SQLite has no `GRANT`, so the INSERT-only grant you'd use in Postgres isn't available. `BEFORE UPDATE`/`BEFORE DELETE` triggers that `RAISE(ABORT)` are the mechanism, and they bind *any* client — including a `sqlite3` shell, not just this code. Worth stating because the Postgres instinct silently produces no protection here.
233
+
234
+ **The ledger append and journal write are one transaction.** The correctness question persistence actually raises. Written separately, a crash between them leaves the journal claiming a plan the ledger never recorded — and afterwards nothing can tell you which is true. Also: entries are prepared, persisted, *then* added to the in-memory chain, so the in-memory head can never outrun the durable one.
235
+
236
+ **No fake freshness after downtime.** The tempting fix for "every proof is stale after an outage" is to stop counting downtime against staleness. That's wrong — an ERP upgrade during the outage is exactly when a spec silently becomes a confident wrong rollback, and a proof that survived on a technicality is a phantom rollback with a certificate. So downtime counts, `startup_report()` says how many proofs went stale and for how long, and `due()` puts them at the front of the queue. Visible and remediated fast beats invisible and assumed good.
237
+
238
+ `startup_report()` also distinguishes a **broken chain from a restart** — a clean restart loses nothing, so verification failure is never an artefact of restarting and says so in as many words.
239
+
240
+ ---
241
+
215
242
  ## The containment benchmark
216
243
 
217
244
  ```bash
@@ -234,7 +261,24 @@ Every malicious technique has a **benign twin on the same tools**, so a policy t
234
261
 
235
262
  Because it runs a real `ControlPlane` against a simulated world, it doubles as a regression suite for the 91 adapter specs — and it earned that keep immediately, finding six real defects including an inverse that relied on implicit convention and a `Rule` that couldn't express "escalate irreversible work only when consequential".
236
263
 
237
- **Honest about the comparison:** ADR-Bench is ~5× larger, drawn from real enterprise telemetry, and far more imbalanced (6:1 benign vs 2.2:1 here). Detection coverage is their strength; verified recoverability is this one's. Complementary instruments, not competing ones.
264
+ ### Importing real benign traffic
265
+
266
+ Hand-authored benign scenarios have a structural blind spot: they contain the false positives I thought to look for. So the corpus can import benign tasks from a [RAS-Eval](https://github.com/lanzer-tree/RAS-Eval) clone — traffic real models actually produced, with real arguments.
267
+
268
+ ```bash
269
+ git clone https://github.com/lanzer-tree/RAS-Eval /path/to/RAS-Eval
270
+ RAS_EVAL_PATH=/path/to/RAS-Eval revoco bench --external
271
+ ```
272
+
273
+ That takes the corpus to **121 scenarios at 5.7:1 benign-to-malicious** — essentially ADR-Bench's ratio — while holding 0% false positives.
274
+
275
+ **Nothing is vendored.** RAS-Eval declares no license, so its data is all-rights-reserved by default; the loader reads a clone you fetch yourself and returns nothing when it's absent, so CI never depends on it.
276
+
277
+ **It found two real bugs on its first run**, at a 16.2% false-positive rate the hand-authored corpus could not have surfaced — because I wrote both the spec and the scenario, and used my own invented argument names in each. `insert_data` takes `db_path`, not the `table` I inferred from the tool's name. `convert_file_to_markdown` takes a `save_path` argument rather than returning `output_path`. In both cases the inverse could never resolve, so every legitimate call raised a phantom rollback. **The same mistake in an SAP adapter would look identical and cost considerably more.**
278
+
279
+ Two disciplines keep the import honest: a tool this package hasn't classified is **skipped**, not imported as `UNKNOWN`, and a call with no observed arguments is **dropped** rather than imported — both would manufacture findings out of missing metadata rather than out of anything the control plane did. And only the 80 *unique tasks* are imported, not all 640 traces: eight models ran the same tasks, so taking every run would multiply the count with near-duplicates. Padding is the thing this corpus exists not to do.
280
+
281
+ **Honest about the comparison:** ADR-Bench's 302 tasks come from real enterprise telemetry across 133 MCP servers. The imported traffic here is consumer-domain — alarms, calendars, disk stats, arXiv lookups — so it broadens the benign distribution without reaching the enterprise write surfaces where the money is. Detection coverage is their strength; verified recoverability is this one's. Complementary instruments.
238
282
 
239
283
  One gap is left visible rather than tuned away: `T09` irreversible fan-out. `PRA01` is a threshold detector, so four one-way wires land before the pattern is visible. The controlled pair `M10`/`M18` measures detection versus the budget on the identical attack, and both stay in the corpus so the difference is attributable.
240
284
 
@@ -314,7 +358,7 @@ This is a **working foundation**, published so it can be read, run, and extended
314
358
  - **Intent-drift detection is lexical overlap.** It flags divergence; it does not establish intent.
315
359
  - **A hash chain does not detect truncation.** Edits, reorders, and interior deletions break verification; dropping the most recent entries leaves a valid prefix. Anchor the head hash externally — `Ledger.checkpoint()` gives you the value to publish.
316
360
  - **In-memory stores are single-process.** `InMemorySessionStore.would_exceed` followed by `commit` is not atomic, so two concurrent calls can both pass a check only one should. A shared store must make that pair atomic.
317
- - **Nothing persists yet.** The ledger, the reversal journal, and the drill register are all in memory, so they reset on restart. Three consequences worth knowing: the horizon forgets undo windows that are still open, `RecoverabilityRegister`'s freshness window means nothing across deploys, and a restart loses the evidence chain rather than breaking it — which is a different failure from tampering and currently indistinguishable from it. Persisting the ledger needs WAL with `synchronous=FULL` and append-only enforced by triggers (SQLite has no `GRANT`), the ledger append and journal write in one transaction, and a startup grace period so a long outage does not mass-degrade every proof to `IRREVERSIBLE` and block legitimate work.
361
+ - **Persistence is opt-in and single-writer.** Pass `store=SqliteStore(path)` and the ledger, journal and drill history survive a restart; omit it and everything is in memory, which is fine for a test and wrong for anything real. The hash chain is single-writer by construction, so two processes sharing one file will collide on sequence numbers — the store raises rather than overwriting, but it does not coordinate. Postgres with advisory locks is the shape for multi-replica; the interface is small enough to swap.
318
362
  - **Control mappings are a self-assessment aid, not a certification** or a legal opinion. NIST AI RMF is voluntary; EU AI Act conformity is assessed against a quality-management system of which logging is one clause.
319
363
 
320
364
  Every place needing production hardening is marked `# HARDENING:` in the source. Search for it before deploying.
@@ -179,6 +179,33 @@ It keeps five states apart, and the distinctions carry the value: a window that
179
179
 
180
180
  ---
181
181
 
182
+ ## Persistence
183
+
184
+ ```python
185
+ from revoco import ControlPlane
186
+ from revoco.store import SqliteStore
187
+
188
+ store = SqliteStore("/var/lib/revoco/revoco.db")
189
+ cp = ControlPlane(store=store, ...)
190
+
191
+ report = store.startup_report() # what this restart means
192
+ print(report.notes)
193
+ ```
194
+
195
+ Four decisions, each with a reason:
196
+
197
+ **WAL with `synchronous=FULL`.** SQLite's defaults are ambiguous enough that you can't rely on them, and `NORMAL` doesn't survive power loss. For an evidence store that isn't a trade-off — a ledger that silently drops its last entries is precisely the truncation case a self-contained hash chain **cannot** detect, because the remaining prefix still verifies.
198
+
199
+ **Append-only by triggers, not permissions.** SQLite has no `GRANT`, so the INSERT-only grant you'd use in Postgres isn't available. `BEFORE UPDATE`/`BEFORE DELETE` triggers that `RAISE(ABORT)` are the mechanism, and they bind *any* client — including a `sqlite3` shell, not just this code. Worth stating because the Postgres instinct silently produces no protection here.
200
+
201
+ **The ledger append and journal write are one transaction.** The correctness question persistence actually raises. Written separately, a crash between them leaves the journal claiming a plan the ledger never recorded — and afterwards nothing can tell you which is true. Also: entries are prepared, persisted, *then* added to the in-memory chain, so the in-memory head can never outrun the durable one.
202
+
203
+ **No fake freshness after downtime.** The tempting fix for "every proof is stale after an outage" is to stop counting downtime against staleness. That's wrong — an ERP upgrade during the outage is exactly when a spec silently becomes a confident wrong rollback, and a proof that survived on a technicality is a phantom rollback with a certificate. So downtime counts, `startup_report()` says how many proofs went stale and for how long, and `due()` puts them at the front of the queue. Visible and remediated fast beats invisible and assumed good.
204
+
205
+ `startup_report()` also distinguishes a **broken chain from a restart** — a clean restart loses nothing, so verification failure is never an artefact of restarting and says so in as many words.
206
+
207
+ ---
208
+
182
209
  ## The containment benchmark
183
210
 
184
211
  ```bash
@@ -201,7 +228,24 @@ Every malicious technique has a **benign twin on the same tools**, so a policy t
201
228
 
202
229
  Because it runs a real `ControlPlane` against a simulated world, it doubles as a regression suite for the 91 adapter specs — and it earned that keep immediately, finding six real defects including an inverse that relied on implicit convention and a `Rule` that couldn't express "escalate irreversible work only when consequential".
203
230
 
204
- **Honest about the comparison:** ADR-Bench is ~5× larger, drawn from real enterprise telemetry, and far more imbalanced (6:1 benign vs 2.2:1 here). Detection coverage is their strength; verified recoverability is this one's. Complementary instruments, not competing ones.
231
+ ### Importing real benign traffic
232
+
233
+ Hand-authored benign scenarios have a structural blind spot: they contain the false positives I thought to look for. So the corpus can import benign tasks from a [RAS-Eval](https://github.com/lanzer-tree/RAS-Eval) clone — traffic real models actually produced, with real arguments.
234
+
235
+ ```bash
236
+ git clone https://github.com/lanzer-tree/RAS-Eval /path/to/RAS-Eval
237
+ RAS_EVAL_PATH=/path/to/RAS-Eval revoco bench --external
238
+ ```
239
+
240
+ That takes the corpus to **121 scenarios at 5.7:1 benign-to-malicious** — essentially ADR-Bench's ratio — while holding 0% false positives.
241
+
242
+ **Nothing is vendored.** RAS-Eval declares no license, so its data is all-rights-reserved by default; the loader reads a clone you fetch yourself and returns nothing when it's absent, so CI never depends on it.
243
+
244
+ **It found two real bugs on its first run**, at a 16.2% false-positive rate the hand-authored corpus could not have surfaced — because I wrote both the spec and the scenario, and used my own invented argument names in each. `insert_data` takes `db_path`, not the `table` I inferred from the tool's name. `convert_file_to_markdown` takes a `save_path` argument rather than returning `output_path`. In both cases the inverse could never resolve, so every legitimate call raised a phantom rollback. **The same mistake in an SAP adapter would look identical and cost considerably more.**
245
+
246
+ Two disciplines keep the import honest: a tool this package hasn't classified is **skipped**, not imported as `UNKNOWN`, and a call with no observed arguments is **dropped** rather than imported — both would manufacture findings out of missing metadata rather than out of anything the control plane did. And only the 80 *unique tasks* are imported, not all 640 traces: eight models ran the same tasks, so taking every run would multiply the count with near-duplicates. Padding is the thing this corpus exists not to do.
247
+
248
+ **Honest about the comparison:** ADR-Bench's 302 tasks come from real enterprise telemetry across 133 MCP servers. The imported traffic here is consumer-domain — alarms, calendars, disk stats, arXiv lookups — so it broadens the benign distribution without reaching the enterprise write surfaces where the money is. Detection coverage is their strength; verified recoverability is this one's. Complementary instruments.
205
249
 
206
250
  One gap is left visible rather than tuned away: `T09` irreversible fan-out. `PRA01` is a threshold detector, so four one-way wires land before the pattern is visible. The controlled pair `M10`/`M18` measures detection versus the budget on the identical attack, and both stay in the corpus so the difference is attributable.
207
251
 
@@ -281,7 +325,7 @@ This is a **working foundation**, published so it can be read, run, and extended
281
325
  - **Intent-drift detection is lexical overlap.** It flags divergence; it does not establish intent.
282
326
  - **A hash chain does not detect truncation.** Edits, reorders, and interior deletions break verification; dropping the most recent entries leaves a valid prefix. Anchor the head hash externally — `Ledger.checkpoint()` gives you the value to publish.
283
327
  - **In-memory stores are single-process.** `InMemorySessionStore.would_exceed` followed by `commit` is not atomic, so two concurrent calls can both pass a check only one should. A shared store must make that pair atomic.
284
- - **Nothing persists yet.** The ledger, the reversal journal, and the drill register are all in memory, so they reset on restart. Three consequences worth knowing: the horizon forgets undo windows that are still open, `RecoverabilityRegister`'s freshness window means nothing across deploys, and a restart loses the evidence chain rather than breaking it — which is a different failure from tampering and currently indistinguishable from it. Persisting the ledger needs WAL with `synchronous=FULL` and append-only enforced by triggers (SQLite has no `GRANT`), the ledger append and journal write in one transaction, and a startup grace period so a long outage does not mass-degrade every proof to `IRREVERSIBLE` and block legitimate work.
328
+ - **Persistence is opt-in and single-writer.** Pass `store=SqliteStore(path)` and the ledger, journal and drill history survive a restart; omit it and everything is in memory, which is fine for a test and wrong for anything real. The hash chain is single-writer by construction, so two processes sharing one file will collide on sequence numbers — the store raises rather than overwriting, but it does not coordinate. Postgres with advisory locks is the shape for multi-replica; the interface is small enough to swap.
285
329
  - **Control mappings are a self-assessment aid, not a certification** or a legal opinion. NIST AI RMF is voluntary; EU AI Act conformity is assessed against a quality-management system of which logging is one clause.
286
330
 
287
331
  Every place needing production hardening is marked `# HARDENING:` in the source. Search for it before deploying.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "revoco"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  description = "Undo for AI agent actions. Plans the rollback before the action runs, proves it still works, and rolls back a compromised grant's whole blast radius in one call."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -48,11 +48,12 @@ from typing import Any
48
48
 
49
49
  from ..reversal.model import InverseSpec, ReversalGate, Reversibility
50
50
  from ..reversal.registry import InverseRegistry
51
- from . import cloud, database, devops, identity, saas, sap, workday, workstation
51
+ from . import cloud, database, devops, identity, ras_eval, saas, sap, workday, workstation
52
52
  from .cloud import CLOUD_GATES, CLOUD_SPECS, cloud_registry
53
53
  from .database import DATABASE_GATES, DATABASE_SPECS, database_registry
54
54
  from .devops import DEVOPS_GATES, DEVOPS_SPECS, devops_registry
55
55
  from .identity import IDENTITY_GATES, IDENTITY_SPECS, identity_registry
56
+ from .ras_eval import RAS_EVAL_SPECS, ras_eval_registry
56
57
  from .saas import SAAS_GATES, SAAS_SPECS, saas_registry
57
58
  from .sap import SAP_GATES, SAP_SPECS, sap_registry
58
59
  from .workday import WORKDAY_GATES, WORKDAY_SPECS, workday_registry
@@ -149,6 +150,7 @@ def summary(*surfaces: str) -> dict[str, Any]:
149
150
  __all__ = [
150
151
  # modules
151
152
  "sap", "workday", "cloud", "identity", "devops", "saas", "workstation", "database",
153
+ "ras_eval", "RAS_EVAL_SPECS", "ras_eval_registry",
152
154
  # per-surface
153
155
  "SAP_SPECS", "SAP_GATES", "sap_registry",
154
156
  "WORKDAY_SPECS", "WORKDAY_GATES", "workday_registry",
@@ -0,0 +1,187 @@
1
+ """
2
+ revoco.adapters.ras_eval
3
+ ========================
4
+ Reversibility classifications for the 29 tools used by the RAS-Eval benchmark.
5
+
6
+ Why this exists
7
+ ---------------
8
+ The containment corpus' benign half is hand-authored, and hand-authored benign
9
+ traffic has a specific blind spot: it contains the false positives I thought to
10
+ look for. RAS-Eval (`arXiv 2506.15253 <https://arxiv.org/abs/2506.15253>`_) has 80
11
+ benign tasks that real models actually executed, with real arguments. That is the
12
+ one thing authorship cannot produce.
13
+
14
+ Its tools are not this package's tools, so the traces are unusable until something
15
+ says what each of them does to the world. That is what this module is — **my
16
+ classifications of their tool surface**, written from the tool names and the
17
+ observed call arguments.
18
+
19
+ No data from RAS-Eval is vendored here. The repository declares no license, so its
20
+ tasks and logs are all-rights-reserved by default and cannot be redistributed in an
21
+ Apache-2.0 package. :mod:`revoco.bench.external` reads them from a clone you obtain
22
+ yourself, and returns nothing when it is absent.
23
+
24
+ Status: **unvalidated, and unvalidatable from here.** These are someone else's
25
+ tools; I cannot execute them, so no drill can confirm any inverse below. They are
26
+ classified for the purpose of measuring *whether ordinary tool use gets blocked* —
27
+ which only needs the reversal posture to be approximately right — and should not be
28
+ used to govern anything.
29
+
30
+ What this surface is good for, and what it is not
31
+ -------------------------------------------------
32
+ Good for: volume and variety of ordinary agent tool use. Thirty distinct tools
33
+ across seven agent personas, argument shapes produced by eight different models.
34
+ That is a false-positive dimension the hand-built corpus does not cover at all.
35
+
36
+ Not good for: enterprise write semantics. There is no ERP posting, no payroll, no
37
+ IAM here — the domains are alarms, calendars, disk stats, weather and arXiv
38
+ lookups. It broadens the benign distribution; it does not deepen it toward the
39
+ surfaces where the money is.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from ..reversal.model import InverseSpec, Reversibility
45
+ from ..reversal.registry import InverseRegistry
46
+
47
+ # Reads. Trivially reversible because they change nothing — which is also why a
48
+ # corpus made only of these would prove very little. Most of RAS-Eval is here.
49
+ _READS = (
50
+ "is_arxiv_identifier",
51
+ "search_identifier",
52
+ "get_disk_info",
53
+ "get_memory_info",
54
+ "get_current_time",
55
+ "get_weather",
56
+ "get_events_from_calendar",
57
+ "search_event",
58
+ "tavily_search",
59
+ "location_encode",
60
+ "get_stock_price",
61
+ "get_cpu_info",
62
+ "list_directory",
63
+ "read_file",
64
+ "get_alarms",
65
+ "web_search",
66
+ )
67
+
68
+ # Paired create/cancel operations. The genuinely interesting part of this surface:
69
+ # each is an exact inverse of the other, which is the shape the reversal model
70
+ # handles best and the shape most real tool surfaces lack.
71
+ _PAIRS: tuple[tuple[str, str, str], ...] = (
72
+ ("add_event_to_calendar", "remove_event_from_calendar", "event_id"),
73
+ ("remove_event_from_calendar", "add_event_to_calendar", "event_id"),
74
+ ("set_alarm", "cancel_alarm", "alarm_id"),
75
+ ("cancel_alarm", "set_alarm", "alarm_id"),
76
+ ("timer", "cancel_timer", "timer_id"),
77
+ )
78
+
79
+
80
+ def _read_specs() -> list[InverseSpec]:
81
+ return [
82
+ InverseSpec(
83
+ tool=t,
84
+ kind=Reversibility.REVERSIBLE,
85
+ inverse_tool="ras.noop",
86
+ notes="Read-only: nothing to undo.",
87
+ )
88
+ for t in _READS
89
+ ]
90
+
91
+
92
+ def _pair_specs() -> list[InverseSpec]:
93
+ return [
94
+ InverseSpec(
95
+ tool=fwd,
96
+ kind=Reversibility.REVERSIBLE,
97
+ inverse_tool=inv,
98
+ arg_map=((key, f"result.{key}"),),
99
+ notes=(
100
+ f"Exact inverse pair with {inv}. The identifier comes from the "
101
+ "response, so the undo path only completes after the forward call "
102
+ "returns — which is precisely the deferred-argument case the plan "
103
+ "model exists to handle."
104
+ ),
105
+ )
106
+ for fwd, inv, key in _PAIRS
107
+ ]
108
+
109
+
110
+ RAS_EVAL_SPECS: list[InverseSpec] = [
111
+ *_read_specs(),
112
+ *_pair_specs(),
113
+ # Writes with an approximate inverse.
114
+ InverseSpec(
115
+ tool="insert_data",
116
+ kind=Reversibility.COMPENSABLE,
117
+ inverse_tool="delete_data",
118
+ # `db_path` and `items`, not `table`. The first version of this spec invented
119
+ # `table` from the tool's name, and the imported traces caught it: an argument
120
+ # that is never passed cannot resolve, so every real insert raised a phantom
121
+ # rollback. The same mistake in an enterprise adapter would look identical.
122
+ arg_map=(("db_path", "args.db_path"), ("row_id", "result.row_id")),
123
+ residue=(
124
+ "Deleting the row removes the data, not its trace: auto-increment "
125
+ "sequences have advanced, and anything that read the row while it existed "
126
+ "acted on it."
127
+ ),
128
+ ),
129
+ InverseSpec(
130
+ tool="download_file",
131
+ kind=Reversibility.COMPENSABLE,
132
+ inverse_tool="delete_file",
133
+ arg_map=(("path", "args.path"),),
134
+ residue=(
135
+ "The file is removed but the transfer happened: bandwidth was consumed and "
136
+ "the remote server logged the request."
137
+ ),
138
+ ),
139
+ InverseSpec(
140
+ tool="convert_file_to_markdown",
141
+ kind=Reversibility.COMPENSABLE,
142
+ inverse_tool="delete_file",
143
+ # The destination is `save_path` in the request, not `output_path` in the
144
+ # response — also found by importing real calls rather than by reading.
145
+ arg_map=(("path", "args.save_path"),),
146
+ residue="The converted output is removed; the source is untouched.",
147
+ ),
148
+ InverseSpec(
149
+ tool="write_file",
150
+ kind=Reversibility.COMPENSABLE,
151
+ inverse_tool="write_file",
152
+ arg_map=(("path", "args.path"), ("content", "snapshot.content")),
153
+ snapshot_fields=("content",),
154
+ residue=(
155
+ "Prior content is restored where it was captured. If the file did not "
156
+ "exist before, this leaves an empty file rather than no file."
157
+ ),
158
+ ),
159
+ # Genuinely one-way, and worth registering so they escalate rather than
160
+ # falling into UNKNOWN by accident.
161
+ InverseSpec(
162
+ tool="send_email",
163
+ kind=Reversibility.IRREVERSIBLE,
164
+ notes="Delivered mail cannot be recalled.",
165
+ ),
166
+ InverseSpec(
167
+ tool="execute_shell_command",
168
+ kind=Reversibility.UNKNOWN,
169
+ notes=(
170
+ "Same reasoning as revoco's own shell.exec: an arbitrary command's effects "
171
+ "cannot be known in advance, so the honest classification is UNKNOWN and "
172
+ "the honest outcome is escalation."
173
+ ),
174
+ ),
175
+ ]
176
+
177
+
178
+ def ras_eval_registry() -> InverseRegistry:
179
+ """Classifications for the RAS-Eval tool surface (unvalidated — see module docs)."""
180
+ return InverseRegistry(list(RAS_EVAL_SPECS))
181
+
182
+
183
+ def classified_tools() -> set[str]:
184
+ return {s.tool for s in RAS_EVAL_SPECS}
185
+
186
+
187
+ __all__ = ["RAS_EVAL_SPECS", "ras_eval_registry", "classified_tools"]
@@ -32,6 +32,9 @@ Usage::
32
32
  """
33
33
 
34
34
  from .corpus import TECHNIQUES, all_scenarios, benign, by_technique, malicious
35
+ from .external import available as external_available
36
+ from .external import provenance as external_provenance
37
+ from .external import ras_eval_scenarios
35
38
  from .harness import DEFAULT_POLICY, Harness, default_policy
36
39
  from .report import Metrics, render, score, to_dict
37
40
  from .scenario import (
@@ -56,6 +59,10 @@ __all__ = [
56
59
  "malicious",
57
60
  "benign",
58
61
  "by_technique",
62
+ # external corpora (opt-in, nothing vendored)
63
+ "ras_eval_scenarios",
64
+ "external_available",
65
+ "external_provenance",
59
66
  # model
60
67
  "Scenario",
61
68
  "Step",