@sema-agent/server 1.317.0 → 1.318.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/dist/plugins/posix-shell-fs.d.ts +35 -0
- package/dist/plugins/posix-shell-fs.js +122 -0
- package/dist/plugins/remote-env-adb.d.ts +2 -1
- package/dist/plugins/remote-env-adb.js +19 -108
- package/dist/plugins/remote-env-file-error.d.ts +1 -0
- package/dist/plugins/remote-env-file-error.js +16 -0
- package/dist/plugins/remote-env-k8s.d.ts +2 -2
- package/dist/plugins/remote-env-k8s.js +22 -97
- package/dist/plugins/remote-env-local-docker.d.ts +2 -2
- package/dist/plugins/remote-env-local-docker.js +32 -109
- package/dist/plugins/remote-env-ssh.d.ts +2 -1
- package/dist/plugins/remote-env-ssh.js +15 -61
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FileError, type ExecutionError, type RemoteExecutionError, type FileInfo, type Result } from "@sema-agent/core";
|
|
2
|
+
export interface ShellRunResult {
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PosixShellFsDeps {
|
|
8
|
+
run: (cmd: string, abortSignal?: AbortSignal) => Promise<Result<ShellRunResult, ExecutionError | RemoteExecutionError>>;
|
|
9
|
+
resolve: (p: string) => string;
|
|
10
|
+
tempDir: () => string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface PosixShellFs {
|
|
13
|
+
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
14
|
+
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
15
|
+
statFileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
16
|
+
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
17
|
+
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
18
|
+
createDir(p: string, options?: {
|
|
19
|
+
recursive?: boolean;
|
|
20
|
+
abortSignal?: AbortSignal;
|
|
21
|
+
}): Promise<Result<void, FileError>>;
|
|
22
|
+
remove(p: string, options?: {
|
|
23
|
+
recursive?: boolean;
|
|
24
|
+
force?: boolean;
|
|
25
|
+
abortSignal?: AbortSignal;
|
|
26
|
+
}): Promise<Result<void, FileError>>;
|
|
27
|
+
createTempDir(prefix?: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
28
|
+
createTempFile(options?: {
|
|
29
|
+
prefix?: string;
|
|
30
|
+
suffix?: string;
|
|
31
|
+
abortSignal?: AbortSignal;
|
|
32
|
+
}): Promise<Result<string, FileError>>;
|
|
33
|
+
}
|
|
34
|
+
export declare function createPosixShellFs(deps: PosixShellFsDeps): PosixShellFs;
|
|
35
|
+
//# sourceMappingURL=posix-shell-fs.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FileError, } from "@sema-agent/core";
|
|
3
|
+
import { shellQuote, kindFromMode } from "./remote-shell.js";
|
|
4
|
+
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
5
|
+
const ok = (value) => ({ ok: true, value });
|
|
6
|
+
export function createPosixShellFs(deps) {
|
|
7
|
+
const { run, resolve } = deps;
|
|
8
|
+
const tempResult = (r) => {
|
|
9
|
+
if (!r.ok)
|
|
10
|
+
return { ok: false, error: fileErrorFromExec(r.error, undefined) };
|
|
11
|
+
if (r.value.exitCode !== 0)
|
|
12
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, undefined, "unknown") };
|
|
13
|
+
return ok(r.value.stdout.trim());
|
|
14
|
+
};
|
|
15
|
+
const fs = {
|
|
16
|
+
async readLink(p, abortSignal) {
|
|
17
|
+
const abs = resolve(p);
|
|
18
|
+
const r = await run(`readlink -- ${shellQuote(abs)}`, abortSignal);
|
|
19
|
+
if (!r.ok)
|
|
20
|
+
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
21
|
+
if (r.value.exitCode !== 0)
|
|
22
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
|
|
23
|
+
return ok(r.value.stdout.trim());
|
|
24
|
+
},
|
|
25
|
+
async canonicalPath(p, abortSignal) {
|
|
26
|
+
const abs = resolve(p);
|
|
27
|
+
const r = await run(`realpath -- ${shellQuote(abs)}`, abortSignal);
|
|
28
|
+
if (!r.ok)
|
|
29
|
+
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
30
|
+
if (r.value.exitCode !== 0)
|
|
31
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
|
|
32
|
+
return ok(r.value.stdout.trim());
|
|
33
|
+
},
|
|
34
|
+
async statFileInfo(p, abortSignal) {
|
|
35
|
+
const abs = resolve(p);
|
|
36
|
+
const r = await run(`stat -c '%s %Y %f' -- ${shellQuote(abs)}`, abortSignal);
|
|
37
|
+
if (!r.ok)
|
|
38
|
+
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
39
|
+
if (r.value.exitCode !== 0)
|
|
40
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
|
|
41
|
+
const [sizeS, mtimeS, modeHex] = r.value.stdout.trim().split(/\s+/);
|
|
42
|
+
return ok({
|
|
43
|
+
name: path.posix.basename(abs),
|
|
44
|
+
path: abs,
|
|
45
|
+
kind: kindFromMode(parseInt(modeHex ?? "0", 16)),
|
|
46
|
+
size: Number(sizeS) || 0,
|
|
47
|
+
mtimeMs: (Number(mtimeS) || 0) * 1000,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
async listDir(p, abortSignal) {
|
|
51
|
+
const abs = resolve(p);
|
|
52
|
+
const r = await run(`ls -1A -- ${shellQuote(abs)}`, abortSignal);
|
|
53
|
+
if (!r.ok)
|
|
54
|
+
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
55
|
+
if (r.value.exitCode !== 0)
|
|
56
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
|
|
57
|
+
const names = r.value.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
const info = await fs.statFileInfo(path.posix.join(abs, name), abortSignal);
|
|
61
|
+
if (info.ok)
|
|
62
|
+
out.push(info.value);
|
|
63
|
+
else
|
|
64
|
+
out.push({ name, path: path.posix.join(abs, name), kind: "file", size: 0, mtimeMs: 0 });
|
|
65
|
+
}
|
|
66
|
+
return ok(out);
|
|
67
|
+
},
|
|
68
|
+
async exists(p, abortSignal) {
|
|
69
|
+
const abs = resolve(p);
|
|
70
|
+
const r = await run(`{ [ -e ${shellQuote(abs)} ] || [ -L ${shellQuote(abs)} ]; } && printf yes || printf no`, abortSignal);
|
|
71
|
+
if (!r.ok)
|
|
72
|
+
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
73
|
+
const out = r.value.stdout.trim();
|
|
74
|
+
if (out === "yes")
|
|
75
|
+
return ok(true);
|
|
76
|
+
if (out === "no")
|
|
77
|
+
return ok(false);
|
|
78
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "unknown") };
|
|
79
|
+
},
|
|
80
|
+
async createDir(p, options) {
|
|
81
|
+
const abs = resolve(p);
|
|
82
|
+
const r = await run(`mkdir -p -- ${shellQuote(abs)}`, options?.abortSignal);
|
|
83
|
+
if (!r.ok)
|
|
84
|
+
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
85
|
+
if (r.value.exitCode !== 0)
|
|
86
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "unknown") };
|
|
87
|
+
return ok(undefined);
|
|
88
|
+
},
|
|
89
|
+
async remove(p, options) {
|
|
90
|
+
const abs = resolve(p);
|
|
91
|
+
const flags = `${options?.recursive ? "r" : ""}${options?.force ? "f" : ""}`;
|
|
92
|
+
const r = await run(`rm ${flags ? `-${flags} ` : ""}-- ${shellQuote(abs)}`, options?.abortSignal);
|
|
93
|
+
if (!r.ok)
|
|
94
|
+
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
95
|
+
if (r.value.exitCode !== 0) {
|
|
96
|
+
if (options?.force && /no such file/i.test(r.value.stderr))
|
|
97
|
+
return ok(undefined);
|
|
98
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "unknown") };
|
|
99
|
+
}
|
|
100
|
+
return ok(undefined);
|
|
101
|
+
},
|
|
102
|
+
async createTempDir(prefix = "tmp-", abortSignal) {
|
|
103
|
+
const dir = deps.tempDir();
|
|
104
|
+
const where = dir === null ? "-t" : `-p ${shellQuote(dir)}`;
|
|
105
|
+
const r = await run(`mktemp -d ${where} ${shellQuote(`${prefix}XXXXXX`)}`, abortSignal);
|
|
106
|
+
return tempResult(r);
|
|
107
|
+
},
|
|
108
|
+
async createTempFile(options) {
|
|
109
|
+
const prefix = options?.prefix ?? "";
|
|
110
|
+
const sfx = options?.suffix ?? "";
|
|
111
|
+
const dir = deps.tempDir();
|
|
112
|
+
const where = dir === null ? "-t" : `-p ${shellQuote(dir)}`;
|
|
113
|
+
const cmd = sfx
|
|
114
|
+
? `f=$(mktemp ${where} ${shellQuote(`${prefix}XXXXXX`)}) && mv -- "$f" "$f"${shellQuote(sfx)} && printf '%s' "$f"${shellQuote(sfx)}`
|
|
115
|
+
: `mktemp ${where} ${shellQuote(`${prefix}XXXXXX`)}`;
|
|
116
|
+
const r = await run(cmd, options?.abortSignal);
|
|
117
|
+
return tempResult(r);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
return fs;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=posix-shell-fs.js.map
|
|
@@ -93,6 +93,8 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
93
93
|
}): Promise<Result<string[], FileError>>;
|
|
94
94
|
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
95
95
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
96
|
+
private posixFsInst?;
|
|
97
|
+
private get posixFs();
|
|
96
98
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
97
99
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
98
100
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
@@ -115,7 +117,6 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
115
117
|
}): Promise<Result<string, FileError>>;
|
|
116
118
|
private adb;
|
|
117
119
|
private withCwdEnv;
|
|
118
|
-
private tempResult;
|
|
119
120
|
}
|
|
120
121
|
export declare function adbExecutionEnvFactory(config: AdbEnvConfig): ExecutionEnvFactory;
|
|
121
122
|
export {};
|
|
@@ -2,9 +2,10 @@ import path from "node:path";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { shellQuote,
|
|
5
|
+
import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
6
6
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
7
7
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
8
|
+
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
8
9
|
const PROVIDER = "adb";
|
|
9
10
|
const ok = (value) => ({ ok: true, value });
|
|
10
11
|
const unsupported = (op) => ({
|
|
@@ -320,123 +321,40 @@ export class RemoteAdbExecutionEnv {
|
|
|
320
321
|
merged.set(add, base.length);
|
|
321
322
|
return this.writeFile(abs, merged, abortSignal);
|
|
322
323
|
}
|
|
324
|
+
posixFsInst;
|
|
325
|
+
get posixFs() {
|
|
326
|
+
return (this.posixFsInst ??= createPosixShellFs({
|
|
327
|
+
run: (cmd, abortSignal) => this.exec(cmd, { abortSignal }),
|
|
328
|
+
resolve: (p) => this.resolve(p),
|
|
329
|
+
tempDir: () => this.cwd,
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
323
332
|
async fileInfo(p, abortSignal) {
|
|
324
|
-
|
|
325
|
-
const r = await this.exec(`stat -c '%s %Y %f' ${shellQuote(abs)}`, { abortSignal });
|
|
326
|
-
if (!r.ok)
|
|
327
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
328
|
-
if (r.value.exitCode !== 0) {
|
|
329
|
-
const msg = r.value.stderr.trim();
|
|
330
|
-
if (/no such file/i.test(msg))
|
|
331
|
-
return { ok: false, error: new FileError("not_found", msg, abs) };
|
|
332
|
-
if (/permission denied/i.test(msg))
|
|
333
|
-
return { ok: false, error: new FileError("permission_denied", msg, abs) };
|
|
334
|
-
return { ok: false, error: new FileError("unknown", msg || `stat failed for ${abs}`, abs) };
|
|
335
|
-
}
|
|
336
|
-
const [sizeS, mtimeS, modeHex] = r.value.stdout.trim().split(/\s+/);
|
|
337
|
-
return ok({ name: path.posix.basename(abs), path: abs, kind: kindFromMode(parseInt(modeHex ?? "0", 16)), size: Number(sizeS) || 0, mtimeMs: (Number(mtimeS) || 0) * 1000 });
|
|
333
|
+
return this.posixFs.statFileInfo(p, abortSignal);
|
|
338
334
|
}
|
|
339
335
|
async listDir(p, abortSignal) {
|
|
340
|
-
|
|
341
|
-
const r = await this.exec(`ls -1 ${shellQuote(abs)}`, { abortSignal });
|
|
342
|
-
if (!r.ok)
|
|
343
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
344
|
-
if (r.value.exitCode !== 0) {
|
|
345
|
-
const msg = r.value.stderr.trim();
|
|
346
|
-
if (/no such file/i.test(msg))
|
|
347
|
-
return { ok: false, error: new FileError("not_found", msg, abs) };
|
|
348
|
-
if (/not a directory/i.test(msg))
|
|
349
|
-
return { ok: false, error: new FileError("not_directory", msg, abs) };
|
|
350
|
-
if (/permission denied/i.test(msg))
|
|
351
|
-
return { ok: false, error: new FileError("permission_denied", msg, abs) };
|
|
352
|
-
return { ok: false, error: new FileError("unknown", msg || `ls failed for ${abs}`, abs) };
|
|
353
|
-
}
|
|
354
|
-
const names = r.value.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
355
|
-
const out = [];
|
|
356
|
-
for (const name of names) {
|
|
357
|
-
const info = await this.fileInfo(path.posix.join(abs, name), abortSignal);
|
|
358
|
-
if (info.ok)
|
|
359
|
-
out.push(info.value);
|
|
360
|
-
else
|
|
361
|
-
out.push({ name, path: path.posix.join(abs, name), kind: "file", size: 0, mtimeMs: 0 });
|
|
362
|
-
}
|
|
363
|
-
return ok(out);
|
|
336
|
+
return this.posixFs.listDir(p, abortSignal);
|
|
364
337
|
}
|
|
365
338
|
async readLink(p, abortSignal) {
|
|
366
|
-
|
|
367
|
-
const r = await this.exec(`readlink ${shellQuote(abs)}`, { abortSignal });
|
|
368
|
-
if (!r.ok)
|
|
369
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
370
|
-
if (r.value.exitCode !== 0)
|
|
371
|
-
return { ok: false, error: new FileError("not_found", r.value.stderr.trim() || `readlink failed for ${abs}`, abs) };
|
|
372
|
-
return ok(r.value.stdout.trim());
|
|
339
|
+
return this.posixFs.readLink(p, abortSignal);
|
|
373
340
|
}
|
|
374
341
|
async canonicalPath(p, abortSignal) {
|
|
375
|
-
|
|
376
|
-
const r = await this.exec(`realpath ${shellQuote(abs)}`, { abortSignal });
|
|
377
|
-
if (!r.ok)
|
|
378
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
379
|
-
if (r.value.exitCode !== 0) {
|
|
380
|
-
const msg = r.value.stderr.trim();
|
|
381
|
-
if (/permission denied/i.test(msg))
|
|
382
|
-
return { ok: false, error: new FileError("permission_denied", msg, abs) };
|
|
383
|
-
if (/not a directory/i.test(msg))
|
|
384
|
-
return { ok: false, error: new FileError("not_directory", msg, abs) };
|
|
385
|
-
return { ok: false, error: new FileError("not_found", msg || `realpath failed for ${abs}`, abs) };
|
|
386
|
-
}
|
|
387
|
-
return ok(r.value.stdout.trim());
|
|
342
|
+
return this.posixFs.canonicalPath(p, abortSignal);
|
|
388
343
|
}
|
|
389
344
|
async exists(p, abortSignal) {
|
|
390
|
-
|
|
391
|
-
if (info.ok)
|
|
392
|
-
return ok(true);
|
|
393
|
-
if (info.error.code === "not_found")
|
|
394
|
-
return ok(false);
|
|
395
|
-
return { ok: false, error: info.error };
|
|
345
|
+
return this.posixFs.exists(p, abortSignal);
|
|
396
346
|
}
|
|
397
347
|
async createDir(p, options) {
|
|
398
|
-
|
|
399
|
-
const r = await this.exec(`mkdir -p ${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
|
|
400
|
-
if (!r.ok)
|
|
401
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
402
|
-
if (r.value.exitCode !== 0) {
|
|
403
|
-
const msg = r.value.stderr.trim();
|
|
404
|
-
if (/permission denied/i.test(msg))
|
|
405
|
-
return { ok: false, error: new FileError("permission_denied", msg, abs) };
|
|
406
|
-
return { ok: false, error: new FileError("unknown", msg || `mkdir failed for ${abs}`, abs) };
|
|
407
|
-
}
|
|
408
|
-
return ok(undefined);
|
|
348
|
+
return this.posixFs.createDir(p, options);
|
|
409
349
|
}
|
|
410
350
|
async remove(p, options) {
|
|
411
|
-
|
|
412
|
-
const flags = `${options?.recursive ? "r" : ""}${options?.force ? "f" : ""}`;
|
|
413
|
-
const r = await this.exec(`rm ${flags ? `-${flags}` : ""} ${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
|
|
414
|
-
if (!r.ok)
|
|
415
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
416
|
-
if (r.value.exitCode !== 0) {
|
|
417
|
-
const msg = r.value.stderr.trim();
|
|
418
|
-
if (options?.force && /no such file/i.test(msg))
|
|
419
|
-
return ok(undefined);
|
|
420
|
-
if (/no such file/i.test(msg))
|
|
421
|
-
return { ok: false, error: new FileError("not_found", msg, abs) };
|
|
422
|
-
if (/permission denied/i.test(msg))
|
|
423
|
-
return { ok: false, error: new FileError("permission_denied", msg, abs) };
|
|
424
|
-
return { ok: false, error: new FileError("unknown", msg || `rm failed for ${abs}`, abs) };
|
|
425
|
-
}
|
|
426
|
-
return ok(undefined);
|
|
351
|
+
return this.posixFs.remove(p, options);
|
|
427
352
|
}
|
|
428
353
|
async createTempDir(prefix = "tmp-", abortSignal) {
|
|
429
|
-
|
|
430
|
-
return this.tempResult(r, "mktemp -d");
|
|
354
|
+
return this.posixFs.createTempDir(prefix, abortSignal);
|
|
431
355
|
}
|
|
432
356
|
async createTempFile(options) {
|
|
433
|
-
|
|
434
|
-
const sfx = options?.suffix ?? "";
|
|
435
|
-
const cmd = sfx
|
|
436
|
-
? `f=$(mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}) && mv -- "$f" "$f"${shellQuote(sfx)} && printf '%s' "$f"${shellQuote(sfx)}`
|
|
437
|
-
: `mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`;
|
|
438
|
-
const r = await this.exec(cmd, { abortSignal: options?.abortSignal });
|
|
439
|
-
return this.tempResult(r, "mktemp");
|
|
357
|
+
return this.posixFs.createTempFile(options);
|
|
440
358
|
}
|
|
441
359
|
adb(args, opts) {
|
|
442
360
|
return new Promise((resolve) => {
|
|
@@ -515,13 +433,6 @@ export class RemoteAdbExecutionEnv {
|
|
|
515
433
|
parts.push(command);
|
|
516
434
|
return parts.join(" ");
|
|
517
435
|
}
|
|
518
|
-
tempResult(r, label) {
|
|
519
|
-
if (!r.ok)
|
|
520
|
-
return { ok: false, error: fileErrorFromExec(r.error, undefined) };
|
|
521
|
-
if (r.value.exitCode !== 0)
|
|
522
|
-
return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `${label} failed`) };
|
|
523
|
-
return ok(r.value.stdout.trim());
|
|
524
|
-
}
|
|
525
436
|
}
|
|
526
437
|
export function adbExecutionEnvFactory(config) {
|
|
527
438
|
return (_ctx) => new RemoteAdbExecutionEnv(config);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { FileError, type ExecutionError, type RemoteExecutionError } from "@sema-agent/core";
|
|
2
2
|
export declare function fileErrorFromExec(e: ExecutionError | RemoteExecutionError, path?: string): FileError;
|
|
3
|
+
export declare function classifyFsStderr(stderr: string, p: string | undefined, fallback?: "unknown" | "not_found"): FileError;
|
|
3
4
|
//# sourceMappingURL=remote-env-file-error.d.ts.map
|
|
@@ -3,4 +3,20 @@ export function fileErrorFromExec(e, path) {
|
|
|
3
3
|
const code = e.code === "aborted" ? "aborted" : "unknown";
|
|
4
4
|
return new FileError(code, e.message, path, e);
|
|
5
5
|
}
|
|
6
|
+
export function classifyFsStderr(stderr, p, fallback = "unknown") {
|
|
7
|
+
const msg = stderr.trim();
|
|
8
|
+
if (/command not found|applet not found|:\s+not found\s*$/im.test(msg))
|
|
9
|
+
return new FileError("unknown", msg, p);
|
|
10
|
+
if (/input\/output error|too many levels|file name too long|no space left/i.test(msg))
|
|
11
|
+
return new FileError("unknown", msg, p);
|
|
12
|
+
if (/no such file/i.test(msg))
|
|
13
|
+
return new FileError("not_found", msg, p);
|
|
14
|
+
if (/permission denied/i.test(msg))
|
|
15
|
+
return new FileError("permission_denied", msg, p);
|
|
16
|
+
if (/is a directory/i.test(msg))
|
|
17
|
+
return new FileError("is_directory", msg, p);
|
|
18
|
+
if (/not a directory/i.test(msg))
|
|
19
|
+
return new FileError("not_directory", msg, p);
|
|
20
|
+
return new FileError(fallback, msg || "command failed", p);
|
|
21
|
+
}
|
|
6
22
|
//# sourceMappingURL=remote-env-file-error.js.map
|
|
@@ -149,6 +149,8 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
149
149
|
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
150
150
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
151
151
|
private writeChunked;
|
|
152
|
+
private posixFsInst?;
|
|
153
|
+
private get posixFs();
|
|
152
154
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
153
155
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
154
156
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
@@ -171,8 +173,6 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
171
173
|
}): Promise<Result<string, FileError>>;
|
|
172
174
|
private execData;
|
|
173
175
|
private withCwdEnv;
|
|
174
|
-
private fileErrorFrom;
|
|
175
|
-
private tempResult;
|
|
176
176
|
}
|
|
177
177
|
export declare function k8sExecutionEnvFactory(config: K8sEnvConfig): ExecutionEnvFactory;
|
|
178
178
|
//# sourceMappingURL=remote-env-k8s.d.ts.map
|
|
@@ -7,7 +7,8 @@ import WebSocket from "ws";
|
|
|
7
7
|
import { shellQuote, kindFromMode } from "./remote-shell.js";
|
|
8
8
|
import { presignS3Url } from "./s3-presign.js";
|
|
9
9
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
10
|
-
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
10
|
+
import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
|
|
11
|
+
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
11
12
|
import { randomUUID } from "node:crypto";
|
|
12
13
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
13
14
|
const PROVIDER = "k8s";
|
|
@@ -833,7 +834,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
833
834
|
if (!r.ok)
|
|
834
835
|
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
835
836
|
if (r.value.exitCode !== 0)
|
|
836
|
-
return { ok: false, error:
|
|
837
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
|
|
837
838
|
return ok(new Uint8Array(Buffer.from(r.value.stdout.replace(/\s+/g, ""), "base64")));
|
|
838
839
|
}
|
|
839
840
|
async readTextFile(p, abortSignal) {
|
|
@@ -859,7 +860,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
859
860
|
if (!mk.ok)
|
|
860
861
|
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
861
862
|
if (mk.value.exitCode !== 0)
|
|
862
|
-
return { ok: false, error:
|
|
863
|
+
return { ok: false, error: classifyFsStderr(mk.value.stderr, dir) };
|
|
863
864
|
return undefined;
|
|
864
865
|
}
|
|
865
866
|
async writeFile(p, content, abortSignal) {
|
|
@@ -885,99 +886,44 @@ export class RemoteK8sExecutionEnv {
|
|
|
885
886
|
if (!r.ok)
|
|
886
887
|
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
887
888
|
if (r.value.exitCode !== 0)
|
|
888
|
-
return { ok: false, error:
|
|
889
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs) };
|
|
889
890
|
}
|
|
890
891
|
return ok(undefined);
|
|
891
892
|
}
|
|
893
|
+
posixFsInst;
|
|
894
|
+
get posixFs() {
|
|
895
|
+
return (this.posixFsInst ??= createPosixShellFs({
|
|
896
|
+
run: (cmd, abortSignal) => this.exec(cmd, { abortSignal }),
|
|
897
|
+
resolve: (p) => this.resolve(p),
|
|
898
|
+
tempDir: () => this.cwd,
|
|
899
|
+
}));
|
|
900
|
+
}
|
|
892
901
|
async fileInfo(p, abortSignal) {
|
|
893
|
-
|
|
894
|
-
const r = await this.exec(`stat -c '%s %Y %f' ${shellQuote(abs)}`, { abortSignal });
|
|
895
|
-
if (!r.ok)
|
|
896
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
897
|
-
if (r.value.exitCode !== 0)
|
|
898
|
-
return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs, "not_found") };
|
|
899
|
-
const [sizeS, mtimeS, modeHex] = r.value.stdout.trim().split(/\s+/);
|
|
900
|
-
return ok({ name: path.posix.basename(abs), path: abs, kind: kindFromMode(parseInt(modeHex ?? "0", 16)), size: Number(sizeS) || 0, mtimeMs: (Number(mtimeS) || 0) * 1000 });
|
|
902
|
+
return this.posixFs.statFileInfo(p, abortSignal);
|
|
901
903
|
}
|
|
902
904
|
async listDir(p, abortSignal) {
|
|
903
|
-
|
|
904
|
-
const r = await this.exec(`ls -1A ${shellQuote(abs)}`, { abortSignal });
|
|
905
|
-
if (!r.ok)
|
|
906
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
907
|
-
if (r.value.exitCode !== 0)
|
|
908
|
-
return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs) };
|
|
909
|
-
const names = r.value.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
910
|
-
const out = [];
|
|
911
|
-
for (const name of names) {
|
|
912
|
-
const info = await this.fileInfo(path.posix.join(abs, name), abortSignal);
|
|
913
|
-
out.push(info.ok ? info.value : { name, path: path.posix.join(abs, name), kind: "file", size: 0, mtimeMs: 0 });
|
|
914
|
-
}
|
|
915
|
-
return ok(out);
|
|
905
|
+
return this.posixFs.listDir(p, abortSignal);
|
|
916
906
|
}
|
|
917
907
|
async readLink(p, abortSignal) {
|
|
918
|
-
|
|
919
|
-
const r = await this.exec(`readlink ${shellQuote(abs)}`, { abortSignal });
|
|
920
|
-
if (!r.ok)
|
|
921
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
922
|
-
if (r.value.exitCode !== 0)
|
|
923
|
-
return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs, "not_found") };
|
|
924
|
-
return ok(r.value.stdout.trim());
|
|
908
|
+
return this.posixFs.readLink(p, abortSignal);
|
|
925
909
|
}
|
|
926
910
|
async canonicalPath(p, abortSignal) {
|
|
927
|
-
|
|
928
|
-
const r = await this.exec(`realpath ${shellQuote(abs)}`, { abortSignal });
|
|
929
|
-
if (!r.ok)
|
|
930
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
931
|
-
if (r.value.exitCode !== 0)
|
|
932
|
-
return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs, "not_found") };
|
|
933
|
-
return ok(r.value.stdout.trim());
|
|
911
|
+
return this.posixFs.canonicalPath(p, abortSignal);
|
|
934
912
|
}
|
|
935
913
|
async exists(p, abortSignal) {
|
|
936
|
-
|
|
937
|
-
const r = await this.exec(`{ [ -e ${shellQuote(abs)} ] || [ -L ${shellQuote(abs)} ]; } && printf yes || printf no`, { abortSignal });
|
|
938
|
-
if (!r.ok)
|
|
939
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
940
|
-
const out = r.value.stdout.trim();
|
|
941
|
-
if (out === "yes")
|
|
942
|
-
return ok(true);
|
|
943
|
-
if (out === "no")
|
|
944
|
-
return ok(false);
|
|
945
|
-
return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs) };
|
|
914
|
+
return this.posixFs.exists(p, abortSignal);
|
|
946
915
|
}
|
|
947
916
|
async createDir(p, options) {
|
|
948
|
-
|
|
949
|
-
const r = await this.exec(`mkdir ${options?.recursive === false ? "" : "-p "}${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
|
|
950
|
-
if (!r.ok)
|
|
951
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
952
|
-
if (r.value.exitCode !== 0)
|
|
953
|
-
return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs) };
|
|
954
|
-
return ok(undefined);
|
|
917
|
+
return this.posixFs.createDir(p, options);
|
|
955
918
|
}
|
|
956
919
|
async remove(p, options) {
|
|
957
|
-
|
|
958
|
-
const flags = `${options?.recursive ? "r" : ""}${options?.force ? "f" : ""}`;
|
|
959
|
-
const r = await this.exec(`rm ${flags ? `-${flags}` : ""} ${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
|
|
960
|
-
if (!r.ok)
|
|
961
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
962
|
-
if (r.value.exitCode !== 0) {
|
|
963
|
-
if (options?.force && /no such file/i.test(r.value.stderr))
|
|
964
|
-
return ok(undefined);
|
|
965
|
-
return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs) };
|
|
966
|
-
}
|
|
967
|
-
return ok(undefined);
|
|
920
|
+
return this.posixFs.remove(p, options);
|
|
968
921
|
}
|
|
969
922
|
async createTempDir(prefix = "tmp-", abortSignal) {
|
|
970
|
-
|
|
971
|
-
return this.tempResult(r, "mktemp -d");
|
|
923
|
+
return this.posixFs.createTempDir(prefix, abortSignal);
|
|
972
924
|
}
|
|
973
925
|
async createTempFile(options) {
|
|
974
|
-
|
|
975
|
-
const sfx = options?.suffix ?? "";
|
|
976
|
-
const cmd = sfx
|
|
977
|
-
? `f=$(mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}) && mv -- "$f" "$f"${shellQuote(sfx)} && printf '%s' "$f"${shellQuote(sfx)}`
|
|
978
|
-
: `mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`;
|
|
979
|
-
const r = await this.exec(cmd, { abortSignal: options?.abortSignal });
|
|
980
|
-
return this.tempResult(r, "mktemp");
|
|
926
|
+
return this.posixFs.createTempFile(options);
|
|
981
927
|
}
|
|
982
928
|
async execData(command, abortSignal) {
|
|
983
929
|
const c = await this.ensureConnected();
|
|
@@ -996,27 +942,6 @@ export class RemoteK8sExecutionEnv {
|
|
|
996
942
|
parts.push(command);
|
|
997
943
|
return parts.join(" ");
|
|
998
944
|
}
|
|
999
|
-
fileErrorFrom(stderr, p, fallback = "unknown") {
|
|
1000
|
-
const msg = stderr.trim();
|
|
1001
|
-
if (/no such file/i.test(msg))
|
|
1002
|
-
return new FileError("not_found", msg, p);
|
|
1003
|
-
if (/permission denied/i.test(msg))
|
|
1004
|
-
return new FileError("permission_denied", msg, p);
|
|
1005
|
-
if (/is a directory/i.test(msg))
|
|
1006
|
-
return new FileError("is_directory", msg, p);
|
|
1007
|
-
if (/not a directory/i.test(msg))
|
|
1008
|
-
return new FileError("not_directory", msg, p);
|
|
1009
|
-
if (/input\/output error|too many levels|file name too long|no space left/i.test(msg))
|
|
1010
|
-
return new FileError("unknown", msg, p);
|
|
1011
|
-
return new FileError(fallback, msg || "command failed", p);
|
|
1012
|
-
}
|
|
1013
|
-
tempResult(r, label) {
|
|
1014
|
-
if (!r.ok)
|
|
1015
|
-
return { ok: false, error: fileErrorFromExec(r.error, undefined) };
|
|
1016
|
-
if (r.value.exitCode !== 0)
|
|
1017
|
-
return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `${label} failed`) };
|
|
1018
|
-
return ok(r.value.stdout.trim());
|
|
1019
|
-
}
|
|
1020
945
|
}
|
|
1021
946
|
export function k8sExecutionEnvFactory(config) {
|
|
1022
947
|
return (ctx) => new RemoteK8sExecutionEnv({ ...config, sessionId: ctx.sessionId });
|
|
@@ -90,6 +90,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
90
90
|
}): Promise<Result<string[], FileError>>;
|
|
91
91
|
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
92
92
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
93
|
+
private posixFsInst?;
|
|
94
|
+
private get posixFs();
|
|
93
95
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
94
96
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
95
97
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
@@ -119,8 +121,6 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
119
121
|
private probeContainerShell;
|
|
120
122
|
private ensureConnectedExec;
|
|
121
123
|
private fsReady;
|
|
122
|
-
private tempResult;
|
|
123
|
-
private fsErrorFromStderr;
|
|
124
124
|
private dockerToRemoteError;
|
|
125
125
|
}
|
|
126
126
|
export declare function localDockerExecutionEnvFactory(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps): ExecutionEnvFactory;
|
|
@@ -3,9 +3,10 @@ import os from "node:os";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
|
-
import { shellQuote,
|
|
6
|
+
import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
7
7
|
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
8
|
-
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
8
|
+
import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
|
|
9
|
+
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
9
10
|
const PROVIDER = "local-docker";
|
|
10
11
|
const DEFAULT_MOUNT_PATH = "/workspace";
|
|
11
12
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -301,7 +302,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
301
302
|
if (!r.ok)
|
|
302
303
|
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
303
304
|
if (r.value.exitCode !== 0)
|
|
304
|
-
return { ok: false, error:
|
|
305
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, abs) };
|
|
305
306
|
return ok(r.value.stdoutBytes ?? new Uint8Array());
|
|
306
307
|
}
|
|
307
308
|
async readTextFile(p, abortSignal) {
|
|
@@ -330,7 +331,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
330
331
|
if (!mk.ok)
|
|
331
332
|
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
332
333
|
if (mk.value.exitCode !== 0)
|
|
333
|
-
return { ok: false, error:
|
|
334
|
+
return { ok: false, error: classifyFsStderr(mk.value.stderr, dir) };
|
|
334
335
|
}
|
|
335
336
|
let local;
|
|
336
337
|
try {
|
|
@@ -340,7 +341,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
340
341
|
if (!cp.ok)
|
|
341
342
|
return { ok: false, error: fileErrorFromExec(cp.error, abs) };
|
|
342
343
|
if (cp.value.exitCode !== 0)
|
|
343
|
-
return { ok: false, error:
|
|
344
|
+
return { ok: false, error: classifyFsStderr(cp.value.stderr, abs) };
|
|
344
345
|
return ok(undefined);
|
|
345
346
|
}
|
|
346
347
|
catch (e) {
|
|
@@ -367,108 +368,49 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
367
368
|
merged.set(add, base.length);
|
|
368
369
|
return this.writeFile(abs, merged, abortSignal);
|
|
369
370
|
}
|
|
371
|
+
posixFsInst;
|
|
372
|
+
get posixFs() {
|
|
373
|
+
return (this.posixFsInst ??= createPosixShellFs({
|
|
374
|
+
run: async (cmd, abortSignal) => {
|
|
375
|
+
if (this.destroyed)
|
|
376
|
+
return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
|
|
377
|
+
if (!this.containerId) {
|
|
378
|
+
const c = await this.connect();
|
|
379
|
+
if (!c.ok)
|
|
380
|
+
return { ok: false, error: new ExecutionError("shell_unavailable", `local-docker connect failed: ${c.error.message}`) };
|
|
381
|
+
}
|
|
382
|
+
return this.docker(this.execRaw(cmd), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
383
|
+
},
|
|
384
|
+
resolve: (p) => this.resolve(p),
|
|
385
|
+
tempDir: () => this.cwd,
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
370
388
|
async fileInfo(p, abortSignal) {
|
|
371
|
-
|
|
372
|
-
if (!ready.ok)
|
|
373
|
-
return ready;
|
|
374
|
-
const abs = this.resolve(p);
|
|
375
|
-
const r = await this.docker(this.execRaw(`stat -c '%s %Y %f' ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
376
|
-
if (!r.ok)
|
|
377
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
378
|
-
if (r.value.exitCode !== 0)
|
|
379
|
-
return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
|
|
380
|
-
const [sizeS, mtimeS, modeHex] = r.value.stdout.trim().split(/\s+/);
|
|
381
|
-
return ok({ name: path.posix.basename(abs), path: abs, kind: kindFromMode(parseInt(modeHex ?? "0", 16)), size: Number(sizeS) || 0, mtimeMs: (Number(mtimeS) || 0) * 1000 });
|
|
389
|
+
return this.posixFs.statFileInfo(p, abortSignal);
|
|
382
390
|
}
|
|
383
391
|
async listDir(p, abortSignal) {
|
|
384
|
-
|
|
385
|
-
if (!ready.ok)
|
|
386
|
-
return ready;
|
|
387
|
-
const abs = this.resolve(p);
|
|
388
|
-
const r = await this.docker(this.execRaw(`ls -1A ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
389
|
-
if (!r.ok)
|
|
390
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
391
|
-
if (r.value.exitCode !== 0)
|
|
392
|
-
return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
|
|
393
|
-
const names = r.value.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
394
|
-
const out = [];
|
|
395
|
-
for (const name of names) {
|
|
396
|
-
const info = await this.fileInfo(path.posix.join(abs, name), abortSignal);
|
|
397
|
-
if (info.ok)
|
|
398
|
-
out.push(info.value);
|
|
399
|
-
else
|
|
400
|
-
out.push({ name, path: path.posix.join(abs, name), kind: "file", size: 0, mtimeMs: 0 });
|
|
401
|
-
}
|
|
402
|
-
return ok(out);
|
|
392
|
+
return this.posixFs.listDir(p, abortSignal);
|
|
403
393
|
}
|
|
404
394
|
async readLink(p, abortSignal) {
|
|
405
|
-
|
|
406
|
-
const r = await this.docker(this.execRaw(`readlink ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
407
|
-
if (!r.ok)
|
|
408
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
409
|
-
if (r.value.exitCode !== 0)
|
|
410
|
-
return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
|
|
411
|
-
return ok(r.value.stdout.trim());
|
|
395
|
+
return this.posixFs.readLink(p, abortSignal);
|
|
412
396
|
}
|
|
413
397
|
async canonicalPath(p, abortSignal) {
|
|
414
|
-
|
|
415
|
-
const r = await this.docker(this.execRaw(`realpath ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
416
|
-
if (!r.ok)
|
|
417
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
418
|
-
if (r.value.exitCode !== 0)
|
|
419
|
-
return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
|
|
420
|
-
return ok(r.value.stdout.trim());
|
|
398
|
+
return this.posixFs.canonicalPath(p, abortSignal);
|
|
421
399
|
}
|
|
422
400
|
async exists(p, abortSignal) {
|
|
423
|
-
|
|
424
|
-
if (info.ok)
|
|
425
|
-
return ok(true);
|
|
426
|
-
if (info.error.code === "not_found")
|
|
427
|
-
return ok(false);
|
|
428
|
-
return { ok: false, error: info.error };
|
|
401
|
+
return this.posixFs.exists(p, abortSignal);
|
|
429
402
|
}
|
|
430
403
|
async createDir(p, options) {
|
|
431
|
-
|
|
432
|
-
if (!ready.ok)
|
|
433
|
-
return ready;
|
|
434
|
-
const abs = this.resolve(p);
|
|
435
|
-
const recursive = options?.recursive ?? true;
|
|
436
|
-
const r = await this.docker(this.execRaw(`mkdir ${recursive ? "-p " : ""}${shellQuote(abs)}`), { signal: options?.abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
437
|
-
if (!r.ok)
|
|
438
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
439
|
-
if (r.value.exitCode !== 0)
|
|
440
|
-
return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
|
|
441
|
-
return ok(undefined);
|
|
404
|
+
return this.posixFs.createDir(p, options);
|
|
442
405
|
}
|
|
443
406
|
async remove(p, options) {
|
|
444
|
-
|
|
445
|
-
if (!ready.ok)
|
|
446
|
-
return ready;
|
|
447
|
-
const abs = this.resolve(p);
|
|
448
|
-
const flags = `${options?.recursive ? "r" : ""}${options?.force ? "f" : ""}`;
|
|
449
|
-
const r = await this.docker(this.execRaw(`rm ${flags ? `-${flags} ` : ""}-- ${shellQuote(abs)}`), { signal: options?.abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
450
|
-
if (!r.ok)
|
|
451
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
452
|
-
if (r.value.exitCode !== 0) {
|
|
453
|
-
const fe = this.fsErrorFromStderr(r.value.stderr, abs);
|
|
454
|
-
if (options?.force && fe.code === "not_found")
|
|
455
|
-
return ok(undefined);
|
|
456
|
-
return { ok: false, error: fe };
|
|
457
|
-
}
|
|
458
|
-
return ok(undefined);
|
|
407
|
+
return this.posixFs.remove(p, options);
|
|
459
408
|
}
|
|
460
409
|
async createTempDir(prefix = "tmp-", abortSignal) {
|
|
461
|
-
|
|
462
|
-
return this.tempResult(r);
|
|
410
|
+
return this.posixFs.createTempDir(prefix, abortSignal);
|
|
463
411
|
}
|
|
464
412
|
async createTempFile(options) {
|
|
465
|
-
|
|
466
|
-
const sfx = options?.suffix ?? "";
|
|
467
|
-
const cmd = sfx
|
|
468
|
-
? `f=$(mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}) && mv -- "$f" "$f"${shellQuote(sfx)} && printf '%s' "$f"${shellQuote(sfx)}`
|
|
469
|
-
: `mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`;
|
|
470
|
-
const r = await this.docker(this.execRaw(cmd), { signal: options?.abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
471
|
-
return this.tempResult(r);
|
|
413
|
+
return this.posixFs.createTempFile(options);
|
|
472
414
|
}
|
|
473
415
|
withHostFlag(args) {
|
|
474
416
|
return this.cfg.dockerHost ? ["-H", this.cfg.dockerHost, ...args] : args;
|
|
@@ -584,25 +526,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
584
526
|
return { ok: false, error: new FileError("unknown", `local-docker connect failed: ${c.error.message}`, this.resolve(p), c.error) };
|
|
585
527
|
return ok(undefined);
|
|
586
528
|
}
|
|
587
|
-
tempResult(r) {
|
|
588
|
-
if (!r.ok)
|
|
589
|
-
return { ok: false, error: fileErrorFromExec(r.error, undefined) };
|
|
590
|
-
if (r.value.exitCode !== 0)
|
|
591
|
-
return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, undefined) };
|
|
592
|
-
return ok(r.value.stdout.trim());
|
|
593
|
-
}
|
|
594
|
-
fsErrorFromStderr(stderr, p) {
|
|
595
|
-
const msg = stderr.trim();
|
|
596
|
-
if (/no such file|not found/i.test(msg))
|
|
597
|
-
return new FileError("not_found", msg || "no such file", p);
|
|
598
|
-
if (/permission denied/i.test(msg))
|
|
599
|
-
return new FileError("permission_denied", msg, p);
|
|
600
|
-
if (/is a directory/i.test(msg))
|
|
601
|
-
return new FileError("is_directory", msg, p);
|
|
602
|
-
if (/not a directory/i.test(msg))
|
|
603
|
-
return new FileError("not_directory", msg, p);
|
|
604
|
-
return new FileError("unknown", msg || "command failed", p);
|
|
605
|
-
}
|
|
606
529
|
dockerToRemoteError(e, fallback) {
|
|
607
530
|
if (e.code === "timeout")
|
|
608
531
|
return new RemoteExecutionError("timeout", e.message, e);
|
|
@@ -101,6 +101,8 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
101
101
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
102
102
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
103
103
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
104
|
+
private posixFsInst?;
|
|
105
|
+
private get posixFs();
|
|
104
106
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
105
107
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
106
108
|
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
@@ -120,7 +122,6 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
120
122
|
abortSignal?: AbortSignal;
|
|
121
123
|
}): Promise<Result<string, FileError>>;
|
|
122
124
|
private withCwdEnv;
|
|
123
|
-
private tempResult;
|
|
124
125
|
private connErr;
|
|
125
126
|
private fileError;
|
|
126
127
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { Client } from "ssh2";
|
|
3
3
|
import { shellQuote, kindFromMode } from "./remote-shell.js";
|
|
4
|
+
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
4
5
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
5
6
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
6
7
|
const PROVIDER = "ssh";
|
|
@@ -503,29 +504,19 @@ export class RemoteSshExecutionEnv {
|
|
|
503
504
|
return { ok: false, error: this.fileError(e, abs) };
|
|
504
505
|
}
|
|
505
506
|
}
|
|
507
|
+
posixFsInst;
|
|
508
|
+
get posixFs() {
|
|
509
|
+
return (this.posixFsInst ??= createPosixShellFs({
|
|
510
|
+
run: (cmd, abortSignal) => this.exec(cmd, { abortSignal }),
|
|
511
|
+
resolve: (p) => this.resolve(p),
|
|
512
|
+
tempDir: () => null,
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
506
515
|
async readLink(p, abortSignal) {
|
|
507
|
-
|
|
508
|
-
const r = await this.exec(`readlink -- ${shellQuote(abs)}`, { abortSignal });
|
|
509
|
-
if (!r.ok)
|
|
510
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
511
|
-
if (r.value.exitCode !== 0)
|
|
512
|
-
return { ok: false, error: new FileError("not_found", r.value.stderr.trim() || `readlink failed for ${abs}`, abs) };
|
|
513
|
-
return ok(r.value.stdout.trim());
|
|
516
|
+
return this.posixFs.readLink(p, abortSignal);
|
|
514
517
|
}
|
|
515
518
|
async canonicalPath(p, abortSignal) {
|
|
516
|
-
|
|
517
|
-
const r = await this.exec(`realpath -- ${shellQuote(abs)}`, { abortSignal });
|
|
518
|
-
if (!r.ok)
|
|
519
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
520
|
-
if (r.value.exitCode !== 0) {
|
|
521
|
-
const msg = r.value.stderr.trim();
|
|
522
|
-
if (/permission denied|access denied/i.test(msg))
|
|
523
|
-
return { ok: false, error: new FileError("permission_denied", msg, abs) };
|
|
524
|
-
if (/not a directory/i.test(msg))
|
|
525
|
-
return { ok: false, error: new FileError("not_directory", msg, abs) };
|
|
526
|
-
return { ok: false, error: new FileError("not_found", msg || `realpath failed for ${abs}`, abs) };
|
|
527
|
-
}
|
|
528
|
-
return ok(r.value.stdout.trim());
|
|
519
|
+
return this.posixFs.canonicalPath(p, abortSignal);
|
|
529
520
|
}
|
|
530
521
|
async exists(p, abortSignal) {
|
|
531
522
|
const info = await this.fileInfo(p, abortSignal);
|
|
@@ -536,46 +527,16 @@ export class RemoteSshExecutionEnv {
|
|
|
536
527
|
return { ok: false, error: info.error };
|
|
537
528
|
}
|
|
538
529
|
async createDir(p, options) {
|
|
539
|
-
|
|
540
|
-
const r = await this.exec(`mkdir -p -- ${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
|
|
541
|
-
if (!r.ok)
|
|
542
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
543
|
-
if (r.value.exitCode !== 0) {
|
|
544
|
-
const msg = r.value.stderr.trim();
|
|
545
|
-
if (/permission denied/i.test(msg))
|
|
546
|
-
return { ok: false, error: new FileError("permission_denied", msg, abs) };
|
|
547
|
-
return { ok: false, error: new FileError("unknown", msg || `mkdir failed for ${abs}`, abs) };
|
|
548
|
-
}
|
|
549
|
-
return ok(undefined);
|
|
530
|
+
return this.posixFs.createDir(p, options);
|
|
550
531
|
}
|
|
551
532
|
async remove(p, options) {
|
|
552
|
-
|
|
553
|
-
const flags = `${options?.recursive ? "r" : ""}${options?.force ? "f" : ""}`;
|
|
554
|
-
const r = await this.exec(`rm ${flags ? `-${flags}` : ""} -- ${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
|
|
555
|
-
if (!r.ok)
|
|
556
|
-
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
557
|
-
if (r.value.exitCode !== 0) {
|
|
558
|
-
const msg = r.value.stderr.trim();
|
|
559
|
-
if (options?.force && /no such file/i.test(msg))
|
|
560
|
-
return ok(undefined);
|
|
561
|
-
if (/no such file/i.test(msg))
|
|
562
|
-
return { ok: false, error: new FileError("not_found", msg, abs) };
|
|
563
|
-
if (/permission denied/i.test(msg))
|
|
564
|
-
return { ok: false, error: new FileError("permission_denied", msg, abs) };
|
|
565
|
-
return { ok: false, error: new FileError("unknown", msg || `rm failed for ${abs}`, abs) };
|
|
566
|
-
}
|
|
567
|
-
return ok(undefined);
|
|
533
|
+
return this.posixFs.remove(p, options);
|
|
568
534
|
}
|
|
569
535
|
async createTempDir(prefix = "tmp-", abortSignal) {
|
|
570
|
-
|
|
571
|
-
return this.tempResult(r, "mktemp -d");
|
|
536
|
+
return this.posixFs.createTempDir(prefix, abortSignal);
|
|
572
537
|
}
|
|
573
538
|
async createTempFile(options) {
|
|
574
|
-
|
|
575
|
-
const suffix = options?.suffix ?? "";
|
|
576
|
-
const suffixArg = suffix ? `--suffix=${shellQuote(suffix)} -t` : "-t";
|
|
577
|
-
const r = await this.exec(`mktemp ${suffixArg} ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal: options?.abortSignal });
|
|
578
|
-
return this.tempResult(r, "mktemp");
|
|
539
|
+
return this.posixFs.createTempFile(options);
|
|
579
540
|
}
|
|
580
541
|
withCwdEnv(command, cwd, env) {
|
|
581
542
|
const parts = [];
|
|
@@ -588,13 +549,6 @@ export class RemoteSshExecutionEnv {
|
|
|
588
549
|
parts.push(command);
|
|
589
550
|
return parts.join(" ");
|
|
590
551
|
}
|
|
591
|
-
tempResult(r, label) {
|
|
592
|
-
if (!r.ok)
|
|
593
|
-
return { ok: false, error: fileErrorFromExec(r.error, undefined) };
|
|
594
|
-
if (r.value.exitCode !== 0)
|
|
595
|
-
return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `${label} failed`) };
|
|
596
|
-
return ok(r.value.stdout.trim());
|
|
597
|
-
}
|
|
598
552
|
connErr(e) {
|
|
599
553
|
const msg = e.message || String(e);
|
|
600
554
|
if (e.level === "client-authentication" || /authentication|all configured authentication methods failed/i.test(msg)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.318.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|