@openclaw/plugin-inspector 0.3.24 → 0.3.25
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.
- package/CHANGELOG.md +16 -0
- package/README.md +123 -0
- package/package.json +1 -1
- package/src/api.js +7 -0
- package/src/artifacts.js +22 -1
- package/src/capture-api.js +15 -0
- package/src/capture-cli.js +1 -1
- package/src/capture-config.js +10 -7
- package/src/cli.js +3 -1
- package/src/cold-import-readiness.js +3 -3
- package/src/import-loop-profile.js +56 -11
- package/src/inspector.js +110 -45
- package/src/mock-sdk-capture-runner.js +61 -22
- package/src/openclaw-version.js +121 -9
- package/src/process-profile.js +255 -107
- package/src/runtime-capture-report.js +6 -0
- package/src/runtime-profile.js +4 -0
- package/src/sdk-mock.js +133 -13
- package/src/synthetic-entrypoint.js +19 -19
- package/src/synthetic-probes-cli.js +79 -2
- package/src/synthetic-probes.js +201 -34
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import { rmSync } from "node:fs";
|
|
2
2
|
import { mkdtemp } from "node:fs/promises";
|
|
3
|
-
import { register } from "node:module";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
7
5
|
import { captureEntrypoint } from "./inspector.js";
|
|
8
|
-
import { createMockSdkPackage } from "./sdk-mock.js";
|
|
6
|
+
import { createMockSdkPackage, installMockSdkLoader } from "./sdk-mock.js";
|
|
9
7
|
import { runCapturedSyntheticProbes } from "./synthetic-probes.js";
|
|
10
8
|
|
|
11
9
|
export async function runEntrypointSyntheticProbes(entrypoint, options = {}) {
|
|
12
|
-
const
|
|
10
|
+
const captureOptions = {
|
|
13
11
|
...options,
|
|
14
12
|
apiOptions: {
|
|
15
13
|
...(options.apiOptions ?? {}),
|
|
16
14
|
retainHandlers: true,
|
|
17
15
|
},
|
|
18
|
-
}
|
|
19
|
-
return runCapturedSyntheticProbes(capture, options);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function captureEntrypointForSyntheticProbes(entrypoint, options) {
|
|
16
|
+
};
|
|
23
17
|
if (options.mockSdk !== true) {
|
|
24
|
-
|
|
18
|
+
const capture = await captureEntrypoint(entrypoint, captureOptions);
|
|
19
|
+
return runCapturedSyntheticProbes(capture, options);
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -29,15 +24,20 @@ async function captureEntrypointForSyntheticProbes(entrypoint, options) {
|
|
|
29
24
|
const pluginRoot = path.resolve(cwd, options.pluginRoot ?? path.dirname(resolvedEntrypoint));
|
|
30
25
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-synthetic-mock-sdk-"));
|
|
31
26
|
cleanupTempDirOnExit(workspace);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
const mockPackage = await createMockSdkPackage(workspace, { pluginRoot });
|
|
28
|
+
const stopLoader = await installMockSdkLoader(mockPackage);
|
|
29
|
+
try {
|
|
30
|
+
const capture = await captureEntrypoint(entrypoint, {
|
|
31
|
+
...captureOptions,
|
|
32
|
+
cwd,
|
|
33
|
+
mockSdk: false,
|
|
34
|
+
pluginRoot,
|
|
35
|
+
});
|
|
36
|
+
// Retained handlers can require SDK modules lazily during invocation.
|
|
37
|
+
return await runCapturedSyntheticProbes(capture, options);
|
|
38
|
+
} finally {
|
|
39
|
+
stopLoader();
|
|
40
|
+
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function cleanupTempDirOnExit(dir) {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { readBoundedJsonArtifact, writeArtifacts } from "./artifacts.js";
|
|
7
|
+
import { resolveProcessLimits, startOwnedProcess } from "./process-profile.js";
|
|
3
8
|
|
|
4
9
|
const args = process.argv.slice(2);
|
|
5
10
|
|
|
@@ -26,7 +31,7 @@ async function run(commandArgs) {
|
|
|
26
31
|
throw new Error("synthetic probes import plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
const results = await
|
|
34
|
+
const results = await runInChild(entrypoint, {
|
|
30
35
|
mockSdk,
|
|
31
36
|
pluginRoot,
|
|
32
37
|
apiOptions: { retainHandlers: true },
|
|
@@ -43,6 +48,78 @@ async function run(commandArgs) {
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
async function runInChild(entrypoint, options) {
|
|
52
|
+
const limits = resolveProcessLimits({}, "PROBE");
|
|
53
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-synthetic-cli-"));
|
|
54
|
+
const outputPath = path.join(workspace, "result.json");
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const cancel = () => controller.abort(new Error("Synthetic probes cancelled"));
|
|
57
|
+
process.once("SIGINT", cancel);
|
|
58
|
+
process.once("SIGTERM", cancel);
|
|
59
|
+
try {
|
|
60
|
+
const runnerPath = fileURLToPath(new URL("./mock-sdk-capture-runner.js", import.meta.url));
|
|
61
|
+
const { result } = startOwnedProcess({
|
|
62
|
+
command: process.execPath,
|
|
63
|
+
args: [
|
|
64
|
+
"--no-warnings",
|
|
65
|
+
...(options.mockSdk ? ["--preserve-symlinks"] : []),
|
|
66
|
+
runnerPath,
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
...options, ...limits, entrypoint, outputPath,
|
|
69
|
+
cwd: process.cwd(), syntheticProbes: true,
|
|
70
|
+
}),
|
|
71
|
+
],
|
|
72
|
+
...limits,
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
}, "PROBE");
|
|
75
|
+
const outcome = await result;
|
|
76
|
+
if (outcome.exitCode !== 0 || outcome.outputTruncated) {
|
|
77
|
+
const message = outcome.cancelled ? "Synthetic probes cancelled"
|
|
78
|
+
: outcome.timedOut ? `Synthetic probes timed out after ${limits.timeoutMs}ms`
|
|
79
|
+
: outcome.outputTruncated ? "Synthetic probe child output exceeded its byte limit"
|
|
80
|
+
: outcome.stderr.trim() || outcome.error?.message || "Synthetic probe child failed";
|
|
81
|
+
throw new Error(message);
|
|
82
|
+
}
|
|
83
|
+
controller.signal.throwIfAborted();
|
|
84
|
+
// The report is separate from plugin stdout, including direct fd writes.
|
|
85
|
+
// Only accept a fresh complete artifact after successful child cleanup.
|
|
86
|
+
const results = await readBoundedJsonArtifact(outputPath, limits.maxOutputBytes);
|
|
87
|
+
validateSyntheticReport(results);
|
|
88
|
+
controller.signal.throwIfAborted();
|
|
89
|
+
return results;
|
|
90
|
+
} finally {
|
|
91
|
+
process.removeListener("SIGINT", cancel);
|
|
92
|
+
process.removeListener("SIGTERM", cancel);
|
|
93
|
+
await rm(workspace, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateSyntheticReport(report) {
|
|
98
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
99
|
+
if (!isObject(report) || typeof report.entrypoint !== "string" ||
|
|
100
|
+
!["captured", "no-register-export"].includes(report.status) ||
|
|
101
|
+
!isObject(report.summary) || !Array.isArray(report.results)) {
|
|
102
|
+
throw new Error("Invalid synthetic probe report: expected entrypoint, status, summary, and results");
|
|
103
|
+
}
|
|
104
|
+
const counts = { probeCount: report.results.length, passCount: 0, failCount: 0, blockedCount: 0 };
|
|
105
|
+
for (const row of report.results) {
|
|
106
|
+
if (!isObject(row) || !Number.isSafeInteger(row.captureIndex) || row.captureIndex < 0 ||
|
|
107
|
+
!["kind", "seam", "label"].every((key) => typeof row[key] === "string") ||
|
|
108
|
+
!["pass", "fail", "blocked"].includes(row.status) ||
|
|
109
|
+
(row.status === "fail" && typeof row.error !== "string") ||
|
|
110
|
+
(row.status === "blocked" && typeof row.reason !== "string")) {
|
|
111
|
+
throw new Error("Invalid synthetic probe report: malformed result row");
|
|
112
|
+
}
|
|
113
|
+
counts[`${row.status}Count`] += 1;
|
|
114
|
+
}
|
|
115
|
+
for (const [key, expected] of Object.entries(counts)) {
|
|
116
|
+
if (!Number.isSafeInteger(report.summary[key]) || report.summary[key] < 0 ||
|
|
117
|
+
report.summary[key] !== expected) {
|
|
118
|
+
throw new Error(`Invalid synthetic probe report: invalid or inconsistent ${key}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
46
123
|
function readFlag(commandArgs, name) {
|
|
47
124
|
const index = commandArgs.indexOf(name);
|
|
48
125
|
if (index === -1) {
|
package/src/synthetic-probes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
2
|
+
import { resolveProcessLimits } from "./process-profile.js";
|
|
2
3
|
|
|
3
4
|
export const syntheticRegistrationExecutionProfiles = {
|
|
4
5
|
createChatChannelPlugin: {
|
|
@@ -46,6 +47,11 @@ export const syntheticRegistrationExecutionProfiles = {
|
|
|
46
47
|
callableProperties: [],
|
|
47
48
|
reason: "auto-enable probes are captured as registration metadata before runtime activation checks",
|
|
48
49
|
},
|
|
50
|
+
registerBoardWidgetContentKind: {
|
|
51
|
+
mode: "metadata-only",
|
|
52
|
+
callableProperties: [],
|
|
53
|
+
reason: "board widget content kinds are captured as registration metadata before source validation or document composition",
|
|
54
|
+
},
|
|
49
55
|
registerCli: {
|
|
50
56
|
mode: "direct",
|
|
51
57
|
callableProperties: ["handler", "run", "execute"],
|
|
@@ -126,6 +132,16 @@ export const syntheticRegistrationExecutionProfiles = {
|
|
|
126
132
|
callableProperties: [],
|
|
127
133
|
reason: "hosted media resolvers are captured as registration metadata before media URL resolution",
|
|
128
134
|
},
|
|
135
|
+
registerMcpServerConnectionResolver: {
|
|
136
|
+
mode: "metadata-only",
|
|
137
|
+
callableProperties: [],
|
|
138
|
+
reason: "MCP server connection resolvers are captured as registration metadata before requester-bound transport resolution",
|
|
139
|
+
},
|
|
140
|
+
registerMemoryPromptPreparation: {
|
|
141
|
+
mode: "metadata-only",
|
|
142
|
+
callableProperties: [],
|
|
143
|
+
reason: "memory prompt preparation callbacks are captured as registration metadata before prompt-runtime execution",
|
|
144
|
+
},
|
|
129
145
|
registerMemoryPromptSection: {
|
|
130
146
|
mode: "metadata-only",
|
|
131
147
|
callableProperties: [],
|
|
@@ -275,6 +291,11 @@ export const syntheticRegistrationExecutionProfiles = {
|
|
|
275
291
|
callableProperties: [],
|
|
276
292
|
reason: "text transforms are captured as registration metadata before content mutation execution",
|
|
277
293
|
},
|
|
294
|
+
registerTranscriptSourceProvider: {
|
|
295
|
+
mode: "metadata-only",
|
|
296
|
+
callableProperties: [],
|
|
297
|
+
reason: "transcript source providers are captured as registration metadata before live capture or transcript import",
|
|
298
|
+
},
|
|
278
299
|
registerVideoGenerationProvider: {
|
|
279
300
|
mode: "metadata-only",
|
|
280
301
|
callableProperties: [],
|
|
@@ -300,6 +321,11 @@ export const syntheticRegistrationExecutionProfiles = {
|
|
|
300
321
|
callableProperties: [],
|
|
301
322
|
reason: "widget presenters are captured as registration metadata before presentation runtime execution",
|
|
302
323
|
},
|
|
324
|
+
registerWorkerProvider: {
|
|
325
|
+
mode: "metadata-only",
|
|
326
|
+
callableProperties: [],
|
|
327
|
+
reason: "worker providers are captured as registration metadata before cloud-worker lifecycle execution",
|
|
328
|
+
},
|
|
303
329
|
};
|
|
304
330
|
|
|
305
331
|
export const defaultSyntheticHookEvents = {
|
|
@@ -483,6 +509,7 @@ export const defaultSyntheticRegistrationProbeInputs = {
|
|
|
483
509
|
registerGatewayMethod: {
|
|
484
510
|
execute: gatewayProbeArgs,
|
|
485
511
|
handler: gatewayProbeArgs,
|
|
512
|
+
invoke: gatewayProbeArgs,
|
|
486
513
|
run: gatewayProbeArgs,
|
|
487
514
|
},
|
|
488
515
|
registerHttpRoute: {
|
|
@@ -597,8 +624,27 @@ export async function writeSyntheticProbePlan(plan, options = {}) {
|
|
|
597
624
|
}
|
|
598
625
|
|
|
599
626
|
export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
627
|
+
options.signal?.throwIfAborted();
|
|
600
628
|
const hookEvents = options.hookEvents ?? defaultSyntheticHookEvents;
|
|
601
629
|
const hookContexts = options.hookContexts ?? defaultSyntheticHookContexts;
|
|
630
|
+
const { timeoutMs } = resolveProcessLimits(options, "PROBE");
|
|
631
|
+
const controller = new AbortController();
|
|
632
|
+
const onCancel = () => controller.abort(new Error("Synthetic probes cancelled"));
|
|
633
|
+
options.signal?.addEventListener("abort", onCancel, { once: true });
|
|
634
|
+
if (options.signal?.aborted) onCancel();
|
|
635
|
+
try {
|
|
636
|
+
const result = await runCapturedProbes(capture, {
|
|
637
|
+
...options, hookEvents, hookContexts, timeoutMs, controller, signal: controller.signal,
|
|
638
|
+
});
|
|
639
|
+
if (options.signal?.aborted) throw controller.signal.reason;
|
|
640
|
+
return result;
|
|
641
|
+
} finally {
|
|
642
|
+
options.signal?.removeEventListener("abort", onCancel);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function runCapturedProbes(capture, options) {
|
|
647
|
+
const { hookEvents, hookContexts, timeoutMs, controller, signal } = options;
|
|
602
648
|
const captured = capture.captured ?? [];
|
|
603
649
|
const retained = new Map((capture.retained ?? []).map((item) => [item.captureIndex, item]));
|
|
604
650
|
const resultsByCaptureIndex = new Map();
|
|
@@ -611,6 +657,10 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
|
611
657
|
);
|
|
612
658
|
|
|
613
659
|
for (const { entry, captureIndex } of executionEntries) {
|
|
660
|
+
if (signal.aborted) {
|
|
661
|
+
resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, signal.reason.message)]);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
614
664
|
const retainedEntry = retained.get(captureIndex);
|
|
615
665
|
if (!retainedEntry) {
|
|
616
666
|
resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, "handler retention was not enabled")]);
|
|
@@ -618,7 +668,7 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
|
618
668
|
}
|
|
619
669
|
if (entry.kind === "hook") {
|
|
620
670
|
resultsByCaptureIndex.set(captureIndex, [
|
|
621
|
-
await runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts }),
|
|
671
|
+
await runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts, timeoutMs, controller }),
|
|
622
672
|
]);
|
|
623
673
|
continue;
|
|
624
674
|
}
|
|
@@ -719,7 +769,7 @@ function probeBlocker({ hasSyntheticArguments, execution }) {
|
|
|
719
769
|
return null;
|
|
720
770
|
}
|
|
721
771
|
|
|
722
|
-
async function runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts }) {
|
|
772
|
+
async function runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts, timeoutMs, controller }) {
|
|
723
773
|
if (typeof retainedEntry.handler !== "function") {
|
|
724
774
|
return blockedResult(entry, captureIndex, "captured hook has no callable handler");
|
|
725
775
|
}
|
|
@@ -728,6 +778,8 @@ async function runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, ho
|
|
|
728
778
|
kind: "hook",
|
|
729
779
|
seam: entry.name,
|
|
730
780
|
label: entry.name,
|
|
781
|
+
timeoutMs,
|
|
782
|
+
controller,
|
|
731
783
|
invoke: () =>
|
|
732
784
|
retainedEntry.handler(
|
|
733
785
|
hookEvents[entry.name] ?? { hook: entry.name },
|
|
@@ -746,7 +798,9 @@ async function runRegistrationProbes(entry, retainedEntry, captureIndex, options
|
|
|
746
798
|
}
|
|
747
799
|
|
|
748
800
|
const descriptor =
|
|
749
|
-
|
|
801
|
+
entry.name === "registerGatewayMethod" && typeof retainedEntry.arguments?.[0] === "string"
|
|
802
|
+
? retainedEntry.returnValue
|
|
803
|
+
: retainedEntry.arguments?.find((value) => value && typeof value === "object") ?? retainedEntry.returnValue;
|
|
750
804
|
if (!descriptor || typeof descriptor !== "object") {
|
|
751
805
|
return [blockedResult(entry, captureIndex, "captured registration has no object descriptor")];
|
|
752
806
|
}
|
|
@@ -759,17 +813,29 @@ async function runRegistrationProbes(entry, retainedEntry, captureIndex, options
|
|
|
759
813
|
return [blockedResult(entry, captureIndex, "captured registration has no supported callable probe")];
|
|
760
814
|
}
|
|
761
815
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
816
|
+
// The profile owns lifecycle order; finish each callback before starting the next.
|
|
817
|
+
const results = [];
|
|
818
|
+
for (const invocation of invocations) {
|
|
819
|
+
if (options.signal.aborted) {
|
|
820
|
+
results.push({
|
|
821
|
+
...blockedResult(entry, captureIndex, options.signal.reason.message),
|
|
822
|
+
label: invocation.label,
|
|
823
|
+
});
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
results.push(
|
|
827
|
+
await runProbe({
|
|
765
828
|
captureIndex,
|
|
766
829
|
kind: "registration",
|
|
767
830
|
seam: entry.name,
|
|
768
831
|
label: invocation.label,
|
|
832
|
+
timeoutMs: options.timeoutMs,
|
|
833
|
+
controller: options.controller,
|
|
769
834
|
invoke: invocation.invoke,
|
|
770
835
|
}),
|
|
771
|
-
)
|
|
772
|
-
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return results;
|
|
773
839
|
}
|
|
774
840
|
|
|
775
841
|
function registrationInvocations(registrar, descriptor, returnValue, profile, options) {
|
|
@@ -786,20 +852,100 @@ function registrationInvocations(registrar, descriptor, returnValue, profile, op
|
|
|
786
852
|
if (typeof callable === "function") {
|
|
787
853
|
invocations.push({
|
|
788
854
|
label: `${registrar}.${property}`,
|
|
789
|
-
invoke: () =>
|
|
855
|
+
invoke: () => registrar === "registerGatewayMethod"
|
|
856
|
+
? invokeGatewayCallable(callable, property, descriptor, options)
|
|
857
|
+
: invokeRegistrationCallable(callable, registrar, property, options),
|
|
790
858
|
});
|
|
859
|
+
// Gateway aliases describe one method, not independent lifecycle callbacks.
|
|
860
|
+
if (registrar === "registerGatewayMethod") break;
|
|
791
861
|
}
|
|
792
862
|
}
|
|
793
863
|
return invocations;
|
|
794
864
|
}
|
|
795
865
|
|
|
796
|
-
function invokeRegistrationCallable(
|
|
797
|
-
|
|
866
|
+
function invokeRegistrationCallable(
|
|
867
|
+
callable, registrar, property, options,
|
|
868
|
+
event = syntheticRegistrationEvent(registrar, property, options),
|
|
869
|
+
) {
|
|
798
870
|
const inputFactory = options.registrationProbeInputs?.[registrar]?.[property] ?? defaultSyntheticRegistrationProbeInputs[registrar]?.[property];
|
|
799
871
|
const args = inputFactory ? inputFactory(event, options) : [event];
|
|
800
872
|
return callable(...args);
|
|
801
873
|
}
|
|
802
874
|
|
|
875
|
+
async function invokeGatewayCallable(callable, property, descriptor, options) {
|
|
876
|
+
let responded = false;
|
|
877
|
+
let closed = false;
|
|
878
|
+
let resolveResponse;
|
|
879
|
+
const response = new Promise((resolve) => { resolveResponse = resolve; });
|
|
880
|
+
const onAbort = () => {
|
|
881
|
+
closed = true;
|
|
882
|
+
resolveResponse({ error: options.signal.reason });
|
|
883
|
+
};
|
|
884
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
885
|
+
const respond = (ok, payload, error) => {
|
|
886
|
+
if (closed || responded) return;
|
|
887
|
+
// Snapshot the first emission, including invalid frames. Later responses
|
|
888
|
+
// cannot recover it; logging metadata is not part of the wire envelope.
|
|
889
|
+
responded = true;
|
|
890
|
+
try {
|
|
891
|
+
resolveResponse({ frame: gatewayResponseFrame(ok, payload, error) });
|
|
892
|
+
} catch (error) {
|
|
893
|
+
resolveResponse({ error });
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
try {
|
|
897
|
+
options.signal.throwIfAborted();
|
|
898
|
+
const event = {
|
|
899
|
+
...syntheticRegistrationEvent("registerGatewayMethod", property, options),
|
|
900
|
+
method: descriptor.method ?? descriptor.name,
|
|
901
|
+
respond,
|
|
902
|
+
};
|
|
903
|
+
const result = await invokeRegistrationCallable(callable, "registerGatewayMethod", property, options, event);
|
|
904
|
+
// OpenClaw's plugin registrar adapts returns only without an explicit reply.
|
|
905
|
+
if (!responded && result !== undefined) respond(true, result);
|
|
906
|
+
// A void handler may respond later. The existing probe deadline bounds this
|
|
907
|
+
// observation as well as handler settlement; it owns timeout/cancellation.
|
|
908
|
+
const outcome = await response;
|
|
909
|
+
if (outcome.error) throw outcome.error;
|
|
910
|
+
if (!outcome.frame.ok) {
|
|
911
|
+
throw new Error(`Gateway response error: ${outcome.frame.error?.message ?? "request failed"}`);
|
|
912
|
+
}
|
|
913
|
+
return outcome.frame;
|
|
914
|
+
} finally {
|
|
915
|
+
closed = true;
|
|
916
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function gatewayResponseFrame(ok, payload, error) {
|
|
921
|
+
let frame;
|
|
922
|
+
try {
|
|
923
|
+
frame = JSON.parse(JSON.stringify({ type: "res", id: "fixture-request", ok, payload, error }));
|
|
924
|
+
} catch {
|
|
925
|
+
throw new Error("Gateway response is not JSON serializable");
|
|
926
|
+
}
|
|
927
|
+
// ResponseFrameSchema / ErrorShapeSchema in OpenClaw v2026.9.3. Payload and
|
|
928
|
+
// error are optional independently of ok; error codes are nonempty strings.
|
|
929
|
+
const responseError = frame.error;
|
|
930
|
+
if (
|
|
931
|
+
typeof frame.ok !== "boolean" ||
|
|
932
|
+
(responseError !== undefined && (
|
|
933
|
+
!responseError ||
|
|
934
|
+
typeof responseError !== "object" ||
|
|
935
|
+
Array.isArray(responseError) ||
|
|
936
|
+
typeof responseError.code !== "string" || responseError.code.length === 0 ||
|
|
937
|
+
typeof responseError.message !== "string" || responseError.message.length === 0 ||
|
|
938
|
+
(responseError.retryable !== undefined && typeof responseError.retryable !== "boolean") ||
|
|
939
|
+
(responseError.retryAfterMs !== undefined &&
|
|
940
|
+
(!Number.isInteger(responseError.retryAfterMs) || responseError.retryAfterMs < 0)) ||
|
|
941
|
+
Object.keys(responseError).some((key) => !["code", "message", "details", "retryable", "retryAfterMs"].includes(key))
|
|
942
|
+
))
|
|
943
|
+
) {
|
|
944
|
+
throw new Error("Gateway response is malformed");
|
|
945
|
+
}
|
|
946
|
+
return frame;
|
|
947
|
+
}
|
|
948
|
+
|
|
803
949
|
function syntheticRegistrationEvent(registrar, property, options) {
|
|
804
950
|
const hookEvents = options.hookEvents ?? defaultSyntheticHookEvents;
|
|
805
951
|
const beforeToolCall = hookEvents.before_tool_call ?? defaultSyntheticHookEvents.before_tool_call;
|
|
@@ -822,21 +968,21 @@ function syntheticRegistrationEvent(registrar, property, options) {
|
|
|
822
968
|
};
|
|
823
969
|
}
|
|
824
970
|
|
|
825
|
-
function toolRunProbeArgs(event) {
|
|
971
|
+
function toolRunProbeArgs(event, options = {}) {
|
|
826
972
|
return [
|
|
827
973
|
event.params,
|
|
828
974
|
{
|
|
829
975
|
source: event.source,
|
|
830
976
|
toolName: event.toolName,
|
|
831
977
|
toolCallId: event.toolCall.id,
|
|
832
|
-
signal: new AbortController().signal,
|
|
978
|
+
signal: options.signal ?? new AbortController().signal,
|
|
833
979
|
logger: console,
|
|
834
980
|
},
|
|
835
981
|
];
|
|
836
982
|
}
|
|
837
983
|
|
|
838
|
-
function toolExecuteProbeArgs(event) {
|
|
839
|
-
return [event.toolCall.id, event.params, new AbortController().signal, () => undefined];
|
|
984
|
+
function toolExecuteProbeArgs(event, options = {}) {
|
|
985
|
+
return [event.toolCall.id, event.params, options.signal ?? new AbortController().signal, () => undefined];
|
|
840
986
|
}
|
|
841
987
|
|
|
842
988
|
function httpRouteProbeArgs(event) {
|
|
@@ -858,34 +1004,31 @@ function httpRouteProbeArgs(event) {
|
|
|
858
1004
|
];
|
|
859
1005
|
}
|
|
860
1006
|
|
|
861
|
-
function commandProbeArgs(event) {
|
|
1007
|
+
function commandProbeArgs(event, options = {}) {
|
|
862
1008
|
return [
|
|
863
1009
|
event.input,
|
|
864
1010
|
{
|
|
865
1011
|
source: event.source,
|
|
866
|
-
signal: new AbortController().signal,
|
|
1012
|
+
signal: options.signal ?? new AbortController().signal,
|
|
867
1013
|
logger: console,
|
|
868
1014
|
},
|
|
869
1015
|
];
|
|
870
1016
|
}
|
|
871
1017
|
|
|
872
|
-
function gatewayProbeArgs(event) {
|
|
1018
|
+
function gatewayProbeArgs(event, options = {}) {
|
|
873
1019
|
return [
|
|
874
1020
|
{
|
|
875
1021
|
...event,
|
|
876
|
-
params: event.params,
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
{
|
|
882
|
-
source: event.source,
|
|
883
|
-
logger: console,
|
|
1022
|
+
req: { type: "req", id: "fixture-request", method: event.method ?? "fixture.gateway.method", params: event.params },
|
|
1023
|
+
client: null,
|
|
1024
|
+
isWebchatConnect: () => false,
|
|
1025
|
+
context: { source: event.source, logger: console },
|
|
1026
|
+
signal: options.signal,
|
|
884
1027
|
},
|
|
885
1028
|
];
|
|
886
1029
|
}
|
|
887
1030
|
|
|
888
|
-
function channelSendProbeArgs(event) {
|
|
1031
|
+
function channelSendProbeArgs(event, options = {}) {
|
|
889
1032
|
return [
|
|
890
1033
|
{
|
|
891
1034
|
source: event.source,
|
|
@@ -896,12 +1039,12 @@ function channelSendProbeArgs(event) {
|
|
|
896
1039
|
replyToId: "fixture-reply",
|
|
897
1040
|
threadId: "fixture-thread",
|
|
898
1041
|
logger: console,
|
|
899
|
-
signal: new AbortController().signal,
|
|
1042
|
+
signal: options.signal ?? new AbortController().signal,
|
|
900
1043
|
},
|
|
901
1044
|
];
|
|
902
1045
|
}
|
|
903
1046
|
|
|
904
|
-
function channelReceiveProbeArgs(event) {
|
|
1047
|
+
function channelReceiveProbeArgs(event, options = {}) {
|
|
905
1048
|
return [
|
|
906
1049
|
{
|
|
907
1050
|
source: event.source,
|
|
@@ -921,7 +1064,7 @@ function channelReceiveProbeArgs(event) {
|
|
|
921
1064
|
to: "fixture-channel",
|
|
922
1065
|
},
|
|
923
1066
|
logger: console,
|
|
924
|
-
signal: new AbortController().signal,
|
|
1067
|
+
signal: options.signal ?? new AbortController().signal,
|
|
925
1068
|
},
|
|
926
1069
|
];
|
|
927
1070
|
}
|
|
@@ -939,7 +1082,7 @@ function interactiveProbeArgs(event) {
|
|
|
939
1082
|
];
|
|
940
1083
|
}
|
|
941
1084
|
|
|
942
|
-
function lifecycleProbeArgs(event) {
|
|
1085
|
+
function lifecycleProbeArgs(event, options = {}) {
|
|
943
1086
|
return [
|
|
944
1087
|
{
|
|
945
1088
|
source: event.source,
|
|
@@ -947,7 +1090,7 @@ function lifecycleProbeArgs(event) {
|
|
|
947
1090
|
logger: console,
|
|
948
1091
|
runtime: { env: {}, logger: console },
|
|
949
1092
|
secrets: { get: async () => null, has: async () => false },
|
|
950
|
-
signal: new AbortController().signal,
|
|
1093
|
+
signal: options.signal ?? new AbortController().signal,
|
|
951
1094
|
},
|
|
952
1095
|
];
|
|
953
1096
|
}
|
|
@@ -965,9 +1108,9 @@ function speechProbeArgs(event) {
|
|
|
965
1108
|
];
|
|
966
1109
|
}
|
|
967
1110
|
|
|
968
|
-
async function runProbe({ captureIndex, kind, seam, label, invoke }) {
|
|
1111
|
+
async function runProbe({ captureIndex, kind, seam, label, invoke, timeoutMs, controller }) {
|
|
969
1112
|
try {
|
|
970
|
-
const output = await invoke
|
|
1113
|
+
const output = await invokeWithTimeout(invoke, timeoutMs, controller);
|
|
971
1114
|
return {
|
|
972
1115
|
captureIndex,
|
|
973
1116
|
kind,
|
|
@@ -988,6 +1131,30 @@ async function runProbe({ captureIndex, kind, seam, label, invoke }) {
|
|
|
988
1131
|
}
|
|
989
1132
|
}
|
|
990
1133
|
|
|
1134
|
+
function invokeWithTimeout(invoke, timeoutMs, controller) {
|
|
1135
|
+
const { signal } = controller;
|
|
1136
|
+
let rejectAborted;
|
|
1137
|
+
const aborted = new Promise((_, reject) => { rejectAborted = reject; });
|
|
1138
|
+
const onAbort = () => rejectAborted(signal.reason);
|
|
1139
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1140
|
+
const timeoutId = setTimeout(() => {
|
|
1141
|
+
controller.abort(new Error(`Synthetic probe timed out after ${timeoutMs}ms`));
|
|
1142
|
+
}, timeoutMs);
|
|
1143
|
+
// Keep observing settlement after abort without starting dependent callbacks.
|
|
1144
|
+
// In-process JavaScript itself is not preempted by this deadline.
|
|
1145
|
+
const result = Promise.resolve().then(() => {
|
|
1146
|
+
signal.throwIfAborted();
|
|
1147
|
+
return invoke();
|
|
1148
|
+
}).then((output) => {
|
|
1149
|
+
signal.throwIfAborted();
|
|
1150
|
+
return output;
|
|
1151
|
+
});
|
|
1152
|
+
return Promise.race([result, aborted]).finally(() => {
|
|
1153
|
+
clearTimeout(timeoutId);
|
|
1154
|
+
signal.removeEventListener("abort", onAbort);
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
991
1158
|
function blockedResult(entry, captureIndex, reason) {
|
|
992
1159
|
return {
|
|
993
1160
|
captureIndex,
|