@pixelbyte-software/pixcode 1.44.0 → 1.45.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/dist/assets/{index-BsxRTx-l.js → index-ChGWpJff.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/server/index.js +3 -0
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/routes/platformization.js +95 -0
- package/dist-server/server/routes/platformization.js.map +1 -0
- package/dist-server/server/services/platformization.js +441 -0
- package/dist-server/server/services/platformization.js.map +1 -0
- package/package.json +1 -1
- package/scripts/smoke/v145-platformization.mjs +46 -0
- package/server/index.js +4 -0
- package/server/routes/platformization.js +129 -0
- package/server/services/platformization.js +460 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createEvaluationRun,
|
|
5
|
+
createEvaluationSuite,
|
|
6
|
+
createSecret,
|
|
7
|
+
createSecurityAuditRun,
|
|
8
|
+
createTeamMember,
|
|
9
|
+
getPlatformizationState,
|
|
10
|
+
listSecrets,
|
|
11
|
+
materializeScopedEnv,
|
|
12
|
+
recordUsageEvent,
|
|
13
|
+
summarizeUsageEvents,
|
|
14
|
+
updateMarketplacePluginHealth,
|
|
15
|
+
updateTeamMember,
|
|
16
|
+
upsertMarketplacePlugin,
|
|
17
|
+
} from '../services/platformization.js';
|
|
18
|
+
|
|
19
|
+
const router = express.Router();
|
|
20
|
+
|
|
21
|
+
function userId(req) {
|
|
22
|
+
return req.user?.id ?? req.user?.userId ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleError(res, error) {
|
|
26
|
+
res.status(400).json({ success: false, error: error.message });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
router.get('/', (_req, res) => {
|
|
30
|
+
res.json({ success: true, state: getPlatformizationState() });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
router.get('/roles', (_req, res) => {
|
|
34
|
+
const state = getPlatformizationState();
|
|
35
|
+
res.json({ success: true, roles: state.roles });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
router.get('/team/members', (_req, res) => {
|
|
39
|
+
res.json({ success: true, members: getPlatformizationState().teamMembers });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
router.post('/team/members', (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
res.status(201).json({ success: true, member: createTeamMember(req.body || {}, userId(req)) });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
handleError(res, error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
router.patch('/team/members/:id', (req, res) => {
|
|
51
|
+
const member = updateTeamMember(req.params.id, req.body || {}, userId(req));
|
|
52
|
+
if (!member) {
|
|
53
|
+
res.status(404).json({ success: false, error: 'Team member not found.' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
res.json({ success: true, member });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
router.get('/secrets', (_req, res) => {
|
|
60
|
+
res.json({ success: true, secrets: listSecrets() });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
router.post('/secrets', (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
res.status(201).json({ success: true, secret: createSecret(req.body || {}, userId(req)) });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
handleError(res, error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
router.post('/secrets/scoped-env', (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
res.json({ success: true, scopedEnv: materializeScopedEnv(req.body || {}, { reveal: req.body?.reveal === true }) });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
handleError(res, error);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
router.get('/marketplace/plugins', (_req, res) => {
|
|
80
|
+
res.json({ success: true, plugins: getPlatformizationState().marketplacePlugins });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
router.post('/marketplace/plugins', (req, res) => {
|
|
84
|
+
res.status(201).json({ success: true, plugin: upsertMarketplacePlugin(req.body || {}, userId(req)) });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
router.post('/marketplace/plugins/:id/health', (req, res) => {
|
|
88
|
+
const plugin = updateMarketplacePluginHealth(req.params.id, req.body || {}, userId(req));
|
|
89
|
+
if (!plugin) {
|
|
90
|
+
res.status(404).json({ success: false, error: 'Marketplace plugin not found.' });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
res.json({ success: true, plugin });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
router.get('/eval/suites', (_req, res) => {
|
|
97
|
+
const state = getPlatformizationState();
|
|
98
|
+
res.json({ success: true, suites: state.evaluationSuites, runs: state.evaluationRuns });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
router.post('/eval/suites', (req, res) => {
|
|
102
|
+
res.status(201).json({ success: true, suite: createEvaluationSuite(req.body || {}, userId(req)) });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
router.post('/eval/runs', (req, res) => {
|
|
106
|
+
res.status(201).json({ success: true, run: createEvaluationRun(req.body || {}, userId(req)) });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
router.get('/usage/summary', (_req, res) => {
|
|
110
|
+
res.json({ success: true, summary: summarizeUsageEvents() });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
router.post('/usage/events', (req, res) => {
|
|
114
|
+
res.status(201).json({ success: true, event: recordUsageEvent(req.body || {}, userId(req)) });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
router.get('/security/audit-runs', (_req, res) => {
|
|
118
|
+
res.json({ success: true, runs: getPlatformizationState().securityAuditRuns });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
router.post('/security/audit-runs', (req, res) => {
|
|
122
|
+
res.status(201).json({ success: true, run: createSecurityAuditRun(req.body || {}, userId(req)) });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
router.get('/audit-log', (_req, res) => {
|
|
126
|
+
res.json({ success: true, auditLog: getPlatformizationState().auditLog });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export default router;
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { appConfigDb } from '../database/db.js';
|
|
4
|
+
|
|
5
|
+
const CONFIG_KEY = 'platformization';
|
|
6
|
+
|
|
7
|
+
export const TEAM_ROLES = {
|
|
8
|
+
owner: [
|
|
9
|
+
'team:manage',
|
|
10
|
+
'project:admin',
|
|
11
|
+
'run:approve',
|
|
12
|
+
'secret:manage',
|
|
13
|
+
'marketplace:manage',
|
|
14
|
+
'eval:run',
|
|
15
|
+
'usage:view',
|
|
16
|
+
'security:audit',
|
|
17
|
+
],
|
|
18
|
+
admin: [
|
|
19
|
+
'project:admin',
|
|
20
|
+
'run:approve',
|
|
21
|
+
'secret:manage',
|
|
22
|
+
'marketplace:manage',
|
|
23
|
+
'eval:run',
|
|
24
|
+
'usage:view',
|
|
25
|
+
'security:audit',
|
|
26
|
+
],
|
|
27
|
+
member: [
|
|
28
|
+
'project:write',
|
|
29
|
+
'run:create',
|
|
30
|
+
'secret:use',
|
|
31
|
+
'eval:run',
|
|
32
|
+
'usage:view',
|
|
33
|
+
],
|
|
34
|
+
viewer: [
|
|
35
|
+
'project:read',
|
|
36
|
+
'usage:view',
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const SECRET_SCOPES = ['global', 'provider', 'project', 'workflow', 'telegram', 'api'];
|
|
41
|
+
|
|
42
|
+
export const MARKETPLACE_PLUGIN_TYPES = ['mcp-server', 'workflow-template', 'provider-adapter', 'notification-channel'];
|
|
43
|
+
|
|
44
|
+
export const SECURITY_AUDIT_CHECKS = [
|
|
45
|
+
'dependency_audit',
|
|
46
|
+
'secret_scan',
|
|
47
|
+
'permission_audit',
|
|
48
|
+
'agent_output_leak_detection',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function nowIso() {
|
|
52
|
+
return new Date().toISOString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function emptyStore() {
|
|
56
|
+
return {
|
|
57
|
+
teamMembers: [],
|
|
58
|
+
secrets: [],
|
|
59
|
+
marketplacePlugins: [],
|
|
60
|
+
evaluationSuites: [],
|
|
61
|
+
evaluationRuns: [],
|
|
62
|
+
usageEvents: [],
|
|
63
|
+
securityAuditRuns: [],
|
|
64
|
+
auditLog: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readStore() {
|
|
69
|
+
const raw = appConfigDb.get(CONFIG_KEY);
|
|
70
|
+
if (!raw) return emptyStore();
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
return {
|
|
74
|
+
teamMembers: Array.isArray(parsed.teamMembers) ? parsed.teamMembers : [],
|
|
75
|
+
secrets: Array.isArray(parsed.secrets) ? parsed.secrets : [],
|
|
76
|
+
marketplacePlugins: Array.isArray(parsed.marketplacePlugins) ? parsed.marketplacePlugins : [],
|
|
77
|
+
evaluationSuites: Array.isArray(parsed.evaluationSuites) ? parsed.evaluationSuites : [],
|
|
78
|
+
evaluationRuns: Array.isArray(parsed.evaluationRuns) ? parsed.evaluationRuns : [],
|
|
79
|
+
usageEvents: Array.isArray(parsed.usageEvents) ? parsed.usageEvents : [],
|
|
80
|
+
securityAuditRuns: Array.isArray(parsed.securityAuditRuns) ? parsed.securityAuditRuns : [],
|
|
81
|
+
auditLog: Array.isArray(parsed.auditLog) ? parsed.auditLog : [],
|
|
82
|
+
};
|
|
83
|
+
} catch {
|
|
84
|
+
return emptyStore();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeStore(store) {
|
|
89
|
+
appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function compact(text, max = 120) {
|
|
93
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
94
|
+
return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function slugify(value) {
|
|
98
|
+
const slug = compact(value, 72)
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
101
|
+
.replace(/^-+|-+$/g, '');
|
|
102
|
+
return slug || crypto.randomUUID();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function addAudit(store, action, actorId, details = {}) {
|
|
106
|
+
store.auditLog.unshift({
|
|
107
|
+
id: crypto.randomUUID(),
|
|
108
|
+
action,
|
|
109
|
+
actorId: actorId || null,
|
|
110
|
+
createdAt: nowIso(),
|
|
111
|
+
details,
|
|
112
|
+
});
|
|
113
|
+
store.auditLog = store.auditLog.slice(0, 250);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeRole(role) {
|
|
117
|
+
return TEAM_ROLES[role] ? role : 'viewer';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeScope(scope) {
|
|
121
|
+
return SECRET_SCOPES.includes(scope) ? scope : 'project';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function vaultKey() {
|
|
125
|
+
const material = process.env.PIXCODE_SECRET_KEY || process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
|
|
126
|
+
return crypto.createHash('sha256').update(material).digest();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function sealSecret(value) {
|
|
130
|
+
const iv = crypto.randomBytes(12);
|
|
131
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', vaultKey(), iv);
|
|
132
|
+
const encrypted = Buffer.concat([cipher.update(String(value || ''), 'utf8'), cipher.final()]);
|
|
133
|
+
return {
|
|
134
|
+
algorithm: 'aes-256-gcm',
|
|
135
|
+
iv: iv.toString('base64'),
|
|
136
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
137
|
+
ciphertext: encrypted.toString('base64'),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function openSecret(sealed) {
|
|
142
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', vaultKey(), Buffer.from(sealed.iv, 'base64'));
|
|
143
|
+
decipher.setAuthTag(Buffer.from(sealed.tag, 'base64'));
|
|
144
|
+
return Buffer.concat([
|
|
145
|
+
decipher.update(Buffer.from(sealed.ciphertext, 'base64')),
|
|
146
|
+
decipher.final(),
|
|
147
|
+
]).toString('utf8');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function redactSecret(secret) {
|
|
151
|
+
return {
|
|
152
|
+
...secret,
|
|
153
|
+
sealedValue: undefined,
|
|
154
|
+
redacted: '********',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function scopeMatches(secret, input = {}) {
|
|
159
|
+
if (secret.scope === 'global') return true;
|
|
160
|
+
if (secret.scope === 'provider') return !input.provider || secret.target === input.provider;
|
|
161
|
+
if (secret.scope === 'project') return !input.projectPath || secret.target === input.projectPath || secret.target === input.projectName;
|
|
162
|
+
if (secret.scope === 'workflow') return !input.workflowId || secret.target === input.workflowId;
|
|
163
|
+
if (secret.scope === 'telegram') return input.channel === 'telegram';
|
|
164
|
+
if (secret.scope === 'api') return input.channel === 'api';
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function getPlatformizationState() {
|
|
169
|
+
const store = readStore();
|
|
170
|
+
return {
|
|
171
|
+
roles: TEAM_ROLES,
|
|
172
|
+
secretScopes: SECRET_SCOPES,
|
|
173
|
+
marketplacePluginTypes: MARKETPLACE_PLUGIN_TYPES,
|
|
174
|
+
securityAuditChecks: SECURITY_AUDIT_CHECKS,
|
|
175
|
+
teamMembers: store.teamMembers,
|
|
176
|
+
secrets: store.secrets.map(redactSecret),
|
|
177
|
+
marketplacePlugins: store.marketplacePlugins,
|
|
178
|
+
evaluationSuites: store.evaluationSuites,
|
|
179
|
+
evaluationRuns: store.evaluationRuns,
|
|
180
|
+
usageSummary: summarizeUsageEvents(store.usageEvents),
|
|
181
|
+
securityAuditRuns: store.securityAuditRuns,
|
|
182
|
+
auditLog: store.auditLog,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function createTeamMember(input = {}, actorId = null) {
|
|
187
|
+
const email = compact(input.email || input.username || '');
|
|
188
|
+
if (!email) throw new Error('Team member email or username is required.');
|
|
189
|
+
const store = readStore();
|
|
190
|
+
const member = {
|
|
191
|
+
id: crypto.randomUUID(),
|
|
192
|
+
email,
|
|
193
|
+
displayName: compact(input.displayName || email, 80),
|
|
194
|
+
role: normalizeRole(input.role || 'viewer'),
|
|
195
|
+
projectScopes: Array.isArray(input.projectScopes) ? input.projectScopes : [],
|
|
196
|
+
status: input.status || 'active',
|
|
197
|
+
createdAt: nowIso(),
|
|
198
|
+
updatedAt: nowIso(),
|
|
199
|
+
};
|
|
200
|
+
member.permissions = TEAM_ROLES[member.role];
|
|
201
|
+
store.teamMembers.unshift(member);
|
|
202
|
+
addAudit(store, 'team.member.created', actorId, { memberId: member.id, role: member.role });
|
|
203
|
+
writeStore(store);
|
|
204
|
+
return member;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function updateTeamMember(memberId, patch = {}, actorId = null) {
|
|
208
|
+
const store = readStore();
|
|
209
|
+
let updated = null;
|
|
210
|
+
store.teamMembers = store.teamMembers.map((member) => {
|
|
211
|
+
if (member.id !== memberId) return member;
|
|
212
|
+
updated = {
|
|
213
|
+
...member,
|
|
214
|
+
...patch,
|
|
215
|
+
id: member.id,
|
|
216
|
+
role: normalizeRole(patch.role || member.role),
|
|
217
|
+
updatedAt: nowIso(),
|
|
218
|
+
};
|
|
219
|
+
updated.permissions = TEAM_ROLES[updated.role];
|
|
220
|
+
return updated;
|
|
221
|
+
});
|
|
222
|
+
if (updated) {
|
|
223
|
+
addAudit(store, 'team.member.updated', actorId, { memberId, role: updated.role });
|
|
224
|
+
writeStore(store);
|
|
225
|
+
}
|
|
226
|
+
return updated;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function createSecret(input = {}, actorId = null) {
|
|
230
|
+
const name = compact(input.name || input.envName || '');
|
|
231
|
+
const value = input.value;
|
|
232
|
+
if (!name || typeof value !== 'string') throw new Error('Secret name and string value are required.');
|
|
233
|
+
const scope = normalizeScope(input.scope || 'project');
|
|
234
|
+
const store = readStore();
|
|
235
|
+
const secret = {
|
|
236
|
+
id: crypto.randomUUID(),
|
|
237
|
+
name,
|
|
238
|
+
envName: compact(input.envName || name).replace(/[^A-Z0-9_]/gi, '_').toUpperCase(),
|
|
239
|
+
scope,
|
|
240
|
+
target: input.target || input.projectPath || input.provider || null,
|
|
241
|
+
createdAt: nowIso(),
|
|
242
|
+
updatedAt: nowIso(),
|
|
243
|
+
fingerprint: crypto.createHash('sha256').update(value).digest('hex').slice(0, 12),
|
|
244
|
+
sealedValue: sealSecret(value),
|
|
245
|
+
};
|
|
246
|
+
store.secrets = store.secrets.filter((existing) => !(existing.envName === secret.envName && existing.scope === secret.scope && existing.target === secret.target));
|
|
247
|
+
store.secrets.unshift(secret);
|
|
248
|
+
addAudit(store, 'secret.created', actorId, { secretId: secret.id, scope: secret.scope, envName: secret.envName });
|
|
249
|
+
writeStore(store);
|
|
250
|
+
return redactSecret(secret);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function listSecrets() {
|
|
254
|
+
return readStore().secrets.map(redactSecret);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function materializeScopedEnv(input = {}, options = {}) {
|
|
258
|
+
const store = readStore();
|
|
259
|
+
const env = {};
|
|
260
|
+
const included = [];
|
|
261
|
+
for (const secret of store.secrets) {
|
|
262
|
+
if (!scopeMatches(secret, input)) continue;
|
|
263
|
+
included.push({
|
|
264
|
+
id: secret.id,
|
|
265
|
+
envName: secret.envName,
|
|
266
|
+
scope: secret.scope,
|
|
267
|
+
target: secret.target,
|
|
268
|
+
redacted: '********',
|
|
269
|
+
});
|
|
270
|
+
if (options.reveal === true) {
|
|
271
|
+
env[secret.envName] = openSecret(secret.sealedValue);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { env, included };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function upsertMarketplacePlugin(input = {}, actorId = null) {
|
|
278
|
+
const pluginId = input.id || slugify(input.name || input.packageName || 'plugin');
|
|
279
|
+
const store = readStore();
|
|
280
|
+
const existing = store.marketplacePlugins.find((plugin) => plugin.id === pluginId);
|
|
281
|
+
const plugin = {
|
|
282
|
+
id: pluginId,
|
|
283
|
+
name: compact(input.name || pluginId, 100),
|
|
284
|
+
type: MARKETPLACE_PLUGIN_TYPES.includes(input.type) ? input.type : 'mcp-server',
|
|
285
|
+
source: input.source || input.packageName || input.repository || null,
|
|
286
|
+
permissionScopes: Array.isArray(input.permissionScopes) ? input.permissionScopes : [],
|
|
287
|
+
installCommand: input.installCommand || null,
|
|
288
|
+
status: input.status || existing?.status || 'available',
|
|
289
|
+
health: input.health || existing?.health || { status: 'unknown', checkedAt: null },
|
|
290
|
+
updatedAt: nowIso(),
|
|
291
|
+
createdAt: existing?.createdAt || nowIso(),
|
|
292
|
+
};
|
|
293
|
+
store.marketplacePlugins = [plugin, ...store.marketplacePlugins.filter((item) => item.id !== pluginId)];
|
|
294
|
+
addAudit(store, 'marketplace.plugin.upserted', actorId, { pluginId, type: plugin.type });
|
|
295
|
+
writeStore(store);
|
|
296
|
+
return plugin;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function updateMarketplacePluginHealth(pluginId, health = {}, actorId = null) {
|
|
300
|
+
const store = readStore();
|
|
301
|
+
let updated = null;
|
|
302
|
+
store.marketplacePlugins = store.marketplacePlugins.map((plugin) => {
|
|
303
|
+
if (plugin.id !== pluginId) return plugin;
|
|
304
|
+
updated = {
|
|
305
|
+
...plugin,
|
|
306
|
+
health: {
|
|
307
|
+
status: health.status || 'unknown',
|
|
308
|
+
message: health.message || '',
|
|
309
|
+
checkedAt: nowIso(),
|
|
310
|
+
},
|
|
311
|
+
updatedAt: nowIso(),
|
|
312
|
+
};
|
|
313
|
+
return updated;
|
|
314
|
+
});
|
|
315
|
+
if (updated) {
|
|
316
|
+
addAudit(store, 'marketplace.plugin.health_checked', actorId, { pluginId, status: updated.health.status });
|
|
317
|
+
writeStore(store);
|
|
318
|
+
}
|
|
319
|
+
return updated;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function createEvaluationSuite(input = {}, actorId = null) {
|
|
323
|
+
const tasks = Array.isArray(input.tasks) ? input.tasks : [];
|
|
324
|
+
const suite = {
|
|
325
|
+
id: input.id || slugify(input.name || 'evaluation-suite'),
|
|
326
|
+
name: compact(input.name || 'Evaluation suite', 100),
|
|
327
|
+
description: compact(input.description || '', 240),
|
|
328
|
+
tasks: tasks.map((task, index) => ({
|
|
329
|
+
id: task.id || `task-${index + 1}`,
|
|
330
|
+
title: compact(task.title || `Task ${index + 1}`, 120),
|
|
331
|
+
acceptanceCriteria: Array.isArray(task.acceptanceCriteria) ? task.acceptanceCriteria : [],
|
|
332
|
+
projectPath: task.projectPath || null,
|
|
333
|
+
})),
|
|
334
|
+
createdAt: nowIso(),
|
|
335
|
+
updatedAt: nowIso(),
|
|
336
|
+
};
|
|
337
|
+
const store = readStore();
|
|
338
|
+
store.evaluationSuites = [suite, ...store.evaluationSuites.filter((item) => item.id !== suite.id)];
|
|
339
|
+
addAudit(store, 'eval.suite.upserted', actorId, { suiteId: suite.id, tasks: suite.tasks.length });
|
|
340
|
+
writeStore(store);
|
|
341
|
+
return suite;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function createEvaluationRun(input = {}, actorId = null) {
|
|
345
|
+
const results = Array.isArray(input.results) ? input.results : [];
|
|
346
|
+
const passed = results.filter((result) => result.status === 'passed').length;
|
|
347
|
+
const run = {
|
|
348
|
+
id: crypto.randomUUID(),
|
|
349
|
+
suiteId: input.suiteId || null,
|
|
350
|
+
provider: input.provider || null,
|
|
351
|
+
model: input.model || null,
|
|
352
|
+
status: input.status || 'completed',
|
|
353
|
+
createdAt: nowIso(),
|
|
354
|
+
results,
|
|
355
|
+
summary: {
|
|
356
|
+
total: results.length,
|
|
357
|
+
passed,
|
|
358
|
+
failed: results.filter((result) => result.status === 'failed').length,
|
|
359
|
+
passRate: results.length ? Math.round((passed / results.length) * 1000) / 10 : 0,
|
|
360
|
+
averageLatencyMs: average(results.map((result) => Number(result.latencyMs || 0)).filter(Boolean)),
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
const store = readStore();
|
|
364
|
+
store.evaluationRuns.unshift(run);
|
|
365
|
+
addAudit(store, 'eval.run.created', actorId, { runId: run.id, suiteId: run.suiteId, passRate: run.summary.passRate });
|
|
366
|
+
writeStore(store);
|
|
367
|
+
return run;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function average(values) {
|
|
371
|
+
if (!values.length) return 0;
|
|
372
|
+
return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function recordUsageEvent(input = {}, actorId = null) {
|
|
376
|
+
const event = {
|
|
377
|
+
id: crypto.randomUUID(),
|
|
378
|
+
createdAt: input.createdAt || nowIso(),
|
|
379
|
+
provider: input.provider || 'unknown',
|
|
380
|
+
model: input.model || 'unknown',
|
|
381
|
+
workflow: input.workflow || input.source || 'manual',
|
|
382
|
+
inputTokens: Number(input.inputTokens || 0),
|
|
383
|
+
outputTokens: Number(input.outputTokens || 0),
|
|
384
|
+
costUsd: Number(input.costUsd || 0),
|
|
385
|
+
latencyMs: Number(input.latencyMs || 0),
|
|
386
|
+
status: input.status || 'ok',
|
|
387
|
+
};
|
|
388
|
+
const store = readStore();
|
|
389
|
+
store.usageEvents.unshift(event);
|
|
390
|
+
store.usageEvents = store.usageEvents.slice(0, 2000);
|
|
391
|
+
addAudit(store, 'usage.event.recorded', actorId, { provider: event.provider, model: event.model, status: event.status });
|
|
392
|
+
writeStore(store);
|
|
393
|
+
return event;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function summarizeUsageEvents(events = readStore().usageEvents) {
|
|
397
|
+
const groups = new Map();
|
|
398
|
+
for (const event of events) {
|
|
399
|
+
const key = `${event.provider}:${event.model}:${event.workflow}`;
|
|
400
|
+
const current = groups.get(key) || {
|
|
401
|
+
provider: event.provider,
|
|
402
|
+
model: event.model,
|
|
403
|
+
workflow: event.workflow,
|
|
404
|
+
runs: 0,
|
|
405
|
+
errors: 0,
|
|
406
|
+
inputTokens: 0,
|
|
407
|
+
outputTokens: 0,
|
|
408
|
+
totalTokens: 0,
|
|
409
|
+
costUsd: 0,
|
|
410
|
+
latencyMs: 0,
|
|
411
|
+
};
|
|
412
|
+
current.runs += 1;
|
|
413
|
+
current.errors += event.status === 'error' ? 1 : 0;
|
|
414
|
+
current.inputTokens += event.inputTokens;
|
|
415
|
+
current.outputTokens += event.outputTokens;
|
|
416
|
+
current.totalTokens += event.inputTokens + event.outputTokens;
|
|
417
|
+
current.costUsd += event.costUsd;
|
|
418
|
+
current.latencyMs += event.latencyMs;
|
|
419
|
+
groups.set(key, current);
|
|
420
|
+
}
|
|
421
|
+
return Array.from(groups.values()).map((group) => ({
|
|
422
|
+
...group,
|
|
423
|
+
costUsd: Math.round(group.costUsd * 10000) / 10000,
|
|
424
|
+
averageLatencyMs: group.runs ? Math.round(group.latencyMs / group.runs) : 0,
|
|
425
|
+
errorRate: group.runs ? Math.round((group.errors / group.runs) * 1000) / 10 : 0,
|
|
426
|
+
latencyMs: undefined,
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function createSecurityAuditRun(input = {}, actorId = null) {
|
|
431
|
+
const checks = Array.isArray(input.checks) && input.checks.length
|
|
432
|
+
? input.checks.filter((check) => SECURITY_AUDIT_CHECKS.includes(check))
|
|
433
|
+
: SECURITY_AUDIT_CHECKS;
|
|
434
|
+
const findings = Array.isArray(input.findings) ? input.findings : [];
|
|
435
|
+
const run = {
|
|
436
|
+
id: crypto.randomUUID(),
|
|
437
|
+
protocol: 'pixcode.security-audit.v1',
|
|
438
|
+
status: input.status || 'queued',
|
|
439
|
+
projectName: input.projectName || null,
|
|
440
|
+
projectPath: input.projectPath || null,
|
|
441
|
+
checks,
|
|
442
|
+
createdAt: nowIso(),
|
|
443
|
+
findings: findings.map((finding, index) => ({
|
|
444
|
+
id: finding.id || `finding-${index + 1}`,
|
|
445
|
+
severity: finding.severity || 'medium',
|
|
446
|
+
title: compact(finding.title || 'Security finding', 140),
|
|
447
|
+
file: finding.file || null,
|
|
448
|
+
recommendation: finding.recommendation || null,
|
|
449
|
+
})),
|
|
450
|
+
checklist: checks.map((check) => ({
|
|
451
|
+
check,
|
|
452
|
+
status: 'pending',
|
|
453
|
+
})),
|
|
454
|
+
};
|
|
455
|
+
const store = readStore();
|
|
456
|
+
store.securityAuditRuns.unshift(run);
|
|
457
|
+
addAudit(store, 'security.audit.created', actorId, { runId: run.id, checks });
|
|
458
|
+
writeStore(store);
|
|
459
|
+
return run;
|
|
460
|
+
}
|