@actiondock/core 2.19.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.
- package/dist/client/config.d.ts +17 -14
- package/dist/client/config.js +112 -14
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +25 -0
- package/dist/execution/service.js +44 -10
- package/dist/execution/types.d.ts +3 -3
- package/dist/host/host.d.ts +20 -1
- package/dist/host/host.js +430 -66
- package/dist/host/types.d.ts +13 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +5 -1
- package/dist/input/config-override.d.ts +5 -0
- package/dist/input/config-override.js +23 -0
- package/dist/input/input-resolver.d.ts +24 -7
- package/dist/input/input-resolver.js +73 -5
- package/dist/invocation/bindings.d.ts +53 -0
- package/dist/invocation/bindings.js +248 -0
- package/dist/invocation/policy.d.ts +5 -1
- package/dist/invocation/policy.js +16 -9
- package/dist/invocation/remote.d.ts +40 -0
- package/dist/invocation/remote.js +489 -0
- package/dist/invocation/types.d.ts +33 -0
- package/dist/invocation/types.js +11 -1
- package/dist/package/index.d.ts +2 -2
- package/dist/package/runtime.js +48 -15
- package/dist/platform/node.d.ts +6 -0
- package/dist/platform/node.js +59 -16
- package/dist/platform/types.d.ts +59 -1
- package/dist/process/command-executor.d.ts +35 -0
- package/dist/process/command-executor.js +326 -0
- package/dist/process/index.d.ts +1 -0
- package/dist/process/index.js +1 -0
- package/dist/process/process-driver.d.ts +19 -6
- package/dist/process/process-driver.js +223 -54
- package/dist/process/run-executor.js +101 -47
- package/dist/project/config-declarations.d.ts +61 -0
- package/dist/project/config-declarations.js +297 -0
- package/dist/project/dependencies.d.ts +92 -0
- package/dist/project/dependencies.js +543 -0
- package/dist/project/index.d.ts +8 -2
- package/dist/project/index.js +8 -2
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +19 -30
- package/dist/project/manifest.js +10 -0
- package/dist/project/package-manager.d.ts +40 -0
- package/dist/project/package-manager.js +224 -0
- package/dist/project/preparation.d.ts +8 -0
- package/dist/project/preparation.js +130 -0
- package/dist/project/transactions.d.ts +5 -1
- package/dist/project/transactions.js +35 -75
- package/dist/project/types.d.ts +30 -0
- package/dist/runtime/context.d.ts +27 -5
- package/dist/runtime/context.js +98 -26
- package/dist/runtime/env.js +15 -1
- package/dist/runtime/run-persistence.d.ts +1 -1
- package/dist/runtime/run-persistence.js +4 -3
- package/dist/runtime/standalone-commands.d.ts +2 -2
- package/dist/runtime/standalone-commands.js +113 -47
- package/dist/runtime/standalone-help.d.ts +1 -0
- package/dist/runtime/standalone-help.js +15 -9
- package/dist/runtime/standalone.js +2 -2
- package/dist/security/config-cipher.d.ts +54 -0
- package/dist/security/config-cipher.js +190 -0
- package/dist/security/config-key.d.ts +35 -0
- package/dist/security/config-key.js +469 -0
- package/dist/server/policy.d.ts +24 -107
- package/dist/server/policy.js +134 -291
- package/dist/server/routes/actions.js +10 -8
- package/dist/server/routes/common.d.ts +5 -20
- package/dist/server/routes/common.js +22 -58
- package/dist/server/routes/config.js +84 -7
- package/dist/server/routes/doctor.d.ts +1 -0
- package/dist/server/routes/doctor.js +4 -3
- package/dist/server/routes/info.js +100 -13
- package/dist/server/routes/playbooks.js +1 -3
- package/dist/server/routes/runs.js +96 -18
- package/dist/server/server.js +25 -53
- package/dist/server/types.d.ts +6 -6
- package/dist/service/factory.js +4 -0
- package/dist/service/remote.js +14 -29
- package/dist/service/types.d.ts +28 -5
- package/dist/service/types.js +8 -0
- package/dist/storage/config-value.d.ts +33 -0
- package/dist/storage/config-value.js +102 -0
- package/dist/storage/file-lock.js +35 -21
- package/dist/storage/index.d.ts +24 -2
- package/dist/storage/index.js +65 -2
- package/dist/storage/mask.d.ts +33 -6
- package/dist/storage/mask.js +298 -16
- package/dist/storage/migrations/index.d.ts +40 -0
- package/dist/storage/migrations/index.js +96 -0
- package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
- package/dist/storage/migrations/v2-to-v3.js +116 -0
- package/dist/storage/sqlite-config.d.ts +62 -3
- package/dist/storage/sqlite-config.js +163 -9
- package/dist/storage/sqlite-driver.d.ts +5 -2
- package/dist/storage/sqlite-driver.js +24 -3
- package/dist/storage/sqlite-schema.d.ts +20 -2
- package/dist/storage/sqlite-schema.js +129 -132
- package/dist/storage/sqlite.d.ts +7 -2
- package/dist/storage/sqlite.js +64 -13
- package/dist/storage/types.d.ts +72 -4
- package/dist/storage/types.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
3
|
import { join } from "node:path";
|
|
@@ -7,6 +6,7 @@ import { isProcessAlive } from "../utils/index.js";
|
|
|
7
6
|
import { ActionDockError, PROJECT_BUSY, PROJECT_RECOVERY_REQUIRED } from "../errors.js";
|
|
8
7
|
import { LOCKFILE_NAME } from "./lockfile.js";
|
|
9
8
|
import { MANIFEST_FILE_NAME } from "./manifest.js";
|
|
9
|
+
import { runPackageManager } from "./package-manager.js";
|
|
10
10
|
export const SNAPSHOT_TRACKED_FILES = [
|
|
11
11
|
"package.json",
|
|
12
12
|
MANIFEST_FILE_NAME,
|
|
@@ -76,58 +76,12 @@ export function acquireProjectLock(projectRoot, options = {}) {
|
|
|
76
76
|
handle.release();
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
80
|
-
* 异步执行子进程并收集退出状态与输出流。
|
|
81
|
-
*
|
|
82
|
-
* 异步化取舍说明:
|
|
83
|
-
* - 避免在事务回滚与恢复链路中执行同步阻塞调用导致事件循环停顿;
|
|
84
|
-
* - Windows 兼容性保留:npm 在 Windows 上是 .cmd 批处理脚本,启用 shell 以防直接调用抛错;
|
|
85
|
-
* - 命令与参数由系统内部锁文件逻辑决定,不接受外部未受控入参。
|
|
86
|
-
*/
|
|
87
|
-
function execSubprocessAsync(command, args, options) {
|
|
88
|
-
return new Promise((resolve, reject) => {
|
|
89
|
-
try {
|
|
90
|
-
const proc = spawn(command, args, {
|
|
91
|
-
cwd: options?.cwd,
|
|
92
|
-
stdio: "pipe",
|
|
93
|
-
shell: process.platform === "win32",
|
|
94
|
-
env: options?.env,
|
|
95
|
-
});
|
|
96
|
-
let stdout = "";
|
|
97
|
-
let stderr = "";
|
|
98
|
-
if (proc.stdout) {
|
|
99
|
-
proc.stdout.on("data", (chunk) => {
|
|
100
|
-
stdout += chunk.toString();
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
if (proc.stderr) {
|
|
104
|
-
proc.stderr.on("data", (chunk) => {
|
|
105
|
-
stderr += chunk.toString();
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
let settled = false;
|
|
109
|
-
proc.on("error", (err) => {
|
|
110
|
-
if (!settled) {
|
|
111
|
-
settled = true;
|
|
112
|
-
reject(err);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
proc.on("close", (status) => {
|
|
116
|
-
if (!settled) {
|
|
117
|
-
settled = true;
|
|
118
|
-
resolve({ status, stdout, stderr });
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
reject(err);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
79
|
/**
|
|
128
80
|
* 依据恢复后的锁文件异步执行禁用安装脚本的冻结安装,使 node_modules 与声明重新一致。
|
|
129
81
|
*/
|
|
130
|
-
export async function runFrozenInstall(projectRoot) {
|
|
82
|
+
export async function runFrozenInstall(projectRoot, options) {
|
|
83
|
+
const signal = options instanceof AbortSignal ? options : options?.signal;
|
|
84
|
+
signal?.throwIfAborted();
|
|
131
85
|
if (process.env.ACTIONDOCK_AUTO_INSTALL === "false") {
|
|
132
86
|
return;
|
|
133
87
|
}
|
|
@@ -137,45 +91,41 @@ export async function runFrozenInstall(projectRoot) {
|
|
|
137
91
|
}
|
|
138
92
|
const hasBunLock = existsSync(join(projectRoot, "bun.lock")) || existsSync(join(projectRoot, "bun.lockb"));
|
|
139
93
|
const hasNpmLock = existsSync(join(projectRoot, "package-lock.json")) || existsSync(join(projectRoot, "npm-shrinkwrap.json"));
|
|
140
|
-
let
|
|
94
|
+
let pm = "npm";
|
|
141
95
|
let args = ["install", "--ignore-scripts"];
|
|
142
96
|
if (hasBunLock) {
|
|
143
|
-
|
|
97
|
+
pm = "bun";
|
|
144
98
|
args = ["install", "--frozen-lockfile", "--ignore-scripts"];
|
|
145
99
|
}
|
|
146
100
|
else if (hasNpmLock) {
|
|
147
|
-
|
|
101
|
+
pm = "npm";
|
|
148
102
|
args = ["ci", "--ignore-scripts"];
|
|
149
103
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
cmd = "npm";
|
|
159
|
-
args = ["install", "--ignore-scripts"];
|
|
160
|
-
}
|
|
104
|
+
const env = {
|
|
105
|
+
...process.env,
|
|
106
|
+
npm_config_allow_scripts: "",
|
|
107
|
+
NPM_CONFIG_ALLOW_SCRIPTS: "",
|
|
108
|
+
npm_config_ignore_scripts: "true",
|
|
109
|
+
NPM_CONFIG_IGNORE_SCRIPTS: "true",
|
|
110
|
+
};
|
|
161
111
|
let proc;
|
|
162
112
|
try {
|
|
163
|
-
proc = await
|
|
113
|
+
proc = await runPackageManager(pm, args, {
|
|
164
114
|
cwd: projectRoot,
|
|
165
|
-
env
|
|
166
|
-
|
|
167
|
-
npm_config_allow_scripts: "",
|
|
168
|
-
NPM_CONFIG_ALLOW_SCRIPTS: "",
|
|
169
|
-
},
|
|
115
|
+
env,
|
|
116
|
+
signal,
|
|
170
117
|
});
|
|
171
118
|
}
|
|
172
119
|
catch (err) {
|
|
120
|
+
if (signal?.aborted || err?.name === "AbortError") {
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
173
123
|
const errorMsg = err?.message || "Unknown error";
|
|
174
|
-
throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Frozen install failed during recovery in ${projectRoot}: ${errorMsg}
|
|
124
|
+
throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Frozen install failed during recovery in ${projectRoot}: ${errorMsg}`, { stage: "recovery-install", projectRoot, manager: pm, cause: err });
|
|
175
125
|
}
|
|
176
126
|
if (proc.status !== 0) {
|
|
177
|
-
const errorMsg = proc.stderr || proc.stdout || "Unknown error";
|
|
178
|
-
throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Frozen install failed during recovery in ${projectRoot}: ${errorMsg}
|
|
127
|
+
const errorMsg = (proc.stderr || proc.stdout || "Unknown error").trim();
|
|
128
|
+
throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Frozen install failed during recovery in ${projectRoot}: ${errorMsg}`, { stage: "recovery-install", projectRoot, manager: pm, status: proc.status, stderr: proc.stderr, stdout: proc.stdout });
|
|
179
129
|
}
|
|
180
130
|
}
|
|
181
131
|
/**
|
|
@@ -214,6 +164,9 @@ export function hasPendingTransactions(projectRoot) {
|
|
|
214
164
|
export async function beginTransaction(projectRoot, description) {
|
|
215
165
|
const releaseLock = acquireProjectLock(projectRoot);
|
|
216
166
|
try {
|
|
167
|
+
if (hasPendingTransactions(projectRoot)) {
|
|
168
|
+
throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Cannot begin transaction in '${projectRoot}': pending transaction exists. Recovery is required.`);
|
|
169
|
+
}
|
|
217
170
|
const txId = `${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
218
171
|
const txDir = join(projectRoot, ".actiondock", "transactions", txId);
|
|
219
172
|
const snapshotDir = join(txDir, "snapshot");
|
|
@@ -300,6 +253,7 @@ export async function beginTransaction(projectRoot, description) {
|
|
|
300
253
|
* 依据事务日志恢复所有待恢复的悬空事务快照,并在成功后执行冻结安装。
|
|
301
254
|
*/
|
|
302
255
|
export async function recoverPendingTransactions(projectRoot, options) {
|
|
256
|
+
options?.signal?.throwIfAborted();
|
|
303
257
|
const txBaseDir = join(projectRoot, ".actiondock", "transactions");
|
|
304
258
|
if (!existsSync(txBaseDir)) {
|
|
305
259
|
return [];
|
|
@@ -315,6 +269,7 @@ export async function recoverPendingTransactions(projectRoot, options) {
|
|
|
315
269
|
throw err;
|
|
316
270
|
}
|
|
317
271
|
try {
|
|
272
|
+
options?.signal?.throwIfAborted();
|
|
318
273
|
const recoveredIds = [];
|
|
319
274
|
const entries = readdirSync(txBaseDir);
|
|
320
275
|
for (const entry of entries) {
|
|
@@ -330,6 +285,11 @@ export async function recoverPendingTransactions(projectRoot, options) {
|
|
|
330
285
|
continue;
|
|
331
286
|
}
|
|
332
287
|
if (meta.status === "pending") {
|
|
288
|
+
options?.signal?.throwIfAborted();
|
|
289
|
+
if (options?.frozenInstall !== false &&
|
|
290
|
+
(options?.allowInstall === false || process.env.ACTIONDOCK_AUTO_INSTALL === "false")) {
|
|
291
|
+
throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Project at '${projectRoot}' has pending transactions requiring recovery install, but installation is disabled (--no-install or ACTIONDOCK_AUTO_INSTALL=false).`);
|
|
292
|
+
}
|
|
333
293
|
const snapshotDir = join(txDir, "snapshot");
|
|
334
294
|
for (const rec of meta.files) {
|
|
335
295
|
const targetPath = join(projectRoot, rec.name);
|
|
@@ -346,7 +306,7 @@ export async function recoverPendingTransactions(projectRoot, options) {
|
|
|
346
306
|
}
|
|
347
307
|
}
|
|
348
308
|
if (options?.frozenInstall !== false) {
|
|
349
|
-
await runFrozenInstall(projectRoot);
|
|
309
|
+
await runFrozenInstall(projectRoot, { signal: options?.signal });
|
|
350
310
|
}
|
|
351
311
|
meta.status = "rolled_back";
|
|
352
312
|
try {
|
|
@@ -362,6 +322,6 @@ export async function recoverPendingTransactions(projectRoot, options) {
|
|
|
362
322
|
return recoveredIds;
|
|
363
323
|
}
|
|
364
324
|
finally {
|
|
365
|
-
releaseLock();
|
|
325
|
+
releaseLock?.();
|
|
366
326
|
}
|
|
367
327
|
}
|
package/dist/project/types.d.ts
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
* 配置项的值类型枚举。用于自动类型推断与环境变量自动转换。
|
|
3
3
|
*/
|
|
4
4
|
export type ConfigValueType = "string" | "number" | "boolean" | "object" | "array";
|
|
5
|
+
/**
|
|
6
|
+
* 配置值受限模式定义契约。
|
|
7
|
+
* 仅允许受支持的纯受限关键字集合,禁止动态计算或高级条件指令。
|
|
8
|
+
*/
|
|
9
|
+
export interface ConfigValueSchema {
|
|
10
|
+
/** 节点数据类型 */
|
|
11
|
+
type?: "object" | "array" | "string" | "integer" | "number" | "boolean";
|
|
12
|
+
/** 对象属性字段子模式映射 */
|
|
13
|
+
properties?: Record<string, ConfigValueSchema>;
|
|
14
|
+
/** 必填属性字段列表 */
|
|
15
|
+
required?: string[];
|
|
16
|
+
/** 额外属性放行策略(布尔开关或子模式) */
|
|
17
|
+
additionalProperties?: boolean | ConfigValueSchema;
|
|
18
|
+
/** 数组元素子模式 */
|
|
19
|
+
items?: ConfigValueSchema;
|
|
20
|
+
/** 枚举有效值列表 */
|
|
21
|
+
enum?: unknown[];
|
|
22
|
+
/** 字符串最小字符长度 */
|
|
23
|
+
minLength?: number;
|
|
24
|
+
/** 字符串最大字符长度 */
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
/** 数值最小值约束 */
|
|
27
|
+
minimum?: number;
|
|
28
|
+
/** 数值最大值约束 */
|
|
29
|
+
maximum?: number;
|
|
30
|
+
/** 是否为敏感字段(仅可在模式节点中扩展指定) */
|
|
31
|
+
secret?: boolean;
|
|
32
|
+
}
|
|
5
33
|
/**
|
|
6
34
|
* 在 actiondock.json 中声明的单项配置定义。
|
|
7
35
|
*/
|
|
@@ -20,6 +48,8 @@ export interface ConfigItemDefinition {
|
|
|
20
48
|
required?: boolean;
|
|
21
49
|
/** 是否允许单次调用覆盖 */
|
|
22
50
|
allowInvocationOverride?: boolean;
|
|
51
|
+
/** 配置值验证与字段级脱敏所绑定的受限模式定义 */
|
|
52
|
+
schema?: ConfigValueSchema;
|
|
23
53
|
}
|
|
24
54
|
/**
|
|
25
55
|
* 单个 Action 在 actiondock.json 清单中的声明项。
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { ActionContext, ActionRef, Config, Logger, ProcessAPI, ProgressReporter, StateStore } from "@actiondock/sdk";
|
|
1
|
+
import type { ActionContext, ActionInvokeOptions, ActionRef, Config, Logger, ProcessAPI, ProgressReporter, StateStore } from "@actiondock/sdk";
|
|
2
2
|
import type { ProjectConfig } from "../project/types.js";
|
|
3
|
+
import type { ConfigItemDefinition } from "../project/types.js";
|
|
3
4
|
import type { RuntimeStorage } from "../storage/types.js";
|
|
4
5
|
import { ProcessManager, type ProcessOwner } from "../process/index.js";
|
|
6
|
+
import type { ConfigSource } from "../service/types.js";
|
|
5
7
|
/**
|
|
6
8
|
* 生产级配置解析器实现。
|
|
7
9
|
* 严格践行 5 层配置解析优先级链:
|
|
@@ -21,17 +23,37 @@ export declare class RuntimeConfig implements Config {
|
|
|
21
23
|
get<T = unknown>(key: string): T | undefined;
|
|
22
24
|
get<T = unknown>(key: string, defaultValue: T): T;
|
|
23
25
|
has(key: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* 含历史保护声明的配置解析结果视图。
|
|
28
|
+
*/
|
|
29
|
+
describeWithProtection<T = unknown>(key: string): ConfigResolutionWithProtection<T>;
|
|
24
30
|
/**
|
|
25
31
|
* 结构化解析指定配置项的来源与取值视图。
|
|
26
32
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
33
|
+
* 委托 describeWithProtection 五层优先级链单一事实源,剥出既有公开契约视图,
|
|
34
|
+
* 避免双实现漂移;历史保护声明仅由管理视图消费。
|
|
35
|
+
* source 仅表达实际生效值来源;undefined 统一视为未命中并继续向低优先级解析,
|
|
36
|
+
* null/false/0/空串均为真实值;整链未命中时 source 为 null。
|
|
29
37
|
*/
|
|
30
38
|
describe<T = unknown>(key: string): {
|
|
31
|
-
source:
|
|
39
|
+
source: ConfigSource | null;
|
|
32
40
|
value: T | undefined;
|
|
33
41
|
};
|
|
34
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* 含历史保护声明的配置解析结果视图。
|
|
45
|
+
*/
|
|
46
|
+
export interface ConfigResolutionWithProtection<T = unknown> {
|
|
47
|
+
/** 实际生效值来源;整链未命中为 null */
|
|
48
|
+
source: ConfigSource | null;
|
|
49
|
+
/** 实际生效值 */
|
|
50
|
+
value: T | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* 值来源存储持久化的历史保护声明(仅 package/global 来源可能存在)。
|
|
53
|
+
* 与值取自同一次原子读取,避免并发删除造成值与规则错配。
|
|
54
|
+
*/
|
|
55
|
+
protection?: readonly ConfigItemDefinition[];
|
|
56
|
+
}
|
|
35
57
|
/**
|
|
36
58
|
* 生产级状态存储实现,将状态持久化到 SQLite 中,完整支持命名空间分段隔离与 TTL。
|
|
37
59
|
*
|
|
@@ -81,7 +103,7 @@ export interface ContextOptions {
|
|
|
81
103
|
owner?: ProcessOwner;
|
|
82
104
|
progress?: ProgressReporter;
|
|
83
105
|
logger?: Logger;
|
|
84
|
-
onActionInvoke?: (action: ActionRef | string, input: unknown, parentRunId?: string) => Promise<unknown>;
|
|
106
|
+
onActionInvoke?: (action: ActionRef | string, input: unknown, parentRunId?: string, options?: ActionInvokeOptions) => Promise<unknown>;
|
|
85
107
|
}
|
|
86
108
|
/**
|
|
87
109
|
* 构建并装配传递给 Action 的完整 ActionContext 运行时上下文。
|
package/dist/runtime/context.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { assertConcreteConfigKey, assertValidConfigDeclarations, resolveConfigDeclaration, validateConfigValue } from "../project/config-declarations.js";
|
|
2
|
+
import { readConfigWithProtection } from "../storage/index.js";
|
|
1
3
|
import { MemoryProcessDriver, ProcessManager } from "../process/index.js";
|
|
2
4
|
import { createDefaultProcessOwner } from "../invocation/types.js";
|
|
3
5
|
import { resolveEnvValue } from "./env.js";
|
|
4
|
-
import { ActionDockError, INVALID_ACTION_REF, INVALID_STATE } from "../errors.js";
|
|
6
|
+
import { ActionDockError, isConfigEncryptionError, INVALID_ACTION_REF, INVALID_ARGUMENT, INVALID_STATE, } from "../errors.js";
|
|
5
7
|
/**
|
|
6
8
|
* 生产级配置解析器实现。
|
|
7
9
|
* 严格践行 5 层配置解析优先级链:
|
|
@@ -20,58 +22,95 @@ export class RuntimeConfig {
|
|
|
20
22
|
constructor(storage, overrides = {}, projectConfig, globalStorage) {
|
|
21
23
|
this.storage = storage;
|
|
22
24
|
this.overrides = new Map(Object.entries(overrides));
|
|
25
|
+
assertValidConfigDeclarations(projectConfig?.config);
|
|
23
26
|
this.projectConfig = projectConfig;
|
|
24
27
|
this.globalStorage = globalStorage;
|
|
25
28
|
}
|
|
26
29
|
get(key, defaultValue) {
|
|
27
|
-
// 委托 describe 五层解析链唯一事实源:整链未命中(source 为
|
|
30
|
+
// 委托 describe 五层解析链唯一事实源:整链未命中(source 为 null 且无值)时回退调用方默认值
|
|
28
31
|
const resolved = this.describe(key);
|
|
29
|
-
if (resolved.source ===
|
|
32
|
+
if (resolved.source === null && resolved.value === undefined) {
|
|
30
33
|
return defaultValue;
|
|
31
34
|
}
|
|
35
|
+
const resolvedDecl = resolveConfigDeclaration(this.projectConfig?.config, key);
|
|
36
|
+
validateConfigValue(key, resolved.value, resolvedDecl?.item);
|
|
32
37
|
return resolved.value;
|
|
33
38
|
}
|
|
34
39
|
has(key) {
|
|
35
|
-
// 委托 describe
|
|
40
|
+
// 委托 describe 五层解析链唯一事实源:来源 null 且无值即视为未配置
|
|
36
41
|
const resolved = this.describe(key);
|
|
37
|
-
return !(resolved.source ===
|
|
42
|
+
return !(resolved.source === null && resolved.value === undefined);
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* 作为五层优先级链的唯一事实源读取视图,供 app 层 getConfig 安全视图直接委托,
|
|
43
|
-
* 避免重复实现解析链导致行为漂移。
|
|
45
|
+
* 含历史保护声明的配置解析结果视图。
|
|
44
46
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
describeWithProtection(key) {
|
|
48
|
+
assertConcreteConfigKey(key);
|
|
49
|
+
// 1. 运行时固定覆盖与单次调用覆盖(值为 undefined 视为未命中,继续解析链)
|
|
47
50
|
if (this.overrides.has(key)) {
|
|
48
|
-
|
|
51
|
+
const overrideValue = this.overrides.get(key);
|
|
52
|
+
if (overrideValue !== undefined) {
|
|
53
|
+
return { source: "override", value: overrideValue };
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
|
-
// 2.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
// 2. 包级持久化存储:原子快照配对读取值与历史保护声明;
|
|
57
|
+
// 加密错误直接透传,绝不降级继续解析链(旧版库已在打开时自动迁移)
|
|
58
|
+
const resolvedDecl = resolveConfigDeclaration(this.projectConfig?.config, key);
|
|
59
|
+
const packageSnapshot = readConfigWithProtection(this.storage, key);
|
|
60
|
+
if (packageSnapshot !== undefined) {
|
|
61
|
+
return {
|
|
62
|
+
source: "package",
|
|
63
|
+
value: packageSnapshot.value,
|
|
64
|
+
...(packageSnapshot.protection !== undefined
|
|
65
|
+
? { protection: packageSnapshot.protection }
|
|
66
|
+
: {}),
|
|
67
|
+
};
|
|
54
68
|
}
|
|
55
|
-
// 3.
|
|
69
|
+
// 3. 全局持久化存储(一般读取异常降级继续解析链并告警;
|
|
70
|
+
// 加密错误严禁回退,必须原样透传)
|
|
56
71
|
if (this.globalStorage) {
|
|
57
72
|
try {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
return {
|
|
73
|
+
const globalSnapshot = readConfigWithProtection(this.globalStorage, key);
|
|
74
|
+
if (globalSnapshot !== undefined) {
|
|
75
|
+
return {
|
|
76
|
+
source: "global",
|
|
77
|
+
value: globalSnapshot.value,
|
|
78
|
+
...(globalSnapshot.protection !== undefined
|
|
79
|
+
? { protection: globalSnapshot.protection }
|
|
80
|
+
: {}),
|
|
81
|
+
};
|
|
61
82
|
}
|
|
62
83
|
}
|
|
63
84
|
catch (err) {
|
|
85
|
+
if (isConfigEncryptionError(err)) {
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
64
88
|
console.warn(`[actiondock] global config read failed for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
65
89
|
}
|
|
66
90
|
}
|
|
67
91
|
// 4. 环境变量
|
|
68
|
-
const itemDef =
|
|
92
|
+
const itemDef = resolvedDecl?.item;
|
|
69
93
|
const envResolved = resolveEnvValue(key, itemDef, this.projectConfig?.id);
|
|
70
94
|
if (envResolved !== undefined) {
|
|
71
95
|
return { source: "env", value: envResolved.value };
|
|
72
96
|
}
|
|
73
|
-
// 5.
|
|
74
|
-
|
|
97
|
+
// 5. 项目声明的默认值;仅存在真实生效的 default 值才报 default,整链未命中为 null
|
|
98
|
+
if (itemDef?.default !== undefined) {
|
|
99
|
+
return { source: "default", value: itemDef.default };
|
|
100
|
+
}
|
|
101
|
+
return { source: null, value: undefined };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 结构化解析指定配置项的来源与取值视图。
|
|
105
|
+
*
|
|
106
|
+
* 委托 describeWithProtection 五层优先级链单一事实源,剥出既有公开契约视图,
|
|
107
|
+
* 避免双实现漂移;历史保护声明仅由管理视图消费。
|
|
108
|
+
* source 仅表达实际生效值来源;undefined 统一视为未命中并继续向低优先级解析,
|
|
109
|
+
* null/false/0/空串均为真实值;整链未命中时 source 为 null。
|
|
110
|
+
*/
|
|
111
|
+
describe(key) {
|
|
112
|
+
const resolved = this.describeWithProtection(key);
|
|
113
|
+
return { source: resolved.source, value: resolved.value };
|
|
75
114
|
}
|
|
76
115
|
}
|
|
77
116
|
/**
|
|
@@ -156,6 +195,38 @@ export class StderrLogger {
|
|
|
156
195
|
process.stderr.write(this.format("error", message, data) + "\n");
|
|
157
196
|
}
|
|
158
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* 校验并规范化 ActionInvokeOptions 调用选项。
|
|
200
|
+
*
|
|
201
|
+
* 针对第三参数运行时检查:
|
|
202
|
+
* - 必须为普通对象或 undefined(2 参数兼容)
|
|
203
|
+
* - 严禁未知字段(如 profile、server、token 等)
|
|
204
|
+
* - binding 若存在必须为非空且非全空白字符串
|
|
205
|
+
*/
|
|
206
|
+
function validateActionInvokeOptions(options) {
|
|
207
|
+
if (options === undefined) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
if (typeof options !== "object" ||
|
|
211
|
+
options === null ||
|
|
212
|
+
Array.isArray(options) ||
|
|
213
|
+
(Object.getPrototypeOf(options) !== Object.prototype && Object.getPrototypeOf(options) !== null)) {
|
|
214
|
+
throw new ActionDockError(INVALID_ARGUMENT, "INVALID_ARGUMENT: ActionInvokeOptions must be a plain object");
|
|
215
|
+
}
|
|
216
|
+
const allowedKeys = new Set(["binding"]);
|
|
217
|
+
for (const key of Reflect.ownKeys(options)) {
|
|
218
|
+
if (typeof key !== "string" || !allowedKeys.has(key)) {
|
|
219
|
+
throw new ActionDockError(INVALID_ARGUMENT, `INVALID_ARGUMENT: Unknown option '${String(key)}' in ActionInvokeOptions. Only 'binding' is allowed.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const rawBinding = options.binding;
|
|
223
|
+
if (rawBinding !== undefined) {
|
|
224
|
+
if (typeof rawBinding !== "string" || rawBinding.trim() === "") {
|
|
225
|
+
throw new ActionDockError(INVALID_ARGUMENT, "INVALID_ARGUMENT: 'binding' option must be a non-empty string");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return options;
|
|
229
|
+
}
|
|
159
230
|
/**
|
|
160
231
|
* 构建并装配传递给 Action 的完整 ActionContext 运行时上下文。
|
|
161
232
|
*
|
|
@@ -169,17 +240,18 @@ export function createActionContext(options) {
|
|
|
169
240
|
const signal = options.signal ?? new AbortController().signal;
|
|
170
241
|
const currentRunId = options.runId || crypto.randomUUID();
|
|
171
242
|
const currentRootRunId = options.rootRunId || options.parentRunId || currentRunId;
|
|
172
|
-
const invoke = async (action, input) => {
|
|
243
|
+
const invoke = async (action, input, rawOptions) => {
|
|
173
244
|
if (typeof action !== "string" &&
|
|
174
245
|
(!action || typeof action !== "object" || typeof action.run === "function" || !("actionId" in action))) {
|
|
175
246
|
throw new ActionDockError(INVALID_ACTION_REF, "INVALID_ACTION_REF: ctx.actions.invoke strictly accepts only ActionRef or string, passing ActionDefinition or function is prohibited");
|
|
176
247
|
}
|
|
248
|
+
const validatedOptions = validateActionInvokeOptions(rawOptions);
|
|
177
249
|
if (options.onActionInvoke) {
|
|
178
|
-
return (await options.onActionInvoke(action, input, currentRunId));
|
|
250
|
+
return (await options.onActionInvoke(action, input, currentRunId, validatedOptions));
|
|
179
251
|
}
|
|
180
252
|
throw new ActionDockError(INVALID_STATE, "ActionInvoker not configured with an invocation delegate");
|
|
181
253
|
};
|
|
182
|
-
const invokerFn = (ref, input) => invoke(ref, input);
|
|
254
|
+
const invokerFn = (ref, input, invokeOpts) => invoke(ref, input, invokeOpts);
|
|
183
255
|
const invoker = Object.assign(invokerFn, { invoke });
|
|
184
256
|
const defaultOwner = createDefaultProcessOwner();
|
|
185
257
|
const effectiveOwner = options.owner || defaultOwner;
|
package/dist/runtime/env.js
CHANGED
|
@@ -142,7 +142,21 @@ export function resolveEnvValue(key, itemDef, packageId) {
|
|
|
142
142
|
// 4. 原始键名 (例如 apiKey)
|
|
143
143
|
candidateKeys.add(key);
|
|
144
144
|
// 按优先级顺序依次检索环境变量
|
|
145
|
-
|
|
145
|
+
let effectiveType = itemDef?.type;
|
|
146
|
+
if (!effectiveType && itemDef?.schema?.type) {
|
|
147
|
+
if (itemDef.schema.type === "integer" || itemDef.schema.type === "number") {
|
|
148
|
+
effectiveType = "number";
|
|
149
|
+
}
|
|
150
|
+
else if (itemDef.schema.type === "string" ||
|
|
151
|
+
itemDef.schema.type === "boolean" ||
|
|
152
|
+
itemDef.schema.type === "object" ||
|
|
153
|
+
itemDef.schema.type === "array") {
|
|
154
|
+
effectiveType = itemDef.schema.type;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!effectiveType) {
|
|
158
|
+
effectiveType = inferTypeFromDefault(itemDef?.default);
|
|
159
|
+
}
|
|
146
160
|
for (const envKey of candidateKeys) {
|
|
147
161
|
const rawVal = process.env[envKey];
|
|
148
162
|
if (rawVal !== undefined) {
|
|
@@ -63,7 +63,7 @@ export interface RunFinalizer {
|
|
|
63
63
|
/** 竞态监听句柄注销函数列表(finalize 时统一清理,覆盖成功路径) */
|
|
64
64
|
raceListenerRemovers: Array<() => void>;
|
|
65
65
|
/** 绑定取消控制器并启动超时定时器 */
|
|
66
|
-
startTimeout(controller: AbortController, timeoutMs: number): void;
|
|
66
|
+
startTimeout(controller: AbortController, timeoutMs: number, displayTimeoutMs?: number): void;
|
|
67
67
|
/** 写入终态(幂等,自动清理超时定时器与全部监听句柄) */
|
|
68
68
|
finalize(status: TerminalRunStatus, output?: unknown, error?: RuntimeError): void;
|
|
69
69
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionDockError, RUN_PERSISTENCE_FAILED, RUN_REPOSITORY_UNAVAILABLE } from "../errors.js";
|
|
1
|
+
import { ActionDockError, ACTION_TIMEOUT, RUN_PERSISTENCE_FAILED, RUN_REPOSITORY_UNAVAILABLE } from "../errors.js";
|
|
2
2
|
import { snapshotRunOrigin } from "./run-origin.js";
|
|
3
3
|
/**
|
|
4
4
|
* 构建运行初始记录(RunRecord 组装单一事实源)。
|
|
@@ -79,11 +79,12 @@ export function createRunFinalizer(storage, runId) {
|
|
|
79
79
|
signal: undefined,
|
|
80
80
|
onAbort: undefined,
|
|
81
81
|
raceListenerRemovers: [],
|
|
82
|
-
startTimeout: (controller, timeoutMs) => {
|
|
82
|
+
startTimeout: (controller, timeoutMs, displayTimeoutMs) => {
|
|
83
83
|
finalizer.controller = controller;
|
|
84
|
+
const display = displayTimeoutMs ?? timeoutMs;
|
|
84
85
|
finalizer.timeoutTimer = setTimeout(() => {
|
|
85
86
|
finalizer.isTimeout = true;
|
|
86
|
-
finalizer.controller?.abort(new
|
|
87
|
+
finalizer.controller?.abort(new ActionDockError(ACTION_TIMEOUT, `Action exceeded timeout of ${display}ms`, { timeoutMs: display }));
|
|
87
88
|
}, timeoutMs);
|
|
88
89
|
},
|
|
89
90
|
finalize: (status, output, error) => {
|
|
@@ -14,5 +14,5 @@ export interface CommandContext {
|
|
|
14
14
|
export declare function handleList(ctx: CommandContext, subArgs: string[]): Promise<number>;
|
|
15
15
|
export declare function handleDescribe(ctx: CommandContext, subArgs: string[]): Promise<number>;
|
|
16
16
|
export declare function handleRun(ctx: CommandContext, subArgs: string[], actionArgs: string[], signal?: AbortSignal): Promise<number>;
|
|
17
|
-
export declare function handleConfig(ctx: CommandContext, subArgs: string[]): Promise<number>;
|
|
18
|
-
export declare function handleState(ctx: CommandContext, subArgs: string[]): Promise<number>;
|
|
17
|
+
export declare function handleConfig(ctx: CommandContext, subArgs: string[], flatArgs?: string[], signal?: AbortSignal, stdin?: NodeJS.ReadableStream): Promise<number>;
|
|
18
|
+
export declare function handleState(ctx: CommandContext, subArgs: string[], flatArgs?: string[], signal?: AbortSignal, stdin?: NodeJS.ReadableStream): Promise<number>;
|