@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.
Files changed (80) hide show
  1. package/README.md +217 -0
  2. package/dist/api.d.ts +6 -0
  3. package/dist/api.js +6 -0
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.js +324 -0
  6. package/dist/runtime-api.d.ts +1 -0
  7. package/dist/runtime-api.js +1 -0
  8. package/dist/setup-entry.d.ts +4 -0
  9. package/dist/setup-entry.js +3 -0
  10. package/dist/src/agency-install/claimLoop.d.ts +41 -0
  11. package/dist/src/agency-install/claimLoop.js +188 -0
  12. package/dist/src/agent-panel/claimLoop.d.ts +32 -0
  13. package/dist/src/agent-panel/claimLoop.js +118 -0
  14. package/dist/src/agent-panel/executor.d.ts +8 -0
  15. package/dist/src/agent-panel/executor.js +368 -0
  16. package/dist/src/agent-panel/plan.d.ts +20 -0
  17. package/dist/src/agent-panel/plan.js +111 -0
  18. package/dist/src/agent-panel/prompts.d.ts +38 -0
  19. package/dist/src/agent-panel/prompts.js +87 -0
  20. package/dist/src/agent-panel/types.d.ts +45 -0
  21. package/dist/src/agent-panel/types.js +1 -0
  22. package/dist/src/agents.d.ts +44 -0
  23. package/dist/src/agents.js +195 -0
  24. package/dist/src/authorization.d.ts +34 -0
  25. package/dist/src/authorization.js +183 -0
  26. package/dist/src/channel.d.ts +5 -0
  27. package/dist/src/channel.js +93 -0
  28. package/dist/src/claimPolling.d.ts +9 -0
  29. package/dist/src/claimPolling.js +13 -0
  30. package/dist/src/client.d.ts +386 -0
  31. package/dist/src/client.js +595 -0
  32. package/dist/src/config.d.ts +28 -0
  33. package/dist/src/config.js +94 -0
  34. package/dist/src/defaults.d.ts +40 -0
  35. package/dist/src/defaults.js +52 -0
  36. package/dist/src/deviceCode.d.ts +16 -0
  37. package/dist/src/deviceCode.js +65 -0
  38. package/dist/src/heartbeat.d.ts +25 -0
  39. package/dist/src/heartbeat.js +65 -0
  40. package/dist/src/imageAttachments.d.ts +14 -0
  41. package/dist/src/imageAttachments.js +81 -0
  42. package/dist/src/inbound.d.ts +10 -0
  43. package/dist/src/inbound.js +140 -0
  44. package/dist/src/installMutex.d.ts +4 -0
  45. package/dist/src/installMutex.js +18 -0
  46. package/dist/src/market-install/claimLoop.d.ts +41 -0
  47. package/dist/src/market-install/claimLoop.js +183 -0
  48. package/dist/src/market-install/downloader.d.ts +36 -0
  49. package/dist/src/market-install/downloader.js +133 -0
  50. package/dist/src/market-install/executor.d.ts +28 -0
  51. package/dist/src/market-install/executor.js +352 -0
  52. package/dist/src/market-install/types.d.ts +62 -0
  53. package/dist/src/market-install/types.js +1 -0
  54. package/dist/src/market-install/verifier.d.ts +32 -0
  55. package/dist/src/market-install/verifier.js +60 -0
  56. package/dist/src/market-publish/packager.d.ts +34 -0
  57. package/dist/src/market-publish/packager.js +168 -0
  58. package/dist/src/market-publish/publishFlow.d.ts +70 -0
  59. package/dist/src/market-publish/publishFlow.js +107 -0
  60. package/dist/src/market-publish/uploader.d.ts +73 -0
  61. package/dist/src/market-publish/uploader.js +132 -0
  62. package/dist/src/openclawVersion.d.ts +4 -0
  63. package/dist/src/openclawVersion.js +13 -0
  64. package/dist/src/outbound.d.ts +13 -0
  65. package/dist/src/outbound.js +41 -0
  66. package/dist/src/pairing.d.ts +32 -0
  67. package/dist/src/pairing.js +64 -0
  68. package/dist/src/runtime.d.ts +52 -0
  69. package/dist/src/runtime.js +26 -0
  70. package/dist/src/telemetry.d.ts +34 -0
  71. package/dist/src/telemetry.js +89 -0
  72. package/dist/src/transport/realtime.d.ts +49 -0
  73. package/dist/src/transport/realtime.js +273 -0
  74. package/dist/src/transport.d.ts +38 -0
  75. package/dist/src/transport.js +15 -0
  76. package/dist/src/wake.d.ts +31 -0
  77. package/dist/src/wake.js +101 -0
  78. package/openclaw.config.example.yaml +10 -0
  79. package/openclaw.plugin.json +122 -0
  80. 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>;