@actiondock/core 2.18.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/client/config.d.ts +17 -14
  2. package/dist/client/config.js +112 -14
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/client/index.js +1 -1
  5. package/dist/client/runs.d.ts +9 -0
  6. package/dist/client/runs.js +7 -0
  7. package/dist/errors.d.ts +3 -1
  8. package/dist/errors.js +2 -0
  9. package/dist/execution/service.js +8 -5
  10. package/dist/execution/types.d.ts +3 -3
  11. package/dist/host/host.d.ts +9 -1
  12. package/dist/host/host.js +266 -45
  13. package/dist/host/types.d.ts +3 -0
  14. package/dist/index.d.ts +4 -3
  15. package/dist/index.js +1 -1
  16. package/dist/input/config-override.d.ts +5 -0
  17. package/dist/input/config-override.js +23 -0
  18. package/dist/input/input-resolver.d.ts +24 -7
  19. package/dist/input/input-resolver.js +73 -5
  20. package/dist/invocation/bindings.d.ts +53 -0
  21. package/dist/invocation/bindings.js +248 -0
  22. package/dist/invocation/policy.d.ts +5 -1
  23. package/dist/invocation/policy.js +16 -9
  24. package/dist/invocation/remote.d.ts +40 -0
  25. package/dist/invocation/remote.js +489 -0
  26. package/dist/invocation/types.d.ts +2 -0
  27. package/dist/invocation/types.js +1 -0
  28. package/dist/package/index.d.ts +1 -1
  29. package/dist/package/runtime.js +105 -15
  30. package/dist/platform/types.d.ts +48 -1
  31. package/dist/project/config-declarations.d.ts +61 -0
  32. package/dist/project/config-declarations.js +297 -0
  33. package/dist/project/index.d.ts +5 -2
  34. package/dist/project/index.js +5 -2
  35. package/dist/project/init.js +2 -2
  36. package/dist/project/loader.js +4 -0
  37. package/dist/project/manifest.js +10 -0
  38. package/dist/project/types.d.ts +30 -0
  39. package/dist/runtime/context.d.ts +6 -3
  40. package/dist/runtime/context.js +60 -13
  41. package/dist/runtime/env.js +15 -1
  42. package/dist/runtime/standalone-commands.d.ts +2 -2
  43. package/dist/runtime/standalone-commands.js +113 -47
  44. package/dist/runtime/standalone-help.d.ts +1 -0
  45. package/dist/runtime/standalone-help.js +15 -9
  46. package/dist/runtime/standalone.js +2 -2
  47. package/dist/server/policy.d.ts +24 -107
  48. package/dist/server/policy.js +134 -291
  49. package/dist/server/routes/actions.js +10 -8
  50. package/dist/server/routes/common.d.ts +5 -20
  51. package/dist/server/routes/common.js +22 -58
  52. package/dist/server/routes/config.js +65 -7
  53. package/dist/server/routes/doctor.d.ts +1 -0
  54. package/dist/server/routes/doctor.js +4 -3
  55. package/dist/server/routes/info.js +100 -13
  56. package/dist/server/routes/playbooks.js +1 -3
  57. package/dist/server/routes/runs.js +112 -18
  58. package/dist/server/server.d.ts +2 -1
  59. package/dist/server/server.js +38 -55
  60. package/dist/server/types.d.ts +6 -6
  61. package/dist/service/factory.js +1 -0
  62. package/dist/service/remote.js +28 -29
  63. package/dist/service/types.d.ts +34 -5
  64. package/dist/service/types.js +8 -0
  65. package/dist/storage/mask.d.ts +33 -6
  66. package/dist/storage/mask.js +298 -16
  67. package/dist/storage/sqlite-runs.d.ts +5 -17
  68. package/dist/storage/sqlite-runs.js +127 -5
  69. package/dist/storage/sqlite.d.ts +5 -17
  70. package/dist/storage/sqlite.js +2 -2
  71. package/dist/storage/types.d.ts +31 -16
  72. package/dist/version.d.ts +1 -1
  73. package/dist/version.js +1 -1
  74. package/package.json +2 -2
@@ -0,0 +1,489 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import { ActionDockError, ACTION_CANCELLED, EXECUTION_FAILED, INVALID_ARGUMENT, NETWORK_ERROR, SERVICE_CLOSED, SERVICE_RESULT_UNKNOWN, TIMEOUT, } from "../errors.js";
4
+ import { parseActionRef } from "../catalog/resolve-action.js";
5
+ import { RemoteActionDockService } from "../service/remote.js";
6
+ /**
7
+ * 清理、反查与发现默认有界等待时限(毫秒)。
8
+ */
9
+ const DEFAULT_CLEANUP_TIMEOUT_MS = 5000;
10
+ const DEFAULT_LOOKUP_TIMEOUT_MS = 3000;
11
+ const DEFAULT_DISCOVERY_TIMEOUT_MS = 10000;
12
+ /**
13
+ * 过滤 URL 中携带的用户、密码凭据与敏感查询参数,防止泄漏至日志或异常元数据。
14
+ */
15
+ function sanitizeServerUrl(urlStr) {
16
+ try {
17
+ const parsed = new URL(urlStr);
18
+ parsed.username = "";
19
+ parsed.password = "";
20
+ if (parsed.searchParams.has("token")) {
21
+ parsed.searchParams.set("token", "[REDACTED]");
22
+ }
23
+ return parsed.origin;
24
+ }
25
+ catch {
26
+ return "[invalid-url]";
27
+ }
28
+ }
29
+ /**
30
+ * 脱敏字符串中的敏感 Token 与 URL 凭据。
31
+ */
32
+ function sanitizeString(str, token) {
33
+ let result = str;
34
+ if (token && token.length > 0) {
35
+ result = result.replaceAll(token, "[REDACTED]");
36
+ }
37
+ // 脱敏 URL 用户密码凭据 (http://user:pass@host)
38
+ result = result.replace(/(https?:\/\/)([^/@\s]+)@/g, "$1[REDACTED]@");
39
+ // 脱敏查询参数中的 token / key / secret / password
40
+ result = result.replace(/([?&](?:token|key|secret|password)=)[^&\s]+/gi, "$1[REDACTED]");
41
+ return result;
42
+ }
43
+ /**
44
+ * 格式化脱敏错误信息。
45
+ */
46
+ function sanitizeErrorMessage(msg, token) {
47
+ if (typeof msg !== "string") {
48
+ return "";
49
+ }
50
+ return sanitizeString(msg, token);
51
+ }
52
+ /**
53
+ * 深度脱敏错误元数据与原始对象,防止 Token 与 URL 凭据泄漏。
54
+ */
55
+ function sanitizeDetails(val, token) {
56
+ if (val === null || val === undefined) {
57
+ return val;
58
+ }
59
+ if (typeof val === "string") {
60
+ return sanitizeString(val, token);
61
+ }
62
+ if (val instanceof Error) {
63
+ return sanitizeString(val.message, token);
64
+ }
65
+ if (Array.isArray(val)) {
66
+ return val.map((item) => sanitizeDetails(item, token));
67
+ }
68
+ if (typeof val === "object") {
69
+ const sanitizedObj = {};
70
+ for (const [k, v] of Object.entries(val)) {
71
+ if (k.toLowerCase().includes("token") ||
72
+ k.toLowerCase().includes("secret") ||
73
+ k.toLowerCase().includes("password")) {
74
+ sanitizedObj[k] = "[REDACTED]";
75
+ }
76
+ else {
77
+ sanitizedObj[k] = sanitizeDetails(v, token);
78
+ }
79
+ }
80
+ return sanitizedObj;
81
+ }
82
+ return val;
83
+ }
84
+ /**
85
+ * 构造统一且剔除敏感凭据的错误详情信封。
86
+ */
87
+ function buildErrorDetails(action, metadata, requestId, resultUnknown, remoteRunId, extra) {
88
+ return {
89
+ action,
90
+ binding: metadata.binding,
91
+ profile: metadata.profile,
92
+ requestId,
93
+ remoteRunId,
94
+ resultUnknown,
95
+ ...extra,
96
+ };
97
+ }
98
+ /**
99
+ * 针对指定唯一 requestId 执行有界反查。
100
+ */
101
+ async function queryRunByRequestId(service, requestId, timeoutMs) {
102
+ const controller = new AbortController();
103
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
104
+ try {
105
+ const runs = await service.runs.list({ requestIds: [requestId], limit: 1 }, { signal: controller.signal });
106
+ return { record: runs && runs.length > 0 ? runs[0] : undefined };
107
+ }
108
+ catch (err) {
109
+ return { error: err };
110
+ }
111
+ finally {
112
+ clearTimeout(timer);
113
+ }
114
+ }
115
+ /**
116
+ * 针对远端任务执行有界取消调用,避免无限阻塞与定时器泄漏。
117
+ */
118
+ async function cancelRunWithBoundedTimeout(service, runId, timeoutMs) {
119
+ let timer;
120
+ try {
121
+ const res = await Promise.race([
122
+ service.runs.cancel(runId, "Parent invocation cancelled"),
123
+ new Promise((_, reject) => {
124
+ timer = setTimeout(() => reject(new Error(`Cancellation timed out after ${timeoutMs}ms`)), timeoutMs);
125
+ }),
126
+ ]);
127
+ if (res.outcome === "not_found" || res.outcome === "not_owner") {
128
+ return {
129
+ ok: false,
130
+ outcome: res.outcome,
131
+ resultUnknown: true,
132
+ error: new Error(`Cancellation failed with outcome: ${res.outcome}`),
133
+ };
134
+ }
135
+ if (res.outcome === "already_terminal") {
136
+ return {
137
+ ok: true,
138
+ outcome: "already_terminal",
139
+ resultUnknown: false,
140
+ };
141
+ }
142
+ // res.outcome === "requested"
143
+ // requested 仅表示已请求,不可宣称终态已停止,resultUnknown 要诚实
144
+ return {
145
+ ok: true,
146
+ outcome: "requested",
147
+ resultUnknown: true,
148
+ };
149
+ }
150
+ catch (err) {
151
+ return {
152
+ ok: false,
153
+ resultUnknown: true,
154
+ error: err,
155
+ };
156
+ }
157
+ finally {
158
+ if (timer !== undefined) {
159
+ clearTimeout(timer);
160
+ }
161
+ }
162
+ }
163
+ /** 等待必须响应调用方取消,不能依赖服务实现主动结束结果 Promise。 */
164
+ function waitWithSignal(operation, signal) {
165
+ if (signal.aborted)
166
+ return Promise.reject(signal.reason ?? new Error("Invocation cancelled"));
167
+ return new Promise((resolve, reject) => {
168
+ const onAbort = () => reject(signal.reason ?? new Error("Invocation cancelled"));
169
+ signal.addEventListener("abort", onAbort, { once: true });
170
+ operation.then((value) => { signal.removeEventListener("abort", onAbort); resolve(value); }, (error) => { signal.removeEventListener("abort", onAbort); reject(error); });
171
+ });
172
+ }
173
+ /**
174
+ * 安全的远端嵌套调用适配执行器。
175
+ *
176
+ * 遵循原则:
177
+ * - 目标 target 由 Host 预先根据 profile 解析并冻结传入,不自作路由决断。
178
+ * - 连接缓存按 target 对象身份隔离复用,并在 close() 时全量释放。
179
+ * - 严格防范凭证与本地私有上下文泄漏,仅透传必要控制字段。
180
+ * - 取消与清理全链路有界且透传远端 runs.cancel,严禁静默吞没未确认终态。
181
+ */
182
+ export class RemoteInvocationExecutor {
183
+ logger;
184
+ services = new Map();
185
+ isClosed = false;
186
+ closeController = new AbortController();
187
+ pending = new Set();
188
+ constructor(options) {
189
+ this.logger = options?.logger;
190
+ }
191
+ /**
192
+ * 触发执行远端 Action 动作。
193
+ */
194
+ invoke(target, ref, input, context, metadata, expectedContract) {
195
+ const operation = this.execute(target, ref, input, {
196
+ ...context,
197
+ signal: AbortSignal.any([context.signal, this.closeController.signal]),
198
+ }, metadata, expectedContract);
199
+ this.pending.add(operation);
200
+ const settled = () => { this.pending.delete(operation); };
201
+ operation.then(settled, settled);
202
+ return operation;
203
+ }
204
+ async execute(target, ref, input, context, metadata, expectedContract) {
205
+ const childRequestId = randomUUID();
206
+ let parsedRef;
207
+ try {
208
+ parsedRef = parseActionRef(ref);
209
+ }
210
+ catch {
211
+ parsedRef = typeof ref === "object" && ref !== null ? ref : { actionId: String(ref) };
212
+ }
213
+ const actionStr = parsedRef.packageId
214
+ ? `${parsedRef.packageId}/${parsedRef.actionId}`
215
+ : parsedRef.actionId;
216
+ const logger = context.logger ?? this.logger;
217
+ const sanitizedTarget = sanitizeServerUrl(target.serverUrl);
218
+ const makeDetails = (resultUnknown, remoteRunId, extra) => buildErrorDetails(actionStr, metadata, childRequestId, resultUnknown, remoteRunId, extra);
219
+ // 1. 检查执行器生命周期状态
220
+ if (this.isClosed) {
221
+ const err = new ActionDockError(SERVICE_CLOSED, "RemoteInvocationExecutor is closed", makeDetails(false, undefined));
222
+ logger?.error?.(`[RemoteInvocation] Executor closed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}`);
223
+ throw err;
224
+ }
225
+ // 2. 检查前置取消信号,阻止任何网络通信
226
+ if (context.signal.aborted) {
227
+ const err = new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled before start", makeDetails(false, undefined));
228
+ logger?.warn?.(`[RemoteInvocation] Pre-cancelled: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}`);
229
+ throw err;
230
+ }
231
+ // 3. 复用或建立 target 对应的远端服务连接
232
+ let service = this.services.get(target);
233
+ if (!service) {
234
+ if (this.isClosed) {
235
+ throw new ActionDockError(SERVICE_CLOSED, "RemoteInvocationExecutor is closed", makeDetails(false, undefined));
236
+ }
237
+ service = new RemoteActionDockService(target);
238
+ this.services.set(target, service);
239
+ }
240
+ // 4. 远端契约发现与校验(使用有界 / AbortSignal race 确保响应父级取消,避免无限等待或取消后仍继续执行)
241
+ let remoteSpec;
242
+ const discoveryTimeoutMs = Math.min(DEFAULT_DISCOVERY_TIMEOUT_MS, Math.max(1000, context.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS));
243
+ try {
244
+ if (context.signal.aborted) {
245
+ throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled before discovery", makeDetails(false, undefined));
246
+ }
247
+ let abortListener;
248
+ let discoveryTimer;
249
+ try {
250
+ remoteSpec = await Promise.race([
251
+ service.discovery.describeAction(ref),
252
+ new Promise((_, reject) => {
253
+ abortListener = () => {
254
+ reject(new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled during discovery", makeDetails(false, undefined)));
255
+ };
256
+ context.signal.addEventListener("abort", abortListener, { once: true });
257
+ discoveryTimer = setTimeout(() => {
258
+ reject(new ActionDockError(NETWORK_ERROR, `Discovery timed out after ${discoveryTimeoutMs}ms for action '${actionStr}'`, makeDetails(false, undefined)));
259
+ }, discoveryTimeoutMs);
260
+ }),
261
+ ]);
262
+ }
263
+ finally {
264
+ if (discoveryTimer !== undefined) {
265
+ clearTimeout(discoveryTimer);
266
+ }
267
+ if (abortListener !== undefined) {
268
+ context.signal.removeEventListener("abort", abortListener);
269
+ }
270
+ }
271
+ }
272
+ catch (err) {
273
+ if (context.signal.aborted) {
274
+ throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled during discovery", makeDetails(false, undefined));
275
+ }
276
+ const code = err?.code || NETWORK_ERROR;
277
+ const sanitizedMsg = sanitizeErrorMessage(err?.message, target.token);
278
+ const wrappedErr = new ActionDockError(code, `Failed to describe remote action '${actionStr}': ${sanitizedMsg}`, makeDetails(false, undefined, {
279
+ originalError: sanitizeDetails(err?.details ?? err?.message, target.token),
280
+ }));
281
+ logger?.error?.(`[RemoteInvocation] Discovery failed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, error=${sanitizedMsg}`);
282
+ throw wrappedErr;
283
+ }
284
+ if (expectedContract) {
285
+ const expectedInput = expectedContract.inputSchema ?? {};
286
+ const actualInput = remoteSpec.inputSchema ?? {};
287
+ const expectedOutput = expectedContract.outputSchema ?? {};
288
+ const actualOutput = remoteSpec.outputSchema ?? {};
289
+ if (!isDeepStrictEqual(expectedInput, actualInput) || !isDeepStrictEqual(expectedOutput, actualOutput)) {
290
+ const err = new ActionDockError(INVALID_ARGUMENT, `Remote action contract schema mismatch for action '${actionStr}'`, makeDetails(false, undefined, {
291
+ expectedInput,
292
+ actualInput,
293
+ expectedOutput,
294
+ actualOutput,
295
+ }));
296
+ logger?.error?.(`[RemoteInvocation] Contract schema mismatch: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}`);
297
+ throw err;
298
+ }
299
+ }
300
+ // 5. 校验通过,准备发起远端异步执行
301
+ if (context.signal.aborted) {
302
+ throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled before start", makeDetails(false, undefined));
303
+ }
304
+ const cleanupTimeoutMs = Math.min(DEFAULT_CLEANUP_TIMEOUT_MS, Math.max(1000, context.timeoutMs ?? DEFAULT_CLEANUP_TIMEOUT_MS));
305
+ const lookupTimeoutMs = Math.min(DEFAULT_LOOKUP_TIMEOUT_MS, Math.max(1000, context.timeoutMs ?? DEFAULT_LOOKUP_TIMEOUT_MS));
306
+ const startOptions = {
307
+ timeoutMs: context.timeoutMs,
308
+ requestId: childRequestId,
309
+ signal: context.signal,
310
+ };
311
+ let ticket;
312
+ let startError;
313
+ try {
314
+ ticket = await service.execution.start(ref, input, startOptions);
315
+ }
316
+ catch (err) {
317
+ startError = err;
318
+ }
319
+ // 检查 ticket 是否代表确定性拒绝(如鉴权失败、入参校验失败等):
320
+ // execution.start 可能返回 { status: 'failed', runId: '', result: Promise.resolve({ ok: false, error: ... }) }
321
+ // 必须先读取 failed ticket 保留确定拒绝 code,不能误判为 SERVICE_RESULT_UNKNOWN。
322
+ if (ticket && (ticket.status === "failed" || !ticket.runId)) {
323
+ if (ticket.result) {
324
+ try {
325
+ const res = await ticket.result;
326
+ if (res && !res.ok && res.error) {
327
+ const errCode = res.error.code;
328
+ const isTransportUnknown = errCode === NETWORK_ERROR ||
329
+ errCode === "ACTION_TIMEOUT" ||
330
+ errCode === "TIMEOUT" ||
331
+ errCode === ACTION_CANCELLED;
332
+ if (!isTransportUnknown) {
333
+ const sanitizedErrMsg = sanitizeErrorMessage(res.error.message, target.token) || "Remote execution rejected";
334
+ logger?.warn?.(`[RemoteInvocation] Remote start rejected deterministically: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, code=${errCode}, error=${sanitizedErrMsg}`);
335
+ throw new ActionDockError(errCode || EXECUTION_FAILED, sanitizedErrMsg, makeDetails(false, undefined, {
336
+ remoteError: sanitizeDetails(res.error.details ?? res.error, target.token),
337
+ }));
338
+ }
339
+ }
340
+ }
341
+ catch (readErr) {
342
+ if (readErr instanceof ActionDockError) {
343
+ throw readErr;
344
+ }
345
+ startError = readErr;
346
+ }
347
+ }
348
+ if (!ticket.runId) {
349
+ ticket = undefined;
350
+ }
351
+ }
352
+ // 6. 接受前中断处理(远端可能已受理执行,进行有界反查与补发取消)
353
+ if (!ticket || !ticket.runId) {
354
+ const lookupRes = await queryRunByRequestId(service, childRequestId, lookupTimeoutMs);
355
+ const foundRun = lookupRes.record;
356
+ if (foundRun) {
357
+ // 使用 RunRecord 的唯一标识 id(不可使用 runId)
358
+ const cancelRes = await cancelRunWithBoundedTimeout(service, foundRun.id, cleanupTimeoutMs);
359
+ if (!cancelRes.ok) {
360
+ const sanitizedCancelMsg = sanitizeErrorMessage(cancelRes.error?.message, target.token);
361
+ logger?.error?.(`[RemoteInvocation] Cancel cleanup failed for accepted run '${foundRun.id}': ${sanitizedCancelMsg}`);
362
+ throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Interrupted before acceptance, and cancel cleanup failed for run '${foundRun.id}': ${sanitizedCancelMsg}`, makeDetails(true, foundRun.id, {
363
+ cancelOutcome: cancelRes.outcome,
364
+ originalError: sanitizedCancelMsg,
365
+ }));
366
+ }
367
+ if (context.signal.aborted) {
368
+ throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled", makeDetails(cancelRes.resultUnknown, foundRun.id, {
369
+ cancelOutcome: cancelRes.outcome,
370
+ }));
371
+ }
372
+ const sanitizedStartMsg = sanitizeErrorMessage(startError?.message, target.token);
373
+ throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Action execution was interrupted before acceptance and cancelled on remote: ${sanitizedStartMsg}`, makeDetails(cancelRes.resultUnknown, foundRun.id, {
374
+ cancelOutcome: cancelRes.outcome,
375
+ originalError: sanitizedStartMsg,
376
+ }));
377
+ }
378
+ // 反查若发生错误,必须提供可观测错误元信息,不可宣称无记录
379
+ if (lookupRes.error) {
380
+ const sanitizedLookupMsg = sanitizeErrorMessage(lookupRes.error?.message, target.token);
381
+ const sanitizedStartMsg = sanitizeErrorMessage(startError?.message, target.token);
382
+ logger?.warn?.(`[RemoteInvocation] Interrupted before acceptance and run lookup failed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, lookupError=${sanitizedLookupMsg}`);
383
+ throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Action execution interrupted before acceptance; query run by requestId failed: ${sanitizedLookupMsg}`, makeDetails(true, undefined, {
384
+ lookupError: sanitizedLookupMsg,
385
+ originalError: sanitizedStartMsg,
386
+ }));
387
+ }
388
+ // 反查成功但无记录:未能确认终态应使用 SERVICE_RESULT_UNKNOWN
389
+ const sanitizedStartMsg = sanitizeErrorMessage(startError?.message, target.token);
390
+ logger?.warn?.(`[RemoteInvocation] Interrupted before acceptance with unknown remote status: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}`);
391
+ throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Action execution interrupted before acceptance; remote execution status is unknown: ${sanitizedStartMsg}`, makeDetails(true, undefined, {
392
+ lookupRecordFound: false,
393
+ originalError: sanitizedStartMsg,
394
+ }));
395
+ }
396
+ // 7. 远端已成功接受调用并返回 runId
397
+ const remoteRunId = ticket.runId;
398
+ logger?.info(`[RemoteInvocation] Accepted: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}`);
399
+ // 显式检查 ticket.result 是否存在,缺失应抛出 EXECUTION_FAILED 且不可触发 TypeError
400
+ if (!ticket.result) {
401
+ const err = new ActionDockError(EXECUTION_FAILED, `Execution ticket missing result Promise for remote run '${remoteRunId}'`, makeDetails(true, remoteRunId));
402
+ logger?.error?.(`[RemoteInvocation] Missing ticket.result: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}`);
403
+ throw err;
404
+ }
405
+ // 防止未处理的 Promise Rejection
406
+ ticket.result.catch(() => { });
407
+ let cancelPromise;
408
+ const abortHandler = () => {
409
+ cancelPromise = cancelRunWithBoundedTimeout(service, remoteRunId, cleanupTimeoutMs);
410
+ };
411
+ if (context.signal.aborted) {
412
+ abortHandler();
413
+ }
414
+ else {
415
+ context.signal.addEventListener("abort", abortHandler, { once: true });
416
+ }
417
+ try {
418
+ let execResult;
419
+ try {
420
+ execResult = await waitWithSignal(ticket.result, context.signal);
421
+ }
422
+ catch (waitErr) {
423
+ if (context.signal.aborted) {
424
+ const cancelRes = await (cancelPromise ?? cancelRunWithBoundedTimeout(service, remoteRunId, cleanupTimeoutMs));
425
+ if (!cancelRes.ok) {
426
+ const sanitizedCancelMsg = sanitizeErrorMessage(cancelRes.error?.message, target.token);
427
+ logger?.error?.(`[RemoteInvocation] Cancellation cleanup failed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, error=${sanitizedCancelMsg}`);
428
+ throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Cancellation cleanup failed: ${sanitizedCancelMsg}`, makeDetails(true, remoteRunId, { cancelOutcome: cancelRes.outcome }));
429
+ }
430
+ throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled", makeDetails(cancelRes.resultUnknown, remoteRunId, { cancelOutcome: cancelRes.outcome }));
431
+ }
432
+ const sanitizedWaitMsg = sanitizeErrorMessage(waitErr?.message, target.token);
433
+ logger?.error?.(`[RemoteInvocation] Network failure while waiting: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, error=${sanitizedWaitMsg}`);
434
+ throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Communication lost while waiting for remote run '${remoteRunId}': ${sanitizedWaitMsg}`, makeDetails(true, remoteRunId, { originalError: sanitizedWaitMsg }));
435
+ }
436
+ // 若在此期间已被信号取消,必须等待远端 runs.cancel 发送完成
437
+ if (context.signal.aborted) {
438
+ const cancelRes = await (cancelPromise ?? cancelRunWithBoundedTimeout(service, remoteRunId, cleanupTimeoutMs));
439
+ if (!cancelRes.ok) {
440
+ const sanitizedCancelMsg = sanitizeErrorMessage(cancelRes.error?.message, target.token);
441
+ logger?.error?.(`[RemoteInvocation] Cancellation cleanup failed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, error=${sanitizedCancelMsg}`);
442
+ throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Cancellation cleanup failed: ${sanitizedCancelMsg}`, makeDetails(true, remoteRunId, { cancelOutcome: cancelRes.outcome }));
443
+ }
444
+ logger?.info?.(`[RemoteInvocation] Cancellation requested: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}`);
445
+ throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled", makeDetails(cancelRes.resultUnknown, remoteRunId, { cancelOutcome: cancelRes.outcome }));
446
+ }
447
+ if (execResult.ok) {
448
+ logger?.info?.(`[RemoteInvocation] Succeeded: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, target=${sanitizedTarget}`);
449
+ return execResult.data;
450
+ }
451
+ // 执行终态失败分支
452
+ const errCode = execResult.error?.code;
453
+ const rawErrMsg = execResult.error?.message || "Remote action execution failed";
454
+ const sanitizedErrMsg = sanitizeErrorMessage(rawErrMsg, target.token);
455
+ if (errCode === NETWORK_ERROR || errCode === TIMEOUT || errCode === SERVICE_RESULT_UNKNOWN) {
456
+ logger?.error?.(`[RemoteInvocation] Network error during execution wait: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}`);
457
+ throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Network error while waiting for remote run '${remoteRunId}': ${sanitizedErrMsg}`, makeDetails(true, remoteRunId, {
458
+ remoteError: sanitizeDetails(execResult.error, target.token),
459
+ }));
460
+ }
461
+ if (errCode === SERVICE_CLOSED) {
462
+ throw new ActionDockError(SERVICE_CLOSED, `Remote service closed: ${sanitizedErrMsg}`, makeDetails(true, remoteRunId));
463
+ }
464
+ // 确定业务失败:保留原错误码
465
+ logger?.warn?.(`[RemoteInvocation] Business error: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, code=${errCode}`);
466
+ throw new ActionDockError(errCode || "ACTION_FAILED", sanitizedErrMsg, makeDetails(false, remoteRunId, {
467
+ remoteError: sanitizeDetails(execResult.error?.details ?? execResult.error, target.token),
468
+ }));
469
+ }
470
+ finally {
471
+ context.signal.removeEventListener("abort", abortHandler);
472
+ }
473
+ }
474
+ /**
475
+ * 关闭所有持有的远端服务连接并释放关联资源,对外拒绝后续调用。
476
+ */
477
+ async close() {
478
+ if (this.isClosed) {
479
+ return;
480
+ }
481
+ this.isClosed = true;
482
+ this.closeController.abort(new ActionDockError(SERVICE_CLOSED, "Remote invocation executor is closing"));
483
+ // 先让在途调用完成有界反查与协作取消,再关闭其依赖的服务连接。
484
+ await Promise.allSettled(Array.from(this.pending));
485
+ const servicesToClose = Array.from(this.services.values());
486
+ this.services.clear();
487
+ await Promise.all(servicesToClose.map((s) => s.close()));
488
+ }
489
+ }
@@ -49,6 +49,8 @@ export interface InvocationContext {
49
49
  readonly caller?: InvocationCaller;
50
50
  /** 调用栈数组快照(用于环路检测与调用深度限制) */
51
51
  readonly callStack: readonly string[];
52
+ /** 已解析目标的调用栈作用域,仅用于宿主内部执行,不跨网络透传。 */
53
+ readonly targetScope?: string;
52
54
  /** 目标包物理与快照身份标识 */
53
55
  readonly package: PackageIdentity;
54
56
  /** 取消信号 */
@@ -31,6 +31,7 @@ export function createInvocationContext(options) {
31
31
  parentRunId: options.parentRunId,
32
32
  caller: options.caller,
33
33
  callStack: options.callStack ? [...options.callStack] : [],
34
+ targetScope: options.targetScope,
34
35
  package: pkg,
35
36
  signal: options.signal ?? new AbortController().signal,
36
37
  timeoutMs: options.timeoutMs,
@@ -10,7 +10,7 @@ export { createPackageRuntime, DefaultPackageRuntime, } from "./runtime.js";
10
10
  export { createNonClosingStorageView } from "./storage-view.js";
11
11
  export { findExecutable } from "../utils/index.js";
12
12
  export type { PackageRuntime, PackageRuntimeOptions, PackageInfo, ActionSpec, ActionSummary, PlaybookSpec, PlaybookSummary, } from "./types.js";
13
- export type { RuntimePlatform, StorageFactory, StorageFactoryOptions, GlobalStorageFactoryOptions, } from "../platform/types.js";
13
+ export type { RuntimePlatform, StorageFactory, StorageFactoryOptions, GlobalStorageFactoryOptions, InvocationTargetResolution, InvocationCallerInfo, InvocationTargetResolver, } from "../platform/types.js";
14
14
  export { type Clock, SystemClock, } from "../storage/clock.js";
15
15
  export type { ModuleLoader } from "../platform/module-loader.js";
16
16
  export { InMemoryEventSink, type EventSink, } from "../runtime/events.js";