capdag 1.226.626 → 1.240.679
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 +126 -61
- package/capdag.js +209 -80
- package/capdag.test.js +622 -103
- package/package.json +1 -1
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
|
|
@@ -2530,21 +2570,22 @@ class Cap {
|
|
|
2530
2570
|
* @param {Object|null} metadataJson - Optional arbitrary metadata as JSON object
|
|
2531
2571
|
* @param {string|null} documentation - Optional long-form markdown documentation. Rendered in capability info panels, the cap navigator, capdag-dot-com, and anywhere else a rich-text explanation of the cap is useful.
|
|
2532
2572
|
*/
|
|
2533
|
-
constructor(urn, title,
|
|
2573
|
+
constructor(urn, title, aliases, capDescription = null, metadata = {}, metadataJson = null, documentation = null) {
|
|
2534
2574
|
if (!(urn instanceof CapUrn)) {
|
|
2535
2575
|
throw new Error('URN must be a CapUrn instance');
|
|
2536
2576
|
}
|
|
2537
2577
|
if (!title || typeof title !== 'string') {
|
|
2538
2578
|
throw new Error('Title is required and must be a string');
|
|
2539
2579
|
}
|
|
2540
|
-
if (!
|
|
2541
|
-
throw new Error('
|
|
2580
|
+
if (!Array.isArray(aliases) || aliases.length === 0) {
|
|
2581
|
+
throw new Error('Aliases are required and must be a non-empty array');
|
|
2542
2582
|
}
|
|
2543
2583
|
|
|
2544
2584
|
this.urn = urn;
|
|
2545
2585
|
this.version = 0;
|
|
2546
2586
|
this.title = title;
|
|
2547
|
-
this.
|
|
2587
|
+
this.aliases = aliases;
|
|
2588
|
+
this.is_abstract = false;
|
|
2548
2589
|
this.cap_description = capDescription;
|
|
2549
2590
|
this.documentation = documentation;
|
|
2550
2591
|
this.metadata = metadata || {};
|
|
@@ -2752,7 +2793,8 @@ class Cap {
|
|
|
2752
2793
|
|
|
2753
2794
|
return this.urn.equals(other.urn) &&
|
|
2754
2795
|
this.title === other.title &&
|
|
2755
|
-
this.
|
|
2796
|
+
JSON.stringify([...this.aliases].sort()) === JSON.stringify([...other.aliases].sort()) &&
|
|
2797
|
+
this.is_abstract === other.is_abstract &&
|
|
2756
2798
|
this.cap_description === other.cap_description &&
|
|
2757
2799
|
this.documentation === other.documentation &&
|
|
2758
2800
|
JSON.stringify(this.metadata) === JSON.stringify(other.metadata) &&
|
|
@@ -2772,7 +2814,7 @@ class Cap {
|
|
|
2772
2814
|
const result = {
|
|
2773
2815
|
urn: this.urn.toString(),
|
|
2774
2816
|
title: this.title,
|
|
2775
|
-
|
|
2817
|
+
aliases: this.aliases,
|
|
2776
2818
|
cap_description: this.cap_description,
|
|
2777
2819
|
metadata: this.metadata,
|
|
2778
2820
|
args: this.args.map(a => a.toJSON()),
|
|
@@ -2783,6 +2825,10 @@ class Cap {
|
|
|
2783
2825
|
result.version = this.version;
|
|
2784
2826
|
}
|
|
2785
2827
|
|
|
2828
|
+
if (this.is_abstract) {
|
|
2829
|
+
result.abstract = true;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2786
2832
|
// Long-form markdown documentation. Only emitted when set, to match
|
|
2787
2833
|
// the Rust serializer which skips this field when None.
|
|
2788
2834
|
if (typeof this.documentation === 'string' && this.documentation.length > 0) {
|
|
@@ -2821,7 +2867,11 @@ class Cap {
|
|
|
2821
2867
|
const documentation = (typeof json.documentation === 'string' && json.documentation.length > 0)
|
|
2822
2868
|
? json.documentation
|
|
2823
2869
|
: null;
|
|
2824
|
-
|
|
2870
|
+
if (!Array.isArray(json.aliases) || json.aliases.length === 0) {
|
|
2871
|
+
throw new Error(`cap '${json.urn}' must declare at least one alias (the 'aliases' field is required and non-empty)`);
|
|
2872
|
+
}
|
|
2873
|
+
const cap = new Cap(urn, json.title, json.aliases, json.cap_description, json.metadata, json.metadata_json, documentation);
|
|
2874
|
+
cap.is_abstract = json.abstract === true;
|
|
2825
2875
|
cap.version = (typeof json.version === 'number' && json.version !== 0) ? json.version : 0;
|
|
2826
2876
|
// Parse args (new format)
|
|
2827
2877
|
if (json.args && Array.isArray(json.args)) {
|
|
@@ -2910,7 +2960,7 @@ class CapGroup {
|
|
|
2910
2960
|
}
|
|
2911
2961
|
|
|
2912
2962
|
/**
|
|
2913
|
-
* Unified cap-based manifest for components (
|
|
2963
|
+
* Unified cap-based manifest for components (cartridges).
|
|
2914
2964
|
*
|
|
2915
2965
|
* `(registry_url, channel, name, version)` is the cartridge's full
|
|
2916
2966
|
* identity — each (registry, channel) pair is an independent namespace.
|
|
@@ -3046,20 +3096,20 @@ class CapManifest {
|
|
|
3046
3096
|
/**
|
|
3047
3097
|
* Helper functions for creating capabilities
|
|
3048
3098
|
*/
|
|
3049
|
-
function createCap(urn, title,
|
|
3050
|
-
return new Cap(urn, title,
|
|
3099
|
+
function createCap(urn, title, aliases) {
|
|
3100
|
+
return new Cap(urn, title, aliases);
|
|
3051
3101
|
}
|
|
3052
3102
|
|
|
3053
|
-
function createCapWithDescription(urn, title,
|
|
3054
|
-
return new Cap(urn, title,
|
|
3103
|
+
function createCapWithDescription(urn, title, aliases, description) {
|
|
3104
|
+
return new Cap(urn, title, aliases, description);
|
|
3055
3105
|
}
|
|
3056
3106
|
|
|
3057
|
-
function createCapWithMetadata(urn, title,
|
|
3058
|
-
return new Cap(urn, title,
|
|
3107
|
+
function createCapWithMetadata(urn, title, aliases, metadata) {
|
|
3108
|
+
return new Cap(urn, title, aliases, null, metadata);
|
|
3059
3109
|
}
|
|
3060
3110
|
|
|
3061
|
-
function createCapWithDescriptionAndMetadata(urn, title,
|
|
3062
|
-
return new Cap(urn, title,
|
|
3111
|
+
function createCapWithDescriptionAndMetadata(urn, title, aliases, description, metadata) {
|
|
3112
|
+
return new Cap(urn, title, aliases, description, metadata);
|
|
3063
3113
|
}
|
|
3064
3114
|
|
|
3065
3115
|
// ============================================================================
|
|
@@ -3747,9 +3797,9 @@ class CapValidator {
|
|
|
3747
3797
|
});
|
|
3748
3798
|
}
|
|
3749
3799
|
|
|
3750
|
-
if (!cap.
|
|
3800
|
+
if (!Array.isArray(cap.aliases) || cap.aliases.length === 0) {
|
|
3751
3801
|
throw new ValidationError('InvalidCapSchema', capUrn, {
|
|
3752
|
-
issue: 'Cap must have
|
|
3802
|
+
issue: 'Cap must have at least one alias'
|
|
3753
3803
|
});
|
|
3754
3804
|
}
|
|
3755
3805
|
|
|
@@ -4355,37 +4405,55 @@ class CartridgeCapGroup {
|
|
|
4355
4405
|
// in synchronous contexts must await.
|
|
4356
4406
|
|
|
4357
4407
|
const DEV_SLUG = "dev";
|
|
4358
|
-
const SLUG_HEX_LEN = 16;
|
|
4359
4408
|
|
|
4360
4409
|
/**
|
|
4361
|
-
*
|
|
4410
|
+
* The authority (host[:port]) of a registry URL: after `://` up to the next
|
|
4411
|
+
* `/`, `?`, or `#` (path/query/fragment discarded).
|
|
4412
|
+
*/
|
|
4413
|
+
function authorityOfRegistryUrl(url) {
|
|
4414
|
+
const i = url.indexOf("://");
|
|
4415
|
+
const afterScheme = i >= 0 ? url.slice(i + 3) : url;
|
|
4416
|
+
const m = afterScheme.search(/[/?#]/);
|
|
4417
|
+
return m >= 0 ? afterScheme.slice(0, m) : afterScheme;
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
/**
|
|
4421
|
+
* Compute the on-disk slug for a registry URL. Mirrors
|
|
4422
|
+
* capdag::cartridge_slug::slug_for byte-for-byte: `DEV_SLUG` for null/undefined,
|
|
4423
|
+
* otherwise a path-safe transform of the URL's authority (host[:port]) —
|
|
4424
|
+
* ASCII-lowercased, every character outside `[a-z0-9.-]` replaced by `-`.
|
|
4425
|
+
* Depends ONLY on the authority; path/version/query/trailing-slash/host-case
|
|
4426
|
+
* do not change it.
|
|
4362
4427
|
*
|
|
4363
|
-
* @param {string|null|undefined} registryUrl
|
|
4364
|
-
*
|
|
4365
|
-
* @returns {Promise<string>} The slug — `DEV_SLUG` for null,
|
|
4366
|
-
* otherwise the first 16 lowercase-hex characters of
|
|
4367
|
-
* sha256(registryUrl).
|
|
4428
|
+
* @param {string|null|undefined} registryUrl
|
|
4429
|
+
* @returns {string}
|
|
4368
4430
|
*/
|
|
4369
|
-
|
|
4431
|
+
function slugForRegistryUrlSync(registryUrl) {
|
|
4370
4432
|
if (registryUrl === null || registryUrl === undefined) {
|
|
4371
4433
|
return DEV_SLUG;
|
|
4372
4434
|
}
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
const digestBytes = new Uint8Array(digestBuffer);
|
|
4378
|
-
let hex = "";
|
|
4379
|
-
for (const b of digestBytes) {
|
|
4380
|
-
hex += b.toString(16).padStart(2, "0");
|
|
4435
|
+
let out = "";
|
|
4436
|
+
for (const ch of authorityOfRegistryUrl(registryUrl)) {
|
|
4437
|
+
const lc = ch >= "A" && ch <= "Z" ? ch.toLowerCase() : ch;
|
|
4438
|
+
out += (lc >= "a" && lc <= "z") || (lc >= "0" && lc <= "9") || lc === "." || lc === "-" ? lc : "-";
|
|
4381
4439
|
}
|
|
4382
|
-
return
|
|
4440
|
+
return out;
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
/**
|
|
4444
|
+
* Async wrapper retained for callers that `await` the slug. The computation is
|
|
4445
|
+
* synchronous (no hashing) — the authority transform needs no crypto.
|
|
4446
|
+
* @returns {Promise<string>}
|
|
4447
|
+
*/
|
|
4448
|
+
async function slugForRegistryUrl(registryUrl) {
|
|
4449
|
+
return slugForRegistryUrlSync(registryUrl);
|
|
4383
4450
|
}
|
|
4384
4451
|
|
|
4385
4452
|
function isRegistrySlug(s) {
|
|
4386
4453
|
return typeof s === "string"
|
|
4387
|
-
&& s.length
|
|
4388
|
-
&&
|
|
4454
|
+
&& s.length > 0
|
|
4455
|
+
&& s !== DEV_SLUG
|
|
4456
|
+
&& /^[a-z0-9.-]+$/.test(s);
|
|
4389
4457
|
}
|
|
4390
4458
|
|
|
4391
4459
|
// =============================================================================
|
|
@@ -4744,6 +4812,18 @@ class CartridgeRepoClient {
|
|
|
4744
4812
|
if (data.schemaVersion !== '5.0') {
|
|
4745
4813
|
throw new Error(`Cartridge registry from ${repoUrl} has schemaVersion '${data.schemaVersion}'; required: 5.0`);
|
|
4746
4814
|
}
|
|
4815
|
+
// Cartridge registry regime version. v0 was the pre-versioning legacy at
|
|
4816
|
+
// the bare /manifest path; this speaks only v1 at /v1/manifest. A manifest
|
|
4817
|
+
// from a different regime version is rejected, not reinterpreted.
|
|
4818
|
+
if (data.registryVersion !== 1) {
|
|
4819
|
+
throw new Error(`Cartridge registry from ${repoUrl} has registryVersion '${data.registryVersion}'; this build speaks v1`);
|
|
4820
|
+
}
|
|
4821
|
+
// A cartridge registry must declare the fabric its caps resolve against. The
|
|
4822
|
+
// client cross-checks this equals its own fabric (so every registry it uses
|
|
4823
|
+
// shares one fabric); here we require it is at least present.
|
|
4824
|
+
if (typeof data.fabricRegistryUrl !== 'string' || data.fabricRegistryUrl.length === 0) {
|
|
4825
|
+
throw new Error(`Cartridge registry from ${repoUrl}: missing required 'fabricRegistryUrl' (the fabric its caps resolve against)`);
|
|
4826
|
+
}
|
|
4747
4827
|
// Self-referential check: the manifest declares its own URL via
|
|
4748
4828
|
// `registryUrl`. It must match the URL we just fetched from
|
|
4749
4829
|
// byte-for-byte — a mismatch is a manifest-corruption signal
|
|
@@ -5021,6 +5101,12 @@ class CartridgeRepoServer {
|
|
|
5021
5101
|
if (this.registry.schemaVersion !== '5.0') {
|
|
5022
5102
|
throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: 5.0`);
|
|
5023
5103
|
}
|
|
5104
|
+
if (this.registry.registryVersion !== 1) {
|
|
5105
|
+
throw new Error(`Unsupported cartridge registry version: ${this.registry.registryVersion}. This build speaks v1.`);
|
|
5106
|
+
}
|
|
5107
|
+
if (typeof this.registry.fabricRegistryUrl !== 'string' || this.registry.fabricRegistryUrl.length === 0) {
|
|
5108
|
+
throw new Error('Registry must declare a non-empty `fabricRegistryUrl` (the fabric its caps resolve against)');
|
|
5109
|
+
}
|
|
5024
5110
|
if (typeof this.registry.registryUrl !== 'string' || this.registry.registryUrl.length === 0) {
|
|
5025
5111
|
throw new Error('Registry must have a non-empty top-level `registryUrl` field (self-referential URL)');
|
|
5026
5112
|
}
|
|
@@ -5240,7 +5326,7 @@ const CartridgeAttachmentErrorKind = Object.freeze({
|
|
|
5240
5326
|
QUARANTINED: 'quarantined',
|
|
5241
5327
|
// On-disk install context disagrees with the cartridge.json the cartridge
|
|
5242
5328
|
// declares — the slug folder doesn't match slug_for(registry_url), the
|
|
5243
|
-
// 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
|
|
5244
5330
|
// integrity proof failed. Structurally well-formed but cannot be trusted
|
|
5245
5331
|
// because its placement on disk does not match what it claims to be.
|
|
5246
5332
|
BAD_INSTALLATION: 'bad_installation',
|
|
@@ -5450,24 +5536,15 @@ class InstalledCartridgeRecord {
|
|
|
5450
5536
|
/**
|
|
5451
5537
|
* Synchronous on-disk slug for a registry URL. Mirrors
|
|
5452
5538
|
* capdag::bifaci::cartridge_slug::slug_for byte-for-byte: `null`/`undefined`
|
|
5453
|
-
* (a dev cartridge) → the literal `DEV_SLUG`; otherwise
|
|
5454
|
-
*
|
|
5455
|
-
*
|
|
5456
|
-
* The async `slugForRegistryUrl` above uses Web Crypto for browser parity;
|
|
5457
|
-
* discovery runs only under Node and needs a synchronous slug in tight
|
|
5458
|
-
* per-file loops, so this uses Node's synchronous `crypto.createHash`. Both
|
|
5459
|
-
* produce identical output.
|
|
5539
|
+
* (a dev cartridge) → the literal `DEV_SLUG`; otherwise a path-safe transform
|
|
5540
|
+
* of the URL's authority (host[:port]). Identical output to the async
|
|
5541
|
+
* `slugForRegistryUrl` above — no hashing, so both are trivially in sync.
|
|
5460
5542
|
*
|
|
5461
5543
|
* @param {string|null|undefined} registryUrl
|
|
5462
5544
|
* @returns {string}
|
|
5463
5545
|
*/
|
|
5464
5546
|
function slugForSync(registryUrl) {
|
|
5465
|
-
|
|
5466
|
-
return DEV_SLUG;
|
|
5467
|
-
}
|
|
5468
|
-
const crypto = require('crypto');
|
|
5469
|
-
const hex = crypto.createHash('sha256').update(registryUrl, 'utf8').digest('hex');
|
|
5470
|
-
return hex.slice(0, SLUG_HEX_LEN);
|
|
5547
|
+
return slugForRegistryUrlSync(registryUrl);
|
|
5471
5548
|
}
|
|
5472
5549
|
|
|
5473
5550
|
/**
|
|
@@ -5780,49 +5857,49 @@ function hashCartridgeDirectory(dir) {
|
|
|
5780
5857
|
}
|
|
5781
5858
|
|
|
5782
5859
|
// ---------------------------------------------------------------------------
|
|
5783
|
-
// Bundled-
|
|
5860
|
+
// Bundled-cartridge integrity (non-macOS).
|
|
5784
5861
|
// ---------------------------------------------------------------------------
|
|
5785
5862
|
//
|
|
5786
|
-
// Mirrors capdag::cartridge_discovery::
|
|
5787
|
-
//
|
|
5788
|
-
// 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
|
|
5789
5866
|
// `installed_from: bundle` cartridge has no baked hash and is rejected.
|
|
5790
|
-
const
|
|
5867
|
+
const BUNDLED_CARTRIDGE_HASHES = Object.freeze([]); // [name, version, hash] triples
|
|
5791
5868
|
|
|
5792
5869
|
/**
|
|
5793
|
-
* 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
|
|
5794
5871
|
* (name, version) was not recorded at build time.
|
|
5795
5872
|
* @returns {string|null}
|
|
5796
5873
|
*/
|
|
5797
|
-
function
|
|
5798
|
-
for (const [n, v, h] of
|
|
5874
|
+
function bundledCartridgeExpectedHash(name, version) {
|
|
5875
|
+
for (const [n, v, h] of BUNDLED_CARTRIDGE_HASHES) {
|
|
5799
5876
|
if (n === name && v === version) return h;
|
|
5800
5877
|
}
|
|
5801
5878
|
return null;
|
|
5802
5879
|
}
|
|
5803
5880
|
|
|
5804
5881
|
/**
|
|
5805
|
-
* Verify a bundled
|
|
5882
|
+
* Verify a bundled cartridge's on-disk content against the hash baked into this
|
|
5806
5883
|
* build. Returns null on success, or an error-reason string. Mirrors
|
|
5807
|
-
*
|
|
5884
|
+
* verify_bundled_cartridge_hash. Non-macOS only: macOS bundled-cartridge
|
|
5808
5885
|
* integrity is the OS code-signature, so the engine there neither bakes nor
|
|
5809
5886
|
* checks these hashes.
|
|
5810
5887
|
*
|
|
5811
5888
|
* @returns {string|null} null on OK, else the failure reason.
|
|
5812
5889
|
*/
|
|
5813
|
-
function
|
|
5814
|
-
const expected =
|
|
5890
|
+
function verifyBundledCartridgeHash(name, version, versionDir) {
|
|
5891
|
+
const expected = bundledCartridgeExpectedHash(name, version);
|
|
5815
5892
|
if (expected === null) {
|
|
5816
|
-
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)`;
|
|
5817
5894
|
}
|
|
5818
5895
|
let actual;
|
|
5819
5896
|
try {
|
|
5820
5897
|
actual = hashCartridgeDirectory(versionDir);
|
|
5821
5898
|
} catch (e) {
|
|
5822
|
-
return `failed to hash bundled
|
|
5899
|
+
return `failed to hash bundled cartridge directory: ${e.message}`;
|
|
5823
5900
|
}
|
|
5824
5901
|
if (actual === expected) return null;
|
|
5825
|
-
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`;
|
|
5826
5903
|
}
|
|
5827
5904
|
|
|
5828
5905
|
/**
|
|
@@ -5838,14 +5915,21 @@ class DiscoveryIdentity {
|
|
|
5838
5915
|
* @param {string|null} opts.registryUrl - Some(url) for release/nightly
|
|
5839
5916
|
* hosts, null for dev hosts.
|
|
5840
5917
|
* @param {number} opts.fabricManifestVersion
|
|
5918
|
+
* @param {number} opts.cartridgeRegistryVersion - The registry regime version
|
|
5919
|
+
* this host speaks; an on-disk PATH level ({slug}/v{N}/{channel}/…), pinned
|
|
5920
|
+
* like the channel so a v1 host never scans a v2 tree.
|
|
5841
5921
|
*/
|
|
5842
|
-
constructor({ channel, registryUrl, fabricManifestVersion }) {
|
|
5922
|
+
constructor({ channel, registryUrl, fabricManifestVersion, cartridgeRegistryVersion }) {
|
|
5843
5923
|
if (channel !== CartridgeChannel.Release && channel !== CartridgeChannel.Nightly) {
|
|
5844
5924
|
throw new Error(`DiscoveryIdentity: invalid channel '${channel}'`);
|
|
5845
5925
|
}
|
|
5926
|
+
if (!Number.isInteger(cartridgeRegistryVersion) || cartridgeRegistryVersion < 1) {
|
|
5927
|
+
throw new Error(`DiscoveryIdentity: cartridgeRegistryVersion must be an integer >= 1, got '${cartridgeRegistryVersion}'`);
|
|
5928
|
+
}
|
|
5846
5929
|
this.channel = channel;
|
|
5847
5930
|
this.registryUrl = registryUrl !== undefined ? registryUrl : null;
|
|
5848
5931
|
this.fabricManifestVersion = fabricManifestVersion;
|
|
5932
|
+
this.cartridgeRegistryVersion = cartridgeRegistryVersion;
|
|
5849
5933
|
}
|
|
5850
5934
|
|
|
5851
5935
|
/** This host's own baked-registry slug (`dev` when registryUrl is null). */
|
|
@@ -5876,7 +5960,7 @@ function unixSecondsNow() {
|
|
|
5876
5960
|
}
|
|
5877
5961
|
|
|
5878
5962
|
// Default protocol limits / version, mirroring capdag::bifaci::frame.
|
|
5879
|
-
const BIFACI_PROTOCOL_VERSION =
|
|
5963
|
+
const BIFACI_PROTOCOL_VERSION = 4;
|
|
5880
5964
|
const BIFACI_DEFAULT_MAX_FRAME = 3670016;
|
|
5881
5965
|
const BIFACI_DEFAULT_MAX_CHUNK = 262144;
|
|
5882
5966
|
const BIFACI_DEFAULT_MAX_REORDER_BUFFER = 64;
|
|
@@ -6011,6 +6095,7 @@ function encodeHelloFrame() {
|
|
|
6011
6095
|
['max_reorder_buffer', BIFACI_DEFAULT_MAX_REORDER_BUFFER],
|
|
6012
6096
|
['initial_credit', BIFACI_DEFAULT_INITIAL_CREDIT],
|
|
6013
6097
|
['version', BIFACI_PROTOCOL_VERSION],
|
|
6098
|
+
['handler_capacity', 0],
|
|
6014
6099
|
],
|
|
6015
6100
|
};
|
|
6016
6101
|
const frame = {
|
|
@@ -6110,8 +6195,27 @@ function probeCartridgeCapGroups(entryPath) {
|
|
|
6110
6195
|
finish(new Error(`cartridge ${entryPath} HELLO failed: expected HELLO, got frame_type ${frameType}`));
|
|
6111
6196
|
return;
|
|
6112
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
|
+
}
|
|
6113
6203
|
const meta = decoded.get(BIFACI_KEY_META);
|
|
6114
|
-
|
|
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');
|
|
6115
6219
|
if (!manifestVal || manifestVal.__bytes === undefined) {
|
|
6116
6220
|
finish(new Error(`cartridge ${entryPath} HELLO failed: Cartridge HELLO missing required manifest`));
|
|
6117
6221
|
return;
|
|
@@ -6175,7 +6279,9 @@ async function discoverCartridges(cartridgesRoot, identity) {
|
|
|
6175
6279
|
continue;
|
|
6176
6280
|
}
|
|
6177
6281
|
const expectedSlug = slugEntry.name;
|
|
6178
|
-
|
|
6282
|
+
// {slug}/v{cartridgeRegistryVersion}/{channel}/… — the registry regime
|
|
6283
|
+
// version is a path level pinned to the host's version (like the channel).
|
|
6284
|
+
const scanRoot = path.join(slugDir, `v${identity.cartridgeRegistryVersion}`, identity.channel);
|
|
6179
6285
|
let scanStat;
|
|
6180
6286
|
try { scanStat = fs.statSync(scanRoot); } catch (_e) { continue; }
|
|
6181
6287
|
if (!scanStat.isDirectory()) continue;
|
|
@@ -6324,7 +6430,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
6324
6430
|
continue;
|
|
6325
6431
|
}
|
|
6326
6432
|
|
|
6327
|
-
// Bundled-
|
|
6433
|
+
// Bundled-cartridge integrity. A cartridge marked installed_from=bundle is
|
|
6328
6434
|
// shipped INSIDE this build and has no upstream registry to verify against,
|
|
6329
6435
|
// so it needs its own integrity proof. Platform-split: on macOS the OS
|
|
6330
6436
|
// code-signature is the guard (baked-hash verification intentionally
|
|
@@ -6332,7 +6438,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
6332
6438
|
// value, else BadInstallation.
|
|
6333
6439
|
if (cj.installedFrom === CartridgeInstallSource.BUNDLE) {
|
|
6334
6440
|
if (process.platform !== 'darwin') {
|
|
6335
|
-
const reason =
|
|
6441
|
+
const reason = verifyBundledCartridgeHash(cj.name, cj.version, versionDir);
|
|
6336
6442
|
if (reason !== null) {
|
|
6337
6443
|
discovered.push(DiscoveredCartridge.incompatible({
|
|
6338
6444
|
version_dir: versionDir,
|
|
@@ -6342,7 +6448,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
6342
6448
|
version: cj.version,
|
|
6343
6449
|
error: new CartridgeAttachmentError(
|
|
6344
6450
|
CartridgeAttachmentErrorKind.BAD_INSTALLATION,
|
|
6345
|
-
`bundled
|
|
6451
|
+
`bundled cartridge integrity check failed: ${reason}`,
|
|
6346
6452
|
detectedAt
|
|
6347
6453
|
),
|
|
6348
6454
|
}));
|
|
@@ -7425,7 +7531,8 @@ class FabricRegistryEntry {
|
|
|
7425
7531
|
constructor(data) {
|
|
7426
7532
|
this.urn = data.urn;
|
|
7427
7533
|
this.title = data.title || '';
|
|
7428
|
-
this.
|
|
7534
|
+
this.aliases = data.aliases || [];
|
|
7535
|
+
this.isAbstract = data.abstract === true;
|
|
7429
7536
|
this.description = data.cap_description || '';
|
|
7430
7537
|
this.args = data.args || [];
|
|
7431
7538
|
this.output = data.output || null;
|
|
@@ -7880,8 +7987,30 @@ class Manifest {
|
|
|
7880
7987
|
}
|
|
7881
7988
|
}
|
|
7882
7989
|
|
|
7990
|
+
// Unified configurable planner vocabulary (mirrors Rust plan_space.rs and the
|
|
7991
|
+
// PlanMachines/DiscoverConvergentTargets proto surface).
|
|
7992
|
+
const planner = require('./planner.js');
|
|
7993
|
+
|
|
7883
7994
|
// Export for CommonJS
|
|
7884
7995
|
module.exports = {
|
|
7996
|
+
// Planner plan-space vocabulary
|
|
7997
|
+
PlanStateError: planner.PlanStateError,
|
|
7998
|
+
ConvergencePresence: planner.ConvergencePresence,
|
|
7999
|
+
ConvergenceLocation: planner.ConvergenceLocation,
|
|
8000
|
+
ConvergenceMechanism: planner.ConvergenceMechanism,
|
|
8001
|
+
ConvergenceArity: planner.ConvergenceArity,
|
|
8002
|
+
DivergencePresence: planner.DivergencePresence,
|
|
8003
|
+
DivergenceLocation: planner.DivergenceLocation,
|
|
8004
|
+
RankPolicy: planner.RankPolicy,
|
|
8005
|
+
PlanMode: planner.PlanMode,
|
|
8006
|
+
PlanRequest: planner.PlanRequest,
|
|
8007
|
+
parsePlanApex: planner.parsePlanApex,
|
|
8008
|
+
parsePlanProfile: planner.parsePlanProfile,
|
|
8009
|
+
parsePlanCost: planner.parsePlanCost,
|
|
8010
|
+
parsePlanCandidate: planner.parsePlanCandidate,
|
|
8011
|
+
parsePlanCandidates: planner.parsePlanCandidates,
|
|
8012
|
+
parseConvergentTarget: planner.parseConvergentTarget,
|
|
8013
|
+
parseConvergentTargets: planner.parseConvergentTargets,
|
|
7885
8014
|
ALIAS_TARGET_CAP,
|
|
7886
8015
|
ALIAS_TARGET_MEDIA,
|
|
7887
8016
|
tokenIsUrn,
|
|
@@ -8079,15 +8208,15 @@ module.exports = {
|
|
|
8079
8208
|
CartridgeJsonError,
|
|
8080
8209
|
CartridgeJson,
|
|
8081
8210
|
hashCartridgeDirectory,
|
|
8082
|
-
|
|
8083
|
-
|
|
8211
|
+
BUNDLED_CARTRIDGE_HASHES,
|
|
8212
|
+
verifyBundledCartridgeHash,
|
|
8084
8213
|
DiscoveryIdentity,
|
|
8085
8214
|
DiscoveredCartridge,
|
|
8086
8215
|
probeCartridgeCapGroups,
|
|
8087
8216
|
discoverCartridges,
|
|
8088
8217
|
// Registry slug
|
|
8089
8218
|
DEV_SLUG,
|
|
8090
|
-
|
|
8219
|
+
slugForRegistryUrlSync,
|
|
8091
8220
|
slugForRegistryUrl,
|
|
8092
8221
|
isRegistrySlug,
|
|
8093
8222
|
// Machine notation
|