@loucompanion/forge-bridge 0.1.3-dev.b6be5e7f9668 → 0.1.3-dev.ba7d36726eda
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 +78 -0
- package/dist/bridge/bridge.base.d.ts +5 -1
- package/dist/bridge/bridge.service.d.ts +52 -3
- package/dist/bridge/bridge.service.js +796 -22
- package/dist/bridge/bridge.types.d.ts +135 -1
- package/dist/cleanup/cleanup.service.d.ts +13 -2
- package/dist/cleanup/cleanup.service.js +435 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +42 -2
- package/dist/cli/cli.service.js +356 -205
- package/dist/cli/local-config.d.ts +6 -0
- package/dist/cli/local-config.js +45 -2
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +15 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +50 -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 +74 -0
- package/dist/runtime/attempt-process-supervisor.js +290 -0
- package/dist/runtime/host-listener-inspector.d.ts +28 -0
- package/dist/runtime/host-listener-inspector.js +185 -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 +39 -0
- package/dist/runtime/host-runtime-contract.js +295 -0
- package/dist/runtime/host-runtime-lease.d.ts +122 -0
- package/dist/runtime/host-runtime-lease.js +1241 -0
- package/dist/runtime/host-runtime-promotion.d.ts +21 -0
- package/dist/runtime/host-runtime-promotion.js +128 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +44 -0
- package/dist/runtime/node-host-runtime-adapter.js +205 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +136 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -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 +45 -0
- package/dist/sandbox/preflight.js +300 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +119 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +86 -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 +67 -0
- package/dist/session/phase-completion.artifacts.d.ts +10 -0
- package/dist/session/phase-completion.artifacts.js +620 -0
- package/dist/session/phase-completion.d.ts +32 -44
- package/dist/session/phase-completion.helpers.d.ts +112 -0
- package/dist/session/phase-completion.helpers.js +665 -0
- package/dist/session/phase-completion.js +28 -3485
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +220 -0
- package/dist/session/phase-completion.runtime.d.ts +36 -0
- package/dist/session/phase-completion.runtime.js +611 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +707 -0
- package/dist/session/phase-completion.validation.d.ts +34 -0
- package/dist/session/phase-completion.validation.js +724 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +778 -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 +329 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +34 -0
- package/dist/session/provider-cli/provider-cli.process.js +316 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +47 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +573 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +65 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +41 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +9 -53
- package/dist/session/provider-cli/provider-cli.service.js +182 -2536
- package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +20 -0
- package/dist/session/provider-cli/provider-output-state.js +445 -0
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +23 -0
- package/dist/session/session.base.d.ts +4 -0
- package/dist/session/session.service.d.ts +6 -1
- package/dist/session/session.service.js +202 -53
- package/dist/session/session.types.d.ts +123 -1
- package/dist/session/session.types.js +301 -3
- package/dist/shared/path/path.js +3 -1
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +40 -0
- package/dist/shared/process/host-process-supervisor.js +195 -0
- 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 +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +10 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +39 -6
- package/dist/worktree/dependency-warmer.d.ts +11 -0
- package/dist/worktree/dependency-warmer.js +45 -1
- package/dist/worktree/index.d.ts +1 -0
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +8 -3
- package/dist/worktree/worktree.service.js +89 -52
- package/dist/worktree/worktree.types.d.ts +3 -1
- package/package.json +6 -3
- package/runtime/windows-job-supervisor.ps1 +287 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LoadedHostRuntimeContract } from "./host-runtime-contract.js";
|
|
2
|
+
export interface HostParallelPromotionRecord {
|
|
3
|
+
readonly schemaVersion: 1;
|
|
4
|
+
readonly repoId: string;
|
|
5
|
+
readonly runtimeContractFingerprint: string;
|
|
6
|
+
readonly observedAt: string;
|
|
7
|
+
readonly observedDispatchAttemptId: string;
|
|
8
|
+
readonly verifiedPortNames: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
/** Persists only observed, contract-bound host parallel-safety evidence. */
|
|
11
|
+
export declare class HostParallelPromotionStore {
|
|
12
|
+
private readonly stateRoot;
|
|
13
|
+
constructor(stateRoot: string);
|
|
14
|
+
load(repoId: string): Promise<HostParallelPromotionRecord | null>;
|
|
15
|
+
observe(repoId: string, runtimeContract: LoadedHostRuntimeContract, dispatchAttemptId: string, verifiedPortNames: readonly string[]): Promise<HostParallelPromotionRecord>;
|
|
16
|
+
invalidate(repoId: string): Promise<void>;
|
|
17
|
+
private recordPath;
|
|
18
|
+
}
|
|
19
|
+
export declare function hostRuntimeContractFingerprint(runtimeContract: LoadedHostRuntimeContract): string;
|
|
20
|
+
export declare function hostRuntimeParallelEligible(runtimeContract: LoadedHostRuntimeContract): boolean;
|
|
21
|
+
export declare function configuredLaunchPortNames(runtimeContract: LoadedHostRuntimeContract): readonly string[];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Persists only observed, contract-bound host parallel-safety evidence. */
|
|
5
|
+
export class HostParallelPromotionStore {
|
|
6
|
+
stateRoot;
|
|
7
|
+
constructor(stateRoot) {
|
|
8
|
+
this.stateRoot = stateRoot;
|
|
9
|
+
}
|
|
10
|
+
async load(repoId) {
|
|
11
|
+
const normalizedRepoId = normalizeRepoId(repoId);
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(await fs.readFile(this.recordPath(normalizedRepoId), "utf8"));
|
|
14
|
+
return validRecord(parsed, normalizedRepoId) ? parsed : null;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError)
|
|
18
|
+
return null;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async observe(repoId, runtimeContract, dispatchAttemptId, verifiedPortNames) {
|
|
23
|
+
if (!hostRuntimeParallelEligible(runtimeContract)) {
|
|
24
|
+
throw new Error("host_parallel_promotion_ineligible: the runtime contract is not eligible for parallel host execution.");
|
|
25
|
+
}
|
|
26
|
+
const requiredPorts = configuredLaunchPortNames(runtimeContract);
|
|
27
|
+
const observedPorts = [...new Set(verifiedPortNames)].sort((left, right) => left.localeCompare(right));
|
|
28
|
+
if (requiredPorts.length !== observedPorts.length
|
|
29
|
+
|| requiredPorts.some((name, index) => name !== observedPorts[index])) {
|
|
30
|
+
throw new Error("host_parallel_promotion_unverified: every configured runtime listener must pass its assigned-port probe.");
|
|
31
|
+
}
|
|
32
|
+
const normalizedRepoId = normalizeRepoId(repoId);
|
|
33
|
+
const record = {
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
repoId: normalizedRepoId,
|
|
36
|
+
runtimeContractFingerprint: hostRuntimeContractFingerprint(runtimeContract),
|
|
37
|
+
observedAt: new Date().toISOString(),
|
|
38
|
+
observedDispatchAttemptId: normalizedUuid(dispatchAttemptId, "dispatch attempt"),
|
|
39
|
+
verifiedPortNames: observedPorts,
|
|
40
|
+
};
|
|
41
|
+
const target = this.recordPath(normalizedRepoId);
|
|
42
|
+
const root = path.dirname(target);
|
|
43
|
+
await ensurePrivateDirectory(root);
|
|
44
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
45
|
+
try {
|
|
46
|
+
const handle = await fs.open(temporary, "wx", 0o600);
|
|
47
|
+
try {
|
|
48
|
+
await handle.writeFile(`${JSON.stringify(record)}\n`, "utf8");
|
|
49
|
+
await handle.sync();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await handle.close();
|
|
53
|
+
}
|
|
54
|
+
await fs.rename(temporary, target);
|
|
55
|
+
return record;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async invalidate(repoId) {
|
|
63
|
+
await fs.rm(this.recordPath(normalizeRepoId(repoId)), { force: true });
|
|
64
|
+
}
|
|
65
|
+
recordPath(normalizedRepoId) {
|
|
66
|
+
const key = createHash("sha256").update(normalizedRepoId).digest("hex");
|
|
67
|
+
return path.join(this.stateRoot, "runtime-promotion", "host", `${key}.json`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function hostRuntimeContractFingerprint(runtimeContract) {
|
|
71
|
+
const canonical = JSON.stringify({
|
|
72
|
+
schemaVersion: runtimeContract.contract.schemaVersion,
|
|
73
|
+
runtime: runtimeContract.contract.runtime,
|
|
74
|
+
});
|
|
75
|
+
return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
|
|
76
|
+
}
|
|
77
|
+
export function hostRuntimeParallelEligible(runtimeContract) {
|
|
78
|
+
const runtime = runtimeContract.contract.runtime;
|
|
79
|
+
return runtimeContract.source === "project"
|
|
80
|
+
&& runtime.hostConcurrency === "parallel"
|
|
81
|
+
&& runtime.browserProfile === "isolated"
|
|
82
|
+
&& runtime.singletonResources.length === 0
|
|
83
|
+
&& runtime.ports.length === 1
|
|
84
|
+
&& runtime.ports[0]?.adapter !== null
|
|
85
|
+
&& runtime.ports[0]?.launch !== null;
|
|
86
|
+
}
|
|
87
|
+
export function configuredLaunchPortNames(runtimeContract) {
|
|
88
|
+
return runtimeContract.contract.runtime.ports
|
|
89
|
+
.filter(port => port.launch !== null)
|
|
90
|
+
.map(port => port.name)
|
|
91
|
+
.sort((left, right) => left.localeCompare(right));
|
|
92
|
+
}
|
|
93
|
+
function validRecord(value, repoId) {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
95
|
+
return false;
|
|
96
|
+
const record = value;
|
|
97
|
+
return record.schemaVersion === 1
|
|
98
|
+
&& record.repoId === repoId
|
|
99
|
+
&& /^sha256:[0-9a-f]{64}$/u.test(record.runtimeContractFingerprint ?? "")
|
|
100
|
+
&& Number.isFinite(Date.parse(record.observedAt ?? ""))
|
|
101
|
+
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(record.observedDispatchAttemptId ?? "")
|
|
102
|
+
&& Array.isArray(record.verifiedPortNames)
|
|
103
|
+
&& record.verifiedPortNames.length > 0
|
|
104
|
+
&& record.verifiedPortNames.length <= 16
|
|
105
|
+
&& record.verifiedPortNames.every(name => typeof name === "string" && /^[a-z][a-z0-9_-]{0,63}$/u.test(name));
|
|
106
|
+
}
|
|
107
|
+
function normalizeRepoId(repoId) {
|
|
108
|
+
const normalized = repoId.trim().toLowerCase();
|
|
109
|
+
if (!normalized || normalized.length > 512 || Array.from(normalized).some(character => {
|
|
110
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
111
|
+
return codePoint < 32 || codePoint === 127;
|
|
112
|
+
})) {
|
|
113
|
+
throw new Error("host_parallel_promotion_invalid: repository identity is invalid.");
|
|
114
|
+
}
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
function normalizedUuid(value, label) {
|
|
118
|
+
const normalized = value.trim().toLowerCase();
|
|
119
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(normalized)) {
|
|
120
|
+
throw new Error(`host_parallel_promotion_invalid: ${label} must be a full UUID.`);
|
|
121
|
+
}
|
|
122
|
+
return normalized;
|
|
123
|
+
}
|
|
124
|
+
async function ensurePrivateDirectory(target) {
|
|
125
|
+
await fs.mkdir(target, { recursive: true, mode: 0o700 });
|
|
126
|
+
if (process.platform !== "win32")
|
|
127
|
+
await fs.chmod(target, 0o700);
|
|
128
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { HostPortBroker, type HostPortLaunch, type HostPortLeaseOwner, type HostPortReservation } from "./host-port-broker.js";
|
|
2
|
+
import { type HostRuntimePortAdapter, type HostRuntimePortConfig } from "./host-runtime-contract.js";
|
|
3
|
+
export type HostListenerOwnership = "unbound" | "owned" | "foreign";
|
|
4
|
+
export interface NodeRuntimeCommand {
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly args: readonly string[];
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly environment?: Readonly<Record<string, string>>;
|
|
9
|
+
}
|
|
10
|
+
export interface NodeRuntimeLaunchRequest extends NodeRuntimeCommand {
|
|
11
|
+
readonly framework: HostRuntimePortAdapter;
|
|
12
|
+
readonly port: HostPortReservation;
|
|
13
|
+
readonly baseUrl: string;
|
|
14
|
+
}
|
|
15
|
+
export interface NodeRuntimeCommandLauncher<T> {
|
|
16
|
+
readonly launch: (request: NodeRuntimeLaunchRequest, signal?: AbortSignal) => Promise<T>;
|
|
17
|
+
readonly inspectListener: (value: T, request: NodeRuntimeLaunchRequest, signal?: AbortSignal) => Promise<HostListenerOwnership>;
|
|
18
|
+
readonly stop: (value: T) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface NodeHostRuntimeAdapterOptions {
|
|
21
|
+
readonly readinessTimeoutMs?: number;
|
|
22
|
+
readonly readinessPollIntervalMs?: number;
|
|
23
|
+
readonly httpProbeTimeoutMs?: number;
|
|
24
|
+
readonly probeHttp?: (url: string, timeoutMs: number, signal?: AbortSignal) => Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
export interface NodeRuntimeAdapterLaunch<T> extends HostPortLaunch<T> {
|
|
27
|
+
readonly request: NodeRuntimeLaunchRequest;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Launches an explicitly configured HTTP project command beneath the caller's
|
|
31
|
+
* attempt supervisor. It never shells a command and never promotes a project
|
|
32
|
+
* by framework detection alone.
|
|
33
|
+
*/
|
|
34
|
+
export declare class NodeHostRuntimeAdapter {
|
|
35
|
+
private readonly broker;
|
|
36
|
+
private readonly readinessTimeoutMs;
|
|
37
|
+
private readonly readinessPollIntervalMs;
|
|
38
|
+
private readonly httpProbeTimeoutMs;
|
|
39
|
+
private readonly probeHttp;
|
|
40
|
+
constructor(broker: HostPortBroker, options?: NodeHostRuntimeAdapterOptions);
|
|
41
|
+
launch<T>(owner: HostPortLeaseOwner, portConfig: HostRuntimePortConfig, command: NodeRuntimeCommand, launcher: NodeRuntimeCommandLauncher<T>, signal?: AbortSignal): Promise<NodeRuntimeAdapterLaunch<T>>;
|
|
42
|
+
private waitUntilReady;
|
|
43
|
+
}
|
|
44
|
+
export declare function buildNodeRuntimeLaunchRequest(framework: HostRuntimePortAdapter, portConfig: HostRuntimePortConfig, command: NodeRuntimeCommand, port: HostPortReservation): NodeRuntimeLaunchRequest;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { hostPortEnvironment, } from "./host-runtime-contract.js";
|
|
3
|
+
/**
|
|
4
|
+
* Launches an explicitly configured HTTP project command beneath the caller's
|
|
5
|
+
* attempt supervisor. It never shells a command and never promotes a project
|
|
6
|
+
* by framework detection alone.
|
|
7
|
+
*/
|
|
8
|
+
export class NodeHostRuntimeAdapter {
|
|
9
|
+
broker;
|
|
10
|
+
readinessTimeoutMs;
|
|
11
|
+
readinessPollIntervalMs;
|
|
12
|
+
httpProbeTimeoutMs;
|
|
13
|
+
probeHttp;
|
|
14
|
+
constructor(broker, options = {}) {
|
|
15
|
+
this.broker = broker;
|
|
16
|
+
this.readinessTimeoutMs = boundedInteger(options.readinessTimeoutMs ?? 30_000, 100, 120_000, "readinessTimeoutMs");
|
|
17
|
+
this.readinessPollIntervalMs = boundedInteger(options.readinessPollIntervalMs ?? 100, 10, 5_000, "readinessPollIntervalMs");
|
|
18
|
+
this.httpProbeTimeoutMs = boundedInteger(options.httpProbeTimeoutMs ?? 1_000, 50, 10_000, "httpProbeTimeoutMs");
|
|
19
|
+
this.probeHttp = options.probeHttp ?? probeHttpReadiness;
|
|
20
|
+
}
|
|
21
|
+
async launch(owner, portConfig, command, launcher, signal) {
|
|
22
|
+
const framework = portConfig.adapter;
|
|
23
|
+
if (framework !== "node" && framework !== "vite" && framework !== "next" && framework !== "aspnetcore") {
|
|
24
|
+
throw new Error("node_runtime_adapter_invalid: port config must explicitly select node, vite, next, or aspnetcore.");
|
|
25
|
+
}
|
|
26
|
+
validateCommand(command);
|
|
27
|
+
if (!portConfig.baseUrlEnv || !portConfig.format) {
|
|
28
|
+
throw new Error("node_runtime_adapter_invalid: an HTTP baseUrlEnv and loopback format are required.");
|
|
29
|
+
}
|
|
30
|
+
let activeRequest = null;
|
|
31
|
+
const launched = await this.broker.launchWithRetry(owner, {
|
|
32
|
+
name: portConfig.name,
|
|
33
|
+
protocol: "tcp",
|
|
34
|
+
}, {
|
|
35
|
+
launch: async (port, launchSignal) => {
|
|
36
|
+
const request = buildNodeRuntimeLaunchRequest(framework, portConfig, command, port);
|
|
37
|
+
activeRequest = request;
|
|
38
|
+
return await launcher.launch(request, launchSignal);
|
|
39
|
+
},
|
|
40
|
+
verifyOwnership: async (value, _port, verifySignal) => {
|
|
41
|
+
if (!activeRequest)
|
|
42
|
+
throw new Error("node_runtime_adapter_state: launch request was not recorded.");
|
|
43
|
+
return await this.waitUntilReady(value, activeRequest, launcher, verifySignal);
|
|
44
|
+
},
|
|
45
|
+
stop: launcher.stop,
|
|
46
|
+
}, signal);
|
|
47
|
+
if (!activeRequest)
|
|
48
|
+
throw new Error("node_runtime_adapter_state: active launch request is missing.");
|
|
49
|
+
return { ...launched, request: activeRequest };
|
|
50
|
+
}
|
|
51
|
+
async waitUntilReady(value, request, launcher, signal) {
|
|
52
|
+
const deadline = Date.now() + this.readinessTimeoutMs;
|
|
53
|
+
while (Date.now() <= deadline) {
|
|
54
|
+
if (signal?.aborted)
|
|
55
|
+
throw cancellationError(signal);
|
|
56
|
+
const ownership = await launcher.inspectListener(value, request, signal);
|
|
57
|
+
if (ownership === "foreign")
|
|
58
|
+
return false;
|
|
59
|
+
if (ownership === "owned" && await this.probeHttp(request.baseUrl, this.httpProbeTimeoutMs, signal)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
await wait(this.readinessPollIntervalMs, signal);
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function buildNodeRuntimeLaunchRequest(framework, portConfig, command, port) {
|
|
68
|
+
validateCommand(command);
|
|
69
|
+
if (port.hostIp !== "127.0.0.1" || port.protocol !== "tcp") {
|
|
70
|
+
throw new Error("node_runtime_adapter_invalid: project adapters require a TCP IPv4 loopback reservation.");
|
|
71
|
+
}
|
|
72
|
+
if (!portConfig.baseUrlEnv || !portConfig.format) {
|
|
73
|
+
throw new Error("node_runtime_adapter_invalid: an HTTP baseUrlEnv and loopback format are required.");
|
|
74
|
+
}
|
|
75
|
+
rejectConflictingFrameworkArguments(framework, command.args);
|
|
76
|
+
const portEnvironment = hostPortEnvironment(portConfig, port.hostIp, port.hostPort);
|
|
77
|
+
const baseUrl = portEnvironment[portConfig.baseUrlEnv];
|
|
78
|
+
if (!baseUrl)
|
|
79
|
+
throw new Error("node_runtime_adapter_invalid: base URL environment could not be rendered.");
|
|
80
|
+
const args = framework === "aspnetcore"
|
|
81
|
+
? aspNetCoreArguments(command.command, command.args)
|
|
82
|
+
: [...command.args, ...frameworkArguments(framework, port.hostIp, port.hostPort)];
|
|
83
|
+
const adapterEnvironment = framework === "aspnetcore"
|
|
84
|
+
? { ASPNETCORE_URLS: baseUrl, DOTNET_NOLOGO: "1" }
|
|
85
|
+
: { HOST: port.hostIp };
|
|
86
|
+
return {
|
|
87
|
+
framework,
|
|
88
|
+
command: command.command,
|
|
89
|
+
args,
|
|
90
|
+
cwd: path.resolve(command.cwd),
|
|
91
|
+
environment: {
|
|
92
|
+
...command.environment,
|
|
93
|
+
...portEnvironment,
|
|
94
|
+
...adapterEnvironment,
|
|
95
|
+
FORGE_RUNTIME_PORT_NAME: portConfig.name,
|
|
96
|
+
FORGE_RUNTIME_PORT: String(port.hostPort),
|
|
97
|
+
FORGE_RUNTIME_BASE_URL: baseUrl,
|
|
98
|
+
},
|
|
99
|
+
port,
|
|
100
|
+
baseUrl,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function frameworkArguments(framework, hostIp, hostPort) {
|
|
104
|
+
if (framework === "vite")
|
|
105
|
+
return ["--host", hostIp, "--port", String(hostPort), "--strictPort"];
|
|
106
|
+
if (framework === "next")
|
|
107
|
+
return ["--hostname", hostIp, "--port", String(hostPort)];
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
function rejectConflictingFrameworkArguments(framework, args) {
|
|
111
|
+
if (framework === "aspnetcore") {
|
|
112
|
+
const forbidden = new Set(["--urls", "--launch-profile", "-lp", "--no-launch-profile"]);
|
|
113
|
+
const conflict = args.find(argument => forbidden.has(argument)
|
|
114
|
+
|| [...forbidden].some(flag => argument.startsWith(`${flag}=`)));
|
|
115
|
+
if (conflict) {
|
|
116
|
+
throw new Error(`node_runtime_adapter_invalid: command already supplies Forge-owned argument '${conflict}'.`);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (framework === "node")
|
|
121
|
+
return;
|
|
122
|
+
const forbidden = framework === "vite"
|
|
123
|
+
? new Set(["--host", "--port", "--strictPort"])
|
|
124
|
+
: new Set(["--hostname", "--port", "-H", "-p"]);
|
|
125
|
+
const conflict = args.find(argument => forbidden.has(argument) || [...forbidden].some(flag => argument.startsWith(`${flag}=`)));
|
|
126
|
+
if (conflict) {
|
|
127
|
+
throw new Error(`node_runtime_adapter_invalid: command already supplies Forge-owned argument '${conflict}'.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function aspNetCoreArguments(command, args) {
|
|
131
|
+
const executable = path.basename(command).toLowerCase();
|
|
132
|
+
if (executable !== "dotnet" && executable !== "dotnet.exe") {
|
|
133
|
+
throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires the dotnet executable.");
|
|
134
|
+
}
|
|
135
|
+
if (args[0]?.toLowerCase() !== "run") {
|
|
136
|
+
throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires a dotnet run command.");
|
|
137
|
+
}
|
|
138
|
+
const applicationArguments = args.indexOf("--");
|
|
139
|
+
const insertionIndex = applicationArguments < 0 ? args.length : applicationArguments;
|
|
140
|
+
return [
|
|
141
|
+
...args.slice(0, insertionIndex),
|
|
142
|
+
"--no-launch-profile",
|
|
143
|
+
...args.slice(insertionIndex),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
function validateCommand(command) {
|
|
147
|
+
if (!boundedText(command.command, 1024)
|
|
148
|
+
|| !path.isAbsolute(command.cwd)
|
|
149
|
+
|| command.args.length > 128
|
|
150
|
+
|| command.args.some(argument => !boundedText(argument, 4096))) {
|
|
151
|
+
throw new Error("node_runtime_adapter_invalid: command, arguments, or absolute working directory are invalid.");
|
|
152
|
+
}
|
|
153
|
+
for (const [name, value] of Object.entries(command.environment ?? {})) {
|
|
154
|
+
if (!/^[A-Z_][A-Z0-9_]{0,127}$/u.test(name) || !boundedText(value, 8192)) {
|
|
155
|
+
throw new Error("node_runtime_adapter_invalid: command environment is invalid.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function probeHttpReadiness(url, timeoutMs, signal) {
|
|
160
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
161
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetch(url, { redirect: "manual", signal: combined });
|
|
164
|
+
await response.body?.cancel().catch(() => undefined);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch (_error) {
|
|
168
|
+
if (signal?.aborted)
|
|
169
|
+
throw cancellationError(signal);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function wait(milliseconds, signal) {
|
|
174
|
+
if (signal?.aborted)
|
|
175
|
+
return Promise.reject(cancellationError(signal));
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
const timer = setTimeout(() => {
|
|
178
|
+
signal?.removeEventListener("abort", abort);
|
|
179
|
+
resolve();
|
|
180
|
+
}, milliseconds);
|
|
181
|
+
const abort = () => {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
reject(cancellationError(signal));
|
|
184
|
+
};
|
|
185
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function boundedInteger(value, minimum, maximum, name) {
|
|
189
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
190
|
+
throw new Error(`node_runtime_adapter_invalid: ${name} must be between ${minimum} and ${maximum}.`);
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
function boundedText(value, maximumLength) {
|
|
195
|
+
return typeof value === "string"
|
|
196
|
+
&& value.length > 0
|
|
197
|
+
&& value.length <= maximumLength
|
|
198
|
+
&& Array.from(value).every(character => {
|
|
199
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
200
|
+
return codePoint > 31 && codePoint !== 127;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function cancellationError(signal) {
|
|
204
|
+
return new Error(String(signal?.reason ?? "Node runtime launch cancelled."));
|
|
205
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HostRuntimePlaywrightConfig } from "./host-runtime-contract.js";
|
|
2
|
+
export interface PlaywrightHostRuntimeContext {
|
|
3
|
+
readonly worktreePath: string;
|
|
4
|
+
readonly temporaryDirectory: string;
|
|
5
|
+
readonly testOutputDirectory: string;
|
|
6
|
+
readonly browserProfileDirectory: string;
|
|
7
|
+
readonly baseUrl: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PreparedPlaywrightHostRuntime {
|
|
10
|
+
readonly configPath: string;
|
|
11
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Prepares a finite Playwright test boundary for a managed-host attempt.
|
|
15
|
+
* The generated overlay points tests at Forge's verified listener, disables
|
|
16
|
+
* Playwright-managed web servers, and keeps writable artifacts attempt-local.
|
|
17
|
+
*/
|
|
18
|
+
export declare class PlaywrightHostRuntimeAdapter {
|
|
19
|
+
prepare(config: HostRuntimePlaywrightConfig, context: PlaywrightHostRuntimeContext): Promise<PreparedPlaywrightHostRuntime>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
const maximumConfigBytes = 1024 * 1024;
|
|
5
|
+
/**
|
|
6
|
+
* Prepares a finite Playwright test boundary for a managed-host attempt.
|
|
7
|
+
* The generated overlay points tests at Forge's verified listener, disables
|
|
8
|
+
* Playwright-managed web servers, and keeps writable artifacts attempt-local.
|
|
9
|
+
*/
|
|
10
|
+
export class PlaywrightHostRuntimeAdapter {
|
|
11
|
+
async prepare(config, context) {
|
|
12
|
+
const worktreePath = path.resolve(context.worktreePath);
|
|
13
|
+
const sourceConfig = path.resolve(worktreePath, config.config);
|
|
14
|
+
const [realWorktree, realSource] = await Promise.all([
|
|
15
|
+
fs.realpath(worktreePath),
|
|
16
|
+
fs.realpath(sourceConfig),
|
|
17
|
+
]);
|
|
18
|
+
assertContained(realWorktree, realSource, "project Playwright config");
|
|
19
|
+
const sourceStat = await fs.stat(realSource);
|
|
20
|
+
if (!sourceStat.isFile() || sourceStat.size < 1 || sourceStat.size > maximumConfigBytes) {
|
|
21
|
+
throw new Error(`playwright_runtime_adapter_invalid: project config must be a regular 1-${maximumConfigBytes} byte file.`);
|
|
22
|
+
}
|
|
23
|
+
const temporaryDirectory = await realContainedDirectory(context.temporaryDirectory, realWorktree, "temporary directory");
|
|
24
|
+
const testOutputDirectory = await realContainedDirectory(context.testOutputDirectory, realWorktree, "test output directory");
|
|
25
|
+
const browserProfileDirectory = await realContainedDirectory(context.browserProfileDirectory, realWorktree, "browser profile directory");
|
|
26
|
+
const attemptRoot = path.dirname(temporaryDirectory);
|
|
27
|
+
if (path.dirname(testOutputDirectory) !== attemptRoot || path.dirname(browserProfileDirectory) !== attemptRoot) {
|
|
28
|
+
throw new Error("playwright_runtime_adapter_invalid: writable directories must belong to the same attempt root.");
|
|
29
|
+
}
|
|
30
|
+
const baseUrl = loopbackHttpUrl(context.baseUrl);
|
|
31
|
+
const overlayPath = path.join(temporaryDirectory, "forge.playwright.config.mjs");
|
|
32
|
+
await fs.writeFile(overlayPath, renderOverlay({
|
|
33
|
+
projectConfigUrl: pathToFileURL(realSource).href,
|
|
34
|
+
projectConfigDirectory: path.dirname(realSource),
|
|
35
|
+
testOutputDirectory,
|
|
36
|
+
baseUrl,
|
|
37
|
+
workers: config.workers,
|
|
38
|
+
}), { encoding: "utf8", mode: 0o600 });
|
|
39
|
+
return {
|
|
40
|
+
configPath: overlayPath,
|
|
41
|
+
environment: {
|
|
42
|
+
FORGE_PLAYWRIGHT_CONFIG: overlayPath,
|
|
43
|
+
FORGE_PLAYWRIGHT_BASE_URL: baseUrl,
|
|
44
|
+
PLAYWRIGHT_HTML_OUTPUT_DIR: path.join(testOutputDirectory, "html-report"),
|
|
45
|
+
PLAYWRIGHT_BLOB_OUTPUT_DIR: path.join(testOutputDirectory, "blob-report"),
|
|
46
|
+
PLAYWRIGHT_JUNIT_OUTPUT_NAME: path.join(testOutputDirectory, "results.xml"),
|
|
47
|
+
PLAYWRIGHT_JSON_OUTPUT_NAME: path.join(testOutputDirectory, "results.json"),
|
|
48
|
+
FORGE_BROWSER_PROFILE_DIR: browserProfileDirectory,
|
|
49
|
+
FORGE_TEST_OUTPUT_DIR: testOutputDirectory,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function renderOverlay(values) {
|
|
55
|
+
return [
|
|
56
|
+
"// Generated by forge-bridge for one fenced host attempt.",
|
|
57
|
+
"import path from \"node:path\";",
|
|
58
|
+
`import projectConfig from ${JSON.stringify(values.projectConfigUrl)};`,
|
|
59
|
+
`const projectConfigDirectory = ${JSON.stringify(values.projectConfigDirectory)};`,
|
|
60
|
+
"const resolveConfigPath = value => typeof value === \"string\" && !path.isAbsolute(value)",
|
|
61
|
+
" ? path.resolve(projectConfigDirectory, value)",
|
|
62
|
+
" : value;",
|
|
63
|
+
"const resolveModulePath = value => typeof value === \"string\" && value.startsWith(\".\")",
|
|
64
|
+
" ? path.resolve(projectConfigDirectory, value)",
|
|
65
|
+
" : value;",
|
|
66
|
+
"const resolveUse = use => ({",
|
|
67
|
+
" ...(use ?? {}),",
|
|
68
|
+
" storageState: typeof use?.storageState === \"string\" ? resolveConfigPath(use.storageState) : use?.storageState,",
|
|
69
|
+
" baseURL: " + JSON.stringify(values.baseUrl) + ",",
|
|
70
|
+
"});",
|
|
71
|
+
"const forgeUse = resolveUse(projectConfig.use);",
|
|
72
|
+
"const forgeProjects = Array.isArray(projectConfig.projects)",
|
|
73
|
+
" ? projectConfig.projects.map(project => ({",
|
|
74
|
+
" ...project,",
|
|
75
|
+
" testDir: resolveConfigPath(project.testDir),",
|
|
76
|
+
" snapshotDir: resolveConfigPath(project.snapshotDir),",
|
|
77
|
+
" use: resolveUse(project.use),",
|
|
78
|
+
" }))",
|
|
79
|
+
" : projectConfig.projects;",
|
|
80
|
+
"const forgeReporter = Array.isArray(projectConfig.reporter)",
|
|
81
|
+
" ? projectConfig.reporter.map(reporter => Array.isArray(reporter)",
|
|
82
|
+
" ? [resolveModulePath(reporter[0]), ...reporter.slice(1)]",
|
|
83
|
+
" : resolveModulePath(reporter))",
|
|
84
|
+
" : resolveModulePath(projectConfig.reporter);",
|
|
85
|
+
"export default {",
|
|
86
|
+
" ...projectConfig,",
|
|
87
|
+
" testDir: resolveConfigPath(projectConfig.testDir),",
|
|
88
|
+
" snapshotDir: resolveConfigPath(projectConfig.snapshotDir),",
|
|
89
|
+
" globalSetup: resolveModulePath(projectConfig.globalSetup),",
|
|
90
|
+
" globalTeardown: resolveModulePath(projectConfig.globalTeardown),",
|
|
91
|
+
" reporter: forgeReporter,",
|
|
92
|
+
` outputDir: ${JSON.stringify(values.testOutputDirectory)},`,
|
|
93
|
+
` workers: ${values.workers},`,
|
|
94
|
+
" webServer: undefined,",
|
|
95
|
+
" use: forgeUse,",
|
|
96
|
+
" projects: forgeProjects,",
|
|
97
|
+
"};",
|
|
98
|
+
"",
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
async function realContainedDirectory(target, realWorktree, label) {
|
|
102
|
+
const realTarget = await fs.realpath(path.resolve(target));
|
|
103
|
+
if (realTarget === realWorktree || isContained(realWorktree, realTarget)) {
|
|
104
|
+
throw new Error(`playwright_runtime_adapter_invalid: ${label} must be attempt-private and outside the worktree.`);
|
|
105
|
+
}
|
|
106
|
+
const stat = await fs.stat(realTarget);
|
|
107
|
+
if (!stat.isDirectory())
|
|
108
|
+
throw new Error(`playwright_runtime_adapter_invalid: ${label} must be a directory.`);
|
|
109
|
+
return realTarget;
|
|
110
|
+
}
|
|
111
|
+
function assertContained(parent, child, label) {
|
|
112
|
+
if (!isContained(parent, child)) {
|
|
113
|
+
throw new Error(`playwright_runtime_adapter_invalid: ${label} must stay inside the prepared worktree.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function isContained(parent, child) {
|
|
117
|
+
const relative = path.relative(parent, child);
|
|
118
|
+
return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
119
|
+
}
|
|
120
|
+
function loopbackHttpUrl(value) {
|
|
121
|
+
let url;
|
|
122
|
+
try {
|
|
123
|
+
url = new URL(value);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
throw new Error("playwright_runtime_adapter_invalid: base URL must be an HTTP loopback URL.");
|
|
127
|
+
}
|
|
128
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:")
|
|
129
|
+
|| url.hostname !== "127.0.0.1"
|
|
130
|
+
|| !url.port
|
|
131
|
+
|| url.username
|
|
132
|
+
|| url.password) {
|
|
133
|
+
throw new Error("playwright_runtime_adapter_invalid: base URL must be an HTTP loopback URL.");
|
|
134
|
+
}
|
|
135
|
+
return url.href.replace(/\/$/u, "");
|
|
136
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export declare const runtimeIsolationLeaseStates: readonly ["reserved", "starting", "active", "releasing", "released", "degraded"];
|
|
2
|
+
export type RuntimeIsolationLeaseState = typeof runtimeIsolationLeaseStates[number];
|
|
3
|
+
export type RuntimeIsolationMode = "docker" | "host";
|
|
4
|
+
export interface RuntimeIsolationPort {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly protocol: "tcp" | "udp";
|
|
7
|
+
readonly hostIp: "127.0.0.1" | "::1";
|
|
8
|
+
readonly hostPort: number;
|
|
9
|
+
readonly targetPort: number;
|
|
10
|
+
}
|
|
11
|
+
export interface RuntimeIsolationPath {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly role: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RuntimeIsolationDockerResource {
|
|
16
|
+
readonly type: "container" | "network" | "volume";
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly role: string;
|
|
19
|
+
readonly id?: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface RuntimeIsolationResources {
|
|
22
|
+
readonly memoryReservationBytes: number;
|
|
23
|
+
readonly cpuMillis: number;
|
|
24
|
+
readonly pids: number;
|
|
25
|
+
readonly ports: readonly RuntimeIsolationPort[];
|
|
26
|
+
readonly paths: readonly RuntimeIsolationPath[];
|
|
27
|
+
readonly processSupervisorId: string | null;
|
|
28
|
+
readonly dockerResources: readonly RuntimeIsolationDockerResource[];
|
|
29
|
+
readonly singletonKeys: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export interface RuntimeIsolationLease {
|
|
32
|
+
readonly schemaVersion: 1;
|
|
33
|
+
readonly leaseId: string;
|
|
34
|
+
readonly dispatchAttemptId: string;
|
|
35
|
+
readonly executionLeaseId: string;
|
|
36
|
+
readonly fencingToken: string;
|
|
37
|
+
readonly runnerId: string;
|
|
38
|
+
readonly runnerInstanceId: string;
|
|
39
|
+
readonly worktreeKey: string;
|
|
40
|
+
readonly worktreePath: string;
|
|
41
|
+
readonly mode: RuntimeIsolationMode;
|
|
42
|
+
readonly state: RuntimeIsolationLeaseState;
|
|
43
|
+
readonly createdAt: string;
|
|
44
|
+
readonly stateChangedAt: string;
|
|
45
|
+
readonly expiresAt: string;
|
|
46
|
+
readonly releasedAt: string | null;
|
|
47
|
+
readonly resources: RuntimeIsolationResources;
|
|
48
|
+
}
|
|
49
|
+
export interface RuntimeIsolationLeaseInput {
|
|
50
|
+
readonly leaseId: string;
|
|
51
|
+
readonly dispatchAttemptId: string;
|
|
52
|
+
readonly executionLeaseId: string;
|
|
53
|
+
readonly fencingToken: string;
|
|
54
|
+
readonly runnerId: string;
|
|
55
|
+
readonly runnerInstanceId: string;
|
|
56
|
+
readonly worktreeKey: string;
|
|
57
|
+
readonly worktreePath: string;
|
|
58
|
+
readonly mode: RuntimeIsolationMode;
|
|
59
|
+
readonly expiresAt: string;
|
|
60
|
+
readonly resources: RuntimeIsolationResources;
|
|
61
|
+
readonly createdAt?: string;
|
|
62
|
+
}
|
|
63
|
+
export declare function createRuntimeIsolationLease(input: RuntimeIsolationLeaseInput): RuntimeIsolationLease;
|
|
64
|
+
export declare function transitionRuntimeIsolationLease(lease: RuntimeIsolationLease, nextState: RuntimeIsolationLeaseState, occurredAt?: string): RuntimeIsolationLease;
|
|
65
|
+
export declare function renewRuntimeIsolationLease(lease: RuntimeIsolationLease, fencingToken: string, expiresAt: string): RuntimeIsolationLease | null;
|
|
66
|
+
export declare function withRuntimeIsolationResources(lease: RuntimeIsolationLease, resources: RuntimeIsolationResources): RuntimeIsolationLease;
|
|
67
|
+
export declare function validRuntimeIsolationLease(value: unknown): value is RuntimeIsolationLease;
|