@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.
@@ -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, kindFromMode, armPipeDestroyGrace } from "./remote-shell.js";
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const info = await this.fileInfo(p, abortSignal);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const r = await this.exec(`mktemp -d -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal });
430
- return this.tempResult(r, "mktemp -d");
354
+ return this.posixFs.createTempDir(prefix, abortSignal);
431
355
  }
432
356
  async createTempFile(options) {
433
- const prefix = options?.prefix ?? "";
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: this.fileErrorFrom(r.value.stderr, abs, "not_found") };
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: this.fileErrorFrom(mk.value.stderr, dir) };
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: this.fileErrorFrom(r.value.stderr, abs) };
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const r = await this.exec(`mktemp -d -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal });
971
- return this.tempResult(r, "mktemp -d");
923
+ return this.posixFs.createTempDir(prefix, abortSignal);
972
924
  }
973
925
  async createTempFile(options) {
974
- const prefix = options?.prefix ?? "";
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, kindFromMode, armPipeDestroyGrace } from "./remote-shell.js";
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: this.fsErrorFromStderr(r.value.stderr, abs) };
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: this.fsErrorFromStderr(mk.value.stderr, dir) };
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: this.fsErrorFromStderr(cp.value.stderr, abs) };
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
- const ready = await this.fsReady(p);
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
- const ready = await this.fsReady(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const info = await this.fileInfo(p, abortSignal);
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
- const ready = await this.fsReady(p);
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
- const ready = await this.fsReady(p);
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
- const r = await this.docker(this.execRaw(`mktemp -d -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
462
- return this.tempResult(r);
410
+ return this.posixFs.createTempDir(prefix, abortSignal);
463
411
  }
464
412
  async createTempFile(options) {
465
- const prefix = options?.prefix ?? "";
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const abs = this.resolve(p);
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
- const r = await this.exec(`mktemp -d -t ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal });
571
- return this.tempResult(r, "mktemp -d");
536
+ return this.posixFs.createTempDir(prefix, abortSignal);
572
537
  }
573
538
  async createTempFile(options) {
574
- const prefix = options?.prefix ?? "";
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.317.0",
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",