@openwop/openwop-conformance 1.20.0 → 1.23.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 (42) hide show
  1. package/CHANGELOG.md +61 -3
  2. package/README.md +61 -63
  3. package/api/asyncapi.yaml +54 -38
  4. package/api/openapi.yaml +34 -6
  5. package/coverage.md +381 -202
  6. package/fixtures/connection-packs/connection-pack-github.json +31 -0
  7. package/fixtures.md +120 -101
  8. package/package.json +2 -2
  9. package/schemas/README.md +1 -0
  10. package/schemas/agent-manifest.schema.json +6 -0
  11. package/schemas/capabilities.schema.json +75 -2
  12. package/schemas/connection-pack-manifest.schema.json +161 -0
  13. package/schemas/orchestrator-decision.schema.json +13 -0
  14. package/schemas/run-event-payloads.schema.json +23 -6
  15. package/schemas/run-event.schema.json +12 -2
  16. package/schemas/run-options.schema.json +1 -2
  17. package/schemas/run-snapshot.schema.json +2 -1
  18. package/schemas/suspend-request.schema.json +5 -0
  19. package/src/scenarios/agent-capability-degraded-projection.test.ts +108 -0
  20. package/src/scenarios/agent-channel-dispatch.test.ts +16 -8
  21. package/src/scenarios/agent-requires-capabilities-shape.test.ts +64 -0
  22. package/src/scenarios/agent-verifier-shape.test.ts +106 -0
  23. package/src/scenarios/aiproviders-input-shape.test.ts +69 -0
  24. package/src/scenarios/callai-multimodal.test.ts +86 -0
  25. package/src/scenarios/connection-pack-manifest-valid.test.ts +122 -0
  26. package/src/scenarios/connection-pack-no-credential-material.test.ts +125 -0
  27. package/src/scenarios/connection-pack-reach-exclusive.test.ts +85 -0
  28. package/src/scenarios/connection-pack-write-reconsent.test.ts +91 -0
  29. package/src/scenarios/connection-provider-resolution.test.ts +153 -0
  30. package/src/scenarios/cross-host-traceparent-propagation.test.ts +3 -3
  31. package/src/scenarios/experimental-tier-shape.test.ts +11 -0
  32. package/src/scenarios/fixtures-valid.test.ts +34 -0
  33. package/src/scenarios/grpc-transport.test.ts +108 -0
  34. package/src/scenarios/i18n-negotiation.test.ts +181 -0
  35. package/src/scenarios/interrupt-token-matrix.test.ts +2 -2
  36. package/src/scenarios/media-url-inline-cap.test.ts +5 -3
  37. package/src/scenarios/spec-corpus-validity.test.ts +129 -4
  38. package/src/scenarios/stream-text-fixture.test.ts +212 -0
  39. package/src/scenarios/verifier-gating.test.ts +73 -0
  40. package/src/scenarios/version-fold.test.ts +193 -0
  41. package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -2
  42. package/src/scenarios/webhook-tenant-isolation.test.ts +184 -0
@@ -51,6 +51,12 @@
51
51
  "items": { "type": "string", "minLength": 1 },
52
52
  "description": "Tool identifiers the agent MAY invoke. Format: `<scope>:<tool-id>` where scope is `openwop:` (Core/protocol tools), `mcp:` (MCP-namespaced tools), or `<vendor>.<host>` for host-extension tools. Hosts MUST enforce this allowlist when dispatching the agent (see RFC 0002 §A14 mapping for the reference-impl `ToolPermissionService.filterTools(harnessRole)` derivation pattern). Hosts MAY treat absence as `[]` (no tools) or as `*` (host-default tool surface) — that policy is host-configured."
53
53
  },
54
+ "requiresCapabilities": {
55
+ "type": "array",
56
+ "uniqueItems": true,
57
+ "items": { "type": "string", "minLength": 1 },
58
+ "description": "RFC 0092. Host-capability keys this agent needs to run fully — the agent-layer analogue of a node-pack's `peerDependencies`. Dotted identifiers from the discovery vocabulary (`capabilities.md` / `host-capabilities.md`), e.g. `host.workspace`, `aiProviders.toolCalling`, `multiAgent.executionModel.verifier`. A host that does NOT advertise a listed key MUST surface this agent as degraded on `GET /v1/agents` via the existing `degraded[]` inventory field (RFC 0072 §C); it MAY still dispatch at the RFC 0070 floor, but a silent satisfied-looking entry is non-conformant. Advisory about NEED only — never widens the agent's authority. Absent ⇒ no declared requirements."
59
+ },
54
60
  "memoryShape": {
55
61
  "type": "object",
56
62
  "description": "Declares which memory backends the agent reads/writes. Hosts that advertise `capabilities.agents.memoryBackends` MAY filter manifests during install based on these flags. The full memory-shape contract lands in RFC 0004 (Phase 3); RFC 0003 only declares the descriptor field and reserves `longTerm: true` as the redaction-harness trigger.",
@@ -64,6 +64,11 @@
64
64
  "type": "number",
65
65
  "minimum": 0,
66
66
  "description": "RFC 0084. Engine-side cost ceiling clamping `RunOptions.configurable.budget.maxCostUsd`. Optional; only meaningful with `capabilities.budget`."
67
+ },
68
+ "maxRequestBodyBytes": {
69
+ "type": "integer",
70
+ "minimum": 1,
71
+ "description": "RFC 0094 §H. Maximum REST request body size (bytes) the host accepts. Optional v1 field per `capabilities.md` §3 (previously documented as reserved — the closed `limits` object made advertising it a schema-validation failure). Hosts that advertise it MUST enforce it."
67
72
  }
68
73
  },
69
74
  "additionalProperties": false,
@@ -279,6 +284,34 @@
279
284
  "items": { "type": "string", "enum": ["rest", "mcp", "a2a", "grpc"] },
280
285
  "description": "Optional v1 transport advertisement. REST is required whether or not this field is present."
281
286
  },
287
+ "grpc": {
288
+ "type": "object",
289
+ "description": "RFC 0094 §H. gRPC transport advertisement per `grpc-transport.md` §\"Capability advertisement\". Optional — absent ⇒ the host exposes no gRPC transport. A host that exposes the gRPC surface advertises this block AND includes `\"grpc\"` in `supportedTransports`. REST + SSE remain exposed regardless.",
290
+ "required": ["supported", "service", "tls"],
291
+ "properties": {
292
+ "supported": {
293
+ "type": "boolean",
294
+ "description": "Toggle — `true` when the gRPC surface is live."
295
+ },
296
+ "endpoint": {
297
+ "type": "string",
298
+ "minLength": 1,
299
+ "pattern": "^grpcs?://",
300
+ "description": "Full URI: `grpc://` (cleartext, intra-trusted-network only) OR `grpcs://` (TLS). Hosts SHOULD require TLS in production."
301
+ },
302
+ "service": {
303
+ "type": "string",
304
+ "const": "openwop.v1.Engine",
305
+ "description": "Canonical service name. v1 hosts MUST use `openwop.v1.Engine` per `grpc-transport.md` §\"Field semantics\"."
306
+ },
307
+ "tls": {
308
+ "type": "string",
309
+ "enum": ["required", "optional", "disabled"],
310
+ "description": "TLS posture. Production hosts MUST set `\"required\"`."
311
+ }
312
+ },
313
+ "additionalProperties": false
314
+ },
282
315
  "configurable": {
283
316
  "type": "object",
284
317
  "description": "Optional v1 per-run overlay schema — what `RunOptions.configurable` keys this server honors."
@@ -403,6 +436,22 @@
403
436
  },
404
437
  "additionalProperties": false
405
438
  },
439
+ "connections": {
440
+ "type": "object",
441
+ "description": "RFC 0095 (`Draft`). Connection packs — portable, registry-distributable provider definitions (`kind: \"connection\"`, `connection-pack-manifest.schema.json`) that the RFC 0045/0047 `provider` string resolves against. Only useful alongside `oauth.supported` (RFC 0047) or `credentials.supported` (RFC 0046); a host SHOULD NOT advertise this block without at least one of those.",
442
+ "required": ["packsSupported"],
443
+ "properties": {
444
+ "supported": {
445
+ "type": "boolean",
446
+ "description": "OPTIONAL convenience flag mirroring the other capability families. RFC 0095 keys behavior on `packsSupported`; hosts MAY also advertise `supported` for family-shape uniformity."
447
+ },
448
+ "packsSupported": {
449
+ "type": "boolean",
450
+ "description": "RFC 0095 §C. When `true`, the host installs `kind: \"connection\"` registry packs and MUST implement the §B.6 resolution contract: an RFC 0045 connector's `auth.provider` (or an RFC 0047 `host.oauth` provider string) resolves against the installed connection pack whose `provider.id` matches, with installed-vs-built-in precedence per SemVer §11 and `connection_provider_unresolved` / `connection_provider_conflict` diagnostics. When `false` or absent, connection packs are not loaded and provider resolution stays implementation-defined."
451
+ }
452
+ },
453
+ "additionalProperties": false
454
+ },
406
455
  "credentials": {
407
456
  "type": "object",
408
457
  "description": "RFC 0046 (`Draft`). Portable credential resolution + lifecycle contract — sibling to `secrets`, first-class store-at-rest + workspace sharing + two-key-overlap rotation. A pack references a credential by `{ ref, scope }` (see `credential-reference.schema.json`); the host resolves it into the node sandbox ONLY — never into inputs, persisted variables, channels, any run.* event payload, the debug bundle, or replay state (SECURITY invariant `credential-payload-redaction`). Supersedes the informal BYOK annex; the `secrets` advertisement stays valid.",
@@ -553,8 +602,18 @@
553
602
  "version": {
554
603
  "type": "integer",
555
604
  "minimum": 1,
556
- "maximum": 5,
557
- "description": "Profile version. 1 = Phase 1 (execution-loop framework + planner→worker handoff). 2 = Phase 2 (confidence-floor escalation + agent-memory lifecycle, RFC 0039). 3 = Phase 3 (cross-host causation, RFC 0040). 4 = Phase 4 (replay determinism under nondeterministic models, RFC 0041). 5 = Phase 5 (stateful agent-loop lifecycle — per-iteration workspace+memory snapshot inputs, the observable `iteration` counter on `runOrchestrator.decided`, and stateful HITL resume, RFC 0061). A host advertising `version: N` MUST implement all phases 1..N additively."
605
+ "maximum": 6,
606
+ "description": "Profile version. 1 = Phase 1 (execution-loop framework + planner→worker handoff). 2 = Phase 2 (confidence-floor escalation + agent-memory lifecycle, RFC 0039). 3 = Phase 3 (cross-host causation, RFC 0040). 4 = Phase 4 (replay determinism under nondeterministic models, RFC 0041). 5 = Phase 5 (stateful agent-loop lifecycle — per-iteration workspace+memory snapshot inputs, the observable `iteration` counter on `runOrchestrator.decided`, and stateful HITL resume, RFC 0061). 6 = Phase 6 (verifier/critic turn — the `agent.verified` event + `successCriteria` on the `terminate` decision, RFC 0090). A host advertising `version: N` MUST implement all phases 1..N additively."
607
+ },
608
+ "verifier": {
609
+ "type": "object",
610
+ "additionalProperties": false,
611
+ "required": ["supported"],
612
+ "description": "RFC 0090 (`version >= 6`). The verifier/critic turn: the host emits `agent.verified` over a prior result and (optionally) gates commit on the verdict. Absent ⇒ no verifier turn; conformance scenarios soft-skip.",
613
+ "properties": {
614
+ "supported": { "type": "boolean", "description": "Host emits `agent.verified` and honors RFC 0090 §A. Applies only when `version >= 6`." },
615
+ "gating": { "type": "boolean", "description": "Host enforces the RFC 0090 §B commit-gating contract: a `fail` verdict blocks merge/terminate (fail-closed, composing RFC 0063); `revise` routes back to an actor turn. Absent/false ⇒ the verdict is observability-only." }
616
+ }
558
617
  },
559
618
  "confidenceEscalationFloor": {
560
619
  "type": "number",
@@ -686,6 +745,20 @@
686
745
  "uniqueItems": true,
687
746
  "description": "Subset of `supported` for which BYOK is permitted. Empty array → all calls use platform-managed keys; non-empty → clients MAY pass `ai.credentialRef` in `RunOptions.configurable` for matching providers."
688
747
  },
748
+ "input": {
749
+ "type": "object",
750
+ "additionalProperties": false,
751
+ "description": "RFC 0091. Multimodal PERCEPTION input on `ctx.callAI` — the modalities a `callAI` message ContentPart may carry as model INPUT. Absent ⇒ text-only (today's behavior); a `string` message content is always valid. Distinct from `imageGeneration` (output) and the `ai-envelope.md` media emission types (output).",
752
+ "properties": {
753
+ "modalities": {
754
+ "type": "array",
755
+ "uniqueItems": true,
756
+ "items": { "type": "string", "enum": ["text", "image", "audio", "document"] },
757
+ "description": "Input modalities the host's `callAI` accepts as ContentParts. `text` is implicit even when omitted. A ContentPart whose `type` is not advertised here MUST be rejected with `unsupported_modality` (never silently dropped)."
758
+ },
759
+ "maxBytesPerPart": { "type": "integer", "minimum": 1, "description": "Optional host cap on a single inline (`data`) or `mediaRef` part." }
760
+ }
761
+ },
689
762
  "authModes": {
690
763
  "type": "object",
691
764
  "description": "RFC 0067 (`Draft`). Optional per-provider advertisement of HOW the host expects a provider's credential to be supplied. Keys are provider ids appearing in `supported`; values are the auth modes the host honors for that provider. Absent ⇒ no advertisement: a provider in `byok` defaults to `apiKey` semantics (client passes `ai.credentialRef`); a provider in `supported` but not in `byok` defaults to `none` (platform-managed). This map only DESCRIBES the supply mechanism — `oauth-pkce`/`oauth-device` flow mechanics compose RFC 0047 `host.oauth` and resolve credentials by `ref` (RFC 0046), never on `ai.credentialRef`. A provider with `apiKey` MUST appear in `byok`; a provider whose modes are exactly `[\"none\"]` MUST NOT appear in `byok`. Consumers MUST ignore an auth mode they don't recognize rather than reject the discovery doc.",
@@ -0,0 +1,161 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openwop.dev/spec/v1/connection-pack-manifest.schema.json",
4
+ "title": "ConnectionPackManifest",
5
+ "description": "RFC 0095. Manifest for a published OpenWOP connection pack — `pack.json` at the pack root with `kind: \"connection\"`. Peer to and disjoint from `node-pack-manifest.schema.json` (RFC 0003), `prompt-pack-manifest.schema.json` (RFC 0028), and `workflow-chain-pack-manifest.schema.json` (RFC 0013) via the `kind` discriminator. A connection pack is a portable PROVIDER definition — the authorize/token/revoke endpoints, the read/write OAuth scope catalog, and how the integration is reached (an MCP server, an OpenAPI surface, or a core integration node). It carries NO secret: the OAuth client credential is a host concern (RFC 0047), supplied out of band. When installed, it makes an RFC 0045 connector's `auth.provider` / an RFC 0047 `host.oauth` provider string resolve against `provider.id` instead of host-locked code. See `spec/v1/connection-packs.md`.",
6
+ "type": "object",
7
+ "required": ["name", "version", "kind", "engines", "provider"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "name": {
11
+ "type": "string",
12
+ "description": "Reverse-DNS pack name per `node-packs.md` §Naming. Reserved scopes are identical (`core.*` / `vendor.<org>.*` / `community.<author>.*` / `private.<host>.*`). Mirror of `prompt-pack-manifest.schema.json#/properties/name`.",
13
+ "pattern": "^(core|vendor|community|private)\\.[a-z][a-z0-9_-]*(\\.[a-z][a-zA-Z0-9_-]*)+$",
14
+ "minLength": 1,
15
+ "maxLength": 256
16
+ },
17
+ "version": {
18
+ "type": "string",
19
+ "description": "Pack-level SemVer 2.0.0.",
20
+ "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
21
+ },
22
+ "kind": {
23
+ "type": "string",
24
+ "const": "connection",
25
+ "description": "Pack kind discriminator. MUST be the literal string `\"connection\"` for this schema."
26
+ },
27
+ "description": { "type": "string", "maxLength": 1024 },
28
+ "author": { "type": "string" },
29
+ "license": { "type": "string", "description": "SPDX license identifier (e.g., `Apache-2.0`)." },
30
+ "homepage": { "type": "string", "format": "uri" },
31
+ "repository": { "type": "string", "format": "uri" },
32
+ "keywords": {
33
+ "type": "array",
34
+ "items": { "type": "string", "maxLength": 64 },
35
+ "maxItems": 50
36
+ },
37
+ "engines": {
38
+ "type": "object",
39
+ "required": ["openwop"],
40
+ "additionalProperties": false,
41
+ "properties": {
42
+ "openwop": { "type": "string", "description": "SemVer range of the openwop spec version this pack targets." }
43
+ }
44
+ },
45
+ "provider": {
46
+ "type": "object",
47
+ "description": "The portable provider definition. Exactly one per connection pack.",
48
+ "required": ["id", "displayName", "category", "auth", "reach"],
49
+ "additionalProperties": false,
50
+ "properties": {
51
+ "id": {
52
+ "type": "string",
53
+ "pattern": "^[a-z][a-z0-9-]*$",
54
+ "description": "Stable provider id an RFC 0045/0047 `provider` string resolves to, e.g. `github`."
55
+ },
56
+ "displayName": { "type": "string", "minLength": 1 },
57
+ "category": {
58
+ "type": "string",
59
+ "enum": ["communication", "docs", "crm", "dev", "storage", "email-calendar", "ticketing", "data-warehouse", "marketing", "finance", "hr", "esignature", "support", "project-management", "payments", "other"]
60
+ },
61
+ "auth": {
62
+ "type": "object",
63
+ "required": ["kind"],
64
+ "additionalProperties": false,
65
+ "properties": {
66
+ "kind": { "type": "string", "enum": ["oauth2", "api_key", "bearer", "basic"] },
67
+ "authFlow": { "type": "string", "enum": ["pkce", "client_credentials", "manual", "none"] },
68
+ "scopeModel": {
69
+ "type": "string",
70
+ "enum": ["groups", "coarse", "capabilities"],
71
+ "default": "groups",
72
+ "description": "`groups` = read/write scope groups (Google/Jira/Graph). `coarse` = a single read-vs-write toggle (Stripe Connect read_only/read_write). `capabilities` = consent is a static developer-portal capability set, not per-request scopes (Notion)."
73
+ },
74
+ "endpoints": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "description": "Fixed, manifest-declared https endpoints (RFC 0095 §B.3). Never derived from runtime user input — not an SSRF surface.",
78
+ "properties": {
79
+ "authorize": { "type": "string", "format": "uri", "pattern": "^https://" },
80
+ "token": { "type": "string", "format": "uri", "pattern": "^https://" },
81
+ "revoke": { "type": "string", "format": "uri", "pattern": "^https://" }
82
+ }
83
+ },
84
+ "scopes": {
85
+ "type": "object",
86
+ "additionalProperties": false,
87
+ "description": "Read/write scope groups. `write` is requested as a SEPARATE re-consent step (RFC 0095 §B.4).",
88
+ "properties": {
89
+ "read": { "type": "array", "items": { "$ref": "#/$defs/ScopeGroup" } },
90
+ "write": { "type": "array", "items": { "$ref": "#/$defs/ScopeGroup" } }
91
+ }
92
+ },
93
+ "instanceUrlTemplate": {
94
+ "type": "string",
95
+ "description": "For per-account-host providers (Snowflake/NetSuite/ServiceNow): a template the host fills per connection, e.g. `https://{account}.snowflakecomputing.com`. Its presence signals the provider cannot use a single global OAuth app."
96
+ }
97
+ }
98
+ },
99
+ "reach": {
100
+ "type": "object",
101
+ "minProperties": 1,
102
+ "maxProperties": 1,
103
+ "additionalProperties": false,
104
+ "description": "Exactly ONE of mcp | openapi | integration — which core node injects the resolved credential (RFC 0095 §B.5).",
105
+ "properties": {
106
+ "mcp": {
107
+ "type": "object",
108
+ "required": ["server"],
109
+ "additionalProperties": false,
110
+ "properties": {
111
+ "server": {
112
+ "type": "object",
113
+ "required": ["url", "transport"],
114
+ "additionalProperties": false,
115
+ "properties": {
116
+ "url": { "type": "string", "format": "uri", "pattern": "^https://" },
117
+ "transport": { "type": "string", "enum": ["http", "sse"] }
118
+ }
119
+ }
120
+ }
121
+ },
122
+ "openapi": {
123
+ "type": "object",
124
+ "required": ["ref"],
125
+ "additionalProperties": false,
126
+ "properties": {
127
+ "ref": { "type": "string", "description": "URL or in-pack path of the OpenAPI document for core.openwop.http.openapi-call." }
128
+ }
129
+ },
130
+ "integration": {
131
+ "type": "object",
132
+ "required": ["node"],
133
+ "additionalProperties": false,
134
+ "properties": {
135
+ "node": { "type": "string", "description": "A core.openwop.integration.* node typeId." }
136
+ }
137
+ }
138
+ }
139
+ },
140
+ "consumerNodes": {
141
+ "type": "array",
142
+ "items": { "type": "string" },
143
+ "description": "The core node typeIds that consume this provider's credential (e.g. core.openwop.mcp.invoke-tool)."
144
+ },
145
+ "docsUrl": { "type": "string", "format": "uri" }
146
+ }
147
+ }
148
+ },
149
+ "$defs": {
150
+ "ScopeGroup": {
151
+ "type": "object",
152
+ "required": ["key", "label", "scopes"],
153
+ "additionalProperties": false,
154
+ "properties": {
155
+ "key": { "type": "string", "pattern": "^[a-z][a-z0-9._-]*$" },
156
+ "label": { "type": "string", "minLength": 1 },
157
+ "scopes": { "type": "array", "items": { "type": "string" }, "description": "The provider's raw scope strings, e.g. `https://www.googleapis.com/auth/drive.readonly`." }
158
+ }
159
+ }
160
+ }
161
+ }
@@ -64,6 +64,19 @@
64
64
  "minimum": 0,
65
65
  "maximum": 1,
66
66
  "description": "RFC 0039 §A — supervisor's stated confidence in this termination decision, in [0, 1]. Optional; absent means 'no opinion stated' (NOT low confidence). Hosts advertising `capabilities.multiAgent.executionModel.version >= 2` MUST honor a confidence floor as documented on NextWorkerDecision.confidence above; the same MUST applies to TerminateDecision because a confidently-wrong terminate is the same class of failure as a confidently-wrong dispatch."
67
+ },
68
+ "successCriteria": {
69
+ "type": "array",
70
+ "description": "RFC 0090 (`multiAgent.executionModel.version >= 6`). Optional structured convergence record — the conditions the supervisor judged when terminating. Content-free: criterion keys + booleans only, never result content. When present, a terminate with any `met: false` entry signals a give-up (NOT goal-satisfied); consumers MUST NOT treat such a run as a success. Absent ⇒ today's free-text `reason` semantics, unchanged.",
71
+ "items": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "required": ["key", "met"],
75
+ "properties": {
76
+ "key": { "type": "string", "minLength": 1, "description": "Criterion identifier (e.g. `goal-answered`)." },
77
+ "met": { "type": "boolean", "description": "Whether the supervisor judged this criterion satisfied." }
78
+ }
79
+ }
67
80
  }
68
81
  },
69
82
  "additionalProperties": false
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://openwop.dev/spec/v1/run-event-payloads.schema.json",
4
4
  "title": "RunEventPayloads",
5
- "description": "Per-RunEventType payload schemas. The base RunEventDoc shape (run-event.schema.json) leaves `payload` permissive for forward-compat. This schema defines the canonical payload contract for each known RunEventType. Consumers MAY pin strict payload validation via `$defs.<typeId>` and `ajv.validate(schema.$defs[event.type], event.payload)`. Unknown event types MUST be tolerated (no $defs match → fold best-effort).\n\n94 variants from `run-event.schema.json#$defs.RunEventType` are covered, grouped into ~20 shape families with shared $defs. Naming convention: camelCase keys mirror dotted RunEventType names (e.g., `run.started` → `runStarted`).",
5
+ "description": "Per-RunEventType payload schemas. The base RunEventDoc shape (run-event.schema.json) leaves `payload` permissive for forward-compat. This schema defines the canonical payload contract for each known RunEventType. Consumers MAY pin strict payload validation via `$defs.<typeId>` and `ajv.validate(schema.$defs[event.type], event.payload)`. Unknown event types MUST be tolerated (no $defs match → fold best-effort).\n\n95 variants from `run-event.schema.json#$defs.RunEventType` are covered, grouped into ~20 shape families with shared $defs. Naming convention: camelCase keys mirror dotted RunEventType names (e.g., `run.started` → `runStarted`).",
6
6
  "type": "object",
7
7
  "$defs": {
8
8
  "_typeIndex": {
@@ -70,6 +70,7 @@
70
70
  "agent.toolReturned": { "$ref": "#/$defs/agentToolReturned" },
71
71
  "agent.handoff": { "$ref": "#/$defs/agentHandoff" },
72
72
  "agent.decided": { "$ref": "#/$defs/agentDecided" },
73
+ "agent.verified": { "$ref": "#/$defs/agentVerified" },
73
74
  "runOrchestrator.decided": { "$ref": "#/$defs/runOrchestratorDecided" },
74
75
  "node.dispatched": { "$ref": "#/$defs/nodeDispatched" },
75
76
  "conversation.opened": { "$ref": "#/$defs/conversationOpened" },
@@ -422,7 +423,7 @@
422
423
  "properties": {
423
424
  "nodeId": { "type": "string", "minLength": 1 },
424
425
  "interruptId": { "type": "string", "minLength": 1 },
425
- "kind": { "type": "string", "enum": ["approval", "clarification", "external-event", "custom"] },
426
+ "kind": { "type": "string", "enum": ["approval", "clarification", "external-event", "custom", "conversation.start", "conversation.exchange", "conversation.close", "low-confidence"], "description": "RFC 0094 §E — the full kind union `interrupt.md` defines (mirrors suspend-request.schema.json). Conversation kinds are gated on the conversation capability per capabilities.md." },
426
427
  "key": { "type": "string", "minLength": 1 }
427
428
  },
428
429
  "additionalProperties": true
@@ -607,7 +608,7 @@
607
608
  "properties": {
608
609
  "nodeId": { "type": "string" },
609
610
  "interruptId": { "type": "string" },
610
- "kind": { "type": "string", "enum": ["approval", "clarification", "external-event", "custom"] },
611
+ "kind": { "type": "string", "enum": ["approval", "clarification", "external-event", "custom", "conversation.start", "conversation.exchange", "conversation.close", "low-confidence"], "description": "RFC 0094 §E — the full kind union `interrupt.md` defines (mirrors suspend-request.schema.json). Conversation kinds are gated on the conversation capability per capabilities.md." },
611
612
  "resumeValue": {}
612
613
  },
613
614
  "additionalProperties": true
@@ -635,12 +636,13 @@
635
636
 
636
637
  "outputChunk": {
637
638
  "type": "object",
638
- "description": "Emitted for streaming output (e.g., LLM token chunks). Stream-mode `messages` consumers see these. Tiered metadata per stream-modes.md §messages (S2 closure): bare {nodeId, chunk} is the minimum compliant payload; `meta` adds Tier 1 typed slots and a Tier 2 provider-pass-through escape hatch.",
639
- "required": ["nodeId", "chunk"],
639
+ "description": "Emitted for streaming output (e.g., LLM token chunks). Stream-mode `messages` consumers see these. RFC 0094 §D single-sources the `ai.message.chunk` payload here: bare {nodeId, runId, chunk, isLast} is the minimum compliant payload per stream-modes.md §messages (the prior {nodeId, chunk}-only required set was the defective restatement). Tiered metadata per stream-modes.md §messages (S2 closure): `meta` adds Tier 1 typed slots and a Tier 2 provider-pass-through escape hatch.",
640
+ "required": ["nodeId", "runId", "chunk", "isLast"],
640
641
  "properties": {
641
642
  "nodeId": { "type": "string", "minLength": 1 },
643
+ "runId": { "type": "string", "minLength": 1, "description": "Run this chunk belongs to. Required so multiplexed consumers (mixed-mode streams, fan-in UIs) can route chunks without out-of-band context." },
642
644
  "chunk": { "type": "string" },
643
- "isLast": { "type": "boolean" },
645
+ "isLast": { "type": "boolean", "description": "True for the final chunk of a given AI node call. Required — both reference consumers rely on it for fold termination." },
644
646
  "channel": { "type": "string", "description": "Optional sub-stream identifier when a node emits multiple parallel streams." },
645
647
  "meta": { "$ref": "#/$defs/_chunkMeta" }
646
648
  },
@@ -1254,6 +1256,21 @@
1254
1256
  "additionalProperties": true
1255
1257
  },
1256
1258
 
1259
+ "agentVerified": {
1260
+ "type": "object",
1261
+ "description": "RFC 0090 (`multiAgent.executionModel.version >= 6`). A critic agent's independent verdict over a prior result, emitted before the result is committed/merged. Content-free: names the target + verdict + (optional) criteria KEYS, never the verified content (SECURITY invariant `verifier-no-content-leak`). A `fail` verdict on a `verifier.gating: true` host MUST block the merge/terminate; `revise` SHOULD route back to another actor turn (bounded by `maxLoopIterations`, RFC 0058).",
1262
+ "required": ["agentId", "target", "verdict"],
1263
+ "properties": {
1264
+ "agentId": { "type": "string", "minLength": 3, "maxLength": 256, "description": "AgentRef.agentId of the verifying (critic) agent. SHOULD differ from the agent whose work is checked; a host MAY allow self-verification but MUST keep verifier identity inspectable." },
1265
+ "target": { "type": "string", "minLength": 1, "description": "Opaque reference to what was checked — the eventId of the verified `agent.decided`, a child runId, or a tool callId. Chains the verdict to its subject." },
1266
+ "verdict": { "type": "string", "enum": ["pass", "fail", "revise"], "description": "`pass`: acceptable, MAY commit/terminate. `fail`: rejected; a `verifier.gating` host MUST NOT commit it. `revise`: needs another actor turn; SHOULD route back, not terminate." },
1267
+ "criteria": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true, "description": "Optional closed list of the criteria KEYS evaluated (e.g. `[\"schema-valid\",\"grounded\"]`). Keys only — never per-criterion verdict text — for SIEM safety." },
1268
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1, "description": "Optional verifier confidence in `[0,1]` — the critic's confidence in its OWN verdict, distinct from the actor's `agent.decided.confidence`. MAY drive the RFC 0039 escalation contract." },
1269
+ "causationHostId": { "type": "string", "minLength": 1, "description": "RFC 0040 §A — cross-host causation pointer when the verified target lives on a different host." }
1270
+ },
1271
+ "additionalProperties": false
1272
+ },
1273
+
1257
1274
  "runOrchestratorDecided": {
1258
1275
  "type": "object",
1259
1276
  "description": "Multi-Agent Shift Phase 5. Emitted exactly once per orchestrator decision by a `core.orchestrator.supervisor` node (or host-extension equivalent). Carries the deciding agent's identity and the typed `OrchestratorDecision` (see `orchestrator-decision.schema.json`). The envelope's top-level `nodeId` carries the supervisor node-id; the payload does NOT duplicate it.",
@@ -25,7 +25,15 @@
25
25
  "maxLength": 128
26
26
  },
27
27
  "type": {
28
- "$ref": "#/$defs/RunEventType"
28
+ "description": "Event-type discriminator. RFC 0094 §C: either a protocol-owned type from the authoritative `RunEventType` catalog, or a correctly-prefixed vendor-extension event per `host-extensions.md` §\"Vendor-prefixed namespaces\" (dotted name whose first segment is the vendor's short identifier or reverse-DNS label — e.g. `acme.canvas.published` — and is NOT a protocol-owned/registry-reserved prefix). Aligns the schema with `COMPATIBILITY.md` (\"Clients MUST ignore unknown event types\") and `version-negotiation.md` (readers ignore unknown).",
29
+ "anyOf": [
30
+ { "$ref": "#/$defs/RunEventType" },
31
+ {
32
+ "type": "string",
33
+ "pattern": "^(?!(?:openwop|core|community|vendor|private|local)\\.)[a-z][a-zA-Z0-9_-]*(\\.[a-zA-Z0-9_-]+)+$",
34
+ "description": "Vendor-extension event type: two or more dot-separated segments; the leading segment is the vendor prefix and MUST NOT be one of the protocol-owned / registry-reserved prefixes (`openwop.*`, `core.*`, `community.*`, `vendor.*`, `private.*`, `local.*`) per `host-extensions.md` §\"Canonical prefixes\"."
35
+ }
36
+ ]
29
37
  },
30
38
  "payload": {
31
39
  "description": "Event-type-specific payload. Servers MAY validate against per-type schemas; this top-level schema accepts any JSON value.",
@@ -57,7 +65,8 @@
57
65
  "maxLength": 128
58
66
  }
59
67
  },
60
- "additionalProperties": false,
68
+ "$comment": "RFC 0094 §G + COMPATIBILITY.md §\"Schema closure\": RunEventDoc is a SERVER-EMITTED shape, so it is open (`additionalProperties: true`) — v1.x hosts may add optional fields additively without breaking schema-validating clients. Client-submitted shapes stay closed at their outermost composition.",
69
+ "additionalProperties": true,
61
70
  "$defs": {
62
71
  "RunEventType": {
63
72
  "type": "string",
@@ -123,6 +132,7 @@
123
132
  "agent.toolReturned",
124
133
  "agent.handoff",
125
134
  "agent.decided",
135
+ "agent.verified",
126
136
  "agent.invocation.started",
127
137
  "agent.invocation.completed",
128
138
  "eval.started",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://openwop.dev/spec/v1/run-options.schema.json",
4
4
  "title": "RunOptions",
5
- "description": "Per-run parameter overlay supplied by the caller in `POST /v1/runs`. See `run-options.md` for full semantics. Reserved keys are typed; unknown keys in `configurable` pass through to the engine.",
5
+ "description": "Per-run parameter overlay supplied by the caller in `POST /v1/runs`. See `run-options.md` for full semantics. Reserved keys are typed; unknown keys in `configurable` pass through to the engine. RFC 0094 §A: this schema is OPEN at its own root (no `additionalProperties: false`) because it participates in the composed `createRun` request body in `api/openapi.yaml`; the closure (`unevaluatedProperties: false`) lives at that composition site, the only place JSON Schema 2020-12 can express closure over an `allOf`.",
6
6
  "type": "object",
7
7
  "properties": {
8
8
  "configurable": {
@@ -21,7 +21,6 @@
21
21
  "description": "Free-form caller metadata. Persisted on the run doc; surfaces back via `RunSnapshot.metadata`. Server MAY enforce a serialized-size limit (recommended: 50KB)."
22
22
  }
23
23
  },
24
- "additionalProperties": false,
25
24
  "$defs": {
26
25
  "Configurable": {
27
26
  "type": "object",
@@ -28,9 +28,10 @@
28
28
  "waiting-external",
29
29
  "completed",
30
30
  "failed",
31
+ "cancelling",
31
32
  "cancelled"
32
33
  ],
33
- "description": "Current run state. `waiting-external` MUST be used when the suspended interrupt's `kind` is `external-event` per `interrupt-profiles.md §openwop-interrupt-external-event` — distinguishes external-event waits from HITL waits at the wire level. Forward-compat: future statuses MAY be added; readers SHOULD treat unknown values as terminal-unknown rather than throw."
34
+ "description": "Current run state. `waiting-external` MUST be used when the suspended interrupt's `kind` is `external-event` per `interrupt-profiles.md §openwop-interrupt-external-event` — distinguishes external-event waits from HITL waits at the wire level. `cancelling` (RFC 0094 §B) is the transitional state between a cancel request being accepted and the terminal `cancelled` — `rest-endpoints.md` and the OpenAPI cancel responses already document the transition; a snapshot read during the cancel cascade carries it. Forward-compat: future statuses MAY be added; readers SHOULD treat unknown values as terminal-unknown rather than throw."
34
35
  },
35
36
  "owner": {
36
37
  "type": "object",
@@ -76,6 +76,11 @@
76
76
  "type": "string",
77
77
  "enum": ["single-veto", "majority"],
78
78
  "default": "single-veto"
79
+ },
80
+ "overrideBypassesQuorum": {
81
+ "type": "boolean",
82
+ "default": false,
83
+ "description": "RFC 0093 §D2 (pins RFC 0051 UQ 2). When `true`, a configured override principal (per `interrupt-profiles.md` §approval-gate `override.requiredRole`) MAY bypass the quorum (`requiredApprovals`) and release the gate alone. Default `false`: an override principal's approval counts as ONE quorum vote; quorum still applies. Optional, additive."
79
84
  }
80
85
  }
81
86
  },
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Agent capability-requirement degraded projection (RFC 0092 §B) — behavioral.
3
+ *
4
+ * Gated on `capabilities.agents.manifestRuntime` (root-first per RFC 0073).
5
+ * Soft-skips when unadvertised (default) / hard-fails under
6
+ * `OPENWOP_REQUIRE_BEHAVIOR=true`. The always-on wire-shape coverage lives in
7
+ * `agent-requires-capabilities-shape.test.ts` (the `requiresCapabilities[]`
8
+ * field); this asserts host BEHAVIOR on the NORMATIVE `GET /v1/agents` inventory:
9
+ *
10
+ * §B iff-contract — when an agent's `requiresCapabilities[]` names a key the
11
+ * host does NOT advertise, that key MUST appear in the inventory entry's
12
+ * `degraded[]` (the canonical RFC 0072 §C field — NOT a `degradedCapabilities`
13
+ * field). An agent whose requirements are all satisfied MUST omit `degraded`
14
+ * (or carry it empty). `degraded[]` members are unique, non-empty strings.
15
+ *
16
+ * Non-vacuity — the inventory MUST be non-empty. When
17
+ * `OPENWOP_DEGRADED_CAPABILITY_AGENT_ID` names an agent the host knows is
18
+ * capability-degraded (its `requiresCapabilities` names an unadvertised key),
19
+ * the degraded branch is asserted NON-VACUOUSLY against that agent.
20
+ *
21
+ * Black-box on the normative path — no POST seam.
22
+ *
23
+ * Spec references:
24
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0092-agent-capability-requirements.md
25
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0072-agent-inventory-and-dispatch.md (§C degraded[])
26
+ */
27
+
28
+ import { describe, it, expect } from 'vitest';
29
+ import { driver } from '../lib/driver.js';
30
+ import { behaviorGate } from '../lib/behavior-gate.js';
31
+ import { readManifestRuntimeCap, listManifestAgents } from '../lib/agentRuntime.js';
32
+
33
+ interface InventoryEntry {
34
+ agentId?: string;
35
+ requiresCapabilities?: unknown;
36
+ degraded?: unknown;
37
+ [k: string]: unknown;
38
+ }
39
+
40
+ describe('agent-capability-degraded-projection (RFC 0092 §B)', () => {
41
+ it('surfaces unmet requiresCapabilities in degraded[] and nothing on the rest', async () => {
42
+ const mr = await readManifestRuntimeCap();
43
+ if (!behaviorGate('openwop-agent-capability-degraded', mr?.supported === true)) return;
44
+
45
+ const inv = await listManifestAgents();
46
+ if (inv === null) return; // cap advertised but /v1/agents unserved — soft-skip
47
+ const agents = (inv.agents ?? []) as InventoryEntry[];
48
+
49
+ // Non-vacuity: an advertising + serving host MUST expose its inventory.
50
+ expect(
51
+ agents.length >= 1,
52
+ driver.describe('RFC 0072 §A', 'GET /v1/agents MUST return the installed manifest agents'),
53
+ ).toBe(true);
54
+
55
+ // §B well-formedness on EVERY entry: degraded[], when present, is a unique
56
+ // list of non-empty strings.
57
+ for (const a of agents) {
58
+ const d = a.degraded;
59
+ if (d !== undefined) {
60
+ expect(
61
+ Array.isArray(d),
62
+ driver.describe('agent-inventory-response.schema.json', `degraded MUST be an array when present (agent ${a.agentId})`),
63
+ ).toBe(true);
64
+ if (Array.isArray(d)) {
65
+ for (const k of d) {
66
+ expect(
67
+ typeof k === 'string' && k.length > 0,
68
+ driver.describe('RFC 0072 §C', `degraded[] members MUST be non-empty strings (agent ${a.agentId}, got ${String(k)})`),
69
+ ).toBe(true);
70
+ }
71
+ expect(
72
+ new Set(d as string[]).size === d.length,
73
+ driver.describe('RFC 0092 §B', `degraded[] MUST be unique (agent ${a.agentId})`),
74
+ ).toBe(true);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Non-vacuous degraded branch when the host names a known capability-degraded agent.
80
+ const degradedId = process.env.OPENWOP_DEGRADED_CAPABILITY_AGENT_ID;
81
+ if (degradedId) {
82
+ const target = agents.find((a) => a.agentId === degradedId);
83
+ expect(
84
+ target !== undefined,
85
+ driver.describe('RFC 0092 §B', `OPENWOP_DEGRADED_CAPABILITY_AGENT_ID=${degradedId} MUST appear in the inventory`),
86
+ ).toBe(true);
87
+ if (target) {
88
+ const d = target.degraded;
89
+ expect(
90
+ Array.isArray(d) && d.length >= 1,
91
+ driver.describe('RFC 0092 §B', 'the named capability-degraded agent MUST carry a non-empty degraded[]'),
92
+ ).toBe(true);
93
+ // When the entry also exposes requiresCapabilities, every degraded key
94
+ // MUST be one the agent actually requires (the projection is a subset of
95
+ // requirements, never invented).
96
+ if (Array.isArray(d) && Array.isArray(target.requiresCapabilities)) {
97
+ const req = new Set(target.requiresCapabilities as unknown[]);
98
+ for (const k of d) {
99
+ expect(
100
+ req.has(k),
101
+ driver.describe('RFC 0092 §B', `a degraded[] key MUST be one the agent requires (got ${String(k)})`),
102
+ ).toBe(true);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ });
108
+ });