@virtue-ai/gateway-connect 0.1.1 → 0.2.0
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 +7 -4
- package/dist/index.js +19 -6
- package/dist/trajectory-plugin.d.ts +19 -0
- package/dist/trajectory-plugin.js +228 -0
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@ This will:
|
|
|
23
23
|
1. Open your browser for OAuth login (select "Authorize the platform")
|
|
24
24
|
2. Save gateway credentials to `~/.openclaw/mcp-gateway.json`
|
|
25
25
|
3. Patch `~/.openclaw/openclaw.json` to connect claude-cli to the gateway
|
|
26
|
-
4.
|
|
26
|
+
4. Install the trajectory recording plugin (sends full session trace to VirtueAI dashboard)
|
|
27
|
+
5. Verify connection and list available tools
|
|
27
28
|
|
|
28
29
|
### Step 3: Start Using
|
|
29
30
|
|
|
@@ -40,7 +41,8 @@ That's it. OpenClaw now has access to all MCP tools on the gateway (GitHub, Goog
|
|
|
40
41
|
1. **OAuth 2.0 PKCE authentication** — Registers an OAuth client, opens browser for login, exchanges authorization code for tokens
|
|
41
42
|
2. **MCP config generation** — Writes `~/.openclaw/mcp-gateway.json` with gateway URL and bearer token
|
|
42
43
|
3. **OpenClaw config patching** — Adds `--mcp-config` to the claude-cli backend args in `~/.openclaw/openclaw.json`
|
|
43
|
-
4. **
|
|
44
|
+
4. **Trajectory recording** — Installs an OpenClaw plugin (`virtueai-trajectory`) that automatically sends every agent step (user prompts, agent responses, tool calls) to the VirtueAI prompt-guard API for dashboard visibility
|
|
45
|
+
5. **Connection verification** — Calls `tools/list` on the gateway and reports available tools
|
|
44
46
|
|
|
45
47
|
## Options
|
|
46
48
|
|
|
@@ -48,8 +50,9 @@ That's it. OpenClaw now has access to all MCP tools on the gateway (GitHub, Goog
|
|
|
48
50
|
npx @virtue-ai/gateway-connect [options]
|
|
49
51
|
|
|
50
52
|
Options:
|
|
51
|
-
--gateway-url <url>
|
|
52
|
-
--
|
|
53
|
+
--gateway-url <url> Gateway URL (required)
|
|
54
|
+
--guard-uuid <uuid> Guard UUID for trajectory recording (or set VIRTUEAI_GUARD_UUID env var)
|
|
55
|
+
--help Show help message
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
## Re-authentication
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import https from 'https';
|
|
|
19
19
|
import os from 'os';
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import { URL } from 'url';
|
|
22
|
+
import { generateTrajectoryPlugin, enableTrajectoryPlugin } from './trajectory-plugin.js';
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Constants
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
@@ -222,7 +223,7 @@ async function authenticate(gatewayUrl) {
|
|
|
222
223
|
// ---------------------------------------------------------------------------
|
|
223
224
|
// Step 2: Write MCP gateway config
|
|
224
225
|
// ---------------------------------------------------------------------------
|
|
225
|
-
function writeMcpConfig(gatewayUrl, accessToken) {
|
|
226
|
+
function writeMcpConfig(gatewayUrl, accessToken, guardUuid) {
|
|
226
227
|
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
227
228
|
const config = {
|
|
228
229
|
mcpServers: {
|
|
@@ -234,6 +235,10 @@ function writeMcpConfig(gatewayUrl, accessToken) {
|
|
|
234
235
|
},
|
|
235
236
|
},
|
|
236
237
|
},
|
|
238
|
+
trajectory: {
|
|
239
|
+
gatewayUrl,
|
|
240
|
+
guardUuid: guardUuid || process.env.VIRTUEAI_GUARD_UUID || '',
|
|
241
|
+
},
|
|
237
242
|
};
|
|
238
243
|
fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
239
244
|
console.log(` Written: ${MCP_CONFIG_PATH}`);
|
|
@@ -341,18 +346,21 @@ Usage:
|
|
|
341
346
|
npx @virtue-ai/gateway-connect [options]
|
|
342
347
|
|
|
343
348
|
Options:
|
|
344
|
-
--gateway-url <url>
|
|
345
|
-
--
|
|
349
|
+
--gateway-url <url> Gateway URL (default: ${DEFAULT_GATEWAY_URL})
|
|
350
|
+
--guard-uuid <uuid> Guard UUID for trajectory recording (or set VIRTUEAI_GUARD_UUID)
|
|
351
|
+
--help Show this help message
|
|
346
352
|
|
|
347
353
|
What it does:
|
|
348
354
|
1. Opens browser for OAuth login
|
|
349
355
|
2. Saves MCP config to ~/.openclaw/mcp-gateway.json
|
|
350
356
|
3. Patches ~/.openclaw/openclaw.json to use the gateway
|
|
351
|
-
4.
|
|
357
|
+
4. Installs trajectory plugin for full session recording
|
|
358
|
+
5. Verifies connection by listing available tools
|
|
352
359
|
`);
|
|
353
360
|
process.exit(0);
|
|
354
361
|
}
|
|
355
362
|
let gatewayUrl = getArg('gateway-url') || DEFAULT_GATEWAY_URL;
|
|
363
|
+
const guardUuid = getArg('guard-uuid') || process.env.VIRTUEAI_GUARD_UUID;
|
|
356
364
|
// Strip /mcp suffix and normalize to lowercase
|
|
357
365
|
gatewayUrl = gatewayUrl.replace(/\/mcp\/?$/, '').toLowerCase();
|
|
358
366
|
console.log('\n VirtueAI Gateway Connect\n');
|
|
@@ -361,16 +369,21 @@ What it does:
|
|
|
361
369
|
const { accessToken } = await authenticate(gatewayUrl);
|
|
362
370
|
// Step 2: Write MCP config
|
|
363
371
|
console.log('\n Configuring OpenClaw...');
|
|
364
|
-
writeMcpConfig(gatewayUrl, accessToken);
|
|
372
|
+
writeMcpConfig(gatewayUrl, accessToken, guardUuid);
|
|
365
373
|
// Step 3: Patch openclaw.json
|
|
366
374
|
patchOpenClawConfig();
|
|
367
|
-
// Step 4:
|
|
375
|
+
// Step 4: Install trajectory plugin
|
|
376
|
+
console.log('\n Setting up trajectory recording...');
|
|
377
|
+
generateTrajectoryPlugin(guardUuid);
|
|
378
|
+
enableTrajectoryPlugin();
|
|
379
|
+
// Step 5: Verify
|
|
368
380
|
console.log('');
|
|
369
381
|
const toolCount = await verifyConnection(gatewayUrl, accessToken);
|
|
370
382
|
// Done
|
|
371
383
|
console.log(`
|
|
372
384
|
Done! OpenClaw is now connected to VirtueAI MCP gateway.
|
|
373
385
|
${toolCount} tools available across the gateway.
|
|
386
|
+
Trajectory recording enabled (via virtueai-trajectory plugin).
|
|
374
387
|
|
|
375
388
|
Config files:
|
|
376
389
|
${MCP_CONFIG_PATH}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trajectory Plugin Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates and installs an OpenClaw plugin that hooks into the agent lifecycle
|
|
5
|
+
* to send every trajectory step to the VirtueAI gateway's prompt-guard API.
|
|
6
|
+
*
|
|
7
|
+
* Hooks used (all fire-and-forget, never block the agent):
|
|
8
|
+
* - llm_input: captures user prompt → role "user"
|
|
9
|
+
* - llm_output: captures agent reply → role "agent"
|
|
10
|
+
* - after_tool_call: captures tool results → role "agent"
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Generate the trajectory plugin files on disk.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateTrajectoryPlugin(guardUuid?: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Enable the trajectory plugin in openclaw.json.
|
|
18
|
+
*/
|
|
19
|
+
export declare function enableTrajectoryPlugin(): void;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trajectory Plugin Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates and installs an OpenClaw plugin that hooks into the agent lifecycle
|
|
5
|
+
* to send every trajectory step to the VirtueAI gateway's prompt-guard API.
|
|
6
|
+
*
|
|
7
|
+
* Hooks used (all fire-and-forget, never block the agent):
|
|
8
|
+
* - llm_input: captures user prompt → role "user"
|
|
9
|
+
* - llm_output: captures agent reply → role "agent"
|
|
10
|
+
* - after_tool_call: captures tool results → role "agent"
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
19
|
+
const OPENCLAW_CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
20
|
+
const MCP_CONFIG_PATH = path.join(OPENCLAW_DIR, 'mcp-gateway.json');
|
|
21
|
+
const PLUGIN_ID = 'virtueai-trajectory';
|
|
22
|
+
const PLUGIN_DIR = path.join(OPENCLAW_DIR, 'extensions', PLUGIN_ID);
|
|
23
|
+
const DEFAULT_GUARD_UUID = '3a2389709528a539a12ba6239e402ef159ecffa88f99af6e13236e69dd9bb2e5';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Plugin source code (generated as a string, written to disk)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function buildPluginSource() {
|
|
28
|
+
return `\
|
|
29
|
+
import { readFileSync } from "fs";
|
|
30
|
+
import { join } from "path";
|
|
31
|
+
import { homedir } from "os";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* VirtueAI Trajectory Plugin
|
|
35
|
+
*
|
|
36
|
+
* Sends every agent interaction step to the VirtueAI gateway prompt-guard API
|
|
37
|
+
* so the full trajectory is visible in the dashboard.
|
|
38
|
+
*
|
|
39
|
+
* All hooks are fire-and-forget — errors are logged, never thrown.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const MCP_CONFIG_PATH = join(homedir(), ".openclaw", "mcp-gateway.json");
|
|
43
|
+
const DEFAULT_GUARD_UUID =
|
|
44
|
+
"${DEFAULT_GUARD_UUID}";
|
|
45
|
+
|
|
46
|
+
function loadConfig() {
|
|
47
|
+
try {
|
|
48
|
+
const raw = readFileSync(MCP_CONFIG_PATH, "utf-8");
|
|
49
|
+
const cfg = JSON.parse(raw);
|
|
50
|
+
const virtueai = cfg.mcpServers?.virtueai;
|
|
51
|
+
if (!virtueai) return null;
|
|
52
|
+
|
|
53
|
+
const authHeader = virtueai.headers?.Authorization ?? "";
|
|
54
|
+
const token = authHeader.replace(/^Bearer\\s+/i, "");
|
|
55
|
+
const gatewayUrl = (virtueai.url ?? "").replace(/\\/mcp\\/?$/, "");
|
|
56
|
+
|
|
57
|
+
const guardUuid =
|
|
58
|
+
cfg.trajectory?.guardUuid ||
|
|
59
|
+
process.env.VIRTUEAI_GUARD_UUID ||
|
|
60
|
+
DEFAULT_GUARD_UUID;
|
|
61
|
+
|
|
62
|
+
if (!gatewayUrl || !token) return null;
|
|
63
|
+
return { gatewayUrl, token, guardUuid };
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function truncate(s, max = 2000) {
|
|
70
|
+
if (typeof s !== "string") {
|
|
71
|
+
try { s = JSON.stringify(s); } catch { s = String(s); }
|
|
72
|
+
}
|
|
73
|
+
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const plugin = {
|
|
77
|
+
id: "${PLUGIN_ID}",
|
|
78
|
+
name: "VirtueAI Trajectory",
|
|
79
|
+
description: "Sends agent trajectory steps to VirtueAI gateway for dashboard visibility",
|
|
80
|
+
|
|
81
|
+
register(api) {
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
if (!config) {
|
|
84
|
+
api.logger.warn("[virtueai-trajectory] No valid config found in " + MCP_CONFIG_PATH + ", plugin disabled");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let gatewaySessionId = null;
|
|
89
|
+
const endpoint = config.gatewayUrl + "/api/prompt-guard/topic_guard";
|
|
90
|
+
|
|
91
|
+
async function sendStep(role, content) {
|
|
92
|
+
const body = {
|
|
93
|
+
user_prompt: truncate(content),
|
|
94
|
+
guard_uuid: config.guardUuid,
|
|
95
|
+
role,
|
|
96
|
+
};
|
|
97
|
+
if (gatewaySessionId) {
|
|
98
|
+
body.session_id = gatewaySessionId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(endpoint, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
Authorization: "Bearer " + config.token,
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
});
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
|
|
112
|
+
if (data?.session_id && !gatewaySessionId) {
|
|
113
|
+
gatewaySessionId = data.session_id;
|
|
114
|
+
api.logger.info("[virtueai-trajectory] Gateway session: " + gatewaySessionId);
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
api.logger.warn("[virtueai-trajectory] Failed to send step: " + (err?.message ?? err));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Hook: user prompt sent to LLM
|
|
122
|
+
api.on("llm_input", (event) => {
|
|
123
|
+
if (event.prompt) {
|
|
124
|
+
sendStep("user", event.prompt);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Hook: LLM response received
|
|
129
|
+
api.on("llm_output", (event) => {
|
|
130
|
+
const text = (event.assistantTexts ?? []).join("\\n").trim();
|
|
131
|
+
if (text) {
|
|
132
|
+
sendStep("agent", text);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Hook: tool call completed
|
|
137
|
+
api.on("after_tool_call", (event) => {
|
|
138
|
+
const params = event.params
|
|
139
|
+
? Object.entries(event.params)
|
|
140
|
+
.map(([k, v]) => k + "=" + JSON.stringify(v))
|
|
141
|
+
.join(", ")
|
|
142
|
+
: "";
|
|
143
|
+
const callStr = event.toolName + "(" + params + ")";
|
|
144
|
+
const resultStr = event.result != null ? truncate(event.result, 500) : (event.error ?? "no result");
|
|
145
|
+
sendStep("agent", callStr + " → " + resultStr);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
api.logger.info("[virtueai-trajectory] Plugin registered, sending to " + config.gatewayUrl);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export default plugin;
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Public API
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Generate the trajectory plugin files on disk.
|
|
160
|
+
*/
|
|
161
|
+
export function generateTrajectoryPlugin(guardUuid) {
|
|
162
|
+
fs.mkdirSync(PLUGIN_DIR, { recursive: true });
|
|
163
|
+
// package.json
|
|
164
|
+
const pkg = {
|
|
165
|
+
name: '@virtue-ai/trajectory',
|
|
166
|
+
version: '1.0.0',
|
|
167
|
+
type: 'module',
|
|
168
|
+
openclaw: {
|
|
169
|
+
extensions: ['./index.ts'],
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
fs.writeFileSync(path.join(PLUGIN_DIR, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
|
|
173
|
+
// index.ts (the actual plugin)
|
|
174
|
+
fs.writeFileSync(path.join(PLUGIN_DIR, 'index.ts'), buildPluginSource());
|
|
175
|
+
// Persist guard UUID in mcp-gateway.json trajectory section
|
|
176
|
+
if (guardUuid) {
|
|
177
|
+
try {
|
|
178
|
+
const raw = fs.readFileSync(MCP_CONFIG_PATH, 'utf-8');
|
|
179
|
+
const cfg = JSON.parse(raw);
|
|
180
|
+
if (!cfg.trajectory)
|
|
181
|
+
cfg.trajectory = {};
|
|
182
|
+
cfg.trajectory.guardUuid = guardUuid;
|
|
183
|
+
fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(cfg, null, 2) + '\n');
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Config may not exist yet — will be written by writeMcpConfig
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
console.log(` Generated trajectory plugin: ${PLUGIN_DIR}`);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Enable the trajectory plugin in openclaw.json.
|
|
193
|
+
*/
|
|
194
|
+
export function enableTrajectoryPlugin() {
|
|
195
|
+
let config = {};
|
|
196
|
+
if (fs.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
197
|
+
try {
|
|
198
|
+
config = JSON.parse(fs.readFileSync(OPENCLAW_CONFIG_PATH, 'utf-8'));
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
config = {};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// plugins.entries
|
|
205
|
+
if (!config.plugins)
|
|
206
|
+
config.plugins = {};
|
|
207
|
+
if (!config.plugins.entries)
|
|
208
|
+
config.plugins.entries = {};
|
|
209
|
+
config.plugins.entries[PLUGIN_ID] = { enabled: true };
|
|
210
|
+
// plugins.allow
|
|
211
|
+
if (!config.plugins.allow)
|
|
212
|
+
config.plugins.allow = [];
|
|
213
|
+
if (!config.plugins.allow.includes(PLUGIN_ID)) {
|
|
214
|
+
config.plugins.allow.push(PLUGIN_ID);
|
|
215
|
+
}
|
|
216
|
+
// plugins.installs (so OpenClaw knows where the plugin lives)
|
|
217
|
+
if (!config.plugins.installs)
|
|
218
|
+
config.plugins.installs = {};
|
|
219
|
+
config.plugins.installs[PLUGIN_ID] = {
|
|
220
|
+
source: 'path',
|
|
221
|
+
sourcePath: PLUGIN_DIR,
|
|
222
|
+
installPath: PLUGIN_DIR,
|
|
223
|
+
version: '1.0.0',
|
|
224
|
+
installedAt: new Date().toISOString(),
|
|
225
|
+
};
|
|
226
|
+
fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
227
|
+
console.log(` Enabled trajectory plugin in: ${OPENCLAW_CONFIG_PATH}`);
|
|
228
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@virtue-ai/gateway-connect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "One-command setup to connect OpenClaw to VirtueAI MCP gateway",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"files": [
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
7
9
|
"main": "dist/index.js",
|
|
8
10
|
"bin": {
|
|
9
|
-
"gateway-connect": "
|
|
11
|
+
"gateway-connect": "dist/index.js"
|
|
10
12
|
},
|
|
11
13
|
"scripts": {
|
|
12
14
|
"build": "tsc",
|
|
13
15
|
"start": "node dist/index.js",
|
|
14
16
|
"dev": "tsx src/index.ts"
|
|
15
17
|
},
|
|
16
|
-
"keywords": [
|
|
18
|
+
"keywords": [
|
|
19
|
+
"openclaw",
|
|
20
|
+
"mcp",
|
|
21
|
+
"gateway",
|
|
22
|
+
"virtueai"
|
|
23
|
+
],
|
|
17
24
|
"license": "MIT",
|
|
18
25
|
"devDependencies": {
|
|
19
26
|
"@types/node": "^22.10.0",
|