@loucompanion/forge-bridge 0.1.3-dev.d1ca67076aa9 → 0.1.3-dev.eeefdb0d19c1

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 +78 -0
  2. package/dist/bridge/bridge.base.d.ts +6 -1
  3. package/dist/bridge/bridge.service.d.ts +60 -3
  4. package/dist/bridge/bridge.service.js +962 -22
  5. package/dist/bridge/bridge.types.d.ts +172 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.js +47 -1
  14. package/dist/cli/cli.service.d.ts +42 -2
  15. package/dist/cli/cli.service.js +351 -22
  16. package/dist/cli/local-config.d.ts +6 -0
  17. package/dist/cli/local-config.js +47 -3
  18. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  19. package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
  20. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  22. package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
  23. package/dist/runtime/attempt-process-supervisor.js +343 -0
  24. package/dist/runtime/generated-output-reset.d.ts +10 -0
  25. package/dist/runtime/generated-output-reset.js +103 -0
  26. package/dist/runtime/host-listener-inspector.d.ts +44 -0
  27. package/dist/runtime/host-listener-inspector.js +324 -0
  28. package/dist/runtime/host-port-broker.d.ts +76 -0
  29. package/dist/runtime/host-port-broker.js +333 -0
  30. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  31. package/dist/runtime/host-runtime-contract.js +700 -0
  32. package/dist/runtime/host-runtime-lease.d.ts +133 -0
  33. package/dist/runtime/host-runtime-lease.js +1460 -0
  34. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  35. package/dist/runtime/host-runtime-promotion.js +159 -0
  36. package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
  37. package/dist/runtime/node-host-runtime-adapter.js +268 -0
  38. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  39. package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
  40. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  41. package/dist/runtime/runtime-isolation-lease.js +158 -0
  42. package/dist/sandbox/command-runner.d.ts +4 -0
  43. package/dist/sandbox/command-runner.js +47 -0
  44. package/dist/sandbox/contract.d.ts +5 -0
  45. package/dist/sandbox/contract.js +276 -0
  46. package/dist/sandbox/event-budget.d.ts +11 -0
  47. package/dist/sandbox/event-budget.js +27 -0
  48. package/dist/sandbox/image-policy.d.ts +35 -0
  49. package/dist/sandbox/image-policy.js +247 -0
  50. package/dist/sandbox/index.d.ts +5 -0
  51. package/dist/sandbox/index.js +5 -0
  52. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  53. package/dist/sandbox/lease-watchdog.js +67 -0
  54. package/dist/sandbox/preflight.d.ts +54 -0
  55. package/dist/sandbox/preflight.js +335 -0
  56. package/dist/sandbox/process.d.ts +20 -0
  57. package/dist/sandbox/process.js +209 -0
  58. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  59. package/dist/sandbox/provider-auth/codex.js +212 -0
  60. package/dist/sandbox/repository-lock.d.ts +5 -0
  61. package/dist/sandbox/repository-lock.js +119 -0
  62. package/dist/sandbox/runtime.d.ts +297 -0
  63. package/dist/sandbox/runtime.js +2338 -0
  64. package/dist/sandbox/sandbox.types.d.ts +86 -0
  65. package/dist/sandbox/sandbox.types.js +1 -0
  66. package/dist/sandbox/smoke.d.ts +76 -0
  67. package/dist/sandbox/smoke.js +380 -0
  68. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  69. package/dist/sandbox/worker-supervisor.js +36 -0
  70. package/dist/sandbox/worker.d.ts +1 -0
  71. package/dist/sandbox/worker.js +225 -0
  72. package/dist/sandbox/workspace.service.d.ts +13 -0
  73. package/dist/sandbox/workspace.service.js +72 -0
  74. package/dist/session/managed-attempt-tools.d.ts +12 -0
  75. package/dist/session/managed-attempt-tools.js +350 -0
  76. package/dist/session/phase-completion.artifacts.js +32 -4
  77. package/dist/session/phase-completion.d.ts +4 -2
  78. package/dist/session/phase-completion.helpers.js +27 -3
  79. package/dist/session/phase-completion.js +12 -3
  80. package/dist/session/phase-completion.prompts.d.ts +3 -3
  81. package/dist/session/phase-completion.prompts.js +19 -5
  82. package/dist/session/phase-completion.runtime.d.ts +12 -1
  83. package/dist/session/phase-completion.runtime.js +127 -7
  84. package/dist/session/phase-completion.schemas.js +108 -3
  85. package/dist/session/phase-completion.validation.d.ts +4 -1
  86. package/dist/session/phase-completion.validation.js +106 -2
  87. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  88. package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
  89. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
  90. package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
  91. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  92. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  93. package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
  94. package/dist/session/provider-cli/provider-cli.process.d.ts +17 -0
  95. package/dist/session/provider-cli/provider-cli.process.js +223 -14
  96. package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
  97. package/dist/session/provider-cli/provider-cli.prompts.js +202 -18
  98. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  99. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  100. package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
  101. package/dist/session/provider-cli/provider-cli.service.d.ts +7 -3
  102. package/dist/session/provider-cli/provider-cli.service.js +399 -98
  103. package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
  104. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  105. package/dist/session/provider-cli/provider-output-state.js +30 -2
  106. package/dist/session/session-redaction.d.ts +3 -0
  107. package/dist/session/session-redaction.js +23 -0
  108. package/dist/session/session.base.d.ts +6 -1
  109. package/dist/session/session.service.d.ts +8 -2
  110. package/dist/session/session.service.js +463 -58
  111. package/dist/session/session.types.d.ts +107 -1
  112. package/dist/session/session.types.js +226 -3
  113. package/dist/shared/path/path.js +3 -1
  114. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  115. package/dist/shared/process/host-process-recovery.js +78 -0
  116. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  117. package/dist/shared/process/host-process-supervisor.js +282 -0
  118. package/dist/shared/process/process.d.ts +1 -0
  119. package/dist/shared/process/process.js +1 -0
  120. package/dist/shared/process/windows-launch.d.ts +1 -1
  121. package/dist/shared/process/windows-launch.js +6 -3
  122. package/dist/shared/provider-capabilities.d.ts +1 -0
  123. package/dist/shared/provider-capabilities.js +3 -0
  124. package/dist/shared/redaction/redaction.js +10 -3
  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 +21 -1
  130. package/dist/worktree/dependency-warmer.js +138 -5
  131. package/dist/worktree/index.d.ts +1 -0
  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 +7 -2
  139. package/dist/worktree/worktree.service.js +72 -44
  140. package/dist/worktree/worktree.types.d.ts +3 -1
  141. package/package.json +6 -3
  142. package/runtime/windows-job-supervisor.ps1 +356 -0
  143. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,220 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ const maximumConfigBytes = 1024 * 1024;
6
+ /**
7
+ * Prepares a finite Playwright test boundary for a managed-host attempt.
8
+ * The generated overlay points tests at Forge's verified listener, disables
9
+ * Playwright-managed web servers, and keeps writable artifacts attempt-local.
10
+ */
11
+ export class PlaywrightHostRuntimeAdapter {
12
+ async prepare(config, context) {
13
+ const worktreePath = path.resolve(context.worktreePath);
14
+ const sourceConfig = path.resolve(worktreePath, config.config);
15
+ const [realWorktree, realSource] = await Promise.all([
16
+ fs.realpath(worktreePath),
17
+ fs.realpath(sourceConfig),
18
+ ]);
19
+ assertContained(realWorktree, realSource, "project Playwright config");
20
+ const sourceStat = await fs.stat(realSource);
21
+ if (!sourceStat.isFile() || sourceStat.size < 1 || sourceStat.size > maximumConfigBytes) {
22
+ throw new Error(`playwright_runtime_adapter_invalid: project config must be a regular 1-${maximumConfigBytes} byte file.`);
23
+ }
24
+ const temporaryDirectory = await realContainedDirectory(context.temporaryDirectory, realWorktree, "temporary directory");
25
+ const testOutputDirectory = await realContainedDirectory(context.testOutputDirectory, realWorktree, "test output directory");
26
+ const browserProfileDirectory = await realContainedDirectory(context.browserProfileDirectory, realWorktree, "browser profile directory");
27
+ const attemptRoot = path.dirname(temporaryDirectory);
28
+ if (path.dirname(testOutputDirectory) !== attemptRoot || path.dirname(browserProfileDirectory) !== attemptRoot) {
29
+ throw new Error("playwright_runtime_adapter_invalid: writable directories must belong to the same attempt root.");
30
+ }
31
+ const baseUrl = loopbackHttpUrl(context.baseUrl);
32
+ const overlayPath = path.join(temporaryDirectory, "forge.playwright.config.mjs");
33
+ const browserCache = await resolvePlaywrightBrowserCache(attemptRoot);
34
+ const browserInstallCommand = await resolvePlaywrightBrowserInstallCommand(context.worktreePath, path.dirname(realSource));
35
+ await fs.writeFile(overlayPath, renderOverlay({
36
+ projectConfigUrl: pathToFileURL(realSource).href,
37
+ projectConfigDirectory: path.dirname(realSource),
38
+ testOutputDirectory,
39
+ baseUrl,
40
+ workers: config.workers,
41
+ }), { encoding: "utf8", mode: 0o600 });
42
+ return {
43
+ configPath: overlayPath,
44
+ environment: {
45
+ FORGE_PLAYWRIGHT_CONFIG: overlayPath,
46
+ FORGE_PLAYWRIGHT_BASE_URL: baseUrl,
47
+ FORGE_E2E_WEB_URL: baseUrl,
48
+ FORGE_PLAYWRIGHT_BROWSER_READY: browserCache.ready ? "true" : "false",
49
+ FORGE_PLAYWRIGHT_BROWSER_INSTALL_COMMAND: browserInstallCommand,
50
+ PLAYWRIGHT_BROWSERS_PATH: browserCache.path,
51
+ PLAYWRIGHT_HTML_OUTPUT_DIR: path.join(testOutputDirectory, "html-report"),
52
+ PLAYWRIGHT_BLOB_OUTPUT_DIR: path.join(testOutputDirectory, "blob-report"),
53
+ PLAYWRIGHT_JUNIT_OUTPUT_NAME: path.join(testOutputDirectory, "results.xml"),
54
+ PLAYWRIGHT_JSON_OUTPUT_NAME: path.join(testOutputDirectory, "results.json"),
55
+ FORGE_BROWSER_PROFILE_DIR: browserProfileDirectory,
56
+ FORGE_TEST_OUTPUT_DIR: testOutputDirectory,
57
+ },
58
+ };
59
+ }
60
+ }
61
+ async function resolvePlaywrightBrowserInstallCommand(worktreePath, configDirectory) {
62
+ const relativeDirectory = path.relative(worktreePath, configDirectory).replaceAll(path.sep, "/") || ".";
63
+ if (await isFile(path.join(configDirectory, "pnpm-lock.yaml"))) {
64
+ return `pnpm --dir ${quoteCommandArgument(relativeDirectory)} exec playwright install chromium`;
65
+ }
66
+ if (await isFile(path.join(configDirectory, "yarn.lock"))) {
67
+ return `yarn --cwd ${quoteCommandArgument(relativeDirectory)} playwright install chromium`;
68
+ }
69
+ return `npm --prefix ${quoteCommandArgument(relativeDirectory)} exec playwright install chromium`;
70
+ }
71
+ async function isFile(candidate) {
72
+ try {
73
+ return (await fs.stat(candidate)).isFile();
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ function quoteCommandArgument(value) {
80
+ return /[\s"']/u.test(value) ? JSON.stringify(value) : value;
81
+ }
82
+ async function resolvePlaywrightBrowserCache(attemptRoot) {
83
+ const candidates = [
84
+ process.env.PLAYWRIGHT_BROWSERS_PATH,
85
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, "ms-playwright") : null,
86
+ path.join(os.homedir(), ".cache", "ms-playwright"),
87
+ ].filter((candidate) => Boolean(candidate?.trim()));
88
+ for (const candidate of [...new Set(candidates.map(candidate => path.resolve(candidate)))]) {
89
+ if (await hasChromiumExecutable(candidate)) {
90
+ return { path: candidate, ready: true };
91
+ }
92
+ }
93
+ const attemptCache = path.join(attemptRoot, "cache", "ms-playwright");
94
+ await fs.mkdir(attemptCache, { recursive: true });
95
+ return { path: attemptCache, ready: false };
96
+ }
97
+ async function hasChromiumExecutable(cachePath) {
98
+ let entries;
99
+ try {
100
+ entries = await fs.readdir(cachePath, { withFileTypes: true });
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ const executableSuffixes = process.platform === "win32"
106
+ ? [path.join("chrome-win64", "chrome.exe"), path.join("chrome-win", "chrome.exe")]
107
+ : process.platform === "darwin"
108
+ ? [path.join("chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium")]
109
+ : [path.join("chrome-linux", "chrome"), path.join("chrome-linux64", "chrome")];
110
+ for (const entry of entries) {
111
+ if (!entry.isDirectory() || !entry.name.startsWith("chromium-"))
112
+ continue;
113
+ for (const suffix of executableSuffixes) {
114
+ try {
115
+ const stat = await fs.stat(path.join(cachePath, entry.name, suffix));
116
+ if (stat.isFile())
117
+ return true;
118
+ }
119
+ catch {
120
+ // Continue through known Playwright layouts.
121
+ }
122
+ }
123
+ }
124
+ return false;
125
+ }
126
+ function renderOverlay(values) {
127
+ return [
128
+ "// Generated by forge-bridge for one fenced host attempt.",
129
+ "import path from \"node:path\";",
130
+ `import projectConfig from ${JSON.stringify(values.projectConfigUrl)};`,
131
+ `const projectConfigDirectory = ${JSON.stringify(values.projectConfigDirectory)};`,
132
+ "const resolveConfigPath = value => typeof value === \"string\" && !path.isAbsolute(value)",
133
+ " ? path.resolve(projectConfigDirectory, value)",
134
+ " : value;",
135
+ "const resolveModulePath = value => typeof value === \"string\" && value.startsWith(\".\")",
136
+ " ? path.resolve(projectConfigDirectory, value)",
137
+ " : value;",
138
+ "const resolveUse = use => ({",
139
+ " ...(use ?? {}),",
140
+ " storageState: typeof use?.storageState === \"string\" ? resolveConfigPath(use.storageState) : use?.storageState,",
141
+ " baseURL: " + JSON.stringify(values.baseUrl) + ",",
142
+ "});",
143
+ "const forgeUse = resolveUse(projectConfig.use);",
144
+ "const forgeProjects = Array.isArray(projectConfig.projects)",
145
+ " ? projectConfig.projects.map(project => ({",
146
+ " ...project,",
147
+ " testDir: resolveConfigPath(project.testDir),",
148
+ " snapshotDir: resolveConfigPath(project.snapshotDir),",
149
+ " use: resolveUse(project.use),",
150
+ " }))",
151
+ " : projectConfig.projects;",
152
+ "const forgeReporter = Array.isArray(projectConfig.reporter)",
153
+ " ? projectConfig.reporter.map(reporter => Array.isArray(reporter)",
154
+ " ? [resolveModulePath(reporter[0]), ...reporter.slice(1)]",
155
+ " : resolveModulePath(reporter))",
156
+ " : resolveModulePath(projectConfig.reporter);",
157
+ "const forgeProviderOnlySpecs = (() => {",
158
+ " try {",
159
+ " const parsed = JSON.parse(process.env.FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS ?? '[]');",
160
+ " return Array.isArray(parsed) ? parsed.filter(value => typeof value === 'string' && value.length > 0) : [];",
161
+ " } catch {",
162
+ " return [];",
163
+ " }",
164
+ "})();",
165
+ "const forgeTestIgnore = forgeProviderOnlySpecs.length === 0",
166
+ " ? projectConfig.testIgnore",
167
+ " : [...(Array.isArray(projectConfig.testIgnore) ? projectConfig.testIgnore : projectConfig.testIgnore ? [projectConfig.testIgnore] : []), ...forgeProviderOnlySpecs];",
168
+ "export default {",
169
+ " ...projectConfig,",
170
+ " testDir: resolveConfigPath(projectConfig.testDir),",
171
+ " snapshotDir: resolveConfigPath(projectConfig.snapshotDir),",
172
+ " globalSetup: resolveModulePath(projectConfig.globalSetup),",
173
+ " globalTeardown: resolveModulePath(projectConfig.globalTeardown),",
174
+ " reporter: forgeReporter,",
175
+ " testIgnore: forgeTestIgnore,",
176
+ ` outputDir: ${JSON.stringify(values.testOutputDirectory)},`,
177
+ ` workers: ${values.workers},`,
178
+ " webServer: undefined,",
179
+ " use: forgeUse,",
180
+ " projects: forgeProjects,",
181
+ "};",
182
+ "",
183
+ ].join("\n");
184
+ }
185
+ async function realContainedDirectory(target, realWorktree, label) {
186
+ const realTarget = await fs.realpath(path.resolve(target));
187
+ if (realTarget === realWorktree || isContained(realWorktree, realTarget)) {
188
+ throw new Error(`playwright_runtime_adapter_invalid: ${label} must be attempt-private and outside the worktree.`);
189
+ }
190
+ const stat = await fs.stat(realTarget);
191
+ if (!stat.isDirectory())
192
+ throw new Error(`playwright_runtime_adapter_invalid: ${label} must be a directory.`);
193
+ return realTarget;
194
+ }
195
+ function assertContained(parent, child, label) {
196
+ if (!isContained(parent, child)) {
197
+ throw new Error(`playwright_runtime_adapter_invalid: ${label} must stay inside the prepared worktree.`);
198
+ }
199
+ }
200
+ function isContained(parent, child) {
201
+ const relative = path.relative(parent, child);
202
+ return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
203
+ }
204
+ function loopbackHttpUrl(value) {
205
+ let url;
206
+ try {
207
+ url = new URL(value);
208
+ }
209
+ catch {
210
+ throw new Error("playwright_runtime_adapter_invalid: base URL must be an HTTP loopback URL.");
211
+ }
212
+ if ((url.protocol !== "http:" && url.protocol !== "https:")
213
+ || url.hostname !== "127.0.0.1"
214
+ || !url.port
215
+ || url.username
216
+ || url.password) {
217
+ throw new Error("playwright_runtime_adapter_invalid: base URL must be an HTTP loopback URL.");
218
+ }
219
+ return url.href.replace(/\/$/u, "");
220
+ }
@@ -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;
@@ -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,4 @@
1
+ import type { CommandOptions, CommandResult, CommandRunner } from "./sandbox.types.js";
2
+ export declare class BoundedCommandRunner implements CommandRunner {
3
+ run(command: string, args: readonly string[], options?: CommandOptions): Promise<CommandResult>;
4
+ }
@@ -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[];