clawnexus 0.3.0 → 0.3.1
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/dist/a2a/card.d.ts +1 -1
- package/dist/a2a/card.js +2 -2
- package/dist/agent/engine.js +9 -2
- package/dist/agent/executor.d.ts +48 -0
- package/dist/agent/executor.js +374 -0
- package/dist/agent/gateway.d.ts +26 -0
- package/dist/agent/gateway.js +298 -0
- package/dist/agent/protocol.js +2 -0
- package/dist/agent/router.d.ts +10 -1
- package/dist/agent/router.js +31 -2
- package/dist/agent/services.d.ts +36 -0
- package/dist/agent/services.js +153 -0
- package/dist/agent/tasks.js +4 -2
- package/dist/api/server.d.ts +7 -2
- package/dist/api/server.js +108 -15
- package/dist/registry/auto-register.js +32 -20
- package/dist/relay/connector.d.ts +5 -1
- package/dist/relay/connector.js +13 -2
- package/package.json +1 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared OpenClaw Gateway WebSocket connection helper
|
|
3
|
+
// Handles Protocol v3 handshake: device identity, Ed25519 signing, connect frame format.
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.loadOrCreateDeviceIdentity = loadOrCreateDeviceIdentity;
|
|
39
|
+
exports.connectGateway = connectGateway;
|
|
40
|
+
const crypto = __importStar(require("node:crypto"));
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const os = __importStar(require("node:os"));
|
|
44
|
+
const ws_1 = require("ws");
|
|
45
|
+
const node_crypto_1 = require("node:crypto");
|
|
46
|
+
const CLAWNEXUS_DIR = path.join(os.homedir(), ".clawnexus");
|
|
47
|
+
const IDENTITY_PATH = path.join(CLAWNEXUS_DIR, "oc-device-identity.json");
|
|
48
|
+
const AUTH_TOKEN_DIR = path.join(CLAWNEXUS_DIR, "device-auth-tokens");
|
|
49
|
+
const PROTOCOL_VERSION = 3;
|
|
50
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
51
|
+
function base64UrlEncode(buf) {
|
|
52
|
+
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
53
|
+
}
|
|
54
|
+
function derivePublicKeyRaw(publicKeyPem) {
|
|
55
|
+
const spki = crypto.createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
|
|
56
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
57
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
58
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
59
|
+
}
|
|
60
|
+
return spki;
|
|
61
|
+
}
|
|
62
|
+
function fingerprintPublicKey(publicKeyPem) {
|
|
63
|
+
const raw = derivePublicKeyRaw(publicKeyPem);
|
|
64
|
+
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
65
|
+
}
|
|
66
|
+
function generateIdentity() {
|
|
67
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
68
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
69
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
70
|
+
return {
|
|
71
|
+
deviceId: fingerprintPublicKey(publicKeyPem),
|
|
72
|
+
publicKeyPem,
|
|
73
|
+
privateKeyPem,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function loadOrCreateDeviceIdentity() {
|
|
77
|
+
try {
|
|
78
|
+
if (fs.existsSync(IDENTITY_PATH)) {
|
|
79
|
+
const raw = fs.readFileSync(IDENTITY_PATH, "utf8");
|
|
80
|
+
const parsed = JSON.parse(raw);
|
|
81
|
+
if (parsed?.version === 1 && parsed.deviceId && parsed.publicKeyPem && parsed.privateKeyPem) {
|
|
82
|
+
return {
|
|
83
|
+
deviceId: parsed.deviceId,
|
|
84
|
+
publicKeyPem: parsed.publicKeyPem,
|
|
85
|
+
privateKeyPem: parsed.privateKeyPem,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Generate fresh identity
|
|
92
|
+
}
|
|
93
|
+
const identity = generateIdentity();
|
|
94
|
+
fs.mkdirSync(CLAWNEXUS_DIR, { recursive: true });
|
|
95
|
+
const stored = { version: 1, ...identity, createdAtMs: Date.now() };
|
|
96
|
+
fs.writeFileSync(IDENTITY_PATH, JSON.stringify(stored, null, 2) + "\n", { mode: 0o600 });
|
|
97
|
+
return identity;
|
|
98
|
+
}
|
|
99
|
+
// --- Device Auth Token Storage ---
|
|
100
|
+
function authTokenPath(deviceId, role) {
|
|
101
|
+
return path.join(AUTH_TOKEN_DIR, `${deviceId}-${role}.json`);
|
|
102
|
+
}
|
|
103
|
+
function loadDeviceAuthToken(deviceId, role) {
|
|
104
|
+
try {
|
|
105
|
+
const p = authTokenPath(deviceId, role);
|
|
106
|
+
if (fs.existsSync(p)) {
|
|
107
|
+
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
108
|
+
return data?.token ?? null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Ignore
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function storeDeviceAuthToken(deviceId, role, token) {
|
|
117
|
+
try {
|
|
118
|
+
fs.mkdirSync(AUTH_TOKEN_DIR, { recursive: true });
|
|
119
|
+
const p = authTokenPath(deviceId, role);
|
|
120
|
+
fs.writeFileSync(p, JSON.stringify({ token, role, storedAtMs: Date.now() }, null, 2), { mode: 0o600 });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Non-fatal
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// --- Protocol Helpers ---
|
|
127
|
+
function signDevicePayload(privateKeyPem, payload) {
|
|
128
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
129
|
+
return base64UrlEncode(crypto.sign(null, Buffer.from(payload, "utf8"), key));
|
|
130
|
+
}
|
|
131
|
+
function publicKeyRawBase64Url(publicKeyPem) {
|
|
132
|
+
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
|
133
|
+
}
|
|
134
|
+
function buildDeviceAuthPayloadV3(params) {
|
|
135
|
+
const platform = (params.platform || "").toLowerCase();
|
|
136
|
+
const deviceFamily = (params.deviceFamily || "").toLowerCase();
|
|
137
|
+
return [
|
|
138
|
+
"v3",
|
|
139
|
+
params.deviceId,
|
|
140
|
+
params.clientId,
|
|
141
|
+
params.clientMode,
|
|
142
|
+
params.role,
|
|
143
|
+
params.scopes.join(","),
|
|
144
|
+
String(params.signedAtMs),
|
|
145
|
+
params.token ?? "",
|
|
146
|
+
params.nonce,
|
|
147
|
+
platform,
|
|
148
|
+
deviceFamily,
|
|
149
|
+
].join("|");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Connect to the OpenClaw Gateway using Protocol v3 handshake.
|
|
153
|
+
* Handles device identity, Ed25519 signing, and device token storage.
|
|
154
|
+
*/
|
|
155
|
+
function connectGateway(opts = {}) {
|
|
156
|
+
const gatewayUrl = opts.gatewayUrl ?? "ws://127.0.0.1:18789";
|
|
157
|
+
const connectTimeoutMs = opts.connectTimeoutMs ?? 10_000;
|
|
158
|
+
const requestTimeoutMs = opts.requestTimeoutMs ?? 30_000;
|
|
159
|
+
const role = opts.role ?? "operator";
|
|
160
|
+
const scopes = opts.scopes ?? ["operator.read", "operator.write"];
|
|
161
|
+
const identity = loadOrCreateDeviceIdentity();
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
const ws = new ws_1.WebSocket(gatewayUrl);
|
|
164
|
+
const pendingRequests = new Map();
|
|
165
|
+
const connectTimeout = setTimeout(() => {
|
|
166
|
+
ws.close();
|
|
167
|
+
reject(new Error("Gateway connection timeout"));
|
|
168
|
+
}, connectTimeoutMs);
|
|
169
|
+
ws.on("error", (err) => {
|
|
170
|
+
clearTimeout(connectTimeout);
|
|
171
|
+
reject(new Error(`Gateway connection error: ${err.message}`));
|
|
172
|
+
});
|
|
173
|
+
ws.on("close", () => {
|
|
174
|
+
clearTimeout(connectTimeout);
|
|
175
|
+
// Reject all pending requests
|
|
176
|
+
for (const [, pending] of pendingRequests) {
|
|
177
|
+
pending.reject(new Error("Gateway connection closed"));
|
|
178
|
+
}
|
|
179
|
+
pendingRequests.clear();
|
|
180
|
+
});
|
|
181
|
+
ws.on("message", (data) => {
|
|
182
|
+
let msg;
|
|
183
|
+
try {
|
|
184
|
+
msg = JSON.parse(data.toString());
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const type = msg.type;
|
|
190
|
+
// Handshake: connect.challenge → connect → hello-ok
|
|
191
|
+
if (type === "event" && msg.event === "connect.challenge") {
|
|
192
|
+
const payload = msg.payload;
|
|
193
|
+
const nonce = payload?.nonce ?? (0, node_crypto_1.randomUUID)();
|
|
194
|
+
const signedAtMs = Date.now();
|
|
195
|
+
const clientId = "gateway-client";
|
|
196
|
+
const clientMode = "backend";
|
|
197
|
+
// Load stored device auth token
|
|
198
|
+
const storedToken = loadDeviceAuthToken(identity.deviceId, role);
|
|
199
|
+
const envToken = process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined;
|
|
200
|
+
const authToken = envToken ?? storedToken ?? undefined;
|
|
201
|
+
// Build device signature
|
|
202
|
+
const authPayload = buildDeviceAuthPayloadV3({
|
|
203
|
+
deviceId: identity.deviceId,
|
|
204
|
+
clientId,
|
|
205
|
+
clientMode,
|
|
206
|
+
role,
|
|
207
|
+
scopes,
|
|
208
|
+
signedAtMs,
|
|
209
|
+
token: authToken ?? null,
|
|
210
|
+
nonce,
|
|
211
|
+
platform: process.platform,
|
|
212
|
+
});
|
|
213
|
+
const signature = signDevicePayload(identity.privateKeyPem, authPayload);
|
|
214
|
+
const connectId = (0, node_crypto_1.randomUUID)();
|
|
215
|
+
const frame = {
|
|
216
|
+
type: "req",
|
|
217
|
+
id: connectId,
|
|
218
|
+
method: "connect",
|
|
219
|
+
params: {
|
|
220
|
+
minProtocol: PROTOCOL_VERSION,
|
|
221
|
+
maxProtocol: PROTOCOL_VERSION,
|
|
222
|
+
client: {
|
|
223
|
+
id: clientId,
|
|
224
|
+
version: "0.3.0",
|
|
225
|
+
platform: process.platform,
|
|
226
|
+
mode: clientMode,
|
|
227
|
+
},
|
|
228
|
+
role,
|
|
229
|
+
scopes,
|
|
230
|
+
auth: authToken ? { token: authToken, deviceToken: storedToken ?? undefined } : undefined,
|
|
231
|
+
device: {
|
|
232
|
+
id: identity.deviceId,
|
|
233
|
+
publicKey: publicKeyRawBase64Url(identity.publicKeyPem),
|
|
234
|
+
signature,
|
|
235
|
+
signedAt: signedAtMs,
|
|
236
|
+
nonce,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
// Track this as a pending request
|
|
241
|
+
pendingRequests.set(connectId, {
|
|
242
|
+
resolve: (payload) => {
|
|
243
|
+
clearTimeout(connectTimeout);
|
|
244
|
+
// Store device token if returned
|
|
245
|
+
const helloOk = payload;
|
|
246
|
+
const authInfo = helloOk?.auth;
|
|
247
|
+
if (authInfo?.deviceToken && typeof authInfo.deviceToken === "string") {
|
|
248
|
+
storeDeviceAuthToken(identity.deviceId, authInfo.role ?? role, authInfo.deviceToken);
|
|
249
|
+
}
|
|
250
|
+
const conn = {
|
|
251
|
+
ws,
|
|
252
|
+
deviceId: identity.deviceId,
|
|
253
|
+
request(method, params) {
|
|
254
|
+
return new Promise((res, rej) => {
|
|
255
|
+
const id = (0, node_crypto_1.randomUUID)();
|
|
256
|
+
const timer = setTimeout(() => {
|
|
257
|
+
pendingRequests.delete(id);
|
|
258
|
+
rej(new Error(`Gateway request timeout: ${method} (${requestTimeoutMs}ms)`));
|
|
259
|
+
}, requestTimeoutMs);
|
|
260
|
+
pendingRequests.set(id, {
|
|
261
|
+
resolve: (v) => { clearTimeout(timer); res(v); },
|
|
262
|
+
reject: (e) => { clearTimeout(timer); rej(e); },
|
|
263
|
+
});
|
|
264
|
+
ws.send(JSON.stringify({ type: "req", id, method, params: params ?? {} }));
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
close() {
|
|
268
|
+
ws.close();
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
resolve(conn);
|
|
272
|
+
},
|
|
273
|
+
reject: (err) => {
|
|
274
|
+
clearTimeout(connectTimeout);
|
|
275
|
+
reject(err);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
ws.send(JSON.stringify(frame));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// Response frame
|
|
282
|
+
if (type === "res") {
|
|
283
|
+
const id = msg.id;
|
|
284
|
+
const pending = pendingRequests.get(id);
|
|
285
|
+
if (pending) {
|
|
286
|
+
pendingRequests.delete(id);
|
|
287
|
+
if (msg.ok === true) {
|
|
288
|
+
pending.resolve(msg.payload);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const error = msg.error;
|
|
292
|
+
pending.reject(new Error(error?.message ?? `Gateway request failed: ${error?.code ?? "unknown"}`));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
package/dist/agent/protocol.js
CHANGED
|
@@ -155,5 +155,7 @@ function validatePayload(type, payload) {
|
|
|
155
155
|
function isExpired(envelope) {
|
|
156
156
|
const ttl = envelope.ttl ?? DEFAULT_TTL;
|
|
157
157
|
const created = new Date(envelope.timestamp).getTime();
|
|
158
|
+
if (Number.isNaN(created))
|
|
159
|
+
return true;
|
|
158
160
|
return Date.now() - created > ttl * 1000;
|
|
159
161
|
}
|
package/dist/agent/router.d.ts
CHANGED
|
@@ -3,22 +3,27 @@ import type { RelayConnector } from "../relay/connector.js";
|
|
|
3
3
|
import type { PolicyEngine } from "./engine.js";
|
|
4
4
|
import type { TaskManager } from "./tasks.js";
|
|
5
5
|
import type { LayerBEnvelope, ProposePayload, TaskRecord } from "./types.js";
|
|
6
|
+
import type { SkillsRegistry } from "./services.js";
|
|
6
7
|
export interface AgentRouterOptions {
|
|
7
8
|
connector: RelayConnector;
|
|
8
9
|
engine: PolicyEngine;
|
|
9
10
|
tasks: TaskManager;
|
|
10
11
|
localClawId: string;
|
|
12
|
+
skillsRegistry?: SkillsRegistry;
|
|
11
13
|
}
|
|
12
14
|
export declare class AgentRouter extends EventEmitter {
|
|
13
|
-
private
|
|
15
|
+
private connector;
|
|
14
16
|
private readonly engine;
|
|
15
17
|
private readonly tasks;
|
|
16
18
|
private readonly localClawId;
|
|
19
|
+
private readonly skillsRegistry;
|
|
17
20
|
private dataHandler;
|
|
18
21
|
private inbox;
|
|
19
22
|
constructor(opts: AgentRouterOptions);
|
|
20
23
|
start(): void;
|
|
21
24
|
stop(): void;
|
|
25
|
+
/** Replace the relay connector (e.g. after token refresh reconnection) */
|
|
26
|
+
setConnector(c: RelayConnector): void;
|
|
22
27
|
sendMessage(roomId: string, envelope: LayerBEnvelope): boolean;
|
|
23
28
|
/** Initiate a propose to a peer (outbound task) */
|
|
24
29
|
propose(roomId: string, targetClawId: string, task: ProposePayload["task"]): TaskRecord;
|
|
@@ -28,6 +33,10 @@ export declare class AgentRouter extends EventEmitter {
|
|
|
28
33
|
approveInbox(messageId: string): TaskRecord | null;
|
|
29
34
|
/** Deny a queued inbound proposal */
|
|
30
35
|
denyInbox(messageId: string, reason?: string): void;
|
|
36
|
+
/** Send a task report to a peer */
|
|
37
|
+
sendReport(roomId: string, targetClawId: string, taskId: string, status: "completed" | "failed", result?: unknown, error?: string): void;
|
|
38
|
+
/** Send a heartbeat for an executing task */
|
|
39
|
+
sendHeartbeat(roomId: string, targetClawId: string, taskId: string, progressPct?: number): void;
|
|
31
40
|
/** Get pending inbox items */
|
|
32
41
|
getInbox(): Array<{
|
|
33
42
|
message_id: string;
|
package/dist/agent/router.js
CHANGED
|
@@ -10,6 +10,7 @@ class AgentRouter extends node_events_1.EventEmitter {
|
|
|
10
10
|
engine;
|
|
11
11
|
tasks;
|
|
12
12
|
localClawId;
|
|
13
|
+
skillsRegistry;
|
|
13
14
|
dataHandler = null;
|
|
14
15
|
// Map queued message_id → { envelope, roomId } for manual approve/deny
|
|
15
16
|
inbox = new Map();
|
|
@@ -19,6 +20,7 @@ class AgentRouter extends node_events_1.EventEmitter {
|
|
|
19
20
|
this.engine = opts.engine;
|
|
20
21
|
this.tasks = opts.tasks;
|
|
21
22
|
this.localClawId = opts.localClawId;
|
|
23
|
+
this.skillsRegistry = opts.skillsRegistry ?? null;
|
|
22
24
|
}
|
|
23
25
|
start() {
|
|
24
26
|
this.dataHandler = (roomId, plaintext) => {
|
|
@@ -32,6 +34,18 @@ class AgentRouter extends node_events_1.EventEmitter {
|
|
|
32
34
|
this.dataHandler = null;
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
/** Replace the relay connector (e.g. after token refresh reconnection) */
|
|
38
|
+
setConnector(c) {
|
|
39
|
+
// Unbind from old connector
|
|
40
|
+
if (this.dataHandler) {
|
|
41
|
+
this.connector.off("data", this.dataHandler);
|
|
42
|
+
}
|
|
43
|
+
this.connector = c;
|
|
44
|
+
// Re-bind to new connector
|
|
45
|
+
if (this.dataHandler) {
|
|
46
|
+
this.connector.on("data", this.dataHandler);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
35
49
|
sendMessage(roomId, envelope) {
|
|
36
50
|
return this.connector.sendData(roomId, JSON.stringify(envelope));
|
|
37
51
|
}
|
|
@@ -83,6 +97,18 @@ class AgentRouter extends node_events_1.EventEmitter {
|
|
|
83
97
|
const reply = (0, protocol_js_1.createEnvelope)(this.localClawId, entry.envelope.from, "reject", { task_id: entry.envelope.message_id, reason: "user_denied", message: reason }, { in_reply_to: entry.envelope.message_id });
|
|
84
98
|
this.sendMessage(entry.roomId, reply);
|
|
85
99
|
}
|
|
100
|
+
/** Send a task report to a peer */
|
|
101
|
+
sendReport(roomId, targetClawId, taskId, status, result, error) {
|
|
102
|
+
const payload = { task_id: taskId, status, result, error };
|
|
103
|
+
const envelope = (0, protocol_js_1.createEnvelope)(this.localClawId, targetClawId, "report", payload, { in_reply_to: taskId });
|
|
104
|
+
this.sendMessage(roomId, envelope);
|
|
105
|
+
}
|
|
106
|
+
/** Send a heartbeat for an executing task */
|
|
107
|
+
sendHeartbeat(roomId, targetClawId, taskId, progressPct) {
|
|
108
|
+
const payload = { task_id: taskId, progress_pct: progressPct };
|
|
109
|
+
const envelope = (0, protocol_js_1.createEnvelope)(this.localClawId, targetClawId, "heartbeat", payload, { in_reply_to: taskId });
|
|
110
|
+
this.sendMessage(roomId, envelope);
|
|
111
|
+
}
|
|
86
112
|
/** Get pending inbox items */
|
|
87
113
|
getInbox() {
|
|
88
114
|
return Array.from(this.inbox.entries()).map(([id, entry]) => ({
|
|
@@ -132,6 +158,7 @@ class AgentRouter extends node_events_1.EventEmitter {
|
|
|
132
158
|
}
|
|
133
159
|
handleProposal(envelope, roomId) {
|
|
134
160
|
const decision = this.engine.evaluate(envelope);
|
|
161
|
+
console.log(`[clawnexus] [Router] Proposal from ${envelope.from}: ${decision.result}`);
|
|
135
162
|
this.emit("decision", envelope, decision, roomId);
|
|
136
163
|
switch (decision.result) {
|
|
137
164
|
case "accept":
|
|
@@ -171,8 +198,10 @@ class AgentRouter extends node_events_1.EventEmitter {
|
|
|
171
198
|
return record;
|
|
172
199
|
}
|
|
173
200
|
handleQuery(envelope, roomId) {
|
|
174
|
-
|
|
175
|
-
|
|
201
|
+
const capabilities = this.skillsRegistry
|
|
202
|
+
? this.skillsRegistry.getCapabilities()
|
|
203
|
+
: [];
|
|
204
|
+
const reply = (0, protocol_js_1.createEnvelope)(this.localClawId, envelope.from, "capability", { capabilities }, { in_reply_to: envelope.message_id });
|
|
176
205
|
this.sendMessage(roomId, reply);
|
|
177
206
|
}
|
|
178
207
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { AgentSkill } from "../a2a/card.js";
|
|
3
|
+
import type { ServiceCapability } from "./types.js";
|
|
4
|
+
declare const DEFAULT_SKILL: AgentSkill;
|
|
5
|
+
export interface SkillsRegistryOptions {
|
|
6
|
+
gatewayUrl?: string;
|
|
7
|
+
refreshIntervalMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class SkillsRegistry extends EventEmitter {
|
|
10
|
+
private readonly gatewayUrl;
|
|
11
|
+
private readonly refreshIntervalMs;
|
|
12
|
+
private skills;
|
|
13
|
+
private lastRefreshed;
|
|
14
|
+
private refreshTimer;
|
|
15
|
+
private closed;
|
|
16
|
+
constructor(opts?: SkillsRegistryOptions);
|
|
17
|
+
/** Start the registry — initial refresh + periodic timer */
|
|
18
|
+
start(): void;
|
|
19
|
+
/** Stop the registry */
|
|
20
|
+
stop(): void;
|
|
21
|
+
/** Get current skills (returns DEFAULT_SKILL if none fetched) */
|
|
22
|
+
getSkills(): AgentSkill[];
|
|
23
|
+
/** Get skills as ServiceCapability[] for Layer B capability responses */
|
|
24
|
+
getCapabilities(): ServiceCapability[];
|
|
25
|
+
/** Get registry status */
|
|
26
|
+
getStatus(): {
|
|
27
|
+
skill_count: number;
|
|
28
|
+
last_refreshed: string | null;
|
|
29
|
+
source: "gateway" | "default";
|
|
30
|
+
};
|
|
31
|
+
/** Refresh skills from the Gateway. Returns true if successful. */
|
|
32
|
+
refresh(): Promise<boolean>;
|
|
33
|
+
/** Connect to Gateway, call tools.catalog, disconnect */
|
|
34
|
+
private fetchToolsCatalog;
|
|
35
|
+
}
|
|
36
|
+
export { DEFAULT_SKILL };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Layer B — Skills Registry
|
|
3
|
+
// Connects to local OpenClaw Gateway, fetches installed tools via tools.catalog,
|
|
4
|
+
// and exposes them as AgentSkill[] for Agent Card and capability queries.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_SKILL = exports.SkillsRegistry = void 0;
|
|
7
|
+
const node_events_1 = require("node:events");
|
|
8
|
+
const gateway_js_1 = require("./gateway.js");
|
|
9
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
10
|
+
const DEFAULT_SKILL = {
|
|
11
|
+
id: "general-assistant",
|
|
12
|
+
name: "General Assistant",
|
|
13
|
+
description: "General-purpose AI assistant",
|
|
14
|
+
tags: ["general"],
|
|
15
|
+
};
|
|
16
|
+
exports.DEFAULT_SKILL = DEFAULT_SKILL;
|
|
17
|
+
class SkillsRegistry extends node_events_1.EventEmitter {
|
|
18
|
+
gatewayUrl;
|
|
19
|
+
refreshIntervalMs;
|
|
20
|
+
skills = [];
|
|
21
|
+
lastRefreshed = null;
|
|
22
|
+
refreshTimer = null;
|
|
23
|
+
closed = false;
|
|
24
|
+
constructor(opts = {}) {
|
|
25
|
+
super();
|
|
26
|
+
this.gatewayUrl = opts.gatewayUrl ?? "ws://127.0.0.1:18789";
|
|
27
|
+
this.refreshIntervalMs = opts.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS;
|
|
28
|
+
}
|
|
29
|
+
/** Start the registry — initial refresh + periodic timer */
|
|
30
|
+
start() {
|
|
31
|
+
// Initial refresh (async, non-blocking)
|
|
32
|
+
this.refresh().catch((err) => {
|
|
33
|
+
console.log(`[clawnexus] [Skills] Initial refresh failed (non-fatal): ${err}`);
|
|
34
|
+
});
|
|
35
|
+
// Periodic refresh
|
|
36
|
+
this.refreshTimer = setInterval(() => {
|
|
37
|
+
this.refresh().catch((err) => {
|
|
38
|
+
console.log(`[clawnexus] [Skills] Periodic refresh failed (non-fatal): ${err}`);
|
|
39
|
+
});
|
|
40
|
+
}, this.refreshIntervalMs);
|
|
41
|
+
}
|
|
42
|
+
/** Stop the registry */
|
|
43
|
+
stop() {
|
|
44
|
+
this.closed = true;
|
|
45
|
+
if (this.refreshTimer) {
|
|
46
|
+
clearInterval(this.refreshTimer);
|
|
47
|
+
this.refreshTimer = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Get current skills (returns DEFAULT_SKILL if none fetched) */
|
|
51
|
+
getSkills() {
|
|
52
|
+
return this.skills.length > 0 ? this.skills : [DEFAULT_SKILL];
|
|
53
|
+
}
|
|
54
|
+
/** Get skills as ServiceCapability[] for Layer B capability responses */
|
|
55
|
+
getCapabilities() {
|
|
56
|
+
return this.getSkills().map((skill) => ({
|
|
57
|
+
service_type: skill.id,
|
|
58
|
+
description: skill.description,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
/** Get registry status */
|
|
62
|
+
getStatus() {
|
|
63
|
+
return {
|
|
64
|
+
skill_count: this.skills.length > 0 ? this.skills.length : 1,
|
|
65
|
+
last_refreshed: this.lastRefreshed,
|
|
66
|
+
source: this.skills.length > 0 ? "gateway" : "default",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Refresh skills from the Gateway. Returns true if successful. */
|
|
70
|
+
async refresh() {
|
|
71
|
+
if (this.closed)
|
|
72
|
+
return false;
|
|
73
|
+
try {
|
|
74
|
+
const tools = await this.fetchToolsCatalog();
|
|
75
|
+
this.skills = tools.map(toolToSkill);
|
|
76
|
+
this.lastRefreshed = new Date().toISOString();
|
|
77
|
+
this.emit("refreshed", this.skills);
|
|
78
|
+
console.log(`[clawnexus] [Skills] Refreshed: ${this.skills.length} skill(s) from Gateway`);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
this.emit("refresh_error", err);
|
|
83
|
+
// Keep existing skills (or default) — don't clear on failure
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Connect to Gateway, call tools.catalog, disconnect */
|
|
88
|
+
async fetchToolsCatalog() {
|
|
89
|
+
const conn = await (0, gateway_js_1.connectGateway)({
|
|
90
|
+
gatewayUrl: this.gatewayUrl,
|
|
91
|
+
scopes: ["operator.read"],
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
const result = await conn.request("tools.catalog", {});
|
|
95
|
+
// v3 catalog returns { groups: [{ tools: [...] }] }
|
|
96
|
+
const groups = result?.groups;
|
|
97
|
+
if (Array.isArray(groups)) {
|
|
98
|
+
const tools = [];
|
|
99
|
+
for (const group of groups) {
|
|
100
|
+
const groupTools = group.tools;
|
|
101
|
+
if (Array.isArray(groupTools)) {
|
|
102
|
+
tools.push(...groupTools);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return tools;
|
|
106
|
+
}
|
|
107
|
+
// Fallback: flat array
|
|
108
|
+
if (Array.isArray(result)) {
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
conn.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.SkillsRegistry = SkillsRegistry;
|
|
119
|
+
/** Convert an OpenClaw tool entry to AgentSkill */
|
|
120
|
+
function toolToSkill(tool) {
|
|
121
|
+
const id = tool.id ?? tool.name ?? "unknown";
|
|
122
|
+
const label = tool.label ?? tool.name ?? id;
|
|
123
|
+
return {
|
|
124
|
+
id,
|
|
125
|
+
name: formatSkillName(label),
|
|
126
|
+
description: tool.description ?? "",
|
|
127
|
+
tags: inferTags(id),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Convert snake_case/kebab-case tool name to human-readable */
|
|
131
|
+
function formatSkillName(name) {
|
|
132
|
+
return name
|
|
133
|
+
.replace(/[_-]/g, " ")
|
|
134
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
135
|
+
}
|
|
136
|
+
/** Infer tags from tool name */
|
|
137
|
+
function inferTags(name) {
|
|
138
|
+
const lower = name.toLowerCase();
|
|
139
|
+
const tags = [];
|
|
140
|
+
if (lower.includes("web") || lower.includes("search") || lower.includes("browse"))
|
|
141
|
+
tags.push("web");
|
|
142
|
+
if (lower.includes("file") || lower.includes("read") || lower.includes("write"))
|
|
143
|
+
tags.push("filesystem");
|
|
144
|
+
if (lower.includes("code") || lower.includes("exec") || lower.includes("run"))
|
|
145
|
+
tags.push("code");
|
|
146
|
+
if (lower.includes("image") || lower.includes("draw") || lower.includes("vision"))
|
|
147
|
+
tags.push("media");
|
|
148
|
+
if (lower.includes("api") || lower.includes("http") || lower.includes("fetch"))
|
|
149
|
+
tags.push("network");
|
|
150
|
+
if (tags.length === 0)
|
|
151
|
+
tags.push("general");
|
|
152
|
+
return tags;
|
|
153
|
+
}
|
package/dist/agent/tasks.js
CHANGED
|
@@ -50,7 +50,7 @@ const TASK_TIMEOUT_S = 600; // 10 minutes default
|
|
|
50
50
|
// Valid state transitions
|
|
51
51
|
const TRANSITIONS = {
|
|
52
52
|
pending: ["accepted", "rejected", "cancelled", "timeout"],
|
|
53
|
-
accepted: ["executing", "cancelled", "timeout"],
|
|
53
|
+
accepted: ["executing", "completed", "failed", "cancelled", "timeout"],
|
|
54
54
|
executing: ["completed", "failed", "cancelled", "timeout"],
|
|
55
55
|
completed: [],
|
|
56
56
|
failed: [],
|
|
@@ -101,6 +101,8 @@ class TaskManager extends node_events_1.EventEmitter {
|
|
|
101
101
|
this.tasks.set(record.task_id, record);
|
|
102
102
|
this.scheduleDirtyFlush();
|
|
103
103
|
this.emit("created", record);
|
|
104
|
+
// Also emit stateChange so listeners (e.g. TaskExecutor) can react to initial state
|
|
105
|
+
this.emit("stateChange", record, record.state);
|
|
104
106
|
}
|
|
105
107
|
updateState(taskId, newState, extra) {
|
|
106
108
|
const task = this.tasks.get(taskId);
|
|
@@ -241,7 +243,7 @@ class TaskManager extends node_events_1.EventEmitter {
|
|
|
241
243
|
if (!ACTIVE_STATES.has(task.state))
|
|
242
244
|
continue;
|
|
243
245
|
const maxDuration = (task.task.constraints?.max_duration_s ?? TASK_TIMEOUT_S) * 1000;
|
|
244
|
-
const elapsed = now - new Date(task.
|
|
246
|
+
const elapsed = now - new Date(task.created_at).getTime();
|
|
245
247
|
if (elapsed > maxDuration) {
|
|
246
248
|
this.updateState(task.task_id, "timeout");
|
|
247
249
|
this.emit("timeout", task);
|
package/dist/api/server.d.ts
CHANGED
|
@@ -8,17 +8,21 @@ import type { UnreachableInstance } from "../types.js";
|
|
|
8
8
|
import { PolicyEngine } from "../agent/engine.js";
|
|
9
9
|
import { TaskManager } from "../agent/tasks.js";
|
|
10
10
|
import { AgentRouter } from "../agent/router.js";
|
|
11
|
+
import { TaskExecutor } from "../agent/executor.js";
|
|
11
12
|
import { BroadcastDiscovery } from "../discovery/broadcast.js";
|
|
12
13
|
import type { IdentityKeys } from "../crypto/keys.js";
|
|
13
14
|
import { RegistryClient } from "../registry/client.js";
|
|
14
15
|
import { AutoRegister } from "../registry/auto-register.js";
|
|
15
16
|
import { RemoteDiscovery } from "../registry/discovery.js";
|
|
16
17
|
import { RelayConnector } from "../relay/connector.js";
|
|
17
|
-
|
|
18
|
+
import { SkillsRegistry } from "../agent/services.js";
|
|
19
|
+
export declare function registerRelayRoutes(app: FastifyInstance, getConnector: () => RelayConnector | null, getTokenRefresher?: () => (() => Promise<string>) | null): void;
|
|
18
20
|
export interface AgentDeps {
|
|
19
21
|
engine: PolicyEngine;
|
|
20
22
|
tasks: TaskManager;
|
|
21
23
|
getRouter: () => AgentRouter | null;
|
|
24
|
+
getExecutor: () => TaskExecutor | null;
|
|
25
|
+
skillsRegistry?: SkillsRegistry;
|
|
22
26
|
}
|
|
23
27
|
export declare function registerAgentRoutes(app: FastifyInstance, deps: AgentDeps): void;
|
|
24
28
|
export interface RegistryDeps {
|
|
@@ -39,7 +43,7 @@ export interface DiagnosticsDeps {
|
|
|
39
43
|
unreachable: UnreachableInstance[];
|
|
40
44
|
}
|
|
41
45
|
export declare function registerDiagnosticsRoutes(app: FastifyInstance, deps: DiagnosticsDeps): void;
|
|
42
|
-
export declare function registerA2aRoutes(app: FastifyInstance, store: RegistryStore, daemonVersion: string): void;
|
|
46
|
+
export declare function registerA2aRoutes(app: FastifyInstance, store: RegistryStore, daemonVersion: string, skillsRegistry?: SkillsRegistry): void;
|
|
43
47
|
export interface DaemonOptions {
|
|
44
48
|
port?: number;
|
|
45
49
|
host?: string;
|
|
@@ -57,6 +61,7 @@ export interface DaemonHandle {
|
|
|
57
61
|
engine: PolicyEngine;
|
|
58
62
|
tasks: TaskManager;
|
|
59
63
|
getRouter: () => AgentRouter | null;
|
|
64
|
+
skillsRegistry: SkillsRegistry;
|
|
60
65
|
registryClient: RegistryClient | null;
|
|
61
66
|
autoRegister: AutoRegister | null;
|
|
62
67
|
remoteDiscovery: RemoteDiscovery | null;
|