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,402 @@
|
|
|
1
|
+
"""Stateless wire contract for governed Colony host-worker actions.
|
|
2
|
+
|
|
3
|
+
This module is the single written-down form of the governed-action wire
|
|
4
|
+
contract: schema names, field sets, identifier grammars, size bounds, and the
|
|
5
|
+
two canonical-JSON digest conventions. Everything here is stdlib-only and
|
|
6
|
+
free of I/O so that any process — the private host worker, tooling, or tests —
|
|
7
|
+
can validate the contract without importing a server.
|
|
8
|
+
|
|
9
|
+
CRITICAL DESIGN RULE — INDEPENDENT VALIDATORS, DO NOT "UNIFY"
|
|
10
|
+
=============================================================
|
|
11
|
+
ColonyAI's endpoint (``sidecar/apsimo/governed_actions.py``) MUST KEEP
|
|
12
|
+
ITS OWN INDEPENDENT VALIDATOR. Do NOT refactor the endpoint to import
|
|
13
|
+
``apsimo_hostworker``. The two implementations are deliberately separate and
|
|
14
|
+
cross-check each other; that redundancy has already caught a real
|
|
15
|
+
incompatibility (the ASCII/UTF-8 canonical-JSON digest split documented
|
|
16
|
+
below). A repo-internal test (``sidecar/tests/test_hostworker_agreement.py``)
|
|
17
|
+
runs BOTH implementations against shared golden vectors and fails if they
|
|
18
|
+
disagree. Anyone later "cleaning this up" by import-unifying them turns this
|
|
19
|
+
from a safety improvement into a safety loss: a single shared bug would then
|
|
20
|
+
validate itself on both sides of the trust boundary.
|
|
21
|
+
|
|
22
|
+
THE TWO CANONICAL-JSON CONVENTIONS
|
|
23
|
+
==================================
|
|
24
|
+
There are — deliberately and permanently — two canonical JSON serializations
|
|
25
|
+
on this wire, differing only in ``ensure_ascii``:
|
|
26
|
+
|
|
27
|
+
* ASCII-escaped (``canonical_json_ascii`` / ``sha256_json_ascii``): the
|
|
28
|
+
historical convention introduced by the Hermes plugin. It computes
|
|
29
|
+
``args_sha256``, ``context_sha256``, ``idempotency_key``, ``intent_digest``,
|
|
30
|
+
and the ``intent_id`` derivation of ``HermesToolActionIntentV1``.
|
|
31
|
+
|
|
32
|
+
* UTF-8 (``canonical_json_utf8`` / ``sha256_json_utf8``): the host Action
|
|
33
|
+
Plane convention. It computes the outer ``execution_digest`` of
|
|
34
|
+
``ColonyGovernedActionExecutionV1``, the returned ``effect_digest``, the
|
|
35
|
+
action ``payload_sha256`` (a.k.a. ``action_digest``), and every receipt
|
|
36
|
+
``evidence_sha256``.
|
|
37
|
+
|
|
38
|
+
The split exists because two codebases grew the two digests independently and
|
|
39
|
+
both are now pinned by durable ledgers and immutable receipts on both sides.
|
|
40
|
+
Re-serializing either family with the other convention changes every digest
|
|
41
|
+
on the wire. The conventions are therefore named separately, pinned by golden
|
|
42
|
+
vectors, and must never be merged or "fixed".
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import hashlib
|
|
48
|
+
import json
|
|
49
|
+
import math
|
|
50
|
+
import re
|
|
51
|
+
from typing import Any, Mapping
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GovernedContractError(ValueError):
|
|
55
|
+
"""A value is not the exact bounded governed-action contract."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --------------------------------------------------------------------------
|
|
59
|
+
# Schema names (public wire strings; never rename)
|
|
60
|
+
# --------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
INTENT_SCHEMA = "HermesToolActionIntentV1"
|
|
63
|
+
INTENT_ENVELOPE_SCHEMA = "HermesToolActionEnvelopeV1"
|
|
64
|
+
CALL_IDENTITY_SCHEMA = "HermesActionCallV1"
|
|
65
|
+
EXECUTION_REQUEST_SCHEMA = "ColonyGovernedActionExecutionV1"
|
|
66
|
+
APPROVAL_BINDING_SCHEMA = "ColonyOwnerApprovalExecutionBindingV1"
|
|
67
|
+
EXECUTION_RESULT_SCHEMA = "ColonyGovernedActionExecutionResultV1"
|
|
68
|
+
EFFECT_SCHEMA = "ColonyGovernedActionEffectV1"
|
|
69
|
+
|
|
70
|
+
# --------------------------------------------------------------------------
|
|
71
|
+
# Exact field sets
|
|
72
|
+
# --------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
INTENT_FIELDS = frozenset(
|
|
75
|
+
{
|
|
76
|
+
"schema",
|
|
77
|
+
"version",
|
|
78
|
+
"intent_id",
|
|
79
|
+
"idempotency_key",
|
|
80
|
+
"tool_name",
|
|
81
|
+
"args",
|
|
82
|
+
"args_sha256",
|
|
83
|
+
"context",
|
|
84
|
+
"context_sha256",
|
|
85
|
+
"intent_digest",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
CONTEXT_FIELDS = frozenset(
|
|
89
|
+
{
|
|
90
|
+
"api_request_id",
|
|
91
|
+
"authority_lane",
|
|
92
|
+
"contact_id",
|
|
93
|
+
"platform",
|
|
94
|
+
"sender_id",
|
|
95
|
+
"session_id",
|
|
96
|
+
"task_id",
|
|
97
|
+
"tool_call_id",
|
|
98
|
+
"turn_id",
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
EXECUTION_REQUEST_FIELDS = frozenset(
|
|
102
|
+
{
|
|
103
|
+
"schema",
|
|
104
|
+
"version",
|
|
105
|
+
"action_id",
|
|
106
|
+
"action_digest",
|
|
107
|
+
"intent_id",
|
|
108
|
+
"intent_digest",
|
|
109
|
+
"tool_name",
|
|
110
|
+
"args",
|
|
111
|
+
"args_sha256",
|
|
112
|
+
"approval",
|
|
113
|
+
"execution_digest",
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
APPROVAL_BINDING_FIELDS = frozenset(
|
|
117
|
+
{
|
|
118
|
+
"schema",
|
|
119
|
+
"version",
|
|
120
|
+
"approval_id",
|
|
121
|
+
"decision_id",
|
|
122
|
+
"revision",
|
|
123
|
+
"authorization_receipt_sha256",
|
|
124
|
+
"decided_at",
|
|
125
|
+
"expires_at",
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
EXECUTION_RESULT_FIELDS = frozenset(
|
|
129
|
+
{
|
|
130
|
+
"schema",
|
|
131
|
+
"version",
|
|
132
|
+
"execution_digest",
|
|
133
|
+
"action_id",
|
|
134
|
+
"action_digest",
|
|
135
|
+
"intent_id",
|
|
136
|
+
"intent_digest",
|
|
137
|
+
"tool_name",
|
|
138
|
+
"status",
|
|
139
|
+
"effect_state",
|
|
140
|
+
"effect",
|
|
141
|
+
"effect_digest",
|
|
142
|
+
"observed_at",
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
EFFECT_FIELDS = frozenset(
|
|
146
|
+
{"schema", "version", "effect_id", "outcome", "verification"}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# --------------------------------------------------------------------------
|
|
150
|
+
# Identifier grammars
|
|
151
|
+
# --------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
154
|
+
INTENT_ID_RE = re.compile(r"^hti_[0-9a-f]{32}$")
|
|
155
|
+
# The approval-id form ColonyAI's endpoint already enforces on the wire.
|
|
156
|
+
APPROVAL_ID_RE = re.compile(r"^APR-[A-Z0-9]{12}$")
|
|
157
|
+
SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}$")
|
|
158
|
+
# ``(?![\s\S])`` is an exact end-of-string assertion in both Python and the
|
|
159
|
+
# ECMAScript regex dialect used by JSON Schema. A terminal ``$`` is not exact
|
|
160
|
+
# there: it may also match immediately before a final newline.
|
|
161
|
+
IDENTIFIER_RE = re.compile(
|
|
162
|
+
r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}(?![\s\S])"
|
|
163
|
+
)
|
|
164
|
+
ACTION_ID_RE = re.compile(
|
|
165
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# --------------------------------------------------------------------------
|
|
169
|
+
# Size and time bounds
|
|
170
|
+
# --------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
EXECUTION_REQUEST_MAX_BYTES = 32 * 1024
|
|
173
|
+
EXECUTION_RESULT_MAX_BYTES = 16 * 1024
|
|
174
|
+
EFFECT_MAX_BYTES = 8 * 1024
|
|
175
|
+
RESEARCH_TOPIC_MAX_CHARS = 1400
|
|
176
|
+
IDENTIFIER_MAX_CHARS = 256
|
|
177
|
+
BOUNDED_JSON_MAX_NODES = 512
|
|
178
|
+
BOUNDED_JSON_MAX_DEPTH = 8
|
|
179
|
+
BOUNDED_JSON_STRING_MAX_CHARS = 4096
|
|
180
|
+
BOUNDED_JSON_KEY_MAX_CHARS = 128
|
|
181
|
+
BOUNDED_JSON_INTEGER_MAX = (1 << 63) - 1
|
|
182
|
+
APPROVAL_MAX_LIFETIME_SECONDS = 86_400
|
|
183
|
+
# The one skew allowance shared by every "decided in the future?" sanity
|
|
184
|
+
# check on both sides of the boundary today.
|
|
185
|
+
GATE_CLOCK_SKEW_SECONDS = 30.0
|
|
186
|
+
|
|
187
|
+
# --------------------------------------------------------------------------
|
|
188
|
+
# Canonical JSON — both conventions, separately named on purpose
|
|
189
|
+
# --------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def canonical_json_ascii(value: Any) -> str:
|
|
193
|
+
"""Historical ASCII-escaped canonical JSON.
|
|
194
|
+
|
|
195
|
+
Digest family: ``args_sha256``, ``context_sha256``, ``idempotency_key``,
|
|
196
|
+
``intent_digest`` (and the ``intent_id`` derived from the idempotency
|
|
197
|
+
key). See the module docstring for why this must never be merged with
|
|
198
|
+
:func:`canonical_json_utf8`.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
return json.dumps(
|
|
203
|
+
value,
|
|
204
|
+
sort_keys=True,
|
|
205
|
+
separators=(",", ":"),
|
|
206
|
+
ensure_ascii=True,
|
|
207
|
+
allow_nan=False,
|
|
208
|
+
)
|
|
209
|
+
except (TypeError, ValueError, OverflowError, RecursionError) as error:
|
|
210
|
+
raise GovernedContractError("value is not canonical JSON") from error
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def canonical_json_utf8(value: Any) -> str:
|
|
214
|
+
"""Host Action Plane canonical UTF-8 JSON.
|
|
215
|
+
|
|
216
|
+
Digest family: ``execution_digest``, ``effect_digest``,
|
|
217
|
+
``payload_sha256``/``action_digest``, and receipt ``evidence_sha256``.
|
|
218
|
+
See the module docstring for why this must never be merged with
|
|
219
|
+
:func:`canonical_json_ascii`.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
return json.dumps(
|
|
224
|
+
value,
|
|
225
|
+
sort_keys=True,
|
|
226
|
+
separators=(",", ":"),
|
|
227
|
+
ensure_ascii=False,
|
|
228
|
+
allow_nan=False,
|
|
229
|
+
)
|
|
230
|
+
except (TypeError, ValueError, OverflowError, RecursionError) as error:
|
|
231
|
+
raise GovernedContractError("value is not canonical JSON") from error
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def sha256_json_ascii(value: Any) -> str:
|
|
235
|
+
return hashlib.sha256(canonical_json_ascii(value).encode("utf-8")).hexdigest()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def sha256_json_utf8(value: Any) -> str:
|
|
239
|
+
return hashlib.sha256(canonical_json_utf8(value).encode("utf-8")).hexdigest()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# --------------------------------------------------------------------------
|
|
243
|
+
# Shared bounded-scalar validators (used by the catalog and the intent)
|
|
244
|
+
# --------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def exact_mapping(
|
|
248
|
+
value: Any,
|
|
249
|
+
name: str,
|
|
250
|
+
*,
|
|
251
|
+
allowed: frozenset[str] | set[str],
|
|
252
|
+
required=(),
|
|
253
|
+
) -> dict[str, Any]:
|
|
254
|
+
"""Return ``dict(value)`` iff keys are strings within/covering the bounds."""
|
|
255
|
+
|
|
256
|
+
if not isinstance(value, Mapping):
|
|
257
|
+
raise GovernedContractError("%s must be an object" % name)
|
|
258
|
+
keys = set(value)
|
|
259
|
+
if any(not isinstance(key, str) for key in keys):
|
|
260
|
+
raise GovernedContractError("%s keys must be strings" % name)
|
|
261
|
+
if keys - set(allowed) or set(required) - keys:
|
|
262
|
+
raise GovernedContractError("%s fields are invalid" % name)
|
|
263
|
+
return dict(value)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def bounded_text(
|
|
267
|
+
value: Any,
|
|
268
|
+
name: str,
|
|
269
|
+
maximum: int,
|
|
270
|
+
*,
|
|
271
|
+
allow_empty: bool = False,
|
|
272
|
+
identifier: bool = False,
|
|
273
|
+
) -> str:
|
|
274
|
+
if not isinstance(value, str) or len(value) > maximum:
|
|
275
|
+
raise GovernedContractError("%s must be a bounded string" % name)
|
|
276
|
+
if any(
|
|
277
|
+
ord(character) == 0 or 0xD800 <= ord(character) <= 0xDFFF
|
|
278
|
+
for character in value
|
|
279
|
+
):
|
|
280
|
+
raise GovernedContractError("%s contains invalid characters" % name)
|
|
281
|
+
if not allow_empty and not value.strip():
|
|
282
|
+
raise GovernedContractError("%s cannot be empty" % name)
|
|
283
|
+
if identifier and not IDENTIFIER_RE.fullmatch(value):
|
|
284
|
+
raise GovernedContractError("%s is not a canonical identifier" % name)
|
|
285
|
+
return value
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def bounded_integer(value: Any, name: str, minimum: int, maximum: int) -> int:
|
|
289
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
290
|
+
raise GovernedContractError("%s must be an integer" % name)
|
|
291
|
+
if value < minimum or value > maximum:
|
|
292
|
+
raise GovernedContractError("%s is outside its allowed range" % name)
|
|
293
|
+
return value
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def bounded_number(
|
|
297
|
+
value: Any, name: str, minimum: float, maximum: float
|
|
298
|
+
) -> int | float:
|
|
299
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
300
|
+
raise GovernedContractError("%s must be a number" % name)
|
|
301
|
+
try:
|
|
302
|
+
finite = math.isfinite(float(value))
|
|
303
|
+
except (OverflowError, ValueError):
|
|
304
|
+
finite = False
|
|
305
|
+
if not finite or value < minimum or value > maximum:
|
|
306
|
+
raise GovernedContractError("%s is outside its allowed range" % name)
|
|
307
|
+
return value
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def enum_text(value: Any, name: str, allowed: frozenset[str]) -> str:
|
|
311
|
+
if not isinstance(value, str) or value not in allowed:
|
|
312
|
+
raise GovernedContractError("%s is not an allowed value" % name)
|
|
313
|
+
return value
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def bounded_json_value(
|
|
317
|
+
value: Any, name: str, *, depth: int = 0, counter=None
|
|
318
|
+
) -> Any:
|
|
319
|
+
"""Validate one small JSON value without coercion or exotic numerics."""
|
|
320
|
+
|
|
321
|
+
if counter is None:
|
|
322
|
+
counter = [0]
|
|
323
|
+
counter[0] += 1
|
|
324
|
+
if counter[0] > BOUNDED_JSON_MAX_NODES or depth > BOUNDED_JSON_MAX_DEPTH:
|
|
325
|
+
raise GovernedContractError("%s is too complex" % name)
|
|
326
|
+
if value is None or isinstance(value, bool):
|
|
327
|
+
return value
|
|
328
|
+
if isinstance(value, int):
|
|
329
|
+
if abs(value) > BOUNDED_JSON_INTEGER_MAX:
|
|
330
|
+
raise GovernedContractError("%s integer is too large" % name)
|
|
331
|
+
return value
|
|
332
|
+
if isinstance(value, float):
|
|
333
|
+
if not math.isfinite(value):
|
|
334
|
+
raise GovernedContractError("%s number must be finite" % name)
|
|
335
|
+
return value
|
|
336
|
+
if isinstance(value, str):
|
|
337
|
+
return bounded_text(
|
|
338
|
+
value, name, BOUNDED_JSON_STRING_MAX_CHARS, allow_empty=True,
|
|
339
|
+
)
|
|
340
|
+
if isinstance(value, list):
|
|
341
|
+
return [
|
|
342
|
+
bounded_json_value(item, name, depth=depth + 1, counter=counter)
|
|
343
|
+
for item in value
|
|
344
|
+
]
|
|
345
|
+
if isinstance(value, Mapping):
|
|
346
|
+
result = {}
|
|
347
|
+
for key, item in value.items():
|
|
348
|
+
key = bounded_text(
|
|
349
|
+
key, "%s key" % name, BOUNDED_JSON_KEY_MAX_CHARS,
|
|
350
|
+
identifier=True,
|
|
351
|
+
)
|
|
352
|
+
result[key] = bounded_json_value(
|
|
353
|
+
item, name, depth=depth + 1, counter=counter
|
|
354
|
+
)
|
|
355
|
+
return result
|
|
356
|
+
raise GovernedContractError("%s contains a non-JSON value" % name)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
__all__ = (
|
|
360
|
+
"ACTION_ID_RE",
|
|
361
|
+
"APPROVAL_BINDING_FIELDS",
|
|
362
|
+
"APPROVAL_BINDING_SCHEMA",
|
|
363
|
+
"APPROVAL_ID_RE",
|
|
364
|
+
"APPROVAL_MAX_LIFETIME_SECONDS",
|
|
365
|
+
"BOUNDED_JSON_INTEGER_MAX",
|
|
366
|
+
"BOUNDED_JSON_KEY_MAX_CHARS",
|
|
367
|
+
"BOUNDED_JSON_MAX_DEPTH",
|
|
368
|
+
"BOUNDED_JSON_MAX_NODES",
|
|
369
|
+
"BOUNDED_JSON_STRING_MAX_CHARS",
|
|
370
|
+
"CALL_IDENTITY_SCHEMA",
|
|
371
|
+
"CONTEXT_FIELDS",
|
|
372
|
+
"EFFECT_FIELDS",
|
|
373
|
+
"EFFECT_MAX_BYTES",
|
|
374
|
+
"EFFECT_SCHEMA",
|
|
375
|
+
"EXECUTION_REQUEST_FIELDS",
|
|
376
|
+
"EXECUTION_REQUEST_MAX_BYTES",
|
|
377
|
+
"EXECUTION_REQUEST_SCHEMA",
|
|
378
|
+
"EXECUTION_RESULT_FIELDS",
|
|
379
|
+
"EXECUTION_RESULT_MAX_BYTES",
|
|
380
|
+
"EXECUTION_RESULT_SCHEMA",
|
|
381
|
+
"GATE_CLOCK_SKEW_SECONDS",
|
|
382
|
+
"GovernedContractError",
|
|
383
|
+
"IDENTIFIER_MAX_CHARS",
|
|
384
|
+
"IDENTIFIER_RE",
|
|
385
|
+
"INTENT_ENVELOPE_SCHEMA",
|
|
386
|
+
"INTENT_FIELDS",
|
|
387
|
+
"INTENT_ID_RE",
|
|
388
|
+
"INTENT_SCHEMA",
|
|
389
|
+
"RESEARCH_TOPIC_MAX_CHARS",
|
|
390
|
+
"SAFE_ID_RE",
|
|
391
|
+
"SHA256_RE",
|
|
392
|
+
"bounded_integer",
|
|
393
|
+
"bounded_json_value",
|
|
394
|
+
"bounded_number",
|
|
395
|
+
"bounded_text",
|
|
396
|
+
"canonical_json_ascii",
|
|
397
|
+
"canonical_json_utf8",
|
|
398
|
+
"enum_text",
|
|
399
|
+
"exact_mapping",
|
|
400
|
+
"sha256_json_ascii",
|
|
401
|
+
"sha256_json_utf8",
|
|
402
|
+
)
|