@loucompanion/forge-bridge 0.1.3-dev.b3bce6ba65a6 → 0.1.3-dev.ba293ccf876b
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 +127 -0
- package/dist/bridge/bridge.base.d.ts +8 -1
- package/dist/bridge/bridge.service.d.ts +98 -3
- package/dist/bridge/bridge.service.js +1425 -45
- package/dist/bridge/bridge.types.d.ts +205 -1
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +100 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +14 -2
- package/dist/cleanup/cleanup.service.js +496 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.js +47 -1
- package/dist/cli/cli.service.d.ts +45 -2
- package/dist/cli/cli.service.js +377 -27
- package/dist/cli/local-config.d.ts +5 -0
- package/dist/cli/local-config.js +45 -1
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
- package/dist/runtime/attempt-process-supervisor.js +386 -0
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +83 -0
- package/dist/runtime/docker-engine-probe.js +300 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-diagnostics.d.ts +4 -0
- package/dist/runtime/host-diagnostics.js +95 -0
- package/dist/runtime/host-listener-inspector.d.ts +46 -0
- package/dist/runtime/host-listener-inspector.js +349 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +56 -0
- package/dist/runtime/host-runtime-contract.js +709 -0
- package/dist/runtime/host-runtime-lease.d.ts +201 -0
- package/dist/runtime/host-runtime-lease.js +2314 -0
- package/dist/runtime/host-runtime-promotion.d.ts +32 -0
- package/dist/runtime/host-runtime-promotion.js +206 -0
- package/dist/runtime/memory-capacity.d.ts +68 -0
- package/dist/runtime/memory-capacity.js +121 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +284 -0
- package/dist/runtime/pinned-directory-process.d.ts +4 -0
- package/dist/runtime/pinned-directory-process.js +20 -0
- package/dist/runtime/pinned-source-directory.d.ts +9 -0
- package/dist/runtime/pinned-source-directory.js +56 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
- package/dist/runtime/prepared-runtime-identity.d.ts +3 -0
- package/dist/runtime/prepared-runtime-identity.js +191 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +22 -0
- package/dist/runtime/prepared-runtime-readiness.js +191 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +121 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +72 -0
- package/dist/session/managed-attempt-tools.d.ts +21 -0
- package/dist/session/managed-attempt-tools.js +912 -0
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +335 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +256 -26
- package/dist/session/phase-completion.d.ts +12 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +51 -10
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +24 -8
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +189 -8
- package/dist/session/phase-completion.schemas.js +117 -4
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +153 -22
- package/dist/session/pre-publication-validation.d.ts +47 -0
- package/dist/session/pre-publication-validation.js +249 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +68 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
- package/dist/session/provider-cli/provider-cli.process.js +474 -23
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +14 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +147 -25
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +25 -2
- package/dist/session/provider-cli/provider-cli.service.js +636 -84
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +91 -16
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +38 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +531 -42
- package/dist/session/session.types.d.ts +115 -1
- package/dist/session/session.types.js +194 -3
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +21 -1
- package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
- package/dist/shared/process/dotnet-toolchain.js +179 -0
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +330 -0
- package/dist/shared/process/process.d.ts +2 -0
- package/dist/shared/process/process.js +11 -6
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +12 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +43 -4
- package/dist/worktree/dependency-warmer.d.ts +28 -1
- package/dist/worktree/dependency-warmer.js +308 -9
- package/dist/worktree/index.d.ts +2 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +14 -2
- package/dist/worktree/worktree.service.js +296 -47
- package/dist/worktree/worktree.types.d.ts +5 -1
- package/package.json +8 -4
- package/runtime/windows-job-supervisor.ps1 +444 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type HostParallelProofIdentity } from "./host-runtime-promotion.js";
|
|
2
|
+
/** Bind evidence to the prepared source, including unstaged replacements, not just HEAD. */
|
|
3
|
+
export declare function preparedRuntimeIdentity(worktreePath: string, signal?: AbortSignal): Promise<HostParallelProofIdentity>;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { GitClient } from "../shared/git/git.js";
|
|
5
|
+
import { hostRuntimeContractPath, loadHostRuntimeContract } from "./host-runtime-contract.js";
|
|
6
|
+
import { hostRuntimeContractFingerprint } from "./host-runtime-promotion.js";
|
|
7
|
+
import { readCanonicalSourceFile } from "./canonical-source-file.js";
|
|
8
|
+
import { assertCanonicalDirectory } from "./attempt-private-directory.js";
|
|
9
|
+
import { readPinnedSourceDirectory } from "./pinned-source-directory.js";
|
|
10
|
+
/** Bind evidence to the prepared source, including unstaged replacements, not just HEAD. */
|
|
11
|
+
export async function preparedRuntimeIdentity(worktreePath, signal) {
|
|
12
|
+
const root = await fs.realpath(worktreePath);
|
|
13
|
+
const rootIdentity = (await assertCanonicalDirectory(root));
|
|
14
|
+
const sourceKey = (target) => process.platform === "win32" ? target.toLowerCase() : target;
|
|
15
|
+
const expectedEntries = new Map();
|
|
16
|
+
const directories = new Map();
|
|
17
|
+
const containedRelative = (target) => {
|
|
18
|
+
const relative = path.relative(root, path.resolve(target));
|
|
19
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
20
|
+
throw new Error("host_runtime_source_invalid: source path escapes the worktree.");
|
|
21
|
+
}
|
|
22
|
+
return relative.replaceAll("\\", "/");
|
|
23
|
+
};
|
|
24
|
+
const directorySnapshot = async (target) => {
|
|
25
|
+
const resolved = path.resolve(target);
|
|
26
|
+
containedRelative(resolved);
|
|
27
|
+
const key = sourceKey(resolved);
|
|
28
|
+
let captured = directories.get(key);
|
|
29
|
+
if (!captured) {
|
|
30
|
+
captured = (async () => {
|
|
31
|
+
let identity;
|
|
32
|
+
if (key === sourceKey(root))
|
|
33
|
+
identity = rootIdentity;
|
|
34
|
+
else {
|
|
35
|
+
const parent = await expectedEntry(resolved);
|
|
36
|
+
if (!parent || parent.kind !== "directory") {
|
|
37
|
+
throw new Error("host_runtime_source_changed: discovery directory identity changed.");
|
|
38
|
+
}
|
|
39
|
+
identity = parent.identity;
|
|
40
|
+
}
|
|
41
|
+
const entries = await readPinnedSourceDirectory(resolved, identity);
|
|
42
|
+
for (const entry of entries)
|
|
43
|
+
expectedEntries.set(sourceKey(path.join(resolved, entry.name)), entry);
|
|
44
|
+
return { path: resolved, identity, entries };
|
|
45
|
+
})();
|
|
46
|
+
directories.set(key, captured);
|
|
47
|
+
}
|
|
48
|
+
return (await captured).entries.map(entry => ({
|
|
49
|
+
name: entry.name,
|
|
50
|
+
isDirectory: entry.kind === "directory",
|
|
51
|
+
isFile: entry.kind === "file",
|
|
52
|
+
}));
|
|
53
|
+
};
|
|
54
|
+
const expectedEntry = async (target) => {
|
|
55
|
+
const resolved = path.resolve(target);
|
|
56
|
+
containedRelative(resolved);
|
|
57
|
+
if (sourceKey(resolved) === sourceKey(root)) {
|
|
58
|
+
return { name: path.basename(root), kind: "directory", identity: rootIdentity, size: 0n };
|
|
59
|
+
}
|
|
60
|
+
const parentPath = path.dirname(resolved);
|
|
61
|
+
if (sourceKey(parentPath) !== sourceKey(root)) {
|
|
62
|
+
const parent = await expectedEntry(parentPath);
|
|
63
|
+
if (!parent)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (parent.kind !== "directory") {
|
|
66
|
+
throw new Error("host_runtime_source_changed: discovery ancestor is not a directory.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await directorySnapshot(parentPath);
|
|
70
|
+
return expectedEntries.get(sourceKey(resolved));
|
|
71
|
+
};
|
|
72
|
+
// Pin the worktree before Git or discovery uses it. Every child directory is
|
|
73
|
+
// subsequently reached through an identity captured from its pinned parent.
|
|
74
|
+
await directorySnapshot(root);
|
|
75
|
+
const git = new GitClient();
|
|
76
|
+
const head = (await git.run(root, ["rev-parse", "HEAD"])).trim();
|
|
77
|
+
const inventory = async () => {
|
|
78
|
+
const included = (name) => name && (!name.startsWith(".forge/") || name === ".forge/forge.runtime.yml");
|
|
79
|
+
// ls-files --deleted loses a deletion once staged. HEAD-relative tombstones
|
|
80
|
+
// keep the same prepared source identity before and after git add -u.
|
|
81
|
+
const deleted = [...new Set((await git.run(root, ["ls-files", "-z", "--deleted"]))
|
|
82
|
+
.split("\0").concat((await git.run(root, ["diff", "--name-only", "--diff-filter=D", "-z", "HEAD", "--"]))
|
|
83
|
+
.split("\0")).filter(included))].sort();
|
|
84
|
+
const names = [...new Set((await git.run(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]))
|
|
85
|
+
.split("\0").concat(deleted, hostRuntimeContractPath).filter(included))].sort();
|
|
86
|
+
return { names, deleted };
|
|
87
|
+
};
|
|
88
|
+
const source = await inventory();
|
|
89
|
+
const { names } = source;
|
|
90
|
+
if (names.length > 100_000)
|
|
91
|
+
throw new Error("host_runtime_source_invalid: source inventory exceeds the file limit.");
|
|
92
|
+
const deleted = new Set(source.deleted);
|
|
93
|
+
const hash = createHash("sha256");
|
|
94
|
+
const snapshot = new Map();
|
|
95
|
+
const additional = new Map();
|
|
96
|
+
let discoveryCapture = Promise.resolve();
|
|
97
|
+
let bytes = 0;
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
signal?.throwIfAborted();
|
|
100
|
+
const target = path.resolve(root, name);
|
|
101
|
+
const relative = path.relative(root, target);
|
|
102
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
103
|
+
throw new Error("host_runtime_source_invalid: source path escapes the worktree.");
|
|
104
|
+
}
|
|
105
|
+
hash.update(JSON.stringify(name));
|
|
106
|
+
try {
|
|
107
|
+
const expected = await expectedEntry(target);
|
|
108
|
+
if (!expected || expected.kind !== "file") {
|
|
109
|
+
throw Object.assign(new Error("host_runtime_source_changed: source inventory no longer names an ordinary file."), { code: "ENOENT" });
|
|
110
|
+
}
|
|
111
|
+
const sourceBytes = await readCanonicalSourceFile(target, 256 * 1024 * 1024 - bytes, expected.identity);
|
|
112
|
+
bytes += sourceBytes.length;
|
|
113
|
+
hash.update(createHash("sha256").update(sourceBytes).digest());
|
|
114
|
+
snapshot.set(sourceKey(target), Promise.resolve(sourceBytes));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code !== "ENOENT" || (!deleted.has(name) && name !== hostRuntimeContractPath))
|
|
118
|
+
throw error;
|
|
119
|
+
hash.update(deleted.has(name) ? "deleted" : "absent");
|
|
120
|
+
snapshot.set(sourceKey(target), Promise.resolve(null));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const contract = await loadHostRuntimeContract(root, target => {
|
|
124
|
+
const key = sourceKey(path.resolve(target));
|
|
125
|
+
const captured = snapshot.get(key);
|
|
126
|
+
if (captured)
|
|
127
|
+
return captured;
|
|
128
|
+
// Discovery also consumes ignored lockfiles and launch metadata. Capture
|
|
129
|
+
// those once, too; omission from Git's inventory must not change discovery.
|
|
130
|
+
const capture = discoveryCapture.then(async () => {
|
|
131
|
+
signal?.throwIfAborted();
|
|
132
|
+
const relative = path.relative(root, target).replaceAll("\\", "/");
|
|
133
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
134
|
+
throw new Error("host_runtime_source_invalid: discovery input escapes the worktree.");
|
|
135
|
+
}
|
|
136
|
+
let sourceBytes;
|
|
137
|
+
try {
|
|
138
|
+
const expected = await expectedEntry(target);
|
|
139
|
+
if (!expected)
|
|
140
|
+
sourceBytes = null;
|
|
141
|
+
else if (expected.kind !== "file") {
|
|
142
|
+
throw new Error("host_runtime_source_changed: discovery input is not an ordinary file.");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
sourceBytes = await readCanonicalSourceFile(target, 256 * 1024 * 1024 - bytes, expected.identity);
|
|
146
|
+
}
|
|
147
|
+
if (sourceBytes !== null) {
|
|
148
|
+
bytes += sourceBytes.length;
|
|
149
|
+
if (bytes > 256 * 1024 * 1024)
|
|
150
|
+
throw new Error("host_runtime_source_invalid: discovery exceeds the byte limit.");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code !== "ENOENT")
|
|
155
|
+
throw error;
|
|
156
|
+
sourceBytes = null;
|
|
157
|
+
}
|
|
158
|
+
additional.set(relative, sourceBytes);
|
|
159
|
+
return sourceBytes;
|
|
160
|
+
});
|
|
161
|
+
// Discovery walks in parallel, but reads share one total byte budget.
|
|
162
|
+
discoveryCapture = capture.then(() => undefined, () => undefined);
|
|
163
|
+
snapshot.set(key, capture);
|
|
164
|
+
return capture;
|
|
165
|
+
}, directorySnapshot);
|
|
166
|
+
for (const [name, sourceBytes] of [...additional].sort(([left], [right]) => left.localeCompare(right))) {
|
|
167
|
+
hash.update(JSON.stringify(["runtime-discovery", name]));
|
|
168
|
+
hash.update(sourceBytes === null ? "absent" : createHash("sha256").update(sourceBytes).digest());
|
|
169
|
+
}
|
|
170
|
+
if (head !== (await git.run(root, ["rev-parse", "HEAD"])).trim()
|
|
171
|
+
|| JSON.stringify(source) !== JSON.stringify(await inventory())) {
|
|
172
|
+
throw new Error("host_runtime_source_changed: prepared source changed during verification.");
|
|
173
|
+
}
|
|
174
|
+
for (const captured of await Promise.all([...directories.values()])) {
|
|
175
|
+
const current = await readPinnedSourceDirectory(captured.path, captured.identity);
|
|
176
|
+
if (directoryInventory(current) !== directoryInventory(captured.entries)) {
|
|
177
|
+
throw new Error("host_runtime_source_changed: runtime discovery inventory changed during verification.");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
inputCommitSha: head,
|
|
182
|
+
runtimeContractFingerprint: hostRuntimeContractFingerprint(contract),
|
|
183
|
+
sourceFingerprint: `sha256:${hash.digest("hex")}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function directoryInventory(entries) {
|
|
187
|
+
return JSON.stringify(entries.map(entry => ({
|
|
188
|
+
name: entry.name,
|
|
189
|
+
kind: entry.kind,
|
|
190
|
+
})));
|
|
191
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RunOnceDispatchPacket } from "../session/session.types.js";
|
|
2
|
+
import type { HostRuntimeAdmissionCheckpointHandler, HostRuntimeLeaseRuntime } from "./host-runtime-lease.js";
|
|
3
|
+
import { HostParallelPromotionStore, type HostParallelProofIdentity } from "./host-runtime-promotion.js";
|
|
4
|
+
/** Foreground barrier: prepare a wave, verify its exact runtimes, then release agents. */
|
|
5
|
+
export declare class PreparedRuntimeReadiness {
|
|
6
|
+
private readonly runtime;
|
|
7
|
+
private readonly store;
|
|
8
|
+
private readonly releasePreparation;
|
|
9
|
+
private readonly admissionExpired;
|
|
10
|
+
private readonly preparations;
|
|
11
|
+
private readonly draining;
|
|
12
|
+
constructor(runtime: HostRuntimeLeaseRuntime, store: HostParallelPromotionStore, releasePreparation: (id: string, fence: string) => Promise<unknown>, admissionExpired: () => Error);
|
|
13
|
+
acquire(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
|
|
14
|
+
has(id: string): boolean;
|
|
15
|
+
hasPending(): boolean;
|
|
16
|
+
verify(dispatch: RunOnceDispatchPacket, worktree: string, checkpoint?: HostRuntimeAdmissionCheckpointHandler): Promise<HostParallelProofIdentity | undefined>;
|
|
17
|
+
renew(id: string, fence: string, expiresAt: string): Promise<void>;
|
|
18
|
+
cancel(id: string, fence: string, reason?: Error): void;
|
|
19
|
+
private drain;
|
|
20
|
+
private scheduleDeadline;
|
|
21
|
+
private check;
|
|
22
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { loadHostRuntimeContract } from "./host-runtime-contract.js";
|
|
3
|
+
import { configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible } from "./host-runtime-promotion.js";
|
|
4
|
+
import { preparedRuntimeIdentity } from "./prepared-runtime-identity.js";
|
|
5
|
+
/** Foreground barrier: prepare a wave, verify its exact runtimes, then release agents. */
|
|
6
|
+
export class PreparedRuntimeReadiness {
|
|
7
|
+
runtime;
|
|
8
|
+
store;
|
|
9
|
+
releasePreparation;
|
|
10
|
+
admissionExpired;
|
|
11
|
+
preparations = new Map();
|
|
12
|
+
draining = new Set();
|
|
13
|
+
constructor(runtime, store, releasePreparation, admissionExpired) {
|
|
14
|
+
this.runtime = runtime;
|
|
15
|
+
this.store = store;
|
|
16
|
+
this.releasePreparation = releasePreparation;
|
|
17
|
+
this.admissionExpired = admissionExpired;
|
|
18
|
+
}
|
|
19
|
+
async acquire(dispatch, signal) {
|
|
20
|
+
const id = dispatch.sandbox.dispatchAttemptId;
|
|
21
|
+
if (this.preparations.has(id))
|
|
22
|
+
throw new Error("host_runtime_preparation_conflict: duplicate preparation.");
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const abort = () => this.cancel(id, dispatch.lease.fencingToken);
|
|
25
|
+
const entry = { dispatch, controller, phase: "preparing", expiresAt: dispatch.lease.expiresAt,
|
|
26
|
+
removeAbort: () => signal?.removeEventListener("abort", abort) };
|
|
27
|
+
this.preparations.set(id, entry);
|
|
28
|
+
this.scheduleDeadline(entry);
|
|
29
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
30
|
+
if (signal?.aborted)
|
|
31
|
+
abort();
|
|
32
|
+
try {
|
|
33
|
+
// Preparation consumes normal capacity, but no project-runtime claim.
|
|
34
|
+
return await this.runtime.acquire({ ...dispatch,
|
|
35
|
+
runtimeIntent: { ...dispatch.runtimeIntent, executionClass: "analysis_only", applicationRuntimeRequired: false },
|
|
36
|
+
}, controller.signal);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const failure = controller.signal.aborted ? controller.signal.reason : error;
|
|
40
|
+
this.cancel(id, dispatch.lease.fencingToken);
|
|
41
|
+
throw failure;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
has(id) { return this.preparations.has(id); }
|
|
45
|
+
hasPending() { return this.preparations.size > 0; }
|
|
46
|
+
async verify(dispatch, worktree, checkpoint) {
|
|
47
|
+
const id = dispatch.sandbox.dispatchAttemptId;
|
|
48
|
+
const entry = this.preparations.get(id);
|
|
49
|
+
if (!entry || entry.dispatch.lease.fencingToken !== dispatch.lease.fencingToken) {
|
|
50
|
+
throw new Error("host_runtime_preparation_missing: preparation ownership changed.");
|
|
51
|
+
}
|
|
52
|
+
// Release preparation headroom before waiting for peers; otherwise a one-slot
|
|
53
|
+
// host would deadlock while the rest of the wave tries to prepare.
|
|
54
|
+
await this.releasePreparation(id, dispatch.lease.fencingToken);
|
|
55
|
+
entry.controller.signal.throwIfAborted();
|
|
56
|
+
const completion = new Promise((resolve, reject) => {
|
|
57
|
+
entry.resolve = resolve;
|
|
58
|
+
entry.reject = reject;
|
|
59
|
+
});
|
|
60
|
+
entry.check = () => this.check(entry, worktree, checkpoint);
|
|
61
|
+
entry.phase = "queued";
|
|
62
|
+
void this.drain(dispatch.repo.id);
|
|
63
|
+
return await completion;
|
|
64
|
+
}
|
|
65
|
+
async renew(id, fence, expiresAt) {
|
|
66
|
+
const entry = this.preparations.get(id);
|
|
67
|
+
if (!entry || entry.dispatch.lease.fencingToken !== fence || Date.parse(expiresAt) <= Date.parse(entry.expiresAt))
|
|
68
|
+
return;
|
|
69
|
+
entry.expiresAt = expiresAt;
|
|
70
|
+
this.scheduleDeadline(entry);
|
|
71
|
+
if (entry.probe)
|
|
72
|
+
await this.runtime.updateLease(entry.probe.sandbox.dispatchAttemptId, entry.probe.lease.fencingToken, expiresAt);
|
|
73
|
+
}
|
|
74
|
+
cancel(id, fence, reason = new Error("host_runtime_preparation_cancelled: prepared readiness was cancelled.")) {
|
|
75
|
+
const entry = this.preparations.get(id);
|
|
76
|
+
if (!entry || entry.dispatch.lease.fencingToken !== fence)
|
|
77
|
+
return;
|
|
78
|
+
if (entry.deadline)
|
|
79
|
+
clearTimeout(entry.deadline);
|
|
80
|
+
entry.controller.abort(reason);
|
|
81
|
+
// Do not return to session teardown while a supervised probe still owns the worktree.
|
|
82
|
+
if (entry.phase === "checking")
|
|
83
|
+
return;
|
|
84
|
+
entry.reject?.(entry.controller.signal.reason);
|
|
85
|
+
entry.removeAbort();
|
|
86
|
+
this.preparations.delete(id);
|
|
87
|
+
void this.drain(entry.dispatch.repo.id);
|
|
88
|
+
}
|
|
89
|
+
async drain(repoId) {
|
|
90
|
+
if (this.draining.has(repoId))
|
|
91
|
+
return;
|
|
92
|
+
const entries = () => [...this.preparations.values()].filter(entry => entry.dispatch.repo.id === repoId);
|
|
93
|
+
if (entries().some(entry => entry.phase === "preparing"))
|
|
94
|
+
return;
|
|
95
|
+
this.draining.add(repoId);
|
|
96
|
+
const completed = [];
|
|
97
|
+
try {
|
|
98
|
+
for (const entry of entries()) {
|
|
99
|
+
if (entry.phase !== "queued")
|
|
100
|
+
continue;
|
|
101
|
+
entry.phase = "checking";
|
|
102
|
+
try {
|
|
103
|
+
completed.push({ entry, identity: await entry.check() });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
completed.push({ entry, error });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// No provider from this wave can start while a sibling proof is running.
|
|
110
|
+
for (const result of completed) {
|
|
111
|
+
const { entry } = result;
|
|
112
|
+
this.preparations.delete(entry.dispatch.sandbox.dispatchAttemptId);
|
|
113
|
+
if (entry.deadline)
|
|
114
|
+
clearTimeout(entry.deadline);
|
|
115
|
+
entry.removeAbort();
|
|
116
|
+
if (entry.controller.signal.aborted)
|
|
117
|
+
entry.reject?.(entry.controller.signal.reason);
|
|
118
|
+
else if (result.error)
|
|
119
|
+
entry.reject?.(result.error);
|
|
120
|
+
else
|
|
121
|
+
entry.resolve?.(result.identity);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
this.draining.delete(repoId);
|
|
126
|
+
if (entries().some(entry => entry.phase === "queued"))
|
|
127
|
+
void this.drain(repoId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
scheduleDeadline(entry) {
|
|
131
|
+
if (entry.deadline)
|
|
132
|
+
clearTimeout(entry.deadline);
|
|
133
|
+
const delay = Date.parse(entry.expiresAt) - Date.now();
|
|
134
|
+
entry.deadline = setTimeout(() => this.cancel(entry.dispatch.sandbox.dispatchAttemptId, entry.dispatch.lease.fencingToken, this.admissionExpired()), Number.isFinite(delay) ? Math.max(0, Math.min(delay, 2_147_483_647)) : 0);
|
|
135
|
+
entry.deadline.unref();
|
|
136
|
+
}
|
|
137
|
+
async check(entry, worktree, checkpoint) {
|
|
138
|
+
const { dispatch, controller } = entry;
|
|
139
|
+
if (!dispatch.runtimeIntent.applicationRuntimeRequired)
|
|
140
|
+
return undefined;
|
|
141
|
+
const contract = await loadHostRuntimeContract(worktree);
|
|
142
|
+
if (!hostRuntimeParallelEligible(contract))
|
|
143
|
+
return undefined;
|
|
144
|
+
const identity = await preparedRuntimeIdentity(worktree, controller.signal);
|
|
145
|
+
if (hostRuntimeContractFingerprint(contract) !== identity.runtimeContractFingerprint) {
|
|
146
|
+
throw new Error("host_runtime_prepared_probe_changed: runtime contract changed while capturing its source identity.");
|
|
147
|
+
}
|
|
148
|
+
if (await this.store.load(dispatch.repo.id, identity))
|
|
149
|
+
return identity;
|
|
150
|
+
await checkpoint?.({ code: "host_runtime_prepared_probe_started", expiresAt: entry.expiresAt,
|
|
151
|
+
message: `Verifying prepared runtime ${identity.inputCommitSha.slice(0, 12)} before agent admission; project-default readiness is not a substitute.` });
|
|
152
|
+
const probeId = randomUUID();
|
|
153
|
+
const probe = { ...dispatch,
|
|
154
|
+
cycleRun: { ...dispatch.cycleRun, phase: "prepared_readiness" },
|
|
155
|
+
lease: { ...dispatch.lease, id: probeId, fencingToken: randomUUID(), expiresAt: entry.expiresAt },
|
|
156
|
+
sandbox: { ...dispatch.sandbox, dispatchAttemptId: probeId, executionLeaseId: probeId,
|
|
157
|
+
inputCommitSha: identity.inputCommitSha },
|
|
158
|
+
};
|
|
159
|
+
entry.probe = probe;
|
|
160
|
+
let cleanupConfirmed;
|
|
161
|
+
try {
|
|
162
|
+
controller.signal.throwIfAborted();
|
|
163
|
+
await this.runtime.acquire(probe, controller.signal);
|
|
164
|
+
const prepared = await this.runtime.start(probe, worktree, controller.signal);
|
|
165
|
+
if (hostRuntimeContractFingerprint(prepared.runtimeContract) !== identity.runtimeContractFingerprint) {
|
|
166
|
+
throw new Error("host_runtime_prepared_probe_changed: probe loaded a different runtime contract; application was not started.");
|
|
167
|
+
}
|
|
168
|
+
const started = await this.runtime.startConfiguredProject(probeId, probe.lease.fencingToken, controller.signal, progress => checkpoint?.({ code: "host_runtime_prepared_probe_progress", expiresAt: entry.expiresAt,
|
|
169
|
+
message: progress.message }));
|
|
170
|
+
const ports = started.lease.resources.ports.map(port => port.name).sort();
|
|
171
|
+
const verified = JSON.stringify(ports) === JSON.stringify(configuredLaunchPortNames(contract))
|
|
172
|
+
&& hostRuntimeContractFingerprint(started.runtimeContract) === identity.runtimeContractFingerprint
|
|
173
|
+
&& JSON.stringify(identity) === JSON.stringify(await preparedRuntimeIdentity(worktree, controller.signal));
|
|
174
|
+
if (!verified)
|
|
175
|
+
throw new Error("host_runtime_prepared_probe_changed: source changed or assigned-port verification was incomplete.");
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
// A failed teardown must not publish reusable evidence.
|
|
179
|
+
cleanupConfirmed = Boolean(await this.runtime.release(probeId, probe.lease.fencingToken));
|
|
180
|
+
entry.probe = undefined;
|
|
181
|
+
}
|
|
182
|
+
if (!cleanupConfirmed) {
|
|
183
|
+
throw new Error("host_runtime_prepared_probe_cleanup_unconfirmed: matching probe teardown was not confirmed.");
|
|
184
|
+
}
|
|
185
|
+
controller.signal.throwIfAborted();
|
|
186
|
+
await this.store.observe(dispatch.repo.id, contract, probeId, configuredLaunchPortNames(contract), identity.inputCommitSha, identity.sourceFingerprint);
|
|
187
|
+
await checkpoint?.({ code: "host_runtime_prepared_probe_completed", expiresAt: entry.expiresAt,
|
|
188
|
+
message: `Prepared runtime ${identity.inputCommitSha.slice(0, 12)} passed isolation verification; its source-bound proof is retained independently.` });
|
|
189
|
+
return identity;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export declare const runtimeIsolationLeaseStates: readonly ["reserved", "starting", "active", "releasing", "released", "degraded"];
|
|
2
|
+
export type RuntimeIsolationLeaseState = typeof runtimeIsolationLeaseStates[number];
|
|
3
|
+
export type RuntimeIsolationMode = "docker" | "host";
|
|
4
|
+
export interface RuntimeIsolationPort {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly protocol: "tcp" | "udp";
|
|
7
|
+
readonly hostIp: "127.0.0.1" | "::1";
|
|
8
|
+
readonly hostPort: number;
|
|
9
|
+
readonly targetPort: number;
|
|
10
|
+
}
|
|
11
|
+
export interface RuntimeIsolationPath {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly role: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RuntimeIsolationDockerResource {
|
|
16
|
+
readonly type: "container" | "network" | "volume";
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly role: string;
|
|
19
|
+
readonly id?: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface RuntimeIsolationResources {
|
|
22
|
+
readonly memoryReservationBytes: number;
|
|
23
|
+
readonly cpuMillis: number;
|
|
24
|
+
readonly pids: number;
|
|
25
|
+
readonly ports: readonly RuntimeIsolationPort[];
|
|
26
|
+
readonly paths: readonly RuntimeIsolationPath[];
|
|
27
|
+
readonly processSupervisorId: string | null;
|
|
28
|
+
readonly dockerResources: readonly RuntimeIsolationDockerResource[];
|
|
29
|
+
readonly singletonKeys: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export interface RuntimeIsolationLease {
|
|
32
|
+
readonly schemaVersion: 1;
|
|
33
|
+
readonly leaseId: string;
|
|
34
|
+
readonly dispatchAttemptId: string;
|
|
35
|
+
readonly executionLeaseId: string;
|
|
36
|
+
readonly fencingToken: string;
|
|
37
|
+
readonly runnerId: string;
|
|
38
|
+
readonly runnerInstanceId: string;
|
|
39
|
+
readonly worktreeKey: string;
|
|
40
|
+
readonly worktreePath: string;
|
|
41
|
+
readonly mode: RuntimeIsolationMode;
|
|
42
|
+
readonly state: RuntimeIsolationLeaseState;
|
|
43
|
+
readonly createdAt: string;
|
|
44
|
+
readonly stateChangedAt: string;
|
|
45
|
+
readonly expiresAt: string;
|
|
46
|
+
readonly releasedAt: string | null;
|
|
47
|
+
readonly resources: RuntimeIsolationResources;
|
|
48
|
+
}
|
|
49
|
+
export interface RuntimeIsolationLeaseInput {
|
|
50
|
+
readonly leaseId: string;
|
|
51
|
+
readonly dispatchAttemptId: string;
|
|
52
|
+
readonly executionLeaseId: string;
|
|
53
|
+
readonly fencingToken: string;
|
|
54
|
+
readonly runnerId: string;
|
|
55
|
+
readonly runnerInstanceId: string;
|
|
56
|
+
readonly worktreeKey: string;
|
|
57
|
+
readonly worktreePath: string;
|
|
58
|
+
readonly mode: RuntimeIsolationMode;
|
|
59
|
+
readonly expiresAt: string;
|
|
60
|
+
readonly resources: RuntimeIsolationResources;
|
|
61
|
+
readonly createdAt?: string;
|
|
62
|
+
}
|
|
63
|
+
export declare function createRuntimeIsolationLease(input: RuntimeIsolationLeaseInput): RuntimeIsolationLease;
|
|
64
|
+
export declare function transitionRuntimeIsolationLease(lease: RuntimeIsolationLease, nextState: RuntimeIsolationLeaseState, occurredAt?: string): RuntimeIsolationLease;
|
|
65
|
+
export declare function renewRuntimeIsolationLease(lease: RuntimeIsolationLease, fencingToken: string, expiresAt: string): RuntimeIsolationLease | null;
|
|
66
|
+
export declare function withRuntimeIsolationResources(lease: RuntimeIsolationLease, resources: RuntimeIsolationResources): RuntimeIsolationLease;
|
|
67
|
+
export declare function validRuntimeIsolationLease(value: unknown): value is RuntimeIsolationLease;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export const runtimeIsolationLeaseStates = [
|
|
2
|
+
"reserved",
|
|
3
|
+
"starting",
|
|
4
|
+
"active",
|
|
5
|
+
"releasing",
|
|
6
|
+
"released",
|
|
7
|
+
"degraded",
|
|
8
|
+
];
|
|
9
|
+
const allowedTransitions = {
|
|
10
|
+
reserved: new Set(["starting", "releasing", "degraded"]),
|
|
11
|
+
starting: new Set(["active", "releasing", "degraded"]),
|
|
12
|
+
active: new Set(["releasing", "degraded"]),
|
|
13
|
+
releasing: new Set(["released", "degraded"]),
|
|
14
|
+
released: new Set(),
|
|
15
|
+
degraded: new Set(["releasing"]),
|
|
16
|
+
};
|
|
17
|
+
export function createRuntimeIsolationLease(input) {
|
|
18
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
19
|
+
const lease = {
|
|
20
|
+
schemaVersion: 1,
|
|
21
|
+
leaseId: input.leaseId,
|
|
22
|
+
dispatchAttemptId: input.dispatchAttemptId,
|
|
23
|
+
executionLeaseId: input.executionLeaseId,
|
|
24
|
+
fencingToken: input.fencingToken,
|
|
25
|
+
runnerId: input.runnerId,
|
|
26
|
+
runnerInstanceId: input.runnerInstanceId,
|
|
27
|
+
worktreeKey: input.worktreeKey,
|
|
28
|
+
worktreePath: input.worktreePath,
|
|
29
|
+
mode: input.mode,
|
|
30
|
+
state: "reserved",
|
|
31
|
+
createdAt,
|
|
32
|
+
stateChangedAt: createdAt,
|
|
33
|
+
expiresAt: input.expiresAt,
|
|
34
|
+
releasedAt: null,
|
|
35
|
+
resources: input.resources,
|
|
36
|
+
};
|
|
37
|
+
if (!validRuntimeIsolationLease(lease)) {
|
|
38
|
+
throw new Error("runtime_lease_invalid: lease identity, lifetime, or resources are invalid.");
|
|
39
|
+
}
|
|
40
|
+
return lease;
|
|
41
|
+
}
|
|
42
|
+
export function transitionRuntimeIsolationLease(lease, nextState, occurredAt = new Date().toISOString()) {
|
|
43
|
+
if (lease.state === nextState)
|
|
44
|
+
return lease;
|
|
45
|
+
if (!allowedTransitions[lease.state].has(nextState) || !validTimestamp(occurredAt)) {
|
|
46
|
+
throw new Error(`runtime_lease_transition_invalid: cannot transition ${lease.state} to ${nextState}.`);
|
|
47
|
+
}
|
|
48
|
+
const transitioned = {
|
|
49
|
+
...lease,
|
|
50
|
+
state: nextState,
|
|
51
|
+
stateChangedAt: occurredAt,
|
|
52
|
+
releasedAt: nextState === "released" ? occurredAt : null,
|
|
53
|
+
};
|
|
54
|
+
if (!validRuntimeIsolationLease(transitioned)) {
|
|
55
|
+
throw new Error("runtime_lease_invalid: transitioned lease is invalid.");
|
|
56
|
+
}
|
|
57
|
+
return transitioned;
|
|
58
|
+
}
|
|
59
|
+
export function renewRuntimeIsolationLease(lease, fencingToken, expiresAt) {
|
|
60
|
+
if (lease.fencingToken !== fencingToken || lease.state === "released" || !validTimestamp(expiresAt))
|
|
61
|
+
return null;
|
|
62
|
+
const renewed = { ...lease, expiresAt };
|
|
63
|
+
return validRuntimeIsolationLease(renewed) ? renewed : null;
|
|
64
|
+
}
|
|
65
|
+
export function withRuntimeIsolationResources(lease, resources) {
|
|
66
|
+
if (lease.state === "released") {
|
|
67
|
+
throw new Error("runtime_lease_transition_invalid: released leases cannot acquire resources.");
|
|
68
|
+
}
|
|
69
|
+
const updated = { ...lease, resources };
|
|
70
|
+
if (!validRuntimeIsolationLease(updated)) {
|
|
71
|
+
throw new Error("runtime_lease_invalid: resources are invalid.");
|
|
72
|
+
}
|
|
73
|
+
return updated;
|
|
74
|
+
}
|
|
75
|
+
export function validRuntimeIsolationLease(value) {
|
|
76
|
+
if (!isRecord(value)
|
|
77
|
+
|| value.schemaVersion !== 1
|
|
78
|
+
|| !isUuid(value.leaseId)
|
|
79
|
+
|| !isUuid(value.dispatchAttemptId)
|
|
80
|
+
|| !isUuid(value.executionLeaseId)
|
|
81
|
+
|| !isUuid(value.fencingToken)
|
|
82
|
+
|| !isUuid(value.runnerId)
|
|
83
|
+
|| !isUuid(value.runnerInstanceId)
|
|
84
|
+
|| !boundedText(value.worktreeKey, 255)
|
|
85
|
+
|| !boundedText(value.worktreePath, 4096)
|
|
86
|
+
|| (value.mode !== "docker" && value.mode !== "host")
|
|
87
|
+
|| !runtimeIsolationLeaseStates.includes(value.state)
|
|
88
|
+
|| !validTimestamp(value.createdAt)
|
|
89
|
+
|| !validTimestamp(value.stateChangedAt)
|
|
90
|
+
|| !validTimestamp(value.expiresAt)
|
|
91
|
+
|| Date.parse(value.stateChangedAt) < Date.parse(value.createdAt)
|
|
92
|
+
|| Date.parse(value.expiresAt) < Date.parse(value.createdAt)
|
|
93
|
+
|| (value.state === "released") !== validTimestamp(value.releasedAt)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (value.releasedAt !== null && Date.parse(value.releasedAt) < Date.parse(value.createdAt))
|
|
97
|
+
return false;
|
|
98
|
+
return validResources(value.resources, value.mode);
|
|
99
|
+
}
|
|
100
|
+
function validResources(value, mode) {
|
|
101
|
+
if (!isRecord(value)
|
|
102
|
+
|| !nonNegativeInteger(value.memoryReservationBytes)
|
|
103
|
+
|| !nonNegativeInteger(value.cpuMillis)
|
|
104
|
+
|| !nonNegativeInteger(value.pids)
|
|
105
|
+
|| !Array.isArray(value.ports) || value.ports.length > 64
|
|
106
|
+
|| !Array.isArray(value.paths) || value.paths.length > 64
|
|
107
|
+
|| !Array.isArray(value.dockerResources) || value.dockerResources.length > 128
|
|
108
|
+
|| !Array.isArray(value.singletonKeys) || value.singletonKeys.length > 64
|
|
109
|
+
|| (value.processSupervisorId !== null && !boundedText(value.processSupervisorId, 255))) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (value.ports.some(port => !validPort(port))
|
|
113
|
+
|| value.paths.some(entry => !isRecord(entry) || !boundedText(entry.path, 4096) || !boundedText(entry.role, 128))
|
|
114
|
+
|| value.singletonKeys.some(key => !boundedText(key, 255))
|
|
115
|
+
|| value.dockerResources.some(resource => !validDockerResource(resource))) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return mode === "docker" || value.dockerResources.length === 0;
|
|
119
|
+
}
|
|
120
|
+
function validPort(value) {
|
|
121
|
+
return isRecord(value)
|
|
122
|
+
&& boundedText(value.name, 128)
|
|
123
|
+
&& (value.protocol === "tcp" || value.protocol === "udp")
|
|
124
|
+
&& (value.hostIp === "127.0.0.1" || value.hostIp === "::1")
|
|
125
|
+
&& validPortNumber(value.hostPort)
|
|
126
|
+
&& validPortNumber(value.targetPort);
|
|
127
|
+
}
|
|
128
|
+
function validDockerResource(value) {
|
|
129
|
+
return isRecord(value)
|
|
130
|
+
&& (value.type === "container" || value.type === "network" || value.type === "volume")
|
|
131
|
+
&& boundedText(value.name, 255)
|
|
132
|
+
&& boundedText(value.role, 128)
|
|
133
|
+
&& (value.id === undefined || value.id === null || boundedText(value.id, 255));
|
|
134
|
+
}
|
|
135
|
+
function validPortNumber(value) {
|
|
136
|
+
return Number.isSafeInteger(value) && Number(value) >= 1 && Number(value) <= 65535;
|
|
137
|
+
}
|
|
138
|
+
function nonNegativeInteger(value) {
|
|
139
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
140
|
+
}
|
|
141
|
+
function validTimestamp(value) {
|
|
142
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
143
|
+
}
|
|
144
|
+
function boundedText(value, maximumLength) {
|
|
145
|
+
return typeof value === "string"
|
|
146
|
+
&& value.length > 0
|
|
147
|
+
&& value.length <= maximumLength
|
|
148
|
+
&& Array.from(value).every((character) => {
|
|
149
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
150
|
+
return codePoint > 31 && codePoint !== 127;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function isUuid(value) {
|
|
154
|
+
return typeof value === "string" && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(value);
|
|
155
|
+
}
|
|
156
|
+
function isRecord(value) {
|
|
157
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
158
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const minimumWorkspaceEstimateBytes: number;
|
|
2
|
+
export declare const architectMinimumWorkspaceEstimateBytes: number;
|
|
3
|
+
export interface RepositoryWorkspaceFootprint {
|
|
4
|
+
readonly sourceBytes: number;
|
|
5
|
+
readonly hasNode: boolean;
|
|
6
|
+
readonly hasDotNet: boolean;
|
|
7
|
+
readonly hasBrowser: boolean;
|
|
8
|
+
readonly serviceCount: number;
|
|
9
|
+
}
|
|
10
|
+
export interface WorkspaceEstimateHints {
|
|
11
|
+
readonly role?: string | null;
|
|
12
|
+
readonly serviceCount?: number;
|
|
13
|
+
readonly browser?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function estimateWorkspaceBytesFromRepo(repoPath: string, hints?: WorkspaceEstimateHints): Promise<number>;
|
|
16
|
+
export declare function calculateWorkspaceBytes(footprint: RepositoryWorkspaceFootprint, role?: string | null): number;
|
|
17
|
+
export declare function measureRepositoryWorkspace(repoPath: string): Promise<RepositoryWorkspaceFootprint>;
|