@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
|
@@ -69,6 +69,13 @@ describe('v2 sse-last-event-id (events.md §SSE frames)', () => {
|
|
|
69
69
|
expect(ids.every((n) => n !== null), req(ID, DOC, `every frame MUST carry id: = its sequence (frames without a parseable id: ${ids.filter((n) => n === null).length} of ${ids.length})`)).toBe(true);
|
|
70
70
|
const seqs = ids as number[];
|
|
71
71
|
expect([...seqs].sort((a, b) => a - b), req(ID, DOC, 'frames MUST arrive in log order')).toEqual(seqs);
|
|
72
|
+
// unfailable-leg audit wave 2, 2026-09-27: with a single frame, `first` ===
|
|
73
|
+
// `last` and the mid-stream resume leg compared two empty sets — a host
|
|
74
|
+
// that ignored Last-Event-ID on a resume with backlog could not fail it. A
|
|
75
|
+
// completed run's log holds at least `run.started` (events.md: it carries
|
|
76
|
+
// the owner block) and its terminal event (§The terminal event), and debug
|
|
77
|
+
// mode streams every event, so ≥2 frames is required, not assumed.
|
|
78
|
+
expect(seqs.length, req(ID, DOC, `a completed run's debug stream MUST carry at least run.started and its terminal event (got ${seqs.length} frame(s))`)).toBeGreaterThanOrEqual(2);
|
|
72
79
|
const first = seqs[0]!; const last = seqs[seqs.length - 1]!;
|
|
73
80
|
|
|
74
81
|
const resumed = await subscribe(path, { timeoutMs: 8_000, extraHeaders: V2, lastEventId: String(first) });
|
|
@@ -145,10 +145,29 @@ describe('v2-version-header-honored (RFC 0172 §A.3)', () => {
|
|
|
145
145
|
// A refusal is a correct answer and ends the check: the host has told the
|
|
146
146
|
// truth about not serving major 2.
|
|
147
147
|
if (asked.status === 406) {
|
|
148
|
+
// unfailable-leg audit wave 2, 2026-09-27: this branch asserted only the
|
|
149
|
+
// status it had just branched on (`406 === 406`), so ANY 406 passed — a
|
|
150
|
+
// host that lists `2.x` in protocolVersions[] and still refuses it
|
|
151
|
+
// (§1.3 row 1: MUST serve a listed major), or one answering 406 with a
|
|
152
|
+
// bare `{}` body. The refusal is now held to §1.3 row 2 exactly as the
|
|
153
|
+
// retired-host branch above is: code `protocol_version_unsupported`,
|
|
154
|
+
// `details.protocolVersions[]` echoing the list, and major 2 NOT listed.
|
|
155
|
+
let refusal: unknown;
|
|
156
|
+
try {
|
|
157
|
+
refusal = JSON.parse(asked.body);
|
|
158
|
+
} catch {
|
|
159
|
+
refusal = undefined;
|
|
160
|
+
}
|
|
161
|
+
const echoed406 = (refusal as { details?: { protocolVersions?: unknown } } | undefined)?.details?.protocolVersions;
|
|
148
162
|
expect(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
163
|
+
{
|
|
164
|
+
status: asked.status,
|
|
165
|
+
code: readErrorCode(refusal),
|
|
166
|
+
echoed: Array.isArray(echoed406) ? [...echoed406].map(String).sort() : echoed406,
|
|
167
|
+
listsMajor2: majors.has('2'),
|
|
168
|
+
},
|
|
169
|
+
req(ID, DOC, `a host that does not serve major 2 MUST refuse OpenWOP-Version: 2.0 with 406 protocol_version_unsupported and details.protocolVersions[] echoing [${advertised.join(', ')}]; a host that LISTS a 2.x member MUST serve it (§1.3 row 1), not refuse it`),
|
|
170
|
+
).toEqual({ status: 406, code: 'protocol_version_unsupported', echoed: [...advertised].sort(), listsMajor2: false });
|
|
152
171
|
return softSkip('inapplicable', 'the host refused major 2 with the specified 406 — a correct answer, and there is no second representation to compare bytes against');
|
|
153
172
|
}
|
|
154
173
|
if (asked.status !== 200) {
|
|
@@ -459,6 +459,14 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
459
459
|
).toBeLessThanOrEqual(policy.maxAttempts);
|
|
460
460
|
}
|
|
461
461
|
if (inSink === null) {
|
|
462
|
+
// partial-witness-ok: the retry half of §Durability is observed above
|
|
463
|
+
// (retried, and capped at maxAttempts). The sink half has no read on a
|
|
464
|
+
// host without the webhooks.deadLetter facet (RFC 0188 §A.5 makes the read
|
|
465
|
+
// a 404, not an obligation), so it is unwitnessable here BY DESIGN. It is
|
|
466
|
+
// not a harness gap that `blocked` would honestly name: a bundle-fatal
|
|
467
|
+
// `blocked` would deny certification to every conforming host that
|
|
468
|
+
// declines the optional read. Reviewed in the unfailable-leg audit wave 2
|
|
469
|
+
// (2026-09-27); the acceptance predicate still refuses this row.
|
|
462
470
|
return softSkip('inapplicable', 'host does not advertise the webhooks.deadLetter facet — RFC 0188 §A.5 makes the read a 404 rather than an obligation, so the sink half of §Durability is unwitnessable here (the retry half above passed)');
|
|
463
471
|
}
|
|
464
472
|
expect(
|
|
@@ -171,8 +171,13 @@ describe('RFC 0215 §B — unregistering stops the attempts (gated on webhooks)'
|
|
|
171
171
|
targetLate.length,
|
|
172
172
|
req(ID, DOC, `after unregisterWebhook answers 204 the host MUST NOT start another attempt for that subscription, including retries already scheduled — ${detail}`),
|
|
173
173
|
).toBe(0);
|
|
174
|
+
// unfailable-leg audit wave 2, 2026-09-27: a plain softSkip after the
|
|
175
|
+
// `targetLate === 0` assert recorded a partial-witness `executed-pass`, so a
|
|
176
|
+
// host that KEPT delivering to an unregistered subscription — but whose
|
|
177
|
+
// retries fell outside the window — certified. Without a late control
|
|
178
|
+
// attempt the target's silence proves nothing: `blocked`.
|
|
174
179
|
if (controlLate.length === 0) {
|
|
175
|
-
return
|
|
180
|
+
return blockedDespiteAssertions(`the control subscription was not retried after the 204 + ${GRACE_MS}ms within ${windowMs}ms either, so the target's silence shows nothing — the schedule finished inside the grace or runs past the window (raise OPENWOP_WEBHOOK_RETRY_WAIT_MS above the host's backoff sum) — ${detail}`);
|
|
176
181
|
}
|
|
177
182
|
}, CAP_MS * 2 + 90_000);
|
|
178
183
|
});
|
|
@@ -74,7 +74,18 @@ describe('version-negotiation: Capabilities advertises a protocolVersion', () =>
|
|
|
74
74
|
'capabilities.md §3 + version-negotiation.md',
|
|
75
75
|
'Capabilities.protocolVersion MUST be a non-empty string',
|
|
76
76
|
)).toBe('string');
|
|
77
|
-
|
|
77
|
+
// unfailable-leg audit wave 2, 2026-09-27: only non-emptiness was checked,
|
|
78
|
+
// so `v1.0`, `1.0.0`, `1` and `banana` passed. RFC 0149 §C constrains
|
|
79
|
+
// protocolVersion to ASCII `<major>.<minor>` with no leading zeros —
|
|
80
|
+
// capabilities.schema.json `protocolVersion.pattern` (the same grammar
|
|
81
|
+
// lib/profiles.ts PROTOCOL_VERSION_GRAMMAR applies to the core predicate).
|
|
82
|
+
expect(
|
|
83
|
+
typeof caps.protocolVersion === 'string' && /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(caps.protocolVersion),
|
|
84
|
+
req('openwop.it.version-negotiation.get-well-known-openwop-returns-capabilities-with-protocolversion-string',
|
|
85
|
+
'RFC 0149 §C; capabilities.schema.json protocolVersion.pattern',
|
|
86
|
+
`Capabilities.protocolVersion MUST be ASCII <major>.<minor> with no leading zeros (got ${JSON.stringify(caps.protocolVersion)})`,
|
|
87
|
+
),
|
|
88
|
+
).toBe(true);
|
|
78
89
|
});
|
|
79
90
|
});
|
|
80
91
|
|
|
@@ -118,6 +118,8 @@ export const REQUIRES_HOST_CALLBACK = 'the host POSTs a signed webhook delivery
|
|
|
118
118
|
interface DeliveredRequest {
|
|
119
119
|
readonly headers: Record<string, string>;
|
|
120
120
|
readonly body: string;
|
|
121
|
+
/** Suite clock (ms) when the receiver recorded the delivery. */
|
|
122
|
+
readonly receivedAtMs: number;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
async function startReceiver(): Promise<ScopedReceiver & { received: DeliveredRequest[] }> {
|
|
@@ -141,7 +143,7 @@ async function startReceiver(): Promise<ScopedReceiver & { received: DeliveredRe
|
|
|
141
143
|
if (typeof v === 'string') headers[k.toLowerCase()] = v;
|
|
142
144
|
else if (Array.isArray(v)) headers[k.toLowerCase()] = v.join(',');
|
|
143
145
|
}
|
|
144
|
-
received.push({ headers, body: hit.body });
|
|
146
|
+
received.push({ headers, body: hit.body, receivedAtMs: Date.now() });
|
|
145
147
|
res.writeHead(204);
|
|
146
148
|
res.end();
|
|
147
149
|
});
|
|
@@ -352,10 +354,23 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
|
|
|
352
354
|
).toBe(sub.webhookId);
|
|
353
355
|
|
|
354
356
|
const timestamp = first.headers['x-openwop-timestamp'];
|
|
357
|
+
// unfailable-leg audit wave 2, 2026-09-27: this only required a non-empty
|
|
358
|
+
// string, so an ISO-8601 date, a milliseconds value, or a stale constant
|
|
359
|
+
// passed "MUST be a Unix-seconds integer" (the HMAC below signs whatever
|
|
360
|
+
// string was sent, so it could not catch it). Now: digits only (≤ 11 —
|
|
361
|
+
// seconds, not ms) and within the ±5 min window webhooks.md §"Verification
|
|
362
|
+
// recipe" tells every subscriber to enforce (a delivery outside it is
|
|
363
|
+
// rejected by a conforming subscriber, so the host's "when the dispatcher
|
|
364
|
+
// signed the body" value must fall inside it at receipt).
|
|
355
365
|
expect(
|
|
356
|
-
typeof timestamp === 'string' && timestamp
|
|
357
|
-
req('openwop.it.webhook-signed-delivery.host-posts-run-events-to-subscriber-with-valid-x-openwop-signature', 'webhooks.md §"Delivery headers"',
|
|
366
|
+
typeof timestamp === 'string' && /^\d{1,11}$/.test(timestamp),
|
|
367
|
+
req('openwop.it.webhook-signed-delivery.host-posts-run-events-to-subscriber-with-valid-x-openwop-signature', 'webhooks.md §"Delivery headers"', `X-openwop-Timestamp MUST be a Unix-seconds integer string (got ${JSON.stringify(timestamp)})`),
|
|
358
368
|
).toBe(true);
|
|
369
|
+
const skewSeconds = Math.abs(Number(timestamp) - first.receivedAtMs / 1000);
|
|
370
|
+
expect(
|
|
371
|
+
skewSeconds,
|
|
372
|
+
req('openwop.it.webhook-signed-delivery.host-posts-run-events-to-subscriber-with-valid-x-openwop-signature', 'webhooks.md §"Delivery headers"; §"Verification recipe" step 1 (±5 minutes)', `X-openwop-Timestamp MUST be the signing time — within ±300 s of receipt (off by ${Math.round(skewSeconds)} s)`),
|
|
373
|
+
).toBeLessThan(300);
|
|
359
374
|
|
|
360
375
|
const signature = first.headers['x-openwop-signature'] ?? '';
|
|
361
376
|
expect(
|