@agen-ai/sandbox 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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -2
  3. package/dist/acp/index.d.ts +9 -0
  4. package/dist/acp/index.js +25 -0
  5. package/dist/backend/contracts.d.ts +41 -0
  6. package/dist/backend/contracts.js +0 -0
  7. package/dist/backend/index.d.ts +6 -0
  8. package/dist/backend/index.js +64 -0
  9. package/dist/backend/spill.d.ts +15 -0
  10. package/dist/backend/spill.js +0 -0
  11. package/dist/contracts.d.ts +37 -0
  12. package/dist/contracts.js +0 -0
  13. package/dist/errors.d.ts +46 -0
  14. package/dist/errors.js +73 -0
  15. package/dist/files/bytes.d.ts +31 -0
  16. package/dist/files/bytes.js +134 -0
  17. package/dist/files/iterator.d.ts +13 -0
  18. package/dist/files/iterator.js +59 -0
  19. package/dist/files/metadata/index.d.ts +12 -0
  20. package/dist/files/metadata/index.js +193 -0
  21. package/dist/files/metadata/records.d.ts +21 -0
  22. package/dist/files/metadata/records.js +50 -0
  23. package/dist/files/metadata/sort.d.ts +13 -0
  24. package/dist/files/metadata/sort.js +63 -0
  25. package/dist/files/metadata/storage.d.ts +12 -0
  26. package/dist/files/metadata/storage.js +69 -0
  27. package/dist/files/metadata/types.d.ts +19 -0
  28. package/dist/files/metadata/types.js +0 -0
  29. package/dist/files/parse.d.ts +20 -0
  30. package/dist/files/parse.js +62 -0
  31. package/dist/files/pathSchemas.d.ts +5 -0
  32. package/dist/files/pathSchemas.js +16 -0
  33. package/dist/files/paths.d.ts +4 -0
  34. package/dist/files/paths.js +12 -0
  35. package/dist/files/quotas.d.ts +18 -0
  36. package/dist/files/quotas.js +37 -0
  37. package/dist/files/types.d.ts +81 -0
  38. package/dist/files/types.js +0 -0
  39. package/dist/index.d.ts +15 -0
  40. package/dist/index.js +35 -0
  41. package/dist/internal/data.d.ts +6 -0
  42. package/dist/internal/data.js +68 -0
  43. package/dist/internal/operations.d.ts +4 -0
  44. package/dist/internal/operations.js +30 -0
  45. package/dist/internal/parse.d.ts +3 -0
  46. package/dist/internal/parse.js +12 -0
  47. package/dist/lifecycle/deadline.d.ts +3 -0
  48. package/dist/lifecycle/deadline.js +18 -0
  49. package/dist/lifecycle/journal.d.ts +14 -0
  50. package/dist/lifecycle/journal.js +51 -0
  51. package/dist/lifecycle/observation.d.ts +9 -0
  52. package/dist/lifecycle/observation.js +41 -0
  53. package/dist/lifecycle/owner.d.ts +38 -0
  54. package/dist/lifecycle/owner.js +288 -0
  55. package/dist/lifecycle/recovered.d.ts +16 -0
  56. package/dist/lifecycle/recovered.js +62 -0
  57. package/dist/lifecycle/recoveryIdentity.d.ts +3 -0
  58. package/dist/lifecycle/recoveryIdentity.js +18 -0
  59. package/dist/lifecycle/transfer.d.ts +10 -0
  60. package/dist/lifecycle/transfer.js +339 -0
  61. package/dist/node/directories/closing.d.ts +2 -0
  62. package/dist/node/directories/closing.js +19 -0
  63. package/dist/node/directories/export.d.ts +6 -0
  64. package/dist/node/directories/export.js +43 -0
  65. package/dist/node/directories/file.d.ts +26 -0
  66. package/dist/node/directories/file.js +104 -0
  67. package/dist/node/directories/import.d.ts +6 -0
  68. package/dist/node/directories/import.js +38 -0
  69. package/dist/node/directories/options.d.ts +4 -0
  70. package/dist/node/directories/options.js +10 -0
  71. package/dist/node/directories/ownership.d.ts +33 -0
  72. package/dist/node/directories/ownership.js +148 -0
  73. package/dist/node/directories/source.d.ts +25 -0
  74. package/dist/node/directories/source.js +143 -0
  75. package/dist/node/directories/stage.d.ts +43 -0
  76. package/dist/node/directories/stage.js +302 -0
  77. package/dist/node/directories/types.d.ts +23 -0
  78. package/dist/node/directories/types.js +17 -0
  79. package/dist/node/hostPath.d.ts +7 -0
  80. package/dist/node/hostPath.js +37 -0
  81. package/dist/node/index.d.ts +8 -0
  82. package/dist/node/index.js +14 -0
  83. package/dist/node/spill.d.ts +14 -0
  84. package/dist/node/spill.js +97 -0
  85. package/dist/node/spillDirectory.d.ts +4 -0
  86. package/dist/node/spillDirectory.js +17 -0
  87. package/dist/node/spillFile.d.ts +24 -0
  88. package/dist/node/spillFile.js +96 -0
  89. package/dist/policy/limits.d.ts +31 -0
  90. package/dist/policy/limits.js +35 -0
  91. package/dist/policy/parse.d.ts +11 -0
  92. package/dist/policy/parse.js +47 -0
  93. package/dist/policy/schemas.d.ts +148 -0
  94. package/dist/policy/schemas.js +75 -0
  95. package/dist/policy/types.d.ts +105 -0
  96. package/dist/policy/types.js +0 -0
  97. package/dist/process/input.d.ts +23 -0
  98. package/dist/process/input.js +93 -0
  99. package/dist/process/output.d.ts +13 -0
  100. package/dist/process/output.js +43 -0
  101. package/dist/process/parse.d.ts +4 -0
  102. package/dist/process/parse.js +32 -0
  103. package/dist/process/streams.d.ts +9 -0
  104. package/dist/process/streams.js +61 -0
  105. package/dist/process/types.d.ts +38 -0
  106. package/dist/process/types.js +0 -0
  107. package/dist/recovery/parse.d.ts +4 -0
  108. package/dist/recovery/parse.js +67 -0
  109. package/dist/recovery/types.d.ts +59 -0
  110. package/dist/recovery/types.js +0 -0
  111. package/dist/withSandbox.d.ts +4 -0
  112. package/dist/withSandbox.js +39 -0
  113. package/package.json +76 -3
@@ -0,0 +1,148 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, unlink } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { SandboxOperationError } from "../../errors.js";
5
+ import { parseSandboxRelativePath } from "../../files/paths.js";
6
+ import { sameIdentity } from "../hostPath.js";
7
+ import { closeAfter } from "./closing.js";
8
+ function identityName(fileStats) {
9
+ return `${fileStats.dev}-${fileStats.ino}`;
10
+ }
11
+ function missing(error) {
12
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
13
+ }
14
+ class DirectoryOwnership {
15
+ constructor(directory, token) {
16
+ this.directory = directory;
17
+ this.token = token;
18
+ }
19
+ handles = /* @__PURE__ */ new Set();
20
+ pending;
21
+ async open(path, flags, mode) {
22
+ const handle = await open(path, flags, mode);
23
+ this.handles.add(handle);
24
+ return handle;
25
+ }
26
+ async closeFile(handle) {
27
+ await handle.close();
28
+ this.handles.delete(handle);
29
+ }
30
+ async remember(path, fileStats, kind, file) {
31
+ const marker = join(this.directory, identityName(fileStats));
32
+ let handle;
33
+ try {
34
+ handle = await this.open(marker, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 384);
35
+ } catch (error) {
36
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
37
+ await this.inspect(path, fileStats, kind);
38
+ return;
39
+ }
40
+ await closeAfter(async () => {
41
+ const own = await handle.stat({ bigint: true });
42
+ this.pending = { path: marker, fileStats: own };
43
+ const record = {
44
+ token: this.token,
45
+ path,
46
+ kind,
47
+ device: String(fileStats.dev),
48
+ inode: String(fileStats.ino),
49
+ markerDevice: String(own.dev),
50
+ markerInode: String(own.ino),
51
+ ...file
52
+ };
53
+ await handle.writeFile(JSON.stringify(record));
54
+ this.pending = void 0;
55
+ }, () => this.closeFile(handle));
56
+ }
57
+ async inspect(path, fileStats, kind) {
58
+ const record = await this.lookup(fileStats, kind);
59
+ if (record.path !== path || record.kind !== kind || record.device !== String(fileStats.dev) || record.inode !== String(fileStats.ino)) {
60
+ throw new SandboxOperationError("output_limit", "Host filesystem path aliases or replaces another owned entry.");
61
+ }
62
+ return record;
63
+ }
64
+ async lookup(fileStats, kind) {
65
+ const record = await this.readMarker(join(this.directory, identityName(fileStats)));
66
+ if (record.kind !== kind || record.device !== String(fileStats.dev) || record.inode !== String(fileStats.ino)) {
67
+ throw new Error("Output inode does not match its retained ownership marker.");
68
+ }
69
+ return record;
70
+ }
71
+ async forget(fileStats) {
72
+ const path = join(this.directory, identityName(fileStats));
73
+ await this.readMarker(path);
74
+ await unlink(path);
75
+ }
76
+ async readMarker(path) {
77
+ const handle = await this.open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
78
+ return closeAfter(async () => {
79
+ const fileStats = await handle.stat({ bigint: true });
80
+ if (!fileStats.isFile() || fileStats.size < 1n || fileStats.size > 4096n) throw new Error("Unrecognized directory ownership marker.");
81
+ const bytes = new Uint8Array(Number(fileStats.size));
82
+ let offset = 0;
83
+ while (offset < bytes.byteLength) {
84
+ const { bytesRead } = await handle.read(bytes, offset, bytes.byteLength - offset, offset);
85
+ if (!bytesRead) throw new Error("Directory ownership marker is truncated.");
86
+ offset += bytesRead;
87
+ }
88
+ const record = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
89
+ if (!record || typeof record !== "object" || !("token" in record) || record.token !== this.token || !("path" in record) || typeof record.path !== "string" || !("kind" in record) || record.kind !== "file" && record.kind !== "directory" || !("device" in record) || typeof record.device !== "string" || !/^[0-9]+$/u.test(record.device) || !("inode" in record) || typeof record.inode !== "string" || !/^[0-9]+$/u.test(record.inode) || !("markerDevice" in record) || record.markerDevice !== String(fileStats.dev) || !("markerInode" in record) || record.markerInode !== String(fileStats.ino)) throw new Error("Unrecognized directory ownership marker.");
90
+ const allowed = record.kind === "file" ? ["token", "path", "kind", "device", "inode", "markerDevice", "markerInode", "byteLength", "executable"] : ["token", "path", "kind", "device", "inode", "markerDevice", "markerInode"];
91
+ if (Object.keys(record).length !== allowed.length || Object.keys(record).some((key) => !allowed.includes(key))) {
92
+ throw new Error("Ownership marker has unrecognized fields.");
93
+ }
94
+ parseSandboxRelativePath(record.path);
95
+ if (record.kind === "file") {
96
+ if (!("byteLength" in record) || typeof record.byteLength !== "number" || !Number.isSafeInteger(record.byteLength) || record.byteLength < 0 || !("executable" in record) || typeof record.executable !== "boolean") throw new Error("Invalid file ownership marker.");
97
+ return {
98
+ token: this.token,
99
+ path: record.path,
100
+ kind: "file",
101
+ device: record.device,
102
+ inode: record.inode,
103
+ markerDevice: String(fileStats.dev),
104
+ markerInode: String(fileStats.ino),
105
+ byteLength: record.byteLength,
106
+ executable: record.executable
107
+ };
108
+ }
109
+ return {
110
+ token: this.token,
111
+ path: record.path,
112
+ kind: "directory",
113
+ device: record.device,
114
+ inode: record.inode,
115
+ markerDevice: String(fileStats.dev),
116
+ markerInode: String(fileStats.ino)
117
+ };
118
+ }, () => this.closeFile(handle));
119
+ }
120
+ async closeHandles() {
121
+ let failure;
122
+ for (const handle of this.handles) {
123
+ try {
124
+ await this.closeFile(handle);
125
+ } catch (error) {
126
+ failure ??= { error };
127
+ }
128
+ }
129
+ if (failure) throw failure.error;
130
+ const pending = this.pending;
131
+ if (pending) {
132
+ let fileStats;
133
+ try {
134
+ fileStats = await lstat(pending.path, { bigint: true });
135
+ } catch (error) {
136
+ if (!missing(error)) throw error;
137
+ this.pending = void 0;
138
+ return;
139
+ }
140
+ if (!fileStats.isFile() || !sameIdentity(fileStats, pending.fileStats)) throw new Error("Partial ownership marker was replaced; it is preserved.");
141
+ await unlink(pending.path);
142
+ this.pending = void 0;
143
+ }
144
+ }
145
+ }
146
+ export {
147
+ DirectoryOwnership
148
+ };
@@ -0,0 +1,25 @@
1
+ import type { BigIntStats } from 'node:fs';
2
+ import type { SandboxTreeEntry } from '../../files/types.js';
3
+ export declare class DirectorySource implements AsyncIterable<SandboxTreeEntry> {
4
+ private readonly root;
5
+ private readonly rootMetadata;
6
+ private readonly chunkBytes;
7
+ private readonly controller;
8
+ private readonly signal;
9
+ private readonly directories;
10
+ private file;
11
+ private opening;
12
+ private pending;
13
+ private closing;
14
+ private phase;
15
+ private iterated;
16
+ constructor(root: string, rootMetadata: BigIntStats, chunkBytes: number, signal?: AbortSignal);
17
+ [Symbol.asyncIterator](): AsyncIterator<SandboxTreeEntry>;
18
+ private next;
19
+ private pull;
20
+ private openDirectory;
21
+ private verifyParents;
22
+ close(): Promise<void>;
23
+ private check;
24
+ }
25
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1,143 @@
1
+ import { lstat, opendir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { SandboxOperationError, SandboxStateError } from "../../errors.js";
4
+ import { parseSandboxRelativePath } from "../../files/paths.js";
5
+ import { verifyDirectoryIdentity } from "../hostPath.js";
6
+ import { DirectoryFile } from "./file.js";
7
+ class DirectorySource {
8
+ constructor(root, rootMetadata, chunkBytes, signal) {
9
+ this.root = root;
10
+ this.rootMetadata = rootMetadata;
11
+ this.chunkBytes = chunkBytes;
12
+ this.signal = signal ? AbortSignal.any([signal, this.controller.signal]) : this.controller.signal;
13
+ }
14
+ controller = new AbortController();
15
+ signal;
16
+ directories = [];
17
+ file;
18
+ opening;
19
+ pending;
20
+ closing;
21
+ phase = "ready";
22
+ iterated = false;
23
+ [Symbol.asyncIterator]() {
24
+ if (this.iterated || this.phase !== "ready") throw new SandboxStateError("Host directory sources admit one transfer iterator.");
25
+ this.iterated = true;
26
+ this.phase = "reading";
27
+ return { next: () => this.next(), return: async () => {
28
+ await this.close();
29
+ return { done: true, value: void 0 };
30
+ } };
31
+ }
32
+ next() {
33
+ if (this.phase !== "reading" || this.pending) return Promise.reject(new SandboxStateError("Host traversal requires one active iterator pull."));
34
+ const actual = this.pull();
35
+ this.pending = actual;
36
+ void actual.then(() => {
37
+ this.pending = void 0;
38
+ }, () => {
39
+ this.pending = void 0;
40
+ });
41
+ return actual;
42
+ }
43
+ async pull() {
44
+ this.check();
45
+ if (this.file) {
46
+ await this.file.close();
47
+ this.file = void 0;
48
+ }
49
+ if (!this.opening) this.opening = this.openDirectory(this.root, "", this.rootMetadata);
50
+ await this.opening;
51
+ this.check();
52
+ while (true) {
53
+ const current = this.directories.at(-1);
54
+ if (!current) return { done: true, value: void 0 };
55
+ await this.verifyParents();
56
+ this.check();
57
+ const entry = await current.directory.read();
58
+ this.check();
59
+ if (!entry) {
60
+ await current.directory.close();
61
+ this.directories.pop();
62
+ continue;
63
+ }
64
+ const relative = parseSandboxRelativePath(current.relative ? `${current.relative}/${entry.name}` : entry.name);
65
+ const path = join(current.path, entry.name);
66
+ const metadata = await lstat(path, { bigint: true });
67
+ this.check();
68
+ if (metadata.isDirectory()) {
69
+ await this.openDirectory(path, relative, metadata);
70
+ return { done: false, value: Object.freeze({ kind: "directory", path: relative }) };
71
+ }
72
+ if (!metadata.isFile() || metadata.size > BigInt(Number.MAX_SAFE_INTEGER)) {
73
+ throw new SandboxOperationError("input_limit", "Host imports require regular files and directories with representable lengths; links and special files are rejected.");
74
+ }
75
+ const file = new DirectoryFile(path, metadata, this.signal, this.chunkBytes, () => this.verifyParents());
76
+ this.file = file;
77
+ return { done: false, value: Object.freeze({
78
+ kind: "file",
79
+ path: relative,
80
+ byteLength: Number(metadata.size),
81
+ executable: (metadata.mode & 0o111n) !== 0n,
82
+ open: (input) => file.open(input)
83
+ }) };
84
+ }
85
+ }
86
+ async openDirectory(path, relative, metadata) {
87
+ this.check();
88
+ await verifyDirectoryIdentity(path, metadata);
89
+ this.check();
90
+ const directory = await opendir(path, { bufferSize: 32 });
91
+ this.directories.push({ path, relative, metadata, directory });
92
+ await verifyDirectoryIdentity(path, metadata);
93
+ this.check();
94
+ }
95
+ async verifyParents() {
96
+ this.check();
97
+ for (const directory of this.directories) {
98
+ await verifyDirectoryIdentity(directory.path, directory.metadata);
99
+ this.check();
100
+ }
101
+ }
102
+ close() {
103
+ if (this.phase === "closed") return Promise.resolve();
104
+ if (this.closing) return this.closing;
105
+ this.phase = "closing";
106
+ this.controller.abort(new SandboxStateError("Host directory source is closing."));
107
+ this.closing = (async () => {
108
+ await this.pending?.catch(() => {
109
+ });
110
+ const failures = [];
111
+ if (this.file) {
112
+ try {
113
+ await this.file.close();
114
+ this.file = void 0;
115
+ } catch (error) {
116
+ failures.push(error);
117
+ }
118
+ }
119
+ for (let index = this.directories.length - 1; index >= 0; index -= 1) {
120
+ const directory = this.directories[index];
121
+ if (!directory) continue;
122
+ try {
123
+ await directory.directory.close();
124
+ this.directories.splice(index, 1);
125
+ } catch (error) {
126
+ failures.push(error);
127
+ }
128
+ }
129
+ if (failures.length) throw new AggregateError(failures, "Host source handles retain cleanup ownership.");
130
+ this.phase = "closed";
131
+ })().finally(() => {
132
+ this.closing = void 0;
133
+ });
134
+ return this.closing;
135
+ }
136
+ check() {
137
+ this.signal.throwIfAborted();
138
+ if (this.phase !== "reading") throw new SandboxStateError("Host directory source admission is closed.");
139
+ }
140
+ }
141
+ export {
142
+ DirectorySource
143
+ };
@@ -0,0 +1,43 @@
1
+ import type { BigIntStats } from 'node:fs';
2
+ import type { SandboxExportEntry } from '../../files/types.js';
3
+ export declare class DirectoryStage {
4
+ readonly destination: string;
5
+ private readonly parent;
6
+ private readonly controller;
7
+ private readonly signal;
8
+ private readonly token;
9
+ private namespace;
10
+ private stage;
11
+ private markers;
12
+ private ownership;
13
+ private unregistered;
14
+ private active;
15
+ private closing;
16
+ private accepting;
17
+ private published;
18
+ private closed;
19
+ private readonly unverified;
20
+ private readonly directoryHandles;
21
+ readonly directory: string;
22
+ constructor(destination: string, parent: BigIntStats, signal?: AbortSignal);
23
+ get publishedDirectory(): string | undefined;
24
+ initialize(): Promise<void>;
25
+ consume(entry: SandboxExportEntry, input: Readonly<{
26
+ signal: AbortSignal;
27
+ }>): Promise<void>;
28
+ private write;
29
+ fence(reason: unknown): void;
30
+ publish(): Promise<void>;
31
+ close(): Promise<void>;
32
+ private walk;
33
+ private removeMarkers;
34
+ private verifyMarkers;
35
+ private openDirectory;
36
+ private closeDirectory;
37
+ private closeDirectories;
38
+ private verifyRoots;
39
+ private readyStage;
40
+ private readyOwnership;
41
+ private check;
42
+ }
43
+ //# sourceMappingURL=stage.d.ts.map
@@ -0,0 +1,302 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, mkdir, opendir, rename, rmdir, unlink } from "node:fs/promises";
3
+ import { randomUUID } from "node:crypto";
4
+ import { dirname, join } from "node:path";
5
+ import { SandboxOperationError, SandboxStateError } from "../../errors.js";
6
+ import { parseSandboxRelativePath } from "../../files/paths.js";
7
+ import { sameIdentity, verifyDirectoryIdentity } from "../hostPath.js";
8
+ import { DirectoryOwnership } from "./ownership.js";
9
+ import { closeAfter } from "./closing.js";
10
+ function missing(error) {
11
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
12
+ }
13
+ class DirectoryStage {
14
+ constructor(destination, parent, signal) {
15
+ this.destination = destination;
16
+ this.parent = parent;
17
+ this.signal = signal ? AbortSignal.any([signal, this.controller.signal]) : this.controller.signal;
18
+ this.directory = join(destination, "files");
19
+ }
20
+ controller = new AbortController();
21
+ signal;
22
+ token = randomUUID();
23
+ namespace;
24
+ stage;
25
+ markers;
26
+ ownership;
27
+ unregistered;
28
+ active;
29
+ closing;
30
+ accepting = true;
31
+ published = false;
32
+ closed = false;
33
+ unverified = /* @__PURE__ */ new Set();
34
+ directoryHandles = /* @__PURE__ */ new Set();
35
+ directory;
36
+ get publishedDirectory() {
37
+ return this.published ? this.directory : void 0;
38
+ }
39
+ async initialize() {
40
+ this.check();
41
+ await verifyDirectoryIdentity(dirname(this.destination), this.parent);
42
+ await mkdir(this.destination, { mode: 448 });
43
+ this.unverified.add(this.destination);
44
+ this.namespace = { path: this.destination, metadata: await lstat(this.destination, { bigint: true }) };
45
+ this.unverified.delete(this.destination);
46
+ this.check();
47
+ const stage = join(this.destination, `.staging-${this.token}`);
48
+ await mkdir(stage, { mode: 448 });
49
+ this.unverified.add(stage);
50
+ this.stage = { path: stage, metadata: await lstat(stage, { bigint: true }) };
51
+ this.unverified.delete(stage);
52
+ this.check();
53
+ const markers = join(this.destination, `.metadata-${this.token}`);
54
+ await mkdir(markers, { mode: 448 });
55
+ this.unverified.add(markers);
56
+ this.markers = { path: markers, metadata: await lstat(markers, { bigint: true }) };
57
+ this.unverified.delete(markers);
58
+ this.ownership = new DirectoryOwnership(markers, this.token);
59
+ this.check();
60
+ }
61
+ consume(entry, input) {
62
+ if (!this.accepting || this.active) return Promise.reject(new SandboxStateError("Directory sink is closed or already consuming an entry."));
63
+ const actual = Promise.resolve().then(() => this.write(entry, AbortSignal.any([input.signal, this.signal])));
64
+ this.active = actual;
65
+ void actual.then(() => {
66
+ this.active = void 0;
67
+ }, (error) => {
68
+ this.active = void 0;
69
+ this.fence(error);
70
+ });
71
+ return actual;
72
+ }
73
+ async write(entry, signal) {
74
+ this.check();
75
+ signal.throwIfAborted();
76
+ await this.verifyRoots();
77
+ this.check();
78
+ signal.throwIfAborted();
79
+ const relative = parseSandboxRelativePath(entry.path);
80
+ const stage = this.readyStage();
81
+ const ownership = this.readyOwnership();
82
+ const segments = relative.split("/");
83
+ const directories = entry.kind === "directory" ? segments : segments.slice(0, -1);
84
+ let parent = stage.path;
85
+ let logical = "";
86
+ for (const segment of directories) {
87
+ this.check();
88
+ signal.throwIfAborted();
89
+ logical = logical ? `${logical}/${segment}` : segment;
90
+ parent = join(parent, segment);
91
+ let created = false;
92
+ try {
93
+ await mkdir(parent, { mode: 448 });
94
+ created = true;
95
+ } catch (error) {
96
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
97
+ }
98
+ const metadata = await lstat(parent, { bigint: true });
99
+ if (!metadata.isDirectory()) throw new SandboxOperationError("output_limit", "Export directory was replaced by a link or non-directory.");
100
+ if (created) this.unregistered = { path: parent, metadata, kind: "directory" };
101
+ await ownership.remember(logical, metadata, "directory");
102
+ this.unregistered = void 0;
103
+ }
104
+ if (entry.kind === "directory") return;
105
+ this.check();
106
+ signal.throwIfAborted();
107
+ const path = join(stage.path, ...segments);
108
+ const handle = await ownership.open(
109
+ path,
110
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
111
+ entry.executable ? 448 : 384
112
+ );
113
+ await closeAfter(async () => {
114
+ const metadata = await handle.stat({ bigint: true });
115
+ this.unregistered = { path, metadata, kind: "file" };
116
+ await ownership.remember(relative, metadata, "file", { byteLength: entry.byteLength, executable: entry.executable });
117
+ this.unregistered = void 0;
118
+ let offset = 0;
119
+ for await (const chunk of entry.bytes) {
120
+ this.check();
121
+ signal.throwIfAborted();
122
+ if (!(chunk instanceof Uint8Array) || chunk.byteLength > entry.byteLength - offset) throw new Error("Export file body exceeds its declared length.");
123
+ let written = 0;
124
+ while (written < chunk.byteLength) {
125
+ const result = await handle.write(chunk, written, chunk.byteLength - written, offset);
126
+ if (!result.bytesWritten) throw new Error("Export file write made no progress.");
127
+ written += result.bytesWritten;
128
+ offset += result.bytesWritten;
129
+ this.check();
130
+ signal.throwIfAborted();
131
+ }
132
+ }
133
+ if (offset !== entry.byteLength) throw new Error("Export file body ended before its declared length.");
134
+ this.check();
135
+ signal.throwIfAborted();
136
+ }, () => ownership.closeFile(handle));
137
+ }
138
+ fence(reason) {
139
+ this.accepting = false;
140
+ if (!this.controller.signal.aborted) this.controller.abort(reason);
141
+ }
142
+ async publish() {
143
+ await this.active;
144
+ this.check();
145
+ await this.verifyRoots();
146
+ await this.readyOwnership().closeHandles();
147
+ await this.walk(this.readyStage().path, "", false);
148
+ await this.verifyMarkers();
149
+ this.check();
150
+ try {
151
+ await lstat(this.directory);
152
+ throw new Error("Published child already exists and is preserved.");
153
+ } catch (error) {
154
+ if (!missing(error)) throw error;
155
+ }
156
+ await rename(this.readyStage().path, this.directory);
157
+ this.published = true;
158
+ this.stage = void 0;
159
+ this.accepting = false;
160
+ }
161
+ close() {
162
+ if (this.closed) return Promise.resolve();
163
+ if (this.closing) return this.closing;
164
+ this.fence(new SandboxStateError("Directory sink is closing."));
165
+ this.closing = (async () => {
166
+ await this.active?.catch(() => {
167
+ });
168
+ await this.ownership?.closeHandles();
169
+ await this.closeDirectories();
170
+ if (this.unverified.size) throw new Error("An acquired output directory has no verified identity and is preserved.");
171
+ if (!this.namespace) {
172
+ this.closed = true;
173
+ return;
174
+ }
175
+ await this.verifyRoots();
176
+ if (this.unregistered) {
177
+ const entry = this.unregistered;
178
+ const current = await lstat(entry.path, { bigint: true });
179
+ if (!sameIdentity(current, entry.metadata)) throw new Error("Partially acquired output entry was replaced and is preserved.");
180
+ if (entry.kind === "file") await unlink(entry.path);
181
+ else await rmdir(entry.path);
182
+ this.unregistered = void 0;
183
+ }
184
+ if (this.stage) {
185
+ if (this.ownership) await this.walk(this.stage.path, "", true);
186
+ await rmdir(this.stage.path);
187
+ this.stage = void 0;
188
+ }
189
+ await this.removeMarkers();
190
+ if (!this.published) await rmdir(this.namespace.path);
191
+ this.closed = true;
192
+ })().finally(() => {
193
+ this.closing = void 0;
194
+ });
195
+ return this.closing;
196
+ }
197
+ async walk(path, relative, remove) {
198
+ const directory = await this.openDirectory(path);
199
+ await closeAfter(async () => {
200
+ while (true) {
201
+ const entry = await directory.read();
202
+ if (!entry) break;
203
+ const child = join(path, entry.name);
204
+ const metadata = await lstat(child, { bigint: true });
205
+ const kind = metadata.isDirectory() ? "directory" : metadata.isFile() ? "file" : void 0;
206
+ if (!kind) throw new Error("Unknown link or special output entry is preserved.");
207
+ const record = await this.readyOwnership().lookup(metadata, kind);
208
+ const separator = record.path.lastIndexOf("/");
209
+ const parent = separator < 0 ? "" : record.path.slice(0, separator);
210
+ const declared = await lstat(join(this.readyStage().path, ...record.path.split("/")), { bigint: true });
211
+ if (parent !== relative || !sameIdentity(declared, metadata) || kind === "file" && metadata.nlink !== 1n) {
212
+ throw new Error("An unexpected alias or relocated output entry is preserved.");
213
+ }
214
+ if (kind === "directory") await this.walk(child, record.path, remove);
215
+ else if (!remove && (metadata.size !== BigInt(record.byteLength ?? -1) || (metadata.mode & 0o111n) !== 0n !== record.executable)) {
216
+ throw new Error("Staged output file length or executable mode changed.");
217
+ }
218
+ if (remove) {
219
+ if (kind === "directory") await rmdir(child);
220
+ else await unlink(child);
221
+ await this.readyOwnership().forget(metadata);
222
+ }
223
+ }
224
+ }, () => this.closeDirectory(directory));
225
+ }
226
+ async removeMarkers() {
227
+ if (!this.markers) return;
228
+ const markers = this.markers;
229
+ const directory = await this.openDirectory(markers.path);
230
+ await closeAfter(async () => {
231
+ while (true) {
232
+ const entry = await directory.read();
233
+ if (!entry) break;
234
+ const path = join(markers.path, entry.name);
235
+ const record = await this.readyOwnership().readMarker(path);
236
+ if (entry.name !== `${record.device}-${record.inode}`) throw new Error("Unexpected control entry is preserved.");
237
+ await unlink(path);
238
+ }
239
+ }, () => this.closeDirectory(directory));
240
+ await rmdir(markers.path);
241
+ this.markers = void 0;
242
+ }
243
+ async verifyMarkers() {
244
+ if (!this.markers) throw new SandboxStateError("Directory ownership storage is not acquired.");
245
+ const markers = this.markers;
246
+ const directory = await this.openDirectory(markers.path);
247
+ await closeAfter(async () => {
248
+ while (true) {
249
+ this.check();
250
+ const entry = await directory.read();
251
+ if (!entry) break;
252
+ const record = await this.readyOwnership().readMarker(join(markers.path, entry.name));
253
+ if (entry.name !== `${record.device}-${record.inode}`) throw new Error("Unexpected control entry is preserved.");
254
+ const metadata = await lstat(join(this.readyStage().path, ...record.path.split("/")), { bigint: true });
255
+ if (String(metadata.dev) !== record.device || String(metadata.ino) !== record.inode || (record.kind === "directory" ? !metadata.isDirectory() : !metadata.isFile() || metadata.size !== BigInt(record.byteLength ?? -1) || metadata.nlink !== 1n || (metadata.mode & 0o111n) !== 0n !== record.executable)) {
256
+ throw new Error("An expected staged entry was changed or replaced before publication.");
257
+ }
258
+ }
259
+ }, () => this.closeDirectory(directory));
260
+ }
261
+ async openDirectory(path) {
262
+ const directory = await opendir(path, { bufferSize: 32 });
263
+ this.directoryHandles.add(directory);
264
+ return directory;
265
+ }
266
+ async closeDirectory(directory) {
267
+ await directory.close();
268
+ this.directoryHandles.delete(directory);
269
+ }
270
+ async closeDirectories() {
271
+ let failure;
272
+ for (const directory of this.directoryHandles) {
273
+ try {
274
+ await this.closeDirectory(directory);
275
+ } catch (error) {
276
+ failure ??= { error };
277
+ }
278
+ }
279
+ if (failure) throw failure.error;
280
+ }
281
+ async verifyRoots() {
282
+ await verifyDirectoryIdentity(dirname(this.destination), this.parent);
283
+ if (this.namespace) await verifyDirectoryIdentity(this.namespace.path, this.namespace.metadata);
284
+ if (this.stage) await verifyDirectoryIdentity(this.stage.path, this.stage.metadata);
285
+ if (this.markers) await verifyDirectoryIdentity(this.markers.path, this.markers.metadata);
286
+ }
287
+ readyStage() {
288
+ if (!this.stage) throw new SandboxStateError("Directory staging is not acquired.");
289
+ return this.stage;
290
+ }
291
+ readyOwnership() {
292
+ if (!this.ownership) throw new SandboxStateError("Directory ownership storage is not acquired.");
293
+ return this.ownership;
294
+ }
295
+ check() {
296
+ this.signal.throwIfAborted();
297
+ if (!this.accepting) throw new SandboxStateError("Directory sink admission is closed.");
298
+ }
299
+ }
300
+ export {
301
+ DirectoryStage
302
+ };
@@ -0,0 +1,23 @@
1
+ import type { SandboxTransferOptions, SandboxTransferSummary } from '../../files/types.js';
2
+ export interface SandboxDirectoryImportOptions extends SandboxTransferOptions {
3
+ /** Match a smaller explicitly configured sandbox buffer. Defaults to 64 KiB. */
4
+ readonly chunkBytes?: number;
5
+ }
6
+ export interface SandboxDirectoryExportResult {
7
+ /** Published child of the exclusively created caller-selected destination namespace. */
8
+ readonly directory: string;
9
+ readonly summary: SandboxTransferSummary;
10
+ }
11
+ export interface DirectoryCleanupOwner {
12
+ close(): Promise<void>;
13
+ }
14
+ export declare class SandboxDirectoryCleanupError extends Error {
15
+ readonly primaryError: unknown;
16
+ readonly cleanupError: unknown;
17
+ readonly directory: string;
18
+ private readonly owner;
19
+ readonly publishedDirectory?: string | undefined;
20
+ constructor(primaryError: unknown, cleanupError: unknown, directory: string, owner: DirectoryCleanupOwner, publishedDirectory?: string | undefined);
21
+ retryCleanup(): Promise<void>;
22
+ }
23
+ //# sourceMappingURL=types.d.ts.map