@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
@@ -34,6 +34,7 @@ import { driver } from '../lib/driver.js';
34
34
  import { pollUntilTerminal } from '../lib/polling.js';
35
35
  import { isFixtureAdvertised } from '../lib/fixtures.js';
36
36
  import { behaviorGate } from '../lib/behavior-gate.js';
37
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
37
38
 
38
39
  const WORKFLOW_ID = 'conformance-prompt-all-four-kinds';
39
40
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
@@ -64,7 +65,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
64
65
  }
65
66
 
66
67
  function promptsSupported(d: DiscoveryDoc | null): boolean {
67
- return d?.capabilities?.prompts?.supported === true;
68
+ return capabilityFamily(d, 'prompts')?.supported === true;
68
69
  }
69
70
 
70
71
  async function readAllEvents(runId: string): Promise<RunEventDoc[]> {
@@ -33,6 +33,7 @@
33
33
  import { describe, it, expect } from 'vitest';
34
34
  import { driver } from '../lib/driver.js';
35
35
  import { behaviorGate } from '../lib/behavior-gate.js';
36
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
36
37
 
37
38
  interface DiscoveryDoc {
38
39
  capabilities?: {
@@ -63,7 +64,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
63
64
  }
64
65
 
65
66
  function promptsSupportFull(d: DiscoveryDoc | null): boolean {
66
- const p = d?.capabilities?.prompts;
67
+ const p = capabilityFamily(d, 'prompts');
67
68
  if (!p) return false;
68
69
  return p.supported === true && p.observability === 'full';
69
70
  }
@@ -32,6 +32,7 @@
32
32
  import { describe, it, expect } from 'vitest';
33
33
  import { driver } from '../lib/driver.js';
34
34
  import { behaviorGate } from '../lib/behavior-gate.js';
35
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
35
36
 
36
37
  interface DiscoveryDoc {
37
38
  capabilities?: {
@@ -59,7 +60,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
59
60
  }
60
61
 
61
62
  function promptsSupportFull(d: DiscoveryDoc | null): boolean {
62
- const p = d?.capabilities?.prompts;
63
+ const p = capabilityFamily(d, 'prompts');
63
64
  if (!p) return false;
64
65
  return p.supported === true && p.observability === 'full';
65
66
  }
@@ -34,6 +34,7 @@ import { driver } from '../lib/driver.js';
34
34
  import { pollUntilTerminal } from '../lib/polling.js';
35
35
  import { isFixtureAdvertised } from '../lib/fixtures.js';
36
36
  import { behaviorGate } from '../lib/behavior-gate.js';
37
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
37
38
 
38
39
  const WORKFLOW_ID = 'conformance-prompt-end-to-end';
39
40
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
@@ -64,7 +65,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
64
65
  }
65
66
 
66
67
  function promptsSupported(d: DiscoveryDoc | null): boolean {
67
- return d?.capabilities?.prompts?.supported === true;
68
+ return capabilityFamily(d, 'prompts')?.supported === true;
68
69
  }
69
70
 
70
71
  /** Drain the run's event log via polling. The fixture is tiny so all
@@ -34,6 +34,7 @@ import { join } from 'node:path';
34
34
  import { driver } from '../lib/driver.js';
35
35
  import { SCHEMAS_DIR } from '../lib/paths.js';
36
36
  import { behaviorGate } from '../lib/behavior-gate.js';
37
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
37
38
 
38
39
  interface DiscoveryDoc {
39
40
  capabilities?: {
@@ -65,7 +66,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
65
66
  }
66
67
 
67
68
  function endpointsSupported(d: DiscoveryDoc | null): boolean {
68
- return d?.capabilities?.prompts?.endpointsSupported === true;
69
+ return capabilityFamily(d, 'prompts')?.endpointsSupported === true;
69
70
  }
70
71
 
71
72
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -35,6 +35,7 @@
35
35
  import { describe, it, expect } from 'vitest';
36
36
  import { driver } from '../lib/driver.js';
37
37
  import { behaviorGate } from '../lib/behavior-gate.js';
38
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
38
39
 
39
40
  interface DiscoveryDoc {
40
41
  capabilities?: {
@@ -60,7 +61,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
60
61
  }
61
62
 
62
63
  function mutableSupport(d: DiscoveryDoc | null): boolean {
63
- const p = d?.capabilities?.prompts;
64
+ const p = capabilityFamily(d, 'prompts');
64
65
  return p?.endpointsSupported === true && p?.mutableLibrary === true;
65
66
  }
66
67
 
@@ -41,6 +41,7 @@
41
41
  import { describe, it, expect } from 'vitest';
42
42
  import { randomUUID } from 'node:crypto';
43
43
  import { driver } from '../lib/driver.js';
44
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
44
45
 
45
46
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
46
47
 
@@ -71,7 +72,7 @@ describe.skipIf(HTTP_SKIP)(
71
72
  ctx.skip();
72
73
  return;
73
74
  }
74
- const mutableLibrary = d.capabilities?.prompts?.mutableLibrary;
75
+ const mutableLibrary = capabilityFamily(d, 'prompts')?.mutableLibrary;
75
76
  if (mutableLibrary !== true) {
76
77
  ctx.skip();
77
78
  return;
@@ -46,6 +46,7 @@
46
46
  import { describe, it, expect } from 'vitest';
47
47
  import { driver } from '../lib/driver.js';
48
48
  import { behaviorGate } from '../lib/behavior-gate.js';
49
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
49
50
 
50
51
  interface DiscoveryDoc {
51
52
  capabilities?: {
@@ -79,7 +80,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
79
80
  }
80
81
 
81
82
  function endpointsSupported(d: DiscoveryDoc | null): boolean {
82
- return d?.capabilities?.prompts?.endpointsSupported === true;
83
+ return capabilityFamily(d, 'prompts')?.endpointsSupported === true;
83
84
  }
84
85
 
85
86
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -67,6 +67,7 @@
67
67
  import { describe, it, expect } from 'vitest';
68
68
  import { randomUUID } from 'node:crypto';
69
69
  import { driver } from '../lib/driver.js';
70
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
70
71
 
71
72
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
72
73
 
@@ -101,7 +102,7 @@ describe.skipIf(HTTP_SKIP)(
101
102
  ctx.skip();
102
103
  return;
103
104
  }
104
- const promptsSupported = d.capabilities?.prompts?.supported;
105
+ const promptsSupported = capabilityFamily(d, 'prompts')?.supported;
105
106
  if (promptsSupported !== true) {
106
107
  ctx.skip();
107
108
  return;
@@ -23,6 +23,7 @@
23
23
  import { describe, it, expect } from 'vitest';
24
24
  import { driver } from '../lib/driver.js';
25
25
  import { behaviorGate } from '../lib/behavior-gate.js';
26
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
26
27
 
27
28
  interface DiscoveryDoc {
28
29
  capabilities?: {
@@ -60,7 +61,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
60
61
  }
61
62
 
62
63
  function endpointsSupported(d: DiscoveryDoc | null): boolean {
63
- return d?.capabilities?.prompts?.endpointsSupported === true;
64
+ return capabilityFamily(d, 'prompts')?.endpointsSupported === true;
64
65
  }
65
66
 
66
67
  /** Pick a template that has at least one input-source variable
@@ -30,6 +30,7 @@
30
30
  import { describe, it, expect } from 'vitest';
31
31
  import { driver } from '../lib/driver.js';
32
32
  import { behaviorGate } from '../lib/behavior-gate.js';
33
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
33
34
 
34
35
  interface DiscoveryDoc {
35
36
  capabilities?: {
@@ -60,7 +61,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
60
61
  }
61
62
 
62
63
  function promptsAgentBindings(d: DiscoveryDoc | null): boolean {
63
- const p = d?.capabilities?.prompts;
64
+ const p = capabilityFamily(d, 'prompts');
64
65
  if (!p) return false;
65
66
  return p.supported === true && p.agentBindings === true;
66
67
  }
@@ -31,6 +31,7 @@
31
31
  import { describe, it, expect } from 'vitest';
32
32
  import { driver } from '../lib/driver.js';
33
33
  import { behaviorGate } from '../lib/behavior-gate.js';
34
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
34
35
 
35
36
  interface DiscoveryDoc {
36
37
  capabilities?: {
@@ -60,7 +61,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
60
61
  }
61
62
 
62
63
  function promptsSupported(d: DiscoveryDoc | null): boolean {
63
- return d?.capabilities?.prompts?.supported === true;
64
+ return capabilityFamily(d, 'prompts')?.supported === true;
64
65
  }
65
66
 
66
67
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -29,6 +29,7 @@
29
29
  import { describe, it, expect } from 'vitest';
30
30
  import { driver } from '../lib/driver.js';
31
31
  import { behaviorGate } from '../lib/behavior-gate.js';
32
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
32
33
 
33
34
  interface DiscoveryDoc {
34
35
  capabilities?: {
@@ -58,7 +59,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
58
59
  }
59
60
 
60
61
  function promptsSupported(d: DiscoveryDoc | null): boolean {
61
- return d?.capabilities?.prompts?.supported === true;
62
+ return capabilityFamily(d, 'prompts')?.supported === true;
62
63
  }
63
64
 
64
65
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -33,6 +33,7 @@ import { readFileSync } from 'node:fs';
33
33
  import { join } from 'node:path';
34
34
  import { driver } from '../lib/driver.js';
35
35
  import { SCHEMAS_DIR } from '../lib/paths.js';
36
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
36
37
 
37
38
  const PROMPT_KIND_VALUES = ['system', 'user', 'few-shot', 'schema-hint'] as const;
38
39
 
@@ -326,7 +327,7 @@ describe.skipIf(HTTP_SKIP)('prompt-template-shape: capabilities.prompts advertis
326
327
  it('capabilities.prompts (when present) carries the optional shape per RFC 0027 §D', async () => {
327
328
  const d = await readDiscovery();
328
329
  if (d === null) return;
329
- const prompts = d.capabilities?.prompts;
330
+ const prompts = capabilityFamily(d, 'prompts');
330
331
  if (prompts === undefined) return; // optional block; host MAY omit
331
332
  expect(
332
333
  typeof prompts.supported,
@@ -26,6 +26,7 @@ import { join } from 'node:path';
26
26
  import { driver } from '../lib/driver.js';
27
27
  import { SCHEMAS_DIR } from '../lib/paths.js';
28
28
  import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
29
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
29
30
 
30
31
  interface DiscoveryDoc {
31
32
  capabilities?: {
@@ -36,7 +37,7 @@ interface DiscoveryDoc {
36
37
  async function readProviderUsageCap(): Promise<{ supported?: boolean; costEstimates?: boolean; currency?: string } | null> {
37
38
  const res = await driver.get('/.well-known/openwop');
38
39
  const body = res.json as DiscoveryDoc | undefined;
39
- const cap = body?.capabilities?.providerUsage;
40
+ const cap = capabilityFamily(body, 'providerUsage');
40
41
  return cap && typeof cap === 'object' ? cap : null;
41
42
  }
42
43
 
@@ -100,7 +100,10 @@ describe('redaction: /.well-known/openwop secrets+aiProviders shape contract', (
100
100
  'when secrets.supported is true, scopes MUST be non-empty',
101
101
  )).toBeGreaterThanOrEqual(1);
102
102
  for (const scope of scopes) {
103
- expect(['tenant', 'user', 'run']).toContain(scope);
103
+ // Allowlist MUST track the `secrets.scopes` enum in capabilities.schema.json
104
+ // (`["tenant", "user", "run", "workspace"]`). `workspace` is the RFC 0046/0048
105
+ // sub-tenant scope — additive; hosts that advertise it (e.g. MyndHyve) are conformant.
106
+ expect(['tenant', 'user', 'run', 'workspace']).toContain(scope);
104
107
  }
105
108
  expect(s.resolution, driver.describe(
106
109
  'capabilities.md §"Secrets"',
@@ -46,6 +46,7 @@
46
46
 
47
47
  import { describe, it, expect } from 'vitest';
48
48
  import { driver } from '../lib/driver.js';
49
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
49
50
 
50
51
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
51
52
 
@@ -79,7 +80,7 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: advertisement shape (R
79
80
  ctx.skip();
80
81
  return;
81
82
  }
82
- const rd = d.capabilities?.multiAgent?.executionModel?.replayDeterminism;
83
+ const rd = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.replayDeterminism;
83
84
  if (rd === undefined) {
84
85
  ctx.skip(); // optional advertisement — host hasn't opted in
85
86
  return;
@@ -94,7 +95,7 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: advertisement shape (R
94
95
  ).toBe('boolean');
95
96
 
96
97
  if (rd.supported === true) {
97
- const version = d.capabilities?.multiAgent?.executionModel?.version as number | undefined;
98
+ const version = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version as number | undefined;
98
99
  expect(
99
100
  typeof version === 'number' && version >= 4,
100
101
  driver.describe(
@@ -170,7 +171,7 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: behavioral (RFC 0041
170
171
 
171
172
  async function gateOnPhase4(ctx: { skip: () => void }): Promise<boolean> {
172
173
  const d = await readDiscovery();
173
- const rd = d?.capabilities?.multiAgent?.executionModel?.replayDeterminism;
174
+ const rd = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.replayDeterminism;
174
175
  if (rd?.supported !== true || rd?.refusalDivergenceEmission !== true) {
175
176
  ctx.skip();
176
177
  return false;
@@ -110,7 +110,9 @@ function structuralShape(
110
110
  return events.map((e) => ({
111
111
  type: e.type,
112
112
  nodeId: e.nodeId ?? null,
113
- data: e.data ?? null,
113
+ // Canonical `payload` (run-event.schema.json) with the legacy `data`
114
+ // field as a fallback for hosts that haven't migrated their envelope.
115
+ data: e.payload ?? e.data ?? null,
114
116
  }));
115
117
  }
116
118
 
@@ -53,6 +53,7 @@
53
53
  import { describe, it, expect } from 'vitest';
54
54
  import { driver } from '../lib/driver.js';
55
55
  import { expectedCacheKey, callCacheKeySeam as callSeam } from '../lib/llm-cache-key-recipe.js';
56
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
56
57
 
57
58
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
58
59
 
@@ -167,7 +168,7 @@ describe.skipIf(HTTP_SKIP)('replay-llm-cache-key-portable: non-recipe-field inva
167
168
  describe.skipIf(HTTP_SKIP)('replay-llm-cache-key-portable: Phase 4 advertisement alignment (RFC 0041 §D)', () => {
168
169
  it('hosts advertising version: 4 MUST advertise replayDeterminism.llmCacheKeyRecipe', async (ctx) => {
169
170
  const d = await readDiscovery();
170
- const em = d?.capabilities?.multiAgent?.executionModel;
171
+ const em = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel;
171
172
  const version = em?.version;
172
173
  if (typeof version !== 'number' || version < 4) {
173
174
  ctx.skip(); // pre-Phase-4 or no multiAgent advertisement
@@ -60,7 +60,9 @@ function structuralShape(events: readonly RawEvent[]): Array<{ type: unknown; no
60
60
  return events.map((e) => ({
61
61
  type: e.type,
62
62
  nodeId: e.nodeId ?? null,
63
- data: e.data ?? null,
63
+ // Canonical `payload` (run-event.schema.json) with the legacy `data`
64
+ // field as a fallback for hosts that haven't migrated their envelope.
65
+ data: e.payload ?? e.data ?? null,
64
66
  }));
65
67
  }
66
68
 
@@ -0,0 +1,133 @@
1
+ /**
2
+ * run-execution-bounds-shape — RFC 0058 advertisement-shape + breach-contract
3
+ * verification for the two run-scoped execution bounds.
4
+ *
5
+ * Status: ACTIVE. RFC 0058 (run execution bounds) is `Active`. The
6
+ * `capabilities.limits.{maxRunDurationMs,maxLoopIterations}` fields and the
7
+ * `run-duration` / `loop-iterations` kinds on `cap.breached` have landed in
8
+ * `schemas/capabilities.schema.json` + `schemas/run-event-payloads.schema.json`.
9
+ *
10
+ * Always runs (shape-only): when the host advertises either limit, its value
11
+ * MUST be well-formed. Behavior is capability- AND fixture-gated. The
12
+ * `run-duration` (wall-clock timeout) block is now enforced + green against the
13
+ * in-memory reference host. The `loop-iterations` block stays soft-skipped until
14
+ * an execution-loop host advertises `multiAgent.executionModel` (RFC 0061),
15
+ * mirroring the RFC 0052 scheduling pattern.
16
+ *
17
+ * What this scenario asserts:
18
+ * 1. `capabilities.limits.maxRunDurationMs`, when present, is an integer ≥ 1000.
19
+ * 2. `capabilities.limits.maxLoopIterations`, when present, is an integer ≥ 1.
20
+ * 3. (gated) A run with `configurable.runTimeoutMs` below its real duration
21
+ * reaches terminal `failed` with `error.code = "run_timeout"` and emits
22
+ * `cap.breached { kind: "run-duration" }` whose `observed > limit`.
23
+ *
24
+ * @see RFCS/0058-run-execution-bounds.md
25
+ * @see spec/v1/run-options.md §Reserved keys (runTimeoutMs / maxLoopIterations)
26
+ * @see spec/v1/capabilities.md §"Engine-enforced limits and the cap.breached event"
27
+ * @see schemas/run-event-payloads.schema.json §capBreached
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { driver } from '../lib/driver.js';
32
+ import { pollUntilTerminal } from '../lib/polling.js';
33
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
34
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
35
+
36
+ interface DiscoveryLimits {
37
+ maxRunDurationMs?: number;
38
+ maxLoopIterations?: number;
39
+ }
40
+
41
+ interface DiscoveryDoc {
42
+ capabilities?: { limits?: DiscoveryLimits };
43
+ }
44
+
45
+ interface RunEvent {
46
+ readonly type: string;
47
+ readonly sequence: number;
48
+ readonly payload?: unknown;
49
+ }
50
+
51
+ const TIMEOUT_FIXTURE = 'conformance-run-duration-breach';
52
+
53
+ async function readLimits(): Promise<DiscoveryLimits | null> {
54
+ const res = await driver.get('/.well-known/openwop');
55
+ const body = res.json as DiscoveryDoc | undefined;
56
+ return capabilityFamily(body, 'limits') ?? null;
57
+ }
58
+
59
+ describe('run-execution-bounds-shape: advertisement shape (RFC 0058)', () => {
60
+ it('maxRunDurationMs is an integer >= 1000 when present', async () => {
61
+ const limits = await readLimits();
62
+ if (limits?.maxRunDurationMs === undefined) return; // not advertised
63
+ expect(
64
+ Number.isInteger(limits.maxRunDurationMs) && limits.maxRunDurationMs >= 1000,
65
+ driver.describe(
66
+ 'capabilities.schema.json §limits.maxRunDurationMs',
67
+ `capabilities.limits.maxRunDurationMs MUST be an integer >= 1000, got: ${limits.maxRunDurationMs}`,
68
+ ),
69
+ ).toBe(true);
70
+ });
71
+
72
+ it('maxLoopIterations is an integer >= 1 when present', async () => {
73
+ const limits = await readLimits();
74
+ if (limits?.maxLoopIterations === undefined) return; // not advertised
75
+ expect(
76
+ Number.isInteger(limits.maxLoopIterations) && limits.maxLoopIterations >= 1,
77
+ driver.describe(
78
+ 'capabilities.schema.json §limits.maxLoopIterations',
79
+ `capabilities.limits.maxLoopIterations MUST be an integer >= 1, got: ${limits.maxLoopIterations}`,
80
+ ),
81
+ ).toBe(true);
82
+ });
83
+ });
84
+
85
+ // Behavior: capability- AND fixture-gated. Skips on hosts that do not enforce
86
+ // run-duration timeouts (incl. the reference hosts) until one wires the seam.
87
+ const SKIP_TIMEOUT = !isFixtureAdvertised(TIMEOUT_FIXTURE);
88
+
89
+ describe.skipIf(SKIP_TIMEOUT)('run-execution-bounds: run-duration breach (RFC 0058)', () => {
90
+ it('a run with runTimeoutMs below its real duration fails with run_timeout + cap.breached{run-duration}', async () => {
91
+ const create = await driver.post('/v1/runs', {
92
+ workflowId: TIMEOUT_FIXTURE,
93
+ configurable: { runTimeoutMs: 1000 },
94
+ });
95
+ expect(create.status, driver.describe(
96
+ 'rest-endpoints.md POST /v1/runs',
97
+ 'run creation MUST accept a runTimeoutMs override',
98
+ )).toBe(201);
99
+ const runId = (create.json as { runId: string }).runId;
100
+
101
+ const terminal = await pollUntilTerminal(runId);
102
+ expect(terminal.status, driver.describe(
103
+ 'run-options.md §runTimeoutMs',
104
+ 'a run exceeding its runTimeoutMs MUST reach terminal `failed`',
105
+ )).toBe('failed');
106
+ expect(terminal.error?.code, driver.describe(
107
+ 'rest-endpoints.md §run_timeout',
108
+ 'RunSnapshot.error.code MUST equal "run_timeout" on wall-clock timeout',
109
+ )).toBe('run_timeout');
110
+
111
+ const eventsRes = await driver.get(
112
+ `/v1/runs/${encodeURIComponent(runId)}/events/poll?lastSequence=0&timeout=1`,
113
+ );
114
+ const events = (eventsRes.json as { events?: RunEvent[] } | undefined)?.events ?? [];
115
+ const breach = events.find((e) => e.type === 'cap.breached');
116
+ expect(breach, driver.describe(
117
+ 'capabilities.md §Engine-enforced limits',
118
+ 'a cap.breached event MUST be emitted on run-duration breach',
119
+ )).toBeDefined();
120
+ const payload = breach!.payload as { kind?: string; limit?: number; observed?: number } | undefined;
121
+ expect(payload?.kind, driver.describe(
122
+ 'run-event-payloads.schema.json §capBreached.kind',
123
+ 'cap.breached payload MUST carry kind="run-duration"',
124
+ )).toBe('run-duration');
125
+ expect(
126
+ typeof payload?.observed === 'number' && typeof payload?.limit === 'number' && payload!.observed > payload!.limit,
127
+ driver.describe(
128
+ 'run-event-payloads.schema.json §capBreached.observed',
129
+ 'observed (elapsedMs) MUST be strictly greater than limit (resolved timeout)',
130
+ ),
131
+ ).toBe(true);
132
+ });
133
+ });
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { describe, it, expect } from 'vitest';
17
17
  import { driver } from '../lib/driver.js';
18
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
18
19
 
19
20
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
20
21
 
@@ -26,7 +27,7 @@ async function readSandbox(): Promise<{ supported: boolean; memoryLimitBytes?: n
26
27
  try {
27
28
  const r = await driver.get('/.well-known/openwop');
28
29
  if (r.status !== 200) return null;
29
- const sb = (r.json as D).capabilities?.sandbox;
30
+ const sb = capabilityFamily((r.json as D), 'sandbox');
30
31
  if (!sb || sb.supported !== true) return null;
31
32
  return {
32
33
  supported: true,
@@ -37,6 +37,7 @@
37
37
 
38
38
  import { describe, it, expect } from 'vitest';
39
39
  import { driver } from '../lib/driver.js';
40
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
40
41
 
41
42
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
42
43
 
@@ -66,7 +67,7 @@ async function isSandboxAdvertised(): Promise<boolean> {
66
67
  try {
67
68
  const res = await driver.get('/.well-known/openwop');
68
69
  if (res.status !== 200) return false;
69
- return (res.json as DiscoveryDoc).capabilities?.sandbox?.supported === true;
70
+ return capabilityFamily((res.json as DiscoveryDoc), 'sandbox')?.supported === true;
70
71
  } catch {
71
72
  return false;
72
73
  }
@@ -26,6 +26,7 @@
26
26
 
27
27
  import { describe, it, expect } from 'vitest';
28
28
  import { driver } from '../lib/driver.js';
29
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
29
30
 
30
31
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
31
32
 
@@ -45,7 +46,7 @@ async function readSandboxCaps(): Promise<SandboxCaps | null> {
45
46
  try {
46
47
  const res = await driver.get('/.well-known/openwop');
47
48
  if (res.status !== 200) return null;
48
- return (res.json as DiscoveryDoc).capabilities?.sandbox ?? null;
49
+ return capabilityFamily((res.json as DiscoveryDoc), 'sandbox') ?? null;
49
50
  } catch {
50
51
  return null;
51
52
  }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { describe, it, expect } from 'vitest';
17
17
  import { driver } from '../lib/driver.js';
18
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
18
19
 
19
20
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
20
21
 
@@ -26,7 +27,7 @@ async function readSandbox(): Promise<{ supported: boolean; wallClockLimitMs?: n
26
27
  try {
27
28
  const r = await driver.get('/.well-known/openwop');
28
29
  if (r.status !== 200) return null;
29
- const sb = (r.json as D).capabilities?.sandbox;
30
+ const sb = capabilityFamily((r.json as D), 'sandbox');
30
31
  if (!sb || sb.supported !== true) return null;
31
32
  return {
32
33
  supported: true,
@@ -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 DiscoveryScheduling {
25
26
  supported?: boolean;
@@ -39,7 +40,7 @@ const ISO_DURATION = /^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M
39
40
  async function readScheduling(): Promise<DiscoveryScheduling | null> {
40
41
  const res = await driver.get('/.well-known/openwop');
41
42
  const body = res.json as DiscoveryDoc | undefined;
42
- return body?.capabilities?.scheduling ?? null;
43
+ return capabilityFamily(body, 'scheduling') ?? null;
43
44
  }
44
45
 
45
46
  describe('scheduling-capability-shape: advertisement shape (RFC 0052 §A)', () => {
@@ -26,6 +26,7 @@
26
26
 
27
27
  import { describe, it, expect } from 'vitest';
28
28
  import { driver } from '../lib/driver.js';
29
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
29
30
 
30
31
  interface DiscoveryDoc {
31
32
  capabilities?: { scheduling?: { supported?: boolean; cron?: boolean } };
@@ -33,7 +34,7 @@ interface DiscoveryDoc {
33
34
 
34
35
  async function readScheduling(): Promise<{ supported?: boolean; cron?: boolean } | null> {
35
36
  const res = await driver.get('/.well-known/openwop');
36
- return (res.json as DiscoveryDoc | undefined)?.capabilities?.scheduling ?? null;
37
+ return capabilityFamily((res.json as DiscoveryDoc | undefined), 'scheduling') ?? null;
37
38
  }
38
39
 
39
40
  describe('scheduling-cron-fires-once: once-per-tick + missed-tick (RFC 0052 §B)', () => {
@@ -55,6 +55,7 @@ import { describe, it, expect } from 'vitest';
55
55
  import { driver } from '../lib/driver.js';
56
56
  import { pollUntilTerminal } from '../lib/polling.js';
57
57
  import { isFixtureAdvertised } from '../lib/fixtures.js';
58
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
58
59
 
59
60
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
60
61
  const BYOK_WORKFLOW_ID = 'openwop-smoke-byok-roundtrip';
@@ -99,8 +100,8 @@ describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
99
100
  return;
100
101
  }
101
102
  const d = await readDiscovery();
102
- const secretsOk = d?.capabilities?.secrets?.supported === true;
103
- const seamOk = d?.capabilities?.observability?.testSeams?.otelScrape === true;
103
+ const secretsOk = capabilityFamily<{ supported?: unknown }>(d, 'secrets')?.supported === true;
104
+ const seamOk = capabilityFamily<{ testSeams?: Record<string, unknown> }>(d, 'observability')?.testSeams?.otelScrape === true;
104
105
  if (!secretsOk || !seamOk) {
105
106
  ctx.skip();
106
107
  return;
@@ -168,8 +169,8 @@ describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
168
169
  return;
169
170
  }
170
171
  const d = await readDiscovery();
171
- const secretsOk = d?.capabilities?.secrets?.supported === true;
172
- const seamOk = d?.capabilities?.observability?.testSeams?.debugBundleExport === true;
172
+ const secretsOk = capabilityFamily<{ supported?: unknown }>(d, 'secrets')?.supported === true;
173
+ const seamOk = capabilityFamily<{ testSeams?: Record<string, unknown> }>(d, 'observability')?.testSeams?.debugBundleExport === true;
173
174
  if (!secretsOk || !seamOk) {
174
175
  ctx.skip();
175
176
  return;
@@ -209,11 +210,11 @@ describe.skipIf(HTTP_SKIP || FIXTURE_SKIP)(
209
210
  () => {
210
211
  it('when secrets.supported is true, observability.testSeams advertisements MUST be boolean if present', async (ctx) => {
211
212
  const d = await readDiscovery();
212
- if (d?.capabilities?.secrets?.supported !== true) {
213
+ if (capabilityFamily<{ supported?: unknown }>(d, 'secrets')?.supported !== true) {
213
214
  ctx.skip();
214
215
  return;
215
216
  }
216
- const seams = d?.capabilities?.observability?.testSeams;
217
+ const seams = capabilityFamily<{ testSeams?: Record<string, unknown> }>(d, 'observability')?.testSeams;
217
218
  if (seams === undefined) {
218
219
  ctx.skip(); // host honest about not exposing the seams — Drift #17 path
219
220
  return;