apsimo-hostworker 0.2.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.
- apsimo_hostworker/__init__.py +134 -0
- apsimo_hostworker/_private_io.py +267 -0
- apsimo_hostworker/admission.py +297 -0
- apsimo_hostworker/catalog.py +754 -0
- apsimo_hostworker/client.py +292 -0
- apsimo_hostworker/compat.py +53 -0
- apsimo_hostworker/conformance/__init__.py +52 -0
- apsimo_hostworker/conformance/__main__.py +29 -0
- apsimo_hostworker/conformance/harness.py +278 -0
- apsimo_hostworker/conformance/suite.py +1109 -0
- apsimo_hostworker/contract.py +402 -0
- apsimo_hostworker/gate.py +446 -0
- apsimo_hostworker/intent.py +221 -0
- apsimo_hostworker/sqlite_store.py +1815 -0
- apsimo_hostworker/store.py +446 -0
- apsimo_hostworker/worker.py +643 -0
- apsimo_hostworker-0.2.0.dist-info/METADATA +24 -0
- apsimo_hostworker-0.2.0.dist-info/RECORD +22 -0
- apsimo_hostworker-0.2.0.dist-info/WHEEL +5 -0
- apsimo_hostworker-0.2.0.dist-info/licenses/LICENSE +21 -0
- apsimo_hostworker-0.2.0.dist-info/top_level.txt +2 -0
- colony_hostworker/__init__.py +4 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Approval-gate invariant core and provenance-shape registry.
|
|
2
|
+
|
|
3
|
+
An owner approval gate is a durable receipt whose ``evidence`` mapping must be
|
|
4
|
+
EXACTLY one registered authorization shape — never a subset, superset, or mix.
|
|
5
|
+
Two shapes are built in:
|
|
6
|
+
|
|
7
|
+
* message-delivery provenance — a per-message owner approval bound to one
|
|
8
|
+
delivered approval request (``channel``, ``thread_id``, ``event_id``,
|
|
9
|
+
``event_key``, ``delivery_id``, ``delivery_message_id``,
|
|
10
|
+
``binding_method``);
|
|
11
|
+
* bounded-grant provenance — an owner-issued grant consumed at
|
|
12
|
+
approval time (``bounded_grant_id``, ``approval_source_request_id``,
|
|
13
|
+
``bounded_grant_expires_at_epoch``, ``binding_method`` exactly
|
|
14
|
+
``"bounded_grant"``). The grant expiry is either a finite epoch or the
|
|
15
|
+
explicit literal ``"unlimited"``; every per-action gate expiry stays finite.
|
|
16
|
+
|
|
17
|
+
Invariants enforced here, in one place, for every shape:
|
|
18
|
+
|
|
19
|
+
* exact field-set equality against exactly one registered shape;
|
|
20
|
+
* ``decision == "approved"``, ``authority == "owner"``, receipt
|
|
21
|
+
``status == "passed"``;
|
|
22
|
+
* ``action_id`` equality and ``action_digest`` bound to the action's
|
|
23
|
+
``payload_sha256`` via :func:`hmac.compare_digest`;
|
|
24
|
+
* ``revision`` pinned to exactly 1;
|
|
25
|
+
* receipt ``external_id`` equal to the evidence ``approval_id``;
|
|
26
|
+
* ``evidence_sha256`` recomputed (UTF-8 canonical JSON) and compared;
|
|
27
|
+
* timestamp sanity with the contract's 30-second skew allowance; and
|
|
28
|
+
* EVERY expiry the shape carries (the gate expiry AND any grant expiry)
|
|
29
|
+
re-enforced at point of use — an expired authority never dispatches, no
|
|
30
|
+
matter what was true when the receipt was written.
|
|
31
|
+
|
|
32
|
+
The grant path additionally fails closed for any tool the catalog marks
|
|
33
|
+
``non_grantable`` (and for any tool the catalog does not know), regardless of
|
|
34
|
+
host configuration: no registry, shape, or configuration input to this module
|
|
35
|
+
can authorize ``colony_autonomy_enable`` / ``colony_autonomy_disable`` on a
|
|
36
|
+
standing grant.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import hmac
|
|
42
|
+
import math
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from typing import Any, Iterable, Mapping
|
|
45
|
+
|
|
46
|
+
from .catalog import TOOL_CATALOG, ToolSpec
|
|
47
|
+
from .contract import (
|
|
48
|
+
APPROVAL_ID_RE,
|
|
49
|
+
GATE_CLOCK_SKEW_SECONDS,
|
|
50
|
+
GovernedContractError,
|
|
51
|
+
sha256_json_utf8,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class OwnerGateError(GovernedContractError):
|
|
56
|
+
"""The owner approval gate does not bind this action."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ProvenanceShapeError(GovernedContractError):
|
|
60
|
+
"""A provenance shape registration is invalid or ambiguous."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Fields common to every authorization shape. A shape adds only provenance.
|
|
64
|
+
GATE_COMMON_FIELDS = frozenset(
|
|
65
|
+
{
|
|
66
|
+
"decision",
|
|
67
|
+
"authority",
|
|
68
|
+
"decision_id",
|
|
69
|
+
"approval_id",
|
|
70
|
+
"action_id",
|
|
71
|
+
"action_digest",
|
|
72
|
+
"revision",
|
|
73
|
+
"principal",
|
|
74
|
+
"decided_at_epoch",
|
|
75
|
+
"expires_at_epoch",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# ``binding_method`` value reserved exclusively for the bounded-grant shape.
|
|
80
|
+
GRANT_BINDING_METHOD = "bounded_grant"
|
|
81
|
+
GRANT_UNLIMITED_SENTINEL = "unlimited"
|
|
82
|
+
|
|
83
|
+
_GATE_STRING_MAX = 512
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True)
|
|
87
|
+
class ProvenanceShape:
|
|
88
|
+
"""One exact evidence field-set an owner approval gate may take.
|
|
89
|
+
|
|
90
|
+
``provenance_fields`` always includes ``binding_method``. A shape either
|
|
91
|
+
pins ``binding_method`` to one exact reserved value (the grant shape pins
|
|
92
|
+
``"bounded_grant"``) or accepts any bounded non-empty string that is not
|
|
93
|
+
reserved by another registered shape. ``expiry_fields`` are provenance
|
|
94
|
+
epoch fields re-enforced at point of use in addition to the common gate
|
|
95
|
+
expiry.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
name: str
|
|
99
|
+
provenance_fields: frozenset[str]
|
|
100
|
+
grants: bool = False
|
|
101
|
+
binding_method: str | None = None
|
|
102
|
+
expiry_fields: tuple[str, ...] = ()
|
|
103
|
+
|
|
104
|
+
def __post_init__(self) -> None:
|
|
105
|
+
if not self.name or not isinstance(self.name, str):
|
|
106
|
+
raise ProvenanceShapeError("provenance shape name is invalid")
|
|
107
|
+
if "binding_method" not in self.provenance_fields:
|
|
108
|
+
raise ProvenanceShapeError(
|
|
109
|
+
"provenance shape must carry binding_method"
|
|
110
|
+
)
|
|
111
|
+
if self.provenance_fields & GATE_COMMON_FIELDS:
|
|
112
|
+
raise ProvenanceShapeError(
|
|
113
|
+
"provenance fields may not shadow common gate fields"
|
|
114
|
+
)
|
|
115
|
+
if any(name not in self.provenance_fields for name in self.expiry_fields):
|
|
116
|
+
raise ProvenanceShapeError(
|
|
117
|
+
"expiry fields must be provenance fields"
|
|
118
|
+
)
|
|
119
|
+
if "binding_method" in self.expiry_fields:
|
|
120
|
+
raise ProvenanceShapeError("binding_method is never an expiry")
|
|
121
|
+
if self.grants and self.binding_method != GRANT_BINDING_METHOD:
|
|
122
|
+
raise ProvenanceShapeError(
|
|
123
|
+
"a grant shape must pin binding_method to the reserved "
|
|
124
|
+
"bounded-grant value"
|
|
125
|
+
)
|
|
126
|
+
if not self.grants and self.binding_method == GRANT_BINDING_METHOD:
|
|
127
|
+
raise ProvenanceShapeError(
|
|
128
|
+
"the bounded-grant binding method is reserved for the "
|
|
129
|
+
"grant shape only"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def evidence_fields(self) -> frozenset[str]:
|
|
134
|
+
return GATE_COMMON_FIELDS | self.provenance_fields
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def string_fields(self) -> tuple[str, ...]:
|
|
138
|
+
"""Provenance fields checked as bounded non-empty strings."""
|
|
139
|
+
|
|
140
|
+
return tuple(
|
|
141
|
+
sorted(self.provenance_fields - set(self.expiry_fields))
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
MESSAGE_DELIVERY_SHAPE = ProvenanceShape(
|
|
146
|
+
name="message_delivery",
|
|
147
|
+
provenance_fields=frozenset(
|
|
148
|
+
{
|
|
149
|
+
"channel",
|
|
150
|
+
"thread_id",
|
|
151
|
+
"event_id",
|
|
152
|
+
"event_key",
|
|
153
|
+
"delivery_id",
|
|
154
|
+
"delivery_message_id",
|
|
155
|
+
"binding_method",
|
|
156
|
+
}
|
|
157
|
+
),
|
|
158
|
+
grants=False,
|
|
159
|
+
binding_method=None,
|
|
160
|
+
expiry_fields=(),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
BOUNDED_GRANT_SHAPE = ProvenanceShape(
|
|
164
|
+
name="bounded_grant",
|
|
165
|
+
provenance_fields=frozenset(
|
|
166
|
+
{
|
|
167
|
+
"bounded_grant_id",
|
|
168
|
+
"approval_source_request_id",
|
|
169
|
+
"bounded_grant_expires_at_epoch",
|
|
170
|
+
"binding_method",
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
grants=True,
|
|
174
|
+
binding_method=GRANT_BINDING_METHOD,
|
|
175
|
+
expiry_fields=("bounded_grant_expires_at_epoch",),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ProvenanceShapeRegistry:
|
|
180
|
+
"""Exact-match shape registry that refuses every ambiguous registration.
|
|
181
|
+
|
|
182
|
+
Selection is by EXACT evidence field-set equality only — never subset or
|
|
183
|
+
superset matching. Registration refuses shapes whose field sets are
|
|
184
|
+
equal to, a subset of, or a superset of an already-registered shape
|
|
185
|
+
(either would invite subset-matching mistakes later), refuses a second
|
|
186
|
+
claim on a pinned ``binding_method`` value, and reserves
|
|
187
|
+
``"bounded_grant"`` for the grant shape only.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, shapes: Iterable[ProvenanceShape] = ()) -> None:
|
|
191
|
+
self._shapes: dict[str, ProvenanceShape] = {}
|
|
192
|
+
for shape in shapes:
|
|
193
|
+
self.register(shape)
|
|
194
|
+
|
|
195
|
+
def register(self, shape: ProvenanceShape) -> None:
|
|
196
|
+
if not isinstance(shape, ProvenanceShape):
|
|
197
|
+
raise ProvenanceShapeError("provenance shape is invalid")
|
|
198
|
+
if shape.name in self._shapes:
|
|
199
|
+
raise ProvenanceShapeError(
|
|
200
|
+
"provenance shape name is already registered"
|
|
201
|
+
)
|
|
202
|
+
for existing in self._shapes.values():
|
|
203
|
+
if (
|
|
204
|
+
shape.evidence_fields == existing.evidence_fields
|
|
205
|
+
or shape.evidence_fields <= existing.evidence_fields
|
|
206
|
+
or shape.evidence_fields >= existing.evidence_fields
|
|
207
|
+
):
|
|
208
|
+
raise ProvenanceShapeError(
|
|
209
|
+
"provenance shape overlaps a registered shape"
|
|
210
|
+
)
|
|
211
|
+
if (
|
|
212
|
+
shape.binding_method is not None
|
|
213
|
+
and shape.binding_method == existing.binding_method
|
|
214
|
+
):
|
|
215
|
+
raise ProvenanceShapeError(
|
|
216
|
+
"binding_method value is already reserved"
|
|
217
|
+
)
|
|
218
|
+
self._shapes[shape.name] = shape
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def shapes(self) -> tuple[ProvenanceShape, ...]:
|
|
222
|
+
return tuple(self._shapes.values())
|
|
223
|
+
|
|
224
|
+
def reserved_binding_methods(self) -> frozenset[str]:
|
|
225
|
+
return frozenset(
|
|
226
|
+
shape.binding_method
|
|
227
|
+
for shape in self._shapes.values()
|
|
228
|
+
if shape.binding_method is not None
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def select(self, evidence_fields: Iterable[str]) -> ProvenanceShape:
|
|
232
|
+
"""Return the one shape whose evidence field set matches EXACTLY."""
|
|
233
|
+
|
|
234
|
+
fields = frozenset(evidence_fields)
|
|
235
|
+
for shape in self._shapes.values():
|
|
236
|
+
if fields == shape.evidence_fields:
|
|
237
|
+
return shape
|
|
238
|
+
raise OwnerGateError("owner approval gate is invalid")
|
|
239
|
+
|
|
240
|
+
def binding_method_valid(self, shape: ProvenanceShape, value: Any) -> bool:
|
|
241
|
+
"""True iff ``value`` is a binding method this shape may carry.
|
|
242
|
+
|
|
243
|
+
A pinned shape requires its exact value. An open shape accepts any
|
|
244
|
+
value except those reserved by other shapes — so a delivery-shaped
|
|
245
|
+
receipt claiming ``"bounded_grant"`` (or any other reserved value) is
|
|
246
|
+
refused even though its field set matched.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
if not isinstance(value, str) or not value:
|
|
250
|
+
return False
|
|
251
|
+
if shape.binding_method is not None:
|
|
252
|
+
return value == shape.binding_method
|
|
253
|
+
return value not in self.reserved_binding_methods()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def default_registry() -> ProvenanceShapeRegistry:
|
|
257
|
+
return ProvenanceShapeRegistry((MESSAGE_DELIVERY_SHAPE, BOUNDED_GRANT_SHAPE))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
DEFAULT_REGISTRY = default_registry()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass(frozen=True, slots=True)
|
|
264
|
+
class GateAuthorization:
|
|
265
|
+
"""The one bounded projection of a passing owner approval gate."""
|
|
266
|
+
|
|
267
|
+
shape: str
|
|
268
|
+
granted: bool
|
|
269
|
+
receipt_key: str
|
|
270
|
+
evidence_sha256: str
|
|
271
|
+
approval_id: str
|
|
272
|
+
decision_id: str
|
|
273
|
+
revision: int
|
|
274
|
+
decided_at: float
|
|
275
|
+
expires_at: float
|
|
276
|
+
expired: bool
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _epoch(evidence: Mapping[str, Any], field_name: str) -> float:
|
|
280
|
+
try:
|
|
281
|
+
return float(evidence.get(field_name))
|
|
282
|
+
except (TypeError, ValueError) as error:
|
|
283
|
+
raise OwnerGateError("owner approval time is invalid") from error
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def validate_owner_gate(
|
|
287
|
+
action: Mapping[str, Any],
|
|
288
|
+
receipts: Iterable[Mapping[str, Any]],
|
|
289
|
+
*,
|
|
290
|
+
tool_name: str,
|
|
291
|
+
now: float,
|
|
292
|
+
registry: ProvenanceShapeRegistry = DEFAULT_REGISTRY,
|
|
293
|
+
catalog: Mapping[str, ToolSpec] = TOOL_CATALOG,
|
|
294
|
+
) -> GateAuthorization:
|
|
295
|
+
"""Validate exactly one still-bindable owner approval gate.
|
|
296
|
+
|
|
297
|
+
``action`` supplies the durable binding targets (``action_id`` and the
|
|
298
|
+
UTF-8 payload digest ``payload_sha256``); ``receipts`` is the action's
|
|
299
|
+
full receipt list; ``now`` is the caller's point-of-use clock reading.
|
|
300
|
+
Raises :class:`OwnerGateError` unless every invariant in the module
|
|
301
|
+
docstring holds. ``expired`` on the returned authorization is True when
|
|
302
|
+
ANY expiry the shape carries has passed — callers must treat an expired
|
|
303
|
+
authorization as non-dispatchable.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
if isinstance(now, bool) or not isinstance(now, (int, float)):
|
|
307
|
+
raise OwnerGateError("owner approval clock is invalid")
|
|
308
|
+
now = float(now)
|
|
309
|
+
if not math.isfinite(now) or now <= 0:
|
|
310
|
+
raise OwnerGateError("owner approval clock is invalid")
|
|
311
|
+
|
|
312
|
+
gates = [
|
|
313
|
+
receipt
|
|
314
|
+
for receipt in receipts
|
|
315
|
+
if isinstance(receipt, Mapping) and receipt.get("kind") == "gate"
|
|
316
|
+
]
|
|
317
|
+
if len(gates) != 1:
|
|
318
|
+
raise OwnerGateError("exactly one owner approval gate is required")
|
|
319
|
+
receipt = gates[0]
|
|
320
|
+
evidence = receipt.get("evidence")
|
|
321
|
+
if not isinstance(evidence, Mapping) or any(
|
|
322
|
+
not isinstance(key, str) for key in evidence
|
|
323
|
+
):
|
|
324
|
+
raise OwnerGateError("owner approval gate is invalid")
|
|
325
|
+
|
|
326
|
+
# Exact field-set match against exactly one registered shape; a partial
|
|
327
|
+
# or mixed shape never selects and is refused here.
|
|
328
|
+
shape = registry.select(evidence)
|
|
329
|
+
|
|
330
|
+
# A standing grant never authorizes a tool the catalog marks
|
|
331
|
+
# non-grantable — or a tool the catalog does not know — regardless of
|
|
332
|
+
# what the grant, the registry, or any host configuration says.
|
|
333
|
+
if shape.grants:
|
|
334
|
+
spec = catalog.get(tool_name)
|
|
335
|
+
if spec is None or spec.non_grantable:
|
|
336
|
+
raise OwnerGateError("bounded grant authority never covers this tool")
|
|
337
|
+
|
|
338
|
+
decided_at = _epoch(evidence, "decided_at_epoch")
|
|
339
|
+
expires_at = _epoch(evidence, "expires_at_epoch")
|
|
340
|
+
standing_grant = bool(
|
|
341
|
+
shape.grants
|
|
342
|
+
and shape.binding_method == GRANT_BINDING_METHOD
|
|
343
|
+
and evidence.get("bounded_grant_expires_at_epoch")
|
|
344
|
+
== GRANT_UNLIMITED_SENTINEL
|
|
345
|
+
)
|
|
346
|
+
extra_expiries = tuple(
|
|
347
|
+
_epoch(evidence, field_name)
|
|
348
|
+
for field_name in shape.expiry_fields
|
|
349
|
+
if not (
|
|
350
|
+
standing_grant
|
|
351
|
+
and field_name == "bounded_grant_expires_at_epoch"
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
receipt_key = receipt.get("receipt_key")
|
|
356
|
+
evidence_sha256 = receipt.get("evidence_sha256")
|
|
357
|
+
if (
|
|
358
|
+
receipt.get("status") != "passed"
|
|
359
|
+
or evidence.get("decision") != "approved"
|
|
360
|
+
or evidence.get("authority") != "owner"
|
|
361
|
+
or evidence.get("action_id") != action.get("action_id")
|
|
362
|
+
or not hmac.compare_digest(
|
|
363
|
+
str(evidence.get("action_digest") or ""),
|
|
364
|
+
str(action.get("payload_sha256") or ""),
|
|
365
|
+
)
|
|
366
|
+
or isinstance(evidence.get("revision"), bool)
|
|
367
|
+
or evidence.get("revision") != 1
|
|
368
|
+
or receipt.get("external_id") != evidence.get("approval_id")
|
|
369
|
+
or not isinstance(receipt_key, str)
|
|
370
|
+
or not receipt_key
|
|
371
|
+
or not isinstance(evidence_sha256, str)
|
|
372
|
+
or not hmac.compare_digest(
|
|
373
|
+
evidence_sha256, sha256_json_utf8(dict(evidence))
|
|
374
|
+
)
|
|
375
|
+
or not APPROVAL_ID_RE.fullmatch(str(evidence.get("approval_id") or ""))
|
|
376
|
+
or any(
|
|
377
|
+
not isinstance(evidence.get(field_name), str)
|
|
378
|
+
or not evidence.get(field_name)
|
|
379
|
+
or len(evidence.get(field_name)) > _GATE_STRING_MAX
|
|
380
|
+
for field_name in ("decision_id", "principal") + shape.string_fields
|
|
381
|
+
)
|
|
382
|
+
or not registry.binding_method_valid(
|
|
383
|
+
shape, evidence.get("binding_method")
|
|
384
|
+
)
|
|
385
|
+
or not math.isfinite(decided_at)
|
|
386
|
+
or not math.isfinite(expires_at)
|
|
387
|
+
or not all(math.isfinite(epoch) for epoch in extra_expiries)
|
|
388
|
+
or decided_at <= 0
|
|
389
|
+
or decided_at > now + GATE_CLOCK_SKEW_SECONDS
|
|
390
|
+
or expires_at <= decided_at
|
|
391
|
+
or any(epoch <= 0 for epoch in extra_expiries)
|
|
392
|
+
):
|
|
393
|
+
raise OwnerGateError("owner approval gate does not bind this action")
|
|
394
|
+
|
|
395
|
+
return GateAuthorization(
|
|
396
|
+
shape=shape.name,
|
|
397
|
+
granted=shape.grants,
|
|
398
|
+
receipt_key=receipt_key,
|
|
399
|
+
evidence_sha256=evidence_sha256,
|
|
400
|
+
approval_id=evidence["approval_id"],
|
|
401
|
+
decision_id=evidence["decision_id"],
|
|
402
|
+
revision=1,
|
|
403
|
+
decided_at=decided_at,
|
|
404
|
+
expires_at=expires_at,
|
|
405
|
+
# Point-of-use re-enforcement of EVERY expiry the shape carries: an
|
|
406
|
+
# authority expired now must never dispatch, regardless of what was
|
|
407
|
+
# true when the receipt was written.
|
|
408
|
+
expired=expires_at <= now
|
|
409
|
+
or any(epoch <= now for epoch in extra_expiries),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def assert_dispatchable(authorization: GateAuthorization) -> GateAuthorization:
|
|
414
|
+
"""Refuse an authorization whose expiry has passed at point of use.
|
|
415
|
+
|
|
416
|
+
:func:`validate_owner_gate` mirrors the deployed worker exactly and
|
|
417
|
+
reports expiry as data (``expired``); dispatchers MUST route through this
|
|
418
|
+
helper (or an equivalent check) so an expired gate or grant can never
|
|
419
|
+
reach a mutation.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
if not isinstance(authorization, GateAuthorization):
|
|
423
|
+
raise OwnerGateError("owner approval authorization is invalid")
|
|
424
|
+
if authorization.expired:
|
|
425
|
+
raise OwnerGateError(
|
|
426
|
+
"owner approval authority is expired at point of use"
|
|
427
|
+
)
|
|
428
|
+
return authorization
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
__all__ = (
|
|
432
|
+
"BOUNDED_GRANT_SHAPE",
|
|
433
|
+
"DEFAULT_REGISTRY",
|
|
434
|
+
"GATE_COMMON_FIELDS",
|
|
435
|
+
"GRANT_BINDING_METHOD",
|
|
436
|
+
"GRANT_UNLIMITED_SENTINEL",
|
|
437
|
+
"GateAuthorization",
|
|
438
|
+
"MESSAGE_DELIVERY_SHAPE",
|
|
439
|
+
"OwnerGateError",
|
|
440
|
+
"ProvenanceShape",
|
|
441
|
+
"ProvenanceShapeError",
|
|
442
|
+
"ProvenanceShapeRegistry",
|
|
443
|
+
"assert_dispatchable",
|
|
444
|
+
"default_registry",
|
|
445
|
+
"validate_owner_gate",
|
|
446
|
+
)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Validation of the immutable ``HermesToolActionIntentV1`` wire document.
|
|
2
|
+
|
|
3
|
+
The schema string on the wire is exactly ``"HermesToolActionIntentV1"`` — it
|
|
4
|
+
is already public and must never be renamed. Neither the intent body nor its
|
|
5
|
+
diagnostic context is authority: this module only proves that the document is
|
|
6
|
+
internally consistent (exact fields, catalog-valid args, and all four
|
|
7
|
+
ASCII-convention digests recomputed and matched) and produces a bounded
|
|
8
|
+
owner-facing approval description. It performs no effects.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Mapping
|
|
16
|
+
|
|
17
|
+
from .catalog import ACTION_TOOL_NAMES, TOOL_CATALOG, validate_tool_args
|
|
18
|
+
from .contract import (
|
|
19
|
+
CALL_IDENTITY_SCHEMA,
|
|
20
|
+
CONTEXT_FIELDS,
|
|
21
|
+
GovernedContractError,
|
|
22
|
+
INTENT_FIELDS,
|
|
23
|
+
INTENT_ID_RE,
|
|
24
|
+
INTENT_SCHEMA,
|
|
25
|
+
SHA256_RE,
|
|
26
|
+
bounded_text,
|
|
27
|
+
canonical_json_ascii,
|
|
28
|
+
exact_mapping,
|
|
29
|
+
sha256_json_ascii,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HermesActionIntentError(GovernedContractError):
|
|
34
|
+
"""The submitted intent is not the exact governed contract."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _validate_context(raw: Any) -> dict[str, str]:
|
|
38
|
+
context = exact_mapping(
|
|
39
|
+
raw,
|
|
40
|
+
"context",
|
|
41
|
+
allowed=CONTEXT_FIELDS,
|
|
42
|
+
required=CONTEXT_FIELDS,
|
|
43
|
+
)
|
|
44
|
+
for key, value in context.items():
|
|
45
|
+
bounded_text(value, "context.%s" % key, 512, allow_empty=True)
|
|
46
|
+
return context
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _call_identity_digest(tool_name: str, context: Mapping[str, str]) -> str:
|
|
50
|
+
return sha256_json_ascii(
|
|
51
|
+
{
|
|
52
|
+
"schema": CALL_IDENTITY_SCHEMA,
|
|
53
|
+
"tool_name": tool_name,
|
|
54
|
+
"api_request_id": context.get("api_request_id", ""),
|
|
55
|
+
"session_id": context.get("session_id", ""),
|
|
56
|
+
"task_id": context.get("task_id", ""),
|
|
57
|
+
"tool_call_id": context.get("tool_call_id", ""),
|
|
58
|
+
"turn_id": context.get("turn_id", ""),
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class HermesToolActionIntentV1:
|
|
65
|
+
"""Immutable, digest-verified governed action intent."""
|
|
66
|
+
|
|
67
|
+
intent_id: str
|
|
68
|
+
idempotency_key: str
|
|
69
|
+
tool_name: str
|
|
70
|
+
args_sha256: str
|
|
71
|
+
context_sha256: str
|
|
72
|
+
intent_digest: str
|
|
73
|
+
_args_json: str
|
|
74
|
+
_context_json: str
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def build(
|
|
78
|
+
cls,
|
|
79
|
+
*,
|
|
80
|
+
tool_name: str,
|
|
81
|
+
args: Mapping[str, Any],
|
|
82
|
+
context: Mapping[str, str],
|
|
83
|
+
) -> "HermesToolActionIntentV1":
|
|
84
|
+
"""Construct a new intent exactly as the Hermes plugin does."""
|
|
85
|
+
|
|
86
|
+
validated_args = validate_tool_args(tool_name, args)
|
|
87
|
+
validated_context = _validate_context(context)
|
|
88
|
+
args_json = canonical_json_ascii(validated_args)
|
|
89
|
+
context_json = canonical_json_ascii(validated_context)
|
|
90
|
+
args_sha = sha256_json_ascii(validated_args)
|
|
91
|
+
context_sha = sha256_json_ascii(validated_context)
|
|
92
|
+
idempotency_key = _call_identity_digest(tool_name, validated_context)
|
|
93
|
+
intent_id = "hti_" + idempotency_key[:32]
|
|
94
|
+
unsigned = {
|
|
95
|
+
"schema": INTENT_SCHEMA,
|
|
96
|
+
"version": 1,
|
|
97
|
+
"intent_id": intent_id,
|
|
98
|
+
"idempotency_key": idempotency_key,
|
|
99
|
+
"tool_name": tool_name,
|
|
100
|
+
"args": json.loads(args_json),
|
|
101
|
+
"args_sha256": args_sha,
|
|
102
|
+
"context": json.loads(context_json),
|
|
103
|
+
"context_sha256": context_sha,
|
|
104
|
+
}
|
|
105
|
+
return cls(
|
|
106
|
+
intent_id=intent_id,
|
|
107
|
+
idempotency_key=idempotency_key,
|
|
108
|
+
tool_name=tool_name,
|
|
109
|
+
args_sha256=args_sha,
|
|
110
|
+
context_sha256=context_sha,
|
|
111
|
+
intent_digest=sha256_json_ascii(unsigned),
|
|
112
|
+
_args_json=args_json,
|
|
113
|
+
_context_json=context_json,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_mapping(cls, value: Any) -> "HermesToolActionIntentV1":
|
|
118
|
+
try:
|
|
119
|
+
document = exact_mapping(
|
|
120
|
+
value,
|
|
121
|
+
"intent",
|
|
122
|
+
allowed=INTENT_FIELDS,
|
|
123
|
+
required=INTENT_FIELDS,
|
|
124
|
+
)
|
|
125
|
+
if document["schema"] != INTENT_SCHEMA:
|
|
126
|
+
raise HermesActionIntentError("intent schema is invalid")
|
|
127
|
+
if isinstance(document["version"], bool) or document["version"] != 1:
|
|
128
|
+
raise HermesActionIntentError("intent version is invalid")
|
|
129
|
+
tool_name = document["tool_name"]
|
|
130
|
+
if not isinstance(tool_name, str) or tool_name not in ACTION_TOOL_NAMES:
|
|
131
|
+
raise HermesActionIntentError("tool is not a governed action intent")
|
|
132
|
+
args = validate_tool_args(tool_name, document["args"])
|
|
133
|
+
context = _validate_context(document["context"])
|
|
134
|
+
except HermesActionIntentError:
|
|
135
|
+
raise
|
|
136
|
+
except GovernedContractError as error:
|
|
137
|
+
raise HermesActionIntentError(str(error)) from error
|
|
138
|
+
args_sha256 = document["args_sha256"]
|
|
139
|
+
context_sha256 = document["context_sha256"]
|
|
140
|
+
idempotency_key = document["idempotency_key"]
|
|
141
|
+
intent_id = document["intent_id"]
|
|
142
|
+
intent_digest = document["intent_digest"]
|
|
143
|
+
for name, digest in (
|
|
144
|
+
("args_sha256", args_sha256),
|
|
145
|
+
("context_sha256", context_sha256),
|
|
146
|
+
("idempotency_key", idempotency_key),
|
|
147
|
+
("intent_digest", intent_digest),
|
|
148
|
+
):
|
|
149
|
+
if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest):
|
|
150
|
+
raise HermesActionIntentError("%s is invalid" % name)
|
|
151
|
+
if not isinstance(intent_id, str) or not INTENT_ID_RE.fullmatch(intent_id):
|
|
152
|
+
raise HermesActionIntentError("intent_id is invalid")
|
|
153
|
+
if args_sha256 != sha256_json_ascii(args):
|
|
154
|
+
raise HermesActionIntentError("args digest does not match")
|
|
155
|
+
if context_sha256 != sha256_json_ascii(context):
|
|
156
|
+
raise HermesActionIntentError("context digest does not match")
|
|
157
|
+
if idempotency_key != _call_identity_digest(tool_name, context):
|
|
158
|
+
raise HermesActionIntentError(
|
|
159
|
+
"idempotency key does not match call identity"
|
|
160
|
+
)
|
|
161
|
+
if intent_id != "hti_" + idempotency_key[:32]:
|
|
162
|
+
raise HermesActionIntentError("intent ID does not match call identity")
|
|
163
|
+
unsigned = {
|
|
164
|
+
"schema": INTENT_SCHEMA,
|
|
165
|
+
"version": 1,
|
|
166
|
+
"intent_id": intent_id,
|
|
167
|
+
"idempotency_key": idempotency_key,
|
|
168
|
+
"tool_name": tool_name,
|
|
169
|
+
"args": args,
|
|
170
|
+
"args_sha256": args_sha256,
|
|
171
|
+
"context": context,
|
|
172
|
+
"context_sha256": context_sha256,
|
|
173
|
+
}
|
|
174
|
+
if intent_digest != sha256_json_ascii(unsigned):
|
|
175
|
+
raise HermesActionIntentError("intent digest does not match")
|
|
176
|
+
try:
|
|
177
|
+
args_json = canonical_json_ascii(args)
|
|
178
|
+
context_json = canonical_json_ascii(context)
|
|
179
|
+
except GovernedContractError as error:
|
|
180
|
+
raise HermesActionIntentError(str(error)) from error
|
|
181
|
+
return cls(
|
|
182
|
+
intent_id=intent_id,
|
|
183
|
+
idempotency_key=idempotency_key,
|
|
184
|
+
tool_name=tool_name,
|
|
185
|
+
args_sha256=args_sha256,
|
|
186
|
+
context_sha256=context_sha256,
|
|
187
|
+
intent_digest=intent_digest,
|
|
188
|
+
_args_json=args_json,
|
|
189
|
+
_context_json=context_json,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def args(self) -> dict[str, Any]:
|
|
194
|
+
return json.loads(self._args_json)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def context(self) -> dict[str, str]:
|
|
198
|
+
return json.loads(self._context_json)
|
|
199
|
+
|
|
200
|
+
def to_dict(self) -> dict[str, Any]:
|
|
201
|
+
return {
|
|
202
|
+
"schema": INTENT_SCHEMA,
|
|
203
|
+
"version": 1,
|
|
204
|
+
"intent_id": self.intent_id,
|
|
205
|
+
"idempotency_key": self.idempotency_key,
|
|
206
|
+
"tool_name": self.tool_name,
|
|
207
|
+
"args": self.args,
|
|
208
|
+
"args_sha256": self.args_sha256,
|
|
209
|
+
"context": self.context,
|
|
210
|
+
"context_sha256": self.context_sha256,
|
|
211
|
+
"intent_digest": self.intent_digest,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def approval_display(self) -> dict[str, str]:
|
|
215
|
+
return TOOL_CATALOG[self.tool_name].approval_display(self.args)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
__all__ = (
|
|
219
|
+
"HermesActionIntentError",
|
|
220
|
+
"HermesToolActionIntentV1",
|
|
221
|
+
)
|