Skip to content

Commit fd9b60b

Browse files
authored
feat(evidence): verify retained production acceptance evidence (#287)
Adds a strict verifier for the Cloud production-acceptance certificate, GitHub provenance, immutable-retention receipt, signed qualification admission, and complete qualification campaign. It recomputes the admission, runtime-validation, workflow-version, organization, workflow, campaign, contract, outcomes, oracle, and trial-count bindings, and derives the failure taxonomy from signed trial rows and receipts instead of author-declared counts or maturity flags. It rejects unknown or revoked admission signers, changed issuer scope, collapsed admission and runtime identities, incomplete trial rows, excluded or hidden trials, duplicate attempt and run IDs, unreferenced receipt envelopes, unsigned receipt bodies, self-hosted runners, and self-supplied trust registries. Path resolution now refuses symlinked components and any resolved path outside the repository root. The public entry point `import_files` refuses every import until one approved private-export contract exists, so no campaign, class, boolean, or summary count can declare production acceptance. The checked-in fixtures are synthetic test vectors. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent b39325c commit fd9b60b

10 files changed

Lines changed: 13718 additions & 37 deletions

.github/workflows/evidence-freshness.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
paths:
1313
- 'docs/eval_results/**'
1414
- 'scripts/check_published_evidence_freshness.py'
15+
- 'scripts/import_production_acceptance.py'
1516
- '.github/workflows/evidence-freshness.yml'
1617

1718
concurrency:
@@ -35,4 +36,14 @@ jobs:
3536

3637
# Standard library only: no dependency install, no lockfile, no cache.
3738
- name: Check published evidence against the current published release
38-
run: python scripts/check_published_evidence_freshness.py
39+
env:
40+
APPROVED_CLOUD_ACCEPTANCE_COMMIT: ${{ vars.APPROVED_CLOUD_ACCEPTANCE_COMMIT }}
41+
QUALIFICATION_ADMISSION_SIGNERS_JSON: ${{ vars.QUALIFICATION_ADMISSION_SIGNERS_JSON }}
42+
REVOKED_QUALIFICATION_ADMISSION_IDS_JSON: ${{ vars.REVOKED_QUALIFICATION_ADMISSION_IDS_JSON }}
43+
REVOKED_QUALIFICATION_SIGNER_KEY_IDS_JSON: ${{ vars.REVOKED_QUALIFICATION_SIGNER_KEY_IDS_JSON }}
44+
run: >-
45+
python scripts/check_published_evidence_freshness.py
46+
--approved-cloud-source-commit "$APPROVED_CLOUD_ACCEPTANCE_COMMIT"
47+
--trusted-admission-signers-json "$QUALIFICATION_ADMISSION_SIGNERS_JSON"
48+
--revoked-admission-ids-json "$REVOKED_QUALIFICATION_ADMISSION_IDS_JSON"
49+
--revoked-admission-signer-key-ids-json "$REVOKED_QUALIFICATION_SIGNER_KEY_IDS_JSON"

docs/eval_results/PRODUCTION_READINESS.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,183 @@ evidence set matches the current published `openadapt-flow` release. It does
1010
not mean the campaign is production acceptance. The evidence manifest records
1111
that distinction in `campaigns[].evidence_scope.production_acceptance`.
1212

13+
That campaign field is now descriptive only and must remain `false`. A person
14+
cannot promote an evidence set by changing it, changing an evidence class, or
15+
adding summary counts. Production acceptance can enter this repository only
16+
through `scripts/import_production_acceptance.py` after the private-export gate
17+
opens.
18+
19+
## Independent hosted-acceptance verifier
20+
21+
The verifier mechanism is implemented. The private-evidence import is not
22+
enabled. `import_files` refuses every file import until OpenAdapt approves one
23+
exact private-export contract. Do not export an admission or campaign to this
24+
repository. Do not set `production_acceptance: true` while this gate is closed.
25+
The checked-in fixtures are synthetic test vectors only.
26+
27+
The future approval must bind the complete payload schema and digest, the
28+
destination account, service, container, and prefix, the encryption-key
29+
identity, the immutable retention mode and period, the authorized importer
30+
workflow and ref, and the approval authority. A broad enable flag or approval
31+
of only the admission and campaign digests is not sufficient.
32+
33+
After approval, the importer will compose four evidence inputs:
34+
35+
1. The closed `openadapt.execute-live-acceptance-record/v2` certificate from
36+
the protected Cloud production workflow.
37+
2. The signed `openadapt.qualification-admission/v2` envelope from the
38+
protected qualification authority.
39+
3. The full `openadapt.qualification-campaign/v2` artifact with every retained
40+
trial row and normalized evidence receipt for the exact qualification
41+
contract.
42+
4. The GitHub artifact-attestation bundle for the certificate bytes.
43+
44+
The future importer also needs external control inputs: the approved Cloud
45+
source commit, the approved qualification signer registry, the admission and
46+
signer revocation lists, and the exact private-export approval. The evidence
47+
artifacts cannot select these values. The full admission and campaign stay in
48+
the encrypted retained evidence package until that approval exists. After
49+
approval, a protected job can stage only the approved payload for
50+
verification. The derived public result contains hashes and bounded counts. It
51+
does not contain tenant or workflow identifiers.
52+
53+
The verifier asks GitHub CLI to verify the certificate against the exact
54+
`OpenAdaptAI/openadapt-cloud` workflow on `refs/heads/main`, the GitHub Actions
55+
OIDC issuer, and a GitHub-hosted runner. It then validates the verified SLSA
56+
provenance. It refuses an unknown repository, workflow, ref, source digest,
57+
issuer, runner class, signature, or empty verification result.
58+
59+
The expected Cloud commit is an external reviewer input. The importer requires
60+
the certificate commit, the GitHub signing-certificate source commit, and the
61+
SLSA resolved `gitCommit` to equal that value. It also requires the signing
62+
certificate and the SLSA source dependency to use `refs/heads/main`. The
63+
certificate cannot select its own approved commit.
64+
65+
The evidence manifest does not store or select this approval. The repository
66+
checker reads it from the protected `APPROVED_CLOUD_ACCEPTANCE_COMMIT` GitHub
67+
repository variable. A production declaration fails when that variable is
68+
absent or differs from the certificate and verified provenance.
69+
70+
The certificate is necessary but not sufficient. It proves one authenticated,
71+
qualified browser transaction and binds the private request, idempotent
72+
responses, runner delivery, result, report, receipt, target attestation,
73+
single-use runner permit, independent observer, webhook, billing outcome, and
74+
separate signing identities by digest. The importer also verifies the external
75+
qualification admission. It checks the exact issuer workflow and main-branch
76+
commit, the Ed25519 signature, the derived key ID, the active time window, and
77+
the external admission and key revocation lists. It refuses a trust key that
78+
the imported evidence supplies for itself.
79+
80+
The admission signs one shared evidence identity for the exact workflow,
81+
campaign, environment, Flow release and wheel, runner build and artifact,
82+
browser image, runtime manifest, signer registry revision, and every admitted
83+
contract. The certificate repeats the public domain-separated campaign,
84+
admission, runtime-validation, and workflow-version digests. The importer
85+
recomputes each digest from the retained admission. The admission also signs
86+
canonical hashes for the full campaign, qualification contract, outcomes
87+
projection, oracle, and exact task inventory. The campaign must retain every
88+
condition in the bound qualification contract and at least three unique trials
89+
for each condition. Trial indexes are one-based and contiguous. Attempt IDs
90+
and run IDs cannot repeat. An excluded or hidden trial causes refusal.
91+
92+
The shared identity calls the receipt authority
93+
`evidence_runner_signer_sha256`. This name applies to browser, native Desktop,
94+
BYOC, RDP, Citrix, and offline customer-controlled evidence. It does not imply
95+
that the evidence runner is an OpenAdapt-managed browser runner.
96+
97+
Each `openadapt.qualification-trial-row/v2` trial refers to hash-keyed
98+
`openadapt.qualification-evidence-receipt/v2` Ed25519 envelopes for the runner,
99+
independent observer, webhook, replay, cleanup, and cleanup-absence result.
100+
Fault cases also require a signed fault receipt. The importer verifies each
101+
envelope body, signature, authority key, source digest, task, condition, trial
102+
index, attempt, run, workflow version, bundle artifact, runtime validation,
103+
admission, evidence identity, verdict, and time. A digest with no signed body is
104+
not evidence. An unused envelope is hidden evidence and causes refusal.
105+
106+
The importer classifies the verified rows itself. The signed observer receipt
107+
contains the exact effect inventory. The signed runner receipt contains the
108+
model-call counters, provider-model inventory, egress-policy digest, report
109+
digest, and operator-intervention inventory. The importer derives failures
110+
from these facts. It rejects a count-only claim, declared production class or
111+
boolean, unsupported failure class, vacuous invariant, healthy-path contract
112+
that permits a model call, healthy-path model call, silent incorrect success,
113+
over-halt, wrong-record effect, duplicate effect, collateral effect, uncertain
114+
delivery, platform failure, or operator intervention.
115+
116+
The derived result binds the certificate's campaign-outcomes and independent
117+
oracle-contract digests. It repeats the validated task, condition, required
118+
trial, and observed trial counts. It includes every closed failure-taxonomy
119+
count used for the verdict. Task and condition identities use domain-separated
120+
SHA-256 values. The public result does not contain the private task or condition
121+
labels. The derived-result file digest therefore changes after a one-field
122+
change to a binding, count, or privacy-safe identity.
123+
124+
## Target-neutral acceptance manifest
125+
126+
The importer contains a pure builder and validator for the closed
127+
`openadapt.production-acceptance/v1` manifest. The mechanism does not write,
128+
sign, export, attest, or publish a manifest. It accepts only a complete accepted
129+
private result. It binds the target and its exact claim scope in both the fixed
130+
policy and the manifest.
131+
132+
| Target | Exact claim scope |
133+
| --- | --- |
134+
| `agent` | `qualified_agent_bridge_release` |
135+
| `capture` | `qualified_native_recorder_release` |
136+
| `cloud` | `qualified_workflow_control_plane_deployment` |
137+
| `desktop` | `qualified_native_workflow_desktop_release` |
138+
| `docs` | `production_documentation_deployment` |
139+
| `flow` | `qualified_workflow_runtime_release` |
140+
| `openadapt` | `qualified_workflow_launcher_release` |
141+
142+
The current browser evidence adapter can build only the Flow target manifest.
143+
The other targets require their own evidence adapter. Cloud also requires a
144+
reviewed deployment-manifest binding. The builder refuses Cloud until that
145+
binding exists. It does not emit a placeholder or failed record when an adapter
146+
is absent.
147+
148+
For Flow, a separate lifecycle verifier accepts the exact raw lifecycle-policy
149+
bytes, one closed public-package release, and the PyPI release metadata. It
150+
requires the exact GitHub source-commit URL. It requires one sorted sdist and
151+
one wheel. Each artifact includes its authority, kind, name, URL, size, and
152+
SHA-256 digest. The verifier matches each field to exactly one non-yanked PyPI
153+
file. It then returns an immutable verified-release object. A caller-supplied
154+
mapping or digest cannot replace this object.
155+
156+
The pure manifest builder accepts only this verified-release object. It matches
157+
the Flow version, source commit, and wheel digest to the private result. It
158+
binds the complete sdist and wheel inventory. It uses the same release and
159+
artifact digest domains as the Production lifecycle validator. The manifest
160+
contains two separate policy digests. The acceptance-policy digest covers the
161+
fixed Evals acceptance rules. The lifecycle-policy digest covers the exact raw
162+
lifecycle-policy bytes.
163+
164+
The public qualification section contains aggregate trial counts and one
165+
campaign-scoped task-condition inventory digest. It does not publish task or
166+
condition labels. The closed source-result digest remains the authority for the
167+
hidden per-condition inventory.
168+
169+
When the private-export gate opens, the output claim will be only
170+
`qualified_browser_workflow_on_bound_environment`. It is not a general product
171+
production-readiness claim. The checker can accept `production_acceptance: true`
172+
only when the evidence manifest contains digest-bound links to all four inputs
173+
and the exact derived output, the protected workflow supplies the external
174+
trust, revocation, and export-approval controls, and the importer reproduces
175+
that output. Until then, the importer refuses. A bare registry boolean or
176+
campaign label fails the check.
177+
178+
Cloud does not issue a complete acceptance record before it verifies durable
179+
retention. The private evidence envelope uses encrypted immutable storage with
180+
Object Lock and KMS. The public record retains only opaque digests and the
181+
verified retention facts. The importer verifies the public retention receipt
182+
and its exact binding to the candidate and retained envelope. GitHub artifact
183+
retention alone does not satisfy this contract.
184+
13185
## What the current public Evals set establishes
14186

15187
The Flow 1.31.0 set contains four campaigns. Every condition has three trials.
188+
This set is development evidence. It is not a Production lifecycle admission,
189+
and this change does not emit a Production record for it.
16190

17191
| Campaign | Environment | What it measures | Production acceptance |
18192
| --- | --- | --- | --- |

0 commit comments

Comments
 (0)