@kici-dev/agent 0.1.14 → 0.1.16
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 +13 -1
- package/dist/diagnostics/mini-bundle.d.ts +9 -0
- package/dist/execution/cache/cache-engine.d.ts +62 -0
- package/dist/execution/cache/cache-phase.d.ts +29 -0
- package/dist/execution/cache/index.d.ts +9 -0
- package/dist/execution/dynamic-job-serializer.d.ts +12 -1
- package/dist/execution/env-init/init-phase.d.ts +64 -0
- package/dist/execution/init-runner.d.ts +1 -1
- package/dist/execution/job-runner.d.ts +30 -0
- package/dist/execution/sandbox/env-delta.d.ts +44 -0
- package/dist/execution/sandbox/env-file.d.ts +39 -0
- package/dist/execution/sandbox/index.d.ts +1 -1
- package/dist/execution/sandbox/ipc-protocol.d.ts +111 -3
- package/dist/execution/sandbox/job-deadline.d.ts +13 -0
- package/dist/execution/sandbox/step-loop.d.ts +53 -1
- package/dist/execution/sandbox/types.d.ts +20 -1
- package/dist/execution/sandbox/workflow-runner.d.ts +9 -1
- package/dist/execution/tmp-gc.d.ts +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +342 -12
- package/dist/server.js +623 -168
- package/dist/workflow-runner.js +2001 -950
- package/dist/ws/orchestrator-client.d.ts +55 -3
- package/package.json +16 -10
- package/sbom.spdx.json +1923 -148
|
@@ -13,5 +13,13 @@
|
|
|
13
13
|
* This file is compiled alongside the agent by rolldown (existing build), but
|
|
14
14
|
* runs as a SEPARATE process spawned by the sandbox backend.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
import type { Job, GenericInitConfig } from '@kici-dev/sdk';
|
|
17
|
+
/** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
|
|
18
|
+
export declare function rawPayloadFromEvent(event: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Normalize `Job.init` (config | config[] | false | undefined) to an ordered
|
|
21
|
+
* array of init specs. `false` is an explicit opt-out and `undefined` (no
|
|
22
|
+
* config) both resolve to an empty list — the init phase is then a no-op.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveInitSpecs(job: Job | undefined): GenericInitConfig[];
|
|
17
25
|
//# sourceMappingURL=workflow-runner.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup garbage collection for this agent's own temp-directory families.
|
|
3
|
+
*
|
|
4
|
+
* Job workdirs (`kici-<6 random chars>`, see job-runner.ts) and isolated
|
|
5
|
+
* pnpm stores (`kici-pnpm-store-*`, see dep-installer.ts) clean themselves
|
|
6
|
+
* up in `finally` blocks — but a hard process death (SIGKILL, OOM kill)
|
|
7
|
+
* skips those, and on a long-lived bare-metal agent the leftovers then
|
|
8
|
+
* accumulate forever. Collecting anything older than a day at startup is
|
|
9
|
+
* safe on shared hosts: no job lives remotely that long (job timeouts are
|
|
10
|
+
* minutes), so a concurrent agent's in-flight dirs are never eligible.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Collect this agent's stale temp dirs. `base` is overridable for tests;
|
|
14
|
+
* production callers use the default temp root. Never throws.
|
|
15
|
+
*/
|
|
16
|
+
export declare function gcStaleAgentTmpDirs(base?: string): Promise<string[]>;
|
|
17
|
+
//# sourceMappingURL=tmp-gc.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { loadConfig, type AppConfig } from './config.js';
|
|
2
2
|
export { installDeps, type InstallDepsOptions } from './execution/dep-installer.js';
|
|
3
3
|
export { findLocalProtocolDeps, assertResolvableDeps, formatUnresolvableDepError, kiciHasLocalProtocolDeps, LocalDepProtocol, type LocalProtocolDep, } from './execution/validate-kici-deps.js';
|
|
4
|
+
export { createCacheApi, packCachePaths, extractCacheTarball, downloadAndExtractCache, resolveCachePath, type CacheTransport, type CacheRoots, } from './execution/cache/index.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
import { fileURLToPath as __cjs_fileURLToPath } from "node:url";
|
|
2
|
-
import { dirname as __cjs_dirname } from "node:path";
|
|
3
|
-
__cjs_dirname(__cjs_fileURLToPath(import.meta.url));
|
|
4
1
|
import "node:module";
|
|
5
|
-
import { hostname, tmpdir } from "node:os";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { homedir, hostname, tmpdir } from "node:os";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
4
|
import { z } from "zod";
|
|
8
5
|
import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
|
|
9
6
|
import { KNOWN_ROLES, validateNoReservedLabels } from "@kici-dev/engine";
|
|
10
7
|
import { execFile } from "node:child_process";
|
|
11
|
-
import { access, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
12
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
8
|
+
import { access, cp, mkdir, mkdtemp, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
9
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
13
10
|
import { promisify } from "node:util";
|
|
14
|
-
import { createLogger, toErrorMessage } from "@kici-dev/shared";
|
|
11
|
+
import { createLogger, sha256, toErrorMessage } from "@kici-dev/shared";
|
|
15
12
|
import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
|
|
16
13
|
import { existsSync } from "node:fs";
|
|
14
|
+
import { Readable, Transform } from "node:stream";
|
|
15
|
+
import { pipeline } from "node:stream/promises";
|
|
16
|
+
import { createGunzip } from "node:zlib";
|
|
17
|
+
import { c, x } from "tar";
|
|
18
|
+
import https from "node:https";
|
|
19
|
+
import http from "node:http";
|
|
20
|
+
import "node:url";
|
|
21
|
+
var __defProp = Object.defineProperty;
|
|
22
|
+
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
23
|
+
var __exportAll = (all, no_symbols) => {
|
|
24
|
+
let target = {};
|
|
25
|
+
for (var name in all) __defProp(target, name, {
|
|
26
|
+
get: all[name],
|
|
27
|
+
enumerable: true
|
|
28
|
+
});
|
|
29
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
30
|
+
return target;
|
|
31
|
+
};
|
|
17
32
|
import.meta.url;
|
|
18
33
|
//#endregion
|
|
19
34
|
//#region src/config.ts
|
|
@@ -444,7 +459,7 @@ async function assertResolvableDeps(args) {
|
|
|
444
459
|
* env vars — the install runs with `--ignore-scripts` whenever a private
|
|
445
460
|
* registry is configured.
|
|
446
461
|
*/
|
|
447
|
-
const logger = createLogger({ prefix: "dep-installer" });
|
|
462
|
+
const logger$2 = createLogger({ prefix: "dep-installer" });
|
|
448
463
|
const execFileAsync = promisify(execFile);
|
|
449
464
|
/** Install subprocess timeout (10 min) and stdout/stderr buffer (128 MiB). */
|
|
450
465
|
const INSTALL_TIMEOUT_MS = 6e5;
|
|
@@ -479,7 +494,7 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
|
|
|
479
494
|
async function installDeps(kiciDir, opts = {}) {
|
|
480
495
|
const repoRoot = opts.repoRoot ?? dirname(kiciDir);
|
|
481
496
|
const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
|
|
482
|
-
logger.info("Installing deps inline", {
|
|
497
|
+
logger$2.info("Installing deps inline", {
|
|
483
498
|
packageManager,
|
|
484
499
|
dir: kiciDir
|
|
485
500
|
});
|
|
@@ -520,7 +535,7 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
520
535
|
if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
|
|
521
536
|
const durationMs = Date.now() - startTime;
|
|
522
537
|
process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
|
|
523
|
-
logger.info("Deps installed inline", {
|
|
538
|
+
logger$2.info("Deps installed inline", {
|
|
524
539
|
packageManager,
|
|
525
540
|
durationMs
|
|
526
541
|
});
|
|
@@ -658,7 +673,322 @@ function logSubprocessStreams(e, tokens) {
|
|
|
658
673
|
if (e && typeof e === "object" && "stdout" in e) process.stderr.write(`[dep-installer:trace] stdout: ${redactNpmOutput(String(e.stdout), tokens).slice(0, 500)}\n`);
|
|
659
674
|
if (e && typeof e === "object" && "stderr" in e) process.stderr.write(`[dep-installer:trace] stderr: ${redactNpmOutput(String(e.stderr), tokens).slice(0, 500)}\n`);
|
|
660
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Rewrite localhost URLs to use the orchestrator host.
|
|
678
|
+
*
|
|
679
|
+
* The orchestrator rewrites file:// cache URLs to http://localhost:PORT/...
|
|
680
|
+
* but agent containers can't reach localhost. This utility replaces the
|
|
681
|
+
* host with the orchestrator's host derived from KICI_ORCHESTRATOR_URL.
|
|
682
|
+
*/
|
|
683
|
+
function resolveOrchestratorUrl(url) {
|
|
684
|
+
if (!url.match(/^https?:\/\/(localhost|127\.0\.0\.1)[:/]/)) return url;
|
|
685
|
+
const orchestratorUrl = process.env.KICI_ORCHESTRATOR_URL;
|
|
686
|
+
if (!orchestratorUrl) return url;
|
|
687
|
+
try {
|
|
688
|
+
const orchestratorParsed = new URL(orchestratorUrl.replace(/^ws/, "http"));
|
|
689
|
+
const parsed = new URL(url);
|
|
690
|
+
parsed.hostname = orchestratorParsed.hostname;
|
|
691
|
+
return parsed.toString();
|
|
692
|
+
} catch {
|
|
693
|
+
return url;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
var SCRATCH_DIR_BASENAME_PREFIX;
|
|
697
|
+
var init_dep_restore = __esmMin((() => {
|
|
698
|
+
createLogger({ prefix: "dep-restore" });
|
|
699
|
+
SCRATCH_DIR_BASENAME_PREFIX = ".dep-restore-scratch-";
|
|
700
|
+
`${SCRATCH_DIR_BASENAME_PREFIX}`;
|
|
701
|
+
}));
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/execution/download.ts
|
|
704
|
+
/**
|
|
705
|
+
* Shared HTTP/HTTPS download utility.
|
|
706
|
+
*
|
|
707
|
+
* Extracted from workflow-loader.ts to avoid duplication across
|
|
708
|
+
* dep-restore.ts and workflow-loader.ts.
|
|
709
|
+
*/
|
|
710
|
+
var download_exports = /* @__PURE__ */ __exportAll({
|
|
711
|
+
downloadUrl: () => downloadUrl,
|
|
712
|
+
uploadToPresignedUrl: () => uploadToPresignedUrl
|
|
713
|
+
});
|
|
714
|
+
/**
|
|
715
|
+
* Download content from an HTTP/HTTPS URL.
|
|
716
|
+
*
|
|
717
|
+
* Includes a 5-minute timeout to prevent the agent from hanging indefinitely
|
|
718
|
+
* on slow or unresponsive endpoints.
|
|
719
|
+
*
|
|
720
|
+
* @param url - The URL to download from
|
|
721
|
+
* @returns The response body as a Buffer
|
|
722
|
+
*/
|
|
723
|
+
function downloadUrl(url) {
|
|
724
|
+
return new Promise((resolve, reject) => {
|
|
725
|
+
(url.startsWith("https:") ? https : http).get(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS$1) }, (res) => {
|
|
726
|
+
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
727
|
+
reject(/* @__PURE__ */ new Error(`HTTP ${res.statusCode} downloading from ${url}`));
|
|
728
|
+
res.resume();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const chunks = [];
|
|
732
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
733
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
734
|
+
res.on("error", reject);
|
|
735
|
+
}).on("error", reject);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Upload a buffer to a pre-signed S3 URL via HTTP PUT.
|
|
740
|
+
*
|
|
741
|
+
* Used for direct-to-S3 uploads of bundles and dep tarballs. Localhost /
|
|
742
|
+
* 127.0.0.1 URLs are rewritten via `resolveOrchestratorUrl` so the
|
|
743
|
+
* filesystem cache backend's signed URLs work from container agents that
|
|
744
|
+
* can't reach the orchestrator's host loopback directly.
|
|
745
|
+
*
|
|
746
|
+
* @param url - The pre-signed URL to upload to
|
|
747
|
+
* @param data - The buffer to upload
|
|
748
|
+
*/
|
|
749
|
+
function uploadToPresignedUrl(url, data) {
|
|
750
|
+
return new Promise((resolve, reject) => {
|
|
751
|
+
const resolved = resolveOrchestratorUrl(url);
|
|
752
|
+
const parsed = new URL(resolved);
|
|
753
|
+
const req = (parsed.protocol === "https:" ? https : http).request({
|
|
754
|
+
hostname: parsed.hostname,
|
|
755
|
+
port: parsed.port,
|
|
756
|
+
path: parsed.pathname + parsed.search,
|
|
757
|
+
method: "PUT",
|
|
758
|
+
headers: { "Content-Length": data.length }
|
|
759
|
+
}, (res) => {
|
|
760
|
+
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
761
|
+
reject(/* @__PURE__ */ new Error(`HTTP ${res.statusCode} uploading to pre-signed URL`));
|
|
762
|
+
res.resume();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
res.resume();
|
|
766
|
+
res.on("end", () => resolve());
|
|
767
|
+
res.on("error", reject);
|
|
768
|
+
});
|
|
769
|
+
req.on("error", reject);
|
|
770
|
+
req.end(data);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
var DOWNLOAD_TIMEOUT_MS$1;
|
|
774
|
+
var init_download = __esmMin((() => {
|
|
775
|
+
init_dep_restore();
|
|
776
|
+
DOWNLOAD_TIMEOUT_MS$1 = 300 * 1e3;
|
|
777
|
+
}));
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region src/execution/cache/cache-engine.ts
|
|
780
|
+
/**
|
|
781
|
+
* User-facing cache engine (sandbox-side).
|
|
782
|
+
*
|
|
783
|
+
* Packs `CacheSpec.paths` into a gzip tarball (mirrors dep-packer's tar+sha256
|
|
784
|
+
* approach) and restores a tarball with on-the-fly SHA-256 verification
|
|
785
|
+
* (mirrors dep-restore's streaming pipeline). Drives the orchestrator over an
|
|
786
|
+
* injected request-response transport (IPC -> agent WS -> orchestrator).
|
|
787
|
+
*
|
|
788
|
+
* Path safety: each path is either `~`-prefixed (home-relative) or
|
|
789
|
+
* repo-root-relative; absolute paths and `..` escapes are rejected so a
|
|
790
|
+
* workflow cannot exfiltrate or clobber files outside its tree/home.
|
|
791
|
+
*
|
|
792
|
+
* Multi-root layout: a spec may mix repo-relative and home-relative paths.
|
|
793
|
+
* Each entry is staged under an anchor prefix — `__repo__/<rel>` for
|
|
794
|
+
* repo-root-relative entries, `__home__/<rel>` for `~`-prefixed entries — so a
|
|
795
|
+
* single tarball can carry both roots and extract restores each group to the
|
|
796
|
+
* right destination (repo entries under `workDir`, home entries under the
|
|
797
|
+
* homedir). Extraction lands in a scratch dir first, then moves each group
|
|
798
|
+
* into place so a partial restore never leaves half-written paths in the live
|
|
799
|
+
* tree (mirrors dep-restore).
|
|
800
|
+
*/
|
|
801
|
+
const logger = createLogger({ prefix: "cache-engine" });
|
|
802
|
+
/** Download timeout for a presigned cache GET: 5 minutes. */
|
|
803
|
+
const DOWNLOAD_TIMEOUT_MS = 300 * 1e3;
|
|
804
|
+
/** Anchor prefix for repo-root-relative cache entries inside the tar. */
|
|
805
|
+
const REPO_ANCHOR = "__repo__";
|
|
806
|
+
/** Anchor prefix for home-relative (`~`) cache entries inside the tar. */
|
|
807
|
+
const HOME_ANCHOR = "__home__";
|
|
808
|
+
/**
|
|
809
|
+
* Resolve a cache path. `~`-prefixed -> home root; otherwise repo-root-relative.
|
|
810
|
+
* Rejects absolute paths and `..` escapes so a workflow cannot read or clobber
|
|
811
|
+
* files outside its tree / home.
|
|
812
|
+
*/
|
|
813
|
+
function resolveCachePath(workDir, p, roots) {
|
|
814
|
+
const home = roots?.home ?? homedir();
|
|
815
|
+
if (p === "~" || p.startsWith("~/")) {
|
|
816
|
+
const rel = p === "~" ? "" : p.slice(2);
|
|
817
|
+
return rel ? join(home, rel) : home;
|
|
818
|
+
}
|
|
819
|
+
if (isAbsolute(p)) throw new Error(`cache path must be repo-relative or ~-prefixed: ${p}`);
|
|
820
|
+
const resolved = resolve(workDir, p);
|
|
821
|
+
const rel = relative(workDir, resolved);
|
|
822
|
+
if (rel === ".." || rel.startsWith(`..${sep}`)) throw new Error(`cache path escapes the repo root: ${p}`);
|
|
823
|
+
return resolved;
|
|
824
|
+
}
|
|
825
|
+
/** Resolve + anchor every spec path; rejects escapes via resolveCachePath. */
|
|
826
|
+
function anchorEntries(workDir, paths, roots) {
|
|
827
|
+
const home = roots?.home ?? homedir();
|
|
828
|
+
return paths.map((p) => {
|
|
829
|
+
const abs = resolveCachePath(workDir, p, roots);
|
|
830
|
+
const isHome = p === "~" || p.startsWith("~/");
|
|
831
|
+
return {
|
|
832
|
+
abs,
|
|
833
|
+
anchor: isHome ? HOME_ANCHOR : REPO_ANCHOR,
|
|
834
|
+
rel: relative(isHome ? home : workDir, abs)
|
|
835
|
+
};
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Pack the spec's paths into a gzip tarball + its SHA-256.
|
|
840
|
+
*
|
|
841
|
+
* Each path is copied into a staging dir under its anchor prefix
|
|
842
|
+
* (`__repo__/<rel>` or `__home__/<rel>`), the staging dir is tarred (portable
|
|
843
|
+
* mode strips uid/gid/mtime), and the staging dir is removed. The resulting
|
|
844
|
+
* tarball self-describes which root each entry restores to.
|
|
845
|
+
*/
|
|
846
|
+
async function packCachePaths(workDir, paths, roots) {
|
|
847
|
+
const entries = anchorEntries(workDir, paths, roots);
|
|
848
|
+
const staging = await mkdtemp(join(tmpdir(), "kici-cache-pack-"));
|
|
849
|
+
try {
|
|
850
|
+
const topLevel = /* @__PURE__ */ new Set();
|
|
851
|
+
for (const e of entries) {
|
|
852
|
+
const dest = join(staging, e.anchor, e.rel);
|
|
853
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
854
|
+
await cp(e.abs, dest, {
|
|
855
|
+
recursive: true,
|
|
856
|
+
verbatimSymlinks: true
|
|
857
|
+
});
|
|
858
|
+
topLevel.add(e.anchor);
|
|
859
|
+
}
|
|
860
|
+
const stream = c({
|
|
861
|
+
gzip: true,
|
|
862
|
+
portable: true,
|
|
863
|
+
cwd: staging
|
|
864
|
+
}, [...topLevel]);
|
|
865
|
+
const chunks = [];
|
|
866
|
+
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
867
|
+
const tarball = Buffer.concat(chunks);
|
|
868
|
+
const hash = sha256(tarball);
|
|
869
|
+
logger.info("packed user cache", {
|
|
870
|
+
sizeBytes: tarball.length,
|
|
871
|
+
hash: hash.slice(0, 12),
|
|
872
|
+
paths
|
|
873
|
+
});
|
|
874
|
+
return {
|
|
875
|
+
tarball,
|
|
876
|
+
hash
|
|
877
|
+
};
|
|
878
|
+
} finally {
|
|
879
|
+
await rm(staging, {
|
|
880
|
+
recursive: true,
|
|
881
|
+
force: true
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/** Move the extracted `__repo__` / `__home__` groups from a scratch dir into place. */
|
|
886
|
+
async function moveAnchoredGroups(scratchDir, workDir, home) {
|
|
887
|
+
for (const anchor of await readdir(scratchDir)) {
|
|
888
|
+
const anchorDir = join(scratchDir, anchor);
|
|
889
|
+
const destRoot = anchor === HOME_ANCHOR ? home : anchor === REPO_ANCHOR ? workDir : null;
|
|
890
|
+
if (!destRoot) continue;
|
|
891
|
+
for (const child of await readdir(anchorDir)) {
|
|
892
|
+
const dest = join(destRoot, child);
|
|
893
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
894
|
+
await rm(dest, {
|
|
895
|
+
recursive: true,
|
|
896
|
+
force: true
|
|
897
|
+
});
|
|
898
|
+
await rename(join(anchorDir, child), dest);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Extract a cache tarball buffer, verifying its SHA-256 first, then move each
|
|
904
|
+
* anchored group into place (repo entries under `workDir`, home entries under
|
|
905
|
+
* the home root). Extracts into a scratch dir so a partial restore never
|
|
906
|
+
* leaves half-written paths in the live tree.
|
|
907
|
+
*/
|
|
908
|
+
async function extractCacheTarball(tarball, workDir, expectedHash, roots) {
|
|
909
|
+
const actual = sha256(tarball);
|
|
910
|
+
if (actual !== expectedHash) throw new Error(`Cache tarball checksum mismatch: expected ${expectedHash}, got ${actual}`);
|
|
911
|
+
const home = roots?.home ?? homedir();
|
|
912
|
+
await mkdir(workDir, { recursive: true });
|
|
913
|
+
const scratch = await mkdtemp(join(tmpdir(), "kici-cache-extract-"));
|
|
914
|
+
try {
|
|
915
|
+
await new Promise((res, rej) => {
|
|
916
|
+
Readable.from(tarball).pipe(x({
|
|
917
|
+
cwd: scratch,
|
|
918
|
+
gzip: true
|
|
919
|
+
})).on("finish", res).on("error", rej);
|
|
920
|
+
});
|
|
921
|
+
await moveAnchoredGroups(scratch, workDir, home);
|
|
922
|
+
} finally {
|
|
923
|
+
await rm(scratch, {
|
|
924
|
+
recursive: true,
|
|
925
|
+
force: true
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Stream-download a presigned URL, verify its SHA-256 on the fly (mirrors
|
|
931
|
+
* dep-restore's response -> hash -> gunzip -> tar pipeline), then move the
|
|
932
|
+
* anchored groups into place. Extracts into a scratch dir so a failed download
|
|
933
|
+
* never half-writes the live tree.
|
|
934
|
+
*/
|
|
935
|
+
async function downloadAndExtractCache(url, workDir, expectedHash, roots) {
|
|
936
|
+
const home = roots?.home ?? homedir();
|
|
937
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
938
|
+
if (!response.ok || !response.body) throw new Error(`cache download HTTP ${response.status}`);
|
|
939
|
+
const hash = createHash("sha256");
|
|
940
|
+
const hashTransform = new Transform({ transform(chunk, _enc, cb) {
|
|
941
|
+
hash.update(chunk);
|
|
942
|
+
cb(null, chunk);
|
|
943
|
+
} });
|
|
944
|
+
await mkdir(workDir, { recursive: true });
|
|
945
|
+
const scratch = await mkdtemp(join(tmpdir(), "kici-cache-extract-"));
|
|
946
|
+
try {
|
|
947
|
+
await pipeline(Readable.fromWeb(response.body), hashTransform, createGunzip(), x({ cwd: scratch }));
|
|
948
|
+
const digest = hash.digest("hex");
|
|
949
|
+
if (digest !== expectedHash) throw new Error(`Cache tarball checksum mismatch on download: expected ${expectedHash}, got ${digest}`);
|
|
950
|
+
await moveAnchoredGroups(scratch, workDir, home);
|
|
951
|
+
} finally {
|
|
952
|
+
await rm(scratch, {
|
|
953
|
+
recursive: true,
|
|
954
|
+
force: true
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/** Build the imperative `ctx.cache` API bound to a workDir + transport. */
|
|
959
|
+
function createCacheApi(workDir, transport, roots) {
|
|
960
|
+
return {
|
|
961
|
+
async restore(spec) {
|
|
962
|
+
const r = await transport.restore(spec.key, spec.restoreKeys);
|
|
963
|
+
if (!r.hit || !r.downloadUrl || !r.tarHash) return { hit: false };
|
|
964
|
+
await downloadAndExtractCache(r.downloadUrl, workDir, r.tarHash, roots);
|
|
965
|
+
logger.info("user cache restored", {
|
|
966
|
+
key: spec.key,
|
|
967
|
+
matchedKey: r.matchedKey
|
|
968
|
+
});
|
|
969
|
+
return {
|
|
970
|
+
hit: true,
|
|
971
|
+
matchedKey: r.matchedKey
|
|
972
|
+
};
|
|
973
|
+
},
|
|
974
|
+
async save(spec) {
|
|
975
|
+
const begin = await transport.beginSave(spec.key);
|
|
976
|
+
if (begin.skip || !begin.uploadUrl) {
|
|
977
|
+
logger.info("user cache save skipped (key exists)", { key: spec.key });
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const { tarball, hash } = await packCachePaths(workDir, spec.paths, roots);
|
|
981
|
+
const { uploadToPresignedUrl } = await Promise.resolve().then(() => (init_download(), download_exports));
|
|
982
|
+
await uploadToPresignedUrl(begin.uploadUrl, tarball);
|
|
983
|
+
await transport.completeSave(spec.key, hash, tarball.length);
|
|
984
|
+
logger.info("user cache saved", {
|
|
985
|
+
key: spec.key,
|
|
986
|
+
sizeBytes: tarball.length
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
}
|
|
661
991
|
//#endregion
|
|
662
|
-
export { LocalDepProtocol, assertResolvableDeps, findLocalProtocolDeps, formatUnresolvableDepError, installDeps, kiciHasLocalProtocolDeps, loadConfig };
|
|
992
|
+
export { LocalDepProtocol, assertResolvableDeps, createCacheApi, downloadAndExtractCache, extractCacheTarball, findLocalProtocolDeps, formatUnresolvableDepError, installDeps, kiciHasLocalProtocolDeps, loadConfig, packCachePaths, resolveCachePath };
|
|
663
993
|
|
|
664
994
|
//# sourceMappingURL=index.js.map
|