@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.
Files changed (108) hide show
  1. package/dist/client/config.d.ts +17 -14
  2. package/dist/client/config.js +112 -14
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/client/index.js +1 -1
  5. package/dist/errors.d.ts +13 -1
  6. package/dist/errors.js +25 -0
  7. package/dist/execution/service.js +44 -10
  8. package/dist/execution/types.d.ts +3 -3
  9. package/dist/host/host.d.ts +20 -1
  10. package/dist/host/host.js +430 -66
  11. package/dist/host/types.d.ts +13 -1
  12. package/dist/index.d.ts +11 -3
  13. package/dist/index.js +5 -1
  14. package/dist/input/config-override.d.ts +5 -0
  15. package/dist/input/config-override.js +23 -0
  16. package/dist/input/input-resolver.d.ts +24 -7
  17. package/dist/input/input-resolver.js +73 -5
  18. package/dist/invocation/bindings.d.ts +53 -0
  19. package/dist/invocation/bindings.js +248 -0
  20. package/dist/invocation/policy.d.ts +5 -1
  21. package/dist/invocation/policy.js +16 -9
  22. package/dist/invocation/remote.d.ts +40 -0
  23. package/dist/invocation/remote.js +489 -0
  24. package/dist/invocation/types.d.ts +33 -0
  25. package/dist/invocation/types.js +11 -1
  26. package/dist/package/index.d.ts +2 -2
  27. package/dist/package/runtime.js +48 -15
  28. package/dist/platform/node.d.ts +6 -0
  29. package/dist/platform/node.js +59 -16
  30. package/dist/platform/types.d.ts +59 -1
  31. package/dist/process/command-executor.d.ts +35 -0
  32. package/dist/process/command-executor.js +326 -0
  33. package/dist/process/index.d.ts +1 -0
  34. package/dist/process/index.js +1 -0
  35. package/dist/process/process-driver.d.ts +19 -6
  36. package/dist/process/process-driver.js +223 -54
  37. package/dist/process/run-executor.js +101 -47
  38. package/dist/project/config-declarations.d.ts +61 -0
  39. package/dist/project/config-declarations.js +297 -0
  40. package/dist/project/dependencies.d.ts +92 -0
  41. package/dist/project/dependencies.js +543 -0
  42. package/dist/project/index.d.ts +8 -2
  43. package/dist/project/index.js +8 -2
  44. package/dist/project/init.js +2 -2
  45. package/dist/project/loader.js +19 -30
  46. package/dist/project/manifest.js +10 -0
  47. package/dist/project/package-manager.d.ts +40 -0
  48. package/dist/project/package-manager.js +224 -0
  49. package/dist/project/preparation.d.ts +8 -0
  50. package/dist/project/preparation.js +130 -0
  51. package/dist/project/transactions.d.ts +5 -1
  52. package/dist/project/transactions.js +35 -75
  53. package/dist/project/types.d.ts +30 -0
  54. package/dist/runtime/context.d.ts +27 -5
  55. package/dist/runtime/context.js +98 -26
  56. package/dist/runtime/env.js +15 -1
  57. package/dist/runtime/run-persistence.d.ts +1 -1
  58. package/dist/runtime/run-persistence.js +4 -3
  59. package/dist/runtime/standalone-commands.d.ts +2 -2
  60. package/dist/runtime/standalone-commands.js +113 -47
  61. package/dist/runtime/standalone-help.d.ts +1 -0
  62. package/dist/runtime/standalone-help.js +15 -9
  63. package/dist/runtime/standalone.js +2 -2
  64. package/dist/security/config-cipher.d.ts +54 -0
  65. package/dist/security/config-cipher.js +190 -0
  66. package/dist/security/config-key.d.ts +35 -0
  67. package/dist/security/config-key.js +469 -0
  68. package/dist/server/policy.d.ts +24 -107
  69. package/dist/server/policy.js +134 -291
  70. package/dist/server/routes/actions.js +10 -8
  71. package/dist/server/routes/common.d.ts +5 -20
  72. package/dist/server/routes/common.js +22 -58
  73. package/dist/server/routes/config.js +84 -7
  74. package/dist/server/routes/doctor.d.ts +1 -0
  75. package/dist/server/routes/doctor.js +4 -3
  76. package/dist/server/routes/info.js +100 -13
  77. package/dist/server/routes/playbooks.js +1 -3
  78. package/dist/server/routes/runs.js +96 -18
  79. package/dist/server/server.js +25 -53
  80. package/dist/server/types.d.ts +6 -6
  81. package/dist/service/factory.js +4 -0
  82. package/dist/service/remote.js +14 -29
  83. package/dist/service/types.d.ts +28 -5
  84. package/dist/service/types.js +8 -0
  85. package/dist/storage/config-value.d.ts +33 -0
  86. package/dist/storage/config-value.js +102 -0
  87. package/dist/storage/file-lock.js +35 -21
  88. package/dist/storage/index.d.ts +24 -2
  89. package/dist/storage/index.js +65 -2
  90. package/dist/storage/mask.d.ts +33 -6
  91. package/dist/storage/mask.js +298 -16
  92. package/dist/storage/migrations/index.d.ts +40 -0
  93. package/dist/storage/migrations/index.js +96 -0
  94. package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
  95. package/dist/storage/migrations/v2-to-v3.js +116 -0
  96. package/dist/storage/sqlite-config.d.ts +62 -3
  97. package/dist/storage/sqlite-config.js +163 -9
  98. package/dist/storage/sqlite-driver.d.ts +5 -2
  99. package/dist/storage/sqlite-driver.js +24 -3
  100. package/dist/storage/sqlite-schema.d.ts +20 -2
  101. package/dist/storage/sqlite-schema.js +129 -132
  102. package/dist/storage/sqlite.d.ts +7 -2
  103. package/dist/storage/sqlite.js +64 -13
  104. package/dist/storage/types.d.ts +72 -4
  105. package/dist/storage/types.js +7 -2
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. 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 cmd = "npm";
94
+ let pm = "npm";
141
95
  let args = ["install", "--ignore-scripts"];
142
96
  if (hasBunLock) {
143
- cmd = "bun";
97
+ pm = "bun";
144
98
  args = ["install", "--frozen-lockfile", "--ignore-scripts"];
145
99
  }
146
100
  else if (hasNpmLock) {
147
- cmd = "npm";
101
+ pm = "npm";
148
102
  args = ["ci", "--ignore-scripts"];
149
103
  }
150
- try {
151
- const check = await execSubprocessAsync(cmd, ["--version"]);
152
- if (check.status !== 0) {
153
- cmd = "npm";
154
- args = ["install", "--ignore-scripts"];
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 execSubprocessAsync(cmd, args, {
113
+ proc = await runPackageManager(pm, args, {
164
114
  cwd: projectRoot,
165
- env: {
166
- ...process.env,
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
  }
@@ -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
- * 作为五层优先级链的唯一事实源读取视图,供 app 层 getConfig 安全视图直接委托,
28
- * 避免重复实现解析链导致行为漂移。
33
+ * 委托 describeWithProtection 五层优先级链单一事实源,剥出既有公开契约视图,
34
+ * 避免双实现漂移;历史保护声明仅由管理视图消费。
35
+ * source 仅表达实际生效值来源;undefined 统一视为未命中并继续向低优先级解析,
36
+ * null/false/0/空串均为真实值;整链未命中时 source 为 null。
29
37
  */
30
38
  describe<T = unknown>(key: string): {
31
- source: "overrides" | "package" | "global" | "env" | "default";
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 运行时上下文。
@@ -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 为 default 且无值)时回退调用方默认值
30
+ // 委托 describe 五层解析链唯一事实源:整链未命中(source 为 null 且无值)时回退调用方默认值
28
31
  const resolved = this.describe(key);
29
- if (resolved.source === "default" && resolved.value === undefined) {
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 五层解析链唯一事实源:default 来源且无值即视为未配置
40
+ // 委托 describe 五层解析链唯一事实源:来源 null 且无值即视为未配置
36
41
  const resolved = this.describe(key);
37
- return !(resolved.source === "default" && resolved.value === undefined);
42
+ return !(resolved.source === null && resolved.value === undefined);
38
43
  }
39
44
  /**
40
- * 结构化解析指定配置项的来源与取值视图。
41
- *
42
- * 作为五层优先级链的唯一事实源读取视图,供 app 层 getConfig 安全视图直接委托,
43
- * 避免重复实现解析链导致行为漂移。
45
+ * 含历史保护声明的配置解析结果视图。
44
46
  */
45
- describe(key) {
46
- // 1. CLI 临时参数覆写
47
+ describeWithProtection(key) {
48
+ assertConcreteConfigKey(key);
49
+ // 1. 运行时固定覆盖与单次调用覆盖(值为 undefined 视为未命中,继续解析链)
47
50
  if (this.overrides.has(key)) {
48
- return { source: "overrides", value: this.overrides.get(key) };
51
+ const overrideValue = this.overrides.get(key);
52
+ if (overrideValue !== undefined) {
53
+ return { source: "override", value: overrideValue };
54
+ }
49
55
  }
50
- // 2. 包级持久化存储
51
- const stored = this.storage.getConfig(key);
52
- if (stored !== undefined) {
53
- return { source: "package", value: stored };
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 globalStored = this.globalStorage.getConfig(key);
59
- if (globalStored !== undefined) {
60
- return { source: "global", value: globalStored };
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 = this.projectConfig?.config?.[key];
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
- return { source: "default", value: itemDef?.default };
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;
@@ -142,7 +142,21 @@ export function resolveEnvValue(key, itemDef, packageId) {
142
142
  // 4. 原始键名 (例如 apiKey)
143
143
  candidateKeys.add(key);
144
144
  // 按优先级顺序依次检索环境变量
145
- const effectiveType = itemDef?.type || inferTypeFromDefault(itemDef?.default);
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 Error(`Action exceeded timeout of ${timeoutMs}ms`));
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>;