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 +224 -0
- interlock/adjudication.py +109 -0
- interlock/anchor.py +663 -0
- interlock/builder.py +246 -0
- interlock/cascade.py +545 -0
- interlock/chain.py +640 -0
- interlock/cli.py +206 -0
- interlock/config.py +134 -0
- interlock/engine.py +1234 -0
- interlock/exceptions.py +280 -0
- interlock/extension.py +674 -0
- interlock/feedback.py +574 -0
- interlock/invariants.py +569 -0
- interlock/postgres.py +1173 -0
- interlock/py.typed +0 -0
- interlock/receipts.py +270 -0
- interlock/reconcile.py +392 -0
- interlock/records.py +544 -0
- interlock/recovery.py +1350 -0
- interlock/repair.py +441 -0
- interlock/runtime.py +284 -0
- interlock/substrate.py +1042 -0
- interlock/types.py +573 -0
- interlock_escrow-0.3.0.dist-info/METADATA +1127 -0
- interlock_escrow-0.3.0.dist-info/RECORD +27 -0
- interlock_escrow-0.3.0.dist-info/WHEEL +4 -0
- interlock_escrow-0.3.0.dist-info/entry_points.txt +2 -0
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))
|