Wire validated core bridge, migrate daemon to it, verify live

This commit is contained in:
Fisto
2026-09-11 14:15:03 +02:00
parent e62d3007d8
commit 7d6650ce4e
27 changed files with 754 additions and 266 deletions
+11 -7
View File
@@ -16,19 +16,22 @@ pnpm install
| --- | --- |
| `pnpm poc:qr` | QR login. Writes the token file. |
| `pnpm control:bridge` | Start the daemon. It listens on `http://127.0.0.1:8788/mcp`. |
| `pnpm control:mock` | Start the mock core. Test the daemon without Electron. |
| `pnpm test:trigger` | Inject a trigger event into the mock core. |
| `pnpm poc:dm` | Send one test DM to zzib. |
| `pnpm test:comm` | Run the unit tests for the pure logic. |
| `pnpm cdp:probe` | Check CDP access and event delivery. |
| `pnpm cdp:dispatch` | Run one CDP expression in the client. |
| `pnpm check` | Type check the daemon code. |
## Transport
The daemon uses Chrome DevTools Protocol at this time. Start the client with
`--remote-debugging-port=9222`. The daemon connects to the Discord page and runs the actions in the
real client.
The primary transport is the validated core bridge at `vesktop-puppet.sock`. The daemon sends
validated actions and receives normalized events. See
[../docs/core-bridge.md](../docs/core-bridge.md).
The target transport is the validated core bridge. See
[../docs/core-bridge.md](../docs/core-bridge.md). The daemon logic is the same on both transports.
The CDP transport is a development fallback. It needs no Vesktop patch, and it has no
core-side guardrails. Use it for debugging only.
## Pacing
@@ -44,8 +47,9 @@ The only permitted live-send target is user zzib. See
## Verified
- QR login works end-to-end. The token came from a phone scan.
- The CDP transport works. `get_state` returned the real account and 7 guilds. `send_dm` delivered
a real DM to zzib.
- The core bridge works live. `get_state` returned the real account and 7 guilds. `send_dm` reached
zzib. A synthetic owner message reached the daemon log with the command parsed.
- The mock path works. The daemon handles state, sends, commands, and the two log buckets.
- Pacing stayed inside the 500 to 2800 ms window.
## Findings
+4 -1
View File
@@ -10,7 +10,10 @@
"test:comm": "tsx src/test-communication.ts",
"cdp:probe": "tsx src/cdp-probe.ts",
"control:bridge": "tsx src/control-bridge.ts",
"check": "tsc --noEmit"
"check": "tsc --noEmit",
"control:mock": "tsx src/mock-core.ts",
"test:trigger": "tsx src/test-trigger.ts",
"cdp:dispatch": "tsx src/cdp-dispatch.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
+55
View File
@@ -0,0 +1,55 @@
/*
* Veskslop development tool — run one expression in the real client over CDP.
*
* Use it with the client started on --remote-debugging-port=9222. The main use
* is to dispatch synthetic events for end-to-end tests.
*
* Usage:
* CDP_EXPRESSION='Vencord.Webpack.Common.FluxDispatcher.dispatch({...})' pnpm cdp:dispatch
*/
import WebSocket from "ws";
const expression = process.env.CDP_EXPRESSION;
if (!expression) {
console.error("CDP_EXPRESSION is required");
process.exit(1);
}
const cdpUrl = process.env.CDP_URL ?? "http://127.0.0.1:9222";
const targets = (await (await fetch(`${cdpUrl}/json`)).json()) as any[];
const page = targets.find(target => target.type === "page" && target.url.includes("discord.com"));
if (!page) {
console.error("No Discord page target");
process.exit(1);
}
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise<void>((resolve, reject) => {
ws.once("open", () => resolve());
ws.once("error", reject);
});
const response = await new Promise<any>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("CDP evaluate timed out")), 10_000);
ws.on("message", raw => {
const message = JSON.parse(raw.toString());
if (message.id !== 1) return;
clearTimeout(timer);
resolve(message);
});
ws.send(
JSON.stringify({
id: 1,
method: "Runtime.evaluate",
params: { expression, returnByValue: true, awaitPromise: true }
})
);
});
console.log(JSON.stringify(response.result ?? response.error));
ws.close();
+27 -27
View File
@@ -1,13 +1,12 @@
/*
* Veskslop daemon — MCP control bridge over Chrome DevTools Protocol.
* Veskslop daemon — MCP control bridge over the validated core bridge.
*
* Talks to an unmodified Vesktop through --remote-debugging-port, evaluates
* actions against the real client, receives events through a CDP binding, and
* runs the trigger/whitelist/log logic locally.
* The daemon holds all logic. It sends validated actions to Vesktop and
* receives normalized events. See docs/core-bridge.md.
*
* Usage:
* VESKSLOP_TOKEN_FILE=... pnpm control:bridge
* CDP_URL=http://127.0.0.1:9222 MCP_PORT=8788 pnpm control:bridge
* pnpm control:bridge
* VESKSLOP_SOCKET=/path/to/vesktop-puppet.sock MCP_PORT=8788 pnpm control:bridge
*/
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
@@ -15,45 +14,43 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import * as z from "zod/v4";
import { CdpClient } from "./cdp-client.js";
import { Discord } from "./discord.js";
import { CoreClient, coreSocketPath } from "./core-client.js";
import { dedupe, getQueryCount, paced, sleep } from "./pacing.js";
import { Puppet } from "./puppet.js";
const HOST = process.env.MCP_HOST ?? "127.0.0.1";
const PORT = Number(process.env.MCP_PORT ?? 8788);
let discord: Discord | undefined;
let core: CoreClient | undefined;
let puppet: Puppet | undefined;
let cdpConnected = false;
let coreConnected = false;
async function connectLoop() {
for (;;) {
try {
const cdp = await CdpClient.connect();
const client = await CoreClient.connect();
discord = new Discord(cdp);
puppet = new Puppet(discord);
core = client;
puppet = new Puppet(client);
await discord.installEventBridge();
cdp.onEvent(event => {
client.onEvent(event => {
puppet!.handleEvent(event).catch(error => console.error("[daemon] event error:", error));
});
cdpConnected = true;
console.log("[daemon] connected to Vesktop via CDP");
coreConnected = true;
console.log(`[daemon] connected to the core bridge at ${coreSocketPath}`);
return;
} catch (error) {
console.warn(`[daemon] waiting for Vesktop CDP: ${(error as Error).message}`);
console.warn(`[daemon] waiting for the core bridge: ${(error as Error).message}`);
await sleep(2000);
}
}
}
function requireDiscord(): Discord {
if (!cdpConnected || !discord) throw new Error("Not connected to Vesktop (run it with --remote-debugging-port)");
return discord;
function requireCore(): CoreClient {
if (!coreConnected || !core) throw new Error("Not connected to Vesktop (is the core bridge running?)");
return core;
}
function requirePuppet(): Puppet {
@@ -73,7 +70,7 @@ function buildServer(): McpServer {
{
description: "Get the logged-in user, guilds and channels. Cache-only, no Discord queries."
},
async () => jsonText(await requireDiscord().getState())
async () => jsonText(await requireCore().getState())
);
server.registerTool(
@@ -86,7 +83,7 @@ function buildServer(): McpServer {
}
},
async ({ channelId, content }) => {
await paced(() => requireDiscord().sendMessage(channelId, content));
await paced(() => requireCore().sendMessage(channelId, content));
return jsonText({ channelId, sent: true, queriesUsed: getQueryCount() });
}
);
@@ -101,7 +98,7 @@ function buildServer(): McpServer {
}
},
async ({ userId, content }) => {
const result = await paced(() => requireDiscord().sendDm(userId, content));
const result = await paced(() => requireCore().sendDm(userId, content));
return jsonText({ ...result, userId, queriesUsed: getQueryCount() });
}
);
@@ -117,7 +114,7 @@ function buildServer(): McpServer {
},
async ({ channelId, limit }) => {
const result = await dedupe<{ cached: number; messages: unknown[] }>(`read:${channelId}:${limit}`, () =>
paced(() => requireDiscord().readMessages(channelId, limit))
paced(() => requireCore().readMessages(channelId, limit))
);
return jsonText({ ...result, channelId, queriesUsed: getQueryCount() });
}
@@ -203,7 +200,7 @@ app.delete("/mcp", (_req, res) => {
});
app.get("/health", (_req, res) => {
res.json({ ok: true, cdpConnected, ownerId: puppet?.owner, queries: getQueryCount() });
res.json({ ok: true, coreConnected, socketPath: coreSocketPath, ownerId: puppet?.owner, queries: getQueryCount() });
});
app.listen(PORT, HOST, (error?: Error) => {
@@ -214,4 +211,7 @@ app.listen(PORT, HOST, (error?: Error) => {
console.log(`[daemon] MCP listening on http://${HOST}:${PORT}/mcp`);
});
process.on("SIGINT", () => process.exit(0));
process.on("SIGINT", () => {
core?.close();
process.exit(0);
});
+137
View File
@@ -0,0 +1,137 @@
/*
* Veskslop daemon — client for the validated core bridge.
*
* Speaks newline-delimited JSON over the core socket. A request gets one
* response line. Events arrive as { type: "event" } lines after subscribe.
*/
import net from "node:net";
export const coreSocketPath =
process.env.VESKSLOP_SOCKET ??
(process.env.XDG_RUNTIME_DIR ? `${process.env.XDG_RUNTIME_DIR}/vesktop-puppet.sock` : "/tmp/vesktop-puppet.sock");
export class CoreError extends Error {
constructor(
public readonly code: string,
message: string
) {
super(message);
}
}
export type CoreEventListener = (event: any) => void;
interface Pending {
resolve: (value: any) => void;
reject: (error: Error) => void;
}
export class CoreClient {
private nextId = 1;
private readonly pending = new Map<number, Pending>();
private readonly listeners = new Set<CoreEventListener>();
private constructor(private readonly socket: net.Socket) {
socket.setEncoding("utf-8");
let buffer = "";
socket.on("data", (chunk: string) => {
buffer += chunk;
let newline: number;
while ((newline = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, newline);
buffer = buffer.slice(newline + 1);
if (line) this.onLine(line);
}
});
socket.on("close", () => {
for (const request of this.pending.values()) {
request.reject(new CoreError("DISCONNECTED", "core disconnected"));
}
this.pending.clear();
});
socket.on("error", () => socket.destroy());
}
static connect(path = coreSocketPath): Promise<CoreClient> {
return new Promise((resolve, reject) => {
const socket = net.connect(path);
const client = new CoreClient(socket);
socket.once("connect", async () => {
try {
await client.request("events.subscribe");
resolve(client);
} catch (error) {
reject(error);
}
});
socket.once("error", reject);
});
}
private onLine(line: string) {
let message: any;
try {
message = JSON.parse(line);
} catch {
return;
}
if (message.type === "event") {
for (const listener of this.listeners) listener(message.event);
return;
}
const request = this.pending.get(message.id);
if (!request) return;
this.pending.delete(message.id);
if (message.ok) request.resolve(message.result);
else request.reject(new CoreError(message.error?.code ?? "UNKNOWN", message.error?.message ?? "core error"));
}
request<T = any>(action: string, params?: Record<string, unknown>): Promise<T> {
const id = this.nextId++;
return new Promise<T>((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.socket.write(JSON.stringify({ id, action, params }) + "\n");
});
}
onEvent(listener: CoreEventListener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
/* Convenience actions */
async getState() {
return this.request("state.get");
}
async getSelfId(): Promise<string | undefined> {
return (await this.getState())?.user?.id;
}
async sendMessage(channelId: string, content: string) {
await this.request("message.send", { channelId, content });
}
async sendDm(userId: string, content: string) {
return this.request("dm.send", { userId, content });
}
async readMessages(channelId: string, limit: number) {
return this.request("message.read", { channelId, limit });
}
close() {
this.socket.destroy();
}
}
-159
View File
@@ -1,159 +0,0 @@
/*
* Veskslop — Discord actions executed inside the real client via CDP.
*
* Every expression runs in the Discord page against Vencord's exposed globals,
* so all traffic is performed by the official client.
*/
import { CdpClient } from "./cdp-client.js";
export interface DiscordUser {
id: string;
username: string;
}
export interface DiscordChannel {
id: string;
name: string;
type: number;
}
export interface DiscordGuild {
id: string;
name: string;
channels: DiscordChannel[];
}
export interface DiscordState {
user: DiscordUser;
guilds: DiscordGuild[];
}
const stringify = (value: unknown) => JSON.stringify(value);
export class Discord {
constructor(private readonly cdp: CdpClient) {}
/** Install the page-side event pusher (idempotent across reconnects). */
async installEventBridge(): Promise<void> {
await this.cdp.evaluate(`
(() => {
if (globalThis.__veskslopBridgeInstalled) return "already";
globalThis.__veskslopBridgeInstalled = true;
const { FluxDispatcher, ChannelStore } = Vencord.Webpack.Common;
FluxDispatcher.subscribe("MESSAGE_CREATE", event => {
try {
const message = event.message;
if (!message?.author) return;
veskSlopEmit(JSON.stringify({
type: "MESSAGE_CREATE",
id: message.id,
channelId: event.channelId,
guildId: event.guildId ?? null,
channelType: ChannelStore.getChannel(event.channelId)?.type ?? null,
author: {
id: message.author.id,
username: message.author.username,
bot: !!message.author.bot
},
content: message.content ?? "",
mentions: [
...(message.mentions ?? []).map(user => user.id),
...(message.mentionIds ?? [])
],
timestamp: message.timestamp ?? new Date().toISOString()
}));
} catch {
// never break the client because of the bridge
}
});
return "installed";
})()
`);
}
async getState(): Promise<DiscordState | null> {
return this.cdp.evaluate<DiscordState | null>(`
(() => {
const { UserStore, GuildStore, ChannelStore } = Vencord.Webpack.Common;
const user = UserStore.getCurrentUser();
if (!user) return null;
return {
user: { id: user.id, username: user.username },
guilds: GuildStore.getGuildsArray().map(guild => ({
id: guild.id,
name: guild.name,
channels: Object.values(ChannelStore.getMutableBasicGuildChannelsForGuild(guild.id)).map(channel => ({
id: channel.id,
name: channel.name,
type: channel.type
}))
}))
};
})()
`);
}
async getSelfId(): Promise<string | undefined> {
return (await this.getState())?.user.id;
}
async sendMessage(channelId: string, content: string): Promise<void> {
await this.cdp.evaluate(`
Vencord.Util.sendMessage(${stringify(channelId)}, { content: ${stringify(content)} }, false)
`);
}
async sendDm(userId: string, content: string): Promise<{ sent: boolean; channelId?: string; error?: string }> {
return this.cdp.evaluate(`
(async () => {
const { ChannelStore } = Vencord.Webpack.Common;
const { openPrivateChannel, sendMessage } = Vencord.Util;
let channelId = ChannelStore.getDMFromUserId(${stringify(userId)});
if (!channelId) {
openPrivateChannel(${stringify(userId)});
for (let i = 0; i < 20 && !channelId; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
channelId = ChannelStore.getDMFromUserId(${stringify(userId)});
}
}
if (!channelId) return { sent: false, error: "No DM channel yet" };
await sendMessage(channelId, { content: ${stringify(content)} }, false);
return { sent: true, channelId };
})()
`);
}
async readMessages(channelId: string, limit: number) {
return this.cdp.evaluate(`
(() => {
const messages = Vencord.Webpack.Common.MessageStore.getMessages(${stringify(channelId)})?._array ?? [];
return {
cached: messages.length,
messages: messages.slice(${-Math.abs(limit)}).map(message => ({
id: message.id,
author: { id: message.author.id, username: message.author.username },
content: message.content,
timestamp: message.timestamp
}))
};
})()
`);
}
/** Seed the auth token and reload the client. Used for fresh profiles. */
async seedToken(token: string): Promise<void> {
await this.cdp.evaluate(`
localStorage.setItem("token", ${stringify(JSON.stringify(token))});
location.reload();
"reloading"
`);
}
}
+109
View File
@@ -0,0 +1,109 @@
/*
* Veskslop — mock core bridge for daemon tests.
*
* Implements the validated protocol with fake data. Use it to test the daemon
* without Electron or Discord. The `debug.emit` action injects an event into
* all subscribers and exists only in the mock.
*
* Usage:
* VESKSLOP_SOCKET=/tmp/vesktop-puppet-mock.sock pnpm control:mock
*/
import { rmSync } from "node:fs";
import { createServer, Socket } from "node:net";
const socketPath = process.env.VESKSLOP_SOCKET ?? "/tmp/vesktop-puppet-mock.sock";
const subscribers = new Set<Socket>();
const FAKE_GUILDS = [
{
id: "100000000000000001",
name: "Mock Guild",
channels: [
{ id: "200000000000000001", name: "general", type: 0 },
{ id: "200000000000000002", name: "voice", type: 2 }
]
}
];
const FAKE_MESSAGES = [
{
id: "300000000000000001",
author: { id: "1", username: "mockuser" },
content: "hello from the mock",
timestamp: new Date().toISOString()
}
];
function handle(action: string | undefined, params: any) {
switch (action) {
case "ping":
return "pong";
case "state.get":
return { user: { id: "999", username: "mockuser" }, guilds: FAKE_GUILDS };
case "message.send":
return { channelId: params?.channelId, sent: true };
case "message.read":
return { cached: FAKE_MESSAGES.length, messages: FAKE_MESSAGES };
case "dm.send":
return { userId: params?.userId, channelId: `dm-${params?.userId}`, sent: true };
case "debug.emit": {
const line = JSON.stringify({ type: "event", event: params?.event }) + "\n";
for (const subscriber of subscribers) subscriber.write(line);
return { emitted: true };
}
default:
throw new Error(`Unknown action: ${action}`);
}
}
rmSync(socketPath, { force: true });
const server = createServer(socket => {
socket.setEncoding("utf-8");
let buffer = "";
socket.on("data", (chunk: string) => {
buffer += chunk;
let newline: number;
while ((newline = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, newline).trim();
buffer = buffer.slice(newline + 1);
if (!line) continue;
try {
const request = JSON.parse(line);
if (request.action === "events.subscribe") {
subscribers.add(socket);
socket.write(JSON.stringify({ id: request.id, ok: true, result: { subscribed: true } }) + "\n");
console.log(`[mock] ${new Date().toISOString()} events.subscribe`);
continue;
}
const result = handle(request.action, request.params);
console.log(`[mock] ${new Date().toISOString()} ${request.action}`);
socket.write(JSON.stringify({ id: request.id, ok: true, result }) + "\n");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`[mock] ${new Date().toISOString()} ERROR ${message}`);
socket.write(JSON.stringify({ ok: false, error: { code: "UNKNOWN_ACTION", message } }) + "\n");
}
}
});
socket.on("error", () => subscribers.delete(socket));
socket.on("close", () => subscribers.delete(socket));
});
server.listen(socketPath, () => {
console.log(`[mock] listening on ${socketPath}`);
});
process.on("SIGINT", () => {
server.close();
rmSync(socketPath, { force: true });
process.exit(0);
});
+7 -2
View File
@@ -15,7 +15,12 @@ import {
type ParsedCommand,
type PuppetMessage
} from "./communication.js";
import type { Discord } from "./discord.js";
/** The subset of the transport that the puppet logic needs. */
export interface PuppetTransport {
getSelfId(): Promise<string | undefined>;
sendMessage(channelId: string, content: string): Promise<void>;
}
const DM_CHANNEL = 1;
const GROUP_DM_CHANNEL = 3;
@@ -36,7 +41,7 @@ export class Puppet {
private whitelistIds: string[] = [];
private selfId?: string;
constructor(private readonly discord: Discord) {
constructor(private readonly discord: PuppetTransport) {
try {
const state = JSON.parse(readFileSync(STATE_FILE, "utf8")) as PersistedState;
if (state.ownerId) this.ownerId = state.ownerId;
+6 -9
View File
@@ -1,5 +1,5 @@
/*
* Veskslop — live DM send test through a running client.
* Veskslop — live DM send test through the core bridge.
*
* Sanctioned test target: user zzib (defaults to their user ID).
*
@@ -8,8 +8,7 @@
* TEST_DM_USER_ID=... TEST_DM_CONTENT="hello" pnpm poc:dm
*/
import { CdpClient } from "./cdp-client.js";
import { Discord } from "./discord.js";
import { CoreClient } from "./core-client.js";
/** zzib — the only sanctioned live-send target. */
const DEFAULT_TEST_DM_USER_ID = "177117296140091393";
@@ -17,10 +16,8 @@ const DEFAULT_TEST_DM_USER_ID = "177117296140091393";
const userId = process.env.TEST_DM_USER_ID ?? DEFAULT_TEST_DM_USER_ID;
const content = process.env.TEST_DM_CONTENT ?? "veskslop test dm";
const cdp = await CdpClient.connect();
const discord = new Discord(cdp);
const core = await CoreClient.connect();
console.log("[dm] connected to the core bridge");
console.log("[dm]", await core.sendDm(userId, content));
console.log("[dm] connected via CDP");
console.log("[dm]", await discord.sendDm(userId, content));
cdp.close();
core.close();
+42
View File
@@ -0,0 +1,42 @@
/*
* Veskslop — inject a synthetic trigger event into the mock core.
* Use it to test the daemon trigger path without Discord.
*
* Usage:
* VESKSLOP_SOCKET=/tmp/vesktop-puppet-mock.sock pnpm test:trigger
* TEST_TRIGGER_CONTENT="whitelist add @123" pnpm test:trigger
*/
import net from "node:net";
import { coreSocketPath } from "./core-client.js";
const authorId = process.env.TEST_TRIGGER_AUTHOR ?? "177117296140091393";
const content = process.env.TEST_TRIGGER_CONTENT ?? "whitelist list";
const event = {
type: "MESSAGE_CREATE",
id: `30000000000000${Date.now() % 10000}`,
channelId: "805255536223715370",
guildId: null,
channelType: 1,
author: { id: authorId, username: "zzib", bot: false },
content,
mentions: [],
timestamp: new Date().toISOString()
};
await new Promise<void>((resolve, reject) => {
const socket = net.connect(coreSocketPath);
socket.setEncoding("utf-8");
socket.once("connect", () =>
socket.write(JSON.stringify({ id: 1, action: "debug.emit", params: { event } }) + "\n")
);
socket.once("data", data => {
console.log("[trigger]", data.toString().trim());
socket.destroy();
resolve();
});
socket.once("error", reject);
});