@agen-ai/sandbox-docker 0.0.0-stage → 0.4.4
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 +21 -0
- package/README.md +131 -2
- package/dist/datasets/configuration.d.ts +10 -0
- package/dist/datasets/configuration.js +20 -0
- package/dist/datasets/mounts.d.ts +21 -0
- package/dist/datasets/mounts.js +50 -0
- package/dist/datasets/types.d.ts +6 -0
- package/dist/datasets/types.js +0 -0
- package/dist/datasets/warnings.d.ts +2 -0
- package/dist/datasets/warnings.js +7 -0
- package/dist/engine/client.d.ts +14 -0
- package/dist/engine/client.js +164 -0
- package/dist/engine/contracts.d.ts +40 -0
- package/dist/engine/contracts.js +22 -0
- package/dist/engine/response.d.ts +18 -0
- package/dist/engine/response.js +109 -0
- package/dist/engine/stream.d.ts +3 -0
- package/dist/engine/stream.js +134 -0
- package/dist/engine/upgrade.d.ts +9 -0
- package/dist/engine/upgrade.js +164 -0
- package/dist/engine/upload.d.ts +4 -0
- package/dist/engine/upload.js +48 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +30 -0
- package/dist/policy/config.d.ts +22 -0
- package/dist/policy/config.js +63 -0
- package/dist/policy/container.d.ts +70 -0
- package/dist/policy/container.js +81 -0
- package/dist/policy/preflight.d.ts +9 -0
- package/dist/policy/preflight.js +89 -0
- package/dist/policy/profiles.d.ts +32 -0
- package/dist/policy/profiles.js +44 -0
- package/dist/process/attachment.d.ts +19 -0
- package/dist/process/attachment.js +109 -0
- package/dist/process/framing.d.ts +10 -0
- package/dist/process/framing.js +43 -0
- package/dist/process/input.d.ts +16 -0
- package/dist/process/input.js +62 -0
- package/dist/process/output.d.ts +13 -0
- package/dist/process/output.js +41 -0
- package/dist/process/owner.d.ts +30 -0
- package/dist/process/owner.js +129 -0
- package/dist/recovery/identity.d.ts +11 -0
- package/dist/recovery/identity.js +78 -0
- package/dist/recovery/reconcile.d.ts +5 -0
- package/dist/recovery/reconcile.js +44 -0
- package/dist/resources/api.d.ts +23 -0
- package/dist/resources/api.js +70 -0
- package/dist/resources/fingerprint.d.ts +3 -0
- package/dist/resources/fingerprint.js +94 -0
- package/dist/resources/inspect.d.ts +49 -0
- package/dist/resources/inspect.js +103 -0
- package/dist/resources/owner.d.ts +46 -0
- package/dist/resources/owner.js +313 -0
- package/dist/workspace/exports.d.ts +5 -0
- package/dist/workspace/exports.js +52 -0
- package/dist/workspace/imports.d.ts +8 -0
- package/dist/workspace/imports.js +49 -0
- package/dist/workspace/paths.d.ts +8 -0
- package/dist/workspace/paths.js +39 -0
- package/dist/workspace/selected.d.ts +5 -0
- package/dist/workspace/selected.js +34 -0
- package/dist/workspace/stat.d.ts +18 -0
- package/dist/workspace/stat.js +63 -0
- package/dist/workspace/tar/contracts.d.ts +28 -0
- package/dist/workspace/tar/contracts.js +0 -0
- package/dist/workspace/tar/decode.d.ts +3 -0
- package/dist/workspace/tar/decode.js +62 -0
- package/dist/workspace/tar/encode.d.ts +3 -0
- package/dist/workspace/tar/encode.js +103 -0
- package/dist/workspace/tar/entry.d.ts +6 -0
- package/dist/workspace/tar/entry.js +80 -0
- package/dist/workspace/tar/header.d.ts +11 -0
- package/dist/workspace/tar/header.js +84 -0
- package/dist/workspace/tar/pax.d.ts +3 -0
- package/dist/workspace/tar/pax.js +40 -0
- package/dist/workspace/tar/reader.d.ts +16 -0
- package/dist/workspace/tar/reader.js +69 -0
- package/package.json +59 -3
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { archiveName, unsafeArchive } from "../paths.js";
|
|
2
|
+
import { consumeTarEntry } from "./entry.js";
|
|
3
|
+
import { BLOCK_BYTES, MAX_PAX_BYTES, MAX_TRAILER_BYTES, parseHeader } from "./header.js";
|
|
4
|
+
import { parsePax } from "./pax.js";
|
|
5
|
+
import { TarReader } from "./reader.js";
|
|
6
|
+
async function readTar(source, consume, options) {
|
|
7
|
+
const reader = new TarReader(source, options);
|
|
8
|
+
let files = 0n;
|
|
9
|
+
let directories = 0n;
|
|
10
|
+
let payloadBytes = 0n;
|
|
11
|
+
let pendingPax;
|
|
12
|
+
let primary;
|
|
13
|
+
let result;
|
|
14
|
+
try {
|
|
15
|
+
for (; ; ) {
|
|
16
|
+
const block = await reader.required(BLOCK_BYTES);
|
|
17
|
+
if (block.every((byte) => byte === 0)) {
|
|
18
|
+
if (pendingPax) unsafeArchive("PAX metadata lacks its following entry.");
|
|
19
|
+
await reader.zeroes(BLOCK_BYTES);
|
|
20
|
+
let trailerBytes = BLOCK_BYTES * 2;
|
|
21
|
+
for (; ; ) {
|
|
22
|
+
const trailing = await reader.exact(BLOCK_BYTES, true);
|
|
23
|
+
if (trailing === void 0) break;
|
|
24
|
+
if (trailing.some((byte) => byte !== 0)) unsafeArchive("nonzero padding or trailer.");
|
|
25
|
+
trailerBytes += BLOCK_BYTES;
|
|
26
|
+
if (trailerBytes > MAX_TRAILER_BYTES) unsafeArchive("trailing padding exceeds the framing bound.");
|
|
27
|
+
}
|
|
28
|
+
result = Object.freeze({ files, directories, payloadBytes, archiveBytes: reader.bytes });
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const header = parseHeader(block);
|
|
32
|
+
if (header.kind === "pax") {
|
|
33
|
+
if (pendingPax || header.size === 0 || header.size > MAX_PAX_BYTES) unsafeArchive("invalid bounded local PAX sequence.");
|
|
34
|
+
pendingPax = { path: parsePax(await reader.required(header.size)) };
|
|
35
|
+
await reader.zeroes((BLOCK_BYTES - header.size % BLOCK_BYTES) % BLOCK_BYTES);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const path = archiveName(pendingPax?.path ?? header.path, header.kind === "directory");
|
|
39
|
+
pendingPax = void 0;
|
|
40
|
+
await consumeTarEntry(reader, { ...header, kind: header.kind }, path, consume, options.chunkBytes);
|
|
41
|
+
payloadBytes += BigInt(header.size);
|
|
42
|
+
await reader.zeroes((BLOCK_BYTES - header.size % BLOCK_BYTES) % BLOCK_BYTES);
|
|
43
|
+
if (header.kind === "file") files += 1n;
|
|
44
|
+
else directories += 1n;
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
primary = { error };
|
|
48
|
+
} finally {
|
|
49
|
+
try {
|
|
50
|
+
await reader.close();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (primary) throw new AggregateError([primary.error, error], "Archive validation and source cleanup both failed.");
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (primary) throw primary.error;
|
|
57
|
+
if (!result) unsafeArchive("archive framing did not settle.");
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
readTar
|
|
62
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { SandboxOperationError, SandboxUnsupportedError } from "@agen-ai/sandbox";
|
|
2
|
+
import { archiveName } from "../paths.js";
|
|
3
|
+
import { BLOCK_BYTES, MAX_OCTAL_SIZE, MAX_PAX_BYTES, makeHeader } from "./header.js";
|
|
4
|
+
import { encodePaxPath } from "./pax.js";
|
|
5
|
+
async function* payload(entry, options) {
|
|
6
|
+
let written = 0;
|
|
7
|
+
if (entry.body !== void 0) {
|
|
8
|
+
const source = entry.body;
|
|
9
|
+
const iterator = Symbol.asyncIterator in source ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
|
|
10
|
+
let done = false;
|
|
11
|
+
let primary;
|
|
12
|
+
try {
|
|
13
|
+
for (; ; ) {
|
|
14
|
+
options.signal.throwIfAborted();
|
|
15
|
+
const item = await iterator.next();
|
|
16
|
+
options.signal.throwIfAborted();
|
|
17
|
+
if (item.done) {
|
|
18
|
+
done = true;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
const chunk = item.value;
|
|
22
|
+
if (!(chunk instanceof Uint8Array) || chunk.length > options.chunkBytes) throw new SandboxOperationError("input_limit", "Input file chunks exceed the selected byte buffer.");
|
|
23
|
+
if (chunk.length > entry.size - written) throw new SandboxOperationError("input_limit", "Input file body exceeds its declared size.");
|
|
24
|
+
written += chunk.length;
|
|
25
|
+
yield chunk;
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
primary = { error };
|
|
29
|
+
} finally {
|
|
30
|
+
if (!done) {
|
|
31
|
+
try {
|
|
32
|
+
await iterator.return?.();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (primary) throw new AggregateError([primary.error, error], "Input file and producer cleanup both failed.");
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (primary) throw primary.error;
|
|
40
|
+
}
|
|
41
|
+
if (written !== entry.size) throw new SandboxOperationError("input_limit", "Input file body ended before its declared size.");
|
|
42
|
+
if (written % BLOCK_BYTES) yield new Uint8Array(BLOCK_BYTES - written % BLOCK_BYTES);
|
|
43
|
+
}
|
|
44
|
+
async function* blocks(entries, options) {
|
|
45
|
+
for await (const entry of entries) {
|
|
46
|
+
options.signal.throwIfAborted();
|
|
47
|
+
archiveName(entry.path, entry.kind === "directory");
|
|
48
|
+
if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > MAX_OCTAL_SIZE) {
|
|
49
|
+
throw new SandboxUnsupportedError("Docker tar entries require exact unsigned-octal sizes no greater than 8 GiB minus one byte.");
|
|
50
|
+
}
|
|
51
|
+
if (entry.kind === "directory" && entry.size !== 0) throw new SandboxUnsupportedError("Directory tar entries require zero bytes.");
|
|
52
|
+
const name = entry.kind === "directory" ? `${entry.path}/` : entry.path;
|
|
53
|
+
let headerName = name;
|
|
54
|
+
if (Buffer.byteLength(name) > 100) {
|
|
55
|
+
const pax = encodePaxPath(name);
|
|
56
|
+
if (pax.length > MAX_PAX_BYTES) throw new SandboxUnsupportedError("Docker path metadata exceeds the selected bounded PAX representation.");
|
|
57
|
+
yield makeHeader({ ...entry, path: "PaxHeader", kind: "pax", size: pax.length });
|
|
58
|
+
for (let offset = 0; offset < pax.length; offset += options.chunkBytes) yield pax.subarray(offset, offset + options.chunkBytes);
|
|
59
|
+
if (pax.length % BLOCK_BYTES) yield new Uint8Array(BLOCK_BYTES - pax.length % BLOCK_BYTES);
|
|
60
|
+
headerName = entry.kind === "directory" ? "entry/" : "entry";
|
|
61
|
+
}
|
|
62
|
+
yield makeHeader({ ...entry, path: headerName });
|
|
63
|
+
yield* payload(entry, options);
|
|
64
|
+
}
|
|
65
|
+
options.signal.throwIfAborted();
|
|
66
|
+
yield new Uint8Array(BLOCK_BYTES);
|
|
67
|
+
yield new Uint8Array(BLOCK_BYTES);
|
|
68
|
+
}
|
|
69
|
+
async function* writeTar(entries, options) {
|
|
70
|
+
let bytes = 0n;
|
|
71
|
+
const iterator = blocks(entries, options);
|
|
72
|
+
let done = false;
|
|
73
|
+
let primary;
|
|
74
|
+
try {
|
|
75
|
+
for (; ; ) {
|
|
76
|
+
const item = await iterator.next();
|
|
77
|
+
if (item.done) {
|
|
78
|
+
done = true;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
bytes += BigInt(item.value.length);
|
|
82
|
+
if (options.maxArchiveBytes !== void 0 && bytes > options.maxArchiveBytes) {
|
|
83
|
+
throw new SandboxOperationError(options.limitCode, "Workspace archive exceeds the caller-selected wire byte quota.");
|
|
84
|
+
}
|
|
85
|
+
yield item.value;
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
primary = { error };
|
|
89
|
+
} finally {
|
|
90
|
+
if (!done) {
|
|
91
|
+
try {
|
|
92
|
+
await iterator.return(void 0);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (primary) throw new AggregateError([primary.error, error], "Archive transfer and producer cleanup both failed.");
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (primary) throw primary.error;
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
writeTar
|
|
103
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TarHeader, TarOutputEntry } from './contracts.js';
|
|
2
|
+
import type { TarReader } from './reader.js';
|
|
3
|
+
export declare function consumeTarEntry(reader: TarReader, header: TarHeader & {
|
|
4
|
+
readonly kind: 'file' | 'directory';
|
|
5
|
+
}, path: string, consume: (entry: TarOutputEntry) => Promise<void>, chunkBytes: number): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=entry.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { archiveError, unsafeArchive } from "../paths.js";
|
|
2
|
+
async function consumeTarEntry(reader, header, path, consume, chunkBytes) {
|
|
3
|
+
let remaining = header.size;
|
|
4
|
+
let active = true;
|
|
5
|
+
let entered = false;
|
|
6
|
+
let pending;
|
|
7
|
+
let failure;
|
|
8
|
+
const reject = (message) => {
|
|
9
|
+
const error = archiveError(message);
|
|
10
|
+
failure ??= { error, origin: "ownership" };
|
|
11
|
+
const result = Promise.reject(error);
|
|
12
|
+
void result.catch(() => {
|
|
13
|
+
});
|
|
14
|
+
return result;
|
|
15
|
+
};
|
|
16
|
+
const iterator = {
|
|
17
|
+
next() {
|
|
18
|
+
if (!active) return reject("entry body is retired outside its callback.");
|
|
19
|
+
if (pending) return reject("concurrent entry body pulls are forbidden.");
|
|
20
|
+
const actual = Promise.resolve().then(async () => {
|
|
21
|
+
try {
|
|
22
|
+
if (remaining === 0) return { done: true, value: void 0 };
|
|
23
|
+
let chunk;
|
|
24
|
+
try {
|
|
25
|
+
chunk = await reader.take(Math.min(remaining, chunkBytes));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
failure ??= { error, origin: "source" };
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
if (!active) unsafeArchive("entry callback returned with an active body pull.");
|
|
31
|
+
if (!chunk) unsafeArchive("unexpected EOF inside payload.");
|
|
32
|
+
remaining -= chunk.length;
|
|
33
|
+
return { done: false, value: chunk };
|
|
34
|
+
} catch (error) {
|
|
35
|
+
failure ??= { error, origin: "ownership" };
|
|
36
|
+
throw error;
|
|
37
|
+
} finally {
|
|
38
|
+
pending = void 0;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
pending = actual;
|
|
42
|
+
void actual.catch(() => {
|
|
43
|
+
});
|
|
44
|
+
return actual;
|
|
45
|
+
},
|
|
46
|
+
async return() {
|
|
47
|
+
active = false;
|
|
48
|
+
await pending?.catch(() => {
|
|
49
|
+
});
|
|
50
|
+
return { done: true, value: void 0 };
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const body = { [Symbol.asyncIterator]() {
|
|
54
|
+
if (!active || entered) unsafeArchive("entry body is single-use and callback-scoped.");
|
|
55
|
+
entered = true;
|
|
56
|
+
return iterator;
|
|
57
|
+
} };
|
|
58
|
+
let callbackFailure;
|
|
59
|
+
try {
|
|
60
|
+
await consume(Object.freeze({ path, kind: header.kind, size: header.size, mode: header.mode, body }));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
callbackFailure = { error };
|
|
63
|
+
}
|
|
64
|
+
const enteredPull = pending;
|
|
65
|
+
active = false;
|
|
66
|
+
await enteredPull?.catch(() => {
|
|
67
|
+
});
|
|
68
|
+
if (callbackFailure) {
|
|
69
|
+
if (failure?.origin === "source" && failure.error !== callbackFailure.error) {
|
|
70
|
+
throw new AggregateError([callbackFailure.error, failure.error], "Archive callback and entered source pull both failed.");
|
|
71
|
+
}
|
|
72
|
+
throw callbackFailure.error;
|
|
73
|
+
}
|
|
74
|
+
if (failure) throw failure.error;
|
|
75
|
+
if (enteredPull) unsafeArchive("entry callback returned with an active body pull.");
|
|
76
|
+
if (remaining !== 0) unsafeArchive("entry callback must drain its complete body.");
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
consumeTarEntry
|
|
80
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TarHeader, TarInputEntry } from './contracts.js';
|
|
2
|
+
export declare const BLOCK_BYTES = 512;
|
|
3
|
+
export declare const MAX_OCTAL_SIZE: number;
|
|
4
|
+
export declare const MAX_PAX_BYTES = 4096;
|
|
5
|
+
export declare const MAX_TRAILER_BYTES: number;
|
|
6
|
+
export declare function text(bytes: Uint8Array): string;
|
|
7
|
+
export declare function parseHeader(block: Uint8Array): TarHeader;
|
|
8
|
+
export declare function makeHeader(entry: Omit<TarInputEntry, 'body' | 'kind'> & {
|
|
9
|
+
readonly kind: TarHeader['kind'];
|
|
10
|
+
}): Uint8Array;
|
|
11
|
+
//# sourceMappingURL=header.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { SandboxUnsupportedError } from "@agen-ai/sandbox";
|
|
2
|
+
import { archiveName, unsafeArchive } from "../paths.js";
|
|
3
|
+
const BLOCK_BYTES = 512;
|
|
4
|
+
const MAX_OCTAL_SIZE = 8 * 1024 ** 3 - 1;
|
|
5
|
+
const MAX_PAX_BYTES = 4096;
|
|
6
|
+
const MAX_TRAILER_BYTES = 10 * 1024;
|
|
7
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
8
|
+
function text(bytes) {
|
|
9
|
+
try {
|
|
10
|
+
return utf8.decode(bytes);
|
|
11
|
+
} catch {
|
|
12
|
+
return unsafeArchive("header text must be valid UTF-8.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function field(block, offset, length) {
|
|
16
|
+
const bytes = block.subarray(offset, offset + length);
|
|
17
|
+
const terminator = bytes.indexOf(0);
|
|
18
|
+
if (terminator < 0) return text(bytes);
|
|
19
|
+
if (bytes.subarray(terminator).some((byte) => byte !== 0)) unsafeArchive("hidden bytes after a text terminator.");
|
|
20
|
+
return text(bytes.subarray(0, terminator));
|
|
21
|
+
}
|
|
22
|
+
function octal(block, offset, length) {
|
|
23
|
+
const bytes = block.subarray(offset, offset + length);
|
|
24
|
+
if (bytes.some((byte) => byte !== 0 && byte !== 32 && (byte < 48 || byte > 55))) unsafeArchive("numeric headers require unsigned octal.");
|
|
25
|
+
const encoded = text(bytes);
|
|
26
|
+
if (!/^[ \0]*[0-7]*[ \0]*$/u.test(encoded)) unsafeArchive("numeric headers require unsigned octal.");
|
|
27
|
+
const digits = encoded.replace(/^[ \0]+|[ \0]+$/gu, "");
|
|
28
|
+
const value = digits ? Number.parseInt(digits, 8) : 0;
|
|
29
|
+
if (!Number.isSafeInteger(value)) unsafeArchive("numeric header exceeds the safe integer range.");
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function parseHeader(block) {
|
|
33
|
+
const checksum = octal(block, 148, 8);
|
|
34
|
+
let sum = 0;
|
|
35
|
+
for (let index = 0; index < BLOCK_BYTES; index += 1) sum += index >= 148 && index < 156 ? 32 : block[index];
|
|
36
|
+
if (sum !== checksum) unsafeArchive("checksum mismatch.");
|
|
37
|
+
if (field(block, 257, 6) !== "ustar" || text(block.subarray(263, 265)) !== "00") unsafeArchive("only POSIX ustar/PAX is supported.");
|
|
38
|
+
const flag = block[156];
|
|
39
|
+
const kind = flag === 53 ? "directory" : flag === 48 || flag === 0 ? "file" : flag === 120 ? "pax" : void 0;
|
|
40
|
+
if (!kind) unsafeArchive("only regular files, directories and local PAX records are supported; links and special files are forbidden.");
|
|
41
|
+
if (field(block, 157, 100)) unsafeArchive("links and link metadata are forbidden.");
|
|
42
|
+
const prefix = field(block, 345, 155);
|
|
43
|
+
const name = field(block, 0, 100);
|
|
44
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
45
|
+
archiveName(path, kind !== "file");
|
|
46
|
+
const mode = octal(block, 100, 8);
|
|
47
|
+
const size = octal(block, 124, 12);
|
|
48
|
+
for (const [offset, length] of [[108, 8], [116, 8], [136, 12], [329, 8], [337, 8]]) octal(block, offset, length);
|
|
49
|
+
field(block, 265, 32);
|
|
50
|
+
field(block, 297, 32);
|
|
51
|
+
if (mode > 4095 || size > MAX_OCTAL_SIZE || block.subarray(500).some((byte) => byte !== 0)) unsafeArchive("unsupported numeric or extension metadata.");
|
|
52
|
+
if (kind === "directory" && size !== 0) unsafeArchive("directory entries cannot carry payload.");
|
|
53
|
+
return { path, kind, mode, size };
|
|
54
|
+
}
|
|
55
|
+
function putOctal(header, offset, length, value) {
|
|
56
|
+
if (!Number.isSafeInteger(value) || value < 0 || value.toString(8).length >= length) {
|
|
57
|
+
throw new SandboxUnsupportedError("Docker tar metadata exceeds the selected unsigned-octal representation.");
|
|
58
|
+
}
|
|
59
|
+
header.write(value.toString(8).padStart(length - 1, "0"), offset, length - 1, "ascii");
|
|
60
|
+
}
|
|
61
|
+
function makeHeader(entry) {
|
|
62
|
+
if (Buffer.byteLength(entry.path) > 100 || entry.mode > 4095) throw new SandboxUnsupportedError("Docker tar header name or mode is not representable.");
|
|
63
|
+
const header = Buffer.alloc(BLOCK_BYTES);
|
|
64
|
+
header.write(entry.path, 0, 100, "utf8");
|
|
65
|
+
for (const [offset, length, value] of [[100, 8, entry.mode], [108, 8, entry.uid], [116, 8, entry.gid], [124, 12, entry.size], [136, 12, 0], [329, 8, 0], [337, 8, 0]]) {
|
|
66
|
+
putOctal(header, offset, length, value);
|
|
67
|
+
}
|
|
68
|
+
header[156] = entry.kind === "directory" ? 53 : entry.kind === "pax" ? 120 : 48;
|
|
69
|
+
header.write("ustar\0", 257, 6, "ascii");
|
|
70
|
+
header.write("00", 263, 2, "ascii");
|
|
71
|
+
header.fill(32, 148, 156);
|
|
72
|
+
header.write(header.reduce((total, byte) => total + byte, 0).toString(8).padStart(6, "0"), 148, 6, "ascii");
|
|
73
|
+
header[154] = 0;
|
|
74
|
+
return header;
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
BLOCK_BYTES,
|
|
78
|
+
MAX_OCTAL_SIZE,
|
|
79
|
+
MAX_PAX_BYTES,
|
|
80
|
+
MAX_TRAILER_BYTES,
|
|
81
|
+
makeHeader,
|
|
82
|
+
parseHeader,
|
|
83
|
+
text
|
|
84
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { archiveName, unsafeArchive } from "../paths.js";
|
|
2
|
+
import { text } from "./header.js";
|
|
3
|
+
function parsePax(bytes) {
|
|
4
|
+
const keys = /* @__PURE__ */ new Set();
|
|
5
|
+
let offset = 0;
|
|
6
|
+
let path;
|
|
7
|
+
while (offset < bytes.length) {
|
|
8
|
+
const space = bytes.indexOf(32, offset);
|
|
9
|
+
if (space < 0 || space - offset > 5) unsafeArchive("PAX requires bounded decimal lengths.");
|
|
10
|
+
const digits = text(bytes.subarray(offset, space));
|
|
11
|
+
if (!/^[1-9][0-9]*$/u.test(digits)) unsafeArchive("PAX length must be canonical decimal.");
|
|
12
|
+
const end = offset + Number(digits);
|
|
13
|
+
if (end > bytes.length || end <= space + 2 || bytes[end - 1] !== 10) unsafeArchive("PAX length or newline mismatch.");
|
|
14
|
+
const record = text(bytes.subarray(space + 1, end - 1));
|
|
15
|
+
const equals = record.indexOf("=");
|
|
16
|
+
const key = record.slice(0, equals);
|
|
17
|
+
const value = record.slice(equals + 1);
|
|
18
|
+
if (equals <= 0 || keys.has(key)) unsafeArchive("PAX keys must be unique.");
|
|
19
|
+
keys.add(key);
|
|
20
|
+
if (key === "path") {
|
|
21
|
+
archiveName(value, true);
|
|
22
|
+
path = value;
|
|
23
|
+
} else if (!["mtime", "atime", "ctime"].includes(key) || !/^-?[0-9]{1,16}(?:\.[0-9]{1,9})?$/u.test(value)) {
|
|
24
|
+
unsafeArchive("only path and inert timestamps are supported in PAX records.");
|
|
25
|
+
}
|
|
26
|
+
offset = end;
|
|
27
|
+
}
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
function encodePaxPath(path) {
|
|
31
|
+
const payload = ` path=${path}
|
|
32
|
+
`;
|
|
33
|
+
let size = Buffer.byteLength(payload) + 1;
|
|
34
|
+
while (String(size).length + Buffer.byteLength(payload) !== size) size = String(size).length + Buffer.byteLength(payload);
|
|
35
|
+
return Buffer.from(`${size}${payload}`);
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
encodePaxPath,
|
|
39
|
+
parsePax
|
|
40
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TarOptions } from './contracts.js';
|
|
2
|
+
export declare class TarReader {
|
|
3
|
+
private readonly options;
|
|
4
|
+
private readonly iterator;
|
|
5
|
+
private current;
|
|
6
|
+
private ended;
|
|
7
|
+
private received;
|
|
8
|
+
bytes: bigint;
|
|
9
|
+
constructor(source: AsyncIterable<Uint8Array>, options: TarOptions);
|
|
10
|
+
take(maximum: number): Promise<Uint8Array | undefined>;
|
|
11
|
+
exact(size: number, eofAllowed?: boolean): Promise<Uint8Array | undefined>;
|
|
12
|
+
required(size: number): Promise<Uint8Array>;
|
|
13
|
+
zeroes(size: number): Promise<void>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=reader.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { SandboxOperationError } from "@agen-ai/sandbox";
|
|
2
|
+
import { unsafeArchive } from "../paths.js";
|
|
3
|
+
class TarReader {
|
|
4
|
+
constructor(source, options) {
|
|
5
|
+
this.options = options;
|
|
6
|
+
this.iterator = source[Symbol.asyncIterator]();
|
|
7
|
+
}
|
|
8
|
+
iterator;
|
|
9
|
+
current = new Uint8Array();
|
|
10
|
+
ended = false;
|
|
11
|
+
received = 0n;
|
|
12
|
+
bytes = 0n;
|
|
13
|
+
async take(maximum) {
|
|
14
|
+
this.options.signal.throwIfAborted();
|
|
15
|
+
while (!this.current.length && !this.ended) {
|
|
16
|
+
const item = await this.iterator.next();
|
|
17
|
+
this.options.signal.throwIfAborted();
|
|
18
|
+
if (item.done) this.ended = true;
|
|
19
|
+
else {
|
|
20
|
+
if (!(item.value instanceof Uint8Array) || item.value.length > this.options.chunkBytes) unsafeArchive("source chunks exceed the selected byte buffer.");
|
|
21
|
+
this.received += BigInt(item.value.length);
|
|
22
|
+
if (this.options.maxArchiveBytes !== void 0 && this.received > this.options.maxArchiveBytes) {
|
|
23
|
+
throw new SandboxOperationError(this.options.limitCode, "Workspace archive exceeds the caller-selected wire byte quota.");
|
|
24
|
+
}
|
|
25
|
+
this.current = item.value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (this.ended) return void 0;
|
|
29
|
+
const chunk = this.current.subarray(0, maximum);
|
|
30
|
+
this.current = this.current.subarray(chunk.length);
|
|
31
|
+
this.bytes += BigInt(chunk.length);
|
|
32
|
+
return chunk;
|
|
33
|
+
}
|
|
34
|
+
async exact(size, eofAllowed = false) {
|
|
35
|
+
const result = new Uint8Array(size);
|
|
36
|
+
let position = 0;
|
|
37
|
+
while (position < size) {
|
|
38
|
+
const chunk = await this.take(size - position);
|
|
39
|
+
if (chunk === void 0) {
|
|
40
|
+
if (eofAllowed && position === 0) return void 0;
|
|
41
|
+
unsafeArchive("unexpected EOF inside framing or payload.");
|
|
42
|
+
}
|
|
43
|
+
result.set(chunk, position);
|
|
44
|
+
position += chunk.length;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
async required(size) {
|
|
49
|
+
const bytes = await this.exact(size);
|
|
50
|
+
if (!bytes) unsafeArchive("unexpected EOF inside framing.");
|
|
51
|
+
return bytes;
|
|
52
|
+
}
|
|
53
|
+
async zeroes(size) {
|
|
54
|
+
let remaining = size;
|
|
55
|
+
while (remaining > 0) {
|
|
56
|
+
const chunk = await this.take(remaining);
|
|
57
|
+
if (!chunk) unsafeArchive("unexpected EOF inside padding.");
|
|
58
|
+
if (chunk.some((byte) => byte !== 0)) unsafeArchive("nonzero padding or trailer.");
|
|
59
|
+
remaining -= chunk.length;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async close() {
|
|
63
|
+
this.current = new Uint8Array();
|
|
64
|
+
if (!this.ended) await this.iterator.return?.();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
TarReader
|
|
69
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agen-ai/sandbox-docker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.4.4",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Explicit local Docker Engine resources, policy enforcement and recovery for AgenAI sandboxes.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/**/*.js",
|
|
14
|
+
"dist/**/*.d.ts",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"zod": "4.4.3",
|
|
27
|
+
"@agen-ai/sandbox": "^0.4.4",
|
|
28
|
+
"@agen-ai/validation": "^0.4.4"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"tar-stream": "3.2.2",
|
|
32
|
+
"tsup": "^8.5.0",
|
|
33
|
+
"@agen-ai/agent-protocol": "^0.4.4",
|
|
34
|
+
"@agen-ai/agent-runtime": "^0.4.4",
|
|
35
|
+
"@agen-ai/provider-acp": "^0.4.4"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.0.0"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/trevor-nichols/agenai-agent-sdk.git",
|
|
43
|
+
"directory": "packages/sandbox-docker"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/trevor-nichols/agenai-agent-sdk#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/trevor-nichols/agenai-agent-sdk/issues"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"provenance": true
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "pnpm run clean && pnpm run build:runtime && pnpm run build:types",
|
|
55
|
+
"build:runtime": "tsup",
|
|
56
|
+
"build:types": "tsc --project tsconfig.json --emitDeclarationOnly --outDir dist",
|
|
57
|
+
"typecheck": "tsc --noEmit && tsc --project tsconfig.test.json --noEmit",
|
|
58
|
+
"test": "node --import tsx --test tests/*.test.ts",
|
|
59
|
+
"test:integration": "node --import tsx --test tests/integration/*.test.ts",
|
|
60
|
+
"clean": "rimraf dist *.tsbuildinfo"
|
|
61
|
+
}
|
|
6
62
|
}
|