ai-control-center 1.15.2
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/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- package/templates/skill-template.md +142 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Stage Architecture — extensible pipeline hooks.
|
|
3
|
+
*
|
|
4
|
+
* Plugins can hook into any pipeline stage to run custom logic
|
|
5
|
+
* before or after the stage executes. Supports lifecycle hooks,
|
|
6
|
+
* custom commands, and event-driven execution.
|
|
7
|
+
*
|
|
8
|
+
* Plugin format in aicc.config.js:
|
|
9
|
+
* plugins: [
|
|
10
|
+
* {
|
|
11
|
+
* name: 'my-plugin',
|
|
12
|
+
* stages: ['review', 'deploy'], // which stages to hook into
|
|
13
|
+
* beforeStage: async (ctx) => { }, // runs before stage
|
|
14
|
+
* afterStage: async (ctx) => { }, // runs after stage
|
|
15
|
+
* commands: { '/mycommand': handler }, // custom Telegram/CLI commands
|
|
16
|
+
* onEvent: { 'feature_approved': handler }, // event listeners
|
|
17
|
+
* }
|
|
18
|
+
* ]
|
|
19
|
+
*
|
|
20
|
+
* Or as a path to a module:
|
|
21
|
+
* plugins: ['./plugins/my-plugin.js']
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync } from 'fs';
|
|
24
|
+
import { resolve } from 'path';
|
|
25
|
+
import { getConfig } from '../config.js';
|
|
26
|
+
import { audit, AUDIT_EVENTS } from './audit-logger.js';
|
|
27
|
+
|
|
28
|
+
// ─── Plugin Registry ─────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
let _loadedPlugins = [];
|
|
31
|
+
let _initialized = false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load and initialize all plugins from config.
|
|
35
|
+
*
|
|
36
|
+
* @returns {Array} Loaded plugin objects
|
|
37
|
+
*/
|
|
38
|
+
export async function loadPlugins() {
|
|
39
|
+
if (_initialized) return _loadedPlugins;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const config = getConfig();
|
|
43
|
+
const pluginDefs = config.plugins || [];
|
|
44
|
+
_loadedPlugins = [];
|
|
45
|
+
|
|
46
|
+
for (const def of pluginDefs) {
|
|
47
|
+
try {
|
|
48
|
+
let plugin;
|
|
49
|
+
|
|
50
|
+
if (typeof def === 'string') {
|
|
51
|
+
// Path to a plugin module
|
|
52
|
+
const pluginPath = resolve(config._root || '.', def);
|
|
53
|
+
if (!existsSync(pluginPath)) {
|
|
54
|
+
audit(AUDIT_EVENTS.PLUGIN_ERROR, { name: def, error: 'File not found' });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const mod = await import(pluginPath);
|
|
58
|
+
plugin = mod.default || mod;
|
|
59
|
+
} else if (typeof def === 'object') {
|
|
60
|
+
plugin = def;
|
|
61
|
+
} else {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate plugin
|
|
66
|
+
if (!plugin.name) {
|
|
67
|
+
plugin.name = `plugin-${_loadedPlugins.length + 1}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_loadedPlugins.push({
|
|
71
|
+
name: plugin.name,
|
|
72
|
+
stages: plugin.stages || [],
|
|
73
|
+
beforeStage: plugin.beforeStage || null,
|
|
74
|
+
afterStage: plugin.afterStage || null,
|
|
75
|
+
commands: plugin.commands || {},
|
|
76
|
+
onEvent: plugin.onEvent || {},
|
|
77
|
+
enabled: plugin.enabled !== false,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
audit(AUDIT_EVENTS.PLUGIN_LOADED, { name: plugin.name, stages: plugin.stages });
|
|
81
|
+
} catch (err) {
|
|
82
|
+
audit(AUDIT_EVENTS.PLUGIN_ERROR, {
|
|
83
|
+
name: typeof def === 'string' ? def : def?.name || 'unknown',
|
|
84
|
+
error: err.message,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_initialized = true;
|
|
90
|
+
return _loadedPlugins;
|
|
91
|
+
} catch {
|
|
92
|
+
_initialized = true;
|
|
93
|
+
return _loadedPlugins;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run beforeStage hooks for all plugins registered for this stage.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} stage - Pipeline stage name
|
|
101
|
+
* @param {object} context - Stage context (prompt, featureId, config, etc.)
|
|
102
|
+
* @returns {object} Modified context (plugins can mutate it)
|
|
103
|
+
*/
|
|
104
|
+
export async function runBeforeStage(stage, context = {}) {
|
|
105
|
+
const plugins = await loadPlugins();
|
|
106
|
+
const applicable = plugins.filter(p => p.enabled && p.beforeStage && (p.stages.length === 0 || p.stages.includes(stage)));
|
|
107
|
+
|
|
108
|
+
for (const plugin of applicable) {
|
|
109
|
+
try {
|
|
110
|
+
const result = await plugin.beforeStage({ ...context, stage, pluginName: plugin.name });
|
|
111
|
+
if (result && typeof result === 'object') {
|
|
112
|
+
Object.assign(context, result); // Allow plugins to modify context
|
|
113
|
+
}
|
|
114
|
+
audit(AUDIT_EVENTS.PLUGIN_EXECUTED, {
|
|
115
|
+
name: plugin.name,
|
|
116
|
+
hook: 'beforeStage',
|
|
117
|
+
stage,
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
audit(AUDIT_EVENTS.PLUGIN_ERROR, {
|
|
121
|
+
name: plugin.name,
|
|
122
|
+
hook: 'beforeStage',
|
|
123
|
+
stage,
|
|
124
|
+
error: err.message,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return context;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Run afterStage hooks for all plugins registered for this stage.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} stage - Pipeline stage name
|
|
136
|
+
* @param {object} context - Stage context + result from stage execution
|
|
137
|
+
* @returns {object} Modified context
|
|
138
|
+
*/
|
|
139
|
+
export async function runAfterStage(stage, context = {}) {
|
|
140
|
+
const plugins = await loadPlugins();
|
|
141
|
+
const applicable = plugins.filter(p => p.enabled && p.afterStage && (p.stages.length === 0 || p.stages.includes(stage)));
|
|
142
|
+
|
|
143
|
+
for (const plugin of applicable) {
|
|
144
|
+
try {
|
|
145
|
+
const result = await plugin.afterStage({ ...context, stage, pluginName: plugin.name });
|
|
146
|
+
if (result && typeof result === 'object') {
|
|
147
|
+
Object.assign(context, result);
|
|
148
|
+
}
|
|
149
|
+
audit(AUDIT_EVENTS.PLUGIN_EXECUTED, {
|
|
150
|
+
name: plugin.name,
|
|
151
|
+
hook: 'afterStage',
|
|
152
|
+
stage,
|
|
153
|
+
});
|
|
154
|
+
} catch (err) {
|
|
155
|
+
audit(AUDIT_EVENTS.PLUGIN_ERROR, {
|
|
156
|
+
name: plugin.name,
|
|
157
|
+
hook: 'afterStage',
|
|
158
|
+
stage,
|
|
159
|
+
error: err.message,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return context;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Fire a plugin event — runs all plugins listening for this event.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} eventName - Event name
|
|
171
|
+
* @param {object} data - Event data
|
|
172
|
+
*/
|
|
173
|
+
export async function firePluginEvent(eventName, data = {}) {
|
|
174
|
+
const plugins = await loadPlugins();
|
|
175
|
+
|
|
176
|
+
for (const plugin of plugins) {
|
|
177
|
+
if (!plugin.enabled || !plugin.onEvent[eventName]) continue;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await plugin.onEvent[eventName](data);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
audit(AUDIT_EVENTS.PLUGIN_ERROR, {
|
|
183
|
+
name: plugin.name,
|
|
184
|
+
hook: 'onEvent',
|
|
185
|
+
event: eventName,
|
|
186
|
+
error: err.message,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get all custom commands registered by plugins.
|
|
194
|
+
*
|
|
195
|
+
* @returns {object} Map of command → { pluginName, handler }
|
|
196
|
+
*/
|
|
197
|
+
export async function getPluginCommands() {
|
|
198
|
+
const plugins = await loadPlugins();
|
|
199
|
+
const commands = {};
|
|
200
|
+
|
|
201
|
+
for (const plugin of plugins) {
|
|
202
|
+
if (!plugin.enabled) continue;
|
|
203
|
+
for (const [cmd, handler] of Object.entries(plugin.commands)) {
|
|
204
|
+
commands[cmd] = { pluginName: plugin.name, handler };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return commands;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get loaded plugin info for status display.
|
|
213
|
+
*
|
|
214
|
+
* @returns {Array} Plugin metadata
|
|
215
|
+
*/
|
|
216
|
+
export function getPluginStatus() {
|
|
217
|
+
return _loadedPlugins.map(p => ({
|
|
218
|
+
name: p.name,
|
|
219
|
+
enabled: p.enabled,
|
|
220
|
+
stages: p.stages,
|
|
221
|
+
hasBeforeStage: !!p.beforeStage,
|
|
222
|
+
hasAfterStage: !!p.afterStage,
|
|
223
|
+
commands: Object.keys(p.commands),
|
|
224
|
+
events: Object.keys(p.onEvent),
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Reset plugin registry (for testing or config reload).
|
|
230
|
+
*/
|
|
231
|
+
export function resetPlugins() {
|
|
232
|
+
_loadedPlugins = [];
|
|
233
|
+
_initialized = false;
|
|
234
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const DEFAULT_RATE_LIMIT = {
|
|
2
|
+
windowMs: 60_000,
|
|
3
|
+
max: 60,
|
|
4
|
+
keyGenerator: (req) => req.ip,
|
|
5
|
+
skipPaths: ['/api/status', '/metrics'],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/** @type {Map<string, { timestamps: number[], blocked: number }>} */
|
|
9
|
+
const clients = new Map();
|
|
10
|
+
let totalBlocked = 0;
|
|
11
|
+
let cleanupTimer = null;
|
|
12
|
+
|
|
13
|
+
function startCleanup(windowMs) {
|
|
14
|
+
if (cleanupTimer) return;
|
|
15
|
+
cleanupTimer = setInterval(() => {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
for (const [key, entry] of clients) {
|
|
18
|
+
entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
|
|
19
|
+
if (entry.timestamps.length === 0) clients.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}, 60_000);
|
|
22
|
+
if (cleanupTimer.unref) cleanupTimer.unref();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createRateLimiter(options = {}) {
|
|
26
|
+
const opts = { ...DEFAULT_RATE_LIMIT, ...options };
|
|
27
|
+
const { windowMs, max, keyGenerator, skipPaths } = opts;
|
|
28
|
+
|
|
29
|
+
startCleanup(windowMs);
|
|
30
|
+
|
|
31
|
+
return function rateLimitMiddleware(req, res, next) {
|
|
32
|
+
// Skip WebSocket upgrades
|
|
33
|
+
if (req.headers.upgrade === 'websocket') return next();
|
|
34
|
+
|
|
35
|
+
// Skip configured paths
|
|
36
|
+
if (skipPaths.some((p) => req.path === p || req.path.startsWith(p + '/'))) {
|
|
37
|
+
return next();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const key = keyGenerator(req);
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
|
|
43
|
+
if (!clients.has(key)) {
|
|
44
|
+
clients.set(key, { timestamps: [], blocked: 0 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const entry = clients.get(key);
|
|
48
|
+
// Sliding window: keep only timestamps within the current window
|
|
49
|
+
entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
|
|
50
|
+
|
|
51
|
+
if (entry.timestamps.length >= max) {
|
|
52
|
+
const oldestInWindow = entry.timestamps[0];
|
|
53
|
+
const retryAfter = Math.ceil((oldestInWindow + windowMs - now) / 1000);
|
|
54
|
+
|
|
55
|
+
entry.blocked++;
|
|
56
|
+
totalBlocked++;
|
|
57
|
+
|
|
58
|
+
res.set('Retry-After', String(retryAfter));
|
|
59
|
+
return res.status(429).json({
|
|
60
|
+
error: 'Too many requests',
|
|
61
|
+
retryAfter,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
entry.timestamps.push(now);
|
|
66
|
+
return next();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getRateLimitStatus() {
|
|
71
|
+
let activeClients = 0;
|
|
72
|
+
let totalTracked = 0;
|
|
73
|
+
for (const entry of clients.values()) {
|
|
74
|
+
if (entry.timestamps.length > 0) {
|
|
75
|
+
activeClients++;
|
|
76
|
+
totalTracked += entry.timestamps.length;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
activeClients,
|
|
81
|
+
totalTracked,
|
|
82
|
+
totalBlocked,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getConfig } from '../config.js';
|
|
2
|
+
|
|
3
|
+
export const ROLES = {
|
|
4
|
+
admin: {
|
|
5
|
+
permissions: [
|
|
6
|
+
'feature', 'deploy', 'approve', 'reject', 'review', 'implement',
|
|
7
|
+
'fix', 'reset', 'cleanup', 'config', 'retry', 'dryrun', 'audit',
|
|
8
|
+
'cost', 'status', 'logs', 'docs', 'health', 'ask'
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
developer: {
|
|
12
|
+
permissions: [
|
|
13
|
+
'feature', 'review', 'implement', 'fix', 'status', 'logs',
|
|
14
|
+
'docs', 'cost', 'retry', 'dryrun', 'ask'
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
viewer: {
|
|
18
|
+
permissions: ['status', 'logs', 'docs', 'health', 'cost', 'ask']
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const PERMISSIONS = [...new Set(
|
|
23
|
+
Object.values(ROLES).flatMap(r => r.permissions)
|
|
24
|
+
)];
|
|
25
|
+
|
|
26
|
+
function getRBACConfig() {
|
|
27
|
+
try {
|
|
28
|
+
const config = getConfig();
|
|
29
|
+
return config.rbac || null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isRBACEnabled() {
|
|
36
|
+
const rbac = getRBACConfig();
|
|
37
|
+
return rbac !== null && Object.keys(rbac).length > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getUserRole(userId) {
|
|
41
|
+
if (!isRBACEnabled()) return 'admin';
|
|
42
|
+
const rbac = getRBACConfig();
|
|
43
|
+
const users = rbac.users || {};
|
|
44
|
+
return users[String(userId)] || rbac.defaultRole || 'viewer';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hasPermission(userId, action) {
|
|
48
|
+
const role = getUserRole(userId);
|
|
49
|
+
const roleDef = ROLES[role];
|
|
50
|
+
if (!roleDef) return false;
|
|
51
|
+
return roleDef.permissions.includes(action);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function requirePermission(userId, action) {
|
|
55
|
+
if (!hasPermission(userId, action)) {
|
|
56
|
+
const role = getUserRole(userId);
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Permission denied: role '${role}' cannot perform '${action}'`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function checkApiKey(apiKey) {
|
|
65
|
+
if (!isRBACEnabled()) return 'admin';
|
|
66
|
+
const rbac = getRBACConfig();
|
|
67
|
+
const apiKeys = rbac.apiKeys || {};
|
|
68
|
+
return apiKeys[apiKey] || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function listPermissions(role) {
|
|
72
|
+
const roleDef = ROLES[role];
|
|
73
|
+
return roleDef ? [...roleDef.permissions] : [];
|
|
74
|
+
}
|