@openwop/openwop-conformance 1.18.1 → 1.20.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 +17 -0
- package/README.md +2 -2
- package/api/.redocly.lint-ignore.yaml +22 -0
- package/api/openapi.yaml +13 -4
- package/coverage.md +2 -1
- package/dist/cli.js +235 -4
- package/dist/lib/paths.js +160 -0
- package/dist/lib/profiles.js +461 -0
- package/fixtures/conformance-agent-channel-dispatch.json +27 -0
- package/fixtures.md +15 -0
- package/package.json +1 -1
- package/schemas/README.md +1 -0
- package/schemas/capabilities.schema.json +5 -0
- package/schemas/conformance-certification-bundle.schema.json +86 -0
- package/src/cli.ts +268 -4
- package/src/lib/profiles.ts +85 -0
- package/src/scenarios/agent-channel-dispatch.test.ts +234 -0
- package/src/scenarios/spec-corpus-validity.test.ts +183 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility-profile derivation for openwop v1.x.
|
|
3
|
+
*
|
|
4
|
+
* Profiles are a named set of capability requirements. A host's profile
|
|
5
|
+
* set is derived from the `/.well-known/openwop` discovery payload — never
|
|
6
|
+
* declared as a separate wire field. See `spec/v1/profiles.md` for the
|
|
7
|
+
* normative predicate definitions.
|
|
8
|
+
*
|
|
9
|
+
* This module is the single canonical implementation of profile membership.
|
|
10
|
+
* Conformance scenarios use it to gate profile-specific assertions; SDKs
|
|
11
|
+
* MAY re-export the derivation helper to give clients a way to ask
|
|
12
|
+
* "does this host satisfy `openwop-secrets`?" without re-implementing the
|
|
13
|
+
* predicates.
|
|
14
|
+
*
|
|
15
|
+
* **Derivation is deterministic and pure.** Same payload, same profile
|
|
16
|
+
* set. No time-of-day, host-specific state, or hidden inputs.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Closed v1.x catalog. Adding a profile requires an RFC per
|
|
20
|
+
* `RFCS/0001-rfc-process.md`.
|
|
21
|
+
*/
|
|
22
|
+
export const PROFILE_NAMES = [
|
|
23
|
+
'openwop-core',
|
|
24
|
+
'openwop-interrupts',
|
|
25
|
+
'openwop-stream-sse',
|
|
26
|
+
'openwop-stream-poll',
|
|
27
|
+
'openwop-secrets',
|
|
28
|
+
'openwop-provider-policy',
|
|
29
|
+
'openwop-node-packs',
|
|
30
|
+
'openwop-replay-fork',
|
|
31
|
+
'openwop-fixtures',
|
|
32
|
+
'openwop-memory',
|
|
33
|
+
'openwop-trigger-bridge',
|
|
34
|
+
];
|
|
35
|
+
function isStringArray(value) {
|
|
36
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
37
|
+
}
|
|
38
|
+
function isNonNegativeInteger(value) {
|
|
39
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `openwop-core` predicate. Every other profile implies `openwop-core`. A host
|
|
43
|
+
* that fails this predicate is not openwop-compatible.
|
|
44
|
+
*
|
|
45
|
+
* @see spec/v1/profiles.md §`openwop-core`
|
|
46
|
+
*/
|
|
47
|
+
export function isCore(c) {
|
|
48
|
+
if (typeof c.protocolVersion !== 'string')
|
|
49
|
+
return false;
|
|
50
|
+
if (!c.protocolVersion.startsWith('1.'))
|
|
51
|
+
return false;
|
|
52
|
+
if (!Array.isArray(c.supportedEnvelopes))
|
|
53
|
+
return false;
|
|
54
|
+
if (!c.supportedEnvelopes.every((entry) => typeof entry === 'string'))
|
|
55
|
+
return false;
|
|
56
|
+
if (typeof c.schemaVersions !== 'object' || c.schemaVersions === null)
|
|
57
|
+
return false;
|
|
58
|
+
if (typeof c.limits !== 'object' || c.limits === null)
|
|
59
|
+
return false;
|
|
60
|
+
if (!isNonNegativeInteger(c.limits.clarificationRounds))
|
|
61
|
+
return false;
|
|
62
|
+
if (!isNonNegativeInteger(c.limits.schemaRounds))
|
|
63
|
+
return false;
|
|
64
|
+
if (!isNonNegativeInteger(c.limits.envelopesPerTurn))
|
|
65
|
+
return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `openwop-interrupts` predicate.
|
|
70
|
+
*
|
|
71
|
+
* @see spec/v1/profiles.md §`openwop-interrupts`
|
|
72
|
+
*/
|
|
73
|
+
export function isInterrupts(c) {
|
|
74
|
+
if (!isCore(c))
|
|
75
|
+
return false;
|
|
76
|
+
if (!isStringArray(c.supportedEnvelopes))
|
|
77
|
+
return false;
|
|
78
|
+
return c.supportedEnvelopes.includes('clarification.request');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `openwop-stream-sse` predicate (discovery-payload only — runtime SSE
|
|
82
|
+
* behavior is verified by `stream-modes*.test.ts`).
|
|
83
|
+
*
|
|
84
|
+
* @see spec/v1/profiles.md §`openwop-stream-sse`
|
|
85
|
+
*/
|
|
86
|
+
export function isStreamSse(c) {
|
|
87
|
+
if (!isCore(c))
|
|
88
|
+
return false;
|
|
89
|
+
if (c.supportedTransports == null)
|
|
90
|
+
return true;
|
|
91
|
+
if (!isStringArray(c.supportedTransports))
|
|
92
|
+
return false;
|
|
93
|
+
return c.supportedTransports.includes('rest');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `openwop-stream-poll` predicate (discovery-payload only — runtime polling
|
|
97
|
+
* behavior is verified by `stream-modes.test.ts`).
|
|
98
|
+
*
|
|
99
|
+
* @see spec/v1/profiles.md §`openwop-stream-poll`
|
|
100
|
+
*/
|
|
101
|
+
export function isStreamPoll(c) {
|
|
102
|
+
if (!isCore(c))
|
|
103
|
+
return false;
|
|
104
|
+
if (c.supportedTransports == null)
|
|
105
|
+
return true;
|
|
106
|
+
if (!isStringArray(c.supportedTransports))
|
|
107
|
+
return false;
|
|
108
|
+
return c.supportedTransports.includes('rest');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `openwop-secrets` predicate.
|
|
112
|
+
*
|
|
113
|
+
* @see spec/v1/profiles.md §`openwop-secrets`
|
|
114
|
+
*/
|
|
115
|
+
export function isSecrets(c) {
|
|
116
|
+
if (!isCore(c))
|
|
117
|
+
return false;
|
|
118
|
+
if (c.secrets == null || typeof c.secrets !== 'object')
|
|
119
|
+
return false;
|
|
120
|
+
if (c.secrets.supported !== true)
|
|
121
|
+
return false;
|
|
122
|
+
if (!isStringArray(c.secrets.scopes))
|
|
123
|
+
return false;
|
|
124
|
+
return c.secrets.scopes.includes('user');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `openwop-provider-policy` predicate.
|
|
128
|
+
*
|
|
129
|
+
* @see spec/v1/profiles.md §`openwop-provider-policy`
|
|
130
|
+
*/
|
|
131
|
+
export function isProviderPolicy(c) {
|
|
132
|
+
if (!isCore(c))
|
|
133
|
+
return false;
|
|
134
|
+
if (c.aiProviders == null || typeof c.aiProviders !== 'object')
|
|
135
|
+
return false;
|
|
136
|
+
const policies = c.aiProviders.policies;
|
|
137
|
+
if (policies == null || typeof policies !== 'object')
|
|
138
|
+
return false;
|
|
139
|
+
if (!isStringArray(policies.modes))
|
|
140
|
+
return false;
|
|
141
|
+
if (policies.modes.length === 0)
|
|
142
|
+
return false;
|
|
143
|
+
return policies.modes.includes('optional');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* `openwop-node-packs` discovery-only predicate. Runtime registry behavior
|
|
147
|
+
* is verified by `pack-registry*.test.ts`. Discovery alone cannot tell
|
|
148
|
+
* whether GET /v1/packs returns a list-shaped body.
|
|
149
|
+
*
|
|
150
|
+
* @see spec/v1/profiles.md §`openwop-node-packs`
|
|
151
|
+
*/
|
|
152
|
+
export function isNodePacksDiscovery(c) {
|
|
153
|
+
return isCore(c);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* `openwop-replay-fork` predicate. Host advertises `replay.supported: true`
|
|
157
|
+
* with at least one entry in `replay.modes`. Runtime determinism /
|
|
158
|
+
* branch behavior is verified by `replayDeterminism.test.ts` and
|
|
159
|
+
* `replay-fork.test.ts`.
|
|
160
|
+
*
|
|
161
|
+
* @see spec/v1/profiles.md §`openwop-replay-fork`
|
|
162
|
+
* @see spec/v1/replay.md
|
|
163
|
+
*/
|
|
164
|
+
export function isReplayFork(c) {
|
|
165
|
+
if (!isCore(c))
|
|
166
|
+
return false;
|
|
167
|
+
if (c.replay == null || typeof c.replay !== 'object')
|
|
168
|
+
return false;
|
|
169
|
+
if (c.replay.supported !== true)
|
|
170
|
+
return false;
|
|
171
|
+
if (!isStringArray(c.replay.modes))
|
|
172
|
+
return false;
|
|
173
|
+
return c.replay.modes.length > 0;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* `openwop-fixtures` predicate (RFC 0003). Host advertises `fixtures` as a
|
|
177
|
+
* non-empty array of non-empty strings — fixture-workflow IDs the host
|
|
178
|
+
* has seeded. Per-fixture skip decisions are made by the suite via
|
|
179
|
+
* `lib/fixtures.ts`; the profile predicate is the all-up "any-advertised"
|
|
180
|
+
* check.
|
|
181
|
+
*
|
|
182
|
+
* @see spec/v1/profiles.md §`openwop-fixtures`
|
|
183
|
+
* @see spec/v1/capabilities.md §`fixtures`
|
|
184
|
+
* @see RFCS/0003-fixture-gating.md
|
|
185
|
+
*/
|
|
186
|
+
export function isFixtures(c) {
|
|
187
|
+
if (!isCore(c))
|
|
188
|
+
return false;
|
|
189
|
+
if (!Array.isArray(c.fixtures))
|
|
190
|
+
return false;
|
|
191
|
+
if (c.fixtures.length === 0)
|
|
192
|
+
return false;
|
|
193
|
+
return c.fixtures.every((id) => typeof id === 'string' && id.length > 0);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* `openwop-memory` predicate (RFC 0080). Host implements the reconciled
|
|
197
|
+
* memory-capability model at the core tier: a read/write `MemoryAdapter`
|
|
198
|
+
* (`memory.supported: true` and `memory.writable !== false`) plus a cross-run
|
|
199
|
+
* durable store (`agents.memoryBackends` includes `'long-term'`). Capability
|
|
200
|
+
* families are document-root properties of the discovery payload (RFC 0073),
|
|
201
|
+
* so this reads `c.memory` / `c.agents`, matching `isReplayFork`.
|
|
202
|
+
*
|
|
203
|
+
* @see spec/v1/profiles.md §`openwop-memory`
|
|
204
|
+
* @see spec/v1/agent-memory.md §"Memory capability model"
|
|
205
|
+
*/
|
|
206
|
+
export function isMemory(c) {
|
|
207
|
+
if (!isCore(c))
|
|
208
|
+
return false;
|
|
209
|
+
const memory = c.memory;
|
|
210
|
+
if (memory == null || typeof memory !== 'object')
|
|
211
|
+
return false;
|
|
212
|
+
if (memory.supported !== true)
|
|
213
|
+
return false;
|
|
214
|
+
if (memory.writable === false)
|
|
215
|
+
return false;
|
|
216
|
+
const agents = c.agents;
|
|
217
|
+
if (agents == null || !isStringArray(agents.memoryBackends))
|
|
218
|
+
return false;
|
|
219
|
+
return agents.memoryBackends.includes('long-term');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* `openwop-trigger-bridge` predicate (RFC 0083). Host composes the durable
|
|
223
|
+
* inbound-work contract: advertises the `triggerBridge`, has a `deadLetter`
|
|
224
|
+
* sink for exhausted deliveries, and has at least one durable inbound source
|
|
225
|
+
* (queue bus, durable webhooks, or scheduling). Capability families are
|
|
226
|
+
* document-root properties (RFC 0073), so this reads `c.triggerBridge` /
|
|
227
|
+
* `c.deadLetter` / `c.queueBus` / `c.webhooks` / `c.scheduling`.
|
|
228
|
+
*
|
|
229
|
+
* @see spec/v1/profiles.md §`openwop-trigger-bridge`
|
|
230
|
+
* @see spec/v1/trigger-bridge.md
|
|
231
|
+
*/
|
|
232
|
+
export function isTriggerBridge(c) {
|
|
233
|
+
if (!isCore(c))
|
|
234
|
+
return false;
|
|
235
|
+
const supported = (v) => v != null && typeof v === 'object' && v.supported === true;
|
|
236
|
+
if (!supported(c.triggerBridge))
|
|
237
|
+
return false;
|
|
238
|
+
if (!supported(c.deadLetter))
|
|
239
|
+
return false;
|
|
240
|
+
const webhooks = c.webhooks;
|
|
241
|
+
const durableSource = supported(c.queueBus) ||
|
|
242
|
+
supported(c.scheduling) ||
|
|
243
|
+
(webhooks != null && typeof webhooks === 'object' && webhooks.durable === true);
|
|
244
|
+
return durableSource;
|
|
245
|
+
}
|
|
246
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
247
|
+
// Operational annex: openwop-agent-platform (RFC 0085).
|
|
248
|
+
//
|
|
249
|
+
// NOT part of the closed `profiles.md` predicate catalog (PROFILE_NAMES /
|
|
250
|
+
// deriveProfiles above) — it is an operational ANNEX (the production-profile.md /
|
|
251
|
+
// auth-profiles.md pattern) combining a discovery predicate with required runtime
|
|
252
|
+
// conformance evidence + documentation + a badge. These helpers compute only the
|
|
253
|
+
// discovery-PREDICATE part; the live aggregate-evidence assertion (does every
|
|
254
|
+
// constituent scenario actually pass?) lives in agent-platform-profile.test.ts.
|
|
255
|
+
//
|
|
256
|
+
// @see spec/v1/agent-platform-profile.md
|
|
257
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
258
|
+
/** Narrow helper: a capability sub-block with `supported === true`. */
|
|
259
|
+
function blockSupported(v) {
|
|
260
|
+
return v != null && typeof v === 'object' && v.supported === true;
|
|
261
|
+
}
|
|
262
|
+
/** The `openwop-agent-platform` FLOOR (`partial`) discovery predicate — RFC 0085 §B. */
|
|
263
|
+
export function isAgentPlatformPartial(c) {
|
|
264
|
+
if (!isCore(c))
|
|
265
|
+
return false;
|
|
266
|
+
const agents = c.agents;
|
|
267
|
+
const httpClient = c.httpClient;
|
|
268
|
+
const replay = c.replay;
|
|
269
|
+
const nondet = c.nondeterminismPolicy;
|
|
270
|
+
return (blockSupported(agents?.manifestRuntime) &&
|
|
271
|
+
blockSupported(agents?.liveRuntime) &&
|
|
272
|
+
blockSupported(c.toolCatalog) &&
|
|
273
|
+
blockSupported(c.toolHooks) &&
|
|
274
|
+
blockSupported(httpClient?.safeFetch) &&
|
|
275
|
+
blockSupported(c.providerUsage) &&
|
|
276
|
+
blockSupported(c.prompts) &&
|
|
277
|
+
blockSupported(c.memory) &&
|
|
278
|
+
blockSupported(c.feedback) &&
|
|
279
|
+
(replay?.supported === true || nondet?.declared === true));
|
|
280
|
+
}
|
|
281
|
+
/** The `openwop-agent-platform` `full` discovery predicate (floor + governance tier) — RFC 0085 §B. */
|
|
282
|
+
export function isAgentPlatformFull(c) {
|
|
283
|
+
if (!isAgentPlatformPartial(c))
|
|
284
|
+
return false;
|
|
285
|
+
const agents = c.agents;
|
|
286
|
+
const memory = c.memory;
|
|
287
|
+
// Debug bundle is advertised at `capabilities.debugBundle.supported` (debug-bundle.md /
|
|
288
|
+
// RFC 0009), NOT under `production.*` — the production block only adds stricter truncation MUSTs.
|
|
289
|
+
const httpClient = c.httpClient;
|
|
290
|
+
return (blockSupported(c.authorization) &&
|
|
291
|
+
agents?.manifestRuntime?.installScope === 'tenant' &&
|
|
292
|
+
blockSupported(memory?.attribution) &&
|
|
293
|
+
blockSupported(c.debugBundle) &&
|
|
294
|
+
blockSupported(c.triggerBridge) &&
|
|
295
|
+
blockSupported(httpClient?.egressPolicy));
|
|
296
|
+
}
|
|
297
|
+
/** The host-reported annex status: `full` ⊃ `partial` ⊃ `none` (discovery-predicate only). */
|
|
298
|
+
export function agentPlatformStatus(c) {
|
|
299
|
+
if (isAgentPlatformFull(c))
|
|
300
|
+
return 'full';
|
|
301
|
+
if (isAgentPlatformPartial(c))
|
|
302
|
+
return 'partial';
|
|
303
|
+
return 'none';
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* The per-term satisfaction breakdown (RFC 0085 §D) — the richer interop signal
|
|
307
|
+
* alongside the flat `none`/`partial`/`full` ladder. Adoption is NON-CONTIGUOUS:
|
|
308
|
+
* a real host built feature-by-feature can satisfy `full`-tier terms (RBAC,
|
|
309
|
+
* memory-attribution, tenant-scoping) while still failing `floor` terms, so the
|
|
310
|
+
* flat status would understate it (reads identical to a do-nothing host). This
|
|
311
|
+
* returns exactly the term ids a host satisfies, so a `none` host honoring 6/16
|
|
312
|
+
* terms is distinguishable from one honoring 0/16.
|
|
313
|
+
*/
|
|
314
|
+
export function agentPlatformSatisfiedTerms(c) {
|
|
315
|
+
const agents = c.agents;
|
|
316
|
+
const httpClient = c.httpClient;
|
|
317
|
+
const memory = c.memory;
|
|
318
|
+
const replay = c.replay;
|
|
319
|
+
const nondet = c.nondeterminismPolicy;
|
|
320
|
+
const checks = [
|
|
321
|
+
// floor
|
|
322
|
+
['floor:agents.manifestRuntime', blockSupported(agents?.manifestRuntime)],
|
|
323
|
+
['floor:agents.liveRuntime', blockSupported(agents?.liveRuntime)],
|
|
324
|
+
['floor:toolCatalog', blockSupported(c.toolCatalog)],
|
|
325
|
+
['floor:toolHooks', blockSupported(c.toolHooks)],
|
|
326
|
+
['floor:httpClient.safeFetch', blockSupported(httpClient?.safeFetch)],
|
|
327
|
+
['floor:providerUsage', blockSupported(c.providerUsage)],
|
|
328
|
+
['floor:prompts', blockSupported(c.prompts)],
|
|
329
|
+
['floor:memory', blockSupported(c.memory)],
|
|
330
|
+
['floor:feedback', blockSupported(c.feedback)],
|
|
331
|
+
['floor:replay-or-nondeterminism', replay?.supported === true || nondet?.declared === true],
|
|
332
|
+
// full (governance)
|
|
333
|
+
['full:authorization', blockSupported(c.authorization)],
|
|
334
|
+
['full:tenant-installScope', agents?.manifestRuntime?.installScope === 'tenant'],
|
|
335
|
+
['full:memory.attribution', blockSupported(memory?.attribution)],
|
|
336
|
+
['full:debugBundle', blockSupported(c.debugBundle)],
|
|
337
|
+
['full:triggerBridge', blockSupported(c.triggerBridge)],
|
|
338
|
+
['full:egressPolicy', blockSupported(httpClient?.egressPolicy)],
|
|
339
|
+
];
|
|
340
|
+
return checks.filter(([, ok]) => ok).map(([id]) => id);
|
|
341
|
+
}
|
|
342
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
343
|
+
// `openwop-core-standard` operational-annex predicate (RFC 0088). Like the
|
|
344
|
+
// agent-platform annex above, this is NOT a closed-catalog profile (so it is
|
|
345
|
+
// absent from deriveProfiles) — it is an operational ANNEX whose claim is backed
|
|
346
|
+
// by the §C floor scenarios passing black-box. This helper computes only the §B
|
|
347
|
+
// discovery predicate (the floor of MUSTs with black-box production-path proof).
|
|
348
|
+
//
|
|
349
|
+
// @see spec/v1/core-standard-profile.md
|
|
350
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
351
|
+
/** The `openwop-core-standard` floor discovery predicate — RFC 0088 §B. */
|
|
352
|
+
export function isCoreStandard(c) {
|
|
353
|
+
return isCore(c) && isInterrupts(c) && (isStreamSse(c) || isStreamPoll(c));
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Derive the full profile set from a discovery payload.
|
|
357
|
+
*
|
|
358
|
+
* Returns a set sorted by `PROFILE_NAMES` order so output is stable
|
|
359
|
+
* across calls and across implementations.
|
|
360
|
+
*/
|
|
361
|
+
export function deriveProfiles(c) {
|
|
362
|
+
const result = [];
|
|
363
|
+
if (isCore(c))
|
|
364
|
+
result.push('openwop-core');
|
|
365
|
+
if (isInterrupts(c))
|
|
366
|
+
result.push('openwop-interrupts');
|
|
367
|
+
if (isStreamSse(c))
|
|
368
|
+
result.push('openwop-stream-sse');
|
|
369
|
+
if (isStreamPoll(c))
|
|
370
|
+
result.push('openwop-stream-poll');
|
|
371
|
+
if (isSecrets(c))
|
|
372
|
+
result.push('openwop-secrets');
|
|
373
|
+
if (isProviderPolicy(c))
|
|
374
|
+
result.push('openwop-provider-policy');
|
|
375
|
+
if (isNodePacksDiscovery(c))
|
|
376
|
+
result.push('openwop-node-packs');
|
|
377
|
+
if (isReplayFork(c))
|
|
378
|
+
result.push('openwop-replay-fork');
|
|
379
|
+
if (isFixtures(c))
|
|
380
|
+
result.push('openwop-fixtures');
|
|
381
|
+
if (isMemory(c))
|
|
382
|
+
result.push('openwop-memory');
|
|
383
|
+
if (isTriggerBridge(c))
|
|
384
|
+
result.push('openwop-trigger-bridge');
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* One-shot membership check.
|
|
389
|
+
*/
|
|
390
|
+
export function hasProfile(c, profile) {
|
|
391
|
+
switch (profile) {
|
|
392
|
+
case 'openwop-core':
|
|
393
|
+
return isCore(c);
|
|
394
|
+
case 'openwop-interrupts':
|
|
395
|
+
return isInterrupts(c);
|
|
396
|
+
case 'openwop-stream-sse':
|
|
397
|
+
return isStreamSse(c);
|
|
398
|
+
case 'openwop-stream-poll':
|
|
399
|
+
return isStreamPoll(c);
|
|
400
|
+
case 'openwop-secrets':
|
|
401
|
+
return isSecrets(c);
|
|
402
|
+
case 'openwop-provider-policy':
|
|
403
|
+
return isProviderPolicy(c);
|
|
404
|
+
case 'openwop-node-packs':
|
|
405
|
+
return isNodePacksDiscovery(c);
|
|
406
|
+
case 'openwop-replay-fork':
|
|
407
|
+
return isReplayFork(c);
|
|
408
|
+
case 'openwop-fixtures':
|
|
409
|
+
return isFixtures(c);
|
|
410
|
+
case 'openwop-memory':
|
|
411
|
+
return isMemory(c);
|
|
412
|
+
case 'openwop-trigger-bridge':
|
|
413
|
+
return isTriggerBridge(c);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
export const PROFILE_FLOOR_SCENARIOS = {
|
|
417
|
+
'openwop-core-standard': {
|
|
418
|
+
required: [
|
|
419
|
+
'runs-lifecycle.test.ts',
|
|
420
|
+
'discovery.test.ts',
|
|
421
|
+
'auth.test.ts',
|
|
422
|
+
'eventOrdering.test.ts',
|
|
423
|
+
'failure-path.test.ts',
|
|
424
|
+
'idempotency.test.ts',
|
|
425
|
+
'idempotency-key-determinism.test.ts',
|
|
426
|
+
'webhook-negative.test.ts',
|
|
427
|
+
'audit-log-verification.test.ts',
|
|
428
|
+
],
|
|
429
|
+
requiredAnyPrefix: ['interrupt-'],
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
/** Is `profile` derivable from a discovery document? Maps a profile name to its predicate (RFC 0089 §B(1)). */
|
|
433
|
+
export function profileDerivable(c, profile) {
|
|
434
|
+
if (profile === 'openwop-core-standard')
|
|
435
|
+
return isCoreStandard(c);
|
|
436
|
+
if (profile === 'openwop-agent-platform')
|
|
437
|
+
return agentPlatformStatus(c) !== 'none';
|
|
438
|
+
if (PROFILE_NAMES.includes(profile)) {
|
|
439
|
+
return deriveProfiles(c).includes(profile);
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
const scenarioBasename = (id) => id.split('/').pop() ?? id;
|
|
444
|
+
/**
|
|
445
|
+
* Verify a bundle's claim for one profile per RFC 0089 §B. A consumer MUST
|
|
446
|
+
* re-derive (this function) rather than trust `claimedProfiles` verbatim.
|
|
447
|
+
*/
|
|
448
|
+
export function verifyBundleProfile(bundle, profile) {
|
|
449
|
+
const derivable = profileDerivable(bundle.discovery.document, profile);
|
|
450
|
+
const floor = PROFILE_FLOOR_SCENARIOS[profile];
|
|
451
|
+
const passed = new Set(bundle.results.passed.map(scenarioBasename));
|
|
452
|
+
const missingFloor = floor ? floor.required.filter((r) => !passed.has(scenarioBasename(r))) : [];
|
|
453
|
+
const prefixOk = (floor?.requiredAnyPrefix ?? []).every((p) => [...passed].some((s) => s.startsWith(p)));
|
|
454
|
+
const floorProven = missingFloor.length === 0 && prefixOk;
|
|
455
|
+
return { profile, derivable, floorProven, valid: derivable && floorProven, missingFloor };
|
|
456
|
+
}
|
|
457
|
+
/** Verify every profile in `bundle.claimedProfiles`; the bundle is valid iff all claims are valid. */
|
|
458
|
+
export function verifyBundle(bundle) {
|
|
459
|
+
const verdicts = bundle.claimedProfiles.map((p) => verifyBundleProfile(bundle, p));
|
|
460
|
+
return { valid: verdicts.every((v) => v.valid), verdicts };
|
|
461
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "conformance-agent-channel-dispatch",
|
|
3
|
+
"name": "Conformance: Agent Channel Dispatch",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"description": "RFC 0082 §B. Single-node run whose `agent` binding pins a deployment CHANNEL (`channel: \"stable\"`) instead of an exact `version`. A host advertising `agents.deployment.supported:true` MUST resolve the channel to a concrete version at first resolution and record it as `resolvedChannel` + `resolvedAgentVersion` on `agent.invocation.started` (RFC 0077); a `:fork {mode:\"replay\"}` MUST re-read that recorded version and MUST NOT re-resolve a since-moved channel. Hosts that omit `agents.deployment` MUST reject this channel-bearing ref with `validation_error` (agent-ref.schema.json) and so cannot seed it. See agent-channel-dispatch.test.ts.",
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"id": "resolve",
|
|
9
|
+
"typeId": "core.identity",
|
|
10
|
+
"name": "Channel-bound agent dispatch",
|
|
11
|
+
"position": { "x": 0, "y": 0 },
|
|
12
|
+
"config": {},
|
|
13
|
+
"inputs": {},
|
|
14
|
+
"agent": {
|
|
15
|
+
"agentId": "core.conformance.channel-agent",
|
|
16
|
+
"channel": "stable"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"edges": [],
|
|
21
|
+
"triggers": [
|
|
22
|
+
{ "id": "manual", "type": "manual", "enabled": true }
|
|
23
|
+
],
|
|
24
|
+
"variables": [],
|
|
25
|
+
"metadata": { "tags": ["conformance", "multi-agent", "deployment", "channel", "rfc-0082"] },
|
|
26
|
+
"settings": { "timeout": 10000 }
|
|
27
|
+
}
|
package/fixtures.md
CHANGED
|
@@ -412,6 +412,21 @@ Hosts that don't ship a BYOK SecretResolver MAY return `404` / `422` on the star
|
|
|
412
412
|
|
|
413
413
|
---
|
|
414
414
|
|
|
415
|
+
## `conformance-agent-channel-dispatch` (RFC 0082 §B — production-path channel pin)
|
|
416
|
+
|
|
417
|
+
> **Status: capability-gated (RFC 0082 `agents.deployment`).** Only a host advertising `agents.deployment.supported:true` can seed this fixture — a host that omits `agents.deployment` MUST reject the channel-bearing `agent` ref with `validation_error` (`agent-ref.schema.json`). Exercised by `src/scenarios/agent-channel-dispatch.test.ts`.
|
|
418
|
+
|
|
419
|
+
- **Purpose**: prove the RFC 0082 §B channel resolve-and-pin contract from a real run graph (complementing `agent-deployment-lifecycle.test.ts` Leg 4, which uses the host-sample seam). A node binds a deployment CHANNEL (`agent.channel: "stable"`) instead of an exact `version`.
|
|
420
|
+
- **Topology**: a single `core.identity` node whose `agent` binding is `{ "agentId": "core.conformance.channel-agent", "channel": "stable" }` (no `version`). The host MUST have an active deployment of `core.conformance.channel-agent` on the `stable` channel.
|
|
421
|
+
- **Inputs**: none. Trigger `manual`.
|
|
422
|
+
- **Conformance test driver**:
|
|
423
|
+
1. POST `/v1/runs` with `{workflowId: "conformance-agent-channel-dispatch"}`; poll until terminal.
|
|
424
|
+
2. **Assert** the first `agent.invocation.started` carries `resolvedChannel: "stable"` and a concrete non-empty `resolvedAgentVersion` (the recorded fact, RFC 0077).
|
|
425
|
+
3. **Replay** via `POST /v1/runs/{runId}:fork {mode:"replay"}`; **assert** the fork's `agent.invocation.started` re-reads the SAME `resolvedAgentVersion`.
|
|
426
|
+
4. **(Seam-guarded)** Move the `stable` channel via the deployment seam; **assert** a replay of the original run STILL carries the original pin — never re-resolving the moved channel.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
415
430
|
## NodeModule registration
|
|
416
431
|
|
|
417
432
|
The fixtures reference these typeIds:
|
package/package.json
CHANGED
package/schemas/README.md
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
| `capabilities.schema.json` | `capabilities.md` | `/.well-known/openwop` response — protocolVersion + supportedEnvelopes + schemaVersions + limits + optional v1 discovery surface |
|
|
33
33
|
| `channel-written-payload.schema.json` | `channels-and-reducers.md` §Channel write event | Payload of the `channel.written` RunEvent — write input + reducer name |
|
|
34
34
|
| `chat-card-pack-manifest.schema.json` | `chat-card-packs.md` + RFC 0071 | DRAFT — manifest for `kind: "card"` registry packs (RFC 0071 Phase 2). Peer to the node/workflow-chain/prompt/artifact-type pack manifests; disjoint via the `kind` discriminator. Distributes AI chat cards: a prompt template + typed input subset bound to a typed `outputArtifactType`. |
|
|
35
|
+
| `conformance-certification-bundle.schema.json` | `conformance-certification.md` + RFC 0089 | DRAFT — machine-readable attestation binding a host's claimed profiles to the reproducible run that substantiates them (suite version + per-scenario pass list + host identity/commit + captured discovery document). Out-of-band; a consumer re-derives each claim via the §B binding rule. |
|
|
35
36
|
| `conversation-event.schema.json` | `channels-and-reducers.md` + conversation RFC | Multi-turn conversation event shape for orchestrator-driven HITL flows |
|
|
36
37
|
| `conversation-turn.schema.json` | `channels-and-reducers.md` + conversation RFC | Conversation turn shape for user/agent/system messages |
|
|
37
38
|
| `core-conformance-mock-agent-config.schema.json` | `node-packs.md` + RFC 0023 | Config shape for the conformance-only `core.conformance.mock-agent` typeId — drives `agent.*` event emission on cue (`mockReasoning` / `mockToolCalls` / `mockHandoff` / `mockDecision` / `mockConfidence`). Hosts MUST refuse this typeId for production tenants unless `capabilities.conformance.mockAgent` is advertised. |
|
|
@@ -769,6 +769,11 @@
|
|
|
769
769
|
"mockAgent": {
|
|
770
770
|
"type": "boolean",
|
|
771
771
|
"description": "RFC 0023 §B.2. When `true`, the host has registered the `core.conformance.mock-agent` typeId. The scenarios `agentReasoningEvents.test.ts` and `agentConfidenceEscalation.test.ts` rely on the typeId being reachable. Hosts that register the typeId only for workflow ids matching the conformance fixture prefix (`conformance-*`) and refuse it for other tenants MAY still advertise `true` — the advertisement says only that the typeId is reachable from the conformance suite, not that it is reachable from arbitrary workflows."
|
|
772
|
+
},
|
|
773
|
+
"certificationBundleUrl": {
|
|
774
|
+
"type": "string",
|
|
775
|
+
"format": "uri",
|
|
776
|
+
"description": "OPTIONAL (RFC 0089). URL of the host's most recent conformance certification bundle (`conformance-certification-bundle.schema.json`) — a machine-readable attestation binding this host's claimed profiles to the reproducible run that substantiates them. Omitting it is fully conformant; clients MUST tolerate its absence."
|
|
772
777
|
}
|
|
773
778
|
},
|
|
774
779
|
"additionalProperties": false
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/conformance-certification-bundle.schema.json",
|
|
4
|
+
"title": "OpenWOP Conformance Certification Bundle",
|
|
5
|
+
"description": "RFC 0089. A machine-readable attestation binding a host's claimed profiles to the reproducible run that substantiates them: suite version, per-scenario pass list, host identity + commit, and the captured discovery document. An out-of-band artifact emitted by the conformance harness (not a runtime wire message). A consumer MUST re-derive each claimed profile from the embedded `discovery.document` rather than trusting `claimedProfiles` verbatim (RFC 0089 §B).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["bundleVersion", "generatedAt", "generator", "suite", "host", "discovery", "claimedProfiles", "results"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"bundleVersion": {
|
|
11
|
+
"const": "1",
|
|
12
|
+
"description": "Certification-bundle format version. `1` for RFC 0089."
|
|
13
|
+
},
|
|
14
|
+
"generatedAt": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"format": "date-time",
|
|
17
|
+
"description": "RFC 3339 timestamp when the bundle was generated. Marks the bundle as a point-in-time attestation."
|
|
18
|
+
},
|
|
19
|
+
"generator": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"required": ["name", "version"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"name": { "type": "string", "description": "Tool that produced the bundle, e.g. `@openwop/openwop-conformance --certify`." },
|
|
25
|
+
"version": { "type": "string" }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"suite": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"required": ["package", "version"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"package": { "const": "@openwop/openwop-conformance" },
|
|
34
|
+
"version": { "type": "string", "description": "Exact conformance suite version the results were produced against." }
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"host": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"additionalProperties": false,
|
|
40
|
+
"required": ["name", "version"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"name": { "type": "string" },
|
|
43
|
+
"version": { "type": "string" },
|
|
44
|
+
"vendor": { "type": "string" },
|
|
45
|
+
"commit": { "type": "string", "description": "VCS commit / build id of the host under test, when known. Self-reported; authoritative only for independent-verifier-generated bundles (RFC 0089 §Security)." }
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"discovery": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"required": ["url", "sha256", "document"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"url": { "type": "string", "format": "uri", "description": "The `/.well-known/openwop` URL fetched for this run." },
|
|
54
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of the canonical-JSON serialization of `document`, so a verifier can confirm it matches a live fetch." },
|
|
55
|
+
"document": { "type": "object", "description": "The verbatim `/.well-known/openwop` discovery document captured for this run. Profile derivation re-runs against THIS document." }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"claimedProfiles": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": { "type": "string" },
|
|
61
|
+
"minItems": 0,
|
|
62
|
+
"description": "Profiles the host claims. A bundle MUST NOT list a profile its own `discovery.document` does not derive (RFC 0089 §B(1))."
|
|
63
|
+
},
|
|
64
|
+
"results": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"required": ["totals", "passed", "failed", "skipped"],
|
|
68
|
+
"properties": {
|
|
69
|
+
"totals": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"required": ["passed", "failed", "skipped", "total"],
|
|
73
|
+
"properties": {
|
|
74
|
+
"passed": { "type": "integer", "minimum": 0 },
|
|
75
|
+
"failed": { "type": "integer", "minimum": 0 },
|
|
76
|
+
"skipped": { "type": "integer", "minimum": 0 },
|
|
77
|
+
"total": { "type": "integer", "minimum": 0 }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"passed": { "type": "array", "items": { "type": "string" }, "description": "Stable scenario IDs that passed non-vacuously. The generator MUST NOT list a scenario here that did not run non-vacuously." },
|
|
81
|
+
"failed": { "type": "array", "items": { "type": "string" } },
|
|
82
|
+
"skipped": { "type": "array", "items": { "type": "string" } }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|