@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,85 @@
1
+ /**
2
+ * agent-manifest-runtime — RFC 0070. When a host advertises
3
+ * `capabilities.agents.manifestRuntime.supported`, it has loaded pack `agents[]`
4
+ * (RFC 0003) into an AgentRegistry and can dispatch a manifest agent on the
5
+ * existing core.dispatch/orchestrator loop, enforcing toolAllowlist (RFC 0002
6
+ * §A14) and confidence escalation (§F).
7
+ *
8
+ * The inventory leg is exercised against the NORMATIVE `GET /v1/agents` surface
9
+ * (RFC 0072 §A), so it runs black-box against any conformant host. The dispatch
10
+ * leg uses the sample-extension seam and soft-skips on hosts that don't expose
11
+ * it (full black-box dispatch is the sequenced executor-integration tier). Both
12
+ * legs gate on `capabilities.agents.manifestRuntime.supported`.
13
+ *
14
+ * @see RFCS/0070-agent-manifest-runtime.md, RFCS/0072-agent-inventory-and-dispatch.md
15
+ */
16
+
17
+ import { describe, it, expect } from 'vitest';
18
+ import { driver } from '../lib/driver.js';
19
+ import { readManifestRuntimeCap, listManifestAgents, dispatchAgent } from '../lib/agentRuntime.js';
20
+
21
+ describe('agent-manifest-runtime (RFC 0070)', () => {
22
+ it('lists installed manifest agents and dispatches one with attributed events', async () => {
23
+ const cap = await readManifestRuntimeCap();
24
+ if (cap?.supported !== true) return; // unadvertised — soft-skip
25
+
26
+ // RFC 0074 §B — installScope governs how GET /v1/agents is scoped.
27
+ const installScope = typeof cap.installScope === 'string' ? cap.installScope : 'host';
28
+ expect(
29
+ installScope === 'host' || installScope === 'tenant',
30
+ driver.describe('RFC 0074 §B', "agents.manifestRuntime.installScope (when present) MUST be 'host' or 'tenant'"),
31
+ ).toBe(true);
32
+
33
+ const inv = await listManifestAgents();
34
+ if (inv === null) return; // seam absent — soft-skip
35
+ const agents = inv.agents ?? [];
36
+ expect(
37
+ Array.isArray(agents),
38
+ driver.describe('RFC 0072 §A', 'GET /v1/agents MUST return an agents[] array'),
39
+ ).toBe(true);
40
+
41
+ if (installScope === 'host') {
42
+ // Host-global inventory (RFC 0072 §A): a manifestRuntime host MUST surface ≥1 agent.
43
+ expect(
44
+ agents.length > 0,
45
+ driver.describe('RFC 0070 §A', 'a host-scoped manifestRuntime host MUST surface ≥1 installed manifest agent'),
46
+ ).toBe(true);
47
+ } else if (agents.length === 0) {
48
+ // RFC 0074 §A + Unresolved Q3 — tenant-scoped: GET /v1/agents is the
49
+ // authenticated principal's workspace set, which MAY be empty (the workspace
50
+ // approved no agent packs) while manifestRuntime is advertised host-wide.
51
+ // Empty is conformant; the cross-tenant no-disclosure 404 is covered by the
52
+ // owner-triple isolation harness (RFC 0048/0059), not re-probed here. Nothing
53
+ // to dispatch.
54
+ return;
55
+ }
56
+
57
+ const agentId = agents[0]?.agentId;
58
+ if (typeof agentId !== 'string') return;
59
+
60
+ // Opaque-payload dispatch (validateHandoff:false) so the assertion is
61
+ // independent of the chosen agent's handoff schema.
62
+ const res = await dispatchAgent(agentId, { task: {}, validateHandoff: false, availableTools: [] });
63
+ if (res === null) return; // seam absent — soft-skip
64
+ expect(
65
+ res.status === 'completed' || res.status === 'escalated',
66
+ driver.describe('RFC 0070', 'dispatch MUST resolve to a terminal status (completed | escalated)'),
67
+ ).toBe(true);
68
+ const types = (res.events ?? []).map((e) => e.type);
69
+ expect(
70
+ types.includes('agent.reasoned') && types.includes('agent.decided'),
71
+ driver.describe('RFC 0002 §A', 'dispatch MUST emit attributed agent.reasoned + agent.decided events'),
72
+ ).toBe(true);
73
+ expect(
74
+ (res.events ?? []).every((e) => e.agentId === agentId),
75
+ driver.describe('RFC 0002 §A', 'every emitted agent.* event MUST carry the dispatched agentId'),
76
+ ).toBe(true);
77
+ });
78
+
79
+ // NOTE: RFC 0002 §F confidence escalation is NOT asserted here. Forcing a
80
+ // sub-threshold decision black-box would require a non-normative test hook
81
+ // (the reference host's `simulateConfidence`); a conformant host need not
82
+ // expose one, so asserting it here would wrongly fail conformant peers.
83
+ // Escalation is covered against the reference host in
84
+ // apps/workflow-engine/backend/typescript/test/{agents,agent-dispatch-route}.test.ts.
85
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Agent org-chart — record + capability + the non-authority guarantee (RFC 0087).
3
+ *
4
+ * Always-on, server-free schema-shape probe. Verifies that:
5
+ * - `capabilities.agents.orgChart` is declared with its `supported` /
6
+ * `installScope` / `departmentNesting` / `responsibilityView` sub-flags.
7
+ * - `agent-org-chart.schema.json` compiles and round-trips a conforming
8
+ * chart, and rejects malformed ones (a non-`host:` member rosterId).
9
+ * - the §B structural non-authority guarantee: the schema REJECTS an
10
+ * authority-bearing field on a member (`scopes` / `canDispatch` /
11
+ * `permissions`) — every object is `additionalProperties:false`, so a
12
+ * host cannot express position-as-authority through it. This is the public
13
+ * test for the protocol-tier SECURITY invariant
14
+ * `org-position-no-authority-escalation`.
15
+ *
16
+ * Behavioral assertions (a manager's tool over-reach is refused; an RFC 0049
17
+ * decision is invariant to org position; the cross-tenant 404; the §D roll-up
18
+ * over live roster portfolios) are gated on `capabilities.agents.orgChart.supported`
19
+ * and land at Active → Accepted (reference-host org store deferred per RFC 0087
20
+ * §Conformance — the host-extension at `/v1/host/sample/org-chart`, #371, is the
21
+ * reference demonstration). This scenario asserts the wire contract, not host behavior.
22
+ *
23
+ * Spec references:
24
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-org-chart.md
25
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0087-agent-org-chart.md
26
+ * - https://github.com/openwop/openwop/blob/main/SECURITY/invariants.yaml (org-position-no-authority-escalation)
27
+ */
28
+
29
+ import { describe, it, expect } from 'vitest';
30
+ import { readFileSync } from 'node:fs';
31
+ import { join } from 'node:path';
32
+ import Ajv2020 from 'ajv/dist/2020.js';
33
+ import addFormats from 'ajv-formats';
34
+ import { SCHEMAS_DIR } from '../lib/paths.js';
35
+
36
+ /** Server-free assertion-message helper. */
37
+ const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
38
+
39
+ function loadSchema(name: string): Record<string, unknown> {
40
+ return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
41
+ }
42
+
43
+ const CHART = {
44
+ owner: { tenantId: 'acme', workspaceId: 'growth' },
45
+ departments: [
46
+ {
47
+ departmentId: 'dept-marketing',
48
+ name: 'Marketing',
49
+ parentDepartmentId: null,
50
+ roles: [
51
+ { roleId: 'role-cm', name: 'Campaign Manager' },
52
+ { roleId: 'role-bw', name: 'Brief Writer' },
53
+ ],
54
+ },
55
+ ],
56
+ members: [
57
+ { rosterId: 'host:sally-marketing', departmentId: 'dept-marketing', roleId: 'role-bw', reportsTo: 'host:morgan-cmo' },
58
+ { rosterId: 'host:morgan-cmo', departmentId: 'dept-marketing', roleId: 'role-cm', reportsTo: null },
59
+ ],
60
+ };
61
+
62
+ describe('agent-org-chart-shape: capability advertisement (RFC 0087, server-free)', () => {
63
+ it('the capabilities schema declares agents.orgChart with its sub-flags', () => {
64
+ const caps = loadSchema('capabilities.schema.json');
65
+ const agents = (caps.properties as Record<string, { properties?: Record<string, { properties?: Record<string, unknown> }> }>).agents;
66
+ const orgChart = agents?.properties?.orgChart;
67
+ expect(orgChart, why('capabilities.md §agents', 'agents.orgChart MUST be declared')).toBeDefined();
68
+ for (const flag of ['supported', 'installScope', 'departmentNesting', 'responsibilityView']) {
69
+ expect(orgChart?.properties?.[flag], why('agent-org-chart.md §E', `agents.orgChart.${flag} MUST be declared`)).toBeDefined();
70
+ }
71
+ });
72
+ });
73
+
74
+ describe('agent-org-chart-shape: chart record (RFC 0087 §A, server-free)', () => {
75
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
76
+ addFormats(ajv);
77
+ const chart = ajv.compile(loadSchema('agent-org-chart.schema.json'));
78
+
79
+ it('AgentOrgChart validates a conforming chart', () => {
80
+ expect(chart(CHART), why('RFC 0087 §A', 'a conforming org-chart MUST validate')).toBe(true);
81
+ });
82
+
83
+ it('rejects a non-host: member rosterId and a chart missing required arrays', () => {
84
+ const badMember = { ...CHART, members: [{ rosterId: 'core.openwop.agents.sally', departmentId: 'dept-marketing', roleId: 'role-bw', reportsTo: null }] };
85
+ expect(chart(badMember), why('RFC 0087 §A', 'a non-`host:` member rosterId MUST be rejected')).toBe(false);
86
+ expect(chart({ owner: { tenantId: 'acme' }, departments: [] }), why('RFC 0087 §A', 'a chart without `members` MUST be rejected')).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('agent-org-chart-shape: §B non-authority guarantee (RFC 0087, server-free)', () => {
91
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
92
+ addFormats(ajv);
93
+ const chart = ajv.compile(loadSchema('agent-org-chart.schema.json'));
94
+
95
+ it('the schema rejects an authority-bearing field on a member (org-position-no-authority-escalation)', () => {
96
+ for (const authorityField of ['scopes', 'canDispatch', 'permissions', 'authority']) {
97
+ const withAuthority = {
98
+ ...CHART,
99
+ members: [{ ...CHART.members[1], [authorityField]: ['anything'] }],
100
+ };
101
+ expect(
102
+ chart(withAuthority),
103
+ why('SECURITY invariant org-position-no-authority-escalation', `a member carrying \`${authorityField}\` MUST be rejected (additionalProperties:false — position confers no authority)`),
104
+ ).toBe(false);
105
+ }
106
+ });
107
+
108
+ it('a conforming member object carries exactly the descriptive key set — nothing authority-bearing', () => {
109
+ const memberKeys = Object.keys(CHART.members[1]!).sort();
110
+ expect(memberKeys, why('RFC 0087 §B', 'a member is descriptive only: {departmentId, reportsTo, roleId, rosterId}')).toEqual(['departmentId', 'reportsTo', 'roleId', 'rosterId']);
111
+ });
112
+
113
+ it('the GET /v1/agents/org-chart/{departmentId} responsibility-view response validates (RFC 0087 §D)', () => {
114
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
115
+ addFormats(ajv);
116
+ ajv.addSchema(loadSchema('agent-org-chart.schema.json'), 'https://openwop.dev/spec/v1/agent-org-chart.schema.json');
117
+ const view = ajv.compile(loadSchema('org-chart-responsibility-view.schema.json'));
118
+ const good = {
119
+ department: CHART.departments[0],
120
+ members: CHART.members,
121
+ responsibilities: ['marketing-email-campaign', 'social-post-scheduler'],
122
+ };
123
+ expect(view(good), why('RFC 0087 §D', 'a conforming responsibility-view response MUST validate')).toBe(true);
124
+ expect(view({ ...good, unexpected: true }), why('RFC 0087 §D', 'an extra top-level property MUST be rejected')).toBe(false);
125
+ expect(view({ department: CHART.departments[0], members: CHART.members }), why('RFC 0087 §D', '`responsibilities` is required')).toBe(false);
126
+ });
127
+ });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * openwop-agent-platform — operational-annex predicate + status derivation (RFC 0085).
3
+ *
4
+ * Always-on, server-free derivation probe. Verifies that:
5
+ * - `isAgentPlatformPartial` / `isAgentPlatformFull` / `agentPlatformStatus`
6
+ * derive `none` / `partial` / `full` correctly from representative discovery
7
+ * payloads (RFC 0085 §B).
8
+ * - the floor's replay-OR-nondeterminism term is honored: a host with no
9
+ * `replay.supported` but `nondeterminismPolicy.declared: true` still meets the
10
+ * floor.
11
+ * - the `full` tier requires the governance terms (RBAC + tenant installScope +
12
+ * memory.attribution + debug-bundle + trigger-bridge + egress-policy); a host
13
+ * missing any reports `partial`, never `full` (the honest-advertisement rule).
14
+ * - `capabilities.nondeterminismPolicy.declared` is declared in the schema.
15
+ *
16
+ * The LIVE aggregate-evidence assertion (does every required constituent scenario
17
+ * actually pass against a host claiming `full`?) is the `Active → Accepted` step
18
+ * per RFC 0085 §C — naturally gated on a reference host reaching partial/full, and
19
+ * deferred here. This scenario asserts the discovery-predicate derivation only.
20
+ *
21
+ * Spec references:
22
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-platform-profile.md
23
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0085-agent-platform-meta-profile.md
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import { readFileSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ import { SCHEMAS_DIR } from '../lib/paths.js';
30
+ import { isAgentPlatformPartial, isAgentPlatformFull, agentPlatformStatus, agentPlatformSatisfiedTerms } from '../lib/profiles.js';
31
+
32
+ const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
33
+
34
+ const CORE = {
35
+ protocolVersion: '1.0',
36
+ supportedEnvelopes: ['clarification.request'],
37
+ schemaVersions: {},
38
+ limits: { clarificationRounds: 1, schemaRounds: 1, envelopesPerTurn: 1 },
39
+ };
40
+
41
+ /** A discovery payload meeting the §B floor (partial). */
42
+ function floorPayload(extra: Record<string, unknown> = {}): Record<string, unknown> {
43
+ return {
44
+ ...CORE,
45
+ agents: { manifestRuntime: { supported: true }, liveRuntime: { supported: true } },
46
+ toolCatalog: { supported: true },
47
+ toolHooks: { supported: true },
48
+ httpClient: { safeFetch: { supported: true } },
49
+ providerUsage: { supported: true },
50
+ prompts: { supported: true },
51
+ memory: { supported: true },
52
+ feedback: { supported: true },
53
+ replay: { supported: true },
54
+ ...extra,
55
+ };
56
+ }
57
+
58
+ describe('agent-platform-profile: floor (partial) predicate (RFC 0085 §B, server-free)', () => {
59
+ it('a host meeting all floor flags is partial', () => {
60
+ const c = floorPayload();
61
+ expect(isAgentPlatformPartial(c), why('agent-platform-profile.md §B', 'all floor flags ⇒ partial')).toBe(true);
62
+ expect(agentPlatformStatus(c)).toBe('partial');
63
+ });
64
+
65
+ it('missing a single floor flag (feedback) ⇒ none', () => {
66
+ const c = floorPayload({ feedback: { supported: false } });
67
+ expect(isAgentPlatformPartial(c), why('agent-platform-profile.md §B', 'a missing floor flag ⇒ not partial')).toBe(false);
68
+ expect(agentPlatformStatus(c)).toBe('none');
69
+ });
70
+
71
+ it('replay-OR-nondeterminism: no replay but declared nondeterminism still meets the floor', () => {
72
+ const c = floorPayload({ replay: { supported: false }, nondeterminismPolicy: { declared: true } });
73
+ expect(isAgentPlatformPartial(c), why('agent-platform-profile.md §B', 'declared nondeterminism satisfies the replay-OR term')).toBe(true);
74
+ });
75
+
76
+ it('neither replay nor declared nondeterminism ⇒ floor unmet', () => {
77
+ const c = floorPayload({ replay: { supported: false } });
78
+ expect(isAgentPlatformPartial(c), why('agent-platform-profile.md §B', 'neither replay nor declared policy ⇒ not partial')).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('agent-platform-profile: full predicate + honest-advertisement (RFC 0085 §B/§D, server-free)', () => {
83
+ const fullExtra = {
84
+ authorization: { supported: true },
85
+ agents: { manifestRuntime: { supported: true, installScope: 'tenant' }, liveRuntime: { supported: true } },
86
+ memory: { supported: true, attribution: { supported: true } },
87
+ debugBundle: { supported: true },
88
+ triggerBridge: { supported: true },
89
+ httpClient: { safeFetch: { supported: true }, egressPolicy: { supported: true } },
90
+ };
91
+
92
+ it('a host meeting floor + all governance terms is full', () => {
93
+ const c = floorPayload(fullExtra);
94
+ expect(isAgentPlatformFull(c), why('agent-platform-profile.md §B', 'floor + governance ⇒ full')).toBe(true);
95
+ expect(agentPlatformStatus(c)).toBe('full');
96
+ });
97
+
98
+ it('a host advertising governance flags but missing tenant installScope reports partial, not full', () => {
99
+ const c = floorPayload({
100
+ ...fullExtra,
101
+ agents: { manifestRuntime: { supported: true, installScope: 'host' }, liveRuntime: { supported: true } },
102
+ });
103
+ expect(isAgentPlatformFull(c), why('agent-platform-profile.md §D', 'missing a governance term ⇒ MUST NOT be full')).toBe(false);
104
+ expect(agentPlatformStatus(c)).toBe('partial');
105
+ });
106
+
107
+ it('eval/deploy/budget are NOT hard full terms (a full host without them is still full)', () => {
108
+ const c = floorPayload(fullExtra); // no agents.evalSuite / agents.deployment / budget
109
+ expect(isAgentPlatformFull(c), why('agent-platform-profile.md §B', 'platform-plus tier is advisory, not a hard full term')).toBe(true);
110
+ });
111
+ });
112
+
113
+ describe('agent-platform-profile: satisfiedTerms[] non-contiguous adoption (RFC 0085 §D, server-free)', () => {
114
+ it('a host honoring full-tier terms but failing floor terms is status none yet has a non-empty satisfiedTerms[]', () => {
115
+ // The real-host (MyndHyve) shape: RBAC + memory.attribution + tenant installScope (3 full terms)
116
+ // satisfied, while liveRuntime / toolCatalog / providerUsage / memory floor terms are absent.
117
+ const c = {
118
+ ...CORE,
119
+ agents: { manifestRuntime: { supported: true, installScope: 'tenant' } }, // no liveRuntime
120
+ authorization: { supported: true },
121
+ memory: { attribution: { supported: true } }, // attribution but NOT memory.supported
122
+ toolHooks: { supported: true },
123
+ httpClient: { safeFetch: { supported: true } },
124
+ prompts: { supported: true },
125
+ feedback: { supported: true },
126
+ replay: { supported: true },
127
+ } as Record<string, unknown>;
128
+ expect(agentPlatformStatus(c), why('agent-platform-profile.md §D', 'floor unmet ⇒ none')).toBe('none');
129
+ const terms = agentPlatformSatisfiedTerms(c);
130
+ expect(terms.includes('full:authorization'), why('§D', 'a satisfied full term is reported even at none')).toBe(true);
131
+ expect(terms.includes('full:memory.attribution')).toBe(true);
132
+ expect(terms.includes('full:tenant-installScope')).toBe(true);
133
+ expect(terms.includes('floor:agents.liveRuntime'), why('§D', 'an unmet floor term is NOT reported')).toBe(false);
134
+ expect(terms.length).toBeGreaterThan(0); // distinguishable from a 0/16 do-nothing host
135
+ });
136
+
137
+ it('a full host reports all sixteen terms satisfied', () => {
138
+ const c = floorPayload({
139
+ authorization: { supported: true },
140
+ agents: { manifestRuntime: { supported: true, installScope: 'tenant' }, liveRuntime: { supported: true } },
141
+ memory: { supported: true, attribution: { supported: true } },
142
+ debugBundle: { supported: true },
143
+ triggerBridge: { supported: true },
144
+ httpClient: { safeFetch: { supported: true }, egressPolicy: { supported: true } },
145
+ });
146
+ expect(agentPlatformSatisfiedTerms(c).length, why('§D', 'a full host satisfies all 16 terms')).toBe(16);
147
+ });
148
+ });
149
+
150
+ describe('agent-platform-profile: capability shape (RFC 0085, server-free)', () => {
151
+ it('capabilities.nondeterminismPolicy.declared is declared', () => {
152
+ const caps = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'capabilities.schema.json'), 'utf8')) as { properties?: Record<string, { properties?: Record<string, unknown> }> };
153
+ expect(
154
+ caps.properties?.nondeterminismPolicy?.properties?.declared,
155
+ why('agent-platform-profile.md §B', 'capabilities.nondeterminismPolicy.declared MUST be declared'),
156
+ ).toBeDefined();
157
+ });
158
+ });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Standing-agent roster attribution + ordering (RFC 0086 §B/§C) — behavioral.
3
+ *
4
+ * Gated on `capabilities.agents.roster.supported` (root-first per RFC 0073).
5
+ * Soft-skips when unadvertised (default) / hard-fails under
6
+ * `OPENWOP_REQUIRE_BEHAVIOR=true` via `behaviorGate`. The companion always-on
7
+ * wire-shape coverage lives in `agent-roster-shape.test.ts`; this scenario
8
+ * asserts host BEHAVIOR:
9
+ *
10
+ * 1. NORMATIVE read — `GET /v1/agents/roster` (RFC 0086 §B) returns the
11
+ * `agent-roster-response` shape (roster[] + `total == roster.length`), and
12
+ * every entry carries a `host:<id>` `rosterId`, a `persona`, an
13
+ * `agentRef.agentId`, and an `owner.tenantId`. Runs black-box against the
14
+ * normative path on any roster host.
15
+ * 2. ATTRIBUTION + ORDERING (seam-gated) — a portfolio fire emits
16
+ * `roster.run.initiated` as the run's FIRST attribution event, BEFORE any
17
+ * `agent.invocation.*` / `agent.*` event (§C), content-free (no work-item
18
+ * `body`/`prompt`/credential — the `roster-attribution-no-content`
19
+ * invariant), with `rosterId`/`persona`/`agentId`/`workflowId`/
20
+ * `triggerSource`. A durable work-item fire additionally carries
21
+ * `triggerSubscriptionId` (RFC 0083) traceable on the run's `causationId`.
22
+ * 3. TENANT SCOPING (§B / RFC 0074) — a `GET /v1/agents/roster/{id}` for an id
23
+ * outside the caller's owner triple 404s (probed only when a cross-tenant id
24
+ * is supplied via `OPENWOP_CROSS_TENANT_ROSTER_ID`; soft-skip otherwise).
25
+ *
26
+ * The fire + event-log seams are OPTIONAL (reference roster store deferred per
27
+ * RFC 0086 §Conformance); each leg soft-skips independently so a host that
28
+ * serves only the normative read still exercises leg 1.
29
+ *
30
+ * Spec references:
31
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-roster.md
32
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0086-standing-agent-roster-and-workflow-portfolio.md
33
+ * - https://github.com/openwop/openwop/blob/main/SECURITY/invariants.yaml (roster-attribution-no-content)
34
+ */
35
+
36
+ import { describe, it, expect } from 'vitest';
37
+ import { driver } from '../lib/driver.js';
38
+ import { behaviorGate } from '../lib/behavior-gate.js';
39
+ import { readRosterCap, listRoster, getRosterEntry, fireRosterPortfolio } from '../lib/agentRoster.js';
40
+ import {
41
+ queryTestEvents,
42
+ isEventLogSeamAvailable,
43
+ resetTestSeam,
44
+ type TestEvent,
45
+ } from '../lib/event-log-query.js';
46
+
47
+ const ROSTER_ID_RE = /^host:[a-z0-9][a-z0-9._-]*$/;
48
+
49
+ /** Lowest-sequence event matching one of `types`; undefined when none present. */
50
+ function firstOf(events: TestEvent[], types: string[]): TestEvent | undefined {
51
+ return events
52
+ .filter((e) => types.includes(e.type))
53
+ .sort((a, b) => a.sequence - b.sequence)[0];
54
+ }
55
+
56
+ describe('agent-roster-attribution (RFC 0086 §B/§C)', () => {
57
+ it('serves the normative roster, attributes a portfolio fire content-free + ordered, and tenant-scopes', async () => {
58
+ const cap = await readRosterCap();
59
+ if (!behaviorGate('openwop-roster-attribution', cap?.supported === true)) return;
60
+
61
+ // RFC 0074 carry-forward: installScope MUST be host|tenant when present.
62
+ const installScope = typeof cap?.installScope === 'string' ? cap.installScope : 'host';
63
+ expect(
64
+ installScope === 'host' || installScope === 'tenant',
65
+ driver.describe('RFC 0086 §F / RFC 0074 §B', "agents.roster.installScope (when present) MUST be 'host' or 'tenant'"),
66
+ ).toBe(true);
67
+
68
+ // ---- Leg 1: normative read (black-box on any roster host) -------------
69
+ const body = await listRoster();
70
+ if (body === null) return; // host advertises roster but doesn't serve the read yet — soft-skip
71
+ const roster = body.roster ?? [];
72
+ expect(
73
+ Array.isArray(roster),
74
+ driver.describe('agent-roster.md §B', 'GET /v1/agents/roster MUST return a roster[] array'),
75
+ ).toBe(true);
76
+ expect(
77
+ body.total === roster.length,
78
+ driver.describe('agent-roster-response.schema.json', 'total MUST equal roster.length'),
79
+ ).toBe(true);
80
+ for (const entry of roster) {
81
+ expect(
82
+ typeof entry.rosterId === 'string' && ROSTER_ID_RE.test(entry.rosterId),
83
+ driver.describe('agent-roster-entry.schema.json', 'each entry MUST carry a host:<id> rosterId'),
84
+ ).toBe(true);
85
+ expect(
86
+ typeof entry.persona === 'string' && entry.persona.length > 0,
87
+ driver.describe('agent-roster.md §A', 'each entry MUST carry a non-empty persona'),
88
+ ).toBe(true);
89
+ expect(
90
+ typeof entry.agentRef?.agentId === 'string',
91
+ driver.describe('agent-roster.md §A', 'each entry MUST reference an agentRef.agentId'),
92
+ ).toBe(true);
93
+ expect(
94
+ typeof entry.owner?.tenantId === 'string',
95
+ driver.describe('agent-roster.md §B / RFC 0074', 'each entry MUST carry an owner.tenantId scope'),
96
+ ).toBe(true);
97
+ // RFC 0082 §A XOR: an agentRef MUST NOT pin both version and channel.
98
+ expect(
99
+ !(entry.agentRef?.version !== undefined && entry.agentRef?.channel !== undefined),
100
+ driver.describe('RFC 0082 §A', 'agentRef MUST NOT carry both version and channel'),
101
+ ).toBe(true);
102
+ }
103
+
104
+ // ---- Leg 2: attribution + ordering (seam-gated) ----------------------
105
+ if (await isEventLogSeamAvailable()) {
106
+ // Scheduled portfolio fire.
107
+ const fired = await fireRosterPortfolio({ triggerSource: 'schedule' });
108
+ if (fired?.runId) {
109
+ const q = await queryTestEvents(fired.runId);
110
+ if (q.ok) {
111
+ const init = firstOf(q.events, ['roster.run.initiated']);
112
+ expect(
113
+ init !== undefined,
114
+ driver.describe('agent-roster.md §C', 'a portfolio fire MUST emit roster.run.initiated'),
115
+ ).toBe(true);
116
+
117
+ if (init) {
118
+ // Ordering: roster.run.initiated precedes ANY agent invocation/event.
119
+ const firstAgent = firstOf(q.events, [
120
+ 'agent.invocation.started',
121
+ 'agent.reasoned',
122
+ 'agent.decided',
123
+ ]);
124
+ if (firstAgent) {
125
+ expect(
126
+ init.sequence < firstAgent.sequence,
127
+ driver.describe('agent-roster.md §C', 'roster.run.initiated MUST precede any agent.* event in the run'),
128
+ ).toBe(true);
129
+ }
130
+
131
+ // Content-free: required ids present; NO work-item body/prompt/credential.
132
+ const p = init.payload;
133
+ for (const key of ['rosterId', 'persona', 'agentId', 'workflowId', 'triggerSource']) {
134
+ expect(
135
+ typeof p[key] === 'string' && (p[key] as string).length > 0,
136
+ driver.describe('run-event-payloads.schema.json#rosterRunInitiated', `roster.run.initiated MUST carry ${key}`),
137
+ ).toBe(true);
138
+ }
139
+ for (const forbidden of ['body', 'prompt', 'input', 'payload', 'apiKey', 'secret', 'credentials', 'token']) {
140
+ expect(
141
+ !(forbidden in p),
142
+ driver.describe('SECURITY roster-attribution-no-content', `roster.run.initiated MUST be content-free (no ${forbidden})`),
143
+ ).toBe(true);
144
+ }
145
+ expect(
146
+ typeof p.rosterId === 'string' && ROSTER_ID_RE.test(p.rosterId),
147
+ driver.describe('agent-roster.md §C', 'roster.run.initiated.rosterId MUST be a host:<id> AgentRef id'),
148
+ ).toBe(true);
149
+ }
150
+ }
151
+ }
152
+
153
+ // Durable work-item fire: carries the RFC 0083 triggerSubscriptionId + causation.
154
+ const work = await fireRosterPortfolio({ triggerSource: 'webhook', asWorkItem: true });
155
+ if (work?.runId) {
156
+ const q = await queryTestEvents(work.runId, { type: 'roster.run.initiated' });
157
+ if (q.ok && q.events[0]) {
158
+ const p = q.events[0].payload;
159
+ expect(
160
+ typeof p.triggerSubscriptionId === 'string' && (p.triggerSubscriptionId as string).length > 0,
161
+ driver.describe('agent-roster.md §D / RFC 0083', 'a durable work-item fire MUST carry triggerSubscriptionId for trigger→run→roster ancestry'),
162
+ ).toBe(true);
163
+ }
164
+ }
165
+
166
+ await resetTestSeam();
167
+ }
168
+
169
+ // ---- Leg 3: tenant scoping (RFC 0074) --------------------------------
170
+ const crossTenantId = process.env.OPENWOP_CROSS_TENANT_ROSTER_ID;
171
+ if (typeof crossTenantId === 'string' && crossTenantId.length > 0) {
172
+ const probe = await getRosterEntry(crossTenantId);
173
+ expect(
174
+ probe.status === 404,
175
+ driver.describe('agent-roster.md §B / RFC 0074', "GET /v1/agents/roster/{id} for a cross-tenant id MUST 404 (no cross-tenant disclosure)"),
176
+ ).toBe(true);
177
+ }
178
+ });
179
+ });