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.
- package/cap-fab-renderer.js +125 -60
- package/capdag.js +87 -27
- package/capdag.test.js +430 -83
- package/package.json +2 -1
- package/planner.js +424 -0
package/cap-fab-renderer.js
CHANGED
|
@@ -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`
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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.
|
|
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.
|
|
836
|
-
&& (typeof outcome.
|
|
837
|
-
throw new Error(`CapFabRenderer: ${path}.
|
|
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 (!
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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 =
|
|
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)
|
|
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)
|
|
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
|
|
1402
|
-
//
|
|
1403
|
-
//
|
|
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.
|
|
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
|
-
|
|
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
|
|
1895
|
-
const
|
|
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
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 (
|
|
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-
|
|
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-
|
|
5860
|
+
// Bundled-cartridge integrity (non-macOS).
|
|
5821
5861
|
// ---------------------------------------------------------------------------
|
|
5822
5862
|
//
|
|
5823
|
-
// Mirrors capdag::cartridge_discovery::
|
|
5824
|
-
//
|
|
5825
|
-
// and in this mirror, which bundles no
|
|
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
|
|
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
|
|
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
|
|
5835
|
-
for (const [n, v, h] of
|
|
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
|
|
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
|
-
*
|
|
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
|
|
5851
|
-
const expected =
|
|
5890
|
+
function verifyBundledCartridgeHash(name, version, versionDir) {
|
|
5891
|
+
const expected = bundledCartridgeExpectedHash(name, version);
|
|
5852
5892
|
if (expected === null) {
|
|
5853
|
-
return `no baked hash for bundled
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
8152
|
-
|
|
8211
|
+
BUNDLED_CARTRIDGE_HASHES,
|
|
8212
|
+
verifyBundledCartridgeHash,
|
|
8153
8213
|
DiscoveryIdentity,
|
|
8154
8214
|
DiscoveredCartridge,
|
|
8155
8215
|
probeCartridgeCapGroups,
|