@openwop/openwop-conformance 1.21.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.
- package/CHANGELOG.md +43 -2
- package/README.md +61 -63
- package/api/asyncapi.yaml +54 -38
- package/api/openapi.yaml +34 -6
- package/coverage.md +381 -202
- package/fixtures/connection-packs/connection-pack-github.json +31 -0
- package/fixtures.md +120 -101
- package/package.json +1 -1
- package/schemas/README.md +1 -0
- package/schemas/capabilities.schema.json +49 -0
- package/schemas/connection-pack-manifest.schema.json +161 -0
- package/schemas/run-event-payloads.schema.json +6 -5
- package/schemas/run-event.schema.json +11 -2
- package/schemas/run-options.schema.json +1 -2
- package/schemas/run-snapshot.schema.json +2 -1
- package/schemas/suspend-request.schema.json +5 -0
- package/src/scenarios/connection-pack-manifest-valid.test.ts +122 -0
- package/src/scenarios/connection-pack-no-credential-material.test.ts +125 -0
- package/src/scenarios/connection-pack-reach-exclusive.test.ts +85 -0
- package/src/scenarios/connection-pack-write-reconsent.test.ts +91 -0
- package/src/scenarios/connection-provider-resolution.test.ts +153 -0
- package/src/scenarios/cross-host-traceparent-propagation.test.ts +3 -3
- package/src/scenarios/fixtures-valid.test.ts +34 -0
- package/src/scenarios/grpc-transport.test.ts +108 -0
- package/src/scenarios/i18n-negotiation.test.ts +181 -0
- package/src/scenarios/interrupt-token-matrix.test.ts +2 -2
- package/src/scenarios/media-url-inline-cap.test.ts +5 -3
- package/src/scenarios/spec-corpus-validity.test.ts +107 -0
- package/src/scenarios/stream-text-fixture.test.ts +212 -0
- package/src/scenarios/version-fold.test.ts +193 -0
- package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -2
- package/src/scenarios/webhook-tenant-isolation.test.ts +184 -0
|
@@ -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.",
|
|
@@ -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
|
+
}
|
|
@@ -423,7 +423,7 @@
|
|
|
423
423
|
"properties": {
|
|
424
424
|
"nodeId": { "type": "string", "minLength": 1 },
|
|
425
425
|
"interruptId": { "type": "string", "minLength": 1 },
|
|
426
|
-
"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." },
|
|
427
427
|
"key": { "type": "string", "minLength": 1 }
|
|
428
428
|
},
|
|
429
429
|
"additionalProperties": true
|
|
@@ -608,7 +608,7 @@
|
|
|
608
608
|
"properties": {
|
|
609
609
|
"nodeId": { "type": "string" },
|
|
610
610
|
"interruptId": { "type": "string" },
|
|
611
|
-
"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." },
|
|
612
612
|
"resumeValue": {}
|
|
613
613
|
},
|
|
614
614
|
"additionalProperties": true
|
|
@@ -636,12 +636,13 @@
|
|
|
636
636
|
|
|
637
637
|
"outputChunk": {
|
|
638
638
|
"type": "object",
|
|
639
|
-
"description": "Emitted for streaming output (e.g., LLM token chunks). Stream-mode `messages` consumers see these.
|
|
640
|
-
"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"],
|
|
641
641
|
"properties": {
|
|
642
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." },
|
|
643
644
|
"chunk": { "type": "string" },
|
|
644
|
-
"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." },
|
|
645
646
|
"channel": { "type": "string", "description": "Optional sub-stream identifier when a node emits multiple parallel streams." },
|
|
646
647
|
"meta": { "$ref": "#/$defs/_chunkMeta" }
|
|
647
648
|
},
|
|
@@ -25,7 +25,15 @@
|
|
|
25
25
|
"maxLength": 128
|
|
26
26
|
},
|
|
27
27
|
"type": {
|
|
28
|
-
"
|
|
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
|
-
"
|
|
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",
|
|
@@ -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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection-pack manifest validity — `connection-packs.md` §Manifest clauses 1/3
|
|
3
|
+
* + `schemas/connection-pack-manifest.schema.json` (RFC 0095 §A).
|
|
4
|
+
*
|
|
5
|
+
* Always-on, server-free schema probe. Exercises the new
|
|
6
|
+
* `connection-pack-manifest.schema.json` with the canonical positive fixture
|
|
7
|
+
* and the kind-discriminator negatives:
|
|
8
|
+
*
|
|
9
|
+
* 1. Positive: the `connection-pack-github` fixture (a complete `kind:
|
|
10
|
+
* "connection"` manifest, MCP reach) validates cleanly.
|
|
11
|
+
* 2. Capability shape: `capabilities.schema.json` declares
|
|
12
|
+
* `connections.packsSupported` (RFC 0095 §C).
|
|
13
|
+
* 3. Negative — kind discriminator: the same manifest with `kind: "node"`
|
|
14
|
+
* is rejected (`const` violation) — the discriminator routes a
|
|
15
|
+
* connection manifest away from the other pack schemas.
|
|
16
|
+
* 4. Negative — kind/contents mixing: a manifest carrying BOTH `provider`
|
|
17
|
+
* AND `nodes[]` is rejected. Surface-level outcome at the registry is
|
|
18
|
+
* `pack_kind_invalid` per `node-packs.md` §"Pack kinds"; schema-level
|
|
19
|
+
* outcome is an `additionalProperties` violation on `nodes`.
|
|
20
|
+
* 5. Negative — non-https token endpoint: `http://` is rejected with a
|
|
21
|
+
* `pattern` violation (clause 3).
|
|
22
|
+
* 6. Positive — a SemVer prerelease `version` (`1.0.0-alpha.1`) is
|
|
23
|
+
* schema-VALID: prerelease *precedence* (clause 6, SemVer §11) is a
|
|
24
|
+
* host resolution concern, not a manifest-shape constraint.
|
|
25
|
+
*
|
|
26
|
+
* Behavioral resolution legs live in `connection-provider-resolution.test.ts`
|
|
27
|
+
* (capability-gated on `capabilities.connections.packsSupported`).
|
|
28
|
+
*
|
|
29
|
+
* @see spec/v1/connection-packs.md
|
|
30
|
+
* @see schemas/connection-pack-manifest.schema.json
|
|
31
|
+
* @see RFCS/0095-connection-packs-portable-provider-definitions.md
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { describe, it, expect } from 'vitest';
|
|
35
|
+
import { readFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
38
|
+
import addFormats from 'ajv-formats';
|
|
39
|
+
import type { ErrorObject } from 'ajv';
|
|
40
|
+
import { SCHEMAS_DIR, FIXTURES_DIR } from '../lib/paths.js';
|
|
41
|
+
|
|
42
|
+
const SCHEMA_PATH = join(SCHEMAS_DIR, 'connection-pack-manifest.schema.json');
|
|
43
|
+
const FIXTURE_PATH = join(FIXTURES_DIR, 'connection-packs', 'connection-pack-github.json');
|
|
44
|
+
|
|
45
|
+
type Manifest = Record<string, unknown> & {
|
|
46
|
+
provider: Record<string, unknown> & { auth: Record<string, unknown> };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function fixture(): Manifest {
|
|
50
|
+
return JSON.parse(readFileSync(FIXTURE_PATH, 'utf8')) as Manifest;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('category: connection-pack manifest validation (RFC 0095 §A)', () => {
|
|
54
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
55
|
+
addFormats(ajv);
|
|
56
|
+
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
57
|
+
const validate = ajv.compile(schema);
|
|
58
|
+
|
|
59
|
+
const failsWith = (manifest: unknown, keyword: string): ErrorObject[] => {
|
|
60
|
+
const ok = validate(manifest);
|
|
61
|
+
expect(ok).toBe(false);
|
|
62
|
+
return (validate.errors ?? []).filter((e) => e.keyword === keyword);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
it('positive: the connection-pack-github fixture validates cleanly', () => {
|
|
66
|
+
expect(
|
|
67
|
+
validate(fixture()),
|
|
68
|
+
`connection-packs.md §Manifest clause 1: a well-formed kind:"connection" manifest MUST validate. Errors: ${JSON.stringify(validate.errors)}`,
|
|
69
|
+
).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('capabilities.schema.json declares connections.packsSupported (RFC 0095 §C)', () => {
|
|
73
|
+
const caps = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'capabilities.schema.json'), 'utf8')) as {
|
|
74
|
+
properties?: Record<string, { properties?: Record<string, unknown>; required?: string[] }>;
|
|
75
|
+
};
|
|
76
|
+
const connections = caps.properties?.connections;
|
|
77
|
+
expect(connections, 'capabilities.md §connections — the connections block MUST be declared').toBeDefined();
|
|
78
|
+
expect(
|
|
79
|
+
connections?.properties?.packsSupported,
|
|
80
|
+
'RFC 0095 §C — connections.packsSupported MUST be declared',
|
|
81
|
+
).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('negative: the kind discriminator routes other kinds away (kind: "node" rejected)', () => {
|
|
85
|
+
const m = { ...fixture(), kind: 'node' };
|
|
86
|
+
const errs = failsWith(m, 'const');
|
|
87
|
+
expect(
|
|
88
|
+
errs.length,
|
|
89
|
+
'connection-packs.md §Manifest clause 1: kind MUST be the const "connection"',
|
|
90
|
+
).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('negative: a manifest mixing provider and nodes[] is rejected (pack_kind_invalid at the registry)', () => {
|
|
94
|
+
const m = {
|
|
95
|
+
...fixture(),
|
|
96
|
+
nodes: [{ typeId: 'vendor.acme.x', version: '1.0.0', category: 'data', role: 'pure' }],
|
|
97
|
+
};
|
|
98
|
+
const errs = failsWith(m, 'additionalProperties');
|
|
99
|
+
expect(
|
|
100
|
+
errs.some((e) => (e.params as { additionalProperty?: string }).additionalProperty === 'nodes'),
|
|
101
|
+
'node-packs.md §"Pack kinds": one kind per pack — a foreign `nodes[]` field MUST be rejected (additionalProperties:false)',
|
|
102
|
+
).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('negative: a non-https token endpoint is rejected (clause 3)', () => {
|
|
106
|
+
const m = fixture();
|
|
107
|
+
(m.provider.auth.endpoints as Record<string, string>).token = 'http://example.com/token';
|
|
108
|
+
const errs = failsWith(m, 'pattern');
|
|
109
|
+
expect(
|
|
110
|
+
errs.length,
|
|
111
|
+
'connection-packs.md §Manifest clause 3: auth endpoints MUST be absolute https:// URLs',
|
|
112
|
+
).toBeGreaterThan(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('positive: a SemVer prerelease version is schema-valid (precedence is a host concern, clause 6)', () => {
|
|
116
|
+
const m = { ...fixture(), version: '1.0.0-alpha.1' };
|
|
117
|
+
expect(
|
|
118
|
+
validate(m),
|
|
119
|
+
`connection-packs.md §Manifest clause 6: prerelease ordering is resolution-time SemVer §11, not manifest shape. Errors: ${JSON.stringify(validate.errors)}`,
|
|
120
|
+
).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection packs carry NO credential material — `connection-packs.md`
|
|
3
|
+
* §Manifest clause 2 (RFC 0095 §B.2). Public test for the protocol-tier
|
|
4
|
+
* SECURITY invariant `connection-pack-no-credential-material`.
|
|
5
|
+
*
|
|
6
|
+
* Two layers:
|
|
7
|
+
*
|
|
8
|
+
* A. Always-on, server-free schema probe — every name on the normative
|
|
9
|
+
* minimum blocklist (`clientSecret`, `client_secret`, `apiKey`,
|
|
10
|
+
* `api_key`, `token`, `accessToken`, `refreshToken`, `password`,
|
|
11
|
+
* `privateKey`, `secret`), injected at the manifest root, under
|
|
12
|
+
* `provider`, and under `provider.auth`, is rejected by
|
|
13
|
+
* `connection-pack-manifest.schema.json` (`additionalProperties:false`
|
|
14
|
+
* everywhere — the schema layer never admits a secret-named field).
|
|
15
|
+
* The single normative EXEMPTION — the property named `token` at
|
|
16
|
+
* exactly `provider.auth.endpoints.token` (the OAuth token-endpoint
|
|
17
|
+
* URL) — IS schema-valid.
|
|
18
|
+
*
|
|
19
|
+
* B. Capability-gated behavioral leg — on a host advertising
|
|
20
|
+
* `capabilities.connections.packsSupported: true` that exposes the
|
|
21
|
+
* `POST /v1/host/sample/connection-packs/install` test seam
|
|
22
|
+
* (`host-sample-test-seams.md`), installing a manifest that carries
|
|
23
|
+
* `clientSecret` MUST be rejected with the SPECIFIC error code
|
|
24
|
+
* `connection_pack_credential_material` — not a generic schema-shape
|
|
25
|
+
* error — because clause 2 requires the credential-material scan to
|
|
26
|
+
* run BEFORE generic schema validation. Hosts without the seam
|
|
27
|
+
* soft-skip (404); unadvertised hosts skip via the behavior gate.
|
|
28
|
+
*
|
|
29
|
+
* @see spec/v1/connection-packs.md
|
|
30
|
+
* @see SECURITY/invariants.yaml id: connection-pack-no-credential-material
|
|
31
|
+
* @see RFCS/0095-connection-packs-portable-provider-definitions.md
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { describe, it, expect } from 'vitest';
|
|
35
|
+
import { readFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
38
|
+
import addFormats from 'ajv-formats';
|
|
39
|
+
import { SCHEMAS_DIR, FIXTURES_DIR } from '../lib/paths.js';
|
|
40
|
+
import { driver } from '../lib/driver.js';
|
|
41
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
42
|
+
import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
43
|
+
|
|
44
|
+
const SCHEMA_PATH = join(SCHEMAS_DIR, 'connection-pack-manifest.schema.json');
|
|
45
|
+
const FIXTURE_PATH = join(FIXTURES_DIR, 'connection-packs', 'connection-pack-github.json');
|
|
46
|
+
|
|
47
|
+
const BLOCKLIST = [
|
|
48
|
+
'clientSecret',
|
|
49
|
+
'client_secret',
|
|
50
|
+
'apiKey',
|
|
51
|
+
'api_key',
|
|
52
|
+
'token',
|
|
53
|
+
'accessToken',
|
|
54
|
+
'refreshToken',
|
|
55
|
+
'password',
|
|
56
|
+
'privateKey',
|
|
57
|
+
'secret',
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
type Manifest = Record<string, unknown> & {
|
|
61
|
+
provider: Record<string, unknown> & { auth: Record<string, unknown> };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function fixture(): Manifest {
|
|
65
|
+
return JSON.parse(readFileSync(FIXTURE_PATH, 'utf8')) as Manifest;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('connection-pack-no-credential-material: schema layer (always-on, server-free)', () => {
|
|
69
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
70
|
+
addFormats(ajv);
|
|
71
|
+
const validate = ajv.compile(JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')));
|
|
72
|
+
|
|
73
|
+
it('every blocklisted property name is schema-rejected at the root, provider, and auth levels', () => {
|
|
74
|
+
for (const name of BLOCKLIST) {
|
|
75
|
+
const atRoot = { ...fixture(), [name]: 'xxx' };
|
|
76
|
+
const atProvider = fixture();
|
|
77
|
+
(atProvider.provider as Record<string, unknown>)[name] = 'xxx';
|
|
78
|
+
const atAuth = fixture();
|
|
79
|
+
(atAuth.provider.auth as Record<string, unknown>)[name] = 'xxx';
|
|
80
|
+
for (const [where, m] of [['root', atRoot], ['provider', atProvider], ['provider.auth', atAuth]] as const) {
|
|
81
|
+
expect(
|
|
82
|
+
validate(m),
|
|
83
|
+
`SECURITY invariant connection-pack-no-credential-material — a property named "${name}" at ${where} MUST NOT validate (additionalProperties:false)`,
|
|
84
|
+
).toBe(false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('the exemption: provider.auth.endpoints.token (the token-endpoint URL) IS valid', () => {
|
|
90
|
+
const m = fixture();
|
|
91
|
+
expect(
|
|
92
|
+
typeof (m.provider.auth.endpoints as Record<string, string>).token,
|
|
93
|
+
'fixture sanity — the github fixture declares the token endpoint',
|
|
94
|
+
).toBe('string');
|
|
95
|
+
expect(
|
|
96
|
+
validate(m),
|
|
97
|
+
`connection-packs.md §Manifest clause 2 — the property named "token" at exactly provider.auth.endpoints.token is the OAuth token-endpoint URL and MUST validate. Errors: ${JSON.stringify(validate.errors)}`,
|
|
98
|
+
).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('connection-pack-no-credential-material: specific rejection code (capability-gated, RFC 0095 §B.2)', () => {
|
|
103
|
+
it('installing a manifest carrying clientSecret is rejected with connection_pack_credential_material', async () => {
|
|
104
|
+
const connections = await readCapabilityFamily<{ packsSupported?: boolean }>('connections');
|
|
105
|
+
if (!behaviorGate('connections.packsSupported', connections?.packsSupported === true)) return;
|
|
106
|
+
|
|
107
|
+
const leaky = fixture();
|
|
108
|
+
(leaky.provider.auth as Record<string, unknown>).clientSecret = 'ghs_conformance_canary';
|
|
109
|
+
const res = await driver.post('/v1/host/sample/connection-packs/install', { manifest: leaky });
|
|
110
|
+
if (res.status === 404 || res.status === 403) return; // seam unwired — soft-skip
|
|
111
|
+
|
|
112
|
+
const body = res.json as { installed?: boolean; errors?: Array<{ code?: string }> } | undefined;
|
|
113
|
+
expect(
|
|
114
|
+
body?.installed,
|
|
115
|
+
driver.describe('connection-packs.md §Manifest clause 2', 'a manifest carrying credential material MUST NOT install'),
|
|
116
|
+
).toBe(false);
|
|
117
|
+
expect(
|
|
118
|
+
(body?.errors ?? []).some((e) => e.code === 'connection_pack_credential_material'),
|
|
119
|
+
driver.describe(
|
|
120
|
+
'connection-packs.md §Manifest clause 2',
|
|
121
|
+
'the credential-material scan runs BEFORE generic schema validation — the SPECIFIC code connection_pack_credential_material MUST surface, not a generic shape error',
|
|
122
|
+
),
|
|
123
|
+
).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
});
|