@fastgpt-sdk/sandbox-adapter 0.0.35 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/BaseSandboxAdapter.d.ts +6 -0
- package/dist/index.cjs +196 -61
- package/dist/index.js +196 -61
- package/dist/polyfill/CommandPolyfillService.d.ts +35 -3
- package/dist/types/execution.d.ts +10 -0
- package/dist/utils/outputBuffer.d.ts +34 -0
- package/package.json +1 -1
|
@@ -61,6 +61,12 @@ export declare abstract class BaseSandboxAdapter implements ISandbox {
|
|
|
61
61
|
listDirectory(path: string): Promise<DirectoryEntry[]>;
|
|
62
62
|
readFileStream(path: string): AsyncIterable<Uint8Array>;
|
|
63
63
|
writeFileStream(path: string, stream: ReadableStream<Uint8Array>): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Stream a ReadableStream into a file via a temporary file that is
|
|
66
|
+
* atomically renamed on success. If the stream fails mid-write, the
|
|
67
|
+
* original file is left untouched and the temp file is cleaned up.
|
|
68
|
+
*/
|
|
69
|
+
private writeStreamToFile;
|
|
64
70
|
getFileInfo(paths: string[]): Promise<Map<string, FileInfo>>;
|
|
65
71
|
setPermissions(entries: PermissionEntry[]): Promise<void>;
|
|
66
72
|
search(pattern: string, path?: string): Promise<SearchResult[]>;
|
package/dist/index.cjs
CHANGED
|
@@ -45023,13 +45023,34 @@ class CommandPolyfillService {
|
|
|
45023
45023
|
constructor(executor) {
|
|
45024
45024
|
this.executor = executor;
|
|
45025
45025
|
}
|
|
45026
|
+
static READ_CHUNK_SIZE = 256 * 1024;
|
|
45026
45027
|
async readFile(path) {
|
|
45027
45028
|
try {
|
|
45028
|
-
const
|
|
45029
|
-
if (
|
|
45030
|
-
|
|
45029
|
+
const size = await this.statSize(path);
|
|
45030
|
+
if (size === undefined) {
|
|
45031
|
+
const result = await this.executor.execute(`cat "${this.escapePath(path)}" | base64 -w 0`);
|
|
45032
|
+
if (result.exitCode !== 0) {
|
|
45033
|
+
throw this.createFileError(path, result.stderr);
|
|
45034
|
+
}
|
|
45035
|
+
return base64ToBytes(result.stdout);
|
|
45036
|
+
}
|
|
45037
|
+
if (size === 0)
|
|
45038
|
+
return new Uint8Array;
|
|
45039
|
+
const chunks = [];
|
|
45040
|
+
let totalLength = 0;
|
|
45041
|
+
for (let offset = 0;offset < size; offset += CommandPolyfillService.READ_CHUNK_SIZE) {
|
|
45042
|
+
const end = Math.min(offset + CommandPolyfillService.READ_CHUNK_SIZE, size);
|
|
45043
|
+
const chunk = await this.readFileRange(path, offset, end);
|
|
45044
|
+
chunks.push(chunk);
|
|
45045
|
+
totalLength += chunk.length;
|
|
45031
45046
|
}
|
|
45032
|
-
|
|
45047
|
+
const combined = new Uint8Array(totalLength);
|
|
45048
|
+
let writeOffset = 0;
|
|
45049
|
+
for (const chunk of chunks) {
|
|
45050
|
+
combined.set(chunk, writeOffset);
|
|
45051
|
+
writeOffset += chunk.length;
|
|
45052
|
+
}
|
|
45053
|
+
return combined;
|
|
45033
45054
|
} catch (error) {
|
|
45034
45055
|
if (error instanceof FileOperationError) {
|
|
45035
45056
|
throw error;
|
|
@@ -45040,9 +45061,18 @@ class CommandPolyfillService {
|
|
|
45040
45061
|
throw error;
|
|
45041
45062
|
}
|
|
45042
45063
|
}
|
|
45064
|
+
async statSize(path) {
|
|
45065
|
+
const result = await this.executor.execute(`stat -c '%s' "${this.escapePath(path)}" 2>/dev/null || stat -f '%z' "${this.escapePath(path)}" 2>/dev/null || echo STAT_FAILED`);
|
|
45066
|
+
const stdout = result.stdout.trim();
|
|
45067
|
+
if (!stdout || stdout.includes("STAT_FAILED"))
|
|
45068
|
+
return;
|
|
45069
|
+
const parsed = Number.parseInt(stdout, 10);
|
|
45070
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
45071
|
+
}
|
|
45043
45072
|
async readFileRange(path, start, end) {
|
|
45044
|
-
const
|
|
45045
|
-
const
|
|
45073
|
+
const escaped = this.escapePath(path);
|
|
45074
|
+
const tailPos = start + 1;
|
|
45075
|
+
const cmd = end !== undefined ? `tail -c +${tailPos} "${escaped}" | head -c ${end - start} | base64 -w 0` : `tail -c +${tailPos} "${escaped}" | base64 -w 0`;
|
|
45046
45076
|
const result = await this.executor.execute(cmd);
|
|
45047
45077
|
if (result.exitCode !== 0) {
|
|
45048
45078
|
throw this.createFileError(path, result.stderr);
|
|
@@ -45050,10 +45080,25 @@ class CommandPolyfillService {
|
|
|
45050
45080
|
return base64ToBytes(result.stdout);
|
|
45051
45081
|
}
|
|
45052
45082
|
async writeFile(path, data) {
|
|
45083
|
+
await this.appendBytes(path, data, { truncate: true });
|
|
45084
|
+
return data.length;
|
|
45085
|
+
}
|
|
45086
|
+
async appendBytes(path, data, options) {
|
|
45087
|
+
if (options?.truncate) {
|
|
45088
|
+
await this.createParentDirectory(path);
|
|
45089
|
+
}
|
|
45090
|
+
if (data.length === 0) {
|
|
45091
|
+
if (options?.truncate) {
|
|
45092
|
+
const result = await this.executor.execute(`: > "${this.escapePath(path)}"`);
|
|
45093
|
+
if (result.exitCode !== 0) {
|
|
45094
|
+
throw this.createFileError(path, result.stderr);
|
|
45095
|
+
}
|
|
45096
|
+
}
|
|
45097
|
+
return;
|
|
45098
|
+
}
|
|
45053
45099
|
const base64 = bytesToBase64(data);
|
|
45054
45100
|
const chunkSize = 1024;
|
|
45055
|
-
|
|
45056
|
-
let first = true;
|
|
45101
|
+
let first = Boolean(options?.truncate);
|
|
45057
45102
|
for (let i = 0;i < base64.length; i += chunkSize) {
|
|
45058
45103
|
const chunk = base64.slice(i, i + chunkSize);
|
|
45059
45104
|
const redirect = first ? ">" : ">>";
|
|
@@ -45063,7 +45108,6 @@ class CommandPolyfillService {
|
|
|
45063
45108
|
}
|
|
45064
45109
|
first = false;
|
|
45065
45110
|
}
|
|
45066
|
-
return data.length;
|
|
45067
45111
|
}
|
|
45068
45112
|
async writeTextFile(path, content) {
|
|
45069
45113
|
await this.createParentDirectory(path);
|
|
@@ -45406,23 +45450,7 @@ class BaseSandboxAdapter {
|
|
|
45406
45450
|
const arrayBuffer = await entry.data.arrayBuffer();
|
|
45407
45451
|
bytesWritten = await polyfillService.writeFile(entry.path, new Uint8Array(arrayBuffer));
|
|
45408
45452
|
} else {
|
|
45409
|
-
|
|
45410
|
-
const reader = entry.data.getReader();
|
|
45411
|
-
while (true) {
|
|
45412
|
-
const { done, value } = await reader.read();
|
|
45413
|
-
if (done) {
|
|
45414
|
-
break;
|
|
45415
|
-
}
|
|
45416
|
-
chunks.push(value);
|
|
45417
|
-
}
|
|
45418
|
-
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
45419
|
-
const combined = new Uint8Array(totalLength);
|
|
45420
|
-
let offset = 0;
|
|
45421
|
-
for (const chunk of chunks) {
|
|
45422
|
-
combined.set(chunk, offset);
|
|
45423
|
-
offset += chunk.length;
|
|
45424
|
-
}
|
|
45425
|
-
bytesWritten = await polyfillService.writeFile(entry.path, combined);
|
|
45453
|
+
bytesWritten = await this.writeStreamToFile(polyfillService, entry.path, entry.data);
|
|
45426
45454
|
}
|
|
45427
45455
|
results.push({ path: entry.path, bytesWritten, error: null });
|
|
45428
45456
|
} catch (error) {
|
|
@@ -45497,23 +45525,36 @@ class BaseSandboxAdapter {
|
|
|
45497
45525
|
}
|
|
45498
45526
|
}
|
|
45499
45527
|
async writeFileStream(path, stream) {
|
|
45528
|
+
const polyfillService = this.requirePolyfillService("writeFileStream", "File stream write not supported by this provider");
|
|
45529
|
+
await this.writeStreamToFile(polyfillService, this.normalizePath(path), stream);
|
|
45530
|
+
}
|
|
45531
|
+
async writeStreamToFile(polyfillService, path, stream) {
|
|
45532
|
+
const tmpPath = `${path}.tmp.${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
45500
45533
|
const reader = stream.getReader();
|
|
45501
|
-
|
|
45502
|
-
|
|
45503
|
-
|
|
45504
|
-
|
|
45505
|
-
|
|
45534
|
+
let totalBytes = 0;
|
|
45535
|
+
let first = true;
|
|
45536
|
+
try {
|
|
45537
|
+
while (true) {
|
|
45538
|
+
const { done, value } = await reader.read();
|
|
45539
|
+
if (done)
|
|
45540
|
+
break;
|
|
45541
|
+
if (!value || value.length === 0)
|
|
45542
|
+
continue;
|
|
45543
|
+
await polyfillService.appendBytes(tmpPath, value, first ? { truncate: true } : undefined);
|
|
45544
|
+
totalBytes += value.length;
|
|
45545
|
+
first = false;
|
|
45506
45546
|
}
|
|
45507
|
-
|
|
45508
|
-
|
|
45509
|
-
|
|
45510
|
-
|
|
45511
|
-
|
|
45512
|
-
|
|
45513
|
-
|
|
45514
|
-
|
|
45547
|
+
if (first) {
|
|
45548
|
+
await polyfillService.appendBytes(tmpPath, new Uint8Array, { truncate: true });
|
|
45549
|
+
}
|
|
45550
|
+
await polyfillService.moveFiles([{ source: tmpPath, destination: path }]);
|
|
45551
|
+
} catch (error) {
|
|
45552
|
+
await polyfillService.deleteFiles([tmpPath]).catch(() => {});
|
|
45553
|
+
throw error;
|
|
45554
|
+
} finally {
|
|
45555
|
+
reader.releaseLock();
|
|
45515
45556
|
}
|
|
45516
|
-
|
|
45557
|
+
return totalBytes;
|
|
45517
45558
|
}
|
|
45518
45559
|
async getFileInfo(paths) {
|
|
45519
45560
|
const polyfillService = this.requirePolyfillService("getFileInfo", "File info not supported by this provider");
|
|
@@ -47909,7 +47950,79 @@ var Sandbox = class _Sandbox {
|
|
|
47909
47950
|
}
|
|
47910
47951
|
};
|
|
47911
47952
|
|
|
47953
|
+
// src/utils/outputBuffer.ts
|
|
47954
|
+
class BoundedOutputBuffer {
|
|
47955
|
+
maxBytes;
|
|
47956
|
+
chunks = [];
|
|
47957
|
+
currentBytes = 0;
|
|
47958
|
+
_totalBytes = 0;
|
|
47959
|
+
_truncated = false;
|
|
47960
|
+
separatorBuf;
|
|
47961
|
+
constructor(maxBytes, separator) {
|
|
47962
|
+
this.maxBytes = maxBytes;
|
|
47963
|
+
if (!(maxBytes > 0)) {
|
|
47964
|
+
throw new Error(`BoundedOutputBuffer: maxBytes must be > 0 (got ${maxBytes})`);
|
|
47965
|
+
}
|
|
47966
|
+
if (separator) {
|
|
47967
|
+
this.separatorBuf = Buffer.from(separator, "utf8");
|
|
47968
|
+
}
|
|
47969
|
+
}
|
|
47970
|
+
append(text) {
|
|
47971
|
+
if (!text)
|
|
47972
|
+
return;
|
|
47973
|
+
const needsSep = this.separatorBuf && this.chunks.length > 0;
|
|
47974
|
+
const textBuf = Buffer.from(text, "utf8");
|
|
47975
|
+
const sepLen = needsSep ? this.separatorBuf.length : 0;
|
|
47976
|
+
this._totalBytes += textBuf.length + sepLen;
|
|
47977
|
+
if (textBuf.length + sepLen > this.maxBytes) {
|
|
47978
|
+
this._truncated = true;
|
|
47979
|
+
const keep = Math.min(textBuf.length, this.maxBytes);
|
|
47980
|
+
this.chunks = [
|
|
47981
|
+
keep < textBuf.length ? Buffer.from(textBuf.subarray(textBuf.length - keep)) : textBuf
|
|
47982
|
+
];
|
|
47983
|
+
this.currentBytes = keep;
|
|
47984
|
+
return;
|
|
47985
|
+
}
|
|
47986
|
+
const chunk = needsSep ? Buffer.concat([this.separatorBuf, textBuf]) : textBuf;
|
|
47987
|
+
this.chunks.push(chunk);
|
|
47988
|
+
this.currentBytes += chunk.length;
|
|
47989
|
+
if (this.currentBytes <= this.maxBytes)
|
|
47990
|
+
return;
|
|
47991
|
+
this._truncated = true;
|
|
47992
|
+
while (this.chunks.length > 1 && this.currentBytes - this.chunks[0].length >= this.maxBytes) {
|
|
47993
|
+
this.currentBytes -= this.chunks[0].length;
|
|
47994
|
+
this.chunks.shift();
|
|
47995
|
+
}
|
|
47996
|
+
if (this.currentBytes > this.maxBytes && this.chunks.length > 0) {
|
|
47997
|
+
const overflow = this.currentBytes - this.maxBytes;
|
|
47998
|
+
this.chunks[0] = this.chunks[0].subarray(overflow);
|
|
47999
|
+
this.currentBytes -= overflow;
|
|
48000
|
+
}
|
|
48001
|
+
if (this.separatorBuf && this.chunks.length > 0) {
|
|
48002
|
+
const first = this.chunks[0];
|
|
48003
|
+
const sep = this.separatorBuf;
|
|
48004
|
+
if (first.subarray(0, sep.length).equals(sep)) {
|
|
48005
|
+
this.chunks[0] = first.subarray(sep.length);
|
|
48006
|
+
this.currentBytes -= sep.length;
|
|
48007
|
+
}
|
|
48008
|
+
}
|
|
48009
|
+
}
|
|
48010
|
+
get totalBytes() {
|
|
48011
|
+
return this._totalBytes;
|
|
48012
|
+
}
|
|
48013
|
+
get truncated() {
|
|
48014
|
+
return this._truncated;
|
|
48015
|
+
}
|
|
48016
|
+
toString() {
|
|
48017
|
+
if (this.chunks.length === 0)
|
|
48018
|
+
return "";
|
|
48019
|
+
return Buffer.concat(this.chunks, this.currentBytes).toString("utf8");
|
|
48020
|
+
}
|
|
48021
|
+
}
|
|
48022
|
+
|
|
47912
48023
|
// src/adapters/OpenSandboxAdapter/index.ts
|
|
48024
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
48025
|
+
|
|
47913
48026
|
class OpenSandboxAdapter extends BaseSandboxAdapter {
|
|
47914
48027
|
connectionConfig;
|
|
47915
48028
|
createConfig;
|
|
@@ -48293,21 +48406,30 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
|
|
|
48293
48406
|
return results;
|
|
48294
48407
|
}
|
|
48295
48408
|
async execute(command, options) {
|
|
48409
|
+
const maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
48410
|
+
const stdoutBuf = new BoundedOutputBuffer(maxBytes, `
|
|
48411
|
+
`);
|
|
48412
|
+
const stderrBuf = new BoundedOutputBuffer(maxBytes, `
|
|
48413
|
+
`);
|
|
48296
48414
|
try {
|
|
48297
48415
|
const execution = await this.sandbox.commands.run(command, {
|
|
48298
48416
|
workingDirectory: this.normalizePath(options?.workingDirectory),
|
|
48299
48417
|
background: options?.background
|
|
48418
|
+
}, {
|
|
48419
|
+
onStdout: (msg) => {
|
|
48420
|
+
stdoutBuf.append(msg.text);
|
|
48421
|
+
},
|
|
48422
|
+
onStderr: (msg) => {
|
|
48423
|
+
stderrBuf.append(msg.text);
|
|
48424
|
+
}
|
|
48300
48425
|
});
|
|
48301
|
-
const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
|
|
48302
|
-
`);
|
|
48303
|
-
const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
|
|
48304
|
-
`);
|
|
48305
48426
|
const exitCode = this.extractExitCode(execution);
|
|
48306
|
-
|
|
48307
|
-
|
|
48308
|
-
|
|
48309
|
-
|
|
48310
|
-
|
|
48427
|
+
return {
|
|
48428
|
+
stdout: stdoutBuf.toString(),
|
|
48429
|
+
stderr: stderrBuf.toString(),
|
|
48430
|
+
exitCode,
|
|
48431
|
+
truncated: stdoutBuf.truncated || stderrBuf.truncated
|
|
48432
|
+
};
|
|
48311
48433
|
} catch (error) {
|
|
48312
48434
|
if (error instanceof SandboxStateError)
|
|
48313
48435
|
throw error;
|
|
@@ -48315,10 +48437,26 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
|
|
|
48315
48437
|
}
|
|
48316
48438
|
}
|
|
48317
48439
|
async executeStream(command, handlers, options) {
|
|
48440
|
+
const wantsComplete = Boolean(handlers.onComplete);
|
|
48441
|
+
const maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
48442
|
+
const stdoutBuf = wantsComplete ? new BoundedOutputBuffer(maxBytes, `
|
|
48443
|
+
`) : undefined;
|
|
48444
|
+
const stderrBuf = wantsComplete ? new BoundedOutputBuffer(maxBytes, `
|
|
48445
|
+
`) : undefined;
|
|
48318
48446
|
try {
|
|
48319
48447
|
const sdkHandlers = {
|
|
48320
|
-
...handlers.
|
|
48321
|
-
|
|
48448
|
+
...handlers.onStdout || wantsComplete ? {
|
|
48449
|
+
onStdout: async (msg) => {
|
|
48450
|
+
stdoutBuf?.append(msg.text);
|
|
48451
|
+
await handlers.onStdout?.(msg);
|
|
48452
|
+
}
|
|
48453
|
+
} : {},
|
|
48454
|
+
...handlers.onStderr || wantsComplete ? {
|
|
48455
|
+
onStderr: async (msg) => {
|
|
48456
|
+
stderrBuf?.append(msg.text);
|
|
48457
|
+
await handlers.onStderr?.(msg);
|
|
48458
|
+
}
|
|
48459
|
+
} : {},
|
|
48322
48460
|
...handlers.onError ? {
|
|
48323
48461
|
onError: async (err) => {
|
|
48324
48462
|
const error = new Error(err.value || err.name || "Execution error");
|
|
@@ -48335,17 +48473,14 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
|
|
|
48335
48473
|
workingDirectory: this.normalizePath(options?.workingDirectory),
|
|
48336
48474
|
background: options?.background
|
|
48337
48475
|
}, sdkHandlers);
|
|
48338
|
-
if (
|
|
48339
|
-
const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
|
|
48340
|
-
`);
|
|
48341
|
-
const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
|
|
48342
|
-
`);
|
|
48476
|
+
if (wantsComplete) {
|
|
48343
48477
|
const exitCode = this.extractExitCode(execution);
|
|
48344
|
-
|
|
48345
|
-
|
|
48346
|
-
|
|
48347
|
-
|
|
48348
|
-
|
|
48478
|
+
await handlers.onComplete({
|
|
48479
|
+
stdout: stdoutBuf.toString(),
|
|
48480
|
+
stderr: stderrBuf.toString(),
|
|
48481
|
+
exitCode,
|
|
48482
|
+
truncated: stdoutBuf.truncated || stderrBuf.truncated
|
|
48483
|
+
});
|
|
48349
48484
|
}
|
|
48350
48485
|
} catch (error) {
|
|
48351
48486
|
throw new CommandExecutionError(`Streaming command execution failed: ${command}`, command, error instanceof Error ? error : undefined);
|
package/dist/index.js
CHANGED
|
@@ -45007,13 +45007,34 @@ class CommandPolyfillService {
|
|
|
45007
45007
|
constructor(executor) {
|
|
45008
45008
|
this.executor = executor;
|
|
45009
45009
|
}
|
|
45010
|
+
static READ_CHUNK_SIZE = 256 * 1024;
|
|
45010
45011
|
async readFile(path) {
|
|
45011
45012
|
try {
|
|
45012
|
-
const
|
|
45013
|
-
if (
|
|
45014
|
-
|
|
45013
|
+
const size = await this.statSize(path);
|
|
45014
|
+
if (size === undefined) {
|
|
45015
|
+
const result = await this.executor.execute(`cat "${this.escapePath(path)}" | base64 -w 0`);
|
|
45016
|
+
if (result.exitCode !== 0) {
|
|
45017
|
+
throw this.createFileError(path, result.stderr);
|
|
45018
|
+
}
|
|
45019
|
+
return base64ToBytes(result.stdout);
|
|
45020
|
+
}
|
|
45021
|
+
if (size === 0)
|
|
45022
|
+
return new Uint8Array;
|
|
45023
|
+
const chunks = [];
|
|
45024
|
+
let totalLength = 0;
|
|
45025
|
+
for (let offset = 0;offset < size; offset += CommandPolyfillService.READ_CHUNK_SIZE) {
|
|
45026
|
+
const end = Math.min(offset + CommandPolyfillService.READ_CHUNK_SIZE, size);
|
|
45027
|
+
const chunk = await this.readFileRange(path, offset, end);
|
|
45028
|
+
chunks.push(chunk);
|
|
45029
|
+
totalLength += chunk.length;
|
|
45015
45030
|
}
|
|
45016
|
-
|
|
45031
|
+
const combined = new Uint8Array(totalLength);
|
|
45032
|
+
let writeOffset = 0;
|
|
45033
|
+
for (const chunk of chunks) {
|
|
45034
|
+
combined.set(chunk, writeOffset);
|
|
45035
|
+
writeOffset += chunk.length;
|
|
45036
|
+
}
|
|
45037
|
+
return combined;
|
|
45017
45038
|
} catch (error) {
|
|
45018
45039
|
if (error instanceof FileOperationError) {
|
|
45019
45040
|
throw error;
|
|
@@ -45024,9 +45045,18 @@ class CommandPolyfillService {
|
|
|
45024
45045
|
throw error;
|
|
45025
45046
|
}
|
|
45026
45047
|
}
|
|
45048
|
+
async statSize(path) {
|
|
45049
|
+
const result = await this.executor.execute(`stat -c '%s' "${this.escapePath(path)}" 2>/dev/null || stat -f '%z' "${this.escapePath(path)}" 2>/dev/null || echo STAT_FAILED`);
|
|
45050
|
+
const stdout = result.stdout.trim();
|
|
45051
|
+
if (!stdout || stdout.includes("STAT_FAILED"))
|
|
45052
|
+
return;
|
|
45053
|
+
const parsed = Number.parseInt(stdout, 10);
|
|
45054
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
45055
|
+
}
|
|
45027
45056
|
async readFileRange(path, start, end) {
|
|
45028
|
-
const
|
|
45029
|
-
const
|
|
45057
|
+
const escaped = this.escapePath(path);
|
|
45058
|
+
const tailPos = start + 1;
|
|
45059
|
+
const cmd = end !== undefined ? `tail -c +${tailPos} "${escaped}" | head -c ${end - start} | base64 -w 0` : `tail -c +${tailPos} "${escaped}" | base64 -w 0`;
|
|
45030
45060
|
const result = await this.executor.execute(cmd);
|
|
45031
45061
|
if (result.exitCode !== 0) {
|
|
45032
45062
|
throw this.createFileError(path, result.stderr);
|
|
@@ -45034,10 +45064,25 @@ class CommandPolyfillService {
|
|
|
45034
45064
|
return base64ToBytes(result.stdout);
|
|
45035
45065
|
}
|
|
45036
45066
|
async writeFile(path, data) {
|
|
45067
|
+
await this.appendBytes(path, data, { truncate: true });
|
|
45068
|
+
return data.length;
|
|
45069
|
+
}
|
|
45070
|
+
async appendBytes(path, data, options) {
|
|
45071
|
+
if (options?.truncate) {
|
|
45072
|
+
await this.createParentDirectory(path);
|
|
45073
|
+
}
|
|
45074
|
+
if (data.length === 0) {
|
|
45075
|
+
if (options?.truncate) {
|
|
45076
|
+
const result = await this.executor.execute(`: > "${this.escapePath(path)}"`);
|
|
45077
|
+
if (result.exitCode !== 0) {
|
|
45078
|
+
throw this.createFileError(path, result.stderr);
|
|
45079
|
+
}
|
|
45080
|
+
}
|
|
45081
|
+
return;
|
|
45082
|
+
}
|
|
45037
45083
|
const base64 = bytesToBase64(data);
|
|
45038
45084
|
const chunkSize = 1024;
|
|
45039
|
-
|
|
45040
|
-
let first = true;
|
|
45085
|
+
let first = Boolean(options?.truncate);
|
|
45041
45086
|
for (let i = 0;i < base64.length; i += chunkSize) {
|
|
45042
45087
|
const chunk = base64.slice(i, i + chunkSize);
|
|
45043
45088
|
const redirect = first ? ">" : ">>";
|
|
@@ -45047,7 +45092,6 @@ class CommandPolyfillService {
|
|
|
45047
45092
|
}
|
|
45048
45093
|
first = false;
|
|
45049
45094
|
}
|
|
45050
|
-
return data.length;
|
|
45051
45095
|
}
|
|
45052
45096
|
async writeTextFile(path, content) {
|
|
45053
45097
|
await this.createParentDirectory(path);
|
|
@@ -45390,23 +45434,7 @@ class BaseSandboxAdapter {
|
|
|
45390
45434
|
const arrayBuffer = await entry.data.arrayBuffer();
|
|
45391
45435
|
bytesWritten = await polyfillService.writeFile(entry.path, new Uint8Array(arrayBuffer));
|
|
45392
45436
|
} else {
|
|
45393
|
-
|
|
45394
|
-
const reader = entry.data.getReader();
|
|
45395
|
-
while (true) {
|
|
45396
|
-
const { done, value } = await reader.read();
|
|
45397
|
-
if (done) {
|
|
45398
|
-
break;
|
|
45399
|
-
}
|
|
45400
|
-
chunks.push(value);
|
|
45401
|
-
}
|
|
45402
|
-
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
45403
|
-
const combined = new Uint8Array(totalLength);
|
|
45404
|
-
let offset = 0;
|
|
45405
|
-
for (const chunk of chunks) {
|
|
45406
|
-
combined.set(chunk, offset);
|
|
45407
|
-
offset += chunk.length;
|
|
45408
|
-
}
|
|
45409
|
-
bytesWritten = await polyfillService.writeFile(entry.path, combined);
|
|
45437
|
+
bytesWritten = await this.writeStreamToFile(polyfillService, entry.path, entry.data);
|
|
45410
45438
|
}
|
|
45411
45439
|
results.push({ path: entry.path, bytesWritten, error: null });
|
|
45412
45440
|
} catch (error) {
|
|
@@ -45481,23 +45509,36 @@ class BaseSandboxAdapter {
|
|
|
45481
45509
|
}
|
|
45482
45510
|
}
|
|
45483
45511
|
async writeFileStream(path, stream) {
|
|
45512
|
+
const polyfillService = this.requirePolyfillService("writeFileStream", "File stream write not supported by this provider");
|
|
45513
|
+
await this.writeStreamToFile(polyfillService, this.normalizePath(path), stream);
|
|
45514
|
+
}
|
|
45515
|
+
async writeStreamToFile(polyfillService, path, stream) {
|
|
45516
|
+
const tmpPath = `${path}.tmp.${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
45484
45517
|
const reader = stream.getReader();
|
|
45485
|
-
|
|
45486
|
-
|
|
45487
|
-
|
|
45488
|
-
|
|
45489
|
-
|
|
45518
|
+
let totalBytes = 0;
|
|
45519
|
+
let first = true;
|
|
45520
|
+
try {
|
|
45521
|
+
while (true) {
|
|
45522
|
+
const { done, value } = await reader.read();
|
|
45523
|
+
if (done)
|
|
45524
|
+
break;
|
|
45525
|
+
if (!value || value.length === 0)
|
|
45526
|
+
continue;
|
|
45527
|
+
await polyfillService.appendBytes(tmpPath, value, first ? { truncate: true } : undefined);
|
|
45528
|
+
totalBytes += value.length;
|
|
45529
|
+
first = false;
|
|
45490
45530
|
}
|
|
45491
|
-
|
|
45492
|
-
|
|
45493
|
-
|
|
45494
|
-
|
|
45495
|
-
|
|
45496
|
-
|
|
45497
|
-
|
|
45498
|
-
|
|
45531
|
+
if (first) {
|
|
45532
|
+
await polyfillService.appendBytes(tmpPath, new Uint8Array, { truncate: true });
|
|
45533
|
+
}
|
|
45534
|
+
await polyfillService.moveFiles([{ source: tmpPath, destination: path }]);
|
|
45535
|
+
} catch (error) {
|
|
45536
|
+
await polyfillService.deleteFiles([tmpPath]).catch(() => {});
|
|
45537
|
+
throw error;
|
|
45538
|
+
} finally {
|
|
45539
|
+
reader.releaseLock();
|
|
45499
45540
|
}
|
|
45500
|
-
|
|
45541
|
+
return totalBytes;
|
|
45501
45542
|
}
|
|
45502
45543
|
async getFileInfo(paths) {
|
|
45503
45544
|
const polyfillService = this.requirePolyfillService("getFileInfo", "File info not supported by this provider");
|
|
@@ -47893,7 +47934,79 @@ var Sandbox = class _Sandbox {
|
|
|
47893
47934
|
}
|
|
47894
47935
|
};
|
|
47895
47936
|
|
|
47937
|
+
// src/utils/outputBuffer.ts
|
|
47938
|
+
class BoundedOutputBuffer {
|
|
47939
|
+
maxBytes;
|
|
47940
|
+
chunks = [];
|
|
47941
|
+
currentBytes = 0;
|
|
47942
|
+
_totalBytes = 0;
|
|
47943
|
+
_truncated = false;
|
|
47944
|
+
separatorBuf;
|
|
47945
|
+
constructor(maxBytes, separator) {
|
|
47946
|
+
this.maxBytes = maxBytes;
|
|
47947
|
+
if (!(maxBytes > 0)) {
|
|
47948
|
+
throw new Error(`BoundedOutputBuffer: maxBytes must be > 0 (got ${maxBytes})`);
|
|
47949
|
+
}
|
|
47950
|
+
if (separator) {
|
|
47951
|
+
this.separatorBuf = Buffer.from(separator, "utf8");
|
|
47952
|
+
}
|
|
47953
|
+
}
|
|
47954
|
+
append(text) {
|
|
47955
|
+
if (!text)
|
|
47956
|
+
return;
|
|
47957
|
+
const needsSep = this.separatorBuf && this.chunks.length > 0;
|
|
47958
|
+
const textBuf = Buffer.from(text, "utf8");
|
|
47959
|
+
const sepLen = needsSep ? this.separatorBuf.length : 0;
|
|
47960
|
+
this._totalBytes += textBuf.length + sepLen;
|
|
47961
|
+
if (textBuf.length + sepLen > this.maxBytes) {
|
|
47962
|
+
this._truncated = true;
|
|
47963
|
+
const keep = Math.min(textBuf.length, this.maxBytes);
|
|
47964
|
+
this.chunks = [
|
|
47965
|
+
keep < textBuf.length ? Buffer.from(textBuf.subarray(textBuf.length - keep)) : textBuf
|
|
47966
|
+
];
|
|
47967
|
+
this.currentBytes = keep;
|
|
47968
|
+
return;
|
|
47969
|
+
}
|
|
47970
|
+
const chunk = needsSep ? Buffer.concat([this.separatorBuf, textBuf]) : textBuf;
|
|
47971
|
+
this.chunks.push(chunk);
|
|
47972
|
+
this.currentBytes += chunk.length;
|
|
47973
|
+
if (this.currentBytes <= this.maxBytes)
|
|
47974
|
+
return;
|
|
47975
|
+
this._truncated = true;
|
|
47976
|
+
while (this.chunks.length > 1 && this.currentBytes - this.chunks[0].length >= this.maxBytes) {
|
|
47977
|
+
this.currentBytes -= this.chunks[0].length;
|
|
47978
|
+
this.chunks.shift();
|
|
47979
|
+
}
|
|
47980
|
+
if (this.currentBytes > this.maxBytes && this.chunks.length > 0) {
|
|
47981
|
+
const overflow = this.currentBytes - this.maxBytes;
|
|
47982
|
+
this.chunks[0] = this.chunks[0].subarray(overflow);
|
|
47983
|
+
this.currentBytes -= overflow;
|
|
47984
|
+
}
|
|
47985
|
+
if (this.separatorBuf && this.chunks.length > 0) {
|
|
47986
|
+
const first = this.chunks[0];
|
|
47987
|
+
const sep = this.separatorBuf;
|
|
47988
|
+
if (first.subarray(0, sep.length).equals(sep)) {
|
|
47989
|
+
this.chunks[0] = first.subarray(sep.length);
|
|
47990
|
+
this.currentBytes -= sep.length;
|
|
47991
|
+
}
|
|
47992
|
+
}
|
|
47993
|
+
}
|
|
47994
|
+
get totalBytes() {
|
|
47995
|
+
return this._totalBytes;
|
|
47996
|
+
}
|
|
47997
|
+
get truncated() {
|
|
47998
|
+
return this._truncated;
|
|
47999
|
+
}
|
|
48000
|
+
toString() {
|
|
48001
|
+
if (this.chunks.length === 0)
|
|
48002
|
+
return "";
|
|
48003
|
+
return Buffer.concat(this.chunks, this.currentBytes).toString("utf8");
|
|
48004
|
+
}
|
|
48005
|
+
}
|
|
48006
|
+
|
|
47896
48007
|
// src/adapters/OpenSandboxAdapter/index.ts
|
|
48008
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
48009
|
+
|
|
47897
48010
|
class OpenSandboxAdapter extends BaseSandboxAdapter {
|
|
47898
48011
|
connectionConfig;
|
|
47899
48012
|
createConfig;
|
|
@@ -48277,21 +48390,30 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
|
|
|
48277
48390
|
return results;
|
|
48278
48391
|
}
|
|
48279
48392
|
async execute(command, options) {
|
|
48393
|
+
const maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
48394
|
+
const stdoutBuf = new BoundedOutputBuffer(maxBytes, `
|
|
48395
|
+
`);
|
|
48396
|
+
const stderrBuf = new BoundedOutputBuffer(maxBytes, `
|
|
48397
|
+
`);
|
|
48280
48398
|
try {
|
|
48281
48399
|
const execution = await this.sandbox.commands.run(command, {
|
|
48282
48400
|
workingDirectory: this.normalizePath(options?.workingDirectory),
|
|
48283
48401
|
background: options?.background
|
|
48402
|
+
}, {
|
|
48403
|
+
onStdout: (msg) => {
|
|
48404
|
+
stdoutBuf.append(msg.text);
|
|
48405
|
+
},
|
|
48406
|
+
onStderr: (msg) => {
|
|
48407
|
+
stderrBuf.append(msg.text);
|
|
48408
|
+
}
|
|
48284
48409
|
});
|
|
48285
|
-
const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
|
|
48286
|
-
`);
|
|
48287
|
-
const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
|
|
48288
|
-
`);
|
|
48289
48410
|
const exitCode = this.extractExitCode(execution);
|
|
48290
|
-
|
|
48291
|
-
|
|
48292
|
-
|
|
48293
|
-
|
|
48294
|
-
|
|
48411
|
+
return {
|
|
48412
|
+
stdout: stdoutBuf.toString(),
|
|
48413
|
+
stderr: stderrBuf.toString(),
|
|
48414
|
+
exitCode,
|
|
48415
|
+
truncated: stdoutBuf.truncated || stderrBuf.truncated
|
|
48416
|
+
};
|
|
48295
48417
|
} catch (error) {
|
|
48296
48418
|
if (error instanceof SandboxStateError)
|
|
48297
48419
|
throw error;
|
|
@@ -48299,10 +48421,26 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
|
|
|
48299
48421
|
}
|
|
48300
48422
|
}
|
|
48301
48423
|
async executeStream(command, handlers, options) {
|
|
48424
|
+
const wantsComplete = Boolean(handlers.onComplete);
|
|
48425
|
+
const maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
48426
|
+
const stdoutBuf = wantsComplete ? new BoundedOutputBuffer(maxBytes, `
|
|
48427
|
+
`) : undefined;
|
|
48428
|
+
const stderrBuf = wantsComplete ? new BoundedOutputBuffer(maxBytes, `
|
|
48429
|
+
`) : undefined;
|
|
48302
48430
|
try {
|
|
48303
48431
|
const sdkHandlers = {
|
|
48304
|
-
...handlers.
|
|
48305
|
-
|
|
48432
|
+
...handlers.onStdout || wantsComplete ? {
|
|
48433
|
+
onStdout: async (msg) => {
|
|
48434
|
+
stdoutBuf?.append(msg.text);
|
|
48435
|
+
await handlers.onStdout?.(msg);
|
|
48436
|
+
}
|
|
48437
|
+
} : {},
|
|
48438
|
+
...handlers.onStderr || wantsComplete ? {
|
|
48439
|
+
onStderr: async (msg) => {
|
|
48440
|
+
stderrBuf?.append(msg.text);
|
|
48441
|
+
await handlers.onStderr?.(msg);
|
|
48442
|
+
}
|
|
48443
|
+
} : {},
|
|
48306
48444
|
...handlers.onError ? {
|
|
48307
48445
|
onError: async (err) => {
|
|
48308
48446
|
const error = new Error(err.value || err.name || "Execution error");
|
|
@@ -48319,17 +48457,14 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
|
|
|
48319
48457
|
workingDirectory: this.normalizePath(options?.workingDirectory),
|
|
48320
48458
|
background: options?.background
|
|
48321
48459
|
}, sdkHandlers);
|
|
48322
|
-
if (
|
|
48323
|
-
const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
|
|
48324
|
-
`);
|
|
48325
|
-
const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
|
|
48326
|
-
`);
|
|
48460
|
+
if (wantsComplete) {
|
|
48327
48461
|
const exitCode = this.extractExitCode(execution);
|
|
48328
|
-
|
|
48329
|
-
|
|
48330
|
-
|
|
48331
|
-
|
|
48332
|
-
|
|
48462
|
+
await handlers.onComplete({
|
|
48463
|
+
stdout: stdoutBuf.toString(),
|
|
48464
|
+
stderr: stderrBuf.toString(),
|
|
48465
|
+
exitCode,
|
|
48466
|
+
truncated: stdoutBuf.truncated || stderrBuf.truncated
|
|
48467
|
+
});
|
|
48333
48468
|
}
|
|
48334
48469
|
} catch (error) {
|
|
48335
48470
|
throw new CommandExecutionError(`Streaming command execution failed: ${command}`, command, error instanceof Error ? error : undefined);
|
|
@@ -13,12 +13,33 @@ export declare class CommandPolyfillService {
|
|
|
13
13
|
private readonly executor;
|
|
14
14
|
constructor(executor: ICommandExecution);
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Chunk size used when reading files through command execution. Each chunk
|
|
17
|
+
* produces a base64-encoded stdout of roughly `READ_CHUNK_SIZE * 4 / 3`
|
|
18
|
+
* bytes, which must fit within the executor's stdout byte cap (default
|
|
19
|
+
* 1 MiB) with room to spare.
|
|
20
|
+
*/
|
|
21
|
+
private static readonly READ_CHUNK_SIZE;
|
|
22
|
+
/**
|
|
23
|
+
* Read a file in chunks via `dd | base64`. Uses `stat` to discover the file
|
|
24
|
+
* size, then issues range reads so that no single command's stdout exceeds
|
|
25
|
+
* the executor's bounded output limit.
|
|
26
|
+
*
|
|
27
|
+
* Falls back to a single `cat | base64` read when `stat` fails (e.g. when
|
|
28
|
+
* the sandbox lacks GNU stat). That fallback is bounded by the caller's
|
|
29
|
+
* `maxOutputBytes`, so very large files require stat-based chunking.
|
|
18
30
|
*/
|
|
19
31
|
readFile(path: string): Promise<Uint8Array>;
|
|
20
32
|
/**
|
|
21
|
-
*
|
|
33
|
+
* Return the file size in bytes, or undefined if stat fails (e.g. the file
|
|
34
|
+
* does not exist or stat is unavailable).
|
|
35
|
+
*/
|
|
36
|
+
private statSize;
|
|
37
|
+
/**
|
|
38
|
+
* Read a portion of a file via `tail -c +N | head -c M | base64`.
|
|
39
|
+
*
|
|
40
|
+
* `tail -c +N` emits bytes starting at position N (1-indexed). `head -c M`
|
|
41
|
+
* caps the length. Both are POSIX-ish and avoid the `dd bs=1` one-syscall-
|
|
42
|
+
* per-byte trap that made the old implementation unusable for large reads.
|
|
22
43
|
*/
|
|
23
44
|
readFileRange(path: string, start: number, end?: number): Promise<Uint8Array>;
|
|
24
45
|
/**
|
|
@@ -26,6 +47,17 @@ export declare class CommandPolyfillService {
|
|
|
26
47
|
* Uses: echo <base64> | base64 -d > <file>
|
|
27
48
|
*/
|
|
28
49
|
writeFile(path: string, data: Uint8Array): Promise<number>;
|
|
50
|
+
/**
|
|
51
|
+
* Append `data` to `path`, chunking the base64 payload to stay under the
|
|
52
|
+
* shell command line length limit. Set `truncate` to rewrite the file
|
|
53
|
+
* from scratch on the first append.
|
|
54
|
+
*
|
|
55
|
+
* Parent directory creation runs once when `truncate` is set, so streaming
|
|
56
|
+
* writes pay the mkdir cost only on the first chunk.
|
|
57
|
+
*/
|
|
58
|
+
appendBytes(path: string, data: Uint8Array, options?: {
|
|
59
|
+
truncate?: boolean;
|
|
60
|
+
}): Promise<void>;
|
|
29
61
|
/**
|
|
30
62
|
* Write a text file directly.
|
|
31
63
|
*/
|
|
@@ -12,6 +12,16 @@ export interface ExecuteOptions {
|
|
|
12
12
|
env?: Record<string, string>;
|
|
13
13
|
/** Abort signal for cancellation */
|
|
14
14
|
signal?: AbortSignal;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of bytes to retain in stdout / stderr for the returned
|
|
17
|
+
* {@link ExecuteResult}. Output beyond this limit is dropped (oldest first)
|
|
18
|
+
* and `truncated` is set to true. Streaming handlers (`onStdout` / `onStderr`)
|
|
19
|
+
* still receive every chunk regardless of this limit.
|
|
20
|
+
*
|
|
21
|
+
* Defaults to 1 MiB per stream. Set a larger value for commands whose full
|
|
22
|
+
* output you need, bearing in mind the memory cost.
|
|
23
|
+
*/
|
|
24
|
+
maxOutputBytes?: number;
|
|
15
25
|
}
|
|
16
26
|
/**
|
|
17
27
|
* Result of command execution.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, append-only text buffer that keeps only the tail when the total
|
|
3
|
+
* byte count exceeds `maxBytes`. The head is dropped first, because for
|
|
4
|
+
* command output the tail is almost always the most useful part (error
|
|
5
|
+
* messages, summaries, stack traces, final lines).
|
|
6
|
+
*
|
|
7
|
+
* Chunks are stored as UTF-8 `Buffer`s so byte counting is O(1) per chunk
|
|
8
|
+
* and slicing is exact. Decoding only happens in {@link toString}, which
|
|
9
|
+
* gracefully handles any broken leading multi-byte sequence created by a
|
|
10
|
+
* mid-character slice.
|
|
11
|
+
*
|
|
12
|
+
* Memory is O(maxBytes) regardless of how much data is appended over the
|
|
13
|
+
* lifetime of the buffer.
|
|
14
|
+
*/
|
|
15
|
+
export declare class BoundedOutputBuffer {
|
|
16
|
+
private readonly maxBytes;
|
|
17
|
+
private chunks;
|
|
18
|
+
private currentBytes;
|
|
19
|
+
private _totalBytes;
|
|
20
|
+
private _truncated;
|
|
21
|
+
private readonly separatorBuf;
|
|
22
|
+
/**
|
|
23
|
+
* @param maxBytes Maximum retained bytes.
|
|
24
|
+
* @param separator Optional string inserted between consecutive appends
|
|
25
|
+
* (e.g. `'\n'` to replicate the old `join('\n')` behaviour).
|
|
26
|
+
*/
|
|
27
|
+
constructor(maxBytes: number, separator?: string);
|
|
28
|
+
append(text: string): void;
|
|
29
|
+
/** Total bytes appended over the buffer's lifetime (not just the bytes currently stored). */
|
|
30
|
+
get totalBytes(): number;
|
|
31
|
+
/** True once any data has been dropped. */
|
|
32
|
+
get truncated(): boolean;
|
|
33
|
+
toString(): string;
|
|
34
|
+
}
|
package/package.json
CHANGED