@usine/executor 0.3.0 → 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.
- package/LICENSE +661 -21
- package/README.md +116 -19
- package/dist/child-command.d.ts +1 -1
- package/dist/child-command.d.ts.map +1 -1
- package/dist/child-command.js +6 -4
- package/dist/child-command.js.map +1 -1
- package/dist/child-environment.d.ts +19 -0
- package/dist/child-environment.d.ts.map +1 -0
- package/dist/child-environment.js +18 -0
- package/dist/child-environment.js.map +1 -0
- package/dist/claude-login-lock.d.ts +3 -2
- package/dist/claude-login-lock.d.ts.map +1 -1
- package/dist/claude-login-lock.js +10 -4
- package/dist/claude-login-lock.js.map +1 -1
- package/dist/cloudflare/client.d.ts +50 -0
- package/dist/cloudflare/client.d.ts.map +1 -0
- package/dist/cloudflare/client.js +108 -0
- package/dist/cloudflare/client.js.map +1 -0
- package/dist/cloudflare/cloudflare.d.ts +17 -0
- package/dist/cloudflare/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare/cloudflare.js +704 -0
- package/dist/cloudflare/cloudflare.js.map +1 -0
- package/dist/cloudflare/model.d.ts +42 -0
- package/dist/cloudflare/model.d.ts.map +1 -0
- package/dist/cloudflare/model.js +2 -0
- package/dist/cloudflare/model.js.map +1 -0
- package/dist/cloudflare/shell/environment.d.ts +12 -0
- package/dist/cloudflare/shell/environment.d.ts.map +1 -0
- package/dist/cloudflare/shell/environment.js +12 -0
- package/dist/cloudflare/shell/environment.js.map +1 -0
- package/dist/cloudflare/shell/shell.d.ts +2 -0
- package/dist/cloudflare/shell/shell.d.ts.map +1 -0
- package/dist/cloudflare/shell/shell.js +155 -0
- package/dist/cloudflare/shell/shell.js.map +1 -0
- package/dist/durable-session.d.ts +21 -3
- package/dist/durable-session.d.ts.map +1 -1
- package/dist/durable-session.js +69 -10
- package/dist/durable-session.js.map +1 -1
- package/dist/exe-dev/commands.d.ts +1 -1
- package/dist/exe-dev/commands.d.ts.map +1 -1
- package/dist/exe-dev/exe-dev.d.ts +1 -1
- package/dist/exe-dev/exe-dev.d.ts.map +1 -1
- package/dist/exe-dev/exe-dev.js +9 -1
- package/dist/exe-dev/exe-dev.js.map +1 -1
- package/dist/exe-dev/preflight.d.ts +2 -2
- package/dist/exe-dev/preflight.d.ts.map +1 -1
- package/dist/exe-dev/preflight.js +5 -4
- package/dist/exe-dev/preflight.js.map +1 -1
- package/dist/executor.d.ts +164 -15
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +100 -3
- package/dist/executor.js.map +1 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +1 -0
- package/dist/guard.js.map +1 -1
- package/dist/harness-preflight.d.ts +9 -8
- package/dist/harness-preflight.d.ts.map +1 -1
- package/dist/harness-preflight.js +15 -10
- package/dist/harness-preflight.js.map +1 -1
- package/dist/local/local.d.ts +1 -1
- package/dist/local/local.d.ts.map +1 -1
- package/dist/local/local.js +76 -37
- package/dist/local/local.js.map +1 -1
- package/dist/memory-preflight.d.ts +3 -1
- package/dist/memory-preflight.d.ts.map +1 -1
- package/dist/memory-preflight.js +1 -1
- package/dist/memory-preflight.js.map +1 -1
- package/dist/pre-push-hook.d.ts +1 -1
- package/dist/pre-push-hook.js +1 -1
- package/dist/prepared-attempt.d.ts +8 -0
- package/dist/prepared-attempt.d.ts.map +1 -1
- package/dist/prepared-attempt.js.map +1 -1
- package/dist/provision.d.ts +64 -13
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +99 -42
- package/dist/provision.js.map +1 -1
- package/dist/remote/commands.d.ts +1 -1
- package/dist/remote/commands.d.ts.map +1 -1
- package/dist/remote/commands.js +1 -1
- package/dist/remote/commands.js.map +1 -1
- package/dist/remote/container.d.ts +90 -0
- package/dist/remote/container.d.ts.map +1 -0
- package/dist/remote/container.js +480 -0
- package/dist/remote/container.js.map +1 -0
- package/dist/remote/liveness.d.ts +1 -1
- package/dist/remote/liveness.d.ts.map +1 -1
- package/dist/remote/liveness.js +6 -4
- package/dist/remote/liveness.js.map +1 -1
- package/dist/remote/machine.d.ts +52 -6
- package/dist/remote/machine.d.ts.map +1 -1
- package/dist/remote/remote.d.ts +11 -7
- package/dist/remote/remote.d.ts.map +1 -1
- package/dist/remote/remote.js +340 -92
- package/dist/remote/remote.js.map +1 -1
- package/dist/remote/sweep.d.ts +3 -1
- package/dist/remote/sweep.d.ts.map +1 -1
- package/dist/remote/sweep.js +2 -1
- package/dist/remote/sweep.js.map +1 -1
- package/dist/salvage.d.ts +18 -10
- package/dist/salvage.d.ts.map +1 -1
- package/dist/salvage.js +123 -29
- package/dist/salvage.js.map +1 -1
- package/dist/ssh/api-keys.d.ts +36 -0
- package/dist/ssh/api-keys.d.ts.map +1 -0
- package/dist/ssh/api-keys.js +72 -0
- package/dist/ssh/api-keys.js.map +1 -0
- package/dist/ssh/codex-homes.d.ts +21 -0
- package/dist/ssh/codex-homes.d.ts.map +1 -0
- package/dist/ssh/codex-homes.js +78 -0
- package/dist/ssh/codex-homes.js.map +1 -0
- package/dist/ssh/container.d.ts +32 -0
- package/dist/ssh/container.d.ts.map +1 -0
- package/dist/ssh/container.js +282 -0
- package/dist/ssh/container.js.map +1 -0
- package/dist/ssh/credentials.d.ts +3 -0
- package/dist/ssh/credentials.d.ts.map +1 -1
- package/dist/ssh/credentials.js +3 -0
- package/dist/ssh/credentials.js.map +1 -1
- package/dist/ssh/gateway/admin.d.ts +1 -1
- package/dist/ssh/gateway/admin.d.ts.map +1 -1
- package/dist/ssh/gateway/admin.js +2 -2
- package/dist/ssh/gateway/admin.js.map +1 -1
- package/dist/ssh/gateway/attachments.d.ts +15 -6
- package/dist/ssh/gateway/attachments.d.ts.map +1 -1
- package/dist/ssh/gateway/attachments.js +39 -25
- package/dist/ssh/gateway/attachments.js.map +1 -1
- package/dist/ssh/gateway/environment.d.ts +21 -2
- package/dist/ssh/gateway/environment.d.ts.map +1 -1
- package/dist/ssh/gateway/environment.js +76 -6
- package/dist/ssh/gateway/environment.js.map +1 -1
- package/dist/ssh/gateway/forward.d.ts +1 -1
- package/dist/ssh/gateway/forward.d.ts.map +1 -1
- package/dist/ssh/gateway/forward.js +1 -1
- package/dist/ssh/gateway/forward.js.map +1 -1
- package/dist/ssh/gateway/preflight.d.ts +51 -12
- package/dist/ssh/gateway/preflight.d.ts.map +1 -1
- package/dist/ssh/gateway/preflight.js +155 -69
- package/dist/ssh/gateway/preflight.js.map +1 -1
- package/dist/ssh/limits.d.ts +60 -0
- package/dist/ssh/limits.d.ts.map +1 -0
- package/dist/ssh/limits.js +84 -0
- package/dist/ssh/limits.js.map +1 -0
- package/dist/ssh/model.d.ts +97 -9
- package/dist/ssh/model.d.ts.map +1 -1
- package/dist/ssh/preflight.d.ts +26 -7
- package/dist/ssh/preflight.d.ts.map +1 -1
- package/dist/ssh/preflight.js +50 -10
- package/dist/ssh/preflight.js.map +1 -1
- package/dist/ssh/provisioner/boat.d.ts +33 -0
- package/dist/ssh/provisioner/boat.d.ts.map +1 -0
- package/dist/ssh/provisioner/boat.js +247 -0
- package/dist/ssh/provisioner/boat.js.map +1 -0
- package/dist/ssh/provisioner/hetzner.d.ts +32 -0
- package/dist/ssh/provisioner/hetzner.d.ts.map +1 -0
- package/dist/ssh/provisioner/hetzner.js +337 -0
- package/dist/ssh/provisioner/hetzner.js.map +1 -0
- package/dist/ssh/provisioner/host-key.d.ts +11 -0
- package/dist/ssh/provisioner/host-key.d.ts.map +1 -0
- package/dist/ssh/provisioner/host-key.js +57 -0
- package/dist/ssh/provisioner/host-key.js.map +1 -0
- package/dist/ssh/provisioner/http.d.ts +45 -0
- package/dist/ssh/provisioner/http.d.ts.map +1 -0
- package/dist/ssh/provisioner/http.js +72 -0
- package/dist/ssh/provisioner/http.js.map +1 -0
- package/dist/ssh/provisioner/machines.d.ts +48 -0
- package/dist/ssh/provisioner/machines.d.ts.map +1 -0
- package/dist/ssh/provisioner/machines.js +686 -0
- package/dist/ssh/provisioner/machines.js.map +1 -0
- package/dist/ssh/provisioner/model.d.ts +92 -0
- package/dist/ssh/provisioner/model.d.ts.map +1 -0
- package/dist/ssh/provisioner/model.js +37 -0
- package/dist/ssh/provisioner/model.js.map +1 -0
- package/dist/ssh/ssh.d.ts +14 -11
- package/dist/ssh/ssh.d.ts.map +1 -1
- package/dist/ssh/ssh.js +94 -27
- package/dist/ssh/ssh.js.map +1 -1
- package/dist/ssh/worker-records.d.ts +18 -0
- package/dist/ssh/worker-records.d.ts.map +1 -0
- package/dist/ssh/worker-records.js +100 -0
- package/dist/ssh/worker-records.js.map +1 -0
- package/dist/ssh/workers.d.ts +34 -7
- package/dist/ssh/workers.d.ts.map +1 -1
- package/dist/ssh/workers.js +92 -18
- package/dist/ssh/workers.js.map +1 -1
- package/package.json +19 -11
package/dist/remote/remote.js
CHANGED
|
@@ -4,13 +4,14 @@ import { InfraFailure } from "@usine/core/attempt/failure";
|
|
|
4
4
|
import { RepositoryName, safeSegment } from "@usine/core/repository";
|
|
5
5
|
import { harvestFacts, harvestPostedComments, } from "@usine/core/step/harvest";
|
|
6
6
|
import { AttemptRuntime } from "@usine/durable/attempt-runtime";
|
|
7
|
-
import { Deferred, Effect, Exit, Option, Schema, Scope } from "effect";
|
|
7
|
+
import { Deferred, Effect, Exit, Option, Schema, Scope, SynchronizedRef } from "effect";
|
|
8
8
|
import { durableSessionFor } from "../durable-session.js";
|
|
9
9
|
import { CheckoutRequest, Executor, ExecutorError, ExecutorOperationError, WorkerCapabilityError, } from "../executor.js";
|
|
10
10
|
import { verifyHarnessReadiness } from "../harness-preflight.js";
|
|
11
|
+
import { childEnvironment, commitIdentity } from "../child-environment.js";
|
|
11
12
|
import { keyedLockFor } from "../keyed-lock.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
13
|
+
import { cacheDirectoryName, keptCheckoutRecovery, linkedWorktree, provisionWorktree, reattachWorktree, recoverWorktree, recoveryFields, } from "../provision.js";
|
|
14
|
+
import { attemptContainerName, containerCheckoutMarker, containersFor, } from "./container.js";
|
|
14
15
|
import { workerLeftoversFor } from "./leftovers.js";
|
|
15
16
|
import { workerWatchesFor } from "./liveness.js";
|
|
16
17
|
import { abandonedWorktreeSweepFor } from "./sweep.js";
|
|
@@ -23,10 +24,31 @@ mkdir -p "$(dirname "$1")"
|
|
|
23
24
|
printf '%s' "$2" > "$1"
|
|
24
25
|
chmod 755 "$1"
|
|
25
26
|
`.trim();
|
|
27
|
+
/**
|
|
28
|
+
* A script that creates the checkout `$1` as its own repository at commit `$4`: objects come from
|
|
29
|
+
* the cached clone `$2` through an alternate, and missing blobs from the partial-clone remote
|
|
30
|
+
* `$3`. The checkout writes only its own directory, so the cached clone can be read-only.
|
|
31
|
+
*/
|
|
32
|
+
const STANDALONE_CHECKOUT_SCRIPT = String.raw `
|
|
33
|
+
set -e
|
|
34
|
+
git init -q "$1"
|
|
35
|
+
printf '%s\n' "$2/objects" > "$1/.git/objects/info/alternates"
|
|
36
|
+
git -C "$1" config core.repositoryformatversion 1
|
|
37
|
+
git -C "$1" config extensions.partialClone origin
|
|
38
|
+
git -C "$1" remote add origin "$3"
|
|
39
|
+
git -C "$1" config remote.origin.promisor true
|
|
40
|
+
git -C "$1" config remote.origin.partialclonefilter blob:none
|
|
41
|
+
git -C "$1" checkout -q --detach "$4"
|
|
42
|
+
`.trim();
|
|
43
|
+
/** A script that removes the checkout `$1`, then its container marker `$2`. */
|
|
44
|
+
const REMOVE_CHECKOUT_SCRIPT = 'rm -rf "$1" && rm -f "$2"';
|
|
45
|
+
/** A script that removes everything inside the directory `$1` and keeps the directory. */
|
|
46
|
+
const EMPTY_DIRECTORY_SCRIPT = 'find "$1" -mindepth 1 -maxdepth 1 -exec rm -rf {} +';
|
|
26
47
|
/**
|
|
27
48
|
* Returns the executor for Workers accessed over SSH. Each Attempt's Pi parent is a durable task
|
|
28
49
|
* in this process, and each child runs on the machine as one SSH command with a pid file next
|
|
29
|
-
* to the worktree. `worker` provides the machine, preflight,
|
|
50
|
+
* to the worktree, or inside the Attempt's container. `worker` provides the machine, preflight,
|
|
51
|
+
* isolation, and repository access.
|
|
30
52
|
*/
|
|
31
53
|
export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
32
54
|
const { commands, label } = worker;
|
|
@@ -38,9 +60,22 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
38
60
|
const session = yield* durableSessionFor({ interruptGrace: worker.interruptGrace });
|
|
39
61
|
const runtime = yield* AttemptRuntime;
|
|
40
62
|
const workerWatches = yield* workerWatchesFor(commands);
|
|
41
|
-
const
|
|
63
|
+
const containers = containersFor(commands);
|
|
64
|
+
const attemptMachine = worker.attemptMachine ??
|
|
65
|
+
((name, _attemptId) => worker.machineFor(name));
|
|
66
|
+
const leftoversOf = (attempt) => worker.leftovers === undefined
|
|
67
|
+
? workerLeftovers(attempt.worker).pipe(Effect.map((found) => Option.some(found)))
|
|
68
|
+
: worker.leftovers(attempt, workerLeftovers(attempt.worker));
|
|
69
|
+
const recovered = worker.recovered ?? (() => Effect.void);
|
|
70
|
+
const checkoutHeld = worker.checkoutHeld ?? (() => Effect.void);
|
|
71
|
+
/** Returns `removal` that records the checkout of `attemptId` as gone once it succeeds. */
|
|
72
|
+
const removing = (attemptId, removal) => Effect.tap(removal, () => checkoutHeld(attemptId, false));
|
|
73
|
+
const cachePath = (machine, repository) => `${machine.workerDirectory}/repos/${cacheDirectoryName(repository)}`;
|
|
74
|
+
const stopChildren = Effect.fn(span("stopChildren"))(function* (operation, machine, worktree, attemptId, isolation) {
|
|
42
75
|
const host = machine.shell.destination;
|
|
43
|
-
yield* Effect.annotateCurrentSpan({ host, worktree });
|
|
76
|
+
yield* Effect.annotateCurrentSpan({ host, worktree, isolation: isolation._tag });
|
|
77
|
+
if (isolation._tag === "Container")
|
|
78
|
+
return yield* containers.stopChildren(operation, machine.shell, attemptContainerName(attemptId), worktree);
|
|
44
79
|
const outcome = yield* commands.run(operation, machine.shell, "sh", [
|
|
45
80
|
"-c",
|
|
46
81
|
CHILD_KILL_SCRIPT,
|
|
@@ -53,74 +88,212 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
53
88
|
children: outcome.trim().split("\n"),
|
|
54
89
|
});
|
|
55
90
|
});
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Returns how commands run in `attemptId`'s existing checkout at `worktree`: in its container
|
|
93
|
+
* if the container marker, which no container can reach, records one, or as the SSH user if
|
|
94
|
+
* it records none. Nothing in the checkout decides, since an Attempt can change it. Fails if
|
|
95
|
+
* the Worker's `isolation` can't run the checkout that way: a container's checkout on a
|
|
96
|
+
* machine without Docker, or a checkout from before Docker, as the SSH user who can now use
|
|
97
|
+
* Docker.
|
|
98
|
+
*/
|
|
99
|
+
const isolationOfCheckout = Effect.fn(span("isolationOfCheckout"))(function* (operation, machine, attemptId, worktree, isolation) {
|
|
100
|
+
if (!worker.runsContainers)
|
|
101
|
+
return isolation;
|
|
102
|
+
const host = machine.shell.destination;
|
|
103
|
+
yield* Effect.annotateCurrentSpan({ host, worktree, isolation: isolation._tag });
|
|
104
|
+
const contained = yield* containers.marked(operation, machine.shell, containerCheckoutMarker(machine.workerDirectory, attemptId));
|
|
105
|
+
if (contained === (isolation._tag === "Container"))
|
|
106
|
+
return isolation;
|
|
107
|
+
const message = contained
|
|
108
|
+
? `The checkout ${worktree} was mounted in an attempt container, and the machine no longer has Docker, so usine runs no command in it as the SSH user. Install Docker, or salvage and remove the checkout by hand.`
|
|
109
|
+
: `The checkout ${worktree} is from before the machine had Docker, and the SSH user can use Docker, so usine runs no command in it as that user. Salvage and remove the checkout by hand.`;
|
|
110
|
+
yield* Effect.logError("Refused to run commands in the checkout", {
|
|
111
|
+
attemptId,
|
|
112
|
+
host,
|
|
113
|
+
worktree,
|
|
114
|
+
contained,
|
|
115
|
+
message,
|
|
116
|
+
});
|
|
117
|
+
return yield* new ExecutorOperationError({
|
|
118
|
+
operation,
|
|
119
|
+
message,
|
|
120
|
+
cause: new Error(contained ? "container marker without Docker" : "no container marker"),
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
/** Returns the container of an Attempt, which mounts its checkout and the paths of `access`. */
|
|
124
|
+
const containerSpec = (machine, settings, attempt, worktree, access, harnesses) => ({
|
|
125
|
+
name: attemptContainerName(attempt.attemptId),
|
|
126
|
+
settings,
|
|
127
|
+
worker: attempt.worker,
|
|
128
|
+
attemptId: Option.some(attempt.attemptId),
|
|
129
|
+
checkout: Option.some({ path: worktree, parent: `${machine.workerDirectory}/attempts` }),
|
|
130
|
+
readOnly: [cachePath(machine, attempt.repository), ...access.readOnlyPaths],
|
|
131
|
+
writable: access.writablePaths,
|
|
132
|
+
harnesses,
|
|
133
|
+
environment: access.environment,
|
|
134
|
+
secrets: access.secrets,
|
|
135
|
+
});
|
|
136
|
+
/**
|
|
137
|
+
* Returns where the Attempt's commands in its checkout run. In a container, the container
|
|
138
|
+
* starts on first use in `scope` and is removed when `scope` closes.
|
|
139
|
+
*/
|
|
140
|
+
const checkoutHostFor = Effect.fn(span("checkoutHost"))(function* (operation, scope, machine, attempt, worktree, access, isolation, harnesses) {
|
|
141
|
+
if (isolation._tag === "SharedUser") {
|
|
142
|
+
const shell = Effect.succeed(access.shell);
|
|
143
|
+
const shared = {
|
|
144
|
+
isolation,
|
|
145
|
+
shell,
|
|
146
|
+
salvageShell: shell,
|
|
147
|
+
recordOutOfMemory: () => Effect.void,
|
|
148
|
+
};
|
|
149
|
+
return shared;
|
|
150
|
+
}
|
|
151
|
+
const started = yield* SynchronizedRef.make(Option.none());
|
|
152
|
+
const spec = containerSpec(machine, isolation.settings, attempt, worktree, access, harnesses);
|
|
153
|
+
const marker = containerCheckoutMarker(machine.workerDirectory, attempt.attemptId);
|
|
154
|
+
const memory = yield* containers.trackMemory(machine.shell, spec);
|
|
155
|
+
const shell = SynchronizedRef.modifyEffect(started, (current) => Option.match(current, {
|
|
156
|
+
onSome: (shell) => Effect.succeed([shell, current]),
|
|
157
|
+
onNone: () => containers.mark(operation, machine.shell, marker).pipe(Effect.andThen(containers.start(operation, machine.shell, spec)),
|
|
158
|
+
// Added after the start's removal, so it runs before it.
|
|
159
|
+
Effect.tap(() => Effect.addFinalizer(() => memory.recordKills())), Scope.provide(scope), Effect.map((shell) => [shell, Option.some(shell)])),
|
|
160
|
+
}));
|
|
161
|
+
const contained = {
|
|
162
|
+
isolation,
|
|
163
|
+
shell,
|
|
164
|
+
salvageShell: yield* Effect.cached(Effect.tap(shell, () => memory.ensureRunning)),
|
|
165
|
+
recordOutOfMemory: memory.recordTo,
|
|
166
|
+
};
|
|
167
|
+
return contained;
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* Runs git on the machine with the Attempt's repository access and `author` as the author and
|
|
171
|
+
* committer, refreshing the credential first if it's about to expire.
|
|
172
|
+
*/
|
|
173
|
+
const accessGit = (operation, access, author, shell = Effect.succeed(access.shell)) => (args) => {
|
|
174
|
+
const environment = { ...access.gitEnvironment, ...commitIdentity(author) };
|
|
175
|
+
return access.refresh.pipe(Effect.andThen(shell), Effect.flatMap((on) => commands.run(operation, on, "git", args, { environment, scrub: access.scrub })));
|
|
176
|
+
};
|
|
177
|
+
const harvestCommand = (operation, worktree, access, shell) => (command, args) => access.refresh
|
|
178
|
+
.pipe(Effect.andThen(commands.run(operation, shell, command, args, {
|
|
63
179
|
cwd: worktree,
|
|
64
180
|
environment: access.environment,
|
|
65
181
|
scrub: access.scrub,
|
|
66
182
|
})))
|
|
67
183
|
.pipe(Effect.map((stdout) => (stdout === "" ? [] : stdout.replace(/\n$/u, "").split("\n"))));
|
|
68
|
-
|
|
69
|
-
|
|
184
|
+
/** Returns whether the checkout at `worktree` exists on `machine`. */
|
|
185
|
+
const checkoutExists = (machine, worktree) => Effect.gen(function* () {
|
|
186
|
+
const inspected = yield* commands.inspect("cleanup", machine.shell, "test", [
|
|
187
|
+
"-d",
|
|
188
|
+
worktree,
|
|
189
|
+
]);
|
|
190
|
+
if (inspected.exitCode === 1)
|
|
191
|
+
return false;
|
|
192
|
+
if (inspected.exitCode !== 0)
|
|
193
|
+
return yield* executorError("Can't inspect the recovery checkout", new Error(inspected.stderr));
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
196
|
+
/**
|
|
197
|
+
* Removes a container's checkout at `worktree` from the machine, then its container marker.
|
|
198
|
+
* It holds no registration in the cached clone.
|
|
199
|
+
*/
|
|
200
|
+
const removeStandalone = (operation, machine, attemptId, worktree) => Effect.asVoid(commands.run(operation, machine.shell, "sh", [
|
|
201
|
+
"-c",
|
|
202
|
+
REMOVE_CHECKOUT_SCRIPT,
|
|
203
|
+
"sh",
|
|
204
|
+
worktree,
|
|
205
|
+
containerCheckoutMarker(machine.workerDirectory, attemptId),
|
|
206
|
+
]));
|
|
207
|
+
/**
|
|
208
|
+
* Stops the children in `attempt`'s checkout at `worktree`, salvages it with a new
|
|
209
|
+
* credential, records the salvage through `record`, and removes the checkout once its work is
|
|
210
|
+
* pushed. See `recoverWorktree`.
|
|
211
|
+
*/
|
|
212
|
+
const reconcileCheckout = Effect.fn(span("reconcileCheckout"))(function* (attempt, machine, worktree, record, commitAuthor) {
|
|
213
|
+
yield* Effect.annotateCurrentSpan({ attemptId: attempt.attemptId, worker: attempt.worker });
|
|
214
|
+
const isolation = yield* isolationOfCheckout("cleanup", machine, attempt.attemptId, worktree, yield* worker.isolation(machine));
|
|
215
|
+
const reconcile = (git, removeCheckout) => recoverWorktree({ attempt, host: machine.shell.destination, worktree, git, removeCheckout }, record);
|
|
216
|
+
const removeCheckout = (git) => removing(attempt.attemptId, isolation._tag === "Container"
|
|
217
|
+
? removeStandalone("cleanup", machine, attempt.attemptId, worktree)
|
|
218
|
+
: Effect.asVoid(git([
|
|
219
|
+
"--git-dir",
|
|
220
|
+
cachePath(machine, attempt.repository),
|
|
221
|
+
"worktree",
|
|
222
|
+
"remove",
|
|
223
|
+
"--force",
|
|
224
|
+
worktree,
|
|
225
|
+
])));
|
|
226
|
+
if (attempt.succeeded || attempt.permissions === "read") {
|
|
227
|
+
yield* stopChildren("cleanup", machine, worktree, attempt.attemptId, isolation);
|
|
228
|
+
const git = (args) => commands.run("cleanup", machine.shell, "git", args);
|
|
229
|
+
yield* reconcile(git, removeCheckout(git));
|
|
230
|
+
if (isolation._tag === "Container")
|
|
231
|
+
yield* containers.remove(machine.shell, attemptContainerName(attempt.attemptId));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const author = yield* commitAuthor;
|
|
235
|
+
const grant = yield* worker.grantFor(attempt.repository);
|
|
236
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
237
|
+
const access = yield* grant.acquire(attempt.attemptId, attempt.worker, attempt.permissions, machine, "cleanup");
|
|
238
|
+
yield* stopChildren("cleanup", machine, worktree, attempt.attemptId, isolation);
|
|
239
|
+
const checkout = yield* checkoutHostFor("cleanup", yield* Effect.scope, machine, attempt, worktree, access, isolation, []);
|
|
240
|
+
const git = accessGit("cleanup", access, author, checkout.shell);
|
|
241
|
+
yield* reconcile(git, removeCheckout(git));
|
|
242
|
+
}));
|
|
243
|
+
});
|
|
244
|
+
const recover = Effect.fn(span("recover"))(function* (attempt, record, commitAuthor) {
|
|
245
|
+
yield* Effect.annotateCurrentSpan(recoveryFields(attempt));
|
|
70
246
|
yield* session.stopForRecovery(attempt);
|
|
71
|
-
const
|
|
247
|
+
const located = yield* leftoversOf(attempt);
|
|
248
|
+
if (Option.isNone(located)) {
|
|
249
|
+
yield* Effect.logInfo("The Attempt's machine is gone; nothing is left to reconcile", {
|
|
250
|
+
attemptId: attempt.attemptId,
|
|
251
|
+
worker: attempt.worker,
|
|
252
|
+
});
|
|
253
|
+
yield* session.recoverPostedComments(attempt, Option.none(), record);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const { machine, checkouts } = located.value;
|
|
72
257
|
const worktree = `${machine.workerDirectory}/attempts/${safeSegment(attempt.attemptId)}`;
|
|
73
|
-
|
|
74
|
-
const inspected = yield* commands.inspect("cleanup", machine.shell, "test", [
|
|
75
|
-
"-d",
|
|
76
|
-
worktree,
|
|
77
|
-
]);
|
|
78
|
-
if (inspected.exitCode === 1)
|
|
79
|
-
return false;
|
|
80
|
-
if (inspected.exitCode !== 0)
|
|
81
|
-
return yield* executorError("Can't inspect the recovery checkout", new Error(inspected.stderr));
|
|
82
|
-
return true;
|
|
83
|
-
});
|
|
258
|
+
yield* Effect.addFinalizer((exit) => recovered(attempt, Exit.isSuccess(exit)));
|
|
84
259
|
// Checks the cached listing first to avoid an SSH round trip per persisted Attempt.
|
|
85
|
-
const present = checkouts.has(worktree) && (yield*
|
|
260
|
+
const present = checkouts.has(worktree) && (yield* checkoutExists(machine, worktree));
|
|
261
|
+
yield* checkoutHeld(attempt.attemptId, present);
|
|
86
262
|
yield* session.recoverPostedComments(attempt, present
|
|
87
263
|
? Option.some((spec, windows) => Effect.scoped(Effect.gen(function* () {
|
|
88
264
|
const grant = yield* worker.grantFor(attempt.repository);
|
|
89
265
|
const access = yield* grant.acquire(attempt.attemptId, attempt.worker, attempt.permissions, machine, "cleanup");
|
|
90
|
-
|
|
266
|
+
const checkout = yield* checkoutHostFor("cleanup", yield* Effect.scope, machine, attempt, worktree, access, yield* isolationOfCheckout("cleanup", machine, attempt.attemptId, worktree, yield* worker.isolation(machine)), []);
|
|
267
|
+
return yield* harvestPostedComments(spec, harvestCommand("cleanup", worktree, access, yield* checkout.shell), windows);
|
|
91
268
|
})))
|
|
92
269
|
: Option.none(), record);
|
|
93
270
|
if (!present)
|
|
94
271
|
return;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
272
|
+
yield* reconcileCheckout(attempt, machine, worktree, record.salvage, commitAuthor);
|
|
273
|
+
}, Effect.scoped);
|
|
274
|
+
/**
|
|
275
|
+
* Reconciles the checkout at `worktree` on `machine` that the launch of `request` kept while
|
|
276
|
+
* its parent task's stop was unconfirmed. Does nothing if the checkout is gone.
|
|
277
|
+
*/
|
|
278
|
+
const reconcileKept = Effect.fn(span("reconcileKept"))(function* (request, machine, worktree, record) {
|
|
279
|
+
const fields = { attemptId: request.attemptId, worker: request.worker, worktree };
|
|
280
|
+
yield* Effect.annotateCurrentSpan(fields);
|
|
281
|
+
if (!(yield* checkoutExists(machine, worktree))) {
|
|
282
|
+
yield* Effect.logInfo("The kept checkout is gone; nothing to reconcile", fields);
|
|
105
283
|
return;
|
|
106
284
|
}
|
|
107
|
-
|
|
108
|
-
yield* Effect.scoped(Effect.gen(function* () {
|
|
109
|
-
const access = yield* grant.acquire(attempt.attemptId, attempt.worker, attempt.permissions, machine, "cleanup");
|
|
110
|
-
yield* stopChildren("cleanup", machine, worktree);
|
|
111
|
-
yield* reconcile(accessGit("cleanup", access));
|
|
112
|
-
}));
|
|
285
|
+
yield* reconcileCheckout(keptCheckoutRecovery(request), machine, worktree, record, Effect.succeed(request.commitAuthor));
|
|
113
286
|
});
|
|
114
|
-
const harvest = (workspace, access, windows) => harvestFacts(workspace.spec, harvestCommand("launch", workspace.worktree, access), windows).pipe(Effect.mapError((error) => new InfraFailure({
|
|
287
|
+
const harvest = (workspace, access, shell, windows) => harvestFacts(workspace.spec, harvestCommand("launch", workspace.worktree, access, shell), windows).pipe(Effect.mapError((error) => new InfraFailure({
|
|
115
288
|
message: `Can't harvest the attempt's checkout: ${error.message}`,
|
|
116
289
|
cause: error,
|
|
117
290
|
})));
|
|
118
291
|
/**
|
|
119
|
-
* Checks that a reattached Attempt's parent task was created for this worktree
|
|
120
|
-
*
|
|
121
|
-
* parent's RunSpec.
|
|
292
|
+
* Checks that a reattached Attempt's parent task was created for this worktree, the worktree
|
|
293
|
+
* still exists, and `isolation` can run commands in it, then stops children left from before
|
|
294
|
+
* the restart. Returns the parent's RunSpec.
|
|
122
295
|
*/
|
|
123
|
-
const takeOver = Effect.fn(span("takeOver"))(function* (request, machine, worktree) {
|
|
296
|
+
const takeOver = Effect.fn(span("takeOver"))(function* (request, machine, worktree, isolation) {
|
|
124
297
|
const { cwd, spec } = yield* session.storedRequest(request.attemptId);
|
|
125
298
|
if (cwd !== worktree)
|
|
126
299
|
return yield* executorError(`The reattached attempt's checkout isn't on the machine of this ${label} worker`, new Error(cwd));
|
|
@@ -129,37 +302,85 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
129
302
|
return yield* executorError("The reattached attempt's checkout no longer exists", new Error(worktree));
|
|
130
303
|
if (inspected.exitCode !== 0)
|
|
131
304
|
return yield* executorError("Can't inspect the reattached attempt's checkout", new Error(inspected.stderr.trim()));
|
|
132
|
-
yield*
|
|
305
|
+
yield* isolationOfCheckout("prepare", machine, request.attemptId, worktree, isolation);
|
|
306
|
+
yield* stopChildren("prepare", machine, worktree, request.attemptId, isolation);
|
|
133
307
|
return spec;
|
|
134
308
|
});
|
|
309
|
+
/**
|
|
310
|
+
* Returns the checkout operations of an Attempt in its own container: a standalone repository
|
|
311
|
+
* that borrows the cached clone's objects, with every git command run in the container.
|
|
312
|
+
*/
|
|
313
|
+
const containerCheckout = (request, machine, access, worktree, cache, url, checkout) => {
|
|
314
|
+
const inContainer = (operation, command, args) => access.refresh.pipe(
|
|
315
|
+
// The only cleanup command in the checkout is the salvage's git.
|
|
316
|
+
Effect.andThen(operation === "cleanup" ? checkout.salvageShell : checkout.shell), Effect.flatMap((shell) => commands.run(operation, shell, command, args, {
|
|
317
|
+
environment: { ...access.gitEnvironment, ...commitIdentity(request.commitAuthor) },
|
|
318
|
+
scrub: access.scrub,
|
|
319
|
+
})));
|
|
320
|
+
return {
|
|
321
|
+
add: (commit) => commands
|
|
322
|
+
.run("prepare", machine.shell, "mkdir", ["-p", worktree])
|
|
323
|
+
.pipe(Effect.andThen(inContainer("prepare", "sh", [
|
|
324
|
+
"-c",
|
|
325
|
+
STANDALONE_CHECKOUT_SCRIPT,
|
|
326
|
+
"sh",
|
|
327
|
+
worktree,
|
|
328
|
+
cache,
|
|
329
|
+
url,
|
|
330
|
+
commit,
|
|
331
|
+
])), Effect.asVoid),
|
|
332
|
+
// The container mounts the directory itself, so only its contents go.
|
|
333
|
+
discard: Effect.asVoid(commands.run("prepare", machine.shell, "sh", [
|
|
334
|
+
"-c",
|
|
335
|
+
EMPTY_DIRECTORY_SCRIPT,
|
|
336
|
+
"sh",
|
|
337
|
+
worktree,
|
|
338
|
+
])),
|
|
339
|
+
git: (operation, args) => inContainer(operation, "git", args),
|
|
340
|
+
writeExecutable: (target, content) => Effect.asVoid(inContainer("prepare", "sh", ["-c", WRITE_EXECUTABLE_SCRIPT, "sh", target, content])),
|
|
341
|
+
remove: (cleanup) => cleanup("worktree-directory", removing(request.attemptId, removeStandalone("cleanup", machine, request.attemptId, worktree))),
|
|
342
|
+
};
|
|
343
|
+
};
|
|
135
344
|
/** Returns the `WorkspaceOps` for the machine; git runs with the Attempt's repository access. */
|
|
136
|
-
const workspaceOps = (request, machine, access, layout) =>
|
|
137
|
-
|
|
138
|
-
"-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
345
|
+
const workspaceOps = (request, machine, access, layout, checkout) => {
|
|
346
|
+
const host = {
|
|
347
|
+
removePath: (operation, target) => Effect.asVoid(commands.run(operation, machine.shell, "rm", ["-rf", target])),
|
|
348
|
+
cacheExists: Effect.gen(function* () {
|
|
349
|
+
const inspected = yield* commands.inspect("prepare", machine.shell, "test", [
|
|
350
|
+
"-d",
|
|
351
|
+
layout.cache,
|
|
352
|
+
]);
|
|
353
|
+
if (inspected.exitCode === 0)
|
|
354
|
+
return true;
|
|
355
|
+
if (inspected.exitCode === 1)
|
|
356
|
+
return false;
|
|
357
|
+
return yield* executorError(`Can't inspect the cached clone on worker "${request.worker}"`, new Error(inspected.stderr.trim()));
|
|
358
|
+
}),
|
|
359
|
+
git: (operation, args) => accessGit(operation, access, request.commitAuthor)(args),
|
|
360
|
+
};
|
|
361
|
+
return {
|
|
362
|
+
...host,
|
|
363
|
+
ensureRoots: Effect.asVoid(commands.run("prepare", machine.shell, "mkdir", [
|
|
364
|
+
"-p",
|
|
365
|
+
`${machine.workerDirectory}/repos`,
|
|
366
|
+
`${machine.workerDirectory}/attempts`,
|
|
367
|
+
])),
|
|
368
|
+
promoteStaging: Effect.asVoid(commands.run("prepare", machine.shell, "mv", [layout.staging, layout.cache])),
|
|
369
|
+
checkout: checkout.isolation._tag === "Container"
|
|
370
|
+
? containerCheckout(request, machine, access, layout.worktree, layout.cache, worker.repositoryUrl(machine, request.repository), checkout)
|
|
371
|
+
: linkedCheckout(request.attemptId, linkedWorktree(host, layout, (target, content) => Effect.asVoid(commands.run("prepare", machine.shell, "sh", [
|
|
372
|
+
"-c",
|
|
373
|
+
WRITE_EXECUTABLE_SCRIPT,
|
|
374
|
+
"sh",
|
|
375
|
+
target,
|
|
376
|
+
content,
|
|
377
|
+
])))),
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
/** Returns `ops` whose removal records the checkout as gone once its directory is removed. */
|
|
381
|
+
const linkedCheckout = (attemptId, ops) => ({
|
|
382
|
+
...ops,
|
|
383
|
+
remove: (cleanup) => ops.remove((resource, effect) => cleanup(resource, resource === "worktree-directory" ? removing(attemptId, effect) : effect)),
|
|
163
384
|
});
|
|
164
385
|
const prepare = Effect.fn(span("prepare"))(function* (request) {
|
|
165
386
|
yield* Effect.annotateCurrentSpan({
|
|
@@ -187,13 +408,17 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
187
408
|
});
|
|
188
409
|
}
|
|
189
410
|
const grant = yield* worker.grantFor(request.repository);
|
|
190
|
-
const machine = yield*
|
|
191
|
-
yield*
|
|
411
|
+
const machine = yield* attemptMachine(request.worker, request.attemptId);
|
|
412
|
+
const isolation = yield* worker.isolation(machine);
|
|
413
|
+
const probeBrain = (shell) => verifyHarnessReadiness(request.worker, machine.provisioned, [request.agent.brain], worker.harnessProbe(machine, shell), logins);
|
|
414
|
+
// An Attempt container doesn't exist yet, so its probe runs once the checkout is prepared.
|
|
415
|
+
if (isolation._tag === "SharedUser")
|
|
416
|
+
yield* probeBrain(machine.shell);
|
|
192
417
|
const host = machine.shell.destination;
|
|
193
|
-
const cache =
|
|
418
|
+
const cache = cachePath(machine, request.repository);
|
|
194
419
|
const worktree = `${machine.workerDirectory}/attempts/${safeSegment(request.attemptId)}`;
|
|
195
420
|
const staging = `${cache}.cloning`;
|
|
196
|
-
yield* Effect.annotateCurrentSpan({ host });
|
|
421
|
+
yield* Effect.annotateCurrentSpan({ host, isolation: isolation._tag });
|
|
197
422
|
yield* Effect.logInfo(request.reattach
|
|
198
423
|
? `Reattaching the Attempt to its ${label} checkout`
|
|
199
424
|
: `Preparing ${label} Attempt workspace`, {
|
|
@@ -203,13 +428,18 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
203
428
|
repository: request.repository,
|
|
204
429
|
loop: request.loop,
|
|
205
430
|
permissions: request.permissions,
|
|
431
|
+
isolation: isolation._tag,
|
|
206
432
|
worktree,
|
|
207
433
|
});
|
|
208
434
|
yield* sweepAbandonedWorktrees(request.attemptId, request.worker, machine.shell, machine.workerDirectory);
|
|
209
435
|
// A reattachment reuses its checkout before any credential is granted.
|
|
210
436
|
const reattached = request.reattach
|
|
211
|
-
? Option.some(yield* takeOver(request, machine, worktree))
|
|
437
|
+
? Option.some(yield* takeOver(request, machine, worktree, isolation))
|
|
212
438
|
: Option.none();
|
|
439
|
+
const harness = Option.match(reattached, {
|
|
440
|
+
onNone: () => request.agent.brain.harness,
|
|
441
|
+
onSome: (spec) => spec.agent.brain.harness,
|
|
442
|
+
});
|
|
213
443
|
// A checkout kept after a failed remote stop never closes its scope, so its salvage result
|
|
214
444
|
// is completed here instead of by the checkout finalizer.
|
|
215
445
|
const salvaged = yield* Deferred.make();
|
|
@@ -220,7 +450,7 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
220
450
|
host,
|
|
221
451
|
});
|
|
222
452
|
// Closes the checkout scope only if stopping the remote children succeeds.
|
|
223
|
-
yield* stopChildren("cleanup", machine, resource.provisioned.workspace.worktree);
|
|
453
|
+
yield* stopChildren("cleanup", machine, resource.provisioned.workspace.worktree, request.attemptId, isolation);
|
|
224
454
|
yield* Scope.close(resource.scope, exit);
|
|
225
455
|
const salvage = yield* Effect.exit(resource.provisioned.salvaged);
|
|
226
456
|
yield* Deferred.done(salvaged, salvage);
|
|
@@ -231,11 +461,16 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
231
461
|
worktree,
|
|
232
462
|
message: error.message,
|
|
233
463
|
})), Effect.ignore, Effect.andThen(Deferred.succeed(salvaged, Option.none()))));
|
|
464
|
+
const prepareScope = yield* Effect.scope;
|
|
234
465
|
const acquire = Effect.gen(function* () {
|
|
235
466
|
// Credentials are scoped to the Attempt, not the checkout, so they are released exactly once
|
|
236
467
|
// even when checkout cleanup is skipped.
|
|
237
468
|
const access = yield* grant.acquire(request.attemptId, request.worker, request.permissions, machine, "prepare");
|
|
238
|
-
|
|
469
|
+
// Forked before the checkout's release is registered, so the container is removed after
|
|
470
|
+
// the release salvages the checkout in it.
|
|
471
|
+
const containerScope = yield* Scope.fork(prepareScope);
|
|
472
|
+
const checkoutHost = yield* checkoutHostFor("prepare", containerScope, machine, request, worktree, access, isolation, [harness]);
|
|
473
|
+
const ops = workspaceOps(request, machine, access, { cache, staging, worktree }, checkoutHost);
|
|
239
474
|
const checkout = Option.match(reattached, {
|
|
240
475
|
onNone: () => provisionWorktree({
|
|
241
476
|
request,
|
|
@@ -244,7 +479,7 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
244
479
|
layout: { cache, staging, worktree },
|
|
245
480
|
ops,
|
|
246
481
|
}),
|
|
247
|
-
onSome: (spec) => reattachWorktree({ request, host, layout: {
|
|
482
|
+
onSome: (spec) => reattachWorktree({ request, host, layout: { worktree }, ops, spec }),
|
|
248
483
|
});
|
|
249
484
|
const { provisioned } = yield* Effect.acquireRelease(Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
250
485
|
const scope = yield* Scope.make();
|
|
@@ -253,11 +488,14 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
253
488
|
const provisioned = yield* restore(checkout).pipe(Scope.provide(scope), Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))));
|
|
254
489
|
return { provisioned, scope };
|
|
255
490
|
})), releaseWorkspace, { interruptible: true });
|
|
256
|
-
return { provisioned, access };
|
|
491
|
+
return { provisioned, access, checkoutHost };
|
|
257
492
|
});
|
|
258
|
-
const { provisioned, access } = yield* (request.reattach ? acquire : fetches.withLock(request.repository)(acquire)).pipe(Effect.mapError((cause) => isExecutorError(cause)
|
|
493
|
+
const { provisioned, access, checkoutHost } = yield* (request.reattach ? acquire : fetches.withLock(request.repository)(acquire)).pipe(Effect.mapError((cause) => isExecutorError(cause)
|
|
259
494
|
? cause
|
|
260
495
|
: executorError(`Can't prepare the ${label} attempt workspace`, cause)));
|
|
496
|
+
yield* checkoutHeld(request.attemptId, true);
|
|
497
|
+
if (isolation._tag === "Container")
|
|
498
|
+
yield* probeBrain(yield* checkoutHost.shell);
|
|
261
499
|
const prepared = provisioned.workspace;
|
|
262
500
|
yield* Effect.logInfo(`${label} Attempt workspace ready`, {
|
|
263
501
|
attemptId: request.attemptId,
|
|
@@ -267,6 +505,7 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
267
505
|
baseCommit: prepared.spec.baseCommit,
|
|
268
506
|
resume: Option.map(prepared.spec.resume, ({ ref }) => ref),
|
|
269
507
|
reattach: request.reattach,
|
|
508
|
+
isolation: isolation._tag,
|
|
270
509
|
worktree,
|
|
271
510
|
});
|
|
272
511
|
return {
|
|
@@ -275,6 +514,11 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
275
514
|
// Uses the RunSpec's brain, including on reattachment. Each child reads the kind when it
|
|
276
515
|
// starts, so a preflight during the Attempt takes effect.
|
|
277
516
|
const gatewayCredential = worker.gatewayCredential(machine, prepared.spec.agent.brain.harness);
|
|
517
|
+
yield* checkoutHost.recordOutOfMemory(io.outOfMemory);
|
|
518
|
+
const shell = yield* checkoutHost.shell.pipe(Effect.mapError((error) => new InfraFailure({
|
|
519
|
+
message: `Can't start the attempt's container: ${error.message}`,
|
|
520
|
+
cause: error,
|
|
521
|
+
})));
|
|
278
522
|
yield* workerWatches.watch({
|
|
279
523
|
attemptId: request.attemptId,
|
|
280
524
|
worker: request.worker,
|
|
@@ -282,12 +526,15 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
282
526
|
report: (notice) => runtime.notify(request.attemptId, notice),
|
|
283
527
|
});
|
|
284
528
|
return yield* session.launch(prepared, io, {
|
|
285
|
-
host:
|
|
286
|
-
|
|
529
|
+
host: shell,
|
|
530
|
+
machine: machine.provisioned,
|
|
531
|
+
environment: childEnvironment(access.environment, request.commitAuthor, provisioned.gitHooks),
|
|
287
532
|
gatewayCredential,
|
|
288
|
-
harvest: (windows) => harvest(prepared, access, windows),
|
|
289
|
-
harvestComments: (windows) => harvestPostedComments(prepared.spec, harvestCommand("launch", prepared.worktree, access), windows),
|
|
533
|
+
harvest: (windows) => harvest(prepared, access, shell, windows),
|
|
534
|
+
harvestComments: (windows) => harvestPostedComments(prepared.spec, harvestCommand("launch", prepared.worktree, access, shell), windows),
|
|
290
535
|
keepForReattach: provisioned.keepForReattach,
|
|
536
|
+
keepUntilStopped: provisioned.keepUntilStopped,
|
|
537
|
+
reconcileKept: (record) => reconcileKept(request, machine, worktree, record),
|
|
291
538
|
});
|
|
292
539
|
})),
|
|
293
540
|
salvaged: Deferred.await(salvaged),
|
|
@@ -303,6 +550,7 @@ export const remoteExecutorFor = (worker) => Effect.gen(function* () {
|
|
|
303
550
|
outcome: session.outcome,
|
|
304
551
|
preflight: worker.preflight,
|
|
305
552
|
prepare,
|
|
553
|
+
sweep: worker.sweep ?? Effect.void,
|
|
306
554
|
});
|
|
307
555
|
});
|
|
308
556
|
//# sourceMappingURL=remote.js.map
|