-
Notifications
You must be signed in to change notification settings - Fork 0
feat(kernel): hash-linked event chain with anchored doctor gate (v0.10.0, slice 1/5) #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
e4d308a
feat(chain): canonical-JSON event hashing (pure stdlib leaf module)
SollanSystems f054e25
feat(chain): incremental link verification + pure verify_chain over e…
SollanSystems 4d96499
feat(events): store generations — chain columns + user_version, share…
SollanSystems 3e1404f
feat(events): store-computed hash chain on append; typed operational …
SollanSystems 1bcaee3
feat(schema): optional event@1 chain fields with structural-fallback …
SollanSystems ff15e6b
feat(migrate): explicit legacy-store chain migration verb
SollanSystems d25aa58
feat(reducer): enforce hash chain at fold time via typed ChainBreakError
SollanSystems 5dad893
feat(runtime): chain surfaced through status/replay/doctor/run; enfor…
SollanSystems 4fb7943
fix(runner): D4 retry parity on the run/simulate read path + typed de…
SollanSystems 23a67ee
feat(doctor): --expect-chain-head anchor gate, downgrade cross-check,…
SollanSystems d0d5578
test(chain): adversarial coverage + four pinned honest limitations (r…
SollanSystems 6378361
test(chain): pin event-store cleanliness flags on the full-rewrite ho…
SollanSystems d25a9d1
test(zero-writes): read verbs proven side-effect-free on both store g…
SollanSystems 3a0d2b5
feat(action): record the chain head on every run and optionally enfor…
SollanSystems 2cdf6d8
docs(contract): normative chain canonicalization, conformance vectors…
SollanSystems d7ccdce
fix(review): whole-branch fix wave — claims-accuracy tightening, test…
SollanSystems 5aa55a2
merge: origin/main (#80 sidecar fix) into feat/event-chain
SollanSystems File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| """Pure hash-chain canonicalization and verification for event@1 records. | ||
|
|
||
| Stdlib-only and import-free of other loop modules: verify_chain() must work over | ||
| any ordered event list (a SQLite read or a JSONL export) so third parties can | ||
| re-verify a chain without this package's store code. Canonical form is | ||
| json.dumps(sort_keys, separators=(",",":"), ensure_ascii=False, allow_nan=False) | ||
| encoded UTF-8 — pinned normatively in reference/repo-os-contract.md #16. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import hashlib | ||
| import json | ||
| from typing import Any, Iterable, Mapping | ||
|
|
||
| _PREIMAGE_FIELDS = ( | ||
| "schema", "run_id", "sequence", "event_id", "type", "actor", "ts", | ||
| "causation_id", "correlation_id", "payload", "artifact_hashes", | ||
| "prev_event_hash", | ||
| ) | ||
|
|
||
|
|
||
| class ChainHashError(ValueError): | ||
| """A value cannot be canonically hashed (non-JSON type, non-finite float, lone surrogate).""" | ||
|
|
||
|
|
||
| def canonical_json(value: Any) -> str: | ||
| try: | ||
| text = json.dumps(value, sort_keys=True, separators=(",", ":"), | ||
| ensure_ascii=False, allow_nan=False) | ||
| except (TypeError, ValueError) as exc: | ||
| raise ChainHashError(f"value is not canonically serializable: {exc}") from exc | ||
| try: | ||
| text.encode("utf-8") | ||
| except UnicodeEncodeError as exc: | ||
| raise ChainHashError(f"value contains a lone surrogate: {exc}") from exc | ||
| return text | ||
|
|
||
|
|
||
| def compute_event_hash(record: Mapping[str, Any]) -> str: | ||
| preimage = {field: record.get(field) for field in _PREIMAGE_FIELDS} | ||
| return hashlib.sha256(canonical_json(preimage).encode("utf-8")).hexdigest() | ||
|
|
||
|
|
||
| def link_issue(record: Mapping[str, Any], prev_head: Mapping[str, Any] | None) -> str | None: | ||
| """One incremental chain check; None means record legally extends prev_head.""" | ||
| sequence = record.get("sequence") | ||
| stored = record.get("event_hash") | ||
| if stored is None: | ||
| if prev_head is None: | ||
| return None | ||
| return (f"unchained event after chained prefix at sequence {sequence!r} " | ||
| "(a pre-0.10.0 writer appended to a chained store, or the row was tampered)") | ||
| expected_prev = prev_head["event_hash"] if prev_head is not None else None | ||
| if record.get("prev_event_hash") != expected_prev: | ||
| return f"prev_event_hash mismatch at sequence {sequence!r}" | ||
| try: | ||
| recomputed = compute_event_hash(record) | ||
| except ChainHashError as exc: | ||
| return f"unhashable record at sequence {sequence!r}: {exc}" | ||
| if recomputed != stored: | ||
| return f"event_hash mismatch at sequence {sequence!r}" | ||
| return None | ||
|
|
||
|
|
||
| def verify_chain(events: Iterable[Mapping[str, Any]], *, expected_head: str | None = None) -> dict[str, Any]: | ||
| """Verify a COMPLETE run stream's hash chain (sequence 0 onward); pure, I/O-free.""" | ||
| issues: list[str] = [] | ||
| unchained_prefix = 0 | ||
| chained_events = 0 | ||
| head: dict[str, Any] | None = None | ||
| for record in events: | ||
| issue = link_issue(record, head) | ||
| if issue is not None: | ||
| issues.append(issue) | ||
| break | ||
| if record.get("event_hash") is None: | ||
| unchained_prefix += 1 | ||
| continue | ||
| chained_events += 1 | ||
| head = {"sequence": record.get("sequence"), "event_hash": record["event_hash"]} | ||
| if expected_head is not None and not issues: | ||
| if head is None: | ||
| issues.append("expected chain head, but the stream has no chained events") | ||
| elif head["event_hash"] != expected_head: | ||
| issues.append(f"chain head {head['event_hash']} does not match expected {expected_head}") | ||
| return {"ok": not issues, "issues": issues, "chained_events": chained_events, | ||
| "unchained_prefix": unchained_prefix, "head": head} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
verify_chain()never checks that the first event has sequence 0 or that later sequences are contiguous. Consequently, deleting rows from an unchained legacy prefix—or passing a self-consistent genesis whose sequence is greater than zero—still returnsok: trueand underreportsunchained_prefix, even though this API is documented as verifying a complete run stream. Validate sequence continuity while iterating so incomplete exports cannot be certified.Useful? React with 👍 / 👎.