From 5a95ba0636f16b713b40f2af6a387414eb9405b4 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 9 May 2026 03:22:04 +0800 Subject: [PATCH] test(enterprise): verify runtime isolation checklist --- backend/package.json | 2 +- ...nterpriseRuntimeIsolationChecklist.test.ts | 84 ++++ .../enterpriseRuntimeIsolationChecklist.ts | 384 ++++++++++++++++++ .../enterprise-multi-tenant/README.md | 3 +- .../runtime-isolation-checklist.md | 29 ++ 5 files changed, 500 insertions(+), 2 deletions(-) create mode 100644 backend/src/scripts/__tests__/enterpriseRuntimeIsolationChecklist.test.ts create mode 100644 backend/src/scripts/enterpriseRuntimeIsolationChecklist.ts create mode 100644 docs/features/enterprise-multi-tenant/runtime-isolation-checklist.md diff --git a/backend/package.json b/backend/package.json index 6cbcacc3..45769efc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,7 +41,7 @@ "prepack": "npm run build", "typecheck": "tsc --noEmit", "test": "jest", - "test:core": "jest --runInBand --forceExit src/agent/communication/__tests__/agentMessageBus.test.ts src/agent/core/executors/__tests__/strategyExecutor.test.ts src/agent/core/executors/__tests__/hypothesisExecutor.test.ts src/agent/context/__tests__/enhancedSessionContext.test.ts src/tests/adbTools.test.ts src/services/__tests__/sessionLogger.test.ts src/services/__tests__/traceAnalysisSkillConfig.test.ts src/agent/agents/domain/__tests__/registry.test.ts src/agentv3/__tests__/sqlIncludeInjector.test.ts src/agentv3/__tests__/analysisPatternMemory.test.ts src/agentv3/__tests__/claudeRuntimeRuntimeSnapshots.test.ts src/middleware/__tests__/auth.test.ts src/assistant/application/__tests__/assistantApplicationService.test.ts src/services/__tests__/rbac.test.ts src/routes/__tests__/agentRoutesRbac.test.ts src/routes/__tests__/ownerGuardRoutes.test.ts src/routes/__tests__/requestContextRouteCoverage.test.ts src/middleware/__tests__/legacyApiCompatibility.test.ts src/services/__tests__/enterpriseDb.test.ts src/services/__tests__/enterpriseSchema.test.ts src/services/__tests__/enterpriseRepository.test.ts src/services/__tests__/processRss.test.ts src/services/__tests__/workingTraceProcessor.enterpriseIsolation.test.ts src/services/__tests__/traceProcessorLeaseStore.test.ts src/services/__tests__/traceProcessorLeaseModeDecision.test.ts src/services/__tests__/traceProcessorLeaseProcessorRouting.test.ts src/services/__tests__/traceProcessorSqlWorker.test.ts src/services/__tests__/traceProcessorRamBudget.test.ts src/scripts/__tests__/benchmarkTraceProcessorRss.test.ts src/services/__tests__/analysisRunStore.test.ts src/services/__tests__/agentEventStore.test.ts src/services/__tests__/enterpriseKnowledgeScope.test.ts src/services/__tests__/enterpriseMigration.test.ts src/services/__tests__/runtimeSnapshotStore.test.ts src/services/providerManager/__tests__/localSecretStore.test.ts src/services/providerManager/__tests__/enterpriseProviderStore.test.ts src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts src/routes/__tests__/traceProcessorProxyRoutes.test.ts src/routes/__tests__/enterpriseReportRoutes.test.ts src/routes/__tests__/enterpriseRestartPersistence.test.ts", + "test:core": "jest --runInBand --forceExit src/agent/communication/__tests__/agentMessageBus.test.ts src/agent/core/executors/__tests__/strategyExecutor.test.ts src/agent/core/executors/__tests__/hypothesisExecutor.test.ts src/agent/context/__tests__/enhancedSessionContext.test.ts src/tests/adbTools.test.ts src/services/__tests__/sessionLogger.test.ts src/services/__tests__/traceAnalysisSkillConfig.test.ts src/agent/agents/domain/__tests__/registry.test.ts src/agentv3/__tests__/sqlIncludeInjector.test.ts src/agentv3/__tests__/analysisPatternMemory.test.ts src/agentv3/__tests__/claudeRuntimeRuntimeSnapshots.test.ts src/middleware/__tests__/auth.test.ts src/assistant/application/__tests__/assistantApplicationService.test.ts src/services/__tests__/rbac.test.ts src/routes/__tests__/agentRoutesRbac.test.ts src/routes/__tests__/ownerGuardRoutes.test.ts src/middleware/__tests__/legacyApiCompatibility.test.ts src/services/__tests__/enterpriseDb.test.ts src/services/__tests__/enterpriseSchema.test.ts src/services/__tests__/enterpriseRepository.test.ts src/services/__tests__/processRss.test.ts src/services/__tests__/workingTraceProcessor.enterpriseIsolation.test.ts src/services/__tests__/traceProcessorLeaseStore.test.ts src/services/__tests__/traceProcessorLeaseModeDecision.test.ts src/services/__tests__/traceProcessorLeaseProcessorRouting.test.ts src/services/__tests__/traceProcessorSqlWorker.test.ts src/services/__tests__/traceProcessorRamBudget.test.ts src/scripts/__tests__/benchmarkTraceProcessorRss.test.ts src/scripts/__tests__/enterpriseRuntimeIsolationChecklist.test.ts src/services/__tests__/analysisRunStore.test.ts src/services/__tests__/agentEventStore.test.ts src/services/__tests__/enterpriseKnowledgeScope.test.ts src/services/__tests__/enterpriseMigration.test.ts src/services/__tests__/runtimeSnapshotStore.test.ts src/services/providerManager/__tests__/localSecretStore.test.ts src/services/providerManager/__tests__/enterpriseProviderStore.test.ts src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts src/routes/__tests__/traceProcessorProxyRoutes.test.ts src/routes/__tests__/enterpriseReportRoutes.test.ts src/routes/__tests__/enterpriseRestartPersistence.test.ts", "test:watch": "jest --watch", "test:coverage": "jest --coverage", "test:unit": "jest --testPathPatterns=src/tests", diff --git a/backend/src/scripts/__tests__/enterpriseRuntimeIsolationChecklist.test.ts b/backend/src/scripts/__tests__/enterpriseRuntimeIsolationChecklist.test.ts new file mode 100644 index 00000000..4902b2fd --- /dev/null +++ b/backend/src/scripts/__tests__/enterpriseRuntimeIsolationChecklist.test.ts @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) +// This file is part of SmartPerfetto. See LICENSE for details. + +import fs from 'fs'; +import path from 'path'; +import { RUNTIME_ISOLATION_CHECKLIST } from '../enterpriseRuntimeIsolationChecklist'; + +const REPO_ROOT = path.resolve(__dirname, '../../../..'); +const README_PATH = 'docs/features/enterprise-multi-tenant/README.md'; +const CHECKLIST_DOC_PATH = 'docs/features/enterprise-multi-tenant/runtime-isolation-checklist.md'; + +const EXPECTED_IDS = [ + 'proxy-status-websocket-query', + 'http-rpc-target-lease-proxy', + 'websocket-fifo-query-order', + 'agent-frontend-same-lease-stats', + 'sse-terminal-events-persisted', + 'running-run-independent-cleanup', + 'upload-temp-path-unique', + 'url-upload-streaming', + 'legacy-register-rpc-disabled', + 'cleanup-draining-audit', + 'window-scoped-session-storage', + 'report-generation-isolated-priority', + 'single-supervisor-crash-recovery', + 'timeout-health-admin-drain', + 'rss-budget-observed-highwater', +] as const; + +function readRepoFile(relativePath: string): string { + return fs.readFileSync(path.join(REPO_ROOT, relativePath), 'utf8'); +} + +describe('enterprise runtime isolation checklist', () => { + it('keeps the §11.11 checklist as an explicit 15-item contract', () => { + expect(RUNTIME_ISOLATION_CHECKLIST.map(item => item.id)).toEqual(EXPECTED_IDS); + expect(new Set(RUNTIME_ISOLATION_CHECKLIST.map(item => item.vulnerability)).size).toBe(EXPECTED_IDS.length); + expect(new Set(RUNTIME_ISOLATION_CHECKLIST.map(item => item.acceptance)).size).toBe(EXPECTED_IDS.length); + }); + + it('maps every README §11.11 vulnerability and acceptance row to the checklist', () => { + const readme = readRepoFile(README_PATH); + + for (const item of RUNTIME_ISOLATION_CHECKLIST) { + expect(readme).toContain(item.vulnerability); + expect(readme).toContain(item.acceptance); + } + expect(readme).toContain('- [x] 4.12 §11.11 漏洞清单:每行设计验收都打勾验证'); + expect(readme).toContain(`- [x] \`${CHECKLIST_DOC_PATH}\``); + }); + + it('verifies the evidence file and pattern for every checklist item', () => { + const checkedEvidence = new Set(); + + for (const item of RUNTIME_ISOLATION_CHECKLIST) { + expect(item.evidence.length).toBeGreaterThan(0); + for (const evidence of item.evidence) { + const evidencePath = path.join(REPO_ROOT, evidence.file); + expect(fs.existsSync(evidencePath)).toBe(true); + + const content = readRepoFile(evidence.file); + expect(evidence.patterns.length).toBeGreaterThan(0); + for (const pattern of evidence.patterns) { + expect(content).toContain(pattern); + checkedEvidence.add(`${evidence.file}:${pattern}`); + } + } + } + + expect(checkedEvidence.size).toBeGreaterThanOrEqual(EXPECTED_IDS.length * 2); + }); + + it('keeps the runtime isolation checklist document synchronized with the contract', () => { + const doc = readRepoFile(CHECKLIST_DOC_PATH); + + for (const item of RUNTIME_ISOLATION_CHECKLIST) { + expect(doc).toContain(item.id); + expect(doc).toContain(item.vulnerability); + expect(doc).toContain(item.acceptance); + } + expect(doc).toContain('cd backend && npx jest --runInBand src/scripts/__tests__/enterpriseRuntimeIsolationChecklist.test.ts'); + }); +}); diff --git a/backend/src/scripts/enterpriseRuntimeIsolationChecklist.ts b/backend/src/scripts/enterpriseRuntimeIsolationChecklist.ts new file mode 100644 index 00000000..6b3647ab --- /dev/null +++ b/backend/src/scripts/enterpriseRuntimeIsolationChecklist.ts @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) +// This file is part of SmartPerfetto. See LICENSE for details. + +export interface RuntimeIsolationEvidence { + file: string; + patterns: string[]; +} + +export interface RuntimeIsolationChecklistItem { + id: string; + vulnerability: string; + fix: string; + acceptance: string; + evidence: RuntimeIsolationEvidence[]; +} + +export const RUNTIME_ISOLATION_CHECKLIST = [ + { + id: 'proxy-status-websocket-query', + vulnerability: 'Proxy 只代理 HTTP `/query`', + fix: 'Backend lease proxy covers `/status`, `/websocket`, `/query`, and the enterprise UI uses lease proxy targets instead of raw local ports.', + acceptance: '企业模式前端 network 不再访问 `127.0.0.1:9100-9900`', + evidence: [ + { + file: 'backend/src/routes/__tests__/traceProcessorProxyRoutes.test.ts', + patterns: [ + 'proxies status and query bytes through the scoped lease', + 'tunnels websocket upgrades to the leased trace processor port', + ], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: [ + 'frontendTimelineUsesLeaseProxy', + '/api/tp/${encodeURIComponent(frontendLease.id)}/websocket', + "!target.includes('127.0.0.1')", + ], + }, + { + file: 'perfetto/ui/src/plugins/com.smartperfetto.AIAssistant/ai_panel.ts', + patterns: ["rpcTarget.mode === 'backend-lease-proxy'"], + }, + ], + }, + { + id: 'http-rpc-target-lease-proxy', + vulnerability: '`HttpRpcEngine` 仍只接受 port', + fix: '`HttpRpcEngine` accepts a structured `HttpRpcTarget`, so enterprise mode can hide port changes behind a stable lease proxy.', + acceptance: 'processor restart 后前端 leaseId 不变', + evidence: [ + { + file: 'perfetto/ui/src/trace_processor/http_rpc_engine.ts', + patterns: [ + 'export interface HttpRpcTarget', + "mode: 'direct-port' | 'backend-lease-proxy'", + 'static setRpcTarget(target: HttpRpcTarget): void', + ], + }, + { + file: 'perfetto/ui/src/trace_processor/http_rpc_engine_unittest.ts', + patterns: ['uses backend lease proxy targets when configured'], + }, + { + file: 'backend/src/routes/__tests__/traceProcessorProxyRoutes.test.ts', + patterns: ['lets workspace admins restart a scoped lease without changing the lease id'], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: ['leaseIdStableAcrossCrashRestart'], + }, + ], + }, + { + id: 'websocket-fifo-query-order', + vulnerability: '同一 WebSocket 内重排 query', + fix: 'Frontend WebSocket responses are drained in FIFO order, and the backend SQL worker keeps queued work ordered by priority and FIFO within a priority.', + acceptance: '并发 timeline 查询结果不错位', + evidence: [ + { + file: 'perfetto/ui/src/trace_processor/http_rpc_engine.ts', + patterns: [ + 'private isProcessingQueue = false', + 'this.queue.push(blob)', + 'while (this.queue.length > 0)', + ], + }, + { + file: 'backend/src/services/__tests__/traceProcessorSqlWorker.test.ts', + patterns: [ + 'does not preempt the running query, but runs queued P0 before queued P1/P2', + 'keeps FIFO order inside the same priority level', + ], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: ['queryResultsStayAssociatedWithOriginalSql'], + }, + ], + }, + { + id: 'agent-frontend-same-lease-stats', + vulnerability: 'Agent query 和前端 WebSocket 走两套 processor', + fix: 'Frontend, agent, and report paths acquire typed holders through `TraceProcessorLease`, with shared/isolated mode visible in lease stats.', + acceptance: '同 trace 的 frontend/agent/report holder 出现在同一 lease stats', + evidence: [ + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: [ + 'frontendAndAgentUseLeaseHolders', + "holderType: 'frontend_http_rpc'", + "holderType: 'agent_run'", + "holderType: 'report_generation'", + ], + }, + { + file: 'backend/src/routes/__tests__/agentRoutesRbac.test.ts', + patterns: ['selects an isolated lease for full analysis runs'], + }, + { + file: 'backend/src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts', + patterns: ['reports isolated report-generation lease queue length separately from the frontend shared queue'], + }, + ], + }, + { + id: 'sse-terminal-events-persisted', + vulnerability: 'SSE terminal event 不进持久事件表', + fix: 'SSE replay is backed by persisted `AgentEvent` rows and clients reconnect with `Last-Event-ID`.', + acceptance: '断线发生在 conclusion 和 report 之间也能 replay reportUrl', + evidence: [ + { + file: 'backend/src/routes/__tests__/agentRoutesRbac.test.ts', + patterns: ['replays persisted terminal SSE events before falling back to the in-memory buffer'], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: ['persistedEventsReplayAfterRestart'], + }, + { + file: 'perfetto/ui/src/plugins/com.smartperfetto.AIAssistant/agent_sse_transport.ts', + patterns: ["headers['Last-Event-ID']"], + }, + { + file: 'perfetto/ui/src/plugins/com.smartperfetto.AIAssistant/agent_sse_transport_unittest.ts', + patterns: ['sends replay cursor through Last-Event-ID header'], + }, + ], + }, + { + id: 'running-run-independent-cleanup', + vulnerability: 'running run cleanup 只看 SSE client', + fix: 'Cleanup/delete decisions check persisted run state and run heartbeat, not only the SSE client connection.', + acceptance: '长 run 断开 SSE 后仍可恢复', + evidence: [ + { + file: 'backend/src/services/enterpriseSchema.ts', + patterns: [ + "addColumnIfMissing(db, 'analysis_runs', 'heartbeat_at', 'INTEGER')", + 'idx_analysis_runs_heartbeat', + ], + }, + { + file: 'backend/src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts', + patterns: ['blocks enterprise trace delete while runs, active leases, or report holders still own the trace'], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: [ + 'deleteDetectsRunningRunBeforeRemovingTrace', + "activeRuns[0]?.status === 'running'", + 'aEventStreamContinuesAfterBStart', + ], + }, + ], + }, + { + id: 'upload-temp-path-unique', + vulnerability: '上传临时文件名冲突', + fix: 'Uploads are persisted through trace-id scoped storage and atomic finalization instead of shared original filenames.', + acceptance: '两窗口上传同名 trace 不互相覆盖', + evidence: [ + { + file: 'backend/src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts', + patterns: ['stores uploaded trace metadata in trace_assets and moves the trace into scoped data storage'], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: [ + 'sameFilenameUsesDistinctTraceIds', + 'sameFilenameUsesDistinctFiles', + ], + }, + ], + }, + { + id: 'url-upload-streaming', + vulnerability: 'URL 上传全量 buffer', + fix: 'URL uploads stream directly into scoped trace storage without buffering the full response in Node memory.', + acceptance: '大 trace 上传期间第一窗口心跳和 SSE 不超时', + evidence: [ + { + file: 'backend/src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts', + patterns: ['streams URL uploads into scoped trace storage without buffering the response body'], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: [ + 'aEventStreamContinuesAfterBStart', + '/api/tp/${encodeURIComponent(frontendLease.id)}/heartbeat', + ], + }, + ], + }, + { + id: 'legacy-register-rpc-disabled', + vulnerability: 'ExternalRpc/register-rpc 兼容路径绕过新 lease', + fix: 'Enterprise mode rejects legacy direct RPC registration before it can create naked-port processor state.', + acceptance: '代码搜索无企业模式可用裸 port 注册路径', + evidence: [ + { + file: 'backend/src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts', + patterns: ['disables legacy direct RPC registration in enterprise mode before creating naked-port state'], + }, + { + file: 'backend/src/services/__tests__/workingTraceProcessor.enterpriseIsolation.test.ts', + patterns: [ + 'applies the same wall-clock timeout to external raw RPC queries', + 'runs health SELECT 1 on a dedicated channel outside the SQL worker queue', + ], + }, + ], + }, + { + id: 'cleanup-draining-audit', + vulnerability: '`/api/traces/cleanup` 悬崖 endpoint', + fix: 'Enterprise cleanup is admin-only, drains idle leases, blocks active holders, and records audit events.', + acceptance: 'running lease 存在时 cleanup 返回 blocked', + evidence: [ + { + file: 'backend/src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts', + patterns: [ + 'blocks enterprise cleanup when scoped trace processor leases still have active holders and audits the attempt', + 'drains idle enterprise leases before scoped processor cleanup and records an audit event', + 'hides enterprise cleanup from non-admin analysts', + ], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: [ + 'activeLeaseBlocksCleanupOrDelete', + 'drainingLeaseRejectsNewWork', + ], + }, + ], + }, + { + id: 'window-scoped-session-storage', + vulnerability: 'localStorage 跨窗口覆盖', + fix: 'Pending trace state is scoped by workspace and window id in `sessionStorage`, with stale save merging to avoid read-modify-write overwrites.', + acceptance: '双窗口刷新后各自 traceId/sessionId 不变', + evidence: [ + { + file: 'perfetto/ui/src/plugins/com.smartperfetto.AIAssistant/session_manager.ts', + patterns: [ + 'windowId = getSmartPerfettoWindowId()', + 'sessionStorage.setItem', + 'sessionStorage.getItem(scopedKey)', + ], + }, + { + file: 'perfetto/ui/src/plugins/com.smartperfetto.AIAssistant/session_manager_unittest.ts', + patterns: [ + 'stores pending backend traces under a workspace and window-scoped sessionStorage key', + 'does not recover another window pending trace', + 'recovers pending backend traces by lease id for proxy mode', + 'merges stale read-modify-write saves instead of overwriting concurrent sessions', + ], + }, + ], + }, + { + id: 'report-generation-isolated-priority', + vulnerability: 'report generation 长 SQL 阻塞前端', + fix: 'Report generation uses a report holder, P2 priority, and isolated heavy-work visibility so frontend P0 timeline work remains protected.', + acceptance: 'report 生成时前端 P0 query 延迟在阈值内或明确显示 isolated', + evidence: [ + { + file: 'backend/src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts', + patterns: ['reports isolated report-generation lease queue length separately from the frontend shared queue'], + }, + { + file: 'backend/src/services/__tests__/traceProcessorSqlWorker.test.ts', + patterns: ['does not preempt the running query, but runs queued P0 before queued P1/P2'], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: [ + 'p0DoesNotWaitBehindQueuedP1OrP2', + 'workerStatsExposeQueuedP0', + 'reportGenerationHolderIsProtected', + ], + }, + ], + }, + { + id: 'single-supervisor-crash-recovery', + vulnerability: 'processor crash recovery 重启风暴', + fix: 'Crash recovery is coordinated by one lease supervisor with backoff, while holders wait on lease state.', + acceptance: 'crash 后只有一个 restart 序列', + evidence: [ + { + file: 'backend/src/services/__tests__/traceProcessorLeaseProcessorRouting.test.ts', + patterns: [ + 'uses one supervisor restart for concurrent crashed lease holders and preserves the lease id', + 'marks the lease failed after the 1s/5s/15s backoff restart attempts all fail', + ], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: [ + 'leaseIdStableAcrossCrashRestart', + 'stateMachineUsesSingleRestartSequence', + ], + }, + ], + }, + { + id: 'timeout-health-admin-drain', + vulnerability: '24h timeout 掩盖挂死 query', + fix: 'Long query timeout is paired with a dedicated health channel and admin drain/restart controls for bad leases.', + acceptance: '挂死 query 可被管理员标记 lease draining/restart', + evidence: [ + { + file: 'backend/src/services/__tests__/workingTraceProcessor.enterpriseIsolation.test.ts', + patterns: [ + 'defaults trace processor query timeout to 24 hours', + 'runs health SELECT 1 on a dedicated channel outside the SQL worker queue', + ], + }, + { + file: 'backend/src/routes/__tests__/traceProcessorProxyRoutes.test.ts', + patterns: [ + 'lets workspace admins drain a scoped lease and block new proxy work', + 'lets workspace admins restart a scoped lease without changing the lease id', + ], + }, + ], + }, + { + id: 'rss-budget-observed-highwater', + vulnerability: 'RSS budget 估算错误', + fix: 'Admission uses a RAM budget, observed processor RSS, and benchmark coverage so high-water behavior rejects new leases instead of killing existing windows.', + acceptance: '压测记录 RSS,高水位超过预算时拒绝新 lease', + evidence: [ + { + file: 'backend/src/services/__tests__/traceProcessorRamBudget.test.ts', + patterns: [ + 'subtracts observed processor RSS from explicit RAM budget', + 'rejects a new trace when the estimate exceeds the remaining budget', + ], + }, + { + file: 'backend/src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts', + patterns: ['records observed processor RSS on the frontend lease and exposes RAM budget stats'], + }, + { + file: 'backend/src/scripts/__tests__/benchmarkTraceProcessorRss.test.ts', + patterns: [ + 'marks the §0.4.3 matrix incomplete when required scene/size cells are missing', + 'marks the §0.4.3 matrix complete only after every scene and size bucket is covered', + ], + }, + { + file: 'backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts', + patterns: [ + 'admissionRejectsNewLeaseNearRamBudget', + 'activeLeaseSurvivesRejectedAdmission', + 'noOomStyleCleanupOfExistingWindow', + ], + }, + ], + }, +] satisfies readonly RuntimeIsolationChecklistItem[]; diff --git a/docs/features/enterprise-multi-tenant/README.md b/docs/features/enterprise-multi-tenant/README.md index 45494631..a78629a7 100644 --- a/docs/features/enterprise-multi-tenant/README.md +++ b/docs/features/enterprise-multi-tenant/README.md @@ -72,7 +72,7 @@ - [x] 4.9 24h query timeout 覆盖 WorkingTraceProcessor 与 ExternalRpcProcessor 双路径;独立 health channel `SELECT 1`(§11.8) - [x] 4.10 crash recovery + backoff(1s/5s/15s + jitter)+ 稳定 leaseId;单 supervisor 重启,holder 不各自重试 - [x] 4.11 `/api/traces/cleanup` 企业模式禁用或 admin-only + draining + audit -- [ ] 4.12 §11.11 漏洞清单:每行设计验收都打勾验证 +- [x] 4.12 §11.11 漏洞清单:每行设计验收都打勾验证 ### 0.5 主线 D:控制面与合规(§18) - [ ] 5.1 tenant / workspace / member / provider / quota 管理 UI 与后端 API @@ -139,6 +139,7 @@ - [ ] (新增 ADR / 设计 / runbook 在此追加,例如:`docs/adr/0001-enterprise-db-choice.md` …) - [x] `docs/features/enterprise-multi-tenant/adr-0001-sqlite-wal-repository.md`(§0.3.1 SQLite WAL + repository abstraction 决策) - [ ] `docs/features/enterprise-multi-tenant/rss-benchmark.md`(§0.4.3 RSS benchmark runbook;等待 100MB/500MB/1GB 大 trace 实测) +- [x] `docs/features/enterprise-multi-tenant/runtime-isolation-checklist.md`(§0.4.12 §11.11 漏洞清单设计验收证据) ### 0.10 PR / 提交收尾(每次 PR 都要走) - [ ] 每个主线 / 子主线一个独立 PR;不跨主线串改动 diff --git a/docs/features/enterprise-multi-tenant/runtime-isolation-checklist.md b/docs/features/enterprise-multi-tenant/runtime-isolation-checklist.md new file mode 100644 index 00000000..aaacdaec --- /dev/null +++ b/docs/features/enterprise-multi-tenant/runtime-isolation-checklist.md @@ -0,0 +1,29 @@ +# Enterprise Runtime Isolation Checklist + +本文件把 `README.md` §11.11 的 15 个“双窗口双 trace”漏洞项落成可验证清单。每行都对应 `backend/src/scripts/enterpriseRuntimeIsolationChecklist.ts` 的一个稳定 ID,并由 `enterpriseRuntimeIsolationChecklist.test.ts` 检查 README 原文、证据文件和本文档是否同步。 + +验证命令: + +```bash +cd backend && npx jest --runInBand src/scripts/__tests__/enterpriseRuntimeIsolationChecklist.test.ts +``` + +说明:这里验证的是运行时隔离设计验收是否已有自动化证据闭环;§0.4.3 的 100MB/500MB/1GB RSS 实测矩阵仍由 `rss-benchmark.md` 单独跟踪。 + +| ID | 漏洞 | 验收 | 自动证据 | +| --- | --- | --- | --- | +| `proxy-status-websocket-query` | Proxy 只代理 HTTP `/query` | 企业模式前端 network 不再访问 `127.0.0.1:9100-9900` | `traceProcessorProxyRoutes.test.ts` 覆盖 `/status`、`/query`、`/websocket`;D3 脚本断言前端只走 `/api/tp/:leaseId/*`,不访问裸本地端口;UI panel 使用 `backend-lease-proxy`。 | +| `http-rpc-target-lease-proxy` | `HttpRpcEngine` 仍只接受 port | processor restart 后前端 leaseId 不变 | `HttpRpcTarget` 支持 `direct-port` 与 `backend-lease-proxy`;前端单测覆盖 lease proxy target;proxy route 和 D4 脚本覆盖 restart 后 leaseId 稳定。 | +| `websocket-fifo-query-order` | 同一 WebSocket 内重排 query | 并发 timeline 查询结果不错位 | `HttpRpcEngine` 串行 drain WebSocket response queue;SQL worker 单测覆盖 non-preemptive priority 与同优先级 FIFO;D3 脚本断言 query 结果与原 SQL 关联不串。 | +| `agent-frontend-same-lease-stats` | Agent query 和前端 WebSocket 走两套 processor | 同 trace 的 frontend/agent/report holder 出现在同一 lease stats | D3 脚本断言 `frontend_http_rpc`、`agent_run`、`report_generation` holder 进入 lease stats;agent route 单测覆盖 full analysis lease 选择;metadata route 单测覆盖 report queue 与 shared frontend queue 可观测。 | +| `sse-terminal-events-persisted` | SSE terminal event 不进持久事件表 | 断线发生在 conclusion 和 report 之间也能 replay reportUrl | `agentRoutesRbac.test.ts` 覆盖 terminal SSE event 持久回放;D9 脚本覆盖重启后 `AgentEvent` replay;前端 SSE transport 单测覆盖 `Last-Event-ID`。 | +| `running-run-independent-cleanup` | running run cleanup 只看 SSE client | 长 run 断开 SSE 后仍可恢复 | schema 为 `analysis_runs` 增加 `heartbeat_at` 与索引;trace delete route 单测按 running run / active lease / report holder 阻断;D7/D2 脚本覆盖 running run 与 SSE 独立存活。 | +| `upload-temp-path-unique` | 上传临时文件名冲突 | 两窗口上传同名 trace 不互相覆盖 | metadata route 单测覆盖上传落入 scoped trace storage;D1 脚本断言同名上传生成不同 traceId 与不同文件路径。 | +| `url-upload-streaming` | URL 上传全量 buffer | 大 trace 上传期间第一窗口心跳和 SSE 不超时 | metadata route 单测覆盖 URL response streaming;D2/D3 脚本覆盖 A 窗口 SSE 继续推进、前端 lease heartbeat 仍存在。 | +| `legacy-register-rpc-disabled` | ExternalRpc/register-rpc 兼容路径绕过新 lease | 代码搜索无企业模式可用裸 port 注册路径 | metadata route 单测在 enterprise mode 禁用 legacy direct RPC registration;working trace processor 单测覆盖 ExternalRpc timeout 与 dedicated health query,避免裸路径绕过隔离控制。 | +| `cleanup-draining-audit` | `/api/traces/cleanup` 悬崖 endpoint | running lease 存在时 cleanup 返回 blocked | metadata route 单测覆盖 admin-only、active lease blocked、idle lease draining 与 audit;D7 脚本覆盖 cleanup/delete 被 active lease 和 draining 保护。 | +| `window-scoped-session-storage` | localStorage 跨窗口覆盖 | 双窗口刷新后各自 traceId/sessionId 不变 | session manager 使用 workspace/window scoped `sessionStorage`;单测覆盖不同窗口不互相恢复、proxy mode 按 leaseId 恢复、并发保存 merge。 | +| `report-generation-isolated-priority` | report generation 长 SQL 阻塞前端 | report 生成时前端 P0 query 延迟在阈值内或明确显示 isolated | metadata route 单测暴露 isolated report queue;SQL worker 单测覆盖 P0 在 queued P1/P2 前执行;D3/D7 脚本覆盖 P0 headroom、queued P0 stats 与 report holder 保护。 | +| `single-supervisor-crash-recovery` | processor crash recovery 重启风暴 | crash 后只有一个 restart 序列 | lease processor routing 单测覆盖多 holder crash 只走一个 supervisor restart、失败后进入 failed;D4 脚本覆盖 leaseId 稳定与单 restart sequence。 | +| `timeout-health-admin-drain` | 24h timeout 掩盖挂死 query | 挂死 query 可被管理员标记 lease draining/restart | working trace processor 单测覆盖 24h timeout 默认值与 dedicated health `SELECT 1`;proxy route 单测覆盖 admin drain/restart lease。 | +| `rss-budget-observed-highwater` | RSS budget 估算错误 | 压测记录 RSS,高水位超过预算时拒绝新 lease | RAM budget 单测覆盖 observed RSS 扣减与 admission reject;metadata route 单测暴露 observed processor RSS;RSS benchmark helper 单测约束 required matrix;D10 脚本覆盖拒绝新 lease 时已有窗口不被 OOM 式清理。 |