@gjermundgaraba/clankerbox-rivet-sandbox-agent 0.16.0 → 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.mjs +31 -46
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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";
|
|
@@ -15,19 +15,11 @@ const agentPort = 2468;
|
|
|
15
15
|
const healthPath = "/v1/health";
|
|
16
16
|
//#endregion
|
|
17
17
|
//#region src/tunnel.ts
|
|
18
|
-
/**
|
|
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
|
-
*/
|
|
23
18
|
const loopback = "127.0.0.1";
|
|
24
19
|
/** A failure on this machine's side, such as a taken port, which no start of the guest changes. */
|
|
25
20
|
var LocalTunnelError = class extends Data.TaggedError("LocalTunnelError") {};
|
|
26
|
-
/** ssh's default ConnectTimeout is the system's TCP timeout, minutes; the SDK waits on this. */
|
|
27
21
|
const openWait = Duration.seconds(30);
|
|
28
|
-
/** A login takes about a second. */
|
|
29
22
|
const openPause = Duration.millis(100);
|
|
30
|
-
/** ssh ends in milliseconds once told to. */
|
|
31
23
|
const closeWait = Duration.seconds(5);
|
|
32
24
|
const freePort = Effect.callback((resume) => {
|
|
33
25
|
const server = createServer();
|
|
@@ -72,9 +64,7 @@ const forwarding = (port, extra) => [
|
|
|
72
64
|
`${loopback}:${port}:${loopback}:${agentPort}`,
|
|
73
65
|
"cat"
|
|
74
66
|
];
|
|
75
|
-
/** Node sets an exit code for a child that failed to start too. */
|
|
76
67
|
const over = (child) => child.exitCode !== null || child.signalCode !== null;
|
|
77
|
-
/** A program that couldn't be run emits `error`, with no `exit`. */
|
|
78
68
|
const exited = (child) => Effect.callback((resume) => {
|
|
79
69
|
if (over(child)) return resume(Effect.void);
|
|
80
70
|
const done = () => resume(Effect.void);
|
|
@@ -87,7 +77,9 @@ const exited = (child) => Effect.callback((resume) => {
|
|
|
87
77
|
});
|
|
88
78
|
/**
|
|
89
79
|
* 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.
|
|
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.
|
|
91
83
|
*/
|
|
92
84
|
const open = (start) => Effect.gen(function* () {
|
|
93
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` });
|
|
@@ -132,14 +124,18 @@ const open = (start) => Effect.gen(function* () {
|
|
|
132
124
|
});
|
|
133
125
|
//#endregion
|
|
134
126
|
//#region src/sandboxes.ts
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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);
|
|
141
138
|
const name = "clankerbox";
|
|
142
|
-
/** The agent answers on loopback in milliseconds, so a slower one counts as not answering. */
|
|
143
139
|
const healthWait = Duration.seconds(3);
|
|
144
140
|
const Health = Schema.Struct({ status: Schema.Literal("ok") });
|
|
145
141
|
const isHealth = Schema.is(Health);
|
|
@@ -154,7 +150,6 @@ const make = (options, ssh = "ssh") => {
|
|
|
154
150
|
sandboxes.set(id, known);
|
|
155
151
|
return known;
|
|
156
152
|
};
|
|
157
|
-
/** A sandbox's operations never overlap: the host would refuse a second start or stop as busy. */
|
|
158
153
|
const locked = (id, use) => Effect.suspend(() => {
|
|
159
154
|
const known = sandbox(id);
|
|
160
155
|
return Semaphore.withPermit(known.lock, Effect.suspend(() => use(known)));
|
|
@@ -172,13 +167,11 @@ const make = (options, ssh = "ssh") => {
|
|
|
172
167
|
known.held = void 0;
|
|
173
168
|
return was === void 0 ? Effect.void : Scope.close(was.scope, Exit.void);
|
|
174
169
|
});
|
|
175
|
-
/** The machine is read for each tunnel, as its endpoint changes: on boat at every start. */
|
|
176
170
|
const opened = (id, known) => Effect.gen(function* () {
|
|
177
171
|
if (known.held?.tunnel.running() === true) return yield* portOf(known);
|
|
178
172
|
yield* closed(known);
|
|
179
173
|
const client = yield* Client.Client;
|
|
180
|
-
const target = sshTarget(yield* client.
|
|
181
|
-
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 }));
|
|
182
175
|
const port = yield* portOf(known);
|
|
183
176
|
const scope = yield* Scope.make();
|
|
184
177
|
known.held = {
|
|
@@ -201,16 +194,21 @@ const make = (options, ssh = "ssh") => {
|
|
|
201
194
|
duration: healthWait,
|
|
202
195
|
orElse: () => Effect.succeed(false)
|
|
203
196
|
}), Effect.catch(() => Effect.succeed(false)));
|
|
204
|
-
/** A start reruns a running machine's start hook and may move its endpoint: the tunnel ends. */
|
|
205
197
|
const started = (id, known) => Effect.gen(function* () {
|
|
206
|
-
yield* (yield* Client.Client).
|
|
198
|
+
yield* (yield* Client.Client).startMachine({ id });
|
|
207
199
|
yield* closed(known);
|
|
208
200
|
}).pipe(Effect.catchTag("NotFound", (cause) => Effect.fail(new SandboxDestroyedError(id, name, { cause }))));
|
|
209
201
|
const made = (named) => Effect.gen(function* () {
|
|
210
202
|
const client = yield* Client.Client;
|
|
211
203
|
const { from } = options;
|
|
212
|
-
if ("checkpoint" in from) return yield* client.
|
|
213
|
-
|
|
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
|
+
});
|
|
214
212
|
const { spec, host } = yield* specRequest("create", yield* loadProfile(from.profile), {});
|
|
215
213
|
const { machine } = yield* Prebuild.create(client, named, spec, {
|
|
216
214
|
host,
|
|
@@ -218,41 +216,28 @@ const make = (options, ssh = "ssh") => {
|
|
|
218
216
|
});
|
|
219
217
|
return machine;
|
|
220
218
|
});
|
|
221
|
-
/**
|
|
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.
|
|
224
|
-
*/
|
|
225
219
|
const discard = (error) => Effect.gen(function* () {
|
|
226
220
|
const client = yield* Client.Client;
|
|
227
|
-
if (error.
|
|
221
|
+
if (error.made !== void 0 && isActionName(error.action) && makesMachine[error.action]) yield* Effect.ignore(client.deleteMachine({ id: error.made }));
|
|
228
222
|
return yield* error;
|
|
229
223
|
});
|
|
230
224
|
return {
|
|
231
225
|
create: Effect.suspend(() => {
|
|
232
|
-
const named = `${options.namePrefix ?? "sandbox"}-${
|
|
233
|
-
return made(named).pipe(Effect.map((machine) => machine.id), Effect.
|
|
234
|
-
Precondition: discard,
|
|
235
|
-
Internal: discard,
|
|
236
|
-
Unavailable: discard
|
|
237
|
-
}));
|
|
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)));
|
|
238
228
|
}),
|
|
239
229
|
destroy: (id) => locked(id, (known) => Effect.gen(function* () {
|
|
240
230
|
const client = yield* Client.Client;
|
|
241
231
|
yield* closed(known);
|
|
242
|
-
yield*
|
|
232
|
+
yield* client.deleteMachine({ id });
|
|
243
233
|
known.port = void 0;
|
|
244
234
|
})),
|
|
245
235
|
pause: (id) => locked(id, (known) => Effect.gen(function* () {
|
|
246
236
|
const client = yield* Client.Client;
|
|
247
237
|
yield* closed(known);
|
|
248
|
-
yield* client.
|
|
238
|
+
yield* client.stopMachine({ id });
|
|
249
239
|
})),
|
|
250
|
-
/** The SDK's `ensureServer`, which follows, opens the tunnel. */
|
|
251
240
|
reconnect: (id) => locked(id, (known) => started(id, known)),
|
|
252
|
-
/**
|
|
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.
|
|
255
|
-
*/
|
|
256
241
|
ensureServer: (id) => locked(id, (known) => Effect.gen(function* () {
|
|
257
242
|
const port = yield* opened(id, known).pipe(Effect.asSome, Effect.catchIf((error) => !(error instanceof LocalTunnelError), () => Effect.succeedNone));
|
|
258
243
|
if (Option.isSome(port) && (yield* healthy(port.value))) return;
|
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",
|