@kosa8/sdk 0.0.0-stage → 0.3.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/LICENSE +26 -0
- package/README.md +260 -2
- package/dist/index.cjs +1791 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1343 -0
- package/dist/index.d.ts +1343 -0
- package/dist/index.js +1718 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1718 @@
|
|
|
1
|
+
// src/checkpoints.ts
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var Kosa8Error = class extends Error {
|
|
8
|
+
/** HTTP status the daemon answered with, when there was an answer. */
|
|
9
|
+
status;
|
|
10
|
+
/** The daemon's `error` text, verbatim. */
|
|
11
|
+
daemonMessage;
|
|
12
|
+
/** The daemon's `remediation` text: what to do about it. */
|
|
13
|
+
remediation;
|
|
14
|
+
method;
|
|
15
|
+
path;
|
|
16
|
+
/** The decoded response body, when there was one. */
|
|
17
|
+
body;
|
|
18
|
+
/** kosa8 Cloud's stable error code (payment_required, quota_exceeded, ...). */
|
|
19
|
+
code;
|
|
20
|
+
constructor(message, options = {}) {
|
|
21
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
22
|
+
this.name = new.target.name;
|
|
23
|
+
this.status = options.status;
|
|
24
|
+
this.daemonMessage = options.daemonMessage;
|
|
25
|
+
this.remediation = options.remediation;
|
|
26
|
+
this.method = options.method;
|
|
27
|
+
this.path = options.path;
|
|
28
|
+
this.body = options.body;
|
|
29
|
+
this.code = options.code;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var Kosa8ConnectionError = class extends Kosa8Error {
|
|
33
|
+
};
|
|
34
|
+
var Kosa8TimeoutError = class extends Kosa8ConnectionError {
|
|
35
|
+
};
|
|
36
|
+
var Kosa8ApiError = class extends Kosa8Error {
|
|
37
|
+
};
|
|
38
|
+
var Kosa8BadRequestError = class extends Kosa8ApiError {
|
|
39
|
+
};
|
|
40
|
+
var Kosa8AuthError = class extends Kosa8ApiError {
|
|
41
|
+
};
|
|
42
|
+
var Kosa8PolicyError = class extends Kosa8ApiError {
|
|
43
|
+
};
|
|
44
|
+
var Kosa8ApprovalRequiredError = class extends Kosa8PolicyError {
|
|
45
|
+
approval;
|
|
46
|
+
constructor(message, approval, options = {}) {
|
|
47
|
+
super(message, options);
|
|
48
|
+
this.approval = approval;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var Kosa8NotFoundError = class extends Kosa8ApiError {
|
|
52
|
+
};
|
|
53
|
+
var Kosa8ConflictError = class extends Kosa8ApiError {
|
|
54
|
+
};
|
|
55
|
+
var Kosa8UnsupportedError = class extends Kosa8ApiError {
|
|
56
|
+
};
|
|
57
|
+
var Kosa8StreamError = class extends Kosa8Error {
|
|
58
|
+
stdout;
|
|
59
|
+
stderr;
|
|
60
|
+
constructor(message, out, options = {}) {
|
|
61
|
+
super(message, options);
|
|
62
|
+
this.stdout = out.stdout;
|
|
63
|
+
this.stderr = out.stderr;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var Kosa8PaymentRequiredError = class extends Kosa8ApiError {
|
|
67
|
+
};
|
|
68
|
+
var Kosa8RateLimitError = class extends Kosa8ApiError {
|
|
69
|
+
};
|
|
70
|
+
var Kosa8CheckpointError = class extends Kosa8Error {
|
|
71
|
+
};
|
|
72
|
+
function isRefusal(v) {
|
|
73
|
+
return typeof v === "object" && v !== null && typeof v.why === "string";
|
|
74
|
+
}
|
|
75
|
+
function errorFromResponse(status, body, ctx = {}) {
|
|
76
|
+
let daemonMessage;
|
|
77
|
+
let remediation;
|
|
78
|
+
let approval;
|
|
79
|
+
if (typeof body === "object" && body !== null) {
|
|
80
|
+
const b = body;
|
|
81
|
+
if (typeof b.error === "string" && b.error !== "") daemonMessage = b.error;
|
|
82
|
+
else if (typeof b.message === "string" && b.message !== "") daemonMessage = b.message;
|
|
83
|
+
if (typeof b.remediation === "string" && b.remediation !== "") remediation = b.remediation;
|
|
84
|
+
approval = b.approval;
|
|
85
|
+
} else if (typeof body === "string" && body.trim() !== "") {
|
|
86
|
+
daemonMessage = body.trim();
|
|
87
|
+
}
|
|
88
|
+
const base = daemonMessage ?? `request failed with HTTP ${status}`;
|
|
89
|
+
const message = remediation ? `${base} (${remediation})` : base;
|
|
90
|
+
const opts = { status, daemonMessage, remediation, body, ...ctx };
|
|
91
|
+
if (status === 403 && isRefusal(approval)) {
|
|
92
|
+
return new Kosa8ApprovalRequiredError(message, approval, opts);
|
|
93
|
+
}
|
|
94
|
+
switch (status) {
|
|
95
|
+
case 400:
|
|
96
|
+
return new Kosa8BadRequestError(message, opts);
|
|
97
|
+
case 401:
|
|
98
|
+
return new Kosa8AuthError(message, opts);
|
|
99
|
+
case 403:
|
|
100
|
+
return new Kosa8PolicyError(message, opts);
|
|
101
|
+
case 404:
|
|
102
|
+
return new Kosa8NotFoundError(message, opts);
|
|
103
|
+
case 402:
|
|
104
|
+
return new Kosa8PaymentRequiredError(message, opts);
|
|
105
|
+
case 409:
|
|
106
|
+
return new Kosa8ConflictError(message, opts);
|
|
107
|
+
case 429:
|
|
108
|
+
return new Kosa8RateLimitError(message, opts);
|
|
109
|
+
case 501:
|
|
110
|
+
return new Kosa8UnsupportedError(message, opts);
|
|
111
|
+
default:
|
|
112
|
+
return new Kosa8ApiError(message, opts);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/checkpoints.ts
|
|
117
|
+
var MAX_CHECKPOINTS_PER_SANDBOX = 10;
|
|
118
|
+
function validateSandboxName(name) {
|
|
119
|
+
if (name === "" || name === "." || name === ".." || /[/\\]/.test(name) || name.startsWith(".")) {
|
|
120
|
+
throw new Kosa8CheckpointError(`invalid sandbox name ${JSON.stringify(name)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
var MemoryCheckpointStore = class {
|
|
124
|
+
data = {};
|
|
125
|
+
queue = Promise.resolve();
|
|
126
|
+
async load() {
|
|
127
|
+
return structuredClone(this.data);
|
|
128
|
+
}
|
|
129
|
+
update(fn) {
|
|
130
|
+
const run = this.queue.then(async () => {
|
|
131
|
+
const copy = structuredClone(this.data);
|
|
132
|
+
const out = await fn(copy);
|
|
133
|
+
this.data = copy;
|
|
134
|
+
return out;
|
|
135
|
+
});
|
|
136
|
+
this.queue = run.catch(() => void 0);
|
|
137
|
+
return run;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var FileCheckpointStore = class {
|
|
141
|
+
path;
|
|
142
|
+
queue = Promise.resolve();
|
|
143
|
+
constructor(filePath) {
|
|
144
|
+
this.path = filePath ?? path.join(os.homedir(), ".kosa8", "checkpoints.json");
|
|
145
|
+
}
|
|
146
|
+
async load() {
|
|
147
|
+
let buf;
|
|
148
|
+
try {
|
|
149
|
+
buf = await fs.readFile(this.path, "utf8");
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err.code === "ENOENT") return {};
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(buf);
|
|
156
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
157
|
+
} catch {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
update(fn) {
|
|
162
|
+
const run = this.queue.then(() => this.locked(fn));
|
|
163
|
+
this.queue = run.catch(() => void 0);
|
|
164
|
+
return run;
|
|
165
|
+
}
|
|
166
|
+
async locked(fn) {
|
|
167
|
+
await fs.mkdir(path.dirname(this.path), { recursive: true, mode: 448 });
|
|
168
|
+
const lock = this.path + ".node.lock";
|
|
169
|
+
const deadline = Date.now() + 1e4;
|
|
170
|
+
for (; ; ) {
|
|
171
|
+
try {
|
|
172
|
+
const h = await fs.open(lock, "wx", 384);
|
|
173
|
+
await h.close();
|
|
174
|
+
break;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err.code !== "EEXIST") throw err;
|
|
177
|
+
try {
|
|
178
|
+
const st = await fs.stat(lock);
|
|
179
|
+
if (Date.now() - st.mtimeMs > 3e4) {
|
|
180
|
+
await fs.rm(lock, { force: true });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (Date.now() > deadline) throw new Kosa8CheckpointError(`timed out waiting for the checkpoint store lock ${lock}`);
|
|
187
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const data = await this.load();
|
|
192
|
+
const out = await fn(data);
|
|
193
|
+
const tmp = this.path + ".tmp";
|
|
194
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
195
|
+
await fs.rename(tmp, this.path);
|
|
196
|
+
return out;
|
|
197
|
+
} finally {
|
|
198
|
+
await fs.rm(lock, { force: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
function seqOf(id, sandbox) {
|
|
203
|
+
const pre = sandbox + "-ckpt-";
|
|
204
|
+
if (!id.startsWith(pre)) return 0;
|
|
205
|
+
const m = /^(\d+)/.exec(id.slice(pre.length));
|
|
206
|
+
return m ? Number(m[1]) : 0;
|
|
207
|
+
}
|
|
208
|
+
function sorted(list) {
|
|
209
|
+
return list.map((c, i) => ({ c, i, t: Date.parse(c.created) || 0 })).sort((a, b) => a.t - b.t || a.i - b.i).map((x) => x.c);
|
|
210
|
+
}
|
|
211
|
+
var CheckpointBook = class {
|
|
212
|
+
constructor(store) {
|
|
213
|
+
this.store = store;
|
|
214
|
+
}
|
|
215
|
+
/** The snapshot id the next checkpoint should use, and ids to evict to stay under the cap. */
|
|
216
|
+
async nextId(sandbox) {
|
|
217
|
+
validateSandboxName(sandbox);
|
|
218
|
+
const list = (await this.store.load())[sandbox] ?? [];
|
|
219
|
+
let n = list.length + 1;
|
|
220
|
+
for (const c of list) {
|
|
221
|
+
const v = seqOf(c.id, sandbox);
|
|
222
|
+
if (v >= n) n = v + 1;
|
|
223
|
+
}
|
|
224
|
+
const evict = [];
|
|
225
|
+
const over = list.length + 1 - MAX_CHECKPOINTS_PER_SANDBOX;
|
|
226
|
+
if (over > 0) for (const c of list.slice(0, over)) evict.push(c.id);
|
|
227
|
+
return { id: `${sandbox}-ckpt-${n}`, evict };
|
|
228
|
+
}
|
|
229
|
+
async record(c, evicted = []) {
|
|
230
|
+
validateSandboxName(c.sandbox);
|
|
231
|
+
const rec = {
|
|
232
|
+
id: c.id,
|
|
233
|
+
sandbox: c.sandbox,
|
|
234
|
+
label: c.label ?? "",
|
|
235
|
+
...c.auto ? { auto: true } : {},
|
|
236
|
+
created: c.created ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
237
|
+
};
|
|
238
|
+
await this.store.update((s) => {
|
|
239
|
+
const drop = new Set(evicted);
|
|
240
|
+
s[c.sandbox] = [...(s[c.sandbox] ?? []).filter((e) => !drop.has(e.id)), rec];
|
|
241
|
+
});
|
|
242
|
+
return rec;
|
|
243
|
+
}
|
|
244
|
+
async list(sandbox) {
|
|
245
|
+
return sorted([...(await this.store.load())[sandbox] ?? []]);
|
|
246
|
+
}
|
|
247
|
+
async latest(sandbox) {
|
|
248
|
+
const list = await this.list(sandbox);
|
|
249
|
+
if (list.length === 0) {
|
|
250
|
+
throw new Kosa8CheckpointError(
|
|
251
|
+
`no checkpoints for ${JSON.stringify(sandbox)} \u2014 take one with checkpoint() or \`kosa8 checkpoint ${sandbox}\``
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return list[list.length - 1];
|
|
255
|
+
}
|
|
256
|
+
/** Find by id or (case-insensitive) label, newest match first. */
|
|
257
|
+
async resolve(sandbox, ref) {
|
|
258
|
+
const list = await this.list(sandbox);
|
|
259
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
260
|
+
const c = list[i];
|
|
261
|
+
if (c.id === ref || c.label.toLowerCase() === ref.toLowerCase()) return c;
|
|
262
|
+
}
|
|
263
|
+
throw new Kosa8CheckpointError(`no checkpoint ${JSON.stringify(ref)} for sandbox ${JSON.stringify(sandbox)}`);
|
|
264
|
+
}
|
|
265
|
+
/** Keep `id` as the top of the stack; return the ids taken after it (now meaningless). */
|
|
266
|
+
async pop(sandbox, id) {
|
|
267
|
+
return this.store.update((s) => {
|
|
268
|
+
const list = s[sandbox] ?? [];
|
|
269
|
+
const idx = list.findIndex((c) => c.id === id);
|
|
270
|
+
if (idx < 0) throw new Kosa8CheckpointError(`no checkpoint ${JSON.stringify(id)} for sandbox ${JSON.stringify(sandbox)}`);
|
|
271
|
+
const orphaned = list.slice(idx + 1).map((c) => c.id);
|
|
272
|
+
s[sandbox] = list.slice(0, idx + 1);
|
|
273
|
+
return orphaned;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/** Drop every checkpoint of a sandbox; return the snapshot ids to delete. */
|
|
277
|
+
async forget(sandbox) {
|
|
278
|
+
return this.store.update((s) => {
|
|
279
|
+
const ids = (s[sandbox] ?? []).map((c) => c.id);
|
|
280
|
+
delete s[sandbox];
|
|
281
|
+
return ids;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// src/tar.ts
|
|
287
|
+
import { promises as fsp } from "fs";
|
|
288
|
+
import * as path2 from "path";
|
|
289
|
+
var BLOCK = 512;
|
|
290
|
+
function octal(buf, off, len, n) {
|
|
291
|
+
buf.write(n.toString(8).padStart(len - 1, "0") + "\0", off, len, "ascii");
|
|
292
|
+
}
|
|
293
|
+
function header(name, type, size, mode, mtime, link = "") {
|
|
294
|
+
const h = Buffer.alloc(BLOCK);
|
|
295
|
+
h.write(name, 0, 100, "utf8");
|
|
296
|
+
octal(h, 100, 8, mode & 4095);
|
|
297
|
+
octal(h, 108, 8, 0);
|
|
298
|
+
octal(h, 116, 8, 0);
|
|
299
|
+
octal(h, 124, 12, size);
|
|
300
|
+
octal(h, 136, 12, mtime);
|
|
301
|
+
h.fill(" ", 148, 156);
|
|
302
|
+
h.write(type, 156, 1, "ascii");
|
|
303
|
+
h.write(link, 157, 100, "utf8");
|
|
304
|
+
h.write("ustar\0", 257, 6, "ascii");
|
|
305
|
+
h.write("00", 263, 2, "ascii");
|
|
306
|
+
let sum = 0;
|
|
307
|
+
for (const b of h) sum += b;
|
|
308
|
+
h.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
|
|
309
|
+
return h;
|
|
310
|
+
}
|
|
311
|
+
function paxRecord(key, value) {
|
|
312
|
+
const body = ` ${key}=${value}
|
|
313
|
+
`;
|
|
314
|
+
let n = Buffer.byteLength(body);
|
|
315
|
+
let len = n + String(n).length;
|
|
316
|
+
if (String(len).length !== String(n).length) len = n + String(len).length;
|
|
317
|
+
return `${len}${body}`;
|
|
318
|
+
}
|
|
319
|
+
function pad(n) {
|
|
320
|
+
const r = n % BLOCK;
|
|
321
|
+
return Buffer.alloc(r === 0 ? 0 : BLOCK - r);
|
|
322
|
+
}
|
|
323
|
+
function entry(out, name, type, mode, mtime, data, link = "") {
|
|
324
|
+
let pax = "";
|
|
325
|
+
if (Buffer.byteLength(name) > 100) pax += paxRecord("path", name);
|
|
326
|
+
if (Buffer.byteLength(link) > 100) pax += paxRecord("linkpath", link);
|
|
327
|
+
if (pax !== "") {
|
|
328
|
+
const p = Buffer.from(pax, "utf8");
|
|
329
|
+
out.push(header("PaxHeader", "x", p.length, 420, mtime), p, pad(p.length));
|
|
330
|
+
}
|
|
331
|
+
const size = data?.length ?? 0;
|
|
332
|
+
out.push(header(name.slice(0, 100), type, size, mode, mtime, link.slice(0, 100)));
|
|
333
|
+
if (data) out.push(data, pad(size));
|
|
334
|
+
}
|
|
335
|
+
async function readIgnore(dir) {
|
|
336
|
+
let text;
|
|
337
|
+
try {
|
|
338
|
+
text = await fsp.readFile(path2.join(dir, ".dockerignore"), "utf8");
|
|
339
|
+
} catch {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
const out = [];
|
|
343
|
+
for (const line of text.split(/\r?\n/)) {
|
|
344
|
+
const l = line.trim();
|
|
345
|
+
if (l === "" || line.startsWith("#")) continue;
|
|
346
|
+
out.push(path2.posix.normalize(l.replace(/\\/g, "/")).replace(/\/+$/, "") || ".");
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
async function tarContext(dir, dockerfile = "Dockerfile") {
|
|
351
|
+
try {
|
|
352
|
+
await fsp.access(path2.join(dir, dockerfile));
|
|
353
|
+
} catch {
|
|
354
|
+
throw new Kosa8Error(`no ${dockerfile} in ${dir}`);
|
|
355
|
+
}
|
|
356
|
+
const ignore = await readIgnore(dir);
|
|
357
|
+
const skip = (rel) => ignore.some((p) => rel === p || rel.startsWith(p + "/"));
|
|
358
|
+
const out = [];
|
|
359
|
+
const walk = async (rel) => {
|
|
360
|
+
const ents = await fsp.readdir(path2.join(dir, rel), { withFileTypes: true });
|
|
361
|
+
ents.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
362
|
+
const subdirs = [];
|
|
363
|
+
for (const e of ents) {
|
|
364
|
+
const r = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
365
|
+
if (skip(r)) continue;
|
|
366
|
+
const full = path2.join(dir, r);
|
|
367
|
+
const st = await fsp.lstat(full);
|
|
368
|
+
const mtime = Math.floor(st.mtimeMs / 1e3);
|
|
369
|
+
if (st.isSymbolicLink()) {
|
|
370
|
+
entry(out, r, "2", st.mode, mtime, void 0, await fsp.readlink(full));
|
|
371
|
+
} else if (st.isDirectory()) {
|
|
372
|
+
entry(out, r + "/", "5", st.mode, mtime);
|
|
373
|
+
subdirs.push(r);
|
|
374
|
+
} else if (st.isFile()) {
|
|
375
|
+
entry(out, r, "0", st.mode, mtime, await fsp.readFile(full));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const s of subdirs) await walk(s);
|
|
379
|
+
};
|
|
380
|
+
await walk("");
|
|
381
|
+
out.push(Buffer.alloc(BLOCK * 2));
|
|
382
|
+
return Buffer.concat(out);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/transport.ts
|
|
386
|
+
import * as fs2 from "fs";
|
|
387
|
+
import * as http from "http";
|
|
388
|
+
import * as https from "https";
|
|
389
|
+
import * as os2 from "os";
|
|
390
|
+
import * as path3 from "path";
|
|
391
|
+
var APPROVAL_HEADER = "X-Kosa8-Approval";
|
|
392
|
+
var ACTOR_HEADER = "X-Kosa8-Actor";
|
|
393
|
+
var AGENT_TOKEN_HEADER = "X-Kosa8-Agent-Token";
|
|
394
|
+
function defaultSocketPath() {
|
|
395
|
+
return path3.join(os2.homedir(), ".kosa8", "run", "kosa8.sock");
|
|
396
|
+
}
|
|
397
|
+
function defaultWorkerFile() {
|
|
398
|
+
return path3.join(os2.homedir(), ".kosa8", "worker.json");
|
|
399
|
+
}
|
|
400
|
+
function loadSavedWorker(file) {
|
|
401
|
+
let w;
|
|
402
|
+
try {
|
|
403
|
+
w = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
404
|
+
} catch {
|
|
405
|
+
return void 0;
|
|
406
|
+
}
|
|
407
|
+
if (!w || typeof w !== "object") return void 0;
|
|
408
|
+
const o = w;
|
|
409
|
+
const host = typeof o.host === "string" ? o.host.trim() : "";
|
|
410
|
+
if (host === "") return void 0;
|
|
411
|
+
return {
|
|
412
|
+
host,
|
|
413
|
+
token: typeof o.token === "string" && o.token !== "" ? o.token : void 0,
|
|
414
|
+
insecure: o.insecure === true
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function isLoopback(hostname) {
|
|
418
|
+
const h = hostname.replace(/^\[|\]$/g, "");
|
|
419
|
+
return h === "localhost" || h === "::1" || /^127\./.test(h);
|
|
420
|
+
}
|
|
421
|
+
function resolveEndpoint(opts) {
|
|
422
|
+
const env = opts.env ?? process.env;
|
|
423
|
+
const fromEnv = (env.KOSA8_HOST ?? "").trim();
|
|
424
|
+
const raw = (opts.host ?? fromEnv).trim();
|
|
425
|
+
const envInsecure = opts.insecure === true || env.KOSA8_INSECURE === "1";
|
|
426
|
+
if (raw !== "") {
|
|
427
|
+
const token = opts.token ?? (opts.host === void 0 && fromEnv !== "" ? env.KOSA8_TOKEN : void 0);
|
|
428
|
+
return parseHost(raw, envInsecure, token);
|
|
429
|
+
}
|
|
430
|
+
if (opts.socketPath === void 0 && opts.workerFile !== null) {
|
|
431
|
+
const saved = loadSavedWorker(opts.workerFile ?? defaultWorkerFile());
|
|
432
|
+
if (saved) return parseHost(saved.host, envInsecure || saved.insecure, opts.token ?? saved.token);
|
|
433
|
+
}
|
|
434
|
+
return { kind: "unix", socketPath: opts.socketPath ?? defaultSocketPath() };
|
|
435
|
+
}
|
|
436
|
+
function parseHost(raw, insecure, token) {
|
|
437
|
+
let h = raw.replace(/\/+$/, "");
|
|
438
|
+
const i = h.indexOf("://");
|
|
439
|
+
if (i > 0) h = h.slice(0, i).toLowerCase() + h.slice(i);
|
|
440
|
+
if (h.startsWith("unix://")) {
|
|
441
|
+
return { kind: "unix", socketPath: h.slice("unix://".length) };
|
|
442
|
+
}
|
|
443
|
+
if (h.startsWith("tcp://")) h = "http://" + h.slice("tcp://".length);
|
|
444
|
+
else if (!h.startsWith("http://") && !h.startsWith("https://")) {
|
|
445
|
+
if (h.includes("://")) {
|
|
446
|
+
throw new Kosa8Error(
|
|
447
|
+
`KOSA8_HOST scheme "${h.slice(0, h.indexOf("://"))}" is not supported; use http://, https://, tcp:// or unix://`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
h = "http://" + h;
|
|
451
|
+
}
|
|
452
|
+
const url = new URL(h);
|
|
453
|
+
if (url.protocol === "http:" && !insecure && !isLoopback(url.hostname)) {
|
|
454
|
+
throw new Kosa8Error(
|
|
455
|
+
`refusing to talk to ${url.host} over plain http: the token and every request would cross the network unencrypted. Use https://${url.host} (a worker behind a TLS-terminating tunnel or proxy), or set insecure: true / KOSA8_INSECURE=1 if this link is already private and trusted`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return { kind: "tcp", url, token: token || void 0 };
|
|
459
|
+
}
|
|
460
|
+
var Transport = class {
|
|
461
|
+
endpoint;
|
|
462
|
+
actor;
|
|
463
|
+
agentToken;
|
|
464
|
+
extraHeaders;
|
|
465
|
+
constructor(opts = {}) {
|
|
466
|
+
this.endpoint = resolveEndpoint(opts);
|
|
467
|
+
this.actor = opts.actor ?? "tool";
|
|
468
|
+
const env = opts.env ?? process.env;
|
|
469
|
+
this.agentToken = opts.agentToken ?? (env.KOSA8_AGENT_TOKEN || void 0);
|
|
470
|
+
this.extraHeaders = { ...opts.headers ?? {} };
|
|
471
|
+
}
|
|
472
|
+
/** Human-readable description of where requests go. */
|
|
473
|
+
get target() {
|
|
474
|
+
return this.endpoint.kind === "unix" ? `unix://${this.endpoint.socketPath}` : this.endpoint.url.origin;
|
|
475
|
+
}
|
|
476
|
+
buildPath(p, query) {
|
|
477
|
+
const base = this.endpoint.kind === "tcp" ? this.endpoint.url.pathname.replace(/\/+$/, "") : "";
|
|
478
|
+
let out = base + p;
|
|
479
|
+
if (query) {
|
|
480
|
+
const qs = new URLSearchParams();
|
|
481
|
+
for (const [k, v] of Object.entries(query)) {
|
|
482
|
+
if (v === void 0 || v === false) continue;
|
|
483
|
+
qs.set(k, v === true ? "1" : String(v));
|
|
484
|
+
}
|
|
485
|
+
const s = qs.toString();
|
|
486
|
+
if (s) out += "?" + s;
|
|
487
|
+
}
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
/** Send a request and return the raw response once headers arrive. */
|
|
491
|
+
raw(method, p, opts = {}) {
|
|
492
|
+
const headers = {
|
|
493
|
+
Accept: "application/json",
|
|
494
|
+
[ACTOR_HEADER]: this.actor,
|
|
495
|
+
...this.extraHeaders,
|
|
496
|
+
...opts.headers ?? {}
|
|
497
|
+
};
|
|
498
|
+
if (opts.approval && opts.approval.trim() !== "") headers[APPROVAL_HEADER] = opts.approval.trim();
|
|
499
|
+
if (this.agentToken) headers[AGENT_TOKEN_HEADER] = this.agentToken;
|
|
500
|
+
let payload;
|
|
501
|
+
let streamBody;
|
|
502
|
+
if (opts.rawBody !== void 0) {
|
|
503
|
+
headers["Content-Type"] = "application/octet-stream";
|
|
504
|
+
if (typeof opts.rawBody === "string" || opts.rawBody instanceof Uint8Array) {
|
|
505
|
+
payload = Buffer.from(opts.rawBody);
|
|
506
|
+
headers["Content-Length"] = String(payload.length);
|
|
507
|
+
} else {
|
|
508
|
+
streamBody = opts.rawBody;
|
|
509
|
+
}
|
|
510
|
+
} else if (opts.body !== void 0) {
|
|
511
|
+
payload = Buffer.from(JSON.stringify(opts.body));
|
|
512
|
+
headers["Content-Type"] = "application/json";
|
|
513
|
+
headers["Content-Length"] = String(payload.length);
|
|
514
|
+
} else if (method !== "GET" && method !== "HEAD") {
|
|
515
|
+
headers["Content-Length"] = "0";
|
|
516
|
+
}
|
|
517
|
+
const reqPath = this.buildPath(p, opts.query);
|
|
518
|
+
let reqOpts;
|
|
519
|
+
let mod = http;
|
|
520
|
+
if (this.endpoint.kind === "unix") {
|
|
521
|
+
reqOpts = { socketPath: this.endpoint.socketPath, path: reqPath, method, headers: { Host: "kosa8", ...headers } };
|
|
522
|
+
} else {
|
|
523
|
+
const u = this.endpoint.url;
|
|
524
|
+
if (this.endpoint.token) headers.Authorization = `Bearer ${this.endpoint.token}`;
|
|
525
|
+
mod = u.protocol === "https:" ? https : http;
|
|
526
|
+
reqOpts = { protocol: u.protocol, hostname: u.hostname, port: u.port || void 0, path: reqPath, method, headers };
|
|
527
|
+
}
|
|
528
|
+
return new Promise((resolve, reject) => {
|
|
529
|
+
const signal = opts.signal;
|
|
530
|
+
if (signal?.aborted) {
|
|
531
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const req = mod.request(reqOpts, (res) => {
|
|
535
|
+
resolve({ status: res.statusCode ?? 0, headers: res.headers, stream: res });
|
|
536
|
+
});
|
|
537
|
+
const onAbort = () => {
|
|
538
|
+
req.destroy(signal?.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError"));
|
|
539
|
+
};
|
|
540
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
541
|
+
req.on("close", () => signal?.removeEventListener("abort", onAbort));
|
|
542
|
+
req.on("error", (err) => {
|
|
543
|
+
if (signal?.aborted) {
|
|
544
|
+
reject(signal.reason ?? err);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
reject(
|
|
548
|
+
new Kosa8ConnectionError(
|
|
549
|
+
`kosa8 daemon unreachable at ${this.target}: ${err.message}` + (err.code === "ENOENT" || err.code === "ECONNREFUSED" ? this.endpoint.kind === "unix" && process.platform === "win32" ? " (on Windows the engine runs in WSL2: run `kosa8 wsl setup`, or set KOSA8_HOST)" : " (is kosa8 installed and its daemon running? start it with `kosa8 daemon start`)" : ""),
|
|
550
|
+
{ method, path: p, cause: err }
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
if (streamBody) {
|
|
555
|
+
const src = streamBody;
|
|
556
|
+
void (async () => {
|
|
557
|
+
try {
|
|
558
|
+
for await (const chunk of src) {
|
|
559
|
+
if (!req.write(chunk)) await new Promise((r) => req.once("drain", r));
|
|
560
|
+
}
|
|
561
|
+
req.end();
|
|
562
|
+
} catch (err) {
|
|
563
|
+
req.destroy(err);
|
|
564
|
+
}
|
|
565
|
+
})();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (payload) req.write(payload);
|
|
569
|
+
req.end();
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/** Send a request and decode a JSON answer; HTTP errors become typed errors. */
|
|
573
|
+
async json(method, p, opts = {}) {
|
|
574
|
+
const res = await this.raw(method, p, opts);
|
|
575
|
+
const text = (await readAll(res.stream, opts.signal)).toString("utf8");
|
|
576
|
+
let body = void 0;
|
|
577
|
+
if (text.trim() !== "") {
|
|
578
|
+
try {
|
|
579
|
+
body = JSON.parse(text);
|
|
580
|
+
} catch {
|
|
581
|
+
body = text;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (res.status >= 400) throw errorFromResponse(res.status, body, { method, path: p });
|
|
585
|
+
return body;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Send a request whose answer is a stream of NDJSON frames (run, exec,
|
|
589
|
+
* attach). HTTP errors (a refusal before streaming began) become typed
|
|
590
|
+
* errors; each frame's base64 `data` is decoded to a Buffer.
|
|
591
|
+
*/
|
|
592
|
+
async *frames(method, p, opts = {}) {
|
|
593
|
+
const res = await this.raw(method, p, opts);
|
|
594
|
+
if (res.status >= 400) {
|
|
595
|
+
const text = (await readAll(res.stream, opts.signal)).toString("utf8");
|
|
596
|
+
let body = text;
|
|
597
|
+
try {
|
|
598
|
+
body = JSON.parse(text);
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
throw errorFromResponse(res.status, body, { method, path: p });
|
|
602
|
+
}
|
|
603
|
+
let buf = "";
|
|
604
|
+
try {
|
|
605
|
+
for await (const chunk of res.stream) {
|
|
606
|
+
buf += chunk.toString("utf8");
|
|
607
|
+
let nl;
|
|
608
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
609
|
+
const line = buf.slice(0, nl).trim();
|
|
610
|
+
buf = buf.slice(nl + 1);
|
|
611
|
+
if (line !== "") yield decodeFrame(line);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} catch (err) {
|
|
615
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? err;
|
|
616
|
+
throw err;
|
|
617
|
+
} finally {
|
|
618
|
+
res.stream.destroy();
|
|
619
|
+
}
|
|
620
|
+
const rest = buf.trim();
|
|
621
|
+
if (rest !== "") yield decodeFrame(rest);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
function decodeFrame(line) {
|
|
625
|
+
const f = JSON.parse(line);
|
|
626
|
+
const out = { ...f };
|
|
627
|
+
if (typeof f.data === "string") out.data = Buffer.from(f.data, "base64");
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
630
|
+
async function readAll(stream, signal) {
|
|
631
|
+
const chunks = [];
|
|
632
|
+
try {
|
|
633
|
+
for await (const c of stream) chunks.push(c);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
if (signal?.aborted) throw signal.reason ?? err;
|
|
636
|
+
throw err;
|
|
637
|
+
}
|
|
638
|
+
return Buffer.concat(chunks);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/client.ts
|
|
642
|
+
function egressString(egress, allow) {
|
|
643
|
+
if (allow && allow.length > 0) {
|
|
644
|
+
if (egress !== void 0 && egress !== "deny") {
|
|
645
|
+
throw new Kosa8Error("egressAllow already limits the network to the hosts it lists; do not also set egress");
|
|
646
|
+
}
|
|
647
|
+
return "allow:" + allow.map((h) => h.trim()).join(",");
|
|
648
|
+
}
|
|
649
|
+
if (egress === void 0) return void 0;
|
|
650
|
+
if (typeof egress === "string") return egress;
|
|
651
|
+
if (egress.allow.length === 0) {
|
|
652
|
+
throw new Kosa8Error('an egress allow-list needs at least one host; use egress: "deny" for no network');
|
|
653
|
+
}
|
|
654
|
+
return "allow:" + egress.allow.map((h) => h.trim()).join(",");
|
|
655
|
+
}
|
|
656
|
+
function envList(env) {
|
|
657
|
+
if (!env) return void 0;
|
|
658
|
+
if (Array.isArray(env)) return env;
|
|
659
|
+
return Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
660
|
+
}
|
|
661
|
+
var enc = encodeURIComponent;
|
|
662
|
+
function str(v) {
|
|
663
|
+
return typeof v === "string" ? v : "";
|
|
664
|
+
}
|
|
665
|
+
function num(v) {
|
|
666
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
667
|
+
}
|
|
668
|
+
var Kosa8Client = class {
|
|
669
|
+
transport;
|
|
670
|
+
checkpoints;
|
|
671
|
+
constructor(options = {}) {
|
|
672
|
+
this.transport = new Transport(options);
|
|
673
|
+
this.checkpoints = new CheckpointBook(options.checkpointStore ?? new FileCheckpointStore());
|
|
674
|
+
}
|
|
675
|
+
// --- status ---------------------------------------------------------------
|
|
676
|
+
/** GET /kosa8/v1/health: daemon status and version. */
|
|
677
|
+
health(opts = {}) {
|
|
678
|
+
return this.transport.json("GET", "/kosa8/v1/health", opts);
|
|
679
|
+
}
|
|
680
|
+
/** The daemon's version string. */
|
|
681
|
+
async version(opts = {}) {
|
|
682
|
+
return (await this.health(opts)).version;
|
|
683
|
+
}
|
|
684
|
+
// --- host engine ------------------------------------------------------------
|
|
685
|
+
/** POST /kosa8/v1/pull: pull an image into the host engine (checked against the registry allow-list). */
|
|
686
|
+
pull(ref, opts = {}) {
|
|
687
|
+
return this.transport.json("POST", "/kosa8/v1/pull", { ...opts, body: { ref } });
|
|
688
|
+
}
|
|
689
|
+
// --- sandboxes ---------------------------------------------------------------
|
|
690
|
+
/** GET /kosa8/v1/sandboxes */
|
|
691
|
+
listSandboxes(opts = {}) {
|
|
692
|
+
return this.transport.json("GET", "/kosa8/v1/sandboxes", opts);
|
|
693
|
+
}
|
|
694
|
+
/** Find one sandbox by id or name (from the list; there is no single-sandbox GET). */
|
|
695
|
+
async getSandbox(idOrName, opts = {}) {
|
|
696
|
+
const list = await this.listSandboxes(opts);
|
|
697
|
+
return list.find((s) => s.id === idOrName) ?? list.find((s) => s.name === idOrName);
|
|
698
|
+
}
|
|
699
|
+
/** POST /kosa8/v1/sandboxes: boot a microVM sandbox. */
|
|
700
|
+
async createSandbox(options = {}) {
|
|
701
|
+
const body = {};
|
|
702
|
+
if (options.name) body.name = options.name;
|
|
703
|
+
if (options.cpus) body.cpus = options.cpus;
|
|
704
|
+
if (options.memMb) body.mem_mb = options.memMb;
|
|
705
|
+
if (options.diskGb) body.disk_gb = options.diskGb;
|
|
706
|
+
if (options.mounts) body.mounts = options.mounts;
|
|
707
|
+
const egress = egressString(options.egress, options.egressAllow);
|
|
708
|
+
if (egress !== void 0) body.egress = egress;
|
|
709
|
+
if (options.capture) body.capture = options.capture;
|
|
710
|
+
if (options.labels) body.labels = options.labels;
|
|
711
|
+
if (options.broker) body.broker = options.broker;
|
|
712
|
+
if (options.autoStopMin) body.auto_stop_min = options.autoStopMin;
|
|
713
|
+
if (options.autoDeleteMin) body.auto_delete_min = options.autoDeleteMin;
|
|
714
|
+
if (options.ttlMin) body.ttl_min = options.ttlMin;
|
|
715
|
+
if (options.ephemeral) body.ephemeral = true;
|
|
716
|
+
if (options.template) body.template = options.template;
|
|
717
|
+
return this.transport.json("POST", "/kosa8/v1/sandboxes", {
|
|
718
|
+
body,
|
|
719
|
+
approval: options.approval,
|
|
720
|
+
signal: options.signal
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
/** POST /kosa8/v1/sandboxes/{id}/up: boot a stopped sandbox from its own disk. */
|
|
724
|
+
startSandbox(id, opts = {}) {
|
|
725
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/up`, opts);
|
|
726
|
+
}
|
|
727
|
+
/** POST /kosa8/v1/sandboxes/{id}/down: stop without deleting (disk kept). */
|
|
728
|
+
stopSandbox(id, opts = {}) {
|
|
729
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/down`, opts);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* DELETE /kosa8/v1/sandboxes/{id}. Like `kosa8 sandbox rm`, the sandbox's
|
|
733
|
+
* checkpoints and their snapshots are dropped too unless
|
|
734
|
+
* `keepCheckpoints` is set.
|
|
735
|
+
*/
|
|
736
|
+
async deleteSandbox(id, opts = {}) {
|
|
737
|
+
const out = await this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}`, {
|
|
738
|
+
query: { wipe: opts.wipe },
|
|
739
|
+
approval: opts.approval,
|
|
740
|
+
signal: opts.signal
|
|
741
|
+
});
|
|
742
|
+
let dropped = 0;
|
|
743
|
+
if (!opts.keepCheckpoints) {
|
|
744
|
+
try {
|
|
745
|
+
const ids = await this.checkpoints.forget(id);
|
|
746
|
+
for (const sid of ids) {
|
|
747
|
+
await this.deleteSnapshot(sid, { wipe: opts.wipe, approval: opts.approval }).catch(() => void 0);
|
|
748
|
+
}
|
|
749
|
+
dropped = ids.length;
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return { ...out, checkpoints_dropped: dropped };
|
|
754
|
+
}
|
|
755
|
+
/** POST /kosa8/v1/sandboxes/{id}/pull: pull an image into the sandbox's private engine (fetched on the host). */
|
|
756
|
+
pullIntoSandbox(id, ref, opts = {}) {
|
|
757
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/pull`, { ...opts, body: { ref } });
|
|
758
|
+
}
|
|
759
|
+
/** GET /kosa8/v1/sandboxes/{id}/images */
|
|
760
|
+
sandboxImages(id, opts = {}) {
|
|
761
|
+
return this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/images`, opts);
|
|
762
|
+
}
|
|
763
|
+
/** GET /kosa8/v1/sandboxes/{id}/containers */
|
|
764
|
+
sandboxContainers(id, opts = {}) {
|
|
765
|
+
return this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/containers`, opts);
|
|
766
|
+
}
|
|
767
|
+
/** GET /kosa8/v1/sandboxes/{id}/egress: the policy the host enforces and what it decided. */
|
|
768
|
+
sandboxEgress(id, opts = {}) {
|
|
769
|
+
return this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/egress`, opts);
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* POST /kosa8/v1/sandboxes/{id}/run: run a container to completion inside
|
|
773
|
+
* the sandbox's engine. The image must already be in the sandbox
|
|
774
|
+
* (`pullIntoSandbox`). Resolves with the exit code; a non-zero exit is not
|
|
775
|
+
* an error.
|
|
776
|
+
*/
|
|
777
|
+
run(id, image, args = [], opts = {}) {
|
|
778
|
+
return this.collect(
|
|
779
|
+
this.transport.frames("POST", `/kosa8/v1/sandboxes/${enc(id)}/run`, {
|
|
780
|
+
body: { image, args, env: envList(opts.env) },
|
|
781
|
+
approval: opts.approval,
|
|
782
|
+
signal: opts.signal
|
|
783
|
+
}),
|
|
784
|
+
opts
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
/** POST /kosa8/v1/sandboxes/{id}/start: start a detached container; resolves with its id. */
|
|
788
|
+
async startContainer(id, image, args = [], opts = {}) {
|
|
789
|
+
const out = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/start`, {
|
|
790
|
+
...opts,
|
|
791
|
+
body: { image, args }
|
|
792
|
+
});
|
|
793
|
+
return out.id;
|
|
794
|
+
}
|
|
795
|
+
/** POST /kosa8/v1/sandboxes/{id}/exec: run a command in a running container of the sandbox. */
|
|
796
|
+
exec(id, container, args, opts = {}) {
|
|
797
|
+
return this.collect(this.execFrames(id, container, args, opts), opts);
|
|
798
|
+
}
|
|
799
|
+
/** The raw frame stream of an exec, for callers that want to process output as it arrives. */
|
|
800
|
+
execFrames(id, container, args, opts = {}) {
|
|
801
|
+
return this.transport.frames("POST", `/kosa8/v1/sandboxes/${enc(id)}/exec`, {
|
|
802
|
+
body: { container, args },
|
|
803
|
+
approval: opts.approval,
|
|
804
|
+
signal: opts.signal
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
/** POST /kosa8/v1/sandboxes/{id}/start-interactive: start a container with stdin held open. */
|
|
808
|
+
async startInteractive(id, image, args = [], opts = {}) {
|
|
809
|
+
const out = await this.transport.json(
|
|
810
|
+
"POST",
|
|
811
|
+
`/kosa8/v1/sandboxes/${enc(id)}/start-interactive`,
|
|
812
|
+
{ body: { image, args, env: envList(opts.env), tty: opts.tty ?? false }, approval: opts.approval, signal: opts.signal }
|
|
813
|
+
);
|
|
814
|
+
return out.id;
|
|
815
|
+
}
|
|
816
|
+
/** POST /kosa8/v1/sandboxes/{id}/containers/{cid}/stdin: stream bytes into an interactive container's stdin. */
|
|
817
|
+
writeStdin(id, container, data, opts = {}) {
|
|
818
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/containers/${enc(container)}/stdin`, {
|
|
819
|
+
...opts,
|
|
820
|
+
rawBody: data
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
/** POST /kosa8/v1/sandboxes/{id}/containers/{cid}/attach: follow a container's output until it exits. */
|
|
824
|
+
attach(id, container, opts = {}) {
|
|
825
|
+
return this.collect(
|
|
826
|
+
this.transport.frames("POST", `/kosa8/v1/sandboxes/${enc(id)}/containers/${enc(container)}/attach`, {
|
|
827
|
+
approval: opts.approval,
|
|
828
|
+
signal: opts.signal
|
|
829
|
+
}),
|
|
830
|
+
opts
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
// --- snapshots, fork, explore -------------------------------------------------
|
|
834
|
+
/** POST /kosa8/v1/sandboxes/{id}/snapshot: capture memory, processes and disks. */
|
|
835
|
+
snapshot(id, opts = {}) {
|
|
836
|
+
const body = {};
|
|
837
|
+
if (opts.id) body.id = opts.id;
|
|
838
|
+
if (opts.encrypt) body.encrypt = true;
|
|
839
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/snapshot`, {
|
|
840
|
+
body,
|
|
841
|
+
approval: opts.approval,
|
|
842
|
+
signal: opts.signal
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
/** GET /kosa8/v1/snapshots */
|
|
846
|
+
listSnapshots(opts = {}) {
|
|
847
|
+
return this.transport.json("GET", "/kosa8/v1/snapshots", opts);
|
|
848
|
+
}
|
|
849
|
+
/** POST /kosa8/v1/snapshots/{id}/restore: boot a sandbox from a snapshot, processes resumed. */
|
|
850
|
+
restoreSnapshot(snapshotId, opts = {}) {
|
|
851
|
+
const body = {};
|
|
852
|
+
if (opts.name) body.name = opts.name;
|
|
853
|
+
if (opts.label) body.label = opts.label;
|
|
854
|
+
if (opts.allowTampered) body.allow_tampered = true;
|
|
855
|
+
return this.transport.json("POST", `/kosa8/v1/snapshots/${enc(snapshotId)}/restore`, {
|
|
856
|
+
body,
|
|
857
|
+
approval: opts.approval,
|
|
858
|
+
signal: opts.signal
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
/** DELETE /kosa8/v1/snapshots/{id} */
|
|
862
|
+
deleteSnapshot(snapshotId, opts = {}) {
|
|
863
|
+
return this.transport.json("DELETE", `/kosa8/v1/snapshots/${enc(snapshotId)}`, {
|
|
864
|
+
query: { wipe: opts.wipe, force: opts.force },
|
|
865
|
+
approval: opts.approval,
|
|
866
|
+
signal: opts.signal
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
/** POST /kosa8/v1/sandboxes/{id}/fork: N live copies of a running sandbox. */
|
|
870
|
+
fork(id, opts = {}) {
|
|
871
|
+
const body = { n: opts.n ?? 1 };
|
|
872
|
+
if (opts.prefix) body.prefix = opts.prefix;
|
|
873
|
+
if (opts.label) body.label = opts.label;
|
|
874
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/fork`, {
|
|
875
|
+
body,
|
|
876
|
+
approval: opts.approval,
|
|
877
|
+
signal: opts.signal
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Best-of-N, as `kosa8 explore`: fork the sandbox once per variant, run
|
|
882
|
+
* each variant (`sh -c VARIANT` in `image`) in its own branch
|
|
883
|
+
* concurrently, keep the fastest successful branch and delete the rest.
|
|
884
|
+
*/
|
|
885
|
+
async explore(id, opts) {
|
|
886
|
+
const variants = opts.variants ?? [];
|
|
887
|
+
if (variants.length < 2) {
|
|
888
|
+
throw new Kosa8Error("give at least two variants; with one there is nothing to compare");
|
|
889
|
+
}
|
|
890
|
+
const image = opts.image ?? "alpine:latest";
|
|
891
|
+
const call = { approval: opts.approval, signal: opts.signal };
|
|
892
|
+
if (opts.pull !== false) await this.pullIntoSandbox(id, image, call);
|
|
893
|
+
const prefix = opts.prefix ?? `${id}-explore-${Math.floor(Date.now() / 1e3) % 1e5}`;
|
|
894
|
+
const forked = await this.fork(id, { n: variants.length, prefix, ...call });
|
|
895
|
+
const names = forked.forks.map((f) => f.id).filter((x) => typeof x === "string");
|
|
896
|
+
if (names.length < variants.length) {
|
|
897
|
+
throw new Kosa8Error(`forked ${names.length} branches for ${variants.length} variants; see listSandboxes() for what was created`);
|
|
898
|
+
}
|
|
899
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
900
|
+
const branches = await Promise.all(
|
|
901
|
+
variants.map((v, i) => this.runBranch(names[i], image, v, timeoutMs, opts))
|
|
902
|
+
);
|
|
903
|
+
let winner = "";
|
|
904
|
+
let best = Infinity;
|
|
905
|
+
for (const b of branches) {
|
|
906
|
+
if (!b.error && b.exit === 0 && b.duration_ms < best) {
|
|
907
|
+
winner = b.name;
|
|
908
|
+
best = b.duration_ms;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (opts.keep !== void 0) {
|
|
912
|
+
if (!names.includes(opts.keep)) {
|
|
913
|
+
throw new Kosa8Error(`keep ${opts.keep} is not one of the branches (${names.join(", ")}); all of them were kept`);
|
|
914
|
+
}
|
|
915
|
+
winner = opts.keep;
|
|
916
|
+
}
|
|
917
|
+
let removed = 0;
|
|
918
|
+
if (!opts.keepAll) {
|
|
919
|
+
for (const b of branches) {
|
|
920
|
+
if (b.name !== winner) {
|
|
921
|
+
try {
|
|
922
|
+
await this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(b.name)}`, { approval: opts.approval });
|
|
923
|
+
removed++;
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return { parent: id, branches, winner, removed };
|
|
930
|
+
}
|
|
931
|
+
async runBranch(name, image, variant, timeoutMs, opts) {
|
|
932
|
+
const start = Date.now();
|
|
933
|
+
const r = { name, variant, exit: 0, output: "", duration_ms: 0 };
|
|
934
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
935
|
+
const ctl = new AbortController();
|
|
936
|
+
const timer = setTimeout(() => ctl.abort(new Error(`branch timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
937
|
+
const onAbort = () => ctl.abort(opts.signal?.reason);
|
|
938
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
939
|
+
try {
|
|
940
|
+
let out = "";
|
|
941
|
+
const res = await this.run(name, image, ["sh", "-c", variant], {
|
|
942
|
+
approval: opts.approval,
|
|
943
|
+
signal: ctl.signal,
|
|
944
|
+
onStdout: (c) => out += c.toString("utf8"),
|
|
945
|
+
onStderr: (c) => out += c.toString("utf8")
|
|
946
|
+
});
|
|
947
|
+
r.exit = res.exitCode;
|
|
948
|
+
r.output = out.replace(/\n+$/, "");
|
|
949
|
+
delete r.error;
|
|
950
|
+
break;
|
|
951
|
+
} catch (err) {
|
|
952
|
+
r.error = err instanceof Error ? err.message : String(err);
|
|
953
|
+
if (err instanceof Kosa8StreamError) r.output = (err.stdout + err.stderr).replace(/\n+$/, "");
|
|
954
|
+
const transient = /connection refused|EOF|connection reset|broken pipe|no such file or directory|stream ended|ECONNREFUSED|ECONNRESET|socket hang up/i.test(
|
|
955
|
+
r.error
|
|
956
|
+
);
|
|
957
|
+
if (!transient || ctl.signal.aborted) break;
|
|
958
|
+
await new Promise((res) => setTimeout(res, (attempt + 1) * 500));
|
|
959
|
+
} finally {
|
|
960
|
+
clearTimeout(timer);
|
|
961
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
r.duration_ms = Date.now() - start;
|
|
965
|
+
return r;
|
|
966
|
+
}
|
|
967
|
+
// --- checkpoints (client-side over snapshots, as pkg/checkpoint) ---------------
|
|
968
|
+
/**
|
|
969
|
+
* Save a restore point: a snapshot with id `<sandbox>-ckpt-<n>`, recorded on
|
|
970
|
+
* the sandbox's checkpoint stack (the same store `kosa8 checkpoint` uses).
|
|
971
|
+
* The oldest is evicted past 10.
|
|
972
|
+
*/
|
|
973
|
+
async checkpoint(sandbox, opts = {}) {
|
|
974
|
+
const { id, evict } = await this.checkpoints.nextId(sandbox);
|
|
975
|
+
const snap = await this.snapshot(sandbox, { id, approval: opts.approval, signal: opts.signal });
|
|
976
|
+
for (const old of evict) {
|
|
977
|
+
await this.deleteSnapshot(old, { approval: opts.approval }).catch(() => void 0);
|
|
978
|
+
}
|
|
979
|
+
const checkpoint = await this.checkpoints.record({ id, sandbox, label: opts.label ?? "" }, evict);
|
|
980
|
+
return { checkpoint, evicted: evict, snapshot: snap };
|
|
981
|
+
}
|
|
982
|
+
/** A sandbox's restore points, oldest first. */
|
|
983
|
+
listCheckpoints(sandbox) {
|
|
984
|
+
return this.checkpoints.list(sandbox);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Undo: put the sandbox back to a checkpoint under the same name, processes
|
|
988
|
+
* resumed. Deletes the sandbox, then restores the snapshot with its name;
|
|
989
|
+
* checkpoints taken after the target are discarded. Current state is lost.
|
|
990
|
+
*/
|
|
991
|
+
async rewind(sandbox, opts = {}) {
|
|
992
|
+
const target = opts.to ? await this.checkpoints.resolve(sandbox, opts.to) : await this.checkpoints.latest(sandbox);
|
|
993
|
+
const call = { approval: opts.approval, signal: opts.signal };
|
|
994
|
+
await this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(sandbox)}`, call);
|
|
995
|
+
let restored;
|
|
996
|
+
try {
|
|
997
|
+
restored = await this.restoreSnapshot(target.id, { name: sandbox, ...call });
|
|
998
|
+
} catch (err) {
|
|
999
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1000
|
+
throw new Kosa8Error(
|
|
1001
|
+
`restore failed and ${sandbox} is now stopped: ${msg} \u2014 recover with restoreSnapshot(${JSON.stringify(target.id)}, { name: ${JSON.stringify(sandbox)} })`,
|
|
1002
|
+
{ cause: err }
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
let dropped = [];
|
|
1006
|
+
try {
|
|
1007
|
+
dropped = await this.checkpoints.pop(sandbox, target.id);
|
|
1008
|
+
for (const sid of dropped) await this.deleteSnapshot(sid, { approval: opts.approval }).catch(() => void 0);
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
return { sandbox: restored.sandbox, checkpoint: target, dropped, restore_ms: restored.restore_ms };
|
|
1012
|
+
}
|
|
1013
|
+
// --- toolbox: code -------------------------------------------------------------
|
|
1014
|
+
/**
|
|
1015
|
+
* POST /kosa8/v1/sandboxes/{id}/code: run a Python, JavaScript or
|
|
1016
|
+
* TypeScript snippet. It starts in `/workspace`. Charts a Python snippet
|
|
1017
|
+
* draws with matplotlib come back in `artifacts` as PNG bytes.
|
|
1018
|
+
*/
|
|
1019
|
+
async runCode(id, code, opts = {}) {
|
|
1020
|
+
const body = { language: opts.language ?? "python", code };
|
|
1021
|
+
if (opts.image) body.image = opts.image;
|
|
1022
|
+
if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
|
|
1023
|
+
if (opts.timeoutS) body.timeout_s = opts.timeoutS;
|
|
1024
|
+
const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/code`, {
|
|
1025
|
+
body,
|
|
1026
|
+
approval: opts.approval,
|
|
1027
|
+
signal: opts.signal
|
|
1028
|
+
});
|
|
1029
|
+
const exitCode = num(d.exit);
|
|
1030
|
+
const timedOut = d.timed_out === true;
|
|
1031
|
+
const arts = Array.isArray(d.artifacts) ? d.artifacts : [];
|
|
1032
|
+
return {
|
|
1033
|
+
ok: exitCode === 0 && !timedOut,
|
|
1034
|
+
exitCode,
|
|
1035
|
+
stdout: str(d.stdout),
|
|
1036
|
+
stderr: str(d.stderr),
|
|
1037
|
+
language: str(d.language),
|
|
1038
|
+
image: str(d.image),
|
|
1039
|
+
artifacts: arts.map((a) => ({ type: str(a.type), name: str(a.name), data: Buffer.from(str(a.data), "base64") })),
|
|
1040
|
+
durationMs: num(d.duration_ms),
|
|
1041
|
+
timedOut,
|
|
1042
|
+
truncated: d.truncated === true
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
// --- toolbox: files ------------------------------------------------------------
|
|
1046
|
+
/** GET …/files: a folder in the workspace (default `/workspace`), or one entry. */
|
|
1047
|
+
async listFiles(id, path4 = "/workspace", opts = {}) {
|
|
1048
|
+
const d = await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
|
|
1049
|
+
...opts,
|
|
1050
|
+
query: { path: path4 }
|
|
1051
|
+
});
|
|
1052
|
+
return d?.files ?? [];
|
|
1053
|
+
}
|
|
1054
|
+
/** GET …/files/stat: one entry, not following a symlink. */
|
|
1055
|
+
statFile(id, path4, opts = {}) {
|
|
1056
|
+
return this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files/stat`, { ...opts, query: { path: path4 } });
|
|
1057
|
+
}
|
|
1058
|
+
/** GET …/files/content: a file's bytes. A body cut short is a Kosa8StreamError. */
|
|
1059
|
+
async readFile(id, path4, opts = {}) {
|
|
1060
|
+
const p = `/kosa8/v1/sandboxes/${enc(id)}/files/content`;
|
|
1061
|
+
const res = await this.transport.raw("GET", p, { query: { path: path4 }, signal: opts.signal, approval: opts.approval });
|
|
1062
|
+
let data;
|
|
1063
|
+
try {
|
|
1064
|
+
data = await readAll(res.stream, opts.signal);
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
if (opts.signal?.aborted) throw err;
|
|
1067
|
+
throw new Kosa8StreamError(
|
|
1068
|
+
`${path4}: read cut short: ${err instanceof Error ? err.message : String(err)}`,
|
|
1069
|
+
{ stdout: "", stderr: "" },
|
|
1070
|
+
{ method: "GET", path: p, cause: err }
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
if (res.status >= 400) {
|
|
1074
|
+
const text = data.toString("utf8");
|
|
1075
|
+
let body = text;
|
|
1076
|
+
try {
|
|
1077
|
+
body = JSON.parse(text);
|
|
1078
|
+
} catch {
|
|
1079
|
+
}
|
|
1080
|
+
throw errorFromResponse(res.status, body, { method: "GET", path: p });
|
|
1081
|
+
}
|
|
1082
|
+
const want = res.headers["content-length"];
|
|
1083
|
+
if (want !== void 0 && Number(want) !== data.length) {
|
|
1084
|
+
throw new Kosa8StreamError(`${path4}: got ${data.length} of ${want} bytes`, { stdout: "", stderr: "" }, { method: "GET", path: p });
|
|
1085
|
+
}
|
|
1086
|
+
return data;
|
|
1087
|
+
}
|
|
1088
|
+
/** A file's contents as text (UTF-8 by default). */
|
|
1089
|
+
async readText(id, path4, opts = {}) {
|
|
1090
|
+
return (await this.readFile(id, path4, opts)).toString(opts.encoding ?? "utf8");
|
|
1091
|
+
}
|
|
1092
|
+
/** PUT …/files/content: replace a file, making its folders. Answers the new entry. */
|
|
1093
|
+
writeFile(id, path4, data, opts = {}) {
|
|
1094
|
+
return this.transport.json("PUT", `/kosa8/v1/sandboxes/${enc(id)}/files/content`, {
|
|
1095
|
+
query: { path: path4, mode: opts.mode },
|
|
1096
|
+
rawBody: data,
|
|
1097
|
+
approval: opts.approval,
|
|
1098
|
+
signal: opts.signal
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
/** DELETE …/files: a file, or with `recursive` a folder and everything in it. */
|
|
1102
|
+
deleteFile(id, path4, opts = {}) {
|
|
1103
|
+
return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
|
|
1104
|
+
query: { path: path4, recursive: opts.recursive },
|
|
1105
|
+
approval: opts.approval,
|
|
1106
|
+
signal: opts.signal
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
/** POST …/files/mkdir: a folder, with its parents. */
|
|
1110
|
+
makeDir(id, path4, opts = {}) {
|
|
1111
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/mkdir`, { ...opts, body: { path: path4 } });
|
|
1112
|
+
}
|
|
1113
|
+
/** POST …/files/move: rename or move a file or folder. Answers the entry at `to`. */
|
|
1114
|
+
moveFile(id, from, to, opts = {}) {
|
|
1115
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/move`, { ...opts, body: { from, to } });
|
|
1116
|
+
}
|
|
1117
|
+
// --- toolbox: git ----------------------------------------------------------------
|
|
1118
|
+
/**
|
|
1119
|
+
* POST …/git: one git operation in the workspace. A git failure resolves
|
|
1120
|
+
* with `ok: false`; only a refusal (bad op, no sandbox, policy) throws.
|
|
1121
|
+
*/
|
|
1122
|
+
async git(id, op, opts = {}) {
|
|
1123
|
+
const { approval, signal, authorName, authorEmail, ...fields } = opts;
|
|
1124
|
+
const body = { op };
|
|
1125
|
+
for (const [k, v] of Object.entries(fields)) if (v !== void 0) body[k] = v;
|
|
1126
|
+
if (authorName !== void 0) body.author_name = authorName;
|
|
1127
|
+
if (authorEmail !== void 0) body.author_email = authorEmail;
|
|
1128
|
+
const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/git`, {
|
|
1129
|
+
body,
|
|
1130
|
+
approval,
|
|
1131
|
+
signal
|
|
1132
|
+
});
|
|
1133
|
+
const out = {
|
|
1134
|
+
op: str(d.op) || op,
|
|
1135
|
+
ok: d.ok === true,
|
|
1136
|
+
exitCode: num(d.exit),
|
|
1137
|
+
stdout: str(d.stdout),
|
|
1138
|
+
stderr: str(d.stderr),
|
|
1139
|
+
path: str(d.path)
|
|
1140
|
+
};
|
|
1141
|
+
if (d.status) out.status = d.status;
|
|
1142
|
+
if (d.log) out.log = d.log;
|
|
1143
|
+
if (d.branches) out.branches = d.branches;
|
|
1144
|
+
return out;
|
|
1145
|
+
}
|
|
1146
|
+
/** Clone `url` into the workspace (into `path`, else a folder named after the repo). */
|
|
1147
|
+
gitClone(id, url, opts = {}) {
|
|
1148
|
+
return this.git(id, "clone", { ...opts, url });
|
|
1149
|
+
}
|
|
1150
|
+
/** `git status` of the repository at `path`, parsed into `status`. */
|
|
1151
|
+
gitStatus(id, path4, opts = {}) {
|
|
1152
|
+
return this.git(id, "status", { ...opts, path: path4 });
|
|
1153
|
+
}
|
|
1154
|
+
/** Stage everything (unless `addAll: false`) and commit. A failed add is returned as is. */
|
|
1155
|
+
async gitCommit(id, path4, message, opts = {}) {
|
|
1156
|
+
const { addAll = true, ...rest } = opts;
|
|
1157
|
+
if (addAll) {
|
|
1158
|
+
const r = await this.git(id, "add", { path: path4, image: rest.image, approval: rest.approval, signal: rest.signal });
|
|
1159
|
+
if (!r.ok) return r;
|
|
1160
|
+
}
|
|
1161
|
+
return this.git(id, "commit", { ...rest, path: path4, message });
|
|
1162
|
+
}
|
|
1163
|
+
// --- toolbox: shell sessions -------------------------------------------------------
|
|
1164
|
+
/** POST …/sessions: a shell that keeps `cd` and `export` between commands. */
|
|
1165
|
+
createSession(id, name, opts = {}) {
|
|
1166
|
+
const body = {};
|
|
1167
|
+
if (name) body.name = name;
|
|
1168
|
+
if (opts.image) body.image = opts.image;
|
|
1169
|
+
if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
|
|
1170
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, {
|
|
1171
|
+
body,
|
|
1172
|
+
approval: opts.approval,
|
|
1173
|
+
signal: opts.signal
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
/** GET …/sessions */
|
|
1177
|
+
async listSessions(id, opts = {}) {
|
|
1178
|
+
return await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, opts) ?? [];
|
|
1179
|
+
}
|
|
1180
|
+
/** POST …/sessions/{name}/exec: run one command line in the session. */
|
|
1181
|
+
async sessionExec(id, session, command, opts = {}) {
|
|
1182
|
+
const body = { command };
|
|
1183
|
+
if (opts.timeoutS) body.timeout_s = opts.timeoutS;
|
|
1184
|
+
const d = await this.transport.json(
|
|
1185
|
+
"POST",
|
|
1186
|
+
`/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}/exec`,
|
|
1187
|
+
{ body, approval: opts.approval, signal: opts.signal }
|
|
1188
|
+
);
|
|
1189
|
+
const exitCode = num(d.exit);
|
|
1190
|
+
const timedOut = d.timed_out === true;
|
|
1191
|
+
return {
|
|
1192
|
+
ok: exitCode === 0 && !timedOut,
|
|
1193
|
+
exitCode,
|
|
1194
|
+
stdout: str(d.stdout),
|
|
1195
|
+
stderr: str(d.stderr),
|
|
1196
|
+
cwd: str(d.cwd),
|
|
1197
|
+
durationMs: num(d.duration_ms),
|
|
1198
|
+
timedOut,
|
|
1199
|
+
truncated: d.truncated === true
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
/** Run a command in a session (default `main`), making the session on first use. */
|
|
1203
|
+
async shell(id, command, opts = {}) {
|
|
1204
|
+
const { session = "main", ...rest } = opts;
|
|
1205
|
+
try {
|
|
1206
|
+
return await this.sessionExec(id, session, command, rest);
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
if (!(err instanceof Kosa8NotFoundError) || !err.message.includes("no such session")) throw err;
|
|
1209
|
+
}
|
|
1210
|
+
await this.createSession(id, session, { approval: rest.approval, signal: rest.signal });
|
|
1211
|
+
return this.sessionExec(id, session, command, rest);
|
|
1212
|
+
}
|
|
1213
|
+
/** DELETE …/sessions/{name} */
|
|
1214
|
+
deleteSession(id, session, opts = {}) {
|
|
1215
|
+
return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}`, opts);
|
|
1216
|
+
}
|
|
1217
|
+
// --- toolbox: terminals ----------------------------------------------------------
|
|
1218
|
+
/**
|
|
1219
|
+
* POST …/pty: a container with a terminal; resolves with its id. Connect
|
|
1220
|
+
* to `/kosa8/v1/sandboxes/{id}/containers/{cid}/tty` with any WebSocket
|
|
1221
|
+
* client: binary messages are the terminal, text messages JSON control.
|
|
1222
|
+
*/
|
|
1223
|
+
async startPty(id, opts = {}) {
|
|
1224
|
+
const body = {};
|
|
1225
|
+
if (opts.image) body.image = opts.image;
|
|
1226
|
+
if (opts.command && opts.command.length > 0) body.command = opts.command;
|
|
1227
|
+
if (opts.cols && opts.rows) {
|
|
1228
|
+
body.cols = opts.cols;
|
|
1229
|
+
body.rows = opts.rows;
|
|
1230
|
+
}
|
|
1231
|
+
if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
|
|
1232
|
+
const out = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/pty`, {
|
|
1233
|
+
body,
|
|
1234
|
+
approval: opts.approval,
|
|
1235
|
+
signal: opts.signal
|
|
1236
|
+
});
|
|
1237
|
+
return out.id;
|
|
1238
|
+
}
|
|
1239
|
+
/** POST …/containers/{cid}/resize: set a terminal's size. */
|
|
1240
|
+
resizePty(id, container, cols, rows, opts = {}) {
|
|
1241
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/containers/${enc(container)}/resize`, {
|
|
1242
|
+
...opts,
|
|
1243
|
+
body: { cols, rows }
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
// --- toolbox: previews -----------------------------------------------------------
|
|
1247
|
+
/** POST …/previews: a signed link, which expires, to a web server in the sandbox. */
|
|
1248
|
+
createPreview(id, port, opts = {}) {
|
|
1249
|
+
const body = { port };
|
|
1250
|
+
if (opts.session) body.session = opts.session;
|
|
1251
|
+
if (opts.container) body.container = opts.container;
|
|
1252
|
+
if (opts.expiresInS) body.expires_in_s = opts.expiresInS;
|
|
1253
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/previews`, {
|
|
1254
|
+
body,
|
|
1255
|
+
approval: opts.approval,
|
|
1256
|
+
signal: opts.signal
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
/** POST /kosa8/v1/previews/rotate: end every preview link at once. */
|
|
1260
|
+
revokePreviews(opts = {}) {
|
|
1261
|
+
return this.transport.json("POST", "/kosa8/v1/previews/rotate", opts);
|
|
1262
|
+
}
|
|
1263
|
+
// --- toolbox: lifecycle ----------------------------------------------------------
|
|
1264
|
+
/**
|
|
1265
|
+
* POST …/lifecycle: change auto-stop, auto-delete, TTL or ephemeral.
|
|
1266
|
+
* Options left out keep their value; 0 (or `false`) turns one off.
|
|
1267
|
+
*/
|
|
1268
|
+
setLifecycle(id, opts = {}) {
|
|
1269
|
+
const body = {};
|
|
1270
|
+
if (opts.autoStopMin !== void 0) body.auto_stop_min = opts.autoStopMin;
|
|
1271
|
+
if (opts.autoDeleteMin !== void 0) body.auto_delete_min = opts.autoDeleteMin;
|
|
1272
|
+
if (opts.ttlMin !== void 0) body.ttl_min = opts.ttlMin;
|
|
1273
|
+
if (opts.ephemeral !== void 0) body.ephemeral = opts.ephemeral;
|
|
1274
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/lifecycle`, {
|
|
1275
|
+
body,
|
|
1276
|
+
approval: opts.approval,
|
|
1277
|
+
signal: opts.signal
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
// --- toolbox: templates ----------------------------------------------------------
|
|
1281
|
+
/** GET /kosa8/v1/templates */
|
|
1282
|
+
async listTemplates(opts = {}) {
|
|
1283
|
+
return await this.transport.json("GET", "/kosa8/v1/templates", opts) ?? [];
|
|
1284
|
+
}
|
|
1285
|
+
/** DELETE /kosa8/v1/templates/{name}. Sandboxes made from it keep their copy. */
|
|
1286
|
+
deleteTemplate(name, opts = {}) {
|
|
1287
|
+
return this.transport.json("DELETE", `/kosa8/v1/templates/${enc(name)}`, opts);
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* POST /kosa8/v1/templates: build `contextDir`'s Dockerfile in a throwaway
|
|
1291
|
+
* sandbox and keep the image as a template, for `createSandbox({ template })`.
|
|
1292
|
+
* The context is packed honouring `.dockerignore`. Resolves with the final
|
|
1293
|
+
* frame (`{ done, ok, image }`); a failed build throws Kosa8StreamError.
|
|
1294
|
+
*/
|
|
1295
|
+
async buildTemplate(name, contextDir = ".", opts = {}) {
|
|
1296
|
+
const dockerfile = opts.dockerfile ?? "Dockerfile";
|
|
1297
|
+
const q = new URLSearchParams();
|
|
1298
|
+
q.append("name", name);
|
|
1299
|
+
q.append("dockerfile", dockerfile);
|
|
1300
|
+
for (const [k, v] of Object.entries(opts.buildArgs ?? {})) q.append("buildarg", `${k}=${v}`);
|
|
1301
|
+
const egress = egressString(opts.egress);
|
|
1302
|
+
if (egress) q.append("egress", egress);
|
|
1303
|
+
if (opts.replace) q.append("replace", "1");
|
|
1304
|
+
const ctx = await tarContext(contextDir, dockerfile);
|
|
1305
|
+
let out = "";
|
|
1306
|
+
for await (const f of this.transport.frames("POST", `/kosa8/v1/templates?${q.toString()}`, {
|
|
1307
|
+
rawBody: ctx,
|
|
1308
|
+
approval: opts.approval,
|
|
1309
|
+
signal: opts.signal
|
|
1310
|
+
})) {
|
|
1311
|
+
if (f.done) {
|
|
1312
|
+
if (f.error) throw new Kosa8StreamError(f.error, { stdout: out, stderr: "" }, { daemonMessage: f.error });
|
|
1313
|
+
return f;
|
|
1314
|
+
}
|
|
1315
|
+
if (f.data) {
|
|
1316
|
+
out += f.data.toString("utf8");
|
|
1317
|
+
opts.onOutput?.(f.data, f.stream ?? "stdout");
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
throw new Kosa8StreamError("template build stream ended without a final frame", { stdout: out, stderr: "" });
|
|
1321
|
+
}
|
|
1322
|
+
// --- helpers -------------------------------------------------------------------
|
|
1323
|
+
/** A handle bound to one sandbox. */
|
|
1324
|
+
sandbox(id) {
|
|
1325
|
+
return new SandboxHandle(this, id);
|
|
1326
|
+
}
|
|
1327
|
+
async collect(frames, opts) {
|
|
1328
|
+
const out = [];
|
|
1329
|
+
const err = [];
|
|
1330
|
+
for await (const f of frames) {
|
|
1331
|
+
if (f.done) {
|
|
1332
|
+
const stdoutBytes = Buffer.concat(out);
|
|
1333
|
+
const stderrBytes = Buffer.concat(err);
|
|
1334
|
+
if (f.error) {
|
|
1335
|
+
throw new Kosa8StreamError(f.error, {
|
|
1336
|
+
stdout: stdoutBytes.toString("utf8"),
|
|
1337
|
+
stderr: stderrBytes.toString("utf8")
|
|
1338
|
+
}, { daemonMessage: f.error });
|
|
1339
|
+
}
|
|
1340
|
+
return {
|
|
1341
|
+
exitCode: typeof f.exit === "number" ? f.exit : 0,
|
|
1342
|
+
stdout: stdoutBytes.toString("utf8"),
|
|
1343
|
+
stderr: stderrBytes.toString("utf8"),
|
|
1344
|
+
stdoutBytes,
|
|
1345
|
+
stderrBytes
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
if (!f.data) continue;
|
|
1349
|
+
if (f.stream === "stderr") {
|
|
1350
|
+
err.push(f.data);
|
|
1351
|
+
opts.onStderr?.(f.data);
|
|
1352
|
+
} else if (f.stream === "status") {
|
|
1353
|
+
opts.onStatus?.(f.data);
|
|
1354
|
+
} else {
|
|
1355
|
+
out.push(f.data);
|
|
1356
|
+
opts.onStdout?.(f.data);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
throw new Kosa8StreamError("stream ended unexpectedly (no final frame)", {
|
|
1360
|
+
stdout: Buffer.concat(out).toString("utf8"),
|
|
1361
|
+
stderr: Buffer.concat(err).toString("utf8")
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
var SandboxHandle = class {
|
|
1366
|
+
constructor(client, id) {
|
|
1367
|
+
this.client = client;
|
|
1368
|
+
this.id = id;
|
|
1369
|
+
}
|
|
1370
|
+
info(opts) {
|
|
1371
|
+
return this.client.getSandbox(this.id, opts);
|
|
1372
|
+
}
|
|
1373
|
+
start(opts) {
|
|
1374
|
+
return this.client.startSandbox(this.id, opts);
|
|
1375
|
+
}
|
|
1376
|
+
stop(opts) {
|
|
1377
|
+
return this.client.stopSandbox(this.id, opts);
|
|
1378
|
+
}
|
|
1379
|
+
delete(opts) {
|
|
1380
|
+
return this.client.deleteSandbox(this.id, opts);
|
|
1381
|
+
}
|
|
1382
|
+
pull(ref, opts) {
|
|
1383
|
+
return this.client.pullIntoSandbox(this.id, ref, opts);
|
|
1384
|
+
}
|
|
1385
|
+
run(image, args, opts) {
|
|
1386
|
+
return this.client.run(this.id, image, args, opts);
|
|
1387
|
+
}
|
|
1388
|
+
startContainer(image, args, opts) {
|
|
1389
|
+
return this.client.startContainer(this.id, image, args, opts);
|
|
1390
|
+
}
|
|
1391
|
+
exec(container, args, opts) {
|
|
1392
|
+
return this.client.exec(this.id, container, args, opts);
|
|
1393
|
+
}
|
|
1394
|
+
containers(opts) {
|
|
1395
|
+
return this.client.sandboxContainers(this.id, opts);
|
|
1396
|
+
}
|
|
1397
|
+
images(opts) {
|
|
1398
|
+
return this.client.sandboxImages(this.id, opts);
|
|
1399
|
+
}
|
|
1400
|
+
egress(opts) {
|
|
1401
|
+
return this.client.sandboxEgress(this.id, opts);
|
|
1402
|
+
}
|
|
1403
|
+
snapshot(opts) {
|
|
1404
|
+
return this.client.snapshot(this.id, opts);
|
|
1405
|
+
}
|
|
1406
|
+
fork(opts) {
|
|
1407
|
+
return this.client.fork(this.id, opts);
|
|
1408
|
+
}
|
|
1409
|
+
explore(opts) {
|
|
1410
|
+
return this.client.explore(this.id, opts);
|
|
1411
|
+
}
|
|
1412
|
+
checkpoint(opts) {
|
|
1413
|
+
return this.client.checkpoint(this.id, opts);
|
|
1414
|
+
}
|
|
1415
|
+
checkpoints() {
|
|
1416
|
+
return this.client.listCheckpoints(this.id);
|
|
1417
|
+
}
|
|
1418
|
+
rewind(opts) {
|
|
1419
|
+
return this.client.rewind(this.id, opts);
|
|
1420
|
+
}
|
|
1421
|
+
runCode(code, opts) {
|
|
1422
|
+
return this.client.runCode(this.id, code, opts);
|
|
1423
|
+
}
|
|
1424
|
+
listFiles(path4, opts) {
|
|
1425
|
+
return this.client.listFiles(this.id, path4, opts);
|
|
1426
|
+
}
|
|
1427
|
+
statFile(path4, opts) {
|
|
1428
|
+
return this.client.statFile(this.id, path4, opts);
|
|
1429
|
+
}
|
|
1430
|
+
readFile(path4, opts) {
|
|
1431
|
+
return this.client.readFile(this.id, path4, opts);
|
|
1432
|
+
}
|
|
1433
|
+
readText(path4, opts) {
|
|
1434
|
+
return this.client.readText(this.id, path4, opts);
|
|
1435
|
+
}
|
|
1436
|
+
writeFile(path4, data, opts) {
|
|
1437
|
+
return this.client.writeFile(this.id, path4, data, opts);
|
|
1438
|
+
}
|
|
1439
|
+
deleteFile(path4, opts) {
|
|
1440
|
+
return this.client.deleteFile(this.id, path4, opts);
|
|
1441
|
+
}
|
|
1442
|
+
makeDir(path4, opts) {
|
|
1443
|
+
return this.client.makeDir(this.id, path4, opts);
|
|
1444
|
+
}
|
|
1445
|
+
moveFile(from, to, opts) {
|
|
1446
|
+
return this.client.moveFile(this.id, from, to, opts);
|
|
1447
|
+
}
|
|
1448
|
+
git(op, opts) {
|
|
1449
|
+
return this.client.git(this.id, op, opts);
|
|
1450
|
+
}
|
|
1451
|
+
gitClone(url, opts) {
|
|
1452
|
+
return this.client.gitClone(this.id, url, opts);
|
|
1453
|
+
}
|
|
1454
|
+
gitStatus(path4, opts) {
|
|
1455
|
+
return this.client.gitStatus(this.id, path4, opts);
|
|
1456
|
+
}
|
|
1457
|
+
gitCommit(path4, message, opts) {
|
|
1458
|
+
return this.client.gitCommit(this.id, path4, message, opts);
|
|
1459
|
+
}
|
|
1460
|
+
createSession(name, opts) {
|
|
1461
|
+
return this.client.createSession(this.id, name, opts);
|
|
1462
|
+
}
|
|
1463
|
+
sessions(opts) {
|
|
1464
|
+
return this.client.listSessions(this.id, opts);
|
|
1465
|
+
}
|
|
1466
|
+
sessionExec(session, command, opts) {
|
|
1467
|
+
return this.client.sessionExec(this.id, session, command, opts);
|
|
1468
|
+
}
|
|
1469
|
+
shell(command, opts) {
|
|
1470
|
+
return this.client.shell(this.id, command, opts);
|
|
1471
|
+
}
|
|
1472
|
+
deleteSession(session, opts) {
|
|
1473
|
+
return this.client.deleteSession(this.id, session, opts);
|
|
1474
|
+
}
|
|
1475
|
+
startPty(opts) {
|
|
1476
|
+
return this.client.startPty(this.id, opts);
|
|
1477
|
+
}
|
|
1478
|
+
resizePty(container, cols, rows, opts) {
|
|
1479
|
+
return this.client.resizePty(this.id, container, cols, rows, opts);
|
|
1480
|
+
}
|
|
1481
|
+
createPreview(port, opts) {
|
|
1482
|
+
return this.client.createPreview(this.id, port, opts);
|
|
1483
|
+
}
|
|
1484
|
+
setLifecycle(opts) {
|
|
1485
|
+
return this.client.setLifecycle(this.id, opts);
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
// src/cloud.ts
|
|
1490
|
+
var DEFAULT_CLOUD_URL = "https://api.kosa8.cloud";
|
|
1491
|
+
var CLOUD_KEY_PREFIX = "k8c_live_";
|
|
1492
|
+
function trimSlash(s) {
|
|
1493
|
+
return s.replace(/\/+$/, "");
|
|
1494
|
+
}
|
|
1495
|
+
function checkBase(url) {
|
|
1496
|
+
let u;
|
|
1497
|
+
try {
|
|
1498
|
+
u = new URL(url);
|
|
1499
|
+
} catch {
|
|
1500
|
+
throw new Kosa8Error(`kosa8 Cloud address is not a URL: ${url}`);
|
|
1501
|
+
}
|
|
1502
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
|
|
1503
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
|
|
1504
|
+
throw new Kosa8Error(`kosa8 Cloud address must use https: ${url}`);
|
|
1505
|
+
}
|
|
1506
|
+
return trimSlash(u.toString());
|
|
1507
|
+
}
|
|
1508
|
+
var Kosa8Cloud = class {
|
|
1509
|
+
baseUrl;
|
|
1510
|
+
sandboxes;
|
|
1511
|
+
functions;
|
|
1512
|
+
apiKey;
|
|
1513
|
+
fetchImpl;
|
|
1514
|
+
timeoutMs;
|
|
1515
|
+
constructor(options = {}) {
|
|
1516
|
+
const env = options.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
1517
|
+
const key = (options.apiKey ?? env.KOSA8_API_KEY ?? "").trim();
|
|
1518
|
+
if (!key) throw new Kosa8Error("no kosa8 Cloud API key: pass apiKey or set KOSA8_API_KEY");
|
|
1519
|
+
if (!key.startsWith(CLOUD_KEY_PREFIX)) throw new Kosa8Error(`a kosa8 Cloud API key starts with ${CLOUD_KEY_PREFIX}`);
|
|
1520
|
+
this.apiKey = key;
|
|
1521
|
+
this.baseUrl = checkBase(options.baseUrl ?? env.KOSA8_CLOUD_URL ?? DEFAULT_CLOUD_URL);
|
|
1522
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
1523
|
+
if (typeof f !== "function") throw new Kosa8Error("no fetch available; pass options.fetch");
|
|
1524
|
+
this.fetchImpl = f;
|
|
1525
|
+
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
1526
|
+
this.sandboxes = new CloudSandboxes(this);
|
|
1527
|
+
this.functions = new CloudFunctions(this);
|
|
1528
|
+
}
|
|
1529
|
+
/** The org, the key in use and, for a member's key, the member. */
|
|
1530
|
+
me() {
|
|
1531
|
+
return this.request("GET", "/v1/me");
|
|
1532
|
+
}
|
|
1533
|
+
/** This month's usage, bill estimate and whether work is paused for payment. */
|
|
1534
|
+
usage() {
|
|
1535
|
+
return this.request("GET", "/v1/usage");
|
|
1536
|
+
}
|
|
1537
|
+
/** @internal */
|
|
1538
|
+
async request(method, path4, body) {
|
|
1539
|
+
const ctl = new AbortController();
|
|
1540
|
+
const timer = setTimeout(() => ctl.abort(), this.timeoutMs);
|
|
1541
|
+
let resp;
|
|
1542
|
+
try {
|
|
1543
|
+
resp = await this.fetchImpl(`${this.baseUrl}${path4}`, {
|
|
1544
|
+
method,
|
|
1545
|
+
headers: {
|
|
1546
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1547
|
+
Accept: "application/json",
|
|
1548
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
1549
|
+
},
|
|
1550
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
1551
|
+
signal: ctl.signal
|
|
1552
|
+
});
|
|
1553
|
+
} catch (e) {
|
|
1554
|
+
if (ctl.signal.aborted) {
|
|
1555
|
+
throw new Kosa8TimeoutError(`kosa8 Cloud did not answer within ${this.timeoutMs} ms`, { method, path: path4, cause: e });
|
|
1556
|
+
}
|
|
1557
|
+
throw new Kosa8ConnectionError(`could not reach kosa8 Cloud at ${this.baseUrl}: ${String(e?.message ?? e)}`, { method, path: path4, cause: e });
|
|
1558
|
+
} finally {
|
|
1559
|
+
clearTimeout(timer);
|
|
1560
|
+
}
|
|
1561
|
+
const text = await resp.text();
|
|
1562
|
+
let data = text;
|
|
1563
|
+
try {
|
|
1564
|
+
data = text === "" ? null : JSON.parse(text);
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1567
|
+
if (!resp.ok) throw cloudError(resp.status, data, { method, path: path4 });
|
|
1568
|
+
return data;
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
function cloudError(status, body, ctx) {
|
|
1572
|
+
let code;
|
|
1573
|
+
let msgBody = body;
|
|
1574
|
+
if (typeof body === "object" && body !== null) {
|
|
1575
|
+
const b = body;
|
|
1576
|
+
if (typeof b.error === "string") code = b.error;
|
|
1577
|
+
msgBody = { error: typeof b.message === "string" && b.message ? b.message : b.error };
|
|
1578
|
+
}
|
|
1579
|
+
return errorFromResponse(status, msgBody, { ...ctx, code });
|
|
1580
|
+
}
|
|
1581
|
+
var CloudSandboxes = class {
|
|
1582
|
+
constructor(cloud) {
|
|
1583
|
+
this.cloud = cloud;
|
|
1584
|
+
}
|
|
1585
|
+
/** Start a sandbox; resolves once it is running. */
|
|
1586
|
+
async create(opts = {}) {
|
|
1587
|
+
const body = {};
|
|
1588
|
+
if (opts.template !== void 0) body.template = opts.template;
|
|
1589
|
+
if (opts.region !== void 0) body.region = opts.region;
|
|
1590
|
+
if (opts.sleepAfterS !== void 0) body.sleep_after_s = opts.sleepAfterS;
|
|
1591
|
+
const r = await this.cloud.request("POST", "/v1/sandboxes", body);
|
|
1592
|
+
return new CloudSandbox(this.cloud, r.sandbox);
|
|
1593
|
+
}
|
|
1594
|
+
/** The org's sandboxes; with all, deleted ones too. */
|
|
1595
|
+
async list(opts = {}) {
|
|
1596
|
+
const r = await this.cloud.request("GET", `/v1/sandboxes${opts.all ? "?all=1" : ""}`);
|
|
1597
|
+
return r.sandboxes.map((s) => new CloudSandbox(this.cloud, s));
|
|
1598
|
+
}
|
|
1599
|
+
async get(id) {
|
|
1600
|
+
const r = await this.cloud.request("GET", `/v1/sandboxes/${encodeURIComponent(id)}`);
|
|
1601
|
+
return new CloudSandbox(this.cloud, r.sandbox);
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
var CloudSandbox = class {
|
|
1605
|
+
constructor(cloud, info) {
|
|
1606
|
+
this.cloud = cloud;
|
|
1607
|
+
this.info = info;
|
|
1608
|
+
}
|
|
1609
|
+
get id() {
|
|
1610
|
+
return this.info.id;
|
|
1611
|
+
}
|
|
1612
|
+
path(suffix = "") {
|
|
1613
|
+
return `/v1/sandboxes/${encodeURIComponent(this.info.id)}${suffix}`;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Run a command and wait for it. A string runs through `sh -c`. A
|
|
1617
|
+
* sleeping sandbox wakes for it.
|
|
1618
|
+
*/
|
|
1619
|
+
async exec(cmd, opts = {}) {
|
|
1620
|
+
const argv = typeof cmd === "string" ? ["sh", "-c", cmd] : cmd;
|
|
1621
|
+
const body = { cmd: argv };
|
|
1622
|
+
if (opts.image !== void 0) body.image = opts.image;
|
|
1623
|
+
if (opts.env) body.env = Object.entries(opts.env).map(([k, v]) => `${k}=${v}`);
|
|
1624
|
+
const r = await this.cloud.request(
|
|
1625
|
+
"POST",
|
|
1626
|
+
this.path("/exec"),
|
|
1627
|
+
body
|
|
1628
|
+
);
|
|
1629
|
+
const out = { exitCode: r.exit, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
1630
|
+
if (typeof r.woke_ms === "number") out.wokeMs = r.woke_ms;
|
|
1631
|
+
return out;
|
|
1632
|
+
}
|
|
1633
|
+
/** Snapshot it and free its VM; it then costs only snapshot storage. */
|
|
1634
|
+
async sleep() {
|
|
1635
|
+
this.info = (await this.cloud.request("POST", this.path("/sleep"))).sandbox;
|
|
1636
|
+
return this;
|
|
1637
|
+
}
|
|
1638
|
+
/** Restore a sleeping sandbox, files and all. */
|
|
1639
|
+
async wake() {
|
|
1640
|
+
this.info = (await this.cloud.request("POST", this.path("/wake"))).sandbox;
|
|
1641
|
+
return this;
|
|
1642
|
+
}
|
|
1643
|
+
async refresh() {
|
|
1644
|
+
this.info = (await this.cloud.request("GET", this.path())).sandbox;
|
|
1645
|
+
return this;
|
|
1646
|
+
}
|
|
1647
|
+
async delete() {
|
|
1648
|
+
await this.cloud.request("DELETE", this.path());
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
var CloudFunctions = class {
|
|
1652
|
+
constructor(cloud) {
|
|
1653
|
+
this.cloud = cloud;
|
|
1654
|
+
}
|
|
1655
|
+
/** Deploy (create or replace) a function; every deploy bumps its version. */
|
|
1656
|
+
deploy(name, spec) {
|
|
1657
|
+
return this.cloud.request("PUT", `/v1/functions/${encodeURIComponent(name)}`, spec);
|
|
1658
|
+
}
|
|
1659
|
+
async list() {
|
|
1660
|
+
return (await this.cloud.request("GET", "/v1/functions")).functions;
|
|
1661
|
+
}
|
|
1662
|
+
get(name) {
|
|
1663
|
+
return this.cloud.request("GET", `/v1/functions/${encodeURIComponent(name)}`);
|
|
1664
|
+
}
|
|
1665
|
+
async delete(name) {
|
|
1666
|
+
await this.cloud.request("DELETE", `/v1/functions/${encodeURIComponent(name)}`);
|
|
1667
|
+
}
|
|
1668
|
+
/** Call it once. A handler that raises resolves with ok: false, not a throw. */
|
|
1669
|
+
invoke(name, event = null) {
|
|
1670
|
+
return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/invoke`, { event });
|
|
1671
|
+
}
|
|
1672
|
+
/** One call per event, results in input order. */
|
|
1673
|
+
map(name, events, opts = {}) {
|
|
1674
|
+
const body = { events };
|
|
1675
|
+
if (opts.concurrency !== void 0) body.concurrency = opts.concurrency;
|
|
1676
|
+
return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/map`, body);
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
export {
|
|
1680
|
+
ACTOR_HEADER,
|
|
1681
|
+
AGENT_TOKEN_HEADER,
|
|
1682
|
+
APPROVAL_HEADER,
|
|
1683
|
+
CLOUD_KEY_PREFIX,
|
|
1684
|
+
CheckpointBook,
|
|
1685
|
+
CloudFunctions,
|
|
1686
|
+
CloudSandbox,
|
|
1687
|
+
CloudSandboxes,
|
|
1688
|
+
DEFAULT_CLOUD_URL,
|
|
1689
|
+
FileCheckpointStore,
|
|
1690
|
+
Kosa8ApiError,
|
|
1691
|
+
Kosa8ApprovalRequiredError,
|
|
1692
|
+
Kosa8AuthError,
|
|
1693
|
+
Kosa8BadRequestError,
|
|
1694
|
+
Kosa8CheckpointError,
|
|
1695
|
+
Kosa8Client,
|
|
1696
|
+
Kosa8Cloud,
|
|
1697
|
+
Kosa8ConflictError,
|
|
1698
|
+
Kosa8ConnectionError,
|
|
1699
|
+
Kosa8Error,
|
|
1700
|
+
Kosa8NotFoundError,
|
|
1701
|
+
Kosa8PaymentRequiredError,
|
|
1702
|
+
Kosa8PolicyError,
|
|
1703
|
+
Kosa8RateLimitError,
|
|
1704
|
+
Kosa8StreamError,
|
|
1705
|
+
Kosa8TimeoutError,
|
|
1706
|
+
Kosa8UnsupportedError,
|
|
1707
|
+
MAX_CHECKPOINTS_PER_SANDBOX,
|
|
1708
|
+
MemoryCheckpointStore,
|
|
1709
|
+
SandboxHandle,
|
|
1710
|
+
Transport,
|
|
1711
|
+
defaultSocketPath,
|
|
1712
|
+
defaultWorkerFile,
|
|
1713
|
+
errorFromResponse,
|
|
1714
|
+
resolveEndpoint,
|
|
1715
|
+
tarContext,
|
|
1716
|
+
validateSandboxName
|
|
1717
|
+
};
|
|
1718
|
+
//# sourceMappingURL=index.js.map
|