@gjermundgaraba/clankerbox-rivet-sandbox-agent 0.15.5 → 0.16.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 +50 -149
- package/package.json +6 -6
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
|
@@ -2,49 +2,41 @@ 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
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
import { Client, Internal, Prebuild, Precondition, loadProfile, pinnedSsh,
|
|
5
|
+
import { Client, Internal, Prebuild, Precondition, actionsByName, loadProfile, pinnedSsh, specRequest, sshTarget } from "@gjermundgaraba/clankerbox-sdk";
|
|
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
18
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
19
|
+
* The tunnel's ssh runs `cat` on a pipe this process holds, so it ends however this process ends.
|
|
20
|
+
* Neither keeps the event loop alive; that takes `node:child_process`, as Effect's process handle
|
|
21
|
+
* can't unref the pipe.
|
|
22
22
|
*/
|
|
23
23
|
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
|
-
*/
|
|
24
|
+
/** A failure on this machine's side, such as a taken port, which no start of the guest changes. */
|
|
28
25
|
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
|
-
*/
|
|
26
|
+
/** ssh's default ConnectTimeout is the system's TCP timeout, minutes; the SDK waits on this. */
|
|
33
27
|
const openWait = Duration.seconds(30);
|
|
34
|
-
/**
|
|
28
|
+
/** A login takes about a second. */
|
|
35
29
|
const openPause = Duration.millis(100);
|
|
36
|
-
/**
|
|
30
|
+
/** ssh ends in milliseconds once told to. */
|
|
37
31
|
const closeWait = Duration.seconds(5);
|
|
38
|
-
/** A loopback port nothing listens on now. */
|
|
39
32
|
const freePort = Effect.callback((resume) => {
|
|
40
33
|
const server = createServer();
|
|
41
|
-
server.once("error", (error) => resume(Effect.fail(new
|
|
34
|
+
server.once("error", (error) => resume(Effect.fail(new LocalTunnelError({ message: `couldn't find a free port: ${error.message}` }))));
|
|
42
35
|
server.listen(0, loopback, () => {
|
|
43
36
|
const address = server.address();
|
|
44
|
-
server.close(() => resume(address === null || Predicate.isString(address) ? Effect.fail(new
|
|
37
|
+
server.close(() => resume(address === null || Predicate.isString(address) ? Effect.fail(new LocalTunnelError({ message: "couldn't find a free port" })) : Effect.succeed(address.port)));
|
|
45
38
|
});
|
|
46
39
|
});
|
|
47
|
-
/** Whether something accepts a connection on the loopback port. */
|
|
48
40
|
const listening = (port) => Effect.callback((resume) => {
|
|
49
41
|
const socket = connect({
|
|
50
42
|
host: loopback,
|
|
@@ -57,14 +49,9 @@ const listening = (port) => Effect.callback((resume) => {
|
|
|
57
49
|
socket.once("error", () => resume(Effect.succeed(false)));
|
|
58
50
|
return Effect.sync(() => socket.destroy());
|
|
59
51
|
});
|
|
60
|
-
/** The port the Sandbox Agent listens on in the guest: its default, which the recipes keep. */
|
|
61
|
-
const agentPort = 2468;
|
|
62
52
|
/**
|
|
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.
|
|
53
|
+
* No connection master, which would outlive the forward, and keepalives that end it when the
|
|
54
|
+
* guest stops answering. These precede `extra`: OpenSSH keeps an option's first value.
|
|
68
55
|
*/
|
|
69
56
|
const forwarding = (port, extra) => [
|
|
70
57
|
"-T",
|
|
@@ -85,12 +72,9 @@ const forwarding = (port, extra) => [
|
|
|
85
72
|
`${loopback}:${port}:${loopback}:${agentPort}`,
|
|
86
73
|
"cat"
|
|
87
74
|
];
|
|
88
|
-
/**
|
|
75
|
+
/** Node sets an exit code for a child that failed to start too. */
|
|
89
76
|
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
|
-
*/
|
|
77
|
+
/** A program that couldn't be run emits `error`, with no `exit`. */
|
|
94
78
|
const exited = (child) => Effect.callback((resume) => {
|
|
95
79
|
if (over(child)) return resume(Effect.void);
|
|
96
80
|
const done = () => resume(Effect.void);
|
|
@@ -102,10 +86,8 @@ const exited = (child) => Effect.callback((resume) => {
|
|
|
102
86
|
});
|
|
103
87
|
});
|
|
104
88
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* the last of what it printed, and one that couldn't be run with why. A port another program
|
|
108
|
-
* holds is refused first: the wait for the port to listen can't tell whose listener it finds.
|
|
89
|
+
* A port another program holds is refused first: the wait for the port to listen can't tell whose
|
|
90
|
+
* listener it finds. Closing the scope waits for ssh's exit, so the port is free again.
|
|
109
91
|
*/
|
|
110
92
|
const open = (start) => Effect.gen(function* () {
|
|
111
93
|
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 +132,17 @@ const open = (start) => Effect.gen(function* () {
|
|
|
150
132
|
});
|
|
151
133
|
//#endregion
|
|
152
134
|
//#region src/sandboxes.ts
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
* `/etc/clankerbox/start` launches it on the guest's loopback at every activation, as the
|
|
160
|
-
* recipes beside this package do. A machine exposes only guest port 22, so the provider reaches
|
|
161
|
-
* the agent through an ssh port forward, pinned to the machine's host key, and gives the SDK a
|
|
162
|
-
* loopback URL of this machine.
|
|
163
|
-
*/
|
|
164
|
-
/** The provider's name, which prefixes every sandbox ID the SDK hands out. */
|
|
135
|
+
/** The calls that make a machine, whose failure may leave it on its host. */
|
|
136
|
+
const makers = [
|
|
137
|
+
actionsByName.createMachine,
|
|
138
|
+
actionsByName.forkMachine,
|
|
139
|
+
actionsByName.restoreMachine
|
|
140
|
+
];
|
|
165
141
|
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
|
-
*/
|
|
142
|
+
/** The agent answers on loopback in milliseconds, so a slower one counts as not answering. */
|
|
170
143
|
const healthWait = Duration.seconds(3);
|
|
171
144
|
const Health = Schema.Struct({ status: Schema.Literal("ok") });
|
|
172
145
|
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
146
|
const make = (options, ssh = "ssh") => {
|
|
178
147
|
const sandboxes = /* @__PURE__ */ new Map();
|
|
179
148
|
const sandbox = (id) => {
|
|
@@ -185,16 +154,11 @@ const make = (options, ssh = "ssh") => {
|
|
|
185
154
|
sandboxes.set(id, known);
|
|
186
155
|
return known;
|
|
187
156
|
};
|
|
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
|
-
*/
|
|
157
|
+
/** A sandbox's operations never overlap: the host would refuse a second start or stop as busy. */
|
|
193
158
|
const locked = (id, use) => Effect.suspend(() => {
|
|
194
159
|
const known = sandbox(id);
|
|
195
160
|
return Semaphore.withPermit(known.lock, Effect.suspend(() => use(known)));
|
|
196
161
|
});
|
|
197
|
-
/** The sandbox's port: its own, or a free one no other sandbox of this provider keeps. */
|
|
198
162
|
const portOf = (known) => Effect.gen(function* () {
|
|
199
163
|
if (known.port !== void 0) return known.port;
|
|
200
164
|
const taken = (port) => [...sandboxes.values()].some((other) => other.port === port);
|
|
@@ -203,16 +167,12 @@ const make = (options, ssh = "ssh") => {
|
|
|
203
167
|
known.port = port;
|
|
204
168
|
return port;
|
|
205
169
|
});
|
|
206
|
-
/** Ends the sandbox's tunnel, if it has one. */
|
|
207
170
|
const closed = (known) => Effect.suspend(() => {
|
|
208
171
|
const was = known.held;
|
|
209
172
|
known.held = void 0;
|
|
210
173
|
return was === void 0 ? Effect.void : Scope.close(was.scope, Exit.void);
|
|
211
174
|
});
|
|
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
|
-
*/
|
|
175
|
+
/** The machine is read for each tunnel, as its endpoint changes: on boat at every start. */
|
|
216
176
|
const opened = (id, known) => Effect.gen(function* () {
|
|
217
177
|
if (known.held?.tunnel.running() === true) return yield* portOf(known);
|
|
218
178
|
yield* closed(known);
|
|
@@ -237,105 +197,61 @@ const make = (options, ssh = "ssh") => {
|
|
|
237
197
|
};
|
|
238
198
|
return port;
|
|
239
199
|
});
|
|
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({
|
|
200
|
+
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
201
|
duration: healthWait,
|
|
243
202
|
orElse: () => Effect.succeed(false)
|
|
244
203
|
}), 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
|
-
*/
|
|
204
|
+
/** A start reruns a running machine's start hook and may move its endpoint: the tunnel ends. */
|
|
251
205
|
const started = (id, known) => Effect.gen(function* () {
|
|
252
206
|
yield* (yield* Client.Client).start(id);
|
|
253
207
|
yield* closed(known);
|
|
254
208
|
}).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
209
|
const made = (named) => Effect.gen(function* () {
|
|
260
210
|
const client = yield* Client.Client;
|
|
261
211
|
const { from } = options;
|
|
262
212
|
if ("checkpoint" in from) return yield* client.restore(from.checkpoint, named);
|
|
263
213
|
if ("machine" in from) return yield* client.fork(from.machine, named);
|
|
264
|
-
const {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
};
|
|
269
|
-
if (setup !== void 0) {
|
|
270
|
-
const { path, ...timeout } = setup;
|
|
271
|
-
spec = {
|
|
272
|
-
...spec,
|
|
273
|
-
setup: {
|
|
274
|
-
script: yield* readSetup(path),
|
|
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 });
|
|
214
|
+
const { spec, host } = yield* specRequest("create", yield* loadProfile(from.profile), {});
|
|
215
|
+
const { machine } = yield* Prebuild.create(client, named, spec, {
|
|
216
|
+
host,
|
|
217
|
+
fresh: options.fresh
|
|
218
|
+
});
|
|
281
219
|
return machine;
|
|
282
220
|
});
|
|
283
221
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
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`.
|
|
222
|
+
* Only a failed create, fork or restore leaves its machine on the host, and then its error names
|
|
223
|
+
* it. A placement or prebuild read failure left nothing. A failed delete leaves it to the user.
|
|
288
224
|
*/
|
|
289
|
-
const discard = (
|
|
225
|
+
const discard = (error) => Effect.gen(function* () {
|
|
290
226
|
const client = yield* Client.Client;
|
|
291
|
-
|
|
292
|
-
yield*
|
|
227
|
+
if (error.id !== void 0 && makers.some(({ name }) => name === error.action)) yield* Effect.ignore(client.delete(error.id));
|
|
228
|
+
return yield* error;
|
|
293
229
|
});
|
|
294
230
|
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
231
|
create: Effect.suspend(() => {
|
|
301
232
|
const named = `${options.namePrefix ?? "sandbox"}-${randomBytes(4).toString("hex")}`;
|
|
302
|
-
const discarding = (error) => Effect.andThen(discard(named), Effect.fail(error));
|
|
303
233
|
return made(named).pipe(Effect.map((machine) => machine.id), Effect.catchTags({
|
|
304
|
-
Precondition:
|
|
305
|
-
Internal:
|
|
306
|
-
Unavailable:
|
|
234
|
+
Precondition: discard,
|
|
235
|
+
Internal: discard,
|
|
236
|
+
Unavailable: discard
|
|
307
237
|
}));
|
|
308
238
|
}),
|
|
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
239
|
destroy: (id) => locked(id, (known) => Effect.gen(function* () {
|
|
315
240
|
const client = yield* Client.Client;
|
|
316
241
|
yield* closed(known);
|
|
317
242
|
yield* Effect.catchTag(client.delete(id), "NotFound", () => Effect.void);
|
|
318
243
|
known.port = void 0;
|
|
319
244
|
})),
|
|
320
|
-
/** Stops the machine: its disk stays, and `reconnect` starts it again. */
|
|
321
245
|
pause: (id) => locked(id, (known) => Effect.gen(function* () {
|
|
322
246
|
const client = yield* Client.Client;
|
|
323
247
|
yield* closed(known);
|
|
324
248
|
yield* client.stop(id);
|
|
325
249
|
})),
|
|
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
|
-
*/
|
|
250
|
+
/** The SDK's `ensureServer`, which follows, opens the tunnel. */
|
|
331
251
|
reconnect: (id) => locked(id, (known) => started(id, known)),
|
|
332
252
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
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.
|
|
253
|
+
* A local tunnel failure fails at once, since no start changes it. The SDK calls this only
|
|
254
|
+
* while it starts: a tunnel that ends later stays down until a caller calls it again.
|
|
339
255
|
*/
|
|
340
256
|
ensureServer: (id) => locked(id, (known) => Effect.gen(function* () {
|
|
341
257
|
const port = yield* opened(id, known).pipe(Effect.asSome, Effect.catchIf((error) => !(error instanceof LocalTunnelError), () => Effect.succeedNone));
|
|
@@ -343,22 +259,12 @@ const make = (options, ssh = "ssh") => {
|
|
|
343
259
|
yield* started(id, known);
|
|
344
260
|
yield* opened(id, known);
|
|
345
261
|
})),
|
|
346
|
-
/** The loopback URL that leads to the sandbox's agent. */
|
|
347
262
|
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
263
|
closeAll: Effect.suspend(() => Effect.forEach([...sandboxes.keys()], (id) => locked(id, closed), { discard: true }))
|
|
350
264
|
};
|
|
351
265
|
};
|
|
352
266
|
//#endregion
|
|
353
267
|
//#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
268
|
const provider = (options, layer, ssh) => {
|
|
363
269
|
const runtime = ManagedRuntime.make(layer);
|
|
364
270
|
const sandboxes = make(options, ssh);
|
|
@@ -380,13 +286,8 @@ const provider = (options, layer, ssh) => {
|
|
|
380
286
|
//#endregion
|
|
381
287
|
//#region src/index.ts
|
|
382
288
|
/**
|
|
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.
|
|
289
|
+
* Sandboxes on clankerbox machines, with IDs `clankerbox/<machine ID>`, so `sandboxId` reconnects
|
|
290
|
+
* to a machine. Their tunnels end with the process, or at `dispose`.
|
|
390
291
|
*/
|
|
391
292
|
const clankerbox = (options) => provider(options, Layer.mergeAll(NodeClient.layer(options.hosts), NodeServices.layer));
|
|
392
293
|
//#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.16.0",
|
|
4
4
|
"description": "clankerbox machines as sandboxes for Rivet's Sandbox Agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@gjermundgaraba/clankerbox-sdk": "0.
|
|
28
|
+
"@gjermundgaraba/clankerbox-sdk": "0.16.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@effect/platform-node": "4.0.
|
|
31
|
+
"@effect/platform-node": "4.0.4",
|
|
32
32
|
"@types/node": "26.6.4",
|
|
33
|
-
"effect": "4.0.
|
|
33
|
+
"effect": "4.0.4",
|
|
34
34
|
"sandbox-agent": "0.5.2",
|
|
35
35
|
"typescript": "7.0.2",
|
|
36
36
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
|
|
37
37
|
"vite-plus": "1.1.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@effect/platform-node": "~4.0.
|
|
41
|
-
"effect": "~4.0.
|
|
40
|
+
"@effect/platform-node": "~4.0.4",
|
|
41
|
+
"effect": "~4.0.4",
|
|
42
42
|
"sandbox-agent": "~0.5.2"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|