@openwop/openwop-conformance 1.6.1 → 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 (159) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +57 -0
  4. package/api/openapi.yaml +250 -0
  5. package/coverage.md +14 -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 +10 -0
  10. package/schemas/agent-inventory-response.schema.json +90 -0
  11. package/schemas/ai-envelope.schema.json +28 -0
  12. package/schemas/artifact-type-pack-manifest.schema.json +160 -0
  13. package/schemas/capabilities.schema.json +171 -4
  14. package/schemas/chat-card-pack-manifest.schema.json +158 -0
  15. package/schemas/envelopes/media.audio.schema.json +38 -0
  16. package/schemas/envelopes/media.file.schema.json +37 -0
  17. package/schemas/envelopes/media.image.schema.json +33 -0
  18. package/schemas/heartbeat-evaluated.schema.json +14 -0
  19. package/schemas/heartbeat-state-changed.schema.json +14 -0
  20. package/schemas/node-pack-manifest.schema.json +16 -1
  21. package/schemas/run-event-payloads.schema.json +96 -5
  22. package/schemas/run-event.schema.json +4 -0
  23. package/schemas/workflow-definition.schema.json +5 -0
  24. package/schemas/workspace-file-create.schema.json +20 -0
  25. package/schemas/workspace-file.schema.json +39 -0
  26. package/src/lib/agentLoop.ts +44 -0
  27. package/src/lib/agentRuntime.ts +45 -0
  28. package/src/lib/artifactTypes.ts +96 -0
  29. package/src/lib/cardPacks.ts +52 -0
  30. package/src/lib/discovery-capabilities.ts +50 -0
  31. package/src/lib/distillation.ts +38 -0
  32. package/src/lib/feedback.ts +3 -3
  33. package/src/lib/heartbeat.ts +31 -0
  34. package/src/lib/memoryAttribution.ts +48 -0
  35. package/src/lib/subRunAttestation.ts +35 -0
  36. package/src/lib/toolHooks.ts +33 -0
  37. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
  38. package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
  39. package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
  40. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
  41. package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
  42. package/src/scenarios/ai-envelope-shape.test.ts +14 -18
  43. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
  44. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
  45. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
  46. package/src/scenarios/approval-gate-flow.test.ts +4 -6
  47. package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
  48. package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
  49. package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
  50. package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
  51. package/src/scenarios/audit-log-integrity.test.ts +3 -2
  52. package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
  53. package/src/scenarios/auth-mtls.test.ts +2 -1
  54. package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
  55. package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
  56. package/src/scenarios/auth-saml-profile.test.ts +2 -1
  57. package/src/scenarios/auth-scim-profile.test.ts +2 -1
  58. package/src/scenarios/authorization-fail-closed.test.ts +2 -1
  59. package/src/scenarios/authorization-roles-shape.test.ts +2 -1
  60. package/src/scenarios/byok-auth-modes.test.ts +141 -0
  61. package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
  62. package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
  63. package/src/scenarios/commitment-fired.test.ts +83 -0
  64. package/src/scenarios/credential-payload-redaction.test.ts +2 -1
  65. package/src/scenarios/credentials-capability-shape.test.ts +2 -1
  66. package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
  67. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
  68. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  69. package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
  70. package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
  71. package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
  72. package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
  73. package/src/scenarios/distillation-shape.test.ts +41 -0
  74. package/src/scenarios/distillation-stable-archive.test.ts +37 -0
  75. package/src/scenarios/distillation-token-budget.test.ts +45 -0
  76. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
  77. package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
  78. package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
  79. package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
  80. package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
  81. package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
  82. package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
  83. package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
  84. package/src/scenarios/experimental-tier-shape.test.ts +5 -4
  85. package/src/scenarios/fs-path-traversal.test.ts +2 -1
  86. package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
  87. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
  88. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
  89. package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
  90. package/src/scenarios/http-client-ssrf.test.ts +10 -13
  91. package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
  92. package/src/scenarios/media-url-inline-cap.test.ts +167 -0
  93. package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
  94. package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
  95. package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
  96. package/src/scenarios/memory-attribution-shape.test.ts +28 -0
  97. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
  98. package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
  99. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
  100. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
  101. package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
  102. package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
  103. package/src/scenarios/model-capability-substituted.test.ts +2 -1
  104. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  105. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
  106. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
  107. package/src/scenarios/multi-region-idempotency.test.ts +10 -10
  108. package/src/scenarios/oauth-capability-shape.test.ts +2 -1
  109. package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
  110. package/src/scenarios/pause-resume.test.ts +3 -3
  111. package/src/scenarios/production-backpressure.test.ts +2 -2
  112. package/src/scenarios/production-retention-expiry.test.ts +2 -2
  113. package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
  114. package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
  115. package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
  116. package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
  117. package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
  118. package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
  119. package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
  120. package/src/scenarios/prompt-pack-install.test.ts +2 -1
  121. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
  122. package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
  123. package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
  124. package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
  125. package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
  126. package/src/scenarios/prompt-template-shape.test.ts +2 -1
  127. package/src/scenarios/provider-usage.test.ts +2 -1
  128. package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
  129. package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
  130. package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
  131. package/src/scenarios/replayDeterminism.test.ts +3 -1
  132. package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
  133. package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
  134. package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
  135. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  136. package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
  137. package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
  138. package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
  139. package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
  140. package/src/scenarios/spec-corpus-validity.test.ts +1 -1
  141. package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
  142. package/src/scenarios/subrun-approval-gate.test.ts +35 -0
  143. package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
  144. package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
  145. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
  146. package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
  147. package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
  148. package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
  149. package/src/scenarios/tool-hooks-shape.test.ts +34 -0
  150. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
  151. package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
  152. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
  153. package/src/scenarios/wasm-pack-load.test.ts +2 -2
  154. package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
  155. package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
  156. package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
  157. package/src/scenarios/workspace-behavior.test.ts +134 -0
  158. package/src/scenarios/workspace-capability-shape.test.ts +73 -0
  159. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
@@ -16,14 +16,14 @@ import { describe, it, expect } from 'vitest';
16
16
  import { driver } from '../lib/driver.js';
17
17
  import { pollUntilTerminal } from '../lib/polling.js';
18
18
  import { isFixtureAdvertised } from '../lib/fixtures.js';
19
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
19
20
 
20
21
  const FIXTURE = 'conformance-wasm-pack-roundtrip';
21
22
 
22
23
  async function isWasmSupported(): Promise<boolean> {
23
24
  const disco = await driver.get('/.well-known/openwop');
24
25
  return Boolean(
25
- (disco.json as { capabilities?: { nodePackRuntimes?: { wasm?: { supported?: boolean } } } })
26
- .capabilities?.nodePackRuntimes?.wasm?.supported,
26
+ capabilityFamily<{ wasm?: { supported?: boolean } }>(disco.json, 'nodePackRuntimes')?.wasm?.supported,
27
27
  );
28
28
  }
29
29
 
@@ -20,14 +20,14 @@ import { describe, it, expect } from 'vitest';
20
20
  import { driver } from '../lib/driver.js';
21
21
  import { pollUntilTerminal } from '../lib/polling.js';
22
22
  import { isFixtureAdvertised } from '../lib/fixtures.js';
23
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
23
24
 
24
25
  const FIXTURE = 'conformance-wasm-pack-roundtrip';
25
26
 
26
27
  async function isWasmSupported(): Promise<boolean> {
27
28
  const disco = await driver.get('/.well-known/openwop');
28
29
  return Boolean(
29
- (disco.json as { capabilities?: { nodePackRuntimes?: { wasm?: { supported?: boolean } } } })
30
- .capabilities?.nodePackRuntimes?.wasm?.supported,
30
+ capabilityFamily<{ wasm?: { supported?: boolean } }>(disco.json, 'nodePackRuntimes')?.wasm?.supported,
31
31
  );
32
32
  }
33
33
 
@@ -15,6 +15,7 @@
15
15
  import { describe, it, expect } from 'vitest';
16
16
  import { driver } from '../lib/driver.js';
17
17
  import { isFixtureAdvertised } from '../lib/fixtures.js';
18
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
18
19
 
19
20
  const FIXTURE = 'conformance-wasm-pack-roundtrip';
20
21
 
@@ -28,8 +29,7 @@ interface WasmCaps {
28
29
  async function getWasmCaps(): Promise<WasmCaps | null> {
29
30
  const disco = await driver.get('/.well-known/openwop');
30
31
  const caps =
31
- (disco.json as { capabilities?: { nodePackRuntimes?: { wasm?: WasmCaps } } })
32
- .capabilities?.nodePackRuntimes?.wasm ?? null;
32
+ capabilityFamily<{ wasm?: WasmCaps }>(disco.json, 'nodePackRuntimes')?.wasm ?? null;
33
33
  return caps;
34
34
  }
35
35
 
@@ -26,6 +26,7 @@ import { describe, it, expect } from 'vitest';
26
26
  import { driver } from '../lib/driver.js';
27
27
  import { pollUntilTerminal } from '../lib/polling.js';
28
28
  import { isFixtureAdvertised } from '../lib/fixtures.js';
29
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
29
30
 
30
31
  const CAP_BREACH_FIXTURE = 'conformance-wasm-pack-memory-cap-breach';
31
32
 
@@ -33,9 +34,7 @@ describe('wasm-pack-memory-cap: host advertises maxMemoryBytes', () => {
33
34
  it('capabilities.nodePackRuntimes.wasm.maxMemoryBytes is a plausible number', async () => {
34
35
  const disco = await driver.get('/.well-known/openwop');
35
36
  const wasm =
36
- (disco.json as {
37
- capabilities?: { nodePackRuntimes?: { wasm?: { supported?: boolean; maxMemoryBytes?: unknown } } };
38
- }).capabilities?.nodePackRuntimes?.wasm;
37
+ capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
39
38
 
40
39
  if (!wasm?.supported) return;
41
40
 
@@ -64,9 +63,7 @@ describe('wasm-pack-memory-cap: positive path via misbehaving pack', () => {
64
63
  }
65
64
  const disco = await driver.get('/.well-known/openwop');
66
65
  const wasm =
67
- (disco.json as {
68
- capabilities?: { nodePackRuntimes?: { wasm?: { supported?: boolean } } };
69
- }).capabilities?.nodePackRuntimes?.wasm;
66
+ capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
70
67
  if (!wasm?.supported) return;
71
68
 
72
69
  const create = await driver.post('/v1/runs', {
@@ -14,14 +14,14 @@ import { describe, it, expect } from 'vitest';
14
14
  import { driver } from '../lib/driver.js';
15
15
  import { pollUntilTerminal } from '../lib/polling.js';
16
16
  import { isFixtureAdvertised } from '../lib/fixtures.js';
17
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
17
18
 
18
19
  const FIXTURE = 'conformance-wasm-pack-roundtrip';
19
20
 
20
21
  async function isWasmSupported(): Promise<boolean> {
21
22
  const disco = await driver.get('/.well-known/openwop');
22
23
  return Boolean(
23
- (disco.json as { capabilities?: { nodePackRuntimes?: { wasm?: { supported?: boolean } } } })
24
- .capabilities?.nodePackRuntimes?.wasm?.supported,
24
+ capabilityFamily<{ wasm?: { supported?: boolean } }>(disco.json, 'nodePackRuntimes')?.wasm?.supported,
25
25
  );
26
26
  }
27
27
 
@@ -0,0 +1,142 @@
1
+ /**
2
+ * workflow-primary-output-annotation — RFC 0065 schema shape conformance.
3
+ *
4
+ * Server-free schema assertions that the optional `outputRole` field on
5
+ * `WorkflowNode` is exactly that — optional, additive, and a closed enum:
6
+ * 1. A WorkflowDefinition with one node declaring `outputRole: "primary"`
7
+ * and another declaring `outputRole: "secondary"` validates.
8
+ * 2. A WorkflowDefinition with the field absent (legacy shape) still
9
+ * validates — preserves the additive promise.
10
+ * 3. An unknown `outputRole` value is rejected by the closed enum.
11
+ * 4. The field set to a non-string is rejected.
12
+ *
13
+ * Always runs (pure on-disk Ajv2020 validation; no host involvement —
14
+ * the field has no engine-observable effect by design).
15
+ *
16
+ * @see RFCS/0065-workflow-node-primary-output-annotation.md
17
+ * @see schemas/workflow-definition.schema.json ($defs.WorkflowNode.outputRole)
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import Ajv2020 from 'ajv/dist/2020.js';
22
+ import addFormats from 'ajv-formats';
23
+ import { readFileSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import { SCHEMAS_DIR } from '../lib/paths.js';
26
+
27
+ function compileWorkflowDefinition(): ReturnType<Ajv2020['compile']> {
28
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
29
+ addFormats(ajv);
30
+ // Register cross-file `$ref` targets — same pattern as
31
+ // `fixtures-valid.test.ts`. Without these, Ajv throws
32
+ // `missingRef` when compiling `workflow-definition.schema.json`
33
+ // because it references agent-ref + prompt-ref by URL.
34
+ const agentRefSchema = JSON.parse(
35
+ readFileSync(join(SCHEMAS_DIR, 'agent-ref.schema.json'), 'utf8'),
36
+ ) as Record<string, unknown>;
37
+ const promptRefSchema = JSON.parse(
38
+ readFileSync(join(SCHEMAS_DIR, 'prompt-ref.schema.json'), 'utf8'),
39
+ ) as Record<string, unknown>;
40
+ const promptKindSchema = JSON.parse(
41
+ readFileSync(join(SCHEMAS_DIR, 'prompt-kind.schema.json'), 'utf8'),
42
+ ) as Record<string, unknown>;
43
+ ajv.addSchema(agentRefSchema, 'agent-ref.schema.json');
44
+ ajv.addSchema(promptRefSchema, 'prompt-ref.schema.json');
45
+ ajv.addSchema(promptRefSchema, './prompt-ref.schema.json');
46
+ ajv.addSchema(promptKindSchema, 'prompt-kind.schema.json');
47
+ ajv.addSchema(promptKindSchema, './prompt-kind.schema.json');
48
+ const schema = JSON.parse(
49
+ readFileSync(join(SCHEMAS_DIR, 'workflow-definition.schema.json'), 'utf8'),
50
+ ) as Record<string, unknown>;
51
+ return ajv.compile(schema);
52
+ }
53
+
54
+ /** Build the minimal-required shape of a WorkflowDefinition. Tests
55
+ * inject per-case node overrides via the `nodes` arg. */
56
+ function baseDefinition(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
57
+ return {
58
+ id: 'wf-test',
59
+ name: 'Test',
60
+ version: '1.0.0',
61
+ nodes,
62
+ edges: [],
63
+ triggers: [],
64
+ variables: [],
65
+ metadata: { createdAt: '2026-05-25T00:00:00Z' },
66
+ settings: {},
67
+ };
68
+ }
69
+
70
+ function baseNode(id: string, extras: Record<string, unknown> = {}): Record<string, unknown> {
71
+ return {
72
+ id,
73
+ typeId: 'core.test.noop',
74
+ name: id,
75
+ position: { x: 0, y: 0 },
76
+ config: {},
77
+ inputs: {},
78
+ ...extras,
79
+ };
80
+ }
81
+
82
+ describe('workflow-primary-output-annotation: outputRole shape (RFC 0065)', () => {
83
+ const validate = compileWorkflowDefinition();
84
+
85
+ it('accepts a workflow with one node declaring outputRole="primary"', () => {
86
+ const def = baseDefinition([
87
+ baseNode('a', { outputRole: 'primary' }),
88
+ baseNode('b'),
89
+ ]);
90
+ const ok = validate(def);
91
+ expect(ok, JSON.stringify(validate.errors, null, 2)).toBe(true);
92
+ });
93
+
94
+ it('accepts primary AND secondary annotations on different nodes', () => {
95
+ const def = baseDefinition([
96
+ baseNode('a', { outputRole: 'primary' }),
97
+ baseNode('b', { outputRole: 'secondary' }),
98
+ baseNode('c'),
99
+ ]);
100
+ const ok = validate(def);
101
+ expect(ok, JSON.stringify(validate.errors, null, 2)).toBe(true);
102
+ });
103
+
104
+ it('accepts a workflow with the field absent (additive promise)', () => {
105
+ const def = baseDefinition([
106
+ baseNode('a'),
107
+ baseNode('b'),
108
+ ]);
109
+ const ok = validate(def);
110
+ expect(ok, JSON.stringify(validate.errors, null, 2)).toBe(true);
111
+ });
112
+
113
+ it('rejects an unknown outputRole enum value', () => {
114
+ const def = baseDefinition([
115
+ baseNode('a', { outputRole: 'tertiary' }),
116
+ ]);
117
+ const ok = validate(def);
118
+ expect(ok).toBe(false);
119
+ expect(validate.errors).toBeTruthy();
120
+ });
121
+
122
+ it('rejects outputRole set to a non-string', () => {
123
+ const def = baseDefinition([
124
+ baseNode('a', { outputRole: 1 }),
125
+ ]);
126
+ const ok = validate(def);
127
+ expect(ok).toBe(false);
128
+ });
129
+
130
+ it('permits multiple nodes declaring outputRole="primary" (tooling decides)', () => {
131
+ // The schema doesn't reject multiple primaries — tooling MAY pick
132
+ // any (lexicographic node id is the RFC's recommended tiebreaker).
133
+ // This test pins that the schema-layer doesn't enforce uniqueness,
134
+ // matching the RFC's "schema permits N primaries" promise.
135
+ const def = baseDefinition([
136
+ baseNode('a', { outputRole: 'primary' }),
137
+ baseNode('b', { outputRole: 'primary' }),
138
+ ]);
139
+ const ok = validate(def);
140
+ expect(ok, JSON.stringify(validate.errors, null, 2)).toBe(true);
141
+ });
142
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * workspace-behavior — RFC 0059 §C/§D behavioral verification for a host
3
+ * advertising `capabilities.workspace.supported: true`.
4
+ *
5
+ * Status: ACTIVE. Capability-gated: every block soft-skips when the host does
6
+ * not advertise the workspace store.
7
+ *
8
+ * What this scenario asserts:
9
+ * 1. §C CRUD round-trip — PUT create (version 1, etag), GET, list (metadata
10
+ * only, no `content`), DELETE, then GET → 404.
11
+ * 2. §C optimistic concurrency — a PUT with a stale `If-Match` MUST return
12
+ * `409 workspace_conflict` with `details.currentVersion`; a matching
13
+ * `If-Match` MUST bump `version`.
14
+ * 3. §C size ceiling — `content` beyond `maxFileBytes` MUST return
15
+ * `workspace_too_large`.
16
+ * 4. §D run snapshot — a run started after a write exposes the workspace
17
+ * read snapshot on its run snapshot.
18
+ *
19
+ * @see RFCS/0059-agent-workspace.md §C §D
20
+ * @see spec/v1/agent-workspace.md §"§C — Endpoints" / §"§D — Run-time exposure"
21
+ */
22
+
23
+ import { describe, it, expect } from 'vitest';
24
+ import { driver } from '../lib/driver.js';
25
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
26
+
27
+ interface DiscoveryWorkspace {
28
+ supported?: boolean;
29
+ maxFileBytes?: number;
30
+ }
31
+ interface DiscoveryDoc {
32
+ capabilities?: { workspace?: DiscoveryWorkspace };
33
+ }
34
+
35
+ async function workspaceCap(): Promise<DiscoveryWorkspace | null> {
36
+ const res = await driver.get('/.well-known/openwop');
37
+ const ws = capabilityFamily((res.json as DiscoveryDoc | undefined), 'workspace');
38
+ return ws?.supported === true ? ws : null;
39
+ }
40
+
41
+ const FILES = '/v1/host/workspace/files';
42
+
43
+ interface WorkspaceFile {
44
+ path: string;
45
+ content?: string;
46
+ version: number;
47
+ etag?: string;
48
+ }
49
+
50
+ describe('workspace-behavior: §C CRUD round-trip (RFC 0059)', () => {
51
+ it('PUT creates v1, GET returns it, list omits content, DELETE removes it', async () => {
52
+ if (!(await workspaceCap())) return;
53
+ const path = 'behavior/CRUD.md';
54
+
55
+ const created = await driver.put(`${FILES}/${encodeURIComponent(path)}`, { content: 'first body' });
56
+ expect(created.status, driver.describe('agent-workspace.md §C PUT', 'create MUST return 200')).toBe(200);
57
+ const file = created.json as WorkspaceFile;
58
+ expect(file.version, driver.describe('agent-workspace.md §File model', 'version MUST start at 1')).toBe(1);
59
+ expect(typeof file.etag, driver.describe('agent-workspace.md §File model', 'PUT MUST return an etag')).toBe('string');
60
+
61
+ const got = await driver.get(`${FILES}/${encodeURIComponent(path)}`);
62
+ expect((got.json as WorkspaceFile).content, driver.describe('agent-workspace.md §C GET', 'GET MUST return the content')).toBe('first body');
63
+
64
+ const list = await driver.get(FILES);
65
+ const rows = (list.json as { files?: WorkspaceFile[] }).files ?? [];
66
+ const row = rows.find((r) => r.path === path);
67
+ expect(row, driver.describe('agent-workspace.md §C list', 'list MUST include the created file')).toBeDefined();
68
+ expect('content' in (row as object), driver.describe('agent-workspace.md §C list', 'list MUST NOT include file bodies (metadata only)')).toBe(false);
69
+
70
+ const del = await driver.del(`${FILES}/${encodeURIComponent(path)}`);
71
+ expect(del.status, driver.describe('agent-workspace.md §C DELETE', 'DELETE MUST return 2xx')).toBeGreaterThanOrEqual(200);
72
+ expect(del.status, 'DELETE MUST be < 300').toBeLessThan(300);
73
+
74
+ const gone = await driver.get(`${FILES}/${encodeURIComponent(path)}`);
75
+ expect(gone.status, driver.describe('agent-workspace.md §C GET', 'GET after DELETE MUST be 404')).toBe(404);
76
+ });
77
+ });
78
+
79
+ describe('workspace-behavior: §C optimistic concurrency (RFC 0059)', () => {
80
+ it('stale If-Match → 409 workspace_conflict; matching If-Match bumps version', async () => {
81
+ if (!(await workspaceCap())) return;
82
+ const path = 'behavior/ETAG.md';
83
+
84
+ const v1 = await driver.put(`${FILES}/${encodeURIComponent(path)}`, { content: 'v1' });
85
+ const etag = (v1.json as WorkspaceFile).etag!;
86
+
87
+ const stale = await driver.put(`${FILES}/${encodeURIComponent(path)}`, { content: 'nope' }, { headers: { 'If-Match': '"definitely-stale"' } });
88
+ expect(stale.status, driver.describe('agent-workspace.md §C PUT', 'a stale If-Match MUST return 409 workspace_conflict')).toBe(409);
89
+ expect((stale.json as { error?: string }).error, driver.describe('rest-endpoints.md workspace_conflict', 'error code MUST be workspace_conflict')).toBe('workspace_conflict');
90
+ const details = (stale.json as { details?: { currentVersion?: number } }).details;
91
+ expect(typeof details?.currentVersion, driver.describe('agent-workspace.md §C PUT', '409 MUST carry details.currentVersion')).toBe('number');
92
+
93
+ const v2 = await driver.put(`${FILES}/${encodeURIComponent(path)}`, { content: 'v2' }, { headers: { 'If-Match': etag } });
94
+ expect(v2.status, 'a matching If-Match MUST succeed').toBe(200);
95
+ expect((v2.json as WorkspaceFile).version, driver.describe('agent-workspace.md §File model', 'a successful PUT MUST bump version')).toBe(2);
96
+
97
+ await driver.del(`${FILES}/${encodeURIComponent(path)}`);
98
+ });
99
+ });
100
+
101
+ describe('workspace-behavior: §C size ceiling (RFC 0059)', () => {
102
+ it('content beyond maxFileBytes returns workspace_too_large', async () => {
103
+ const cap = await workspaceCap();
104
+ if (cap === null) return;
105
+ const max = typeof cap.maxFileBytes === 'number' ? cap.maxFileBytes : 1_048_576;
106
+ const tooBig = 'x'.repeat(max + 1);
107
+ const res = await driver.put(`${FILES}/${encodeURIComponent('behavior/TOOBIG.md')}`, { content: tooBig });
108
+ expect(res.status, driver.describe('agent-workspace.md §C PUT', 'oversize content MUST be rejected (4xx)')).toBeGreaterThanOrEqual(400);
109
+ expect((res.json as { error?: string }).error, driver.describe('rest-endpoints.md workspace_too_large', 'error code MUST be workspace_too_large')).toBe('workspace_too_large');
110
+ });
111
+ });
112
+
113
+ describe('workspace-behavior: §D run-start snapshot (RFC 0059)', () => {
114
+ it('a run started after a write exposes the workspace snapshot', async () => {
115
+ if (!(await workspaceCap())) return;
116
+ const path = 'behavior/SNAPSHOT.md';
117
+ await driver.put(`${FILES}/${encodeURIComponent(path)}`, { content: 'snapshot body' });
118
+
119
+ const create = await driver.post('/v1/runs', { workflowId: 'conformance-noop' });
120
+ if (create.status !== 201 && create.status !== 200) return; // host lacks the noop fixture — skip
121
+ const runId = (create.json as { runId?: string }).runId;
122
+ if (runId === undefined) return;
123
+
124
+ const snap = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
125
+ const ws = (snap.json as { workspace?: Array<{ path: string }> }).workspace;
126
+ if (ws === undefined) return; // host doesn't expose the snapshot field — skip
127
+ expect(
128
+ ws.some((f) => f.path === path),
129
+ driver.describe('agent-workspace.md §D', 'the run snapshot MUST reflect a workspace file present at run start'),
130
+ ).toBe(true);
131
+
132
+ await driver.del(`${FILES}/${encodeURIComponent(path)}`);
133
+ });
134
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * workspace-capability-shape — RFC 0059 §A advertisement-shape verification.
3
+ *
4
+ * Status: ACTIVE (advertisement-shape; always runs). RFC 0059 (agent
5
+ * workspace) promoted Draft → Active 2026-05-25 — the `capabilities.workspace`
6
+ * block has landed in `schemas/capabilities.schema.json`.
7
+ *
8
+ * Always runs (shape-only): when the host advertises `capabilities.workspace`,
9
+ * its fields MUST be well-formed.
10
+ *
11
+ * What this scenario asserts:
12
+ * 1. `capabilities.workspace` is either absent or a well-formed object.
13
+ * 2. `supported` is a boolean when the block is present.
14
+ * 3. `maxFileBytes` / `maxFiles` / `maxVersions` are positive integers when present.
15
+ *
16
+ * Behavioral coverage (CRUD / ETag / cross-tenant isolation / run-snapshot)
17
+ * lands at the implementation milestone (RFC 0059 §E), capability-gated on
18
+ * `capabilities.workspace.supported`.
19
+ *
20
+ * @see RFCS/0059-agent-workspace.md §A
21
+ * @see spec/v1/agent-workspace.md §"Capability advertisement"
22
+ */
23
+
24
+ import { describe, it, expect } from 'vitest';
25
+ import { driver } from '../lib/driver.js';
26
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
27
+
28
+ interface DiscoveryWorkspace {
29
+ supported?: boolean;
30
+ versioned?: boolean;
31
+ maxFileBytes?: number;
32
+ maxFiles?: number;
33
+ maxVersions?: number;
34
+ }
35
+
36
+ interface DiscoveryDoc {
37
+ capabilities?: { workspace?: DiscoveryWorkspace };
38
+ }
39
+
40
+ async function readWorkspace(): Promise<DiscoveryWorkspace | null> {
41
+ const res = await driver.get('/.well-known/openwop');
42
+ const body = res.json as DiscoveryDoc | undefined;
43
+ return capabilityFamily(body, 'workspace') ?? null;
44
+ }
45
+
46
+ const POSITIVE_INT_FIELDS = ['maxFileBytes', 'maxFiles', 'maxVersions'] as const;
47
+
48
+ describe('workspace-capability-shape: advertisement shape (RFC 0059 §A)', () => {
49
+ it('capabilities.workspace is either absent or well-formed', async () => {
50
+ const ws = await readWorkspace();
51
+ if (ws === null) return; // host doesn't advertise workspace at all
52
+ expect(
53
+ typeof ws.supported,
54
+ driver.describe(
55
+ 'capabilities.schema.json §workspace',
56
+ 'capabilities.workspace.supported MUST be a boolean when workspace is advertised',
57
+ ),
58
+ ).toBe('boolean');
59
+ });
60
+
61
+ it('maxFileBytes / maxFiles / maxVersions are positive integers when present', async () => {
62
+ const ws = await readWorkspace();
63
+ if (ws === null) return;
64
+ for (const field of POSITIVE_INT_FIELDS) {
65
+ const v = ws[field];
66
+ if (v === undefined) continue;
67
+ expect(
68
+ Number.isInteger(v) && v >= 1,
69
+ driver.describe('RFC 0059 §A', `capabilities.workspace.${field} MUST be an integer >= 1, got: ${v}`),
70
+ ).toBe(true);
71
+ }
72
+ });
73
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * workspace-cross-tenant-isolation — RFC 0059 §E WCT-1.
3
+ *
4
+ * Status: ACTIVE (advertisement + behavioral). Public test for the
5
+ * `workspace-cross-tenant-isolation` SECURITY invariant: a workspace file
6
+ * owned by `{tenant, workspace}` MUST NOT be readable (get or list) under a
7
+ * different `{tenant′, workspace′}`, regardless of the caller's permissions
8
+ * elsewhere. Mirrors `kv-cross-tenant-isolation` / `agent-memory-cti-1`.
9
+ *
10
+ * The two owners are driven through the documented test seam
11
+ * `POST /v1/host/sample/workspace/op` (host-sample-test-seams.md §9), which
12
+ * lets a single-credential host exercise distinct owners. Hosts without the
13
+ * seam soft-skip the behavioral probe; the advertisement-shape assertion still
14
+ * runs whenever `capabilities.workspace.supported` is advertised.
15
+ *
16
+ * @see RFCS/0059-agent-workspace.md §E WCT-1
17
+ * @see spec/v1/agent-workspace.md §"§E — Invariants"
18
+ * @see SECURITY/invariants.yaml workspace-cross-tenant-isolation
19
+ */
20
+
21
+ import { describe, it, expect } from 'vitest';
22
+ import { driver } from '../lib/driver.js';
23
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
24
+
25
+ interface DiscoveryDoc {
26
+ capabilities?: { workspace?: { supported?: boolean } };
27
+ }
28
+
29
+ async function workspaceSupported(): Promise<boolean> {
30
+ const res = await driver.get('/.well-known/openwop');
31
+ const body = res.json as DiscoveryDoc | undefined;
32
+ return capabilityFamily(body, 'workspace')?.supported === true;
33
+ }
34
+
35
+ function seam(args: Record<string, unknown>) {
36
+ return driver.post('/v1/host/sample/workspace/op', args);
37
+ }
38
+
39
+ const SEAM_PATH = 'WCT1-SECRET.md';
40
+
41
+ describe('workspace-cross-tenant-isolation: a workspace file MUST NOT leak across owners (RFC 0059 §E WCT-1)', () => {
42
+ it('a file written under {tenant A, workspace A} is not readable under a different owner', async () => {
43
+ if (!(await workspaceSupported())) return; // capability not advertised — skip
44
+
45
+ // Owner A writes a file.
46
+ const put = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-a', op: 'put', path: SEAM_PATH, content: 'A-only secret body' });
47
+ if (put.status === 404) return; // seam unwired — soft-skip the behavioral probe
48
+ expect(put.status, driver.describe('agent-workspace.md §C PUT', 'seam put MUST succeed for the owning workspace')).toBe(200);
49
+
50
+ // A DIFFERENT workspace (same tenant) MUST NOT read it.
51
+ const crossWs = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-b', op: 'get', path: SEAM_PATH });
52
+ expect(
53
+ crossWs.status === 404 || crossWs.status === 403,
54
+ driver.describe('agent-workspace.md §E WCT-1', `a cross-workspace get MUST fail closed (404/403, no existence leak), got ${crossWs.status}`),
55
+ ).toBe(true);
56
+ const crossWsBody = JSON.stringify(crossWs.json ?? '');
57
+ expect(
58
+ !crossWsBody.includes('A-only secret body'),
59
+ driver.describe('agent-workspace.md §E WCT-1', 'a cross-workspace read MUST NOT surface the other owner\'s content'),
60
+ ).toBe(true);
61
+
62
+ // A DIFFERENT tenant MUST NOT read it either.
63
+ const crossTenant = await seam({ tenant: 'wct1-tenant-b', workspace: 'ws-a', op: 'get', path: SEAM_PATH });
64
+ expect(
65
+ crossTenant.status === 404 || crossTenant.status === 403,
66
+ driver.describe('agent-workspace.md §E WCT-1', `a cross-tenant get MUST fail closed, got ${crossTenant.status}`),
67
+ ).toBe(true);
68
+
69
+ // And list MUST NOT enumerate the other owner's path.
70
+ const crossList = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-b', op: 'list' });
71
+ const listed = JSON.stringify((crossList.json as { files?: unknown })?.files ?? []);
72
+ expect(
73
+ !listed.includes(SEAM_PATH),
74
+ driver.describe('agent-workspace.md §E WCT-1', 'a cross-workspace list MUST NOT enumerate another owner\'s file'),
75
+ ).toBe(true);
76
+
77
+ // Sanity: the owner itself still reads its file (isolation, not loss).
78
+ const ownerRead = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-a', op: 'get', path: SEAM_PATH });
79
+ expect(
80
+ (ownerRead.json as { content?: string } | undefined)?.content,
81
+ driver.describe('agent-workspace.md §C GET', 'the owning workspace MUST still read its own file'),
82
+ ).toBe('A-only secret body');
83
+ });
84
+ });