Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- Quality dependency validation now canonicalizes package names using the packaging project identity rules and rejects duplicate package-version stanzas, so equivalent spellings cannot bypass duplicate detection or split a required wheel hash across declarations.
- Extended the hash-locked PostgreSQL quality dependency set with the CPython 3.14 `psycopg-binary` wheel required by the central coverage runner; the repository contract now keeps that interpreter boundary executable.
- Added the deterministic bank-reconciliation proposal engine from ADR 0054: stable provider, end-to-end, and account-servicer references take precedence; exact decimal amount and currency evidence must agree; exact-money plus bounded-date fallback is permitted only for one unique candidate; and ambiguity, conflicts, out-of-window evidence, or no candidate produce explicit abstention with an operator next action. Match proposals are read-only evidence with no automatic journal posting; any accounting adjustment must re-enter the existing journal command boundary.
- Added the exact book-to-bank reconciliation bridge with exact Decimal equations, fail-closed one minor unit differences, statement-population and book-population provenance, no automatic journal posting, and ADR 0054.
- Added a read-only reconciliation close-review projection over deterministic decisions and the exact bridge: controllers receive exact bank/book/reconciled/outstanding/unexplained Decimal values, immutable run and population provenance, preceding-run deltas, unresolved statement-entry references, and an explicit next action. JSON and CSV exports preserve money as decimal strings. `suitable_for_period_close_review` is evidence eligibility only; the projection cannot approve reconciliation, close a period, or post a journal. ADR 0054 records the authority boundary.
Expand Down
4 changes: 2 additions & 2 deletions docs/adr/0048-reproducible-package-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The Accounting Foundation CI separates **premerge exact-source evidence** from *
2. The build/test job has `contents: read` only. It does not receive OIDC, attestation, or artifact-metadata write authority while executing repository-controlled tests, validation, package build code, or smoke tests.
3. The job derives `SOURCE_DATE_EPOCH` from that verified commit timestamp and builds the wheel twice from clean build metadata. The two SHA-256 digests must be identical. A non-reproducible wheel fails the job.
4. `scripts/generate_supply_chain_evidence.py` emits deterministic `sbom.spdx.json` and `source-provenance.json`. The source-provenance manifest binds the exact verified source SHA and source timestamp to the wheel file/digest, SPDX SBOM file/digest, repository, and build definition. The generator fails closed if runtime dependencies are introduced before explicit dependency relationships are represented in the SBOM.
5. `SHA256SUMS` covers the wheel, the SPDX SBOM, and `source-provenance.json`. The workflow verifies those checksums, then installs the rebuilt wheel with `--require-hashes` from a requirements line that carries the measured `--hash=sha256:` digest (pip does not accept `--hash` as a CLI option) before the isolated smoke test. All four files are retained together in the same-head Actions artifact.
5. `SHA256SUMS` covers the wheel, the SPDX SBOM, and `source-provenance.json`. The workflow verifies those checksums, then installs the rebuilt wheel with `--require-hashes` from a requirements line that carries the measured `--hash=sha256:` digest (pip does not accept `--hash` as a CLI option) before the isolated smoke test. The repository quality validator canonicalizes package names using the packaging project identity rules before binding versions, duplicate stanzas, and wheel hashes, so equivalent spellings cannot split or bypass a lock. All four files are retained together in the same-head Actions artifact.
6. On `pull_request`, `exact-head-dependency-diff` checks out `pull_request.head.sha`, independently fetches the live base branch tip, records the live base/head identities, dependency-manifest diff and manifest SHA-256 values, and fails if the live base is not an ancestor of the exact head. It scans the complete hash-locked `requirements-quality.txt` with a digest-pinned OSV-Scanner container. A vulnerability finding, scanner failure, unavailable scanner/evidence path, wrong head identity or missing expected evidence is non-passing. Because the bootstrap base has no dependency manifests, the foundation PR records those files as additions and requires the complete exact-head dependency set to be vulnerability-free rather than treating an empty base scan as evidence. The SHA-named artifact retains the identity/diff record and OSV JSON result.
7. On `pull_request`, the deterministic source-provenance manifest is the authoritative artifact-to-exact-PR-head evidence. No job with `id-token: write`, `attestations: write`, or `artifact-metadata: write` executes for that event.
8. On `push` to `develop` or `main`, a distinct `integrated-attestations` job runs only after `accounting-foundation` succeeds. It downloads the immutable SHA-named package-evidence artifact, re-verifies `SHA256SUMS`, verifies `source-provenance.json.source_sha == github.sha`, and only then receives the OIDC/attestation permissions required by the full-SHA-pinned `actions/attest` action to create build-provenance and SPDX-SBOM attestations. Those integrated-head attestations are mandatory release evidence.
Expand Down Expand Up @@ -52,4 +52,4 @@ Google Open Source Security Team. (2026b). *OSV-Scanner: Supported artifacts and

SPDX Workgroup. (n.d.). *SPDX specifications*. https://spdx.dev/use/specifications/

Supply-chain Levels for Software Artifacts. (2026). *SLSA specification, version 1.2*. https://slsa.dev/spec/v1.2/
Supply-chain Levels for Software Artifacts. (2026). *SLSA specification, version 1.2*. https://slsa.dev/spec/v1.2/
2 changes: 1 addition & 1 deletion docs/doctoring/STANDARD_TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
| RFC 9112 | The standalone HTTP/1.1 command boundary deliberately does not implement transfer coding: any request carrying `Transfer-Encoding` fails closed with HTTP 400 and connection close rather than being combined with a `Content-Length` interpretation. A valid `Content-Length` is an exact octet contract; premature EOF/short reads are incomplete messages, fail with HTTP 400, and close the connection before JSON/domain processing. This prevents ambiguous message boundaries from becoming request-smuggling or valid-prefix acceptance paths | `JournalProposalHandler._read_body`, HTTP request-boundary RED/GREEN regressions, RFC 9112 §§6.2–6.3 and §8 |
| RFC 9562 | New persistence identifiers use UUIDv7 | Initial migration |
| CloudEvents 1.0.2 | Commit authoritative events through a transactional outbox and replay by event identity | Outbox table and architecture |
| SLSA 1.2 / SPDX 2.3 / GitHub artifact attestations | Exact-head package evidence builds the wheel twice from a source-derived `SOURCE_DATE_EPOCH`, requires byte-identical SHA-256 digests, emits a deterministic SPDX 2.3 SBOM plus `source-provenance.json`, and makes `SHA256SUMS` cover the wheel, SBOM and source-provenance manifest. After checksum verification, the rebuilt wheel is installed with `--require-hashes` from a requirements line that carries the measured `--hash=sha256:` digest. The intermediate public-API smoke test imports the source tree over `PYTHONPATH` instead of an unhashed editable install. The manifest binds the verified source SHA to the wheel digest and SBOM digest before merge. Pull-request-controlled build/test code runs with `contents: read` only; OIDC, attestation and artifact-metadata write permissions are isolated in a distinct push-only `integrated-attestations` job. That job depends on the successful foundation build, downloads the immutable SHA-named evidence bundle, re-verifies checksums and `source_sha == github.sha`, and only then creates GitHub OIDC-backed signed provenance and SBOM attestations on integrated `develop`/`main` heads. A new runtime dependency fails closed until the SBOM generator represents its dependency relationship. This is evidence readiness, not a claimed SLSA level or certification | Accounting Foundation CI, `scripts/generate_supply_chain_evidence.py`, supply-chain evidence tests, GitHub workflow-permissions/OIDC/artifact-attestation guidance, and ADR 0048 |
| SLSA 1.2 / SPDX 2.3 / GitHub artifact attestations | Exact-head package evidence builds the wheel twice from a source-derived `SOURCE_DATE_EPOCH`, requires byte-identical SHA-256 digests, emits a deterministic SPDX 2.3 SBOM plus `source-provenance.json`, and makes `SHA256SUMS` cover the wheel, SBOM and source-provenance manifest. After checksum verification, the rebuilt wheel is installed with `--require-hashes` from a requirements line that carries the measured `--hash=sha256:` digest. The quality validator canonicalizes package names using packaging project identity rules before binding versions, duplicate stanzas, and wheel hashes. The intermediate public-API smoke test imports the source tree over `PYTHONPATH` instead of an unhashed editable install. The manifest binds the verified source SHA to the wheel digest and SBOM digest before merge. Pull-request-controlled build/test code runs with `contents: read` only; OIDC, attestation and artifact-metadata write permissions are isolated in a distinct push-only `integrated-attestations` job. That job depends on the successful foundation build, downloads the immutable SHA-named evidence bundle, re-verifies checksums and `source_sha == github.sha`, and only then creates GitHub OIDC-backed signed provenance and SBOM attestations on integrated `develop`/`main` heads. A new runtime dependency fails closed until the SBOM generator represents its dependency relationship. This is evidence readiness, not a claimed SLSA level or certification | Accounting Foundation CI, `scripts/generate_supply_chain_evidence.py`, supply-chain evidence tests, GitHub workflow-permissions/OIDC/artifact-attestation guidance, and ADR 0048 |
| OSV-Scanner / OSV.dev vulnerability data | Pull-request dependency evidence is tied to the immutable PR head and an independently fetched live base tip. The gate records dependency-manifest diffs and SHA-256 values, rejects stale/non-ancestor base identity, and scans the complete hash-locked exact-head Python dependency set with a digest-pinned OSV-Scanner image. A known vulnerability, scanner failure, skipped/unavailable evidence path or wrong checkout identity is non-passing; aggregate organization workflow success cannot substitute for an unexecuted dependency-review step | `exact-head-dependency-diff` CI job, `tests/test_dependency_review_contract.py`, OSV-Scanner source/lockfile guidance, and ADR 0048 |
| AICPA Trust Services Criteria (SOC 2) | Auditors read an append-only history of posted, reversed, and closed facts from existing `outbox_event` rows, including already-published rows, without marking publish. Controllers also list stored `journal_reversal` lineage and durable hard-close receipts over HTTP without SQL. A HomeTax filing command fail-closes and persists a rejected receipt when the VAT register or the purpose-limited HomeTax credential is missing, and this slice never claims `transmitted` | HTTP audit-event history, HTTP journal-reversal list, HTTP period-close list, HTTP fail-closed HomeTax submission, ADR 0027, ADR 0029, ADR 0030, and ADR 0046 |
| W3C PROV-O | Preserve entity, activity, agent, derivation, and attribution references across source proposal, posting, and append-only journal reversal lineage. The in-memory posting oracle scopes those identities by tenant and refuses to overwrite a posted `proposal_id`. Checked-in migrations cannot `UPDATE` or `DELETE` `general_journal` or `journal_entry_line` | Source-reference and receipt contracts, HTTP journal-reversal list, repository migration validation, ADR 0003, and ADR 0029 |
Expand Down
5 changes: 3 additions & 2 deletions requirements-quality.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hash-locked quality and packaging tools for --require-hashes --only-binary=:all:.
# coverage 7.15.4: every CPython 3.13 wheel plus py3-none-any.
# psycopg 3.3.4 plus every CPython 3.13 psycopg-binary wheel.
# psycopg 3.3.4 plus every CPython 3.13 wheel and the CPython 3.14 manylinux2014 x86_64 wheel.
coverage==7.15.4 \
--hash=sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921 \
--hash=sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e \
Expand Down Expand Up @@ -37,4 +37,5 @@ psycopg-binary==3.3.4 \
--hash=sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf \
--hash=sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260 \
--hash=sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf \
--hash=sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38
--hash=sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38 \
--hash=sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277
38 changes: 31 additions & 7 deletions scripts/validate_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,18 @@
)
TWO_WORD_SNAKE_PATTERN = re.compile(r"^[a-z][a-z0-9]*_[a-z0-9_]+$")
HASH_TOKEN_PATTERN = re.compile(r"--hash=sha256:([0-9a-f]{64})")
PINNED_REQUIREMENT_PATTERN = re.compile(r"^([A-Za-z0-9][A-Za-z0-9._-]*)==")
PINNED_REQUIREMENT_PATTERN = re.compile(
r"^([A-Za-z0-9][A-Za-z0-9._-]*)==([^\s\\]+)"
)
COVERAGE_UNIVERSAL_WHEEL_HASH = (
"964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84"
)
COVERAGE_CP313_MANYLINUX_X86_64_WHEEL_HASH = (
"12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4"
)
PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH = (
"8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277"
)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
SCHEMA_NAME_PATTERN = re.compile(
r"\bCREATE\s+SCHEMA(?:\s+IF\s+NOT\s+EXISTS)?\s+([A-Za-z_][A-Za-z0-9_]*)",
re.IGNORECASE,
Expand Down Expand Up @@ -334,7 +339,9 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]:
"""Require hash-locked coverage wheels and the no-build-isolation packaging backend."""
errors: list[str] = []
package_hashes: dict[str, set[str]] = {}
current_package: str | None = None
package_version_hashes: dict[tuple[str, str], set[str]] = {}
seen_requirements: set[tuple[str, str]] = set()
current_requirement: tuple[str, str] | None = None

for raw_line in requirements_text.splitlines():
line = raw_line.strip()
Expand All @@ -346,15 +353,26 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]:
name_match = PINNED_REQUIREMENT_PATTERN.match(requirement_line)
if name_match is None:
errors.append(f"unrecognized quality dependency line: {requirement_line}")
current_package = None
current_requirement = None
else:
current_package = name_match.group(1).lower()
package_hashes.setdefault(current_package, set())
current_requirement = (
re.sub(r"[-_.]+", "-", name_match.group(1)).lower(),
Comment thread
seonghobae marked this conversation as resolved.
name_match.group(2),
)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
if current_requirement in seen_requirements:
errors.append(
"quality dependency stanza appears more than once: "
f"{current_requirement[0]}=={current_requirement[1]}"
)
seen_requirements.add(current_requirement)
package_hashes.setdefault(current_requirement[0], set())
package_version_hashes.setdefault(current_requirement, set())
if hashes:
if current_package is None:
if current_requirement is None:
errors.append("hash lock is not attached to a quality dependency")
else:
package_hashes.setdefault(current_package, set()).update(hashes)
package_hashes[current_requirement[0]].update(hashes)
package_version_hashes[current_requirement].update(hashes)
Comment thread
seonghobae marked this conversation as resolved.

if not any(package_hashes.values()):
errors.append("quality dependencies must be hash locked")
Expand Down Expand Up @@ -404,6 +422,12 @@ def validate_quality_requirements(requirements_text: str) -> tuple[str, ...]:
)
elif not package_hashes["psycopg-binary"]:
errors.append("psycopg-binary must be hash locked")
elif PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH not in package_version_hashes.get(
("psycopg-binary", "3.3.4"), set()
):
errors.append(
"psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash"
)

return tuple(errors)

Expand Down
59 changes: 57 additions & 2 deletions tests/test_repository_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
APPEND_ONLY_JOURNAL_MUTATION_ERROR,
COVERAGE_CP313_MANYLINUX_X86_64_WHEEL_HASH,
COVERAGE_UNIVERSAL_WHEEL_HASH,
PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH,
main,
find_mutable_action_references,
find_placeholder_tokens,
Expand Down Expand Up @@ -366,7 +367,10 @@ def test_quality_requirements_require_ci_coverage_wheels_and_packaging_backend(
)
self.assertEqual(
universal_only,
("coverage must pin the CPython 3.13 manylinux x86_64 wheel hash",),
(
"coverage must pin the CPython 3.13 manylinux x86_64 wheel hash",
"psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash",
),
)

orphan_and_unpinned = validate_quality_requirements(
Expand Down Expand Up @@ -405,11 +409,62 @@ def test_quality_requirements_require_ci_coverage_wheels_and_packaging_backend(
"psycopg==3.3.4 --hash=sha256:"
"b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a\n"
"psycopg-binary==3.3.4 --hash=sha256:"
"c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3\n"
"c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3 "
f"--hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}\n"
"# comment and blank lines are ignored\n\n"
)
self.assertEqual(inline_valid, ())

def test_quality_requirements_include_central_python_314_psycopg_wheel(self) -> None:
"""The central coverage image must install the native PostgreSQL wheel."""
quality_requirements = (ROOT / "requirements-quality.txt").read_text(
encoding="utf-8"
)
psycopg_binary_stanza = re.search(
r"(?ms)^psycopg-binary==3\.3\.4 \\\n"
r"(?: --hash=sha256:[0-9a-f]{64}(?: \\\n|\n))+",
quality_requirements,
)
self.assertIsNotNone(psycopg_binary_stanza)
self.assertIn(
f"--hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}",
psycopg_binary_stanza.group(0) if psycopg_binary_stanza else "",
)
without_cp314 = quality_requirements.replace(
f" --hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}\n",
"",
)
self.assertIn(
"psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash",
validate_quality_requirements(without_cp314),
)
Comment thread
seonghobae marked this conversation as resolved.
wrong_version = (
without_cp314
+ "\npsycopg-binary==3.3.5 \\\n"
f" --hash=sha256:{PSYCOPG_BINARY_CP314_MANYLINUX_X86_64_WHEEL_HASH}\n"
)
self.assertIn(
"psycopg-binary must pin the CPython 3.14 manylinux x86_64 wheel hash",
validate_quality_requirements(wrong_version),
)

duplicate_version = quality_requirements + (
"\npsycopg-binary==3.3.4\n"
)
self.assertIn(
"quality dependency stanza appears more than once: psycopg-binary==3.3.4",
validate_quality_requirements(duplicate_version),
)

for equivalent_name in ("psycopg_binary", "psycopg.binary"):
equivalent_duplicate = quality_requirements + (
f"\n{equivalent_name}==3.3.4\n"
)
self.assertIn(
"quality dependency stanza appears more than once: psycopg-binary==3.3.4",
validate_quality_requirements(equivalent_duplicate),
)

def test_append_only_journal_sql_rejects_update_and_delete(self) -> None:
"""Migrations cannot UPDATE or DELETE posted journal tables."""
forbidden_statements = (
Expand Down
Loading