@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
package/dist/host/host.js CHANGED
@@ -8,16 +8,20 @@ import { hasPendingTransactions, isProjectLockHeld, recoverPendingTransactions }
8
8
  import { listLinkedPackages } from "../registry/registry.js";
9
9
  import { InMemoryEventSink } from "../runtime/events.js";
10
10
  import { snapshotRunOrigin } from "../runtime/run-origin.js";
11
- import { ActionDockError, ACTION_NOT_FOUND, ACTION_PACKAGE_VERSION_CONFLICT, ACTION_SUBRUN_LIMIT, EXECUTION_FAILED, INVALID_ACTION_REF, PACKAGE_ID_CONFLICT, PACKAGE_NOT_FOUND, PROJECT_BUSY, PROJECT_RECOVERY_REQUIRED, SERVICE_CLOSED, UNDECLARED_ACTION_DEPENDENCY, } from "../errors.js";
11
+ import { ActionDockError, ACTION_CANCELLED, ACTION_NOT_FOUND, ACTION_PACKAGE_VERSION_CONFLICT, ACTION_SUBRUN_LIMIT, ACTION_TIMEOUT, EXECUTION_FAILED, INVALID_ACTION_REF, PACKAGE_ID_CONFLICT, PACKAGE_NOT_FOUND, PROJECT_BUSY, PROJECT_RECOVERY_REQUIRED, SERVICE_CLOSED, UNDECLARED_ACTION_DEPENDENCY, } from "../errors.js";
12
12
  import { InvocationPolicy } from "../invocation/policy.js";
13
+ import { loadActionBindings, resolveActionBinding, validateActionBindings, } from "../invocation/bindings.js";
14
+ import { RemoteInvocationExecutor } from "../invocation/remote.js";
15
+ import { resolveTarget } from "../profile/manager.js";
13
16
  import { createAbortError } from "../ipc/service.js";
14
17
  import { DataDirLock } from "../storage/data-dir-lock.js";
15
18
  import { DefaultActionCatalog, DefaultPackageGraph, PackageGraphBuilder, resolveAction, } from "../catalog/index.js";
16
19
  import { parseActionRef } from "../catalog/resolve-action.js";
17
20
  import { buildRuntimeError, collectPackageInfos, describeActionAcrossRuntimes, describeVisiblePlaybook, isRootCallVisible, listVisiblePlaybooks, packageNotFoundMessage, parseRefLoose, resolveProjectRoot, } from "./routing.js";
18
21
  import { CAPABILITY_UNAVAILABLE } from "../errors.js";
19
- import { createGlobalStorage, isSecretConfigKey } from "../storage/index.js";
22
+ import { createGlobalStorage, redactConfigValueAtPaths, redactConfigValueForDeclarations, readConfigWithProtection, redactConfigValueWithProtection } from "../storage/index.js";
20
23
  import { filterByIntent } from "../utils/intent.js";
24
+ import { assertConcreteConfigKey, isConfigTemplate, resolveConfigDeclaration, validateConfigValue, } from "../project/config-declarations.js";
21
25
  function isPackageRuntime(item) {
22
26
  return (typeof item === "object" &&
23
27
  item !== null &&
@@ -56,16 +60,33 @@ function withAbortSignal(op, signal) {
56
60
  });
57
61
  });
58
62
  }
63
+ function isActionTimeoutReason(reason) {
64
+ if (!reason)
65
+ return false;
66
+ const code = reason?.code;
67
+ if (code === ACTION_TIMEOUT)
68
+ return true;
69
+ if (reason?.details?.code === ACTION_TIMEOUT)
70
+ return true;
71
+ return false;
72
+ }
59
73
  export class DefaultActionDockHost {
60
74
  hostSessionId;
61
75
  options;
76
+ allowInstall;
62
77
  runtimes = new Map();
78
+ extraRuntimes = new Set();
79
+ extraRuntimeScopes = new Map();
63
80
  policy;
64
81
  hostPublicPackageIds = new Set();
65
82
  maxCallDepth;
66
83
  maxSubRuns;
67
84
  eventSink;
68
85
  isClosed = false;
86
+ prepareLocalPackage;
87
+ closeController = new AbortController();
88
+ activePreparations = new Set();
89
+ closePromise;
69
90
  dataDirLock;
70
91
  failedLinkedPackages = new Map();
71
92
  failedAutoLoad;
@@ -76,6 +97,9 @@ export class DefaultActionDockHost {
76
97
  globalStorage;
77
98
  initPromise;
78
99
  isInitialized = false;
100
+ actionBindings = new Map();
101
+ remoteTargets = new Map();
102
+ remoteInvoker;
79
103
  discovery;
80
104
  execution;
81
105
  runs;
@@ -100,6 +124,9 @@ export class DefaultActionDockHost {
100
124
  }
101
125
  this.hostSessionId = randomUUID();
102
126
  this.options = options;
127
+ this.allowInstall = options.allowInstall;
128
+ this.prepareLocalPackage = options.prepareLocalPackage;
129
+ this.remoteInvoker = new RemoteInvocationExecutor({ logger: options.logger });
103
130
  this.maxCallDepth = options.maxCallDepth ?? 16;
104
131
  this.maxSubRuns = options.maxSubRuns ?? 64;
105
132
  this.policy = new InvocationPolicy({
@@ -348,6 +375,21 @@ export class DefaultActionDockHost {
348
375
  // 持有者在返回前打开共享全局存储,保留对全局库存储错误的提前发现能力
349
376
  this.getGlobalStorage();
350
377
  }
378
+ /** 返回的 Runtime 由 Host 统一接管,端口只清理自身缓存和附属资源。 */
379
+ async closeInvocationTargets() {
380
+ const extras = Array.from(this.extraRuntimes).filter((runtime) => this.runtimes.get(runtime.packageId) !== runtime);
381
+ this.extraRuntimes.clear();
382
+ this.extraRuntimeScopes.clear();
383
+ const results = await Promise.allSettled(extras.map((runtime) => runtime.close()));
384
+ const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
385
+ try {
386
+ await this.options.platform?.invocationTargets?.close?.();
387
+ }
388
+ catch (error) {
389
+ errors.push(error);
390
+ }
391
+ return errors;
392
+ }
351
393
  /**
352
394
  * 异步安全回滚初始化失败时所占用的资源。
353
395
  */
@@ -363,7 +405,11 @@ export class DefaultActionDockHost {
363
405
  // 忽略 Runtime 关闭异常
364
406
  }
365
407
  }));
408
+ const invocationCloseErrors = await this.closeInvocationTargets();
366
409
  this.runtimes.clear();
410
+ this.actionBindings.clear();
411
+ this.remoteTargets.clear();
412
+ await this.remoteInvoker.close();
367
413
  try {
368
414
  this.globalStorage?.close();
369
415
  }
@@ -379,6 +425,8 @@ export class DefaultActionDockHost {
379
425
  }
380
426
  this.dataDirLock = undefined;
381
427
  this.isClosed = true;
428
+ if (invocationCloseErrors.length)
429
+ throw new AggregateError(invocationCloseErrors, "Invocation target cleanup failed");
382
430
  }
383
431
  /**
384
432
  * 阶段函数:注册显式传入的 packages 列表(PackageRuntimeOptions 配置或包目录物理路径)。
@@ -438,7 +486,10 @@ export class DefaultActionDockHost {
438
486
  if (!this.recoverOrphans) {
439
487
  throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Project at '${root}' has pending transactions requiring recovery. Run an owner command (e.g. 'ad add' or 'ad run') to complete recovery before querying.`);
440
488
  }
441
- await recoverPendingTransactions(root, { frozenInstall: true });
489
+ if (this.allowInstall === false || process.env.ACTIONDOCK_AUTO_INSTALL === "false") {
490
+ throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Project at '${root}' has pending transactions requiring recovery install, but installation is disabled (--no-install or ACTIONDOCK_AUTO_INSTALL=false).`);
491
+ }
492
+ await recoverPendingTransactions(root, { frozenInstall: true, allowInstall: this.allowInstall });
442
493
  }
443
494
  try {
444
495
  const config = loadProjectConfig(root);
@@ -551,21 +602,75 @@ export class DefaultActionDockHost {
551
602
  * 铁律 2:跨包调用改走 ActionInvoker 回到 Host 执行主链,经由唯一的 InvocationPolicy 授权后调用目标 Package 的 ExecutionService。
552
603
  */
553
604
  createActionInvoker(callerRuntime) {
554
- return async (childAction, childInput, context) => {
555
- const resolved = resolveAction(childAction, {
605
+ return async (childAction, childInput, context, options) => {
606
+ if (this.isClosed)
607
+ throw new ActionDockError(SERVICE_CLOSED, "ActionDockHost is closed");
608
+ if (context.signal.aborted)
609
+ throw new ActionDockError(ACTION_CANCELLED, "Caller invocation is cancelled");
610
+ const parsed = parseActionRef(childAction);
611
+ // 裸引用仍由既有解析器消歧;远端专属依赖须使用完全限定引用。
612
+ const localResolved = parsed.packageId ? undefined : resolveAction(childAction, {
556
613
  caller: callerRuntime.packageId,
557
614
  graph: this.graph,
558
615
  catalog: this.catalog,
559
616
  });
560
- const targetPackageId = resolved.package.id;
561
- const targetActionId = resolved.ref.actionId;
562
- const isSamePackage = targetPackageId === callerRuntime.packageId;
563
- const targetRuntime = isSamePackage ? callerRuntime : this.getRuntime(targetPackageId);
564
- if (!targetRuntime) {
565
- throw new ActionDockError(PACKAGE_NOT_FOUND, packageNotFoundMessage(targetPackageId, this.failedLinkedPackages));
617
+ const targetPackageId = parsed.packageId ?? localResolved.package.id;
618
+ const targetActionId = parsed.actionId;
619
+ const canonicalRef = `${targetPackageId}/${targetActionId}`;
620
+ const canonicalActionRef = { packageId: targetPackageId, actionId: targetActionId };
621
+ const callerBindings = this.actionBindings.get(callerRuntime.packageId) ?? {};
622
+ const isNamedTarget = Boolean(options?.binding && options.binding !== "default");
623
+ // 目标装配是同步解析,不在此引入另一条执行或网络调用链。
624
+ const targetOverride = this.options.platform?.invocationTargets?.resolve(canonicalActionRef, options, {
625
+ packageId: callerRuntime.packageId,
626
+ actionId: context.caller?.actionId,
627
+ });
628
+ let targetRuntime;
629
+ let targetScope;
630
+ let isRemote = false;
631
+ let remoteProfile;
632
+ if (targetOverride) {
633
+ if (targetOverride.runtime.packageId !== targetPackageId) {
634
+ throw new ActionDockError(INVALID_ACTION_REF, `Invocation target packageId mismatch: expected '${targetPackageId}', got '${targetOverride.runtime.packageId}'`);
635
+ }
636
+ targetRuntime = targetOverride.runtime;
637
+ this.extraRuntimes.add(targetRuntime);
638
+ this.bindRuntime(targetRuntime);
639
+ targetScope = targetOverride.scope ?? (isNamedTarget ? `binding:${options?.binding}` : undefined);
640
+ if (targetScope)
641
+ this.extraRuntimeScopes.set(targetRuntime, targetScope);
566
642
  }
643
+ else {
644
+ // 没有绑定的旧调用不引入额外的清单命名约束,保持编程式 Runtime 兼容。
645
+ const binding = options?.binding !== undefined || Object.hasOwn(callerBindings, canonicalRef)
646
+ ? resolveActionBinding(callerBindings, canonicalRef, options?.binding)
647
+ : undefined;
648
+ if (binding?.type === "remote") {
649
+ isRemote = true;
650
+ remoteProfile = binding.profile;
651
+ targetScope = `profile:${binding.profile}`;
652
+ }
653
+ else {
654
+ isRemote = false;
655
+ if (!localResolved) {
656
+ resolveAction(childAction, { caller: callerRuntime.packageId, graph: this.graph, catalog: this.catalog });
657
+ }
658
+ const isLogicalSamePackage = targetPackageId === callerRuntime.packageId;
659
+ targetRuntime = isLogicalSamePackage ? callerRuntime : this.getRuntime(targetPackageId);
660
+ if (targetRuntime)
661
+ targetScope = this.extraRuntimeScopes.get(targetRuntime);
662
+ if (!targetRuntime) {
663
+ throw new ActionDockError(PACKAGE_NOT_FOUND, packageNotFoundMessage(targetPackageId, this.failedLinkedPackages));
664
+ }
665
+ }
666
+ }
667
+ const isLogicalSamePackage = targetPackageId === callerRuntime.packageId;
668
+ const isPhysicalSameRuntime = Boolean(targetRuntime && targetRuntime === callerRuntime);
669
+ const requiresExplicitUses = Boolean(isRemote ||
670
+ targetOverride?.requiresExplicitUses ||
671
+ (isNamedTarget && targetOverride !== undefined));
567
672
  // 跨包 uses 依赖声明校验(统一委托 InvocationPolicy 单一事实源)
568
- if (!isSamePackage) {
673
+ if (!isLogicalSamePackage || requiresExplicitUses) {
569
674
  let declaredUses = context.caller?.declaredUses;
570
675
  if (!declaredUses) {
571
676
  try {
@@ -580,7 +685,11 @@ export class DefaultActionDockHost {
580
685
  packageId: callerRuntime.packageId,
581
686
  actionId: context.caller?.actionId || "",
582
687
  declaredUses,
583
- }, { packageId: targetPackageId, actionId: targetActionId }, this.graph);
688
+ }, {
689
+ packageId: targetPackageId,
690
+ actionId: targetActionId,
691
+ requiresExplicitUses,
692
+ }, this.graph);
584
693
  if (authErr) {
585
694
  const hint = authErr.details?.hint;
586
695
  const msg = hint ? `${authErr.message}\n${hint}` : authErr.message;
@@ -593,7 +702,7 @@ export class DefaultActionDockHost {
593
702
  throw new ActionDockError(depthErr.code, depthErr.message, depthErr.details);
594
703
  }
595
704
  // 调用链环路死锁检测
596
- const cycle = this.policy.checkCycle(context.callStack, targetActionId, targetPackageId, callerRuntime.packageId);
705
+ const cycle = this.policy.checkCycle(context.callStack, targetActionId, targetPackageId, callerRuntime.packageId, targetScope);
597
706
  if (cycle.error) {
598
707
  throw new ActionDockError(cycle.error.code, cycle.error.message, cycle.error.details);
599
708
  }
@@ -608,14 +717,18 @@ export class DefaultActionDockHost {
608
717
  }
609
718
  try {
610
719
  const nextCallStack = [...context.callStack, cycle.callKey];
720
+ if (isRemote) {
721
+ const candidate = this.catalog.find({ packageId: targetPackageId, actionId: targetActionId })[0];
722
+ return await this.remoteInvoker.invoke(this.remoteTargets.get(remoteProfile), { packageId: targetPackageId, actionId: targetActionId }, childInput, { ...context, callStack: nextCallStack }, { binding: options?.binding ?? "default", profile: remoteProfile }, candidate?.contract);
723
+ }
611
724
  const targetOwner = context.owner
612
725
  ? {
613
726
  tenantId: context.owner.tenantId,
614
727
  principalId: context.owner.principalId,
615
- packageInstanceId: isSamePackage
728
+ packageInstanceId: isPhysicalSameRuntime
616
729
  ? (context.owner.packageInstanceId || callerRuntime.identity.instanceId)
617
730
  : targetRuntime.identity.instanceId,
618
- generationId: isSamePackage
731
+ generationId: isPhysicalSameRuntime
619
732
  ? (context.owner.generationId || callerRuntime.identity.generation)
620
733
  : targetRuntime.identity.generation,
621
734
  }
@@ -626,6 +739,7 @@ export class DefaultActionDockHost {
626
739
  parentRunId: context.parentRunId,
627
740
  caller: context.caller,
628
741
  callStack: nextCallStack,
742
+ targetScope,
629
743
  package: targetRuntime.identity,
630
744
  signal: context.signal,
631
745
  timeoutMs: context.timeoutMs,
@@ -641,7 +755,7 @@ export class DefaultActionDockHost {
641
755
  owner: targetOwner,
642
756
  origin: context.origin,
643
757
  };
644
- const ticket = await targetRuntime.startInvocation(targetActionId, childInput, subInvocationContext);
758
+ const ticket = await this.dispatchLocalInvocation(targetRuntime, targetActionId, childInput, subInvocationContext);
645
759
  if (!ticket.result) {
646
760
  throw new ActionDockError(EXECUTION_FAILED, `Execution ticket for run '${ticket.runId}' has no result Promise`);
647
761
  }
@@ -699,6 +813,27 @@ export class DefaultActionDockHost {
699
813
  }
700
814
  throw new ActionDockError(PACKAGE_ID_CONFLICT, `Package ID conflict: package '${runtime.packageId}' is already registered in host`);
701
815
  }
816
+ const explicitBindings = this.options.actionBindings;
817
+ const bindings = explicitBindings && Object.hasOwn(explicitBindings, runtime.packageId)
818
+ ? validateActionBindings(explicitBindings[runtime.packageId])
819
+ : runtime.packageRoot ? loadActionBindings(runtime.packageRoot) : validateActionBindings({});
820
+ for (const group of Object.values(bindings)) {
821
+ for (const target of Object.values(group)) {
822
+ if (target.type !== "remote" || this.remoteTargets.has(target.profile))
823
+ continue;
824
+ const resolved = resolveTarget({ profile: target.profile }, this.options.customHome);
825
+ if (resolved.type !== "remote" || !resolved.serverUrl) {
826
+ throw new ActionDockError(INVALID_ACTION_REF, `Binding profile '${target.profile}' must identify a remote service`);
827
+ }
828
+ this.remoteTargets.set(target.profile, Object.freeze({
829
+ serverUrl: resolved.serverUrl,
830
+ token: resolved.token,
831
+ insecure: resolved.insecure,
832
+ allowInsecureHttp: resolved.allowInsecureHttp,
833
+ }));
834
+ }
835
+ }
836
+ this.actionBindings.set(runtime.packageId, bindings);
702
837
  this.runtimes.set(runtime.packageId, runtime);
703
838
  if (isPublic) {
704
839
  this.hostPublicPackageIds.add(runtime.packageId);
@@ -856,8 +991,182 @@ export class DefaultActionDockHost {
856
991
  process: this.options.process,
857
992
  origin: effectiveOptions.origin,
858
993
  });
859
- // 调度目标包执行服务
860
- return targetRuntime.startInvocation(targetActionId, input, rootContext);
994
+ // 调度目标包执行服务(经由本地分发中枢统一准备与复核)
995
+ return this.dispatchLocalInvocation(targetRuntime, targetActionId, input, rootContext);
996
+ }
997
+ /**
998
+ * 私有本地 Action 调度中枢。
999
+ * 统一处理本地已授权目标的准备(若已注入 prepareLocalPackage 且存在物理包根路径)、
1000
+ * 中断与关闭复核,并分发至目标 Runtime 的 startInvocation。
1001
+ */
1002
+ async dispatchLocalInvocation(targetRuntime, targetActionId, input, context) {
1003
+ if (this.isClosed) {
1004
+ const runId = context.runId;
1005
+ const error = buildRuntimeError(SERVICE_CLOSED, "ActionDockHost is closed: new tasks rejected", { package: targetRuntime.packageId, action: targetActionId, phase: "dispatch" });
1006
+ return {
1007
+ runId,
1008
+ status: "failed",
1009
+ result: Promise.resolve({ ok: false, runId, error }),
1010
+ };
1011
+ }
1012
+ const isRootCall = !context.caller;
1013
+ const initialTimeoutMs = context.initialTimeoutMs ?? (typeof context.timeoutMs === "number" && context.timeoutMs > 0 ? context.timeoutMs : undefined);
1014
+ const deadline = context.deadline ?? (initialTimeoutMs !== undefined ? Date.now() + initialTimeoutMs : undefined);
1015
+ if (context.signal.aborted) {
1016
+ const runId = context.runId;
1017
+ const isTimeout = isActionTimeoutReason(context.signal.reason);
1018
+ const error = buildRuntimeError(isTimeout ? ACTION_TIMEOUT : ACTION_CANCELLED, isTimeout
1019
+ ? `Action exceeded timeout of ${initialTimeoutMs ?? context.timeoutMs}ms`
1020
+ : "Action execution was cancelled before starting", { package: targetRuntime.packageId, action: targetActionId, phase: "dispatch" });
1021
+ return {
1022
+ runId,
1023
+ status: "failed",
1024
+ result: Promise.resolve({ ok: false, runId, error }),
1025
+ };
1026
+ }
1027
+ let timeoutController;
1028
+ let prepTimeoutTimer;
1029
+ if (isRootCall && typeof context.timeoutMs === "number" && context.timeoutMs > 0) {
1030
+ timeoutController = new AbortController();
1031
+ const remaining = deadline ? Math.max(0, deadline - Date.now()) : context.timeoutMs;
1032
+ const timeoutReason = new ActionDockError(ACTION_TIMEOUT, `Action exceeded timeout of ${initialTimeoutMs ?? context.timeoutMs}ms`, { package: targetRuntime.packageId, action: targetActionId, phase: "preparation" });
1033
+ if (remaining <= 0) {
1034
+ timeoutController.abort(timeoutReason);
1035
+ }
1036
+ else {
1037
+ prepTimeoutTimer = setTimeout(() => {
1038
+ timeoutController?.abort(timeoutReason);
1039
+ }, remaining);
1040
+ }
1041
+ }
1042
+ const signalSources = [context.signal, this.closeController.signal];
1043
+ if (timeoutController) {
1044
+ signalSources.push(timeoutController.signal);
1045
+ }
1046
+ const mergedSignal = AbortSignal.any(signalSources);
1047
+ if (this.prepareLocalPackage && targetRuntime.packageRoot) {
1048
+ const prepPromise = Promise.resolve().then(() => this.prepareLocalPackage({
1049
+ identity: targetRuntime.identity,
1050
+ packageRoot: targetRuntime.packageRoot,
1051
+ }, {
1052
+ signal: mergedSignal,
1053
+ rootRunId: context.rootRunId,
1054
+ }));
1055
+ this.activePreparations.add(prepPromise);
1056
+ try {
1057
+ await prepPromise;
1058
+ }
1059
+ catch (err) {
1060
+ if (prepTimeoutTimer !== undefined) {
1061
+ clearTimeout(prepTimeoutTimer);
1062
+ prepTimeoutTimer = undefined;
1063
+ }
1064
+ const errCode = err?.code;
1065
+ const isClosed = this.closeController.signal.aborted;
1066
+ const isExternalCancelled = context.signal.aborted;
1067
+ const isTimeout = timeoutController?.signal.aborted ||
1068
+ isActionTimeoutReason(context.signal.reason) ||
1069
+ errCode === ACTION_TIMEOUT ||
1070
+ (err?.details && err.details.code === ACTION_TIMEOUT);
1071
+ let code;
1072
+ let message = err?.message || "Package preparation failed";
1073
+ if (isTimeout) {
1074
+ code = ACTION_TIMEOUT;
1075
+ message = (err?.message && err.message.includes("exceeded timeout"))
1076
+ ? err.message
1077
+ : `Action exceeded timeout of ${initialTimeoutMs ?? context.timeoutMs}ms`;
1078
+ }
1079
+ else if (err instanceof ActionDockError) {
1080
+ code = err.code;
1081
+ }
1082
+ else if (isClosed) {
1083
+ code = SERVICE_CLOSED;
1084
+ message = "ActionDockHost is closed: new tasks rejected";
1085
+ }
1086
+ else if (isExternalCancelled) {
1087
+ code = ACTION_CANCELLED;
1088
+ message = "Action execution was cancelled";
1089
+ }
1090
+ else if (err?.code && typeof err.code === "string") {
1091
+ code = err.code;
1092
+ }
1093
+ else {
1094
+ code = EXECUTION_FAILED;
1095
+ }
1096
+ const hint = err?.details?.hint ?? err?.hint;
1097
+ const details = {
1098
+ ...(err?.details || {}),
1099
+ package: targetRuntime.packageId,
1100
+ action: targetActionId,
1101
+ phase: "preparation",
1102
+ ...(hint ? { hint } : {}),
1103
+ };
1104
+ const runId = context.runId;
1105
+ const error = buildRuntimeError(code, message, details);
1106
+ return {
1107
+ runId,
1108
+ status: "failed",
1109
+ result: Promise.resolve({ ok: false, runId, error }),
1110
+ };
1111
+ }
1112
+ finally {
1113
+ this.activePreparations.delete(prepPromise);
1114
+ }
1115
+ }
1116
+ if (prepTimeoutTimer !== undefined) {
1117
+ clearTimeout(prepTimeoutTimer);
1118
+ prepTimeoutTimer = undefined;
1119
+ }
1120
+ // 准备后再次复核取消与 Host 关闭状态
1121
+ if (this.isClosed || this.closeController.signal.aborted) {
1122
+ const runId = context.runId;
1123
+ const error = buildRuntimeError(SERVICE_CLOSED, "ActionDockHost is closed: new tasks rejected", { package: targetRuntime.packageId, action: targetActionId, phase: "post-preparation" });
1124
+ return {
1125
+ runId,
1126
+ status: "failed",
1127
+ result: Promise.resolve({ ok: false, runId, error }),
1128
+ };
1129
+ }
1130
+ if (context.signal.aborted) {
1131
+ const runId = context.runId;
1132
+ const isTimeout = isActionTimeoutReason(context.signal.reason);
1133
+ const error = buildRuntimeError(isTimeout ? ACTION_TIMEOUT : ACTION_CANCELLED, isTimeout
1134
+ ? `Action exceeded timeout of ${initialTimeoutMs ?? context.timeoutMs}ms`
1135
+ : "Action execution was cancelled after preparation", { package: targetRuntime.packageId, action: targetActionId, phase: "post-preparation" });
1136
+ return {
1137
+ runId,
1138
+ status: "failed",
1139
+ result: Promise.resolve({ ok: false, runId, error }),
1140
+ };
1141
+ }
1142
+ const remainingBudget = deadline !== undefined
1143
+ ? Math.max(0, deadline - Date.now())
1144
+ : context.timeoutMs;
1145
+ if (deadline !== undefined && typeof remainingBudget === "number" && remainingBudget <= 0) {
1146
+ const runId = context.runId;
1147
+ const error = buildRuntimeError(ACTION_TIMEOUT, `Action exceeded timeout of ${initialTimeoutMs ?? context.timeoutMs}ms`, { package: targetRuntime.packageId, action: targetActionId, phase: "post-preparation" });
1148
+ return {
1149
+ runId,
1150
+ status: "failed",
1151
+ result: Promise.resolve({ ok: false, runId, error }),
1152
+ };
1153
+ }
1154
+ const effectiveContext = {
1155
+ ...context,
1156
+ signal: mergedSignal,
1157
+ initialTimeoutMs,
1158
+ deadline,
1159
+ timeoutMs: remainingBudget,
1160
+ };
1161
+ const ticket = await targetRuntime.startInvocation(targetActionId, input, effectiveContext);
1162
+ if (ticket.result) {
1163
+ ticket.result.finally(() => {
1164
+ if (prepTimeoutTimer !== undefined) {
1165
+ clearTimeout(prepTimeoutTimer);
1166
+ }
1167
+ });
1168
+ }
1169
+ return ticket;
861
1170
  }
862
1171
  async getRun(runId, signal) {
863
1172
  await this.ensureInitialized();
@@ -1227,31 +1536,47 @@ export class DefaultActionDockHost {
1227
1536
  }) ||
1228
1537
  Array.from(this.runtimes.values()).some((rt) => Boolean(rt.inMemory || rt.options?.inMemory));
1229
1538
  const inMemory = Boolean(this.options.inMemory || hasInMemoryPackage);
1539
+ // 旧版本全局库迁移的声明并集:取首次打开时已加载包的声明字典;
1540
+ // 后续 registerRuntime 带来的新声明属当前版本正常写入,不重跑历史迁移
1541
+ const configDeclarations = this.listRuntimes()
1542
+ .map((runtime) => runtime.projectConfig?.config)
1543
+ .filter((config) => Boolean(config && Object.keys(config).length > 0));
1230
1544
  this.globalStorage =
1231
1545
  this.options.platform?.storage?.createGlobalStorage?.({
1232
1546
  dataDir: this.options.dataDir,
1233
1547
  customHome: this.options.customHome,
1234
1548
  ...(inMemory ? { inMemory: true } : {}),
1549
+ configDeclarations,
1235
1550
  }) ??
1236
1551
  createGlobalStorage({
1237
1552
  dataDir: this.options.dataDir,
1238
1553
  customHome: this.options.customHome,
1239
1554
  inMemory,
1555
+ configDeclarations,
1240
1556
  });
1241
1557
  }
1242
1558
  return this.globalStorage;
1243
1559
  }
1244
- findDeclaredConfigItem(key) {
1245
- let foundItem;
1560
+ findAllDeclaredConfigItems(key) {
1561
+ const items = [];
1246
1562
  for (const runtime of this.listRuntimes()) {
1247
- const item = runtime.projectConfig?.config?.[key];
1248
- if (item) {
1249
- if (item.secret)
1250
- return item;
1251
- foundItem = item;
1563
+ const decl = resolveConfigDeclaration(runtime.projectConfig?.config, key);
1564
+ if (decl?.item) {
1565
+ items.push(decl.item);
1252
1566
  }
1253
1567
  }
1254
- return foundItem;
1568
+ return items;
1569
+ }
1570
+ protectGlobalConfigView(view) {
1571
+ if (view.source !== "global")
1572
+ return view;
1573
+ const items = this.findAllDeclaredConfigItems(view.key);
1574
+ if (items.length === 0)
1575
+ return view;
1576
+ // 外部注入的运行时可能持有独立全局库;只能加强它返回的安全值,不能换读 Host 的库。
1577
+ const merged = redactConfigValueForDeclarations(view.value, items);
1578
+ const safe = redactConfigValueAtPaths(merged.value, [...view.redactedPaths, ...merged.redactedPaths]);
1579
+ return { ...view, ...safe, secret: view.secret || merged.secret };
1255
1580
  }
1256
1581
  resolveRuntime(packageId) {
1257
1582
  if (packageId) {
@@ -1272,32 +1597,43 @@ export class DefaultActionDockHost {
1272
1597
  }
1273
1598
  async getConfig(packageId, key) {
1274
1599
  await this.ensureInitialized();
1600
+ assertConcreteConfigKey(key);
1275
1601
  if (packageId === "global") {
1276
1602
  const globalStorage = this.getGlobalStorage();
1277
- const val = globalStorage.getConfig(key);
1278
- const configured = val !== undefined;
1279
- const declaredItem = this.findDeclaredConfigItem(key);
1280
- const isSecret = isSecretConfigKey(key, declaredItem);
1603
+ // 原子快照配对读取值与历史保护声明,消除并发删除造成的值与规则错配
1604
+ const snapshot = readConfigWithProtection(globalStorage, key);
1605
+ const configured = snapshot !== undefined;
1606
+ const items = this.findAllDeclaredConfigItems(key);
1607
+ const redacted = redactConfigValueWithProtection(snapshot?.value, items, snapshot?.protection);
1281
1608
  return {
1282
1609
  key,
1283
1610
  configured,
1284
- secret: isSecret,
1285
- source: configured ? "global" : "default",
1286
- value: !isSecret && configured ? val : undefined,
1611
+ secret: redacted.secret,
1612
+ source: configured ? "global" : null,
1613
+ value: redacted.value,
1614
+ redactedPaths: redacted.redactedPaths,
1287
1615
  };
1288
1616
  }
1289
- return this.requireRuntime(packageId).getConfig(key);
1617
+ return this.protectGlobalConfigView(await this.requireRuntime(packageId).getConfig(key));
1290
1618
  }
1291
1619
  async setConfig(packageId, key, value) {
1292
1620
  await this.ensureInitialized();
1621
+ assertConcreteConfigKey(key);
1293
1622
  if (packageId === "global") {
1294
- await this.getGlobalStorage().setConfig(key, value);
1623
+ const items = this.findAllDeclaredConfigItems(key);
1624
+ for (const item of items) {
1625
+ validateConfigValue(key, value, item);
1626
+ }
1627
+ // 传递全部已加载包各自解析出的有效声明并集(各包内具体键优先于模板),
1628
+ // 由存储统一判定整键加密并合并历史保护
1629
+ await this.getGlobalStorage().setConfig(key, value, items.length > 0 ? { declarations: items } : undefined);
1295
1630
  return;
1296
1631
  }
1297
1632
  await this.requireRuntime(packageId).setConfig(key, value);
1298
1633
  }
1299
1634
  async deleteConfig(packageId, key) {
1300
1635
  await this.ensureInitialized();
1636
+ assertConcreteConfigKey(key);
1301
1637
  if (packageId === "global") {
1302
1638
  return await this.getGlobalStorage().deleteConfig(key);
1303
1639
  }
@@ -1308,19 +1644,32 @@ export class DefaultActionDockHost {
1308
1644
  if (packageId === "global") {
1309
1645
  const globalStorage = this.getGlobalStorage();
1310
1646
  const all = globalStorage.listConfig();
1311
- return Object.entries(all).map(([key, val]) => {
1312
- const declaredItem = this.findDeclaredConfigItem(key);
1313
- const isSecret = isSecretConfigKey(key, declaredItem);
1314
- return {
1647
+ const views = [];
1648
+ for (const key of Object.keys(all)) {
1649
+ if (isConfigTemplate(key)) {
1650
+ continue;
1651
+ }
1652
+ // 同源原子重读:值与历史保护声明取自同一次读取;
1653
+ // 行在列键之后又被并发删除时跳过该项,不回退旧值
1654
+ const snapshot = readConfigWithProtection(globalStorage, key);
1655
+ if (snapshot === undefined) {
1656
+ continue;
1657
+ }
1658
+ const items = this.findAllDeclaredConfigItems(key);
1659
+ const redacted = redactConfigValueWithProtection(snapshot.value, items, snapshot.protection);
1660
+ views.push({
1315
1661
  key,
1316
1662
  configured: true,
1317
- secret: isSecret,
1663
+ secret: redacted.secret,
1318
1664
  source: "global",
1319
- value: isSecret ? undefined : val,
1320
- };
1321
- });
1665
+ value: redacted.value,
1666
+ redactedPaths: redacted.redactedPaths,
1667
+ });
1668
+ }
1669
+ return views;
1322
1670
  }
1323
- return this.requireRuntime(packageId).listConfig();
1671
+ const views = await this.requireRuntime(packageId).listConfig();
1672
+ return views.map((view) => this.protectGlobalConfigView(view));
1324
1673
  }
1325
1674
  async getState(packageId, actionId, key, options) {
1326
1675
  await this.ensureInitialized();
@@ -1365,33 +1714,48 @@ export class DefaultActionDockHost {
1365
1714
  * 外部传入借用的 Runtime 实例生命周期完全由调用方负责管理,Host 关闭时仅解绑引用并清理自身内部实例。
1366
1715
  */
1367
1716
  async close(options) {
1717
+ if (this.closePromise)
1718
+ return this.closePromise;
1368
1719
  if (this.isClosed)
1369
1720
  return;
1370
- this.isClosed = true;
1371
- const allRuntimes = Array.from(this.runtimes.values());
1372
- await Promise.all(allRuntimes.map(async (runtime) => {
1721
+ this.closePromise = (async () => {
1722
+ this.isClosed = true;
1723
+ this.closeController.abort(new ActionDockError(SERVICE_CLOSED, "ActionDockHost is closing"));
1724
+ if (this.activePreparations.size > 0) {
1725
+ await Promise.allSettled(Array.from(this.activePreparations));
1726
+ }
1727
+ const allRuntimes = Array.from(this.runtimes.values());
1728
+ await Promise.all(allRuntimes.map(async (runtime) => {
1729
+ try {
1730
+ await runtime.close(options);
1731
+ }
1732
+ catch {
1733
+ // 忽略 Runtime 关闭异常,确保全部安全释放
1734
+ }
1735
+ }));
1736
+ const invocationCloseErrors = await this.closeInvocationTargets();
1737
+ this.runtimes.clear();
1738
+ this.actionBindings.clear();
1739
+ this.remoteTargets.clear();
1740
+ await this.remoteInvoker.close();
1373
1741
  try {
1374
- await runtime.close(options);
1742
+ this.globalStorage?.close();
1375
1743
  }
1376
1744
  catch {
1377
- // 忽略 Runtime 关闭异常,确保全部安全释放
1745
+ // 忽略全局存储关闭异常
1378
1746
  }
1379
- }));
1380
- this.runtimes.clear();
1381
- try {
1382
- this.globalStorage?.close();
1383
- }
1384
- catch {
1385
- // 忽略全局存储关闭异常
1386
- }
1387
- this.globalStorage = undefined;
1388
- try {
1389
- this.dataDirLock?.release();
1390
- }
1391
- catch {
1392
- // 忽略目录排他锁释放异常
1393
- }
1394
- this.dataDirLock = undefined;
1747
+ this.globalStorage = undefined;
1748
+ try {
1749
+ this.dataDirLock?.release();
1750
+ }
1751
+ catch {
1752
+ // 忽略目录排他锁释放异常
1753
+ }
1754
+ this.dataDirLock = undefined;
1755
+ if (invocationCloseErrors.length)
1756
+ throw new AggregateError(invocationCloseErrors, "Invocation target cleanup failed");
1757
+ })();
1758
+ return this.closePromise;
1395
1759
  }
1396
1760
  }
1397
1761
  /**