@sleep2agi/commhub-server 0.5.0-preview.10 → 0.5.0-preview.12
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/package.json +1 -1
- package/src/db.ts +29 -0
- package/src/index.ts +49 -1
- package/src/tools.ts +14 -11
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -201,6 +201,26 @@ db.exec(`
|
|
|
201
201
|
CREATE INDEX IF NOT EXISTS idx_tokens_user ON api_tokens(user_id);
|
|
202
202
|
`);
|
|
203
203
|
|
|
204
|
+
// ── V3: audit_log table ──
|
|
205
|
+
db.exec(`
|
|
206
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
207
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
208
|
+
user_id TEXT,
|
|
209
|
+
username TEXT,
|
|
210
|
+
action TEXT NOT NULL,
|
|
211
|
+
target_type TEXT,
|
|
212
|
+
target_id TEXT,
|
|
213
|
+
detail TEXT,
|
|
214
|
+
ip TEXT,
|
|
215
|
+
network_id TEXT,
|
|
216
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log(created_at DESC);
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id);
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_audit_network ON audit_log(network_id);
|
|
222
|
+
`);
|
|
223
|
+
|
|
204
224
|
// ── V3: add network_id to existing tables ──
|
|
205
225
|
for (const table of ["sessions", "nodes", "tasks", "inbox", "task_events"]) {
|
|
206
226
|
try { db.exec(`ALTER TABLE ${table} ADD COLUMN network_id TEXT`); } catch {}
|
|
@@ -230,6 +250,15 @@ export function generateToken(): string {
|
|
|
230
250
|
return `atok_${crypto.randomUUID().replace(/-/g, "")}`;
|
|
231
251
|
}
|
|
232
252
|
|
|
253
|
+
export function logAudit(userId: string | null, username: string | null, action: string, targetType?: string, targetId?: string, detail?: string, ip?: string, networkId?: string) {
|
|
254
|
+
try {
|
|
255
|
+
db.run(
|
|
256
|
+
"INSERT INTO audit_log (user_id, username, action, target_type, target_id, detail, ip, network_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
|
257
|
+
[userId, username, action, targetType ?? null, targetId ?? null, detail ?? null, ip ?? null, networkId ?? null]
|
|
258
|
+
);
|
|
259
|
+
} catch {}
|
|
260
|
+
}
|
|
261
|
+
|
|
233
262
|
export function logTaskEvent(taskId: string, fromStatus: string | null, toStatus: string, actor: string, detail?: string) {
|
|
234
263
|
try {
|
|
235
264
|
db.run(
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
4
|
import { registerTools } from "./tools.js";
|
|
5
|
-
import { db, logTaskEvent } from "./db.js";
|
|
5
|
+
import { db, logTaskEvent, logAudit } from "./db.js";
|
|
6
6
|
import { createSSEStream, pushEvent, pushBroadcast, getSSEStats } from "./push.js";
|
|
7
7
|
import { register, login, resolveToken, getUserNetworks, createNetwork, type AuthUser } from "./auth.js";
|
|
8
8
|
|
|
@@ -146,6 +146,7 @@ Bun.serve({
|
|
|
146
146
|
try {
|
|
147
147
|
const body = await req.json() as any;
|
|
148
148
|
const result = register(body.username, body.password, body.email, body.display_name);
|
|
149
|
+
if (result.ok) logAudit(result.user!.user_id, body.username, "register", "user", result.user!.user_id);
|
|
149
150
|
return withCors(req, Response.json(result, { status: result.ok ? 200 : 400 }));
|
|
150
151
|
} catch (e: any) {
|
|
151
152
|
return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
|
|
@@ -156,6 +157,8 @@ Bun.serve({
|
|
|
156
157
|
try {
|
|
157
158
|
const body = await req.json() as any;
|
|
158
159
|
const result = login(body.username, body.password);
|
|
160
|
+
if (result.ok) logAudit(result.user!.user_id, body.username, "login", "user", result.user!.user_id);
|
|
161
|
+
else logAudit(null, body.username, "login_failed", "user", null, "invalid credentials");
|
|
159
162
|
return withCors(req, Response.json(result, { status: result.ok ? 200 : 401 }));
|
|
160
163
|
} catch (e: any) {
|
|
161
164
|
return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
|
|
@@ -171,6 +174,30 @@ Bun.serve({
|
|
|
171
174
|
return withCors(req, Response.json({ ok: true, user: resolved.user, networks, current_network: resolved.networkId }));
|
|
172
175
|
}
|
|
173
176
|
|
|
177
|
+
if (url.pathname === "/api/auth/me" && req.method === "PUT") {
|
|
178
|
+
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
|
|
179
|
+
if (!token) return withCors(req, Response.json({ ok: false, error: "token required" }, { status: 401 }));
|
|
180
|
+
const resolved = resolveToken(token);
|
|
181
|
+
if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
|
|
182
|
+
try {
|
|
183
|
+
const body = await req.json() as any;
|
|
184
|
+
const updates: string[] = [];
|
|
185
|
+
const params: any[] = [];
|
|
186
|
+
if (body.display_name) { updates.push(`display_name = ?${params.length + 1}`); params.push(body.display_name); }
|
|
187
|
+
if (body.email) { updates.push(`email = ?${params.length + 1}`); params.push(body.email); }
|
|
188
|
+
if (updates.length > 0) {
|
|
189
|
+
updates.push(`updated_at = datetime('now')`);
|
|
190
|
+
params.push(resolved.user.user_id);
|
|
191
|
+
db.run(`UPDATE users SET ${updates.join(", ")} WHERE user_id = ?${params.length}`, params);
|
|
192
|
+
}
|
|
193
|
+
// Re-fetch
|
|
194
|
+
const user = db.query<any, [string]>("SELECT user_id, username, display_name, email, role FROM users WHERE user_id = ?1").get(resolved.user.user_id);
|
|
195
|
+
return withCors(req, Response.json({ ok: true, user }));
|
|
196
|
+
} catch (e: any) {
|
|
197
|
+
return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
174
201
|
// ── V3: Network management ──
|
|
175
202
|
if (url.pathname === "/api/networks" && req.method === "GET") {
|
|
176
203
|
const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
|
|
@@ -390,6 +417,27 @@ Bun.serve({
|
|
|
390
417
|
}));
|
|
391
418
|
}
|
|
392
419
|
|
|
420
|
+
// ── REST: audit log (V3) ──
|
|
421
|
+
if (url.pathname === "/api/audit-log") {
|
|
422
|
+
const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
|
|
423
|
+
if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
|
|
424
|
+
const resolved = resolveToken(token);
|
|
425
|
+
if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
|
|
426
|
+
const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 200);
|
|
427
|
+
const action = url.searchParams.get("action");
|
|
428
|
+
const userId = url.searchParams.get("user_id");
|
|
429
|
+
let sql = "SELECT * FROM audit_log WHERE 1=1";
|
|
430
|
+
const params: any[] = [];
|
|
431
|
+
// Non-admin can only see own logs
|
|
432
|
+
if (resolved.user.role !== "admin") { sql += ` AND user_id = ?${params.length + 1}`; params.push(resolved.user.user_id); }
|
|
433
|
+
if (action) { sql += ` AND action = ?${params.length + 1}`; params.push(action); }
|
|
434
|
+
if (userId && resolved.user.role === "admin") { sql += ` AND user_id = ?${params.length + 1}`; params.push(userId); }
|
|
435
|
+
sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
|
|
436
|
+
params.push(limit);
|
|
437
|
+
const logs = db.query(sql).all(...params);
|
|
438
|
+
return withCors(req, Response.json({ ok: true, logs, count: logs.length }));
|
|
439
|
+
}
|
|
440
|
+
|
|
393
441
|
// ── REST: task events (V2 Sprint 2) ──
|
|
394
442
|
if (url.pathname === "/api/task_events") {
|
|
395
443
|
const taskId = url.searchParams.get("task_id");
|
package/src/tools.ts
CHANGED
|
@@ -36,8 +36,9 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
36
36
|
channels: z.string().max(2000).optional().describe("JSON array of channels"),
|
|
37
37
|
model: z.string().max(200).optional().describe("AI model name"),
|
|
38
38
|
node_name: z.string().max(200).optional().describe("Stable node display name (may differ from alias)"),
|
|
39
|
+
network_id: z.string().max(200).optional().describe("Network this agent belongs to"),
|
|
39
40
|
},
|
|
40
|
-
async ({ resume_id, alias, status, task, output, score, progress, server: srv, hostname: hn, agent: ag, project_dir: pd, version: ver, tmux_name: tmux, node_id, session_id, config_path, channels, model: mdl, node_name: nn }) => {
|
|
41
|
+
async ({ resume_id, alias, status, task, output, score, progress, server: srv, hostname: hn, agent: ag, project_dir: pd, version: ver, tmux_name: tmux, node_id, session_id, config_path, channels, model: mdl, node_name: nn, network_id: netId }) => {
|
|
41
42
|
console.log(`[${ts()}] ${alias} (${resume_id.slice(0, 8)}) → report_status: ${status}${task ? " | " + task.slice(0, 60) : ""}`);
|
|
42
43
|
const trimmedOutput = output?.slice(0, 4000);
|
|
43
44
|
|
|
@@ -45,8 +46,8 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
45
46
|
db.run("BEGIN IMMEDIATE");
|
|
46
47
|
db.run("DELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2", [alias, resume_id]);
|
|
47
48
|
db.run(
|
|
48
|
-
`INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, last_seen_at, updated_at)
|
|
49
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, datetime('now'), datetime('now'))
|
|
49
|
+
`INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, network_id, last_seen_at, updated_at)
|
|
50
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, datetime('now'), datetime('now'))
|
|
50
51
|
ON CONFLICT(resume_id) DO UPDATE SET
|
|
51
52
|
alias = COALESCE(?2, sessions.alias),
|
|
52
53
|
tmux_name = COALESCE(?3, sessions.tmux_name),
|
|
@@ -65,9 +66,10 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
65
66
|
session_id = COALESCE(?16, sessions.session_id),
|
|
66
67
|
config_path = COALESCE(?17, sessions.config_path),
|
|
67
68
|
channels = COALESCE(?18, sessions.channels),
|
|
69
|
+
network_id = COALESCE(?19, sessions.network_id),
|
|
68
70
|
last_seen_at = datetime('now'),
|
|
69
71
|
updated_at = datetime('now')`,
|
|
70
|
-
[resume_id, alias, tmux ?? null, srv ?? null, clientIP ?? null, hn ?? null, ag ?? null, pd ?? null, ver ?? null, status, task ?? null, trimmedOutput ?? null, progress ?? null, score ?? null, node_id ?? null, session_id ?? null, config_path ?? null, channels ?? null]
|
|
72
|
+
[resume_id, alias, tmux ?? null, srv ?? null, clientIP ?? null, hn ?? null, ag ?? null, pd ?? null, ver ?? null, status, task ?? null, trimmedOutput ?? null, progress ?? null, score ?? null, node_id ?? null, session_id ?? null, config_path ?? null, channels ?? null, netId ?? null]
|
|
71
73
|
);
|
|
72
74
|
db.run("COMMIT");
|
|
73
75
|
} catch (e) {
|
|
@@ -334,22 +336,23 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
334
336
|
context: z.string().max(10000).optional(),
|
|
335
337
|
from_session: z.string().max(200).optional().default("hub"),
|
|
336
338
|
ttl_seconds: z.number().min(1).max(86400).optional().describe("Task TTL in seconds (default: 3600)"),
|
|
339
|
+
network_id: z.string().max(200).optional().describe("Network scope"),
|
|
337
340
|
},
|
|
338
|
-
async ({ alias, task, priority, context, from_session, ttl_seconds }) => {
|
|
341
|
+
async ({ alias, task, priority, context, from_session, ttl_seconds, network_id: netId }) => {
|
|
339
342
|
console.log(`[${ts()}] ${from_session} → send_task → ${alias}: ${task.slice(0, 60)}${priority === "high" ? " [HIGH]" : ""}`);
|
|
340
343
|
const id = uuidv4();
|
|
341
344
|
// 事务:inbox + tasks 双写
|
|
342
345
|
try {
|
|
343
346
|
db.run("BEGIN IMMEDIATE");
|
|
344
347
|
db.run(
|
|
345
|
-
`INSERT INTO inbox (id, session_name, type, priority, content, context, from_session, requires_response)
|
|
346
|
-
VALUES (?1, ?2, 'task', ?3, ?4, ?5, ?6, 'reply')`,
|
|
347
|
-
[id, alias, priority, task, context ?? null, from_session]
|
|
348
|
+
`INSERT INTO inbox (id, session_name, type, priority, content, context, from_session, requires_response, network_id)
|
|
349
|
+
VALUES (?1, ?2, 'task', ?3, ?4, ?5, ?6, 'reply', ?7)`,
|
|
350
|
+
[id, alias, priority, task, context ?? null, from_session, netId ?? null]
|
|
348
351
|
);
|
|
349
352
|
db.run(
|
|
350
|
-
`INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at)
|
|
351
|
-
VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6))`,
|
|
352
|
-
[id, from_session, alias, priority, task, `+${ttl_seconds || 3600} seconds
|
|
353
|
+
`INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id)
|
|
354
|
+
VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7)`,
|
|
355
|
+
[id, from_session, alias, priority, task, `+${ttl_seconds || 3600} seconds`, netId ?? null]
|
|
353
356
|
);
|
|
354
357
|
db.run("COMMIT");
|
|
355
358
|
logTaskEvent(id, null, "delivered", from_session, `→ ${alias}`);
|