@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.bfa11ebe98c6
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 +88 -2
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +72 -3
- package/dist/bridge/bridge.service.js +1562 -152
- package/dist/bridge/bridge.types.d.ts +46 -1
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- 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 +121 -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 +1 -0
- package/dist/cleanup/cleanup.service.js +70 -9
- package/dist/cli/cli.help.js +2 -2
- package/dist/cli/cli.service.d.ts +14 -6
- package/dist/cli/cli.service.js +75 -16
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +4 -0
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
- 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.d.ts +11 -0
- package/dist/runtime/attempt-process-supervisor.js +67 -20
- 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 +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +62 -8
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +191 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +8 -1
- package/dist/runtime/host-listener-inspector.js +121 -60
- package/dist/runtime/host-runtime-contract.d.ts +12 -1
- package/dist/runtime/host-runtime-contract.js +107 -53
- package/dist/runtime/host-runtime-lease.d.ts +90 -8
- package/dist/runtime/host-runtime-lease.js +1097 -108
- package/dist/runtime/host-runtime-promotion.d.ts +12 -3
- package/dist/runtime/host-runtime-promotion.js +59 -12
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
- package/dist/runtime/node-host-runtime-adapter.js +28 -11
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.js +4 -1
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +31 -1
- package/dist/sandbox/preflight.js +237 -51
- package/dist/sandbox/repository-lock.d.ts +2 -1
- package/dist/sandbox/repository-lock.js +17 -8
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +132 -103
- package/dist/sandbox/sandbox.types.d.ts +7 -0
- package/dist/sandbox/workspace.service.js +2 -0
- package/dist/session/managed-attempt-tools.d.ts +17 -1
- package/dist/session/managed-attempt-tools.js +481 -31
- 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 +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +224 -22
- package/dist/session/phase-completion.d.ts +11 -1
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +30 -7
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.js +17 -18
- package/dist/session/phase-completion.runtime.d.ts +3 -1
- package/dist/session/phase-completion.runtime.js +118 -16
- package/dist/session/phase-completion.schemas.js +49 -2
- package/dist/session/phase-completion.validation.d.ts +1 -0
- package/dist/session/phase-completion.validation.js +111 -21
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +657 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
- package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
- package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
- package/dist/session/provider-cli/provider-cli.process.js +422 -22
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
- 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 +8 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +40 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +771 -68
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
- package/dist/session/provider-cli/provider-output-state.js +361 -17
- 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.js +434 -9
- package/dist/session/session.service.js +145 -23
- package/dist/session/session.types.d.ts +147 -2
- package/dist/session/session.types.js +587 -7
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- 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 +18 -0
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +57 -9
- package/dist/shared/process/process.d.ts +2 -0
- package/dist/shared/process/process.js +12 -7
- package/dist/shared/provider-capabilities.d.ts +3 -0
- package/dist/shared/provider-capabilities.js +85 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +39 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +41 -9
- package/dist/worktree/dependency-warmer.js +497 -22
- package/dist/worktree/index.d.ts +1 -1
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree.service.d.ts +7 -0
- package/dist/worktree/worktree.service.js +253 -21
- package/dist/worktree/worktree.types.d.ts +7 -1
- package/package.json +7 -5
- package/runtime/windows-job-supervisor.ps1 +380 -14
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { GitClient } from "../shared/git/git.js";
|
|
3
|
+
import { loadHostRuntimeContract } from "./host-runtime-contract.js";
|
|
4
|
+
import { configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible } from "./host-runtime-promotion.js";
|
|
5
|
+
import { preparedRuntimeIdentity } from "./prepared-runtime-identity.js";
|
|
6
|
+
/** Foreground barrier: prepare a wave, verify its exact runtimes, then release agents. */
|
|
7
|
+
export class PreparedRuntimeReadiness {
|
|
8
|
+
runtime;
|
|
9
|
+
store;
|
|
10
|
+
releasePreparation;
|
|
11
|
+
admissionExpired;
|
|
12
|
+
preparations = new Map();
|
|
13
|
+
draining = new Set();
|
|
14
|
+
constructor(runtime, store, releasePreparation, admissionExpired) {
|
|
15
|
+
this.runtime = runtime;
|
|
16
|
+
this.store = store;
|
|
17
|
+
this.releasePreparation = releasePreparation;
|
|
18
|
+
this.admissionExpired = admissionExpired;
|
|
19
|
+
}
|
|
20
|
+
async acquire(dispatch, signal) {
|
|
21
|
+
const id = dispatch.sandbox.dispatchAttemptId;
|
|
22
|
+
if (this.preparations.has(id))
|
|
23
|
+
throw new Error("host_runtime_preparation_conflict: duplicate preparation.");
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const abort = () => this.cancel(id, dispatch.lease.fencingToken);
|
|
26
|
+
const entry = { dispatch, controller, phase: "preparing", expiresAt: dispatch.lease.expiresAt,
|
|
27
|
+
removeAbort: () => signal?.removeEventListener("abort", abort) };
|
|
28
|
+
this.preparations.set(id, entry);
|
|
29
|
+
this.scheduleDeadline(entry);
|
|
30
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
31
|
+
if (signal?.aborted)
|
|
32
|
+
abort();
|
|
33
|
+
try {
|
|
34
|
+
// Preparation consumes normal capacity, but no project-runtime claim.
|
|
35
|
+
return await this.runtime.acquire({ ...dispatch,
|
|
36
|
+
runtimeIntent: { ...dispatch.runtimeIntent, executionClass: "analysis_only", applicationRuntimeRequired: false },
|
|
37
|
+
}, controller.signal);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const failure = controller.signal.aborted ? controller.signal.reason : error;
|
|
41
|
+
this.cancel(id, dispatch.lease.fencingToken);
|
|
42
|
+
throw failure;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
has(id) { return this.preparations.has(id); }
|
|
46
|
+
hasPending() { return this.preparations.size > 0; }
|
|
47
|
+
pendingSnapshots() {
|
|
48
|
+
return [...this.preparations.values()].map(entry => ({
|
|
49
|
+
repoId: entry.dispatch.repo.id,
|
|
50
|
+
dispatchAttemptId: entry.dispatch.sandbox.dispatchAttemptId,
|
|
51
|
+
fencingToken: entry.dispatch.lease.fencingToken,
|
|
52
|
+
...(entry.probe
|
|
53
|
+
? {
|
|
54
|
+
probeDispatchAttemptId: entry.probe.sandbox.dispatchAttemptId,
|
|
55
|
+
probeFencingToken: entry.probe.lease.fencingToken,
|
|
56
|
+
}
|
|
57
|
+
: {}),
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
async verify(dispatch, worktree, checkpoint) {
|
|
61
|
+
const id = dispatch.sandbox.dispatchAttemptId;
|
|
62
|
+
const entry = this.preparations.get(id);
|
|
63
|
+
if (!entry || entry.dispatch.lease.fencingToken !== dispatch.lease.fencingToken) {
|
|
64
|
+
throw new Error("host_runtime_preparation_missing: preparation ownership changed.");
|
|
65
|
+
}
|
|
66
|
+
// Release preparation headroom before waiting for peers; otherwise a one-slot
|
|
67
|
+
// host would deadlock while the rest of the wave tries to prepare.
|
|
68
|
+
await this.releasePreparation(id, dispatch.lease.fencingToken);
|
|
69
|
+
entry.controller.signal.throwIfAborted();
|
|
70
|
+
const completion = new Promise((resolve, reject) => {
|
|
71
|
+
entry.resolve = resolve;
|
|
72
|
+
entry.reject = reject;
|
|
73
|
+
});
|
|
74
|
+
entry.check = () => this.check(entry, worktree, checkpoint);
|
|
75
|
+
entry.phase = "queued";
|
|
76
|
+
void this.drain(dispatch.repo.id);
|
|
77
|
+
return await completion;
|
|
78
|
+
}
|
|
79
|
+
async renew(id, fence, expiresAt) {
|
|
80
|
+
const entry = this.preparations.get(id);
|
|
81
|
+
if (!entry || entry.dispatch.lease.fencingToken !== fence || Date.parse(expiresAt) <= Date.parse(entry.expiresAt))
|
|
82
|
+
return;
|
|
83
|
+
entry.expiresAt = expiresAt;
|
|
84
|
+
this.scheduleDeadline(entry);
|
|
85
|
+
if (entry.probe)
|
|
86
|
+
await this.runtime.updateLease(entry.probe.sandbox.dispatchAttemptId, entry.probe.lease.fencingToken, expiresAt);
|
|
87
|
+
}
|
|
88
|
+
cancel(id, fence, reason = new Error("host_runtime_preparation_cancelled: prepared readiness was cancelled.")) {
|
|
89
|
+
const entry = this.preparations.get(id);
|
|
90
|
+
if (!entry || entry.dispatch.lease.fencingToken !== fence)
|
|
91
|
+
return;
|
|
92
|
+
if (entry.deadline)
|
|
93
|
+
clearTimeout(entry.deadline);
|
|
94
|
+
entry.controller.abort(reason);
|
|
95
|
+
// Do not return to session teardown while a supervised probe still owns the worktree.
|
|
96
|
+
if (entry.phase === "checking")
|
|
97
|
+
return;
|
|
98
|
+
entry.reject?.(entry.controller.signal.reason);
|
|
99
|
+
entry.removeAbort();
|
|
100
|
+
this.preparations.delete(id);
|
|
101
|
+
void this.drain(entry.dispatch.repo.id);
|
|
102
|
+
}
|
|
103
|
+
async drain(repoId) {
|
|
104
|
+
if (this.draining.has(repoId))
|
|
105
|
+
return;
|
|
106
|
+
const entries = () => [...this.preparations.values()].filter(entry => entry.dispatch.repo.id === repoId);
|
|
107
|
+
if (entries().some(entry => entry.phase === "preparing"))
|
|
108
|
+
return;
|
|
109
|
+
this.draining.add(repoId);
|
|
110
|
+
const completed = [];
|
|
111
|
+
try {
|
|
112
|
+
for (const entry of entries()) {
|
|
113
|
+
if (entry.phase !== "queued")
|
|
114
|
+
continue;
|
|
115
|
+
entry.phase = "checking";
|
|
116
|
+
try {
|
|
117
|
+
completed.push({ entry, identity: await entry.check() });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
completed.push({ entry, error });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// No provider from this wave can start while a sibling proof is running.
|
|
124
|
+
for (const result of completed) {
|
|
125
|
+
const { entry } = result;
|
|
126
|
+
this.preparations.delete(entry.dispatch.sandbox.dispatchAttemptId);
|
|
127
|
+
if (entry.deadline)
|
|
128
|
+
clearTimeout(entry.deadline);
|
|
129
|
+
entry.removeAbort();
|
|
130
|
+
if (entry.controller.signal.aborted)
|
|
131
|
+
entry.reject?.(entry.controller.signal.reason);
|
|
132
|
+
else if (result.error)
|
|
133
|
+
entry.reject?.(result.error);
|
|
134
|
+
else
|
|
135
|
+
entry.resolve?.(result.identity);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
this.draining.delete(repoId);
|
|
140
|
+
if (entries().some(entry => entry.phase === "queued"))
|
|
141
|
+
void this.drain(repoId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
scheduleDeadline(entry) {
|
|
145
|
+
if (entry.deadline)
|
|
146
|
+
clearTimeout(entry.deadline);
|
|
147
|
+
const delay = Date.parse(entry.expiresAt) - Date.now();
|
|
148
|
+
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);
|
|
149
|
+
entry.deadline.unref();
|
|
150
|
+
}
|
|
151
|
+
async check(entry, worktree, checkpoint) {
|
|
152
|
+
const { dispatch, controller } = entry;
|
|
153
|
+
if (!dispatch.runtimeIntent.applicationRuntimeRequired)
|
|
154
|
+
return undefined;
|
|
155
|
+
const contract = await loadHostRuntimeContract(worktree);
|
|
156
|
+
if (!hostRuntimeParallelEligible(contract))
|
|
157
|
+
return undefined;
|
|
158
|
+
const preparedDiagnostics = { entries: new Map() };
|
|
159
|
+
const identity = await preparedRuntimeIdentity(worktree, controller.signal, preparedDiagnostics);
|
|
160
|
+
if (hostRuntimeContractFingerprint(contract) !== identity.runtimeContractFingerprint) {
|
|
161
|
+
throw new Error("host_runtime_prepared_source_changed: runtime contract changed while capturing its source identity.");
|
|
162
|
+
}
|
|
163
|
+
if (await this.store.load(dispatch.repo.id, identity))
|
|
164
|
+
return identity;
|
|
165
|
+
await checkpoint?.({ code: "host_runtime_prepared_probe_started", expiresAt: entry.expiresAt,
|
|
166
|
+
message: `Verifying prepared runtime ${identity.inputCommitSha.slice(0, 12)} before agent admission; project-default readiness is not a substitute.` });
|
|
167
|
+
const probeId = randomUUID();
|
|
168
|
+
const probe = { ...dispatch,
|
|
169
|
+
cycleRun: { ...dispatch.cycleRun, phase: "prepared_readiness" },
|
|
170
|
+
lease: { ...dispatch.lease, id: probeId, fencingToken: randomUUID(), expiresAt: entry.expiresAt },
|
|
171
|
+
sandbox: { ...dispatch.sandbox, dispatchAttemptId: probeId, executionLeaseId: probeId,
|
|
172
|
+
inputCommitSha: identity.inputCommitSha },
|
|
173
|
+
};
|
|
174
|
+
entry.probe = probe;
|
|
175
|
+
const stopPreparedHeadMonitor = new AbortController();
|
|
176
|
+
const preparedHeadMonitor = monitorPreparedHead(worktree, identity.inputCommitSha, controller, stopPreparedHeadMonitor.signal);
|
|
177
|
+
let cleanupConfirmed;
|
|
178
|
+
let verifiedRuntimeContractFingerprint;
|
|
179
|
+
let verifiedPorts;
|
|
180
|
+
try {
|
|
181
|
+
controller.signal.throwIfAborted();
|
|
182
|
+
await this.runtime.acquire(probe, controller.signal);
|
|
183
|
+
const prepared = await this.runtime.start(probe, worktree, controller.signal);
|
|
184
|
+
if (hostRuntimeContractFingerprint(prepared.runtimeContract) !== identity.runtimeContractFingerprint) {
|
|
185
|
+
throw new Error("host_runtime_prepared_source_changed: probe loaded a different runtime contract; application was not started.");
|
|
186
|
+
}
|
|
187
|
+
const started = await this.runtime.startConfiguredProject(probeId, probe.lease.fencingToken, controller.signal, progress => checkpoint?.({ code: "host_runtime_prepared_probe_progress", expiresAt: entry.expiresAt,
|
|
188
|
+
message: progress.message }));
|
|
189
|
+
verifiedRuntimeContractFingerprint = hostRuntimeContractFingerprint(started.runtimeContract);
|
|
190
|
+
verifiedPorts = started.lease.resources.ports.map(port => port.name).sort();
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
stopPreparedHeadMonitor.abort();
|
|
194
|
+
await preparedHeadMonitor;
|
|
195
|
+
// A failed teardown must not publish reusable evidence.
|
|
196
|
+
cleanupConfirmed = Boolean(await this.runtime.release(probeId, probe.lease.fencingToken));
|
|
197
|
+
entry.probe = undefined;
|
|
198
|
+
}
|
|
199
|
+
if (!cleanupConfirmed) {
|
|
200
|
+
throw new Error("host_runtime_prepared_probe_cleanup_unconfirmed: matching probe teardown was not confirmed.");
|
|
201
|
+
}
|
|
202
|
+
// Probe-owned processes may create transient, untracked files while they
|
|
203
|
+
// are alive. Verify the prepared source only after matching teardown is
|
|
204
|
+
// confirmed: persistent source edits still fail closed, while files that
|
|
205
|
+
// belong to the probe lifecycle cannot create a false source-change loop.
|
|
206
|
+
const currentDiagnostics = { entries: new Map() };
|
|
207
|
+
const currentIdentity = await preparedRuntimeIdentity(worktree, controller.signal, currentDiagnostics);
|
|
208
|
+
const changed = [];
|
|
209
|
+
if (verifiedRuntimeContractFingerprint !== identity.runtimeContractFingerprint
|
|
210
|
+
|| currentIdentity.runtimeContractFingerprint !== identity.runtimeContractFingerprint) {
|
|
211
|
+
changed.push("runtime contract");
|
|
212
|
+
}
|
|
213
|
+
if (currentIdentity.inputCommitSha !== identity.inputCommitSha)
|
|
214
|
+
changed.push("HEAD");
|
|
215
|
+
if (currentIdentity.sourceFingerprint !== identity.sourceFingerprint) {
|
|
216
|
+
const paths = changedIdentityPaths(preparedDiagnostics.entries, currentDiagnostics.entries);
|
|
217
|
+
changed.push(paths.length > 0
|
|
218
|
+
? `source fingerprint (${paths.join(", ")})`
|
|
219
|
+
: "source fingerprint");
|
|
220
|
+
}
|
|
221
|
+
if (changed.length > 0) {
|
|
222
|
+
throw new Error(`host_runtime_prepared_source_changed: ${changed.join(", ")} changed during isolation verification.`);
|
|
223
|
+
}
|
|
224
|
+
if (JSON.stringify(verifiedPorts) !== JSON.stringify(configuredLaunchPortNames(contract))) {
|
|
225
|
+
throw new Error("host_runtime_prepared_probe_incomplete: assigned-port verification did not confirm every configured launch port.");
|
|
226
|
+
}
|
|
227
|
+
controller.signal.throwIfAborted();
|
|
228
|
+
await this.store.observe(dispatch.repo.id, contract, probeId, configuredLaunchPortNames(contract), identity.inputCommitSha, identity.sourceFingerprint);
|
|
229
|
+
await checkpoint?.({ code: "host_runtime_prepared_probe_completed", expiresAt: entry.expiresAt,
|
|
230
|
+
message: `Prepared runtime ${identity.inputCommitSha.slice(0, 12)} passed isolation verification; its source-bound proof is retained independently.` });
|
|
231
|
+
return identity;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const preparedHeadPollIntervalMs = 1_000;
|
|
235
|
+
async function monitorPreparedHead(worktree, expectedCommitSha, preparationController, stopSignal) {
|
|
236
|
+
const git = new GitClient();
|
|
237
|
+
while (!stopSignal.aborted && !preparationController.signal.aborted) {
|
|
238
|
+
try {
|
|
239
|
+
const currentCommitSha = (await git.run(worktree, ["rev-parse", "--verify", "HEAD^{commit}"]))
|
|
240
|
+
.trim()
|
|
241
|
+
.toLowerCase();
|
|
242
|
+
if (currentCommitSha !== expectedCommitSha.toLowerCase()) {
|
|
243
|
+
throw new Error("host_runtime_prepared_source_changed: HEAD changed during isolation verification; the obsolete probe was cancelled.");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (!stopSignal.aborted && !preparationController.signal.aborted) {
|
|
248
|
+
const failure = error instanceof Error && error.message.startsWith("host_runtime_prepared_source_changed:")
|
|
249
|
+
? error
|
|
250
|
+
: new Error(`host_runtime_prepared_source_changed: HEAD could not be revalidated during isolation verification: ${errorMessage(error)}`);
|
|
251
|
+
preparationController.abort(failure);
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
await waitForPreparedHeadPoll(stopSignal);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function waitForPreparedHeadPoll(signal) {
|
|
259
|
+
if (signal.aborted)
|
|
260
|
+
return;
|
|
261
|
+
await new Promise(resolve => {
|
|
262
|
+
const complete = () => {
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
signal.removeEventListener("abort", complete);
|
|
265
|
+
resolve();
|
|
266
|
+
};
|
|
267
|
+
const timer = setTimeout(complete, preparedHeadPollIntervalMs);
|
|
268
|
+
timer.unref();
|
|
269
|
+
signal.addEventListener("abort", complete, { once: true });
|
|
270
|
+
if (signal.aborted)
|
|
271
|
+
complete();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function errorMessage(error) {
|
|
275
|
+
return error instanceof Error ? error.message : String(error);
|
|
276
|
+
}
|
|
277
|
+
function changedIdentityPaths(before, after) {
|
|
278
|
+
const paths = new Set([...before.keys(), ...after.keys()]);
|
|
279
|
+
const changed = [...paths]
|
|
280
|
+
.filter(path => before.get(path) !== after.get(path))
|
|
281
|
+
.sort((left, right) => left.localeCompare(right));
|
|
282
|
+
if (changed.length <= 8)
|
|
283
|
+
return changed;
|
|
284
|
+
return [...changed.slice(0, 8), `+${changed.length - 8} more`];
|
|
285
|
+
}
|
|
@@ -57,7 +57,10 @@ export function transitionRuntimeIsolationLease(lease, nextState, occurredAt = n
|
|
|
57
57
|
return transitioned;
|
|
58
58
|
}
|
|
59
59
|
export function renewRuntimeIsolationLease(lease, fencingToken, expiresAt) {
|
|
60
|
-
if (lease.fencingToken !== fencingToken
|
|
60
|
+
if (lease.fencingToken !== fencingToken
|
|
61
|
+
|| lease.state === "releasing"
|
|
62
|
+
|| lease.state === "released"
|
|
63
|
+
|| !validTimestamp(expiresAt))
|
|
61
64
|
return null;
|
|
62
65
|
const renewed = { ...lease, expiresAt };
|
|
63
66
|
return validRuntimeIsolationLease(renewed) ? renewed : null;
|
|
@@ -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>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
const GiB = 1024 ** 3;
|
|
6
|
+
const alignmentBytes = 512 * 1024 ** 2;
|
|
7
|
+
export const minimumWorkspaceEstimateBytes = 2 * GiB;
|
|
8
|
+
export const architectMinimumWorkspaceEstimateBytes = GiB;
|
|
9
|
+
const maximumBytes = 100 * GiB;
|
|
10
|
+
const footprintCacheLifetimeMs = 5 * 60_000;
|
|
11
|
+
const excludedDirectories = new Set([
|
|
12
|
+
".git",
|
|
13
|
+
".artifacts",
|
|
14
|
+
".forge-worktrees",
|
|
15
|
+
".local",
|
|
16
|
+
".next",
|
|
17
|
+
".nuget",
|
|
18
|
+
".pnpm-store",
|
|
19
|
+
".tmp",
|
|
20
|
+
".turbo",
|
|
21
|
+
"artifacts",
|
|
22
|
+
"bin",
|
|
23
|
+
"build",
|
|
24
|
+
"coverage",
|
|
25
|
+
"dist",
|
|
26
|
+
"node_modules",
|
|
27
|
+
"obj",
|
|
28
|
+
"testresults",
|
|
29
|
+
]);
|
|
30
|
+
const footprintCache = new Map();
|
|
31
|
+
export async function estimateWorkspaceBytesFromRepo(repoPath, hints = {}) {
|
|
32
|
+
const cacheKey = path.resolve(repoPath);
|
|
33
|
+
const cached = footprintCache.get(cacheKey);
|
|
34
|
+
const footprint = cached && Date.now() - cached.measuredAt < footprintCacheLifetimeMs
|
|
35
|
+
? cached.footprint
|
|
36
|
+
: await measureRepositoryWorkspace(cacheKey);
|
|
37
|
+
if (!cached || cached.footprint !== footprint) {
|
|
38
|
+
footprintCache.set(cacheKey, { measuredAt: Date.now(), footprint });
|
|
39
|
+
}
|
|
40
|
+
return calculateWorkspaceBytes({
|
|
41
|
+
...footprint,
|
|
42
|
+
hasBrowser: footprint.hasBrowser || hints.browser === true,
|
|
43
|
+
serviceCount: Math.max(footprint.serviceCount, Math.max(0, hints.serviceCount ?? 0)),
|
|
44
|
+
}, hints.role);
|
|
45
|
+
}
|
|
46
|
+
export function calculateWorkspaceBytes(footprint, role) {
|
|
47
|
+
const normalizedRole = role?.trim().toLowerCase() ?? "";
|
|
48
|
+
if (normalizedRole === "architect") {
|
|
49
|
+
// Architect owns a checkout and planning artifacts, not dependencies,
|
|
50
|
+
// build outputs, browser state, services, or application runtime output.
|
|
51
|
+
const estimate = architectMinimumWorkspaceEstimateBytes
|
|
52
|
+
+ Math.max(0, footprint.sourceBytes) * 2;
|
|
53
|
+
return Math.min(maximumBytes, Math.max(architectMinimumWorkspaceEstimateBytes, alignUp(estimate, alignmentBytes)));
|
|
54
|
+
}
|
|
55
|
+
let estimate = 2 * GiB + Math.max(0, footprint.sourceBytes) * 3;
|
|
56
|
+
if (footprint.hasNode)
|
|
57
|
+
estimate += 3 * GiB;
|
|
58
|
+
if (footprint.hasDotNet)
|
|
59
|
+
estimate += 2 * GiB;
|
|
60
|
+
if (footprint.hasBrowser)
|
|
61
|
+
estimate += 1536 * 1024 ** 2;
|
|
62
|
+
estimate += Math.max(0, footprint.serviceCount) * GiB;
|
|
63
|
+
if (["qa", "ci_repair", "pr_fix"].includes(normalizedRole)) {
|
|
64
|
+
estimate += GiB;
|
|
65
|
+
}
|
|
66
|
+
return Math.min(maximumBytes, Math.max(minimumWorkspaceEstimateBytes, alignUp(estimate, alignmentBytes)));
|
|
67
|
+
}
|
|
68
|
+
export async function measureRepositoryWorkspace(repoPath) {
|
|
69
|
+
return await measureGitWorkspace(repoPath) ?? await measureFilesystemFallback(repoPath);
|
|
70
|
+
}
|
|
71
|
+
async function measureGitWorkspace(repoPath) {
|
|
72
|
+
let sourceBytes = 0;
|
|
73
|
+
let hasNode = false;
|
|
74
|
+
let hasDotNet = false;
|
|
75
|
+
let hasBrowser = false;
|
|
76
|
+
let serviceCount = 0;
|
|
77
|
+
const child = spawn("git", ["-c", "core.quotepath=false", "ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
78
|
+
cwd: repoPath,
|
|
79
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
80
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
const closed = new Promise((resolve) => {
|
|
84
|
+
child.once("close", resolve);
|
|
85
|
+
child.once("error", () => resolve(null));
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
89
|
+
for await (const relativePath of lines) {
|
|
90
|
+
if (!relativePath.trim())
|
|
91
|
+
continue;
|
|
92
|
+
const file = await inspectWorkspaceFile(path.join(repoPath, relativePath));
|
|
93
|
+
if (!file)
|
|
94
|
+
continue;
|
|
95
|
+
sourceBytes += file.bytes;
|
|
96
|
+
hasNode ||= file.hasNode;
|
|
97
|
+
hasDotNet ||= file.hasDotNet;
|
|
98
|
+
hasBrowser ||= file.hasBrowser;
|
|
99
|
+
serviceCount = Math.max(serviceCount, file.serviceCount);
|
|
100
|
+
}
|
|
101
|
+
return await closed === 0 ? { sourceBytes, hasNode, hasDotNet, hasBrowser, serviceCount } : null;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
child.kill();
|
|
105
|
+
await closed;
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function measureFilesystemFallback(repoPath) {
|
|
110
|
+
let sourceBytes = 0;
|
|
111
|
+
let hasNode = false;
|
|
112
|
+
let hasDotNet = false;
|
|
113
|
+
let hasBrowser = false;
|
|
114
|
+
let serviceCount = 0;
|
|
115
|
+
const pending = [path.resolve(repoPath)];
|
|
116
|
+
while (pending.length > 0) {
|
|
117
|
+
const current = pending.pop();
|
|
118
|
+
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const entryPath = path.join(current, entry.name);
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
if (!excludedDirectories.has(entry.name.toLowerCase()))
|
|
123
|
+
pending.push(entryPath);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!entry.isFile())
|
|
127
|
+
continue;
|
|
128
|
+
const file = await inspectWorkspaceFile(entryPath);
|
|
129
|
+
if (!file)
|
|
130
|
+
continue;
|
|
131
|
+
sourceBytes += file.bytes;
|
|
132
|
+
hasNode ||= file.hasNode;
|
|
133
|
+
hasDotNet ||= file.hasDotNet;
|
|
134
|
+
hasBrowser ||= file.hasBrowser;
|
|
135
|
+
serviceCount = Math.max(serviceCount, file.serviceCount);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { sourceBytes, hasNode, hasDotNet, hasBrowser, serviceCount };
|
|
139
|
+
}
|
|
140
|
+
async function inspectWorkspaceFile(filePath) {
|
|
141
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
142
|
+
if (!stat?.isFile())
|
|
143
|
+
return null;
|
|
144
|
+
const name = path.basename(filePath).toLowerCase();
|
|
145
|
+
const sandbox = name === "forge.sandbox.yml" || name === "forge.sandbox.yaml";
|
|
146
|
+
return {
|
|
147
|
+
bytes: stat.size,
|
|
148
|
+
hasNode: ["package.json", "pnpm-lock.yaml", "package-lock.json", "yarn.lock"].includes(name),
|
|
149
|
+
hasDotNet: name.endsWith(".sln") || name.endsWith(".csproj") || name.endsWith(".fsproj"),
|
|
150
|
+
hasBrowser: name.startsWith("playwright.config"),
|
|
151
|
+
serviceCount: sandbox
|
|
152
|
+
? countSandboxServices(await fs.readFile(filePath, "utf8").catch(() => ""))
|
|
153
|
+
: 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function countSandboxServices(yaml) {
|
|
157
|
+
let inServices = false;
|
|
158
|
+
let count = 0;
|
|
159
|
+
for (const rawLine of yaml.split(/\r?\n/gu)) {
|
|
160
|
+
const line = rawLine.trim();
|
|
161
|
+
if (!line || line.startsWith("#"))
|
|
162
|
+
continue;
|
|
163
|
+
if (!/^\s/u.test(rawLine)) {
|
|
164
|
+
inServices = line.toLowerCase() === "services:";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (inServices && line.startsWith("-"))
|
|
168
|
+
count += 1;
|
|
169
|
+
}
|
|
170
|
+
return count;
|
|
171
|
+
}
|
|
172
|
+
function alignUp(value, alignment) {
|
|
173
|
+
return Math.ceil(value / alignment) * alignment;
|
|
174
|
+
}
|
|
@@ -19,23 +19,53 @@ export declare class DockerPreflight {
|
|
|
19
19
|
private readonly cacheMs;
|
|
20
20
|
private generation;
|
|
21
21
|
private cached;
|
|
22
|
-
private
|
|
22
|
+
private fullPreflightAt;
|
|
23
|
+
private staticPreflight;
|
|
24
|
+
private inspectionInFlight;
|
|
25
|
+
private lastCapacitySample;
|
|
23
26
|
private readonly options;
|
|
24
27
|
private readonly docker;
|
|
25
28
|
private readonly ownership;
|
|
26
29
|
private diskPressureLatched;
|
|
30
|
+
private readonly memoryAvailability;
|
|
27
31
|
constructor(options?: DockerPreflightOptions | string, commands?: CommandRunner, cacheMs?: number);
|
|
32
|
+
private unavailable;
|
|
33
|
+
private memoryReserveBytes;
|
|
34
|
+
private defaultAdmissionMemoryBytes;
|
|
28
35
|
current(): SandboxPreflightResult | null;
|
|
29
36
|
refresh(): Promise<SandboxPreflightResult>;
|
|
30
37
|
release(): Promise<void>;
|
|
31
38
|
run(force?: boolean): Promise<SandboxPreflightResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Refreshes only the admission inputs that change while the daemon's static
|
|
41
|
+
* capability snapshot remains valid. Heartbeats use this path so periodic
|
|
42
|
+
* liveness reporting does not wake Docker/WSL to rerun feature probes.
|
|
43
|
+
*/
|
|
44
|
+
refreshCapacity(): Promise<SandboxPreflightResult>;
|
|
45
|
+
/**
|
|
46
|
+
* When the cached static preflight remains usable, produces a capacity sample
|
|
47
|
+
* that began after this admission request. An older inspection is awaited,
|
|
48
|
+
* not preempted or reused. Later admission callers may share the next sample
|
|
49
|
+
* only when it began after they entered this method. Without a usable static
|
|
50
|
+
* snapshot, the existing fail-closed result is returned without probing.
|
|
51
|
+
*/
|
|
52
|
+
refreshCapacityForAdmission(): Promise<SandboxPreflightResult>;
|
|
53
|
+
private startInspection;
|
|
32
54
|
private inspect;
|
|
55
|
+
private inspectCapacity;
|
|
33
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Uses the same reviewed reserve vector as host-runtime admission. The input
|
|
59
|
+
* is always the physical host total; Docker Desktop's VM total is tracked as
|
|
60
|
+
* a separate daemon commitment domain.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveSandboxHostMemoryReserveBytes(totalMemoryBytes: number, explicitBytes?: number, environment?: NodeJS.ProcessEnv): number;
|
|
34
63
|
export declare function canRunDockerMaintenance(preflight: {
|
|
35
64
|
readonly healthy: boolean;
|
|
36
65
|
readonly unavailableCode?: string | null;
|
|
37
66
|
}): boolean;
|
|
38
67
|
export declare function evaluateDiskPressure(availableBytes: number, totalBytes: number, minimumAvailableBytes: number, highWatermarkPercent: number, lowWatermarkPercent: number, pressureLatched: boolean): boolean;
|
|
68
|
+
export declare function requiredAvailableBytesToClearDiskPressure(totalBytes: number, minimumAvailableBytes: number, lowWatermarkPercent: number): number;
|
|
39
69
|
export declare function storagePolicyAvailable(storageQuota: boolean, environment?: NodeJS.ProcessEnv): boolean;
|
|
40
70
|
export declare class DockerContextOwnershipLock {
|
|
41
71
|
private lockPath;
|