@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,339 @@
1
+ import { SandboxOperationError } from "../errors.js";
2
+ import { TransferBody } from "../files/bytes.js";
3
+ import { TransferIterator } from "../files/iterator.js";
4
+ import { createMetadataLedger, MetadataCleanupError } from "../files/metadata/index.js";
5
+ import { parseTreeMetadata, transferData } from "../files/parse.js";
6
+ import { parseSandboxRelativePath } from "../files/paths.js";
7
+ import { TransferCounters } from "../files/quotas.js";
8
+ class TransferDispatch {
9
+ constructor(pull, interrupt) {
10
+ this.pull = pull;
11
+ this.interrupt = interrupt;
12
+ }
13
+ claimed = false;
14
+ retired = false;
15
+ ended = false;
16
+ pending;
17
+ failure;
18
+ [Symbol.asyncIterator]() {
19
+ if (this.claimed || this.retired) throw this.fail("Tree sources are once-only and scoped to the transfer.");
20
+ this.claimed = true;
21
+ return { next: () => this.next(), return: async () => {
22
+ this.retired = true;
23
+ return { done: true, value: void 0 };
24
+ } };
25
+ }
26
+ fail(message) {
27
+ const error = new SandboxOperationError("driver_failure", message);
28
+ this.failure ??= { error };
29
+ this.interrupt(error);
30
+ return error;
31
+ }
32
+ next() {
33
+ if (this.pending || this.retired) return Promise.reject(this.fail("Concurrent or retired tree pulls are forbidden."));
34
+ if (this.ended) return Promise.resolve({ done: true, value: void 0 });
35
+ const actual = Promise.resolve().then(this.pull);
36
+ this.pending = actual;
37
+ void actual.then(
38
+ (result) => {
39
+ this.pending = void 0;
40
+ this.ended = result.done === true;
41
+ },
42
+ (error) => {
43
+ this.pending = void 0;
44
+ this.failure ??= { error };
45
+ }
46
+ );
47
+ return actual;
48
+ }
49
+ async finish() {
50
+ if (this.pending) this.fail("Backend returned with a tree pull still pending.");
51
+ await this.close();
52
+ if (this.failure) throw this.failure.error;
53
+ if (!this.ended) throw this.fail("Backend did not completely consume its tree source.");
54
+ }
55
+ async close() {
56
+ this.retired = true;
57
+ await this.pending?.catch(() => {
58
+ });
59
+ }
60
+ }
61
+ class TransferResources {
62
+ ledgers = [];
63
+ acquisitionCleanup;
64
+ source;
65
+ body;
66
+ entered;
67
+ dispatch;
68
+ clean = false;
69
+ closing;
70
+ closeFailed = false;
71
+ get cleanupFailed() {
72
+ return this.closeFailed;
73
+ }
74
+ close(retry = false) {
75
+ if (this.closing && (!retry || !this.closeFailed)) return this.closing;
76
+ this.closeFailed = false;
77
+ const actual = this.closeResources(retry);
78
+ this.closing = actual;
79
+ void actual.catch(() => {
80
+ this.closeFailed = true;
81
+ });
82
+ return actual;
83
+ }
84
+ async closeResources(retry) {
85
+ await this.entered?.catch(() => {
86
+ });
87
+ await this.dispatch?.close();
88
+ const results = await Promise.allSettled([
89
+ this.body?.close(retry),
90
+ this.source?.close(retry),
91
+ this.acquisitionCleanup?.close(retry),
92
+ ...this.ledgers.map((ledger2) => ledger2.close())
93
+ ]);
94
+ const failures = results.filter((result) => result.status === "rejected");
95
+ if (failures.length !== 0) throw new AggregateError(failures.map((result) => result.reason), "Transfer cleanup retains unfinished resources.");
96
+ this.clean = true;
97
+ }
98
+ async finishBody() {
99
+ const body = this.body;
100
+ if (!body) return;
101
+ body.assertComplete();
102
+ await body.close();
103
+ this.body = void 0;
104
+ }
105
+ }
106
+ function ownTransfer(controller, perform) {
107
+ const resources = new TransferResources();
108
+ const actual = Promise.resolve().then(async () => {
109
+ let primary;
110
+ let summary;
111
+ try {
112
+ controller.signal.throwIfAborted();
113
+ summary = await perform(resources);
114
+ } catch (error) {
115
+ primary = { error };
116
+ controller.abort(error);
117
+ }
118
+ try {
119
+ await resources.close();
120
+ } catch (error) {
121
+ if (primary && error instanceof AggregateError && error.errors.every((cause) => Object.is(cause, primary.error))) throw primary.error;
122
+ if (primary) throw new AggregateError([primary.error, error], "Transfer and its cleanup both failed.");
123
+ throw error;
124
+ }
125
+ if (primary) throw primary.error;
126
+ if (!summary) throw new SandboxOperationError("driver_failure", "Transfer returned no summary.");
127
+ return summary;
128
+ });
129
+ return { actual, get clean() {
130
+ return resources.clean;
131
+ }, async cleanup() {
132
+ const retry = resources.cleanupFailed;
133
+ await actual.catch(() => {
134
+ });
135
+ if (!resources.clean) await resources.close(retry);
136
+ } };
137
+ }
138
+ async function ledger(ready, options, resources) {
139
+ try {
140
+ const result = await createMetadataLedger({
141
+ openSpill: () => ready.openSpill({ signal: options.signal }),
142
+ batchBytes: options.buffers.metadataBatchBytes,
143
+ fanIn: options.buffers.metadataMergeFanIn,
144
+ signal: options.signal
145
+ });
146
+ resources.ledgers.push(result);
147
+ return result;
148
+ } catch (error) {
149
+ if (error instanceof MetadataCleanupError) resources.acquisitionCleanup = {
150
+ close: (retry = false) => retry ? error.retryCleanup() : Promise.reject(error.cleanupError)
151
+ };
152
+ throw error;
153
+ }
154
+ }
155
+ function importTree(ready, entries, selected) {
156
+ const controller = new AbortController();
157
+ const options = { ...selected, signal: AbortSignal.any([selected.signal, controller.signal]) };
158
+ return ownTransfer(controller, async (resources) => {
159
+ const metadata = await ledger(ready, options, resources);
160
+ const source = new TransferIterator(entries);
161
+ resources.source = source;
162
+ const counts = new TransferCounters(options.quotas, "import");
163
+ const admitted = new TransferDispatch(async () => {
164
+ options.signal.throwIfAborted();
165
+ await resources.finishBody();
166
+ const next = await source.next();
167
+ options.signal.throwIfAborted();
168
+ if (next.done) return { done: true, value: void 0 };
169
+ const entry = parseTreeMetadata(next.value, "open");
170
+ counts.admit(entry);
171
+ await metadata.append(entry);
172
+ if (entry.kind === "directory") return { done: false, value: entry };
173
+ const open = entry.body;
174
+ const body = new TransferBody(
175
+ entry.byteLength,
176
+ options.buffers.chunkBytes,
177
+ true,
178
+ options.signal,
179
+ () => Reflect.apply(open, void 0, [{ signal: options.signal }]),
180
+ (error) => controller.abort(error)
181
+ );
182
+ resources.body = body;
183
+ let opened = false;
184
+ return { done: false, value: Object.freeze({
185
+ kind: "file",
186
+ path: entry.path,
187
+ byteLength: entry.byteLength,
188
+ executable: entry.executable,
189
+ open() {
190
+ if (opened) return body.rejectReuse();
191
+ opened = true;
192
+ return body;
193
+ }
194
+ }) };
195
+ }, (error) => controller.abort(error));
196
+ resources.dispatch = admitted;
197
+ const summary = await ready.importTree(admitted, options);
198
+ await admitted.finish();
199
+ await resources.finishBody();
200
+ await metadata.finish();
201
+ options.signal.throwIfAborted();
202
+ return counts.verify(summary);
203
+ });
204
+ }
205
+ async function selection(ready, input, options, resources, interrupt) {
206
+ const data = transferData(input, ["kind", "paths", "path"]);
207
+ if (data.kind === "workspace" && Reflect.ownKeys(data).length === 1) return { selected: Object.freeze({ kind: "workspace" }) };
208
+ if (data.kind === "subtree" && Reflect.ownKeys(data).length === 2) {
209
+ const path = parseSandboxRelativePath(data.path);
210
+ new TransferCounters(options.quotas, "export").check("maxPaths", 1n);
211
+ return { selected: Object.freeze({ kind: "subtree", path }) };
212
+ }
213
+ if (data.kind !== "files" || Reflect.ownKeys(data).length !== 2) throw new SandboxOperationError("input_limit", "Export requires a workspace, exact-file list or one directory subtree.");
214
+ const expected = await ledger(ready, options, resources);
215
+ const source = new TransferIterator(data.paths);
216
+ resources.source = source;
217
+ const counts = new TransferCounters(options.quotas, "export");
218
+ let paths = 0n;
219
+ while (true) {
220
+ options.signal.throwIfAborted();
221
+ const next = await source.next();
222
+ if (next.done) break;
223
+ const path = parseSandboxRelativePath(next.value);
224
+ paths += 1n;
225
+ counts.check("maxPaths", paths);
226
+ await expected.append({ kind: "file", path });
227
+ }
228
+ await source.close();
229
+ resources.source = void 0;
230
+ await expected.finish();
231
+ const replay = new TransferDispatch(async () => {
232
+ options.signal.throwIfAborted();
233
+ const next = await cursor.next();
234
+ return next.done ? { done: true, value: void 0 } : { done: false, value: next.value.path };
235
+ }, interrupt);
236
+ const cursor = new TransferIterator(expected.records());
237
+ resources.dispatch = replay;
238
+ resources.source = cursor;
239
+ return { selected: Object.freeze({ kind: "files", paths: replay }), expected, complete: () => replay.finish() };
240
+ }
241
+ async function verifySelected(expected, observed) {
242
+ const left = new TransferIterator(expected.records());
243
+ const right = new TransferIterator(observed.records());
244
+ try {
245
+ while (true) {
246
+ const [wanted, actual] = await Promise.all([left.next(), right.next()]);
247
+ if (wanted.done && actual.done) return;
248
+ if (wanted.done || actual.done || wanted.value.path !== actual.value.path || actual.value.kind !== "file") {
249
+ throw new SandboxOperationError("driver_failure", "Backend did not export exactly the selected regular files.");
250
+ }
251
+ }
252
+ } finally {
253
+ await Promise.all([left.close(), right.close()]);
254
+ }
255
+ }
256
+ function exportTree(ready, input, consume, selectedOptions) {
257
+ const controller = new AbortController();
258
+ const options = { ...selectedOptions, signal: AbortSignal.any([selectedOptions.signal, controller.signal]) };
259
+ return ownTransfer(controller, async (resources) => {
260
+ if (typeof consume !== "function") throw new SandboxOperationError("input_limit", "Export requires an asynchronous entry consumer.");
261
+ const { selected, expected, complete } = await selection(ready, input, options, resources, (error) => controller.abort(error));
262
+ const metadata = await ledger(ready, options, resources);
263
+ const counts = new TransferCounters(options.quotas, "export");
264
+ let accepting = true;
265
+ let callback;
266
+ let failure;
267
+ let subtreeRootSeen = false;
268
+ const deliver = (raw) => {
269
+ if (!accepting || callback) {
270
+ const error = new SandboxOperationError("driver_failure", "Export callbacks must be awaited sequentially within the transfer.");
271
+ failure ??= { error };
272
+ return Promise.reject(error);
273
+ }
274
+ const actual = Promise.resolve().then(async () => {
275
+ options.signal.throwIfAborted();
276
+ const entry = parseTreeMetadata(raw, "bytes");
277
+ if (selected.kind === "subtree") {
278
+ if (entry.path === selected.path) {
279
+ if (entry.kind !== "directory") throw new SandboxOperationError("driver_failure", "The selected subtree root must be a directory.");
280
+ subtreeRootSeen = true;
281
+ } else if (!entry.path.startsWith(`${selected.path}/`)) {
282
+ throw new SandboxOperationError("driver_failure", "Backend exported an entry outside the selected subtree.");
283
+ }
284
+ }
285
+ counts.admit(entry);
286
+ await metadata.append(entry);
287
+ if (entry.kind === "directory") await consume(entry, { signal: options.signal });
288
+ else {
289
+ const body = new TransferBody(
290
+ entry.byteLength,
291
+ options.buffers.chunkBytes,
292
+ false,
293
+ options.signal,
294
+ () => entry.body,
295
+ (error) => controller.abort(error)
296
+ );
297
+ resources.body = body;
298
+ await consume(Object.freeze({ kind: "file", path: entry.path, byteLength: entry.byteLength, executable: entry.executable, bytes: body }), { signal: options.signal });
299
+ await resources.finishBody();
300
+ }
301
+ options.signal.throwIfAborted();
302
+ });
303
+ callback = actual;
304
+ resources.entered = actual;
305
+ void actual.then(() => {
306
+ callback = void 0;
307
+ }, (error) => {
308
+ callback = void 0;
309
+ failure ??= { error };
310
+ });
311
+ return actual;
312
+ };
313
+ let summary;
314
+ try {
315
+ summary = await ready.exportTree(selected, deliver, options);
316
+ } finally {
317
+ accepting = false;
318
+ }
319
+ if (callback) {
320
+ failure ??= { error: new SandboxOperationError("driver_failure", "Backend returned while its export consumer was still running.") };
321
+ controller.abort(failure.error);
322
+ }
323
+ await callback?.catch(() => {
324
+ });
325
+ if (failure) throw failure.error;
326
+ await complete?.();
327
+ await resources.source?.close();
328
+ resources.source = void 0;
329
+ await metadata.finish();
330
+ if (selected.kind === "subtree" && !subtreeRootSeen) throw new SandboxOperationError("driver_failure", "Backend omitted the selected subtree root directory.");
331
+ if (expected) await verifySelected(expected, metadata);
332
+ options.signal.throwIfAborted();
333
+ return counts.verify(summary);
334
+ });
335
+ }
336
+ export {
337
+ exportTree,
338
+ importTree
339
+ };
@@ -0,0 +1,2 @@
1
+ export declare function closeAfter<Value>(perform: () => Promise<Value>, close: () => Promise<void>): Promise<Value>;
2
+ //# sourceMappingURL=closing.d.ts.map
@@ -0,0 +1,19 @@
1
+ async function closeAfter(perform, close) {
2
+ let outcome;
3
+ try {
4
+ outcome = { kind: "returned", value: await perform() };
5
+ } catch (error) {
6
+ outcome = { kind: "threw", error };
7
+ }
8
+ try {
9
+ await close();
10
+ } catch (cleanupError) {
11
+ if (outcome.kind === "threw") throw new AggregateError([outcome.error, cleanupError], "Filesystem work and native handle closure both failed.", { cause: outcome.error });
12
+ throw cleanupError;
13
+ }
14
+ if (outcome.kind === "threw") throw outcome.error;
15
+ return outcome.value;
16
+ }
17
+ export {
18
+ closeAfter
19
+ };
@@ -0,0 +1,6 @@
1
+ import type { Sandbox } from '../../contracts.js';
2
+ import type { SandboxTreeSelection, SandboxTransferOptions } from '../../files/types.js';
3
+ import type { SandboxDirectoryExportResult } from './types.js';
4
+ /** Destination is a new exclusive namespace; the published tree is its returned `files` child. */
5
+ export declare function exportSandboxDirectory(sandbox: Sandbox, selection: SandboxTreeSelection, destination: string, options?: SandboxTransferOptions): Promise<SandboxDirectoryExportResult>;
6
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { dirname } from "node:path";
2
+ import { SandboxOperationError, SandboxUnsupportedError } from "../../errors.js";
3
+ import { parseTransferOptions } from "../../files/parse.js";
4
+ import { canonicalHostPath, verifyHostDirectory } from "../hostPath.js";
5
+ import { DirectoryStage } from "./stage.js";
6
+ import { capturedTransferOptions } from "./options.js";
7
+ import { SandboxDirectoryCleanupError } from "./types.js";
8
+ async function exportSandboxDirectory(sandbox, selection, destination, options) {
9
+ const path = canonicalHostPath(destination);
10
+ const parsed = parseTransferOptions(options);
11
+ const selected = capturedTransferOptions(parsed);
12
+ parsed.signal?.throwIfAborted();
13
+ if (parsed.deadlineAt !== void 0 && parsed.deadlineAt <= Date.now()) throw new SandboxOperationError("deadline_exceeded", "Directory export deadline elapsed before entry.");
14
+ if (process.platform === "win32") throw new SandboxUnsupportedError("Directory publication requires the qualified POSIX filesystem profile.");
15
+ const parent = await verifyHostDirectory(dirname(path), parsed.signal);
16
+ const stage = new DirectoryStage(path, parent, parsed.signal);
17
+ let outcome;
18
+ try {
19
+ await stage.initialize();
20
+ const summary = await sandbox.exportTree(selection, (entry, input) => stage.consume(entry, input), selected);
21
+ await stage.publish();
22
+ outcome = { kind: "returned", summary };
23
+ } catch (error) {
24
+ stage.fence(error);
25
+ outcome = { kind: "threw", error };
26
+ }
27
+ try {
28
+ await stage.close();
29
+ } catch (cleanupError) {
30
+ throw new SandboxDirectoryCleanupError(
31
+ outcome.kind === "threw" ? outcome.error : void 0,
32
+ cleanupError,
33
+ path,
34
+ stage,
35
+ stage.publishedDirectory
36
+ );
37
+ }
38
+ if (outcome.kind === "threw") throw outcome.error;
39
+ return Object.freeze({ directory: stage.directory, summary: outcome.summary });
40
+ }
41
+ export {
42
+ exportSandboxDirectory
43
+ };
@@ -0,0 +1,26 @@
1
+ import type { BigIntStats } from 'node:fs';
2
+ export declare class DirectoryFile implements AsyncIterable<Uint8Array> {
3
+ private readonly path;
4
+ private readonly fileStats;
5
+ private readonly ownerSignal;
6
+ private readonly verifyParents;
7
+ private file;
8
+ private pending;
9
+ private closing;
10
+ private phase;
11
+ private signal;
12
+ private opened;
13
+ private iterated;
14
+ private done;
15
+ private position;
16
+ private readonly buffer;
17
+ constructor(path: string, fileStats: BigIntStats, ownerSignal: AbortSignal, chunkBytes: number, verifyParents: () => Promise<void>);
18
+ open(input: Readonly<{
19
+ signal: AbortSignal;
20
+ }>): AsyncIterable<Uint8Array>;
21
+ [Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
22
+ private next;
23
+ close(): Promise<void>;
24
+ private check;
25
+ }
26
+ //# sourceMappingURL=file.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open } from "node:fs/promises";
3
+ import { SandboxOperationError, SandboxStateError } from "../../errors.js";
4
+ import { verifyRegularIdentity } from "../hostPath.js";
5
+ class DirectoryFile {
6
+ constructor(path, fileStats, ownerSignal, chunkBytes, verifyParents) {
7
+ this.path = path;
8
+ this.fileStats = fileStats;
9
+ this.ownerSignal = ownerSignal;
10
+ this.verifyParents = verifyParents;
11
+ this.buffer = new Uint8Array(chunkBytes);
12
+ }
13
+ file;
14
+ pending;
15
+ closing;
16
+ phase = "ready";
17
+ signal;
18
+ opened = false;
19
+ iterated = false;
20
+ done = false;
21
+ position = 0;
22
+ buffer;
23
+ open(input) {
24
+ if (this.opened || this.phase !== "ready") throw new SandboxStateError("Host input files are once-only and transfer scoped.");
25
+ this.ownerSignal.throwIfAborted();
26
+ this.signal = AbortSignal.any([this.ownerSignal, input.signal]);
27
+ this.opened = true;
28
+ this.phase = "reading";
29
+ return this;
30
+ }
31
+ [Symbol.asyncIterator]() {
32
+ if (this.iterated || !this.opened) throw new SandboxStateError("Host input bodies admit one iterator after opening.");
33
+ this.iterated = true;
34
+ return { next: () => this.next(), return: async () => {
35
+ await this.close();
36
+ return { done: true, value: void 0 };
37
+ } };
38
+ }
39
+ next() {
40
+ if (this.phase !== "reading" || this.pending) return Promise.reject(new SandboxStateError("Host file reads require one active reader."));
41
+ if (this.done) return Promise.resolve({ done: true, value: void 0 });
42
+ const actual = (async () => {
43
+ this.check();
44
+ if (!this.file) {
45
+ await this.verifyParents();
46
+ this.check();
47
+ this.file = await open(this.path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
48
+ verifyRegularIdentity(await this.file.stat({ bigint: true }), this.fileStats);
49
+ await this.verifyParents();
50
+ verifyRegularIdentity(await lstat(this.path, { bigint: true }), this.fileStats);
51
+ this.check();
52
+ }
53
+ const length = Number(this.fileStats.size);
54
+ if (this.position === length) {
55
+ const { bytesRead: bytesRead2 } = await this.file.read(this.buffer, 0, 1, this.position);
56
+ this.check();
57
+ if (bytesRead2 !== 0) throw new SandboxOperationError("input_limit", "Host input grew beyond its declared length.");
58
+ verifyRegularIdentity(await this.file.stat({ bigint: true }), this.fileStats);
59
+ await this.verifyParents();
60
+ verifyRegularIdentity(await lstat(this.path, { bigint: true }), this.fileStats);
61
+ this.check();
62
+ this.done = true;
63
+ return { done: true, value: void 0 };
64
+ }
65
+ const { bytesRead } = await this.file.read(this.buffer, 0, Math.min(this.buffer.byteLength, length - this.position), this.position);
66
+ this.check();
67
+ if (bytesRead === 0) throw new SandboxOperationError("input_limit", "Host input ended before its declared length.");
68
+ this.position += bytesRead;
69
+ return { done: false, value: this.buffer.subarray(0, bytesRead) };
70
+ })();
71
+ this.pending = actual;
72
+ void actual.then(() => {
73
+ this.pending = void 0;
74
+ }, () => {
75
+ this.pending = void 0;
76
+ });
77
+ return actual;
78
+ }
79
+ close() {
80
+ if (this.phase === "closed") return Promise.resolve();
81
+ if (this.closing) return this.closing;
82
+ this.phase = "closing";
83
+ this.closing = (async () => {
84
+ await this.pending?.catch(() => {
85
+ });
86
+ if (this.file) {
87
+ await this.file.close();
88
+ this.file = void 0;
89
+ }
90
+ this.phase = "closed";
91
+ })().finally(() => {
92
+ this.closing = void 0;
93
+ });
94
+ return this.closing;
95
+ }
96
+ check() {
97
+ this.ownerSignal.throwIfAborted();
98
+ this.signal?.throwIfAborted();
99
+ if (this.phase !== "reading") throw new SandboxStateError("Host file admission is closed.");
100
+ }
101
+ }
102
+ export {
103
+ DirectoryFile
104
+ };
@@ -0,0 +1,6 @@
1
+ import type { Sandbox } from '../../contracts.js';
2
+ import type { SandboxTransferSummary } from '../../files/types.js';
3
+ import type { SandboxDirectoryImportOptions } from './types.js';
4
+ /** The caller keeps the hierarchy stable; descriptor checks detect changes but do not create a snapshot. */
5
+ export declare function importSandboxDirectory(sandbox: Sandbox, root: string, input?: SandboxDirectoryImportOptions): Promise<SandboxTransferSummary>;
6
+ //# sourceMappingURL=import.d.ts.map
@@ -0,0 +1,38 @@
1
+ import { SandboxUnsupportedError } from "../../errors.js";
2
+ import { invalidInput } from "../../internal/data.js";
3
+ import { parseTransferOptions, transferData } from "../../files/parse.js";
4
+ import { SANDBOX_LIMITS } from "../../policy/limits.js";
5
+ import { canonicalHostPath, verifyHostDirectory } from "../hostPath.js";
6
+ import { DirectorySource } from "./source.js";
7
+ import { capturedTransferOptions } from "./options.js";
8
+ import { SandboxDirectoryCleanupError } from "./types.js";
9
+ async function importSandboxDirectory(sandbox, root, input) {
10
+ const path = canonicalHostPath(root);
11
+ const data = input === void 0 ? {} : transferData(input, ["signal", "deadlineAt", "quotas", "chunkBytes"]);
12
+ const { chunkBytes = SANDBOX_LIMITS.maxTransferChunkBytes, ...transfer } = data;
13
+ if (typeof chunkBytes !== "number" || !Number.isSafeInteger(chunkBytes) || chunkBytes < 1 || chunkBytes > SANDBOX_LIMITS.maxTransferChunkBytes) {
14
+ invalidInput("Host file chunkBytes must be between one byte and 64 KiB");
15
+ }
16
+ const options = parseTransferOptions(transfer);
17
+ const selected = capturedTransferOptions(options);
18
+ if (process.platform === "win32") throw new SandboxUnsupportedError("Directory imports require the qualified POSIX filesystem profile.");
19
+ options.signal?.throwIfAborted();
20
+ const metadata = await verifyHostDirectory(path, options.signal);
21
+ const source = new DirectorySource(path, metadata, chunkBytes, options.signal);
22
+ let outcome;
23
+ try {
24
+ outcome = { kind: "returned", summary: await sandbox.importTree(source, selected) };
25
+ } catch (error) {
26
+ outcome = { kind: "threw", error };
27
+ }
28
+ try {
29
+ await source.close();
30
+ } catch (cleanupError) {
31
+ throw new SandboxDirectoryCleanupError(outcome.kind === "threw" ? outcome.error : void 0, cleanupError, path, source);
32
+ }
33
+ if (outcome.kind === "threw") throw outcome.error;
34
+ return outcome.summary;
35
+ }
36
+ export {
37
+ importSandboxDirectory
38
+ };
@@ -0,0 +1,4 @@
1
+ import type { ParsedTransferOptions } from '../../files/parse.js';
2
+ import type { SandboxTransferOptions } from '../../files/types.js';
3
+ export declare function capturedTransferOptions(options: ParsedTransferOptions): SandboxTransferOptions;
4
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1,10 @@
1
+ function capturedTransferOptions(options) {
2
+ return Object.freeze({
3
+ quotas: options.quotas,
4
+ ...options.signal === void 0 ? {} : { signal: options.signal },
5
+ ...options.deadlineAt === void 0 ? {} : { deadlineAt: new Date(options.deadlineAt).toISOString() }
6
+ });
7
+ }
8
+ export {
9
+ capturedTransferOptions
10
+ };
@@ -0,0 +1,33 @@
1
+ import type { BigIntStats } from 'node:fs';
2
+ import type { FileHandle } from 'node:fs/promises';
3
+ interface OwnershipRecord {
4
+ readonly token: string;
5
+ readonly path: string;
6
+ readonly kind: 'file' | 'directory';
7
+ readonly device: string;
8
+ readonly inode: string;
9
+ readonly markerDevice: string;
10
+ readonly markerInode: string;
11
+ readonly byteLength?: number;
12
+ readonly executable?: boolean;
13
+ }
14
+ export declare class DirectoryOwnership {
15
+ private readonly directory;
16
+ private readonly token;
17
+ private readonly handles;
18
+ private pending;
19
+ constructor(directory: string, token: string);
20
+ open(path: string, flags: number, mode?: number): Promise<FileHandle>;
21
+ closeFile(handle: FileHandle): Promise<void>;
22
+ remember(path: string, fileStats: BigIntStats, kind: 'file' | 'directory', file?: Readonly<{
23
+ byteLength: number;
24
+ executable: boolean;
25
+ }>): Promise<void>;
26
+ inspect(path: string, fileStats: BigIntStats, kind: 'file' | 'directory'): Promise<OwnershipRecord>;
27
+ lookup(fileStats: BigIntStats, kind: 'file' | 'directory'): Promise<OwnershipRecord>;
28
+ forget(fileStats: BigIntStats): Promise<void>;
29
+ readMarker(path: string): Promise<OwnershipRecord>;
30
+ closeHandles(): Promise<void>;
31
+ }
32
+ export {};
33
+ //# sourceMappingURL=ownership.d.ts.map