@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.eeefdb0d19c1
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 +34 -0
- package/dist/bridge/bridge.base.d.ts +2 -1
- package/dist/bridge/bridge.service.d.ts +22 -3
- package/dist/bridge/bridge.service.js +416 -40
- package/dist/bridge/bridge.types.d.ts +59 -1
- package/dist/cleanup/cleanup.service.d.ts +13 -2
- package/dist/cleanup/cleanup.service.js +435 -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 +37 -3
- package/dist/cli/cli.service.d.ts +30 -2
- package/dist/cli/cli.service.js +182 -23
- package/dist/cli/local-config.d.ts +4 -0
- package/dist/cli/local-config.js +41 -1
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +101 -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 +86 -0
- package/dist/runtime/attempt-process-supervisor.js +343 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +44 -0
- package/dist/runtime/host-listener-inspector.js +324 -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 +47 -0
- package/dist/runtime/host-runtime-contract.js +700 -0
- package/dist/runtime/host-runtime-lease.d.ts +133 -0
- package/dist/runtime/host-runtime-lease.js +1460 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
- package/dist/runtime/node-host-runtime-adapter.js +268 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/sandbox/preflight.d.ts +14 -0
- package/dist/sandbox/preflight.js +91 -12
- package/dist/sandbox/process.js +125 -40
- package/dist/sandbox/repository-lock.js +37 -1
- package/dist/sandbox/runtime.d.ts +66 -3
- package/dist/sandbox/runtime.js +670 -105
- package/dist/sandbox/sandbox.types.d.ts +3 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.js +1 -1
- package/dist/sandbox/worker.js +2 -5
- package/dist/sandbox/workspace.service.d.ts +1 -0
- package/dist/sandbox/workspace.service.js +28 -5
- package/dist/session/managed-attempt-tools.d.ts +12 -0
- package/dist/session/managed-attempt-tools.js +350 -0
- package/dist/session/phase-completion.artifacts.js +9 -2
- package/dist/session/phase-completion.d.ts +4 -2
- package/dist/session/phase-completion.helpers.js +27 -3
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +19 -5
- package/dist/session/phase-completion.runtime.d.ts +12 -1
- package/dist/session/phase-completion.runtime.js +127 -7
- package/dist/session/phase-completion.schemas.js +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +106 -2
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
- package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +16 -0
- package/dist/session/provider-cli/provider-cli.process.js +212 -25
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
- package/dist/session/provider-cli/provider-cli.prompts.js +84 -17
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +3 -2
- package/dist/session/provider-cli/provider-cli.service.js +254 -71
- 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 +30 -2
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +23 -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 +436 -41
- package/dist/session/session.types.d.ts +55 -0
- package/dist/session/session.types.js +69 -2
- package/dist/shared/path/path.js +3 -1
- 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 +57 -0
- package/dist/shared/process/host-process-supervisor.js +282 -0
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/redaction/redaction.js +6 -1
- 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 +10 -1
- package/dist/worktree/dependency-warmer.js +94 -5
- package/dist/worktree/index.d.ts +1 -0
- 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 +7 -2
- package/dist/worktree/worktree.service.js +72 -44
- package/dist/worktree/worktree.types.d.ts +3 -1
- package/package.json +3 -2
- package/runtime/windows-job-supervisor.ps1 +356 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,1460 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { StreamingProviderProcessRunner, } from "../session/provider-cli/provider-cli.process.js";
|
|
4
|
+
import { createRuntimeIsolationLease, renewRuntimeIsolationLease, transitionRuntimeIsolationLease, validRuntimeIsolationLease, withRuntimeIsolationResources, } from "./runtime-isolation-lease.js";
|
|
5
|
+
import { hostPortEnvironment, loadHostRuntimeContract, } from "./host-runtime-contract.js";
|
|
6
|
+
import { HostPortBroker, } from "./host-port-broker.js";
|
|
7
|
+
import { NodeHostRuntimeAdapter, } from "./node-host-runtime-adapter.js";
|
|
8
|
+
import { AttemptProcessSupervisorRegistry } from "./attempt-process-supervisor-registry.js";
|
|
9
|
+
import { AttemptNodeRuntimeLauncher } from "./attempt-node-runtime-launcher.js";
|
|
10
|
+
import { HostListenerInspector } from "./host-listener-inspector.js";
|
|
11
|
+
import { PlaywrightHostRuntimeAdapter } from "./playwright-host-runtime-adapter.js";
|
|
12
|
+
import { HostParallelPromotionStore, configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible, } from "./host-runtime-promotion.js";
|
|
13
|
+
import { ExactHostProcessRecovery, } from "../shared/process/host-process-recovery.js";
|
|
14
|
+
import { resetGeneratedOutputs } from "./generated-output-reset.js";
|
|
15
|
+
export class HostRuntimeLeaseManager {
|
|
16
|
+
stateRoot;
|
|
17
|
+
processSupervisors;
|
|
18
|
+
processRunner;
|
|
19
|
+
listenerInspector;
|
|
20
|
+
promotionStore;
|
|
21
|
+
playwrightAdapter;
|
|
22
|
+
processRecovery;
|
|
23
|
+
active = new Map();
|
|
24
|
+
pendingRenewals = new Map();
|
|
25
|
+
admissions = new Map();
|
|
26
|
+
singletonOwners = new Map();
|
|
27
|
+
singletonWaiters = [];
|
|
28
|
+
parallelProjectOwners = new Map();
|
|
29
|
+
parallelProjectWaiters = [];
|
|
30
|
+
degraded = new Map();
|
|
31
|
+
knownPromotions = new Map();
|
|
32
|
+
portBroker;
|
|
33
|
+
nodeAdapter;
|
|
34
|
+
constructor(stateRoot, portBroker = new HostPortBroker(), nodeAdapter, processSupervisors = new AttemptProcessSupervisorRegistry(), processRunner = new StreamingProviderProcessRunner(), listenerInspector = new HostListenerInspector(), promotionStore = new HostParallelPromotionStore(stateRoot), playwrightAdapter = new PlaywrightHostRuntimeAdapter(), processRecovery = new ExactHostProcessRecovery()) {
|
|
35
|
+
this.stateRoot = stateRoot;
|
|
36
|
+
this.processSupervisors = processSupervisors;
|
|
37
|
+
this.processRunner = processRunner;
|
|
38
|
+
this.listenerInspector = listenerInspector;
|
|
39
|
+
this.promotionStore = promotionStore;
|
|
40
|
+
this.playwrightAdapter = playwrightAdapter;
|
|
41
|
+
this.processRecovery = processRecovery;
|
|
42
|
+
this.portBroker = portBroker;
|
|
43
|
+
this.nodeAdapter = nodeAdapter ?? new NodeHostRuntimeAdapter(portBroker);
|
|
44
|
+
}
|
|
45
|
+
async reap(options) {
|
|
46
|
+
await this.refreshKnownPromotions();
|
|
47
|
+
const report = {
|
|
48
|
+
examined: 0,
|
|
49
|
+
reaped: 0,
|
|
50
|
+
processesTerminated: 0,
|
|
51
|
+
reusedPidsProtected: 0,
|
|
52
|
+
degraded: 0,
|
|
53
|
+
};
|
|
54
|
+
for (const ledger of await this.readLedgers()) {
|
|
55
|
+
const lease = ledger.runtimeLease;
|
|
56
|
+
if (lease.runnerId !== options.runnerId)
|
|
57
|
+
continue;
|
|
58
|
+
const active = this.active.get(lease.dispatchAttemptId);
|
|
59
|
+
if (active) {
|
|
60
|
+
if (active.lease.state !== "degraded")
|
|
61
|
+
continue;
|
|
62
|
+
report.examined += 1;
|
|
63
|
+
try {
|
|
64
|
+
const released = await this.release(lease.dispatchAttemptId, lease.fencingToken);
|
|
65
|
+
if (released?.state === "released")
|
|
66
|
+
report.reaped += 1;
|
|
67
|
+
else
|
|
68
|
+
report.degraded += 1;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
report.degraded += 1;
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const staleInstance = lease.runnerInstanceId !== options.runnerInstanceId;
|
|
76
|
+
const expired = Date.parse(lease.expiresAt) <= (options.nowMs ?? Date.now());
|
|
77
|
+
if (!staleInstance && !expired && lease.state !== "released" && lease.state !== "degraded")
|
|
78
|
+
continue;
|
|
79
|
+
report.examined += 1;
|
|
80
|
+
const attemptRoot = this.attemptRoot(lease.dispatchAttemptId);
|
|
81
|
+
let outcomes;
|
|
82
|
+
try {
|
|
83
|
+
outcomes = await this.recoverPersistedProcesses(lease, attemptRoot);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
await this.markLedgerDegraded(ledger);
|
|
87
|
+
report.degraded += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
report.processesTerminated += outcomes.filter(outcome => outcome === "terminated").length;
|
|
91
|
+
report.reusedPidsProtected += outcomes.filter(outcome => outcome === "pid_reused").length;
|
|
92
|
+
if (outcomes.includes("unverifiable")) {
|
|
93
|
+
await this.markLedgerDegraded(ledger);
|
|
94
|
+
report.degraded += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const releasing = transitionHostLeaseForCleanup(lease, "releasing");
|
|
99
|
+
await this.writeLedger(releasing, ledger.repoId);
|
|
100
|
+
await fs.rm(attemptRoot, { recursive: true, force: true });
|
|
101
|
+
await fs.rm(this.ledgerPath(lease.dispatchAttemptId), { force: true });
|
|
102
|
+
this.forgetDegraded(lease.dispatchAttemptId, lease.fencingToken);
|
|
103
|
+
report.reaped += 1;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
await this.markLedgerDegraded(ledger);
|
|
107
|
+
report.degraded += 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return report;
|
|
111
|
+
}
|
|
112
|
+
snapshot() {
|
|
113
|
+
const projects = new Map();
|
|
114
|
+
for (const promotion of this.knownPromotions.values()) {
|
|
115
|
+
const project = capacityProject(projects, promotion.repoId);
|
|
116
|
+
project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_parallel_safe");
|
|
117
|
+
project.runtimeContractFingerprint = promotion.runtimeContractFingerprint;
|
|
118
|
+
project.runtimeContractSource = promotion.runtimeContractSource ?? null;
|
|
119
|
+
project.promotedAt = promotion.observedAt;
|
|
120
|
+
}
|
|
121
|
+
for (const [dispatchAttemptId, admission] of this.admissions) {
|
|
122
|
+
const active = this.active.get(dispatchAttemptId);
|
|
123
|
+
const project = capacityProject(projects, admission.repoId);
|
|
124
|
+
if (active) {
|
|
125
|
+
if (active.lease.state === "degraded") {
|
|
126
|
+
project.degradedAttempts += 1;
|
|
127
|
+
project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_serial_only");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
project.activeAttempts += 1;
|
|
131
|
+
const effectiveConcurrency = effectiveHostConcurrency(active.lease, active.runtimeContract, active.executionClass);
|
|
132
|
+
project.safetyClass = conservativeSafetyClass(project.safetyClass, effectiveConcurrency === "host_parallel_safe"
|
|
133
|
+
? effectiveConcurrency
|
|
134
|
+
: admission.executionClass === "analysis_only"
|
|
135
|
+
? "host_parallel_safe"
|
|
136
|
+
: admission.parallelProjectKey && admission.provisionalPromotionFingerprint
|
|
137
|
+
? "host_parallel_candidate"
|
|
138
|
+
: "host_serial_only");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
project.reservedAttempts += 1;
|
|
143
|
+
project.safetyClass = conservativeSafetyClass(project.safetyClass, admission.executionClass === "analysis_only"
|
|
144
|
+
? "host_parallel_safe"
|
|
145
|
+
: admission.provisionalPromotionFingerprint ? "host_parallel_candidate" : "host_serial_only");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const [dispatchAttemptId, degraded] of this.degraded) {
|
|
149
|
+
if (this.active.has(dispatchAttemptId) || !degraded.repoId)
|
|
150
|
+
continue;
|
|
151
|
+
const project = capacityProject(projects, degraded.repoId);
|
|
152
|
+
project.degradedAttempts += 1;
|
|
153
|
+
project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_serial_only");
|
|
154
|
+
}
|
|
155
|
+
for (const waiter of this.singletonWaiters) {
|
|
156
|
+
const repoId = repoIdFromProjectSingletonKeys(waiter.keys);
|
|
157
|
+
if (!repoId)
|
|
158
|
+
continue;
|
|
159
|
+
const project = capacityProject(projects, repoId);
|
|
160
|
+
project.waitingAttempts += 1;
|
|
161
|
+
project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_serial_only");
|
|
162
|
+
}
|
|
163
|
+
for (const waiter of this.parallelProjectWaiters) {
|
|
164
|
+
const repoId = repoIdFromProjectSingletonKey(waiter.key);
|
|
165
|
+
if (!repoId)
|
|
166
|
+
continue;
|
|
167
|
+
const project = capacityProject(projects, repoId);
|
|
168
|
+
project.waitingAttempts += 1;
|
|
169
|
+
project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_parallel_candidate");
|
|
170
|
+
}
|
|
171
|
+
const activeAttempts = [...this.active.values()]
|
|
172
|
+
.filter(active => active.lease.state !== "degraded").length;
|
|
173
|
+
const degradedAttempts = new Set([
|
|
174
|
+
...this.degraded.keys(),
|
|
175
|
+
...[...this.active.entries()]
|
|
176
|
+
.filter(([, active]) => active.lease.state === "degraded")
|
|
177
|
+
.map(([dispatchAttemptId]) => dispatchAttemptId),
|
|
178
|
+
]).size;
|
|
179
|
+
return {
|
|
180
|
+
activeAttempts,
|
|
181
|
+
reservedAttempts: Math.max(0, this.admissions.size - this.active.size),
|
|
182
|
+
waitingAttempts: this.singletonWaiters.length + this.parallelProjectWaiters.length,
|
|
183
|
+
degradedAttempts,
|
|
184
|
+
exclusiveReservations: this.singletonOwners.size,
|
|
185
|
+
sharedReservations: [...this.parallelProjectOwners.values()]
|
|
186
|
+
.reduce((count, owners) => count + owners.size, 0),
|
|
187
|
+
projects: [...projects.values()]
|
|
188
|
+
.map(project => ({
|
|
189
|
+
...project,
|
|
190
|
+
safetyClass: project.safetyClass ?? "host_serial_only",
|
|
191
|
+
}))
|
|
192
|
+
.sort((left, right) => left.repoId.localeCompare(right.repoId)),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async acquire(dispatch, signal) {
|
|
196
|
+
const sandbox = dispatch.sandbox;
|
|
197
|
+
if (!sandbox || sandbox.executionMode !== "host") {
|
|
198
|
+
throw new Error("host_runtime_lease_invalid: managed host sandbox context is required.");
|
|
199
|
+
}
|
|
200
|
+
uuidHex(sandbox.dispatchAttemptId, "dispatch attempt");
|
|
201
|
+
uuidHex(dispatch.lease.fencingToken, "fencing token");
|
|
202
|
+
const owner = {
|
|
203
|
+
dispatchAttemptId: sandbox.dispatchAttemptId,
|
|
204
|
+
fencingToken: dispatch.lease.fencingToken,
|
|
205
|
+
};
|
|
206
|
+
const existing = this.admissions.get(sandbox.dispatchAttemptId);
|
|
207
|
+
if (existing) {
|
|
208
|
+
if (existing.fencingToken !== owner.fencingToken) {
|
|
209
|
+
throw new Error("host_runtime_singleton_conflict: dispatch attempt already holds admission under a different fence.");
|
|
210
|
+
}
|
|
211
|
+
return existing.keys;
|
|
212
|
+
}
|
|
213
|
+
const executionClass = runtimeExecutionClass(dispatch);
|
|
214
|
+
const promotion = executionClass === "managed_runtime"
|
|
215
|
+
? await this.promotionStore.load(dispatch.repo.id)
|
|
216
|
+
: null;
|
|
217
|
+
if (promotion)
|
|
218
|
+
this.knownPromotions.set(promotion.repoId, promotion);
|
|
219
|
+
const projectKey = executionClass === "analysis_only"
|
|
220
|
+
? projectHostAnalysisKey(dispatch.repo.id)
|
|
221
|
+
: projectHostRuntimeSingletonKey(dispatch.repo.id);
|
|
222
|
+
let keys = promotion || executionClass === "analysis_only" ? [] : [projectKey];
|
|
223
|
+
const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
|
|
224
|
+
? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
|
|
225
|
+
: dispatch.lease.expiresAt;
|
|
226
|
+
try {
|
|
227
|
+
if (promotion || executionClass === "analysis_only") {
|
|
228
|
+
await this.reserveParallelProject(projectKey, owner, expiresAt, signal);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
keys = await this.reserveSingletonKeys(keys, owner, expiresAt, signal);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
this.clearPendingRenewal(owner);
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
const admittedPromotion = promotion
|
|
239
|
+
?? (executionClass === "managed_runtime" && keys.length === 0
|
|
240
|
+
? this.knownPromotions.get(dispatch.repo.id) ?? null
|
|
241
|
+
: null);
|
|
242
|
+
this.admissions.set(sandbox.dispatchAttemptId, {
|
|
243
|
+
repoId: dispatch.repo.id,
|
|
244
|
+
fencingToken: owner.fencingToken,
|
|
245
|
+
executionClass,
|
|
246
|
+
provisionalPromotionFingerprint: admittedPromotion?.runtimeContractFingerprint ?? null,
|
|
247
|
+
keys,
|
|
248
|
+
parallelProjectKey: admittedPromotion || executionClass === "analysis_only" ? projectKey : null,
|
|
249
|
+
});
|
|
250
|
+
return keys;
|
|
251
|
+
}
|
|
252
|
+
async reserveSingletonKeys(keys, owner, expiresAt, signal) {
|
|
253
|
+
const owned = keys.every(key => sameSingletonOwner(this.singletonOwners.get(key), owner));
|
|
254
|
+
if (owned)
|
|
255
|
+
return keys;
|
|
256
|
+
const staleFence = [...this.singletonOwners.values()].find(existing => existing.dispatchAttemptId === owner.dispatchAttemptId
|
|
257
|
+
&& existing.fencingToken !== owner.fencingToken);
|
|
258
|
+
if (staleFence) {
|
|
259
|
+
throw new Error("host_runtime_singleton_conflict: dispatch attempt already holds the singleton under a different fence.");
|
|
260
|
+
}
|
|
261
|
+
if (signal?.aborted)
|
|
262
|
+
throw cancellationError(signal);
|
|
263
|
+
throwIfAdmissionExpired(expiresAt);
|
|
264
|
+
if (this.singletonKeysAvailable(keys)) {
|
|
265
|
+
this.assignSingletonKeys(keys, owner);
|
|
266
|
+
return keys;
|
|
267
|
+
}
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
const waiter = {
|
|
270
|
+
owner,
|
|
271
|
+
keys,
|
|
272
|
+
resolve,
|
|
273
|
+
reject,
|
|
274
|
+
signal,
|
|
275
|
+
expiresAt,
|
|
276
|
+
deadlineTimer: null,
|
|
277
|
+
abort: () => {
|
|
278
|
+
const index = this.singletonWaiters.indexOf(waiter);
|
|
279
|
+
if (index >= 0)
|
|
280
|
+
this.singletonWaiters.splice(index, 1);
|
|
281
|
+
clearAdmissionDeadline(waiter);
|
|
282
|
+
this.clearPendingRenewal(owner);
|
|
283
|
+
reject(cancellationError(signal));
|
|
284
|
+
this.drainAdmissionWaiters();
|
|
285
|
+
},
|
|
286
|
+
expire: () => {
|
|
287
|
+
const index = this.singletonWaiters.indexOf(waiter);
|
|
288
|
+
if (index < 0)
|
|
289
|
+
return;
|
|
290
|
+
this.singletonWaiters.splice(index, 1);
|
|
291
|
+
clearAdmissionDeadline(waiter);
|
|
292
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
293
|
+
this.clearPendingRenewal(owner);
|
|
294
|
+
reject(new HostRuntimeAdmissionExpiredError());
|
|
295
|
+
this.drainAdmissionWaiters();
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
this.singletonWaiters.push(waiter);
|
|
299
|
+
signal?.addEventListener("abort", waiter.abort, { once: true });
|
|
300
|
+
waiter.deadlineTimer = scheduleAdmissionDeadline(waiter.expiresAt, waiter.expire);
|
|
301
|
+
this.drainAdmissionWaiters();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async reserveParallelProject(key, owner, expiresAt, signal) {
|
|
305
|
+
if (this.parallelProjectOwned(key, owner))
|
|
306
|
+
return;
|
|
307
|
+
const staleFence = [...(this.parallelProjectOwners.get(key)?.values() ?? [])]
|
|
308
|
+
.find(existing => existing.dispatchAttemptId === owner.dispatchAttemptId
|
|
309
|
+
&& existing.fencingToken !== owner.fencingToken);
|
|
310
|
+
if (staleFence) {
|
|
311
|
+
throw new Error("host_runtime_parallel_conflict: dispatch attempt already holds shared admission under a different fence.");
|
|
312
|
+
}
|
|
313
|
+
if (signal?.aborted)
|
|
314
|
+
throw cancellationError(signal);
|
|
315
|
+
throwIfAdmissionExpired(expiresAt);
|
|
316
|
+
if (this.parallelProjectAvailable(key)) {
|
|
317
|
+
this.assignParallelProject(key, owner);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
await new Promise((resolve, reject) => {
|
|
321
|
+
const waiter = {
|
|
322
|
+
owner,
|
|
323
|
+
key,
|
|
324
|
+
resolve,
|
|
325
|
+
reject,
|
|
326
|
+
signal,
|
|
327
|
+
expiresAt,
|
|
328
|
+
deadlineTimer: null,
|
|
329
|
+
abort: () => {
|
|
330
|
+
const index = this.parallelProjectWaiters.indexOf(waiter);
|
|
331
|
+
if (index >= 0)
|
|
332
|
+
this.parallelProjectWaiters.splice(index, 1);
|
|
333
|
+
clearAdmissionDeadline(waiter);
|
|
334
|
+
this.clearPendingRenewal(owner);
|
|
335
|
+
reject(cancellationError(signal));
|
|
336
|
+
this.drainAdmissionWaiters();
|
|
337
|
+
},
|
|
338
|
+
expire: () => {
|
|
339
|
+
const index = this.parallelProjectWaiters.indexOf(waiter);
|
|
340
|
+
if (index < 0)
|
|
341
|
+
return;
|
|
342
|
+
this.parallelProjectWaiters.splice(index, 1);
|
|
343
|
+
clearAdmissionDeadline(waiter);
|
|
344
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
345
|
+
this.clearPendingRenewal(owner);
|
|
346
|
+
reject(new HostRuntimeAdmissionExpiredError());
|
|
347
|
+
this.drainAdmissionWaiters();
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
this.parallelProjectWaiters.push(waiter);
|
|
351
|
+
signal?.addEventListener("abort", waiter.abort, { once: true });
|
|
352
|
+
waiter.deadlineTimer = scheduleAdmissionDeadline(waiter.expiresAt, waiter.expire);
|
|
353
|
+
this.drainAdmissionWaiters();
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async start(dispatch, worktreePath, signal) {
|
|
357
|
+
const sandbox = dispatch.sandbox;
|
|
358
|
+
if (!sandbox || sandbox.executionMode !== "host") {
|
|
359
|
+
throw new Error("host_runtime_lease_invalid: managed host sandbox context is required.");
|
|
360
|
+
}
|
|
361
|
+
const existing = this.active.get(sandbox.dispatchAttemptId);
|
|
362
|
+
if (existing) {
|
|
363
|
+
if (existing.lease.fencingToken !== dispatch.lease.fencingToken) {
|
|
364
|
+
throw new Error("host_runtime_lease_conflict: an active host lease has a different fence.");
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
lease: existing.lease,
|
|
368
|
+
environment: hostRuntimeEnvironment(dispatch.session.id, existing.lease, existing.runtimeContract, existing.executionClass),
|
|
369
|
+
runtimeContract: existing.runtimeContract,
|
|
370
|
+
executionClass: existing.executionClass,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const admission = this.admissions.get(sandbox.dispatchAttemptId);
|
|
374
|
+
const owner = { dispatchAttemptId: sandbox.dispatchAttemptId, fencingToken: dispatch.lease.fencingToken };
|
|
375
|
+
if (!admission || admission.fencingToken !== owner.fencingToken) {
|
|
376
|
+
throw new Error("host_runtime_singleton_unreserved: managed host admission is required before start.");
|
|
377
|
+
}
|
|
378
|
+
const attemptHex = uuidHex(sandbox.dispatchAttemptId, "dispatch attempt");
|
|
379
|
+
const normalizedWorktreePath = path.resolve(worktreePath);
|
|
380
|
+
const attemptRoot = path.join(this.stateRoot, "host-attempts", attemptHex);
|
|
381
|
+
const temporaryPath = path.join(attemptRoot, "tmp");
|
|
382
|
+
const logPath = path.join(attemptRoot, "logs");
|
|
383
|
+
const cachePath = path.join(attemptRoot, "cache");
|
|
384
|
+
const browserProfilePath = path.join(attemptRoot, "browser-profile");
|
|
385
|
+
const testOutputPath = path.join(attemptRoot, "test-output");
|
|
386
|
+
const processSupervisorMetadataPath = path.join(logPath, "attempt-process-supervisor.json");
|
|
387
|
+
const processSupervisorId = `host-${attemptHex}`;
|
|
388
|
+
const renewedExpiry = this.pendingRenewals.get(sandbox.dispatchAttemptId);
|
|
389
|
+
const expiresAt = renewedExpiry?.fencingToken === dispatch.lease.fencingToken
|
|
390
|
+
? renewedExpiry.expiresAt
|
|
391
|
+
: dispatch.lease.expiresAt;
|
|
392
|
+
let processSupervisor = null;
|
|
393
|
+
try {
|
|
394
|
+
let runtimeContract;
|
|
395
|
+
try {
|
|
396
|
+
runtimeContract = await loadHostRuntimeContract(normalizedWorktreePath);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
if (admission.executionClass === "managed_runtime" && admission.provisionalPromotionFingerprint) {
|
|
400
|
+
await this.invalidatePromotion(dispatch.repo.id).catch(() => undefined);
|
|
401
|
+
}
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
if (runtimeContract.contract.runtime.hostConcurrency === "disabled") {
|
|
405
|
+
throw new Error("host_runtime_disabled: project runtime configuration requires Docker execution.");
|
|
406
|
+
}
|
|
407
|
+
const runtimeContractFingerprint = hostRuntimeContractFingerprint(runtimeContract);
|
|
408
|
+
const promotion = admission.executionClass === "managed_runtime"
|
|
409
|
+
? await this.promotionStore.load(dispatch.repo.id)
|
|
410
|
+
: null;
|
|
411
|
+
const promotionMatches = hostRuntimeParallelEligible(runtimeContract)
|
|
412
|
+
&& promotion?.runtimeContractFingerprint === runtimeContractFingerprint;
|
|
413
|
+
if (admission.executionClass === "managed_runtime" && admission.keys.length === 0 && !promotionMatches) {
|
|
414
|
+
await this.invalidatePromotion(dispatch.repo.id);
|
|
415
|
+
const projectKeys = hostRuntimeSingletonKeys(dispatch);
|
|
416
|
+
if (admission.parallelProjectKey) {
|
|
417
|
+
this.releaseParallelProjectNoDrain(admission.parallelProjectKey, owner);
|
|
418
|
+
admission.parallelProjectKey = null;
|
|
419
|
+
}
|
|
420
|
+
const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
|
|
421
|
+
? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
|
|
422
|
+
: dispatch.lease.expiresAt;
|
|
423
|
+
admission.keys = await this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal);
|
|
424
|
+
while (admission.keys.length === 0) {
|
|
425
|
+
const promoted = this.knownPromotions.get(dispatch.repo.id);
|
|
426
|
+
if (!promoted) {
|
|
427
|
+
throw new Error("host_runtime_parallel_promotion_missing: shared admission requires matching promotion evidence.");
|
|
428
|
+
}
|
|
429
|
+
if (promoted.runtimeContractFingerprint !== runtimeContractFingerprint) {
|
|
430
|
+
this.releaseParallelProjectNoDrain(projectKeys[0], owner);
|
|
431
|
+
await this.invalidatePromotion(dispatch.repo.id);
|
|
432
|
+
admission.keys = await this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
admission.parallelProjectKey = projectKeys[0] ?? null;
|
|
436
|
+
admission.provisionalPromotionFingerprint = promoted.runtimeContractFingerprint;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const declaredSingletonKeys = runtimeContract.contract.runtime.singletonResources
|
|
440
|
+
.map(hostRuntimeDeclaredSingletonKey);
|
|
441
|
+
const missingSingletonKeys = declaredSingletonKeys
|
|
442
|
+
.filter(key => !admission.keys.includes(key));
|
|
443
|
+
if (missingSingletonKeys.length > 0) {
|
|
444
|
+
const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
|
|
445
|
+
? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
|
|
446
|
+
: dispatch.lease.expiresAt;
|
|
447
|
+
const reservedSingletonKeys = await this.reserveSingletonKeys(missingSingletonKeys, owner, expiresAt, signal);
|
|
448
|
+
admission.keys = [...admission.keys, ...reservedSingletonKeys]
|
|
449
|
+
.sort((left, right) => left.localeCompare(right));
|
|
450
|
+
}
|
|
451
|
+
const singletonKeys = admission.keys;
|
|
452
|
+
if (!singletonKeys.every(key => sameSingletonOwner(this.singletonOwners.get(key), owner))) {
|
|
453
|
+
throw new Error("host_runtime_singleton_unreserved: managed host singleton ownership is required before start.");
|
|
454
|
+
}
|
|
455
|
+
if (admission.parallelProjectKey && !this.parallelProjectOwned(admission.parallelProjectKey, owner)) {
|
|
456
|
+
throw new Error("host_runtime_parallel_unreserved: matching shared project admission is required before start.");
|
|
457
|
+
}
|
|
458
|
+
await Promise.all([
|
|
459
|
+
ensurePrivateDirectory(temporaryPath),
|
|
460
|
+
ensurePrivateDirectory(logPath),
|
|
461
|
+
ensurePrivateDirectory(cachePath),
|
|
462
|
+
ensurePrivateDirectory(browserProfilePath),
|
|
463
|
+
ensurePrivateDirectory(testOutputPath),
|
|
464
|
+
]);
|
|
465
|
+
await resetGeneratedOutputs(normalizedWorktreePath, testOutputPath);
|
|
466
|
+
let lease = createRuntimeIsolationLease({
|
|
467
|
+
leaseId: sandbox.executionLeaseId,
|
|
468
|
+
dispatchAttemptId: sandbox.dispatchAttemptId,
|
|
469
|
+
executionLeaseId: sandbox.executionLeaseId,
|
|
470
|
+
fencingToken: dispatch.lease.fencingToken,
|
|
471
|
+
runnerId: dispatch.execution.runnerId ?? "",
|
|
472
|
+
runnerInstanceId: sandbox.runnerInstanceId,
|
|
473
|
+
worktreeKey: dispatch.lease.worktreeKey,
|
|
474
|
+
worktreePath: normalizedWorktreePath,
|
|
475
|
+
mode: "host",
|
|
476
|
+
expiresAt,
|
|
477
|
+
resources: {
|
|
478
|
+
memoryReservationBytes: 0,
|
|
479
|
+
cpuMillis: 0,
|
|
480
|
+
pids: 0,
|
|
481
|
+
ports: [],
|
|
482
|
+
paths: [
|
|
483
|
+
{ path: normalizedWorktreePath, role: "worktree" },
|
|
484
|
+
{ path: temporaryPath, role: "temporary" },
|
|
485
|
+
{ path: logPath, role: "logs" },
|
|
486
|
+
{ path: cachePath, role: "cache" },
|
|
487
|
+
{ path: browserProfilePath, role: "browser_profile" },
|
|
488
|
+
{ path: testOutputPath, role: "test_output" },
|
|
489
|
+
{ path: processSupervisorMetadataPath, role: "process_supervisor_metadata" },
|
|
490
|
+
],
|
|
491
|
+
processSupervisorId,
|
|
492
|
+
dockerResources: [],
|
|
493
|
+
singletonKeys,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
await this.writeLedger(lease);
|
|
497
|
+
lease = transitionRuntimeIsolationLease(lease, "starting");
|
|
498
|
+
await this.writeLedger(lease);
|
|
499
|
+
lease = transitionRuntimeIsolationLease(lease, "active");
|
|
500
|
+
await this.writeLedger(lease);
|
|
501
|
+
processSupervisor = this.processSupervisors.register({
|
|
502
|
+
processSupervisorId,
|
|
503
|
+
dispatchAttemptId: lease.dispatchAttemptId,
|
|
504
|
+
executionLeaseId: lease.executionLeaseId,
|
|
505
|
+
fencingToken: lease.fencingToken,
|
|
506
|
+
logDirectory: logPath,
|
|
507
|
+
}, this.processRunner, { signal });
|
|
508
|
+
this.active.set(sandbox.dispatchAttemptId, {
|
|
509
|
+
lease,
|
|
510
|
+
repoId: dispatch.repo.id,
|
|
511
|
+
sessionId: dispatch.session.id,
|
|
512
|
+
attemptRoot,
|
|
513
|
+
runtimeContract,
|
|
514
|
+
runtimeContractFingerprint,
|
|
515
|
+
executionClass: admission.executionClass,
|
|
516
|
+
parallelPromotionEligible: hostRuntimeParallelEligible(runtimeContract),
|
|
517
|
+
processSupervisor,
|
|
518
|
+
adapterCleanups: [],
|
|
519
|
+
pendingAdapterLaunches: new Map(),
|
|
520
|
+
releasing: false,
|
|
521
|
+
});
|
|
522
|
+
this.pendingRenewals.delete(sandbox.dispatchAttemptId);
|
|
523
|
+
return {
|
|
524
|
+
lease,
|
|
525
|
+
environment: hostRuntimeEnvironment(dispatch.session.id, lease, runtimeContract, admission.executionClass),
|
|
526
|
+
runtimeContract,
|
|
527
|
+
executionClass: admission.executionClass,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
this.pendingRenewals.delete(sandbox.dispatchAttemptId);
|
|
532
|
+
if (processSupervisor) {
|
|
533
|
+
await processSupervisor.stopAll(dispatch.lease.fencingToken, "Host runtime start rolled back.").catch(() => undefined);
|
|
534
|
+
this.processSupervisors.unregister(sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
|
|
535
|
+
}
|
|
536
|
+
await fs.rm(attemptRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
537
|
+
await fs.rm(this.ledgerPath(sandbox.dispatchAttemptId), { force: true }).catch(() => undefined);
|
|
538
|
+
this.releaseSingletonKeys(sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
|
|
539
|
+
this.releaseParallelProjects(sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
|
|
540
|
+
this.deleteAdmission(sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async updateLease(dispatchAttemptId, fencingToken, expiresAt) {
|
|
545
|
+
try {
|
|
546
|
+
uuidHex(dispatchAttemptId, "dispatch attempt");
|
|
547
|
+
uuidHex(fencingToken, "fencing token");
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const active = this.active.get(dispatchAttemptId);
|
|
553
|
+
if (!active) {
|
|
554
|
+
if (Number.isFinite(Date.parse(expiresAt))) {
|
|
555
|
+
const pending = this.pendingRenewals.get(dispatchAttemptId);
|
|
556
|
+
if (!pending
|
|
557
|
+
|| pending.fencingToken !== fencingToken
|
|
558
|
+
|| Date.parse(expiresAt) > Date.parse(pending.expiresAt)) {
|
|
559
|
+
this.pendingRenewals.set(dispatchAttemptId, { fencingToken, expiresAt });
|
|
560
|
+
}
|
|
561
|
+
this.renewAdmissionWaiterDeadline(dispatchAttemptId, fencingToken, expiresAt);
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const renewed = renewRuntimeIsolationLease(active.lease, fencingToken, expiresAt);
|
|
566
|
+
if (!renewed)
|
|
567
|
+
return;
|
|
568
|
+
active.lease = renewed;
|
|
569
|
+
await this.writeLedger(renewed);
|
|
570
|
+
}
|
|
571
|
+
async startConfiguredProject(dispatchAttemptId, fencingToken, signal) {
|
|
572
|
+
const active = this.active.get(dispatchAttemptId);
|
|
573
|
+
if (!active || active.lease.fencingToken !== fencingToken || active.lease.state !== "active" || active.releasing) {
|
|
574
|
+
throw new Error("host_runtime_project_unavailable: an active matching host runtime lease is required.");
|
|
575
|
+
}
|
|
576
|
+
if (active.executionClass === "analysis_only") {
|
|
577
|
+
return {
|
|
578
|
+
lease: active.lease,
|
|
579
|
+
environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass),
|
|
580
|
+
runtimeContract: active.runtimeContract,
|
|
581
|
+
executionClass: active.executionClass,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const configuredPorts = active.runtimeContract.contract.runtime.ports.filter((port) => port.launch !== null);
|
|
585
|
+
if (configuredPorts.length === 0) {
|
|
586
|
+
return {
|
|
587
|
+
lease: active.lease,
|
|
588
|
+
environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass),
|
|
589
|
+
runtimeContract: active.runtimeContract,
|
|
590
|
+
executionClass: active.executionClass,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const owner = hostPortOwner(active.lease);
|
|
594
|
+
const logDirectory = active.lease.resources.paths.find(entry => entry.role === "logs")?.path;
|
|
595
|
+
if (!logDirectory)
|
|
596
|
+
throw new Error("host_runtime_project_invalid: attempt log directory is unavailable.");
|
|
597
|
+
const launcher = new AttemptNodeRuntimeLauncher(active.processSupervisor, owner, logDirectory, this.listenerInspector);
|
|
598
|
+
for (const configuredPort of configuredPorts) {
|
|
599
|
+
const workingDirectory = await containedWorkingDirectory(active.lease.worktreePath, configuredPort.launch.workingDirectory);
|
|
600
|
+
await this.launchNodeRuntime(dispatchAttemptId, fencingToken, configuredPort.name, {
|
|
601
|
+
command: configuredPort.launch.command,
|
|
602
|
+
args: configuredPort.launch.args,
|
|
603
|
+
cwd: workingDirectory,
|
|
604
|
+
environment: {
|
|
605
|
+
...configuredPort.launch.environment,
|
|
606
|
+
...hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass),
|
|
607
|
+
},
|
|
608
|
+
}, launcher, signal);
|
|
609
|
+
}
|
|
610
|
+
const promotedLease = await this.promoteVerifiedRuntime(active, active.lease);
|
|
611
|
+
const runtimeEnvironment = hostRuntimeEnvironment(active.sessionId, promotedLease, active.runtimeContract, active.executionClass);
|
|
612
|
+
const playwright = active.runtimeContract.contract.runtime.playwright;
|
|
613
|
+
if (!playwright) {
|
|
614
|
+
return {
|
|
615
|
+
lease: promotedLease,
|
|
616
|
+
environment: runtimeEnvironment,
|
|
617
|
+
runtimeContract: active.runtimeContract,
|
|
618
|
+
executionClass: active.executionClass,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const playwrightPort = promotedLease.resources.ports.find(port => port.name === playwright.baseUrlPort);
|
|
622
|
+
const playwrightPortConfig = active.runtimeContract.contract.runtime.ports
|
|
623
|
+
.find(port => port.name === playwright.baseUrlPort);
|
|
624
|
+
if (!playwrightPort || !playwrightPortConfig?.baseUrlEnv) {
|
|
625
|
+
throw new Error("host_runtime_playwright_unavailable: the configured verified base URL is unavailable.");
|
|
626
|
+
}
|
|
627
|
+
if (playwrightPort.hostIp !== "127.0.0.1") {
|
|
628
|
+
throw new Error("host_runtime_playwright_unavailable: Playwright requires an IPv4 loopback listener.");
|
|
629
|
+
}
|
|
630
|
+
const baseUrl = hostPortEnvironment(playwrightPortConfig, playwrightPort.hostIp, playwrightPort.hostPort)[playwrightPortConfig.baseUrlEnv];
|
|
631
|
+
if (!baseUrl)
|
|
632
|
+
throw new Error("host_runtime_playwright_unavailable: the configured base URL could not be rendered.");
|
|
633
|
+
const pathFor = (role) => promotedLease.resources.paths.find(entry => entry.role === role)?.path ?? "";
|
|
634
|
+
const preparedPlaywright = await this.playwrightAdapter.prepare(playwright, {
|
|
635
|
+
worktreePath: promotedLease.worktreePath,
|
|
636
|
+
temporaryDirectory: pathFor("temporary"),
|
|
637
|
+
testOutputDirectory: pathFor("test_output"),
|
|
638
|
+
browserProfileDirectory: pathFor("browser_profile"),
|
|
639
|
+
baseUrl,
|
|
640
|
+
});
|
|
641
|
+
return {
|
|
642
|
+
lease: promotedLease,
|
|
643
|
+
environment: { ...runtimeEnvironment, ...preparedPlaywright.environment },
|
|
644
|
+
runtimeContract: active.runtimeContract,
|
|
645
|
+
executionClass: active.executionClass,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
providerProcessRunner(dispatchAttemptId, fencingToken) {
|
|
649
|
+
const active = this.active.get(dispatchAttemptId);
|
|
650
|
+
if (!active || active.lease.fencingToken !== fencingToken || active.releasing)
|
|
651
|
+
return null;
|
|
652
|
+
return new AttemptSupervisedProviderProcessRunner(active.processSupervisor, hostPortOwner(active.lease), this.listenerInspector, active.executionClass === "analysis_only");
|
|
653
|
+
}
|
|
654
|
+
async promoteVerifiedRuntime(active, runtimeLease) {
|
|
655
|
+
if (!active.parallelPromotionEligible)
|
|
656
|
+
return runtimeLease;
|
|
657
|
+
const verifiedPortNames = runtimeLease.resources.ports.map(port => port.name).sort((left, right) => left.localeCompare(right));
|
|
658
|
+
const requiredPortNames = configuredLaunchPortNames(active.runtimeContract);
|
|
659
|
+
if (requiredPortNames.length !== verifiedPortNames.length
|
|
660
|
+
|| requiredPortNames.some((name, index) => name !== verifiedPortNames[index])) {
|
|
661
|
+
return runtimeLease;
|
|
662
|
+
}
|
|
663
|
+
const projectKey = projectHostRuntimeSingletonKey(active.repoId);
|
|
664
|
+
const promotedLease = withRuntimeIsolationResources(runtimeLease, {
|
|
665
|
+
...runtimeLease.resources,
|
|
666
|
+
singletonKeys: runtimeLease.resources.singletonKeys.filter(key => key !== projectKey),
|
|
667
|
+
});
|
|
668
|
+
await this.writeLedger(promotedLease);
|
|
669
|
+
active.lease = promotedLease;
|
|
670
|
+
try {
|
|
671
|
+
const promotion = await this.promotionStore.observe(active.repoId, active.runtimeContract, runtimeLease.dispatchAttemptId, verifiedPortNames);
|
|
672
|
+
this.knownPromotions.set(promotion.repoId, promotion);
|
|
673
|
+
this.convertSingletonToParallelProject(projectKey, {
|
|
674
|
+
dispatchAttemptId: runtimeLease.dispatchAttemptId,
|
|
675
|
+
fencingToken: runtimeLease.fencingToken,
|
|
676
|
+
}, promotion);
|
|
677
|
+
const admission = this.admissions.get(runtimeLease.dispatchAttemptId);
|
|
678
|
+
if (admission?.fencingToken === runtimeLease.fencingToken) {
|
|
679
|
+
this.admissions.set(runtimeLease.dispatchAttemptId, {
|
|
680
|
+
...admission,
|
|
681
|
+
keys: promotedLease.resources.singletonKeys,
|
|
682
|
+
provisionalPromotionFingerprint: promotion.runtimeContractFingerprint,
|
|
683
|
+
parallelProjectKey: projectKey,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return promotedLease;
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
await this.invalidatePromotion(active.repoId).catch(() => undefined);
|
|
690
|
+
active.lease = runtimeLease;
|
|
691
|
+
await this.writeLedger(runtimeLease);
|
|
692
|
+
return runtimeLease;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async launchNodeRuntime(dispatchAttemptId, fencingToken, portName, command, launcher, signal) {
|
|
696
|
+
const active = this.active.get(dispatchAttemptId);
|
|
697
|
+
if (!active || active.lease.fencingToken !== fencingToken || active.lease.state !== "active" || active.releasing) {
|
|
698
|
+
throw new Error("host_runtime_adapter_unavailable: an active matching host runtime lease is required.");
|
|
699
|
+
}
|
|
700
|
+
const portConfig = active.runtimeContract.contract.runtime.ports.find(port => port.name === portName);
|
|
701
|
+
if (!portConfig)
|
|
702
|
+
throw new Error(`host_runtime_adapter_invalid: configured port '${portName}' was not found.`);
|
|
703
|
+
if (active.lease.resources.ports.some(port => port.name === portName) || active.pendingAdapterLaunches.has(portName)) {
|
|
704
|
+
throw new Error(`host_runtime_adapter_conflict: configured port '${portName}' is already active.`);
|
|
705
|
+
}
|
|
706
|
+
const controller = linkedAbortController(signal);
|
|
707
|
+
let retainedByActiveRuntime = false;
|
|
708
|
+
const operation = (async () => {
|
|
709
|
+
const owner = hostPortOwner(active.lease);
|
|
710
|
+
const launched = await this.nodeAdapter.launch(owner, portConfig, command, launcher, controller.signal);
|
|
711
|
+
let cleaned = false;
|
|
712
|
+
const cleanup = async () => {
|
|
713
|
+
if (cleaned)
|
|
714
|
+
return;
|
|
715
|
+
await launcher.stop(launched.value);
|
|
716
|
+
await this.portBroker.release(launched.reservation.reservationId, owner);
|
|
717
|
+
cleaned = true;
|
|
718
|
+
unlinkAbortController(controller, signal);
|
|
719
|
+
};
|
|
720
|
+
active.adapterCleanups.push(cleanup);
|
|
721
|
+
try {
|
|
722
|
+
if (active.releasing)
|
|
723
|
+
throw new Error("host_runtime_adapter_cancelled: host runtime release started during launch.");
|
|
724
|
+
const runtimeLease = withRuntimeIsolationResources(active.lease, {
|
|
725
|
+
...active.lease.resources,
|
|
726
|
+
ports: [...active.lease.resources.ports, {
|
|
727
|
+
name: launched.reservation.name,
|
|
728
|
+
protocol: launched.reservation.protocol,
|
|
729
|
+
hostIp: launched.reservation.hostIp,
|
|
730
|
+
hostPort: launched.reservation.hostPort,
|
|
731
|
+
targetPort: launched.reservation.targetPort,
|
|
732
|
+
}],
|
|
733
|
+
});
|
|
734
|
+
await this.writeLedger(runtimeLease);
|
|
735
|
+
active.lease = runtimeLease;
|
|
736
|
+
retainedByActiveRuntime = true;
|
|
737
|
+
return { ...launched, runtimeLease };
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
try {
|
|
741
|
+
await cleanup();
|
|
742
|
+
removeCleanup(active.adapterCleanups, cleanup);
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
// Preserve the cleanup and broker claim for the fenced release retry.
|
|
746
|
+
}
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
})();
|
|
750
|
+
active.pendingAdapterLaunches.set(portName, { controller, promise: operation });
|
|
751
|
+
try {
|
|
752
|
+
return await operation;
|
|
753
|
+
}
|
|
754
|
+
finally {
|
|
755
|
+
active.pendingAdapterLaunches.delete(portName);
|
|
756
|
+
if (!retainedByActiveRuntime)
|
|
757
|
+
unlinkAbortController(controller, signal);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async release(dispatchAttemptId, fencingToken) {
|
|
761
|
+
const active = this.active.get(dispatchAttemptId);
|
|
762
|
+
this.pendingRenewals.delete(dispatchAttemptId);
|
|
763
|
+
if (!active) {
|
|
764
|
+
this.cancelAdmissionWaiters(dispatchAttemptId, fencingToken, "Managed host admission was released before acquisition.");
|
|
765
|
+
this.releaseSingletonKeys(dispatchAttemptId, fencingToken);
|
|
766
|
+
this.releaseParallelProjects(dispatchAttemptId, fencingToken);
|
|
767
|
+
this.deleteAdmission(dispatchAttemptId, fencingToken);
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
if (active.lease.fencingToken !== fencingToken)
|
|
771
|
+
return null;
|
|
772
|
+
active.releasing = true;
|
|
773
|
+
for (const pending of active.pendingAdapterLaunches.values()) {
|
|
774
|
+
pending.controller.abort("Host runtime release cancelled the pending adapter launch.");
|
|
775
|
+
}
|
|
776
|
+
await Promise.allSettled([...active.pendingAdapterLaunches.values()].map(pending => pending.promise));
|
|
777
|
+
let releasing = transitionRuntimeIsolationLease(active.lease, "releasing");
|
|
778
|
+
active.lease = releasing;
|
|
779
|
+
await this.writeLedger(releasing);
|
|
780
|
+
try {
|
|
781
|
+
await active.processSupervisor.stopAll(fencingToken, "Host runtime lease released.");
|
|
782
|
+
await releaseAdapterResources(active.adapterCleanups);
|
|
783
|
+
await fs.rm(active.attemptRoot, { recursive: true, force: true });
|
|
784
|
+
releasing = transitionRuntimeIsolationLease(releasing, "released");
|
|
785
|
+
active.lease = releasing;
|
|
786
|
+
await this.writeLedger(releasing);
|
|
787
|
+
await fs.rm(this.ledgerPath(dispatchAttemptId), { force: true });
|
|
788
|
+
this.active.delete(dispatchAttemptId);
|
|
789
|
+
this.processSupervisors.unregister(dispatchAttemptId, fencingToken);
|
|
790
|
+
this.releaseSingletonKeys(dispatchAttemptId, fencingToken);
|
|
791
|
+
this.releaseParallelProjects(dispatchAttemptId, fencingToken);
|
|
792
|
+
this.deleteAdmission(dispatchAttemptId, fencingToken);
|
|
793
|
+
return releasing;
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
const degraded = transitionRuntimeIsolationLease(releasing, "degraded");
|
|
797
|
+
active.lease = degraded;
|
|
798
|
+
await this.writeLedger(degraded).catch(() => undefined);
|
|
799
|
+
throw new HostRuntimeLeaseCleanupError(degraded, error);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async readLedgers() {
|
|
803
|
+
const root = path.join(this.stateRoot, "runtime-ledger", "host");
|
|
804
|
+
const entries = await fs.readdir(root, { withFileTypes: true }).catch(error => {
|
|
805
|
+
if (error.code === "ENOENT")
|
|
806
|
+
return [];
|
|
807
|
+
throw error;
|
|
808
|
+
});
|
|
809
|
+
const ledgers = [];
|
|
810
|
+
for (const entry of entries) {
|
|
811
|
+
if (!entry.isFile() || !/^[0-9a-f]{32}\.json$/u.test(entry.name))
|
|
812
|
+
continue;
|
|
813
|
+
try {
|
|
814
|
+
const value = JSON.parse(await fs.readFile(path.join(root, entry.name), "utf8"));
|
|
815
|
+
if (!validHostRuntimeLedger(value))
|
|
816
|
+
continue;
|
|
817
|
+
const expectedName = `${uuidHex(value.runtimeLease.dispatchAttemptId, "dispatch attempt")}.json`;
|
|
818
|
+
if (entry.name !== expectedName)
|
|
819
|
+
continue;
|
|
820
|
+
ledgers.push(value);
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
// An invalid ledger has no trustworthy ownership identity and is never reaped.
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return ledgers;
|
|
827
|
+
}
|
|
828
|
+
async recoverPersistedProcesses(lease, attemptRoot) {
|
|
829
|
+
const logsRoot = path.join(attemptRoot, "logs");
|
|
830
|
+
const expectedAttemptRoot = path.resolve(this.attemptRoot(lease.dispatchAttemptId));
|
|
831
|
+
if (path.resolve(attemptRoot) !== expectedAttemptRoot) {
|
|
832
|
+
throw new Error("host_runtime_reaper_invalid: attempt path is outside the fenced attempt root.");
|
|
833
|
+
}
|
|
834
|
+
const entries = await fs.readdir(logsRoot, { withFileTypes: true }).catch(error => {
|
|
835
|
+
if (error.code === "ENOENT")
|
|
836
|
+
return [];
|
|
837
|
+
throw error;
|
|
838
|
+
});
|
|
839
|
+
const outcomes = [];
|
|
840
|
+
for (const entry of entries) {
|
|
841
|
+
if (!entry.isFile() || !/^process-supervisor\.[a-z][a-z0-9_-]{0,31}\.\d+\.json$/u.test(entry.name))
|
|
842
|
+
continue;
|
|
843
|
+
const metadataPath = path.join(logsRoot, entry.name);
|
|
844
|
+
const value = JSON.parse(await fs.readFile(metadataPath, "utf8"));
|
|
845
|
+
if (!validHostProcessSupervisorMetadata(value, lease)) {
|
|
846
|
+
throw new Error("host_runtime_reaper_invalid: process ownership metadata did not match the runtime lease.");
|
|
847
|
+
}
|
|
848
|
+
outcomes.push(await this.processRecovery.recover(value));
|
|
849
|
+
}
|
|
850
|
+
return outcomes;
|
|
851
|
+
}
|
|
852
|
+
async markLedgerDegraded(ledger) {
|
|
853
|
+
const lease = ledger.runtimeLease;
|
|
854
|
+
if (lease.state === "released")
|
|
855
|
+
return;
|
|
856
|
+
const degraded = transitionHostLeaseForCleanup(lease, "degraded");
|
|
857
|
+
const repoId = ledger.repoId ?? repoIdFromProjectSingletonKeys(lease.resources.singletonKeys);
|
|
858
|
+
this.rememberDegraded(degraded, repoId);
|
|
859
|
+
await this.writeLedger(degraded, repoId ?? undefined).catch(() => undefined);
|
|
860
|
+
}
|
|
861
|
+
rememberDegraded(lease, repoId) {
|
|
862
|
+
this.degraded.set(lease.dispatchAttemptId, { lease, repoId });
|
|
863
|
+
if (!repoId)
|
|
864
|
+
return;
|
|
865
|
+
const owner = { dispatchAttemptId: lease.dispatchAttemptId, fencingToken: lease.fencingToken };
|
|
866
|
+
const projectKey = projectHostRuntimeSingletonKey(repoId);
|
|
867
|
+
const existing = this.singletonOwners.get(projectKey);
|
|
868
|
+
if (!existing || sameSingletonOwner(existing, owner))
|
|
869
|
+
this.singletonOwners.set(projectKey, owner);
|
|
870
|
+
}
|
|
871
|
+
forgetDegraded(dispatchAttemptId, fencingToken) {
|
|
872
|
+
if (this.degraded.get(dispatchAttemptId)?.lease.fencingToken !== fencingToken)
|
|
873
|
+
return;
|
|
874
|
+
this.degraded.delete(dispatchAttemptId);
|
|
875
|
+
this.releaseSingletonKeys(dispatchAttemptId, fencingToken);
|
|
876
|
+
this.releaseParallelProjects(dispatchAttemptId, fencingToken);
|
|
877
|
+
}
|
|
878
|
+
attemptRoot(dispatchAttemptId) {
|
|
879
|
+
return path.join(this.stateRoot, "host-attempts", uuidHex(dispatchAttemptId, "dispatch attempt"));
|
|
880
|
+
}
|
|
881
|
+
async writeLedger(runtimeLease, explicitRepoId) {
|
|
882
|
+
const root = path.join(this.stateRoot, "runtime-ledger", "host");
|
|
883
|
+
await ensurePrivateDirectory(root);
|
|
884
|
+
const target = this.ledgerPath(runtimeLease.dispatchAttemptId);
|
|
885
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
886
|
+
try {
|
|
887
|
+
const handle = await fs.open(temporary, "wx", 0o600);
|
|
888
|
+
try {
|
|
889
|
+
const repoId = explicitRepoId
|
|
890
|
+
?? this.active.get(runtimeLease.dispatchAttemptId)?.repoId
|
|
891
|
+
?? this.admissions.get(runtimeLease.dispatchAttemptId)?.repoId
|
|
892
|
+
?? this.degraded.get(runtimeLease.dispatchAttemptId)?.repoId
|
|
893
|
+
?? undefined;
|
|
894
|
+
const ledger = {
|
|
895
|
+
schemaVersion: 1,
|
|
896
|
+
...(repoId ? { repoId } : {}),
|
|
897
|
+
runtimeLease,
|
|
898
|
+
};
|
|
899
|
+
await handle.writeFile(`${JSON.stringify(ledger)}\n`);
|
|
900
|
+
await handle.sync();
|
|
901
|
+
}
|
|
902
|
+
finally {
|
|
903
|
+
await handle.close();
|
|
904
|
+
}
|
|
905
|
+
await fs.rename(temporary, target);
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
ledgerPath(dispatchAttemptId) {
|
|
913
|
+
const attemptHex = uuidHex(dispatchAttemptId, "dispatch attempt");
|
|
914
|
+
return path.join(this.stateRoot, "runtime-ledger", "host", `${attemptHex}.json`);
|
|
915
|
+
}
|
|
916
|
+
singletonKeysAvailable(keys) {
|
|
917
|
+
return keys.every(key => !this.singletonOwners.has(key) && !this.parallelProjectOwners.has(key));
|
|
918
|
+
}
|
|
919
|
+
assignSingletonKeys(keys, owner) {
|
|
920
|
+
for (const key of keys)
|
|
921
|
+
this.singletonOwners.set(key, owner);
|
|
922
|
+
}
|
|
923
|
+
releaseSingletonKeys(dispatchAttemptId, fencingToken) {
|
|
924
|
+
for (const [key, owner] of this.singletonOwners) {
|
|
925
|
+
if (owner.dispatchAttemptId === dispatchAttemptId && owner.fencingToken === fencingToken) {
|
|
926
|
+
this.singletonOwners.delete(key);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
this.drainAdmissionWaiters();
|
|
930
|
+
}
|
|
931
|
+
convertSingletonToParallelProject(key, owner, promotion) {
|
|
932
|
+
if (!sameSingletonOwner(this.singletonOwners.get(key), owner)) {
|
|
933
|
+
throw new Error("host_runtime_parallel_promotion_stale: matching exclusive project admission is required.");
|
|
934
|
+
}
|
|
935
|
+
this.assignParallelProject(key, owner);
|
|
936
|
+
this.singletonOwners.delete(key);
|
|
937
|
+
for (let index = 0; index < this.singletonWaiters.length;) {
|
|
938
|
+
const waiter = this.singletonWaiters[index];
|
|
939
|
+
if (waiter.keys.length !== 1 || waiter.keys[0] !== key) {
|
|
940
|
+
index += 1;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
this.singletonWaiters.splice(index, 1);
|
|
944
|
+
clearAdmissionDeadline(waiter);
|
|
945
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
946
|
+
this.assignParallelProject(key, waiter.owner);
|
|
947
|
+
this.knownPromotions.set(promotion.repoId, promotion);
|
|
948
|
+
waiter.resolve([]);
|
|
949
|
+
}
|
|
950
|
+
this.drainAdmissionWaiters();
|
|
951
|
+
}
|
|
952
|
+
parallelProjectOwned(key, owner) {
|
|
953
|
+
return sameSingletonOwner(this.parallelProjectOwners.get(key)?.get(owner.dispatchAttemptId), owner);
|
|
954
|
+
}
|
|
955
|
+
parallelProjectAvailable(key) {
|
|
956
|
+
return !this.singletonOwners.has(key)
|
|
957
|
+
&& !this.singletonWaiters.some(waiter => waiter.keys.includes(key));
|
|
958
|
+
}
|
|
959
|
+
assignParallelProject(key, owner) {
|
|
960
|
+
const owners = this.parallelProjectOwners.get(key) ?? new Map();
|
|
961
|
+
owners.set(owner.dispatchAttemptId, owner);
|
|
962
|
+
this.parallelProjectOwners.set(key, owners);
|
|
963
|
+
}
|
|
964
|
+
releaseParallelProjectNoDrain(key, owner) {
|
|
965
|
+
const owners = this.parallelProjectOwners.get(key);
|
|
966
|
+
if (!sameSingletonOwner(owners?.get(owner.dispatchAttemptId), owner))
|
|
967
|
+
return;
|
|
968
|
+
owners.delete(owner.dispatchAttemptId);
|
|
969
|
+
if (owners.size === 0)
|
|
970
|
+
this.parallelProjectOwners.delete(key);
|
|
971
|
+
}
|
|
972
|
+
releaseParallelProjects(dispatchAttemptId, fencingToken) {
|
|
973
|
+
const owner = { dispatchAttemptId, fencingToken };
|
|
974
|
+
for (const key of [...this.parallelProjectOwners.keys()]) {
|
|
975
|
+
this.releaseParallelProjectNoDrain(key, owner);
|
|
976
|
+
}
|
|
977
|
+
this.drainAdmissionWaiters();
|
|
978
|
+
}
|
|
979
|
+
deleteAdmission(dispatchAttemptId, fencingToken) {
|
|
980
|
+
if (this.admissions.get(dispatchAttemptId)?.fencingToken === fencingToken) {
|
|
981
|
+
this.admissions.delete(dispatchAttemptId);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
renewAdmissionWaiterDeadline(dispatchAttemptId, fencingToken, expiresAt) {
|
|
985
|
+
for (const waiter of [...this.singletonWaiters, ...this.parallelProjectWaiters]) {
|
|
986
|
+
if (!sameSingletonOwner(waiter.owner, { dispatchAttemptId, fencingToken })
|
|
987
|
+
|| Date.parse(expiresAt) <= Date.parse(waiter.expiresAt))
|
|
988
|
+
continue;
|
|
989
|
+
waiter.expiresAt = expiresAt;
|
|
990
|
+
clearAdmissionDeadline(waiter);
|
|
991
|
+
waiter.deadlineTimer = scheduleAdmissionDeadline(waiter.expiresAt, waiter.expire);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
clearPendingRenewal(owner) {
|
|
995
|
+
if (this.pendingRenewals.get(owner.dispatchAttemptId)?.fencingToken === owner.fencingToken) {
|
|
996
|
+
this.pendingRenewals.delete(owner.dispatchAttemptId);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
cancelAdmissionWaiters(dispatchAttemptId, fencingToken, reason) {
|
|
1000
|
+
const owner = { dispatchAttemptId, fencingToken };
|
|
1001
|
+
for (const waiters of [this.singletonWaiters, this.parallelProjectWaiters]) {
|
|
1002
|
+
for (let index = waiters.length - 1; index >= 0; index -= 1) {
|
|
1003
|
+
const waiter = waiters[index];
|
|
1004
|
+
if (!sameSingletonOwner(waiter.owner, owner))
|
|
1005
|
+
continue;
|
|
1006
|
+
waiters.splice(index, 1);
|
|
1007
|
+
clearAdmissionDeadline(waiter);
|
|
1008
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
1009
|
+
waiter.reject(new Error(`host_runtime_admission_cancelled: ${reason}`));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
this.drainAdmissionWaiters();
|
|
1013
|
+
}
|
|
1014
|
+
drainSingletonWaiters() {
|
|
1015
|
+
for (let index = 0; index < this.singletonWaiters.length;) {
|
|
1016
|
+
const waiter = this.singletonWaiters[index];
|
|
1017
|
+
if (waiter.signal?.aborted) {
|
|
1018
|
+
this.singletonWaiters.splice(index, 1);
|
|
1019
|
+
clearAdmissionDeadline(waiter);
|
|
1020
|
+
waiter.signal.removeEventListener("abort", waiter.abort);
|
|
1021
|
+
this.clearPendingRenewal(waiter.owner);
|
|
1022
|
+
waiter.reject(cancellationError(waiter.signal));
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (admissionExpired(waiter.expiresAt)) {
|
|
1026
|
+
this.singletonWaiters.splice(index, 1);
|
|
1027
|
+
clearAdmissionDeadline(waiter);
|
|
1028
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
1029
|
+
this.clearPendingRenewal(waiter.owner);
|
|
1030
|
+
waiter.reject(new HostRuntimeAdmissionExpiredError());
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (!this.singletonKeysAvailable(waiter.keys)) {
|
|
1034
|
+
index += 1;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
this.singletonWaiters.splice(index, 1);
|
|
1038
|
+
clearAdmissionDeadline(waiter);
|
|
1039
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
1040
|
+
this.assignSingletonKeys(waiter.keys, waiter.owner);
|
|
1041
|
+
waiter.resolve(waiter.keys);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
drainParallelProjectWaiters() {
|
|
1045
|
+
for (let index = 0; index < this.parallelProjectWaiters.length;) {
|
|
1046
|
+
const waiter = this.parallelProjectWaiters[index];
|
|
1047
|
+
if (waiter.signal?.aborted) {
|
|
1048
|
+
this.parallelProjectWaiters.splice(index, 1);
|
|
1049
|
+
clearAdmissionDeadline(waiter);
|
|
1050
|
+
waiter.signal.removeEventListener("abort", waiter.abort);
|
|
1051
|
+
this.clearPendingRenewal(waiter.owner);
|
|
1052
|
+
waiter.reject(cancellationError(waiter.signal));
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (admissionExpired(waiter.expiresAt)) {
|
|
1056
|
+
this.parallelProjectWaiters.splice(index, 1);
|
|
1057
|
+
clearAdmissionDeadline(waiter);
|
|
1058
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
1059
|
+
this.clearPendingRenewal(waiter.owner);
|
|
1060
|
+
waiter.reject(new HostRuntimeAdmissionExpiredError());
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (!this.parallelProjectAvailable(waiter.key)) {
|
|
1064
|
+
index += 1;
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
this.parallelProjectWaiters.splice(index, 1);
|
|
1068
|
+
clearAdmissionDeadline(waiter);
|
|
1069
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
1070
|
+
this.assignParallelProject(waiter.key, waiter.owner);
|
|
1071
|
+
waiter.resolve();
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async refreshKnownPromotions() {
|
|
1075
|
+
const promotions = await this.promotionStore.list();
|
|
1076
|
+
this.knownPromotions.clear();
|
|
1077
|
+
for (const promotion of promotions)
|
|
1078
|
+
this.knownPromotions.set(promotion.repoId, promotion);
|
|
1079
|
+
}
|
|
1080
|
+
async invalidatePromotion(repoId) {
|
|
1081
|
+
await this.promotionStore.invalidate(repoId);
|
|
1082
|
+
this.knownPromotions.delete(repoId.trim().toLowerCase());
|
|
1083
|
+
}
|
|
1084
|
+
drainAdmissionWaiters() {
|
|
1085
|
+
this.drainSingletonWaiters();
|
|
1086
|
+
this.drainParallelProjectWaiters();
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
class AttemptSupervisedProviderProcessRunner {
|
|
1090
|
+
supervisor;
|
|
1091
|
+
owner;
|
|
1092
|
+
listenerInspector;
|
|
1093
|
+
enforceNoListeners;
|
|
1094
|
+
constructor(supervisor, owner, listenerInspector, enforceNoListeners) {
|
|
1095
|
+
this.supervisor = supervisor;
|
|
1096
|
+
this.owner = owner;
|
|
1097
|
+
this.listenerInspector = listenerInspector;
|
|
1098
|
+
this.enforceNoListeners = enforceNoListeners;
|
|
1099
|
+
}
|
|
1100
|
+
async run(request, handlers = {}, signal) {
|
|
1101
|
+
const child = await this.supervisor.startChildReady("provider", request, handlers, signal, { maxStartAttempts: request.stdin === undefined ? 1 : 3, retryDelayMs: 100 });
|
|
1102
|
+
if (!this.enforceNoListeners)
|
|
1103
|
+
return await child.result;
|
|
1104
|
+
let settled = false;
|
|
1105
|
+
let inspectionFailure;
|
|
1106
|
+
const monitor = (async () => {
|
|
1107
|
+
while (!settled) {
|
|
1108
|
+
try {
|
|
1109
|
+
const ports = await this.listenerInspector.inspectOwnedLoopbackListeners(child, this.owner);
|
|
1110
|
+
if (ports.length > 0) {
|
|
1111
|
+
inspectionFailure = new Error(`host_analysis_listener_violation: analysis-only provider opened undeclared loopback listener(s): ${ports.join(", ")}.`);
|
|
1112
|
+
await child.stop(this.owner.fencingToken).catch(() => undefined);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
catch (error) {
|
|
1117
|
+
inspectionFailure = error;
|
|
1118
|
+
await child.stop(this.owner.fencingToken).catch(() => undefined);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
await waitForAnalysisInspection(250, signal).catch(error => {
|
|
1122
|
+
if (!settled && !signal?.aborted)
|
|
1123
|
+
inspectionFailure = error;
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
})();
|
|
1127
|
+
try {
|
|
1128
|
+
const result = await child.result;
|
|
1129
|
+
settled = true;
|
|
1130
|
+
await monitor;
|
|
1131
|
+
if (inspectionFailure)
|
|
1132
|
+
throw inspectionFailure;
|
|
1133
|
+
return result;
|
|
1134
|
+
}
|
|
1135
|
+
catch (error) {
|
|
1136
|
+
settled = true;
|
|
1137
|
+
await monitor.catch(() => undefined);
|
|
1138
|
+
if (inspectionFailure)
|
|
1139
|
+
throw inspectionFailure;
|
|
1140
|
+
throw error;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function waitForAnalysisInspection(milliseconds, signal) {
|
|
1145
|
+
if (signal?.aborted)
|
|
1146
|
+
return Promise.reject(cancellationError(signal));
|
|
1147
|
+
return new Promise((resolve, reject) => {
|
|
1148
|
+
const timer = setTimeout(() => {
|
|
1149
|
+
signal?.removeEventListener("abort", abort);
|
|
1150
|
+
resolve();
|
|
1151
|
+
}, milliseconds);
|
|
1152
|
+
const abort = () => {
|
|
1153
|
+
clearTimeout(timer);
|
|
1154
|
+
reject(cancellationError(signal));
|
|
1155
|
+
};
|
|
1156
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
export class HostRuntimeLeaseCleanupError extends Error {
|
|
1160
|
+
lease;
|
|
1161
|
+
constructor(lease, cause) {
|
|
1162
|
+
super(`host_runtime_cleanup_degraded: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
1163
|
+
this.lease = lease;
|
|
1164
|
+
this.name = "HostRuntimeLeaseCleanupError";
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
export class HostRuntimeAdmissionExpiredError extends Error {
|
|
1168
|
+
constructor() {
|
|
1169
|
+
super("host_runtime_admission_expired: dispatch lease expired while waiting for managed host admission.");
|
|
1170
|
+
this.name = "HostRuntimeAdmissionExpiredError";
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
export function hostRuntimeEventResources(lease, runtimeContract, executionClass = "managed_runtime") {
|
|
1174
|
+
const requestedHostConcurrency = runtimeContract?.contract.runtime.hostConcurrency ?? "serial";
|
|
1175
|
+
const configuredLaunches = runtimeContract?.contract.runtime.ports.filter(port => port.launch !== null) ?? [];
|
|
1176
|
+
const verifiedLaunches = configuredLaunches.filter(configured => lease.resources.ports.some(port => port.name === configured.name));
|
|
1177
|
+
const effectiveConcurrency = effectiveHostConcurrency(lease, runtimeContract, executionClass);
|
|
1178
|
+
return {
|
|
1179
|
+
agentContainer: null,
|
|
1180
|
+
sidecarContainers: [],
|
|
1181
|
+
networks: [],
|
|
1182
|
+
volumes: [],
|
|
1183
|
+
publishedPorts: lease.resources.ports,
|
|
1184
|
+
hostConcurrency: effectiveConcurrency,
|
|
1185
|
+
requestedHostConcurrency,
|
|
1186
|
+
hostParallelPromotion: effectiveConcurrency === "host_parallel_safe"
|
|
1187
|
+
? executionClass === "analysis_only" ? "analysis_profile_enforced" : "observed_probe_passed"
|
|
1188
|
+
: requestedHostConcurrency !== "parallel"
|
|
1189
|
+
? "not_requested"
|
|
1190
|
+
: configuredLaunches.length > 0 && verifiedLaunches.length === configuredLaunches.length
|
|
1191
|
+
? "pending_live_parallel_proof"
|
|
1192
|
+
: "pending_adapter_probe",
|
|
1193
|
+
verifiedRuntimePorts: verifiedLaunches.map(port => port.name),
|
|
1194
|
+
runtimeContractSource: runtimeContract?.source ?? "default",
|
|
1195
|
+
executionClass,
|
|
1196
|
+
requestedPorts: runtimeContract?.contract.runtime.ports.map(port => port.name) ?? [],
|
|
1197
|
+
singletonKeys: lease.resources.singletonKeys,
|
|
1198
|
+
processSupervisorId: lease.resources.processSupervisorId,
|
|
1199
|
+
runtimeLease: lease,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
export function hostRuntimeSingletonKeys(dispatch) {
|
|
1203
|
+
return [projectHostRuntimeSingletonKey(dispatch.repo.id)];
|
|
1204
|
+
}
|
|
1205
|
+
function hostRuntimeEnvironment(sessionId, lease, runtimeContract, executionClass) {
|
|
1206
|
+
const pathFor = (role) => lease.resources.paths.find(entry => entry.role === role)?.path ?? "";
|
|
1207
|
+
const environment = {
|
|
1208
|
+
FORGE_SESSION_ID: sessionId,
|
|
1209
|
+
FORGE_RUNTIME_MODE: "host",
|
|
1210
|
+
FORGE_RESOURCE_LEASE_ID: lease.leaseId,
|
|
1211
|
+
FORGE_DISPATCH_ATTEMPT_ID: lease.dispatchAttemptId,
|
|
1212
|
+
FORGE_EXECUTION_LEASE_ID: lease.executionLeaseId,
|
|
1213
|
+
FORGE_FENCING_TOKEN: lease.fencingToken,
|
|
1214
|
+
FORGE_WORKSPACE: lease.worktreePath,
|
|
1215
|
+
FORGE_WORKTREE_PATH: lease.worktreePath,
|
|
1216
|
+
FORGE_TMPDIR: pathFor("temporary"),
|
|
1217
|
+
FORGE_LOG_DIR: pathFor("logs"),
|
|
1218
|
+
FORGE_CACHE_DIR: pathFor("cache"),
|
|
1219
|
+
FORGE_BROWSER_PROFILE_DIR: pathFor("browser_profile"),
|
|
1220
|
+
FORGE_TEST_OUTPUT_DIR: pathFor("test_output"),
|
|
1221
|
+
FORGE_PROCESS_SUPERVISOR_ID: lease.resources.processSupervisorId ?? "",
|
|
1222
|
+
FORGE_PROCESS_SUPERVISOR_METADATA_PATH: pathFor("process_supervisor_metadata"),
|
|
1223
|
+
FORGE_HOST_CONCURRENCY: effectiveHostConcurrency(lease, runtimeContract, executionClass),
|
|
1224
|
+
FORGE_HOST_CONCURRENCY_REQUESTED: runtimeContract.contract.runtime.hostConcurrency,
|
|
1225
|
+
FORGE_RUNTIME_EXECUTION_CLASS: executionClass,
|
|
1226
|
+
FORGE_HOST_RUNTIME_CONTRACT_SOURCE: runtimeContract.source,
|
|
1227
|
+
FORGE_SINGLETON_KEYS: lease.resources.singletonKeys.join(","),
|
|
1228
|
+
TMPDIR: pathFor("temporary"),
|
|
1229
|
+
TMP: pathFor("temporary"),
|
|
1230
|
+
TEMP: pathFor("temporary"),
|
|
1231
|
+
XDG_CACHE_HOME: pathFor("cache"),
|
|
1232
|
+
NPM_CONFIG_CACHE: path.join(pathFor("cache"), "npm"),
|
|
1233
|
+
};
|
|
1234
|
+
if (lease.resources.singletonKeys.length === 0)
|
|
1235
|
+
delete environment.FORGE_SINGLETON_KEYS;
|
|
1236
|
+
const resolvedBaseUrls = [];
|
|
1237
|
+
for (const assigned of lease.resources.ports) {
|
|
1238
|
+
const configured = runtimeContract.contract.runtime.ports.find(port => port.name === assigned.name);
|
|
1239
|
+
if (!configured || assigned.hostIp !== "127.0.0.1")
|
|
1240
|
+
continue;
|
|
1241
|
+
Object.assign(environment, hostPortEnvironment(configured, assigned.hostIp, assigned.hostPort));
|
|
1242
|
+
const normalizedName = assigned.name.toUpperCase().replaceAll(/[^A-Z0-9]/gu, "_");
|
|
1243
|
+
environment[`FORGE_${normalizedName}_PORT`] = String(assigned.hostPort);
|
|
1244
|
+
if (configured.baseUrlEnv && configured.format) {
|
|
1245
|
+
const baseUrl = hostPortEnvironment(configured, assigned.hostIp, assigned.hostPort)[configured.baseUrlEnv];
|
|
1246
|
+
if (baseUrl) {
|
|
1247
|
+
environment[`FORGE_${normalizedName}_URL`] = baseUrl;
|
|
1248
|
+
resolvedBaseUrls.push(baseUrl);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (environment.FORGE_API_URL)
|
|
1253
|
+
environment.FORGE_E2E_API_URL = environment.FORGE_API_URL;
|
|
1254
|
+
if (environment.FORGE_WEB_URL)
|
|
1255
|
+
environment.FORGE_E2E_WEB_URL = environment.FORGE_WEB_URL;
|
|
1256
|
+
if (resolvedBaseUrls.length === 1)
|
|
1257
|
+
environment.FORGE_BASE_URL = resolvedBaseUrls[0];
|
|
1258
|
+
return environment;
|
|
1259
|
+
}
|
|
1260
|
+
function effectiveHostConcurrency(lease, runtimeContract, executionClass = "managed_runtime") {
|
|
1261
|
+
if (executionClass === "analysis_only") {
|
|
1262
|
+
const analysisSingletonHeld = lease.resources.singletonKeys.length > 0;
|
|
1263
|
+
return analysisSingletonHeld ? "host_serial_only" : "host_parallel_safe";
|
|
1264
|
+
}
|
|
1265
|
+
if (!runtimeContract || !hostRuntimeParallelEligible(runtimeContract))
|
|
1266
|
+
return "host_serial_only";
|
|
1267
|
+
const requiredPorts = configuredLaunchPortNames(runtimeContract);
|
|
1268
|
+
const everyPortVerified = requiredPorts.length > 0
|
|
1269
|
+
&& requiredPorts.every(name => lease.resources.ports.some(port => port.name === name));
|
|
1270
|
+
const projectSingletonHeld = lease.resources.singletonKeys.some(key => key.endsWith(":host-runtime"));
|
|
1271
|
+
return everyPortVerified && !projectSingletonHeld ? "host_parallel_safe" : "host_serial_only";
|
|
1272
|
+
}
|
|
1273
|
+
function projectHostRuntimeSingletonKey(repoId) {
|
|
1274
|
+
return `project:${repoId.toLowerCase()}:host-runtime`;
|
|
1275
|
+
}
|
|
1276
|
+
function projectHostAnalysisKey(repoId) {
|
|
1277
|
+
return `project:${repoId.toLowerCase()}:host-analysis`;
|
|
1278
|
+
}
|
|
1279
|
+
function hostRuntimeDeclaredSingletonKey(resource) {
|
|
1280
|
+
return `runtime:${resource}`;
|
|
1281
|
+
}
|
|
1282
|
+
function runtimeExecutionClass(dispatch) {
|
|
1283
|
+
return dispatch.runtimeIntent?.executionClass === "analysis_only"
|
|
1284
|
+
&& dispatch.runtimeIntent.applicationRuntimeRequired === false
|
|
1285
|
+
? "analysis_only"
|
|
1286
|
+
: "managed_runtime";
|
|
1287
|
+
}
|
|
1288
|
+
function capacityProject(projects, repoId) {
|
|
1289
|
+
const normalized = repoId.trim().toLowerCase();
|
|
1290
|
+
const existing = projects.get(normalized);
|
|
1291
|
+
if (existing)
|
|
1292
|
+
return existing;
|
|
1293
|
+
const created = {
|
|
1294
|
+
repoId: normalized,
|
|
1295
|
+
safetyClass: null,
|
|
1296
|
+
activeAttempts: 0,
|
|
1297
|
+
reservedAttempts: 0,
|
|
1298
|
+
waitingAttempts: 0,
|
|
1299
|
+
degradedAttempts: 0,
|
|
1300
|
+
runtimeContractFingerprint: null,
|
|
1301
|
+
runtimeContractSource: null,
|
|
1302
|
+
promotedAt: null,
|
|
1303
|
+
};
|
|
1304
|
+
projects.set(normalized, created);
|
|
1305
|
+
return created;
|
|
1306
|
+
}
|
|
1307
|
+
function conservativeSafetyClass(left, right) {
|
|
1308
|
+
if (!left)
|
|
1309
|
+
return right;
|
|
1310
|
+
const rank = {
|
|
1311
|
+
host_serial_only: 0,
|
|
1312
|
+
host_parallel_candidate: 1,
|
|
1313
|
+
host_parallel_safe: 2,
|
|
1314
|
+
};
|
|
1315
|
+
return rank[right] < rank[left] ? right : left;
|
|
1316
|
+
}
|
|
1317
|
+
function repoIdFromProjectSingletonKeys(keys) {
|
|
1318
|
+
return keys.map(repoIdFromProjectSingletonKey).find((value) => value !== null) ?? null;
|
|
1319
|
+
}
|
|
1320
|
+
function repoIdFromProjectSingletonKey(key) {
|
|
1321
|
+
const prefix = "project:";
|
|
1322
|
+
const suffix = [":host-runtime", ":host-analysis"].find(candidate => key.endsWith(candidate));
|
|
1323
|
+
return key.startsWith(prefix) && suffix
|
|
1324
|
+
? key.slice(prefix.length, -suffix.length)
|
|
1325
|
+
: null;
|
|
1326
|
+
}
|
|
1327
|
+
function sameSingletonOwner(left, right) {
|
|
1328
|
+
return left?.dispatchAttemptId === right.dispatchAttemptId && left.fencingToken === right.fencingToken;
|
|
1329
|
+
}
|
|
1330
|
+
function hostPortOwner(lease) {
|
|
1331
|
+
const processSupervisorId = lease.resources.processSupervisorId;
|
|
1332
|
+
if (!processSupervisorId) {
|
|
1333
|
+
throw new Error("host_runtime_adapter_invalid: process supervisor identity is required.");
|
|
1334
|
+
}
|
|
1335
|
+
return {
|
|
1336
|
+
dispatchAttemptId: lease.dispatchAttemptId,
|
|
1337
|
+
executionLeaseId: lease.executionLeaseId,
|
|
1338
|
+
fencingToken: lease.fencingToken,
|
|
1339
|
+
processSupervisorId,
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
function linkedAbortController(signal) {
|
|
1343
|
+
const controller = new AbortController();
|
|
1344
|
+
if (signal?.aborted) {
|
|
1345
|
+
controller.abort(signal.reason);
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
signal?.addEventListener("abort", () => controller.abort(signal.reason), { once: true, signal: controller.signal });
|
|
1349
|
+
}
|
|
1350
|
+
return controller;
|
|
1351
|
+
}
|
|
1352
|
+
function unlinkAbortController(controller, _signal) {
|
|
1353
|
+
if (!controller.signal.aborted)
|
|
1354
|
+
controller.abort("Host adapter launch settled.");
|
|
1355
|
+
}
|
|
1356
|
+
function removeCleanup(cleanups, cleanup) {
|
|
1357
|
+
const index = cleanups.indexOf(cleanup);
|
|
1358
|
+
if (index >= 0)
|
|
1359
|
+
cleanups.splice(index, 1);
|
|
1360
|
+
}
|
|
1361
|
+
async function releaseAdapterResources(cleanups) {
|
|
1362
|
+
const pending = cleanups.splice(0);
|
|
1363
|
+
const failed = [];
|
|
1364
|
+
let firstError;
|
|
1365
|
+
for (const cleanup of pending.reverse()) {
|
|
1366
|
+
try {
|
|
1367
|
+
await cleanup();
|
|
1368
|
+
}
|
|
1369
|
+
catch (error) {
|
|
1370
|
+
firstError ??= error;
|
|
1371
|
+
failed.unshift(cleanup);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
cleanups.push(...failed);
|
|
1375
|
+
if (firstError !== undefined)
|
|
1376
|
+
throw firstError;
|
|
1377
|
+
}
|
|
1378
|
+
function cancellationError(signal) {
|
|
1379
|
+
return new Error(String(signal?.reason ?? "Dispatch cancelled while waiting for host runtime singleton ownership."));
|
|
1380
|
+
}
|
|
1381
|
+
function validHostRuntimeLedger(value) {
|
|
1382
|
+
if (typeof value !== "object" || value === null)
|
|
1383
|
+
return false;
|
|
1384
|
+
const ledger = value;
|
|
1385
|
+
return ledger.schemaVersion === 1
|
|
1386
|
+
&& (ledger.repoId === undefined || /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(ledger.repoId))
|
|
1387
|
+
&& validRuntimeIsolationLease(ledger.runtimeLease)
|
|
1388
|
+
&& ledger.runtimeLease.mode === "host";
|
|
1389
|
+
}
|
|
1390
|
+
function validHostProcessSupervisorMetadata(value, lease) {
|
|
1391
|
+
if (typeof value !== "object" || value === null)
|
|
1392
|
+
return false;
|
|
1393
|
+
const metadata = value;
|
|
1394
|
+
return metadata.schemaVersion === 1
|
|
1395
|
+
&& /^host-[0-9a-f]{32}$/u.test(metadata.supervisorId)
|
|
1396
|
+
&& (metadata.strategy === "windows_job_object" || metadata.strategy === "unix_process_group")
|
|
1397
|
+
&& Number.isSafeInteger(metadata.rootPid)
|
|
1398
|
+
&& metadata.rootPid > 0
|
|
1399
|
+
&& Number.isFinite(Date.parse(metadata.rootCreatedAt))
|
|
1400
|
+
&& (metadata.rootIdentity == null
|
|
1401
|
+
|| (typeof metadata.rootIdentity === "string" && metadata.rootIdentity.length <= 256))
|
|
1402
|
+
&& /^[0-9a-f]{64}$/u.test(metadata.commandDigest)
|
|
1403
|
+
&& containedMetadataWorktreePath(lease.worktreePath, metadata.worktreePath)
|
|
1404
|
+
&& metadata.runtimeLeaseId === lease.executionLeaseId
|
|
1405
|
+
&& metadata.dispatchAttemptId === lease.dispatchAttemptId
|
|
1406
|
+
&& metadata.fencingToken === lease.fencingToken
|
|
1407
|
+
&& Number.isSafeInteger(metadata.bridgePid)
|
|
1408
|
+
&& metadata.bridgePid > 0;
|
|
1409
|
+
}
|
|
1410
|
+
function containedMetadataWorktreePath(worktreePath, metadataWorktreePath) {
|
|
1411
|
+
const relative = path.relative(path.resolve(worktreePath), path.resolve(metadataWorktreePath));
|
|
1412
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
1413
|
+
}
|
|
1414
|
+
function transitionHostLeaseForCleanup(lease, state) {
|
|
1415
|
+
if (lease.state === state || lease.state === "released")
|
|
1416
|
+
return lease;
|
|
1417
|
+
return transitionRuntimeIsolationLease(lease, state);
|
|
1418
|
+
}
|
|
1419
|
+
function throwIfAdmissionExpired(expiresAt) {
|
|
1420
|
+
if (admissionExpired(expiresAt))
|
|
1421
|
+
throw new HostRuntimeAdmissionExpiredError();
|
|
1422
|
+
}
|
|
1423
|
+
function admissionExpired(expiresAt) {
|
|
1424
|
+
const deadline = Date.parse(expiresAt);
|
|
1425
|
+
return !Number.isFinite(deadline) || deadline <= Date.now();
|
|
1426
|
+
}
|
|
1427
|
+
function scheduleAdmissionDeadline(expiresAt, expire) {
|
|
1428
|
+
const delay = Math.max(0, Date.parse(expiresAt) - Date.now());
|
|
1429
|
+
const timer = setTimeout(expire, Math.min(delay, 2_147_483_647));
|
|
1430
|
+
timer.unref?.();
|
|
1431
|
+
return timer;
|
|
1432
|
+
}
|
|
1433
|
+
function clearAdmissionDeadline(waiter) {
|
|
1434
|
+
if (waiter.deadlineTimer)
|
|
1435
|
+
clearTimeout(waiter.deadlineTimer);
|
|
1436
|
+
waiter.deadlineTimer = null;
|
|
1437
|
+
}
|
|
1438
|
+
async function ensurePrivateDirectory(directory) {
|
|
1439
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
1440
|
+
if (process.platform !== "win32")
|
|
1441
|
+
await fs.chmod(directory, 0o700);
|
|
1442
|
+
}
|
|
1443
|
+
async function containedWorkingDirectory(worktreePath, configuredDirectory) {
|
|
1444
|
+
const [realWorktree, realDirectory] = await Promise.all([
|
|
1445
|
+
fs.realpath(worktreePath),
|
|
1446
|
+
fs.realpath(path.resolve(worktreePath, configuredDirectory)),
|
|
1447
|
+
]);
|
|
1448
|
+
const relative = path.relative(realWorktree, realDirectory);
|
|
1449
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
1450
|
+
throw new Error("host_runtime_project_invalid: launch working directory must remain inside the prepared worktree.");
|
|
1451
|
+
}
|
|
1452
|
+
return realDirectory;
|
|
1453
|
+
}
|
|
1454
|
+
function uuidHex(value, label) {
|
|
1455
|
+
const normalized = value.replaceAll("-", "").toLowerCase();
|
|
1456
|
+
if (!/^[0-9a-f]{32}$/u.test(normalized)) {
|
|
1457
|
+
throw new Error(`host_runtime_lease_invalid: ${label} id must be a full UUID.`);
|
|
1458
|
+
}
|
|
1459
|
+
return normalized;
|
|
1460
|
+
}
|