revoco 0.1.1__py3-none-any.whl

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 (51) hide show
  1. revoco/__init__.py +183 -0
  2. revoco/adapters/__init__.py +163 -0
  3. revoco/adapters/cloud.py +362 -0
  4. revoco/adapters/database.py +217 -0
  5. revoco/adapters/devops.py +362 -0
  6. revoco/adapters/identity.py +256 -0
  7. revoco/adapters/saas.py +237 -0
  8. revoco/adapters/sap.py +358 -0
  9. revoco/adapters/workday.py +291 -0
  10. revoco/adapters/workstation.py +305 -0
  11. revoco/authority/__init__.py +31 -0
  12. revoco/authority/action.py +151 -0
  13. revoco/authority/delegation.py +133 -0
  14. revoco/authority/engine.py +403 -0
  15. revoco/authority/principals.py +167 -0
  16. revoco/authority/revocation.py +101 -0
  17. revoco/authority/scope.py +227 -0
  18. revoco/bench/__init__.py +77 -0
  19. revoco/bench/corpus.py +1168 -0
  20. revoco/bench/harness.py +352 -0
  21. revoco/bench/report.py +343 -0
  22. revoco/bench/scenario.py +295 -0
  23. revoco/bench/world.py +351 -0
  24. revoco/cli.py +299 -0
  25. revoco/controlplane.py +778 -0
  26. revoco/core/__init__.py +42 -0
  27. revoco/core/crypto.py +146 -0
  28. revoco/core/errors.py +90 -0
  29. revoco/core/ids.py +31 -0
  30. revoco/demo.py +376 -0
  31. revoco/detect.py +552 -0
  32. revoco/drills.py +586 -0
  33. revoco/evidence.py +362 -0
  34. revoco/gate/__init__.py +36 -0
  35. revoco/gate/conditions.py +192 -0
  36. revoco/gate/decision.py +67 -0
  37. revoco/gate/engine.py +222 -0
  38. revoco/gate/policy.py +326 -0
  39. revoco/gate/session.py +71 -0
  40. revoco/gate/threats.py +175 -0
  41. revoco/ledger.py +275 -0
  42. revoco/reversal/__init__.py +62 -0
  43. revoco/reversal/budget.py +310 -0
  44. revoco/reversal/engine.py +768 -0
  45. revoco/reversal/model.py +938 -0
  46. revoco/reversal/registry.py +224 -0
  47. revoco-0.1.1.dist-info/METADATA +302 -0
  48. revoco-0.1.1.dist-info/RECORD +51 -0
  49. revoco-0.1.1.dist-info/WHEEL +4 -0
  50. revoco-0.1.1.dist-info/entry_points.txt +2 -0
  51. revoco-0.1.1.dist-info/licenses/LICENSE +201 -0
revoco/__init__.py ADDED
@@ -0,0 +1,183 @@
1
+ """
2
+ Revoco — an action control plane for AI agents.
3
+
4
+ Four questions, one pipeline, one verifiable record:
5
+
6
+ 1. **Authority** — is there valid authority for this, traceable to a human?
7
+ 2. **Enforcement** — does policy permit this specific action, with these
8
+ arguments, under this session's accumulated spend?
9
+ 3. **Reversibility** — if this turns out to be wrong, can we take it back?
10
+ 4. **Evidence** — can we prove all of the above to someone who does not trust us?
11
+
12
+ Question 3 is the one most agent-governance tooling leaves out, and it is the one
13
+ that decides whether an incident costs an afternoon or a quarter.
14
+
15
+ Quick start::
16
+
17
+ from revoco import ControlPlane, Scope, crypto
18
+ from revoco.reversal import ap_starter_registry
19
+
20
+ cp = ControlPlane(inverse_registry=ap_starter_registry())
21
+
22
+ h_priv, h_pub = crypto.generate_keypair()
23
+ a_priv, a_pub = crypto.generate_keypair()
24
+ cfo = cp.register_human("Alice (CFO)", h_pub)
25
+ bot = cp.register_agent("ap-bot", a_pub, roles={"ap-clerk"})
26
+
27
+ grant = cp.issue_root_delegation(
28
+ human_private_key=h_priv, human_id=cfo.id, agent_id=bot.id,
29
+ scope=Scope.make(tools={"invoices.pay"}, actions={"write"}, max_risk=60),
30
+ purpose="pay approved invoices", ttl_seconds=3600,
31
+ )
32
+
33
+ v = cp.authorize(
34
+ actor_private_key=a_priv, actor_id=bot.id, delegation_id=grant.id,
35
+ tool="invoices.pay", args={"invoice_id": "INV-1", "amount": 900},
36
+ risk=50, description="pay approved invoice INV-1",
37
+ )
38
+ if v.allowed:
39
+ result = pay(...) # your code does the real work
40
+ cp.confirm(v, result=result)
41
+
42
+ cp.undo(v.action_id, my_executor) # one action
43
+ cp.contain(grant.id, my_executor) # revoke the grant, roll back its subtree
44
+
45
+ This package merges three earlier tools — ``veritrail`` (provenance and signed
46
+ delegation), ``mcp-gate`` (per-call policy enforcement), and ``mnemosyne``
47
+ (memory/context integrity) — and adds the reversal layer that none of them had.
48
+ """
49
+
50
+ from .authority import (
51
+ ActionRecord,
52
+ AuthorityEngine,
53
+ ChainResult,
54
+ Delegation,
55
+ Principal,
56
+ PrincipalKind,
57
+ Scope,
58
+ )
59
+ from .controlplane import ControlPlane, Verdict
60
+ from .core import crypto, ids
61
+ from .core.errors import RevocoError
62
+ from .detect import DetectionEngine, Finding, Severity, journal_health
63
+ from .drills import (
64
+ Canary,
65
+ DrillOutcome,
66
+ DrillResult,
67
+ DrillRunner,
68
+ RecoverabilityAttestation,
69
+ RecoverabilityRegister,
70
+ attest,
71
+ )
72
+ from .evidence import EvidencePack, build_evidence_pack, readiness_report
73
+ from .gate import Decision, Effect, Policy, PolicyEngine, load_policy, starter_policy
74
+ from .ledger import Ledger, LedgerEntry
75
+ from .reversal import (
76
+ CascadeReport,
77
+ InverseRegistry,
78
+ InverseSpec,
79
+ InverseStep,
80
+ JournalEntry,
81
+ JournalState,
82
+ ReversalEngine,
83
+ ReversalGate,
84
+ ReversalPlan,
85
+ ReversalReceipt,
86
+ Reversibility,
87
+ ap_starter_registry,
88
+ )
89
+ from .reversal.budget import IrreversibilityBudget
90
+
91
+
92
+ def _detect_version() -> str:
93
+ """Read the version from installed package metadata.
94
+
95
+ ``pyproject.toml`` is the single source of truth, so a released wheel and
96
+ ``revoco.__version__`` can never disagree — the release workflow bumps one
97
+ number in one file and this follows automatically.
98
+
99
+ Resolved through ``packages_distributions()`` rather than a hardcoded
100
+ distribution name. Import name and distribution name happen to match today, and
101
+ this keeps working if they ever stop matching — a hardcoded name would silently
102
+ return the fallback instead of raising, which is the worst way to be wrong about
103
+ a version number.
104
+ """
105
+ try:
106
+ from importlib.metadata import (
107
+ PackageNotFoundError,
108
+ packages_distributions,
109
+ version,
110
+ )
111
+
112
+ for dist in packages_distributions().get("revoco", []):
113
+ try:
114
+ return version(dist)
115
+ except PackageNotFoundError:
116
+ continue
117
+ except Exception:
118
+ pass
119
+ # Running from a source checkout with nothing installed.
120
+ return "0.0.0+local"
121
+
122
+
123
+ __version__ = _detect_version()
124
+
125
+ __all__ = [
126
+ "__version__",
127
+ # control plane
128
+ "ControlPlane",
129
+ # unrecoverable-exposure ceiling
130
+ "IrreversibilityBudget",
131
+ # recovery drills: reversibility as a claim that expires
132
+ "DrillRunner",
133
+ "Canary",
134
+ "DrillOutcome",
135
+ "DrillResult",
136
+ "RecoverabilityRegister",
137
+ "RecoverabilityAttestation",
138
+ "attest",
139
+ "Verdict",
140
+ # authority
141
+ "Scope",
142
+ "Principal",
143
+ "PrincipalKind",
144
+ "Delegation",
145
+ "ActionRecord",
146
+ "AuthorityEngine",
147
+ "ChainResult",
148
+ # enforcement
149
+ "Policy",
150
+ "PolicyEngine",
151
+ "Decision",
152
+ "Effect",
153
+ "load_policy",
154
+ "starter_policy",
155
+ # reversal
156
+ "Reversibility",
157
+ "InverseSpec",
158
+ "InverseStep",
159
+ "ReversalGate",
160
+ "InverseRegistry",
161
+ "ap_starter_registry",
162
+ "ReversalEngine",
163
+ "ReversalPlan",
164
+ "ReversalReceipt",
165
+ "JournalEntry",
166
+ "JournalState",
167
+ "CascadeReport",
168
+ # detection
169
+ "DetectionEngine",
170
+ "Finding",
171
+ "Severity",
172
+ "journal_health",
173
+ # evidence
174
+ "EvidencePack",
175
+ "build_evidence_pack",
176
+ "readiness_report",
177
+ # infrastructure
178
+ "Ledger",
179
+ "LedgerEntry",
180
+ "crypto",
181
+ "ids",
182
+ "RevocoError",
183
+ ]
@@ -0,0 +1,163 @@
1
+ """
2
+ System-of-record adapters: the inverse-operation specs that do not generalize.
3
+
4
+ Everything else in revoco is transferable across customers. This package is not:
5
+ knowing that an SAP payment reversal is a three-step sequence, that a Workday
6
+ rescind dies the moment payroll runs, or that an S3 delete is recoverable only if
7
+ someone enabled versioning first, is per-system knowledge that has to be built once
8
+ and maintained forever.
9
+
10
+ Seven surfaces are covered:
11
+
12
+ ============ =============================================================
13
+ ``sap`` S/4HANA financial postings, supplier master, payments
14
+ ``workday`` HCM business processes — compensation, staffing, payroll
15
+ ``cloud`` AWS — S3, IAM, EC2/networking, RDS, Route 53, KMS
16
+ ``identity`` Microsoft Entra ID and Okta
17
+ ``devops`` GitHub refs and protection, Kubernetes, feature flags
18
+ ``saas`` Salesforce records, Slack messages, Stripe payments
19
+ ``workstation`` filesystem, git, shell — the surface coding agents touch
20
+ ``database`` row writes, arbitrary SQL, schema migrations
21
+ ============ =============================================================
22
+
23
+ Every spec is **unvalidated** — written from documentation and practitioner
24
+ sources, not executed against a live system. See ``docs/ADAPTERS.md`` for
25
+ per-spec citations and the validation checklist to work through before any of it
26
+ governs a real write.
27
+
28
+ The two things this collection taught the core model
29
+ ----------------------------------------------------
30
+ **Reversibility is a property of the target, not the tool.** ``aws.s3.delete_object``
31
+ is recoverable against a versioned bucket and final against an unversioned one, with
32
+ identical arguments. ``entra.group.delete`` soft-deletes a security group and hard-
33
+ deletes a distribution group. That is what authorize-phase gates exist for: they are
34
+ evaluated *before* the write, and a closed gate degrades the classification so policy
35
+ escalates while escalation still means something.
36
+
37
+ **Snapshot-before-write creates undo paths that do not otherwise exist.** A deleted
38
+ Kubernetes object, a deleted git branch, a force-pushed ref, an overwritten file, a
39
+ revoked security-group rule — none of these has a native undo, and all of them become
40
+ recoverable once prior state is captured. Roughly a third of the specs here are
41
+ recoverable *only* because of that ordering. It is the clearest statement of what this
42
+ architecture buys.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ from typing import Any
48
+
49
+ from ..reversal.model import InverseSpec, ReversalGate, Reversibility
50
+ from ..reversal.registry import InverseRegistry
51
+ from . import cloud, database, devops, identity, saas, sap, workday, workstation
52
+ from .cloud import CLOUD_GATES, CLOUD_SPECS, cloud_registry
53
+ from .database import DATABASE_GATES, DATABASE_SPECS, database_registry
54
+ from .devops import DEVOPS_GATES, DEVOPS_SPECS, devops_registry
55
+ from .identity import IDENTITY_GATES, IDENTITY_SPECS, identity_registry
56
+ from .saas import SAAS_GATES, SAAS_SPECS, saas_registry
57
+ from .sap import SAP_GATES, SAP_SPECS, sap_registry
58
+ from .workday import WORKDAY_GATES, WORKDAY_SPECS, workday_registry
59
+ from .workstation import WORKSTATION_GATES, WORKSTATION_SPECS, workstation_registry
60
+
61
+ # Surface name -> (specs, gates). Ordered roughly by blast radius.
62
+ SURFACES: dict[str, tuple[list[InverseSpec], tuple[ReversalGate, ...]]] = {
63
+ "sap": (SAP_SPECS, SAP_GATES),
64
+ "workday": (WORKDAY_SPECS, WORKDAY_GATES),
65
+ "cloud": (CLOUD_SPECS, CLOUD_GATES),
66
+ "identity": (IDENTITY_SPECS, IDENTITY_GATES),
67
+ "devops": (DEVOPS_SPECS, DEVOPS_GATES),
68
+ "database": (DATABASE_SPECS, DATABASE_GATES),
69
+ "saas": (SAAS_SPECS, SAAS_GATES),
70
+ "workstation": (WORKSTATION_SPECS, WORKSTATION_GATES),
71
+ }
72
+
73
+
74
+ def all_specs(*surfaces: str) -> list[InverseSpec]:
75
+ """Every spec across the named surfaces, or all of them if none are named."""
76
+ names = surfaces or tuple(SURFACES)
77
+ unknown = [n for n in names if n not in SURFACES]
78
+ if unknown:
79
+ raise KeyError(f"unknown surface(s) {unknown}; available: {sorted(SURFACES)}")
80
+ out: list[InverseSpec] = []
81
+ for name in names:
82
+ out.extend(SURFACES[name][0])
83
+ return out
84
+
85
+
86
+ def registry_for(*surfaces: str) -> InverseRegistry:
87
+ """A combined registry for the named surfaces (all of them if none named).
88
+
89
+ Tool names are namespaced per surface, so combining is safe. Prefer loading
90
+ only the surfaces you actually govern: a registry claiming to classify SAP
91
+ postings in a shop with no SAP is noise in every coverage report.
92
+ """
93
+ return InverseRegistry(all_specs(*surfaces))
94
+
95
+
96
+ def gate_catalog(*surfaces: str) -> dict[str, dict[str, Any]]:
97
+ """Every gate across the named surfaces — the integrator's to-do list.
98
+
99
+ Each entry your ``GateEvaluator`` does not handle fails closed, so this is the
100
+ definitive list of questions you must be able to answer before the
101
+ corresponding specs can execute. ``phase`` matters: ``authorize`` gates change
102
+ whether an action is classified as undoable at all and are asked *before* the
103
+ write, while ``undo`` gates are asked immediately before a rollback runs.
104
+ """
105
+ names = surfaces or tuple(SURFACES)
106
+ out: dict[str, dict[str, Any]] = {}
107
+ for name in names:
108
+ if name not in SURFACES:
109
+ raise KeyError(f"unknown surface {name!r}; available: {sorted(SURFACES)}")
110
+ for gate in SURFACES[name][1]:
111
+ out[gate.name] = {
112
+ "surface": name,
113
+ "phase": gate.check_at,
114
+ "description": gate.description,
115
+ "remediation": gate.remediation,
116
+ }
117
+ return out
118
+
119
+
120
+ def summary(*surfaces: str) -> dict[str, Any]:
121
+ """Counts by reversal posture, plus the two figures worth reporting upward.
122
+
123
+ ``snapshot_dependent`` is how many specs are recoverable only because prior
124
+ state is captured before the write — the share of the undo surface this
125
+ architecture creates rather than merely records.
126
+
127
+ ``degradable`` is how many can turn out to be irreversible for a particular
128
+ target despite an optimistic classification. That gap is why authorize-phase
129
+ gates exist, and it is the number to watch: a spec that degrades and is never
130
+ checked is a phantom rollback waiting to happen.
131
+ """
132
+ specs = all_specs(*surfaces)
133
+ by_kind: dict[str, int] = {k.value: 0 for k in Reversibility}
134
+ for s in specs:
135
+ by_kind[s.kind.value] += 1
136
+ return {
137
+ "surfaces": list(surfaces or tuple(SURFACES)),
138
+ "specs": len(specs),
139
+ "by_kind": by_kind,
140
+ "sequenced": len([s for s in specs if len(s.effective_steps) > 1]),
141
+ "one_shot": len([s for s in specs if s.one_shot]),
142
+ "gated": len([s for s in specs if s.gates]),
143
+ "degradable": len([s for s in specs if s.authorize_gates]),
144
+ "snapshot_dependent": len([s for s in specs if s.snapshot_fields]),
145
+ "gates": len(gate_catalog(*surfaces)),
146
+ }
147
+
148
+
149
+ __all__ = [
150
+ # modules
151
+ "sap", "workday", "cloud", "identity", "devops", "saas", "workstation", "database",
152
+ # per-surface
153
+ "SAP_SPECS", "SAP_GATES", "sap_registry",
154
+ "WORKDAY_SPECS", "WORKDAY_GATES", "workday_registry",
155
+ "CLOUD_SPECS", "CLOUD_GATES", "cloud_registry",
156
+ "IDENTITY_SPECS", "IDENTITY_GATES", "identity_registry",
157
+ "DEVOPS_SPECS", "DEVOPS_GATES", "devops_registry",
158
+ "SAAS_SPECS", "SAAS_GATES", "saas_registry",
159
+ "WORKSTATION_SPECS", "WORKSTATION_GATES", "workstation_registry",
160
+ "DATABASE_SPECS", "DATABASE_GATES", "database_registry",
161
+ # combined
162
+ "SURFACES", "all_specs", "registry_for", "gate_catalog", "summary",
163
+ ]