@perkos/perkos-a2a 0.12.34 → 0.12.36

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 CHANGED
@@ -183,24 +183,24 @@ schema, and protects identity, relay/chat credentials, endpoints, runtime kind,
183
183
  and session key. Bind-mounted `openclaw.json` files are updated in place when an
184
184
  atomic rename is unavailable, while a persistent backup and the quarantined
185
185
  legacy extension remain recoverable. A process lock prevents two simultaneous
186
- repairs from registering the same agent. Version 0.12.34 writes the signed
186
+ repairs from registering the same agent. Version 0.12.35 writes the signed
187
187
  attempt before OpenClaw can load the newly enabled plugin, so a clean first
188
188
  generation started during `config validate` is attributable to that exact
189
189
  command rather than being rejected as pre-restart evidence.
190
190
 
191
191
  Every successful install is anchored by a private, content-addressed receipt;
192
192
  the resumable repair journal rolls forward after an install has committed
193
- instead of restoring an incompatible pre-upgrade source. On restart, the
194
- plugin writes HMAC-bound runtime evidence proving that the expected version and
195
- normalized config were actually loaded. The receipt, journal, attempt, and
196
- runtime status files are created with owner-only permissions.
197
-
198
- It validates config and plugin health, performs a verifiable restart, then reads
199
- only plugin-owned runtime evidence. It does not launch a second relay/chat client
200
- and does not scrape `openclaw gateway status` or gateway log files during the
201
- verification window. A normal service must return a new PID/start identity; a
202
- containerized PID 1 gateway restarts in-process through OpenClaw's authorized
203
- `SIGUSR1` path. Relay, registration, heartbeat, and chat authentication must
193
+ instead of restoring an incompatible pre-upgrade source. The plugin writes
194
+ HMAC-bound runtime evidence proving that the expected version and normalized
195
+ config were actually loaded. The receipt, journal, attempt, and runtime status
196
+ files are created with owner-only permissions.
197
+
198
+ It validates config and plugin health, then reads only plugin-owned runtime
199
+ evidence. It never mutates `commands.restart`, sends `SIGUSR1`, waits for an
200
+ OpenClaw gateway drain, launches a second relay/chat client, or scrapes gateway
201
+ status/logs. Clean installs complete through hot reload. Certified upgrades use
202
+ a fenced supervisor rollover while the gateway and its active model/runtime
203
+ remain untouched. Relay, registration, heartbeat, and chat authentication must
204
204
  remain healthy—with no duplicate plugin or registration messages—for three
205
205
  continuous minutes and at least three fresh successful heartbeats. Success or
206
206
  failure is emitted as one JSON object; secret values are never printed.
@@ -220,13 +220,15 @@ READY and accept the matching claim. The supervisor records its PID, process
220
220
  start identity, boot id, claim time, and transport counts in private health
221
221
  evidence so `connect` can distinguish a certified owner from stale files.
222
222
 
223
- For a clean first install, OpenClaw's PID 1 hot reload remains supported. When
224
- upgrading a pre-supervisor or otherwise uncertified installation, `connect`
225
- requires evidence that the gateway PID/start identity actually changed. If the
226
- current environment cannot perform that restart safely, it exits with the
227
- machine-readable action `ACTION_REQUIRED_REAL_RESTART`; rerunning the same
228
- idempotent command after the service/container restart resumes the migration
229
- without replacing identity or credentials.
223
+ For a clean first install, OpenClaw's PID 1 hot reload remains supported without
224
+ a gateway restart. A certified upgrade fences and stops the old supervisor,
225
+ waits for `STOPPED` plus socket/process exit, starts the supervisor CLI from the
226
+ new verified install root, and requires target-version health before opening a
227
+ single relay client. When upgrading a pre-supervisor or otherwise uncertified
228
+ installation, `connect` exits promptly with the machine-readable action
229
+ `ACTION_REQUIRED_REAL_RESTART`; rerunning the same idempotent command after an
230
+ out-of-band service/container restart resumes the migration without replacing
231
+ identity or credentials.
230
232
 
231
233
  On successful verification, `connect` removes the active-attempt file and marks
232
234
  the private repair journal `done`. Stale OpenClaw install metadata is reported
@@ -237,14 +239,16 @@ version; stale metadata is never allowed to weaken those identity checks.
237
239
  Each claim carries the parent PID/start identity, boot id, instance id, and
238
240
  generation. Before accepting a newer generation, the supervisor waits for
239
241
  confirmed transport shutdown; a timeout fails closed with
240
- `OWNER_STOP_TIMEOUT`. Lease and health evidence are owner-only files and never
242
+ `OWNER_STOP_TIMEOUT`. A private TTL rollover lock serializes concurrent upgrades,
243
+ and health certifies the supervisor package version and real install root. Lease
244
+ and health evidence are owner-only files and never
241
245
  contain relay or chat credentials. This ownership boundary applies to OpenClaw;
242
246
  Hermes/custom runtimes keep their existing runtime-owned model and transport
243
247
  routing.
244
248
 
245
249
  ```bash
246
250
  A2A_RELAY_API_KEY='<credential-from-invitation>' \
247
- npx --yes @perkos/perkos-a2a@0.12.34 connect \
251
+ npx --yes @perkos/perkos-a2a@0.12.36 connect \
248
252
  --artifact ./perkos-a2a.tgz \
249
253
  --sha256 '<sha256-from-the-official-invitation>' \
250
254
  --agent-name '<agent-name>' \
package/dist/index.d.ts CHANGED
@@ -53,5 +53,12 @@ export declare function runOpenClawChatTurn(api: any, config: A2APluginConfig, s
53
53
  export declare function formatRecentChatContext(messages: ChatMessage[], currentMessageId: string): string;
54
54
  /** The identity the relay treats as exclusive; names are display metadata. */
55
55
  export declare function a2aLifecycleKey(config: A2APluginConfig): string;
56
+ /**
57
+ * OpenClaw may briefly call register while a plugin entry is being rewritten.
58
+ * Treat partial/empty config as an inert, valid shape instead of crashing the
59
+ * whole service on string operations. The repair CLI restores the complete
60
+ * identity atomically before the service is enabled.
61
+ */
62
+ export declare function normalizeOpenClawPluginConfig(value: unknown): A2APluginConfig;
56
63
  export default function register(api: any): void;
57
64
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAGV,WAAW,EAEZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,cAAc,YAAY,CAAC;AA0C3B,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAItF;AAED,KAAK,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASpD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CA2BT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,oBAAoB,EAC3B,eAAe,CAAC,EAAE,MAAM,GACvB,oBAAoB,CA+BtB;AAiED,KAAK,qBAAqB,GAAG;IAC3B,QAAQ,CAAC,EAAE,KAAK,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC,CAAC;IACH,IAAI,CAAC,EAAE;QACL,yBAAyB,CAAC,EAAE,MAAM,CAAC;QACnC,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,CAYrF;AAQD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CAAE,GAClF,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuFxB;AAYD,oFAAoF;AACpF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,EAAE,EACvB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAWR;AA4CD,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAe/D;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAG,EAAE,GAAG,QAqzBxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAGV,WAAW,EAEZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,cAAc,YAAY,CAAC;AA0C3B,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAItF;AAED,KAAK,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASpD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CA2BT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,oBAAoB,EAC3B,eAAe,CAAC,EAAE,MAAM,GACvB,oBAAoB,CA+BtB;AAiED,KAAK,qBAAqB,GAAG;IAC3B,QAAQ,CAAC,EAAE,KAAK,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC,CAAC;IACH,IAAI,CAAC,EAAE;QACL,yBAAyB,CAAC,EAAE,MAAM,CAAC;QACnC,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,CAYrF;AAQD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CAAE,GAClF,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuFxB;AAYD,oFAAoF;AACpF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,EAAE,EACvB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAWR;AA4CD,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAe/D;AAMD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAc7E;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAG,EAAE,GAAG,QAozBxC"}
package/dist/index.js CHANGED
@@ -18429,7 +18429,7 @@ var require_view = __commonJS({
18429
18429
  var debug = require_src()("express:view");
18430
18430
  var path = __require("path");
18431
18431
  var fs = __require("fs");
18432
- var dirname3 = path.dirname;
18432
+ var dirname4 = path.dirname;
18433
18433
  var basename = path.basename;
18434
18434
  var extname = path.extname;
18435
18435
  var join4 = path.join;
@@ -18468,7 +18468,7 @@ var require_view = __commonJS({
18468
18468
  for (var i = 0; i < roots.length && !path2; i++) {
18469
18469
  var root = roots[i];
18470
18470
  var loc = resolve4(root, name);
18471
- var dir = dirname3(loc);
18471
+ var dir = dirname4(loc);
18472
18472
  var file = basename(loc);
18473
18473
  path2 = this.resolve(dir, file);
18474
18474
  }
@@ -27718,7 +27718,7 @@ var A2AServer = class {
27718
27718
  // src/index.ts
27719
27719
  import { randomUUID as randomUUID7 } from "crypto";
27720
27720
  import { readFileSync as readFileSync5 } from "node:fs";
27721
- import { dirname as dirname2, resolve as resolve3 } from "node:path";
27721
+ import { dirname as dirname3, resolve as resolve3 } from "node:path";
27722
27722
  import { fileURLToPath as fileURLToPath2 } from "node:url";
27723
27723
 
27724
27724
  // src/agentic-actions.ts
@@ -28339,10 +28339,10 @@ function isNotFound(err) {
28339
28339
  // src/openclaw-supervisor-client.ts
28340
28340
  import { randomUUID as randomUUID5 } from "node:crypto";
28341
28341
  import { spawn } from "node:child_process";
28342
- import { existsSync, readFileSync as readFileSync3, unlinkSync } from "node:fs";
28342
+ import { closeSync, existsSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
28343
28343
  import { homedir as homedir3 } from "node:os";
28344
28344
  import net from "node:net";
28345
- import { resolve } from "node:path";
28345
+ import { dirname as dirname2, resolve } from "node:path";
28346
28346
  import { fileURLToPath } from "node:url";
28347
28347
 
28348
28348
  // src/supervisor-protocol.ts
@@ -28352,6 +28352,7 @@ import { tmpdir } from "node:os";
28352
28352
  import { dirname, join as join2 } from "node:path";
28353
28353
  var SUPERVISOR_PROTOCOL_VERSION = 2;
28354
28354
  var SUPERVISOR_RENEW_MS = 5e3;
28355
+ var CURRENT_PROCESS_EPOCH_MS = Math.floor(Date.now() - process.uptime() * 1e3);
28355
28356
  function supervisorOwnerHash(lifecycleKey) {
28356
28357
  return createHash2("sha256").update(lifecycleKey).digest("hex");
28357
28358
  }
@@ -28367,6 +28368,9 @@ function supervisorSocketPath(stateDir, ownerHash) {
28367
28368
  function supervisorBootstrapPath(stateDir, ownerHash) {
28368
28369
  return join2(supervisorDir(stateDir), `${ownerHash}.bootstrap.json`);
28369
28370
  }
28371
+ function supervisorRolloverLockPath(stateDir, ownerHash) {
28372
+ return join2(supervisorDir(stateDir), `${ownerHash}.rollover.lock`);
28373
+ }
28370
28374
  function currentBootId() {
28371
28375
  try {
28372
28376
  return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
@@ -28380,7 +28384,7 @@ function processStartIdentity(pid = process.pid) {
28380
28384
  const afterName = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/u);
28381
28385
  return `linux-start-ticks:${afterName[19] ?? "unknown"}`;
28382
28386
  } catch {
28383
- if (pid === process.pid) return `epoch-ms:${Math.floor(Date.now() - process.uptime() * 1e3)}`;
28387
+ if (pid === process.pid) return `epoch-ms:${CURRENT_PROCESS_EPOCH_MS}`;
28384
28388
  return "unknown";
28385
28389
  }
28386
28390
  }
@@ -28390,6 +28394,14 @@ function encodeSupervisorMessage(message) {
28390
28394
  }
28391
28395
 
28392
28396
  // src/openclaw-supervisor-client.ts
28397
+ function processIsAlive(pid) {
28398
+ try {
28399
+ process.kill(pid, 0);
28400
+ return true;
28401
+ } catch {
28402
+ return false;
28403
+ }
28404
+ }
28393
28405
  var OpenClawSupervisorClient = class {
28394
28406
  stateDir;
28395
28407
  ownerHash;
@@ -28398,6 +28410,7 @@ var OpenClawSupervisorClient = class {
28398
28410
  logger;
28399
28411
  handlers;
28400
28412
  supervisorCliPath;
28413
+ spawnSupervisor;
28401
28414
  lease;
28402
28415
  socket = null;
28403
28416
  startPromise = null;
@@ -28410,10 +28423,16 @@ var OpenClawSupervisorClient = class {
28410
28423
  this.stateDir = resolve(input.stateDir ?? process.env.OPENCLAW_STATE_DIR ?? `${homedir3()}/.openclaw`);
28411
28424
  this.ownerHash = input.ownerHash;
28412
28425
  this.socketPath = supervisorSocketPath(this.stateDir, this.ownerHash);
28413
- this.config = input.config;
28426
+ let installRoot = resolve(input.config.installRoot);
28427
+ try {
28428
+ installRoot = realpathSync(installRoot);
28429
+ } catch {
28430
+ }
28431
+ this.config = { ...input.config, installRoot };
28414
28432
  this.logger = input.logger;
28415
28433
  this.handlers = input.handlers;
28416
28434
  this.supervisorCliPath = input.supervisorCliPath;
28435
+ this.spawnSupervisor = input.spawnSupervisor;
28417
28436
  this.lease = {
28418
28437
  ownerKey: input.ownerHash,
28419
28438
  instanceId: input.instanceId,
@@ -28439,15 +28458,18 @@ var OpenClawSupervisorClient = class {
28439
28458
  this.stopped = false;
28440
28459
  const starting = (async () => {
28441
28460
  await this.connectOrSpawn();
28442
- const claim = await this.requestWithMessage({
28443
- protocolVersion: 2,
28444
- type: "claim",
28445
- requestId: randomUUID5(),
28446
- lease: this.freshLease(),
28447
- config: this.config
28448
- }, 15e3);
28461
+ let claim;
28462
+ try {
28463
+ claim = await this.claimCurrentSupervisor();
28464
+ } catch (error) {
28465
+ if (!String(error).includes("SUPERVISOR_TARGET_MISMATCH")) throw error;
28466
+ claim = await this.rolloverSupervisor("target-mismatch");
28467
+ }
28468
+ if (!this.healthMatchesTarget(claim.health)) {
28469
+ claim = await this.rolloverSupervisor("legacy-health-missing-target");
28470
+ }
28449
28471
  const health = claim?.health;
28450
- if (claim?.state !== "READY" || health?.schemaVersion !== 2 || health.authority !== "process-external-supervisor" || health.state !== "running" || health.ownerKey !== this.ownerHash || health.instanceId !== this.lease.instanceId || health.generation !== this.lease.generation || health.relayClientCount !== (this.config.relay?.enabled && this.config.relay.url ? 1 : 0) || !health.claimedAt) {
28472
+ if (claim?.state !== "READY" || health?.schemaVersion !== 2 || health.authority !== "process-external-supervisor" || health.state !== "running" || health.ownerKey !== this.ownerHash || health.instanceId !== this.lease.instanceId || health.generation !== this.lease.generation || !this.healthMatchesTarget(health) || health.relayClientCount !== (this.config.relay?.enabled && this.config.relay.url ? 1 : 0) || !health.claimedAt) {
28451
28473
  throw new Error("OWNERSHIP_PRIMITIVE_UNAVAILABLE:supervisor claim did not return certified READY/CLAIMED health");
28452
28474
  }
28453
28475
  this.health = health;
@@ -28460,6 +28482,148 @@ var OpenClawSupervisorClient = class {
28460
28482
  this.startPromise = starting;
28461
28483
  return starting;
28462
28484
  }
28485
+ claimCurrentSupervisor() {
28486
+ return this.requestWithMessage({
28487
+ protocolVersion: 2,
28488
+ type: "claim",
28489
+ requestId: randomUUID5(),
28490
+ lease: this.freshLease(),
28491
+ config: this.config
28492
+ }, 15e3);
28493
+ }
28494
+ healthMatchesTarget(health) {
28495
+ if (!health || health.targetVersion !== this.config.version || !health.installRoot) return false;
28496
+ let healthRoot = resolve(health.installRoot);
28497
+ try {
28498
+ healthRoot = realpathSync(healthRoot);
28499
+ } catch {
28500
+ }
28501
+ return healthRoot === this.config.installRoot;
28502
+ }
28503
+ async rolloverSupervisor(reason) {
28504
+ const releaseLock = await this.acquireRolloverLock();
28505
+ try {
28506
+ this.closeSocket(new Error("SUPERVISOR_ROLLOVER_RECHECK"));
28507
+ await this.connectOrSpawn();
28508
+ try {
28509
+ const current = await this.claimCurrentSupervisor();
28510
+ if (this.healthMatchesTarget(current.health)) return current;
28511
+ const stopped = await this.requestWithMessage({
28512
+ protocolVersion: 2,
28513
+ type: "release",
28514
+ requestId: randomUUID5(),
28515
+ lease: this.freshLease()
28516
+ }, 8e3);
28517
+ if (stopped?.state !== "STOPPED") throw new Error("OWNER_STOP_TIMEOUT:legacy supervisor did not return STOPPED");
28518
+ } catch (error) {
28519
+ if (!String(error).includes("SUPERVISOR_TARGET_MISMATCH")) throw error;
28520
+ const stopped = await this.requestWithMessage({
28521
+ protocolVersion: 2,
28522
+ type: "prepareRollover",
28523
+ requestId: randomUUID5(),
28524
+ lease: this.freshLease(),
28525
+ targetVersion: this.config.version,
28526
+ targetInstallRoot: this.config.installRoot
28527
+ }, 8e3);
28528
+ if (stopped?.state !== "STOPPED") throw new Error("OWNER_STOP_TIMEOUT:supervisor rollover did not return STOPPED");
28529
+ }
28530
+ const oldHealth = this.health;
28531
+ this.closeSocket(new Error("SUPERVISOR_ROLLOVER"));
28532
+ await this.waitForSupervisorStop(oldHealth, 1e4);
28533
+ const target = await this.claimTargetAfterRollover(reason);
28534
+ this.logger.info(`[perkos-a2a] Supervisor rollover complete targetVersion=${this.config.version}`);
28535
+ return target;
28536
+ } finally {
28537
+ releaseLock();
28538
+ }
28539
+ }
28540
+ /**
28541
+ * OpenClaw can register two adjacent hot-reload generations while its config
28542
+ * file is settling. The first generation may successfully bootstrap the
28543
+ * target supervisor just as its client socket is replaced. Reconnect to that
28544
+ * single fenced supervisor instead of abandoning the agent offline.
28545
+ */
28546
+ async claimTargetAfterRollover(reason) {
28547
+ let lastError;
28548
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
28549
+ try {
28550
+ await this.connectOrSpawn();
28551
+ const target = await this.claimCurrentSupervisor();
28552
+ if (!this.healthMatchesTarget(target.health)) {
28553
+ throw new Error(`SUPERVISOR_ROLLOVER_TARGET_MISMATCH:${reason}`);
28554
+ }
28555
+ return target;
28556
+ } catch (error) {
28557
+ lastError = error;
28558
+ this.closeSocket(new Error("SUPERVISOR_ROLLOVER_TARGET_RETRY"));
28559
+ this.logger.error(
28560
+ `[perkos-a2a] Supervisor target claim retry ${attempt}/3: ${error instanceof Error ? error.message : String(error)}`
28561
+ );
28562
+ if (attempt < 3) await new Promise((resolveWait) => setTimeout(resolveWait, 250));
28563
+ }
28564
+ }
28565
+ throw new Error(
28566
+ `SUPERVISOR_ROLLOVER_TARGET_START_FAILED:${lastError instanceof Error ? lastError.message : String(lastError)}`
28567
+ );
28568
+ }
28569
+ async acquireRolloverLock(timeoutMs = 15e3) {
28570
+ const path = supervisorRolloverLockPath(this.stateDir, this.ownerHash);
28571
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
28572
+ const deadline = Date.now() + timeoutMs;
28573
+ while (Date.now() < deadline) {
28574
+ try {
28575
+ const fd = openSync(path, "wx", 384);
28576
+ writeFileSync2(fd, `${JSON.stringify({
28577
+ schemaVersion: 1,
28578
+ instanceId: this.lease.instanceId,
28579
+ pid: process.pid,
28580
+ processStartTime: processStartIdentity(),
28581
+ expiresAt: new Date(Date.now() + 2e4).toISOString()
28582
+ })}
28583
+ `);
28584
+ closeSync(fd);
28585
+ return () => {
28586
+ try {
28587
+ const lock = JSON.parse(readFileSync3(path, "utf8"));
28588
+ if (lock.instanceId === this.lease.instanceId) unlinkSync(path);
28589
+ } catch {
28590
+ }
28591
+ };
28592
+ } catch (error) {
28593
+ if (error.code !== "EEXIST") throw error;
28594
+ try {
28595
+ const lock = JSON.parse(readFileSync3(path, "utf8"));
28596
+ const expired = !lock.expiresAt || Date.parse(lock.expiresAt) <= Date.now();
28597
+ const observedStart = lock.pid ? processStartIdentity(lock.pid) : "unknown";
28598
+ const reusedLinuxPid = observedStart.startsWith("linux-start-ticks:") && Boolean(lock.processStartTime?.startsWith("linux-start-ticks:")) && observedStart !== lock.processStartTime;
28599
+ const dead = !lock.pid || !processIsAlive(lock.pid) || reusedLinuxPid;
28600
+ if (expired || dead) {
28601
+ unlinkSync(path);
28602
+ continue;
28603
+ }
28604
+ } catch {
28605
+ try {
28606
+ unlinkSync(path);
28607
+ } catch {
28608
+ }
28609
+ continue;
28610
+ }
28611
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
28612
+ }
28613
+ }
28614
+ throw new Error("ROLLOVER_LOCK_TIMEOUT");
28615
+ }
28616
+ async waitForSupervisorStop(health, timeoutMs) {
28617
+ const deadline = Date.now() + timeoutMs;
28618
+ while (Date.now() < deadline) {
28619
+ const socketGone = !existsSync(this.socketPath);
28620
+ const processGone = !health || processStartIdentity(health.pid) !== health.supervisorStartTime;
28621
+ if (socketGone && (processGone || health?.state === "stopped")) return;
28622
+ if (socketGone && processStartIdentity(health?.pid ?? -1) === "unknown") return;
28623
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
28624
+ }
28625
+ throw new Error("OWNER_STOP_TIMEOUT:old supervisor process/socket remained active");
28626
+ }
28463
28627
  stop() {
28464
28628
  if (this.stopPromise) return this.stopPromise;
28465
28629
  if (this.stopped && !this.socket) return Promise.resolve();
@@ -28541,27 +28705,29 @@ var OpenClawSupervisorClient = class {
28541
28705
  } catch {
28542
28706
  }
28543
28707
  const cliPath = this.supervisorCliPath ?? fileURLToPath(new URL("./openclaw-supervisor-cli.js", import.meta.url));
28544
- if (!existsSync(cliPath)) {
28545
- throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:supervisor CLI is missing at ${cliPath}`);
28546
- }
28708
+ if (!this.spawnSupervisor && !existsSync(cliPath)) throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:supervisor CLI is missing at ${cliPath}`);
28547
28709
  const bootstrapPath = supervisorBootstrapPath(this.stateDir, this.ownerHash);
28548
28710
  try {
28549
28711
  unlinkSync(bootstrapPath);
28550
28712
  } catch {
28551
28713
  }
28552
28714
  let childExit;
28553
- const child = spawn(process.execPath, [cliPath, "--state-dir", this.stateDir, "--owner-key", this.ownerHash], {
28554
- detached: true,
28555
- stdio: "ignore",
28556
- env: { ...process.env }
28557
- });
28558
- child.once("error", (error) => {
28559
- childExit = `spawn error: ${error.message}`;
28560
- });
28561
- child.once("exit", (code, signal) => {
28562
- childExit = `exit code=${code ?? "null"} signal=${signal ?? "none"}`;
28563
- });
28564
- child.unref();
28715
+ if (this.spawnSupervisor) {
28716
+ await this.spawnSupervisor();
28717
+ } else {
28718
+ const child = spawn(process.execPath, [cliPath, "--state-dir", this.stateDir, "--owner-key", this.ownerHash], {
28719
+ detached: true,
28720
+ stdio: "ignore",
28721
+ env: { ...process.env }
28722
+ });
28723
+ child.once("error", (error) => {
28724
+ childExit = `spawn error: ${error.message}`;
28725
+ });
28726
+ child.once("exit", (code, signal) => {
28727
+ childExit = `exit code=${code ?? "null"} signal=${signal ?? "none"}`;
28728
+ });
28729
+ child.unref();
28730
+ }
28565
28731
  const deadline = Date.now() + 1e4;
28566
28732
  let lastError;
28567
28733
  while (Date.now() < deadline) {
@@ -28692,10 +28858,10 @@ var OpenClawSupervisorClient = class {
28692
28858
  import { createHmac } from "node:crypto";
28693
28859
  import {
28694
28860
  chmodSync as chmodSync2,
28695
- mkdirSync as mkdirSync2,
28861
+ mkdirSync as mkdirSync3,
28696
28862
  readFileSync as readFileSync4,
28697
28863
  renameSync as renameSync2,
28698
- writeFileSync as writeFileSync2
28864
+ writeFileSync as writeFileSync3
28699
28865
  } from "node:fs";
28700
28866
  import { homedir as homedir4 } from "node:os";
28701
28867
  import { join as join3, resolve as resolve2 } from "node:path";
@@ -28754,10 +28920,10 @@ function recordRuntimeLoadEvidence(input) {
28754
28920
  bootId: currentBootId(),
28755
28921
  pluginRoot: resolve2(input.pluginRoot)
28756
28922
  };
28757
- mkdirSync2(evidenceDir, { recursive: true, mode: 448 });
28923
+ mkdirSync3(evidenceDir, { recursive: true, mode: 448 });
28758
28924
  const target = runtimeStatusPath(input.stateDir);
28759
28925
  const temporary = `${target}.${process.pid}.tmp`;
28760
- writeFileSync2(temporary, `${JSON.stringify(status, null, 2)}
28926
+ writeFileSync3(temporary, `${JSON.stringify(status, null, 2)}
28761
28927
  `, { mode: 384 });
28762
28928
  renameSync2(temporary, target);
28763
28929
  chmodSync2(target, 384);
@@ -29466,22 +29632,36 @@ function a2aLifecycleKey(config) {
29466
29632
  }
29467
29633
  const heartbeatUrl = config.platform?.heartbeatUrl || "";
29468
29634
  const idFromHeartbeat = heartbeatUrl.match(/\/agents\/([^/]+)\/heartbeat(?:\?|$)/u)?.[1];
29469
- const agentId = config.platform?.agentId || idFromHeartbeat || config.agentName.trim().toLowerCase();
29635
+ const agentId = config.platform?.agentId || idFromHeartbeat || safeAgentName(config.agentName).toLowerCase();
29470
29636
  return `${relayUrl}|${agentId}`;
29471
29637
  }
29472
- function register(api) {
29473
- const pluginConfig = api.config?.plugins?.entries?.["perkos-a2a"]?.config || {
29474
- agentName: "agent",
29475
- port: 5050,
29476
- skills: [],
29477
- peers: {}
29638
+ function safeAgentName(value) {
29639
+ return typeof value === "string" && value.trim() ? value.trim() : "agent";
29640
+ }
29641
+ function normalizeOpenClawPluginConfig(value) {
29642
+ const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
29643
+ const platform = raw.platform && typeof raw.platform === "object" ? raw.platform : void 0;
29644
+ const heartbeatAgentId = platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u)?.[1];
29645
+ const agentName = safeAgentName(raw.agentName || platform?.agentId || heartbeatAgentId);
29646
+ return {
29647
+ ...raw,
29648
+ agentName,
29649
+ port: typeof raw.port === "number" ? raw.port : 5050,
29650
+ skills: Array.isArray(raw.skills) ? raw.skills : [],
29651
+ peers: raw.peers && typeof raw.peers === "object" ? raw.peers : {}
29478
29652
  };
29653
+ }
29654
+ function register(api) {
29655
+ const pluginConfig = normalizeOpenClawPluginConfig(
29656
+ api.config?.plugins?.entries?.["perkos-a2a"]?.config
29657
+ );
29479
29658
  const logger = api.logger || console;
29480
29659
  const pluginVersion = loadedPluginVersion();
29660
+ const pluginRoot = resolve3(dirname3(fileURLToPath2(import.meta.url)), "..");
29481
29661
  recordRuntimeLoadEvidence({
29482
29662
  config: pluginConfig,
29483
29663
  version: pluginVersion,
29484
- pluginRoot: resolve3(dirname2(fileURLToPath2(import.meta.url)), ".."),
29664
+ pluginRoot,
29485
29665
  logger
29486
29666
  });
29487
29667
  const server = new A2AServer(pluginConfig, logger, { relayOwnership: "external-required" });
@@ -29738,7 +29918,7 @@ function register(api) {
29738
29918
  if (pluginConfig.chat?.enabled && !chatApiKey) {
29739
29919
  logger.error("[perkos-chat] chat.enabled=true but no apiKey (set chat.apiKey or fall back from relay.apiKey)");
29740
29920
  }
29741
- const platformAgentId = pluginConfig.platform?.agentId || pluginConfig.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u)?.[1] || pluginConfig.agentName.trim().toLowerCase();
29921
+ const platformAgentId = pluginConfig.platform?.agentId || pluginConfig.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u)?.[1] || safeAgentName(pluginConfig.agentName).toLowerCase();
29742
29922
  supervisorClient = new OpenClawSupervisorClient({
29743
29923
  ownerHash: supervisorOwnerHash(lifecycleKey),
29744
29924
  generation: Date.now(),
@@ -29747,6 +29927,7 @@ function register(api) {
29747
29927
  agentName: pluginConfig.agentName,
29748
29928
  agentId: platformAgentId,
29749
29929
  version: pluginVersion,
29930
+ installRoot: pluginRoot,
29750
29931
  relay: pluginConfig.relay,
29751
29932
  chat: chatEnabled ? {
29752
29933
  enabled: true,
@@ -30182,6 +30363,7 @@ export {
30182
30363
  extractOpenClawChatReply,
30183
30364
  formatRecentChatContext,
30184
30365
  isWebhookEvent,
30366
+ normalizeOpenClawPluginConfig,
30185
30367
  omitUnavailableAgentHarness,
30186
30368
  openclawChatSessionKey,
30187
30369
  runOpenClawChatTurn