@openwop/openwop-conformance 1.6.1 → 1.11.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 (200) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +127 -0
  4. package/api/openapi.yaml +518 -1
  5. package/coverage.md +44 -2
  6. package/fixtures/conformance-run-duration-breach.json +33 -0
  7. package/fixtures/oauth-providers/synthetic.json +38 -0
  8. package/fixtures.md +29 -0
  9. package/package.json +1 -1
  10. package/schemas/README.md +22 -0
  11. package/schemas/agent-deployment-transition.schema.json +49 -0
  12. package/schemas/agent-deployment.schema.json +54 -0
  13. package/schemas/agent-eval-suite.schema.json +140 -0
  14. package/schemas/agent-inventory-response.schema.json +115 -0
  15. package/schemas/agent-manifest.schema.json +5 -0
  16. package/schemas/agent-org-chart.schema.json +82 -0
  17. package/schemas/agent-ref.schema.json +12 -2
  18. package/schemas/agent-roster-entry.schema.json +81 -0
  19. package/schemas/agent-roster-response.schema.json +21 -0
  20. package/schemas/ai-envelope.schema.json +28 -0
  21. package/schemas/artifact-type-pack-manifest.schema.json +160 -0
  22. package/schemas/budget-policy.schema.json +18 -0
  23. package/schemas/capabilities.schema.json +448 -4
  24. package/schemas/chat-card-pack-manifest.schema.json +158 -0
  25. package/schemas/credential-provenance.schema.json +18 -0
  26. package/schemas/envelopes/media.audio.schema.json +38 -0
  27. package/schemas/envelopes/media.file.schema.json +37 -0
  28. package/schemas/envelopes/media.image.schema.json +33 -0
  29. package/schemas/eval-summary.schema.json +92 -0
  30. package/schemas/heartbeat-evaluated.schema.json +14 -0
  31. package/schemas/heartbeat-state-changed.schema.json +14 -0
  32. package/schemas/node-pack-manifest.schema.json +33 -1
  33. package/schemas/org-chart-responsibility-view.schema.json +26 -0
  34. package/schemas/run-event-payloads.schema.json +380 -6
  35. package/schemas/run-event.schema.json +23 -0
  36. package/schemas/tool-descriptor.schema.json +63 -0
  37. package/schemas/trigger-subscription.schema.json +26 -0
  38. package/schemas/workflow-definition.schema.json +5 -0
  39. package/schemas/workspace-file-create.schema.json +20 -0
  40. package/schemas/workspace-file.schema.json +39 -0
  41. package/src/lib/agentLoop.ts +44 -0
  42. package/src/lib/agentRoster.ts +76 -0
  43. package/src/lib/agentRuntime.ts +45 -0
  44. package/src/lib/artifactTypes.ts +96 -0
  45. package/src/lib/cardPacks.ts +52 -0
  46. package/src/lib/discovery-capabilities.ts +50 -0
  47. package/src/lib/distillation.ts +38 -0
  48. package/src/lib/feedback.ts +3 -3
  49. package/src/lib/heartbeat.ts +31 -0
  50. package/src/lib/liveRuntime.ts +59 -0
  51. package/src/lib/memoryAttribution.ts +48 -0
  52. package/src/lib/profiles.ts +157 -0
  53. package/src/lib/runtimeRequires.ts +38 -0
  54. package/src/lib/safeFetch.ts +87 -0
  55. package/src/lib/subRunAttestation.ts +35 -0
  56. package/src/lib/toolHooks.ts +33 -0
  57. package/src/scenarios/agent-deployment-shape.test.ts +139 -0
  58. package/src/scenarios/agent-eval-suite-shape.test.ts +167 -0
  59. package/src/scenarios/agent-live-allowlist-enforced.test.ts +53 -0
  60. package/src/scenarios/agent-live-invocation-bracket.test.ts +98 -0
  61. package/src/scenarios/agent-live-runtime-shape.test.ts +98 -0
  62. package/src/scenarios/agent-live-structured-output.test.ts +58 -0
  63. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
  64. package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
  65. package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
  66. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
  67. package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
  68. package/src/scenarios/agent-org-chart-shape.test.ts +127 -0
  69. package/src/scenarios/agent-platform-profile.test.ts +158 -0
  70. package/src/scenarios/agent-roster-attribution.test.ts +179 -0
  71. package/src/scenarios/agent-roster-shape.test.ts +146 -0
  72. package/src/scenarios/ai-envelope-shape.test.ts +14 -18
  73. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
  74. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
  75. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
  76. package/src/scenarios/approval-gate-flow.test.ts +4 -6
  77. package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
  78. package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
  79. package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
  80. package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
  81. package/src/scenarios/audit-log-integrity.test.ts +3 -2
  82. package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
  83. package/src/scenarios/auth-mtls.test.ts +2 -1
  84. package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
  85. package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
  86. package/src/scenarios/auth-saml-profile.test.ts +2 -1
  87. package/src/scenarios/auth-scim-profile.test.ts +2 -1
  88. package/src/scenarios/authorization-fail-closed.test.ts +2 -1
  89. package/src/scenarios/authorization-roles-shape.test.ts +2 -1
  90. package/src/scenarios/budget-policy-shape.test.ts +136 -0
  91. package/src/scenarios/byok-auth-modes.test.ts +141 -0
  92. package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
  93. package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
  94. package/src/scenarios/commitment-fired.test.ts +83 -0
  95. package/src/scenarios/credential-payload-redaction.test.ts +2 -1
  96. package/src/scenarios/credentials-capability-shape.test.ts +2 -1
  97. package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
  98. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
  99. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  100. package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
  101. package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
  102. package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
  103. package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
  104. package/src/scenarios/distillation-shape.test.ts +41 -0
  105. package/src/scenarios/distillation-stable-archive.test.ts +37 -0
  106. package/src/scenarios/distillation-token-budget.test.ts +45 -0
  107. package/src/scenarios/egress-provenance-shape.test.ts +137 -0
  108. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
  109. package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
  110. package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
  111. package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
  112. package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
  113. package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
  114. package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
  115. package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
  116. package/src/scenarios/experimental-tier-shape.test.ts +5 -4
  117. package/src/scenarios/fs-path-traversal.test.ts +2 -1
  118. package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
  119. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
  120. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
  121. package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
  122. package/src/scenarios/http-client-ssrf.test.ts +10 -13
  123. package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
  124. package/src/scenarios/media-url-inline-cap.test.ts +167 -0
  125. package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
  126. package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
  127. package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
  128. package/src/scenarios/memory-attribution-shape.test.ts +28 -0
  129. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
  130. package/src/scenarios/memory-capability-model-shape.test.ts +186 -0
  131. package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
  132. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
  133. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
  134. package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
  135. package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
  136. package/src/scenarios/model-capability-substituted.test.ts +2 -1
  137. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  138. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
  139. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
  140. package/src/scenarios/multi-region-idempotency.test.ts +10 -10
  141. package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +145 -0
  142. package/src/scenarios/oauth-capability-shape.test.ts +2 -1
  143. package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
  144. package/src/scenarios/pause-resume.test.ts +3 -3
  145. package/src/scenarios/production-backpressure.test.ts +2 -2
  146. package/src/scenarios/production-retention-expiry.test.ts +2 -2
  147. package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
  148. package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
  149. package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
  150. package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
  151. package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
  152. package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
  153. package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
  154. package/src/scenarios/prompt-pack-install.test.ts +2 -1
  155. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
  156. package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
  157. package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
  158. package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
  159. package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
  160. package/src/scenarios/prompt-template-shape.test.ts +2 -1
  161. package/src/scenarios/provider-usage.test.ts +2 -1
  162. package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
  163. package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
  164. package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
  165. package/src/scenarios/replayDeterminism.test.ts +3 -1
  166. package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
  167. package/src/scenarios/runtime-requires-install-gate.test.ts +92 -0
  168. package/src/scenarios/runtime-requires-shape.test.ts +134 -0
  169. package/src/scenarios/safefetch-behavior.test.ts +99 -0
  170. package/src/scenarios/safefetch-live-audit.test.ts +175 -0
  171. package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
  172. package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
  173. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  174. package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
  175. package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
  176. package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
  177. package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
  178. package/src/scenarios/spec-corpus-validity.test.ts +20 -4
  179. package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
  180. package/src/scenarios/subrun-approval-gate.test.ts +35 -0
  181. package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
  182. package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
  183. package/src/scenarios/tool-descriptor-shape.test.ts +133 -0
  184. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
  185. package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
  186. package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
  187. package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
  188. package/src/scenarios/tool-hooks-shape.test.ts +34 -0
  189. package/src/scenarios/trigger-bridge-shape.test.ts +135 -0
  190. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
  191. package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
  192. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
  193. package/src/scenarios/wasm-pack-load.test.ts +2 -2
  194. package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
  195. package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
  196. package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
  197. package/src/scenarios/workspace-behavior.test.ts +134 -0
  198. package/src/scenarios/workspace-capability-shape.test.ts +73 -0
  199. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
  200. package/src/scenarios/x-openwop-form-pack-manifest.test.ts +155 -0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * heartbeat-fires-once-per-tick — RFC 0060 §B.1. A tick produces exactly one
3
+ * `heartbeat.evaluated`; an overlapping tick while a prior evaluation is still
4
+ * running is skipped (not queued).
5
+ *
6
+ * Gated on `capabilities.heartbeat.supported` + the host heartbeat tick seam
7
+ * (`POST /v1/host/sample/heartbeat/tick`); soft-skips when either is absent.
8
+ *
9
+ * @see RFCS/0060-host-heartbeat-capability.md §B
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { driver } from '../lib/driver.js';
14
+ import { readHeartbeatCap, heartbeatSupported, tickHeartbeat } from '../lib/heartbeat.js';
15
+
16
+ describe('heartbeat-fires-once-per-tick (RFC 0060 §B.1)', () => {
17
+ it('one tick emits exactly one heartbeat.evaluated', async () => {
18
+ if (!heartbeatSupported(await readHeartbeatCap())) return;
19
+ const res = await tickHeartbeat({ heartbeatId: 'conformance-hb', observedState: { n: 0 } });
20
+ if (res === null) return; // seam absent — soft-skip
21
+ const evaluated = (res.json as { evaluated?: unknown[] } | undefined)?.evaluated;
22
+ if (!Array.isArray(evaluated)) return; // host doesn't surface per-tick events on the seam
23
+ expect(
24
+ evaluated.length,
25
+ driver.describe('RFC 0060 §B.1', 'a single tick MUST emit exactly one heartbeat.evaluated'),
26
+ ).toBe(1);
27
+ });
28
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * heartbeat-idempotent-no-spam — RFC 0060 §B.5. Two ticks at unchanged state
3
+ * produce zero enqueued runs and zero `heartbeat.stateChanged`; only the
4
+ * transitioning tick produces exactly one of each. This is the anti-spam
5
+ * guarantee — action is gated on a state *transition*, not on the tick.
6
+ *
7
+ * Gated on `capabilities.heartbeat.supported` + the host tick seam;
8
+ * soft-skips when either is absent.
9
+ *
10
+ * @see RFCS/0060-host-heartbeat-capability.md §B
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest';
14
+ import { driver } from '../lib/driver.js';
15
+ import { readHeartbeatCap, heartbeatSupported, tickHeartbeat } from '../lib/heartbeat.js';
16
+
17
+ function changedCount(json: unknown): number | null {
18
+ const sc = (json as { stateChanged?: unknown[] } | undefined)?.stateChanged;
19
+ return Array.isArray(sc) ? sc.length : null;
20
+ }
21
+
22
+ describe('heartbeat-idempotent-no-spam (RFC 0060 §B.5)', () => {
23
+ it('an unchanged tick enqueues nothing; only a transition does', async () => {
24
+ if (!heartbeatSupported(await readHeartbeatCap())) return;
25
+ const hb = 'conformance-hb-spam';
26
+ const first = await tickHeartbeat({ heartbeatId: hb, observedState: { unread: 0 } });
27
+ if (first === null) return; // seam absent — soft-skip
28
+ const second = await tickHeartbeat({ heartbeatId: hb, observedState: { unread: 0 } });
29
+ if (second === null) return;
30
+ const unchanged = changedCount(second.json);
31
+ if (unchanged === null) return; // host doesn't surface stateChanged on the seam
32
+ expect(
33
+ unchanged,
34
+ driver.describe('RFC 0060 §B.5', 'an unchanged tick MUST NOT emit heartbeat.stateChanged'),
35
+ ).toBe(0);
36
+ const transition = await tickHeartbeat({ heartbeatId: hb, observedState: { unread: 3 } });
37
+ if (transition === null) return;
38
+ expect(
39
+ changedCount(transition.json),
40
+ driver.describe('RFC 0060 §B.5', 'a transitioning tick MUST emit exactly one heartbeat.stateChanged'),
41
+ ).toBe(1);
42
+ });
43
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * heartbeat-runtime-bound — RFC 0060 §B.2. A predicate exceeding `maxRuntimeMs`
3
+ * is terminated and reported `heartbeat.evaluated { status: "timeout" }`,
4
+ * never left running.
5
+ *
6
+ * Gated on `capabilities.heartbeat.supported` + the host tick seam;
7
+ * soft-skips when either is absent.
8
+ *
9
+ * @see RFCS/0060-host-heartbeat-capability.md §B
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { driver } from '../lib/driver.js';
14
+ import { readHeartbeatCap, heartbeatSupported, tickHeartbeat } from '../lib/heartbeat.js';
15
+
16
+ describe('heartbeat-runtime-bound (RFC 0060 §B.2)', () => {
17
+ it('an over-budget predicate is reported as timeout', async () => {
18
+ if (!heartbeatSupported(await readHeartbeatCap())) return;
19
+ // `simulateSlowMs` is a host-seam hint asking the predicate to overrun
20
+ // its maxRuntimeMs budget; hosts not honoring it surface no `timeout`.
21
+ const res = await tickHeartbeat({ heartbeatId: 'conformance-hb-slow', observedState: {}, simulateSlowMs: 60_000 });
22
+ if (res === null) return; // seam absent — soft-skip
23
+ const evaluated = (res.json as { evaluated?: Array<{ status?: unknown }> } | undefined)?.evaluated;
24
+ if (!Array.isArray(evaluated) || evaluated.length === 0) return; // host doesn't surface per-tick events
25
+ expect(
26
+ evaluated.every((e) => e.status === 'timeout'),
27
+ driver.describe('RFC 0060 §B.2', 'an over-budget predicate MUST be terminated and reported status:"timeout"'),
28
+ ).toBe(true);
29
+ });
30
+ });
@@ -20,12 +20,13 @@
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
  async function isHttpClientSupported(): Promise<boolean> {
25
26
  const disco = await driver.get('/.well-known/openwop');
26
- const caps = (disco.json as { capabilities?: { httpClient?: { supported?: boolean } } })
27
- .capabilities;
28
- return caps?.httpClient?.supported === true;
27
+ return (
28
+ capabilityFamily<{ supported?: boolean }>(disco.json, 'httpClient')?.supported === true
29
+ );
29
30
  }
30
31
 
31
32
  describe('http-client-ssrf: capability advertisement contract', () => {
@@ -36,16 +37,12 @@ describe('http-client-ssrf: capability advertisement contract', () => {
36
37
  return;
37
38
  }
38
39
  const disco = await driver.get('/.well-known/openwop');
39
- const cap = (disco.json as {
40
- capabilities?: {
41
- httpClient?: {
42
- supported?: boolean;
43
- ssrfGuard?: boolean;
44
- maxResponseBodyBytes?: number;
45
- methods?: unknown;
46
- };
47
- };
48
- }).capabilities?.httpClient;
40
+ const cap = capabilityFamily<{
41
+ supported?: boolean;
42
+ ssrfGuard?: boolean;
43
+ maxResponseBodyBytes?: number;
44
+ methods?: unknown;
45
+ }>(disco.json, 'httpClient');
49
46
 
50
47
  expect(cap?.supported, driver.describe(
51
48
  'capabilities.md §httpClient',
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { describe, it, expect } from 'vitest';
23
23
  import { driver } from '../lib/driver.js';
24
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
24
25
 
25
26
  async function isMcpClientSupported(): Promise<boolean> {
26
27
  const disco = await driver.get('/.well-known/openwop');
@@ -37,11 +38,11 @@ describe('mcp-toolcall-redaction: capability advertisement contract', () => {
37
38
  return;
38
39
  }
39
40
  const disco = await driver.get('/.well-known/openwop');
40
- const cap = (disco.json as {
41
+ const cap = capabilityFamily((disco.json as {
41
42
  capabilities?: {
42
43
  mcpClient?: { supported?: boolean; transports?: unknown; trustBoundary?: string };
43
44
  };
44
- }).capabilities?.mcpClient;
45
+ }), 'mcpClient');
45
46
 
46
47
  expect(cap?.supported, driver.describe(
47
48
  'host-capabilities.md §host.mcp',
@@ -0,0 +1,167 @@
1
+ /**
2
+ * media-url-inline-cap — RFC 0055 §C media envelope kinds + asset-URL discipline.
3
+ *
4
+ * SECURITY invariant: `media-asset-url-tenant-scoped` (RFC 0055 §C rule 1 + 4).
5
+ *
6
+ * Always-on (server-free):
7
+ * 1. The three `media.{image,audio,file}` payload schemas compile (Ajv2020).
8
+ * 2. Positive round-trip: a URL-reference payload and an inline-base64
9
+ * payload each validate.
10
+ * 3. Negative: a payload missing the required `bytes` is rejected; an
11
+ * unknown property is rejected (additionalProperties:false).
12
+ *
13
+ * Advertisement-shape (HTTP, soft-skip offline):
14
+ * 4. When a host advertises `aiProviders.maxInlineMediaBytes`, it MUST be a
15
+ * non-negative integer.
16
+ *
17
+ * Behavioral (cross-tenant scoping + cap enforcement) is staged via `it.todo`
18
+ * until a reference host wires tenant-scoped asset serving (greenfield;
19
+ * RFC 0027 §G precedent — advertisement + schema land first).
20
+ *
21
+ * @see RFCS/0055-multimodal-envelope-variants-and-rendering-hints.md §C
22
+ * @see spec/v1/ai-envelope.md §"Media reference payloads"
23
+ * @see SECURITY/invariants.yaml#media-asset-url-tenant-scoped
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import Ajv2020 from 'ajv/dist/2020.js';
28
+ import addFormats from 'ajv-formats';
29
+ import { readFileSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+ import { driver } from '../lib/driver.js';
32
+ import { SCHEMAS_DIR } from '../lib/paths.js';
33
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
34
+
35
+ const MEDIA_KINDS = ['media.image', 'media.audio', 'media.file'] as const;
36
+ const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
37
+
38
+ function compile(kind: string): ReturnType<Ajv2020['compile']> {
39
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
40
+ addFormats(ajv);
41
+ const schema = JSON.parse(
42
+ readFileSync(join(SCHEMAS_DIR, `envelopes/${kind}.schema.json`), 'utf8'),
43
+ ) as Record<string, unknown>;
44
+ return ajv.compile(schema);
45
+ }
46
+
47
+ describe('media-url-inline-cap: media payload schemas compile + round-trip (RFC 0055 §C)', () => {
48
+ for (const kind of MEDIA_KINDS) {
49
+ it(`envelopes/${kind}.schema.json compiles under Ajv2020`, () => {
50
+ expect(
51
+ compile(kind),
52
+ `ai-envelope.md §"Media reference payloads": ${kind} payload schema MUST compile`,
53
+ ).toBeTypeOf('function');
54
+ });
55
+ }
56
+
57
+ it('accepts a URL-reference image payload', () => {
58
+ const ok = compile('media.image')({
59
+ url: 'https://host.example/v1/runs/run_1/assets/img_9.png',
60
+ bytes: 184320,
61
+ mimeType: 'image/png',
62
+ });
63
+ expect(ok, 'URL-reference media payload MUST validate').toBe(true);
64
+ });
65
+
66
+ it('accepts an inline-base64 audio payload', () => {
67
+ const ok = compile('media.audio')({ base64: 'AAAA', bytes: 3, mimeType: 'audio/ogg', durationSeconds: 1.2 });
68
+ expect(ok, 'inline-base64 media payload MUST validate').toBe(true);
69
+ });
70
+
71
+ it('rejects a payload missing required bytes', () => {
72
+ const ok = compile('media.file')({ url: 'https://host.example/v1/runs/run_1/assets/report.pdf' });
73
+ expect(ok, 'ai-envelope.md §"Media reference payloads": `bytes` is required').toBe(false);
74
+ });
75
+
76
+ it('rejects an unknown property (additionalProperties:false)', () => {
77
+ const ok = compile('media.image')({ bytes: 1, wat: true });
78
+ expect(ok, 'media payload is additionalProperties:false').toBe(false);
79
+ });
80
+ });
81
+
82
+ interface DiscoveryDoc {
83
+ capabilities?: { aiProviders?: { maxInlineMediaBytes?: unknown } };
84
+ }
85
+
86
+ describe.skipIf(HTTP_SKIP)('media-url-inline-cap: advertisement shape (RFC 0055 §C rule 2)', () => {
87
+ it('aiProviders.maxInlineMediaBytes is a non-negative integer when advertised', async () => {
88
+ const res = await driver.get('/.well-known/openwop');
89
+ if (res.status !== 200) return;
90
+ const cap = capabilityFamily((res.json as DiscoveryDoc), 'aiProviders')?.maxInlineMediaBytes;
91
+ if (cap === undefined) return; // optional — soft-skip when absent
92
+ expect(
93
+ Number.isInteger(cap) && (cap as number) >= 0,
94
+ driver.describe('capabilities.md §aiProviders.maxInlineMediaBytes', 'cap MUST be a non-negative integer'),
95
+ ).toBe(true);
96
+ });
97
+
98
+ // Behavioral assertions for `media-asset-url-tenant-scoped`. Driven via the
99
+ // reference host's media-asset seam (store: POST /v1/host/sample/media/put,
100
+ // env-gated; serve: GET /v1/host/sample/assets/{token}, public token-auth).
101
+ // Soft-skip (return) when the host doesn't expose the store seam (404).
102
+ const PNG_1x1 =
103
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
104
+
105
+ it('a stored media asset is served by a tenant-scoped URL, not inlined', async () => {
106
+ const stored = await driver.post('/v1/host/sample/media/put', { contentBase64: PNG_1x1, contentType: 'image/png' });
107
+ if (stored.status === 404) return; // store seam disabled — soft-skip
108
+ expect(stored.status, 'media store MUST return 201').toBe(201);
109
+ const body = stored.json as { url?: string; bytes?: number };
110
+ expect(
111
+ typeof body.url === 'string' && /\/v1\/host\/sample\/assets\//.test(body.url!),
112
+ driver.describe('ai-envelope.md §"Media reference payloads"', 'asset MUST be served by a URL reference, not inlined'),
113
+ ).toBe(true);
114
+ const served = await driver.get(body.url!);
115
+ expect(served.status, 'the asset URL MUST resolve').toBe(200);
116
+ });
117
+
118
+ it('an unminted/guessed asset token does not resolve (media-asset-url-tenant-scoped)', async () => {
119
+ // Probe whether the serve route exists at all; soft-skip if not.
120
+ const probe = await driver.get('/v1/host/sample/assets/probe-never-minted-token');
121
+ if (probe.status === 404 && !process.env.OPENWOP_BASE_URL) return;
122
+ expect(
123
+ probe.status,
124
+ driver.describe('SECURITY/invariants.yaml#media-asset-url-tenant-scoped', 'a token not held by the caller (unguessable 256-bit) MUST NOT resolve'),
125
+ ).toBe(404);
126
+ });
127
+
128
+ it('a media.* payload in a run debug bundle is referenced by URL, not inlined (RFC 0055 §C rule 3)', async () => {
129
+ // RFC 0055 §C rule 3: asset URLs are part of a run's debug-bundle manifest
130
+ // BY REFERENCE, never by inlining the binary. Gated on a host that both
131
+ // serves media (advertises aiProviders.maxInlineMediaBytes) and exports
132
+ // debug bundles (capabilities.debugBundle.supported). Soft-skips otherwise
133
+ // — and on the reference host, which exports debug bundles but has no node
134
+ // that emits a media.* envelope into a run (so no media payload appears).
135
+ const disc = await driver.get('/.well-known/openwop');
136
+ if (disc.status !== 200) return;
137
+ const caps = (disc.json as {
138
+ capabilities?: { aiProviders?: { maxInlineMediaBytes?: unknown }; debugBundle?: { supported?: unknown } };
139
+ }).capabilities;
140
+ if (caps?.aiProviders?.maxInlineMediaBytes === undefined || caps.debugBundle?.supported !== true) {
141
+ return; // host doesn't serve media + export debug bundles — contract not exercisable
142
+ }
143
+ // Find a recent run and inspect its debug bundle for any media.* event.
144
+ const runs = await driver.get('/v1/runs?limit=20');
145
+ if (runs.status !== 200) return;
146
+ const runIds = ((runs.json as { runs?: { runId?: string }[] }).runs ?? [])
147
+ .map((r) => r.runId)
148
+ .filter((id): id is string => typeof id === 'string');
149
+ for (const runId of runIds) {
150
+ const bundle = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/debug-bundle`);
151
+ if (bundle.status !== 200) continue;
152
+ const events = (bundle.json as { events?: { type?: string; payload?: { url?: unknown; base64?: unknown } }[] }).events ?? [];
153
+ for (const ev of events) {
154
+ if (typeof ev.type === 'string' && ev.type.startsWith('media.')) {
155
+ // The §C rule-3 contract: served by URL, not inlined binary.
156
+ expect(
157
+ typeof ev.payload?.url === 'string' && ev.payload?.base64 === undefined,
158
+ driver.describe('ai-envelope.md §"Media reference payloads"', 'a media.* payload in a debug bundle MUST be a URL reference, never inlined binary'),
159
+ ).toBe(true);
160
+ return; // asserted one — contract proven
161
+ }
162
+ }
163
+ }
164
+ // No media.* payload surfaced in any recent run's debug bundle on this
165
+ // host — nothing to assert (the contract holds vacuously).
166
+ });
167
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * memory-attribution-emits-on-write — RFC 0057 §A/§B. A host advertising
3
+ * `capabilities.memory.attribution.emitsWriteEvents: true` emits a
4
+ * `memory.written` event (with resolvable identifiers) for the memory its run
5
+ * writes. A host NOT advertising the capability emits none and still passes
6
+ * the locked core.
7
+ *
8
+ * @see RFCS/0057-memory-write-attribution-event.md §A
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest';
12
+ import { driver } from '../lib/driver.js';
13
+ import { pollUntilTerminal } from '../lib/polling.js';
14
+ import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
15
+
16
+ describe('memory-attribution-emits-on-write (RFC 0057 §A/§B)', () => {
17
+ it('an advertised host emits memory.written carrying a stable memoryId', async () => {
18
+ const cap = await readMemoryAttributionCap();
19
+ if (!emitsWriteEvents(cap)) return;
20
+ const runId = await seedRun('mem-attr-emit');
21
+ if (!runId) return;
22
+ try {
23
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
24
+ } catch {
25
+ return;
26
+ }
27
+ const events = await memoryWrittenEvents(runId);
28
+ if (events.length === 0) return; // run wrote no memory — soft-skip
29
+ for (const e of events) {
30
+ const memoryId = (e.payload as { memoryId?: unknown } | undefined)?.memoryId;
31
+ expect(
32
+ typeof memoryId === 'string' && memoryId.length > 0,
33
+ driver.describe('RFC 0057 §B', 'memory.written.memoryId MUST be a stable, non-empty identifier'),
34
+ ).toBe(true);
35
+ }
36
+ });
37
+
38
+ it('a host without the capability emits no memory.written', async () => {
39
+ const cap = await readMemoryAttributionCap();
40
+ if (emitsWriteEvents(cap)) return; // advertised — N/A
41
+ const runId = await seedRun('mem-attr-absent');
42
+ if (!runId) return;
43
+ try {
44
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
45
+ } catch {
46
+ return;
47
+ }
48
+ const events = await memoryWrittenEvents(runId);
49
+ expect(
50
+ events.length,
51
+ driver.describe('RFC 0057 §A', 'a host not advertising memory.attribution MUST NOT emit memory.written'),
52
+ ).toBe(0);
53
+ });
54
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * memory-attribution-no-content — RFC 0057 §C + SECURITY/invariants.yaml
3
+ * `memory-attribution-no-content`. A `memory.written` payload carries
4
+ * identifiers + non-secret tags only — never the memory entry content (the
5
+ * read-side serves that, already SR-1-redacted).
6
+ *
7
+ * Gated on `capabilities.memory.attribution.emitsWriteEvents`; soft-skips when
8
+ * unadvertised or when the seeded run wrote no memory.
9
+ *
10
+ * @see RFCS/0057-memory-write-attribution-event.md §C
11
+ * @see SECURITY/invariants.yaml — memory-attribution-no-content
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import { driver } from '../lib/driver.js';
16
+ import { pollUntilTerminal } from '../lib/polling.js';
17
+ import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
18
+
19
+ describe('memory-attribution-no-content (RFC 0057 §C)', () => {
20
+ it('memory.written payloads carry no entry content', async () => {
21
+ const cap = await readMemoryAttributionCap();
22
+ if (!emitsWriteEvents(cap)) return;
23
+ const runId = await seedRun('mem-attr-no-content');
24
+ if (!runId) return;
25
+ try {
26
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
27
+ } catch {
28
+ return;
29
+ }
30
+ const events = await memoryWrittenEvents(runId);
31
+ if (events.length === 0) return; // run wrote no memory — soft-skip
32
+ for (const e of events) {
33
+ const payload = e.payload ?? {};
34
+ expect(
35
+ 'content' in payload,
36
+ driver.describe('RFC 0057 §C', 'memory.written MUST NOT carry the entry content field'),
37
+ ).toBe(false);
38
+ expect(
39
+ typeof (payload as { memoryRef?: unknown }).memoryRef === 'string' &&
40
+ typeof (payload as { memoryId?: unknown }).memoryId === 'string',
41
+ driver.describe('RFC 0057 §B', 'memory.written MUST carry memoryRef + memoryId identifiers'),
42
+ ).toBe(true);
43
+ }
44
+ });
45
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * memory-attribution-replay-stable — RFC 0057 §D. `memory.written` is an
3
+ * immutable recorded fact: a `replay`-mode fork MUST NOT mint a new
4
+ * `memoryId` for a write the source run already recorded. This asserts the
5
+ * "MUST NOT regenerate" half — every `memory.written` on a replayed run
6
+ * reuses a `memoryId` the source run recorded (a compliant host that
7
+ * suppresses re-mint on replay satisfies this vacuously with zero events).
8
+ *
9
+ * Gated on `capabilities.memory.attribution.emitsWriteEvents`; soft-skips
10
+ * when unadvertised, when the seeded run wrote no memory, or when the host
11
+ * doesn't support `:fork` in `replay` mode.
12
+ *
13
+ * @see RFCS/0057-memory-write-attribution-event.md §D
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { driver } from '../lib/driver.js';
18
+ import { pollUntilTerminal } from '../lib/polling.js';
19
+ import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
20
+
21
+ function memoryIdOf(payload: Record<string, unknown> | undefined): string | null {
22
+ const id = (payload ?? {})['memoryId'];
23
+ return typeof id === 'string' ? id : null;
24
+ }
25
+
26
+ describe('memory-attribution-replay-stable (RFC 0057 §D)', () => {
27
+ it('a replay-mode fork introduces no memory.written with a new memoryId', async () => {
28
+ const cap = await readMemoryAttributionCap();
29
+ if (!emitsWriteEvents(cap)) return;
30
+ const runId = await seedRun('mem-attr-replay');
31
+ if (!runId) return;
32
+ try {
33
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
34
+ } catch {
35
+ return;
36
+ }
37
+ const original = await memoryWrittenEvents(runId);
38
+ if (original.length === 0) return; // run wrote no memory — nothing to test
39
+ const recordedIds = new Set(original.map((e) => memoryIdOf(e.payload)).filter((x): x is string => x !== null));
40
+
41
+ const fork = await driver.post(`/v1/runs/${runId}:fork`, { fromSeq: 0, mode: 'replay' });
42
+ if (fork.status !== 200 && fork.status !== 201) return; // replay fork unsupported — soft-skip
43
+ const forkId = (fork.json as { runId?: string } | undefined)?.runId;
44
+ if (!forkId) return;
45
+ try {
46
+ await pollUntilTerminal(forkId, { timeoutMs: 10_000 });
47
+ } catch {
48
+ /* still assert on whatever the replay emitted */
49
+ }
50
+
51
+ const replayed = await memoryWrittenEvents(forkId);
52
+ for (const e of replayed) {
53
+ const id = memoryIdOf(e.payload);
54
+ expect(
55
+ id !== null && recordedIds.has(id),
56
+ driver.describe('RFC 0057 §D', 'a replay MUST NOT regenerate memoryId — every replayed memory.written reuses a recorded id'),
57
+ ).toBe(true);
58
+ }
59
+ });
60
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * memory-attribution-shape — RFC 0057 §A. The `capabilities.memory.attribution`
3
+ * advertisement block is either absent or a well-formed object.
4
+ *
5
+ * Status: ACTIVE (advertisement-shape; always runs). Behavioral coverage lives
6
+ * in the sibling memory-attribution-*.test.ts scenarios, gated on
7
+ * `capabilities.memory.attribution.emitsWriteEvents`.
8
+ *
9
+ * @see RFCS/0057-memory-write-attribution-event.md §A
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { driver } from '../lib/driver.js';
14
+ import { readMemoryAttributionCap } from '../lib/memoryAttribution.js';
15
+
16
+ describe('memory-attribution-shape: advertisement (RFC 0057 §A)', () => {
17
+ it('capabilities.memory.attribution is absent or a well-formed object', async () => {
18
+ const cap = await readMemoryAttributionCap();
19
+ if (cap === null) return; // not advertised — valid
20
+ expect(
21
+ cap.supported,
22
+ driver.describe('capabilities.schema.json §memory.attribution', 'memory.attribution.supported MUST be the literal true when the block is present'),
23
+ ).toBe(true);
24
+ if (cap.emitsWriteEvents !== undefined) {
25
+ expect(typeof cap.emitsWriteEvents).toBe('boolean');
26
+ }
27
+ });
28
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * memory-attribution-tenant-scoped — RFC 0057 §C + SECURITY/invariants.yaml
3
+ * `memory-attribution-tenant-scoped`. A run's `memory.written` events appear
4
+ * only on that run's stream (mirrors CTI-1). The full cross-tenant proof
5
+ * (tenant B cannot read tenant A's run stream) needs a multi-tenant auth seam
6
+ * not standardized for this surface — that half soft-skips, mirroring
7
+ * `feedback-cross-tenant-isolation`.
8
+ *
9
+ * Gated on `capabilities.memory.attribution.emitsWriteEvents`.
10
+ *
11
+ * @see RFCS/0057-memory-write-attribution-event.md §C
12
+ * @see SECURITY/invariants.yaml — memory-attribution-tenant-scoped
13
+ */
14
+
15
+ import { describe, it, expect } from 'vitest';
16
+ import { driver } from '../lib/driver.js';
17
+ import { pollUntilTerminal } from '../lib/polling.js';
18
+ import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
19
+
20
+ describe('memory-attribution-tenant-scoped (RFC 0057 §C)', () => {
21
+ it("a run's memory.written events appear only on that run's stream", async () => {
22
+ const cap = await readMemoryAttributionCap();
23
+ if (!emitsWriteEvents(cap)) return;
24
+ const runId = await seedRun('mem-attr-cti');
25
+ if (!runId) return;
26
+ try {
27
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
28
+ } catch {
29
+ return;
30
+ }
31
+ const events = await memoryWrittenEvents(runId);
32
+ if (events.length === 0) return;
33
+ // Every memory.written we read came from THIS run's /events stream; if the
34
+ // host echoes a runId in the event it MUST be this run's (no cross-run leak).
35
+ for (const e of events) {
36
+ if (typeof e.runId === 'string') {
37
+ expect(
38
+ e.runId,
39
+ driver.describe('RFC 0057 §C', "a memory.written event MUST belong to its own run's stream (CTI-1)"),
40
+ ).toBe(runId);
41
+ }
42
+ }
43
+ });
44
+ });