@perkos/perkos-a2a 0.12.29 → 0.12.30
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 +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +128 -33
- package/dist/index.js.map +4 -4
- package/dist/repair-cli.js +31 -2
- package/dist/repair-cli.js.map +1 -1
- package/dist/repair.d.ts +30 -2
- package/dist/repair.d.ts.map +1 -1
- package/dist/repair.js +303 -43
- package/dist/repair.js.map +1 -1
- package/dist/runtime-evidence.d.ts +42 -0
- package/dist/runtime-evidence.d.ts.map +1 -0
- package/dist/runtime-evidence.js +81 -0
- package/dist/runtime-evidence.js.map +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/repair.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { chmod, mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { configFingerprint, runtimeAttemptPath, runtimeStatusPath, } from "./runtime-evidence.js";
|
|
8
|
+
export class RepairFailure extends Error {
|
|
9
|
+
code;
|
|
10
|
+
phase;
|
|
11
|
+
retryable;
|
|
12
|
+
requiresProductFix;
|
|
13
|
+
constructor(code, phase, message, retryable, requiresProductFix) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.phase = phase;
|
|
17
|
+
this.retryable = retryable;
|
|
18
|
+
this.requiresProductFix = requiresProductFix;
|
|
19
|
+
this.name = "RepairFailure";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
7
22
|
const PROTECTED_PATHS = [
|
|
8
23
|
"agentName",
|
|
9
24
|
"relay.apiKey",
|
|
@@ -58,6 +73,16 @@ function assertProtected(before, after) {
|
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
}
|
|
76
|
+
function redactProtectedValues(message, config) {
|
|
77
|
+
let redacted = message;
|
|
78
|
+
for (const dotted of PROTECTED_PATHS) {
|
|
79
|
+
const value = getAt(config, dotted);
|
|
80
|
+
if (typeof value === "string" && value.length >= 4) {
|
|
81
|
+
redacted = redacted.split(value).join("[REDACTED]");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return redacted;
|
|
85
|
+
}
|
|
61
86
|
async function pathExists(path) {
|
|
62
87
|
try {
|
|
63
88
|
await stat(path);
|
|
@@ -83,6 +108,7 @@ export async function writeJsonConfig(path, value, renameFile = rename) {
|
|
|
83
108
|
await writeFile(temporary, serialized, { mode: 0o600 });
|
|
84
109
|
try {
|
|
85
110
|
await renameFile(temporary, path);
|
|
111
|
+
return "atomic";
|
|
86
112
|
}
|
|
87
113
|
catch (error) {
|
|
88
114
|
const code = errorCode(error);
|
|
@@ -98,6 +124,7 @@ export async function writeJsonConfig(path, value, renameFile = rename) {
|
|
|
98
124
|
finally {
|
|
99
125
|
await handle.close();
|
|
100
126
|
}
|
|
127
|
+
return "inplace-ebusy-fallback";
|
|
101
128
|
}
|
|
102
129
|
finally {
|
|
103
130
|
await rm(temporary, { force: true });
|
|
@@ -221,13 +248,17 @@ export function postRestartStateFromLogs(output, agentName, startedAt) {
|
|
|
221
248
|
return verifyLogState(logTextSince(output, startedAt), agentName);
|
|
222
249
|
}
|
|
223
250
|
function verifyLogState(text, agentName) {
|
|
251
|
+
const heartbeatCount = text.match(/\[perkos-heartbeat\].*status=200/gu)?.length ?? 0;
|
|
224
252
|
return {
|
|
253
|
+
configLoaded: false,
|
|
225
254
|
connected: text.includes("Connected to relay hub"),
|
|
226
255
|
registered: text.includes("Registered with relay hub"),
|
|
227
|
-
heartbeat200:
|
|
256
|
+
heartbeat200: heartbeatCount > 0,
|
|
257
|
+
heartbeatCount,
|
|
228
258
|
chatAuthed: text.includes(`[perkos-chat] authed as agent:${agentName}`),
|
|
229
259
|
duplicatePlugin: text.toLowerCase().includes("duplicate plugin id detected"),
|
|
230
260
|
duplicateRegistration: text.toLowerCase().includes("duplicate registration replaced"),
|
|
261
|
+
stableSeconds: 0,
|
|
231
262
|
};
|
|
232
263
|
}
|
|
233
264
|
export const defaultCommandRunner = (command, args) => new Promise((resolveResult, reject) => {
|
|
@@ -296,6 +327,115 @@ function assertUnder(parent, child) {
|
|
|
296
327
|
return;
|
|
297
328
|
throw new Error("managed install escaped the OpenClaw npm root");
|
|
298
329
|
}
|
|
330
|
+
async function sha256File(path) {
|
|
331
|
+
return createHash("sha256").update(await readFile(path)).digest("hex");
|
|
332
|
+
}
|
|
333
|
+
async function readJsonOptional(path) {
|
|
334
|
+
try {
|
|
335
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function writePrivateJson(path, value) {
|
|
342
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
343
|
+
const mode = await writeJsonConfig(path, value);
|
|
344
|
+
await chmod(path, 0o600);
|
|
345
|
+
return mode;
|
|
346
|
+
}
|
|
347
|
+
function repairStateRoot(stateDir) {
|
|
348
|
+
return join(stateDir, "perkos-a2a");
|
|
349
|
+
}
|
|
350
|
+
function receiptPath(stateDir, version, artifactSha256) {
|
|
351
|
+
return join(repairStateRoot(stateDir), "receipts", `${version}-${artifactSha256}.json`);
|
|
352
|
+
}
|
|
353
|
+
function journalPath(stateDir) {
|
|
354
|
+
return join(repairStateRoot(stateDir), "repair-journal.json");
|
|
355
|
+
}
|
|
356
|
+
async function inspectManagedInstall(input) {
|
|
357
|
+
const inspectResult = await mustRun(input.run, input.openclaw, ["plugins", "inspect", "perkos-a2a", "--json"], "plugin inspection");
|
|
358
|
+
const inspect = parseJsonOutput(inspectResult.stdout);
|
|
359
|
+
if (!isObject(inspect))
|
|
360
|
+
throw new Error("plugin inspection result is invalid");
|
|
361
|
+
const plugin = isObject(inspect.plugin) ? inspect.plugin : {};
|
|
362
|
+
const install = isObject(inspect.install) ? inspect.install : {};
|
|
363
|
+
const installPathValue = install.installPath ?? plugin.installPath;
|
|
364
|
+
if (plugin.id !== "perkos-a2a" || typeof installPathValue !== "string" || typeof plugin.rootDir !== "string") {
|
|
365
|
+
throw new Error("managed npm install was not recorded");
|
|
366
|
+
}
|
|
367
|
+
const npmRoot = await realpath(join(input.stateDir, "npm"));
|
|
368
|
+
const managedPath = await realpath(installPathValue);
|
|
369
|
+
const activeRoot = await realpath(plugin.rootDir);
|
|
370
|
+
assertUnder(npmRoot, managedPath);
|
|
371
|
+
if (activeRoot !== managedPath)
|
|
372
|
+
throw new Error("PerkOS is still loading from a non-managed plugin source");
|
|
373
|
+
const packagePath = join(managedPath, "package.json");
|
|
374
|
+
const manifestPath = join(managedPath, "openclaw.plugin.json");
|
|
375
|
+
const indexPath = join(managedPath, "dist", "index.js");
|
|
376
|
+
const repairCliPath = join(managedPath, "dist", "repair-cli.js");
|
|
377
|
+
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
378
|
+
const installedManifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
379
|
+
const bin = isObject(packageJson.bin) ? packageJson.bin : {};
|
|
380
|
+
if (packageJson.name !== "@perkos/perkos-a2a"
|
|
381
|
+
|| packageJson.version !== input.targetVersion
|
|
382
|
+
|| installedManifest.id !== "perkos-a2a"
|
|
383
|
+
|| installedManifest.version !== input.targetVersion
|
|
384
|
+
|| bin["perkos-a2a"] !== "dist/repair-cli.js"
|
|
385
|
+
|| !await pathExists(indexPath)
|
|
386
|
+
|| !await pathExists(repairCliPath)) {
|
|
387
|
+
throw new Error("installed plugin identity/version does not match the repair target");
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
installRoot: managedPath,
|
|
391
|
+
artifactKind: typeof install.artifactKind === "string" ? install.artifactKind : undefined,
|
|
392
|
+
source: typeof install.source === "string" ? install.source : typeof plugin.source === "string" ? plugin.source : undefined,
|
|
393
|
+
spec: typeof install.spec === "string" ? install.spec : typeof plugin.spec === "string" ? plugin.spec : undefined,
|
|
394
|
+
packageJsonHash: await sha256File(packagePath),
|
|
395
|
+
manifestHash: await sha256File(manifestPath),
|
|
396
|
+
indexHash: await sha256File(indexPath),
|
|
397
|
+
repairCliHash: await sha256File(repairCliPath),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function receiptMatches(receipt, evidence, version, artifactSha256) {
|
|
401
|
+
return Boolean(receipt)
|
|
402
|
+
&& receipt?.schemaVersion === 1
|
|
403
|
+
&& receipt.targetVersion === version
|
|
404
|
+
&& receipt.artifactSha256 === artifactSha256
|
|
405
|
+
&& receipt.installRootRealpath === evidence.installRoot
|
|
406
|
+
&& receipt.packageJsonHash === evidence.packageJsonHash
|
|
407
|
+
&& receipt.manifestHash === evidence.manifestHash
|
|
408
|
+
&& receipt.indexHash === evidence.indexHash
|
|
409
|
+
&& receipt.repairCliHash === evidence.repairCliHash;
|
|
410
|
+
}
|
|
411
|
+
function mergeWriteMode(current, next) {
|
|
412
|
+
return current === "inplace-ebusy-fallback" || next === "inplace-ebusy-fallback"
|
|
413
|
+
? "inplace-ebusy-fallback"
|
|
414
|
+
: "atomic";
|
|
415
|
+
}
|
|
416
|
+
function configLoadedLogMatches(text, input) {
|
|
417
|
+
return text.includes(`[perkos-a2a] config loaded version=${input.version} agentId=${input.agentId} attemptId=${input.attemptId} fingerprint=${input.fingerprint}`);
|
|
418
|
+
}
|
|
419
|
+
async function runtimeStatusMatches(path, input) {
|
|
420
|
+
const status = await readJsonOptional(path);
|
|
421
|
+
if (!status)
|
|
422
|
+
return false;
|
|
423
|
+
const loadedAt = Date.parse(status.loadedAt);
|
|
424
|
+
if (status.schemaVersion !== 1
|
|
425
|
+
|| status.version !== input.version
|
|
426
|
+
|| status.agentId !== input.agentId
|
|
427
|
+
|| status.attemptId !== input.attemptId
|
|
428
|
+
|| status.fingerprint !== input.fingerprint
|
|
429
|
+
|| !Number.isFinite(loadedAt)
|
|
430
|
+
|| loadedAt < input.startedAt - 2_000)
|
|
431
|
+
return false;
|
|
432
|
+
try {
|
|
433
|
+
return await realpath(status.pluginRoot) === input.installRoot;
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
299
439
|
async function wait(ms) {
|
|
300
440
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
301
441
|
}
|
|
@@ -312,17 +452,46 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
312
452
|
throw new Error("repair CLI manifest/schema is invalid");
|
|
313
453
|
if (!await pathExists(artifactPath))
|
|
314
454
|
throw new Error("verified plugin artifact is missing");
|
|
315
|
-
const artifactDigest =
|
|
455
|
+
const artifactDigest = await sha256File(artifactPath);
|
|
316
456
|
if (artifactDigest !== options.artifactSha256.toLowerCase())
|
|
317
457
|
throw new Error(`plugin artifact checksum mismatch: ${artifactDigest}`);
|
|
318
458
|
const releaseRepairLock = await acquireRepairLock(stateDir);
|
|
319
459
|
try {
|
|
460
|
+
const priorJournal = await readJsonOptional(journalPath(stateDir));
|
|
461
|
+
const canResume = priorJournal?.schemaVersion === 1
|
|
462
|
+
&& priorJournal.phase !== "done"
|
|
463
|
+
&& priorJournal.targetVersion === version
|
|
464
|
+
&& priorJournal.artifactSha256 === artifactDigest
|
|
465
|
+
&& priorJournal.expectedAgentName === options.expectedAgentName
|
|
466
|
+
&& priorJournal.expectedAgentId === options.expectedAgentId;
|
|
467
|
+
const resumedFromPhase = canResume ? priorJournal.phase : undefined;
|
|
468
|
+
let journal = canResume ? priorJournal : {
|
|
469
|
+
schemaVersion: 1,
|
|
470
|
+
attemptId: randomUUID(),
|
|
471
|
+
hmacKeyHex: randomBytes(32).toString("hex"),
|
|
472
|
+
targetVersion: version,
|
|
473
|
+
artifactSha256: artifactDigest,
|
|
474
|
+
expectedAgentName: options.expectedAgentName,
|
|
475
|
+
expectedAgentId: options.expectedAgentId,
|
|
476
|
+
phase: "detect",
|
|
477
|
+
createdAt: new Date().toISOString(),
|
|
478
|
+
updatedAt: new Date().toISOString(),
|
|
479
|
+
};
|
|
480
|
+
const setPhase = async (phase) => {
|
|
481
|
+
journal = { ...journal, phase, updatedAt: new Date().toISOString() };
|
|
482
|
+
await writePrivateJson(journalPath(stateDir), journal);
|
|
483
|
+
};
|
|
484
|
+
await setPhase("detect");
|
|
320
485
|
const backupRoot = join(stateDir, "perkos-migration-backups");
|
|
321
486
|
await mkdir(backupRoot, { recursive: true, mode: 0o700 });
|
|
322
|
-
const
|
|
487
|
+
const reusableBackup = canResume && priorJournal.backupPath && await pathExists(priorJournal.backupPath)
|
|
488
|
+
? priorJournal.backupPath
|
|
489
|
+
: undefined;
|
|
490
|
+
const backupPath = reusableBackup ?? join(backupRoot, `openclaw-${Date.now()}.before.json`);
|
|
323
491
|
const legacyExtension = join(stateDir, "extensions", "perkos-a2a");
|
|
324
492
|
const quarantinePath = join(backupRoot, `perkos-a2a-extension-${Date.now()}`);
|
|
325
493
|
const originalRaw = await readFile(configPath, "utf8");
|
|
494
|
+
const identitySourceRaw = reusableBackup ? await readFile(reusableBackup, "utf8") : originalRaw;
|
|
326
495
|
let document;
|
|
327
496
|
try {
|
|
328
497
|
document = JSON.parse(originalRaw);
|
|
@@ -330,21 +499,38 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
330
499
|
catch {
|
|
331
500
|
throw new Error("OpenClaw config must be strict JSON for safe automatic repair");
|
|
332
501
|
}
|
|
333
|
-
|
|
502
|
+
let identitySource;
|
|
503
|
+
try {
|
|
504
|
+
identitySource = JSON.parse(identitySourceRaw);
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
throw new Error("OpenClaw repair backup must be strict JSON");
|
|
508
|
+
}
|
|
509
|
+
const existingConfig = openClawConfig(identitySource);
|
|
334
510
|
const before = structuredClone(existingConfig ?? options.initialConfig);
|
|
335
511
|
if (!isObject(before))
|
|
336
512
|
throw new Error("existing PerkOS plugin config is missing");
|
|
337
|
-
|
|
513
|
+
if (!reusableBackup) {
|
|
514
|
+
await writeFile(backupPath, originalRaw, { mode: 0o600 });
|
|
515
|
+
await chmod(backupPath, 0o600);
|
|
516
|
+
}
|
|
517
|
+
journal = { ...journal, backupPath };
|
|
518
|
+
await setPhase("snapshot");
|
|
338
519
|
const normalized = normalizeToSchema(before, manifest.configSchema);
|
|
339
520
|
if (!isObject(normalized.value))
|
|
340
521
|
throw new Error("normalized PerkOS config is invalid");
|
|
341
522
|
assertProtected(before, normalized.value);
|
|
342
523
|
assertIdentity(normalized.value, options.expectedAgentName, options.expectedAgentId);
|
|
343
524
|
let quarantined = false;
|
|
344
|
-
let
|
|
525
|
+
let installCommitted = journal.installCommitted === true;
|
|
526
|
+
let configCommitted = false;
|
|
527
|
+
let configWriteMode = "atomic";
|
|
528
|
+
let receiptUsed = false;
|
|
529
|
+
let receiptCreated = false;
|
|
530
|
+
let installEvidence;
|
|
345
531
|
try {
|
|
346
532
|
replacePluginEntry(document, {}, false);
|
|
347
|
-
await writeJsonConfig(configPath, document);
|
|
533
|
+
configWriteMode = mergeWriteMode(configWriteMode, await writeJsonConfig(configPath, document));
|
|
348
534
|
if (await pathExists(legacyExtension)) {
|
|
349
535
|
const legacyManifest = JSON.parse(await readFile(join(legacyExtension, "openclaw.plugin.json"), "utf8"));
|
|
350
536
|
if (legacyManifest.id !== "perkos-a2a")
|
|
@@ -352,43 +538,75 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
352
538
|
await rename(legacyExtension, quarantinePath);
|
|
353
539
|
quarantined = true;
|
|
354
540
|
}
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
541
|
+
await setPhase("install");
|
|
542
|
+
const receiptFile = receiptPath(stateDir, version, artifactDigest);
|
|
543
|
+
const receipt = await readJsonOptional(receiptFile);
|
|
544
|
+
if (receipt) {
|
|
545
|
+
try {
|
|
546
|
+
const candidate = await inspectManagedInstall({ run, openclaw, stateDir, targetVersion: version });
|
|
547
|
+
if (receiptMatches(receipt, candidate, version, artifactDigest)) {
|
|
548
|
+
installEvidence = candidate;
|
|
549
|
+
receiptUsed = true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch { /* reinstall and certify from the verified artifact */ }
|
|
359
553
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
554
|
+
if (!installEvidence) {
|
|
555
|
+
const installResult = await run(openclaw, ["plugins", "install", `npm-pack:${artifactPath}`, "--force"]);
|
|
556
|
+
const installError = `${installResult.stderr}\n${installResult.stdout}`;
|
|
557
|
+
if (installResult.code !== 0 && !/EBUSY|resource busy|rename/iu.test(installError)) {
|
|
558
|
+
throw new Error(`managed plugin install failed: ${installError.trim().slice(0, 500)}`);
|
|
559
|
+
}
|
|
560
|
+
installCommitted = true;
|
|
561
|
+
journal = { ...journal, installCommitted: true };
|
|
562
|
+
await setPhase("install");
|
|
563
|
+
installEvidence = await inspectManagedInstall({ run, openclaw, stateDir, targetVersion: version });
|
|
564
|
+
const newReceipt = {
|
|
565
|
+
schemaVersion: 1,
|
|
566
|
+
targetVersion: version,
|
|
567
|
+
artifactSha256: artifactDigest,
|
|
568
|
+
packageJsonHash: installEvidence.packageJsonHash,
|
|
569
|
+
manifestHash: installEvidence.manifestHash,
|
|
570
|
+
indexHash: installEvidence.indexHash,
|
|
571
|
+
repairCliHash: installEvidence.repairCliHash,
|
|
572
|
+
installRootRealpath: installEvidence.installRoot,
|
|
573
|
+
createdAt: new Date().toISOString(),
|
|
574
|
+
};
|
|
575
|
+
await writePrivateJson(receiptFile, newReceipt);
|
|
576
|
+
receiptCreated = true;
|
|
368
577
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
assertUnder(npmRoot, managedPath);
|
|
373
|
-
if (activeRoot !== managedPath)
|
|
374
|
-
throw new Error("PerkOS is still loading from a non-managed plugin source");
|
|
375
|
-
const installedManifest = JSON.parse(await readFile(join(managedPath, "openclaw.plugin.json"), "utf8"));
|
|
376
|
-
if (installedManifest.id !== "perkos-a2a" || installedManifest.version !== version)
|
|
377
|
-
throw new Error("installed plugin version does not match repair CLI");
|
|
578
|
+
installCommitted = true;
|
|
579
|
+
journal = { ...journal, installCommitted: true };
|
|
580
|
+
await setPhase("migrate-config");
|
|
378
581
|
const currentRaw = await readFile(configPath, "utf8");
|
|
379
582
|
const currentDocument = JSON.parse(currentRaw);
|
|
380
583
|
replacePluginEntry(currentDocument, normalized.value, true);
|
|
381
|
-
await writeJsonConfig(configPath, currentDocument);
|
|
584
|
+
configWriteMode = mergeWriteMode(configWriteMode, await writeJsonConfig(configPath, currentDocument));
|
|
585
|
+
configCommitted = true;
|
|
586
|
+
await setPhase("validate-config");
|
|
382
587
|
await mustRun(run, openclaw, ["config", "validate"], "config validation");
|
|
383
588
|
const doctor = await mustRun(run, openclaw, ["plugins", "doctor"], "plugin doctor");
|
|
384
589
|
const doctorText = `${doctor.stdout}\n${doctor.stderr}`.toLowerCase();
|
|
385
590
|
if (doctorText.includes("duplicate plugin id detected"))
|
|
386
591
|
throw new Error("duplicate plugin source remains after migration");
|
|
387
|
-
|
|
592
|
+
const activeEvidence = await inspectManagedInstall({ run, openclaw, stateDir, targetVersion: version });
|
|
593
|
+
if (activeEvidence.installRoot !== installEvidence.installRoot)
|
|
594
|
+
throw new Error("managed plugin root changed during config migration");
|
|
388
595
|
const emptyPostRestart = verifyLogState("", options.expectedAgentName);
|
|
389
596
|
const restartResult = { requested: false, verified: false };
|
|
390
597
|
let postRestart = emptyPostRestart;
|
|
598
|
+
const expectedFingerprint = configFingerprint(normalized.value, journal.hmacKeyHex);
|
|
391
599
|
if (!options.skipRestart) {
|
|
600
|
+
const runtimeAttempt = {
|
|
601
|
+
schemaVersion: 1,
|
|
602
|
+
attemptId: journal.attemptId,
|
|
603
|
+
targetVersion: version,
|
|
604
|
+
expectedAgentId: options.expectedAgentId,
|
|
605
|
+
hmacKeyHex: journal.hmacKeyHex,
|
|
606
|
+
createdAt: new Date().toISOString(),
|
|
607
|
+
};
|
|
608
|
+
await writePrivateJson(runtimeAttemptPath(stateDir), runtimeAttempt);
|
|
609
|
+
await setPhase("restart");
|
|
392
610
|
const beforeStatusResult = await mustRun(run, openclaw, ["gateway", "status", "--json"], "pre-restart gateway status");
|
|
393
611
|
const beforeStatus = await processIdentityWithStart(run, parseJsonOutput(beforeStatusResult.stdout));
|
|
394
612
|
restartResult.beforePid = beforeStatus.pid;
|
|
@@ -400,11 +618,11 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
400
618
|
const permissionRaw = await readFile(configPath, "utf8");
|
|
401
619
|
const permissionDocument = JSON.parse(permissionRaw);
|
|
402
620
|
const previousPermission = enableExternalRestart(permissionDocument);
|
|
403
|
-
await writeJsonConfig(configPath, permissionDocument);
|
|
621
|
+
configWriteMode = mergeWriteMode(configWriteMode, await writeJsonConfig(configPath, permissionDocument));
|
|
404
622
|
restoreRestartPermission = async () => {
|
|
405
623
|
const latest = JSON.parse(await readFile(configPath, "utf8"));
|
|
406
624
|
restoreExternalRestart(latest, previousPermission);
|
|
407
|
-
await writeJsonConfig(configPath, latest);
|
|
625
|
+
configWriteMode = mergeWriteMode(configWriteMode, await writeJsonConfig(configPath, latest));
|
|
408
626
|
};
|
|
409
627
|
await wait(options.pollIntervalMs ?? 2_000);
|
|
410
628
|
await mustRun(run, "kill", ["-USR1", "1"], "PID 1 in-process gateway restart");
|
|
@@ -415,8 +633,10 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
415
633
|
restartResult.method = "service";
|
|
416
634
|
}
|
|
417
635
|
restartResult.requested = true;
|
|
418
|
-
|
|
419
|
-
const
|
|
636
|
+
await setPhase("runtime-checks");
|
|
637
|
+
const stabilityWindowMs = options.stabilityWindowMs ?? 180_000;
|
|
638
|
+
const requiredHeartbeatCount = options.requiredHeartbeatCount ?? 3;
|
|
639
|
+
const deadline = Date.now() + (options.timeoutMs ?? 300_000);
|
|
420
640
|
let healthySince;
|
|
421
641
|
let recentLogs = "";
|
|
422
642
|
while (Date.now() < deadline) {
|
|
@@ -429,10 +649,24 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
429
649
|
const afterStatus = await processIdentityWithStart(run, parseJsonOutput(status.stdout));
|
|
430
650
|
restartResult.afterPid = afterStatus.pid;
|
|
431
651
|
restartResult.afterStartTime = afterStatus.startTime;
|
|
432
|
-
const logs = await run(openclaw, ["logs", "--json", "--limit", "
|
|
652
|
+
const logs = await run(openclaw, ["logs", "--json", "--limit", "2000", "--max-bytes", "4000000"]);
|
|
433
653
|
if (logs.code === 0)
|
|
434
654
|
recentLogs = logs.stdout;
|
|
435
|
-
|
|
655
|
+
const freshText = logTextSince(recentLogs, restartStartedAt);
|
|
656
|
+
postRestart = verifyLogState(freshText, options.expectedAgentName);
|
|
657
|
+
postRestart.configLoaded = configLoadedLogMatches(freshText, {
|
|
658
|
+
version,
|
|
659
|
+
agentId: options.expectedAgentId,
|
|
660
|
+
attemptId: journal.attemptId,
|
|
661
|
+
fingerprint: expectedFingerprint,
|
|
662
|
+
}) || await runtimeStatusMatches(runtimeStatusPath(stateDir), {
|
|
663
|
+
version,
|
|
664
|
+
agentId: options.expectedAgentId,
|
|
665
|
+
attemptId: journal.attemptId,
|
|
666
|
+
fingerprint: expectedFingerprint,
|
|
667
|
+
installRoot: installEvidence.installRoot,
|
|
668
|
+
startedAt: restartStartedAt,
|
|
669
|
+
});
|
|
436
670
|
const processIdentityChanged = processChanged(beforeStatus, afterStatus);
|
|
437
671
|
const freshRestartLogs = restartObservedInLogs(recentLogs, restartStartedAt);
|
|
438
672
|
restartResult.verified = processIdentityChanged || (restartResult.method === "in-process-sigusr1" && freshRestartLogs);
|
|
@@ -442,9 +676,10 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
442
676
|
? "fresh-restart-logs"
|
|
443
677
|
: undefined;
|
|
444
678
|
const healthy = restartResult.verified
|
|
679
|
+
&& postRestart.configLoaded
|
|
445
680
|
&& postRestart.connected
|
|
446
681
|
&& postRestart.registered
|
|
447
|
-
&& postRestart.
|
|
682
|
+
&& postRestart.heartbeatCount >= requiredHeartbeatCount
|
|
448
683
|
&& postRestart.chatAuthed
|
|
449
684
|
&& !postRestart.duplicatePlugin
|
|
450
685
|
&& !postRestart.duplicateRegistration;
|
|
@@ -453,14 +688,20 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
453
688
|
continue;
|
|
454
689
|
}
|
|
455
690
|
healthySince ??= Date.now();
|
|
691
|
+
postRestart.stableSeconds = Math.floor((Date.now() - healthySince) / 1_000);
|
|
456
692
|
if (Date.now() - healthySince >= stabilityWindowMs)
|
|
457
693
|
break;
|
|
458
694
|
}
|
|
459
695
|
if (!restartResult.verified)
|
|
460
696
|
throw new Error("gateway restart was not verified by process identity or fresh in-process restart logs");
|
|
461
|
-
if (!postRestart.
|
|
697
|
+
if (!postRestart.configLoaded
|
|
698
|
+
|| !postRestart.connected
|
|
699
|
+
|| !postRestart.registered
|
|
700
|
+
|| postRestart.heartbeatCount < requiredHeartbeatCount
|
|
701
|
+
|| !postRestart.chatAuthed
|
|
702
|
+
|| postRestart.duplicatePlugin
|
|
703
|
+
|| postRestart.duplicateRegistration)
|
|
462
704
|
throw new Error(`post-restart verification failed: ${JSON.stringify(postRestart)}`);
|
|
463
|
-
}
|
|
464
705
|
if (healthySince === undefined || Date.now() - healthySince < stabilityWindowMs) {
|
|
465
706
|
throw new Error(`post-restart transport did not remain healthy for ${stabilityWindowMs}ms`);
|
|
466
707
|
}
|
|
@@ -469,12 +710,27 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
469
710
|
await restoreRestartPermission?.();
|
|
470
711
|
}
|
|
471
712
|
}
|
|
713
|
+
await setPhase("done");
|
|
472
714
|
return {
|
|
715
|
+
schemaVersion: 1,
|
|
473
716
|
ok: true,
|
|
717
|
+
phase: "done",
|
|
474
718
|
version,
|
|
475
719
|
mode: existingConfig ? "repaired" : "installed",
|
|
720
|
+
trustLevel: "trusted",
|
|
721
|
+
strictMode: true,
|
|
722
|
+
resumedFromPhase,
|
|
476
723
|
artifactVerified: true,
|
|
477
724
|
source: "managed-npm",
|
|
725
|
+
installEvidence: {
|
|
726
|
+
artifactKind: installEvidence.artifactKind,
|
|
727
|
+
source: installEvidence.source,
|
|
728
|
+
spec: installEvidence.spec,
|
|
729
|
+
receiptUsed,
|
|
730
|
+
receiptCreated,
|
|
731
|
+
installRoot: installEvidence.installRoot,
|
|
732
|
+
},
|
|
733
|
+
configWriteMode,
|
|
478
734
|
normalized: true,
|
|
479
735
|
removedUnsupportedPaths: normalized.removed,
|
|
480
736
|
quarantinedLegacyExtension: quarantined,
|
|
@@ -485,14 +741,18 @@ export async function repairOpenClaw(options, run = defaultCommandRunner) {
|
|
|
485
741
|
};
|
|
486
742
|
}
|
|
487
743
|
catch (error) {
|
|
488
|
-
if (!
|
|
744
|
+
if (!installCommitted) {
|
|
489
745
|
await writeFile(configPath, originalRaw, { mode: 0o600 });
|
|
490
746
|
if (quarantined && await pathExists(quarantinePath) && !await pathExists(legacyExtension)) {
|
|
491
747
|
await mkdir(dirname(legacyExtension), { recursive: true });
|
|
492
748
|
await rename(quarantinePath, legacyExtension);
|
|
493
749
|
}
|
|
494
750
|
}
|
|
495
|
-
|
|
751
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
752
|
+
const message = redactProtectedValues(rawMessage, before);
|
|
753
|
+
if (error instanceof RepairFailure)
|
|
754
|
+
throw error;
|
|
755
|
+
throw new RepairFailure(installCommitted && !configCommitted ? "REPAIR_RESUME_REQUIRED" : "REPAIR_FAILED", journal.phase, message, installCommitted, false);
|
|
496
756
|
}
|
|
497
757
|
}
|
|
498
758
|
finally {
|