fable-engine 1.3.1__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.
- fable_compressor.py +356 -0
- fable_engine/__init__.py +1 -0
- fable_engine/actions/__init__.py +291 -0
- fable_engine/actions/cas.py +182 -0
- fable_engine/actions/deliberation.py +523 -0
- fable_engine/actions/fleet.py +807 -0
- fable_engine/actions/lifecycle.py +298 -0
- fable_engine/actions/scrapers.py +116 -0
- fable_engine/actions/system3.py +815 -0
- fable_engine/browser.py +824 -0
- fable_engine/cas.py +974 -0
- fable_engine/fable_session.json +510 -0
- fable_engine/guards.py +283 -0
- fable_engine/schema.py +714 -0
- fable_engine/scrapers/__init__.py +32 -0
- fable_engine/scrapers/arxiv.py +115 -0
- fable_engine/scrapers/base.py +386 -0
- fable_engine/scrapers/github.py +129 -0
- fable_engine/scrapers/reddit.py +154 -0
- fable_engine/scrapers/web.py +120 -0
- fable_engine/scrapers/x.py +125 -0
- fable_engine/scrapers/youtube.py +132 -0
- fable_engine/server.py +414 -0
- fable_engine/session.py +1819 -0
- fable_engine/test_server.py +1362 -0
- fable_engine/updater.py +541 -0
- fable_engine-1.3.1.dist-info/LICENSE +22 -0
- fable_engine-1.3.1.dist-info/METADATA +173 -0
- fable_engine-1.3.1.dist-info/RECORD +104 -0
- fable_engine-1.3.1.dist-info/WHEEL +5 -0
- fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
- fable_engine-1.3.1.dist-info/top_level.txt +6 -0
- fable_mode/__init__.py +3 -0
- fable_mode/__main__.py +4 -0
- fable_mode/adapters.py +1014 -0
- fable_mode/installer.py +553 -0
- fable_mode/launcher.py +437 -0
- fable_mode/manifest.py +142 -0
- fable_mode/resources.json +114 -0
- fable_mode/safety.py +103 -0
- fable_mode_entry.py +10 -0
- fable_v2/__init__.py +146 -0
- fable_v2/adapters.py +151 -0
- fable_v2/coder_fleet/__init__.py +100 -0
- fable_v2/coder_fleet/ast_tools.py +158 -0
- fable_v2/coder_fleet/compute.py +199 -0
- fable_v2/coder_fleet/design_engine.py +1316 -0
- fable_v2/coder_fleet/diagnostics.py +293 -0
- fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
- fable_v2/coder_fleet/mock_auditor.py +306 -0
- fable_v2/coder_fleet/mutation.py +216 -0
- fable_v2/coder_fleet/property_oracle.py +260 -0
- fable_v2/coder_fleet/receipt_attestor.py +122 -0
- fable_v2/coder_fleet/red_team_swarm.py +908 -0
- fable_v2/coder_fleet/test_harness.py +198 -0
- fable_v2/coder_fleet/vector_engine.py +1287 -0
- fable_v2/coder_fleet/visual.py +357 -0
- fable_v2/coder_fleet/workspace.py +153 -0
- fable_v2/cortical/__init__.py +20 -0
- fable_v2/cortical/plasticity_engine.py +992 -0
- fable_v2/execution_broker.py +811 -0
- fable_v2/proof_engine.py +1141 -0
- fable_v2/protocol.py +485 -0
- fable_v2/runtime.py +1010 -0
- fable_v2/system3/__init__.py +204 -0
- fable_v2/system3/causal.py +558 -0
- fable_v2/system3/dialectical.py +577 -0
- fable_v2/system3/evolution.py +503 -0
- fable_v2/system3/executive.py +338 -0
- fable_v2/system3/free_energy.py +479 -0
- fable_v2/system3/hyperbolic.py +555 -0
- fable_v2/system3/induction.py +336 -0
- fable_v2/system3/kripke.py +548 -0
- fable_v2/system3/oracle.py +745 -0
- fable_v2/verifiers.py +72 -0
- tests/__init__.py +1 -0
- tests/test_anti_loop_circuit_breaker.py +64 -0
- tests/test_auto_updater.py +407 -0
- tests/test_coder_fleet.py +535 -0
- tests/test_delegation_compiler.py +54 -0
- tests/test_descriptor_boundaries.py +126 -0
- tests/test_design_engine.py +603 -0
- tests/test_epistemic_evidence_validator.py +66 -0
- tests/test_execution_broker.py +233 -0
- tests/test_fable_v2.py +406 -0
- tests/test_fleet_transitions.py +116 -0
- tests/test_fsm_redteam_evolution.py +406 -0
- tests/test_goal_rubric_and_pipeline.py +367 -0
- tests/test_hebbian_plasticity.py +585 -0
- tests/test_packaging_runtime.py +194 -0
- tests/test_proof_engine.py +259 -0
- tests/test_red_team_swarm.py +645 -0
- tests/test_redteam_remediation.py +169 -0
- tests/test_registration_transaction.py +375 -0
- tests/test_requested_regressions.py +467 -0
- tests/test_scrapers.py +370 -0
- tests/test_server_actions.py +93 -0
- tests/test_server_frontier_actions.py +269 -0
- tests/test_server_protocol.py +88 -0
- tests/test_stealth_browser.py +970 -0
- tests/test_system3.py +381 -0
- tests/test_system3_deep_integration.py +385 -0
- tests/test_system3_frontier.py +436 -0
- tests/test_vector_engine.py +608 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Property Oracle Engine for Boundary Matrix Generation and Algebraic Invariant Verification.
|
|
2
|
+
|
|
3
|
+
Generates extreme edge-case fuzzing matrices and verifies roundtrip invariants
|
|
4
|
+
(e.g., decode(encode(x)) == x) using isolated execution harnesses.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import pickle
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .test_harness import TestHarnessEngine
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PropertyOracleEngine:
|
|
19
|
+
"""Engine for generating boundary property testing matrices and verifying algebraic invariants."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, test_harness: TestHarnessEngine | None = None) -> None:
|
|
22
|
+
self._harness = test_harness or TestHarnessEngine()
|
|
23
|
+
|
|
24
|
+
def generate_property_matrix(self, types: list[str], count: int = 50) -> list[Any]:
|
|
25
|
+
"""Generate extreme boundary values (empty strings, MAX_INT, unicode, negative values,
|
|
26
|
+
|
|
27
|
+
None, large lists, floats) for the specified types.
|
|
28
|
+
"""
|
|
29
|
+
pools: list[list[Any]] = []
|
|
30
|
+
normalized_types = {t.lower().strip() for t in types}
|
|
31
|
+
|
|
32
|
+
if any(t in normalized_types for t in ("str", "string", "text")):
|
|
33
|
+
pools.append([
|
|
34
|
+
"",
|
|
35
|
+
" ",
|
|
36
|
+
"\t",
|
|
37
|
+
"\n",
|
|
38
|
+
"\r\n",
|
|
39
|
+
"\x00",
|
|
40
|
+
"null",
|
|
41
|
+
"None",
|
|
42
|
+
"undefined",
|
|
43
|
+
"0",
|
|
44
|
+
"-1",
|
|
45
|
+
"🔥🚀✨🎉",
|
|
46
|
+
"A" * 1024,
|
|
47
|
+
"a\x00b\x00c",
|
|
48
|
+
"<script>alert('xss')</script>",
|
|
49
|
+
"' OR '1'='1' --",
|
|
50
|
+
'{"key": "value", "nested": [1, 2]}',
|
|
51
|
+
"https://example.com/api?q=test&page=1",
|
|
52
|
+
"c:\\Windows\\System32\\cmd.exe",
|
|
53
|
+
"/dev/null",
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
if any(t in normalized_types for t in ("int", "integer", "number")):
|
|
57
|
+
pools.append([
|
|
58
|
+
0,
|
|
59
|
+
1,
|
|
60
|
+
-1,
|
|
61
|
+
2,
|
|
62
|
+
-2,
|
|
63
|
+
42,
|
|
64
|
+
-42,
|
|
65
|
+
100,
|
|
66
|
+
-100,
|
|
67
|
+
255,
|
|
68
|
+
256,
|
|
69
|
+
65535,
|
|
70
|
+
65536,
|
|
71
|
+
2**31 - 1,
|
|
72
|
+
-2**31,
|
|
73
|
+
2**63 - 1,
|
|
74
|
+
-2**63,
|
|
75
|
+
10**18,
|
|
76
|
+
-10**18,
|
|
77
|
+
sys.maxsize,
|
|
78
|
+
-sys.maxsize - 1,
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
if any(t in normalized_types for t in ("float", "decimal", "real")):
|
|
82
|
+
pools.append([
|
|
83
|
+
0.0,
|
|
84
|
+
-0.0,
|
|
85
|
+
1.0,
|
|
86
|
+
-1.0,
|
|
87
|
+
0.5,
|
|
88
|
+
-0.5,
|
|
89
|
+
math.pi,
|
|
90
|
+
math.e,
|
|
91
|
+
1e-15,
|
|
92
|
+
1e15,
|
|
93
|
+
1e-300,
|
|
94
|
+
1e300,
|
|
95
|
+
float("inf"),
|
|
96
|
+
float("-inf"),
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
if any(t in normalized_types for t in ("bool", "boolean")):
|
|
100
|
+
pools.append([True, False])
|
|
101
|
+
|
|
102
|
+
if any(t in normalized_types for t in ("list", "array", "sequence")):
|
|
103
|
+
pools.append([
|
|
104
|
+
[],
|
|
105
|
+
[None],
|
|
106
|
+
[0],
|
|
107
|
+
[""],
|
|
108
|
+
[True, False],
|
|
109
|
+
[1, 2, 3, 4, 5],
|
|
110
|
+
["alpha", "beta", "gamma"],
|
|
111
|
+
[[], [[]]],
|
|
112
|
+
list(range(50)),
|
|
113
|
+
[0] * 100,
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
if any(t in normalized_types for t in ("dict", "map", "object")):
|
|
117
|
+
pools.append([
|
|
118
|
+
{},
|
|
119
|
+
{"": ""},
|
|
120
|
+
{"key": "value"},
|
|
121
|
+
{"a": 1, "b": 2, "c": 3},
|
|
122
|
+
{"nested": {"level1": {"level2": True}}},
|
|
123
|
+
{"list": [1, 2, 3]},
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
if any(t in normalized_types for t in ("none", "null", "nonetype")):
|
|
127
|
+
pools.append([None])
|
|
128
|
+
|
|
129
|
+
if any(t in normalized_types for t in ("bytes", "binary")):
|
|
130
|
+
pools.append([
|
|
131
|
+
b"",
|
|
132
|
+
b"\x00",
|
|
133
|
+
b"hello world",
|
|
134
|
+
b"\xff\xfe\xfd\xfc",
|
|
135
|
+
bytes([i % 256 for i in range(128)]),
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
if not pools:
|
|
139
|
+
pools.append(["", 0, 0.0, False, None, [], {}])
|
|
140
|
+
|
|
141
|
+
# Interleave round-robin across all requested pools to ensure balanced coverage
|
|
142
|
+
result: list[Any] = []
|
|
143
|
+
max_pool_len = max(len(p) for p in pools)
|
|
144
|
+
for idx in range(max_pool_len):
|
|
145
|
+
for pool in pools:
|
|
146
|
+
if idx < len(pool):
|
|
147
|
+
result.append(pool[idx])
|
|
148
|
+
if len(result) >= count:
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
# If count still exceeds unique items, cycle through
|
|
152
|
+
cycle_idx = 0
|
|
153
|
+
while len(result) < count and pools:
|
|
154
|
+
pool = pools[cycle_idx % len(pools)]
|
|
155
|
+
result.append(pool[(cycle_idx // len(pools)) % len(pool)])
|
|
156
|
+
cycle_idx += 1
|
|
157
|
+
|
|
158
|
+
return result[:count]
|
|
159
|
+
|
|
160
|
+
def verify_algebraic_invariants(
|
|
161
|
+
self,
|
|
162
|
+
module_code: str,
|
|
163
|
+
encode_fn: str,
|
|
164
|
+
decode_fn: str,
|
|
165
|
+
sample_inputs: list[Any],
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Test decode(encode(x)) == x roundtrip consistency."""
|
|
168
|
+
if not sample_inputs:
|
|
169
|
+
return {
|
|
170
|
+
"total_tested": 0,
|
|
171
|
+
"passed": 0,
|
|
172
|
+
"failed": 0,
|
|
173
|
+
"success_rate": 1.0,
|
|
174
|
+
"roundtrip_invariant_holds": True,
|
|
175
|
+
"failure_examples": [],
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
encoded_payload = base64.b64encode(pickle.dumps(sample_inputs)).decode("ascii")
|
|
179
|
+
|
|
180
|
+
driver = f"""
|
|
181
|
+
import base64
|
|
182
|
+
import json
|
|
183
|
+
import pickle
|
|
184
|
+
import sys
|
|
185
|
+
|
|
186
|
+
{module_code}
|
|
187
|
+
|
|
188
|
+
raw_inputs = pickle.loads(base64.b64decode("{encoded_payload}".encode("ascii")))
|
|
189
|
+
encoder = globals().get("{encode_fn}")
|
|
190
|
+
decoder = globals().get("{decode_fn}")
|
|
191
|
+
|
|
192
|
+
if encoder is None or decoder is None:
|
|
193
|
+
print("__INVARIANT_OUTPUT__" + json.dumps({{"error": "Target functions not found in module"}}))
|
|
194
|
+
sys.exit(0)
|
|
195
|
+
|
|
196
|
+
total = len(raw_inputs)
|
|
197
|
+
passed = 0
|
|
198
|
+
failed = 0
|
|
199
|
+
failure_examples = []
|
|
200
|
+
|
|
201
|
+
for item in raw_inputs:
|
|
202
|
+
try:
|
|
203
|
+
enc = encoder(item)
|
|
204
|
+
dec = decoder(enc)
|
|
205
|
+
# Handle float nan comparison
|
|
206
|
+
if isinstance(item, float) and item != item:
|
|
207
|
+
is_equal = isinstance(dec, float) and dec != dec
|
|
208
|
+
else:
|
|
209
|
+
is_equal = (dec == item)
|
|
210
|
+
|
|
211
|
+
if is_equal:
|
|
212
|
+
passed += 1
|
|
213
|
+
else:
|
|
214
|
+
failed += 1
|
|
215
|
+
if len(failure_examples) < 10:
|
|
216
|
+
failure_examples.append({{
|
|
217
|
+
"input": repr(item),
|
|
218
|
+
"encoded": repr(enc),
|
|
219
|
+
"decoded": repr(dec),
|
|
220
|
+
"reason": "dec(enc(x)) != x",
|
|
221
|
+
}})
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
failed += 1
|
|
224
|
+
if len(failure_examples) < 10:
|
|
225
|
+
failure_examples.append({{
|
|
226
|
+
"input": repr(item),
|
|
227
|
+
"error": f"{{type(exc).__name__}}: {{exc}}",
|
|
228
|
+
"reason": "Exception during roundtrip",
|
|
229
|
+
}})
|
|
230
|
+
|
|
231
|
+
output = {{
|
|
232
|
+
"total_tested": total,
|
|
233
|
+
"passed": passed,
|
|
234
|
+
"failed": failed,
|
|
235
|
+
"success_rate": round(passed / max(1, total), 4),
|
|
236
|
+
"roundtrip_invariant_holds": failed == 0,
|
|
237
|
+
"failure_examples": failure_examples,
|
|
238
|
+
}}
|
|
239
|
+
print("__INVARIANT_OUTPUT__" + json.dumps(output))
|
|
240
|
+
"""
|
|
241
|
+
res = self._harness.run_scratch_test(driver, timeout_sec=6.0)
|
|
242
|
+
marker = "__INVARIANT_OUTPUT__"
|
|
243
|
+
|
|
244
|
+
if marker in res.get("stdout", ""):
|
|
245
|
+
payload_str = res["stdout"].split(marker, 1)[1].strip()
|
|
246
|
+
try:
|
|
247
|
+
data = json.loads(payload_str)
|
|
248
|
+
return data
|
|
249
|
+
except json.JSONDecodeError:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
"total_tested": len(sample_inputs),
|
|
254
|
+
"passed": 0,
|
|
255
|
+
"failed": len(sample_inputs),
|
|
256
|
+
"success_rate": 0.0,
|
|
257
|
+
"roundtrip_invariant_holds": False,
|
|
258
|
+
"error": res.get("stderr") or "Failed to execute roundtrip verification",
|
|
259
|
+
"failure_examples": [],
|
|
260
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Receipt Attestor Engine for Cryptographic Tool Execution Attestation.
|
|
2
|
+
|
|
3
|
+
Records process execution telemetry (PID, exit code, stdout/stderr hashes, timestamps)
|
|
4
|
+
and signs them using HMAC-SHA256 into a tamper-evident ToolReceipt.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReceiptAttestorEngine:
|
|
18
|
+
"""Engine for attesting subprocess execution and cryptographically verifying execution receipts."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, secret_key: bytes | None = None) -> None:
|
|
21
|
+
self._secret_key = secret_key or hashlib.sha256(b"fable_receipt_attestor_default_secret").digest()
|
|
22
|
+
|
|
23
|
+
def _compute_signature(
|
|
24
|
+
self,
|
|
25
|
+
cmd: list[str],
|
|
26
|
+
cwd: str,
|
|
27
|
+
pid: int,
|
|
28
|
+
started_at: str,
|
|
29
|
+
finished_at: str,
|
|
30
|
+
exit_code: int,
|
|
31
|
+
stdout_hash: str,
|
|
32
|
+
stderr_hash: str,
|
|
33
|
+
) -> str:
|
|
34
|
+
payload = f"{' '.join(cmd)}|{cwd}|{pid}|{started_at}|{finished_at}|{exit_code}|{stdout_hash}|{stderr_hash}"
|
|
35
|
+
return hmac.new(self._secret_key, payload.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
36
|
+
|
|
37
|
+
def attest_execution(self, cmd: list[str], cwd: str = ".") -> dict[str, Any]:
|
|
38
|
+
"""Execute subprocess, record OS PID, start/end time, exit code, stdout hash,
|
|
39
|
+
|
|
40
|
+
and HMAC-SHA256 signature, creating a tamper-evident ToolReceipt.
|
|
41
|
+
"""
|
|
42
|
+
started_at = datetime.now(timezone.utc).isoformat()
|
|
43
|
+
t0 = time.perf_counter()
|
|
44
|
+
|
|
45
|
+
proc = subprocess.Popen(
|
|
46
|
+
cmd,
|
|
47
|
+
cwd=cwd,
|
|
48
|
+
stdout=subprocess.PIPE,
|
|
49
|
+
stderr=subprocess.PIPE,
|
|
50
|
+
text=True,
|
|
51
|
+
)
|
|
52
|
+
pid = proc.pid
|
|
53
|
+
stdout, stderr = proc.communicate()
|
|
54
|
+
duration_ms = (time.perf_counter() - t0) * 1000.0
|
|
55
|
+
finished_at = datetime.now(timezone.utc).isoformat()
|
|
56
|
+
exit_code = proc.returncode
|
|
57
|
+
|
|
58
|
+
stdout_hash = hashlib.sha256(stdout.encode("utf-8")).hexdigest()
|
|
59
|
+
stderr_hash = hashlib.sha256(stderr.encode("utf-8")).hexdigest()
|
|
60
|
+
|
|
61
|
+
signature = self._compute_signature(
|
|
62
|
+
cmd=cmd,
|
|
63
|
+
cwd=cwd,
|
|
64
|
+
pid=pid,
|
|
65
|
+
started_at=started_at,
|
|
66
|
+
finished_at=finished_at,
|
|
67
|
+
exit_code=exit_code,
|
|
68
|
+
stdout_hash=stdout_hash,
|
|
69
|
+
stderr_hash=stderr_hash,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
receipt_id = f"rcpt_{uuid.uuid4().hex[:12]}"
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"receipt_id": receipt_id,
|
|
76
|
+
"cmd": list(cmd),
|
|
77
|
+
"cwd": cwd,
|
|
78
|
+
"pid": pid,
|
|
79
|
+
"started_at": started_at,
|
|
80
|
+
"finished_at": finished_at,
|
|
81
|
+
"duration_ms": round(duration_ms, 2),
|
|
82
|
+
"exit_code": exit_code,
|
|
83
|
+
"stdout": stdout,
|
|
84
|
+
"stderr": stderr,
|
|
85
|
+
"stdout_hash": stdout_hash,
|
|
86
|
+
"stderr_hash": stderr_hash,
|
|
87
|
+
"signature": signature,
|
|
88
|
+
"tamper_evident": True,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def verify_receipt(self, receipt: dict[str, Any]) -> bool:
|
|
92
|
+
"""Validate hash and HMAC-SHA256 signature integrity of a ToolReceipt."""
|
|
93
|
+
if not isinstance(receipt, dict):
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
required = ("cmd", "cwd", "pid", "started_at", "finished_at", "exit_code", "stdout_hash", "stderr_hash", "signature")
|
|
97
|
+
if not all(k in receipt for k in required):
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# If full stdout / stderr text is provided, verify against hashes
|
|
101
|
+
if "stdout" in receipt:
|
|
102
|
+
actual_stdout_hash = hashlib.sha256(str(receipt["stdout"]).encode("utf-8")).hexdigest()
|
|
103
|
+
if actual_stdout_hash != receipt["stdout_hash"]:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
if "stderr" in receipt:
|
|
107
|
+
actual_stderr_hash = hashlib.sha256(str(receipt["stderr"]).encode("utf-8")).hexdigest()
|
|
108
|
+
if actual_stderr_hash != receipt["stderr_hash"]:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
expected_sig = self._compute_signature(
|
|
112
|
+
cmd=receipt["cmd"],
|
|
113
|
+
cwd=receipt["cwd"],
|
|
114
|
+
pid=int(receipt["pid"]),
|
|
115
|
+
started_at=str(receipt["started_at"]),
|
|
116
|
+
finished_at=str(receipt["finished_at"]),
|
|
117
|
+
exit_code=int(receipt["exit_code"]),
|
|
118
|
+
stdout_hash=str(receipt["stdout_hash"]),
|
|
119
|
+
stderr_hash=str(receipt["stderr_hash"]),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return hmac.compare_digest(expected_sig, str(receipt["signature"]))
|