@sema-agent/server 1.316.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.
Files changed (62) hide show
  1. package/README.md +9 -0
  2. package/README.zh-CN.md +7 -0
  3. package/dist/config-types.d.ts +1 -1
  4. package/dist/config.d.ts +14 -0
  5. package/dist/config.js +148 -45
  6. package/dist/elicitation.js +2 -2
  7. package/dist/http/route-ctx.d.ts +51 -2
  8. package/dist/http/routes/approvals-assistant.d.ts +11 -0
  9. package/dist/http/routes/approvals-assistant.js +530 -0
  10. package/dist/http/routes/attachments.js +7 -7
  11. package/dist/http/routes/fleet.d.ts +4 -0
  12. package/dist/http/routes/fleet.js +147 -0
  13. package/dist/http/routes/images.js +29 -29
  14. package/dist/http/routes/leader.d.ts +4 -0
  15. package/dist/http/routes/leader.js +48 -0
  16. package/dist/http/routes/memory-policy.js +10 -10
  17. package/dist/http/routes/notify-wake.d.ts +4 -0
  18. package/dist/http/routes/notify-wake.js +133 -0
  19. package/dist/http/routes/observability.js +5 -5
  20. package/dist/http/routes/runs.d.ts +19 -0
  21. package/dist/http/routes/runs.js +967 -0
  22. package/dist/http/routes/session-sync.js +28 -28
  23. package/dist/http/routes/sessions-list.js +8 -8
  24. package/dist/http/routes/sessions.js +47 -47
  25. package/dist/http/routes/side-query.d.ts +4 -0
  26. package/dist/http/routes/side-query.js +88 -0
  27. package/dist/http/routes/tasks.d.ts +4 -0
  28. package/dist/http/routes/tasks.js +632 -0
  29. package/dist/http/routes/trace-usage.d.ts +4 -0
  30. package/dist/http/routes/trace-usage.js +239 -0
  31. package/dist/http/routes/workflows.d.ts +5 -0
  32. package/dist/http/routes/workflows.js +337 -0
  33. package/dist/http/run-meta.d.ts +11 -0
  34. package/dist/http/run-meta.js +16 -0
  35. package/dist/http/send.d.ts +1 -0
  36. package/dist/http/send.js +15 -0
  37. package/dist/http/server.d.ts +6 -5
  38. package/dist/http/server.js +241 -3166
  39. package/dist/http/sse-log.js +2 -2
  40. package/dist/http/wire-types.d.ts +6 -0
  41. package/dist/leader/endpoint.js +4 -4
  42. package/dist/main.js +5 -5
  43. package/dist/plugins/posix-shell-fs.d.ts +35 -0
  44. package/dist/plugins/posix-shell-fs.js +122 -0
  45. package/dist/plugins/remote-env-adb.d.ts +2 -1
  46. package/dist/plugins/remote-env-adb.js +19 -108
  47. package/dist/plugins/remote-env-file-error.d.ts +1 -0
  48. package/dist/plugins/remote-env-file-error.js +16 -0
  49. package/dist/plugins/remote-env-host.js +4 -3
  50. package/dist/plugins/remote-env-k8s.d.ts +2 -2
  51. package/dist/plugins/remote-env-k8s.js +22 -97
  52. package/dist/plugins/remote-env-local-docker.d.ts +2 -2
  53. package/dist/plugins/remote-env-local-docker.js +32 -109
  54. package/dist/plugins/remote-env-ssh.d.ts +2 -1
  55. package/dist/plugins/remote-env-ssh.js +15 -61
  56. package/dist/question.js +2 -2
  57. package/dist/run-local.js +1 -1
  58. package/dist/tool-approval.js +2 -2
  59. package/dist/trace/ledger-sink.js +1 -1
  60. package/dist/trace/project.d.ts +1 -0
  61. package/dist/trace/project.js +3 -0
  62. package/package.json +1 -1
@@ -1,11 +1,11 @@
1
- import { sendJson, sseHeaders } from "./send.js";
1
+ import { sendJson, sendError, sseHeaders } from "./send.js";
2
2
  export async function streamSseLog(req, res, provider, id, staleMs) {
3
3
  const lastId = Number(req.headers["last-event-id"] ?? new URL(req.url ?? "", "http://x").searchParams.get("from") ?? 0);
4
4
  let from = Number.isFinite(lastId) ? lastId : 0;
5
5
  if (from > 0) {
6
6
  const retainedFrom = await provider.retainedFrom(id);
7
7
  if (retainedFrom > from + 1) {
8
- sendJson(res, 416, { error: "resume point evicted past retention", retainedFrom });
8
+ sendError(res, 416, "limit.retention_evicted", "resume point evicted past retention", { retainedFrom });
9
9
  return;
10
10
  }
11
11
  }
@@ -81,4 +81,10 @@ export interface TaskRequestBody {
81
81
  enableFork?: boolean;
82
82
  [k: string]: unknown;
83
83
  }
84
+ export type DecideBinding = {
85
+ checkpointToken?: string;
86
+ boundCallId?: string;
87
+ boundInputHash?: string;
88
+ updatedInput?: unknown;
89
+ };
84
90
  //# sourceMappingURL=wire-types.d.ts.map
@@ -27,12 +27,12 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
27
27
  if (method === "POST" && url === "/v1/leader") {
28
28
  const b = body;
29
29
  if (!b || typeof b.objective !== "string" || b.objective.trim() === "") {
30
- return { status: 400, body: { error: "missing 'objective' string" } };
30
+ return { status: 400, body: { error: "missing 'objective' string", errorCode: "request.body_shape" } };
31
31
  }
32
32
  if (opts.requiredFields) {
33
33
  const missing = opts.requiredFields.filter((f) => typeof b[f] !== "string" || b[f].trim() === "");
34
34
  if (missing.length > 0) {
35
- return { status: 400, body: { error: `missing required field(s): ${missing.join(", ")}` } };
35
+ return { status: 400, body: { error: `missing required field(s): ${missing.join(", ")}`, errorCode: "request.body_shape" } };
36
36
  }
37
37
  }
38
38
  reap();
@@ -61,9 +61,9 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
61
61
  if (m) {
62
62
  const run = runs.get(m[1]);
63
63
  if (!run)
64
- return { status: 404, body: { error: "leader run not found" } };
64
+ return { status: 404, body: { error: "leader run not found", errorCode: "not_found.leader_run" } };
65
65
  if (run.owner != null && run.owner !== (requester ?? null)) {
66
- return { status: 404, body: { error: "leader run not found" } };
66
+ return { status: 404, body: { error: "leader run not found", errorCode: "not_found.leader_run" } };
67
67
  }
68
68
  return {
69
69
  status: 200,
package/dist/main.js CHANGED
@@ -24,7 +24,7 @@ import { stat as fsStat, readFile as fsReadFile } from "node:fs/promises";
24
24
  import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, purgeScratchpadDir, sweepStaleScratchpads, resumeFactsForLane } from "./env-facts.js";
25
25
  import { normalizeSuggestNextPrompts, normalizeResilience, normalizeAttachments, normalizeResumeAtMode, resolveTaskLimits, taskAgentsSpecFragment, retainBackgroundProcessesFromBody, toolNameListFromBody, promptProfileFromBody } from "./spec-fields.js";
26
26
  import { createBrain, brainSummary } from "./brain.js";
27
- import { loadConfig, logConfigDiagnostics, resolveBindHost } from "./config.js";
27
+ import { configLkgEnabled, loadConfig, logConfigDiagnostics, resolveBindHost } from "./config.js";
28
28
  import { resourceSuspendOptIn } from "./resource-suspend.js";
29
29
  import { createSessionStore, ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
30
30
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
@@ -247,7 +247,7 @@ async function main() {
247
247
  }
248
248
  };
249
249
  let bootConfigPending;
250
- const lkgEnabled = configProvider?.kind === "remote" && process.env.CONFIG_LKG_DISABLED !== "true";
250
+ const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
251
251
  const lkgPath = process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker);
252
252
  const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
253
253
  let lkgBooted = false;
@@ -1089,8 +1089,8 @@ async function main() {
1089
1089
  executionEnvFactory: executionEnvFactory ? executionEnvFactory : undefined,
1090
1090
  lspManager: lspManager ? lspManager : undefined,
1091
1091
  onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
1092
- loadProjectMemory: config.requirePrincipal !== true && !config.projectMemoryDisabled ? makeLoadProjectMemory({ logger }) : undefined,
1093
- probeInstructionSources: config.remoteExec?.provider === "host" && config.requirePrincipal !== true && !config.projectMemoryDisabled
1092
+ loadProjectMemory: config.requirePrincipal !== true && config.projectMemoryEnabled ? makeLoadProjectMemory({ logger }) : undefined,
1093
+ probeInstructionSources: config.remoteExec?.provider === "host" && config.requirePrincipal !== true && config.projectMemoryEnabled
1094
1094
  ? makeProbeInstructionSources()
1095
1095
  : undefined,
1096
1096
  hooks: deploymentHooks,
@@ -1266,7 +1266,7 @@ async function main() {
1266
1266
  : undefined;
1267
1267
  let skills = loadSkills(config.skillsDir);
1268
1268
  if (effective?.skills && config.configCenter) {
1269
- skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, process.env.CONFIG_LKG_DISABLED !== "true" ? defaultSkillCacheDir() : undefined);
1269
+ skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir() : undefined);
1270
1270
  }
1271
1271
  if (effective?.plugins && config.configCenter) {
1272
1272
  const pluginOut = await applyCenterPlugins(skills, effective, {
@@ -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
@@ -6,6 +6,7 @@ import { randomBytes } from "node:crypto";
6
6
  import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, realpathSync } from "node:fs";
7
7
  import { StringDecoder } from "node:string_decoder";
8
8
  import { armPipeDestroyGrace } from "./remote-shell.js";
9
+ import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
9
10
  import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys } from "./host-platform.js";
10
11
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
11
12
  import { FileError, ExecutionError, RemoteExecutionError, scrubSecretEnv, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
@@ -149,11 +150,11 @@ export class RemoteHostExecutionEnv {
149
150
  }
150
151
  get backgroundCapabilities() {
151
152
  return {
152
- supported: (this.cfg.backgroundShell ?? true) && process.env.HOST_BG_DISABLED !== "true",
153
+ supported: (this.cfg.backgroundShell ?? true) && hostBackgroundShellEnabled(),
153
154
  maxConcurrent: HOST_BG_MAX_CONCURRENT,
154
155
  defaultBgTimeoutSec: HOST_BG_DEFAULT_TIMEOUT_SEC,
155
156
  maxBgTimeoutSec: HOST_BG_MAX_TIMEOUT_SEC,
156
- supportsDetach: (this.cfg.backgroundShell ?? true) && process.env.HOST_BG_DISABLED !== "true",
157
+ supportsDetach: (this.cfg.backgroundShell ?? true) && hostBackgroundShellEnabled(),
157
158
  };
158
159
  }
159
160
  _bgManager;
@@ -331,7 +332,7 @@ export class RemoteHostExecutionEnv {
331
332
  const cwd = this.resolve(options?.cwd ?? this.cwd);
332
333
  const timeoutMs = options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs;
333
334
  const detachSignal = options?.detachSignal;
334
- if (process.platform !== "win32" && process.env.HOST_EXEC_SPOOL_DISABLED !== "true") {
335
+ if (process.platform !== "win32" && hostExecSpoolEnabled()) {
335
336
  const spool = this.openFgSpool();
336
337
  if (spool)
337
338
  return this.execViaSpool(command, options, cwd, timeoutMs, detachSignal, spool);
@@ -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