@openwop/openwop-conformance 2.46.38 → 2.46.40

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,13 @@
1
+ ## [2.46.40] — 2026-11-01 — No suite change
2
+
3
+ - **Version only.** Ships with corpus 2.46.40, which closes two gap-register rows; no scenario, fixture or library changed.
4
+
5
+ ## [2.46.39] — 2026-10-31 — Read back an installed pack (RFC 0253)
6
+
7
+ - **New scenario `v2-proposal-pack-readback` (RFC 0253).** A server-free leg (`0253.shape`) and eleven host legs gated per pack kind on `agents.proposals` with `clientDrafts` and the kind in `artifactKinds` and `readback`: inertness, apply, byte-match, §D refusals, install conflict, cross-tenant reads.
8
+ - **Judges** in `lib/proposal-pack-readback-witness.ts` with eight self-tests. A §D refusal at submit counts when nothing installs. Before merge every host leg failed its targeted sabotage on a fake host and passed a conforming one.
9
+ - **`v2-unadvertised-operation-not-found`** gains `0253.unadvertised-not-found`: `GET /installed-packs/{packName}` is `404 not_found` when `readback` lists no pack kind.
10
+
1
11
  ## [2.46.38] — 2026-10-31 — Last AI Envelope names out of v2 descriptions
2
12
 
3
13
  - **No suite change.** The cycle carries description-only edits to six v2 schemas and facets, which change the packed `@openwop/spec-artifacts`.
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 664 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 224 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
140
+ The current suite has 665 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 225 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: 664 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: 665 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.38",
4
- "stampSha256": "7c1c63b2ee12b7a0437c970de613d04e1e003fb4d495641256d8a6cdc1782211"
3
+ "version": "2.46.40",
4
+ "stampSha256": "681c2972a462f2bb843619c0774e2dd6e26d2c957534138c5ca69f9b2af8b3ca"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.46.38",
3
+ "version": "2.46.40",
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.38"
59
+ "@openwop/spec-artifacts": "2.46.40"
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": 2855,
7
- "withStableId": 2855,
5
+ "files": 748,
6
+ "tests": 2868,
7
+ "withStableId": 2868,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2725
9
+ "explicitIds": 2738
10
10
  },
11
11
  "records": [
12
12
  {
@@ -36063,6 +36063,128 @@
36063
36063
  "explicitId": "openwop.requirement.0249.cross-tenant-not-found",
36064
36064
  "citations": []
36065
36065
  },
36066
+ {
36067
+ "id": "openwop.it.v2-proposal-pack-readback.installedartifact-names-getinstalledpack-with-a-version-the-fresh-artifacts-are",
36068
+ "file": "v2-proposal-pack-readback.test.ts",
36069
+ "line": 209,
36070
+ "title": "installedArtifact names getInstalledPack with a version; the fresh artifacts are valid and admissible; the negatives break §D",
36071
+ "explicitId": "openwop.requirement.0253.shape",
36072
+ "citations": [
36073
+ {
36074
+ "section": "proposal.schema.json installedArtifact, the two manifest schemas, agents.schema.json §proposals.readback (RFC 0253 §B–§D)",
36075
+ "requirement": "an installedArtifact naming getInstalledPack with a version MUST validate"
36076
+ },
36077
+ {
36078
+ "section": "proposal.schema.json installedArtifact, the two manifest schemas, agents.schema.json §proposals.readback (RFC 0253 §B–§D)",
36079
+ "requirement": "the fresh chain pack MUST validate against workflow-chain-pack-manifest"
36080
+ },
36081
+ {
36082
+ "section": "proposal.schema.json installedArtifact, the two manifest schemas, agents.schema.json §proposals.readback (RFC 0253 §B–§D)",
36083
+ "requirement": "the fresh agent pack MUST validate against node-pack-manifest"
36084
+ },
36085
+ {
36086
+ "section": "proposal.schema.json installedArtifact, the two manifest schemas, agents.schema.json §proposals.readback (RFC 0253 §B–§D)",
36087
+ "requirement": "the fresh chain pack MUST satisfy §D"
36088
+ },
36089
+ {
36090
+ "section": "proposal.schema.json installedArtifact, the two manifest schemas, agents.schema.json §proposals.readback (RFC 0253 §B–§D)",
36091
+ "requirement": "the fresh agent pack MUST satisfy §D"
36092
+ },
36093
+ {
36094
+ "section": "proposal.schema.json installedArtifact, the two manifest schemas, agents.schema.json §proposals.readback (RFC 0253 §B–§D)",
36095
+ "requirement": null,
36096
+ "interpolated": true
36097
+ }
36098
+ ]
36099
+ },
36100
+ {
36101
+ "id": "openwop.it.v2-proposal-pack-readback.an-unapplied-workflow-chain-pack-is-not-served",
36102
+ "file": "v2-proposal-pack-readback.test.ts",
36103
+ "line": 235,
36104
+ "title": "an unapplied workflow-chain-pack is not served",
36105
+ "explicitId": "openwop.requirement.0253.chain-draft-inert",
36106
+ "citations": []
36107
+ },
36108
+ {
36109
+ "id": "openwop.it.v2-proposal-pack-readback.an-authorized-apply-of-a-workflow-chain-pack-names-getinstalledpack-the-name-and",
36110
+ "file": "v2-proposal-pack-readback.test.ts",
36111
+ "line": 241,
36112
+ "title": "an authorized apply of a workflow-chain-pack names getInstalledPack, the name and the version",
36113
+ "explicitId": "openwop.requirement.0253.chain-apply-installs",
36114
+ "citations": []
36115
+ },
36116
+ {
36117
+ "id": "openwop.it.v2-proposal-pack-readback.the-installed-workflow-chain-pack-is-the-reviewed-revision-byte-for-byte",
36118
+ "file": "v2-proposal-pack-readback.test.ts",
36119
+ "line": 247,
36120
+ "title": "the installed workflow-chain-pack is the reviewed revision, byte for byte",
36121
+ "explicitId": "openwop.requirement.0253.chain-readback-byte-match",
36122
+ "citations": []
36123
+ },
36124
+ {
36125
+ "id": "openwop.it.v2-proposal-pack-readback.an-unapplied-agent-pack-is-not-served-and-its-agent-is-not-resolvable-or-listed",
36126
+ "file": "v2-proposal-pack-readback.test.ts",
36127
+ "line": 255,
36128
+ "title": "an unapplied agent-pack is not served, and its agent is not resolvable or listed",
36129
+ "explicitId": "openwop.requirement.0253.agent-draft-inert",
36130
+ "citations": []
36131
+ },
36132
+ {
36133
+ "id": "openwop.it.v2-proposal-pack-readback.an-authorized-apply-of-an-agent-pack-names-getinstalledpack-the-name-and-the-ver",
36134
+ "file": "v2-proposal-pack-readback.test.ts",
36135
+ "line": 261,
36136
+ "title": "an authorized apply of an agent-pack names getInstalledPack, the name and the version, and resolves the agent",
36137
+ "explicitId": "openwop.requirement.0253.agent-apply-installs",
36138
+ "citations": []
36139
+ },
36140
+ {
36141
+ "id": "openwop.it.v2-proposal-pack-readback.the-installed-agent-pack-is-the-reviewed-revision-byte-for-byte",
36142
+ "file": "v2-proposal-pack-readback.test.ts",
36143
+ "line": 267,
36144
+ "title": "the installed agent-pack is the reviewed revision, byte for byte",
36145
+ "explicitId": "openwop.requirement.0253.agent-readback-byte-match",
36146
+ "citations": []
36147
+ },
36148
+ {
36149
+ "id": "openwop.it.v2-proposal-pack-readback.an-agent-pack-with-a-file-reference-or-with-nodes-is-refused-and-installs-nothin",
36150
+ "file": "v2-proposal-pack-readback.test.ts",
36151
+ "line": 275,
36152
+ "title": "an agent pack with a file reference, or with nodes, is refused and installs nothing",
36153
+ "explicitId": "openwop.requirement.0253.agent-file-ref-refused",
36154
+ "citations": []
36155
+ },
36156
+ {
36157
+ "id": "openwop.it.v2-proposal-pack-readback.a-pack-named-outside-private-is-refused-and-installs-nothing",
36158
+ "file": "v2-proposal-pack-readback.test.ts",
36159
+ "line": 294,
36160
+ "title": "a pack named outside private.* is refused and installs nothing",
36161
+ "explicitId": "openwop.requirement.0253.namespace-refused",
36162
+ "citations": []
36163
+ },
36164
+ {
36165
+ "id": "openwop.it.v2-proposal-pack-readback.a-pack-carrying-signing-is-refused-and-installs-nothing",
36166
+ "file": "v2-proposal-pack-readback.test.ts",
36167
+ "line": 304,
36168
+ "title": "a pack carrying signing is refused and installs nothing",
36169
+ "explicitId": "openwop.requirement.0253.signing-refused",
36170
+ "citations": []
36171
+ },
36172
+ {
36173
+ "id": "openwop.it.v2-proposal-pack-readback.a-second-apply-at-the-installed-name-and-version-is-an-install-conflict",
36174
+ "file": "v2-proposal-pack-readback.test.ts",
36175
+ "line": 314,
36176
+ "title": "a second apply at the installed name and version is an install conflict",
36177
+ "explicitId": "openwop.requirement.0253.install-conflict",
36178
+ "citations": []
36179
+ },
36180
+ {
36181
+ "id": "openwop.it.v2-proposal-pack-readback.another-tenant-sees-the-installed-pack-and-agent-as-unknown-names",
36182
+ "file": "v2-proposal-pack-readback.test.ts",
36183
+ "line": 323,
36184
+ "title": "another tenant sees the installed pack and agent as unknown names",
36185
+ "explicitId": "openwop.requirement.0253.cross-tenant-not-found",
36186
+ "citations": []
36187
+ },
36066
36188
  {
36067
36189
  "id": "openwop.it.v2-protected-resource-metadata.serves-rfc-9728-metadata-unauthenticated-at-the-uri-derived-from-its-resource-id",
36068
36190
  "file": "v2-protected-resource-metadata.test.ts",
@@ -38674,7 +38796,7 @@
38674
38796
  {
38675
38797
  "id": "openwop.it.v2-unadvertised-operation-not-found.get-prompts-answers-404-not-found-when-prompts-endpointssupported-is-not-adverti",
38676
38798
  "file": "v2-unadvertised-operation-not-found.test.ts",
38677
- "line": 45,
38799
+ "line": 47,
38678
38800
  "title": "GET /prompts answers 404 not_found when prompts.endpointsSupported is not advertised",
38679
38801
  "explicitId": "openwop.requirement.0228.unadvertised-prompts-not-found",
38680
38802
  "citations": [
@@ -38692,7 +38814,7 @@
38692
38814
  {
38693
38815
  "id": "openwop.it.v2-unadvertised-operation-not-found.get-content-settings-answers-404-not-found-when-content-is-not-advertised",
38694
38816
  "file": "v2-unadvertised-operation-not-found.test.ts",
38695
- "line": 56,
38817
+ "line": 58,
38696
38818
  "title": "GET /content/settings answers 404 not_found when content is not advertised",
38697
38819
  "explicitId": "openwop.requirement.0228.unadvertised-content-not-found",
38698
38820
  "citations": [
@@ -38710,7 +38832,7 @@
38710
38832
  {
38711
38833
  "id": "openwop.it.v2-unadvertised-operation-not-found.get-proposals-answers-404-not-found-when-agents-proposals-is-not-advertised-rfc",
38712
38834
  "file": "v2-unadvertised-operation-not-found.test.ts",
38713
- "line": 66,
38835
+ "line": 68,
38714
38836
  "title": "GET /proposals answers 404 not_found when agents.proposals is not advertised (RFC 0249)",
38715
38837
  "explicitId": "openwop.requirement.0249.unadvertised-not-found",
38716
38838
  "citations": [
@@ -38725,6 +38847,24 @@
38725
38847
  }
38726
38848
  ]
38727
38849
  },
38850
+ {
38851
+ "id": "openwop.it.v2-unadvertised-operation-not-found.get-installed-packs-packname-answers-404-not-found-when-readback-lists-no-pack-k",
38852
+ "file": "v2-unadvertised-operation-not-found.test.ts",
38853
+ "line": 80,
38854
+ "title": "GET /installed-packs/{packName} answers 404 not_found when readback lists no pack kind (RFC 0253)",
38855
+ "explicitId": "openwop.requirement.0253.unadvertised-not-found",
38856
+ "citations": [
38857
+ {
38858
+ "section": "spec/v2/core/errors.md §Unadvertised operations",
38859
+ "requirement": null,
38860
+ "interpolated": true
38861
+ },
38862
+ {
38863
+ "section": "spec/v2/core/errors.md §Unadvertised operations",
38864
+ "requirement": "the 404 for an unadvertised gated operation MUST carry the registered code not_found"
38865
+ }
38866
+ ]
38867
+ },
38728
38868
  {
38729
38869
  "id": "openwop.it.v2-unmapped-type-refused.poll-over-a-log-with-an-unmapped-unprefixed-type-fails-with-500-event-type-unmap",
38730
38870
  "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": 664,
4
+ "files": 665,
5
5
  "v1": 455,
6
- "v2": 224
6
+ "v2": 225
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1695,6 +1695,9 @@
1695
1695
  "v2-proposal-lifecycle.test.ts": [
1696
1696
  2
1697
1697
  ],
1698
+ "v2-proposal-pack-readback.test.ts": [
1699
+ 2
1700
+ ],
1698
1701
  "v2-protected-resource-metadata.test.ts": [
1699
1702
  2
1700
1703
  ],
@@ -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.38",
5
- "corpusTag": "v2.46.38",
4
+ "version": "2.46.40",
5
+ "corpusTag": "v2.46.40",
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": "27a3edfe025994989a2296f3d5d4a0c52d8e1ce924a8f450a0dddc5ad736d8ff",
13
- "api/v2/asyncapi.yaml": "9f06497fcd4d96f96aacb59d43e88f6f1126c5f52846c9d3d0704bce458de6ae",
14
- "api/v2/openapi.yaml": "46bbe19ff9bc87b9c3155dc6fd49a9af6bb8a154edf40df9edb6aeaf26f7b770",
12
+ "api/seams-v2.yaml": "07047393b4d55b90cfca9d2ac70c2f57bd5ae82b08eb0180e0dcb072aa8e76fd",
13
+ "api/v2/asyncapi.yaml": "f7823f811bc889b2f9c1db6a63c2ffbd9a5291456e527b1f61fdcc856c38088a",
14
+ "api/v2/openapi.yaml": "9b70b5b52fb57fc074552c741c15a8d7a81c979e9c2ef82bc80cf6a393be4eb4",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -115,7 +115,7 @@
115
115
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
116
116
  "schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
117
117
  "schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
118
- "schemas/v2/capabilities.schema.json": "4ec06ca94cc9314aa2331916738c12f329bd28c7efb7d3ddfde75b2055bbb05c",
118
+ "schemas/v2/capabilities.schema.json": "93cd1f6f4ffbd98ec5439e4d014233b1ed5e96a0e6d1a166cc2fa4d85ae028fc",
119
119
  "schemas/v2/certification-bundle.schema.json": "29d2ff33d8256d8729cd4914e2ecff5ff07afc310264e2db483a9c0ae13df2a6",
120
120
  "schemas/v2/channel-presence-payload.schema.json": "fc03df0c20cc12a25f7ee6a47503a4c5931d79fe3d5ff24815e8fd7c1bcb9a87",
121
121
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
@@ -164,7 +164,7 @@
164
164
  "schemas/v2/proposal-list-response.schema.json": "1b5e78fc1e4256dbe6bc866487879deb8f7b9cc18f5bc875db3fde46d79bea02",
165
165
  "schemas/v2/proposal-revise.schema.json": "065e75cccd01fcc619e63c25f2a1775adcc24b0467689dca42462ace6bfa1146",
166
166
  "schemas/v2/proposal-submit.schema.json": "00fb91a4a76ebcee530295f5262b8b369ed024055cd29a47aa520db03a7fc6c9",
167
- "schemas/v2/proposal.schema.json": "52a1e54cfce966a07e2588d5e323a4fa896470ccd3a3a21d2ed19d977d2922b7",
167
+ "schemas/v2/proposal.schema.json": "b820928a6a2acb09a6d5044acd6b217268e23bbd045ee0b7f2d6c1184a3b2dc5",
168
168
  "schemas/v2/registry-version-manifest.schema.json": "940cfd82511690cd6f2539e5676b185e7812628d0707c283fd7ffb33b6e54afd",
169
169
  "schemas/v2/residency.schema.json": "3fd23c9732beb140504e81560fc426eb6764d547e56da5fb92c5a5c7bf04db3a",
170
170
  "schemas/v2/run-ancestry-response.schema.json": "334c17f2062dea62726ca1d07b8be3f169cdc422af843c1def5fb317039814c9",
@@ -203,14 +203,14 @@
203
203
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
204
204
  "spec/v1/alias-detectors.json": "202a443678e592a515e33aad7f6ab679a07d8408809f2d4b0f59d156c84ffc65",
205
205
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
206
- "spec/v1/core-standard-manifest.json": "3fcea0739a3d914b30dcbc2f001c25d3fee12aea5402369522bad7bfc5c8b691",
206
+ "spec/v1/core-standard-manifest.json": "9024c5b7ba78638ccca217619fd2a1a1a1034e8aacea5a78a081e2f58834a877",
207
207
  "spec/v1/deprecations.json": "901fdab0a7212627f1838cbb06ae7b7204f0dff528299e56d3d938deecea27c1",
208
208
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
209
209
  "spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
211
211
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
212
212
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
213
- "spec/v1/gaps.json": "b984976759acb1b8d329a49acfa2425938d7d86cd1433f43c48ab22a6c51e9ca",
213
+ "spec/v1/gaps.json": "142a92c571e6e4762a910889fcd31a249556be69bc54dc6ae75949b1987a2f5a",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
215
  "spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
216
216
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -226,7 +226,7 @@
226
226
  "spec/v2/core/events.md": "5834308616f2e722e217db776edea37338c283196b6d2a7c241256575ccca825",
227
227
  "spec/v2/core/execution.md": "d6e1ab7336a88b7a598a192434f584e6787e60aeeb56fcdbc32fa548cde3086a",
228
228
  "spec/v2/core/form-content-packs.md": "d56022c207c8406fed4b2e9501bd14e70a4300e371c0cfc371787899ae0bd997",
229
- "spec/v2/core/headers.md": "c86c56868db64724ca8e7afd34bff33740deb2612bf77fa09d412a1d6c65f3c6",
229
+ "spec/v2/core/headers.md": "71f5024adfa754789cf751001308baa87decff6a3afd514c75d573674cbf50ee",
230
230
  "spec/v2/core/host-services.md": "68fbe359ec9efc453105a225998b8d0dada07ae61bfd05e324dd8649af31261d",
231
231
  "spec/v2/core/i18n.md": "19aea08290a7604f36fa8b416ad270cf84e193cc8fa39d85e52015e312a95565",
232
232
  "spec/v2/core/idempotency.md": "adbd317a6ebb57c2759c076297b31f67def9e391576bccd0bbea787b13e85872",
@@ -275,7 +275,7 @@
275
275
  "spec/v2/ext/sandbox-runtime-notes/README.md": "0d1290fb1e20b2e35c0b15051541ab6546e4e2a6f693b4f912cb3f7d6a65e92c",
276
276
  "spec/v2/ext/webResearch/README.md": "2771686719161b811c2a50361815a334c58f09e442528fb8e23e4fcdcd42d630",
277
277
  "spec/v2/facets/a2a.schema.json": "b68bf5e29e455e46c2ad7cfcccf5963730ec2310ff9db81884550d8df30fd6ef",
278
- "spec/v2/facets/agents.schema.json": "cc9abae19f500b8a2c6f2b3494284f6f9bb782cde7eec13e4f2f5363a1b44ad5",
278
+ "spec/v2/facets/agents.schema.json": "2bbd9188c7cecb8cc63d1584609bb1627a4ee5d55ea1a4e4e3b0d1ff8ccfcb11",
279
279
  "spec/v2/facets/aiProviders.schema.json": "cb2638174c233835203408f78a18da72a9e7aaa240a29220fc709dd43c6930c4",
280
280
  "spec/v2/facets/auditLogIntegrity.schema.json": "9a7f81320c8f940c6f4129bc7dd895a4063231c5458a717a08c2cbf9bb886c46",
281
281
  "spec/v2/facets/auth.schema.json": "7d98bc81a0f7d307460d039eebfebd21388091bfe1d538762583940f94c0bd2a",
@@ -302,12 +302,12 @@
302
302
  "spec/v2/interop-map.schema.json": "0017c882df34ee215e2e0729de051490e09d43aac3d2c949285698ed51832f12",
303
303
  "spec/v2/migrations.json": "651954938a1e154c0b6f1e3dd6e105889d30de3bb65789f21de7f77a12869e9e",
304
304
  "spec/v2/migrations.schema.json": "b91ade6320ad1b1185c9495b9424f3aa63438ed8e412c45171c227d82c0afd86",
305
- "spec/v2/path-manifest.json": "20f598f56df460ad3ed50aba3d6bc2933ea16a3c96ce125da1c5dd6989d4155c",
305
+ "spec/v2/path-manifest.json": "3720e502614f301efc7556e0bfcbfa7b7ad8a37c579256ba6bca82417a2728f1",
306
306
  "spec/v2/peer-dependency-aliases.json": "cbf62f551f9295b0ac7ba6cc0466abb153563354a57b0622f3ec6e6689cba47e",
307
307
  "spec/v2/profiles.json": "b382b261c7844798575f7c1b37c934f0da19846016a2337d883d4f5ef6e05479",
308
- "spec/v2/release.json": "7e083fce19c55779cca6f89f30a511127519b4da544c488b83075ba23fc58e18",
308
+ "spec/v2/release.json": "1ed31f6349b7b4cf948b9826f9541edaf78c9508dcd76438410b2b99204aa5e5",
309
309
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
310
- "spec/v2/surface-baseline.json": "bf4bfd1310b78e3307534a0867f7601c39fc988d1659979753188f35fd627f62"
310
+ "spec/v2/surface-baseline.json": "c7ae2c8adc508e30b969bd28786b2d3576663165cc5def0abacf816cb432e4b5"
311
311
  },
312
- "corpusCommit": "8dd71a2ff82f90e1369b8aac9132d1bd1b9d27c6"
312
+ "corpusCommit": "e9e319e699ca8b6cb0cd4a299bd329ea90f151bb"
313
313
  }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * RFC 0253 — pure judges for `v2-proposal-pack-readback.test.ts`. The scenario
3
+ * drives a client-submitted `workflow-chain-pack` and `agent-pack` proposal
4
+ * through submit → revise → apply and records every response; these functions
5
+ * decide each leg from those records, so the self-tests can fail every wrong
6
+ * host answer without a host.
7
+ *
8
+ * The rules are the `getInstalledPack` operation in `api/v2/openapi.yaml`, the
9
+ * `agents.proposals.readback` description in `spec/v2/facets/agents.schema.json`
10
+ * and `activation.installedArtifact` in `proposal.schema.json`. For the two pack
11
+ * kinds no field is host-owned, so the byte-match is plain JCS equality.
12
+ *
13
+ * @see RFCS/0253-installed-pack-readback.md
14
+ */
15
+
16
+ import { canonicalJSON } from './jcs.js';
17
+ import { readErrorCode } from './error-envelope.js';
18
+ import type { Exchange, ProposalFinding } from './proposal-lifecycle-witness.js';
19
+
20
+ export type PackKind = 'workflow-chain-pack' | 'agent-pack';
21
+
22
+ type Rec = Record<string, unknown>;
23
+ const isRec = (v: unknown): v is Rec => typeof v === 'object' && v !== null && !Array.isArray(v);
24
+ const f = (ok: boolean, message: string): ProposalFinding => ({ ok, message });
25
+ const code = (x: Exchange): string | undefined => readErrorCode(x?.json);
26
+ const details = (x: Exchange): Rec => {
27
+ const body = isRec(x?.json) ? x.json : {};
28
+ if (isRec(body['details'])) return body['details'];
29
+ const nested = body['error'];
30
+ return isRec(nested) && isRec(nested['details']) ? nested['details'] : {};
31
+ };
32
+ const shown = (x: Exchange): string => (x === null ? 'no answer' : `${x.status}${code(x) ? ` ${code(x)}` : ''}`);
33
+ const stateOf = (x: Exchange): unknown => (isRec(x?.json) ? x.json['state'] : undefined);
34
+
35
+ /** RFC 8785 (JCS) equality; the host-owned field set is empty for both pack kinds. */
36
+ export function packMatches(served: unknown, artifact: unknown): boolean {
37
+ return canonicalJSON(served) === canonicalJSON(artifact);
38
+ }
39
+
40
+ const ENGINES = { openwop: '>=2.0 <3.0.0' };
41
+
42
+ /** A fresh one-chain `private.*` chain pack the host has never seen. */
43
+ export function freshChainPack(nonce: string, description: string): Rec {
44
+ const name = `private.conformance.c${nonce}`;
45
+ return {
46
+ name, version: '1.0.0', kind: 'workflow-chain', engines: ENGINES,
47
+ chains: [{
48
+ chainId: `${name}.passthrough`, version: '1.0.0', label: 'Pass through', description,
49
+ parameters: { type: 'object', properties: {} },
50
+ dag: { nodes: [{ id: 'n1', typeId: 'core.noop' }] },
51
+ }],
52
+ };
53
+ }
54
+
55
+ /**
56
+ * A fresh pure-agent `private.*` pack with one inline-prompt agent. The entry is
57
+ * under `.invalid` (RFC 6761), so a host that wrongly dereferences it gains nothing.
58
+ */
59
+ export function freshAgentPack(nonce: string, systemPrompt: string): Rec {
60
+ const name = `private.conformance.a${nonce}`;
61
+ return {
62
+ name, version: '1.0.0', kind: 'node', engines: ENGINES,
63
+ runtime: { language: 'remote', entry: 'https://conformance.invalid/agents' },
64
+ agents: [{ agentId: `${name}.triage`, persona: 'Conformance triage', modelClass: 'general', systemPrompt }],
65
+ };
66
+ }
67
+
68
+ /** The first agent's id in an agent pack. */
69
+ export function agentIdOf(pack: Rec): string {
70
+ const agents = Array.isArray(pack['agents']) ? pack['agents'] : [];
71
+ const first = agents[0];
72
+ return isRec(first) && typeof first['agentId'] === 'string' ? first['agentId'] : '';
73
+ }
74
+
75
+ /**
76
+ * The §D content rules, as violations (empty = admissible). Schema validity is
77
+ * checked separately; these are the rules a valid manifest can still break.
78
+ */
79
+ export function packContentViolations(kind: PackKind, pack: Rec): string[] {
80
+ const out: string[] = [];
81
+ if (typeof pack['name'] !== 'string' || !pack['name'].startsWith('private.')) out.push('name outside private.*');
82
+ if ('signing' in pack) out.push('carries signing');
83
+ if (kind === 'agent-pack') {
84
+ if (Array.isArray(pack['nodes']) && pack['nodes'].length > 0) out.push('carries nodes');
85
+ const rt = isRec(pack['runtime']) ? pack['runtime'] : {};
86
+ if (rt['language'] !== 'remote') out.push('runtime.language is not remote');
87
+ const agents = Array.isArray(pack['agents']) ? pack['agents'].filter(isRec) : [];
88
+ if (agents.length === 0) out.push('no agents');
89
+ for (const a of agents) {
90
+ if (typeof a['systemPrompt'] !== 'string') out.push(`${String(a['agentId'])}: no inline systemPrompt`);
91
+ for (const k of ['systemPromptRef', 'evalSuiteRef']) if (k in a) out.push(`${String(a['agentId'])}: carries ${k}`);
92
+ const h = isRec(a['handoff']) ? a['handoff'] : {};
93
+ for (const k of ['taskSchemaRef', 'returnSchemaRef']) if (k in h) out.push(`${String(a['agentId'])}: carries handoff.${k}`);
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+
99
+ const listed = (list: Exchange, agentId: string): boolean | null => {
100
+ const items = isRec(list?.json) && Array.isArray(list.json['agents']) ? (list.json['agents'] as unknown[]).filter(isRec) : null;
101
+ return items === null ? null : items.some((a) => a['agentId'] === agentId);
102
+ };
103
+
104
+ /** An unapplied pack is not served; for an agent pack, its agent is not resolvable or listed. */
105
+ export function judgePackInert(pack: Exchange, agent?: { get: Exchange; list: Exchange; agentId: string }): ProposalFinding[] {
106
+ const out = [f(pack?.status === 404, `getInstalledPack MUST answer 404 for an unapplied proposal's name (got ${shown(pack)})`)];
107
+ if (agent !== undefined) {
108
+ const inList = listed(agent.list, agent.agentId);
109
+ out.push(
110
+ f(agent.get?.status === 404, `getAgent MUST answer 404 for an unapplied proposal's agent (got ${shown(agent.get)})`),
111
+ f(agent.list?.status === 200 && inList !== null, `listAgents MUST answer 200 with agents (got ${shown(agent.list)})`),
112
+ f(inList === false, 'listAgents MUST NOT list an unapplied proposal\'s agent'),
113
+ );
114
+ }
115
+ return out;
116
+ }
117
+
118
+ /** A successful apply names `getInstalledPack`, the artifact's own name, and its version. */
119
+ export function judgePackApplied(apply: Exchange, kind: PackKind, artifact: Rec, agentGet?: Exchange): ProposalFinding[] {
120
+ const p = isRec(apply?.json) ? apply.json : {};
121
+ const act = isRec(p['activation']) ? p['activation'] : {};
122
+ const ia = isRec(act['installedArtifact']) ? act['installedArtifact'] : {};
123
+ const out = [
124
+ f(apply?.status === 200 && p['state'] === 'applied', `an authorized apply MUST answer 200 with state applied (got ${shown(apply)}, state ${JSON.stringify(p['state'])})`),
125
+ f(ia['kind'] === kind && ia['operationId'] === 'getInstalledPack', `activation.installedArtifact MUST name kind ${kind} and operationId getInstalledPack (got ${JSON.stringify(ia)})`),
126
+ f(ia['id'] === artifact['name'], `installedArtifact.id MUST be the artifact's name, never a minted one (got ${JSON.stringify(ia['id'])})`),
127
+ f(ia['version'] === artifact['version'], `installedArtifact.version MUST be the artifact's version (got ${JSON.stringify(ia['version'])})`),
128
+ ];
129
+ if (agentGet !== undefined) out.push(f(agentGet?.status === 200, `getAgent MUST resolve an applied agent pack's agent for the applying caller (got ${shown(agentGet)})`));
130
+ return out;
131
+ }
132
+
133
+ /** The installed pack is the reviewed revision, byte for byte, with nothing removed. */
134
+ export function judgePackReadback(served: Exchange, revised: Rec, original: Rec): ProposalFinding[] {
135
+ return [
136
+ f(served?.status === 200, `getInstalledPack MUST serve the installed pack after apply (got ${shown(served)})`),
137
+ f(served?.status === 200 && packMatches(served.json, revised), 'the read-back MUST be JCS-equal to the last-persisted (revised) artifact, with no field removed or added'),
138
+ f(served?.status === 200 && !packMatches(served.json, original), 'the read-back MUST NOT be the pre-revision artifact: that is a silent re-synthesis'),
139
+ ];
140
+ }
141
+
142
+ /** An apply of an artifact §D forbids is `422`, installs nothing and leaves `state` unchanged. */
143
+ export function judgePackRefused(what: string, apply: Exchange, after: Exchange, pack: Exchange, agentGet?: Exchange): ProposalFinding[] {
144
+ const out = [
145
+ f(apply?.status === 422, `${what}: the apply MUST be refused 422 (got ${shown(apply)})`),
146
+ f(stateOf(after) === 'draft', `${what}: a refused apply MUST leave state unchanged (got ${JSON.stringify(stateOf(after))})`),
147
+ f(pack?.status === 404, `${what}: a refused apply MUST install nothing (getInstalledPack ${shown(pack)})`),
148
+ ];
149
+ if (agentGet !== undefined) out.push(f(agentGet?.status === 404, `${what}: a refused apply MUST install no agent (getAgent ${shown(agentGet)})`));
150
+ return out;
151
+ }
152
+
153
+ /**
154
+ * RFC 0249 §B lets a host validate an artifact at submit. A §D violation refused
155
+ * there (`400` or `422`) is as good as one refused at apply, if nothing installed.
156
+ */
157
+ export function judgePackRefusedAtSubmit(what: string, submit: Exchange, pack: Exchange): ProposalFinding[] {
158
+ return [
159
+ f(submit?.status === 400 || submit?.status === 422, `${what}: a submit refused for its content MUST answer 400 or 422 (got ${shown(submit)})`),
160
+ f(pack?.status === 404, `${what}: a refused submit MUST install nothing (getInstalledPack ${shown(pack)})`),
161
+ ];
162
+ }
163
+
164
+ /** A second apply at the installed name and version is `409 proposal_install_conflict` `version-not-greater`. */
165
+ export function judgeInstallConflict(apply: Exchange, after: Exchange): ProposalFinding[] {
166
+ return [
167
+ f(apply?.status === 409 && code(apply) === 'proposal_install_conflict', `an apply at an equal installed version MUST be refused 409 proposal_install_conflict (got ${shown(apply)})`),
168
+ f(details(apply)['reason'] === 'version-not-greater', `the refusal MUST name details.reason "version-not-greater" (got ${JSON.stringify(details(apply)['reason'])})`),
169
+ f(stateOf(after) === 'draft', `a refused install MUST leave state unchanged (got ${JSON.stringify(stateOf(after))})`),
170
+ ];
171
+ }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * v2 — an applied `workflow-chain-pack` or `agent-pack` proposal is read back
3
+ * through `getInstalledPack`, and is inert until applied (RFC 0253, completing
4
+ * RFC 0249 §D for the two pack kinds; the rules are the `getInstalledPack`
5
+ * operation in `api/v2/openapi.yaml`, the `agents.proposals.readback`
6
+ * description in `spec/v2/facets/agents.schema.json` and
7
+ * `activation.installedArtifact` in `proposal.schema.json`).
8
+ *
9
+ * Server-free (always on):
10
+ * 1. `0253.shape` — an `installedArtifact` naming `getInstalledPack` with a
11
+ * `version` validates; the two fresh artifacts validate against their
12
+ * manifest schemas and satisfy §D; the §Examples negatives break §D.
13
+ *
14
+ * Host legs — gated per kind on `agents.proposals` with `clientDrafts: true`
15
+ * and the kind in both `artifactKinds` and `readback`; legs that apply also
16
+ * need `activation: direct-rbac`. One proposal per kind is driven once per
17
+ * worker with a fresh `private.conformance.*` name: submit P → read → revise
18
+ * to P' → apply → read back → a second proposal at the same name and version
19
+ * → foreign reads. Judges in `lib/proposal-pack-readback-witness.ts`.
20
+ * 2. `0253.chain-draft-inert` / `0253.agent-draft-inert` — the unapplied
21
+ * pack is `404`; for an agent pack its agent is `404` and unlisted
22
+ * (`proposal-inert-until-applied`).
23
+ * 3. `0253.chain-apply-installs` / `0253.agent-apply-installs` — the apply
24
+ * names `getInstalledPack`, the artifact's `name` and `version`.
25
+ * 4. `0253.chain-readback-byte-match` / `0253.agent-readback-byte-match` —
26
+ * the installed pack is JCS-equal to P' and not to P
27
+ * (`proposal-no-resynthesis`).
28
+ * 5. `0253.agent-file-ref-refused` — an agent with `systemPromptRef`, then a
29
+ * pack with `nodes`, is refused `422` and installs nothing.
30
+ * 6. `0253.namespace-refused` — a `vendor.*` name is refused `422`.
31
+ * 7. `0253.signing-refused` — a pack carrying `signing` is refused `422`.
32
+ * 8. `0253.install-conflict` — a second apply at the installed version is
33
+ * `409 proposal_install_conflict` `version-not-greater`.
34
+ * 9. `0253.cross-tenant-not-found` — `OPENWOP_TEST_TENANT_B_API_KEY` reads
35
+ * the installed pack (and agent) exactly as unknown names.
36
+ *
37
+ * Legs 5–7 accept a refusal at submit (RFC 0249 §B lets a host validate there)
38
+ * when nothing installs. A harness key that is unset records `blocked`. The
39
+ * unadvertised case is in `v2-unadvertised-operation-not-found`.
40
+ */
41
+
42
+ import { describe, it, expect } from 'vitest';
43
+ import { randomBytes } from 'node:crypto';
44
+ import { driver, type OpenWOPRequestInit } from '../lib/driver.js';
45
+ import { v2Discovery, v2Validator, v2RefValidator } from '../lib/v2.js';
46
+ import { softSkip } from '../lib/soft-skip.js';
47
+ import { req } from '../lib/requirement-ids.js';
48
+ import { artifactDigest, judgeForeign, type Exchange, type ProposalFinding } from '../lib/proposal-lifecycle-witness.js';
49
+ import {
50
+ agentIdOf, freshAgentPack, freshChainPack, judgeInstallConflict, judgePackApplied, judgePackInert, judgePackReadback,
51
+ judgePackRefused, judgePackRefusedAtSubmit, packContentViolations, type PackKind,
52
+ } from '../lib/proposal-pack-readback-witness.js';
53
+
54
+ const DOC = 'api/v2 getInstalledPack, agents.schema.json §proposals.readback, proposal.schema.json installedArtifact (RFC 0253)';
55
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0';
56
+ const ID_SHAPE = 'openwop.requirement.0253.shape';
57
+ const ID_CHAIN_DRAFT_INERT = 'openwop.requirement.0253.chain-draft-inert';
58
+ const ID_CHAIN_APPLY_INSTALLS = 'openwop.requirement.0253.chain-apply-installs';
59
+ const ID_CHAIN_READBACK_BYTE_MATCH = 'openwop.requirement.0253.chain-readback-byte-match';
60
+ const ID_AGENT_DRAFT_INERT = 'openwop.requirement.0253.agent-draft-inert';
61
+ const ID_AGENT_APPLY_INSTALLS = 'openwop.requirement.0253.agent-apply-installs';
62
+ const ID_AGENT_READBACK_BYTE_MATCH = 'openwop.requirement.0253.agent-readback-byte-match';
63
+ const ID_AGENT_FILE_REF_REFUSED = 'openwop.requirement.0253.agent-file-ref-refused';
64
+ const ID_NAMESPACE_REFUSED = 'openwop.requirement.0253.namespace-refused';
65
+ const ID_SIGNING_REFUSED = 'openwop.requirement.0253.signing-refused';
66
+ const ID_INSTALL_CONFLICT = 'openwop.requirement.0253.install-conflict';
67
+ const ID_CROSS_TENANT_NOT_FOUND = 'openwop.requirement.0253.cross-tenant-not-found';
68
+
69
+ type Rec = Record<string, unknown>;
70
+ const isRec = (v: unknown): v is Rec => typeof v === 'object' && v !== null && !Array.isArray(v);
71
+
72
+ async function ask(method: string, path: string, body?: unknown, bearer?: string): Promise<Exchange> {
73
+ const init: OpenWOPRequestInit = {
74
+ ...(body === undefined ? {} : { body }),
75
+ ...(bearer === undefined ? {} : { authenticated: false, headers: { Authorization: `Bearer ${bearer}` } }),
76
+ };
77
+ try {
78
+ const r = await driver.request(method, path, init);
79
+ return { status: r.status, json: r.json };
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ const packPath = (name: string): string => `/installed-packs/${encodeURIComponent(name)}`;
85
+ const agentPath = (agentId: string): string => `/agents/${encodeURIComponent(agentId)}`;
86
+
87
+ type Skip = { skip: [Parameters<typeof softSkip>[0], string] };
88
+ async function gate(kind: PackKind, needsApply: boolean): Promise<Skip | { facet: Rec }> {
89
+ let doc: Rec | null;
90
+ try { doc = await v2Discovery(); } catch { doc = null; }
91
+ if (!doc) return { skip: ['blocked', UNREADABLE] };
92
+ const agents = doc['agents'];
93
+ const facet = isRec(agents) && isRec(agents['proposals']) ? agents['proposals'] : null;
94
+ if (facet === null) return { skip: ['inapplicable', 'the host does not advertise agents.proposals'] };
95
+ if (facet['clientDrafts'] !== true) return { skip: ['inapplicable', 'the host does not advertise agents.proposals.clientDrafts'] };
96
+ const rb = Array.isArray(facet['readback']) ? facet['readback'] : [];
97
+ const kinds = Array.isArray(facet['artifactKinds']) ? facet['artifactKinds'] : [];
98
+ if (!rb.includes(kind) || !kinds.includes(kind)) return { skip: ['inapplicable', `the host does not advertise ${kind} in both agents.proposals.artifactKinds and readback`] };
99
+ if (needsApply && facet['activation'] !== 'direct-rbac') return { skip: ['inapplicable', 'agents.proposals.activation is not direct-rbac — the apply is gated by an approval run'] };
100
+ return { facet };
101
+ }
102
+
103
+ /** The first pack kind the host reads back, for the kind-neutral refusal legs. */
104
+ async function anyKind(): Promise<Skip | { kind: PackKind }> {
105
+ let last: Skip | undefined;
106
+ for (const kind of ['workflow-chain-pack', 'agent-pack'] as const) {
107
+ const g = await gate(kind, true);
108
+ if (!('skip' in g)) return { kind };
109
+ last = g;
110
+ }
111
+ return last ?? { skip: ['inapplicable', 'the host reads back no pack kind'] };
112
+ }
113
+
114
+ interface Lifecycle {
115
+ readonly blockedAt?: string;
116
+ readonly p: Rec; readonly p2: Rec; readonly name: string; readonly agentId?: string;
117
+ readonly inertPack?: Exchange; readonly inertAgent?: { get: Exchange; list: Exchange; agentId: string };
118
+ readonly apply?: Exchange; readonly agentAfter?: Exchange;
119
+ readonly readback?: Exchange;
120
+ readonly conflict?: { apply: Exchange; after: Exchange } | string;
121
+ readonly foreign?: Array<{ what: string; foreign: Exchange; unknown: Exchange }> | string;
122
+ }
123
+
124
+ const runs = new Map<PackKind, Promise<Lifecycle>>();
125
+ /** One lifecycle per kind per worker: every host leg judges a slice of the same records. */
126
+ function lifecycle(kind: PackKind): Promise<Lifecycle> {
127
+ let r = runs.get(kind);
128
+ if (r === undefined) { r = drive(kind); runs.set(kind, r); }
129
+ return r;
130
+ }
131
+
132
+ async function drive(kind: PackKind): Promise<Lifecycle> {
133
+ const nonce = randomBytes(6).toString('hex');
134
+ const p = kind === 'agent-pack' ? freshAgentPack(nonce, 'You triage the input into one of three queues.') : freshChainPack(nonce, 'Passes its input through unchanged.');
135
+ const p2 = kind === 'agent-pack'
136
+ ? { ...p, agents: [{ ...(p['agents'] as Rec[])[0], systemPrompt: 'You triage the input into exactly one of three queues.' }] }
137
+ : { ...p, description: 'A one-node chain that passes its input through.' };
138
+ const name = p['name'] as string;
139
+ const agentId = kind === 'agent-pack' ? agentIdOf(p) : undefined;
140
+ const base = { p, p2, name, ...(agentId === undefined ? {} : { agentId }) };
141
+
142
+ const submit = await ask('POST', '/proposals', { kind, artifact: p, title: 'conformance pack proposal' });
143
+ const pid = isRec(submit?.json) && typeof submit.json['id'] === 'string' ? submit.json['id'] : undefined;
144
+ if (submit?.status !== 201 || pid === undefined) return { ...base, blockedAt: `submitProposal answered ${submit?.status ?? 'nothing'}; no proposal to drive` };
145
+ const path = `/proposals/${encodeURIComponent(pid)}`;
146
+
147
+ const inertPack = await ask('GET', packPath(name));
148
+ const inertAgent = agentId === undefined ? undefined : { get: await ask('GET', agentPath(agentId)), list: await ask('GET', '/agents'), agentId };
149
+ const revise = await ask('PATCH', path, { artifact: p2 });
150
+ if (revise?.status !== 200) return { ...base, inertPack, ...(inertAgent ? { inertAgent } : {}), blockedAt: `reviseProposal answered ${revise?.status ?? 'nothing'}` };
151
+
152
+ const apply = await ask('POST', `${path}:apply`, { artifactDigest: artifactDigest(p2) });
153
+ const readback = await ask('GET', packPath(name));
154
+ const agentAfter = agentId === undefined ? undefined : await ask('GET', agentPath(agentId));
155
+
156
+ let conflict: Lifecycle['conflict'];
157
+ const again = await ask('POST', '/proposals', { kind, artifact: p2, title: 'conformance pack proposal (same version)' });
158
+ const pid2 = isRec(again?.json) && typeof again.json['id'] === 'string' ? again.json['id'] : undefined;
159
+ if (again?.status !== 201 || pid2 === undefined) conflict = `a second submitProposal at the same name answered ${again?.status ?? 'nothing'}`;
160
+ else {
161
+ const path2 = `/proposals/${encodeURIComponent(pid2)}`;
162
+ conflict = { apply: await ask('POST', `${path2}:apply`, { artifactDigest: artifactDigest(p2) }), after: await ask('GET', path2) };
163
+ }
164
+
165
+ const tenantB = process.env['OPENWOP_TEST_TENANT_B_API_KEY']?.trim();
166
+ let foreign: Lifecycle['foreign'];
167
+ if (!tenantB) foreign = 'OPENWOP_TEST_TENANT_B_API_KEY (a credential bound to a second tenant) is not set';
168
+ else if (tenantB === process.env['OPENWOP_API_KEY']) foreign = 'OPENWOP_TEST_TENANT_B_API_KEY equals OPENWOP_API_KEY — the leg needs a second tenant';
169
+ else {
170
+ foreign = [{ what: 'getInstalledPack for the installed pack', foreign: await ask('GET', packPath(name), undefined, tenantB),
171
+ unknown: await ask('GET', packPath(`private.conformance.u${nonce}`), undefined, tenantB) }];
172
+ if (agentId !== undefined) {
173
+ foreign.push({ what: 'getAgent for the installed agent', foreign: await ask('GET', agentPath(agentId), undefined, tenantB),
174
+ unknown: await ask('GET', agentPath(`private.conformance.u${nonce}.triage`), undefined, tenantB) });
175
+ }
176
+ }
177
+ return { ...base, inertPack, ...(inertAgent ? { inertAgent } : {}), apply, readback, ...(agentAfter ? { agentAfter } : {}), conflict, foreign };
178
+ }
179
+
180
+ /** Submit a forbidden artifact and try to apply it; judge the refusal wherever it lands. */
181
+ async function refused(what: string, kind: PackKind, artifact: Rec): Promise<ProposalFinding[] | string> {
182
+ const name = artifact['name'] as string;
183
+ const agentId = kind === 'agent-pack' ? agentIdOf(artifact) : undefined;
184
+ const submit = await ask('POST', '/proposals', { kind, artifact, title: `conformance ${what}` });
185
+ if (submit?.status === 400 || submit?.status === 422) return judgePackRefusedAtSubmit(what, submit, await ask('GET', packPath(name)));
186
+ const pid = isRec(submit?.json) && typeof submit.json['id'] === 'string' ? submit.json['id'] : undefined;
187
+ if (submit?.status !== 201 || pid === undefined) return `submitProposal for ${what} answered ${submit?.status ?? 'nothing'}`;
188
+ const path = `/proposals/${encodeURIComponent(pid)}`;
189
+ const apply = await ask('POST', `${path}:apply`, { artifactDigest: artifactDigest(artifact) });
190
+ return judgePackRefused(what, apply, await ask('GET', path), await ask('GET', packPath(name)),
191
+ agentId === undefined ? undefined : await ask('GET', agentPath(agentId)));
192
+ }
193
+
194
+ function expectAll(id: string, findings: readonly ProposalFinding[]): void {
195
+ for (const x of findings) expect(x.ok, req(id, DOC, x.message)).toBe(true);
196
+ }
197
+
198
+ type Leg = Skip | { l: Lifecycle };
199
+ /** Gate, then drive (or reuse) the kind's lifecycle; `blocked` when it stopped before `has`. */
200
+ async function legFor(kind: PackKind, needsApply: boolean, has: (l: Lifecycle) => boolean, stage: string): Promise<Leg> {
201
+ const g = await gate(kind, needsApply);
202
+ if ('skip' in g) return g;
203
+ const l = await lifecycle(kind);
204
+ if (!has(l)) return { skip: ['blocked', l.blockedAt ?? `the lifecycle did not reach ${stage}`] };
205
+ return { l };
206
+ }
207
+
208
+ describe('v2 RFC 0253 — pack read-back wire shape (server-free)', () => {
209
+ it('installedArtifact names getInstalledPack with a version; the fresh artifacts are valid and admissible; the negatives break §D', () => {
210
+ const S = 'proposal.schema.json installedArtifact, the two manifest schemas, agents.schema.json §proposals.readback (RFC 0253 §B–§D)';
211
+ const installed = v2RefValidator('proposal.schema.json#/properties/activation/properties/installedArtifact');
212
+ expect(installed({ kind: 'agent-pack', operationId: 'getInstalledPack', id: 'private.acme.triage-agents', version: '1.0.0' }).ok,
213
+ req(ID_SHAPE, S, 'an installedArtifact naming getInstalledPack with a version MUST validate')).toBe(true);
214
+ const chain = freshChainPack('s1', 'shape');
215
+ const agent = freshAgentPack('s1', 'You triage.');
216
+ expect(v2Validator('workflow-chain-pack-manifest')(chain).ok, req(ID_SHAPE, S, 'the fresh chain pack MUST validate against workflow-chain-pack-manifest')).toBe(true);
217
+ expect(v2Validator('node-pack-manifest')(agent).ok, req(ID_SHAPE, S, 'the fresh agent pack MUST validate against node-pack-manifest')).toBe(true);
218
+ expect(packContentViolations('workflow-chain-pack', chain), req(ID_SHAPE, S, 'the fresh chain pack MUST satisfy §D')).toEqual([]);
219
+ expect(packContentViolations('agent-pack', agent), req(ID_SHAPE, S, 'the fresh agent pack MUST satisfy §D')).toEqual([]);
220
+ const a0 = (agent['agents'] as Rec[])[0] as Rec;
221
+ const { systemPrompt: _sp, ...noInline } = a0;
222
+ const negatives: Array<[PackKind, Rec, string]> = [
223
+ ['agent-pack', { ...agent, agents: [{ ...noInline, systemPromptRef: 'prompts/a.md' }] }, 'an agent carrying systemPromptRef'],
224
+ ['agent-pack', { ...agent, nodes: [{ typeId: `${String(agent['name'])}.n`, version: '1.0.0', category: 'data', role: 'pure' }] }, 'an agent pack with nodes'],
225
+ ['workflow-chain-pack', { ...chain, signing: { keyId: 'conformance-key', scheme: 'ed25519-canonical-json' } }, 'a chain pack carrying signing'],
226
+ ['agent-pack', { ...agent, name: 'vendor.acme.triage' }, 'a vendor.* name'],
227
+ ];
228
+ for (const [kind, doc, what] of negatives) {
229
+ expect(packContentViolations(kind, doc).length, req(ID_SHAPE, S, `${what} MUST break §D`)).toBeGreaterThan(0);
230
+ }
231
+ });
232
+ });
233
+
234
+ describe('v2 RFC 0253 — workflow-chain-pack read-back (host)', () => {
235
+ it('an unapplied workflow-chain-pack is not served', async () => {
236
+ const r = await legFor('workflow-chain-pack', false, (l) => l.inertPack !== undefined, 'the draft');
237
+ if ('skip' in r) return softSkip(...r.skip);
238
+ expectAll(ID_CHAIN_DRAFT_INERT, judgePackInert(r.l.inertPack ?? null));
239
+ });
240
+
241
+ it('an authorized apply of a workflow-chain-pack names getInstalledPack, the name and the version', async () => {
242
+ const r = await legFor('workflow-chain-pack', true, (l) => l.apply !== undefined, 'the apply');
243
+ if ('skip' in r) return softSkip(...r.skip);
244
+ expectAll(ID_CHAIN_APPLY_INSTALLS, judgePackApplied(r.l.apply ?? null, 'workflow-chain-pack', r.l.p2));
245
+ });
246
+
247
+ it('the installed workflow-chain-pack is the reviewed revision, byte for byte', async () => {
248
+ const r = await legFor('workflow-chain-pack', true, (l) => l.readback !== undefined, 'the read-back');
249
+ if ('skip' in r) return softSkip(...r.skip);
250
+ expectAll(ID_CHAIN_READBACK_BYTE_MATCH, judgePackReadback(r.l.readback ?? null, r.l.p2, r.l.p));
251
+ });
252
+ });
253
+
254
+ describe('v2 RFC 0253 — agent-pack read-back (host)', () => {
255
+ it('an unapplied agent-pack is not served, and its agent is not resolvable or listed', async () => {
256
+ const r = await legFor('agent-pack', false, (l) => l.inertPack !== undefined, 'the draft');
257
+ if ('skip' in r) return softSkip(...r.skip);
258
+ expectAll(ID_AGENT_DRAFT_INERT, judgePackInert(r.l.inertPack ?? null, r.l.inertAgent));
259
+ });
260
+
261
+ it('an authorized apply of an agent-pack names getInstalledPack, the name and the version, and resolves the agent', async () => {
262
+ const r = await legFor('agent-pack', true, (l) => l.apply !== undefined, 'the apply');
263
+ if ('skip' in r) return softSkip(...r.skip);
264
+ expectAll(ID_AGENT_APPLY_INSTALLS, judgePackApplied(r.l.apply ?? null, 'agent-pack', r.l.p2, r.l.agentAfter ?? null));
265
+ });
266
+
267
+ it('the installed agent-pack is the reviewed revision, byte for byte', async () => {
268
+ const r = await legFor('agent-pack', true, (l) => l.readback !== undefined, 'the read-back');
269
+ if ('skip' in r) return softSkip(...r.skip);
270
+ expectAll(ID_AGENT_READBACK_BYTE_MATCH, judgePackReadback(r.l.readback ?? null, r.l.p2, r.l.p));
271
+ });
272
+ });
273
+
274
+ describe('v2 RFC 0253 — refusals, conflicts and tenancy (host)', () => {
275
+ it('an agent pack with a file reference, or with nodes, is refused and installs nothing', async () => {
276
+ const g = await gate('agent-pack', true);
277
+ if ('skip' in g) return softSkip(...g.skip);
278
+ const nonce = randomBytes(6).toString('hex');
279
+ const withRef = freshAgentPack(`r${nonce}`, 'unused');
280
+ const { systemPrompt: _sp, ...a0 } = (withRef['agents'] as Rec[])[0] as Rec;
281
+ const withNodes = freshAgentPack(`n${nonce}`, 'You triage.');
282
+ const findings: ProposalFinding[] = [];
283
+ for (const [what, artifact] of [
284
+ ['an agent carrying systemPromptRef', { ...withRef, agents: [{ ...a0, systemPromptRef: 'prompts/a.md' }] }],
285
+ ['an agent pack with nodes', { ...withNodes, nodes: [{ typeId: `${String(withNodes['name'])}.n`, version: '1.0.0', category: 'data', role: 'pure' }] }],
286
+ ] as const) {
287
+ const r = await refused(what, 'agent-pack', artifact);
288
+ if (typeof r === 'string') return softSkip('blocked', r);
289
+ findings.push(...r);
290
+ }
291
+ expectAll(ID_AGENT_FILE_REF_REFUSED, findings);
292
+ });
293
+
294
+ it('a pack named outside private.* is refused and installs nothing', async () => {
295
+ const g = await anyKind();
296
+ if ('skip' in g) return softSkip(...g.skip);
297
+ const nonce = randomBytes(6).toString('hex');
298
+ const base = g.kind === 'agent-pack' ? freshAgentPack(nonce, 'You triage.') : freshChainPack(nonce, 'namespace');
299
+ const r = await refused('a vendor.* name', g.kind, { ...base, name: `vendor.conformance.v${nonce}` });
300
+ if (typeof r === 'string') return softSkip('blocked', r);
301
+ expectAll(ID_NAMESPACE_REFUSED, r);
302
+ });
303
+
304
+ it('a pack carrying signing is refused and installs nothing', async () => {
305
+ const g = await anyKind();
306
+ if ('skip' in g) return softSkip(...g.skip);
307
+ const nonce = randomBytes(6).toString('hex');
308
+ const base = g.kind === 'agent-pack' ? freshAgentPack(nonce, 'You triage.') : freshChainPack(nonce, 'signing');
309
+ const r = await refused('a pack carrying signing', g.kind, { ...base, signing: { keyId: 'conformance-key', scheme: 'ed25519-canonical-json' } });
310
+ if (typeof r === 'string') return softSkip('blocked', r);
311
+ expectAll(ID_SIGNING_REFUSED, r);
312
+ });
313
+
314
+ it('a second apply at the installed name and version is an install conflict', async () => {
315
+ const g = await anyKind();
316
+ if ('skip' in g) return softSkip(...g.skip);
317
+ const l = await lifecycle(g.kind);
318
+ if (l.conflict === undefined) return softSkip('blocked', l.blockedAt ?? 'the lifecycle did not reach the second proposal');
319
+ if (typeof l.conflict === 'string') return softSkip('blocked', l.conflict);
320
+ expectAll(ID_INSTALL_CONFLICT, judgeInstallConflict(l.conflict.apply, l.conflict.after));
321
+ });
322
+
323
+ it('another tenant sees the installed pack and agent as unknown names', async () => {
324
+ const g = await anyKind();
325
+ if ('skip' in g) return softSkip(...g.skip);
326
+ const pairs: Array<{ what: string; foreign: Exchange; unknown: Exchange }> = [];
327
+ for (const kind of ['workflow-chain-pack', 'agent-pack'] as const) {
328
+ if ('skip' in (await gate(kind, true))) continue;
329
+ const l = await lifecycle(kind);
330
+ if (l.foreign === undefined) return softSkip('blocked', l.blockedAt ?? 'the lifecycle did not reach the foreign reads');
331
+ if (typeof l.foreign === 'string') return softSkip('blocked', l.foreign);
332
+ pairs.push(...l.foreign);
333
+ }
334
+ expectAll(ID_CROSS_TENANT_NOT_FOUND, judgeForeign(pairs));
335
+ });
336
+ });
@@ -16,6 +16,8 @@
16
16
  * answers `404 not_found`.
17
17
  * proposals (hosts that do not advertise `agents.proposals`) `GET /proposals`
18
18
  * answers `404 not_found` (RFC 0249).
19
+ * installed-packs (hosts whose `agents.proposals.readback` lists no pack kind)
20
+ * `GET /installed-packs/{packName}` answers `404 not_found` (RFC 0253).
19
21
  *
20
22
  * A host that advertises the facet records the leg `inapplicable`.
21
23
  */
@@ -74,4 +76,18 @@ describe('v2 unadvertised operations answer 404 not_found (RFC 0228 §H)', () =>
74
76
  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
77
  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
78
  });
79
+
80
+ it('GET /installed-packs/{packName} answers 404 not_found when readback lists no pack kind (RFC 0253)', async () => {
81
+ const doc = await discovery();
82
+ if (!doc) return softSkip('blocked', UNREACHABLE);
83
+ const agents = record(doc, 'agents');
84
+ const proposals = agents === null ? null : record(agents, 'proposals');
85
+ const rb = Array.isArray(proposals?.['readback']) ? (proposals['readback'] as unknown[]) : [];
86
+ if (rb.includes('workflow-chain-pack') || rb.includes('agent-pack')) return softSkip('inapplicable', 'the host reads back a pack kind — getInstalledPack is served');
87
+ const res = await get('/installed-packs/private.conformance.unadvertised');
88
+ if (res === null) return softSkip('blocked', 'GET /installed-packs/{packName} unreachable (fetch failed)');
89
+ const ID = 'openwop.requirement.0253.unadvertised-not-found';
90
+ expect(res.status, req(ID, DOC, `getInstalledPack, gated on a pack kind in agents.proposals.readback, which the host does not advertise, MUST answer 404 (got ${res.status})`)).toBe(404);
91
+ expect(readErrorCode(res.json), req(ID, DOC, 'the 404 for an unadvertised gated operation MUST carry the registered code not_found')).toBe('not_found');
92
+ });
77
93
  });