@openwop/openwop-conformance 2.46.19 → 2.46.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [2.46.21] — 2026-10-24 — RFC 0249 Active: v2 proposal operations
2
+
3
+ - **New scenario `v2-proposal-lifecycle`.** One client-submitted `prompt-template` proposal is driven through submit, revise and apply. The legs are `0249.shape` (server-free), `submit-kind-refused`, `draft-inert`, `apply-stale-refused`, `apply-unscoped-refused`, `apply-installs`, `readback-byte-match`, `terminal-refused` and `cross-tenant-not-found`. Judges are in `lib/proposal-lifecycle-witness.ts`, whose self-tests fail each wrong host answer, including a hash passed off as an install.
4
+ - **Fix before release (found by openwop-app's pre-release run):** the judges read error codes from a nested `{error: {code}}`. The v2 envelope is flat (`{error: "<code>", message, details?}`), so three legs failed on a correct host. They now use `readErrorCode` and read `details` at the root; self-tests cover the flat form.
5
+ - **`0249.unadvertised-not-found`** in `v2-unadvertised-operation-not-found`: `GET /proposals` on a host without `agents.proposals` answers `404 not_found`.
6
+ - **Gating.** The host legs need `agents.proposals` with `clientDrafts: true` and `readback` ∋ `prompt-template`, plus `OPENWOP_TEST_LOW_SCOPE_KEY` (read and write, no `proposals:apply`) and `OPENWOP_TEST_TENANT_B_API_KEY`. An unset key records `blocked`.
7
+ - **Invariants.** `proposal-inert-until-applied` and `proposal-no-resynthesis` move `seam-gated` → `witnessable-gated`.
8
+ - **New top-level name `proposals`** is pinned in `check-manifest-top-level-segments`. No host in INTEROP-MATRIX serves `/proposals` unversioned: openwop-app's proposals are under `/host/openwop-app/`, and MyndHyve's v1 surface was `/v1/host/sample/proposals`.
9
+ - **Host impact:** none today. No host advertises `clientDrafts`. MyndHyve advertises `agents.proposals` without it, so its legs are `inapplicable`.
10
+
11
+ ## [2.46.20] — 2026-10-24 — RFC 0081 eval envelope legs; RFC 0249 Draft
12
+
13
+ - **Two new legs in `v2-eval-trajectory-trials` (RFC 0081 §C)**, on the existing `conformance-eval-trajectory` run, so no extra run: `0081.eval-envelope` (one `eval.started` and one `eval.completed` bracket every `eval.scored`, `eval.completed` precedes `run.completed`, and its counts equal the summary's) and `0081.summary-served` (the served `EvalSummary` validates and its counts match its rows). Judges in `lib/eval-envelope-witness.ts`, whose self-tests fail each wrong host answer. Before this, no v2 leg read `eval.completed`.
14
+ - **Host impact:** hosts that advertise `agents.evalSuite.trajectory` and list the fixture (openwop-app, MyndHyve) run both legs. A host that omits `eval.completed` fails `0081.eval-envelope`.
15
+ - RFC 0249 (v2 proposal operations and an install read-back for RFC 0096) is filed as `Draft`. Its gap register lands in the packed `spec/v1/gaps.json`, which opened this cycle.
16
+
1
17
  ## [2.46.19] — 2026-10-22 — RFC 0248: ctx.callAI attempt accounting and retry policy
2
18
 
3
19
  - **New scenario `v2-callai-attempt-accounting`** with eight legs: `0248.attempts-count`, `output-tokens-sum`, `usage-incomplete`, `structured-output`, `attempts-replayed`, `max-attempts-cap`, `no-budget-growth` and `retry-malformed-refused`. They are gated on `aiProviders.callAccounting.attempts` / `.retryPolicy` and the fixture.
package/README.md CHANGED
@@ -137,7 +137,7 @@ Each scenario file records its disposition and assertion count in `<report-dir>/
137
137
 
138
138
  ## What's Covered
139
139
 
140
- The current suite has 659 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 219 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
140
+ The current suite has 660 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 220 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
141
141
 
142
142
  The v2 scenarios (`src/scenarios/v2-*.test.ts`) cover, by area:
143
143
 
@@ -166,7 +166,7 @@ Three kinds of scenario share the tree:
166
166
  - **Host** checks drive a live host over HTTP.
167
167
  - **Gated** checks run only when the host advertises the family, profile or fixture they need, and otherwise record `inapplicable` or `blocked` with a reason. `--require-behavior` turns an advertised-but-unwitnessable behaviour into a failure.
168
168
 
169
- Current source tree: 659 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
169
+ Current source tree: 660 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
170
170
 
171
171
  ## How the 2.x suite is built
172
172
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.46.19",
4
- "stampSha256": "8b0bfeeda6c1a1e8bbac41bc73b01f70e71ad88ca5dc98aee4a373b6611db396"
3
+ "version": "2.46.21",
4
+ "stampSha256": "bf5d66dc96dadf165262981562db0cb1a45b8fca7d4eb0910b6c60e6e0c5b7ad"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.46.19",
3
+ "version": "2.46.21",
4
4
  "description": "Black-box conformance suite for OpenWOP hosts (v2 current; v1 scenarios retained for the frozen v1 tree).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,6 +56,6 @@
56
56
  "@openwop/spec-artifacts": "file:../spec-artifacts"
57
57
  },
58
58
  "peerDependencies": {
59
- "@openwop/spec-artifacts": "2.46.19"
59
+ "@openwop/spec-artifacts": "2.46.21"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
- "files": 747,
6
- "tests": 2822,
7
- "withStableId": 2822,
5
+ "files": 748,
6
+ "tests": 2834,
7
+ "withStableId": 2834,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2695
9
+ "explicitIds": 2707
10
10
  },
11
11
  "records": [
12
12
  {
@@ -31092,7 +31092,7 @@
31092
31092
  {
31093
31093
  "id": "openwop.it.v2-eval-trajectory-trials.the-rfc-0244-suite-summary-and-event-fields-validate-and-the-negatives-are-refus",
31094
31094
  "file": "v2-eval-trajectory-trials.test.ts",
31095
- "line": 150,
31095
+ "line": 162,
31096
31096
  "title": "the RFC 0244 suite, summary and event fields validate and the negatives are refused",
31097
31097
  "explicitId": "openwop.requirement.0244.wire-shape",
31098
31098
  "citations": [
@@ -31131,7 +31131,7 @@
31131
31131
  {
31132
31132
  "id": "openwop.it.v2-eval-trajectory-trials.the-rfc-0244-additions-are-content-free-eval-summary-no-content-leak",
31133
31133
  "file": "v2-eval-trajectory-trials.test.ts",
31134
- "line": 173,
31134
+ "line": 185,
31135
31135
  "title": "the RFC 0244 additions are content-free (eval-summary-no-content-leak)",
31136
31136
  "explicitId": "openwop.requirement.0244.content-free",
31137
31137
  "citations": [
@@ -31148,7 +31148,7 @@
31148
31148
  {
31149
31149
  "id": "openwop.it.v2-eval-trajectory-trials.the-eval-suite-fixtures-validate-against-the-v2-suite-schema",
31150
31150
  "file": "v2-eval-trajectory-trials.test.ts",
31151
- "line": 180,
31151
+ "line": 192,
31152
31152
  "title": "the eval-suite fixtures validate against the v2 suite schema",
31153
31153
  "explicitId": "openwop.requirement.0244.fixtures-valid",
31154
31154
  "citations": [
@@ -31162,7 +31162,7 @@
31162
31162
  {
31163
31163
  "id": "openwop.it.v2-eval-trajectory-trials.the-rfc-0246-gating-fields-validate-and-stay-in-0-1",
31164
31164
  "file": "v2-eval-trajectory-trials.test.ts",
31165
- "line": 187,
31165
+ "line": 199,
31166
31166
  "title": "the RFC 0246 gating fields validate and stay in 0–1",
31167
31167
  "explicitId": "openwop.requirement.0246.gated-fields-shape",
31168
31168
  "citations": [
@@ -31184,7 +31184,7 @@
31184
31184
  {
31185
31185
  "id": "openwop.it.v2-eval-trajectory-trials.a-fixture-entry-may-be-marked-iserror-and-only-as-a-boolean-rfc-0245-a",
31186
31186
  "file": "v2-eval-trajectory-trials.test.ts",
31187
- "line": 195,
31187
+ "line": 207,
31188
31188
  "title": "a fixture entry may be marked isError, and only as a boolean (RFC 0245 §A)",
31189
31189
  "explicitId": "openwop.requirement.0245.fixture-error-shape",
31190
31190
  "citations": [
@@ -31206,7 +31206,7 @@
31206
31206
  {
31207
31207
  "id": "openwop.it.v2-eval-trajectory-trials.a-trajectory-suite-is-graded-per-the-reference-grader",
31208
31208
  "file": "v2-eval-trajectory-trials.test.ts",
31209
- "line": 205,
31209
+ "line": 217,
31210
31210
  "title": "a trajectory suite is graded per the reference grader",
31211
31211
  "explicitId": "openwop.requirement.0244.trajectory-graded",
31212
31212
  "citations": [
@@ -31237,10 +31237,38 @@
31237
31237
  }
31238
31238
  ]
31239
31239
  },
31240
+ {
31241
+ "id": "openwop.it.v2-eval-trajectory-trials.an-eval-run-brackets-its-scores-with-one-eval-started-and-one-eval-completed-rfc",
31242
+ "file": "v2-eval-trajectory-trials.test.ts",
31243
+ "line": 251,
31244
+ "title": "an eval run brackets its scores with one eval.started and one eval.completed (RFC 0081 §C)",
31245
+ "explicitId": "openwop.requirement.0081.eval-envelope",
31246
+ "citations": [
31247
+ {
31248
+ "section": "run-event-payloads evalStarted / evalCompleted, eval-summary.schema.json (RFC 0081 §C)",
31249
+ "requirement": null,
31250
+ "interpolated": true
31251
+ }
31252
+ ]
31253
+ },
31254
+ {
31255
+ "id": "openwop.it.v2-eval-trajectory-trials.the-served-evalsummary-validates-and-its-counts-match-its-rows-rfc-0081-c",
31256
+ "file": "v2-eval-trajectory-trials.test.ts",
31257
+ "line": 259,
31258
+ "title": "the served EvalSummary validates and its counts match its rows (RFC 0081 §C)",
31259
+ "explicitId": "openwop.requirement.0081.summary-served",
31260
+ "citations": [
31261
+ {
31262
+ "section": "run-event-payloads evalStarted / evalCompleted, eval-summary.schema.json (RFC 0081 §C)",
31263
+ "requirement": null,
31264
+ "interpolated": true
31265
+ }
31266
+ ]
31267
+ },
31240
31268
  {
31241
31269
  "id": "openwop.it.v2-eval-trajectory-trials.repeated-trials-emit-one-eval-scored-per-task-per-trial",
31242
31270
  "file": "v2-eval-trajectory-trials.test.ts",
31243
- "line": 241,
31271
+ "line": 280,
31244
31272
  "title": "repeated trials emit one eval.scored per task per trial",
31245
31273
  "explicitId": "openwop.requirement.0244.trials-per-task",
31246
31274
  "citations": [
@@ -31258,7 +31286,7 @@
31258
31286
  {
31259
31287
  "id": "openwop.it.v2-eval-trajectory-trials.the-per-task-trials-block-and-trialstats-follow-the-b-formulas",
31260
31288
  "file": "v2-eval-trajectory-trials.test.ts",
31261
- "line": 252,
31289
+ "line": 291,
31262
31290
  "title": "the per-task trials block and trialStats follow the §B formulas",
31263
31291
  "explicitId": "openwop.requirement.0244.trial-stats-formula",
31264
31292
  "citations": [
@@ -31297,7 +31325,7 @@
31297
31325
  {
31298
31326
  "id": "openwop.it.v2-eval-trajectory-trials.a-fixture-miss-under-onmiss-fail-fails-the-trial-and-is-counted",
31299
31327
  "file": "v2-eval-trajectory-trials.test.ts",
31300
- "line": 275,
31328
+ "line": 314,
31301
31329
  "title": "a fixture miss under onMiss \"fail\" fails the trial and is counted",
31302
31330
  "explicitId": "openwop.requirement.0244.fixture-miss-fails",
31303
31331
  "citations": [
@@ -31334,7 +31362,7 @@
31334
31362
  {
31335
31363
  "id": "openwop.it.v2-eval-trajectory-trials.a-suite-using-an-unadvertised-rfc-0244-feature-is-refused-at-createrun",
31336
31364
  "file": "v2-eval-trajectory-trials.test.ts",
31337
- "line": 295,
31365
+ "line": 334,
31338
31366
  "title": "a suite using an unadvertised RFC 0244 feature is refused at createRun",
31339
31367
  "explicitId": "openwop.requirement.0244.unadvertised-feature-refused",
31340
31368
  "citations": [
@@ -31352,7 +31380,7 @@
31352
31380
  {
31353
31381
  "id": "openwop.it.v2-eval-trajectory-trials.under-passrule-mean-a-task-whose-trials-failed-their-trajectory-does-not-pass-rf",
31354
31382
  "file": "v2-eval-trajectory-trials.test.ts",
31355
- "line": 311,
31383
+ "line": 350,
31356
31384
  "title": "under passRule \"mean\" a task whose trials failed their trajectory does not pass (RFC 0246)",
31357
31385
  "explicitId": "openwop.requirement.0246.mean-gated",
31358
31386
  "citations": [
@@ -31366,7 +31394,7 @@
31366
31394
  {
31367
31395
  "id": "openwop.it.v2-eval-trajectory-trials.a-suite-whose-tasks-failed-their-trajectory-does-not-pass-on-the-output-scores-r",
31368
31396
  "file": "v2-eval-trajectory-trials.test.ts",
31369
- "line": 324,
31397
+ "line": 363,
31370
31398
  "title": "a suite whose tasks failed their trajectory does not pass on the output scores (RFC 0246)",
31371
31399
  "explicitId": "openwop.requirement.0246.suite-gated",
31372
31400
  "citations": [
@@ -31380,7 +31408,7 @@
31380
31408
  {
31381
31409
  "id": "openwop.it.v2-eval-trajectory-trials.a-fixture-entry-marked-iserror-is-replayed-as-a-tool-error-not-a-success-rfc-024",
31382
31410
  "file": "v2-eval-trajectory-trials.test.ts",
31383
- "line": 335,
31411
+ "line": 374,
31384
31412
  "title": "a fixture entry marked isError is replayed as a tool error, not a success (RFC 0245)",
31385
31413
  "explicitId": "openwop.requirement.0245.fixture-error-replayed",
31386
31414
  "citations": [
@@ -31394,7 +31422,7 @@
31394
31422
  {
31395
31423
  "id": "openwop.it.v2-eval-trajectory-trials.a-suite-using-iserror-is-refused-at-createrun-without-fixtureerrors-rfc-0245-c",
31396
31424
  "file": "v2-eval-trajectory-trials.test.ts",
31397
- "line": 349,
31425
+ "line": 388,
31398
31426
  "title": "a suite using isError is refused at createRun without fixtureErrors (RFC 0245 §C)",
31399
31427
  "explicitId": "openwop.requirement.0245.unadvertised-refused",
31400
31428
  "citations": [
@@ -35818,6 +35846,111 @@
35818
35846
  }
35819
35847
  ]
35820
35848
  },
35849
+ {
35850
+ "id": "openwop.it.v2-proposal-lifecycle.the-submit-revise-and-apply-bodies-and-installedartifact-validate-and-the-negati",
35851
+ "file": "v2-proposal-lifecycle.test.ts",
35852
+ "line": 186,
35853
+ "title": "the submit, revise and apply bodies and installedArtifact validate, and the negatives are refused",
35854
+ "explicitId": "openwop.requirement.0249.shape",
35855
+ "citations": [
35856
+ {
35857
+ "section": "proposal-submit / proposal-revise / proposal-apply / proposal.schema.json (RFC 0249)",
35858
+ "requirement": "a minimal submit MUST validate"
35859
+ },
35860
+ {
35861
+ "section": "proposal-submit / proposal-revise / proposal-apply / proposal.schema.json (RFC 0249)",
35862
+ "requirement": "an unknown kind MUST be refused"
35863
+ },
35864
+ {
35865
+ "section": "proposal-submit / proposal-revise / proposal-apply / proposal.schema.json (RFC 0249)",
35866
+ "requirement": "a submit carrying state MUST be refused (closed body)"
35867
+ },
35868
+ {
35869
+ "section": "proposal-submit / proposal-revise / proposal-apply / proposal.schema.json (RFC 0249)",
35870
+ "requirement": "an empty revise MUST be refused"
35871
+ },
35872
+ {
35873
+ "section": "proposal-submit / proposal-revise / proposal-apply / proposal.schema.json (RFC 0249)",
35874
+ "requirement": "a sha256 digest MUST validate"
35875
+ },
35876
+ {
35877
+ "section": "proposal-submit / proposal-revise / proposal-apply / proposal.schema.json (RFC 0249)",
35878
+ "requirement": "a digest without the sha256: prefix and 64 hex MUST be refused"
35879
+ },
35880
+ {
35881
+ "section": "proposal-submit / proposal-revise / proposal-apply / proposal.schema.json (RFC 0249)",
35882
+ "requirement": "a minimal installedArtifact MUST validate"
35883
+ },
35884
+ {
35885
+ "section": "proposal-submit / proposal-revise / proposal-apply / proposal.schema.json (RFC 0249)",
35886
+ "requirement": "installedArtifact is closed"
35887
+ }
35888
+ ]
35889
+ },
35890
+ {
35891
+ "id": "openwop.it.v2-proposal-lifecycle.a-kind-outside-artifactkinds-is-refused-at-submit",
35892
+ "file": "v2-proposal-lifecycle.test.ts",
35893
+ "line": 204,
35894
+ "title": "a kind outside artifactKinds is refused at submit",
35895
+ "explicitId": "openwop.requirement.0249.submit-kind-refused",
35896
+ "citations": []
35897
+ },
35898
+ {
35899
+ "id": "openwop.it.v2-proposal-lifecycle.a-draft-prompt-template-is-not-served-listed-or-renderable",
35900
+ "file": "v2-proposal-lifecycle.test.ts",
35901
+ "line": 213,
35902
+ "title": "a draft prompt-template is not served, listed or renderable",
35903
+ "explicitId": "openwop.requirement.0249.draft-inert",
35904
+ "citations": []
35905
+ },
35906
+ {
35907
+ "id": "openwop.it.v2-proposal-lifecycle.a-revision-re-digests-and-an-apply-bound-to-the-old-bytes-is-stale",
35908
+ "file": "v2-proposal-lifecycle.test.ts",
35909
+ "line": 221,
35910
+ "title": "a revision re-digests, and an apply bound to the old bytes is stale",
35911
+ "explicitId": "openwop.requirement.0249.apply-stale-refused",
35912
+ "citations": []
35913
+ },
35914
+ {
35915
+ "id": "openwop.it.v2-proposal-lifecycle.an-apply-without-proposals-apply-is-refused-and-installs-nothing",
35916
+ "file": "v2-proposal-lifecycle.test.ts",
35917
+ "line": 229,
35918
+ "title": "an apply without proposals:apply is refused and installs nothing",
35919
+ "explicitId": "openwop.requirement.0249.apply-unscoped-refused",
35920
+ "citations": []
35921
+ },
35922
+ {
35923
+ "id": "openwop.it.v2-proposal-lifecycle.an-authorized-current-apply-names-the-read-that-serves-the-installed-template",
35924
+ "file": "v2-proposal-lifecycle.test.ts",
35925
+ "line": 239,
35926
+ "title": "an authorized, current apply names the read that serves the installed template",
35927
+ "explicitId": "openwop.requirement.0249.apply-installs",
35928
+ "citations": []
35929
+ },
35930
+ {
35931
+ "id": "openwop.it.v2-proposal-lifecycle.the-installed-template-is-the-reviewed-revision-byte-for-byte",
35932
+ "file": "v2-proposal-lifecycle.test.ts",
35933
+ "line": 248,
35934
+ "title": "the installed template is the reviewed revision, byte for byte",
35935
+ "explicitId": "openwop.requirement.0249.readback-byte-match",
35936
+ "citations": []
35937
+ },
35938
+ {
35939
+ "id": "openwop.it.v2-proposal-lifecycle.an-applied-proposal-is-terminal",
35940
+ "file": "v2-proposal-lifecycle.test.ts",
35941
+ "line": 257,
35942
+ "title": "an applied proposal is terminal",
35943
+ "explicitId": "openwop.requirement.0249.terminal-refused",
35944
+ "citations": []
35945
+ },
35946
+ {
35947
+ "id": "openwop.it.v2-proposal-lifecycle.another-tenant-sees-the-proposal-and-the-installed-template-as-unknown-ids",
35948
+ "file": "v2-proposal-lifecycle.test.ts",
35949
+ "line": 266,
35950
+ "title": "another tenant sees the proposal and the installed template as unknown ids",
35951
+ "explicitId": "openwop.requirement.0249.cross-tenant-not-found",
35952
+ "citations": []
35953
+ },
35821
35954
  {
35822
35955
  "id": "openwop.it.v2-protected-resource-metadata.serves-rfc-9728-metadata-unauthenticated-at-the-uri-derived-from-its-resource-id",
35823
35956
  "file": "v2-protected-resource-metadata.test.ts",
@@ -38289,7 +38422,7 @@
38289
38422
  {
38290
38423
  "id": "openwop.it.v2-unadvertised-operation-not-found.get-prompts-answers-404-not-found-when-prompts-endpointssupported-is-not-adverti",
38291
38424
  "file": "v2-unadvertised-operation-not-found.test.ts",
38292
- "line": 43,
38425
+ "line": 45,
38293
38426
  "title": "GET /prompts answers 404 not_found when prompts.endpointsSupported is not advertised",
38294
38427
  "explicitId": "openwop.requirement.0228.unadvertised-prompts-not-found",
38295
38428
  "citations": [
@@ -38307,7 +38440,7 @@
38307
38440
  {
38308
38441
  "id": "openwop.it.v2-unadvertised-operation-not-found.get-content-settings-answers-404-not-found-when-content-is-not-advertised",
38309
38442
  "file": "v2-unadvertised-operation-not-found.test.ts",
38310
- "line": 54,
38443
+ "line": 56,
38311
38444
  "title": "GET /content/settings answers 404 not_found when content is not advertised",
38312
38445
  "explicitId": "openwop.requirement.0228.unadvertised-content-not-found",
38313
38446
  "citations": [
@@ -38322,6 +38455,24 @@
38322
38455
  }
38323
38456
  ]
38324
38457
  },
38458
+ {
38459
+ "id": "openwop.it.v2-unadvertised-operation-not-found.get-proposals-answers-404-not-found-when-agents-proposals-is-not-advertised-rfc",
38460
+ "file": "v2-unadvertised-operation-not-found.test.ts",
38461
+ "line": 66,
38462
+ "title": "GET /proposals answers 404 not_found when agents.proposals is not advertised (RFC 0249)",
38463
+ "explicitId": "openwop.requirement.0249.unadvertised-not-found",
38464
+ "citations": [
38465
+ {
38466
+ "section": "spec/v2/core/errors.md §Unadvertised operations",
38467
+ "requirement": null,
38468
+ "interpolated": true
38469
+ },
38470
+ {
38471
+ "section": "spec/v2/core/errors.md §Unadvertised operations",
38472
+ "requirement": "the 404 for an unadvertised gated operation MUST carry the registered code not_found"
38473
+ }
38474
+ ]
38475
+ },
38325
38476
  {
38326
38477
  "id": "openwop.it.v2-unmapped-type-refused.poll-over-a-log-with-an-unmapped-unprefixed-type-fails-with-500-event-type-unmap",
38327
38478
  "file": "v2-unmapped-type-refused.test.ts",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
3
3
  "counts": {
4
- "files": 659,
4
+ "files": 660,
5
5
  "v1": 455,
6
- "v2": 219
6
+ "v2": 220
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1686,6 +1686,9 @@
1686
1686
  "v2-prompt-template-shape.test.ts": [
1687
1687
  2
1688
1688
  ],
1689
+ "v2-proposal-lifecycle.test.ts": [
1690
+ 2
1691
+ ],
1689
1692
  "v2-protected-resource-metadata.test.ts": [
1690
1693
  2
1691
1694
  ],
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.46.19",
5
- "corpusTag": "v2.46.19",
4
+ "version": "2.46.21",
5
+ "corpusTag": "v2.46.21",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
8
8
  "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "ac6de5303872664b78ff419c035915593e01599e02835e777a0123de1baeb398",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "9133614d3cad4ff6ecf2758f2f986dfd257dc5a3a2b10110e7537114a2c03a65",
13
- "api/v2/asyncapi.yaml": "5e44771306b5dfa1824967a177208228d12dca66a36c10de5bb0441b8bd26c72",
14
- "api/v2/openapi.yaml": "2f7e4863b8ba09d77e5ae8a7c977311b6138878ba910d7a41d50546034cad7c2",
12
+ "api/seams-v2.yaml": "804179658b0f947c68a735eb33b652dd045b93980ab58a6f266a8de17c22daad",
13
+ "api/v2/asyncapi.yaml": "b881e2ca9b6d4b1de799d1be704118cb10249dbf3b1ecdf0f25a585658cfa690",
14
+ "api/v2/openapi.yaml": "9bd1bd491b690b3b5d0a358b0ef8235d8ded11b040e1e12ce1d4cfb41c24374d",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -117,7 +117,7 @@
117
117
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
118
118
  "schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
119
119
  "schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
120
- "schemas/v2/capabilities.schema.json": "fe28a95bc36767b99cdf529fe7db47dce49c49c5635a724e552c728871c48b06",
120
+ "schemas/v2/capabilities.schema.json": "9d5b222dcd2003141b7f09e9b836744a1b63defc01bffea5d10c1da7747bc2d8",
121
121
  "schemas/v2/certification-bundle.schema.json": "5cfa7fb5247160333e33a58b60d42bcd4e5269d8391d6a112d301e14828892ed",
122
122
  "schemas/v2/channel-presence-payload.schema.json": "fc03df0c20cc12a25f7ee6a47503a4c5931d79fe3d5ff24815e8fd7c1bcb9a87",
123
123
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
@@ -144,7 +144,7 @@
144
144
  "schemas/v2/envelopes/schema.request.schema.json": "be2c5f7aa881cdd252ccff47ac02193659e1393633a1541f4297383205d384a2",
145
145
  "schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
146
146
  "schemas/v2/envelopes/ui.a2ui-surface.schema.json": "e92a2548ba65454c9e0a34f72ea6768d18f59e61fa5da7668d0b2d5baddaddc8",
147
- "schemas/v2/error-envelope.schema.json": "8ddb1d80330dfef17572a74dfe3a53e1a06411e4b0af6a6f1dd6e7e7461da987",
147
+ "schemas/v2/error-envelope.schema.json": "0b15d6a3a2baad6bf314a66a4a8da4756460574887b929b3b06cb23a6c02f0bd",
148
148
  "schemas/v2/eval-summary.schema.json": "d07f4d116c5410997a81d0bc5387545fb61933a6333c360a26ab8a71c749336b",
149
149
  "schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
150
150
  "schemas/v2/form-content-pack-manifest.schema.json": "a1942a124e250a6e9fbdb6b811c5a125e78fbcbcaa46dfc71d0636f24710a339",
@@ -169,7 +169,11 @@
169
169
  "schemas/v2/prompt-pack-manifest.schema.json": "752407469475396ad250085437a6cc6bd56a017ac6d3e52196aecc65dda4e51a",
170
170
  "schemas/v2/prompt-ref.schema.json": "8288495bf353fbc158cef6525d16dd1dcd9373941d15ec072044f05d143355d0",
171
171
  "schemas/v2/prompt-template.schema.json": "63825b39807e4ee85d41b9ad296ff2a0dd7bd2076d91e76a6c15a337e3e3bb84",
172
- "schemas/v2/proposal.schema.json": "a5c582ce1e854e5fe10412ebed00e537961ee827bbaca506ca374b200a685992",
172
+ "schemas/v2/proposal-apply.schema.json": "eb187cfec8cf9a799d94463c34550fa84879e29bbc201a03b5d6ab58efb398d9",
173
+ "schemas/v2/proposal-list-response.schema.json": "1b5e78fc1e4256dbe6bc866487879deb8f7b9cc18f5bc875db3fde46d79bea02",
174
+ "schemas/v2/proposal-revise.schema.json": "065e75cccd01fcc619e63c25f2a1775adcc24b0467689dca42462ace6bfa1146",
175
+ "schemas/v2/proposal-submit.schema.json": "00fb91a4a76ebcee530295f5262b8b369ed024055cd29a47aa520db03a7fc6c9",
176
+ "schemas/v2/proposal.schema.json": "52a1e54cfce966a07e2588d5e323a4fa896470ccd3a3a21d2ed19d977d2922b7",
173
177
  "schemas/v2/registry-version-manifest.schema.json": "940cfd82511690cd6f2539e5676b185e7812628d0707c283fd7ffb33b6e54afd",
174
178
  "schemas/v2/residency.schema.json": "3fd23c9732beb140504e81560fc426eb6764d547e56da5fb92c5a5c7bf04db3a",
175
179
  "schemas/v2/run-ancestry-response.schema.json": "334c17f2062dea62726ca1d07b8be3f169cdc422af843c1def5fb317039814c9",
@@ -208,14 +212,14 @@
208
212
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
209
213
  "spec/v1/alias-detectors.json": "22444c22bb88c568339ef512553f9fc38146e22b566d11ab8608f655d688d19c",
210
214
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
211
- "spec/v1/core-standard-manifest.json": "6b5a2e873b59e22f27bd9832559382a42ad23e29cac8f9156d592bb19e168fb1",
215
+ "spec/v1/core-standard-manifest.json": "6e33742deb9559c5d995750c0a13c7f83446ab8e74c6f2f2d728b107f929e053",
212
216
  "spec/v1/deprecations.json": "e723bc11d7272c71789ad1303bb7f21809a8a024abf57594f9d5c35dc199ece9",
213
217
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
214
218
  "spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
215
219
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
216
220
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
217
221
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
218
- "spec/v1/gaps.json": "52777098a223e678d12751507b1ac0d02eef7465d6263c25175489734055cc3e",
222
+ "spec/v1/gaps.json": "9db4f2d06006cbfa55773e3a1dce2b0418a8da94ede4bf6e2992e9f18eaac4d1",
219
223
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
220
224
  "spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
221
225
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -227,11 +231,11 @@
227
231
  "spec/v2/core/conformance.md": "3ca291e63bc70a6ef829ae8759ad0ded3741ea7aa507bbbdffc57de443ab2ae2",
228
232
  "spec/v2/core/connection-packs.md": "c80f95bd6a03698f35c72cdb2b46bd973f49203bbd786c26760e35fa86e6f23d",
229
233
  "spec/v2/core/conversation.md": "8d6abedf6af408a74577753bfca24719d318e2632e10b45ff5d63bdcf31a8aaa",
230
- "spec/v2/core/errors.md": "e48838701b86bbfe0ffee77b18a8ffc4e5f04724b4318507d9771da653f8da0f",
234
+ "spec/v2/core/errors.md": "e932dd663ec7434480038ca9cdbe8fe37f6e422350ce95574522af4006b8a356",
231
235
  "spec/v2/core/events.md": "c3dcece6f1b1aa43e1e01c5ece23b2df075a505d5f6c2f37de0801aeaa486b62",
232
236
  "spec/v2/core/execution.md": "4218dbb397534940f83ca5d46561f02bf8f3fdf68d2ae7fefe7f2eb2ed771103",
233
237
  "spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
234
- "spec/v2/core/headers.md": "f69fe81e4a3ad21879bb85948660c16860eb05bf152928c31aa4898f6d7d1887",
238
+ "spec/v2/core/headers.md": "dae37c2b3980099272e5b9581107c0410d6b23aac95ea6c5a0890c34f0e51c8f",
235
239
  "spec/v2/core/host-services.md": "a6b5af882cb1e5c992dddc53c4de06feb36eee01bd566614330041213b3ce657",
236
240
  "spec/v2/core/i18n.md": "882904b7d59f992ab1e5f7346167161a54cf665b8fee47e64a3862cf9133a580",
237
241
  "spec/v2/core/idempotency.md": "adbd317a6ebb57c2759c076297b31f67def9e391576bccd0bbea787b13e85872",
@@ -246,7 +250,7 @@
246
250
  "spec/v2/core/portability.md": "ab13a6ca43544b9a6a91edfb377ec680c768d14be841c84fb79125b231426891",
247
251
  "spec/v2/core/replay.md": "9e66e2029b65866187c0920428a8c5bf8ad1c2b39feefbd5c02f0e59909afc7d",
248
252
  "spec/v2/core/runs.md": "1c292c0ab4a3119aeff3a6e9b6e99cff69782df78c3872179f9f0661b6204f23",
249
- "spec/v2/core/security-defaults.md": "887d8c4bc592094e3b931c23f1c2175c18ebbd47528b074016f9d2091bc513b2",
253
+ "spec/v2/core/security-defaults.md": "d201fbcd964a624ab45a8bdb7ceca113c561df4c33e9017c2952dd8ef62919a4",
250
254
  "spec/v2/core/storage.md": "dc94b985847c1341c7a064f04a6010f0fdbe6c3ab3e555d4362878c81d764c18",
251
255
  "spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
252
256
  "spec/v2/core/versioning.md": "348a6dc5bd3425efc0016bb6772d5d04ebf578b73eebcdfec6c2d82346a8577e",
@@ -257,7 +261,7 @@
257
261
  "spec/v2/declaration.json": "5ccdcf9865ca02022c01a551583e1f343a4b7c73fd228049f1be620858a60ad5",
258
262
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
259
263
  "spec/v2/eos-override.json": "4c72d2f6a658943b01c1de1643cf294642091933af7832f38ee5495df684cb2d",
260
- "spec/v2/errors.json": "1f0e29455edbd22e28f67f221766209c3c55ac4987beb77993b5702dd7cae131",
264
+ "spec/v2/errors.json": "33f9b4e450c51dfb1046bb00b327ee88163fb53360b0b43bcd204061ec966a13",
261
265
  "spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
262
266
  "spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
263
267
  "spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
@@ -281,7 +285,7 @@
281
285
  "spec/v2/ext/sandbox-runtime-notes/README.md": "0d1290fb1e20b2e35c0b15051541ab6546e4e2a6f693b4f912cb3f7d6a65e92c",
282
286
  "spec/v2/ext/webResearch/README.md": "b675e6f6646128d781b661f754c2393eed86e5935d16c3a22df582a5ce025b9b",
283
287
  "spec/v2/facets/a2a.schema.json": "b68bf5e29e455e46c2ad7cfcccf5963730ec2310ff9db81884550d8df30fd6ef",
284
- "spec/v2/facets/agents.schema.json": "88fe144572c25e1b09b99fd83e2ad12b2611d4d7366b5d53c3a50d0310faad93",
288
+ "spec/v2/facets/agents.schema.json": "0eb1172ea140607cb46d164af8bf405b32f5b47aa54baac7844e2c4161290025",
285
289
  "spec/v2/facets/aiProviders.schema.json": "6e4327bcd48110686cc1adfd26ddd6fbbf4ff529c476dcc74eeff8c5ae9965ce",
286
290
  "spec/v2/facets/auditLogIntegrity.schema.json": "9a7f81320c8f940c6f4129bc7dd895a4063231c5458a717a08c2cbf9bb886c46",
287
291
  "spec/v2/facets/auth.schema.json": "7d98bc81a0f7d307460d039eebfebd21388091bfe1d538762583940f94c0bd2a",
@@ -303,20 +307,20 @@
303
307
  "spec/v2/facets/uiPlugins.schema.json": "1fd3f8440cfb6e4422553a7c4b5f091eb3e10a40da0d43e8b977a18929116758",
304
308
  "spec/v2/facets/webhooks.schema.json": "54b576f1d150e28e1642693d846ce471c118e9c521bdf3924185305fbd1d0de4",
305
309
  "spec/v2/facets/workflowChainPacks.schema.json": "ea625ce818fbeb746478f00f31f96ee9613d538e4409c258c1bfb517acdeaba9",
306
- "spec/v2/generated/error-codes.md": "eafb66f1632da783c9a0347a416ae32bcf322f41b9f8783375f579791e9cdcf0",
310
+ "spec/v2/generated/error-codes.md": "e8441e944d2a8856a9112c1de8175e5bfae8bb36b8f389dce9dfa838702c21b6",
307
311
  "spec/v2/harness-trust-anchors.json": "6007b92eae16a070fbc3777424efb0abfe865fda89d096963109b96282b91205",
308
312
  "spec/v2/harness-trust-anchors.schema.json": "372b5d92337c893d3449eff3f6bfd009ac09fdac285296d8bdbe0a08fff9e34c",
309
- "spec/v2/id-field-bindings.json": "aa3e7c3590a490a77286e3ae60c1ab7407e7d627e05f981c3e08f221af4bd5dc",
313
+ "spec/v2/id-field-bindings.json": "97e96b6c35c405d0408b5a877c5859714dbbf4507853de9aab381163371ac098",
310
314
  "spec/v2/interop-map.json": "02255dafe7eb55f61ca7f9ae7c21f079d1848a6756991343088975ac9d184b8d",
311
315
  "spec/v2/interop-map.schema.json": "0017c882df34ee215e2e0729de051490e09d43aac3d2c949285698ed51832f12",
312
316
  "spec/v2/migrations.json": "651954938a1e154c0b6f1e3dd6e105889d30de3bb65789f21de7f77a12869e9e",
313
317
  "spec/v2/migrations.schema.json": "b91ade6320ad1b1185c9495b9424f3aa63438ed8e412c45171c227d82c0afd86",
314
- "spec/v2/path-manifest.json": "7b5679a4dad98c80d855361d88a98c8a4e045af15e310862a05b285b815dc9fc",
318
+ "spec/v2/path-manifest.json": "20f598f56df460ad3ed50aba3d6bc2933ea16a3c96ce125da1c5dd6989d4155c",
315
319
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
316
320
  "spec/v2/profiles.json": "b382b261c7844798575f7c1b37c934f0da19846016a2337d883d4f5ef6e05479",
317
- "spec/v2/release.json": "9870141128549af0cac3a0e7c4906c87a5ce65047591a3824690b9e20dcf59c1",
321
+ "spec/v2/release.json": "a777b332ad7a5ed3c721a42be6fd72b8cc30eaba7e1673d86f1249aca26772e0",
318
322
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
319
- "spec/v2/surface-baseline.json": "5ba73afbabe33b983c3ac74670a6aacfe32042a0d40de2faf6cdb151b055a1ae"
323
+ "spec/v2/surface-baseline.json": "ed538138c27c16b2d16618fe3cf96737e285f00f5b19710c61a89e33364d050d"
320
324
  },
321
- "corpusCommit": "d49a2818d766f78f7c3fc9d55403ed42a404ba71"
325
+ "corpusCommit": "d2f718e938cc2f7e68ce050376e563749a5af995"
322
326
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * RFC 0081 §C — the `eval.*` envelope of an eval run, and the summary it
3
+ * serves. A pure judge over one eval run, for
4
+ * `v2-eval-trajectory-trials.test.ts`.
5
+ *
6
+ * The rules are the `run-event-payloads` descriptions, which the `agents`
7
+ * family lists as normative text:
8
+ * - `evalStarted` — emitted ONCE, at the start of the run;
9
+ * - `evalScored` — once per task (per trial under RFC 0244 §B);
10
+ * - `evalCompleted` — emitted ONCE, after all tasks and before
11
+ * `run.completed`; its aggregates describe the same run the summary does.
12
+ * The served `EvalSummary` validates against `eval-summary.schema.json`.
13
+ *
14
+ * Until 2.46.20 no v2 leg read `eval.completed`; the frozen v1
15
+ * `agent-eval-run.test.ts` did (RFC 0081 correction note, 2026-10-24).
16
+ *
17
+ * @see RFCS/0081-agent-evaluation-and-scorecards.md
18
+ */
19
+
20
+ export interface EnvelopeFinding { readonly ok: boolean; readonly doc: string; readonly message: string }
21
+
22
+ type Rec = Record<string, unknown>;
23
+ interface Event { readonly type: string; readonly sequence: number; readonly payload: Rec }
24
+
25
+ const isRec = (v: unknown): v is Rec => typeof v === 'object' && v !== null && !Array.isArray(v);
26
+ const f = (ok: boolean, doc: string, message: string): EnvelopeFinding => ({ ok, doc, message });
27
+
28
+ const C = 'run-event-payloads evalStarted / evalScored / evalCompleted (RFC 0081 §C)';
29
+
30
+ export interface EnvelopeOptions {
31
+ /** Validates an `eval.completed` payload against `run-event-payloads#/$defs/evalCompleted`. */
32
+ readonly completedValid: (payload: unknown) => { ok: boolean; errors: string };
33
+ }
34
+
35
+ export function judgeEvalEnvelope(events: readonly Event[], summary: Rec, opts: EnvelopeOptions): EnvelopeFinding[] {
36
+ const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
37
+ const at = (type: string): number[] => ordered.flatMap((e, i) => (e.type === type ? [i] : []));
38
+ const started = at('eval.started');
39
+ const scored = at('eval.scored');
40
+ const completed = at('eval.completed');
41
+ const runCompleted = at('run.completed');
42
+ const out: EnvelopeFinding[] = [];
43
+
44
+ out.push(f(started.length === 1, C, `eval.started MUST be emitted once (got ${started.length})`));
45
+ out.push(f(completed.length === 1, C, `eval.completed MUST be emitted once (got ${completed.length})`));
46
+ out.push(f(scored.length > 0, C, 'the run MUST emit eval.scored for its tasks'));
47
+ if (started.length === 1 && scored.length > 0) {
48
+ out.push(f(started[0]! < scored[0]!, C, 'eval.started MUST precede every eval.scored'));
49
+ }
50
+ if (completed.length !== 1) return out;
51
+ const c = completed[0]!;
52
+ if (scored.length > 0) out.push(f(c > scored[scored.length - 1]!, C, 'eval.completed MUST follow every eval.scored ("after all tasks")'));
53
+ if (runCompleted.length > 0) out.push(f(c < runCompleted[0]!, C, 'eval.completed MUST precede run.completed'));
54
+
55
+ const payload = ordered[c]!.payload;
56
+ const v = opts.completedValid(payload);
57
+ out.push(f(v.ok, C, `eval.completed MUST validate: ${v.errors}`));
58
+ for (const k of ['taskCount', 'passedCount', 'passed'] as const) {
59
+ if (k in summary) out.push(f(payload[k] === summary[k], C, `eval.completed.${k} MUST equal the served summary's (event ${JSON.stringify(payload[k])}, summary ${JSON.stringify(summary[k])})`));
60
+ }
61
+ return out;
62
+ }
63
+
64
+ /** The served summary validates, and its per-task rows agree with its own counts. */
65
+ export function judgeSummaryServed(summary: Rec, valid: (s: unknown) => { ok: boolean; errors: string }): EnvelopeFinding[] {
66
+ const out: EnvelopeFinding[] = [];
67
+ const v = valid(summary);
68
+ out.push(f(v.ok, 'eval-summary.schema.json (RFC 0081 §C)', `the served EvalSummary MUST validate: ${v.errors}`));
69
+ const tasks = Array.isArray(summary['tasks']) ? (summary['tasks'] as unknown[]).filter(isRec) : [];
70
+ out.push(f(summary['taskCount'] === tasks.length, 'eval-summary.schema.json (RFC 0081 §C)', `taskCount MUST equal the number of tasks (got ${JSON.stringify(summary['taskCount'])}, ${tasks.length} tasks)`));
71
+ const passed = tasks.filter((t) => t['passed'] === true).length;
72
+ out.push(f(summary['passedCount'] === passed, 'eval-summary.schema.json (RFC 0081 §C)', `passedCount MUST count the passed tasks (got ${JSON.stringify(summary['passedCount'])}, ${passed} passed)`));
73
+ return out;
74
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * RFC 0249 — pure judges for `v2-proposal-lifecycle.test.ts`. The scenario
3
+ * drives one client-submitted `prompt-template` proposal through submit →
4
+ * revise → apply and records every response; these functions decide each leg
5
+ * from those records, so the self-tests can fail every wrong host answer
6
+ * without a host.
7
+ *
8
+ * The rules are the `agents.proposals` facet description
9
+ * (`spec/v2/facets/agents.schema.json`), `proposal.schema.json`, the proposal
10
+ * operations in `api/v2/openapi.yaml` and `spec/v2/errors.json`. They make
11
+ * RFC 0096's `proposal-inert-until-applied` and `proposal-no-resynthesis`
12
+ * observable for the one kind with a v2 read-back.
13
+ *
14
+ * @see RFCS/0249-proposal-operations-and-install-readback.md
15
+ */
16
+
17
+ import { createHash } from 'node:crypto';
18
+ import { canonicalJSON } from './jcs.js';
19
+ import { readErrorCode } from './error-envelope.js';
20
+
21
+ export interface ProposalFinding { readonly ok: boolean; readonly message: string }
22
+
23
+ type Rec = Record<string, unknown>;
24
+ /** One recorded HTTP exchange. `null` means the request never got an answer. */
25
+ export type Exchange = { readonly status: number; readonly json: unknown } | null;
26
+
27
+ const isRec = (v: unknown): v is Rec => typeof v === 'object' && v !== null && !Array.isArray(v);
28
+ const f = (ok: boolean, message: string): ProposalFinding => ({ ok, message });
29
+ // The v2 envelope is flat: `{ error: "<code>", message, details? }`
30
+ // (`error-envelope.schema.json`). `readErrorCode` reads it (and tolerates the
31
+ // legacy nested form); `details` sits at the root.
32
+ const code = (x: Exchange): string | undefined => readErrorCode(x?.json);
33
+ const details = (x: Exchange): Rec => {
34
+ const body = isRec(x?.json) ? x.json : {};
35
+ if (isRec(body['details'])) return body['details'];
36
+ const nested = body['error'];
37
+ return isRec(nested) && isRec(nested['details']) ? nested['details'] : {};
38
+ };
39
+ const shown = (x: Exchange): string => (x === null ? 'no answer' : `${x.status}${code(x) ? ` ${code(x)}` : ''}`);
40
+
41
+ /** `sha256:` + lowercase hex SHA-256 of the RFC 8785 (JCS) serialization. */
42
+ export function artifactDigest(artifact: unknown): string {
43
+ return `sha256:${createHash('sha256').update(canonicalJSON(artifact), 'utf8').digest('hex')}`;
44
+ }
45
+
46
+ /** The kind's host-owned fields removed (for `prompt-template`, `meta`). */
47
+ export function withoutHostOwned(body: unknown): unknown {
48
+ if (!isRec(body)) return body;
49
+ const { meta: _meta, ...rest } = body;
50
+ return rest;
51
+ }
52
+
53
+ /** JCS equality after removing host-owned fields from both sides. */
54
+ export function byteMatches(served: unknown, artifact: unknown): boolean {
55
+ return canonicalJSON(withoutHostOwned(served)) === canonicalJSON(withoutHostOwned(artifact));
56
+ }
57
+
58
+ /** A fresh, author-minted template id the host has never seen. */
59
+ export function freshTemplate(nonce: string, text: string): Rec {
60
+ return { templateId: `conformance.proposal.${nonce}`, version: '1.0.0', kind: 'system', text };
61
+ }
62
+
63
+ /** A submitted kind outside `artifactKinds` is `400 validation_error` naming `kind`. */
64
+ export function judgeSubmitKindRefused(x: Exchange): ProposalFinding[] {
65
+ return [
66
+ f(x?.status === 400 && code(x) === 'validation_error', `a kind outside artifactKinds MUST be refused 400 validation_error (got ${shown(x)})`),
67
+ f(details(x)['field'] === 'kind', `the refusal MUST name details.field "kind" (got ${JSON.stringify(details(x)['field'])})`),
68
+ ];
69
+ }
70
+
71
+ /** The draft's template is not served, listed or renderable while unapplied. */
72
+ export function judgeInert(get: Exchange, list: Exchange, render: Exchange, templateId: string, when: string): ProposalFinding[] {
73
+ const items = isRec(list?.json) && Array.isArray(list.json['items']) ? (list.json['items'] as unknown[]).filter(isRec) : null;
74
+ return [
75
+ f(get?.status === 404, `${when}: getPromptTemplate MUST NOT serve the unapplied draft's template (got ${shown(get)})`),
76
+ f(list?.status === 200 && items !== null, `${when}: listPromptTemplates MUST answer 200 with items (got ${shown(list)})`),
77
+ f(items !== null && !items.some((t) => t['templateId'] === templateId), `${when}: listPromptTemplates MUST NOT list the unapplied draft's template`),
78
+ f(render !== null && render.status >= 400, `${when}: :render naming the unapplied draft's template MUST fail as for an unknown template (got ${shown(render)})`),
79
+ ];
80
+ }
81
+
82
+ /** An apply the caller is not scoped for installs nothing and changes no state. */
83
+ export function judgeApplyUnscoped(apply: Exchange, after: Exchange, prompt: Exchange, wwwAuthenticate: string | null): ProposalFinding[] {
84
+ const state = isRec(after?.json) ? after.json['state'] : undefined;
85
+ return [
86
+ f(apply?.status === 403, `an apply without proposals:apply MUST be refused 403 (got ${shown(apply)})`),
87
+ f(typeof wwwAuthenticate === 'string' && /error="insufficient_scope"/.test(wwwAuthenticate), `the 403 MUST carry error="insufficient_scope" (got ${wwwAuthenticate ?? 'no WWW-Authenticate'})`),
88
+ f(state === 'revised', `a refused apply MUST leave state unchanged (got ${JSON.stringify(state)})`),
89
+ f(prompt?.status === 404, `a refused apply MUST install nothing (getPromptTemplate ${shown(prompt)})`),
90
+ ];
91
+ }
92
+
93
+ /** An apply bound to bytes the reviewer no longer holds is `409 proposal_stale`. */
94
+ export function judgeApplyStale(apply: Exchange, after: Exchange, prompt: Exchange): ProposalFinding[] {
95
+ const state = isRec(after?.json) ? after.json['state'] : undefined;
96
+ return [
97
+ f(apply?.status === 409 && code(apply) === 'proposal_stale', `an apply whose artifactDigest is not the persisted artifact's MUST be refused 409 proposal_stale (got ${shown(apply)})`),
98
+ f(state === 'revised', `a stale apply MUST leave state unchanged (got ${JSON.stringify(state)})`),
99
+ f(prompt?.status === 404, `a stale apply MUST install nothing (getPromptTemplate ${shown(prompt)})`),
100
+ ];
101
+ }
102
+
103
+ /** The revision is persisted, and the served digest is the revised artifact's. */
104
+ export function judgeRevised(revise: Exchange, revised: Rec): ProposalFinding[] {
105
+ const p = isRec(revise?.json) ? revise.json : {};
106
+ return [
107
+ f(revise?.status === 200 && p['state'] === 'revised', `a revise MUST answer 200 with state revised (got ${shown(revise)}, state ${JSON.stringify(p['state'])})`),
108
+ f(p['artifactDigest'] === artifactDigest(revised), `artifactDigest MUST be sha256 over the JCS of the revised artifact (got ${JSON.stringify(p['artifactDigest'])})`),
109
+ ];
110
+ }
111
+
112
+ /** A successful apply names the read that serves the installed artifact. */
113
+ export function judgeApplied(apply: Exchange, templateId: string): ProposalFinding[] {
114
+ const p = isRec(apply?.json) ? apply.json : {};
115
+ const act = isRec(p['activation']) ? p['activation'] : {};
116
+ const ia = isRec(act['installedArtifact']) ? act['installedArtifact'] : {};
117
+ return [
118
+ f(apply?.status === 200 && p['state'] === 'applied', `an authorized, current apply MUST answer 200 with state applied (got ${shown(apply)}, state ${JSON.stringify(p['state'])})`),
119
+ f(ia['kind'] === 'prompt-template' && ia['operationId'] === 'getPromptTemplate', `activation.installedArtifact MUST name kind prompt-template and operationId getPromptTemplate (got ${JSON.stringify(ia)})`),
120
+ f(ia['id'] === templateId, `installedArtifact.id MUST be the artifact's templateId, never a minted one (got ${JSON.stringify(ia['id'])})`),
121
+ ];
122
+ }
123
+
124
+ /** The installed bytes are the revised artifact's, not the original's. */
125
+ export function judgeReadback(served: Exchange, revised: Rec, original: Rec): ProposalFinding[] {
126
+ const body = served?.json;
127
+ return [
128
+ f(served?.status === 200, `getPromptTemplate MUST serve the installed template after apply (got ${shown(served)})`),
129
+ f(served?.status === 200 && byteMatches(body, revised), 'the read-back minus meta MUST be JCS-equal to the last-persisted (revised) artifact minus meta'),
130
+ f(served?.status === 200 && !byteMatches(body, original), 'the read-back MUST NOT be the pre-revision artifact: that is a silent re-synthesis'),
131
+ ];
132
+ }
133
+
134
+ /** Revise and apply on an applied (terminal) proposal are `409 proposal_state_conflict`. */
135
+ export function judgeTerminal(revise: Exchange, apply: Exchange): ProposalFinding[] {
136
+ return [revise, apply].flatMap((x, i) => {
137
+ const action = i === 0 ? 'revise' : 'apply';
138
+ return [
139
+ f(x?.status === 409 && code(x) === 'proposal_state_conflict', `${action} on an applied proposal MUST be refused 409 proposal_state_conflict (got ${shown(x)})`),
140
+ f(details(x)['state'] === 'applied', `the ${action} refusal MUST name details.state "applied" (got ${JSON.stringify(details(x)['state'])})`),
141
+ ];
142
+ });
143
+ }
144
+
145
+ /** Another tenant sees the proposal and the installed template as ids that do not exist. */
146
+ export function judgeForeign(pairs: ReadonlyArray<{ what: string; foreign: Exchange; unknown: Exchange }>): ProposalFinding[] {
147
+ return pairs.flatMap(({ what, foreign, unknown }) => [
148
+ f(foreign?.status === 404, `${what} as another tenant MUST answer 404 (got ${shown(foreign)})`),
149
+ f(foreign?.status === unknown?.status && code(foreign) === code(unknown), `${what} as another tenant MUST answer exactly as an unknown id (foreign ${shown(foreign)}, unknown ${shown(unknown)}) — otherwise it is an existence oracle`),
150
+ ]);
151
+ }
@@ -32,6 +32,13 @@
32
32
  * lists `conformance-eval-trials` refuses it at createRun with
33
33
  * `400 validation_error`.
34
34
  *
35
+ * RFC 0081 §C, on the `conformance-eval-trajectory` run (2.46.20). Judges in
36
+ * `lib/eval-envelope-witness.ts`.
37
+ * - `eval.started` and `eval.completed` are emitted once each, bracketing
38
+ * every `eval.scored`, and `eval.completed` precedes `run.completed`; its
39
+ * counts equal the served summary's.
40
+ * - The served `EvalSummary` validates, and its counts match its rows.
41
+ *
35
42
  * RFC 0246 (safety fix): a trial that failed its trajectory counts 0 in the
36
43
  * two pass decisions, while every reported score is unchanged. Judges in
37
44
  * `lib/eval-gate-witness.ts`.
@@ -73,6 +80,7 @@ import { majorProfile } from '../lib/major-profile.js';
73
80
  import { isFixtureAdvertised } from '../lib/fixtures.js';
74
81
  import { FIXTURES_DIR } from '../lib/paths.js';
75
82
  import { pollTerminal, readEvents, isRecord, type Ev } from '../lib/fixture-run-observer.js';
83
+ import { judgeEvalEnvelope, judgeSummaryServed } from '../lib/eval-envelope-witness.js';
76
84
  import { gradeTrajectory, passAtK, passHatK, scoreStderr, withinTolerance, type TrajectorySpec } from '../lib/eval-trajectory.js';
77
85
  import { judgeMeanGated, judgeSuiteGated } from '../lib/eval-gate-witness.js';
78
86
  import { judgeFixtureErrorReplayed } from '../lib/eval-fixture-error-witness.js';
@@ -97,6 +105,9 @@ const DOC_0246 = 'eval-summary.schema.json gatedScore / gatedAggregateScore (RFC
97
105
  const ID_FIXTURE_ERROR_SHAPE = 'openwop.requirement.0245.fixture-error-shape';
98
106
  const ID_FIXTURE_ERROR = 'openwop.requirement.0245.fixture-error-replayed';
99
107
  const ID_FIXTURE_ERROR_REFUSED = 'openwop.requirement.0245.unadvertised-refused';
108
+ const ID_ENVELOPE = 'openwop.requirement.0081.eval-envelope';
109
+ const ID_SUMMARY = 'openwop.requirement.0081.summary-served';
110
+ const DOC_0081 = 'run-event-payloads evalStarted / evalCompleted, eval-summary.schema.json (RFC 0081 §C)';
100
111
  const DOC_0245 = 'agents.schema.json §evalSuite.fixtureErrors (RFC 0245)';
101
112
 
102
113
  interface Task { taskId: string; input: { script: Array<{ tool: string; arguments?: unknown }>; output: string }; expected: { kind: string; trajectory?: TrajectorySpec }; fixtures?: { toolResponses?: Array<Record<string, unknown>> } }
@@ -107,6 +118,7 @@ const suiteSchema = v2Validator('agent-eval-suite');
107
118
  const summarySchema = v2Validator('eval-summary');
108
119
  const scored = v2RefValidator('run-event-payloads.schema.json#/$defs/evalScored');
109
120
  const started = v2RefValidator('run-event-payloads.schema.json#/$defs/evalStarted');
121
+ const completedPayload = v2RefValidator('run-event-payloads.schema.json#/$defs/evalCompleted');
110
122
 
111
123
  async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
112
124
  try { return await fn(); } catch { return null; }
@@ -225,6 +237,33 @@ describe('v2 RFC 0244 — host behaviour', () => {
225
237
  expect(calls, req(ID_TRAJECTORY, DOC, `the log MUST carry one agent.tool-called per scripted call (${scripted})`)).toBe(scripted);
226
238
  });
227
239
 
240
+ async function trajectoryRun(): Promise<{ skip: [Parameters<typeof softSkip>[0], string] } | { o: Extract<Observed, { kind: 'ok' }> }> {
241
+ const { doc, facet } = await evalSuiteFacet();
242
+ if (!doc) return { skip: ['blocked', UNREADABLE] };
243
+ if (facet?.['trajectory'] !== true) return { skip: ['inapplicable', 'the host does not advertise agents.evalSuite.trajectory'] };
244
+ const id = 'conformance-eval-trajectory';
245
+ if (!isFixtureAdvertised(id)) return { skip: ['inapplicable', `the host does not advertise fixture ${id}`] };
246
+ const o = await runSuite(id);
247
+ if (o.kind === 'skip') return { skip: [o.disposition, o.reason] };
248
+ return { o };
249
+ }
250
+
251
+ it('an eval run brackets its scores with one eval.started and one eval.completed (RFC 0081 §C)', async () => {
252
+ const r = await trajectoryRun();
253
+ if ('skip' in r) return softSkip(...r.skip);
254
+ for (const x of judgeEvalEnvelope(r.o.events, r.o.summary, { completedValid: completedPayload })) {
255
+ expect(x.ok, req(ID_ENVELOPE, DOC_0081, x.message)).toBe(true);
256
+ }
257
+ });
258
+
259
+ it('the served EvalSummary validates and its counts match its rows (RFC 0081 §C)', async () => {
260
+ const r = await trajectoryRun();
261
+ if ('skip' in r) return softSkip(...r.skip);
262
+ for (const x of judgeSummaryServed(r.o.summary, summarySchema)) {
263
+ expect(x.ok, req(ID_SUMMARY, DOC_0081, x.message)).toBe(true);
264
+ }
265
+ });
266
+
228
267
  async function trialsRun(): Promise<{ skip: [Parameters<typeof softSkip>[0], string] } | { o: Extract<Observed, { kind: 'ok' }>; suite: Suite; n: number; k: number }> {
229
268
  const { doc, facet } = await evalSuiteFacet();
230
269
  if (!doc) return { skip: ['blocked', UNREADABLE] };
@@ -0,0 +1,275 @@
1
+ /**
2
+ * v2 — reviewable-learning proposals are inert until applied, and what is
3
+ * installed is what was reviewed (RFC 0249, making RFC 0096 observable; the
4
+ * rules are the `agents.proposals` facet description in
5
+ * `spec/v2/facets/agents.schema.json`, `proposal.schema.json`, the proposal
6
+ * operations in `api/v2/openapi.yaml` and `spec/v2/errors.json`).
7
+ *
8
+ * Server-free (always on):
9
+ * 1. `0249.shape` — the submit / revise / apply bodies and the closed
10
+ * `installedArtifact` validate, and their negatives are refused.
11
+ *
12
+ * Host legs — gated on `agents.proposals` with `clientDrafts: true` and
13
+ * `readback` ∋ `prompt-template`. One client-submitted `prompt-template`
14
+ * proposal with a fresh `templateId` is driven once per worker:
15
+ * submit T → revise to T' → stale apply → unscoped apply → foreign reads →
16
+ * apply → read back → terminal refusals. Judges in
17
+ * `lib/proposal-lifecycle-witness.ts`.
18
+ * 2. `0249.submit-kind-refused` — a kind outside `artifactKinds` is `400`.
19
+ * 3. `0249.draft-inert` — the draft's template is not served, listed or
20
+ * renderable (`proposal-inert-until-applied`).
21
+ * 4. `0249.apply-stale-refused` — the revision's digest is served, and an
22
+ * apply bound to the pre-revision digest is `409 proposal_stale`.
23
+ * 5. `0249.apply-unscoped-refused` — `OPENWOP_TEST_LOW_SCOPE_KEY` (read +
24
+ * write, no `proposals:apply`) is refused `403 insufficient_scope`.
25
+ * 6. `0249.apply-installs` — the apply names `getPromptTemplate` and the
26
+ * artifact's own `templateId`.
27
+ * 7. `0249.readback-byte-match` — the installed template minus `meta` is
28
+ * JCS-equal to T' and not to T (`proposal-no-resynthesis`).
29
+ * 8. `0249.terminal-refused` — revise and apply on the applied proposal are
30
+ * `409 proposal_state_conflict`.
31
+ * 9. `0249.cross-tenant-not-found` — `OPENWOP_TEST_TENANT_B_API_KEY` sees the
32
+ * proposal and the installed template exactly as unknown ids.
33
+ *
34
+ * A harness key that is unset records `blocked` (the `v2-auth-challenge`
35
+ * precedent): the suite will not infer a credential. The unadvertised case is
36
+ * in `v2-unadvertised-operation-not-found`.
37
+ */
38
+
39
+ import { describe, it, expect } from 'vitest';
40
+ import { randomBytes } from 'node:crypto';
41
+ import { driver, type OpenWOPRequestInit } from '../lib/driver.js';
42
+ import { v2Discovery, v2Validator, v2RefValidator } from '../lib/v2.js';
43
+ import { softSkip } from '../lib/soft-skip.js';
44
+ import { req } from '../lib/requirement-ids.js';
45
+ import {
46
+ artifactDigest, freshTemplate, judgeApplied, judgeApplyStale, judgeApplyUnscoped, judgeForeign, judgeInert,
47
+ judgeReadback, judgeRevised, judgeSubmitKindRefused, judgeTerminal, type Exchange, type ProposalFinding,
48
+ } from '../lib/proposal-lifecycle-witness.js';
49
+
50
+ const DOC = 'agents.schema.json §proposals, proposal.schema.json, api/v2 proposals operations (RFC 0249)';
51
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0';
52
+ const KINDS = ['agent-pack', 'workflow-chain-pack', 'prompt-template', 'automation'] as const;
53
+ const ID_SHAPE = 'openwop.requirement.0249.shape';
54
+ const ID_SUBMIT_KIND_REFUSED = 'openwop.requirement.0249.submit-kind-refused';
55
+ const ID_DRAFT_INERT = 'openwop.requirement.0249.draft-inert';
56
+ const ID_APPLY_STALE_REFUSED = 'openwop.requirement.0249.apply-stale-refused';
57
+ const ID_APPLY_UNSCOPED_REFUSED = 'openwop.requirement.0249.apply-unscoped-refused';
58
+ const ID_APPLY_INSTALLS = 'openwop.requirement.0249.apply-installs';
59
+ const ID_READBACK_BYTE_MATCH = 'openwop.requirement.0249.readback-byte-match';
60
+ const ID_TERMINAL_REFUSED = 'openwop.requirement.0249.terminal-refused';
61
+ const ID_CROSS_TENANT_NOT_FOUND = 'openwop.requirement.0249.cross-tenant-not-found';
62
+
63
+ type Rec = Record<string, unknown>;
64
+ const isRec = (v: unknown): v is Rec => typeof v === 'object' && v !== null && !Array.isArray(v);
65
+
66
+ interface Answer { readonly x: Exchange; readonly wwwAuthenticate: string | null }
67
+ async function ask(method: string, path: string, body?: unknown, bearer?: string): Promise<Answer> {
68
+ const init: OpenWOPRequestInit = {
69
+ ...(body === undefined ? {} : { body }),
70
+ ...(bearer === undefined ? {} : { authenticated: false, headers: { Authorization: `Bearer ${bearer}` } }),
71
+ };
72
+ try {
73
+ const r = await driver.request(method, path, init);
74
+ return { x: { status: r.status, json: r.json }, wwwAuthenticate: r.headers.get('www-authenticate') };
75
+ } catch {
76
+ return { x: null, wwwAuthenticate: null };
77
+ }
78
+ }
79
+
80
+ interface Facet { readonly doc: Rec | null; readonly facet: Rec | null; readonly renderPath: string }
81
+ async function proposalsFacet(): Promise<Facet> {
82
+ let doc: Rec | null;
83
+ try { doc = await v2Discovery(); } catch { doc = null; }
84
+ const agents = doc?.['agents'];
85
+ const facet = isRec(agents) && isRec(agents['proposals']) ? agents['proposals'] : null;
86
+ const prompts = doc?.['prompts'];
87
+ const renderPath = isRec(prompts) && typeof prompts['renderEndpoint'] === 'string' ? prompts['renderEndpoint'] : '/prompts:render';
88
+ return { doc, facet, renderPath };
89
+ }
90
+
91
+ type Skip = { skip: [Parameters<typeof softSkip>[0], string] };
92
+ async function gate(): Promise<Skip | { facet: Rec; renderPath: string }> {
93
+ const { doc, facet, renderPath } = await proposalsFacet();
94
+ if (!doc) return { skip: ['blocked', UNREADABLE] };
95
+ if (facet === null) return { skip: ['inapplicable', 'the host does not advertise agents.proposals'] };
96
+ if (facet['clientDrafts'] !== true) return { skip: ['inapplicable', 'the host does not advertise agents.proposals.clientDrafts'] };
97
+ const rb = Array.isArray(facet['readback']) ? facet['readback'] : [];
98
+ const kinds = Array.isArray(facet['artifactKinds']) ? facet['artifactKinds'] : [];
99
+ if (!rb.includes('prompt-template') || !kinds.includes('prompt-template')) return { skip: ['inapplicable', 'the host does not advertise prompt-template in both agents.proposals.artifactKinds and readback'] };
100
+ return { facet, renderPath };
101
+ }
102
+
103
+ interface Lifecycle {
104
+ readonly blockedAt?: string;
105
+ readonly t: Rec; readonly t2: Rec; readonly templateId: string;
106
+ readonly inert?: { get: Exchange; list: Exchange; render: Exchange };
107
+ readonly revise?: Exchange;
108
+ readonly stale?: { apply: Exchange; after: Exchange; prompt: Exchange };
109
+ readonly unscoped?: { apply: Exchange; after: Exchange; prompt: Exchange; www: string | null } | string;
110
+ readonly foreignProposal?: Array<{ what: string; foreign: Exchange; unknown: Exchange }> | string;
111
+ readonly apply?: Exchange;
112
+ readonly readback?: Exchange;
113
+ readonly foreignTemplate?: { what: string; foreign: Exchange; unknown: Exchange } | string;
114
+ readonly terminal?: { revise: Exchange; apply: Exchange };
115
+ }
116
+
117
+ let run: Promise<Lifecycle> | undefined;
118
+ /** One lifecycle per worker: every host leg judges a slice of the same records. */
119
+ function lifecycle(renderPath: string): Promise<Lifecycle> {
120
+ run ??= drive(renderPath);
121
+ return run;
122
+ }
123
+
124
+ async function drive(renderPath: string): Promise<Lifecycle> {
125
+ const nonce = randomBytes(6).toString('hex');
126
+ const t = freshTemplate(nonce, 'You summarize the input in one sentence.');
127
+ const t2 = { ...t, text: 'You summarize the input in exactly one short sentence.' };
128
+ const templateId = t['templateId'] as string;
129
+ const prompt = `/prompts/${encodeURIComponent(templateId)}`;
130
+ const base = { t, t2, templateId };
131
+
132
+ const submit = (await ask('POST', '/proposals', { kind: 'prompt-template', artifact: t, title: 'conformance proposal' })).x;
133
+ const pid = isRec(submit?.json) && typeof submit.json['id'] === 'string' ? submit.json['id'] : undefined;
134
+ if (submit?.status !== 201 || pid === undefined) return { ...base, blockedAt: `submitProposal answered ${submit?.status ?? 'nothing'}; no proposal to drive` };
135
+ const path = `/proposals/${encodeURIComponent(pid)}`;
136
+ const getP = async (bearer?: string): Promise<Exchange> => (await ask('GET', path, undefined, bearer)).x;
137
+
138
+ const inert = {
139
+ get: (await ask('GET', prompt)).x,
140
+ list: (await ask('GET', '/prompts')).x,
141
+ render: (await ask('POST', renderPath, { ref: `prompt:${templateId}@1.0.0`, variables: {} })).x,
142
+ };
143
+ const revise = (await ask('PATCH', path, { artifact: t2 })).x;
144
+ if (revise?.status !== 200) return { ...base, inert, revise, blockedAt: `reviseProposal answered ${revise?.status ?? 'nothing'}` };
145
+
146
+ const staleApply = (await ask('POST', `${path}:apply`, { artifactDigest: artifactDigest(t) })).x;
147
+ const stale = { apply: staleApply, after: await getP(), prompt: (await ask('GET', prompt)).x };
148
+
149
+ const low = process.env['OPENWOP_TEST_LOW_SCOPE_KEY']?.trim();
150
+ let unscoped: Lifecycle['unscoped'];
151
+ if (!low) unscoped = 'OPENWOP_TEST_LOW_SCOPE_KEY (a key with proposals:read and proposals:write but not proposals:apply) is not set';
152
+ else {
153
+ const a = await ask('POST', `${path}:apply`, { artifactDigest: artifactDigest(t2) }, low);
154
+ unscoped = { apply: a.x, www: a.wwwAuthenticate, after: await getP(), prompt: (await ask('GET', prompt)).x };
155
+ }
156
+
157
+ const tenantB = process.env['OPENWOP_TEST_TENANT_B_API_KEY']?.trim();
158
+ const unknownPid = `/proposals/conformance-unknown-${nonce}`;
159
+ let foreignProposal: Lifecycle['foreignProposal'];
160
+ if (!tenantB) foreignProposal = 'OPENWOP_TEST_TENANT_B_API_KEY (a credential bound to a second tenant) is not set';
161
+ else if (tenantB === process.env['OPENWOP_API_KEY']) foreignProposal = 'OPENWOP_TEST_TENANT_B_API_KEY equals OPENWOP_API_KEY — the leg needs a second tenant';
162
+ else {
163
+ foreignProposal = [
164
+ { what: 'getProposal', foreign: await getP(tenantB), unknown: (await ask('GET', unknownPid, undefined, tenantB)).x },
165
+ { what: 'applyProposal', foreign: (await ask('POST', `${path}:apply`, { artifactDigest: artifactDigest(t2) }, tenantB)).x,
166
+ unknown: (await ask('POST', `${unknownPid}:apply`, { artifactDigest: artifactDigest(t2) }, tenantB)).x },
167
+ ];
168
+ }
169
+
170
+ const apply = (await ask('POST', `${path}:apply`, { artifactDigest: artifactDigest(t2) })).x;
171
+ const readback = (await ask('GET', prompt)).x;
172
+ const foreignTemplate = typeof foreignProposal === 'string' ? foreignProposal : {
173
+ what: 'getPromptTemplate for the installed template',
174
+ foreign: (await ask('GET', prompt, undefined, tenantB)).x,
175
+ unknown: (await ask('GET', `/prompts/conformance.proposal.unknown-${nonce}`, undefined, tenantB)).x,
176
+ };
177
+ const terminal = { revise: (await ask('PATCH', path, { title: 'after apply' })).x, apply: (await ask('POST', `${path}:apply`, { artifactDigest: artifactDigest(t2) })).x };
178
+ return { ...base, inert, revise, stale, unscoped, foreignProposal, apply, readback, foreignTemplate, terminal };
179
+ }
180
+
181
+ function expectAll(id: string, findings: readonly ProposalFinding[]): void {
182
+ for (const x of findings) expect(x.ok, req(id, DOC, x.message)).toBe(true);
183
+ }
184
+
185
+ describe('v2 RFC 0249 — proposal wire shape (server-free)', () => {
186
+ it('the submit, revise and apply bodies and installedArtifact validate, and the negatives are refused', () => {
187
+ const submit = v2Validator('proposal-submit');
188
+ const revise = v2Validator('proposal-revise');
189
+ const apply = v2Validator('proposal-apply');
190
+ const installed = v2RefValidator('proposal.schema.json#/properties/activation/properties/installedArtifact');
191
+ const S = 'proposal-submit / proposal-revise / proposal-apply / proposal.schema.json (RFC 0249)';
192
+ expect(submit({ kind: 'prompt-template', artifact: { templateId: 'x' } }).ok, req(ID_SHAPE, S, 'a minimal submit MUST validate')).toBe(true);
193
+ expect(submit({ kind: 'skill', artifact: {} }).ok, req(ID_SHAPE, S, 'an unknown kind MUST be refused')).toBe(false);
194
+ expect(submit({ kind: 'prompt-template', artifact: {}, state: 'applied' }).ok, req(ID_SHAPE, S, 'a submit carrying state MUST be refused (closed body)')).toBe(false);
195
+ expect(revise({}).ok, req(ID_SHAPE, S, 'an empty revise MUST be refused')).toBe(false);
196
+ expect(apply({ artifactDigest: `sha256:${'a'.repeat(64)}` }).ok, req(ID_SHAPE, S, 'a sha256 digest MUST validate')).toBe(true);
197
+ expect(apply({ artifactDigest: 'abc123' }).ok, req(ID_SHAPE, S, 'a digest without the sha256: prefix and 64 hex MUST be refused')).toBe(false);
198
+ expect(installed({ kind: 'prompt-template', operationId: 'getPromptTemplate', id: 't' }).ok, req(ID_SHAPE, S, 'a minimal installedArtifact MUST validate')).toBe(true);
199
+ expect(installed({ kind: 'prompt-template', operationId: 'getPromptTemplate', id: 't', url: 'https://x' }).ok, req(ID_SHAPE, S, 'installedArtifact is closed')).toBe(false);
200
+ });
201
+ });
202
+
203
+ describe('v2 RFC 0249 — proposal lifecycle (host)', () => {
204
+ it('a kind outside artifactKinds is refused at submit', async () => {
205
+ const g = await gate();
206
+ if ('skip' in g) return softSkip(...g.skip);
207
+ const kinds = Array.isArray(g.facet['artifactKinds']) ? g.facet['artifactKinds'] : [];
208
+ const missing = KINDS.find((k) => !kinds.includes(k));
209
+ if (missing === undefined) return softSkip('inapplicable', 'the host advertises every proposal kind, so no kind is outside artifactKinds');
210
+ expectAll(ID_SUBMIT_KIND_REFUSED, judgeSubmitKindRefused((await ask('POST', '/proposals', { kind: missing, artifact: {} })).x));
211
+ });
212
+
213
+ it('a draft prompt-template is not served, listed or renderable', async () => {
214
+ const g = await gate();
215
+ if ('skip' in g) return softSkip(...g.skip);
216
+ const l = await lifecycle(g.renderPath);
217
+ if (l.inert === undefined) return softSkip('blocked', l.blockedAt ?? 'the lifecycle did not reach the draft');
218
+ expectAll(ID_DRAFT_INERT, judgeInert(l.inert.get, l.inert.list, l.inert.render, l.templateId, 'draft'));
219
+ });
220
+
221
+ it('a revision re-digests, and an apply bound to the old bytes is stale', async () => {
222
+ const g = await gate();
223
+ if ('skip' in g) return softSkip(...g.skip);
224
+ const l = await lifecycle(g.renderPath);
225
+ if (l.stale === undefined || l.revise === undefined) return softSkip('blocked', l.blockedAt ?? 'the lifecycle did not reach the revision');
226
+ expectAll(ID_APPLY_STALE_REFUSED, [...judgeRevised(l.revise, l.t2), ...judgeApplyStale(l.stale.apply, l.stale.after, l.stale.prompt)]);
227
+ });
228
+
229
+ it('an apply without proposals:apply is refused and installs nothing', async () => {
230
+ const g = await gate();
231
+ if ('skip' in g) return softSkip(...g.skip);
232
+ if (g.facet['activation'] !== 'direct-rbac') return softSkip('inapplicable', 'agents.proposals.activation is not direct-rbac — the apply is gated by an approval run');
233
+ const l = await lifecycle(g.renderPath);
234
+ if (l.unscoped === undefined) return softSkip('blocked', l.blockedAt ?? 'the lifecycle did not reach the apply');
235
+ if (typeof l.unscoped === 'string') return softSkip('blocked', l.unscoped);
236
+ expectAll(ID_APPLY_UNSCOPED_REFUSED, judgeApplyUnscoped(l.unscoped.apply, l.unscoped.after, l.unscoped.prompt, l.unscoped.www));
237
+ });
238
+
239
+ it('an authorized, current apply names the read that serves the installed template', async () => {
240
+ const g = await gate();
241
+ if ('skip' in g) return softSkip(...g.skip);
242
+ if (g.facet['activation'] !== 'direct-rbac') return softSkip('inapplicable', 'agents.proposals.activation is not direct-rbac — the apply is gated by an approval run');
243
+ const l = await lifecycle(g.renderPath);
244
+ if (l.apply === undefined) return softSkip('blocked', l.blockedAt ?? 'the lifecycle did not reach the apply');
245
+ expectAll(ID_APPLY_INSTALLS, judgeApplied(l.apply, l.templateId));
246
+ });
247
+
248
+ it('the installed template is the reviewed revision, byte for byte', async () => {
249
+ const g = await gate();
250
+ if ('skip' in g) return softSkip(...g.skip);
251
+ if (g.facet['activation'] !== 'direct-rbac') return softSkip('inapplicable', 'agents.proposals.activation is not direct-rbac — the apply is gated by an approval run');
252
+ const l = await lifecycle(g.renderPath);
253
+ if (l.readback === undefined) return softSkip('blocked', l.blockedAt ?? 'the lifecycle did not reach the read-back');
254
+ expectAll(ID_READBACK_BYTE_MATCH, judgeReadback(l.readback, l.t2, l.t));
255
+ });
256
+
257
+ it('an applied proposal is terminal', async () => {
258
+ const g = await gate();
259
+ if ('skip' in g) return softSkip(...g.skip);
260
+ if (g.facet['activation'] !== 'direct-rbac') return softSkip('inapplicable', 'agents.proposals.activation is not direct-rbac — the apply is gated by an approval run');
261
+ const l = await lifecycle(g.renderPath);
262
+ if (l.terminal === undefined) return softSkip('blocked', l.blockedAt ?? 'the lifecycle did not reach the applied state');
263
+ expectAll(ID_TERMINAL_REFUSED, judgeTerminal(l.terminal.revise, l.terminal.apply));
264
+ });
265
+
266
+ it('another tenant sees the proposal and the installed template as unknown ids', async () => {
267
+ const g = await gate();
268
+ if ('skip' in g) return softSkip(...g.skip);
269
+ const l = await lifecycle(g.renderPath);
270
+ if (l.foreignProposal === undefined) return softSkip('blocked', l.blockedAt ?? 'the lifecycle did not reach the foreign reads');
271
+ if (typeof l.foreignProposal === 'string') return softSkip('blocked', l.foreignProposal);
272
+ const pairs = [...l.foreignProposal, ...(l.foreignTemplate !== undefined && typeof l.foreignTemplate !== 'string' ? [l.foreignTemplate] : [])];
273
+ expectAll(ID_CROSS_TENANT_NOT_FOUND, judgeForeign(pairs));
274
+ });
275
+ });
@@ -14,6 +14,8 @@
14
14
  * `404 not_found`.
15
15
  * content (hosts that do not advertise `content`) `GET /content/settings`
16
16
  * answers `404 not_found`.
17
+ * proposals (hosts that do not advertise `agents.proposals`) `GET /proposals`
18
+ * answers `404 not_found` (RFC 0249).
17
19
  *
18
20
  * A host that advertises the facet records the leg `inapplicable`.
19
21
  */
@@ -61,4 +63,15 @@ describe('v2 unadvertised operations answer 404 not_found (RFC 0228 §H)', () =>
61
63
  expect(res.status, req(ID, DOC, `an operation gated on content, which the host does not advertise, MUST answer 404 (got ${res.status}; v1's 501 is not a v2 answer)`)).toBe(404);
62
64
  expect(readErrorCode(res.json), req(ID, DOC, 'the 404 for an unadvertised gated operation MUST carry the registered code not_found')).toBe('not_found');
63
65
  });
66
+ it('GET /proposals answers 404 not_found when agents.proposals is not advertised (RFC 0249)', async () => {
67
+ const doc = await discovery();
68
+ if (!doc) return softSkip('blocked', UNREACHABLE);
69
+ const agents = record(doc, 'agents');
70
+ if (agents !== null && record(agents, 'proposals') !== null) return softSkip('inapplicable', 'the host advertises agents.proposals — the /proposals surface is served');
71
+ const res = await get('/proposals');
72
+ if (res === null) return softSkip('blocked', 'GET /proposals unreachable (fetch failed)');
73
+ const ID = 'openwop.requirement.0249.unadvertised-not-found';
74
+ expect(res.status, req(ID, DOC, `an operation gated on agents.proposals, which the host does not advertise, MUST answer 404 (got ${res.status})`)).toBe(404);
75
+ expect(readErrorCode(res.json), req(ID, DOC, 'the 404 for an unadvertised gated operation MUST carry the registered code not_found')).toBe('not_found');
76
+ });
64
77
  });