Verify reproducible release artifacts and map acceptance evidence

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e6f0-a6f6-7b93-9124-55ef584930e2
This commit is contained in:
tegwick 2026-09-28 10:22:52 +02:00
parent c976c9c277
commit c29bf39b5d
13 changed files with 577 additions and 3 deletions

View file

@ -29,3 +29,7 @@ a sample cannot admit its own key or renew trust. Deployment and credentials
remain with their existing owners.
[Registration and handoff evidence](docs/registration.md).
[Release verification](docs/release-verification.md) and
[acceptance coverage](docs/acceptance-coverage.md) describe repeatable package
checks and the limits of local test evidence.

View file

@ -0,0 +1,47 @@
# Acceptance coverage — 2026-09-28
This maps the current repository tests to the obligations in
[acceptance v0.1](../specs/acceptance-v0.1.md). A listed test is evidence for its
specific assertion, not completion of the whole criterion. The local suite uses
disposable keys, fake clocks and loopback HTTP; it does not prove independent UTC
accuracy, production admission or native consumer correctness.
Test names below are in `tests/test_clock.py`, `tests/test_authority.py`, and
`tests/test_health.py` unless otherwise noted. Parameterized cases count as tests,
not additional acceptance criteria. The release smoke script is separate from
pytest and runs against an installed wheel.
| ID | Existing local evidence | Uncovered or native obligation; existing owner record |
| --- | --- | --- |
| C01 | `test_interval_contains_send_with_asymmetric_delay` changes `time.time` and exercises the injected timer | No complete ±5-minute/zone/wall-step platform matrix; native Windows is unsupported by LinuxTimer. Supported Linux/WSL evidence: 0002-T03, 0004-T02 |
| C02 | `test_interval_contains_send_with_asymmetric_delay`; `test_lifecycle_during_exchange_refuses[delay]` | One deterministic asymmetric-delay case, not property-based interval coverage or measured network guarantees: 0002-T03, 0004-T02 |
| C03 | `test_signed_wrong_binding_refuses`, `test_bad_signature`, `test_rotation_requires_fresh_client_and_matching_signer`; wheel smoke refuses wrong key | No exhaustive version/header/modified-uncertainty matrix; accepted signing/trust profile: 0002-T02 |
| C04 | `test_replay_refused`, delayed-exchange refusal, `test_restart_preserves_floor`, `test_new_client_cannot_reset_expired_trust` | Exact replay across process restart and every nonce-timeout interleaving are not separately tested; native lifecycle evidence: 0004-T02 |
| C05 | `test_authority_refuses_unusable_health`, `test_restart_changes_epoch`, `test_step_at_final_timestamp_latches_refusal`, `test_health_expires_during_sample_preparation`, `test_health_model_refusals`, collector replacement test | Disposable host upstream outage/reboot/recovery and accepted error model: 0005-T02/T05, RAIL-HO-WP-0013 |
| C06 | `test_lifecycle_during_exchange_refuses[suspend/domain/backward]`; boot-bound admission behavior | Fake timer disruption does not establish real VM suspend/reboot detection or oscillator bounds: 0002-T03, 0004-T02 |
| C07 | `test_validity_uses_entire_interval` checks entirely valid, not-before overlap and exact expiry | Native signed-artifact semantics and protected-side-effect recheck remain consumer-owned: 0002-T04, 0004-T03 |
| C08 | Expired-session, delayed-sample and lifecycle refusals; health expiry during preparation | No general offline holdover offered; native loss/age/uncertainty policy evidence: 0004-T02 |
| C09 | Wrong signer and changed/revoked trust refusal; wheel smoke requires explicit disposable loopback admission | No real invalid-date TLS bootstrap or redirect-server integration test in this suite. Transport trust acceptance: 0002-T02, 0005-T03 |
| C10 | No approval/CAS concurrency fixture in this repository | Clock interval checks do not establish atomic consume or check-to-use safety. Consumer native proof: 0002-T04, 0004-T03 |
| C11 | `attribution.py` emits recording-time evidence; no direct attribution test | No historical occurrence translation or calibration-domain proof is implemented here. Audit/consumer contract and native evidence: 0002-T04, 0004-T03 |
| C12 | No occurrence-ordering test; application clock returns intervals | Overlap/order handling belongs to the consuming event system; audit acceptance: 0002-T04, 0004-T03 |
| C13 | `test_strict_json`, `test_http_framing_refused`, `test_malformed_health_is_stable_http_refusal`, `test_bad_policy_refuses`, `test_helper_failures_are_sanitized` | Coverage is not exhaustive numeric/schema fuzzing. Profile acceptance remains 0002-T02/T03 |
| C14 | `test_rotation_requires_fresh_client_and_matching_signer`, changed-trust refusal, expired admission and persisted rollback tests | Local file replacement is not distributed revocation; custody visibility deadlines and compromise recovery: 0002-T02, 0005-T03/T05 |
| C15 | `test_connection_limit_and_capacity_recovery`, HTTP liveness/readiness separation, framing and delayed-exchange tests | Eight held connections and recovery are not sustained-load/SLO proof. Host measurements and alerts: 0004-T02, 0005-T06 |
| C16 | `tools/wheel_smoke.py` checks installed CLI and signed authority/client HTTP exchange, wrong-key and unhealthy refusal | This is reference integration only. Named native consumer identity/tenant/expiry/CAS migration: 0004-T03 |
## Release and dependency evidence
`tools/verify_release.py` separately checks byte equality of two builds from
separate source roots with different file mtimes, packaged/spec schema equality,
wheel installation into a fresh runtime environment, CLI startup and a signed
loopback exchange. Build tools are hash-pinned in `tools/build-requirements.txt`;
runtime dependencies are installed from a hash-bearing export of `uv.lock`.
The SBOM lists installed runtime distributions and versions, not all platform
libraries, transitive native-library internals, licenses or vulnerability findings.
See [release verification](release-verification.md) for the exact receipt and
reproduction procedure. No test or artifact publication makes the proposed
contract adopted. The 0003 closeout is limited to the reference implementation;
its original task prose is broader than the evidence in several rows above.
Those unproven native claims remain in existing 0002/0004/0005 records.

View file

@ -0,0 +1,73 @@
{
"bomFormat": "CycloneDX",
"components": [
{
"name": "PyJWT",
"purl": "pkg:pypi/pyjwt@2.14.0",
"type": "library",
"version": "2.14.0"
},
{
"name": "attrs",
"purl": "pkg:pypi/attrs@26.1.0",
"type": "library",
"version": "26.1.0"
},
{
"name": "cffi",
"purl": "pkg:pypi/cffi@2.1.1",
"type": "library",
"version": "2.1.1"
},
{
"name": "cryptography",
"purl": "pkg:pypi/cryptography@50.0.1",
"type": "library",
"version": "50.0.1"
},
{
"name": "jsonschema",
"purl": "pkg:pypi/jsonschema@4.26.0",
"type": "library",
"version": "4.26.0"
},
{
"name": "jsonschema-specifications",
"purl": "pkg:pypi/jsonschema-specifications@2025.9.1",
"type": "library",
"version": "2025.9.1"
},
{
"name": "pycparser",
"purl": "pkg:pypi/pycparser@3.0",
"type": "library",
"version": "3.0"
},
{
"name": "railiance-clock",
"purl": "pkg:pypi/railiance-clock@0.1.0",
"type": "library",
"version": "0.1.0"
},
{
"name": "referencing",
"purl": "pkg:pypi/referencing@0.37.0",
"type": "library",
"version": "0.37.0"
},
{
"name": "rpds-py",
"purl": "pkg:pypi/rpds-py@2026.6.3",
"type": "library",
"version": "2026.6.3"
},
{
"name": "typing_extensions",
"purl": "pkg:pypi/typing-extensions@4.16.0",
"type": "library",
"version": "4.16.0"
}
],
"specVersion": "1.5",
"version": 1
}

View file

@ -0,0 +1,127 @@
{
"build_requirements_sha256": "ff153e73ea3a33c7d33858be50eda03943c5ef5e2706477a90ca76d250878adb",
"build_tools": [
[
"hatchling",
"1.32.4"
],
[
"packaging",
"26.3"
],
[
"pathspec",
"1.1.1"
],
[
"pluggy",
"1.6.0"
],
[
"tomlkit",
"0.15.1"
],
[
"trove-classifiers",
"2026.9.21.13"
]
],
"byte_identical": true,
"checkout_build": {
"railiance_clock-0.1.0-py3-none-any.whl": "7a4790efc97f19b69e942ae1f91a09b1b2a3b22ceca68e41ea4e45655848e420",
"railiance_clock-0.1.0.tar.gz": "cc23505334fbe7a29c5892d52db50883f62082c3885b1e61b559b6b0f5f0d12f"
},
"independent_builds": [
{
"railiance_clock-0.1.0-py3-none-any.whl": "7a4790efc97f19b69e942ae1f91a09b1b2a3b22ceca68e41ea4e45655848e420",
"railiance_clock-0.1.0.tar.gz": "cc23505334fbe7a29c5892d52db50883f62082c3885b1e61b559b6b0f5f0d12f"
},
{
"railiance_clock-0.1.0-py3-none-any.whl": "7a4790efc97f19b69e942ae1f91a09b1b2a3b22ceca68e41ea4e45655848e420",
"railiance_clock-0.1.0.tar.gz": "cc23505334fbe7a29c5892d52db50883f62082c3885b1e61b559b6b0f5f0d12f"
}
],
"limits": "Local Linux build/install evidence; no native UTC, security admission, vulnerability audit or cross-platform reproducibility claim.",
"platform": "Linux/x86_64",
"python": "3.12.3",
"runtime_lock_sha256": "fe4b4ab7fb1740648a13ff87d028914d94e4c793bb5957830e776a878c9adbb9",
"sbom_sha256": "69b45bf19e94e485d08e7af238f2aa3e3fa4f3969d8a552fcf874a12aa1eb07d",
"smoke": {
"checks": [
"installed_import",
"three_packaged_schemas",
"cli_help",
"signed_http_cli_read",
"wrong_key_refusal",
"unhealthy_cli_refusal"
],
"distributions": [
[
"PyJWT",
"2.14.0"
],
[
"attrs",
"26.1.0"
],
[
"cffi",
"2.1.1"
],
[
"cryptography",
"50.0.1"
],
[
"jsonschema",
"4.26.0"
],
[
"jsonschema-specifications",
"2025.9.1"
],
[
"pycparser",
"3.0"
],
[
"railiance-clock",
"0.1.0"
],
[
"referencing",
"0.37.0"
],
[
"rpds-py",
"2026.6.3"
],
[
"typing_extensions",
"4.16.0"
]
]
},
"smoke_script_sha256": "cdb3adb57a68f95d8fd6878195d0ee7dd7a0a00060bacc2a3f95073e6c0b27ec",
"source_base_commit": "c976c9c277033e9fca384b9e9fcd5d79ff776d43",
"source_date_epoch": "315532800",
"source_inputs_sha256": {
".gitignore": "74e4a2bdd348ce27bd64fbafbc09bef61433a821df2f23c726b062ead4896ebf",
"README.md": "c544450bc755e5743bac961fd3832f20ec4d9f86ec3562ca4a2d734a2d694a3e",
"pyproject.toml": "11a3e79fcd7b2929137ebad31647c839edc40572958b08b95f08b0605482416d",
"src/railiance_clock/__init__.py": "7c9c93a7d6bee6c68da5c19055c899d84040ecd5186169441f3a4d24d12c45c4",
"src/railiance_clock/admission.py": "afbea12a31e8ac6bea002a4d0477564d76698b633e88edeb0d2f60c582b5fc47",
"src/railiance_clock/attribution.py": "6cc6a5603295bda8fed47c5b144bdec1c6faff8b27acc440a65b81eb376415bd",
"src/railiance_clock/authority.py": "9a7ba85d17d70aeaad86cbb850b36515b7a82aef085799591bdfdf410cd023ee",
"src/railiance_clock/cli.py": "8ed77836cbdd823b86de849f76f228355bf47fcbfbbc572e22e6e48662929b59",
"src/railiance_clock/client.py": "34401db8cecf76a791013b383f9dc6bd56c0478472fe222f86b2be93505ac00f",
"src/railiance_clock/core.py": "32ac078f8729533dbbee15166cd6e450cd8638d178294304c1c60665d536b9a1",
"src/railiance_clock/health.py": "366e725eaf68d88df3a236d1f71bde5e5b7f24846932041fbaa46182e88d0f07",
"src/railiance_clock/protocol.py": "d076bfd0482d553e1c57133e7ab532c8360db3c100f164404a7c44211c3290a2",
"src/railiance_clock/schemas/protected-header-v0.1.schema.json": "15de9f38e61b49677479b431c3ef7e37a674e4b817ef8f78a19bc8fc47bbf4f8",
"src/railiance_clock/schemas/time-request-v0.1.schema.json": "c5a122b1f1f48872a25fffa5950e60dddd585e87a4da502fc4e873a44ae60f4c",
"src/railiance_clock/schemas/time-sample-v0.1.schema.json": "97e026520fff98c7140b8a91b9332ffcba22fd5d509fa983057c7cac6e6aa2d2"
},
"uv": "uv 0.5.9",
"verifier_sha256": "f3eb2a489ddf32bca164a78c0b3c22358bf45703bcd6d0572413353c1875fb81"
}

View file

@ -0,0 +1,62 @@
# Local release verification
Run from this repository on Linux with Python 3.11+ and `uv`:
```sh
python3 tools/verify_release.py --output /tmp/rclk-release-UNIQUE
```
The output directory must not already exist. Temporary keys, virtual environments
and build trees are removed at exit. The output retains `verification.json` and
`runtime.cdx.json`; it does not publish or deploy a package. Package downloads may
require registry access. No production credentials or host clock changes are used.
The verifier performs:
1. Byte comparison of the three packaged JSON schemas against `specs/`.
2. A fresh build environment installed from hash-pinned
`tools/build-requirements.txt`, independently of runtime dependencies.
3. Two sdist/wheel builds from separate source roots with deliberately different
mtimes and fixed `SOURCE_DATE_EPOCH=315532800`. The sdist explicitly includes
source, project metadata and README; Hatchling also includes `.gitignore`.
All four are represented in the input manifest; generated evidence is excluded.
4. A build from the actual checkout, which must match both copied-source builds.
5. A fresh runtime environment installed from the hash-bearing locked runtime
requirements plus the built wheel, followed by `uv pip check`.
6. An isolated (`python -I`) smoke check outside the checkout: installed-package
import, schema presence, CLI startup, a signed HTTP exchange with a disposable
authority, wrong-key refusal, and sanitized CLI failure on unusable health.
7. Installed distribution/version comparison against `uv.lock` and a CycloneDX
runtime component inventory. Build dependencies are listed separately in the
receipt; neither development dependencies nor build tools enter the runtime.
Build constraints can be deliberately refreshed with:
```sh
uv pip compile tools/build-requirements.in --generate-hashes -o tools/build-requirements.txt
```
Review that diff and rerun verification after a refresh. Normal verification
uses the committed pins and does not resolve newer build versions. Runtime
installation similarly uses `uv export --locked` and verifies archive hashes.
## Retained evidence
The September 28 run is in
[evidence/2026-09-28-release/verification.json](evidence/2026-09-28-release/verification.json)
and its [runtime SBOM](evidence/2026-09-28-release/runtime.cdx.json).
The receipt records source-input hashes, lock/build-requirement hashes, verifier
hashes, Python/uv/build-tool versions, artifact digests and smoke checks. Its base
commit identifies the checkout before the verification changes were committed;
the input manifest identifies the exact built contents, including pending edits.
This demonstrates repeatability on the recorded Linux/Python toolchain. It does
not establish reproducibility on other platforms/interpreters, native UTC accuracy,
consumer admission, deployment, license clearance or absence of vulnerabilities.
The component inventory covers installed Python distributions, not system libraries
or native libraries embedded inside third-party wheels. Build timestamps are
normalized metadata and are never used as trusted time.
The [acceptance coverage map](acceptance-coverage.md) distinguishes local checks
from unproven native obligations. RCLK-WP-0003-T03 owns this package evidence;
existing 0002/0004/0005 records retain owner acceptance and deployment gates.

View file

@ -13,5 +13,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/railiance_clock"]
[tool.hatch.build.targets.sdist]
include = ["/src", "/pyproject.toml", "/README.md"]
[tool.pytest.ini_options]
testpaths = ["tests"]

View file

@ -1,7 +1,8 @@
# Acceptance and threat model — draft v0.1
No tests below are claimed executed by the specification seed. IDs are future
behavioral acceptance obligations, not a count of implementation coverage.
IDs below are behavioral acceptance obligations, not a count of implementation
coverage. The [current coverage map](../docs/acceptance-coverage.md) identifies
local test evidence, gaps and the existing owner records for native acceptance.
| ID | Scenario | Required evidence |
| --- | --- | --- |

View file

@ -22,3 +22,11 @@ later workplan tasks.
The 2026-09-14 live JSON receipt accompanies the human-readable host inventory.
Six unit tests cover transport failure, host argument validation, fixed command
transport and unknown-versus-synchronized semantics.
# Release verification
`python3 tools/verify_release.py --output /tmp/rclk-release-UNIQUE` builds and
compares source archives and wheels, installs the wheel into a fresh environment,
and runs `wheel_smoke.py` with isolated imports. It retains artifact/input hashes
and a runtime component SBOM. See [the procedure](../docs/release-verification.md)
for build pins, evidence and platform limits.

View file

@ -0,0 +1,2 @@
# Build-only dependencies; runtime dependencies remain in uv.lock.
hatchling>=1.27,<2

View file

@ -0,0 +1,26 @@
# This file was autogenerated by uv via the following command:
# uv pip compile tools/build-requirements.in --generate-hashes -o tools/build-requirements.txt
hatchling==1.32.4 \
--hash=sha256:08ecf7548fb48205e7f213d70c71e67b8271b7242093dc3f1da578b42c734a2c \
--hash=sha256:c4468f73144c054d2aab4ef0f0378c43b9878bf07f8ffd6b79690e970d375f07
# via -r tools/build-requirements.in
packaging==26.3 \
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
# via hatchling
pathspec==1.1.1 \
--hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
--hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
# via hatchling
pluggy==1.6.0 \
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
# via hatchling
tomlkit==0.15.1 \
--hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \
--hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97
# via hatchling
trove-classifiers==2026.9.21.13 \
--hash=sha256:0a9ebc8d4e2f3e8a22848c5258033035bec17a3012ac3fea16dbaa764489eb71 \
--hash=sha256:8b1ff4f9c191b1040b71c37f1e445ab99732911e3cd91de52838453a854d7a17
# via hatchling

118
tools/verify_release.py Normal file
View file

@ -0,0 +1,118 @@
"""Build twice, smoke-test an isolated wheel, and emit release evidence and SBOM."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tempfile
import tomllib
import zipfile
ROOT = Path(__file__).resolve().parents[1]
EPOCH = '315532800' # Fixed ZIP-compatible build timestamp, not trusted time.
def run(*args, cwd=ROOT, env=None):
try:
return subprocess.run([str(a) for a in args], cwd=cwd, env=env,
check=True, capture_output=True, text=True).stdout
except subprocess.CalledProcessError as exc:
print(exc.stderr, file=sys.stderr)
raise
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def write(path, value):
path.write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True, help='New evidence directory')
args = parser.parse_args()
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=False)
env = dict(os.environ, SOURCE_DATE_EPOCH=EPOCH, PYTHONHASHSEED='0')
env.pop('PYTHONPATH', None)
inputs = [ROOT / 'pyproject.toml', ROOT / 'README.md', ROOT / '.gitignore', *sorted((ROOT / 'src').rglob('*.py')),
*sorted((ROOT / 'src').rglob('*.json'))]
manifest = {str(p.relative_to(ROOT)): sha(p) for p in inputs}
# Verify the separately reviewed spec and packaged schema copies agree.
for p in (ROOT / 'src/railiance_clock/schemas').glob('*.json'):
assert p.read_bytes() == (ROOT / 'specs' / p.name).read_bytes(), p.name
with tempfile.TemporaryDirectory(prefix='rclk-release-') as tmp:
temp = Path(tmp)
build_env = temp / 'build-env'
run('uv', 'venv', '--python', sys.executable, build_env)
build_python = build_env / 'bin/python'
run('uv', 'pip', 'install', '--python', build_python, '--require-hashes',
'-r', ROOT / 'tools/build-requirements.txt')
artifacts = []
for number in (1, 2):
source = temp / f'source-{number}'
source.mkdir()
for original in inputs:
target = source / original.relative_to(ROOT)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(original, target)
# Deliberately vary source mtimes between the independent roots.
os.utime(target, (int(EPOCH) + number * 100, int(EPOCH) + number * 100))
dest = temp / f'dist-{number}'
run('uv', 'build', source, '--python', build_python, '--no-build-isolation',
'--out-dir', dest, env=env)
artifacts.append({p.name: sha(p) for p in dest.iterdir()
if p.name.endswith(('.whl', '.tar.gz'))})
assert artifacts[0] == artifacts[1], 'build outputs differ'
checkout_dist = temp / 'checkout-dist'
run('uv', 'build', ROOT, '--python', build_python, '--no-build-isolation',
'--out-dir', checkout_dist, env=env)
checkout_artifacts = {p.name: sha(p) for p in checkout_dist.iterdir()
if p.name.endswith(('.whl', '.tar.gz'))}
assert artifacts[0] == checkout_artifacts, 'checkout build differs from copied inputs'
wheel = next((temp / 'dist-1').glob('*.whl'))
with zipfile.ZipFile(wheel) as archive:
members = archive.namelist()
assert len([m for m in members if m.endswith('.schema.json')]) == 3
requirements = temp / 'runtime.txt'
run('uv', 'export', '--locked', '--no-dev', '--no-emit-project', '-o', requirements)
install_env = temp / 'installed-env'
run('uv', 'venv', '--python', sys.executable, install_env)
python = install_env / 'bin/python'
run('uv', 'pip', 'install', '--python', python, '--require-hashes', '-r', requirements)
run('uv', 'pip', 'install', '--python', python, '--no-deps', wheel)
run('uv', 'pip', 'check', '--python', python)
smoke = json.loads(run(python, '-I', ROOT / 'tools/wheel_smoke.py', cwd=temp, env=env))
locked = {p['name']: p['version'] for p in tomllib.loads((ROOT / 'uv.lock').read_text())['package']}
for name, version in smoke['distributions']:
assert locked[name.lower().replace('_', '-')] == version, (name, version)
components = [{'type': 'library', 'name': name, 'version': version,
'purl': f'pkg:pypi/{name.lower().replace("_", "-")}@{version}'}
for name, version in smoke['distributions']]
write(output / 'runtime.cdx.json', {'bomFormat': 'CycloneDX', 'specVersion': '1.5',
'version': 1, 'components': components})
build_tools = json.loads(run(build_python, '-I', '-c',
'import importlib.metadata as m,json; print(json.dumps(sorted((d.metadata["Name"],d.version) for d in m.distributions())))'))
write(output / 'verification.json', {
'source_base_commit': run('git', 'rev-parse', 'HEAD').strip(),
'source_inputs_sha256': manifest, 'runtime_lock_sha256': sha(ROOT / 'uv.lock'),
'build_requirements_sha256': sha(ROOT / 'tools/build-requirements.txt'),
'verifier_sha256': sha(Path(__file__)), 'smoke_script_sha256': sha(ROOT / 'tools/wheel_smoke.py'),
'python': platform.python_version(), 'platform': platform.system() + '/' + platform.machine(),
'uv': run('uv', '--version').strip(), 'build_tools': build_tools,
'source_date_epoch': EPOCH, 'independent_builds': artifacts,
'checkout_build': checkout_artifacts,
'byte_identical': True, 'smoke': smoke,
'sbom_sha256': sha(output / 'runtime.cdx.json'),
'limits': 'Local Linux build/install evidence; no native UTC, security admission, vulnerability audit or cross-platform reproducibility claim.'})
print(f'Verified two identical builds and isolated wheel smoke test: {output}')
if __name__ == '__main__':
main()

91
tools/wheel_smoke.py Normal file
View file

@ -0,0 +1,91 @@
"""Run with an isolated interpreter containing only the installed wheel and deps."""
import importlib.metadata
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import threading
import time
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
import railiance_clock
from railiance_clock.admission import admit
from railiance_clock.authority import Authority, Server
from railiance_clock.client import Clock, FileTrust
from railiance_clock.core import ClockError
def main():
installed = Path(railiance_clock.__file__).resolve()
assert installed.is_relative_to(Path(sys.prefix).resolve()), installed
schemas = installed.parent / 'schemas'
for name in ('time-request', 'time-sample', 'protected-header'):
assert json.loads((schemas / (name + '-v0.1.schema.json')).read_text())['type'] == 'object'
cli = Path(sys.executable).parent / 'railiance-clock'
help_result = subprocess.run([str(cli), '--help'], capture_output=True, text=True, check=True)
assert all(command in help_result.stdout for command in ('read', 'serve', 'health-export'))
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
key = ec.generate_private_key(ec.SECP256R1())
private = root / 'key.pem'
private.write_bytes(key.private_bytes(serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))
private.chmod(0o600)
health = root / 'health.json'
health.write_text(json.dumps({'quality': 'bounded',
'boot_id': Path('/proc/sys/kernel/random/boot_id').read_text().strip(),
'policy_id': 'wheel-test', 'observed_monotonic_ns': time.monotonic_ns(), 'error_ns': 1000}))
config = {'private_key_file': str(private), 'health_file': str(health),
'health_owner_uid': os.getuid(), 'policy_id': 'wheel-test',
'authority_id': 'disposable', 'environment': 'test', 'kid': 'test-key',
'max_health_age_ns': 30_000_000_000, 'host_drift_ppm': 1000,
'max_error_ns': 100_000_000, 'max_continuity_error_ns': 50_000_000,
'max_sample_age_ns': 5_000_000_000}
authority = Authority(config)
server = Server(('127.0.0.1', 0), authority)
thread = threading.Thread(target=server.serve_forever)
thread.start()
try:
trust = admit(directory=root, authority_id='disposable', environment='test',
kid='test-key', epoch=authority.epoch, policy_id='wheel-test',
public_key_pem=key.public_key().public_bytes(serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo).decode(),
endpoint=f'http://127.0.0.1:{server.server_port}/v1/time-samples',
admission_ref='disposable-wheel-test-only', transport='admitted-loopback',
limits={'max_age_ns': 5_000_000_000, 'max_rtt_ns': 2_000_000_000,
'max_width_ns': 3_000_000_000, 'max_server_error_ns': 100_000_000,
'timer_ppm': 1000, 'timer_resolution_ns': 5_000_000,
'suspend_tolerance_ns': 5_000_000, 'trust_session_ns': 60_000_000_000})
result = subprocess.run([str(cli), 'read', '--trust', str(trust)],
capture_output=True, text=True, check=True)
window = json.loads(result.stdout)
assert window['authority_id'] == 'disposable'
assert window['epoch'] == authority.epoch
assert window['lower_ns'] <= window['upper_ns']
bad = json.loads(trust.read_text()); bad['kid'] = 'wrong-key'
trust.write_text(json.dumps(bad))
try:
Clock(FileTrust(trust)).read()
except ClockError:
pass
else:
raise AssertionError('wrong key accepted')
bad['kid'] = 'test-key'; trust.write_text(json.dumps(bad))
health.write_text('{"quality":"unusable"}')
refused = subprocess.run([str(cli), 'read', '--trust', str(trust)],
capture_output=True, text=True)
assert refused.returncode == 1 and not refused.stdout
assert refused.stderr == 'Railiance time unavailable or untrusted.\n'
finally:
server.shutdown(); server.server_close(); thread.join()
distributions = sorted((d.metadata['Name'], d.version) for d in importlib.metadata.distributions())
print(json.dumps({'checks': ['installed_import', 'three_packaged_schemas', 'cli_help',
'signed_http_cli_read', 'wrong_key_refusal', 'unhealthy_cli_refusal'],
'distributions': distributions}, sort_keys=True))
if __name__ == '__main__':
main()

View file

@ -11,7 +11,7 @@ depends_on:
owner: codex
topic_slug: custodian
created: "2026-09-14"
updated: "2026-09-24"
updated: "2026-09-28"
depends_on_workplans:
- RCLK-WP-0002
state_hub_workstream_id: "904e08b5-978f-5bfc-b543-ca5347a56ac5"
@ -63,3 +63,16 @@ records and are not leftovers of this plan. `RCLK-WP-0005` owns
infrastructure-as-code delivery. `RCLK-WP-0004` owns the railiance01 pilot and
the first consumer migration, and it waits on `RCLK-WP-0005` as well as this
plan.
### Release-evidence follow-up — 2026-09-28
T03 now has a repeatable [release verifier](../docs/release-verification.md),
hash-pinned build tools, independent byte-identical wheel/sdist build evidence,
an isolated installed-wheel signed HTTP/CLI smoke check, and a runtime SBOM.
The [acceptance coverage map](../docs/acceptance-coverage.md) explicitly records
that the original task prose exceeds current evidence: no local approval/CAS
concurrency fixture or historical event-translation test, and no complete native
platform or consumer proof. Finished status retains only the previously stated
reference boundary; those broader claims are not asserted. Existing
0002-T04/0004-T02/T03 and 0005 retain their owner/native gates.