@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.1ff37038366e
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 +165 -1
- 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 +9 -1
- package/dist/bridge/bridge.service.d.ts +129 -3
- package/dist/bridge/bridge.service.js +2427 -70
- package/dist/bridge/bridge.types.d.ts +218 -2
- 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 +14 -2
- package/dist/cleanup/cleanup.service.js +496 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +55 -7
- package/dist/cli/cli.service.js +429 -219
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- 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 +18 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
- package/dist/runtime/attempt-process-supervisor.js +397 -0
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +20 -0
- package/dist/runtime/generated-output-reset.js +157 -0
- 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 +51 -0
- package/dist/runtime/host-listener-inspector.js +385 -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 +58 -0
- package/dist/runtime/host-runtime-contract.js +813 -0
- package/dist/runtime/host-runtime-lease.d.ts +217 -0
- package/dist/runtime/host-runtime-lease.js +2487 -0
- package/dist/runtime/host-runtime-promotion.d.ts +32 -0
- package/dist/runtime/host-runtime-promotion.js +206 -0
- package/dist/runtime/memory-capacity.d.ts +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 +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +285 -0
- 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 +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
- 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.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +161 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +6 -0
- package/dist/sandbox/repository-lock.js +128 -0
- package/dist/sandbox/runtime.d.ts +302 -0
- package/dist/sandbox/runtime.js +2367 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +74 -0
- package/dist/session/managed-attempt-tools.d.ts +28 -0
- package/dist/session/managed-attempt-tools.js +1015 -0
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +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 +27 -0
- package/dist/session/phase-completion.artifacts.js +829 -0
- package/dist/session/phase-completion.d.ts +53 -42
- package/dist/session/phase-completion.helpers.d.ts +117 -0
- package/dist/session/phase-completion.helpers.js +707 -0
- package/dist/session/phase-completion.js +125 -3007
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +233 -0
- package/dist/session/phase-completion.runtime.d.ts +49 -0
- package/dist/session/phase-completion.runtime.js +840 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +859 -0
- package/dist/session/phase-completion.validation.d.ts +38 -0
- package/dist/session/phase-completion.validation.js +918 -0
- 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 +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
- package/dist/session/provider-cli/provider-cli.process.js +874 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +16 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +81 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
- package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
- package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
- package/dist/session/provider-cli/provider-output-state.js +817 -0
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +448 -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 +586 -59
- package/dist/session/session.types.d.ts +324 -3
- package/dist/session/session.types.js +939 -10
- 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 +21 -1
- 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.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +330 -0
- package/dist/shared/process/process.d.ts +3 -0
- package/dist/shared/process/process.js +13 -7
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +4 -0
- package/dist/shared/provider-capabilities.js +88 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +47 -4
- 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 +82 -10
- package/dist/worktree/dependency-warmer.d.ts +53 -1
- package/dist/worktree/dependency-warmer.js +619 -11
- package/dist/worktree/index.d.ts +2 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -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 +16 -3
- package/dist/worktree/worktree.service.js +334 -54
- package/dist/worktree/worktree.types.d.ts +9 -1
- package/package.json +12 -7
- package/runtime/windows-job-supervisor.ps1 +722 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { RunOnceDispatchPacket } from "../session/session.types.js";
|
|
2
|
+
export interface SandboxContract {
|
|
3
|
+
readonly schemaVersion: 1;
|
|
4
|
+
readonly workingDirectory: "/workspace";
|
|
5
|
+
readonly commands: Readonly<Record<string, string>>;
|
|
6
|
+
readonly caches: readonly SandboxCache[];
|
|
7
|
+
readonly services: readonly SandboxService[];
|
|
8
|
+
readonly environment: readonly string[];
|
|
9
|
+
readonly artifacts: readonly string[];
|
|
10
|
+
readonly logs: readonly string[];
|
|
11
|
+
readonly browser: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SandboxCache {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly mode: "readonly" | "per_attempt";
|
|
16
|
+
}
|
|
17
|
+
export interface SandboxService {
|
|
18
|
+
readonly type: "postgres" | "redis" | "browser";
|
|
19
|
+
readonly port: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ValidatedSandboxContract {
|
|
22
|
+
readonly contract: SandboxContract;
|
|
23
|
+
readonly normalizedJson: string;
|
|
24
|
+
readonly digest: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SandboxPreflightResult {
|
|
27
|
+
readonly available: boolean;
|
|
28
|
+
readonly healthy: boolean;
|
|
29
|
+
readonly generation: number;
|
|
30
|
+
readonly sampledAt: string;
|
|
31
|
+
readonly runtimeVersion: string | null;
|
|
32
|
+
readonly maxConcurrentSandboxes: number;
|
|
33
|
+
readonly totalMemoryBytes: number;
|
|
34
|
+
/** Raw free memory reported by the host operating system. */
|
|
35
|
+
readonly rawHostAvailableMemoryBytes: number;
|
|
36
|
+
/** Raw free memory in Docker's capacity domain; null when that domain cannot be sampled. */
|
|
37
|
+
readonly rawDaemonAvailableMemoryBytes: number | null;
|
|
38
|
+
/** Conservative raw admission bound selected from every required memory domain. */
|
|
39
|
+
readonly rawAvailableMemoryBytes: number;
|
|
40
|
+
readonly availableMemoryBytes: number;
|
|
41
|
+
readonly hostReserveBytes: number;
|
|
42
|
+
readonly admissionMemoryBytes: number;
|
|
43
|
+
readonly availableCpuMillis: number;
|
|
44
|
+
readonly availableWorkspaceBytes: number;
|
|
45
|
+
readonly diskLow: boolean;
|
|
46
|
+
readonly unavailableCode: string | null;
|
|
47
|
+
readonly unavailableReason: string | null;
|
|
48
|
+
readonly features: SandboxFeatureMatrix;
|
|
49
|
+
}
|
|
50
|
+
export interface SandboxFeatureMatrix {
|
|
51
|
+
readonly rootless: boolean;
|
|
52
|
+
readonly userNamespaces: boolean;
|
|
53
|
+
readonly cgroupV2: boolean;
|
|
54
|
+
readonly seccomp: boolean;
|
|
55
|
+
readonly lsm: boolean;
|
|
56
|
+
readonly storageQuota: boolean;
|
|
57
|
+
readonly storagePolicy: boolean;
|
|
58
|
+
readonly compose: boolean;
|
|
59
|
+
readonly internalNetwork: boolean;
|
|
60
|
+
readonly buildkit: boolean;
|
|
61
|
+
readonly imagePolicy: boolean;
|
|
62
|
+
readonly securityProfile: boolean;
|
|
63
|
+
readonly egressProxy: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface SandboxReservation {
|
|
66
|
+
readonly dispatchAttemptId: string;
|
|
67
|
+
readonly leaseId: string;
|
|
68
|
+
readonly fencingToken: string;
|
|
69
|
+
readonly runnerInstanceId: string;
|
|
70
|
+
readonly expiresAt: string;
|
|
71
|
+
readonly reservedUntil: string;
|
|
72
|
+
readonly dispatch: RunOnceDispatchPacket;
|
|
73
|
+
readonly contract?: ValidatedSandboxContract;
|
|
74
|
+
readonly inputCommitSha?: string;
|
|
75
|
+
readonly sandboxConfigCommitSha?: string;
|
|
76
|
+
readonly imageDigest?: string;
|
|
77
|
+
}
|
|
78
|
+
export interface CommandResult {
|
|
79
|
+
readonly exitCode: number;
|
|
80
|
+
readonly stdout: string;
|
|
81
|
+
readonly stderr: string;
|
|
82
|
+
}
|
|
83
|
+
export interface CommandRunner {
|
|
84
|
+
run(command: string, args: readonly string[], options?: CommandOptions): Promise<CommandResult>;
|
|
85
|
+
}
|
|
86
|
+
export interface CommandOptions {
|
|
87
|
+
readonly cwd?: string;
|
|
88
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
89
|
+
readonly stdin?: string | Uint8Array;
|
|
90
|
+
readonly timeoutMs?: number;
|
|
91
|
+
readonly signal?: AbortSignal;
|
|
92
|
+
readonly maxOutputBytes?: number;
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { BridgeServiceConfig } from "../bridge/bridge.types.js";
|
|
2
|
+
import type { CommandRunner } from "./sandbox.types.js";
|
|
3
|
+
export interface SandboxSmokeRequest {
|
|
4
|
+
readonly repoPath: string;
|
|
5
|
+
readonly reportPath?: string;
|
|
6
|
+
readonly requirePinnedBase?: boolean;
|
|
7
|
+
readonly probeLoopbackPort?: boolean;
|
|
8
|
+
readonly onProgress?: (message: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface SandboxSmokeStage {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly status: "passed" | "failed";
|
|
13
|
+
readonly durationMs: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SandboxSmokeReport {
|
|
16
|
+
readonly schemaVersion: 1;
|
|
17
|
+
readonly kind: "forge_sandbox_smoke";
|
|
18
|
+
readonly status: "passed" | "failed";
|
|
19
|
+
readonly startedAt: string;
|
|
20
|
+
readonly completedAt: string;
|
|
21
|
+
readonly dispatchAttemptId: string;
|
|
22
|
+
readonly resourceLeaseId: string;
|
|
23
|
+
readonly repository: {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly contractDigest: string | null;
|
|
26
|
+
readonly dockerfileDigest: string | null;
|
|
27
|
+
readonly policyWarnings: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
readonly docker: {
|
|
30
|
+
readonly runtimeVersion: string | null;
|
|
31
|
+
readonly contextConfigured: boolean;
|
|
32
|
+
readonly capacity: number;
|
|
33
|
+
readonly availableMemoryBytes: number;
|
|
34
|
+
readonly availableCpuMillis: number;
|
|
35
|
+
readonly availableWorkspaceBytes: number;
|
|
36
|
+
};
|
|
37
|
+
readonly publishedPortProbe: {
|
|
38
|
+
readonly containerPort: number;
|
|
39
|
+
readonly hostIp: "127.0.0.1";
|
|
40
|
+
readonly hostPort: number;
|
|
41
|
+
} | null;
|
|
42
|
+
readonly stages: readonly SandboxSmokeStage[];
|
|
43
|
+
readonly cleanup: {
|
|
44
|
+
readonly verified: boolean;
|
|
45
|
+
readonly removed: readonly string[];
|
|
46
|
+
readonly residual: {
|
|
47
|
+
readonly containers: number;
|
|
48
|
+
readonly networks: number;
|
|
49
|
+
readonly volumes: number;
|
|
50
|
+
readonly images: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
readonly failure: {
|
|
54
|
+
readonly code: string;
|
|
55
|
+
readonly stage: string;
|
|
56
|
+
readonly message: string;
|
|
57
|
+
} | null;
|
|
58
|
+
}
|
|
59
|
+
export interface SandboxSmokeResult {
|
|
60
|
+
readonly report: SandboxSmokeReport;
|
|
61
|
+
readonly reportPath: string;
|
|
62
|
+
}
|
|
63
|
+
export declare class DockerSandboxSmoke {
|
|
64
|
+
private readonly config;
|
|
65
|
+
private readonly commands;
|
|
66
|
+
constructor(config: BridgeServiceConfig, commands?: CommandRunner);
|
|
67
|
+
run(request: SandboxSmokeRequest): Promise<SandboxSmokeResult>;
|
|
68
|
+
private createPreflight;
|
|
69
|
+
private stage;
|
|
70
|
+
private labelArgs;
|
|
71
|
+
private dockerArgs;
|
|
72
|
+
private requireDocker;
|
|
73
|
+
private cleanup;
|
|
74
|
+
private inventoryCount;
|
|
75
|
+
}
|
|
76
|
+
export declare function parseDockerAssignedLoopbackPort(output: string): SandboxSmokeReport["publishedPortProbe"];
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { redactSecrets } from "../shared/redaction/redaction.js";
|
|
5
|
+
import { BoundedCommandRunner } from "./command-runner.js";
|
|
6
|
+
import { validateDockerfilePolicy, validateSandboxContract } from "./contract.js";
|
|
7
|
+
import { DockerPreflight } from "./preflight.js";
|
|
8
|
+
const managedLabel = "dev.forge.managed";
|
|
9
|
+
const smokeLabel = "dev.forge.smoke";
|
|
10
|
+
const attemptLabel = "dev.forge.dispatch-attempt-id";
|
|
11
|
+
const resourceLeaseLabel = "dev.forge.resource-lease-id";
|
|
12
|
+
export class DockerSandboxSmoke {
|
|
13
|
+
config;
|
|
14
|
+
commands;
|
|
15
|
+
constructor(config, commands = new BoundedCommandRunner()) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.commands = commands;
|
|
18
|
+
}
|
|
19
|
+
async run(request) {
|
|
20
|
+
const startedAt = new Date();
|
|
21
|
+
const dispatchAttemptId = randomUUID();
|
|
22
|
+
const resourceLeaseId = randomUUID();
|
|
23
|
+
const token = dispatchAttemptId.replaceAll("-", "").slice(0, 20);
|
|
24
|
+
const resources = {
|
|
25
|
+
image: `forge-sandbox-smoke:${token}`,
|
|
26
|
+
container: `forge-smoke-agent-${token}`,
|
|
27
|
+
network: `forge-smoke-network-${token}`,
|
|
28
|
+
volume: `forge-smoke-workspace-${token}`,
|
|
29
|
+
portFixtureContainer: `forge-smoke-port-${token}`,
|
|
30
|
+
portFixtureNetwork: `forge-smoke-port-network-${token}`,
|
|
31
|
+
};
|
|
32
|
+
const reportPath = path.resolve(request.reportPath ?? path.join(this.config.sandboxStateRoot, "smoke-reports", `${startedAt.toISOString().replaceAll(":", "-")}-${token}.json`));
|
|
33
|
+
const evidence = {
|
|
34
|
+
contractDigest: null,
|
|
35
|
+
dockerfileDigest: null,
|
|
36
|
+
policyWarnings: [],
|
|
37
|
+
preflight: null,
|
|
38
|
+
stages: [],
|
|
39
|
+
removed: [],
|
|
40
|
+
residual: { containers: -1, networks: -1, volumes: -1, images: -1 },
|
|
41
|
+
cleanupVerified: false,
|
|
42
|
+
publishedPortProbe: null,
|
|
43
|
+
failure: null,
|
|
44
|
+
};
|
|
45
|
+
const preflight = this.createPreflight();
|
|
46
|
+
try {
|
|
47
|
+
await this.stage("doctor", evidence, request, async () => {
|
|
48
|
+
const forgePath = path.join(path.resolve(request.repoPath), ".forge");
|
|
49
|
+
const contractSource = await fs.readFile(path.join(forgePath, "forge.sandbox.yml"), "utf8");
|
|
50
|
+
const dockerfile = await fs.readFile(path.join(forgePath, "Dockerfile"), "utf8");
|
|
51
|
+
evidence.contractDigest = validateSandboxContract(contractSource).digest;
|
|
52
|
+
evidence.dockerfileDigest = `sha256:${createHash("sha256").update(dockerfile).digest("hex")}`;
|
|
53
|
+
evidence.policyWarnings = [...validateDockerfilePolicy(dockerfile, request.requirePinnedBase ?? false)];
|
|
54
|
+
evidence.preflight = await preflight.refresh();
|
|
55
|
+
if (!evidence.preflight.healthy) {
|
|
56
|
+
throw new SmokeFailure(evidence.preflight.unavailableCode ?? "sandbox_preflight_failed", "doctor", evidence.preflight.unavailableReason ?? "Docker preflight did not report a healthy runtime.");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
await this.stage("build_image", evidence, request, async () => {
|
|
60
|
+
const dockerfilePath = path.join(path.resolve(request.repoPath), ".forge", "Dockerfile");
|
|
61
|
+
await this.requireDocker("sandbox_smoke_build_failed", "build_image", [
|
|
62
|
+
"build",
|
|
63
|
+
"--tag", resources.image,
|
|
64
|
+
...this.labelArgs(dispatchAttemptId, resourceLeaseId),
|
|
65
|
+
"--file", dockerfilePath,
|
|
66
|
+
path.resolve(request.repoPath),
|
|
67
|
+
], { timeoutMs: 15 * 60_000, maxOutputBytes: 64 * 1024 });
|
|
68
|
+
});
|
|
69
|
+
await this.stage("create_resources", evidence, request, async () => {
|
|
70
|
+
await this.requireDocker("sandbox_smoke_network_failed", "create_resources", [
|
|
71
|
+
"network", "create", "--internal",
|
|
72
|
+
...this.labelArgs(dispatchAttemptId, resourceLeaseId),
|
|
73
|
+
resources.network,
|
|
74
|
+
]);
|
|
75
|
+
await this.requireDocker("sandbox_smoke_volume_failed", "create_resources", [
|
|
76
|
+
"volume", "create",
|
|
77
|
+
...this.labelArgs(dispatchAttemptId, resourceLeaseId),
|
|
78
|
+
resources.volume,
|
|
79
|
+
]);
|
|
80
|
+
await this.requireDocker("sandbox_smoke_container_failed", "create_resources", [
|
|
81
|
+
"create",
|
|
82
|
+
"--name", resources.container,
|
|
83
|
+
...this.labelArgs(dispatchAttemptId, resourceLeaseId),
|
|
84
|
+
"--network", resources.network,
|
|
85
|
+
"--read-only",
|
|
86
|
+
"--cap-drop", "ALL",
|
|
87
|
+
"--security-opt", "no-new-privileges",
|
|
88
|
+
"--pids-limit", "64",
|
|
89
|
+
"--memory", String(256 * 1024 ** 2),
|
|
90
|
+
"--memory-swap", String(256 * 1024 ** 2),
|
|
91
|
+
"--cpus", "0.5",
|
|
92
|
+
"--tmpfs", "/tmp:rw,nosuid,nodev,size=64m,uid=10001,gid=10001",
|
|
93
|
+
"--user", "10001:10001",
|
|
94
|
+
"--workdir", "/workspace",
|
|
95
|
+
"--mount", `type=volume,src=${resources.volume},dst=/workspace,volume-nocopy`,
|
|
96
|
+
"--env", `FORGE_SMOKE_ATTEMPT_ID=${dispatchAttemptId}`,
|
|
97
|
+
"--env", `FORGE_RESOURCE_LEASE_ID=${resourceLeaseId}`,
|
|
98
|
+
"--entrypoint", "/bin/sh",
|
|
99
|
+
resources.image,
|
|
100
|
+
"-lc",
|
|
101
|
+
`set -eu; test "$PWD" = /workspace; test "$FORGE_SMOKE_ATTEMPT_ID" = "${dispatchAttemptId}"; test "$FORGE_RESOURCE_LEASE_ID" = "${resourceLeaseId}"; test ! -S /var/run/docker.sock; printf 'FORGE_SANDBOX_SMOKE_OK\\n'`,
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
await this.stage("verify_isolation", evidence, request, async () => {
|
|
105
|
+
const inspect = await this.requireDocker("sandbox_smoke_inspect_failed", "verify_isolation", [
|
|
106
|
+
"inspect", "--format",
|
|
107
|
+
"{{json .Mounts}}|{{.Config.User}}|{{.HostConfig.ReadonlyRootfs}}|{{.HostConfig.Privileged}}|{{json .HostConfig.CapDrop}}|{{json .HostConfig.PortBindings}}",
|
|
108
|
+
resources.container,
|
|
109
|
+
]);
|
|
110
|
+
const [mountsJson, user, readonlyRootfs, privileged, capDropJson, portBindingsJson] = inspect.stdout.trim().split("|");
|
|
111
|
+
const mounts = JSON.parse(mountsJson ?? "[]");
|
|
112
|
+
const capDrop = JSON.parse(capDropJson ?? "[]");
|
|
113
|
+
const workspaceMounted = mounts.some((mount) => mount.Destination === "/workspace" && mount.Name === resources.volume);
|
|
114
|
+
if (!workspaceMounted || user !== "10001:10001" || readonlyRootfs !== "true" || privileged !== "false"
|
|
115
|
+
|| !capDrop.includes("ALL") || !["null", "{}"].includes(portBindingsJson ?? "")) {
|
|
116
|
+
throw new SmokeFailure("sandbox_smoke_isolation_failed", "verify_isolation", "The disposable container did not match the hardened smoke profile.");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
await this.stage("execute", evidence, request, async () => {
|
|
120
|
+
const result = await this.requireDocker("sandbox_smoke_execution_failed", "execute", ["start", "--attach", resources.container]);
|
|
121
|
+
if (!result.stdout.includes("FORGE_SANDBOX_SMOKE_OK")) {
|
|
122
|
+
throw new SmokeFailure("sandbox_smoke_marker_missing", "execute", "The disposable container exited without its success marker.");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (request.probeLoopbackPort) {
|
|
126
|
+
await this.stage("discover_loopback_port", evidence, request, async () => {
|
|
127
|
+
await this.requireDocker("sandbox_smoke_port_discovery_failed", "discover_loopback_port", [
|
|
128
|
+
"network", "create",
|
|
129
|
+
...this.labelArgs(dispatchAttemptId, resourceLeaseId),
|
|
130
|
+
resources.portFixtureNetwork,
|
|
131
|
+
]);
|
|
132
|
+
await this.requireDocker("sandbox_smoke_port_discovery_failed", "discover_loopback_port", [
|
|
133
|
+
"create",
|
|
134
|
+
"--name", resources.portFixtureContainer,
|
|
135
|
+
...this.labelArgs(dispatchAttemptId, resourceLeaseId),
|
|
136
|
+
"--network", resources.portFixtureNetwork,
|
|
137
|
+
"--publish", "127.0.0.1::8080/tcp",
|
|
138
|
+
"--read-only",
|
|
139
|
+
"--cap-drop", "ALL",
|
|
140
|
+
"--security-opt", "no-new-privileges",
|
|
141
|
+
"--pids-limit", "32",
|
|
142
|
+
"--memory", String(128 * 1024 ** 2),
|
|
143
|
+
"--memory-swap", String(128 * 1024 ** 2),
|
|
144
|
+
"--cpus", "0.25",
|
|
145
|
+
"--tmpfs", "/tmp:rw,nosuid,nodev,size=16m,uid=10001,gid=10001",
|
|
146
|
+
"--user", "10001:10001",
|
|
147
|
+
"--entrypoint", "/bin/sh",
|
|
148
|
+
resources.image,
|
|
149
|
+
"-lc", "exec sleep 300",
|
|
150
|
+
]);
|
|
151
|
+
await this.requireDocker("sandbox_smoke_port_discovery_failed", "discover_loopback_port", [
|
|
152
|
+
"start", resources.portFixtureContainer,
|
|
153
|
+
]);
|
|
154
|
+
const inspect = await this.requireDocker("sandbox_smoke_port_discovery_failed", "discover_loopback_port", ["inspect", "--format", "{{json .NetworkSettings.Ports}}", resources.portFixtureContainer]);
|
|
155
|
+
evidence.publishedPortProbe = parseDockerAssignedLoopbackPort(inspect.stdout);
|
|
156
|
+
if (!evidence.publishedPortProbe) {
|
|
157
|
+
throw new SmokeFailure("sandbox_smoke_port_discovery_failed", "discover_loopback_port", "Docker did not assign exactly one valid loopback host port to the fixture.");
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
evidence.failure = failureFrom(error);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await this.cleanup(resources, dispatchAttemptId, evidence, request);
|
|
167
|
+
await preflight.release();
|
|
168
|
+
}
|
|
169
|
+
if (!evidence.cleanupVerified && !evidence.failure) {
|
|
170
|
+
evidence.failure = {
|
|
171
|
+
code: "sandbox_smoke_cleanup_failed",
|
|
172
|
+
stage: "cleanup",
|
|
173
|
+
message: "Exact-label cleanup verification found residual or unreadable Docker resources.",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const completedAt = new Date();
|
|
177
|
+
const report = redactSecrets({
|
|
178
|
+
schemaVersion: 1,
|
|
179
|
+
kind: "forge_sandbox_smoke",
|
|
180
|
+
status: evidence.failure || !evidence.cleanupVerified ? "failed" : "passed",
|
|
181
|
+
startedAt: startedAt.toISOString(),
|
|
182
|
+
completedAt: completedAt.toISOString(),
|
|
183
|
+
dispatchAttemptId,
|
|
184
|
+
resourceLeaseId,
|
|
185
|
+
repository: {
|
|
186
|
+
name: path.basename(path.resolve(request.repoPath)),
|
|
187
|
+
contractDigest: evidence.contractDigest,
|
|
188
|
+
dockerfileDigest: evidence.dockerfileDigest,
|
|
189
|
+
policyWarnings: evidence.policyWarnings,
|
|
190
|
+
},
|
|
191
|
+
docker: {
|
|
192
|
+
runtimeVersion: evidence.preflight?.runtimeVersion ?? null,
|
|
193
|
+
contextConfigured: this.config.dockerContext.length > 0,
|
|
194
|
+
capacity: evidence.preflight?.maxConcurrentSandboxes ?? 0,
|
|
195
|
+
availableMemoryBytes: evidence.preflight?.availableMemoryBytes ?? 0,
|
|
196
|
+
availableCpuMillis: evidence.preflight?.availableCpuMillis ?? 0,
|
|
197
|
+
availableWorkspaceBytes: evidence.preflight?.availableWorkspaceBytes ?? 0,
|
|
198
|
+
},
|
|
199
|
+
publishedPortProbe: evidence.publishedPortProbe,
|
|
200
|
+
stages: evidence.stages,
|
|
201
|
+
cleanup: {
|
|
202
|
+
verified: evidence.cleanupVerified,
|
|
203
|
+
removed: evidence.removed,
|
|
204
|
+
residual: evidence.residual,
|
|
205
|
+
},
|
|
206
|
+
failure: evidence.failure,
|
|
207
|
+
});
|
|
208
|
+
await writeReport(reportPath, report);
|
|
209
|
+
return { report, reportPath };
|
|
210
|
+
}
|
|
211
|
+
createPreflight() {
|
|
212
|
+
return new DockerPreflight({
|
|
213
|
+
dockerExecutable: this.config.dockerExecutable,
|
|
214
|
+
dockerContext: this.config.dockerContext,
|
|
215
|
+
stateRoot: this.config.sandboxStateRoot,
|
|
216
|
+
maxConcurrentSandboxes: this.config.maxConcurrentSandboxes,
|
|
217
|
+
diskHighWatermarkPercent: this.config.diskHighWatermarkPercent,
|
|
218
|
+
diskLowWatermarkPercent: this.config.diskLowWatermarkPercent,
|
|
219
|
+
minimumWorkspaceBytes: this.config.minimumWorkspaceBytes,
|
|
220
|
+
hostMemoryReserveBytes: this.config.sandboxHostMemoryReserveBytes,
|
|
221
|
+
defaultWorkerMemoryLimitBytes: this.config.defaultSandboxWorkerMemoryLimitBytes,
|
|
222
|
+
defaultServiceMemoryLimitBytes: this.config.defaultSandboxServiceMemoryLimitBytes,
|
|
223
|
+
runtimeOverheadBytes: this.config.sandboxRuntimeOverheadBytes,
|
|
224
|
+
requireRootless: this.config.requireRootlessDocker,
|
|
225
|
+
}, this.commands);
|
|
226
|
+
}
|
|
227
|
+
async stage(name, evidence, request, action) {
|
|
228
|
+
request.onProgress?.(name);
|
|
229
|
+
const started = Date.now();
|
|
230
|
+
try {
|
|
231
|
+
await action();
|
|
232
|
+
evidence.stages.push({ name, status: "passed", durationMs: Date.now() - started });
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
evidence.stages.push({ name, status: "failed", durationMs: Date.now() - started });
|
|
236
|
+
if (error instanceof SmokeFailure)
|
|
237
|
+
throw error;
|
|
238
|
+
if (name === "doctor" && error instanceof Error && error.message.includes("docker_context_owned")) {
|
|
239
|
+
throw new SmokeFailure("sandbox_smoke_context_owned", name, "The Docker context is owned by a connected Forge bridge. Stop the bridge and retry.");
|
|
240
|
+
}
|
|
241
|
+
throw new SmokeFailure(`sandbox_smoke_${name}_failed`, name, name === "doctor"
|
|
242
|
+
? "The repository contract, Dockerfile, or Docker preflight check failed."
|
|
243
|
+
: `The ${name.replaceAll("_", " ")} stage failed.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
labelArgs(dispatchAttemptId, resourceLeaseId) {
|
|
247
|
+
return [
|
|
248
|
+
"--label", `${managedLabel}=true`,
|
|
249
|
+
"--label", `${smokeLabel}=true`,
|
|
250
|
+
"--label", `${attemptLabel}=${dispatchAttemptId}`,
|
|
251
|
+
"--label", `${resourceLeaseLabel}=${resourceLeaseId}`,
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
dockerArgs(args) {
|
|
255
|
+
return this.config.dockerContext ? ["--context", this.config.dockerContext, ...args] : [...args];
|
|
256
|
+
}
|
|
257
|
+
async requireDocker(code, stage, args, options = {}) {
|
|
258
|
+
let result;
|
|
259
|
+
try {
|
|
260
|
+
result = await this.commands.run(this.config.dockerExecutable, this.dockerArgs(args), {
|
|
261
|
+
timeoutMs: options.timeoutMs ?? 60_000,
|
|
262
|
+
maxOutputBytes: options.maxOutputBytes ?? 16 * 1024,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw new SmokeFailure(code, stage, "Docker could not execute the requested smoke stage.");
|
|
267
|
+
}
|
|
268
|
+
if (result.exitCode !== 0) {
|
|
269
|
+
throw new SmokeFailure(code, stage, `Docker exited with code ${result.exitCode} during the smoke stage.`);
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
async cleanup(resources, dispatchAttemptId, evidence, request) {
|
|
274
|
+
const started = Date.now();
|
|
275
|
+
request.onProgress?.("cleanup");
|
|
276
|
+
const removals = [
|
|
277
|
+
["portFixtureContainer", ["rm", "--force", resources.portFixtureContainer]],
|
|
278
|
+
["container", ["rm", "--force", resources.container]],
|
|
279
|
+
["portFixtureNetwork", ["network", "rm", resources.portFixtureNetwork]],
|
|
280
|
+
["network", ["network", "rm", resources.network]],
|
|
281
|
+
["volume", ["volume", "rm", "--force", resources.volume]],
|
|
282
|
+
["image", ["image", "rm", "--force", resources.image]],
|
|
283
|
+
];
|
|
284
|
+
for (const [kind, args] of removals) {
|
|
285
|
+
try {
|
|
286
|
+
const result = await this.commands.run(this.config.dockerExecutable, this.dockerArgs(args), {
|
|
287
|
+
timeoutMs: 60_000,
|
|
288
|
+
maxOutputBytes: 8 * 1024,
|
|
289
|
+
});
|
|
290
|
+
if (result.exitCode === 0)
|
|
291
|
+
evidence.removed.push(kind);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Exact-label inventory below is authoritative for cleanup verification.
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
evidence.residual = {
|
|
298
|
+
containers: await this.inventoryCount(["ps", "--all", "--quiet", "--filter", `label=${attemptLabel}=${dispatchAttemptId}`]),
|
|
299
|
+
networks: await this.inventoryCount(["network", "ls", "--quiet", "--filter", `label=${attemptLabel}=${dispatchAttemptId}`]),
|
|
300
|
+
volumes: await this.inventoryCount(["volume", "ls", "--quiet", "--filter", `label=${attemptLabel}=${dispatchAttemptId}`]),
|
|
301
|
+
images: await this.inventoryCount(["image", "ls", "--quiet", "--filter", `label=${attemptLabel}=${dispatchAttemptId}`]),
|
|
302
|
+
};
|
|
303
|
+
evidence.cleanupVerified = Object.values(evidence.residual).every((count) => count === 0);
|
|
304
|
+
evidence.stages.push({
|
|
305
|
+
name: "cleanup",
|
|
306
|
+
status: evidence.cleanupVerified ? "passed" : "failed",
|
|
307
|
+
durationMs: Date.now() - started,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async inventoryCount(args) {
|
|
311
|
+
try {
|
|
312
|
+
const result = await this.commands.run(this.config.dockerExecutable, this.dockerArgs(args), {
|
|
313
|
+
timeoutMs: 30_000,
|
|
314
|
+
maxOutputBytes: 64 * 1024,
|
|
315
|
+
});
|
|
316
|
+
if (result.exitCode !== 0)
|
|
317
|
+
return -1;
|
|
318
|
+
return result.stdout.split(/\r?\n/u).filter((line) => line.trim().length > 0).length;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return -1;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
export function parseDockerAssignedLoopbackPort(output) {
|
|
326
|
+
try {
|
|
327
|
+
const parsed = JSON.parse(output.trim());
|
|
328
|
+
const bindings = Array.isArray(parsed)
|
|
329
|
+
? parsed
|
|
330
|
+
: typeof parsed === "object" && parsed !== null
|
|
331
|
+
? parsed["8080/tcp"]
|
|
332
|
+
: null;
|
|
333
|
+
if (!Array.isArray(bindings) || bindings.length !== 1)
|
|
334
|
+
return null;
|
|
335
|
+
const binding = bindings[0];
|
|
336
|
+
if (binding.HostIp !== "127.0.0.1" || typeof binding.HostPort !== "string" || !/^\d{1,5}$/u.test(binding.HostPort)) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const hostPort = Number(binding.HostPort);
|
|
340
|
+
return hostPort >= 1 && hostPort <= 65535
|
|
341
|
+
? { containerPort: 8080, hostIp: "127.0.0.1", hostPort }
|
|
342
|
+
: null;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
class SmokeFailure extends Error {
|
|
349
|
+
code;
|
|
350
|
+
stage;
|
|
351
|
+
constructor(code, stage, message) {
|
|
352
|
+
super(message);
|
|
353
|
+
this.code = code;
|
|
354
|
+
this.stage = stage;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function failureFrom(error) {
|
|
358
|
+
if (error instanceof SmokeFailure) {
|
|
359
|
+
return { code: error.code, stage: error.stage, message: error.message };
|
|
360
|
+
}
|
|
361
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string") {
|
|
362
|
+
return {
|
|
363
|
+
code: `sandbox_smoke_${String(error.code).toLowerCase()}`,
|
|
364
|
+
stage: "doctor",
|
|
365
|
+
message: "The repository sandbox contract or Dockerfile could not be read or validated.",
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
code: "sandbox_smoke_failed",
|
|
370
|
+
stage: "unknown",
|
|
371
|
+
message: "The disposable sandbox smoke attempt failed.",
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
async function writeReport(reportPath, report) {
|
|
375
|
+
await fs.mkdir(path.dirname(reportPath), { recursive: true, mode: 0o700 });
|
|
376
|
+
const temporary = `${reportPath}.${randomUUID()}.tmp`;
|
|
377
|
+
await fs.writeFile(temporary, `${JSON.stringify(report, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
378
|
+
await fs.rename(temporary, reportPath);
|
|
379
|
+
await fs.chmod(reportPath, 0o600).catch(() => undefined);
|
|
380
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MonotonicLeaseDeadline } from "./lease-watchdog.js";
|
|
2
|
+
export type SandboxTerminationCode = "sandbox_lease_expired" | "sandbox_wall_timeout" | "sandbox_idle_timeout";
|
|
3
|
+
export declare class WorkerDeadlineSupervisor {
|
|
4
|
+
readonly lease: MonotonicLeaseDeadline;
|
|
5
|
+
readonly hardWallMs: number;
|
|
6
|
+
readonly idleMs: number;
|
|
7
|
+
private readonly startedAtMs;
|
|
8
|
+
private lastActivityMs;
|
|
9
|
+
constructor(lease: MonotonicLeaseDeadline, hardWallMs: number, idleMs: number, startedAtMs?: number);
|
|
10
|
+
touch(nowMs?: number): void;
|
|
11
|
+
termination(nowMs?: number): SandboxTerminationCode | null;
|
|
12
|
+
}
|
|
13
|
+
export declare function terminationMessage(code: SandboxTerminationCode): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class WorkerDeadlineSupervisor {
|
|
2
|
+
lease;
|
|
3
|
+
hardWallMs;
|
|
4
|
+
idleMs;
|
|
5
|
+
startedAtMs;
|
|
6
|
+
lastActivityMs;
|
|
7
|
+
constructor(lease, hardWallMs, idleMs, startedAtMs = performance.now()) {
|
|
8
|
+
this.lease = lease;
|
|
9
|
+
this.hardWallMs = hardWallMs;
|
|
10
|
+
this.idleMs = idleMs;
|
|
11
|
+
this.startedAtMs = startedAtMs;
|
|
12
|
+
if (!Number.isFinite(hardWallMs) || hardWallMs <= 0 || !Number.isFinite(idleMs) || idleMs <= 0) {
|
|
13
|
+
throw new Error("Sandbox watchdog limits must be positive.");
|
|
14
|
+
}
|
|
15
|
+
this.lastActivityMs = startedAtMs;
|
|
16
|
+
}
|
|
17
|
+
touch(nowMs = performance.now()) {
|
|
18
|
+
this.lastActivityMs = Math.max(this.lastActivityMs, nowMs);
|
|
19
|
+
}
|
|
20
|
+
termination(nowMs = performance.now()) {
|
|
21
|
+
if (this.lease.expired(nowMs))
|
|
22
|
+
return "sandbox_lease_expired";
|
|
23
|
+
if (nowMs - this.startedAtMs >= this.hardWallMs)
|
|
24
|
+
return "sandbox_wall_timeout";
|
|
25
|
+
if (nowMs - this.lastActivityMs >= this.idleMs)
|
|
26
|
+
return "sandbox_idle_timeout";
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function terminationMessage(code) {
|
|
31
|
+
switch (code) {
|
|
32
|
+
case "sandbox_lease_expired": return "The durable execution lease expired inside the sandbox.";
|
|
33
|
+
case "sandbox_wall_timeout": return "The sandbox reached its hard wall-clock limit.";
|
|
34
|
+
case "sandbox_idle_timeout": return "The provider produced no process activity before the sandbox idle limit.";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|