@openwop/openwop-conformance 2.45.29 → 2.45.30
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 +9 -0
- package/README.md +3 -3
- package/coverage.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +80 -56
- package/scenario-majors.json +8 -2
- package/schemas/CORPUS-STAMP.json +13 -13
- package/src/scenarios/v2-effect-identity-business-key.test.ts +7 -79
- package/src/scenarios/v2-effect-ledger-keying.test.ts +102 -0
- package/src/scenarios/v2-v1-signed-webhook-accepted.test.ts +4 -12
- package/src/scenarios/v2-webhooks-v1-scheme-listed.test.ts +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.45.30] — 2026-10-09 — the seams profile floor is seam-driven
|
|
4
|
+
|
|
5
|
+
- **`openwop-conformance-seams-v2` no longer certifies on a host that mounts no seams.** Its predicate is a `conformance` block at the discovery root, deliberately necessary and not sufficient: on a host that does not advertise `conformance.seamsProfile`, every floor leg records `inapplicable`, so the profile is claimed with `witnessCount 0` and not certified. But a floor row is recorded per scenario file, and two floor files had a leg that runs without a seam. openwop-app's production bundle `4683d9f89` (suite 2.45.28) carries a `conformance` block holding only `certificationBundleUrl`, and certified the profile on the facet leg alone.
|
|
6
|
+
- **The facet leg** of `v2-v1-signed-webhook-accepted` (`0176.v1-signed-webhook-accepted.facet`) moves to `v2-webhooks-v1-scheme-listed`.
|
|
7
|
+
- **The ledger-read leg** of `v2-effect-identity-business-key` (`0173.effect-identity-business-key`) moves to `v2-effect-ledger-keying`.
|
|
8
|
+
- Requirement ids are unchanged, and so are the profile's floor files. The floor files keep only their seam-driven legs.
|
|
9
|
+
- **New coherence check** `seams-floor-legs-gated`: every leg of a seams-profile floor file reaches `seamsProfileAdvertised(` or `era2Gate(` before its first assertion. Proven against both files as they were: each fails, naming its seam-free leg.
|
|
10
|
+
- **What a host's next bundle shows:** a host that doesn't advertise the seams profile but ran these legs sees `openwop.floor.v2-effect-identity-business-key` and `openwop.floor.v2-v1-signed-webhook-accepted` go `executed-pass` to `inapplicable`, while both requirement ids keep passing under the new files. On MyndHyve that costs no certification, since it doesn't claim the profile. On openwop-app the profile is claimed and not certified, which is the correct outcome. A seams host is unaffected. Bundles already published verify as before.
|
|
11
|
+
|
|
3
12
|
## [2.45.29] — 2026-10-08 — purpose labels witnessed without a seam; RFC 0241 Accepted
|
|
4
13
|
|
|
5
14
|
- **`v2-purpose-propagation-onward` witnesses purpose labels through normative operations.** New fixture `conformance-purpose-relay`: routed as the only skill of one agent's A2A card, it relays an inbound A2A message's text to the suite's fake peer through `core.conformance.a2a-invoke`. The suite labels the inbound message with `metadata.openwop.permittedPurposes`, finds the agent through `GET /agents` and `GetExtendedAgentCard`, and reads the onward label at its peer; a nonce matches each onward message to its inbound one. The §22 seam stays as the fallback, and each note names the path. A host with the fixture advertised but no card routing it, or with the suite's peer not running, records `blocked`. Self-test double: a widened label, a dropped label and a call made under `[]` each fail their leg (`lib/purpose-relay-witness.test.ts`). Proven on a seams-off v2 reference host (openwop-examples #166): both rows `executed-pass`, and each defect `executed-fail`. This replaces the planned RFC 0240: the label already arrives on a real carrier.
|
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
|
|
140
|
+
The current suite has 652 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 212 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
|
|
|
@@ -147,7 +147,7 @@ The v2 scenarios (`src/scenarios/v2-*.test.ts`) cover, by area:
|
|
|
147
147
|
| Runs | Create, read, list, cancel, bulk cancel, pause/resume, run options and limits, outputs on completion, snapshots | `v2-created-run-readable`, `v2-run-cancel`, `v2-run-pause-resume`, `v2-run-options-limits`, `v2-run-completed-outputs` |
|
|
148
148
|
| Events and streaming | Closed event types, SSE projection, `Last-Event-ID` resumption, poll cursors, terminal-event-once | `v2-stream-sse-projection`, `v2-sse-last-event-id-cursor`, `v2-poll-cursor-v2`, `v2-terminal-event-once` |
|
|
149
149
|
| Errors and headers | The error registry and envelope, malformed bodies, CORS preflight, header scheme | `v2-error-registry`, `v2-malformed-body-envelope`, `v2-cors-preflight`, `v2-header-scheme` |
|
|
150
|
-
| Idempotency | Key grammar, in-flight concurrency, effect identity | `v2-idempotency-key-grammar`, `v2-idempotency-in-flight`, `v2-effect-identity-business-key` |
|
|
150
|
+
| Idempotency | Key grammar, in-flight concurrency, effect identity | `v2-idempotency-key-grammar`, `v2-idempotency-in-flight`, `v2-effect-identity-business-key`, `v2-effect-ledger-keying` |
|
|
151
151
|
| Interrupts and approvals | Resolve after terminal, token scheme, approver enforcement, reject disposition, credential interrupts | `v2-interrupt-resolve-terminal`, `v2-approver-enforced`, `v2-credential-interrupt` |
|
|
152
152
|
| Replay and fork | Fork prefix and ancestry, refusals, replay side-effect suppression, forking a v1 run | `v2-run-fork-prefix`, `v2-run-fork-refusals`, `v2-replay-suppression-ordinal` |
|
|
153
153
|
| Identity, auth and OAuth | Auth challenges, bound ids, workspace scope from identity, OIDC audience, protected-resource metadata, PKCE | `v2-auth-challenge`, `v2-bound-id-kinds`, `v2-oidc-id-token-audience`, `v2-oauth-client-pkce-state-iss` |
|
|
@@ -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:
|
|
169
|
+
Current source tree: 652 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
|
|
package/coverage.md
CHANGED
|
@@ -39,11 +39,11 @@ Each scenario that targets major 2 is listed once, under the [`spec/v2/core/`](.
|
|
|
39
39
|
| [`portability.md`](../spec/v2/core/portability.md) | `v2-portability-advertisement.test.ts` |
|
|
40
40
|
| [`replay.md`](../spec/v2/core/replay.md) | `context-summarization-replay.test.ts`, `memory-attribution-replay-stable.test.ts`, `v2-effect-seam-manifest.test.ts`, `v2-effect-seam-no-refire.test.ts`, `v2-fork-a-v1-run.test.ts`, `v2-nondeterminism-policy-advertisement.test.ts`, `v2-nondeterminism-sources.test.ts`, `v2-replay-suppression-ordinal.test.ts`, `v2-run-fork-prefix.test.ts` |
|
|
41
41
|
| [`runs.md`](../spec/v2/core/runs.md) | `v2-artifact-a2a-shape.test.ts`, `v2-budget-enforcement.test.ts`, `v2-budget-exhaustion-facet.test.ts`, `v2-configurable-closed.test.ts`, `v2-conversation-turn-parts.test.ts`, `v2-created-run-readable.test.ts`, `v2-data-residency-admission.test.ts`, `v2-data-residency-advertisement.test.ts`, `v2-dead-letter-advertisement.test.ts`, `v2-eval-mode-unadvertised-refused.test.ts`, `v2-limits-envelope-caps-advertisement.test.ts`, `v2-run-annotation-not-event.test.ts`, `v2-run-bulk-cancel.test.ts`, `v2-run-cancel.test.ts`, `v2-run-diff-identical.test.ts`, `v2-run-execution-bounds.test.ts`, `v2-run-fork-ancestry.test.ts`, `v2-run-fork-refusals.test.ts`, `v2-run-list.test.ts`, `v2-run-options-limits.test.ts`, `v2-run-pause-resume.test.ts`, `v2-run-snapshot-etag.test.ts` |
|
|
42
|
-
| [`security-defaults.md`](../spec/v2/core/security-defaults.md) | `audit-anomaly-shape.test.ts`, `audit-checkpoint-signature.test.ts`, `audit-checkpoint-vectors.test.ts`, `audit-log-integrity.test.ts`, `inbound-credential-no-passthrough.test.ts`, `v2-compensation-read-projection.test.ts`, `v2-effect-identity-business-key.test.ts`, `v2-pack-isolation.test.ts`, `v2-purpose-propagation-advertisement.test.ts`, `v2-purpose-propagation-onward.test.ts`, `v2-relaxation-recorded.test.ts` |
|
|
42
|
+
| [`security-defaults.md`](../spec/v2/core/security-defaults.md) | `audit-anomaly-shape.test.ts`, `audit-checkpoint-signature.test.ts`, `audit-checkpoint-vectors.test.ts`, `audit-log-integrity.test.ts`, `inbound-credential-no-passthrough.test.ts`, `v2-compensation-read-projection.test.ts`, `v2-effect-identity-business-key.test.ts`, `v2-effect-ledger-keying.test.ts`, `v2-pack-isolation.test.ts`, `v2-purpose-propagation-advertisement.test.ts`, `v2-purpose-propagation-onward.test.ts`, `v2-relaxation-recorded.test.ts` |
|
|
43
43
|
| [`storage.md`](../spec/v2/core/storage.md) | `v2-fs-sandbox-escape-refused.test.ts`, `v2-storage-cross-tenant-isolation.test.ts`, `v2-table-schema-enforcement.test.ts` |
|
|
44
44
|
| [`tool-catalog.md`](../spec/v2/core/tool-catalog.md) | `tool-catalog-compact-projection.test.ts`, `tool-catalog-projection.test.ts`, `tool-descriptor-shape.test.ts`, `v2-tool-catalog-annotations.test.ts` |
|
|
45
45
|
| [`versioning.md`](../spec/v2/core/versioning.md) | `v2-advertised-path-space-served.test.ts`, `v2-client-version-header.test.ts`, `v2-dual-stack-negotiation.test.ts`, `v2-malformed-body-envelope.test.ts`, `v2-min-client-version.test.ts`, `v2-preferred-version-default.test.ts`, `v2-version-header-honored.test.ts`, `v2-well-known-one-resource.test.ts` |
|
|
46
|
-
| [`webhooks.md`](../spec/v2/core/webhooks.md) | `v2-trigger-bridge-delivery.test.ts`, `v2-trigger-dead-letter-read.test.ts`, `v2-v1-signed-webhook-accepted.test.ts`, `v2-webhook-delivery-isolation.test.ts`, `v2-webhook-delivery-shape.test.ts`, `v2-webhook-durable-delivery.test.ts`, `v2-webhook-egress-refusal.test.ts`, `v2-webhook-endpoint-verification.test.ts`, `v2-webhook-generated-secret.test.ts`, `v2-webhook-message-id-stable.test.ts`, `v2-webhook-secret-rotation.test.ts`, `v2-webhook-standard-webhooks-delivery.test.ts`, `v2-webhook-unregister-stops-delivery.test.ts` |
|
|
46
|
+
| [`webhooks.md`](../spec/v2/core/webhooks.md) | `v2-trigger-bridge-delivery.test.ts`, `v2-trigger-dead-letter-read.test.ts`, `v2-v1-signed-webhook-accepted.test.ts`, `v2-webhooks-v1-scheme-listed.test.ts`, `v2-webhook-delivery-isolation.test.ts`, `v2-webhook-delivery-shape.test.ts`, `v2-webhook-durable-delivery.test.ts`, `v2-webhook-egress-refusal.test.ts`, `v2-webhook-endpoint-verification.test.ts`, `v2-webhook-generated-secret.test.ts`, `v2-webhook-message-id-stable.test.ts`, `v2-webhook-secret-rotation.test.ts`, `v2-webhook-standard-webhooks-delivery.test.ts`, `v2-webhook-unregister-stops-delivery.test.ts` |
|
|
47
47
|
| [`workflow-chain-packs.md`](../spec/v2/core/workflow-chain-packs.md) | `v2-chain-pin-exact.test.ts` |
|
|
48
48
|
|
|
49
49
|
`overview.md`, the introduction to the other documents, has no scenario filed under it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.45.
|
|
3
|
+
"version": "2.45.30",
|
|
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.45.
|
|
59
|
+
"@openwop/spec-artifacts": "2.45.30"
|
|
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":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 740,
|
|
6
|
+
"tests": 2783,
|
|
7
|
+
"withStableId": 2783,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2645
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -30331,85 +30331,85 @@
|
|
|
30331
30331
|
]
|
|
30332
30332
|
},
|
|
30333
30333
|
{
|
|
30334
|
-
"id": "openwop.it.v2-effect-identity-business-key.
|
|
30334
|
+
"id": "openwop.it.v2-effect-identity-business-key.the-same-provider-key-is-presented-across-two-transport-retries",
|
|
30335
30335
|
"file": "v2-effect-identity-business-key.test.ts",
|
|
30336
|
-
"line":
|
|
30337
|
-
"title": "
|
|
30338
|
-
"explicitId": "openwop.requirement.0173.effect-identity-business-key",
|
|
30336
|
+
"line": 48,
|
|
30337
|
+
"title": "the same provider key is presented across two transport retries",
|
|
30338
|
+
"explicitId": "openwop.requirement.0173.effect-identity-business-key.retry",
|
|
30339
30339
|
"citations": [
|
|
30340
30340
|
{
|
|
30341
|
-
"section": "
|
|
30342
|
-
"requirement":
|
|
30341
|
+
"section": "spec/v2/core/idempotency.md §Layer 2: effect identity",
|
|
30342
|
+
"requirement": null,
|
|
30343
|
+
"interpolated": true
|
|
30343
30344
|
},
|
|
30344
30345
|
{
|
|
30345
|
-
"section": "
|
|
30346
|
-
"requirement":
|
|
30346
|
+
"section": "spec/v2/core/idempotency.md §Layer 2: effect identity",
|
|
30347
|
+
"requirement": null,
|
|
30348
|
+
"interpolated": true
|
|
30347
30349
|
},
|
|
30348
30350
|
{
|
|
30349
|
-
"section": "effect
|
|
30351
|
+
"section": "spec/v2/core/idempotency.md §Layer 2: effect identity",
|
|
30350
30352
|
"requirement": null,
|
|
30351
30353
|
"interpolated": true
|
|
30352
30354
|
},
|
|
30353
30355
|
{
|
|
30354
|
-
"section": "
|
|
30355
|
-
"requirement":
|
|
30356
|
+
"section": "spec/v2/core/idempotency.md §Layer 2: effect identity",
|
|
30357
|
+
"requirement": null,
|
|
30358
|
+
"interpolated": true
|
|
30356
30359
|
},
|
|
30357
30360
|
{
|
|
30358
|
-
"section": "
|
|
30361
|
+
"section": "spec/v2/core/idempotency.md §Layer 2: effect identity",
|
|
30359
30362
|
"requirement": null,
|
|
30360
30363
|
"interpolated": true
|
|
30361
30364
|
},
|
|
30362
30365
|
{
|
|
30363
|
-
"section": "
|
|
30366
|
+
"section": "spec/v2/core/idempotency.md §Layer 2: effect identity",
|
|
30364
30367
|
"requirement": null,
|
|
30365
30368
|
"interpolated": true
|
|
30366
30369
|
},
|
|
30367
30370
|
{
|
|
30368
|
-
"section": "
|
|
30369
|
-
"requirement": "
|
|
30371
|
+
"section": "spec/v2/core/idempotency.md §Layer 2: effect identity",
|
|
30372
|
+
"requirement": "the key the ledger records MUST be the key the provider received — a ledger that disagrees with the wire is not a witness"
|
|
30370
30373
|
}
|
|
30371
30374
|
]
|
|
30372
30375
|
},
|
|
30373
30376
|
{
|
|
30374
|
-
"id": "openwop.it.v2-effect-
|
|
30375
|
-
"file": "v2-effect-
|
|
30376
|
-
"line":
|
|
30377
|
-
"title": "
|
|
30378
|
-
"explicitId": "openwop.requirement.0173.effect-identity-business-key
|
|
30377
|
+
"id": "openwop.it.v2-effect-ledger-keying.get-runs-runid-effects-validates-and-every-row-is-keyed-on-business-identity-or",
|
|
30378
|
+
"file": "v2-effect-ledger-keying.test.ts",
|
|
30379
|
+
"line": 42,
|
|
30380
|
+
"title": "GET /runs/{runId}/effects validates and every row is keyed on business identity or the activity recipe",
|
|
30381
|
+
"explicitId": "openwop.requirement.0173.effect-identity-business-key",
|
|
30379
30382
|
"citations": [
|
|
30380
30383
|
{
|
|
30381
|
-
"section": "
|
|
30382
|
-
"requirement":
|
|
30383
|
-
"interpolated": true
|
|
30384
|
+
"section": "runs.md §Create",
|
|
30385
|
+
"requirement": "POST /runs MUST answer 201 for the noop fixture"
|
|
30384
30386
|
},
|
|
30385
30387
|
{
|
|
30386
|
-
"section": "
|
|
30387
|
-
"requirement":
|
|
30388
|
-
"interpolated": true
|
|
30388
|
+
"section": "security-defaults.md §Layer-2 effect identity",
|
|
30389
|
+
"requirement": "a host advertising `idempotency` MUST serve GET /runs/{runId}/effects with 200 (RFC 0173 §B)"
|
|
30389
30390
|
},
|
|
30390
30391
|
{
|
|
30391
|
-
"section": "
|
|
30392
|
+
"section": "effect-ledger-projection.schema.json",
|
|
30392
30393
|
"requirement": null,
|
|
30393
30394
|
"interpolated": true
|
|
30394
30395
|
},
|
|
30395
30396
|
{
|
|
30396
|
-
"section": "
|
|
30397
|
-
"requirement":
|
|
30398
|
-
"interpolated": true
|
|
30397
|
+
"section": "effect-ledger-projection.schema.json runId",
|
|
30398
|
+
"requirement": "runId MUST echo the run read"
|
|
30399
30399
|
},
|
|
30400
30400
|
{
|
|
30401
|
-
"section": "
|
|
30401
|
+
"section": "security-defaults.md §Layer-2 effect identity",
|
|
30402
30402
|
"requirement": null,
|
|
30403
30403
|
"interpolated": true
|
|
30404
30404
|
},
|
|
30405
30405
|
{
|
|
30406
|
-
"section": "
|
|
30406
|
+
"section": "RFC 0150 §B",
|
|
30407
30407
|
"requirement": null,
|
|
30408
30408
|
"interpolated": true
|
|
30409
30409
|
},
|
|
30410
30410
|
{
|
|
30411
|
-
"section": "
|
|
30412
|
-
"requirement": "
|
|
30411
|
+
"section": "effect-ledger-projection.schema.json providerKey",
|
|
30412
|
+
"requirement": "providerKey is a redaction-safe identity, never credential material"
|
|
30413
30413
|
}
|
|
30414
30414
|
]
|
|
30415
30415
|
},
|
|
@@ -37849,27 +37849,10 @@
|
|
|
37849
37849
|
}
|
|
37850
37850
|
]
|
|
37851
37851
|
},
|
|
37852
|
-
{
|
|
37853
|
-
"id": "openwop.it.v2-v1-signed-webhook-accepted.the-webhooks-facet-lists-scheme-v1-the-scheme-a-v1-signed-delivery-is-verified-u",
|
|
37854
|
-
"file": "v2-v1-signed-webhook-accepted.test.ts",
|
|
37855
|
-
"line": 79,
|
|
37856
|
-
"title": "the webhooks facet lists scheme \"v1\" — the scheme a v1-signed delivery is verified under",
|
|
37857
|
-
"explicitId": "openwop.requirement.0176.v1-signed-webhook-accepted.facet",
|
|
37858
|
-
"citations": [
|
|
37859
|
-
{
|
|
37860
|
-
"section": "spec/v2/core/webhooks.md §Surfaces",
|
|
37861
|
-
"requirement": "the webhooks facet is { signatureAlgorithms[] }"
|
|
37862
|
-
},
|
|
37863
|
-
{
|
|
37864
|
-
"section": "spec/v2/core/webhooks.md §Surfaces",
|
|
37865
|
-
"requirement": "signatureAlgorithms MUST list \"v1\" — the cut adds no signature scheme (RFC 0176 §D.2)"
|
|
37866
|
-
}
|
|
37867
|
-
]
|
|
37868
|
-
},
|
|
37869
37852
|
{
|
|
37870
37853
|
"id": "openwop.it.v2-v1-signed-webhook-accepted.the-host-s-v2-receiver-path-accepts-an-x-openwop-only-scheme-v1-delivery-and-ref",
|
|
37871
37854
|
"file": "v2-v1-signed-webhook-accepted.test.ts",
|
|
37872
|
-
"line":
|
|
37855
|
+
"line": 81,
|
|
37873
37856
|
"title": "the host's v2 receiver path accepts an X-openwop-*-only scheme-v1 delivery and refuses a tampered one",
|
|
37874
37857
|
"explicitId": "openwop.requirement.0176.v1-signed-webhook-accepted",
|
|
37875
37858
|
"citations": [
|
|
@@ -38622,6 +38605,23 @@
|
|
|
38622
38605
|
}
|
|
38623
38606
|
]
|
|
38624
38607
|
},
|
|
38608
|
+
{
|
|
38609
|
+
"id": "openwop.it.v2-webhooks-v1-scheme-listed.the-webhooks-facet-lists-scheme-v1-the-scheme-a-v1-signed-delivery-is-verified-u",
|
|
38610
|
+
"file": "v2-webhooks-v1-scheme-listed.test.ts",
|
|
38611
|
+
"line": 24,
|
|
38612
|
+
"title": "the webhooks facet lists scheme \"v1\" — the scheme a v1-signed delivery is verified under",
|
|
38613
|
+
"explicitId": "openwop.requirement.0176.v1-signed-webhook-accepted.facet",
|
|
38614
|
+
"citations": [
|
|
38615
|
+
{
|
|
38616
|
+
"section": "spec/v2/core/webhooks.md §Surfaces",
|
|
38617
|
+
"requirement": "the webhooks facet is { signatureAlgorithms[] }"
|
|
38618
|
+
},
|
|
38619
|
+
{
|
|
38620
|
+
"section": "spec/v2/core/webhooks.md §Surfaces",
|
|
38621
|
+
"requirement": "signatureAlgorithms MUST list \"v1\" — the cut adds no signature scheme (RFC 0176 §D.2)"
|
|
38622
|
+
}
|
|
38623
|
+
]
|
|
38624
|
+
},
|
|
38625
38625
|
{
|
|
38626
38626
|
"id": "openwop.it.v2-well-known-one-resource.a-header-less-get-and-an-openwop-version-2-0-get-are-two-representations-of-one",
|
|
38627
38627
|
"file": "v2-well-known-one-resource.test.ts",
|
|
@@ -44699,6 +44699,30 @@
|
|
|
44699
44699
|
}
|
|
44700
44700
|
]
|
|
44701
44701
|
},
|
|
44702
|
+
{
|
|
44703
|
+
"id": "openwop.it.seams-floor-legs-gated.every-leg-of-a-seams-profile-floor-file-reaches-a-seams-gate-before-its-first-as",
|
|
44704
|
+
"file": "seams-floor-legs-gated.test.ts",
|
|
44705
|
+
"line": 84,
|
|
44706
|
+
"title": "every leg of a seams-profile floor file reaches a seams gate before its first assertion",
|
|
44707
|
+
"explicitId": "openwop.it.seams-floor-legs-gated.every-leg-of-a-seams-profile-floor-file-reaches-a-seams-gate-before-its-first-a",
|
|
44708
|
+
"citations": [
|
|
44709
|
+
{
|
|
44710
|
+
"section": "spec/v2/profiles.json",
|
|
44711
|
+
"requirement": null,
|
|
44712
|
+
"interpolated": true
|
|
44713
|
+
},
|
|
44714
|
+
{
|
|
44715
|
+
"section": "spec/v2/profiles.json",
|
|
44716
|
+
"requirement": null,
|
|
44717
|
+
"interpolated": true
|
|
44718
|
+
},
|
|
44719
|
+
{
|
|
44720
|
+
"section": "spec/v2/profiles.json",
|
|
44721
|
+
"requirement": null,
|
|
44722
|
+
"interpolated": true
|
|
44723
|
+
}
|
|
44724
|
+
]
|
|
44725
|
+
},
|
|
44702
44726
|
{
|
|
44703
44727
|
"id": "openwop.it.semantic-digest-v2.the-digest-recipe-section-is-found-at-all",
|
|
44704
44728
|
"file": "semantic-digest-v2.test.ts",
|
package/scenario-majors.json
CHANGED
|
@@ -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":
|
|
4
|
+
"files": 652,
|
|
5
5
|
"v1": 455,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 212
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1431,6 +1431,9 @@
|
|
|
1431
1431
|
"v2-effect-identity-business-key.test.ts": [
|
|
1432
1432
|
2
|
|
1433
1433
|
],
|
|
1434
|
+
"v2-effect-ledger-keying.test.ts": [
|
|
1435
|
+
2
|
|
1436
|
+
],
|
|
1434
1437
|
"v2-effect-seam-manifest.test.ts": [
|
|
1435
1438
|
2
|
|
1436
1439
|
],
|
|
@@ -1854,6 +1857,9 @@
|
|
|
1854
1857
|
"v2-webhook-unregister-stops-delivery.test.ts": [
|
|
1855
1858
|
2
|
|
1856
1859
|
],
|
|
1860
|
+
"v2-webhooks-v1-scheme-listed.test.ts": [
|
|
1861
|
+
2
|
|
1862
|
+
],
|
|
1857
1863
|
"v2-well-known-one-resource.test.ts": [
|
|
1858
1864
|
2
|
|
1859
1865
|
],
|
|
@@ -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.45.
|
|
5
|
-
"corpusTag": "v2.45.
|
|
4
|
+
"version": "2.45.30",
|
|
5
|
+
"corpusTag": "v2.45.30",
|
|
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": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "87950b612a26253dfce8dd55703b32ca164e61c56ba6d038e40626e3e6d69b1e",
|
|
13
|
+
"api/v2/asyncapi.yaml": "99aef28ec33b361d146d2b176714122f95c444967de4993f416f54b17a37ac81",
|
|
14
|
+
"api/v2/openapi.yaml": "97ed2c29a064cfce269dc746503e4059c42093e64590ae828de9f392917698dc",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
|
|
119
119
|
"schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
|
|
120
120
|
"schemas/v2/capabilities.schema.json": "d859a413ff463ec39acc32a9cb76f2e8c7419e45683d6b7f886861765a9c36c7",
|
|
121
|
-
"schemas/v2/certification-bundle.schema.json": "
|
|
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",
|
|
124
124
|
"schemas/v2/chat-card-pack-manifest.schema.json": "92a57db2fd4b2463f14ef155fef8e3f18afc78960b646459dcf32efc79ff6bf5",
|
|
@@ -208,14 +208,14 @@
|
|
|
208
208
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
209
209
|
"spec/v1/alias-detectors.json": "22444c22bb88c568339ef512553f9fc38146e22b566d11ab8608f655d688d19c",
|
|
210
210
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
211
|
-
"spec/v1/core-standard-manifest.json": "
|
|
211
|
+
"spec/v1/core-standard-manifest.json": "1b343f616416e2b92ae3c085bc147f6fb23aa9d366a4488431207e3d0f86bd97",
|
|
212
212
|
"spec/v1/deprecations.json": "e723bc11d7272c71789ad1303bb7f21809a8a024abf57594f9d5c35dc199ece9",
|
|
213
213
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
214
214
|
"spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
|
|
215
215
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
216
216
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
217
217
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
218
|
-
"spec/v1/gaps.json": "
|
|
218
|
+
"spec/v1/gaps.json": "44a1f30d4dd851ac7609fef7dfbd3f7e55404f97130dbe094132f9cd4b71c468",
|
|
219
219
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
220
220
|
"spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
|
|
221
221
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
"spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
|
|
255
255
|
"spec/v2/corrections.json": "3a8d93e33865b692d26a3b6bfd3819766e206b2e0c2a69669baa4bf833d05a49",
|
|
256
256
|
"spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
|
|
257
|
-
"spec/v2/declaration.json": "
|
|
257
|
+
"spec/v2/declaration.json": "22530d99c92b9e0d3ae0dce25507f4cc6350486d560ad8cc01acda054e9face3",
|
|
258
258
|
"spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
|
|
259
259
|
"spec/v2/eos-override.json": "4c72d2f6a658943b01c1de1643cf294642091933af7832f38ee5495df684cb2d",
|
|
260
260
|
"spec/v2/errors.json": "1f0e29455edbd22e28f67f221766209c3c55ac4987beb77993b5702dd7cae131",
|
|
@@ -312,10 +312,10 @@
|
|
|
312
312
|
"spec/v2/migrations.schema.json": "b91ade6320ad1b1185c9495b9424f3aa63438ed8e412c45171c227d82c0afd86",
|
|
313
313
|
"spec/v2/path-manifest.json": "7b5679a4dad98c80d855361d88a98c8a4e045af15e310862a05b285b815dc9fc",
|
|
314
314
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
315
|
-
"spec/v2/profiles.json": "
|
|
316
|
-
"spec/v2/release.json": "
|
|
315
|
+
"spec/v2/profiles.json": "3d84d7712e93d1e823e362615d5f4aaba0789ea4e044b3774965876fbb8829a7",
|
|
316
|
+
"spec/v2/release.json": "b7dea408ef966d034c55aeb0987fe926d88c4009479bd92816c7c87acfb384cf",
|
|
317
317
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
318
|
-
"spec/v2/surface-baseline.json": "
|
|
318
|
+
"spec/v2/surface-baseline.json": "5f58750fdbd55de45cf31c1f7dee09f68c7b1a40c86d20a046f8c2bc52c0c740"
|
|
319
319
|
},
|
|
320
|
-
"corpusCommit": "
|
|
320
|
+
"corpusCommit": "0f13b5191bd0e29c8b4a8219a059361e27834ea2"
|
|
321
321
|
}
|
|
@@ -10,20 +10,19 @@
|
|
|
10
10
|
* state, at }`, content-free of provider payloads (RFC 0150 §B; RFC 0173 §B row
|
|
11
11
|
* C6.7; `spec/v2/core/security-defaults.md` §Layer-2 effect identity).
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* catalogued, so that leg records `blocked` naming it.
|
|
13
|
+
* One leg: the "same provider key across two transport retries" leg, driven
|
|
14
|
+
* through the seams profile (`forceEffectTransportRetry`, RFC 0173 §D.2 G4).
|
|
15
|
+
* This file is a floor of `openwop-conformance-seams-v2`, so every leg in it
|
|
16
|
+
* MUST be seam-driven (`coherence/seams-floor-legs-gated.test.ts`). The
|
|
17
|
+
* seam-free ledger-read leg lives in `v2-effect-ledger-keying.test.ts` since
|
|
18
|
+
* 2.45.30: here it passed the floor on hosts that mount no seams.
|
|
20
19
|
*
|
|
21
20
|
* @see spec/v2/core/security-defaults.md §Layer-2 effect identity
|
|
22
21
|
*/
|
|
23
22
|
|
|
24
23
|
import { describe, it, expect } from 'vitest';
|
|
25
24
|
import { driver } from '../lib/driver.js';
|
|
26
|
-
import { v2Discovery, gateFamily
|
|
25
|
+
import { v2Discovery, gateFamily } from '../lib/v2.js';
|
|
27
26
|
import { seamsProfileAdvertised, SEAMS_PREFIX } from '../lib/seams.js';
|
|
28
27
|
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
29
28
|
import { startEffectReceiver } from '../lib/effect-receiver.js';
|
|
@@ -38,85 +37,14 @@ import { req } from '../lib/requirement-ids.js';
|
|
|
38
37
|
*/
|
|
39
38
|
export const REQUIRES_HOST_CALLBACK = 'the host makes the retried outbound effect call to the suite-owned effect receiver (OPENWOP_WEBHOOK_RECEIVER_PORT)';
|
|
40
39
|
|
|
41
|
-
const FIXTURE = 'conformance-noop';
|
|
42
40
|
const KEYING = ['business-identity', 'activity-recipe'];
|
|
43
|
-
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
44
41
|
|
|
45
42
|
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
46
43
|
try { return await v2Discovery(); } catch { return null; }
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
async function waitTerminal(runId: string, timeoutMs: number): Promise<void> {
|
|
50
|
-
const deadline = Date.now() + timeoutMs;
|
|
51
|
-
while (Date.now() < deadline) {
|
|
52
|
-
const res = await driver.get(`/runs/${encodeURIComponent(runId)}`);
|
|
53
|
-
if (res.status === 200 && TERMINAL.has(String((res.json as { status?: unknown } | null)?.status))) return;
|
|
54
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
46
|
|
|
58
47
|
describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)', () => {
|
|
59
|
-
it('GET /runs/{runId}/effects validates and every row is keyed on business identity or the activity recipe', async () => {
|
|
60
|
-
const doc = await discovery();
|
|
61
|
-
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
62
|
-
if (!(await gateFamily('idempotency'))) return softSkip('inapplicable', 'idempotency family not advertised — no Layer-2 obligation (gate recorded under openwop.family.idempotency)');
|
|
63
|
-
const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
|
|
64
|
-
if (!fixtures.includes(FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no run to read`);
|
|
65
|
-
|
|
66
|
-
// Unfailable-leg audit (2026-09-26): the noop fixture issues no effects, so
|
|
67
|
-
// the per-row loop asserted nothing on any host, and the trailing
|
|
68
|
-
// `softSkip('inapplicable')` (called without `return`, after four passing
|
|
69
|
-
// setup asserts) recorded `executed-pass` with a `partial-witness:` detail —
|
|
70
|
-
// a host that mis-keyed every effect passed this row. Now no expect passes
|
|
71
|
-
// before the empty-ledger check: a failing setup read still fails, and an
|
|
72
|
-
// empty but well-formed ledger returns `inapplicable` with zero assertions.
|
|
73
|
-
const create = await driver.post('/runs', { workflowId: FIXTURE });
|
|
74
|
-
if (create.status !== 201) {
|
|
75
|
-
expect(create.status, req('openwop.requirement.0173.effect-identity-business-key', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
|
|
76
|
-
}
|
|
77
|
-
const runId = (create.json as { runId: string }).runId;
|
|
78
|
-
await waitTerminal(runId, 10_000);
|
|
79
|
-
|
|
80
|
-
const res = await driver.get(`/runs/${encodeURIComponent(runId)}/effects`);
|
|
81
|
-
const check = v2Validator('effect-ledger-projection')(res.json);
|
|
82
|
-
const body = res.json as { runId?: unknown; effects?: Array<{ effectId?: unknown; keying?: unknown; providerKey?: unknown }> } | null;
|
|
83
|
-
const effects = body?.effects ?? [];
|
|
84
|
-
if (res.status === 200 && check.ok && body?.runId === runId && effects.length === 0) {
|
|
85
|
-
// partial-witness-ok: no assertion has passed at runtime. The only earlier
|
|
86
|
-
// expect runs solely when the create is not 201, and then it throws, so this
|
|
87
|
-
// skip is a zero-assertion `inapplicable` (the gate reads source order).
|
|
88
|
-
return softSkip('inapplicable', 'the noop fixture issued no external effect — the ledger read is well-formed but the per-row keying leg had no rows (an effect-issuing fixture would exercise it)');
|
|
89
|
-
}
|
|
90
|
-
expect(
|
|
91
|
-
res.status,
|
|
92
|
-
req('openwop.requirement.0173.effect-identity-business-key', 'security-defaults.md §Layer-2 effect identity', 'a host advertising `idempotency` MUST serve GET /runs/{runId}/effects with 200 (RFC 0173 §B)'),
|
|
93
|
-
).toBe(200);
|
|
94
|
-
expect(
|
|
95
|
-
check.ok,
|
|
96
|
-
req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json', `the ledger projection MUST validate: ${check.errors}`),
|
|
97
|
-
).toBe(true);
|
|
98
|
-
expect(body?.runId, req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json runId', 'runId MUST echo the run read')).toBe(runId);
|
|
99
|
-
const ids = new Set<string>();
|
|
100
|
-
for (const e of effects) {
|
|
101
|
-
expect(
|
|
102
|
-
KEYING,
|
|
103
|
-
req('openwop.requirement.0173.effect-identity-business-key', 'security-defaults.md §Layer-2 effect identity', `keying MUST be business-identity or activity-recipe (the documented fallback) — effect ${String(e.effectId)} declares ${String(e.keying)}`),
|
|
104
|
-
).toContain(e.keying);
|
|
105
|
-
// One logical effect id per effect: a duplicate row is a re-assignment.
|
|
106
|
-
expect(
|
|
107
|
-
ids.has(String(e.effectId)),
|
|
108
|
-
req('openwop.requirement.0173.effect-identity-business-key', 'RFC 0150 §B', `effectId ${String(e.effectId)} MUST be assigned once per effect (duplicate ledger row)`),
|
|
109
|
-
).toBe(false);
|
|
110
|
-
ids.add(String(e.effectId));
|
|
111
|
-
if (typeof e.providerKey === 'string') {
|
|
112
|
-
expect(
|
|
113
|
-
/(secret|bearer |sk-[a-z0-9]{8,})/i.test(e.providerKey),
|
|
114
|
-
req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json providerKey', 'providerKey is a redaction-safe identity, never credential material'),
|
|
115
|
-
).toBe(false);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
48
|
it('the same provider key is presented across two transport retries', async () => {
|
|
121
49
|
const doc = await discovery();
|
|
122
50
|
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0173 §B — the effect ledger read and its keying (suite 2.45.30, target
|
|
3
|
+
* major 2; gated on `idempotency` and the `conformance-noop` fixture).
|
|
4
|
+
*
|
|
5
|
+
* A host that advertises `idempotency` serves `GET /runs/{runId}/effects`
|
|
6
|
+
* (`schemas/v2/effect-ledger-projection.schema.json`); every row's `keying` is
|
|
7
|
+
* `business-identity` or the documented fallback `activity-recipe`, each
|
|
8
|
+
* effect id is assigned once, and `providerKey` is never credential material
|
|
9
|
+
* (RFC 0150 §B; `spec/v2/core/security-defaults.md` §Layer-2 effect identity).
|
|
10
|
+
*
|
|
11
|
+
* This leg needs no seam. Until 2.45.30 it was the first leg of
|
|
12
|
+
* `v2-effect-identity-business-key`, a floor of `openwop-conformance-seams-v2`,
|
|
13
|
+
* and its pass certified that profile on hosts that mount no seams.
|
|
14
|
+
*
|
|
15
|
+
* @see spec/v2/core/security-defaults.md §Layer-2 effect identity
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest';
|
|
19
|
+
import { driver } from '../lib/driver.js';
|
|
20
|
+
import { v2Discovery, gateFamily, v2Validator } from '../lib/v2.js';
|
|
21
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
22
|
+
import { req } from '../lib/requirement-ids.js';
|
|
23
|
+
|
|
24
|
+
const FIXTURE = 'conformance-noop';
|
|
25
|
+
const KEYING = ['business-identity', 'activity-recipe'];
|
|
26
|
+
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
27
|
+
|
|
28
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
29
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function waitTerminal(runId: string, timeoutMs: number): Promise<void> {
|
|
33
|
+
const deadline = Date.now() + timeoutMs;
|
|
34
|
+
while (Date.now() < deadline) {
|
|
35
|
+
const res = await driver.get(`/runs/${encodeURIComponent(runId)}`);
|
|
36
|
+
if (res.status === 200 && TERMINAL.has(String((res.json as { status?: unknown } | null)?.status))) return;
|
|
37
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('RFC 0173 §B — effect ledger keying (gated on idempotency)', () => {
|
|
42
|
+
it('GET /runs/{runId}/effects validates and every row is keyed on business identity or the activity recipe', async () => {
|
|
43
|
+
const doc = await discovery();
|
|
44
|
+
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
45
|
+
if (!(await gateFamily('idempotency'))) return softSkip('inapplicable', 'idempotency family not advertised — no Layer-2 obligation (gate recorded under openwop.family.idempotency)');
|
|
46
|
+
const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
|
|
47
|
+
if (!fixtures.includes(FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no run to read`);
|
|
48
|
+
|
|
49
|
+
// Unfailable-leg audit (2026-09-26): the noop fixture issues no effects, so
|
|
50
|
+
// the per-row loop asserted nothing on any host, and the trailing
|
|
51
|
+
// `softSkip('inapplicable')` (called without `return`, after four passing
|
|
52
|
+
// setup asserts) recorded `executed-pass` with a `partial-witness:` detail —
|
|
53
|
+
// a host that mis-keyed every effect passed this row. Now no expect passes
|
|
54
|
+
// before the empty-ledger check: a failing setup read still fails, and an
|
|
55
|
+
// empty but well-formed ledger returns `inapplicable` with zero assertions.
|
|
56
|
+
const create = await driver.post('/runs', { workflowId: FIXTURE });
|
|
57
|
+
if (create.status !== 201) {
|
|
58
|
+
expect(create.status, req('openwop.requirement.0173.effect-identity-business-key', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
|
|
59
|
+
}
|
|
60
|
+
const runId = (create.json as { runId: string }).runId;
|
|
61
|
+
await waitTerminal(runId, 10_000);
|
|
62
|
+
|
|
63
|
+
const res = await driver.get(`/runs/${encodeURIComponent(runId)}/effects`);
|
|
64
|
+
const check = v2Validator('effect-ledger-projection')(res.json);
|
|
65
|
+
const body = res.json as { runId?: unknown; effects?: Array<{ effectId?: unknown; keying?: unknown; providerKey?: unknown }> } | null;
|
|
66
|
+
const effects = body?.effects ?? [];
|
|
67
|
+
if (res.status === 200 && check.ok && body?.runId === runId && effects.length === 0) {
|
|
68
|
+
// partial-witness-ok: no assertion has passed at runtime. The only earlier
|
|
69
|
+
// expect runs solely when the create is not 201, and then it throws, so this
|
|
70
|
+
// skip is a zero-assertion `inapplicable` (the gate reads source order).
|
|
71
|
+
return softSkip('inapplicable', 'the noop fixture issued no external effect — the ledger read is well-formed but the per-row keying leg had no rows (an effect-issuing fixture would exercise it)');
|
|
72
|
+
}
|
|
73
|
+
expect(
|
|
74
|
+
res.status,
|
|
75
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'security-defaults.md §Layer-2 effect identity', 'a host advertising `idempotency` MUST serve GET /runs/{runId}/effects with 200 (RFC 0173 §B)'),
|
|
76
|
+
).toBe(200);
|
|
77
|
+
expect(
|
|
78
|
+
check.ok,
|
|
79
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json', `the ledger projection MUST validate: ${check.errors}`),
|
|
80
|
+
).toBe(true);
|
|
81
|
+
expect(body?.runId, req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json runId', 'runId MUST echo the run read')).toBe(runId);
|
|
82
|
+
const ids = new Set<string>();
|
|
83
|
+
for (const e of effects) {
|
|
84
|
+
expect(
|
|
85
|
+
KEYING,
|
|
86
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'security-defaults.md §Layer-2 effect identity', `keying MUST be business-identity or activity-recipe (the documented fallback) — effect ${String(e.effectId)} declares ${String(e.keying)}`),
|
|
87
|
+
).toContain(e.keying);
|
|
88
|
+
// One logical effect id per effect: a duplicate row is a re-assignment.
|
|
89
|
+
expect(
|
|
90
|
+
ids.has(String(e.effectId)),
|
|
91
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'RFC 0150 §B', `effectId ${String(e.effectId)} MUST be assigned once per effect (duplicate ledger row)`),
|
|
92
|
+
).toBe(false);
|
|
93
|
+
ids.add(String(e.effectId));
|
|
94
|
+
if (typeof e.providerKey === 'string') {
|
|
95
|
+
expect(
|
|
96
|
+
/(secret|bearer |sk-[a-z0-9]{8,})/i.test(e.providerKey),
|
|
97
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json providerKey', 'providerKey is a redaction-safe identity, never credential material'),
|
|
98
|
+
).toBe(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* HMAC-SHA256 with the subscription secret); this adds no signature scheme and
|
|
8
8
|
* per-subscription secrets are unchanged across the cut (`spec/v2/core/webhooks.md`
|
|
9
9
|
* §Dual emission through the overlap; persistence.md §Everything else a v1 host
|
|
10
|
-
* persisted; migration row C9.9). The facet
|
|
11
|
-
* `"v1"`
|
|
10
|
+
* persisted; migration row C9.9). The facet check (`signatureAlgorithms[]` lists
|
|
11
|
+
* `"v1"`) needs no seam and lives in `v2-webhooks-v1-scheme-listed.test.ts`
|
|
12
|
+
* since 2.45.30: this file is a floor of `openwop-conformance-seams-v2`, so
|
|
13
|
+
* every leg in it MUST be seam-driven (`coherence/seams-floor-legs-gated.test.ts`).
|
|
12
14
|
*
|
|
13
15
|
* The receiver under test is the HOST's inbound receiver path — the v2 host as
|
|
14
16
|
* a subscriber (its trigger bridge, its A2A/MCP peer callbacks) — which the
|
|
@@ -76,16 +78,6 @@ async function deliver(secret: string, delivery: { headers: Record<string, strin
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
describe('RFC 0176 §D.2 — v1-signed-webhook-accepted (gated on webhooks + seams)', () => {
|
|
79
|
-
it('the webhooks facet lists scheme "v1" — the scheme a v1-signed delivery is verified under', async () => {
|
|
80
|
-
const doc = await discovery();
|
|
81
|
-
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
82
|
-
const webhooks = await gateFamily('webhooks');
|
|
83
|
-
if (!webhooks) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
|
|
84
|
-
const algorithms = webhooks['signatureAlgorithms'];
|
|
85
|
-
expect(Array.isArray(algorithms), req('openwop.requirement.0176.v1-signed-webhook-accepted.facet', 'spec/v2/core/webhooks.md §Surfaces', 'the webhooks facet is { signatureAlgorithms[] }')).toBe(true);
|
|
86
|
-
expect(algorithms, req('openwop.requirement.0176.v1-signed-webhook-accepted.facet', 'spec/v2/core/webhooks.md §Surfaces', 'signatureAlgorithms MUST list "v1" — the cut adds no signature scheme (RFC 0176 §D.2)')).toContain('v1');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
81
|
it('the host\'s v2 receiver path accepts an X-openwop-*-only scheme-v1 delivery and refuses a tampered one', async () => {
|
|
90
82
|
const doc = await discovery();
|
|
91
83
|
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0176 §D.2 — the webhooks facet lists scheme `v1` (suite 2.45.30, target
|
|
3
|
+
* major 2; gated on `webhooks`).
|
|
4
|
+
*
|
|
5
|
+
* The facet's `signatureAlgorithms[]` MUST list `"v1"`: the cut adds no
|
|
6
|
+
* signature scheme, so a v1-signed delivery is verified under it
|
|
7
|
+
* (`spec/v2/core/webhooks.md` §Surfaces). Until 2.45.30 this was the first leg
|
|
8
|
+
* of `v2-v1-signed-webhook-accepted`, a floor of `openwop-conformance-seams-v2`,
|
|
9
|
+
* and its pass certified that profile on hosts that mount no seams.
|
|
10
|
+
*
|
|
11
|
+
* @see spec/v2/core/webhooks.md §Surfaces
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { v2Discovery, gateFamily } from '../lib/v2.js';
|
|
16
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
17
|
+
import { req } from '../lib/requirement-ids.js';
|
|
18
|
+
|
|
19
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
20
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('RFC 0176 §D.2 — the webhooks facet lists scheme v1 (gated on webhooks)', () => {
|
|
24
|
+
it('the webhooks facet lists scheme "v1" — the scheme a v1-signed delivery is verified under', async () => {
|
|
25
|
+
const doc = await discovery();
|
|
26
|
+
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
27
|
+
const webhooks = await gateFamily('webhooks');
|
|
28
|
+
if (!webhooks) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
|
|
29
|
+
const algorithms = webhooks['signatureAlgorithms'];
|
|
30
|
+
expect(Array.isArray(algorithms), req('openwop.requirement.0176.v1-signed-webhook-accepted.facet', 'spec/v2/core/webhooks.md §Surfaces', 'the webhooks facet is { signatureAlgorithms[] }')).toBe(true);
|
|
31
|
+
expect(algorithms, req('openwop.requirement.0176.v1-signed-webhook-accepted.facet', 'spec/v2/core/webhooks.md §Surfaces', 'signatureAlgorithms MUST list "v1" — the cut adds no signature scheme (RFC 0176 §D.2)')).toContain('v1');
|
|
32
|
+
});
|
|
33
|
+
});
|