@usine/executor 0.2.0 → 0.3.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 (177) hide show
  1. package/README.md +115 -37
  2. package/dist/child-command.d.ts +4 -7
  3. package/dist/child-command.d.ts.map +1 -1
  4. package/dist/child-command.js +6 -12
  5. package/dist/child-command.js.map +1 -1
  6. package/dist/claude-login-lock.d.ts +8 -0
  7. package/dist/claude-login-lock.d.ts.map +1 -0
  8. package/dist/claude-login-lock.js +9 -0
  9. package/dist/claude-login-lock.js.map +1 -0
  10. package/dist/credential-environment.d.ts +20 -0
  11. package/dist/credential-environment.d.ts.map +1 -0
  12. package/dist/credential-environment.js +20 -0
  13. package/dist/credential-environment.js.map +1 -0
  14. package/dist/durable-session.d.ts +63 -0
  15. package/dist/durable-session.d.ts.map +1 -0
  16. package/dist/durable-session.js +491 -0
  17. package/dist/durable-session.js.map +1 -0
  18. package/dist/exe-dev/commands.d.ts +23 -13
  19. package/dist/exe-dev/commands.d.ts.map +1 -1
  20. package/dist/exe-dev/commands.js +11 -57
  21. package/dist/exe-dev/commands.js.map +1 -1
  22. package/dist/exe-dev/exe-dev.d.ts +9 -3
  23. package/dist/exe-dev/exe-dev.d.ts.map +1 -1
  24. package/dist/exe-dev/exe-dev.js +53 -127
  25. package/dist/exe-dev/exe-dev.js.map +1 -1
  26. package/dist/exe-dev/grants.d.ts.map +1 -1
  27. package/dist/exe-dev/grants.js +35 -15
  28. package/dist/exe-dev/grants.js.map +1 -1
  29. package/dist/exe-dev/model.d.ts +15 -19
  30. package/dist/exe-dev/model.d.ts.map +1 -1
  31. package/dist/exe-dev/model.js +9 -12
  32. package/dist/exe-dev/model.js.map +1 -1
  33. package/dist/exe-dev/preflight.d.ts +8 -4
  34. package/dist/exe-dev/preflight.d.ts.map +1 -1
  35. package/dist/exe-dev/preflight.js +23 -33
  36. package/dist/exe-dev/preflight.js.map +1 -1
  37. package/dist/executor.d.ts +141 -38
  38. package/dist/executor.d.ts.map +1 -1
  39. package/dist/executor.js +90 -17
  40. package/dist/executor.js.map +1 -1
  41. package/dist/guard.d.ts +61 -0
  42. package/dist/guard.d.ts.map +1 -0
  43. package/dist/guard.js +288 -0
  44. package/dist/guard.js.map +1 -0
  45. package/dist/harness-preflight.d.ts +25 -4
  46. package/dist/harness-preflight.d.ts.map +1 -1
  47. package/dist/harness-preflight.js +25 -29
  48. package/dist/harness-preflight.js.map +1 -1
  49. package/dist/keyed-lock.d.ts +2 -3
  50. package/dist/keyed-lock.d.ts.map +1 -1
  51. package/dist/keyed-lock.js +2 -3
  52. package/dist/keyed-lock.js.map +1 -1
  53. package/dist/leftovers.d.ts +9 -0
  54. package/dist/leftovers.d.ts.map +1 -0
  55. package/dist/leftovers.js +14 -0
  56. package/dist/leftovers.js.map +1 -0
  57. package/dist/local/credentials.d.ts +21 -0
  58. package/dist/local/credentials.d.ts.map +1 -0
  59. package/dist/local/credentials.js +39 -0
  60. package/dist/local/credentials.js.map +1 -0
  61. package/dist/local/leftovers.d.ts +8 -0
  62. package/dist/local/leftovers.d.ts.map +1 -0
  63. package/dist/local/leftovers.js +23 -0
  64. package/dist/local/leftovers.js.map +1 -0
  65. package/dist/local/local.d.ts +16 -10
  66. package/dist/local/local.d.ts.map +1 -1
  67. package/dist/local/local.js +164 -129
  68. package/dist/local/local.js.map +1 -1
  69. package/dist/memory-preflight.d.ts +25 -0
  70. package/dist/memory-preflight.d.ts.map +1 -0
  71. package/dist/memory-preflight.js +83 -0
  72. package/dist/memory-preflight.js.map +1 -0
  73. package/dist/pre-push-hook.d.ts +20 -0
  74. package/dist/pre-push-hook.d.ts.map +1 -0
  75. package/dist/pre-push-hook.js +33 -0
  76. package/dist/pre-push-hook.js.map +1 -0
  77. package/dist/prepared-attempt.d.ts +33 -3
  78. package/dist/prepared-attempt.d.ts.map +1 -1
  79. package/dist/prepared-attempt.js +109 -39
  80. package/dist/prepared-attempt.js.map +1 -1
  81. package/dist/provision.d.ts +77 -14
  82. package/dist/provision.d.ts.map +1 -1
  83. package/dist/provision.js +357 -55
  84. package/dist/provision.js.map +1 -1
  85. package/dist/remote/commands.d.ts +51 -0
  86. package/dist/remote/commands.d.ts.map +1 -0
  87. package/dist/remote/commands.js +94 -0
  88. package/dist/remote/commands.js.map +1 -0
  89. package/dist/remote/leftovers.d.ts +14 -0
  90. package/dist/remote/leftovers.d.ts.map +1 -0
  91. package/dist/remote/leftovers.js +22 -0
  92. package/dist/remote/leftovers.js.map +1 -0
  93. package/dist/remote/liveness.d.ts +33 -0
  94. package/dist/remote/liveness.d.ts.map +1 -0
  95. package/dist/remote/liveness.js +194 -0
  96. package/dist/remote/liveness.js.map +1 -0
  97. package/dist/remote/machine.d.ts +64 -0
  98. package/dist/remote/machine.d.ts.map +1 -0
  99. package/dist/remote/machine.js +2 -0
  100. package/dist/remote/machine.js.map +1 -0
  101. package/dist/remote/remote.d.ts +24 -0
  102. package/dist/remote/remote.d.ts.map +1 -0
  103. package/dist/remote/remote.js +308 -0
  104. package/dist/remote/remote.js.map +1 -0
  105. package/dist/remote/sweep.d.ts +15 -0
  106. package/dist/remote/sweep.d.ts.map +1 -0
  107. package/dist/remote/sweep.js +116 -0
  108. package/dist/remote/sweep.js.map +1 -0
  109. package/dist/renewable-credential.d.ts +17 -0
  110. package/dist/renewable-credential.d.ts.map +1 -0
  111. package/dist/renewable-credential.js +58 -0
  112. package/dist/renewable-credential.js.map +1 -0
  113. package/dist/salvage.d.ts +30 -0
  114. package/dist/salvage.d.ts.map +1 -0
  115. package/dist/salvage.js +104 -0
  116. package/dist/salvage.js.map +1 -0
  117. package/dist/ssh/credentials.d.ts +40 -0
  118. package/dist/ssh/credentials.d.ts.map +1 -0
  119. package/dist/ssh/credentials.js +83 -0
  120. package/dist/ssh/credentials.js.map +1 -0
  121. package/dist/ssh/gateway/admin.d.ts +45 -0
  122. package/dist/ssh/gateway/admin.d.ts.map +1 -0
  123. package/dist/ssh/gateway/admin.js +129 -0
  124. package/dist/ssh/gateway/admin.js.map +1 -0
  125. package/dist/ssh/gateway/attachments.d.ts +22 -0
  126. package/dist/ssh/gateway/attachments.d.ts.map +1 -0
  127. package/dist/ssh/gateway/attachments.js +154 -0
  128. package/dist/ssh/gateway/attachments.js.map +1 -0
  129. package/dist/ssh/gateway/environment.d.ts +52 -0
  130. package/dist/ssh/gateway/environment.d.ts.map +1 -0
  131. package/dist/ssh/gateway/environment.js +78 -0
  132. package/dist/ssh/gateway/environment.js.map +1 -0
  133. package/dist/ssh/gateway/forward.d.ts +74 -0
  134. package/dist/ssh/gateway/forward.d.ts.map +1 -0
  135. package/dist/ssh/gateway/forward.js +247 -0
  136. package/dist/ssh/gateway/forward.js.map +1 -0
  137. package/dist/ssh/gateway/preflight.d.ts +28 -0
  138. package/dist/ssh/gateway/preflight.d.ts.map +1 -0
  139. package/dist/ssh/gateway/preflight.js +234 -0
  140. package/dist/ssh/gateway/preflight.js.map +1 -0
  141. package/dist/ssh/model.d.ts +65 -0
  142. package/dist/ssh/model.d.ts.map +1 -0
  143. package/dist/ssh/model.js +2 -0
  144. package/dist/ssh/model.js.map +1 -0
  145. package/dist/ssh/preflight.d.ts +16 -0
  146. package/dist/ssh/preflight.d.ts.map +1 -0
  147. package/dist/ssh/preflight.js +47 -0
  148. package/dist/ssh/preflight.js.map +1 -0
  149. package/dist/ssh/ssh.d.ts +20 -0
  150. package/dist/ssh/ssh.d.ts.map +1 -0
  151. package/dist/ssh/ssh.js +78 -0
  152. package/dist/ssh/ssh.js.map +1 -0
  153. package/dist/ssh/workers.d.ts +53 -0
  154. package/dist/ssh/workers.d.ts.map +1 -0
  155. package/dist/ssh/workers.js +173 -0
  156. package/dist/ssh/workers.js.map +1 -0
  157. package/package.json +12 -4
  158. package/dist/exe-dev/channel.d.ts +0 -11
  159. package/dist/exe-dev/channel.d.ts.map +0 -1
  160. package/dist/exe-dev/channel.js +0 -41
  161. package/dist/exe-dev/channel.js.map +0 -1
  162. package/dist/exe-dev/shell.d.ts +0 -12
  163. package/dist/exe-dev/shell.d.ts.map +0 -1
  164. package/dist/exe-dev/shell.js +0 -31
  165. package/dist/exe-dev/shell.js.map +0 -1
  166. package/dist/exe-dev/sweep.d.ts +0 -10
  167. package/dist/exe-dev/sweep.d.ts.map +0 -1
  168. package/dist/exe-dev/sweep.js +0 -117
  169. package/dist/exe-dev/sweep.js.map +0 -1
  170. package/dist/invalid-frame.d.ts +0 -17
  171. package/dist/invalid-frame.d.ts.map +0 -1
  172. package/dist/invalid-frame.js +0 -111
  173. package/dist/invalid-frame.js.map +0 -1
  174. package/dist/run-host-session.d.ts +0 -26
  175. package/dist/run-host-session.d.ts.map +0 -1
  176. package/dist/run-host-session.js +0 -241
  177. package/dist/run-host-session.js.map +0 -1
@@ -0,0 +1,104 @@
1
+ import { SalvagePushed, 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
+ /** The git identity for commits usine creates in an Attempt's worktree. */
6
+ export const SALVAGE_GIT_CONFIG = [
7
+ "-c",
8
+ "user.name=usine",
9
+ "-c",
10
+ "user.email=usine@basaltbytes.dev",
11
+ "-c",
12
+ "commit.gpgsign=false",
13
+ ];
14
+ const decodeCommit = Schema.decodeUnknownEffect(CommitSha);
15
+ /**
16
+ * Returns the operator-facing line for the run drawer: the failure's message followed by its
17
+ * cause's message. For a failed command, the cause is the stderr, which local and SSH executors
18
+ * scrub of the Attempt's token.
19
+ */
20
+ export const failureMessage = (failure) => {
21
+ const detail = failure.cause instanceof Error ? failure.cause.message.trim() : "";
22
+ return detail.length === 0 ? failure.message : `${failure.message}: ${detail}`;
23
+ };
24
+ /**
25
+ * Commits everything in an unsuccessful Attempt's checkout and pushes HEAD to its salvage ref
26
+ * before the worktree is removed. Skips a clean checkout at its base commit. If the push fails,
27
+ * keeps the worktree and reports its path. Never fails; skipped and failed stages are logged.
28
+ */
29
+ export const salvageWorktree = Effect.fn("Executor.salvageWorktree")(function* (options) {
30
+ const fields = {
31
+ attemptId: options.attemptId,
32
+ worker: options.worker,
33
+ host: options.host,
34
+ worktree: options.worktree,
35
+ };
36
+ yield* Effect.annotateCurrentSpan(fields);
37
+ const git = (args) => options.git(["-C", options.worktree, ...args]);
38
+ const head = () => git(["rev-parse", "HEAD"]).pipe(Effect.flatMap((output) => decodeCommit(output.trim())));
39
+ const inspected = yield* Effect.all({
40
+ head: head(),
41
+ status: git(["status", "--porcelain", "--untracked-files=all"]),
42
+ }).pipe(Effect.result);
43
+ if (Result.isFailure(inspected)) {
44
+ const message = failureMessage(inspected.failure);
45
+ yield* Effect.logError("Could not inspect the Attempt worktree for salvage", {
46
+ ...fields,
47
+ message,
48
+ });
49
+ return Option.some(new SalvageRetained({ path: options.worktree, commit: options.baseCommit, message }));
50
+ }
51
+ const dirty = inspected.success.status.trim().length > 0;
52
+ if (!dirty && Option.contains(options.baseCommit, inspected.success.head)) {
53
+ yield* Effect.logInfo("Nothing to salvage from the Attempt worktree", fields);
54
+ return Option.none();
55
+ }
56
+ let commit = inspected.success.head;
57
+ if (dirty) {
58
+ const committed = yield* git(["add", "--all"]).pipe(Effect.andThen(git([
59
+ ...SALVAGE_GIT_CONFIG,
60
+ "commit",
61
+ "--quiet",
62
+ "--no-verify",
63
+ "--message",
64
+ `wip(usine): salvage attempt ${options.attemptId}`,
65
+ ])), Effect.andThen(head()), Effect.result);
66
+ if (Result.isFailure(committed)) {
67
+ const message = failureMessage(committed.failure);
68
+ yield* Effect.logError("Could not commit the Attempt's uncommitted changes", {
69
+ ...fields,
70
+ message,
71
+ });
72
+ return Option.some(new SalvageRetained({ path: options.worktree, commit: Option.some(commit), message }));
73
+ }
74
+ else {
75
+ commit = committed.success;
76
+ }
77
+ }
78
+ const pushFailure = yield* git([
79
+ "push",
80
+ "--force",
81
+ "--no-verify",
82
+ "origin",
83
+ `${commit}:refs/heads/${options.ref}`,
84
+ ]).pipe(Effect.timeoutOption(PUSH_TIMEOUT), Effect.match({
85
+ onFailure: (failure) => Option.some(failureMessage(failure)),
86
+ onSuccess: Option.match({
87
+ onNone: () => Option.some(`git push didn't finish within ${PUSH_TIMEOUT}`),
88
+ onSome: () => Option.none(),
89
+ }),
90
+ }));
91
+ if (Option.isSome(pushFailure)) {
92
+ const message = pushFailure.value;
93
+ yield* Effect.logError("Salvage push failed; retaining the Attempt worktree", {
94
+ ...fields,
95
+ ref: options.ref,
96
+ commit,
97
+ message,
98
+ });
99
+ return Option.some(new SalvageRetained({ path: options.worktree, commit: Option.some(commit), message }));
100
+ }
101
+ yield* Effect.logInfo("Salvaged the Attempt's work", { ...fields, ref: options.ref, commit });
102
+ return Option.some(new SalvagePushed({ ref: options.ref, commit }));
103
+ });
104
+ //# 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,40 @@
1
+ import type { WorkerName } from "@usine/core/worker";
2
+ import { Effect } from "effect";
3
+ import { CredentialProvider, type ExecutorError } from "../executor.ts";
4
+ import type { RemoteCommands } from "../remote/commands.ts";
5
+ import type { SshMachine } from "./model.ts";
6
+ /**
7
+ * Writes each Attempt's token to a directory next to the Worker's checkouts that only the SSH
8
+ * user can read, renews it before expiry, and removes it when the Attempt's scope closes. All
9
+ * Attempts on the Worker can read each other's tokens (ADR 0026). `reclaim` removes credentials
10
+ * left by an earlier process, once per process.
11
+ */
12
+ export declare const sshCredentialsFor: (commands: RemoteCommands, machineFor: (worker: WorkerName) => Effect.Effect<SshMachine, ExecutorError>) => Effect.Effect<{
13
+ acquire: (attemptId: string & import("effect/Brand").Brand<"AttemptId">, worker: string & import("effect/Brand").Brand<"WorkerName">, repository: string & import("effect/Brand").Brand<"RepositoryName">, permissions: "read" | "write", machine: SshMachine, operation: "cancel" | "cleanup" | "launch" | "preflight" | "prepare") => Effect.Effect<{
14
+ environment: {
15
+ GH_CONFIG_DIR: string;
16
+ GH_PROMPT_DISABLED: string;
17
+ USINE_GITHUB_TOKEN_FILE: string;
18
+ GIT_CONFIG_COUNT: string;
19
+ GIT_CONFIG_KEY_0: string;
20
+ GIT_CONFIG_VALUE_0: string;
21
+ GIT_CONFIG_KEY_1: string;
22
+ GIT_CONFIG_VALUE_1: string;
23
+ };
24
+ gitEnvironment: {
25
+ GH_CONFIG_DIR: string;
26
+ GH_PROMPT_DISABLED: string;
27
+ USINE_GITHUB_TOKEN_FILE: string;
28
+ GIT_CONFIG_COUNT: string;
29
+ GIT_CONFIG_KEY_0: string;
30
+ GIT_CONFIG_VALUE_0: string;
31
+ GIT_CONFIG_KEY_1: string;
32
+ GIT_CONFIG_VALUE_1: string;
33
+ };
34
+ shell: import("@usine/agents/child-host").RemoteShell;
35
+ refresh: Effect.Effect<void, import("../executor.ts").CredentialUnavailableError | import("../executor.ts").ExecutorOperationError, never>;
36
+ scrub: (text: string) => string;
37
+ }, import("../executor.ts").CredentialUnavailableError | import("../executor.ts").ExecutorOperationError, import("effect/Scope").Scope>;
38
+ reclaim: (worker: WorkerName) => Effect.Effect<void, never, never>;
39
+ }, never, CredentialProvider>;
40
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/ssh/credentials.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAS,MAAM,EAAY,MAAM,QAAQ,CAAC;AAEjD,OAAO,EACL,kBAAkB,EAClB,KAAK,aAAa,EAGnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAuB7C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,kDAEP,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;sBA+BnD,UAAU;6BA0EnC,CAAC"}
@@ -0,0 +1,83 @@
1
+ import { safeSegment } from "@usine/core/repository";
2
+ import { Cache, Effect, Redacted } from "effect";
3
+ import { credentialEnvironment, GH_CONFIG, ghHostsConfig } from "../credential-environment.js";
4
+ import { CredentialProvider, } from "../executor.js";
5
+ import { renewableCredential } from "../renewable-credential.js";
6
+ /**
7
+ * A script that writes the Attempt's credential directory `$3` inside the credentials root
8
+ * `$2`: the token from the first stdin line, gh's `hosts.yml` from the rest, and gh's
9
+ * `config.yml` from `$1`. Sets mode 700 on both directories. Reading the token from stdin keeps
10
+ * it out of arguments and the environment; each file is written then renamed.
11
+ */
12
+ const WRITE_CREDENTIAL_SCRIPT = String.raw `
13
+ set -e
14
+ umask 077
15
+ mkdir -p "$2" "$3"
16
+ chmod 700 "$2" "$3"
17
+ IFS= read -r token
18
+ printf '%s' "$token" > "$3/token.next"
19
+ cat > "$3/hosts.next"
20
+ test -f "$3/config.yml" || printf '%s' "$1" > "$3/config.yml"
21
+ mv "$3/token.next" "$3/token"
22
+ mv "$3/hosts.next" "$3/hosts.yml"
23
+ `.trim();
24
+ const credentialsRoot = (machine) => `${machine.workerDirectory}/credentials`;
25
+ /**
26
+ * Writes each Attempt's token to a directory next to the Worker's checkouts that only the SSH
27
+ * user can read, renews it before expiry, and removes it when the Attempt's scope closes. All
28
+ * Attempts on the Worker can read each other's tokens (ADR 0026). `reclaim` removes credentials
29
+ * left by an earlier process, once per process.
30
+ */
31
+ export const sshCredentialsFor = Effect.fn("SshExecutor.credentialsFor")(function* (commands, machineFor) {
32
+ const provider = yield* CredentialProvider;
33
+ const removeStale = Effect.fn("SshExecutor.removeStaleCredentials")(function* (worker) {
34
+ const machine = yield* machineFor(worker);
35
+ const host = machine.shell.destination;
36
+ yield* Effect.annotateCurrentSpan({ worker, host });
37
+ yield* commands.run("cleanup", machine.shell, "rm", ["-rf", credentialsRoot(machine)]);
38
+ yield* Effect.logInfo("Removed credentials an earlier process left on the SSH Worker", {
39
+ worker,
40
+ host,
41
+ });
42
+ });
43
+ // A failed removal is logged and cached, not retried: the remaining tokens expire within an
44
+ // hour and the next start removes them.
45
+ const reclaimed = yield* Cache.make({
46
+ capacity: Number.POSITIVE_INFINITY,
47
+ lookup: (worker) => removeStale(worker).pipe(Effect.tapError((error) => Effect.logWarning("Could not remove stale credentials from the SSH Worker", {
48
+ worker,
49
+ message: error.message,
50
+ })), Effect.ignore),
51
+ });
52
+ const reclaim = (worker) => Cache.get(reclaimed, worker);
53
+ const write = Effect.fn("SshExecutor.writeCredential")(function* (operation, machine, directory, { value }) {
54
+ yield* Effect.annotateCurrentSpan({ host: machine.shell.destination, directory });
55
+ const payload = Redacted.make(`${Redacted.value(value)}\n${ghHostsConfig(Redacted.value(value))}`);
56
+ yield* commands
57
+ .runWithInput(operation, machine.shell, "sh", ["-c", WRITE_CREDENTIAL_SCRIPT, "sh", GH_CONFIG, credentialsRoot(machine), directory], payload)
58
+ .pipe(Effect.ensuring(Effect.sync(() => Redacted.wipeUnsafe(payload))));
59
+ },
60
+ // Scope closure waits for an in-flight write, so a late renewal cannot run after the removal.
61
+ Effect.uninterruptible);
62
+ const acquire = Effect.fn("SshExecutor.acquireCredential")(function* (attemptId, worker, repository, permissions, machine, operation) {
63
+ const host = machine.shell.destination;
64
+ yield* Effect.annotateCurrentSpan({ attemptId, worker, repository, permissions, host });
65
+ const directory = `${credentialsRoot(machine)}/${safeSegment(attemptId)}`;
66
+ yield* Effect.addFinalizer(() => commands.run("cleanup", machine.shell, "rm", ["-rf", directory]).pipe(Effect.tap(() => Effect.logInfo("Removed the Attempt's credential from the SSH Worker", {
67
+ attemptId,
68
+ worker,
69
+ host,
70
+ })), Effect.tapError((error) => Effect.logError("Could not remove the Attempt's credential from the SSH Worker; it expires within the hour", { attemptId, worker, host, message: error.message })), Effect.ignore));
71
+ const credential = yield* renewableCredential(provider.issue, repository, attemptId, permissions, (issued) => write(operation, machine, directory, issued));
72
+ const environment = credentialEnvironment(directory);
73
+ return {
74
+ environment,
75
+ gitEnvironment: environment,
76
+ shell: machine.shell,
77
+ refresh: credential.refresh(),
78
+ scrub: credential.scrub,
79
+ };
80
+ });
81
+ return { acquire, reclaim };
82
+ });
83
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/ssh/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import { type AttachRequest, type Attached, type AttachmentId, type AttachmentList, type Detached, type LlmProviderList } from "@usine/gateway/admin";
2
+ import { Effect, Option, Schema } from "effect";
3
+ import { HttpClient } from "effect/unstable/http";
4
+ import type { SshWorkerGateway } from "../model.ts";
5
+ import { type GatewayForward } from "./forward.ts";
6
+ declare const GatewayAdminError_base: Schema.Class<GatewayAdminError, Schema.TaggedStruct<"GatewayAdminError", {
7
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
8
+ readonly operation: Schema.Literals<readonly ["attach", "renew", "detach", "list", "providers"]>;
9
+ readonly message: Schema.NonEmptyString;
10
+ }>, import("effect/Cause").YieldableError>;
11
+ /**
12
+ * The error for a failed Gateway admin API request. `message` contains no token; the request
13
+ * and raw failure are discarded because an attach request contains the installation token.
14
+ */
15
+ export declare class GatewayAdminError extends GatewayAdminError_base {
16
+ }
17
+ export interface GatewayAdmin {
18
+ /** Attaches an Attempt's token for the Worker and a repository. Returns the attachment ID and its caller token. */
19
+ readonly attach: (request: AttachRequest) => Effect.Effect<Attached, GatewayAdminError>;
20
+ /** Replaces the token and expiry of attachment `id`, or re-creates it after a Gateway restart. The caller token doesn't change. */
21
+ readonly renew: (id: AttachmentId, request: AttachRequest) => Effect.Effect<Attached, GatewayAdminError>;
22
+ /** Removes attachment `id` and returns whether it existed. */
23
+ readonly detach: (id: AttachmentId) => Effect.Effect<Detached, GatewayAdminError>;
24
+ /** Returns the active attachments for all the Gateway's Workers. */
25
+ readonly list: Effect.Effect<AttachmentList, GatewayAdminError>;
26
+ /**
27
+ * Returns the LLM providers the Gateway has credentials for, with each credential kind. Returns
28
+ * `None` if the Gateway predates this endpoint.
29
+ */
30
+ readonly providers: Effect.Effect<Option.Option<LlmProviderList>, GatewayAdminError>;
31
+ }
32
+ /**
33
+ * Returns the admin API client for the Gateway in front of `worker`, connected through
34
+ * `forward` with the admin token as a bearer token. If the forward is closed or a request gets
35
+ * no response, reopens the forward and retries once.
36
+ */
37
+ export declare const gatewayAdminFor: (worker: string & import("effect/Brand").Brand<"WorkerName">, gateway: SshWorkerGateway, forward: GatewayForward) => Effect.Effect<{
38
+ attach: (request: AttachRequest) => Effect.Effect<Attached, GatewayAdminError, never>;
39
+ renew: (id: string & import("effect/Brand").Brand<"AttachmentId">, request: AttachRequest) => Effect.Effect<Attached, GatewayAdminError, never>;
40
+ detach: (id: string & import("effect/Brand").Brand<"AttachmentId">) => Effect.Effect<Detached, GatewayAdminError, never>;
41
+ list: Effect.Effect<AttachmentList, GatewayAdminError, never>;
42
+ providers: Effect.Effect<Option.Option<never> | Option.Option<LlmProviderList>, GatewayAdminError, never>;
43
+ }, never, HttpClient.HttpClient>;
44
+ export {};
45
+ //# sourceMappingURL=admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../../src/ssh/gateway/admin.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,QAAQ,EAEb,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAkB,MAAM,EAAE,MAAM,EAAyB,MAAM,EAAE,MAAM,QAAQ,CAAC;AACvF,OAAO,EAAE,UAAU,EAAsC,MAAM,sBAAsB,CAAC;AAEtF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,KAAK,cAAc,EAA0C,MAAM,cAAc,CAAC;;;;;;AAa3F;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,sBAMrC;CAAG;AAEL,MAAM,WAAW,YAAY;IAC3B,mHAAmH;IACnH,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC;IACxF,mIAAmI;IACnI,QAAQ,CAAC,KAAK,EAAE,CACd,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE,aAAa,KACnB,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC;IAChD,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC;IAClF,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,CAAC;IAChE;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE,iBAAiB,CAAC,CAAC;CACtF;AAmED;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;;;;gCAgF1B,CAAC"}
@@ -0,0 +1,129 @@
1
+ import { WorkerName } from "@usine/core/worker";
2
+ import { GatewayAdminApi, } from "@usine/gateway/admin";
3
+ import { Data, DateTime, Effect, Option, Result, Schema } from "effect";
4
+ import { HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http";
5
+ import { HttpApiClient } from "effect/unstable/httpapi";
6
+ import { GatewayForwardClosed } from "./forward.js";
7
+ const GatewayAdminOperation = Schema.Literals(["attach", "renew", "detach", "list", "providers"]);
8
+ const ACTIONS = {
9
+ attach: "attach a token",
10
+ renew: "renew an attachment",
11
+ detach: "detach an attachment",
12
+ list: "list the attachments",
13
+ providers: "list the LLM providers",
14
+ };
15
+ /**
16
+ * The error for a failed Gateway admin API request. `message` contains no token; the request
17
+ * and raw failure are discarded because an attach request contains the installation token.
18
+ */
19
+ export class GatewayAdminError extends Schema.TaggedError("@usine/executor/GatewayAdminError")("GatewayAdminError", {
20
+ worker: WorkerName,
21
+ operation: GatewayAdminOperation,
22
+ message: Schema.NonEmptyString,
23
+ }) {
24
+ }
25
+ /** The timeout for one admin request. A publish keeps the Attempt's scope open while it runs. */
26
+ const ADMIN_REQUEST_TIMEOUT = "30 seconds";
27
+ /**
28
+ * The error when an admin request through the open forward gets no response. `since` is when
29
+ * the first unanswered request after the last response was sent.
30
+ */
31
+ class GatewayAdminUnanswered extends Data.TaggedError("GatewayAdminUnanswered") {
32
+ }
33
+ /** Returns the failure reason, built only from the forward's state or the failure's tag and status. */
34
+ const reason = (failure) => {
35
+ if (failure instanceof GatewayForwardClosed)
36
+ return `the forward through ${failure.gateway} has been closed since ${DateTime.formatIso(failure.since)}: ${failure.reason}`;
37
+ if (failure instanceof GatewayAdminUnanswered)
38
+ return `the admin API through ${failure.gateway} hasn't responded since ${DateTime.formatIso(failure.since)}: ${failure.detail}`;
39
+ if (HttpClientError.isHttpClientError(failure))
40
+ return failure.response === undefined
41
+ ? `no response (${failure.reason._tag})`
42
+ : `the admin API returned status ${String(failure.response.status)}`;
43
+ switch (failure._tag) {
44
+ case "TimeoutError":
45
+ return `no response within ${ADMIN_REQUEST_TIMEOUT}`;
46
+ case "Unauthorized":
47
+ return "the gateway rejected the admin token";
48
+ case "AttachmentAlreadyExpired":
49
+ return "the token had already expired";
50
+ case "AttachmentConflict":
51
+ return "the attachment ID belongs to another worker or repository";
52
+ case "SchemaError":
53
+ return "the admin API response doesn't match its schema";
54
+ default:
55
+ return `the request failed with ${failure._tag}`;
56
+ }
57
+ };
58
+ /** Returns whether the admin API returned 404 because the Gateway predates the endpoint. */
59
+ const unknownRoute = (failure) => HttpClientError.isHttpClientError(failure) && failure.response?.status === 404;
60
+ /** Returns whether the request got no response, possibly because the forward's SSH connection died unnoticed. */
61
+ const unanswered = (failure) => failure._tag === "TimeoutError" ||
62
+ (HttpClientError.isHttpClientError(failure) && failure.response === undefined);
63
+ /** Returns the log field with the time the admin API became unreachable, if the failure has it. */
64
+ const sinceFields = (failure) => {
65
+ if (failure instanceof GatewayForwardClosed)
66
+ return { closedSince: DateTime.formatIso(failure.since) };
67
+ if (failure instanceof GatewayAdminUnanswered)
68
+ return { unansweredSince: DateTime.formatIso(failure.since) };
69
+ return {};
70
+ };
71
+ const adminClientAt = (url, adminToken) => HttpApiClient.make(GatewayAdminApi, {
72
+ baseUrl: url,
73
+ transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(adminToken)),
74
+ });
75
+ /**
76
+ * Returns the admin API client for the Gateway in front of `worker`, connected through
77
+ * `forward` with the admin token as a bearer token. If the forward is closed or a request gets
78
+ * no response, reopens the forward and retries once.
79
+ */
80
+ export const gatewayAdminFor = Effect.fn("SshExecutor.gatewayAdminFor")(function* (worker, gateway, forward) {
81
+ const http = yield* HttpClient.HttpClient;
82
+ const exchange = Effect.fn("SshExecutor.gatewayAdminRequest")(function* (operation, call) {
83
+ yield* Effect.annotateCurrentSpan({ worker, operation, gateway: forward.gateway });
84
+ const through = Effect.fnUntraced(function* (open) {
85
+ const sentAt = yield* DateTime.now;
86
+ const sent = yield* Effect.result(adminClientAt(open.url, gateway.adminToken).pipe(Effect.provideService(HttpClient.HttpClient, http), Effect.flatMap(call), Effect.timeout(ADMIN_REQUEST_TIMEOUT)));
87
+ if (Result.isFailure(sent) && unanswered(sent.failure))
88
+ return yield* new GatewayAdminUnanswered({
89
+ gateway: forward.gateway,
90
+ since: yield* forward.unanswered(sentAt),
91
+ detail: reason(sent.failure),
92
+ });
93
+ yield* forward.answered;
94
+ if (Result.isFailure(sent))
95
+ return yield* Effect.fail(sent.failure);
96
+ return sent.success;
97
+ });
98
+ const retried = (generation, failure) => Effect.logWarning("A Gateway admin request did not get through; reopening the forward and sending it once more", {
99
+ worker,
100
+ operation,
101
+ gateway: forward.gateway,
102
+ generation,
103
+ reason: reason(failure),
104
+ ...sinceFields(failure),
105
+ }).pipe(Effect.andThen(forward.reopen(generation)), Effect.flatMap(through));
106
+ const opened = yield* Effect.result(forward.current);
107
+ if (Result.isFailure(opened))
108
+ return yield* retried(opened.failure.generation, opened.failure);
109
+ const answered = yield* Effect.result(through(opened.success));
110
+ if (Result.isSuccess(answered))
111
+ return answered.success;
112
+ if (!(answered.failure instanceof GatewayAdminUnanswered))
113
+ return yield* Effect.fail(answered.failure);
114
+ return yield* retried(opened.success.generation, answered.failure);
115
+ });
116
+ const bounded = (operation) => (call) => exchange(operation, call).pipe(Effect.mapError((failure) => new GatewayAdminError({
117
+ worker,
118
+ operation,
119
+ message: `Can't ${ACTIONS[operation]} on the admin API of gateway ${forward.gateway} for worker "${worker}": ${reason(failure)}`,
120
+ })));
121
+ return {
122
+ attach: (request) => bounded("attach")((client) => client.attachments.attach({ payload: request })),
123
+ renew: (id, request) => bounded("renew")((client) => client.attachments.renew({ params: { id }, payload: request })),
124
+ detach: (id) => bounded("detach")((client) => client.attachments.detach({ params: { id } })),
125
+ list: bounded("list")((client) => client.attachments.list()),
126
+ providers: bounded("providers")((client) => client.llm.providers().pipe(Effect.map(Option.some), Effect.catchIf(unknownRoute, () => Effect.succeedNone))),
127
+ };
128
+ });
129
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.js","sourceRoot":"","sources":["../../../src/ssh/gateway/admin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { WorkerName } from "@usine/core/worker";
2
+ import { Effect } from "effect";
3
+ import { CredentialProvider, CredentialUnavailableError, type ExecutorError } from "../../executor.ts";
4
+ import { type RemoteCommands } from "../../remote/commands.ts";
5
+ import type { MachineGateway, SshMachine } from "../model.ts";
6
+ /**
7
+ * Grants each Attempt repository access through the Gateway without a token on the machine
8
+ * (ADR 0027). Attaches the token, renews it before expiry, re-stores it every two minutes, and
9
+ * detaches it when the scope closes. On first use of a Worker, detaches attachments left by an
10
+ * earlier process.
11
+ */
12
+ export declare const gatewayAttachmentsFor: (commands: RemoteCommands, machineFor: (worker: WorkerName) => Effect.Effect<SshMachine, ExecutorError>) => Effect.Effect<{
13
+ acquire: (attemptId: string & import("effect/Brand").Brand<"AttemptId">, worker: string & import("effect/Brand").Brand<"WorkerName">, repository: string & import("effect/Brand").Brand<"RepositoryName">, permissions: "read" | "write", machine: SshMachine, gateway: MachineGateway, operation: "cancel" | "cleanup" | "launch" | "preflight" | "prepare") => Effect.Effect<{
14
+ shell: import("@usine/agents/child-host").RemoteShell;
15
+ environment: Readonly<Record<string, string>>;
16
+ gitEnvironment: Readonly<Record<string, string>>;
17
+ refresh: Effect.Effect<void, CredentialUnavailableError, never>;
18
+ scrub: (text: string) => string;
19
+ }, CredentialUnavailableError | import("../../executor.ts").ExecutorOperationError, import("effect/Scope").Scope>;
20
+ reclaim: (worker: WorkerName) => Effect.Effect<void, never, never>;
21
+ }, never, CredentialProvider>;
22
+ //# sourceMappingURL=attachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../../../src/ssh/gateway/attachments.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,EAAmB,MAAM,EAAoC,MAAM,QAAQ,CAAC;AACnF,OAAO,EACL,kBAAkB,EAClB,0BAA0B,EAC1B,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,cAAc,EAAgB,MAAM,0BAA0B,CAAC;AAG7E,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA2B9D;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,kDAEX,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC;;;;;;;;sBA4DnD,UAAU;6BA+JnC,CAAC"}
@@ -0,0 +1,154 @@
1
+ import { AttachRequest, InstallationCredential } from "@usine/gateway/admin";
2
+ import { Cache, DateTime, Effect, Option, Redacted, Ref, Semaphore } from "effect";
3
+ import { CredentialProvider, CredentialUnavailableError, } from "../../executor.js";
4
+ import { sendingShell } from "../../remote/commands.js";
5
+ import { renewableCredential } from "../../renewable-credential.js";
6
+ import { gatewayEnvironment, ghGatewayConfig } from "./environment.js";
7
+ /**
8
+ * A script that writes `$2` and `$3` as `config.yml` and `hosts.yml` in directory `$1`. Each
9
+ * file is written to a temporary name and renamed.
10
+ */
11
+ const WRITE_GH_CONFIG_SCRIPT = String.raw `
12
+ set -e
13
+ mkdir -p "$1"
14
+ printf '%s' "$2" > "$1/config.yml.$$"
15
+ printf '%s' "$3" > "$1/hosts.yml.$$"
16
+ mv "$1/config.yml.$$" "$1/config.yml"
17
+ mv "$1/hosts.yml.$$" "$1/hosts.yml"
18
+ `.trim();
19
+ /** The interval for re-storing a live Attempt's attachment, which bounds the outage after a Gateway restart. */
20
+ const RESTORE_INTERVAL = "2 minutes";
21
+ /** Returns gh's configuration directory on a Worker behind a Gateway. The directory contains no token. */
22
+ const ghConfigDirectory = (machine) => `${machine.workerDirectory}/gh`;
23
+ /**
24
+ * Grants each Attempt repository access through the Gateway without a token on the machine
25
+ * (ADR 0027). Attaches the token, renews it before expiry, re-stores it every two minutes, and
26
+ * detaches it when the scope closes. On first use of a Worker, detaches attachments left by an
27
+ * earlier process.
28
+ */
29
+ export const gatewayAttachmentsFor = Effect.fn("SshExecutor.gatewayAttachmentsFor")(function* (commands, machineFor) {
30
+ const provider = yield* CredentialProvider;
31
+ const detachLeftovers = Effect.fn("SshExecutor.detachLeftoverAttachments")(function* (worker) {
32
+ const machine = yield* machineFor(worker);
33
+ yield* Effect.annotateCurrentSpan({ worker });
34
+ if (Option.isNone(machine.gateway))
35
+ return yield* Effect.logDebug("The Worker has no Gateway to detach leftovers from", {
36
+ worker,
37
+ });
38
+ const { admin, settings } = machine.gateway.value;
39
+ yield* Effect.annotateCurrentSpan({ domain: settings.domain });
40
+ const { attachments } = yield* admin.list;
41
+ const leftovers = attachments.filter((attachment) => attachment.worker === worker);
42
+ if (leftovers.length === 0)
43
+ return yield* Effect.logDebug("No earlier process left a Gateway attachment for the Worker", {
44
+ worker,
45
+ domain: settings.domain,
46
+ });
47
+ // Detaches each leftover separately so one failure doesn't stop the others.
48
+ yield* Effect.forEach(leftovers, ({ id, repository }) => admin.detach(id).pipe(Effect.tap(({ detached }) => Effect.logInfo("Detached a Gateway attachment an earlier process left", {
49
+ worker,
50
+ repository,
51
+ attachment: id,
52
+ detached,
53
+ })), Effect.tapError((error) => Effect.logWarning("Could not detach a Gateway attachment an earlier process left; it expires with its token", { worker, repository, attachment: id, message: error.message })), Effect.ignore), { discard: true });
54
+ });
55
+ // Caches a failed listing; the leftover attachments expire with their tokens within an hour.
56
+ const reclaimed = yield* Cache.make({
57
+ capacity: Number.POSITIVE_INFINITY,
58
+ lookup: (worker) => detachLeftovers(worker).pipe(Effect.tapError((error) => Effect.logWarning("Could not list the Gateway attachments an earlier process left; they expire with their tokens", { worker, message: error.message })), Effect.ignore),
59
+ });
60
+ const reclaim = (worker) => Cache.get(reclaimed, worker);
61
+ const acquire = Effect.fn("SshExecutor.attachToGateway")(function* (attemptId, worker, repository, permissions, machine, gateway, operation) {
62
+ const { admin, settings } = gateway;
63
+ const fields = { attemptId, worker, repository, permissions, domain: settings.domain };
64
+ yield* Effect.annotateCurrentSpan(fields);
65
+ const config = ghGatewayConfig(settings.domain);
66
+ yield* commands.run(operation, machine.shell, "sh", [
67
+ "-c",
68
+ WRITE_GH_CONFIG_SCRIPT,
69
+ "sh",
70
+ ghConfigDirectory(machine),
71
+ config["config.yml"],
72
+ config["hosts.yml"],
73
+ ]);
74
+ const attached = yield* Ref.make(Option.none());
75
+ // Serializes publish and restore so the Gateway never gets an older token.
76
+ const turn = yield* Semaphore.make(1);
77
+ yield* Effect.addFinalizer(() => Ref.get(attached).pipe(Effect.flatMap(Option.match({
78
+ onNone: () => Effect.logDebug("The Attempt has no Gateway attachment to detach", fields),
79
+ onSome: ({ id }) => admin.detach(id).pipe(Effect.tap(({ detached }) => Effect.logInfo("Detached the Attempt's token from the Gateway", {
80
+ ...fields,
81
+ attachment: id,
82
+ detached,
83
+ })), Effect.tapError((error) => Effect.logWarning("Could not detach the Attempt's token from the Gateway; the attachment expires with the token", { ...fields, attachment: id, message: error.message })), Effect.ignore),
84
+ }))));
85
+ const requestFor = (credential) => new AttachRequest({
86
+ worker,
87
+ repository,
88
+ credential: new InstallationCredential({ token: credential.value }),
89
+ expiresAt: credential.expiresAt,
90
+ });
91
+ const publish = Effect.fn("SshExecutor.publishAttachment")(function* (credential) {
92
+ yield* Effect.annotateCurrentSpan(fields);
93
+ const current = yield* Ref.get(attached);
94
+ const answer = yield* Option.match(current, {
95
+ onNone: () => admin.attach(requestFor(credential)),
96
+ onSome: ({ id }) => admin.renew(id, requestFor(credential)),
97
+ }).pipe(Effect.mapError((cause) => new CredentialUnavailableError({
98
+ repository,
99
+ reason: "unreachable",
100
+ message: cause.message,
101
+ cause,
102
+ })));
103
+ yield* Ref.set(attached, Option.some({ id: answer.attachment.id, credential }));
104
+ yield* Effect.annotateCurrentSpan({ attachment: answer.attachment.id });
105
+ yield* Effect.logInfo(Option.isNone(current)
106
+ ? "Attached the Attempt's token to the Gateway"
107
+ : "Renewed the Attempt's attachment on the Gateway", {
108
+ ...fields,
109
+ attachment: answer.attachment.id,
110
+ expiresAt: DateTime.formatIso(answer.attachment.expiresAt),
111
+ });
112
+ return answer.callerToken;
113
+ },
114
+ // Scope closure waits for an in-flight attach, so the detach has the attachment ID.
115
+ turn.withPermits(1), Effect.uninterruptible);
116
+ // The Gateway stores attachments in memory. After a restart, re-creates the attachment with
117
+ // the same ID and the current token.
118
+ const restore = Effect.fn("SshExecutor.restoreAttachment")(function* () {
119
+ yield* Effect.annotateCurrentSpan(fields);
120
+ const current = yield* Ref.get(attached);
121
+ if (Option.isNone(current))
122
+ return;
123
+ const { id, credential } = current.value;
124
+ yield* Effect.annotateCurrentSpan({ attachment: id });
125
+ yield* admin.renew(id, requestFor(credential));
126
+ yield* Effect.logDebug("Confirmed the Attempt's attachment on the Gateway", {
127
+ ...fields,
128
+ attachment: id,
129
+ });
130
+ }, turn.withPermits(1), Effect.uninterruptible);
131
+ const credential = yield* renewableCredential(provider.issue, repository, attemptId, permissions, publish);
132
+ const callerToken = credential.published;
133
+ // Forked after renewal so scope closure stops it before the token is wiped and detached.
134
+ yield* Effect.forever(Effect.sleep(RESTORE_INTERVAL).pipe(Effect.andThen(restore()), Effect.tapError((error) => Effect.logWarning("Could not confirm the Attempt's attachment on the Gateway; will retry", {
135
+ ...fields,
136
+ message: error.message,
137
+ })), Effect.ignore)).pipe(Effect.forkScoped);
138
+ const { exported, sent } = gatewayEnvironment({
139
+ domain: settings.domain,
140
+ llmProviders: settings.llmProviders,
141
+ ghConfigDirectory: ghConfigDirectory(machine),
142
+ callerToken,
143
+ });
144
+ return {
145
+ shell: sendingShell(machine.shell, sent),
146
+ environment: exported,
147
+ gitEnvironment: exported,
148
+ refresh: credential.refresh(),
149
+ scrub: (text) => credential.scrub(text).replaceAll(Redacted.value(callerToken), "[REDACTED]"),
150
+ };
151
+ });
152
+ return { acquire, reclaim };
153
+ });
154
+ //# sourceMappingURL=attachments.js.map