@actiondock/core 2.20.0 → 2.21.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 (67) hide show
  1. package/dist/errors.d.ts +11 -1
  2. package/dist/errors.js +23 -0
  3. package/dist/execution/service.js +36 -5
  4. package/dist/host/host.d.ts +11 -0
  5. package/dist/host/host.js +258 -39
  6. package/dist/host/types.d.ts +10 -1
  7. package/dist/index.d.ts +8 -1
  8. package/dist/index.js +5 -1
  9. package/dist/invocation/types.d.ts +31 -0
  10. package/dist/invocation/types.js +10 -1
  11. package/dist/package/index.d.ts +1 -1
  12. package/dist/package/runtime.js +18 -4
  13. package/dist/platform/node.d.ts +6 -0
  14. package/dist/platform/node.js +59 -16
  15. package/dist/platform/types.d.ts +11 -0
  16. package/dist/process/command-executor.d.ts +35 -0
  17. package/dist/process/command-executor.js +326 -0
  18. package/dist/process/index.d.ts +1 -0
  19. package/dist/process/index.js +1 -0
  20. package/dist/process/process-driver.d.ts +19 -6
  21. package/dist/process/process-driver.js +223 -54
  22. package/dist/process/run-executor.js +101 -47
  23. package/dist/project/dependencies.d.ts +92 -0
  24. package/dist/project/dependencies.js +543 -0
  25. package/dist/project/index.d.ts +3 -0
  26. package/dist/project/index.js +3 -0
  27. package/dist/project/init.js +2 -2
  28. package/dist/project/loader.js +15 -30
  29. package/dist/project/package-manager.d.ts +40 -0
  30. package/dist/project/package-manager.js +224 -0
  31. package/dist/project/preparation.d.ts +8 -0
  32. package/dist/project/preparation.js +130 -0
  33. package/dist/project/transactions.d.ts +5 -1
  34. package/dist/project/transactions.js +35 -75
  35. package/dist/runtime/context.d.ts +21 -2
  36. package/dist/runtime/context.js +42 -17
  37. package/dist/runtime/run-persistence.d.ts +1 -1
  38. package/dist/runtime/run-persistence.js +4 -3
  39. package/dist/security/config-cipher.d.ts +54 -0
  40. package/dist/security/config-cipher.js +190 -0
  41. package/dist/security/config-key.d.ts +35 -0
  42. package/dist/security/config-key.js +469 -0
  43. package/dist/server/routes/config.js +20 -1
  44. package/dist/service/factory.js +3 -0
  45. package/dist/service/types.d.ts +8 -0
  46. package/dist/storage/config-value.d.ts +33 -0
  47. package/dist/storage/config-value.js +102 -0
  48. package/dist/storage/file-lock.js +35 -21
  49. package/dist/storage/index.d.ts +24 -2
  50. package/dist/storage/index.js +65 -2
  51. package/dist/storage/migrations/index.d.ts +40 -0
  52. package/dist/storage/migrations/index.js +96 -0
  53. package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
  54. package/dist/storage/migrations/v2-to-v3.js +116 -0
  55. package/dist/storage/sqlite-config.d.ts +62 -3
  56. package/dist/storage/sqlite-config.js +163 -9
  57. package/dist/storage/sqlite-driver.d.ts +5 -2
  58. package/dist/storage/sqlite-driver.js +24 -3
  59. package/dist/storage/sqlite-schema.d.ts +20 -2
  60. package/dist/storage/sqlite-schema.js +129 -132
  61. package/dist/storage/sqlite.d.ts +7 -2
  62. package/dist/storage/sqlite.js +64 -13
  63. package/dist/storage/types.d.ts +72 -4
  64. package/dist/storage/types.js +7 -2
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/package.json +2 -2
@@ -0,0 +1,326 @@
1
+ import { INVALID_ARGUMENT, INPUT_CLOSED, PROCESS_CANCELLED, PROCESS_SPAWN_ERROR, PROCESS_TIMEOUT, ProcessError, } from "../errors.js";
2
+ import { NodeProcessDriver } from "./process-driver.js";
3
+ /**
4
+ * 独立深拷贝字节切片,脱离原始底层 ArrayBuffer 并杜绝驱动缓冲区复用污染。
5
+ */
6
+ function cloneSlice(source, length) {
7
+ const cloned = new Uint8Array(length);
8
+ cloned.set(source.subarray(0, length));
9
+ return cloned;
10
+ }
11
+ /**
12
+ * 辅助将多个字节切片合并并解码为 UTF-8 文本。
13
+ */
14
+ function decodeChunks(chunks, totalBytes) {
15
+ if (totalBytes === 0) {
16
+ return "";
17
+ }
18
+ const merged = new Uint8Array(totalBytes);
19
+ let offset = 0;
20
+ for (const chunk of chunks) {
21
+ merged.set(chunk, offset);
22
+ offset += chunk.byteLength;
23
+ }
24
+ return new TextDecoder("utf-8").decode(merged);
25
+ }
26
+ /**
27
+ * 单流有界输出缓冲。
28
+ *
29
+ * 契约:
30
+ * - 超出上限的切片截断保留并置 truncated,之后的数据仅标记不再累积;
31
+ * - 捕获字节经独立深拷贝隔离,脱离驱动底层缓冲区以防复用污染;
32
+ * - decode 一次性合并全部切片并按 UTF-8 解码。
33
+ */
34
+ function makeStreamBuffer(maxBytes) {
35
+ const chunks = [];
36
+ let totalBytes = 0;
37
+ let truncated = false;
38
+ return {
39
+ push(data) {
40
+ const remaining = maxBytes - totalBytes;
41
+ if (remaining > 0) {
42
+ if (data.byteLength <= remaining) {
43
+ chunks.push(cloneSlice(data, data.byteLength));
44
+ totalBytes += data.byteLength;
45
+ }
46
+ else {
47
+ chunks.push(cloneSlice(data, remaining));
48
+ totalBytes += remaining;
49
+ truncated = true;
50
+ }
51
+ }
52
+ else {
53
+ truncated = true;
54
+ }
55
+ },
56
+ decode() {
57
+ return decodeChunks(chunks, totalBytes);
58
+ },
59
+ get truncated() {
60
+ return truncated;
61
+ },
62
+ };
63
+ }
64
+ /**
65
+ * 面向工程工具的一次性命令适配执行器。
66
+ *
67
+ * 核心设计契约:
68
+ * - 复用底层 ProcessDriver 平台驱动,严格保持进程组隔离与进程树收敛能力;
69
+ * - 绝不启用系统 Shell,参数列表原样透传,杜绝命令注入与参数转义漏洞;
70
+ * - 默认每流上限 512KiB 字节有界捕获,超出上限继续排空或转发并置 truncated 标志;
71
+ * - 捕获字节独立深拷贝,脱离原始底层缓冲区以防内存滞留与复用篡改;
72
+ * - 仅当显式指定 onOutput 时流式转发,严格保证跨分块 UTF-8 字符解码完整性;
73
+ * - 规范化取消、超时与异常收敛:等待 driver.terminate 与 outputClosed 终态后抛出结构化 ProcessError。
74
+ */
75
+ export async function executeCommand(executable, args, options) {
76
+ // 参数合法性前置校验
77
+ if (options?.maxOutputBytes !== undefined) {
78
+ if (!Number.isSafeInteger(options.maxOutputBytes) ||
79
+ options.maxOutputBytes < 0) {
80
+ throw new ProcessError(INVALID_ARGUMENT, "maxOutputBytes must be a non-negative finite number");
81
+ }
82
+ }
83
+ if (options?.timeoutMs !== undefined) {
84
+ if (!Number.isFinite(options.timeoutMs) ||
85
+ options.timeoutMs > 2_147_483_647 ||
86
+ options.timeoutMs < 0) {
87
+ throw new ProcessError(INVALID_ARGUMENT, "timeoutMs must be a non-negative finite number");
88
+ }
89
+ }
90
+ const driver = options?.driver ??
91
+ new NodeProcessDriver({ waitForOutputClose: true });
92
+ // 若信号在启动前已触发中止,直接以 PROCESS_CANCELLED 拒绝
93
+ if (options?.signal?.aborted) {
94
+ throw new ProcessError(PROCESS_CANCELLED, `Command execution was cancelled before spawn: ${executable}`, { cause: options.signal.reason, executable, args: [...args] });
95
+ }
96
+ // 组装环境变量:未传 env 时显式复制 process.env 中非 undefined 项,通过 inherit: 'none' 保留私有凭据与配置
97
+ const envSet = {};
98
+ if (options?.env) {
99
+ for (const [k, v] of Object.entries(options.env)) {
100
+ if (v !== undefined) {
101
+ envSet[k] = v;
102
+ }
103
+ }
104
+ }
105
+ else {
106
+ for (const [k, v] of Object.entries(process.env)) {
107
+ if (v !== undefined) {
108
+ envSet[k] = v;
109
+ }
110
+ }
111
+ }
112
+ const spec = {
113
+ executable,
114
+ args: [...args],
115
+ cwd: options?.cwd,
116
+ env: {
117
+ inherit: "none",
118
+ set: envSet,
119
+ },
120
+ io: {
121
+ mode: "pipe",
122
+ },
123
+ };
124
+ const maxBytes = options?.maxOutputBytes ?? 512 * 1024;
125
+ const stdoutBuffer = makeStreamBuffer(maxBytes);
126
+ const stderrBuffer = makeStreamBuffer(maxBytes);
127
+ const stdoutDecoder = options?.onOutput ? new TextDecoder("utf-8") : undefined;
128
+ const stderrDecoder = options?.onOutput ? new TextDecoder("utf-8") : undefined;
129
+ let abortReason = null;
130
+ let abortCause = null;
131
+ let resolveAbort;
132
+ const abortTriggeredPromise = new Promise((r) => {
133
+ resolveAbort = r;
134
+ });
135
+ const triggerAbort = (reason, cause) => {
136
+ if (abortReason)
137
+ return;
138
+ abortReason = reason;
139
+ abortCause = cause;
140
+ resolveAbort();
141
+ };
142
+ const dispatchOutput = (stream, text) => {
143
+ if (state.closed || abortReason !== null || !options?.onOutput)
144
+ return;
145
+ try {
146
+ options.onOutput(stream, text);
147
+ }
148
+ catch (err) {
149
+ triggerAbort("output-error", err);
150
+ }
151
+ };
152
+ const flushDecoders = () => {
153
+ if (stdoutDecoder) {
154
+ const rem = stdoutDecoder.decode();
155
+ if (rem.length > 0) {
156
+ dispatchOutput("stdout", rem);
157
+ }
158
+ }
159
+ if (stderrDecoder) {
160
+ const rem = stderrDecoder.decode();
161
+ if (rem.length > 0) {
162
+ dispatchOutput("stderr", rem);
163
+ }
164
+ }
165
+ };
166
+ const state = { exitResult: null, faultError: null, closed: false };
167
+ let resolveOutputClosed;
168
+ const outputClosedPromise = new Promise((r) => {
169
+ resolveOutputClosed = r;
170
+ });
171
+ // 在 spawn 之前创建 observer 与关联 promise,杜绝同步驱动或测试桩发出的生命周期事件丢失
172
+ const observer = {
173
+ output(stream, data) {
174
+ if (state.closed || abortReason !== null) {
175
+ return;
176
+ }
177
+ // pty 输出不属于一次性管道命令的捕获范围,保持与原实现一致地忽略
178
+ if (stream !== "stdout" && stream !== "stderr") {
179
+ return;
180
+ }
181
+ const buffer = stream === "stdout" ? stdoutBuffer : stderrBuffer;
182
+ const decoder = stream === "stdout" ? stdoutDecoder : stderrDecoder;
183
+ buffer.push(data);
184
+ if (decoder) {
185
+ const text = decoder.decode(data, { stream: true });
186
+ if (text.length > 0) {
187
+ dispatchOutput(stream, text);
188
+ }
189
+ }
190
+ },
191
+ exited(result) {
192
+ state.exitResult = result;
193
+ },
194
+ outputClosed(reason = "natural") {
195
+ if (state.closed)
196
+ return;
197
+ if (reason === "natural") {
198
+ flushDecoders();
199
+ state.closed = true;
200
+ resolveOutputClosed();
201
+ return;
202
+ }
203
+ state.closed = true;
204
+ if (reason === "drain-timeout") {
205
+ triggerAbort("drain-timeout", new ProcessError(PROCESS_TIMEOUT, `Process output drain timed out before completion: ${executable}`, { executable, args: [...args] }));
206
+ }
207
+ else {
208
+ triggerAbort("host-lost", new ProcessError(PROCESS_SPAWN_ERROR, `Process host connection was lost: ${executable}`, { executable, args: [...args] }));
209
+ }
210
+ resolveOutputClosed();
211
+ },
212
+ fault(err) {
213
+ state.faultError = err;
214
+ triggerAbort("fault", err);
215
+ },
216
+ };
217
+ // 绑定外部取消信号与超时定时器
218
+ let timeoutTimer;
219
+ const onSignalAbort = () => {
220
+ triggerAbort("cancelled", options?.signal?.reason);
221
+ };
222
+ if (options?.signal) {
223
+ options.signal.addEventListener("abort", onSignalAbort, { once: true });
224
+ // 监听注册后再次检查,消除注册前后的时序竞态
225
+ if (options.signal.aborted) {
226
+ triggerAbort("cancelled", options.signal.reason);
227
+ }
228
+ }
229
+ if (options?.timeoutMs && options.timeoutMs > 0) {
230
+ timeoutTimer = setTimeout(() => {
231
+ triggerAbort("timeout");
232
+ }, options.timeoutMs);
233
+ }
234
+ let handle;
235
+ // 将当前中止原因与关联异常映射为规范的结构化 ProcessError
236
+ const buildAbortError = () => {
237
+ switch (abortReason) {
238
+ case "output-error":
239
+ return abortCause;
240
+ case "fault":
241
+ return new ProcessError(PROCESS_SPAWN_ERROR, `Process reported fault: ${state.faultError?.message ?? String(abortCause)}`, { cause: abortCause, executable });
242
+ case "drain-timeout":
243
+ case "host-lost":
244
+ if (abortCause instanceof Error) {
245
+ return abortCause;
246
+ }
247
+ return abortReason === "drain-timeout"
248
+ ? new ProcessError(PROCESS_TIMEOUT, `Process output drain timed out before completion: ${executable}`, { executable, args: [...args] })
249
+ : new ProcessError(PROCESS_SPAWN_ERROR, `Process host connection was lost: ${executable}`, { executable, args: [...args] });
250
+ case "timeout":
251
+ return new ProcessError(PROCESS_TIMEOUT, `Command execution timed out after ${options?.timeoutMs}ms: ${executable}`, { timeoutMs: options?.timeoutMs, executable });
252
+ default:
253
+ return new ProcessError(PROCESS_CANCELLED, `Command execution was cancelled: ${executable}`, { cause: abortCause, executable });
254
+ }
255
+ };
256
+ // 统一的中止清理与错误抛出函数:真实等待驱动终止与输出闭合,绝不吞错,绝不假放行
257
+ const performAbortCleanupAndThrow = async (activeHandle) => {
258
+ if (activeHandle) {
259
+ await driver.terminate(activeHandle, abortReason === "output-error" ? 0 : 200);
260
+ await outputClosedPromise;
261
+ }
262
+ throw buildAbortError();
263
+ };
264
+ try {
265
+ try {
266
+ handle = await driver.spawn(spec, observer);
267
+ }
268
+ catch (err) {
269
+ const spawnErr = err instanceof ProcessError && err.code === PROCESS_SPAWN_ERROR
270
+ ? err
271
+ : new ProcessError(PROCESS_SPAWN_ERROR, `Failed to spawn command "${executable}": ${err?.message || String(err)}`, { cause: err, executable, args: [...args] });
272
+ throw spawnErr;
273
+ }
274
+ // 检查在派生过程(await driver.spawn)中是否发生了取消、超时或异常
275
+ if (abortReason) {
276
+ await performAbortCleanupAndThrow(handle);
277
+ }
278
+ // 派生成功后立即发送 EOF 关闭标准输入,防止子进程因等待输入而死锁挂起
279
+ if (driver.inputEOF && !state.closed) {
280
+ const eof = Promise.resolve().then(() => driver.inputEOF(handle)).then(() => ({ error: undefined, failed: false }), (error) => ({ error, failed: true }));
281
+ const result = await Promise.race([
282
+ eof,
283
+ abortTriggeredPromise.then(() => undefined),
284
+ ]);
285
+ if (result?.failed) {
286
+ const error = result.error;
287
+ const isClosedError = error?.code === INPUT_CLOSED || error?.code === "EPIPE" || error?.code === "ERR_STREAM_DESTROYED";
288
+ if (!isClosedError && !state.closed) {
289
+ triggerAbort("output-error", result.error);
290
+ }
291
+ }
292
+ }
293
+ // 检查发送 EOF 期间是否已被触发中止
294
+ if (abortReason) {
295
+ await performAbortCleanupAndThrow(handle);
296
+ }
297
+ // 等待输出完全收敛或中途触发中止
298
+ await Promise.race([outputClosedPromise, abortTriggeredPromise]);
299
+ if (abortReason) {
300
+ await performAbortCleanupAndThrow(handle);
301
+ }
302
+ // 若底层 observer 上报严重故障,无论 exitResult 是否存在,均终结为 PROCESS_SPAWN_ERROR
303
+ if (state.faultError) {
304
+ throw new ProcessError(PROCESS_SPAWN_ERROR, `Process reported fault: ${state.faultError.message}`, { cause: state.faultError, executable });
305
+ }
306
+ return {
307
+ status: state.exitResult?.code ?? null,
308
+ signal: state.exitResult?.signal ?? null,
309
+ stdout: stdoutBuffer.decode(),
310
+ stderr: stderrBuffer.decode(),
311
+ truncated: stdoutBuffer.truncated || stderrBuffer.truncated,
312
+ };
313
+ }
314
+ finally {
315
+ if (timeoutTimer) {
316
+ clearTimeout(timeoutTimer);
317
+ timeoutTimer = undefined;
318
+ }
319
+ if (options?.signal) {
320
+ options.signal.removeEventListener("abort", onSignalAbort);
321
+ }
322
+ if (handle && driver.dispose) {
323
+ await driver.dispose(handle);
324
+ }
325
+ }
326
+ }
@@ -10,4 +10,5 @@ export * from "./input-dispatcher.js";
10
10
  export * from "./process-manager.js";
11
11
  export * from "./context-process.js";
12
12
  export * from "./process-driver.js";
13
+ export * from "./command-executor.js";
13
14
  export * from "./converters.js";
@@ -10,4 +10,5 @@ export * from "./input-dispatcher.js";
10
10
  export * from "./process-manager.js";
11
11
  export * from "./context-process.js";
12
12
  export * from "./process-driver.js";
13
+ export * from "./command-executor.js";
13
14
  export * from "./converters.js";
@@ -3,6 +3,17 @@ import type { Capabilities, LaunchSpec } from "@actiondock/sdk";
3
3
  import { DEFAULT_ENV_ALLOWLIST, resolveProcessEnv, type ProcessDriver, type ProcessDriverCallbacks, type ProcessDriverHandle, type ProcessHandle, type ProcessObserver } from "./driver.js";
4
4
  export { DEFAULT_ENV_ALLOWLIST, resolveProcessEnv };
5
5
  export type { ProcessDriver, ProcessObserver, ProcessHandle, ProcessDriverCallbacks, ProcessDriverHandle };
6
+ /**
7
+ * 跨平台探测进程组是否在操作系统中仍然存活。
8
+ *
9
+ * - 在 POSIX 环境下驱动创建了独立进程组,仅向负数进程标识 (-pid) 发送探测信号 0;
10
+ * 若返回 EPERM 视为仍存活;若返回 ESRCH 则表示进程组已彻底消亡;其他错误不静默当作消亡;
11
+ * 绝不对正 pid 探测,杜绝命中操作系统已重用 PID 的其他进程。
12
+ * - 在 Windows 环境下通过 process.kill(pid, 0) 探测父进程存活性。
13
+ *
14
+ * @param pid 目标进程标识
15
+ */
16
+ export declare function isProcessGroupAlive(pid: number): boolean;
6
17
  /**
7
18
  * 跨平台终止进程组,确保不会遗留孤儿进程。
8
19
  *
@@ -22,6 +33,8 @@ export interface NodeProcessDriverOptions {
22
33
  drainDeadlineMs?: number;
23
34
  /** 进程派生函数,默认为 childProcess.spawn */
24
35
  spawnFn?: typeof spawn;
36
+ /** 是否在自然 exit 后等待输出流真实 close(默认 false) */
37
+ waitForOutputClose?: boolean;
25
38
  }
26
39
  /**
27
40
  * 基于 Node.js 标准子进程实现的平台驱动。
@@ -37,6 +50,7 @@ export interface NodeProcessDriverOptions {
37
50
  export declare class NodeProcessDriver implements ProcessDriver {
38
51
  private readonly drainDeadlineMs;
39
52
  readonly spawnFn: typeof spawn;
53
+ private readonly waitForOutputClose;
40
54
  private readonly instances;
41
55
  constructor(options?: NodeProcessDriverOptions);
42
56
  /**
@@ -86,16 +100,15 @@ export declare class NodeProcessDriver implements ProcessDriver {
86
100
  */
87
101
  resize(handle: ProcessHandle, cols: number, rows: number): Promise<void>;
88
102
  /**
89
- * 优雅终止进程,超时未退出则发送 SIGKILL 强杀兜底。
90
- * 终态自清理(selfRelease)或 dispose 先行移除实例时视为已终止,直接无害返回。
103
+ * 优雅终止进程整棵进程树,超时未退出则发送 SIGKILL 强杀兜底。
104
+ * 修复父进程先退出导致的短路,确保整组存活成员(包括忽略 SIGTERM 的孙进程及未闭合输出的活实例)被彻底终止,
105
+ * 并等待父进程与输出收敛。
91
106
  */
92
107
  terminate(handle: ProcessHandle, graceMs: number): Promise<void>;
93
108
  /**
94
109
  * 销毁进程句柄并清理相关资源。
95
- * 对仍在运行的实例先执行 terminate(0) 兜底(SIGTERM 后立即 SIGKILL),
96
- * 绝不从受管表移除仍存活的子进程而遗留孤儿;已退出实例直接清理。
97
- * 终态自清理(selfRelease)与 dispose 可能并发到达,二者均按实例身份判断,
98
- * 后到方对已删除条目为无害空操作。
110
+ * 对仍在运行或进程树存活或输出未闭合的实例先执行 terminate(0) 兜底(SIGTERM 后立即 SIGKILL),
111
+ * 绝不从受管表移除仍存活的子进程而遗留孤儿;已退出且输出闭合实例直接清理。
99
112
  */
100
113
  dispose(handle: ProcessHandle): Promise<void>;
101
114
  /**