Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a25e35d
feat(release): replay reproducible evidence on current main
seonghobae Aug 12, 2026
7658431
test(release): bind uv pin to repository contract
seonghobae Aug 12, 2026
d40fdf1
fix(release): compose current toolchain contracts
seonghobae Aug 12, 2026
c02ce17
merge: reconcile protected main into release evidence
seonghobae Aug 12, 2026
dcc8b04
docs(release): record descriptor-bound TOCTOU hardening
seonghobae Aug 12, 2026
a398fe8
test(config): restore malformed boolean regression
seonghobae Aug 12, 2026
bdc7fdc
fix(release): preserve current-main lifecycle contracts
seonghobae Aug 12, 2026
d6d8059
docs(release): align manifest durability and uv frontend
seonghobae Aug 12, 2026
b91c72a
test(release): derive uv frontend version from config
seonghobae Aug 12, 2026
9a76488
docs(release): follow configured uv frontend pin
seonghobae Aug 12, 2026
52c642d
docs(release): document full entry identity snapshots
seonghobae Aug 12, 2026
2257667
test(release): make in-place mutation proof deterministic
seonghobae Aug 12, 2026
a8027df
test(release): record descriptor close instead of reusing fd
seonghobae Aug 12, 2026
54df5db
docs(release): align hardening plan with replacement PR
seonghobae Aug 12, 2026
19c94d8
docs(release): align hardening design with replacement
seonghobae Aug 12, 2026
5a24c98
merge: reconcile protected main into release evidence
seonghobae Aug 12, 2026
5b0ec13
merge: preserve provider retention from protected main
seonghobae Aug 12, 2026
3db3c9a
Merge branch 'main' into feat/release-evidence-current-main
opencode-agent[bot] Aug 12, 2026
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
133 changes: 133 additions & 0 deletions .github/workflows/release-acceptance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: Release Acceptance

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- .github/workflows/release-acceptance.yml
- pg_llm_batch/**
- tests/**
- docs/**
- docker/**
- AGENTS.md
- ARCHITECTURE.md
- CHANGELOG.md
- CLAUDE.md
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- README.md
- SECURITY.md
- pyproject.toml
- uv.lock
- LICENSE
- NOTICE

permissions:
contents: read

concurrency:
group: release-acceptance-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
reproducible-distributions:
name: Reproducible wheel and sdist
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Harden runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit

- name: Checkout exact pull request head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"

- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.12.3"
prune-cache: true

- name: Install locked verification dependencies
run: uv sync --locked

- name: Derive exact-source reproducibility environment
shell: bash
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}"
source_date_epoch="$(git show -s --format=%ct HEAD)"
case "$source_date_epoch" in
''|*[!0-9]*) echo "invalid exact-head commit timestamp" >&2; exit 1 ;;
esac
{
echo "SOURCE_DATE_EPOCH=$source_date_epoch"
echo "PYTHONHASHSEED=0"
echo "TZ=UTC"
echo "LC_ALL=C.UTF-8"
} >> "$GITHUB_ENV"

- name: Materialize two clean exact-head source trees
shell: bash
run: |
set -euo pipefail
mkdir source-first source-second
git archive --format=tar HEAD | tar -xf - -C source-first
git archive --format=tar HEAD | tar -xf - -C source-second

- name: Build first distribution set
shell: bash
run: |
set -euo pipefail
uv build --no-sources --out-dir dist-first --no-create-gitignore source-first

- name: Build second distribution set
shell: bash
run: |
set -euo pipefail
uv build --no-sources --out-dir dist-second --no-create-gitignore source-second

- name: Verify artifact identity and write bounded evidence
shell: bash
env:
SOURCE_COMMIT: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
uv run python - <<'PY'
import os
import tomllib
from pathlib import Path

from pg_llm_batch.release_evidence import (
verify_reproducible_release,
write_release_manifest,
)

project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
manifest = verify_reproducible_release(
"dist-first",
"dist-second",
distribution_name=project["name"],
version=project["version"],
source_commit=os.environ["SOURCE_COMMIT"],
source_date_epoch=int(os.environ["SOURCE_DATE_EPOCH"]),
)
write_release_manifest(manifest, "release-evidence/release-manifest.json")
PY

- name: Preserve bounded release evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-evidence-${{ github.event.pull_request.head.sha }}
path: release-evidence/release-manifest.json
if-no-files-found: error
retention-days: 14
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Read-only exact-head release acceptance that builds wheel and source distribution artifacts twice from clean Git archives, proves byte-identical SHA-256 identity, records bounded canonical evidence, and keeps publication and attestation authority separate.
- Optional bounded provider output/error-file lifetime controls for batch creation, with exact local validation before credential resolution and backward-compatible omission for provider-neutral callers.
- Trusted tenant-scoped durable lifecycle identities for shared-table MSA deployments, including `TenantDurableBatchAPIClient`, tenant-qualified persistence and read helpers, transaction-local PostgreSQL context, forced default-deny row-level security, and explicit standalone compatibility.
- Independent 1 MiB bounded-stream decoding for Files and Batches control-plane JSON before strict UTF-8 and object parsing.
Expand Down Expand Up @@ -37,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Bound release-artifact traversal, hashing, identity validation, and manifest publication to descriptor-relative no-follow operations with bounded enumeration, atomic replacement, and file plus parent-directory synchronization so symlink or same-name replacement cannot convert a verified artifact set into different release evidence; this closes the documented time-of-check/time-of-use boundary while version `0.1.0` remains unchanged.
- Rejected non-callable standalone and tenant lifecycle recorders or observation reservers during client construction, before any provider operation can succeed without a usable persistence path.
- Made the tenant lifecycle migration atomic across owner-enforcement relaxation, legacy-row backfill, constraint replacement, and forced-RLS restoration so psql autocommit cannot commit an intermediate owner-bypass state.
- Bootstrap DSN and Fernet-key source selection now consults process environment
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
# pg-llm-batch component image: CLI + /healthz server.
FROM ghcr.io/astral-sh/uv:0.12.0@sha256:606e70c71c852d03f611b1e56a195d08648507018a7057fab82c4974c4eae105 AS uv
FROM ghcr.io/astral-sh/uv:0.12.1@sha256:cf4eedcaa81655197f625739489effcbe71b61ceb1506f332c3facae5deceded AS uv

FROM python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6 AS builder

WORKDIR /app

COPY --from=uv /uv /uvx /bin/
COPY pyproject.toml uv.lock README.md ./
COPY pyproject.toml uv.lock README.md LICENSE NOTICE ./
COPY pg_llm_batch ./pg_llm_batch
RUN uv sync --frozen --no-dev --no-editable

Expand Down
201 changes: 201 additions & 0 deletions docs/adr/0003-reproducible-release-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# ADR 0003: Reproducible release evidence before publication

- **Status:** Proposed
- **Date:** 2026-08-06
- **Decision owners:** ContextualWisdomLab maintainers

## Context

The ordinary CI workflow proves tests, coverage, docstrings, lint, packaging, and
container construction. It does not prove that two clean exact-head builds emit
the same Python distribution bytes. A buyer, operator, or release reviewer
therefore cannot distinguish a reproducible package from an artifact affected by
ambient timestamps, a dirty source tree, hidden generated files, or a changed
build input.

Artifact provenance and publication are separate trust decisions. A signed
attestation can accurately describe a non-reproducible build, while a
reproducibility check does not authenticate the builder or publish an artifact.
Both properties matter, but they require different permissions and review gates.

PEP 517 build-system requirements are resolved independently from ordinary
project dependencies: build-system requirements are not pinned by `uv.lock`.
The acceptance path therefore pins the build frontend to `uv` 0.12.3 and the
backend requirement to `uv_build==0.12.1`. The exact frontend can use its
compatible bundled backend, while external PEP 517 frontends are constrained to
the same backend version instead of silently selecting a later patch release.

The artifact verifier must fail on a third directory entry without materializing
an unbounded output directory. Because directory iteration order is not a stable
evidence source, count failures must not embed whichever filenames happened to
appear in the bounded sample.

The canonical evidence writer runs against pull-request-controlled workspace
paths. Rejecting a symlink and then later opening the same pathname is a
CWE-367 time-of-check/time-of-use boundary: a same-UID process can rename a
checked parent and put a symlink at its former pathname before temporary-file
creation. Repeating a lexical check, shortening the interval, or resolving the
pathname does not bind later operations to the object that was checked.

## Decision

Every release-relevant pull request runs a read-only acceptance workflow that:

1. checks out the exact pull-request head with persisted credentials disabled;
2. derives `SOURCE_DATE_EPOCH` from that exact commit;
3. creates two clean source trees from the same Git object;
4. performs two clean exact-head builds with the exact `uv` 0.12.3 frontend and
`uv_build==0.12.1` backend contract;
5. reads at most three output-directory entries, requires exactly one wheel and
one source distribution, and uses a fixed filesystem-order-independent
diagnostic for missing or extra counts;
6. verifies regular non-symlink files, expected distribution/version identity,
byte size, and streaming SHA-256 equality;
7. serializes one bounded canonical `release-manifest.json` payload before any
filesystem mutation;
8. fails closed on unsupported platforms unless Python exposes the required
descriptor-relative operations, `O_DIRECTORY`, `O_NOFOLLOW`, and no-follow
status inspection;
9. opens the filesystem root or current directory, then walks or creates every
parent component relative to a held directory descriptor using
`O_DIRECTORY` and `O_NOFOLLOW`;
10. inspects the final destination without following a link and permits only an
absent or regular file;
11. creates the temporary entry relative to the final parent descriptor with
exclusive creation, `O_NOFOLLOW`, mode `0600`, and close-on-exec where
available;
12. writes the payload, synchronizes the file, performs the atomic replacement
with descriptor-relative `os.rename()`, and then synchronizes the final
parent directory;
13. removes only the temporary entry created by the current invocation if a
later write or replacement step fails; and
14. retains only the completed manifest for 14 days as review evidence.

New evidence directories request mode `0700`. Errors use bounded operation
categories and do not include manifest contents, source files, credentials,
environment variables, provider identifiers, resolved external targets, or
arbitrary operating-system exception text.

The pull-request workflow has only `contents: read`. It does not publish,
does not attest, does not request an OpenID Connect token, and does not receive
package or attestation write permissions. Publication remains a future,
separately reviewed release workflow. That workflow must bind its provenance to
the integrated release commit, use trusted publishing, and satisfy independent
approval, branch protection, exact-head security, packaging, and release gates.

## Consequences

- Non-deterministic package bytes fail before a tag or release is considered.
- Build frontend and backend patch selection cannot drift between governed or
external PEP 517 builds without an explicit reviewed source change.
- Artifact identity evidence is machine-readable, bounded, and free of source
payloads, credentials, provider data, and environment dumps.
- Missing and extra artifact-count failures produce stable evidence even when
filesystem iteration order changes.
- Parent traversal, direct or nested symlinks, and concurrent lexical parent
replacement cannot redirect a write that is anchored to held directory
descriptors.
- The temporary file and destination replacement remain in the originally
opened final parent even when another process changes its pathname during the
operation.
- Synchronizing the file and final parent directory gives operators explicit
evidence of both byte and directory-entry durability boundaries.
- The function does not reserve or preserve the human-readable lexical pathname
after the function returns. Another same-UID process may rename directories or
entries later; callers requiring post-return ownership must enforce a separate
workspace-isolation boundary.
- The manifest is evidence for review, not a signature, SBOM, provenance
attestation, release authorization, or substitute for independent approval.
- A future release workflow may consume the same verified artifact set and add
SLSA provenance and SPDX SBOM attestations, but must not reuse a pull-request
artifact as release evidence without proving its exact integrated source.
- Version `0.1.0` remains unchanged and no release is published by this decision.

## Alternatives considered

### Build only once

Rejected because one successful build proves package validity, not deterministic
artifact identity.

### Permit a compatible backend patch range

Rejected for the governed release contract because an external PEP 517 frontend
could resolve a later compatible `uv_build` patch independently of `uv.lock`.
Patch upgrades remain straightforward, but they require an explicit reviewed pin
change and fresh exact-head evidence.

### Include sampled filenames in extra-artifact count failures

Rejected because the verifier intentionally stops after the third entry and the
bounded sample depends on filesystem iteration order. A fixed count diagnostic
is sufficient to fail closed and preserves deterministic incident evidence.

### Repeat lexical symlink checks immediately before use

Rejected because the filesystem object can still change after the check. This
narrows but does not remove the CWE-367 interval and creates a false security
claim.

### Resolve the destination with `Path.resolve()` or `realpath()`

Rejected because resolution follows links and produces another mutable pathname.
It also changes the contract from refusing links to accepting their targets.

### Descriptor-relative no-follow traversal and rename

Selected because openat-style operations bind component lookup to held directory
objects and renameat-style replacement keeps source and destination in that
opened final directory. POSIX explicitly defines these interfaces to avoid path
replacement races.

### Fall back to the predecessor pathname writer on unsupported platforms

Rejected. Silent fallback would make the security property platform-dependent
while preserving the same success signal. Unsupported platforms receive one
fixed fail-closed error instead.

### Upload both complete build directories from every pull request

Rejected because complete artifacts increase retention, disclosure, and artifact
substitution risk without improving the bounded equality decision.

### Grant attestation permissions to the pull-request workflow

Rejected because untrusted pull-request verification does not need write-capable
identity or attestation permissions. Least privilege keeps validation separate
from release authority.

## References (APA 7th edition)

Astral. (n.d.). *The uv build backend*. uv documentation. Retrieved August 6,
2026, from https://docs.astral.sh/uv/concepts/build-backend/

GitHub. (n.d.). *Using artifact attestations to establish provenance for builds*.
GitHub Docs. Retrieved August 6, 2026, from
https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds

MITRE. (2026). *CWE-367: Time-of-check time-of-use (TOCTOU) race condition*
(Version 4.20). https://cwe.mitre.org/data/definitions/367.html

Python Packaging Authority. (n.d.). *Source distribution format*.
Python Packaging User Guide. Retrieved August 6, 2026, from
https://packaging.python.org/en/latest/specifications/source-distribution-format/

Python Software Foundation. (2026). *os—Miscellaneous operating system
interfaces*. Python 3.14 documentation. Retrieved August 6, 2026, from
https://docs.python.org/3.14/library/os.html

Reproducible Builds. (n.d.). *SOURCE_DATE_EPOCH specification*. Retrieved
August 6, 2026, from https://reproducible-builds.org/specs/source-date-epoch/

Supply-chain Levels for Software Artifacts. (2025). *SLSA specification,
version 1.2*. https://slsa.dev/spec/v1.2/

The Open Group. (2024). *open, openat—Open file*. In *The Open Group Base
Specifications Issue 8, IEEE Std 1003.1-2024*.
https://pubs.opengroup.org/onlinepubs/9799919799/functions/open.html

The Open Group. (2024). *rename, renameat—Rename file*. In *The Open Group Base
Specifications Issue 8, IEEE Std 1003.1-2024*.
https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html
Loading
Loading