@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.
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +292 -103
- package/schemas/CORPUS-STAMP.json +9 -9
- package/src/lib/agentOrgChart.ts +21 -4
- package/src/lib/effect-receiver.ts +23 -2
- package/src/lib/toolCatalog.ts +29 -5
- package/src/scenarios/agent-manifest-runtime.test.ts +17 -4
- package/src/scenarios/agent-org-chart-scoping.test.ts +27 -4
- package/src/scenarios/agent-roster-attribution.test.ts +28 -4
- package/src/scenarios/agentPackExport.test.ts +33 -0
- package/src/scenarios/agentPackInstall.test.ts +32 -0
- package/src/scenarios/agentReasoningEvents.test.ts +22 -13
- package/src/scenarios/agents-run-tool-allowlist.test.ts +6 -2
- package/src/scenarios/ai-envelope-shape.test.ts +9 -8
- package/src/scenarios/anonymous-actor-write-gated.test.ts +12 -6
- package/src/scenarios/audit-log-integrity.test.ts +37 -0
- package/src/scenarios/context-budget-transcript-bound.test.ts +14 -4
- package/src/scenarios/context-summarization-replay.test.ts +17 -8
- package/src/scenarios/conversationLifecycle.test.ts +12 -1
- package/src/scenarios/conversationReplayDeterminism.test.ts +19 -3
- package/src/scenarios/cost-attribution.test.ts +12 -0
- package/src/scenarios/debug-bundle-truncation.test.ts +16 -3
- package/src/scenarios/distillation-shape.test.ts +30 -0
- package/src/scenarios/envelope-truncated.test.ts +6 -2
- package/src/scenarios/feedback-fork-not-copied.test.ts +28 -7
- package/src/scenarios/idempotency-concurrent-claim.test.ts +27 -9
- package/src/scenarios/idempotency-key-determinism.test.ts +6 -2
- package/src/scenarios/mcp-server-untrusted-args.test.ts +20 -5
- package/src/scenarios/mcp-tool-roundtrip.test.ts +7 -21
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +11 -3
- package/src/scenarios/orchestratorDispatch.test.ts +18 -2
- package/src/scenarios/pack-registry.test.ts +8 -1
- package/src/scenarios/production-backpressure.test.ts +17 -11
- package/src/scenarios/redaction.test.ts +8 -2
- package/src/scenarios/replay-fanout-suppression.test.ts +18 -2
- package/src/scenarios/runner-ledger.test.ts +7 -2
- package/src/scenarios/stream-modes.test.ts +27 -0
- package/src/scenarios/tool-catalog-projection.test.ts +35 -12
- package/src/scenarios/v2-effect-identity-business-key.test.ts +40 -6
- package/src/scenarios/v2-era-2-append-vocabulary.test.ts +12 -3
- package/src/scenarios/v2-idempotency-in-flight.test.ts +16 -0
- package/src/scenarios/v2-mcp-tasks.test.ts +17 -3
- package/src/scenarios/v2-min-client-version.test.ts +11 -1
- package/src/scenarios/v2-run-diff-identical.test.ts +13 -5
- package/src/scenarios/v2-run-pause-resume.test.ts +4 -1
- package/src/scenarios/v2-sse-last-event-id.test.ts +7 -0
- package/src/scenarios/v2-version-header-honored.test.ts +22 -3
- package/src/scenarios/v2-webhook-durable-delivery.test.ts +8 -0
- package/src/scenarios/v2-webhook-unregister-stops-delivery.test.ts +6 -1
- package/src/scenarios/version-negotiation.test.ts +12 -1
- 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.
|
|
5
|
-
"corpusTag": "v2.42.
|
|
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": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
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": "
|
|
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": "
|
|
297
|
+
"spec/v2/release.json": "5132e4752b9e36d2ab61076455e3efbda31127e76565faadb67446f77707a246",
|
|
298
298
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
299
|
-
"spec/v2/surface-baseline.json": "
|
|
299
|
+
"spec/v2/surface-baseline.json": "ef98caceff85da0d8bae10e78d044762d47960432b7fa885c4c56697c7038274"
|
|
300
300
|
},
|
|
301
|
-
"corpusCommit": "
|
|
301
|
+
"corpusCommit": "bfa55619a403957a27412e0fa2c8865a48199c38"
|
|
302
302
|
}
|
package/src/lib/agentOrgChart.ts
CHANGED
|
@@ -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
|
|
58
|
-
if (
|
|
59
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/lib/toolCatalog.ts
CHANGED
|
@@ -69,12 +69,36 @@ export interface ToolDescriptor {
|
|
|
69
69
|
[k: string]: unknown;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
52
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
103
|
-
|
|
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
|
|
111
|
-
//
|
|
112
|
-
//
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
});
|