noumen 0.3.0 → 0.5.0
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/README.md +63 -8
- package/dist/a2a/index.d.ts +4 -2
- package/dist/acp/index.d.ts +5 -3
- package/dist/{agent-1nFVUP9E.d.ts → agent-C3eDRsxs.d.ts} +19 -508
- package/dist/chunk-I5SBSOS6.js +40 -0
- package/dist/chunk-I5SBSOS6.js.map +1 -0
- package/dist/{chunk-4HW6LN6D.js → chunk-WPCYGZOE.js} +58 -1228
- package/dist/chunk-WPCYGZOE.js.map +1 -0
- package/dist/{chunk-5JN4SPI7.js → chunk-WTLK2ZAR.js} +1 -1
- package/dist/{chunk-HL6JCRZJ.js → chunk-XZN4QZLK.js} +4 -4
- package/dist/cli/index.js +10 -10
- package/dist/computer-BPdxSo6X.d.ts +88 -0
- package/dist/docker.d.ts +129 -0
- package/dist/docker.js +401 -0
- package/dist/docker.js.map +1 -0
- package/dist/e2b.d.ts +157 -0
- package/dist/e2b.js +202 -0
- package/dist/e2b.js.map +1 -0
- package/dist/freestyle.d.ts +174 -0
- package/dist/freestyle.js +240 -0
- package/dist/freestyle.js.map +1 -0
- package/dist/index.d.ts +9 -201
- package/dist/index.js +24 -48
- package/dist/lsp/index.d.ts +3 -2
- package/dist/mcp/index.d.ts +4 -3
- package/dist/mcp/index.js +2 -2
- package/dist/{provider-factory-KCLIF34X.js → provider-factory-KI7OZUY3.js} +2 -2
- package/dist/{resolve-4JA2BBDA.js → resolve-GDSHNMG6.js} +2 -2
- package/dist/sandbox-9qeMTNrD.d.ts +126 -0
- package/dist/server/index.d.ts +4 -2
- package/dist/{server-CHMxuWKq.d.ts → server-Cu9gv1dk.d.ts} +1 -1
- package/dist/sprites.d.ts +136 -0
- package/dist/sprites.js +334 -0
- package/dist/sprites.js.map +1 -0
- package/dist/ssh.d.ts +187 -0
- package/dist/ssh.js +392 -0
- package/dist/ssh.js.map +1 -0
- package/dist/{types-RPKUTu1k.d.ts → types-BA87bHPV.d.ts} +2 -88
- package/package.json +28 -1
- package/dist/chunk-4HW6LN6D.js.map +0 -1
- /package/dist/{chunk-5JN4SPI7.js.map → chunk-WTLK2ZAR.js.map} +0 -0
- /package/dist/{chunk-HL6JCRZJ.js.map → chunk-XZN4QZLK.js.map} +0 -0
- /package/dist/{provider-factory-KCLIF34X.js.map → provider-factory-KI7OZUY3.js.map} +0 -0
- /package/dist/{resolve-4JA2BBDA.js.map → resolve-GDSHNMG6.js.map} +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
formatZodValidationError
|
|
3
|
-
} from "./chunk-3SK5GCI6.js";
|
|
4
1
|
import {
|
|
5
2
|
IMAGE_EXTENSIONS,
|
|
6
3
|
compressImageBufferWithTokenLimit,
|
|
7
4
|
createImageMetadataText,
|
|
8
5
|
maybeResizeAndDownsampleImageBuffer
|
|
9
6
|
} from "./chunk-5GEX6ZSB.js";
|
|
7
|
+
import {
|
|
8
|
+
formatZodValidationError
|
|
9
|
+
} from "./chunk-3SK5GCI6.js";
|
|
10
10
|
import {
|
|
11
11
|
contentToString
|
|
12
12
|
} from "./chunk-JACGEMTF.js";
|
|
@@ -3109,4 +3109,4 @@ export {
|
|
|
3109
3109
|
resolveToolFlag,
|
|
3110
3110
|
ToolRegistry
|
|
3111
3111
|
};
|
|
3112
|
-
//# sourceMappingURL=chunk-
|
|
3112
|
+
//# sourceMappingURL=chunk-XZN4QZLK.js.map
|
package/dist/cli/index.js
CHANGED
|
@@ -16,21 +16,21 @@ import {
|
|
|
16
16
|
Agent,
|
|
17
17
|
LocalSandbox,
|
|
18
18
|
UnsandboxedLocal
|
|
19
|
-
} from "../chunk-
|
|
20
|
-
import "../chunk-42PHHZUA.js";
|
|
19
|
+
} from "../chunk-WPCYGZOE.js";
|
|
21
20
|
import {
|
|
22
21
|
DEFAULT_MODELS,
|
|
23
22
|
SUPPORTED_PROVIDERS,
|
|
24
23
|
detectProvider,
|
|
25
24
|
resolveProvider
|
|
26
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-WTLK2ZAR.js";
|
|
26
|
+
import "../chunk-42PHHZUA.js";
|
|
27
|
+
import "../chunk-XZN4QZLK.js";
|
|
28
|
+
import "../chunk-5GEX6ZSB.js";
|
|
29
|
+
import "../chunk-4SQA2UCV.js";
|
|
30
|
+
import "../chunk-3SK5GCI6.js";
|
|
27
31
|
import "../chunk-HEQQQGK5.js";
|
|
28
32
|
import "../chunk-L3L3FG5T.js";
|
|
29
33
|
import "../chunk-3HEYCV26.js";
|
|
30
|
-
import "../chunk-HL6JCRZJ.js";
|
|
31
|
-
import "../chunk-3SK5GCI6.js";
|
|
32
|
-
import "../chunk-5GEX6ZSB.js";
|
|
33
|
-
import "../chunk-4SQA2UCV.js";
|
|
34
34
|
import "../chunk-JACGEMTF.js";
|
|
35
35
|
import "../chunk-DGUM43GV.js";
|
|
36
36
|
|
|
@@ -532,7 +532,7 @@ async function runAgent(config) {
|
|
|
532
532
|
const { createInterface: createInterface3 } = await import("readline/promises");
|
|
533
533
|
const rl = createInterface3({ input: process.stdin, output: process.stderr, terminal: true });
|
|
534
534
|
try {
|
|
535
|
-
const { SUPPORTED_PROVIDERS: SUPPORTED_PROVIDERS2, isOllamaRunning: isOllamaRunning2, ollamaBaseURL: ollamaBaseURL2 } = await import("../provider-factory-
|
|
535
|
+
const { SUPPORTED_PROVIDERS: SUPPORTED_PROVIDERS2, isOllamaRunning: isOllamaRunning2, ollamaBaseURL: ollamaBaseURL2 } = await import("../provider-factory-KI7OZUY3.js");
|
|
536
536
|
const providerAnswer = await rl.question(
|
|
537
537
|
` Provider (${SUPPORTED_PROVIDERS2.join(", ")}) [${chalk3.bold("ollama")}]: `
|
|
538
538
|
);
|
|
@@ -589,9 +589,9 @@ async function runAgent(config) {
|
|
|
589
589
|
return runAgent(config);
|
|
590
590
|
}
|
|
591
591
|
if (!config.model) {
|
|
592
|
-
const { DEFAULT_MODELS: DEFAULT_MODELS2 } = await import("../provider-factory-
|
|
592
|
+
const { DEFAULT_MODELS: DEFAULT_MODELS2 } = await import("../provider-factory-KI7OZUY3.js");
|
|
593
593
|
if (providerName === "ollama") {
|
|
594
|
-
const { ollamaBaseURL: ollamaBaseURL2 } = await import("../provider-factory-
|
|
594
|
+
const { ollamaBaseURL: ollamaBaseURL2 } = await import("../provider-factory-KI7OZUY3.js");
|
|
595
595
|
const models = await listLocalOllamaModels(ollamaBaseURL2());
|
|
596
596
|
const preferred = DEFAULT_MODELS2[providerName];
|
|
597
597
|
config.model = models.includes(preferred) ? preferred : models[0] ?? preferred;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
interface FileEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
isDirectory: boolean;
|
|
5
|
+
isFile: boolean;
|
|
6
|
+
size?: number;
|
|
7
|
+
}
|
|
8
|
+
interface FileStat {
|
|
9
|
+
size: number;
|
|
10
|
+
isDirectory: boolean;
|
|
11
|
+
isFile: boolean;
|
|
12
|
+
createdAt?: Date;
|
|
13
|
+
modifiedAt?: Date;
|
|
14
|
+
}
|
|
15
|
+
interface ReadOptions {
|
|
16
|
+
encoding?: BufferEncoding;
|
|
17
|
+
/**
|
|
18
|
+
* Maximum number of bytes to read. When set, only the first `maxBytes`
|
|
19
|
+
* bytes are returned (decoded as a string). Implementations that do not
|
|
20
|
+
* support this option may ignore it and return the full content.
|
|
21
|
+
*/
|
|
22
|
+
maxBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sandboxed filesystem interface.
|
|
26
|
+
*
|
|
27
|
+
* `VirtualFs` is noumen's primary isolation boundary for file I/O. Every
|
|
28
|
+
* built-in tool that touches the filesystem (ReadFile, WriteFile, EditFile)
|
|
29
|
+
* delegates to this interface — the agent never accesses `node:fs` directly.
|
|
30
|
+
*
|
|
31
|
+
* Swap implementations to control where files live and what the agent can reach:
|
|
32
|
+
* - `LocalFs` — reads/writes on the host filesystem (no isolation, for local dev)
|
|
33
|
+
* - `SpritesFs` — reads/writes inside a remote sprites.dev container (full sandbox)
|
|
34
|
+
* - Custom — implement this interface for Docker volumes, E2B, S3, in-memory, etc.
|
|
35
|
+
*/
|
|
36
|
+
interface VirtualFs {
|
|
37
|
+
readFile(path: string, opts?: ReadOptions): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Read raw bytes from a file. Used for binary content (images, PDFs).
|
|
40
|
+
* Implementations SHOULD cap the read at `maxBytes` to prevent OOM on
|
|
41
|
+
* very large files. When `maxBytes` is omitted, the entire file is read.
|
|
42
|
+
*
|
|
43
|
+
* Returns a Buffer (Node.js) or Uint8Array.
|
|
44
|
+
*/
|
|
45
|
+
readFileBytes?(path: string, maxBytes?: number): Promise<Buffer>;
|
|
46
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
47
|
+
appendFile(path: string, content: string): Promise<void>;
|
|
48
|
+
deleteFile(path: string, opts?: {
|
|
49
|
+
recursive?: boolean;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
mkdir(path: string, opts?: {
|
|
52
|
+
recursive?: boolean;
|
|
53
|
+
}): Promise<void>;
|
|
54
|
+
readdir(path: string, opts?: {
|
|
55
|
+
recursive?: boolean;
|
|
56
|
+
}): Promise<FileEntry[]>;
|
|
57
|
+
exists(path: string): Promise<boolean>;
|
|
58
|
+
stat(path: string): Promise<FileStat>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ExecOptions {
|
|
62
|
+
timeout?: number;
|
|
63
|
+
cwd?: string;
|
|
64
|
+
env?: Record<string, string>;
|
|
65
|
+
signal?: AbortSignal;
|
|
66
|
+
}
|
|
67
|
+
interface CommandResult {
|
|
68
|
+
exitCode: number;
|
|
69
|
+
stdout: string;
|
|
70
|
+
stderr: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Sandboxed shell execution interface.
|
|
74
|
+
*
|
|
75
|
+
* `VirtualComputer` is noumen's primary isolation boundary for command
|
|
76
|
+
* execution. Every built-in tool that runs shell commands (Bash, Glob, Grep)
|
|
77
|
+
* delegates to this interface — the agent never spawns processes directly.
|
|
78
|
+
*
|
|
79
|
+
* Swap implementations to control where and how commands run:
|
|
80
|
+
* - `LocalComputer` — runs on the host machine (no isolation, for local dev)
|
|
81
|
+
* - `SpritesComputer` — runs in a remote sprites.dev container (full sandbox)
|
|
82
|
+
* - Custom — implement this interface for Docker, E2B, Daytona, etc.
|
|
83
|
+
*/
|
|
84
|
+
interface VirtualComputer {
|
|
85
|
+
executeCommand(command: string, opts?: ExecOptions): Promise<CommandResult>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type { CommandResult as C, ExecOptions as E, FileEntry as F, ReadOptions as R, VirtualComputer as V, VirtualFs as a, FileStat as b };
|
package/dist/docker.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { S as Sandbox } from './sandbox-9qeMTNrD.js';
|
|
2
|
+
import { V as VirtualComputer, E as ExecOptions, C as CommandResult, a as VirtualFs, R as ReadOptions, F as FileEntry, b as FileStat } from './computer-BPdxSo6X.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal subset of the dockerode Container interface used by DockerComputer.
|
|
6
|
+
* Avoids a hard import of dockerode at the module level.
|
|
7
|
+
*/
|
|
8
|
+
interface DockerContainer {
|
|
9
|
+
exec(options: Record<string, unknown>): Promise<{
|
|
10
|
+
start(opts?: Record<string, unknown>): Promise<NodeJS.ReadableStream>;
|
|
11
|
+
inspect(): Promise<{
|
|
12
|
+
ExitCode: number;
|
|
13
|
+
}>;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
interface DockerComputerOptions {
|
|
17
|
+
/** A dockerode Container instance for the target container. */
|
|
18
|
+
container: DockerContainer;
|
|
19
|
+
/** Default working directory for commands (default: /). */
|
|
20
|
+
defaultCwd?: string;
|
|
21
|
+
/** Default timeout in ms for commands (default: 30000). */
|
|
22
|
+
defaultTimeout?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* VirtualComputer backed by command execution inside a Docker container.
|
|
26
|
+
*
|
|
27
|
+
* Requires `dockerode` as an optional peer dependency.
|
|
28
|
+
* The user is responsible for container lifecycle (create, start, stop).
|
|
29
|
+
*/
|
|
30
|
+
declare class DockerComputer implements VirtualComputer {
|
|
31
|
+
private container;
|
|
32
|
+
private defaultCwd;
|
|
33
|
+
private defaultTimeout;
|
|
34
|
+
constructor(opts: DockerComputerOptions);
|
|
35
|
+
executeCommand(command: string, opts?: ExecOptions): Promise<CommandResult>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface DockerSandboxOptions {
|
|
39
|
+
/**
|
|
40
|
+
* A pre-existing dockerode Container instance. When provided the sandbox
|
|
41
|
+
* attaches to this container directly — no auto-creation occurs and
|
|
42
|
+
* `dispose()` will **not** stop or remove it.
|
|
43
|
+
*
|
|
44
|
+
* When omitted, a new container is created from `image` on the first
|
|
45
|
+
* `init()` call via a dynamic import of `dockerode`. The auto-created
|
|
46
|
+
* container is stopped and removed when `dispose()` is called.
|
|
47
|
+
*/
|
|
48
|
+
container?: DockerContainer;
|
|
49
|
+
/**
|
|
50
|
+
* Docker image to use for auto-creation (e.g. `"ubuntu:22.04"`).
|
|
51
|
+
* Required when `container` is omitted; ignored when `container` is provided.
|
|
52
|
+
*/
|
|
53
|
+
image?: string;
|
|
54
|
+
/** Command to run in the auto-created container (default: `["sleep", "infinity"]`). */
|
|
55
|
+
cmd?: string[];
|
|
56
|
+
/** Environment variables for the auto-created container. */
|
|
57
|
+
env?: string[];
|
|
58
|
+
/** Extra options passed to dockerode `createContainer`. */
|
|
59
|
+
dockerOptions?: Record<string, unknown>;
|
|
60
|
+
/** Working directory inside the container. */
|
|
61
|
+
cwd?: string;
|
|
62
|
+
/** Default timeout (ms) for shell commands. */
|
|
63
|
+
defaultTimeout?: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create a `Sandbox` backed by a Docker container.
|
|
67
|
+
* Requires `dockerode` as an optional peer dependency.
|
|
68
|
+
*
|
|
69
|
+
* **Auto-creation:** When `container` is omitted and `image` is provided,
|
|
70
|
+
* the container is created and started lazily on the first `init()` call.
|
|
71
|
+
* The container ID is available through `sandboxId()` for session
|
|
72
|
+
* persistence. Pass the stored ID back through `init(storedId)` to
|
|
73
|
+
* reattach to an existing container on resume.
|
|
74
|
+
*
|
|
75
|
+
* **Explicit container:** When `container` is provided, `init()` binds
|
|
76
|
+
* it immediately. `dispose()` is a no-op — the caller owns the
|
|
77
|
+
* container's lifecycle.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* // Auto-create from image
|
|
82
|
+
* const sandbox = DockerSandbox({ image: "ubuntu:22.04", cwd: "/workspace" });
|
|
83
|
+
*
|
|
84
|
+
* // Explicit container (lifecycle managed externally)
|
|
85
|
+
* const sandbox = DockerSandbox({ container: myDockerodeContainer });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
declare function DockerSandbox(opts: DockerSandboxOptions): Sandbox;
|
|
89
|
+
|
|
90
|
+
interface DockerFsOptions {
|
|
91
|
+
/** A dockerode Container instance for the target container. */
|
|
92
|
+
container: DockerContainer;
|
|
93
|
+
/** Working directory for relative path resolution (default: /). */
|
|
94
|
+
workingDir?: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* VirtualFs backed by file operations inside a Docker container.
|
|
98
|
+
*
|
|
99
|
+
* Uses `container.exec()` to run filesystem commands (cat, tee, rm, mkdir,
|
|
100
|
+
* stat, etc.) inside the container. File writes use exec + tee to avoid
|
|
101
|
+
* tar archive overhead for text content.
|
|
102
|
+
*
|
|
103
|
+
* Requires `dockerode` as an optional peer dependency.
|
|
104
|
+
* The user is responsible for container lifecycle.
|
|
105
|
+
*/
|
|
106
|
+
declare class DockerFs implements VirtualFs {
|
|
107
|
+
private container;
|
|
108
|
+
private workingDir;
|
|
109
|
+
constructor(opts: DockerFsOptions);
|
|
110
|
+
private resolvePath;
|
|
111
|
+
private exec;
|
|
112
|
+
readFile(path: string, _opts?: ReadOptions): Promise<string>;
|
|
113
|
+
readFileBytes(path: string, maxBytes?: number): Promise<Buffer>;
|
|
114
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
115
|
+
appendFile(path: string, content: string): Promise<void>;
|
|
116
|
+
deleteFile(path: string, opts?: {
|
|
117
|
+
recursive?: boolean;
|
|
118
|
+
}): Promise<void>;
|
|
119
|
+
mkdir(path: string, opts?: {
|
|
120
|
+
recursive?: boolean;
|
|
121
|
+
}): Promise<void>;
|
|
122
|
+
readdir(path: string, _opts?: {
|
|
123
|
+
recursive?: boolean;
|
|
124
|
+
}): Promise<FileEntry[]>;
|
|
125
|
+
exists(path: string): Promise<boolean>;
|
|
126
|
+
stat(path: string): Promise<FileStat>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { DockerComputer, type DockerComputerOptions, type DockerContainer, DockerFs, type DockerFsOptions, DockerSandbox, type DockerSandboxOptions };
|
package/dist/docker.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createComputerProxy,
|
|
3
|
+
createFsProxy
|
|
4
|
+
} from "./chunk-I5SBSOS6.js";
|
|
5
|
+
import "./chunk-DGUM43GV.js";
|
|
6
|
+
|
|
7
|
+
// src/virtual/docker-fs.ts
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
var DockerFs = class {
|
|
10
|
+
container;
|
|
11
|
+
workingDir;
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
this.container = opts.container;
|
|
14
|
+
this.workingDir = opts.workingDir ?? "/";
|
|
15
|
+
}
|
|
16
|
+
resolvePath(p) {
|
|
17
|
+
if (p.includes("\0")) {
|
|
18
|
+
throw new Error("Path contains null bytes");
|
|
19
|
+
}
|
|
20
|
+
const normalizedBase = this.workingDir.endsWith("/") ? this.workingDir : this.workingDir + "/";
|
|
21
|
+
if (p.startsWith("/")) {
|
|
22
|
+
const normalized = path.normalize(p);
|
|
23
|
+
if (normalized !== this.workingDir && !normalized.startsWith(normalizedBase)) {
|
|
24
|
+
throw new Error(`Absolute path "${p}" is outside working directory "${this.workingDir}"`);
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
const resolved = path.resolve(this.workingDir, p);
|
|
29
|
+
if (resolved !== this.workingDir && !resolved.startsWith(normalizedBase)) {
|
|
30
|
+
throw new Error(`Path "${p}" escapes working directory "${this.workingDir}"`);
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
async exec(cmd) {
|
|
35
|
+
const execInstance = await this.container.exec({
|
|
36
|
+
Cmd: cmd,
|
|
37
|
+
AttachStdout: true,
|
|
38
|
+
AttachStderr: true,
|
|
39
|
+
Tty: false
|
|
40
|
+
});
|
|
41
|
+
const stream = await execInstance.start({ hijack: true, stdin: false });
|
|
42
|
+
const result = await collectExecStream(stream);
|
|
43
|
+
const inspection = await execInstance.inspect();
|
|
44
|
+
return { exitCode: inspection.ExitCode, ...result };
|
|
45
|
+
}
|
|
46
|
+
async readFile(path2, _opts) {
|
|
47
|
+
const resolved = this.resolvePath(path2);
|
|
48
|
+
const { exitCode, stdout, stderr } = await this.exec([
|
|
49
|
+
"cat",
|
|
50
|
+
resolved
|
|
51
|
+
]);
|
|
52
|
+
if (exitCode !== 0) {
|
|
53
|
+
throw new Error(`DockerFs readFile failed: ${stderr.trim() || `exit code ${exitCode}`}`);
|
|
54
|
+
}
|
|
55
|
+
return stdout;
|
|
56
|
+
}
|
|
57
|
+
async readFileBytes(path2, maxBytes) {
|
|
58
|
+
const resolved = this.resolvePath(path2);
|
|
59
|
+
const cmd = maxBytes !== void 0 ? ["head", "-c", String(maxBytes), resolved] : ["cat", resolved];
|
|
60
|
+
const { exitCode, stdout, stderr } = await this.exec([
|
|
61
|
+
"bash",
|
|
62
|
+
"-c",
|
|
63
|
+
`${cmd.map(shellEscape).join(" ")} | base64`
|
|
64
|
+
]);
|
|
65
|
+
if (exitCode !== 0) {
|
|
66
|
+
throw new Error(`DockerFs readFileBytes failed: ${stderr.trim() || `exit code ${exitCode}`}`);
|
|
67
|
+
}
|
|
68
|
+
return Buffer.from(stdout.trim(), "base64");
|
|
69
|
+
}
|
|
70
|
+
async writeFile(path2, content) {
|
|
71
|
+
const resolved = this.resolvePath(path2);
|
|
72
|
+
const dir = resolved.substring(0, resolved.lastIndexOf("/"));
|
|
73
|
+
if (dir) {
|
|
74
|
+
await this.exec(["mkdir", "-p", dir]);
|
|
75
|
+
}
|
|
76
|
+
const encoded = Buffer.from(content, "utf-8").toString("base64");
|
|
77
|
+
const MAX_INLINE_LEN = 1e5;
|
|
78
|
+
if (encoded.length <= MAX_INLINE_LEN) {
|
|
79
|
+
const { exitCode, stderr } = await this.exec([
|
|
80
|
+
"bash",
|
|
81
|
+
"-c",
|
|
82
|
+
`echo ${shellEscape(encoded)} | base64 -d > ${shellEscape(resolved)}`
|
|
83
|
+
]);
|
|
84
|
+
if (exitCode !== 0) {
|
|
85
|
+
throw new Error(`DockerFs writeFile failed: ${stderr.trim()}`);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const execInstance = await this.container.exec({
|
|
89
|
+
Cmd: ["bash", "-c", `base64 -d > ${shellEscape(resolved)}`],
|
|
90
|
+
AttachStdout: true,
|
|
91
|
+
AttachStderr: true,
|
|
92
|
+
AttachStdin: true,
|
|
93
|
+
Tty: false
|
|
94
|
+
});
|
|
95
|
+
const stream = await execInstance.start({ hijack: true, stdin: true });
|
|
96
|
+
const writable = stream;
|
|
97
|
+
writable.write(encoded);
|
|
98
|
+
writable.end();
|
|
99
|
+
const result = await collectExecStream(stream);
|
|
100
|
+
const inspection = await execInstance.inspect();
|
|
101
|
+
if (inspection.ExitCode !== 0) {
|
|
102
|
+
throw new Error(`DockerFs writeFile failed: ${result.stderr.trim()}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async appendFile(path2, content) {
|
|
107
|
+
const resolved = this.resolvePath(path2);
|
|
108
|
+
const dir = resolved.substring(0, resolved.lastIndexOf("/"));
|
|
109
|
+
if (dir) {
|
|
110
|
+
await this.exec(["mkdir", "-p", dir]);
|
|
111
|
+
}
|
|
112
|
+
const encoded = Buffer.from(content, "utf-8").toString("base64");
|
|
113
|
+
const { exitCode, stderr } = await this.exec([
|
|
114
|
+
"bash",
|
|
115
|
+
"-c",
|
|
116
|
+
`echo ${shellEscape(encoded)} | base64 -d >> ${shellEscape(resolved)}`
|
|
117
|
+
]);
|
|
118
|
+
if (exitCode !== 0) {
|
|
119
|
+
throw new Error(`DockerFs appendFile failed: ${stderr.trim()}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async deleteFile(path2, opts) {
|
|
123
|
+
const resolved = this.resolvePath(path2);
|
|
124
|
+
const args = opts?.recursive ? ["rm", "-rf", resolved] : ["rm", "-f", resolved];
|
|
125
|
+
await this.exec(args);
|
|
126
|
+
}
|
|
127
|
+
async mkdir(path2, opts) {
|
|
128
|
+
const resolved = this.resolvePath(path2);
|
|
129
|
+
const args = opts?.recursive ? ["mkdir", "-p", resolved] : ["mkdir", resolved];
|
|
130
|
+
await this.exec(args);
|
|
131
|
+
}
|
|
132
|
+
async readdir(path2, _opts) {
|
|
133
|
+
const resolved = this.resolvePath(path2);
|
|
134
|
+
const { exitCode, stdout, stderr } = await this.exec([
|
|
135
|
+
"bash",
|
|
136
|
+
"-c",
|
|
137
|
+
`find ${shellEscape(resolved)} -maxdepth 1 -mindepth 1 -printf '%y %p\\n' 2>/dev/null`
|
|
138
|
+
]);
|
|
139
|
+
if (exitCode !== 0 && stderr.trim()) {
|
|
140
|
+
throw new Error(`DockerFs readdir failed: ${stderr.trim()}`);
|
|
141
|
+
}
|
|
142
|
+
const entries = [];
|
|
143
|
+
for (const line of stdout.trim().split("\n")) {
|
|
144
|
+
if (!line) continue;
|
|
145
|
+
const spaceIdx = line.indexOf(" ");
|
|
146
|
+
const type = line.substring(0, spaceIdx);
|
|
147
|
+
const fullPath = line.substring(spaceIdx + 1);
|
|
148
|
+
const name = fullPath.substring(fullPath.lastIndexOf("/") + 1);
|
|
149
|
+
entries.push({
|
|
150
|
+
name,
|
|
151
|
+
path: fullPath,
|
|
152
|
+
isDirectory: type === "d",
|
|
153
|
+
isFile: type === "f"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
async exists(path2) {
|
|
159
|
+
const resolved = this.resolvePath(path2);
|
|
160
|
+
const { exitCode } = await this.exec(["test", "-e", resolved]);
|
|
161
|
+
return exitCode === 0;
|
|
162
|
+
}
|
|
163
|
+
async stat(path2) {
|
|
164
|
+
const resolved = this.resolvePath(path2);
|
|
165
|
+
const { exitCode, stdout, stderr } = await this.exec([
|
|
166
|
+
"stat",
|
|
167
|
+
"-c",
|
|
168
|
+
"%s %F %W %Y",
|
|
169
|
+
resolved
|
|
170
|
+
]);
|
|
171
|
+
if (exitCode !== 0) {
|
|
172
|
+
throw new Error(`DockerFs stat failed: ${stderr.trim() || `exit code ${exitCode}`}`);
|
|
173
|
+
}
|
|
174
|
+
const parts = stdout.trim().split(" ");
|
|
175
|
+
const size = parseInt(parts[0], 10);
|
|
176
|
+
const fileType = parts[1];
|
|
177
|
+
const createdEpoch = parseInt(parts[2], 10);
|
|
178
|
+
const modifiedEpoch = parseInt(parts[3], 10);
|
|
179
|
+
return {
|
|
180
|
+
size,
|
|
181
|
+
isDirectory: fileType === "directory",
|
|
182
|
+
isFile: fileType.startsWith("regular"),
|
|
183
|
+
createdAt: createdEpoch > 0 ? new Date(createdEpoch * 1e3) : void 0,
|
|
184
|
+
modifiedAt: modifiedEpoch > 0 ? new Date(modifiedEpoch * 1e3) : void 0
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
function shellEscape(s) {
|
|
189
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
190
|
+
}
|
|
191
|
+
function collectExecStream(stream) {
|
|
192
|
+
return new Promise((resolve2, reject) => {
|
|
193
|
+
const stdoutBufs = [];
|
|
194
|
+
const stderrBufs = [];
|
|
195
|
+
let pending = Buffer.alloc(0);
|
|
196
|
+
stream.on("data", (chunk) => {
|
|
197
|
+
let buf = pending.length > 0 ? Buffer.concat([pending, chunk]) : chunk;
|
|
198
|
+
let offset = 0;
|
|
199
|
+
while (offset + 8 <= buf.length) {
|
|
200
|
+
const payloadLen = buf.readUInt32BE(offset + 4);
|
|
201
|
+
if (offset + 8 + payloadLen > buf.length) break;
|
|
202
|
+
const streamType = buf[offset];
|
|
203
|
+
const payload = buf.subarray(offset + 8, offset + 8 + payloadLen);
|
|
204
|
+
if (streamType === 2) {
|
|
205
|
+
stderrBufs.push(payload);
|
|
206
|
+
} else {
|
|
207
|
+
stdoutBufs.push(payload);
|
|
208
|
+
}
|
|
209
|
+
offset += 8 + payloadLen;
|
|
210
|
+
}
|
|
211
|
+
pending = offset < buf.length ? buf.subarray(offset) : Buffer.alloc(0);
|
|
212
|
+
});
|
|
213
|
+
stream.on("end", () => {
|
|
214
|
+
resolve2({
|
|
215
|
+
stdout: Buffer.concat(stdoutBufs).toString("utf-8"),
|
|
216
|
+
stderr: Buffer.concat(stderrBufs).toString("utf-8")
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
stream.on("error", (err) => reject(err));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/virtual/docker-computer.ts
|
|
224
|
+
var DockerComputer = class {
|
|
225
|
+
container;
|
|
226
|
+
defaultCwd;
|
|
227
|
+
defaultTimeout;
|
|
228
|
+
constructor(opts) {
|
|
229
|
+
this.container = opts.container;
|
|
230
|
+
this.defaultCwd = opts.defaultCwd ?? "/";
|
|
231
|
+
this.defaultTimeout = opts.defaultTimeout ?? 3e4;
|
|
232
|
+
}
|
|
233
|
+
async executeCommand(command, opts) {
|
|
234
|
+
const cwd = opts?.cwd ?? this.defaultCwd;
|
|
235
|
+
const timeout = opts?.timeout ?? this.defaultTimeout;
|
|
236
|
+
const execOpts = {
|
|
237
|
+
Cmd: ["bash", "-c", `cd ${shellEscape2(cwd)} && ${command}`],
|
|
238
|
+
AttachStdout: true,
|
|
239
|
+
AttachStderr: true,
|
|
240
|
+
Tty: false
|
|
241
|
+
};
|
|
242
|
+
if (opts?.env) {
|
|
243
|
+
execOpts.Env = Object.entries(opts.env).map(
|
|
244
|
+
([k, v]) => `${k}=${v}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const exec = await this.container.exec(execOpts);
|
|
248
|
+
const stream = await exec.start({ hijack: true, stdin: false });
|
|
249
|
+
const { stdout, stderr } = await collectStream(stream, timeout);
|
|
250
|
+
const inspection = await exec.inspect();
|
|
251
|
+
return {
|
|
252
|
+
exitCode: inspection.ExitCode,
|
|
253
|
+
stdout,
|
|
254
|
+
stderr
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
function shellEscape2(s) {
|
|
259
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
260
|
+
}
|
|
261
|
+
function collectStream(stream, timeout) {
|
|
262
|
+
return new Promise((resolve2, reject) => {
|
|
263
|
+
const stdoutBufs = [];
|
|
264
|
+
const stderrBufs = [];
|
|
265
|
+
let pending = Buffer.alloc(0);
|
|
266
|
+
const timer = setTimeout(() => {
|
|
267
|
+
stream.destroy?.();
|
|
268
|
+
resolve2({
|
|
269
|
+
stdout: Buffer.concat(stdoutBufs).toString("utf-8"),
|
|
270
|
+
stderr: Buffer.concat(stderrBufs).toString("utf-8") + "\n[timeout after " + timeout + "ms]"
|
|
271
|
+
});
|
|
272
|
+
}, timeout);
|
|
273
|
+
stream.on("data", (chunk) => {
|
|
274
|
+
let buf = pending.length > 0 ? Buffer.concat([pending, chunk]) : chunk;
|
|
275
|
+
let offset = 0;
|
|
276
|
+
while (offset + 8 <= buf.length) {
|
|
277
|
+
const payloadLen = buf.readUInt32BE(offset + 4);
|
|
278
|
+
if (offset + 8 + payloadLen > buf.length) break;
|
|
279
|
+
const streamType = buf[offset];
|
|
280
|
+
const payload = buf.subarray(offset + 8, offset + 8 + payloadLen);
|
|
281
|
+
if (streamType === 2) {
|
|
282
|
+
stderrBufs.push(payload);
|
|
283
|
+
} else {
|
|
284
|
+
stdoutBufs.push(payload);
|
|
285
|
+
}
|
|
286
|
+
offset += 8 + payloadLen;
|
|
287
|
+
}
|
|
288
|
+
pending = offset < buf.length ? buf.subarray(offset) : Buffer.alloc(0);
|
|
289
|
+
});
|
|
290
|
+
stream.on("end", () => {
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
resolve2({
|
|
293
|
+
stdout: Buffer.concat(stdoutBufs).toString("utf-8"),
|
|
294
|
+
stderr: Buffer.concat(stderrBufs).toString("utf-8")
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
stream.on("error", (err) => {
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
reject(err);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/virtual/docker-sandbox.ts
|
|
305
|
+
function DockerSandbox(opts) {
|
|
306
|
+
if (opts.container) {
|
|
307
|
+
const c = opts.container;
|
|
308
|
+
return {
|
|
309
|
+
fs: new DockerFs({ container: c, workingDir: opts.cwd }),
|
|
310
|
+
computer: new DockerComputer({
|
|
311
|
+
container: c,
|
|
312
|
+
defaultCwd: opts.cwd,
|
|
313
|
+
defaultTimeout: opts.defaultTimeout
|
|
314
|
+
}),
|
|
315
|
+
sandboxId: () => c.id
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (!opts.image) {
|
|
319
|
+
throw new Error("DockerSandbox requires either `container` or `image`");
|
|
320
|
+
}
|
|
321
|
+
const fsProxy = createFsProxy();
|
|
322
|
+
const computerProxy = createComputerProxy();
|
|
323
|
+
let containerId;
|
|
324
|
+
let containerRef;
|
|
325
|
+
let autoCreated = false;
|
|
326
|
+
let initPromise = null;
|
|
327
|
+
async function doInit(reconnectId) {
|
|
328
|
+
const Docker = (await import("dockerode")).default;
|
|
329
|
+
const docker = new Docker();
|
|
330
|
+
let container;
|
|
331
|
+
if (reconnectId) {
|
|
332
|
+
container = docker.getContainer(reconnectId);
|
|
333
|
+
try {
|
|
334
|
+
await container.inspect();
|
|
335
|
+
} catch {
|
|
336
|
+
container = await docker.createContainer({
|
|
337
|
+
Image: opts.image,
|
|
338
|
+
Cmd: opts.cmd ?? ["sleep", "infinity"],
|
|
339
|
+
Env: opts.env,
|
|
340
|
+
Tty: false,
|
|
341
|
+
...opts.dockerOptions
|
|
342
|
+
});
|
|
343
|
+
await container.start();
|
|
344
|
+
autoCreated = true;
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
container = await docker.createContainer({
|
|
348
|
+
Image: opts.image,
|
|
349
|
+
Cmd: opts.cmd ?? ["sleep", "infinity"],
|
|
350
|
+
Env: opts.env,
|
|
351
|
+
Tty: false,
|
|
352
|
+
...opts.dockerOptions
|
|
353
|
+
});
|
|
354
|
+
await container.start();
|
|
355
|
+
autoCreated = true;
|
|
356
|
+
}
|
|
357
|
+
containerRef = container;
|
|
358
|
+
containerId = container.id;
|
|
359
|
+
fsProxy.setTarget(new DockerFs({ container, workingDir: opts.cwd }));
|
|
360
|
+
computerProxy.setTarget(new DockerComputer({
|
|
361
|
+
container,
|
|
362
|
+
defaultCwd: opts.cwd,
|
|
363
|
+
defaultTimeout: opts.defaultTimeout
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
fs: fsProxy,
|
|
368
|
+
computer: computerProxy,
|
|
369
|
+
sandboxId: () => containerId,
|
|
370
|
+
init(sandboxId) {
|
|
371
|
+
if (!initPromise) {
|
|
372
|
+
initPromise = doInit(sandboxId).catch((err) => {
|
|
373
|
+
initPromise = null;
|
|
374
|
+
throw err;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return initPromise;
|
|
378
|
+
},
|
|
379
|
+
async dispose() {
|
|
380
|
+
if (initPromise) {
|
|
381
|
+
await initPromise.catch(() => {
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (!autoCreated || !containerRef) return;
|
|
385
|
+
try {
|
|
386
|
+
await containerRef.stop();
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
await containerRef.remove();
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
export {
|
|
397
|
+
DockerComputer,
|
|
398
|
+
DockerFs,
|
|
399
|
+
DockerSandbox
|
|
400
|
+
};
|
|
401
|
+
//# sourceMappingURL=docker.js.map
|