@openwop/openwop-conformance 1.72.2 → 1.98.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 (50) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +58 -0
  4. package/api/openapi.yaml +4 -1
  5. package/dist/cli.js +107 -1
  6. package/dist/lib/profiles.js +70 -4
  7. package/package.json +2 -1
  8. package/schemas/CORPUS-STAMP.json +2 -2
  9. package/schemas/README.md +2 -0
  10. package/schemas/capabilities.schema.json +2054 -572
  11. package/schemas/certification-bundle-v2.schema.json +108 -0
  12. package/schemas/run-event-payloads.schema.json +3411 -857
  13. package/schemas/run-event.schema.json +31 -9
  14. package/schemas/workflow-definition.schema.json +492 -130
  15. package/schemas/workload-identity.schema.json +73 -0
  16. package/src/cli.ts +119 -1
  17. package/src/lib/a2a-fake-peer.ts +20 -0
  18. package/src/lib/behavior-gate.ts +42 -7
  19. package/src/lib/llm-cache-key-recipe.ts +51 -0
  20. package/src/lib/mcp-fake-server.ts +20 -0
  21. package/src/lib/profiles.ts +95 -4
  22. package/src/lib/requirement-ledger.ts +138 -0
  23. package/src/lib/requirement-registry.ts +62 -0
  24. package/src/scenarios/a2a-version-negotiation.test.ts +159 -0
  25. package/src/scenarios/capability-example-root-layout.test.ts +113 -0
  26. package/src/scenarios/certification-bundle-v2.test.ts +157 -0
  27. package/src/scenarios/certification-floor-enforcement.test.ts +115 -0
  28. package/src/scenarios/compensation-behavior.test.ts +164 -0
  29. package/src/scenarios/compensation-profile.test.ts +175 -0
  30. package/src/scenarios/contract-provenance.test.ts +209 -0
  31. package/src/scenarios/core-manifest-and-extension-registry.test.ts +198 -0
  32. package/src/scenarios/discovery-canonical-family-no-shadow.test.ts +219 -0
  33. package/src/scenarios/effect-identity-composition.test.ts +129 -0
  34. package/src/scenarios/effect-identity-cross-scope.test.ts +82 -0
  35. package/src/scenarios/mcp-version-negotiation.test.ts +159 -0
  36. package/src/scenarios/multi-region-effect-vocabulary.test.ts +175 -0
  37. package/src/scenarios/multi-region-idempotency.test.ts +17 -7
  38. package/src/scenarios/openapi-resolved-paths.test.ts +127 -0
  39. package/src/scenarios/protocol-version-grammar.test.ts +119 -0
  40. package/src/scenarios/requirement-ledger.test.ts +162 -0
  41. package/src/scenarios/rfc-0147-self-audit.test.ts +104 -0
  42. package/src/scenarios/rfc-lifecycle-coherence.test.ts +137 -0
  43. package/src/scenarios/semantic-digest-v2.test.ts +128 -0
  44. package/src/scenarios/semantic-digest-vectors.test.ts +140 -0
  45. package/src/scenarios/spec-corpus-validity.test.ts +22 -8
  46. package/src/scenarios/strict-behavior-gate.test.ts +120 -0
  47. package/src/scenarios/versioned-composition-profiles.test.ts +183 -0
  48. package/src/scenarios/workload-identity-behavior.test.ts +188 -0
  49. package/src/scenarios/workload-identity-profile.test.ts +175 -0
  50. package/vectors/semantic-request-digest-v2.json +236 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * RFC 0154 §A/§B — workload identity and delegation, shape only.
3
+ *
4
+ * **What this proves:** the schema admits the identity and delegation shapes §A
5
+ * and §B describe, and — more importantly — **rejects credential material**. It
6
+ * verifies no host. RFC 0147 §A.5 forbids `Accepted` on shape-only evidence for
7
+ * a behavioral requirement, and §A's actual requirements are behavioral:
8
+ * cryptographically verify the presented identity, bind it to the request,
9
+ * resolve it to a principal before authorization, and fail closed when
10
+ * unresolvable. A schema cannot demonstrate any of those.
11
+ *
12
+ * What a schema *can* do is make the dangerous shape unrepresentable, and that
13
+ * is what this checks. The governing rule is negative: **raw certificates,
14
+ * tokens, proofs, and credentials MUST NOT enter these objects.** `subject` is
15
+ * an opaque identifier; `proofRef` and `thumbprintRef` are digest *references*.
16
+ * A verified identity is a fact about a completed verification, not a container
17
+ * for the material that proved it — and these objects are projected into events,
18
+ * spans, and audit records, which is precisely where credential material must
19
+ * never reach.
20
+ *
21
+ * The second rule the schema encodes is **identity is not authorization**
22
+ * (RFC 0147 R12). Knowing which workload called says nothing about what it may
23
+ * do. That one cannot be schema-enforced at all, which is why `audience` is
24
+ * required on a delegation: an identity minted for somewhere else is the
25
+ * confused-deputy path, and the audience check is what closes it.
26
+ *
27
+ * Server-free.
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { readFileSync } from 'node:fs';
32
+ import { join, resolve as pathResolve } from 'node:path';
33
+ import Ajv2020 from 'ajv/dist/2020.js';
34
+ import addFormats from 'ajv-formats';
35
+ import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
36
+
37
+ /**
38
+ * Schemas ship inside the package; RFC prose does not. The two need different
39
+ * treatment, and collapsing them onto one `ROOT` derived from `V1_DIR` is what
40
+ * broke this file in the published layout — `V1_DIR` is null there, and the
41
+ * `as string` cast carried the null straight into `join()`.
42
+ */
43
+ const RFCS_DIR = V1_DIR === null ? null : pathResolve(V1_DIR, '..', '..', 'RFCS');
44
+
45
+ function validator() {
46
+ const schema = JSON.parse(
47
+ readFileSync(join(SCHEMAS_DIR, 'workload-identity.schema.json'), 'utf8'),
48
+ ) as object;
49
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
50
+ addFormats(ajv);
51
+ return ajv.compile(schema);
52
+ }
53
+
54
+ const DIGEST = `sha256:${'a'.repeat(64)}`;
55
+
56
+ describe('RFC 0154 §A — workload identity shape', () => {
57
+ const validate = validator();
58
+
59
+ it('a verified identity validates', () => {
60
+ expect(
61
+ validate({ scheme: 'spiffe', subject: 'spiffe://example/dispatcher', issuer: 'spiffe://example', audience: 'openwop-host' }),
62
+ JSON.stringify(validate.errors),
63
+ ).toBe(true);
64
+ });
65
+
66
+ it('the object is closed — credential material cannot be smuggled in', () => {
67
+ // The governing rule, and the one a schema can actually enforce. These
68
+ // objects reach events, spans, and audit records; an open object is an
69
+ // invitation for a token to ride along into all three.
70
+ for (const extra of [
71
+ { certificate: '-----BEGIN CERTIFICATE-----' },
72
+ { token: 'eyJhbGciOiJIUzI1NiJ9.x.y' },
73
+ { privateKey: 'secret' },
74
+ { proof: 'raw-proof-bytes' },
75
+ ]) {
76
+ expect(
77
+ validate({ scheme: 'spiffe', subject: 'spiffe://example/x', ...extra }),
78
+ `RFC 0154 §A: raw credentials MUST NOT enter the identity object — rejected \`${Object.keys(extra)[0]}\``,
79
+ ).toBe(false);
80
+ }
81
+ });
82
+
83
+ it('the scheme is a closed enum', () => {
84
+ expect(
85
+ validate({ scheme: 'trust-me', subject: 'x' }),
86
+ 'RFC 0154 §A: an unrecognized scheme is a verification path nobody implemented. Accepting ' +
87
+ 'the NAME without the verification is exactly the failure this profile prevents.',
88
+ ).toBe(false);
89
+ });
90
+
91
+ it('subject is required and non-empty', () => {
92
+ expect(validate({ scheme: 'spiffe' })).toBe(false);
93
+ expect(validate({ scheme: 'spiffe', subject: '' })).toBe(false);
94
+ });
95
+
96
+ it('a key binding carries a digest reference, never a key', () => {
97
+ expect(validate({ scheme: 'mtls-san', subject: 'x', keyBinding: { method: 'mtls', thumbprintRef: DIGEST } })).toBe(true);
98
+ expect(
99
+ validate({ scheme: 'mtls-san', subject: 'x', keyBinding: { method: 'mtls', thumbprintRef: 'MIIBIjANBgkqh' } }),
100
+ 'RFC 0154 §A: `thumbprintRef` is pattern-constrained to a sha256 digest so a raw key cannot ' +
101
+ 'be pasted here and still validate.',
102
+ ).toBe(false);
103
+ expect(validate({ scheme: 'mtls-san', subject: 'x', keyBinding: { method: 'telepathy' } })).toBe(false);
104
+ });
105
+ });
106
+
107
+ describe('RFC 0154 §B — delegated actor chain', () => {
108
+ const validate = validator();
109
+ const base = { scheme: 'spiffe' as const, subject: 'spiffe://example/planner' };
110
+
111
+ it('a verified delegation validates', () => {
112
+ expect(
113
+ validate({
114
+ ...base,
115
+ delegation: {
116
+ chain: [{ subject: 'spiffe://example/dispatcher', issuer: 'spiffe://example' }],
117
+ audience: 'openwop-host',
118
+ expiresAt: '2026-08-13T16:00:00Z',
119
+ proofRef: DIGEST,
120
+ },
121
+ }),
122
+ JSON.stringify(validate.errors),
123
+ ).toBe(true);
124
+ });
125
+
126
+ it('audience is required on a delegation', () => {
127
+ // The confused-deputy guard (RFC 0147 R12). An identity minted for somewhere
128
+ // else, accepted here, is how a credential valid elsewhere becomes a
129
+ // credential valid here.
130
+ expect(
131
+ validate({ ...base, delegation: { chain: [{ subject: 'x' }] } }),
132
+ 'RFC 0154 §B: a delegation without an audience cannot be checked against this host.',
133
+ ).toBe(false);
134
+ });
135
+
136
+ it('an empty chain is rejected', () => {
137
+ expect(
138
+ validate({ ...base, delegation: { chain: [], audience: 'openwop-host' } }),
139
+ 'RFC 0154 §B: a delegation context with no chain claims a delegation nobody can inspect.',
140
+ ).toBe(false);
141
+ });
142
+
143
+ it('proofRef is a digest reference, not the proof', () => {
144
+ expect(
145
+ validate({ ...base, delegation: { chain: [{ subject: 'x' }], audience: 'h', proofRef: 'eyJhbGciOiJIUzI1NiJ9.a.b' } }),
146
+ 'RFC 0154 §B: `proofRef` is a REFERENCE. A JWT pasted here would put the proof itself into ' +
147
+ 'every event and span this object reaches.',
148
+ ).toBe(false);
149
+ });
150
+
151
+ it('a chain hop cannot carry extra fields', () => {
152
+ expect(validate({ ...base, delegation: { chain: [{ subject: 'x', token: 'y' }], audience: 'h' } })).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe.skipIf(RFCS_DIR === null)('RFC 0154 — what this does NOT establish', () => {
157
+ it('the behavioral and external-audit items stay unticked and annotated', () => {
158
+ // §A's real requirements are behavioral — verify, bind, resolve, fail closed
159
+ // — and none is schema-checkable. §A.5 forbids `Accepted` on this evidence.
160
+ const rfc = readFileSync(
161
+ join(RFCS_DIR as string, '0154-workload-identity-delegation-telemetry-and-provenance.md'),
162
+ 'utf8',
163
+ );
164
+ for (const needle of [
165
+ 'Verification, fail-closed authorization, sender constraint, and negative chain tests pass.',
166
+ 'External auditor reviews the identity and provenance implementation.',
167
+ ]) {
168
+ expect(
169
+ new RegExp(`- \\[ \\] ${needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`).test(rfc),
170
+ `RFC 0154: "${needle}" MUST remain unticked and annotated — a schema cannot demonstrate ` +
171
+ 'cryptographic verification, and no host implements this profile.',
172
+ ).toBe(true);
173
+ }
174
+ });
175
+ });
@@ -0,0 +1,236 @@
1
+ {
2
+ "$comment": [
3
+ "RFC 0150 §C — golden vectors for the semantic request digest v2.",
4
+ "",
5
+ "These are the NORMATIVE cross-language vectors. An SDK implementing the digest",
6
+ "MUST reproduce every `digest` below from its `input`. They exist because §C's",
7
+ "acceptance criterion is that TypeScript, Python, and Go agree — and three",
8
+ "independent readings of prose is exactly how they would fail to.",
9
+ "",
10
+ "Each vector pins one rule, and several come in pairs whose whole purpose is the",
11
+ "relationship between them: tools sorted vs reversed MUST match, message order",
12
+ "reversed MUST NOT, and the two Unicode forms MUST NOT — because JCS does not",
13
+ "apply NFC and a fallback that adds it silently breaks cross-host agreement.",
14
+ "",
15
+ "`canonical` is the exact JCS byte string hashed. It is included so a failing",
16
+ "implementation can diff the preimage rather than stare at two hashes."
17
+ ],
18
+ "recipe": "openwop-semantic-request-v2",
19
+ "spec": "spec/v1/replay.md §\"LLM cache-key recipe\"",
20
+ "rfc": "RFC 0150 §C",
21
+ "hash": "sha256, lowercase hex, over the JCS-canonical UTF-8 bytes of `canonical`",
22
+ "vectors": [
23
+ {
24
+ "id": "minimal",
25
+ "why": "The floor: provider, model, one message. Anything simpler is not a request.",
26
+ "input": {
27
+ "provider": "anthropic",
28
+ "model": "claude-x",
29
+ "messages": [
30
+ {
31
+ "role": "user",
32
+ "content": "hello"
33
+ }
34
+ ]
35
+ },
36
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"hello\",\"role\":\"user\"}]}}",
37
+ "digest": "c92498949e9cc0e38bb2887e88e55b6936932e219b8344f0a7059b1de960073d"
38
+ },
39
+ {
40
+ "id": "stop-changes-digest",
41
+ "why": "v1 EXCLUDED `stop`. Two requests differing only here produce different text, so they must not share a digest.",
42
+ "input": {
43
+ "provider": "anthropic",
44
+ "model": "claude-x",
45
+ "messages": [
46
+ {
47
+ "role": "user",
48
+ "content": "hello"
49
+ }
50
+ ],
51
+ "stop": [
52
+ "END"
53
+ ]
54
+ },
55
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"hello\",\"role\":\"user\"}],\"stop\":[\"END\"]}}",
56
+ "digest": "2103c61f7b0e1e05c6535e562f52335649beae49e266b8264e60bc93c169dd87"
57
+ },
58
+ {
59
+ "id": "seed-changes-digest",
60
+ "why": "v1 EXCLUDED `seed`. Its entire purpose is to change the output.",
61
+ "input": {
62
+ "provider": "anthropic",
63
+ "model": "claude-x",
64
+ "messages": [
65
+ {
66
+ "role": "user",
67
+ "content": "hello"
68
+ }
69
+ ],
70
+ "seed": 7
71
+ },
72
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"hello\",\"role\":\"user\"}],\"seed\":7}}",
73
+ "digest": "fb942dce9de88688c89d048499f4a4e5a19427775da08d0d5a7ad709c10b42d0"
74
+ },
75
+ {
76
+ "id": "max-output-changes-digest",
77
+ "why": "v1 EXCLUDED the output bound. It decides whether the response is truncated.",
78
+ "input": {
79
+ "provider": "anthropic",
80
+ "model": "claude-x",
81
+ "messages": [
82
+ {
83
+ "role": "user",
84
+ "content": "hello"
85
+ }
86
+ ],
87
+ "maxOutputTokens": 256
88
+ },
89
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"maxOutputTokens\":256,\"messages\":[{\"content\":\"hello\",\"role\":\"user\"}]}}",
90
+ "digest": "96f74b752619868e3b7fad15f093c03b86b223974ea3e2b89a1831d22a4845ad"
91
+ },
92
+ {
93
+ "id": "tools-sorted-by-name",
94
+ "why": "Tool order is not semantic; the recipe sorts by name so two orderings agree.",
95
+ "input": {
96
+ "provider": "anthropic",
97
+ "model": "claude-x",
98
+ "messages": [
99
+ {
100
+ "role": "user",
101
+ "content": "hello"
102
+ }
103
+ ],
104
+ "tools": [
105
+ {
106
+ "name": "zeta",
107
+ "parameters": {}
108
+ },
109
+ {
110
+ "name": "alpha",
111
+ "parameters": {}
112
+ }
113
+ ]
114
+ },
115
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"hello\",\"role\":\"user\"}],\"tools\":[{\"name\":\"alpha\",\"parameters\":{}},{\"name\":\"zeta\",\"parameters\":{}}]}}",
116
+ "digest": "afb590dd7bf6c78915852e15318fd4c9d6d6e0d473288283f413e5e425ba0cd6"
117
+ },
118
+ {
119
+ "id": "tools-reversed-same-digest",
120
+ "why": "Same tools, opposite input order. MUST equal the previous vector.",
121
+ "input": {
122
+ "provider": "anthropic",
123
+ "model": "claude-x",
124
+ "messages": [
125
+ {
126
+ "role": "user",
127
+ "content": "hello"
128
+ }
129
+ ],
130
+ "tools": [
131
+ {
132
+ "name": "alpha",
133
+ "parameters": {}
134
+ },
135
+ {
136
+ "name": "zeta",
137
+ "parameters": {}
138
+ }
139
+ ]
140
+ },
141
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"hello\",\"role\":\"user\"}],\"tools\":[{\"name\":\"alpha\",\"parameters\":{}},{\"name\":\"zeta\",\"parameters\":{}}]}}",
142
+ "digest": "afb590dd7bf6c78915852e15318fd4c9d6d6e0d473288283f413e5e425ba0cd6"
143
+ },
144
+ {
145
+ "id": "message-order-is-semantic",
146
+ "why": "Message order IS semantic and MUST NOT be sorted — this must differ from a reordered twin.",
147
+ "input": {
148
+ "provider": "anthropic",
149
+ "model": "claude-x",
150
+ "messages": [
151
+ {
152
+ "role": "user",
153
+ "content": "a"
154
+ },
155
+ {
156
+ "role": "assistant",
157
+ "content": "b"
158
+ }
159
+ ]
160
+ },
161
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"a\",\"role\":\"user\"},{\"content\":\"b\",\"role\":\"assistant\"}]}}",
162
+ "digest": "06eb9873af60fb5c98222161bafdaec8f13a0f59b7b2c62a608a82a09520a74a"
163
+ },
164
+ {
165
+ "id": "message-order-reversed",
166
+ "why": "The reordered twin. MUST differ from the previous vector.",
167
+ "input": {
168
+ "provider": "anthropic",
169
+ "model": "claude-x",
170
+ "messages": [
171
+ {
172
+ "role": "assistant",
173
+ "content": "b"
174
+ },
175
+ {
176
+ "role": "user",
177
+ "content": "a"
178
+ }
179
+ ]
180
+ },
181
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"b\",\"role\":\"assistant\"},{\"content\":\"a\",\"role\":\"user\"}]}}",
182
+ "digest": "896b9d814f5d9fe5da72fa50e2d2ea9751d9e2b78da324729dcfbd3c8bb09109"
183
+ },
184
+ {
185
+ "id": "provider-options-carried",
186
+ "why": "Unknown provider options are namespaced and hashed, never dropped.",
187
+ "input": {
188
+ "provider": "anthropic",
189
+ "model": "claude-x",
190
+ "messages": [
191
+ {
192
+ "role": "user",
193
+ "content": "hello"
194
+ }
195
+ ],
196
+ "providerOptions": {
197
+ "vendor.anthropic.reasoningEffort": "high"
198
+ }
199
+ },
200
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"providerOptions\":{\"vendor.anthropic.reasoningEffort\":\"high\"},\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"hello\",\"role\":\"user\"}]}}",
201
+ "digest": "fa34555d16eb6fedb2c587a4c4ac7e2cbeec8d0e8be0299b60910fe79c4055bf"
202
+ },
203
+ {
204
+ "id": "non-ascii-not-normalized",
205
+ "why": "JCS does not apply NFC. The decomposed form MUST NOT hash equal to the composed form.",
206
+ "input": {
207
+ "provider": "anthropic",
208
+ "model": "claude-x",
209
+ "messages": [
210
+ {
211
+ "role": "user",
212
+ "content": "é"
213
+ }
214
+ ]
215
+ },
216
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"é\",\"role\":\"user\"}]}}",
217
+ "digest": "7365573baec75c4eeda16eed9d60a17aa18cd44871f652d7dd25e602941956e9"
218
+ },
219
+ {
220
+ "id": "non-ascii-composed",
221
+ "why": "The composed twin (U+00E9). MUST differ from the decomposed vector above.",
222
+ "input": {
223
+ "provider": "anthropic",
224
+ "model": "claude-x",
225
+ "messages": [
226
+ {
227
+ "role": "user",
228
+ "content": "é"
229
+ }
230
+ ]
231
+ },
232
+ "canonical": "{\"model\":\"claude-x\",\"provider\":\"anthropic\",\"recipe\":\"openwop-semantic-request-v2\",\"request\":{\"messages\":[{\"content\":\"é\",\"role\":\"user\"}]}}",
233
+ "digest": "a4a0d2077fa984fe50411e671a77ee4cdab81911cf6bffe33b4f680c696b01af"
234
+ }
235
+ ]
236
+ }