@sema-agent/server 7.92.0 → 7.92.1
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/config-catalog.js
CHANGED
|
@@ -163,6 +163,7 @@ export const CONFIG_CATALOG = [
|
|
|
163
163
|
r("HOST_BG_READ_CAP", "orchestration", "number", "host 后台 shell 每次轮询增量读上限(字节)", { staticDefault: String(1024 * 1024) }),
|
|
164
164
|
r("HOST_COMMAND_TIMEOUT_MS", "orchestration", "number", "host 车道单命令超时(ms)"),
|
|
165
165
|
r("HOST_EXEC_SPOOL_ENABLED", "orchestration", "boolean", "host 车道执行 spool 开关", { staticDefault: "true" }),
|
|
166
|
+
r("HOST_FS_DRAIN_WAIT_MS", "orchestration", "number", "host 执行环境拆除时等在飞文件写落地的上界(ms;到点 destroy 返回,rm 仍排在那批写之后;下限 1)", { staticDefault: "10000" }),
|
|
166
167
|
r("HOST_SHELL_PATH", "orchestration", "path", "host 车道 shell 覆写口(POSIX 与 win32 同源解析)"),
|
|
167
168
|
r("HOST_WORKSPACE_BASE", "orchestration", "path", "host 车道工作区 base"),
|
|
168
169
|
r("IMAGE_BAKES_ENABLED", "orchestration", "boolean", "烤镜像 job API(/v1/images/bakes*;需 SQL 池 + 非空 operator 名单,boot 断言)", { staticDefault: "false", danger: SEC }),
|
|
@@ -103,6 +103,55 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
103
103
|
* `exitCode 0`。对模型而言那与「成功且无输出」**不可区分**。(与 core [ref] 同形:写已释放的 inode。)
|
|
104
104
|
* 判据与 `hasShells()` 并列:**只要还有人在读写这个根下的文件,就不删它**。 */
|
|
105
105
|
private readonly liveFgSpools;
|
|
106
|
+
/** 🔴 S-539 在飞的 fs **写**登记表(`liveFgSpools` 同律:「还有人在动这棵树,就别删它」)。
|
|
107
|
+
* 为什么按 promise 存而不是计数:只有 promise 才能被 `destroy()` **await**;计数只能轮询。 */
|
|
108
|
+
private readonly inflightFs;
|
|
109
|
+
/** 一次性:慢 drain 只抱怨一次(一条按次数刷屏的 teardown 读数没人会看)。 */
|
|
110
|
+
private inflightFsDrainWarned;
|
|
111
|
+
/**
|
|
112
|
+
* 🔴 S-539 —— 把一只**会在 workspace / spool 根里建路径或改路径**的 fs 动作登记为「在飞」,直到它 settle。
|
|
113
|
+
*
|
|
114
|
+
* ── 病(旧 `destroy()` 的自称与实情)──────────────────────────────────────────────────────────
|
|
115
|
+
* `destroy()` 只 await 了 `ensurePrepared()` 那只 memo(`preparing`),于是它顶注里的
|
|
116
|
+
* 「eliminating the prepared-flag race entirely」只对 **prepare 那一段**成立。写面在
|
|
117
|
+
* `await this.fsReady(p)` 放行**之后**还会自己碰一次磁盘(`writeFile`/`appendFile` 的
|
|
118
|
+
* `mkdir(dirname)`、`createDir` 的 `mkdir`、`createTempDir`/`createTempFile` 在 spool 根下建),
|
|
119
|
+
* 那一次调用与 destroy 的 `fs.rm` 是**两条独立 fs 调用在 libuv 线程池里赛跑**:rm 先 ⇒ `mkdir -p` 把已拆除
|
|
120
|
+
* 的工作区重建成**孤儿**(而 rm 成功后 `untrackEphemeralHostWorkspace()` 已注销 ⇒ 兜底腿也捞不回);
|
|
121
|
+
* mkdir 先 ⇒ 随后 rm 删掉,调用方拿到 `EINVAL`/`ENOENT` 而不是 "already destroyed"。
|
|
122
|
+
*
|
|
123
|
+
* ── 治(一处定义,十几个面共用)────────────────────────────────────────────────────────────────
|
|
124
|
+
* 调用点必须在方法体的**第一个同步段**里调它(第一个 `await` 之前),登记才与 `destroy()` 置 `destroyed`
|
|
125
|
+
* 的同步段互斥。于是二者只有两种相对次序,**两种都安全**:
|
|
126
|
+
* · 先登记 ⇒ destroy 的快照看得到它,`rm` 等它 settle 之后才发;
|
|
127
|
+
* · 后登记 ⇒ 这一面自己的 `fsReady` 看得到 `destroyed`,一个字节都不碰磁盘。
|
|
128
|
+
* 登记放在 `await this.fsReady(...)` **之后**就没有这个性质:那一刻已经跨过一个 microtask,destroy 可能刚好
|
|
129
|
+
* 在这中间把快照取完了。所以形状是 `return this.withInflightFs(async () => { …原方法体… })`,
|
|
130
|
+
* 而不是在方法体里补一句 `if (this.destroyed)` —— 后者仍然不原子(查完到动手之间还有让出点)。
|
|
131
|
+
*
|
|
132
|
+
* ── 读面刻意不进来 ─────────────────────────────────────────────────────────────────────────────
|
|
133
|
+
* ① 读不建路径:拆除之后一次在飞的读拿到 ENOENT 是**诚实**的,复活目录这条病与它无关;
|
|
134
|
+
* ② 把读拉进来会让 teardown 去等一次可能**永不 settle** 的读(workspace 里的 FIFO / 特殊文件),
|
|
135
|
+
* 把 destroy 挂死 —— 拿一条新的可用性病换一条不存在的正确性收益。
|
|
136
|
+
* 判据因此是一句话:**「可能建路径或改路径的那些面」进在飞集合**,不是「过了 fsReady 的都进」。
|
|
137
|
+
*/
|
|
138
|
+
private withInflightFs;
|
|
139
|
+
/**
|
|
140
|
+
* 等 `destroy()` 取到的那批在飞写全部 settle。**永不抛**;答「在时限内清空了吗」。
|
|
141
|
+
*
|
|
142
|
+
* 🔴 两条都不能要,所以有第三条(合并复审 [high] 打出来的):
|
|
143
|
+
* ① 无界等 —— workspace 里的 FIFO / 特殊文件会让一次 `writeFile` **永不 settle**,而拆除在 run 收尾路径上
|
|
144
|
+
* ⇒ 整条收尾停摆。用一条新的可用性病换一条正确性收益,不划算。
|
|
145
|
+
* ② 到点就照删 —— 把 S-539 的竞态**原样**搬回来(rm 与在飞 mkdir 再次在线程池里赛跑),只是把窗口从
|
|
146
|
+
* 「总是」改成「慢的时候」;一条确定的病换一条间歇的病,更糟。
|
|
147
|
+
* ③ **到点就不再占着调用方,但 rm 依旧排在那批写之后**(`destroy()` 里的 `void …then(removeOwnedTrees)`)。
|
|
148
|
+
* 次序保证一个字节不松,teardown 变成「有界返回」。teardown 契约(core 1.258.2)禁的是**抛**,不是**慢**;
|
|
149
|
+
* 这一条连慢都不欠。
|
|
150
|
+
*
|
|
151
|
+
* 快照是 `destroy()` 在同步段取的,所以 destroy **之后**才登记的那些面(唯一一族:`appendFile` 的
|
|
152
|
+
* `lingeringBgWrite` 残余镜像写)不在这一批里 —— 它们按设计就是跨 destroy 存活的,等它们等于等后台 shell。
|
|
153
|
+
*/
|
|
154
|
+
private drainInflightFs;
|
|
106
155
|
private ensureSpoolRoot;
|
|
107
156
|
/** True when `workspaceDir` was given (user-owned persistent dir) → destroy() must NOT delete it. */
|
|
108
157
|
private readonly persistent;
|
|
@@ -155,6 +204,10 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
155
204
|
* core 留活过的驻留 watcher 又杀了一遍(run 尾 dispose→destroy 顺序,destroy 是第二刀)。隔离 env(容器/VM)
|
|
156
205
|
* 整体拆机自然带走进程,与本契约不冲突(k8s/e2b 各自 destroy 不走这条路径)。 */
|
|
157
206
|
destroy(): Promise<void>;
|
|
207
|
+
/** `destroy()` 真正动手删的那一段(本 env **自有**的两棵树:spool 根 + ephemeral workspace)。
|
|
208
|
+
* 抽成一只是因为它有两个调用时机 —— 在飞写按时清空时由 `destroy()` 直接 await,超时时挂在那批写后面异步跑
|
|
209
|
+
* (见 {@link drainInflightFs} 头注③)。永不抛(teardown 契约)。 */
|
|
210
|
+
private removeOwnedTrees;
|
|
158
211
|
cleanup(): Promise<void>;
|
|
159
212
|
get backgroundCapabilities(): BackgroundShellCapability["backgroundCapabilities"];
|
|
160
213
|
private _bgManager?;
|
|
@@ -210,7 +263,12 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
210
263
|
abortSignal?: AbortSignal;
|
|
211
264
|
}): Promise<Result<string[], FileError>>;
|
|
212
265
|
readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
|
|
266
|
+
/** S-539:整只方法体(**含** `fsReady` 那道门)走 `withInflightFs` —— 登记必须与调用方同一同步段,
|
|
267
|
+
* 否则 destroy 的快照可能刚好在 `await this.fsReady(...)` 的让出点上取完。见那处头注。 */
|
|
213
268
|
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
269
|
+
/** S-539(写面一律 `withInflightFs`,见那处头注)。本面是唯一能在 `destroyed` **之后**过门的写面
|
|
270
|
+
* (`lingeringBgWrite`):那一族登记在 destroy 的快照**之后**,所以 destroy 不会去等它们 —— 正确,
|
|
271
|
+
* 它们按设计跨 destroy 存活(等它们等于等后台 shell)。 */
|
|
214
272
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
215
273
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
216
274
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
@@ -231,10 +289,14 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
231
289
|
*/
|
|
232
290
|
canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
|
|
233
291
|
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
292
|
+
/** S-539(写面一律 `withInflightFs`,见那处头注)。 */
|
|
234
293
|
createDir(p: string, options?: {
|
|
235
294
|
recursive?: boolean;
|
|
236
295
|
abortSignal?: AbortSignal;
|
|
237
296
|
}): Promise<Result<void, FileError>>;
|
|
297
|
+
/** S-539(写面一律 `withInflightFs`,见那处头注)。本面**不会**复活目录(`fs.rm` 只删),进在飞集合的理由是
|
|
298
|
+
* 同一条:destroy 的 rm 把树从它脚下抽走 ⇒ 一次本该 ok 的删返回 `not_found`。判据是「可能建或改路径」,
|
|
299
|
+
* 不是「可能复活目录」—— 少一条例外就少一处以后要重新想清楚的地方。 */
|
|
238
300
|
remove(p: string, options?: {
|
|
239
301
|
recursive?: boolean;
|
|
240
302
|
force?: boolean;
|
|
@@ -252,7 +314,10 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
252
314
|
* · 镜像写仍然可用 —— `isLingeringMirrorTarget` 同批放行这个根(见那里的旁注);
|
|
253
315
|
* · ephemeral 形反而更好:`destroy()` 在**有后台 shell 在册**时不删这个根,而 workspace 是会被 rm 的。
|
|
254
316
|
* ⚠️ 这是 **host-only**:k8s/local-docker 的临时文件必须落在**容器内**可见的 cwd,ssh/e2b 本来就用远端系统 tmp。 */
|
|
317
|
+
/** S-539(写面一律 `withInflightFs`,见那处头注)。这一面建在 **spool 根**下,而 destroy 也会整树删那个根 ——
|
|
318
|
+
* `fs.mkdtemp` 不建父目录,所以它不会复活什么,坏处是一次本该成功的调用返回 `not_found`。 */
|
|
255
319
|
createTempDir(prefix?: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
320
|
+
/** S-539(写面一律 `withInflightFs`,见那处头注;spool 根同 `createTempDir`)。 */
|
|
256
321
|
createTempFile(options?: {
|
|
257
322
|
prefix?: string;
|
|
258
323
|
suffix?: string;
|
|
@@ -14,6 +14,7 @@ import { recordSecretEnvScrub } from "../observability/secret-env-scrub.js";
|
|
|
14
14
|
import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
|
|
15
15
|
const PROVIDER = "host";
|
|
16
16
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
|
|
17
|
+
const inflightFsDrainWaitMs = () => numEnvOr("HOST_FS_DRAIN_WAIT_MS", 10_000, 1);
|
|
17
18
|
const HOST_BG_MAX_CONCURRENT = numEnvOr("HOST_BG_MAX_CONCURRENT", 8, 1);
|
|
18
19
|
const HOST_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("HOST_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
|
|
19
20
|
const HOST_BG_MAX_TIMEOUT_SEC = numEnvOr("HOST_BG_MAX_TIMEOUT_SEC", 1800, HOST_BG_DEFAULT_TIMEOUT_SEC);
|
|
@@ -62,6 +63,41 @@ export class RemoteHostExecutionEnv {
|
|
|
62
63
|
destroyed = false;
|
|
63
64
|
spoolRootDir;
|
|
64
65
|
liveFgSpools = new Set();
|
|
66
|
+
inflightFs = new Set();
|
|
67
|
+
inflightFsDrainWarned = false;
|
|
68
|
+
withInflightFs(fn) {
|
|
69
|
+
const p = fn();
|
|
70
|
+
this.inflightFs.add(p);
|
|
71
|
+
const forget = () => void this.inflightFs.delete(p);
|
|
72
|
+
void p.then(forget, forget);
|
|
73
|
+
return p;
|
|
74
|
+
}
|
|
75
|
+
async drainInflightFs(inflight) {
|
|
76
|
+
if (inflight.length === 0)
|
|
77
|
+
return true;
|
|
78
|
+
const settled = Promise.allSettled(inflight);
|
|
79
|
+
const waitMs = inflightFsDrainWaitMs();
|
|
80
|
+
const timedOut = Symbol("inflight-fs-drain-timeout");
|
|
81
|
+
let timer;
|
|
82
|
+
const deadline = new Promise((resolve) => {
|
|
83
|
+
timer = setTimeout(() => resolve(timedOut), waitMs);
|
|
84
|
+
timer.unref?.();
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const drained = (await Promise.race([settled, deadline])) !== timedOut;
|
|
88
|
+
if (!drained && !this.inflightFsDrainWarned) {
|
|
89
|
+
this.inflightFsDrainWarned = true;
|
|
90
|
+
console.warn(`RemoteHostExecutionEnv.destroy: ${this.inflightFs.size} in-flight filesystem write(s) did not settle within ` +
|
|
91
|
+
`${waitMs}ms — returning from teardown now; removal of ${this.workspaceDir} stays ordered AFTER those writes ` +
|
|
92
|
+
`and will land when they finish (a write that never settles, e.g. a FIFO target inside the workspace, defers it)`);
|
|
93
|
+
}
|
|
94
|
+
return drained;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (timer)
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
65
101
|
ensureSpoolRoot() {
|
|
66
102
|
if (!this.spoolRootDir)
|
|
67
103
|
this.spoolRootDir = mkdtempSync(path.join(os.tmpdir(), "sema-host-spool-"));
|
|
@@ -130,25 +166,32 @@ export class RemoteHostExecutionEnv {
|
|
|
130
166
|
this.destroyed = true;
|
|
131
167
|
const p = this.preparing;
|
|
132
168
|
this.preparing = undefined;
|
|
169
|
+
const inflight = [...this.inflightFs];
|
|
133
170
|
try {
|
|
134
171
|
await p?.catch(() => { });
|
|
135
172
|
}
|
|
136
173
|
finally {
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return;
|
|
174
|
+
if (await this.drainInflightFs(inflight))
|
|
175
|
+
await this.removeOwnedTrees();
|
|
176
|
+
else
|
|
177
|
+
void Promise.allSettled(inflight).then(() => this.removeOwnedTrees());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async removeOwnedTrees() {
|
|
181
|
+
if (this.spoolRootDir && !this.bgManager.hasShells() && this.liveFgSpools.size === 0) {
|
|
146
182
|
try {
|
|
147
|
-
await fs.rm(this.
|
|
148
|
-
untrackEphemeralHostWorkspace(this.workspaceDir);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
183
|
+
await fs.rm(this.spoolRootDir, { recursive: true, force: true });
|
|
151
184
|
}
|
|
185
|
+
catch { }
|
|
186
|
+
this.spoolRootDir = undefined;
|
|
187
|
+
}
|
|
188
|
+
if (this.persistent)
|
|
189
|
+
return;
|
|
190
|
+
try {
|
|
191
|
+
await fs.rm(this.workspaceDir, { recursive: true, force: true });
|
|
192
|
+
untrackEphemeralHostWorkspace(this.workspaceDir);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
152
195
|
}
|
|
153
196
|
}
|
|
154
197
|
async cleanup() {
|
|
@@ -905,37 +948,41 @@ export class RemoteHostExecutionEnv {
|
|
|
905
948
|
return { ok: false, error: this.fileError(e, abs, abortSignal) };
|
|
906
949
|
}
|
|
907
950
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
951
|
+
writeFile(p, content, abortSignal) {
|
|
952
|
+
return this.withInflightFs(async () => {
|
|
953
|
+
const ready = await this.fsReady(p);
|
|
954
|
+
if (!ready.ok)
|
|
955
|
+
return ready;
|
|
956
|
+
const abs = this.resolve(p);
|
|
957
|
+
try {
|
|
958
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
959
|
+
const data = typeof content === "string" ? content : Buffer.from(content);
|
|
960
|
+
await fs.writeFile(abs, data, { signal: abortSignal });
|
|
961
|
+
return ok(undefined);
|
|
962
|
+
}
|
|
963
|
+
catch (e) {
|
|
964
|
+
return { ok: false, error: this.fileError(e, abs, abortSignal) };
|
|
965
|
+
}
|
|
966
|
+
});
|
|
922
967
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
968
|
+
appendFile(p, content, abortSignal) {
|
|
969
|
+
return this.withInflightFs(async () => {
|
|
970
|
+
const ready = await this.fsReady(p, { lingeringBgWrite: true });
|
|
971
|
+
if (!ready.ok)
|
|
972
|
+
return ready;
|
|
973
|
+
const abs = this.resolve(p);
|
|
974
|
+
try {
|
|
975
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
976
|
+
if (abortSignal?.aborted)
|
|
977
|
+
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
978
|
+
const data = typeof content === "string" ? content : Buffer.from(content);
|
|
979
|
+
await fs.appendFile(abs, data);
|
|
980
|
+
return ok(undefined);
|
|
981
|
+
}
|
|
982
|
+
catch (e) {
|
|
983
|
+
return { ok: false, error: this.fileError(e, abs, abortSignal) };
|
|
984
|
+
}
|
|
985
|
+
});
|
|
939
986
|
}
|
|
940
987
|
async fileInfo(p, abortSignal) {
|
|
941
988
|
const ready = await this.fsReady(p);
|
|
@@ -1058,66 +1105,74 @@ export class RemoteHostExecutionEnv {
|
|
|
1058
1105
|
return { ok: false, error: fe };
|
|
1059
1106
|
}
|
|
1060
1107
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
catch (e) {
|
|
1071
|
-
return { ok: false, error: this.fileError(e, abs, options?.abortSignal) };
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
async remove(p, options) {
|
|
1075
|
-
const ready = await this.fsReady(p);
|
|
1076
|
-
if (!ready.ok)
|
|
1077
|
-
return ready;
|
|
1078
|
-
const abs = this.resolve(p);
|
|
1079
|
-
try {
|
|
1080
|
-
if (!options?.force) {
|
|
1081
|
-
await fs.lstat(abs);
|
|
1108
|
+
createDir(p, options) {
|
|
1109
|
+
return this.withInflightFs(async () => {
|
|
1110
|
+
const ready = await this.fsReady(p);
|
|
1111
|
+
if (!ready.ok)
|
|
1112
|
+
return ready;
|
|
1113
|
+
const abs = this.resolve(p);
|
|
1114
|
+
try {
|
|
1115
|
+
await fs.mkdir(abs, { recursive: options?.recursive ?? true });
|
|
1116
|
+
return ok(undefined);
|
|
1082
1117
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1118
|
+
catch (e) {
|
|
1119
|
+
return { ok: false, error: this.fileError(e, abs, options?.abortSignal) };
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
remove(p, options) {
|
|
1124
|
+
return this.withInflightFs(async () => {
|
|
1125
|
+
const ready = await this.fsReady(p);
|
|
1126
|
+
if (!ready.ok)
|
|
1127
|
+
return ready;
|
|
1128
|
+
const abs = this.resolve(p);
|
|
1129
|
+
try {
|
|
1130
|
+
if (!options?.force) {
|
|
1131
|
+
await fs.lstat(abs);
|
|
1132
|
+
}
|
|
1133
|
+
await fs.rm(abs, { recursive: options?.recursive ?? false, force: options?.force ?? false });
|
|
1089
1134
|
return ok(undefined);
|
|
1090
|
-
|
|
1091
|
-
|
|
1135
|
+
}
|
|
1136
|
+
catch (e) {
|
|
1137
|
+
const fe = this.fileError(e, abs, options?.abortSignal);
|
|
1138
|
+
if (options?.force && fe.code === "not_found")
|
|
1139
|
+
return ok(undefined);
|
|
1140
|
+
return { ok: false, error: fe };
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1092
1143
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1144
|
+
createTempDir(prefix = "tmp-", abortSignal) {
|
|
1145
|
+
return this.withInflightFs(async () => {
|
|
1146
|
+
const ready = await this.fsReady(".");
|
|
1147
|
+
if (!ready.ok)
|
|
1148
|
+
return ready;
|
|
1149
|
+
try {
|
|
1150
|
+
return ok(await fs.mkdtemp(path.join(this.ensureSpoolRoot(), prefix)));
|
|
1151
|
+
}
|
|
1152
|
+
catch (e) {
|
|
1153
|
+
return { ok: false, error: this.fileError(e, this.workspaceDir, abortSignal) };
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1103
1156
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1157
|
+
createTempFile(options) {
|
|
1158
|
+
return this.withInflightFs(async () => {
|
|
1159
|
+
const ready = await this.fsReady(".");
|
|
1160
|
+
if (!ready.ok)
|
|
1161
|
+
return ready;
|
|
1162
|
+
const prefix = options?.prefix ?? "";
|
|
1163
|
+
const suffix = options?.suffix ?? "";
|
|
1164
|
+
try {
|
|
1165
|
+
const name = `${prefix}${randomBytes(8).toString("hex")}${suffix}`;
|
|
1166
|
+
const abs = path.join(this.ensureSpoolRoot(), name);
|
|
1167
|
+
if (options?.abortSignal?.aborted)
|
|
1168
|
+
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
1169
|
+
await fs.writeFile(abs, "", { flag: "wx" });
|
|
1170
|
+
return ok(abs);
|
|
1171
|
+
}
|
|
1172
|
+
catch (e) {
|
|
1173
|
+
return { ok: false, error: this.fileError(e, this.workspaceDir, options?.abortSignal) };
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1121
1176
|
}
|
|
1122
1177
|
resolve(p) {
|
|
1123
1178
|
return path.isAbsolute(p) ? path.normalize(p) : path.normalize(path.resolve(this.cwd, p));
|
|
@@ -1207,7 +1262,7 @@ export class RemoteHostExecutionEnv {
|
|
|
1207
1262
|
const rel = path.relative(real(root), real(abs));
|
|
1208
1263
|
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
1209
1264
|
};
|
|
1210
|
-
return inside(this.
|
|
1265
|
+
return (this.spoolRootDir !== undefined && inside(this.spoolRootDir)) || (this.persistent && inside(this.workspaceDir));
|
|
1211
1266
|
}
|
|
1212
1267
|
async fsReady(p, opts) {
|
|
1213
1268
|
if (this.destroyed) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.92.
|
|
3
|
+
"version": "7.92.1",
|
|
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",
|