interlock-escrow 0.3.0__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.
interlock/__init__.py ADDED
@@ -0,0 +1,224 @@
1
+ """Interlock: a reference monitor for agent side effects.
2
+
3
+ An agent proposes a plan. It holds no credentials and cannot execute. Interlock
4
+ applies the plan against a real but uncommitted substrate, measures what
5
+ actually changed, evaluates deterministic predicates against that measurement,
6
+ and only then commits or rolls back.
7
+
8
+ The position exists because a prompt-injected agent is a fully authorized
9
+ agent. It is doing something it has permission to do, so an authorization check
10
+ passes, and the injected instruction arrived inside a tool result after every
11
+ provider-side filter had run, so a provider-side filter does not see it. What
12
+ is left is measuring the consequence before it becomes durable.
13
+
14
+ from interlock import EscrowRuntime, TableSpec
15
+
16
+ runtime = EscrowRuntime(
17
+ "prod.db",
18
+ tables=[TableSpec("orders", columns=["id", "tenant", "total"],
19
+ tenant_column="tenant")],
20
+ scope_id="support-agent",
21
+ )
22
+ result = runtime.execute_sql(
23
+ "UPDATE orders SET total = :total WHERE id = :id",
24
+ {"total": 100.0, "id": 1},
25
+ tenant_id="acme",
26
+ stated_rows=1,
27
+ )
28
+
29
+ if not result.committed:
30
+ print(result.blocked_by, result.diff.blast_radius)
31
+
32
+ ``EscrowRuntime`` owns the substrate, chain, anchor and engine. Drop to
33
+ :class:`EscrowEngine` and :class:`PlanBuilder` when you need the primitives;
34
+ the runtime is sugar over them and hands both back.
35
+
36
+ Every adjudication is written to a hash-linked chain, anchored to an AgentGov
37
+ ledger when one is attached. See :mod:`interlock.anchor` for the seam, which is
38
+ read-only by default.
39
+
40
+ Two substrates, SQLite and PostgreSQL, and a measurement scoped to the tables
41
+ each was configured to observe. ``docs/ESCROW_SPEC.md`` has a conformance
42
+ section listing what is implemented, partial and unimplemented; read it before
43
+ putting this in front of a production database.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ from interlock.anchor import AnchorPoint, LedgerAnchor
49
+ from interlock.builder import PlanBuilder, new_effect_id, new_plan_id
50
+ from interlock.cascade import CascadeReport
51
+ from interlock.chain import EscrowChain, EscrowRecord, RecordType
52
+ from interlock.engine import EscrowEngine, StageResult
53
+ from interlock.exceptions import (
54
+ AdmissionError,
55
+ AnchorError,
56
+ ChainIntegrityError,
57
+ ChainInUseError,
58
+ CommitUnsettledError,
59
+ CyclicPlanError,
60
+ ExtensionError,
61
+ ForbiddenStatementError,
62
+ InterlockError,
63
+ LedgerUnverifiedError,
64
+ PlanError,
65
+ RecordIntegrityError,
66
+ RecoveryError,
67
+ RecoveryExhaustedError,
68
+ ScopeHaltedError,
69
+ StageConflictError,
70
+ StageError,
71
+ StageExpiredError,
72
+ SubstrateConfigurationError,
73
+ SubstrateUnavailableError,
74
+ ToolRevokedError,
75
+ UncompensatableEffectError,
76
+ )
77
+ from interlock.extension import (
78
+ BudgetGuard,
79
+ Estimate,
80
+ ExtensionGrant,
81
+ ExtensionRequest,
82
+ Milestones,
83
+ Progress,
84
+ Spend,
85
+ )
86
+ from interlock.feedback import AgentFeedback, ConstraintFeedback, OperatorEvidence, Refusal
87
+ from interlock.invariants import (
88
+ BlastRadius,
89
+ ColumnValueGuard,
90
+ InvariantChecker,
91
+ NoDelete,
92
+ NoSchemaChange,
93
+ StatedFootprint,
94
+ TableAllowlist,
95
+ TenantDrawdownGuard,
96
+ TenantIsolation,
97
+ TruncationGuard,
98
+ default_checkers,
99
+ )
100
+ from interlock.postgres import PostgresSubstrate
101
+ from interlock.receipts import ReceiptIssuer
102
+ from interlock.records import RecordKind, RecordLog, SignedRecord, check_anchors, verify_records
103
+ from interlock.recovery import (
104
+ Channel,
105
+ Directive,
106
+ RecoveryPolicy,
107
+ RecoveryRuntime,
108
+ RecoveryStep,
109
+ Rung,
110
+ Trip,
111
+ TripKind,
112
+ )
113
+ from interlock.repair import DroppedEffect, Repair, RepairFeedback
114
+ from interlock.runtime import EscrowRuntime
115
+ from interlock.substrate import ShadowSubstrate, SqliteSubstrate, TableSpec
116
+ from interlock.types import (
117
+ Compensation,
118
+ Effect,
119
+ EffectDiff,
120
+ EffectId,
121
+ EffectKind,
122
+ EffectPlan,
123
+ InvariantViolation,
124
+ PlanId,
125
+ RowDelta,
126
+ Severity,
127
+ StageState,
128
+ Verdict,
129
+ )
130
+
131
+ __version__ = "0.2.1"
132
+
133
+ __all__ = [
134
+ "AdmissionError",
135
+ "AgentFeedback",
136
+ "AnchorError",
137
+ "AnchorPoint",
138
+ "BlastRadius",
139
+ "BudgetGuard",
140
+ "CascadeReport",
141
+ "ChainInUseError",
142
+ "ChainIntegrityError",
143
+ "Channel",
144
+ "ColumnValueGuard",
145
+ "CommitUnsettledError",
146
+ "Compensation",
147
+ "ConstraintFeedback",
148
+ "CyclicPlanError",
149
+ "Directive",
150
+ "DroppedEffect",
151
+ "Effect",
152
+ "EffectDiff",
153
+ "EffectId",
154
+ "EffectKind",
155
+ "EffectPlan",
156
+ "EscrowChain",
157
+ "EscrowEngine",
158
+ "EscrowRecord",
159
+ "EscrowRuntime",
160
+ "Estimate",
161
+ "ExtensionError",
162
+ "ExtensionGrant",
163
+ "ExtensionRequest",
164
+ "ForbiddenStatementError",
165
+ "InterlockError",
166
+ "InvariantChecker",
167
+ "InvariantViolation",
168
+ "LedgerAnchor",
169
+ "LedgerUnverifiedError",
170
+ "Milestones",
171
+ "NoDelete",
172
+ "NoSchemaChange",
173
+ "OperatorEvidence",
174
+ "PlanBuilder",
175
+ "PlanError",
176
+ "PlanId",
177
+ "PostgresSubstrate",
178
+ "Progress",
179
+ "ReceiptIssuer",
180
+ "RecordIntegrityError",
181
+ "RecordKind",
182
+ "RecordLog",
183
+ "RecordType",
184
+ "RecoveryError",
185
+ "RecoveryExhaustedError",
186
+ "RecoveryPolicy",
187
+ "RecoveryRuntime",
188
+ "RecoveryStep",
189
+ "Refusal",
190
+ "Repair",
191
+ "RepairFeedback",
192
+ "RowDelta",
193
+ "Rung",
194
+ "ScopeHaltedError",
195
+ "Severity",
196
+ "ShadowSubstrate",
197
+ "SignedRecord",
198
+ "Spend",
199
+ "SqliteSubstrate",
200
+ "StageConflictError",
201
+ "StageError",
202
+ "StageExpiredError",
203
+ "StageResult",
204
+ "StageState",
205
+ "StatedFootprint",
206
+ "SubstrateConfigurationError",
207
+ "SubstrateUnavailableError",
208
+ "TableAllowlist",
209
+ "TableSpec",
210
+ "TenantDrawdownGuard",
211
+ "TenantIsolation",
212
+ "ToolRevokedError",
213
+ "Trip",
214
+ "TripKind",
215
+ "TruncationGuard",
216
+ "UncompensatableEffectError",
217
+ "Verdict",
218
+ "__version__",
219
+ "check_anchors",
220
+ "default_checkers",
221
+ "new_effect_id",
222
+ "new_plan_id",
223
+ "verify_records",
224
+ ]
@@ -0,0 +1,109 @@
1
+ """Running the checkers: one verdict, and a feedback hint for each violation.
2
+
3
+ Split out of the engine because two paths adjudicate: :meth:`EscrowEngine.execute`
4
+ for a submitted plan, and :meth:`EscrowEngine.repair` for every candidate
5
+ sub-plan it tries. Both have to decide the same way.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import uuid
12
+ from collections.abc import Sequence
13
+ from dataclasses import dataclass
14
+ from datetime import UTC, datetime
15
+
16
+ from interlock.feedback import (
17
+ AgentFeedback,
18
+ FeedbackHint,
19
+ Guidance,
20
+ HintedViolation,
21
+ OperatorEvidence,
22
+ Refusal,
23
+ agent_feedback,
24
+ )
25
+ from interlock.invariants import BUILT_IN_CHECKERS, InvariantChecker
26
+ from interlock.types import EffectDiff, EffectPlan, InvariantViolation, Severity, Verdict
27
+
28
+ __all__ = ["Adjudication", "adjudicate"]
29
+
30
+ logger = logging.getLogger("interlock.engine")
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class Adjudication:
35
+ """A verdict, with what each of its violations may tell the agent."""
36
+
37
+ plan: EffectPlan
38
+ diff: EffectDiff
39
+ verdict: Verdict
40
+ hints: tuple[HintedViolation, ...]
41
+
42
+ @property
43
+ def admitted(self) -> bool:
44
+ return self.verdict.admitted
45
+
46
+ def feedback(self, *, committed: bool) -> AgentFeedback:
47
+ return agent_feedback(self.plan, self.hints, committed=committed)
48
+
49
+ def refusal(self) -> Refusal:
50
+ return Refusal(
51
+ evidence=OperatorEvidence.of(self.plan, diff=self.diff, verdict=self.verdict),
52
+ feedback=self.feedback(committed=False),
53
+ )
54
+
55
+
56
+ def adjudicate(
57
+ plan: EffectPlan,
58
+ diff: EffectDiff,
59
+ checkers: Sequence[InvariantChecker],
60
+ *,
61
+ stage_id: uuid.UUID,
62
+ ) -> Adjudication:
63
+ """Run every checker and union the violations.
64
+
65
+ A checker that raises becomes a blocking violation: a predicate that did
66
+ not finish has not approved anything. A hint that raises, or is not a
67
+ :class:`FeedbackHint`, becomes the generic operator-constraint hint: the
68
+ agent loses detail, never the refusal.
69
+ """
70
+ violations: list[InvariantViolation] = []
71
+ hinted: list[HintedViolation] = []
72
+ names: list[str] = []
73
+ for checker in checkers:
74
+ names.append(checker.name)
75
+ try:
76
+ found = tuple(checker.check(plan, diff))
77
+ except Exception as exc: # a broken checker must never approve
78
+ logger.exception("invariant %s raised", checker.name)
79
+ broken = InvariantViolation(
80
+ invariant=checker.name,
81
+ severity=Severity.BLOCKING,
82
+ message=f"checker raised {type(exc).__name__}: {exc}",
83
+ )
84
+ violations.append(broken)
85
+ hinted.append(HintedViolation(broken, FeedbackHint(kind=Guidance.OPERATOR)))
86
+ continue
87
+ violations.extend(found)
88
+ trusted = type(checker) in BUILT_IN_CHECKERS
89
+ offer = getattr(checker, "hint", None)
90
+ for violation in found:
91
+ hint = FeedbackHint(kind=Guidance.OPERATOR)
92
+ if callable(offer):
93
+ try:
94
+ candidate = offer(plan, diff, violation)
95
+ except Exception:
96
+ logger.exception("feedback hint for %s raised", checker.name)
97
+ else:
98
+ if isinstance(candidate, FeedbackHint):
99
+ hint = candidate
100
+ hinted.append(HintedViolation(violation, hint, trusted))
101
+ verdict = Verdict(
102
+ plan_id=plan.plan_id,
103
+ stage_id=stage_id,
104
+ diff_hash=diff.content_hash(),
105
+ decided_at=datetime.now(UTC),
106
+ checkers_run=tuple(names),
107
+ violations=tuple(violations),
108
+ )
109
+ return Adjudication(plan=plan, diff=diff, verdict=verdict, hints=tuple(hinted))