@gjermundgaraba/clankerbox-rivet-sandbox-agent 0.15.6 → 0.18.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/dist/index.d.mts +14 -23
- package/dist/index.mjs +52 -166
- package/package.json +4 -4
package/dist/index.d.mts
CHANGED
|
@@ -2,9 +2,8 @@ import { Client } from "@gjermundgaraba/clankerbox-sdk";
|
|
|
2
2
|
import { SandboxProvider } from "sandbox-agent";
|
|
3
3
|
//#region src/options.d.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* restore carries its source's sessions and processes, so take either from an idle machine.
|
|
5
|
+
* A profile whose setup installs the Sandbox Agent, a checkpoint of a machine that has it, or a
|
|
6
|
+
* machine to fork. A fork or `ram` restore carries its source's sessions: take it from an idle one.
|
|
8
7
|
*/
|
|
9
8
|
type Source = {
|
|
10
9
|
readonly profile: string;
|
|
@@ -14,34 +13,27 @@ type Source = {
|
|
|
14
13
|
readonly machine: string;
|
|
15
14
|
};
|
|
16
15
|
interface Options {
|
|
17
|
-
/**
|
|
16
|
+
/** In placement order, as in the client config. */
|
|
18
17
|
readonly hosts: ReadonlyArray<Client.HostEntry>;
|
|
19
18
|
readonly from: Source;
|
|
20
|
-
/**
|
|
21
|
-
* Whether a profile's setup runs even when the sandbox's host has a prebuild of it. By default
|
|
22
|
-
* a create from a profile with a setup restores that prebuild when there is one, as
|
|
23
|
-
* `clankerbox create` does; `clankerbox prebuild --profile <the same file>` makes it.
|
|
24
|
-
*/
|
|
19
|
+
/** Runs a profile's setup even when the host has its prebuild, from `clankerbox prebuild`. */
|
|
25
20
|
readonly fresh?: boolean | undefined;
|
|
26
|
-
/**
|
|
21
|
+
/** `sandbox` by default. */
|
|
27
22
|
readonly namePrefix?: string | undefined;
|
|
28
|
-
/** The
|
|
23
|
+
/** The machine's own login user by default. */
|
|
29
24
|
readonly sshUser?: string | undefined;
|
|
30
25
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* ssh process, and one of those would outlive it.
|
|
26
|
+
* Such as `["-i", "/path/to/key"]`. Neither these nor ssh config may fork the forward (`-f`,
|
|
27
|
+
* `-N`, `ForkAfterAuthentication`, `SessionType none`) or give it to a connection master (`-M`,
|
|
28
|
+
* `-S`): the tunnel ends with its ssh, and those would outlive it.
|
|
35
29
|
*/
|
|
36
30
|
readonly sshArguments?: ReadonlyArray<string> | undefined;
|
|
37
|
-
/**
|
|
31
|
+
/** `/home/sandbox` by default, as the recipes make it. */
|
|
38
32
|
readonly cwd?: string | undefined;
|
|
39
33
|
}
|
|
40
34
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* The SDK's optional members it has are declared present, so a caller can call them; each takes
|
|
44
|
-
* the machine's ID, `sdk.sandboxId` without its `clankerbox/`.
|
|
35
|
+
* `dispose` ends the tunnels and leaves the machines; the SDK never calls it. Each method takes the
|
|
36
|
+
* machine's ID, `sdk.sandboxId` without its `clankerbox/`.
|
|
45
37
|
*/
|
|
46
38
|
interface ClankerboxProvider extends SandboxProvider {
|
|
47
39
|
reconnect(sandboxId: string): Promise<void>;
|
|
@@ -54,9 +46,8 @@ interface ClankerboxProvider extends SandboxProvider {
|
|
|
54
46
|
//#endregion
|
|
55
47
|
//#region src/index.d.ts
|
|
56
48
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* reconnects to a machine by its ID. Its tunnels end with the process; `dispose` ends them sooner.
|
|
49
|
+
* Sandboxes on clankerbox machines, with IDs `clankerbox/<machine ID>`, so `sandboxId` reconnects
|
|
50
|
+
* to a machine. Their tunnels end with the process, or at `dispose`.
|
|
60
51
|
*/
|
|
61
52
|
export declare const clankerbox: (options: Options) => ClankerboxProvider;
|
|
62
53
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -1,50 +1,34 @@
|
|
|
1
1
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
2
2
|
import * as NodeClient from "@gjermundgaraba/clankerbox-sdk/node";
|
|
3
3
|
import { Data, Duration, Effect, Exit, FileSystem, Layer, ManagedRuntime, Option, Predicate, Schedule, Schema, Scope, Semaphore } from "effect";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { ActionName, Client, Conflict, Internal, Prebuild, loadProfile, pinnedSsh, specRequest, sshTarget } from "@gjermundgaraba/clankerbox-sdk";
|
|
5
|
+
import { Hex } from "effect/encoding";
|
|
6
6
|
import { SandboxDestroyedError } from "sandbox-agent";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { closeSync, openSync } from "node:fs";
|
|
9
9
|
import { readFile } from "node:fs/promises";
|
|
10
10
|
import { Socket, connect, createServer } from "node:net";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
+
//#region src/agent.ts
|
|
13
|
+
/** The Sandbox Agent's default port, which the recipes keep, on the guest's loopback. */
|
|
14
|
+
const agentPort = 2468;
|
|
15
|
+
const healthPath = "/v1/health";
|
|
16
|
+
//#endregion
|
|
12
17
|
//#region src/tunnel.ts
|
|
13
|
-
/**
|
|
14
|
-
* A sandbox's tunnel: the system `ssh` forwarding a loopback port of this machine to the Sandbox
|
|
15
|
-
* Agent's port on the guest's loopback, over the machine's one published port, guest port 22.
|
|
16
|
-
*
|
|
17
|
-
* The tunnel runs `cat` in the guest and holds the pipe to its stdin, so it ends when this
|
|
18
|
-
* process does, however that ends: the pipe closes, `cat` reads its end, and ssh exits. Neither
|
|
19
|
-
* the child nor the pipe keeps the event loop alive, so a program that leaves a sandbox running
|
|
20
|
-
* still exits. That takes `node:child_process` itself: Effect's process handle has no way to
|
|
21
|
-
* unref the pipe.
|
|
22
|
-
*/
|
|
23
18
|
const loopback = "127.0.0.1";
|
|
24
|
-
/**
|
|
25
|
-
* A tunnel that couldn't open for a reason of this machine's: its port taken, ssh not runnable,
|
|
26
|
-
* its files not written. No start of the sandbox's machine changes it.
|
|
27
|
-
*/
|
|
19
|
+
/** A failure on this machine's side, such as a taken port, which no start of the guest changes. */
|
|
28
20
|
var LocalTunnelError = class extends Data.TaggedError("LocalTunnelError") {};
|
|
29
|
-
/**
|
|
30
|
-
* How long ssh may take to log in and listen: ssh's own default ConnectTimeout is the system's
|
|
31
|
-
* TCP timeout, minutes, and a tunnel the SDK waits on should fail sooner than that.
|
|
32
|
-
*/
|
|
33
21
|
const openWait = Duration.seconds(30);
|
|
34
|
-
/** How often the port is tried while ssh logs in: a login takes about a second. */
|
|
35
22
|
const openPause = Duration.millis(100);
|
|
36
|
-
/** How long ssh may take to end once told to, before it is killed: it ends in milliseconds. */
|
|
37
23
|
const closeWait = Duration.seconds(5);
|
|
38
|
-
/** A loopback port nothing listens on now. */
|
|
39
24
|
const freePort = Effect.callback((resume) => {
|
|
40
25
|
const server = createServer();
|
|
41
|
-
server.once("error", (error) => resume(Effect.fail(new
|
|
26
|
+
server.once("error", (error) => resume(Effect.fail(new LocalTunnelError({ message: `couldn't find a free port: ${error.message}` }))));
|
|
42
27
|
server.listen(0, loopback, () => {
|
|
43
28
|
const address = server.address();
|
|
44
|
-
server.close(() => resume(address === null || Predicate.isString(address) ? Effect.fail(new
|
|
29
|
+
server.close(() => resume(address === null || Predicate.isString(address) ? Effect.fail(new LocalTunnelError({ message: "couldn't find a free port" })) : Effect.succeed(address.port)));
|
|
45
30
|
});
|
|
46
31
|
});
|
|
47
|
-
/** Whether something accepts a connection on the loopback port. */
|
|
48
32
|
const listening = (port) => Effect.callback((resume) => {
|
|
49
33
|
const socket = connect({
|
|
50
34
|
host: loopback,
|
|
@@ -57,14 +41,9 @@ const listening = (port) => Effect.callback((resume) => {
|
|
|
57
41
|
socket.once("error", () => resume(Effect.succeed(false)));
|
|
58
42
|
return Effect.sync(() => socket.destroy());
|
|
59
43
|
});
|
|
60
|
-
/** The port the Sandbox Agent listens on in the guest: its default, which the recipes keep. */
|
|
61
|
-
const agentPort = 2468;
|
|
62
44
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* would outlive it; and keepalives end it when the guest stops answering without closing. These
|
|
66
|
-
* come before `extra`: OpenSSH keeps an option's first value, so neither `extra` nor the user's
|
|
67
|
-
* ssh config can change them.
|
|
45
|
+
* No connection master, which would outlive the forward, and keepalives that end it when the
|
|
46
|
+
* guest stops answering. These precede `extra`: OpenSSH keeps an option's first value.
|
|
68
47
|
*/
|
|
69
48
|
const forwarding = (port, extra) => [
|
|
70
49
|
"-T",
|
|
@@ -85,12 +64,7 @@ const forwarding = (port, extra) => [
|
|
|
85
64
|
`${loopback}:${port}:${loopback}:${agentPort}`,
|
|
86
65
|
"cat"
|
|
87
66
|
];
|
|
88
|
-
/** Whether the child has ended, or failed to start: Node sets an exit code for either. */
|
|
89
67
|
const over = (child) => child.exitCode !== null || child.signalCode !== null;
|
|
90
|
-
/**
|
|
91
|
-
* Waits for the child to end. A program that couldn't be run says so in an `error` event, with
|
|
92
|
-
* no `exit`.
|
|
93
|
-
*/
|
|
94
68
|
const exited = (child) => Effect.callback((resume) => {
|
|
95
69
|
if (over(child)) return resume(Effect.void);
|
|
96
70
|
const done = () => resume(Effect.void);
|
|
@@ -102,10 +76,10 @@ const exited = (child) => Effect.callback((resume) => {
|
|
|
102
76
|
});
|
|
103
77
|
});
|
|
104
78
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
79
|
+
* A port another program holds is refused first: the wait for the port to listen can't tell whose
|
|
80
|
+
* listener it finds. Closing the scope waits for ssh's exit, so the port is free again. The ssh
|
|
81
|
+
* runs `cat` on a pipe this process holds, so it ends however this process ends, and neither keeps
|
|
82
|
+
* the event loop alive.
|
|
109
83
|
*/
|
|
110
84
|
const open = (start) => Effect.gen(function* () {
|
|
111
85
|
if (yield* listening(start.port)) return yield* new LocalTunnelError({ message: `the tunnel to ${start.machine} can't use port ${start.port}: another program listens on it` });
|
|
@@ -150,30 +124,21 @@ const open = (start) => Effect.gen(function* () {
|
|
|
150
124
|
});
|
|
151
125
|
//#endregion
|
|
152
126
|
//#region src/sandboxes.ts
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
/** The provider's name, which prefixes every sandbox ID the SDK hands out. */
|
|
127
|
+
const makesMachine = {
|
|
128
|
+
createMachine: true,
|
|
129
|
+
forkMachine: true,
|
|
130
|
+
restoreMachine: true,
|
|
131
|
+
startMachine: false,
|
|
132
|
+
stopMachine: false,
|
|
133
|
+
deleteMachine: false,
|
|
134
|
+
captureCheckpoint: false,
|
|
135
|
+
deleteCheckpoint: false
|
|
136
|
+
};
|
|
137
|
+
const isActionName = Schema.is(ActionName);
|
|
165
138
|
const name = "clankerbox";
|
|
166
|
-
/**
|
|
167
|
-
* How long a health check through the tunnel may take: the agent answers on loopback in
|
|
168
|
-
* milliseconds, so one that takes longer is treated as not answering.
|
|
169
|
-
*/
|
|
170
139
|
const healthWait = Duration.seconds(3);
|
|
171
140
|
const Health = Schema.Struct({ status: Schema.Literal("ok") });
|
|
172
141
|
const isHealth = Schema.is(Health);
|
|
173
|
-
/**
|
|
174
|
-
* The sandboxes of one provider. `ssh` is the program its tunnels run. A sandbox's ID is its
|
|
175
|
-
* machine's ID.
|
|
176
|
-
*/
|
|
177
142
|
const make = (options, ssh = "ssh") => {
|
|
178
143
|
const sandboxes = /* @__PURE__ */ new Map();
|
|
179
144
|
const sandbox = (id) => {
|
|
@@ -185,16 +150,10 @@ const make = (options, ssh = "ssh") => {
|
|
|
185
150
|
sandboxes.set(id, known);
|
|
186
151
|
return known;
|
|
187
152
|
};
|
|
188
|
-
/**
|
|
189
|
-
* Runs `use` on the sandbox's record, holding its lock: each of the sandbox's operations runs
|
|
190
|
-
* whole under it, so two never overlap, as the host would refuse the second's start or stop
|
|
191
|
-
* while the first's runs.
|
|
192
|
-
*/
|
|
193
153
|
const locked = (id, use) => Effect.suspend(() => {
|
|
194
154
|
const known = sandbox(id);
|
|
195
155
|
return Semaphore.withPermit(known.lock, Effect.suspend(() => use(known)));
|
|
196
156
|
});
|
|
197
|
-
/** The sandbox's port: its own, or a free one no other sandbox of this provider keeps. */
|
|
198
157
|
const portOf = (known) => Effect.gen(function* () {
|
|
199
158
|
if (known.port !== void 0) return known.port;
|
|
200
159
|
const taken = (port) => [...sandboxes.values()].some((other) => other.port === port);
|
|
@@ -203,22 +162,16 @@ const make = (options, ssh = "ssh") => {
|
|
|
203
162
|
known.port = port;
|
|
204
163
|
return port;
|
|
205
164
|
});
|
|
206
|
-
/** Ends the sandbox's tunnel, if it has one. */
|
|
207
165
|
const closed = (known) => Effect.suspend(() => {
|
|
208
166
|
const was = known.held;
|
|
209
167
|
known.held = void 0;
|
|
210
168
|
return was === void 0 ? Effect.void : Scope.close(was.scope, Exit.void);
|
|
211
169
|
});
|
|
212
|
-
/**
|
|
213
|
-
* The sandbox's port, with its tunnel open: the one it has while that runs, and otherwise a
|
|
214
|
-
* new one. The machine is read for each tunnel, as its endpoint changes: on boat at every start.
|
|
215
|
-
*/
|
|
216
170
|
const opened = (id, known) => Effect.gen(function* () {
|
|
217
171
|
if (known.held?.tunnel.running() === true) return yield* portOf(known);
|
|
218
172
|
yield* closed(known);
|
|
219
173
|
const client = yield* Client.Client;
|
|
220
|
-
const target = sshTarget(yield* client.
|
|
221
|
-
if ("lacks" in target) return yield* new Precondition({ message: `${id} has no ${target.lacks}, which its tunnel needs: see clankerbox get ${id}` });
|
|
174
|
+
const target = yield* sshTarget(yield* client.getMachine({ id }));
|
|
222
175
|
const port = yield* portOf(known);
|
|
223
176
|
const scope = yield* Scope.make();
|
|
224
177
|
known.held = {
|
|
@@ -237,128 +190,66 @@ const make = (options, ssh = "ssh") => {
|
|
|
237
190
|
};
|
|
238
191
|
return port;
|
|
239
192
|
});
|
|
240
|
-
|
|
241
|
-
const healthy = (port) => Effect.tryPromise((signal) => fetch(`http://127.0.0.1:${port}/v1/health`, { signal }).then((reply) => reply.json())).pipe(Effect.map(isHealth), Effect.timeoutOrElse({
|
|
193
|
+
const healthy = (port) => Effect.tryPromise((signal) => fetch(`http://127.0.0.1:${port}${healthPath}`, { signal }).then((reply) => reply.json())).pipe(Effect.map(isHealth), Effect.timeoutOrElse({
|
|
242
194
|
duration: healthWait,
|
|
243
195
|
orElse: () => Effect.succeed(false)
|
|
244
196
|
}), Effect.catch(() => Effect.succeed(false)));
|
|
245
|
-
/**
|
|
246
|
-
* Starts the machine, which boots a stopped one and runs a running one's preparation again,
|
|
247
|
-
* and so its `/etc/clankerbox/start`. Its tunnel ends: the endpoint may have changed. A machine
|
|
248
|
-
* that no longer exists is the SDK's `SandboxDestroyedError`, for `reconnect` and
|
|
249
|
-
* `ensureServer` alike.
|
|
250
|
-
*/
|
|
251
197
|
const started = (id, known) => Effect.gen(function* () {
|
|
252
|
-
yield* (yield* Client.Client).
|
|
198
|
+
yield* (yield* Client.Client).startMachine({ id });
|
|
253
199
|
yield* closed(known);
|
|
254
200
|
}).pipe(Effect.catchTag("NotFound", (cause) => Effect.fail(new SandboxDestroyedError(id, name, { cause }))));
|
|
255
|
-
/**
|
|
256
|
-
* Makes the machine `named`, from the options' source. A profile with a setup is restored from
|
|
257
|
-
* its host's prebuild of it when there is one, as `clankerbox create` does, unless `fresh`.
|
|
258
|
-
*/
|
|
259
201
|
const made = (named) => Effect.gen(function* () {
|
|
260
202
|
const client = yield* Client.Client;
|
|
261
203
|
const { from } = options;
|
|
262
|
-
if ("checkpoint" in from) return yield* client.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
...timeout
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
if (options.fresh === true) return yield* client.create(named, spec, { host });
|
|
280
|
-
const { machine } = yield* Prebuild.create(client, named, spec, { host });
|
|
204
|
+
if ("checkpoint" in from) return yield* client.restoreMachine({
|
|
205
|
+
checkpoint: from.checkpoint,
|
|
206
|
+
name: named
|
|
207
|
+
});
|
|
208
|
+
if ("machine" in from) return yield* client.forkMachine({
|
|
209
|
+
machine: from.machine,
|
|
210
|
+
name: named
|
|
211
|
+
});
|
|
212
|
+
const { spec, host } = yield* specRequest("create", yield* loadProfile(from.profile), {});
|
|
213
|
+
const { machine } = yield* Prebuild.create(client, named, spec, {
|
|
214
|
+
host,
|
|
215
|
+
fresh: options.fresh
|
|
216
|
+
});
|
|
281
217
|
return machine;
|
|
282
218
|
});
|
|
283
|
-
|
|
284
|
-
* Tries to delete what a failed create may have left: a machine whose setup failed, or whose
|
|
285
|
-
* fork or restore failed natively, stays on its host, and only the provider knows its name. The
|
|
286
|
-
* host is the placement's to pick, so the machine is found by its name, which is random. A host
|
|
287
|
-
* that doesn't answer, or a delete that fails, leaves it to `clankerbox machines`.
|
|
288
|
-
*/
|
|
289
|
-
const discard = (named) => Effect.gen(function* () {
|
|
219
|
+
const discard = (error) => Effect.gen(function* () {
|
|
290
220
|
const client = yield* Client.Client;
|
|
291
|
-
|
|
292
|
-
yield*
|
|
221
|
+
if (error.made !== void 0 && isActionName(error.action) && makesMachine[error.action]) yield* Effect.ignore(client.deleteMachine({ id: error.made }));
|
|
222
|
+
return yield* error;
|
|
293
223
|
});
|
|
294
224
|
return {
|
|
295
|
-
/**
|
|
296
|
-
* Makes a sandbox's machine, and returns its ID once its create, fork or restore is done.
|
|
297
|
-
* Only the failures that can leave a machine behind discard it: a refusal, such as a name
|
|
298
|
-
* already taken, leaves nothing of this create's.
|
|
299
|
-
*/
|
|
300
225
|
create: Effect.suspend(() => {
|
|
301
|
-
const named = `${options.namePrefix ?? "sandbox"}-${
|
|
302
|
-
|
|
303
|
-
return made(named).pipe(Effect.map((machine) => machine.id), Effect.catchTags({
|
|
304
|
-
Precondition: discarding,
|
|
305
|
-
Internal: discarding,
|
|
306
|
-
Unavailable: discarding
|
|
307
|
-
}));
|
|
226
|
+
const named = `${options.namePrefix ?? "sandbox"}-${Hex.encode(globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(4)))}`;
|
|
227
|
+
return made(named).pipe(Effect.map((machine) => machine.id), Effect.catch((error) => error instanceof Conflict ? Effect.fail(error) : discard(error)));
|
|
308
228
|
}),
|
|
309
|
-
/**
|
|
310
|
-
* Deletes the machine, holding the sandbox's lock throughout. One that is already gone is
|
|
311
|
-
* destroyed. Its port goes only once it is: a delete that fails leaves the sandbox as it was,
|
|
312
|
-
* its tunnel aside.
|
|
313
|
-
*/
|
|
314
229
|
destroy: (id) => locked(id, (known) => Effect.gen(function* () {
|
|
315
230
|
const client = yield* Client.Client;
|
|
316
231
|
yield* closed(known);
|
|
317
|
-
yield*
|
|
232
|
+
yield* client.deleteMachine({ id });
|
|
318
233
|
known.port = void 0;
|
|
319
234
|
})),
|
|
320
|
-
/** Stops the machine: its disk stays, and `reconnect` starts it again. */
|
|
321
235
|
pause: (id) => locked(id, (known) => Effect.gen(function* () {
|
|
322
236
|
const client = yield* Client.Client;
|
|
323
237
|
yield* closed(known);
|
|
324
|
-
yield* client.
|
|
238
|
+
yield* client.stopMachine({ id });
|
|
325
239
|
})),
|
|
326
|
-
/**
|
|
327
|
-
* Starts the machine, which boots a stopped one and leaves a running one running. A machine
|
|
328
|
-
* that no longer exists is the SDK's `SandboxDestroyedError`. The SDK's `ensureServer`,
|
|
329
|
-
* which follows, opens the tunnel.
|
|
330
|
-
*/
|
|
331
240
|
reconnect: (id) => locked(id, (known) => started(id, known)),
|
|
332
|
-
/**
|
|
333
|
-
* Brings back what a failed health check may have lost. A tunnel that ended is opened again;
|
|
334
|
-
* if the agent still doesn't answer, or the tunnel can't open for a reason on the machine's
|
|
335
|
-
* side, the machine is started, whose start hook relaunches an agent that doesn't answer, and
|
|
336
|
-
* its tunnel opened anew. A tunnel that fails for a reason of this machine's fails at once,
|
|
337
|
-
* since no start changes it. The SDK calls this only while it starts: a tunnel that ends
|
|
338
|
-
* later stays down until a caller calls it again.
|
|
339
|
-
*/
|
|
340
241
|
ensureServer: (id) => locked(id, (known) => Effect.gen(function* () {
|
|
341
242
|
const port = yield* opened(id, known).pipe(Effect.asSome, Effect.catchIf((error) => !(error instanceof LocalTunnelError), () => Effect.succeedNone));
|
|
342
243
|
if (Option.isSome(port) && (yield* healthy(port.value))) return;
|
|
343
244
|
yield* started(id, known);
|
|
344
245
|
yield* opened(id, known);
|
|
345
246
|
})),
|
|
346
|
-
/** The loopback URL that leads to the sandbox's agent. */
|
|
347
247
|
url: (id) => locked(id, (known) => Effect.map(opened(id, known), (port) => `http://127.0.0.1:${port}`)),
|
|
348
|
-
/** Ends every tunnel. The machines stay as they are. */
|
|
349
248
|
closeAll: Effect.suspend(() => Effect.forEach([...sandboxes.keys()], (id) => locked(id, closed), { discard: true }))
|
|
350
249
|
};
|
|
351
250
|
};
|
|
352
251
|
//#endregion
|
|
353
252
|
//#region src/provider.ts
|
|
354
|
-
/**
|
|
355
|
-
* The `SandboxProvider` the Sandbox Agent SDK takes: the sandboxes' effects, run as promises
|
|
356
|
-
* over the layer that gives them the clankerbox client and the platform.
|
|
357
|
-
*/
|
|
358
|
-
/**
|
|
359
|
-
* The provider over `layer`, whose tunnels run `ssh`. Nothing is built until the first call.
|
|
360
|
-
* Tests give it an in-memory host and a stand-in for ssh.
|
|
361
|
-
*/
|
|
362
253
|
const provider = (options, layer, ssh) => {
|
|
363
254
|
const runtime = ManagedRuntime.make(layer);
|
|
364
255
|
const sandboxes = make(options, ssh);
|
|
@@ -380,13 +271,8 @@ const provider = (options, layer, ssh) => {
|
|
|
380
271
|
//#endregion
|
|
381
272
|
//#region src/index.ts
|
|
382
273
|
/**
|
|
383
|
-
* clankerbox machines
|
|
384
|
-
*
|
|
385
|
-
*/
|
|
386
|
-
/**
|
|
387
|
-
* A Sandbox Agent provider whose sandboxes are clankerbox machines on `options.hosts`. Each
|
|
388
|
-
* sandbox's ID is `clankerbox/<machine ID>`, so `SandboxAgent.start({ sandbox, sandboxId })`
|
|
389
|
-
* reconnects to a machine by its ID. Its tunnels end with the process; `dispose` ends them sooner.
|
|
274
|
+
* Sandboxes on clankerbox machines, with IDs `clankerbox/<machine ID>`, so `sandboxId` reconnects
|
|
275
|
+
* to a machine. Their tunnels end with the process, or at `dispose`.
|
|
390
276
|
*/
|
|
391
277
|
const clankerbox = (options) => provider(options, Layer.mergeAll(NodeClient.layer(options.hosts), NodeServices.layer));
|
|
392
278
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjermundgaraba/clankerbox-rivet-sandbox-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "clankerbox machines as sandboxes for Rivet's Sandbox Agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@gjermundgaraba/clankerbox-sdk": "0.
|
|
28
|
+
"@gjermundgaraba/clankerbox-sdk": "0.18.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@effect/platform-node": "4.0.4",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"effect": "4.0.4",
|
|
34
34
|
"sandbox-agent": "0.5.2",
|
|
35
35
|
"typescript": "7.0.2",
|
|
36
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@1.
|
|
37
|
-
"vite-plus": "1.
|
|
36
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.2.0",
|
|
37
|
+
"vite-plus": "1.2.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@effect/platform-node": "~4.0.4",
|