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 +29 -2
- package/package.json +2 -2
- package/src/index.d.ts +1 -1
- package/src/index.js +33 -4
package/README.md
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
# zygo — Node client
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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.
|
|
4
|
-
"description": "Node client for Zygo
|
|
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 =
|
|
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
|
-
/**
|
|
856
|
-
|
|
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`'
|