@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/dist/datasets/configuration.d.ts +10 -0
  4. package/dist/datasets/configuration.js +20 -0
  5. package/dist/datasets/mounts.d.ts +21 -0
  6. package/dist/datasets/mounts.js +50 -0
  7. package/dist/datasets/types.d.ts +6 -0
  8. package/dist/datasets/types.js +0 -0
  9. package/dist/datasets/warnings.d.ts +2 -0
  10. package/dist/datasets/warnings.js +7 -0
  11. package/dist/engine/client.d.ts +14 -0
  12. package/dist/engine/client.js +164 -0
  13. package/dist/engine/contracts.d.ts +40 -0
  14. package/dist/engine/contracts.js +22 -0
  15. package/dist/engine/response.d.ts +18 -0
  16. package/dist/engine/response.js +109 -0
  17. package/dist/engine/stream.d.ts +3 -0
  18. package/dist/engine/stream.js +134 -0
  19. package/dist/engine/upgrade.d.ts +9 -0
  20. package/dist/engine/upgrade.js +164 -0
  21. package/dist/engine/upload.d.ts +4 -0
  22. package/dist/engine/upload.js +48 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.js +30 -0
  25. package/dist/policy/config.d.ts +22 -0
  26. package/dist/policy/config.js +63 -0
  27. package/dist/policy/container.d.ts +70 -0
  28. package/dist/policy/container.js +81 -0
  29. package/dist/policy/preflight.d.ts +9 -0
  30. package/dist/policy/preflight.js +89 -0
  31. package/dist/policy/profiles.d.ts +32 -0
  32. package/dist/policy/profiles.js +44 -0
  33. package/dist/process/attachment.d.ts +19 -0
  34. package/dist/process/attachment.js +109 -0
  35. package/dist/process/framing.d.ts +10 -0
  36. package/dist/process/framing.js +43 -0
  37. package/dist/process/input.d.ts +16 -0
  38. package/dist/process/input.js +62 -0
  39. package/dist/process/output.d.ts +13 -0
  40. package/dist/process/output.js +41 -0
  41. package/dist/process/owner.d.ts +30 -0
  42. package/dist/process/owner.js +129 -0
  43. package/dist/recovery/identity.d.ts +11 -0
  44. package/dist/recovery/identity.js +78 -0
  45. package/dist/recovery/reconcile.d.ts +5 -0
  46. package/dist/recovery/reconcile.js +44 -0
  47. package/dist/resources/api.d.ts +23 -0
  48. package/dist/resources/api.js +70 -0
  49. package/dist/resources/fingerprint.d.ts +3 -0
  50. package/dist/resources/fingerprint.js +94 -0
  51. package/dist/resources/inspect.d.ts +49 -0
  52. package/dist/resources/inspect.js +103 -0
  53. package/dist/resources/owner.d.ts +46 -0
  54. package/dist/resources/owner.js +313 -0
  55. package/dist/workspace/exports.d.ts +5 -0
  56. package/dist/workspace/exports.js +52 -0
  57. package/dist/workspace/imports.d.ts +8 -0
  58. package/dist/workspace/imports.js +49 -0
  59. package/dist/workspace/paths.d.ts +8 -0
  60. package/dist/workspace/paths.js +39 -0
  61. package/dist/workspace/selected.d.ts +5 -0
  62. package/dist/workspace/selected.js +34 -0
  63. package/dist/workspace/stat.d.ts +18 -0
  64. package/dist/workspace/stat.js +63 -0
  65. package/dist/workspace/tar/contracts.d.ts +28 -0
  66. package/dist/workspace/tar/contracts.js +0 -0
  67. package/dist/workspace/tar/decode.d.ts +3 -0
  68. package/dist/workspace/tar/decode.js +62 -0
  69. package/dist/workspace/tar/encode.d.ts +3 -0
  70. package/dist/workspace/tar/encode.js +103 -0
  71. package/dist/workspace/tar/entry.d.ts +6 -0
  72. package/dist/workspace/tar/entry.js +80 -0
  73. package/dist/workspace/tar/header.d.ts +11 -0
  74. package/dist/workspace/tar/header.js +84 -0
  75. package/dist/workspace/tar/pax.d.ts +3 -0
  76. package/dist/workspace/tar/pax.js +40 -0
  77. package/dist/workspace/tar/reader.d.ts +16 -0
  78. package/dist/workspace/tar/reader.js +69 -0
  79. package/package.json +59 -3
@@ -0,0 +1,129 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { z } from "zod/v4";
4
+ import { parseSandboxProcessId, SandboxOperationError, SandboxStateError, SandboxUnsupportedError } from "@agen-ai/sandbox";
5
+ import { DOCKER_API } from "../policy/config.js";
6
+ import { DOCKER_ENVIRONMENT } from "../policy/container.js";
7
+ import { DockerProcessAttachment } from "./attachment.js";
8
+ const createdSchema = z.object({ Id: z.string().regex(/^[a-f0-9]{64}$/u) });
9
+ const inspectionSchema = z.object({
10
+ ID: z.string().regex(/^[a-f0-9]{64}$/u),
11
+ ContainerID: z.string().regex(/^[a-f0-9]{64}$/u),
12
+ Running: z.boolean(),
13
+ ExitCode: z.number().int().min(0).max(255).nullable()
14
+ });
15
+ const fixedEnvironment = Object.freeze(Object.fromEntries(DOCKER_ENVIRONMENT.map((entry) => {
16
+ const separator = entry.indexOf("=");
17
+ return [entry.slice(0, separator), entry.slice(separator + 1)];
18
+ })));
19
+ class DockerProcessOwner {
20
+ constructor(api, config, containerId, spec, policy, assertActive, stopSandbox) {
21
+ this.api = api;
22
+ this.config = config;
23
+ this.containerId = containerId;
24
+ this.spec = spec;
25
+ this.policy = policy;
26
+ this.assertActive = assertActive;
27
+ this.stopSandbox = stopSandbox;
28
+ for (const [name, value] of Object.entries(fixedEnvironment)) {
29
+ if (spec.env[name] !== void 0 && spec.env[name] !== value) {
30
+ throw new SandboxUnsupportedError(`Docker requires its fixed safe ${name} environment value.`);
31
+ }
32
+ }
33
+ void this.exited.promise.catch(() => {
34
+ });
35
+ }
36
+ id = parseSandboxProcessId(`docker-process-${randomUUID()}`);
37
+ stopping = new AbortController();
38
+ exited = Promise.withResolvers();
39
+ entered = false;
40
+ startActual;
41
+ exitSettled = false;
42
+ attachment;
43
+ monitoring;
44
+ start(options) {
45
+ if (this.entered || this.stopping.signal.aborted) return Promise.reject(new SandboxStateError("Docker process startup is once-only and cannot follow shutdown."));
46
+ this.entered = true;
47
+ this.startActual = this.launch(options);
48
+ return this.startActual;
49
+ }
50
+ async launch(options) {
51
+ const signal = options?.signal ? AbortSignal.any([options.signal, this.stopping.signal]) : this.stopping.signal;
52
+ signal.throwIfAborted();
53
+ await this.assertActive(signal);
54
+ signal.throwIfAborted();
55
+ const created = await this.api.json("POST", `${DOCKER_API}/containers/${this.containerId}/exec`, createdSchema, { signal, body: {
56
+ AttachStdin: true,
57
+ AttachStdout: true,
58
+ AttachStderr: true,
59
+ Tty: false,
60
+ Privileged: false,
61
+ Cmd: [this.spec.executable, ...this.spec.args],
62
+ Env: Object.entries({ ...fixedEnvironment, ...this.spec.env }).map(([key, value]) => `${key}=${value}`),
63
+ WorkingDir: this.spec.cwd,
64
+ User: `${this.config.user.uid}:${this.config.user.gid}`
65
+ } });
66
+ signal.throwIfAborted();
67
+ await this.assertActive(signal);
68
+ signal.throwIfAborted();
69
+ const attached = await this.api.upgrade(`${DOCKER_API}/exec/${created.Id}/start`, { Detach: false, Tty: false }, signal);
70
+ this.attachment = new DockerProcessAttachment(attached, this.policy.streams, (error) => this.fail(error));
71
+ if (signal.aborted) {
72
+ this.attachment.close();
73
+ throw new SandboxOperationError("cancelled", "Docker process attached after shutdown entered.");
74
+ }
75
+ this.monitoring = this.monitor(created.Id).catch((error) => {
76
+ if (!this.stopping.signal.aborted) this.fail(error instanceof Error ? error : new SandboxOperationError("driver_failure", "Docker process inspection failed.", { cause: error }));
77
+ });
78
+ return Object.freeze({ stdin: this.attachment.stdin, stdout: this.attachment.stdout, stderr: this.attachment.stderr, exit: this.exited.promise });
79
+ }
80
+ closeAdmission() {
81
+ if (!this.stopping.signal.aborted) this.stopping.abort(new SandboxOperationError("cancelled", "Docker sandbox compute is stopping."));
82
+ this.attachment?.close();
83
+ }
84
+ async computeStopped() {
85
+ this.closeAdmission();
86
+ this.exit({ kind: "unavailable", reason: "sandbox_stopped" });
87
+ await this.startActual?.catch(() => {
88
+ });
89
+ await this.monitoring;
90
+ await this.attachment?.settled;
91
+ }
92
+ async monitor(execId) {
93
+ while (!this.stopping.signal.aborted) {
94
+ const response = await this.api.request("GET", `${DOCKER_API}/exec/${execId}/json`, { statuses: [200, 404], signal: this.stopping.signal });
95
+ if (this.stopping.signal.aborted) return;
96
+ if (response.statusCode === 404) {
97
+ throw new SandboxOperationError("driver_failure", "Docker lost the owned exec before sandbox removal was confirmed.");
98
+ }
99
+ let value;
100
+ try {
101
+ value = JSON.parse(Buffer.from(response.body).toString("utf8"));
102
+ } catch (cause) {
103
+ throw new SandboxOperationError("driver_failure", "Docker exec inspection returned invalid JSON.", { cause });
104
+ }
105
+ const result = inspectionSchema.safeParse(value);
106
+ if (!result.success || result.data.ID !== execId || result.data.ContainerID !== this.containerId) {
107
+ throw new SandboxOperationError("driver_failure", "Docker exec inspection differs from the exact owned exec and workload.");
108
+ }
109
+ if (!result.data.Running && result.data.ExitCode !== null) {
110
+ this.exit({ kind: "exited", code: result.data.ExitCode });
111
+ return;
112
+ }
113
+ await delay(100, void 0, { signal: this.stopping.signal });
114
+ }
115
+ }
116
+ exit(result) {
117
+ if (this.exitSettled) return;
118
+ this.exitSettled = true;
119
+ this.exited.resolve(Object.freeze(result));
120
+ }
121
+ fail(error) {
122
+ this.attachment?.close(error);
123
+ void this.stopSandbox().catch(() => {
124
+ });
125
+ }
126
+ }
127
+ export {
128
+ DockerProcessOwner
129
+ };
@@ -0,0 +1,11 @@
1
+ import type { SandboxRecoveryRecord, SandboxRecoveryResource, SandboxRequest } from '@agen-ai/sandbox';
2
+ import type { DockerSandboxConfig } from '../policy/config.js';
3
+ export type ResourceRole = SandboxRecoveryResource['role'];
4
+ export declare function digest(value: unknown): string;
5
+ export declare function resourceName(namespace: string, operationId: string, role: ResourceRole): string;
6
+ export declare function configurationDigest(config: DockerSandboxConfig): string;
7
+ export declare function newRecoveryRecord(config: DockerSandboxConfig, request: SandboxRequest): SandboxRecoveryRecord;
8
+ export declare function resourceLabels(record: SandboxRecoveryRecord, role: ResourceRole): Readonly<Record<string, string>>;
9
+ export declare function assertLabels(actual: Readonly<Record<string, string>>, record: SandboxRecoveryRecord, role: ResourceRole): void;
10
+ export declare function validateRecord(record: SandboxRecoveryRecord, config: DockerSandboxConfig): boolean;
11
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { parseSandboxId, parseSandboxNamespace, parseSandboxRecoveryRecord, SandboxStateError } from "@agen-ai/sandbox";
3
+ import { DOCKER_BACKEND_ID } from "../policy/config.js";
4
+ import { dockerBackendRevision } from "../policy/profiles.js";
5
+ function digest(value) {
6
+ return `sha256:${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
7
+ }
8
+ function resourceName(namespace, operationId, role) {
9
+ return `agenai-sbx-${createHash("sha256").update(`${namespace}\0${operationId}`).digest("hex").slice(0, 32)}-${role}`;
10
+ }
11
+ function configurationDigest(config) {
12
+ return digest({ profile: config.profile, user: config.user, imageDigest: config.imageDigest, datasets: config.datasets });
13
+ }
14
+ function newRecoveryRecord(config, request) {
15
+ const operationId = randomUUID();
16
+ const roles = ["compute", "storage", "export"];
17
+ if (request.policy.network.kind === "outbound") roles.push("network");
18
+ return parseSandboxRecoveryRecord({
19
+ format: "agenai.sandbox-recovery.v2",
20
+ backendId: DOCKER_BACKEND_ID,
21
+ backendRevision: dockerBackendRevision(config.profile),
22
+ namespace: parseSandboxNamespace(config.namespace),
23
+ operationId,
24
+ sandboxId: parseSandboxId(`docker-${operationId}`),
25
+ imageDigest: config.imageDigest,
26
+ configurationDigest: configurationDigest(config),
27
+ policyDigest: digest(request.policy),
28
+ phase: "reserved",
29
+ authority: { kind: "unverified" },
30
+ resources: roles.map((role) => ({ role, name: resourceName(config.namespace, operationId, role), identity: { kind: "planned" } }))
31
+ });
32
+ }
33
+ function recordBinding(record) {
34
+ return digest({
35
+ format: record.format,
36
+ backendId: record.backendId,
37
+ backendRevision: record.backendRevision,
38
+ namespace: record.namespace,
39
+ operationId: record.operationId,
40
+ sandboxId: record.sandboxId,
41
+ imageDigest: record.imageDigest,
42
+ configurationDigest: record.configurationDigest,
43
+ policyDigest: record.policyDigest,
44
+ resources: record.resources.map(({ role, name }) => ({ role, name }))
45
+ });
46
+ }
47
+ function resourceLabels(record, role) {
48
+ return Object.freeze({
49
+ "ai.agen.sandbox.record": recordBinding(record),
50
+ "ai.agen.sandbox.role": role,
51
+ "ai.agen.sandbox.namespace": record.namespace,
52
+ "ai.agen.sandbox.operation": record.operationId,
53
+ "ai.agen.sandbox.backend": record.backendId,
54
+ "ai.agen.sandbox.revision": record.backendRevision,
55
+ "ai.agen.sandbox.image": record.imageDigest,
56
+ "ai.agen.sandbox.configuration": record.configurationDigest,
57
+ "ai.agen.sandbox.policy": record.policyDigest
58
+ });
59
+ }
60
+ function assertLabels(actual, record, role) {
61
+ const expected = resourceLabels(record, role);
62
+ if (Object.entries(expected).some(([key, value]) => actual[key] !== value)) {
63
+ throw new SandboxStateError("Docker resource labels do not bind the original reserved ownership record.");
64
+ }
65
+ }
66
+ function validateRecord(record, config) {
67
+ const roles = record.resources.map((resource) => resource.role);
68
+ return record.backendId === DOCKER_BACKEND_ID && record.backendRevision === dockerBackendRevision(config.profile) && record.namespace === config.namespace && record.imageDigest === config.imageDigest && record.configurationDigest === configurationDigest(config) && /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u.test(record.operationId) && record.sandboxId === `docker-${record.operationId}` && roles.length >= 3 && roles.length <= 4 && ["compute", "storage", "export"].every((role) => roles.includes(role)) && record.resources.every((resource) => resource.name === resourceName(config.namespace, record.operationId, resource.role));
69
+ }
70
+ export {
71
+ assertLabels,
72
+ configurationDigest,
73
+ digest,
74
+ newRecoveryRecord,
75
+ resourceLabels,
76
+ resourceName,
77
+ validateRecord
78
+ };
@@ -0,0 +1,5 @@
1
+ import type { OperationOptions, SandboxReconciliation, SandboxRecoveryRecord } from '@agen-ai/sandbox';
2
+ import type { DockerSandboxConfig } from '../policy/config.js';
3
+ import type { DockerApi } from '../resources/api.js';
4
+ export declare function reconcileDocker(api: DockerApi, config: DockerSandboxConfig, original: SandboxRecoveryRecord, options?: OperationOptions): Promise<SandboxReconciliation>;
5
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { SandboxStateError, parseSandboxRecoveryRecord } from "@agen-ai/sandbox";
2
+ import { verifyDaemon } from "../policy/preflight.js";
3
+ import { inspectResource } from "../resources/inspect.js";
4
+ import { DockerResourceOwner } from "../resources/owner.js";
5
+ import { validateRecord } from "./identity.js";
6
+ async function reconcileDocker(api, config, original, options) {
7
+ if (!validateRecord(original, config)) return Object.freeze({ kind: "conflict", reason: "Recovery record does not match this Docker backend profile, namespace, image or reserved identity." });
8
+ const backendIdentity = await verifyDaemon(api, config, options?.signal);
9
+ if (original.authority.kind === "verified" && original.authority.backendIdentity !== backendIdentity) {
10
+ return Object.freeze({ kind: "conflict", reason: "Recovery record belongs to another Docker daemon identity." });
11
+ }
12
+ let current = original;
13
+ let present = false;
14
+ let uncertain = false;
15
+ for (const resource of original.resources) {
16
+ const reference = resource.identity.kind === "identified" ? resource.identity.id : resource.name;
17
+ try {
18
+ const inspection = await inspectResource(api, current, resource, reference, options?.signal);
19
+ if (!inspection) {
20
+ if (resource.identity.kind === "creating") uncertain = true;
21
+ continue;
22
+ }
23
+ if (resource.identity.kind === "planned") {
24
+ return Object.freeze({ kind: "conflict", reason: "A planned resource name is already present; recover from the latest durable record after the previous controller is quiescent." });
25
+ }
26
+ present = true;
27
+ if (resource.identity.kind === "creating") current = parseSandboxRecoveryRecord({
28
+ ...current,
29
+ resources: current.resources.map((candidate) => candidate.role === resource.role ? {
30
+ ...candidate,
31
+ identity: { kind: "identified", id: inspection.id, fingerprint: inspection.fingerprint }
32
+ } : candidate)
33
+ });
34
+ } catch (error) {
35
+ if (!(error instanceof SandboxStateError)) throw error;
36
+ return Object.freeze({ kind: "conflict", reason: "Docker resource identity, configuration or original recovery binding does not match." });
37
+ }
38
+ }
39
+ if (!present && !uncertain) return Object.freeze({ kind: "absent", record: current });
40
+ return Object.freeze({ kind: uncertain ? "uncertain" : "matched", owner: new DockerResourceOwner(api, config, void 0, current) });
41
+ }
42
+ export {
43
+ reconcileDocker
44
+ };
@@ -0,0 +1,23 @@
1
+ import type { ZodType } from 'zod/v4';
2
+ import { DockerEngine } from '../engine/client.js';
3
+ import type { DockerEngineStreamOptions, DockerEngineStreamResponse, DockerEngineStreamResult } from '../engine/contracts.js';
4
+ export declare class DockerApi {
5
+ readonly engine: DockerEngine;
6
+ readonly timeoutMs: number;
7
+ constructor(engine: DockerEngine, timeoutMs: number);
8
+ stream<Value>(method: 'GET' | 'HEAD' | 'PUT', path: string, options: DockerEngineStreamOptions, consume: (response: DockerEngineStreamResponse) => Promise<Value>): Promise<DockerEngineStreamResult<Value>>;
9
+ upgrade(path: string, body: unknown, signal?: AbortSignal): Promise<import("../engine/upgrade.js").DockerEngineAttachment>;
10
+ json<Value>(method: 'GET' | 'POST' | 'DELETE', path: string, schema: ZodType<Value>, options?: {
11
+ body?: unknown;
12
+ signal?: AbortSignal;
13
+ statuses?: readonly number[];
14
+ }): Promise<Value>;
15
+ request(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, options?: {
16
+ body?: Uint8Array;
17
+ contentType?: 'application/json' | 'application/x-tar';
18
+ maxResponseBytes?: number;
19
+ signal?: AbortSignal;
20
+ statuses?: readonly number[];
21
+ }): Promise<import("../engine/contracts.js").DockerEngineResponse>;
22
+ }
23
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { SandboxOperationError } from "@agen-ai/sandbox";
2
+ import { DockerEngineTransportError } from "../engine/client.js";
3
+ class DockerApi {
4
+ constructor(engine, timeoutMs) {
5
+ this.engine = engine;
6
+ this.timeoutMs = timeoutMs;
7
+ }
8
+ async stream(method, path, options, consume) {
9
+ try {
10
+ return await this.engine.stream(method, path, options, async (response) => {
11
+ if (response.statusCode !== 200) throw new SandboxOperationError("driver_failure", `Docker transfer returned HTTP ${response.statusCode}.`);
12
+ return consume(response);
13
+ });
14
+ } catch (cause) {
15
+ throw transportFailure(cause);
16
+ }
17
+ }
18
+ async upgrade(path, body, signal) {
19
+ try {
20
+ return await this.engine.upgrade(path, {
21
+ body: Buffer.from(JSON.stringify(body)),
22
+ contentType: "application/json",
23
+ timeoutMs: this.timeoutMs,
24
+ maxResponseBytes: 1024 * 1024,
25
+ signal
26
+ });
27
+ } catch (cause) {
28
+ throw transportFailure(cause);
29
+ }
30
+ }
31
+ async json(method, path, schema, options = {}) {
32
+ const body = options.body === void 0 ? void 0 : Buffer.from(JSON.stringify(options.body), "utf8");
33
+ const response = await this.request(method, path, { ...options, body, maxResponseBytes: 1024 * 1024, contentType: "application/json" });
34
+ let value;
35
+ try {
36
+ value = JSON.parse(Buffer.from(response.body).toString("utf8"));
37
+ } catch (cause) {
38
+ throw new SandboxOperationError("driver_failure", "Docker returned invalid JSON.", { cause });
39
+ }
40
+ const parsed = schema.safeParse(value);
41
+ if (!parsed.success) throw new SandboxOperationError("driver_failure", "Docker returned an invalid resource response.");
42
+ return parsed.data;
43
+ }
44
+ async request(method, path, options = {}) {
45
+ let response;
46
+ try {
47
+ response = await this.engine.request(method, path, {
48
+ timeoutMs: this.timeoutMs,
49
+ maxResponseBytes: options.maxResponseBytes ?? 1024 * 1024,
50
+ body: options.body,
51
+ contentType: options.contentType,
52
+ signal: options.signal
53
+ });
54
+ } catch (cause) {
55
+ throw transportFailure(cause);
56
+ }
57
+ if (!(options.statuses ?? [200, 201, 204]).includes(response.statusCode)) {
58
+ throw new SandboxOperationError("driver_failure", `Docker resource operation returned HTTP ${response.statusCode}.`);
59
+ }
60
+ return response;
61
+ }
62
+ }
63
+ function transportFailure(cause) {
64
+ if (!(cause instanceof DockerEngineTransportError)) return cause;
65
+ const code = cause.code === "deadline" ? "deadline_exceeded" : cause.code === "aborted" ? "cancelled" : "driver_failure";
66
+ return new SandboxOperationError(code, "Docker resource transport did not settle successfully.", { cause });
67
+ }
68
+ export {
69
+ DockerApi
70
+ };
@@ -0,0 +1,3 @@
1
+ import type { ContainerInspection } from './inspect.js';
2
+ export declare function containerFingerprint(container: ContainerInspection): string;
3
+ //# sourceMappingURL=fingerprint.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { digest } from "../recovery/identity.js";
2
+ const CONFIGURATION_FIELDS = [
3
+ "Image",
4
+ "User",
5
+ "WorkingDir",
6
+ "Entrypoint",
7
+ "Cmd",
8
+ "Env",
9
+ "Labels",
10
+ "Tty",
11
+ "OpenStdin",
12
+ "AttachStdin",
13
+ "AttachStdout",
14
+ "AttachStderr",
15
+ "NetworkDisabled",
16
+ "Healthcheck",
17
+ "Volumes",
18
+ "ExposedPorts",
19
+ "StopSignal",
20
+ "StopTimeout"
21
+ ];
22
+ const HOST_POLICY_FIELDS = [
23
+ "ReadonlyRootfs",
24
+ "Privileged",
25
+ "CapDrop",
26
+ "CapAdd",
27
+ "SecurityOpt",
28
+ "PidMode",
29
+ "IpcMode",
30
+ "CgroupnsMode",
31
+ "UsernsMode",
32
+ "UTSMode",
33
+ "CgroupParent",
34
+ "NetworkMode",
35
+ "PublishAllPorts",
36
+ "PortBindings",
37
+ "ExtraHosts",
38
+ "Links",
39
+ "Dns",
40
+ "DnsOptions",
41
+ "DnsSearch",
42
+ "Devices",
43
+ "DeviceRequests",
44
+ "DeviceCgroupRules",
45
+ "Binds",
46
+ "Mounts",
47
+ "Tmpfs",
48
+ "VolumesFrom",
49
+ "NanoCpus",
50
+ "Memory",
51
+ "MemorySwap",
52
+ "MemoryReservation",
53
+ "MemorySwappiness",
54
+ "PidsLimit",
55
+ "CpuShares",
56
+ "CpuPeriod",
57
+ "CpuQuota",
58
+ "CpuRealtimePeriod",
59
+ "CpuRealtimeRuntime",
60
+ "CpusetCpus",
61
+ "CpusetMems",
62
+ "RestartPolicy",
63
+ "AutoRemove",
64
+ "Runtime",
65
+ "Init",
66
+ "StorageOpt",
67
+ "Ulimits"
68
+ ];
69
+ function selected(value, keys) {
70
+ return Object.fromEntries(keys.map((key) => [key, value[key]]));
71
+ }
72
+ function containerFingerprint(container) {
73
+ return digest({
74
+ Id: container.Id,
75
+ Name: container.Name,
76
+ Image: container.Image,
77
+ Config: selected(container.Config, CONFIGURATION_FIELDS),
78
+ HostConfig: selected(container.HostConfig, HOST_POLICY_FIELDS),
79
+ // Moby clears an unsupported false OOM switch to null when starting on cgroup v2.
80
+ OomKillDisabled: container.HostConfig.OomKillDisable === true,
81
+ Mounts: [...container.Mounts].sort((left, right) => left.Destination < right.Destination ? -1 : left.Destination > right.Destination ? 1 : 0).map((mount) => ({
82
+ Type: mount.Type,
83
+ Name: mount.Name,
84
+ Source: mount.Source,
85
+ Destination: mount.Destination,
86
+ RW: mount.RW,
87
+ Driver: mount.Driver,
88
+ Propagation: mount.Propagation
89
+ }))
90
+ });
91
+ }
92
+ export {
93
+ containerFingerprint
94
+ };
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod/v4';
2
+ import type { SandboxRecoveryRecord, SandboxRecoveryResource } from '@agen-ai/sandbox';
3
+ import type { DockerApi } from './api.js';
4
+ export declare const containerSchema: z.ZodObject<{
5
+ Id: z.ZodString;
6
+ Name: z.ZodString;
7
+ Image: z.ZodString;
8
+ Config: z.ZodObject<{
9
+ Labels: z.ZodRecord<z.ZodString, z.ZodString>;
10
+ Image: z.ZodString;
11
+ User: z.ZodString;
12
+ WorkingDir: z.ZodString;
13
+ Entrypoint: z.ZodNullable<z.ZodArray<z.ZodString>>;
14
+ Cmd: z.ZodNullable<z.ZodArray<z.ZodString>>;
15
+ Env: z.ZodNullable<z.ZodArray<z.ZodString>>;
16
+ Tty: z.ZodBoolean;
17
+ OpenStdin: z.ZodBoolean;
18
+ Volumes: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
19
+ }, z.core.$loose>;
20
+ HostConfig: z.ZodObject<{}, z.core.$loose>;
21
+ Mounts: z.ZodArray<z.ZodObject<{
22
+ Type: z.ZodString;
23
+ Name: z.ZodOptional<z.ZodString>;
24
+ Source: z.ZodString;
25
+ Destination: z.ZodString;
26
+ RW: z.ZodBoolean;
27
+ Propagation: z.ZodOptional<z.ZodString>;
28
+ }, z.core.$loose>>;
29
+ NetworkSettings: z.ZodObject<{
30
+ Networks: z.ZodRecord<z.ZodString, z.ZodObject<{
31
+ NetworkID: z.ZodString;
32
+ }, z.core.$strip>>;
33
+ Ports: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
34
+ }, z.core.$strip>;
35
+ State: z.ZodObject<{
36
+ Running: z.ZodBoolean;
37
+ Status: z.ZodString;
38
+ }, z.core.$loose>;
39
+ AppArmorProfile: z.ZodString;
40
+ }, z.core.$loose>;
41
+ export type ContainerInspection = z.infer<typeof containerSchema>;
42
+ export interface ResourceInspection {
43
+ readonly id: string;
44
+ readonly fingerprint: string;
45
+ readonly container?: ContainerInspection;
46
+ }
47
+ export declare function inspectPath(resource: SandboxRecoveryResource, identity: string): string;
48
+ export declare function inspectResource(api: DockerApi, record: SandboxRecoveryRecord, resource: SandboxRecoveryResource, reference: string, signal?: AbortSignal): Promise<ResourceInspection | undefined>;
49
+ //# sourceMappingURL=inspect.d.ts.map
@@ -0,0 +1,103 @@
1
+ import { z } from "zod/v4";
2
+ import { SandboxStateError } from "@agen-ai/sandbox";
3
+ import { DOCKER_API } from "../policy/config.js";
4
+ import { assertLabels, digest } from "../recovery/identity.js";
5
+ import { containerFingerprint } from "./fingerprint.js";
6
+ const labelsSchema = z.record(z.string(), z.string());
7
+ const mountSchema = z.object({ Type: z.string(), Name: z.string().optional(), Source: z.string(), Destination: z.string(), RW: z.boolean(), Propagation: z.string().optional() }).passthrough();
8
+ const containerSchema = z.object({
9
+ Id: z.string().regex(/^[a-f0-9]{64}$/u),
10
+ Name: z.string(),
11
+ Image: z.string(),
12
+ Config: z.object({
13
+ Labels: labelsSchema,
14
+ Image: z.string(),
15
+ User: z.string(),
16
+ WorkingDir: z.string(),
17
+ Entrypoint: z.array(z.string()).nullable(),
18
+ Cmd: z.array(z.string()).nullable(),
19
+ Env: z.array(z.string()).nullable(),
20
+ Tty: z.boolean(),
21
+ OpenStdin: z.boolean(),
22
+ Volumes: z.record(z.string(), z.unknown()).nullish()
23
+ }).passthrough(),
24
+ HostConfig: z.object({}).passthrough(),
25
+ Mounts: z.array(mountSchema),
26
+ NetworkSettings: z.object({ Networks: z.record(z.string(), z.object({ NetworkID: z.string() })), Ports: z.record(z.string(), z.unknown()).nullable() }),
27
+ State: z.object({ Running: z.boolean(), Status: z.string() }).passthrough(),
28
+ AppArmorProfile: z.string()
29
+ }).passthrough();
30
+ const volumeSchema = z.object({
31
+ Name: z.string(),
32
+ Driver: z.string(),
33
+ Mountpoint: z.string(),
34
+ CreatedAt: z.string(),
35
+ Labels: labelsSchema,
36
+ Options: z.record(z.string(), z.string()).nullable(),
37
+ Scope: z.string()
38
+ });
39
+ const networkSchema = z.object({
40
+ Id: z.string().regex(/^[a-f0-9]{64}$/u),
41
+ Name: z.string(),
42
+ Created: z.string(),
43
+ Driver: z.string(),
44
+ Scope: z.string(),
45
+ Internal: z.boolean(),
46
+ Attachable: z.boolean(),
47
+ Ingress: z.boolean(),
48
+ EnableIPv6: z.boolean(),
49
+ ConfigOnly: z.boolean(),
50
+ IPAM: z.object({}).passthrough(),
51
+ Options: z.record(z.string(), z.string()),
52
+ Labels: labelsSchema
53
+ });
54
+ function inspectPath(resource, identity) {
55
+ const encoded = encodeURIComponent(identity);
56
+ if (resource.role === "compute" || resource.role === "export") return `${DOCKER_API}/containers/${encoded}/json`;
57
+ if (resource.role === "storage") return `${DOCKER_API}/volumes/${encoded}`;
58
+ return `${DOCKER_API}/networks/${encoded}`;
59
+ }
60
+ async function inspectResource(api, record, resource, reference, signal) {
61
+ const response = await api.request("GET", inspectPath(resource, reference), { statuses: [200, 404], signal });
62
+ if (response.statusCode === 404) return void 0;
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(Buffer.from(response.body).toString("utf8"));
66
+ } catch {
67
+ throw new SandboxStateError("Docker resource inspection returned invalid JSON.");
68
+ }
69
+ let result;
70
+ if (resource.role === "compute" || resource.role === "export") {
71
+ const value = containerSchema.safeParse(parsed);
72
+ if (!value.success) throw new SandboxStateError("Docker returned an invalid container inspection.");
73
+ const container = value.data;
74
+ assertLabels(container.Config.Labels, record, resource.role);
75
+ if (container.Name !== `/${resource.name}` || container.Image !== record.imageDigest || container.Config.Image !== record.imageDigest) {
76
+ throw new SandboxStateError("Docker container name or image differs from reserved ownership.");
77
+ }
78
+ result = { id: container.Id, fingerprint: containerFingerprint(container), container };
79
+ } else if (resource.role === "storage") {
80
+ const value = volumeSchema.safeParse(parsed);
81
+ if (!value.success) throw new SandboxStateError("Docker returned an invalid volume inspection.");
82
+ const volume = value.data;
83
+ assertLabels(volume.Labels, record, resource.role);
84
+ if (volume.Name !== resource.name || volume.Driver !== "local" || volume.Scope !== "local" || Object.keys(volume.Options ?? {}).length !== 0) throw new SandboxStateError("Docker volume does not match the owned local volume profile.");
85
+ result = { id: volume.Name, fingerprint: digest(value.data) };
86
+ } else {
87
+ const value = networkSchema.safeParse(parsed);
88
+ if (!value.success) throw new SandboxStateError("Docker returned an invalid network inspection.");
89
+ const network = value.data;
90
+ assertLabels(network.Labels, record, resource.role);
91
+ if (network.Name !== resource.name || network.Driver !== "bridge" || network.Scope !== "local" || network.Internal || network.Attachable || network.Ingress || network.ConfigOnly || network.EnableIPv6 || Object.keys(network.Options).length !== 0) throw new SandboxStateError("Docker network does not match the owned outbound bridge profile.");
92
+ result = { id: network.Id, fingerprint: digest(network) };
93
+ }
94
+ if (resource.identity.kind === "identified" && (resource.identity.id !== result.id || resource.identity.fingerprint !== result.fingerprint)) {
95
+ throw new SandboxStateError("Docker resource no longer matches its exact inspected identity and fingerprint.");
96
+ }
97
+ return result;
98
+ }
99
+ export {
100
+ containerSchema,
101
+ inspectPath,
102
+ inspectResource
103
+ };
@@ -0,0 +1,46 @@
1
+ import type { SandboxCleanupOwner, SandboxRecoveryRecord, SandboxRequest } from '@agen-ai/sandbox';
2
+ import type { SandboxDriverReady, SandboxDriverReservation, SandboxDriverStartOptions } from '@agen-ai/sandbox/backend';
3
+ import type { DockerSandboxConfig } from '../policy/config.js';
4
+ import type { ResourceRole } from '../recovery/identity.js';
5
+ import { DockerApi } from './api.js';
6
+ import type { ResourceInspection } from './inspect.js';
7
+ export declare class DockerResourceOwner implements SandboxDriverReservation, SandboxCleanupOwner {
8
+ private readonly api;
9
+ private readonly config;
10
+ private readonly request;
11
+ private current;
12
+ private closed;
13
+ private startActual;
14
+ private stopActual;
15
+ private destroyActual;
16
+ private checkpoint;
17
+ private checkpointTail;
18
+ private readonly returnedIds;
19
+ private readonly createEffects;
20
+ private readonly absent;
21
+ private process;
22
+ constructor(api: DockerApi, config: DockerSandboxConfig, request: SandboxRequest | undefined, record?: SandboxRecoveryRecord);
23
+ get id(): import("@agen-ai/sandbox").SandboxId;
24
+ get recoveryRecord(): SandboxRecoveryRecord;
25
+ start(options: SandboxDriverStartOptions): Promise<SandboxDriverReady>;
26
+ private acquire;
27
+ private importTree;
28
+ private exportTree;
29
+ private createResource;
30
+ stop(): Promise<void>;
31
+ private cleanupCompute;
32
+ destroy(): Promise<void>;
33
+ private cleanupAll;
34
+ resolveResource(role: ResourceRole, signal?: AbortSignal): Promise<ResourceInspection | undefined>;
35
+ private requirePresent;
36
+ private deleteResource;
37
+ private resource;
38
+ private updateIdentity;
39
+ private cleanupPhase;
40
+ private publishCleanup;
41
+ private publish;
42
+ private submitCheckpoint;
43
+ private assertAuthority;
44
+ private assertOpen;
45
+ }
46
+ //# sourceMappingURL=owner.d.ts.map