@actiondock/core 2.18.0 → 2.20.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 (74) 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/client/runs.d.ts +9 -0
  6. package/dist/client/runs.js +7 -0
  7. package/dist/errors.d.ts +3 -1
  8. package/dist/errors.js +2 -0
  9. package/dist/execution/service.js +8 -5
  10. package/dist/execution/types.d.ts +3 -3
  11. package/dist/host/host.d.ts +9 -1
  12. package/dist/host/host.js +266 -45
  13. package/dist/host/types.d.ts +3 -0
  14. package/dist/index.d.ts +4 -3
  15. package/dist/index.js +1 -1
  16. package/dist/input/config-override.d.ts +5 -0
  17. package/dist/input/config-override.js +23 -0
  18. package/dist/input/input-resolver.d.ts +24 -7
  19. package/dist/input/input-resolver.js +73 -5
  20. package/dist/invocation/bindings.d.ts +53 -0
  21. package/dist/invocation/bindings.js +248 -0
  22. package/dist/invocation/policy.d.ts +5 -1
  23. package/dist/invocation/policy.js +16 -9
  24. package/dist/invocation/remote.d.ts +40 -0
  25. package/dist/invocation/remote.js +489 -0
  26. package/dist/invocation/types.d.ts +2 -0
  27. package/dist/invocation/types.js +1 -0
  28. package/dist/package/index.d.ts +1 -1
  29. package/dist/package/runtime.js +105 -15
  30. package/dist/platform/types.d.ts +48 -1
  31. package/dist/project/config-declarations.d.ts +61 -0
  32. package/dist/project/config-declarations.js +297 -0
  33. package/dist/project/index.d.ts +5 -2
  34. package/dist/project/index.js +5 -2
  35. package/dist/project/init.js +2 -2
  36. package/dist/project/loader.js +4 -0
  37. package/dist/project/manifest.js +10 -0
  38. package/dist/project/types.d.ts +30 -0
  39. package/dist/runtime/context.d.ts +6 -3
  40. package/dist/runtime/context.js +60 -13
  41. package/dist/runtime/env.js +15 -1
  42. package/dist/runtime/standalone-commands.d.ts +2 -2
  43. package/dist/runtime/standalone-commands.js +113 -47
  44. package/dist/runtime/standalone-help.d.ts +1 -0
  45. package/dist/runtime/standalone-help.js +15 -9
  46. package/dist/runtime/standalone.js +2 -2
  47. package/dist/server/policy.d.ts +24 -107
  48. package/dist/server/policy.js +134 -291
  49. package/dist/server/routes/actions.js +10 -8
  50. package/dist/server/routes/common.d.ts +5 -20
  51. package/dist/server/routes/common.js +22 -58
  52. package/dist/server/routes/config.js +65 -7
  53. package/dist/server/routes/doctor.d.ts +1 -0
  54. package/dist/server/routes/doctor.js +4 -3
  55. package/dist/server/routes/info.js +100 -13
  56. package/dist/server/routes/playbooks.js +1 -3
  57. package/dist/server/routes/runs.js +112 -18
  58. package/dist/server/server.d.ts +2 -1
  59. package/dist/server/server.js +38 -55
  60. package/dist/server/types.d.ts +6 -6
  61. package/dist/service/factory.js +1 -0
  62. package/dist/service/remote.js +28 -29
  63. package/dist/service/types.d.ts +34 -5
  64. package/dist/service/types.js +8 -0
  65. package/dist/storage/mask.d.ts +33 -6
  66. package/dist/storage/mask.js +298 -16
  67. package/dist/storage/sqlite-runs.d.ts +5 -17
  68. package/dist/storage/sqlite-runs.js +127 -5
  69. package/dist/storage/sqlite.d.ts +5 -17
  70. package/dist/storage/sqlite.js +2 -2
  71. package/dist/storage/types.d.ts +31 -16
  72. package/dist/version.d.ts +1 -1
  73. package/dist/version.js +1 -1
  74. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ export { loadActionBindings, validateActionBindings, resolveActionBinding, hasRemoteActionBinding, validateCanonicalActionRef, validateBindingName, } from "../invocation/bindings.js";
1
2
  export * from "./digest.js";
2
3
  export * from "./init.js";
3
4
  export * from "./loader.js";
@@ -7,12 +8,14 @@ export { SNAPSHOT_TRACKED_FILES, isProjectLockHeld, acquireProjectLock, runFroze
7
8
  export * from "./types.js";
8
9
  export * from "./types-generator.js";
9
10
  export { assertPathWithinRoot, isPathOutsideBoundary, getPackageSlug, parseDuration, traverseDirectory, } from "../utils/index.js";
10
- export { isSecretConfigKey, maskSecretValue, sanitizeConfigDefinitions, } from "../storage/mask.js";
11
+ export { isSecretConfigKey, maskSecretValue, sanitizeConfigDefinitions, redactConfigValue, redactConfigValueForDeclarations, } from "../storage/mask.js";
12
+ export { isConfigTemplate, resolveConfigDeclaration, validateConfigDeclarations, assertValidConfigDeclarations, assertConcreteConfigKey, validateConfigValue, } from "./config-declarations.js";
11
13
  export { runDoctorChecks, } from "../doctor/doctor.js";
12
14
  // 动作入参解析、输出契约分析、校验与格式化(含 BOM 剥离与标准输入有界读取的单一事实源转引)
13
- export { resolveActionInput, buildActionDescribePayload, formatActionDetail, ACTION_DESCRIBE_SYNTAX_REFERENCE, inspectActionTextFieldAnnotation, analyzeActionOutputContract, mapInputValidationFailure, stripBom, readStdinBounded, } from "../input/index.js";
15
+ export { resolveInput, resolveActionInput, resolveValueInput, hasValueInput, parseValueLiteral, buildActionDescribePayload, formatActionDetail, ACTION_DESCRIBE_SYNTAX_REFERENCE, inspectActionTextFieldAnnotation, analyzeActionOutputContract, mapInputValidationFailure, stripBom, readStdinBounded, } from "../input/index.js";
14
16
  export { validateActionInputValue } from "../value-validator.js";
15
17
  export { parseJson } from "../input/input-resolver.js";
18
+ export { parseConfigOverrideAssignment } from "../input/config-override.js";
16
19
  export { InputError, FlatInputError, } from "../input/flat-errors.js";
17
20
  // 意图过滤与查询
18
21
  export { filterWithFallbackInfo, compileIntentRegex, matchIntent, } from "../utils/intent.js";
@@ -86,10 +86,10 @@ export function initProject(targetDir, options = {}) {
86
86
  node: ">=24.12.0",
87
87
  },
88
88
  dependencies: {
89
- "@actiondock/sdk": "^2.18.0",
89
+ "@actiondock/sdk": "^2.20.0",
90
90
  },
91
91
  devDependencies: {
92
- "@actiondock/testing": "^2.18.0",
92
+ "@actiondock/testing": "^2.20.0",
93
93
  "@types/node": "^22.13.0",
94
94
  "tsx": "^4.19.0",
95
95
  "typescript": "^5.7.0",
@@ -4,6 +4,7 @@ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { NodeModuleLoader, unwrapDefaultExport } from "../platform/module-loader.js";
5
5
  import { ActionDockError, ACTION_LOAD_FAILED, describeActionLoadFailure, INVALID_ACTION_ID, INVALID_JSON, INVALID_PACKAGE_ID, INVALID_PLAYBOOK_ID, MANIFEST_LOAD_FAILED, PLAYBOOK_NOT_FOUND, PROJECT_CONFIG_NOT_FOUND, } from "../errors.js";
6
6
  import { loadManifest, ACTION_ID_REGEX, PLAYBOOK_ID_REGEX } from "./manifest.js";
7
+ import { assertValidConfigDeclarations } from "./config-declarations.js";
7
8
  /**
8
9
  * 从指定目录开始向上逐级递归查找包含 `actiondock.json` 的项目根目录。
9
10
  *
@@ -68,6 +69,9 @@ export function loadProjectConfig(projectRoot) {
68
69
  if (parsed.playbooksDir) {
69
70
  assertWithinProjectRoot(projectRoot, parsed.playbooksDir, "playbooksDir");
70
71
  }
72
+ if (parsed.config !== undefined) {
73
+ assertValidConfigDeclarations(parsed.config);
74
+ }
71
75
  return parsed;
72
76
  }
73
77
  catch (err) {
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
2
2
  import { basename, isAbsolute, join, relative, resolve } from "node:path";
3
+ import { assertValidConfigDeclarations, validateConfigDeclarations } from "./config-declarations.js";
3
4
  import { isPathOutsideBoundary, PACKAGE_ID_REGEX } from "../utils/index.js";
4
5
  import { ActionDockError, MANIFEST_CORRUPTED, MANIFEST_INVALID, MANIFEST_READ_FAILED, UNSUPPORTED_MANIFEST_SCHEMA, } from "../errors.js";
5
6
  export const MANIFEST_FILE_NAME = "actiondock.json";
@@ -38,6 +39,9 @@ export function loadManifest(projectRoot) {
38
39
  if (!parsed.id) {
39
40
  parsed.id = basename(projectRoot);
40
41
  }
42
+ if (parsed.config !== undefined) {
43
+ assertValidConfigDeclarations(parsed.config);
44
+ }
41
45
  return parsed;
42
46
  }
43
47
  /**
@@ -65,6 +69,12 @@ export function validateManifest(manifest, options) {
65
69
  errors.push(`Manifest 'id' is invalid: '${m.id}' (must match ${PACKAGE_ID_REGEX})`);
66
70
  }
67
71
  }
72
+ if (m.config !== undefined) {
73
+ const configErrors = validateConfigDeclarations(m.config);
74
+ for (const err of configErrors) {
75
+ errors.push(err);
76
+ }
77
+ }
68
78
  if (m.actions !== undefined) {
69
79
  if (!m.actions || typeof m.actions !== "object" || Array.isArray(m.actions)) {
70
80
  errors.push("Manifest 'actions' must be an object");
@@ -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,8 @@
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
3
  import type { RuntimeStorage } from "../storage/types.js";
4
4
  import { ProcessManager, type ProcessOwner } from "../process/index.js";
5
+ import type { ConfigSource } from "../service/types.js";
5
6
  /**
6
7
  * 生产级配置解析器实现。
7
8
  * 严格践行 5 层配置解析优先级链:
@@ -26,9 +27,11 @@ export declare class RuntimeConfig implements Config {
26
27
  *
27
28
  * 作为五层优先级链的唯一事实源读取视图,供 app 层 getConfig 安全视图直接委托,
28
29
  * 避免重复实现解析链导致行为漂移。
30
+ * source 仅表达实际生效值来源;undefined 统一视为未命中并继续向低优先级解析,
31
+ * null/false/0/空串均为真实值;整链未命中时 source 为 null。
29
32
  */
30
33
  describe<T = unknown>(key: string): {
31
- source: "overrides" | "package" | "global" | "env" | "default";
34
+ source: ConfigSource | null;
32
35
  value: T | undefined;
33
36
  };
34
37
  }
@@ -81,7 +84,7 @@ export interface ContextOptions {
81
84
  owner?: ProcessOwner;
82
85
  progress?: ProgressReporter;
83
86
  logger?: Logger;
84
- onActionInvoke?: (action: ActionRef | string, input: unknown, parentRunId?: string) => Promise<unknown>;
87
+ onActionInvoke?: (action: ActionRef | string, input: unknown, parentRunId?: string, options?: ActionInvokeOptions) => Promise<unknown>;
85
88
  }
86
89
  /**
87
90
  * 构建并装配传递给 Action 的完整 ActionContext 运行时上下文。
@@ -1,7 +1,8 @@
1
+ import { assertConcreteConfigKey, assertValidConfigDeclarations, resolveConfigDeclaration, validateConfigValue } from "../project/config-declarations.js";
1
2
  import { MemoryProcessDriver, ProcessManager } from "../process/index.js";
2
3
  import { createDefaultProcessOwner } from "../invocation/types.js";
3
4
  import { resolveEnvValue } from "./env.js";
4
- import { ActionDockError, INVALID_ACTION_REF, INVALID_STATE } from "../errors.js";
5
+ import { ActionDockError, INVALID_ACTION_REF, INVALID_ARGUMENT, INVALID_STATE } from "../errors.js";
5
6
  /**
6
7
  * 生产级配置解析器实现。
7
8
  * 严格践行 5 层配置解析优先级链:
@@ -20,32 +21,41 @@ export class RuntimeConfig {
20
21
  constructor(storage, overrides = {}, projectConfig, globalStorage) {
21
22
  this.storage = storage;
22
23
  this.overrides = new Map(Object.entries(overrides));
24
+ assertValidConfigDeclarations(projectConfig?.config);
23
25
  this.projectConfig = projectConfig;
24
26
  this.globalStorage = globalStorage;
25
27
  }
26
28
  get(key, defaultValue) {
27
- // 委托 describe 五层解析链唯一事实源:整链未命中(source 为 default 且无值)时回退调用方默认值
29
+ // 委托 describe 五层解析链唯一事实源:整链未命中(source 为 null 且无值)时回退调用方默认值
28
30
  const resolved = this.describe(key);
29
- if (resolved.source === "default" && resolved.value === undefined) {
31
+ if (resolved.source === null && resolved.value === undefined) {
30
32
  return defaultValue;
31
33
  }
34
+ const resolvedDecl = resolveConfigDeclaration(this.projectConfig?.config, key);
35
+ validateConfigValue(key, resolved.value, resolvedDecl?.item);
32
36
  return resolved.value;
33
37
  }
34
38
  has(key) {
35
- // 委托 describe 五层解析链唯一事实源:default 来源且无值即视为未配置
39
+ // 委托 describe 五层解析链唯一事实源:来源 null 且无值即视为未配置
36
40
  const resolved = this.describe(key);
37
- return !(resolved.source === "default" && resolved.value === undefined);
41
+ return !(resolved.source === null && resolved.value === undefined);
38
42
  }
39
43
  /**
40
44
  * 结构化解析指定配置项的来源与取值视图。
41
45
  *
42
46
  * 作为五层优先级链的唯一事实源读取视图,供 app 层 getConfig 安全视图直接委托,
43
47
  * 避免重复实现解析链导致行为漂移。
48
+ * source 仅表达实际生效值来源;undefined 统一视为未命中并继续向低优先级解析,
49
+ * null/false/0/空串均为真实值;整链未命中时 source 为 null。
44
50
  */
45
51
  describe(key) {
46
- // 1. CLI 临时参数覆写
52
+ assertConcreteConfigKey(key);
53
+ // 1. 运行时固定覆盖与单次调用覆盖(值为 undefined 视为未命中,继续解析链)
47
54
  if (this.overrides.has(key)) {
48
- return { source: "overrides", value: this.overrides.get(key) };
55
+ const overrideValue = this.overrides.get(key);
56
+ if (overrideValue !== undefined) {
57
+ return { source: "override", value: overrideValue };
58
+ }
49
59
  }
50
60
  // 2. 包级持久化存储
51
61
  const stored = this.storage.getConfig(key);
@@ -65,13 +75,17 @@ export class RuntimeConfig {
65
75
  }
66
76
  }
67
77
  // 4. 环境变量
68
- const itemDef = this.projectConfig?.config?.[key];
78
+ const resolvedDecl = resolveConfigDeclaration(this.projectConfig?.config, key);
79
+ const itemDef = resolvedDecl?.item;
69
80
  const envResolved = resolveEnvValue(key, itemDef, this.projectConfig?.id);
70
81
  if (envResolved !== undefined) {
71
82
  return { source: "env", value: envResolved.value };
72
83
  }
73
- // 5. 项目声明的默认值
74
- return { source: "default", value: itemDef?.default };
84
+ // 5. 项目声明的默认值;仅存在真实生效的 default 值才报 default,整链未命中为 null
85
+ if (itemDef?.default !== undefined) {
86
+ return { source: "default", value: itemDef.default };
87
+ }
88
+ return { source: null, value: undefined };
75
89
  }
76
90
  }
77
91
  /**
@@ -156,6 +170,38 @@ export class StderrLogger {
156
170
  process.stderr.write(this.format("error", message, data) + "\n");
157
171
  }
158
172
  }
173
+ /**
174
+ * 校验并规范化 ActionInvokeOptions 调用选项。
175
+ *
176
+ * 针对第三参数运行时检查:
177
+ * - 必须为普通对象或 undefined(2 参数兼容)
178
+ * - 严禁未知字段(如 profile、server、token 等)
179
+ * - binding 若存在必须为非空且非全空白字符串
180
+ */
181
+ function validateActionInvokeOptions(options) {
182
+ if (options === undefined) {
183
+ return undefined;
184
+ }
185
+ if (typeof options !== "object" ||
186
+ options === null ||
187
+ Array.isArray(options) ||
188
+ (Object.getPrototypeOf(options) !== Object.prototype && Object.getPrototypeOf(options) !== null)) {
189
+ throw new ActionDockError(INVALID_ARGUMENT, "INVALID_ARGUMENT: ActionInvokeOptions must be a plain object");
190
+ }
191
+ const allowedKeys = new Set(["binding"]);
192
+ for (const key of Reflect.ownKeys(options)) {
193
+ if (typeof key !== "string" || !allowedKeys.has(key)) {
194
+ throw new ActionDockError(INVALID_ARGUMENT, `INVALID_ARGUMENT: Unknown option '${String(key)}' in ActionInvokeOptions. Only 'binding' is allowed.`);
195
+ }
196
+ }
197
+ const rawBinding = options.binding;
198
+ if (rawBinding !== undefined) {
199
+ if (typeof rawBinding !== "string" || rawBinding.trim() === "") {
200
+ throw new ActionDockError(INVALID_ARGUMENT, "INVALID_ARGUMENT: 'binding' option must be a non-empty string");
201
+ }
202
+ }
203
+ return options;
204
+ }
159
205
  /**
160
206
  * 构建并装配传递给 Action 的完整 ActionContext 运行时上下文。
161
207
  *
@@ -169,17 +215,18 @@ export function createActionContext(options) {
169
215
  const signal = options.signal ?? new AbortController().signal;
170
216
  const currentRunId = options.runId || crypto.randomUUID();
171
217
  const currentRootRunId = options.rootRunId || options.parentRunId || currentRunId;
172
- const invoke = async (action, input) => {
218
+ const invoke = async (action, input, rawOptions) => {
173
219
  if (typeof action !== "string" &&
174
220
  (!action || typeof action !== "object" || typeof action.run === "function" || !("actionId" in action))) {
175
221
  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
222
  }
223
+ const validatedOptions = validateActionInvokeOptions(rawOptions);
177
224
  if (options.onActionInvoke) {
178
- return (await options.onActionInvoke(action, input, currentRunId));
225
+ return (await options.onActionInvoke(action, input, currentRunId, validatedOptions));
179
226
  }
180
227
  throw new ActionDockError(INVALID_STATE, "ActionInvoker not configured with an invocation delegate");
181
228
  };
182
- const invokerFn = (ref, input) => invoke(ref, input);
229
+ const invokerFn = (ref, input, invokeOpts) => invoke(ref, input, invokeOpts);
183
230
  const invoker = Object.assign(invokerFn, { invoke });
184
231
  const defaultOwner = createDefaultProcessOwner();
185
232
  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) {
@@ -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>;
@@ -1,8 +1,9 @@
1
1
  import { decodeStateKey } from "@actiondock/sdk";
2
2
  import { filterWithFallbackInfo } from "../utils/intent.js";
3
- import { resolveExecutionHint } from "../errors.js";
4
- import { resolveActionInput, formatActionDetail, buildActionDescribePayload, mapInputValidationFailure, } from "../input/index.js";
3
+ import { ActionDockError, INVALID_ARGUMENT, resolveExecutionHint } from "../errors.js";
4
+ import { resolveInput, resolveValueInput, formatActionDetail, buildActionDescribePayload, mapInputValidationFailure, } from "../input/index.js";
5
5
  import { validateActionInputValue } from "../value-validator.js";
6
+ import { assertConcreteConfigKey } from "../project/config-declarations.js";
6
7
  import { parseDuration } from "../utils/index.js";
7
8
  import { ExitCode } from "./standalone.js";
8
9
  export async function handleList(ctx, subArgs) {
@@ -148,7 +149,7 @@ export async function handleRun(ctx, subArgs, actionArgs, signal) {
148
149
  }
149
150
  let input;
150
151
  try {
151
- input = await resolveActionInput({
152
+ input = await resolveInput({
152
153
  input: inputStr,
153
154
  inputFile,
154
155
  stdinField,
@@ -273,7 +274,57 @@ function isServiceCapabilityError(err) {
273
274
  return (code === "CAPABILITY_UNAVAILABLE" ||
274
275
  code === "TARGET_CAPABILITY_UNAVAILABLE");
275
276
  }
276
- export async function handleConfig(ctx, subArgs) {
277
+ function parseSetInput(subArgs, isState) {
278
+ const positional = [];
279
+ const input = {};
280
+ let namespace;
281
+ let ttl;
282
+ for (let i = 1; i < subArgs.length; i++) {
283
+ const arg = subArgs[i];
284
+ if (arg === "--json")
285
+ continue;
286
+ if (arg === "--stdin") {
287
+ if (input.fromStdin)
288
+ throw new ActionDockError(INVALID_ARGUMENT, "Duplicate --stdin option");
289
+ input.fromStdin = true;
290
+ continue;
291
+ }
292
+ const equals = arg.indexOf("=");
293
+ const option = equals < 0 ? arg : arg.slice(0, equals);
294
+ if (option === "--stdin-field" || (isState && ["-n", "--namespace", "--ttl"].includes(option))) {
295
+ const value = equals < 0 ? subArgs[++i] : arg.slice(equals + 1);
296
+ if (value === undefined) {
297
+ throw new ActionDockError(INVALID_ARGUMENT, `Option '${option}' requires a value`);
298
+ }
299
+ if (option === "--stdin-field") {
300
+ if (input.stdinField !== undefined) {
301
+ throw new ActionDockError(INVALID_ARGUMENT, "Duplicate --stdin-field option");
302
+ }
303
+ input.stdinField = value;
304
+ }
305
+ else if (option === "--ttl") {
306
+ ttl = parseInt(value, 10);
307
+ }
308
+ else {
309
+ namespace = value;
310
+ }
311
+ }
312
+ else if (arg.startsWith("-") && !/^-\d/.test(arg)) {
313
+ throw new ActionDockError(INVALID_ARGUMENT, "Unknown set option");
314
+ }
315
+ else {
316
+ positional.push(arg);
317
+ if (positional.length > 2) {
318
+ throw new ActionDockError(INVALID_ARGUMENT, "Only one complete positional value is allowed");
319
+ }
320
+ }
321
+ }
322
+ if (!positional[0])
323
+ throw new ActionDockError(INVALID_ARGUMENT, "Key is required");
324
+ input.value = positional[1];
325
+ return { key: positional[0], input, namespace, ttl };
326
+ }
327
+ export async function handleConfig(ctx, subArgs, flatArgs, signal, stdin) {
277
328
  const sub = subArgs[0] || "list";
278
329
  if (!ctx.service.management) {
279
330
  ctx.writeErr("Error: management port is not enabled on this service");
@@ -281,10 +332,15 @@ export async function handleConfig(ctx, subArgs) {
281
332
  }
282
333
  if (sub === "list") {
283
334
  const views = await ctx.service.management.config.list(ctx.options.packageId);
284
- const dict = {};
335
+ const dict = Object.create(null);
285
336
  for (const v of views) {
286
- if (v.configured && v.value !== undefined) {
287
- dict[v.key] = v.value;
337
+ if (v.configured) {
338
+ if (v.secret && v.value === undefined) {
339
+ dict[v.key] = "********";
340
+ }
341
+ else if (v.value !== undefined) {
342
+ dict[v.key] = v.value;
343
+ }
288
344
  }
289
345
  }
290
346
  ctx.writeOut(JSON.stringify(dict, null, 2));
@@ -298,7 +354,15 @@ export async function handleConfig(ctx, subArgs) {
298
354
  }
299
355
  try {
300
356
  const item = await ctx.service.management.config.get(ctx.options.packageId, key);
301
- ctx.writeOut(item.configured ? JSON.stringify(item.value) : "undefined");
357
+ if (item.redactedPaths.includes("") || (item.configured && item.value === undefined)) {
358
+ ctx.writeOut("<hidden>");
359
+ }
360
+ else if (item.value === undefined) {
361
+ ctx.writeOut("<not configured>");
362
+ }
363
+ else {
364
+ ctx.writeOut(JSON.stringify(item.value));
365
+ }
302
366
  return ExitCode.SUCCESS;
303
367
  }
304
368
  catch (err) {
@@ -311,21 +375,27 @@ export async function handleConfig(ctx, subArgs) {
311
375
  }
312
376
  }
313
377
  if (sub === "set") {
314
- const key = subArgs[1];
315
- const rawVal = subArgs[2];
316
- if (!key || rawVal === undefined) {
317
- ctx.writeErr("Error: key and value required");
318
- return ExitCode.INVALID_ARGUMENT;
319
- }
320
- let parsed = rawVal;
378
+ let parsed;
379
+ let value;
321
380
  try {
322
- parsed = JSON.parse(rawVal);
381
+ parsed = parseSetInput(subArgs, false);
382
+ assertConcreteConfigKey(parsed.key);
383
+ value = await resolveValueInput({
384
+ ...parsed.input,
385
+ flatArgs,
386
+ signal,
387
+ stdin,
388
+ policy: { sanitizeInputErrors: true },
389
+ });
390
+ signal?.throwIfAborted();
323
391
  }
324
- catch {
325
- parsed = rawVal;
392
+ catch (err) {
393
+ return ctx.emitError(subArgs.includes("--json"), err?.code || "INVALID_ARGUMENT", err?.message || String(err), ExitCode.INVALID_ARGUMENT, {
394
+ details: err?.details,
395
+ });
326
396
  }
327
- await ctx.service.management.config.set(ctx.options.packageId, key, parsed);
328
- ctx.writeOut(`Config '${key}' updated`);
397
+ await ctx.service.management.config.set(ctx.options.packageId, parsed.key, value);
398
+ ctx.writeOut(`Config '${parsed.key}' updated`);
329
399
  return ExitCode.SUCCESS;
330
400
  }
331
401
  if (sub === "delete") {
@@ -349,7 +419,7 @@ function decodeStateKeyArg(key) {
349
419
  return { namespace: "", key };
350
420
  }
351
421
  }
352
- export async function handleState(ctx, subArgs) {
422
+ export async function handleState(ctx, subArgs, flatArgs, signal, stdin) {
353
423
  const sub = subArgs[0] || "list";
354
424
  let namespace;
355
425
  let isAll = false;
@@ -405,36 +475,32 @@ export async function handleState(ctx, subArgs) {
405
475
  return ExitCode.SUCCESS;
406
476
  }
407
477
  if (sub === "set") {
408
- const key = subArgs[1];
409
- const rawVal = subArgs[2];
410
- if (!key || rawVal === undefined) {
411
- ctx.writeErr("Error: key and value required");
412
- return ExitCode.INVALID_ARGUMENT;
413
- }
414
- let ns = namespace || "";
415
- let actualKey = key;
416
- if (namespace === undefined) {
417
- const decoded = decodeStateKeyArg(key);
418
- ns = decoded.namespace;
419
- actualKey = decoded.key;
420
- }
421
- let parsed = rawVal;
478
+ let parsed;
479
+ let value;
422
480
  try {
423
- parsed = JSON.parse(rawVal);
481
+ parsed = parseSetInput(subArgs, true);
482
+ value = await resolveValueInput({
483
+ ...parsed.input,
484
+ flatArgs,
485
+ signal,
486
+ stdin,
487
+ policy: { sanitizeInputErrors: true },
488
+ });
489
+ signal?.throwIfAborted();
424
490
  }
425
- catch {
426
- parsed = rawVal;
491
+ catch (err) {
492
+ return ctx.emitError(isJson, err?.code || "INVALID_ARGUMENT", err?.message || String(err), ExitCode.INVALID_ARGUMENT, {
493
+ details: err?.details,
494
+ });
427
495
  }
428
- let ttl;
429
- for (let i = 3; i < subArgs.length; i++) {
430
- if (subArgs[i] === "--ttl" && i + 1 < subArgs.length) {
431
- ttl = parseInt(subArgs[++i], 10);
432
- }
433
- else if (subArgs[i].startsWith("--ttl=")) {
434
- ttl = parseInt(subArgs[i].slice(6), 10);
435
- }
496
+ let ns = parsed.namespace || "";
497
+ let actualKey = parsed.key;
498
+ if (parsed.namespace === undefined) {
499
+ const decoded = decodeStateKeyArg(parsed.key);
500
+ ns = decoded.namespace;
501
+ actualKey = decoded.key;
436
502
  }
437
- await ctx.service.management.state.set(ctx.options.packageId, "", actualKey, parsed, { namespace: ns, ttl });
503
+ await ctx.service.management.state.set(ctx.options.packageId, "", actualKey, value, { namespace: ns, ttl: parsed.ttl });
438
504
  const displayKey = ns ? `${ns}:${actualKey}` : actualKey;
439
505
  ctx.writeOut(`State '${displayKey}' updated`);
440
506
  return ExitCode.SUCCESS;
@@ -1,3 +1,4 @@
1
+ export { parseConfigOverrideAssignment } from "../input/config-override.js";
1
2
  export interface ParsedArgs {
2
3
  controlArgs: string[];
3
4
  actionArgs: string[];
@@ -1,3 +1,5 @@
1
+ import { parseConfigOverrideAssignment } from "../input/config-override.js";
2
+ export { parseConfigOverrideAssignment } from "../input/config-override.js";
1
3
  export function parseStandaloneArgs(argv) {
2
4
  const separator = argv.indexOf("--");
3
5
  const controlArgs = separator >= 0 ? argv.slice(0, separator) : argv;
@@ -13,17 +15,15 @@ export function parseStandaloneArgs(argv) {
13
15
  else if (arg.startsWith("--data-dir=")) {
14
16
  dataDir = arg.slice(11);
15
17
  }
16
- else if (arg === "--config" && i + 1 < controlArgs.length) {
17
- const pair = controlArgs[++i];
18
- const [k, ...v] = pair.split("=");
19
- if (k)
20
- configOverrides[k] = v.join("=");
18
+ else if (arg === "--config" || arg === "-c") {
19
+ const pair = controlArgs[++i] ?? "";
20
+ const { key, value } = parseConfigOverrideAssignment(pair);
21
+ Object.defineProperty(configOverrides, key, { value, enumerable: true, writable: true, configurable: true });
21
22
  }
22
23
  else if (arg.startsWith("--config=")) {
23
24
  const pair = arg.slice(9);
24
- const [k, ...v] = pair.split("=");
25
- if (k)
26
- configOverrides[k] = v.join("=");
25
+ const { key, value } = parseConfigOverrideAssignment(pair);
26
+ Object.defineProperty(configOverrides, key, { value, enumerable: true, writable: true, configurable: true });
27
27
  }
28
28
  else {
29
29
  filteredArgs.push(arg);
@@ -45,10 +45,16 @@ export function printHelp(writeOut, options) {
45
45
  writeOut(" <cmd> run <id> [--stdin-field <field>] [-- <assignments...>]");
46
46
  writeOut(" Bind raw stdin text to an input field");
47
47
  writeOut(" <cmd> config list/get/set/delete Manage package configuration");
48
+ writeOut(" <cmd> config set <key> [<value> | --stdin]");
49
+ writeOut(" <cmd> config set <key> [--stdin-field <field>] [-- <assignments...>]");
48
50
  writeOut(" <cmd> state list/get/set/delete Manage shared state store");
51
+ writeOut(" <cmd> state set <key> [<value> | --stdin] [-n <namespace>] [--ttl <seconds>]");
52
+ writeOut(" <cmd> state set <key> [--stdin-field <field>] [-n <namespace>] [--ttl <seconds>] [-- <assignments...>]");
53
+ writeOut(" set 整键替换;完整值、--stdin 与结构化输入互斥");
54
+ writeOut(" 扁平赋值:field=text 或 field:=json;--stdin-field 保留原始正文");
49
55
  writeOut("\nGlobal options:");
50
56
  writeOut(" --data-dir <path> Custom runtime database directory");
51
- writeOut(" --config <KEY=val> Temporary config override");
57
+ writeOut(" --config <KEY=val|KEY:=json> Temporary config override");
52
58
  }
53
59
  export function emitError(writeOut, writeErr, isJson, code, message, exitCode, options) {
54
60
  if (isJson) {
@@ -149,10 +149,10 @@ export class StandaloneDispatcher {
149
149
  code = await handleRun(ctx, subArgs, actionArgs, control?.signal);
150
150
  break;
151
151
  case "config":
152
- code = await handleConfig(ctx, subArgs);
152
+ code = await handleConfig(ctx, subArgs, argv.includes("--") ? actionArgs : undefined, control?.signal);
153
153
  break;
154
154
  case "state":
155
- code = await handleState(ctx, subArgs);
155
+ code = await handleState(ctx, subArgs, argv.includes("--") ? actionArgs : undefined, control?.signal);
156
156
  break;
157
157
  default:
158
158
  this.writeErr(`Unknown command: '${command}'`);