@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,117 @@
1
+ import { AttemptId } from "@usine/core/attempt";
2
+ import { WorkerName } from "@usine/core/worker";
3
+ import { Effect, Ref, Schema } from "effect";
4
+ import { ExecutorOperationError } from "../executor.js";
5
+ import { keyedLockFor } from "../keyed-lock.js";
6
+ const SWEEP_CONCURRENCY = 4;
7
+ const directChildSchema = (parent) => {
8
+ const prefix = `${parent}/`;
9
+ return Schema.String.check(Schema.makeFilter((value) => {
10
+ const segment = value.slice(prefix.length);
11
+ return (value.startsWith(prefix) &&
12
+ segment.length > 0 &&
13
+ segment !== "." &&
14
+ segment !== ".." &&
15
+ !segment.includes("/"));
16
+ }, { message: `Expected a direct child of ${parent}` }));
17
+ };
18
+ /** Returns the NUL-separated paths in `output`. Fails if one isn't directly under `parent`. */
19
+ export const directChildren = (operation, parent, output) => Schema.decodeUnknownEffect(Schema.Array(directChildSchema(parent)))(output.split("\0").filter((value) => value.length > 0)).pipe(Effect.mapError((cause) => new ExecutorOperationError({
20
+ operation,
21
+ message: `Worker returned an invalid path while listing ${parent}`,
22
+ cause,
23
+ })));
24
+ /**
25
+ * Lists paths directly under a Worker's `attempts` directory, creating it first so a new Worker
26
+ * lists as empty. Verifies that each path is a direct child of `attemptsRoot`.
27
+ */
28
+ export const attemptPathsOn = Effect.fn("RemoteExecutor.attemptPathsOn")(function* (commands, operation, shell, attemptsRoot) {
29
+ yield* Effect.annotateCurrentSpan({ host: shell.destination, operation, attemptsRoot });
30
+ yield* commands.run(operation, shell, "mkdir", ["-p", attemptsRoot]);
31
+ const output = yield* commands.run(operation, shell, "find", [
32
+ attemptsRoot,
33
+ "-mindepth",
34
+ "1",
35
+ "-maxdepth",
36
+ "1",
37
+ "-print0",
38
+ ]);
39
+ return yield* directChildren(operation, attemptsRoot, output);
40
+ });
41
+ /**
42
+ * Returns a sweep that prunes stale cache registrations once per Worker and keeps crash
43
+ * leftovers. Only durable Attempt recovery removes an existing checkout.
44
+ */
45
+ export const abandonedWorktreeSweepFor = Effect.fn("RemoteExecutor.abandonedWorktreeSweepFor")(function* (commands) {
46
+ const locks = yield* keyedLockFor();
47
+ const swept = yield* Ref.make(new Set());
48
+ const sweep = Effect.fn("RemoteExecutor.sweepAbandonedWorktrees")(function* (attemptId, worker, shell, workerDirectory) {
49
+ const host = shell.destination;
50
+ yield* Effect.annotateCurrentSpan({ attemptId, worker, host });
51
+ yield* locks.withLock(worker)(Effect.gen(function* () {
52
+ if ((yield* Ref.get(swept)).has(worker)) {
53
+ yield* Effect.logDebug("Remote abandoned-worktree sweep was already complete", {
54
+ attemptId,
55
+ worker,
56
+ host,
57
+ });
58
+ return;
59
+ }
60
+ // Marked done before running so a failed sweep is not retried before every prepare; the
61
+ // next process start retries it.
62
+ yield* Ref.update(swept, (current) => new Set(current).add(worker));
63
+ const attemptsRoot = `${workerDirectory}/attempts`;
64
+ const reposRoot = `${workerDirectory}/repos`;
65
+ yield* Effect.logInfo("Sweeping abandoned Attempt worktrees on the remote Worker", {
66
+ attemptId,
67
+ worker,
68
+ host,
69
+ });
70
+ yield* commands.run("prepare", shell, "mkdir", ["-p", reposRoot]);
71
+ const attempts = yield* attemptPathsOn(commands, "prepare", shell, attemptsRoot);
72
+ // An unrecognized path can contain unsalvaged work from a crash; only recovery from the
73
+ // durable Attempt record removes it.
74
+ yield* Effect.forEach(attempts, (worktree) => Effect.logWarning("Preserving abandoned remote path for salvage reconciliation", {
75
+ attemptId,
76
+ worker,
77
+ host,
78
+ worktree,
79
+ }), { discard: true });
80
+ const caches = yield* commands
81
+ .run("prepare", shell, "find", [
82
+ reposRoot,
83
+ "-mindepth",
84
+ "1",
85
+ "-maxdepth",
86
+ "1",
87
+ "-type",
88
+ "d",
89
+ "-name",
90
+ "*.git",
91
+ "-print0",
92
+ ])
93
+ .pipe(Effect.flatMap((output) => directChildren("prepare", reposRoot, output)));
94
+ yield* Effect.forEach(caches, (cache) => commands.run("prepare", shell, "git", ["--git-dir", cache, "worktree", "prune"]).pipe(Effect.tapError((error) => Effect.logWarning("Could not prune a remote cache during the sweep", {
95
+ attemptId,
96
+ worker,
97
+ host,
98
+ cache,
99
+ message: error.message,
100
+ })), Effect.ignore), { concurrency: SWEEP_CONCURRENCY, discard: true });
101
+ yield* Effect.logInfo("Remote abandoned-worktree sweep completed", {
102
+ attemptId,
103
+ worker,
104
+ host,
105
+ retained: attempts.length,
106
+ caches: caches.length,
107
+ });
108
+ }).pipe(Effect.tapError((error) => Effect.logWarning("Remote abandoned-worktree sweep failed; continuing", {
109
+ attemptId,
110
+ worker,
111
+ host,
112
+ message: error.message,
113
+ })), Effect.ignore));
114
+ });
115
+ return sweep;
116
+ });
117
+ //# sourceMappingURL=sweep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweep.js","sourceRoot":"","sources":["../../src/remote/sweep.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { AttemptId } from "@usine/core/attempt";
2
+ import type { LoopPermissions } from "@usine/core/loop";
3
+ import type { RepositoryName } from "@usine/core/repository";
4
+ import { Effect } from "effect";
5
+ import type { CredentialUnavailableError, WorkerCredential } from "./executor.ts";
6
+ /**
7
+ * Issues the Attempt's single-repository token with its Loop `permissions`, passes it to
8
+ * `publish`, and publishes a new one before each expiry until the caller's scope closes. Then
9
+ * wipes every issued token, so `Redacted.value` throws. Register cleanup for what `publish`
10
+ * writes before calling this, so the cleanup runs after renewal stops.
11
+ */
12
+ export declare const renewableCredential: <A, E>(issue: (repository: RepositoryName, attemptId: AttemptId, permissions: LoopPermissions) => Effect.Effect<WorkerCredential, CredentialUnavailableError>, repository: string & import("effect/Brand").Brand<"RepositoryName">, attemptId: string & import("effect/Brand").Brand<"AttemptId">, permissions: "read" | "write", publish: (credential: WorkerCredential) => Effect.Effect<A, E>) => Effect.Effect<{
13
+ published: A;
14
+ refresh: () => Effect.Effect<void, E | CredentialUnavailableError, never>;
15
+ scrub: (text: string) => string;
16
+ }, E | CredentialUnavailableError, import("effect/Scope").Scope>;
17
+ //# sourceMappingURL=renewable-credential.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renewable-credential.d.ts","sourceRoot":"","sources":["../src/renewable-credential.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAmB,MAAM,EAA6B,MAAM,QAAQ,CAAC;AAC5E,OAAO,KAAK,EAEV,0BAA0B,EAC1B,gBAAgB,EACjB,MAAM,eAAe,CAAC;AAcvB;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAwD,CAAC,EAAE,CAAC,mVAKpE,gBAAgB,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;;;kBAoD9C,MAAM,KAAG,MAAM;gEAK/B,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { Clock, DateTime, Effect, Redacted, SynchronizedRef } from "effect";
2
+ /** The remaining lifetime, in milliseconds, below which a new token is issued. */
3
+ const RENEWAL_WINDOW_MILLIS = 5 * 60_000;
4
+ /**
5
+ * Issues the Attempt's single-repository token with its Loop `permissions`, passes it to
6
+ * `publish`, and publishes a new one before each expiry until the caller's scope closes. Then
7
+ * wipes every issued token, so `Redacted.value` throws. Register cleanup for what `publish`
8
+ * writes before calling this, so the cleanup runs after renewal stops.
9
+ */
10
+ export const renewableCredential = Effect.fn("Executor.renewableCredential")(function* (issue, repository, attemptId, permissions, publish) {
11
+ yield* Effect.annotateCurrentSpan({ repository, attemptId, permissions });
12
+ const issued = new Set();
13
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
14
+ for (const token of issued)
15
+ Redacted.wipeUnsafe(token);
16
+ }));
17
+ const publishFresh = Effect.fn("Executor.publishCredential")(function* () {
18
+ yield* Effect.annotateCurrentSpan({ repository, attemptId, permissions });
19
+ const credential = yield* issue(repository, attemptId, permissions);
20
+ issued.add(credential.value);
21
+ const published = yield* publish(credential);
22
+ yield* Effect.logInfo("Published renewed Attempt repository credential", {
23
+ repository,
24
+ attemptId,
25
+ permissions,
26
+ expiresAt: DateTime.formatIso(credential.expiresAt),
27
+ });
28
+ return { expiresAt: credential.expiresAt, published };
29
+ });
30
+ const first = yield* publishFresh();
31
+ const expiry = yield* SynchronizedRef.make(first.expiresAt);
32
+ const refresh = Effect.fn("Executor.refreshCredential")(function* () {
33
+ yield* Effect.annotateCurrentSpan({ repository, attemptId });
34
+ yield* SynchronizedRef.updateEffect(expiry, (expiresAt) => Effect.gen(function* () {
35
+ const now = yield* Clock.currentTimeMillis;
36
+ if (DateTime.toEpochMillis(expiresAt) - now > RENEWAL_WINDOW_MILLIS)
37
+ return expiresAt;
38
+ return (yield* publishFresh()).expiresAt;
39
+ }));
40
+ });
41
+ // Forked in a scope opened before the checkout finalizers, so renewal continues through
42
+ // salvage and stops before the published credential is removed.
43
+ yield* Effect.forever(Effect.sleep("30 seconds").pipe(Effect.andThen(refresh()), Effect.tapError((error) => Effect.logWarning("Attempt credential renewal failed; will retry", {
44
+ repository,
45
+ attemptId,
46
+ message: String(error),
47
+ })), Effect.ignore)).pipe(Effect.forkScoped);
48
+ return {
49
+ published: first.published,
50
+ refresh,
51
+ scrub: (text) => {
52
+ for (const token of issued)
53
+ text = text.replaceAll(Redacted.value(token), "[REDACTED]");
54
+ return text;
55
+ },
56
+ };
57
+ });
58
+ //# sourceMappingURL=renewable-credential.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renewable-credential.js","sourceRoot":"","sources":["../src/renewable-credential.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { AttemptId } from "@usine/core/attempt";
2
+ import { type SalvageRef, SalvagePartlyPushed, SalvagePushed, SalvageRejected, SalvageRetained } from "@usine/core/attempt/salvage";
3
+ import { CommitSha } from "@usine/core/repository";
4
+ import type { WorkerName } from "@usine/core/worker";
5
+ import { Effect, Option } from "effect";
6
+ export interface SalvageOptions<E extends Error> {
7
+ readonly attemptId: AttemptId;
8
+ readonly worker: WorkerName;
9
+ readonly host: string;
10
+ readonly worktree: string;
11
+ readonly baseCommit: Option.Option<CommitSha>;
12
+ readonly ref: SalvageRef;
13
+ /** An earlier salvage that the remote rejected; its commit isn't pushed again. */
14
+ readonly rejected: Option.Option<SalvageRejected>;
15
+ /**
16
+ * Runs git with the Worker's repository credentials and the Attempt's `commitIdentity`, which
17
+ * authors and commits the salvage commit. `salvageWorktree` prepends `-C <worktree>`.
18
+ */
19
+ readonly git: (args: ReadonlyArray<string>) => Effect.Effect<string, E>;
20
+ }
21
+ /**
22
+ * Returns the operator-facing line for the run drawer: the failure's message followed by its
23
+ * cause's message, if the cause has one. For a failed command, the cause is the stderr, which
24
+ * local and SSH executors scrub of the Attempt's token.
25
+ */
26
+ export declare const failureMessage: (failure: Error) => string;
27
+ /**
28
+ * Commits the changes in an unsuccessful Attempt's checkout and pushes HEAD to its salvage ref
29
+ * before the worktree is removed. The commit leaves out package stores and build caches, even if
30
+ * the Attempt staged them, and what Git ignores. Skips a checkout with nothing to push beyond its
31
+ * base commit, or whose commit the remote rejected in `rejected`. If the push fails, or the
32
+ * commit leaves out a file larger than GitHub accepts, keeps the worktree and reports its path.
33
+ * If the inspection or the commit fails while the checkout's head can still be the rejected
34
+ * commit, returns `rejected` again, so that the next recovery doesn't push that commit either.
35
+ * Never fails; skipped and failed stages are logged.
36
+ */
37
+ export declare const salvageWorktree: <E extends Error>(options: SalvageOptions<E>) => Effect.Effect<Option.Option<SalvagePartlyPushed | SalvagePushed | SalvageRejected | SalvageRetained>, never, never>;
38
+ //# sourceMappingURL=salvage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"salvage.d.ts","sourceRoot":"","sources":["../src/salvage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAEL,KAAK,UAAU,EACf,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,eAAe,EAChB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAkB,MAAM,QAAQ,CAAC;AAwBxD,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,KAAK;IAC7C,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAClD;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;CACzE;AAMD;;;;GAIG;AACH,eAAO,MAAM,cAAc,YAAa,KAAK,KAAG,MAM/C,CAAC;AA+BF;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAoD,CAAC,SAAS,KAAK,oJAmL7F,CAAC"}
@@ -0,0 +1,198 @@
1
+ import { SalvagePartlyPushed, SalvagePushed, SalvageRejected, SalvageRetained, } from "@usine/core/attempt/salvage";
2
+ import { CommitSha } from "@usine/core/repository";
3
+ import { Effect, Option, Result, Schema } from "effect";
4
+ const PUSH_TIMEOUT = "2 minutes";
5
+ /** GitHub refuses a push with a file larger than 100 MiB. */
6
+ const GITHUB_FILE_LIMIT_BYTES = 100 * 1024 * 1024;
7
+ /** GitHub's code for a push refused for a file over its size limit, in the push's stderr. */
8
+ const LARGE_FILES_REFUSAL = /\bGH001\b/u;
9
+ /** The package stores and build caches that the salvage commit leaves out at any depth. */
10
+ const CACHE_DIRECTORIES = [".pnpm-store", "node_modules", ".turbo"];
11
+ const CACHE_PATHS = CACHE_DIRECTORIES.map((directory) => `:(glob)**/${directory}/**`);
12
+ /**
13
+ * The checkout's paths that the salvage commits: everything that Git doesn't ignore, except
14
+ * package stores and build caches.
15
+ */
16
+ const SALVAGED_PATHS = [
17
+ ".",
18
+ ...CACHE_DIRECTORIES.map((directory) => `:(exclude,glob)**/${directory}/**`),
19
+ ];
20
+ const decodeCommit = Schema.decodeUnknownEffect(CommitSha);
21
+ const decodeCauseMessage = Schema.decodeUnknownOption(Schema.Struct({ message: Schema.String }));
22
+ /**
23
+ * Returns the operator-facing line for the run drawer: the failure's message followed by its
24
+ * cause's message, if the cause has one. For a failed command, the cause is the stderr, which
25
+ * local and SSH executors scrub of the Attempt's token.
26
+ */
27
+ export const failureMessage = (failure) => {
28
+ const detail = Option.match(decodeCauseMessage(failure.cause), {
29
+ onNone: () => "",
30
+ onSome: ({ message }) => message.trim(),
31
+ });
32
+ return detail.length === 0 ? failure.message : `${failure.message}: ${detail}`;
33
+ };
34
+ /**
35
+ * Returns the staged files in the `ls-tree -r -l -z` listing of the index's tree that are larger
36
+ * than GitHub accepts.
37
+ */
38
+ const oversizedIn = (listing, staged) => listing.split("\0").flatMap((entry) => {
39
+ // <mode> SP <type> SP <object> SP <padded size> TAB <path>
40
+ const tab = entry.indexOf("\t");
41
+ if (tab < 0)
42
+ return [];
43
+ const [, type, , size] = entry.slice(0, tab).split(/ +/u);
44
+ const path = entry.slice(tab + 1);
45
+ return type === "blob" && Number(size) > GITHUB_FILE_LIMIT_BYTES && staged.has(path)
46
+ ? [{ path, size: Number(size) }]
47
+ : [];
48
+ });
49
+ const describeFiles = (files) => files.map(({ path, size }) => `${path} (${(size / 1024 / 1024).toFixed(1)} MiB)`).join(", ");
50
+ /** Returns the operator-facing reason that a salvage keeps the checkout of `files`. */
51
+ const leftOutMessage = (files) => `The checkout has files larger than GitHub's 100 MiB limit, which the salvage can't push: ${describeFiles(files)}`;
52
+ /**
53
+ * Commits the changes in an unsuccessful Attempt's checkout and pushes HEAD to its salvage ref
54
+ * before the worktree is removed. The commit leaves out package stores and build caches, even if
55
+ * the Attempt staged them, and what Git ignores. Skips a checkout with nothing to push beyond its
56
+ * base commit, or whose commit the remote rejected in `rejected`. If the push fails, or the
57
+ * commit leaves out a file larger than GitHub accepts, keeps the worktree and reports its path.
58
+ * If the inspection or the commit fails while the checkout's head can still be the rejected
59
+ * commit, returns `rejected` again, so that the next recovery doesn't push that commit either.
60
+ * Never fails; skipped and failed stages are logged.
61
+ */
62
+ export const salvageWorktree = Effect.fn("Executor.salvageWorktree")(function* (options) {
63
+ const fields = {
64
+ attemptId: options.attemptId,
65
+ worker: options.worker,
66
+ host: options.host,
67
+ worktree: options.worktree,
68
+ };
69
+ yield* Effect.annotateCurrentSpan(fields);
70
+ const git = (args) => options.git(["-C", options.worktree, ...args]);
71
+ const head = () => git(["rev-parse", "HEAD"]).pipe(Effect.flatMap((output) => decodeCommit(output.trim())));
72
+ /**
73
+ * Returns what the salvage keeps of a checkout that it couldn't inspect or commit, at `current`
74
+ * if its head is known. While `current` can be the commit of the recorded rejection, that's the
75
+ * rejection; otherwise it's the retained checkout, at the base commit if the head is unknown.
76
+ */
77
+ const unsalvaged = (message, current) => {
78
+ const rejected = Option.filter(options.rejected, ({ commit }) => Option.isNone(current) || Option.contains(current, commit));
79
+ if (Option.isSome(rejected))
80
+ return Effect.logWarning("Keeping the recorded rejection of the Attempt's salvage, so that recovery doesn't push its commit again", { ...fields, ref: options.ref, commit: rejected.value.commit, message }).pipe(Effect.as(Option.some(rejected.value)));
81
+ const commit = Option.orElse(current, () => options.baseCommit);
82
+ return Effect.succeed(Option.some(new SalvageRetained({ path: options.worktree, commit, message })));
83
+ };
84
+ const inspected = yield* Effect.all({
85
+ head: head(),
86
+ status: git(["status", "--porcelain", "--untracked-files=all", "--", ...SALVAGED_PATHS]),
87
+ }).pipe(Effect.result);
88
+ if (Result.isFailure(inspected)) {
89
+ const message = failureMessage(inspected.failure);
90
+ yield* Effect.logError("Could not inspect the Attempt worktree for salvage", {
91
+ ...fields,
92
+ message,
93
+ });
94
+ return yield* unsalvaged(message, Option.none());
95
+ }
96
+ const dirty = inspected.success.status.trim().length > 0;
97
+ /**
98
+ * Commits the salvaged paths without the files larger than GitHub accepts, and returns the
99
+ * checkout's head and the files left out.
100
+ */
101
+ const snapshot = (current) => Effect.gen(function* () {
102
+ yield* git(["add", "--all", "--", ...SALVAGED_PATHS]);
103
+ // Exclusion pathspecs don't unstage the caches that the Attempt's own `git add` staged.
104
+ yield* git(["reset", "--quiet", "--", ...CACHE_PATHS]);
105
+ const staged = (yield* git(["diff", "--cached", "--name-only", "--no-renames", "-z"]))
106
+ .split("\0")
107
+ .filter((path) => path.length > 0);
108
+ if (staged.length === 0)
109
+ return { commit: current, leftOut: [] };
110
+ const tree = (yield* git(["write-tree"])).trim();
111
+ const oversized = oversizedIn(yield* git(["ls-tree", "-r", "-l", "-z", tree]), new Set(staged));
112
+ if (oversized.length > 0) {
113
+ yield* git([
114
+ "--literal-pathspecs",
115
+ "reset",
116
+ "--quiet",
117
+ "--",
118
+ ...oversized.map(({ path }) => path),
119
+ ]);
120
+ yield* Effect.logWarning("Left files larger than GitHub's 100 MiB limit out of the salvage commit", { ...fields, files: describeFiles(oversized) });
121
+ }
122
+ if (staged.length === oversized.length)
123
+ return { commit: current, leftOut: oversized };
124
+ yield* git([
125
+ "-c",
126
+ "commit.gpgsign=false",
127
+ "commit",
128
+ "--quiet",
129
+ "--no-verify",
130
+ "--message",
131
+ `wip(usine): salvage attempt ${options.attemptId}`,
132
+ ]);
133
+ return { commit: yield* head(), leftOut: oversized };
134
+ });
135
+ let commit = inspected.success.head;
136
+ let leftOut = [];
137
+ if (dirty) {
138
+ const committed = yield* snapshot(commit).pipe(Effect.result);
139
+ if (Result.isFailure(committed)) {
140
+ const message = failureMessage(committed.failure);
141
+ yield* Effect.logError("Could not commit the Attempt's uncommitted changes", {
142
+ ...fields,
143
+ message,
144
+ });
145
+ return yield* unsalvaged(message, Option.some(commit));
146
+ }
147
+ ({ commit, leftOut } = committed.success);
148
+ }
149
+ if (Option.contains(options.baseCommit, commit)) {
150
+ if (leftOut.length === 0) {
151
+ yield* Effect.logInfo("Nothing to salvage from the Attempt worktree", fields);
152
+ return Option.none();
153
+ }
154
+ const message = leftOutMessage(leftOut);
155
+ yield* Effect.logWarning("Nothing to push but files larger than GitHub's limit; retaining the Attempt worktree", { ...fields, commit, message });
156
+ return Option.some(new SalvageRetained({ path: options.worktree, commit: Option.some(commit), message }));
157
+ }
158
+ const rejected = Option.filter(options.rejected, (salvage) => salvage.commit === commit);
159
+ if (Option.isSome(rejected)) {
160
+ yield* Effect.logInfo("The remote rejected this salvage commit before; retaining the Attempt worktree without pushing it again", { ...fields, ref: options.ref, commit });
161
+ return Option.some(rejected.value);
162
+ }
163
+ const pushFailure = yield* git([
164
+ "push",
165
+ "--force",
166
+ "--no-verify",
167
+ "origin",
168
+ `${commit}:refs/heads/${options.ref}`,
169
+ ]).pipe(Effect.timeoutOption(PUSH_TIMEOUT), Effect.match({
170
+ onFailure: (failure) => Option.some(failureMessage(failure)),
171
+ onSuccess: Option.match({
172
+ onNone: () => Option.some(`git push didn't finish within ${PUSH_TIMEOUT}`),
173
+ onSome: () => Option.none(),
174
+ }),
175
+ }));
176
+ if (Option.isSome(pushFailure)) {
177
+ const message = pushFailure.value;
178
+ if (LARGE_FILES_REFUSAL.test(message)) {
179
+ yield* Effect.logError("GitHub refused the salvage push for a file over its size limit; retaining the Attempt worktree, which recovery doesn't push again until its commit changes", { ...fields, ref: options.ref, commit, message });
180
+ return Option.some(new SalvageRejected({ path: options.worktree, commit, message }));
181
+ }
182
+ yield* Effect.logError("Salvage push failed; retaining the Attempt worktree", {
183
+ ...fields,
184
+ ref: options.ref,
185
+ commit,
186
+ message,
187
+ });
188
+ return Option.some(new SalvageRetained({ path: options.worktree, commit: Option.some(commit), message }));
189
+ }
190
+ if (leftOut.length > 0) {
191
+ const message = leftOutMessage(leftOut);
192
+ yield* Effect.logWarning("Salvaged the Attempt's work without files larger than GitHub's limit; retaining the Attempt worktree, which has their only copy", { ...fields, ref: options.ref, commit, message });
193
+ return Option.some(new SalvagePartlyPushed({ ref: options.ref, commit, path: options.worktree, message }));
194
+ }
195
+ yield* Effect.logInfo("Salvaged the Attempt's work", { ...fields, ref: options.ref, commit });
196
+ return Option.some(new SalvagePushed({ ref: options.ref, commit }));
197
+ });
198
+ //# sourceMappingURL=salvage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"salvage.js","sourceRoot":"","sources":["../src/salvage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { Harness } from "@usine/core/agent/brain";
2
+ import { Effect, Option, Redacted } from "effect";
3
+ import { type RemoteCommands } from "../remote/commands.ts";
4
+ import type { RemoteAccess } from "../remote/machine.ts";
5
+ import type { MachineGateway, SshMachine, SshWorkerAddress } from "./model.ts";
6
+ /** Returns whether the Gateway holds a credential for `harness`'s provider. */
7
+ export declare const gatewayHolds: (gateway: Option.Option<MachineGateway>, harness: Harness) => boolean;
8
+ /**
9
+ * Returns the API key that each Attempt on a Worker's machine gets for `harness`, or `None` if
10
+ * the Worker isn't provisioned, the provisioner sets no key, or the Gateway has the provider's
11
+ * credential.
12
+ */
13
+ export declare const provisionedApiKey: (worker: {
14
+ readonly address: SshWorkerAddress;
15
+ readonly gateway: Option.Option<MachineGateway>;
16
+ }, harness: Harness) => Option.Option<Redacted.Redacted<string>>;
17
+ /**
18
+ * Returns `access` with the provisioner's API keys among the Attempt's secrets. With an OpenAI
19
+ * key, also writes the Attempt's Codex home, which `holdCodexHome` gives it, with
20
+ * `CODEX_API_KEY_CONFIG`.
21
+ */
22
+ export declare const withApiKeys: (commands: RemoteCommands, holdCodexHome: (machine: SshMachine, attemptId: string & import("effect/Brand").Brand<"AttemptId">) => Effect.Effect<string, never, import("effect/Scope").Scope>, machine: SshMachine, attemptId: string & import("effect/Brand").Brand<"AttemptId">, access: RemoteAccess, operation: "cancel" | "cleanup" | "launch" | "preflight" | "prepare") => Effect.Effect<RemoteAccess | {
23
+ readOnlyPaths: ReadonlyArray<string>;
24
+ gitEnvironment: Readonly<Record<string, string>>;
25
+ refresh: Effect.Effect<void, import("../executor.ts").ExecutorError>;
26
+ shell: import("@usine/agents/child-host").RemoteShell;
27
+ secrets: {
28
+ [x: string]: Redacted.Redacted<string>;
29
+ };
30
+ writablePaths: string[];
31
+ environment: Readonly<Record<string, string>> | {
32
+ CODEX_HOME: string;
33
+ };
34
+ scrub: (text: string) => string;
35
+ }, import("../executor.ts").ExecutorOperationError, import("effect/Scope").Scope>;
36
+ //# sourceMappingURL=api-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-keys.d.ts","sourceRoot":"","sources":["../../src/ssh/api-keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAGvD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElD,OAAO,EAAE,KAAK,cAAc,EAAgB,MAAM,uBAAuB,CAAC;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGzD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA4B/E,+EAA+E;AAC/E,eAAO,MAAM,YAAY,YAAa,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,WAAW,OAAO,KAAG,OAKrF,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,WACpB;IAAE,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAA;CAAE,WACtF,OAAO,KACf,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAOzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;kBAsCN,MAAM;iFAMtB,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { Effect, Option, Redacted } from "effect";
2
+ import { sendingShell } from "../remote/commands.js";
3
+ import { writeMachineFiles } from "./gateway/attachments.js";
4
+ /** The provider each harness needs a credential from. Pi children run only on local Workers. */
5
+ const PROVIDER_OF_HARNESS = {
6
+ claude: Option.some("anthropic"),
7
+ codex: Option.some("openai"),
8
+ pi: Option.none(),
9
+ };
10
+ /** The variable with the OpenAI API key that Codex's `model_provider` reads (`env_key`). */
11
+ const OPENAI_KEY_VARIABLE = "OPENAI_API_KEY";
12
+ /**
13
+ * Codex's `config.toml` for an OpenAI API key in `OPENAI_API_KEY`: a `model_provider` that
14
+ * sends the Responses API to `api.openai.com` and needs no login. The file contains no key.
15
+ */
16
+ const CODEX_API_KEY_CONFIG = [
17
+ 'model_provider = "usine-openai"',
18
+ "",
19
+ "[model_providers.usine-openai]",
20
+ 'name = "OpenAI API key"',
21
+ 'base_url = "https://api.openai.com/v1"',
22
+ `env_key = "${OPENAI_KEY_VARIABLE}"`,
23
+ 'wire_api = "responses"',
24
+ "requires_openai_auth = false",
25
+ "",
26
+ ].join("\n");
27
+ /** Returns whether the Gateway holds a credential for `harness`'s provider. */
28
+ export const gatewayHolds = (gateway, harness) => Option.exists(gateway, ({ settings }) => Option.exists(PROVIDER_OF_HARNESS[harness], (provider) => settings.llmProviders.includes(provider)));
29
+ /**
30
+ * Returns the API key that each Attempt on a Worker's machine gets for `harness`, or `None` if
31
+ * the Worker isn't provisioned, the provisioner sets no key, or the Gateway has the provider's
32
+ * credential.
33
+ */
34
+ export const provisionedApiKey = (worker, harness) => {
35
+ const { machine } = worker.address;
36
+ if (machine._tag !== "Provisioned" || gatewayHolds(worker.gateway, harness))
37
+ return Option.none();
38
+ return Option.flatMap(PROVIDER_OF_HARNESS[harness], (provider) => machine.provisioner.apiKeys[provider]);
39
+ };
40
+ /**
41
+ * Returns `access` with the provisioner's API keys among the Attempt's secrets. With an OpenAI
42
+ * key, also writes the Attempt's Codex home, which `holdCodexHome` gives it, with
43
+ * `CODEX_API_KEY_CONFIG`.
44
+ */
45
+ export const withApiKeys = Effect.fn("SshExecutor.withApiKeys")(function* (commands, holdCodexHome, machine, attemptId, access, operation) {
46
+ const anthropic = provisionedApiKey(machine, "claude");
47
+ const openai = provisionedApiKey(machine, "codex");
48
+ const secrets = Object.fromEntries([
49
+ ...Option.toArray(Option.map(anthropic, (key) => ["ANTHROPIC_API_KEY", key])),
50
+ ...Option.toArray(Option.map(openai, (key) => [OPENAI_KEY_VARIABLE, key])),
51
+ ]);
52
+ if (Object.keys(secrets).length === 0)
53
+ return access;
54
+ const codexHome = yield* Effect.transposeOption(Option.map(openai, () => holdCodexHome(machine, attemptId).pipe(Effect.tap((home) => writeMachineFiles(commands, operation, machine.shell, [
55
+ [`${home}/config.toml`, CODEX_API_KEY_CONFIG],
56
+ ])))));
57
+ const values = Object.values(secrets).map(Redacted.value);
58
+ return {
59
+ ...access,
60
+ // An Attempt container gets the secrets when it starts; without one, the SSH connection
61
+ // sends them.
62
+ shell: sendingShell(access.shell, secrets),
63
+ secrets: { ...access.secrets, ...secrets },
64
+ writablePaths: [...access.writablePaths, ...Option.toArray(codexHome)],
65
+ environment: Option.match(codexHome, {
66
+ onNone: () => access.environment,
67
+ onSome: (home) => ({ ...access.environment, CODEX_HOME: home }),
68
+ }),
69
+ scrub: (text) => values.reduce((scrubbed, value) => scrubbed.replaceAll(value, "<redacted>"), access.scrub(text)),
70
+ };
71
+ });
72
+ //# sourceMappingURL=api-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../../src/ssh/api-keys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { AttemptId } from "@usine/core/attempt";
2
+ import { Effect } from "effect";
3
+ import type { RemoteCommands } from "../remote/commands.ts";
4
+ import type { SshMachine } from "./model.ts";
5
+ /** Returns the Codex home of `attemptId` in the Worker directory `workerDirectory`. */
6
+ export declare const codexHomeIn: (workerDirectory: string, attemptId: AttemptId) => string;
7
+ /** Returns the Attempt's Codex home on `machine`, which Codex also writes its sessions to. */
8
+ export declare const attemptCodexHome: (machine: SshMachine, attemptId: AttemptId) => string;
9
+ /**
10
+ * Tracks the Codex homes that Attempts in this process use. `hold` gives an Attempt its Codex
11
+ * home and removes the directory when the caller's scope closes. `sweep` removes the Codex homes
12
+ * on a long-lived machine that no Attempt holds and whose Attempt has no checkout there, which
13
+ * a crash or a failed removal left.
14
+ */
15
+ export declare const codexHomesFor: (commands: RemoteCommands) => Effect.Effect<{
16
+ hold: (machine: SshMachine, attemptId: string & import("effect/Brand").Brand<"AttemptId">) => Effect.Effect<string, never, import("effect/Scope").Scope>;
17
+ sweep: (machine: SshMachine) => Effect.Effect<void, import("../executor.ts").ExecutorOperationError, never>;
18
+ }, never, never>;
19
+ /** The Codex homes of the SSH executor's Attempts. See `codexHomesFor`. */
20
+ export type CodexHomes = Effect.Success<ReturnType<typeof codexHomesFor>>;
21
+ //# sourceMappingURL=codex-homes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-homes.d.ts","sourceRoot":"","sources":["../../src/ssh/codex-homes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,EAAE,MAAM,EAAO,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAe7C,uFAAuF;AACvF,eAAO,MAAM,WAAW,oBAAqB,MAAM,aAAa,SAAS,KAAG,MACtB,CAAC;AAEvD,8FAA8F;AAC9F,eAAO,MAAM,gBAAgB,YAAa,UAAU,aAAa,SAAS,KAAG,MAC5B,CAAC;AAElD;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;gBAsFxB,CAAC;AAEH,2EAA2E;AAC3E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC"}