agent-hitch 0.2.12 → 0.2.14
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/benchmark-packages/automationbench/RESOURCES.md +27 -0
- package/benchmark-packages/automationbench/resources.mjs +86 -0
- package/benchmark-packages/shared-tree/README.md +14 -0
- package/benchmark-packages/shared-tree/import.mjs +39 -0
- package/dist/scripts/canary-automation-resources.js +86 -0
- package/dist/scripts/canary-automation-resources.js.map +1 -0
- package/dist/scripts/canary-resource-empty-daemon.js +195 -0
- package/dist/scripts/canary-resource-empty-daemon.js.map +1 -0
- package/dist/scripts/canary-resources.js +89 -0
- package/dist/scripts/canary-resources.js.map +1 -0
- package/dist/scripts/check-architecture.js +6 -5
- package/dist/scripts/check-architecture.js.map +1 -1
- package/dist/scripts/resource-space-sampler.js +83 -0
- package/dist/scripts/resource-space-sampler.js.map +1 -0
- package/dist/src/adapters/providers/shared.js +0 -1
- package/dist/src/adapters/providers/shared.js.map +1 -1
- package/dist/src/backends/harbor/backend.js +5 -1
- package/dist/src/backends/harbor/backend.js.map +1 -1
- package/dist/src/backends/harbor/resource-admission.js +23 -0
- package/dist/src/backends/harbor/resource-admission.js.map +1 -0
- package/dist/src/cli/commands/resources.js +23 -0
- package/dist/src/cli/commands/resources.js.map +1 -0
- package/dist/src/cli/main.js +2 -0
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/control-plane/remote-offer-contract.js +11 -2
- package/dist/src/control-plane/remote-offer-contract.js.map +1 -1
- package/dist/src/control-plane/remote-result-transport.js +10 -0
- package/dist/src/control-plane/remote-result-transport.js.map +1 -1
- package/dist/src/control-plane/remote-work-coordinator.js +4 -1
- package/dist/src/control-plane/remote-work-coordinator.js.map +1 -1
- package/dist/src/control-plane/remote-work-inputs.js +16 -4
- package/dist/src/control-plane/remote-work-inputs.js.map +1 -1
- package/dist/src/control-plane/remote-worker-client.js +22 -0
- package/dist/src/control-plane/remote-worker-client.js.map +1 -1
- package/dist/src/control-plane/remote-worker-codecs.js +1 -1
- package/dist/src/control-plane/remote-worker-codecs.js.map +1 -1
- package/dist/src/control-plane/remote-worker-protocol.js +12 -2
- package/dist/src/control-plane/remote-worker-protocol.js.map +1 -1
- package/dist/src/control-plane/remote-worker-runner.js +1 -1
- package/dist/src/control-plane/remote-worker-runner.js.map +1 -1
- package/dist/src/control-plane/remote-workers.js +2 -1
- package/dist/src/control-plane/remote-workers.js.map +1 -1
- package/dist/src/domain/validation.js +41 -6
- package/dist/src/domain/validation.js.map +1 -1
- package/dist/src/evals/benchmark-adapter-manifest.js +4 -0
- package/dist/src/evals/benchmark-adapter-manifest.js.map +1 -1
- package/dist/src/evals/benchmark-candidate.js +11 -3
- package/dist/src/evals/benchmark-candidate.js.map +1 -1
- package/dist/src/evals/eval-result-builder.js +22 -1
- package/dist/src/evals/eval-result-builder.js.map +1 -1
- package/dist/src/evals/index.js +1 -0
- package/dist/src/evals/index.js.map +1 -1
- package/dist/src/evals/planned-execution.js +5 -14
- package/dist/src/evals/planned-execution.js.map +1 -1
- package/dist/src/evals/request.js +9 -0
- package/dist/src/evals/request.js.map +1 -1
- package/dist/src/evals/resource-eval-lifecycle.js +31 -0
- package/dist/src/evals/resource-eval-lifecycle.js.map +1 -0
- package/dist/src/evals/resource-legacy-export.js +57 -0
- package/dist/src/evals/resource-legacy-export.js.map +1 -0
- package/dist/src/evals/service.js +5 -20
- package/dist/src/evals/service.js.map +1 -1
- package/dist/src/evals/task-resources.js +12 -4
- package/dist/src/evals/task-resources.js.map +1 -1
- package/dist/src/evals/trial-bundle-files.js +81 -0
- package/dist/src/evals/trial-bundle-files.js.map +1 -0
- package/dist/src/evals/trial-import.js +7 -78
- package/dist/src/evals/trial-import.js.map +1 -1
- package/dist/src/foundation/durable-json.js +33 -0
- package/dist/src/foundation/durable-json.js.map +1 -0
- package/dist/src/foundation/index.js +2 -0
- package/dist/src/foundation/index.js.map +1 -1
- package/dist/src/foundation/resource-image-references.js +52 -0
- package/dist/src/foundation/resource-image-references.js.map +1 -0
- package/dist/src/images/gc.js +2 -2
- package/dist/src/images/gc.js.map +1 -1
- package/dist/src/images/registry.js +27 -2
- package/dist/src/images/registry.js.map +1 -1
- package/dist/src/resources/api.js +207 -0
- package/dist/src/resources/api.js.map +1 -0
- package/dist/src/resources/bundle.js +117 -0
- package/dist/src/resources/bundle.js.map +1 -0
- package/dist/src/resources/evidence.js +93 -0
- package/dist/src/resources/evidence.js.map +1 -0
- package/dist/src/resources/gc.js +123 -0
- package/dist/src/resources/gc.js.map +1 -0
- package/dist/src/resources/index.js +14 -0
- package/dist/src/resources/index.js.map +1 -0
- package/dist/src/resources/materialize.js +143 -0
- package/dist/src/resources/materialize.js.map +1 -0
- package/dist/src/resources/object-store.js +191 -0
- package/dist/src/resources/object-store.js.map +1 -0
- package/dist/src/resources/oci-layer-validation.js +202 -0
- package/dist/src/resources/oci-layer-validation.js.map +1 -0
- package/dist/src/resources/oci-provider.js +101 -0
- package/dist/src/resources/oci-provider.js.map +1 -0
- package/dist/src/resources/oci-registry-bundle.js +253 -0
- package/dist/src/resources/oci-registry-bundle.js.map +1 -0
- package/dist/src/resources/protocol.js +222 -0
- package/dist/src/resources/protocol.js.map +1 -0
- package/dist/src/resources/result-retention.js +93 -0
- package/dist/src/resources/result-retention.js.map +1 -0
- package/dist/src/resources/role-context.js +54 -0
- package/dist/src/resources/role-context.js.map +1 -0
- package/dist/src/resources/stats.js +36 -0
- package/dist/src/resources/stats.js.map +1 -0
- package/dist/src/resources/store.js +310 -0
- package/dist/src/resources/store.js.map +1 -0
- package/dist/src/resources/tasks.js +153 -0
- package/dist/src/resources/tasks.js.map +1 -0
- package/dist/src/resources/transfer.js +162 -0
- package/dist/src/resources/transfer.js.map +1 -0
- package/dist/src/resources/views.js +130 -0
- package/dist/src/resources/views.js.map +1 -0
- package/dist/src/trajectories/analysis.js +31 -9
- package/dist/src/trajectories/analysis.js.map +1 -1
- package/dist/src/trajectories/chunk-projection.js +24 -11
- package/dist/src/trajectories/chunk-projection.js.map +1 -1
- package/dist/src/trajectories/contract.js +4 -0
- package/dist/src/trajectories/contract.js.map +1 -1
- package/dist/src/trajectories/dsh-chunk-contract.js +2 -2
- package/dist/src/trajectories/dsh-chunk-contract.js.map +1 -1
- package/dist/src/trajectories/dsh-contract.js +114 -17
- package/dist/src/trajectories/dsh-contract.js.map +1 -1
- package/dist/src/trajectories/embedded-stream.js +95 -0
- package/dist/src/trajectories/embedded-stream.js.map +1 -0
- package/dist/src/trajectories/events-chunk-drill.js +40 -0
- package/dist/src/trajectories/events-chunk-drill.js.map +1 -1
- package/dist/src/trajectories/events-page.js +43 -3
- package/dist/src/trajectories/events-page.js.map +1 -1
- package/dist/src/trajectories/format.js +3 -7
- package/dist/src/trajectories/format.js.map +1 -1
- package/dist/src/trajectories/index.js +1 -1
- package/dist/src/trajectories/index.js.map +1 -1
- package/dist/src/trajectories/providers/deepseek-codec.js +134 -0
- package/dist/src/trajectories/providers/deepseek-codec.js.map +1 -0
- package/dist/src/trajectories/providers/deepseek.js +32 -20
- package/dist/src/trajectories/providers/deepseek.js.map +1 -1
- package/dist/src/trajectories/request-attempt.js +16 -3
- package/dist/src/trajectories/request-attempt.js.map +1 -1
- package/dist/src/trajectories/store.js +28 -13
- package/dist/src/trajectories/store.js.map +1 -1
- package/dist/src/trajectories/stream-reader.js +12 -4
- package/dist/src/trajectories/stream-reader.js.map +1 -1
- package/dist/src/trajectories/surface-fold.js +26 -6
- package/dist/src/trajectories/surface-fold.js.map +1 -1
- package/dist/src/trajectories/unstarted-tool-repair.js +65 -0
- package/dist/src/trajectories/unstarted-tool-repair.js.map +1 -0
- package/dist/src/workers/remote-harbor-work-spec.js +9 -3
- package/dist/src/workers/remote-harbor-work-spec.js.map +1 -1
- package/dist/src/workers/remote-harbor-worker.js +24 -9
- package/dist/src/workers/remote-harbor-worker.js.map +1 -1
- package/docs/resource-storage.zh-CN.md +95 -0
- package/docs/schemas/execution-provider-status.schema.json +20 -0
- package/docs/schemas/hitch-resource-dataset-v2.schema.json +590 -0
- package/docs/schemas/hitch-resource-lock-v1.schema.json +341 -0
- package/docs/schemas/hitch-resource-plan-v1.schema.json +423 -0
- package/docs/schemas/hitch-resource-selection-v1.schema.json +608 -0
- package/docs/schemas/hitch-tree-v1.schema.json +75 -0
- package/docs/schemas/remote-harbor-work-spec.schema.json +47 -3
- package/docs/schemas/remote-work-offer.schema.json +2 -1
- package/docs/schemas/remote-worker-registration.schema.json +20 -0
- package/docs/schemas/trajectory-analysis.schema.json +17 -3
- package/package.json +9 -3
- package/test-contracts/hitch-resources-v1.json +197 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { durableWriteJSON as atomicWriteJSON, runCommand } from "../foundation/index.js";
|
|
4
|
+
import { canonical, identity, parseStrictJson, sha } from "./protocol.js";
|
|
5
|
+
import { finishResourceWorkspace } from "./materialize.js";
|
|
6
|
+
import { parseResourceTask, taskClosureLock } from "./tasks.js";
|
|
7
|
+
import { exportResourceResultProof } from "./result-retention.js";
|
|
8
|
+
export async function readResourceEvidence(file) {
|
|
9
|
+
const info = await lstat(file).catch(e => { if (e.code !== "ENOENT")
|
|
10
|
+
throw e; return undefined; });
|
|
11
|
+
if (!info)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (!info.isFile() || info.size > 16 * 1024 ** 2)
|
|
14
|
+
throw new Error("invalid resource execution evidence");
|
|
15
|
+
const evidence = parseStrictJson(await readFile(file, "utf8"));
|
|
16
|
+
const { digest, ...body } = evidence.plan;
|
|
17
|
+
parseResourceTask(evidence.plan.task);
|
|
18
|
+
if (evidence.protocol !== "hitch-resource-workspace@1" || digest !== identity("hitch-resource-plan@1", body) || !/^[a-f0-9-]{36}$/.test(evidence.id) || typeof evidence.executionEnded !== "boolean")
|
|
19
|
+
throw new Error("resource execution evidence identity mismatch");
|
|
20
|
+
sha(evidence.materialized_task_digest);
|
|
21
|
+
if (!evidence.materializedTree || identity("hitch-materialized-task@1", evidence.materializedTree) !== evidence.materialized_task_digest)
|
|
22
|
+
throw new Error("materialized tree proof missing");
|
|
23
|
+
return evidence;
|
|
24
|
+
}
|
|
25
|
+
/** Run evidence is sealed before the execution lease can be released. */
|
|
26
|
+
export async function sealResourceEvidence(input) {
|
|
27
|
+
const evidence = await readResourceEvidence(input.file);
|
|
28
|
+
if (!evidence)
|
|
29
|
+
return;
|
|
30
|
+
if (evidence.plan.task.task_id !== input.taskId)
|
|
31
|
+
throw new Error("resource execution task mismatch");
|
|
32
|
+
const observed = [];
|
|
33
|
+
const docker = process.env.HITCH_DOCKER_PATH || "docker", cache = new Map();
|
|
34
|
+
const inspect = async (digest) => {
|
|
35
|
+
sha(digest);
|
|
36
|
+
let value = cache.get(digest);
|
|
37
|
+
if (value)
|
|
38
|
+
return value;
|
|
39
|
+
value = JSON.parse((await runCommand(docker, ["image", "inspect", "--format", "{{json .}}", digest], { timeoutMs: 30_000 })).stdout);
|
|
40
|
+
if (value.Id !== digest || !Array.isArray(value.RootFS?.Layers))
|
|
41
|
+
throw new Error("observed OCI config identity mismatch");
|
|
42
|
+
cache.set(digest, value);
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
for (const container of input.execution?.observed.containers ?? []) {
|
|
46
|
+
if (!container.image_config_digest)
|
|
47
|
+
continue;
|
|
48
|
+
const image = await inspect(container.image_config_digest), labels = image.Config?.Labels ?? {};
|
|
49
|
+
if (labels["dev.hitch.resource.plan"] !== evidence.plan.digest)
|
|
50
|
+
continue;
|
|
51
|
+
const role = labels["dev.hitch.resource.consumer"], consumer = parseStrictJson(role);
|
|
52
|
+
const bindings = evidence.plan.task.lock.bindings.filter(b => canonical(b.consumer) === canonical(consumer) && b.use === "environment-image");
|
|
53
|
+
if (!bindings.length)
|
|
54
|
+
throw new Error("observed resource consumer is undeclared");
|
|
55
|
+
const platform = `${image.Os}/${image.Architecture}${image.Variant ? `/${image.Variant}` : ""}`;
|
|
56
|
+
if (platform !== evidence.plan.platform)
|
|
57
|
+
throw new Error("observed resource platform changed");
|
|
58
|
+
const baseConfigs = [];
|
|
59
|
+
for (const binding of bindings) {
|
|
60
|
+
const expected = evidence.images.find(i => i.resource === binding.resource);
|
|
61
|
+
if (!expected)
|
|
62
|
+
throw new Error("OCI base evidence missing");
|
|
63
|
+
const base = await inspect(expected.configDigest);
|
|
64
|
+
if (canonical(image.RootFS.Layers.slice(0, base.RootFS.Layers.length)) !== canonical(base.RootFS.Layers))
|
|
65
|
+
throw new Error("observed image does not descend from declared resource base");
|
|
66
|
+
baseConfigs.push(expected.configDigest);
|
|
67
|
+
}
|
|
68
|
+
observed.push({ consumer, configDigest: container.image_config_digest, platform, baseConfigs });
|
|
69
|
+
}
|
|
70
|
+
if (input.requireObserved)
|
|
71
|
+
for (const binding of evidence.plan.task.lock.bindings.filter(b => b.use === "environment-image"))
|
|
72
|
+
if (!observed.some(i => canonical(i.consumer) === canonical(binding.consumer)))
|
|
73
|
+
throw new Error("successful resource trial lacks observed role/image evidence");
|
|
74
|
+
const lock = taskClosureLock(evidence.plan.task);
|
|
75
|
+
if ("hitch-materialized-task" in lock.resources)
|
|
76
|
+
throw new Error("reserved materialization resource ID");
|
|
77
|
+
const root = await input.store.pin({ owner: input.owner, generation: 1, purpose: "sealed-run", lock: { ...lock, resources: { ...lock.resources, "hitch-materialized-task": evidence.materializedTree } } });
|
|
78
|
+
const proofObjects = await exportResourceResultProof(input.store, evidence.plan, root.closure.objectDigests, input.runDirectory);
|
|
79
|
+
await atomicWriteJSON(path.join(input.runDirectory, "resource.execution.json"), { protocol: "hitch-resource-execution@1", plan: evidence.plan, materialized_task_digest: evidence.materialized_task_digest, materializedTree: evidence.materializedTree, images: evidence.images, observed, proofObjects, materialization: { clonedBytes: evidence.clonedBytes, copiedBytes: evidence.copiedBytes, fallbackReasons: evidence.fallbackReasons } });
|
|
80
|
+
}
|
|
81
|
+
export async function confirmResourceResultSealed(file) {
|
|
82
|
+
const evidence = await readResourceEvidence(file);
|
|
83
|
+
if (evidence)
|
|
84
|
+
await atomicWriteJSON(file, { ...evidence, resultSealed: true });
|
|
85
|
+
}
|
|
86
|
+
export async function reclaimResourceExecution(store, file, cleanupConfirmed) {
|
|
87
|
+
const evidence = await readResourceEvidence(file);
|
|
88
|
+
if (!evidence || !cleanupConfirmed || !evidence.executionEnded || !evidence.resultSealed)
|
|
89
|
+
return false;
|
|
90
|
+
await finishResourceWorkspace(store, evidence.id, { executionEnded: true, resultSealed: true });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=evidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evidence.js","sourceRoot":"","sources":["../../../src/resources/evidence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdir, readdir, rename, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { durableWriteJSON as atomicWriteJSON, readJSON } from "../foundation/index.js";
|
|
4
|
+
import { canonical, natural, object } from "./protocol.js";
|
|
5
|
+
import { validateLease, validateRoot } from "./store.js";
|
|
6
|
+
import { planViewGc } from "./views.js";
|
|
7
|
+
export async function auditResources(store, options = {}) {
|
|
8
|
+
const graceMs = options.graceMs ?? 24 * 60 * 60 * 1000;
|
|
9
|
+
natural(graceMs);
|
|
10
|
+
return store.locked(async () => {
|
|
11
|
+
const roots = await store.records("roots"), leases = await store.records("leases"), marked = new Map();
|
|
12
|
+
const mark = (digest, reason) => { const reasons = marked.get(digest) ?? new Set(); reasons.add(reason); marked.set(digest, reasons); };
|
|
13
|
+
for (const root of roots) {
|
|
14
|
+
validateRoot(root);
|
|
15
|
+
if (root.state !== "active")
|
|
16
|
+
continue;
|
|
17
|
+
const actual = await fileClosure(store, Object.values(root.lock.resources));
|
|
18
|
+
if (canonical(actual) !== canonical(root.closure.objectDigests))
|
|
19
|
+
throw new Error("durable resource closure is incomplete; GC stopped");
|
|
20
|
+
for (const digest of actual)
|
|
21
|
+
mark(digest, `owner:${root.owner}:${root.generation}`);
|
|
22
|
+
}
|
|
23
|
+
for (const lease of leases) {
|
|
24
|
+
validateLease(lease);
|
|
25
|
+
if (lease.state === "released")
|
|
26
|
+
continue;
|
|
27
|
+
// No PID or expiry test: unknown remote execution keeps its protection.
|
|
28
|
+
for (const digest of lease.objectDigests)
|
|
29
|
+
mark(digest, `${lease.state}-lease:${lease.id}`);
|
|
30
|
+
}
|
|
31
|
+
const report = { protocol: "hitch-resource-gc@1", dryRun: !options.apply, objects: 0, logicalBytes: 0, retained: [], eligible: [], quarantined: [], deleted: [] };
|
|
32
|
+
const directory = path.join(store.directory, "objects", "sha256"), quarantine = path.join(store.directory, "quarantine");
|
|
33
|
+
const names = async (dir) => readdir(dir).catch((e) => { if (e.code !== "ENOENT")
|
|
34
|
+
throw e; return []; });
|
|
35
|
+
// Verify every present object before any mutation. A partial producer may
|
|
36
|
+
// protect its next not-yet-published digest; active roots may never do so.
|
|
37
|
+
const files = await names(directory), quarantined = await names(quarantine);
|
|
38
|
+
for (const name of [...files, ...quarantined])
|
|
39
|
+
if (!/^[a-f0-9]{64}$/.test(name))
|
|
40
|
+
throw new Error("unknown object store entry; GC stopped");
|
|
41
|
+
const quarantineTimes = new Map();
|
|
42
|
+
for (const name of quarantined) {
|
|
43
|
+
const digest = `sha256:${name}`;
|
|
44
|
+
if (files.includes(name) || marked.has(digest))
|
|
45
|
+
throw new Error("duplicate or referenced quarantine object; GC stopped");
|
|
46
|
+
const index = object(await readJSON(path.join(store.directory, "quarantine-index", `${name}.json`)), ["protocol", "digest", "quarantinedAt"]);
|
|
47
|
+
if (index.protocol !== "hitch-resource-quarantine@1" || index.digest !== digest || !Number.isSafeInteger(index.quarantinedAt) || index.quarantinedAt < 0)
|
|
48
|
+
throw new Error("invalid quarantine record; GC stopped");
|
|
49
|
+
await store.objects.verify(digest, undefined, path.join(quarantine, name));
|
|
50
|
+
quarantineTimes.set(name, index.quarantinedAt);
|
|
51
|
+
}
|
|
52
|
+
for (const lease of leases)
|
|
53
|
+
if (lease.state !== "released" && lease.purpose !== "import") {
|
|
54
|
+
for (const digest of lease.objectDigests)
|
|
55
|
+
if (!files.includes(digest.slice(7)))
|
|
56
|
+
throw new Error("active resource lease has missing content; GC stopped");
|
|
57
|
+
}
|
|
58
|
+
for (const name of files) {
|
|
59
|
+
options.signal?.throwIfAborted();
|
|
60
|
+
report.logicalBytes += await store.objects.verify(`sha256:${name}`);
|
|
61
|
+
report.objects++;
|
|
62
|
+
}
|
|
63
|
+
const collectViews = await planViewGc(store, marked, graceMs);
|
|
64
|
+
if (options.apply)
|
|
65
|
+
await collectViews();
|
|
66
|
+
for (const name of files) {
|
|
67
|
+
const digest = `sha256:${name}`, reasons = marked.get(digest);
|
|
68
|
+
if (reasons) {
|
|
69
|
+
report.retained.push({ digest, reasons: [...reasons].sort() });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
report.eligible.push(digest);
|
|
73
|
+
if (!options.apply)
|
|
74
|
+
continue;
|
|
75
|
+
options.signal?.throwIfAborted();
|
|
76
|
+
await mkdir(quarantine, { recursive: true });
|
|
77
|
+
await atomicWriteJSON(path.join(store.directory, "quarantine-index", `${name}.json`), { protocol: "hitch-resource-quarantine@1", digest, quarantinedAt: Date.now() });
|
|
78
|
+
await rename(path.join(directory, name), path.join(quarantine, name));
|
|
79
|
+
report.quarantined.push(digest);
|
|
80
|
+
}
|
|
81
|
+
for (const name of quarantined) {
|
|
82
|
+
const digest = `sha256:${name}`;
|
|
83
|
+
if (marked.has(digest))
|
|
84
|
+
throw new Error("referenced object is quarantined; acquire must restore it before GC");
|
|
85
|
+
if (!options.apply || Date.now() - quarantineTimes.get(name) < graceMs)
|
|
86
|
+
continue;
|
|
87
|
+
options.signal?.throwIfAborted();
|
|
88
|
+
await rm(path.join(quarantine, name));
|
|
89
|
+
await rm(path.join(store.directory, "quarantine-index", `${name}.json`));
|
|
90
|
+
report.deleted.push(digest);
|
|
91
|
+
}
|
|
92
|
+
return report;
|
|
93
|
+
}, options.signal);
|
|
94
|
+
}
|
|
95
|
+
export async function fileClosure(store, resources) {
|
|
96
|
+
const found = new Set();
|
|
97
|
+
let bytes = 0, files = 0;
|
|
98
|
+
for (const resource of resources) {
|
|
99
|
+
if (resource.kind === "oci-image")
|
|
100
|
+
continue;
|
|
101
|
+
const digest = resource.kind === "blob" ? resource.digest : resource.manifestDigest;
|
|
102
|
+
await store.objects.verify(digest, resource.kind === "blob" ? resource.size : undefined);
|
|
103
|
+
found.add(digest);
|
|
104
|
+
if (resource.kind === "tree") {
|
|
105
|
+
const tree = await store.objects.readTree(digest);
|
|
106
|
+
files += tree.entries.length;
|
|
107
|
+
for (const entry of tree.entries)
|
|
108
|
+
if (entry.kind === "file") {
|
|
109
|
+
await store.objects.verify(entry.digest, entry.size);
|
|
110
|
+
found.add(entry.digest);
|
|
111
|
+
bytes += entry.size;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
bytes += resource.size;
|
|
116
|
+
files++;
|
|
117
|
+
}
|
|
118
|
+
if (bytes > store.objects.budget.maxTotalBytes || files > store.objects.budget.maxFiles)
|
|
119
|
+
throw new Error("resource closure budget exceeded");
|
|
120
|
+
}
|
|
121
|
+
return [...found].sort();
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=gc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gc.js","sourceRoot":"","sources":["../../../src/resources/gc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { ResourceStore } from "./store.js";
|
|
2
|
+
export { resourceRequest, resourceFileRequest, configuredResourceStore, preflightResourceInput } from "./api.js";
|
|
3
|
+
export { readResourceInput, parseResourceDataset, parseResourceSelection, parseResourceTask, selectResources, sealResourceTask, preflightResourceTask, taskClosureLock, resourceDescriptorDirectory } from "./tasks.js";
|
|
4
|
+
export { materializeResourceTask, finishResourceWorkspace } from "./materialize.js";
|
|
5
|
+
export { parseResource, parseResourceLock, parseStrictJson, parseTree, canonical, identity, hash, RESOURCE_CAPABILITIES } from "./protocol.js";
|
|
6
|
+
export { DockerResourceImageProvider } from "./oci-provider.js";
|
|
7
|
+
export { auditResources } from "./gc.js";
|
|
8
|
+
export { DEFAULT_RESOURCE_LIMITS } from "./object-store.js";
|
|
9
|
+
export { createResourceDelivery, parseResourceDelivery, receiveResourceDelivery } from "./transfer.js";
|
|
10
|
+
export { exportResourceBundle, importResourceBundle } from "./bundle.js";
|
|
11
|
+
export { readResourceEvidence, sealResourceEvidence, confirmResourceResultSealed, reclaimResourceExecution } from "./evidence.js";
|
|
12
|
+
export { resourceStorageStats } from "./stats.js";
|
|
13
|
+
export { retainResourceResult } from "./result-retention.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/resources/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEjH,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,eAAe,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAExN,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAGpF,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE/I,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAEvG,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAClI,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { roleDirectory, validateRoleContexts } from "./role-context.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { appendFile, chmod, copyFile, lstat, mkdir, readFile, rename, rm, statfs, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { durableWriteJSON as atomicWriteJSON } from "../foundation/index.js";
|
|
7
|
+
import { canonical, identity, parseStrictJson } from "./protocol.js";
|
|
8
|
+
import { validateLease } from "./store.js";
|
|
9
|
+
import { preflightResourceTask, parseResourceTask, taskClosureLock } from "./tasks.js";
|
|
10
|
+
export async function materializeResourceTask(store, plan, input) {
|
|
11
|
+
const checked = await preflightResourceTask(store, plan.task, { platform: plan.platform, owner: input.owner, generation: input.generation, ...(input.signal ? { signal: input.signal } : {}) });
|
|
12
|
+
if (checked.plan.digest !== plan.digest)
|
|
13
|
+
throw new Error("resource execution plan changed");
|
|
14
|
+
const { lease } = await store.acquire(input.owner, taskClosureLock(plan.task), "execution", input.signal), id = randomUUID();
|
|
15
|
+
const directory = path.join(store.directory, "workspaces", id), temp = `${directory}.tmp`, taskDirectory = path.join(directory, plan.task.task_id);
|
|
16
|
+
const evidence = { protocol: "hitch-resource-workspace@1", id, state: "active", leaseId: lease.id, plan, taskDirectory, materialized_task_digest: "", clonedBytes: 0, copiedBytes: 0, fallbackReasons: {}, images: checked.closure.images };
|
|
17
|
+
try {
|
|
18
|
+
return await store.locked(async () => {
|
|
19
|
+
const mode = input.policy?.mode ?? "auto", maxWorkspaces = input.policy?.maxWorkspaces ?? 4, maxCopy = input.policy?.maxCopyBytes ?? store.objects.budget.maxTotalBytes;
|
|
20
|
+
if (!["auto", "copy", "require-clone"].includes(mode) || !Number.isSafeInteger(maxWorkspaces) || maxWorkspaces < 1 || !Number.isSafeInteger(maxCopy) || maxCopy < 0)
|
|
21
|
+
throw new TypeError("invalid workspace policy");
|
|
22
|
+
const active = (await store.records("leases")).filter(l => { validateLease(l); return l.purpose === "execution" && l.state !== "released"; });
|
|
23
|
+
if (active.length > maxWorkspaces)
|
|
24
|
+
throw new Error("resource workspace concurrency budget exceeded");
|
|
25
|
+
await mkdir(path.dirname(directory), { recursive: true });
|
|
26
|
+
await mkdir(temp, { mode: 0o700 });
|
|
27
|
+
const destination = path.join(temp, plan.task.task_id);
|
|
28
|
+
await mkdir(destination);
|
|
29
|
+
const file = async (digest, target, size, executable) => {
|
|
30
|
+
input.signal?.throwIfAborted();
|
|
31
|
+
await store.objects.verify(digest, size);
|
|
32
|
+
let clone = false;
|
|
33
|
+
if (mode !== "copy")
|
|
34
|
+
try {
|
|
35
|
+
await copyFile(store.objects.objectPath(digest), target, constants.COPYFILE_FICLONE_FORCE | constants.COPYFILE_EXCL);
|
|
36
|
+
clone = true;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const code = error.code ?? "unknown";
|
|
40
|
+
if (mode === "require-clone" || !["ENOTSUP", "EOPNOTSUPP", "EXDEV", "ENOSYS"].includes(code))
|
|
41
|
+
throw error;
|
|
42
|
+
evidence.fallbackReasons[code] = (evidence.fallbackReasons[code] ?? 0) + 1;
|
|
43
|
+
}
|
|
44
|
+
if (clone)
|
|
45
|
+
evidence.clonedBytes += size;
|
|
46
|
+
else {
|
|
47
|
+
if (evidence.copiedBytes + size > maxCopy)
|
|
48
|
+
throw new Error("resource workspace copy budget exceeded");
|
|
49
|
+
const space = await statfs(temp, { bigint: true });
|
|
50
|
+
if (space.bavail * space.bsize < BigInt(size + store.objects.budget.minFreeBytes))
|
|
51
|
+
throw new Error("resource workspace free-space reserve exceeded");
|
|
52
|
+
await copyFile(store.objects.objectPath(digest), target, constants.COPYFILE_EXCL);
|
|
53
|
+
evidence.copiedBytes += size;
|
|
54
|
+
}
|
|
55
|
+
input.signal?.throwIfAborted();
|
|
56
|
+
await store.objects.verify(digest, size, target);
|
|
57
|
+
await chmod(target, executable ? 0o755 : 0o644);
|
|
58
|
+
};
|
|
59
|
+
const tree = async (resource, target) => {
|
|
60
|
+
const manifest = await store.objects.readTree(resource.manifestDigest);
|
|
61
|
+
for (const entry of manifest.entries) {
|
|
62
|
+
const output = path.join(target, entry.path);
|
|
63
|
+
if (entry.kind === "directory")
|
|
64
|
+
await mkdir(output, { mode: 0o755 });
|
|
65
|
+
else
|
|
66
|
+
await file(entry.digest, output, entry.size, entry.executable);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
await tree(plan.task.sourceTree, destination);
|
|
70
|
+
await validateRoleContexts(destination, plan.task.lock.bindings, plan.platform, checked.closure.images);
|
|
71
|
+
for (const binding of plan.task.lock.bindings) {
|
|
72
|
+
if (binding.use === "environment-image")
|
|
73
|
+
continue;
|
|
74
|
+
const target = path.join(destination, roleDirectory(binding.consumer), binding.target), resource = plan.task.lock.resources[binding.resource];
|
|
75
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
76
|
+
// A source descriptor may not overwrite or shadow a resource binding.
|
|
77
|
+
if (await lstat(target).catch((e) => { if (e.code !== "ENOENT")
|
|
78
|
+
throw e; return undefined; }))
|
|
79
|
+
throw new Error("resource target collides with task descriptor");
|
|
80
|
+
if (resource.kind === "tree") {
|
|
81
|
+
await mkdir(target);
|
|
82
|
+
await tree(resource, target);
|
|
83
|
+
}
|
|
84
|
+
else if (resource.kind === "blob" && binding.use === "input-file")
|
|
85
|
+
await file(resource.digest, target, resource.size, binding.executable);
|
|
86
|
+
else
|
|
87
|
+
throw new Error("resource binding changed after preflight");
|
|
88
|
+
}
|
|
89
|
+
const roles = new Map(plan.task.lock.bindings.map(b => [canonical(b.consumer), b.consumer]));
|
|
90
|
+
for (const [role, consumer] of roles) {
|
|
91
|
+
const file = path.join(destination, roleDirectory(consumer), "Dockerfile"), text = await readFile(file, "utf8");
|
|
92
|
+
const transported = text.replace(/^(FROM\s+)(\S+)(.*)$/gim, (_all, prefix, ref, suffix) => {
|
|
93
|
+
const image = checked.closure.images.find(i => ref.endsWith(`@${i.manifestDigest}`));
|
|
94
|
+
if (!image)
|
|
95
|
+
throw new Error("unresolved OCI build base");
|
|
96
|
+
return `${prefix}${image.reference}${suffix}`;
|
|
97
|
+
});
|
|
98
|
+
await writeFile(file, transported);
|
|
99
|
+
await appendFile(file, `\nLABEL dev.hitch.resource.plan=${JSON.stringify(plan.digest)} dev.hitch.resource.consumer=${JSON.stringify(role)}\n`);
|
|
100
|
+
}
|
|
101
|
+
// Import the actual output as an identity proof while holding the same lock.
|
|
102
|
+
// Its independent evidence lease keeps it reproducible until result sealing.
|
|
103
|
+
const staging = path.join(temp, "proof");
|
|
104
|
+
await mkdir(staging);
|
|
105
|
+
const actual = await store.objects.importTree(destination, staging, d => store.protect(lease, d), input.signal);
|
|
106
|
+
evidence.materializedTree = actual.resource;
|
|
107
|
+
evidence.materialized_task_digest = identity("hitch-materialized-task@1", actual.resource);
|
|
108
|
+
await rm(staging, { recursive: true });
|
|
109
|
+
input.signal?.throwIfAborted();
|
|
110
|
+
await rename(temp, directory);
|
|
111
|
+
await atomicWriteJSON(path.join(store.directory, "workspace-records", `${id}.json`), evidence);
|
|
112
|
+
return evidence;
|
|
113
|
+
}, input.signal);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
await rm(temp, { recursive: true, force: true });
|
|
117
|
+
// If published, retain the workspace and execution lease for explicit recovery.
|
|
118
|
+
if (!await lstat(directory).catch(() => undefined))
|
|
119
|
+
await store.finishLease(lease.id, "ended");
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function finishResourceWorkspace(store, id, confirmation) {
|
|
124
|
+
if (!/^[a-f0-9-]{36}$/.test(id) || confirmation.executionEnded !== true || confirmation.resultSealed !== true)
|
|
125
|
+
throw new TypeError("workspace cleanup requires execution end and result sealing confirmation");
|
|
126
|
+
await store.locked(async () => {
|
|
127
|
+
const file = path.join(store.directory, "workspace-records", `${id}.json`), record = parseStrictJson(await readFile(file, "utf8"));
|
|
128
|
+
parseResourceTask(record.plan.task);
|
|
129
|
+
if (record.protocol !== "hitch-resource-workspace@1" || record.id !== id || record.taskDirectory !== path.join(store.directory, "workspaces", id, record.plan.task.task_id))
|
|
130
|
+
throw new TypeError("workspace ownership record mismatch");
|
|
131
|
+
record.state = "sealed";
|
|
132
|
+
await atomicWriteJSON(file, record);
|
|
133
|
+
await rm(path.join(store.directory, "workspaces", id), { recursive: true, force: true });
|
|
134
|
+
const lease = await store.readRecord("leases", record.leaseId);
|
|
135
|
+
if (!lease)
|
|
136
|
+
throw new Error("workspace lease missing");
|
|
137
|
+
validateLease(lease);
|
|
138
|
+
await store.finishLeaseLocked(lease);
|
|
139
|
+
record.state = "removed";
|
|
140
|
+
await atomicWriteJSON(file, record);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=materialize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"materialize.js","sourceRoot":"","sources":["../../../src/resources/materialize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, readFile, readdir, rename, rm, statfs, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { syncDirectory } from "../foundation/index.js";
|
|
6
|
+
import { canonical, compare, hash, natural, parseStrictJson, parseTree, portablePath, sha } from "./protocol.js";
|
|
7
|
+
export const DEFAULT_RESOURCE_LIMITS = { maxObjectBytes: 1024 ** 3, maxTotalBytes: 8 * 1024 ** 3, maxFiles: 100_000, minFreeBytes: 64 * 1024 ** 2, maxViewBytes: 512 * 1024 ** 2 };
|
|
8
|
+
export function limits(input = {}) {
|
|
9
|
+
if (Object.keys(input).some(k => !Object.keys(DEFAULT_RESOURCE_LIMITS).includes(k)))
|
|
10
|
+
throw new TypeError("unknown resource budget");
|
|
11
|
+
const value = { ...DEFAULT_RESOURCE_LIMITS, ...input };
|
|
12
|
+
Object.values(value).forEach(natural);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
const signature = (s) => [s.dev, s.ino, s.mode, s.size, s.mtimeMs, s.ctimeMs].join(":");
|
|
16
|
+
export class ObjectStore {
|
|
17
|
+
root;
|
|
18
|
+
budget;
|
|
19
|
+
directory;
|
|
20
|
+
constructor(root, budget = DEFAULT_RESOURCE_LIMITS) {
|
|
21
|
+
this.root = root;
|
|
22
|
+
this.budget = budget;
|
|
23
|
+
this.directory = path.join(root, "store", "benchmark-resources");
|
|
24
|
+
}
|
|
25
|
+
objectPath(digest) { return path.join(this.directory, "objects", "sha256", sha(digest).slice(7)); }
|
|
26
|
+
async verify(digest, expectedSize, file = this.objectPath(digest)) {
|
|
27
|
+
const info = await lstat(file);
|
|
28
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > this.budget.maxObjectBytes || expectedSize !== undefined && info.size !== expectedSize)
|
|
29
|
+
throw new Error("resource object type or size mismatch");
|
|
30
|
+
const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
31
|
+
try {
|
|
32
|
+
if (signature(await handle.stat()) !== signature(info))
|
|
33
|
+
throw new Error("resource object changed while opening");
|
|
34
|
+
const h = createHash("sha256");
|
|
35
|
+
let size = 0;
|
|
36
|
+
for await (const chunk of handle.createReadStream({ autoClose: false })) {
|
|
37
|
+
size += chunk.length;
|
|
38
|
+
if (size > this.budget.maxObjectBytes)
|
|
39
|
+
throw new Error("resource object byte budget exceeded");
|
|
40
|
+
h.update(chunk);
|
|
41
|
+
}
|
|
42
|
+
if (`sha256:${h.digest("hex")}` !== digest || size !== info.size || signature(await handle.stat()) !== signature(info) || signature(await lstat(file)) !== signature(info))
|
|
43
|
+
throw new Error("resource object digest mismatch or concurrent modification");
|
|
44
|
+
return size;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await handle.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async readTree(digest) {
|
|
51
|
+
const size = await this.verify(digest);
|
|
52
|
+
if (size > 16 * 1024 ** 2)
|
|
53
|
+
throw new Error("tree manifest byte budget exceeded");
|
|
54
|
+
const text = await readFile(this.objectPath(digest), "utf8"), tree = parseTree(parseStrictJson(text));
|
|
55
|
+
if (hash(text) !== digest || text !== canonical(tree))
|
|
56
|
+
throw new Error("non-canonical tree manifest");
|
|
57
|
+
if (tree.entries.length > this.budget.maxFiles)
|
|
58
|
+
throw new Error("tree file budget exceeded");
|
|
59
|
+
return tree;
|
|
60
|
+
}
|
|
61
|
+
async publish(staged, digest, size, protect) {
|
|
62
|
+
await protect(digest); // Durable producer lease precedes first visibility.
|
|
63
|
+
const destination = this.objectPath(digest);
|
|
64
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
65
|
+
const existing = await lstat(destination).catch((e) => { if (e.code !== "ENOENT")
|
|
66
|
+
throw e; return undefined; });
|
|
67
|
+
if (existing) {
|
|
68
|
+
await this.verify(digest, size);
|
|
69
|
+
await rm(staged);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const quarantined = path.join(this.directory, "quarantine", digest.slice(7));
|
|
73
|
+
if (await lstat(quarantined).catch((e) => { if (e.code !== "ENOENT")
|
|
74
|
+
throw e; return undefined; })) {
|
|
75
|
+
await this.verify(digest, size, quarantined);
|
|
76
|
+
await rename(quarantined, destination);
|
|
77
|
+
await syncDirectory(path.dirname(destination));
|
|
78
|
+
await rm(path.join(this.directory, "quarantine-index", `${digest.slice(7)}.json`), { force: true });
|
|
79
|
+
await rm(staged);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
// All publishers and GC hold the store lock. An existing empty/corrupt object
|
|
83
|
+
// is never overwritten and shared files are never hardlinked into workspaces.
|
|
84
|
+
const handle = await open(staged, "r");
|
|
85
|
+
try {
|
|
86
|
+
await handle.sync();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await handle.close();
|
|
90
|
+
}
|
|
91
|
+
await rename(staged, destination);
|
|
92
|
+
await syncDirectory(path.dirname(destination));
|
|
93
|
+
await this.verify(digest, size);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
async importFile(source, staging, protect, signal) {
|
|
97
|
+
signal?.throwIfAborted();
|
|
98
|
+
const info = await lstat(source);
|
|
99
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
100
|
+
throw new TypeError("resource import requires a regular file");
|
|
101
|
+
if (info.size > this.budget.maxObjectBytes || info.size > this.budget.maxTotalBytes)
|
|
102
|
+
throw new Error("resource object byte budget exceeded");
|
|
103
|
+
const space = await statfs(staging, { bigint: true });
|
|
104
|
+
if (space.bsize * space.bavail < BigInt(info.size + this.budget.minFreeBytes))
|
|
105
|
+
throw new Error("resource import free-space reserve exceeded");
|
|
106
|
+
const input = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW), file = path.join(staging, randomUUID());
|
|
107
|
+
try {
|
|
108
|
+
if (signature(await input.stat()) !== signature(info))
|
|
109
|
+
throw new Error("resource source changed");
|
|
110
|
+
const output = await open(file, "wx", 0o600), digest = createHash("sha256");
|
|
111
|
+
let size = 0;
|
|
112
|
+
try {
|
|
113
|
+
for await (const chunk of input.createReadStream({ autoClose: false })) {
|
|
114
|
+
signal?.throwIfAborted();
|
|
115
|
+
size += chunk.length;
|
|
116
|
+
if (size > this.budget.maxObjectBytes || size > info.size)
|
|
117
|
+
throw new Error("resource source grew past byte budget");
|
|
118
|
+
digest.update(chunk);
|
|
119
|
+
await output.writeFile(chunk);
|
|
120
|
+
}
|
|
121
|
+
await output.sync();
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await output.close();
|
|
125
|
+
}
|
|
126
|
+
if (size !== info.size || signature(await input.stat()) !== signature(info) || signature(await lstat(source)) !== signature(info))
|
|
127
|
+
throw new Error("resource source changed during import");
|
|
128
|
+
const resource = { kind: "blob", digest: `sha256:${digest.digest("hex")}`, size };
|
|
129
|
+
signal?.throwIfAborted();
|
|
130
|
+
return { resource, storedBytes: await this.publish(file, resource.digest, size, protect) ? size : 0 };
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
await input.close();
|
|
134
|
+
await rm(file, { force: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async importTree(source, staging, protect, signal) {
|
|
138
|
+
const snapshot = [];
|
|
139
|
+
let logicalBytes = 0, storedBytes = 0;
|
|
140
|
+
const visit = async (file, name) => {
|
|
141
|
+
signal?.throwIfAborted();
|
|
142
|
+
const info = await lstat(file);
|
|
143
|
+
if (!info.isFile() && !info.isDirectory())
|
|
144
|
+
throw new TypeError("resource import refuses links and special files");
|
|
145
|
+
if (name)
|
|
146
|
+
portablePath(name);
|
|
147
|
+
snapshot.push({ name, file, info });
|
|
148
|
+
if (snapshot.length > this.budget.maxFiles + 1)
|
|
149
|
+
throw new Error("resource file budget exceeded");
|
|
150
|
+
if (info.isFile()) {
|
|
151
|
+
logicalBytes += info.size;
|
|
152
|
+
if (info.size > this.budget.maxObjectBytes || logicalBytes > this.budget.maxTotalBytes)
|
|
153
|
+
throw new Error("resource total byte budget exceeded");
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
for (const child of (await readdir(file)).sort(compare))
|
|
157
|
+
await visit(path.join(file, child), name ? `${name}/${child}` : child);
|
|
158
|
+
};
|
|
159
|
+
await visit(source, "");
|
|
160
|
+
if (!snapshot[0]?.info.isDirectory())
|
|
161
|
+
throw new TypeError("tree source must be a directory");
|
|
162
|
+
// Validate shape, Unicode and case collisions before publishing any content.
|
|
163
|
+
const skeleton = { protocol: "hitch-tree@1", entries: snapshot.filter(e => e.name).map(e => e.info.isDirectory()
|
|
164
|
+
? { path: e.name, kind: "directory" } : { path: e.name, kind: "file", digest: `sha256:${"0".repeat(64)}`, size: e.info.size, executable: Boolean(e.info.mode & 0o111) }).sort((a, b) => compare(a.path, b.path)) };
|
|
165
|
+
parseTree(skeleton);
|
|
166
|
+
const entries = [];
|
|
167
|
+
for (const e of snapshot) {
|
|
168
|
+
if (signature(await lstat(e.file)) !== signature(e.info))
|
|
169
|
+
throw new Error("resource source path changed");
|
|
170
|
+
if (!e.name)
|
|
171
|
+
continue;
|
|
172
|
+
if (e.info.isDirectory())
|
|
173
|
+
entries.push({ path: e.name, kind: "directory" });
|
|
174
|
+
else {
|
|
175
|
+
const imported = await this.importFile(e.file, staging, protect, signal);
|
|
176
|
+
storedBytes += imported.storedBytes;
|
|
177
|
+
entries.push({ path: e.name, kind: "file", digest: imported.resource.digest, size: imported.resource.size, executable: Boolean(e.info.mode & 0o111) });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const e of snapshot)
|
|
181
|
+
if (signature(await lstat(e.file)) !== signature(e.info))
|
|
182
|
+
throw new Error("resource source changed during tree import");
|
|
183
|
+
const tree = parseTree({ protocol: "hitch-tree@1", entries: entries.sort((a, b) => compare(a.path, b.path)) }), text = canonical(tree), manifestDigest = hash(text), file = path.join(staging, randomUUID());
|
|
184
|
+
if (Buffer.byteLength(text) > Math.min(this.budget.maxObjectBytes, 16 * 1024 ** 2))
|
|
185
|
+
throw new Error("tree manifest byte budget exceeded");
|
|
186
|
+
await writeFile(file, text, { flag: "wx", mode: 0o600 });
|
|
187
|
+
storedBytes += await this.publish(file, manifestDigest, Buffer.byteLength(text), protect) ? Buffer.byteLength(text) : 0;
|
|
188
|
+
return { resource: { kind: "tree", format: "hitch-tree@1", manifestDigest }, logicalBytes, storedBytes };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=object-store.js.map
|