@usine/executor 0.2.1 → 0.4.0

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 (250) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +212 -37
  3. package/dist/child-command.d.ts +5 -8
  4. package/dist/child-command.d.ts.map +1 -1
  5. package/dist/child-command.js +10 -14
  6. package/dist/child-command.js.map +1 -1
  7. package/dist/child-environment.d.ts +19 -0
  8. package/dist/child-environment.d.ts.map +1 -0
  9. package/dist/child-environment.js +18 -0
  10. package/dist/child-environment.js.map +1 -0
  11. package/dist/claude-login-lock.d.ts +9 -0
  12. package/dist/claude-login-lock.d.ts.map +1 -0
  13. package/dist/claude-login-lock.js +15 -0
  14. package/dist/claude-login-lock.js.map +1 -0
  15. package/dist/cloudflare/client.d.ts +50 -0
  16. package/dist/cloudflare/client.d.ts.map +1 -0
  17. package/dist/cloudflare/client.js +108 -0
  18. package/dist/cloudflare/client.js.map +1 -0
  19. package/dist/cloudflare/cloudflare.d.ts +17 -0
  20. package/dist/cloudflare/cloudflare.d.ts.map +1 -0
  21. package/dist/cloudflare/cloudflare.js +704 -0
  22. package/dist/cloudflare/cloudflare.js.map +1 -0
  23. package/dist/cloudflare/model.d.ts +42 -0
  24. package/dist/cloudflare/model.d.ts.map +1 -0
  25. package/dist/cloudflare/model.js +2 -0
  26. package/dist/cloudflare/model.js.map +1 -0
  27. package/dist/cloudflare/shell/environment.d.ts +12 -0
  28. package/dist/cloudflare/shell/environment.d.ts.map +1 -0
  29. package/dist/cloudflare/shell/environment.js +12 -0
  30. package/dist/cloudflare/shell/environment.js.map +1 -0
  31. package/dist/cloudflare/shell/shell.d.ts +2 -0
  32. package/dist/cloudflare/shell/shell.d.ts.map +1 -0
  33. package/dist/cloudflare/shell/shell.js +155 -0
  34. package/dist/cloudflare/shell/shell.js.map +1 -0
  35. package/dist/credential-environment.d.ts +20 -0
  36. package/dist/credential-environment.d.ts.map +1 -0
  37. package/dist/credential-environment.js +20 -0
  38. package/dist/credential-environment.js.map +1 -0
  39. package/dist/durable-session.d.ts +81 -0
  40. package/dist/durable-session.d.ts.map +1 -0
  41. package/dist/durable-session.js +550 -0
  42. package/dist/durable-session.js.map +1 -0
  43. package/dist/exe-dev/commands.d.ts +23 -13
  44. package/dist/exe-dev/commands.d.ts.map +1 -1
  45. package/dist/exe-dev/commands.js +11 -57
  46. package/dist/exe-dev/commands.js.map +1 -1
  47. package/dist/exe-dev/exe-dev.d.ts +9 -3
  48. package/dist/exe-dev/exe-dev.d.ts.map +1 -1
  49. package/dist/exe-dev/exe-dev.js +61 -128
  50. package/dist/exe-dev/exe-dev.js.map +1 -1
  51. package/dist/exe-dev/grants.d.ts.map +1 -1
  52. package/dist/exe-dev/grants.js +35 -15
  53. package/dist/exe-dev/grants.js.map +1 -1
  54. package/dist/exe-dev/model.d.ts +15 -19
  55. package/dist/exe-dev/model.d.ts.map +1 -1
  56. package/dist/exe-dev/model.js +9 -12
  57. package/dist/exe-dev/model.js.map +1 -1
  58. package/dist/exe-dev/preflight.d.ts +8 -4
  59. package/dist/exe-dev/preflight.d.ts.map +1 -1
  60. package/dist/exe-dev/preflight.js +26 -35
  61. package/dist/exe-dev/preflight.js.map +1 -1
  62. package/dist/executor.d.ts +292 -44
  63. package/dist/executor.d.ts.map +1 -1
  64. package/dist/executor.js +187 -19
  65. package/dist/executor.js.map +1 -1
  66. package/dist/guard.d.ts +57 -7
  67. package/dist/guard.d.ts.map +1 -1
  68. package/dist/guard.js +274 -59
  69. package/dist/guard.js.map +1 -1
  70. package/dist/harness-preflight.d.ts +27 -5
  71. package/dist/harness-preflight.d.ts.map +1 -1
  72. package/dist/harness-preflight.js +32 -31
  73. package/dist/harness-preflight.js.map +1 -1
  74. package/dist/keyed-lock.d.ts +2 -3
  75. package/dist/keyed-lock.d.ts.map +1 -1
  76. package/dist/keyed-lock.js +2 -3
  77. package/dist/keyed-lock.js.map +1 -1
  78. package/dist/leftovers.d.ts +9 -0
  79. package/dist/leftovers.d.ts.map +1 -0
  80. package/dist/leftovers.js +14 -0
  81. package/dist/leftovers.js.map +1 -0
  82. package/dist/local/credentials.d.ts +21 -0
  83. package/dist/local/credentials.d.ts.map +1 -0
  84. package/dist/local/credentials.js +39 -0
  85. package/dist/local/credentials.js.map +1 -0
  86. package/dist/local/leftovers.d.ts +8 -0
  87. package/dist/local/leftovers.d.ts.map +1 -0
  88. package/dist/local/leftovers.js +23 -0
  89. package/dist/local/leftovers.js.map +1 -0
  90. package/dist/local/local.d.ts +17 -11
  91. package/dist/local/local.d.ts.map +1 -1
  92. package/dist/local/local.js +205 -132
  93. package/dist/local/local.js.map +1 -1
  94. package/dist/memory-preflight.d.ts +27 -0
  95. package/dist/memory-preflight.d.ts.map +1 -0
  96. package/dist/memory-preflight.js +83 -0
  97. package/dist/memory-preflight.js.map +1 -0
  98. package/dist/pre-push-hook.d.ts +20 -0
  99. package/dist/pre-push-hook.d.ts.map +1 -0
  100. package/dist/pre-push-hook.js +33 -0
  101. package/dist/pre-push-hook.js.map +1 -0
  102. package/dist/prepared-attempt.d.ts +41 -3
  103. package/dist/prepared-attempt.d.ts.map +1 -1
  104. package/dist/prepared-attempt.js +109 -31
  105. package/dist/prepared-attempt.js.map +1 -1
  106. package/dist/provision.d.ts +129 -15
  107. package/dist/provision.d.ts.map +1 -1
  108. package/dist/provision.js +428 -69
  109. package/dist/provision.js.map +1 -1
  110. package/dist/remote/commands.d.ts +51 -0
  111. package/dist/remote/commands.d.ts.map +1 -0
  112. package/dist/remote/commands.js +94 -0
  113. package/dist/remote/commands.js.map +1 -0
  114. package/dist/remote/container.d.ts +90 -0
  115. package/dist/remote/container.d.ts.map +1 -0
  116. package/dist/remote/container.js +480 -0
  117. package/dist/remote/container.js.map +1 -0
  118. package/dist/remote/leftovers.d.ts +14 -0
  119. package/dist/remote/leftovers.d.ts.map +1 -0
  120. package/dist/remote/leftovers.js +22 -0
  121. package/dist/remote/leftovers.js.map +1 -0
  122. package/dist/remote/liveness.d.ts +33 -0
  123. package/dist/remote/liveness.d.ts.map +1 -0
  124. package/dist/remote/liveness.js +196 -0
  125. package/dist/remote/liveness.js.map +1 -0
  126. package/dist/remote/machine.d.ts +110 -0
  127. package/dist/remote/machine.d.ts.map +1 -0
  128. package/dist/remote/machine.js +2 -0
  129. package/dist/remote/machine.js.map +1 -0
  130. package/dist/remote/remote.d.ts +28 -0
  131. package/dist/remote/remote.d.ts.map +1 -0
  132. package/dist/remote/remote.js +556 -0
  133. package/dist/remote/remote.js.map +1 -0
  134. package/dist/remote/sweep.d.ts +17 -0
  135. package/dist/remote/sweep.d.ts.map +1 -0
  136. package/dist/remote/sweep.js +117 -0
  137. package/dist/remote/sweep.js.map +1 -0
  138. package/dist/renewable-credential.d.ts +17 -0
  139. package/dist/renewable-credential.d.ts.map +1 -0
  140. package/dist/renewable-credential.js +58 -0
  141. package/dist/renewable-credential.js.map +1 -0
  142. package/dist/salvage.d.ts +38 -0
  143. package/dist/salvage.d.ts.map +1 -0
  144. package/dist/salvage.js +198 -0
  145. package/dist/salvage.js.map +1 -0
  146. package/dist/ssh/api-keys.d.ts +36 -0
  147. package/dist/ssh/api-keys.d.ts.map +1 -0
  148. package/dist/ssh/api-keys.js +72 -0
  149. package/dist/ssh/api-keys.js.map +1 -0
  150. package/dist/ssh/codex-homes.d.ts +21 -0
  151. package/dist/ssh/codex-homes.d.ts.map +1 -0
  152. package/dist/ssh/codex-homes.js +78 -0
  153. package/dist/ssh/codex-homes.js.map +1 -0
  154. package/dist/ssh/container.d.ts +32 -0
  155. package/dist/ssh/container.d.ts.map +1 -0
  156. package/dist/ssh/container.js +282 -0
  157. package/dist/ssh/container.js.map +1 -0
  158. package/dist/ssh/credentials.d.ts +43 -0
  159. package/dist/ssh/credentials.d.ts.map +1 -0
  160. package/dist/ssh/credentials.js +86 -0
  161. package/dist/ssh/credentials.js.map +1 -0
  162. package/dist/ssh/gateway/admin.d.ts +45 -0
  163. package/dist/ssh/gateway/admin.d.ts.map +1 -0
  164. package/dist/ssh/gateway/admin.js +129 -0
  165. package/dist/ssh/gateway/admin.js.map +1 -0
  166. package/dist/ssh/gateway/attachments.d.ts +31 -0
  167. package/dist/ssh/gateway/attachments.d.ts.map +1 -0
  168. package/dist/ssh/gateway/attachments.js +168 -0
  169. package/dist/ssh/gateway/attachments.js.map +1 -0
  170. package/dist/ssh/gateway/environment.d.ts +71 -0
  171. package/dist/ssh/gateway/environment.d.ts.map +1 -0
  172. package/dist/ssh/gateway/environment.js +148 -0
  173. package/dist/ssh/gateway/environment.js.map +1 -0
  174. package/dist/ssh/gateway/forward.d.ts +74 -0
  175. package/dist/ssh/gateway/forward.d.ts.map +1 -0
  176. package/dist/ssh/gateway/forward.js +247 -0
  177. package/dist/ssh/gateway/forward.js.map +1 -0
  178. package/dist/ssh/gateway/preflight.d.ts +67 -0
  179. package/dist/ssh/gateway/preflight.d.ts.map +1 -0
  180. package/dist/ssh/gateway/preflight.js +320 -0
  181. package/dist/ssh/gateway/preflight.js.map +1 -0
  182. package/dist/ssh/limits.d.ts +60 -0
  183. package/dist/ssh/limits.d.ts.map +1 -0
  184. package/dist/ssh/limits.js +84 -0
  185. package/dist/ssh/limits.js.map +1 -0
  186. package/dist/ssh/model.d.ts +153 -0
  187. package/dist/ssh/model.d.ts.map +1 -0
  188. package/dist/ssh/model.js +2 -0
  189. package/dist/ssh/model.js.map +1 -0
  190. package/dist/ssh/preflight.d.ts +35 -0
  191. package/dist/ssh/preflight.d.ts.map +1 -0
  192. package/dist/ssh/preflight.js +87 -0
  193. package/dist/ssh/preflight.js.map +1 -0
  194. package/dist/ssh/provisioner/boat.d.ts +33 -0
  195. package/dist/ssh/provisioner/boat.d.ts.map +1 -0
  196. package/dist/ssh/provisioner/boat.js +247 -0
  197. package/dist/ssh/provisioner/boat.js.map +1 -0
  198. package/dist/ssh/provisioner/hetzner.d.ts +32 -0
  199. package/dist/ssh/provisioner/hetzner.d.ts.map +1 -0
  200. package/dist/ssh/provisioner/hetzner.js +337 -0
  201. package/dist/ssh/provisioner/hetzner.js.map +1 -0
  202. package/dist/ssh/provisioner/host-key.d.ts +11 -0
  203. package/dist/ssh/provisioner/host-key.d.ts.map +1 -0
  204. package/dist/ssh/provisioner/host-key.js +57 -0
  205. package/dist/ssh/provisioner/host-key.js.map +1 -0
  206. package/dist/ssh/provisioner/http.d.ts +45 -0
  207. package/dist/ssh/provisioner/http.d.ts.map +1 -0
  208. package/dist/ssh/provisioner/http.js +72 -0
  209. package/dist/ssh/provisioner/http.js.map +1 -0
  210. package/dist/ssh/provisioner/machines.d.ts +48 -0
  211. package/dist/ssh/provisioner/machines.d.ts.map +1 -0
  212. package/dist/ssh/provisioner/machines.js +686 -0
  213. package/dist/ssh/provisioner/machines.js.map +1 -0
  214. package/dist/ssh/provisioner/model.d.ts +92 -0
  215. package/dist/ssh/provisioner/model.d.ts.map +1 -0
  216. package/dist/ssh/provisioner/model.js +37 -0
  217. package/dist/ssh/provisioner/model.js.map +1 -0
  218. package/dist/ssh/ssh.d.ts +23 -0
  219. package/dist/ssh/ssh.d.ts.map +1 -0
  220. package/dist/ssh/ssh.js +145 -0
  221. package/dist/ssh/ssh.js.map +1 -0
  222. package/dist/ssh/worker-records.d.ts +18 -0
  223. package/dist/ssh/worker-records.d.ts.map +1 -0
  224. package/dist/ssh/worker-records.js +100 -0
  225. package/dist/ssh/worker-records.js.map +1 -0
  226. package/dist/ssh/workers.d.ts +80 -0
  227. package/dist/ssh/workers.d.ts.map +1 -0
  228. package/dist/ssh/workers.js +247 -0
  229. package/dist/ssh/workers.js.map +1 -0
  230. package/package.json +25 -9
  231. package/dist/exe-dev/channel.d.ts +0 -12
  232. package/dist/exe-dev/channel.d.ts.map +0 -1
  233. package/dist/exe-dev/channel.js +0 -42
  234. package/dist/exe-dev/channel.js.map +0 -1
  235. package/dist/exe-dev/shell.d.ts +0 -12
  236. package/dist/exe-dev/shell.d.ts.map +0 -1
  237. package/dist/exe-dev/shell.js +0 -31
  238. package/dist/exe-dev/shell.js.map +0 -1
  239. package/dist/exe-dev/sweep.d.ts +0 -10
  240. package/dist/exe-dev/sweep.d.ts.map +0 -1
  241. package/dist/exe-dev/sweep.js +0 -117
  242. package/dist/exe-dev/sweep.js.map +0 -1
  243. package/dist/invalid-frame.d.ts +0 -17
  244. package/dist/invalid-frame.d.ts.map +0 -1
  245. package/dist/invalid-frame.js +0 -111
  246. package/dist/invalid-frame.js.map +0 -1
  247. package/dist/run-host-session.d.ts +0 -27
  248. package/dist/run-host-session.d.ts.map +0 -1
  249. package/dist/run-host-session.js +0 -303
  250. package/dist/run-host-session.js.map +0 -1
@@ -0,0 +1,39 @@
1
+ import { Effect, FileSystem, Redacted } from "effect";
2
+ import { credentialEnvironment, GH_CONFIG, ghHostsConfig } from "../credential-environment.js";
3
+ import { CredentialProvider, ExecutorOperationError } from "../executor.js";
4
+ import { renewableCredential } from "../renewable-credential.js";
5
+ /**
6
+ * Writes the Attempt's renewed credential to a private temporary directory outside the
7
+ * checkout, removed when the Attempt scope closes.
8
+ */
9
+ export const localCredentialsFor = Effect.gen(function* () {
10
+ const provider = yield* CredentialProvider;
11
+ const fs = yield* FileSystem.FileSystem;
12
+ return Effect.fn("LocalExecutor.credentials")(function* (repository, attemptId, permissions, baseEnvironment) {
13
+ yield* Effect.annotateCurrentSpan({ repository, attemptId, permissions });
14
+ const directory = yield* fs.makeTempDirectoryScoped({ prefix: "usine-credentials-" });
15
+ yield* fs.chmod(directory, 0o700);
16
+ yield* fs.writeFileString(`${directory}/config.yml`, GH_CONFIG, { mode: 0o600 });
17
+ const { refresh, scrub } = yield* renewableCredential(provider.issue, repository, attemptId, permissions, Effect.fn("LocalExecutor.writeCredential")(function* ({ value }) {
18
+ // Writes then renames so readers never see a partial token.
19
+ yield* fs.writeFileString(`${directory}/token.next`, Redacted.value(value), {
20
+ mode: 0o600,
21
+ });
22
+ yield* fs.rename(`${directory}/token.next`, `${directory}/token`);
23
+ yield* fs.writeFileString(`${directory}/hosts.next`, ghHostsConfig(Redacted.value(value)), {
24
+ mode: 0o600,
25
+ });
26
+ yield* fs.rename(`${directory}/hosts.next`, `${directory}/hosts.yml`);
27
+ }));
28
+ return {
29
+ refresh,
30
+ scrub,
31
+ environment: { ...baseEnvironment, ...credentialEnvironment(directory) },
32
+ };
33
+ }, (effect) => effect.pipe(Effect.catchTag("PlatformError", (cause) => Effect.fail(new ExecutorOperationError({
34
+ operation: "prepare",
35
+ message: "Can't write the attempt's credential files",
36
+ cause,
37
+ })))));
38
+ });
39
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/local/credentials.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC/F,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAyB,MAAM,gBAAgB,CAAC;AACnG,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACrD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,kBAAkB,CAAC;IAC3C,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,OAAO,MAAM,CAAC,EAAE,CAAC,2BAA2B,CAAC,CAC3C,QAAQ,CAAC,EACP,UAA0B,EAC1B,SAAoB,EACpB,WAA4B,EAC5B,eAAuC;QAEvC,KAAK,CAAC,CAAC,MAAM,CAAC,mBAAmB,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;QAC1E,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC,CAAC;QACtF,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QAClC,KAAK,CAAC,CAAC,EAAE,CAAC,eAAe,CAAC,GAAG,SAAS,aAAa,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACjF,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC,CAAC,mBAAmB,CACnD,QAAQ,CAAC,KAAK,EACd,UAAU,EACV,SAAS,EACT,WAAW,EACX,MAAM,CAAC,EAAE,CAAC,+BAA+B,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAoB;YAC/E,4DAA4D;YAC5D,KAAK,CAAC,CAAC,EAAE,CAAC,eAAe,CAAC,GAAG,SAAS,aAAa,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;gBAC1E,IAAI,EAAE,KAAK;aACZ,CAAC,CAAC;YACH,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,SAAS,aAAa,EAAE,GAAG,SAAS,QAAQ,CAAC,CAAC;YAClE,KAAK,CAAC,CAAC,EAAE,CAAC,eAAe,CACvB,GAAG,SAAS,aAAa,EACzB,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EACpC;gBACE,IAAI,EAAE,KAAK;aACZ,CACF,CAAC;YACF,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,SAAS,aAAa,EAAE,GAAG,SAAS,YAAY,CAAC,CAAC;QACxE,CAAC,CAAC,CACH,CAAC;QACF,OAAO;YACL,OAAO;YACP,KAAK;YACL,WAAW,EAAE,EAAE,GAAG,eAAe,EAAE,GAAG,qBAAqB,CAAC,SAAS,CAAC,EAAE;SACzE,CAAC;IACJ,CAAC,EACD,CAAC,MAAM,EAAE,EAAE,CACT,MAAM,CAAC,IAAI,CACT,MAAM,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CACzC,MAAM,CAAC,IAAI,CACT,IAAI,sBAAsB,CAAC;QACzB,SAAS,EAAE,SAAS;QACpB,OAAO,EAAE,4CAA4C;QACrD,KAAK;KACN,CAAC,CACH,CACF,CACF,CACJ,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { WorkerName } from "@usine/core/worker";
2
+ import { Effect, FileSystem, Path } from "effect";
3
+ /**
4
+ * Lists checkout paths under each local Worker's `attempts` directory, once per Worker per
5
+ * executor. Returns an empty set if the directory doesn't exist.
6
+ */
7
+ export declare const workerLeftoversFor: (workersRoot: string) => Effect.Effect<(worker: WorkerName) => Effect.Effect<ReadonlySet<string>, import("effect/PlatformError").PlatformError, never>, never, FileSystem.FileSystem | Path.Path>;
8
+ //# sourceMappingURL=leftovers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leftovers.d.ts","sourceRoot":"","sources":["../../src/local/leftovers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGlD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,mMAqB7B,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { Effect, FileSystem, Path } from "effect";
2
+ import { listedOncePerWorker } from "../leftovers.js";
3
+ /**
4
+ * Lists checkout paths under each local Worker's `attempts` directory, once per Worker per
5
+ * executor. Returns an empty set if the directory doesn't exist.
6
+ */
7
+ export const workerLeftoversFor = Effect.fn("LocalExecutor.workerLeftoversFor")(function* (workersRoot) {
8
+ const fileSystem = yield* FileSystem.FileSystem;
9
+ const path = yield* Path.Path;
10
+ return yield* listedOncePerWorker(Effect.fn("LocalExecutor.listWorkerLeftovers")(function* (worker) {
11
+ yield* Effect.annotateCurrentSpan({ worker, host: "local" });
12
+ const root = path.join(workersRoot, worker, "attempts");
13
+ const entries = (yield* fileSystem.exists(root)) ? yield* fileSystem.readDirectory(root) : [];
14
+ const checkouts = new Set(entries.map((entry) => path.join(root, entry)));
15
+ yield* Effect.logInfo("Listed local checkouts left for reconciliation", {
16
+ worker,
17
+ host: "local",
18
+ checkouts: checkouts.size,
19
+ });
20
+ return checkouts;
21
+ }));
22
+ });
23
+ //# sourceMappingURL=leftovers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leftovers.js","sourceRoot":"","sources":["../../src/local/leftovers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,EAAE,CAAC,kCAAkC,CAAC,CAAC,QAAQ,CAAC,EACvF,WAAmB;IAEnB,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IAChD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC9B,OAAO,KAAK,CAAC,CAAC,mBAAmB,CAC/B,MAAM,CAAC,EAAE,CAAC,mCAAmC,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAkB;QAC1E,KAAK,CAAC,CAAC,MAAM,CAAC,mBAAmB,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QACxD,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9F,MAAM,SAAS,GAAwB,IAAI,GAAG,CAC5C,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAC/C,CAAC;QACF,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,gDAAgD,EAAE;YACtE,MAAM;YACN,IAAI,EAAE,OAAO;YACb,SAAS,EAAE,SAAS,CAAC,IAAI;SAC1B,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC,CACH,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -1,20 +1,26 @@
1
+ import { ClaudeLoginLock } from "@usine/agents/runtime";
1
2
  import { RepositoryName } from "@usine/core/repository";
2
3
  import { InstanceDirectory } from "@usine/core/instance";
4
+ import type { AttemptRuntime } from "@usine/durable/attempt-runtime";
3
5
  import { Effect, FileSystem, Layer, Path } from "effect";
4
- import { ChildProcessSpawner } from "effect/unstable/process";
5
- import { CredentialProvider, Executor, type RunHostCommand } from "../executor.ts";
6
- export type { RunHostCommand } from "../executor.ts";
7
- /** Storage and run-host settings for Attempts executed on the local machine. */
6
+ import { ChildProcessSpawner } from "effect/process";
7
+ import { CredentialProvider, Executor } from "../executor.ts";
8
+ import { type HarnessProbe } from "../harness-preflight.ts";
9
+ export type { HarnessProbe } from "../harness-preflight.ts";
8
10
  export interface LocalExecutorOptions {
9
- /** Factory folder under which local Worker caches and Attempt checkouts are stored. */
11
+ /** The directory for local Worker caches and Attempt checkouts. */
10
12
  readonly instanceDirectory: InstanceDirectory;
11
- /** How to launch the one-shot run host; the composition root resolves it. */
12
- readonly runHost: RunHostCommand;
13
- /** Repository fetch URL override, for example a local mirror; defaults to the GitHub HTTPS clone URL. */
13
+ /** Returns the fetch URL for `repository`, for example a local mirror. Defaults to the GitHub HTTPS clone URL. */
14
14
  readonly repositoryUrl?: (repository: RepositoryName) => string;
15
- /** Grace for delivering a stop request, then for the run host to exit; each wait is bounded separately. Defaults to two seconds. */
15
+ /** The timeout for the parent task's cancellation, then separately for the Attempt to finish. Defaults to two seconds. */
16
16
  readonly interruptGrace?: Parameters<typeof Effect.timeoutOption>[1];
17
+ /** Defaults to the `@usine/agents/preflight` checks, which send no prompt. */
18
+ readonly harnessReadiness?: HarnessProbe;
17
19
  }
18
- /** Runs Attempts in isolated checkouts on the local machine. Requires Node; worker contracts are platform-free. */
19
- export declare const layer: (options: LocalExecutorOptions) => Layer.Layer<Executor, never, CredentialProvider | FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
20
+ /**
21
+ * The executor for the local machine. Each Attempt runs in its own checkout with its Pi parent
22
+ * as a durable task in this process (`AttemptRuntime`), so a live Attempt reattaches after an
23
+ * orchestrator restart. Requires Node.
24
+ */
25
+ export declare const layer: (options: LocalExecutorOptions) => Layer.Layer<Executor, never, AttemptRuntime | ClaudeLoginLock | CredentialProvider | FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
20
26
  //# sourceMappingURL=local.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/local/local.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,MAAM,EACN,UAAU,EACV,KAAK,EACL,IAAI,EAML,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE5E,OAAO,EAEL,kBAAkB,EAClB,QAAQ,EAKR,KAAK,cAAc,EAGpB,MAAM,gBAAgB,CAAC;AAaxB,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,gFAAgF;AAChF,MAAM,WAAW,oBAAoB;IACnC,uFAAuF;IACvF,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAC9C,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,yGAAyG;IACzG,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,MAAM,CAAC;IAChE,oIAAoI;IACpI,QAAQ,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,OAAO,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;CACtE;AAoVD,mHAAmH;AACnH,eAAO,MAAM,KAAK,YACP,oBAAoB,KAC5B,KAAK,CAAC,KAAK,CACZ,QAAQ,EACR,KAAK,EACL,kBAAkB,GAAG,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC,mBAAmB,CAC3C,CAAC"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/local/local.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAKxD,OAAO,EAAE,cAAc,EAAe,MAAM,wBAAwB,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAOzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EACL,MAAM,EACN,UAAU,EACV,KAAK,EAEL,IAAI,EAIL,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAqB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAIxE,OAAO,EAGL,kBAAkB,EAClB,QAAQ,EAOT,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,yBAAyB,CAAC;AAqBjC,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAC9C,kHAAkH;IAClH,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,MAAM,CAAC;IAChE,0HAA0H;IAC1H,QAAQ,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,OAAO,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,YAAY,CAAC;CAC1C;AA0gBD;;;;GAIG;AACH,eAAO,MAAM,KAAK,YACP,oBAAoB,KAC5B,KAAK,CAAC,KAAK,CACZ,QAAQ,EACR,KAAK,EACH,cAAc,GACd,eAAe,GACf,kBAAkB,GAClB,UAAU,CAAC,UAAU,GACrB,IAAI,CAAC,IAAI,GACT,mBAAmB,CAAC,mBAAmB,CACY,CAAC"}
@@ -1,21 +1,25 @@
1
+ import { workerLeftoversFor } from "./leftovers.js";
2
+ import { localCredentialsFor } from "./credentials.js";
3
+ import { LocalProcess } from "@usine/agents/child-host";
4
+ import { ClaudeLoginLock } from "@usine/agents/runtime";
1
5
  import * as AttemptChannel from "@usine/core/attempt/channel";
2
- import { RepositoryName } from "@usine/core/repository";
6
+ import { InfraFailure } from "@usine/core/attempt/failure";
7
+ import { RepositoryName, safeSegment } from "@usine/core/repository";
3
8
  import { InstanceDirectory } from "@usine/core/instance";
4
- import { Effect, FileSystem, Layer, Path, Redacted, Result, Schema, Stream, } from "effect";
5
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
9
+ import { harvestFacts, harvestPostedComments, } from "@usine/core/step/harvest";
10
+ import { Effect, FileSystem, Layer, Option, Path, Result, Schema, } from "effect";
11
+ import { ChildProcessSpawner } from "effect/process";
12
+ import { platform, totalmem } from "node:os";
6
13
  import { allowlistedEnvironment, collectCommand, requireSuccess } from "../child-command.js";
7
- import { CheckoutRequest, CredentialProvider, Executor, ExecutorError, ExecutorOperationError, logPreflightFailure, WorkerCapabilityError, } from "../executor.js";
8
- import { verifyHarnesses } from "../harness-preflight.js";
9
- import { prepareAttempt } from "../prepared-attempt.js";
14
+ import { durableSessionFor } from "../durable-session.js";
15
+ import { CheckoutRequest, CredentialProvider, Executor, ExecutorError, ExecutorOperationError, fullPreflight, logPreflightFailure, WorkerCapabilityError, } from "../executor.js";
16
+ import { agentsHarnessProbe, verifyHarnessReadiness, } from "../harness-preflight.js";
17
+ import { childEnvironment, commitIdentity } from "../child-environment.js";
10
18
  import { keyedLockFor } from "../keyed-lock.js";
11
- import { cacheDirectoryName, isCacheDirectoryName, provisionWorktree, safeSegment, } from "../provision.js";
12
- import { runHostSessionFor } from "../run-host-session.js";
19
+ import { SwapOnDemand, verifyLinuxMemory, verifyWorkerMemory, WorkerMemory, } from "../memory-preflight.js";
20
+ import { cacheDirectoryName, isCacheDirectoryName, keptCheckoutRecovery, linkedWorktree, provisionWorktree, reattachWorktree, recoverWorktree, recoveryFields, } from "../provision.js";
13
21
  const isExecutorError = Schema.is(ExecutorError);
14
- /**
15
- * The run host inherits only these variables. The App private key and every
16
- * other orchestrator secret stay out; the single-repo worker token is the only
17
- * credential it receives (ADR 0011).
18
- */
22
+ /** The only process variables passed to an Attempt's git and gh commands (ADR 0011). */
19
23
  const HOST_ENVIRONMENT_ALLOWLIST = [
20
24
  "HOME",
21
25
  "LANG",
@@ -26,31 +30,20 @@ const HOST_ENVIRONMENT_ALLOWLIST = [
26
30
  "USER",
27
31
  ];
28
32
  const executorError = (operation, message, cause) => new ExecutorOperationError({ operation, message, cause });
29
- /** Removes the credential from text before it can reach a persisted error message. */
30
- const scrubbed = (text, credential) => text.replaceAll(Redacted.value(credential), "[REDACTED]");
31
- const credentialEnvironment = (base, credential) => {
32
- const token = Redacted.value(credential);
33
- return {
34
- ...base,
35
- GH_TOKEN: token,
36
- GITHUB_TOKEN: token,
37
- GIT_CONFIG_COUNT: "1",
38
- GIT_CONFIG_KEY_0: "credential.helper",
39
- GIT_CONFIG_VALUE_0: "!f() { echo username=x-access-token; echo password=$GITHUB_TOKEN; }; f",
40
- };
41
- };
42
33
  const makeLocalExecutor = (options) => Effect.gen(function* () {
43
- const credentials = yield* CredentialProvider;
34
+ const credentials = yield* localCredentialsFor;
44
35
  const fileSystem = yield* FileSystem.FileSystem;
45
36
  const path = yield* Path.Path;
46
37
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
47
38
  const fetches = yield* keyedLockFor();
39
+ const logins = yield* ClaudeLoginLock;
48
40
  const baseEnvironment = yield* allowlistedEnvironment(HOST_ENVIRONMENT_ALLOWLIST);
41
+ const harnessReadiness = options.harnessReadiness ?? agentsHarnessProbe(spawner, new LocalProcess(), baseEnvironment);
49
42
  const repositoryUrl = options.repositoryUrl ??
50
43
  ((repository) => `https://github.com/${repository}.git`);
51
- const runHost = options.runHost;
52
44
  const interruptGrace = options.interruptGrace ?? "2 seconds";
53
45
  const workersRoot = path.join(options.instanceDirectory, ".usine", "workers");
46
+ const session = yield* durableSessionFor({ interruptGrace });
54
47
  const executeCommand = Effect.fn("LocalExecutor.executeCommand")(function* (operation, command, args, commandOptions = {}) {
55
48
  yield* Effect.annotateCurrentSpan({ host: "local", operation });
56
49
  const result = yield* collectCommand(spawner, operation, command, args, {
@@ -63,27 +56,30 @@ const makeLocalExecutor = (options) => Effect.gen(function* () {
63
56
  const run = Effect.fn("LocalExecutor.run")(function* (operation, command, args, commandOptions = {}) {
64
57
  yield* executeCommand(operation, command, args, commandOptions);
65
58
  });
66
- // Worktrees never outlive their Attempt (ADR 0006). A crashed orchestrator
67
- // leaves directories and bare-cache registrations behind; the sweep reclaims
68
- // them at the next layer build, before new Attempts start.
59
+ // Git can print the token in diagnostics, so output is scrubbed before it's put in an error or
60
+ // parsed.
61
+ const credentialedGit = (credential, author) => {
62
+ const environment = { ...credential.environment, ...commitIdentity(author) };
63
+ return (operation, args) => Effect.gen(function* () {
64
+ yield* credential.refresh();
65
+ const result = yield* collectCommand(spawner, operation, "git", args, {
66
+ env: environment,
67
+ extendEnv: false,
68
+ });
69
+ return yield* requireSuccess(operation, "local", "git", args, {
70
+ ...result,
71
+ stdout: credential.scrub(result.stdout),
72
+ stderr: credential.scrub(result.stderr),
73
+ });
74
+ });
75
+ };
76
+ // Startup only prunes worktree registrations whose directories are gone; recovery removes
77
+ // checkouts. Unknown leftovers and checkouts awaiting reattachment stay on disk.
69
78
  const sweepAbandonedWorktrees = Effect.fn("LocalExecutor.sweepAbandonedWorktrees")(function* () {
70
79
  if (!(yield* fileSystem.exists(workersRoot)))
71
80
  return;
72
81
  const workers = yield* fileSystem.readDirectory(workersRoot);
73
82
  for (const worker of workers) {
74
- const attemptsRoot = path.join(workersRoot, worker, "attempts");
75
- if (yield* fileSystem.exists(attemptsRoot)) {
76
- const leftovers = yield* fileSystem.readDirectory(attemptsRoot);
77
- for (const leftover of leftovers) {
78
- const target = path.join(attemptsRoot, leftover);
79
- yield* Effect.logInfo("Sweeping abandoned Attempt worktree", {
80
- worker,
81
- host: "local",
82
- worktree: target,
83
- });
84
- yield* fileSystem.remove(target, { recursive: true, force: true });
85
- }
86
- }
87
83
  const reposRoot = path.join(workersRoot, worker, "repos");
88
84
  if (yield* fileSystem.exists(reposRoot)) {
89
85
  const entries = yield* fileSystem.readDirectory(reposRoot);
@@ -131,144 +127,221 @@ const makeLocalExecutor = (options) => Effect.gen(function* () {
131
127
  yield* sweepAbandonedWorktrees().pipe(Effect.tapError((error) => Effect.logWarning("Abandoned-worktree sweep failed; continuing", {
132
128
  message: String(error),
133
129
  })), Effect.ignore);
134
- const preflight = Effect.fn("LocalExecutor.preflight")(function* (worker, brains) {
135
- yield* Effect.annotateCurrentSpan({ worker, host: "local" });
130
+ const workerLeftovers = yield* workerLeftoversFor(workersRoot);
131
+ const harvestCommand = (operation, worktree, credential, environment) => (command, args) => Effect.gen(function* () {
132
+ yield* credential.refresh();
133
+ const result = yield* collectCommand(spawner, operation, command, args, {
134
+ cwd: worktree,
135
+ env: environment,
136
+ extendEnv: false,
137
+ });
138
+ const stdout = yield* requireSuccess(operation, "local", command, args, {
139
+ ...result,
140
+ stdout: credential.scrub(result.stdout),
141
+ stderr: credential.scrub(result.stderr),
142
+ });
143
+ return stdout === "" ? [] : stdout.replace(/\n$/u, "").split("\n");
144
+ });
145
+ const recover = Effect.fn("LocalExecutor.recover")(function* (attempt, record, commitAuthor) {
146
+ yield* Effect.annotateCurrentSpan({ ...recoveryFields(attempt), host: "local" });
147
+ yield* session.stopForRecovery(attempt);
148
+ const root = path.join(workersRoot, attempt.worker);
149
+ const worktree = path.join(root, "attempts", safeSegment(attempt.attemptId));
150
+ const present = (yield* workerLeftovers(attempt.worker)).has(worktree) &&
151
+ (yield* fileSystem.exists(worktree));
152
+ yield* session.recoverPostedComments(attempt, present
153
+ ? Option.some((spec, windows) => Effect.scoped(Effect.gen(function* () {
154
+ const credential = yield* credentials(attempt.repository, attempt.attemptId, attempt.permissions, baseEnvironment);
155
+ return yield* harvestPostedComments(spec, harvestCommand("cleanup", worktree, credential, credential.environment), windows);
156
+ })))
157
+ : Option.none(), record);
158
+ if (!present)
159
+ return;
160
+ yield* reconcileCheckout(attempt, worktree, record.salvage, commitAuthor);
161
+ });
162
+ /**
163
+ * Salvages `attempt`'s checkout at `worktree` with a new credential, records the salvage
164
+ * through `record`, and removes the checkout once its work is pushed. See `recoverWorktree`.
165
+ */
166
+ const reconcileCheckout = Effect.fn("LocalExecutor.reconcileCheckout")(function* (attempt, worktree, record, commitAuthor) {
167
+ yield* Effect.annotateCurrentSpan({ attemptId: attempt.attemptId, worker: attempt.worker });
168
+ const cache = path.join(workersRoot, attempt.worker, "repos", cacheDirectoryName(attempt.repository));
169
+ const removeCheckout = (git) => Effect.asVoid(git(["--git-dir", cache, "worktree", "remove", "--force", worktree]));
170
+ if (attempt.succeeded || attempt.permissions === "read") {
171
+ const git = (args) => executeCommand("cleanup", "git", args);
172
+ return yield* recoverWorktree({ attempt, host: "local", worktree, git, removeCheckout: removeCheckout(git) }, record);
173
+ }
174
+ const author = yield* commitAuthor;
175
+ yield* Effect.scoped(Effect.gen(function* () {
176
+ const credential = yield* credentials(attempt.repository, attempt.attemptId, attempt.permissions, baseEnvironment);
177
+ const git = credentialedGit(credential, author);
178
+ const inCheckout = (args) => git("cleanup", args);
179
+ yield* recoverWorktree({
180
+ attempt,
181
+ host: "local",
182
+ worktree,
183
+ git: inCheckout,
184
+ removeCheckout: removeCheckout(inCheckout),
185
+ }, record);
186
+ }));
187
+ });
188
+ /**
189
+ * Reconciles the checkout at `worktree` that the launch of `request` kept while its parent
190
+ * task's stop was unconfirmed. Does nothing if the checkout is gone.
191
+ */
192
+ const reconcileKept = Effect.fn("LocalExecutor.reconcileKept")(function* (request, worktree, record) {
193
+ yield* Effect.annotateCurrentSpan({ attemptId: request.attemptId, worker: request.worker });
194
+ if (!(yield* fileSystem.exists(worktree))) {
195
+ yield* Effect.logInfo("The kept checkout is gone; nothing to reconcile", {
196
+ attemptId: request.attemptId,
197
+ worker: request.worker,
198
+ worktree,
199
+ });
200
+ return;
201
+ }
202
+ yield* reconcileCheckout(keptCheckoutRecovery(request), worktree, record, Effect.succeed(request.commitAuthor));
203
+ }, (effect) => effect.pipe(Effect.catchTag("PlatformError", (cause) => Effect.fail(executorError("cleanup", "Can't reconcile the kept local checkout", cause)))));
204
+ const preflight = Effect.fn("LocalExecutor.preflight")(function* (worker, brains, maxConcurrentAttempts) {
205
+ yield* Effect.annotateCurrentSpan({ worker, host: "local", maxConcurrentAttempts });
136
206
  const gitVersion = (yield* executeCommand("preflight", "git", ["--version"])).trim();
137
207
  if (!gitVersion.startsWith("git version ")) {
138
208
  return yield* new WorkerCapabilityError({
139
209
  worker,
140
210
  capability: "toolchain",
141
- message: `Worker ${worker} returned an unrecognized git version`,
211
+ message: `Worker "${worker}" returned an unrecognized Git version`,
142
212
  cause: new Error(gitVersion),
143
213
  });
144
214
  }
145
- const reported = (yield* executeCommand("preflight", runHost.command, [
146
- ...runHost.args,
147
- "--protocol-version",
148
- ])).trim();
149
- if (reported !== String(AttemptChannel.PROTOCOL_VERSION)) {
150
- return yield* new WorkerCapabilityError({
151
- worker,
152
- capability: "run-host",
153
- message: `Worker ${worker} has run-host protocol ${reported || "unknown"}; expected ${AttemptChannel.PROTOCOL_VERSION}`,
154
- cause: new Error("run-host protocol mismatch"),
155
- });
156
- }
157
- yield* verifyHarnesses(worker, brains, runHost, (command, args) => executeCommand("preflight", command, args));
215
+ // macOS has no /proc/meminfo and adds swap files as memory runs short.
216
+ yield* platform() === "darwin"
217
+ ? verifyWorkerMemory(worker, maxConcurrentAttempts, new WorkerMemory({ totalBytes: totalmem(), swap: SwapOnDemand.make({}) }))
218
+ : verifyLinuxMemory(worker, maxConcurrentAttempts, executeCommand("preflight", "cat", ["/proc/meminfo"]));
219
+ yield* verifyHarnessReadiness(worker, Option.none(), brains, harnessReadiness, logins);
158
220
  yield* Effect.logInfo("Worker preflight verified", {
159
221
  worker,
160
222
  host: "local",
161
223
  gitVersion,
162
- protocolVersion: AttemptChannel.PROTOCOL_VERSION,
163
224
  });
225
+ return fullPreflight;
164
226
  }, logPreflightFailure);
227
+ // Attempt commands receive only the allowlist and the Attempt's credential, never this
228
+ // process's environment, which contains the App private key (ADR 0011).
229
+ const attemptEnvironment = (workspace, credential) => ({
230
+ ...credential.environment,
231
+ // Turbo writes its cache next to the git common dir, which for a linked worktree is the bare
232
+ // cache. One cache per Worker, outside the directory the startup sweep treats as cache storage.
233
+ TURBO_CACHE_DIR: path.join(workersRoot, workspace.worker, "turbo"),
234
+ });
235
+ const harvest = (workspace, credential, environment, windows) => harvestFacts(workspace.spec, harvestCommand("launch", workspace.worktree, credential, environment), windows).pipe(Effect.mapError((error) => new InfraFailure({
236
+ message: `Can't harvest the attempt's checkout: ${error.message}`,
237
+ cause: error,
238
+ })));
165
239
  const prepare = Effect.fn("LocalExecutor.prepare")(function* (request) {
166
240
  yield* Effect.annotateCurrentSpan({
167
241
  attemptId: request.attemptId,
168
242
  worker: request.worker,
169
243
  host: "local",
170
244
  repository: request.repository,
245
+ loop: request.loop,
246
+ permissions: request.permissions,
247
+ reattach: request.reattach,
171
248
  });
172
- yield* verifyHarnesses(request.worker, [request.agent.brain], runHost, (command, args) => executeCommand("preflight", command, args));
173
- const credential = yield* Effect.acquireRelease(credentials.issue(request.repository, request.attemptId), (token) => Effect.sync(() => {
174
- Redacted.wipeUnsafe(token);
175
- }), { interruptible: true });
249
+ yield* verifyHarnessReadiness(request.worker, Option.none(), [request.agent.brain], harnessReadiness, logins);
250
+ const credential = yield* credentials(request.repository, request.attemptId, request.permissions, baseEnvironment);
176
251
  const workerRoot = path.join(workersRoot, request.worker);
177
252
  const cache = path.join(workerRoot, "repos", cacheDirectoryName(request.repository));
178
253
  const worktree = path.join(workerRoot, "attempts", safeSegment(request.attemptId));
179
- const environment = credentialEnvironment(baseEnvironment, credential);
180
- const commandOptions = { env: environment, extendEnv: false };
181
- yield* Effect.logInfo("Preparing Attempt workspace", {
254
+ yield* Effect.logInfo(request.reattach
255
+ ? "Reattaching the Attempt to its checkout"
256
+ : "Preparing Attempt workspace", {
182
257
  attemptId: request.attemptId,
183
258
  worker: request.worker,
184
259
  repository: request.repository,
185
260
  revision: request.revision,
261
+ loop: request.loop,
262
+ permissions: request.permissions,
186
263
  cache,
187
264
  worktree,
188
265
  });
189
266
  const remove = (_operation, target) => fileSystem.remove(target, { recursive: true, force: true });
190
- const ops = {
191
- ensureRoots: Effect.andThen(fileSystem.makeDirectory(path.dirname(cache), { recursive: true }), fileSystem.makeDirectory(path.dirname(worktree), { recursive: true })),
267
+ const host = {
192
268
  removePath: remove,
193
269
  cacheExists: fileSystem.exists(cache),
270
+ git: credentialedGit(credential, request.commitAuthor),
271
+ };
272
+ const ops = {
273
+ ...host,
274
+ ensureRoots: Effect.andThen(fileSystem.makeDirectory(path.dirname(cache), { recursive: true }), fileSystem.makeDirectory(path.dirname(worktree), { recursive: true })),
194
275
  promoteStaging: fileSystem.rename(`${cache}.cloning`, cache),
195
- git: (operation, args) => Effect.gen(function* () {
196
- // Git can echo the ambient token in diagnostics. Scrub before forming
197
- // an error or passing its stdout to the revision parser.
198
- const result = yield* collectCommand(spawner, operation, "git", args, commandOptions);
199
- return yield* requireSuccess(operation, "local", "git", args, {
200
- ...result,
201
- stdout: scrubbed(result.stdout, credential),
202
- stderr: scrubbed(result.stderr, credential),
203
- });
204
- }),
276
+ checkout: linkedWorktree(host, { cache, worktree }, (target, content) => fileSystem
277
+ .makeDirectory(path.dirname(target), { recursive: true })
278
+ .pipe(Effect.andThen(fileSystem.writeFileString(target, content, { mode: 0o755 })))),
205
279
  };
206
- const prepared = yield* fetches
207
- .withLock(request.repository)(provisionWorktree({
208
- request,
209
- url: repositoryUrl(request.repository),
210
- host: "local",
211
- layout: { cache, staging: `${cache}.cloning`, worktree },
212
- ops,
213
- }))
214
- .pipe(Effect.mapError((error) => isExecutorError(error)
280
+ const acquire = Effect.gen(function* () {
281
+ if (!request.reattach)
282
+ return yield* fetches.withLock(request.repository)(provisionWorktree({
283
+ request,
284
+ url: repositoryUrl(request.repository),
285
+ host: "local",
286
+ layout: { cache, staging: `${cache}.cloning`, worktree },
287
+ ops,
288
+ }));
289
+ if (!(yield* fileSystem.exists(worktree)))
290
+ return yield* executorError("prepare", "The reattached attempt's checkout no longer exists", new Error(worktree));
291
+ const { spec } = yield* session.storedRequest(request.attemptId);
292
+ return yield* reattachWorktree({
293
+ request,
294
+ host: "local",
295
+ layout: { worktree },
296
+ ops,
297
+ spec,
298
+ });
299
+ });
300
+ const prepared = yield* acquire.pipe(Effect.mapError((error) => isExecutorError(error)
215
301
  ? error
216
- : executorError("prepare", "Attempt workspace preparation failed", error)));
302
+ : executorError("prepare", "Can't prepare the attempt workspace", error)));
217
303
  yield* Effect.logInfo("Attempt workspace ready", {
218
304
  attemptId: request.attemptId,
219
- baseCommit: prepared.spec.baseCommit,
305
+ baseCommit: prepared.workspace.spec.baseCommit,
306
+ resume: Option.map(prepared.workspace.spec.resume, ({ ref }) => ref),
307
+ reattach: request.reattach,
220
308
  worktree,
221
309
  });
310
+ const environment = childEnvironment(attemptEnvironment(prepared.workspace, credential), request.commitAuthor, prepared.gitHooks);
222
311
  return {
223
- workspace: prepared,
224
- launch: (io) => session.launch(prepared, io, (input) => spawn(prepared, credential, input)),
225
- };
226
- }, prepareAttempt);
227
- const spawn = Effect.fn("LocalExecutor.spawnRunHost")(function* (prepared, credential, input) {
228
- const attemptId = prepared.spec.attemptId;
229
- yield* Effect.annotateCurrentSpan({
230
- attemptId,
231
- worker: prepared.worker,
232
- host: "local",
233
- });
234
- // A linked worktree's git common dir is the bare cache itself, and Turbo
235
- // puts its cache next to the common dir, so an unscoped run from the
236
- // worktree would write `repos/.turbo` beside the bare caches. The Worker
237
- // cache is shared across that Worker's Attempts and stays out of the
238
- // directory the startup sweep treats as cache storage.
239
- const turboCache = path.join(workersRoot, prepared.worker, "turbo");
240
- yield* Effect.logInfo("Launching the run host", {
241
- attemptId,
242
- worker: prepared.worker,
243
- host: "local",
244
- command: runHost.command,
245
- worktree: prepared.worktree,
246
- turboCache,
247
- });
248
- const handle = yield* spawner.spawn(ChildProcess.make(runHost.command, runHost.args, {
249
- cwd: prepared.worktree,
250
- env: {
251
- ...credentialEnvironment(baseEnvironment, credential),
252
- TURBO_CACHE_DIR: turboCache,
253
- },
254
- extendEnv: false,
255
- stdin: { stream: Stream.fromQueue(input), endOnDone: true },
256
- }));
257
- return {
258
- handle,
259
- host: "local",
260
- scrub: (text) => scrubbed(text, credential),
312
+ workspace: prepared.workspace,
313
+ launch: (io) => session.launch(prepared.workspace, io, {
314
+ host: new LocalProcess(),
315
+ machine: Option.none(),
316
+ environment,
317
+ // Local Workers have no Gateway; every harness uses its own login.
318
+ gatewayCredential: Effect.succeedNone,
319
+ harvest: (windows) => harvest(prepared.workspace, credential, environment, windows),
320
+ harvestComments: (windows) => harvestPostedComments(prepared.workspace.spec, harvestCommand("launch", prepared.workspace.worktree, credential, environment), windows),
321
+ keepForReattach: prepared.keepForReattach,
322
+ keepUntilStopped: prepared.keepUntilStopped,
323
+ reconcileKept: (record) => reconcileKept(request, worktree, record),
324
+ }),
325
+ salvaged: prepared.salvaged,
326
+ recordSalvage: prepared.recordSalvage,
261
327
  };
262
- });
263
- const session = yield* runHostSessionFor({ interruptGrace });
328
+ }, session.prepare);
264
329
  return Executor.of({
265
330
  kind: "local",
331
+ recover: (attempt, record, commitAuthor) => recover(attempt, record, commitAuthor).pipe(Effect.catchTag("PlatformError", (cause) => Effect.fail(executorError("cleanup", "Can't reconcile the local salvage", cause)))),
266
332
  cancel: session.cancel,
267
333
  intervene: session.intervene,
334
+ isLive: session.isLive,
335
+ outcome: session.outcome,
268
336
  preflight,
269
337
  prepare,
338
+ sweep: Effect.void,
270
339
  });
271
340
  });
272
- /** Runs Attempts in isolated checkouts on the local machine. Requires Node; worker contracts are platform-free. */
341
+ /**
342
+ * The executor for the local machine. Each Attempt runs in its own checkout with its Pi parent
343
+ * as a durable task in this process (`AttemptRuntime`), so a live Attempt reattaches after an
344
+ * orchestrator restart. Requires Node.
345
+ */
273
346
  export const layer = (options) => Layer.effect(Executor, makeLocalExecutor(options));
274
347
  //# sourceMappingURL=local.js.map