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.
- {revoco-0.2.2 → revoco-0.3.0}/PKG-INFO +47 -3
- {revoco-0.2.2 → revoco-0.3.0}/README.md +46 -2
- {revoco-0.2.2 → revoco-0.3.0}/pyproject.toml +1 -1
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/__init__.py +3 -1
- revoco-0.3.0/src/revoco/adapters/ras_eval.py +187 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/__init__.py +7 -0
- revoco-0.3.0/src/revoco/bench/external.py +325 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/cli.py +12 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/controlplane.py +40 -6
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/drills.py +23 -1
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/ledger.py +34 -19
- revoco-0.3.0/src/revoco/store/__init__.py +15 -0
- revoco-0.3.0/src/revoco/store/sqlite.py +448 -0
- revoco-0.3.0/tests/test_external_corpus.py +178 -0
- revoco-0.3.0/tests/test_store.py +361 -0
- {revoco-0.2.2 → revoco-0.3.0}/.github/workflows/ci.yml +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/.github/workflows/release.yml +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/.gitignore +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/LICENSE +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/docs/ADAPTERS.md +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/docs/RELEASING.md +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/inverses.yaml +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_cloud.json +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_database.json +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_devops.json +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_identity.json +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_saas.json +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_sap.json +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_workday.json +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/inverses_workstation.json +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/examples/policy.yaml +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/scripts/bump_version.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/scripts/validate_workstation.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/__init__.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/cloud.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/database.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/devops.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/identity.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/saas.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/sap.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/workday.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/adapters/workstation.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/__init__.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/action.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/delegation.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/engine.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/principals.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/revocation.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/authority/scope.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/corpus.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/harness.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/report.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/scenario.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/bench/world.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/core/__init__.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/core/crypto.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/core/errors.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/core/ids.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/demo.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/detect.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/evidence.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/__init__.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/conditions.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/decision.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/engine.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/policy.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/session.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/gate/threats.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/__init__.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/budget.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/engine.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/horizon.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/model.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/src/revoco/reversal/registry.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_adapter_catalog.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_adapters.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_authority.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_bench.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_budget_and_drills.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_controlplane.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_core.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_demo_and_cli.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_gate.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_horizon_and_scheduling.py +0 -0
- {revoco-0.2.2 → revoco-0.3.0}/tests/test_ledger.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
-
- **
|
|
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.
|
|
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",
|