mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-15 02:42:45 +08:00
feat: add orchestrator execution engine, observability panel, and LSP document caching
Wire FlowExecutor into orchestrator routes for actual flow execution with
pause/resume/stop lifecycle management. Add CLI session audit system with
audit-routes backend and Observability tab in IssueHub frontend. Introduce
cli-session-mux for cross-workspace session routing and QueueSendToOrchestrator
UI component. Normalize frontend API response handling for { data: ... }
wrapper format and propagate projectPath through flow hooks.
In codex-lens, add per-server opened-document cache in StandaloneLspManager
to avoid redundant didOpen notifications (using didChange for updates), and
skip warmup delay for already-warmed LSP server instances in ChainSearchEngine.
This commit is contained in:
154
ccw/src/core/routes/audit-routes.ts
Normal file
154
ccw/src/core/routes/audit-routes.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Audit Routes Module
|
||||
* Read-only APIs for audit/observability panels.
|
||||
*
|
||||
* Currently supported:
|
||||
* - GET /api/audit/cli-sessions - Read CLI session (PTY) audit events (JSONL)
|
||||
*/
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { validatePath as validateAllowedPath } from '../../utils/path-validator.js';
|
||||
import type { CliSessionAuditEvent, CliSessionAuditEventType } from '../services/cli-session-audit.js';
|
||||
import type { RouteContext } from './types.js';
|
||||
|
||||
function clampInt(value: number, min: number, max: number): number {
|
||||
if (!Number.isFinite(value)) return min;
|
||||
return Math.min(max, Math.max(min, Math.trunc(value)));
|
||||
}
|
||||
|
||||
function parseCsvParam(value: string | null): string[] {
|
||||
if (!value) return [];
|
||||
return value
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function isCliSessionAuditEventType(value: string): value is CliSessionAuditEventType {
|
||||
return [
|
||||
'session_created',
|
||||
'session_closed',
|
||||
'session_send',
|
||||
'session_execute',
|
||||
'session_resize',
|
||||
'session_share_created',
|
||||
'session_share_revoked',
|
||||
'session_idle_reaped',
|
||||
].includes(value);
|
||||
}
|
||||
|
||||
function matchesSearch(event: CliSessionAuditEvent, qLower: string): boolean {
|
||||
if (!qLower) return true;
|
||||
|
||||
const haystacks: string[] = [];
|
||||
if (event.type) haystacks.push(event.type);
|
||||
if (event.timestamp) haystacks.push(event.timestamp);
|
||||
if (event.sessionKey) haystacks.push(event.sessionKey);
|
||||
if (event.tool) haystacks.push(event.tool);
|
||||
if (event.resumeKey) haystacks.push(event.resumeKey);
|
||||
if (event.workingDir) haystacks.push(event.workingDir);
|
||||
if (event.ip) haystacks.push(event.ip);
|
||||
if (event.userAgent) haystacks.push(event.userAgent);
|
||||
if (event.details) {
|
||||
try {
|
||||
haystacks.push(JSON.stringify(event.details));
|
||||
} catch {
|
||||
// Ignore non-serializable details
|
||||
}
|
||||
}
|
||||
|
||||
return haystacks.some((h) => h.toLowerCase().includes(qLower));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle audit routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
*/
|
||||
export async function handleAuditRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, initialPath } = ctx;
|
||||
|
||||
// GET /api/audit/cli-sessions
|
||||
if (pathname === '/api/audit/cli-sessions' && req.method === 'GET') {
|
||||
const projectPathParam = url.searchParams.get('path') || initialPath;
|
||||
|
||||
const limit = clampInt(parseInt(url.searchParams.get('limit') || '200', 10), 1, 1000);
|
||||
const offset = clampInt(parseInt(url.searchParams.get('offset') || '0', 10), 0, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const sessionKey = url.searchParams.get('sessionKey');
|
||||
const qLower = (url.searchParams.get('q') || '').trim().toLowerCase();
|
||||
|
||||
const typeFilters = parseCsvParam(url.searchParams.get('type'))
|
||||
.filter(isCliSessionAuditEventType);
|
||||
const typeFilterSet = typeFilters.length > 0 ? new Set<CliSessionAuditEventType>(typeFilters) : null;
|
||||
|
||||
try {
|
||||
const projectRoot = await validateAllowedPath(projectPathParam, {
|
||||
mustExist: true,
|
||||
allowedDirectories: [initialPath],
|
||||
});
|
||||
|
||||
const filePath = join(projectRoot, '.workflow', 'audit', 'cli-sessions.jsonl');
|
||||
if (!existsSync(filePath)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: { events: [], total: 0, limit, offset, hasMore: false },
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
const raw = await readFile(filePath, 'utf-8');
|
||||
const parsed: CliSessionAuditEvent[] = [];
|
||||
for (const line of raw.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
parsed.push(JSON.parse(trimmed) as CliSessionAuditEvent);
|
||||
} catch {
|
||||
// Skip invalid JSONL line
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = parsed.filter((ev) => {
|
||||
if (sessionKey && ev.sessionKey !== sessionKey) return false;
|
||||
if (typeFilterSet && !typeFilterSet.has(ev.type)) return false;
|
||||
if (qLower && !matchesSearch(ev, qLower)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Best-effort: file is append-only, so reverse for newest-first.
|
||||
filtered.reverse();
|
||||
|
||||
const total = filtered.length;
|
||||
const page = filtered.slice(offset, offset + limit);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: {
|
||||
events: page,
|
||||
total,
|
||||
limit,
|
||||
offset,
|
||||
hasMore: offset + limit < total,
|
||||
},
|
||||
}));
|
||||
return true;
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const lowered = message.toLowerCase();
|
||||
const status = lowered.includes('access denied') ? 403 : 400;
|
||||
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: status === 403 ? 'Access denied' : 'Invalid request',
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user