Add the staging regression evidence harness

1c210c0d2e0e · Christopher David · · parent 6dd43f51992a

Add the staging regression evidence harness

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md
  • added docs/operations/staging-regression.md
  • added docs/operations/staging-report-template.md
  • modified infra/staging/README.md
  • modified ops/ci/contracts.sh
  • added ops/staging/finalize-report.sh
  • added ops/staging/new-report.sh
  • added ops/staging/record-result.sh
  • added ops/staging/regression-matrix.json
  • added ops/staging/regression.sh
  • added ops/staging/run-public-smoke.sh
  • added ops/staging/scan-evidence.sh
  • added ops/staging/validate-report.sh
  • added test/openagents/staging_regression_contract_test.exs

Diff

14 files changed, +1984 -2

docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md modified +38 -1

@@ -2,7 +2,8 @@

2 2
3 3
Date: 2026-08-20
4 4
5
Status: In progress; Gates 0–11 complete locally, Gate 12 cloud and live cleanup proof pending
5
Status: In progress; Gates 0–11 complete locally, Gate 12 cloud proof pending,
6
Gate 13 live deployment pending, and the Gate 14 harness complete locally
6 7
7 8
## Outcome
8 9

@@ -1493,6 +1494,40 @@ erase the first failure; record both attempts and explain the result.

1493 1494
every failed first attempt has a documented cause and successful corrective
1494 1495
verification.
1495 1496
1497
### Gate 14 implementation status
1498
1499
Implemented locally on 2026-08-20:
1500
1501
- Added a versioned matrix with 69 uniquely identified cases across all 10
1502
  Gate 14 groups. Every case declares whether its execution is automated,
1503
  hybrid, or manual.
1504
- Added a private report generator that binds the complete result set to one
1505
  candidate Git SHA, application and builder manifest digest, release, SBOM,
1506
  and exact-SHA local gate receipt.
1507
- Added a bounded public staging smoke runner for health, status, favicon,
1508
  public pages, CSP nonce binding, and the microphone permissions policy. Its
1509
  receipt retains only status codes, content types, byte counts, policy
1510
  booleans, and hashes—not response bodies or header values.
1511
- Added a fail-closed evidence scanner, append-only attempt recorder, report
1512
  validator, and atomic state finalizer. Failed attempts remain in the report
1513
  after a successful retry. Evidence must remain inside the report directory,
1514
  match its checksum, use owner-only permissions, and pass the private-content
1515
  scan.
1516
- Added contract tests and included shell syntax plus the harness suite in the
1517
  owned contracts stage. The local dry run proves all 69 cases are present,
1518
  unsafe evidence is refused without echoing its value, and a draft cannot pass
1519
  regression validation.
1520
- Added the [staging regression runbook](operations/staging-regression.md) and
1521
  [evidence report contract](operations/staging-report-template.md), including
1522
  persistent-session handling, revision binding, fake-media voice checks,
1523
  exact-window operational truth, retries, finalization, and manifest-scoped
1524
  cleanup.
1525
1526
The harness dry run and focused tests pass without a network request. Gate 14
1527
itself remains open until Gate 12 isolation and Gate 13 deployment complete and
1528
all applicable cases pass live on one candidate. No staging or production
1529
resource changed while implementing the harness.
1530
1496 1531
## Gate 15: Run failure injection and soak staging
1497 1532
1498 1533
After functional regression passes, test the system under controlled failure.

@@ -1624,6 +1659,8 @@ each handoff.

1624 1659
- [x] Boot convergence controls readiness.
1625 1660
- [ ] Relup and rolling replacement pass their staging drills.
1626 1661
- [x] Owned local gates produce exact-SHA receipts.
1662
- [x] The versioned staging matrix, private evidence report, scanner, recorder,
1663
      validator, and network-free harness dry run exist.
1627 1664
- [ ] Web and distributed staging are isolated from production.
1628 1665
- [ ] Staging has a separate database instance and failure domain.
1629 1666
- [ ] The migration lineage is mapped and rehearsed for every nonempty target.
docs/operations/staging-regression.md added +342

@@ -0,0 +1,342 @@

1
# Run the staging regression and retain safe evidence
2
3
Date: 2026-08-20
4
5
Status: Harness implemented and locally proven; live Gate 14 execution pending
6
7
Use this runbook to test one immutable candidate on the isolated staging
8
environment and produce one checksummed report. The harness covers every Gate
9
14 case, preserves failed attempts, refuses incomplete reports, and keeps raw
10
product content and credentials out of retained evidence.
11
12
This runbook never authorizes a production action. Its only network target is
13
`https://staging.openagents.com`.
14
15
## Understand the harness boundary
16
17
The harness provides these controls:
18
19
- `regression-matrix.json` defines 69 cases in 10 groups.
20
- `regression.sh check` validates the matrix, report generator, validator,
21
  evidence scanner, and public-smoke preflight without making a network request.
22
- `new-report.sh` binds a report to the exact candidate manifest and its image,
23
  release, SBOM, and release-gate digests.
24
- `run-public-smoke.sh` makes bounded anonymous requests and retains only status,
25
  type, size, policy booleans, and response hashes.
26
- `record-result.sh` appends attempts and copies sanitized, hashed evidence into
27
  the private report directory.
28
- `finalize-report.sh` changes report state only after strict validation passes.
29
30
The scripts do not claim that browser, account, data-rights, voice, database,
31
forge, log, failure-injection, or accessibility checks happened. An operator or
32
owned test runner must execute those cases and attach bounded proof.
33
34
## Meet the prerequisites
35
36
Start only after Gates 12 and 13 produce all of the following:
37
38
- A dedicated staging project, network, database instance, web lane,
39
  distributed fleet, deployer, identities, secrets, buckets, and registry.
40
- A clean `main` commit with an exact local release-gate receipt.
41
- An immutable candidate directory produced by `publish-candidate.sh`.
42
- A completed migration classification, snapshot, copy rehearsal, migration
43
  receipt, and rollback-compatibility receipt for the actual staging target.
44
- Web and distributed nodes reporting the same candidate Git SHA and digest.
45
- A manifest-scoped disposable test run registered for every account,
46
  repository, recording, and product machine that the regression creates.
47
48
Keep the candidate directory private under `.git`. Confirm its checksum before
49
each use. Install `curl`, `jq`, `sha256sum`, `strings`, and a current browser test
50
runner on the owned test machine.
51
52
Do not start Gate 14 if the candidate changed, staging shares any production
53
failure domain, or the retained authenticated browser session cannot be safely
54
recovered.
55
56
## Prove the local harness
57
58
Run the complete network-free dry check:
59
60
```sh
61
ops/staging/regression.sh check
62
```
63
64
Expected result:
65
66
```text
67
Staging regression harness dry run passed (69 cases; no network requests sent).
68
```
69
70
Also run `mix precommit` and the exact-SHA release gate for the candidate. Do
71
not turn a failed local check into a staging-only exception.
72
73
## Create one report for the candidate
74
75
Choose a unique lowercase run ID. Point the generator at the exact candidate
76
directory:
77
78
```sh
79
candidate_sha=$(git rev-parse HEAD)
80
candidate_dir=".git/openagents/staging-candidates/$candidate_sha"
81
run_id="gate14-20260820-0001"
82
83
ops/staging/new-report.sh "$candidate_dir" "$run_id"
84
```
85
86
The report is created with mode `0600` at:
87
88
```text
89
.git/openagents/staging-reports/<candidate-sha>/<run-id>/
90
  report.json
91
  report.sha256
92
```
93
94
Do not move the working report into the source tree. Retain the final sanitized
95
report and evidence in the staging-only versioned evidence bucket after its
96
scanner and checksum pass.
97
98
Verify the draft before testing:
99
100
```sh
101
report=".git/openagents/staging-reports/$candidate_sha/$run_id/report.json"
102
ops/staging/validate-report.sh --draft "$report"
103
```
104
105
Confirm that the target, Git SHA, application and builder image digests,
106
release, SBOM, and release-gate digest match the candidate under test.
107
108
## Preserve browser and revision state
109
110
Use an incognito or isolated browser context for anonymous checks. Keep the
111
owned persistent staging browser profile for authenticated checks. Do not log
112
out that profile unless the operator has separately proved that it can be
113
restored without exposing a user credential.
114
115
After every deployment, hard-reload each persistent tab before testing. An
116
open LiveView can remain connected to a draining old revision, which makes a
117
new feature appear broken and invalidates revision-bound evidence. Confirm the
118
new revision from `/status` before the first authenticated action.
119
120
Record one UTC test window for each attempt. Do not combine evidence from an old
121
revision, a different image digest, or a different database state.
122
123
## Run the bounded public smoke
124
125
Write the smoke receipt outside the report, then attach it through the recorder:
126
127
```sh
128
smoke_root=$(mktemp -d /tmp/openagents-public-smoke.XXXXXX)
129
smoke_receipt="$smoke_root/public-smoke.json"
130
131
ops/staging/run-public-smoke.sh --run "$candidate_dir" "$smoke_receipt"
132
ops/staging/record-result.sh \
133
  "$report" public-001 passed public-smoke "$smoke_receipt"
134
```
135
136
The smoke checks `/healthz`, `/status`, `/api/status`, `/favicon.ico`, `/`,
137
`/leaderboard`, `/changelog`, `/docs`, and `/components`. JSON endpoints must
138
report the candidate SHA. The home response must bind the CSP nonce to the
139
theme bootstrap and publish the microphone permissions policy. The receipt
140
contains no response bodies or header values.
141
142
The receipt proves all of `public-001` and only the automated portions of
143
`public-002` and `public-005`. Complete the configured forge, browser-policy,
144
cookie, origin, image, microphone, and manual browser checks before marking
145
those hybrid cases passed.
146
147
## Record every attempt
148
149
Attach one sanitized evidence file to each passed attempt:
150
151
```sh
152
ops/staging/record-result.sh \
153
  "$report" chat-001 passed browser-assertions /path/to/sanitized-receipt.json
154
```
155
156
For a failure or block, put the bounded explanation in a mode-`0600` text file.
157
Do not include a prompt, message, transcript, memory value, credential, raw
158
request, or personal data:
159
160
```sh
161
reason_file=$(mktemp /tmp/openagents-staging-reason.XXXXXX)
162
chmod 600 "$reason_file"
163
# Write a short operational cause into $reason_file with an editor.
164
165
ops/staging/record-result.sh \
166
  "$report" chat-001 failed browser-assertions \
167
  /path/to/sanitized-receipt.json "$reason_file"
168
```
169
170
After correction, record a new passed attempt. The first failure and its proof
171
remain in the attempt list:
172
173
```sh
174
ops/staging/record-result.sh \
175
  "$report" chat-001 passed browser-assertions \
176
  /path/to/corrective-verification.json
177
```
178
179
Mark a case not applicable only when the architecture makes the case genuinely
180
inapplicable, not when a dependency is unavailable or a check is inconvenient:
181
182
```sh
183
ops/staging/record-result.sh \
184
  "$report" CASE_ID not_applicable /path/to/bounded-reason.txt
185
```
186
187
The recorder scans the source, copies it as mode `0600`, computes its SHA-256,
188
updates the report atomically, and reruns draft validation. It refuses edits to
189
a regression-passed or complete report. Adding a corrective attempt to a
190
`recorded` report returns it to `draft` and preserves the previous attempts.
191
192
## Execute groups in dependency order
193
194
Run the groups in this order so a lower-level failure does not contaminate a
195
higher-level result:
196
197
| Order | Group | Cases | Primary proof |
198
| --- | --- | ---: | --- |
199
| 1 | Public and browser | 6 | Public smoke, anonymous browser, policy and accessibility receipts |
200
| 2 | Authentication | 5 | OAuth boundary tests, retained-session checks, account-state receipts |
201
| 3 | Typed chat and Markdown | 6 | Browser assertions, persisted turn receipts, bounded failure checks |
202
| 4 | Memory and data rights | 6 | UI assertions, export/reset/delete receipts, scoped database counts |
203
| 5 | Voice and recording | 12 | Fake-media results, request statuses, generation and recording receipts |
204
| 6 | Leaderboard and admin | 6 | Anonymous field checks, authorization checks, operator receipts |
205
| 7 | Issues and Projects | 6 | API and LiveView checks, cross-repository refusal receipts |
206
| 8 | Machines and work | 5 | Pairing lifecycle, real harmless job, restart and cleanup receipts |
207
| 9 | Forge and deployment | 11 | Git, WAL, build, rollback, relup, and rolling-replacement receipts |
208
| 10 | Logs and truth | 6 | Exact-window log summary, content scan, and bounded database truth |
209
210
Use `ops/staging/regression-matrix.json` as the canonical case inventory. Never
211
delete, rename, or merge cases in a report. Change the versioned matrix and its
212
contract tests in a dedicated source commit if the product contract changes.
213
214
## Exercise authenticated and destructive flows safely
215
216
Use only staging-owned accounts and resources registered to the current run.
217
Confirm the authenticated owner before export, reset, or deletion. Verify the
218
reset through `#reset-conversation-form`, then confirm one greeting and no
219
remaining message or memory rows for that owner. Never infer deletion from the
220
UI alone.
221
222
Run cross-account and cross-repository checks with two staging-owned identities.
223
Prove refusal with statuses and bounded row counts. Do not capture access tokens,
224
session cookies, OAuth codes, CSRF values, response bodies, or database values.
225
226
For machine tests, keep claim and machine credentials only in process memory.
227
Use a harmless disposable project, revoke the machine, prove replay refusal,
228
and remove its ephemeral controller home and project before cleanup.
229
230
## Exercise voice with fake media
231
232
Use an owned browser harness with a synthetic audio fixture and fake-media
233
browser flags. Store authentication in the private browser profile, not in the
234
script or evidence. Only one tab may own the active voice call.
235
236
Assert these observable outcomes:
237
238
- `#voice-start`, `#voice-status`, and `#voice-end` remain usable.
239
- The call progresses through listening, speaking, interruption, and clean end.
240
- Call creation returns `201` and final deletion returns `204`.
241
- Typed input during voice persists without ending the call or starting a
242
  competing typed response.
243
- The next spoken response can use the injected typed content.
244
- Recording disclosure exists before microphone access.
245
- Recording chunks and completion use the admitted generation.
246
- An operator can play the assembled recording and an unauthorized account
247
  cannot.
248
- Recorder failure leaves both live voice and typed chat usable.
249
250
Refresh chat after the harness exits and verify durable transcript items and
251
interruption markers. Evidence should contain selector states, request status
252
codes, timestamps, sizes, generations, and hashes—not audio, transcript text,
253
SDP, provider events, or cookies. Retry a transient harness failure at most once
254
and preserve both attempts.
255
256
## Bind operational truth to the test window
257
258
Query logs for the exact candidate revision and UTC attempt window. Inspect
259
both severity errors and application error text because they can use different
260
log severities. Separate deployment-overlap connection noise from candidate
261
regressions by revision and timestamp; do not dismiss unexplained errors as
262
expected noise.
263
264
Export only a bounded summary with counts, revision, query window, and content
265
scan result. Run the evidence scanner before attachment:
266
267
```sh
268
ops/staging/scan-evidence.sh /path/to/sanitized-log-summary.json
269
```
270
271
Use direct staging database queries when the UI, cache, and persistence disagree.
272
Retain schema names, bounded counts, booleans, row ownership IDs hashed for the
273
run, and transaction timestamps. Never retain raw product columns.
274
275
## Complete the report-level evidence
276
277
Copy sanitized global receipts under the report's `evidence/` directory with
278
mode `0600`. Scan and hash each file, then add an evidence reference of this
279
shape to the matching `staging_evidence` field:
280
281
```json
282
{
283
  "path": "evidence/migration-rehearsal.json",
284
  "sha256": "64-lowercase-hex-characters",
285
  "kind": "migration-rehearsal"
286
}
287
```
288
289
Populate every field described in the
290
[staging evidence report contract](staging-report-template.md). Run draft
291
validation after each reviewable edit. The validator recomputes every referenced
292
file hash, rejects links and path escapes, requires mode `0400` or `0600`, and
293
rescans the content.
294
295
## Finalize without erasing failures
296
297
After the first complete pass, seal a report with failures or blocks as
298
`recorded`:
299
300
```sh
301
ops/staging/finalize-report.sh --recorded "$report"
302
```
303
304
The command refuses pending cases. Record corrective attempts as needed; each
305
new attempt returns the report to draft while preserving history.
306
307
When every applicable Gate 14 case passes and every common evidence field is
308
complete, seal the regression result:
309
310
```sh
311
ops/staging/finalize-report.sh --regression "$report"
312
```
313
314
Do not edit a regression-passed report. Gate 15 adds failure-injection timeline,
315
soak, and known-issue evidence to a reviewed copy, then seals it with:
316
317
```sh
318
ops/staging/finalize-report.sh --final "$report"
319
```
320
321
Verify `report.sha256`, scan the complete report directory, and upload only the
322
sanitized set to staging evidence storage. A passed Gate 14 report does not
323
authorize production.
324
325
## Clean up the disposable run
326
327
Quiesce the harness so it cannot create more resources. Preview the exact
328
manifest-scoped cleanup:
329
330
```sh
331
ops/staging/cleanup-run.sh "$run_id" check
332
```
333
334
Apply cleanup only after the bounded counts match the run:
335
336
```sh
337
ops/staging/cleanup-run.sh "$run_id" --apply
338
```
339
340
Prove the run manifest is empty, machine credentials no longer work, the
341
ephemeral controller files are gone, and retained evidence still validates.
342
Cleanup never converts a failed regression into a pass.
docs/operations/staging-report-template.md added +115

@@ -0,0 +1,115 @@

1
# Staging evidence report contract
2
3
Date: 2026-08-20
4
5
Status: Current Gate 14 and Gate 15 evidence contract
6
7
`ops/staging/new-report.sh` generates the machine-readable report template from
8
one immutable candidate and the versioned 69-case regression matrix. Keep the
9
working report private under `.git/openagents/staging-reports/`; retain only a
10
sanitized, checksummed result in staging evidence storage.
11
12
## Report identity
13
14
The generator fixes these values. Do not edit them:
15
16
- Schema and matrix revision.
17
- Run ID, creation time, staging target, project, and region.
18
- Git SHA and `main` branch.
19
- Candidate-manifest checksum.
20
- Application and builder image references and manifest digests.
21
- Release version and checksum.
22
- SBOM and exact-SHA release-gate checksums.
23
24
The application and builder references must end in their recorded manifest
25
digests. Tags alone are not evidence.
26
27
## Result contract
28
29
The generator creates one result for each matrix case. Each result keeps its
30
group, title, execution class, status, reason, attempts, and aggregate evidence.
31
32
Allowed statuses are:
33
34
- `pending`: no attempt or reason exists.
35
- `passed`: the last attempt passed and evidence exists.
36
- `failed`: the last attempt failed, evidence exists, and a reason exists.
37
- `blocked`: the last attempt was blocked, evidence exists, and a reason exists.
38
- `not_applicable`: a bounded architectural reason exists.
39
40
Attempt ordinals start at 1 and remain contiguous. Every retry appends an
41
attempt; it never replaces the failed observation. Each attempt records UTC
42
start and completion timestamps, outcome, retry classification, bounded
43
explanation, and evidence references.
44
45
Use `record-result.sh` instead of hand-editing result arrays.
46
47
## Common staging evidence
48
49
Gate 14 regression finalization requires all of these fields:
50
51
| Field | Required value |
52
| --- | --- |
53
| `migration.classification` | `empty_current`, `known_prior`, or `already_baselined` |
54
| `migration.snapshot_receipt` | Sanitized pre-migration snapshot receipt |
55
| `migration.rehearsal_receipt` | Disposable-copy rehearsal receipt |
56
| `migration.migration_versions_receipt` | Exact applied-version receipt |
57
| `migration.rollback_compatibility_receipt` | Last-known-good compatibility receipt |
58
| `configuration_readiness_receipt` | Redacted staging-only configuration result |
59
| `local_gate.default_test_count` | Positive default-suite test count |
60
| `local_gate.cluster_test_count` | Positive cluster-suite test count |
61
| `local_gate.javascript_test_count` | Positive browser JavaScript test count |
62
| `local_gate.coverage_summary_receipt` | Merged bounded coverage receipt |
63
| `deployment.web_revision` | Exact staging web revision name |
64
| `deployment.web_image_digest` | Candidate application manifest digest |
65
| `deployment.distributed_node_release_receipt` | Same-SHA fleet identity receipt |
66
| `forge.build_receipt` | Immutable builder input and artifact receipt |
67
| `forge.deployment_receipt` | Transactional fleet deployment receipt |
68
| `forge.rollback_receipt` | Complete rollback receipt |
69
| `forge.relup_receipt` | Upgrade, downgrade, and re-upgrade receipt |
70
| `forge.rolling_replacement_receipt` | Drain-bounded replacement receipt |
71
| `sanitized_artifacts` | One or more sanitized screenshots, recordings, or UI receipts |
72
73
An evidence reference contains only a relative `evidence/` path, SHA-256, and
74
short kind. Referenced files must be regular, unlinked, mode `0400` or `0600`,
75
inside the report directory, checksum-correct, no larger than 50 MiB, and safe
76
under `scan-evidence.sh`.
77
78
## Gate 15 evidence
79
80
Final completion adds:
81
82
- One or more evidence references in `failure_injection_timeline`.
83
- A 48-hour `soak_receipt` that includes candidate identity, start, end,
84
  scheduled canary counts, bounded resource summaries, and post-soak smoke.
85
- Every known issue with ID, owner, severity, and disposition.
86
87
An empty known-issue list is valid only when review found no issues. A failure,
88
retry, crash, stale row, divergent node, leak, or content-bearing log must not
89
disappear from the timeline because it was later corrected.
90
91
## Report states
92
93
The validator recognizes four states:
94
95
| State | Validator | Meaning |
96
| --- | --- | --- |
97
| `draft` | `--draft` | Work is in progress; pending and failed cases may exist |
98
| `recorded` | `--recorded` | Every case has an outcome; failures or blocks may remain |
99
| `regression_passed` | `--regression` | Every applicable Gate 14 case passed and common evidence is complete |
100
| `complete` | `--final` | Gate 14, failure injection, and the 48-hour soak all passed |
101
102
Only `finalize-report.sh` should change the state and completion timestamp. It
103
validates a temporary copy before replacing the report and recomputes
104
`report.sha256`.
105
106
## Prohibited content
107
108
Never retain session cookies, OAuth codes, access or refresh tokens, database
109
passwords, machine credentials, provider keys, private keys, authenticated
110
database URLs, raw prompts, messages, transcripts, memory values, tool payloads,
111
SDP, audio, or private administrative content.
112
113
Prefer counts, booleans, status codes, timestamps, bounded IDs, digests, and
114
content hashes. A passing scanner reduces accidental disclosure; it does not
115
replace human review.
infra/staging/README.md modified +4 -1

@@ -36,7 +36,10 @@ staging-only secret versions during Gate 13.

36 36
After this boundary exists, use the
37 37
[immutable candidate runbook](../../docs/operations/staging-candidate-artifacts.md)
38 38
to publish the exact application image, builder image, release archive, SBOM,
39
and candidate manifest for Gate 13.
39
and candidate manifest for Gate 13. Use the
40
[staging regression runbook](../../docs/operations/staging-regression.md) only
41
after the candidate and selected migration path are proven on this isolated
42
target.
40 43
41 44
## Prerequisites
42 45
ops/ci/contracts.sh modified +8

@@ -9,7 +9,14 @@ cd "$repo_root"

9 9
ops/ci/reference-check.sh
10 10
sh -n rel/overlays/bin/migration-lineage
11 11
sh -n ops/staging/cleanup-run.sh
12
sh -n ops/staging/finalize-report.sh
13
sh -n ops/staging/new-report.sh
12 14
sh -n ops/staging/publish-candidate.sh
15
sh -n ops/staging/record-result.sh
16
sh -n ops/staging/regression.sh
17
sh -n ops/staging/run-public-smoke.sh
18
sh -n ops/staging/scan-evidence.sh
19
sh -n ops/staging/validate-report.sh
13 20
elixir ops/ci/docs-check.exs
14 21
MIX_ENV=test mix test --warnings-as-errors \
15 22
  test/openagents/log_safety_test.exs \

@@ -17,6 +24,7 @@ MIX_ENV=test mix test --warnings-as-errors \

17 24
  test/openagents/runtime_config_test.exs \
18 25
  test/openagents/staging_cleanup_test.exs \
19 26
  test/openagents/staging_candidate_contract_test.exs \
27
  test/openagents/staging_regression_contract_test.exs \
20 28
  test/openagents_web/icon_affordances_test.exs \
21 29
  test/openagents_web/icons_test.exs \
22 30
  test/openagents_web/ui_test.exs
ops/staging/finalize-report.sh added +55

@@ -0,0 +1,55 @@

1
#!/bin/sh
2
set -eu
3
4
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
mode=${1:-}
6
report=${2:-}
7
8
case "$mode" in
9
  --recorded) state=recorded ;;
10
  --regression) state=regression_passed ;;
11
  --final) state=complete ;;
12
  *)
13
    echo "usage: ops/staging/finalize-report.sh [--recorded|--regression|--final] REPORT" >&2
14
    exit 64
15
    ;;
16
esac
17
18
if [ "$#" -ne 2 ] || [ ! -f "$report" ] || [ -L "$report" ]; then
19
  echo "REPORT must be a regular, unlinked file" >&2
20
  exit 1
21
fi
22
23
for command_name in jq sha256sum; do
24
  if ! command -v "$command_name" >/dev/null 2>&1; then
25
    echo "$command_name is required to finalize a staging report" >&2
26
    exit 1
27
  fi
28
done
29
30
report_dir=$(CDPATH= cd -- "$(dirname -- "$report")" && pwd)
31
report_temp=$(mktemp "$report_dir/.report.XXXXXX")
32
cleanup() {
33
  unlink "$report_temp" 2>/dev/null || true
34
}
35
trap cleanup EXIT INT TERM
36
37
completed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
38
jq --arg state "$state" --arg completed_at "$completed_at" '
39
  .state = $state |
40
  .completed_at = $completed_at
41
' "$report" >"$report_temp"
42
chmod 600 "$report_temp"
43
44
"$script_dir/validate-report.sh" "$mode" "$report_temp" >/dev/null || {
45
  echo "report remains unchanged because $mode validation failed" >&2
46
  exit 1
47
}
48
49
mv "$report_temp" "$report"
50
report_temp=
51
report_sha256=$(sha256sum "$report" | cut -d ' ' -f 1)
52
printf '%s  report.json\n' "$report_sha256" >"$report_dir/report.sha256"
53
chmod 600 "$report_dir/report.sha256"
54
55
echo "Staging report finalized as $state."
ops/staging/new-report.sh added +254

@@ -0,0 +1,254 @@

1
#!/bin/sh
2
set -eu
3
4
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
repo_root=$(CDPATH= cd -- "$script_dir/../.." && pwd)
6
matrix="$script_dir/regression-matrix.json"
7
mode=${1:-}
8
umask 077
9
10
usage() {
11
  echo "usage: ops/staging/new-report.sh CANDIDATE_DIRECTORY RUN_ID" >&2
12
  echo "       ops/staging/new-report.sh --dry-run OUTPUT" >&2
13
  exit 64
14
}
15
16
for command_name in git jq sha256sum; do
17
  if ! command -v "$command_name" >/dev/null 2>&1; then
18
    echo "$command_name is required to create a staging report" >&2
19
    exit 1
20
  fi
21
done
22
23
generated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
24
25
write_report() {
26
  candidate_manifest=$1
27
  candidate_manifest_sha256=$2
28
  run_id=$3
29
  synthetic=$4
30
  output=$5
31
32
  jq -n \
33
    --slurpfile matrix "$matrix" \
34
    --slurpfile candidate "$candidate_manifest" \
35
    --arg run_id "$run_id" \
36
    --arg created_at "$generated_at" \
37
    --arg candidate_manifest_sha256 "$candidate_manifest_sha256" \
38
    --argjson synthetic "$synthetic" '
39
    ($matrix[0]) as $matrix |
40
    ($candidate[0]) as $candidate |
41
    {
42
      schema: "openagents.staging-report.v1",
43
      matrix_revision: $matrix.revision,
44
      state: "draft",
45
      synthetic: $synthetic,
46
      run_id: $run_id,
47
      created_at: $created_at,
48
      completed_at: null,
49
      target: {
50
        environment: "staging",
51
        base_url: "https://staging.openagents.com",
52
        project: $candidate.target.project,
53
        region: $candidate.target.region
54
      },
55
      candidate: {
56
        git_sha: $candidate.git_sha,
57
        branch: $candidate.branch,
58
        candidate_manifest_sha256: $candidate_manifest_sha256,
59
        application_image: $candidate.images.application.reference,
60
        application_manifest_digest: $candidate.images.application.manifest_digest,
61
        builder_image: $candidate.images.builder.reference,
62
        builder_manifest_digest: $candidate.images.builder.manifest_digest,
63
        release_version: $candidate.release.version,
64
        release_sha256: $candidate.release.sha256,
65
        sbom_sha256: $candidate.sbom.sha256,
66
        release_gate_sha256: $candidate.receipts.release_gate_sha256
67
      },
68
      staging_evidence: {
69
        migration: {
70
          classification: null,
71
          snapshot_receipt: null,
72
          rehearsal_receipt: null,
73
          migration_versions_receipt: null,
74
          rollback_compatibility_receipt: null
75
        },
76
        configuration_readiness_receipt: null,
77
        local_gate: {
78
          default_test_count: null,
79
          cluster_test_count: null,
80
          javascript_test_count: null,
81
          coverage_summary_receipt: null
82
        },
83
        deployment: {
84
          web_revision: null,
85
          web_image_digest: null,
86
          distributed_node_release_receipt: null
87
        },
88
        forge: {
89
          build_receipt: null,
90
          deployment_receipt: null,
91
          rollback_receipt: null,
92
          relup_receipt: null,
93
          rolling_replacement_receipt: null
94
        },
95
        sanitized_artifacts: [],
96
        failure_injection_timeline: [],
97
        soak_receipt: null,
98
        known_issues: []
99
      },
100
      results: [
101
        $matrix.groups[] as $group |
102
        $group.cases[] |
103
        {
104
          id: .id,
105
          group: $group.id,
106
          title: .title,
107
          execution: .execution,
108
          status: "pending",
109
          reason: null,
110
          attempts: [],
111
          evidence: []
112
        }
113
      ]
114
    }
115
  ' >"$output"
116
}
117
118
if [ "$mode" = "--dry-run" ]; then
119
  [ "$#" -eq 2 ] || usage
120
  output=$2
121
122
  if [ -e "$output" ]; then
123
    echo "dry-run report output already exists" >&2
124
    exit 1
125
  fi
126
127
  dry_root=$(mktemp -d /tmp/openagents-staging-report-dry-run.XXXXXX)
128
  cleanup() {
129
    find "$dry_root" -depth -delete 2>/dev/null || true
130
  }
131
  trap cleanup EXIT INT TERM
132
133
  git_sha=$(git -C "$repo_root" rev-parse --verify HEAD)
134
  zero_digest=0000000000000000000000000000000000000000000000000000000000000000
135
  candidate_manifest="$dry_root/candidate-manifest.json"
136
137
  jq -n \
138
    --arg git_sha "$git_sha" \
139
    --arg digest "sha256:$zero_digest" \
140
    --arg sha256 "$zero_digest" '
141
    {
142
      schema: "openagents.staging-candidate.v1",
143
      git_sha: $git_sha,
144
      branch: "main",
145
      target: {environment: "staging", project: "openagents-staging-dry-run", region: "us-central1"},
146
      images: {
147
        application: {
148
          reference: ("us-central1-docker.pkg.dev/openagents-staging-dry-run/openagents-staging/openagents@" + $digest),
149
          manifest_digest: $digest
150
        },
151
        builder: {
152
          reference: ("us-central1-docker.pkg.dev/openagents-staging-dry-run/openagents-staging/openagents-builder@" + $digest),
153
          manifest_digest: $digest
154
        }
155
      },
156
      release: {version: "dry-run", sha256: $sha256},
157
      sbom: {sha256: $sha256},
158
      receipts: {release_gate_sha256: $sha256}
159
    }
160
  ' >"$candidate_manifest"
161
162
  write_report "$candidate_manifest" "$zero_digest" dry-run true "$output"
163
  chmod 600 "$output"
164
  exit 0
165
fi
166
167
[ "$#" -eq 2 ] || usage
168
candidate_dir=$1
169
run_id=$2
170
171
case "$run_id" in
172
  [a-z0-9]*[a-z0-9]) ;;
173
  [a-z0-9]) ;;
174
  *) echo "RUN_ID must use lowercase letters, digits, and interior hyphens" >&2; exit 1 ;;
175
esac
176
177
case "$run_id" in
178
  *[!a-z0-9-]* | *--* | *- | -* )
179
    echo "RUN_ID must use lowercase letters, digits, and single interior hyphens" >&2
180
    exit 1
181
    ;;
182
esac
183
184
if [ "${#run_id}" -gt 63 ]; then
185
  echo "RUN_ID must contain at most 63 characters" >&2
186
  exit 1
187
fi
188
189
candidate_manifest="$candidate_dir/candidate-manifest.json"
190
candidate_checksum="$candidate_dir/candidate-manifest.sha256"
191
192
if [ ! -f "$candidate_manifest" ] || [ ! -f "$candidate_checksum" ]; then
193
  echo "candidate directory must contain the manifest and its checksum" >&2
194
  exit 1
195
fi
196
197
(cd "$candidate_dir" && sha256sum --check --strict candidate-manifest.sha256 >/dev/null)
198
199
jq -e '
200
  . as $manifest |
201
  .schema == "openagents.staging-candidate.v1" and
202
  (.git_sha | test("^[0-9a-f]{40}$")) and
203
  .branch == "main" and
204
  .target.environment == "staging" and
205
  (.target.project | test("stag"; "i")) and
206
  (.target.region | type == "string" and length > 0) and
207
  (.images.application.manifest_digest | test("^sha256:[0-9a-f]{64}$")) and
208
  (.images.builder.manifest_digest | test("^sha256:[0-9a-f]{64}$")) and
209
  (.images.application.reference | endswith("@" + $manifest.images.application.manifest_digest)) and
210
  (.images.builder.reference | endswith("@" + $manifest.images.builder.manifest_digest)) and
211
  (.release.sha256 | test("^[0-9a-f]{64}$")) and
212
  (.sbom.sha256 | test("^[0-9a-f]{64}$")) and
213
  (.receipts.release_gate_sha256 | test("^[0-9a-f]{64}$"))
214
' "$candidate_manifest" >/dev/null || {
215
  echo "candidate manifest does not satisfy the staging report contract" >&2
216
  exit 1
217
}
218
219
git_sha=$(jq -r '.git_sha' "$candidate_manifest")
220
candidate_manifest_sha256=$(sha256sum "$candidate_manifest" | cut -d ' ' -f 1)
221
evidence_root="$repo_root/.git/openagents/staging-reports/$git_sha"
222
report_root="$evidence_root/$run_id"
223
report_temp=
224
225
if [ -e "$report_root" ]; then
226
  echo "staging report already exists for this candidate and run ID" >&2
227
  exit 1
228
fi
229
230
umask 077
231
mkdir -p "$evidence_root"
232
report_temp=$(mktemp -d "$evidence_root/.report.$run_id.XXXXXX")
233
234
cleanup() {
235
  if [ -n "$report_temp" ] && [ -d "$report_temp" ]; then
236
    find "$report_temp" -depth -delete 2>/dev/null || true
237
  fi
238
}
239
trap cleanup EXIT INT TERM
240
241
write_report \
242
  "$candidate_manifest" \
243
  "$candidate_manifest_sha256" \
244
  "$run_id" \
245
  false \
246
  "$report_temp/report.json"
247
248
report_sha256=$(sha256sum "$report_temp/report.json" | cut -d ' ' -f 1)
249
printf '%s  report.json\n' "$report_sha256" >"$report_temp/report.sha256"
250
mv "$report_temp" "$report_root"
251
report_temp=
252
253
echo "Created staging report for $git_sha"
254
echo "Report: .git/openagents/staging-reports/$git_sha/$run_id/report.json"
ops/staging/record-result.sh added +204

@@ -0,0 +1,204 @@

1
#!/bin/sh
2
set -eu
3
4
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
6
usage() {
7
  echo "usage: ops/staging/record-result.sh REPORT CASE_ID passed EVIDENCE_KIND EVIDENCE_FILE" >&2
8
  echo "       ops/staging/record-result.sh REPORT CASE_ID failed EVIDENCE_KIND EVIDENCE_FILE REASON_FILE" >&2
9
  echo "       ops/staging/record-result.sh REPORT CASE_ID blocked EVIDENCE_KIND EVIDENCE_FILE REASON_FILE" >&2
10
  echo "       ops/staging/record-result.sh REPORT CASE_ID not_applicable REASON_FILE" >&2
11
  exit 64
12
}
13
14
[ "$#" -ge 4 ] || usage
15
report=$1
16
case_id=$2
17
outcome=$3
18
19
if [ ! -f "$report" ] || [ -L "$report" ]; then
20
  echo "REPORT must be a regular, unlinked file" >&2
21
  exit 1
22
fi
23
24
for command_name in jq sha256sum; do
25
  if ! command -v "$command_name" >/dev/null 2>&1; then
26
    echo "$command_name is required to record a staging result" >&2
27
    exit 1
28
  fi
29
done
30
31
case "$case_id" in
32
  [a-z]*-[0-9][0-9][0-9]) ;;
33
  *) echo "CASE_ID has an invalid shape" >&2; exit 1 ;;
34
esac
35
36
evidence_kind=
37
evidence_source=
38
reason_file=/dev/null
39
40
case "$outcome" in
41
  passed)
42
    [ "$#" -eq 5 ] || usage
43
    evidence_kind=$4
44
    evidence_source=$5
45
    ;;
46
  failed | blocked)
47
    [ "$#" -eq 6 ] || usage
48
    evidence_kind=$4
49
    evidence_source=$5
50
    reason_file=$6
51
    ;;
52
  not_applicable)
53
    [ "$#" -eq 4 ] || usage
54
    reason_file=$4
55
    ;;
56
  *) usage ;;
57
esac
58
59
report_state=$(jq -r '.state // empty' "$report")
60
case "$report_state" in
61
  draft) "$script_dir/validate-report.sh" --draft "$report" >/dev/null ;;
62
  recorded) "$script_dir/validate-report.sh" --recorded "$report" >/dev/null ;;
63
  *)
64
    echo "only draft or recorded reports accept result changes" >&2
65
    exit 1
66
    ;;
67
esac
68
69
if ! jq -e --arg case_id "$case_id" '
70
  any(.results[]; .id == $case_id)
71
' "$report" >/dev/null; then
72
  echo "CASE_ID is not present in the staging matrix" >&2
73
  exit 1
74
fi
75
76
if [ "$reason_file" != /dev/null ]; then
77
  if [ ! -f "$reason_file" ] || [ -L "$reason_file" ]; then
78
    echo "REASON_FILE must be a regular, unlinked file" >&2
79
    exit 1
80
  fi
81
82
  reason_size=$(wc -c <"$reason_file" | tr -d ' ')
83
  if [ "$reason_size" -lt 1 ] || [ "$reason_size" -gt 500 ]; then
84
    echo "REASON_FILE must contain 1 through 500 bytes" >&2
85
    exit 1
86
  fi
87
  "$script_dir/scan-evidence.sh" "$reason_file" >/dev/null
88
fi
89
90
if ! jq -e -Rs 'rtrimstr("\n") | (length >= 0)' "$reason_file" >/dev/null; then
91
  echo "REASON_FILE is not valid UTF-8 text" >&2
92
  exit 1
93
fi
94
95
report_dir=$(CDPATH= cd -- "$(dirname -- "$report")" && pwd)
96
evidence_dir="$report_dir/evidence"
97
report_temp=
98
evidence_destination=
99
100
cleanup() {
101
  if [ -n "$report_temp" ] && [ -f "$report_temp" ]; then
102
    unlink "$report_temp" 2>/dev/null || true
103
  fi
104
  if [ -n "$evidence_destination" ] && [ -f "$evidence_destination" ]; then
105
    unlink "$evidence_destination" 2>/dev/null || true
106
  fi
107
}
108
trap cleanup EXIT INT TERM
109
110
umask 077
111
mkdir -p "$evidence_dir"
112
113
ordinal=$(jq -r --arg case_id "$case_id" '
114
  (.results[] | select(.id == $case_id) | .attempts | length) + 1
115
' "$report")
116
started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
117
completed_at=$started_at
118
evidence_relative=
119
evidence_sha256=
120
121
if [ "$outcome" != not_applicable ]; then
122
  if [ ! -f "$evidence_source" ] || [ -L "$evidence_source" ]; then
123
    echo "EVIDENCE_FILE must be a regular, unlinked file" >&2
124
    exit 1
125
  fi
126
127
  case "$evidence_kind" in
128
    "" | *[!A-Za-z0-9._/-]*)
129
      echo "EVIDENCE_KIND must be a short machine-readable label" >&2
130
      exit 1
131
      ;;
132
  esac
133
134
  if [ "${#evidence_kind}" -gt 80 ]; then
135
    echo "EVIDENCE_KIND must contain at most 80 characters" >&2
136
    exit 1
137
  fi
138
139
  "$script_dir/scan-evidence.sh" "$evidence_source" >/dev/null
140
  evidence_relative="evidence/$case_id-attempt-$ordinal.bin"
141
  evidence_destination="$report_dir/$evidence_relative"
142
143
  if [ -e "$evidence_destination" ]; then
144
    echo "evidence destination already exists" >&2
145
    exit 1
146
  fi
147
148
  cp "$evidence_source" "$evidence_destination"
149
  chmod 600 "$evidence_destination"
150
  evidence_sha256=$(sha256sum "$evidence_destination" | cut -d ' ' -f 1)
151
fi
152
153
report_temp=$(mktemp "$report_dir/.report.XXXXXX")
154
155
jq \
156
  --arg case_id "$case_id" \
157
  --arg outcome "$outcome" \
158
  --arg started_at "$started_at" \
159
  --arg completed_at "$completed_at" \
160
  --arg evidence_kind "$evidence_kind" \
161
  --arg evidence_relative "$evidence_relative" \
162
  --arg evidence_sha256 "$evidence_sha256" \
163
  --rawfile reason "$reason_file" '
164
  ($reason | rtrimstr("\n")) as $reason |
165
  .state = "draft" |
166
  .completed_at = null |
167
  .results |= map(
168
    if .id != $case_id then .
169
    elif $outcome == "not_applicable" then
170
      .status = "not_applicable" |
171
      .reason = $reason
172
    else
173
      ({path: $evidence_relative, sha256: $evidence_sha256, kind: $evidence_kind}) as $evidence |
174
      .status = $outcome |
175
      .reason = (if $outcome == "passed" and $reason == "" then .reason else $reason end) |
176
      .attempts += [{
177
        ordinal: ((.attempts | length) + 1),
178
        outcome: $outcome,
179
        started_at: $started_at,
180
        completed_at: $completed_at,
181
        automatic_retry: false,
182
        explanation: (if $reason == "" then null else $reason end),
183
        evidence: [$evidence]
184
      }] |
185
      .evidence += [$evidence]
186
    end
187
  )
188
' "$report" >"$report_temp"
189
chmod 600 "$report_temp"
190
191
"$script_dir/validate-report.sh" --draft "$report_temp" >/dev/null
192
mv "$report_temp" "$report"
193
report_temp=
194
195
report_sha256=$(sha256sum "$report" | cut -d ' ' -f 1)
196
printf '%s  report.json\n' "$report_sha256" >"$report_dir/report.sha256"
197
chmod 600 "$report_dir/report.sha256"
198
evidence_destination=
199
200
if [ "$outcome" = not_applicable ]; then
201
  echo "Recorded not_applicable for $case_id."
202
else
203
  echo "Recorded $outcome for $case_id (attempt $ordinal)."
204
fi
ops/staging/regression-matrix.json added +135

@@ -0,0 +1,135 @@

1
{
2
  "schema": "openagents.staging-regression-matrix.v1",
3
  "revision": 1,
4
  "groups": [
5
    {
6
      "id": "public_browser",
7
      "title": "Public and browser surfaces",
8
      "cases": [
9
        {"id": "public-001", "title": "Bounded health, status, API status, and favicon responses", "execution": "automated"},
10
        {"id": "public-002", "title": "Anonymous public pages and configured public Forge pages render", "execution": "hybrid"},
11
        {"id": "public-003", "title": "Hidden, private, operator, and unconfigured Forge surfaces do not disclose existence", "execution": "hybrid"},
12
        {"id": "public-004", "title": "LiveView reconnects after a transient network interruption", "execution": "manual"},
13
        {"id": "public-005", "title": "CSP, cookies, origins, images, and microphone policies match the architecture", "execution": "hybrid"},
14
        {"id": "public-006", "title": "Critical flows pass responsive, keyboard, reduced-motion, and screen-reader checks", "execution": "manual"}
15
      ]
16
    },
17
    {
18
      "id": "authentication",
19
      "title": "Authentication and account state",
20
      "cases": [
21
        {"id": "auth-001", "title": "GitHub OAuth state, PKCE, expiry, replay, callback errors, and banned users behave correctly", "execution": "hybrid"},
22
        {"id": "auth-002", "title": "The retained staging browser session remains recoverable", "execution": "manual"},
23
        {"id": "auth-003", "title": "Session renewal, logout, and concurrent-browser continuity behave correctly", "execution": "manual"},
24
        {"id": "auth-004", "title": "GitHub token retention or disposal matches the declared contract", "execution": "hybrid"},
25
        {"id": "auth-005", "title": "Account export, reset, and deletion affect only the authenticated owner", "execution": "hybrid"}
26
      ]
27
    },
28
    {
29
      "id": "chat_markdown",
30
      "title": "Typed chat and Markdown",
31
      "cases": [
32
        {"id": "chat-001", "title": "A first conversation creates one greeting and one canonical conversation", "execution": "hybrid"},
33
        {"id": "chat-002", "title": "Typed messages stream, persist, reload, paginate, cancel, fail, and recover", "execution": "hybrid"},
34
        {"id": "chat-003", "title": "The staging reset removes messages and memory and restores one greeting", "execution": "hybrid"},
35
        {"id": "chat-004", "title": "Markdown supports admitted structures and refuses unsafe or oversized input", "execution": "hybrid"},
36
        {"id": "chat-005", "title": "Tool activity remains bounded and content-free", "execution": "hybrid"},
37
        {"id": "chat-006", "title": "Rate, size, continuation, and tool budgets fail honestly", "execution": "hybrid"}
38
      ]
39
    },
40
    {
41
      "id": "memory_data_rights",
42
      "title": "Memory and data rights",
43
      "cases": [
44
        {"id": "memory-001", "title": "Remember, list, search, correct, forget, export, reset, and delete flows pass", "execution": "hybrid"},
45
        {"id": "memory-002", "title": "Activity chips and memory panels match PostgreSQL after reload", "execution": "hybrid"},
46
        {"id": "memory-003", "title": "Cross-account and cross-conversation reads fail", "execution": "hybrid"},
47
        {"id": "memory-004", "title": "Snapshot fences exclude writes made after capture", "execution": "hybrid"},
48
        {"id": "memory-005", "title": "Semantic failure returns the documented lexical fallback", "execution": "hybrid"},
49
        {"id": "memory-006", "title": "Direct database truth resolves ambiguous UI or cache behavior", "execution": "manual"}
50
      ]
51
    },
52
    {
53
      "id": "voice_recording",
54
      "title": "Voice and recording",
55
      "cases": [
56
        {"id": "voice-001", "title": "Voice controls drive a clean call lifecycle", "execution": "hybrid"},
57
        {"id": "voice-002", "title": "The fake-media harness observes listening, speaking, interruption, and clean end", "execution": "automated"},
58
        {"id": "voice-003", "title": "Voice creation and final deletion return the expected statuses", "execution": "automated"},
59
        {"id": "voice-004", "title": "Typed input during voice persists without ending or competing with the call", "execution": "hybrid"},
60
        {"id": "voice-005", "title": "The next spoken response can use injected typed content", "execution": "manual"},
61
        {"id": "voice-006", "title": "Voice tool calls complete within their budgets", "execution": "hybrid"},
62
        {"id": "voice-007", "title": "Reloaded chat shows durable voice transcript and interruption markers", "execution": "manual"},
63
        {"id": "voice-008", "title": "Recording disclosure appears before microphone access", "execution": "hybrid"},
64
        {"id": "voice-009", "title": "Recording chunks and completion carry the correct generation", "execution": "automated"},
65
        {"id": "voice-010", "title": "Only an operator can play an assembled recording", "execution": "hybrid"},
66
        {"id": "voice-011", "title": "Recording layout, encryption, truncation, retention, export, and deletion match the invariant", "execution": "hybrid"},
67
        {"id": "voice-012", "title": "Recorder failure leaves voice and typed chat usable", "execution": "automated"}
68
      ]
69
    },
70
    {
71
      "id": "leaderboard_admin",
72
      "title": "Leaderboard and administrative surfaces",
73
      "cases": [
74
        {"id": "admin-001", "title": "Anonymous leaderboard rows expose only published fields", "execution": "hybrid"},
75
        {"id": "admin-002", "title": "Typed and voice usage refresh the leaderboard without per-viewer database queries", "execution": "hybrid"},
76
        {"id": "admin-003", "title": "Operator allowlisting uses immutable GitHub IDs", "execution": "hybrid"},
77
        {"id": "admin-004", "title": "Unauthorized admin routes match the documented unauthenticated path", "execution": "hybrid"},
78
        {"id": "admin-005", "title": "Promotion accepts only a pushed commit and writes an immutable operator receipt", "execution": "hybrid"},
79
        {"id": "admin-006", "title": "Administrative pages expose no private product or credential content", "execution": "manual"}
80
      ]
81
    },
82
    {
83
      "id": "issues_projects",
84
      "title": "Issues and Projects",
85
      "cases": [
86
        {"id": "tracker-001", "title": "Public reads and authenticated writes follow route policy", "execution": "hybrid"},
87
        {"id": "tracker-002", "title": "Tracker endpoints return documented statuses and shapes", "execution": "hybrid"},
88
        {"id": "tracker-003", "title": "Tracker LiveViews cover empty, populated, validation, and interaction states", "execution": "hybrid"},
89
        {"id": "tracker-004", "title": "Repository boundaries prevent cross-repository issue effects", "execution": "hybrid"},
90
        {"id": "tracker-005", "title": "Issue and milestone numbers can repeat across repositories", "execution": "hybrid"},
91
        {"id": "tracker-006", "title": "Pagination, filters, malformed input, missing rows, size limits, and rate limits behave consistently", "execution": "hybrid"}
92
      ]
93
    },
94
    {
95
      "id": "machines_work",
96
      "title": "Machines and delegated work",
97
      "cases": [
98
        {"id": "machine-001", "title": "Pairing, one-time claim, approval, inventory, state, and revoke work", "execution": "hybrid"},
99
        {"id": "machine-002", "title": "Machine token replay fails after claim and revoke", "execution": "automated"},
100
        {"id": "machine-003", "title": "A harmless real coding-agent job reports live and terminal state in chat", "execution": "manual"},
101
        {"id": "machine-004", "title": "Cancellation and worker restart preserve committed evidence without duplicate execution", "execution": "hybrid"},
102
        {"id": "machine-005", "title": "Cleanup removes the ephemeral controller home and disposable project", "execution": "manual"}
103
      ]
104
    },
105
    {
106
      "id": "forge_deployment",
107
      "title": "Forge and deployment",
108
      "cases": [
109
        {"id": "forge-001", "title": "Clone, fetch, and push work against the staging Forge", "execution": "automated"},
110
        {"id": "forge-002", "title": "WAL and mirror receipts match pushed refs", "execution": "hybrid"},
111
        {"id": "forge-003", "title": "An allowlisted change passes build, canary, fleet transaction, and live receipt", "execution": "automated"},
112
        {"id": "forge-004", "title": "An off-allowlist module refuses the complete direct-load candidate", "execution": "automated"},
113
        {"id": "forge-005", "title": "Invalid artifact identities and shapes fail before loading", "execution": "automated"},
114
        {"id": "forge-006", "title": "Remote timeout and load failure restore every affected node", "execution": "automated"},
115
        {"id": "forge-007", "title": "A cold replacement node converges before readiness", "execution": "automated"},
116
        {"id": "forge-008", "title": "Stateful relup preserves PID and state through upgrade, downgrade, and re-upgrade", "execution": "automated"},
117
        {"id": "forge-009", "title": "Interrupted relup returns to the prior permanent release and permits retry", "execution": "automated"},
118
        {"id": "forge-010", "title": "Structural rolling replacement drains only one node at a time", "execution": "automated"},
119
        {"id": "forge-011", "title": "Failed rolling replacement stops before another node drains", "execution": "automated"}
120
      ]
121
    },
122
    {
123
      "id": "logs_truth",
124
      "title": "Logs and operational truth",
125
      "cases": [
126
        {"id": "logs-001", "title": "Log queries bind to the exact revision and test window", "execution": "automated"},
127
        {"id": "logs-002", "title": "Severity errors and application error text are both inspected", "execution": "hybrid"},
128
        {"id": "logs-003", "title": "Deploy-overlap connection noise is separated from regressions", "execution": "manual"},
129
        {"id": "logs-004", "title": "Failures correlate through bounded request and receipt IDs", "execution": "manual"},
130
        {"id": "logs-005", "title": "Direct database truth distinguishes projection and persistence failures", "execution": "manual"},
131
        {"id": "logs-006", "title": "Logs contain no secrets or private product content", "execution": "hybrid"}
132
      ]
133
    }
134
  ]
135
}
ops/staging/regression.sh added +86

@@ -0,0 +1,86 @@

1
#!/bin/sh
2
set -eu
3
4
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
repo_root=$(CDPATH= cd -- "$script_dir/../.." && pwd)
6
matrix="$script_dir/regression-matrix.json"
7
mode=${1:-}
8
9
if [ "$mode" != check ] || [ "$#" -ne 1 ]; then
10
  echo "usage: ops/staging/regression.sh check" >&2
11
  exit 64
12
fi
13
14
for command_name in git jq; do
15
  if ! command -v "$command_name" >/dev/null 2>&1; then
16
    echo "$command_name is required to check the staging regression harness" >&2
17
    exit 1
18
  fi
19
done
20
21
jq -e '
22
  .schema == "openagents.staging-regression-matrix.v1" and
23
  .revision == 1 and
24
  (.groups | type == "array" and length == 10) and
25
  ([.groups[].id] | length == (unique | length)) and
26
  ([.groups[].cases[]] | length == 69) and
27
  ([.groups[].cases[].id] | length == (unique | length)) and
28
  all(.groups[];
29
    (.id | type == "string" and test("^[a-z][a-z0-9_]+$")) and
30
    (.title | type == "string" and length > 0) and
31
    (.cases | type == "array" and length > 0) and
32
    all(.cases[];
33
      (.id | test("^[a-z]+-[0-9]{3}$")) and
34
      (.title | type == "string" and length > 0) and
35
      (.execution | IN("automated", "hybrid", "manual"))))
36
' "$matrix" >/dev/null || {
37
  echo "staging regression matrix contract failed" >&2
38
  exit 1
39
}
40
41
check_root=$(mktemp -d /tmp/openagents-regression-check.XXXXXX)
42
cleanup() {
43
  find "$check_root" -depth -delete 2>/dev/null || true
44
}
45
trap cleanup EXIT INT TERM
46
47
dry_report="$check_root/report.json"
48
safe_evidence="$check_root/safe.json"
49
unsafe_evidence="$check_root/unsafe.txt"
50
51
"$script_dir/new-report.sh" --dry-run "$dry_report"
52
"$script_dir/validate-report.sh" --draft "$dry_report" >/dev/null
53
54
if "$script_dir/validate-report.sh" --regression "$dry_report" >/dev/null 2>&1; then
55
  echo "draft staging report unexpectedly passed regression validation" >&2
56
  exit 1
57
fi
58
59
printf '%s\n' '{"schema":"openagents.staging-safe-test.v1","status":"ok"}' >"$safe_evidence"
60
chmod 600 "$safe_evidence"
61
"$script_dir/scan-evidence.sh" "$safe_evidence" >/dev/null
62
63
credential_label=Bearer
64
credential_body=000000000000000000000000
65
printf '%s %s\n' "$credential_label" "$credential_body" >"$unsafe_evidence"
66
chmod 600 "$unsafe_evidence"
67
68
if "$script_dir/scan-evidence.sh" "$unsafe_evidence" >/dev/null 2>&1; then
69
  echo "staging evidence scanner unexpectedly accepted a credential-shaped value" >&2
70
  exit 1
71
fi
72
73
"$script_dir/run-public-smoke.sh" check >/dev/null
74
75
head_sha=$(git -C "$repo_root" rev-parse --verify HEAD)
76
case "$head_sha" in
77
  "" | *[!0-9a-f]*) echo "repository HEAD is not a Git object ID" >&2; exit 1 ;;
78
  *) ;;
79
esac
80
81
if [ "${#head_sha}" -ne 40 ]; then
82
  echo "repository HEAD is not a Git object ID" >&2
83
  exit 1
84
fi
85
86
echo "Staging regression harness dry run passed (69 cases; no network requests sent)."
ops/staging/run-public-smoke.sh added +245

@@ -0,0 +1,245 @@

1
#!/bin/sh
2
set -eu
3
4
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
base_url=https://staging.openagents.com
6
mode=${1:-}
7
8
usage() {
9
  echo "usage: ops/staging/run-public-smoke.sh check" >&2
10
  echo "       ops/staging/run-public-smoke.sh --run CANDIDATE_DIRECTORY OUTPUT" >&2
11
  exit 64
12
}
13
14
for command_name in curl jq sha256sum; do
15
  if ! command -v "$command_name" >/dev/null 2>&1; then
16
    echo "$command_name is required to run the public staging smoke" >&2
17
    exit 1
18
  fi
19
done
20
21
if [ "$mode" = "check" ]; then
22
  [ "$#" -eq 1 ] || usage
23
  echo "Public staging smoke preflight passed (no network requests sent)."
24
  exit 0
25
fi
26
27
[ "$mode" = "--run" ] && [ "$#" -eq 3 ] || usage
28
candidate_dir=$2
29
output=$3
30
candidate_manifest="$candidate_dir/candidate-manifest.json"
31
candidate_checksum="$candidate_dir/candidate-manifest.sha256"
32
33
if [ -e "$output" ]; then
34
  echo "public smoke output already exists" >&2
35
  exit 1
36
fi
37
38
if [ ! -f "$candidate_manifest" ] || [ ! -f "$candidate_checksum" ]; then
39
  echo "candidate directory must contain the manifest and its checksum" >&2
40
  exit 1
41
fi
42
43
(cd "$candidate_dir" && sha256sum --check --strict candidate-manifest.sha256 >/dev/null)
44
45
jq -e --arg base_url "$base_url" '
46
  . as $manifest |
47
  .schema == "openagents.staging-candidate.v1" and
48
  (.git_sha | test("^[0-9a-f]{40}$")) and
49
  .branch == "main" and
50
  .target.environment == "staging" and
51
  (.target.project | test("stag"; "i")) and
52
  (.images.application.manifest_digest | test("^sha256:[0-9a-f]{64}$")) and
53
  (.images.application.reference | endswith("@" + $manifest.images.application.manifest_digest)) and
54
  $base_url == "https://staging.openagents.com"
55
' "$candidate_manifest" >/dev/null || {
56
  echo "candidate manifest does not satisfy the public staging smoke contract" >&2
57
  exit 1
58
}
59
60
git_sha=$(jq -r '.git_sha' "$candidate_manifest")
61
image_digest=$(jq -r '.images.application.manifest_digest' "$candidate_manifest")
62
started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
63
output_parent=$(dirname -- "$output")
64
65
if [ ! -d "$output_parent" ]; then
66
  echo "public smoke output directory does not exist" >&2
67
  exit 1
68
fi
69
70
umask 077
71
smoke_root=$(mktemp -d /tmp/openagents-public-smoke.XXXXXX)
72
checks="$smoke_root/checks.jsonl"
73
: >"$checks"
74
overall=passed
75
76
cleanup() {
77
  find "$smoke_root" -depth -delete 2>/dev/null || true
78
}
79
trap cleanup EXIT INT TERM
80
81
record_check() {
82
  case_id=$1
83
  path=$2
84
  expected_kind=$3
85
  body="$smoke_root/body"
86
  headers="$smoke_root/headers"
87
  curl_error="$smoke_root/curl-error"
88
  : >"$body"
89
  : >"$headers"
90
  : >"$curl_error"
91
92
  request_ok=true
93
  if status_code=$(curl -q -sS \
94
      --proto '=https' \
95
      --tlsv1.2 \
96
      --connect-timeout 10 \
97
      --max-time 30 \
98
      --output "$body" \
99
      --dump-header "$headers" \
100
      --write-out '%{http_code}' \
101
      "$base_url$path" 2>"$curl_error"); then
102
    :
103
  else
104
    request_ok=false
105
    status_code=000
106
  fi
107
108
  content_type=$(grep -i '^content-type:' "$headers" 2>/dev/null | tail -n 1 | cut -d ':' -f 2- | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
109
  body_size=$(wc -c <"$body" | tr -d ' ')
110
  body_sha256=$(sha256sum "$body" | cut -d ' ' -f 1)
111
  headers_sha256=$(sha256sum "$headers" | cut -d ' ' -f 1)
112
  semantic_ok=false
113
114
  if [ "$request_ok" = true ] && [ "$status_code" = 200 ]; then
115
    case "$expected_kind" in
116
      json)
117
        if printf '%s' "$content_type" | grep -Eiq '^application/json([[:space:]]*;|$)' &&
118
           jq -e --arg git_sha "$git_sha" '
119
             .status == "ok" and .revision == $git_sha
120
           ' "$body" >/dev/null 2>&1; then
121
          semantic_ok=true
122
        fi
123
        ;;
124
      html)
125
        if printf '%s' "$content_type" | grep -Eiq '^text/html([[:space:]]*;|$)' &&
126
           [ "$body_size" -gt 0 ]; then
127
          semantic_ok=true
128
        fi
129
        ;;
130
      binary)
131
        if [ "$body_size" -gt 0 ]; then
132
          semantic_ok=true
133
        fi
134
        ;;
135
    esac
136
  fi
137
138
  csp_present=false
139
  permissions_policy_present=false
140
  nonce_bound=false
141
  if [ "$path" = "/" ]; then
142
    if grep -Eiq '^content-security-policy:' "$headers" &&
143
       grep -Eiq "^content-security-policy:.*default-src 'self'.*script-src 'self' 'nonce-[A-Za-z0-9_-]+'" "$headers"; then
144
      csp_present=true
145
    fi
146
    if grep -Eiq '^permissions-policy:[[:space:]]*microphone=\(self\)' "$headers"; then
147
      permissions_policy_present=true
148
    fi
149
150
    header_nonce=$(sed -n "s/^content-security-policy:.*script-src 'self' 'nonce-\([A-Za-z0-9_-]*\)'.*/\1/ip" "$headers" | tail -n 1)
151
    if [ -n "$header_nonce" ] && grep -Fq "nonce=\"$header_nonce\"" "$body"; then
152
      nonce_bound=true
153
    fi
154
155
    if [ "$csp_present" != true ] || [ "$permissions_policy_present" != true ] || [ "$nonce_bound" != true ]; then
156
      semantic_ok=false
157
    fi
158
  fi
159
160
  if [ "$semantic_ok" != true ]; then
161
    overall=failed
162
  fi
163
164
  jq -cn \
165
    --arg case_id "$case_id" \
166
    --arg path "$path" \
167
    --arg expected_kind "$expected_kind" \
168
    --arg status_code "$status_code" \
169
    --arg content_type "$content_type" \
170
    --argjson body_size "$body_size" \
171
    --arg body_sha256 "$body_sha256" \
172
    --arg headers_sha256 "$headers_sha256" \
173
    --argjson request_ok "$request_ok" \
174
    --argjson semantic_ok "$semantic_ok" \
175
    --argjson csp_present "$csp_present" \
176
    --argjson permissions_policy_present "$permissions_policy_present" \
177
    --argjson nonce_bound "$nonce_bound" '
178
    {
179
      case_id: $case_id,
180
      path: $path,
181
      expected_kind: $expected_kind,
182
      status_code: ($status_code | tonumber),
183
      content_type: $content_type,
184
      body_bytes: $body_size,
185
      body_sha256: $body_sha256,
186
      response_headers_sha256: $headers_sha256,
187
      request_ok: $request_ok,
188
      semantic_ok: $semantic_ok,
189
      browser_policy: (if $path == "/" then {
190
        csp_present: $csp_present,
191
        permissions_policy_present: $permissions_policy_present,
192
        response_nonce_bound_to_theme_bootstrap: $nonce_bound
193
      } else null end)
194
    }
195
  ' >>"$checks"
196
}
197
198
record_check public-001 /healthz json
199
record_check public-001 /status json
200
record_check public-001 /api/status json
201
record_check public-001 /favicon.ico binary
202
record_check public-002 / html
203
record_check public-002 /leaderboard html
204
record_check public-002 /changelog html
205
record_check public-002 /docs html
206
record_check public-002 /components html
207
208
completed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
209
candidate_manifest_sha256=$(sha256sum "$candidate_manifest" | cut -d ' ' -f 1)
210
temporary_output="$smoke_root/public-smoke.json"
211
212
jq -s \
213
  --arg git_sha "$git_sha" \
214
  --arg image_digest "$image_digest" \
215
  --arg candidate_manifest_sha256 "$candidate_manifest_sha256" \
216
  --arg base_url "$base_url" \
217
  --arg started_at "$started_at" \
218
  --arg completed_at "$completed_at" \
219
  --arg outcome "$overall" '
220
  {
221
    schema: "openagents.staging-public-smoke.v1",
222
    target: {environment: "staging", base_url: $base_url},
223
    candidate: {
224
      git_sha: $git_sha,
225
      application_manifest_digest: $image_digest,
226
      candidate_manifest_sha256: $candidate_manifest_sha256
227
    },
228
    started_at: $started_at,
229
    completed_at: $completed_at,
230
    outcome: $outcome,
231
    content_retained: false,
232
    checks: .
233
  }
234
' "$checks" >"$temporary_output"
235
236
"$script_dir/scan-evidence.sh" "$temporary_output" >/dev/null
237
mv "$temporary_output" "$output"
238
chmod 600 "$output"
239
240
if [ "$overall" != passed ]; then
241
  echo "Public staging smoke failed; review the content-free receipt: $output" >&2
242
  exit 1
243
fi
244
245
echo "Public staging smoke passed: $output"
ops/staging/scan-evidence.sh added +72

@@ -0,0 +1,72 @@

1
#!/bin/sh
2
set -eu
3
4
if [ "$#" -eq 0 ]; then
5
  echo "usage: ops/staging/scan-evidence.sh FILE..." >&2
6
  exit 64
7
fi
8
9
for command_name in grep strings; do
10
  if ! command -v "$command_name" >/dev/null 2>&1; then
11
    echo "$command_name is required to scan staging evidence" >&2
12
    exit 1
13
  fi
14
done
15
16
scan_file() {
17
  file=$1
18
19
  if [ ! -f "$file" ]; then
20
    echo "evidence path is not a regular file: $file" >&2
21
    return 1
22
  fi
23
24
  size=$(wc -c <"$file" | tr -d ' ')
25
  if [ "$size" -gt 52428800 ]; then
26
    echo "evidence file exceeds the 50 MiB review bound: $file" >&2
27
    return 1
28
  fi
29
30
  scan_root=$(mktemp -d /tmp/openagents-evidence-scan.XXXXXX)
31
  scan_text="$scan_root/content"
32
33
  cleanup_scan() {
34
    find "$scan_root" -depth -delete 2>/dev/null || true
35
  }
36
  trap cleanup_scan EXIT INT TERM
37
38
  if grep -Iq . "$file" 2>/dev/null; then
39
    cp "$file" "$scan_text"
40
  else
41
    strings -a "$file" >"$scan_text"
42
  fi
43
44
  refuse_pattern() {
45
    label=$1
46
    pattern=$2
47
48
    if LC_ALL=C grep -Eiq -- "$pattern" "$scan_text"; then
49
      echo "evidence safety scan found $label in $file" >&2
50
      return 1
51
    fi
52
  }
53
54
  refuse_pattern "a private key" '-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----'
55
  refuse_pattern "an OpenAI-style credential" '(^|[^A-Za-z0-9])sk-[A-Za-z0-9_-]{16,}'
56
  refuse_pattern "a GitHub credential" '(^|[^A-Za-z0-9])(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})'
57
  refuse_pattern "a bearer credential" '(^|[^A-Za-z0-9])Bearer[[:space:]]+[A-Za-z0-9._~+/-]{16,}'
58
  refuse_pattern "a machine credential" '(^|[^A-Za-z0-9])smct_[A-Za-z0-9_-]{16,}'
59
  refuse_pattern "an authenticated database URL" '(ecto|postgres|postgresql)://[^/@[:space:]]+:[^/@[:space:]]+@'
60
  refuse_pattern "a browser session value" '(_openagents[^=[:space:]]*|_csrf_token|session(_id)?)[[:space:]]*=[[:space:]]*[^;[:space:]]{12,}'
61
  refuse_pattern "a credential-bearing URL query" '[?&](code|token|access_token|refresh_token|key|secret)=[^&[:space:]]{8,}'
62
  refuse_pattern "a private product payload key" '"(prompt|transcript|memory_value|memory_content|audio_base64|sdp|oauth_code|access_token|refresh_token|database_password|release_cookie)"[[:space:]]*:'
63
64
  cleanup_scan
65
  trap - EXIT INT TERM
66
}
67
68
for evidence_file in "$@"; do
69
  scan_file "$evidence_file"
70
done
71
72
echo "Staging evidence safety scan passed ($# files)."
ops/staging/validate-report.sh added +210

@@ -0,0 +1,210 @@

1
#!/bin/sh
2
set -eu
3
4
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
matrix="$script_dir/regression-matrix.json"
6
mode=${1:-}
7
report=${2:-}
8
9
case "$mode" in
10
  --draft | --recorded | --regression | --final) ;;
11
  *)
12
    echo "usage: ops/staging/validate-report.sh [--draft|--recorded|--regression|--final] REPORT" >&2
13
    exit 64
14
    ;;
15
esac
16
17
if [ "$#" -ne 2 ] || [ ! -f "$report" ]; then
18
  echo "REPORT must be a regular file" >&2
19
  exit 1
20
fi
21
22
for command_name in jq realpath sha256sum stat; do
23
  if ! command -v "$command_name" >/dev/null 2>&1; then
24
    echo "$command_name is required to validate a staging report" >&2
25
    exit 1
26
  fi
27
done
28
29
"$script_dir/scan-evidence.sh" "$report" >/dev/null
30
31
jq -e \
32
  --arg mode "$mode" \
33
  --slurpfile matrix "$matrix" '
34
  def digest: type == "string" and test("^[0-9a-f]{64}$");
35
  def manifest_digest: type == "string" and test("^sha256:[0-9a-f]{64}$");
36
  def timestamp: type == "string" and test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$");
37
  def nonempty: type == "string" and length > 0 and length <= 500;
38
  def evidence_ref:
39
    type == "object" and
40
    (.path | type == "string" and test("^evidence/[A-Za-z0-9._/-]+$") and (contains("..") | not)) and
41
    (.sha256 | digest) and
42
    (.kind | nonempty);
43
  def valid_attempt:
44
    type == "object" and
45
    (.ordinal | type == "number" and . >= 1 and floor == .) and
46
    (.outcome | IN("passed", "failed", "blocked")) and
47
    (.started_at | timestamp) and
48
    (.completed_at | timestamp) and
49
    (.automatic_retry | type == "boolean") and
50
    (.explanation == null or (.explanation | nonempty)) and
51
    (.evidence | type == "array" and all(.[]; evidence_ref));
52
  def all_receipt_objects_valid:
53
    [.. | objects | select(has("path") or has("sha256"))] |
54
    all(.[]; evidence_ref);
55
  def common_staging_evidence_complete:
56
    (.staging_evidence.migration.classification | IN("empty_current", "known_prior", "already_baselined")) and
57
    (.staging_evidence.migration.snapshot_receipt | evidence_ref) and
58
    (.staging_evidence.migration.rehearsal_receipt | evidence_ref) and
59
    (.staging_evidence.migration.migration_versions_receipt | evidence_ref) and
60
    (.staging_evidence.migration.rollback_compatibility_receipt | evidence_ref) and
61
    (.staging_evidence.configuration_readiness_receipt | evidence_ref) and
62
    (.staging_evidence.local_gate.default_test_count | type == "number" and . > 0 and floor == .) and
63
    (.staging_evidence.local_gate.cluster_test_count | type == "number" and . > 0 and floor == .) and
64
    (.staging_evidence.local_gate.javascript_test_count | type == "number" and . > 0 and floor == .) and
65
    (.staging_evidence.local_gate.coverage_summary_receipt | evidence_ref) and
66
    (.staging_evidence.deployment.web_revision | nonempty) and
67
    (.staging_evidence.deployment.web_image_digest == .candidate.application_manifest_digest) and
68
    (.staging_evidence.deployment.distributed_node_release_receipt | evidence_ref) and
69
    (.staging_evidence.forge.build_receipt | evidence_ref) and
70
    (.staging_evidence.forge.deployment_receipt | evidence_ref) and
71
    (.staging_evidence.forge.rollback_receipt | evidence_ref) and
72
    (.staging_evidence.forge.relup_receipt | evidence_ref) and
73
    (.staging_evidence.forge.rolling_replacement_receipt | evidence_ref) and
74
    (.staging_evidence.sanitized_artifacts | type == "array" and length > 0 and all(.[]; evidence_ref));
75
76
  [
77
    $matrix[0].groups[] as $group |
78
    $group.cases[] |
79
    {id: .id, group: $group.id, title: .title, execution: .execution}
80
  ] as $expected |
81
  . as $report |
82
83
  .schema == "openagents.staging-report.v1" and
84
  .matrix_revision == $matrix[0].revision and
85
  (.synthetic | type == "boolean") and
86
  (.run_id | type == "string" and test("^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")) and
87
  (.created_at | timestamp) and
88
  (.completed_at == null or (.completed_at | timestamp)) and
89
  .target.environment == "staging" and
90
  .target.base_url == "https://staging.openagents.com" and
91
  (.target.project | type == "string" and test("stag"; "i")) and
92
  (.target.region | nonempty) and
93
  (.candidate.git_sha | type == "string" and test("^[0-9a-f]{40}$")) and
94
  .candidate.branch == "main" and
95
  (.candidate.candidate_manifest_sha256 | digest) and
96
  (.candidate.application_manifest_digest | manifest_digest) and
97
  (.candidate.builder_manifest_digest | manifest_digest) and
98
  (.candidate.application_image | endswith("@" + $report.candidate.application_manifest_digest)) and
99
  (.candidate.builder_image | endswith("@" + $report.candidate.builder_manifest_digest)) and
100
  (.candidate.release_version | nonempty) and
101
  (.candidate.release_sha256 | digest) and
102
  (.candidate.sbom_sha256 | digest) and
103
  (.candidate.release_gate_sha256 | digest) and
104
  (.results | type == "array" and length == ($expected | length)) and
105
  ([.results[].id] | sort) == ([$expected[].id] | sort) and
106
  all(.results[]; . as $result |
107
    ($expected | map(select(.id == $result.id)) | .[0]) as $case |
108
    $result.group == $case.group and
109
    $result.title == $case.title and
110
    $result.execution == $case.execution and
111
    ($result.status | IN("pending", "passed", "failed", "blocked", "not_applicable")) and
112
    ($result.reason == null or ($result.reason | nonempty)) and
113
    ($result.attempts | type == "array" and all(.[]; valid_attempt)) and
114
    ($result.evidence | type == "array" and all(.[]; evidence_ref)) and
115
    ($result.attempts as $attempts |
116
      [$attempts[].ordinal] == [range(1; ($attempts | length) + 1)]) and
117
    (if $result.status == "pending" then
118
       ($result.attempts | length) == 0 and $result.reason == null
119
     elif $result.status == "not_applicable" then
120
       ($result.reason | nonempty)
121
     else
122
       ($result.attempts | length) > 0 and
123
       ($result.attempts[-1].outcome == $result.status) and
124
       ($result.evidence | length) > 0 and
125
       (if $result.status == "passed" then true else ($result.reason | nonempty) end)
126
     end)
127
  ) and
128
  all_receipt_objects_valid and
129
  (if $mode == "--draft" then
130
     .state == "draft"
131
   elif $mode == "--recorded" then
132
     .state == "recorded" and .synthetic == false and (.completed_at | timestamp) and
133
     all(.results[]; .status != "pending")
134
   elif $mode == "--regression" then
135
     .state == "regression_passed" and .synthetic == false and (.completed_at | timestamp) and
136
     all(.results[]; .status | IN("passed", "not_applicable")) and
137
     common_staging_evidence_complete
138
   else
139
     .state == "complete" and .synthetic == false and (.completed_at | timestamp) and
140
     all(.results[]; .status | IN("passed", "not_applicable")) and
141
     common_staging_evidence_complete and
142
     (.staging_evidence.failure_injection_timeline | type == "array" and length > 0 and all(.[]; evidence_ref)) and
143
     (.staging_evidence.soak_receipt | evidence_ref) and
144
     (.staging_evidence.known_issues | type == "array" and all(.[];
145
       (.id | nonempty) and (.owner | nonempty) and
146
       (.severity | IN("low", "medium", "high", "critical")) and
147
       (.disposition | nonempty)))
148
   end)
149
' "$report" >/dev/null || {
150
  echo "staging report does not satisfy $mode validation" >&2
151
  exit 1
152
}
153
154
report_dir=$(CDPATH= cd -- "$(dirname -- "$report")" && pwd)
155
refs=$(mktemp /tmp/openagents-staging-report-refs.XXXXXX)
156
cleanup() {
157
  unlink "$refs" 2>/dev/null || true
158
}
159
trap cleanup EXIT INT TERM
160
161
jq -r '
162
  .. | objects |
163
  select(has("path") and has("sha256")) |
164
  [.path, .sha256] | @tsv
165
' "$report" >"$refs"
166
167
tab=$(printf '\t')
168
while IFS="$tab" read -r relative_path expected_sha256; do
169
  [ -n "$relative_path" ] || continue
170
171
  case "$relative_path" in
172
    evidence/*) ;;
173
    *) echo "evidence reference must stay under evidence/: $relative_path" >&2; exit 1 ;;
174
  esac
175
176
  case "$relative_path" in
177
    *..* | /* | *[!A-Za-z0-9._/-]*)
178
      echo "unsafe evidence reference: $relative_path" >&2
179
      exit 1
180
      ;;
181
  esac
182
183
  evidence_path="$report_dir/$relative_path"
184
  if [ ! -f "$evidence_path" ] || [ -L "$evidence_path" ]; then
185
    echo "missing or linked evidence file: $relative_path" >&2
186
    exit 1
187
  fi
188
189
  resolved=$(realpath "$evidence_path")
190
  case "$resolved" in
191
    "$report_dir"/evidence/*) ;;
192
    *) echo "evidence path escapes the report directory: $relative_path" >&2; exit 1 ;;
193
  esac
194
195
  actual_sha256=$(sha256sum "$evidence_path" | cut -d ' ' -f 1)
196
  if [ "$actual_sha256" != "$expected_sha256" ]; then
197
    echo "evidence checksum mismatch: $relative_path" >&2
198
    exit 1
199
  fi
200
201
  mode_bits=$(stat -c '%a' "$evidence_path")
202
  case "$mode_bits" in
203
    400 | 600) ;;
204
    *) echo "evidence file must not grant group or world access: $relative_path" >&2; exit 1 ;;
205
  esac
206
207
  "$script_dir/scan-evidence.sh" "$evidence_path" >/dev/null
208
done <"$refs"
209
210
echo "Staging report $mode validation passed."
test/openagents/staging_regression_contract_test.exs added +216

@@ -0,0 +1,216 @@

1
defmodule OpenAgents.StagingRegressionContractTest do
2
  use ExUnit.Case, async: false
3
4
  @scripts_root Path.expand("ops/staging")
5
6
  setup do
7
    test_root =
8
      Path.join(
9
        System.tmp_dir!(),
10
        "openagents-staging-regression-#{System.unique_integer([:positive])}"
11
      )
12
13
    File.mkdir_p!(test_root)
14
    on_exit(fn -> File.rm_rf!(test_root) end)
15
16
    %{test_root: test_root}
17
  end
18
19
  test "the matrix has one unique result contract for every Gate 14 case" do
20
    matrix =
21
      @scripts_root |> Path.join("regression-matrix.json") |> File.read!() |> Jason.decode!()
22
23
    cases = Enum.flat_map(matrix["groups"], & &1["cases"])
24
25
    assert matrix["schema"] == "openagents.staging-regression-matrix.v1"
26
    assert matrix["revision"] == 1
27
    assert length(matrix["groups"]) == 10
28
    assert length(cases) == 69
29
    assert Enum.uniq_by(cases, & &1["id"]) == cases
30
    assert Enum.all?(cases, &(&1["execution"] in ["automated", "hybrid", "manual"]))
31
  end
32
33
  test "the harness dry run is local, fail-closed, and network-free" do
34
    {output, 0} = command("regression.sh", ["check"])
35
36
    assert output =~ "69 cases"
37
    assert output =~ "no network requests sent"
38
  end
39
40
  test "a report preserves failed attempts and binds copied evidence", %{test_root: test_root} do
41
    report = Path.join(test_root, "report.json")
42
    evidence = Path.join(test_root, "evidence.json")
43
    reason = Path.join(test_root, "reason.txt")
44
45
    assert {_, 0} = command("new-report.sh", ["--dry-run", report])
46
    assert {_, 0} = command("validate-report.sh", ["--draft", report])
47
48
    File.write!(evidence, Jason.encode!(%{"schema" => "openagents.test-receipt.v1"}))
49
    File.chmod!(evidence, 0o600)
50
    File.write!(reason, "The initial bounded probe observed an unavailable dependency.")
51
    File.chmod!(reason, 0o600)
52
53
    assert {_, 0} =
54
             command("record-result.sh", [
55
               report,
56
               "public-001",
57
               "failed",
58
               "public-smoke",
59
               evidence,
60
               reason
61
             ])
62
63
    assert {_, 0} =
64
             command("record-result.sh", [
65
               report,
66
               "public-001",
67
               "passed",
68
               "public-smoke",
69
               evidence
70
             ])
71
72
    assert {_, 0} = command("validate-report.sh", ["--draft", report])
73
    assert {output, 1} = command("finalize-report.sh", ["--regression", report])
74
    assert output =~ "report remains unchanged"
75
76
    decoded = report |> File.read!() |> Jason.decode!()
77
    result = hd(decoded["results"])
78
79
    assert decoded["state"] == "draft"
80
    assert result["status"] == "passed"
81
    assert Enum.map(result["attempts"], & &1["outcome"]) == ["failed", "passed"]
82
    assert Enum.map(result["attempts"], & &1["ordinal"]) == [1, 2]
83
    assert length(result["evidence"]) == 2
84
    assert Enum.all?(result["evidence"], &String.match?(&1["sha256"], ~r/^[0-9a-f]{64}$/))
85
86
    for reference <- result["evidence"] do
87
      assert File.exists?(Path.join(test_root, reference["path"]))
88
    end
89
  end
90
91
  test "the scanner refuses a credential shape without echoing its value", %{test_root: test_root} do
92
    unsafe = Path.join(test_root, "unsafe.txt")
93
    credential = "Bearer " <> String.duplicate("0", 24)
94
    File.write!(unsafe, credential)
95
    File.chmod!(unsafe, 0o600)
96
97
    {output, 1} = command("scan-evidence.sh", [unsafe])
98
99
    assert output =~ "bearer credential"
100
    refute output =~ credential
101
  end
102
103
  test "regression and final states require their complete evidence sets", %{test_root: test_root} do
104
    report = Path.join(test_root, "report.json")
105
    evidence_dir = Path.join(test_root, "evidence")
106
    evidence = Path.join(evidence_dir, "bounded-receipt.json")
107
108
    assert {_, 0} = command("new-report.sh", ["--dry-run", report])
109
    File.mkdir_p!(evidence_dir)
110
    File.write!(evidence, Jason.encode!(%{"schema" => "openagents.bounded-receipt.v1"}))
111
    File.chmod!(evidence, 0o600)
112
113
    evidence_sha256 =
114
      evidence |> File.read!() |> then(&:crypto.hash(:sha256, &1)) |> Base.encode16(case: :lower)
115
116
    reference = %{
117
      "path" => "evidence/bounded-receipt.json",
118
      "sha256" => evidence_sha256,
119
      "kind" => "bounded-contract-proof"
120
    }
121
122
    decoded = report |> File.read!() |> Jason.decode!()
123
    image_digest = decoded["candidate"]["application_manifest_digest"]
124
125
    complete_common_evidence = %{
126
      "migration" => %{
127
        "classification" => "empty_current",
128
        "snapshot_receipt" => reference,
129
        "rehearsal_receipt" => reference,
130
        "migration_versions_receipt" => reference,
131
        "rollback_compatibility_receipt" => reference
132
      },
133
      "configuration_readiness_receipt" => reference,
134
      "local_gate" => %{
135
        "default_test_count" => 1,
136
        "cluster_test_count" => 1,
137
        "javascript_test_count" => 1,
138
        "coverage_summary_receipt" => reference
139
      },
140
      "deployment" => %{
141
        "web_revision" => "openagents-staging-contract-proof",
142
        "web_image_digest" => image_digest,
143
        "distributed_node_release_receipt" => reference
144
      },
145
      "forge" => %{
146
        "build_receipt" => reference,
147
        "deployment_receipt" => reference,
148
        "rollback_receipt" => reference,
149
        "relup_receipt" => reference,
150
        "rolling_replacement_receipt" => reference
151
      },
152
      "sanitized_artifacts" => [reference],
153
      "failure_injection_timeline" => [],
154
      "soak_receipt" => nil,
155
      "known_issues" => []
156
    }
157
158
    complete_results =
159
      Enum.map(decoded["results"], fn result ->
160
        result
161
        |> Map.put("status", "not_applicable")
162
        |> Map.put("reason", "Not applicable in the synthetic contract proof.")
163
      end)
164
165
    decoded =
166
      decoded
167
      |> Map.put("synthetic", false)
168
      |> Map.put("results", complete_results)
169
      |> Map.put("staging_evidence", complete_common_evidence)
170
171
    File.write!(report, Jason.encode!(decoded))
172
    File.chmod!(report, 0o600)
173
174
    assert {_, 0} = command("finalize-report.sh", ["--regression", report])
175
    assert Jason.decode!(File.read!(report))["state"] == "regression_passed"
176
177
    decoded = Jason.decode!(File.read!(report))
178
179
    staging_evidence =
180
      decoded["staging_evidence"]
181
      |> Map.put("failure_injection_timeline", [reference])
182
      |> Map.put("soak_receipt", reference)
183
184
    decoded = Map.put(decoded, "staging_evidence", staging_evidence)
185
    File.write!(report, Jason.encode!(decoded))
186
    File.chmod!(report, 0o600)
187
188
    assert {_, 0} = command("finalize-report.sh", ["--final", report])
189
    assert Jason.decode!(File.read!(report))["state"] == "complete"
190
191
    [checksum, "report.json"] =
192
      test_root |> Path.join("report.sha256") |> File.read!() |> String.split()
193
194
    actual_checksum =
195
      report |> File.read!() |> then(&:crypto.hash(:sha256, &1)) |> Base.encode16(case: :lower)
196
197
    assert checksum == actual_checksum
198
  end
199
200
  test "the public smoke retains hashes and metrics but no response content" do
201
    smoke = File.read!(Path.join(@scripts_root, "run-public-smoke.sh"))
202
203
    assert smoke =~ "https://staging.openagents.com"
204
    assert smoke =~ "content_retained: false"
205
    assert smoke =~ "body_sha256"
206
    assert smoke =~ "response_headers_sha256"
207
    assert smoke =~ "response_nonce_bound_to_theme_bootstrap"
208
    refute smoke =~ "stage.openagents.com"
209
    refute smoke =~ "--cookie"
210
    refute smoke =~ "--netrc"
211
  end
212
213
  defp command(script, arguments) do
214
    System.cmd(Path.join(@scripts_root, script), arguments, stderr_to_stdout: true)
215
  end
216
end

This page updates live while a promote is in flight · changelog