@clawhive/openclaw-plugin 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +324 -0
- package/dist/runtime-api.d.ts +1 -0
- package/dist/runtime-api.js +1 -0
- package/dist/setup-entry.d.ts +4 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/agency-install/claimLoop.d.ts +41 -0
- package/dist/src/agency-install/claimLoop.js +188 -0
- package/dist/src/agent-panel/claimLoop.d.ts +32 -0
- package/dist/src/agent-panel/claimLoop.js +118 -0
- package/dist/src/agent-panel/executor.d.ts +8 -0
- package/dist/src/agent-panel/executor.js +368 -0
- package/dist/src/agent-panel/plan.d.ts +20 -0
- package/dist/src/agent-panel/plan.js +111 -0
- package/dist/src/agent-panel/prompts.d.ts +38 -0
- package/dist/src/agent-panel/prompts.js +87 -0
- package/dist/src/agent-panel/types.d.ts +45 -0
- package/dist/src/agent-panel/types.js +1 -0
- package/dist/src/agents.d.ts +44 -0
- package/dist/src/agents.js +195 -0
- package/dist/src/authorization.d.ts +34 -0
- package/dist/src/authorization.js +183 -0
- package/dist/src/channel.d.ts +5 -0
- package/dist/src/channel.js +93 -0
- package/dist/src/claimPolling.d.ts +9 -0
- package/dist/src/claimPolling.js +13 -0
- package/dist/src/client.d.ts +386 -0
- package/dist/src/client.js +595 -0
- package/dist/src/config.d.ts +28 -0
- package/dist/src/config.js +94 -0
- package/dist/src/defaults.d.ts +40 -0
- package/dist/src/defaults.js +52 -0
- package/dist/src/deviceCode.d.ts +16 -0
- package/dist/src/deviceCode.js +65 -0
- package/dist/src/heartbeat.d.ts +25 -0
- package/dist/src/heartbeat.js +65 -0
- package/dist/src/imageAttachments.d.ts +14 -0
- package/dist/src/imageAttachments.js +81 -0
- package/dist/src/inbound.d.ts +10 -0
- package/dist/src/inbound.js +140 -0
- package/dist/src/installMutex.d.ts +4 -0
- package/dist/src/installMutex.js +18 -0
- package/dist/src/market-install/claimLoop.d.ts +41 -0
- package/dist/src/market-install/claimLoop.js +183 -0
- package/dist/src/market-install/downloader.d.ts +36 -0
- package/dist/src/market-install/downloader.js +133 -0
- package/dist/src/market-install/executor.d.ts +28 -0
- package/dist/src/market-install/executor.js +352 -0
- package/dist/src/market-install/types.d.ts +62 -0
- package/dist/src/market-install/types.js +1 -0
- package/dist/src/market-install/verifier.d.ts +32 -0
- package/dist/src/market-install/verifier.js +60 -0
- package/dist/src/market-publish/packager.d.ts +34 -0
- package/dist/src/market-publish/packager.js +168 -0
- package/dist/src/market-publish/publishFlow.d.ts +70 -0
- package/dist/src/market-publish/publishFlow.js +107 -0
- package/dist/src/market-publish/uploader.d.ts +73 -0
- package/dist/src/market-publish/uploader.js +132 -0
- package/dist/src/openclawVersion.d.ts +4 -0
- package/dist/src/openclawVersion.js +13 -0
- package/dist/src/outbound.d.ts +13 -0
- package/dist/src/outbound.js +41 -0
- package/dist/src/pairing.d.ts +32 -0
- package/dist/src/pairing.js +64 -0
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +26 -0
- package/dist/src/telemetry.d.ts +34 -0
- package/dist/src/telemetry.js +89 -0
- package/dist/src/transport/realtime.d.ts +49 -0
- package/dist/src/transport/realtime.js +273 -0
- package/dist/src/transport.d.ts +38 -0
- package/dist/src/transport.js +15 -0
- package/dist/src/wake.d.ts +31 -0
- package/dist/src/wake.js +101 -0
- package/openclaw.config.example.yaml +10 -0
- package/openclaw.plugin.json +122 -0
- package/package.json +84 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ClawHiveApiError } from "../client.js";
|
|
4
|
+
import { addInstallBreadcrumb, captureInstallFailure, } from "../telemetry.js";
|
|
5
|
+
/**
|
|
6
|
+
* Compare dotted versions without pulling in a full semver dependency.
|
|
7
|
+
*/
|
|
8
|
+
export function compareLooseVersions(current, minimum) {
|
|
9
|
+
const currentParts = current.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
10
|
+
const minimumParts = minimum.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
11
|
+
const maxLength = Math.max(currentParts.length, minimumParts.length);
|
|
12
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
13
|
+
const left = currentParts[index] ?? 0;
|
|
14
|
+
const right = minimumParts[index] ?? 0;
|
|
15
|
+
if (left > right) {
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
if (left < right) {
|
|
19
|
+
return -1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build a stable error instance so durable install failures and telemetry match.
|
|
26
|
+
*/
|
|
27
|
+
function createInstallFailure(reasonKind, errorMessage, details = {}, status = "failed") {
|
|
28
|
+
return {
|
|
29
|
+
reasonKind,
|
|
30
|
+
errorMessage,
|
|
31
|
+
details,
|
|
32
|
+
status,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function buildInstallDetails(job, input = {}) {
|
|
36
|
+
return {
|
|
37
|
+
listingId: job.listingId,
|
|
38
|
+
releaseId: job.releaseId,
|
|
39
|
+
sourceRef: job.sourceRef,
|
|
40
|
+
sourceType: job.sourceType,
|
|
41
|
+
fingerprint: job.fingerprint,
|
|
42
|
+
targetNodeId: job.targetNodeId,
|
|
43
|
+
targetNodeName: job.targetNodeName,
|
|
44
|
+
version: job.version,
|
|
45
|
+
...input,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function formatInstallError(error) {
|
|
49
|
+
if (error instanceof ClawHiveApiError) {
|
|
50
|
+
return `${error.message} (status=${error.status})`;
|
|
51
|
+
}
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
return error.message;
|
|
54
|
+
}
|
|
55
|
+
return String(error);
|
|
56
|
+
}
|
|
57
|
+
function asRecord(value) {
|
|
58
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
59
|
+
? value
|
|
60
|
+
: null;
|
|
61
|
+
}
|
|
62
|
+
function extractAgencySourcePath(sourceRef) {
|
|
63
|
+
if (!sourceRef?.trim()) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const marker = ":integrations/";
|
|
67
|
+
const markerIndex = sourceRef.indexOf(marker);
|
|
68
|
+
return markerIndex >= 0
|
|
69
|
+
? sourceRef.slice(markerIndex + 1)
|
|
70
|
+
: undefined;
|
|
71
|
+
}
|
|
72
|
+
function ensureWorkspaceRelativePath(relativePath) {
|
|
73
|
+
const normalized = path.posix.normalize(relativePath.trim().replaceAll("\\", "/"));
|
|
74
|
+
if (!normalized || normalized === "." || normalized.startsWith("../") || normalized.startsWith("/")) {
|
|
75
|
+
throw new Error(`Workspace file path must stay inside the agent workspace: ${relativePath}`);
|
|
76
|
+
}
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
function isWithinDirectory(parentDir, targetPath) {
|
|
80
|
+
const relative = path.relative(parentDir, targetPath);
|
|
81
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
82
|
+
}
|
|
83
|
+
function resolveManifestAgentId(job, manifest) {
|
|
84
|
+
const explicitId = manifest.agent.id?.trim();
|
|
85
|
+
if (explicitId) {
|
|
86
|
+
return explicitId;
|
|
87
|
+
}
|
|
88
|
+
if (job.listingId?.trim()) {
|
|
89
|
+
return job.listingId.trim();
|
|
90
|
+
}
|
|
91
|
+
return job.releaseId;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Confirm the fetched manifest still matches the immutable release that the job claimed.
|
|
95
|
+
*/
|
|
96
|
+
function validateFetchedManifest(job, manifest) {
|
|
97
|
+
if (manifest.release.releaseId !== job.releaseId) {
|
|
98
|
+
return createInstallFailure("artifact_fetch_failed", `The fetched manifest points at release ${manifest.release.releaseId}, but the claimed job expects ${job.releaseId}.`, buildInstallDetails(job, {
|
|
99
|
+
manifestReleaseId: manifest.release.releaseId,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
if (job.fingerprint?.trim() &&
|
|
103
|
+
manifest.release.fingerprint?.trim() &&
|
|
104
|
+
job.fingerprint.trim() !== manifest.release.fingerprint.trim()) {
|
|
105
|
+
return createInstallFailure("artifact_fetch_failed", "The fetched manifest fingerprint does not match the claimed release fingerprint.", buildInstallDetails(job, {
|
|
106
|
+
manifestFingerprint: manifest.release.fingerprint,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
if (job.sourceRef?.trim() &&
|
|
110
|
+
manifest.release.sourceRef?.trim() &&
|
|
111
|
+
job.sourceRef.trim() !== manifest.release.sourceRef.trim()) {
|
|
112
|
+
return createInstallFailure("artifact_fetch_failed", "The fetched manifest source reference does not match the claimed release pin.", buildInstallDetails(job, {
|
|
113
|
+
manifestSourceRef: manifest.release.sourceRef,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function upsertInstalledAgentConfig(currentConfig, manifest, agentId) {
|
|
119
|
+
const agentsSection = currentConfig.agents;
|
|
120
|
+
const existingList = Array.isArray(agentsSection?.list)
|
|
121
|
+
? [...agentsSection.list]
|
|
122
|
+
: [];
|
|
123
|
+
const existingEntry = existingList.find((entry) => entry?.id === agentId);
|
|
124
|
+
const explicitEntry = asRecord(manifest.agent.entry) ?? {};
|
|
125
|
+
const nextEntry = {
|
|
126
|
+
...(existingEntry ?? {}),
|
|
127
|
+
id: agentId,
|
|
128
|
+
name: manifest.agent.name.trim(),
|
|
129
|
+
};
|
|
130
|
+
const provenance = manifest.release.sourceType === "agency-github"
|
|
131
|
+
? {
|
|
132
|
+
agentId,
|
|
133
|
+
repo: "msitarzewski/agency-agents",
|
|
134
|
+
sourcePath: typeof explicitEntry.sourcePath === "string"
|
|
135
|
+
? explicitEntry.sourcePath
|
|
136
|
+
: extractAgencySourcePath(manifest.release.sourceRef),
|
|
137
|
+
sourceRef: manifest.release.sourceRef,
|
|
138
|
+
sourceType: manifest.release.sourceType,
|
|
139
|
+
}
|
|
140
|
+
: undefined;
|
|
141
|
+
const filteredList = existingList.filter((entry) => entry?.id !== agentId);
|
|
142
|
+
filteredList.push(nextEntry);
|
|
143
|
+
return {
|
|
144
|
+
nextConfig: {
|
|
145
|
+
...currentConfig,
|
|
146
|
+
agents: {
|
|
147
|
+
...(agentsSection ?? {}),
|
|
148
|
+
list: filteredList,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
nextEntry,
|
|
152
|
+
provenance,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Materialize the approved manifest into local workspace files and an OpenClaw agent entry.
|
|
157
|
+
*/
|
|
158
|
+
async function applyManifestToLocalAgent(job, manifest, options) {
|
|
159
|
+
const currentConfig = options.pluginApi.runtime.config.loadConfig();
|
|
160
|
+
const agentId = resolveManifestAgentId(job, manifest);
|
|
161
|
+
const { nextConfig, nextEntry, provenance } = upsertInstalledAgentConfig(currentConfig, manifest, agentId);
|
|
162
|
+
const workspaceDir = options.pluginApi.runtime.agent.resolveAgentWorkspaceDir(nextConfig, agentId);
|
|
163
|
+
await options.pluginApi.runtime.agent.ensureAgentWorkspace({
|
|
164
|
+
dir: workspaceDir,
|
|
165
|
+
ensureBootstrapFiles: true,
|
|
166
|
+
});
|
|
167
|
+
const workspaceFiles = manifest.agent.workspaceFiles ?? [];
|
|
168
|
+
for (const workspaceFile of workspaceFiles) {
|
|
169
|
+
const relativePath = ensureWorkspaceRelativePath(workspaceFile.path);
|
|
170
|
+
const targetPath = path.resolve(workspaceDir, relativePath);
|
|
171
|
+
if (!isWithinDirectory(workspaceDir, targetPath)) {
|
|
172
|
+
throw new Error(`Workspace file escapes the target directory: ${workspaceFile.path}`);
|
|
173
|
+
}
|
|
174
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
175
|
+
await writeFile(targetPath, workspaceFile.content, "utf8");
|
|
176
|
+
}
|
|
177
|
+
await options.pluginApi.runtime.config.writeConfigFile(nextConfig);
|
|
178
|
+
try {
|
|
179
|
+
const syncResult = await options.api.syncAgents([{
|
|
180
|
+
openclawAgentId: agentId,
|
|
181
|
+
name: nextEntry.name?.trim() || agentId,
|
|
182
|
+
description: manifest.agent.description ?? null,
|
|
183
|
+
avatarUrl: manifest.agent.avatarUrl ?? null,
|
|
184
|
+
config: {
|
|
185
|
+
agency: provenance,
|
|
186
|
+
workspace: nextEntry.workspace,
|
|
187
|
+
agentDir: nextEntry.agentDir,
|
|
188
|
+
},
|
|
189
|
+
}]);
|
|
190
|
+
options.onAgentsSynced?.(syncResult.items);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
options.logger.warn(`ClawHive installed agent ${agentId} locally but failed to sync it to cloud immediately: ${formatInstallError(error)}`);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
agentId,
|
|
197
|
+
workspaceDir,
|
|
198
|
+
workspaceFileCount: workspaceFiles.length,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function failInstallJob(job, failure, options) {
|
|
202
|
+
options.onStageChange?.(failure.status === "cancelled" ? "cancelled" : "failed");
|
|
203
|
+
addInstallBreadcrumb({
|
|
204
|
+
stage: failure.status === "cancelled" ? "cancelled" : "failed",
|
|
205
|
+
job,
|
|
206
|
+
reasonKind: failure.reasonKind,
|
|
207
|
+
data: failure.details,
|
|
208
|
+
});
|
|
209
|
+
captureInstallFailure({
|
|
210
|
+
error: new Error(failure.errorMessage),
|
|
211
|
+
reasonKind: failure.reasonKind,
|
|
212
|
+
job,
|
|
213
|
+
});
|
|
214
|
+
await options.api.updateInstallJob({
|
|
215
|
+
pluginNodeId: options.pluginNodeId,
|
|
216
|
+
jobId: job.jobId,
|
|
217
|
+
userId: options.userId,
|
|
218
|
+
status: failure.status === "cancelled" ? "cancelled" : "failed",
|
|
219
|
+
reasonKind: failure.reasonKind,
|
|
220
|
+
errorMessage: failure.errorMessage,
|
|
221
|
+
details: buildInstallDetails(job, failure.details),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Execute one claimed install job and always durably report the final result.
|
|
226
|
+
*/
|
|
227
|
+
export async function executeInstallJob(job, options) {
|
|
228
|
+
addInstallBreadcrumb({
|
|
229
|
+
stage: "claimed",
|
|
230
|
+
job,
|
|
231
|
+
});
|
|
232
|
+
if (job.targetNodeId !== options.pluginNodeId) {
|
|
233
|
+
await failInstallJob(job, createInstallFailure("claim_rejected", "The claimed install job targets a different OpenClaw node.", buildInstallDetails(job, {
|
|
234
|
+
expectedTargetNodeId: options.pluginNodeId,
|
|
235
|
+
})), options);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const requireClaimedArtifact = options.requireClaimedArtifact ?? true;
|
|
239
|
+
if (requireClaimedArtifact && !job.version?.trim()) {
|
|
240
|
+
await failInstallJob(job, createInstallFailure("claim_rejected", "The install job is missing the approved release version."), options);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (job.minPluginVersion &&
|
|
244
|
+
compareLooseVersions(options.pluginVersion, job.minPluginVersion) < 0) {
|
|
245
|
+
await failInstallJob(job, createInstallFailure("compatibility_rejected", `This node runs plugin ${options.pluginVersion}, but the release requires plugin ${job.minPluginVersion} or newer.`, buildInstallDetails(job, {
|
|
246
|
+
currentPluginVersion: options.pluginVersion,
|
|
247
|
+
minPluginVersion: job.minPluginVersion,
|
|
248
|
+
})), options);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (job.minOpenClawVersion && !options.openClawVersion) {
|
|
252
|
+
await failInstallJob(job, createInstallFailure("compatibility_rejected", `This release requires OpenClaw ${job.minOpenClawVersion} or newer, but the host version could not be determined.`, buildInstallDetails(job, {
|
|
253
|
+
minOpenClawVersion: job.minOpenClawVersion,
|
|
254
|
+
})), options);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (job.minOpenClawVersion &&
|
|
258
|
+
options.openClawVersion &&
|
|
259
|
+
compareLooseVersions(options.openClawVersion, job.minOpenClawVersion) < 0) {
|
|
260
|
+
await failInstallJob(job, createInstallFailure("compatibility_rejected", `This node runs OpenClaw ${options.openClawVersion}, but the release requires OpenClaw ${job.minOpenClawVersion} or newer.`, buildInstallDetails(job, {
|
|
261
|
+
currentOpenClawVersion: options.openClawVersion,
|
|
262
|
+
minOpenClawVersion: job.minOpenClawVersion,
|
|
263
|
+
})), options);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!job.sourceRef?.trim() ||
|
|
267
|
+
(requireClaimedArtifact && !job.fingerprint?.trim())) {
|
|
268
|
+
await failInstallJob(job, createInstallFailure("artifact_fetch_failed", requireClaimedArtifact
|
|
269
|
+
? "The claimed release is missing publishSourceRef or publishFingerprint, so the plugin cannot verify the approved artifact."
|
|
270
|
+
: "The claimed release is missing publishSourceRef, so the plugin cannot fetch the approved artifact.", buildInstallDetails(job)), options);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
options.onStageChange?.("installing");
|
|
274
|
+
addInstallBreadcrumb({
|
|
275
|
+
stage: "installing",
|
|
276
|
+
job,
|
|
277
|
+
data: {
|
|
278
|
+
compatibilityNotes: job.compatibilityNotes,
|
|
279
|
+
currentOpenClawVersion: options.openClawVersion,
|
|
280
|
+
currentPluginVersion: options.pluginVersion,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
await options.api.updateInstallJob({
|
|
284
|
+
pluginNodeId: options.pluginNodeId,
|
|
285
|
+
jobId: job.jobId,
|
|
286
|
+
userId: options.userId,
|
|
287
|
+
status: "installing",
|
|
288
|
+
details: buildInstallDetails(job, {
|
|
289
|
+
compatibilityNotes: job.compatibilityNotes,
|
|
290
|
+
currentOpenClawVersion: options.openClawVersion,
|
|
291
|
+
currentPluginVersion: options.pluginVersion,
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
let manifest;
|
|
295
|
+
try {
|
|
296
|
+
const result = await options.api.fetchInstallManifest({
|
|
297
|
+
pluginNodeId: options.pluginNodeId,
|
|
298
|
+
jobId: job.jobId,
|
|
299
|
+
userId: options.userId,
|
|
300
|
+
});
|
|
301
|
+
manifest = result.manifest;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
await failInstallJob(job, createInstallFailure("artifact_fetch_failed", `Failed to fetch the approved install manifest: ${formatInstallError(error)}`, buildInstallDetails(job, {
|
|
305
|
+
currentOpenClawVersion: options.openClawVersion,
|
|
306
|
+
currentPluginVersion: options.pluginVersion,
|
|
307
|
+
})), options);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const manifestFailure = validateFetchedManifest(job, manifest);
|
|
311
|
+
if (manifestFailure) {
|
|
312
|
+
await failInstallJob(job, manifestFailure, options);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
let applyResult;
|
|
316
|
+
try {
|
|
317
|
+
applyResult = await applyManifestToLocalAgent(job, manifest, options);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
await failInstallJob(job, createInstallFailure("install_apply_failed", `Failed to apply the approved manifest locally: ${formatInstallError(error)}`, buildInstallDetails(job, {
|
|
321
|
+
agentId: resolveManifestAgentId(job, manifest),
|
|
322
|
+
currentOpenClawVersion: options.openClawVersion,
|
|
323
|
+
currentPluginVersion: options.pluginVersion,
|
|
324
|
+
workspaceFiles: (manifest.agent.workspaceFiles ?? []).map((file) => file.path),
|
|
325
|
+
})), options);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
options.onStageChange?.("succeeded");
|
|
329
|
+
addInstallBreadcrumb({
|
|
330
|
+
stage: "succeeded",
|
|
331
|
+
job,
|
|
332
|
+
data: {
|
|
333
|
+
agentId: applyResult.agentId,
|
|
334
|
+
workspaceDir: applyResult.workspaceDir,
|
|
335
|
+
workspaceFileCount: applyResult.workspaceFileCount,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
options.logger.info(`ClawHive installed market release ${job.releaseId} as local agent ${applyResult.agentId}.`);
|
|
339
|
+
await options.api.updateInstallJob({
|
|
340
|
+
pluginNodeId: options.pluginNodeId,
|
|
341
|
+
jobId: job.jobId,
|
|
342
|
+
userId: options.userId,
|
|
343
|
+
status: "succeeded",
|
|
344
|
+
details: buildInstallDetails(job, {
|
|
345
|
+
agentId: applyResult.agentId,
|
|
346
|
+
workspaceDir: applyResult.workspaceDir,
|
|
347
|
+
workspaceFileCount: applyResult.workspaceFileCount,
|
|
348
|
+
manifestSourceRef: manifest.release.sourceRef,
|
|
349
|
+
manifestFingerprint: manifest.release.fingerprint,
|
|
350
|
+
}),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type MarketInstallUpdateStatus = "installing" | "succeeded" | "failed" | "cancelled";
|
|
2
|
+
export type MarketInstallReasonKind = "claim_rejected" | "artifact_fetch_failed" | "compatibility_rejected" | "install_apply_failed" | "cancelled";
|
|
3
|
+
export type MarketInstallClaimedJob = {
|
|
4
|
+
jobId: string;
|
|
5
|
+
listingId: string | null;
|
|
6
|
+
listingName: string | null;
|
|
7
|
+
releaseId: string;
|
|
8
|
+
version: string | null;
|
|
9
|
+
sourceRef: string | null;
|
|
10
|
+
sourceType: string | null;
|
|
11
|
+
fingerprint: string | null;
|
|
12
|
+
minPluginVersion: string | null;
|
|
13
|
+
minOpenClawVersion: string | null;
|
|
14
|
+
compatibilityNotes: string | null;
|
|
15
|
+
targetNodeId: string | null;
|
|
16
|
+
targetNodeName: string | null;
|
|
17
|
+
status: "dispatched";
|
|
18
|
+
reasonKind: string | null;
|
|
19
|
+
failureReason: string | null;
|
|
20
|
+
createdAt: string | null;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
};
|
|
23
|
+
export type ActiveInstallStage = "claimed" | "installing" | "succeeded" | "failed" | "cancelled";
|
|
24
|
+
export type ActiveInstallJobState = {
|
|
25
|
+
jobId: string;
|
|
26
|
+
listingId: string | null;
|
|
27
|
+
listingName: string | null;
|
|
28
|
+
releaseId: string;
|
|
29
|
+
version: string | null;
|
|
30
|
+
targetNodeId: string | null;
|
|
31
|
+
targetNodeName: string | null;
|
|
32
|
+
stage: ActiveInstallStage;
|
|
33
|
+
updatedAt: string;
|
|
34
|
+
};
|
|
35
|
+
export type InstallExecutionFailure = {
|
|
36
|
+
reasonKind: MarketInstallReasonKind;
|
|
37
|
+
errorMessage: string;
|
|
38
|
+
details?: Record<string, unknown>;
|
|
39
|
+
status?: Extract<MarketInstallUpdateStatus, "failed" | "cancelled">;
|
|
40
|
+
};
|
|
41
|
+
export type MarketInstallWorkspaceFile = {
|
|
42
|
+
path: string;
|
|
43
|
+
content: string;
|
|
44
|
+
};
|
|
45
|
+
export type MarketInstallAgentManifest = {
|
|
46
|
+
agent: {
|
|
47
|
+
id?: string | null;
|
|
48
|
+
name: string;
|
|
49
|
+
description?: string | null;
|
|
50
|
+
avatarUrl?: string | null;
|
|
51
|
+
entry?: Record<string, unknown>;
|
|
52
|
+
workspaceFiles?: MarketInstallWorkspaceFile[];
|
|
53
|
+
};
|
|
54
|
+
release: {
|
|
55
|
+
releaseId: string;
|
|
56
|
+
listingId: string | null;
|
|
57
|
+
version: string | null;
|
|
58
|
+
fingerprint: string | null;
|
|
59
|
+
sourceRef: string | null;
|
|
60
|
+
sourceType: string | null;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when package integrity verification fails.
|
|
3
|
+
*/
|
|
4
|
+
export declare class PackageIntegrityError extends Error {
|
|
5
|
+
readonly expected: string;
|
|
6
|
+
readonly actual: string;
|
|
7
|
+
constructor(expected: string, actual: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Compute SHA-256 hash of a file.
|
|
11
|
+
*
|
|
12
|
+
* @param filePath - Absolute path to the file
|
|
13
|
+
* @returns Promise resolving to hex-encoded SHA-256 hash
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeFileSha256(filePath: string): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Verify package integrity by comparing computed SHA-256 against expected fingerprint.
|
|
18
|
+
*
|
|
19
|
+
* @param filePath - Absolute path to the downloaded package
|
|
20
|
+
* @param expectedFingerprint - Expected SHA-256 hash from market_releases.fingerprint
|
|
21
|
+
* @throws {PackageIntegrityError} If computed hash does not match expected fingerprint
|
|
22
|
+
*/
|
|
23
|
+
export declare function verifyPackageIntegrity(filePath: string, expectedFingerprint: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Assert package integrity and throw clear error on mismatch.
|
|
26
|
+
* Alias for verifyPackageIntegrity with more explicit naming.
|
|
27
|
+
*
|
|
28
|
+
* @param filePath - Absolute path to the downloaded package
|
|
29
|
+
* @param expectedFingerprint - Expected SHA-256 hash from market_releases.fingerprint
|
|
30
|
+
* @throws {PackageIntegrityError} If computed hash does not match expected fingerprint
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertPackageIntegrity(filePath: string, expectedFingerprint: string): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when package integrity verification fails.
|
|
5
|
+
*/
|
|
6
|
+
export class PackageIntegrityError extends Error {
|
|
7
|
+
expected;
|
|
8
|
+
actual;
|
|
9
|
+
constructor(expected, actual) {
|
|
10
|
+
super(`Package integrity check failed: expected SHA-256 ${expected}, got ${actual}`);
|
|
11
|
+
this.expected = expected;
|
|
12
|
+
this.actual = actual;
|
|
13
|
+
this.name = "PackageIntegrityError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Compute SHA-256 hash of a file.
|
|
18
|
+
*
|
|
19
|
+
* @param filePath - Absolute path to the file
|
|
20
|
+
* @returns Promise resolving to hex-encoded SHA-256 hash
|
|
21
|
+
*/
|
|
22
|
+
export async function computeFileSha256(filePath) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const hash = createHash("sha256");
|
|
25
|
+
const stream = createReadStream(filePath);
|
|
26
|
+
stream.on("data", (chunk) => {
|
|
27
|
+
hash.update(chunk);
|
|
28
|
+
});
|
|
29
|
+
stream.on("end", () => {
|
|
30
|
+
resolve(hash.digest("hex"));
|
|
31
|
+
});
|
|
32
|
+
stream.on("error", (error) => {
|
|
33
|
+
reject(new Error(`Failed to compute SHA-256 for ${filePath}: ${error.message}`));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Verify package integrity by comparing computed SHA-256 against expected fingerprint.
|
|
39
|
+
*
|
|
40
|
+
* @param filePath - Absolute path to the downloaded package
|
|
41
|
+
* @param expectedFingerprint - Expected SHA-256 hash from market_releases.fingerprint
|
|
42
|
+
* @throws {PackageIntegrityError} If computed hash does not match expected fingerprint
|
|
43
|
+
*/
|
|
44
|
+
export async function verifyPackageIntegrity(filePath, expectedFingerprint) {
|
|
45
|
+
const actualHash = await computeFileSha256(filePath);
|
|
46
|
+
if (actualHash !== expectedFingerprint) {
|
|
47
|
+
throw new PackageIntegrityError(expectedFingerprint, actualHash);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Assert package integrity and throw clear error on mismatch.
|
|
52
|
+
* Alias for verifyPackageIntegrity with more explicit naming.
|
|
53
|
+
*
|
|
54
|
+
* @param filePath - Absolute path to the downloaded package
|
|
55
|
+
* @param expectedFingerprint - Expected SHA-256 hash from market_releases.fingerprint
|
|
56
|
+
* @throws {PackageIntegrityError} If computed hash does not match expected fingerprint
|
|
57
|
+
*/
|
|
58
|
+
export async function assertPackageIntegrity(filePath, expectedFingerprint) {
|
|
59
|
+
await verifyPackageIntegrity(filePath, expectedFingerprint);
|
|
60
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type PackageResult = {
|
|
2
|
+
buffer: Buffer;
|
|
3
|
+
sizeBytes: number;
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
};
|
|
6
|
+
export type PackageOptions = {
|
|
7
|
+
agentDir: string;
|
|
8
|
+
manifestPath?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare class AgentManifestNotFoundError extends Error {
|
|
11
|
+
constructor(agentDir: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class PackageTooLargeError extends Error {
|
|
14
|
+
constructor(sizeBytes: number, limitBytes: number);
|
|
15
|
+
}
|
|
16
|
+
export declare class PackagingError extends Error {
|
|
17
|
+
constructor(message: string, cause?: unknown);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Packages an agent directory into a tar.gz archive.
|
|
21
|
+
*
|
|
22
|
+
* Per D-01, D-02, D-03, D-04:
|
|
23
|
+
* - Format: tar.gz (standard Unix archive with gzip compression)
|
|
24
|
+
* - Contents: manifest (openclaw.agent.json) + definition files only
|
|
25
|
+
* - Manifest: embedded at tar.gz root
|
|
26
|
+
* - Fingerprint: SHA-256 over the tar.gz byte stream
|
|
27
|
+
*
|
|
28
|
+
* @param options - Packaging options
|
|
29
|
+
* @returns Package result with buffer, size, and fingerprint
|
|
30
|
+
* @throws {AgentManifestNotFoundError} if openclaw.agent.json is missing
|
|
31
|
+
* @throws {PackageTooLargeError} if package exceeds 10 MiB limit
|
|
32
|
+
* @throws {PackagingError} if tar operation fails
|
|
33
|
+
*/
|
|
34
|
+
export declare function packageAgentDirectory(options: PackageOptions): Promise<PackageResult>;
|