@openwop/openwop-conformance 1.72.2 → 1.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +58 -0
  4. package/api/openapi.yaml +4 -1
  5. package/dist/cli.js +107 -1
  6. package/dist/lib/profiles.js +70 -4
  7. package/package.json +2 -1
  8. package/schemas/CORPUS-STAMP.json +2 -2
  9. package/schemas/README.md +2 -0
  10. package/schemas/capabilities.schema.json +2054 -572
  11. package/schemas/certification-bundle-v2.schema.json +108 -0
  12. package/schemas/run-event-payloads.schema.json +3411 -857
  13. package/schemas/run-event.schema.json +31 -9
  14. package/schemas/workflow-definition.schema.json +492 -130
  15. package/schemas/workload-identity.schema.json +73 -0
  16. package/src/cli.ts +119 -1
  17. package/src/lib/a2a-fake-peer.ts +20 -0
  18. package/src/lib/behavior-gate.ts +42 -7
  19. package/src/lib/llm-cache-key-recipe.ts +51 -0
  20. package/src/lib/mcp-fake-server.ts +20 -0
  21. package/src/lib/profiles.ts +95 -4
  22. package/src/lib/requirement-ledger.ts +138 -0
  23. package/src/lib/requirement-registry.ts +62 -0
  24. package/src/scenarios/a2a-version-negotiation.test.ts +159 -0
  25. package/src/scenarios/capability-example-root-layout.test.ts +113 -0
  26. package/src/scenarios/certification-bundle-v2.test.ts +157 -0
  27. package/src/scenarios/certification-floor-enforcement.test.ts +115 -0
  28. package/src/scenarios/compensation-behavior.test.ts +164 -0
  29. package/src/scenarios/compensation-profile.test.ts +175 -0
  30. package/src/scenarios/contract-provenance.test.ts +209 -0
  31. package/src/scenarios/core-manifest-and-extension-registry.test.ts +198 -0
  32. package/src/scenarios/discovery-canonical-family-no-shadow.test.ts +219 -0
  33. package/src/scenarios/effect-identity-composition.test.ts +129 -0
  34. package/src/scenarios/effect-identity-cross-scope.test.ts +82 -0
  35. package/src/scenarios/mcp-version-negotiation.test.ts +159 -0
  36. package/src/scenarios/multi-region-effect-vocabulary.test.ts +175 -0
  37. package/src/scenarios/multi-region-idempotency.test.ts +17 -7
  38. package/src/scenarios/openapi-resolved-paths.test.ts +127 -0
  39. package/src/scenarios/protocol-version-grammar.test.ts +119 -0
  40. package/src/scenarios/requirement-ledger.test.ts +162 -0
  41. package/src/scenarios/rfc-0147-self-audit.test.ts +104 -0
  42. package/src/scenarios/rfc-lifecycle-coherence.test.ts +137 -0
  43. package/src/scenarios/semantic-digest-v2.test.ts +128 -0
  44. package/src/scenarios/semantic-digest-vectors.test.ts +140 -0
  45. package/src/scenarios/spec-corpus-validity.test.ts +22 -8
  46. package/src/scenarios/strict-behavior-gate.test.ts +120 -0
  47. package/src/scenarios/versioned-composition-profiles.test.ts +183 -0
  48. package/src/scenarios/workload-identity-behavior.test.ts +188 -0
  49. package/src/scenarios/workload-identity-profile.test.ts +175 -0
  50. package/vectors/semantic-request-digest-v2.json +236 -0
package/api/asyncapi.yaml CHANGED
@@ -456,6 +456,64 @@ components:
456
456
  payload:
457
457
  $ref: '#/components/schemas/RunEventDoc'
458
458
 
459
+ CompensationRequested:
460
+ name: compensation.requested
461
+ title: Compensation plan persisted (RFC 0151 §C/§D)
462
+ summary: >-
463
+ The unwind plan is durable. §C requires this BEFORE the first inverse
464
+ action executes — a host that starts unwinding first cannot resume after
465
+ a crash, and the crash is when resumption matters.
466
+ contentType: application/json
467
+ payload:
468
+ $ref: '#/components/schemas/RunEventDoc'
469
+
470
+ CompensationStarted:
471
+ name: compensation.started
472
+ title: First inverse action started (RFC 0151 §D)
473
+ summary: Compensation began executing. MUST follow compensation.requested.
474
+ contentType: application/json
475
+ payload:
476
+ $ref: '#/components/schemas/RunEventDoc'
477
+
478
+ CompensationCompleted:
479
+ name: compensation.completed
480
+ title: Compensation completed (RFC 0151 §D)
481
+ summary: Every inverse action in the plan succeeded.
482
+ contentType: application/json
483
+ payload:
484
+ $ref: '#/components/schemas/RunEventDoc'
485
+
486
+ CompensationFailed:
487
+ name: compensation.failed
488
+ title: Compensation failed (RFC 0151 §D)
489
+ summary: >-
490
+ An inverse action exhausted its retries. Compensation is a second effect,
491
+ not an undo — it can fail, and the run's compensationStatus records that
492
+ rather than the run silently reporting success.
493
+ contentType: application/json
494
+ payload:
495
+ $ref: '#/components/schemas/RunEventDoc'
496
+
497
+ CompensationPaused:
498
+ name: compensation.paused
499
+ title: Compensation paused for authorized intervention (RFC 0151 §C)
500
+ summary: >-
501
+ Cancelling the parent MUST NOT silently abandon an active compensation.
502
+ It continues, pauses here, or records manual intervention required.
503
+ contentType: application/json
504
+ payload:
505
+ $ref: '#/components/schemas/RunEventDoc'
506
+
507
+ CompensationManualInterventionRequired:
508
+ name: compensation.manual_intervention_required
509
+ title: Compensation needs an operator (RFC 0151 §C/§E)
510
+ summary: >-
511
+ The host cannot proceed unaided. Recorded explicitly because an unwind
512
+ that stops without saying so is indistinguishable from one that finished.
513
+ contentType: application/json
514
+ payload:
515
+ $ref: '#/components/schemas/RunEventDoc'
516
+
459
517
  RunPaused:
460
518
  name: run.paused
461
519
  title: Run paused
package/api/openapi.yaml CHANGED
@@ -38,7 +38,10 @@ externalDocs:
38
38
  url: https://openwop.dev/spec/v1/
39
39
 
40
40
  servers:
41
- - url: https://{host}/v1
41
+ # No `/v1` here: the version segment lives in the path keys below, so a
42
+ # server base carrying it too resolves every operation to `/v1/v1/*` and
43
+ # the unversioned discovery route to `/v1/.well-known/openwop` (RFC 0149 §A).
44
+ - url: https://{host}
42
45
  description: openwop-compliant server
43
46
  variables:
44
47
  host:
package/dist/cli.js CHANGED
@@ -42,6 +42,7 @@ function parseArgs(argv) {
42
42
  let impl;
43
43
  let implVersion;
44
44
  let certify;
45
+ let bundleVersion = '1';
45
46
  for (let i = 0; i < argv.length; i++) {
46
47
  const arg = argv[i] ?? '';
47
48
  if (arg === '-h' || arg === '--help') {
@@ -83,6 +84,15 @@ function parseArgs(argv) {
83
84
  case '--implementation-version':
84
85
  implVersion = nextValue();
85
86
  break;
87
+ case '--bundle-version': {
88
+ const v = nextValue();
89
+ if (v !== '1' && v !== '2') {
90
+ process.stderr.write(`--bundle-version must be 1 or 2 (got '${v}')\n`);
91
+ process.exit(2);
92
+ }
93
+ bundleVersion = v;
94
+ break;
95
+ }
86
96
  case '--certify':
87
97
  certify = nextValue();
88
98
  break;
@@ -92,7 +102,7 @@ function parseArgs(argv) {
92
102
  }
93
103
  }
94
104
  }
95
- return { baseUrl, apiKey, offline, filter, help, impl, implVersion, certify };
105
+ return { baseUrl, apiKey, offline, filter, help, impl, implVersion, certify, bundleVersion };
96
106
  }
97
107
  const HELP_TEXT = `openwop-conformance — run the openwop conformance suite against a server
98
108
 
@@ -112,6 +122,10 @@ Implementation labels (cosmetic — surface in failure messages):
112
122
  --impl-version <version> Implementation version (env: OPENWOP_IMPLEMENTATION_VERSION)
113
123
 
114
124
  Certification (RFC 0089):
125
+ --bundle-version <1|2> Certification bundle format. Default 1. Version 2 (RFC 0148
126
+ §C) records per-requirement DISPOSITIONS instead of pass/fail/skip
127
+ file lists, so "we could not check" stops being indistinguishable
128
+ from "checked and it holds". See the note it prints.
115
129
  --certify <out.json> Generate a machine-readable conformance certification
116
130
  bundle: fetch /.well-known/openwop (captured verbatim +
117
131
  SHA-256), derive claimedProfiles from it, run the suite
@@ -310,6 +324,98 @@ async function runCertify(args, baseUrl, apiKey) {
310
324
  skipped,
311
325
  },
312
326
  };
327
+ // (d2) RFC 0148 §C — bundle v2.
328
+ //
329
+ // v1's `{passed, failed, skipped}` file lists cannot express the distinction
330
+ // this program exists for. A file counted as `passed` whether its assertions
331
+ // ran or its runner returned early, and `skipped` flattened three different
332
+ // claims — operator-excluded, not-applicable, and could-not-check — of which
333
+ // only the first two are certifiable.
334
+ //
335
+ // THE HONEST LIMIT, stated because it changes what a v2 bundle from this
336
+ // runner means: the runner reads vitest's per-FILE outcome. It does not read
337
+ // the RFC 0148 §A requirement ledger, because scenarios do not yet record
338
+ // into it. So a skipped file cannot be split into `skipped` /`inapplicable` /
339
+ // `blocked` here, and §A's rule is that an unclassifiable requirement resolves
340
+ // to `blocked` — never to a pass. That is what this emits.
341
+ //
342
+ // The consequence is deliberate and correct: a v2 bundle produced today shows
343
+ // a large `blocked` count and therefore does NOT certify. That is not a defect
344
+ // in the emitter. It is the true state of the evidence, which v1 was unable to
345
+ // represent and therefore reported as a clean skip.
346
+ if (args.bundleVersion === '2') {
347
+ const scenarioIds = [...passed, ...failed, ...skipped].sort();
348
+ const manifestSha = createHash('sha256').update(scenarioIds.join('\n'), 'utf8').digest('hex');
349
+ // Configuration identity per RFC 0147 §A.4: what the run was pointed at, and
350
+ // the discovery it saw. Two runs against differently-configured hosts are
351
+ // different evidence, and this is what says so.
352
+ const configSha = createHash('sha256')
353
+ .update(`${args.baseUrl}\n${sha256}\n${process.env['OPENWOP_REQUIRE_BEHAVIOR'] ?? ''}`, 'utf8')
354
+ .digest('hex');
355
+ const requirements = [
356
+ ...passed.map((f) => ({
357
+ requirementId: `openwop.scenario.${f.replace(/\.test\.ts$/, '')}`,
358
+ scenarioId: f,
359
+ disposition: 'executed-pass',
360
+ })),
361
+ ...failed.map((f) => ({
362
+ requirementId: `openwop.scenario.${f.replace(/\.test\.ts$/, '')}`,
363
+ scenarioId: f,
364
+ disposition: 'executed-fail',
365
+ detail: 'the scenario executed and failed',
366
+ })),
367
+ ...skipped.map((f) => ({
368
+ requirementId: `openwop.scenario.${f.replace(/\.test\.ts$/, '')}`,
369
+ scenarioId: f,
370
+ disposition: 'blocked',
371
+ detail: 'runner cannot classify a skipped file: the RFC 0148 §A requirement ledger is not ' +
372
+ 'populated during a run, so skipped/inapplicable/blocked are indistinguishable here. ' +
373
+ '§A resolves an unclassifiable requirement to `blocked`, never to a pass.',
374
+ })),
375
+ ];
376
+ const v2 = {
377
+ bundleVersion: '2',
378
+ generatedAt: new Date().toISOString(),
379
+ generator: { name: '@openwop/openwop-conformance --certify', version },
380
+ suite: { package: '@openwop/openwop-conformance', version },
381
+ host,
382
+ discovery: { url: discoveryUrl, sha256, document },
383
+ claimedProfiles,
384
+ results: {
385
+ totals: {
386
+ executedPass: passed.length,
387
+ executedFail: failed.length,
388
+ skipped: 0,
389
+ inapplicable: 0,
390
+ blocked: skipped.length,
391
+ },
392
+ requirements,
393
+ },
394
+ scenarioManifestSha256: manifestSha,
395
+ targetConfigurationSha256: configSha,
396
+ };
397
+ const v2Schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'certification-bundle-v2.schema.json'), 'utf8'));
398
+ const v2Ajv = new Ajv2020({ allErrors: true, strict: false });
399
+ addFormats(v2Ajv);
400
+ const v2Validate = v2Ajv.compile(v2Schema);
401
+ if (!v2Validate(v2)) {
402
+ process.stderr.write('openwop-conformance --certify: assembled v2 bundle FAILED schema validation:\n' +
403
+ `${JSON.stringify(v2Validate.errors, null, 2)}\n`);
404
+ process.exit(2);
405
+ }
406
+ writeFileSync(outPath, `${JSON.stringify(v2, null, 2)}\n`);
407
+ process.stdout.write(`openwop-conformance --certify: wrote bundle v2 to ${outPath}\n` +
408
+ ` host: ${host.name}@${host.version}\n` +
409
+ ` executed-pass ${passed.length} / executed-fail ${failed.length} / blocked ${skipped.length}\n` +
410
+ (skipped.length > 0
411
+ ? ` NOTE: ${skipped.length} requirement(s) are 'blocked', not 'skipped'. The runner cannot\n` +
412
+ ` classify a skipped file until scenarios record into the RFC 0148 §A ledger, and §A\n` +
413
+ ` resolves an unclassifiable requirement to 'blocked' rather than to a pass. A bundle\n` +
414
+ ` with blocked > 0 does NOT certify — that is the true state of the evidence, not a\n` +
415
+ ` defect in this emitter.\n`
416
+ : ''));
417
+ process.exit(failed.length > 0 ? 1 : 0);
418
+ }
313
419
  // (e) Validate against the bundle schema BEFORE writing.
314
420
  const schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'conformance-certification-bundle.schema.json'), 'utf8'));
315
421
  const ajv = new Ajv2020({ allErrors: true, strict: false });
@@ -38,6 +38,13 @@ function isStringArray(value) {
38
38
  function isNonNegativeInteger(value) {
39
39
  return typeof value === 'number' && Number.isInteger(value) && value >= 0;
40
40
  }
41
+ /**
42
+ * RFC 0149 §C — `protocolVersion` is ASCII `<major>.<minor>`, no leading zero
43
+ * except zero itself. Kept identical to the `pattern` in
44
+ * `schemas/capabilities.schema.json`; `protocol-version-grammar.test.ts` asserts
45
+ * the two agree, so this cannot drift into being the looser of the pair again.
46
+ */
47
+ const PROTOCOL_VERSION_GRAMMAR = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
41
48
  /**
42
49
  * `openwop-core` predicate. Every other profile implies `openwop-core`. A host
43
50
  * that fails this predicate is not openwop-compatible.
@@ -47,7 +54,14 @@ function isNonNegativeInteger(value) {
47
54
  export function isCore(c) {
48
55
  if (typeof c.protocolVersion !== 'string')
49
56
  return false;
50
- if (!c.protocolVersion.startsWith('1.'))
57
+ // RFC 0149 §C. `startsWith('1.')` admitted `1.0.0` and `1.banana` while
58
+ // rejecting `1`, so the predicate deciding whether a host is openwop-compatible
59
+ // at all was looser than the schema every host validates against. The major is
60
+ // the hard boundary; a higher minor stays core under v1 additive rules.
61
+ const version = PROTOCOL_VERSION_GRAMMAR.exec(c.protocolVersion);
62
+ if (version === null)
63
+ return false;
64
+ if (version[1] !== '1')
51
65
  return false;
52
66
  if (!Array.isArray(c.supportedEnvelopes))
53
67
  return false;
@@ -437,6 +451,42 @@ export const PROFILE_FLOOR_SCENARIOS = {
437
451
  ],
438
452
  requiredAnyPrefix: ['interrupt-'],
439
453
  },
454
+ // ── Floors TRANSCRIBED from `profiles.md`, not invented here ────────────────
455
+ // `profiles.md` §"Claiming vs passing" already states the rule normatively:
456
+ // "A host CLAIMS a profile by satisfying its predicate AND passing the
457
+ // conformance scenarios labelled with the profile tag." Each per-profile
458
+ // section then names its scenarios. This map was an incomplete transcription
459
+ // of that prose, and every profile it omitted verified as floor-proven
460
+ // against nothing (RFC 0148 §C, gap G6).
461
+ // `profiles.md` §`openwop-core` is a pure discovery-payload predicate — it
462
+ // names no runtime scenario, so the predicate IS the whole claim.
463
+ 'openwop-core': { required: [], discoveryOnly: true },
464
+ // `profiles.md` §`openwop-fixtures`: "The profile is discovery-payload-only —
465
+ // it confirms the host claims SOME fixture, not that any specific fixture is
466
+ // wired."
467
+ 'openwop-fixtures': { required: [], discoveryOnly: true },
468
+ // `profiles.md` §`openwop-stream-sse`: "A host passes `openwop-stream-sse`
469
+ // when discovery passes the predicate AND those scenarios pass."
470
+ 'openwop-stream-sse': {
471
+ required: ['stream-modes.test.ts', 'stream-modes-buffer.test.ts', 'stream-modes-mixed.test.ts'],
472
+ },
473
+ // `profiles.md` §`openwop-stream-poll`: "Conformance scenarios in
474
+ // `stream-modes.test.ts` exercise polling."
475
+ 'openwop-stream-poll': { required: ['stream-modes.test.ts'] },
476
+ // `profiles.md` §`openwop-node-packs`: "a host passes `openwop-node-packs`
477
+ // when it passes those scenarios."
478
+ 'openwop-node-packs': { required: ['pack-registry.test.ts', 'pack-registry-publish.test.ts'] },
479
+ // ── Deliberately NOT transcribed ───────────────────────────────────────────
480
+ // `openwop-replay-fork` cannot be expressed by this model. `profiles.md`
481
+ // §`openwop-replay-fork` says "Hosts MAY support either or both modes; the
482
+ // conformance scenarios pass on whichever mode the host advertises" — a floor
483
+ // conditional on the advertised mode, which a flat `required[]` cannot state.
484
+ // Forcing it would either over-require (failing an honest single-mode host) or
485
+ // under-require (the vacuity this fix removes). It stays unspecified, and is
486
+ // therefore unprovable, until the model can express a discovery-conditional
487
+ // floor. Same for `openwop-interrupts`, `openwop-secrets`,
488
+ // `openwop-provider-policy`, `openwop-memory`, and `openwop-trigger-bridge`,
489
+ // whose prose sections do not yet name a settled floor set.
440
490
  };
441
491
  /** Is `profile` derivable from a discovery document? Maps a profile name to its predicate (RFC 0089 §B(1)). */
442
492
  export function profileDerivable(c, profile) {
@@ -457,11 +507,27 @@ const scenarioBasename = (id) => id.split('/').pop() ?? id;
457
507
  export function verifyBundleProfile(bundle, profile) {
458
508
  const derivable = profileDerivable(bundle.discovery.document, profile);
459
509
  const floor = PROFILE_FLOOR_SCENARIOS[profile];
510
+ // An UNDEFINED floor set makes §B(2) unevaluable, so the claim is unprovable —
511
+ // never proven. Reading the old code, `missingFloor` fell to `[]` and
512
+ // `prefixOk` to `[].every(...)` === true, so `floorProven` came out TRUE for
513
+ // every profile this map omitted: the verifier reported a claim as floor-proven
514
+ // having checked nothing. An empty floor that is a real decision says so with
515
+ // `discoveryOnly` (RFC 0148 §C, gap G6).
516
+ if (floor === undefined) {
517
+ return { profile, derivable, floorProven: false, valid: false, missingFloor: [], floorUnspecified: true };
518
+ }
460
519
  const passed = new Set(bundle.results.passed.map(scenarioBasename));
461
- const missingFloor = floor ? floor.required.filter((r) => !passed.has(scenarioBasename(r))) : [];
462
- const prefixOk = (floor?.requiredAnyPrefix ?? []).every((p) => [...passed].some((s) => s.startsWith(p)));
520
+ const missingFloor = floor.required.filter((r) => !passed.has(scenarioBasename(r)));
521
+ const prefixOk = (floor.requiredAnyPrefix ?? []).every((p) => [...passed].some((s) => s.startsWith(p)));
463
522
  const floorProven = missingFloor.length === 0 && prefixOk;
464
- return { profile, derivable, floorProven, valid: derivable && floorProven, missingFloor };
523
+ return {
524
+ profile,
525
+ derivable,
526
+ floorProven,
527
+ valid: derivable && floorProven,
528
+ missingFloor,
529
+ floorUnspecified: false,
530
+ };
465
531
  }
466
532
  /** Verify every profile in `bundle.claimedProfiles`; the bundle is valid iff all claims are valid. */
467
533
  export function verifyBundle(bundle) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "1.72.2",
3
+ "version": "1.98.0",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,6 +20,7 @@
20
20
  "coverage.md",
21
21
  "api",
22
22
  "schemas",
23
+ "vectors",
23
24
  "vitest.config.ts",
24
25
  "README.md",
25
26
  "CHANGELOG.md",
@@ -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.72.2",
4
- "corpusCommit": "e58d3553617279966cb010b68d7ac4caf8110ec7"
3
+ "suiteVersion": "1.98.0",
4
+ "corpusCommit": "74e2402caae98a97d16a2d8d56f1b46aae759741"
5
5
  }
package/schemas/README.md CHANGED
@@ -87,6 +87,8 @@
87
87
  | `suspend-request.schema.json` | `interrupt.md` | `InterruptPayload` with 8 `kind` discriminators (approval, clarification, external-event, custom, conversation.start, conversation.exchange, conversation.close, low-confidence) |
88
88
  | `workflow-chain-pack-manifest.schema.json` | `workflow-chain-packs.md` + RFC 0013 | Manifest for workflow-chain packs (`kind: "workflow-chain"`) — pre-configured DAG fragments expanded inline at workflow-author time. Peer to `node-pack-manifest.schema.json`; disjoint via the `kind` discriminator. |
89
89
  | `workflow-definition.schema.json` | `channels-and-reducers.md` + `node-packs.md` | DAG of nodes + edges + triggers + variables + channels |
90
+ | `certification-bundle-v2.schema.json` | `profiles.md` + RFC 0148 §C | Certification bundle v2 — per-requirement dispositions instead of pass/fail/skip file lists. `blocked` is a REQUIRED total because `blocked: 0` asserted differs from `blocked` unstated, and `assertionCount` makes an `executed-pass` that ran nothing visible in the artifact |
91
+ | `workload-identity.schema.json` | `auth.md` + RFC 0154 §A/§B | Verified workload identity + delegated actor chain. Closed by design: raw certificates, tokens, proofs, and credentials MUST NOT enter these objects — `subject` is opaque and `proofRef`/`thumbprintRef` are digest references, because these objects reach events, spans, and audit records |
90
92
  | `workspace-file.schema.json` | `agent-workspace.md` + `RFCS/0059` | RFC 0059 — a versioned workspace file (`{path, content, version, etag, updatedAt}`); response of `GET/PUT /v1/host/workspace/files/{path}`. |
91
93
  | `workspace-file-create.schema.json` | `agent-workspace.md` + `RFCS/0059` | RFC 0059 — `PUT /v1/host/workspace/files/{path}` request body (content + optional contentType; path from the URL, version/etag host-assigned). |
92
94