@openwop/openwop-conformance 1.0.0 → 1.1.0

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +31 -6
  3. package/api/grpc/openwop.proto +251 -0
  4. package/api/openapi.yaml +109 -3
  5. package/coverage.md +48 -9
  6. package/fixtures/conformance-configurable-schema.json +39 -0
  7. package/fixtures/conformance-subworkflow-parent.json +1 -1
  8. package/fixtures/conformance-wasm-pack-memory-cap-breach.json +23 -0
  9. package/fixtures/openwop-smoke-byok-roundtrip.json +25 -0
  10. package/fixtures.md +21 -0
  11. package/package.json +3 -1
  12. package/schemas/README.md +4 -0
  13. package/schemas/audit-verify-result.schema.json +90 -0
  14. package/schemas/capabilities.schema.json +293 -1
  15. package/schemas/node-pack-manifest.schema.json +4 -4
  16. package/schemas/pack-lockfile.schema.json +92 -0
  17. package/schemas/registry-version-manifest.schema.json +145 -0
  18. package/schemas/run-event-payloads.schema.json +2 -2
  19. package/schemas/security-advisory.schema.json +109 -0
  20. package/src/lib/a2a-fake-peer.ts +143 -56
  21. package/src/lib/behavior-gate.ts +68 -0
  22. package/src/lib/env.ts +10 -0
  23. package/src/lib/grpc-framing.test.ts +96 -0
  24. package/src/lib/grpc-framing.ts +76 -0
  25. package/src/lib/oidc-issuer.test.ts +328 -0
  26. package/src/lib/oidc-issuer.ts +241 -0
  27. package/src/lib/otel-collector-grpc.test.ts +191 -0
  28. package/src/lib/otel-collector.test.ts +303 -0
  29. package/src/lib/otel-collector.ts +318 -14
  30. package/src/lib/otlp-protobuf.test.ts +461 -0
  31. package/src/lib/otlp-protobuf.ts +529 -0
  32. package/src/scenarios/a2a-task-roundtrip.test.ts +147 -28
  33. package/src/scenarios/agentConfidenceEscalation.test.ts +1 -0
  34. package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +1 -0
  35. package/src/scenarios/agentMemoryRedactionContract.test.ts +1 -0
  36. package/src/scenarios/agentMemoryRoundTrip.test.ts +1 -0
  37. package/src/scenarios/agentMemoryTtlExpiry.test.ts +1 -0
  38. package/src/scenarios/agentMessageReducer.test.ts +1 -0
  39. package/src/scenarios/agentMetadata.test.ts +1 -0
  40. package/src/scenarios/agentPackExport.test.ts +1 -0
  41. package/src/scenarios/agentPackInstall.test.ts +1 -0
  42. package/src/scenarios/agentPackProvenance.test.ts +1 -0
  43. package/src/scenarios/audit-log-integrity.test.ts +3 -6
  44. package/src/scenarios/auth-api-key-rotation.test.ts +182 -0
  45. package/src/scenarios/auth-mtls.test.ts +274 -0
  46. package/src/scenarios/auth-oauth2-client-credentials.test.ts +259 -0
  47. package/src/scenarios/auth-oidc-user-bearer.test.ts +361 -0
  48. package/src/scenarios/bulk-cancel.test.ts +111 -0
  49. package/src/scenarios/configurable-schema.test.ts +48 -0
  50. package/src/scenarios/conversationCapabilityNegotiation.test.ts +1 -0
  51. package/src/scenarios/conversationLifecycle.test.ts +1 -0
  52. package/src/scenarios/conversationReplayDeterminism.test.ts +1 -0
  53. package/src/scenarios/conversationVsLegacySuspend.test.ts +1 -0
  54. package/src/scenarios/debug-bundle-truncation.test.ts +95 -0
  55. package/src/scenarios/discovery.test.ts +183 -0
  56. package/src/scenarios/http-client-ssrf.test.ts +71 -0
  57. package/src/scenarios/idempotency.test.ts +6 -0
  58. package/src/scenarios/idempotencyRetry.test.ts +3 -0
  59. package/src/scenarios/mcp-tool-roundtrip.test.ts +198 -34
  60. package/src/scenarios/mcp-toolcall-redaction.test.ts +66 -0
  61. package/src/scenarios/metric-emission.test.ts +113 -0
  62. package/src/scenarios/orchestratorConservativePath.test.ts +1 -0
  63. package/src/scenarios/orchestratorDispatch.test.ts +1 -0
  64. package/src/scenarios/orchestratorTermination.test.ts +1 -0
  65. package/src/scenarios/otel-emission-grpc.test.ts +98 -0
  66. package/src/scenarios/pause-resume.test.ts +119 -0
  67. package/src/scenarios/production-backpressure.test.ts +342 -0
  68. package/src/scenarios/production-retention-expiry.test.ts +164 -0
  69. package/src/scenarios/registry-public.test.ts +131 -0
  70. package/src/scenarios/replay-llm-cache-key.test.ts +35 -0
  71. package/src/scenarios/replay-retention-expiry.test.ts +178 -0
  72. package/src/scenarios/restart-during-run.test.ts +177 -0
  73. package/src/scenarios/spec-corpus-validity.test.ts +54 -26
  74. package/src/scenarios/staleClaim.test.ts +3 -0
  75. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +67 -10
  76. package/src/scenarios/wasm-pack-memory-cap.test.ts +64 -9
  77. package/src/scenarios/webhook-negative.test.ts +90 -0
  78. package/src/scenarios/webhook-signed-delivery.test.ts +178 -0
  79. package/src/setup.ts +25 -1
  80. package/vitest.config.ts +5 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # `@openwop/openwop-conformance` Changelog
2
+
3
+ ## [1.0.0] — 2026-05-10
4
+
5
+ Reset to the OpenWOP v1.0 production-release baseline.
6
+
7
+ ### What's covered
8
+
9
+ - Server-free spec-corpus validation across JSON Schemas, OpenAPI, AsyncAPI, REST endpoint docs, fixture docs, SDK helper surfaces, and TypeScript publish artifacts.
10
+ - Black-box scenarios for discovery, workflow listing, run lifecycle, events, interrupts, cancellation, replay/fork behavior, idempotency, concurrency, malicious manifests, and route coverage.
11
+ - Packaged API contracts (`api/`, `schemas/`, fixtures, and coverage docs) so installed conformance runs do not depend on a repository checkout.
12
+ - Production metadata gates for package names, licenses, repository URLs, stale import paths, and v1.0 release posture.
13
+
14
+ ### v1.x additions
15
+
16
+ - Reference deployment compatibility matrix automation.
17
+ - Optional server-required scenario bundles for deployment-specific auth and credential profile checks.
package/README.md CHANGED
@@ -56,17 +56,43 @@ npm install
56
56
  export OPENWOP_BASE_URL="https://api.example.com"
57
57
  export OPENWOP_API_KEY="hk_test_..."
58
58
 
59
- npx vitest run # full suite
59
+ npx vitest run # full suite (parallel files, ~95s)
60
60
  npx vitest run src/scenarios/discovery.test.ts # single file
61
+ npm run test:strict # full suite, no-file-parallelism
61
62
  ```
62
63
 
64
+ **`test:strict` vs `test`.** The default `test` script runs files in parallel (vitest default, ~3-5× faster). Most scenarios are isolation-safe at that level. Two exceptions document `--no-file-parallelism` as their canonical execution mode:
65
+
66
+ - **`production-backpressure.test.ts`** — saturates the host's `inflightCap`; under parallel execution, neighbor tests posting `/v1/runs` during the saturation window see a 503 from the cap (cap-collateral). The scenario soft-skips its envelope assertions when this happens (logs a warning); `test:strict` exercises the full envelope contract.
67
+ - **OTel scenarios** (`otel-emission.test.ts`, `otel-trace-propagation.test.ts`, `metric-emission.test.ts`) — each vitest worker spawns its own collector and only one can bind the configured OTLP port; concurrent file execution causes ephemeral-port fallbacks that don't receive the host's traffic.
68
+
69
+ Run `npm run test` for normal CI cadence; `npm run test:strict` when claiming full envelope coverage for production-profile + OTel claims.
70
+
71
+
72
+ ### Optional environment flags
73
+
74
+ | Variable | Effect |
75
+ |---|---|
76
+ | `OPENWOP_REQUIRE_BEHAVIOR=true` | Capability-gated scenarios (audit-log integrity, rate-limit envelope, multi-region idempotency, `configurableSchema`, webhook sig versioning, etc.) FAIL instead of skipping when the host doesn't advertise the profile. Lets a host claim "full coverage" mechanically. See [`coverage.md`](./coverage.md) §"Capability-gated scenarios". |
77
+ | `OPENWOP_TEST_PUBLIC_REGISTRY=true` | Runs `registry-public.test.ts` against the hosted registry at `packs.openwop.dev`. Skipped by default so the suite doesn't depend on outbound connectivity. |
78
+ | `OPENWOP_OTEL_COLLECTOR=true` | Boots the in-suite OTLP collector for `otel-emission.test.ts`, `otel-trace-propagation.test.ts`, `metric-emission.test.ts`, and `otel-emission-grpc.test.ts`. The collector accepts OTLP/HTTP-JSON (`application/json`), OTLP/HTTP-protobuf (`application/x-protobuf` — hand-rolled decoder at `src/lib/otlp-protobuf.ts`), and OTLP/gRPC (when `OPENWOP_OTEL_COLLECTOR_GRPC=true` — h2c HTTP/2 + hand-rolled framing at `src/lib/grpc-framing.ts`). Zero new npm deps. Hosts may emit via `OTEL_EXPORTER_OTLP_PROTOCOL=http/json`, `http/protobuf`, or `grpc`. Skipped by default. **Run OTel scenarios with `--no-file-parallelism`** — each vitest worker spawns its own collector and only one can bind the same port, so concurrent file execution causes ephemeral-port fallbacks that don't receive the host's OTLP traffic. |
79
+ | `OPENWOP_OTEL_COLLECTOR_GRPC=true` | Boots the parallel OTLP/gRPC collector alongside the HTTP one (h2c HTTP/2 on a separate port). Shares the same `spans()` + `metrics()` store; spans captured over either transport surface in `getCollector().spans()`. Requires `OPENWOP_OTEL_COLLECTOR=true`. Same `--no-file-parallelism` requirement applies. |
80
+ | `OPENWOP_OTEL_COLLECTOR_GRPC_PORT=4317` | Bind the OTLP/gRPC collector on a specific port (default `4317`, OTLP/gRPC convention). The host MUST be configured with `OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:<port>` AND `OTEL_EXPORTER_OTLP_PROTOCOL=grpc`. |
81
+ | `OPENWOP_OTEL_COLLECTOR_PORT=14318` | Bind the OTel collector on a specific port (default `4318`). The host MUST be configured with `OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:<port>`. |
82
+ | `OPENWOP_WEBHOOK_ALLOW_PRIVATE=true` | Hosts implementing the webhook SSRF guard (rejecting loopback / RFC1918 / link-local destinations) MUST advertise this flag for the loopback test receiver in `webhook-signed-delivery.test.ts` to be accepted. The SQLite reference host honors this env var; the scenario soft-skips when the host rejects the URL. |
83
+ | `OPENWOP_MCP_FAKE_SERVER=true` | Boots the synthetic MCP peer for `mcp-tool-roundtrip.test.ts`. |
84
+ | `OPENWOP_MCP_REAL_SERVER_URL=<base-url>` | Points the MCP wire-shape probe at a real MCP server. The probe POSTs JSON-RPC and reads a single-JSON response — matches MCP's `streamable-http` transport in single-response mode. **Does NOT support** stdio transport (which is what most `modelcontextprotocol/servers` references default to) or SSE-streamed responses; an operator collecting interop evidence today runs a custom `StreamableHTTPServerTransport`-style server that returns a single JSON body per request. Adding SSE-frame parsing is tracked in `docs/PROTOCOL-GAP-CLOSURE-PLAN.md` Track 6. Assertions relax to shape-only. When both this and `OPENWOP_MCP_FAKE_SERVER` are set, the real URL wins. Phase 3 T3.4 interop-evidence path. |
85
+ | `OPENWOP_A2A_FAKE_PEER=true` | Boots the synthetic A2A peer for `a2a-task-roundtrip.test.ts`. |
86
+ | `OPENWOP_A2A_REAL_PEER_URL=<base-url>` | Points the A2A AgentCard + task-lifecycle probe at a real reference A2A peer. Drift-point subtests (AUTH_REQUIRED / REJECTED) stay fake-peer-only — real peers don't expose a state-forcing API. Phase 3 T3.4 interop-evidence path. |
87
+ | `OPENWOP_FORCE_RATE_LIMIT=true` | Signals the host (test-only key) to fabricate a 429 so `rate-limit-envelope.test.ts` can exercise envelope shape deterministically. |
88
+
63
89
  Exit code is non-zero on any failed assertion.
64
90
 
65
91
  ---
66
92
 
67
93
  ## What's Covered
68
94
 
69
- The current suite has 85 scenario files under `src/scenarios/`. This includes 18 Multi-Agent Shift scenarios (Phases 1-5) added 2026-05-10. The maintained scenario-to-spec map lives in [`coverage.md`](./coverage.md); this README keeps the operator quickstart and the historical scenario notes below.
95
+ The current suite has 103 scenario files under `src/scenarios/`. This includes 18 Multi-Agent Shift scenarios (Phases 1-5) added 2026-05-10, the `registry-public.test.ts` public-registry healthcheck added 2026-05-11 (opt-in via `OPENWOP_TEST_PUBLIC_REGISTRY=true`), the `replay-llm-cache-key.test.ts` placeholder added 2026-05-11 (three `it.todo()` cases for the cross-host LLM cache-key recipe per `replay.md` §"LLM cache-key recipe"), the two `production-*.test.ts` scenarios added 2026-05-11 for the `openwop-production` profile per RFC 0009 (`production-backpressure.test.ts`, `production-retention-expiry.test.ts`), the four `auth-*.test.ts` scenarios added 2026-05-11/12 for the production-auth profiles per RFC 0010 (`auth-api-key-rotation.test.ts`, `auth-oauth2-client-credentials.test.ts`, `auth-oidc-user-bearer.test.ts`, `auth-mtls.test.ts` (opt-in via `OPENWOP_TEST_MTLS=1`)), `replay-retention-expiry.test.ts` added 2026-05-12 (capability shape + 410/422 envelope per `replay.md` §"Retention and garbage collection"), `bulk-cancel.test.ts` added 2026-05-12 (Phase B close-out of R1 — `POST /v1/runs:bulk-cancel`), the two Phase H launch-blocker advertisement-contract scenarios added 2026-05-12 (`mcp-toolcall-redaction.test.ts` for the MCP-1 invariant per `host-capabilities.md §host.mcp` + `threat-model-prompt-injection.md §UNTRUSTED`, and `http-client-ssrf.test.ts` for the SSRF + body-size cap advertisement contract on `capabilities.httpClient`), and the `wasm-pack-abi-version-rejection.test.ts` Track 7 scenario added 2026-05-12 for the ABI-mismatch positive path via the `vendor.openwop.misbehaving-abi` pack per RFC 0008 §H. The maintained scenario-to-spec map lives in [`coverage.md`](./coverage.md); this README keeps the operator quickstart and the historical scenario notes below.
70
96
 
71
97
  High-level coverage includes:
72
98
 
@@ -145,7 +171,7 @@ Server-required (added in 1.7.0):
145
171
  |---|---|---|
146
172
  | **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
147
173
 
148
- Current source tree: 85 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
174
+ Current source tree: 103 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
149
175
 
150
176
  ## Remaining Gaps
151
177
 
@@ -155,9 +181,8 @@ Current source tree: 85 scenario files. Use [`coverage.md`](./coverage.md) for c
155
181
  | `messages` mode AI chunks | Needs server-side AI provider mock (fixture spec gap F1 in `fixtures.md`). |
156
182
  | Cross-version compat | Needs server-controllable `engineVersion` cycle to test forward-fold-best-effort. |
157
183
  | Capability-limit fixtures | Needs fixtures that deliberately exceed `clarificationRounds` / `schemaRounds` / `envelopesPerTurn` to assert `cap.breached` shape beyond the shipped node-execution cap case. |
158
- | Auth profiles | Needs API-key rotation, OAuth2 client-credentials, and mTLS scenarios for hosts that claim those profiles. |
159
- | Interrupt profiles | Needs quorum approval, auth-required resume, external-event correlation, and parent/child cancel cascade scenarios. |
160
- | Production profile | Needs backpressure envelope, restart durability, retention, and debug-bundle truncation scenarios. |
184
+ | Auth profiles | Capability-shape scenarios for `openwop-auth-api-key-rotation`, `openwop-auth-oauth2-client-credentials`, `openwop-auth-oidc-user-bearer`, and `openwop-auth-mtls` (opt-in) shipped 2026-05-11/12 under RFC 0010, with a synthetic OIDC issuer harness at `conformance/src/lib/oidc-issuer.ts`. Remaining: live-IdP positive-path validation against at least one reference host. |
185
+ | Production profile | Capability-shape scenarios shipped 2026-05-11 (`production-backpressure.test.ts`, `production-retention-expiry.test.ts`) under RFC 0009; durability + debug-bundle truncation predicates covered by re-labeled `restart-during-run.test.ts`, `staleClaim.test.ts`, `debug-bundle-truncation.test.ts`. Remaining: end-to-end behavior validation against a host advertising `capabilities.production.supported: true` under `OPENWOP_REQUIRE_BEHAVIOR=true`. |
161
186
 
162
187
  ---
163
188
 
@@ -0,0 +1,251 @@
1
+ // openwop v1 — gRPC service definition (optional alternative to REST + SSE).
2
+ //
3
+ // Status: FINAL v1 (2026-05-12). Closes R3 in rest-endpoints.md
4
+ // §"Open spec gaps". Hosts that advertise `capabilities.grpc.supported:
5
+ // true` expose this service in addition to (NOT instead of) the
6
+ // REST + SSE surface. Per-method semantics are normatively defined by
7
+ // the corresponding REST operation in `api/openapi.yaml`; this file
8
+ // supplies the wire-shape only.
9
+ //
10
+ // Service-name canonical: openwop.v1.Engine (one service per major).
11
+ // Method names: PascalCase form of `api/openapi.yaml` operationIds.
12
+ // Error mapping: see `spec/v1/grpc-transport.md` §"Error mapping".
13
+ //
14
+ // Message shapes mirror the JSON Schemas — full shape definitions are
15
+ // abridged here for readability; reference impls SHOULD `import` each
16
+ // JSON Schema's corresponding `.proto` snippet from a paired
17
+ // `schemas/grpc/<name>.proto` once those land (deferred to v1.x). For
18
+ // the v1 reference, hosts MAY hand-roll the messages from the JSON
19
+ // Schema definitions; the wire surface is byte-equivalent across both
20
+ // projections.
21
+
22
+ syntax = "proto3";
23
+
24
+ package openwop.v1;
25
+
26
+ import "google/protobuf/empty.proto";
27
+ import "google/protobuf/struct.proto";
28
+ import "google/protobuf/timestamp.proto";
29
+
30
+ option go_package = "github.com/openwop/openwop/api/grpc/openwopv1";
31
+ option java_package = "dev.openwop.api.v1";
32
+ option csharp_namespace = "Openwop.V1";
33
+
34
+ // ─────────────────────────────────────────────────────────────────────
35
+ // Service — one per protocol major version.
36
+ // ─────────────────────────────────────────────────────────────────────
37
+ service Engine {
38
+ // Discovery — 1:1 with REST `GET /.well-known/openwop`.
39
+ // Unauthenticated.
40
+ rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse);
41
+
42
+ // Workflow + Run lifecycle.
43
+ rpc GetWorkflow(GetWorkflowRequest) returns (GetWorkflowResponse);
44
+ rpc CreateRun(CreateRunRequest) returns (CreateRunResponse);
45
+ rpc GetRun(GetRunRequest) returns (GetRunResponse);
46
+ rpc CancelRun(CancelRunRequest) returns (CancelRunResponse);
47
+ rpc BulkCancelRuns(BulkCancelRunsRequest) returns (BulkCancelRunsResponse);
48
+ rpc ForkRun(ForkRunRequest) returns (ForkRunResponse);
49
+ rpc PauseRun(PauseRunRequest) returns (PauseRunResponse);
50
+ rpc ResumeRun(ResumeRunRequest) returns (ResumeRunResponse);
51
+
52
+ // Events — server-streaming replaces SSE.
53
+ rpc StreamRunEvents(StreamRunEventsRequest) returns (stream RunEventEnvelope);
54
+
55
+ // HITL — interrupt resolution.
56
+ rpc ResolveInterruptByRun(ResolveInterruptByRunRequest) returns (ResolveInterruptResponse);
57
+ rpc ResolveInterruptByToken(ResolveInterruptByTokenRequest) returns (ResolveInterruptResponse);
58
+ rpc InspectInterruptByToken(InspectInterruptByTokenRequest) returns (InspectInterruptResponse);
59
+
60
+ // Artifacts + Webhooks + Audit verify.
61
+ rpc GetArtifact(GetArtifactRequest) returns (GetArtifactResponse);
62
+ rpc RegisterWebhook(RegisterWebhookRequest) returns (RegisterWebhookResponse);
63
+ rpc UnregisterWebhook(UnregisterWebhookRequest) returns (google.protobuf.Empty);
64
+ rpc VerifyAuditLog(VerifyAuditLogRequest) returns (VerifyAuditLogResponse);
65
+ }
66
+
67
+ // ─────────────────────────────────────────────────────────────────────
68
+ // Messages — abridged. Each message mirrors the corresponding REST
69
+ // request/response body; field shapes use Struct for sub-trees that
70
+ // don't yet have hand-rolled Protobuf representations. A future v1.x
71
+ // minor expands every Struct into its typed Protobuf message.
72
+ // ─────────────────────────────────────────────────────────────────────
73
+
74
+ message GetCapabilitiesRequest {}
75
+
76
+ message GetCapabilitiesResponse {
77
+ // Body matches schemas/capabilities.schema.json.
78
+ google.protobuf.Struct capabilities = 1;
79
+ }
80
+
81
+ message GetWorkflowRequest {
82
+ string workflow_id = 1;
83
+ }
84
+
85
+ message GetWorkflowResponse {
86
+ // Body matches schemas/workflow-definition.schema.json.
87
+ google.protobuf.Struct workflow = 1;
88
+ }
89
+
90
+ message CreateRunRequest {
91
+ string workflow_id = 1;
92
+ google.protobuf.Struct inputs = 2; // schemas/run-options.schema.json §inputs
93
+ google.protobuf.Struct configurable = 3; // schemas/run-options.schema.json §configurable
94
+ repeated string tags = 4;
95
+ google.protobuf.Struct metadata = 5;
96
+ string idempotency_key = 6; // Idempotency-Key, optional
97
+ }
98
+
99
+ message CreateRunResponse {
100
+ string run_id = 1;
101
+ string status = 2;
102
+ }
103
+
104
+ message GetRunRequest {
105
+ string run_id = 1;
106
+ }
107
+
108
+ message GetRunResponse {
109
+ // Body matches schemas/run-snapshot.schema.json.
110
+ google.protobuf.Struct snapshot = 1;
111
+ }
112
+
113
+ message CancelRunRequest {
114
+ string run_id = 1;
115
+ string reason = 2;
116
+ }
117
+
118
+ message CancelRunResponse {
119
+ string run_id = 1;
120
+ string status = 2;
121
+ }
122
+
123
+ message BulkCancelRunsRequest {
124
+ repeated string run_ids = 1;
125
+ string reason = 2;
126
+ }
127
+
128
+ message BulkCancelRunResult {
129
+ string run_id = 1;
130
+ bool ok = 2;
131
+ string status = 3; // "cancelled" / "cancelling" when ok=true
132
+ google.protobuf.Struct error = 4; // error-envelope.schema.json when ok=false
133
+ }
134
+
135
+ message BulkCancelRunsResponse {
136
+ repeated BulkCancelRunResult results = 1;
137
+ }
138
+
139
+ message ForkRunRequest {
140
+ string run_id = 1;
141
+ int64 from_seq = 2;
142
+ string mode = 3; // "replay" | "branch"
143
+ google.protobuf.Struct overlay = 4; // optional run-options overlay
144
+ }
145
+
146
+ message ForkRunResponse {
147
+ string forked_run_id = 1;
148
+ string status = 2;
149
+ }
150
+
151
+ message PauseRunRequest {
152
+ string run_id = 1;
153
+ string reason = 2;
154
+ string drain_policy = 3; // "immediate" | "drain-current-node"
155
+ }
156
+
157
+ message PauseRunResponse {
158
+ string run_id = 1;
159
+ string status = 2;
160
+ google.protobuf.Timestamp paused_at = 3;
161
+ }
162
+
163
+ message ResumeRunRequest {
164
+ string run_id = 1;
165
+ string reason = 2;
166
+ }
167
+
168
+ message ResumeRunResponse {
169
+ string run_id = 1;
170
+ string status = 2;
171
+ google.protobuf.Timestamp resumed_at = 3;
172
+ }
173
+
174
+ message StreamRunEventsRequest {
175
+ string run_id = 1;
176
+ int64 last_sequence = 2; // -1 = from start
177
+ repeated string stream_modes = 3; // subset of stream-modes.md
178
+ int32 buffer_ms = 4;
179
+ }
180
+
181
+ message RunEventEnvelope {
182
+ string event_id = 1;
183
+ int64 seq = 2;
184
+ string run_id = 3;
185
+ string type = 4;
186
+ string node_id = 5;
187
+ google.protobuf.Struct payload = 6; // run-event-payloads.schema.json §<type>
188
+ google.protobuf.Timestamp timestamp = 7;
189
+ string causation_id = 8; // optional, run-event.schema.json §causationId
190
+ }
191
+
192
+ message ResolveInterruptByRunRequest {
193
+ string run_id = 1;
194
+ string node_id = 2;
195
+ google.protobuf.Struct resume_value = 3; // shape per InterruptPayload.resumeSchema
196
+ }
197
+
198
+ message ResolveInterruptByTokenRequest {
199
+ string token = 1;
200
+ google.protobuf.Struct resume_value = 2;
201
+ }
202
+
203
+ message InspectInterruptByTokenRequest {
204
+ string token = 1;
205
+ }
206
+
207
+ message ResolveInterruptResponse {
208
+ string run_id = 1;
209
+ string node_id = 2;
210
+ string status = 3;
211
+ }
212
+
213
+ message InspectInterruptResponse {
214
+ // Body matches schemas/suspend-request.schema.json.
215
+ google.protobuf.Struct interrupt = 1;
216
+ }
217
+
218
+ message GetArtifactRequest {
219
+ string run_id = 1;
220
+ string artifact_id = 2;
221
+ }
222
+
223
+ message GetArtifactResponse {
224
+ string artifact_id = 1;
225
+ string content_type = 2;
226
+ bytes content = 3;
227
+ }
228
+
229
+ message RegisterWebhookRequest {
230
+ string url = 1;
231
+ repeated string event_types = 2;
232
+ string secret = 3; // operator-supplied HMAC secret
233
+ }
234
+
235
+ message RegisterWebhookResponse {
236
+ string webhook_id = 1;
237
+ }
238
+
239
+ message UnregisterWebhookRequest {
240
+ string webhook_id = 1;
241
+ }
242
+
243
+ message VerifyAuditLogRequest {
244
+ int64 from_seq = 1;
245
+ int64 to_seq = 2;
246
+ }
247
+
248
+ message VerifyAuditLogResponse {
249
+ // Body matches schemas/audit-verify-result.schema.json.
250
+ google.protobuf.Struct result = 1;
251
+ }
package/api/openapi.yaml CHANGED
@@ -57,6 +57,8 @@ tags:
57
57
  description: Run-produced artifacts.
58
58
  - name: webhooks
59
59
  description: Subscribe to run events via outbound HTTP.
60
+ - name: audit
61
+ description: Audit-log integrity verification (gated on the `openwop-audit-log-integrity` profile).
60
62
 
61
63
  # ─────────────────────────────────────────────────────────────────────────────
62
64
  # PATHS
@@ -350,6 +352,62 @@ paths:
350
352
  '403': { $ref: '#/components/responses/Forbidden' }
351
353
  '404': { $ref: '#/components/responses/NotFound' }
352
354
 
355
+ /v1/runs:bulk-cancel:
356
+ post:
357
+ tags: [runs]
358
+ summary: Cancel a set of in-flight runs in a single request.
359
+ description: |
360
+ Per `spec/v1/rest-endpoints.md` §"POST /v1/runs:bulk-cancel". Accepts
361
+ a non-empty array of runIds and processes each cancellation
362
+ independently. Returns `200` with a per-id results array even when
363
+ some individual cancellations fail; the top-level operation succeeds
364
+ when the request reached the host, regardless of per-id outcomes.
365
+ Hosts enforce a host-defined cap on the array length (RECOMMENDED
366
+ 100); over-cap requests return `400 validation_error`.
367
+ operationId: bulkCancelRuns
368
+ parameters:
369
+ - $ref: '#/components/parameters/IdempotencyKey'
370
+ requestBody:
371
+ required: true
372
+ content:
373
+ application/json:
374
+ schema:
375
+ type: object
376
+ required: [runIds]
377
+ properties:
378
+ runIds:
379
+ type: array
380
+ minItems: 1
381
+ maxItems: 100
382
+ items: { type: string, minLength: 1, maxLength: 128 }
383
+ reason: { type: string, maxLength: 512 }
384
+ additionalProperties: false
385
+ responses:
386
+ '200':
387
+ description: Per-id cancel results.
388
+ content:
389
+ application/json:
390
+ schema:
391
+ type: object
392
+ required: [results]
393
+ properties:
394
+ results:
395
+ type: array
396
+ items:
397
+ type: object
398
+ required: [runId, ok]
399
+ properties:
400
+ runId: { type: string, minLength: 1 }
401
+ ok: { type: boolean }
402
+ status: { type: string, enum: [cancelled, cancelling] }
403
+ error:
404
+ $ref: '../schemas/error-envelope.schema.json'
405
+ additionalProperties: false
406
+ additionalProperties: false
407
+ '400': { $ref: '#/components/responses/ValidationError' }
408
+ '401': { $ref: '#/components/responses/Unauthenticated' }
409
+ '403': { $ref: '#/components/responses/Forbidden' }
410
+
353
411
  /v1/runs/{runId}:fork:
354
412
  post:
355
413
  tags: [runs]
@@ -364,12 +422,16 @@ paths:
364
422
  application/json:
365
423
  schema:
366
424
  type: object
367
- required: [fromSeq, mode]
425
+ required: [mode]
368
426
  properties:
369
427
  fromSeq:
370
428
  type: integer
371
429
  minimum: 0
372
- description: Inclusive — events `< fromSeq` are fixed history; `>= fromSeq` are re-executed.
430
+ description: |
431
+ Inclusive — events `< fromSeq` are fixed history; `>= fromSeq` are re-executed.
432
+ Required for `branch` (the branch point). Optional for `replay`; when omitted,
433
+ defaults to `0` (full re-execution from source-run start) per `replay.md`
434
+ §"Replay-mode defaults".
373
435
  mode:
374
436
  type: string
375
437
  enum: [replay, branch]
@@ -684,6 +746,50 @@ paths:
684
746
  '403': { $ref: '#/components/responses/Forbidden' }
685
747
  '404': { $ref: '#/components/responses/NotFound' }
686
748
 
749
+ # ── Audit-log integrity verification (gated on profile) ────────────────
750
+ /v1/audit/verify:
751
+ get:
752
+ tags: [audit]
753
+ summary: Verify the audit-log hash chain over [fromSeq, toSeq].
754
+ description: |
755
+ Per `spec/v1/auth-profiles.md` §`openwop-audit-log-integrity` §4. The
756
+ verifier re-walks audit-log entries in the requested range,
757
+ re-computes each entry's `prevHash` from the canonical RFC 8785 JCS
758
+ serialization of the prior entry, verifies signed checkpoints
759
+ against the host's advertised `auditLogIntegrity.checkpointPublicKey`,
760
+ and returns `chainValid` + an enumeration of any anomalies.
761
+ Hosts MUST require the `audit:read` scope. Hosts that do NOT
762
+ advertise the `openwop-audit-log-integrity` profile MAY omit this
763
+ endpoint entirely (clients SHOULD pre-flight via `/.well-known/openwop`).
764
+ operationId: verifyAuditLog
765
+ parameters:
766
+ - in: query
767
+ name: fromSeq
768
+ required: true
769
+ schema: { type: integer, minimum: 0 }
770
+ description: First audit-log sequence to include (inclusive).
771
+ - in: query
772
+ name: toSeq
773
+ required: true
774
+ schema: { type: integer, minimum: 0 }
775
+ description: Last audit-log sequence to include (inclusive). MUST be >= fromSeq.
776
+ responses:
777
+ '200':
778
+ description: Verification result.
779
+ content:
780
+ application/json:
781
+ schema:
782
+ $ref: '../schemas/audit-verify-result.schema.json'
783
+ '400': { $ref: '#/components/responses/ValidationError' }
784
+ '401': { $ref: '#/components/responses/Unauthenticated' }
785
+ '403': { $ref: '#/components/responses/Forbidden' }
786
+ '404':
787
+ description: Host does not advertise the audit-log-integrity profile.
788
+ content:
789
+ application/json:
790
+ schema:
791
+ $ref: '../schemas/error-envelope.schema.json'
792
+
687
793
  # ─────────────────────────────────────────────────────────────────────────────
688
794
  # COMPONENTS
689
795
  # ─────────────────────────────────────────────────────────────────────────────
@@ -698,7 +804,7 @@ components:
698
804
  openwop API key. Format implementation-defined; reference impl uses `hk_`/`hk_test_` prefixes.
699
805
  Each key carries one or more scopes from the canonical vocabulary
700
806
  (`manifest:read`, `runs:create`, `runs:read`, `runs:cancel`,
701
- `artifacts:read`, `webhooks:manage`, `approvals:respond`).
807
+ `artifacts:read`, `webhooks:manage`, `approvals:respond`, `audit:read`).
702
808
  See `auth.md`.
703
809
 
704
810
  parameters:
package/coverage.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # OpenWOP Conformance Coverage Map
2
2
 
3
- > **Status: Living document. Updated 2026-05-10.** This map connects the current scenario files to the protocol surfaces they protect and records the remaining gaps from the protocol deep dive. Scenario names are source-of-truth file names under `conformance/src/scenarios/`.
3
+ > **Status: Living document. Updated 2026-05-11.** This map connects the current scenario files to the protocol surfaces they protect and records the remaining gaps from the protocol deep dive. Scenario names are source-of-truth file names under `conformance/src/scenarios/`.
4
+
5
+ > **Shape grade vs behavior grade.** Some optional-profile scenarios validate **capability shape** (the host's discovery advertisement is well-formed) without yet exercising **behavior** (the host actually implements the profile end-to-end). The "Current grade" column reflects shape; see §"Capability-gated scenarios: shape vs behavior" below for the dual-grade view and the `OPENWOP_REQUIRE_BEHAVIOR=true` strict-mode runner flag.
4
6
 
5
7
  ---
6
8
 
@@ -8,27 +10,62 @@
8
10
 
9
11
  | Surface | Scenario files | Current grade | Remaining gaps |
10
12
  |---|---|---|---|
11
- | Discovery and capability handshake | `discovery.test.ts`, `runtime-capabilities.test.ts`, `profileDerivation.test.ts`, `mcp-discoverability.test.ts` | A | `Capabilities-Etag` optional runtime shape is covered; scoped discovery and non-HTTP handoff remain host-advertised follow-ups. |
12
- | Auth and errors | `auth.test.ts`, `errors.test.ts`, `policies.test.ts`, `providerPolicyEnforcement.test.ts` | B | OAuth2, API-key rotation, mTLS, richer scope matrix. |
13
- | Run lifecycle | `runs-lifecycle.test.ts`, `failure-path.test.ts`, `cancellation.test.ts`, `eventOrdering.test.ts` | A- | Restart-during-run production scenario. |
13
+ | Discovery and capability handshake | `discovery.test.ts` (incl. RFC 0011 auth-scoped subtests), `runtime-capabilities.test.ts`, `profileDerivation.test.ts`, `mcp-discoverability.test.ts` | A | `Capabilities-Etag` optional runtime shape covered; auth-scoped discovery covered under `openwop-discovery-auth-scoped` (RFC 0011); non-HTTP handoff remains host-advertised follow-up. |
14
+ | Auth and errors | `auth.test.ts`, `errors.test.ts`, `policies.test.ts`, `providerPolicyEnforcement.test.ts`, `auth-api-key-rotation.test.ts`, `auth-oauth2-client-credentials.test.ts`, `auth-oidc-user-bearer.test.ts` | A− | Three auth-profile capability-shape + negative-case scenarios shipped under RFC 0010 (capability-gated). Remaining: live-IdP positive-path validation + opt-in mTLS scenario + richer scope matrix. |
15
+ | Run lifecycle | `runs-lifecycle.test.ts`, `failure-path.test.ts`, `cancellation.test.ts`, `eventOrdering.test.ts`, `restart-during-run.test.ts` | A | Restart-during-run scenario shipped; gated under `openwop-production` profile (RFC 0009). |
14
16
  | Idempotency and retry | `idempotency.test.ts`, `idempotencyRetry.test.ts`, `highConcurrency.test.ts` | A- | Long retention proof beyond the fast CI window. |
15
17
  | Interrupts | `interrupt-approval.test.ts`, `interrupt-clarification.test.ts`, `approval-payload.test.ts`, `interruptRace.test.ts`, `interrupt-quorum-resolution.test.ts`, `interrupt-external-event-correlation.test.ts`, `interrupt-auth-required-resume.test.ts`, `interrupt-parent-child-cascade.test.ts` | A− | All four optional profile scenarios landed 2026-05-10. Remaining: positive end-to-end run against a host that advertises every profile. |
16
18
  | Streaming | `stream-modes.test.ts`, `stream-modes-buffer.test.ts`, `stream-modes-mixed.test.ts`, `streamReconnect.test.ts` | A | Browser/proxy timeout matrix and long-running stream soak. |
17
- | Replay and fork | `replay-fork.test.ts`, `replayDeterminism.test.ts`, `staleClaim.test.ts` | A- | Fork from arbitrary event types and retention-expiry behavior remain uncovered; retention/privacy/scoring semantics are now specified in `replay.md`. |
19
+ | Replay and fork | `replay-fork.test.ts`, `replay-fork-arbitrary.test.ts`, `replay-retention-expiry.test.ts`, `replayDeterminism.test.ts`, `staleClaim.test.ts` | A | Arbitrary-event fork shipped (`replay-fork-arbitrary.test.ts`). Retention-expiry envelope shipped (`replay-retention-expiry.test.ts`) gated on `OPENWOP_TEST_EXPIRED_REPLAY_RUN_ID` because hosts don't standardize a force-expire endpoint. Retention/privacy/scoring semantics specified in `replay.md`. |
18
20
  | Capabilities and limits | `cap-breach.test.ts`, `dispatchLoop.test.ts` | B+ | Clarification/schema/envelope cap-breach fixtures beyond node-execution cap. |
19
21
  | State channels and reducers | `channel-ttl.test.ts` | B+ | Cross-adapter reducer consistency and conflict cases. |
20
22
  | Sub-workflows and dispatch | `subworkflow.test.ts`, `multi-node-ordering.test.ts` | B+ | Parallel fan-out floors by scale tier, parent/child cancellation. |
21
- | Node packs and registry | `pack-registry.test.ts`, `pack-registry-publish.test.ts`, `maliciousManifest.test.ts`, `wasm-pack-load.test.ts`, `wasm-pack-invoke-completed.test.ts`, `wasm-pack-invoke-suspended.test.ts`, `wasm-pack-replay-determinism.test.ts`, `wasm-pack-memory-cap.test.ts`, `wasm-pack-abi-version-rejection.test.ts` | A | RFC 0008 WASM ABI scenarios landed 2026-05-10; gated on `capabilities.nodePackRuntimes.wasm.supported`. Remaining: hosted registry interoperability once `packs.openwop.dev` exists; deliberately-misbehaving pack for memory-cap + ABI-version-rejection positive paths. |
23
+ | Node packs and registry | `pack-registry.test.ts`, `pack-registry-publish.test.ts`, `maliciousManifest.test.ts`, `wasm-pack-load.test.ts`, `wasm-pack-invoke-completed.test.ts`, `wasm-pack-invoke-suspended.test.ts`, `wasm-pack-replay-determinism.test.ts`, `wasm-pack-memory-cap.test.ts`, `wasm-pack-abi-version-rejection.test.ts` | A | RFC 0008 WASM ABI scenarios landed 2026-05-10; gated on `capabilities.nodePackRuntimes.wasm.supported`. Memory-cap positive path closed 2026-05-12 via `examples/packs/rust-misbehaving-memory/` + in-memory loader's `memory.grow` probe + `capBreached.kind` schema extension. ABI-mismatch positive path closed 2026-05-12 via `examples/packs/rust-misbehaving-abi/` (declares ABI 999) + new `capabilities.nodePackRuntimes.wasm.loadedPacks[]` discovery field (rejected packs omitted). Remaining: hosted registry interoperability once `packs.openwop.dev` exists. |
22
24
  | Secrets and redaction | `redaction.test.ts`, `redactionAdversarial.test.ts`, `byok-roundtrip.test.ts` | A- | Cross-provider BYOK matrix and debug-bundle redaction under high volume. |
23
- | Observability and diagnostics | `cost-attribution.test.ts`, `debugBundle.test.ts`, `otel-emission.test.ts`, `otel-trace-propagation.test.ts` | B+ | OTLP/HTTP-JSON receiver harness now wired; opt-in via `OPENWOP_OTEL_COLLECTOR=true`. Remaining: real-OTLP-protobuf path, metric-emission scenario, debug-bundle truncation. |
25
+ | Observability and diagnostics | `cost-attribution.test.ts`, `debugBundle.test.ts`, `otel-emission.test.ts`, `otel-trace-propagation.test.ts`, `metric-emission.test.ts`, `otel-emission-grpc.test.ts` | A | OTLP collector accepts all three OTLP transports: HTTP-JSON (2026-05-11), HTTP-protobuf (2026-05-12), gRPC over h2c HTTP/2 (2026-05-12 — hand-rolled framing at `conformance/src/lib/grpc-framing.ts`). Zero new npm deps for any of them. Opt-in via `OPENWOP_OTEL_COLLECTOR=true` (+ `OPENWOP_OTEL_COLLECTOR_GRPC=true` for the gRPC variant). Hosts advertise supported transports via `capabilities.observability.otel.exportProtocols ⊆ {http/json, http/protobuf, grpc}`; conformance scenario gates on the array. |
24
26
  | Fixtures and corpus validity | `fixtures-valid.test.ts`, `fixtures-gating.test.ts`, `spec-corpus-validity.test.ts` | A | Keep fixture manifest synchronized as new optional profiles land. |
25
27
  | Run control — pause/resume | `pause-resume.test.ts` | B | Lifecycle + 409-on-non-paused covered; remaining: pause-during-suspend race, immediate-vs-drain-current-node policy assertion. |
26
28
  | Rate-limit envelope | `rate-limit-envelope.test.ts` | B− | Shape validation when 429 observed; remaining: deterministic 429-induction harness so the scenario reliably triggers under CI. |
27
29
  | Per-workflow `configurableSchema` | `configurable-schema.test.ts` | C+ | Negative validation covered; remaining: positive accepted-overlay scenario + `GET /v1/workflows/{id}` schema surface assertion. |
28
30
  | Append-reducer ordering | `append-ordering.test.ts` | B | Intra-engine sequence-order check; remaining: cross-engine ordering under a multi-engine fixture. |
29
- | Webhook signature algorithms | `webhook-sig-algorithm.test.ts` | C+ | Discovery shape covered; remaining: end-to-end signed delivery exercising `X-openwop-Signature-Algorithm: v1`. |
30
- | Audit-log integrity profile | `audit-log-integrity.test.ts` | C+ | Profile claim + `/v1/audit/verify` shape covered; remaining: tamper-detection scenario (requires admin access to host's audit store) + multi-checkpoint chain verification. |
31
+ | Webhook signature algorithms | `webhook-sig-algorithm.test.ts`, `webhook-signed-delivery.test.ts`, `webhook-negative.test.ts` | A− | Discovery shape covered; end-to-end signed delivery with HMAC verification (`webhook-signed-delivery`); negative paths: SSRF guard, validation, unknown-unregister (`webhook-negative`). Remaining: HMAC mismatch + replay-attack rejection on the receiver side (not host-testable from black-box). |
32
+ | Audit-log integrity profile | `audit-log-integrity.test.ts` | A− | Profile claim + `/v1/audit/verify` shape + checkpoint-signature verification; chain re-walk with `chainValid` and `checkpointsValid` bits. Tamper detection covered host-internally at `examples/hosts/sqlite/test/audit-tamper.test.ts` (mutate-entry + forge-signature paths). Remaining: cross-host checkpoint export so an out-of-band verifier can re-anchor against the same chain. |
31
33
  | Multi-region idempotency capability | `multi-region-idempotency.test.ts` | C | Discovery enum coverage; remaining: cross-region partition simulation (requires multi-region harness). |
34
+ | Public hosted registry (`packs.openwop.dev`) | `registry-public.test.ts` | A− | Discovery, index, and per-pack manifest assertions against the public registry. Opt-in via `OPENWOP_TEST_PUBLIC_REGISTRY=true` so default conformance runs don't depend on outbound `packs.openwop.dev` reachability. Remaining: tarball-fetch + signature-verify roundtrip. |
35
+
36
+ ---
37
+
38
+ ## Capability-gated scenarios: shape vs behavior
39
+
40
+ Seventeen scenarios (or scenario groups) validate optional profiles where the host's discovery advertisement is well-formed (shape grade) but no reference host yet implements the profile end-to-end (behavior grade is `host-pending`). Default suite runs skip these with a warning; set `OPENWOP_REQUIRE_BEHAVIOR=true` to convert skips into hard failures.
41
+
42
+ | Scenario | Profile / capability | Shape grade | Behavior grade | Behavior-unlock dependency |
43
+ |---|---|---|---|---|
44
+ | `audit-log-integrity.test.ts` | `openwop-audit-log-integrity` (`auth-profiles.md`) | A− (discovery + verify endpoint shape) | `host-pending` | Track 1.1 — SQLite host implements hash-chain + signed checkpoints |
45
+ | `rate-limit-envelope.test.ts` | normative `429` envelope (`rest-endpoints.md`) | B− (observational — checks shape when 429 fires) | `host-pending` | Deterministic 429-induction harness (e.g., `OPENWOP_FORCE_RATE_LIMIT=true` on a test-only key) |
46
+ | `multi-region-idempotency.test.ts` | `capabilities.idempotency.crossRegion` (`idempotency.md`) | C (enum shape only) | `host-pending` | Multi-region host fixture; cross-region partition simulation |
47
+ | `configurable-schema.test.ts` | per-workflow `configurableSchema` (`run-options.md`) | C+ (negative validation) | `host-pending` | Positive accepted-overlay scenario + `GET /v1/workflows/{id}` schema surface |
48
+ | `webhook-sig-algorithm.test.ts` | `X-openwop-Signature-Algorithm: v1` (`webhooks.md`) | C+ (discovery shape) | `host-pending` | End-to-end signed delivery against a test receiver |
49
+ | `pause-resume.test.ts` | `pauseRun` / `resumeRun` lifecycle (`rest-endpoints.md`) | B (lifecycle + 409-on-non-paused) | partial | Pause-during-suspend race; immediate-vs-drain policy assertion |
50
+ | `append-ordering.test.ts` | `append` reducer ordering (`channels-and-reducers.md`) | B (intra-engine) | partial | Cross-engine multi-engine fixture |
51
+ | `otel-emission.test.ts`, `otel-emission-grpc.test.ts` | `openwop.*` OTel spans (`observability.md`) | A (OTLP/HTTP-JSON + OTLP/HTTP-protobuf + OTLP/gRPC all accepted; collector content-type-routes JSON/protobuf and the gRPC HTTP/2 path frame-decodes via `grpc-framing.ts`) | full | Cross-host trace-context propagation across `core.subWorkflow` (filed under `otel-trace-propagation.test.ts` Remaining) |
52
+ | `otel-trace-propagation.test.ts` | W3C trace-context propagation (`observability.md`) | B (trace continuity across `runs:fork` + interrupt resolve) | partial | Cross-host propagation across `core.subWorkflow` invocation |
53
+ | `wasm-pack-*.test.ts` (six scenarios) | `capabilities.nodePackRuntimes.wasm` (`RFCS/0008`) | A (load + invoke + replay + memory-cap positive-path + ABI-mismatch positive-path) | full | Memory-cap positive path closed 2026-05-12 via misbehaving-memory pack + `memory.grow` probe in loader. ABI-mismatch positive path closed 2026-05-12 via misbehaving-abi pack + `loadedPacks[]` discovery field. |
54
+ | `production-backpressure.test.ts`, `production-retention-expiry.test.ts`, `restart-during-run.test.ts`, `staleClaim.test.ts`, `debug-bundle-truncation.test.ts`, `idempotency.test.ts`, `idempotencyRetry.test.ts` (seven scenarios) | `openwop-production` (`production-profile.md`, RFC 0009) | A− (capability shape + 503 envelope under saturation + discovery-exemption; durable-restart + debug-bundle-truncation predicates exercised end-to-end; retention-expiry envelope soft-skipped pending RFC 0009 Q#1) | host-pass | Postgres reference host advertises `capabilities.production.supported: true` since 2026-05-11 and passes all 11 assertions across the 5 non-opt-in scenarios under `OPENWOP_REQUIRE_BEHAVIOR=true` with `--no-file-parallelism` (the backpressure scenario saturates the inflight cap; parallel file execution collides with `idempotencyRetry.test.ts`'s burst). RFC 0009 unresolved questions #1 (force-expire endpoint normation) + #3 (inflightCap vs probing) gate the path to A. |
55
+ | `auth-api-key-rotation.test.ts` | `openwop-auth-api-key-rotation` (`auth-profiles.md`, RFC 0010) | B (capability shape + secondary-key overlap when env-supplied + canary-redaction) | `host-pending` | Reference host advertises the profile + supplies `OPENWOP_TEST_SECONDARY_API_KEY` for the overlap check. |
56
+ | `auth-oauth2-client-credentials.test.ts` | `openwop-auth-oauth2-client-credentials` (`auth-profiles.md`, RFC 0010) | B (capability shape + malformed-JWT negative + harness-minted negatives gated on `OPENWOP_TEST_OAUTH_ISSUER_TRUSTED`) | `host-pending` | Reference host advertises the profile + trusts the conformance harness + (optional) supplies `OPENWOP_TEST_OAUTH_TOKEN` for positive-path. |
57
+ | `auth-oidc-user-bearer.test.ts` | `openwop-auth-oidc-user-bearer` (`auth-profiles.md`, RFC 0010) | B (capability shape + six harness-driven validation cases gated on `OPENWOP_TEST_OIDC_ISSUER_URL`) | `host-pending` | Reference host advertises the profile + is pre-configured to trust `OPENWOP_TEST_OIDC_ISSUER_URL` as a trusted issuer. Synthetic OIDC issuer harness at `conformance/src/lib/oidc-issuer.ts` (RS256 + ES256 via node:crypto stdlib). |
58
+ | `auth-mtls.test.ts` | `openwop-auth-mtls` (`auth-profiles.md`, RFC 0010) | B (capability shape always; opt-in behavior assertions via `OPENWOP_TEST_MTLS=1` + cert paths; uses node:https.request for client-cert handshake — no new npm deps) | `host-pending` | Reference host advertises the profile + listens on HTTPS with mTLS enforcement; operator supplies `OPENWOP_TEST_MTLS_CLIENT_{CERT,KEY}_PATH` and (optionally) `OPENWOP_TEST_MTLS_CA_PATH`. |
59
+ | `replay-retention-expiry.test.ts` | `openwop-replay-fork` (`replay.md` §"Retention and garbage collection") | B (capability shape always; 410/422 envelope on expired-range fork gated on `OPENWOP_TEST_EXPIRED_REPLAY_RUN_ID`; details.{sourceRunId, fromSeq, retentionBoundary} soft-checks per spec SHOULD) | `host-pending` | Reference host advertises `replay.supported: true` + operator produces a known-expired run id (no standardized force-expire endpoint per RFC 0009 Q#1). |
60
+ | `discovery.test.ts` — auth-scoped subtests (3 of them) | `openwop-discovery-auth-scoped` (`capabilities-change-detection.md` §"Scoped capability views", RFC 0011) | B (capability shape + mode/endpointPath typing always; required-field-preservation in authenticated view always; authorization-oracle probe gated on `OPENWOP_TEST_UNAUTHORIZED_API_KEY`) | `host-pending` | Reference host advertises `capabilities.discovery.authScoped.supported: true` + serves an authenticated capability view that satisfies the base schema + a tenant-scoped key pair for the oracle probe. |
61
+
62
+ Strict-mode runner usage:
63
+
64
+ ```bash
65
+ OPENWOP_REQUIRE_BEHAVIOR=true npx vitest run
66
+ ```
67
+
68
+ The flag is read at scenario startup via `conformance/src/lib/env.ts` → `loadEnv().requireBehavior`. Scenarios use the `behaviorGate(profileName, advertised)` helper from `conformance/src/lib/behavior-gate.ts` so the strict-mode failure message cites the relevant spec section. `audit-log-integrity.test.ts` is the worked example as of 2026-05-11; the remaining nine scenarios will adopt the helper as their host-side profiles land (tracked in `docs/PROTOCOL-GAP-CLOSURE-PLAN.md` Phase-1 tracks T1.1 onward).
32
69
 
33
70
  ---
34
71
 
@@ -51,6 +88,8 @@ Every OpenAPI operation should have:
51
88
  | `streamRunEvents` | `stream-modes.test.ts`, `stream-modes-buffer.test.ts`, `stream-modes-mixed.test.ts`, `streamReconnect.test.ts` | Unsupported mode and invalid buffer assertions | Add long-running proxy timeout soak outside fast CI. |
52
89
  | `pollRunEvents` | `multi-node-ordering.test.ts`, `version-negotiation.test.ts`, redaction tests | Past-end and validation assertions | Good. Add malformed `lastSequence` if missing. |
53
90
  | `cancelRun` | `cancellation.test.ts` | Unknown/terminal idempotency cases partial | Add explicit already-terminal cancel behavior. |
91
+ | `bulkCancelRuns` | `bulk-cancel.test.ts` (Phase B close-out) | `bulk-cancel.test.ts` covers per-id error envelopes (`not_found`, `forbidden`, `run_terminal`), oversized-array `400 validation_error`, and 100-id cap | Add multi-tenant scope-narrowing scenario when host advertises per-key scope. |
92
+ | `verifyAuditLog` | `audit-log-integrity.test.ts` covers `/v1/audit/verify` shape end-to-end against the `openwop-audit-log-integrity` profile | `audit-log-integrity.test.ts` covers chain-valid + tamper detection (host-internal) | Add cross-host checkpoint export so an out-of-band verifier can re-anchor against the same chain. |
54
93
  | `pauseRun` | Lifecycle scenarios cover paused state via `runs-lifecycle.test.ts` (`run.paused` event projection) | None dedicated yet | Add explicit `pauseRun` route exerciser (running → paused, paused → resumed, error envelope on terminal target). |
55
94
  | `resumeRun` | Lifecycle scenarios cover resumed state via `runs-lifecycle.test.ts` (`run.resumed` event projection) | None dedicated yet | Add explicit `resumeRun` route exerciser (paused → running, error envelope on running / terminal target). |
56
95
  | `forkRun` | `replay-fork.test.ts`, `replayDeterminism.test.ts` | Negative `fromSeq`, past-end, unknown source, invalid overlay | Add arbitrary-event fork and retention-expired source. |
@@ -0,0 +1,39 @@
1
+ {
2
+ "id": "conformance-configurable-schema",
3
+ "name": "Conformance: Per-workflow configurableSchema",
4
+ "version": "1.0",
5
+ "description": "Workflow that declares a `configurableSchema` constraining `RunOptions.configurable` (run-options.md §'Per-workflow configurableSchema'). The schema forbids unknown additional properties and requires `recursionLimit` to be a positive integer when present. Conformance scenario `configurable-schema.test.ts` verifies: (a) the manifest endpoint `GET /v1/workflows/{id}` surfaces the configurableSchema; (b) `POST /v1/runs` with a mismatched configurable returns `validation_error`. Body is a noop.",
6
+ "nodes": [
7
+ {
8
+ "id": "noop",
9
+ "typeId": "core.noop",
10
+ "name": "Noop",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": {},
13
+ "inputs": {}
14
+ }
15
+ ],
16
+ "edges": [],
17
+ "triggers": [
18
+ { "id": "manual", "type": "manual", "enabled": true }
19
+ ],
20
+ "variables": [],
21
+ "configurableSchema": {
22
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "properties": {
26
+ "recursionLimit": {
27
+ "type": "integer",
28
+ "minimum": 1,
29
+ "description": "Cap on per-run node executions; host enforces."
30
+ },
31
+ "tags": {
32
+ "type": "array",
33
+ "items": { "type": "string" }
34
+ }
35
+ }
36
+ },
37
+ "metadata": { "tags": ["conformance", "configurable-schema"] },
38
+ "settings": { "timeout": 10000 }
39
+ }