autosnippet 2.0.2 → 2.4.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 +189 -113
- package/bin/api-server.js +1 -4
- package/bin/cli.js +1 -50
- package/config/constitution.yaml +33 -107
- package/dashboard/dist/assets/{icons-B4FfLfBA.js → icons-B5rs8uNb.js} +85 -80
- package/dashboard/dist/assets/index-0YzLw2ga.css +1 -0
- package/dashboard/dist/assets/index-B9py3ybr.js +154 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/bootstrap.js +5 -31
- package/lib/cli/SetupService.js +16 -14
- package/lib/core/capability/CapabilityProbe.js +8 -6
- package/lib/core/constitution/Constitution.js +13 -4
- package/lib/core/constitution/ConstitutionValidator.js +106 -211
- package/lib/core/gateway/Gateway.js +34 -98
- package/lib/core/gateway/GatewayActionRegistry.js +12 -1
- package/lib/core/permission/PermissionManager.js +2 -2
- package/lib/external/mcp/McpServer.js +4 -7
- package/lib/external/mcp/handlers/bootstrap.js +13 -1
- package/lib/external/mcp/handlers/browse.js +0 -7
- package/lib/external/mcp/handlers/candidate.js +1 -1
- package/lib/external/mcp/handlers/guard.js +11 -0
- package/lib/external/mcp/handlers/skill.js +186 -18
- package/lib/external/mcp/tools.js +40 -1
- package/lib/http/middleware/roleResolver.js +1 -1
- package/lib/http/routes/auth.js +2 -2
- package/lib/http/routes/commands.js +58 -3
- package/lib/http/routes/monitoring.js +4 -4
- package/lib/http/routes/recipes.js +96 -4
- package/lib/http/routes/search.js +34 -35
- package/lib/injection/ServiceContainer.js +21 -40
- package/lib/service/candidate/CandidateService.js +12 -1
- package/lib/service/chat/ChatAgent.js +171 -30
- package/lib/service/chat/Memory.js +104 -0
- package/lib/service/chat/tools.js +244 -10
- package/lib/service/guard/GuardCheckEngine.js +9 -1
- package/lib/service/knowledge/KnowledgeGraphService.js +20 -9
- package/lib/service/recipe/RecipeService.js +8 -0
- package/lib/service/skills/SkillHooks.js +126 -0
- package/package.json +1 -1
- package/scripts/init-db.js +1 -2
- package/templates/constitution.yaml +29 -85
- package/dashboard/dist/assets/index-ChxJxX4B.js +0 -154
- package/dashboard/dist/assets/index-DwAp1mx5.css +0 -1
- package/lib/core/session/SessionManager.js +0 -232
- package/lib/infrastructure/logging/ReasoningLogger.js +0 -269
- package/lib/infrastructure/monitoring/RoleDriftMonitor.js +0 -259
- package/lib/infrastructure/quality/ComplianceEvaluator.js +0 -326
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ReasoningLogger - 透明推理日志系统
|
|
3
|
-
*
|
|
4
|
-
* 记录 AI 操作的完整推理过程,包括:
|
|
5
|
-
* - Candidate 提交时的推理上下文
|
|
6
|
-
* - Guard 检查时的规则匹配过程
|
|
7
|
-
* - 搜索时的排名决策
|
|
8
|
-
*
|
|
9
|
-
* 所有推理日志不可篡改,存储在 SQLite 中
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
13
|
-
import Logger from '../logging/Logger.js';
|
|
14
|
-
|
|
15
|
-
export class ReasoningLogger {
|
|
16
|
-
constructor(db) {
|
|
17
|
-
this.db = typeof db?.getDb === 'function' ? db.getDb() : db;
|
|
18
|
-
this.logger = Logger.getInstance();
|
|
19
|
-
this._ensureTable();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
_ensureTable() {
|
|
23
|
-
this.db.exec(`
|
|
24
|
-
CREATE TABLE IF NOT EXISTS reasoning_logs (
|
|
25
|
-
id TEXT PRIMARY KEY,
|
|
26
|
-
type TEXT NOT NULL,
|
|
27
|
-
actor TEXT NOT NULL,
|
|
28
|
-
resource_type TEXT,
|
|
29
|
-
resource_id TEXT,
|
|
30
|
-
reasoning_json TEXT NOT NULL DEFAULT '{}',
|
|
31
|
-
quality_score REAL DEFAULT 0,
|
|
32
|
-
context_json TEXT DEFAULT '{}',
|
|
33
|
-
created_at INTEGER NOT NULL
|
|
34
|
-
)
|
|
35
|
-
`);
|
|
36
|
-
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_reasoning_logs_type ON reasoning_logs(type)`);
|
|
37
|
-
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_reasoning_logs_actor ON reasoning_logs(actor)`);
|
|
38
|
-
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_reasoning_logs_created ON reasoning_logs(created_at)`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 记录 Candidate 推理过程
|
|
43
|
-
*/
|
|
44
|
-
logCandidateReasoning(candidate, actor, context = {}) {
|
|
45
|
-
const reasoning = candidate.reasoning || {};
|
|
46
|
-
const qualityScore = this._assessReasoningQuality(reasoning);
|
|
47
|
-
|
|
48
|
-
return this._insert({
|
|
49
|
-
type: 'candidate_reasoning',
|
|
50
|
-
actor,
|
|
51
|
-
resourceType: 'candidate',
|
|
52
|
-
resourceId: candidate.id,
|
|
53
|
-
reasoning: {
|
|
54
|
-
whyStandard: reasoning.whyStandard || '',
|
|
55
|
-
sources: reasoning.sources || [],
|
|
56
|
-
qualitySignals: reasoning.qualitySignals || {},
|
|
57
|
-
alternatives: reasoning.alternatives || [],
|
|
58
|
-
confidence: reasoning.confidence || 0,
|
|
59
|
-
assessment: {
|
|
60
|
-
completeness: this._assessCompleteness(reasoning),
|
|
61
|
-
sourceQuality: this._assessSourceQuality(reasoning),
|
|
62
|
-
overall: qualityScore,
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
qualityScore,
|
|
66
|
-
context,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 记录 Guard 规则匹配推理
|
|
72
|
-
*/
|
|
73
|
-
logGuardReasoning(code, matchedRules, actor, context = {}) {
|
|
74
|
-
return this._insert({
|
|
75
|
-
type: 'guard_reasoning',
|
|
76
|
-
actor,
|
|
77
|
-
resourceType: 'code_check',
|
|
78
|
-
resourceId: context.fileId || null,
|
|
79
|
-
reasoning: {
|
|
80
|
-
codeLength: code.length,
|
|
81
|
-
rulesChecked: matchedRules.length,
|
|
82
|
-
violations: matchedRules.map(r => ({
|
|
83
|
-
ruleId: r.ruleId || r.id,
|
|
84
|
-
ruleName: r.name,
|
|
85
|
-
severity: r.severity,
|
|
86
|
-
matchCount: r.matches?.length || 0,
|
|
87
|
-
sourceRecipeId: r.sourceRecipeId,
|
|
88
|
-
})),
|
|
89
|
-
checkDuration: context.duration,
|
|
90
|
-
},
|
|
91
|
-
qualityScore: 0,
|
|
92
|
-
context,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 记录搜索排名推理
|
|
98
|
-
*/
|
|
99
|
-
logSearchReasoning(query, results, rankingFactors, actor, context = {}) {
|
|
100
|
-
return this._insert({
|
|
101
|
-
type: 'search_reasoning',
|
|
102
|
-
actor,
|
|
103
|
-
resourceType: 'search',
|
|
104
|
-
resourceId: null,
|
|
105
|
-
reasoning: {
|
|
106
|
-
query,
|
|
107
|
-
resultCount: results.length,
|
|
108
|
-
topResults: results.slice(0, 5).map(r => ({
|
|
109
|
-
id: r.id,
|
|
110
|
-
score: r.score,
|
|
111
|
-
title: r.title,
|
|
112
|
-
})),
|
|
113
|
-
rankingFactors,
|
|
114
|
-
},
|
|
115
|
-
qualityScore: 0,
|
|
116
|
-
context,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* 查询推理日志
|
|
122
|
-
*/
|
|
123
|
-
query(filters = {}, pagination = { page: 1, pageSize: 20 }) {
|
|
124
|
-
const conditions = ['1=1'];
|
|
125
|
-
const params = [];
|
|
126
|
-
|
|
127
|
-
if (filters.type) {
|
|
128
|
-
conditions.push('type = ?');
|
|
129
|
-
params.push(filters.type);
|
|
130
|
-
}
|
|
131
|
-
if (filters.actor) {
|
|
132
|
-
conditions.push('actor = ?');
|
|
133
|
-
params.push(filters.actor);
|
|
134
|
-
}
|
|
135
|
-
if (filters.resourceType) {
|
|
136
|
-
conditions.push('resource_type = ?');
|
|
137
|
-
params.push(filters.resourceType);
|
|
138
|
-
}
|
|
139
|
-
if (filters.resourceId) {
|
|
140
|
-
conditions.push('resource_id = ?');
|
|
141
|
-
params.push(filters.resourceId);
|
|
142
|
-
}
|
|
143
|
-
if (filters.minQuality !== undefined) {
|
|
144
|
-
conditions.push('quality_score >= ?');
|
|
145
|
-
params.push(filters.minQuality);
|
|
146
|
-
}
|
|
147
|
-
if (filters.since) {
|
|
148
|
-
conditions.push('created_at >= ?');
|
|
149
|
-
params.push(filters.since);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const offset = (pagination.page - 1) * pagination.pageSize;
|
|
153
|
-
const where = conditions.join(' AND ');
|
|
154
|
-
|
|
155
|
-
const rows = this.db.prepare(
|
|
156
|
-
`SELECT * FROM reasoning_logs WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
157
|
-
).all(...params, pagination.pageSize, offset);
|
|
158
|
-
|
|
159
|
-
const countRow = this.db.prepare(
|
|
160
|
-
`SELECT COUNT(*) as total FROM reasoning_logs WHERE ${where}`
|
|
161
|
-
).get(...params);
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
items: rows.map(this._mapRow),
|
|
165
|
-
total: countRow.total,
|
|
166
|
-
page: pagination.page,
|
|
167
|
-
pageSize: pagination.pageSize,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* 获取推理质量统计
|
|
173
|
-
*/
|
|
174
|
-
getQualityStats(since = null) {
|
|
175
|
-
const params = [];
|
|
176
|
-
let where = '1=1';
|
|
177
|
-
if (since) {
|
|
178
|
-
where = 'created_at >= ?';
|
|
179
|
-
params.push(since);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const stats = this.db.prepare(`
|
|
183
|
-
SELECT
|
|
184
|
-
type,
|
|
185
|
-
COUNT(*) as count,
|
|
186
|
-
AVG(quality_score) as avg_quality,
|
|
187
|
-
MIN(quality_score) as min_quality,
|
|
188
|
-
MAX(quality_score) as max_quality
|
|
189
|
-
FROM reasoning_logs
|
|
190
|
-
WHERE ${where}
|
|
191
|
-
GROUP BY type
|
|
192
|
-
`).all(...params);
|
|
193
|
-
|
|
194
|
-
return stats;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ========== Private Methods ==========
|
|
198
|
-
|
|
199
|
-
_insert({ type, actor, resourceType, resourceId, reasoning, qualityScore, context }) {
|
|
200
|
-
const id = uuidv4();
|
|
201
|
-
const now = Math.floor(Date.now() / 1000);
|
|
202
|
-
|
|
203
|
-
this.db.prepare(`
|
|
204
|
-
INSERT INTO reasoning_logs (id, type, actor, resource_type, resource_id, reasoning_json, quality_score, context_json, created_at)
|
|
205
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
206
|
-
`).run(id, type, actor, resourceType, resourceId, JSON.stringify(reasoning), qualityScore, JSON.stringify(context), now);
|
|
207
|
-
|
|
208
|
-
return { id, type, actor, qualityScore, createdAt: now };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
_mapRow(row) {
|
|
212
|
-
return {
|
|
213
|
-
id: row.id,
|
|
214
|
-
type: row.type,
|
|
215
|
-
actor: row.actor,
|
|
216
|
-
resourceType: row.resource_type,
|
|
217
|
-
resourceId: row.resource_id,
|
|
218
|
-
reasoning: JSON.parse(row.reasoning_json || '{}'),
|
|
219
|
-
qualityScore: row.quality_score,
|
|
220
|
-
context: JSON.parse(row.context_json || '{}'),
|
|
221
|
-
createdAt: row.created_at,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* 评估推理质量(0-1)
|
|
227
|
-
*/
|
|
228
|
-
_assessReasoningQuality(reasoning) {
|
|
229
|
-
if (!reasoning) return 0;
|
|
230
|
-
|
|
231
|
-
const completeness = this._assessCompleteness(reasoning);
|
|
232
|
-
const sourceQuality = this._assessSourceQuality(reasoning);
|
|
233
|
-
const confidenceScore = typeof reasoning.confidence === 'number' ? reasoning.confidence : 0;
|
|
234
|
-
|
|
235
|
-
// 权重: 完整性 40%, 来源质量 35%, 置信度 25%
|
|
236
|
-
return Math.round((completeness * 0.4 + sourceQuality * 0.35 + confidenceScore * 0.25) * 100) / 100;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
_assessCompleteness(reasoning) {
|
|
240
|
-
if (!reasoning) return 0;
|
|
241
|
-
let score = 0;
|
|
242
|
-
if (reasoning.whyStandard && reasoning.whyStandard.length > 10) score += 0.3;
|
|
243
|
-
if (Array.isArray(reasoning.sources) && reasoning.sources.length > 0) score += 0.25;
|
|
244
|
-
if (reasoning.qualitySignals && Object.keys(reasoning.qualitySignals).length > 0) score += 0.2;
|
|
245
|
-
if (Array.isArray(reasoning.alternatives) && reasoning.alternatives.length > 0) score += 0.15;
|
|
246
|
-
if (typeof reasoning.confidence === 'number') score += 0.1;
|
|
247
|
-
return Math.min(1, score);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
_assessSourceQuality(reasoning) {
|
|
251
|
-
if (!reasoning || !Array.isArray(reasoning.sources) || reasoning.sources.length === 0) return 0;
|
|
252
|
-
// 多来源 + 来源有类型或链接 → 较高质量
|
|
253
|
-
const count = Math.min(reasoning.sources.length, 5);
|
|
254
|
-
return count / 5;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
let instance = null;
|
|
259
|
-
|
|
260
|
-
export function initReasoningLogger(db) {
|
|
261
|
-
instance = new ReasoningLogger(db);
|
|
262
|
-
return instance;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export function getReasoningLogger() {
|
|
266
|
-
return instance;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export default ReasoningLogger;
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RoleDriftMonitor - 角色漂移检测系统
|
|
3
|
-
*
|
|
4
|
-
* 功能:
|
|
5
|
-
* - 监控 AI 角色切换行为
|
|
6
|
-
* - 检测非法角色转移
|
|
7
|
-
* - 记录漂移事件供审计
|
|
8
|
-
* - 提供角色稳定性评分
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import Logger from '../logging/Logger.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 角色转移矩阵
|
|
15
|
-
* 定义每个角色允许转移到哪些角色
|
|
16
|
-
*/
|
|
17
|
-
const ALLOWED_TRANSITIONS = {
|
|
18
|
-
cursor_agent: ['cursor_agent'],
|
|
19
|
-
asd_ais: ['asd_ais'],
|
|
20
|
-
guard_engine: ['guard_engine'],
|
|
21
|
-
developer_admin: ['developer_admin', 'developer_contributor'],
|
|
22
|
-
developer_contributor: ['developer_contributor'],
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 角色能力边界
|
|
27
|
-
* 每个角色可以执行的 action 前缀
|
|
28
|
-
*/
|
|
29
|
-
const ROLE_CAPABILITIES = {
|
|
30
|
-
cursor_agent: ['candidate:create', 'candidate:list', 'candidate:search', 'recipe:list', 'recipe:search', 'recipe:get', 'recipe:guard_check', 'search:query'],
|
|
31
|
-
asd_ais: ['candidate:create', 'candidate:list', 'candidate:search', 'recipe:list', 'recipe:search', 'search:query'],
|
|
32
|
-
guard_engine: ['recipe:guard_check', 'recipe:list', 'recipe:get'],
|
|
33
|
-
developer_admin: ['*'],
|
|
34
|
-
developer_contributor: ['candidate:create', 'candidate:list', 'candidate:approve', 'candidate:reject', 'recipe:create', 'recipe:list', 'recipe:search', 'recipe:publish', 'recipe:guard_create', 'search:query'],
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export class RoleDriftMonitor {
|
|
38
|
-
constructor(db) {
|
|
39
|
-
this.db = typeof db?.getDb === 'function' ? db.getDb() : db;
|
|
40
|
-
this.logger = Logger.getInstance();
|
|
41
|
-
this.sessionRoles = new Map(); // sessionId → { currentRole, history[], driftCount }
|
|
42
|
-
this._ensureTable();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
_ensureTable() {
|
|
46
|
-
this.db.exec(`
|
|
47
|
-
CREATE TABLE IF NOT EXISTS role_drift_events (
|
|
48
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
-
session_id TEXT NOT NULL,
|
|
50
|
-
actor TEXT NOT NULL,
|
|
51
|
-
from_role TEXT,
|
|
52
|
-
to_role TEXT,
|
|
53
|
-
action_attempted TEXT,
|
|
54
|
-
drift_type TEXT NOT NULL,
|
|
55
|
-
severity TEXT NOT NULL DEFAULT 'warning',
|
|
56
|
-
details_json TEXT DEFAULT '{}',
|
|
57
|
-
created_at INTEGER NOT NULL
|
|
58
|
-
)
|
|
59
|
-
`);
|
|
60
|
-
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_drift_session ON role_drift_events(session_id)`);
|
|
61
|
-
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_drift_actor ON role_drift_events(actor)`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 检查角色行为是否合法
|
|
66
|
-
* @param {string} actor - 角色 ID
|
|
67
|
-
* @param {string} action - 尝试执行的操作
|
|
68
|
-
* @param {string} sessionId - 会话 ID
|
|
69
|
-
* @returns {{ allowed: boolean, drift?: object }}
|
|
70
|
-
*/
|
|
71
|
-
checkAction(actor, action, sessionId) {
|
|
72
|
-
// 获取角色能力
|
|
73
|
-
const capabilities = ROLE_CAPABILITIES[actor];
|
|
74
|
-
if (!capabilities) {
|
|
75
|
-
const drift = this._recordDrift(sessionId, actor, null, null, action, 'unknown_role', 'error');
|
|
76
|
-
return { allowed: false, drift };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 通配符 (admin)
|
|
80
|
-
if (capabilities.includes('*')) {
|
|
81
|
-
return { allowed: true };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 检查 action 是否在角色能力范围内
|
|
85
|
-
const isAllowed = capabilities.some(cap => {
|
|
86
|
-
if (cap === action) return true;
|
|
87
|
-
// 前缀匹配: 'candidate:*' 匹配 'candidate:create'
|
|
88
|
-
if (cap.endsWith(':*')) {
|
|
89
|
-
return action.startsWith(cap.slice(0, -1));
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (!isAllowed) {
|
|
95
|
-
const drift = this._recordDrift(
|
|
96
|
-
sessionId, actor, actor, actor, action,
|
|
97
|
-
'capability_violation', 'warning'
|
|
98
|
-
);
|
|
99
|
-
return { allowed: false, drift };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { allowed: true };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 检查角色转移是否合法
|
|
107
|
-
* @param {string} sessionId - 会话 ID
|
|
108
|
-
* @param {string} currentRole - 当前角色
|
|
109
|
-
* @param {string} nextRole - 目标角色
|
|
110
|
-
* @returns {{ allowed: boolean, drift?: object }}
|
|
111
|
-
*/
|
|
112
|
-
checkRoleTransition(sessionId, currentRole, nextRole) {
|
|
113
|
-
if (currentRole === nextRole) return { allowed: true };
|
|
114
|
-
|
|
115
|
-
const allowed = ALLOWED_TRANSITIONS[currentRole];
|
|
116
|
-
if (!allowed || !allowed.includes(nextRole)) {
|
|
117
|
-
const drift = this._recordDrift(
|
|
118
|
-
sessionId, currentRole, currentRole, nextRole, null,
|
|
119
|
-
'illegal_transition', 'error'
|
|
120
|
-
);
|
|
121
|
-
return { allowed: false, drift };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { allowed: true };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 获取会话的漂移统计
|
|
129
|
-
*/
|
|
130
|
-
getSessionDriftStats(sessionId) {
|
|
131
|
-
const events = this.db.prepare(
|
|
132
|
-
`SELECT drift_type, severity, COUNT(*) as count FROM role_drift_events WHERE session_id = ? GROUP BY drift_type, severity`
|
|
133
|
-
).all(sessionId);
|
|
134
|
-
|
|
135
|
-
const total = events.reduce((sum, e) => sum + e.count, 0);
|
|
136
|
-
const errorCount = events.filter(e => e.severity === 'error').reduce((s, e) => s + e.count, 0);
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
sessionId,
|
|
140
|
-
totalDrifts: total,
|
|
141
|
-
errorDrifts: errorCount,
|
|
142
|
-
warningDrifts: total - errorCount,
|
|
143
|
-
stability: total === 0 ? 1.0 : Math.max(0, 1 - total * 0.1),
|
|
144
|
-
byType: events,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* 获取全局漂移统计
|
|
150
|
-
*/
|
|
151
|
-
getGlobalStats(since = null) {
|
|
152
|
-
const params = [];
|
|
153
|
-
let where = '1=1';
|
|
154
|
-
if (since) {
|
|
155
|
-
where = 'created_at >= ?';
|
|
156
|
-
params.push(since);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const stats = this.db.prepare(`
|
|
160
|
-
SELECT
|
|
161
|
-
actor,
|
|
162
|
-
drift_type,
|
|
163
|
-
severity,
|
|
164
|
-
COUNT(*) as count
|
|
165
|
-
FROM role_drift_events
|
|
166
|
-
WHERE ${where}
|
|
167
|
-
GROUP BY actor, drift_type, severity
|
|
168
|
-
ORDER BY count DESC
|
|
169
|
-
`).all(...params);
|
|
170
|
-
|
|
171
|
-
const total = this.db.prepare(
|
|
172
|
-
`SELECT COUNT(*) as total FROM role_drift_events WHERE ${where}`
|
|
173
|
-
).get(...params);
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
totalDrifts: total.total,
|
|
177
|
-
byActor: stats,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* 获取最近的漂移事件
|
|
183
|
-
*/
|
|
184
|
-
getRecentDrifts(limit = 20) {
|
|
185
|
-
const rows = this.db.prepare(
|
|
186
|
-
`SELECT * FROM role_drift_events ORDER BY created_at DESC LIMIT ?`
|
|
187
|
-
).all(limit);
|
|
188
|
-
|
|
189
|
-
return rows.map(r => ({
|
|
190
|
-
id: r.id,
|
|
191
|
-
sessionId: r.session_id,
|
|
192
|
-
actor: r.actor,
|
|
193
|
-
fromRole: r.from_role,
|
|
194
|
-
toRole: r.to_role,
|
|
195
|
-
actionAttempted: r.action_attempted,
|
|
196
|
-
driftType: r.drift_type,
|
|
197
|
-
severity: r.severity,
|
|
198
|
-
details: JSON.parse(r.details_json || '{}'),
|
|
199
|
-
createdAt: r.created_at,
|
|
200
|
-
}));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ========== Private ==========
|
|
204
|
-
|
|
205
|
-
_recordDrift(sessionId, actor, fromRole, toRole, action, driftType, severity) {
|
|
206
|
-
const now = Math.floor(Date.now() / 1000);
|
|
207
|
-
const details = { timestamp: now };
|
|
208
|
-
|
|
209
|
-
this.db.prepare(`
|
|
210
|
-
INSERT INTO role_drift_events (session_id, actor, from_role, to_role, action_attempted, drift_type, severity, details_json, created_at)
|
|
211
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
212
|
-
`).run(sessionId || 'unknown', actor, fromRole, toRole, action, driftType, severity, JSON.stringify(details), now);
|
|
213
|
-
|
|
214
|
-
this.logger.warn('Role drift detected', {
|
|
215
|
-
sessionId, actor, fromRole, toRole, action, driftType, severity,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
return { driftType, severity, actor, fromRole, toRole, action };
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Gateway 插件:角色漂移检测
|
|
224
|
-
*/
|
|
225
|
-
export function createRoleDriftPlugin(roleDriftMonitor) {
|
|
226
|
-
return {
|
|
227
|
-
name: 'RoleDriftPlugin',
|
|
228
|
-
async pre(context) {
|
|
229
|
-
// Monitor-only: log drift events but don't block
|
|
230
|
-
// Authorization is handled by PermissionManager
|
|
231
|
-
const result = roleDriftMonitor.checkAction(
|
|
232
|
-
context.actor,
|
|
233
|
-
context.action,
|
|
234
|
-
context.session || 'default'
|
|
235
|
-
);
|
|
236
|
-
if (!result.allowed && result.drift) {
|
|
237
|
-
// 仅记录漂移事件,不阻断请求
|
|
238
|
-
roleDriftMonitor.logger.warn('Role drift detected', {
|
|
239
|
-
actor: context.actor,
|
|
240
|
-
action: context.action,
|
|
241
|
-
driftType: result.drift.driftType,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
let instance = null;
|
|
249
|
-
|
|
250
|
-
export function initRoleDriftMonitor(db) {
|
|
251
|
-
instance = new RoleDriftMonitor(db);
|
|
252
|
-
return instance;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export function getRoleDriftMonitor() {
|
|
256
|
-
return instance;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export default RoleDriftMonitor;
|