Skip to content

Commit 119149b

Browse files
Pigbibicodex
andcommitted
harden trusted weekly bundle rerun verification
Co-Authored-By: Codex <noreply@openai.com>
1 parent f614746 commit 119149b

3 files changed

Lines changed: 141 additions & 15 deletions

File tree

docs/weekly_period_bundle_contract.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ truth.
1414
`input_snapshot.json`, and `bundle_manifest.json` bytes plus the external
1515
artifact evidence. Its SHA-256 properties cover all three byte members.
1616
- `RerunContext` accepts only current `run_attempt=2` and reuses the original
17-
context; it cannot derive a new period or snapshot.
17+
context; it requires the same `run_id`, repository, and reviewed workflow
18+
SHA as the original context and cannot derive a new period or snapshot.
19+
20+
The repository is an explicit `owner/name` binding in the manifest. Untrusted
21+
bundle members are bounded before parsing: lock 64 KiB, snapshot 256 KiB,
22+
manifest 64 KiB, snapshot depth 16, object keys 64, list items 256, strings
23+
4096 characters, and source artifacts 64. Exceeding any bound fails closed.
1824

1925
The canonical manifest binds the bundle version, artifact name, original run
2026
identity, workflow and producer SHAs, lock/snapshot versions, and exact lock

src/political_event_tracking_research/weekly_period_bundle.py

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@
2929
_SHA1_RE = re.compile(r"^[0-9a-f]{40}$")
3030
_ARTIFACT_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
3131
_ARTIFACT_NAME_RE = re.compile(r"^pert-weekly-period-lock-[1-9][0-9]*$")
32+
_REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
33+
MAX_LOCK_BYTES = 64 * 1024
34+
MAX_SNAPSHOT_BYTES = 256 * 1024
35+
MAX_MANIFEST_BYTES = 64 * 1024
36+
MAX_SNAPSHOT_DEPTH = 16
37+
MAX_SNAPSHOT_OBJECT_KEYS = 64
38+
MAX_SNAPSHOT_LIST_LENGTH = 256
39+
MAX_SNAPSHOT_STRING_LENGTH = 4096
40+
MAX_SOURCE_ARTIFACTS = 64
3241
_SNAPSHOT_KEYS = frozenset(
3342
{
3443
"snapshot_version",
@@ -47,6 +56,7 @@
4756
{
4857
"bundle_version",
4958
"artifact_name",
59+
"repository",
5060
"source_run_id",
5161
"source_attempt",
5262
"workflow_sha",
@@ -83,19 +93,29 @@ def _safe_int(value: object, code: str) -> int:
8393
return value
8494

8595

86-
def _snapshot_tree(value: object) -> object:
96+
def _snapshot_tree(value: object, depth: int = 0) -> object:
97+
if depth > MAX_SNAPSHOT_DEPTH:
98+
raise _error("bundle_snapshot_depth_exceeded")
8799
if isinstance(value, Mapping):
100+
if len(value) > MAX_SNAPSHOT_OBJECT_KEYS:
101+
raise _error("bundle_snapshot_object_oversized")
88102
result: dict[str, object] = {}
89103
for key, item in value.items():
90104
if type(key) is not str:
91105
raise _error("bundle_snapshot_invalid")
92106
if key in result:
93107
raise _error("bundle_snapshot_duplicate_key")
94-
result[key] = _snapshot_tree(item)
108+
result[key] = _snapshot_tree(item, depth + 1)
95109
return result
96110
if type(value) is list:
97-
return [_snapshot_tree(item) for item in value]
98-
if value is None or type(value) is bool or type(value) is str:
111+
if len(value) > MAX_SNAPSHOT_LIST_LENGTH:
112+
raise _error("bundle_snapshot_list_oversized")
113+
return [_snapshot_tree(item, depth + 1) for item in value]
114+
if value is None or type(value) is bool:
115+
return value
116+
if type(value) is str:
117+
if len(value) > MAX_SNAPSHOT_STRING_LENGTH:
118+
raise _error("bundle_snapshot_string_oversized")
99119
return value
100120
if type(value) is int:
101121
return _safe_int(value, "bundle_snapshot_invalid")
@@ -127,7 +147,7 @@ def _validate_snapshot(value: object) -> dict[str, object]:
127147
_string(value["source_snapshot_digest"], re.compile(r"^[0-9a-f]{64}$"), "bundle_snapshot_digest_invalid")
128148
_string(value["source_provenance"], re.compile(r"^[a-z][a-z0-9_]*$"), "bundle_snapshot_provenance_invalid")
129149
artifacts = value["source_artifacts"]
130-
if type(artifacts) is not list or not artifacts:
150+
if type(artifacts) is not list or not artifacts or len(artifacts) > MAX_SOURCE_ARTIFACTS:
131151
raise _error("bundle_snapshot_artifacts_invalid")
132152
for item in artifacts:
133153
if not isinstance(item, dict) or set(item) != _ARTIFACT_KEYS:
@@ -142,7 +162,10 @@ def _validate_snapshot(value: object) -> dict[str, object]:
142162
def _snapshot_bytes(value: Mapping[str, object]) -> bytes:
143163
try:
144164
tree = _snapshot_tree(value)
145-
return _canonical_json(_validate_snapshot(tree), "bundle_snapshot_invalid")
165+
result = _canonical_json(_validate_snapshot(tree), "bundle_snapshot_invalid")
166+
if len(result) > MAX_SNAPSHOT_BYTES:
167+
raise _error("bundle_snapshot_oversized")
168+
return result
146169
except WeeklyBundleError:
147170
raise
148171
except (TypeError, ValueError, UnicodeError, OverflowError, RecursionError):
@@ -152,6 +175,8 @@ def _snapshot_bytes(value: Mapping[str, object]) -> bytes:
152175
def _parse_snapshot(raw: bytes) -> dict[str, object]:
153176
if type(raw) is not bytes:
154177
raise _error("bundle_snapshot_invalid")
178+
if len(raw) > MAX_SNAPSHOT_BYTES:
179+
raise _error("bundle_snapshot_oversized")
155180

156181
def pairs(items: list[tuple[str, object]]) -> dict[str, object]:
157182
result: dict[str, object] = {}
@@ -170,7 +195,7 @@ def reject_constant(_: str) -> None:
170195
raise
171196
except (UnicodeError, json.JSONDecodeError, TypeError, ValueError, RecursionError):
172197
raise _error("bundle_snapshot_invalid") from None
173-
validated = _validate_snapshot(value)
198+
validated = _validate_snapshot(_snapshot_tree(value))
174199
if _canonical_json(validated, "bundle_snapshot_invalid") != raw:
175200
raise _error("bundle_snapshot_noncanonical")
176201
return validated
@@ -215,13 +240,15 @@ def __post_init__(self) -> None:
215240
@dataclass(frozen=True, slots=True)
216241
class BundleContext:
217242
run_id: str
243+
repository: str
218244
workflow_sha: str
219245
producer_sha: str
220246
artifact: ArtifactEvidence
221247
period_lock: PoliticalEventWeeklyPeriodLockV1
222248

223249
def __post_init__(self) -> None:
224250
_string(self.run_id, _RUN_ID_RE, "bundle_run_invalid")
251+
_string(self.repository, _REPOSITORY_RE, "bundle_repository_invalid")
225252
_string(self.workflow_sha, _SHA1_RE, "bundle_workflow_invalid")
226253
_string(self.producer_sha, _SHA1_RE, "bundle_producer_invalid")
227254
if type(self.artifact) is not ArtifactEvidence:
@@ -239,10 +266,23 @@ def __post_init__(self) -> None:
239266
@dataclass(frozen=True, slots=True)
240267
class RerunContext:
241268
original: BundleContext
269+
current_run_id: str
270+
current_repository: str
271+
current_workflow_sha: str
242272
current_run_attempt: int
243273

244274
def __post_init__(self) -> None:
245-
if type(self.current_run_attempt) is not int or self.current_run_attempt != 2:
275+
if (
276+
type(self.original) is not BundleContext
277+
or type(self.current_run_attempt) is not int
278+
or self.current_run_attempt != 2
279+
or type(self.current_run_id) is not str
280+
or self.current_run_id != self.original.run_id
281+
or type(self.current_repository) is not str
282+
or self.current_repository != self.original.repository
283+
or type(self.current_workflow_sha) is not str
284+
or self.current_workflow_sha != self.original.workflow_sha
285+
):
246286
raise _error("bundle_rerun_attempt_invalid")
247287

248288

@@ -275,6 +315,7 @@ def _manifest_bytes(context: BundleContext, lock_bytes: bytes, snapshot_bytes: b
275315
{
276316
"bundle_version": BUNDLE_VERSION,
277317
"artifact_name": context.artifact.name,
318+
"repository": context.repository,
278319
"source_run_id": context.run_id,
279320
"source_attempt": 1,
280321
"workflow_sha": context.workflow_sha,
@@ -315,6 +356,16 @@ def verify_period_bundle(bundle: BundleWire, expected: BundleContext) -> BundleW
315356
raise _error("bundle_input_invalid")
316357
if bundle.artifact != expected.artifact:
317358
raise _error("bundle_artifact_mismatch")
359+
if type(bundle.lock_bytes) is not bytes or len(bundle.lock_bytes) > MAX_LOCK_BYTES:
360+
raise _error("bundle_lock_oversized" if type(bundle.lock_bytes) is bytes else "bundle_lock_invalid")
361+
if type(bundle.snapshot_bytes) is not bytes or len(bundle.snapshot_bytes) > MAX_SNAPSHOT_BYTES:
362+
raise _error(
363+
"bundle_snapshot_oversized" if type(bundle.snapshot_bytes) is bytes else "bundle_snapshot_invalid"
364+
)
365+
if type(bundle.manifest_bytes) is not bytes or len(bundle.manifest_bytes) > MAX_MANIFEST_BYTES:
366+
raise _error(
367+
"bundle_manifest_oversized" if type(bundle.manifest_bytes) is bytes else "bundle_manifest_invalid"
368+
)
318369
try:
319370
parsed_lock = parse_period_lock_bytes(bundle.lock_bytes)
320371
except PeriodLockError:
@@ -338,6 +389,8 @@ def verify_period_bundle(bundle: BundleWire, expected: BundleContext) -> BundleW
338389
def _parse_manifest(raw: bytes) -> dict[str, object]:
339390
if type(raw) is not bytes:
340391
raise _error("bundle_manifest_invalid")
392+
if len(raw) > MAX_MANIFEST_BYTES:
393+
raise _error("bundle_manifest_oversized")
341394

342395
def pairs(items: list[tuple[str, object]]) -> dict[str, object]:
343396
result: dict[str, object] = {}
@@ -361,6 +414,13 @@ def pairs(items: list[tuple[str, object]]) -> dict[str, object]:
361414
def verify_rerun_context(bundle: BundleWire, rerun: RerunContext) -> BundleWire:
362415
if type(rerun) is not RerunContext:
363416
raise _error("bundle_rerun_context_invalid")
417+
if (
418+
rerun.current_run_id != rerun.original.run_id
419+
or rerun.current_repository != rerun.original.repository
420+
or rerun.current_workflow_sha != rerun.original.workflow_sha
421+
or rerun.current_run_attempt != 2
422+
):
423+
raise _error("bundle_rerun_attempt_invalid")
364424
return verify_period_bundle(bundle, rerun.original)
365425

366426

tests/test_weekly_period_bundle.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
from political_event_tracking_research.weekly_period_bundle import (
1010
BUNDLE_VERSION,
11+
MAX_SNAPSHOT_BYTES,
12+
MAX_SNAPSHOT_DEPTH,
13+
MAX_SNAPSHOT_STRING_LENGTH,
14+
MAX_SOURCE_ARTIFACTS,
1115
ArtifactEvidence,
1216
BundleContext,
1317
BundleWire,
@@ -74,6 +78,7 @@ def snapshot() -> dict[str, object]:
7478
def context(**overrides: object) -> BundleContext:
7579
values: dict[str, object] = {
7680
"run_id": RUN_ID,
81+
"repository": "QuantStrategyLab/PoliticalEventTrackingResearch",
7782
"workflow_sha": WORKFLOW_SHA,
7883
"producer_sha": PRODUCER_SHA,
7984
"artifact": ArtifactEvidence(
@@ -149,10 +154,36 @@ def test_context_mismatch_fails_closed(field: str) -> None:
149154

150155
def test_same_run_attempt_two_context_is_explicit() -> None:
151156
bundle = build_period_bundle(context(), lock(), snapshot())
152-
assert verify_rerun_context(bundle, RerunContext(context(), current_run_attempt=2)) == bundle
157+
rerun = RerunContext(
158+
context(),
159+
current_run_id=RUN_ID,
160+
current_repository="QuantStrategyLab/PoliticalEventTrackingResearch",
161+
current_workflow_sha=WORKFLOW_SHA,
162+
current_run_attempt=2,
163+
)
164+
assert verify_rerun_context(bundle, rerun) == bundle
165+
for field, value in (
166+
("current_run_id", "29420000002"),
167+
("current_repository", "QuantStrategyLab/Other"),
168+
("current_workflow_sha", "1" * 40),
169+
):
170+
with pytest.raises(WeeklyBundleError, match="bundle_rerun_attempt_invalid"):
171+
RerunContext(
172+
context(),
173+
current_run_id=value if field == "current_run_id" else RUN_ID,
174+
current_repository=value if field == "current_repository" else "QuantStrategyLab/PoliticalEventTrackingResearch",
175+
current_workflow_sha=value if field == "current_workflow_sha" else WORKFLOW_SHA,
176+
current_run_attempt=2,
177+
)
153178
for attempt in (1, 3):
154179
with pytest.raises(WeeklyBundleError, match="bundle_rerun_attempt_invalid"):
155-
verify_rerun_context(bundle, RerunContext(context(), current_run_attempt=attempt))
180+
RerunContext(
181+
context(),
182+
current_run_id=RUN_ID,
183+
current_repository="QuantStrategyLab/PoliticalEventTrackingResearch",
184+
current_workflow_sha=WORKFLOW_SHA,
185+
current_run_attempt=attempt,
186+
)
156187

157188

158189
def test_missing_or_multiple_bundle_collection_fails_closed() -> None:
@@ -174,7 +205,7 @@ def test_missing_or_multiple_bundle_collection_fails_closed() -> None:
174205
)
175206
def test_artifact_metadata_is_strict(values: tuple[object, ...]) -> None:
176207
with pytest.raises(WeeklyBundleError):
177-
BundleContext(RUN_ID, WORKFLOW_SHA, PRODUCER_SHA, ArtifactEvidence(*values), lock())
208+
BundleContext(RUN_ID, "QuantStrategyLab/PoliticalEventTrackingResearch", WORKFLOW_SHA, PRODUCER_SHA, ArtifactEvidence(*values), lock())
178209

179210

180211
@pytest.mark.parametrize("mutation", ["unknown", "missing", "duplicate", "unsafe_int"])
@@ -198,12 +229,41 @@ def test_snapshot_wire_shape_is_strict(mutation: str) -> None:
198229
build_period_bundle(context(), lock(), base)
199230

200231

201-
def test_unexpected_runtime_error_is_not_broad_caught(monkeypatch: pytest.MonkeyPatch) -> None:
232+
@pytest.mark.parametrize("kind", ["oversized_bytes", "deep", "artifacts", "string"])
233+
def test_snapshot_resource_bounds_fail_closed(kind: str) -> None:
234+
bundle = build_period_bundle(context(), lock(), snapshot())
235+
if kind == "oversized_bytes":
236+
wire = BundleWire(
237+
bundle.lock_bytes,
238+
b"{" + b"a" * MAX_SNAPSHOT_BYTES,
239+
bundle.manifest_bytes,
240+
bundle.artifact,
241+
)
242+
else:
243+
value = snapshot()
244+
if kind == "deep":
245+
nested: object = "x"
246+
for _ in range(MAX_SNAPSHOT_DEPTH + 1):
247+
nested = [nested]
248+
value["source_provenance"] = nested
249+
elif kind == "artifacts":
250+
value["source_artifacts"] = value["source_artifacts"] * (MAX_SOURCE_ARTIFACTS + 1)
251+
else:
252+
value["source_snapshot_id"] = "a" * (MAX_SNAPSHOT_STRING_LENGTH + 1)
253+
with pytest.raises(WeeklyBundleError, match="bundle_snapshot_"):
254+
build_period_bundle(context(), lock(), value)
255+
return
256+
with pytest.raises(WeeklyBundleError, match="bundle_snapshot_oversized"):
257+
verify_period_bundle(wire, context())
258+
259+
260+
@pytest.mark.parametrize("exception", [RuntimeError, SystemExit, MemoryError])
261+
def test_unexpected_exceptions_are_not_broad_caught(monkeypatch: pytest.MonkeyPatch, exception: type[BaseException]) -> None:
202262
import political_event_tracking_research.weekly_period_bundle as module
203263

204264
def fail(*_: object, **__: object) -> bytes:
205-
raise RuntimeError("programming failure")
265+
raise exception("programming failure")
206266

207267
monkeypatch.setattr(module.json, "dumps", fail)
208-
with pytest.raises(RuntimeError, match="programming failure"):
268+
with pytest.raises(exception, match="programming failure"):
209269
build_period_bundle(context(), lock(), snapshot())

0 commit comments

Comments
 (0)