@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
@@ -1576,16 +1576,30 @@ describe.skipIf(RFC0089_BUNDLE_PATH === null || !existsSync(RFC0089_BUNDLE_PATH)
1576
1576
  expect(ok, JSON.stringify(validate.errors)).toBe(true);
1577
1577
  });
1578
1578
 
1579
- it('verifyBundle ACCEPTS the committed reference bundle — every claimed profile re-derives + is floor-proven (§B)', () => {
1579
+ it('verifyBundle REJECTS the committed reference bundle — it is `invalidated` (RFC 0148 §D)', () => {
1580
+ // This assertion was inverted on 2026-08-12. It previously asserted the
1581
+ // bundle was ACCEPTED, commented "the host honestly claims ONLY profiles its
1582
+ // discovery document derives, none of which it fails a floor scenario for."
1583
+ // That comment was false: the bundle claims `openwop-stream-sse` while all
1584
+ // three `stream-modes*` scenarios sit in its own `results.failed`. The old
1585
+ // assertion passed only because those profiles had no floor definition, so
1586
+ // `floorProven` came out vacuously true — a test defending a claim the
1587
+ // bundle's own failure list contradicts.
1588
+ //
1589
+ // The bundle is now marked `invalidated` in
1590
+ // `docs/CERTIFICATION-BUNDLE-INVENTORY.md`; reissue requires bundle v2.
1591
+ // Until then the correct expectation is rejection, and the reasons are
1592
+ // asserted individually so a future reissue cannot turn this green for the
1593
+ // wrong cause.
1580
1594
  const bundle = readJson(bundlePath) as Parameters<typeof verifyBundle>[0];
1581
1595
  const r = verifyBundle(bundle);
1582
- // The host honestly claims ONLY profiles its discovery document derives, none
1583
- // of which it fails a floor scenario for — so verifyBundle MUST accept it.
1584
- const offending = r.verdicts.filter((v) => !v.valid);
1585
- expect(
1586
- r.valid,
1587
- `verifyBundle rejected claimed profile(s): ${JSON.stringify(offending)}`,
1588
- ).toBe(true);
1596
+ expect(r.valid, 'the committed v1 bundle is invalidated, not merely historical').toBe(false);
1597
+
1598
+ const sse = r.verdicts.find((v) => v.profile === 'openwop-stream-sse');
1599
+ expect(sse?.floorProven, 'profiles.md §openwop-stream-sse: predicate AND those scenarios pass').toBe(false);
1600
+ expect(sse?.missingFloor, 'its own results.failed lists the stream-modes scenarios').toContain(
1601
+ 'stream-modes.test.ts',
1602
+ );
1589
1603
  });
1590
1604
 
1591
1605
  it('discovery.sha256 is the canonical-JSON SHA-256 of the captured discovery.document', () => {
@@ -0,0 +1,120 @@
1
+ /**
2
+ * RFC 0148 §B — strict behavior, and the MUST NOT that was enforced by a warning.
3
+ *
4
+ * §B: "When a host advertises a capability, every required behavioral assertion
5
+ * for that capability MUST execute or fail. `OPENWOP_REQUIRE_BEHAVIOR=true` MUST
6
+ * fail on `blocked`, unclassified early return, or a missing seam unless the
7
+ * profile was explicitly opted out before discovery capture. **A host MUST NOT
8
+ * both advertise and opt out of the same profile.**"
9
+ *
10
+ * That last sentence was not enforced. `behaviorGate()` detected the
11
+ * contradiction, emitted `console.warn`, and proceeded as if advertised. A
12
+ * warning in a conformance run is not a check — nothing consumes it, nothing
13
+ * fails on it, and a certification bundle produced from that run records a pass.
14
+ *
15
+ * It matters because the two claims are opposite in kind. Advertising says *this
16
+ * host implements the profile*; opting out says *the operator declares it does
17
+ * not*. A run where both are true has no defensible reading, and the one the
18
+ * gate chose — advertisement wins — is the one that produces MORE certification
19
+ * claim from a MORE contradictory input.
20
+ *
21
+ * The second half wires §B to §A: a gate decision now records a ledger
22
+ * disposition, so "skipped because opted out" and "not exercised at all" stop
23
+ * being the same observable. Before the ledger there was nothing to record into,
24
+ * which is why §B could not be implemented before §A.
25
+ *
26
+ * Server-free. Manipulates process env, so it restores it per-test.
27
+ */
28
+
29
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
30
+ import { behaviorGate } from '../lib/behavior-gate.js';
31
+ import { dispositionOf, resetLedger } from '../lib/requirement-ledger.js';
32
+ import { __resetEnvCacheForTests } from '../lib/env.js';
33
+
34
+ const ENV_KEYS = ['OPENWOP_REQUIRE_BEHAVIOR', 'OPENWOP_OPTED_OUT_PROFILES', 'OPENWOP_BASE_URL', 'OPENWOP_API_KEY'] as const;
35
+ let saved: Record<string, string | undefined> = {};
36
+
37
+ beforeEach(() => {
38
+ resetLedger();
39
+ saved = {};
40
+ for (const k of ENV_KEYS) {
41
+ saved[k] = process.env[k];
42
+ delete process.env[k];
43
+ }
44
+ // `loadEnv()` requires a base URL AND an api key even on paths that reach no
45
+ // host, and it caches — so both are stubbed and the cache dropped per test.
46
+ process.env['OPENWOP_BASE_URL'] = 'https://conformance.invalid';
47
+ process.env['OPENWOP_API_KEY'] = 'dummy-not-used-no-request-is-made';
48
+ __resetEnvCacheForTests();
49
+ });
50
+
51
+ afterEach(() => {
52
+ for (const k of ENV_KEYS) {
53
+ if (saved[k] === undefined) delete process.env[k];
54
+ else process.env[k] = saved[k];
55
+ }
56
+ __resetEnvCacheForTests();
57
+ });
58
+
59
+ describe('RFC 0148 §B — advertise and opt-out are mutually exclusive', () => {
60
+ it('advertising AND opting out of the same profile fails', () => {
61
+ // The defect: this was a `console.warn` that then proceeded as advertised.
62
+ // A MUST NOT enforced by a warning is not enforced — nothing consumes the
63
+ // warning, and the bundle produced from that run records a pass.
64
+ process.env['OPENWOP_OPTED_OUT_PROFILES'] = 'openwop-audit-log-integrity';
65
+ __resetEnvCacheForTests();
66
+ expect(
67
+ () => behaviorGate('openwop-audit-log-integrity', true),
68
+ 'RFC 0148 §B: "A host MUST NOT both advertise and opt out of the same profile." ' +
69
+ 'The two claims are opposite in kind — one says the host implements the profile, the ' +
70
+ 'other says the operator declares it does not. A run where both hold has no defensible ' +
71
+ 'reading, and resolving it in favour of advertisement extracts MORE certification claim ' +
72
+ 'from a MORE contradictory input.',
73
+ ).toThrow(/MUST NOT both advertise and opt out/);
74
+ });
75
+
76
+ it('advertising alone proceeds', () => {
77
+ expect(behaviorGate('openwop-audit-log-integrity', true)).toBe(true);
78
+ });
79
+
80
+ it('opting out alone skips, in strict mode too', () => {
81
+ process.env['OPENWOP_OPTED_OUT_PROFILES'] = 'openwop-audit-log-integrity';
82
+ __resetEnvCacheForTests();
83
+ process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
84
+ __resetEnvCacheForTests();
85
+ expect(behaviorGate('openwop-audit-log-integrity', false)).toBe(false);
86
+ });
87
+
88
+ it('strict mode fails an unadvertised, non-opted-out profile', () => {
89
+ process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
90
+ __resetEnvCacheForTests();
91
+ expect(() => behaviorGate('openwop-audit-log-integrity', false)).toThrow();
92
+ });
93
+ });
94
+
95
+ describe('RFC 0148 §B — gate decisions record a ledger disposition', () => {
96
+ it('an honest opt-out records `skipped`, not silence', () => {
97
+ // §A resolves an unrecorded requirement to `blocked`. Without this wiring an
98
+ // opted-out profile and a never-run profile are the same observable, which
99
+ // is the distinction §B exists to make.
100
+ process.env['OPENWOP_OPTED_OUT_PROFILES'] = 'openwop-audit-log-integrity';
101
+ __resetEnvCacheForTests();
102
+ behaviorGate('openwop-audit-log-integrity', false);
103
+ expect(dispositionOf('openwop.profile.openwop-audit-log-integrity')).toBe('skipped');
104
+ });
105
+
106
+ it('a default-mode soft-skip records `inapplicable`', () => {
107
+ // The host does not advertise it and the operator made no declaration, so
108
+ // the requirement does not apply to this discovery set. That is a different
109
+ // statement from "we could not check", and it is certifiable where
110
+ // `blocked` is not.
111
+ behaviorGate('openwop-audit-log-integrity', false);
112
+ expect(dispositionOf('openwop.profile.openwop-audit-log-integrity')).toBe('inapplicable');
113
+ });
114
+
115
+ it('an unexercised profile stays `blocked`', () => {
116
+ // Nothing called the gate at all. §A's default holds: silence is not a pass,
117
+ // and it is not certifiable either.
118
+ expect(dispositionOf('openwop.profile.openwop-never-touched')).toBe('blocked');
119
+ });
120
+ });
@@ -0,0 +1,183 @@
1
+ /**
2
+ * RFC 0152 + RFC 0153 — versioned A2A and MCP composition, shape only.
3
+ *
4
+ * **Stated first, because RFC 0147 §A.5 turns on it:** this file proves the
5
+ * discovery schema admits the versioned shapes §A describes and rejects the ones
6
+ * it forbids. It contacts no peer. It is therefore **not** evidence that this
7
+ * host interoperates with any A2A 1.0 or MCP 2026-07-28 implementation, and
8
+ * neither RFC can reach a defensible `Accepted` on it. Both additionally require
9
+ * a real upstream peer in CI, which is not a corpus deliverable.
10
+ *
11
+ * The defect both RFCs address is the same, which is why they land together:
12
+ * **`supported: true` with no version is a claim a peer cannot negotiate
13
+ * against.** It asserts the host speaks *some* A2A or *some* MCP. Two hosts can
14
+ * both advertise it, share no revision, and discover that only when a call
15
+ * fails — and the failure surfaces at the peer, not at the handshake that was
16
+ * supposed to prevent it. Versioning the advertisement moves the disagreement to
17
+ * the one place both sides are looking.
18
+ *
19
+ * Two shape choices worth keeping:
20
+ *
21
+ * - **MCP versions are date-patterned, not free strings.** MCP revisions *are*
22
+ * dates. Accepting `latest` or `2026-7-28` would let two hosts disagree
23
+ * about which revision they share while both validate.
24
+ * - **The MCP feature list is closed.** An unrecognized feature name is
25
+ * indistinguishable from a typo, and a peer that silently ignores one has
26
+ * negotiated a capability neither side implements.
27
+ *
28
+ * Server-free.
29
+ */
30
+
31
+ import { describe, it, expect } from 'vitest';
32
+ import { readFileSync } from 'node:fs';
33
+ import { join, resolve as pathResolve } from 'node:path';
34
+ import Ajv2020 from 'ajv/dist/2020.js';
35
+ import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
36
+
37
+ /**
38
+ * Schemas ship inside the package; RFC prose does not. Deriving both from
39
+ * `V1_DIR` — null in the published tarball — and casting the null away with
40
+ * `as string` is what took this file down at import when installed from npm.
41
+ */
42
+ const RFCS_DIR = V1_DIR === null ? null : pathResolve(V1_DIR, '..', '..', 'RFCS');
43
+
44
+ function caps(): Record<string, unknown> {
45
+ return JSON.parse(
46
+ readFileSync(join(SCHEMAS_DIR, 'capabilities.schema.json'), 'utf8'),
47
+ ) as Record<string, unknown>;
48
+ }
49
+
50
+ /** Compile one capability family in isolation. */
51
+ function familyValidator(family: string) {
52
+ const schema = caps() as { properties: Record<string, object> };
53
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
54
+ return ajv.compile(schema.properties[family] as object);
55
+ }
56
+
57
+ describe('RFC 0152 §A — A2A versioned discovery', () => {
58
+ const validate = familyValidator('a2a');
59
+
60
+ /**
61
+ * The `a2a` family independently requires `agentCardUrl` (RFC 0100: it must
62
+ * GET-resolve credential-less). Every fixture below carries it.
63
+ *
64
+ * This is not tidiness. Without it the NEGATIVE legs would have failed
65
+ * validation for the missing card URL rather than for the version defect they
66
+ * name — passing for the wrong reason, which is the vacuity this whole
67
+ * program exists to close, committed inside a test written to prevent it.
68
+ */
69
+ const a2a = (over: Record<string, unknown>) => ({
70
+ supported: true,
71
+ agentCardUrl: 'https://host.example/.well-known/agent-card.json',
72
+ ...over,
73
+ });
74
+
75
+ it('a versioned advertisement validates', () => {
76
+ expect(
77
+ validate(a2a({
78
+ protocolVersions: ['1.0', '0.3'],
79
+ preferredVersion: '1.0',
80
+ profiles: ['a2a-1.0', 'a2a-0.3-legacy'],
81
+ durableTasks: true,
82
+ })),
83
+ JSON.stringify(validate.errors),
84
+ ).toBe(true);
85
+ });
86
+
87
+ it('a legacy-only host can say so explicitly', () => {
88
+ // The point of naming the legacy profile: a deprecation you can see is one
89
+ // you can time-bound. A bare `supported: true` hides the same fact.
90
+ expect(
91
+ validate(a2a({ protocolVersions: ['0.3'], preferredVersion: '0.3', profiles: ['a2a-0.3-legacy'] })),
92
+ JSON.stringify(validate.errors),
93
+ ).toBe(true);
94
+ });
95
+
96
+ it('an empty version list is rejected', () => {
97
+ expect(
98
+ validate(a2a({ protocolVersions: [] })),
99
+ 'RFC 0152 §A: an A2A-capable host MUST advertise a NON-EMPTY `protocolVersions`. An empty ' +
100
+ 'array is the bare `supported: true` problem wearing a schema — it validates and tells a ' +
101
+ 'peer nothing it can negotiate against.',
102
+ ).toBe(false);
103
+ });
104
+
105
+ it('duplicate versions are rejected', () => {
106
+ expect(validate(a2a({ protocolVersions: ['1.0', '1.0'] }))).toBe(false);
107
+ });
108
+
109
+ it('a malformed version is rejected', () => {
110
+ for (const v of ['1', 'v1.0', 'latest', '1.0.0']) {
111
+ expect(
112
+ validate(a2a({ protocolVersions: [v] })),
113
+ `RFC 0152 §A: '${v}' is not an A2A major.minor version`,
114
+ ).toBe(false);
115
+ }
116
+ });
117
+
118
+ it('a malformed profile id is rejected', () => {
119
+ expect(validate(a2a({ protocolVersions: ['1.0'], profiles: ['a2a-latest'] }))).toBe(false);
120
+ expect(validate(a2a({ protocolVersions: ['1.0'], profiles: ['a2a-1.0'] }))).toBe(true);
121
+ });
122
+ });
123
+
124
+ describe('RFC 0153 §A — MCP versioned discovery', () => {
125
+ const validate = familyValidator('mcp');
126
+
127
+ it('a versioned advertisement validates', () => {
128
+ expect(
129
+ validate({
130
+ supported: true,
131
+ protocolVersions: ['2026-07-28', '2025-06-18'],
132
+ preferredVersion: '2026-07-28',
133
+ profiles: ['mcp-2026-07-28', 'mcp-2025-06-18-legacy'],
134
+ features: ['server-discover', 'mrtr', 'cacheable-lists', 'extensions'],
135
+ }),
136
+ JSON.stringify(validate.errors),
137
+ ).toBe(true);
138
+ });
139
+
140
+ it('versions use MCP date form exactly', () => {
141
+ // MCP revisions ARE dates. `latest` or a non-padded month would let two
142
+ // hosts disagree about which revision they share while both validate.
143
+ for (const v of ['latest', '2026-7-28', '2026-07', 'v2026-07-28']) {
144
+ expect(
145
+ validate({ supported: true, protocolVersions: [v] }),
146
+ `RFC 0153 §A: '${v}' is not MCP's date form`,
147
+ ).toBe(false);
148
+ }
149
+ expect(validate({ supported: true, protocolVersions: ['2026-07-28'] })).toBe(true);
150
+ });
151
+
152
+ it('the feature list is closed', () => {
153
+ expect(
154
+ validate({ supported: true, protocolVersions: ['2026-07-28'], features: ['server-discover', 'telepathy'] }),
155
+ 'RFC 0153 §A: an unrecognized feature name is indistinguishable from a typo, and a peer ' +
156
+ 'that silently ignores one has negotiated a capability neither side implements.',
157
+ ).toBe(false);
158
+ });
159
+
160
+ it('an empty version list is rejected', () => {
161
+ expect(validate({ supported: true, protocolVersions: [] })).toBe(false);
162
+ });
163
+ });
164
+
165
+ describe.skipIf(RFCS_DIR === null)('RFC 0152 + 0153 — what these do NOT establish', () => {
166
+ it('both RFCs keep their upstream-peer acceptance items unticked and annotated', () => {
167
+ // RFC 0147 §A.5 forbids `Accepted` on shape-only evidence for a behavioral
168
+ // requirement. Schema validity is not interop: nothing here speaks to a
169
+ // peer, and both RFCs require a real upstream implementation in CI. This
170
+ // leg exists so the suite cannot read as more than it is.
171
+ for (const [file, needle] of [
172
+ ['0152-a2a-1-0-versioned-composition.md', 'Real upstream A2A 1.0 peer passes in CI.'],
173
+ ['0153-mcp-2026-07-28-versioned-composition.md', 'Pinned real MCP current peer passes in CI.'],
174
+ ] as const) {
175
+ const rfc = readFileSync(join(RFCS_DIR as string, file), 'utf8');
176
+ expect(
177
+ new RegExp(`- \\[ \\] ${needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`).test(rfc),
178
+ `${file}: the upstream-peer item MUST remain unticked and annotated — no peer is contacted ` +
179
+ 'anywhere in this corpus, and interop is the one thing a schema cannot demonstrate.',
180
+ ).toBe(true);
181
+ }
182
+ });
183
+ });
@@ -0,0 +1,188 @@
1
+ /**
2
+ * RFC 0154 §A/§B — the workload-identity BEHAVIORAL witness.
3
+ *
4
+ * §A's requirements are behavioral and, from the wire alone, **invisible**: a
5
+ * host must cryptographically verify the presented identity, bind it to the
6
+ * request, resolve it to an OpenWOP principal *before* authorization, and fail
7
+ * closed when it cannot. A normal request either succeeds or 401s, and **both
8
+ * outcomes look identical whether the host verified anything or simply trusted
9
+ * a header.**
10
+ *
11
+ * That invisibility is why `host-sample-test-seams.md` §20 exists, and why this
12
+ * file reports a missing seam as `blocked` rather than skipping quietly. RFC
13
+ * 0148 §A resolves an unobservable requirement to `blocked`, never to a pass —
14
+ * so without the seam, RFC 0154 cannot be certified at all. That is the honest
15
+ * position, and stating it is the point.
16
+ *
17
+ * **The negative cases carry the weight here.** A host that echoes its input can
18
+ * satisfy every positive assertion; only the rejections distinguish a verifier
19
+ * from a passthrough. So each negative names the specific confusion it rules
20
+ * out:
21
+ *
22
+ * - **audience mismatch** — an identity minted for another host, accepted
23
+ * here, is how a credential valid elsewhere becomes valid here;
24
+ * - **expired delegation** — a delegation without a live expiry is a standing
25
+ * grant, which is not what delegation means;
26
+ * - **missing sender constraint** — without proof-of-possession a bearer
27
+ * credential is replayable by anyone who observed it, so the host would be
28
+ * asserting *who called* rather than *that the caller held the key*.
29
+ *
30
+ * And the framing that no assertion can enforce, stated because it governs how
31
+ * a `200` here should be read: **identity is not authorization** (RFC 0147 R12).
32
+ * A resolution means the identity resolved. It never means the caller may act.
33
+ */
34
+
35
+ import { describe, it, expect } from 'vitest';
36
+ import { driver } from '../lib/driver.js';
37
+ import { behaviorGate } from '../lib/behavior-gate.js';
38
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
39
+
40
+ const PROFILE = 'openwop-workload-identity';
41
+ /**
42
+ * §B gates on its OWN flag, not on §A's.
43
+ *
44
+ * The first draft of this file gated every leg on `workloadIdentity.supported`,
45
+ * which meant a host that verifies workload identity but does NOT implement
46
+ * delegation had two options: advertise and hard-fail the §B legs under strict
47
+ * mode, or advertise nothing and get no witness for the §A behavior it does
48
+ * have. That is the overclaim-or-silence bind RFC 0155 §A names for
49
+ * `openwop-core`, reproduced one capability down.
50
+ *
51
+ * A tier-1 host caught it by asking whether the scenarios gate per-section or
52
+ * all-on-`supported`. They gate per-section now. §A-only is an honest,
53
+ * witnessable posture.
54
+ */
55
+ const DELEGATION_PROFILE = 'openwop-workload-identity-delegation';
56
+ const SEAM = '/v1/host/sample/test/workload-identity/resolve';
57
+
58
+ interface AuthCaps {
59
+ readonly workloadIdentity?: {
60
+ readonly supported?: boolean;
61
+ readonly schemes?: readonly string[];
62
+ readonly senderConstraint?: readonly string[];
63
+ readonly delegation?: { readonly supported?: boolean; readonly maxChainDepth?: number };
64
+ };
65
+ }
66
+
67
+ async function caps(): Promise<AuthCaps['workloadIdentity']> {
68
+ const disco = await driver.get('/.well-known/openwop');
69
+ return capabilityFamily<AuthCaps>(disco.json, 'auth')?.workloadIdentity;
70
+ }
71
+
72
+ /** A seam response, or `null` when the seam is absent. */
73
+ async function resolve(body: Record<string, unknown>): Promise<{ status: number; json: unknown } | null> {
74
+ const r = await driver.post(SEAM, body);
75
+ return r.status === 404 || r.status === 403 ? null : { status: r.status, json: r.json };
76
+ }
77
+
78
+ function reasonOf(json: unknown): string | undefined {
79
+ return (json as { error?: { code?: string } })?.error?.code;
80
+ }
81
+
82
+ const IDENTITY = { scheme: 'spiffe', subject: 'spiffe://example/dispatcher', issuer: 'spiffe://example' };
83
+
84
+ describe('RFC 0154 §A — workload identity resolution (capability-gated behavior)', () => {
85
+ it('the seam is wired, or the requirement is blocked rather than skipped', async () => {
86
+ if (!behaviorGate(PROFILE, (await caps())?.supported === true)) return;
87
+ const r = await resolve({ identity: { ...IDENTITY, audience: 'openwop-host' } });
88
+ expect(
89
+ r,
90
+ driver.describe(
91
+ 'spec/v1/host-sample-test-seams.md §20',
92
+ 'a host advertising `auth.workloadIdentity` MUST expose the resolution seam. §A\'s ' +
93
+ 'requirements — verify, bind, resolve-before-authorize, fail closed — are invisible from ' +
94
+ 'a normal request, where success and 401 look identical whether the host verified ' +
95
+ 'anything or trusted a header. RFC 0148 §A resolves an unobservable requirement to ' +
96
+ '`blocked`, so without this seam RFC 0154 cannot be certified at all.',
97
+ ),
98
+ ).not.toBeNull();
99
+ });
100
+
101
+ it('a verified identity resolves to a principal', async () => {
102
+ if (!behaviorGate(PROFILE, (await caps())?.supported === true)) return;
103
+ const r = await resolve({ identity: { ...IDENTITY, audience: 'openwop-host' } });
104
+ if (r === null) return;
105
+ expect(r.status, driver.describe('RFCS/0154 §A', 'a verified identity resolves')).toBe(200);
106
+ const principalId = (r.json as { principalId?: string }).principalId;
107
+ expect(principalId, driver.describe('RFCS/0154 §A', 'resolution yields an OpenWOP principal')).toBeTruthy();
108
+ });
109
+
110
+ it('an identity for another audience is rejected', async () => {
111
+ if (!behaviorGate(PROFILE, (await caps())?.supported === true)) return;
112
+ const r = await resolve({ identity: { ...IDENTITY, audience: 'some-other-host' }, expectedAudience: 'openwop-host' });
113
+ if (r === null) return;
114
+ // The load-bearing negative. A host that echoes its input passes every
115
+ // positive assertion; only this distinguishes a verifier from a passthrough.
116
+ expect(
117
+ r.status >= 400,
118
+ driver.describe(
119
+ 'RFCS/0154 §A + RFC 0147 R12',
120
+ 'an identity minted for another host, accepted here, is how a credential valid elsewhere ' +
121
+ 'becomes a credential valid here — the confused-deputy path this profile prevents',
122
+ ),
123
+ ).toBe(true);
124
+ expect(reasonOf(r.json)).toBe('audience_mismatch');
125
+ });
126
+
127
+ it('an expired delegation is rejected', async () => {
128
+ // §B, gated on §B's own flag. A host doing §A-only identity resolution is
129
+ // not failing this requirement — it has not claimed it.
130
+ if (!behaviorGate(DELEGATION_PROFILE, (await caps())?.delegation?.supported === true)) return;
131
+ const r = await resolve({
132
+ identity: {
133
+ ...IDENTITY,
134
+ audience: 'openwop-host',
135
+ delegation: {
136
+ chain: [{ subject: 'spiffe://example/dispatcher' }],
137
+ audience: 'openwop-host',
138
+ expiresAt: '2020-01-01T00:00:00Z',
139
+ },
140
+ },
141
+ });
142
+ if (r === null) return;
143
+ expect(
144
+ r.status >= 400,
145
+ driver.describe(
146
+ 'RFCS/0154 §B',
147
+ 'a delegation without a live expiry is a standing grant, which is not what delegation means',
148
+ ),
149
+ ).toBe(true);
150
+ expect(reasonOf(r.json)).toBe('delegation_expired');
151
+ });
152
+
153
+ it('a failure is non-retriable and carries a closed reason code', async () => {
154
+ if (!behaviorGate(PROFILE, (await caps())?.supported === true)) return;
155
+ const r = await resolve({ identity: { scheme: 'spiffe', subject: 'spiffe://example/unknown' } });
156
+ if (r === null || r.status < 400) return;
157
+ const err = (r.json as { error?: { code?: string; retriable?: boolean } }).error;
158
+ expect(
159
+ err?.retriable,
160
+ driver.describe(
161
+ 'spec/v1/host-sample-test-seams.md §20',
162
+ 'an identity that does not resolve will not resolve on retry. Marking it retriable invites ' +
163
+ 'a caller to hammer a failing authorization path.',
164
+ ),
165
+ ).toBe(false);
166
+ expect(
167
+ ['identity_unverified', 'identity_unresolvable', 'audience_mismatch', 'delegation_expired', 'sender_constraint_missing'],
168
+ driver.describe('RFCS/0154 §A', 'failures use a closed reason vocabulary'),
169
+ ).toContain(err?.code);
170
+ });
171
+
172
+ it('a resolution response carries no credential material', async () => {
173
+ if (!behaviorGate(PROFILE, (await caps())?.supported === true)) return;
174
+ const r = await resolve({ identity: { ...IDENTITY, audience: 'openwop-host' } });
175
+ if (r === null) return;
176
+ const serialized = JSON.stringify(r.json ?? {});
177
+ for (const forbidden of ['-----BEGIN', 'Bearer ', 'eyJ']) {
178
+ expect(
179
+ serialized.includes(forbidden),
180
+ driver.describe(
181
+ 'RFCS/0154 §A',
182
+ 'a verified identity is a fact about a completed verification, not a container for the ' +
183
+ `material that proved it. Found: ${forbidden}`,
184
+ ),
185
+ ).toBe(false);
186
+ }
187
+ });
188
+ });