@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.
- package/README.md +142 -0
- package/dist/__tests__/naming.test.d.ts +9 -0
- package/dist/__tests__/naming.test.js +143 -0
- package/dist/credentials.d.ts +11 -0
- package/dist/credentials.js +63 -0
- package/dist/cursor.d.ts +25 -0
- package/dist/cursor.js +144 -0
- package/dist/dispatcher.d.ts +38 -0
- package/dist/dispatcher.js +266 -0
- package/dist/http-client.d.ts +22 -0
- package/dist/http-client.js +204 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +345 -0
- package/dist/outbox.d.ts +37 -0
- package/dist/outbox.js +142 -0
- package/dist/poller.d.ts +25 -0
- package/dist/poller.js +78 -0
- package/dist/queue.d.ts +18 -0
- package/dist/queue.js +29 -0
- package/dist/sanitize.d.ts +9 -0
- package/dist/sanitize.js +39 -0
- package/dist/session.d.ts +6 -0
- package/dist/session.js +35 -0
- package/dist/sse-listener.d.ts +23 -0
- package/dist/sse-listener.js +170 -0
- package/dist/webhook-handler.d.ts +5 -0
- package/dist/webhook-handler.js +104 -0
- package/openclaw.plugin.json +29 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# AgenTrux Plugin for OpenClaw
|
|
2
|
+
|
|
3
|
+
Connect your OpenClaw agent to other agents via AgenTrux — authenticated Pub/Sub for autonomous agents.
|
|
4
|
+
|
|
5
|
+
**v0.3.1**: Now supports **Ingress mode** — external clients can send commands to OpenClaw via AgenTrux Topics and receive LLM-processed results back.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install @agentrux/agentrux-openclaw-plugin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### 1. Activate
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
OpenClaw> AgenTrux に接続して。activation code は ac_Abc123...
|
|
19
|
+
|
|
20
|
+
🔑 Activating...
|
|
21
|
+
✅ Connected! Credentials saved.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Configure Ingress (optional)
|
|
25
|
+
|
|
26
|
+
Add to your OpenClaw config (`plugins.entries.agentrux-openclaw-plugin.config`):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"commandTopicId": "your-command-topic-uuid",
|
|
31
|
+
"resultTopicId": "your-result-topic-uuid",
|
|
32
|
+
"agentId": "agentrux-rpa",
|
|
33
|
+
"ingressMode": "sse",
|
|
34
|
+
"webhookSecret": "whsec_...",
|
|
35
|
+
"pollIntervalMs": 60000,
|
|
36
|
+
"maxConcurrency": 3,
|
|
37
|
+
"subagentTimeoutMs": 120000
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Send commands from anywhere
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
curl -X POST "https://api.agentrux.com/topics/{commandTopicId}/events" \
|
|
45
|
+
-H "Authorization: Bearer $JWT" \
|
|
46
|
+
-d '{"type":"openclaw.request","payload":{"request_id":"req-001","message":"Check disk usage"}}'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
OpenClaw processes the request using its LLM + tools (exec, browser, etc.) and publishes the result to `resultTopicId`.
|
|
50
|
+
|
|
51
|
+
## Tools (LLM-callable)
|
|
52
|
+
|
|
53
|
+
| Tool | Description |
|
|
54
|
+
|------|-------------|
|
|
55
|
+
| `agentrux_activate` | Connect with a one-time activation code |
|
|
56
|
+
| `agentrux_publish` | Send an event to a topic |
|
|
57
|
+
| `agentrux_read` | Read events from a topic |
|
|
58
|
+
| `agentrux_send_message` | Send a message and wait for reply |
|
|
59
|
+
| `agentrux_redeem_grant` | Redeem an invite code for cross-account access |
|
|
60
|
+
|
|
61
|
+
## Ingress Modes
|
|
62
|
+
|
|
63
|
+
| Mode | How it works | When to use |
|
|
64
|
+
|------|-------------|-------------|
|
|
65
|
+
| `webhook` (default) | AgenTrux pushes hints to `/agentrux/webhook` | Public IP + HTTPS available |
|
|
66
|
+
| `sse` | Plugin connects to AgenTrux SSE stream | No public IP (e.g. Spot VM, NAT) |
|
|
67
|
+
|
|
68
|
+
Both modes include a **safety poller** (default 60s) as fallback for gap detection.
|
|
69
|
+
|
|
70
|
+
## Message Format
|
|
71
|
+
|
|
72
|
+
### Request (external → Topic)
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"type": "openclaw.request",
|
|
77
|
+
"payload": {
|
|
78
|
+
"request_id": "req-001",
|
|
79
|
+
"conversation_key": "user-42/session-1",
|
|
80
|
+
"message": "Check disk usage and report"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Response (OpenClaw → Topic)
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"type": "openclaw.response",
|
|
90
|
+
"payload": {
|
|
91
|
+
"request_id": "req-001",
|
|
92
|
+
"status": "completed",
|
|
93
|
+
"message": "Disk usage: /dev/root 49G 4.8G 44G 10%"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Architecture
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
External Client OpenClaw Gateway
|
|
102
|
+
───────────── ────────────────
|
|
103
|
+
publish(commandTopic, ┌─ Webhook / SSE (real-time hints)
|
|
104
|
+
{message: "check disk"}) │
|
|
105
|
+
│ ├─ Safety Poller (60s fallback)
|
|
106
|
+
▼ │
|
|
107
|
+
AgenTrux Topic ──────────────────→ Dispatcher
|
|
108
|
+
│
|
|
109
|
+
▼
|
|
110
|
+
subagent.run() → LLM + Tools
|
|
111
|
+
│
|
|
112
|
+
▼
|
|
113
|
+
Outbox → publish → Results Topic
|
|
114
|
+
│
|
|
115
|
+
read(resultTopic) ←─────────────────────┘
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Reliability
|
|
119
|
+
|
|
120
|
+
- **Waterline cursor**: Contiguous ack with in-flight tracking
|
|
121
|
+
- **Outbox pattern**: Decouples agent completion from publish success
|
|
122
|
+
- **Two-layer dedup**: event_id (transport) + request_id (application)
|
|
123
|
+
- **Crash recovery**: In-flight events re-enqueued on restart
|
|
124
|
+
- **Published history**: Retained for idempotency across restarts
|
|
125
|
+
|
|
126
|
+
## Credentials
|
|
127
|
+
|
|
128
|
+
Stored at `~/.agentrux/credentials.json` (permissions: 0600).
|
|
129
|
+
|
|
130
|
+
| Credential | Lifetime | Storage |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| script_id + client_secret | Permanent | File |
|
|
133
|
+
| JWT (access_token) | 1 hour | Memory (auto-refresh) |
|
|
134
|
+
| Refresh token | Single-use | Memory (auto-rotate) |
|
|
135
|
+
|
|
136
|
+
## Security
|
|
137
|
+
|
|
138
|
+
- Webhook signature verification (HMAC-SHA256, constant-time compare)
|
|
139
|
+
- `reply_topic` and `agent_id` fixed in config (not from request)
|
|
140
|
+
- Prompt injection mitigation via message template wrapping
|
|
141
|
+
- `sessionKey` hashed with topic scope
|
|
142
|
+
- `execPolicy`: exec tool disabled by default, opt-in with command allowlist
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token naming correctness tests for the OpenClaw plugin.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the plugin uses the final unified naming:
|
|
5
|
+
* clientSecret, activation_code, inv_, ac_, api.agentrux.com.
|
|
6
|
+
* Ensures no legacy names (secret, token as activation param,
|
|
7
|
+
* process.env.HOME for credentials path) remain.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Token naming correctness tests for the OpenClaw plugin.
|
|
4
|
+
*
|
|
5
|
+
* Verifies that the plugin uses the final unified naming:
|
|
6
|
+
* clientSecret, activation_code, inv_, ac_, api.agentrux.com.
|
|
7
|
+
* Ensures no legacy names (secret, token as activation param,
|
|
8
|
+
* process.env.HOME for credentials path) remain.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
// We read the source file directly because the module's default export
|
|
47
|
+
// is a function that registers tools on an api object — we need to
|
|
48
|
+
// inspect both the static source text and the runtime registrations.
|
|
49
|
+
const SOURCE_PATH = path.resolve(__dirname, "..", "index.ts");
|
|
50
|
+
const source = fs.readFileSync(SOURCE_PATH, "utf-8");
|
|
51
|
+
function captureTools() {
|
|
52
|
+
const tools = [];
|
|
53
|
+
const fakeApi = {
|
|
54
|
+
registerTool(def, _opts) {
|
|
55
|
+
tools.push(def);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
// Import the default export and invoke it with our fake api.
|
|
59
|
+
// We need to isolate the module to avoid side effects from credential loading.
|
|
60
|
+
jest.isolateModules(() => {
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
62
|
+
const pluginModule = require("../index");
|
|
63
|
+
const register = pluginModule.default || pluginModule;
|
|
64
|
+
register(fakeApi);
|
|
65
|
+
});
|
|
66
|
+
return tools;
|
|
67
|
+
}
|
|
68
|
+
function findTool(tools, name) {
|
|
69
|
+
return tools.find((t) => t.name === name);
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Credential interface
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
describe("Credentials interface", () => {
|
|
75
|
+
test("has clientSecret field (not secret)", () => {
|
|
76
|
+
// Check the Credentials interface definition in source
|
|
77
|
+
expect(source).toContain("clientSecret: string");
|
|
78
|
+
// Must not have a bare "secret: string" field in the interface
|
|
79
|
+
expect(source).not.toMatch(/^\s+secret:\s+string/m);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Activate tool parameter naming
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
describe("Activate tool", () => {
|
|
86
|
+
let tools;
|
|
87
|
+
let activateTool;
|
|
88
|
+
beforeAll(() => {
|
|
89
|
+
tools = captureTools();
|
|
90
|
+
activateTool = findTool(tools, "agentrux_activate");
|
|
91
|
+
});
|
|
92
|
+
test("activate tool exists", () => {
|
|
93
|
+
expect(activateTool).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
test("parameter is activation_code (not token)", () => {
|
|
96
|
+
const props = activateTool.parameters.properties;
|
|
97
|
+
expect(props).toHaveProperty("activation_code");
|
|
98
|
+
// "token" should not be the parameter name for activation
|
|
99
|
+
expect(props).not.toHaveProperty("activation_token");
|
|
100
|
+
});
|
|
101
|
+
test("activation_code description references ac_ prefix", () => {
|
|
102
|
+
const desc = activateTool.parameters.properties.activation_code.description;
|
|
103
|
+
expect(desc).toContain("ac_");
|
|
104
|
+
expect(desc).not.toContain("atk_");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Default base URL
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
describe("Default base URL", () => {
|
|
111
|
+
test("default is https://api.agentrux.com", () => {
|
|
112
|
+
// Check the source for the default base_url assignment
|
|
113
|
+
expect(source).toContain("https://api.agentrux.com");
|
|
114
|
+
expect(source).not.toContain("example.com");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Credentials path
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
describe("Credentials path", () => {
|
|
121
|
+
test("does NOT use process.env.HOME", () => {
|
|
122
|
+
expect(source).not.toContain("process.env.HOME");
|
|
123
|
+
});
|
|
124
|
+
test("uses .agentrux directory", () => {
|
|
125
|
+
expect(source).toContain(".agentrux");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// No legacy names in source
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
describe("No legacy names in source", () => {
|
|
132
|
+
test("no old token prefixes", () => {
|
|
133
|
+
expect(source).not.toContain("atk_");
|
|
134
|
+
expect(source).not.toContain("gtk_");
|
|
135
|
+
});
|
|
136
|
+
test("no old placeholder domains", () => {
|
|
137
|
+
expect(source).not.toContain("example.com");
|
|
138
|
+
expect(source).not.toContain("your-org");
|
|
139
|
+
});
|
|
140
|
+
test("invite code uses inv_ prefix", () => {
|
|
141
|
+
expect(source).toContain("inv_");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgenTrux credential management.
|
|
3
|
+
* Credentials persisted to ~/.agentrux/credentials.json (0600).
|
|
4
|
+
*/
|
|
5
|
+
export interface Credentials {
|
|
6
|
+
base_url: string;
|
|
7
|
+
script_id: string;
|
|
8
|
+
clientSecret: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function loadCredentials(): Credentials | null;
|
|
11
|
+
export declare function saveCredentials(creds: Credentials): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AgenTrux credential management.
|
|
4
|
+
* Credentials persisted to ~/.agentrux/credentials.json (0600).
|
|
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.loadCredentials = loadCredentials;
|
|
41
|
+
exports.saveCredentials = saveCredentials;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || "~";
|
|
45
|
+
const CREDENTIALS_DIR = path.join(HOME, ".agentrux");
|
|
46
|
+
const CREDENTIALS_PATH = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
47
|
+
function loadCredentials() {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(CREDENTIALS_PATH)) {
|
|
50
|
+
return JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8"));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function saveCredentials(creds) {
|
|
57
|
+
if (!fs.existsSync(CREDENTIALS_DIR)) {
|
|
58
|
+
fs.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
59
|
+
}
|
|
60
|
+
fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), {
|
|
61
|
+
mode: 0o600,
|
|
62
|
+
});
|
|
63
|
+
}
|
package/dist/cursor.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waterline cursor with atomic persistence.
|
|
3
|
+
* Tracks: waterline (contiguous ack), inFlight, completed, processedEvents.
|
|
4
|
+
*/
|
|
5
|
+
export interface CursorState {
|
|
6
|
+
version: number;
|
|
7
|
+
waterline: number;
|
|
8
|
+
inFlight: Set<number>;
|
|
9
|
+
completed: Set<number>;
|
|
10
|
+
processedEvents: Map<string, number>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createEmptyCursor(): CursorState;
|
|
13
|
+
export declare function loadCursor(): CursorState;
|
|
14
|
+
export declare function persistCursor(state: CursorState): void;
|
|
15
|
+
export declare function markInFlight(state: CursorState, seq: number): void;
|
|
16
|
+
export declare function markCompleted(state: CursorState, seq: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* Mark as completed for waterline purposes (used for dead_letter too).
|
|
19
|
+
* Dead letter items are treated as completed to avoid head-of-line blocking.
|
|
20
|
+
*/
|
|
21
|
+
export declare function markDeadLetter(state: CursorState, seq: number): void;
|
|
22
|
+
export declare function advanceWaterline(state: CursorState): void;
|
|
23
|
+
export declare function isEventProcessed(state: CursorState, eventId: string): boolean;
|
|
24
|
+
export declare function recordProcessedEvent(state: CursorState, eventId: string): void;
|
|
25
|
+
export declare function cleanupExpiredEvents(state: CursorState): void;
|
package/dist/cursor.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Waterline cursor with atomic persistence.
|
|
4
|
+
* Tracks: waterline (contiguous ack), inFlight, completed, processedEvents.
|
|
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.createEmptyCursor = createEmptyCursor;
|
|
41
|
+
exports.loadCursor = loadCursor;
|
|
42
|
+
exports.persistCursor = persistCursor;
|
|
43
|
+
exports.markInFlight = markInFlight;
|
|
44
|
+
exports.markCompleted = markCompleted;
|
|
45
|
+
exports.markDeadLetter = markDeadLetter;
|
|
46
|
+
exports.advanceWaterline = advanceWaterline;
|
|
47
|
+
exports.isEventProcessed = isEventProcessed;
|
|
48
|
+
exports.recordProcessedEvent = recordProcessedEvent;
|
|
49
|
+
exports.cleanupExpiredEvents = cleanupExpiredEvents;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || "~";
|
|
53
|
+
const CURSOR_PATH = path.join(HOME, ".agentrux", "cursor.json");
|
|
54
|
+
const MAX_PROCESSED_EVENTS = 10_000;
|
|
55
|
+
const EVENT_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
56
|
+
function createEmptyCursor() {
|
|
57
|
+
return {
|
|
58
|
+
version: 1,
|
|
59
|
+
waterline: 0,
|
|
60
|
+
inFlight: new Set(),
|
|
61
|
+
completed: new Set(),
|
|
62
|
+
processedEvents: new Map(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function loadCursor() {
|
|
66
|
+
try {
|
|
67
|
+
if (fs.existsSync(CURSOR_PATH)) {
|
|
68
|
+
const raw = JSON.parse(fs.readFileSync(CURSOR_PATH, "utf-8"));
|
|
69
|
+
return {
|
|
70
|
+
version: raw.version || 1,
|
|
71
|
+
waterline: raw.waterline || 0,
|
|
72
|
+
inFlight: new Set(raw.inFlight || []),
|
|
73
|
+
completed: new Set(raw.completed || []),
|
|
74
|
+
processedEvents: new Map(Object.entries(raw.processedEvents || {})),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
return createEmptyCursor();
|
|
80
|
+
}
|
|
81
|
+
function persistCursor(state) {
|
|
82
|
+
const dir = path.dirname(CURSOR_PATH);
|
|
83
|
+
if (!fs.existsSync(dir)) {
|
|
84
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
85
|
+
}
|
|
86
|
+
const data = JSON.stringify({
|
|
87
|
+
version: state.version,
|
|
88
|
+
waterline: state.waterline,
|
|
89
|
+
inFlight: [...state.inFlight],
|
|
90
|
+
completed: [...state.completed],
|
|
91
|
+
processedEvents: Object.fromEntries(state.processedEvents),
|
|
92
|
+
}, null, 2);
|
|
93
|
+
// Atomic write: temp → fsync → rename
|
|
94
|
+
const tmpPath = CURSOR_PATH + ".tmp." + process.pid;
|
|
95
|
+
fs.writeFileSync(tmpPath, data, { mode: 0o600 });
|
|
96
|
+
fs.fsyncSync(fs.openSync(tmpPath, "r"));
|
|
97
|
+
fs.renameSync(tmpPath, CURSOR_PATH);
|
|
98
|
+
}
|
|
99
|
+
function markInFlight(state, seq) {
|
|
100
|
+
state.inFlight.add(seq);
|
|
101
|
+
}
|
|
102
|
+
function markCompleted(state, seq) {
|
|
103
|
+
state.inFlight.delete(seq);
|
|
104
|
+
state.completed.add(seq);
|
|
105
|
+
advanceWaterline(state);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Mark as completed for waterline purposes (used for dead_letter too).
|
|
109
|
+
* Dead letter items are treated as completed to avoid head-of-line blocking.
|
|
110
|
+
*/
|
|
111
|
+
function markDeadLetter(state, seq) {
|
|
112
|
+
markCompleted(state, seq);
|
|
113
|
+
}
|
|
114
|
+
function advanceWaterline(state) {
|
|
115
|
+
while (state.completed.has(state.waterline + 1)) {
|
|
116
|
+
state.waterline++;
|
|
117
|
+
state.completed.delete(state.waterline);
|
|
118
|
+
state.inFlight.delete(state.waterline);
|
|
119
|
+
}
|
|
120
|
+
persistCursor(state);
|
|
121
|
+
}
|
|
122
|
+
function isEventProcessed(state, eventId) {
|
|
123
|
+
return state.processedEvents.has(eventId);
|
|
124
|
+
}
|
|
125
|
+
function recordProcessedEvent(state, eventId) {
|
|
126
|
+
state.processedEvents.set(eventId, Date.now());
|
|
127
|
+
}
|
|
128
|
+
function cleanupExpiredEvents(state) {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (const [id, ts] of state.processedEvents) {
|
|
131
|
+
if (now - ts > EVENT_TTL_MS) {
|
|
132
|
+
state.processedEvents.delete(id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Cap size
|
|
136
|
+
if (state.processedEvents.size > MAX_PROCESSED_EVENTS) {
|
|
137
|
+
const entries = [...state.processedEvents.entries()]
|
|
138
|
+
.sort((a, b) => a[1] - b[1]);
|
|
139
|
+
const toRemove = entries.slice(0, entries.length - MAX_PROCESSED_EVENTS);
|
|
140
|
+
for (const [id] of toRemove) {
|
|
141
|
+
state.processedEvents.delete(id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatcher: async loop.
|
|
3
|
+
* Consumes queue → Pull → dedup → subagent.run() → outbox → waterline.
|
|
4
|
+
*/
|
|
5
|
+
import { type Credentials } from "./credentials";
|
|
6
|
+
import { type BoundedQueue } from "./queue";
|
|
7
|
+
import { type CursorState } from "./cursor";
|
|
8
|
+
export interface DispatcherConfig {
|
|
9
|
+
commandTopicId: string;
|
|
10
|
+
resultTopicId: string;
|
|
11
|
+
agentId: string;
|
|
12
|
+
maxConcurrency: number;
|
|
13
|
+
subagentTimeoutMs: number;
|
|
14
|
+
gatewayPort: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class Dispatcher {
|
|
17
|
+
private config;
|
|
18
|
+
private creds;
|
|
19
|
+
private cursor;
|
|
20
|
+
private queue;
|
|
21
|
+
private logger;
|
|
22
|
+
private running;
|
|
23
|
+
private stopped;
|
|
24
|
+
private processedRequestIds;
|
|
25
|
+
private processingSeqs;
|
|
26
|
+
private statusPublished;
|
|
27
|
+
constructor(config: DispatcherConfig, creds: Credentials, cursor: CursorState, queue: BoundedQueue, logger: {
|
|
28
|
+
info: (...a: any[]) => void;
|
|
29
|
+
error: (...a: any[]) => void;
|
|
30
|
+
warn: (...a: any[]) => void;
|
|
31
|
+
});
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
stop(): void;
|
|
34
|
+
private loop;
|
|
35
|
+
private tick;
|
|
36
|
+
private processEvent;
|
|
37
|
+
private callDispatchEndpoint;
|
|
38
|
+
}
|