@actiondock/core 2.19.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 (66) 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 +3 -1
  6. package/dist/errors.js +2 -0
  7. package/dist/execution/service.js +8 -5
  8. package/dist/execution/types.d.ts +3 -3
  9. package/dist/host/host.d.ts +9 -1
  10. package/dist/host/host.js +183 -38
  11. package/dist/host/types.d.ts +3 -0
  12. package/dist/index.d.ts +4 -3
  13. package/dist/index.js +1 -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 +2 -0
  25. package/dist/invocation/types.js +1 -0
  26. package/dist/package/index.d.ts +1 -1
  27. package/dist/package/runtime.js +31 -12
  28. package/dist/platform/types.d.ts +48 -1
  29. package/dist/project/config-declarations.d.ts +61 -0
  30. package/dist/project/config-declarations.js +297 -0
  31. package/dist/project/index.d.ts +5 -2
  32. package/dist/project/index.js +5 -2
  33. package/dist/project/init.js +2 -2
  34. package/dist/project/loader.js +4 -0
  35. package/dist/project/manifest.js +10 -0
  36. package/dist/project/types.d.ts +30 -0
  37. package/dist/runtime/context.d.ts +6 -3
  38. package/dist/runtime/context.js +60 -13
  39. package/dist/runtime/env.js +15 -1
  40. package/dist/runtime/standalone-commands.d.ts +2 -2
  41. package/dist/runtime/standalone-commands.js +113 -47
  42. package/dist/runtime/standalone-help.d.ts +1 -0
  43. package/dist/runtime/standalone-help.js +15 -9
  44. package/dist/runtime/standalone.js +2 -2
  45. package/dist/server/policy.d.ts +24 -107
  46. package/dist/server/policy.js +134 -291
  47. package/dist/server/routes/actions.js +10 -8
  48. package/dist/server/routes/common.d.ts +5 -20
  49. package/dist/server/routes/common.js +22 -58
  50. package/dist/server/routes/config.js +65 -7
  51. package/dist/server/routes/doctor.d.ts +1 -0
  52. package/dist/server/routes/doctor.js +4 -3
  53. package/dist/server/routes/info.js +100 -13
  54. package/dist/server/routes/playbooks.js +1 -3
  55. package/dist/server/routes/runs.js +96 -18
  56. package/dist/server/server.js +25 -53
  57. package/dist/server/types.d.ts +6 -6
  58. package/dist/service/factory.js +1 -0
  59. package/dist/service/remote.js +14 -29
  60. package/dist/service/types.d.ts +20 -5
  61. package/dist/service/types.js +8 -0
  62. package/dist/storage/mask.d.ts +33 -6
  63. package/dist/storage/mask.js +298 -16
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +2 -2
@@ -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}'`);
@@ -3,144 +3,61 @@ import { ActionDockError } from "../errors.js";
3
3
  import type { McpEndpointHandler } from "./mcp-endpoint.js";
4
4
  import type { EffectiveServerPolicy, ServerOptions, ServerViewOptions } from "./types.js";
5
5
  export { extractBearerToken } from "./security.js";
6
- /**
7
- * 动作未列入白名单拒绝访问异常。
8
- */
6
+ type PolicyAction = {
7
+ actionId?: string;
8
+ id?: string;
9
+ packageId?: string;
10
+ } | string;
11
+ /** 校验两类白名单授权;省略或空数组不增加授权,包列表中的独立 * 显式授予全部权限。 */
12
+ export declare function assertServerPolicyValid(item: {
13
+ packageAllowlist?: unknown;
14
+ actionAllowlist?: unknown;
15
+ }, scope?: string): void;
16
+ /** 是否显式授予全部包与动作;不替代独立的管理能力门禁。 */
17
+ export declare function isFullAccessGranted(policy?: EffectiveServerPolicy | null): boolean;
9
18
  export declare class ActionForbiddenError extends ActionDockError {
10
19
  status: number;
11
20
  statusCode: number;
12
21
  constructor(actionRef: string);
13
22
  }
14
- /**
15
- * 包未列入白名单拒绝访问异常。
16
- */
17
23
  export declare class PackageNotAllowedError extends ActionDockError {
18
24
  status: number;
19
25
  statusCode: number;
20
26
  constructor(packageId: string);
21
27
  }
22
- /**
23
- * 归一化后的单个服务视图结构。
24
- */
25
28
  export interface NormalizedServerView {
26
- /** 视图唯一标识名称 */
27
29
  name: string;
28
- /** 该视图生效的统一安全策略 */
29
30
  policy: EffectiveServerPolicy;
30
- /** 该视图是否启用 MCP 端点 */
31
31
  enableMcp: boolean;
32
- /** 原始配置选项 */
33
32
  rawOptions: ServerViewOptions;
34
- /** 为该视图独立装配的 MCP 端点处理器 */
35
33
  mcpEndpointHandler?: McpEndpointHandler;
36
34
  }
37
- /**
38
- * 根据客户端携带的 Token 智能匹配已注册的视图。
39
- * 使用 safeEqual 进行恒定时间比较以防范时序攻击。
40
- *
41
- * @param token 客户端提供的 Bearer Token
42
- * @param views 可遍历的视图集合
43
- * @returns 匹配成功的视图对象,未命中返回 undefined
44
- */
35
+ /** 遍历全部视图并以常数时间比较令牌,避免提前退出泄露匹配位置。 */
45
36
  export declare function matchViewByToken(token: string, views: Iterable<NormalizedServerView>): NormalizedServerView | undefined;
46
- /**
47
- * 归一化解析服务端配置的所有视图集合。
48
- * 保证默认视图与显式自定义视图具有统一的数据结构。
49
- *
50
- * @param options 服务端配置选项
51
- * @returns 默认视图与全部视图 Map 映射
52
- */
37
+ /** 全局选项仅配置默认视图;命名视图独立授权,缺少授权的视图默认拒绝全部。 */
53
38
  export declare function normalizeServerViews(options: ServerOptions): {
54
39
  defaultView: NormalizedServerView;
55
40
  views: Map<string, NormalizedServerView>;
56
41
  };
57
- /**
58
- * 校验目标动作是否在 actionAllowlist 允许白名单中。
59
- * 支持短名 actionId 与全限定名 packageId/actionId 匹配:
60
- * - 白名单项若含 '/'(如 pkg/act),则必须 packageId 与 actionId 同时精确匹配。
61
- * - 白名单项若不含 '/'(如 act),则只需 actionId 匹配(不限包)。
62
- *
63
- * @param action 目标动作对象或引用字符串
64
- * @param allowlist 允许动作白名单列表
65
- * @returns 是否允许访问
66
- */
67
- export declare function isActionAllowed(action: {
68
- actionId?: string;
69
- id?: string;
70
- packageId?: string;
71
- } | string | null | undefined, allowlist?: string[]): boolean;
72
- /**
73
- * 根据生效策略统一判定目标动作是否允许访问。
74
- * 同时综合校验策略中的 packageAllowlist 与 actionAllowlist 约束。
75
- *
76
- * @param action 目标动作对象或引用字符串
77
- * @param policy 当前生效的服务端策略
78
- * @returns 是否允许访问
79
- */
80
- export declare function isActionAllowedByPolicy(action: {
81
- actionId?: string;
82
- id?: string;
83
- packageId?: string;
84
- } | string | null | undefined, policy?: EffectiveServerPolicy): boolean;
85
- /**
86
- * 根据生效策略统一判定目标包标识是否允许访问。
87
- *
88
- * @param packageId 目标包 ID
89
- * @param policy 当前生效的服务端策略
90
- * @returns 是否允许访问
91
- */
42
+ /** 动作列表仅按全限定引用匹配;省略或空列表不会授予任何动作。 */
43
+ export declare function isActionAllowed(action: PolicyAction | null | undefined, allowlist?: string[]): boolean;
44
+ /** 整包授权与全限定单动作授权取并集;仅缺少策略对象的内部调用不执行过滤。 */
45
+ export declare function isActionAllowedByPolicy(action: PolicyAction | null | undefined, policy?: EffectiveServerPolicy): boolean;
46
+ /** 整包能力授权,不从某个动作的授权推导配置、状态、规程或诊断权限。 */
92
47
  export declare function isPackageAllowedByPolicy(packageId?: string | null, policy?: EffectiveServerPolicy): boolean;
93
- /**
94
- * 根据生效策略统一判定配置与状态管理路由是否开启。
95
- *
96
- * @param policy 当前生效的服务端策略
97
- * @returns 是否允许管理操作
98
- */
48
+ /** 单动作授权的包可发现必要身份信息,但不因此获得整包能力。 */
49
+ export declare function isPackageVisibleByPolicy(packageId?: string | null, policy?: EffectiveServerPolicy): boolean;
50
+ export declare function requiresMinimalPackageProjection(packageId?: string | null, policy?: EffectiveServerPolicy): boolean;
99
51
  export declare function isManagementAllowedByPolicy(policy?: EffectiveServerPolicy): boolean;
100
- /**
101
- * 根据生效策略对动作列表进行统一过滤。
102
- *
103
- * @param actions 待过滤的动作列表
104
- * @param policy 当前生效的服务端策略
105
- * @returns 过滤后允许访问的动作列表
106
- */
107
52
  export declare function filterActionsByPolicy<T extends {
108
53
  id: string;
109
54
  packageId?: string;
110
55
  actionId?: string;
111
56
  }>(actions: T[], policy?: EffectiveServerPolicy): T[];
112
- /**
113
- * 根据生效策略对包列表进行统一过滤。
114
- *
115
- * @param packages 待过滤的包列表
116
- * @param policy 当前生效的服务端策略
117
- * @returns 过滤后允许访问的包列表
118
- */
119
57
  export declare function filterPackagesByPolicy<T extends {
120
58
  id: string;
121
59
  }>(packages: T[], policy?: EffectiveServerPolicy): T[];
122
- /**
123
- * 校验目标动作是否属于允许白名单,不符合则抛出 403 异常。
124
- *
125
- * @param action 目标动作对象或引用
126
- * @param policy 当前生效的服务端策略
127
- */
128
- export declare function assertActionAllowedByPolicy(action: {
129
- actionId?: string;
130
- id?: string;
131
- packageId?: string;
132
- } | string, policy?: EffectiveServerPolicy): void;
133
- /**
134
- * 校验目标包是否属于允许白名单,不符合则抛出 403 异常。
135
- *
136
- * @param packageId 目标包 ID
137
- * @param policy 当前生效的服务端策略
138
- */
60
+ export declare function assertActionAllowedByPolicy(action: PolicyAction, policy?: EffectiveServerPolicy): void;
139
61
  export declare function assertPackageAllowedByPolicy(packageId: string, policy?: EffectiveServerPolicy): void;
140
- /**
141
- * 构造受信任的服务端执行来源快照。
142
- *
143
- * 仅挑选 channel、viewName(默认 "default")与 authenticated(视图配置非空有效令牌)。
144
- * 严禁暴露令牌原文、片段或摘要。
145
- */
62
+ /** 只记录受信任的执行来源,不能包含令牌原文、片段或摘要。 */
146
63
  export declare function createServerRunOrigin(policy: Pick<EffectiveServerPolicy, "viewName" | "token">, channel: RunOrigin["channel"]): RunOrigin;