sandboxedjs 0.1.93 → 0.1.95
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/bin/sandboxedjs-pack.mjs +363 -0
- package/dist/agent.d.cts +2 -2
- package/dist/agent.d.ts +2 -2
- package/dist/{container-BGmhPbSD.d.cts → container-CFieZcbe.d.ts} +10 -1
- package/dist/{container-DCwYX_Cx.d.ts → container-CPwyHoUy.d.cts} +10 -1
- package/dist/{contracts-BkWBTH8E.d.cts → contracts-B6SHFjma.d.cts} +109 -21
- package/dist/{contracts-BkWBTH8E.d.ts → contracts-B6SHFjma.d.ts} +109 -21
- package/dist/index.cjs +1678 -102
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1678 -102
- package/dist/{memory-volume-DCm-DiXH.d.cts → memory-volume-CRhXUyCN.d.cts} +1 -1
- package/dist/{memory-volume-DJA-vFYW.d.ts → memory-volume-CwiXvRfs.d.ts} +1 -1
- package/dist/python/python.data +461 -303
- package/dist/python/python.js +1 -1
- package/dist/python/python.wasm +0 -0
- package/dist/python/runtime.json +3 -3
- package/dist/python-abi.cjs +56 -10
- package/dist/python-abi.d.cts +4 -3
- package/dist/python-abi.d.ts +4 -3
- package/dist/python-abi.js +56 -10
- package/dist/python-worker.js +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build and sign a `.sbjs` application package.
|
|
4
|
+
*
|
|
5
|
+
* sandboxedjs-pack keygen --out publisher.key --publisher "Your Name"
|
|
6
|
+
* sandboxedjs-pack build ./app-dir --out officesuite-0.1.0.sbjs --key publisher.key
|
|
7
|
+
* sandboxedjs-pack inspect officesuite-0.1.0.sbjs
|
|
8
|
+
*
|
|
9
|
+
* The directory must contain an `app.json` describing the application. What
|
|
10
|
+
* the packer adds is `META/manifest.json` — a sha256 for every file — and
|
|
11
|
+
* `META/signature.json`, an Ed25519 signature over that manifest's bytes.
|
|
12
|
+
*
|
|
13
|
+
* The private key never leaves the machine that signs, and is written with
|
|
14
|
+
* mode 600. There is no key recovery: a lost signing key means publishing
|
|
15
|
+
* under a new one, which every container then has to be told to trust.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash, generateKeyPairSync, sign as signBytes, createPrivateKey, createPublicKey } from "node:crypto";
|
|
19
|
+
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
21
|
+
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
22
|
+
|
|
23
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
24
|
+
const flags = parseFlags(rest);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
switch (command) {
|
|
28
|
+
case "keygen": await keygen(); break;
|
|
29
|
+
case "build": await build(); break;
|
|
30
|
+
case "inspect": await inspect(); break;
|
|
31
|
+
case "help": case "--help": case "-h": case undefined: usage(); break;
|
|
32
|
+
default:
|
|
33
|
+
console.error(`sandboxedjs-pack: unknown command ${command}`);
|
|
34
|
+
usage();
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`sandboxedjs-pack: ${error.message}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function usage() {
|
|
43
|
+
console.log(`sandboxedjs-pack — build and sign .sbjs application packages
|
|
44
|
+
|
|
45
|
+
keygen --out KEY --publisher NAME
|
|
46
|
+
build DIR --out FILE.sbjs --key KEY [--unsigned]
|
|
47
|
+
inspect FILE.sbjs
|
|
48
|
+
|
|
49
|
+
A .sbjs is a zip: the application's files, plus META/manifest.json listing a
|
|
50
|
+
sha256 for each of them, plus META/signature.json signing that manifest.
|
|
51
|
+
\`pm\` checks both before installing, and refuses a package whose signing key
|
|
52
|
+
the container does not trust.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ------------------------------------------------------------------ keygen
|
|
56
|
+
|
|
57
|
+
async function keygen() {
|
|
58
|
+
const out = flags.out ?? "publisher.key";
|
|
59
|
+
const publisher = flags.publisher;
|
|
60
|
+
if (!publisher) throw new Error("keygen needs --publisher, the name packages are signed as");
|
|
61
|
+
|
|
62
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
63
|
+
const secret = {
|
|
64
|
+
format: "sandboxedjs-publisher-key",
|
|
65
|
+
publisher,
|
|
66
|
+
privateKey: privateKey.export({ type: "pkcs8", format: "der" }).toString("base64"),
|
|
67
|
+
publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64"),
|
|
68
|
+
createdAt: new Date().toISOString(),
|
|
69
|
+
};
|
|
70
|
+
writeFileSync(out, `${JSON.stringify(secret, null, 2)}\n`, { mode: 0o600 });
|
|
71
|
+
|
|
72
|
+
const print = fingerprint(secret.publicKey);
|
|
73
|
+
console.log(`Wrote ${out} (keep it secret; there is no recovery)`);
|
|
74
|
+
console.log(`publisher: ${publisher}`);
|
|
75
|
+
console.log(`fingerprint: ${print}`);
|
|
76
|
+
console.log(`public key: ${secret.publicKey}`);
|
|
77
|
+
console.log("");
|
|
78
|
+
console.log("Containers trust it with:");
|
|
79
|
+
console.log(` pm trust add ${JSON.stringify(publisher)} ${secret.publicKey}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ------------------------------------------------------------------- build
|
|
83
|
+
|
|
84
|
+
async function build() {
|
|
85
|
+
const directory = resolve(rest.find((argument) => !argument.startsWith("--")) ?? ".");
|
|
86
|
+
const manifestPath = join(directory, "app.json");
|
|
87
|
+
let app;
|
|
88
|
+
try {
|
|
89
|
+
app = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(`cannot read ${manifestPath}: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
for (const field of ["name", "version"]) {
|
|
94
|
+
if (typeof app[field] !== "string" || !app[field]) {
|
|
95
|
+
throw new Error(`app.json has no ${field}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const files = collect(directory).filter((path) => !path.startsWith("META/"));
|
|
100
|
+
if (!files.includes("app.json")) throw new Error("app.json must be in the package");
|
|
101
|
+
for (const binary of app.bins ?? []) {
|
|
102
|
+
if (!files.includes(binary.path)) {
|
|
103
|
+
/* Caught here rather than at install time, where the package is already
|
|
104
|
+
* on someone else's machine. */
|
|
105
|
+
throw new Error(`app.json declares the command ${binary.name} at ${binary.path}, which is not in ${directory}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const digests = {};
|
|
110
|
+
const contents = new Map();
|
|
111
|
+
for (const path of files) {
|
|
112
|
+
const bytes = readFileSync(join(directory, path));
|
|
113
|
+
contents.set(path, bytes);
|
|
114
|
+
digests[path] = createHash("sha256").update(bytes).digest("hex");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const manifest = {
|
|
118
|
+
format: "sandboxedjs-package",
|
|
119
|
+
schemaVersion: 1,
|
|
120
|
+
app,
|
|
121
|
+
files: digests,
|
|
122
|
+
built: new Date().toISOString(),
|
|
123
|
+
};
|
|
124
|
+
/* The signature covers these exact bytes, so they are written once and used
|
|
125
|
+
* both for signing and for the archive. Re-serialising before signing would
|
|
126
|
+
* risk signing a different string than the one shipped. */
|
|
127
|
+
const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
128
|
+
contents.set("META/manifest.json", manifestBytes);
|
|
129
|
+
|
|
130
|
+
if (!flags.unsigned) {
|
|
131
|
+
const keyPath = flags.key;
|
|
132
|
+
if (!keyPath) {
|
|
133
|
+
throw new Error("build needs --key, or --unsigned to publish without a publisher");
|
|
134
|
+
}
|
|
135
|
+
const key = JSON.parse(readFileSync(keyPath, "utf8"));
|
|
136
|
+
const privateKey = createPrivateKey({
|
|
137
|
+
key: Buffer.from(key.privateKey, "base64"), format: "der", type: "pkcs8",
|
|
138
|
+
});
|
|
139
|
+
const signature = {
|
|
140
|
+
format: "sandboxedjs-signature",
|
|
141
|
+
schemaVersion: 1,
|
|
142
|
+
algorithm: "ed25519",
|
|
143
|
+
publisher: key.publisher,
|
|
144
|
+
publicKey: key.publicKey
|
|
145
|
+
?? createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString("base64"),
|
|
146
|
+
signature: signBytes(null, manifestBytes, privateKey).toString("base64"),
|
|
147
|
+
};
|
|
148
|
+
contents.set("META/signature.json", Buffer.from(`${JSON.stringify(signature, null, 2)}\n`));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const out = flags.out ?? `${app.name}-${app.version}.sbjs`;
|
|
152
|
+
mkdirSync(dirname(resolve(out)), { recursive: true });
|
|
153
|
+
const archive = zip(contents, new Set((app.bins ?? []).map((binary) => binary.path)));
|
|
154
|
+
writeFileSync(out, archive);
|
|
155
|
+
|
|
156
|
+
const digest = createHash("sha256").update(archive).digest("hex");
|
|
157
|
+
console.log(`${out} ${(archive.length / 1024).toFixed(1)} kB`);
|
|
158
|
+
console.log(` ${app.name} ${app.version}, ${files.length} files`);
|
|
159
|
+
console.log(` sha256: ${digest}`);
|
|
160
|
+
console.log(flags.unsigned ? " unsigned" : ` signed by ${JSON.parse(readFileSync(flags.key, "utf8")).publisher}`);
|
|
161
|
+
/* What the registry index needs, so publishing is copy and paste. */
|
|
162
|
+
console.log("\nRegistry entry:");
|
|
163
|
+
console.log(JSON.stringify({
|
|
164
|
+
name: app.name, version: app.version, summary: app.summary,
|
|
165
|
+
filename: basename(out), sha256: digest, size: archive.length,
|
|
166
|
+
bins: app.bins ?? [], services: app.services ?? [], requires: app.requires ?? [],
|
|
167
|
+
docs: app.docs, license: app.license,
|
|
168
|
+
}, null, 2));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ----------------------------------------------------------------- inspect
|
|
172
|
+
|
|
173
|
+
async function inspect() {
|
|
174
|
+
const file = rest.find((argument) => !argument.startsWith("--"));
|
|
175
|
+
if (!file) throw new Error("inspect needs a .sbjs file");
|
|
176
|
+
const bytes = readFileSync(file);
|
|
177
|
+
const entries = readZip(bytes);
|
|
178
|
+
const manifestEntry = entries.find((entry) => entry.name === "META/manifest.json");
|
|
179
|
+
if (!manifestEntry) throw new Error(`${file} has no META/manifest.json`);
|
|
180
|
+
const manifest = JSON.parse(manifestEntry.data.toString("utf8"));
|
|
181
|
+
const signatureEntry = entries.find((entry) => entry.name === "META/signature.json");
|
|
182
|
+
|
|
183
|
+
console.log(`${manifest.app.name} ${manifest.app.version}`);
|
|
184
|
+
if (manifest.app.summary) console.log(` ${manifest.app.summary}`);
|
|
185
|
+
console.log(` built: ${manifest.built ?? "unknown"}`);
|
|
186
|
+
console.log(` files: ${Object.keys(manifest.files).length}`);
|
|
187
|
+
|
|
188
|
+
let mismatched = 0;
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if (entry.name.startsWith("META/")) continue;
|
|
191
|
+
const actual = createHash("sha256").update(entry.data).digest("hex");
|
|
192
|
+
if (manifest.files[entry.name] !== actual) {
|
|
193
|
+
console.log(` ALTERED: ${entry.name}`);
|
|
194
|
+
mismatched += 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
console.log(mismatched === 0 ? " every file matches the manifest" : ` ${mismatched} files do not match`);
|
|
198
|
+
|
|
199
|
+
if (!signatureEntry) {
|
|
200
|
+
console.log(" unsigned: this package has no publisher");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const signature = JSON.parse(signatureEntry.data.toString("utf8"));
|
|
204
|
+
console.log(` signed by: ${signature.publisher ?? "unnamed"}`);
|
|
205
|
+
console.log(` fingerprint: ${fingerprint(signature.publicKey)}`);
|
|
206
|
+
const { verify } = await import("node:crypto");
|
|
207
|
+
const valid = verify(
|
|
208
|
+
null, manifestEntry.data,
|
|
209
|
+
createPublicKey({ key: Buffer.from(signature.publicKey, "base64"), format: "der", type: "spki" }),
|
|
210
|
+
Buffer.from(signature.signature, "base64"),
|
|
211
|
+
);
|
|
212
|
+
console.log(valid ? " signature: valid" : " signature: DOES NOT MATCH");
|
|
213
|
+
if (!valid || mismatched > 0) process.exitCode = 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ------------------------------------------------------------------ pieces
|
|
217
|
+
|
|
218
|
+
function collect(root, prefix = "") {
|
|
219
|
+
const out = [];
|
|
220
|
+
for (const entry of readdirSync(join(root, prefix), { withFileTypes: true })) {
|
|
221
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
out.push(...collect(root, path));
|
|
224
|
+
} else if (entry.isFile()) {
|
|
225
|
+
out.push(path);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out.sort();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function fingerprint(publicKey) {
|
|
232
|
+
return createHash("sha256")
|
|
233
|
+
.update(Buffer.from(publicKey, "base64"))
|
|
234
|
+
.digest("hex")
|
|
235
|
+
.slice(0, 32)
|
|
236
|
+
.replace(/(.{4})(?=.)/g, "$1-");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseFlags(args) {
|
|
240
|
+
const flags = {};
|
|
241
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
242
|
+
const argument = args[index];
|
|
243
|
+
if (!argument.startsWith("--")) continue;
|
|
244
|
+
const name = argument.slice(2);
|
|
245
|
+
if (name.includes("=")) {
|
|
246
|
+
const [key, value] = name.split("=");
|
|
247
|
+
flags[key] = value;
|
|
248
|
+
} else if (args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
249
|
+
flags[name] = args[index + 1];
|
|
250
|
+
index += 1;
|
|
251
|
+
} else {
|
|
252
|
+
flags[name] = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return flags;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** A zip with deflate, and the executable bit on declared binaries. */
|
|
259
|
+
function zip(contents, executables) {
|
|
260
|
+
const locals = [];
|
|
261
|
+
const central = [];
|
|
262
|
+
let offset = 0;
|
|
263
|
+
|
|
264
|
+
for (const [name, data] of contents) {
|
|
265
|
+
const nameBytes = Buffer.from(name, "utf8");
|
|
266
|
+
const compressed = deflateRawSync(data);
|
|
267
|
+
const crc = crc32(data);
|
|
268
|
+
const mode = executables.has(name) ? 0o100755 : 0o100644;
|
|
269
|
+
|
|
270
|
+
const local = Buffer.alloc(30 + nameBytes.length);
|
|
271
|
+
/* Offsets are the zip specification's, exactly: writing the CRC two bytes
|
|
272
|
+
* late put it where the compressed size belongs, which our own reader
|
|
273
|
+
* ignored and `unzip` rejected as "bad CRC". */
|
|
274
|
+
local.writeUInt32LE(0x04034b50, 0); // signature
|
|
275
|
+
local.writeUInt16LE(20, 4); // version needed
|
|
276
|
+
local.writeUInt16LE(0, 6); // flags
|
|
277
|
+
local.writeUInt16LE(8, 8); // deflate
|
|
278
|
+
local.writeUInt16LE(0x0021, 10); // a fixed time, so two builds of the
|
|
279
|
+
local.writeUInt16LE(0x5a21, 12); // same input produce the same package
|
|
280
|
+
local.writeUInt32LE(crc, 14);
|
|
281
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
282
|
+
local.writeUInt32LE(data.length, 22);
|
|
283
|
+
local.writeUInt16LE(nameBytes.length, 26);
|
|
284
|
+
local.writeUInt16LE(0, 28); // extra length
|
|
285
|
+
nameBytes.copy(local, 30);
|
|
286
|
+
locals.push(local, compressed);
|
|
287
|
+
|
|
288
|
+
const entry = Buffer.alloc(46 + nameBytes.length);
|
|
289
|
+
entry.writeUInt32LE(0x02014b50, 0); // signature
|
|
290
|
+
entry.writeUInt16LE(0x031e, 4); // made by: unix, so the mode is read
|
|
291
|
+
entry.writeUInt16LE(20, 6); // version needed
|
|
292
|
+
entry.writeUInt16LE(0, 8); // flags
|
|
293
|
+
entry.writeUInt16LE(8, 10); // deflate
|
|
294
|
+
entry.writeUInt16LE(0x0021, 12); // time
|
|
295
|
+
entry.writeUInt16LE(0x5a21, 14); // date
|
|
296
|
+
entry.writeUInt32LE(crc, 16);
|
|
297
|
+
entry.writeUInt32LE(compressed.length, 20);
|
|
298
|
+
entry.writeUInt32LE(data.length, 24);
|
|
299
|
+
entry.writeUInt16LE(nameBytes.length, 28);
|
|
300
|
+
entry.writeUInt16LE(0, 30); // extra length
|
|
301
|
+
entry.writeUInt16LE(0, 32); // comment length
|
|
302
|
+
entry.writeUInt16LE(0, 34); // disk number
|
|
303
|
+
entry.writeUInt16LE(0, 36); // internal attributes
|
|
304
|
+
/* External attributes: the unix mode lives in the high 16 bits, and
|
|
305
|
+
* `<< 16` overflows a signed 32-bit int in JavaScript -- writeUInt32LE
|
|
306
|
+
* then refuses the negative number rather than storing the bits. */
|
|
307
|
+
entry.writeUInt32LE((mode * 0x10000) >>> 0, 38);
|
|
308
|
+
entry.writeUInt32LE(offset, 42);
|
|
309
|
+
nameBytes.copy(entry, 46);
|
|
310
|
+
central.push(entry);
|
|
311
|
+
|
|
312
|
+
offset += local.length + compressed.length;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const directory = Buffer.concat(central);
|
|
316
|
+
const end = Buffer.alloc(22);
|
|
317
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
318
|
+
end.writeUInt16LE(contents.size, 8);
|
|
319
|
+
end.writeUInt16LE(contents.size, 10);
|
|
320
|
+
end.writeUInt32LE(directory.length, 12);
|
|
321
|
+
end.writeUInt32LE(offset, 16);
|
|
322
|
+
return Buffer.concat([...locals, directory, end]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Minimal central-directory reader, for `inspect`. */
|
|
326
|
+
function readZip(bytes) {
|
|
327
|
+
const entries = [];
|
|
328
|
+
let end = bytes.length - 22;
|
|
329
|
+
while (end >= 0 && bytes.readUInt32LE(end) !== 0x06054b50) end -= 1;
|
|
330
|
+
if (end < 0) throw new Error("not a zip archive");
|
|
331
|
+
const count = bytes.readUInt16LE(end + 10);
|
|
332
|
+
let at = bytes.readUInt32LE(end + 16);
|
|
333
|
+
|
|
334
|
+
for (let index = 0; index < count; index += 1) {
|
|
335
|
+
if (bytes.readUInt32LE(at) !== 0x02014b50) throw new Error("damaged central directory");
|
|
336
|
+
const method = bytes.readUInt16LE(at + 10);
|
|
337
|
+
const compressedSize = bytes.readUInt32LE(at + 20);
|
|
338
|
+
const nameLength = bytes.readUInt16LE(at + 28);
|
|
339
|
+
const extraLength = bytes.readUInt16LE(at + 30);
|
|
340
|
+
const commentLength = bytes.readUInt16LE(at + 32);
|
|
341
|
+
const localOffset = bytes.readUInt32LE(at + 42);
|
|
342
|
+
const name = bytes.toString("utf8", at + 46, at + 46 + nameLength);
|
|
343
|
+
|
|
344
|
+
const localNameLength = bytes.readUInt16LE(localOffset + 26);
|
|
345
|
+
const localExtraLength = bytes.readUInt16LE(localOffset + 28);
|
|
346
|
+
const start = localOffset + 30 + localNameLength + localExtraLength;
|
|
347
|
+
const raw = bytes.subarray(start, start + compressedSize);
|
|
348
|
+
entries.push({ name, data: method === 0 ? raw : inflateRawSync(raw) });
|
|
349
|
+
at += 46 + nameLength + extraLength + commentLength;
|
|
350
|
+
}
|
|
351
|
+
return entries;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function crc32(buffer) {
|
|
355
|
+
let crc = ~0;
|
|
356
|
+
for (const byte of buffer) {
|
|
357
|
+
crc ^= byte;
|
|
358
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
359
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return (~crc) >>> 0;
|
|
363
|
+
}
|
package/dist/agent.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as Container } from './container-
|
|
2
|
-
import './contracts-
|
|
1
|
+
import { C as Container } from './container-CPwyHoUy.cjs';
|
|
2
|
+
import './contracts-B6SHFjma.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Structural copies of the LangChain Deep Agents backend contract.
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-
|
|
1
|
+
import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-B6SHFjma.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Byte streams for stdin/stdout/stderr, pipelines and redirections.
|
|
@@ -1455,6 +1455,15 @@ interface ContainerOptions {
|
|
|
1455
1455
|
/** CPU count reported by `nproc` and `/proc/cpuinfo`. Default 4. */
|
|
1456
1456
|
cpus?: number;
|
|
1457
1457
|
network?: NetworkOptions;
|
|
1458
|
+
/**
|
|
1459
|
+
* Applications to install before the container is handed back, by name or
|
|
1460
|
+
* `name@version` — the same specifiers `pm install` takes.
|
|
1461
|
+
*
|
|
1462
|
+
* These are programs, not language packages: `pip` and `npm` install those.
|
|
1463
|
+
* Installing needs the registry to be reachable, so a container with
|
|
1464
|
+
* outbound access turned off has to carry them another way.
|
|
1465
|
+
*/
|
|
1466
|
+
modules?: string[];
|
|
1458
1467
|
timezone?: string;
|
|
1459
1468
|
/** Default wall-clock limit for `exec`. Omit for no limit. */
|
|
1460
1469
|
timeoutMs?: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-
|
|
1
|
+
import { V as Vfs, C as Cred, f as RuntimePod, O as OutboundPolicy, D as DirEntry, p as Stats } from './contracts-B6SHFjma.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Byte streams for stdin/stdout/stderr, pipelines and redirections.
|
|
@@ -1455,6 +1455,15 @@ interface ContainerOptions {
|
|
|
1455
1455
|
/** CPU count reported by `nproc` and `/proc/cpuinfo`. Default 4. */
|
|
1456
1456
|
cpus?: number;
|
|
1457
1457
|
network?: NetworkOptions;
|
|
1458
|
+
/**
|
|
1459
|
+
* Applications to install before the container is handed back, by name or
|
|
1460
|
+
* `name@version` — the same specifiers `pm install` takes.
|
|
1461
|
+
*
|
|
1462
|
+
* These are programs, not language packages: `pip` and `npm` install those.
|
|
1463
|
+
* Installing needs the registry to be reachable, so a container with
|
|
1464
|
+
* outbound access turned off has to carry them another way.
|
|
1465
|
+
*/
|
|
1466
|
+
modules?: string[];
|
|
1458
1467
|
timezone?: string;
|
|
1459
1468
|
/** Default wall-clock limit for `exec`. Omit for no limit. */
|
|
1460
1469
|
timeoutMs?: number;
|
|
@@ -95,6 +95,101 @@ declare function createChildProcessModule(spawnChild: SpawnChild, defaultCwd: ()
|
|
|
95
95
|
ipc?: IpcTransport;
|
|
96
96
|
}): Record<string, unknown>;
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The outbound network policy, as plain functions every client consults.
|
|
100
|
+
*
|
|
101
|
+
* The container has several ways out — `curl` and `wget` in the shell, `http`,
|
|
102
|
+
* `https`, `fetch` and `WebSocket` in a Node program, sockets in Python — and
|
|
103
|
+
* they used to decide separately. Only the shell asked: a Node program's
|
|
104
|
+
* `fetch("https://…")` reached the internet from a container booted with
|
|
105
|
+
* outbound access off. One policy, applied at each exit, is what makes
|
|
106
|
+
* `network: { allowOutbound: false }` mean what it says.
|
|
107
|
+
*
|
|
108
|
+
* Loopback is not "outbound" at all. `127.0.0.1` inside the container is the
|
|
109
|
+
* container, so those requests are routed to its own servers and never handed
|
|
110
|
+
* to the host's network stack, whatever the policy allows.
|
|
111
|
+
*/
|
|
112
|
+
interface OutboundPolicy {
|
|
113
|
+
/** Whether requests may leave the container at all. */
|
|
114
|
+
allowOutbound: boolean;
|
|
115
|
+
/** When outbound is on, the hosts it may reach (subdomains included). `null` means any. */
|
|
116
|
+
allowedHosts: string[] | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Outbound TCP: names resolved by the host, connections dialled by the host.
|
|
121
|
+
*
|
|
122
|
+
* Until now a guest socket could reach only loopback. Everything that speaks a
|
|
123
|
+
* protocol other than HTTP -- Postgres, Redis, SMTP, an LLM gateway over a raw
|
|
124
|
+
* stream -- was therefore unreachable, and so was every HTTP client that opens
|
|
125
|
+
* its own socket rather than going through the egress. Extensions do not help:
|
|
126
|
+
* no wheel can create a connection the container cannot make.
|
|
127
|
+
*
|
|
128
|
+
* Two things are needed, and both belong to the host.
|
|
129
|
+
*
|
|
130
|
+
* **Names.** Emscripten's own `getaddrinfo` invents an address per hostname and
|
|
131
|
+
* keeps the table inside the guest's JavaScript module, where the kernel cannot
|
|
132
|
+
* see it -- so a later `connect` arrived as an address nobody could map back to
|
|
133
|
+
* a name. Resolution is therefore a host operation: the host allocates the
|
|
134
|
+
* address, remembers which name it stands for, and recognises it on connect. A
|
|
135
|
+
* guest still sees ordinary addresses, `getaddrinfo` still returns tuples, and
|
|
136
|
+
* reverse lookup answers.
|
|
137
|
+
*
|
|
138
|
+
* **The connection.** Only the host can open a socket. In Node that is
|
|
139
|
+
* `node:net`; in a browser there is no such thing, and a page cannot be given
|
|
140
|
+
* one, so a browser host supplies no dialer and outbound connects fail with a
|
|
141
|
+
* message that says to use the HTTP egress instead.
|
|
142
|
+
*
|
|
143
|
+
* The outbound policy applies here as it does at every other exit, and it is
|
|
144
|
+
* applied to the *name* the guest asked for, not to the address it was handed.
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
/** A real connection the host owns, as this module needs to use it. */
|
|
148
|
+
interface HostTcpConnection {
|
|
149
|
+
write(bytes: Uint8Array): void;
|
|
150
|
+
/** Half-close: the guest has finished writing. */
|
|
151
|
+
end(): void;
|
|
152
|
+
close(): void;
|
|
153
|
+
onData(handler: (bytes: Uint8Array) => void): void;
|
|
154
|
+
onClose(handler: () => void): void;
|
|
155
|
+
/** The address the host actually connected to, for `getpeername`. */
|
|
156
|
+
readonly remoteAddress: string;
|
|
157
|
+
readonly remotePort: number;
|
|
158
|
+
readonly localPort: number;
|
|
159
|
+
}
|
|
160
|
+
/** What a host must provide for a guest to reach the network. */
|
|
161
|
+
interface TcpDialer {
|
|
162
|
+
(host: string, port: number): Promise<HostTcpConnection>;
|
|
163
|
+
}
|
|
164
|
+
declare class OutboundTcp {
|
|
165
|
+
private readonly policy;
|
|
166
|
+
private readonly dialer;
|
|
167
|
+
private readonly byName;
|
|
168
|
+
private readonly byAddress;
|
|
169
|
+
private next;
|
|
170
|
+
constructor(policy: () => OutboundPolicy, dialer: TcpDialer | null);
|
|
171
|
+
/** Whether this address was handed out by `resolve`. */
|
|
172
|
+
knows(address: string): boolean;
|
|
173
|
+
hostnameFor(address: string): string | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* The address for `hostname`, allocating one on first use.
|
|
176
|
+
*
|
|
177
|
+
* Refusal happens here as well as at connect, because a name that cannot be
|
|
178
|
+
* reached should fail as a resolution failure -- which is what every client
|
|
179
|
+
* reports as "unknown host" rather than as a mid-connection error.
|
|
180
|
+
*/
|
|
181
|
+
resolve(hostname: string): string;
|
|
182
|
+
/** Open a connection to a resolved address, or to a literal one. */
|
|
183
|
+
connect(address: string, port: number, local: {
|
|
184
|
+
address: string;
|
|
185
|
+
port: number;
|
|
186
|
+
}): Promise<{
|
|
187
|
+
connection: VirtualTcpConnection;
|
|
188
|
+
host: HostTcpConnection;
|
|
189
|
+
}>;
|
|
190
|
+
private allowed;
|
|
191
|
+
}
|
|
192
|
+
|
|
98
193
|
type FileKind = "file" | "directory" | "symlink" | "chardev" | "blockdev" | "fifo" | "socket";
|
|
99
194
|
/** Render as `drwxr-xr-x`, honouring setuid/setgid/sticky. */
|
|
100
195
|
declare function formatMode(mode: number): string;
|
|
@@ -368,36 +463,29 @@ declare class VirtualTcpListener {
|
|
|
368
463
|
declare class VirtualTcpNetwork {
|
|
369
464
|
private readonly occupied?;
|
|
370
465
|
private readonly listeners;
|
|
466
|
+
/** The host's outbound stack, when this host can open sockets at all. */
|
|
467
|
+
outbound: OutboundTcp | null;
|
|
468
|
+
/**
|
|
469
|
+
* Ports held by in-container servers that are not sockets.
|
|
470
|
+
*
|
|
471
|
+
* A JavaScript HTTP server in the container is registered with the request
|
|
472
|
+
* router and never binds one of these sockets, so a guest connecting to its
|
|
473
|
+
* port found nothing listening. Before sockets could leave the container
|
|
474
|
+
* this did not arise -- every guest client went out through the egress --
|
|
475
|
+
* and afterwards `urllib` dialling a container server got ECONNREFUSED.
|
|
476
|
+
*/
|
|
477
|
+
loopbackHttp: ((port: number, connection: VirtualTcpConnection) => boolean) | null;
|
|
371
478
|
constructor(occupied?: ((port: number) => boolean) | undefined);
|
|
372
479
|
listen(port: number, address?: string, backlog?: number): VirtualTcpListener;
|
|
373
480
|
close(listener: VirtualTcpListener): void;
|
|
374
481
|
connect(port: number, localPort?: number, localAddress?: string): VirtualTcpConnection;
|
|
482
|
+
/** A connection to a port an in-container server holds without a socket. */
|
|
483
|
+
private connectToNonSocketServer;
|
|
375
484
|
hasListener(port: number): boolean;
|
|
376
485
|
ports(): number[];
|
|
377
486
|
closeAll(): void;
|
|
378
487
|
}
|
|
379
488
|
|
|
380
|
-
/**
|
|
381
|
-
* The outbound network policy, as plain functions every client consults.
|
|
382
|
-
*
|
|
383
|
-
* The container has several ways out — `curl` and `wget` in the shell, `http`,
|
|
384
|
-
* `https`, `fetch` and `WebSocket` in a Node program, sockets in Python — and
|
|
385
|
-
* they used to decide separately. Only the shell asked: a Node program's
|
|
386
|
-
* `fetch("https://…")` reached the internet from a container booted with
|
|
387
|
-
* outbound access off. One policy, applied at each exit, is what makes
|
|
388
|
-
* `network: { allowOutbound: false }` mean what it says.
|
|
389
|
-
*
|
|
390
|
-
* Loopback is not "outbound" at all. `127.0.0.1` inside the container is the
|
|
391
|
-
* container, so those requests are routed to its own servers and never handed
|
|
392
|
-
* to the host's network stack, whatever the policy allows.
|
|
393
|
-
*/
|
|
394
|
-
interface OutboundPolicy {
|
|
395
|
-
/** Whether requests may leave the container at all. */
|
|
396
|
-
allowOutbound: boolean;
|
|
397
|
-
/** When outbound is on, the hosts it may reach (subdomains included). `null` means any. */
|
|
398
|
-
allowedHosts: string[] | null;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
489
|
/**
|
|
402
490
|
* Clean-room contracts between SandboxedJS and its JavaScript runtime.
|
|
403
491
|
*
|