@openclaw/plugin-inspector 0.3.23 → 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 +23 -0
- package/README.md +129 -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 +116 -50
- 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 +206 -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: [],
|
|
@@ -295,6 +316,16 @@ export const syntheticRegistrationExecutionProfiles = {
|
|
|
295
316
|
callableProperties: [],
|
|
296
317
|
reason: "web search providers are captured as registration metadata before provider runtime execution",
|
|
297
318
|
},
|
|
319
|
+
registerWidgetPresenter: {
|
|
320
|
+
mode: "metadata-only",
|
|
321
|
+
callableProperties: [],
|
|
322
|
+
reason: "widget presenters are captured as registration metadata before presentation runtime execution",
|
|
323
|
+
},
|
|
324
|
+
registerWorkerProvider: {
|
|
325
|
+
mode: "metadata-only",
|
|
326
|
+
callableProperties: [],
|
|
327
|
+
reason: "worker providers are captured as registration metadata before cloud-worker lifecycle execution",
|
|
328
|
+
},
|
|
298
329
|
};
|
|
299
330
|
|
|
300
331
|
export const defaultSyntheticHookEvents = {
|
|
@@ -478,6 +509,7 @@ export const defaultSyntheticRegistrationProbeInputs = {
|
|
|
478
509
|
registerGatewayMethod: {
|
|
479
510
|
execute: gatewayProbeArgs,
|
|
480
511
|
handler: gatewayProbeArgs,
|
|
512
|
+
invoke: gatewayProbeArgs,
|
|
481
513
|
run: gatewayProbeArgs,
|
|
482
514
|
},
|
|
483
515
|
registerHttpRoute: {
|
|
@@ -592,8 +624,27 @@ export async function writeSyntheticProbePlan(plan, options = {}) {
|
|
|
592
624
|
}
|
|
593
625
|
|
|
594
626
|
export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
627
|
+
options.signal?.throwIfAborted();
|
|
595
628
|
const hookEvents = options.hookEvents ?? defaultSyntheticHookEvents;
|
|
596
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;
|
|
597
648
|
const captured = capture.captured ?? [];
|
|
598
649
|
const retained = new Map((capture.retained ?? []).map((item) => [item.captureIndex, item]));
|
|
599
650
|
const resultsByCaptureIndex = new Map();
|
|
@@ -606,6 +657,10 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
|
606
657
|
);
|
|
607
658
|
|
|
608
659
|
for (const { entry, captureIndex } of executionEntries) {
|
|
660
|
+
if (signal.aborted) {
|
|
661
|
+
resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, signal.reason.message)]);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
609
664
|
const retainedEntry = retained.get(captureIndex);
|
|
610
665
|
if (!retainedEntry) {
|
|
611
666
|
resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, "handler retention was not enabled")]);
|
|
@@ -613,7 +668,7 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
|
613
668
|
}
|
|
614
669
|
if (entry.kind === "hook") {
|
|
615
670
|
resultsByCaptureIndex.set(captureIndex, [
|
|
616
|
-
await runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts }),
|
|
671
|
+
await runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts, timeoutMs, controller }),
|
|
617
672
|
]);
|
|
618
673
|
continue;
|
|
619
674
|
}
|
|
@@ -714,7 +769,7 @@ function probeBlocker({ hasSyntheticArguments, execution }) {
|
|
|
714
769
|
return null;
|
|
715
770
|
}
|
|
716
771
|
|
|
717
|
-
async function runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts }) {
|
|
772
|
+
async function runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts, timeoutMs, controller }) {
|
|
718
773
|
if (typeof retainedEntry.handler !== "function") {
|
|
719
774
|
return blockedResult(entry, captureIndex, "captured hook has no callable handler");
|
|
720
775
|
}
|
|
@@ -723,6 +778,8 @@ async function runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, ho
|
|
|
723
778
|
kind: "hook",
|
|
724
779
|
seam: entry.name,
|
|
725
780
|
label: entry.name,
|
|
781
|
+
timeoutMs,
|
|
782
|
+
controller,
|
|
726
783
|
invoke: () =>
|
|
727
784
|
retainedEntry.handler(
|
|
728
785
|
hookEvents[entry.name] ?? { hook: entry.name },
|
|
@@ -741,7 +798,9 @@ async function runRegistrationProbes(entry, retainedEntry, captureIndex, options
|
|
|
741
798
|
}
|
|
742
799
|
|
|
743
800
|
const descriptor =
|
|
744
|
-
|
|
801
|
+
entry.name === "registerGatewayMethod" && typeof retainedEntry.arguments?.[0] === "string"
|
|
802
|
+
? retainedEntry.returnValue
|
|
803
|
+
: retainedEntry.arguments?.find((value) => value && typeof value === "object") ?? retainedEntry.returnValue;
|
|
745
804
|
if (!descriptor || typeof descriptor !== "object") {
|
|
746
805
|
return [blockedResult(entry, captureIndex, "captured registration has no object descriptor")];
|
|
747
806
|
}
|
|
@@ -754,17 +813,29 @@ async function runRegistrationProbes(entry, retainedEntry, captureIndex, options
|
|
|
754
813
|
return [blockedResult(entry, captureIndex, "captured registration has no supported callable probe")];
|
|
755
814
|
}
|
|
756
815
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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({
|
|
760
828
|
captureIndex,
|
|
761
829
|
kind: "registration",
|
|
762
830
|
seam: entry.name,
|
|
763
831
|
label: invocation.label,
|
|
832
|
+
timeoutMs: options.timeoutMs,
|
|
833
|
+
controller: options.controller,
|
|
764
834
|
invoke: invocation.invoke,
|
|
765
835
|
}),
|
|
766
|
-
)
|
|
767
|
-
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return results;
|
|
768
839
|
}
|
|
769
840
|
|
|
770
841
|
function registrationInvocations(registrar, descriptor, returnValue, profile, options) {
|
|
@@ -781,20 +852,100 @@ function registrationInvocations(registrar, descriptor, returnValue, profile, op
|
|
|
781
852
|
if (typeof callable === "function") {
|
|
782
853
|
invocations.push({
|
|
783
854
|
label: `${registrar}.${property}`,
|
|
784
|
-
invoke: () =>
|
|
855
|
+
invoke: () => registrar === "registerGatewayMethod"
|
|
856
|
+
? invokeGatewayCallable(callable, property, descriptor, options)
|
|
857
|
+
: invokeRegistrationCallable(callable, registrar, property, options),
|
|
785
858
|
});
|
|
859
|
+
// Gateway aliases describe one method, not independent lifecycle callbacks.
|
|
860
|
+
if (registrar === "registerGatewayMethod") break;
|
|
786
861
|
}
|
|
787
862
|
}
|
|
788
863
|
return invocations;
|
|
789
864
|
}
|
|
790
865
|
|
|
791
|
-
function invokeRegistrationCallable(
|
|
792
|
-
|
|
866
|
+
function invokeRegistrationCallable(
|
|
867
|
+
callable, registrar, property, options,
|
|
868
|
+
event = syntheticRegistrationEvent(registrar, property, options),
|
|
869
|
+
) {
|
|
793
870
|
const inputFactory = options.registrationProbeInputs?.[registrar]?.[property] ?? defaultSyntheticRegistrationProbeInputs[registrar]?.[property];
|
|
794
871
|
const args = inputFactory ? inputFactory(event, options) : [event];
|
|
795
872
|
return callable(...args);
|
|
796
873
|
}
|
|
797
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
|
+
|
|
798
949
|
function syntheticRegistrationEvent(registrar, property, options) {
|
|
799
950
|
const hookEvents = options.hookEvents ?? defaultSyntheticHookEvents;
|
|
800
951
|
const beforeToolCall = hookEvents.before_tool_call ?? defaultSyntheticHookEvents.before_tool_call;
|
|
@@ -817,21 +968,21 @@ function syntheticRegistrationEvent(registrar, property, options) {
|
|
|
817
968
|
};
|
|
818
969
|
}
|
|
819
970
|
|
|
820
|
-
function toolRunProbeArgs(event) {
|
|
971
|
+
function toolRunProbeArgs(event, options = {}) {
|
|
821
972
|
return [
|
|
822
973
|
event.params,
|
|
823
974
|
{
|
|
824
975
|
source: event.source,
|
|
825
976
|
toolName: event.toolName,
|
|
826
977
|
toolCallId: event.toolCall.id,
|
|
827
|
-
signal: new AbortController().signal,
|
|
978
|
+
signal: options.signal ?? new AbortController().signal,
|
|
828
979
|
logger: console,
|
|
829
980
|
},
|
|
830
981
|
];
|
|
831
982
|
}
|
|
832
983
|
|
|
833
|
-
function toolExecuteProbeArgs(event) {
|
|
834
|
-
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];
|
|
835
986
|
}
|
|
836
987
|
|
|
837
988
|
function httpRouteProbeArgs(event) {
|
|
@@ -853,34 +1004,31 @@ function httpRouteProbeArgs(event) {
|
|
|
853
1004
|
];
|
|
854
1005
|
}
|
|
855
1006
|
|
|
856
|
-
function commandProbeArgs(event) {
|
|
1007
|
+
function commandProbeArgs(event, options = {}) {
|
|
857
1008
|
return [
|
|
858
1009
|
event.input,
|
|
859
1010
|
{
|
|
860
1011
|
source: event.source,
|
|
861
|
-
signal: new AbortController().signal,
|
|
1012
|
+
signal: options.signal ?? new AbortController().signal,
|
|
862
1013
|
logger: console,
|
|
863
1014
|
},
|
|
864
1015
|
];
|
|
865
1016
|
}
|
|
866
1017
|
|
|
867
|
-
function gatewayProbeArgs(event) {
|
|
1018
|
+
function gatewayProbeArgs(event, options = {}) {
|
|
868
1019
|
return [
|
|
869
1020
|
{
|
|
870
1021
|
...event,
|
|
871
|
-
params: event.params,
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
{
|
|
877
|
-
source: event.source,
|
|
878
|
-
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,
|
|
879
1027
|
},
|
|
880
1028
|
];
|
|
881
1029
|
}
|
|
882
1030
|
|
|
883
|
-
function channelSendProbeArgs(event) {
|
|
1031
|
+
function channelSendProbeArgs(event, options = {}) {
|
|
884
1032
|
return [
|
|
885
1033
|
{
|
|
886
1034
|
source: event.source,
|
|
@@ -891,12 +1039,12 @@ function channelSendProbeArgs(event) {
|
|
|
891
1039
|
replyToId: "fixture-reply",
|
|
892
1040
|
threadId: "fixture-thread",
|
|
893
1041
|
logger: console,
|
|
894
|
-
signal: new AbortController().signal,
|
|
1042
|
+
signal: options.signal ?? new AbortController().signal,
|
|
895
1043
|
},
|
|
896
1044
|
];
|
|
897
1045
|
}
|
|
898
1046
|
|
|
899
|
-
function channelReceiveProbeArgs(event) {
|
|
1047
|
+
function channelReceiveProbeArgs(event, options = {}) {
|
|
900
1048
|
return [
|
|
901
1049
|
{
|
|
902
1050
|
source: event.source,
|
|
@@ -916,7 +1064,7 @@ function channelReceiveProbeArgs(event) {
|
|
|
916
1064
|
to: "fixture-channel",
|
|
917
1065
|
},
|
|
918
1066
|
logger: console,
|
|
919
|
-
signal: new AbortController().signal,
|
|
1067
|
+
signal: options.signal ?? new AbortController().signal,
|
|
920
1068
|
},
|
|
921
1069
|
];
|
|
922
1070
|
}
|
|
@@ -934,7 +1082,7 @@ function interactiveProbeArgs(event) {
|
|
|
934
1082
|
];
|
|
935
1083
|
}
|
|
936
1084
|
|
|
937
|
-
function lifecycleProbeArgs(event) {
|
|
1085
|
+
function lifecycleProbeArgs(event, options = {}) {
|
|
938
1086
|
return [
|
|
939
1087
|
{
|
|
940
1088
|
source: event.source,
|
|
@@ -942,7 +1090,7 @@ function lifecycleProbeArgs(event) {
|
|
|
942
1090
|
logger: console,
|
|
943
1091
|
runtime: { env: {}, logger: console },
|
|
944
1092
|
secrets: { get: async () => null, has: async () => false },
|
|
945
|
-
signal: new AbortController().signal,
|
|
1093
|
+
signal: options.signal ?? new AbortController().signal,
|
|
946
1094
|
},
|
|
947
1095
|
];
|
|
948
1096
|
}
|
|
@@ -960,9 +1108,9 @@ function speechProbeArgs(event) {
|
|
|
960
1108
|
];
|
|
961
1109
|
}
|
|
962
1110
|
|
|
963
|
-
async function runProbe({ captureIndex, kind, seam, label, invoke }) {
|
|
1111
|
+
async function runProbe({ captureIndex, kind, seam, label, invoke, timeoutMs, controller }) {
|
|
964
1112
|
try {
|
|
965
|
-
const output = await invoke
|
|
1113
|
+
const output = await invokeWithTimeout(invoke, timeoutMs, controller);
|
|
966
1114
|
return {
|
|
967
1115
|
captureIndex,
|
|
968
1116
|
kind,
|
|
@@ -983,6 +1131,30 @@ async function runProbe({ captureIndex, kind, seam, label, invoke }) {
|
|
|
983
1131
|
}
|
|
984
1132
|
}
|
|
985
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
|
+
|
|
986
1158
|
function blockedResult(entry, captureIndex, reason) {
|
|
987
1159
|
return {
|
|
988
1160
|
captureIndex,
|