@agentrux/agentrux-openclaw-plugin 0.3.2

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,266 @@
1
+ "use strict";
2
+ /**
3
+ * Dispatcher: async loop.
4
+ * Consumes queue → Pull → dedup → subagent.run() → outbox → waterline.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.Dispatcher = void 0;
41
+ const crypto = __importStar(require("crypto"));
42
+ const http = __importStar(require("http"));
43
+ const http_client_1 = require("./http-client");
44
+ const cursor_1 = require("./cursor");
45
+ const outbox_1 = require("./outbox");
46
+ const session_1 = require("./session");
47
+ const sanitize_1 = require("./sanitize");
48
+ class Dispatcher {
49
+ constructor(config, creds, cursor, queue, logger) {
50
+ this.config = config;
51
+ this.creds = creds;
52
+ this.cursor = cursor;
53
+ this.queue = queue;
54
+ this.logger = logger;
55
+ this.running = false;
56
+ this.stopped = false;
57
+ // In-memory dedup: prevents same seq/request from being processed twice
58
+ this.processedRequestIds = new Set();
59
+ this.processingSeqs = new Set();
60
+ // Status publish tracking: only publish "processing" once per request
61
+ this.statusPublished = new Set();
62
+ }
63
+ async start() {
64
+ if (this.running)
65
+ throw new Error("Dispatcher already running");
66
+ this.running = true;
67
+ this.stopped = false;
68
+ // Crash recovery: re-enqueue inFlight seqs from previous run
69
+ // NOTE: do NOT clear inFlight here — they stay in-flight until processEvent
70
+ // completes and the outbox flush finalizes them via markCompleted.
71
+ if (this.cursor.inFlight.size > 0) {
72
+ this.logger.info(`Crash recovery: re-enqueueing ${this.cursor.inFlight.size} in-flight seqs`);
73
+ const recovered = [];
74
+ for (const seq of this.cursor.inFlight) {
75
+ const ok = this.queue.enqueue({
76
+ topicId: this.config.commandTopicId,
77
+ latestSequenceNo: seq,
78
+ timestamp: Date.now(),
79
+ });
80
+ if (ok)
81
+ recovered.push(seq);
82
+ }
83
+ // Only remove from inFlight the ones we failed to enqueue (queue full)
84
+ // The successfully enqueued ones will be cleared by processEvent → markCompleted
85
+ for (const seq of this.cursor.inFlight) {
86
+ if (!recovered.includes(seq)) {
87
+ this.logger.warn(`Crash recovery: could not re-enqueue seq=${seq} (queue full)`);
88
+ }
89
+ }
90
+ }
91
+ this.loop();
92
+ }
93
+ stop() {
94
+ this.stopped = true;
95
+ }
96
+ async loop() {
97
+ try {
98
+ while (!this.stopped) {
99
+ try {
100
+ await this.tick();
101
+ }
102
+ catch (e) {
103
+ this.logger.error("Dispatcher tick error:", e.message);
104
+ }
105
+ await sleep(500); // 500ms between ticks (reduce CPU)
106
+ }
107
+ }
108
+ finally {
109
+ this.running = false;
110
+ }
111
+ }
112
+ async tick() {
113
+ // 1. Dequeue hints
114
+ const hints = this.queue.dequeue(this.config.maxConcurrency);
115
+ // 2. Pull full events for hints
116
+ if (hints.length > 0) {
117
+ const events = await (0, http_client_1.pullEvents)(this.creds, this.config.commandTopicId, this.cursor.waterline, 50);
118
+ for (const evt of events) {
119
+ await this.processEvent(evt);
120
+ }
121
+ }
122
+ // 3. Flush outbox (always, regardless of hints)
123
+ const finalized = await (0, outbox_1.flushOutbox)(this.creds, this.config.resultTopicId, this.logger);
124
+ for (const seq of finalized) {
125
+ (0, cursor_1.markCompleted)(this.cursor, seq);
126
+ }
127
+ // 4. Periodic cleanup (every ~50 ticks)
128
+ if (Math.random() < 0.02) {
129
+ (0, cursor_1.cleanupExpiredEvents)(this.cursor);
130
+ (0, cursor_1.persistCursor)(this.cursor);
131
+ // Cap in-memory sets
132
+ if (this.processedRequestIds.size > 10_000) {
133
+ this.processedRequestIds.clear();
134
+ }
135
+ if (this.statusPublished.size > 10_000) {
136
+ this.statusPublished.clear();
137
+ }
138
+ }
139
+ }
140
+ async processEvent(evt) {
141
+ const seq = evt.sequence_no;
142
+ const eventId = evt.event_id;
143
+ // ---- DEDUP LAYER 1: Transport (seq + event_id) ----
144
+ if (seq <= this.cursor.waterline)
145
+ return;
146
+ if (this.cursor.completed.has(seq))
147
+ return;
148
+ if (this.processingSeqs.has(seq))
149
+ return;
150
+ // inFlight check: skip if already being processed concurrently,
151
+ // but allow re-processing for crash recovery (inFlight from previous run)
152
+ if (this.cursor.inFlight.has(seq) && this.processingSeqs.has(seq))
153
+ return;
154
+ if ((0, cursor_1.isEventProcessed)(this.cursor, eventId))
155
+ return;
156
+ const payload = evt.payload || {};
157
+ const requestId = payload.request_id || eventId;
158
+ // ---- DEDUP LAYER 2: Application (request_id) ----
159
+ if (this.processedRequestIds.has(requestId)) {
160
+ // Already processed in this session — just advance cursor
161
+ (0, cursor_1.markCompleted)(this.cursor, seq);
162
+ return;
163
+ }
164
+ const existingOutbox = (0, outbox_1.findByRequestId)(requestId);
165
+ if (existingOutbox) {
166
+ (0, cursor_1.markCompleted)(this.cursor, seq);
167
+ return;
168
+ }
169
+ // ---- LOCK ----
170
+ this.processingSeqs.add(seq);
171
+ this.processedRequestIds.add(requestId);
172
+ (0, cursor_1.markInFlight)(this.cursor, seq);
173
+ // NOTE: recordProcessedEvent is deferred until durable completion (outbox write)
174
+ try {
175
+ const messageText = payload.message || payload.text || JSON.stringify(payload);
176
+ const conversationKey = payload.conversation_key
177
+ ? (0, session_1.validateConversationKey)(payload.conversation_key)
178
+ : "default";
179
+ const sessionKey = (0, session_1.buildSessionKey)(this.config.agentId, this.config.commandTopicId, conversationKey);
180
+ const wrappedMessage = (0, sanitize_1.wrapMessage)(messageText);
181
+ const idempotencyKey = crypto.randomUUID();
182
+ this.logger.info(`Processing: seq=${seq} requestId=${requestId}`);
183
+ // Publish "processing" status ONCE per request
184
+ if (!this.statusPublished.has(requestId)) {
185
+ this.statusPublished.add(requestId);
186
+ try {
187
+ await (0, http_client_1.publishEvent)(this.creds, this.config.resultTopicId, "openclaw.status", {
188
+ request_id: requestId,
189
+ status: "processing",
190
+ });
191
+ }
192
+ catch { }
193
+ }
194
+ // Call internal dispatch endpoint
195
+ const dispatchResult = await this.callDispatchEndpoint({
196
+ sessionKey,
197
+ message: wrappedMessage,
198
+ idempotencyKey,
199
+ timeoutMs: this.config.subagentTimeoutMs,
200
+ });
201
+ const responseText = dispatchResult.responseText || "";
202
+ // Add to outbox (waterline is NOT advanced here — outbox flush owns that)
203
+ (0, outbox_1.addToOutbox)({
204
+ eventId,
205
+ requestId,
206
+ sequenceNo: seq,
207
+ result: {
208
+ message: (0, sanitize_1.sanitizeResponse)(responseText || "No response from agent"),
209
+ status: dispatchResult.status === "ok" ? "completed" : "failed",
210
+ conversationKey,
211
+ },
212
+ });
213
+ // Record processed event AFTER durable outbox write
214
+ (0, cursor_1.recordProcessedEvent)(this.cursor, eventId);
215
+ this.logger.info(`Outboxed: seq=${seq} requestId=${requestId}`);
216
+ }
217
+ catch (e) {
218
+ this.logger.error(`Event processing failed: seq=${seq} error=${e.message}`);
219
+ (0, outbox_1.addToOutbox)({
220
+ eventId,
221
+ requestId,
222
+ sequenceNo: seq,
223
+ result: {
224
+ message: `Agent error: ${e.message}`,
225
+ status: "failed",
226
+ },
227
+ });
228
+ (0, cursor_1.recordProcessedEvent)(this.cursor, eventId);
229
+ }
230
+ finally {
231
+ this.processingSeqs.delete(seq);
232
+ }
233
+ }
234
+ callDispatchEndpoint(params) {
235
+ return new Promise((resolve, reject) => {
236
+ const body = JSON.stringify(params);
237
+ const req = http.request({
238
+ hostname: "127.0.0.1",
239
+ port: this.config.gatewayPort,
240
+ path: "/agentrux/dispatch",
241
+ method: "POST",
242
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
243
+ timeout: params.timeoutMs + 10_000,
244
+ }, (res) => {
245
+ let raw = "";
246
+ res.on("data", (c) => (raw += c.toString()));
247
+ res.on("end", () => {
248
+ try {
249
+ resolve(JSON.parse(raw));
250
+ }
251
+ catch {
252
+ reject(new Error(`Dispatch response parse error: ${raw.slice(0, 200)}`));
253
+ }
254
+ });
255
+ });
256
+ req.on("error", reject);
257
+ req.on("timeout", () => { req.destroy(); reject(new Error("Dispatch timeout")); });
258
+ req.write(body);
259
+ req.end();
260
+ });
261
+ }
262
+ }
263
+ exports.Dispatcher = Dispatcher;
264
+ function sleep(ms) {
265
+ return new Promise((r) => setTimeout(r, ms));
266
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * AgenTrux HTTP client — no external dependencies.
3
+ * Handles JWT auth with auto-refresh.
4
+ */
5
+ import { type Credentials } from "./credentials";
6
+ export declare function httpJson(method: string, url: string, body?: Record<string, unknown>, headers?: Record<string, string>): Promise<{
7
+ status: number;
8
+ data: any;
9
+ }>;
10
+ export declare function ensureToken(creds: Credentials): Promise<string>;
11
+ export declare function invalidateToken(): void;
12
+ export declare function authRequest(creds: Credentials, method: string, urlPath: string, body?: Record<string, unknown>): Promise<any>;
13
+ export declare function pullEvents(creds: Credentials, topicId: string, afterSeq: number, limit?: number): Promise<any[]>;
14
+ export declare function publishEvent(creds: Credentials, topicId: string, eventType: string, payload: Record<string, unknown>): Promise<string>;
15
+ /**
16
+ * Upload a file to AgenTrux via presigned URL.
17
+ * Returns { object_id, download_url } for attaching to events.
18
+ */
19
+ export declare function uploadFile(creds: Credentials, topicId: string, filePath: string, contentType: string): Promise<{
20
+ object_id: string;
21
+ download_url: string;
22
+ }>;
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ /**
3
+ * AgenTrux HTTP client — no external dependencies.
4
+ * Handles JWT auth with auto-refresh.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.httpJson = httpJson;
41
+ exports.ensureToken = ensureToken;
42
+ exports.invalidateToken = invalidateToken;
43
+ exports.authRequest = authRequest;
44
+ exports.pullEvents = pullEvents;
45
+ exports.publishEvent = publishEvent;
46
+ exports.uploadFile = uploadFile;
47
+ const https = __importStar(require("https"));
48
+ const http = __importStar(require("http"));
49
+ // ---------------------------------------------------------------------------
50
+ // HTTP helper
51
+ // ---------------------------------------------------------------------------
52
+ function httpJson(method, url, body, headers) {
53
+ return new Promise((resolve, reject) => {
54
+ const u = new URL(url);
55
+ const mod = u.protocol === "https:" ? https : http;
56
+ const opts = {
57
+ method,
58
+ hostname: u.hostname,
59
+ port: u.port,
60
+ path: u.pathname + u.search,
61
+ headers: {
62
+ "Content-Type": "application/json",
63
+ ...headers,
64
+ },
65
+ };
66
+ const req = mod.request(opts, (res) => {
67
+ let raw = "";
68
+ res.on("data", (c) => (raw += c.toString()));
69
+ res.on("end", () => {
70
+ try {
71
+ resolve({ status: res.statusCode || 0, data: JSON.parse(raw) });
72
+ }
73
+ catch {
74
+ resolve({ status: res.statusCode || 0, data: raw });
75
+ }
76
+ });
77
+ });
78
+ req.on("error", reject);
79
+ if (body)
80
+ req.write(JSON.stringify(body));
81
+ req.end();
82
+ });
83
+ }
84
+ let tokenState = null;
85
+ async function ensureToken(creds) {
86
+ // Valid token with 60s buffer
87
+ if (tokenState && tokenState.expires_at > Date.now() + 60_000) {
88
+ return tokenState.access_token;
89
+ }
90
+ // Try refresh
91
+ if (tokenState?.refresh_token) {
92
+ const r = await httpJson("POST", `${creds.base_url}/auth/refresh`, {
93
+ refresh_token: tokenState.refresh_token,
94
+ });
95
+ if (r.status === 200) {
96
+ tokenState = {
97
+ access_token: r.data.access_token,
98
+ refresh_token: r.data.refresh_token,
99
+ expires_at: parseExpiresAt(r.data.expires_at),
100
+ };
101
+ return tokenState.access_token;
102
+ }
103
+ }
104
+ // Full auth
105
+ const r = await httpJson("POST", `${creds.base_url}/auth/token`, {
106
+ script_id: creds.script_id,
107
+ client_secret: creds.clientSecret,
108
+ });
109
+ if (r.status !== 200)
110
+ throw new Error(`Auth failed: ${JSON.stringify(r.data)}`);
111
+ tokenState = {
112
+ access_token: r.data.access_token,
113
+ refresh_token: r.data.refresh_token,
114
+ expires_at: parseExpiresAt(r.data.expires_at),
115
+ };
116
+ return tokenState.access_token;
117
+ }
118
+ function invalidateToken() {
119
+ tokenState = null;
120
+ }
121
+ function parseExpiresAt(ea) {
122
+ if (typeof ea === "string" && ea.includes("T")) {
123
+ return new Date(ea).getTime();
124
+ }
125
+ return typeof ea === "number" ? ea : Date.now() + 3600_000;
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Authenticated requests
129
+ // ---------------------------------------------------------------------------
130
+ async function authRequest(creds, method, urlPath, body) {
131
+ const token = await ensureToken(creds);
132
+ const r = await httpJson(method, `${creds.base_url}${urlPath}`, body, {
133
+ Authorization: `Bearer ${token}`,
134
+ });
135
+ if (r.status === 401) {
136
+ invalidateToken();
137
+ const newToken = await ensureToken(creds);
138
+ const retry = await httpJson(method, `${creds.base_url}${urlPath}`, body, {
139
+ Authorization: `Bearer ${newToken}`,
140
+ });
141
+ if (retry.status >= 400)
142
+ throw new Error(`Request failed: ${JSON.stringify(retry.data)}`);
143
+ return retry.data;
144
+ }
145
+ if (r.status >= 400)
146
+ throw new Error(`Request failed (${r.status}): ${JSON.stringify(r.data)}`);
147
+ return r.data;
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // AgenTrux API operations
151
+ // ---------------------------------------------------------------------------
152
+ async function pullEvents(creds, topicId, afterSeq, limit = 20) {
153
+ const result = await authRequest(creds, "GET", `/topics/${topicId}/events?after_sequence_no=${afterSeq}&limit=${limit}`);
154
+ return result.items || [];
155
+ }
156
+ async function publishEvent(creds, topicId, eventType, payload) {
157
+ const result = await authRequest(creds, "POST", `/topics/${topicId}/events`, {
158
+ type: eventType,
159
+ payload,
160
+ });
161
+ return result.event_id;
162
+ }
163
+ /**
164
+ * Upload a file to AgenTrux via presigned URL.
165
+ * Returns { object_id, download_url } for attaching to events.
166
+ */
167
+ async function uploadFile(creds, topicId, filePath, contentType) {
168
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
169
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
170
+ const data = fs.readFileSync(filePath);
171
+ const filename = path.basename(filePath);
172
+ // 1. Get presigned upload URL
173
+ const info = await authRequest(creds, "POST", `/topics/${topicId}/payloads`, {
174
+ content_type: contentType,
175
+ filename,
176
+ });
177
+ // 2. Upload file to presigned URL
178
+ const uploadUrl = new URL(info.upload_url);
179
+ const mod = uploadUrl.protocol === "https:" ? await Promise.resolve().then(() => __importStar(require("https"))) : await Promise.resolve().then(() => __importStar(require("http")));
180
+ await new Promise((resolve, reject) => {
181
+ const req = mod.request({
182
+ hostname: uploadUrl.hostname,
183
+ port: uploadUrl.port,
184
+ path: uploadUrl.pathname + uploadUrl.search,
185
+ method: "PUT",
186
+ headers: { "Content-Type": contentType, "Content-Length": data.length },
187
+ }, (res) => {
188
+ res.resume();
189
+ res.on("end", () => {
190
+ if (res.statusCode && res.statusCode < 300)
191
+ resolve();
192
+ else
193
+ reject(new Error(`Upload failed: ${res.statusCode}`));
194
+ });
195
+ });
196
+ req.on("error", reject);
197
+ req.write(data);
198
+ req.end();
199
+ });
200
+ return {
201
+ object_id: info.object_id,
202
+ download_url: info.download_url || `${creds.base_url}/topics/${topicId}/payloads/${info.object_id}`,
203
+ };
204
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * AgenTrux Ingress + Tools plugin for OpenClaw.
3
+ *
4
+ * Tools (LLM-callable):
5
+ * - agentrux_activate, agentrux_publish, agentrux_read,
6
+ * agentrux_send_message, agentrux_redeem_grant
7
+ *
8
+ * Ingress (external → OpenClaw):
9
+ * - registerHttpRoute("/agentrux/webhook") — AgenTrux Webhook receiver
10
+ * - Dispatcher (async loop) — subagent.run() + outbox + waterline cursor
11
+ * - SafetyPoller — gap detection fallback
12
+ *
13
+ * Credentials: ~/.agentrux/credentials.json (0600)
14
+ */
15
+ export default function (api: any): void;