@openwop/openwop-conformance 1.6.0 → 1.10.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 (169) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +74 -1
  4. package/api/openapi.yaml +316 -0
  5. package/coverage.md +16 -0
  6. package/fixtures/conformance-run-duration-breach.json +33 -0
  7. package/fixtures.md +19 -0
  8. package/package.json +1 -1
  9. package/schemas/README.md +12 -0
  10. package/schemas/agent-inventory-response.schema.json +90 -0
  11. package/schemas/ai-envelope.schema.json +28 -0
  12. package/schemas/annotation-create.schema.json +37 -0
  13. package/schemas/annotation.schema.json +56 -0
  14. package/schemas/artifact-type-pack-manifest.schema.json +160 -0
  15. package/schemas/capabilities.schema.json +195 -4
  16. package/schemas/chat-card-pack-manifest.schema.json +158 -0
  17. package/schemas/envelopes/media.audio.schema.json +38 -0
  18. package/schemas/envelopes/media.file.schema.json +37 -0
  19. package/schemas/envelopes/media.image.schema.json +33 -0
  20. package/schemas/heartbeat-evaluated.schema.json +14 -0
  21. package/schemas/heartbeat-state-changed.schema.json +14 -0
  22. package/schemas/node-pack-manifest.schema.json +16 -1
  23. package/schemas/run-event-payloads.schema.json +96 -5
  24. package/schemas/run-event.schema.json +4 -0
  25. package/schemas/workflow-definition.schema.json +5 -0
  26. package/schemas/workspace-file-create.schema.json +20 -0
  27. package/schemas/workspace-file.schema.json +39 -0
  28. package/src/lib/agentLoop.ts +44 -0
  29. package/src/lib/agentRuntime.ts +45 -0
  30. package/src/lib/artifactTypes.ts +96 -0
  31. package/src/lib/cardPacks.ts +52 -0
  32. package/src/lib/discovery-capabilities.ts +50 -0
  33. package/src/lib/distillation.ts +38 -0
  34. package/src/lib/feedback.ts +31 -0
  35. package/src/lib/heartbeat.ts +31 -0
  36. package/src/lib/memoryAttribution.ts +48 -0
  37. package/src/lib/subRunAttestation.ts +35 -0
  38. package/src/lib/toolHooks.ts +33 -0
  39. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
  40. package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
  41. package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
  42. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
  43. package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
  44. package/src/scenarios/ai-envelope-shape.test.ts +14 -18
  45. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
  46. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
  47. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
  48. package/src/scenarios/approval-gate-flow.test.ts +4 -6
  49. package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
  50. package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
  51. package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
  52. package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
  53. package/src/scenarios/audit-log-integrity.test.ts +3 -2
  54. package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
  55. package/src/scenarios/auth-mtls.test.ts +2 -1
  56. package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
  57. package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
  58. package/src/scenarios/auth-saml-profile.test.ts +2 -1
  59. package/src/scenarios/auth-scim-profile.test.ts +2 -1
  60. package/src/scenarios/authorization-fail-closed.test.ts +2 -1
  61. package/src/scenarios/authorization-roles-shape.test.ts +2 -1
  62. package/src/scenarios/byok-auth-modes.test.ts +141 -0
  63. package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
  64. package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
  65. package/src/scenarios/commitment-fired.test.ts +83 -0
  66. package/src/scenarios/credential-payload-redaction.test.ts +2 -1
  67. package/src/scenarios/credentials-capability-shape.test.ts +2 -1
  68. package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
  69. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
  70. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  71. package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
  72. package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
  73. package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
  74. package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
  75. package/src/scenarios/distillation-shape.test.ts +41 -0
  76. package/src/scenarios/distillation-stable-archive.test.ts +37 -0
  77. package/src/scenarios/distillation-token-budget.test.ts +45 -0
  78. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
  79. package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
  80. package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
  81. package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
  82. package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
  83. package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
  84. package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
  85. package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
  86. package/src/scenarios/experimental-tier-shape.test.ts +5 -4
  87. package/src/scenarios/feedback-capability-shape.test.ts +35 -0
  88. package/src/scenarios/feedback-correction-redaction.test.ts +35 -0
  89. package/src/scenarios/feedback-cross-tenant-isolation.test.ts +37 -0
  90. package/src/scenarios/feedback-fork-not-copied.test.ts +40 -0
  91. package/src/scenarios/feedback-on-terminal-run.test.ts +32 -0
  92. package/src/scenarios/feedback-record-and-list.test.ts +32 -0
  93. package/src/scenarios/feedback-unsupported-501.test.ts +32 -0
  94. package/src/scenarios/fs-path-traversal.test.ts +2 -1
  95. package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
  96. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
  97. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
  98. package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
  99. package/src/scenarios/http-client-ssrf.test.ts +10 -13
  100. package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
  101. package/src/scenarios/media-url-inline-cap.test.ts +167 -0
  102. package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
  103. package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
  104. package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
  105. package/src/scenarios/memory-attribution-shape.test.ts +28 -0
  106. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
  107. package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
  108. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
  109. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
  110. package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
  111. package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
  112. package/src/scenarios/model-capability-substituted.test.ts +2 -1
  113. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  114. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
  115. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
  116. package/src/scenarios/multi-region-idempotency.test.ts +10 -10
  117. package/src/scenarios/oauth-capability-shape.test.ts +2 -1
  118. package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
  119. package/src/scenarios/pause-resume.test.ts +3 -3
  120. package/src/scenarios/production-backpressure.test.ts +2 -2
  121. package/src/scenarios/production-retention-expiry.test.ts +2 -2
  122. package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
  123. package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
  124. package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
  125. package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
  126. package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
  127. package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
  128. package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
  129. package/src/scenarios/prompt-pack-install.test.ts +2 -1
  130. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
  131. package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
  132. package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
  133. package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
  134. package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
  135. package/src/scenarios/prompt-template-shape.test.ts +2 -1
  136. package/src/scenarios/provider-usage.test.ts +2 -1
  137. package/src/scenarios/redaction.test.ts +4 -1
  138. package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
  139. package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
  140. package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
  141. package/src/scenarios/replayDeterminism.test.ts +3 -1
  142. package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
  143. package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
  144. package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
  145. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  146. package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
  147. package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
  148. package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
  149. package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
  150. package/src/scenarios/spec-corpus-validity.test.ts +4 -1
  151. package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
  152. package/src/scenarios/subrun-approval-gate.test.ts +35 -0
  153. package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
  154. package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
  155. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
  156. package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
  157. package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
  158. package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
  159. package/src/scenarios/tool-hooks-shape.test.ts +34 -0
  160. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
  161. package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
  162. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
  163. package/src/scenarios/wasm-pack-load.test.ts +2 -2
  164. package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
  165. package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
  166. package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
  167. package/src/scenarios/workspace-behavior.test.ts +134 -0
  168. package/src/scenarios/workspace-capability-shape.test.ts +73 -0
  169. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Bounded artifact-schema compilation (RFC 0071, `Active`).
3
+ *
4
+ * Always-on, server-free assertion for the SECURITY invariant
5
+ * `artifact-schema-compile-bounded`. Artifact-type packs ship third-party
6
+ * JSON Schemas that the engine compiles (Ajv) at install + validation time;
7
+ * an unbounded compile is a denial-of-service vector (schema bombs:
8
+ * pathological `$ref` recursion, keyword-count explosion, oversized payloads,
9
+ * catastrophic-backtracking `pattern`s). This scenario asserts two things
10
+ * that must hold for every release regardless of which host runs it:
11
+ *
12
+ * PART 1 — contract present. `artifact-type-packs.md` carries the normative
13
+ * bounded-compilation MUST (serialized-size, `$ref`-depth, keyword-count
14
+ * bounds + wall-clock timeout), and `host-capabilities.md` §host.artifactTypes
15
+ * references it. Guards against the requirement being silently dropped.
16
+ *
17
+ * PART 2 — defense is well-defined + implementable. A reference bounding
18
+ * predicate built from representative finite limits rejects three schema
19
+ * bombs and admits a benign artifact schema. The specific numeric limits are
20
+ * host-configurable per the spec (advertised, not protocol-mandated); the
21
+ * point is that *some* finite bound exists and catches the bombs while
22
+ * passing legitimate schemas.
23
+ *
24
+ * The behavioral end-to-end form (a host rejects an over-bounds pack at
25
+ * registry `PUT` with `pack_validation_failed`) is capability-gated on
26
+ * `host.artifactTypes.supported` and is `host-pending` until a reference host
27
+ * lands; this server-free scenario is the always-on floor.
28
+ *
29
+ * @see spec/v1/artifact-type-packs.md §"Bounded schema compilation (normative)"
30
+ * @see SECURITY/threat-model-node-packs.md §"Distributed artifact schemas"
31
+ * @see RFCS/0071-artifact-type-and-chat-card-packs.md
32
+ */
33
+
34
+ import { describe, it, expect } from 'vitest';
35
+ import { readFileSync } from 'node:fs';
36
+ import { join } from 'node:path';
37
+ import { V1_DIR } from '../lib/paths.js';
38
+
39
+ const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
40
+
41
+ describe('artifact-schema-compile-bounded: contract present in the corpus (RFC 0071, server-free)', () => {
42
+ const artifactDoc = V1_DIR ? readFileSync(join(V1_DIR, 'artifact-type-packs.md'), 'utf8') : '';
43
+ const hostCaps = V1_DIR ? readFileSync(join(V1_DIR, 'host-capabilities.md'), 'utf8') : '';
44
+
45
+ it.skipIf(V1_DIR === null)('artifact-type-packs.md declares the bounded-compilation MUST', () => {
46
+ expect(
47
+ /Bounded schema compilation/i.test(artifactDoc),
48
+ why('artifact-type-packs.md', 'a "Bounded schema compilation" section MUST exist'),
49
+ ).toBe(true);
50
+ expect(
51
+ /MUST bound/i.test(artifactDoc) && /MUST reject/i.test(artifactDoc),
52
+ why('artifact-type-packs.md §"Bounded schema compilation"', 'host MUST bound + MUST reject over-limit schemas'),
53
+ ).toBe(true);
54
+ // The three structural axes + the timeout MUST all be named.
55
+ for (const axis of [/byte size/i, /\$ref/i, /keyword/i, /timeout/i]) {
56
+ expect(axis.test(artifactDoc), why('artifact-type-packs.md', `bound axis ${axis} MUST be named`)).toBe(true);
57
+ }
58
+ });
59
+
60
+ it.skipIf(V1_DIR === null)('host-capabilities.md §host.artifactTypes references the bound', () => {
61
+ expect(
62
+ /artifact-schema-compile-bounded/.test(hostCaps),
63
+ why('host-capabilities.md §host.artifactTypes', 'MUST reference the bounded-compilation invariant'),
64
+ ).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe('artifact-schema-compile-bounded: a finite bound catches schema bombs (RFC 0071, server-free)', () => {
69
+ // Representative, host-configurable limits (the spec leaves the exact values
70
+ // to host advertisement; these stand in for "some finite bound").
71
+ const LIMITS = { maxBytes: 64 * 1024, maxRefDepth: 16, maxKeywords: 2000 };
72
+
73
+ function refDepth(node: unknown, seen = 0): number {
74
+ if (node === null || typeof node !== 'object') return seen;
75
+ const obj = node as Record<string, unknown>;
76
+ const here = '$ref' in obj ? seen + 1 : seen;
77
+ let max = here;
78
+ for (const v of Object.values(obj)) max = Math.max(max, refDepth(v, here));
79
+ return max;
80
+ }
81
+ function keywordCount(node: unknown): number {
82
+ if (node === null || typeof node !== 'object') return 0;
83
+ const obj = node as Record<string, unknown>;
84
+ let n = Object.keys(obj).length;
85
+ for (const v of Object.values(obj)) n += keywordCount(v);
86
+ return n;
87
+ }
88
+ /** Reference bound predicate — the shape a conformant host applies at PUT/install. */
89
+ function exceedsBounds(schema: unknown): boolean {
90
+ const bytes = Buffer.byteLength(JSON.stringify(schema), 'utf8');
91
+ if (bytes > LIMITS.maxBytes) return true;
92
+ if (refDepth(schema) > LIMITS.maxRefDepth) return true;
93
+ if (keywordCount(schema) > LIMITS.maxKeywords) return true;
94
+ return false;
95
+ }
96
+
97
+ it('admits a benign artifact schema', () => {
98
+ const benign = {
99
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
100
+ $id: 'https://h.example/schemas/artifacts/vendor.acme.cad.model.schema.json',
101
+ type: 'object',
102
+ additionalProperties: false,
103
+ required: ['name'],
104
+ properties: { name: { type: 'string' }, dims: { type: 'array', items: { type: 'number' } } },
105
+ };
106
+ expect(exceedsBounds(benign), why('artifact-type-packs.md', 'a legitimate artifact schema MUST NOT be rejected')).toBe(false);
107
+ });
108
+
109
+ it('rejects a $ref-depth bomb', () => {
110
+ // Nest $ref-bearing objects deeper than maxRefDepth so resolution depth accumulates.
111
+ let node: Record<string, unknown> = { type: 'string' };
112
+ for (let i = 0; i < LIMITS.maxRefDepth + 4; i++) node = { $ref: '#/x', properties: { nested: node } };
113
+ expect(exceedsBounds({ type: 'object', properties: { deep: node } }), why('threat-model-node-packs.md', 'a $ref-depth bomb MUST be rejected')).toBe(true);
114
+ });
115
+
116
+ it('rejects a keyword-count bomb', () => {
117
+ const props: Record<string, unknown> = {};
118
+ for (let i = 0; i < LIMITS.maxKeywords + 100; i++) props[`p${i}`] = { type: 'string' };
119
+ expect(exceedsBounds({ type: 'object', properties: props }), why('threat-model-node-packs.md', 'a keyword-count bomb MUST be rejected')).toBe(true);
120
+ });
121
+
122
+ it('rejects an oversized schema', () => {
123
+ const huge = { type: 'object', description: 'x'.repeat(LIMITS.maxBytes + 1) };
124
+ expect(exceedsBounds(huge), why('threat-model-node-packs.md', 'an over-size schema MUST be rejected')).toBe(true);
125
+ });
126
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * artifact-type-pack-install — RFC 0071 Phase 1 §"Binding the existing artifact
3
+ * surfaces". A host that advertises `host.artifactTypes` installs an
4
+ * artifact-type pack, then produces an artifact of a registered type:
5
+ *
6
+ * - a payload that conforms to the pack schema is stored and surfaces an
7
+ * `artifact.created` with `registered: true` (validated against the pack);
8
+ * - a payload that violates the schema is rejected (not stored, no
9
+ * `registered: true` artifact.created).
10
+ *
11
+ * Gated on `capabilities.host.artifactTypes.supported` + the host-sample
12
+ * install/produce seam; soft-skips when either is absent (`host-pending`
13
+ * until a reference host wires RFC 0071 — see the migration request at
14
+ * docs/openwop-adoption/0071-artifact-type-packs-migration-request.md).
15
+ *
16
+ * @see spec/v1/artifact-type-packs.md §"Binding the existing artifact surfaces"
17
+ * @see RFCS/0071-artifact-type-and-chat-card-packs.md
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import { driver } from '../lib/driver.js';
22
+ import {
23
+ readArtifactTypesCap,
24
+ artifactTypesSupported,
25
+ installArtifactTypePack,
26
+ produceArtifact,
27
+ sampleArtifactTypePack,
28
+ } from '../lib/artifactTypes.js';
29
+
30
+ describe('artifact-type-pack-install: registered artifacts are schema-validated (RFC 0071)', () => {
31
+ it('a conforming payload yields artifact.created { registered: true }', async () => {
32
+ if (!artifactTypesSupported(await readArtifactTypesCap())) return; // unadvertised — soft-skip
33
+ const { artifactTypeId, manifest, schema } = sampleArtifactTypePack();
34
+
35
+ const installed = await installArtifactTypePack(manifest, { [artifactTypeId]: schema });
36
+ if (installed === null) return; // seam absent — soft-skip
37
+ expect(
38
+ installed.status >= 200 && installed.status < 300,
39
+ driver.describe('artifact-type-packs.md §"Pack kind"', 'a valid artifact-type pack MUST install cleanly'),
40
+ ).toBe(true);
41
+
42
+ const produced = await produceArtifact(artifactTypeId, { title: 'Hello', body: 'World' });
43
+ if (produced === null) return; // seam absent — soft-skip
44
+ expect(
45
+ produced.json['registered'],
46
+ driver.describe('artifact-type-packs.md §"Binding the existing artifact surfaces"', 'a payload matching a registered artifactTypeId MUST be marked registered'),
47
+ ).toBe(true);
48
+ expect(
49
+ produced.json['validated'],
50
+ driver.describe('artifact-type-packs.md', 'the host MUST validate the payload against the pack schema before emitting artifact.created'),
51
+ ).toBe(true);
52
+ const evt = produced.json['artifactCreated'] as { registered?: unknown } | undefined;
53
+ if (evt && 'registered' in evt) {
54
+ expect(
55
+ evt.registered,
56
+ driver.describe('run-event-payloads.schema.json §artifactCreated', 'artifact.created.registered MUST be true for a validated registered artifact'),
57
+ ).toBe(true);
58
+ }
59
+ });
60
+
61
+ it('a schema-violating payload is rejected (not stored as a validated registered artifact)', async () => {
62
+ if (!artifactTypesSupported(await readArtifactTypesCap())) return;
63
+ const { artifactTypeId, manifest, schema } = sampleArtifactTypePack();
64
+ if ((await installArtifactTypePack(manifest, { [artifactTypeId]: schema })) === null) return;
65
+
66
+ // `body` missing + a foreign key → fails additionalProperties:false + required.
67
+ const produced = await produceArtifact(artifactTypeId, { title: 'Hello', extra: true });
68
+ if (produced === null) return;
69
+ const rejected =
70
+ produced.status >= 400 ||
71
+ produced.json['validated'] === false ||
72
+ produced.json['stored'] === false;
73
+ expect(
74
+ rejected,
75
+ driver.describe('artifact-type-packs.md §"Binding the existing artifact surfaces"', 'a payload that fails the pack schema MUST NOT be emitted as a validated registered artifact'),
76
+ ).toBe(true);
77
+ });
78
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Artifact-type pack manifest validation — `artifact-type-packs.md` §"Manifest format"
3
+ * + `schemas/artifact-type-pack-manifest.schema.json` (RFC 0071 Phase 1).
4
+ *
5
+ * Server-free schema-validation scenario. Exercises the new
6
+ * `artifact-type-pack-manifest.schema.json` with a positive sample and the
7
+ * negative samples derived from the RFC's "Examples" section that are
8
+ * expressible at the JSON-Schema layer:
9
+ *
10
+ * 1. Positive: a valid `kind: "artifact-type"` manifest with a single
11
+ * `artifactTypes[]` entry validates cleanly.
12
+ * 2. Negative — kind/contents mismatch: a manifest carrying BOTH
13
+ * `artifactTypes[]` AND `nodes[]` is rejected. Surface-level outcome at
14
+ * the registry HTTP API is `pack_kind_invalid` per the spec;
15
+ * schema-level outcome is an `additionalProperties` violation on
16
+ * `nodes` (this schema does not declare that field).
17
+ * 3. Negative — empty `artifactTypes[]`: rejected with a `minItems`
18
+ * violation (a pack MUST declare at least one artifact type).
19
+ * 4. Negative — invalid `artifactTypeId`: a value that does not match the
20
+ * reverse-DNS pattern (e.g. an uppercase scope) is rejected with a
21
+ * `pattern` violation.
22
+ * 5. Negative — unknown `rendering.display`: a value outside the closed
23
+ * enum (`"3d-viewport"`) is rejected with an `enum` violation
24
+ * (the RenderingHint vocabulary is reused from RFC 0055, `card` excluded).
25
+ * 6. Negative — non-conforming `exportFormats` identifier: an uppercase /
26
+ * unprefixed value is rejected with a `pattern` violation (reserved-core
27
+ * + `vendor.*`/`x-` extension idiom).
28
+ *
29
+ * NOTE: the RFC's "core scope published from a non-core account" negative is a
30
+ * registry-PUT enforcement rule (account ↔ scope binding), NOT a schema
31
+ * constraint — `core.*` is a valid `artifactTypeId` pattern. It is therefore
32
+ * not asserted here; it belongs to the capability-gated publish scenario.
33
+ *
34
+ * Capability-gated end-to-end scenarios (install + validate; store-without-
35
+ * render negotiation) are deferred and gate on `host.artifactTypes.supported`
36
+ * per the RFC; behavior grade is `host-pending` until a reference host lands.
37
+ *
38
+ * @see spec/v1/artifact-type-packs.md
39
+ * @see schemas/artifact-type-pack-manifest.schema.json
40
+ * @see RFCS/0071-artifact-type-and-chat-card-packs.md
41
+ */
42
+
43
+ import { describe, it, expect } from 'vitest';
44
+ import { readFileSync } from 'node:fs';
45
+ import { join } from 'node:path';
46
+ import Ajv2020 from 'ajv/dist/2020.js';
47
+ import addFormats from 'ajv-formats';
48
+ import type { ErrorObject } from 'ajv';
49
+ import { SCHEMAS_DIR } from '../lib/paths.js';
50
+
51
+ const SCHEMA_PATH = join(SCHEMAS_DIR, 'artifact-type-pack-manifest.schema.json');
52
+
53
+ function validManifest() {
54
+ return {
55
+ kind: 'artifact-type',
56
+ name: 'vendor.acme.cad',
57
+ version: '1.0.0',
58
+ engines: { openwop: '>=1.1 <2.0.0' },
59
+ artifactTypes: [
60
+ {
61
+ artifactTypeId: 'vendor.acme.cad.model',
62
+ schemaVersion: 1,
63
+ schemaRef: 'schemas/cad-model.schema.json',
64
+ rendering: { display: 'file', mimeType: 'model/step' },
65
+ exportFormats: ['step', 'stl', 'pdf'],
66
+ syncOn: 'completion',
67
+ supportsCheckpoint: true,
68
+ },
69
+ ],
70
+ };
71
+ }
72
+
73
+ describe('category: artifact-type-pack manifest validation', () => {
74
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
75
+ addFormats(ajv);
76
+ const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
77
+ const validate = ajv.compile(schema);
78
+
79
+ const failsWith = (manifest: unknown, keyword: string): ErrorObject[] => {
80
+ const ok = validate(manifest);
81
+ expect(ok).toBe(false);
82
+ const errs = (validate.errors ?? []).filter((e) => e.keyword === keyword);
83
+ return errs;
84
+ };
85
+
86
+ it('positive: a valid artifact-type pack manifest validates cleanly', () => {
87
+ const ok = validate(validManifest());
88
+ expect(
89
+ ok,
90
+ `artifact-type-packs.md §"Manifest format": a well-formed kind:"artifact-type" manifest MUST validate. Errors: ${JSON.stringify(validate.errors)}`,
91
+ ).toBe(true);
92
+ });
93
+
94
+ it('negative: a manifest mixing artifactTypes[] and nodes[] is rejected (pack_kind_invalid at the registry)', () => {
95
+ const manifest = { ...validManifest(), nodes: [{ typeId: 'vendor.acme.x', version: '1.0.0', category: 'data', role: 'pure' }] };
96
+ const errs = failsWith(manifest, 'additionalProperties');
97
+ expect(
98
+ errs.some((e) => (e.params as { additionalProperty?: string }).additionalProperty === 'nodes'),
99
+ 'artifact-type-packs.md §"Pack kind": one kind per pack — a foreign `nodes[]` field MUST be rejected (additionalProperties:false)',
100
+ ).toBe(true);
101
+ });
102
+
103
+ it('negative: an empty artifactTypes[] is rejected (a pack MUST declare ≥1 type)', () => {
104
+ const manifest = { ...validManifest(), artifactTypes: [] };
105
+ const errs = failsWith(manifest, 'minItems');
106
+ expect(errs.length, 'artifact-type-pack-manifest.schema.json: artifactTypes minItems:1').toBeGreaterThan(0);
107
+ });
108
+
109
+ it('negative: an artifactTypeId that is not reverse-DNS scoped is rejected', () => {
110
+ const manifest = validManifest();
111
+ manifest.artifactTypes[0]!.artifactTypeId = 'Vendor.Acme.Model'; // uppercase scope
112
+ const errs = failsWith(manifest, 'pattern');
113
+ expect(errs.length, 'artifact-type-packs.md: artifactTypeId MUST match the reverse-DNS pattern').toBeGreaterThan(0);
114
+ });
115
+
116
+ it('negative: an unknown rendering.display value is rejected (closed RFC 0055 enum, card excluded)', () => {
117
+ const manifest = validManifest();
118
+ (manifest.artifactTypes[0]!.rendering as { display: string }).display = '3d-viewport';
119
+ const errs = failsWith(manifest, 'enum');
120
+ expect(errs.length, 'artifact-type-packs.md: rendering.display reuses the closed ai-envelope §"Rendering hints" enum').toBeGreaterThan(0);
121
+ });
122
+
123
+ it('negative: a non-conforming exportFormats identifier is rejected (reserved-core + vendor.*/x- only)', () => {
124
+ const manifest = validManifest();
125
+ manifest.artifactTypes[0]!.exportFormats = ['PPTX']; // uppercase, unprefixed
126
+ const errs = failsWith(manifest, 'pattern');
127
+ expect(errs.length, 'artifact-type-packs.md: exportFormats identifiers are lowercase core ids OR vendor.*/x- extensions').toBeGreaterThan(0);
128
+ });
129
+
130
+ it('positive: the validation field accepts "open"/"closed" and rejects other values (RFC 0075)', () => {
131
+ for (const v of ['open', 'closed']) {
132
+ const m = validManifest();
133
+ (m.artifactTypes[0] as Record<string, unknown>).validation = v;
134
+ expect(validate(m), `artifact-type-packs.md §validation: "${v}" MUST validate (RFC 0075)`).toBe(true);
135
+ }
136
+ const bad = validManifest();
137
+ (bad.artifactTypes[0] as Record<string, unknown>).validation = 'lenient';
138
+ expect(failsWith(bad, 'enum').length, 'validation MUST be open|closed').toBeGreaterThan(0);
139
+ });
140
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * artifact-type-store-without-render — RFC 0071 Phase 1 §host.artifactTypes.
3
+ * The cross-host negotiation guarantee: a host that can STORE an artifact type
4
+ * but cannot RENDER it MUST still accept + store the artifact and MUST NOT fail
5
+ * the run for lack of a renderer. An artifact produced on a richly-rendering
6
+ * host stays storable + forwardable + inspectable on a store-only host.
7
+ *
8
+ * Gated on `host.artifactTypes.supported` AND the advertised facets
9
+ * `store: true, render: false` (a host that renders everything can't exercise
10
+ * this path — it soft-skips), plus the host-sample produce seam. `host-pending`
11
+ * until a reference host advertises a store-without-render posture.
12
+ *
13
+ * @see spec/v1/artifact-type-packs.md §host.artifactTypes
14
+ * @see spec/v1/host-capabilities.md §host.artifactTypes
15
+ * @see RFCS/0071-artifact-type-and-chat-card-packs.md
16
+ */
17
+
18
+ import { describe, it, expect } from 'vitest';
19
+ import { driver } from '../lib/driver.js';
20
+ import {
21
+ readArtifactTypesCap,
22
+ artifactTypesSupported,
23
+ installArtifactTypePack,
24
+ produceArtifact,
25
+ sampleArtifactTypePack,
26
+ } from '../lib/artifactTypes.js';
27
+
28
+ describe('artifact-type-store-without-render: store-only hosts must not fail the run (RFC 0071)', () => {
29
+ it('a stored-but-unrendered artifact completes the run', async () => {
30
+ const cap = await readArtifactTypesCap();
31
+ if (!artifactTypesSupported(cap)) return; // unadvertised — soft-skip
32
+ // Only meaningful for a host that stores but does NOT render.
33
+ if (cap?.['store'] !== true || cap?.['render'] !== false) return; // not a store-without-render host — soft-skip
34
+
35
+ const { artifactTypeId, manifest, schema } = sampleArtifactTypePack();
36
+ if ((await installArtifactTypePack(manifest, { [artifactTypeId]: schema })) === null) return;
37
+
38
+ const produced = await produceArtifact(artifactTypeId, { title: 'Stored', body: 'Not rendered here' });
39
+ if (produced === null) return; // seam absent — soft-skip
40
+
41
+ expect(
42
+ produced.json['stored'],
43
+ driver.describe('artifact-type-packs.md §host.artifactTypes', 'a host advertising store:true MUST persist the artifact'),
44
+ ).toBe(true);
45
+ expect(
46
+ produced.json['rendered'],
47
+ driver.describe('artifact-type-packs.md §host.artifactTypes', 'render:false host MUST NOT render'),
48
+ ).toBe(false);
49
+ expect(
50
+ produced.json['runStatus'],
51
+ driver.describe('artifact-type-packs.md §host.artifactTypes', 'a host MUST NOT fail the run solely because it lacks a renderer for a stored artifact type'),
52
+ ).toBe('completed');
53
+ });
54
+ });
@@ -18,6 +18,7 @@
18
18
  import { describe, it, expect } from 'vitest';
19
19
  import { driver } from '../lib/driver.js';
20
20
  import { behaviorGate } from '../lib/behavior-gate.js';
21
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
21
22
 
22
23
  interface AuditIntegrityCaps {
23
24
  hashChain?: boolean;
@@ -34,7 +35,7 @@ interface AuthCaps {
34
35
 
35
36
  async function isProfileAdvertised(): Promise<boolean> {
36
37
  const disco = await driver.get('/.well-known/openwop');
37
- const auth = (disco.json as { capabilities?: { auth?: AuthCaps } }).capabilities?.auth ?? {};
38
+ const auth = capabilityFamily<AuthCaps>(disco.json, 'auth') ?? {};
38
39
  return Array.isArray(auth.profiles) && auth.profiles.includes('openwop-audit-log-integrity');
39
40
  }
40
41
 
@@ -46,7 +47,7 @@ describe('audit-log-integrity: profile shape', () => {
46
47
 
47
48
  const disco = await driver.get('/.well-known/openwop');
48
49
  const integrity =
49
- (disco.json as { capabilities?: { auth?: AuthCaps } }).capabilities?.auth
50
+ capabilityFamily<AuthCaps>(disco.json, 'auth')
50
51
  ?.auditLogIntegrity ?? {};
51
52
 
52
53
  expect(integrity.hashChain, driver.describe(
@@ -28,6 +28,7 @@ import { describe, it, expect } from 'vitest';
28
28
  import { driver } from '../lib/driver.js';
29
29
  import { behaviorGate } from '../lib/behavior-gate.js';
30
30
  import { isFixtureAdvertised } from '../lib/fixtures.js';
31
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
31
32
 
32
33
  interface RotationCaps {
33
34
  supported?: boolean;
@@ -45,7 +46,7 @@ const CANARY = 'hk_openwop_canary_d1d2d3d4_NOT_A_REAL_KEY';
45
46
 
46
47
  async function readAuthCaps(): Promise<AuthCaps | undefined> {
47
48
  const disco = await driver.get('/.well-known/openwop');
48
- return (disco.json as { capabilities?: { auth?: AuthCaps } }).capabilities?.auth;
49
+ return capabilityFamily((disco.json as { capabilities?: { auth?: AuthCaps } }), 'auth');
49
50
  }
50
51
 
51
52
  function isProfileAdvertised(auth: AuthCaps | undefined): boolean {
@@ -45,6 +45,7 @@ import { driver } from '../lib/driver.js';
45
45
  import { loadEnv } from '../lib/env.js';
46
46
  import { behaviorGate } from '../lib/behavior-gate.js';
47
47
  import { isFixtureAdvertised } from '../lib/fixtures.js';
48
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
48
49
 
49
50
  interface MtlsCaps {
50
51
  supported?: boolean;
@@ -74,7 +75,7 @@ interface HttpsResponse {
74
75
 
75
76
  async function readAuthCaps(): Promise<AuthCaps | undefined> {
76
77
  const disco = await driver.get('/.well-known/openwop');
77
- return (disco.json as { capabilities?: { auth?: AuthCaps } }).capabilities?.auth;
78
+ return capabilityFamily((disco.json as { capabilities?: { auth?: AuthCaps } }), 'auth');
78
79
  }
79
80
 
80
81
  function isProfileAdvertised(auth: AuthCaps | undefined): boolean {
@@ -33,6 +33,7 @@ import { driver } from '../lib/driver.js';
33
33
  import { behaviorGate } from '../lib/behavior-gate.js';
34
34
  import { isFixtureAdvertised } from '../lib/fixtures.js';
35
35
  import { createSyntheticOIDCIssuer } from '../lib/oidc-issuer.js';
36
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
36
37
 
37
38
  interface OAuth2Caps {
38
39
  supported?: boolean;
@@ -51,7 +52,7 @@ const FIXTURE = 'conformance-noop';
51
52
 
52
53
  async function readAuthCaps(): Promise<AuthCaps | undefined> {
53
54
  const disco = await driver.get('/.well-known/openwop');
54
- return (disco.json as { capabilities?: { auth?: AuthCaps } }).capabilities?.auth;
55
+ return capabilityFamily((disco.json as { capabilities?: { auth?: AuthCaps } }), 'auth');
55
56
  }
56
57
 
57
58
  function isProfileAdvertised(auth: AuthCaps | undefined): boolean {
@@ -47,6 +47,7 @@ import {
47
47
  createSyntheticOIDCIssuer,
48
48
  type SyntheticOIDCIssuer,
49
49
  } from '../lib/oidc-issuer.js';
50
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
50
51
 
51
52
  interface OIDCCaps {
52
53
  supported?: boolean;
@@ -66,7 +67,7 @@ const FIXTURE = 'conformance-noop';
66
67
 
67
68
  async function readAuthCaps(): Promise<AuthCaps | undefined> {
68
69
  const disco = await driver.get('/.well-known/openwop');
69
- return (disco.json as { capabilities?: { auth?: AuthCaps } }).capabilities?.auth;
70
+ return capabilityFamily((disco.json as { capabilities?: { auth?: AuthCaps } }), 'auth');
70
71
  }
71
72
 
72
73
  function isProfileAdvertised(auth: AuthCaps | undefined): boolean {
@@ -20,6 +20,7 @@
20
20
  import { describe, it, expect } from 'vitest';
21
21
  import { driver } from '../lib/driver.js';
22
22
  import { createSyntheticSamlIdp, type SamlVariant } from '../lib/saml-idp.js';
23
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
23
24
 
24
25
  const SAML_PROFILE = 'openwop-auth-saml';
25
26
 
@@ -35,7 +36,7 @@ interface DiscoveryDoc {
35
36
  async function readProfiles(): Promise<string[] | null> {
36
37
  const res = await driver.get('/.well-known/openwop');
37
38
  const body = res.json as DiscoveryDoc | undefined;
38
- return body?.capabilities?.auth?.profiles ?? body?.extensions?.auth?.profiles ?? null;
39
+ return capabilityFamily<{ profiles?: string[] }>(body, 'auth')?.profiles ?? body?.extensions?.auth?.profiles ?? null;
39
40
  }
40
41
 
41
42
  describe('auth-saml-profile: advertisement shape (RFC 0050)', () => {
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { describe, it, expect } from 'vitest';
19
19
  import { driver } from '../lib/driver.js';
20
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
20
21
 
21
22
  const SCIM_PROFILE = 'openwop-auth-scim';
22
23
 
@@ -32,7 +33,7 @@ interface DiscoveryDoc {
32
33
  async function readProfiles(): Promise<string[] | null> {
33
34
  const res = await driver.get('/.well-known/openwop');
34
35
  const body = res.json as DiscoveryDoc | undefined;
35
- return body?.capabilities?.auth?.profiles ?? body?.extensions?.auth?.profiles ?? null;
36
+ return capabilityFamily<{ profiles?: string[] }>(body, 'auth')?.profiles ?? body?.extensions?.auth?.profiles ?? null;
36
37
  }
37
38
 
38
39
  describe('auth-scim-profile: advertisement shape (RFC 0050)', () => {
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { describe, it, expect } from 'vitest';
26
26
  import { driver } from '../lib/driver.js';
27
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
27
28
 
28
29
  interface DiscoveryAuthorization {
29
30
  supported?: boolean;
@@ -39,7 +40,7 @@ interface DiscoveryDoc {
39
40
  async function readAuthorization(): Promise<DiscoveryAuthorization | null> {
40
41
  const res = await driver.get('/.well-known/openwop');
41
42
  const body = res.json as DiscoveryDoc | undefined;
42
- return body?.capabilities?.authorization ?? null;
43
+ return capabilityFamily(body, 'authorization') ?? null;
43
44
  }
44
45
 
45
46
  describe('authorization-fail-closed: advertisement shape (RFC 0049 §C)', () => {
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { describe, it, expect } from 'vitest';
22
22
  import { driver } from '../lib/driver.js';
23
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
23
24
 
24
25
  interface DiscoveryRole {
25
26
  role?: string;
@@ -41,7 +42,7 @@ interface DiscoveryDoc {
41
42
  async function readAuthorization(): Promise<DiscoveryAuthorization | null> {
42
43
  const res = await driver.get('/.well-known/openwop');
43
44
  const body = res.json as DiscoveryDoc | undefined;
44
- return body?.capabilities?.authorization ?? null;
45
+ return capabilityFamily(body, 'authorization') ?? null;
45
46
  }
46
47
 
47
48
  describe('authorization-roles-shape: advertisement shape (RFC 0049 §A)', () => {