@outerlayer/cli 0.2.0 → 0.4.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 (141) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +229 -60
  3. package/dist/agent-setup-IFH3FB5X.js +197 -0
  4. package/dist/build-TZUUTRGH.js +9 -0
  5. package/dist/build-info.json +1 -1
  6. package/dist/check-7MFH6HNC.js +94 -0
  7. package/dist/chunk-3TFPDVTI.js +94 -0
  8. package/dist/{chunk-4TNZMO7V.js → chunk-4C4THABW.js} +5566 -754
  9. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  10. package/dist/chunk-5Y3QQRUH.js +300 -0
  11. package/dist/chunk-774HEQL6.js +178 -0
  12. package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-7WERLFVR.js} +9 -26
  13. package/dist/chunk-ABIWDUMS.js +104 -0
  14. package/dist/chunk-AUFG23AA.js +534 -0
  15. package/dist/chunk-B2G7JHHB.js +42 -0
  16. package/dist/chunk-BCJNHMZT.js +34 -0
  17. package/dist/chunk-BCLJSQCV.js +56 -0
  18. package/dist/chunk-BFESLKPP.js +61 -0
  19. package/dist/chunk-BJ3KTMDH.js +63 -0
  20. package/dist/chunk-BKO6JEZI.js +47 -0
  21. package/dist/chunk-BOLTI6LR.js +37 -0
  22. package/dist/chunk-CBPPJSAR.js +89 -0
  23. package/dist/chunk-DM3VFDS3.js +1461 -0
  24. package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
  25. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  26. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  27. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  28. package/dist/chunk-F6GFZXZ3.js +60 -0
  29. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  30. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  31. package/dist/chunk-HZRNMGLF.js +2233 -0
  32. package/dist/chunk-LF6MLJCY.js +38 -0
  33. package/dist/{chunk-U32VSRLO.js → chunk-LT5TZHYH.js} +95 -634
  34. package/dist/chunk-LYCDNMOH.js +83 -0
  35. package/dist/chunk-M5POMKKW.js +1051 -0
  36. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  37. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  38. package/dist/chunk-N6LURUEF.js +64 -0
  39. package/dist/chunk-OGFGZQA3.js +23 -0
  40. package/dist/chunk-QDQEUVUF.js +9 -0
  41. package/dist/chunk-QOZKPLVJ.js +226 -0
  42. package/dist/chunk-QY22NZUW.js +1764 -0
  43. package/dist/chunk-RA3O54FC.js +30 -0
  44. package/dist/chunk-RFUPN6KX.js +71 -0
  45. package/dist/chunk-SY4GK4SP.js +55 -0
  46. package/dist/chunk-TBT347UY.js +41 -0
  47. package/dist/chunk-TWNWNS2Q.js +1084 -0
  48. package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
  49. package/dist/chunk-USO2DKBD.js +421 -0
  50. package/dist/chunk-W4SQCOZU.js +206 -0
  51. package/dist/{chunk-Q6CL5THG.js → chunk-W5FVUZXV.js} +208 -61
  52. package/dist/chunk-WFJ65NUD.js +383 -0
  53. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  54. package/dist/chunk-WO2BXCTQ.js +83 -0
  55. package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
  56. package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
  57. package/dist/chunk-Y7KJLXYN.js +232 -0
  58. package/dist/chunk-YFHIIH4B.js +102 -0
  59. package/dist/chunk-YYYXRJUW.js +101 -0
  60. package/dist/chunk-ZM2IMMYN.js +76 -0
  61. package/dist/chunk-ZMYPLFG3.js +71 -0
  62. package/dist/chunk-ZNA27WEV.js +470 -0
  63. package/dist/{cli-G7DILYJY.js → cli-W62AFRSW.js} +616 -878
  64. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  65. package/dist/config-XVJYZ4IQ.js +9 -0
  66. package/dist/connect-cmd-ZTJONP37.js +16 -0
  67. package/dist/context-adopt-UMA4O5NY.js +105 -0
  68. package/dist/context-materialize-G2FUFC72.js +19 -0
  69. package/dist/dist-44CQVWGT.js +6 -0
  70. package/dist/docker-NEGDLU6D.js +7 -0
  71. package/dist/doctor-53F2PYY7.js +43 -0
  72. package/dist/doctor-ZLGZYMDK.js +426 -0
  73. package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-AKZP7TMF.js} +87 -23
  74. package/dist/emit-cmd-DCERAU27.js +9 -0
  75. package/dist/emit-criteria-cmd-7MXQVCKD.js +166 -0
  76. package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-N2FTGCVC.js} +34 -31
  77. package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-4CPDBMN2.js} +34 -62
  78. package/dist/exec-client-4XGXV3XN.js +8 -0
  79. package/dist/guest-init-GENRHNP7.js +8 -0
  80. package/dist/hook-fast-EYENPK6F.js +9 -0
  81. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  82. package/dist/host-key-KDYZ5ECC.js +7 -0
  83. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  84. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  85. package/dist/index.js +4 -4
  86. package/dist/init-55Y4LHWF.js +36 -0
  87. package/dist/init-XXEKWZQP.js +221 -0
  88. package/dist/init-cmd-WLST2F7G.js +135 -0
  89. package/dist/install-cmd-7WXOIFDO.js +55 -0
  90. package/dist/lima-XN65D7GN.js +55 -0
  91. package/dist/login-browser-7XGSV7MA.js +10 -0
  92. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  93. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  94. package/dist/loop-JFIBP4B7.js +43 -0
  95. package/dist/machine-ZBPT2J3R.js +35 -0
  96. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  97. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-N6UD2KBX.js} +20 -9
  98. package/dist/paths-OYKMVYJP.js +6 -0
  99. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  100. package/dist/real-deps-SU24ZA2K.js +21 -0
  101. package/dist/relay-L76HDX72.js +46 -0
  102. package/dist/settings-J2652U5N.js +7 -0
  103. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  104. package/dist/{status-I27IST4L.js → status-UJXWZRN5.js} +30 -13
  105. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  106. package/dist/sync-cmd-4G4A7EVN.js +28 -0
  107. package/dist/version-BWM6VLDI.js +6 -0
  108. package/dist/{watch-3DTPJETH.js → watch-7SKJKGAA.js} +19 -8
  109. package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-5F7VZ42N.js} +34 -21
  110. package/dist/work-cmd-QAUZ7MTD.js +17 -0
  111. package/dist/work-comment-cmd-BNCSBTLH.js +106 -0
  112. package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
  113. package/dist/work-open-pr-cmd-VFYZHIIW.js +156 -0
  114. package/dist/work-pr-cmd-PV32ZLSI.js +16 -0
  115. package/package.json +13 -3
  116. package/skill-pack/maintained/amend/SKILL.md +104 -0
  117. package/skill-pack/maintained/emitting-evidence/SKILL.md +108 -0
  118. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  119. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  120. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  121. package/skill-pack/template/build/SKILL.md +193 -0
  122. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  123. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  124. package/skill-pack/template/build/references/evidence.md +42 -0
  125. package/skill-pack/template/build/references/release.md +74 -0
  126. package/skill-pack/template/build/references/review-briefs.md +275 -0
  127. package/skill-pack/template/build/references/review-loop.md +158 -0
  128. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  129. package/skill-pack/template/spec/SKILL.md +84 -0
  130. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  131. package/dist/chunk-DCNOXRMV.js +0 -589
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-OZ7C3XUE.js +0 -34
  134. package/dist/chunk-WQ6VGRGZ.js +0 -150
  135. package/dist/hook-fast-J5LCDHUJ.js +0 -8
  136. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  137. package/dist/init-PTBITAUO.js +0 -103
  138. package/dist/login-cmd-IRX6LZT7.js +0 -62
  139. package/dist/loop-ASZRX3CZ.js +0 -648
  140. package/dist/sync-cmd-BBAUZ5JD.js +0 -17
  141. package/dist/work-cmd-2P4BVX47.js +0 -18
@@ -0,0 +1,2233 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { readDefaultBranchHead, cloneRepository, buildRepositoryImage, CLAUDE_CODE_VERSION, firecrackerArch, MICROVM_ARCHES, DEFAULT_BASE_IMAGE, GUEST_HELPER_SHA256, GUEST_ASSETS_BASE_URL, GUEST_KERNEL_SHA256, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, FIRECRACKER_VERSION, GUEST_ASSETS_VERSION } from './chunk-ZNA27WEV.js';
4
+ import { startTimeOf, isSameProcess } from './chunk-N5FOE5PS.js';
5
+ import { currentCliBuild } from './chunk-OGFGZQA3.js';
6
+ import { createDockerHost, childEnvironment, spawnRun } from './chunk-774HEQL6.js';
7
+ import { RecipeError } from './chunk-QOZKPLVJ.js';
8
+ import { execCapture } from './chunk-W4SQCOZU.js';
9
+ import { GUEST_CID } from './chunk-B2G7JHHB.js';
10
+ import { WorkCommandError, postJson } from './chunk-W5FVUZXV.js';
11
+ import { parseDuKilobytes, hostAllowed } from './chunk-F6GFZXZ3.js';
12
+ import { RELAY_PORT, CHECKOUT_DIR, JOB_MOUNT, claudeCredential, buildEnvironment, startBuildServices, AGENT_CREDENTIAL_MISSING, GATEWAY_PORT, GATEWAY_SOCKET, CLAUDE_PORT, CLAUDE_SOCKET, DESTINATION_BASE_PORT } from './chunk-AUFG23AA.js';
13
+ import { defaultGitRunner, materializeContext } from './chunk-XDDW4FRS.js';
14
+ import { readContextManifest, readSourceCache } from './chunk-WIOZAJ2W.js';
15
+ import { signingFetchFor } from './chunk-BCLJSQCV.js';
16
+ import { signerFor } from './chunk-M5POMKKW.js';
17
+ import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, stagedContextDir, contextHomeDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, vmAssetsDir } from './chunk-ZM2IMMYN.js';
18
+ import { relative, join, dirname, basename } from 'path';
19
+ import { spawnSync, spawn } from 'child_process';
20
+ import { randomBytes, createHash } from 'crypto';
21
+ import { existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, renameSync, lstatSync, readFileSync, unlinkSync, readdirSync, openSync, closeSync, mkdtempSync, createWriteStream, statSync, createReadStream } from 'fs';
22
+ import { createServer, request } from 'http';
23
+ import { request as request$1 } from 'https';
24
+ import { Transform } from 'stream';
25
+ import { pipeline, finished } from 'stream/promises';
26
+ import { createGunzip, gunzipSync, constants } from 'zlib';
27
+ import { isIP, connect, createServer as createServer$1 } from 'net';
28
+ import { lookup } from 'dns/promises';
29
+ import { networkInterfaces } from 'os';
30
+
31
+ createRequire(import.meta.url);
32
+ function logStamp(at) {
33
+ return at.toISOString().replace(/\.\d{3}Z$/, "Z");
34
+ }
35
+ function createRunnerLog(now = () => /* @__PURE__ */ new Date()) {
36
+ const write = (stream, text) => {
37
+ stream.write(`${logStamp(now())} ${text}
38
+ `);
39
+ };
40
+ return {
41
+ line: (text) => write(process.stdout, text),
42
+ error: (text) => {
43
+ write(process.stdout, text);
44
+ write(process.stderr, text);
45
+ }
46
+ };
47
+ }
48
+ function formatDuration(ms) {
49
+ const total = Math.max(0, Math.round(ms / 1e3));
50
+ const hours = Math.floor(total / 3600);
51
+ const minutes = Math.floor(total % 3600 / 60);
52
+ const seconds = total % 60;
53
+ if (hours > 0) return `${hours}h ${minutes}m`;
54
+ if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
55
+ return `${seconds}s`;
56
+ }
57
+ function formatClock(at, today) {
58
+ const hhmm = `${String(at.getUTCHours()).padStart(2, "0")}:${String(at.getUTCMinutes()).padStart(2, "0")} UTC`;
59
+ const sameDay = at.getUTCFullYear() === today.getUTCFullYear() && at.getUTCMonth() === today.getUTCMonth() && at.getUTCDate() === today.getUTCDate();
60
+ if (sameDay) return hhmm;
61
+ const month = at.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
62
+ return `${month} ${at.getUTCDate()} ${hhmm}`;
63
+ }
64
+ function shortClaim(claimId) {
65
+ return claimId.length <= 8 ? claimId : `${claimId.slice(0, 8)}\u2026`;
66
+ }
67
+ function relativeToRunner(runnerDirPath, path) {
68
+ const rel = relative(runnerDirPath, path);
69
+ return rel === "" || rel.startsWith("..") ? path : rel;
70
+ }
71
+ function describeCallFailure(err) {
72
+ if (err instanceof Error) {
73
+ const status = err.status;
74
+ return status === void 0 ? err.message : `${err.message} (${status})`;
75
+ }
76
+ return String(err);
77
+ }
78
+
79
+ // src/runner/retry.ts
80
+ var RETRY_WINDOW_MS = 12e4;
81
+ var FIRST_BACKOFF_MS = 1e3;
82
+ var MAX_BACKOFF_MS = 15e3;
83
+ function isRetryable(err) {
84
+ if (err instanceof WorkCommandError) return err.retryable;
85
+ return true;
86
+ }
87
+ async function retrySend(send, deps = {}) {
88
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
89
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
90
+ const windowMs = deps.windowMs ?? RETRY_WINDOW_MS;
91
+ const startedMs = now().getTime();
92
+ let attempts = 0;
93
+ let backoffMs = FIRST_BACKOFF_MS;
94
+ for (; ; ) {
95
+ attempts++;
96
+ try {
97
+ return { ok: true, value: await send(), attempts };
98
+ } catch (err) {
99
+ const reason = describeCallFailure(err);
100
+ const code = err instanceof WorkCommandError ? err.code : void 0;
101
+ const status = err instanceof WorkCommandError ? err.status : void 0;
102
+ const message = err instanceof WorkCommandError ? err.message : void 0;
103
+ if (!isRetryable(err)) return { ok: false, attempts, reason, retryable: false, code, status, message };
104
+ const elapsedMs = now().getTime() - startedMs;
105
+ if (elapsedMs + backoffMs >= windowMs) return { ok: false, attempts, reason, retryable: true, code, status, message };
106
+ await sleep(backoffMs);
107
+ backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
108
+ }
109
+ }
110
+ }
111
+
112
+ // src/runner/build-tokens.ts
113
+ var ISSUE_RETRY_WINDOW_MS = 3e4;
114
+ var REVOKE_TIMEOUT_MS = 15e3;
115
+ function isDue(expiresAtMs, nowMs) {
116
+ return nowMs >= expiresAtMs - 10 * 6e4;
117
+ }
118
+ function classifyRefusal(err) {
119
+ if (!(err instanceof WorkCommandError)) return { kind: "retry" };
120
+ if (err.status === 401 || err.code === "claim_expired" || err.code === "claim_not_held") return { kind: "lease_lost" };
121
+ if (err.code === "github_unavailable") return { kind: "retry" };
122
+ if (err.retryable && err.code !== "repository_tokens_unavailable") return { kind: "retry" };
123
+ return { kind: "failed", reason: refusalCode(err) };
124
+ }
125
+ function refusalCode(err) {
126
+ if (!(err instanceof WorkCommandError)) return "unavailable";
127
+ return err.code ?? `http_${err.status ?? "unknown"}`;
128
+ }
129
+ var BuildTokens = class {
130
+ constructor(options) {
131
+ this.options = options;
132
+ this.held = /* @__PURE__ */ new Map();
133
+ this.asking = /* @__PURE__ */ new Map();
134
+ this.issued = [];
135
+ this.usedKinds = /* @__PURE__ */ new Set();
136
+ this.revoked = false;
137
+ }
138
+ now() {
139
+ return (this.options.now ?? (() => /* @__PURE__ */ new Date()))();
140
+ }
141
+ /** The token of `kind`, asking the gateway for a new one when the build holds none or its token is within ten minutes of expiry. */
142
+ token(kind) {
143
+ const held = this.held.get(kind);
144
+ if (held !== void 0 && !isDue(held.expiresAtMs, this.now().getTime())) return Promise.resolve(held.token);
145
+ let asking = this.asking.get(kind);
146
+ if (asking === void 0) {
147
+ asking = this.issueOrKeep(kind).finally(() => this.asking.delete(kind));
148
+ this.asking.set(kind, asking);
149
+ }
150
+ return asking;
151
+ }
152
+ /** A due token that has not yet expired is kept when the gateway cannot issue a new one. */
153
+ async issueOrKeep(kind) {
154
+ try {
155
+ return await this.issue(kind);
156
+ } catch (err) {
157
+ const held = this.held.get(kind);
158
+ if (held !== void 0 && this.now().getTime() < held.expiresAtMs && classifyRefusal(err).kind === "retry") return held.token;
159
+ throw err;
160
+ }
161
+ }
162
+ async issue(kind) {
163
+ const { config, item, fetchImpl } = this.options;
164
+ const asked = await retrySend(
165
+ async () => {
166
+ try {
167
+ return await postJson(
168
+ config.url,
169
+ `/v1/work-items/${item}/claim/tokens`,
170
+ config.apiKey,
171
+ config.appId,
172
+ { access: kind },
173
+ fetchImpl,
174
+ void 0,
175
+ await signerFor(config.configPath)
176
+ );
177
+ } catch (err2) {
178
+ if (err2 instanceof WorkCommandError && err2.code === "repository_tokens_unavailable") {
179
+ throw new WorkCommandError(err2.message, false, err2.status, err2.code);
180
+ }
181
+ throw err2;
182
+ }
183
+ },
184
+ { now: this.options.now, sleep: this.options.sleep, windowMs: ISSUE_RETRY_WINDOW_MS }
185
+ );
186
+ let err;
187
+ if (!asked.ok) {
188
+ err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
189
+ } else {
190
+ const { token, expiresAt } = asked.value.data;
191
+ const expiresAtMs = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
192
+ if (typeof token === "string" && token !== "" && Number.isFinite(expiresAtMs)) {
193
+ if (this.revoked) {
194
+ await this.revoke([token]);
195
+ throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
196
+ }
197
+ this.held.set(kind, { token, expiresAtMs });
198
+ this.issued.push(token);
199
+ if (kind === "read") this.readSink?.(token);
200
+ return token;
201
+ }
202
+ err = new WorkCommandError("the gateway's token answer carried no token or no expiry", false, void 0, "malformed_token_response");
203
+ }
204
+ const refusal = classifyRefusal(err);
205
+ if (refusal.kind !== "retry") this.failure = refusal;
206
+ throw err;
207
+ }
208
+ /** Asks for a new read token when the build holds one within ten minutes of expiry. A refusal is kept for `refusal()`, not thrown. */
209
+ async renewReadIfDue() {
210
+ const held = this.held.get("read");
211
+ if (held === void 0 || !isDue(held.expiresAtMs, this.now().getTime())) return;
212
+ await this.token("read").catch(() => void 0);
213
+ }
214
+ /** Calls `sink` with the read token now, and again with each one that replaces it. */
215
+ onRead(sink) {
216
+ this.readSink = sink;
217
+ const held = this.held.get("read");
218
+ if (held !== void 0) sink(held.token);
219
+ }
220
+ markUsed(kind) {
221
+ if (this.usedKinds.has(kind)) return;
222
+ this.usedKinds.add(kind);
223
+ this.options.onUsed?.(this.used());
224
+ }
225
+ /** The kinds of token the build has used, read before push. */
226
+ used() {
227
+ return ["read", "push"].filter((kind) => this.usedKinds.has(kind));
228
+ }
229
+ /** The last refusal that ends the build, if a renewal or a push's token request met one. */
230
+ refusal() {
231
+ return this.failure;
232
+ }
233
+ /** Revokes every token this holder was issued, the superseded ones too, at GitHub with the token itself. A failure is not an error: the token lapses within the hour. */
234
+ async revokeAll() {
235
+ this.revoked = true;
236
+ const tokens = this.issued.splice(0);
237
+ this.held.clear();
238
+ await this.revoke(tokens);
239
+ }
240
+ async revoke(tokens) {
241
+ const apiUrl = this.options.apiUrl ?? "https://api.github.com";
242
+ await Promise.allSettled(
243
+ tokens.map(
244
+ (token) => this.options.fetchImpl(`${apiUrl}/installation/token`, {
245
+ method: "DELETE",
246
+ headers: {
247
+ authorization: `Bearer ${token}`,
248
+ accept: "application/vnd.github+json",
249
+ "x-github-api-version": "2022-11-28",
250
+ "user-agent": "outerlayer-runner"
251
+ },
252
+ signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
253
+ })
254
+ )
255
+ );
256
+ }
257
+ };
258
+ var BranchPrepareError = class extends Error {
259
+ };
260
+ var OWN_PREFIX = "outerlayer/";
261
+ var GIT_TIMEOUT_MS = 12e4;
262
+ var REPO_BINDING_GIT_VARS = new Set(
263
+ `GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_OBJECT_DIRECTORY GIT_DIR GIT_WORK_TREE GIT_IMPLICIT_WORK_TREE
264
+ GIT_GRAFT_FILE GIT_INDEX_FILE GIT_NO_REPLACE_OBJECTS GIT_REPLACE_REF_BASE GIT_PREFIX
265
+ GIT_INTERNAL_SUPER_PREFIX GIT_SHALLOW_FILE GIT_COMMON_DIR GIT_NAMESPACE`.split(/\s+/)
266
+ );
267
+ function localGit(cwd, args) {
268
+ const env = {};
269
+ for (const [key, value] of Object.entries(process.env)) {
270
+ if (!REPO_BINDING_GIT_VARS.has(key)) env[key] = value;
271
+ }
272
+ env.GIT_TERMINAL_PROMPT = "0";
273
+ const result = spawnSync("git", args, { cwd, env, encoding: "utf8", timeout: GIT_TIMEOUT_MS });
274
+ if (result.error !== void 0) {
275
+ return Promise.resolve({ status: null, stdout: "", stderr: result.error.message });
276
+ }
277
+ return Promise.resolve({ status: result.status, stdout: result.stdout.trim(), stderr: result.stderr.trim() });
278
+ }
279
+ function firstLine(stderr) {
280
+ return (stderr.split("\n")[0] ?? "").replace(/\/\/[^/@\s]+@/g, "//");
281
+ }
282
+ async function mustGit(git2, cwd, step, args) {
283
+ const result = await git2(cwd, args);
284
+ if (result.status !== 0) {
285
+ const detail = firstLine(result.stderr);
286
+ throw new BranchPrepareError(`${step} failed${detail === "" ? "" : `: ${detail}`}`);
287
+ }
288
+ return result.stdout;
289
+ }
290
+ async function remoteDefaultBranch(git2, workdir) {
291
+ const out = await mustGit(git2, workdir, "reading the remote's default branch", ["ls-remote", "--symref", "origin", "HEAD"]);
292
+ const match = /^ref: refs\/heads\/(\S+)\s+HEAD$/m.exec(out);
293
+ if (match === null) {
294
+ throw new BranchPrepareError("reading the remote's default branch failed: the remote names none");
295
+ }
296
+ return match[1];
297
+ }
298
+ async function remoteHasBranch(git2, workdir, branch) {
299
+ return await mustGit(git2, workdir, `looking for ${branch} on the remote`, ["ls-remote", "--heads", "origin", `refs/heads/${branch}`]) !== "";
300
+ }
301
+ async function fetchBranch(git2, workdir, branch) {
302
+ await mustGit(git2, workdir, `fetching ${branch}`, ["fetch", "--quiet", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]);
303
+ }
304
+ async function prepareBranch(opts) {
305
+ const { workdir, branch } = opts;
306
+ const git2 = opts.git ?? localGit;
307
+ const defaultBranch = await remoteDefaultBranch(git2, workdir);
308
+ const exists = await remoteHasBranch(git2, workdir, branch);
309
+ await fetchBranch(git2, workdir, defaultBranch);
310
+ if (exists) await fetchBranch(git2, workdir, branch);
311
+ let start;
312
+ if (exists && !(branch.startsWith(OWN_PREFIX) && await isMerged(git2, workdir, branch, defaultBranch))) {
313
+ start = `origin/${branch}`;
314
+ } else if (branch.startsWith(OWN_PREFIX)) {
315
+ start = `origin/${defaultBranch}`;
316
+ } else {
317
+ throw new BranchPrepareError(`the pull request's branch ${branch} is not on the remote`);
318
+ }
319
+ await mustGit(git2, workdir, `checking out ${branch}`, ["checkout", "--quiet", "-B", branch, start]);
320
+ return { branch, head: await mustGit(git2, workdir, "reading the checked-out commit", ["rev-parse", "HEAD"]) };
321
+ }
322
+ async function isMerged(git2, workdir, branch, defaultBranch) {
323
+ const result = await git2(workdir, ["merge-base", "--is-ancestor", `origin/${branch}`, `origin/${defaultBranch}`]);
324
+ if (result.status === 0) return true;
325
+ if (result.status === 1) return false;
326
+ const detail = firstLine(result.stderr);
327
+ throw new BranchPrepareError(`comparing ${branch} with ${defaultBranch} failed${detail === "" ? "" : `: ${detail}`}`);
328
+ }
329
+
330
+ // src/runner/container/lifecycle.ts
331
+ var ORDER = ["onCreateCommand", "updateContentCommand", "postCreateCommand", "postStartCommand"];
332
+ function commandArgv(command) {
333
+ if (typeof command === "string") return command === "" ? void 0 : ["/bin/sh", "-c", command];
334
+ if (Array.isArray(command) && command.length > 0 && command.every((part) => typeof part === "string")) return command;
335
+ return void 0;
336
+ }
337
+ function lifecycleSteps(lifecycle) {
338
+ const steps = [];
339
+ for (const name of ORDER) {
340
+ const entries = lifecycle[name];
341
+ if (entries === void 0) continue;
342
+ for (const { source, command } of entries) {
343
+ if (typeof command === "object" && command !== null && !Array.isArray(command)) {
344
+ for (const [key, value] of Object.entries(command)) {
345
+ const argv2 = commandArgv(value);
346
+ if (argv2 !== void 0) steps.push({ name: `${name}[${key}]`, source, argv: argv2 });
347
+ }
348
+ continue;
349
+ }
350
+ const argv = commandArgv(command);
351
+ if (argv !== void 0) steps.push({ name, source, argv });
352
+ }
353
+ }
354
+ return steps;
355
+ }
356
+ function lifecycleFailureReason(step, exitCode) {
357
+ return `lifecycle command ${step.name} (${step.source}) exited ${exitCode}`;
358
+ }
359
+ var AssetError = class extends Error {
360
+ constructor(file, message) {
361
+ super(message);
362
+ this.file = file;
363
+ }
364
+ };
365
+ var FIRECRACKER_RELEASES = "https://github.com/firecracker-microvm/firecracker/releases/download";
366
+ var DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
367
+ function pinnedAssets(arch) {
368
+ const version = FIRECRACKER_VERSION;
369
+ return {
370
+ firecracker: {
371
+ url: `${FIRECRACKER_RELEASES}/v${version}/firecracker-v${version}-${arch}.tgz`,
372
+ sha256: FIRECRACKER_SHA256[arch],
373
+ binarySha256: FIRECRACKER_BINARY_SHA256[arch]
374
+ },
375
+ kernel: { url: `${GUEST_ASSETS_BASE_URL}/vmlinux-${arch}`, sha256: GUEST_KERNEL_SHA256[arch] },
376
+ helper: { url: `${GUEST_ASSETS_BASE_URL}/outerlayer-guest-${arch}`, sha256: GUEST_HELPER_SHA256[arch] }
377
+ };
378
+ }
379
+ var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
380
+ function pinProblem(file, expected, actual, where) {
381
+ const release = file.startsWith("vmlinux-") || file.startsWith("outerlayer-guest-") ? ` (guest assets release ${GUEST_ASSETS_VERSION})` : "";
382
+ return new AssetError(file, `${file} ${where} has sha256 ${actual}, not the ${expected} this release pins${release}, so no microVM build starts`);
383
+ }
384
+ function writeAtomic(path, bytes, mode) {
385
+ const temp = `${path}.${process.pid}.tmp`;
386
+ writeFileSync(temp, bytes, { mode });
387
+ renameSync(temp, path);
388
+ }
389
+ async function download(file, url, fetchImpl) {
390
+ let response;
391
+ try {
392
+ response = await fetchImpl(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
393
+ } catch (err) {
394
+ throw new AssetError(file, `${file} could not be downloaded from ${url}: ${err instanceof Error ? err.message : String(err)}`);
395
+ }
396
+ if (!response.ok) throw new AssetError(file, `${file} could not be downloaded from ${url}: the server answered ${response.status}`);
397
+ return Buffer.from(await response.arrayBuffer());
398
+ }
399
+ async function ensureFile(dir, file, pin, fetchImpl, mode) {
400
+ const path = join(dir, file);
401
+ if (existsSync(path)) {
402
+ const actual2 = sha256Of(readFileSync(path));
403
+ if (actual2 !== pin.sha256) throw pinProblem(file, pin.sha256, actual2, `at ${path}`);
404
+ return path;
405
+ }
406
+ const bytes = await download(file, pin.url, fetchImpl);
407
+ const actual = sha256Of(bytes);
408
+ if (actual !== pin.sha256) throw pinProblem(file, pin.sha256, actual, `downloaded from ${pin.url}`);
409
+ writeAtomic(path, bytes, mode);
410
+ return path;
411
+ }
412
+ function extractWithTar(arch) {
413
+ const name = `firecracker-v${FIRECRACKER_VERSION}-${arch}`;
414
+ return async (tarball, destDir) => {
415
+ const result = spawnSync("tar", ["-xzf", tarball, "-C", destDir, "--strip-components=1", `release-v${FIRECRACKER_VERSION}-${arch}/${name}`], { encoding: "utf8" });
416
+ if (result.status !== 0) throw new Error(`tar could not unpack ${tarball}: ${result.stderr.trim()}`);
417
+ return join(destDir, name);
418
+ };
419
+ }
420
+ async function ensureAssets(options) {
421
+ const arch = options.arch ?? firecrackerArch(process.arch);
422
+ if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("firecracker", `no Firecracker or guest assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
423
+ const pins = options.pins ?? pinnedAssets(arch);
424
+ const fetchImpl = options.fetchImpl ?? fetch;
425
+ const dir = vmAssetsDir(options.runnerDir);
426
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
427
+ const firecracker = join(dir, "firecracker");
428
+ if (existsSync(firecracker)) {
429
+ const actual = sha256Of(readFileSync(firecracker));
430
+ if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `at ${firecracker}`);
431
+ } else {
432
+ const tarballName = `firecracker-${arch}.tgz`;
433
+ const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
434
+ const scratch = mkdtempSync(join(dir, ".extract-"));
435
+ try {
436
+ const extracted = await (options.extractFirecracker ?? extractWithTar(arch))(tarball, scratch);
437
+ const bytes = readFileSync(extracted);
438
+ const actual = sha256Of(bytes);
439
+ if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `unpacked from ${tarballName}`);
440
+ writeAtomic(firecracker, bytes, 493);
441
+ } finally {
442
+ rmSync(scratch, { recursive: true, force: true });
443
+ }
444
+ }
445
+ const kernel = await ensureFile(dir, `vmlinux-${arch}`, pins.kernel, fetchImpl, RUNNER_FILE_MODE);
446
+ const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
447
+ return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
448
+ }
449
+ function converterScript() {
450
+ return [
451
+ "set -eu",
452
+ "rootfs=/tmp/rootfs",
453
+ 'mkdir -p "$rootfs"',
454
+ 'tar -xf - -C "$rootfs"',
455
+ 'used_kb=$(du -sk "$rootfs" | cut -f1)',
456
+ 'files=$(find "$rootfs" | wc -l)',
457
+ // Room for the file system's own metadata, and for an inode per file with some to spare.
458
+ "total_kb=$(( used_kb + used_kb / 4 + 65536 ))",
459
+ 'truncate -s "${total_kb}K" "/out/$OUT_NAME"',
460
+ 'mke2fs -q -F -t ext4 -m 0 -N $(( files + files / 4 + 4096 )) -d "$rootfs" "/out/$OUT_NAME"',
461
+ 'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"'
462
+ ].join("\n");
463
+ }
464
+ function templateScript() {
465
+ return [
466
+ "set -eu",
467
+ 'truncate -s "$BYTES" "/out/$OUT_NAME"',
468
+ 'mke2fs -q -F -t ext4 -m 0 -E lazy_itable_init=1,lazy_journal_init=1,nodiscard "/out/$OUT_NAME"',
469
+ 'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"'
470
+ ].join("\n");
471
+ }
472
+ var ids = () => ({ uid: String(process.getuid?.() ?? 0), gid: String(process.getgid?.() ?? 0) });
473
+ function converterArgs(dest, extra, script) {
474
+ const { uid, gid } = ids();
475
+ return [
476
+ "run",
477
+ "--rm",
478
+ "-i",
479
+ "--network",
480
+ "none",
481
+ "--mount",
482
+ `type=bind,src=${dirname(dest)},dst=/out`,
483
+ "-e",
484
+ `OUT_NAME=${basename(dest)}`,
485
+ "-e",
486
+ `HOST_UID=${uid}`,
487
+ "-e",
488
+ `HOST_GID=${gid}`,
489
+ ...extra,
490
+ // The image that runs `mke2fs` as root is the default base image: Ubuntu holds e2fsprogs.
491
+ DEFAULT_BASE_IMAGE,
492
+ "sh",
493
+ "-c",
494
+ script
495
+ ];
496
+ }
497
+ var firstLine2 = (text) => text.trim().split("\n")[0] ?? "";
498
+ function exportInto(container, converter) {
499
+ return new Promise((resolve, reject) => {
500
+ const exporter = spawn("docker", ["export", container], { stdio: ["ignore", "pipe", "pipe"], env: childEnvironment(process.env) });
501
+ const writer = spawn("docker", converter, { stdio: ["pipe", "ignore", "pipe"], env: childEnvironment(process.env) });
502
+ const errors = [];
503
+ exporter.stderr.on("data", (chunk) => errors.push(`docker export: ${chunk.toString("utf8")}`));
504
+ writer.stderr.on("data", (chunk) => errors.push(`converter: ${chunk.toString("utf8")}`));
505
+ writer.stdin.on("error", () => void 0);
506
+ exporter.stdout.on("error", () => void 0);
507
+ exporter.stdout.pipe(writer.stdin);
508
+ exporter.once("error", reject);
509
+ writer.once("error", reject);
510
+ let exits = 0;
511
+ let failure;
512
+ const done = (name, code) => {
513
+ if (code !== 0 && failure === void 0) failure = `${name} exited ${code}`;
514
+ exits += 1;
515
+ if (exits < 2) return;
516
+ if (failure === void 0) resolve();
517
+ else reject(new Error(`${failure}: ${firstLine2(errors.join("\n"))}`));
518
+ };
519
+ exporter.once("exit", (code) => done("docker export", code));
520
+ writer.once("exit", (code) => done("the converter", code));
521
+ });
522
+ }
523
+ function createRootfsHost() {
524
+ const env = () => childEnvironment(process.env);
525
+ return {
526
+ async convert({ imageId, dest }) {
527
+ const created = await spawnRun("docker", ["create", imageId, "none"], { env: env() });
528
+ if (created.status !== 0) throw new Error(`docker create ${imageId} failed: ${firstLine2(created.stderr)}`);
529
+ const container = created.stdout.trim();
530
+ try {
531
+ await exportInto(container, converterArgs(dest, [], converterScript()));
532
+ } finally {
533
+ await spawnRun("docker", ["rm", "--force", "--volumes", container], { env: env() });
534
+ }
535
+ },
536
+ async makeTemplate({ bytes, dest }) {
537
+ const made = await spawnRun("docker", converterArgs(dest, ["-e", `BYTES=${bytes}`], templateScript()), { env: env() });
538
+ if (made.status !== 0) throw new Error(`making the empty build disk failed: ${firstLine2(made.stderr)}`);
539
+ },
540
+ async imageConfig(imageId) {
541
+ const inspected = await spawnRun("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId], { env: env() });
542
+ if (inspected.status !== 0) throw new Error(`docker image inspect ${imageId} failed: ${firstLine2(inspected.stderr)}`);
543
+ const config = JSON.parse(inspected.stdout);
544
+ const variables = {};
545
+ for (const entry of config.Env ?? []) {
546
+ const eq = entry.indexOf("=");
547
+ if (eq > 0) variables[entry.slice(0, eq)] = entry.slice(eq + 1);
548
+ }
549
+ return { env: variables, user: config.User ?? "" };
550
+ }
551
+ };
552
+ }
553
+ function sweepStalePartials(dir) {
554
+ let names;
555
+ try {
556
+ names = readdirSync(dir);
557
+ } catch {
558
+ return;
559
+ }
560
+ for (const name of names) {
561
+ const pid = /\.partial-(\d+)-[0-9a-f]+$/.exec(name)?.[1];
562
+ if (pid === void 0 || Number(pid) === process.pid) continue;
563
+ try {
564
+ process.kill(Number(pid), 0);
565
+ } catch (err) {
566
+ if (err.code === "ESRCH") rmSync(join(dir, name), { force: true });
567
+ }
568
+ }
569
+ }
570
+ async function madeAtomically(path, make) {
571
+ mkdirSync(dirname(path), { recursive: true, mode: RUNNER_DIR_MODE });
572
+ const temp = `${path}.partial-${process.pid}-${randomBytes(3).toString("hex")}`;
573
+ sweepStalePartials(dirname(path));
574
+ try {
575
+ await make(temp);
576
+ renameSync(temp, path);
577
+ } catch (err) {
578
+ rmSync(temp, { force: true });
579
+ throw err;
580
+ }
581
+ }
582
+ async function ensureRootfs(input) {
583
+ const path = rootfsPathFor(input.runnerDir, input.imageId);
584
+ if (existsSync(path)) return path;
585
+ const { uid, gid } = ids();
586
+ await madeAtomically(path, (temp) => input.host.convert({ imageId: input.imageId, dest: temp, uid: Number(uid), gid: Number(gid) }));
587
+ return path;
588
+ }
589
+ async function createBuildDisk(input) {
590
+ const template = join(diskTemplatesDir(input.runnerDir), `${input.bytes}.ext4`);
591
+ if (!existsSync(template)) {
592
+ const { uid, gid } = ids();
593
+ await madeAtomically(template, (temp) => input.host.makeTemplate({ bytes: input.bytes, dest: temp, uid: Number(uid), gid: Number(gid) }));
594
+ }
595
+ mkdirSync(dirname(input.dest), { recursive: true });
596
+ if (input.host.copyTemplate) input.host.copyTemplate(template, input.dest);
597
+ else {
598
+ const copied = spawnSync("cp", ["--sparse=always", template, input.dest], { encoding: "utf8" });
599
+ if (copied.status !== 0) throw new Error(`copying the empty build disk failed: ${firstLine2(copied.stderr)}`);
600
+ }
601
+ chmodSync(input.dest, RUNNER_FILE_MODE);
602
+ truncateSync(input.dest, input.bytes);
603
+ }
604
+ function deleteBuildDisk(path) {
605
+ rmSync(path, { force: true });
606
+ }
607
+ var MIB = 1024 ** 2;
608
+ var INIT = "/opt/outerlayer/bin/outerlayer-guest";
609
+ function machineConfig(limits2, paths) {
610
+ const bootArgs = [
611
+ "console=ttyS0",
612
+ "reboot=k",
613
+ "panic=1",
614
+ "pci=off",
615
+ "i8042.noaux",
616
+ "i8042.nomux",
617
+ "i8042.nopnp",
618
+ "i8042.dumbkbd",
619
+ "ro",
620
+ "root=/dev/vda",
621
+ "rootfstype=ext4",
622
+ `init=${INIT}`,
623
+ ...paths.relayPorts.length === 0 ? [] : [`outerlayer.relay=${paths.relayPorts.join(",")}`],
624
+ ...paths.docker === true ? ["outerlayer.docker=1"] : []
625
+ ];
626
+ return {
627
+ "boot-source": { kernel_image_path: paths.kernel, boot_args: bootArgs.join(" ") },
628
+ drives: [
629
+ { drive_id: "rootfs", path_on_host: paths.rootfs, is_root_device: true, is_read_only: true },
630
+ { drive_id: "build", path_on_host: paths.buildDisk, is_root_device: false, is_read_only: false }
631
+ ],
632
+ "machine-config": { vcpu_count: limits2.cpus, mem_size_mib: Math.max(1, Math.floor(limits2.memoryBytes / MIB)), smt: false },
633
+ vsock: { guest_cid: GUEST_CID, uds_path: paths.vsock }
634
+ };
635
+ }
636
+ function firecrackerCommand(input) {
637
+ return { command: input.binary, args: ["--no-api", "--config-file", input.configPath] };
638
+ }
639
+ var STOP_WAIT_MS = 1e4;
640
+ var POLL_MS = 20;
641
+ function isZombie(pid) {
642
+ try {
643
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
644
+ return stat.slice(stat.lastIndexOf(")") + 2, stat.lastIndexOf(")") + 3) === "Z";
645
+ } catch {
646
+ return false;
647
+ }
648
+ }
649
+ function createFirecrackerHost(options = {}) {
650
+ const launch = options.launch ?? firecrackerCommand;
651
+ const exits = /* @__PURE__ */ new Map();
652
+ const alive = (vm) => isSameProcess(vm.pid, vm.start) && !isZombie(vm.pid);
653
+ return {
654
+ async spawn(input) {
655
+ const { command, args } = launch(input);
656
+ mkdirSync(dirname(input.consolePath), { recursive: true });
657
+ const consoleFd = openSync(input.consolePath, "a", 384);
658
+ try {
659
+ const child = spawn(command, args, { detached: true, stdio: ["ignore", consoleFd, consoleFd] });
660
+ const failed = new Promise((_resolve, reject) => child.once("error", reject));
661
+ const pid = child.pid;
662
+ if (pid === void 0) return await failed;
663
+ exits.set(pid, new Promise((resolve) => child.once("exit", () => resolve())));
664
+ child.unref();
665
+ const start = startTimeOf(pid);
666
+ if (start === null) throw new Error(`${command} exited as soon as it started; its console is at ${input.consolePath}`);
667
+ return { pid, start };
668
+ } finally {
669
+ closeSync(consoleFd);
670
+ }
671
+ },
672
+ async stop(vm) {
673
+ if (!alive(vm)) return;
674
+ try {
675
+ process.kill(vm.pid, "SIGKILL");
676
+ } catch {
677
+ }
678
+ const exited = exits.get(vm.pid);
679
+ const deadline = Date.now() + STOP_WAIT_MS;
680
+ if (exited !== void 0) await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, STOP_WAIT_MS))]);
681
+ while (alive(vm) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, POLL_MS));
682
+ exits.delete(vm.pid);
683
+ },
684
+ alive
685
+ };
686
+ }
687
+
688
+ // src/runner/microvm/host.ts
689
+ function createMicroVmPorts(over = {}) {
690
+ return {
691
+ assets: over.assets ?? ((runnerDir) => ensureAssets({ runnerDir })),
692
+ rootfs: over.rootfs ?? createRootfsHost(),
693
+ firecracker: over.firecracker ?? createFirecrackerHost(),
694
+ exec: over.exec ?? execCapture
695
+ };
696
+ }
697
+
698
+ // src/runner/build-steps.ts
699
+ var aborted = () => ({ ok: false, reason: "", aborted: true });
700
+ function firstLine3(text) {
701
+ return text.trim().split("\n")[0] ?? "";
702
+ }
703
+ function adoptionFailureReason(result) {
704
+ const line = result.stdout.trim().split("\n").at(-1) ?? "";
705
+ try {
706
+ const answer = JSON.parse(line);
707
+ if (typeof answer.code === "string" && typeof answer.message === "string") return `${answer.code}: ${answer.message}`;
708
+ } catch {
709
+ }
710
+ return `context_adoption_failed: adopting the control plane's context failed (exit ${result.status})${firstLine3(result.stderr) === "" ? "" : `: ${firstLine3(result.stderr)}`}`;
711
+ }
712
+ async function runBuildSteps(input) {
713
+ const { exec, log, tag, repository } = input;
714
+ const rewrite = await exec(["git", "config", "--global", `url.http://127.0.0.1:${RELAY_PORT}/github.com/.insteadOf`, "https://github.com/"], "/build/work");
715
+ if (rewrite.status !== 0) return { ok: false, reason: `configuring the build's git remote failed (exit ${rewrite.status})` };
716
+ const clone = await exec(["git", "clone", "--filter=blob:none", "--single-branch", `https://github.com/${repository}.git`, CHECKOUT_DIR], "/build/work");
717
+ if (clone.status !== 0) return { ok: false, reason: `the clone of ${repository} failed (exit ${clone.status})` };
718
+ try {
719
+ const prepared = await prepareBranch({
720
+ workdir: CHECKOUT_DIR,
721
+ branch: input.branch,
722
+ git: async (cwd, args) => {
723
+ const ran = await exec(["git", ...args], cwd, { quiet: true });
724
+ return { status: ran.status, stdout: ran.stdout.trim(), stderr: ran.stderr.trim() };
725
+ }
726
+ });
727
+ log.line(`${tag} on ${prepared.branch} at ${prepared.head.slice(0, 8)}`);
728
+ } catch (err) {
729
+ if (err instanceof BranchPrepareError) return { ok: false, reason: err.message };
730
+ throw err;
731
+ }
732
+ for (const step of lifecycleSteps(input.lifecycle)) {
733
+ if (input.aborted()) return aborted();
734
+ log.line(`${tag} lifecycle ${step.name} (${step.source})`);
735
+ const ran = await exec(step.argv, CHECKOUT_DIR);
736
+ if (ran.status !== 0) return { ok: false, reason: lifecycleFailureReason(step, ran.status) };
737
+ }
738
+ if (input.aborted()) return aborted();
739
+ const staged = await input.stageContext();
740
+ if (staged.kind === "refused") return { ok: false, reason: staged.reason };
741
+ if (staged.kind === "staged") {
742
+ try {
743
+ const delivered = await input.deliverContext?.(staged) ?? { ok: true };
744
+ if (!delivered.ok) return { ok: false, reason: delivered.reason };
745
+ const adopted = await exec(["outerlayer", "context", "adopt", `${JOB_MOUNT}/context`, "--json"], CHECKOUT_DIR);
746
+ if (adopted.status !== 0) return { ok: false, reason: adoptionFailureReason(adopted) };
747
+ log.line(`${tag} governed context ${staged.sha.slice(0, 7)} adopted (${staged.files.length} files)`);
748
+ } finally {
749
+ input.removeStagedContext();
750
+ }
751
+ if (input.aborted()) return aborted();
752
+ }
753
+ return { ok: true };
754
+ }
755
+
756
+ // src/runner/container/agent-user.ts
757
+ var ADDED_USER = "ol-agent";
758
+ var AgentUserError = class extends Error {
759
+ };
760
+ async function resolveAgentUser(input) {
761
+ const named = input.remoteUser !== void 0 && input.remoteUser !== "" ? input.remoteUser : input.imageUser;
762
+ const candidate = (named ?? "").split(":")[0];
763
+ if (candidate === "" || candidate === "root") return ADDED_USER;
764
+ if (/^\d+$/.test(candidate)) return Number(candidate) === 0 ? ADDED_USER : candidate;
765
+ const uid = await input.probe(candidate);
766
+ if (uid === null) throw new AgentUserError(`the user "${candidate}" is not in the image, so no command can run as it`);
767
+ return uid === 0 ? ADDED_USER : candidate;
768
+ }
769
+ var unavailable = (why) => ({ kind: "refused", reason: `context_unavailable: ${why}` });
770
+ function makeReadable(tree, dir, files) {
771
+ chmodSync(dir, 493);
772
+ chmodSync(tree, 493);
773
+ const directories = /* @__PURE__ */ new Set();
774
+ for (const rel of files) {
775
+ for (let current = dirname(join(tree, rel)); current !== tree; current = dirname(current)) directories.add(current);
776
+ chmodSync(join(tree, rel), (lstatSync(join(tree, rel)).mode & 73) !== 0 ? 493 : 420);
777
+ }
778
+ for (const directory of directories) chmodSync(directory, 493);
779
+ }
780
+ async function stageGovernedContext(input) {
781
+ const git2 = input.git ?? defaultGitRunner;
782
+ const dir = stagedContextDir(input.buildDir);
783
+ const tree = join(dir, "tree");
784
+ const finish = (outcome) => {
785
+ removeStagedContext(input.buildDir);
786
+ return outcome;
787
+ };
788
+ try {
789
+ mkdirSync(tree, { recursive: true });
790
+ for (const args of [
791
+ ["init", "-q", "--initial-branch=main", tree],
792
+ ["-C", tree, "remote", "add", "origin", `https://github.com/${input.repository}.git`]
793
+ ]) {
794
+ const done = git2(args, { cwd: dir, timeoutMs: 1e4 });
795
+ if (!done.ok) return finish(unavailable(`the staging directory could not be prepared: ${done.stderr.trim().split("\n")[0] ?? ""}`));
796
+ }
797
+ const { url, apiKey, appId, configPath } = input.config;
798
+ const result = await materializeContext({
799
+ cwd: tree,
800
+ sessionId: `build-${input.buildId}`,
801
+ home: contextHomeDir(configPath),
802
+ cloud: { url, apiKey, appId },
803
+ fetchImpl: signingFetchFor(input.fetchImpl, configPath, apiKey),
804
+ git: git2,
805
+ now: input.now,
806
+ resolveControlPlaneUrl: (repository) => input.controlPlaneUrl(repository)
807
+ });
808
+ switch (result.status) {
809
+ case "materialized": {
810
+ if (result.unfetched !== void 0 && result.unfetched.length > 0) {
811
+ return finish(unavailable(`the host's git login cannot read ${result.unfetched.join(" or ")}, a control plane named for ${input.repository}, so it cannot be ruled out as a second claimant`));
812
+ }
813
+ const manifest = readContextManifest(join(tree, ".git"));
814
+ const source = readSourceCache(join(tree, ".git"));
815
+ if (result.sha === null || manifest === void 0 || source?.status !== "governed" || source.repository === void 0 || source.ref === void 0) {
816
+ return finish(unavailable(`the context of ${input.repository} was materialized without a record of it`));
817
+ }
818
+ writeFileSync(
819
+ join(dir, "staged.json"),
820
+ JSON.stringify({ schemaVersion: 1, sha: result.sha, repository: source.repository, ref: source.ref, files: manifest.files }) + "\n"
821
+ );
822
+ makeReadable(tree, dir, manifest.files);
823
+ chmodSync(join(dir, "staged.json"), 420);
824
+ return { kind: "staged", dir, sha: result.sha, files: manifest.files };
825
+ }
826
+ case "self":
827
+ if (result.unfetched !== void 0 && result.unfetched.length > 0) {
828
+ return finish(unavailable(`the host's git login cannot read ${result.unfetched.join(" or ")}, the control plane named for ${input.repository}`));
829
+ }
830
+ if (result.designation === "answered") return finish({ kind: "none" });
831
+ if (result.authRejected === true) {
832
+ return finish(unavailable(`the gateway rejected the runner's API key (401/403) when asked which repository governs ${input.repository}`));
833
+ }
834
+ return finish(unavailable(`the gateway did not say which repository governs ${input.repository}`));
835
+ case "conflict":
836
+ return finish({ kind: "refused", reason: `context_conflict: ${result.message ?? "two control planes claim this repository"}` });
837
+ default:
838
+ return finish(unavailable(result.message ?? `the context of ${input.repository} could not be materialized (${result.status})`));
839
+ }
840
+ } catch (err) {
841
+ return finish(unavailable(`the context of ${input.repository} could not be staged: ${err instanceof Error ? err.message : String(err)}`));
842
+ }
843
+ }
844
+ function removeStagedContext(buildDir2) {
845
+ rmSync(stagedContextDir(buildDir2), { recursive: true, force: true });
846
+ }
847
+
848
+ // src/runner/container/run-args.ts
849
+ var BUILD_LABEL = "ai.outerlayer.build";
850
+ function hardened(input) {
851
+ const { memory, cpus, pids } = input.limits;
852
+ return [
853
+ "--label",
854
+ `${BUILD_LABEL}=${input.buildId}`,
855
+ "--network",
856
+ "none",
857
+ "--cap-drop",
858
+ "ALL",
859
+ "--security-opt",
860
+ "no-new-privileges",
861
+ "--read-only",
862
+ "--user",
863
+ input.user,
864
+ // The same figure for swap as for memory leaves the container none.
865
+ "--memory",
866
+ memory,
867
+ "--memory-swap",
868
+ memory,
869
+ "--cpus",
870
+ String(cpus),
871
+ "--pids-limit",
872
+ String(pids)
873
+ ];
874
+ }
875
+ function mountPath(path) {
876
+ if (/[",]/.test(path)) throw new Error(`${path} holds a comma or a quote, which a Docker mount cannot carry; move the runner's config file`);
877
+ return path;
878
+ }
879
+ function containerRunArgs(input) {
880
+ return [
881
+ "--detach",
882
+ "--name",
883
+ input.container,
884
+ ...hardened(input),
885
+ "--mount",
886
+ `type=volume,src=${input.volume},dst=/build`,
887
+ // `/tmp` is a directory of the same volume, so it is measured and limited with the rest of the build.
888
+ "--mount",
889
+ `type=volume,src=${input.volume},dst=/tmp,volume-subpath=tmp`,
890
+ "--mount",
891
+ `type=bind,src=${mountPath(input.buildDir)},dst=${JOB_MOUNT},readonly`,
892
+ "--entrypoint",
893
+ "/opt/outerlayer/bin/tini",
894
+ input.image,
895
+ "--",
896
+ "/opt/outerlayer/bin/node",
897
+ "/opt/outerlayer/cli/dist/index.js",
898
+ "broker-relay",
899
+ "--port",
900
+ String(RELAY_PORT),
901
+ "--socket",
902
+ `${JOB_MOUNT}/tunnel.sock`,
903
+ ...Array.from({ length: input.destinations ?? 0 }, (_unused, index) => [
904
+ "--forward",
905
+ `${DESTINATION_BASE_PORT + index}=${JOB_MOUNT}/dest-${index}.sock`
906
+ ]).flat(),
907
+ ...[
908
+ "--forward",
909
+ `${GATEWAY_PORT}=${JOB_MOUNT}/${GATEWAY_SOCKET}`,
910
+ "--forward",
911
+ `${CLAUDE_PORT}=${JOB_MOUNT}/${CLAUDE_SOCKET}`
912
+ ]
913
+ ];
914
+ }
915
+ function prepareRunArgs(input) {
916
+ return [
917
+ "--rm",
918
+ ...hardened(input),
919
+ "--mount",
920
+ `type=volume,src=${input.volume},dst=/build`,
921
+ "--entrypoint",
922
+ "mkdir",
923
+ input.image,
924
+ "-p",
925
+ "-m",
926
+ "1777",
927
+ "/build/tmp",
928
+ "/build/home",
929
+ "/build/work"
930
+ ];
931
+ }
932
+ function containerExecArgs(input) {
933
+ const literals = Object.keys(input.env.literals).sort().flatMap((name) => ["-e", `${name}=${input.env.literals[name]}`]);
934
+ const passed = Object.keys(input.env.passed).filter((name) => input.env.passed[name] !== void 0).sort().flatMap((name) => ["-e", name]);
935
+ return [
936
+ "-u",
937
+ input.user,
938
+ ...input.workdir === void 0 ? [] : ["-w", input.workdir],
939
+ ...literals,
940
+ ...passed,
941
+ input.container,
942
+ ...input.argv
943
+ ];
944
+ }
945
+
946
+ // src/runner/container/docker.ts
947
+ function createContainerHost(deps = {}) {
948
+ const run = deps.run ?? spawnRun;
949
+ return {
950
+ async version() {
951
+ const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
952
+ return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
953
+ },
954
+ async operatingSystem() {
955
+ const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
956
+ return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
957
+ },
958
+ async imageUser(imageId) {
959
+ const result = await run("docker", ["image", "inspect", "--format", "{{.Config.User}}", imageId]);
960
+ return result.status === 0 ? result.stdout.trim() : "";
961
+ },
962
+ createVolume(name, labels) {
963
+ const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
964
+ return run("docker", ["volume", "create", ...flags, name]);
965
+ },
966
+ removeVolume(name) {
967
+ return run("docker", ["volume", "rm", "--force", name]);
968
+ },
969
+ run(args, opts) {
970
+ return run("docker", ["run", ...args], opts);
971
+ },
972
+ exec(input, opts) {
973
+ const env = {};
974
+ for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
975
+ return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env });
976
+ },
977
+ stop(container) {
978
+ return run("docker", ["stop", container]);
979
+ },
980
+ start(container) {
981
+ return run("docker", ["start", container]);
982
+ },
983
+ remove(container) {
984
+ return run("docker", ["rm", "--force", "--volumes", container]);
985
+ }
986
+ };
987
+ }
988
+ var FILE_MODE = 420;
989
+ var DIR_MODE = 493;
990
+ function replaceFile(path, text) {
991
+ const partial = `${path}.${process.pid}.partial`;
992
+ writeFileSync(partial, text);
993
+ chmodSync(partial, FILE_MODE);
994
+ renameSync(partial, path);
995
+ }
996
+ function ghHostsYml(token) {
997
+ return [
998
+ "github.com:",
999
+ " users:",
1000
+ " x-access-token:",
1001
+ ` oauth_token: ${token}`,
1002
+ ` oauth_token: ${token}`,
1003
+ " user: x-access-token",
1004
+ " git_protocol: https",
1005
+ ""
1006
+ ].join("\n");
1007
+ }
1008
+ var GH_CONFIG_YML = 'version: "1"\n';
1009
+ function writeGhConfig(dir, token) {
1010
+ mkdirSync(dir, { recursive: true, mode: DIR_MODE });
1011
+ chmodSync(dir, DIR_MODE);
1012
+ replaceFile(join(dir, "hosts.yml"), ghHostsYml(token));
1013
+ replaceFile(join(dir, "config.yml"), GH_CONFIG_YML);
1014
+ }
1015
+ function gitEnv(extra) {
1016
+ const env = {};
1017
+ for (const [name, value] of Object.entries(process.env)) if (!name.startsWith("GIT_")) env[name] = value;
1018
+ return { ...env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", ...extra };
1019
+ }
1020
+ async function git(args, cwd, env, input) {
1021
+ const timeoutMs = 10 * 6e4;
1022
+ const maxOutputBytes = 1024 * 1024;
1023
+ const child = spawn("git", args, { cwd, env, stdio: [input === void 0 ? "ignore" : "pipe", "pipe", "ignore"], timeout: timeoutMs });
1024
+ const out = [];
1025
+ let size = 0;
1026
+ child.stdout.on("data", (chunk) => {
1027
+ if (size < maxOutputBytes) out.push(chunk);
1028
+ size += chunk.length;
1029
+ });
1030
+ const closed = new Promise((resolve, reject) => {
1031
+ child.once("error", reject);
1032
+ child.once("close", (status) => resolve(status));
1033
+ });
1034
+ if (input !== void 0) {
1035
+ await pipeline(input, child.stdin).catch(() => void 0);
1036
+ }
1037
+ return { status: await closed, stdout: Buffer.concat(out) };
1038
+ }
1039
+ async function isFastForward(input) {
1040
+ const ref = `refs/heads/${input.branch}`;
1041
+ mkdirSync(input.mirrorDir, { recursive: true });
1042
+ const init = await git(["init", "--bare", "--quiet"], input.mirrorDir, gitEnv({}));
1043
+ if (init.status !== 0) throw new Error("git could not prepare the mirror");
1044
+ const credential = `Authorization: Basic ${Buffer.from(`x-access-token:${input.token}`).toString("base64")}`;
1045
+ const remoteEnv = gitEnv({ GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: `http.${input.remoteUrl}.extraHeader`, GIT_CONFIG_VALUE_0: credential });
1046
+ const fetched = await git(["fetch", "--quiet", "--no-tags", "--force", input.remoteUrl, `+${ref}:${ref}`], input.mirrorDir, remoteEnv);
1047
+ if (fetched.status !== 0) {
1048
+ const listed = await git(["ls-remote", "--exit-code", input.remoteUrl, ref], input.mirrorDir, remoteEnv);
1049
+ if (listed.status === 2) return false;
1050
+ throw new Error(`git could not fetch ${input.branch} from GitHub`);
1051
+ }
1052
+ const body = createReadStream(input.bodyPath);
1053
+ let decoded = body;
1054
+ if (input.gzip) {
1055
+ decoded = body.pipe(createGunzip());
1056
+ body.on("error", (err) => decoded.destroy(err));
1057
+ }
1058
+ const received = await git(
1059
+ ["-c", "receive.denyNonFastForwards=true", "-c", "receive.denyDeletes=true", "receive-pack", "--stateless-rpc", input.mirrorDir],
1060
+ input.mirrorDir,
1061
+ gitEnv({}),
1062
+ decoded
1063
+ );
1064
+ return received.stdout.includes(`ok ${ref}
1065
+ `, 0, "utf8");
1066
+ }
1067
+ function limits() {
1068
+ return { sectionBytes: 64 * 1024, compressedWindow: 16 * 1024, inflatedBytes: 1024 * 1024 };
1069
+ }
1070
+ var TOO_LONG = "the push commands are longer than the runner reads";
1071
+ var UNREADABLE = "the push commands are not in a form the runner reads";
1072
+ var NOT_GZIP = "the push body is not valid gzip";
1073
+ var COMMAND = /^([0-9a-f]{40}|[0-9a-f]{64}) ([0-9a-f]{40}|[0-9a-f]{64}) ([^\u0000- \u007f]+)$/;
1074
+ function refused(reason) {
1075
+ return { kind: "refused", reason };
1076
+ }
1077
+ function decode(prefix, gzip) {
1078
+ if (!gzip) return prefix;
1079
+ try {
1080
+ const { compressedWindow, inflatedBytes } = limits();
1081
+ return gunzipSync(prefix.subarray(0, compressedWindow), { finishFlush: constants.Z_SYNC_FLUSH, maxOutputLength: inflatedBytes });
1082
+ } catch (err) {
1083
+ const code = err.code;
1084
+ if (code === "ERR_BUFFER_TOO_LARGE") return refused(TOO_LONG);
1085
+ return refused(NOT_GZIP);
1086
+ }
1087
+ }
1088
+ function decodeText(bytes) {
1089
+ try {
1090
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1091
+ } catch {
1092
+ return void 0;
1093
+ }
1094
+ }
1095
+ function parse(data) {
1096
+ const commands = [];
1097
+ let capabilities = [];
1098
+ let offset = 0;
1099
+ for (; ; ) {
1100
+ if (data.length - offset < 4) return { kind: "need-more" };
1101
+ const header = data.toString("latin1", offset, offset + 4);
1102
+ if (!/^[0-9a-f]{4}$/i.test(header)) return refused(UNREADABLE);
1103
+ const length = Number.parseInt(header, 16);
1104
+ if (length === 0) return { kind: "commands", commands, capabilities };
1105
+ if (data.length - offset < length) return { kind: "need-more" };
1106
+ const line = data.subarray(offset + 4, offset + length);
1107
+ offset += length;
1108
+ if (offset > limits().sectionBytes) return refused(TOO_LONG);
1109
+ const first = commands.length === 0;
1110
+ const nul = line.indexOf(0);
1111
+ const decoded = decodeText(first && nul !== -1 ? line.subarray(0, nul) : line);
1112
+ if (decoded === void 0) return refused(UNREADABLE);
1113
+ const text = decoded.replace(/\n$/, "");
1114
+ if (first && text.startsWith("shallow ")) continue;
1115
+ const match = COMMAND.exec(text);
1116
+ if (match === null) return refused(UNREADABLE);
1117
+ if (first && nul !== -1) capabilities = line.subarray(nul + 1).toString("latin1").split(/\s+/).filter((word) => word !== "");
1118
+ commands.push({ oldId: match[1], newId: match[2], ref: match[3] });
1119
+ }
1120
+ }
1121
+ function readCommandSection(prefix, gzip) {
1122
+ const decoded = decode(prefix, gzip);
1123
+ if (!Buffer.isBuffer(decoded)) return decoded;
1124
+ const section = parse(decoded);
1125
+ if (section.kind === "need-more" && gzip && prefix.length >= limits().compressedWindow) return refused(TOO_LONG);
1126
+ return section;
1127
+ }
1128
+ function pkt(payload) {
1129
+ const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
1130
+ return Buffer.concat([Buffer.from((body.length + 4).toString(16).padStart(4, "0")), body]);
1131
+ }
1132
+ var FLUSH = Buffer.from("0000");
1133
+ function banded(channel, data, max) {
1134
+ const packets = [];
1135
+ for (let at = 0; at < data.length; at += max) {
1136
+ packets.push(pkt(Buffer.concat([Buffer.from([channel]), data.subarray(at, at + max)])));
1137
+ }
1138
+ return packets;
1139
+ }
1140
+ function encodeRefusal(commands, capabilities, message) {
1141
+ const wantsReport = capabilities.includes("report-status") || capabilities.includes("report-status-v2");
1142
+ const bigBand = capabilities.includes("side-band-64k");
1143
+ const sideBand = bigBand || capabilities.includes("side-band");
1144
+ const report = wantsReport ? Buffer.concat([pkt("unpack ok\n"), ...commands.map((c) => pkt(`ng ${c.ref} ${message}
1145
+ `)), FLUSH]) : Buffer.alloc(0);
1146
+ if (!sideBand) return wantsReport ? report : FLUSH;
1147
+ const max = bigBand ? 65515 : 995;
1148
+ return Buffer.concat([...banded(2, Buffer.from(`${message}
1149
+ `), max), ...banded(1, report, max), FLUSH]);
1150
+ }
1151
+
1152
+ // src/runner/container/push-rule.ts
1153
+ var OWNED_PREFIX = "outerlayer/";
1154
+ function isZero(id) {
1155
+ return /^0+$/.test(id);
1156
+ }
1157
+ function needsFastForwardCheck(commands, branch) {
1158
+ if (branch.startsWith(OWNED_PREFIX) || commands.length !== 1) return false;
1159
+ const [only] = commands;
1160
+ return only.ref === `refs/heads/${branch}` && !isZero(only.oldId) && !isZero(only.newId);
1161
+ }
1162
+ function judgePush(input) {
1163
+ const { commands, branch } = input;
1164
+ if (commands.some((c) => c.ref !== `refs/heads/${branch}`)) {
1165
+ return { allowed: false, message: `this build may push only to ${branch}` };
1166
+ }
1167
+ if (branch.startsWith(OWNED_PREFIX)) return { allowed: true };
1168
+ const creates = commands.every((c) => isZero(c.oldId) && !isZero(c.newId));
1169
+ const forwards = commands.length === 1 && !isZero(commands[0].newId) && input.fastForward === true;
1170
+ if (creates || forwards) return { allowed: true };
1171
+ return { allowed: false, message: `${branch} is not an outerlayer/ branch, so this build may only fast-forward it` };
1172
+ }
1173
+
1174
+ // src/runner/container/git-remote.ts
1175
+ var SEGMENT = /^[A-Za-z0-9_.-]+$/;
1176
+ var WORKFLOW_REFUSAL = /refusing to allow a GitHub App to create or update workflow .* without `workflows` permission/;
1177
+ function classifyRequest(method, rawUrl) {
1178
+ const [path = "", ...queryParts] = rawUrl.split("?");
1179
+ const search = queryParts.join("?");
1180
+ const segments = path.split("/").slice(1);
1181
+ if (segments[0] !== "github.com" || segments.length < 4) return void 0;
1182
+ const [, owner, repo, ...rest] = segments;
1183
+ const name = repo.replace(/\.git$/, "");
1184
+ if (![owner, name, ...rest].every((segment) => SEGMENT.test(segment) && segment !== "." && segment !== "..")) return void 0;
1185
+ const where = { owner, name };
1186
+ const suffix = rest.join("/");
1187
+ if (suffix === "info/refs") {
1188
+ if (method !== "GET") return void 0;
1189
+ const service = new URLSearchParams(search).get("service");
1190
+ if (service !== "git-upload-pack" && service !== "git-receive-pack") return void 0;
1191
+ return { ...where, kind: "info-refs", service, search: `service=${service}` };
1192
+ }
1193
+ if (method !== "POST") return void 0;
1194
+ if (suffix === "git-upload-pack") return { ...where, kind: "upload-pack", search: "" };
1195
+ if (suffix === "git-receive-pack") return { ...where, kind: "receive-pack", search: "" };
1196
+ if (suffix === "info/lfs/objects/batch") return { ...where, kind: "lfs-batch", search: "" };
1197
+ return void 0;
1198
+ }
1199
+ function isOwnRepository(own, owner, name) {
1200
+ return `${owner}/${name}`.toLowerCase() === own.replace(/\.git$/, "").toLowerCase();
1201
+ }
1202
+ function basicAuthorization(token) {
1203
+ return `Basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`;
1204
+ }
1205
+ function upstreamHeaders(incoming, authorization) {
1206
+ const headers = {};
1207
+ for (const name of ["content-type", "content-encoding", "accept", "git-protocol", "user-agent"]) {
1208
+ const value = incoming[name];
1209
+ if (typeof value === "string") headers[name] = value;
1210
+ }
1211
+ if (authorization !== void 0) headers.authorization = authorization;
1212
+ return headers;
1213
+ }
1214
+ function isWorkflowRefusal(text) {
1215
+ return WORKFLOW_REFUSAL.test(text);
1216
+ }
1217
+ function plain(res, status, text) {
1218
+ res.writeHead(status, { "content-type": "text/plain", "content-length": Buffer.byteLength(text), connection: "close" });
1219
+ res.end(text);
1220
+ }
1221
+ async function drain(req) {
1222
+ req.resume();
1223
+ try {
1224
+ await finished(req);
1225
+ } catch {
1226
+ }
1227
+ }
1228
+ function readPrefix(req, gzip) {
1229
+ return new Promise((resolve, reject) => {
1230
+ const held = [];
1231
+ const finish = (section) => {
1232
+ req.off("data", onData);
1233
+ req.off("end", onEnd);
1234
+ req.off("error", reject);
1235
+ req.pause();
1236
+ resolve({ held, section });
1237
+ };
1238
+ const onData = (chunk) => {
1239
+ held.push(chunk);
1240
+ const section = readCommandSection(Buffer.concat(held), gzip);
1241
+ if (section.kind !== "need-more") finish(section);
1242
+ };
1243
+ const onEnd = () => finish({ kind: "need-more" });
1244
+ req.on("data", onData);
1245
+ req.once("end", onEnd);
1246
+ req.once("error", reject);
1247
+ });
1248
+ }
1249
+ function readBody(req, limit) {
1250
+ return new Promise((resolve, reject) => {
1251
+ const chunks = [];
1252
+ let size = 0;
1253
+ req.on("data", (chunk) => {
1254
+ size += chunk.length;
1255
+ if (size > limit) {
1256
+ req.pause();
1257
+ resolve(void 0);
1258
+ return;
1259
+ }
1260
+ chunks.push(chunk);
1261
+ });
1262
+ req.once("end", () => resolve(Buffer.concat(chunks)));
1263
+ req.once("error", reject);
1264
+ });
1265
+ }
1266
+ function createGitRemote(options) {
1267
+ const gitUrl = options.gitUrl ?? "https://github.com";
1268
+ const maxLfsBodyBytes = 1024 * 1024;
1269
+ const hopByHop = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-authenticate", "proxy-authorization", "te", "trailer", "set-cookie"]);
1270
+ const log = (line) => options.log?.(line);
1271
+ function forward(input) {
1272
+ const { route, res } = input;
1273
+ const url = new URL(`${gitUrl}/${route.owner}/${route.name}.git/${route.kind === "info-refs" ? "info/refs" : route.kind === "lfs-batch" ? "info/lfs/objects/batch" : `git-${route.kind}`}${route.search === "" ? "" : `?${route.search}`}`);
1274
+ const send = url.protocol === "https:" ? request$1 : request;
1275
+ return new Promise((resolve) => {
1276
+ const upstream = send(url, { method: input.method, headers: { ...input.headers, connection: "close" } }, (answer) => {
1277
+ const headers = {};
1278
+ for (const [name, value] of Object.entries(answer.headers)) {
1279
+ if (!hopByHop.has(name)) headers[name] = value;
1280
+ }
1281
+ res.writeHead(answer.statusCode ?? 502, headers);
1282
+ let tail = "";
1283
+ let told = false;
1284
+ answer.on("data", (chunk) => {
1285
+ if (input.watchForWorkflowRefusal === true && !told) {
1286
+ const window = tail + chunk.toString("latin1");
1287
+ if (isWorkflowRefusal(window)) {
1288
+ told = true;
1289
+ options.onWorkflowRefused?.();
1290
+ }
1291
+ tail = window.slice(-256);
1292
+ }
1293
+ res.write(chunk);
1294
+ });
1295
+ answer.once("end", () => {
1296
+ res.end();
1297
+ resolve();
1298
+ });
1299
+ answer.once("error", () => {
1300
+ res.destroy();
1301
+ resolve();
1302
+ });
1303
+ });
1304
+ upstream.on("error", (err) => {
1305
+ log(`could not reach GitHub for ${route.kind}: ${err.message}`);
1306
+ if (res.headersSent) res.destroy();
1307
+ else plain(res, 502, "the runner could not reach GitHub\n");
1308
+ resolve();
1309
+ });
1310
+ res.once("close", () => upstream.destroy());
1311
+ input.feed(upstream);
1312
+ });
1313
+ }
1314
+ async function tokenOrRefuse(res, kind) {
1315
+ try {
1316
+ return await options.tokens.token(kind);
1317
+ } catch (err) {
1318
+ log(`could not get a ${kind} token: ${refusalCode(err)}`);
1319
+ plain(res, 502, `the runner could not get a ${kind} token: ${refusalCode(err)}
1320
+ `);
1321
+ return void 0;
1322
+ }
1323
+ }
1324
+ async function push(req, res, route) {
1325
+ const encoding = req.headers["content-encoding"];
1326
+ const gzip = encoding === "gzip";
1327
+ const { held, section } = await readPrefix(req, gzip);
1328
+ const refuse = async (reason) => {
1329
+ log(`push refused: ${reason}`);
1330
+ await drain(req);
1331
+ plain(res, 400, `the runner refused this push: ${reason}
1332
+ `);
1333
+ };
1334
+ if (encoding !== void 0 && encoding !== "gzip" && encoding !== "identity") {
1335
+ return refuse("the push body is encoded in a way the runner does not read");
1336
+ }
1337
+ if (section.kind === "need-more") return refuse("the request ended before its commands did");
1338
+ if (section.kind === "refused") return refuse(section.reason);
1339
+ const { commands, capabilities } = section;
1340
+ const refuseInBand = async (message) => {
1341
+ log(`push refused: ${commands.map((c) => c.ref).join(", ")} \u2014 ${message}`);
1342
+ await drain(req);
1343
+ res.writeHead(200, { "content-type": "application/x-git-receive-pack-result", "cache-control": "no-cache" });
1344
+ res.end(encodeRefusal(commands, capabilities, message));
1345
+ };
1346
+ let bodyPath;
1347
+ try {
1348
+ let fastForward;
1349
+ if (needsFastForwardCheck(commands, options.branch)) {
1350
+ const read = await tokenOrRefuse(res, "read");
1351
+ if (read === void 0) return void await drain(req);
1352
+ mkdirSync(options.spoolDir, { recursive: true, mode: 448 });
1353
+ bodyPath = join(options.spoolDir, randomBytes(8).toString("hex"));
1354
+ const file = createWriteStream(bodyPath);
1355
+ const cap = options.maxPushBytes ?? 2 * 1024 ** 3;
1356
+ let size = 0;
1357
+ for (const chunk of held) {
1358
+ size += chunk.length;
1359
+ file.write(chunk);
1360
+ }
1361
+ const capped = new Transform({
1362
+ transform(chunk, _encoding, done) {
1363
+ size += chunk.length;
1364
+ done(null, size > cap ? void 0 : chunk);
1365
+ }
1366
+ });
1367
+ await pipeline(req, capped, file);
1368
+ if (size > cap) return void await refuseInBand(`the push is larger than the ${cap} bytes the runner holds to check it`);
1369
+ try {
1370
+ fastForward = await isFastForward({
1371
+ mirrorDir: options.mirrorDir,
1372
+ remoteUrl: `${gitUrl}/${route.owner}/${route.name}.git`,
1373
+ token: read,
1374
+ branch: options.branch,
1375
+ bodyPath,
1376
+ gzip
1377
+ });
1378
+ } catch (err) {
1379
+ log(`fast-forward check could not run: ${err instanceof Error ? err.message : String(err)}`);
1380
+ return void await refuseInBand(`the runner could not check whether this push fast-forwards ${options.branch}`);
1381
+ }
1382
+ }
1383
+ const verdict = judgePush({ commands, branch: options.branch, fastForward });
1384
+ if (!verdict.allowed) return void await refuseInBand(verdict.message);
1385
+ const token = await tokenOrRefuse(res, "push");
1386
+ if (token === void 0) return void await drain(req);
1387
+ options.tokens.markUsed("push");
1388
+ const headers = upstreamHeaders(req.headers, basicAuthorization(token));
1389
+ const spooled = bodyPath;
1390
+ if (spooled !== void 0) headers["content-length"] = String(statSync(spooled).size);
1391
+ else if (typeof req.headers["content-length"] === "string") headers["content-length"] = req.headers["content-length"];
1392
+ await forward({
1393
+ route,
1394
+ method: "POST",
1395
+ headers,
1396
+ res,
1397
+ watchForWorkflowRefusal: true,
1398
+ feed: (upstream) => {
1399
+ if (spooled !== void 0) {
1400
+ createReadStream(spooled).pipe(upstream);
1401
+ return;
1402
+ }
1403
+ for (const chunk of held) upstream.write(chunk);
1404
+ req.pipe(upstream);
1405
+ }
1406
+ });
1407
+ } finally {
1408
+ if (bodyPath !== void 0) rmSync(bodyPath, { force: true });
1409
+ }
1410
+ }
1411
+ async function handle(req, res) {
1412
+ const route = classifyRequest(req.method ?? "", req.url ?? "");
1413
+ if (route === void 0) {
1414
+ req.resume();
1415
+ return plain(res, 404, "not found\n");
1416
+ }
1417
+ const own = isOwnRepository(options.repository, route.owner, route.name);
1418
+ if (own && route.kind === "receive-pack") return push(req, res, route);
1419
+ let kind = route.kind === "info-refs" && route.service === "git-receive-pack" ? "push" : "read";
1420
+ let body;
1421
+ if (route.kind === "lfs-batch") {
1422
+ body = await readBody(req, maxLfsBodyBytes);
1423
+ if (body === void 0) return plain(res, 413, "the request is too large\n");
1424
+ kind = isLfsUpload(body) ? "push" : "read";
1425
+ }
1426
+ let authorization;
1427
+ if (own) {
1428
+ const token = await tokenOrRefuse(res, kind);
1429
+ if (token === void 0) return void await drain(req);
1430
+ authorization = basicAuthorization(token);
1431
+ if (kind === "read" || route.kind === "lfs-batch") options.tokens.markUsed(kind);
1432
+ }
1433
+ const headers = upstreamHeaders(req.headers, authorization);
1434
+ if (body !== void 0) headers["content-length"] = String(body.length);
1435
+ else if (typeof req.headers["content-length"] === "string") headers["content-length"] = req.headers["content-length"];
1436
+ await forward({
1437
+ route,
1438
+ method: req.method ?? "GET",
1439
+ headers,
1440
+ res,
1441
+ feed: (upstream) => {
1442
+ if (body !== void 0) upstream.end(body);
1443
+ else req.pipe(upstream);
1444
+ }
1445
+ });
1446
+ }
1447
+ return async (req, res) => {
1448
+ try {
1449
+ await handle(req, res);
1450
+ } catch (err) {
1451
+ log(`could not serve a request: ${err instanceof Error ? err.message : String(err)}`);
1452
+ if (res.headersSent) res.destroy();
1453
+ else plain(res, 502, "the runner could not serve this request\n");
1454
+ }
1455
+ };
1456
+ }
1457
+ function isLfsUpload(body) {
1458
+ try {
1459
+ return JSON.parse(body.toString("utf8"))?.operation === "upload";
1460
+ } catch {
1461
+ return false;
1462
+ }
1463
+ }
1464
+
1465
+ // src/runner/container/ranges.ts
1466
+ var V4 = [
1467
+ ["unspecified", "0.0.0.0/8"],
1468
+ ["loopback", "127.0.0.0/8"],
1469
+ ["private", "10.0.0.0/8"],
1470
+ ["private", "172.16.0.0/12"],
1471
+ ["private", "192.168.0.0/16"],
1472
+ ["link-local", "169.254.0.0/16"],
1473
+ ["shared-address", "100.64.0.0/10"],
1474
+ ["multicast", "224.0.0.0/4"],
1475
+ ["reserved", "240.0.0.0/4"]
1476
+ ];
1477
+ var V6 = [
1478
+ ["unspecified", "::/128"],
1479
+ ["loopback", "::1/128"],
1480
+ ["ipv4-mapped", "::ffff:0:0/96"],
1481
+ ["nat64", "64:ff9b::/96"],
1482
+ ["ipv4-compatible", "::/96"],
1483
+ ["link-local", "fe80::/10"],
1484
+ ["unique-local", "fc00::/7"],
1485
+ ["multicast", "ff00::/8"]
1486
+ ];
1487
+ var UNPARSEABLE = { name: "unparseable", cidr: "-" };
1488
+ function parseV4(text) {
1489
+ const parts = text.split(".");
1490
+ if (parts.length !== 4) return void 0;
1491
+ let value = 0;
1492
+ for (const part of parts) {
1493
+ if (!/^\d{1,3}$/.test(part)) return void 0;
1494
+ const octet = Number(part);
1495
+ if (octet > 255) return void 0;
1496
+ value = value * 256 + octet;
1497
+ }
1498
+ return value;
1499
+ }
1500
+ function parseV6(input) {
1501
+ const zone = input.indexOf("%");
1502
+ let text = zone === -1 ? input : input.slice(0, zone);
1503
+ const lastColon = text.lastIndexOf(":");
1504
+ const tail = text.slice(lastColon + 1);
1505
+ if (tail.includes(".")) {
1506
+ const v4 = parseV4(tail);
1507
+ if (v4 === void 0) return void 0;
1508
+ text = `${text.slice(0, lastColon + 1)}${(v4 >>> 16).toString(16)}:${(v4 & 65535).toString(16)}`;
1509
+ }
1510
+ const halves = text.split("::");
1511
+ if (halves.length > 2) return void 0;
1512
+ const groups = (part) => {
1513
+ if (part === "") return [];
1514
+ const found = part.split(":");
1515
+ return found.every((g) => /^[0-9a-fA-F]{1,4}$/.test(g)) ? found : void 0;
1516
+ };
1517
+ const head = groups(halves[0]);
1518
+ const rest = groups(halves[1] ?? "");
1519
+ if (head === void 0 || rest === void 0) return void 0;
1520
+ let words;
1521
+ if (halves.length === 2) {
1522
+ const missing = 8 - head.length - rest.length;
1523
+ if (missing < 1) return void 0;
1524
+ words = [...head, ...Array(missing).fill("0"), ...rest];
1525
+ } else {
1526
+ if (head.length !== 8) return void 0;
1527
+ words = head;
1528
+ }
1529
+ return words.reduce((acc, word) => acc << BigInt(16) | BigInt(parseInt(word, 16)), BigInt(0));
1530
+ }
1531
+ function build4() {
1532
+ return V4.map(([name, cidr]) => {
1533
+ const [address, length] = cidr.split("/");
1534
+ return { name, cidr, base: parseV4(address), bits: Number(length) };
1535
+ });
1536
+ }
1537
+ function build6() {
1538
+ return V6.map(([name, cidr]) => {
1539
+ const [address, length] = cidr.split("/");
1540
+ return { name, cidr, base: parseV6(address), bits: Number(length) };
1541
+ });
1542
+ }
1543
+ var RANGES_4 = build4();
1544
+ var RANGES_6 = build6();
1545
+ [...RANGES_4, ...RANGES_6].map(({ name, cidr }) => ({ name, cidr }));
1546
+ function classifyAddress(address) {
1547
+ const v4 = parseV4(address);
1548
+ if (v4 !== void 0) {
1549
+ for (const range of RANGES_4) {
1550
+ const shift = 32 - range.bits;
1551
+ if (Math.floor(v4 / 2 ** shift) === Math.floor(range.base / 2 ** shift)) return { name: range.name, cidr: range.cidr };
1552
+ }
1553
+ return void 0;
1554
+ }
1555
+ const v6 = parseV6(address);
1556
+ if (v6 === void 0) return UNPARSEABLE;
1557
+ for (const range of RANGES_6) {
1558
+ const shift = BigInt(128 - range.bits);
1559
+ if (v6 >> shift === range.base >> shift) return { name: range.name, cidr: range.cidr };
1560
+ }
1561
+ return void 0;
1562
+ }
1563
+
1564
+ // src/runner/container/tunnel.ts
1565
+ function isLiteralAddress(text) {
1566
+ return isIP(text) !== 0;
1567
+ }
1568
+ var MAX_SOCKET_PATH_BYTES = 100;
1569
+ var MAX_KEPT_HOSTS = 1e3;
1570
+ var MAX_KEPT_REFUSALS = 200;
1571
+ var MAX_COUNTED_EXTRA_HOSTS = 1e5;
1572
+ var CONNECT_TIMEOUT_MS = 3e4;
1573
+ var NOT_ALLOWED_RANGE = "not-allowed";
1574
+ var NOT_RESOLVED = "not resolved";
1575
+ var IDLE_TIMEOUT_MS = 10 * 6e4;
1576
+ async function systemResolve(host) {
1577
+ const found = await lookup(host, { all: true, verbatim: true });
1578
+ return found.map((a) => ({ address: a.address }));
1579
+ }
1580
+ function systemHostAddresses() {
1581
+ return Object.values(networkInterfaces()).flatMap((list) => (list ?? []).map((i) => i.address));
1582
+ }
1583
+ function normalize(address) {
1584
+ const zone = address.indexOf("%");
1585
+ return (zone === -1 ? address : address.slice(0, zone)).toLowerCase();
1586
+ }
1587
+ function parseTarget(target) {
1588
+ const match = /^\[([^\]]+)\]:(\d+)$/.exec(target) ?? /^([^:\[\]]+):(\d+)$/.exec(target);
1589
+ if (match === null) return void 0;
1590
+ const port = Number(match[2]);
1591
+ return port >= 1 && port <= 65535 ? { host: match[1], port } : void 0;
1592
+ }
1593
+ async function startTunnel(socketPath, opts = {}) {
1594
+ if (Buffer.byteLength(socketPath) > MAX_SOCKET_PATH_BYTES) {
1595
+ throw new Error(`${socketPath} is too long for a Unix socket; keep the runner's config file nearer the root of the filesystem`);
1596
+ }
1597
+ const resolve = opts.resolve ?? systemResolve;
1598
+ const openConnection = opts.connect ?? ((t) => connect({ host: t.address, port: t.port }));
1599
+ const ownAddresses = opts.hostAddresses ?? systemHostAddresses;
1600
+ const connectTimeoutMs = opts.connectTimeoutMs ?? CONNECT_TIMEOUT_MS;
1601
+ const idleTimeoutMs = opts.idleTimeoutMs ?? IDLE_TIMEOUT_MS;
1602
+ const hosts = [];
1603
+ const seen = /* @__PURE__ */ new Set();
1604
+ const extraHosts = /* @__PURE__ */ new Set();
1605
+ let hostsMore = 0;
1606
+ const refusals = [];
1607
+ const refusalKeys = /* @__PURE__ */ new Set();
1608
+ const extraRefusalKeys = /* @__PURE__ */ new Set();
1609
+ let refusalsMore = 0;
1610
+ const destinations = [];
1611
+ if (opts.recordPath !== void 0 && existsSync(opts.recordPath)) {
1612
+ try {
1613
+ const saved = JSON.parse(readFileSync(opts.recordPath, "utf8"));
1614
+ for (const host of saved.hosts ?? []) {
1615
+ hosts.push(host);
1616
+ seen.add(host);
1617
+ }
1618
+ hostsMore = saved.hostsMore ?? 0;
1619
+ for (const name of saved.destinations ?? []) destinations.push(name);
1620
+ refusalsMore = saved.refusalsMore ?? 0;
1621
+ for (const refusal of saved.refusals ?? []) {
1622
+ refusals.push(refusal);
1623
+ refusalKeys.add(`${refusal.host}|${refusal.address}|${refusal.range}`);
1624
+ }
1625
+ } catch {
1626
+ }
1627
+ }
1628
+ const record = () => ({
1629
+ hosts: [...hosts],
1630
+ hostsMore,
1631
+ refusalsMore,
1632
+ refusals: refusals.map((r) => ({ ...r })),
1633
+ destinations: [...destinations]
1634
+ });
1635
+ const persist = () => {
1636
+ if (opts.recordPath === void 0) return;
1637
+ try {
1638
+ writeFileSync(opts.recordPath, JSON.stringify(record()), { mode: RUNNER_FILE_MODE });
1639
+ } catch {
1640
+ }
1641
+ };
1642
+ const destinationUsed = (name) => {
1643
+ if (destinations.includes(name)) return;
1644
+ destinations.push(name);
1645
+ persist();
1646
+ };
1647
+ const reached = (host) => {
1648
+ const name = host.toLowerCase();
1649
+ if (seen.has(name)) return;
1650
+ if (hosts.length >= MAX_KEPT_HOSTS) {
1651
+ if (extraHosts.has(name)) return;
1652
+ if (extraHosts.size < MAX_COUNTED_EXTRA_HOSTS) extraHosts.add(name);
1653
+ hostsMore++;
1654
+ } else {
1655
+ hosts.push(name);
1656
+ seen.add(name);
1657
+ }
1658
+ persist();
1659
+ };
1660
+ const refused2 = (host, address, range) => {
1661
+ const key = `${host}|${address}|${range}`;
1662
+ if (refusalKeys.has(key)) return;
1663
+ if (refusals.length >= MAX_KEPT_REFUSALS) {
1664
+ if (extraRefusalKeys.has(key)) return;
1665
+ if (extraRefusalKeys.size < MAX_COUNTED_EXTRA_HOSTS) extraRefusalKeys.add(key);
1666
+ refusalsMore++;
1667
+ } else {
1668
+ refusalKeys.add(key);
1669
+ refusals.push({ host, address, range });
1670
+ }
1671
+ persist();
1672
+ };
1673
+ async function decide(host) {
1674
+ if (opts.allowHosts !== void 0 && !hostAllowed(host, opts.allowHosts)) {
1675
+ refused2(host, NOT_RESOLVED, NOT_ALLOWED_RANGE);
1676
+ return { ok: false, status: 403, body: `refused: ${host} is not in runner.build.allowHosts
1677
+ ` };
1678
+ }
1679
+ let addresses;
1680
+ try {
1681
+ addresses = await resolve(host);
1682
+ } catch {
1683
+ return { ok: false, status: 502, body: `could not resolve ${host}
1684
+ ` };
1685
+ }
1686
+ if (addresses.length === 0) return { ok: false, status: 502, body: `${host} has no address
1687
+ ` };
1688
+ const own = new Set(ownAddresses().map(normalize));
1689
+ let firstRefusal;
1690
+ for (const { address: address2 } of addresses) {
1691
+ const range = own.has(normalize(address2)) ? { name: "host-address", cidr: address2 } : classifyAddress(address2);
1692
+ if (range === void 0) return { ok: true, address: address2 };
1693
+ firstRefusal ??= { address: address2, name: range.name, cidr: range.cidr };
1694
+ }
1695
+ const { address, name, cidr } = firstRefusal;
1696
+ refused2(host, address, name);
1697
+ return { ok: false, status: 403, body: `refused: ${host} is ${address}, in the ${name} range ${cidr}
1698
+ ` };
1699
+ }
1700
+ const sockets = /* @__PURE__ */ new Set();
1701
+ const server = createServer((req, res) => {
1702
+ void proxyPlain(req, res);
1703
+ });
1704
+ server.on("connection", (socket) => {
1705
+ sockets.add(socket);
1706
+ socket.on("close", () => sockets.delete(socket));
1707
+ });
1708
+ server.on("connect", (req, client, head) => {
1709
+ void tunnelConnect(req, client, head);
1710
+ });
1711
+ const statusLine = (status) => `HTTP/1.1 ${status} ${status === 403 ? "Forbidden" : "Bad Gateway"}`;
1712
+ const reply = (client, status, body) => {
1713
+ client.end(`${statusLine(status)}\r
1714
+ Content-Type: text/plain\r
1715
+ Content-Length: ${Buffer.byteLength(body)}\r
1716
+ Connection: close\r
1717
+ \r
1718
+ ${body}`);
1719
+ };
1720
+ async function tunnelConnect(req, client, head) {
1721
+ client.on("error", () => client.destroy());
1722
+ const target = parseTarget(req.url ?? "");
1723
+ if (target === void 0) {
1724
+ client.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
1725
+ return;
1726
+ }
1727
+ const decision = await decide(target.host);
1728
+ if (!decision.ok) {
1729
+ reply(client, decision.status, decision.body);
1730
+ return;
1731
+ }
1732
+ const upstream = openConnection({ address: decision.address, port: target.port });
1733
+ upstream.setTimeout(connectTimeoutMs);
1734
+ const failed = () => {
1735
+ upstream.destroy();
1736
+ reply(client, 502, `could not connect to ${target.host}
1737
+ `);
1738
+ };
1739
+ upstream.once("error", failed);
1740
+ upstream.once("timeout", failed);
1741
+ client.on("close", () => upstream.destroy());
1742
+ const established = () => {
1743
+ upstream.removeListener("error", failed);
1744
+ upstream.removeListener("timeout", failed);
1745
+ upstream.setTimeout(idleTimeoutMs, () => upstream.destroy());
1746
+ upstream.on("error", () => void 0);
1747
+ upstream.on("close", () => client.destroy());
1748
+ reached(target.host);
1749
+ client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
1750
+ upstream.write(head);
1751
+ upstream.pipe(client);
1752
+ client.pipe(upstream);
1753
+ };
1754
+ if (upstream.readyState === "open") established();
1755
+ else upstream.once("connect", established);
1756
+ }
1757
+ async function proxyPlain(req, res) {
1758
+ if (opts.gitRemote !== void 0 && (req.url ?? "").startsWith("/")) {
1759
+ await opts.gitRemote(req, res);
1760
+ return;
1761
+ }
1762
+ let url;
1763
+ try {
1764
+ url = new URL(req.url ?? "");
1765
+ } catch {
1766
+ res.writeHead(400, { "Content-Type": "text/plain", Connection: "close" });
1767
+ res.end("this is a proxy: send an absolute http:// URL or CONNECT\n");
1768
+ return;
1769
+ }
1770
+ if (url.protocol !== "http:") {
1771
+ res.writeHead(400, { "Content-Type": "text/plain", Connection: "close" });
1772
+ res.end("only http:// URLs are proxied; use CONNECT for https\n");
1773
+ return;
1774
+ }
1775
+ const host = url.hostname.replace(/^\[|\]$/g, "");
1776
+ const port = url.port === "" ? 80 : Number(url.port);
1777
+ const decision = await decide(host);
1778
+ if (!decision.ok) {
1779
+ res.writeHead(decision.status, { "Content-Type": "text/plain", Connection: "close" });
1780
+ res.end(decision.body);
1781
+ return;
1782
+ }
1783
+ const headers = { ...req.headers, host: url.host, connection: "close" };
1784
+ delete headers["proxy-connection"];
1785
+ delete headers["proxy-authorization"];
1786
+ const upstream = request(
1787
+ {
1788
+ method: req.method,
1789
+ path: `${url.pathname}${url.search}`,
1790
+ headers,
1791
+ // With no agent named, `createConnection` is what opens the socket, so the address the tunnel chose is the one connected to.
1792
+ createConnection: () => openConnection({ address: decision.address, port })
1793
+ },
1794
+ (answer) => {
1795
+ reached(host);
1796
+ res.writeHead(answer.statusCode ?? 502, answer.headers);
1797
+ answer.pipe(res);
1798
+ }
1799
+ );
1800
+ upstream.setTimeout(connectTimeoutMs, () => upstream.destroy());
1801
+ res.on("close", () => upstream.destroy());
1802
+ upstream.on("error", () => {
1803
+ if (!res.headersSent) {
1804
+ res.writeHead(502, { "Content-Type": "text/plain", Connection: "close" });
1805
+ res.end(`could not reach ${host}
1806
+ `);
1807
+ } else {
1808
+ res.destroy();
1809
+ }
1810
+ });
1811
+ req.pipe(upstream);
1812
+ }
1813
+ if (existsSync(socketPath)) unlinkSync(socketPath);
1814
+ await new Promise((resolveListen, reject) => {
1815
+ server.once("error", reject);
1816
+ server.listen(socketPath, () => resolveListen());
1817
+ });
1818
+ chmodSync(socketPath, 438);
1819
+ const tcpServers = [];
1820
+ return {
1821
+ socketPath,
1822
+ record,
1823
+ destinationUsed,
1824
+ async listenTcp(address) {
1825
+ const tcp = createServer$1((socket) => server.emit("connection", socket));
1826
+ await new Promise((resolveListen, reject) => {
1827
+ tcp.once("error", reject);
1828
+ tcp.listen(address.port, address.host, () => resolveListen());
1829
+ });
1830
+ tcpServers.push(tcp);
1831
+ return { port: tcp.address().port };
1832
+ },
1833
+ close: () => new Promise((resolveClose) => {
1834
+ for (const socket of sockets) socket.destroy();
1835
+ for (const tcp of tcpServers) tcp.close();
1836
+ server.close(() => resolveClose());
1837
+ })
1838
+ };
1839
+ }
1840
+
1841
+ // src/runner/container/hooks.ts
1842
+ var MIN_ENGINE_MAJOR = 26;
1843
+ var RELAY_ATTEMPTS = 20;
1844
+ var RELAY_WAIT_MS = 500;
1845
+ var BUSY_IMAGE_WAIT_MS = 5e3;
1846
+ var VOLUME_REMOVE_ATTEMPTS = 4;
1847
+ var WORKFLOW_PERMISSION_REASON = "workflow_permission_required";
1848
+ function createContainerRuntime(opts = {}) {
1849
+ const { cli, remoteFor } = opts;
1850
+ return {
1851
+ host: opts.host ?? createContainerHost(),
1852
+ tunnels: opts.tunnels ?? /* @__PURE__ */ new Map(),
1853
+ buildImage: opts.buildImage ?? ((build) => buildRepositoryImage(
1854
+ {
1855
+ repository: build.repository,
1856
+ remote: remoteFor?.(build.repository),
1857
+ configPath: build.configPath,
1858
+ cli: cli ?? currentCliBuild(),
1859
+ readToken: build.readToken,
1860
+ guestHelper: build.guestHelper
1861
+ },
1862
+ { host: opts.imageHost ?? createDockerHost() }
1863
+ )),
1864
+ startTunnel: opts.startTunnel ?? startTunnel,
1865
+ tunnelOptions: opts.tunnelOptions,
1866
+ sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
1867
+ hostEnv: opts.hostEnv ?? process.env,
1868
+ credentials: opts.credentials ?? /* @__PURE__ */ new Map(),
1869
+ github: { gitUrl: opts.github?.gitUrl ?? "https://github.com", apiUrl: opts.github?.apiUrl ?? "https://api.github.com" },
1870
+ remoteFor,
1871
+ readHead: opts.readHead ?? readDefaultBranchHead,
1872
+ clone: opts.clone ?? ((input) => cloneRepository(input.remote, input.dest)),
1873
+ destinationOptions: opts.destinationOptions,
1874
+ claudeUpstream: opts.claudeUpstream,
1875
+ vm: createMicroVmPorts(opts.vm),
1876
+ cliBuild: () => cli ?? currentCliBuild()
1877
+ };
1878
+ }
1879
+ function keepGhConfigCurrent(tokens, buildDir2, onError) {
1880
+ tokens.onRead((token) => {
1881
+ try {
1882
+ writeGhConfig(ghConfigDir(buildDir2), token);
1883
+ } catch (err) {
1884
+ onError(err);
1885
+ }
1886
+ });
1887
+ }
1888
+ function gitRemoteFor(runtime, input) {
1889
+ const [owner = "", name = ""] = input.repository.split("/");
1890
+ const runnerDirPath = runnerDirOfBuild(input.buildDir);
1891
+ return createGitRemote({
1892
+ repository: input.repository,
1893
+ branch: input.branch,
1894
+ tokens: input.tokens,
1895
+ gitUrl: runtime.github.gitUrl,
1896
+ mirrorDir: mirrorDir(runnerDirPath, owner, name),
1897
+ spoolDir: spoolDir(runnerDirPath),
1898
+ onWorkflowRefused: () => {
1899
+ input.tokens.reason = WORKFLOW_PERMISSION_REASON;
1900
+ },
1901
+ log: input.log
1902
+ });
1903
+ }
1904
+ function firstLine4(text) {
1905
+ return text.trim().split("\n")[0] ?? "";
1906
+ }
1907
+ var NO_ENV = { literals: {}, passed: {} };
1908
+ var RELAY_PROBE = `const s=require("net").connect(${RELAY_PORT},"127.0.0.1");s.on("connect",()=>process.exit(0));s.on("error",()=>process.exit(1));setTimeout(()=>process.exit(1),2000)`;
1909
+ async function waitForRelay(runtime, state) {
1910
+ for (let attempt = 0; attempt < RELAY_ATTEMPTS; attempt++) {
1911
+ const probe = await runtime.host.exec({
1912
+ container: state.container,
1913
+ user: ADDED_USER,
1914
+ env: NO_ENV,
1915
+ argv: ["/opt/outerlayer/bin/node", "-e", RELAY_PROBE]
1916
+ });
1917
+ if (probe.status === 0) return true;
1918
+ await runtime.sleep(RELAY_WAIT_MS);
1919
+ }
1920
+ return false;
1921
+ }
1922
+ async function obtainImage(input) {
1923
+ const { config, runtime, tokens, repository, tag, log } = input;
1924
+ for (; ; ) {
1925
+ if (input.aborted()) return { ok: false, reason: "ended before provision finished", aborted: true };
1926
+ let readToken;
1927
+ try {
1928
+ readToken = await tokens.token("read");
1929
+ } catch (err) {
1930
+ return {
1931
+ ok: false,
1932
+ reason: err instanceof WorkCommandError ? `the build's read token could not be had: ${refusalCode(err)}` : `the build's read token could not be had: ${describeCallFailure(err)}`
1933
+ };
1934
+ }
1935
+ try {
1936
+ return { ok: true, built: await runtime.buildImage({ repository, configPath: config.configPath, readToken, guestHelper: input.guestHelper }) };
1937
+ } catch (err) {
1938
+ if (err instanceof RecipeError && err.code === "build_in_progress" && (input.now?.() ?? /* @__PURE__ */ new Date()).getTime() < input.deadlineMs) {
1939
+ log.line(`${tag} waiting for another build of ${repository}'s image`);
1940
+ await runtime.sleep(BUSY_IMAGE_WAIT_MS);
1941
+ continue;
1942
+ }
1943
+ if (err instanceof RecipeError) {
1944
+ log.error(`${tag} image build for ${repository} failed: ${err.code}: ${err.message}`);
1945
+ return { ok: false, reason: err.code };
1946
+ }
1947
+ return { ok: false, reason: `the image build failed: ${describeCallFailure(err)}` };
1948
+ }
1949
+ }
1950
+ }
1951
+ async function provisionContainer(input) {
1952
+ const { config, runtime, item, itemKey, jobDir, log, tokens } = input;
1953
+ const { host } = runtime;
1954
+ const tag = `#${item.number}`;
1955
+ const logPath = join(jobDir, "provision.log");
1956
+ const fail = (reason) => ({ ok: false, reason });
1957
+ if (claudeCredential(runtime.hostEnv) === void 0) return fail(AGENT_CREDENTIAL_MISSING);
1958
+ const repository = item.specRepository;
1959
+ if (repository === void 0 || repository === null || repository === "") {
1960
+ return fail("the work item names no repository to clone");
1961
+ }
1962
+ const engine = await host.version();
1963
+ if (engine === null) return fail("Docker is not available: `docker version` failed");
1964
+ if (!(Number.parseInt(engine, 10) >= MIN_ENGINE_MAJOR)) {
1965
+ return fail(`Docker Engine ${engine} is too old: container builds need Engine ${MIN_ENGINE_MAJOR} or later`);
1966
+ }
1967
+ if (/docker desktop/i.test(await host.operatingSystem() ?? "")) {
1968
+ return fail("Docker Desktop cannot pass the tunnel's socket into a container, so container builds need Docker Engine: install it (on Windows, inside WSL2) or use builtin:process");
1969
+ }
1970
+ const obtained = await obtainImage({ config, runtime, tokens, repository, tag, log, aborted: input.aborted, deadlineMs: input.deadlineMs, now: input.now });
1971
+ if (!obtained.ok) return obtained.aborted === true ? { ok: false, reason: obtained.reason, aborted: true } : fail(obtained.reason);
1972
+ const built = obtained.built;
1973
+ const { report } = built;
1974
+ const buildId = randomBytes(6).toString("hex");
1975
+ const name = `outerlayer-build-${buildId}`;
1976
+ const buildDir2 = buildDir(config.configPath, buildId);
1977
+ const state = {
1978
+ buildId,
1979
+ container: name,
1980
+ volume: name,
1981
+ buildDir: buildDir2,
1982
+ repository,
1983
+ imageId: report.imageId,
1984
+ containerEnv: report.apply.containerEnv,
1985
+ builtIns: true
1986
+ };
1987
+ const env = buildEnvironment({
1988
+ config,
1989
+ item: item.number,
1990
+ itemKey,
1991
+ repository,
1992
+ hostEnv: runtime.hostEnv,
1993
+ containerEnv: report.apply.containerEnv,
1994
+ builtIns: true
1995
+ });
1996
+ if (env.skippedTokenVariables.length > 0) {
1997
+ log.line(`${tag} not passing ${env.skippedTokenVariables.join(", ")} from runner.build.variables into the build: its repository token comes from the runner`);
1998
+ }
1999
+ const environment = {
2000
+ isolation: "container",
2001
+ recipe: { sourceCommit: report.sourceCommit, key: report.recipeKey, path: report.recipe },
2002
+ imageId: report.imageId,
2003
+ ignoredProperties: report.ignored.map(({ property, source }) => ({ property, source })),
2004
+ variables: env.variables,
2005
+ cliVersion: input.cliVersion
2006
+ };
2007
+ input.save(state, environment);
2008
+ const bail = async (reason, aborted2 = false) => {
2009
+ try {
2010
+ await cleanupContainer(runtime, state);
2011
+ } catch (err) {
2012
+ log.error(`${tag} cleanup failed: ${describeCallFailure(err)}`);
2013
+ }
2014
+ return aborted2 ? { ok: false, reason: "ended before provision finished", aborted: true } : fail(reason);
2015
+ };
2016
+ const dockerFailed = (what, result) => bail(`${what} failed (exit ${result.status})${firstLine4(result.stderr) === "" ? "" : `: ${firstLine4(result.stderr)}`}`);
2017
+ if (input.aborted()) return bail("", true);
2018
+ mkdirSync(buildDir2, { recursive: true });
2019
+ chmodSync(buildDir2, 493);
2020
+ const ghDir = ghConfigDir(buildDir2);
2021
+ try {
2022
+ writeGhConfig(ghDir, await tokens.token("read"));
2023
+ } catch (err) {
2024
+ return bail(
2025
+ err instanceof WorkCommandError ? `the build's read token could not be had: ${refusalCode(err)}` : `the build's gh configuration could not be written: ${describeCallFailure(err)}`
2026
+ );
2027
+ }
2028
+ keepGhConfigCurrent(tokens, buildDir2, (err) => log.error(`${tag} the read token could not be written for gh: ${describeCallFailure(err)}`));
2029
+ tokens.markUsed("read");
2030
+ environment.credentials = tokens.used();
2031
+ try {
2032
+ const gitRemote = gitRemoteFor(runtime, { repository, branch: input.branch, tokens, buildDir: buildDir2, log: (line) => log.line(`${tag} ${line}`) });
2033
+ runtime.tunnels.set(buildId, await startServicesFor(runtime, config, buildDir2, jobDir, itemKey, true, gitRemote));
2034
+ } catch (err) {
2035
+ return bail(`the tunnel could not start: ${describeCallFailure(err)}`);
2036
+ }
2037
+ const limits2 = { memory: config.build?.memory ?? "8g", cpus: config.build?.cpus ?? 4, pids: config.build?.pids ?? 4096 };
2038
+ const volume = await host.createVolume(name, { [BUILD_LABEL]: buildId });
2039
+ if (volume.status !== 0) return dockerFailed("creating the build's volume", volume);
2040
+ if (input.aborted()) return bail("", true);
2041
+ const runInput = {
2042
+ buildId,
2043
+ container: name,
2044
+ volume: name,
2045
+ buildDir: buildDir2,
2046
+ image: report.imageId,
2047
+ user: ADDED_USER,
2048
+ limits: limits2,
2049
+ destinations: config.build?.destinations?.length ?? 0};
2050
+ const prepared = await host.run(prepareRunArgs(runInput), { logPath });
2051
+ if (prepared.status !== 0) return dockerFailed("preparing the build's volume", prepared);
2052
+ const started = await host.run(containerRunArgs(runInput), { logPath });
2053
+ if (started.status !== 0) return dockerFailed("starting the build's container", started);
2054
+ if (input.aborted()) return bail("", true);
2055
+ if (!await waitForRelay(runtime, state)) return bail("the relay in the build's container did not start");
2056
+ const imageUser = await host.imageUser(report.imageId);
2057
+ let agentUser;
2058
+ try {
2059
+ agentUser = await resolveAgentUser({
2060
+ remoteUser: report.apply.remoteUser,
2061
+ imageUser,
2062
+ probe: async (user) => {
2063
+ const probe = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: ["id", "-u", user] });
2064
+ const uid = Number(probe.stdout);
2065
+ return probe.status === 0 && Number.isInteger(uid) ? uid : null;
2066
+ }
2067
+ });
2068
+ } catch (err) {
2069
+ if (err instanceof AgentUserError) return bail(err.message);
2070
+ throw err;
2071
+ }
2072
+ state.agentUser = agentUser;
2073
+ const tools = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: ["cat", "/opt/outerlayer/tools.json"] });
2074
+ environment.agentVersion = agentVersionOf(tools.status === 0 ? tools.stdout : "");
2075
+ input.save(state, environment);
2076
+ const exec = (argv, workdir, opts) => host.exec({ container: name, user: agentUser, workdir, env, argv }, opts?.quiet === true ? void 0 : { logPath });
2077
+ const steps = await runBuildSteps({
2078
+ repository,
2079
+ branch: input.branch,
2080
+ lifecycle: report.apply.lifecycle,
2081
+ exec,
2082
+ log,
2083
+ tag,
2084
+ aborted: input.aborted,
2085
+ // The control plane is read on the host, with the host's git login. The container reads the staged files from its mount of the build's directory.
2086
+ stageContext: () => stageGovernedContext({
2087
+ config,
2088
+ repository,
2089
+ buildId,
2090
+ buildDir: buildDir2,
2091
+ fetchImpl: input.fetchImpl,
2092
+ controlPlaneUrl: (controlPlane) => runtime.remoteFor?.(controlPlane) ?? `${runtime.github.gitUrl}/${controlPlane}.git`
2093
+ }),
2094
+ removeStagedContext: () => removeStagedContext(buildDir2)
2095
+ });
2096
+ if (!steps.ok) return bail(steps.reason, steps.aborted === true);
2097
+ return { ok: true, workdir: CHECKOUT_DIR, state, environment };
2098
+ }
2099
+ function agentVersionOf(toolsJson) {
2100
+ try {
2101
+ const version = JSON.parse(toolsJson).claudeCodeVersion;
2102
+ if (typeof version === "string" && version !== "") return version;
2103
+ } catch {
2104
+ }
2105
+ return CLAUDE_CODE_VERSION;
2106
+ }
2107
+ function shellQuote(word) {
2108
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
2109
+ }
2110
+ function containerCommandLine(input) {
2111
+ const state = input.job.container;
2112
+ const env = buildEnvironment({
2113
+ config: input.config,
2114
+ item: input.job.item,
2115
+ itemKey: input.job.itemKey,
2116
+ branch: input.job.branch,
2117
+ repository: state.repository,
2118
+ hostEnv: input.runtime.hostEnv,
2119
+ containerEnv: state.containerEnv ?? {},
2120
+ builtIns: state.builtIns === true,
2121
+ identity: input.job.commitIdentity
2122
+ });
2123
+ const args = containerExecArgs({
2124
+ container: state.container,
2125
+ user: state.agentUser ?? ADDED_USER,
2126
+ workdir: CHECKOUT_DIR,
2127
+ env,
2128
+ argv: ["/bin/sh", "-c", input.cmdline]
2129
+ });
2130
+ return {
2131
+ line: ["docker", "exec", ...args].map(shellQuote).join(" "),
2132
+ env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed }
2133
+ };
2134
+ }
2135
+ async function stopContainer(runtime, state) {
2136
+ await runtime.host.stop(state.container);
2137
+ state.stopped = true;
2138
+ }
2139
+ async function measureDisk(runtime, state) {
2140
+ const result = await runtime.host.exec({
2141
+ container: state.container,
2142
+ user: state.agentUser ?? ADDED_USER,
2143
+ env: NO_ENV,
2144
+ argv: ["du", "-skx", "/build"]
2145
+ });
2146
+ return result.status === 0 ? parseDuKilobytes(result.stdout) : void 0;
2147
+ }
2148
+ async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, builtIns, gitRemote) {
2149
+ return startBuildServices({
2150
+ startTunnel: runtime.startTunnel,
2151
+ tunnelOptions: { ...runtime.tunnelOptions, ...gitRemote === void 0 ? {} : { gitRemote } },
2152
+ buildDir: buildDir2,
2153
+ jobDir,
2154
+ destinations: config.build?.destinations ?? [],
2155
+ allowHosts: config.build?.allowHosts,
2156
+ serve: "socket",
2157
+ ...builtIns ? { builtIns: { gatewayUrl: config.url, itemKey, claudeUpstream: runtime.claudeUpstream } } : {},
2158
+ hostEnv: runtime.hostEnv,
2159
+ destinationOptions: runtime.destinationOptions
2160
+ });
2161
+ }
2162
+ async function ensureTunnel(runtime, state, jobDir, config, itemKey, branch) {
2163
+ if (runtime.tunnels.has(state.buildId)) return;
2164
+ const tokens = runtime.credentials.get(jobDir);
2165
+ const gitRemote = tokens === void 0 || state.repository === void 0 || branch === void 0 ? void 0 : gitRemoteFor(runtime, { repository: state.repository, branch, tokens, buildDir: state.buildDir });
2166
+ runtime.tunnels.set(state.buildId, await startServicesFor(runtime, config, state.buildDir, jobDir, itemKey, state.builtIns === true, gitRemote));
2167
+ }
2168
+ async function syncInContainer(input) {
2169
+ const { runtime, job } = input;
2170
+ const state = job.container;
2171
+ try {
2172
+ await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch);
2173
+ if (state.stopped) {
2174
+ await runtime.host.start(state.container);
2175
+ state.stopped = false;
2176
+ if (!await waitForRelay(runtime, state)) return { ok: false, reason: "the relay did not start again" };
2177
+ }
2178
+ const env = buildEnvironment({
2179
+ config: input.config,
2180
+ item: job.item,
2181
+ itemKey: job.itemKey,
2182
+ branch: job.branch,
2183
+ repository: state.repository,
2184
+ hostEnv: runtime.hostEnv,
2185
+ containerEnv: state.containerEnv ?? {},
2186
+ builtIns: state.builtIns === true,
2187
+ identity: job.commitIdentity
2188
+ });
2189
+ const result = await runtime.host.exec(
2190
+ { container: state.container, user: state.agentUser ?? ADDED_USER, workdir: job.workdir ?? CHECKOUT_DIR, env, argv: ["outerlayer", "sync"] },
2191
+ { logPath: join(input.jobDir, "sync.log") }
2192
+ );
2193
+ return result.status === 0 ? { ok: true } : { ok: false, reason: `exit ${result.status}` };
2194
+ } catch (err) {
2195
+ return { ok: false, reason: describeCallFailure(err) };
2196
+ }
2197
+ }
2198
+ var HOOK_ERRORS_IN_CONTAINER = "/build/home/.outerlayer/spool/hook-errors.log";
2199
+ async function copyHookErrors(runtime, state, jobDir) {
2200
+ try {
2201
+ if (state.stopped) {
2202
+ const started = await runtime.host.start(state.container);
2203
+ if (started.status !== 0) return;
2204
+ }
2205
+ const log = await runtime.host.exec({ container: state.container, user: state.agentUser ?? ADDED_USER, env: NO_ENV, argv: ["cat", HOOK_ERRORS_IN_CONTAINER] });
2206
+ if (log.status === 0 && log.stdout !== "") writeFileSync(join(jobDir, "hook-errors.log"), log.stdout);
2207
+ } catch {
2208
+ }
2209
+ }
2210
+ async function cleanupContainer(runtime, state, jobDir) {
2211
+ const failures = [];
2212
+ if (jobDir !== void 0) await copyHookErrors(runtime, state, jobDir);
2213
+ const removedContainer = await runtime.host.remove(state.container);
2214
+ if (removedContainer.status !== 0 && !/no such container/i.test(removedContainer.stderr)) {
2215
+ failures.push(`container ${state.container} was not removed: ${removedContainer.stderr.trim()}`);
2216
+ }
2217
+ let volumeGone = false;
2218
+ for (let attempt = 0; attempt < VOLUME_REMOVE_ATTEMPTS && !volumeGone; attempt++) {
2219
+ const removed = await runtime.host.removeVolume(state.volume);
2220
+ volumeGone = removed.status === 0 || /no such volume/i.test(removed.stderr);
2221
+ if (!volumeGone) await runtime.sleep(500);
2222
+ }
2223
+ if (!volumeGone) failures.push(`volume ${state.volume} is still in use`);
2224
+ const tunnel = runtime.tunnels.get(state.buildId);
2225
+ if (tunnel !== void 0) {
2226
+ runtime.tunnels.delete(state.buildId);
2227
+ await tunnel.close();
2228
+ }
2229
+ rmSync(state.buildDir, { recursive: true, force: true });
2230
+ if (failures.length > 0) throw new Error(failures.join("; "));
2231
+ }
2232
+
2233
+ export { ADDED_USER, AgentUserError, AssetError, BuildTokens, GH_CONFIG_YML, MIN_ENGINE_MAJOR, classifyRefusal, cleanupContainer, containerCommandLine, createBuildDisk, createContainerRuntime, createRunnerLog, deleteBuildDisk, describeCallFailure, ensureRootfs, ensureTunnel, formatClock, formatDuration, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, prepareBranch, provisionContainer, refusalCode, relativeToRunner, removeStagedContext, resolveAgentUser, retrySend, runBuildSteps, shellQuote, shortClaim, stageGovernedContext, startServicesFor, stopContainer, syncInContainer };