@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.41da3780c451
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 +1563 -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-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -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 +70 -22
- 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 +192 -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 +168 -55
- package/dist/runtime/host-runtime-lease.d.ts +92 -8
- package/dist/runtime/host-runtime-lease.js +1141 -114
- 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 +29 -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 +19 -9
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +137 -107
- 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 +489 -34
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +106 -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 +126 -17
- 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 +661 -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 +14 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +773 -69
- 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-recovery.js +3 -2
- package/dist/shared/process/host-process-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +60 -11
- 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/shared/time/monotonic.d.ts +1 -0
- package/dist/shared/time/monotonic.js +8 -0
- 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,54 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runAttemptPathOperation } from "./attempt-path-operation.js";
|
|
4
|
+
/** Validate every existing ancestor before creating anything through a pathname. */
|
|
5
|
+
export async function assertCanonicalDirectory(target, allowMissing = false) {
|
|
6
|
+
const resolved = path.resolve(target);
|
|
7
|
+
const parent = path.dirname(resolved);
|
|
8
|
+
if (parent !== resolved)
|
|
9
|
+
await assertCanonicalDirectory(parent, allowMissing);
|
|
10
|
+
try {
|
|
11
|
+
const stat = await fs.lstat(resolved, { bigint: true });
|
|
12
|
+
const real = await fs.realpath(resolved);
|
|
13
|
+
const realStat = await fs.lstat(real, { bigint: true });
|
|
14
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || !samePath(real, resolved)
|
|
15
|
+
|| stat.dev !== realStat.dev || stat.ino !== realStat.ino) {
|
|
16
|
+
throw new Error("playwright_runtime_adapter_invalid: writable paths must be canonical, link-free directories.");
|
|
17
|
+
}
|
|
18
|
+
return realStat;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (allowMissing && error.code === "ENOENT")
|
|
22
|
+
return;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function createCanonicalDirectory(target) {
|
|
27
|
+
if (await assertCanonicalDirectory(target, true))
|
|
28
|
+
return;
|
|
29
|
+
const resolved = path.resolve(target);
|
|
30
|
+
let parent = path.dirname(resolved);
|
|
31
|
+
let identity = await assertCanonicalDirectory(parent, true);
|
|
32
|
+
while (!identity) {
|
|
33
|
+
parent = path.dirname(parent);
|
|
34
|
+
identity = await assertCanonicalDirectory(parent, true);
|
|
35
|
+
}
|
|
36
|
+
await runAttemptPathOperation(parent, identity, { kind: "mkdir", segments: path.relative(parent, resolved).split(path.sep) });
|
|
37
|
+
await assertCanonicalDirectory(resolved);
|
|
38
|
+
}
|
|
39
|
+
export async function createPrivateCanonicalDirectory(target) {
|
|
40
|
+
await createCanonicalDirectory(target);
|
|
41
|
+
await restorePrivateDirectoryMode(target);
|
|
42
|
+
}
|
|
43
|
+
export async function restorePrivateDirectoryMode(target) {
|
|
44
|
+
if (process.platform === "win32")
|
|
45
|
+
return;
|
|
46
|
+
const resolved = path.resolve(target);
|
|
47
|
+
const parent = path.dirname(resolved);
|
|
48
|
+
const identity = await assertCanonicalDirectory(parent);
|
|
49
|
+
await runAttemptPathOperation(parent, identity, { kind: "private-directory-mode", segments: [path.basename(resolved)] });
|
|
50
|
+
await assertCanonicalDirectory(resolved);
|
|
51
|
+
}
|
|
52
|
+
function samePath(left, right) {
|
|
53
|
+
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
54
|
+
}
|
|
@@ -19,6 +19,10 @@ export interface AttemptProcessChildLedgerEntry {
|
|
|
19
19
|
readonly settledAt?: string;
|
|
20
20
|
readonly exitCode?: number;
|
|
21
21
|
readonly failure?: string;
|
|
22
|
+
readonly peakMemoryBytes?: number;
|
|
23
|
+
readonly memorySampleCount?: number;
|
|
24
|
+
readonly memorySource?: string;
|
|
25
|
+
readonly memoryObservedAt?: string;
|
|
22
26
|
}
|
|
23
27
|
export interface AttemptProcessSupervisorLedger {
|
|
24
28
|
readonly schemaVersion: 1;
|
|
@@ -79,8 +83,15 @@ export declare class AttemptProcessSupervisor {
|
|
|
79
83
|
stopAll(fencingToken: string, reason?: unknown): Promise<void>;
|
|
80
84
|
private performStopAll;
|
|
81
85
|
snapshot(): AttemptProcessSupervisorLedger;
|
|
86
|
+
providerMemoryTelemetry(): AttemptProcessTreeMemoryTelemetry | null;
|
|
82
87
|
private observeChildReady;
|
|
83
88
|
private settleChild;
|
|
84
89
|
private assertFence;
|
|
85
90
|
private persist;
|
|
86
91
|
}
|
|
92
|
+
export interface AttemptProcessTreeMemoryTelemetry {
|
|
93
|
+
readonly peakMemoryBytes: number;
|
|
94
|
+
readonly sampleCount: number;
|
|
95
|
+
readonly source: string;
|
|
96
|
+
readonly observedAt: string;
|
|
97
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { ForgeFailureError, sanitizeFailureError } from "../session/session.types.js";
|
|
5
|
+
import { monotonicNowMs } from "../shared/time/monotonic.js";
|
|
4
6
|
const terminalChildStatuses = new Set(["exited", "failed"]);
|
|
5
7
|
const defaultMetadataReadyTimeoutMs = process.platform === "win32" ? 60_000 : 10_000;
|
|
6
8
|
/**
|
|
@@ -32,9 +34,11 @@ export class AttemptProcessSupervisor {
|
|
|
32
34
|
}
|
|
33
35
|
else {
|
|
34
36
|
options.signal?.addEventListener("abort", () => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
// stopAll already persists a degraded terminal snapshot when a child
|
|
38
|
+
// rejects during cancellation. Do not relabel that process failure as
|
|
39
|
+
// a persistence failure: doing so poisons every later cleanup retry,
|
|
40
|
+
// even after all supervised processes have exited.
|
|
41
|
+
void this.stopAll(this.identity.fencingToken, options.signal?.reason).catch(() => undefined);
|
|
38
42
|
}, { once: true });
|
|
39
43
|
}
|
|
40
44
|
}
|
|
@@ -57,8 +61,10 @@ export class AttemptProcessSupervisor {
|
|
|
57
61
|
const combinedSignal = signal
|
|
58
62
|
? AbortSignal.any([this.attemptController.signal, controller.signal, signal])
|
|
59
63
|
: AbortSignal.any([this.attemptController.signal, controller.signal]);
|
|
64
|
+
const failureRedactionValues = [...new Set((request.failureRedactionValues ?? []).filter(value => value.length > 0))];
|
|
65
|
+
const { failureRedactionValues: _failureRedactionValues, ...executableRequest } = request;
|
|
60
66
|
const childRequest = {
|
|
61
|
-
...
|
|
67
|
+
...executableRequest,
|
|
62
68
|
env: {
|
|
63
69
|
...request.env,
|
|
64
70
|
FORGE_RUNTIME_MODE: "host",
|
|
@@ -82,6 +88,7 @@ export class AttemptProcessSupervisor {
|
|
|
82
88
|
processSupervisorId,
|
|
83
89
|
metadataPath,
|
|
84
90
|
controller,
|
|
91
|
+
failureRedactionValues,
|
|
85
92
|
startedAt,
|
|
86
93
|
status: "starting",
|
|
87
94
|
result: Promise.resolve({ exitCode: 1, stdout: "", stderr: "not started" }),
|
|
@@ -90,7 +97,7 @@ export class AttemptProcessSupervisor {
|
|
|
90
97
|
this.currentByName.set(name, record);
|
|
91
98
|
const result = this.runner.run(childRequest, handlers, combinedSignal);
|
|
92
99
|
record.result = result;
|
|
93
|
-
void result.then(value => this.settleChild(record, "exited", value.exitCode), error => this.settleChild(record, "failed", undefined, error));
|
|
100
|
+
void result.then(value => this.settleChild(record, "exited", value.exitCode, undefined, value), error => this.settleChild(record, "failed", undefined, error));
|
|
94
101
|
try {
|
|
95
102
|
await this.persist();
|
|
96
103
|
}
|
|
@@ -180,7 +187,9 @@ export class AttemptProcessSupervisor {
|
|
|
180
187
|
record.status = "stopping";
|
|
181
188
|
await this.persist();
|
|
182
189
|
const results = await Promise.allSettled(active.map(record => record.result));
|
|
183
|
-
const rejected = results.
|
|
190
|
+
const rejected = results.flatMap((result, index) => result.status === "rejected"
|
|
191
|
+
? [sanitizeFailureError(result.reason, active[index].failureRedactionValues)]
|
|
192
|
+
: []);
|
|
184
193
|
const terminalStatus = rejected.length > 0 || this.persistenceFailure !== undefined ? "degraded" : "stopped";
|
|
185
194
|
await this.persist({
|
|
186
195
|
...this.snapshot(),
|
|
@@ -189,7 +198,7 @@ export class AttemptProcessSupervisor {
|
|
|
189
198
|
});
|
|
190
199
|
this.status = terminalStatus;
|
|
191
200
|
if (rejected.length > 0) {
|
|
192
|
-
throw new AggregateError(rejected
|
|
201
|
+
throw new AggregateError(rejected, "attempt_process_supervisor_cleanup_degraded");
|
|
193
202
|
}
|
|
194
203
|
if (this.persistenceFailure !== undefined)
|
|
195
204
|
throw this.persistenceFailure;
|
|
@@ -213,12 +222,35 @@ export class AttemptProcessSupervisor {
|
|
|
213
222
|
...(record.settledAt ? { settledAt: record.settledAt } : {}),
|
|
214
223
|
...(record.exitCode === undefined ? {} : { exitCode: record.exitCode }),
|
|
215
224
|
...(record.failure ? { failure: record.failure } : {}),
|
|
225
|
+
...(record.peakMemoryBytes === undefined ? {} : { peakMemoryBytes: record.peakMemoryBytes }),
|
|
226
|
+
...(record.memorySampleCount === undefined ? {} : { memorySampleCount: record.memorySampleCount }),
|
|
227
|
+
...(record.memorySource ? { memorySource: record.memorySource } : {}),
|
|
228
|
+
...(record.memoryObservedAt ? { memoryObservedAt: record.memoryObservedAt } : {}),
|
|
216
229
|
})),
|
|
217
230
|
};
|
|
218
231
|
}
|
|
232
|
+
providerMemoryTelemetry() {
|
|
233
|
+
const observations = this.records
|
|
234
|
+
.filter(record => record.name === "provider"
|
|
235
|
+
&& record.peakMemoryBytes !== undefined
|
|
236
|
+
&& record.peakMemoryBytes > 0
|
|
237
|
+
&& record.memorySampleCount !== undefined
|
|
238
|
+
&& record.memorySampleCount > 0
|
|
239
|
+
&& record.memorySource
|
|
240
|
+
&& record.memoryObservedAt);
|
|
241
|
+
if (observations.length === 0)
|
|
242
|
+
return null;
|
|
243
|
+
const peak = observations.reduce((current, candidate) => (candidate.peakMemoryBytes ?? 0) > (current.peakMemoryBytes ?? 0) ? candidate : current);
|
|
244
|
+
return {
|
|
245
|
+
peakMemoryBytes: peak.peakMemoryBytes,
|
|
246
|
+
sampleCount: observations.reduce((total, candidate) => total + (candidate.memorySampleCount ?? 0), 0),
|
|
247
|
+
source: peak.memorySource,
|
|
248
|
+
observedAt: peak.memoryObservedAt,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
219
251
|
async observeChildReady(record) {
|
|
220
|
-
const deadline =
|
|
221
|
-
while (
|
|
252
|
+
const deadline = monotonicNowMs() + this.metadataReadyTimeoutMs;
|
|
253
|
+
while (monotonicNowMs() <= deadline) {
|
|
222
254
|
if (await fileExists(record.metadataPath)) {
|
|
223
255
|
if (record.status === "starting") {
|
|
224
256
|
record.status = "running";
|
|
@@ -228,19 +260,32 @@ export class AttemptProcessSupervisor {
|
|
|
228
260
|
}
|
|
229
261
|
if (terminalChildStatuses.has(record.status)) {
|
|
230
262
|
const detail = await childStartFailureDetail(record);
|
|
231
|
-
throw new
|
|
263
|
+
throw new ForgeFailureError("attempt_process_supervisor_start_failed", `attempt_process_supervisor_start_failed: child '${record.childId}' exited before supervision metadata was produced${detail ? `: ${detail}` : "."}`, "bridge");
|
|
232
264
|
}
|
|
233
265
|
await wait(25);
|
|
234
266
|
}
|
|
235
267
|
record.controller.abort("Supervised child metadata was not produced before the readiness deadline.");
|
|
236
268
|
const detail = await childStartFailureDetail(record);
|
|
237
|
-
throw new
|
|
269
|
+
throw new ForgeFailureError("attempt_process_supervisor_start_timeout", `attempt_process_supervisor_start_timeout: child '${record.childId}' did not become supervised${detail ? `: ${detail}` : "."}`, "bridge");
|
|
238
270
|
}
|
|
239
|
-
async settleChild(record, status, exitCode, failure) {
|
|
271
|
+
async settleChild(record, status, exitCode, failure, result) {
|
|
240
272
|
record.status = status;
|
|
241
273
|
record.settledAt = new Date().toISOString();
|
|
242
274
|
record.exitCode = exitCode;
|
|
243
|
-
record.failure = failure === undefined
|
|
275
|
+
record.failure = failure === undefined
|
|
276
|
+
? undefined
|
|
277
|
+
: boundedFailure(failure, record.failureRedactionValues);
|
|
278
|
+
if (result?.peakMemoryBytes !== undefined
|
|
279
|
+
&& result.peakMemoryBytes > 0
|
|
280
|
+
&& result.memorySampleCount !== undefined
|
|
281
|
+
&& result.memorySampleCount > 0
|
|
282
|
+
&& result.memorySource
|
|
283
|
+
&& result.memoryObservedAt) {
|
|
284
|
+
record.peakMemoryBytes = result.peakMemoryBytes;
|
|
285
|
+
record.memorySampleCount = result.memorySampleCount;
|
|
286
|
+
record.memorySource = result.memorySource;
|
|
287
|
+
record.memoryObservedAt = result.memoryObservedAt;
|
|
288
|
+
}
|
|
244
289
|
await this.persist().catch(error => {
|
|
245
290
|
this.persistenceFailure ??= error;
|
|
246
291
|
this.status = "degraded";
|
|
@@ -310,29 +355,32 @@ function boundedInteger(value, minimum, maximum, name) {
|
|
|
310
355
|
}
|
|
311
356
|
return value;
|
|
312
357
|
}
|
|
313
|
-
function boundedFailure(error) {
|
|
314
|
-
const message = error
|
|
358
|
+
function boundedFailure(error, exactSecrets = []) {
|
|
359
|
+
const message = sanitizeFailureError(error, exactSecrets).message;
|
|
315
360
|
return message.replaceAll(/[\r\n\t]+/gu, " ").slice(0, 512);
|
|
316
361
|
}
|
|
317
362
|
async function childStartFailureDetail(record) {
|
|
318
363
|
try {
|
|
319
364
|
const result = await record.result;
|
|
320
|
-
const stderr = boundedDiagnostic(result.stderr);
|
|
365
|
+
const stderr = boundedDiagnostic(result.stderr, record.failureRedactionValues);
|
|
321
366
|
return stderr
|
|
322
367
|
? `exit ${result.exitCode}; stderr: ${stderr}`
|
|
323
368
|
: `exit ${result.exitCode}`;
|
|
324
369
|
}
|
|
325
370
|
catch (error) {
|
|
326
|
-
return boundedFailure(error);
|
|
371
|
+
return boundedFailure(error, record.failureRedactionValues);
|
|
327
372
|
}
|
|
328
373
|
}
|
|
329
|
-
function boundedDiagnostic(value) {
|
|
330
|
-
return value
|
|
374
|
+
function boundedDiagnostic(value, exactSecrets = []) {
|
|
375
|
+
return sanitizeFailureError(value, exactSecrets).message
|
|
376
|
+
.replaceAll(/[\r\n\t]+/gu, " ")
|
|
377
|
+
.trim()
|
|
378
|
+
.slice(0, 512);
|
|
331
379
|
}
|
|
332
380
|
function isRetryableStartFailure(error) {
|
|
333
|
-
return error instanceof
|
|
334
|
-
&& (error.
|
|
335
|
-
|| error.
|
|
381
|
+
return error instanceof ForgeFailureError
|
|
382
|
+
&& (error.failureCode === "attempt_process_supervisor_start_failed"
|
|
383
|
+
|| error.failureCode === "attempt_process_supervisor_start_timeout");
|
|
336
384
|
}
|
|
337
385
|
function cancellationError(signal) {
|
|
338
386
|
return new Error(String(signal.reason ?? "Supervised child launch was cancelled."));
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PinnedDirectoryIdentity } from "./pinned-directory-process.js";
|
|
2
|
+
/** Keep the validated source inode open: reopening a checked pathname can follow a replacement. */
|
|
3
|
+
export declare function readCanonicalSourceFile(target: string, maximumBytes: number, expectedIdentity?: PinnedDirectoryIdentity): Promise<Buffer>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Keep the validated source inode open: reopening a checked pathname can follow a replacement. */
|
|
5
|
+
export async function readCanonicalSourceFile(target, maximumBytes, expectedIdentity) {
|
|
6
|
+
const resolved = path.resolve(target);
|
|
7
|
+
const pathBefore = await canonicalStat(resolved);
|
|
8
|
+
if (expectedIdentity && !sameFile(pathBefore, expectedIdentity)) {
|
|
9
|
+
throw new Error("host_runtime_source_changed: source identity changed before reading.");
|
|
10
|
+
}
|
|
11
|
+
const handle = await fs.open(resolved, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
|
|
12
|
+
| (constants.O_NONBLOCK ?? 0));
|
|
13
|
+
try {
|
|
14
|
+
const before = await handle.stat({ bigint: true });
|
|
15
|
+
if (!before.isFile() || before.nlink !== 1n || !sameFile(pathBefore, before) || before.size > BigInt(maximumBytes)) {
|
|
16
|
+
throw new Error("host_runtime_source_invalid: source changed or exceeds the byte bound.");
|
|
17
|
+
}
|
|
18
|
+
if (!sameFile(before, await canonicalStat(resolved))) {
|
|
19
|
+
throw new Error("host_runtime_source_changed: source changed before reading.");
|
|
20
|
+
}
|
|
21
|
+
const bytes = Buffer.alloc(Number(before.size));
|
|
22
|
+
let offset = 0;
|
|
23
|
+
while (offset < bytes.length) {
|
|
24
|
+
const read = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
25
|
+
if (!read.bytesRead)
|
|
26
|
+
throw new Error("host_runtime_source_changed: incomplete source read.");
|
|
27
|
+
offset += read.bytesRead;
|
|
28
|
+
}
|
|
29
|
+
const after = await handle.stat({ bigint: true });
|
|
30
|
+
const pathAfter = await canonicalStat(resolved);
|
|
31
|
+
if (after.nlink !== 1n || !sameFile(before, after) || !sameFile(after, pathAfter) || before.size !== after.size
|
|
32
|
+
|| before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs) {
|
|
33
|
+
throw new Error("host_runtime_source_changed: source changed while reading.");
|
|
34
|
+
}
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await handle.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function canonicalStat(target) {
|
|
42
|
+
const stat = await fs.lstat(target, { bigint: true });
|
|
43
|
+
const real = await fs.realpath(target);
|
|
44
|
+
const realStat = await fs.lstat(real, { bigint: true });
|
|
45
|
+
const samePath = process.platform === "win32" ? real.toLowerCase() === target.toLowerCase() : real === target;
|
|
46
|
+
if (!samePath || !stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n || !realStat.isFile()
|
|
47
|
+
|| realStat.isSymbolicLink() || realStat.nlink !== 1n || !sameFile(stat, realStat)) {
|
|
48
|
+
throw new Error("host_runtime_source_invalid: source must be a canonical regular file.");
|
|
49
|
+
}
|
|
50
|
+
return realStat;
|
|
51
|
+
}
|
|
52
|
+
function sameFile(left, right) {
|
|
53
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
54
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { CommandRunner } from "../sandbox/sandbox.types.js";
|
|
2
|
+
export interface DockerEngineCapabilitySnapshot {
|
|
3
|
+
readonly available: boolean;
|
|
4
|
+
readonly sampledAt: string;
|
|
5
|
+
readonly runtimeVersion: string | null;
|
|
6
|
+
readonly operatingSystem: string | null;
|
|
7
|
+
readonly context: string;
|
|
8
|
+
readonly unavailableCode: string | null;
|
|
9
|
+
readonly unavailableReason: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface DockerEngineProbeOptions {
|
|
12
|
+
readonly dockerExecutable?: string;
|
|
13
|
+
readonly dockerContext?: string;
|
|
14
|
+
readonly recovery?: DockerEngineRecoveryPolicy;
|
|
15
|
+
readonly recoveryExecutor?: DockerEngineRecoveryExecutor;
|
|
16
|
+
readonly platform?: NodeJS.Platform;
|
|
17
|
+
readonly dockerHost?: string;
|
|
18
|
+
readonly now?: () => number;
|
|
19
|
+
readonly wait?: (delayMs: number) => Promise<void>;
|
|
20
|
+
readonly onStatus?: (message: string) => void;
|
|
21
|
+
}
|
|
22
|
+
export interface DockerEngineRecoveryPolicy {
|
|
23
|
+
readonly enabled: boolean;
|
|
24
|
+
readonly failureThreshold?: number;
|
|
25
|
+
readonly confirmationDelayMs?: number;
|
|
26
|
+
readonly cooldownMs?: number;
|
|
27
|
+
readonly startupTimeoutMs?: number;
|
|
28
|
+
readonly pollIntervalMs?: number;
|
|
29
|
+
readonly canRecover?: () => boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface DockerEngineRecoveryResult {
|
|
32
|
+
readonly started: boolean;
|
|
33
|
+
readonly reason?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface DockerEngineRecoveryExecutor {
|
|
36
|
+
restart(): Promise<DockerEngineRecoveryResult>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Probes ordinary Docker-daemon access for host-run Testcontainers and project
|
|
40
|
+
* services. This is deliberately independent from the stricter Forge sandbox
|
|
41
|
+
* preflight: a usable Docker Desktop engine may satisfy docker_engine even
|
|
42
|
+
* when it is not approved to host an agent sandbox.
|
|
43
|
+
*/
|
|
44
|
+
export declare class DockerEngineProbe {
|
|
45
|
+
private readonly commands;
|
|
46
|
+
private readonly cacheMs;
|
|
47
|
+
private cached;
|
|
48
|
+
private sampledAtMs;
|
|
49
|
+
private inFlight;
|
|
50
|
+
private requiredWorkRecoveryInFlight;
|
|
51
|
+
private consecutiveFailures;
|
|
52
|
+
private lastRecoveryAttemptAtMs;
|
|
53
|
+
private readonly docker;
|
|
54
|
+
private readonly context;
|
|
55
|
+
private readonly platform;
|
|
56
|
+
private readonly dockerHost;
|
|
57
|
+
private readonly now;
|
|
58
|
+
private readonly wait;
|
|
59
|
+
private readonly recovery;
|
|
60
|
+
private readonly recoveryExecutor;
|
|
61
|
+
private readonly onStatus;
|
|
62
|
+
constructor(options?: DockerEngineProbeOptions, commands?: CommandRunner, cacheMs?: number);
|
|
63
|
+
current(): DockerEngineCapabilitySnapshot | null;
|
|
64
|
+
refresh(force?: boolean): Promise<DockerEngineCapabilitySnapshot>;
|
|
65
|
+
/**
|
|
66
|
+
* Establishes a fresh daemon fence for work that declares Docker as a hard
|
|
67
|
+
* prerequisite. This remains bounded, cooldown protected and single-flight;
|
|
68
|
+
* it never contacts Docker unless a caller explicitly invokes this method.
|
|
69
|
+
*/
|
|
70
|
+
ensureAvailableForRequiredWork(): Promise<DockerEngineCapabilitySnapshot>;
|
|
71
|
+
private inspectSafely;
|
|
72
|
+
private recoverIfEligible;
|
|
73
|
+
private isLocalDockerDesktopContext;
|
|
74
|
+
private inspect;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Restarts only the local Docker Desktop installation. It never targets a
|
|
78
|
+
* remote daemon and is used only behind the explicit bridge recovery policy.
|
|
79
|
+
*/
|
|
80
|
+
export declare class WindowsDockerDesktopRecoveryExecutor implements DockerEngineRecoveryExecutor {
|
|
81
|
+
private readonly platform;
|
|
82
|
+
private readonly commands;
|
|
83
|
+
private readonly programFiles;
|
|
84
|
+
private readonly wait;
|
|
85
|
+
constructor(platform?: NodeJS.Platform, commands?: CommandRunner, programFiles?: string, wait?: (delayMs: number) => Promise<void>);
|
|
86
|
+
restart(): Promise<DockerEngineRecoveryResult>;
|
|
87
|
+
private readProcessState;
|
|
88
|
+
private cleanupOrphanedCompanions;
|
|
89
|
+
private startDesktop;
|
|
90
|
+
}
|