reeval 0.0.1

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 (213) hide show
  1. package/LICENSE +202 -0
  2. package/THIRD_PARTY_NOTICES.md +2160 -0
  3. package/assets/package.json +9 -0
  4. package/assets/pnpm-lock.yaml +1336 -0
  5. package/dist/agent-DVHkx8aT.js +532 -0
  6. package/dist/agent-DVHkx8aT.js.map +1 -0
  7. package/dist/analysis-CxSCt1YY.js +16 -0
  8. package/dist/analysis-CxSCt1YY.js.map +1 -0
  9. package/dist/assemble-B-nmz8oH.js +695 -0
  10. package/dist/assemble-B-nmz8oH.js.map +1 -0
  11. package/dist/assemble-CmRVDZ2Q.js +2 -0
  12. package/dist/attempt-BdRQ9jKX.js +176 -0
  13. package/dist/attempt-BdRQ9jKX.js.map +1 -0
  14. package/dist/bridge/index.mjs +1911 -0
  15. package/dist/bridge/package.json +13 -0
  16. package/dist/bridge/pnpm-lock.yaml +1041 -0
  17. package/dist/bridge/pnpm-workspace.yaml +2 -0
  18. package/dist/budget-DBUrH4jX.js +8 -0
  19. package/dist/budget-DBUrH4jX.js.map +1 -0
  20. package/dist/cli--CQ3Iy9q.js +1865 -0
  21. package/dist/cli--CQ3Iy9q.js.map +1 -0
  22. package/dist/cli-spec-DKAu09Gi.js +478 -0
  23. package/dist/cli-spec-DKAu09Gi.js.map +1 -0
  24. package/dist/cli.d.ts +1 -0
  25. package/dist/cli.js +8 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/colors-QV9dFVhN.js +34 -0
  28. package/dist/colors-QV9dFVhN.js.map +1 -0
  29. package/dist/command-BSJzHMtP.js +280 -0
  30. package/dist/command-BSJzHMtP.js.map +1 -0
  31. package/dist/command-CEm8PZ1N.js +1043 -0
  32. package/dist/command-CEm8PZ1N.js.map +1 -0
  33. package/dist/command-D-jnQWVG.js +673 -0
  34. package/dist/command-D-jnQWVG.js.map +1 -0
  35. package/dist/command-DQAvbG7d.js +432 -0
  36. package/dist/command-DQAvbG7d.js.map +1 -0
  37. package/dist/commands-A4udfyKZ.js +2 -0
  38. package/dist/commands-CF8mOfiu.js +207 -0
  39. package/dist/commands-CF8mOfiu.js.map +1 -0
  40. package/dist/config-Cuj0kVYj.js +415 -0
  41. package/dist/config-Cuj0kVYj.js.map +1 -0
  42. package/dist/context-loi9OCZv.js +106 -0
  43. package/dist/context-loi9OCZv.js.map +1 -0
  44. package/dist/core-tLN-z1Be.js +96 -0
  45. package/dist/core-tLN-z1Be.js.map +1 -0
  46. package/dist/db-Cjj2ZsfQ.js +2 -0
  47. package/dist/db-DxDzMhtB.js +827 -0
  48. package/dist/db-DxDzMhtB.js.map +1 -0
  49. package/dist/debug-D24NOQH1.js +89 -0
  50. package/dist/debug-D24NOQH1.js.map +1 -0
  51. package/dist/debug-SEmW4w8f.js +2 -0
  52. package/dist/dist-BZXYHN2X.js +1673 -0
  53. package/dist/dist-BZXYHN2X.js.map +1 -0
  54. package/dist/docker-DQDZWXF9.js +20 -0
  55. package/dist/docker-DQDZWXF9.js.map +1 -0
  56. package/dist/docker-DnWAVdEm.js +2 -0
  57. package/dist/docker-oSbCExHL.js +153 -0
  58. package/dist/docker-oSbCExHL.js.map +1 -0
  59. package/dist/doctor-CxlH1pDM.js +525 -0
  60. package/dist/doctor-CxlH1pDM.js.map +1 -0
  61. package/dist/egress-73SQfKdx.js +619 -0
  62. package/dist/egress-73SQfKdx.js.map +1 -0
  63. package/dist/env-BQqj93gu.js +21 -0
  64. package/dist/env-BQqj93gu.js.map +1 -0
  65. package/dist/environment-Mz7wvrtD.js +552 -0
  66. package/dist/environment-Mz7wvrtD.js.map +1 -0
  67. package/dist/errors-C9w_ud7g.js +160 -0
  68. package/dist/errors-C9w_ud7g.js.map +1 -0
  69. package/dist/errors-CSxfJ4qu.js +11 -0
  70. package/dist/errors-CSxfJ4qu.js.map +1 -0
  71. package/dist/files--6CEUXm7.js +2 -0
  72. package/dist/files-BLlrhU4-.js +62 -0
  73. package/dist/files-BLlrhU4-.js.map +1 -0
  74. package/dist/files-Bd25OAQH.js +2 -0
  75. package/dist/files-BvHCFWgD.js +2 -0
  76. package/dist/files-D2-qn_Dl.js +275 -0
  77. package/dist/files-D2-qn_Dl.js.map +1 -0
  78. package/dist/files-bF2fPuT9.js +99 -0
  79. package/dist/files-bF2fPuT9.js.map +1 -0
  80. package/dist/git-D6hMeieS.js +44 -0
  81. package/dist/git-D6hMeieS.js.map +1 -0
  82. package/dist/harness-ls5aX1UO.js +131 -0
  83. package/dist/harness-ls5aX1UO.js.map +1 -0
  84. package/dist/harness.d.ts +74 -0
  85. package/dist/harness.js +2 -0
  86. package/dist/header-DzFDoMCI.js +78 -0
  87. package/dist/header-DzFDoMCI.js.map +1 -0
  88. package/dist/ids-Bp6ut7ZD.js +49 -0
  89. package/dist/ids-Bp6ut7ZD.js.map +1 -0
  90. package/dist/images-Cc1-coFc.js +286 -0
  91. package/dist/images-Cc1-coFc.js.map +1 -0
  92. package/dist/index.d.ts +97 -0
  93. package/dist/index.js +1959 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/init-CFljS8BJ.js +344 -0
  96. package/dist/init-CFljS8BJ.js.map +1 -0
  97. package/dist/init-z2ZPn9Tj.js +2 -0
  98. package/dist/invocations-rXktgSAn.js +157 -0
  99. package/dist/invocations-rXktgSAn.js.map +1 -0
  100. package/dist/jiti-BOLXpKyG.js +46 -0
  101. package/dist/jiti-BOLXpKyG.js.map +1 -0
  102. package/dist/judge-D8Uf4dA9.js +146 -0
  103. package/dist/judge-D8Uf4dA9.js.map +1 -0
  104. package/dist/judge.d.ts +21 -0
  105. package/dist/judge.js +19 -0
  106. package/dist/judge.js.map +1 -0
  107. package/dist/live-Bs3LReMV.js +98 -0
  108. package/dist/live-Bs3LReMV.js.map +1 -0
  109. package/dist/matchers-DyLvm7EG.js +374 -0
  110. package/dist/matchers-DyLvm7EG.js.map +1 -0
  111. package/dist/model-BdO79gWz.js +44 -0
  112. package/dist/model-BdO79gWz.js.map +1 -0
  113. package/dist/model-CDL98xue.js +725 -0
  114. package/dist/model-CDL98xue.js.map +1 -0
  115. package/dist/model-v-0_Ik7S.js +2 -0
  116. package/dist/node-kG64XxdE.js +506 -0
  117. package/dist/node-kG64XxdE.js.map +1 -0
  118. package/dist/opencode-providers-DZuD_oYt.js +3149 -0
  119. package/dist/opencode-providers-DZuD_oYt.js.map +1 -0
  120. package/dist/options-CAQhu--C.js +452 -0
  121. package/dist/options-CAQhu--C.js.map +1 -0
  122. package/dist/patch-BkKje4cp.js +69 -0
  123. package/dist/patch-BkKje4cp.js.map +1 -0
  124. package/dist/paths-CBvm6pap.js +72 -0
  125. package/dist/paths-CBvm6pap.js.map +1 -0
  126. package/dist/plan-gwk6dJ8c.js +237 -0
  127. package/dist/plan-gwk6dJ8c.js.map +1 -0
  128. package/dist/preflight-BtubXKQ8.js +2 -0
  129. package/dist/preflight-BypZMNBG.js +185 -0
  130. package/dist/preflight-BypZMNBG.js.map +1 -0
  131. package/dist/prepare-Cwv0bHs_.js +3 -0
  132. package/dist/prepare-D6Uzcubb.js +213 -0
  133. package/dist/prepare-D6Uzcubb.js.map +1 -0
  134. package/dist/provenance-Dfb8YY5Y.js +129 -0
  135. package/dist/provenance-Dfb8YY5Y.js.map +1 -0
  136. package/dist/provider-BDbNOQhQ.js +2 -0
  137. package/dist/provider-CmAYTQQs.js +14 -0
  138. package/dist/provider-CmAYTQQs.js.map +1 -0
  139. package/dist/providers-2p3ylMHI.js +107 -0
  140. package/dist/providers-2p3ylMHI.js.map +1 -0
  141. package/dist/providers-BUOxitcT.js +2 -0
  142. package/dist/query-CFATHZmV.js +91 -0
  143. package/dist/query-CFATHZmV.js.map +1 -0
  144. package/dist/query-DR4m6CXf.js +2 -0
  145. package/dist/record-DQ8wzyco.js +99 -0
  146. package/dist/record-DQ8wzyco.js.map +1 -0
  147. package/dist/redact-C4fL_bZP.js +167 -0
  148. package/dist/redact-C4fL_bZP.js.map +1 -0
  149. package/dist/results-CeXb8Wde.js +37 -0
  150. package/dist/results-CeXb8Wde.js.map +1 -0
  151. package/dist/root-CFDjpWuc.js +110 -0
  152. package/dist/root-CFDjpWuc.js.map +1 -0
  153. package/dist/sandbox-By2gkdkC.js +133 -0
  154. package/dist/sandbox-By2gkdkC.js.map +1 -0
  155. package/dist/sandbox.d.ts +41 -0
  156. package/dist/sandbox.js +2 -0
  157. package/dist/server-Bi61uiY0.js +350 -0
  158. package/dist/server-Bi61uiY0.js.map +1 -0
  159. package/dist/server-CsSBL8NC.js +2 -0
  160. package/dist/source-BXzrmof5.js +2 -0
  161. package/dist/source-Bsfa3LZH.js +40 -0
  162. package/dist/source-Bsfa3LZH.js.map +1 -0
  163. package/dist/src-55j1zNHH.js +3 -0
  164. package/dist/src-AEwxd1UP.js +602 -0
  165. package/dist/src-AEwxd1UP.js.map +1 -0
  166. package/dist/src-BDwSuyIo.js +3 -0
  167. package/dist/src-BQyMiWri.js +490 -0
  168. package/dist/src-BQyMiWri.js.map +1 -0
  169. package/dist/src-C-LHyTSV.js +3 -0
  170. package/dist/src-Cmp-7YrD.js +1136 -0
  171. package/dist/src-Cmp-7YrD.js.map +1 -0
  172. package/dist/src-D0RdqpNb.js +252 -0
  173. package/dist/src-D0RdqpNb.js.map +1 -0
  174. package/dist/src-D8KYzxBX.js +793 -0
  175. package/dist/src-D8KYzxBX.js.map +1 -0
  176. package/dist/src-DEgff9Z7.js +4 -0
  177. package/dist/src-DXx_29pq.js +567 -0
  178. package/dist/src-DXx_29pq.js.map +1 -0
  179. package/dist/src-DwwiVLqv.js +2937 -0
  180. package/dist/src-DwwiVLqv.js.map +1 -0
  181. package/dist/src-N7E_mfcZ.js +2 -0
  182. package/dist/src-W9g-y5X5.js +3 -0
  183. package/dist/src-daS0wKH2.js +3 -0
  184. package/dist/state-Cyw3QTsN.js +2 -0
  185. package/dist/state-DZ7eqb1F.js +167 -0
  186. package/dist/state-DZ7eqb1F.js.map +1 -0
  187. package/dist/stats-BNAHlNMD.js +114 -0
  188. package/dist/stats-BNAHlNMD.js.map +1 -0
  189. package/dist/studio-app/assets/index-D6XyfFSH.js +58 -0
  190. package/dist/studio-app/assets/index-xwzWdcE_.css +2 -0
  191. package/dist/studio-app/favicon.svg +1 -0
  192. package/dist/studio-app/index.html +15 -0
  193. package/dist/summary-D7Ttky0z.js +166 -0
  194. package/dist/summary-D7Ttky0z.js.map +1 -0
  195. package/dist/text-gEcKjAyp.js +29 -0
  196. package/dist/text-gEcKjAyp.js.map +1 -0
  197. package/dist/types-C5vI3g1C.js +9 -0
  198. package/dist/types-C5vI3g1C.js.map +1 -0
  199. package/dist/types-DLec-6Cx.d.ts +1535 -0
  200. package/dist/usage-DsYUqIUM.js +24 -0
  201. package/dist/usage-DsYUqIUM.js.map +1 -0
  202. package/dist/view/index.d.ts +645 -0
  203. package/dist/view/index.js +8 -0
  204. package/dist/views-BrHOv6wg.js +220 -0
  205. package/dist/views-BrHOv6wg.js.map +1 -0
  206. package/dist/views-CwCWLWqo.js +3 -0
  207. package/dist/worker-entry-DnYkANPG.js +506 -0
  208. package/dist/worker-entry-DnYkANPG.js.map +1 -0
  209. package/dist/workspace-C2S0Z6gl.js +327 -0
  210. package/dist/workspace-C2S0Z6gl.js.map +1 -0
  211. package/dist/workspace-CyoXG6CY.js +2 -0
  212. package/docker/Dockerfile +19 -0
  213. package/package.json +79 -0
@@ -0,0 +1,2937 @@
1
+ import { l as ownFileURL } from "./root-CFDjpWuc.js";
2
+ import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync } from "node:fs";
3
+ import { basename, dirname, join, posix, relative, win32 } from "node:path";
4
+ import net, { BlockList, isIP } from "node:net";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createHash, generateKeyPairSync, randomBytes, randomInt, sign } from "node:crypto";
7
+ import { prepareSandboxForHarness } from "@ai-sdk/harness/agent";
8
+ import { spawn } from "node:child_process";
9
+ import { isUtf8 } from "node:buffer";
10
+ import { Duplex, Readable, Transform } from "node:stream";
11
+ import http from "node:http";
12
+ import http2 from "node:http2";
13
+ import https from "node:https";
14
+ import tls from "node:tls";
15
+ import { lookup } from "node:dns/promises";
16
+ //#region ../sandbox-docker/src/build-failure.ts
17
+ var ImageBuildError = class extends Error {
18
+ name = "ImageBuildError";
19
+ failure;
20
+ constructor(message, failure) {
21
+ super(message);
22
+ this.failure = failure;
23
+ }
24
+ };
25
+ const stripAnsi = (text) => text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
26
+ /** The failing step and its output from `--progress plain` build output. */
27
+ function parseBuildOutput(text) {
28
+ const lines = stripAnsi(text).split(/\r?\n/);
29
+ let step = null;
30
+ let error = null;
31
+ for (const line of lines) {
32
+ const failed = /^#(\d+) ERROR: /.exec(line);
33
+ if (failed) step = failed[1] ?? null;
34
+ const top = /^ERROR: (.*)$/.exec(line);
35
+ if (top) error = top[1] ?? null;
36
+ }
37
+ error = (error ?? "").replace(/^(failed to build: )?(failed to solve: )?/, "").trim() || null;
38
+ const output = step ? lines.flatMap((line) => {
39
+ const m = new RegExp(`^#${step} \\d+\\.\\d+ (.*)$`).exec(line);
40
+ return m?.[1]?.trim() ? [m[1]] : [];
41
+ }) : [];
42
+ const code = /exit code: (\d+)/.exec(error ?? "")?.[1];
43
+ return {
44
+ error,
45
+ output: output.slice(-8),
46
+ exitCode: code ? Number(code) : null
47
+ };
48
+ }
49
+ /** ENOSPC or a SIGKILL (137, usually the out-of-memory killer), from anything the build printed. */
50
+ function failureCause(text, exitCode) {
51
+ if (/no space left on device|ENOSPC/i.test(text)) return "enospc";
52
+ if (exitCode === 137 || /exit(?:ed| code:?) 137\b|signal: killed|OOMKilled/i.test(text)) return "oom";
53
+ return null;
54
+ }
55
+ const causeLine = (cause) => cause === "enospc" ? "out of disk space (ENOSPC): free space for Docker (docker system df, reeval clean --images) and rerun" : "killed (exit 137): usually out of memory; give Docker more memory or lower the concurrency";
56
+ const DETERMINISTIC = /did not complete successfully|dockerfile parse error|unknown instruction|should not be blank|pull access denied|repository does not exist|manifest unknown|not found: manifest|: not found\b|unauthorized|denied:|reeval needs a Debian or Ubuntu based image/i;
57
+ const TRANSIENT = /EAI_AGAIN|ECONNRESET|ETIMEDOUT|ECONNREFUSED|i\/o timeout|TLS handshake timeout|no such host|temporary failure in name resolution|connection reset by peer|dial tcp|net\/http: request canceled|unexpected EOF|\b(?:500|502|503|504) |429 Too Many Requests|toomanyrequests|Service Unavailable|Bad Gateway|Gateway Timeout|ENOTFOUND/i;
58
+ /** A registry or network error: a pull, a metadata lookup, or a download in the harness bootstrap. */
59
+ const isTransient = (text) => !DETERMINISTIC.test(text) && TRANSIENT.test(text);
60
+ /** Up to `retries` more tries on a transient failure, after `delays` ms each; any other throws at once. */
61
+ async function retryTransient(run, options = {}) {
62
+ const delays = options.delays ?? RETRY_DELAYS;
63
+ for (let i = 0;; i++) try {
64
+ return await run();
65
+ } catch (error) {
66
+ if (!(error instanceof ImageBuildError ? error.failure.transient : isTransient(error.message)) || i >= delays.length || options.signal?.aborted) throw error;
67
+ options.onRetry?.(error);
68
+ await new Promise((resolve) => setTimeout(resolve, delays[i]));
69
+ }
70
+ }
71
+ /** Two more tries, after 2 s and then 8 s. */
72
+ const RETRY_DELAYS = [2e3, 8e3];
73
+ /**
74
+ * A failure's text: the headline (what failed and why), the named cause, the step's own last
75
+ * lines, then the log. Never the generated Dockerfile.
76
+ */
77
+ function failureMessage(parts) {
78
+ const lines = [parts.headline];
79
+ if (parts.cause) lines.push(causeLine(parts.cause));
80
+ if (parts.output.length) lines.push(...parts.output.map((l) => ` | ${l}`));
81
+ if (parts.log) lines.push(`(full log: ${parts.log})`);
82
+ return lines.join("\n");
83
+ }
84
+ /** The last lines of a bootstrap command's output, past the error's own first line. */
85
+ function outputTail(text, max = 8) {
86
+ return stripAnsi(text).split(/\r?\n/).map((l) => l.trimEnd()).filter((l) => l.trim()).slice(-max);
87
+ }
88
+ //#endregion
89
+ //#region ../sandbox-docker/src/process.ts
90
+ const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
91
+ const cmdEscape = (arg, twice) => {
92
+ const quoted = `"${arg.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\*)$/, "$1$1")}"`.replace(CMD_META, "^$1");
93
+ return twice ? quoted.replace(CMD_META, "^$1") : quoted;
94
+ };
95
+ /**
96
+ * How to spawn `cmd` on Windows, where npm, npx and pnpm are `.cmd` scripts that only cmd.exe
97
+ * runs: the program PATH and PATHEXT find, and for a script, cmd.exe with every argument escaped.
98
+ */
99
+ function windowsSpawn(cmd, args, env = process.env, exists = existsSync) {
100
+ const exts = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
101
+ const file = (/[\\/]/.test(cmd) ? [""] : (env.PATH ?? env.Path ?? "").split(";").filter(Boolean)).flatMap((dir) => {
102
+ const base = dir ? win32.join(dir, cmd) : cmd;
103
+ return [...win32.extname(cmd) ? [base] : [], ...exts.map((ext) => base + ext.toLowerCase())];
104
+ }).find(exists) ?? cmd;
105
+ if (!/\.(cmd|bat)$/i.test(file)) return {
106
+ file,
107
+ args,
108
+ verbatim: false
109
+ };
110
+ const twice = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i.test(file);
111
+ const line = [win32.normalize(file).replace(CMD_META, "^$1"), ...args.map((a) => cmdEscape(a, twice))].join(" ");
112
+ return {
113
+ file: env.ComSpec ?? "cmd.exe",
114
+ args: [
115
+ "/d",
116
+ "/s",
117
+ "/c",
118
+ `"${line}"`
119
+ ],
120
+ verbatim: true
121
+ };
122
+ }
123
+ /**
124
+ * Kills a process and everything it started: its process group on POSIX (a detached child leads
125
+ * one), its process tree with `taskkill` on Windows, which has neither groups nor signals.
126
+ */
127
+ function killTree(pid) {
128
+ if (process.platform === "win32") {
129
+ spawn("taskkill", [
130
+ "/pid",
131
+ String(pid),
132
+ "/t",
133
+ "/f"
134
+ ], {
135
+ stdio: "ignore",
136
+ windowsHide: true
137
+ }).on("error", () => {});
138
+ return;
139
+ }
140
+ try {
141
+ process.kill(-pid, "SIGKILL");
142
+ } catch {
143
+ try {
144
+ process.kill(pid, "SIGKILL");
145
+ } catch {}
146
+ }
147
+ }
148
+ /** `s` as one single-quoted POSIX shell word. */
149
+ const shellQuote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
150
+ function run(cmd, args, options = {}) {
151
+ const { signal } = options;
152
+ return new Promise((resolve, reject) => {
153
+ if (signal?.aborted) return reject(signal.reason);
154
+ const detached = options.detached ?? true;
155
+ const target = process.platform === "win32" ? windowsSpawn(cmd, args, options.env ? {
156
+ ...process.env,
157
+ ...options.env
158
+ } : process.env) : {
159
+ file: cmd,
160
+ args,
161
+ verbatim: false
162
+ };
163
+ const child = spawn(target.file, target.args, {
164
+ cwd: options.cwd,
165
+ stdio: "pipe",
166
+ detached: detached && !target.verbatim,
167
+ windowsHide: true,
168
+ windowsVerbatimArguments: target.verbatim,
169
+ env: options.env ? {
170
+ ...process.env,
171
+ ...options.env
172
+ } : process.env
173
+ });
174
+ const out = [];
175
+ let stderr = "";
176
+ child.stdout.on("data", (d) => out.push(d));
177
+ child.stderr.on("data", (d) => {
178
+ stderr += d;
179
+ });
180
+ const onAbort = () => {
181
+ if ((detached || process.platform === "win32") && child.pid) killTree(child.pid);
182
+ else child.kill("SIGKILL");
183
+ };
184
+ signal?.addEventListener("abort", onAbort, { once: true });
185
+ child.on("error", reject);
186
+ child.on("close", (code) => {
187
+ signal?.removeEventListener("abort", onAbort);
188
+ if (signal?.aborted) return reject(signal.reason);
189
+ const stdout = Buffer.concat(out);
190
+ resolve({
191
+ exitCode: code ?? 1,
192
+ stdout: options.binary ? stdout : stdout.toString(),
193
+ stderr
194
+ });
195
+ });
196
+ if (child.pid) options.onSpawn?.(child.pid);
197
+ child.stdin.on("error", () => {});
198
+ child.stdin.end(options.input ?? "");
199
+ });
200
+ }
201
+ //#endregion
202
+ //#region ../sandbox-docker/src/trace.ts
203
+ /** Most of each stream a trace keeps; the rest is counted. */
204
+ const MAX_STREAM = 4096;
205
+ /** Most of each argument a trace keeps, e.g. the gateway's forwarder script. */
206
+ const MAX_ARG = 300;
207
+ let listener;
208
+ /** Calls `fn` for every docker command this process runs, or stops with `undefined`. */
209
+ function onDockerCommand(fn) {
210
+ listener = fn;
211
+ }
212
+ const clip = (text, max) => text.length > max ? `${text.slice(0, max)}… (${text.length - max} more)` : text;
213
+ /** Text as is (clipped), or a byte count for binary output such as a file read with `cat`. */
214
+ function streamPreview(data) {
215
+ if (typeof data === "string") return clip(data, MAX_STREAM);
216
+ if (data.includes(0) || !isUtf8(data)) return `<${data.length} bytes>`;
217
+ return clip(data.toString("utf8"), MAX_STREAM);
218
+ }
219
+ function traceDocker(args, startedAt, result) {
220
+ if (!listener) return;
221
+ const base = {
222
+ args: args.map((a) => clip(a, MAX_ARG)),
223
+ ms: Date.now() - startedAt
224
+ };
225
+ try {
226
+ if ("error" in result) {
227
+ const error = result.error instanceof Error ? result.error.message : String(result.error);
228
+ listener({
229
+ ...base,
230
+ exitCode: null,
231
+ stdout: "",
232
+ stderr: "",
233
+ error
234
+ });
235
+ return;
236
+ }
237
+ listener({
238
+ ...base,
239
+ exitCode: result.exitCode,
240
+ stdout: result.stdout === void 0 ? "" : streamPreview(result.stdout),
241
+ stderr: clip(result.stderr ?? "", MAX_STREAM),
242
+ ...result.stdin ? { stdin: result.stdin } : {}
243
+ });
244
+ } catch {}
245
+ }
246
+ //#endregion
247
+ //#region ../sandbox-docker/src/docker-cli.ts
248
+ /** Runs the docker CLI. `env` values are passed via the CLI's environment, never argv. */
249
+ async function docker(args, options = {}) {
250
+ const startedAt = Date.now();
251
+ const stdin = options.input?.length ?? 0;
252
+ try {
253
+ const r = await run("docker", args, {
254
+ ...options,
255
+ binary: true
256
+ });
257
+ traceDocker(args, startedAt, {
258
+ ...r,
259
+ stdin
260
+ });
261
+ return r;
262
+ } catch (error) {
263
+ traceDocker(args, startedAt, { error });
264
+ throw error;
265
+ }
266
+ }
267
+ async function dockerOk(args, opts) {
268
+ const r = await docker(args, opts);
269
+ if (r.exitCode !== 0) throw new Error(`docker ${args[0]} failed: ${r.stderr.trim()}`);
270
+ return r.stdout.toString("utf8").trim();
271
+ }
272
+ /** Removes a container in its own process group, so removal survives this process being killed. */
273
+ function removeContainerDetached(id) {
274
+ return new Promise((resolve) => {
275
+ const keepAlive = setInterval(() => {}, 1e3);
276
+ const startedAt = Date.now();
277
+ const args = [
278
+ "rm",
279
+ "-f",
280
+ id
281
+ ];
282
+ const done = (exitCode) => {
283
+ clearInterval(keepAlive);
284
+ traceDocker(args, startedAt, { exitCode });
285
+ resolve();
286
+ };
287
+ const child = spawn("docker", args, {
288
+ detached: true,
289
+ stdio: "ignore",
290
+ windowsHide: true
291
+ });
292
+ child.on("error", () => done(null));
293
+ child.on("close", done);
294
+ });
295
+ }
296
+ /**
297
+ * Removes every container with `label` (`key=value`) for the next few seconds, in its own process
298
+ * group so it outlives this process. For a `docker run` killed mid-way: the daemon may create the
299
+ * container after the CLI dies, and its id never came back.
300
+ */
301
+ function sweepLabelDetached(label, seconds = 15) {
302
+ const child = spawn(process.execPath, [
303
+ "-e",
304
+ `const { execFileSync } = require('node:child_process')
305
+ const [filter, seconds] = process.argv.slice(1)
306
+ const o = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }
307
+ for (let i = 0; i < Number(seconds); i++) {
308
+ try {
309
+ const ids = execFileSync('docker', ['ps', '-aq', '--filter', filter], o).split(/\\s+/).filter(Boolean)
310
+ if (ids.length) execFileSync('docker', ['rm', '-f', ...ids], o)
311
+ } catch {}
312
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000)
313
+ }`,
314
+ `label=${label}`,
315
+ String(seconds)
316
+ ], {
317
+ detached: true,
318
+ stdio: "ignore",
319
+ windowsHide: true,
320
+ env: {
321
+ ...process.env,
322
+ BUN_BE_BUN: "1"
323
+ }
324
+ });
325
+ child.on("error", () => {});
326
+ child.unref();
327
+ }
328
+ async function dockerState() {
329
+ const r = await docker([
330
+ "info",
331
+ "--format",
332
+ "{{.ServerVersion}} {{.OSType}}"
333
+ ]).catch(() => null);
334
+ const [version, os] = r?.exitCode === 0 ? r.stdout.toString("utf8").trim().split(/\s+/) : [];
335
+ if (!version) return "down";
336
+ return os === "windows" ? "windows-containers" : "running";
337
+ }
338
+ /** Whether Docker is running Linux containers, as every sandbox needs. */
339
+ async function dockerAvailable() {
340
+ return await dockerState() === "running";
341
+ }
342
+ /** CPUs and memory of the machine containers run on (the VM, under Docker Desktop). */
343
+ async function dockerCapacity() {
344
+ const r = await docker([
345
+ "info",
346
+ "--format",
347
+ "{{.NCPU}} {{.MemTotal}}"
348
+ ]).catch(() => null);
349
+ if (r?.exitCode !== 0) return null;
350
+ const [cpus, memoryBytes] = r.stdout.toString("utf8").trim().split(/\s+/).map(Number);
351
+ return cpus && memoryBytes ? {
352
+ cpus,
353
+ memoryBytes
354
+ } : null;
355
+ }
356
+ /**
357
+ * Whether a RepoDigest shows the image is in a registry someone can pull from. The containerd
358
+ * image store (Docker 29+, recent Docker Desktop) also gives locally built images a digest, under
359
+ * their bare local name (`reeval-sandbox@sha256:…`). A bare name is Docker Hub's `library/`
360
+ * namespace, which only official images can push to, so only a digest whose name has a registry
361
+ * host or a namespace counts. Official images such as `busybox` count as local; no image with
362
+ * the harness baked in is one.
363
+ */
364
+ const isRegistryDigest = (digest) => (digest.split("@")[0] ?? "").includes("/");
365
+ async function imageInfo(image) {
366
+ const r = await docker([
367
+ "image",
368
+ "inspect",
369
+ "--format",
370
+ "{\"config\":{{json .Config}},\"id\":{{json .Id}},\"repoDigests\":{{json .RepoDigests}}}",
371
+ image
372
+ ]).catch(() => null);
373
+ if (r?.exitCode !== 0) return null;
374
+ const info = JSON.parse(r.stdout.toString("utf8"));
375
+ const repoDigests = (info.repoDigests ?? []).filter(isRegistryDigest);
376
+ return {
377
+ labels: info.config?.Labels ?? {},
378
+ id: info.id,
379
+ repoDigests
380
+ };
381
+ }
382
+ //#endregion
383
+ //#region ../sandbox-docker/src/image.ts
384
+ /**
385
+ * The harness adapters an image can bake. Each pin must match the exact dependency (or
386
+ * `workspace:<version>` for this repo's own adapters); a test enforces it. `patch` is the first 8
387
+ * hex of the sha256 of the adapter's pnpm patch (`patchedDependencies` in pnpm-workspace.yaml),
388
+ * whose bridge the image bakes; a test holds it to the patch file.
389
+ */
390
+ const HARNESS_PACKAGES = {
391
+ cursor: {
392
+ name: "@reeval/harness-cursor-sdk",
393
+ version: "0.1.0"
394
+ },
395
+ "claude-code": {
396
+ name: "@ai-sdk/harness-claude-code",
397
+ version: "1.0.128",
398
+ patch: "5d1c985c"
399
+ },
400
+ antigravity: {
401
+ name: "@reeval/harness-antigravity",
402
+ version: "0.1.0"
403
+ },
404
+ opencode: {
405
+ name: "@reeval/harness-opencode",
406
+ version: "0.2.0"
407
+ },
408
+ goose: {
409
+ name: "@reeval/harness-goose",
410
+ version: "0.1.0"
411
+ },
412
+ pi: {
413
+ name: "@reeval/harness-pi",
414
+ version: "0.1.0"
415
+ },
416
+ codex: {
417
+ name: "@reeval/harness-codex",
418
+ version: "0.1.0"
419
+ }
420
+ };
421
+ /** Every harness with an image, in {@link HARNESS_PACKAGES} order. */
422
+ const BAKED_HARNESSES = Object.keys(HARNESS_PACKAGES);
423
+ const isBakedHarness = (kind) => Object.hasOwn(HARNESS_PACKAGES, kind);
424
+ /** The adapter build reeval runs for a harness: its version, and its patch if reeval patches it. */
425
+ const adapterVersion = (harness) => {
426
+ const { version, patch } = HARNESS_PACKAGES[harness];
427
+ return patch ? `${version}-patch.${patch}` : version;
428
+ };
429
+ const BASE_IMAGE = "reeval-sandbox-base:latest";
430
+ /** reeval's image for a harness; the tag pins the adapter, whose bootstrap pins the CLI. */
431
+ const imageFor = (harness) => `reeval-sandbox:${harness}-${adapterVersion(harness)}`;
432
+ /** Cursor's image, which the fake harness also runs in by default. */
433
+ const DEFAULT_IMAGE = imageFor("cursor");
434
+ /** The `ai.reeval.harness` label value an image baked for `harness` carries. */
435
+ const harnessLabel = (harness) => `${HARNESS_PACKAGES[harness].name}@${adapterVersion(harness)}`;
436
+ const LABEL = {
437
+ managed: "ai.reeval.managed",
438
+ run: "ai.reeval.run",
439
+ /** Unique per session, so a create cut short can find what it made. */
440
+ session: "ai.reeval.session",
441
+ agentVersion: "ai.reeval.agent-version",
442
+ harness: "ai.reeval.harness",
443
+ /** On reeval's own images: the hash of the base image's build context, to tell a stale one. */
444
+ context: "ai.reeval.context",
445
+ /** On a `from` image: the digest (or, for a local-only base, the image id) it was built on. */
446
+ fromDigest: "ai.reeval.from-digest"
447
+ };
448
+ /**
449
+ * The user the agent, setup steps and checks run as, which every image reeval builds creates. Image
450
+ * builds (`install`, Dockerfiles) stay root.
451
+ */
452
+ const SANDBOX_USER = "reeval";
453
+ /** {@link SANDBOX_USER}'s home. */
454
+ const SANDBOX_HOME = `/home/${SANDBOX_USER}`;
455
+ /** The container's default working directory, as seen by the harness. */
456
+ const SANDBOX_ROOT = "/reeval";
457
+ /** The agent's working directory, relative to {@link SANDBOX_ROOT}. */
458
+ const WORKSPACE_DIR = "workspace";
459
+ const BRIDGE_PORT = 43210;
460
+ //#endregion
461
+ //#region ../sandbox-docker/src/egress/cert.ts
462
+ const der = (tag, ...content) => {
463
+ const body = Buffer.concat(content);
464
+ const n = body.length;
465
+ const length = n < 128 ? Buffer.from([n]) : n < 256 ? Buffer.from([129, n]) : n < 65536 ? Buffer.from([
466
+ 130,
467
+ n >> 8,
468
+ n & 255
469
+ ]) : Buffer.from([
470
+ 131,
471
+ n >> 16,
472
+ n >> 8 & 255,
473
+ n & 255
474
+ ]);
475
+ return Buffer.concat([
476
+ Buffer.from([tag]),
477
+ length,
478
+ body
479
+ ]);
480
+ };
481
+ const seq = (...items) => der(48, ...items);
482
+ const set = (...items) => der(49, ...items);
483
+ const octets = (bytes) => der(4, bytes);
484
+ const utf8 = (text) => der(12, Buffer.from(text, "utf8"));
485
+ const bool = (value) => der(1, Buffer.from([value ? 255 : 0]));
486
+ const explicit = (n, inner) => der(160 + n, inner);
487
+ const integer = (bytes) => {
488
+ let i = 0;
489
+ while (i < bytes.length - 1 && bytes[i] === 0 && bytes[i + 1] < 128) i++;
490
+ const trimmed = bytes.subarray(i);
491
+ return der(2, trimmed[0] >= 128 ? Buffer.concat([Buffer.from([0]), trimmed]) : trimmed);
492
+ };
493
+ const bits = (bytes, unused = 0) => der(3, Buffer.from([unused]), bytes);
494
+ const oid = (dotted) => {
495
+ const [a = 0, b = 0, ...rest] = dotted.split(".").map(Number);
496
+ const out = [40 * a + b];
497
+ for (const arc of rest) {
498
+ const chunk = [arc & 127];
499
+ for (let v = arc >> 7; v > 0; v >>= 7) chunk.unshift(v & 127 | 128);
500
+ out.push(...chunk);
501
+ }
502
+ return der(6, Buffer.from(out));
503
+ };
504
+ const time = (date) => {
505
+ const iso = date.toISOString().replace(/[-:T]/g, "").slice(0, 14);
506
+ return date.getUTCFullYear() < 2050 ? der(23, Buffer.from(`${iso.slice(2)}Z`)) : der(24, Buffer.from(`${iso}Z`));
507
+ };
508
+ const ECDSA_SHA256 = seq(oid("1.2.840.10045.4.3.2"));
509
+ const name = (commonName) => seq(set(seq(oid("2.5.4.10"), utf8("reeval"))), set(seq(oid("2.5.4.3"), utf8(commonName))));
510
+ const extension = (id, critical, value) => seq(oid(id), ...critical ? [bool(true)] : [], octets(value));
511
+ /** Key usage bits, most significant first, with DER's minimal "unused bits" count. */
512
+ const keyUsage = (usage) => {
513
+ let unused = 0;
514
+ while (unused < 7 && !(usage & 1 << unused)) unused++;
515
+ return bits(Buffer.from([usage]), unused);
516
+ };
517
+ const keyId = (spki) => createHash("sha1").update(spki).digest();
518
+ const pem = (bytes) => `-----BEGIN CERTIFICATE-----\n${bytes.toString("base64").replace(/.{64}/g, "$&\n").replace(/\n?$/, "\n")}-----END CERTIFICATE-----\n`;
519
+ function issue(options) {
520
+ const spki = options.publicKey.export({
521
+ type: "spki",
522
+ format: "der"
523
+ });
524
+ const serial = randomBytes(16);
525
+ serial[0] = serial[0] & 127;
526
+ const notBefore = /* @__PURE__ */ new Date(Date.now() - 36e5);
527
+ const notAfter = new Date(notBefore.getTime() + options.days * 864e5);
528
+ const tbs = seq(explicit(0, integer(Buffer.from([2]))), integer(serial), ECDSA_SHA256, name(options.issuer), seq(time(notBefore), time(notAfter)), name(options.subject), spki, explicit(3, seq(extension("2.5.29.14", false, octets(keyId(spki))), extension("2.5.29.35", false, seq(der(128, options.authorityKeyId))), ...options.extensions)));
529
+ return pem(seq(tbs, ECDSA_SHA256, bits(sign("sha256", tbs, options.signer))));
530
+ }
531
+ const newKeyPair = () => generateKeyPairSync("ec", { namedCurve: "prime256v1" });
532
+ /** A CA for one gateway. Its private key only ever exists in this process's memory. */
533
+ function createCertificateAuthority(days = 7) {
534
+ const { publicKey, privateKey } = newKeyPair();
535
+ const spki = publicKey.export({
536
+ type: "spki",
537
+ format: "der"
538
+ });
539
+ const subject = `reeval egress CA ${randomBytes(4).toString("hex")}`;
540
+ return {
541
+ cert: issue({
542
+ subject,
543
+ issuer: subject,
544
+ publicKey,
545
+ signer: privateKey,
546
+ authorityKeyId: keyId(spki),
547
+ days,
548
+ extensions: [extension("2.5.29.19", true, seq(bool(true), integer(Buffer.from([0])))), extension("2.5.29.15", true, keyUsage(134))]
549
+ }),
550
+ key: privateKey,
551
+ spki,
552
+ subject
553
+ };
554
+ }
555
+ /** A TLS server certificate for `host` (a DNS name or an IP address), signed by `ca`. */
556
+ function issueServerCertificate(ca, host) {
557
+ const { publicKey, privateKey } = newKeyPair();
558
+ const version = isIP(host);
559
+ const altName = version ? der(135, Buffer.from(version === 4 ? host.split(".").map(Number) : ipv6Bytes(host))) : der(130, Buffer.from(host, "ascii"));
560
+ return {
561
+ cert: issue({
562
+ subject: host,
563
+ issuer: ca.subject,
564
+ publicKey,
565
+ signer: ca.key,
566
+ authorityKeyId: keyId(ca.spki),
567
+ days: 7,
568
+ extensions: [
569
+ extension("2.5.29.19", true, seq()),
570
+ extension("2.5.29.15", true, keyUsage(128)),
571
+ extension("2.5.29.37", false, seq(oid("1.3.6.1.5.5.7.3.1"))),
572
+ extension("2.5.29.17", false, seq(altName))
573
+ ]
574
+ }),
575
+ key: privateKey
576
+ };
577
+ }
578
+ function ipv6Bytes(address) {
579
+ const [head = "", tail = ""] = address.split("::");
580
+ const parts = (s) => s ? s.split(":") : [];
581
+ const h = parts(head);
582
+ const t = parts(tail);
583
+ return [
584
+ ...h,
585
+ ...Array(8 - h.length - t.length).fill("0"),
586
+ ...t
587
+ ].flatMap((g) => {
588
+ const v = Number.parseInt(g, 16);
589
+ return [v >> 8, v & 255];
590
+ });
591
+ }
592
+ //#endregion
593
+ //#region ../sandbox-docker/src/egress/mux.ts
594
+ /**
595
+ * Many TCP streams over one byte pipe (a `docker exec` stdio), with per-stream credit so a slow
596
+ * reader only stalls its own stream. Frames are `type u8, id u32, length u32, payload`.
597
+ *
598
+ * Must stay self-contained, with no imports or references to module scope: the forwarder runs
599
+ * this function's source inside the container.
600
+ */
601
+ function createMux(options) {
602
+ const OPEN = 1;
603
+ const DATA = 2;
604
+ const FIN = 3;
605
+ const RST = 4;
606
+ const WINDOW = 5;
607
+ const READY = 6;
608
+ const windowSize = options.window ?? 262144;
609
+ const maxPayload = 65536;
610
+ const streams = /* @__PURE__ */ new Map();
611
+ let nextId = options.parity === 0 ? 2 : 1;
612
+ let pending = Buffer.alloc(0);
613
+ const frame = (type, id, payload = Buffer.alloc(0)) => {
614
+ const header = Buffer.alloc(9);
615
+ header.writeUInt8(type, 0);
616
+ header.writeUInt32BE(id, 1);
617
+ header.writeUInt32BE(payload.length, 5);
618
+ options.send(Buffer.concat([header, payload]));
619
+ };
620
+ const u32 = (n) => {
621
+ const b = Buffer.alloc(4);
622
+ b.writeUInt32BE(n, 0);
623
+ return b;
624
+ };
625
+ const create = (id) => {
626
+ const grant = () => {
627
+ if (state.unacked > 0 && !state.reset) frame(WINDOW, id, u32(state.unacked));
628
+ state.unacked = 0;
629
+ };
630
+ const stream = new options.Duplex({
631
+ allowHalfOpen: true,
632
+ read() {
633
+ grant();
634
+ },
635
+ write(chunk, _encoding, callback) {
636
+ for (let i = 0; i < chunk.length; i += maxPayload) frame(DATA, id, chunk.subarray(i, i + maxPayload));
637
+ state.credit -= chunk.length;
638
+ if (state.credit > 0) callback();
639
+ else state.blocked = callback;
640
+ },
641
+ final(callback) {
642
+ state.localEnded = true;
643
+ frame(FIN, id);
644
+ callback();
645
+ },
646
+ destroy(error, callback) {
647
+ streams.delete(id);
648
+ if (!state.reset && !(state.localEnded && state.remoteEnded)) frame(RST, id);
649
+ state.blocked = null;
650
+ callback(error);
651
+ }
652
+ });
653
+ const state = {
654
+ stream,
655
+ credit: windowSize,
656
+ unacked: 0,
657
+ blocked: null,
658
+ localEnded: false,
659
+ remoteEnded: false,
660
+ reset: false
661
+ };
662
+ streams.set(id, state);
663
+ return stream;
664
+ };
665
+ const handle = (type, id, payload) => {
666
+ if (type === READY) return options.onReady?.();
667
+ if (type === OPEN) return options.onOpen(create(id), payload.readUInt16BE(0));
668
+ const state = streams.get(id);
669
+ if (!state) return;
670
+ if (type === DATA) {
671
+ state.unacked += payload.length;
672
+ if (state.stream.push(payload) && state.unacked >= windowSize / 2) {
673
+ frame(WINDOW, id, u32(state.unacked));
674
+ state.unacked = 0;
675
+ }
676
+ } else if (type === WINDOW) {
677
+ state.credit += payload.readUInt32BE(0);
678
+ const blocked = state.blocked;
679
+ if (state.credit > 0 && blocked) {
680
+ state.blocked = null;
681
+ blocked();
682
+ }
683
+ } else if (type === FIN) {
684
+ state.remoteEnded = true;
685
+ state.stream.push(null);
686
+ } else if (type === RST) {
687
+ state.reset = true;
688
+ state.stream.destroy();
689
+ }
690
+ };
691
+ return {
692
+ receive(chunk) {
693
+ pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
694
+ while (pending.length >= 9) {
695
+ const length = pending.readUInt32BE(5);
696
+ if (pending.length < 9 + length) break;
697
+ const type = pending.readUInt8(0);
698
+ const id = pending.readUInt32BE(1);
699
+ const payload = pending.subarray(9, 9 + length);
700
+ pending = pending.subarray(9 + length);
701
+ handle(type, id, payload);
702
+ }
703
+ },
704
+ open(port) {
705
+ const id = nextId;
706
+ nextId += 2;
707
+ const stream = create(id);
708
+ const p = Buffer.alloc(2);
709
+ p.writeUInt16BE(port, 0);
710
+ frame(OPEN, id, p);
711
+ return stream;
712
+ },
713
+ ready() {
714
+ frame(READY, 0);
715
+ },
716
+ destroy() {
717
+ for (const state of [...streams.values()]) state.stream.destroy();
718
+ }
719
+ };
720
+ }
721
+ /**
722
+ * Pipes two duplexes into each other. An error, or one side closing before its end, tears both
723
+ * down; a clean end half-closes the other side through `pipe`.
724
+ */
725
+ function link(a, b) {
726
+ a.pipe(b);
727
+ b.pipe(a);
728
+ const kill = () => {
729
+ a.destroy();
730
+ b.destroy();
731
+ };
732
+ for (const s of [a, b]) {
733
+ s.on("error", kill);
734
+ s.on("close", () => {
735
+ if (!s.readableEnded) kill();
736
+ });
737
+ }
738
+ }
739
+ /**
740
+ * The container side: listens on 127.0.0.1:`proxyPort` and sends each connection to the host,
741
+ * and connects the host's streams to 127.0.0.1:<port> (the harness bridge). Self-contained for
742
+ * the same reason as {@link createMux}.
743
+ */
744
+ function forwarderMain(io, makeMux, linkStreams, proxyPort) {
745
+ const mux = makeMux({
746
+ Duplex: io.Duplex,
747
+ parity: 1,
748
+ send: (f) => io.output.write(f),
749
+ onOpen(stream, port) {
750
+ linkStreams(io.net.connect({
751
+ port,
752
+ host: "127.0.0.1",
753
+ allowHalfOpen: true
754
+ }), stream);
755
+ }
756
+ });
757
+ io.input.on("data", (chunk) => mux.receive(chunk));
758
+ io.input.on("end", () => io.exit(0));
759
+ const server = io.net.createServer({ allowHalfOpen: true }, (socket) => linkStreams(socket, mux.open(proxyPort)));
760
+ server.on("error", (error) => {
761
+ io.error(`reeval egress forwarder: ${error.message}`);
762
+ io.exit(1);
763
+ });
764
+ server.listen(proxyPort, "127.0.0.1", () => mux.ready());
765
+ }
766
+ /** The `node -e` script that runs {@link forwarderMain} in the container. */
767
+ const forwarderScript = (proxyPort) => `(${forwarderMain.toString()})({ net: require('node:net'), Duplex: require('node:stream').Duplex, input: process.stdin, output: process.stdout, error: (m) => process.stderr.write(m + '\\n'), exit: (c) => process.exit(c) }, ${createMux.toString()}, ${link.toString()}, ${proxyPort})`;
768
+ //#endregion
769
+ //#region ../sandbox-docker/src/egress/policy.ts
770
+ /** The Docker sandbox enforces every policy through its egress gateway, and records what passes. */
771
+ const DOCKER_EGRESS = {
772
+ enforce: "allowlist",
773
+ record: true
774
+ };
775
+ /** The hosts each harness's CLI talks to at runtime. */
776
+ const HARNESS_HOSTS = {
777
+ cursor: ["api2.cursor.sh", "*.cursor.sh"],
778
+ "claude-code": ["api.anthropic.com", "*.anthropic.com"],
779
+ antigravity: ["generativelanguage.googleapis.com"],
780
+ opencode: ["generativelanguage.googleapis.com", "mcp.exa.ai"],
781
+ goose: ["generativelanguage.googleapis.com"],
782
+ pi: ["generativelanguage.googleapis.com"],
783
+ codex: ["api.openai.com"]
784
+ };
785
+ /** The package registries and GitHub, for a restricted policy that still installs. */
786
+ const PACKAGE_HOSTS = Object.freeze([
787
+ "registry.npmjs.org",
788
+ "registry.yarnpkg.com",
789
+ "github.com",
790
+ "*.github.com",
791
+ "*.githubusercontent.com",
792
+ "pypi.org",
793
+ "files.pythonhosted.org"
794
+ ]);
795
+ /**
796
+ * The harness's own hosts plus `PACKAGE_HOSTS`. Only that harness's hosts: a Cursor attempt
797
+ * can't reach Anthropic.
798
+ */
799
+ const defaultAllowedHosts = (harness) => [...HARNESS_HOSTS[harness], ...PACKAGE_HOSTS];
800
+ /**
801
+ * Never reachable through the gateway, whatever the policy: the worker's own loopback (the
802
+ * container's `localhost` is its own) and link-local addresses such as cloud metadata. A
803
+ * connection to an unspecified address (`0.0.0.0`, `::`) reaches the local host too.
804
+ */
805
+ const ALWAYS_DENIED = [
806
+ "127.0.0.0/8",
807
+ "169.254.0.0/16",
808
+ "0.0.0.0/8",
809
+ "::/128",
810
+ "::1/128",
811
+ "fe80::/10"
812
+ ];
813
+ const isRestricted = (policy) => policy.mode !== "allow-all";
814
+ const blockList = (cidrs = []) => {
815
+ const list = new BlockList();
816
+ for (const cidr of cidrs) {
817
+ const [net = "", prefix] = cidr.split("/");
818
+ const type = isIP(net) === 6 ? "ipv6" : "ipv4";
819
+ list.addSubnet(net, Number(prefix ?? (type === "ipv6" ? 128 : 32)), type);
820
+ }
821
+ return list;
822
+ };
823
+ const inList = (list, address) => {
824
+ const v = isIP(address);
825
+ return v !== 0 && list.check(address, v === 6 ? "ipv6" : "ipv4");
826
+ };
827
+ const normalise = (host) => host.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
828
+ /**
829
+ * Private, carrier-grade NAT and unique-local ranges: a name matched only by a `*.` pattern
830
+ * mustn't resolve into them, or anyone who can add a DNS record under the pattern's domain
831
+ * reaches the worker's LAN.
832
+ */
833
+ const PRIVATE = blockList([
834
+ "10.0.0.0/8",
835
+ "100.64.0.0/10",
836
+ "172.16.0.0/12",
837
+ "192.168.0.0/16",
838
+ "fc00::/7"
839
+ ]);
840
+ /** How `host` matches: listed as it is, only by a `*.` pattern (never an IP address), or not. */
841
+ function hostMatch(patterns, host) {
842
+ const h = normalise(host);
843
+ const ps = patterns.map(normalise);
844
+ if (ps.includes(h)) return "exact";
845
+ if (isIP(h) || /(?:^|\.)\d+$/.test(h)) return void 0;
846
+ return ps.some((p) => p.startsWith("*.") && h.endsWith(p.slice(1))) ? "wildcard" : void 0;
847
+ }
848
+ /** `*.example.com` matches any subdomain of example.com, not example.com itself or an IP. */
849
+ const hostAllowed = (patterns, host) => hostMatch(patterns, host) !== void 0;
850
+ const defaultLookup = async (host) => (await lookup(host, {
851
+ all: true,
852
+ verbatim: true
853
+ })).map((a) => a.address);
854
+ /**
855
+ * Decides whether `host` may be reached and which address to connect to. The gateway connects
856
+ * to the address it checked, so a DNS answer can't change between the check and the connect.
857
+ */
858
+ function createPolicyCheck(policy, options = {}) {
859
+ const lookup = options.lookup ?? defaultLookup;
860
+ const always = blockList(options.allowLoopback ? [] : ALWAYS_DENIED);
861
+ const custom = policy.mode === "custom" ? policy : void 0;
862
+ const allowedCidrs = blockList(custom?.allowedCIDRs);
863
+ const deniedCidrs = blockList(custom?.deniedCIDRs);
864
+ return async (rawHost) => {
865
+ const host = normalise(rawHost);
866
+ if (policy.mode === "deny-all") return {
867
+ allowed: false,
868
+ status: 403,
869
+ reason: "the policy denies all egress"
870
+ };
871
+ const listed = hostMatch(custom?.allowedHosts ?? [], host);
872
+ if (custom && !listed && !custom.allowedCIDRs?.length) return {
873
+ allowed: false,
874
+ status: 403,
875
+ reason: `${host} isn't in the allowlist`
876
+ };
877
+ let addresses;
878
+ try {
879
+ addresses = isIP(host) ? [host] : await lookup(host);
880
+ } catch (error) {
881
+ return {
882
+ allowed: false,
883
+ status: 502,
884
+ reason: `can't resolve ${host}: ${error.message}`
885
+ };
886
+ }
887
+ const first = addresses[0];
888
+ if (!first) return {
889
+ allowed: false,
890
+ status: 502,
891
+ reason: `${host} has no addresses`
892
+ };
893
+ if (addresses.some((a) => inList(always, a))) return {
894
+ allowed: false,
895
+ status: 403,
896
+ reason: `${host} is a loopback or link-local address`
897
+ };
898
+ if (!custom) return {
899
+ allowed: true,
900
+ address: first
901
+ };
902
+ if (addresses.some((a) => inList(deniedCidrs, a))) return {
903
+ allowed: false,
904
+ status: 403,
905
+ reason: `${host} is in a denied range`
906
+ };
907
+ if (listed === "wildcard" && addresses.some((a) => inList(PRIVATE, a))) return {
908
+ allowed: false,
909
+ status: 403,
910
+ reason: `${host} resolves to a private address, which a *. pattern doesn't allow; list the host itself`
911
+ };
912
+ if (listed) return {
913
+ allowed: true,
914
+ address: first
915
+ };
916
+ if (custom.allowedCIDRs?.length && addresses.every((a) => inList(allowedCidrs, a))) return {
917
+ allowed: true,
918
+ address: first
919
+ };
920
+ return {
921
+ allowed: false,
922
+ status: 403,
923
+ reason: `${host} isn't in the allowlist`
924
+ };
925
+ };
926
+ }
927
+ //#endregion
928
+ //#region ../sandbox-docker/src/egress/rules.ts
929
+ const normaliseHost = (host) => host.toLowerCase().replace(/\.$/, "");
930
+ function matchPart(matcher, value, ignoreCase = false) {
931
+ const v = ignoreCase ? value.toLowerCase() : value;
932
+ if ("exact" in matcher) return v === (ignoreCase ? matcher.exact.toLowerCase() : matcher.exact);
933
+ if ("startsWith" in matcher) return v.startsWith(ignoreCase ? matcher.startsWith.toLowerCase() : matcher.startsWith);
934
+ return new RegExp(matcher.regex, ignoreCase ? "i" : "").test(value);
935
+ }
936
+ /** Every matcher must match at least one pair; keys compare case-insensitively for headers. */
937
+ function matchPairs(matchers, pairs, ignoreKeyCase) {
938
+ return (matchers ?? []).every((m) => pairs.some(([k, v]) => (!m.key || matchPart(m.key, k, ignoreKeyCase)) && (!m.value || matchPart(m.value, v))));
939
+ }
940
+ const headerPairs = (headers) => Object.entries(headers).flatMap(([k, v]) => v === void 0 ? [] : (Array.isArray(v) ? v : [v]).map((x) => [k, x]));
941
+ function matches(rule, request) {
942
+ const { match } = rule;
943
+ if (normaliseHost(match.host) !== normaliseHost(request.host)) return false;
944
+ const url = new URL(request.path, "https://placeholder.invalid");
945
+ if (match.path && !matchPart(match.path, url.pathname)) return false;
946
+ if (match.method && !match.method.some((m) => m.toUpperCase() === request.method.toUpperCase())) return false;
947
+ if (match.queryString && !matchPairs(match.queryString, [...url.searchParams], false)) return false;
948
+ return !match.headers || matchPairs(match.headers, headerPairs(request.headers), true);
949
+ }
950
+ /** Whether the request is one of the endpoints (host, path and method, where given). */
951
+ const onEndpoint = (endpoints, request) => endpoints.some((match) => matches({
952
+ match,
953
+ transform: { headers: {} }
954
+ }, request));
955
+ /** Whether the gateway must decrypt connections to `host`: some rule could apply there. */
956
+ const decrypts = (rules, host) => rules.some((r) => normaliseHost(r.match.host) === normaliseHost(host));
957
+ /**
958
+ * The request's headers after every matching rule, in order, so later rules win. Returns the
959
+ * (lower-case) names that were replaced, for logging without values. With `endpoints`, no rule
960
+ * applies off them, and a request carrying a rule's placeholder there is `refused`.
961
+ */
962
+ function applyTransformations(rules, request, endpoints) {
963
+ const headers = { ...request.headers };
964
+ const replaced = /* @__PURE__ */ new Set();
965
+ const matching = rules.filter((rule) => matches(rule, request));
966
+ if (endpoints && matching.length && !onEndpoint(endpoints, request)) return {
967
+ headers,
968
+ replaced: [],
969
+ refused: matching.some((r) => r.match.headers?.length)
970
+ };
971
+ for (const rule of matching) for (const [name, value] of Object.entries(rule.transform.headers)) {
972
+ headers[name.toLowerCase()] = value;
973
+ replaced.add(name.toLowerCase());
974
+ }
975
+ return {
976
+ headers,
977
+ replaced: [...replaced],
978
+ refused: false
979
+ };
980
+ }
981
+ //#endregion
982
+ //#region ../sandbox-docker/src/egress/substitution.ts
983
+ const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
984
+ /** A fresh `reeval_secret_<32 base62>`: URL-safe, so it survives any encoding a client applies. */
985
+ function placeholder() {
986
+ let out = "reeval_secret_";
987
+ for (let i = 0; i < 32; i++) out += BASE62[randomInt(62)];
988
+ return out;
989
+ }
990
+ /** The substitutions whose hosts include `host`. */
991
+ const substitutionsFor = (subs, host) => subs.filter((s) => hostAllowed(s.hosts, host));
992
+ /** Whether the gateway must decrypt connections to `host` for some substitution. */
993
+ const substitutes = (subs, host) => substitutionsFor(subs, host).length > 0;
994
+ const replaceIn = (value, from, to) => value.includes(from) ? value.split(from).join(to) : value;
995
+ /**
996
+ * The request after its host's substitutions: placeholders replaced in header values, and in the
997
+ * query string (URL-encoded) for those opted in. Returns the names substituted, for logging without values.
998
+ */
999
+ function substituteRequest(subs, request) {
1000
+ const own = substitutionsFor(subs, request.host);
1001
+ const names = /* @__PURE__ */ new Set();
1002
+ const headers = {};
1003
+ for (const [key, value] of Object.entries(request.headers)) {
1004
+ if (value === void 0) continue;
1005
+ const one = (v) => {
1006
+ let out = v;
1007
+ for (const s of own) {
1008
+ if (!s.in.includes("header") || !out.includes(s.placeholder)) continue;
1009
+ out = replaceIn(out, s.placeholder, s.value);
1010
+ names.add(s.name);
1011
+ }
1012
+ return out;
1013
+ };
1014
+ headers[key] = Array.isArray(value) ? value.map(one) : one(value);
1015
+ }
1016
+ let path = request.path;
1017
+ const q = path.indexOf("?");
1018
+ if (q !== -1) {
1019
+ let query = path.slice(q);
1020
+ for (const s of own) {
1021
+ if (!s.in.includes("query") || !query.includes(s.placeholder)) continue;
1022
+ query = replaceIn(query, s.placeholder, encodeURIComponent(s.value));
1023
+ names.add(s.name);
1024
+ }
1025
+ path = path.slice(0, q) + query;
1026
+ }
1027
+ return {
1028
+ headers,
1029
+ path,
1030
+ names: [...names]
1031
+ };
1032
+ }
1033
+ /**
1034
+ * Where a request to the secrets' host still carries a placeholder the gateway leaves in place,
1035
+ * because the secret doesn't substitute there: the query without `in: ['query']`, or a header
1036
+ * with only `in: ['query']` or `['body']`. Such a request fails upstream; this names why.
1037
+ */
1038
+ function unsubstituted(subs, request) {
1039
+ const out = [];
1040
+ const q = request.path.indexOf("?");
1041
+ const query = q === -1 ? "" : request.path.slice(q);
1042
+ const headers = Object.values(request.headers).flat().filter(Boolean).join("\n");
1043
+ for (const s of substitutionsFor(subs, request.host)) {
1044
+ if (!s.in.includes("query") && query.includes(s.placeholder)) out.push({
1045
+ name: s.name,
1046
+ where: "query",
1047
+ in: s.in
1048
+ });
1049
+ if (!s.in.includes("header") && headers.includes(s.placeholder)) out.push({
1050
+ name: s.name,
1051
+ where: "header",
1052
+ in: s.in
1053
+ });
1054
+ }
1055
+ return out;
1056
+ }
1057
+ /** A response header value with every real value on the host put back as its placeholder. */
1058
+ const scrubValue = (subs, value) => subs.reduce((out, s) => replaceIn(out, s.value, s.placeholder), value);
1059
+ /** Response headers with every real value on the host put back as its placeholder. */
1060
+ function scrubHeaders(subs, headers) {
1061
+ if (subs.length === 0) return headers;
1062
+ const one = (v) => scrubValue(subs, v);
1063
+ return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, v === void 0 ? v : Array.isArray(v) ? v.map(one) : one(String(v))]));
1064
+ }
1065
+ /**
1066
+ * Replaces byte strings in a stream, including across chunk boundaries: it holds back the
1067
+ * longest `from` less one byte, so a match split between chunks is still found.
1068
+ */
1069
+ var ReplaceStream = class extends Transform {
1070
+ #pairs;
1071
+ #hold;
1072
+ #tail = Buffer.alloc(0);
1073
+ /** How many replacements were made. */
1074
+ count = 0;
1075
+ constructor(pairs) {
1076
+ super();
1077
+ this.#pairs = pairs.filter(([from]) => from.length > 0).map(([from, to]) => [Buffer.from(from), Buffer.from(to)]);
1078
+ this.#hold = Math.max(0, ...this.#pairs.map(([from]) => from.length - 1));
1079
+ }
1080
+ /**
1081
+ * Replaces every whole match in `data` (original bytes only: output is never re-scanned), and
1082
+ * returns where the unscanned tail starts, which is `data.length` when `final`.
1083
+ */
1084
+ #scan(data, final) {
1085
+ const parts = [];
1086
+ let pos = 0;
1087
+ for (;;) {
1088
+ let best = -1;
1089
+ let pair;
1090
+ for (const p of this.#pairs) {
1091
+ const i = data.indexOf(p[0], pos);
1092
+ if (i !== -1 && (best === -1 || i < best)) {
1093
+ best = i;
1094
+ pair = p;
1095
+ }
1096
+ }
1097
+ if (best === -1 || !pair) break;
1098
+ parts.push(data.subarray(pos, best), pair[1]);
1099
+ pos = best + pair[0].length;
1100
+ this.count++;
1101
+ }
1102
+ const rest = final ? data.length : Math.max(pos, data.length - this.#hold);
1103
+ parts.push(data.subarray(pos, rest));
1104
+ return {
1105
+ out: Buffer.concat(parts),
1106
+ rest
1107
+ };
1108
+ }
1109
+ _transform(chunk, _enc, done) {
1110
+ const data = Buffer.concat([this.#tail, chunk]);
1111
+ const { out, rest } = this.#scan(data, false);
1112
+ this.#tail = Buffer.from(data.subarray(rest));
1113
+ if (out.length) this.push(out);
1114
+ done();
1115
+ }
1116
+ _flush(done) {
1117
+ const { out } = this.#scan(this.#tail, true);
1118
+ if (out.length) this.push(out);
1119
+ done();
1120
+ }
1121
+ };
1122
+ /** The substitutions whose values are scrubbed from response bodies (see `scrubBodies`). */
1123
+ const scrubbedInBodies = (subs) => subs.filter((s) => s.scrubBodies !== false);
1124
+ /** A stream putting each real value back as its placeholder: for responses from brokered hosts. */
1125
+ const scrubber = (subs) => new ReplaceStream(scrubbedInBodies(subs).map((s) => [s.value, s.placeholder]));
1126
+ /** A stream swapping placeholders for values, for request bodies of secrets that opt in. */
1127
+ const bodySubstituter = (subs) => new ReplaceStream(subs.filter((s) => s.in.includes("body")).map((s) => [s.placeholder, s.value]));
1128
+ //#endregion
1129
+ //#region ../sandbox-docker/src/egress/tokens.ts
1130
+ const JWT = /^[\w-]+\.[\w-]+\.[\w-]+$/;
1131
+ /**
1132
+ * A placeholder for a token. A JWT keeps its header and claims, so a client reading its `exp` sees
1133
+ * the real one, and gets `reeval_secret_…` for a signature; anything else is a plain `reeval_secret_…`.
1134
+ */
1135
+ function tokenPlaceholder(token) {
1136
+ if (!JWT.test(token)) return placeholder();
1137
+ const [header, claims] = token.split(".");
1138
+ return `${header}.${claims}.${placeholder()}`;
1139
+ }
1140
+ /**
1141
+ * The exchange response with each token field replaced by a placeholder, and a substitution per
1142
+ * token. A token already seen keeps its placeholder. Throws when the body isn't a JSON object.
1143
+ */
1144
+ function tokeniseResponse(derived, body, known) {
1145
+ const parsed = JSON.parse(body);
1146
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("the response is not a JSON object");
1147
+ const out = { ...parsed };
1148
+ const added = [];
1149
+ const fields = [];
1150
+ for (const field of derived.fields) {
1151
+ const value = out[field];
1152
+ if (typeof value !== "string" || value === "") continue;
1153
+ const existing = [...known, ...added].find((s) => s.value === value);
1154
+ const sub = existing ?? {
1155
+ name: `${derived.name} ${field}`,
1156
+ placeholder: tokenPlaceholder(value),
1157
+ value,
1158
+ hosts: derived.hosts,
1159
+ in: ["header"],
1160
+ scrubBodies: false
1161
+ };
1162
+ if (!existing) added.push(sub);
1163
+ out[field] = sub.placeholder;
1164
+ fields.push(field);
1165
+ }
1166
+ return {
1167
+ body: JSON.stringify(out),
1168
+ added,
1169
+ fields
1170
+ };
1171
+ }
1172
+ //#endregion
1173
+ //#region ../sandbox-docker/src/egress/proxy.ts
1174
+ const HOP_BY_HOP = /* @__PURE__ */ new Set([
1175
+ "connection",
1176
+ "keep-alive",
1177
+ "proxy-connection",
1178
+ "proxy-authorization",
1179
+ "proxy-authenticate",
1180
+ "te",
1181
+ "trailer",
1182
+ "transfer-encoding",
1183
+ "upgrade",
1184
+ "http2-settings",
1185
+ "host"
1186
+ ]);
1187
+ const endToEnd = (headers, keep = []) => Object.fromEntries(Object.entries(headers).filter(([k, v]) => v !== void 0 && !k.startsWith(":") && (!HOP_BY_HOP.has(k) || keep.includes(k))));
1188
+ /** `host:port` from a CONNECT target or an authority, IPv6 in brackets. */
1189
+ function parseAuthority(authority, defaultPort) {
1190
+ const m = /^\[([^\]]+)\](?::(\d+))?$/.exec(authority) ?? /^([^:]+)(?::(\d+))?$/.exec(authority);
1191
+ return {
1192
+ host: (m?.[1] ?? authority).toLowerCase(),
1193
+ port: Number(m?.[2] ?? defaultPort)
1194
+ };
1195
+ }
1196
+ const pathOnly = (path) => path.split("?")[0];
1197
+ function reply(socket, status, reason) {
1198
+ const text = http.STATUS_CODES[status] ?? "";
1199
+ socket.end(`HTTP/1.1 ${status} ${text}\r\ncontent-type: text/plain\r\nconnection: close\r\n\r\n${reason}\n`);
1200
+ }
1201
+ /** Counts the bytes passing through, without changing the flow. */
1202
+ const counter = (count) => new Transform({ transform(chunk, _encoding, done) {
1203
+ count(chunk.length);
1204
+ done(null, chunk);
1205
+ } });
1206
+ const plainHeaders = (headers) => Object.fromEntries(Object.entries(headers).flatMap(([k, v]) => v === void 0 ? [] : [[k, Array.isArray(v) ? v.map(String) : String(v)]]));
1207
+ function createEgressProxy(options) {
1208
+ const log = options.log ?? (() => {});
1209
+ const emit = (event) => {
1210
+ try {
1211
+ options.onEvent?.(event);
1212
+ } catch {}
1213
+ };
1214
+ let rules = [];
1215
+ let subs = [];
1216
+ /** Substitutions for derived tokens, added as their exchanges answer; kept by setSubstitutions. */
1217
+ let tokens = [];
1218
+ const allSubs = () => tokens.length ? [...subs, ...tokens] : subs;
1219
+ const derived = options.derivedTokens ?? [];
1220
+ const tokenHosts = derived.flatMap((d) => [d.endpoint.host, ...d.hosts]);
1221
+ let policyCheck = createPolicyCheck(options.policy, options);
1222
+ const check = async (host) => {
1223
+ const verdict = await policyCheck(host);
1224
+ emit(verdict.allowed ? {
1225
+ type: "dns",
1226
+ host,
1227
+ allowed: true,
1228
+ address: verdict.address
1229
+ } : {
1230
+ type: "dns",
1231
+ host,
1232
+ allowed: false,
1233
+ reason: verdict.reason
1234
+ });
1235
+ return verdict;
1236
+ };
1237
+ /** Bytes each sandbox connection carried, both ways, for its tunnel's event. */
1238
+ const carried = /* @__PURE__ */ new WeakMap();
1239
+ const leaves = /* @__PURE__ */ new Map();
1240
+ const open = /* @__PURE__ */ new Set();
1241
+ const sessions = /* @__PURE__ */ new Map();
1242
+ const agent = new https.Agent({ keepAlive: true });
1243
+ const ca = options.upstreamCA ? [...tls.rootCertificates, options.upstreamCA] : void 0;
1244
+ const track = (s) => {
1245
+ open.add(s);
1246
+ s.once("close", () => open.delete(s));
1247
+ return s;
1248
+ };
1249
+ const contacted = /* @__PURE__ */ new Map();
1250
+ const blocked = /* @__PURE__ */ new Map();
1251
+ /** The host each sandbox connection's bytes count towards: its latest allowed request's. */
1252
+ const counting = /* @__PURE__ */ new WeakMap();
1253
+ /** Bytes a connection sent before the policy decided whose they are: the request itself. */
1254
+ const unclaimed = /* @__PURE__ */ new WeakMap();
1255
+ const contact = (connection, host, port) => {
1256
+ const key = `${host}:${port}`;
1257
+ let entry = contacted.get(key);
1258
+ if (!entry) {
1259
+ entry = {
1260
+ host,
1261
+ port,
1262
+ connections: 0,
1263
+ sent: 0,
1264
+ received: 0
1265
+ };
1266
+ contacted.set(key, entry);
1267
+ }
1268
+ entry.connections++;
1269
+ entry.sent += unclaimed.get(connection) ?? 0;
1270
+ unclaimed.delete(connection);
1271
+ counting.set(connection, entry);
1272
+ };
1273
+ const block = (connection, host, port, reason) => {
1274
+ const key = `${host}:${port}`;
1275
+ const entry = blocked.get(key) ?? {
1276
+ host,
1277
+ port,
1278
+ connections: 0,
1279
+ reason
1280
+ };
1281
+ entry.connections++;
1282
+ blocked.set(key, entry);
1283
+ counting.delete(connection);
1284
+ unclaimed.delete(connection);
1285
+ };
1286
+ /** Counts what the connection carries each way, without reading it or changing its flow. */
1287
+ const countBytes = (connection) => {
1288
+ const total = {
1289
+ sent: 0,
1290
+ received: 0
1291
+ };
1292
+ carried.set(connection, total);
1293
+ const push = connection.push.bind(connection);
1294
+ connection.push = (chunk, encoding) => {
1295
+ const entry = counting.get(connection);
1296
+ const n = chunk ? Buffer.byteLength(chunk) : 0;
1297
+ total.sent += n;
1298
+ if (entry) entry.sent += n;
1299
+ else unclaimed.set(connection, (unclaimed.get(connection) ?? 0) + n);
1300
+ return push(chunk, encoding);
1301
+ };
1302
+ const write = connection.write.bind(connection);
1303
+ connection.write = ((chunk, ...rest) => {
1304
+ const entry = counting.get(connection);
1305
+ const n = chunk ? Buffer.byteLength(chunk) : 0;
1306
+ total.received += n;
1307
+ if (entry) entry.received += n;
1308
+ return write(chunk, ...rest);
1309
+ });
1310
+ };
1311
+ const byHost = (entries) => [...entries].map((e) => ({ ...e })).sort((a, b) => a.host.localeCompare(b.host) || a.port - b.port);
1312
+ /** Collects one request's event as it goes, and emits it when the response closes. */
1313
+ const requestEvent = (res, base) => {
1314
+ const startedAt = Date.now();
1315
+ const event = {
1316
+ type: "request",
1317
+ ...base,
1318
+ allowed: true,
1319
+ sent: 0,
1320
+ received: 0,
1321
+ substituted: [],
1322
+ credentials: [],
1323
+ ms: 0
1324
+ };
1325
+ let emitted = false;
1326
+ res.once("close", () => {
1327
+ if (emitted) return;
1328
+ emitted = true;
1329
+ emit({
1330
+ ...event,
1331
+ status: event.status ?? res.statusCode,
1332
+ ms: Date.now() - startedAt
1333
+ });
1334
+ });
1335
+ return {
1336
+ refuse(reason) {
1337
+ event.allowed = false;
1338
+ event.reason = reason;
1339
+ },
1340
+ set(fields) {
1341
+ Object.assign(event, fields);
1342
+ },
1343
+ sent: (n) => {
1344
+ event.sent += n;
1345
+ },
1346
+ received: (n) => {
1347
+ event.received += n;
1348
+ }
1349
+ };
1350
+ };
1351
+ const credentialsFor = (host) => {
1352
+ let creds = leaves.get(host);
1353
+ if (!creds) {
1354
+ const leaf = issueServerCertificate(options.ca, host);
1355
+ creds = {
1356
+ key: leaf.key.export({
1357
+ type: "pkcs8",
1358
+ format: "pem"
1359
+ }),
1360
+ cert: leaf.cert + options.ca.cert
1361
+ };
1362
+ leaves.set(host, creds);
1363
+ }
1364
+ return creds;
1365
+ };
1366
+ const front = http.createServer();
1367
+ front.on("connect", (req, socket, head) => {
1368
+ const { host, port } = parseAuthority(req.url ?? "", 443);
1369
+ const startedAt = Date.now();
1370
+ const tunnel = {
1371
+ type: "request",
1372
+ method: "CONNECT",
1373
+ url: `${host}:${port}`,
1374
+ host,
1375
+ port,
1376
+ via: "tunnel",
1377
+ substituted: [],
1378
+ credentials: [],
1379
+ requestHeaders: plainHeaders(req.headers)
1380
+ };
1381
+ check(host).then((verdict) => {
1382
+ if (!verdict.allowed) {
1383
+ log(`reeval:egress CONNECT ${host}:${port} denied (${verdict.reason})`);
1384
+ block(socket, host, port, verdict.reason);
1385
+ emit({
1386
+ ...tunnel,
1387
+ allowed: false,
1388
+ status: verdict.status,
1389
+ reason: verdict.reason,
1390
+ sent: 0,
1391
+ received: 0,
1392
+ ms: Date.now() - startedAt
1393
+ });
1394
+ return reply(socket, verdict.status, `blocked by the reeval egress policy: ${verdict.reason}`);
1395
+ }
1396
+ contact(socket, host, port);
1397
+ const target = {
1398
+ host,
1399
+ port,
1400
+ address: verdict.address
1401
+ };
1402
+ if (decrypts(rules, host) || substitutes(allSubs(), host) || hostAllowed(tokenHosts, host)) {
1403
+ log(`reeval:egress CONNECT ${host}:${port} decrypt`);
1404
+ socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
1405
+ if (head.length) socket.unshift(head);
1406
+ return decrypt(socket, target);
1407
+ }
1408
+ log(`reeval:egress CONNECT ${host}:${port} tunnel`);
1409
+ let status = 200;
1410
+ let reason;
1411
+ const before = { ...carried.get(socket) ?? {
1412
+ sent: 0,
1413
+ received: 0
1414
+ } };
1415
+ socket.once("close", () => {
1416
+ const after = carried.get(socket) ?? before;
1417
+ emit({
1418
+ ...tunnel,
1419
+ allowed: true,
1420
+ status,
1421
+ ...reason ? { reason } : {},
1422
+ sent: after.sent - before.sent,
1423
+ received: after.received - before.received,
1424
+ ms: Date.now() - startedAt
1425
+ });
1426
+ });
1427
+ const upstream = track(net.connect({
1428
+ host: target.address,
1429
+ port,
1430
+ allowHalfOpen: true
1431
+ }));
1432
+ upstream.once("connect", () => {
1433
+ socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
1434
+ if (head.length) upstream.write(head);
1435
+ link(socket, upstream);
1436
+ });
1437
+ upstream.once("error", (error) => {
1438
+ log(`reeval:egress CONNECT ${host}:${port} failed (${error.message})`);
1439
+ status = 502;
1440
+ reason = `couldn't reach ${host}:${port}: ${error.message}`;
1441
+ reply(socket, 502, `reeval egress gateway couldn't reach ${host}:${port}: ${error.message}`);
1442
+ });
1443
+ });
1444
+ });
1445
+ front.on("request", (req, res) => {
1446
+ let url;
1447
+ try {
1448
+ url = new URL(req.url ?? "");
1449
+ } catch {
1450
+ return res.writeHead(400).end("reeval egress gateway: expected an absolute URL\n");
1451
+ }
1452
+ if (url.protocol !== "http:") return res.writeHead(400).end("reeval egress gateway: http only\n");
1453
+ const { host, port } = parseAuthority(url.host, 80);
1454
+ const done = requestEvent(res, {
1455
+ method: req.method ?? "GET",
1456
+ url: `http://${host}:${port}${url.pathname}`,
1457
+ host,
1458
+ port,
1459
+ via: "http",
1460
+ requestHeaders: plainHeaders(req.headers)
1461
+ });
1462
+ check(host).then((verdict) => {
1463
+ if (!verdict.allowed) {
1464
+ log(`reeval:egress ${req.method} http://${host}:${port} denied (${verdict.reason})`);
1465
+ block(req.socket, host, port, verdict.reason);
1466
+ done.refuse(verdict.reason);
1467
+ return res.writeHead(verdict.status).end(`blocked by the reeval egress policy: ${verdict.reason}\n`);
1468
+ }
1469
+ log(`reeval:egress ${req.method} http://${host}:${port}${url.pathname}`);
1470
+ contact(req.socket, host, port);
1471
+ const upstream = http.request({
1472
+ host: verdict.address,
1473
+ port,
1474
+ method: req.method,
1475
+ path: url.pathname + url.search,
1476
+ headers: {
1477
+ ...endToEnd(req.headers),
1478
+ host: url.host
1479
+ }
1480
+ });
1481
+ forwardHttp1(req.pipe(counter(done.sent)), res, upstream, [], void 0, done);
1482
+ });
1483
+ });
1484
+ front.on("upgrade", (_req, socket) => {
1485
+ reply(socket, 501, "reeval egress gateway: use CONNECT for websockets");
1486
+ });
1487
+ /**
1488
+ * Terminates TLS with a certificate for the CONNECT host. One server per connection, since the
1489
+ * host comes from CONNECT; it serves HTTP/2 and HTTP/1.1 (with upgrades) by ALPN.
1490
+ */
1491
+ function decrypt(socket, target) {
1492
+ const server = http2.createSecureServer({
1493
+ ...credentialsFor(target.host),
1494
+ allowHTTP1: true
1495
+ }, (req, res) => void onDecrypted(target, req, res));
1496
+ server.on("upgrade", (req, s, head) => onUpgrade(target, req, track(s), head));
1497
+ server.on("tlsClientError", () => socket.destroy());
1498
+ server.on("sessionError", () => socket.destroy());
1499
+ server.emit("connection", socket);
1500
+ }
1501
+ /**
1502
+ * The headers and path to send upstream, the substitutions that apply there, or why the request
1503
+ * is refused (already logged): a Host that isn't the CONNECT target, or an unbrokered endpoint.
1504
+ */
1505
+ function transformed(target, method, path, headers, version) {
1506
+ const named = String(headers[":authority"] ?? headers.host ?? "");
1507
+ const asked = parseAuthority(named, target.port).host;
1508
+ if (named && asked !== target.host) {
1509
+ const reason = `reeval egress gateway: the request names ${asked}, but the connection is to ${target.host}; send it on a connection to ${asked}.`;
1510
+ log(`reeval:egress ${method} https://${target.host}${pathOnly(path)} refused (Host ${asked})`);
1511
+ return {
1512
+ headers: {},
1513
+ path,
1514
+ own: [],
1515
+ names: [],
1516
+ credentials: [],
1517
+ refused: {
1518
+ status: 421,
1519
+ reason
1520
+ }
1521
+ };
1522
+ }
1523
+ const { headers: out, replaced, refused } = applyTransformations(rules, {
1524
+ host: target.host,
1525
+ method,
1526
+ path,
1527
+ headers: endToEnd(headers, ["upgrade", "connection"])
1528
+ }, options.endpoints);
1529
+ const url = `https://${target.host}${pathOnly(path)}`;
1530
+ if (refused) {
1531
+ log(`reeval:egress ${method} ${url} HTTP/${version} refused (not a brokered endpoint)`);
1532
+ options.onRefused?.({
1533
+ method,
1534
+ url
1535
+ });
1536
+ return {
1537
+ headers: {},
1538
+ path,
1539
+ own: [],
1540
+ names: [],
1541
+ credentials: [],
1542
+ refused: {
1543
+ status: 403,
1544
+ reason: `reeval egress gateway: refused ${method} ${url}. It carries a brokered credential, and it isn't one of the endpoints reeval brokers it for.`
1545
+ }
1546
+ };
1547
+ }
1548
+ const own = substitutionsFor(allSubs(), target.host);
1549
+ const request = {
1550
+ host: target.host,
1551
+ path,
1552
+ headers: out
1553
+ };
1554
+ const substituted = substituteRequest(own, request);
1555
+ const kept = unsubstituted(own, request);
1556
+ for (const k of kept) options.onUnsubstituted?.({
1557
+ ...k,
1558
+ method,
1559
+ url
1560
+ });
1561
+ const exchange = derived.find((d) => onEndpoint([d.endpoint], {
1562
+ host: target.host,
1563
+ method,
1564
+ path,
1565
+ headers: {}
1566
+ }));
1567
+ const readsModel = onEndpoint(options.modelResponses ?? [], {
1568
+ host: target.host,
1569
+ method,
1570
+ path,
1571
+ headers: {}
1572
+ });
1573
+ if (scrubbedInBodies(own).length || exchange || readsModel) delete substituted.headers["accept-encoding"];
1574
+ const notes = [
1575
+ replaced.length ? `set ${replaced.join(", ")}` : "",
1576
+ substituted.names.length ? `substituted ${substituted.names.join(", ")}` : "",
1577
+ ...kept.map((k) => `kept ${k.name}'s placeholder in the ${k.where}`)
1578
+ ].filter(Boolean);
1579
+ log(`reeval:egress ${method} ${url} HTTP/${version}${notes.length ? ` (${notes.join("; ")})` : ""}`);
1580
+ return {
1581
+ headers: substituted.headers,
1582
+ path: substituted.path,
1583
+ own,
1584
+ names: substituted.names,
1585
+ credentials: replaced,
1586
+ ...exchange ? { exchange } : {},
1587
+ ...readsModel ? { readsModel } : {}
1588
+ };
1589
+ }
1590
+ /**
1591
+ * The request body as sent upstream: through the substituter when a secret there opts in to
1592
+ * bodies, unless the body is encoded (gzip and the like), which is sent as is and fails closed.
1593
+ */
1594
+ function requestBody(req, headers, own, t) {
1595
+ const counted = t ? req.pipe(counter(t.sent)) : req;
1596
+ if (String(headers["content-encoding"] ?? "identity") !== "identity" || !own.some((s) => s.in.includes("body"))) return counted;
1597
+ delete headers["content-length"];
1598
+ return counted.pipe(bodySubstituter(own));
1599
+ }
1600
+ /** The response body as sent to the sandbox: real values put back as placeholders. */
1601
+ const scrubs = options.scrubResponses !== false;
1602
+ function responseBody(up, own, t, readsModel = false) {
1603
+ if (readsModel && t) readModel(up, t);
1604
+ const body = scrubbedInBodies(own).length && scrubs ? up.pipe(scrubber(own)) : up;
1605
+ return t ? body.pipe(counter(t.received)) : body;
1606
+ }
1607
+ const responseHeaders = (headers, own) => {
1608
+ const out = endToEnd(headers);
1609
+ if (!own.length || !scrubs) return out;
1610
+ if (scrubbedInBodies(own).length) delete out["content-length"];
1611
+ return scrubHeaders(own, out);
1612
+ };
1613
+ /**
1614
+ * How far into a response the gateway looks for the model that served it: OpenAI's first event
1615
+ * echoes the request's instructions and tools before it.
1616
+ */
1617
+ const MODEL_WINDOW = 524288;
1618
+ /**
1619
+ * Records the first `"model"` value in the response body, and the `"effort"` of the
1620
+ * `"reasoning"` object after it, reading alongside its flow. Inside a JSON string the quotes are
1621
+ * escaped, so text that merely mentions them doesn't match.
1622
+ */
1623
+ function readModel(up, t) {
1624
+ let seen = "";
1625
+ const onData = (chunk) => {
1626
+ seen += chunk.toString("utf8");
1627
+ const model = /"model"\s*:\s*"([^"\\]{1,200})"/.exec(seen);
1628
+ const effort = model ? /"reasoning"\s*:\s*\{[^{}]*?"effort"\s*:\s*"(\w{1,40})"/.exec(seen.slice(model.index)) : null;
1629
+ if (model) t.set({ responseModel: model[1] });
1630
+ if (effort) t.set({ responseEffort: effort[1] });
1631
+ if (effort || seen.length > MODEL_WINDOW) up.off("data", onData);
1632
+ };
1633
+ up.on("data", onData);
1634
+ }
1635
+ /** Most an exchange's response may be; it's a few hundred bytes of JSON. */
1636
+ const EXCHANGE_LIMIT = 1 << 20;
1637
+ /**
1638
+ * A token exchange's response, read whole, with each token replaced by a placeholder. A success
1639
+ * that can't be read (compressed, too big, not JSON) is a 502: the real tokens never go through.
1640
+ */
1641
+ function tokenised(exchange, url, status, headers, up, res, own, t) {
1642
+ const chunks = [];
1643
+ let size = 0;
1644
+ const refuse = (why) => {
1645
+ log(`reeval:egress ${url} response refused (${why})`);
1646
+ if (!res.headersSent) res.writeHead(502, { "content-type": "text/plain" }).end(`reeval egress gateway: couldn't keep ${exchange.name}'s tokens out of the sandbox: ${why}
1647
+ `);
1648
+ else res.destroy();
1649
+ };
1650
+ const scrubbed = responseBody(up, own, t);
1651
+ scrubbed.on("data", (chunk) => {
1652
+ size += chunk.length;
1653
+ if (size <= EXCHANGE_LIMIT) chunks.push(chunk);
1654
+ });
1655
+ up.once("error", (error) => refuse(error.message));
1656
+ scrubbed.once("end", () => {
1657
+ const body = Buffer.concat(chunks);
1658
+ const out = responseHeaders(headers, own);
1659
+ t?.set({ responseHeaders: plainHeaders(out) });
1660
+ if (status < 200 || status >= 300) return res.writeHead(status, out).end(body);
1661
+ const encoding = String(headers["content-encoding"] ?? "identity");
1662
+ if (encoding !== "identity") return refuse(`the response is ${encoding}-encoded`);
1663
+ if (size > EXCHANGE_LIMIT) return refuse(`the response is over ${EXCHANGE_LIMIT} bytes`);
1664
+ let result;
1665
+ try {
1666
+ result = tokeniseResponse(exchange, body.toString("utf8"), tokens);
1667
+ } catch (error) {
1668
+ return refuse(error.message);
1669
+ }
1670
+ tokens = [...tokens, ...result.added];
1671
+ log(`reeval:egress ${url} tokenised ${result.fields.join(", ") || "nothing"} (${exchange.name})`);
1672
+ const text = Buffer.from(result.body);
1673
+ delete out["content-encoding"];
1674
+ res.writeHead(status, {
1675
+ ...out,
1676
+ "content-length": String(text.length)
1677
+ }).end(text);
1678
+ });
1679
+ }
1680
+ async function onDecrypted(target, req, res) {
1681
+ const method = req.method ?? "GET";
1682
+ const url = `https://${target.host}${pathOnly(req.url ?? "/")}`;
1683
+ const t = requestEvent(res, {
1684
+ method,
1685
+ url,
1686
+ host: target.host,
1687
+ port: target.port,
1688
+ via: "decrypted",
1689
+ requestHeaders: plainHeaders(req.headers)
1690
+ });
1691
+ const { headers, path, own, names, credentials, refused, exchange, readsModel } = transformed(target, method, req.url ?? "/", req.headers, req.httpVersion);
1692
+ t.set({
1693
+ substituted: names,
1694
+ credentials
1695
+ });
1696
+ if (refused) {
1697
+ t.refuse(refused.reason);
1698
+ req.resume();
1699
+ return res.writeHead(refused.status, { "content-type": "text/plain" }).end(`${refused.reason}\n`);
1700
+ }
1701
+ delete headers.upgrade;
1702
+ delete headers.connection;
1703
+ const body = requestBody(req, headers, own, t);
1704
+ const authority = target.port === 443 ? target.host : `${target.host}:${target.port}`;
1705
+ const session = req.httpVersionMajor === 2 ? await upstreamSession(target) : null;
1706
+ if (!session) return forwardHttp1(body, res, https.request({
1707
+ host: target.address,
1708
+ servername: net.isIP(target.host) ? void 0 : target.host,
1709
+ port: target.port,
1710
+ method,
1711
+ path,
1712
+ headers: {
1713
+ ...headers,
1714
+ host: authority
1715
+ },
1716
+ agent,
1717
+ ca
1718
+ }), own, exchange && {
1719
+ exchange,
1720
+ url
1721
+ }, t, readsModel);
1722
+ const stream = session.request({
1723
+ ...headers,
1724
+ ":method": method,
1725
+ ":path": path,
1726
+ ":scheme": "https",
1727
+ ":authority": authority
1728
+ });
1729
+ const fail = (error) => {
1730
+ if (!res.headersSent) res.writeHead(502).end(`reeval egress gateway: ${error.message}\n`);
1731
+ else res.destroy();
1732
+ };
1733
+ stream.once("error", fail);
1734
+ stream.once("response", (h) => {
1735
+ if (exchange) return tokenised(exchange, url, Number(h[":status"]), h, stream, res, own, t);
1736
+ const out = responseHeaders(h, own);
1737
+ t.set({ responseHeaders: plainHeaders(out) });
1738
+ res.writeHead(Number(h[":status"]), out);
1739
+ stream.once("trailers", (trailers) => res.addTrailers(responseHeaders(trailers, own)));
1740
+ responseBody(stream, own, t, readsModel).pipe(res);
1741
+ });
1742
+ body.pipe(stream);
1743
+ req.once("aborted", () => stream.close(http2.constants.NGHTTP2_CANCEL));
1744
+ }
1745
+ function forwardHttp1(req, res, upstream, own = [], tokenise, t, readsModel = false) {
1746
+ upstream.once("response", (up) => {
1747
+ if (tokenise) {
1748
+ const { exchange, url } = tokenise;
1749
+ return tokenised(exchange, url, up.statusCode ?? 502, up.headers, up, res, own, t);
1750
+ }
1751
+ const out = responseHeaders(up.headers, own);
1752
+ t?.set({ responseHeaders: plainHeaders(out) });
1753
+ res.writeHead(up.statusCode ?? 502, out);
1754
+ up.once("end", () => {
1755
+ if (Object.keys(up.trailers).length) res.addTrailers(responseHeaders(up.trailers, own));
1756
+ });
1757
+ responseBody(up, own, t, readsModel).pipe(res);
1758
+ });
1759
+ upstream.once("error", (error) => {
1760
+ t?.set({
1761
+ status: 502,
1762
+ reason: error.message
1763
+ });
1764
+ if (!res.headersSent) res.writeHead(502).end(`reeval egress gateway: ${error.message}\n`);
1765
+ else res.destroy();
1766
+ });
1767
+ req.pipe(upstream);
1768
+ }
1769
+ function onUpgrade(target, req, socket, head) {
1770
+ const method = req.method ?? "GET";
1771
+ const startedAt = Date.now();
1772
+ const { headers, path, own, names, credentials, refused } = transformed(target, method, req.url ?? "/", req.headers, req.httpVersion);
1773
+ const scrubbed = (value) => scrubs ? scrubValue(own, value) : value;
1774
+ const upgraded = (fields) => emit({
1775
+ type: "request",
1776
+ method,
1777
+ url: `https://${target.host}${pathOnly(req.url ?? "/")}`,
1778
+ host: target.host,
1779
+ port: target.port,
1780
+ via: "decrypted",
1781
+ allowed: true,
1782
+ sent: 0,
1783
+ received: 0,
1784
+ substituted: names,
1785
+ credentials,
1786
+ requestHeaders: plainHeaders(req.headers),
1787
+ ...fields,
1788
+ ms: Date.now() - startedAt
1789
+ });
1790
+ if (refused) {
1791
+ upgraded({
1792
+ allowed: false,
1793
+ status: refused.status,
1794
+ reason: refused.reason
1795
+ });
1796
+ return reply(socket, refused.status, refused.reason);
1797
+ }
1798
+ const upstream = https.request({
1799
+ host: target.address,
1800
+ servername: net.isIP(target.host) ? void 0 : target.host,
1801
+ port: target.port,
1802
+ method,
1803
+ path,
1804
+ headers: {
1805
+ ...headers,
1806
+ host: target.host
1807
+ },
1808
+ ca
1809
+ });
1810
+ upstream.once("upgrade", (up, upSocket, upHead) => {
1811
+ const out = scrubs ? scrubHeaders(own, up.headers) : up.headers;
1812
+ upgraded({
1813
+ status: up.statusCode,
1814
+ responseHeaders: plainHeaders(out)
1815
+ });
1816
+ const lines = [`HTTP/1.1 ${up.statusCode} ${up.statusMessage}`];
1817
+ for (let i = 0; i < up.rawHeaders.length; i += 2) lines.push(`${up.rawHeaders[i]}: ${scrubbed(up.rawHeaders[i + 1] ?? "")}`);
1818
+ socket.write(`${lines.join("\r\n")}\r\n\r\n`);
1819
+ if (upHead.length) socket.write(upHead);
1820
+ if (head.length) upSocket.write(head);
1821
+ link(socket, track(upSocket));
1822
+ });
1823
+ upstream.once("response", (up) => {
1824
+ reply(socket, up.statusCode ?? 502, `upstream refused the upgrade (${up.statusCode})`);
1825
+ up.resume();
1826
+ });
1827
+ upstream.once("error", (error) => reply(socket, 502, `reeval egress gateway: ${error.message}`));
1828
+ upstream.end();
1829
+ }
1830
+ /** One HTTP/2 session per upstream, or null when it doesn't speak h2. */
1831
+ function upstreamSession(target) {
1832
+ const key = `${target.host}:${target.port}`;
1833
+ const existing = sessions.get(key);
1834
+ if (existing && !existing.closed && !existing.destroyed) return Promise.resolve(existing);
1835
+ return new Promise((resolve) => {
1836
+ const socket = tls.connect({
1837
+ host: target.address,
1838
+ port: target.port,
1839
+ servername: net.isIP(target.host) ? void 0 : target.host,
1840
+ ALPNProtocols: ["h2", "http/1.1"],
1841
+ ...ca ? { ca } : {}
1842
+ });
1843
+ socket.once("error", () => resolve(null));
1844
+ socket.once("secureConnect", () => {
1845
+ if (socket.alpnProtocol !== "h2") {
1846
+ socket.destroy();
1847
+ return resolve(null);
1848
+ }
1849
+ const session = http2.connect(`https://${key}`, { createConnection: () => socket });
1850
+ session.on("error", () => {});
1851
+ session.once("close", () => sessions.get(key) === session && sessions.delete(key));
1852
+ session.once("goaway", () => sessions.get(key) === session && sessions.delete(key));
1853
+ sessions.set(key, session);
1854
+ resolve(session);
1855
+ });
1856
+ });
1857
+ }
1858
+ return {
1859
+ handle(connection) {
1860
+ track(connection);
1861
+ countBytes(connection);
1862
+ connection.on("error", () => connection.destroy());
1863
+ front.emit("connection", connection);
1864
+ },
1865
+ traffic: () => ({
1866
+ contacted: byHost(contacted.values()),
1867
+ blocked: byHost(blocked.values())
1868
+ }),
1869
+ addTransformations(more) {
1870
+ rules = [...rules, ...more];
1871
+ },
1872
+ setTransformations(all) {
1873
+ rules = [...all];
1874
+ },
1875
+ setPolicy(policy) {
1876
+ policyCheck = createPolicyCheck(policy, options);
1877
+ },
1878
+ setSubstitutions(all) {
1879
+ subs = [...all];
1880
+ },
1881
+ close() {
1882
+ for (const s of open) s.destroy();
1883
+ for (const s of sessions.values()) s.destroy();
1884
+ sessions.clear();
1885
+ agent.destroy();
1886
+ }
1887
+ };
1888
+ }
1889
+ //#endregion
1890
+ //#region ../sandbox-docker/src/egress/gateway.ts
1891
+ /** Where the forwarder listens inside the container; every exec gets it as its HTTP(S) proxy. */
1892
+ const PROXY_PORT = 43128;
1893
+ /** The gateway's CA certificate in the container. The private key never leaves the host. */
1894
+ const CA_PATH = "/usr/local/share/ca-certificates/reeval-egress.crt";
1895
+ /** Debian's system bundle, which `update-ca-certificates` rebuilds with the gateway's CA in it. */
1896
+ const CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
1897
+ const BUNDLED = "es:ca-bundle";
1898
+ const READY_TIMEOUT_MS = 15e3;
1899
+ /**
1900
+ * The gateway's side of the container runs as root, whoever the sandbox runs as: the sandbox user
1901
+ * can't signal, trace or replace the forwarder, or change the CA it installed.
1902
+ */
1903
+ const AS_ROOT = ["--user", "0:0"];
1904
+ const installCa = (containerId, cert, signal) => docker([
1905
+ "exec",
1906
+ "--interactive",
1907
+ ...AS_ROOT,
1908
+ containerId,
1909
+ "sh",
1910
+ "-c",
1911
+ `umask 022 && mkdir -p "$(dirname ${CA_PATH})" && cat > ${CA_PATH} && { command -v update-ca-certificates >/dev/null && update-ca-certificates >/dev/null 2>&1 && grep -q "$(sed -n 2p ${CA_PATH})" ${CA_BUNDLE} && echo ${BUNDLED}; true; }`
1912
+ ], {
1913
+ input: cert,
1914
+ signal
1915
+ });
1916
+ /**
1917
+ * Starts the egress gateway for one container: a forwarder inside it, tunnelled over one
1918
+ * `docker exec -i`, and the proxy in this process, which holds the real credentials.
1919
+ */
1920
+ async function startGateway(containerId, options, signal) {
1921
+ const ca = createCertificateAuthority();
1922
+ const installed = await installCa(containerId, ca.cert, signal);
1923
+ if (installed.exitCode !== 0) throw new Error(`couldn't install the gateway CA: ${installed.stderr.trim()}`);
1924
+ const bundled = installed.stdout.toString("utf8").includes(BUNDLED);
1925
+ const proxy = createEgressProxy({
1926
+ ...options,
1927
+ ca
1928
+ });
1929
+ const args = [
1930
+ "exec",
1931
+ "--interactive",
1932
+ ...AS_ROOT,
1933
+ containerId,
1934
+ "node",
1935
+ "-e",
1936
+ forwarderScript(PROXY_PORT)
1937
+ ];
1938
+ const startedAt = Date.now();
1939
+ const child = spawn("docker", args, {
1940
+ stdio: "pipe",
1941
+ detached: true
1942
+ });
1943
+ child.once("close", (code) => traceDocker(args, startedAt, {
1944
+ exitCode: code,
1945
+ stderr
1946
+ }));
1947
+ let stderr = "";
1948
+ child.stderr.on("data", (d) => {
1949
+ stderr += d;
1950
+ });
1951
+ child.stdin.on("error", () => {});
1952
+ let closed = false;
1953
+ const mux = createMux({
1954
+ Duplex,
1955
+ parity: 0,
1956
+ send: (frame) => {
1957
+ if (!closed) child.stdin.write(frame);
1958
+ },
1959
+ onOpen(stream, port) {
1960
+ if (port === 43128) proxy.handle(stream);
1961
+ else stream.destroy();
1962
+ },
1963
+ onReady: () => ready.resolve()
1964
+ });
1965
+ const ready = {};
1966
+ ready.promise = new Promise((resolve, reject) => Object.assign(ready, {
1967
+ resolve,
1968
+ reject
1969
+ }));
1970
+ child.stdout.on("data", (chunk) => mux.receive(chunk));
1971
+ child.once("error", (error) => ready.reject(error));
1972
+ const listeners = /* @__PURE__ */ new Map();
1973
+ const tunnels = /* @__PURE__ */ new Set();
1974
+ let stopped;
1975
+ child.once("close", (code) => {
1976
+ const asked = closed;
1977
+ closed = true;
1978
+ mux.destroy();
1979
+ const reason = `the forwarder exited (${code}): ${stderr.trim().slice(-500)}`.replace(/: $/, "");
1980
+ ready.reject(new Error(reason));
1981
+ if (asked) return;
1982
+ stopped = reason;
1983
+ for (const socket of tunnels) socket.destroy();
1984
+ for (const l of listeners.values()) l.then((s) => s.close());
1985
+ });
1986
+ const timer = setTimeout(() => ready.reject(/* @__PURE__ */ new Error(`the forwarder didn't start within ${READY_TIMEOUT_MS / 1e3}s`)), READY_TIMEOUT_MS);
1987
+ const onAbort = () => ready.reject(signal?.reason);
1988
+ signal?.addEventListener("abort", onAbort, { once: true });
1989
+ const close = () => {
1990
+ closed = true;
1991
+ child.kill("SIGKILL");
1992
+ mux.destroy();
1993
+ proxy.close();
1994
+ for (const l of listeners.values()) l.then((s) => s.close());
1995
+ };
1996
+ try {
1997
+ await ready.promise;
1998
+ } catch (error) {
1999
+ close();
2000
+ throw error;
2001
+ } finally {
2002
+ clearTimeout(timer);
2003
+ signal?.removeEventListener("abort", onAbort);
2004
+ }
2005
+ const proxyUrl = `http://127.0.0.1:${PROXY_PORT}`;
2006
+ const noProxy = "localhost,127.0.0.1,::1";
2007
+ return {
2008
+ env: {
2009
+ HTTPS_PROXY: proxyUrl,
2010
+ HTTP_PROXY: proxyUrl,
2011
+ https_proxy: proxyUrl,
2012
+ http_proxy: proxyUrl,
2013
+ NO_PROXY: noProxy,
2014
+ no_proxy: noProxy,
2015
+ NODE_EXTRA_CA_CERTS: CA_PATH,
2016
+ NODE_USE_ENV_PROXY: "1",
2017
+ ...bundled ? {
2018
+ SSL_CERT_FILE: CA_BUNDLE,
2019
+ REQUESTS_CA_BUNDLE: CA_BUNDLE,
2020
+ CURL_CA_BUNDLE: CA_BUNDLE,
2021
+ GIT_SSL_CAINFO: CA_BUNDLE
2022
+ } : {}
2023
+ },
2024
+ addTransformations: proxy.addTransformations,
2025
+ setTransformations: proxy.setTransformations,
2026
+ setPolicy: proxy.setPolicy,
2027
+ setSubstitutions: proxy.setSubstitutions,
2028
+ traffic: proxy.traffic,
2029
+ get stopped() {
2030
+ return stopped;
2031
+ },
2032
+ async portUrl(port, protocol) {
2033
+ if (stopped) throw new Error(`the egress gateway stopped: ${stopped}`);
2034
+ let listening = listeners.get(port);
2035
+ if (!listening) {
2036
+ listening = new Promise((resolve, reject) => {
2037
+ const server = net.createServer({ allowHalfOpen: true }, (socket) => {
2038
+ if (stopped) return socket.destroy();
2039
+ tunnels.add(socket);
2040
+ socket.once("close", () => tunnels.delete(socket));
2041
+ link(socket, mux.open(port));
2042
+ });
2043
+ server.once("error", reject);
2044
+ server.listen(0, "127.0.0.1", () => resolve(server));
2045
+ });
2046
+ listeners.set(port, listening);
2047
+ }
2048
+ return `${protocol}://127.0.0.1:${(await listening).address().port}`;
2049
+ },
2050
+ close
2051
+ };
2052
+ }
2053
+ //#endregion
2054
+ //#region ../sandbox-docker/src/session.ts
2055
+ const DEFAULT_LIMITS = {
2056
+ memory: "4g",
2057
+ cpus: 2,
2058
+ pids: 1024
2059
+ };
2060
+ /** What `root` adds back: what `apt-get` (and its `_apt` sandbox) and `dpkg` need as root. */
2061
+ const ROOT_CAPABILITIES = [
2062
+ "CHOWN",
2063
+ "DAC_OVERRIDE",
2064
+ "FOWNER",
2065
+ "SETUID",
2066
+ "SETGID"
2067
+ ];
2068
+ let hostCpus;
2069
+ /** Docker refuses `--cpus` above the host's count, so cap it there. */
2070
+ async function cpuLimit(cpus) {
2071
+ hostCpus ??= dockerCapacity().then((c) => c?.cpus, () => void 0);
2072
+ const available = await hostCpus;
2073
+ return String(available ? Math.min(cpus, available) : cpus);
2074
+ }
2075
+ /** The `docker run` flags that confine the container: its user, capabilities and limits. */
2076
+ async function confinementArgs(options) {
2077
+ const limits = {
2078
+ ...DEFAULT_LIMITS,
2079
+ ...options.limits
2080
+ };
2081
+ const caps = [...options.root ? ROOT_CAPABILITIES : [], ...options.capabilities ?? []];
2082
+ return [
2083
+ "--user",
2084
+ options.root ? "0:0" : SANDBOX_USER,
2085
+ "--cap-drop",
2086
+ "ALL",
2087
+ ...[...new Set(caps)].flatMap((c) => ["--cap-add", c]),
2088
+ "--security-opt",
2089
+ "no-new-privileges",
2090
+ "--memory",
2091
+ String(limits.memory),
2092
+ "--memory-swap",
2093
+ String(limits.memory),
2094
+ "--pids-limit",
2095
+ String(limits.pids),
2096
+ "--cpus",
2097
+ await cpuLimit(limits.cpus)
2098
+ ];
2099
+ }
2100
+ /** Runtime `npm i -g` goes to the sandbox user's own prefix, whose `bin` the images put on PATH. */
2101
+ const userEnv = (options) => options.root ? {} : { NPM_CONFIG_PREFIX: `${SANDBOX_HOME}/.local` };
2102
+ /** Set on every exec, so a kill can find the command's whole process tree in the container. */
2103
+ const EXEC_MARKER = "REEVAL_EXEC_ID";
2104
+ /** The Docker networks a container is attached to. */
2105
+ async function networksOf(id) {
2106
+ const out = await dockerOk([
2107
+ "inspect",
2108
+ "--format",
2109
+ "{{json .NetworkSettings.Networks}}",
2110
+ id
2111
+ ]);
2112
+ return Object.keys(JSON.parse(out) ?? {});
2113
+ }
2114
+ /**
2115
+ * A harness sandbox session backed by one local Docker container. Commands and file I/O go
2116
+ * through `docker exec`. With the egress gateway, ports are reached through its tunnel whatever
2117
+ * the network policy; without it, the bridge port is published on 127.0.0.1.
2118
+ */
2119
+ async function createDockerSession(options) {
2120
+ const nonce = randomBytes(8).toString("hex");
2121
+ const labels = {
2122
+ [LABEL.managed]: "true",
2123
+ [LABEL.session]: nonce,
2124
+ ...options.labels
2125
+ };
2126
+ const lifetime = Math.ceil((options.lifetimeMs ?? 864e5) / 1e3);
2127
+ const signal = options.abortSignal;
2128
+ const env = {
2129
+ ...userEnv(options),
2130
+ ...options.env
2131
+ };
2132
+ const id = await dockerOk([
2133
+ "run",
2134
+ "--detach",
2135
+ "--rm",
2136
+ "--init",
2137
+ ...await confinementArgs(options),
2138
+ ...Object.entries(labels).flatMap(([k, v]) => ["--label", `${k}=${v}`]),
2139
+ ...Object.keys(env).flatMap((k) => ["--env", k]),
2140
+ "--publish",
2141
+ `127.0.0.1::${BRIDGE_PORT}`,
2142
+ "--workdir",
2143
+ SANDBOX_ROOT,
2144
+ "--entrypoint",
2145
+ "sleep",
2146
+ options.image,
2147
+ String(lifetime)
2148
+ ], {
2149
+ signal,
2150
+ env
2151
+ }).catch((error) => {
2152
+ if (signal?.aborted) sweepLabelDetached(`${LABEL.session}=${nonce}`);
2153
+ if (/unable to find user/i.test(error.message)) {
2154
+ sweepLabelDetached(`${LABEL.session}=${nonce}`);
2155
+ throw new Error(`${options.image} has no ${SANDBOX_USER} user to run the sandbox as. Images reeval builds have one: rebuild it (reeval image build), or start from reeval's image or docker({ from }). An eval that needs root at run time can opt in with docker({ root: true }).`);
2156
+ }
2157
+ throw error;
2158
+ });
2159
+ let gateway;
2160
+ let removed;
2161
+ const remove = () => {
2162
+ gateway?.close();
2163
+ removed ??= removeContainerDetached(id);
2164
+ return removed;
2165
+ };
2166
+ if (signal?.aborted) {
2167
+ await remove();
2168
+ throw signal.reason;
2169
+ }
2170
+ const egressOptions = options.egress === true ? {} : options.egress || void 0;
2171
+ let egress;
2172
+ let detached = [];
2173
+ const attach = async (policy) => {
2174
+ if (isRestricted(policy) && detached.length === 0) {
2175
+ detached = await networksOf(id);
2176
+ for (const n of detached) await dockerOk([
2177
+ "network",
2178
+ "disconnect",
2179
+ "--force",
2180
+ n,
2181
+ id
2182
+ ]);
2183
+ } else if (!isRestricted(policy) && detached.length > 0) {
2184
+ for (const n of detached) await dockerOk([
2185
+ "network",
2186
+ "connect",
2187
+ n,
2188
+ id
2189
+ ]);
2190
+ detached = [];
2191
+ }
2192
+ };
2193
+ if (egressOptions) {
2194
+ const { policy = { mode: "allow-all" }, ...rest } = egressOptions;
2195
+ try {
2196
+ await attach(policy);
2197
+ gateway = await startGateway(id, {
2198
+ ...rest,
2199
+ policy
2200
+ }, signal);
2201
+ egress = { active: true };
2202
+ } catch (error) {
2203
+ if (signal?.aborted || isRestricted(policy)) {
2204
+ await remove();
2205
+ if (signal?.aborted) throw signal.reason;
2206
+ throw new Error(`Can't enforce the egress policy: ${error.message}`);
2207
+ }
2208
+ egress = {
2209
+ active: false,
2210
+ reason: error.message
2211
+ };
2212
+ }
2213
+ }
2214
+ const envFor = (env) => gateway ? {
2215
+ ...gateway.env,
2216
+ ...env
2217
+ } : env;
2218
+ let execs = 0;
2219
+ const execArgs = (command, marker, cwd, env) => [
2220
+ "exec",
2221
+ "--interactive",
2222
+ "--workdir",
2223
+ cwd ? posix.resolve(SANDBOX_ROOT, cwd) : SANDBOX_ROOT,
2224
+ "--env",
2225
+ `${EXEC_MARKER}=${marker}`,
2226
+ ...Object.keys(env ?? {}).flatMap((k) => ["--env", k]),
2227
+ id,
2228
+ "bash",
2229
+ "-c",
2230
+ command
2231
+ ];
2232
+ const nextMarker = () => `${id.slice(0, 12)}-${++execs}`;
2233
+ /**
2234
+ * Killing the host's `docker exec` client doesn't stop the command in the container, so kill
2235
+ * every process there that inherited this exec's marker: the command and all it started.
2236
+ */
2237
+ const killExec = (marker) => docker([
2238
+ "exec",
2239
+ id,
2240
+ "sh",
2241
+ "-c",
2242
+ `for e in /proc/[0-9]*/environ; do grep -qsxzF ${shellQuote(`${EXEC_MARKER}=${marker}`)} "$e" && kill -9 "$(basename "$(dirname "$e")")" 2>/dev/null; done; true`
2243
+ ]).then(() => {}, () => {});
2244
+ const run = async (o) => {
2245
+ const env = envFor(o.env);
2246
+ const marker = nextMarker();
2247
+ try {
2248
+ const r = await docker(execArgs(o.command, marker, o.workingDirectory, env), {
2249
+ env,
2250
+ signal: o.abortSignal
2251
+ });
2252
+ return {
2253
+ exitCode: r.exitCode,
2254
+ stdout: r.stdout.toString("utf8"),
2255
+ stderr: r.stderr
2256
+ };
2257
+ } catch (error) {
2258
+ if (o.abortSignal?.aborted) await killExec(marker);
2259
+ throw error;
2260
+ }
2261
+ };
2262
+ const readBinaryFile = async (o) => {
2263
+ const r = await docker([
2264
+ "exec",
2265
+ id,
2266
+ "cat",
2267
+ "--",
2268
+ posix.resolve(SANDBOX_ROOT, o.path)
2269
+ ], { signal: o.abortSignal });
2270
+ return r.exitCode === 0 ? new Uint8Array(r.stdout) : null;
2271
+ };
2272
+ const writeBinaryFile = async (o) => {
2273
+ const path = posix.resolve(SANDBOX_ROOT, o.path);
2274
+ await dockerOk([
2275
+ "exec",
2276
+ "--interactive",
2277
+ id,
2278
+ "sh",
2279
+ "-c",
2280
+ `mkdir -p ${shellQuote(posix.dirname(path))} && cat > ${shellQuote(path)}`
2281
+ ], {
2282
+ input: Buffer.from(o.content),
2283
+ signal: o.abortSignal
2284
+ });
2285
+ };
2286
+ const brokering = gateway ? {
2287
+ async setNetworkPolicy(policy) {
2288
+ gateway?.setPolicy(policy);
2289
+ await attach(policy);
2290
+ },
2291
+ async addRequestTransformations(rules) {
2292
+ gateway?.addTransformations(rules);
2293
+ },
2294
+ async setRequestTransformations(rules) {
2295
+ gateway?.setTransformations(rules);
2296
+ }
2297
+ } : {};
2298
+ const substituting = gateway ? {
2299
+ async setSubstitutions(subs) {
2300
+ gateway?.setSubstitutions(subs);
2301
+ },
2302
+ traffic: () => gateway.traffic()
2303
+ } : {};
2304
+ const session = {
2305
+ id,
2306
+ description: `Docker container ${id.slice(0, 12)} (${options.image})`,
2307
+ defaultWorkingDirectory: SANDBOX_ROOT,
2308
+ ports: [BRIDGE_PORT],
2309
+ get egress() {
2310
+ if (egress?.active && gateway?.stopped) return {
2311
+ active: false,
2312
+ reason: gateway.stopped
2313
+ };
2314
+ return egress;
2315
+ },
2316
+ ...brokering,
2317
+ ...substituting,
2318
+ run,
2319
+ async spawn(o) {
2320
+ const env = envFor(o.env);
2321
+ const marker = nextMarker();
2322
+ const args = execArgs(o.command, marker, o.workingDirectory, env);
2323
+ const startedAt = Date.now();
2324
+ const child = spawn("docker", args, {
2325
+ stdio: [
2326
+ "ignore",
2327
+ "pipe",
2328
+ "pipe"
2329
+ ],
2330
+ detached: true,
2331
+ env: env ? {
2332
+ ...process.env,
2333
+ ...env
2334
+ } : process.env
2335
+ });
2336
+ const exited = new Promise((resolve) => child.on("close", (code) => {
2337
+ traceDocker(args, startedAt, {
2338
+ exitCode: code,
2339
+ stdout: "<streamed>"
2340
+ });
2341
+ resolve({ exitCode: code ?? 1 });
2342
+ }));
2343
+ const kill = async () => {
2344
+ child.kill("SIGKILL");
2345
+ await killExec(marker);
2346
+ };
2347
+ o.abortSignal?.addEventListener("abort", () => void kill(), { once: true });
2348
+ return {
2349
+ pid: child.pid,
2350
+ stdout: Readable.toWeb(child.stdout),
2351
+ stderr: Readable.toWeb(child.stderr),
2352
+ wait: () => exited,
2353
+ kill
2354
+ };
2355
+ },
2356
+ readBinaryFile,
2357
+ async readFile(o) {
2358
+ const bytes = await readBinaryFile(o);
2359
+ return bytes === null ? null : Readable.toWeb(Readable.from([bytes]));
2360
+ },
2361
+ async readTextFile(o) {
2362
+ const bytes = await readBinaryFile(o);
2363
+ if (bytes === null) return null;
2364
+ const text = Buffer.from(bytes).toString("utf8");
2365
+ if (o.startLine === void 0 && o.endLine === void 0) return text;
2366
+ return text.split("\n").slice((o.startLine ?? 1) - 1, o.endLine).join("\n");
2367
+ },
2368
+ writeBinaryFile,
2369
+ writeTextFile: (o) => writeBinaryFile({
2370
+ ...o,
2371
+ content: Buffer.from(o.content, "utf8")
2372
+ }),
2373
+ async writeFile(o) {
2374
+ const chunks = [];
2375
+ for await (const chunk of o.content) chunks.push(chunk);
2376
+ await writeBinaryFile({
2377
+ ...o,
2378
+ content: Buffer.concat(chunks)
2379
+ });
2380
+ },
2381
+ async getPortEndpoint({ port, protocol = "ws" }) {
2382
+ const scheme = protocol === "ws" ? "ws" : "http";
2383
+ if (gateway?.stopped) throw new Error(`Port ${port} of ${id.slice(0, 12)} isn't reachable: the egress gateway stopped: ${gateway.stopped}`);
2384
+ if (gateway) return { url: await gateway.portUrl(port, scheme) };
2385
+ const mapped = await docker([
2386
+ "port",
2387
+ id,
2388
+ `${port}/tcp`
2389
+ ]);
2390
+ const hostPort = mapped.stdout.toString("utf8").split("\n")[0]?.split(":").at(-1);
2391
+ if (mapped.exitCode !== 0 || !hostPort) {
2392
+ const reason = mapped.stderr.trim() || "Docker reports no host port for it";
2393
+ throw new Error(`Port ${port} of ${id.slice(0, 12)} isn't reachable: ${reason}`);
2394
+ }
2395
+ return { url: `${scheme}://127.0.0.1:${hostPort}` };
2396
+ },
2397
+ async getPortUrl(o) {
2398
+ return (await session.getPortEndpoint(o)).url;
2399
+ },
2400
+ stop: remove,
2401
+ destroy: remove,
2402
+ restricted: () => {
2403
+ const { description, readFile, readBinaryFile, readTextFile, writeFile } = session;
2404
+ const { writeBinaryFile, writeTextFile, spawn, run } = session;
2405
+ return {
2406
+ description,
2407
+ readFile,
2408
+ readBinaryFile,
2409
+ readTextFile,
2410
+ writeFile,
2411
+ writeBinaryFile,
2412
+ writeTextFile,
2413
+ spawn,
2414
+ run
2415
+ };
2416
+ }
2417
+ };
2418
+ return session;
2419
+ }
2420
+ /** The session as an AI SDK harness sandbox provider. */
2421
+ const dockerSandboxProvider = (options) => ({
2422
+ specificationVersion: "harness-sandbox-v1",
2423
+ providerId: "reeval-docker",
2424
+ async createSession(o) {
2425
+ const session = await createDockerSession({
2426
+ ...options,
2427
+ abortSignal: o?.abortSignal
2428
+ });
2429
+ await o?.onFirstCreate?.(session, { abortSignal: o.abortSignal });
2430
+ return session;
2431
+ }
2432
+ });
2433
+ //#endregion
2434
+ //#region ../sandbox-docker/src/build-image.ts
2435
+ const PNPM_VERSION = "10.33.3";
2436
+ /** npm before 11.6 (node:22 ships 10.9) can crash on circular peer sets (npm/cli#8261); the same as docker/Dockerfile's. */
2437
+ const NPM_VERSION = "11.20.0";
2438
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
2439
+ const slugify = (text, max) => text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max).replace(/-+$/, "");
2440
+ /** The build context of reeval's base image, shipped in the package. */
2441
+ const DOCKER_CONTEXT = fileURLToPath(ownFileURL("docker"));
2442
+ /**
2443
+ * The image `docker({ from })` evals run in: `from` plus Node, git and the harness's CLI. The hash
2444
+ * covers the toolchain layer too, so a change to the sandbox user or paths builds anew.
2445
+ */
2446
+ const harnessImageFor = (harness, from) => `${imageFor(harness)}-${slugify(from, 40) || "image"}-${sha256(toolchainDockerfile(from)).slice(0, 8)}`;
2447
+ /**
2448
+ * How each harness's CLI gets into an image: its adapter's bootstrap, and the binary it leaves
2449
+ * (or, for an adapter without one, a command that prints what it installed).
2450
+ */
2451
+ const BOOTSTRAPS = {
2452
+ cursor: {
2453
+ name: "Cursor",
2454
+ cli: "@cursor/sdk",
2455
+ version: `node -p "'@cursor/sdk ' + require(process.env.HOME + '/.ai-sdk-harness/.harness-bootstrap/cursor-sdk/node_modules/@cursor/sdk/package.json').version"`,
2456
+ adapter: async () => (await import("./src-daS0wKH2.js")).createCursorSdk()
2457
+ },
2458
+ "claude-code": {
2459
+ name: "Claude Code",
2460
+ cli: "claude",
2461
+ adapter: async () => (await import("./dist-BZXYHN2X.js")).createClaudeCode()
2462
+ },
2463
+ antigravity: {
2464
+ name: "Antigravity",
2465
+ cli: "agy",
2466
+ adapter: async () => (await import("./src-W9g-y5X5.js")).createAntigravity()
2467
+ },
2468
+ opencode: {
2469
+ name: "OpenCode",
2470
+ cli: "opencode",
2471
+ adapter: async () => (await import("./src-DEgff9Z7.js")).createOpenCode({
2472
+ auth: { GOOGLE_GENERATIVE_AI_API_KEY: "unused" },
2473
+ model: "google/unused"
2474
+ })
2475
+ },
2476
+ goose: {
2477
+ name: "Goose",
2478
+ cli: "goose",
2479
+ adapter: async () => (await import("./src-C-LHyTSV.js")).createGoose({
2480
+ auth: { GOOGLE_API_KEY: "unused" },
2481
+ model: "unused"
2482
+ })
2483
+ },
2484
+ pi: {
2485
+ name: "pi",
2486
+ cli: "pi",
2487
+ adapter: async () => (await import("./src-BDwSuyIo.js")).createPi({ auth: { GEMINI_API_KEY: "unused" } })
2488
+ },
2489
+ codex: {
2490
+ name: "Codex",
2491
+ cli: "codex",
2492
+ adapter: async () => (await import("./src-55j1zNHH.js")).createCodex({ auth: { OPENAI_API_KEY: "unused" } })
2493
+ }
2494
+ };
2495
+ /**
2496
+ * Adds what the harness bridge needs to a Debian or Ubuntu based toolchain image: Node (copied
2497
+ * from the official image, so the base's own toolchain is untouched), pnpm, git, curl and bash.
2498
+ */
2499
+ const toolchainDockerfile = (from) => `FROM ${from}
2500
+ SHELL ["/bin/sh", "-c"]
2501
+ RUN command -v apt-get >/dev/null || { echo "reeval needs a Debian or Ubuntu based image (glibc and apt-get)" >&2; exit 1; } \\
2502
+ && apt-get update \\
2503
+ && apt-get install -y --no-install-recommends bash ca-certificates curl git procps \\
2504
+ && rm -rf /var/lib/apt/lists/*
2505
+ COPY --from=node:22-bookworm-slim /usr/local/bin/node /usr/local/bin/node
2506
+ COPY --from=node:22-bookworm-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
2507
+ RUN ln -sf ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \\
2508
+ && ln -sf ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \\
2509
+ && npm install --global npm@${NPM_VERSION} \\
2510
+ && npm install --global pnpm@${PNPM_VERSION} \\
2511
+ && { id -u ${SANDBOX_USER} >/dev/null 2>&1 \\
2512
+ || useradd --create-home --shell /bin/bash --uid 10001 --user-group ${SANDBOX_USER} 2>/dev/null \\
2513
+ || useradd --create-home --shell /bin/bash --user-group ${SANDBOX_USER}; } \\
2514
+ && mkdir -p ${SANDBOX_ROOT}/${WORKSPACE_DIR} && chown -R ${SANDBOX_USER}: ${SANDBOX_ROOT}
2515
+ ENV PATH=${SANDBOX_HOME}/.local/bin:$PATH
2516
+ WORKDIR ${SANDBOX_ROOT}/${WORKSPACE_DIR}
2517
+ `;
2518
+ const tail = (text) => text.trim().split("\n").slice(-15).join("\n");
2519
+ const clipLine = (text, max = 300) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
2520
+ /** Why a `docker build` failed, in a line: the failing step's exit, or BuildKit's own error. */
2521
+ function buildReason(error) {
2522
+ if (!error) return "docker build failed";
2523
+ const exit = /^process ".*" did not complete successfully: exit code: (\d+)$/s.exec(error);
2524
+ return exit ? `a RUN step exited ${exit[1]}` : clipLine(error.replace(/\s+/g, " "));
2525
+ }
2526
+ /**
2527
+ * A failed `docker build --progress plain` as an {@link ImageBuildError}: `headline` then the
2528
+ * reason, the named cause, and the failing step's own last lines.
2529
+ */
2530
+ function buildError(output, what, reason, keep = () => true) {
2531
+ const parsed = parseBuildOutput(output);
2532
+ const text = [parsed.error ?? "", ...parsed.output].join("\n");
2533
+ const cause = failureCause(text, parsed.exitCode);
2534
+ parsed.output = parsed.output.filter(keep);
2535
+ const why = reason?.(parsed) ?? buildReason(parsed.error);
2536
+ return new ImageBuildError(failureMessage({
2537
+ headline: `${what.headline} failed: ${why}`,
2538
+ cause,
2539
+ output: parsed.output,
2540
+ log: what.log ?? null
2541
+ }), {
2542
+ stage: what.stage,
2543
+ ref: what.ref,
2544
+ transient: isTransient(text),
2545
+ cause,
2546
+ log: what.log ?? null
2547
+ });
2548
+ }
2549
+ /** Every image reeval builds carries this label, so `reeval clean --images` can find what it left. */
2550
+ const BUILT_BY = ["--label", `${LABEL.managed}=image`];
2551
+ /**
2552
+ * Removes the images reeval built that nothing tags any more (a rebuild's old image) and the
2553
+ * intermediate `-base` tags of `from` builds. Returns how many it removed.
2554
+ */
2555
+ async function pruneImages() {
2556
+ const list = async (args) => (await docker([
2557
+ "image",
2558
+ "ls",
2559
+ ...args
2560
+ ])).stdout.toString("utf8").split("\n").map((l) => l.trim()).filter(Boolean);
2561
+ const dangling = [...await list([
2562
+ "-q",
2563
+ "--filter",
2564
+ "dangling=true",
2565
+ "--filter",
2566
+ `label=${LABEL.managed}=image`
2567
+ ]), ...await list([
2568
+ "-q",
2569
+ "--filter",
2570
+ "dangling=true",
2571
+ "--filter",
2572
+ `label=${LABEL.harness}`
2573
+ ])];
2574
+ const bases = (await list([
2575
+ "--format",
2576
+ "{{.Repository}}:{{.Tag}}",
2577
+ "reeval-sandbox"
2578
+ ])).filter((ref) => ref.endsWith("-base"));
2579
+ let removed = 0;
2580
+ for (const ref of [...bases, ...new Set(dangling)]) if ((await docker([
2581
+ "image",
2582
+ "rm",
2583
+ ref
2584
+ ]).catch(() => null))?.exitCode === 0) removed++;
2585
+ return removed;
2586
+ }
2587
+ /**
2588
+ * What a `from` names now, without pulling: its registry digest, or for an image that's only
2589
+ * local, its id (`local`). `error` when neither resolves, e.g. a typo or no network.
2590
+ */
2591
+ async function resolveFrom(from, signal) {
2592
+ const pinned = /@(sha256:[0-9a-f]{64})$/.exec(from)?.[1];
2593
+ if (pinned) return {
2594
+ digest: pinned,
2595
+ local: false
2596
+ };
2597
+ const remote = await docker([
2598
+ "buildx",
2599
+ "imagetools",
2600
+ "inspect",
2601
+ "--format",
2602
+ "{{json .Manifest.Digest}}",
2603
+ from
2604
+ ], { signal }).catch(() => null);
2605
+ const digest = remote?.exitCode === 0 ? JSON.parse(remote.stdout.toString("utf8").trim()) : null;
2606
+ if (typeof digest === "string" && digest.startsWith("sha256:")) return {
2607
+ digest,
2608
+ local: false
2609
+ };
2610
+ const local = await imageInfo(from);
2611
+ if (local) return {
2612
+ digest: local.id,
2613
+ local: true
2614
+ };
2615
+ return { error: remote?.stderr.trim().replace(/^ERROR: /, "").replace(/\s+/g, " ") || `${from} doesn't resolve` };
2616
+ }
2617
+ /** Removes a tag or an image reeval replaced, unless something still uses it. */
2618
+ const removeQuietly = async (ref) => {
2619
+ await docker([
2620
+ "image",
2621
+ "rm",
2622
+ ref
2623
+ ]).catch(() => {});
2624
+ };
2625
+ /** The hash of reeval's base image build context, which its images carry as a label. */
2626
+ function contextHash() {
2627
+ const hash = createHash("sha256");
2628
+ for (const path of contextFiles(DOCKER_CONTEXT, join(DOCKER_CONTEXT, "Dockerfile"))) {
2629
+ hash.update(`${path}\0`);
2630
+ hash.update(readFileSync(join(DOCKER_CONTEXT, path)));
2631
+ hash.update("\0");
2632
+ }
2633
+ return hash.digest("hex").slice(0, 16);
2634
+ }
2635
+ /**
2636
+ * Bakes a harness's bootstrap (which installs its agent's CLI) into an image, so the image tag
2637
+ * pins the CLI build. Rebuild after bumping the adapter. With `from`, the base is that toolchain
2638
+ * image instead of reeval's Node image. `log` gets progress lines, `output` the build's full output
2639
+ * (for a log file, whose path failures name). The previous image under the tag, and the
2640
+ * intermediate `-base` tag of a `from` build, are removed once the new one is committed.
2641
+ */
2642
+ async function buildImage(log, options) {
2643
+ const { harness, from, signal } = options;
2644
+ const resolved = from ? await resolveFrom(from, signal) : null;
2645
+ const digest = resolved && "digest" in resolved ? resolved.digest : null;
2646
+ const pinned = from && digest && resolved && "local" in resolved && !resolved.local && !from.includes("@") ? `${from}@${digest}` : from;
2647
+ const bootstrap = BOOTSTRAPS[harness];
2648
+ const base = from ? `${harnessImageFor(harness, from)}-base` : BASE_IMAGE;
2649
+ const target = from ? harnessImageFor(harness, from) : imageFor(harness);
2650
+ const previous = [(await imageInfo(target))?.id, from ? null : (await imageInfo(base))?.id];
2651
+ log(`Building ${base}${from ? ` from ${from}` : ""}`);
2652
+ const build = from ? await docker([
2653
+ "build",
2654
+ "--progress",
2655
+ "plain",
2656
+ ...BUILT_BY,
2657
+ ...options.pull ? ["--pull"] : [],
2658
+ "--tag",
2659
+ base,
2660
+ "-"
2661
+ ], {
2662
+ input: toolchainDockerfile(pinned ?? from),
2663
+ signal
2664
+ }) : await docker([
2665
+ "build",
2666
+ "--progress",
2667
+ "plain",
2668
+ ...BUILT_BY,
2669
+ "--tag",
2670
+ base,
2671
+ DOCKER_CONTEXT
2672
+ ], { signal });
2673
+ const output = `${build.stdout.toString("utf8")}${build.stderr}`;
2674
+ options.output?.write(output);
2675
+ if (build.exitCode !== 0) throw buildError(output, {
2676
+ stage: from ? "from" : "harness",
2677
+ ref: target,
2678
+ headline: from ? `Building the toolchain layer FROM ${from}` : "Building reeval's base image",
2679
+ log: options.output?.path ?? null
2680
+ }, (parsed) => parsed.output.find((l) => l.startsWith("reeval needs a Debian")) ?? null);
2681
+ log(`Installing the ${bootstrap.name} harness bootstrap`);
2682
+ const session = await createDockerSession({
2683
+ image: base,
2684
+ lifetimeMs: 18e5,
2685
+ ...signal ? { abortSignal: signal } : {}
2686
+ });
2687
+ let agentVersion;
2688
+ try {
2689
+ agentVersion = await bake(session, bootstrap);
2690
+ log(`${bootstrap.name} ${bootstrap.version ? "" : "CLI "}${agentVersion}`);
2691
+ await dockerOk([
2692
+ "commit",
2693
+ "--change",
2694
+ `LABEL ${LABEL.agentVersion}="${agentVersion}" ${LABEL.harness}="${harnessLabel(harness)}"${from ? digest ? ` ${LABEL.fromDigest}="${digest}"` : "" : ` ${LABEL.context}="${contextHash()}"`}`,
2695
+ "--change",
2696
+ `WORKDIR ${SANDBOX_ROOT}`,
2697
+ "--change",
2698
+ "USER root",
2699
+ "--change",
2700
+ "ENTRYPOINT []",
2701
+ "--change",
2702
+ "CMD [\"bash\"]",
2703
+ session.id,
2704
+ target
2705
+ ]);
2706
+ } catch (error) {
2707
+ if (signal?.aborted || error instanceof ImageBuildError) throw error;
2708
+ const [first = "", ...rest] = error.message.split("\n");
2709
+ const cause = failureCause(error.message, null);
2710
+ options.output?.write(`${output}\n${error.message}\n`);
2711
+ throw new ImageBuildError(failureMessage({
2712
+ headline: `Baking the ${bootstrap.name} CLI onto ${from ?? "reeval-sandbox-base:latest"} failed: ${clipLine(first)}`,
2713
+ cause,
2714
+ output: outputTail(rest.join("\n")),
2715
+ log: options.output?.path ?? null
2716
+ }), {
2717
+ stage: "harness",
2718
+ ref: target,
2719
+ transient: isTransient(error.message),
2720
+ cause,
2721
+ log: options.output?.path ?? null
2722
+ });
2723
+ } finally {
2724
+ await session.destroy();
2725
+ }
2726
+ if (from) await removeQuietly(base);
2727
+ const now = (await imageInfo(target))?.id;
2728
+ for (const id of previous) if (id && id !== now) await removeQuietly(id);
2729
+ return {
2730
+ image: target,
2731
+ agentVersion
2732
+ };
2733
+ }
2734
+ /** Runs the harness bootstrap in `session` and returns the CLI version it installed. */
2735
+ async function bake(session, bootstrap) {
2736
+ await prepareSandboxForHarness({
2737
+ session,
2738
+ harnesses: [await bootstrap.adapter()],
2739
+ sandboxConfig: { workDir: WORKSPACE_DIR }
2740
+ });
2741
+ let probe = bootstrap.version;
2742
+ if (!probe) {
2743
+ const bin = (await session.run({ command: `find "$HOME/.ai-sdk-harness" -name ${bootstrap.cli} \\( -type f -o -type l \\) -perm -u+x | head -1` })).stdout.trim();
2744
+ if (!bin) throw new Error(`The harness bootstrap did not install the ${bootstrap.name} CLI`);
2745
+ probe = `'${bin}' --version`;
2746
+ }
2747
+ const version = await session.run({ command: probe });
2748
+ if (bootstrap.version && version.exitCode !== 0) throw new Error(`The harness bootstrap did not install ${bootstrap.cli}:\n${tail(version.stderr)}`);
2749
+ return version.stdout.trim() || "unknown";
2750
+ }
2751
+ /**
2752
+ * The image `docker({ install })` runs in: the commands on top of `parent`, tagged by the parent's
2753
+ * id and the exact list, so it's built once. Secrets and the workspace never exist here.
2754
+ */
2755
+ const installLayerTag = (parent, steps) => `reeval-install:${sha256(JSON.stringify({
2756
+ parent: parent.id,
2757
+ steps
2758
+ })).slice(0, 16)}`;
2759
+ /** The commands as one bash script: in order, from `/`, stopping at the first that fails. */
2760
+ function installScript(steps) {
2761
+ const lines = ["cd /"];
2762
+ for (const [i, step] of steps.entries()) {
2763
+ const n = `${i + 1}/${steps.length}`;
2764
+ const run = step.timeoutMs ? `timeout --kill-after=10 ${Math.ceil(step.timeoutMs / 1e3)} bash -c ${shellQuote(step.run)}` : `bash -c ${shellQuote(step.run)}`;
2765
+ const label = shellQuote(step.run.split("\n")[0] ?? "");
2766
+ lines.push(`echo "reeval: install ${n}: "${label}`, `${run} || { code=$?; echo "reeval: install ${n} exited $code: "${label} >&2; exit $code; }`);
2767
+ }
2768
+ return `${lines.join("\n")}\n`;
2769
+ }
2770
+ /** Builds the install layer if it's missing. `output` gets the build's output, for a log file. */
2771
+ async function buildInstallLayer(parent, steps, options = {}) {
2772
+ const { signal } = options;
2773
+ const tag = installLayerTag(parent, steps);
2774
+ if ((await docker([
2775
+ "image",
2776
+ "inspect",
2777
+ "--format",
2778
+ "{{.Id}}",
2779
+ tag
2780
+ ], { signal })).exitCode === 0) return tag;
2781
+ const script = installScript(steps);
2782
+ const encoded = Buffer.from(script).toString("base64");
2783
+ const home = `$(getent passwd ${SANDBOX_USER} | cut -d: -f6)`;
2784
+ const dockerfile = [
2785
+ `FROM ${parent.ref}`,
2786
+ "USER root",
2787
+ `RUN echo ${encoded} | base64 -d > /tmp/reeval-install.sh \\`,
2788
+ ` && if id -u ${SANDBOX_USER} >/dev/null 2>&1; then export HOME="${home}"; fi \\`,
2789
+ " && bash /tmp/reeval-install.sh && rm /tmp/reeval-install.sh \\",
2790
+ ` && { ! id -u ${SANDBOX_USER} >/dev/null 2>&1 || find "$HOME" -xdev ! -user ${SANDBOX_USER} -exec chown -h ${SANDBOX_USER}: {} +; }`,
2791
+ `WORKDIR ${SANDBOX_ROOT}`
2792
+ ].join("\n");
2793
+ const build = await docker([
2794
+ "build",
2795
+ "--progress",
2796
+ "plain",
2797
+ ...BUILT_BY,
2798
+ "--tag",
2799
+ tag,
2800
+ "-"
2801
+ ], {
2802
+ input: dockerfile,
2803
+ signal
2804
+ });
2805
+ const output = `${build.stdout.toString("utf8")}${build.stderr}`;
2806
+ options.output?.write(`${script}\n${dockerfile}\n\n${output}`);
2807
+ if (build.exitCode !== 0) {
2808
+ const failed = /reeval: install (\d+\/\d+) exited (\d+): (.*)/.exec(output);
2809
+ throw buildError(output, {
2810
+ stage: "install",
2811
+ ref: tag,
2812
+ headline: `Building the install layer (${`${steps.length} command${steps.length === 1 ? "" : "s"}`})`,
2813
+ log: options.output?.path ?? null
2814
+ }, () => failed ? `install ${failed[1]} exited ${failed[2]}: ${clipLine(failed[3] ?? "")}` : null, (line) => !line.startsWith("reeval: install "));
2815
+ }
2816
+ return tag;
2817
+ }
2818
+ /** The build arg a Dockerfile can start `FROM` to layer on reeval's sandbox image. */
2819
+ const REEVAL_BASE_ARG = "REEVAL_BASE";
2820
+ /**
2821
+ * `.dockerignore` rules as Docker reads them: paths from the context root, `*`, `?` and `**`
2822
+ * globs, a pattern that matches a directory excludes what's in it, `!` re-includes, and the last
2823
+ * matching rule wins.
2824
+ */
2825
+ function dockerignoreMatcher(text) {
2826
+ const rules = text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
2827
+ const negate = line.startsWith("!");
2828
+ const body = (negate ? line.slice(1) : line).trim().replace(/^(?:\.?\/)+/, "").replace(/\/+$/, "");
2829
+ let re = "";
2830
+ for (let i = 0; i < body.length; i++) {
2831
+ const c = body[i];
2832
+ if (c === "*" && body[i + 1] === "*") {
2833
+ i++;
2834
+ if (body[i + 1] === "/") {
2835
+ i++;
2836
+ re += "(?:.*/)?";
2837
+ } else re += ".*";
2838
+ } else if (c === "*") re += "[^/]*";
2839
+ else if (c === "?") re += "[^/]";
2840
+ else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2841
+ }
2842
+ return {
2843
+ negate,
2844
+ re: new RegExp(`^${re}$`)
2845
+ };
2846
+ });
2847
+ return (path) => {
2848
+ const parts = path.split("/");
2849
+ const prefixes = parts.map((_, i) => parts.slice(0, i + 1).join("/"));
2850
+ let excluded = false;
2851
+ for (const rule of rules) if (prefixes.some((p) => rule.re.test(p))) excluded = !rule.negate;
2852
+ return excluded;
2853
+ };
2854
+ }
2855
+ /** Files in the build context that `docker build` would send, sorted, relative to `context`. */
2856
+ function contextFiles(context, dockerfile) {
2857
+ const own = [`${dockerfile}.dockerignore`, join(context, ".dockerignore")].find(existsSync);
2858
+ const ignored = dockerignoreMatcher(own ? readFileSync(own, "utf8") : "");
2859
+ const always = /* @__PURE__ */ new Set([relative(context, dockerfile), ".dockerignore"]);
2860
+ const files = [];
2861
+ const walk = (rel) => {
2862
+ for (const entry of readdirSync(join(context, rel), { withFileTypes: true })) {
2863
+ const path = rel ? `${rel}/${entry.name}` : entry.name;
2864
+ if (ignored(path) && !always.has(path)) continue;
2865
+ if (entry.isDirectory()) walk(path);
2866
+ else files.push(path);
2867
+ }
2868
+ };
2869
+ walk("");
2870
+ return files.sort();
2871
+ }
2872
+ /** Whether the Dockerfile starts from reeval's sandbox image, which then has to exist first. */
2873
+ const usesEsBase = (dockerfile) => new RegExp(`\\$\\{?${REEVAL_BASE_ARG}\\b`).test(readFileSync(dockerfile, "utf8"));
2874
+ /**
2875
+ * The tag of a `docker({ dockerfile })` build for a harness: a hash of the Dockerfile, every file
2876
+ * of its build context (its directory, less `.dockerignore`) and, for a Dockerfile `FROM
2877
+ * ${REEVAL_BASE}`, the id of the harness's image it names, so an edit to any of them, or a rebuilt reeval
2878
+ * image, builds anew. `esBase` is that id (null when it isn't built).
2879
+ */
2880
+ function dockerfileTag(dockerfile, harness, esBase = null) {
2881
+ const context = dirname(dockerfile);
2882
+ const base = usesEsBase(dockerfile) ? `${imageFor(harness)}@${esBase ?? "unbuilt"}` : "";
2883
+ const hash = createHash("sha256").update(`${REEVAL_BASE_ARG}=${base}\0`);
2884
+ for (const path of contextFiles(context, dockerfile)) {
2885
+ const full = join(context, path);
2886
+ const stat = lstatSync(full);
2887
+ hash.update(`${path}\0${stat.mode & 73 ? "x" : "-"}\0`);
2888
+ hash.update(stat.isSymbolicLink() ? readlinkSync(full) : readFileSync(full));
2889
+ hash.update("\0");
2890
+ }
2891
+ const digest = hash.digest("hex");
2892
+ return {
2893
+ tag: `reeval-dockerfile:${slugify(basename(dockerfile) === "Dockerfile" ? basename(context) : basename(dockerfile), 40) || "image"}-${digest.slice(0, 16)}`,
2894
+ sha256: digest
2895
+ };
2896
+ }
2897
+ /**
2898
+ * Builds the user's Dockerfile with plain `docker build` (so Docker's layer cache applies), from
2899
+ * its directory, with `REEVAL_BASE` set to the harness's sandbox image (whose id is `esBase`).
2900
+ * Rebuilds even when the tag exists with `force`, as `reeval image build` does, and then removes the
2901
+ * image the tag pointed at before. `name` is the path failures name.
2902
+ */
2903
+ async function buildDockerfile(dockerfile, options) {
2904
+ const { harness, signal } = options;
2905
+ const { tag } = dockerfileTag(dockerfile, harness, options.esBase ?? null);
2906
+ const previous = (await imageInfo(tag))?.id;
2907
+ if (previous && !options.force) return tag;
2908
+ const build = await docker([
2909
+ "build",
2910
+ "--progress",
2911
+ "plain",
2912
+ ...BUILT_BY,
2913
+ ...options.pull ? ["--pull"] : [],
2914
+ "--file",
2915
+ dockerfile,
2916
+ "--build-arg",
2917
+ `${REEVAL_BASE_ARG}=${imageFor(harness)}`,
2918
+ "--tag",
2919
+ tag,
2920
+ dirname(dockerfile)
2921
+ ], { signal });
2922
+ const output = `${build.stdout.toString("utf8")}${build.stderr}`;
2923
+ options.output?.write(output);
2924
+ if (build.exitCode !== 0) throw buildError(output, {
2925
+ stage: "dockerfile",
2926
+ ref: tag,
2927
+ headline: `Building ${options.name ?? dockerfile}`,
2928
+ log: options.output?.path ?? null
2929
+ });
2930
+ const now = (await imageInfo(tag))?.id;
2931
+ if (previous && previous !== now) await removeQuietly(previous);
2932
+ return tag;
2933
+ }
2934
+ //#endregion
2935
+ export { onDockerCommand as $, hostAllowed as A, WORKSPACE_DIR as B, applyTransformations as C, HARNESS_HOSTS as D, DOCKER_EGRESS as E, HARNESS_PACKAGES as F, docker as G, harnessLabel as H, LABEL as I, dockerOk as J, dockerAvailable as K, SANDBOX_HOME as L, BASE_IMAGE as M, BRIDGE_PORT as N, PACKAGE_HOSTS as O, DEFAULT_IMAGE as P, removeContainerDetached as Q, SANDBOX_ROOT as R, placeholder as S, onEndpoint as T, imageFor as U, adapterVersion as V, isBakedHarness as W, imageInfo as X, dockerState as Y, isRegistryDigest as Z, ROOT_CAPABILITIES as _, buildInstallLayer as a, failureCause as at, CA_PATH as b, dockerfileTag as c, retryTransient as ct, installLayerTag as d, run as et, installScript as f, DEFAULT_LIMITS as g, usesEsBase as h, buildImage as i, causeLine as it, BAKED_HARNESSES as j, defaultAllowedHosts as k, dockerignoreMatcher as l, resolveFrom as m, REEVAL_BASE_ARG as n, ImageBuildError as nt, contextFiles as o, isTransient as ot, pruneImages as p, dockerCapacity as q, buildDockerfile as r, RETRY_DELAYS as rt, contextHash as s, parseBuildOutput as st, DOCKER_CONTEXT as t, shellQuote as tt, harnessImageFor as u, createDockerSession as v, matches as w, PROXY_PORT as x, dockerSandboxProvider as y, SANDBOX_USER as z };
2936
+
2937
+ //# sourceMappingURL=src-DwwiVLqv.js.map