@openwop/openwop-conformance 1.136.4 → 1.136.6
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/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
|
|
3
|
-
"suiteVersion": "1.136.
|
|
4
|
-
"corpusCommit": "
|
|
3
|
+
"suiteVersion": "1.136.6",
|
|
4
|
+
"corpusCommit": "8f907101a7f1b6b630c0d1f2271b6e60d1e7c8fe"
|
|
5
5
|
}
|
|
@@ -306,6 +306,26 @@ export function expandChain(chain: WorkflowChain, ctx: ExpansionContext): Expand
|
|
|
306
306
|
// If it is gated on an advertised capability, it goes below.
|
|
307
307
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
308
308
|
|
|
309
|
+
// ─── Begin the MIRRORED COMPENSATION pass ───────────────────────────────────
|
|
310
|
+
//
|
|
311
|
+
// SP-01 (2026-08-18): this pass is a SECOND byte-mirrored region, gated by
|
|
312
|
+
// `scripts/check-workflow-chain-expansion-sync.mjs` alongside the core above.
|
|
313
|
+
//
|
|
314
|
+
// It sits below "End of the MIRRORED CORE" because it composes on the core's
|
|
315
|
+
// OUTPUT rather than editing it — but unlike the capability-gated surfaces that
|
|
316
|
+
// follow (RFC 0124 deferred parameters, RFC 0133 sub-chains, which a host that
|
|
317
|
+
// does not advertise them MUST REFUSE rather than implement), carrying a
|
|
318
|
+
// chain's compensation declaration is UNCONDITIONAL: `compensation.md`
|
|
319
|
+
// §"Workflow policy" makes the declaration descriptive, so a host that does not
|
|
320
|
+
// advertise `capabilities.compensation` still MUST carry it verbatim and refuse
|
|
321
|
+
// only a chain-level POLICY (`capability_required`). Unconditional ⇒ genuinely
|
|
322
|
+
// mirrorable ⇒ gated. RFC 0157's own commit claimed this pass was already
|
|
323
|
+
// "CI-gated against the reference host"; it was not — the gate stopped at the
|
|
324
|
+
// core sentinel — so the mirror never received it. Anything added between the
|
|
325
|
+
// two sentinels below MUST be mirrored in the reference host — that is the
|
|
326
|
+
// whole RFC 0157 unit: the types, the two error classes, the helpers, and
|
|
327
|
+
// `carryCompensation` / `expandChainWithCompensation`.
|
|
328
|
+
|
|
309
329
|
// ---------------------------------------------------------------------------
|
|
310
330
|
// RFC 0157 (RFC 0013 revision × RFC 0151 §B) — chain fragments carry
|
|
311
331
|
// compensation.
|
|
@@ -525,6 +545,8 @@ export function expandChainWithCompensation(
|
|
|
525
545
|
return { ...expanded, nodes: carried.nodes, settingsCompensation: carried.settingsCompensation };
|
|
526
546
|
}
|
|
527
547
|
|
|
548
|
+
// ─── End of the MIRRORED COMPENSATION pass ──────────────────────────────────
|
|
549
|
+
|
|
528
550
|
// ---------------------------------------------------------------------------
|
|
529
551
|
// RFC 0124 (WCP4) — Portable per-run parameter deferral.
|
|
530
552
|
//
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { describe, it, expect } from 'vitest';
|
|
18
18
|
import { driver } from '../lib/driver.js';
|
|
19
19
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
20
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
20
21
|
|
|
21
22
|
const WORKFLOW_ID = 'conformance-idempotent';
|
|
22
23
|
const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
|
|
@@ -85,5 +86,44 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + different body conflic
|
|
|
85
86
|
'idempotency.md §Layer 1',
|
|
86
87
|
'same Idempotency-Key with a different body MUST return 409',
|
|
87
88
|
)).toBe(409);
|
|
89
|
+
|
|
90
|
+
// SP-03 (2026-08-18): the code, not just the status. `idempotency.md`
|
|
91
|
+
// named NO mismatch error until v1.5, and implementations diverged exactly
|
|
92
|
+
// as an unnamed error invites: `grpc-transport.md` mapped BOTH
|
|
93
|
+
// `idempotency_key_conflict` and `idempotency_key_mismatch` in one row, the
|
|
94
|
+
// published TypeScript SDK's `HTTP_ERROR_CODES` carried
|
|
95
|
+
// `idempotency_key_mismatch`, the SQLite reference host emitted
|
|
96
|
+
// `idempotency_key_conflict`, and a tier-1 host emitted
|
|
97
|
+
// `idempotency_key_replay_mismatch` — a spelling in no corpus artifact at
|
|
98
|
+
// all. This leg asserted the status alone, so every one of them passed.
|
|
99
|
+
//
|
|
100
|
+
// `idempotency_key_mismatch` is canonical (idempotency.md §"Record shape,
|
|
101
|
+
// digest, and lease"): the only spelling already in more than one shipped
|
|
102
|
+
// artifact. The two legacy spellings are TOLERATED here through the first
|
|
103
|
+
// minor after 2026-11-10 so a converging host is not red on a rename it is
|
|
104
|
+
// mid-flight on — the same deprecation shape S22 used for the error
|
|
105
|
+
// envelope. Remove the tolerance then; the canonical assertion stays.
|
|
106
|
+
const code = readErrorCode(conflict.json);
|
|
107
|
+
const LEGACY = ['idempotency_key_conflict', 'idempotency_key_replay_mismatch'];
|
|
108
|
+
expect(
|
|
109
|
+
code === 'idempotency_key_mismatch' || LEGACY.includes(code ?? ''),
|
|
110
|
+
driver.describe(
|
|
111
|
+
'idempotency.md §"Record shape, digest, and lease"',
|
|
112
|
+
`a different request digest under the same scoped key MUST fail with the canonical ` +
|
|
113
|
+
`\`idempotency_key_mismatch\` (legacy \`${LEGACY.join('` / `')}\` tolerated through the ` +
|
|
114
|
+
`first minor after 2026-11-10); got \`${code ?? '<none>'}\``,
|
|
115
|
+
),
|
|
116
|
+
).toBe(true);
|
|
117
|
+
|
|
118
|
+
// …and MUST NOT hand back the first run's body. Answering a question the
|
|
119
|
+
// caller did not ask is the read half of the same confusion.
|
|
120
|
+
const conflictRunId = (conflict.json as { runId?: unknown } | undefined)?.runId;
|
|
121
|
+
expect(
|
|
122
|
+
conflictRunId,
|
|
123
|
+
driver.describe(
|
|
124
|
+
'idempotency.md §"Record shape, digest, and lease"',
|
|
125
|
+
'a digest mismatch MUST NOT return the cached body (no runId on the 409)',
|
|
126
|
+
),
|
|
127
|
+
).toBeUndefined();
|
|
88
128
|
});
|
|
89
129
|
});
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { gzipSync } from 'node:zlib';
|
|
24
25
|
import { driver } from '../lib/driver.js';
|
|
25
26
|
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
26
27
|
import { recordRequirement } from '../lib/requirement-ledger.js';
|
|
@@ -81,6 +82,14 @@ function freshPackName(scope: string = 'core'): string {
|
|
|
81
82
|
* impl PR will likely extend the driver with an octet-stream variant.
|
|
82
83
|
* The shape-only error-catalog tests below only need the host's first
|
|
83
84
|
* validation step (URL pattern, body-presence, etc.) to fire. */
|
|
85
|
+
/**
|
|
86
|
+
* S45 — a VALID gzip stream whose decompressed size (64 MiB of zeros) exceeds
|
|
87
|
+
* the recommended registry cap (50 MB) while the wire body stays ~64 KB, so no
|
|
88
|
+
* proxy body limit is reached and the host's own decompressed-bytes cap is the
|
|
89
|
+
* only thing that can refuse it. Built once per file.
|
|
90
|
+
*/
|
|
91
|
+
const OVERSIZED_GZIP: Buffer = gzipSync(Buffer.alloc(64 * 1024 * 1024, 0));
|
|
92
|
+
|
|
84
93
|
async function putTest(name: string, version: string, body: unknown, extraHeaders: Record<string, string> = {}) {
|
|
85
94
|
const res = await driver.put(`/v1/packs-test/${encodeURIComponent(name)}/-/${encodeURIComponent(version)}.tgz`, body, {
|
|
86
95
|
headers: { 'Content-Type': 'application/octet-stream', ...extraHeaders },
|
|
@@ -196,14 +205,29 @@ describe('pack-registry-publish: tarball extraction error catalog (RFC 0025)', (
|
|
|
196
205
|
|
|
197
206
|
it('PUT with decompressed bytes exceeding the registry\'s cap MUST return 400 tarball_too_large', async () => {
|
|
198
207
|
if (!(await isTestModeAdvertised())) return;
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
208
|
+
// S45 (2026-08-18): the cap is on DECOMPRESSED bytes (node-packs.md
|
|
209
|
+
// §"Tarball extraction", recommended default 50 MB), so the probe is a
|
|
210
|
+
// small VALID gzip stream that inflates past it — 64 MiB of zeros gzips
|
|
211
|
+
// to ~64 KB on the wire. The previous probe was a 60 MB body of fake
|
|
212
|
+
// gzip magic: a load balancer in front of the host (Cloud Run's front
|
|
213
|
+
// end caps HTTP/1 bodies at 32 MiB) answered its own HTML 413 before the
|
|
214
|
+
// host saw the request, the leg blew the 30 s budget on a normal uplink,
|
|
215
|
+
// and — because `tarball_gunzip_failed` was also accepted — a host with
|
|
216
|
+
// NO cap at all passed it. Measured by MyndHyve on `api.myndhyve.ai`.
|
|
217
|
+
// Only `tarball_too_large` is accepted now: a real gzip that inflates to
|
|
218
|
+
// 64 MiB either trips the cap or reaches the tar layer, and reaching the
|
|
219
|
+
// tar layer means the cap is absent or above 64 MiB (> the recommended
|
|
220
|
+
// default) — say so in the run record if that is a deliberate host choice.
|
|
221
|
+
const res = await putTest(freshPackName(), '1.0.0', OVERSIZED_GZIP);
|
|
204
222
|
if (res.status === 404) return;
|
|
205
|
-
expect(
|
|
206
|
-
|
|
223
|
+
expect(
|
|
224
|
+
res.status,
|
|
225
|
+
driver.describe('node-packs.md §"Tarball extraction"', 'a valid gzip inflating past the decompressed cap MUST be refused with 400 (a 413 here means a proxy refused the wire body — this probe is ~64 KB on the wire, so that is a host/proxy misconfiguration, not the cap)'),
|
|
226
|
+
).toBe(400);
|
|
227
|
+
expect(
|
|
228
|
+
errorCode(res.json),
|
|
229
|
+
driver.describe('node-packs.md §"Tarball extraction"', 'decompressed bytes exceeding the registry cap MUST surface tarball_too_large (recommended default cap 50 MB; this probe inflates to 64 MiB)'),
|
|
230
|
+
).toBe('tarball_too_large');
|
|
207
231
|
});
|
|
208
232
|
|
|
209
233
|
it('PUT with no `pack.json` at the tarball root MUST return 400 tarball_manifest_missing', async () => {
|
|
@@ -152,7 +152,7 @@ describe('redaction: 401 response MUST NOT echo invalid Bearer token (NFR-7)', (
|
|
|
152
152
|
// are observable surfaces and MUST be canary-clean.
|
|
153
153
|
const res = await driver.post(
|
|
154
154
|
'/v1/runs',
|
|
155
|
-
{ workflowId: NOOP_WORKFLOW_ID
|
|
155
|
+
{ workflowId: NOOP_WORKFLOW_ID },
|
|
156
156
|
{
|
|
157
157
|
authenticated: false,
|
|
158
158
|
headers: { Authorization: `Bearer ${canaryValue}` },
|
|
@@ -170,7 +170,7 @@ describe('redaction: 401 response MUST NOT echo invalid Bearer token (NFR-7)', (
|
|
|
170
170
|
it('the marker substring alone never appears in a 401 body (universal)', async () => {
|
|
171
171
|
const res = await driver.post(
|
|
172
172
|
'/v1/runs',
|
|
173
|
-
{ workflowId: NOOP_WORKFLOW_ID
|
|
173
|
+
{ workflowId: NOOP_WORKFLOW_ID },
|
|
174
174
|
{
|
|
175
175
|
authenticated: false,
|
|
176
176
|
headers: { Authorization: `Bearer ${CANARY_MARKER}-direct-marker` },
|
|
@@ -204,9 +204,11 @@ describe.skipIf(SKIP_NO_NOOP)('redaction: credentialRef value MUST NOT appear in
|
|
|
204
204
|
// §configurable echo) — but per capabilities.md §"aiProviders"
|
|
205
205
|
// it MUST NOT appear in any RunEvent payload.
|
|
206
206
|
const c = getCanary('byok-credential-ref');
|
|
207
|
+
// S46 (2026-08-18): no fabricated `tenantId` — the run belongs to the
|
|
208
|
+
// credential's own tenant (S41 rule). A tenant-enforcing host answered
|
|
209
|
+
// 403 to the made-up id and this leg then `return`ed, vacuously.
|
|
207
210
|
const create = await driver.post('/v1/runs', {
|
|
208
211
|
workflowId: NOOP_WORKFLOW_ID,
|
|
209
|
-
tenantId: 'conformance-tenant',
|
|
210
212
|
configurable: { ai: { credentialRef: c.value } },
|
|
211
213
|
});
|
|
212
214
|
if (create.status !== 201) {
|