@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
@@ -2,7 +2,7 @@ import { inputConflict, inputLimitExceeded, inputPathConflict, invalidFlatArgume
2
2
  import { isFlatPathPropertyName, isForbiddenActionInputPropertyName, } from "./flat-predicates.js";
3
3
  import { decodeFlatInput } from "./flat-decode.js";
4
4
  import { assertTopLevelLeafAvailable, assertMaterializedInputWithinLimits, } from "./flat-materializer.js";
5
- import { validateJsonValue, DEFAULT_MAX_JSON_DEPTH, } from "../value-validator.js";
5
+ import { validateJsonValue, hasDangerousKeys, DEFAULT_MAX_JSON_DEPTH, } from "../value-validator.js";
6
6
  import { scanJsonDepth } from "./json-depth-scanner.js";
7
7
  import { decodeUtf8Strict, stripBom } from "./utf8.js";
8
8
  import { readRegularFileBounded, DEFAULT_MAX_INPUT_BYTES, } from "./file-input.js";
@@ -203,8 +203,29 @@ export function parseJson(text, source, policy) {
203
203
  }
204
204
  return parsed;
205
205
  }
206
+ function checkValueProperties(value) {
207
+ if (hasDangerousKeys(value)) {
208
+ throw invalidJson("Input value contains prohibited prototype pollution properties", { reason: "FORBIDDEN_PROPERTY" });
209
+ }
210
+ return value;
211
+ }
206
212
  /**
207
- * 统一解析 Action 执行入参。
213
+ * 解析一个完整值:合法 JSON 解码为对应类型,非 JSON 文本保留为字符串。
214
+ * 只有语法错误允许回退;大小、深度、有限数和危险属性校验失败必须透传。
215
+ */
216
+ export function parseValueLiteral(text, policy) {
217
+ try {
218
+ return checkValueProperties(parseJson(text, "value", policy));
219
+ }
220
+ catch (error) {
221
+ if (error instanceof InputError && error.code === "INVALID_JSON" && !Array.isArray(error.details) && error.details?.reason === "SYNTAX_ERROR") {
222
+ return text;
223
+ }
224
+ throw policy?.sanitizeInputErrors ? sanitizeError(error, "inline-json") : error;
225
+ }
226
+ }
227
+ /**
228
+ * 统一解析 JSON 值及结构化输入,不承担 Action、配置或状态的业务校验。
208
229
  *
209
230
  * 输入模式仲裁规则(Input Mode Arbitration):
210
231
  * - effectiveFlat: flatArgs 存在且 length > 0
@@ -221,16 +242,16 @@ export function parseJson(text, source, policy) {
221
242
  * - stdinField 的 stdin 读取为原始文本绑定:不做 JSON 解析、不剥 BOM、保留首尾空白,空 stdin 得到空字符串。
222
243
  *
223
244
  * @param options 输入解析选项
224
- * @returns 解析后的 JSON 对象
245
+ * @returns 解析后的 JSON 值
225
246
  */
226
- export async function resolveActionInput(options) {
247
+ export async function resolveInput(options) {
227
248
  const effectiveFlat = (options.flatArgs?.length ?? 0) > 0;
228
249
  const hasInlineJson = options.input !== undefined;
229
250
  const hasFileJson = options.inputFile !== undefined;
230
251
  const hasStdinField = options.stdinField !== undefined;
231
252
  // stdin 字段绑定与完整 JSON 来源(内联 / 文件 / stdin JSON)严格互斥
232
253
  if (hasStdinField && (hasInlineJson || hasFileJson)) {
233
- const err = inputConflict("Input options conflict: --stdin-field binds raw stdin text to an action input field and cannot be combined with inline JSON (-i/--input) or file input (-f/--input-file).", {
254
+ const err = inputConflict("Input options conflict: --stdin-field binds raw stdin text to a top-level field and cannot be combined with inline JSON (-i/--input) or file input (-f/--input-file).", {
234
255
  reason: "MULTIPLE_INPUT_MODES",
235
256
  hasFlatArgs: effectiveFlat,
236
257
  hasInput: hasInlineJson,
@@ -371,3 +392,50 @@ export async function resolveActionInput(options) {
371
392
  }
372
393
  return {};
373
394
  }
395
+ /** Action 入参复用同一解码器,避免各命令的扁平路径与 stdin 绑定语义漂移。 */
396
+ export const resolveActionInput = resolveInput;
397
+ export function hasValueInput(input) {
398
+ return input.value !== undefined || input.flatArgs !== undefined || Boolean(input.fromStdin) || input.stdinField !== undefined;
399
+ }
400
+ /** 先仲裁来源再读流,配置、状态及独立运行入口使用同一完整值解码规则。 */
401
+ export async function resolveValueInput(options) {
402
+ options.signal?.throwIfAborted();
403
+ const wholeModes = Number(options.value !== undefined) + Number(Boolean(options.fromStdin));
404
+ const structured = options.flatArgs !== undefined || options.stdinField !== undefined;
405
+ if (wholeModes + Number(structured) > 1) {
406
+ throw inputConflict("Complete value, --stdin, and structured input are mutually exclusive; --stdin-field may only be combined with flat assignments.", { reason: "MULTIPLE_INPUT_MODES" });
407
+ }
408
+ if (structured) {
409
+ if (options.stdinField === undefined && options.flatArgs?.length === 0) {
410
+ throw invalidFlatArgument("At least one flat assignment is required after '--'.", { reason: "MISSING_ASSIGNMENT" });
411
+ }
412
+ const value = checkValueProperties(await resolveInput(options));
413
+ options.signal?.throwIfAborted();
414
+ return value;
415
+ }
416
+ let text = options.value;
417
+ if (options.fromStdin) {
418
+ try {
419
+ text = await readStdinBounded(options.stdin || process.stdin, {
420
+ maxInputBytes: options.policy?.maxInputBytes ?? DEFAULT_MAX_INPUT_BYTES,
421
+ signal: options.signal,
422
+ strictUtf8: options.policy?.strictUtf8 ?? false,
423
+ rawText: true,
424
+ byteStreamOnly: options.policy?.byteStreamOnly,
425
+ });
426
+ // 完整 stdin 值保留原配置命令的末尾换行裁剪;字段绑定则原样保留文本。
427
+ text = text.replace(/[\r\n]+$/, "");
428
+ }
429
+ catch (error) {
430
+ throw options.policy?.sanitizeInputErrors ? sanitizeError(error, "stdin") : error;
431
+ }
432
+ if (text.length === 0)
433
+ throw invalidFlatArgument("No data received from standard input", { reason: "MISSING_VALUE" });
434
+ }
435
+ if (text === undefined) {
436
+ throw invalidFlatArgument("A complete value, flat assignments, --stdin, or --stdin-field is required.", { reason: "MISSING_VALUE" });
437
+ }
438
+ const value = parseValueLiteral(text, options.policy);
439
+ options.signal?.throwIfAborted();
440
+ return value;
441
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 动作绑定目标描述。
3
+ */
4
+ export type ActionBindingTarget = {
5
+ type: "local";
6
+ } | {
7
+ type: "remote";
8
+ profile: string;
9
+ };
10
+ /**
11
+ * 动作命名绑定字典单一事实源类型。
12
+ */
13
+ export type ActionBindings = Readonly<Record<string, Readonly<Record<string, ActionBindingTarget>>>>;
14
+ /** 绑定文件名与目录常量 */
15
+ export declare const BINDINGS_DIR_NAME = ".actiondock";
16
+ export declare const BINDINGS_FILE_NAME = "bindings.json";
17
+ /** 绑定名称正则约束(稳定简单标识符) */
18
+ export declare const ACTION_BINDING_NAME_REGEX: RegExp;
19
+ /** 环境配置名称正则约束 */
20
+ export declare const PROFILE_NAME_REGEX: RegExp;
21
+ /**
22
+ * 校验并规范化完全限定的动作引用字符串。
23
+ * 铁律:必须为完全限定包与动作,禁止通配符、路径逃逸、空白与冒号。
24
+ */
25
+ export declare function validateCanonicalActionRef(ref: unknown): string;
26
+ /**
27
+ * 校验绑定名称合法性。
28
+ */
29
+ export declare function validateBindingName(name: unknown, actionRef?: string): string;
30
+ /**
31
+ * 校验单个绑定目标对象的合法性并返回深拷贝冻结快照。
32
+ */
33
+ export declare function validateBindingTarget(target: unknown, bindingName: string, actionRef: string): ActionBindingTarget;
34
+ /**
35
+ * 校验并深拷贝冻结动作绑定配置快照。
36
+ */
37
+ export declare function validateActionBindings(value: unknown): ActionBindings;
38
+ /**
39
+ * 从调用方项目目录读取动作绑定配置。
40
+ * 若文件不存在则返回空冻结映射,其余 IO 或 JSON 异常统一转换为 ActionDockError 透传。
41
+ */
42
+ export declare function loadActionBindings(projectRoot: string): ActionBindings;
43
+ /**
44
+ * 解析目标动作引用在当前项目中的具体绑定。
45
+ * 未显式传入 binding 则仅检索 default;若 default 亦不存在则返回 undefined。
46
+ * 若显式传入的 binding 不存在,抛出 ActionDockError(NOT_FOUND) 并在 details 中携带 action 与 binding。
47
+ */
48
+ export declare function resolveActionBinding(bindings: ActionBindings, ref: string, binding?: string): ActionBindingTarget | undefined;
49
+ /**
50
+ * 检测指定动作是否存在任意 remote 类型的命名绑定。
51
+ * 仅做完全限定精确匹配,供构建工具快速识别可外置的跨节点依赖。
52
+ */
53
+ export declare function hasRemoteActionBinding(bindings: ActionBindings, ref: string): boolean;
@@ -0,0 +1,248 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ActionDockError, INVALID_ACTION_REF, INVALID_ARGUMENT, INVALID_JSON, NOT_FOUND, } from "../errors.js";
4
+ import { parseActionRef } from "../catalog/resolve-action.js";
5
+ import { PACKAGE_ID_REGEX } from "../utils/index.js";
6
+ import { ACTION_ID_REGEX } from "../project/manifest.js";
7
+ /** 绑定文件名与目录常量 */
8
+ export const BINDINGS_DIR_NAME = ".actiondock";
9
+ export const BINDINGS_FILE_NAME = "bindings.json";
10
+ /** 绑定名称正则约束(稳定简单标识符) */
11
+ export const ACTION_BINDING_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
12
+ /** 环境配置名称正则约束 */
13
+ export const PROFILE_NAME_REGEX = /^[a-zA-Z0-9_\-\.]+$/;
14
+ /** 空冻结绑定快照单一事实源 */
15
+ const EMPTY_ACTION_BINDINGS = Object.freeze(Object.create(null));
16
+ /**
17
+ * 校验并规范化完全限定的动作引用字符串。
18
+ * 铁律:必须为完全限定包与动作,禁止通配符、路径逃逸、空白与冒号。
19
+ */
20
+ export function validateCanonicalActionRef(ref) {
21
+ if (typeof ref !== "string") {
22
+ throw new ActionDockError(INVALID_ACTION_REF, "Invalid action reference: must be a string");
23
+ }
24
+ if (ref.length === 0 || /\s/.test(ref)) {
25
+ throw new ActionDockError(INVALID_ACTION_REF, `Invalid action reference '${ref}': cannot be empty or contain whitespace`);
26
+ }
27
+ if (ref.includes(":")) {
28
+ throw new ActionDockError(INVALID_ACTION_REF, `Invalid action reference '${ref}': colon syntax is not permitted, use '/'`);
29
+ }
30
+ if (ref.includes("*") || ref.includes("?")) {
31
+ throw new ActionDockError(INVALID_ACTION_REF, `Invalid action reference '${ref}': wildcards are not permitted`);
32
+ }
33
+ if (ref.includes("..")) {
34
+ throw new ActionDockError(INVALID_ACTION_REF, `Invalid action reference '${ref}': path traversal is not permitted`);
35
+ }
36
+ if (!ref.includes("/")) {
37
+ throw new ActionDockError(INVALID_ACTION_REF, `Invalid action reference '${ref}': must be fully qualified with package and action (e.g. 'pkg/action')`);
38
+ }
39
+ const parsed = parseActionRef(ref);
40
+ if (!parsed.packageId || !parsed.actionId) {
41
+ throw new ActionDockError(INVALID_ACTION_REF, `Invalid action reference '${ref}': must be fully qualified with package and action`);
42
+ }
43
+ const isScoped = /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(parsed.packageId);
44
+ if (!PACKAGE_ID_REGEX.test(parsed.packageId) && !isScoped) {
45
+ throw new ActionDockError(INVALID_ACTION_REF, `Invalid package ID '${parsed.packageId}' in action reference '${ref}'`);
46
+ }
47
+ if (!ACTION_ID_REGEX.test(parsed.actionId)) {
48
+ throw new ActionDockError(INVALID_ACTION_REF, `Invalid action ID '${parsed.actionId}' in action reference '${ref}'`);
49
+ }
50
+ return `${parsed.packageId}/${parsed.actionId}`;
51
+ }
52
+ /**
53
+ * 校验绑定名称合法性。
54
+ */
55
+ export function validateBindingName(name, actionRef) {
56
+ if (typeof name !== "string") {
57
+ throw new ActionDockError(INVALID_ARGUMENT, "Invalid binding name: must be a string");
58
+ }
59
+ if (name.length === 0 || /\s/.test(name)) {
60
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid binding name '${name}': cannot be empty or contain whitespace`);
61
+ }
62
+ if (name === "__proto__" || name === "constructor" || name === "prototype") {
63
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid binding name '${name}': prototype property names are forbidden`);
64
+ }
65
+ if (!ACTION_BINDING_NAME_REGEX.test(name)) {
66
+ const actionMsg = actionRef ? ` for action '${actionRef}'` : "";
67
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid binding name '${name}'${actionMsg}: must match ${ACTION_BINDING_NAME_REGEX}`);
68
+ }
69
+ return name;
70
+ }
71
+ /**
72
+ * 校验单个绑定目标对象的合法性并返回深拷贝冻结快照。
73
+ */
74
+ export function validateBindingTarget(target, bindingName, actionRef) {
75
+ if (target === null || typeof target !== "object" || Array.isArray(target)) {
76
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid binding target for action '${actionRef}' binding '${bindingName}': expected a JSON object`);
77
+ }
78
+ if (!Object.prototype.hasOwnProperty.call(target, "type")) {
79
+ throw new ActionDockError(INVALID_ARGUMENT, `Missing 'type' in binding '${bindingName}' for action '${actionRef}'`);
80
+ }
81
+ const keys = Object.keys(target);
82
+ const type = target.type;
83
+ if (type === "local") {
84
+ for (const key of keys) {
85
+ if (!Object.prototype.hasOwnProperty.call(target, key))
86
+ continue;
87
+ if (key !== "type") {
88
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid property '${key}' in local binding '${bindingName}' for action '${actionRef}': local targets do not permit extra properties`);
89
+ }
90
+ }
91
+ return Object.freeze({ type: "local" });
92
+ }
93
+ if (type === "remote") {
94
+ for (const key of keys) {
95
+ if (!Object.prototype.hasOwnProperty.call(target, key))
96
+ continue;
97
+ if (key !== "type" && key !== "profile") {
98
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid property '${key}' in remote binding '${bindingName}' for action '${actionRef}': remote targets only permit 'type' and 'profile'`);
99
+ }
100
+ }
101
+ if (!Object.prototype.hasOwnProperty.call(target, "profile")) {
102
+ throw new ActionDockError(INVALID_ARGUMENT, `Missing 'profile' in remote binding '${bindingName}' for action '${actionRef}'`);
103
+ }
104
+ const profile = target.profile;
105
+ if (typeof profile !== "string" ||
106
+ profile.length === 0 ||
107
+ /\s/.test(profile) ||
108
+ !PROFILE_NAME_REGEX.test(profile)) {
109
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid profile '${String(profile)}' in remote binding '${bindingName}' for action '${actionRef}': profile must be non-empty and cannot contain whitespace`);
110
+ }
111
+ return Object.freeze({ type: "remote", profile });
112
+ }
113
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid target type '${String(type)}' in binding '${bindingName}' for action '${actionRef}': expected 'local' or 'remote'`);
114
+ }
115
+ /**
116
+ * 校验并深拷贝冻结动作绑定配置快照。
117
+ */
118
+ export function validateActionBindings(value) {
119
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
120
+ throw new ActionDockError(INVALID_ARGUMENT, "Invalid action bindings: expected a JSON object");
121
+ }
122
+ const result = Object.create(null);
123
+ for (const rawRef of Object.keys(value)) {
124
+ if (!Object.prototype.hasOwnProperty.call(value, rawRef)) {
125
+ continue;
126
+ }
127
+ const canonicalRef = validateCanonicalActionRef(rawRef);
128
+ const rawGroup = value[rawRef];
129
+ if (rawGroup === null || typeof rawGroup !== "object" || Array.isArray(rawGroup)) {
130
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid bindings group for action '${canonicalRef}': expected a JSON object`);
131
+ }
132
+ const group = Object.create(null);
133
+ for (const rawBindingName of Object.keys(rawGroup)) {
134
+ if (!Object.prototype.hasOwnProperty.call(rawGroup, rawBindingName)) {
135
+ continue;
136
+ }
137
+ const bindingName = validateBindingName(rawBindingName, canonicalRef);
138
+ const rawTarget = rawGroup[rawBindingName];
139
+ const target = validateBindingTarget(rawTarget, bindingName, canonicalRef);
140
+ group[bindingName] = target;
141
+ }
142
+ result[canonicalRef] = Object.freeze(group);
143
+ }
144
+ return Object.freeze(result);
145
+ }
146
+ /**
147
+ * 从调用方项目目录读取动作绑定配置。
148
+ * 若文件不存在则返回空冻结映射,其余 IO 或 JSON 异常统一转换为 ActionDockError 透传。
149
+ */
150
+ export function loadActionBindings(projectRoot) {
151
+ if (typeof projectRoot !== "string" || projectRoot.length === 0) {
152
+ throw new ActionDockError(INVALID_ARGUMENT, "Invalid projectRoot: must be a non-empty string");
153
+ }
154
+ const filePath = join(projectRoot, BINDINGS_DIR_NAME, BINDINGS_FILE_NAME);
155
+ let raw;
156
+ try {
157
+ raw = readFileSync(filePath, "utf-8");
158
+ }
159
+ catch (err) {
160
+ if (err?.code === "ENOENT") {
161
+ return EMPTY_ACTION_BINDINGS;
162
+ }
163
+ throw new ActionDockError(INVALID_ARGUMENT, `Failed to read action bindings at ${filePath}: ${err.message}`);
164
+ }
165
+ let parsed;
166
+ try {
167
+ parsed = JSON.parse(raw);
168
+ }
169
+ catch (err) {
170
+ throw new ActionDockError(INVALID_JSON, `Failed to parse action bindings JSON at ${filePath}: ${err.message}`);
171
+ }
172
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
173
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid action bindings format in ${filePath}: expected a JSON object`);
174
+ }
175
+ const rootKeys = Object.keys(parsed);
176
+ for (const key of rootKeys) {
177
+ if (!Object.prototype.hasOwnProperty.call(parsed, key)) {
178
+ continue;
179
+ }
180
+ if (key === "bindings") {
181
+ continue;
182
+ }
183
+ if (key === "schemaVersion") {
184
+ const v = parsed.schemaVersion;
185
+ if (v !== 1) {
186
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid schemaVersion '${String(v)}' in ${filePath}: only schemaVersion 1 is supported`);
187
+ }
188
+ continue;
189
+ }
190
+ throw new ActionDockError(INVALID_ARGUMENT, `Unknown property '${key}' in ${filePath}: only 'bindings' is allowed`);
191
+ }
192
+ if (!Object.prototype.hasOwnProperty.call(parsed, "bindings")) {
193
+ throw new ActionDockError(INVALID_ARGUMENT, `Missing 'bindings' property in ${filePath}`);
194
+ }
195
+ return validateActionBindings(parsed.bindings);
196
+ }
197
+ /**
198
+ * 解析目标动作引用在当前项目中的具体绑定。
199
+ * 未显式传入 binding 则仅检索 default;若 default 亦不存在则返回 undefined。
200
+ * 若显式传入的 binding 不存在,抛出 ActionDockError(NOT_FOUND) 并在 details 中携带 action 与 binding。
201
+ */
202
+ export function resolveActionBinding(bindings, ref, binding) {
203
+ const canonicalRef = validateCanonicalActionRef(ref);
204
+ const group = bindings &&
205
+ typeof bindings === "object" &&
206
+ Object.prototype.hasOwnProperty.call(bindings, canonicalRef)
207
+ ? bindings[canonicalRef]
208
+ : undefined;
209
+ if (binding === undefined) {
210
+ if (!group) {
211
+ return undefined;
212
+ }
213
+ return Object.prototype.hasOwnProperty.call(group, "default")
214
+ ? group["default"]
215
+ : undefined;
216
+ }
217
+ if (!group || !Object.prototype.hasOwnProperty.call(group, binding)) {
218
+ throw new ActionDockError(NOT_FOUND, `Action binding '${binding}' not found for action '${canonicalRef}'`, { action: canonicalRef, binding });
219
+ }
220
+ return group[binding];
221
+ }
222
+ /**
223
+ * 检测指定动作是否存在任意 remote 类型的命名绑定。
224
+ * 仅做完全限定精确匹配,供构建工具快速识别可外置的跨节点依赖。
225
+ */
226
+ export function hasRemoteActionBinding(bindings, ref) {
227
+ const canonicalRef = validateCanonicalActionRef(ref);
228
+ if (!bindings || typeof bindings !== "object") {
229
+ return false;
230
+ }
231
+ if (!Object.prototype.hasOwnProperty.call(bindings, canonicalRef)) {
232
+ return false;
233
+ }
234
+ const group = bindings[canonicalRef];
235
+ if (!group || typeof group !== "object") {
236
+ return false;
237
+ }
238
+ for (const name of Object.keys(group)) {
239
+ if (!Object.prototype.hasOwnProperty.call(group, name)) {
240
+ continue;
241
+ }
242
+ const target = group[name];
243
+ if (target?.type === "remote") {
244
+ return true;
245
+ }
246
+ }
247
+ return false;
248
+ }
@@ -1,5 +1,7 @@
1
1
  import { type ErrorCode } from "../errors.js";
2
2
  import type { PackageGraph } from "../catalog/graph.js";
3
+ /** 动作调用栈键的单一事实源,执行目标作用域不占用额外深度。 */
4
+ export declare function createActionCallKey(actionId: string, packageId?: string, targetScope?: string): string;
3
5
  /**
4
6
  * 调用治理策略初始化选项。
5
7
  */
@@ -32,6 +34,8 @@ export interface CallerActionInfo {
32
34
  export interface TargetActionInfo {
33
35
  packageId: string;
34
36
  actionId: string;
37
+ /** 跨执行目标时,即使同名包也必须显式声明依赖。 */
38
+ requiresExplicitUses?: boolean;
35
39
  }
36
40
  /**
37
41
  * 调用治理策略错误描述。
@@ -127,7 +131,7 @@ export declare class InvocationPolicy {
127
131
  * @param currentPackageId 当前执行发起方包标识
128
132
  * @returns 包含规范化 callKey 与可选错误的结构体
129
133
  */
130
- checkCycle(callStack: readonly string[], targetActionId: string, targetPackageId?: string, currentPackageId?: string): CycleCheckResult;
134
+ checkCycle(callStack: readonly string[], targetActionId: string, targetPackageId?: string, currentPackageId?: string, targetScope?: string): CycleCheckResult;
131
135
  /**
132
136
  * 并发活跃子任务配额检查(Sub-Run Quota Policy)。
133
137
  *
@@ -1,5 +1,10 @@
1
1
  import { ACTION_CALL_CYCLE, ACTION_SUBRUN_LIMIT, UNDECLARED_ACTION_DEPENDENCY, ActionDockError, } from "../errors.js";
2
2
  import { isUsesDeclared } from "../catalog/graph.js";
3
+ /** 动作调用栈键的单一事实源,执行目标作用域不占用额外深度。 */
4
+ export function createActionCallKey(actionId, packageId, targetScope) {
5
+ const logicalKey = packageId ? `${packageId}/${actionId}` : actionId;
6
+ return targetScope ? `${targetScope}::${logicalKey}` : logicalKey;
7
+ }
3
8
  /**
4
9
  * 全局统一调用治理策略服务(InvocationPolicy)。
5
10
  *
@@ -77,7 +82,7 @@ export class InvocationPolicy {
77
82
  */
78
83
  checkUsesAuthorization(caller, target, graph) {
79
84
  // 同包调用完全自由开放,不作限制
80
- if (!caller.packageId || !target.packageId || caller.packageId === target.packageId) {
85
+ if (!target.requiresExplicitUses && (!caller.packageId || !target.packageId || caller.packageId === target.packageId)) {
81
86
  return undefined;
82
87
  }
83
88
  const targetRef = `${target.packageId}/${target.actionId}`;
@@ -89,9 +94,12 @@ export class InvocationPolicy {
89
94
  callerNode?.manifest?.actions?.[shortActionId]?.uses ||
90
95
  callerNode?.manifest?.actions?.[caller.actionId]?.uses ||
91
96
  [];
92
- const isAllowed = graph
93
- ? graph.canCascadeCall(caller.packageId, caller.actionId, target.packageId, target.actionId)
94
- : usesList.some((u) => isUsesDeclared(u, target.packageId, target.actionId));
97
+ const isAllowed = target.requiresExplicitUses
98
+ ? usesList.some((u) => isUsesDeclared(u, target.packageId, target.actionId) ||
99
+ (caller.packageId === target.packageId && u === target.actionId))
100
+ : graph && caller.declaredUses === undefined
101
+ ? graph.canCascadeCall(caller.packageId, caller.actionId, target.packageId, target.actionId)
102
+ : usesList.some((u) => isUsesDeclared(u, target.packageId, target.actionId));
95
103
  if (!isAllowed) {
96
104
  const hint = `Hint: Add '${targetRef}' to the 'uses' array of action '${caller.actionId}' in actiondock.json, then run 'ad validate'.`;
97
105
  return {
@@ -144,11 +152,10 @@ export class InvocationPolicy {
144
152
  * @param currentPackageId 当前执行发起方包标识
145
153
  * @returns 包含规范化 callKey 与可选错误的结构体
146
154
  */
147
- checkCycle(callStack, targetActionId, targetPackageId, currentPackageId) {
148
- const isExternal = Boolean(targetPackageId && currentPackageId && targetPackageId !== currentPackageId);
149
- const callKey = targetPackageId
150
- ? `${targetPackageId}/${targetActionId}`
151
- : targetActionId;
155
+ checkCycle(callStack, targetActionId, targetPackageId, currentPackageId, targetScope) {
156
+ const isExternal = Boolean(targetScope ||
157
+ (targetPackageId && currentPackageId && targetPackageId !== currentPackageId));
158
+ const callKey = createActionCallKey(targetActionId, targetPackageId, targetScope);
152
159
  let hasCycle = false;
153
160
  if (isExternal) {
154
161
  hasCycle = callStack.includes(callKey);
@@ -0,0 +1,40 @@
1
+ import type { ActionContract, ActionRef, Logger } from "@actiondock/sdk";
2
+ import type { RemoteServiceOptions } from "../service/types.js";
3
+ import type { InvocationContext } from "./types.js";
4
+ /**
5
+ * 远端调用绑定与 Profile 元信息。
6
+ */
7
+ export interface RemoteInvocationMetadata {
8
+ /** 宿主绑定的逻辑标识 */
9
+ binding: string;
10
+ /** 目标 Profile 标识 */
11
+ profile: string;
12
+ }
13
+ /**
14
+ * 安全的远端嵌套调用适配执行器。
15
+ *
16
+ * 遵循原则:
17
+ * - 目标 target 由 Host 预先根据 profile 解析并冻结传入,不自作路由决断。
18
+ * - 连接缓存按 target 对象身份隔离复用,并在 close() 时全量释放。
19
+ * - 严格防范凭证与本地私有上下文泄漏,仅透传必要控制字段。
20
+ * - 取消与清理全链路有界且透传远端 runs.cancel,严禁静默吞没未确认终态。
21
+ */
22
+ export declare class RemoteInvocationExecutor {
23
+ private readonly logger?;
24
+ private readonly services;
25
+ private isClosed;
26
+ private readonly closeController;
27
+ private readonly pending;
28
+ constructor(options?: {
29
+ logger?: Logger;
30
+ });
31
+ /**
32
+ * 触发执行远端 Action 动作。
33
+ */
34
+ invoke(target: RemoteServiceOptions, ref: ActionRef | string, input: unknown, context: InvocationContext, metadata: RemoteInvocationMetadata, expectedContract?: ActionContract): Promise<unknown>;
35
+ private execute;
36
+ /**
37
+ * 关闭所有持有的远端服务连接并释放关联资源,对外拒绝后续调用。
38
+ */
39
+ close(): Promise<void>;
40
+ }