@cofy-x/axern-sdk 0.2.0-bootstrap.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 +169 -0
- package/dist/client/index.d.ts +63 -0
- package/dist/client/index.js +213 -0
- package/dist/config/index.d.ts +15 -0
- package/dist/config/index.js +65 -0
- package/dist/errors/index.d.ts +46 -0
- package/dist/errors/index.js +123 -0
- package/dist/generated/proto.d.ts +10 -0
- package/dist/generated/proto.js +80 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +13 -0
- package/dist/node/archive.d.ts +9 -0
- package/dist/node/archive.js +41 -0
- package/dist/node/attached_process.d.ts +10 -0
- package/dist/node/attached_process.js +60 -0
- package/dist/node/capabilities.d.ts +8 -0
- package/dist/node/capabilities.js +51 -0
- package/dist/node/client.d.ts +44 -0
- package/dist/node/client.js +96 -0
- package/dist/node/computer_use.d.ts +12 -0
- package/dist/node/computer_use.js +81 -0
- package/dist/node/context.d.ts +22 -0
- package/dist/node/context.js +37 -0
- package/dist/node/exec.d.ts +12 -0
- package/dist/node/exec.js +93 -0
- package/dist/node/files.d.ts +18 -0
- package/dist/node/files.js +101 -0
- package/dist/node/process.d.ts +36 -0
- package/dist/node/process.js +144 -0
- package/dist/node/streams.d.ts +8 -0
- package/dist/node/streams.js +70 -0
- package/dist/proto/axern/common/file/v1/file.proto +31 -0
- package/dist/proto/axern/control/admin/v1/allocation_lifecycle.proto +91 -0
- package/dist/proto/axern/control/admin/v1/audit.proto +53 -0
- package/dist/proto/axern/control/admin/v1/node.proto +49 -0
- package/dist/proto/axern/control/admin/v1/reliability.proto +184 -0
- package/dist/proto/axern/control/admin/v1/service.proto +18 -0
- package/dist/proto/axern/control/admin/v1/storage.proto +81 -0
- package/dist/proto/axern/control/agentprofile/v1/agent_profile.proto +169 -0
- package/dist/proto/axern/control/catalog/v1/catalog.proto +127 -0
- package/dist/proto/axern/control/common/v1/common.proto +171 -0
- package/dist/proto/axern/control/environment/v1/environment.proto +94 -0
- package/dist/proto/axern/control/function/v1/function.proto +130 -0
- package/dist/proto/axern/control/function/v1/function_types.proto +233 -0
- package/dist/proto/axern/control/gateway/v1/gateway.proto +60 -0
- package/dist/proto/axern/control/namespace/v1/namespace.proto +52 -0
- package/dist/proto/axern/control/node/v1/node_control.proto +302 -0
- package/dist/proto/axern/control/quota/v1/quota.proto +113 -0
- package/dist/proto/axern/control/rollout/v1/rollout.proto +344 -0
- package/dist/proto/axern/control/run/v1/run.proto +88 -0
- package/dist/proto/axern/control/secret/v1/secret.proto +75 -0
- package/dist/proto/axern/control/service/v1/service.proto +96 -0
- package/dist/proto/axern/control/service/v1/service_event.proto +47 -0
- package/dist/proto/axern/control/service/v1/service_replica.proto +74 -0
- package/dist/proto/axern/control/service/v1/service_types.proto +155 -0
- package/dist/proto/axern/control/storage/v1/storage.proto +107 -0
- package/dist/proto/axern/control/storage/v1/storage_types.proto +125 -0
- package/dist/proto/axern/control/tunnel/v1/tunnel.proto +195 -0
- package/dist/proto/axern/data/artifact/v1/artifact.proto +19 -0
- package/dist/proto/axern/node/sandbox/v1/node.proto +652 -0
- package/dist/proto/axern/tunnel/v1/tunnel.proto +50 -0
- package/dist/resources.d.ts +13 -0
- package/dist/resources.js +107 -0
- package/dist/sandbox/archive.d.ts +8 -0
- package/dist/sandbox/archive.js +228 -0
- package/dist/sandbox/index.d.ts +86 -0
- package/dist/sandbox/index.js +204 -0
- package/dist/sandbox/lifecycle.d.ts +11 -0
- package/dist/sandbox/lifecycle.js +57 -0
- package/dist/tunnel/connector.d.ts +34 -0
- package/dist/tunnel/connector.js +141 -0
- package/dist/tunnel/control.d.ts +22 -0
- package/dist/tunnel/control.js +91 -0
- package/dist/tunnel/debug.d.ts +6 -0
- package/dist/tunnel/debug.js +19 -0
- package/dist/tunnel/relay.d.ts +17 -0
- package/dist/tunnel/relay.js +41 -0
- package/dist/tunnel/runtime.d.ts +34 -0
- package/dist/tunnel/runtime.js +118 -0
- package/dist/tunnel/session.d.ts +25 -0
- package/dist/tunnel/session.js +95 -0
- package/dist/tunnel/target.d.ts +7 -0
- package/dist/tunnel/target.js +28 -0
- package/dist/tunnel/types.d.ts +25 -0
- package/dist/tunnel/types.js +6 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.js +8 -0
- package/dist/validation.d.ts +10 -0
- package/dist/validation.js +37 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.js +9 -0
- package/package.json +57 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 cofy-x
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { SandboxValidationError } from "./errors/index.js";
|
|
7
|
+
export function buildResourceSpec(options) {
|
|
8
|
+
const requests = resourceQuantity("requestCpu", options.requestCpu, "requestMemory", options.requestMemory);
|
|
9
|
+
const limits = resourceQuantity("limitCpu", options.limitCpu, "limitMemory", options.limitMemory);
|
|
10
|
+
if (requests === undefined && limits === undefined) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return { requests, limits };
|
|
14
|
+
}
|
|
15
|
+
function resourceQuantity(cpuName, cpuValue, memoryName, memoryValue) {
|
|
16
|
+
const cpu = parseCpuQuantity(cpuName, cpuValue);
|
|
17
|
+
const memory = parseMemoryQuantity(memoryName, memoryValue);
|
|
18
|
+
if (cpu <= 0 && memory <= 0) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
cpu_milli: cpu,
|
|
23
|
+
memory_bytes: memory,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function parseCpuQuantity(name, value) {
|
|
27
|
+
const quantity = resourceQuantityText(name, value);
|
|
28
|
+
if (quantity === "") {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
if (quantity.startsWith("-")) {
|
|
32
|
+
throw new SandboxValidationError(`${name} must be non-negative`);
|
|
33
|
+
}
|
|
34
|
+
if (quantity.endsWith("m")) {
|
|
35
|
+
return parseScaledDecimal(name, quantity.slice(0, -1), 1n);
|
|
36
|
+
}
|
|
37
|
+
return parseScaledDecimal(name, quantity, 1000n);
|
|
38
|
+
}
|
|
39
|
+
function parseMemoryQuantity(name, value) {
|
|
40
|
+
const quantity = resourceQuantityText(name, value);
|
|
41
|
+
if (quantity === "") {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
if (quantity.startsWith("-")) {
|
|
45
|
+
throw new SandboxValidationError(`${name} must be non-negative`);
|
|
46
|
+
}
|
|
47
|
+
const [number, factor] = splitMemoryQuantity(quantity);
|
|
48
|
+
return parseScaledDecimal(name, number, factor);
|
|
49
|
+
}
|
|
50
|
+
function resourceQuantityText(name, value) {
|
|
51
|
+
if (value === undefined) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "number") {
|
|
55
|
+
if (!Number.isFinite(value)) {
|
|
56
|
+
throw new SandboxValidationError(`${name} must be a resource quantity`);
|
|
57
|
+
}
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
return value.trim();
|
|
61
|
+
}
|
|
62
|
+
function splitMemoryQuantity(value) {
|
|
63
|
+
const units = [
|
|
64
|
+
["tib", 1024n ** 4n],
|
|
65
|
+
["gib", 1024n ** 3n],
|
|
66
|
+
["mib", 1024n ** 2n],
|
|
67
|
+
["kib", 1024n],
|
|
68
|
+
["ti", 1024n ** 4n],
|
|
69
|
+
["gi", 1024n ** 3n],
|
|
70
|
+
["mi", 1024n ** 2n],
|
|
71
|
+
["ki", 1024n],
|
|
72
|
+
["tb", 1000n ** 4n],
|
|
73
|
+
["gb", 1000n ** 3n],
|
|
74
|
+
["mb", 1000n ** 2n],
|
|
75
|
+
["kb", 1000n],
|
|
76
|
+
["b", 1n],
|
|
77
|
+
];
|
|
78
|
+
const lower = value.toLowerCase();
|
|
79
|
+
for (const [suffix, factor] of units) {
|
|
80
|
+
if (lower.endsWith(suffix)) {
|
|
81
|
+
return [value.slice(0, -suffix.length).trim(), factor];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [value, 1n];
|
|
85
|
+
}
|
|
86
|
+
function parseScaledDecimal(name, value, scale) {
|
|
87
|
+
const quantity = value.trim();
|
|
88
|
+
if (quantity === "") {
|
|
89
|
+
throw new SandboxValidationError(`${name} is required`);
|
|
90
|
+
}
|
|
91
|
+
const match = quantity.match(/^(\+)?(\d+|\d*\.\d+)$/);
|
|
92
|
+
if (match === null) {
|
|
93
|
+
throw new SandboxValidationError(`${name} must be a decimal number`);
|
|
94
|
+
}
|
|
95
|
+
const [wholePart, fractionPart = ""] = quantity.replace(/^\+/, "").split(".");
|
|
96
|
+
const whole = wholePart === "" ? "0" : wholePart;
|
|
97
|
+
const numerator = BigInt(`${whole}${fractionPart}`) * scale;
|
|
98
|
+
const denominator = 10n ** BigInt(fractionPart.length);
|
|
99
|
+
if (numerator % denominator !== 0n) {
|
|
100
|
+
throw new SandboxValidationError(`${name} must resolve to a whole unit`);
|
|
101
|
+
}
|
|
102
|
+
const parsed = numerator / denominator;
|
|
103
|
+
if (parsed > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
104
|
+
throw new SandboxValidationError(`${name} is too large`);
|
|
105
|
+
}
|
|
106
|
+
return Number(parsed);
|
|
107
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 cofy-x
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
export declare const defaultArchiveChunkSize: number;
|
|
7
|
+
export declare function directoryArchiveChunks(localPath: string, chunkSize?: number): AsyncIterable<Buffer>;
|
|
8
|
+
export declare function extractDirectoryArchive(chunks: AsyncIterable<Buffer | Uint8Array>, localPath: string, overwrite?: boolean): Promise<void>;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 cofy-x
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
7
|
+
import { mkdir, opendir, readdir, lstat } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { Readable } from "node:stream";
|
|
10
|
+
import { pipeline } from "node:stream/promises";
|
|
11
|
+
import * as tar from "tar-stream";
|
|
12
|
+
export const defaultArchiveChunkSize = 1024 * 1024;
|
|
13
|
+
export async function* directoryArchiveChunks(localPath, chunkSize = defaultArchiveChunkSize) {
|
|
14
|
+
const pack = tar.pack();
|
|
15
|
+
const producer = writeDirectoryArchive(pack, localPath);
|
|
16
|
+
producer.catch((error) => pack.destroy(error instanceof Error ? error : new Error(String(error))));
|
|
17
|
+
try {
|
|
18
|
+
for await (const chunk of pack) {
|
|
19
|
+
yield* splitChunk(Buffer.from(chunk), chunkSize);
|
|
20
|
+
}
|
|
21
|
+
await producer;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
await producer.catch(() => undefined);
|
|
25
|
+
pack.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function extractDirectoryArchive(chunks, localPath, overwrite = true) {
|
|
30
|
+
if (!overwrite && !(await localPathMissingOrEmpty(localPath))) {
|
|
31
|
+
throw new Error(`local path ${localPath} already exists and is not empty`);
|
|
32
|
+
}
|
|
33
|
+
await ensureNoLocalSymlink(localPath);
|
|
34
|
+
const rootAbs = path.resolve(localPath);
|
|
35
|
+
await mkdir(rootAbs, { recursive: true, mode: 0o755 });
|
|
36
|
+
const extract = tar.extract();
|
|
37
|
+
const input = Readable.from(chunks);
|
|
38
|
+
let entryError;
|
|
39
|
+
extract.on("entry", (header, stream, next) => {
|
|
40
|
+
void (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const target = safeTarTarget(rootAbs, header.name);
|
|
43
|
+
if (target === rootAbs && header.type !== "directory") {
|
|
44
|
+
throw new Error(`archive root entry ${header.name} must be a directory`);
|
|
45
|
+
}
|
|
46
|
+
if (header.type === "symlink" || header.type === "link") {
|
|
47
|
+
throw new Error(`archive entry ${header.name} uses unsupported link type`);
|
|
48
|
+
}
|
|
49
|
+
if (header.type === "directory") {
|
|
50
|
+
await ensureNoLocalSymlinkPath(rootAbs, target);
|
|
51
|
+
await mkdir(target, { recursive: true, mode: (header.mode ?? 0o755) & 0o777 });
|
|
52
|
+
stream.resume();
|
|
53
|
+
next();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (header.type !== "file" && header.type !== "contiguous-file" && header.type !== undefined && header.type !== null) {
|
|
57
|
+
throw new Error(`archive entry ${header.name} has unsupported type ${header.type}`);
|
|
58
|
+
}
|
|
59
|
+
if (!overwrite) {
|
|
60
|
+
await ensureMissing(target);
|
|
61
|
+
}
|
|
62
|
+
const parent = path.dirname(target);
|
|
63
|
+
await ensureNoLocalSymlinkPath(rootAbs, parent);
|
|
64
|
+
await mkdir(parent, { recursive: true, mode: 0o755 });
|
|
65
|
+
await ensureNoLocalSymlink(target);
|
|
66
|
+
await pipeline(stream, createWriteStream(target, { mode: (header.mode ?? 0o644) & 0o777 }));
|
|
67
|
+
next();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
entryError = error;
|
|
71
|
+
extract.destroy();
|
|
72
|
+
stream.resume();
|
|
73
|
+
next();
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
await pipeline(input, extract);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw entryError ?? error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function writeDirectoryArchive(pack, root) {
|
|
85
|
+
try {
|
|
86
|
+
const info = await lstat(root);
|
|
87
|
+
if (info.isSymbolicLink()) {
|
|
88
|
+
throw new Error(`local path ${root} is a symlink`);
|
|
89
|
+
}
|
|
90
|
+
if (!info.isDirectory()) {
|
|
91
|
+
throw new Error(`local path ${root} is not a directory`);
|
|
92
|
+
}
|
|
93
|
+
await addDirectoryEntries(pack, root, root);
|
|
94
|
+
pack.finalize();
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
pack.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function addDirectoryEntries(pack, root, current) {
|
|
102
|
+
const dir = await opendir(current);
|
|
103
|
+
for await (const entry of dir) {
|
|
104
|
+
const fullPath = path.join(current, entry.name);
|
|
105
|
+
const info = await lstat(fullPath);
|
|
106
|
+
if (info.isSymbolicLink()) {
|
|
107
|
+
throw new Error(`local symlink ${fullPath} is not supported`);
|
|
108
|
+
}
|
|
109
|
+
const name = path.relative(root, fullPath).split(path.sep).join("/");
|
|
110
|
+
if (info.isDirectory()) {
|
|
111
|
+
await entryHeader(pack, {
|
|
112
|
+
name: name.endsWith("/") ? name : `${name}/`,
|
|
113
|
+
type: "directory",
|
|
114
|
+
mode: info.mode & 0o777,
|
|
115
|
+
mtime: info.mtime,
|
|
116
|
+
});
|
|
117
|
+
await addDirectoryEntries(pack, root, fullPath);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!info.isFile()) {
|
|
121
|
+
throw new Error(`local path ${fullPath} is not a regular file`);
|
|
122
|
+
}
|
|
123
|
+
const tarEntry = pack.entry({
|
|
124
|
+
name,
|
|
125
|
+
type: "file",
|
|
126
|
+
size: info.size,
|
|
127
|
+
mode: info.mode & 0o777,
|
|
128
|
+
mtime: info.mtime,
|
|
129
|
+
});
|
|
130
|
+
await pipeline(createReadStream(fullPath), tarEntry);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function entryHeader(pack, header) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
pack.entry(header, (error) => {
|
|
136
|
+
if (error) {
|
|
137
|
+
reject(error);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
resolve();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function* splitChunk(chunk, chunkSize) {
|
|
146
|
+
const size = chunkSize > 0 ? chunkSize : defaultArchiveChunkSize;
|
|
147
|
+
for (let offset = 0; offset < chunk.length; offset += size) {
|
|
148
|
+
yield chunk.subarray(offset, Math.min(offset + size, chunk.length));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function safeTarTarget(rootAbs, name) {
|
|
152
|
+
if (name.length === 0) {
|
|
153
|
+
throw new Error("archive entry path is empty");
|
|
154
|
+
}
|
|
155
|
+
const clean = path.posix.normalize(name);
|
|
156
|
+
const rootEntry = clean === "." && (name === "." || name === "./");
|
|
157
|
+
if (path.posix.isAbsolute(name) || (clean === "." && !rootEntry) || clean === ".." || clean.startsWith("../")) {
|
|
158
|
+
throw new Error(`archive entry ${name} escapes target directory`);
|
|
159
|
+
}
|
|
160
|
+
if (rootEntry) {
|
|
161
|
+
return rootAbs;
|
|
162
|
+
}
|
|
163
|
+
const target = path.resolve(rootAbs, ...clean.split("/"));
|
|
164
|
+
const rel = path.relative(rootAbs, target);
|
|
165
|
+
if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
166
|
+
throw new Error(`archive entry ${name} escapes target directory`);
|
|
167
|
+
}
|
|
168
|
+
return target;
|
|
169
|
+
}
|
|
170
|
+
async function ensureMissing(target) {
|
|
171
|
+
try {
|
|
172
|
+
await lstat(target);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (isNodeError(error, "ENOENT")) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`local path ${target} already exists`);
|
|
181
|
+
}
|
|
182
|
+
async function ensureNoLocalSymlink(target) {
|
|
183
|
+
try {
|
|
184
|
+
const info = await lstat(target);
|
|
185
|
+
if (info.isSymbolicLink()) {
|
|
186
|
+
throw new Error(`local path ${target} is a symlink`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (isNodeError(error, "ENOENT")) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function ensureNoLocalSymlinkPath(rootAbs, targetAbs) {
|
|
197
|
+
const rel = path.relative(rootAbs, targetAbs);
|
|
198
|
+
if (rel === "") {
|
|
199
|
+
await ensureNoLocalSymlink(rootAbs);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
let current = rootAbs;
|
|
203
|
+
for (const part of rel.split(path.sep)) {
|
|
204
|
+
current = path.join(current, part);
|
|
205
|
+
await ensureNoLocalSymlink(current);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function localPathMissingOrEmpty(localPath) {
|
|
209
|
+
try {
|
|
210
|
+
const info = await lstat(localPath);
|
|
211
|
+
if (info.isSymbolicLink()) {
|
|
212
|
+
throw new Error(`local path ${localPath} is a symlink`);
|
|
213
|
+
}
|
|
214
|
+
if (!info.isDirectory()) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
return (await readdir(localPath)).length === 0;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (isNodeError(error, "ENOENT")) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function isNodeError(error, code) {
|
|
227
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
228
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 cofy-x
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { AxernClient } from "../client/index.js";
|
|
7
|
+
import type { SandboxProcess } from "../node/process.js";
|
|
8
|
+
import type { ResourceQuantity } from "../resources.js";
|
|
9
|
+
import type { ChmodOptions, CapabilityStatus, Command, ComputerUseDisplay, ComputerUseKeyboardOptions, ComputerUseMouseOptions, ComputerUseScreenshot, ComputerUseScreenshotOptions, ComputerUseStatus, CopyOptions, DownloadDirOptions, ExecOptions, ExecResult, ImageExecOptions, ImageProcessOptions, MkdirOptions, MoveOptions, NodeCallOptions, ProcessOptions, RemoveOptions, SandboxFileInfo, TunnelMetadata, TunnelOptions, TouchOptions, UploadDirOptions, VolumeMount, WriteFileOptions } from "../types.js";
|
|
10
|
+
export interface SandboxOptions {
|
|
11
|
+
client: AxernClient;
|
|
12
|
+
templateId?: string;
|
|
13
|
+
image?: string;
|
|
14
|
+
environmentId?: string;
|
|
15
|
+
namespace?: string;
|
|
16
|
+
argv?: string[];
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
runtimeClass?: string;
|
|
20
|
+
volumes?: readonly VolumeMount[];
|
|
21
|
+
requestCpu?: ResourceQuantity;
|
|
22
|
+
requestMemory?: ResourceQuantity;
|
|
23
|
+
limitCpu?: ResourceQuantity;
|
|
24
|
+
limitMemory?: ResourceQuantity;
|
|
25
|
+
readyTimeoutMs?: number;
|
|
26
|
+
labels?: Record<string, string>;
|
|
27
|
+
registryCredentialId?: string;
|
|
28
|
+
rootfsReadonly?: boolean;
|
|
29
|
+
tunnel?: TunnelOptions;
|
|
30
|
+
}
|
|
31
|
+
export interface SandboxState {
|
|
32
|
+
environmentId: string;
|
|
33
|
+
serviceId: string;
|
|
34
|
+
allocationId: string;
|
|
35
|
+
nodeId: string;
|
|
36
|
+
attempt: number;
|
|
37
|
+
startedAt: Date;
|
|
38
|
+
}
|
|
39
|
+
export interface SandboxMetadata extends SandboxState {
|
|
40
|
+
namespace: string;
|
|
41
|
+
runtimeClass: string;
|
|
42
|
+
labels: Record<string, string>;
|
|
43
|
+
source: "template" | "image" | "environment";
|
|
44
|
+
tunnel?: TunnelMetadata;
|
|
45
|
+
}
|
|
46
|
+
export declare class Sandbox {
|
|
47
|
+
private readonly client;
|
|
48
|
+
private readonly options;
|
|
49
|
+
private createdEnvironment;
|
|
50
|
+
private environmentId;
|
|
51
|
+
private serviceId;
|
|
52
|
+
private currentState?;
|
|
53
|
+
private currentMetadata?;
|
|
54
|
+
private tunnelRuntime?;
|
|
55
|
+
constructor(options: SandboxOptions);
|
|
56
|
+
get state(): SandboxState;
|
|
57
|
+
get metadata(): SandboxMetadata;
|
|
58
|
+
start(): Promise<this>;
|
|
59
|
+
close(): Promise<void>;
|
|
60
|
+
exec(command: Command, options?: ExecOptions): Promise<ExecResult>;
|
|
61
|
+
process(command: Command, options?: ProcessOptions): Promise<SandboxProcess>;
|
|
62
|
+
execImage(image: string, command: Command, options?: ImageExecOptions): Promise<ExecResult>;
|
|
63
|
+
processImage(image: string, command: Command, options?: ImageProcessOptions): Promise<SandboxProcess>;
|
|
64
|
+
capabilityStatus(options?: NodeCallOptions): Promise<CapabilityStatus>;
|
|
65
|
+
computerUseStatus(options?: NodeCallOptions): Promise<ComputerUseStatus>;
|
|
66
|
+
computerUseScreenshot(options?: ComputerUseScreenshotOptions): Promise<ComputerUseScreenshot>;
|
|
67
|
+
computerUseDisplay(options?: NodeCallOptions): Promise<ComputerUseDisplay>;
|
|
68
|
+
computerUseMouse(options?: ComputerUseMouseOptions): Promise<void>;
|
|
69
|
+
computerUseKeyboard(options?: ComputerUseKeyboardOptions): Promise<void>;
|
|
70
|
+
stat(path: string): Promise<SandboxFileInfo>;
|
|
71
|
+
listDir(path: string): Promise<SandboxFileInfo[]>;
|
|
72
|
+
exists(path: string): Promise<boolean>;
|
|
73
|
+
readFile(path: string): Promise<Buffer>;
|
|
74
|
+
readText(path: string, encoding?: BufferEncoding): Promise<string>;
|
|
75
|
+
writeFile(path: string, data: Buffer | Uint8Array | string, options?: WriteFileOptions): Promise<void>;
|
|
76
|
+
writeText(path: string, data: string, options?: WriteFileOptions): Promise<void>;
|
|
77
|
+
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
78
|
+
remove(path: string, options?: RemoveOptions): Promise<void>;
|
|
79
|
+
copy(srcPath: string, dstPath: string, options?: CopyOptions): Promise<void>;
|
|
80
|
+
move(srcPath: string, dstPath: string, options?: MoveOptions): Promise<void>;
|
|
81
|
+
chmod(path: string, mode: number, options?: ChmodOptions): Promise<void>;
|
|
82
|
+
touch(path: string, options?: TouchOptions): Promise<void>;
|
|
83
|
+
uploadDir(localPath: string, remotePath: string, options?: UploadDirOptions): Promise<void>;
|
|
84
|
+
downloadDir(remotePath: string, localPath: string, options?: DownloadDirOptions): Promise<void>;
|
|
85
|
+
private nodeClient;
|
|
86
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 cofy-x
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { SandboxStateError } from "../errors/index.js";
|
|
7
|
+
import { startTunnelRuntime, tunnelMetadata } from "../tunnel/runtime.js";
|
|
8
|
+
import { directoryArchiveChunks, extractDirectoryArchive } from "./archive.js";
|
|
9
|
+
import { defaultSandboxArgv, sandboxLabels, sandboxMetadata, validateSandboxOptions, waitReadyReplica } from "./lifecycle.js";
|
|
10
|
+
export class Sandbox {
|
|
11
|
+
client;
|
|
12
|
+
options;
|
|
13
|
+
createdEnvironment = false;
|
|
14
|
+
environmentId = "";
|
|
15
|
+
serviceId = "";
|
|
16
|
+
currentState;
|
|
17
|
+
currentMetadata;
|
|
18
|
+
tunnelRuntime;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
validateSandboxOptions(options);
|
|
21
|
+
this.client = options.client;
|
|
22
|
+
this.options = options;
|
|
23
|
+
}
|
|
24
|
+
get state() {
|
|
25
|
+
if (this.currentState === undefined) {
|
|
26
|
+
throw new SandboxStateError("sandbox is not started");
|
|
27
|
+
}
|
|
28
|
+
return this.currentState;
|
|
29
|
+
}
|
|
30
|
+
get metadata() {
|
|
31
|
+
if (this.currentMetadata === undefined) {
|
|
32
|
+
throw new SandboxStateError("sandbox is not started");
|
|
33
|
+
}
|
|
34
|
+
return this.currentMetadata;
|
|
35
|
+
}
|
|
36
|
+
async start() {
|
|
37
|
+
if (this.currentState !== undefined) {
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
let environmentId = this.options.environmentId ?? "";
|
|
41
|
+
try {
|
|
42
|
+
if (environmentId === "") {
|
|
43
|
+
const environment = await this.client.createEnvironment({
|
|
44
|
+
namespace: this.options.namespace,
|
|
45
|
+
templateId: this.options.templateId,
|
|
46
|
+
image: this.options.image,
|
|
47
|
+
registryCredentialId: this.options.registryCredentialId,
|
|
48
|
+
rootfsReadonly: this.options.rootfsReadonly,
|
|
49
|
+
labels: sandboxLabels(this.options.labels),
|
|
50
|
+
});
|
|
51
|
+
environmentId = String(environment.id ?? "");
|
|
52
|
+
this.environmentId = environmentId;
|
|
53
|
+
this.createdEnvironment = true;
|
|
54
|
+
}
|
|
55
|
+
const service = await this.client.createService({
|
|
56
|
+
namespace: this.options.namespace,
|
|
57
|
+
environmentId,
|
|
58
|
+
argv: this.options.argv ?? defaultSandboxArgv,
|
|
59
|
+
env: this.options.env,
|
|
60
|
+
cwd: this.options.cwd,
|
|
61
|
+
runtimeClass: this.options.runtimeClass,
|
|
62
|
+
volumes: this.options.volumes,
|
|
63
|
+
requestCpu: this.options.requestCpu,
|
|
64
|
+
requestMemory: this.options.requestMemory,
|
|
65
|
+
limitCpu: this.options.limitCpu,
|
|
66
|
+
limitMemory: this.options.limitMemory,
|
|
67
|
+
labels: sandboxLabels(this.options.labels),
|
|
68
|
+
});
|
|
69
|
+
this.serviceId = String(service.id ?? "");
|
|
70
|
+
const replica = await waitReadyReplica(this.serviceId, this.options.readyTimeoutMs ?? 180_000, (serviceId) => this.client.listServiceReplicas(serviceId));
|
|
71
|
+
this.currentState = {
|
|
72
|
+
environmentId,
|
|
73
|
+
serviceId: this.serviceId,
|
|
74
|
+
allocationId: String(replica.id ?? ""),
|
|
75
|
+
nodeId: String(replica.node_id ?? ""),
|
|
76
|
+
attempt: Number(replica.attempt ?? 0),
|
|
77
|
+
startedAt: new Date(),
|
|
78
|
+
};
|
|
79
|
+
this.currentMetadata = sandboxMetadata(this.options, this.currentState);
|
|
80
|
+
if (this.options.tunnel !== undefined) {
|
|
81
|
+
this.tunnelRuntime = await startTunnelRuntime({
|
|
82
|
+
control: this.client.tunnelClient(),
|
|
83
|
+
allocationId: this.currentState.allocationId,
|
|
84
|
+
tunnel: this.options.tunnel,
|
|
85
|
+
transport: this.client.tunnelTransport(),
|
|
86
|
+
});
|
|
87
|
+
this.currentMetadata = {
|
|
88
|
+
...this.currentMetadata,
|
|
89
|
+
tunnel: tunnelMetadata(this.tunnelRuntime),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
await this.close();
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async close() {
|
|
100
|
+
const serviceId = this.serviceId;
|
|
101
|
+
const tunnelRuntime = this.tunnelRuntime;
|
|
102
|
+
this.serviceId = "";
|
|
103
|
+
this.tunnelRuntime = undefined;
|
|
104
|
+
this.currentState = undefined;
|
|
105
|
+
this.currentMetadata = undefined;
|
|
106
|
+
if (tunnelRuntime !== undefined) {
|
|
107
|
+
await tunnelRuntime.stop().catch(() => undefined);
|
|
108
|
+
}
|
|
109
|
+
if (serviceId !== "") {
|
|
110
|
+
await this.client.deleteService(serviceId).catch(() => undefined);
|
|
111
|
+
}
|
|
112
|
+
if (this.createdEnvironment && this.environmentId !== "") {
|
|
113
|
+
const environmentId = this.environmentId;
|
|
114
|
+
this.environmentId = "";
|
|
115
|
+
this.createdEnvironment = false;
|
|
116
|
+
await this.client.deleteEnvironment(environmentId).catch(() => undefined);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async exec(command, options = {}) {
|
|
120
|
+
return this.nodeClient().exec(command, options);
|
|
121
|
+
}
|
|
122
|
+
async process(command, options = {}) {
|
|
123
|
+
return this.nodeClient().process(command, options);
|
|
124
|
+
}
|
|
125
|
+
async execImage(image, command, options = {}) {
|
|
126
|
+
return this.nodeClient().execImage(image, command, options);
|
|
127
|
+
}
|
|
128
|
+
async processImage(image, command, options = {}) {
|
|
129
|
+
return this.nodeClient().processImage(image, command, options);
|
|
130
|
+
}
|
|
131
|
+
async capabilityStatus(options = {}) {
|
|
132
|
+
return this.nodeClient().capabilityStatus(options);
|
|
133
|
+
}
|
|
134
|
+
async computerUseStatus(options = {}) {
|
|
135
|
+
return this.nodeClient().computerUseStatus(options);
|
|
136
|
+
}
|
|
137
|
+
async computerUseScreenshot(options = {}) {
|
|
138
|
+
return this.nodeClient().computerUseScreenshot(options);
|
|
139
|
+
}
|
|
140
|
+
async computerUseDisplay(options = {}) {
|
|
141
|
+
return this.nodeClient().computerUseDisplay(options);
|
|
142
|
+
}
|
|
143
|
+
async computerUseMouse(options = {}) {
|
|
144
|
+
return this.nodeClient().computerUseMouse(options);
|
|
145
|
+
}
|
|
146
|
+
async computerUseKeyboard(options = {}) {
|
|
147
|
+
return this.nodeClient().computerUseKeyboard(options);
|
|
148
|
+
}
|
|
149
|
+
async stat(path) {
|
|
150
|
+
return this.nodeClient().stat(path);
|
|
151
|
+
}
|
|
152
|
+
async listDir(path) {
|
|
153
|
+
return this.nodeClient().listDir(path);
|
|
154
|
+
}
|
|
155
|
+
async exists(path) {
|
|
156
|
+
return this.nodeClient().exists(path);
|
|
157
|
+
}
|
|
158
|
+
async readFile(path) {
|
|
159
|
+
return this.nodeClient().readFile(path);
|
|
160
|
+
}
|
|
161
|
+
async readText(path, encoding = "utf8") {
|
|
162
|
+
return this.nodeClient().readText(path, encoding);
|
|
163
|
+
}
|
|
164
|
+
async writeFile(path, data, options = {}) {
|
|
165
|
+
return this.nodeClient().writeFile(path, data, options);
|
|
166
|
+
}
|
|
167
|
+
async writeText(path, data, options = {}) {
|
|
168
|
+
return this.nodeClient().writeText(path, data, options);
|
|
169
|
+
}
|
|
170
|
+
async mkdir(path, options = {}) {
|
|
171
|
+
return this.nodeClient().mkdir(path, options);
|
|
172
|
+
}
|
|
173
|
+
async remove(path, options = {}) {
|
|
174
|
+
return this.nodeClient().remove(path, options);
|
|
175
|
+
}
|
|
176
|
+
async copy(srcPath, dstPath, options = {}) {
|
|
177
|
+
return this.nodeClient().copy(srcPath, dstPath, options);
|
|
178
|
+
}
|
|
179
|
+
async move(srcPath, dstPath, options = {}) {
|
|
180
|
+
return this.nodeClient().move(srcPath, dstPath, options);
|
|
181
|
+
}
|
|
182
|
+
async chmod(path, mode, options = {}) {
|
|
183
|
+
return this.nodeClient().chmod(path, mode, options);
|
|
184
|
+
}
|
|
185
|
+
async touch(path, options = {}) {
|
|
186
|
+
return this.nodeClient().touch(path, options);
|
|
187
|
+
}
|
|
188
|
+
async uploadDir(localPath, remotePath, options = {}) {
|
|
189
|
+
await this.nodeClient().uploadArchive(remotePath, () => directoryArchiveChunks(localPath, options.chunkSize), {
|
|
190
|
+
createParents: options.createParents ?? true,
|
|
191
|
+
overwrite: options.overwrite ?? true,
|
|
192
|
+
rpcTimeoutMs: options.rpcTimeoutMs,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async downloadDir(remotePath, localPath, options = {}) {
|
|
196
|
+
await extractDirectoryArchive(this.nodeClient().downloadArchive(remotePath, { rpcTimeoutMs: options.rpcTimeoutMs }), localPath, options.overwrite ?? true);
|
|
197
|
+
}
|
|
198
|
+
nodeClient() {
|
|
199
|
+
if (this.currentState === undefined) {
|
|
200
|
+
throw new SandboxStateError("sandbox is not started");
|
|
201
|
+
}
|
|
202
|
+
return this.client.nodeSandbox(this.currentState.allocationId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 cofy-x
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import type { SandboxMetadata, SandboxOptions, SandboxState } from "./index.js";
|
|
7
|
+
export declare const defaultSandboxArgv: string[];
|
|
8
|
+
export declare function validateSandboxOptions(options: SandboxOptions): void;
|
|
9
|
+
export declare function sandboxLabels(labels: Record<string, string> | undefined): Record<string, string>;
|
|
10
|
+
export declare function sandboxMetadata(options: SandboxOptions, state: SandboxState): SandboxMetadata;
|
|
11
|
+
export declare function waitReadyReplica(serviceId: string, timeoutMs: number, listReplicas: (serviceId: string) => Promise<Record<string, unknown>[]>): Promise<Record<string, unknown>>;
|