@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,167 @@
1
+ /**
2
+ * Agent evaluation — suite + summary + event shapes (RFC 0081).
3
+ *
4
+ * Always-on, server-free schema-shape probe. Verifies that:
5
+ * - `capabilities.agents.evalSuite` is declared with its `supported` / `modes`
6
+ * sub-flags.
7
+ * - the `AgentEvalSuite` + `EvalSummary` schemas compile and round-trip a
8
+ * conforming artifact, and reject malformed ones (a bad `suiteId`; a
9
+ * `thresholds.passScore` out of 0..1).
10
+ * - the `eval.started` / `eval.scored` / `eval.completed` payload $defs
11
+ * validate conforming content-free payloads and reject malformed ones.
12
+ * - both the summary and the per-task `eval.scored` payload are CONTENT-FREE:
13
+ * an `EvalSummary` carrying a task-output body and a `safetyFinding` carrying
14
+ * an excerpt are rejected. This is the public test for the protocol-tier
15
+ * SECURITY invariant `eval-summary-no-content-leak`.
16
+ * - all three event names appear in the RunEventType enum.
17
+ *
18
+ * Behavioral assertions (the eval-run event ordering, per-task scoring, the
19
+ * EvalSummary round-trip against a live host, the `mode: "eval"` 501 on
20
+ * unadvertised hosts) are gated on `capabilities.agents.evalSuite.supported` and
21
+ * land in `agent-eval-run.test.ts` (deferred per RFC 0081 §Conformance — reference
22
+ * host deferred). This scenario asserts the wire contract, not host behavior.
23
+ *
24
+ * Spec references:
25
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-evaluation.md
26
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0081-agent-evaluation-and-scorecards.md
27
+ * - https://github.com/openwop/openwop/blob/main/SECURITY/invariants.yaml (eval-summary-no-content-leak)
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { readFileSync } from 'node:fs';
32
+ import { join } from 'node:path';
33
+ import Ajv2020 from 'ajv/dist/2020.js';
34
+ import addFormats from 'ajv-formats';
35
+ import { SCHEMAS_DIR } from '../lib/paths.js';
36
+
37
+ /** Server-free assertion-message helper (mirrors driver.describe's "spec — requirement" shape without requiring OPENWOP_BASE_URL). */
38
+ const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
39
+
40
+ function loadSchema(name: string): Record<string, unknown> {
41
+ return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
42
+ }
43
+
44
+ describe('agent-eval-suite-shape: capability advertisement (RFC 0081, server-free)', () => {
45
+ it('the capabilities schema declares agents.evalSuite with its sub-flags', () => {
46
+ const caps = loadSchema('capabilities.schema.json');
47
+ const agents = (caps.properties as Record<string, { properties?: Record<string, { properties?: Record<string, unknown> }> }>).agents;
48
+ const evalSuite = agents?.properties?.evalSuite;
49
+ expect(
50
+ evalSuite,
51
+ why('capabilities.md §agents', 'agents.evalSuite MUST be declared'),
52
+ ).toBeDefined();
53
+ for (const flag of ['supported', 'modes']) {
54
+ expect(
55
+ evalSuite?.properties?.[flag],
56
+ why('agent-evaluation.md §Capability advertisement', `agents.evalSuite.${flag} MUST be declared`),
57
+ ).toBeDefined();
58
+ }
59
+ });
60
+ });
61
+
62
+ describe('agent-eval-suite-shape: AgentEvalSuite + EvalSummary schemas (RFC 0081, server-free)', () => {
63
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
64
+ addFormats(ajv);
65
+ const suite = ajv.compile(loadSchema('agent-eval-suite.schema.json'));
66
+ const summary = ajv.compile(loadSchema('eval-summary.schema.json'));
67
+
68
+ it('AgentEvalSuite validates a conforming suite and rejects a malformed suiteId / out-of-range threshold', () => {
69
+ const good = {
70
+ suiteId: 'core.openwop.evals.support-resolver',
71
+ version: '1.0.0',
72
+ modes: ['golden', 'regression'],
73
+ thresholds: { passScore: 0.8 },
74
+ tasks: [
75
+ { taskId: 'refund-window', input: { q: 'refund policy?' }, expected: { kind: 'golden', match: { strategy: 'contains', value: '30 days' } } },
76
+ ],
77
+ };
78
+ expect(suite(good), why('RFC 0081 §A', 'a conforming AgentEvalSuite MUST validate')).toBe(true);
79
+ // Negative: suiteId must carry the `.evals.` infix.
80
+ expect(suite({ ...good, suiteId: 'core.openwop.support-resolver' }), why('RFC 0081 §A', 'a suiteId without the `.evals.` infix MUST be rejected')).toBe(false);
81
+ // Negative: passScore out of 0..1.
82
+ expect(suite({ ...good, thresholds: { passScore: 1.5 } }), why('RFC 0081 §A', 'thresholds.passScore > 1 MUST be rejected')).toBe(false);
83
+ });
84
+
85
+ it('EvalSummary validates a conforming scorecard and rejects an out-of-range score', () => {
86
+ const good = {
87
+ suiteId: 'core.openwop.evals.support-resolver',
88
+ suiteVersion: '1.0.0',
89
+ aggregateScore: 0.86,
90
+ passed: true,
91
+ taskCount: 2,
92
+ passedCount: 2,
93
+ tasks: [{ taskId: 'refund-window', score: 0.9, passed: true, safetyFindings: [{ kind: 'jailbreak', severity: 'low' }] }],
94
+ };
95
+ expect(summary(good), why('RFC 0081 §C', 'a conforming EvalSummary MUST validate')).toBe(true);
96
+ expect(summary({ ...good, aggregateScore: 1.4 }), why('RFC 0081 §C', 'aggregateScore > 1 MUST be rejected')).toBe(false);
97
+ });
98
+
99
+ it('EvalSummary is content-free — a task-output body and a safety-finding excerpt are rejected (eval-summary-no-content-leak)', () => {
100
+ const base = { suiteId: 'core.openwop.evals.x', suiteVersion: '1.0.0', aggregateScore: 0.5, passed: false, taskCount: 1, passedCount: 0 };
101
+ // Negative: a per-task entry carrying the output body.
102
+ expect(
103
+ summary({ ...base, tasks: [{ taskId: 't1', score: 0.5, passed: false, taskOutput: 'the model said …' }] }),
104
+ why('SECURITY invariant eval-summary-no-content-leak', 'an EvalSummary task entry MUST NOT carry an output body'),
105
+ ).toBe(false);
106
+ // Negative: a safety finding carrying excerpted content rather than a {kind, severity} descriptor.
107
+ expect(
108
+ summary({ ...base, tasks: [{ taskId: 't1', score: 0.5, passed: false, safetyFindings: [{ kind: 'pii-leak', severity: 'high', excerpt: 'SSN 123-45-6789' }] }] }),
109
+ why('SECURITY invariant eval-summary-no-content-leak', 'a safetyFinding MUST NOT carry excerpted content'),
110
+ ).toBe(false);
111
+ });
112
+ });
113
+
114
+ describe('agent-eval-suite-shape: eval event payloads (RFC 0081, server-free)', () => {
115
+ const payloads = loadSchema('run-event-payloads.schema.json');
116
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
117
+ addFormats(ajv);
118
+ ajv.addSchema(payloads, 'payloads');
119
+
120
+ const started = ajv.getSchema('payloads#/$defs/evalStarted');
121
+ const scored = ajv.getSchema('payloads#/$defs/evalScored');
122
+ const completed = ajv.getSchema('payloads#/$defs/evalCompleted');
123
+
124
+ it('eval.started validates a content-free start record and requires the suite provenance', () => {
125
+ expect(started, 'the evalStarted $def MUST exist').toBeTruthy();
126
+ expect(
127
+ started!({ suiteId: 'core.openwop.evals.support-resolver', suiteVersion: '1.0.0', taskCount: 12, modes: ['golden'] }),
128
+ why('RFC 0081 §C', 'a conforming eval.started payload MUST validate'),
129
+ ).toBe(true);
130
+ expect(
131
+ started!({ suiteId: 'core.openwop.evals.x' }),
132
+ why('RFC 0081 §C', 'eval.started without suiteVersion/taskCount/modes MUST be rejected'),
133
+ ).toBe(false);
134
+ });
135
+
136
+ it('eval.scored validates a content-free per-task score and requires score + passed', () => {
137
+ expect(scored, 'the evalScored $def MUST exist').toBeTruthy();
138
+ expect(
139
+ scored!({ taskId: 'refund-window', score: 0.9, passed: true, costUsd: 0.012 }),
140
+ why('RFC 0081 §C', 'a conforming eval.scored payload MUST validate'),
141
+ ).toBe(true);
142
+ expect(
143
+ scored!({ taskId: 'refund-window' }),
144
+ why('RFC 0081 §C', 'eval.scored without score/passed MUST be rejected'),
145
+ ).toBe(false);
146
+ });
147
+
148
+ it('eval.completed validates a content-free aggregate record', () => {
149
+ expect(completed, 'the evalCompleted $def MUST exist').toBeTruthy();
150
+ expect(
151
+ completed!({ aggregateScore: 0.86, passed: true, taskCount: 12, passedCount: 11, regressionVsBaseline: 0.04 }),
152
+ why('RFC 0081 §C', 'a conforming eval.completed payload MUST validate'),
153
+ ).toBe(true);
154
+ expect(
155
+ completed!({ aggregateScore: 2 }),
156
+ why('RFC 0081 §C', 'eval.completed with an out-of-range aggregateScore MUST be rejected'),
157
+ ).toBe(false);
158
+ });
159
+
160
+ it('all three eval event names appear in the RunEventType enum', () => {
161
+ const runEvent = loadSchema('run-event.schema.json');
162
+ const enumVals = (runEvent.$defs as Record<string, { enum?: string[] }>).RunEventType?.enum ?? [];
163
+ expect(enumVals).toContain('eval.started');
164
+ expect(enumVals).toContain('eval.scored');
165
+ expect(enumVals).toContain('eval.completed');
166
+ });
167
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Live manifest-dispatch tool-allowlist enforcement (RFC 0077 §F-1) —
3
+ * behavioral.
4
+ *
5
+ * Gated on `capabilities.agents.liveRuntime.supported` (root-first per RFC 0073).
6
+ * Soft-skips when unadvertised (default) / hard-fails under
7
+ * `OPENWOP_REQUIRE_BEHAVIOR=true`.
8
+ *
9
+ * Asserts the §F-1 safety carry-forward: a live invocation MUST NOT call a tool
10
+ * outside the agent's `toolAllowlist` (the per-tool application of the RFC 0002
11
+ * §A14 mandatory-allowlist floor). Driven by the `attemptTool` seam param naming
12
+ * a disallowed tool; the invocation MUST NOT emit an `agent.toolCalled` for it
13
+ * (a refused/failed outcome is acceptable, a silent successful call is not).
14
+ * Soft-skips when the seam/hook is unwired.
15
+ *
16
+ * Spec references:
17
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/multi-agent-execution.md (§"Live manifest dispatch")
18
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0077-agent-run-lifecycle-and-live-manifest-dispatch.md (§F-1)
19
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0002-agent-identity-and-handoff.md (§A14 toolAllowlist)
20
+ */
21
+
22
+ import { describe, it, expect } from 'vitest';
23
+ import { driver } from '../lib/driver.js';
24
+ import { behaviorGate } from '../lib/behavior-gate.js';
25
+ import { readLiveRuntimeCap, invokeLive } from '../lib/liveRuntime.js';
26
+ import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
27
+
28
+ const DISALLOWED_TOOL = 'conformance-disallowed-tool';
29
+
30
+ describe('agent-live-allowlist-enforced (RFC 0077 §F-1)', () => {
31
+ it('does not call a tool outside the agent toolAllowlist', async () => {
32
+ const cap = await readLiveRuntimeCap();
33
+ if (!behaviorGate('openwop-live-allowlist-enforced', cap?.supported === true)) return;
34
+
35
+ if (!(await isEventLogSeamAvailable())) return; // soft-skip
36
+ const res = await invokeLive({ source: 'run-api', attemptTool: DISALLOWED_TOOL });
37
+ if (res === null || !res.runId) return; // seam/hook absent — soft-skip
38
+
39
+ const q = await queryTestEvents(res.runId, { type: 'agent.toolCalled' });
40
+ if (!q.ok) return;
41
+
42
+ const calledDisallowed = q.events.some((e) => {
43
+ const tool = e.payload.tool ?? e.payload.toolId ?? e.payload.name;
44
+ return tool === DISALLOWED_TOOL;
45
+ });
46
+ expect(
47
+ calledDisallowed === false,
48
+ driver.describe('RFC 0077 §F-1 / RFC 0002 §A14', 'a live invocation MUST NOT call a tool outside the agent toolAllowlist'),
49
+ ).toBe(true);
50
+
51
+ await resetTestSeam();
52
+ });
53
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Live manifest-dispatch invocation bracket (RFC 0077 §E) — behavioral.
3
+ *
4
+ * Gated on `capabilities.agents.liveRuntime.supported` (root-first per RFC 0073).
5
+ * Soft-skips when unadvertised (default) / hard-fails under
6
+ * `OPENWOP_REQUIRE_BEHAVIOR=true`. The always-on wire-shape coverage lives in
7
+ * `agent-live-runtime-shape.test.ts`; this asserts host BEHAVIOR: a live
8
+ * invocation brackets its `agent.*` family with
9
+ * `agent.invocation.started` (FIRST agent-scoped event) and
10
+ * `agent.invocation.completed` (LAST), with a matching `invocationId`, a
11
+ * `source` in the enum, an `outcome` in the enum, and both events content-free
12
+ * (no prompt/result body).
13
+ *
14
+ * Drives the OPTIONAL `POST /v1/host/sample/agents/live-invoke` seam + reads the
15
+ * bracket back via the test event-log seam (both deferred per RFC 0077
16
+ * §Conformance — soft-skip on 404).
17
+ *
18
+ * Spec references:
19
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/multi-agent-execution.md (§"Live manifest dispatch")
20
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0077-agent-run-lifecycle-and-live-manifest-dispatch.md
21
+ */
22
+
23
+ import { describe, it, expect } from 'vitest';
24
+ import { driver } from '../lib/driver.js';
25
+ import { behaviorGate } from '../lib/behavior-gate.js';
26
+ import { readLiveRuntimeCap, invokeLive } from '../lib/liveRuntime.js';
27
+ import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
28
+
29
+ const SOURCES = ['workflow-node', 'run-api', 'chat-mention'];
30
+ const OUTCOMES = ['completed', 'handed-off', 'escalated', 'refused', 'failed'];
31
+ const AGENT_SCOPED = (t: string): boolean => t === 'agent.invocation.started' || t === 'agent.invocation.completed' || t.startsWith('agent.');
32
+
33
+ describe('agent-live-invocation-bracket (RFC 0077 §E)', () => {
34
+ it('brackets a live invocation with started-first / completed-last + matching invocationId, content-free', async () => {
35
+ const cap = await readLiveRuntimeCap();
36
+ if (!behaviorGate('openwop-live-invocation-bracket', cap?.supported === true)) return;
37
+
38
+ if (!(await isEventLogSeamAvailable())) return; // event-log seam absent — soft-skip
39
+ const res = await invokeLive({ source: 'run-api' });
40
+ if (res === null || !res.runId) return; // live-invoke seam absent — soft-skip
41
+
42
+ const q = await queryTestEvents(res.runId);
43
+ if (!q.ok) return;
44
+ const events = q.events.slice().sort((a, b) => a.sequence - b.sequence);
45
+
46
+ const started = events.filter((e) => e.type === 'agent.invocation.started');
47
+ const completed = events.filter((e) => e.type === 'agent.invocation.completed');
48
+ expect(
49
+ started.length >= 1 && completed.length >= 1,
50
+ driver.describe('multi-agent-execution.md §"Live manifest dispatch"', 'a live invocation MUST emit agent.invocation.started + agent.invocation.completed'),
51
+ ).toBe(true);
52
+ if (started.length === 0 || completed.length === 0) return;
53
+
54
+ const start = started[0]!;
55
+ const end = completed[completed.length - 1]!;
56
+
57
+ // §E ordering: started is the FIRST agent-scoped event, completed the LAST.
58
+ const agentScoped = events.filter((e) => AGENT_SCOPED(e.type));
59
+ expect(
60
+ agentScoped[0]?.type === 'agent.invocation.started',
61
+ driver.describe('RFC 0077 §E', 'agent.invocation.started MUST be the first agent-scoped event of the invocation'),
62
+ ).toBe(true);
63
+ expect(
64
+ agentScoped[agentScoped.length - 1]?.type === 'agent.invocation.completed',
65
+ driver.describe('RFC 0077 §E', 'agent.invocation.completed MUST be the last agent-scoped event of the invocation'),
66
+ ).toBe(true);
67
+
68
+ // Matching invocationId across the bracket.
69
+ const startId = start.payload.invocationId;
70
+ const endId = end.payload.invocationId;
71
+ expect(
72
+ typeof startId === 'string' && startId === endId,
73
+ driver.describe('run-event-payloads.schema.json#agentInvocation*', 'the bracket MUST share one invocationId'),
74
+ ).toBe(true);
75
+
76
+ // Enum discipline.
77
+ expect(
78
+ typeof start.payload.source === 'string' && SOURCES.includes(start.payload.source as string),
79
+ driver.describe('run-event-payloads.schema.json#agentInvocationStarted', 'source MUST be workflow-node|run-api|chat-mention'),
80
+ ).toBe(true);
81
+ expect(
82
+ typeof end.payload.outcome === 'string' && OUTCOMES.includes(end.payload.outcome as string),
83
+ driver.describe('run-event-payloads.schema.json#agentInvocationCompleted', 'outcome MUST be in the closed enum'),
84
+ ).toBe(true);
85
+
86
+ // Content-free: identifiers + metadata only, never prompt/result body.
87
+ for (const evt of [start, end]) {
88
+ for (const forbidden of ['prompt', 'result', 'body', 'input', 'output', 'apiKey', 'secret', 'credentials', 'token']) {
89
+ expect(
90
+ !(forbidden in evt.payload),
91
+ driver.describe('RFC 0077', `agent.invocation.* MUST be content-free (no ${forbidden})`),
92
+ ).toBe(true);
93
+ }
94
+ }
95
+
96
+ await resetTestSeam();
97
+ });
98
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Live manifest dispatch — capability + invocation-event shapes (RFC 0077).
3
+ *
4
+ * Always-on, server-free schema-shape probe. Verifies that:
5
+ * - `capabilities.agents.liveRuntime` is declared on the capabilities schema
6
+ * (with the `supported` / `structuredOutput` / `confidenceEscalation` /
7
+ * `sources` sub-flags).
8
+ * - the `agent.invocation.started` + `agent.invocation.completed` payload
9
+ * $defs validate conforming content-free payloads and reject malformed
10
+ * ones (a `started` missing `source`; a `completed` with an out-of-enum
11
+ * `outcome`).
12
+ * - both event names appear in the RunEventType enum.
13
+ *
14
+ * Behavioral assertions (the started→completed bracket ordering, structured-
15
+ * output enforcement, toolAllowlist enforcement) are gated on
16
+ * `capabilities.agents.liveRuntime.supported` and soft-skip until a reference
17
+ * host wires the live-invoke seam (RFC 0077 §Conformance — reference host
18
+ * deferred). This scenario asserts the wire contract, not host behavior.
19
+ *
20
+ * Spec references:
21
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/multi-agent-execution.md §"Live manifest dispatch"
22
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0077-agent-run-lifecycle-and-live-manifest-dispatch.md
23
+ */
24
+
25
+ import { describe, it, expect } from 'vitest';
26
+ import { readFileSync } from 'node:fs';
27
+ import { join } from 'node:path';
28
+ import Ajv2020 from 'ajv/dist/2020.js';
29
+ import addFormats from 'ajv-formats';
30
+ import { SCHEMAS_DIR } from '../lib/paths.js';
31
+
32
+ /** Server-free assertion-message helper (mirrors driver.describe's "spec — requirement" shape without requiring OPENWOP_BASE_URL). */
33
+ const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
34
+
35
+ function loadSchema(name: string): Record<string, unknown> {
36
+ return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
37
+ }
38
+
39
+ describe('agent-live-runtime-shape: capability advertisement (RFC 0077, server-free)', () => {
40
+ it('the capabilities schema declares agents.liveRuntime with its sub-flags', () => {
41
+ const caps = loadSchema('capabilities.schema.json');
42
+ const agents = (caps.properties as Record<string, { properties?: Record<string, { properties?: Record<string, unknown> }> }>).agents;
43
+ const live = agents?.properties?.liveRuntime;
44
+ expect(
45
+ live,
46
+ why('capabilities.md §agents', 'agents.liveRuntime MUST be declared'),
47
+ ).toBeDefined();
48
+ for (const flag of ['supported', 'structuredOutput', 'confidenceEscalation', 'sources']) {
49
+ expect(
50
+ live?.properties?.[flag],
51
+ why('multi-agent-execution.md §Live manifest dispatch', `agents.liveRuntime.${flag} MUST be declared`),
52
+ ).toBeDefined();
53
+ }
54
+ });
55
+ });
56
+
57
+ describe('agent-live-runtime-shape: invocation event payloads (RFC 0077, server-free)', () => {
58
+ const payloads = loadSchema('run-event-payloads.schema.json');
59
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
60
+ addFormats(ajv);
61
+ ajv.addSchema(payloads, 'payloads');
62
+
63
+ const started = ajv.getSchema('payloads#/$defs/agentInvocationStarted');
64
+ const completed = ajv.getSchema('payloads#/$defs/agentInvocationCompleted');
65
+
66
+ it('agent.invocation.started validates a content-free start record and requires source', () => {
67
+ expect(started, 'the agentInvocationStarted $def MUST exist').toBeTruthy();
68
+ expect(
69
+ started!({ invocationId: 'inv-1', agentId: 'vendor.acme.review.code-reviewer', source: 'run-api', modelClass: 'coding', toolSurfaceCount: 3, memoryBound: false }),
70
+ why('RFC 0077 §C', 'a conforming agent.invocation.started payload MUST validate'),
71
+ ).toBe(true);
72
+ // Negative: missing source — every invocation must record its entry point.
73
+ expect(
74
+ started!({ invocationId: 'inv-1', agentId: 'vendor.acme.review.code-reviewer' }),
75
+ why('RFC 0077 §C', 'agent.invocation.started without source MUST be rejected'),
76
+ ).toBe(false);
77
+ });
78
+
79
+ it('agent.invocation.completed validates a content-free outcome record and pins the outcome enum', () => {
80
+ expect(completed, 'the agentInvocationCompleted $def MUST exist').toBeTruthy();
81
+ expect(
82
+ completed!({ invocationId: 'inv-1', agentId: 'vendor.acme.review.code-reviewer', outcome: 'completed', schemaValidated: true, confidence: 0.91 }),
83
+ why('RFC 0077 §C', 'a conforming agent.invocation.completed payload MUST validate'),
84
+ ).toBe(true);
85
+ // Negative: out-of-enum outcome — the canonical value is `completed`, not `done`.
86
+ expect(
87
+ completed!({ invocationId: 'inv-1', agentId: 'a', outcome: 'done' }),
88
+ why('RFC 0077 §C', 'agent.invocation.completed with an out-of-enum outcome MUST be rejected'),
89
+ ).toBe(false);
90
+ });
91
+
92
+ it('both invocation event names appear in the RunEventType enum', () => {
93
+ const runEvent = loadSchema('run-event.schema.json');
94
+ const enumVals = (runEvent.$defs as Record<string, { enum?: string[] }>).RunEventType?.enum ?? [];
95
+ expect(enumVals).toContain('agent.invocation.started');
96
+ expect(enumVals).toContain('agent.invocation.completed');
97
+ });
98
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Live manifest-dispatch structured-output enforcement (RFC 0077 §B step 6) —
3
+ * behavioral.
4
+ *
5
+ * Gated on `capabilities.agents.liveRuntime.structuredOutput` (root-first per
6
+ * RFC 0073) — itself meaningful only alongside `liveRuntime.supported`.
7
+ * Soft-skips when unadvertised (default) / hard-fails under
8
+ * `OPENWOP_REQUIRE_BEHAVIOR=true`.
9
+ *
10
+ * Asserts the §B step-6 MUST: when the host advertises `structuredOutput` and an
11
+ * agent declares a `handoff.returnSchemaRef`, a terminal result that VIOLATES
12
+ * that schema MUST fail the invocation (`agent.invocation.completed.outcome ===
13
+ * "failed"`, `schemaValidated !== true`) rather than ship a non-conforming
14
+ * result as `completed`. Driven by the `forceInvalidResult` seam param so the
15
+ * assertion is deterministic; soft-skips when the seam/hook is unwired.
16
+ *
17
+ * Spec references:
18
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/multi-agent-execution.md (§"Live manifest dispatch")
19
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0077-agent-run-lifecycle-and-live-manifest-dispatch.md (§B step 6)
20
+ */
21
+
22
+ import { describe, it, expect } from 'vitest';
23
+ import { driver } from '../lib/driver.js';
24
+ import { behaviorGate } from '../lib/behavior-gate.js';
25
+ import { readLiveRuntimeCap, invokeLive } from '../lib/liveRuntime.js';
26
+ import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
27
+
28
+ describe('agent-live-structured-output (RFC 0077 §B step 6)', () => {
29
+ it('fails the invocation on a result that violates handoff.returnSchemaRef', async () => {
30
+ const cap = await readLiveRuntimeCap();
31
+ // structuredOutput is a sub-flag of a supported liveRuntime; gate on both.
32
+ const advertised = cap?.supported === true && cap?.structuredOutput === true;
33
+ if (!behaviorGate('openwop-live-structured-output', advertised)) return;
34
+
35
+ if (!(await isEventLogSeamAvailable())) return; // soft-skip
36
+ const res = await invokeLive({
37
+ source: 'run-api',
38
+ returnSchemaRef: 'conformance-strict-handoff',
39
+ forceInvalidResult: true,
40
+ });
41
+ if (res === null || !res.runId) return; // seam/hook absent — soft-skip
42
+
43
+ const q = await queryTestEvents(res.runId, { type: 'agent.invocation.completed' });
44
+ if (!q.ok || !q.events[0]) return;
45
+ const payload = q.events[q.events.length - 1]!.payload;
46
+
47
+ expect(
48
+ payload.outcome === 'failed',
49
+ driver.describe('RFC 0077 §B step 6', 'a result violating handoff.returnSchemaRef MUST fail the invocation (outcome "failed"), not ship as completed'),
50
+ ).toBe(true);
51
+ expect(
52
+ payload.schemaValidated !== true,
53
+ driver.describe('RFC 0077 §B step 6', 'schemaValidated MUST NOT be true for a schema-violating result'),
54
+ ).toBe(true);
55
+
56
+ await resetTestSeam();
57
+ });
58
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * agent-loop-iteration-monotonic — RFC 0061 §B. Across a multi-turn loop,
3
+ * `runOrchestrator.decided.iteration` increments 1, 2, 3 … exactly once per turn
4
+ * (1-based, monotonic) — the observable counter `maxLoopIterations` bounds.
5
+ *
6
+ * Gated on `executionModel.version >= 5` + the host agent-loop seam; soft-skips
7
+ * when either is absent.
8
+ *
9
+ * @see RFCS/0061-agent-loop-lifecycle.md §B
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { driver } from '../lib/driver.js';
14
+ import { readExecutionModelCap, isVersion5, invokeAgentLoop } from '../lib/agentLoop.js';
15
+
16
+ describe('agent-loop-iteration-monotonic (RFC 0061 §B)', () => {
17
+ it('iteration increments by exactly 1 per orchestrator turn, 1-based', async () => {
18
+ if (!isVersion5(await readExecutionModelCap())) return;
19
+ const res = await invokeAgentLoop({ turns: 3 });
20
+ if (res === null) return; // seam absent — soft-skip
21
+ const decisions = res.decisions ?? [];
22
+ expect(
23
+ decisions.length >= 1,
24
+ driver.describe('RFC 0061 §B', 'a multi-turn loop MUST emit one runOrchestrator.decided per turn'),
25
+ ).toBe(true);
26
+ const iterations = decisions.map((d) => d.iteration);
27
+ const expected = decisions.map((_, k) => k + 1);
28
+ expect(
29
+ JSON.stringify(iterations),
30
+ driver.describe('RFC 0061 §B', 'iteration MUST be 1-based + monotonic, incrementing by exactly 1 per turn'),
31
+ ).toBe(JSON.stringify(expected));
32
+ });
33
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * agent-loop-stateful-resume — RFC 0061 §D. A loop suspended on a clarify/escalate
3
+ * HITL interrupt resumes at the SAME iteration — the counter does not reset or
4
+ * skip — with the snapshot lineage intact.
5
+ *
6
+ * Gated on `executionModel.statefulResume: true` + the host agent-loop seam;
7
+ * soft-skips when either is absent.
8
+ *
9
+ * @see RFCS/0061-agent-loop-lifecycle.md §D
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { driver } from '../lib/driver.js';
14
+ import { readExecutionModelCap, invokeAgentLoop } from '../lib/agentLoop.js';
15
+
16
+ describe('agent-loop-stateful-resume (RFC 0061 §D)', () => {
17
+ it('a mid-loop suspend resumes at the same iteration, counter intact', async () => {
18
+ const em = await readExecutionModelCap();
19
+ if (em?.statefulResume !== true) return;
20
+ // Suspend at turn 2, then resume: the resumed iteration MUST be 2, not 1 or 3.
21
+ const res = await invokeAgentLoop({ turns: 4, suspendAtTurn: 2, resume: true });
22
+ if (res === null) return; // seam absent — soft-skip
23
+ expect(
24
+ res.resumedIteration,
25
+ driver.describe('RFC 0061 §D', 'a stateful resume MUST continue at the suspend iteration — the counter does not reset or skip'),
26
+ ).toBe(2);
27
+ });
28
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * agent-loop-version5-shape — RFC 0061 §A/§B. The `executionModel.statefulResume`
3
+ * + `transcriptWindow` advertisement fields are well-formed when present, and a
4
+ * host advertising `version >= 5` carries a sane version ceiling.
5
+ *
6
+ * Status: ACTIVE (advertisement-shape; always runs). Behavioral coverage lives
7
+ * in the sibling agent-loop-*.test.ts scenarios, gated on `version >= 5` + the
8
+ * host agent-loop seam.
9
+ *
10
+ * @see RFCS/0061-agent-loop-lifecycle.md §A
11
+ * @see spec/v1/multi-agent-execution.md §"Stateful agent-loop lifecycle"
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import { driver } from '../lib/driver.js';
16
+ import { readExecutionModelCap } from '../lib/agentLoop.js';
17
+
18
+ describe('agent-loop-version5-shape: advertisement (RFC 0061 §A)', () => {
19
+ it('executionModel.statefulResume/transcriptWindow are well-formed when present', async () => {
20
+ const em = await readExecutionModelCap();
21
+ if (em === null) return; // no execution model — valid
22
+ if (em.statefulResume !== undefined) {
23
+ expect(
24
+ typeof em.statefulResume,
25
+ driver.describe('capabilities.schema.json §multiAgent.executionModel', 'statefulResume MUST be a boolean when present'),
26
+ ).toBe('boolean');
27
+ }
28
+ if (em.transcriptWindow !== undefined) {
29
+ expect(
30
+ typeof em.transcriptWindow === 'number' && (em.transcriptWindow as number) >= 1,
31
+ driver.describe('capabilities.schema.json §multiAgent.executionModel', 'transcriptWindow MUST be a positive integer when present'),
32
+ ).toBe(true);
33
+ }
34
+ if (typeof em.version === 'number') {
35
+ expect(
36
+ (em.version as number) >= 1 && (em.version as number) <= 5,
37
+ driver.describe('capabilities.schema.json §multiAgent.executionModel', 'version MUST be within the 1–5 ladder'),
38
+ ).toBe(true);
39
+ }
40
+ });
41
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * agent-loop-workspace-snapshot — RFC 0061 §C. A workspace PUT during turn i is
3
+ * invisible to turn i's snapshot and visible to turn i+1 — per-iteration
4
+ * snapshot immutability (writes land next turn, never retroactively).
5
+ *
6
+ * Gated on `executionModel.version >= 5` AND `host.workspace.supported` + the
7
+ * host agent-loop seam; soft-skips when any is absent.
8
+ *
9
+ * @see RFCS/0061-agent-loop-lifecycle.md §C
10
+ * @see RFCS/0059-agent-workspace.md §D — the workspace read snapshot
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest';
14
+ import { driver } from '../lib/driver.js';
15
+ import { readExecutionModelCap, isVersion5, hasWorkspace, invokeAgentLoop } from '../lib/agentLoop.js';
16
+
17
+ describe('agent-loop-workspace-snapshot (RFC 0061 §C)', () => {
18
+ it('a turn-i workspace write is invisible to turn i, visible to turn i+1', async () => {
19
+ if (!isVersion5(await readExecutionModelCap())) return;
20
+ if (!(await hasWorkspace())) return; // workspace optional — soft-skip
21
+ const res = await invokeAgentLoop({ turns: 2, workspaceWriteAtTurn: 1 });
22
+ if (res === null) return; // seam absent — soft-skip
23
+ const vis = res.workspaceVisible ?? {};
24
+ expect(
25
+ vis.atWriteTurn,
26
+ driver.describe('RFC 0061 §C', 'a workspace write during turn i MUST be invisible to turn i\'s snapshot'),
27
+ ).toBe(false);
28
+ expect(
29
+ vis.atNextTurn,
30
+ driver.describe('RFC 0061 §C', 'a workspace write during turn i MUST be visible to turn i+1'),
31
+ ).toBe(true);
32
+ });
33
+ });