@loucompanion/forge-bridge 0.1.3-dev.9d8f2e7e5241 → 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.
Files changed (143) hide show
  1. package/README.md +83 -0
  2. package/dist/bridge/bridge.base.d.ts +4 -1
  3. package/dist/bridge/bridge.service.d.ts +55 -3
  4. package/dist/bridge/bridge.service.js +834 -87
  5. package/dist/bridge/bridge.types.d.ts +89 -1
  6. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  7. package/dist/bridge/runner-host-capabilities.js +93 -0
  8. package/dist/cleanup/cleanup.service.d.ts +14 -2
  9. package/dist/cleanup/cleanup.service.js +496 -3
  10. package/dist/cleanup/cleanup.types.d.ts +10 -0
  11. package/dist/cleanup/index.d.ts +1 -1
  12. package/dist/cleanup/index.js +1 -1
  13. package/dist/cleanup/internals.d.ts +2 -2
  14. package/dist/cleanup/internals.js +1 -1
  15. package/dist/cli/cli.help.js +37 -3
  16. package/dist/cli/cli.service.d.ts +33 -2
  17. package/dist/cli/cli.service.js +209 -27
  18. package/dist/cli/local-config.d.ts +5 -0
  19. package/dist/cli/local-config.js +45 -1
  20. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  21. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  22. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  23. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  24. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  25. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  26. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  27. package/dist/runtime/attempt-process-supervisor.js +386 -0
  28. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  29. package/dist/runtime/docker-engine-probe.js +300 -0
  30. package/dist/runtime/generated-output-reset.d.ts +10 -0
  31. package/dist/runtime/generated-output-reset.js +103 -0
  32. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  33. package/dist/runtime/host-listener-inspector.js +349 -0
  34. package/dist/runtime/host-port-broker.d.ts +76 -0
  35. package/dist/runtime/host-port-broker.js +333 -0
  36. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  37. package/dist/runtime/host-runtime-contract.js +700 -0
  38. package/dist/runtime/host-runtime-lease.d.ts +197 -0
  39. package/dist/runtime/host-runtime-lease.js +2203 -0
  40. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  41. package/dist/runtime/host-runtime-promotion.js +159 -0
  42. package/dist/runtime/memory-capacity.d.ts +68 -0
  43. package/dist/runtime/memory-capacity.js +121 -0
  44. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  45. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  46. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  47. package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
  48. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  49. package/dist/runtime/runtime-isolation-lease.js +158 -0
  50. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  51. package/dist/runtime/workspace-resource-estimator.js +174 -0
  52. package/dist/sandbox/preflight.d.ts +45 -1
  53. package/dist/sandbox/preflight.js +299 -34
  54. package/dist/sandbox/process.js +21 -2
  55. package/dist/sandbox/repository-lock.js +41 -3
  56. package/dist/sandbox/runtime.d.ts +66 -3
  57. package/dist/sandbox/runtime.js +670 -105
  58. package/dist/sandbox/sandbox.types.d.ts +10 -0
  59. package/dist/sandbox/smoke.d.ts +76 -0
  60. package/dist/sandbox/smoke.js +380 -0
  61. package/dist/sandbox/worker-supervisor.js +1 -1
  62. package/dist/sandbox/worker.js +2 -5
  63. package/dist/sandbox/workspace.service.d.ts +1 -0
  64. package/dist/sandbox/workspace.service.js +28 -5
  65. package/dist/session/managed-attempt-tools.d.ts +21 -0
  66. package/dist/session/managed-attempt-tools.js +912 -0
  67. package/dist/session/managed-build-coordinator.d.ts +5 -0
  68. package/dist/session/managed-build-coordinator.js +105 -0
  69. package/dist/session/managed-build-receipt.d.ts +21 -0
  70. package/dist/session/managed-build-receipt.js +335 -0
  71. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  72. package/dist/session/phase-completion.artifacts.js +233 -24
  73. package/dist/session/phase-completion.d.ts +12 -2
  74. package/dist/session/phase-completion.helpers.d.ts +5 -0
  75. package/dist/session/phase-completion.helpers.js +51 -10
  76. package/dist/session/phase-completion.js +14 -3
  77. package/dist/session/phase-completion.prompts.d.ts +3 -3
  78. package/dist/session/phase-completion.prompts.js +24 -8
  79. package/dist/session/phase-completion.runtime.d.ts +13 -1
  80. package/dist/session/phase-completion.runtime.js +189 -8
  81. package/dist/session/phase-completion.schemas.js +117 -4
  82. package/dist/session/phase-completion.validation.d.ts +4 -1
  83. package/dist/session/phase-completion.validation.js +153 -22
  84. package/dist/session/pre-publication-validation.d.ts +46 -0
  85. package/dist/session/pre-publication-validation.js +208 -0
  86. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  87. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  88. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  89. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  90. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  91. package/dist/session/provider-cli/provider-cli.process.d.ts +25 -0
  92. package/dist/session/provider-cli/provider-cli.process.js +463 -34
  93. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  94. package/dist/session/provider-cli/provider-cli.prompts.js +108 -25
  95. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  96. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  97. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  98. package/dist/session/provider-cli/provider-cli.service.d.ts +22 -2
  99. package/dist/session/provider-cli/provider-cli.service.js +529 -100
  100. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  101. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  102. package/dist/session/provider-cli/provider-output-state.js +91 -16
  103. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  104. package/dist/session/provider-cli/provider-usage.js +28 -5
  105. package/dist/session/session-redaction.d.ts +3 -0
  106. package/dist/session/session-redaction.js +38 -0
  107. package/dist/session/session.base.d.ts +6 -1
  108. package/dist/session/session.service.d.ts +8 -2
  109. package/dist/session/session.service.js +522 -41
  110. package/dist/session/session.types.d.ts +72 -0
  111. package/dist/session/session.types.js +74 -3
  112. package/dist/shared/format/format-bytes.d.ts +1 -0
  113. package/dist/shared/format/format-bytes.js +9 -0
  114. package/dist/shared/path/path.d.ts +1 -0
  115. package/dist/shared/path/path.js +21 -1
  116. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  117. package/dist/shared/process/dotnet-toolchain.js +179 -0
  118. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  119. package/dist/shared/process/host-process-recovery.js +78 -0
  120. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  121. package/dist/shared/process/host-process-supervisor.js +330 -0
  122. package/dist/shared/process/process.d.ts +1 -0
  123. package/dist/shared/process/process.js +1 -0
  124. package/dist/shared/redaction/redaction.js +12 -2
  125. package/dist/version.d.ts +1 -1
  126. package/dist/version.js +1 -1
  127. package/dist/worktree/artifact-injector.d.ts +3 -2
  128. package/dist/worktree/artifact-injector.js +43 -4
  129. package/dist/worktree/dependency-warmer.d.ts +28 -1
  130. package/dist/worktree/dependency-warmer.js +308 -9
  131. package/dist/worktree/index.d.ts +2 -1
  132. package/dist/worktree/index.js +1 -0
  133. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  134. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  135. package/dist/worktree/worktree-paths.d.ts +4 -0
  136. package/dist/worktree/worktree-paths.js +30 -0
  137. package/dist/worktree/worktree.base.d.ts +3 -0
  138. package/dist/worktree/worktree.service.d.ts +13 -2
  139. package/dist/worktree/worktree.service.js +238 -47
  140. package/dist/worktree/worktree.types.d.ts +5 -1
  141. package/package.json +5 -3
  142. package/runtime/windows-job-supervisor.ps1 +444 -0
  143. 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
+ }
@@ -8,6 +8,10 @@ export interface DockerPreflightOptions {
8
8
  readonly diskHighWatermarkPercent?: number;
9
9
  readonly diskLowWatermarkPercent?: number;
10
10
  readonly minimumWorkspaceBytes?: number;
11
+ readonly hostMemoryReserveBytes?: number;
12
+ readonly defaultWorkerMemoryLimitBytes?: number;
13
+ readonly defaultServiceMemoryLimitBytes?: number;
14
+ readonly runtimeOverheadBytes?: number;
11
15
  readonly requireRootless?: boolean;
12
16
  }
13
17
  export declare class DockerPreflight {
@@ -15,26 +19,66 @@ export declare class DockerPreflight {
15
19
  private readonly cacheMs;
16
20
  private generation;
17
21
  private cached;
18
- private cacheAt;
22
+ private fullPreflightAt;
23
+ private staticPreflight;
24
+ private inspectionInFlight;
25
+ private lastCapacitySample;
19
26
  private readonly options;
20
27
  private readonly docker;
21
28
  private readonly ownership;
22
29
  private diskPressureLatched;
30
+ private readonly memoryAvailability;
23
31
  constructor(options?: DockerPreflightOptions | string, commands?: CommandRunner, cacheMs?: number);
32
+ private unavailable;
33
+ private memoryReserveBytes;
34
+ private defaultAdmissionMemoryBytes;
24
35
  current(): SandboxPreflightResult | null;
25
36
  refresh(): Promise<SandboxPreflightResult>;
26
37
  release(): Promise<void>;
27
38
  run(force?: boolean): Promise<SandboxPreflightResult>;
39
+ /**
40
+ * Refreshes only the admission inputs that change while the daemon's static
41
+ * capability snapshot remains valid. Heartbeats use this path so periodic
42
+ * liveness reporting does not wake Docker/WSL to rerun feature probes.
43
+ */
44
+ refreshCapacity(): Promise<SandboxPreflightResult>;
45
+ /**
46
+ * When the cached static preflight remains usable, produces a capacity sample
47
+ * that began after this admission request. An older inspection is awaited,
48
+ * not preempted or reused. Later admission callers may share the next sample
49
+ * only when it began after they entered this method. Without a usable static
50
+ * snapshot, the existing fail-closed result is returned without probing.
51
+ */
52
+ refreshCapacityForAdmission(): Promise<SandboxPreflightResult>;
53
+ private startInspection;
28
54
  private inspect;
55
+ private inspectCapacity;
29
56
  }
57
+ /**
58
+ * Uses the same reviewed reserve vector as host-runtime admission. The input
59
+ * is always the physical host total; Docker Desktop's VM total is tracked as
60
+ * a separate daemon commitment domain.
61
+ */
62
+ export declare function resolveSandboxHostMemoryReserveBytes(totalMemoryBytes: number, explicitBytes?: number, environment?: NodeJS.ProcessEnv): number;
30
63
  export declare function canRunDockerMaintenance(preflight: {
31
64
  readonly healthy: boolean;
32
65
  readonly unavailableCode?: string | null;
33
66
  }): boolean;
34
67
  export declare function evaluateDiskPressure(availableBytes: number, totalBytes: number, minimumAvailableBytes: number, highWatermarkPercent: number, lowWatermarkPercent: number, pressureLatched: boolean): boolean;
68
+ export declare function requiredAvailableBytesToClearDiskPressure(totalBytes: number, minimumAvailableBytes: number, lowWatermarkPercent: number): number;
35
69
  export declare function storagePolicyAvailable(storageQuota: boolean, environment?: NodeJS.ProcessEnv): boolean;
36
70
  export declare class DockerContextOwnershipLock {
37
71
  private lockPath;
72
+ private lockToken;
38
73
  acquire(contextName?: string): Promise<void>;
39
74
  release(): Promise<void>;
40
75
  }
76
+ export declare function parseDockerMemoryUsage(value: string): number;
77
+ interface DockerContextLockOwner {
78
+ readonly token?: string;
79
+ readonly pid?: number;
80
+ readonly processIdentity?: string | null;
81
+ readonly acquiredAt?: string;
82
+ }
83
+ export declare function dockerContextOwnerIsActive(owner: DockerContextLockOwner | null): boolean;
84
+ export {};