capdag 1.233.649 → 1.241.684

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.
@@ -744,6 +744,7 @@ function validateStrandStep(step, path) {
744
744
  if (!step || typeof step !== 'object') {
745
745
  throw new Error(`CapFabRenderer: ${path} is not an object`);
746
746
  }
747
+ assertString(step.token_id, `${path}.token_id`);
747
748
  assertString(step.from_spec, `${path}.from_spec`);
748
749
  assertString(step.to_spec, `${path}.to_spec`);
749
750
  if (!step.step_type || typeof step.step_type !== 'object') {
@@ -774,28 +775,32 @@ function validateStrandStep(step, path) {
774
775
  if (typeof body.output_is_sequence !== 'boolean') {
775
776
  throw new Error(`CapFabRenderer: ${path}.step_type.Cap.output_is_sequence must be a boolean`);
776
777
  }
777
- // `inputs` (capdag CapInput list) carries the data-flow graph: the main
778
- // input plus any convergence inputs. Optional on legacy payloads, but
779
- // when present it must be well-formed — the fan-in edges are drawn from
780
- // it, so a malformed entry is a hard error, not a silent skip.
781
- if (body.inputs !== undefined) {
782
- assertArray(body.inputs, `${path}.step_type.Cap.inputs`);
783
- body.inputs.forEach((input, inputIdx) => {
784
- const inputPath = `${path}.step_type.Cap.inputs[${inputIdx}]`;
785
- assertObject(input, inputPath);
786
- assertString(input.arg_urn, `${inputPath}.arg_urn`);
787
- // serde: unit variant → the string "StrandInput"; struct variant →
788
- // { Step: { token_id } }. Anything else is malformed.
789
- if (input.source === 'StrandInput') return;
790
- if (input.source && typeof input.source === 'object' && input.source.Step) {
791
- assertString(input.source.Step.token_id, `${inputPath}.source.Step.token_id`);
792
- return;
793
- }
794
- throw new Error(
795
- `CapFabRenderer: ${inputPath}.source must be "StrandInput" or { Step: { token_id } }`
796
- );
797
- });
778
+ // `inputs` is the authoritative data-flow graph: the primary input plus
779
+ // every convergence input, with each producer named by stable token.
780
+ assertArray(body.inputs, `${path}.step_type.Cap.inputs`);
781
+ if (body.inputs.length === 0) {
782
+ throw new Error(`CapFabRenderer: ${path}.step_type.Cap.inputs must include the primary input`);
798
783
  }
784
+ const argUrns = new Set();
785
+ body.inputs.forEach((input, inputIdx) => {
786
+ const inputPath = `${path}.step_type.Cap.inputs[${inputIdx}]`;
787
+ assertObject(input, inputPath);
788
+ assertString(input.arg_urn, `${inputPath}.arg_urn`);
789
+ if (argUrns.has(input.arg_urn)) {
790
+ throw new Error(`CapFabRenderer: ${inputPath}.arg_urn duplicates '${input.arg_urn}'`);
791
+ }
792
+ argUrns.add(input.arg_urn);
793
+ // serde: unit variant → the string "StrandInput"; struct variant →
794
+ // { Step: { token_id } }. Anything else is malformed.
795
+ if (input.source === 'StrandInput') return;
796
+ if (input.source && typeof input.source === 'object' && input.source.Step) {
797
+ assertString(input.source.Step.token_id, `${inputPath}.source.Step.token_id`);
798
+ return;
799
+ }
800
+ throw new Error(
801
+ `CapFabRenderer: ${inputPath}.source must be "StrandInput" or { Step: { token_id } }`
802
+ );
803
+ });
799
804
  } else {
800
805
  assertString(body.media_def, `${path}.step_type.${variant}.media_def`);
801
806
  }
@@ -808,8 +813,26 @@ function validateStrandPayload(data) {
808
813
  assertString(data.source_media_urn, 'strand mode data.source_media_urn');
809
814
  assertString(data.target_media_urn, 'strand mode data.target_media_urn');
810
815
  assertArray(data.steps, 'strand mode data.steps');
816
+ const tokenIds = new Set();
811
817
  data.steps.forEach((step, idx) => {
812
818
  validateStrandStep(step, `strand mode data.steps[${idx}]`);
819
+ if (tokenIds.has(step.token_id)) {
820
+ throw new Error(`CapFabRenderer strand mode: duplicate step token_id '${step.token_id}'`);
821
+ }
822
+ tokenIds.add(step.token_id);
823
+ });
824
+ data.steps.forEach((step, idx) => {
825
+ if (!step.step_type.Cap) return;
826
+ step.step_type.Cap.inputs.forEach((input, inputIdx) => {
827
+ if (input.source === 'StrandInput') return;
828
+ const producerToken = input.source.Step.token_id;
829
+ if (!tokenIds.has(producerToken)) {
830
+ throw new Error(
831
+ `CapFabRenderer strand mode: data.steps[${idx}].step_type.Cap.inputs[${inputIdx}]` +
832
+ `.source.Step.token_id '${producerToken}' does not resolve to a strand step`
833
+ );
834
+ }
835
+ });
813
836
  });
814
837
  if (data.media_display_names !== undefined
815
838
  && (data.media_display_names === null || typeof data.media_display_names !== 'object')) {
@@ -824,23 +847,43 @@ function validateBodyOutcome(outcome, path) {
824
847
  if (!outcome || typeof outcome !== 'object') {
825
848
  throw new Error(`CapFabRenderer: ${path} is not an object`);
826
849
  }
827
- if (typeof outcome.body_index !== 'number' || !Number.isInteger(outcome.body_index) || outcome.body_index < 0) {
828
- throw new Error(`CapFabRenderer: ${path}.body_index must be a non-negative integer`);
850
+ if (typeof outcome.body_index !== 'number' || !Number.isSafeInteger(outcome.body_index) || outcome.body_index < 0) {
851
+ throw new Error(`CapFabRenderer: ${path}.body_index must be a non-negative safe integer`);
852
+ }
853
+ if (!Object.prototype.hasOwnProperty.call(outcome, 'foreach_token_id')) {
854
+ throw new Error(
855
+ `CapFabRenderer: ${path}.foreach_token_id must be present as null or a stable ForEach step token`
856
+ );
857
+ }
858
+ if (outcome.foreach_token_id !== null
859
+ && (typeof outcome.foreach_token_id !== 'string' || outcome.foreach_token_id.length === 0)) {
860
+ throw new Error(`CapFabRenderer: ${path}.foreach_token_id must be null or a non-empty string`);
861
+ }
862
+ if (outcome.foreach_token_id === null && outcome.body_index !== 0) {
863
+ throw new Error(`CapFabRenderer: ${path} is linear and must use body_index 0`);
829
864
  }
830
865
  if (typeof outcome.success !== 'boolean') {
831
866
  throw new Error(`CapFabRenderer: ${path}.success must be a boolean`);
832
867
  }
833
868
  assertArray(outcome.cap_urns, `${path}.cap_urns`);
834
869
  outcome.cap_urns.forEach((u, i) => assertString(u, `${path}.cap_urns[${i}]`));
835
- if (outcome.failed_cap !== undefined && outcome.failed_cap !== null
836
- && (typeof outcome.failed_cap !== 'string' || outcome.failed_cap.length === 0)) {
837
- throw new Error(`CapFabRenderer: ${path}.failed_cap must be a non-empty string when present`);
870
+ if (outcome.failed_token_id !== undefined && outcome.failed_token_id !== null
871
+ && (typeof outcome.failed_token_id !== 'string' || outcome.failed_token_id.length === 0)) {
872
+ throw new Error(`CapFabRenderer: ${path}.failed_token_id must be a non-empty string when present`);
838
873
  }
839
- if (!outcome.success && outcome.failed_cap === undefined) {
840
- // Failure without a failed_cap is allowed (e.g. infrastructure
841
- // failure before any cap ran) but we still expect the field to be
842
- // present — either null or a string. Rust's Option<String>
843
- // serializes as null or the string, never missing.
874
+ if (!Object.prototype.hasOwnProperty.call(outcome, 'failed_token_id')) {
875
+ throw new Error(
876
+ `CapFabRenderer: ${path}.failed_token_id must be present as null or a stable step token`
877
+ );
878
+ }
879
+ if (!Object.prototype.hasOwnProperty.call(outcome, 'failed_arg_urn')) {
880
+ throw new Error(
881
+ `CapFabRenderer: ${path}.failed_arg_urn must be present as null or an attributed argument URN`
882
+ );
883
+ }
884
+ if (outcome.failed_arg_urn !== null
885
+ && (typeof outcome.failed_arg_urn !== 'string' || outcome.failed_arg_urn.length === 0)) {
886
+ throw new Error(`CapFabRenderer: ${path}.failed_arg_urn must be null or a non-empty string`);
844
887
  }
845
888
  }
846
889
 
@@ -867,8 +910,29 @@ function validateRunPayload(data) {
867
910
  media_display_names: data.media_display_names,
868
911
  }));
869
912
  assertArray(data.body_outcomes, 'run mode data.body_outcomes');
913
+ const bodyCoordinates = new Set();
870
914
  data.body_outcomes.forEach((o, idx) => {
871
915
  validateBodyOutcome(o, `run mode data.body_outcomes[${idx}]`);
916
+ if (typeof o.foreach_token_id === 'string'
917
+ && !data.resolved_strand.steps.some(step => (
918
+ step.token_id === o.foreach_token_id
919
+ && Object.keys(step.step_type)[0] === 'ForEach'
920
+ ))) {
921
+ throw new Error(
922
+ `CapFabRenderer run mode: body_outcomes[${idx}].foreach_token_id '${o.foreach_token_id}' does not resolve to a ForEach strand step`
923
+ );
924
+ }
925
+ if (typeof o.failed_token_id === 'string'
926
+ && !data.resolved_strand.steps.some(step => step.token_id === o.failed_token_id)) {
927
+ throw new Error(
928
+ `CapFabRenderer run mode: body_outcomes[${idx}].failed_token_id '${o.failed_token_id}' does not resolve to a strand step`
929
+ );
930
+ }
931
+ const coordinate = `${o.foreach_token_id === null ? 'linear' : o.foreach_token_id}:${o.body_index}`;
932
+ if (bodyCoordinates.has(coordinate)) {
933
+ throw new Error(`CapFabRenderer run mode: duplicate body coordinate '${coordinate}'`);
934
+ }
935
+ bodyCoordinates.add(coordinate);
872
936
  });
873
937
  if (typeof data.visible_success_count !== 'number' || data.visible_success_count < 0) {
874
938
  throw new Error('CapFabRenderer run mode: data.visible_success_count must be a non-negative number');
@@ -1383,7 +1447,7 @@ function buildStrandGraphData(data) {
1383
1447
  // ORDER is not — the realizer emits edges greedily in dependency
1384
1448
  // order, so `prevNodeId` (the previously-emitted step) need not be
1385
1449
  // one of this cap's inputs at all.
1386
- const capInputs = Array.isArray(body.inputs) ? body.inputs : [];
1450
+ const capInputs = body.inputs;
1387
1451
  const producerNodeIds = [];
1388
1452
  for (const input of capInputs) {
1389
1453
  const src = input && input.source;
@@ -1392,16 +1456,22 @@ function buildStrandGraphData(data) {
1392
1456
  continue;
1393
1457
  }
1394
1458
  const producerToken = src && typeof src === 'object' && src.Step && src.Step.token_id;
1395
- if (typeof producerToken !== 'string' || producerToken.length === 0) continue;
1459
+ if (typeof producerToken !== 'string' || producerToken.length === 0) {
1460
+ throw new Error(`CapFabRenderer strand: cap step '${step.token_id}' has an invalid producer token`);
1461
+ }
1396
1462
  const producerNodeId = tokenToNodeId.get(producerToken);
1397
- if (producerNodeId) producerNodeIds.push(producerNodeId);
1463
+ if (!producerNodeId) {
1464
+ throw new Error(
1465
+ `CapFabRenderer strand: cap step '${step.token_id}' references unknown producer '${producerToken}'`
1466
+ );
1467
+ }
1468
+ producerNodeIds.push(producerNodeId);
1398
1469
  }
1399
1470
 
1400
1471
  // Pick the source of the single labeled "backbone" edge (the one
1401
- // carrying the cap title + cardinality). Inside a ForEach body — or
1402
- // when the cap declares no inputs — keep the linear `prevNodeId`
1403
- // thread so the ForEach boundary handling and its (1→n) entry marker
1404
- // are preserved exactly. Otherwise anchor the backbone on a REAL
1472
+ // carrying the cap title + cardinality). Inside a ForEach body, keep
1473
+ // the linear `prevNodeId` thread so the boundary handling and its
1474
+ // (1→n) entry marker are preserved exactly. Otherwise anchor it on a REAL
1405
1475
  // input: `prevNodeId` if it is genuinely one, else the first actual
1406
1476
  // producer — so a non-input step ordered just before this cap can
1407
1477
  // never become a spurious backbone edge.
@@ -1803,14 +1873,12 @@ function buildExternalInputRunGraphData(
1803
1873
  if (inputRuns.length <= 1 || allOutcomes.length === 0) return null;
1804
1874
 
1805
1875
  const capSteps = data.resolved_strand.steps
1806
- .filter(step => Object.keys(step.step_type)[0] === 'Cap')
1807
- .map(step => step.step_type.Cap);
1876
+ .filter(step => Object.keys(step.step_type)[0] === 'Cap');
1808
1877
  if (capSteps.length === 0) {
1809
1878
  throw new Error('CapFabRenderer run mode: external multi-input runs require at least one Cap step in resolved_strand.');
1810
1879
  }
1811
1880
 
1812
1881
  const visibleOutcomes = visibleSuccess.concat(visibleFailure);
1813
- const CapUrn = requireHostDependency('CapUrn');
1814
1882
  const sourceCanonical = canonicalMediaUrn(data.resolved_strand.source_media_urn);
1815
1883
  const targetCanonical = canonicalMediaUrn(data.resolved_strand.target_media_urn);
1816
1884
  const anchorNodeId = 'external-input-anchor';
@@ -1874,12 +1942,10 @@ function buildExternalInputRunGraphData(
1874
1942
 
1875
1943
  let traceEnd = capSteps.length;
1876
1944
  if (!success) {
1877
- if (typeof outcome.failed_cap === 'string' && outcome.failed_cap.length > 0) {
1878
- const failedCap = CapUrn.fromString(outcome.failed_cap);
1945
+ if (typeof outcome.failed_token_id === 'string' && outcome.failed_token_id.length > 0) {
1879
1946
  traceEnd = 0;
1880
1947
  for (let i = 0; i < capSteps.length; i++) {
1881
- const candidate = CapUrn.fromString(capSteps[i].cap_urn);
1882
- if (candidate.isEquivalent(failedCap)) {
1948
+ if (capSteps[i].token_id === outcome.failed_token_id) {
1883
1949
  traceEnd = i + 1;
1884
1950
  break;
1885
1951
  }
@@ -1891,8 +1957,13 @@ function buildExternalInputRunGraphData(
1891
1957
 
1892
1958
  let prevNodeId = sourceNodeId;
1893
1959
  for (let i = 0; i < traceEnd; i++) {
1894
- const cap = capSteps[i];
1895
- const targetMedia = canonicalMediaUrn(i === capSteps.length - 1 ? data.resolved_strand.target_media_urn : data.resolved_strand.steps.filter(step => Object.keys(step.step_type)[0] === 'Cap')[i].to_spec);
1960
+ const capStep = capSteps[i];
1961
+ const cap = capStep.step_type.Cap;
1962
+ const targetMedia = canonicalMediaUrn(
1963
+ i === capSteps.length - 1
1964
+ ? data.resolved_strand.target_media_urn
1965
+ : capStep.to_spec
1966
+ );
1896
1967
  const isLastExecutedStep = i === traceEnd - 1;
1897
1968
  const outputs = success ? runDef.outputs : [];
1898
1969
 
@@ -2288,19 +2359,13 @@ function buildRunGraphData(data) {
2288
2359
  const edgeClass = success ? 'body-success' : 'body-failure';
2289
2360
  const colorVar = success ? '--graph-body-edge-success' : '--graph-body-edge-failure';
2290
2361
 
2291
- // Trace end: failures stop at `failed_cap`. `CapUrn.isEquivalent`
2292
- // is used for the match — never string equality.
2293
- let traceEnd = bodyCapSteps.length;
2294
- if (!success && typeof outcome.failed_cap === 'string' && outcome.failed_cap.length > 0) {
2295
- const CapUrn = requireHostDependency('CapUrn');
2296
- const target = CapUrn.fromString(outcome.failed_cap);
2297
- for (let i = 0; i < bodyCapSteps.length; i++) {
2298
- const candidate = CapUrn.fromString(bodyCapSteps[i].step.step_type.Cap.cap_urn);
2299
- if (candidate.isEquivalent(target)) {
2300
- traceEnd = i + 1;
2301
- break;
2302
- }
2303
- }
2362
+ // Trace end: failures stop at the exact stable step token.
2363
+ let traceEnd = success ? bodyCapSteps.length : 0;
2364
+ if (!success && typeof outcome.failed_token_id === 'string' && outcome.failed_token_id.length > 0) {
2365
+ const failedIndex = bodyCapSteps.findIndex(
2366
+ bodyStep => bodyStep.step.token_id === outcome.failed_token_id
2367
+ );
2368
+ if (failedIndex >= 0) traceEnd = failedIndex + 1;
2304
2369
  }
2305
2370
 
2306
2371
  const bodyKey = `body-${outcome.body_index}`;
package/capdag.js CHANGED
@@ -615,7 +615,7 @@ class CapUrn {
615
615
  }
616
616
  }
617
617
 
618
- // Output direction: provider output must conform to requested output.
618
+ // Output direction: candidate output must conform to requested output.
619
619
  // `media:` on the pattern side is wildcard top and skips the check.
620
620
  if (this.outSpec !== 'media:' && this.outSpec !== '*') {
621
621
  const capOut = MediaUrn.fromString(this.outSpec);
@@ -851,7 +851,7 @@ class CapUrn {
851
851
  }
852
852
 
853
853
  /**
854
- * Check if this provider can dispatch the given request.
854
+ * Check if this candidate can dispatch the given request.
855
855
  *
856
856
  * @param {CapUrn} request
857
857
  * @returns {boolean}
@@ -1338,7 +1338,7 @@ const MEDIA_EMBEDDING_VECTOR = 'media:embedding-vector;enc=utf-8;record';
1338
1338
  // and `file-type=txt` (binds the URN to the `.txt` extension).
1339
1339
  const MEDIA_IMAGE_DESCRIPTION = 'media:enc=utf-8;ext=txt;image-description;plain-text';
1340
1340
  // Media URN for finalised plain text — the canonical input/output of cap:save-as-txt.
1341
- // Producers of user-facing prose (LLM text-generation, OCR's extracted text,
1341
+ // Producers of user-facing prose (LLM text-generation, OCR's digitized text,
1342
1342
  // summarisation) declare this URN as their `out` so the planner restricts the .txt
1343
1343
  // persistence path to those caps. See fabric/media/plain-text.toml.
1344
1344
  const MEDIA_PLAIN_TEXT = 'media:enc=utf-8;ext=txt;plain-text';
@@ -2481,6 +2481,46 @@ class CapArg {
2481
2481
  return stdinSource ? stdinSource.stdin : null;
2482
2482
  }
2483
2483
 
2484
+ /**
2485
+ * The media URN the runtime demuxes this arg's input stream by: its
2486
+ * stdin source URN if it declares one, otherwise its declared slot
2487
+ * media URN. A cap need not declare any stdin source at all — a
2488
+ * producer-fed arg may be delivered by its declared URN — so this
2489
+ * never assumes a stdin source exists.
2490
+ * Mirrors Rust: CapArg::stream_urn()
2491
+ * @returns {string} The stream media URN
2492
+ */
2493
+ streamUrn() {
2494
+ const stdinSource = this.sources.find(s => s.stdin !== null);
2495
+ return stdinSource ? stdinSource.stdin : this.media_urn;
2496
+ }
2497
+
2498
+ /**
2499
+ * Whether this arg is the cap's MAIN input relative to `inSpec` (the cap
2500
+ * URN's `in=` value): it declares a stdin source whose URN is `in=`. The
2501
+ * main input is always the value piped in on stdin (like a Unix command's
2502
+ * stdin), so the main arg always declares a stdin source carrying `in=`.
2503
+ * Its DECLARED slot URN may differ from that stdin URN (e.g. a `file-path`
2504
+ * slot whose piped content is a `pdf-stream`) — the stdin URN, not the
2505
+ * slot URN, is `in=`. The main input may ALSO be delivered by
2506
+ * position/cli-flag, but stdin is the defining route. Compared by
2507
+ * tagged-URN equivalence, never as strings.
2508
+ * Mirrors Rust: CapArg::is_main_input(&in_spec)
2509
+ * @param {MediaUrn|string} inSpec - The cap URN's in= spec (parsed if given as string)
2510
+ * @returns {boolean} True if this arg is the cap's main input
2511
+ */
2512
+ isMainInput(inSpec) {
2513
+ const spec = inSpec instanceof MediaUrn ? inSpec : MediaUrn.fromString(inSpec);
2514
+ return this.sources.some(s => {
2515
+ if (s.stdin === null) return false;
2516
+ try {
2517
+ return MediaUrn.fromString(s.stdin).isEquivalent(spec);
2518
+ } catch (_) {
2519
+ return false;
2520
+ }
2521
+ });
2522
+ }
2523
+
2484
2524
  /**
2485
2525
  * Check if this argument has a position source
2486
2526
  * @returns {boolean} True if has position source
@@ -2920,7 +2960,7 @@ class CapGroup {
2920
2960
  }
2921
2961
 
2922
2962
  /**
2923
- * Unified cap-based manifest for components (providers and cartridges).
2963
+ * Unified cap-based manifest for components (cartridges).
2924
2964
  *
2925
2965
  * `(registry_url, channel, name, version)` is the cartridge's full
2926
2966
  * identity — each (registry, channel) pair is an independent namespace.
@@ -5286,7 +5326,7 @@ const CartridgeAttachmentErrorKind = Object.freeze({
5286
5326
  QUARANTINED: 'quarantined',
5287
5327
  // On-disk install context disagrees with the cartridge.json the cartridge
5288
5328
  // declares — the slug folder doesn't match slug_for(registry_url), the
5289
- // channel folder doesn't match the manifest's channel, or a bundled-provider
5329
+ // channel folder doesn't match the manifest's channel, or a bundled-cartridge
5290
5330
  // integrity proof failed. Structurally well-formed but cannot be trusted
5291
5331
  // because its placement on disk does not match what it claims to be.
5292
5332
  BAD_INSTALLATION: 'bad_installation',
@@ -5817,49 +5857,49 @@ function hashCartridgeDirectory(dir) {
5817
5857
  }
5818
5858
 
5819
5859
  // ---------------------------------------------------------------------------
5820
- // Bundled-provider integrity (non-macOS).
5860
+ // Bundled-cartridge integrity (non-macOS).
5821
5861
  // ---------------------------------------------------------------------------
5822
5862
  //
5823
- // Mirrors capdag::cartridge_discovery::verify_bundled_provider_hash and the
5824
- // BUNDLED_PROVIDER_HASHES const codegen'd by build.rs. Under a plain build —
5825
- // and in this mirror, which bundles no providers — the set is empty, so any
5863
+ // Mirrors capdag::cartridge_discovery::verify_bundled_cartridge_hash and the
5864
+ // BUNDLED_CARTRIDGE_HASHES const codegen'd by build.rs. Under a plain build —
5865
+ // and in this mirror, which bundles no cartridges — the set is empty, so any
5826
5866
  // `installed_from: bundle` cartridge has no baked hash and is rejected.
5827
- const BUNDLED_PROVIDER_HASHES = Object.freeze([]); // [name, version, hash] triples
5867
+ const BUNDLED_CARTRIDGE_HASHES = Object.freeze([]); // [name, version, hash] triples
5828
5868
 
5829
5869
  /**
5830
- * Look up the baked expected directory hash for a bundled provider, or null if
5870
+ * Look up the baked expected directory hash for a bundled cartridge, or null if
5831
5871
  * (name, version) was not recorded at build time.
5832
5872
  * @returns {string|null}
5833
5873
  */
5834
- function bundledProviderExpectedHash(name, version) {
5835
- for (const [n, v, h] of BUNDLED_PROVIDER_HASHES) {
5874
+ function bundledCartridgeExpectedHash(name, version) {
5875
+ for (const [n, v, h] of BUNDLED_CARTRIDGE_HASHES) {
5836
5876
  if (n === name && v === version) return h;
5837
5877
  }
5838
5878
  return null;
5839
5879
  }
5840
5880
 
5841
5881
  /**
5842
- * Verify a bundled provider's on-disk content against the hash baked into this
5882
+ * Verify a bundled cartridge's on-disk content against the hash baked into this
5843
5883
  * build. Returns null on success, or an error-reason string. Mirrors
5844
- * verify_bundled_provider_hash. Non-macOS only: macOS bundled-provider
5884
+ * verify_bundled_cartridge_hash. Non-macOS only: macOS bundled-cartridge
5845
5885
  * integrity is the OS code-signature, so the engine there neither bakes nor
5846
5886
  * checks these hashes.
5847
5887
  *
5848
5888
  * @returns {string|null} null on OK, else the failure reason.
5849
5889
  */
5850
- function verifyBundledProviderHash(name, version, versionDir) {
5851
- const expected = bundledProviderExpectedHash(name, version);
5890
+ function verifyBundledCartridgeHash(name, version, versionDir) {
5891
+ const expected = bundledCartridgeExpectedHash(name, version);
5852
5892
  if (expected === null) {
5853
- return `no baked hash for bundled provider ${name} ${version} — this build did not record it (MFR_BUNDLED_PROVIDER_HASHES)`;
5893
+ return `no baked hash for bundled cartridge ${name} ${version} — this build did not record it (MFR_BUNDLED_CARTRIDGE_HASHES)`;
5854
5894
  }
5855
5895
  let actual;
5856
5896
  try {
5857
5897
  actual = hashCartridgeDirectory(versionDir);
5858
5898
  } catch (e) {
5859
- return `failed to hash bundled provider directory: ${e.message}`;
5899
+ return `failed to hash bundled cartridge directory: ${e.message}`;
5860
5900
  }
5861
5901
  if (actual === expected) return null;
5862
- return `content hash mismatch — baked ${expected}, on-disk ${actual}; the shipped provider differs from what this build was compiled to ship`;
5902
+ return `content hash mismatch — baked ${expected}, on-disk ${actual}; the shipped cartridge differs from what this build was compiled to ship`;
5863
5903
  }
5864
5904
 
5865
5905
  /**
@@ -5920,7 +5960,7 @@ function unixSecondsNow() {
5920
5960
  }
5921
5961
 
5922
5962
  // Default protocol limits / version, mirroring capdag::bifaci::frame.
5923
- const BIFACI_PROTOCOL_VERSION = 3;
5963
+ const BIFACI_PROTOCOL_VERSION = 4;
5924
5964
  const BIFACI_DEFAULT_MAX_FRAME = 3670016;
5925
5965
  const BIFACI_DEFAULT_MAX_CHUNK = 262144;
5926
5966
  const BIFACI_DEFAULT_MAX_REORDER_BUFFER = 64;
@@ -6055,6 +6095,7 @@ function encodeHelloFrame() {
6055
6095
  ['max_reorder_buffer', BIFACI_DEFAULT_MAX_REORDER_BUFFER],
6056
6096
  ['initial_credit', BIFACI_DEFAULT_INITIAL_CREDIT],
6057
6097
  ['version', BIFACI_PROTOCOL_VERSION],
6098
+ ['handler_capacity', 0],
6058
6099
  ],
6059
6100
  };
6060
6101
  const frame = {
@@ -6154,8 +6195,27 @@ function probeCartridgeCapGroups(entryPath) {
6154
6195
  finish(new Error(`cartridge ${entryPath} HELLO failed: expected HELLO, got frame_type ${frameType}`));
6155
6196
  return;
6156
6197
  }
6198
+ const version = decoded.get(BIFACI_KEY_VERSION);
6199
+ if (version !== BIFACI_PROTOCOL_VERSION) {
6200
+ finish(new Error(`cartridge ${entryPath} HELLO failed: protocol version ${version} does not match required version ${BIFACI_PROTOCOL_VERSION}`));
6201
+ return;
6202
+ }
6157
6203
  const meta = decoded.get(BIFACI_KEY_META);
6158
- const manifestVal = meta instanceof Map ? meta.get('manifest') : undefined;
6204
+ if (!(meta instanceof Map)) {
6205
+ finish(new Error(`cartridge ${entryPath} HELLO failed: HELLO meta must be a map`));
6206
+ return;
6207
+ }
6208
+ const metaVersion = meta.get('version');
6209
+ if (metaVersion !== BIFACI_PROTOCOL_VERSION) {
6210
+ finish(new Error(`cartridge ${entryPath} HELLO failed: meta.version ${metaVersion} does not match required version ${BIFACI_PROTOCOL_VERSION}`));
6211
+ return;
6212
+ }
6213
+ const handlerCapacity = meta.get('handler_capacity');
6214
+ if (!Number.isSafeInteger(handlerCapacity) || handlerCapacity < 0) {
6215
+ finish(new Error(`cartridge ${entryPath} HELLO failed: missing required non-negative handler_capacity`));
6216
+ return;
6217
+ }
6218
+ const manifestVal = meta.get('manifest');
6159
6219
  if (!manifestVal || manifestVal.__bytes === undefined) {
6160
6220
  finish(new Error(`cartridge ${entryPath} HELLO failed: Cartridge HELLO missing required manifest`));
6161
6221
  return;
@@ -6370,7 +6430,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6370
6430
  continue;
6371
6431
  }
6372
6432
 
6373
- // Bundled-provider integrity. A cartridge marked installed_from=bundle is
6433
+ // Bundled-cartridge integrity. A cartridge marked installed_from=bundle is
6374
6434
  // shipped INSIDE this build and has no upstream registry to verify against,
6375
6435
  // so it needs its own integrity proof. Platform-split: on macOS the OS
6376
6436
  // code-signature is the guard (baked-hash verification intentionally
@@ -6378,7 +6438,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6378
6438
  // value, else BadInstallation.
6379
6439
  if (cj.installedFrom === CartridgeInstallSource.BUNDLE) {
6380
6440
  if (process.platform !== 'darwin') {
6381
- const reason = verifyBundledProviderHash(cj.name, cj.version, versionDir);
6441
+ const reason = verifyBundledCartridgeHash(cj.name, cj.version, versionDir);
6382
6442
  if (reason !== null) {
6383
6443
  discovered.push(DiscoveredCartridge.incompatible({
6384
6444
  version_dir: versionDir,
@@ -6388,7 +6448,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6388
6448
  version: cj.version,
6389
6449
  error: new CartridgeAttachmentError(
6390
6450
  CartridgeAttachmentErrorKind.BAD_INSTALLATION,
6391
- `bundled provider integrity check failed: ${reason}`,
6451
+ `bundled cartridge integrity check failed: ${reason}`,
6392
6452
  detectedAt
6393
6453
  ),
6394
6454
  }));
@@ -8148,8 +8208,8 @@ module.exports = {
8148
8208
  CartridgeJsonError,
8149
8209
  CartridgeJson,
8150
8210
  hashCartridgeDirectory,
8151
- BUNDLED_PROVIDER_HASHES,
8152
- verifyBundledProviderHash,
8211
+ BUNDLED_CARTRIDGE_HASHES,
8212
+ verifyBundledCartridgeHash,
8153
8213
  DiscoveryIdentity,
8154
8214
  DiscoveredCartridge,
8155
8215
  probeCartridgeCapGroups,