@loucompanion/forge-bridge 0.1.3-dev.9bc5014a2270 → 0.1.3-dev.a58d7cd1092f
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 +127 -0
- package/dist/bridge/bridge.base.d.ts +8 -1
- package/dist/bridge/bridge.service.d.ts +93 -3
- package/dist/bridge/bridge.service.js +1358 -47
- package/dist/bridge/bridge.types.d.ts +202 -1
- 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.js +47 -1
- package/dist/cli/cli.service.d.ts +45 -2
- package/dist/cli/cli.service.js +378 -26
- 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 +340 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +105 -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 +386 -0
- package/dist/runtime/docker-engine-probe.d.ts +83 -0
- package/dist/runtime/docker-engine-probe.js +300 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +46 -0
- package/dist/runtime/host-listener-inspector.js +349 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +47 -0
- package/dist/runtime/host-runtime-contract.js +700 -0
- package/dist/runtime/host-runtime-lease.d.ts +197 -0
- package/dist/runtime/host-runtime-lease.js +2203 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/memory-capacity.d.ts +68 -0
- package/dist/runtime/memory-capacity.js +121 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +284 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -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 +5 -0
- package/dist/sandbox/repository-lock.js +121 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -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 +72 -0
- package/dist/session/managed-attempt-tools.d.ts +21 -0
- package/dist/session/managed-attempt-tools.js +912 -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 +335 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +256 -26
- package/dist/session/phase-completion.d.ts +12 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +51 -10
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +24 -8
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +189 -8
- package/dist/session/phase-completion.schemas.js +117 -4
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +153 -22
- package/dist/session/pre-publication-validation.d.ts +46 -0
- package/dist/session/pre-publication-validation.js +208 -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 +3 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
- package/dist/session/provider-cli/provider-cli.process.js +474 -23
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +224 -24
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +26 -3
- package/dist/session/provider-cli/provider-cli.service.js +646 -90
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +91 -16
- 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 +38 -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 +530 -42
- package/dist/session/session.types.d.ts +124 -1
- package/dist/session/session.types.js +231 -4
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- 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 +14 -0
- package/dist/shared/process/dotnet-toolchain.js +179 -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 +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/process/windows-launch.d.ts +1 -1
- package/dist/shared/process/windows-launch.js +6 -3
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +16 -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 +43 -4
- package/dist/worktree/dependency-warmer.d.ts +36 -2
- package/dist/worktree/dependency-warmer.js +347 -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/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 +13 -2
- package/dist/worktree/worktree.service.js +238 -47
- package/dist/worktree/worktree.types.d.ts +5 -1
- package/package.json +8 -4
- package/runtime/windows-job-supervisor.ps1 +444 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export const runtimeIsolationLeaseStates = [
|
|
2
|
+
"reserved",
|
|
3
|
+
"starting",
|
|
4
|
+
"active",
|
|
5
|
+
"releasing",
|
|
6
|
+
"released",
|
|
7
|
+
"degraded",
|
|
8
|
+
];
|
|
9
|
+
const allowedTransitions = {
|
|
10
|
+
reserved: new Set(["starting", "releasing", "degraded"]),
|
|
11
|
+
starting: new Set(["active", "releasing", "degraded"]),
|
|
12
|
+
active: new Set(["releasing", "degraded"]),
|
|
13
|
+
releasing: new Set(["released", "degraded"]),
|
|
14
|
+
released: new Set(),
|
|
15
|
+
degraded: new Set(["releasing"]),
|
|
16
|
+
};
|
|
17
|
+
export function createRuntimeIsolationLease(input) {
|
|
18
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
19
|
+
const lease = {
|
|
20
|
+
schemaVersion: 1,
|
|
21
|
+
leaseId: input.leaseId,
|
|
22
|
+
dispatchAttemptId: input.dispatchAttemptId,
|
|
23
|
+
executionLeaseId: input.executionLeaseId,
|
|
24
|
+
fencingToken: input.fencingToken,
|
|
25
|
+
runnerId: input.runnerId,
|
|
26
|
+
runnerInstanceId: input.runnerInstanceId,
|
|
27
|
+
worktreeKey: input.worktreeKey,
|
|
28
|
+
worktreePath: input.worktreePath,
|
|
29
|
+
mode: input.mode,
|
|
30
|
+
state: "reserved",
|
|
31
|
+
createdAt,
|
|
32
|
+
stateChangedAt: createdAt,
|
|
33
|
+
expiresAt: input.expiresAt,
|
|
34
|
+
releasedAt: null,
|
|
35
|
+
resources: input.resources,
|
|
36
|
+
};
|
|
37
|
+
if (!validRuntimeIsolationLease(lease)) {
|
|
38
|
+
throw new Error("runtime_lease_invalid: lease identity, lifetime, or resources are invalid.");
|
|
39
|
+
}
|
|
40
|
+
return lease;
|
|
41
|
+
}
|
|
42
|
+
export function transitionRuntimeIsolationLease(lease, nextState, occurredAt = new Date().toISOString()) {
|
|
43
|
+
if (lease.state === nextState)
|
|
44
|
+
return lease;
|
|
45
|
+
if (!allowedTransitions[lease.state].has(nextState) || !validTimestamp(occurredAt)) {
|
|
46
|
+
throw new Error(`runtime_lease_transition_invalid: cannot transition ${lease.state} to ${nextState}.`);
|
|
47
|
+
}
|
|
48
|
+
const transitioned = {
|
|
49
|
+
...lease,
|
|
50
|
+
state: nextState,
|
|
51
|
+
stateChangedAt: occurredAt,
|
|
52
|
+
releasedAt: nextState === "released" ? occurredAt : null,
|
|
53
|
+
};
|
|
54
|
+
if (!validRuntimeIsolationLease(transitioned)) {
|
|
55
|
+
throw new Error("runtime_lease_invalid: transitioned lease is invalid.");
|
|
56
|
+
}
|
|
57
|
+
return transitioned;
|
|
58
|
+
}
|
|
59
|
+
export function renewRuntimeIsolationLease(lease, fencingToken, expiresAt) {
|
|
60
|
+
if (lease.fencingToken !== fencingToken || lease.state === "released" || !validTimestamp(expiresAt))
|
|
61
|
+
return null;
|
|
62
|
+
const renewed = { ...lease, expiresAt };
|
|
63
|
+
return validRuntimeIsolationLease(renewed) ? renewed : null;
|
|
64
|
+
}
|
|
65
|
+
export function withRuntimeIsolationResources(lease, resources) {
|
|
66
|
+
if (lease.state === "released") {
|
|
67
|
+
throw new Error("runtime_lease_transition_invalid: released leases cannot acquire resources.");
|
|
68
|
+
}
|
|
69
|
+
const updated = { ...lease, resources };
|
|
70
|
+
if (!validRuntimeIsolationLease(updated)) {
|
|
71
|
+
throw new Error("runtime_lease_invalid: resources are invalid.");
|
|
72
|
+
}
|
|
73
|
+
return updated;
|
|
74
|
+
}
|
|
75
|
+
export function validRuntimeIsolationLease(value) {
|
|
76
|
+
if (!isRecord(value)
|
|
77
|
+
|| value.schemaVersion !== 1
|
|
78
|
+
|| !isUuid(value.leaseId)
|
|
79
|
+
|| !isUuid(value.dispatchAttemptId)
|
|
80
|
+
|| !isUuid(value.executionLeaseId)
|
|
81
|
+
|| !isUuid(value.fencingToken)
|
|
82
|
+
|| !isUuid(value.runnerId)
|
|
83
|
+
|| !isUuid(value.runnerInstanceId)
|
|
84
|
+
|| !boundedText(value.worktreeKey, 255)
|
|
85
|
+
|| !boundedText(value.worktreePath, 4096)
|
|
86
|
+
|| (value.mode !== "docker" && value.mode !== "host")
|
|
87
|
+
|| !runtimeIsolationLeaseStates.includes(value.state)
|
|
88
|
+
|| !validTimestamp(value.createdAt)
|
|
89
|
+
|| !validTimestamp(value.stateChangedAt)
|
|
90
|
+
|| !validTimestamp(value.expiresAt)
|
|
91
|
+
|| Date.parse(value.stateChangedAt) < Date.parse(value.createdAt)
|
|
92
|
+
|| Date.parse(value.expiresAt) < Date.parse(value.createdAt)
|
|
93
|
+
|| (value.state === "released") !== validTimestamp(value.releasedAt)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (value.releasedAt !== null && Date.parse(value.releasedAt) < Date.parse(value.createdAt))
|
|
97
|
+
return false;
|
|
98
|
+
return validResources(value.resources, value.mode);
|
|
99
|
+
}
|
|
100
|
+
function validResources(value, mode) {
|
|
101
|
+
if (!isRecord(value)
|
|
102
|
+
|| !nonNegativeInteger(value.memoryReservationBytes)
|
|
103
|
+
|| !nonNegativeInteger(value.cpuMillis)
|
|
104
|
+
|| !nonNegativeInteger(value.pids)
|
|
105
|
+
|| !Array.isArray(value.ports) || value.ports.length > 64
|
|
106
|
+
|| !Array.isArray(value.paths) || value.paths.length > 64
|
|
107
|
+
|| !Array.isArray(value.dockerResources) || value.dockerResources.length > 128
|
|
108
|
+
|| !Array.isArray(value.singletonKeys) || value.singletonKeys.length > 64
|
|
109
|
+
|| (value.processSupervisorId !== null && !boundedText(value.processSupervisorId, 255))) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (value.ports.some(port => !validPort(port))
|
|
113
|
+
|| value.paths.some(entry => !isRecord(entry) || !boundedText(entry.path, 4096) || !boundedText(entry.role, 128))
|
|
114
|
+
|| value.singletonKeys.some(key => !boundedText(key, 255))
|
|
115
|
+
|| value.dockerResources.some(resource => !validDockerResource(resource))) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return mode === "docker" || value.dockerResources.length === 0;
|
|
119
|
+
}
|
|
120
|
+
function validPort(value) {
|
|
121
|
+
return isRecord(value)
|
|
122
|
+
&& boundedText(value.name, 128)
|
|
123
|
+
&& (value.protocol === "tcp" || value.protocol === "udp")
|
|
124
|
+
&& (value.hostIp === "127.0.0.1" || value.hostIp === "::1")
|
|
125
|
+
&& validPortNumber(value.hostPort)
|
|
126
|
+
&& validPortNumber(value.targetPort);
|
|
127
|
+
}
|
|
128
|
+
function validDockerResource(value) {
|
|
129
|
+
return isRecord(value)
|
|
130
|
+
&& (value.type === "container" || value.type === "network" || value.type === "volume")
|
|
131
|
+
&& boundedText(value.name, 255)
|
|
132
|
+
&& boundedText(value.role, 128)
|
|
133
|
+
&& (value.id === undefined || value.id === null || boundedText(value.id, 255));
|
|
134
|
+
}
|
|
135
|
+
function validPortNumber(value) {
|
|
136
|
+
return Number.isSafeInteger(value) && Number(value) >= 1 && Number(value) <= 65535;
|
|
137
|
+
}
|
|
138
|
+
function nonNegativeInteger(value) {
|
|
139
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
140
|
+
}
|
|
141
|
+
function validTimestamp(value) {
|
|
142
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
143
|
+
}
|
|
144
|
+
function boundedText(value, maximumLength) {
|
|
145
|
+
return typeof value === "string"
|
|
146
|
+
&& value.length > 0
|
|
147
|
+
&& value.length <= maximumLength
|
|
148
|
+
&& Array.from(value).every((character) => {
|
|
149
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
150
|
+
return codePoint > 31 && codePoint !== 127;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function isUuid(value) {
|
|
154
|
+
return typeof value === "string" && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(value);
|
|
155
|
+
}
|
|
156
|
+
function isRecord(value) {
|
|
157
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
158
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const minimumWorkspaceEstimateBytes: number;
|
|
2
|
+
export declare const architectMinimumWorkspaceEstimateBytes: number;
|
|
3
|
+
export interface RepositoryWorkspaceFootprint {
|
|
4
|
+
readonly sourceBytes: number;
|
|
5
|
+
readonly hasNode: boolean;
|
|
6
|
+
readonly hasDotNet: boolean;
|
|
7
|
+
readonly hasBrowser: boolean;
|
|
8
|
+
readonly serviceCount: number;
|
|
9
|
+
}
|
|
10
|
+
export interface WorkspaceEstimateHints {
|
|
11
|
+
readonly role?: string | null;
|
|
12
|
+
readonly serviceCount?: number;
|
|
13
|
+
readonly browser?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function estimateWorkspaceBytesFromRepo(repoPath: string, hints?: WorkspaceEstimateHints): Promise<number>;
|
|
16
|
+
export declare function calculateWorkspaceBytes(footprint: RepositoryWorkspaceFootprint, role?: string | null): number;
|
|
17
|
+
export declare function measureRepositoryWorkspace(repoPath: string): Promise<RepositoryWorkspaceFootprint>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
const GiB = 1024 ** 3;
|
|
6
|
+
const alignmentBytes = 512 * 1024 ** 2;
|
|
7
|
+
export const minimumWorkspaceEstimateBytes = 2 * GiB;
|
|
8
|
+
export const architectMinimumWorkspaceEstimateBytes = GiB;
|
|
9
|
+
const maximumBytes = 100 * GiB;
|
|
10
|
+
const footprintCacheLifetimeMs = 5 * 60_000;
|
|
11
|
+
const excludedDirectories = new Set([
|
|
12
|
+
".git",
|
|
13
|
+
".artifacts",
|
|
14
|
+
".forge-worktrees",
|
|
15
|
+
".local",
|
|
16
|
+
".next",
|
|
17
|
+
".nuget",
|
|
18
|
+
".pnpm-store",
|
|
19
|
+
".tmp",
|
|
20
|
+
".turbo",
|
|
21
|
+
"artifacts",
|
|
22
|
+
"bin",
|
|
23
|
+
"build",
|
|
24
|
+
"coverage",
|
|
25
|
+
"dist",
|
|
26
|
+
"node_modules",
|
|
27
|
+
"obj",
|
|
28
|
+
"testresults",
|
|
29
|
+
]);
|
|
30
|
+
const footprintCache = new Map();
|
|
31
|
+
export async function estimateWorkspaceBytesFromRepo(repoPath, hints = {}) {
|
|
32
|
+
const cacheKey = path.resolve(repoPath);
|
|
33
|
+
const cached = footprintCache.get(cacheKey);
|
|
34
|
+
const footprint = cached && Date.now() - cached.measuredAt < footprintCacheLifetimeMs
|
|
35
|
+
? cached.footprint
|
|
36
|
+
: await measureRepositoryWorkspace(cacheKey);
|
|
37
|
+
if (!cached || cached.footprint !== footprint) {
|
|
38
|
+
footprintCache.set(cacheKey, { measuredAt: Date.now(), footprint });
|
|
39
|
+
}
|
|
40
|
+
return calculateWorkspaceBytes({
|
|
41
|
+
...footprint,
|
|
42
|
+
hasBrowser: footprint.hasBrowser || hints.browser === true,
|
|
43
|
+
serviceCount: Math.max(footprint.serviceCount, Math.max(0, hints.serviceCount ?? 0)),
|
|
44
|
+
}, hints.role);
|
|
45
|
+
}
|
|
46
|
+
export function calculateWorkspaceBytes(footprint, role) {
|
|
47
|
+
const normalizedRole = role?.trim().toLowerCase() ?? "";
|
|
48
|
+
if (normalizedRole === "architect") {
|
|
49
|
+
// Architect owns a checkout and planning artifacts, not dependencies,
|
|
50
|
+
// build outputs, browser state, services, or application runtime output.
|
|
51
|
+
const estimate = architectMinimumWorkspaceEstimateBytes
|
|
52
|
+
+ Math.max(0, footprint.sourceBytes) * 2;
|
|
53
|
+
return Math.min(maximumBytes, Math.max(architectMinimumWorkspaceEstimateBytes, alignUp(estimate, alignmentBytes)));
|
|
54
|
+
}
|
|
55
|
+
let estimate = 2 * GiB + Math.max(0, footprint.sourceBytes) * 3;
|
|
56
|
+
if (footprint.hasNode)
|
|
57
|
+
estimate += 3 * GiB;
|
|
58
|
+
if (footprint.hasDotNet)
|
|
59
|
+
estimate += 2 * GiB;
|
|
60
|
+
if (footprint.hasBrowser)
|
|
61
|
+
estimate += 1536 * 1024 ** 2;
|
|
62
|
+
estimate += Math.max(0, footprint.serviceCount) * GiB;
|
|
63
|
+
if (["qa", "ci_repair", "pr_fix"].includes(normalizedRole)) {
|
|
64
|
+
estimate += GiB;
|
|
65
|
+
}
|
|
66
|
+
return Math.min(maximumBytes, Math.max(minimumWorkspaceEstimateBytes, alignUp(estimate, alignmentBytes)));
|
|
67
|
+
}
|
|
68
|
+
export async function measureRepositoryWorkspace(repoPath) {
|
|
69
|
+
return await measureGitWorkspace(repoPath) ?? await measureFilesystemFallback(repoPath);
|
|
70
|
+
}
|
|
71
|
+
async function measureGitWorkspace(repoPath) {
|
|
72
|
+
let sourceBytes = 0;
|
|
73
|
+
let hasNode = false;
|
|
74
|
+
let hasDotNet = false;
|
|
75
|
+
let hasBrowser = false;
|
|
76
|
+
let serviceCount = 0;
|
|
77
|
+
const child = spawn("git", ["-c", "core.quotepath=false", "ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
78
|
+
cwd: repoPath,
|
|
79
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
80
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
const closed = new Promise((resolve) => {
|
|
84
|
+
child.once("close", resolve);
|
|
85
|
+
child.once("error", () => resolve(null));
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
89
|
+
for await (const relativePath of lines) {
|
|
90
|
+
if (!relativePath.trim())
|
|
91
|
+
continue;
|
|
92
|
+
const file = await inspectWorkspaceFile(path.join(repoPath, relativePath));
|
|
93
|
+
if (!file)
|
|
94
|
+
continue;
|
|
95
|
+
sourceBytes += file.bytes;
|
|
96
|
+
hasNode ||= file.hasNode;
|
|
97
|
+
hasDotNet ||= file.hasDotNet;
|
|
98
|
+
hasBrowser ||= file.hasBrowser;
|
|
99
|
+
serviceCount = Math.max(serviceCount, file.serviceCount);
|
|
100
|
+
}
|
|
101
|
+
return await closed === 0 ? { sourceBytes, hasNode, hasDotNet, hasBrowser, serviceCount } : null;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
child.kill();
|
|
105
|
+
await closed;
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function measureFilesystemFallback(repoPath) {
|
|
110
|
+
let sourceBytes = 0;
|
|
111
|
+
let hasNode = false;
|
|
112
|
+
let hasDotNet = false;
|
|
113
|
+
let hasBrowser = false;
|
|
114
|
+
let serviceCount = 0;
|
|
115
|
+
const pending = [path.resolve(repoPath)];
|
|
116
|
+
while (pending.length > 0) {
|
|
117
|
+
const current = pending.pop();
|
|
118
|
+
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const entryPath = path.join(current, entry.name);
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
if (!excludedDirectories.has(entry.name.toLowerCase()))
|
|
123
|
+
pending.push(entryPath);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!entry.isFile())
|
|
127
|
+
continue;
|
|
128
|
+
const file = await inspectWorkspaceFile(entryPath);
|
|
129
|
+
if (!file)
|
|
130
|
+
continue;
|
|
131
|
+
sourceBytes += file.bytes;
|
|
132
|
+
hasNode ||= file.hasNode;
|
|
133
|
+
hasDotNet ||= file.hasDotNet;
|
|
134
|
+
hasBrowser ||= file.hasBrowser;
|
|
135
|
+
serviceCount = Math.max(serviceCount, file.serviceCount);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { sourceBytes, hasNode, hasDotNet, hasBrowser, serviceCount };
|
|
139
|
+
}
|
|
140
|
+
async function inspectWorkspaceFile(filePath) {
|
|
141
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
142
|
+
if (!stat?.isFile())
|
|
143
|
+
return null;
|
|
144
|
+
const name = path.basename(filePath).toLowerCase();
|
|
145
|
+
const sandbox = name === "forge.sandbox.yml" || name === "forge.sandbox.yaml";
|
|
146
|
+
return {
|
|
147
|
+
bytes: stat.size,
|
|
148
|
+
hasNode: ["package.json", "pnpm-lock.yaml", "package-lock.json", "yarn.lock"].includes(name),
|
|
149
|
+
hasDotNet: name.endsWith(".sln") || name.endsWith(".csproj") || name.endsWith(".fsproj"),
|
|
150
|
+
hasBrowser: name.startsWith("playwright.config"),
|
|
151
|
+
serviceCount: sandbox
|
|
152
|
+
? countSandboxServices(await fs.readFile(filePath, "utf8").catch(() => ""))
|
|
153
|
+
: 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function countSandboxServices(yaml) {
|
|
157
|
+
let inServices = false;
|
|
158
|
+
let count = 0;
|
|
159
|
+
for (const rawLine of yaml.split(/\r?\n/gu)) {
|
|
160
|
+
const line = rawLine.trim();
|
|
161
|
+
if (!line || line.startsWith("#"))
|
|
162
|
+
continue;
|
|
163
|
+
if (!/^\s/u.test(rawLine)) {
|
|
164
|
+
inServices = line.toLowerCase() === "services:";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (inServices && line.startsWith("-"))
|
|
168
|
+
count += 1;
|
|
169
|
+
}
|
|
170
|
+
return count;
|
|
171
|
+
}
|
|
172
|
+
function alignUp(value, alignment) {
|
|
173
|
+
return Math.ceil(value / alignment) * alignment;
|
|
174
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export class BoundedCommandRunner {
|
|
3
|
+
async run(command, args, options = {}) {
|
|
4
|
+
const maxOutputBytes = options.maxOutputBytes ?? 1024 * 1024;
|
|
5
|
+
return await new Promise((resolve, reject) => {
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const abort = () => controller.abort(options.signal?.reason ?? "command cancelled");
|
|
8
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
9
|
+
const timeout = options.timeoutMs
|
|
10
|
+
? setTimeout(() => controller.abort(`command exceeded ${options.timeoutMs}ms`), options.timeoutMs)
|
|
11
|
+
: null;
|
|
12
|
+
timeout?.unref?.();
|
|
13
|
+
const child = spawn(command, [...args], {
|
|
14
|
+
cwd: options.cwd,
|
|
15
|
+
env: options.env ? { ...options.env } : undefined,
|
|
16
|
+
stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
});
|
|
20
|
+
let stdout = Buffer.alloc(0);
|
|
21
|
+
let stderr = Buffer.alloc(0);
|
|
22
|
+
const append = (current, chunk) => {
|
|
23
|
+
if (current.length >= maxOutputBytes)
|
|
24
|
+
return current;
|
|
25
|
+
return Buffer.concat([current, chunk.subarray(0, maxOutputBytes - current.length)]);
|
|
26
|
+
};
|
|
27
|
+
child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
28
|
+
child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
|
|
29
|
+
child.on("error", (error) => {
|
|
30
|
+
cleanup();
|
|
31
|
+
reject(error);
|
|
32
|
+
});
|
|
33
|
+
child.on("close", (code) => {
|
|
34
|
+
cleanup();
|
|
35
|
+
resolve({ exitCode: code ?? 1, stdout: stdout.toString("utf8"), stderr: stderr.toString("utf8") });
|
|
36
|
+
});
|
|
37
|
+
if (options.stdin !== undefined) {
|
|
38
|
+
child.stdin.end(options.stdin);
|
|
39
|
+
}
|
|
40
|
+
function cleanup() {
|
|
41
|
+
if (timeout)
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
options.signal?.removeEventListener("abort", abort);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ValidatedSandboxContract } from "./sandbox.types.js";
|
|
2
|
+
export declare function validateSandboxContract(source: string): ValidatedSandboxContract;
|
|
3
|
+
export declare function validateDockerfilePolicy(source: string, requirePinnedBase: boolean): readonly string[];
|
|
4
|
+
export declare function dockerfileBaseImages(source: string): readonly string[];
|
|
5
|
+
export declare function dockerfileUpstreamImages(source: string): readonly string[];
|