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.
@@ -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
+ )