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.
@@ -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
+ }
@@ -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
  }
@@ -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 readonly connector;
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;
@@ -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
- // Respond with capability info (placeholder — actual capabilities from services.ts in future)
175
- const reply = (0, protocol_js_1.createEnvelope)(this.localClawId, envelope.from, "capability", { capabilities: [] }, { in_reply_to: envelope.message_id });
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
+ }
@@ -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.updated_at).getTime();
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);
@@ -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
- export declare function registerRelayRoutes(app: FastifyInstance, getConnector: () => RelayConnector | null): void;
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;