@intx/tool-packaging 0.2.2 → 0.3.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.
- package/README.md +17 -12
- package/dist/atomic-apply.d.ts +5 -0
- package/dist/atomic-apply.js +1 -0
- package/dist/cache.d.ts +1 -1
- package/dist/cache.js +14 -19
- package/dist/git-materialize.d.ts +19 -0
- package/dist/git-materialize.js +77 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/loader-internal.d.ts +47 -0
- package/dist/loader-internal.js +66 -0
- package/dist/loader.d.ts +71 -62
- package/dist/loader.js +135 -855
- package/dist/registry-fetch.d.ts +40 -0
- package/dist/registry-fetch.js +231 -0
- package/dist/resolver.d.ts +8 -5
- package/dist/resolver.js +23 -7
- package/dist/store-layout.d.ts +25 -0
- package/dist/store-layout.js +683 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -19,23 +19,28 @@ transitives are reachable from inside it.
|
|
|
19
19
|
Concretely the loader:
|
|
20
20
|
|
|
21
21
|
1. Walks every manifest entry and validates that each one is
|
|
22
|
-
|
|
23
|
-
registry resolves end-to-end against that registry
|
|
24
|
-
|
|
25
|
-
map.
|
|
22
|
+
source-consistent: every entry the manifest says lives at a
|
|
23
|
+
registry resolves end-to-end against that registry; an asset entry
|
|
24
|
+
whose `package.format` is `"tarball"` resolves against the asset's
|
|
25
|
+
`assetMounts` map; an asset entry whose `package.format` is
|
|
26
|
+
`"source"` (a git subtree, used by source-format workflow
|
|
27
|
+
definitions) resolves against the separate `gitDirs` map.
|
|
26
28
|
2. Materializes every entry into the content-addressable tarball
|
|
27
29
|
cache: bytes pulled from the entry's source on a miss are verified
|
|
28
30
|
through `cache.put`, and the bytes are then unpacked once via
|
|
29
31
|
`cache.extractTarball`. A single sha512 produces a single
|
|
30
|
-
extraction shared across instances.
|
|
32
|
+
extraction shared across instances. A `"source"` asset entry is the
|
|
33
|
+
exception: it has no tarball and no SRI, so it bypasses the cache
|
|
34
|
+
entirely — its subtree is checked out in place from the indexed
|
|
35
|
+
`gitDir` (`materializeGitEntry`) and verified against the frozen git
|
|
36
|
+
`treeOid` rather than an sha512.
|
|
31
37
|
3. Lays out each entry under `<scratch>/store/<name>/<version>/` by
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
(which has its own `node_modules/`).
|
|
38
|
+
copying the file tree from the cache extraction — or, for a
|
|
39
|
+
`"source"` entry, from its git checkout. Each layout directory then
|
|
40
|
+
gets `node_modules/<dep>` symlinked into it pointing at the sibling
|
|
41
|
+
`store/<dep>/<depVersion>/` chosen for that requirer. Symlinks at
|
|
42
|
+
the `node_modules/` boundary let Node's realpath-based resolver walk
|
|
43
|
+
to the dep's own layout dir (which has its own `node_modules/`).
|
|
39
44
|
4. Dynamically imports each top-level entry's `interchange.tools`
|
|
40
45
|
module and collects the `AnnotatedToolFactory` /
|
|
41
46
|
`AnnotatedPluginFactory` values it exports.
|
package/dist/atomic-apply.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface ApplyAtomicArgs {
|
|
|
12
12
|
* Forwarded verbatim to `ToolLoader.loadManifest`.
|
|
13
13
|
*/
|
|
14
14
|
readonly assetMounts: ReadonlyMap<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* Maps a source-format asset entry's `source.assetId` to an absolute
|
|
17
|
+
* indexed git directory. Forwarded verbatim to `ToolLoader.loadManifest`.
|
|
18
|
+
*/
|
|
19
|
+
readonly gitDirs: ReadonlyMap<string, string>;
|
|
15
20
|
readonly attemptId: string;
|
|
16
21
|
/**
|
|
17
22
|
* The deploy id the instance is currently running. Retained on disk
|
package/dist/atomic-apply.js
CHANGED
package/dist/cache.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface TarballCache {
|
|
|
18
18
|
* cannot reuse the on-disk extraction. The extraction directory's
|
|
19
19
|
* physical reclaim is deferred until every in-flight reader released
|
|
20
20
|
* by `extractTarball` has dropped its reference, so an evict that
|
|
21
|
-
* races a concurrent `
|
|
21
|
+
* races a concurrent `copyTree` walk of the same extraction does
|
|
22
22
|
* not pull the tree out from under the walk and surface as ENOENT.
|
|
23
23
|
*
|
|
24
24
|
* Until every reader releases, the on-disk extraction is left in
|
package/dist/cache.js
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
// one entry (used after extraction discovers a corrupted file on disk).
|
|
12
12
|
//
|
|
13
13
|
// `maxBytes` covers both the tarball bytes and the size of the
|
|
14
|
-
// extracted directory tree the loader
|
|
14
|
+
// extracted directory tree the loader copies from. The extraction
|
|
15
15
|
// tree dominates disk usage in practice (tarballs are gzip-compressed;
|
|
16
|
-
// the unpacked tree is multiples larger), so the cap reflects the
|
|
17
|
-
//
|
|
16
|
+
// the unpacked tree is multiples larger), so the cap reflects the cost
|
|
17
|
+
// of holding one cache entry but excludes the loader's per-instance
|
|
18
|
+
// copies.
|
|
18
19
|
//
|
|
19
20
|
// Integrity is verified on store and re-verified inside
|
|
20
21
|
// `extractTarball` before unpacking. `get` returns bytes without
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
//
|
|
25
26
|
// `extractTarball` is the second face of the same on-disk store: it
|
|
26
27
|
// unpacks the tarball into a sibling `extracted/` directory keyed by
|
|
27
|
-
// the same integrity, so the per-instance loader can
|
|
28
|
+
// the same integrity, so the per-instance loader can copy from a
|
|
28
29
|
// stable, deduplicated extraction without re-doing the tar work on
|
|
29
30
|
// every apply. The unpack is gated by a per-integrity tmp-and-rename
|
|
30
31
|
// dance with the same crash-safety properties as `put`.
|
|
@@ -82,7 +83,7 @@ export function createTarballCache(config) {
|
|
|
82
83
|
// defers physical removal of the extraction tree until the count
|
|
83
84
|
// reaches zero. This decouples mark-as-bad (atomic, prompt) from
|
|
84
85
|
// physical reclaim (deferred, safe) so an integrity-mismatch evict
|
|
85
|
-
// that races a concurrent `
|
|
86
|
+
// that races a concurrent `copyTree` walk against the same
|
|
86
87
|
// extraction does not pull the tree out from under the walk and
|
|
87
88
|
// surface as ENOENT mid-readdir.
|
|
88
89
|
//
|
|
@@ -238,21 +239,15 @@ export function createTarballCache(config) {
|
|
|
238
239
|
}
|
|
239
240
|
/**
|
|
240
241
|
* Sum the on-disk size of every regular file under `dir` recursively.
|
|
241
|
-
* Returns 0 when `dir` does not exist.
|
|
242
|
-
*
|
|
243
|
-
* dedup walking without a manual ino map; the loader hardlinks
|
|
244
|
-
* extraction trees into per-instance store dirs, so the extraction
|
|
245
|
-
* tree itself holds one link per file and `stat.size` per entry is
|
|
246
|
-
* the right number to charge to this cache entry.
|
|
242
|
+
* Returns 0 when `dir` does not exist. Each regular file's
|
|
243
|
+
* `stat.size` is charged to this cache entry once.
|
|
247
244
|
*
|
|
248
245
|
* ACCOUNTING vs. DISK USAGE: `maxBytes` bounds the sum reported by
|
|
249
246
|
* this walker, not the actual disk consumption of the cache plus
|
|
250
|
-
* its downstream
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* The cap is a steady-state ceiling on the cache tree's own
|
|
255
|
-
* accounting, not a disk-usage limit. Concurrent vanishes during
|
|
247
|
+
* its downstream consumers. The loader's per-instance store dirs
|
|
248
|
+
* contain independent copies that are outside this cache tree, so
|
|
249
|
+
* the cap is a steady-state ceiling on the cache's own accounting,
|
|
250
|
+
* not a sidecar-wide disk-usage limit. Concurrent vanishes during
|
|
256
251
|
* the walk are silently dropped via the inner `lstat` try/catch
|
|
257
252
|
* below; under the single-process contract this is rare, but the
|
|
258
253
|
* returned `total` reports the cap-relevant sum within one sweep's
|
|
@@ -262,7 +257,7 @@ export function createTarballCache(config) {
|
|
|
262
257
|
* `lstat` here returns the link itself rather than its target, and
|
|
263
258
|
* `dirent.isSymbolicLink()` is the entry-walk equivalent. An npm
|
|
264
259
|
* tarball that ships symlinks is preserved verbatim by the loader's
|
|
265
|
-
*
|
|
260
|
+
* copy-tree pass; charging the link's own size (zero in our
|
|
266
261
|
* accounting) avoids both symlink-loop divergence and double-counting
|
|
267
262
|
* the target through whatever path also names it directly.
|
|
268
263
|
*
|
|
@@ -464,7 +459,7 @@ export function createTarballCache(config) {
|
|
|
464
459
|
throw err;
|
|
465
460
|
}
|
|
466
461
|
// The extraction is derived from the tarball bytes and is
|
|
467
|
-
// useless once the tarball is gone. If a
|
|
462
|
+
// useless once the tarball is gone. If a copyTree walk is
|
|
468
463
|
// in-flight for the same integrity, removing the tree now would
|
|
469
464
|
// surface as ENOENT mid-walk; defer the physical reclaim until
|
|
470
465
|
// every outstanding `release` from `extractTarball` has fired.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ToolPackageAssetSourceTree } from "@intx/types/tool-packages";
|
|
2
|
+
/**
|
|
3
|
+
* Read the entry's pinned subtree from `gitDir`, verify it against the frozen
|
|
4
|
+
* `treeOid`, and write it into a fresh scratch directory under
|
|
5
|
+
* `instanceScratchDir`. Returns the same `{ dir, release }` shape the tarball
|
|
6
|
+
* path returns; `release` is a no-op because there is no content-addressed
|
|
7
|
+
* cache handle to hold — the scratch dir lives under the deploy directory the
|
|
8
|
+
* caller reclaims.
|
|
9
|
+
*/
|
|
10
|
+
export declare function materializeGitEntry(args: {
|
|
11
|
+
tree: ToolPackageAssetSourceTree;
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
gitDir: string;
|
|
15
|
+
instanceScratchDir: string;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
dir: string;
|
|
18
|
+
release: () => void;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Materialize a source-format asset closure entry (`kind:"asset"` with
|
|
2
|
+
// `package.format:"source"`) from an indexed pack.
|
|
3
|
+
//
|
|
4
|
+
// A source-format entry's files come from a subtree of a hub `workflow` git
|
|
5
|
+
// asset at a pinned commit, not a tarball. The pack is indexed once at
|
|
6
|
+
// checkout time (the caller supplies the resulting `gitDir`); this reads the
|
|
7
|
+
// pinned subtree straight from those authenticated objects, verifies its git
|
|
8
|
+
// tree oid against the frozen `treeOid`, and writes the subtree into a scratch
|
|
9
|
+
// directory the store layout then copies from. The tarball cache is bypassed:
|
|
10
|
+
// `treeOid` is a git oid, not an SRI.
|
|
11
|
+
import { promises as fs } from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import git from "isomorphic-git";
|
|
14
|
+
import { getLogger } from "@intx/log";
|
|
15
|
+
import { DEFAULT_PACK_MATERIALIZATION_LIMITS, writeTreeToDisk, } from "@intx/storage-isogit/node";
|
|
16
|
+
import { ToolLoaderError, describeError } from "./loader-internal.js";
|
|
17
|
+
const logger = getLogger(["sidecar", "tool-packaging", "git-materialize"]);
|
|
18
|
+
/**
|
|
19
|
+
* Read the entry's pinned subtree from `gitDir`, verify it against the frozen
|
|
20
|
+
* `treeOid`, and write it into a fresh scratch directory under
|
|
21
|
+
* `instanceScratchDir`. Returns the same `{ dir, release }` shape the tarball
|
|
22
|
+
* path returns; `release` is a no-op because there is no content-addressed
|
|
23
|
+
* cache handle to hold — the scratch dir lives under the deploy directory the
|
|
24
|
+
* caller reclaims.
|
|
25
|
+
*/
|
|
26
|
+
export async function materializeGitEntry(args) {
|
|
27
|
+
const { tree: source, name, version, gitDir, instanceScratchDir } = args;
|
|
28
|
+
// `readTree` peels the commit to its tree; `filepath` then navigates to the
|
|
29
|
+
// pinned member. `packageDir === "."` selects the repo root, i.e. the
|
|
30
|
+
// commit's own tree, so it takes no `filepath`.
|
|
31
|
+
let subtreeOid;
|
|
32
|
+
try {
|
|
33
|
+
const { oid } = await git.readTree({
|
|
34
|
+
fs,
|
|
35
|
+
dir: gitDir,
|
|
36
|
+
oid: source.commitSha,
|
|
37
|
+
...(source.packageDir === "." ? {} : { filepath: source.packageDir }),
|
|
38
|
+
});
|
|
39
|
+
subtreeOid = oid;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new ToolLoaderError({
|
|
43
|
+
category: "git.materialization.failed",
|
|
44
|
+
message: `git subtree ${JSON.stringify(source.packageDir)} for ${name}@${version} could not be read at ${source.commitSha}: ${describeError(err)}`,
|
|
45
|
+
package: { name, version },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// The frozen `treeOid` is the content identity. The hub read it from the
|
|
49
|
+
// same git objects, so a mismatch means the delivered pack diverges from
|
|
50
|
+
// what was frozen; fail loud rather than materialize unverified bytes.
|
|
51
|
+
if (subtreeOid !== source.treeOid) {
|
|
52
|
+
throw new ToolLoaderError({
|
|
53
|
+
category: "git.materialization.failed",
|
|
54
|
+
message: `git subtree ${JSON.stringify(source.packageDir)} at ${source.commitSha} for ${name}@${version} hashed to tree ${subtreeOid}, not the frozen ${source.treeOid}`,
|
|
55
|
+
package: { name, version },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const dir = await fs.mkdtemp(path.join(instanceScratchDir, "git-"));
|
|
59
|
+
try {
|
|
60
|
+
await writeTreeToDisk(gitDir, dir, subtreeOid, DEFAULT_PACK_MATERIALIZATION_LIMITS);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
// Best-effort cleanup of the partial scratch dir; log a secondary rm
|
|
64
|
+
// failure so it does not silently mask state. The primary error is still
|
|
65
|
+
// thrown. Mirrors the cleanup logging in `applyAssetPack` and the sidecar's
|
|
66
|
+
// source-asset delivery.
|
|
67
|
+
await fs.rm(dir, { recursive: true, force: true }).catch((rmErr) => {
|
|
68
|
+
logger.warn `git subtree scratch cleanup failed at ${dir}: ${describeError(rmErr)}`;
|
|
69
|
+
});
|
|
70
|
+
throw new ToolLoaderError({
|
|
71
|
+
category: "git.materialization.failed",
|
|
72
|
+
message: `writing git subtree for ${name}@${version} failed: ${describeError(err)}`,
|
|
73
|
+
package: { name, version },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return { dir, release: () => undefined };
|
|
77
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type ClosureResolver, type ClosureResolverConfig, type MaterializedRef, type Packument, type PackumentFetcher, type PackumentVersion, type PeerDependencyViolation, type RegistryConfig, type RegistrySource, type ScopeRoute, AssetRegistrySource, HttpRegistrySource, ManifestInvalidError, createClosureResolver, parsePin, } from "./resolver.js";
|
|
2
2
|
export { type TarballCache, type TarballCacheConfig, TarballIntegrityMismatchError, createTarballCache, } from "./cache.js";
|
|
3
|
-
export { type HostPlatform, type LoadManifestArgs, type LoadedDirectorFactory, type LoadedToolFactory, type LoadedToolPackage, type LoaderConfig, type TarballFetcher, type ToolLoader, ToolLoaderError, createToolLoader, } from "./loader.js";
|
|
3
|
+
export { type HostPlatform, type LoadManifestArgs, type LoadedDirectorFactory, type LoadedToolFactory, type LoadedToolPackage, type LoaderConfig, type MaterializeClosureArgs, type MaterializeClosureResult, type TarballFetcher, type ToolLoader, ToolLoaderError, createToolLoader, materializeClosure, storeEntryDir, } from "./loader.js";
|
|
4
4
|
export { type ApplyAtomicArgs, type ApplyAtomicFailure, type ApplyAtomicResult, type ApplyAtomicSuccess, applyAtomic, } from "./atomic-apply.js";
|
|
5
5
|
export { type ExtractPackageJSONOutcome, extractTarballPackageJSON, } from "./package-json-extract.js";
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
// `ApplyAtomicFailure` the caller surfaces as a rejected apply.
|
|
26
26
|
export { AssetRegistrySource, HttpRegistrySource, ManifestInvalidError, createClosureResolver, parsePin, } from "./resolver.js";
|
|
27
27
|
export { TarballIntegrityMismatchError, createTarballCache, } from "./cache.js";
|
|
28
|
-
export { ToolLoaderError, createToolLoader, } from "./loader.js";
|
|
28
|
+
export { ToolLoaderError, createToolLoader, materializeClosure, storeEntryDir, } from "./loader.js";
|
|
29
29
|
export { applyAtomic, } from "./atomic-apply.js";
|
|
30
30
|
export { extractTarballPackageJSON, } from "./package-json-extract.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DeployApplyErrorCategory } from "@intx/types/sidecar";
|
|
2
|
+
/**
|
|
3
|
+
* Maximum bytes a single registry tarball fetch will read before aborting. The
|
|
4
|
+
* cap bounds the memory a malicious or misconfigured registry can force a
|
|
5
|
+
* deploy to buffer while the atomic-apply mirror replays it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_MAX_REGISTRY_TARBALL_BYTES: number;
|
|
8
|
+
/**
|
|
9
|
+
* Default deadline for a single HTTP-registry tarball fetch, covering both the
|
|
10
|
+
* request and the streamed body read. `readResponseWithLimit` consumes the body
|
|
11
|
+
* through a manual reader loop, so the byte cap bounds size but nothing bounds
|
|
12
|
+
* time: a registry that accepts the connection and then stalls mid-stream would
|
|
13
|
+
* block the fetch -- and the deploy's tool materialization awaiting it --
|
|
14
|
+
* indefinitely. The deadline is generous so a legitimately large tarball on a
|
|
15
|
+
* slow link still completes within it. Callers that need a different bound pass
|
|
16
|
+
* `registryFetchTimeoutMs` to `createToolLoader`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_REGISTRY_FETCH_TIMEOUT_MS: number;
|
|
19
|
+
/**
|
|
20
|
+
* A tool-loader failure carrying the atomic-apply error category (and, when the
|
|
21
|
+
* failure is attributable to a specific package, its name and version) so the
|
|
22
|
+
* apply layer can map every loader failure onto a `DeployApplyErrorCategory`.
|
|
23
|
+
*/
|
|
24
|
+
export declare class ToolLoaderError extends Error {
|
|
25
|
+
readonly category: DeployApplyErrorCategory;
|
|
26
|
+
readonly package: {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly version: string;
|
|
29
|
+
} | undefined;
|
|
30
|
+
constructor(opts: {
|
|
31
|
+
category: DeployApplyErrorCategory;
|
|
32
|
+
message: string;
|
|
33
|
+
package?: {
|
|
34
|
+
name: string;
|
|
35
|
+
version: string;
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether a `platform`/`os`/`cpu`-style allowlist matches `host`. A list with a
|
|
41
|
+
* `!`-negated entry matches everything except the negated hosts; an unnegated
|
|
42
|
+
* list matches only its members.
|
|
43
|
+
*/
|
|
44
|
+
export declare function platformListMatches(entries: readonly string[], host: string): boolean;
|
|
45
|
+
export declare function isEEXIST(err: unknown): boolean;
|
|
46
|
+
export declare function isENOENT(err: unknown): boolean;
|
|
47
|
+
export declare function describeError(err: unknown): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Shared internals of the tool-package loader: the failure type, the fetch
|
|
2
|
+
// byte/time caps, and the small cross-cutting helpers. Split out so the
|
|
3
|
+
// registry-fetch and store-layout modules and `loader.ts` itself can all depend
|
|
4
|
+
// on them without an import cycle. `loader.ts` re-exports the public members
|
|
5
|
+
// (`ToolLoaderError`, the `DEFAULT_*` caps) so existing consumers are unaffected.
|
|
6
|
+
/**
|
|
7
|
+
* Maximum bytes a single registry tarball fetch will read before aborting. The
|
|
8
|
+
* cap bounds the memory a malicious or misconfigured registry can force a
|
|
9
|
+
* deploy to buffer while the atomic-apply mirror replays it.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_MAX_REGISTRY_TARBALL_BYTES = 10 * 1024 * 1024;
|
|
12
|
+
/**
|
|
13
|
+
* Default deadline for a single HTTP-registry tarball fetch, covering both the
|
|
14
|
+
* request and the streamed body read. `readResponseWithLimit` consumes the body
|
|
15
|
+
* through a manual reader loop, so the byte cap bounds size but nothing bounds
|
|
16
|
+
* time: a registry that accepts the connection and then stalls mid-stream would
|
|
17
|
+
* block the fetch -- and the deploy's tool materialization awaiting it --
|
|
18
|
+
* indefinitely. The deadline is generous so a legitimately large tarball on a
|
|
19
|
+
* slow link still completes within it. Callers that need a different bound pass
|
|
20
|
+
* `registryFetchTimeoutMs` to `createToolLoader`.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_REGISTRY_FETCH_TIMEOUT_MS = 120 * 1000;
|
|
23
|
+
/**
|
|
24
|
+
* A tool-loader failure carrying the atomic-apply error category (and, when the
|
|
25
|
+
* failure is attributable to a specific package, its name and version) so the
|
|
26
|
+
* apply layer can map every loader failure onto a `DeployApplyErrorCategory`.
|
|
27
|
+
*/
|
|
28
|
+
export class ToolLoaderError extends Error {
|
|
29
|
+
category;
|
|
30
|
+
package;
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
super(opts.message);
|
|
33
|
+
this.name = "ToolLoaderError";
|
|
34
|
+
this.category = opts.category;
|
|
35
|
+
this.package = opts.package;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether a `platform`/`os`/`cpu`-style allowlist matches `host`. A list with a
|
|
40
|
+
* `!`-negated entry matches everything except the negated hosts; an unnegated
|
|
41
|
+
* list matches only its members.
|
|
42
|
+
*/
|
|
43
|
+
export function platformListMatches(entries, host) {
|
|
44
|
+
const hasNegation = entries.some((e) => e.startsWith("!"));
|
|
45
|
+
if (hasNegation) {
|
|
46
|
+
return !entries.includes(`!${host}`);
|
|
47
|
+
}
|
|
48
|
+
return entries.includes(host);
|
|
49
|
+
}
|
|
50
|
+
export function isEEXIST(err) {
|
|
51
|
+
if (err === null || typeof err !== "object")
|
|
52
|
+
return false;
|
|
53
|
+
if (!("code" in err))
|
|
54
|
+
return false;
|
|
55
|
+
return err.code === "EEXIST";
|
|
56
|
+
}
|
|
57
|
+
export function isENOENT(err) {
|
|
58
|
+
if (err === null || typeof err !== "object")
|
|
59
|
+
return false;
|
|
60
|
+
if (!("code" in err))
|
|
61
|
+
return false;
|
|
62
|
+
return err.code === "ENOENT";
|
|
63
|
+
}
|
|
64
|
+
export function describeError(err) {
|
|
65
|
+
return err instanceof Error ? err.message : String(err);
|
|
66
|
+
}
|
package/dist/loader.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { AnnotatedDirectorFactory, AnnotatedPluginFactory, AnnotatedToolFactory, BaseEnv } from "@intx/agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ToolCredentialDeclaration } from "@intx/types/package-json";
|
|
3
3
|
import type { ToolPackageManifest, ToolPackageManifestEntry } from "@intx/types/tool-packages";
|
|
4
4
|
import type { TarballCache } from "./cache.js";
|
|
5
|
+
import { DEFAULT_MAX_REGISTRY_TARBALL_BYTES, DEFAULT_REGISTRY_FETCH_TIMEOUT_MS, ToolLoaderError } from "./loader-internal.js";
|
|
6
|
+
import { buildRegistryFetchOpts, readResponseWithLimit } from "./registry-fetch.js";
|
|
5
7
|
import type { RegistryConfig } from "./resolver.js";
|
|
8
|
+
import { materializeClosure, storeEntryDir } from "./store-layout.js";
|
|
9
|
+
export { ToolLoaderError, DEFAULT_MAX_REGISTRY_TARBALL_BYTES, DEFAULT_REGISTRY_FETCH_TIMEOUT_MS, buildRegistryFetchOpts, readResponseWithLimit, materializeClosure, storeEntryDir, };
|
|
6
10
|
/**
|
|
7
11
|
* Loaded factory shape. We re-export `AnnotatedToolFactory<BaseEnv>` so
|
|
8
12
|
* callers receive the canonical agent type without re-deriving it; the
|
|
@@ -40,6 +44,17 @@ export interface LoadedToolPackage {
|
|
|
40
44
|
* valid.
|
|
41
45
|
*/
|
|
42
46
|
readonly directors: readonly LoadedDirectorFactory[];
|
|
47
|
+
/**
|
|
48
|
+
* The provider-backed credentials the package's tools statically
|
|
49
|
+
* declare via `interchange.credentials`. A declaration is advisory: it
|
|
50
|
+
* names a handle the agent definition binds to a concrete credential and
|
|
51
|
+
* the launch-time grant gate authorizes; it consents to nothing on its
|
|
52
|
+
* own. Surfaced here -- parsed from the SAME `package.json` the code
|
|
53
|
+
* loaded from -- so the declared set is the authoritative one (the loaded
|
|
54
|
+
* package's own) rather than a hub manifest that could drift. Empty when
|
|
55
|
+
* the package omits the field; a tools-only package stays valid.
|
|
56
|
+
*/
|
|
57
|
+
readonly credentials: readonly ToolCredentialDeclaration[];
|
|
43
58
|
}
|
|
44
59
|
export interface HostPlatform {
|
|
45
60
|
readonly os: string;
|
|
@@ -96,25 +111,6 @@ export interface LoaderConfig {
|
|
|
96
111
|
*/
|
|
97
112
|
readonly importModule?: (importUrl: string) => Promise<unknown>;
|
|
98
113
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Default cap on a single HTTP-registry tarball fetch. Matches the
|
|
101
|
-
* hub's `DEFAULT_HUB_MAX_TARBALL_BYTES` so a tarball the hub accepted
|
|
102
|
-
* on upload is one the sidecar can also fetch back when a registry
|
|
103
|
-
* mirror replays it.
|
|
104
|
-
*/
|
|
105
|
-
export declare const DEFAULT_MAX_REGISTRY_TARBALL_BYTES: number;
|
|
106
|
-
/**
|
|
107
|
-
* Default deadline for a single HTTP-registry tarball fetch, covering
|
|
108
|
-
* both the request and the streamed body read. `readResponseWithLimit`
|
|
109
|
-
* consumes the body through a manual reader loop, so the byte cap bounds
|
|
110
|
-
* size but nothing bounds time: a registry that accepts the connection
|
|
111
|
-
* and then stalls mid-stream would block the fetch -- and the deploy's
|
|
112
|
-
* tool materialization awaiting it -- indefinitely.
|
|
113
|
-
* The deadline is generous so a legitimately large tarball on a slow
|
|
114
|
-
* link still completes within it. Callers that need a different bound
|
|
115
|
-
* pass `registryFetchTimeoutMs` to `createToolLoader`.
|
|
116
|
-
*/
|
|
117
|
-
export declare const DEFAULT_REGISTRY_FETCH_TIMEOUT_MS: number;
|
|
118
114
|
export type TarballFetcher = (entry: ToolPackageManifestEntry, ctx: {
|
|
119
115
|
registries: ReadonlyMap<string, RegistryConfig>;
|
|
120
116
|
assetRoot: string;
|
|
@@ -138,52 +134,65 @@ export interface LoadManifestArgs {
|
|
|
138
134
|
* sources from an asset.
|
|
139
135
|
*/
|
|
140
136
|
readonly assetMounts: ReadonlyMap<string, string>;
|
|
137
|
+
/**
|
|
138
|
+
* Maps a source-format asset entry's `source.assetId` to an absolute path
|
|
139
|
+
* to an indexed git directory whose object database holds the pinned
|
|
140
|
+
* commit and its trees. The caller checks the delivered pack out into
|
|
141
|
+
* this directory once per asset before applying. Empty map is valid
|
|
142
|
+
* when no entry sources from a git subtree.
|
|
143
|
+
*/
|
|
144
|
+
readonly gitDirs: ReadonlyMap<string, string>;
|
|
141
145
|
}
|
|
142
146
|
export interface ToolLoader {
|
|
143
147
|
loadManifest(args: LoadManifestArgs): Promise<LoadedToolPackage[]>;
|
|
144
148
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Arguments for `materializeClosure`, the eval-free materialization
|
|
151
|
+
* primitive `loadManifest` wraps. Everything phases 1-2 need is passed
|
|
152
|
+
* explicitly so the function stands alone without a `ToolLoader`
|
|
153
|
+
* instance: the closure's `cache`, `registries`, `host`, and resolved
|
|
154
|
+
* `fetchTarball` are threaded in directly. There is no import seam here
|
|
155
|
+
* because `materializeClosure` never imports author code — that first
|
|
156
|
+
* `import()` belongs to `loadManifest`'s phase-3 loop.
|
|
157
|
+
*/
|
|
158
|
+
export interface MaterializeClosureArgs {
|
|
159
|
+
readonly manifest: ToolPackageManifest;
|
|
160
|
+
readonly instanceScratchDir: string;
|
|
161
|
+
readonly assetRoot: string;
|
|
162
|
+
readonly assetMounts: ReadonlyMap<string, string>;
|
|
163
|
+
/**
|
|
164
|
+
* Maps a source-format asset entry's `source.assetId` to an absolute
|
|
165
|
+
* indexed git directory the pinned subtree is read from. Empty map is valid
|
|
166
|
+
* when no entry sources from a git subtree.
|
|
167
|
+
*/
|
|
168
|
+
readonly gitDirs: ReadonlyMap<string, string>;
|
|
169
|
+
readonly host: HostPlatform;
|
|
170
|
+
readonly cache: TarballCache;
|
|
171
|
+
/**
|
|
172
|
+
* Registry identifier → registry config, the same map
|
|
173
|
+
* `createToolLoader` keys its registries under. Used both to gate
|
|
174
|
+
* `kind: "registry"` entries against the sidecar config and as the
|
|
175
|
+
* `registries` context handed to `fetchTarball`.
|
|
176
|
+
*/
|
|
177
|
+
readonly registries: ReadonlyMap<string, RegistryConfig>;
|
|
178
|
+
/**
|
|
179
|
+
* Resolved tarball fetcher. `createToolLoader` builds the default
|
|
180
|
+
* (npm-registry-fetch + filesystem) fetcher or honors a test seam and
|
|
181
|
+
* threads the result here; `materializeClosure` does not construct one
|
|
182
|
+
* of its own.
|
|
183
|
+
*/
|
|
184
|
+
readonly fetchTarball: TarballFetcher;
|
|
159
185
|
}
|
|
160
|
-
export declare function createToolLoader(config: LoaderConfig): ToolLoader;
|
|
161
|
-
export declare function buildRegistryFetchOpts(registry: RegistryConfig): Record<string, unknown>;
|
|
162
186
|
/**
|
|
163
|
-
*
|
|
164
|
-
* `
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* also rejected so a header like `1e9` cannot read as 1e9 against
|
|
170
|
-
* `Number()` while a digit-only cap check would pass.
|
|
171
|
-
* 2. Stream the body chunk-by-chunk, tallying byte length, and abort
|
|
172
|
-
* the read when the running total crosses the cap. This catches
|
|
173
|
-
* the missing-or-lying header case.
|
|
174
|
-
*
|
|
175
|
-
* An optional `signal` adds a time guard: when it aborts (the caller's
|
|
176
|
-
* fetch deadline), the in-flight read is cancelled and the call rejects,
|
|
177
|
-
* so a registry that streams the body slowly or stalls mid-stream cannot
|
|
178
|
-
* outlast the deadline while staying under the byte cap.
|
|
179
|
-
*
|
|
180
|
-
* All rejections surface as `registry.fetch.failed` so the apply layer
|
|
181
|
-
* routes them the same as any other registry-side fetch defect.
|
|
182
|
-
*
|
|
183
|
-
* Exported for direct unit testing.
|
|
187
|
+
* Result of `materializeClosure`: the per-instance store directory the
|
|
188
|
+
* closure was laid out under (`<instanceScratchDir>/store`), plus the exact
|
|
189
|
+
* host-filtered entries that were laid out under it. A caller computes a
|
|
190
|
+
* specific package's directory with `storeEntryDir`, and iterates `entries` to
|
|
191
|
+
* load exactly what was materialized -- rather than re-deriving the host filter
|
|
192
|
+
* with a second predicate and risking the two drifting apart.
|
|
184
193
|
*/
|
|
185
|
-
export
|
|
186
|
-
readonly
|
|
187
|
-
readonly
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
export interface MaterializeClosureResult {
|
|
195
|
+
readonly storeDir: string;
|
|
196
|
+
readonly entries: readonly ToolPackageManifestEntry[];
|
|
197
|
+
}
|
|
198
|
+
export declare function createToolLoader(config: LoaderConfig): ToolLoader;
|