@openclaw/plugin-inspector 0.3.25 → 0.3.26
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 +15 -0
- package/README.md +18 -0
- package/package.json +4 -2
- package/src/batch.js +7 -12
- package/src/cli.js +3 -1
- package/src/fixture-summary.js +43 -6
- package/src/inspector.js +18 -62
- package/src/openclaw-target.js +110 -5
- package/src/runtime-imports.js +191 -0
- package/src/runtime-reconciliation.js +0 -6
- package/src/sdk-mock.js +47 -52
- package/src/synthetic-probes.js +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.26 - 2026-09-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Recognize OpenClaw's declared private-local Plugin SDK subpaths and reserved bundled-plugin imports when inspecting bundled `extensions/*` fixtures, while continuing to report those imports for external plugins and genuinely missing SDK aliases.
|
|
10
|
+
- Treat active OpenClaw conversation-access compat records with present contract tests as target-owned proof instead of repeatedly emitting a P1 Inspector probe backlog advisory.
|
|
11
|
+
- Record explicit method-scoped Gateway probe prerequisites before calling handlers that require unavailable host state or live credentials; preserve actual response failures for admitted probes.
|
|
12
|
+
- Preserve absent optional strings in SDK mocks and supply a stable runtime config snapshot to Gateway probes instead of inventing configured values or missing host accessors.
|
|
13
|
+
- Generate SDK mocks for literal dynamic imports in retained handlers, including their named exports, while excluding TypeScript import types and keeping source inspection aligned with runtime capture.
|
|
14
|
+
- Honor the OpenClaw lazy-runtime SDK contract in generated mocks, preserving deferred module loading, shared promise caches, explicit cache clearing, and rejected imports instead of returning callable placeholders.
|
|
15
|
+
- Reject invalid batch concurrency instead of reporting success without inspecting any plugins, and preserve relative plugin paths in retained reports so similar directory names cannot overwrite each other's results.
|
|
16
|
+
- Preserve error messages from the OpenClaw `error-runtime` SDK subpath in synthetic probes, including lazy CommonJS imports, so Gateway rejections report their actual cause and remain failures.
|
|
17
|
+
- Keep tool-hook and conversation-privacy contract gaps open when runtime artifacts only capture hook registration. Preserve registration coverage and semantic contract probes.
|
|
18
|
+
- Read OpenClaw compatibility records from the explicitly imported `registry-records.ts` data module, avoiding false missing-record findings after the registry split. Preserve inline registries and report missing delegated data as an error.
|
|
19
|
+
|
|
5
20
|
## 0.3.25 - 2026-09-09
|
|
6
21
|
|
|
7
22
|
### Fixed
|
package/README.md
CHANGED
|
@@ -200,6 +200,13 @@ Common options:
|
|
|
200
200
|
| `--junit [path]` | Write JUnit XML from `check` or `inspect`; `ci` enables this by default. |
|
|
201
201
|
| `--no-sarif` / `--no-junit` | Disable default `ci` outputs. |
|
|
202
202
|
|
|
203
|
+
For `batch`, `--concurrency <n>` must be a finite number (default `4`, rounded
|
|
204
|
+
and clamped to `1`–`32` workers). Invalid or missing values fail before inspection
|
|
205
|
+
or report writes. `--keep-plugin-reports` retains individual reports under
|
|
206
|
+
`<out>/plugins/<relative-plugin-path>/`, preserving the corpus directory layout
|
|
207
|
+
and names so plugins such as `a/b`, `a-b`, and `a b` cannot overwrite each other's
|
|
208
|
+
reports. If the corpus root is itself a plugin, its reports go in `<out>/plugins/`.
|
|
209
|
+
|
|
203
210
|
Run the built-in help for the exact CLI surface:
|
|
204
211
|
|
|
205
212
|
```bash
|
|
@@ -338,6 +345,15 @@ aliases do not create extra calls. The handler receives synthetic Gateway
|
|
|
338
345
|
options and a void `respond(ok, payload, error, meta)` callback. Existing
|
|
339
346
|
`registrationProbeInputs` overrides remain available.
|
|
340
347
|
|
|
348
|
+
For a method that needs unavailable host state or live credentials, pass
|
|
349
|
+
`gatewayMethodPrerequisites: { "fixture.account": "saved account required" }`
|
|
350
|
+
to `runCapturedSyntheticProbes` or `runEntrypointSyntheticProbes`. The named
|
|
351
|
+
method produces a `blocked` row with its method and reason before its handler
|
|
352
|
+
runs. Other methods retain normal response validation. Once a caller supplies
|
|
353
|
+
the required inputs and runtime, omit that method from the prerequisite map;
|
|
354
|
+
rejected or malformed responses still fail. This option never reports a
|
|
355
|
+
missing prerequisite as a passing runtime check.
|
|
356
|
+
|
|
341
357
|
The first emitted response is authoritative, even when malformed. Probes check
|
|
342
358
|
its JSON-serialized response/error shape: `ok: true` passes, `ok: false` fails,
|
|
343
359
|
and later responses cannot overwrite the outcome. Logging `meta` is not a wire
|
|
@@ -567,6 +583,8 @@ npm run check
|
|
|
567
583
|
```
|
|
568
584
|
|
|
569
585
|
`npm run check` runs the Node test suite and the package-contents guard. The
|
|
586
|
+
test runner limits parallel test files to four so process-supervision tests do
|
|
587
|
+
not contend with a machine-wide burst of child processes for their deadlines. The
|
|
570
588
|
contents guard shells through `npm pack --dry-run --json` and verifies the npm
|
|
571
589
|
tarball includes package entrypoints, examples, README assets, and no private
|
|
572
590
|
`test/`, `scripts/`, or `.github/` paths.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/plugin-inspector",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Offline compatibility inspector for OpenClaw plugins.",
|
|
6
6
|
"type": "module",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"release:readiness": "npm run release:local && npm run release:crabpot",
|
|
58
58
|
"release:local": "npm run check",
|
|
59
59
|
"release:notes": "node scripts/release-notes.mjs --unreleased",
|
|
60
|
-
"test": "node --test test/*.test.js",
|
|
60
|
+
"test": "node --test --test-concurrency=4 test/*.test.js",
|
|
61
61
|
"check:changed": "npm run check",
|
|
62
62
|
"test:changed": "npm test",
|
|
63
63
|
"crabbox:hydrate": "crabbox actions hydrate",
|
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
"ci"
|
|
73
73
|
],
|
|
74
74
|
"dependencies": {
|
|
75
|
+
"acorn": "^8.18.0",
|
|
76
|
+
"eslint-scope": "^8.4.0",
|
|
75
77
|
"semver": "^7.8.5",
|
|
76
78
|
"tar": "^7.5.22"
|
|
77
79
|
}
|
package/src/batch.js
CHANGED
|
@@ -21,7 +21,11 @@ export async function runBatchAnalysis(options = {}) {
|
|
|
21
21
|
const rootDir = path.resolve(options.rootDir ?? options.inputDir ?? process.cwd());
|
|
22
22
|
const outDir = options.outDir ?? "reports";
|
|
23
23
|
const outRoot = path.resolve(rootDir, outDir);
|
|
24
|
-
const
|
|
24
|
+
const requestedConcurrency = options.concurrency ?? 4;
|
|
25
|
+
if (!Number.isFinite(requestedConcurrency)) {
|
|
26
|
+
throw new TypeError("batch concurrency must be a finite number");
|
|
27
|
+
}
|
|
28
|
+
const concurrency = Math.max(1, Math.min(Math.round(requestedConcurrency), 32));
|
|
25
29
|
const keepPluginReports = options.keepPluginReports === true;
|
|
26
30
|
const targetOpenClaw =
|
|
27
31
|
options.targetOpenClaw ??
|
|
@@ -35,8 +39,8 @@ export async function runBatchAnalysis(options = {}) {
|
|
|
35
39
|
try {
|
|
36
40
|
await runWithConcurrency(pluginRoots, concurrency, async (pluginRoot) => {
|
|
37
41
|
const reportsRoot = keepPluginReports
|
|
38
|
-
? path.join(outRoot, "plugins",
|
|
39
|
-
: path.join(tempRoot,
|
|
42
|
+
? path.join(outRoot, "plugins", path.relative(rootDir, pluginRoot))
|
|
43
|
+
: path.join(tempRoot, path.relative(rootDir, pluginRoot));
|
|
40
44
|
entries.push(
|
|
41
45
|
await inspectBatchPlugin(pluginRoot, {
|
|
42
46
|
...options,
|
|
@@ -306,12 +310,3 @@ function packageNameFromReport(report) {
|
|
|
306
310
|
"plugin"
|
|
307
311
|
);
|
|
308
312
|
}
|
|
309
|
-
|
|
310
|
-
function slugForPath(value) {
|
|
311
|
-
return (
|
|
312
|
-
String(value)
|
|
313
|
-
.replaceAll(path.sep, "-")
|
|
314
|
-
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
315
|
-
.replace(/^-+|-+$/g, "") || "plugin"
|
|
316
|
-
);
|
|
317
|
-
}
|
package/src/cli.js
CHANGED
|
@@ -61,7 +61,9 @@ async function runBatch(commandArgs) {
|
|
|
61
61
|
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
62
62
|
const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
|
|
63
63
|
const openclawVersion = readOpenClawVersion(commandArgs);
|
|
64
|
-
const concurrency =
|
|
64
|
+
const concurrency = commandArgs.includes("--concurrency")
|
|
65
|
+
? Number(readFlag(commandArgs, "--concurrency") ?? NaN)
|
|
66
|
+
: 4;
|
|
65
67
|
const json = commandArgs.includes("--json");
|
|
66
68
|
const check = commandArgs.includes("--check");
|
|
67
69
|
const keepPluginReports = commandArgs.includes("--keep-plugin-reports");
|
package/src/fixture-summary.js
CHANGED
|
@@ -550,14 +550,16 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
550
550
|
|
|
551
551
|
const conversationHooks = inspection.hooks.filter((hook) => conversationAccessHooks.has(hook));
|
|
552
552
|
const conversationHookDetails = inspection.hookDetails.filter((hook) => conversationAccessHooks.has(hook.name));
|
|
553
|
-
|
|
553
|
+
const conversationCompatRecord = compatRecordForIssueCode("conversation-access-hook");
|
|
554
|
+
const conversationContractCovered = hasActiveTargetContractTests(targetOpenClaw, conversationCompatRecord);
|
|
555
|
+
if (conversationHooks.length > 0 && !conversationContractCovered) {
|
|
554
556
|
warnings.push({
|
|
555
557
|
fixture: fixture.id,
|
|
556
558
|
code: "conversation-access-hook",
|
|
557
559
|
level: "warning",
|
|
558
560
|
message: "fixture observes raw model or conversation content and needs privacy-boundary contract probes",
|
|
559
561
|
evidence: detailEvidence(conversationHookDetails),
|
|
560
|
-
compatRecord:
|
|
562
|
+
compatRecord: conversationCompatRecord,
|
|
561
563
|
});
|
|
562
564
|
decisions.push({
|
|
563
565
|
fixture: fixture.id,
|
|
@@ -713,6 +715,15 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
713
715
|
return { breakages, warnings, suggestions, logs, decisions };
|
|
714
716
|
}
|
|
715
717
|
|
|
718
|
+
function hasActiveTargetContractTests(targetOpenClaw, compatRecord) {
|
|
719
|
+
if (!compatRecord || !["active", "supported"].includes(targetOpenClaw.compatRecordStatuses?.[compatRecord])) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
const tests = targetOpenClaw.compatRecordTests?.[compatRecord] ?? [];
|
|
723
|
+
const missingTests = targetOpenClaw.compatRecordMissingTests?.[compatRecord] ?? [];
|
|
724
|
+
return tests.length > 0 && missingTests.length === 0;
|
|
725
|
+
}
|
|
726
|
+
|
|
716
727
|
function classifySdkDeprecations({ fixture, inspection, fixtureReport, warnings, decisions }) {
|
|
717
728
|
const grouped = new Map();
|
|
718
729
|
for (const finding of fixtureReport.sdkDeprecations ?? inspection.sdkDeprecations ?? []) {
|
|
@@ -883,10 +894,22 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
883
894
|
}
|
|
884
895
|
|
|
885
896
|
const sdkExports = new Set(targetOpenClaw.sdkExports);
|
|
886
|
-
const
|
|
897
|
+
const bundledPluginId = bundledOpenClawPluginId(fixture, targetOpenClaw);
|
|
898
|
+
const isBundledFixture = bundledPluginId !== null;
|
|
899
|
+
const privateLocalSdkExports = new Set(targetOpenClaw.privateLocalSdkExports ?? []);
|
|
900
|
+
const unknownImports = fixtureReport.sdkImportDetails.filter(
|
|
901
|
+
(sdkImport) =>
|
|
902
|
+
!sdkExports.has(sdkImport.specifier) &&
|
|
903
|
+
!(isBundledFixture && privateLocalSdkExports.has(sdkImport.specifier)),
|
|
904
|
+
);
|
|
887
905
|
const reservedSdkExports = new Set(targetOpenClaw.reservedSdkExports ?? []);
|
|
888
|
-
const reservedImports = fixtureReport.sdkImportDetails.filter(
|
|
889
|
-
|
|
906
|
+
const reservedImports = fixtureReport.sdkImportDetails.filter(
|
|
907
|
+
(sdkImport) =>
|
|
908
|
+
reservedSdkExports.has(sdkImport.specifier) &&
|
|
909
|
+
!(
|
|
910
|
+
bundledPluginId !== null &&
|
|
911
|
+
targetOpenClaw.reservedSdkExportOwners?.[sdkImport.specifier] === bundledPluginId
|
|
912
|
+
),
|
|
890
913
|
);
|
|
891
914
|
|
|
892
915
|
if (reservedImports.length === 0 && unknownImports.length === 0) {
|
|
@@ -894,7 +917,9 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
894
917
|
fixture: fixture.id,
|
|
895
918
|
code: "sdk-exports-present",
|
|
896
919
|
level: "log",
|
|
897
|
-
message:
|
|
920
|
+
message: isBundledFixture
|
|
921
|
+
? "all observed plugin SDK imports are exported or declared private-local for bundled plugins"
|
|
922
|
+
: "all observed plugin SDK imports exist in target OpenClaw package exports",
|
|
898
923
|
evidence: fixtureReport.sdkImports,
|
|
899
924
|
});
|
|
900
925
|
return;
|
|
@@ -938,6 +963,18 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
938
963
|
}
|
|
939
964
|
}
|
|
940
965
|
|
|
966
|
+
function bundledOpenClawPluginId(fixture, targetOpenClaw) {
|
|
967
|
+
if (fixture.repo !== "local" || !fixture.checkoutPath || !targetOpenClaw.checkoutPath) return null;
|
|
968
|
+
const fixturePath = normalizeRepoPath(fixture.checkoutPath);
|
|
969
|
+
const targetPath = normalizeRepoPath(targetOpenClaw.checkoutPath);
|
|
970
|
+
const relativePath = targetPath === "."
|
|
971
|
+
? fixturePath
|
|
972
|
+
: fixturePath.startsWith(`${targetPath}/`)
|
|
973
|
+
? fixturePath.slice(targetPath.length + 1)
|
|
974
|
+
: "";
|
|
975
|
+
return relativePath.match(/^extensions\/([^/]+)(?:\/|$)/)?.[1] ?? null;
|
|
976
|
+
}
|
|
977
|
+
|
|
941
978
|
function addVersionDerivedFinding({ finding, fixtureReport, targetOpenClaw, breakages, warnings, suggestions }) {
|
|
942
979
|
const compatibility = targetCompatibility(fixtureReport, targetOpenClaw);
|
|
943
980
|
if (!compatibility) {
|
package/src/inspector.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
-
import * as nodeModule from "node:module";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
5
|
import { createCaptureApi } from "./capture-api.js";
|
|
@@ -12,7 +11,7 @@ import { prepareOpenClawTarget, resolveOpenClawTargetVersion } from "./openclaw-
|
|
|
12
11
|
import { resolveProcessLimits, startOwnedProcess } from "./process-profile.js";
|
|
13
12
|
import { buildCompatibilityReport, buildReport } from "./report.js";
|
|
14
13
|
import { inspectSdkDeprecations } from "./sdk-deprecation-rules.js";
|
|
15
|
-
import {
|
|
14
|
+
import { collectRuntimeModuleImports } from "./runtime-imports.js";
|
|
16
15
|
|
|
17
16
|
const pluginFactoryNames = "defineBundledChannelEntry|defineChannelPluginEntry|createChatChannelPlugin|definePluginEntry";
|
|
18
17
|
// Bundlers emit unbound calls as (0, sdk.factory)(...), including inline require receivers.
|
|
@@ -28,6 +27,13 @@ export async function inspectFixtureSet(config, options = {}) {
|
|
|
28
27
|
|
|
29
28
|
export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
30
29
|
const { inspections, failures } = await inspectConfiguredFixtures(config, options);
|
|
30
|
+
const reportConfig = {
|
|
31
|
+
...config,
|
|
32
|
+
fixtures: config.fixtures.map((fixture) => ({
|
|
33
|
+
...fixture,
|
|
34
|
+
checkoutPath: normalizeFixtureCheckoutPath(config, fixture),
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
31
37
|
const targetOpenClaw =
|
|
32
38
|
options.targetOpenClaw ??
|
|
33
39
|
(options.openclawVersion
|
|
@@ -39,7 +45,7 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
|
39
45
|
}));
|
|
40
46
|
|
|
41
47
|
return buildCompatibilityReport({
|
|
42
|
-
config,
|
|
48
|
+
config: reportConfig,
|
|
43
49
|
inspections,
|
|
44
50
|
failures,
|
|
45
51
|
authorFacing: options.authorFacing,
|
|
@@ -57,6 +63,11 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
|
57
63
|
});
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
function normalizeFixtureCheckoutPath(config, fixture) {
|
|
67
|
+
const relative = path.relative(config.rootDir ?? process.cwd(), fixtureCheckoutPath(config, fixture));
|
|
68
|
+
return (relative || ".").replaceAll("\\", "/");
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
async function inspectConfiguredFixtures(config, options = {}) {
|
|
61
72
|
const inspections = [];
|
|
62
73
|
const failures = [];
|
|
@@ -169,7 +180,7 @@ export function inspectSourceText(text, filePath = "source.js") {
|
|
|
169
180
|
...collectDetailedMatches(searchableText, new RegExp(String.raw`\b(${pluginFactoryNames})\s*\(`, "g"), filePath, "name"),
|
|
170
181
|
...collectDetailedMatches(searchableText, compiledFactoryCall, filePath, "name"),
|
|
171
182
|
];
|
|
172
|
-
const sdkImports = collectSdkImports(searchableText, filePath);
|
|
183
|
+
const sdkImports = collectSdkImports(searchableText, filePath, text);
|
|
173
184
|
const sdkDeprecations = inspectSdkDeprecations(searchableText, filePath);
|
|
174
185
|
|
|
175
186
|
return {
|
|
@@ -440,7 +451,7 @@ function collectDetailedMatches(text, regex, filePath, key) {
|
|
|
440
451
|
return details;
|
|
441
452
|
}
|
|
442
453
|
|
|
443
|
-
function collectSdkImports(text, filePath) {
|
|
454
|
+
function collectSdkImports(text, filePath, sourceText) {
|
|
444
455
|
const details = [];
|
|
445
456
|
for (const candidate of text.matchAll(/(?:^|;)[\t ]*(import|export)\b/gm)) {
|
|
446
457
|
const keyword = candidate[1];
|
|
@@ -457,19 +468,8 @@ function collectSdkImports(text, filePath) {
|
|
|
457
468
|
});
|
|
458
469
|
}
|
|
459
470
|
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
for (const [index, match] of dynamicMatches.entries()) {
|
|
463
|
-
if (!runtimeDynamicImports.has(index)) continue;
|
|
464
|
-
const line = lineForOffset(text, match.index ?? 0);
|
|
465
|
-
details.push({
|
|
466
|
-
specifier: match[1],
|
|
467
|
-
file: filePath,
|
|
468
|
-
line,
|
|
469
|
-
ref: `${filePath}:${line}`,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
for (const { specifier, index } of collectCommonJsRequires(text)) {
|
|
471
|
+
// Comment masking can erase executable interpolations after URL-like template text.
|
|
472
|
+
for (const { specifier, index } of collectRuntimeModuleImports(sourceText)) {
|
|
473
473
|
if (specifier !== "openclaw/plugin-sdk" && !specifier.startsWith("openclaw/plugin-sdk/")) continue;
|
|
474
474
|
const line = lineForOffset(text, index);
|
|
475
475
|
details.push({
|
|
@@ -540,50 +540,6 @@ function skipQuotedText(text, quoteIndex, quote) {
|
|
|
540
540
|
return cursor;
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
-
function runtimeDynamicImportIndexes(text, matches) {
|
|
544
|
-
if (matches.length === 0) return new Set();
|
|
545
|
-
const markedImports = matches.map((match, index) => {
|
|
546
|
-
const specifier = match[1];
|
|
547
|
-
const specifierStart = (match.index ?? 0) + match[0].indexOf(specifier);
|
|
548
|
-
return {
|
|
549
|
-
index,
|
|
550
|
-
specifierStart,
|
|
551
|
-
specifierEnd: specifierStart + specifier.length,
|
|
552
|
-
marker: `${specifier}/__plugin_inspector_runtime_import_${index}__`,
|
|
553
|
-
};
|
|
554
|
-
});
|
|
555
|
-
let markedText = text;
|
|
556
|
-
for (const markedImport of markedImports.toReversed()) {
|
|
557
|
-
markedText =
|
|
558
|
-
markedText.slice(0, markedImport.specifierStart) +
|
|
559
|
-
markedImport.marker +
|
|
560
|
-
markedText.slice(markedImport.specifierEnd);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
try {
|
|
564
|
-
const runtimeText = eraseTypeScript(markedText);
|
|
565
|
-
if (runtimeText === null) {
|
|
566
|
-
return new Set(markedImports.map((markedImport) => markedImport.index));
|
|
567
|
-
}
|
|
568
|
-
return new Set(
|
|
569
|
-
markedImports.filter((markedImport) => runtimeText.includes(markedImport.marker)).map((markedImport) => markedImport.index),
|
|
570
|
-
);
|
|
571
|
-
} catch {
|
|
572
|
-
return new Set(markedImports.map((markedImport) => markedImport.index));
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function eraseTypeScript(text) {
|
|
577
|
-
if (typeof nodeModule.stripTypeScriptTypes === "function") {
|
|
578
|
-
return nodeModule.stripTypeScriptTypes(text, { mode: "transform" });
|
|
579
|
-
}
|
|
580
|
-
if (typeof globalThis.Bun?.Transpiler === "function") {
|
|
581
|
-
const transpiler = new globalThis.Bun.Transpiler({ loader: "ts", target: "bun" });
|
|
582
|
-
return transpiler.transformSync(text);
|
|
583
|
-
}
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
543
|
function isTypeOnlyStaticImportClause(clause) {
|
|
588
544
|
clause = clause.trim();
|
|
589
545
|
if (/^type\b/.test(clause)) {
|
package/src/openclaw-target.js
CHANGED
|
@@ -30,17 +30,36 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
30
30
|
return readPackedOpenClawTargetSurface({ rootDir, requestedPaths, ...match });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const { requestedPath, resolvedPath, registryPath } = match;
|
|
33
|
+
const { requestedPath, resolvedPath, registryPath: registryEntryPath } = match;
|
|
34
34
|
const hookTypesPath = path.join(resolvedPath, "src/plugins/hook-types.ts");
|
|
35
35
|
const apiBuilderPath = path.join(resolvedPath, "src/plugins/api-builder.ts");
|
|
36
36
|
const capturedRegistrationPath = path.join(resolvedPath, "src/plugins/captured-registration.ts");
|
|
37
37
|
const currentManifestTypesPath = path.join(resolvedPath, "src/plugins/manifest-types.ts");
|
|
38
38
|
const legacyManifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
|
|
39
39
|
const pluginSdkEntrypointsPath = path.join(resolvedPath, "src/plugin-sdk/entrypoints.ts");
|
|
40
|
+
const privateLocalSdkSubpathsPath = path.join(
|
|
41
|
+
resolvedPath,
|
|
42
|
+
"scripts/lib/plugin-sdk-private-local-only-subpaths.json",
|
|
43
|
+
);
|
|
40
44
|
const packagePath = path.join(resolvedPath, "package.json");
|
|
41
45
|
|
|
42
|
-
const
|
|
46
|
+
const registryEntrySource = await readFile(registryEntryPath, "utf8");
|
|
47
|
+
const registryPath = importsCompatRecords(registryEntrySource)
|
|
48
|
+
? path.join(path.dirname(registryEntryPath), "registry-records.ts")
|
|
49
|
+
: registryEntryPath;
|
|
50
|
+
const registrySource = registryPath === registryEntryPath
|
|
51
|
+
? registryEntrySource
|
|
52
|
+
: await readFile(registryPath, "utf8");
|
|
43
53
|
const compatRecordEntries = parseCompatRecordEntries(registrySource);
|
|
54
|
+
const compatRecordTests = Object.fromEntries(
|
|
55
|
+
compatRecordEntries.map((record) => [record.code, record.tests]),
|
|
56
|
+
);
|
|
57
|
+
const compatRecordMissingTests = Object.fromEntries(
|
|
58
|
+
compatRecordEntries.map((record) => [
|
|
59
|
+
record.code,
|
|
60
|
+
record.tests.filter((testPath) => !existsSync(path.join(resolvedPath, testPath))),
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
44
63
|
const hookTypesSource = existsSync(hookTypesPath) ? await readFile(hookTypesPath, "utf8") : "";
|
|
45
64
|
const hookNames = hookTypesSource ? parseConstStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
|
|
46
65
|
const apiBuilderSource = existsSync(apiBuilderPath) ? await readFile(apiBuilderPath, "utf8") : "";
|
|
@@ -66,12 +85,21 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
66
85
|
const sdkExports = existsSync(packagePath)
|
|
67
86
|
? parsePluginSdkExports(JSON.parse(await readFile(packagePath, "utf8")))
|
|
68
87
|
: [];
|
|
88
|
+
const privateLocalSdkExports = existsSync(privateLocalSdkSubpathsPath)
|
|
89
|
+
? parsePluginSdkSubpathSpecifiers(
|
|
90
|
+
JSON.parse(await readFile(privateLocalSdkSubpathsPath, "utf8")),
|
|
91
|
+
)
|
|
92
|
+
: [];
|
|
69
93
|
const pluginSdkEntrypointsSource = existsSync(pluginSdkEntrypointsPath)
|
|
70
94
|
? await readFile(pluginSdkEntrypointsPath, "utf8")
|
|
71
95
|
: "";
|
|
72
96
|
const reservedSdkExports = pluginSdkEntrypointsSource
|
|
73
97
|
? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "reservedBundledPluginSdkEntrypoints")
|
|
74
98
|
: [];
|
|
99
|
+
const bundledPluginIds = await readBundledPluginIds(resolvedPath);
|
|
100
|
+
const reservedSdkExportOwners = Object.fromEntries(
|
|
101
|
+
reservedSdkExports.map((specifier) => [specifier, resolveBundledSdkOwner(specifier, bundledPluginIds)]),
|
|
102
|
+
);
|
|
75
103
|
const supportedFacadeSdkExports = pluginSdkEntrypointsSource
|
|
76
104
|
? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "supportedBundledFacadeSdkEntrypoints")
|
|
77
105
|
: [];
|
|
@@ -81,12 +109,15 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
81
109
|
|
|
82
110
|
return {
|
|
83
111
|
configuredPath: requestedPath,
|
|
112
|
+
checkoutPath: relativePath(rootDir, resolvedPath) || ".",
|
|
84
113
|
searchedPaths: requestedPaths,
|
|
85
114
|
status: "ok",
|
|
86
115
|
compatRegistryPath: relativePath(rootDir, registryPath),
|
|
87
116
|
compatRecordCount: compatRecordEntries.length,
|
|
88
117
|
compatRecords: compatRecordEntries.map((record) => record.code).sort(),
|
|
89
118
|
compatRecordStatuses: Object.fromEntries(compatRecordEntries.map((record) => [record.code, record.status])),
|
|
119
|
+
compatRecordTests,
|
|
120
|
+
compatRecordMissingTests,
|
|
90
121
|
hookTypesPath: existsSync(hookTypesPath) ? relativePath(rootDir, hookTypesPath) : null,
|
|
91
122
|
hookNameCount: hookNames.length,
|
|
92
123
|
hookNames,
|
|
@@ -99,11 +130,14 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
99
130
|
packagePath: existsSync(packagePath) ? relativePath(rootDir, packagePath) : null,
|
|
100
131
|
sdkExportCount: sdkExports.length,
|
|
101
132
|
sdkExports,
|
|
133
|
+
privateLocalSdkExportCount: privateLocalSdkExports.length,
|
|
134
|
+
privateLocalSdkExports,
|
|
102
135
|
pluginSdkEntrypointsPath: existsSync(pluginSdkEntrypointsPath)
|
|
103
136
|
? relativePath(rootDir, pluginSdkEntrypointsPath)
|
|
104
137
|
: null,
|
|
105
138
|
reservedSdkExportCount: reservedSdkExports.length,
|
|
106
139
|
reservedSdkExports,
|
|
140
|
+
reservedSdkExportOwners,
|
|
107
141
|
supportedFacadeSdkExports,
|
|
108
142
|
publicPluginOwnedSdkExports,
|
|
109
143
|
manifestTypesPath: existsSync(manifestTypesPath) ? relativePath(rootDir, manifestTypesPath) : null,
|
|
@@ -121,6 +155,23 @@ export function openClawTargetPathCandidates(manifest, configuredPath) {
|
|
|
121
155
|
return unique([manifest?.openclaw?.defaultCheckoutPath, ...defaultOpenClawCheckoutPaths].filter(Boolean));
|
|
122
156
|
}
|
|
123
157
|
|
|
158
|
+
function importsCompatRecords(source) {
|
|
159
|
+
// Keep quoted text atomic and discard comments before recognizing the fixed delegation.
|
|
160
|
+
const tokens = (source.match(/\/\/[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$)|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`|[$A-Z_a-z][$\w]*|[^\s]/g) ?? [])
|
|
161
|
+
.filter((token) => !token.startsWith("//") && !token.startsWith("/*"));
|
|
162
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
163
|
+
if (tokens[index] !== "import" || tokens[index + 1] !== "{") continue;
|
|
164
|
+
const end = tokens.indexOf("}", index + 2);
|
|
165
|
+
if (end === -1 || tokens[end + 1] !== "from") continue;
|
|
166
|
+
if (!['"./registry-records.js"', "'./registry-records.js'"].includes(tokens[end + 2])) continue;
|
|
167
|
+
const bindings = tokens.slice(index + 2, end).join(" ").split(",");
|
|
168
|
+
if (bindings.some((binding) => /^PLUGIN_COMPAT_RECORDS(?:\s+as\s+[$A-Z_a-z][$\w]*)?$/.test(binding.trim()))) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
124
175
|
export function parseCompatRecordEntries(source) {
|
|
125
176
|
const entries = [];
|
|
126
177
|
let cursor = 0;
|
|
@@ -132,8 +183,14 @@ export function parseCompatRecordEntries(source) {
|
|
|
132
183
|
|
|
133
184
|
const statusProperty = readStringProperty(source, "status", codeProperty.end);
|
|
134
185
|
if (statusProperty) {
|
|
135
|
-
|
|
136
|
-
|
|
186
|
+
const nextCodeProperty = readStringProperty(source, "code", statusProperty.end);
|
|
187
|
+
const recordEnd = nextCodeProperty?.propertyIndex ?? source.length;
|
|
188
|
+
entries.push({
|
|
189
|
+
code: codeProperty.value,
|
|
190
|
+
status: statusProperty.value,
|
|
191
|
+
tests: readStringArrayProperty(source, "tests", statusProperty.end, recordEnd),
|
|
192
|
+
});
|
|
193
|
+
cursor = recordEnd;
|
|
137
194
|
} else {
|
|
138
195
|
cursor = codeProperty.end;
|
|
139
196
|
}
|
|
@@ -157,7 +214,21 @@ function readStringProperty(source, property, fromIndex) {
|
|
|
157
214
|
if (!isQuote(source[quoteIndex])) {
|
|
158
215
|
return null;
|
|
159
216
|
}
|
|
160
|
-
|
|
217
|
+
const value = readQuotedValue(source, quoteIndex);
|
|
218
|
+
return value ? { ...value, propertyIndex } : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readStringArrayProperty(source, property, fromIndex, toIndex) {
|
|
222
|
+
const propertyIndex = findProperty(source, property, fromIndex);
|
|
223
|
+
if (propertyIndex === -1 || propertyIndex >= toIndex) return [];
|
|
224
|
+
const colonIndex = source.indexOf(":", propertyIndex + property.length);
|
|
225
|
+
const openIndex = source.indexOf("[", colonIndex + 1);
|
|
226
|
+
if (colonIndex === -1 || openIndex === -1 || openIndex >= toIndex) return [];
|
|
227
|
+
const closeIndex = source.indexOf("]", openIndex + 1);
|
|
228
|
+
if (closeIndex === -1 || closeIndex >= toIndex) return [];
|
|
229
|
+
return unique(
|
|
230
|
+
[...source.slice(openIndex + 1, closeIndex).matchAll(/["']([^"']+)["']/g)].map((match) => match[1]),
|
|
231
|
+
).sort();
|
|
161
232
|
}
|
|
162
233
|
|
|
163
234
|
function findProperty(source, property, fromIndex) {
|
|
@@ -298,6 +369,7 @@ async function readPackedOpenClawTargetSurface({ rootDir, requestedPaths, reques
|
|
|
298
369
|
|
|
299
370
|
return {
|
|
300
371
|
configuredPath: requestedPath,
|
|
372
|
+
checkoutPath: relativePath(rootDir, resolvedPath) || ".",
|
|
301
373
|
searchedPaths: requestedPaths,
|
|
302
374
|
status: "ok",
|
|
303
375
|
version: packageJson.version ?? null,
|
|
@@ -305,6 +377,8 @@ async function readPackedOpenClawTargetSurface({ rootDir, requestedPaths, reques
|
|
|
305
377
|
compatRecordCount: 0,
|
|
306
378
|
compatRecords: [],
|
|
307
379
|
compatRecordStatuses: {},
|
|
380
|
+
compatRecordTests: {},
|
|
381
|
+
compatRecordMissingTests: {},
|
|
308
382
|
hookTypesPath: hookDeclaration ? relativePath(rootDir, hookDeclaration.filePath) : null,
|
|
309
383
|
hookNameCount: hookNames.length,
|
|
310
384
|
hookNames,
|
|
@@ -317,9 +391,12 @@ async function readPackedOpenClawTargetSurface({ rootDir, requestedPaths, reques
|
|
|
317
391
|
packagePath: relativePath(rootDir, packagePath),
|
|
318
392
|
sdkExportCount: sdkExports.length,
|
|
319
393
|
sdkExports,
|
|
394
|
+
privateLocalSdkExportCount: 0,
|
|
395
|
+
privateLocalSdkExports: [],
|
|
320
396
|
pluginSdkEntrypointsPath: null,
|
|
321
397
|
reservedSdkExportCount: 0,
|
|
322
398
|
reservedSdkExports: [],
|
|
399
|
+
reservedSdkExportOwners: {},
|
|
323
400
|
supportedFacadeSdkExports: [],
|
|
324
401
|
publicPluginOwnedSdkExports: [],
|
|
325
402
|
manifestTypesPath: manifestDeclaration ? relativePath(rootDir, manifestDeclaration.filePath) : null,
|
|
@@ -443,15 +520,20 @@ function parseStringUnion(source, typeName) {
|
|
|
443
520
|
function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status }) {
|
|
444
521
|
return {
|
|
445
522
|
configuredPath,
|
|
523
|
+
checkoutPath: null,
|
|
446
524
|
searchedPaths,
|
|
447
525
|
status,
|
|
448
526
|
compatRecords: [],
|
|
449
527
|
compatRecordStatuses: {},
|
|
528
|
+
compatRecordTests: {},
|
|
529
|
+
compatRecordMissingTests: {},
|
|
450
530
|
hookNames: [],
|
|
451
531
|
apiRegistrars: [],
|
|
452
532
|
capturedRegistrars: [],
|
|
453
533
|
sdkExports: [],
|
|
534
|
+
privateLocalSdkExports: [],
|
|
454
535
|
reservedSdkExports: [],
|
|
536
|
+
reservedSdkExportOwners: {},
|
|
455
537
|
supportedFacadeSdkExports: [],
|
|
456
538
|
publicPluginOwnedSdkExports: [],
|
|
457
539
|
manifestFields: [],
|
|
@@ -459,10 +541,33 @@ function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status
|
|
|
459
541
|
};
|
|
460
542
|
}
|
|
461
543
|
|
|
544
|
+
async function readBundledPluginIds(openClawRoot) {
|
|
545
|
+
const extensionsRoot = path.join(openClawRoot, "extensions");
|
|
546
|
+
if (!existsSync(extensionsRoot)) return [];
|
|
547
|
+
return (await readdir(extensionsRoot, { withFileTypes: true }))
|
|
548
|
+
.filter((entry) => entry.isDirectory())
|
|
549
|
+
.map((entry) => entry.name)
|
|
550
|
+
.sort((left, right) => right.length - left.length || left.localeCompare(right));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function resolveBundledSdkOwner(specifier, pluginIds) {
|
|
554
|
+
const entrypoint = specifier.slice("openclaw/plugin-sdk/".length);
|
|
555
|
+
return pluginIds.find((pluginId) => entrypoint === pluginId || entrypoint.startsWith(`${pluginId}-`)) ?? null;
|
|
556
|
+
}
|
|
557
|
+
|
|
462
558
|
export function parsePluginSdkEntrypointSpecifiers(source, exportName) {
|
|
463
559
|
return parseExportedStringArray(source, exportName).map((entrypoint) => `openclaw/plugin-sdk/${entrypoint}`).sort();
|
|
464
560
|
}
|
|
465
561
|
|
|
562
|
+
function parsePluginSdkSubpathSpecifiers(value) {
|
|
563
|
+
if (!Array.isArray(value)) return [];
|
|
564
|
+
return unique(
|
|
565
|
+
value
|
|
566
|
+
.filter((entrypoint) => typeof entrypoint === "string" && !entrypoint.includes("/"))
|
|
567
|
+
.map((entrypoint) => `openclaw/plugin-sdk/${entrypoint}`),
|
|
568
|
+
).sort();
|
|
569
|
+
}
|
|
570
|
+
|
|
466
571
|
function parseCapturedRegistrars(source) {
|
|
467
572
|
return unique([...source.matchAll(/^\s*(register[A-Za-z0-9]+)\s*\(/gm)].map((match) => match[1])).sort();
|
|
468
573
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as nodeModule from "node:module";
|
|
2
|
+
import { parse } from "acorn";
|
|
3
|
+
import { analyze } from "eslint-scope";
|
|
4
|
+
|
|
5
|
+
const literalModuleImport = /(?<![$\w.])(?:(?:const|let|var)\s+(?:\{[^{}]*\}|[$A-Z_a-z][$\w]*)\s*=\s*)?(?<kind>require|import)\s*\(\s*(?<quote>["'`])(?<specifier>[^"'`\\\r\n]+)\k<quote>/dg;
|
|
6
|
+
|
|
7
|
+
export function collectRuntimeModuleImports(text) {
|
|
8
|
+
// Mark literal occurrences without interpreting quotes or regexes; the AST owns real calls.
|
|
9
|
+
const entries = [...text.matchAll(literalModuleImport)].map(moduleImportEntry).filter(Boolean);
|
|
10
|
+
if (entries.length === 0) return entries;
|
|
11
|
+
const { runtimeText, imports } = classifyRuntimeImports(text, entries);
|
|
12
|
+
let ast;
|
|
13
|
+
let scopes;
|
|
14
|
+
try {
|
|
15
|
+
// Accept both source modules and CommonJS wrappers without changing their runtime.
|
|
16
|
+
ast = parse(runtimeText, {
|
|
17
|
+
ecmaVersion: "latest", ranges: true, sourceType: "script",
|
|
18
|
+
allowAwaitOutsideFunction: true, allowReturnOutsideFunction: true, allowImportExportEverywhere: true,
|
|
19
|
+
});
|
|
20
|
+
scopes = analyze(ast, {
|
|
21
|
+
ecmaVersion: 2026,
|
|
22
|
+
sourceType: ast.body.some((node) => /^(?:Import|Export).*Declaration$/.test(node.type)) ? "module" : "commonjs",
|
|
23
|
+
});
|
|
24
|
+
} catch {
|
|
25
|
+
// Keep known imports if syntax is incomplete/unsupported; never guess namespace exports.
|
|
26
|
+
// Type erasure has already classified each occurrence, even when AST analysis fails.
|
|
27
|
+
const retained = new Set(imports.map(({ entry }) => entry.specifierStart));
|
|
28
|
+
return [...scanLiteralModuleImports(text)].filter((entry) => retained.has(entry.specifierStart))
|
|
29
|
+
.map((entry) => ({ ...entry, names: new Set() }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parents = new Map();
|
|
33
|
+
const nodes = [];
|
|
34
|
+
visit(ast);
|
|
35
|
+
function visit(node, parent) {
|
|
36
|
+
if (!node || typeof node.type !== "string") return;
|
|
37
|
+
parents.set(node, parent);
|
|
38
|
+
nodes.push(node);
|
|
39
|
+
for (const value of Object.values(node)) {
|
|
40
|
+
if (Array.isArray(value)) value.forEach((child) => visit(child, node));
|
|
41
|
+
else if (value && typeof value === "object") visit(value, node);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const byMarker = new Map(imports.map(({ marker, entry }) => [marker, entry]));
|
|
45
|
+
const result = [];
|
|
46
|
+
for (const node of nodes) {
|
|
47
|
+
const source = node.type === "ImportExpression" ? node.source
|
|
48
|
+
: node.type === "CallExpression" && node.callee.type === "Identifier"
|
|
49
|
+
&& node.callee.name === "require" && node.arguments.length === 1 ? node.arguments[0] : null;
|
|
50
|
+
const specifier = source?.type === "Literal" ? source.value
|
|
51
|
+
: source?.type === "TemplateLiteral" && source.expressions.length === 0 ? source.quasis[0].value.cooked : null;
|
|
52
|
+
const entry = byMarker.get(specifier);
|
|
53
|
+
if (!entry) continue;
|
|
54
|
+
const names = new Set();
|
|
55
|
+
result.push({ ...entry, names });
|
|
56
|
+
const access = parents.get(node);
|
|
57
|
+
const call = parents.get(access);
|
|
58
|
+
if (node.type === "ImportExpression" && access?.type === "MemberExpression" && access.object === node
|
|
59
|
+
&& !access.computed && access.property.name === "then" && call?.type === "CallExpression" && call.callee === access) {
|
|
60
|
+
const callback = call.arguments[0];
|
|
61
|
+
if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") {
|
|
62
|
+
collectBindingNames(callback.params[0], callback, names);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Promise methods belong to import(), not its awaited module namespace.
|
|
66
|
+
const module = entry.kind === "require" ? node
|
|
67
|
+
: parents.get(node)?.type === "AwaitExpression" ? parents.get(node) : null;
|
|
68
|
+
if (!module) continue;
|
|
69
|
+
const parent = parents.get(module);
|
|
70
|
+
if (parent?.type === "MemberExpression" && parent.object === module && !parent.computed && parent.property.type === "Identifier") {
|
|
71
|
+
names.add(parent.property.name);
|
|
72
|
+
}
|
|
73
|
+
if (parent?.type === "VariableDeclarator" && parent.init === module) collectBindingNames(parent.id, parent, names);
|
|
74
|
+
}
|
|
75
|
+
return result.sort((a, b) => a.index - b.index);
|
|
76
|
+
|
|
77
|
+
function collectBindingNames(binding, owner, names) {
|
|
78
|
+
if (binding?.type === "ObjectPattern") {
|
|
79
|
+
for (const property of binding.properties) {
|
|
80
|
+
if (property.type === "Property" && !property.computed && property.key.type === "Identifier") {
|
|
81
|
+
names.add(property.key.name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else if (binding?.type === "Identifier") {
|
|
85
|
+
// Resolve references to this declaration, including closures but excluding shadowed names.
|
|
86
|
+
for (const variable of scopes.getDeclaredVariables(owner)) {
|
|
87
|
+
if (!variable.identifiers.includes(binding)) continue;
|
|
88
|
+
for (const reference of variable.references) {
|
|
89
|
+
const access = parents.get(reference.identifier);
|
|
90
|
+
if (access?.type === "MemberExpression" && access.object === reference.identifier && !access.computed && access.property.type === "Identifier") {
|
|
91
|
+
names.add(access.property.name);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function* scanLiteralModuleImports(text) {
|
|
100
|
+
const quotedOrComment = /\/\/[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$)|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'/;
|
|
101
|
+
const code = new RegExp(quotedOrComment.source + "|" + literalModuleImport.source + "|[`{}]", "dg");
|
|
102
|
+
const template = /\\[\s\S]|`|\$\{/g;
|
|
103
|
+
const templateDepths = [];
|
|
104
|
+
let inTemplateText = false;
|
|
105
|
+
let cursor = 0;
|
|
106
|
+
// Skip quoted/comment text, but scan executable template interpolations.
|
|
107
|
+
while (cursor < text.length) {
|
|
108
|
+
const pattern = inTemplateText ? template : code;
|
|
109
|
+
pattern.lastIndex = cursor;
|
|
110
|
+
const match = pattern.exec(text);
|
|
111
|
+
if (!match) break;
|
|
112
|
+
cursor = pattern.lastIndex;
|
|
113
|
+
if (inTemplateText) {
|
|
114
|
+
if (match[0] === "`") {
|
|
115
|
+
templateDepths.pop();
|
|
116
|
+
inTemplateText = false;
|
|
117
|
+
} else if (match[0] === "${") {
|
|
118
|
+
templateDepths[templateDepths.length - 1] = 1;
|
|
119
|
+
inTemplateText = false;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (match[0] === "`") {
|
|
124
|
+
templateDepths.push(0);
|
|
125
|
+
inTemplateText = true;
|
|
126
|
+
} else if (templateDepths.length && match[0] === "{") {
|
|
127
|
+
templateDepths[templateDepths.length - 1] += 1;
|
|
128
|
+
} else if (templateDepths.length && match[0] === "}") {
|
|
129
|
+
inTemplateText = --templateDepths[templateDepths.length - 1] === 0;
|
|
130
|
+
}
|
|
131
|
+
const entry = moduleImportEntry(match);
|
|
132
|
+
if (entry) yield entry;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function moduleImportEntry(match) {
|
|
137
|
+
const groups = match.groups;
|
|
138
|
+
if (!groups?.specifier || (groups.quote === "`" && groups.specifier.includes("${"))) return null;
|
|
139
|
+
return {
|
|
140
|
+
specifier: groups.specifier,
|
|
141
|
+
specifierStart: match.indices.groups.specifier[0],
|
|
142
|
+
kind: groups.kind,
|
|
143
|
+
index: groups.kind === "import" ? match.indices.groups.kind[0] : match.index,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function classifyRuntimeImports(text, entries) {
|
|
148
|
+
const markedImports = entries.map((entry, index) => {
|
|
149
|
+
const { specifier, specifierStart } = entry;
|
|
150
|
+
return {
|
|
151
|
+
entry,
|
|
152
|
+
specifierStart,
|
|
153
|
+
specifierEnd: specifierStart + specifier.length,
|
|
154
|
+
marker: `${specifier}__plugin_inspector_runtime_import_${index}__`,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
let markedText = text;
|
|
158
|
+
for (const markedImport of markedImports.toReversed()) {
|
|
159
|
+
markedText =
|
|
160
|
+
markedText.slice(0, markedImport.specifierStart) +
|
|
161
|
+
markedImport.marker +
|
|
162
|
+
markedText.slice(markedImport.specifierEnd);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let runtimeText = null;
|
|
166
|
+
try {
|
|
167
|
+
runtimeText = eraseTypeScript(markedText);
|
|
168
|
+
} catch {
|
|
169
|
+
// Unsupported or incomplete TypeScript cannot prove an import is type-only.
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
runtimeText: runtimeText ?? markedText,
|
|
173
|
+
imports: runtimeText === null ? markedImports : markedImports.filter(({ marker }) => runtimeText.includes(marker)),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function eraseTypeScript(text) {
|
|
178
|
+
if (typeof nodeModule.stripTypeScriptTypes === "function") {
|
|
179
|
+
try {
|
|
180
|
+
return nodeModule.stripTypeScriptTypes(text, { mode: "transform" });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error?.code !== "ERR_INVALID_ARG_VALUE") throw error;
|
|
183
|
+
return nodeModule.stripTypeScriptTypes(text, { mode: "strip" });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (typeof globalThis.Bun?.Transpiler === "function") {
|
|
187
|
+
const transpiler = new globalThis.Bun.Transpiler({ loader: "ts", target: "bun" });
|
|
188
|
+
return transpiler.transformSync(text);
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
@@ -81,12 +81,6 @@ function expectedRuntimeCaptureKeys(finding) {
|
|
|
81
81
|
if (finding.code === "runtime-tool-capture") {
|
|
82
82
|
return ["registration:registerTool"];
|
|
83
83
|
}
|
|
84
|
-
if (finding.code === "conversation-access-hook") {
|
|
85
|
-
return names.map((name) => `hook:${name}`);
|
|
86
|
-
}
|
|
87
|
-
if (finding.code === "before-tool-call-probe") {
|
|
88
|
-
return ["hook:before_tool_call"];
|
|
89
|
-
}
|
|
90
84
|
return [];
|
|
91
85
|
}
|
|
92
86
|
|
package/src/sdk-mock.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, readdir, readFile, realpath, writeFile } from "node:fs/promises"
|
|
|
2
2
|
import * as nodeModule from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { collectRuntimeModuleImports } from "./runtime-imports.js";
|
|
5
6
|
|
|
6
7
|
const SOURCE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]);
|
|
7
8
|
const SKIP_DIRS = new Set([".git", "coverage", "node_modules", "reports"]);
|
|
@@ -164,6 +165,14 @@ export const mockSdkSubpathExports = {
|
|
|
164
165
|
"normalizeSecretInputString",
|
|
165
166
|
],
|
|
166
167
|
"plugin-runtime": ["createLoggerBackedRuntime", "createSubsystemLogger"],
|
|
168
|
+
"lazy-runtime": [
|
|
169
|
+
"createLazyRuntimeModule",
|
|
170
|
+
"createLazyRuntimeMethod",
|
|
171
|
+
"createLazyRuntimeMethodBinder",
|
|
172
|
+
"createLazyRuntimeNamedExport",
|
|
173
|
+
"createLazyRuntimeSurface",
|
|
174
|
+
],
|
|
175
|
+
"error-runtime": ["formatErrorMessage"],
|
|
167
176
|
"secret-input": [
|
|
168
177
|
"buildOptionalSecretInputSchema",
|
|
169
178
|
"buildSecretInputArraySchema",
|
|
@@ -430,62 +439,12 @@ function parseModuleImports(text) {
|
|
|
430
439
|
for (const match of text.matchAll(/\bimport\s+["']([^"']+)["']/g)) {
|
|
431
440
|
entries.push({ specifier: match[1], names: new Set() });
|
|
432
441
|
}
|
|
433
|
-
for (const { specifier,
|
|
434
|
-
|
|
435
|
-
if (binding?.startsWith("{")) {
|
|
436
|
-
for (const part of binding.slice(1, -1).split(",")) {
|
|
437
|
-
const name = part.split(/[:=]/)[0].trim();
|
|
438
|
-
if (isValidExportName(name)) names.add(name);
|
|
439
|
-
}
|
|
440
|
-
} else if (binding) {
|
|
441
|
-
const escaped = binding.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
442
|
-
for (const access of text.matchAll(new RegExp(`(?<![$\\w])${escaped}\\s*\\.\\s*([$\\w]+)`, "g"))) {
|
|
443
|
-
names.add(access[1]);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
if (member) names.add(member);
|
|
447
|
-
entries.push({ specifier, names, require: true });
|
|
442
|
+
for (const { specifier, names, kind } of collectRuntimeModuleImports(text)) {
|
|
443
|
+
entries.push({ specifier, names, require: kind === "require" });
|
|
448
444
|
}
|
|
449
445
|
return entries;
|
|
450
446
|
}
|
|
451
447
|
|
|
452
|
-
export function* collectCommonJsRequires(text) {
|
|
453
|
-
const code = /\/\/[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$)|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|(?<![$\w.])(?:(?:const|let|var)\s+(\{[^{}]*\}|[$A-Z_a-z][$\w]*)\s*=\s*)?require\s*\(\s*["']([^"']+)["']\s*\)(?:\s*\.\s*([$A-Z_a-z][$\w]*))?|[`{}]/g;
|
|
454
|
-
const template = /\\[\s\S]|`|\$\{/g;
|
|
455
|
-
const templateDepths = [];
|
|
456
|
-
let inTemplateText = false;
|
|
457
|
-
let cursor = 0;
|
|
458
|
-
// Skip quoted/comment text, but scan executable template interpolations.
|
|
459
|
-
while (cursor < text.length) {
|
|
460
|
-
const pattern = inTemplateText ? template : code;
|
|
461
|
-
pattern.lastIndex = cursor;
|
|
462
|
-
const match = pattern.exec(text);
|
|
463
|
-
if (!match) break;
|
|
464
|
-
cursor = pattern.lastIndex;
|
|
465
|
-
if (inTemplateText) {
|
|
466
|
-
if (match[0] === "`") {
|
|
467
|
-
templateDepths.pop();
|
|
468
|
-
inTemplateText = false;
|
|
469
|
-
} else if (match[0] === "${") {
|
|
470
|
-
templateDepths[templateDepths.length - 1] = 1;
|
|
471
|
-
inTemplateText = false;
|
|
472
|
-
}
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
if (match[0] === "`") {
|
|
476
|
-
templateDepths.push(0);
|
|
477
|
-
inTemplateText = true;
|
|
478
|
-
} else if (templateDepths.length && match[0] === "{") {
|
|
479
|
-
templateDepths[templateDepths.length - 1] += 1;
|
|
480
|
-
} else if (templateDepths.length && match[0] === "}") {
|
|
481
|
-
inTemplateText = --templateDepths[templateDepths.length - 1] === 0;
|
|
482
|
-
}
|
|
483
|
-
if (match[2]) {
|
|
484
|
-
yield { specifier: match[2], binding: match[1], member: match[3], index: match.index };
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
448
|
function isTypeOnlyImportOrExport(statement, clause) {
|
|
490
449
|
return /^\s*import\s+type\b/u.test(statement) || /^\s*export\s+type\b/u.test(statement) || /^\s*type\b/u.test(clause);
|
|
491
450
|
}
|
|
@@ -741,6 +700,11 @@ function isValidExportName(name) {
|
|
|
741
700
|
}
|
|
742
701
|
|
|
743
702
|
function genericExportStatement(name) {
|
|
703
|
+
if (name === "normalizeOptionalString") {
|
|
704
|
+
// Optional values stay absent; an empty string invents explicit input in
|
|
705
|
+
// callers that distinguish undefined from a configured policy value.
|
|
706
|
+
return 'export function normalizeOptionalString(value) { return typeof value === "string" ? value.trim() || undefined : undefined; }';
|
|
707
|
+
}
|
|
744
708
|
if (name === "isRecord") {
|
|
745
709
|
return "export function isRecord(value) { return isPlainObject(value); }";
|
|
746
710
|
}
|
|
@@ -1758,6 +1722,37 @@ export function resolveRuntimeEnv(env = {}) {
|
|
|
1758
1722
|
return createRuntimeEnv(env);
|
|
1759
1723
|
}
|
|
1760
1724
|
|
|
1725
|
+
export function createLazyRuntimeSurface(importer, select) {
|
|
1726
|
+
let promise;
|
|
1727
|
+
const load = () => {
|
|
1728
|
+
// SDK runtime imports retain the same promise, including rejection, until clear().
|
|
1729
|
+
promise ??= Promise.resolve().then(() => importer().then(select));
|
|
1730
|
+
return promise;
|
|
1731
|
+
};
|
|
1732
|
+
load.peek = () => promise;
|
|
1733
|
+
load.clear = () => { promise = undefined; };
|
|
1734
|
+
return load;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
export function createLazyRuntimeModule(importer) {
|
|
1738
|
+
return createLazyRuntimeSurface(importer, (module) => module);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
export function createLazyRuntimeNamedExport(importer, key) {
|
|
1742
|
+
return createLazyRuntimeSurface(importer, (module) => module[key]);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
export function createLazyRuntimeMethod(load, select) {
|
|
1746
|
+
return async (...args) => {
|
|
1747
|
+
const method = select(await load());
|
|
1748
|
+
return await method(...args);
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
export function createLazyRuntimeMethodBinder(load) {
|
|
1753
|
+
return (select) => createLazyRuntimeMethod(load, select);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1761
1756
|
export function createLoggerBackedRuntime(logger = console) {
|
|
1762
1757
|
return { logger };
|
|
1763
1758
|
}
|
package/src/synthetic-probes.js
CHANGED
|
@@ -635,6 +635,7 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
|
635
635
|
try {
|
|
636
636
|
const result = await runCapturedProbes(capture, {
|
|
637
637
|
...options, hookEvents, hookContexts, timeoutMs, controller, signal: controller.signal,
|
|
638
|
+
gatewayConfig: options.apiOptions?.config ?? {},
|
|
638
639
|
});
|
|
639
640
|
if (options.signal?.aborted) throw controller.signal.reason;
|
|
640
641
|
return result;
|
|
@@ -804,6 +805,16 @@ async function runRegistrationProbes(entry, retainedEntry, captureIndex, options
|
|
|
804
805
|
if (!descriptor || typeof descriptor !== "object") {
|
|
805
806
|
return [blockedResult(entry, captureIndex, "captured registration has no object descriptor")];
|
|
806
807
|
}
|
|
808
|
+
const method = descriptor.method ?? descriptor.name;
|
|
809
|
+
if (entry.name === "registerGatewayMethod" && Object.hasOwn(options.gatewayMethodPrerequisites ?? {}, method)) {
|
|
810
|
+
const reason = options.gatewayMethodPrerequisites[method];
|
|
811
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
812
|
+
throw new TypeError(`Gateway probe prerequisite for ${method} must be a non-empty string`);
|
|
813
|
+
}
|
|
814
|
+
// A method needing host state or live credentials cannot be exercised with
|
|
815
|
+
// the default empty request. Record the missing prerequisite before calling it.
|
|
816
|
+
return [{ ...blockedResult(entry, captureIndex, reason), method }];
|
|
817
|
+
}
|
|
807
818
|
if (profile.option && options[profile.option] !== true) {
|
|
808
819
|
return [blockedResult(entry, captureIndex, `captured registration requires ${profile.option}=true`)];
|
|
809
820
|
}
|
|
@@ -1022,7 +1033,7 @@ function gatewayProbeArgs(event, options = {}) {
|
|
|
1022
1033
|
req: { type: "req", id: "fixture-request", method: event.method ?? "fixture.gateway.method", params: event.params },
|
|
1023
1034
|
client: null,
|
|
1024
1035
|
isWebchatConnect: () => false,
|
|
1025
|
-
context: { source: event.source, logger: console },
|
|
1036
|
+
context: { source: event.source, logger: console, getRuntimeConfig: () => options.gatewayConfig },
|
|
1026
1037
|
signal: options.signal,
|
|
1027
1038
|
},
|
|
1028
1039
|
];
|