zygo-sdk 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -1,7 +1,34 @@
1
1
  # zygo — Node client
2
2
 
3
- Warm, isolated sandboxes for function-shaped code. A warm function costs about
4
- a millisecond and gets a clean process per request.
3
+ Run code you did not write — a customer's script, a plugin, something an
4
+ LLM just generated — without letting it touch your host. Zygo keeps a warm
5
+ sandbox per runtime, forks a fresh process for each request inside
6
+ namespaces, cgroups and seccomp, and answers in about a millisecond.
7
+ This package is the Node side of that: register a script once, then call it
8
+ with JSON in and JSON out.
9
+
10
+ ## Before you start
11
+
12
+ This package is a client. The sandboxes are run by `zygo`, one static binary
13
+ for Linux; on macOS it runs everything in a Linux VM it manages, and the API
14
+ it starts there answers on the Mac at the same address. Install it as
15
+ [the project's README](https://github.com/mhmtskrc2/zygo#install) shows, then:
16
+
17
+ ```bash
18
+ zygo doctor # can this host run sandboxes?
19
+ zygo pull node:22-slim # the API never pulls an image itself
20
+ export ZYGO_API_TOKEN=$(openssl rand -hex 16) # `zygo api` will not start without one
21
+ zygo api --allow-deploy # 127.0.0.1:7700; leave it running
22
+ ```
23
+
24
+ `--allow-deploy` lets a client start functions and one-shot sandboxes.
25
+ The first example calls a function named `resize` that a `sandbox.toml`
26
+ declares and `zygo up` starts; [chapter 11 of the Zygo book](https://github.com/mhmtskrc2/zygo/blob/main/docs/book/11-getting-started.md)
27
+ walks through one. `connect()` finds the API at `ZYGO_API_URL`, or
28
+ `http://127.0.0.1:7700`, and sends `ZYGO_API_TOKEN`, so a program started
29
+ from the same shell needs no settings.
30
+
31
+ ## Using it
5
32
 
6
33
  ```bash
7
34
  npm install zygo-sdk
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zygo-sdk",
3
- "version": "0.1.3",
4
- "description": "Node client for Zygo — warm, isolated sandboxes for function-shaped code",
3
+ "version": "0.1.5",
4
+ "description": "Run untrusted code safely from Node: a client for Zygo, which gives every request a fresh, kernel-isolated sandbox in about a millisecond",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "./src/index.js",
package/src/index.d.ts CHANGED
@@ -395,7 +395,7 @@ export declare class Client {
395
395
 
396
396
  close(): void;
397
397
 
398
- version(): Promise<{ version: string; api: number; control: number; deploy: boolean }>;
398
+ version(): Promise<{ version: string; api: number; control: number; deploy: boolean; private_net: boolean }>;
399
399
  /** Needs no token. Throws {@link Unavailable} once the API is stopping. */
400
400
  health(): Promise<Health>;
401
401
  /**
package/src/index.js CHANGED
@@ -71,6 +71,15 @@ export {
71
71
  */
72
72
  const MAX_BODY = 64 * 1024 * 1024;
73
73
 
74
+ // How long a pooled connection may sit unused before the agent drops it
75
+ // rather than reuse it. `zygo api` closes a connection that has waited 30 s
76
+ // for its next request (hyper's header read timeout), so a pooled connection
77
+ // older than that is one the server has already hung up on. Ten seconds under
78
+ // it, so a slow network or a busy loop cannot close the gap. The agent applies
79
+ // this to a socket only while it is free: on reuse the request's own
80
+ // `timeout` takes over.
81
+ const POOL_IDLE_MS = 20_000;
82
+
74
83
  /**
75
84
  * A connection to a Zygo API.
76
85
  *
@@ -111,7 +120,8 @@ export class Client {
111
120
  // `agent` is how `forTenant` shares this pool rather than opening a
112
121
  // second one. Undocumented on purpose: it is an internal seam, not a
113
122
  // knob, and passing a foreign agent here is a way to lose keep-alive.
114
- this._agent = options.agent ?? new transport.Agent({ keepAlive: true, maxSockets: 64 });
123
+ this._agent =
124
+ options.agent ?? new transport.Agent({ keepAlive: true, maxSockets: 64, timeout: POOL_IDLE_MS });
115
125
  }
116
126
 
117
127
  /** Close every pooled connection. Calling it twice is harmless. */
@@ -852,13 +862,22 @@ export class Client {
852
862
  return Math.max(Number(error.retryAfter) || 0, this.backoff * 2 ** attempt);
853
863
  }
854
864
 
855
- /** One request and its answer, on a pooled connection. */
856
- #exchange(method, path, payload, sent) {
865
+ /**
866
+ * One request and its answer, on a pooled connection.
867
+ *
868
+ * A pooled connection may be one the server closed while it waited. If
869
+ * that shows before a byte of the answer arrives — the write fails, or the
870
+ * socket hangs up where the status line should be — the request did not
871
+ * run, so it goes once more on a fresh connection. Once, and only for a
872
+ * reused socket: a new one that fails is a real failure, reported as such.
873
+ * `fresh` is that second attempt.
874
+ */
875
+ #exchange(method, path, payload, sent, fresh = false) {
857
876
  const options = {
858
877
  method,
859
878
  path,
860
879
  headers: sent,
861
- agent: this._agent,
880
+ agent: fresh ? false : this._agent,
862
881
  timeout: this.timeout,
863
882
  };
864
883
  if (this.endpoint.isUnix) {
@@ -869,7 +888,9 @@ export class Client {
869
888
  }
870
889
 
871
890
  return new Promise((resolvePromise, reject) => {
891
+ let answered = false;
872
892
  const request = this._transport.request(options, (response) => {
893
+ answered = true;
873
894
  const chunks = [];
874
895
  let size = 0;
875
896
  response.on('data', (chunk) => {
@@ -899,6 +920,14 @@ export class Client {
899
920
  reject(new TransportError(`${method} ${path} got no answer within ${this.timeout} ms`));
900
921
  });
901
922
  request.on('error', (e) => {
923
+ // `reusedSocket` is Node's own word for a socket from the pool; a
924
+ // reset or a broken pipe on one, with nothing answered, is the
925
+ // server having closed it. Node recommends exactly this retry.
926
+ const lost = e.code === 'ECONNRESET' || e.code === 'EPIPE';
927
+ if (lost && request.reusedSocket && !answered && !fresh) {
928
+ resolvePromise(this.#exchange(method, path, payload, sent, true));
929
+ return;
930
+ }
902
931
  const hint =
903
932
  e.code === 'ECONNREFUSED' || e.code === 'ENOENT'
904
933
  ? '\n -> nothing is listening there; start one with `zygo api`'