@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.
Files changed (2) hide show
  1. package/dist/index.mjs +31 -46
  2. 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 { randomBytes } from "node:crypto";
5
- import { Client, Internal, Prebuild, Precondition, actionsByName, loadProfile, pinnedSsh, specRequest, sshTarget } from "@gjermundgaraba/clankerbox-sdk";
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
- /** 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
- ];
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.machine(id));
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).start(id);
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.restore(from.checkpoint, named);
213
- if ("machine" in from) return yield* client.fork(from.machine, named);
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.id !== void 0 && makers.some(({ name }) => name === error.action)) yield* Effect.ignore(client.delete(error.id));
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"}-${randomBytes(4).toString("hex")}`;
233
- return made(named).pipe(Effect.map((machine) => machine.id), Effect.catchTags({
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* Effect.catchTag(client.delete(id), "NotFound", () => Effect.void);
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.stop(id);
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.16.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.16.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.1.0",
37
- "vite-plus": "1.1.0"
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",