@perkos/perkos-a2a 0.12.62 → 0.12.64
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 +2 -2
- package/dist/a2a-maintenance.d.ts +44 -0
- package/dist/a2a-maintenance.d.ts.map +1 -0
- package/dist/a2a-maintenance.js +145 -0
- package/dist/a2a-maintenance.js.map +1 -0
- package/dist/a2a-maintenance.test.d.ts +2 -0
- package/dist/a2a-maintenance.test.d.ts.map +1 -0
- package/dist/a2a-maintenance.test.js +60 -0
- package/dist/a2a-maintenance.test.js.map +1 -0
- package/dist/bridge-agent.js +19 -3
- package/dist/bridge-agent.js.map +1 -1
- package/dist/chat-client.d.ts +3 -0
- package/dist/chat-client.d.ts.map +1 -1
- package/dist/chat-client.js +9 -3
- package/dist/chat-client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +170 -14
- package/dist/index.js.map +4 -4
- package/dist/maintenance-runner.d.ts +3 -0
- package/dist/maintenance-runner.d.ts.map +1 -0
- package/dist/maintenance-runner.js +80 -0
- package/dist/maintenance-runner.js.map +1 -0
- package/dist/platform-heartbeat.d.ts +6 -0
- package/dist/platform-heartbeat.d.ts.map +1 -1
- package/dist/platform-heartbeat.js +6 -1
- package/dist/platform-heartbeat.js.map +1 -1
- package/docs/plans/2026-09-02-chat-bound-maintenance-capability-design.md +36 -0
- package/docs/plans/2026-09-02-managed-a2a-self-update-design.md +66 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18441,10 +18441,10 @@ var require_view = __commonJS({
|
|
|
18441
18441
|
var debug = require_src()("express:view");
|
|
18442
18442
|
var path = __require("path");
|
|
18443
18443
|
var fs = __require("fs");
|
|
18444
|
-
var
|
|
18444
|
+
var dirname5 = path.dirname;
|
|
18445
18445
|
var basename = path.basename;
|
|
18446
18446
|
var extname = path.extname;
|
|
18447
|
-
var
|
|
18447
|
+
var join5 = path.join;
|
|
18448
18448
|
var resolve4 = path.resolve;
|
|
18449
18449
|
module.exports = View;
|
|
18450
18450
|
function View(name, options) {
|
|
@@ -18480,7 +18480,7 @@ var require_view = __commonJS({
|
|
|
18480
18480
|
for (var i = 0; i < roots.length && !path2; i++) {
|
|
18481
18481
|
var root = roots[i];
|
|
18482
18482
|
var loc = resolve4(root, name);
|
|
18483
|
-
var dir =
|
|
18483
|
+
var dir = dirname5(loc);
|
|
18484
18484
|
var file = basename(loc);
|
|
18485
18485
|
path2 = this.resolve(dir, file);
|
|
18486
18486
|
}
|
|
@@ -18492,12 +18492,12 @@ var require_view = __commonJS({
|
|
|
18492
18492
|
};
|
|
18493
18493
|
View.prototype.resolve = function resolve5(dir, file) {
|
|
18494
18494
|
var ext = this.ext;
|
|
18495
|
-
var path2 =
|
|
18495
|
+
var path2 = join5(dir, file);
|
|
18496
18496
|
var stat = tryStat(path2);
|
|
18497
18497
|
if (stat && stat.isFile()) {
|
|
18498
18498
|
return path2;
|
|
18499
18499
|
}
|
|
18500
|
-
path2 =
|
|
18500
|
+
path2 = join5(dir, basename(file, ext), "index" + ext);
|
|
18501
18501
|
stat = tryStat(path2);
|
|
18502
18502
|
if (stat && stat.isFile()) {
|
|
18503
18503
|
return path2;
|
|
@@ -19130,7 +19130,7 @@ var require_send = __commonJS({
|
|
|
19130
19130
|
var Stream = __require("stream");
|
|
19131
19131
|
var util = __require("util");
|
|
19132
19132
|
var extname = path.extname;
|
|
19133
|
-
var
|
|
19133
|
+
var join5 = path.join;
|
|
19134
19134
|
var normalize = path.normalize;
|
|
19135
19135
|
var resolve4 = path.resolve;
|
|
19136
19136
|
var sep = path.sep;
|
|
@@ -19349,7 +19349,7 @@ var require_send = __commonJS({
|
|
|
19349
19349
|
return res;
|
|
19350
19350
|
}
|
|
19351
19351
|
parts = path2.split(sep);
|
|
19352
|
-
path2 = normalize(
|
|
19352
|
+
path2 = normalize(join5(root, path2));
|
|
19353
19353
|
} else {
|
|
19354
19354
|
if (UP_PATH_REGEXP.test(path2)) {
|
|
19355
19355
|
debug('malicious path "%s"', path2);
|
|
@@ -19484,7 +19484,7 @@ var require_send = __commonJS({
|
|
|
19484
19484
|
if (err) return self.onStatError(err);
|
|
19485
19485
|
return self.error(404);
|
|
19486
19486
|
}
|
|
19487
|
-
var p =
|
|
19487
|
+
var p = join5(path2, self._index[i]);
|
|
19488
19488
|
debug('stat "%s"', p);
|
|
19489
19489
|
fs.stat(p, function(err2, stat) {
|
|
19490
19490
|
if (err2) return next(err2);
|
|
@@ -27779,8 +27779,8 @@ var A2AServer = class {
|
|
|
27779
27779
|
// src/index.ts
|
|
27780
27780
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
27781
27781
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
27782
|
-
import { dirname as
|
|
27783
|
-
import { fileURLToPath as
|
|
27782
|
+
import { dirname as dirname4, resolve as resolve3 } from "node:path";
|
|
27783
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
27784
27784
|
|
|
27785
27785
|
// src/task-context.ts
|
|
27786
27786
|
function perkosTaskContext(task, trustedDispatcher = process.env.PERKOS_DISPATCHER_AGENT_NAME || "perkos-ci-probe-dispatcher") {
|
|
@@ -29306,6 +29306,143 @@ async function tryHandleInstalledVoiceMarker(message, config, loadVoice = defaul
|
|
|
29306
29306
|
});
|
|
29307
29307
|
}
|
|
29308
29308
|
|
|
29309
|
+
// src/a2a-maintenance.ts
|
|
29310
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
29311
|
+
import { chmod, mkdir as mkdir2, readdir, readFile as readFile2, rename, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
29312
|
+
import { homedir as homedir5 } from "node:os";
|
|
29313
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
29314
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29315
|
+
var PERKOS_A2A_UPDATE_PREFIX = "PERKOS_A2A_UPDATE:";
|
|
29316
|
+
var REQUEST_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
29317
|
+
var BRIDGE_INSTANCE_ID = REQUEST_ID;
|
|
29318
|
+
function parseA2AUpdateMarker(message) {
|
|
29319
|
+
const marker = message.trim();
|
|
29320
|
+
if (!marker.startsWith(PERKOS_A2A_UPDATE_PREFIX)) return null;
|
|
29321
|
+
const [requestId, bridgeInstanceId, extra] = marker.slice(PERKOS_A2A_UPDATE_PREFIX.length).split(":");
|
|
29322
|
+
return !extra && requestId && bridgeInstanceId && REQUEST_ID.test(requestId) && BRIDGE_INSTANCE_ID.test(bridgeInstanceId) ? { requestId, bridgeInstanceId } : null;
|
|
29323
|
+
}
|
|
29324
|
+
function maintenanceControlPlane(config) {
|
|
29325
|
+
const heartbeat = config.platform?.heartbeatUrl;
|
|
29326
|
+
const relayApiKey = config.relay?.apiKey || config.chat?.apiKey;
|
|
29327
|
+
if (!heartbeat || !relayApiKey) return null;
|
|
29328
|
+
let url;
|
|
29329
|
+
try {
|
|
29330
|
+
url = new URL(heartbeat);
|
|
29331
|
+
} catch {
|
|
29332
|
+
return null;
|
|
29333
|
+
}
|
|
29334
|
+
const match = url.pathname.match(/^(.*)\/agents\/([^/]+)\/heartbeat\/?$/u);
|
|
29335
|
+
const agentId = config.platform?.agentId || (match ? decodeURIComponent(match[2]) : "");
|
|
29336
|
+
if (!match || !agentId) return null;
|
|
29337
|
+
return { apiBaseUrl: `${url.origin}${match[1]}`, agentId, relayApiKey };
|
|
29338
|
+
}
|
|
29339
|
+
function maintenanceRoot(agentId) {
|
|
29340
|
+
return join4(homedir5(), ".perkos", "a2a", "hermes", agentId, "maintenance");
|
|
29341
|
+
}
|
|
29342
|
+
async function atomicPrivateJson(path, value) {
|
|
29343
|
+
await mkdir2(dirname3(path), { recursive: true, mode: 448 });
|
|
29344
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
29345
|
+
await writeFile2(temporary, `${JSON.stringify(value)}
|
|
29346
|
+
`, { mode: 384 });
|
|
29347
|
+
await chmod(temporary, 384);
|
|
29348
|
+
await rename(temporary, path);
|
|
29349
|
+
}
|
|
29350
|
+
async function authorizedJson(url, relayApiKey, init = {}) {
|
|
29351
|
+
const response = await fetch(url, {
|
|
29352
|
+
...init,
|
|
29353
|
+
headers: {
|
|
29354
|
+
authorization: `Bearer ${relayApiKey}`,
|
|
29355
|
+
"content-type": "application/json",
|
|
29356
|
+
...init.headers ?? {}
|
|
29357
|
+
},
|
|
29358
|
+
signal: AbortSignal.timeout(15e3)
|
|
29359
|
+
});
|
|
29360
|
+
const body = await response.json().catch(() => null);
|
|
29361
|
+
if (!response.ok || !body) throw new Error(`control_plane_${response.status}`);
|
|
29362
|
+
return body;
|
|
29363
|
+
}
|
|
29364
|
+
async function claimRequest(target, requestId, bridgeInstanceId) {
|
|
29365
|
+
const body = await authorizedJson(
|
|
29366
|
+
`${target.apiBaseUrl}/agents/${encodeURIComponent(target.agentId)}/maintenance/a2a-update/${requestId}/claim`,
|
|
29367
|
+
target.relayApiKey,
|
|
29368
|
+
{ method: "POST", body: "{}", headers: { "x-perkos-bridge-instance": bridgeInstanceId } }
|
|
29369
|
+
);
|
|
29370
|
+
const request = body.request;
|
|
29371
|
+
if (typeof request?.targetVersion !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(request.targetVersion)) {
|
|
29372
|
+
throw new Error("invalid_target_version");
|
|
29373
|
+
}
|
|
29374
|
+
return { targetVersion: request.targetVersion };
|
|
29375
|
+
}
|
|
29376
|
+
async function queueHermesMaintenanceJob(job) {
|
|
29377
|
+
const root = maintenanceRoot(job.agentId);
|
|
29378
|
+
const jobPath = join4(root, "jobs", `${job.requestId}.json`);
|
|
29379
|
+
await atomicPrivateJson(jobPath, job);
|
|
29380
|
+
const runnerPath = fileURLToPath2(new URL("./maintenance-runner.js", import.meta.url));
|
|
29381
|
+
const child = spawn2(process.execPath, [runnerPath, "--job", jobPath], {
|
|
29382
|
+
detached: true,
|
|
29383
|
+
stdio: "ignore"
|
|
29384
|
+
});
|
|
29385
|
+
child.unref();
|
|
29386
|
+
}
|
|
29387
|
+
async function tryHandleA2AMaintenanceMarker(message, config, deps = {}) {
|
|
29388
|
+
const marker = parseA2AUpdateMarker(message);
|
|
29389
|
+
if (!marker) return null;
|
|
29390
|
+
const { requestId, bridgeInstanceId } = marker;
|
|
29391
|
+
if (!deps.bridgeInstanceId || deps.bridgeInstanceId !== bridgeInstanceId) {
|
|
29392
|
+
return "PERKOS_A2A_UPDATE_FAILED:bridge_instance_mismatch";
|
|
29393
|
+
}
|
|
29394
|
+
if (config.runtime?.kind !== "hermes" && config.runtime?.kind !== "hermes-api") {
|
|
29395
|
+
return "PERKOS_A2A_UPDATE_FAILED:runtime_unsupported";
|
|
29396
|
+
}
|
|
29397
|
+
const target = maintenanceControlPlane(config);
|
|
29398
|
+
if (!target) return "PERKOS_A2A_UPDATE_FAILED:a2a_identity_unavailable";
|
|
29399
|
+
try {
|
|
29400
|
+
const { targetVersion } = await (deps.claim ?? claimRequest)(target, requestId, bridgeInstanceId);
|
|
29401
|
+
await (deps.queue ?? queueHermesMaintenanceJob)({
|
|
29402
|
+
schemaVersion: 1,
|
|
29403
|
+
requestId,
|
|
29404
|
+
agentId: target.agentId,
|
|
29405
|
+
targetVersion,
|
|
29406
|
+
apiBaseUrl: target.apiBaseUrl,
|
|
29407
|
+
relayApiKey: target.relayApiKey
|
|
29408
|
+
});
|
|
29409
|
+
return `PERKOS_A2A_UPDATE_ACCEPTED:${requestId}`;
|
|
29410
|
+
} catch {
|
|
29411
|
+
return "PERKOS_A2A_UPDATE_FAILED:request_rejected";
|
|
29412
|
+
}
|
|
29413
|
+
}
|
|
29414
|
+
async function reportReceipt(receipt, target) {
|
|
29415
|
+
await authorizedJson(
|
|
29416
|
+
`${target.apiBaseUrl}/agents/${encodeURIComponent(target.agentId)}/maintenance/a2a-update/${receipt.requestId}/report`,
|
|
29417
|
+
target.relayApiKey,
|
|
29418
|
+
{
|
|
29419
|
+
method: "POST",
|
|
29420
|
+
body: JSON.stringify({
|
|
29421
|
+
state: receipt.state,
|
|
29422
|
+
...receipt.installedVersion ? { installedVersion: receipt.installedVersion } : {},
|
|
29423
|
+
...receipt.errorCode ? { errorCode: receipt.errorCode } : {}
|
|
29424
|
+
})
|
|
29425
|
+
}
|
|
29426
|
+
);
|
|
29427
|
+
}
|
|
29428
|
+
async function flushMaintenanceReceipts(config) {
|
|
29429
|
+
const target = maintenanceControlPlane(config);
|
|
29430
|
+
if (!target) return;
|
|
29431
|
+
const receiptsDir = join4(maintenanceRoot(target.agentId), "receipts");
|
|
29432
|
+
const files = await readdir(receiptsDir).catch(() => []);
|
|
29433
|
+
for (const file of files) {
|
|
29434
|
+
if (!file.endsWith(".json")) continue;
|
|
29435
|
+
const path = join4(receiptsDir, file);
|
|
29436
|
+
try {
|
|
29437
|
+
const receipt = JSON.parse(await readFile2(path, "utf8"));
|
|
29438
|
+
if (receipt.agentId !== target.agentId || receipt.apiBaseUrl !== target.apiBaseUrl || !REQUEST_ID.test(receipt.requestId)) continue;
|
|
29439
|
+
await reportReceipt(receipt, target);
|
|
29440
|
+
await unlink(path);
|
|
29441
|
+
} catch {
|
|
29442
|
+
}
|
|
29443
|
+
}
|
|
29444
|
+
}
|
|
29445
|
+
|
|
29309
29446
|
// src/chat-client.ts
|
|
29310
29447
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
29311
29448
|
var DEFAULT_URL = "wss://chat.perkos.xyz/chat";
|
|
@@ -29331,6 +29468,11 @@ var ChatClient = class {
|
|
|
29331
29468
|
authed = false;
|
|
29332
29469
|
stopped = false;
|
|
29333
29470
|
generation = 0;
|
|
29471
|
+
setAuthed(value) {
|
|
29472
|
+
if (this.authed === value) return;
|
|
29473
|
+
this.authed = value;
|
|
29474
|
+
this.handlers.onConnectionStateChange?.(value);
|
|
29475
|
+
}
|
|
29334
29476
|
constructor(opts) {
|
|
29335
29477
|
this.agentName = opts.agentName;
|
|
29336
29478
|
const cfg = opts.config;
|
|
@@ -29492,7 +29634,7 @@ var ChatClient = class {
|
|
|
29492
29634
|
}
|
|
29493
29635
|
if (!this.isCurrent(generation, socket)) return;
|
|
29494
29636
|
this.connected = false;
|
|
29495
|
-
this.
|
|
29637
|
+
this.setAuthed(false);
|
|
29496
29638
|
this.clearTimers();
|
|
29497
29639
|
if (!this.stopped) {
|
|
29498
29640
|
this.logger.info(`[perkos-chat] disconnected (${code}: ${reason?.toString() || "no reason"}); reconnecting`);
|
|
@@ -29507,7 +29649,7 @@ var ChatClient = class {
|
|
|
29507
29649
|
async handleFrame(frame) {
|
|
29508
29650
|
switch (frame.type) {
|
|
29509
29651
|
case "auth_ok":
|
|
29510
|
-
this.
|
|
29652
|
+
this.setAuthed(true);
|
|
29511
29653
|
this.logger.info(`[perkos-chat] authed as agent:${this.agentName}`);
|
|
29512
29654
|
this.startHeartbeat();
|
|
29513
29655
|
return;
|
|
@@ -29718,7 +29860,7 @@ var ChatClient = class {
|
|
|
29718
29860
|
const socket = this.ws;
|
|
29719
29861
|
this.ws = null;
|
|
29720
29862
|
this.connected = false;
|
|
29721
|
-
this.
|
|
29863
|
+
this.setAuthed(false);
|
|
29722
29864
|
return socket;
|
|
29723
29865
|
}
|
|
29724
29866
|
};
|
|
@@ -30164,7 +30306,7 @@ function register(api) {
|
|
|
30164
30306
|
);
|
|
30165
30307
|
const logger = api.logger || console;
|
|
30166
30308
|
const pluginVersion = loadedPluginVersion();
|
|
30167
|
-
const pluginRoot = resolve3(
|
|
30309
|
+
const pluginRoot = resolve3(dirname4(fileURLToPath3(import.meta.url)), "..");
|
|
30168
30310
|
const server = new A2AServer(pluginConfig, logger, { relayOwnership: "external-required" });
|
|
30169
30311
|
const lifecycleKey = a2aLifecycleKey(pluginConfig);
|
|
30170
30312
|
const lifecycleInstanceId = randomUUID7();
|
|
@@ -30349,6 +30491,13 @@ function register(api) {
|
|
|
30349
30491
|
let supervisorClient;
|
|
30350
30492
|
const handleSupervisorChatDeliver = async (frame) => {
|
|
30351
30493
|
const walletAddress = parseWalletFromIdentity(frame.from);
|
|
30494
|
+
const maintenanceReply = await tryHandleA2AMaintenanceMarker(frame.text, pluginConfig);
|
|
30495
|
+
if (maintenanceReply) {
|
|
30496
|
+
if (walletAddress) {
|
|
30497
|
+
await supervisorClient.sendChatReply({ convId: frame.convId, walletAddress, text: maintenanceReply, replyTo: frame.id });
|
|
30498
|
+
}
|
|
30499
|
+
return;
|
|
30500
|
+
}
|
|
30352
30501
|
const voiceReply = await tryHandleInstalledVoiceMarker(frame.text, pluginConfig);
|
|
30353
30502
|
if (voiceReply) {
|
|
30354
30503
|
if (walletAddress) {
|
|
@@ -30485,6 +30634,7 @@ function register(api) {
|
|
|
30485
30634
|
start: async () => {
|
|
30486
30635
|
try {
|
|
30487
30636
|
await supervisorClient.start();
|
|
30637
|
+
void flushMaintenanceReceipts(pluginConfig);
|
|
30488
30638
|
const ownerHealth = supervisorClient.currentHealth();
|
|
30489
30639
|
recordRuntimeLoadEvidence({
|
|
30490
30640
|
config: pluginConfig,
|
|
@@ -30887,6 +31037,7 @@ export {
|
|
|
30887
31037
|
A2AServer,
|
|
30888
31038
|
ChatClient,
|
|
30889
31039
|
ChatStore,
|
|
31040
|
+
PERKOS_A2A_UPDATE_PREFIX,
|
|
30890
31041
|
PERKOS_VOICE_ENROLL_MARKER,
|
|
30891
31042
|
PERKOS_VOICE_PROBE_MARKER,
|
|
30892
31043
|
RelayClient,
|
|
@@ -30896,15 +31047,20 @@ export {
|
|
|
30896
31047
|
register as default,
|
|
30897
31048
|
detectNetworking,
|
|
30898
31049
|
extractOpenClawChatReply,
|
|
31050
|
+
flushMaintenanceReceipts,
|
|
30899
31051
|
formatRecentChatContext,
|
|
30900
31052
|
isWebhookEvent,
|
|
31053
|
+
maintenanceControlPlane,
|
|
30901
31054
|
normalizeOpenClawPluginConfig,
|
|
30902
31055
|
omitUnavailableAgentHarness,
|
|
30903
31056
|
openclawChatSessionKey,
|
|
31057
|
+
parseA2AUpdateMarker,
|
|
30904
31058
|
prepareOpenClawChatSession,
|
|
31059
|
+
queueHermesMaintenanceJob,
|
|
30905
31060
|
resolveOpenClawRuntimeModelRoute,
|
|
30906
31061
|
runOpenClawChatTurn,
|
|
30907
31062
|
stripPerkosChatTransportMarker,
|
|
31063
|
+
tryHandleA2AMaintenanceMarker,
|
|
30908
31064
|
tryHandleInstalledVoiceMarker,
|
|
30909
31065
|
useNativeHarnessForResolvedRoute
|
|
30910
31066
|
};
|