@openwop/openwop-conformance 2.42.3 → 2.42.5

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 (54) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/dist/spec-artifacts.lock.json +2 -2
  4. package/package.json +2 -2
  5. package/requirements.json +292 -103
  6. package/schemas/CORPUS-STAMP.json +9 -9
  7. package/src/lib/agentOrgChart.ts +21 -4
  8. package/src/lib/effect-receiver.ts +23 -2
  9. package/src/lib/toolCatalog.ts +29 -5
  10. package/src/scenarios/agent-manifest-runtime.test.ts +17 -4
  11. package/src/scenarios/agent-org-chart-scoping.test.ts +27 -4
  12. package/src/scenarios/agent-roster-attribution.test.ts +28 -4
  13. package/src/scenarios/agentPackExport.test.ts +33 -0
  14. package/src/scenarios/agentPackInstall.test.ts +32 -0
  15. package/src/scenarios/agentReasoningEvents.test.ts +22 -13
  16. package/src/scenarios/agents-run-tool-allowlist.test.ts +6 -2
  17. package/src/scenarios/ai-envelope-shape.test.ts +9 -8
  18. package/src/scenarios/anonymous-actor-write-gated.test.ts +12 -6
  19. package/src/scenarios/audit-log-integrity.test.ts +37 -0
  20. package/src/scenarios/context-budget-transcript-bound.test.ts +14 -4
  21. package/src/scenarios/context-summarization-replay.test.ts +17 -8
  22. package/src/scenarios/conversationLifecycle.test.ts +12 -1
  23. package/src/scenarios/conversationReplayDeterminism.test.ts +19 -3
  24. package/src/scenarios/cost-attribution.test.ts +12 -0
  25. package/src/scenarios/debug-bundle-truncation.test.ts +16 -3
  26. package/src/scenarios/distillation-shape.test.ts +30 -0
  27. package/src/scenarios/envelope-truncated.test.ts +6 -2
  28. package/src/scenarios/feedback-fork-not-copied.test.ts +28 -7
  29. package/src/scenarios/idempotency-concurrent-claim.test.ts +27 -9
  30. package/src/scenarios/idempotency-key-determinism.test.ts +6 -2
  31. package/src/scenarios/mcp-server-untrusted-args.test.ts +20 -5
  32. package/src/scenarios/mcp-tool-roundtrip.test.ts +7 -21
  33. package/src/scenarios/memory-compaction-provenance-tag.test.ts +11 -3
  34. package/src/scenarios/orchestratorDispatch.test.ts +18 -2
  35. package/src/scenarios/pack-registry.test.ts +8 -1
  36. package/src/scenarios/production-backpressure.test.ts +17 -11
  37. package/src/scenarios/redaction.test.ts +8 -2
  38. package/src/scenarios/replay-fanout-suppression.test.ts +18 -2
  39. package/src/scenarios/runner-ledger.test.ts +7 -2
  40. package/src/scenarios/stream-modes.test.ts +27 -0
  41. package/src/scenarios/tool-catalog-projection.test.ts +35 -12
  42. package/src/scenarios/v2-effect-identity-business-key.test.ts +40 -6
  43. package/src/scenarios/v2-era-2-append-vocabulary.test.ts +12 -3
  44. package/src/scenarios/v2-idempotency-in-flight.test.ts +16 -0
  45. package/src/scenarios/v2-mcp-tasks.test.ts +17 -3
  46. package/src/scenarios/v2-min-client-version.test.ts +11 -1
  47. package/src/scenarios/v2-run-diff-identical.test.ts +13 -5
  48. package/src/scenarios/v2-run-pause-resume.test.ts +4 -1
  49. package/src/scenarios/v2-sse-last-event-id.test.ts +7 -0
  50. package/src/scenarios/v2-version-header-honored.test.ts +22 -3
  51. package/src/scenarios/v2-webhook-durable-delivery.test.ts +8 -0
  52. package/src/scenarios/v2-webhook-unregister-stops-delivery.test.ts +6 -1
  53. package/src/scenarios/version-negotiation.test.ts +12 -1
  54. package/src/scenarios/webhook-signed-delivery.test.ts +18 -3
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.42.3",
5
- "corpusTag": "v2.42.3",
4
+ "version": "2.42.5",
5
+ "corpusTag": "v2.42.5",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
8
8
  "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "69464bc0e1e71ef3b543a73d4fa4f67202343d2603c80af8033f6c3619ef1b30",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "6824903e9a7d33a6913878d46ad0a1e25746b596c6ab2ae7aa6c31d72babfa63",
13
- "api/v2/asyncapi.yaml": "45ee1825660a8d6336e145042e6e6e7f69d63d823fb7a6af8f5e9c46ec93c45f",
14
- "api/v2/openapi.yaml": "d45f0d99b6d57b45a30640583c0aa828163ba778768dd58835246ed5dad212b1",
12
+ "api/seams-v2.yaml": "33312adcc1cf1185db6f926195dbc2af3cdf626fb686b34e12b3e0f5c2d0f12c",
13
+ "api/v2/asyncapi.yaml": "ef7841d00162c7b89caa6df100f48a62927a619f0363733591a4b9a988ef570d",
14
+ "api/v2/openapi.yaml": "bb0140dc59875b8d0dc0c04a128372c09a088340dda45f139ff4a218412bbab4",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -204,7 +204,7 @@
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
205
  "spec/v1/alias-detectors.json": "2401fcb1c18cdd688c018b3d716ae6bca85c5e356220ee2b793bd9d81872412d",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "4271942ecca7c7455cb6318572bdb5a53ddc34f8d8914e2a68c64b091d131a0f",
207
+ "spec/v1/core-standard-manifest.json": "570aecd605ffb479c3f5aa73e0e0259747d95ecad0fc26f0438f66deaa67ebad",
208
208
  "spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -294,9 +294,9 @@
294
294
  "spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
295
295
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
296
296
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
297
- "spec/v2/release.json": "1b3dde14cbaf3cfc4eab1ae9a461aab6d357c9c4cda511047087c0b57b400055",
297
+ "spec/v2/release.json": "5132e4752b9e36d2ab61076455e3efbda31127e76565faadb67446f77707a246",
298
298
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
299
- "spec/v2/surface-baseline.json": "dc52941e7f3e19da5151bb5c994af9b18b137a5a3b34ee0b63bc9cb2789dc52a"
299
+ "spec/v2/surface-baseline.json": "ef98caceff85da0d8bae10e78d044762d47960432b7fa885c4c56697c7038274"
300
300
  },
301
- "corpusCommit": "d310f781e2dcb00d670e26fc23668c2d61548304"
301
+ "corpusCommit": "bfa55619a403957a27412e0fa2c8865a48199c38"
302
302
  }
@@ -51,12 +51,29 @@ export interface ResponsibilityView {
51
51
  responsibilities?: string[];
52
52
  }
53
53
 
54
+ /** GET the NORMATIVE org-chart (RFC 0087 §A `GET /v1/agents/org-chart`) and
55
+ * return the raw status alongside the parsed body, so a scenario can assert
56
+ * on the status itself (with a `req()` id) instead of a helper that folds it. */
57
+ export async function getOrgChartResponse(): Promise<{ status: number; chart: OrgChart | undefined }> {
58
+ const res = await driver.get('/v1/agents/org-chart');
59
+ const body = res.json;
60
+ return { status: res.status, chart: body && typeof body === 'object' ? (body as OrgChart) : undefined };
61
+ }
62
+
54
63
  /** GET the NORMATIVE org-chart (RFC 0087 §A `GET /v1/agents/org-chart`);
55
- * null when the host doesn't serve it (404/405/501). */
64
+ * null when the host doesn't serve it (404/405/501).
65
+ *
66
+ * unfailable-leg audit wave 2, 2026-09-27: every other status used to fold to
67
+ * `{}` — an advertised host answering 500/403 read as an EMPTY chart, which
68
+ * the tree/member loops then passed vacuously. Any other non-200 (or a 200
69
+ * with no object body) now throws, failing the calling leg. */
56
70
  export async function getOrgChart(): Promise<OrgChart | null> {
57
- const res = await driver.get('/v1/agents/org-chart');
58
- if (res.status === 404 || res.status === 405 || res.status === 501) return null;
59
- return (res.json as OrgChart | undefined) ?? {};
71
+ const { status, chart } = await getOrgChartResponse();
72
+ if (status === 404 || status === 405 || status === 501) return null;
73
+ if (status !== 200 || chart === undefined) {
74
+ throw new Error(`RFC 0087 §A: GET /v1/agents/org-chart MUST return 200 with the org-chart object on a host advertising agents.orgChart — got HTTP ${status}${chart === undefined ? ' with no JSON object body' : ''}`);
75
+ }
76
+ return chart;
60
77
  }
61
78
 
62
79
  /** GET a department's §D responsibility roll-up. `recursive` defaults to the
@@ -50,6 +50,20 @@ export interface EffectArrival {
50
50
  readonly at: number;
51
51
  /** False when the path carried some other exercise's nonce (or none). */
52
52
  readonly mine: boolean;
53
+ /** The `Idempotency-Key` header the host presented on this attempt, if any. */
54
+ readonly idempotencyKey?: string;
55
+ /** True when this receiver cut the connection instead of answering (a forced transport failure). */
56
+ readonly reset: boolean;
57
+ }
58
+
59
+ export interface EffectReceiverOptions {
60
+ /**
61
+ * Destroy the connection, without answering, on the first `failFirst` arrivals
62
+ * that carry this exercise's nonce. The host sees a genuine transport failure,
63
+ * not an HTTP status, so a host that retries only at the transport layer (as
64
+ * RFC 0173 §D.2 G4's seam describes) retries. Default 0.
65
+ */
66
+ readonly failFirst?: number;
53
67
  }
54
68
 
55
69
  export interface EffectReceiver {
@@ -98,7 +112,8 @@ export async function waitForFirstArrival(rx: EffectReceiver, budgetMs: number):
98
112
  * distinct per exercise whether the port is pinned, ephemeral, or replaced by
99
113
  * an operator's public front.
100
114
  */
101
- export async function startEffectReceiver(): Promise<EffectReceiver> {
115
+ export async function startEffectReceiver(opts: EffectReceiverOptions = {}): Promise<EffectReceiver> {
116
+ const failFirst = Math.max(0, Math.floor(opts.failFirst ?? 0));
102
117
  const nonce = randomBytes(9).toString('hex');
103
118
  const path = `/effect/${nonce}`;
104
119
  const seen: EffectArrival[] = [];
@@ -108,7 +123,13 @@ export async function startEffectReceiver(): Promise<EffectReceiver> {
108
123
  const requestPath = request.url ?? '';
109
124
  // Matched by CONTAINMENT, not equality: a TLS-terminating front may add
110
125
  // or strip a prefix, and the nonce is what identifies the exercise.
111
- seen.push({ path: requestPath, method: request.method ?? '', at: Date.now(), mine: requestPath.includes(nonce) });
126
+ const mine = requestPath.includes(nonce);
127
+ const key = request.headers['idempotency-key'];
128
+ const reset = mine && seen.filter((a) => a.mine).length < failFirst;
129
+ seen.push({ path: requestPath, method: request.method ?? '', at: Date.now(), mine, reset, ...(typeof key === 'string' ? { idempotencyKey: key } : {}) });
130
+ // The key is recorded BEFORE the cut, so a failed attempt still counts as
131
+ // an attempt the provider saw.
132
+ if (reset) { request.socket.destroy(); return; }
112
133
  res.writeHead(204);
113
134
  res.end();
114
135
  });
@@ -69,12 +69,36 @@ export interface ToolDescriptor {
69
69
  [k: string]: unknown;
70
70
  }
71
71
 
72
- /** GET the NORMATIVE tool catalog (RFC 0078 §B `GET /v1/tools`); null when the
73
- * host doesn't serve it (404/405/501). */
74
- export async function listTools(): Promise<ToolDescriptor[] | null> {
72
+ /** The outcome of one `GET /v1/tools` read (see `listTools`). */
73
+ export interface ToolListRead {
74
+ /** HTTP status of the read. */
75
+ readonly status: number;
76
+ /** `true` when the host does not serve the read at all (404/405/501). */
77
+ readonly unserved: boolean;
78
+ /** The descriptor list when the read was a `200` carrying a list; `null` otherwise. */
79
+ readonly tools: ToolDescriptor[] | null;
80
+ }
81
+
82
+ /** GET the NORMATIVE tool catalog (RFC 0078 §B `GET /v1/tools`).
83
+ *
84
+ * unfailable-leg audit wave 2, 2026-09-27: this used to return `json ?? []`
85
+ * for ANY status other than 404/405/501, so a host answering `500`, `403`, or
86
+ * `200` with a non-list body read as "an empty catalog" and every per-
87
+ * descriptor assertion ran over zero items. Now `tools` is non-null only for a
88
+ * `200` whose body is a `ToolDescriptor[]` — the bare array `openapi.yaml`,
89
+ * `spec/v2/core/tool-catalog.md`, and the reference host serve, or (major 1
90
+ * only) the `{ tools: ToolDescriptor[] }` envelope `spec/v1/tool-catalog.md`
91
+ * §B prose names — and the caller asserts on `status` / `tools`. */
92
+ export async function listTools(): Promise<ToolListRead> {
75
93
  const res = await driver.get(toolsPath());
76
- if (res.status === 404 || res.status === 405 || res.status === 501) return null;
77
- return (res.json as ToolDescriptor[] | undefined) ?? [];
94
+ const unserved = res.status === 404 || res.status === 405 || res.status === 501;
95
+ if (res.status !== 200) return { status: res.status, unserved, tools: null };
96
+ const body = res.json;
97
+ if (Array.isArray(body)) return { status: res.status, unserved, tools: body as ToolDescriptor[] };
98
+ if (targetMajor() !== 2 && body !== null && typeof body === 'object' && Array.isArray((body as { tools?: unknown }).tools)) {
99
+ return { status: res.status, unserved, tools: (body as { tools: ToolDescriptor[] }).tools };
100
+ }
101
+ return { status: res.status, unserved, tools: null };
78
102
  }
79
103
 
80
104
  /** GET one tool by id (RFC 0078 §B `GET /v1/tools/{toolId}`); returns
@@ -17,7 +17,7 @@
17
17
  import { describe, it, expect } from 'vitest';
18
18
  import { readManifestRuntimeCap, listManifestAgents, dispatchAgent } from '../lib/agentRuntime.js';
19
19
  import { req } from '../lib/requirement-ids.js';
20
- import { softSkip } from '../lib/soft-skip.js';
20
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
21
21
 
22
22
  describe('agent-manifest-runtime (RFC 0070)', () => {
23
23
  it('lists installed manifest agents and dispatches one with attributed events', async () => {
@@ -32,7 +32,16 @@ describe('agent-manifest-runtime (RFC 0070)', () => {
32
32
  ).toBe(true);
33
33
 
34
34
  const inv = await listManifestAgents();
35
- if (inv === null) return softSkip('blocked', 'precondition not met — `inv === null` returned early (seam absent — soft-skip) (seam, prior step, or fixture unavailable)'); // seam absent — soft-skip
35
+ // unfailable-leg audit wave 2, 2026-09-27: a 404/405/501 on GET /v1/agents
36
+ // was `softSkip('blocked')` after the installScope assert — a partial-witness
37
+ // PASS for a host that advertised manifestRuntime and served no inventory.
38
+ // node-packs.md §"Agent inventory (RFC 0072 §A — normative)": an advertiser
39
+ // MUST serve GET /v1/agents, so absence is a failure, not a missing seam.
40
+ expect(
41
+ inv,
42
+ req('openwop.it.agent-manifest-runtime.lists-installed-manifest-agents-and-dispatches-one-with-attributed-events', 'RFC 0072 §A / node-packs.md §Agent inventory', 'a host advertising agents.manifestRuntime.supported MUST serve GET /v1/agents (it answered 404/405/501)'),
43
+ ).not.toBeNull();
44
+ if (inv === null) throw new Error("unreachable: the assertion above fails first");
36
45
  const agents = inv.agents ?? [];
37
46
  expect(
38
47
  Array.isArray(agents),
@@ -56,12 +65,16 @@ describe('agent-manifest-runtime (RFC 0070)', () => {
56
65
  }
57
66
 
58
67
  const agentId = agents[0]?.agentId;
59
- if (typeof agentId !== 'string') return softSkip('blocked', 'precondition not met — `typeof agentId !== \'string\'` returned early (seam, prior step, or fixture unavailable)');
68
+ // unfailable-leg audit wave 2, 2026-09-27: this and the `res === null`
69
+ // return below were plain `softSkip('blocked')` after the inventory
70
+ // asserts, recording a partial-witness PASS for a leg whose dispatch half
71
+ // (terminal status + attributed agent.* events) was never observed.
72
+ if (typeof agentId !== 'string') return blockedDespiteAssertions( 'precondition not met — `typeof agentId !== \'string\'` returned early (seam, prior step, or fixture unavailable)');
60
73
 
61
74
  // Opaque-payload dispatch (validateHandoff:false) so the assertion is
62
75
  // independent of the chosen agent's handoff schema.
63
76
  const res = await dispatchAgent(agentId, { task: {}, validateHandoff: false, availableTools: [] });
64
- if (res === null) return softSkip('blocked', 'precondition not met — `res === null` returned early (seam absent — soft-skip) (seam, prior step, or fixture unavailable)'); // seam absent — soft-skip
77
+ if (res === null) return blockedDespiteAssertions( 'precondition not met — `res === null` returned early (seam absent — soft-skip) (seam, prior step, or fixture unavailable)'); // seam absent — soft-skip
65
78
  expect(
66
79
  res.status === 'completed' || res.status === 'escalated',
67
80
  req('openwop.it.agent-manifest-runtime.lists-installed-manifest-agents-and-dispatches-one-with-attributed-events', 'RFC 0070', 'dispatch MUST resolve to a terminal status (completed | escalated)'),
@@ -30,9 +30,8 @@
30
30
 
31
31
  import { describe, it, expect } from 'vitest';
32
32
  import { behaviorGate } from '../lib/behavior-gate.js';
33
- import { readOrgChartCap, getOrgChart, getDepartmentView } from '../lib/agentOrgChart.js';
33
+ import { readOrgChartCap, getOrgChartResponse, getDepartmentView, type OrgChart } from '../lib/agentOrgChart.js';
34
34
  import { req } from '../lib/requirement-ids.js';
35
- import { softSkip } from '../lib/soft-skip.js';
36
35
 
37
36
  const ROSTER_ID_RE = /^host:[a-z0-9][a-z0-9._-]*$/;
38
37
 
@@ -48,8 +47,22 @@ describe('agent-org-chart-scoping (RFC 0087 §A/§C/§D)', () => {
48
47
  ).toBe(true);
49
48
 
50
49
  // ---- Leg 1: normative read (black-box) -------------------------------
51
- const chart = await getOrgChart();
52
- if (chart === null) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `chart === null` returned early (advertised but read not served yet — soft-skip)'); // advertised but read not served yet — soft-skip
50
+ // unfailable-leg audit wave 2, 2026-09-27: a host that ADVERTISES
51
+ // agents.orgChart but 404s the normative read (or answers 500/403, which
52
+ // getOrgChart used to fold into an empty `{}` chart) previously passed —
53
+ // the 404 as `inapplicable`, the error status as a vacuous empty tree.
54
+ // The behaviorGate above already established the advertisement, so the
55
+ // read MUST now be served: any non-200 fails here.
56
+ const chartRes = await getOrgChartResponse();
57
+ expect(
58
+ chartRes.status,
59
+ req('openwop.it.agent-org-chart-scoping.serves-the-normative-org-chart-responsibility-roll-up-tree-shaped-and-tenant-sco', 'agent-org-chart.md §A / §E', 'a host advertising agents.orgChart.supported MUST serve GET /v1/agents/org-chart with 200'),
60
+ ).toBe(200);
61
+ expect(
62
+ chartRes.chart !== undefined,
63
+ req('openwop.it.agent-org-chart-scoping.serves-the-normative-org-chart-responsibility-roll-up-tree-shaped-and-tenant-sco', 'agent-org-chart.schema.json', 'GET /v1/agents/org-chart MUST return a JSON org-chart object'),
64
+ ).toBe(true);
65
+ const chart: OrgChart = chartRes.chart ?? {};
53
66
  const departments = chart.departments ?? [];
54
67
  const members = chart.members ?? [];
55
68
  expect(
@@ -100,6 +113,16 @@ describe('agent-org-chart-scoping (RFC 0087 §A/§C/§D)', () => {
100
113
  const probeDeptId = departments[0]?.departmentId;
101
114
  if (typeof probeDeptId === 'string') {
102
115
  const { status, view } = await getDepartmentView(probeDeptId);
116
+ // unfailable-leg audit wave 2, 2026-09-27: a host advertising
117
+ // `responsibilityView: true` whose GET /v1/agents/org-chart/{id} errored
118
+ // or 404'd for a department IN ITS OWN chart previously skipped the whole
119
+ // roll-up leg silently. The advertised view MUST be served.
120
+ if (cap?.responsibilityView === true) {
121
+ expect(
122
+ status,
123
+ req('openwop.it.agent-org-chart-scoping.serves-the-normative-org-chart-responsibility-roll-up-tree-shaped-and-tenant-sco', 'agent-org-chart.md §D / §E', 'a host advertising agents.orgChart.responsibilityView MUST serve GET /v1/agents/org-chart/{departmentId} with 200 for a department in the caller\'s chart'),
124
+ ).toBe(200);
125
+ }
103
126
  if (status === 200 && view) {
104
127
  expect(
105
128
  Array.isArray(view.responsibilities),
@@ -43,7 +43,7 @@ import {
43
43
  type TestEvent,
44
44
  } from '../lib/event-log-query.js';
45
45
  import { req } from '../lib/requirement-ids.js';
46
- import { softSkip } from '../lib/soft-skip.js';
46
+ import { blockedDespiteAssertions } from '../lib/soft-skip.js';
47
47
 
48
48
  const ROSTER_ID_RE = /^host:[a-z0-9][a-z0-9._-]*$/;
49
49
 
@@ -67,13 +67,23 @@ describe('agent-roster-attribution (RFC 0086 §B/§C)', () => {
67
67
  ).toBe(true);
68
68
 
69
69
  // ---- Leg 1: normative read (black-box on any roster host) -------------
70
+ // unfailable-leg audit wave 2, 2026-09-27: a host that ADVERTISED
71
+ // agents.roster.supported but 404/405/501'd the normative read used to
72
+ // soft-skip 'inapplicable' after the installScope assert — a partial-witness
73
+ // PASS. Advertised ⇒ the normative read MUST be served, so it now fails.
74
+ // Likewise `body.roster ?? []` made a body with NO roster[] pass the
75
+ // "MUST return a roster[] array" assert; the raw field is checked now.
70
76
  const body = await listRoster();
71
- if (body === null) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `body === null` returned early (host advertises roster but doesn\'t serve the read yet — soft-skip)'); // host advertises roster but doesn't serve the read yet — soft-skip
72
- const roster = body.roster ?? [];
73
77
  expect(
74
- Array.isArray(roster),
78
+ body !== null,
79
+ req('openwop.it.agent-roster-attribution.serves-the-normative-roster-attributes-a-portfolio-fire-content-free-ordered-and', 'agent-roster.md §B', 'a host advertising agents.roster.supported MUST serve GET /v1/agents/roster (got 404/405/501)'),
80
+ ).toBe(true);
81
+ if (body === null) throw new Error("unreachable: the assertion above fails first");
82
+ expect(
83
+ Array.isArray(body.roster),
75
84
  req('openwop.it.agent-roster-attribution.serves-the-normative-roster-attributes-a-portfolio-fire-content-free-ordered-and', 'agent-roster.md §B', 'GET /v1/agents/roster MUST return a roster[] array'),
76
85
  ).toBe(true);
86
+ const roster = Array.isArray(body.roster) ? body.roster : [];
77
87
  expect(
78
88
  body.total === roster.length,
79
89
  req('openwop.it.agent-roster-attribution.serves-the-normative-roster-attributes-a-portfolio-fire-content-free-ordered-and', 'agent-roster-response.schema.json', 'total MUST equal roster.length'),
@@ -103,12 +113,22 @@ describe('agent-roster-attribution (RFC 0086 §B/§C)', () => {
103
113
  }
104
114
 
105
115
  // ---- Leg 2: attribution + ordering (seam-gated) ----------------------
116
+ // unfailable-leg audit wave 2, 2026-09-27: when the event-log seam, the
117
+ // fire seam, or the event query was absent, this leg fell through with
118
+ // nothing observed and the whole `it` (named for attribution + ordering)
119
+ // recorded a full pass on the leg-1 read alone. Track whether attribution
120
+ // was actually observed; if not, the row records `blocked`.
121
+ let attributionObserved = false;
122
+ let attributionGap = 'event-log seam /v1/host/sample/test-events not available';
106
123
  if (await isEventLogSeamAvailable()) {
124
+ attributionGap = 'roster fire seam /v1/host/sample/roster/fire unwired (404/405) or returned no runId';
107
125
  // Scheduled portfolio fire.
108
126
  const fired = await fireRosterPortfolio({ triggerSource: 'schedule' });
109
127
  if (fired?.runId) {
110
128
  const q = await queryTestEvents(fired.runId);
129
+ attributionGap = 'event-log query for the fired run failed';
111
130
  if (q.ok) {
131
+ attributionObserved = true;
112
132
  const init = firstOf(q.events, ['roster.run.initiated']);
113
133
  expect(
114
134
  init !== undefined,
@@ -176,5 +196,9 @@ describe('agent-roster-attribution (RFC 0086 §B/§C)', () => {
176
196
  req('openwop.it.agent-roster-attribution.serves-the-normative-roster-attributes-a-portfolio-fire-content-free-ordered-and', 'agent-roster.md §B / RFC 0074', "GET /v1/agents/roster/{id} for a cross-tenant id MUST 404 (no cross-tenant disclosure)"),
177
197
  ).toBe(true);
178
198
  }
199
+
200
+ if (!attributionObserved) {
201
+ return blockedDespiteAssertions(`roster attribution + ordering (RFC 0086 §C) not observed — ${attributionGap}`);
202
+ }
179
203
  });
180
204
  });
@@ -15,6 +15,11 @@
15
15
  */
16
16
 
17
17
  import { describe, it, expect } from 'vitest';
18
+ import { readFileSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+ import Ajv2020 from 'ajv/dist/2020.js';
21
+ import addFormats from 'ajv-formats';
22
+ import { SCHEMAS_DIR } from '../lib/paths.js';
18
23
  import { driver } from '../lib/driver.js';
19
24
  import { isFixtureAdvertised } from '../lib/fixtures.js';
20
25
  import { isAgentSupported } from '../lib/multi-agent-capabilities.js';
@@ -24,6 +29,26 @@ import { req } from '../lib/requirement-ids.js';
24
29
  const FIXTURE = 'conformance-agent-pack-export';
25
30
  const SKIP = !isAgentSupported() || !isFixtureAdvertised(FIXTURE);
26
31
 
32
+ /**
33
+ * The AgentManifest validator (major 1 — this file is `[1]` in
34
+ * scenario-majors.json). Peers are pre-loaded exactly as `fixtures-valid`
35
+ * does: agent-manifest → prompt-ref → prompt-kind, under both the canonical
36
+ * `$id` and the relative file name.
37
+ */
38
+ function agentManifestValidator(): (doc: unknown) => { ok: boolean; errors: string } {
39
+ const load = (n: string): Record<string, unknown> => JSON.parse(readFileSync(join(SCHEMAS_DIR, n), 'utf8')) as Record<string, unknown>;
40
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
41
+ addFormats(ajv);
42
+ const promptRef = load('prompt-ref.schema.json');
43
+ const promptKind = load('prompt-kind.schema.json');
44
+ ajv.addSchema(promptRef, 'prompt-ref.schema.json');
45
+ ajv.addSchema(promptRef, './prompt-ref.schema.json');
46
+ ajv.addSchema(promptKind, 'prompt-kind.schema.json');
47
+ ajv.addSchema(promptKind, './prompt-kind.schema.json');
48
+ const validate = ajv.compile(load('agent-manifest.schema.json'));
49
+ return (doc: unknown) => ({ ok: validate(doc) as boolean, errors: ajv.errorsText(validate.errors, { separator: '; ' }) });
50
+ }
51
+
27
52
  describe.skipIf(SKIP)('agentPackExport: workspace agents project to AgentManifest', () => {
28
53
  it('exported manifests contain required AgentManifest fields', async () => {
29
54
  const res = await driver.get('/v1/packs/export');
@@ -39,8 +64,16 @@ describe.skipIf(SKIP)('agentPackExport: workspace agents project to AgentManifes
39
64
  const manifests = body.manifests ?? [];
40
65
  expect(manifests.length).toBeGreaterThan(0);
41
66
 
67
+ // unfailable-leg audit wave 2, 2026-09-27: only `typeof agentId ===
68
+ // 'string'` was checked, so a host exporting `[{ agentId: 'x' }]` — no
69
+ // persona, no modelClass, a non-namespaced id — passed a leg named
70
+ // "required AgentManifest fields". Each exported manifest now validates
71
+ // against schemas/agent-manifest.schema.json.
72
+ const validate = agentManifestValidator();
42
73
  for (const m of manifests) {
43
74
  expect(typeof m.agentId).toBe('string');
75
+ const v = validate(m);
76
+ expect(v.ok, req('openwop.it.agentPackExport.exported-manifests-contain-required-agentmanifest-fields', 'schemas/agent-manifest.schema.json', `exported manifest ${String(m.agentId)} MUST validate as an AgentManifest (${v.errors})`)).toBe(true);
44
77
  // Exported manifests SHOULD carry sourceManifestId provenance when
45
78
  // they originated from a prior install (covered by agentPackProvenance).
46
79
  }
@@ -16,6 +16,11 @@
16
16
  */
17
17
 
18
18
  import { describe, it, expect } from 'vitest';
19
+ import { readFileSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ import Ajv2020 from 'ajv/dist/2020.js';
22
+ import addFormats from 'ajv-formats';
23
+ import { SCHEMAS_DIR } from '../lib/paths.js';
19
24
  import { driver } from '../lib/driver.js';
20
25
  import { isFixtureAdvertised } from '../lib/fixtures.js';
21
26
  import { isAgentSupported } from '../lib/multi-agent-capabilities.js';
@@ -25,6 +30,26 @@ import { req } from '../lib/requirement-ids.js';
25
30
  const FIXTURE = 'conformance-agent-pack-install';
26
31
  const SKIP = !isAgentSupported() || !isFixtureAdvertised(FIXTURE);
27
32
 
33
+ /**
34
+ * The AgentManifest validator (major 1 — this file is `[1]` in
35
+ * scenario-majors.json). Peers are pre-loaded exactly as `fixtures-valid`
36
+ * does: agent-manifest → prompt-ref → prompt-kind, under both the canonical
37
+ * `$id` and the relative file name.
38
+ */
39
+ function agentManifestValidator(): (doc: unknown) => { ok: boolean; errors: string } {
40
+ const load = (n: string): Record<string, unknown> => JSON.parse(readFileSync(join(SCHEMAS_DIR, n), 'utf8')) as Record<string, unknown>;
41
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
42
+ addFormats(ajv);
43
+ const promptRef = load('prompt-ref.schema.json');
44
+ const promptKind = load('prompt-kind.schema.json');
45
+ ajv.addSchema(promptRef, 'prompt-ref.schema.json');
46
+ ajv.addSchema(promptRef, './prompt-ref.schema.json');
47
+ ajv.addSchema(promptKind, 'prompt-kind.schema.json');
48
+ ajv.addSchema(promptKind, './prompt-kind.schema.json');
49
+ const validate = ajv.compile(load('agent-manifest.schema.json'));
50
+ return (doc: unknown) => ({ ok: validate(doc) as boolean, errors: ajv.errorsText(validate.errors, { separator: '; ' }) });
51
+ }
52
+
28
53
  describe.skipIf(SKIP)('agentPackInstall: pack agents[] entries surface as AgentManifest', () => {
29
54
  it('host exposes installed agent manifests with required AgentManifest fields', async () => {
30
55
  // Host-specific pack-listing endpoint. The conformance suite probes
@@ -45,9 +70,16 @@ describe.skipIf(SKIP)('agentPackInstall: pack agents[] entries surface as AgentM
45
70
  const allAgents = packs.flatMap((p) => p.agents ?? []);
46
71
 
47
72
  expect(allAgents.length).toBeGreaterThan(0);
73
+ // unfailable-leg audit wave 2, 2026-09-27: only a >=3-char string agentId
74
+ // was checked, so a host listing `agents: [{ agentId: 'abc' }]` passed a
75
+ // leg named "required AgentManifest fields". Each surfaced agents[] entry
76
+ // now validates against schemas/agent-manifest.schema.json.
77
+ const validate = agentManifestValidator();
48
78
  for (const a of allAgents) {
49
79
  expect(typeof a.agentId).toBe('string');
50
80
  expect(a.agentId!.length).toBeGreaterThanOrEqual(3);
81
+ const v = validate(a);
82
+ expect(v.ok, req('openwop.it.agentPackInstall.host-exposes-installed-agent-manifests-with-required-agentmanifest-fields', 'schemas/agent-manifest.schema.json', `pack agents[] entry ${String(a.agentId)} MUST validate as an AgentManifest (${v.errors})`)).toBe(true);
51
83
  }
52
84
  });
53
85
  });
@@ -98,27 +98,36 @@ describe.skipIf(SKIP)('agentReasoningEvents: agent.* event family emission', ()
98
98
  // paired eventId through.
99
99
  const calls = agentEvents.filter((e) => e.type === 'agent.toolCalled');
100
100
  const returns = agentEvents.filter((e) => e.type === 'agent.toolReturned');
101
+ // unfailable-leg audit wave 2, 2026-09-27: a toolReturned with NO callId
102
+ // used to `continue` (skipping pairing entirely), and a matched toolCalled
103
+ // with no eventId skipped the causationId check — so a host that omitted
104
+ // both schema-required fields passed the whole pairing requirement.
105
+ // callId is required on agentToolReturned (run-event-payloads.schema.json)
106
+ // and eventId on every RunEventDoc (run-event.schema.json): both asserted.
101
107
  for (const ret of returns) {
102
- const callId = ret.payload?.callId as string | undefined;
103
- if (callId === undefined) continue;
108
+ const callId = ret.payload?.callId;
109
+ expect(
110
+ typeof callId === 'string' && callId.length > 0,
111
+ req('openwop.it.agentReasoningEvents.host-emits-at-least-one-canonical-agent-event-during-a-reasoning-fixture-run', 'schemas/run-event-payloads.schema.json#/$defs/agentToolReturned', 'agent.toolReturned MUST carry a callId (schema-required)'),
112
+ ).toBe(true);
104
113
  const matched = calls.find((c) => c.payload?.callId === callId);
105
114
  expect(
106
115
  matched,
107
116
  req('openwop.it.agentReasoningEvents.host-emits-at-least-one-canonical-agent-event-during-a-reasoning-fixture-run', 'schemas/run-event-payloads.schema.json', `agent.toolReturned.callId=${callId} MUST pair with a prior agent.toolCalled`),
108
117
  ).toBeDefined();
109
118
 
110
- // Strict causationId chain — only assert when the host actually
111
- // surfaces eventId on the matched toolCalled event. Hosts that
112
- // omit eventId from their `/events` projection skip this check
113
- // (and SHOULD add it — RFC 0002 §B's chain integrity depends on
114
- // it).
119
+ // Strict causationId chain. eventId is required on every RunEventDoc
120
+ // (run-event.schema.json), so a matched toolCalled without one is a
121
+ // projection defect, not a reason to skip RFC 0002 §B's chain check.
115
122
  const matchedEventId = matched?.eventId;
116
- if (typeof matchedEventId === 'string' && matchedEventId.length > 0) {
117
- expect(
118
- ret.causationId,
119
- req('openwop.it.agentReasoningEvents.host-emits-at-least-one-canonical-agent-event-during-a-reasoning-fixture-run', 'schemas/run-event-payloads.schema.json', `agent.toolReturned (callId=${callId}) MUST carry causationId === paired agent.toolCalled.eventId per RFC 0002 §B`),
120
- ).toBe(matchedEventId);
121
- }
123
+ expect(
124
+ typeof matchedEventId === 'string' && matchedEventId.length > 0,
125
+ req('openwop.it.agentReasoningEvents.host-emits-at-least-one-canonical-agent-event-during-a-reasoning-fixture-run', 'schemas/run-event.schema.json', `the agent.toolCalled paired with callId=${String(callId)} MUST carry an eventId (schema-required)`),
126
+ ).toBe(true);
127
+ expect(
128
+ ret.causationId,
129
+ req('openwop.it.agentReasoningEvents.host-emits-at-least-one-canonical-agent-event-during-a-reasoning-fixture-run', 'schemas/run-event-payloads.schema.json', `agent.toolReturned (callId=${String(callId)}) MUST carry causationId === paired agent.toolCalled.eventId per RFC 0002 §B`),
130
+ ).toBe(matchedEventId);
122
131
  }
123
132
  });
124
133
  });
@@ -85,10 +85,14 @@ describe('category: core.openwop.agents.run — tool-allowlist enforcement (OPEN
85
85
  it('skips cleanly when pack source is not bundled', () => {
86
86
  if (!packAvailable) {
87
87
  console.warn('[agents-run-tool-allowlist] pack source not present; skipping');
88
- expect(packAvailable, req('openwop.it.agents-run-tool-allowlist.skips-cleanly-when-pack-source-is-not-bundled', 'threat-model-prompt-injection.md', 'skips cleanly when pack source is not bundled')).toBe(false);
88
+ // unfailable-leg audit wave 2, 2026-09-27: this leg asserted
89
+ // `packAvailable === false` inside `if (!packAvailable)` (and `=== true`
90
+ // in the else) — tautologies that turned a sentinel into an
91
+ // `executed-pass` row measuring nothing. It now records `inapplicable`
92
+ // with zero assertions either way.
89
93
  return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!packAvailable` returned early ([agents-run-tool-allowlist] pack source not present; skipping)');
90
94
  }
91
- expect(packAvailable).toBe(true);
95
+ return softSkip('inapplicable', 'sentinel leg — pack source is present; the behavioural legs in this file carry the assertions');
92
96
  });
93
97
 
94
98
  it('rejects function-typed tool.handler (the defect path)', async () => {
@@ -68,15 +68,16 @@ describe.skipIf(HTTP_SKIP)('ai-envelope-shape: advertisement contract (RFC 0021
68
68
  it('capabilities.supportedEnvelopes is an array of strings (when present)', async () => {
69
69
  const d = await readDiscovery();
70
70
  if (d === null) return softSkip('blocked', 'precondition not met — `d === null` returned early (seam, prior step, or fixture unavailable)');
71
- const env = supportedEnvelopes(d);
72
- // No assertion if absent — the field is optional. When present, each entry MUST be a string.
73
- for (const k of env) {
74
- expect(typeof k, req('openwop.it.ai-envelope-shape.capabilities-supportedenvelopes-is-an-array-of-strings-when-present', 'capabilities.md §supportedEnvelopes', 'each entry MUST be a string')).toBe('string');
75
- }
76
- // Re-affirm shape: array if present (root-first per RFC 0073).
71
+ // unfailable-leg audit wave 2, 2026-09-27: this loop used to iterate
72
+ // `supportedEnvelopes(d)`, which had ALREADY filtered out every non-string
73
+ // entry — so a host advertising `supportedEnvelopes: [42, {}]` passed the
74
+ // "each entry MUST be a string" assertion. Iterate the RAW advertised array.
77
75
  const advertised = capabilityFamily<unknown>(d, 'supportedEnvelopes');
78
- if (advertised !== undefined) {
79
- expect(Array.isArray(advertised), req('openwop.it.ai-envelope-shape.capabilities-supportedenvelopes-is-an-array-of-strings-when-present', 'capabilities.md §supportedEnvelopes', 'supportedEnvelopes MUST be an array')).toBe(true);
76
+ // The field is optional: absent is not a violation, but nothing is observed.
77
+ if (advertised === undefined) return softSkip('inapplicable', 'host does not advertise supportedEnvelopes (optional field)');
78
+ expect(Array.isArray(advertised), req('openwop.it.ai-envelope-shape.capabilities-supportedenvelopes-is-an-array-of-strings-when-present', 'capabilities.md §supportedEnvelopes', 'supportedEnvelopes MUST be an array')).toBe(true);
79
+ for (const k of advertised as unknown[]) {
80
+ expect(typeof k, req('openwop.it.ai-envelope-shape.capabilities-supportedenvelopes-is-an-array-of-strings-when-present', 'capabilities.md §supportedEnvelopes', 'each entry MUST be a string')).toBe('string');
80
81
  }
81
82
  });
82
83
  });
@@ -24,7 +24,7 @@ import { describe, it, expect } from 'vitest';
24
24
  import { behaviorGate } from '../lib/behavior-gate.js';
25
25
  import { readAnonymousActorCap, anonDispatch } from '../lib/anonymousActor.js';
26
26
  import { req } from '../lib/requirement-ids.js';
27
- import { softSkip } from '../lib/soft-skip.js';
27
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
28
28
 
29
29
  const PROFILE = 'openwop-anonymous-actor';
30
30
 
@@ -74,11 +74,17 @@ describe('anonymous-actor-write-gated (RFC 0132 §C.3)', () => {
74
74
  decided?.allowed === false || res.status === 403 || res.status === 429,
75
75
  req('openwop.it.anonymous-actor-write-gated.an-anon-write-with-no-resolvable-control-is-denied-with-a-machine-reason', 'SECURITY anon-actor-write-egress-gated', 'an anon write with no resolvable control MUST be denied'),
76
76
  ).toBe(true);
77
- if (decided && decided.allowed === false) {
78
- expect(
79
- decided.reason,
80
- req('openwop.it.anonymous-actor-write-gated.an-anon-write-with-no-resolvable-control-is-denied-with-a-machine-reason', 'RFC 0132 §C.3', 'an ungated anon write denial carries a machine reason'),
81
- ).toBe('anon-write-ungated');
77
+ // unfailable-leg audit wave 2, 2026-09-27: when the seam surfaced no
78
+ // `authorizationDecided` denial (a bare 403/429), the machine-reason check
79
+ // was silently skipped and the leg passed — a host whose denial carried NO
80
+ // reason (or the wrong one) passed "denied with a machine reason". The
81
+ // reason is now either asserted or the leg records `blocked`.
82
+ if (!decided || decided.allowed !== false) {
83
+ return blockedDespiteAssertions('the denial surfaced no authorizationDecided { allowed: false } payload, so the machine reason (anon-write-ungated) was not observed');
82
84
  }
85
+ expect(
86
+ decided.reason,
87
+ req('openwop.it.anonymous-actor-write-gated.an-anon-write-with-no-resolvable-control-is-denied-with-a-machine-reason', 'RFC 0132 §C.3', 'an ungated anon write denial carries a machine reason'),
88
+ ).toBe('anon-write-ungated');
83
89
  });
84
90
  });