@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,81 @@
|
|
|
1
|
+
import { SandboxUnsupportedError } from "@agen-ai/sandbox";
|
|
2
|
+
import { dockerProfile } from "./profiles.js";
|
|
3
|
+
import { resourceLabels } from "../recovery/identity.js";
|
|
4
|
+
import { containerMounts, verifyContainerMounts } from "../datasets/mounts.js";
|
|
5
|
+
const KEEPER = ["-c", "while :; do sleep 3600; done"];
|
|
6
|
+
const DOCKER_ENVIRONMENT = Object.freeze(["HOME=/home/sandbox", "TMPDIR=/tmp", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]);
|
|
7
|
+
function tmpfs(policy, config) {
|
|
8
|
+
const options = (size) => `rw,nosuid,nodev,noexec,size=${size},mode=0700,uid=${config.user.uid},gid=${config.user.gid}`;
|
|
9
|
+
return { "/home/sandbox": options(policy.temporaryStorage.homeBytes), "/tmp": options(policy.temporaryStorage.tempBytes) };
|
|
10
|
+
}
|
|
11
|
+
function containerBody(config, record, policy, role, storageId, networkId) {
|
|
12
|
+
return {
|
|
13
|
+
Image: config.imageDigest,
|
|
14
|
+
User: `${config.user.uid}:${config.user.gid}`,
|
|
15
|
+
WorkingDir: "/workspace/work",
|
|
16
|
+
Entrypoint: ["/bin/sh"],
|
|
17
|
+
Cmd: KEEPER,
|
|
18
|
+
Env: DOCKER_ENVIRONMENT,
|
|
19
|
+
Tty: false,
|
|
20
|
+
OpenStdin: false,
|
|
21
|
+
AttachStdin: false,
|
|
22
|
+
AttachStdout: false,
|
|
23
|
+
AttachStderr: false,
|
|
24
|
+
NetworkDisabled: role === "export",
|
|
25
|
+
Healthcheck: { Test: ["NONE"] },
|
|
26
|
+
Labels: resourceLabels(record, role),
|
|
27
|
+
HostConfig: {
|
|
28
|
+
ReadonlyRootfs: true,
|
|
29
|
+
CapDrop: ["ALL"],
|
|
30
|
+
CapAdd: [],
|
|
31
|
+
SecurityOpt: ["no-new-privileges:true"],
|
|
32
|
+
Privileged: false,
|
|
33
|
+
PublishAllPorts: false,
|
|
34
|
+
PortBindings: {},
|
|
35
|
+
Devices: [],
|
|
36
|
+
DeviceRequests: [],
|
|
37
|
+
Binds: [],
|
|
38
|
+
PidMode: "",
|
|
39
|
+
IpcMode: "private",
|
|
40
|
+
CgroupnsMode: "private",
|
|
41
|
+
UsernsMode: "",
|
|
42
|
+
NetworkMode: role === "export" || policy.network.kind === "isolated" ? "none" : networkId,
|
|
43
|
+
NanoCpus: policy.resources.cpus * 1e9,
|
|
44
|
+
Memory: policy.resources.memoryBytes,
|
|
45
|
+
MemorySwap: policy.resources.memoryBytes,
|
|
46
|
+
PidsLimit: policy.resources.pids,
|
|
47
|
+
RestartPolicy: { Name: "no", MaximumRetryCount: 0 },
|
|
48
|
+
AutoRemove: false,
|
|
49
|
+
Tmpfs: tmpfs(policy, config),
|
|
50
|
+
Mounts: containerMounts(config.datasets, role, storageId)
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function sameStrings(actual, expected) {
|
|
55
|
+
return Array.isArray(actual) && actual.length === expected.length && actual.every((value, index) => value === expected[index]);
|
|
56
|
+
}
|
|
57
|
+
function empty(actual) {
|
|
58
|
+
return actual === null || actual === void 0 || (Array.isArray(actual) ? actual.length === 0 : typeof actual === "object" && Object.keys(actual).length === 0);
|
|
59
|
+
}
|
|
60
|
+
function verifyContainer(container, config, policy, role, storageId, networkId, started) {
|
|
61
|
+
const host = container.HostConfig;
|
|
62
|
+
const expectedNetwork = role === "export" || policy.network.kind === "isolated" ? "none" : networkId;
|
|
63
|
+
verifyContainerMounts(container, config.datasets, role, storageId);
|
|
64
|
+
const expectedTmpfs = tmpfs(policy, config);
|
|
65
|
+
const actualTmpfs = host.Tmpfs;
|
|
66
|
+
const configuration = container.Config;
|
|
67
|
+
const healthcheck = configuration.Healthcheck;
|
|
68
|
+
const restart = host.RestartPolicy;
|
|
69
|
+
const networkNames = Object.keys(container.NetworkSettings.Networks);
|
|
70
|
+
const attachment = Object.values(container.NetworkSettings.Networks)[0];
|
|
71
|
+
if (configuration.User !== `${config.user.uid}:${config.user.gid}` || configuration.WorkingDir !== "/workspace/work" || !sameStrings(configuration.Entrypoint, ["/bin/sh"]) || !sameStrings(configuration.Cmd, KEEPER) || !sameStrings(configuration.Env, DOCKER_ENVIRONMENT) || configuration.Tty || configuration.OpenStdin || !empty(configuration.Volumes) || typeof healthcheck !== "object" || healthcheck === null || !sameStrings(Reflect.get(healthcheck, "Test"), ["NONE"]) || host.ReadonlyRootfs !== true || host.Privileged !== false || !sameStrings(host.CapDrop, ["ALL"]) || !empty(host.CapAdd) || !sameStrings(host.SecurityOpt, ["no-new-privileges:true"]) || host.PublishAllPorts !== false || !empty(host.PortBindings) || !empty(host.Devices) || !empty(host.DeviceRequests) || !empty(host.Binds) || host.PidMode !== "" || host.IpcMode !== "private" || host.CgroupnsMode !== "private" || host.UsernsMode !== "" || host.NetworkMode !== expectedNetwork || host.NanoCpus !== policy.resources.cpus * 1e9 || host.Memory !== policy.resources.memoryBytes || host.MemorySwap !== policy.resources.memoryBytes || host.PidsLimit !== policy.resources.pids || host.OomKillDisable === true || typeof restart !== "object" || restart === null || Reflect.get(restart, "Name") !== "no" || Reflect.get(restart, "MaximumRetryCount") !== 0 || host.AutoRemove !== false || typeof actualTmpfs !== "object" || actualTmpfs === null || Object.keys(actualTmpfs).length !== 2 || Object.entries(expectedTmpfs).some(([path, value]) => Reflect.get(actualTmpfs, path) !== value) || started && (!container.State.Running || container.AppArmorProfile !== dockerProfile(config.profile).appArmorProfile) || started && (networkNames.length !== 1 || !empty(container.NetworkSettings.Ports) || (expectedNetwork === "none" ? networkNames[0] !== "none" : attachment.NetworkID !== networkId)) || role === "export" && (container.State.Running || container.State.Status !== "created")) {
|
|
72
|
+
throw new SandboxUnsupportedError("Docker resource readback does not match the exact requested workload isolation and resource policy.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
DOCKER_ENVIRONMENT,
|
|
77
|
+
KEEPER,
|
|
78
|
+
containerBody,
|
|
79
|
+
tmpfs,
|
|
80
|
+
verifyContainer
|
|
81
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SandboxPolicy } from '@agen-ai/sandbox';
|
|
2
|
+
import type { DockerApi } from '../resources/api.js';
|
|
3
|
+
import type { DockerSandboxConfig } from './config.js';
|
|
4
|
+
export declare function assertDockerController(config: DockerSandboxConfig): void;
|
|
5
|
+
export declare function verifyDaemon(api: DockerApi, config: DockerSandboxConfig, signal?: AbortSignal): Promise<string>;
|
|
6
|
+
export declare function verifyDatasetImages(api: DockerApi, config: DockerSandboxConfig, signal?: AbortSignal): Promise<void>;
|
|
7
|
+
export declare function verifyImage(api: DockerApi, config: DockerSandboxConfig, signal?: AbortSignal): Promise<void>;
|
|
8
|
+
export declare function assertDockerPolicy(policy: SandboxPolicy): void;
|
|
9
|
+
//# sourceMappingURL=preflight.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
import { SandboxUnsupportedError } from "@agen-ai/sandbox";
|
|
3
|
+
import { DOCKER_API } from "./config.js";
|
|
4
|
+
import { dockerProfile } from "./profiles.js";
|
|
5
|
+
const versionSchema = z.object({ Version: z.string().max(64).optional(), ApiVersion: z.string(), MinAPIVersion: z.string(), Os: z.string(), Arch: z.string() });
|
|
6
|
+
const infoSchema = z.object({
|
|
7
|
+
ID: z.string().min(1).max(256).regex(/^[^\s/\\]+$/u),
|
|
8
|
+
OSType: z.string(),
|
|
9
|
+
Architecture: z.string(),
|
|
10
|
+
CgroupVersion: z.string(),
|
|
11
|
+
CgroupDriver: z.string(),
|
|
12
|
+
SecurityOptions: z.array(z.string()).max(64),
|
|
13
|
+
OperatingSystem: z.string().optional(),
|
|
14
|
+
Driver: z.string().optional(),
|
|
15
|
+
DriverStatus: z.array(z.tuple([z.string(), z.string()])).max(64).optional(),
|
|
16
|
+
MemoryLimit: z.boolean(),
|
|
17
|
+
SwapLimit: z.boolean(),
|
|
18
|
+
PidsLimit: z.boolean(),
|
|
19
|
+
CpuCfsPeriod: z.boolean(),
|
|
20
|
+
CpuCfsQuota: z.boolean()
|
|
21
|
+
});
|
|
22
|
+
const imageSchema = z.object({
|
|
23
|
+
Id: z.string(),
|
|
24
|
+
Os: z.string(),
|
|
25
|
+
Architecture: z.string(),
|
|
26
|
+
Config: z.object({ Volumes: z.record(z.string(), z.unknown()).nullish() })
|
|
27
|
+
});
|
|
28
|
+
const datasetImageSchema = z.object({ Id: z.string(), Os: z.string(), RootFS: z.object({ Type: z.string() }) });
|
|
29
|
+
function versionNumber(value) {
|
|
30
|
+
if (!/^\d+\.\d+$/u.test(value)) return Number.NaN;
|
|
31
|
+
const [major, minor] = value.split(".").map(Number);
|
|
32
|
+
return major * 1e3 + minor;
|
|
33
|
+
}
|
|
34
|
+
function supportsDatasetMountNames(version) {
|
|
35
|
+
if (version === void 0 || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u.test(version)) return false;
|
|
36
|
+
const [major, minor, patch] = version.split(".").map(BigInt);
|
|
37
|
+
return major > 29n || major === 29n && (minor > 1n || minor === 1n && patch >= 4n);
|
|
38
|
+
}
|
|
39
|
+
function assertDockerController(config) {
|
|
40
|
+
const profile = dockerProfile(config.profile);
|
|
41
|
+
if (process.platform !== profile.controllerOS || process.arch !== profile.controllerArchitecture) {
|
|
42
|
+
throw new SandboxUnsupportedError(`Docker profile ${config.profile} requires a ${profile.controllerOS}/${profile.controllerArchitecture} controller.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function verifyDaemon(api, config, signal) {
|
|
46
|
+
assertDockerController(config);
|
|
47
|
+
const profile = dockerProfile(config.profile);
|
|
48
|
+
const version = await api.json("GET", "/version", versionSchema, { signal });
|
|
49
|
+
const info = await api.json("GET", "/info", infoSchema, { signal });
|
|
50
|
+
const security = info.SecurityOptions;
|
|
51
|
+
const imageStore = info.DriverStatus?.filter(([name]) => name === "driver-type");
|
|
52
|
+
if (version.Os !== "linux" || version.Arch !== profile.daemonArchitecture || !profile.daemonInfoArchitectures.includes(info.Architecture) || info.OSType !== "linux" || info.OperatingSystem === "Docker Desktop" !== profile.desktop || !(versionNumber(version.MinAPIVersion) <= 1052 && versionNumber(version.ApiVersion) >= 1052) || info.CgroupVersion !== "2" || info.CgroupDriver !== profile.cgroupDriver || !info.MemoryLimit || !info.SwapLimit || !info.PidsLimit || !info.CpuCfsPeriod || !info.CpuCfsQuota || security.some((item) => /rootless|userns/iu.test(item)) || security.includes("name=apparmor") === profile.desktop || !security.includes("name=seccomp,profile=builtin") || !security.includes("name=cgroupns")) {
|
|
53
|
+
throw new SandboxUnsupportedError(`Docker daemon does not match profile ${config.profile}, its architecture, cgroup-v2 controllers or required default isolation.`);
|
|
54
|
+
}
|
|
55
|
+
if (config.datasets.length > 0 && (info.Driver !== "overlayfs" || imageStore?.length !== 1 || imageStore[0][1] !== "io.containerd.snapshotter.v1")) {
|
|
56
|
+
throw new SandboxUnsupportedError("Docker image datasets require the explicitly admitted overlayfs containerd image store.");
|
|
57
|
+
}
|
|
58
|
+
if (config.datasets.length > 0 && !supportsDatasetMountNames(version.Version)) {
|
|
59
|
+
throw new SandboxUnsupportedError("Docker image datasets require a stable Engine release of at least 29.1.4 with the image-layer filename fix.");
|
|
60
|
+
}
|
|
61
|
+
return info.ID;
|
|
62
|
+
}
|
|
63
|
+
async function verifyDatasetImages(api, config, signal) {
|
|
64
|
+
for (const dataset of config.datasets) {
|
|
65
|
+
const image = await api.json("GET", `${DOCKER_API}/images/${dataset.imageDigest}/json`, datasetImageSchema, { signal });
|
|
66
|
+
if (image.Id !== dataset.imageDigest || image.Os !== "linux" || image.RootFS.Type !== "layers") {
|
|
67
|
+
throw new SandboxUnsupportedError("Docker datasets require their exact existing Linux filesystem image ID.");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function verifyImage(api, config, signal) {
|
|
72
|
+
const profile = dockerProfile(config.profile);
|
|
73
|
+
const image = await api.json("GET", `${DOCKER_API}/images/${config.imageDigest}/json`, imageSchema, { signal });
|
|
74
|
+
if (image.Id !== config.imageDigest || image.Os !== "linux" || image.Architecture !== profile.imageArchitecture || Object.keys(image.Config.Volumes ?? {}).length !== 0) {
|
|
75
|
+
throw new SandboxUnsupportedError(`Docker profile ${config.profile} requires the exact local Linux ${profile.imageArchitecture} image ID without declared image volumes.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function assertDockerPolicy(policy) {
|
|
79
|
+
if (!Number.isSafeInteger(policy.resources.cpus * 1e9) || policy.resources.cpus > 1024 || policy.resources.memoryBytes < 6 * 1024 ** 2) {
|
|
80
|
+
throw new SandboxUnsupportedError("Docker requires representable NanoCPUs and at least 6 MiB of memory.");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
assertDockerController,
|
|
85
|
+
assertDockerPolicy,
|
|
86
|
+
verifyDaemon,
|
|
87
|
+
verifyDatasetImages,
|
|
88
|
+
verifyImage
|
|
89
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const DOCKER_SANDBOX_PROFILES: readonly ["linux-x64", "desktop-apple-virtualization-native-arm64", "desktop-apple-virtualization-rosetta-amd64"];
|
|
2
|
+
export type DockerSandboxProfile = typeof DOCKER_SANDBOX_PROFILES[number];
|
|
3
|
+
export declare function dockerProfile(profile: DockerSandboxProfile): {
|
|
4
|
+
controllerOS: string;
|
|
5
|
+
controllerArchitecture: string;
|
|
6
|
+
daemonArchitecture: string;
|
|
7
|
+
daemonInfoArchitectures: string[];
|
|
8
|
+
imageArchitecture: string;
|
|
9
|
+
cgroupDriver: string;
|
|
10
|
+
appArmorProfile: string;
|
|
11
|
+
desktop: false;
|
|
12
|
+
} | {
|
|
13
|
+
controllerOS: string;
|
|
14
|
+
controllerArchitecture: string;
|
|
15
|
+
daemonArchitecture: string;
|
|
16
|
+
daemonInfoArchitectures: string[];
|
|
17
|
+
imageArchitecture: string;
|
|
18
|
+
cgroupDriver: string;
|
|
19
|
+
appArmorProfile: string;
|
|
20
|
+
desktop: true;
|
|
21
|
+
} | {
|
|
22
|
+
controllerOS: string;
|
|
23
|
+
controllerArchitecture: string;
|
|
24
|
+
daemonArchitecture: string;
|
|
25
|
+
daemonInfoArchitectures: string[];
|
|
26
|
+
imageArchitecture: string;
|
|
27
|
+
cgroupDriver: string;
|
|
28
|
+
appArmorProfile: string;
|
|
29
|
+
desktop: true;
|
|
30
|
+
};
|
|
31
|
+
export declare function dockerBackendRevision(profile: DockerSandboxProfile): string;
|
|
32
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const DOCKER_SANDBOX_PROFILES = ["linux-x64", "desktop-apple-virtualization-native-arm64", "desktop-apple-virtualization-rosetta-amd64"];
|
|
2
|
+
const profiles = {
|
|
3
|
+
"linux-x64": {
|
|
4
|
+
controllerOS: "linux",
|
|
5
|
+
controllerArchitecture: "x64",
|
|
6
|
+
daemonArchitecture: "amd64",
|
|
7
|
+
daemonInfoArchitectures: ["amd64", "x86_64"],
|
|
8
|
+
imageArchitecture: "amd64",
|
|
9
|
+
cgroupDriver: "systemd",
|
|
10
|
+
appArmorProfile: "docker-default",
|
|
11
|
+
desktop: false
|
|
12
|
+
},
|
|
13
|
+
"desktop-apple-virtualization-native-arm64": {
|
|
14
|
+
controllerOS: "darwin",
|
|
15
|
+
controllerArchitecture: "arm64",
|
|
16
|
+
daemonArchitecture: "arm64",
|
|
17
|
+
daemonInfoArchitectures: ["arm64", "aarch64"],
|
|
18
|
+
imageArchitecture: "arm64",
|
|
19
|
+
cgroupDriver: "cgroupfs",
|
|
20
|
+
appArmorProfile: "",
|
|
21
|
+
desktop: true
|
|
22
|
+
},
|
|
23
|
+
"desktop-apple-virtualization-rosetta-amd64": {
|
|
24
|
+
controllerOS: "darwin",
|
|
25
|
+
controllerArchitecture: "arm64",
|
|
26
|
+
daemonArchitecture: "arm64",
|
|
27
|
+
daemonInfoArchitectures: ["arm64", "aarch64"],
|
|
28
|
+
imageArchitecture: "amd64",
|
|
29
|
+
cgroupDriver: "cgroupfs",
|
|
30
|
+
appArmorProfile: "",
|
|
31
|
+
desktop: true
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
function dockerProfile(profile) {
|
|
35
|
+
return profiles[profile];
|
|
36
|
+
}
|
|
37
|
+
function dockerBackendRevision(profile) {
|
|
38
|
+
return `engine-1.52-v4-${profile}`;
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
DOCKER_SANDBOX_PROFILES,
|
|
42
|
+
dockerBackendRevision,
|
|
43
|
+
dockerProfile
|
|
44
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SandboxStreamLimits } from '@agen-ai/sandbox';
|
|
2
|
+
import type { DockerEngineAttachment } from '../engine/client.js';
|
|
3
|
+
import { DockerInput } from './input.js';
|
|
4
|
+
import { DockerOutput } from './output.js';
|
|
5
|
+
export declare class DockerProcessAttachment {
|
|
6
|
+
private readonly attachment;
|
|
7
|
+
private readonly limits;
|
|
8
|
+
readonly stdin: DockerInput;
|
|
9
|
+
readonly stdout: DockerOutput;
|
|
10
|
+
readonly stderr: DockerOutput;
|
|
11
|
+
readonly settled: Promise<void>;
|
|
12
|
+
private closed;
|
|
13
|
+
private outputBytes;
|
|
14
|
+
constructor(attachment: DockerEngineAttachment, limits: SandboxStreamLimits, failure: (error: Error) => void);
|
|
15
|
+
close(error?: Error): void;
|
|
16
|
+
private pump;
|
|
17
|
+
private receiveReady;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=attachment.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { SandboxOperationError } from "@agen-ai/sandbox";
|
|
2
|
+
import { DockerFrameDecoder } from "./framing.js";
|
|
3
|
+
import { DockerInput } from "./input.js";
|
|
4
|
+
import { DockerOutput } from "./output.js";
|
|
5
|
+
const RECEIVE_CHUNK_BYTES = 64 * 1024;
|
|
6
|
+
class DockerProcessAttachment {
|
|
7
|
+
constructor(attachment, limits, failure) {
|
|
8
|
+
this.attachment = attachment;
|
|
9
|
+
this.limits = limits;
|
|
10
|
+
const fail = (error) => {
|
|
11
|
+
if (!this.closed) {
|
|
12
|
+
this.close(error);
|
|
13
|
+
failure(error);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
this.stdin = new DockerInput(attachment.stream, limits.highWaterMarkBytes, fail);
|
|
17
|
+
this.stdout = new DockerOutput(fail);
|
|
18
|
+
this.stderr = new DockerOutput(fail);
|
|
19
|
+
const pumping = this.pump().catch((cause) => {
|
|
20
|
+
if (!this.closed) fail(cause instanceof Error ? cause : new SandboxOperationError("stream_failure", "Docker output failed.", { cause }));
|
|
21
|
+
});
|
|
22
|
+
this.settled = Promise.all([pumping, attachment.closed.catch((cause) => {
|
|
23
|
+
if (!this.closed) fail(cause instanceof Error ? cause : new SandboxOperationError("stream_failure", "Docker attachment failed.", { cause }));
|
|
24
|
+
})]).then(() => void 0);
|
|
25
|
+
}
|
|
26
|
+
stdin;
|
|
27
|
+
stdout;
|
|
28
|
+
stderr;
|
|
29
|
+
settled;
|
|
30
|
+
closed = false;
|
|
31
|
+
outputBytes = 0n;
|
|
32
|
+
close(error) {
|
|
33
|
+
if (this.closed) return;
|
|
34
|
+
this.closed = true;
|
|
35
|
+
this.stdin.close(error);
|
|
36
|
+
this.stdout.close(error);
|
|
37
|
+
this.stderr.close(error);
|
|
38
|
+
this.attachment.stream.destroy(error);
|
|
39
|
+
}
|
|
40
|
+
async pump() {
|
|
41
|
+
const decoder = new DockerFrameDecoder();
|
|
42
|
+
if (this.attachment.head.byteLength > RECEIVE_CHUNK_BYTES || this.attachment.stream.readableHighWaterMark > RECEIVE_CHUNK_BYTES) {
|
|
43
|
+
throw new SandboxOperationError("output_limit", "Docker attachment exceeds its bounded native receive profile.");
|
|
44
|
+
}
|
|
45
|
+
const deliver = async (channel, payload) => {
|
|
46
|
+
for (let offset = 0; offset < payload.byteLength; ) {
|
|
47
|
+
if (this.closed) return;
|
|
48
|
+
const size = Math.min(this.limits.highWaterMarkBytes, payload.byteLength - offset);
|
|
49
|
+
if (this.limits.maxOutputBytes !== void 0 && BigInt(size) > BigInt(this.limits.maxOutputBytes) - this.outputBytes) {
|
|
50
|
+
throw new SandboxOperationError("output_limit", "Docker output exceeded its caller-selected combined byte quota.");
|
|
51
|
+
}
|
|
52
|
+
this.outputBytes += BigInt(size);
|
|
53
|
+
const bytes = Buffer.from(payload.subarray(offset, offset + size));
|
|
54
|
+
await (channel === 1 ? this.stdout : this.stderr).deliver(bytes);
|
|
55
|
+
offset += size;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
await decoder.accept(this.attachment.head, deliver);
|
|
59
|
+
while (!this.attachment.stream.readableEnded) {
|
|
60
|
+
if (this.closed) return;
|
|
61
|
+
const size = Math.min(this.attachment.stream.readableLength, RECEIVE_CHUNK_BYTES);
|
|
62
|
+
if (size === 0) {
|
|
63
|
+
await this.receiveReady();
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const chunk = this.attachment.stream.read(size);
|
|
67
|
+
if (!(chunk instanceof Uint8Array)) throw new SandboxOperationError("stream_failure", "Docker attachment returned non-binary bytes.");
|
|
68
|
+
await decoder.accept(chunk, deliver);
|
|
69
|
+
}
|
|
70
|
+
if (this.closed) return;
|
|
71
|
+
decoder.finish();
|
|
72
|
+
this.stdout.finish();
|
|
73
|
+
this.stderr.finish();
|
|
74
|
+
}
|
|
75
|
+
receiveReady() {
|
|
76
|
+
const stream = this.attachment.stream;
|
|
77
|
+
if (stream.readableEnded || stream.readableLength !== 0 || this.closed) return Promise.resolve();
|
|
78
|
+
if (stream.destroyed) return Promise.reject(new SandboxOperationError("stream_failure", "Docker attachment closed before output EOF."));
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const clear = () => {
|
|
81
|
+
stream.removeListener("readable", ready);
|
|
82
|
+
stream.removeListener("end", ready);
|
|
83
|
+
stream.removeListener("close", closed);
|
|
84
|
+
stream.removeListener("error", failed);
|
|
85
|
+
};
|
|
86
|
+
const ready = () => {
|
|
87
|
+
clear();
|
|
88
|
+
resolve();
|
|
89
|
+
};
|
|
90
|
+
const failed = (error) => {
|
|
91
|
+
clear();
|
|
92
|
+
reject(error);
|
|
93
|
+
};
|
|
94
|
+
const closed = () => {
|
|
95
|
+
clear();
|
|
96
|
+
if (this.closed || stream.readableEnded) resolve();
|
|
97
|
+
else reject(new SandboxOperationError("stream_failure", "Docker attachment closed before output EOF."));
|
|
98
|
+
};
|
|
99
|
+
stream.once("readable", ready);
|
|
100
|
+
stream.once("end", ready);
|
|
101
|
+
stream.once("close", closed);
|
|
102
|
+
stream.once("error", failed);
|
|
103
|
+
stream.read(0);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
DockerProcessAttachment
|
|
109
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const DOCKER_FRAME_LIMIT: number;
|
|
2
|
+
export declare class DockerFrameDecoder {
|
|
3
|
+
private readonly header;
|
|
4
|
+
private headerBytes;
|
|
5
|
+
private remaining;
|
|
6
|
+
private channel;
|
|
7
|
+
accept(bytes: Uint8Array, deliver: (channel: 1 | 2, bytes: Uint8Array) => Promise<void>): Promise<void>;
|
|
8
|
+
finish(): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=framing.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SandboxOperationError } from "@agen-ai/sandbox";
|
|
2
|
+
const DOCKER_FRAME_LIMIT = 1024 * 1024;
|
|
3
|
+
class DockerFrameDecoder {
|
|
4
|
+
header = Buffer.alloc(8);
|
|
5
|
+
headerBytes = 0;
|
|
6
|
+
remaining = 0;
|
|
7
|
+
channel = 1;
|
|
8
|
+
async accept(bytes, deliver) {
|
|
9
|
+
let offset = 0;
|
|
10
|
+
while (offset < bytes.byteLength) {
|
|
11
|
+
if (this.remaining === 0) {
|
|
12
|
+
const copied = Math.min(8 - this.headerBytes, bytes.byteLength - offset);
|
|
13
|
+
this.header.set(bytes.subarray(offset, offset + copied), this.headerBytes);
|
|
14
|
+
this.headerBytes += copied;
|
|
15
|
+
offset += copied;
|
|
16
|
+
if (this.headerBytes !== 8) continue;
|
|
17
|
+
const channel = this.header[0];
|
|
18
|
+
if (channel !== 1 && channel !== 2 || this.header[1] !== 0 || this.header[2] !== 0 || this.header[3] !== 0) {
|
|
19
|
+
throw new SandboxOperationError("stream_failure", "Docker output contains an invalid multiplex header.");
|
|
20
|
+
}
|
|
21
|
+
this.remaining = this.header.readUInt32BE(4);
|
|
22
|
+
this.headerBytes = 0;
|
|
23
|
+
this.channel = channel;
|
|
24
|
+
if (this.remaining > DOCKER_FRAME_LIMIT) throw new SandboxOperationError("output_limit", "Docker declared an oversized output frame.");
|
|
25
|
+
if (this.remaining === 0) continue;
|
|
26
|
+
}
|
|
27
|
+
const size = Math.min(this.remaining, bytes.byteLength - offset);
|
|
28
|
+
if (size === 0) continue;
|
|
29
|
+
await deliver(this.channel, bytes.subarray(offset, offset + size));
|
|
30
|
+
offset += size;
|
|
31
|
+
this.remaining -= size;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
finish() {
|
|
35
|
+
if (this.headerBytes !== 0 || this.remaining !== 0) {
|
|
36
|
+
throw new SandboxOperationError("stream_failure", "Docker output ended inside a multiplex frame.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
DOCKER_FRAME_LIMIT,
|
|
42
|
+
DockerFrameDecoder
|
|
43
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Writable, type Duplex } from 'node:stream';
|
|
2
|
+
type WriteCallback = (error?: Error | null) => void;
|
|
3
|
+
export declare class DockerInput extends Writable {
|
|
4
|
+
private readonly socket;
|
|
5
|
+
private readonly failure;
|
|
6
|
+
private closing;
|
|
7
|
+
private active;
|
|
8
|
+
constructor(socket: Duplex, highWaterMark: number, failure: (error: Error) => void);
|
|
9
|
+
_write(chunk: unknown, _encoding: BufferEncoding, callback: WriteCallback): void;
|
|
10
|
+
_final(callback: WriteCallback): void;
|
|
11
|
+
close(error?: Error): void;
|
|
12
|
+
_destroy(error: Error | null, callback: WriteCallback): void;
|
|
13
|
+
private callback;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=input.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
|
+
import { SandboxOperationError } from "@agen-ai/sandbox";
|
|
3
|
+
class DockerInput extends Writable {
|
|
4
|
+
constructor(socket, highWaterMark, failure) {
|
|
5
|
+
super({ highWaterMark, decodeStrings: false, autoDestroy: false });
|
|
6
|
+
this.socket = socket;
|
|
7
|
+
this.failure = failure;
|
|
8
|
+
this.on("error", () => {
|
|
9
|
+
});
|
|
10
|
+
socket.on("error", (error) => this.close(error));
|
|
11
|
+
socket.once("close", () => {
|
|
12
|
+
if (this.active) this.close(new SandboxOperationError("stream_failure", "Docker attachment closed before stdin delivery settled."));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
closing = false;
|
|
16
|
+
active;
|
|
17
|
+
_write(chunk, _encoding, callback) {
|
|
18
|
+
if (!(chunk instanceof Uint8Array)) {
|
|
19
|
+
callback(new SandboxOperationError("stream_failure", "Docker stdin requires binary bytes."));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const complete = this.callback(callback);
|
|
23
|
+
try {
|
|
24
|
+
this.socket.write(chunk, complete);
|
|
25
|
+
} catch (cause) {
|
|
26
|
+
complete(cause instanceof Error ? cause : new SandboxOperationError("stream_failure", "Docker stdin write failed.", { cause }));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
_final(callback) {
|
|
30
|
+
const complete = this.callback(callback);
|
|
31
|
+
try {
|
|
32
|
+
this.socket.end(complete);
|
|
33
|
+
} catch (cause) {
|
|
34
|
+
complete(cause instanceof Error ? cause : new SandboxOperationError("stream_failure", "Docker stdin half-close failed.", { cause }));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
close(error) {
|
|
38
|
+
this.closing = true;
|
|
39
|
+
this.destroy(error);
|
|
40
|
+
}
|
|
41
|
+
_destroy(error, callback) {
|
|
42
|
+
this.active?.(error ?? new SandboxOperationError("cancelled", "Docker stdin closed before its write settled."));
|
|
43
|
+
if (!this.closing && (!this.writableFinished || error)) {
|
|
44
|
+
this.failure(error ?? new SandboxOperationError("stream_failure", "Docker stdin was abandoned."));
|
|
45
|
+
}
|
|
46
|
+
callback(error);
|
|
47
|
+
}
|
|
48
|
+
callback(callback) {
|
|
49
|
+
let done = false;
|
|
50
|
+
const complete = (error) => {
|
|
51
|
+
if (done) return;
|
|
52
|
+
done = true;
|
|
53
|
+
this.active = void 0;
|
|
54
|
+
callback(error);
|
|
55
|
+
};
|
|
56
|
+
this.active = complete;
|
|
57
|
+
return complete;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
DockerInput
|
|
62
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
export declare class DockerOutput extends Readable {
|
|
3
|
+
private readonly failure;
|
|
4
|
+
private demand;
|
|
5
|
+
private complete;
|
|
6
|
+
constructor(failure: (error: Error) => void);
|
|
7
|
+
_read(): void;
|
|
8
|
+
deliver(bytes: Uint8Array): Promise<void>;
|
|
9
|
+
finish(): void;
|
|
10
|
+
close(error?: Error): void;
|
|
11
|
+
_destroy(error: Error | null, callback: (error?: Error | null) => void): void;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=output.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { SandboxOperationError } from "@agen-ai/sandbox";
|
|
3
|
+
class DockerOutput extends Readable {
|
|
4
|
+
constructor(failure) {
|
|
5
|
+
super({ highWaterMark: 0 });
|
|
6
|
+
this.failure = failure;
|
|
7
|
+
this.on("error", () => {
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
demand;
|
|
11
|
+
complete = false;
|
|
12
|
+
_read() {
|
|
13
|
+
this.demand?.();
|
|
14
|
+
this.demand = void 0;
|
|
15
|
+
}
|
|
16
|
+
async deliver(bytes) {
|
|
17
|
+
if (this.destroyed) throw new SandboxOperationError("stream_failure", "Docker output was closed.");
|
|
18
|
+
const requested = Promise.withResolvers();
|
|
19
|
+
this.demand = requested.resolve;
|
|
20
|
+
this.push(bytes);
|
|
21
|
+
if (this.readableLength !== 0) await requested.promise;
|
|
22
|
+
if (this.demand === requested.resolve) this.demand = void 0;
|
|
23
|
+
}
|
|
24
|
+
finish() {
|
|
25
|
+
this.complete = true;
|
|
26
|
+
this.push(null);
|
|
27
|
+
}
|
|
28
|
+
close(error) {
|
|
29
|
+
this.complete = true;
|
|
30
|
+
this.destroy(error);
|
|
31
|
+
}
|
|
32
|
+
_destroy(error, callback) {
|
|
33
|
+
this.demand?.();
|
|
34
|
+
this.demand = void 0;
|
|
35
|
+
if (!this.complete) this.failure(error ?? new SandboxOperationError("stream_failure", "Docker output was abandoned."));
|
|
36
|
+
callback(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
DockerOutput
|
|
41
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OperationOptions, SandboxPolicy, SandboxProcessSpec, SandboxProcessStreams } from '@agen-ai/sandbox';
|
|
2
|
+
import type { SandboxDriverProcessReservation } from '@agen-ai/sandbox/backend';
|
|
3
|
+
import type { DockerSandboxConfig } from '../policy/config.js';
|
|
4
|
+
import type { DockerApi } from '../resources/api.js';
|
|
5
|
+
export declare class DockerProcessOwner implements SandboxDriverProcessReservation {
|
|
6
|
+
private readonly api;
|
|
7
|
+
private readonly config;
|
|
8
|
+
private readonly containerId;
|
|
9
|
+
private readonly spec;
|
|
10
|
+
private readonly policy;
|
|
11
|
+
private readonly assertActive;
|
|
12
|
+
private readonly stopSandbox;
|
|
13
|
+
readonly id: import("@agen-ai/sandbox").SandboxProcessId;
|
|
14
|
+
private readonly stopping;
|
|
15
|
+
private readonly exited;
|
|
16
|
+
private entered;
|
|
17
|
+
private startActual;
|
|
18
|
+
private exitSettled;
|
|
19
|
+
private attachment;
|
|
20
|
+
private monitoring;
|
|
21
|
+
constructor(api: DockerApi, config: DockerSandboxConfig, containerId: string, spec: SandboxProcessSpec, policy: SandboxPolicy, assertActive: (signal?: AbortSignal) => Promise<void>, stopSandbox: () => Promise<void>);
|
|
22
|
+
start(options?: OperationOptions): Promise<SandboxProcessStreams>;
|
|
23
|
+
private launch;
|
|
24
|
+
closeAdmission(): void;
|
|
25
|
+
computeStopped(): Promise<void>;
|
|
26
|
+
private monitor;
|
|
27
|
+
private exit;
|
|
28
|
+
private fail;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=owner.d.ts.map
|