@openclawbrain/openclaw 0.3.4 → 0.3.6
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/README.md +57 -4
- package/dist/extension/runtime-guard.js +6 -3
- package/dist/src/attachment-truth.js +7 -2
- package/dist/src/cli.js +143 -83
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +30 -6
- package/dist/src/openclaw-hook-truth.d.ts +8 -0
- package/dist/src/openclaw-hook-truth.js +134 -28
- package/dist/src/openclaw-plugin-install.d.ts +40 -0
- package/dist/src/openclaw-plugin-install.js +282 -0
- package/dist/src/resolve-activation-root.js +14 -9
- package/dist/src/shadow-extension-proof.d.ts +3 -0
- package/dist/src/shadow-extension-proof.js +23 -23
- package/extension/runtime-guard.ts +12 -9
- package/openclaw.plugin.json +11 -0
- package/package.json +16 -7
package/dist/src/index.js
CHANGED
|
@@ -1398,13 +1398,30 @@ export function loadWatchTeacherSnapshotState(snapshotPath) {
|
|
|
1398
1398
|
export function persistWatchTeacherSnapshot(snapshotPath, input) {
|
|
1399
1399
|
const persistedAt = new Date().toISOString();
|
|
1400
1400
|
const canonicalSnapshot = loadAsyncTeacherLiveLoopSnapshotFromValue(input.snapshot);
|
|
1401
|
+
const resolvedScanRoot = path.resolve(input.scanRoot);
|
|
1402
|
+
const canonicalRuntime = canonicalSnapshot.runtime ?? {
|
|
1403
|
+
startedAt: input.lastRunAt,
|
|
1404
|
+
lastHeartbeatAt: input.lastRunAt,
|
|
1405
|
+
lastScanAt: input.lastRunAt,
|
|
1406
|
+
scanRoot: resolvedScanRoot,
|
|
1407
|
+
lastAppliedMaterializationJobId: null
|
|
1408
|
+
};
|
|
1409
|
+
canonicalSnapshot.runtime = {
|
|
1410
|
+
...canonicalRuntime,
|
|
1411
|
+
startedAt: canonicalRuntime.startedAt ?? input.lastRunAt,
|
|
1412
|
+
lastHeartbeatAt: input.lastRunAt,
|
|
1413
|
+
lastScanAt: input.lastRunAt,
|
|
1414
|
+
scanRoot: typeof canonicalRuntime.scanRoot === "string" && canonicalRuntime.scanRoot.trim().length > 0
|
|
1415
|
+
? canonicalRuntime.scanRoot
|
|
1416
|
+
: resolvedScanRoot
|
|
1417
|
+
};
|
|
1401
1418
|
const payload = {
|
|
1402
1419
|
contract: "openclaw_watch_teacher_snapshot.v1",
|
|
1403
1420
|
runtimeOwner: "openclaw",
|
|
1404
1421
|
updatedAt: persistedAt,
|
|
1405
1422
|
lastRunAt: input.lastRunAt,
|
|
1406
1423
|
pollIntervalSeconds: input.pollIntervalSeconds,
|
|
1407
|
-
scanRoot:
|
|
1424
|
+
scanRoot: resolvedScanRoot,
|
|
1408
1425
|
sessionTailCursorPath: path.resolve(input.sessionTailCursorPath),
|
|
1409
1426
|
sessionTailCursorUpdatedAt: input.sessionTailCursorUpdatedAt,
|
|
1410
1427
|
sessionTailSessionsTracked: input.sessionTailSessionsTracked,
|
|
@@ -6131,7 +6148,7 @@ function buildOperatorFindings(report) {
|
|
|
6131
6148
|
findings.push({ severity, code, summary, detail });
|
|
6132
6149
|
};
|
|
6133
6150
|
if (report.hook.desynced) {
|
|
6134
|
-
push("fail", "hook_desynced", "profile hook is
|
|
6151
|
+
push("fail", "hook_desynced", "profile hook is present but not loadable", report.hook.detail);
|
|
6135
6152
|
}
|
|
6136
6153
|
else if (report.attachmentTruth.state === "not_attached") {
|
|
6137
6154
|
push("fail", "current_profile_not_attached", "current profile is not attached", report.attachmentTruth.detail);
|
|
@@ -6376,8 +6393,8 @@ function summarizeOperatorAttachmentTruth(report) {
|
|
|
6376
6393
|
watchOnly: false,
|
|
6377
6394
|
activationRoot: report.activationRoot,
|
|
6378
6395
|
servingSlot: report.active === null ? "none" : "active",
|
|
6379
|
-
detail: report.hook.
|
|
6380
|
-
? "current profile hook files exist, but OpenClaw will not load them until the
|
|
6396
|
+
detail: report.hook.loadability === "blocked"
|
|
6397
|
+
? "current profile hook files exist, but OpenClaw will not load them until the installed hook is repaired"
|
|
6381
6398
|
: "current profile hook is present on the selected OpenClaw home"
|
|
6382
6399
|
};
|
|
6383
6400
|
}
|
|
@@ -6404,7 +6421,7 @@ function summarizeCurrentProfileBrainSummary(input) {
|
|
|
6404
6421
|
return "Brain is not attached to the current Profile.";
|
|
6405
6422
|
}
|
|
6406
6423
|
if (input.hookDesynced) {
|
|
6407
|
-
return "Brain hook files exist, but
|
|
6424
|
+
return "Brain hook files exist, but the installed hook is not loadable yet.";
|
|
6408
6425
|
}
|
|
6409
6426
|
if (input.attachmentState === "unknown") {
|
|
6410
6427
|
return input.watchOnly
|
|
@@ -6586,9 +6603,16 @@ function buildCurrentProfileBrainStatusFromReport(report, policyMode, profileId)
|
|
|
6586
6603
|
noun: "Hook",
|
|
6587
6604
|
scope: report.hook.scope,
|
|
6588
6605
|
openclawHome: report.hook.openclawHome,
|
|
6606
|
+
extensionDir: report.hook.extensionDir,
|
|
6589
6607
|
hookPath: report.hook.hookPath,
|
|
6590
6608
|
runtimeGuardPath: report.hook.runtimeGuardPath,
|
|
6591
6609
|
manifestPath: report.hook.manifestPath,
|
|
6610
|
+
packageJsonPath: report.hook.packageJsonPath,
|
|
6611
|
+
manifestId: report.hook.manifestId,
|
|
6612
|
+
installId: report.hook.installId,
|
|
6613
|
+
packageName: report.hook.packageName,
|
|
6614
|
+
installLayout: report.hook.installLayout,
|
|
6615
|
+
additionalInstallCount: report.hook.additionalInstallCount,
|
|
6592
6616
|
installState: report.hook.installState,
|
|
6593
6617
|
loadability: report.hook.loadability,
|
|
6594
6618
|
loadProof: report.hook.loadProof,
|
|
@@ -6855,4 +6879,4 @@ export { buildPassiveLearningSessionExportFromOpenClawSessionStore, buildPassive
|
|
|
6855
6879
|
export { DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_TIMEOUT_MS, OllamaClient, OllamaClientError, createOllamaClient } from "./ollama-client.js";
|
|
6856
6880
|
export { resolveActivationRoot } from "./resolve-activation-root.js";
|
|
6857
6881
|
export { runDaemonCommand, parseDaemonArgs } from "./daemon.js";
|
|
6858
|
-
//# sourceMappingURL=index.js.map
|
|
6882
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OpenClawBrainInstallLayout } from "./openclaw-plugin-install.js";
|
|
1
2
|
export type OpenClawBrainHookInstallState = "installed" | "not_installed" | "blocked_by_allowlist" | "unverified";
|
|
2
3
|
export type OpenClawBrainHookLoadability = "loadable" | "blocked" | "not_installed" | "unverified";
|
|
3
4
|
export type OpenClawBrainHookLoadProof = "status_probe_ready" | "not_ready";
|
|
@@ -5,9 +6,16 @@ export type OpenClawBrainPluginAllowlistState = "unrestricted" | "allowed" | "bl
|
|
|
5
6
|
export interface OpenClawBrainHookInspection {
|
|
6
7
|
scope: "exact_openclaw_home" | "activation_root_only";
|
|
7
8
|
openclawHome: string | null;
|
|
9
|
+
extensionDir: string | null;
|
|
8
10
|
hookPath: string | null;
|
|
9
11
|
runtimeGuardPath: string | null;
|
|
10
12
|
manifestPath: string | null;
|
|
13
|
+
packageJsonPath: string | null;
|
|
14
|
+
manifestId: string | null;
|
|
15
|
+
installId: string | null;
|
|
16
|
+
packageName: string | null;
|
|
17
|
+
installLayout: OpenClawBrainInstallLayout | null;
|
|
18
|
+
additionalInstallCount: number;
|
|
11
19
|
installState: OpenClawBrainHookInstallState;
|
|
12
20
|
loadability: OpenClawBrainHookLoadability;
|
|
13
21
|
pluginAllowlistState: OpenClawBrainPluginAllowlistState;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { describeOpenClawBrainInstallIdentity, describeOpenClawBrainInstallLayout, findInstalledOpenClawBrainPlugin, getOpenClawBrainKnownPluginIds } from "./openclaw-plugin-install.js";
|
|
3
4
|
function toErrorMessage(error) {
|
|
4
5
|
return error instanceof Error ? error.message : String(error);
|
|
5
6
|
}
|
|
@@ -34,8 +35,49 @@ function shortenPath(fullPath) {
|
|
|
34
35
|
}
|
|
35
36
|
return fullPath;
|
|
36
37
|
}
|
|
38
|
+
function inspectInstalledHookActivationRoot(loaderEntryPath) {
|
|
39
|
+
let content;
|
|
40
|
+
try {
|
|
41
|
+
content = readFileSync(loaderEntryPath, "utf8");
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
ready: false,
|
|
46
|
+
detail: `installed loader entry ${shortenPath(loaderEntryPath)} could not be read: ${toErrorMessage(error)}`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const match = content.match(/const\s+ACTIVATION_ROOT\s*=\s*["'`]([^"'`]+)["'`]/) ??
|
|
50
|
+
content.match(/activationRoot:\s*["'`]([^"'`]+)["'`]/);
|
|
51
|
+
if (!match || !match[1]) {
|
|
52
|
+
return {
|
|
53
|
+
ready: false,
|
|
54
|
+
detail: `installed loader entry ${shortenPath(loaderEntryPath)} does not declare a pinned activation root`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const activationRoot = match[1].trim();
|
|
58
|
+
if (activationRoot === "__ACTIVATION_ROOT__" || activationRoot === "__ACTIVATION_" + "ROOT__") {
|
|
59
|
+
return {
|
|
60
|
+
ready: false,
|
|
61
|
+
detail: `installed loader entry ${shortenPath(loaderEntryPath)} still contains the ACTIVATION_ROOT placeholder; rerun openclawbrain install/attach to pin the runtime hook`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
ready: true,
|
|
66
|
+
detail: `installed loader entry ${shortenPath(loaderEntryPath)} pins activation root ${shortenPath(path.resolve(activationRoot))}`
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function describeAdditionalInstallDetail(additionalInstalls) {
|
|
70
|
+
if (additionalInstalls.length === 0) {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
return `; additional OpenClawBrain installs also exist at ${additionalInstalls
|
|
74
|
+
.map((install) => `${shortenPath(install.extensionDir)} (${describeOpenClawBrainInstallLayout(install.installLayout)}, ${describeOpenClawBrainInstallIdentity(install)})`)
|
|
75
|
+
.join(", ")}`;
|
|
76
|
+
}
|
|
37
77
|
export function inspectOpenClawBrainPluginAllowlist(openclawHome) {
|
|
38
78
|
const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
|
|
79
|
+
const installedPlugin = findInstalledOpenClawBrainPlugin(openclawHome);
|
|
80
|
+
const knownPluginIds = getOpenClawBrainKnownPluginIds(installedPlugin.selectedInstall);
|
|
39
81
|
const plugins = readJsonObjectRecord(config.plugins);
|
|
40
82
|
if (plugins === null) {
|
|
41
83
|
return {
|
|
@@ -55,14 +97,15 @@ export function inspectOpenClawBrainPluginAllowlist(openclawHome) {
|
|
|
55
97
|
detail: `${shortenPath(openclawJsonPath)} has a non-array plugins.allow value, so OpenClawBrain load cannot be proven from config`
|
|
56
98
|
};
|
|
57
99
|
}
|
|
58
|
-
|
|
100
|
+
const matchedPluginId = knownPluginIds.find((pluginId) => plugins.allow.includes(pluginId)) ?? null;
|
|
101
|
+
return matchedPluginId !== null
|
|
59
102
|
? {
|
|
60
103
|
state: "allowed",
|
|
61
|
-
detail: `${shortenPath(openclawJsonPath)} plugins.allow explicitly includes
|
|
104
|
+
detail: `${shortenPath(openclawJsonPath)} plugins.allow explicitly includes ${matchedPluginId}; recognized OpenClawBrain ids are ${knownPluginIds.join(", ")}`
|
|
62
105
|
}
|
|
63
106
|
: {
|
|
64
107
|
state: "blocked",
|
|
65
|
-
detail: `${shortenPath(openclawJsonPath)} plugins.allow excludes
|
|
108
|
+
detail: `${shortenPath(openclawJsonPath)} plugins.allow excludes recognized OpenClawBrain ids ${knownPluginIds.join(", ")}`
|
|
66
109
|
};
|
|
67
110
|
}
|
|
68
111
|
export function inspectOpenClawBrainHookStatus(openclawHome) {
|
|
@@ -70,9 +113,16 @@ export function inspectOpenClawBrainHookStatus(openclawHome) {
|
|
|
70
113
|
return {
|
|
71
114
|
scope: "activation_root_only",
|
|
72
115
|
openclawHome: null,
|
|
116
|
+
extensionDir: null,
|
|
73
117
|
hookPath: null,
|
|
74
118
|
runtimeGuardPath: null,
|
|
75
119
|
manifestPath: null,
|
|
120
|
+
packageJsonPath: null,
|
|
121
|
+
manifestId: null,
|
|
122
|
+
installId: null,
|
|
123
|
+
packageName: null,
|
|
124
|
+
installLayout: null,
|
|
125
|
+
additionalInstallCount: 0,
|
|
76
126
|
installState: "unverified",
|
|
77
127
|
loadability: "unverified",
|
|
78
128
|
pluginAllowlistState: "unverified",
|
|
@@ -81,66 +131,122 @@ export function inspectOpenClawBrainHookStatus(openclawHome) {
|
|
|
81
131
|
};
|
|
82
132
|
}
|
|
83
133
|
const resolvedHome = path.resolve(openclawHome);
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
134
|
+
const installedPlugin = findInstalledOpenClawBrainPlugin(resolvedHome);
|
|
135
|
+
if (installedPlugin.selectedInstall === null ||
|
|
136
|
+
installedPlugin.selectedInstall.loaderEntryPath === null ||
|
|
137
|
+
installedPlugin.selectedInstall.runtimeGuardPath === null) {
|
|
138
|
+
const incompleteInstall = installedPlugin.selectedInstall;
|
|
89
139
|
return {
|
|
90
140
|
scope: "exact_openclaw_home",
|
|
91
141
|
openclawHome: resolvedHome,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
142
|
+
extensionDir: incompleteInstall?.extensionDir ?? null,
|
|
143
|
+
hookPath: incompleteInstall?.loaderEntryPath ?? null,
|
|
144
|
+
runtimeGuardPath: incompleteInstall?.runtimeGuardPath ?? null,
|
|
145
|
+
manifestPath: incompleteInstall?.manifestPath ?? null,
|
|
146
|
+
packageJsonPath: incompleteInstall?.packageJsonPath ?? null,
|
|
147
|
+
manifestId: incompleteInstall?.manifestId ?? null,
|
|
148
|
+
installId: incompleteInstall?.installId ?? null,
|
|
149
|
+
packageName: incompleteInstall?.packageName ?? null,
|
|
150
|
+
installLayout: incompleteInstall?.installLayout ?? null,
|
|
151
|
+
additionalInstallCount: installedPlugin.additionalInstalls.length,
|
|
95
152
|
installState: "not_installed",
|
|
96
153
|
loadability: "not_installed",
|
|
97
154
|
pluginAllowlistState: "unverified",
|
|
98
155
|
desynced: false,
|
|
99
|
-
detail:
|
|
156
|
+
detail: incompleteInstall === null
|
|
157
|
+
? `profile hook is not present under ${shortenPath(installedPlugin.extensionsDir)}`
|
|
158
|
+
: `profile hook is incomplete under ${shortenPath(incompleteInstall.extensionDir)} (${describeOpenClawBrainInstallIdentity(incompleteInstall)})`
|
|
100
159
|
};
|
|
101
160
|
}
|
|
161
|
+
const selectedInstall = installedPlugin.selectedInstall;
|
|
162
|
+
const additionalInstallDetail = describeAdditionalInstallDetail(installedPlugin.additionalInstalls);
|
|
102
163
|
const allowlist = inspectOpenClawBrainPluginAllowlist(resolvedHome);
|
|
164
|
+
const layoutLabel = describeOpenClawBrainInstallLayout(selectedInstall.installLayout);
|
|
165
|
+
const identityDetail = describeOpenClawBrainInstallIdentity(selectedInstall);
|
|
166
|
+
const activationRootState = inspectInstalledHookActivationRoot(selectedInstall.loaderEntryPath);
|
|
103
167
|
if (allowlist.state === "blocked") {
|
|
104
168
|
return {
|
|
105
169
|
scope: "exact_openclaw_home",
|
|
106
170
|
openclawHome: resolvedHome,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
171
|
+
extensionDir: selectedInstall.extensionDir,
|
|
172
|
+
hookPath: selectedInstall.loaderEntryPath,
|
|
173
|
+
runtimeGuardPath: selectedInstall.runtimeGuardPath,
|
|
174
|
+
manifestPath: selectedInstall.manifestPath,
|
|
175
|
+
packageJsonPath: selectedInstall.packageJsonPath,
|
|
176
|
+
manifestId: selectedInstall.manifestId,
|
|
177
|
+
installId: selectedInstall.installId,
|
|
178
|
+
packageName: selectedInstall.packageName,
|
|
179
|
+
installLayout: selectedInstall.installLayout,
|
|
180
|
+
additionalInstallCount: installedPlugin.additionalInstalls.length,
|
|
110
181
|
installState: "blocked_by_allowlist",
|
|
111
182
|
loadability: "blocked",
|
|
112
183
|
pluginAllowlistState: allowlist.state,
|
|
113
184
|
desynced: true,
|
|
114
|
-
detail: `profile hook
|
|
115
|
-
|
|
185
|
+
detail: `profile hook is installed via ${layoutLabel} at ${shortenPath(selectedInstall.extensionDir)} (${identityDetail}), but ${allowlist.detail}; ` +
|
|
186
|
+
`the on-disk hook is not loadable and OpenClaw will not load it${additionalInstallDetail}`
|
|
116
187
|
};
|
|
117
188
|
}
|
|
118
189
|
if (allowlist.state === "invalid") {
|
|
119
190
|
return {
|
|
120
191
|
scope: "exact_openclaw_home",
|
|
121
192
|
openclawHome: resolvedHome,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
193
|
+
extensionDir: selectedInstall.extensionDir,
|
|
194
|
+
hookPath: selectedInstall.loaderEntryPath,
|
|
195
|
+
runtimeGuardPath: selectedInstall.runtimeGuardPath,
|
|
196
|
+
manifestPath: selectedInstall.manifestPath,
|
|
197
|
+
packageJsonPath: selectedInstall.packageJsonPath,
|
|
198
|
+
manifestId: selectedInstall.manifestId,
|
|
199
|
+
installId: selectedInstall.installId,
|
|
200
|
+
packageName: selectedInstall.packageName,
|
|
201
|
+
installLayout: selectedInstall.installLayout,
|
|
202
|
+
additionalInstallCount: installedPlugin.additionalInstalls.length,
|
|
125
203
|
installState: "blocked_by_allowlist",
|
|
126
204
|
loadability: "blocked",
|
|
127
205
|
pluginAllowlistState: allowlist.state,
|
|
128
206
|
desynced: true,
|
|
129
|
-
detail: `profile hook
|
|
130
|
-
|
|
207
|
+
detail: `profile hook is installed via ${layoutLabel} at ${shortenPath(selectedInstall.extensionDir)} (${identityDetail}), but ${allowlist.detail}; ` +
|
|
208
|
+
`treat hook-load state as broken until install/attach repairs the config${additionalInstallDetail}`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (!activationRootState.ready) {
|
|
212
|
+
return {
|
|
213
|
+
scope: "exact_openclaw_home",
|
|
214
|
+
openclawHome: resolvedHome,
|
|
215
|
+
extensionDir: selectedInstall.extensionDir,
|
|
216
|
+
hookPath: selectedInstall.loaderEntryPath,
|
|
217
|
+
runtimeGuardPath: selectedInstall.runtimeGuardPath,
|
|
218
|
+
manifestPath: selectedInstall.manifestPath,
|
|
219
|
+
packageJsonPath: selectedInstall.packageJsonPath,
|
|
220
|
+
manifestId: selectedInstall.manifestId,
|
|
221
|
+
installId: selectedInstall.installId,
|
|
222
|
+
packageName: selectedInstall.packageName,
|
|
223
|
+
installLayout: selectedInstall.installLayout,
|
|
224
|
+
additionalInstallCount: installedPlugin.additionalInstalls.length,
|
|
225
|
+
installState: "installed",
|
|
226
|
+
loadability: "blocked",
|
|
227
|
+
pluginAllowlistState: allowlist.state,
|
|
228
|
+
desynced: true,
|
|
229
|
+
detail: `profile hook is installed via ${layoutLabel} at ${shortenPath(selectedInstall.extensionDir)} (${identityDetail}), but ${activationRootState.detail}${additionalInstallDetail}`
|
|
131
230
|
};
|
|
132
231
|
}
|
|
133
232
|
return {
|
|
134
233
|
scope: "exact_openclaw_home",
|
|
135
234
|
openclawHome: resolvedHome,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
235
|
+
extensionDir: selectedInstall.extensionDir,
|
|
236
|
+
hookPath: selectedInstall.loaderEntryPath,
|
|
237
|
+
runtimeGuardPath: selectedInstall.runtimeGuardPath,
|
|
238
|
+
manifestPath: selectedInstall.manifestPath,
|
|
239
|
+
packageJsonPath: selectedInstall.packageJsonPath,
|
|
240
|
+
manifestId: selectedInstall.manifestId,
|
|
241
|
+
installId: selectedInstall.installId,
|
|
242
|
+
packageName: selectedInstall.packageName,
|
|
243
|
+
installLayout: selectedInstall.installLayout,
|
|
244
|
+
additionalInstallCount: installedPlugin.additionalInstalls.length,
|
|
139
245
|
installState: "installed",
|
|
140
246
|
loadability: "loadable",
|
|
141
247
|
pluginAllowlistState: allowlist.state,
|
|
142
248
|
desynced: false,
|
|
143
|
-
detail: `profile hook is installed at ${shortenPath(extensionDir)}`
|
|
249
|
+
detail: `profile hook is installed via ${layoutLabel} at ${shortenPath(selectedInstall.extensionDir)} (${identityDetail})${additionalInstallDetail}`
|
|
144
250
|
};
|
|
145
251
|
}
|
|
146
252
|
export function summarizeOpenClawBrainHookLoad(inspection, statusProbeReady) {
|
|
@@ -151,4 +257,4 @@ export function summarizeOpenClawBrainHookLoad(inspection, statusProbeReady) {
|
|
|
151
257
|
: "not_ready"
|
|
152
258
|
};
|
|
153
259
|
}
|
|
154
|
-
//# sourceMappingURL=openclaw-hook-truth.js.map
|
|
260
|
+
//# sourceMappingURL=openclaw-hook-truth.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type OpenClawBrainInstallLayout = "generated_shadow_extension" | "native_package_plugin";
|
|
2
|
+
export interface OpenClawBrainInstalledPlugin {
|
|
3
|
+
openclawHome: string;
|
|
4
|
+
extensionsDir: string;
|
|
5
|
+
extensionDir: string;
|
|
6
|
+
manifestPath: string;
|
|
7
|
+
packageJsonPath: string;
|
|
8
|
+
loaderEntryPath: string | null;
|
|
9
|
+
runtimeGuardPath: string | null;
|
|
10
|
+
configuredEntries: string[];
|
|
11
|
+
manifestId: string | null;
|
|
12
|
+
installId: string | null;
|
|
13
|
+
packageName: string | null;
|
|
14
|
+
installLayout: OpenClawBrainInstallLayout;
|
|
15
|
+
}
|
|
16
|
+
export interface OpenClawBrainInstallTarget {
|
|
17
|
+
installLayout: OpenClawBrainInstallLayout;
|
|
18
|
+
extensionDir: string;
|
|
19
|
+
hookPath: string;
|
|
20
|
+
writeMode: "pin_native_package" | "write_shadow_extension";
|
|
21
|
+
selectedInstall: OpenClawBrainInstalledPlugin | null;
|
|
22
|
+
additionalInstalls: OpenClawBrainInstalledPlugin[];
|
|
23
|
+
}
|
|
24
|
+
export interface OpenClawBrainInstalledPluginLookup {
|
|
25
|
+
openclawHome: string;
|
|
26
|
+
extensionsDir: string;
|
|
27
|
+
selectedInstall: OpenClawBrainInstalledPlugin | null;
|
|
28
|
+
additionalInstalls: OpenClawBrainInstalledPlugin[];
|
|
29
|
+
}
|
|
30
|
+
export declare const OPENCLAWBRAIN_PLUGIN_ID = "openclawbrain";
|
|
31
|
+
export declare const OPENCLAWBRAIN_SHADOW_PACKAGE_NAME = "openclawbrain";
|
|
32
|
+
export declare const OPENCLAWBRAIN_NATIVE_PACKAGE_NAME = "@openclawbrain/openclaw";
|
|
33
|
+
export declare const OPENCLAWBRAIN_NATIVE_INSTALL_ID = "openclaw";
|
|
34
|
+
export declare function describeOpenClawBrainInstallLayout(installLayout: OpenClawBrainInstallLayout): string;
|
|
35
|
+
export declare function getOpenClawBrainKnownPluginIds(install: OpenClawBrainInstalledPlugin | null | undefined): string[];
|
|
36
|
+
export declare function describeOpenClawBrainInstallIdentity(install: OpenClawBrainInstalledPlugin): string;
|
|
37
|
+
export declare function findInstalledOpenClawBrainPlugin(openclawHome: string, pluginId?: string): OpenClawBrainInstalledPluginLookup;
|
|
38
|
+
export declare function resolveOpenClawBrainInstallTarget(openclawHome: string): OpenClawBrainInstallTarget;
|
|
39
|
+
export declare function pinInstalledOpenClawBrainPluginActivationRoot(loaderEntryPath: string, activationRoot: string): void;
|
|
40
|
+
export declare function resolveOpenClawHomeFromExtensionEntryPath(extensionEntryPath: string): string | null;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const OPENCLAWBRAIN_PLUGIN_ID = "openclawbrain";
|
|
4
|
+
export const OPENCLAWBRAIN_SHADOW_PACKAGE_NAME = "openclawbrain";
|
|
5
|
+
export const OPENCLAWBRAIN_NATIVE_PACKAGE_NAME = "@openclawbrain/openclaw";
|
|
6
|
+
export const OPENCLAWBRAIN_NATIVE_INSTALL_ID = "openclaw";
|
|
7
|
+
function readJsonObject(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
10
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function resolveConfiguredEntries(packageJson) {
|
|
20
|
+
const entries = packageJson?.openclaw?.extensions;
|
|
21
|
+
if (!Array.isArray(entries)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return entries
|
|
25
|
+
.filter((entry) => typeof entry === "string")
|
|
26
|
+
.map((entry) => entry.trim())
|
|
27
|
+
.filter((entry) => entry.length > 0);
|
|
28
|
+
}
|
|
29
|
+
function resolveLoaderEntryPath(extensionDir, configuredEntries) {
|
|
30
|
+
for (const configuredEntry of configuredEntries) {
|
|
31
|
+
const candidate = path.join(extensionDir, configuredEntry);
|
|
32
|
+
if (existsSync(candidate)) {
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function resolveRuntimeGuardPath(loaderEntryPath) {
|
|
39
|
+
const runtimeGuardPath = path.join(path.dirname(loaderEntryPath), "runtime-guard.js");
|
|
40
|
+
return existsSync(runtimeGuardPath) ? runtimeGuardPath : null;
|
|
41
|
+
}
|
|
42
|
+
function normalizeOptionalString(value) {
|
|
43
|
+
if (typeof value !== "string") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const trimmed = value.trim();
|
|
47
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
48
|
+
}
|
|
49
|
+
function normalizeInstallId(value) {
|
|
50
|
+
const normalized = normalizeOptionalString(value);
|
|
51
|
+
if (normalized === null) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (!normalized.startsWith("@")) {
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
const parts = normalized.split("/");
|
|
58
|
+
return normalizeOptionalString(parts[1] ?? null);
|
|
59
|
+
}
|
|
60
|
+
function listCandidateExtensionDirs(extensionsDir) {
|
|
61
|
+
const candidates = [];
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = readdirSync(extensionsDir, { withFileTypes: true });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (!entry.isDirectory()) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const directPath = path.join(extensionsDir, entry.name);
|
|
74
|
+
candidates.push(directPath);
|
|
75
|
+
if (!entry.name.startsWith("@")) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
let scopedEntries;
|
|
79
|
+
try {
|
|
80
|
+
scopedEntries = readdirSync(directPath, { withFileTypes: true });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const scopedEntry of scopedEntries) {
|
|
86
|
+
if (!scopedEntry.isDirectory()) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
candidates.push(path.join(directPath, scopedEntry.name));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return candidates.sort((left, right) => left.localeCompare(right));
|
|
93
|
+
}
|
|
94
|
+
function inferInstallLayout(input) {
|
|
95
|
+
if (input.packageName === OPENCLAWBRAIN_NATIVE_PACKAGE_NAME) {
|
|
96
|
+
return "native_package_plugin";
|
|
97
|
+
}
|
|
98
|
+
if (input.packageName === OPENCLAWBRAIN_SHADOW_PACKAGE_NAME) {
|
|
99
|
+
return "generated_shadow_extension";
|
|
100
|
+
}
|
|
101
|
+
if (input.installId === OPENCLAWBRAIN_NATIVE_INSTALL_ID &&
|
|
102
|
+
input.loaderEntryPath !== null &&
|
|
103
|
+
input.loaderEntryPath.endsWith(path.join("dist", "extension", "index.js"))) {
|
|
104
|
+
return "native_package_plugin";
|
|
105
|
+
}
|
|
106
|
+
if (input.loaderEntryPath !== null &&
|
|
107
|
+
input.loaderEntryPath.endsWith(path.join("dist", "extension", "index.js"))) {
|
|
108
|
+
return "native_package_plugin";
|
|
109
|
+
}
|
|
110
|
+
if (input.loaderEntryPath !== null &&
|
|
111
|
+
path.basename(input.loaderEntryPath) === "index.ts" &&
|
|
112
|
+
(input.installId === OPENCLAWBRAIN_PLUGIN_ID ||
|
|
113
|
+
path.basename(input.extensionDir) === OPENCLAWBRAIN_PLUGIN_ID)) {
|
|
114
|
+
return "generated_shadow_extension";
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function compareInstalledPluginPriority(left, right) {
|
|
119
|
+
const leftCompleteness = Number(left.loaderEntryPath !== null) + Number(left.runtimeGuardPath !== null);
|
|
120
|
+
const rightCompleteness = Number(right.loaderEntryPath !== null) + Number(right.runtimeGuardPath !== null);
|
|
121
|
+
if (leftCompleteness !== rightCompleteness) {
|
|
122
|
+
return rightCompleteness - leftCompleteness;
|
|
123
|
+
}
|
|
124
|
+
const leftLayoutRank = left.installLayout === "native_package_plugin" ? 0 : 1;
|
|
125
|
+
const rightLayoutRank = right.installLayout === "native_package_plugin" ? 0 : 1;
|
|
126
|
+
if (leftLayoutRank !== rightLayoutRank) {
|
|
127
|
+
return leftLayoutRank - rightLayoutRank;
|
|
128
|
+
}
|
|
129
|
+
return left.extensionDir.localeCompare(right.extensionDir);
|
|
130
|
+
}
|
|
131
|
+
function inspectOpenClawBrainPluginInstall(extensionDir, pluginId) {
|
|
132
|
+
const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
|
|
133
|
+
if (!existsSync(manifestPath)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const manifest = readJsonObject(manifestPath);
|
|
137
|
+
const manifestId = normalizeOptionalString(manifest?.id);
|
|
138
|
+
if (manifestId !== pluginId) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const packageJsonPath = path.join(extensionDir, "package.json");
|
|
142
|
+
if (!existsSync(packageJsonPath)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const packageJson = readJsonObject(packageJsonPath);
|
|
146
|
+
if (packageJson === null) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const configuredEntries = resolveConfiguredEntries(packageJson);
|
|
150
|
+
const loaderEntryPath = resolveLoaderEntryPath(extensionDir, configuredEntries);
|
|
151
|
+
const packageName = typeof packageJson.name === "string" && packageJson.name.trim().length > 0
|
|
152
|
+
? packageJson.name.trim()
|
|
153
|
+
: null;
|
|
154
|
+
const installId = normalizeInstallId(packageName) ?? normalizeInstallId(path.basename(extensionDir));
|
|
155
|
+
const installLayout = inferInstallLayout({
|
|
156
|
+
extensionDir,
|
|
157
|
+
packageName,
|
|
158
|
+
installId,
|
|
159
|
+
loaderEntryPath
|
|
160
|
+
});
|
|
161
|
+
if (installLayout === null) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
extensionDir,
|
|
166
|
+
manifestPath,
|
|
167
|
+
packageJsonPath,
|
|
168
|
+
loaderEntryPath,
|
|
169
|
+
runtimeGuardPath: loaderEntryPath === null ? null : resolveRuntimeGuardPath(loaderEntryPath),
|
|
170
|
+
configuredEntries,
|
|
171
|
+
manifestId,
|
|
172
|
+
installId,
|
|
173
|
+
packageName,
|
|
174
|
+
installLayout
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function describeOpenClawBrainInstallLayout(installLayout) {
|
|
178
|
+
return installLayout === "native_package_plugin"
|
|
179
|
+
? "native package plugin"
|
|
180
|
+
: "generated shadow extension";
|
|
181
|
+
}
|
|
182
|
+
export function getOpenClawBrainKnownPluginIds(install) {
|
|
183
|
+
const ids = [];
|
|
184
|
+
const push = (value) => {
|
|
185
|
+
const normalized = normalizeOptionalString(value);
|
|
186
|
+
if (normalized === null || ids.includes(normalized)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
ids.push(normalized);
|
|
190
|
+
};
|
|
191
|
+
push(OPENCLAWBRAIN_PLUGIN_ID);
|
|
192
|
+
push(install?.manifestId ?? null);
|
|
193
|
+
push(install?.installId ?? null);
|
|
194
|
+
if (install?.packageName === OPENCLAWBRAIN_NATIVE_PACKAGE_NAME) {
|
|
195
|
+
push(OPENCLAWBRAIN_NATIVE_INSTALL_ID);
|
|
196
|
+
}
|
|
197
|
+
return ids;
|
|
198
|
+
}
|
|
199
|
+
export function describeOpenClawBrainInstallIdentity(install) {
|
|
200
|
+
return [
|
|
201
|
+
`manifest=${install.manifestId ?? OPENCLAWBRAIN_PLUGIN_ID}`,
|
|
202
|
+
`install=${install.installId ?? path.basename(install.extensionDir)}`,
|
|
203
|
+
`package=${install.packageName ?? "unknown"}`
|
|
204
|
+
].join(" ");
|
|
205
|
+
}
|
|
206
|
+
export function findInstalledOpenClawBrainPlugin(openclawHome, pluginId = OPENCLAWBRAIN_PLUGIN_ID) {
|
|
207
|
+
const resolvedOpenclawHome = path.resolve(openclawHome);
|
|
208
|
+
const extensionsDir = path.join(resolvedOpenclawHome, "extensions");
|
|
209
|
+
if (!existsSync(extensionsDir)) {
|
|
210
|
+
return {
|
|
211
|
+
openclawHome: resolvedOpenclawHome,
|
|
212
|
+
extensionsDir,
|
|
213
|
+
selectedInstall: null,
|
|
214
|
+
additionalInstalls: []
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const installs = listCandidateExtensionDirs(extensionsDir)
|
|
218
|
+
.map((extensionDir) => inspectOpenClawBrainPluginInstall(extensionDir, pluginId))
|
|
219
|
+
.filter((install) => install !== null)
|
|
220
|
+
.map((install) => ({
|
|
221
|
+
...install,
|
|
222
|
+
openclawHome: resolvedOpenclawHome,
|
|
223
|
+
extensionsDir
|
|
224
|
+
}))
|
|
225
|
+
.sort(compareInstalledPluginPriority);
|
|
226
|
+
return {
|
|
227
|
+
openclawHome: resolvedOpenclawHome,
|
|
228
|
+
extensionsDir,
|
|
229
|
+
selectedInstall: installs[0] ?? null,
|
|
230
|
+
additionalInstalls: installs.slice(1)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export function resolveOpenClawBrainInstallTarget(openclawHome) {
|
|
234
|
+
const installedPlugin = findInstalledOpenClawBrainPlugin(openclawHome);
|
|
235
|
+
const selectedInstall = installedPlugin.selectedInstall;
|
|
236
|
+
if (selectedInstall !== null &&
|
|
237
|
+
selectedInstall.installLayout === "native_package_plugin" &&
|
|
238
|
+
selectedInstall.loaderEntryPath !== null &&
|
|
239
|
+
selectedInstall.runtimeGuardPath !== null) {
|
|
240
|
+
return {
|
|
241
|
+
installLayout: selectedInstall.installLayout,
|
|
242
|
+
extensionDir: selectedInstall.extensionDir,
|
|
243
|
+
hookPath: selectedInstall.loaderEntryPath,
|
|
244
|
+
writeMode: "pin_native_package",
|
|
245
|
+
selectedInstall,
|
|
246
|
+
additionalInstalls: installedPlugin.additionalInstalls
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const extensionDir = path.join(openclawHome, "extensions", OPENCLAWBRAIN_PLUGIN_ID);
|
|
250
|
+
return {
|
|
251
|
+
installLayout: "generated_shadow_extension",
|
|
252
|
+
extensionDir,
|
|
253
|
+
hookPath: path.join(extensionDir, "index.ts"),
|
|
254
|
+
writeMode: "write_shadow_extension",
|
|
255
|
+
selectedInstall,
|
|
256
|
+
additionalInstalls: installedPlugin.additionalInstalls
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
export function pinInstalledOpenClawBrainPluginActivationRoot(loaderEntryPath, activationRoot) {
|
|
260
|
+
const loaderSource = readFileSync(loaderEntryPath, "utf8");
|
|
261
|
+
const pinnedActivationRoot = `const ACTIVATION_ROOT = ${JSON.stringify(activationRoot)};`;
|
|
262
|
+
const nextLoaderSource = loaderSource.replace(/const\s+ACTIVATION_ROOT\s*=\s*["'`][^"'`]*["'`];/, pinnedActivationRoot);
|
|
263
|
+
if (nextLoaderSource === loaderSource) {
|
|
264
|
+
throw new Error(`Installed loader entry ${loaderEntryPath} does not expose a patchable ACTIVATION_ROOT constant`);
|
|
265
|
+
}
|
|
266
|
+
writeFileSync(loaderEntryPath, nextLoaderSource, "utf8");
|
|
267
|
+
}
|
|
268
|
+
export function resolveOpenClawHomeFromExtensionEntryPath(extensionEntryPath) {
|
|
269
|
+
let currentPath = path.resolve(extensionEntryPath);
|
|
270
|
+
while (true) {
|
|
271
|
+
const parentPath = path.dirname(currentPath);
|
|
272
|
+
if (parentPath === currentPath) {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
if (path.basename(parentPath) === "extensions") {
|
|
276
|
+
return path.dirname(parentPath);
|
|
277
|
+
}
|
|
278
|
+
currentPath = parentPath;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=openclaw-plugin-install.js.map
|