feat(actions): config-driven action whitelist registry
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
29
lib/actions/registry.js
Normal file
29
lib/actions/registry.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const DEFAULT = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../config/actions.json');
|
||||
const ID_RE = /^[a-z0-9-]+$/;
|
||||
const KINDS = new Set(['service_restart', 'guest_power']);
|
||||
const OPS = new Set(['start', 'stop', 'shutdown', 'reboot']);
|
||||
const TIERS = new Set(['safe', 'risky']);
|
||||
|
||||
// Loads + validates the action whitelist. `raw` overrides file read (tests).
|
||||
export function loadActions(file = DEFAULT, raw) {
|
||||
const cfg = raw || JSON.parse(readFileSync(file, 'utf8'));
|
||||
const hosts = cfg.hosts || {};
|
||||
const byId = new Map();
|
||||
for (const a of (cfg.actions || [])) {
|
||||
if (!ID_RE.test(a.id || '')) throw new Error(`invalid action id: ${a.id}`);
|
||||
if (!KINDS.has(a.kind)) throw new Error(`unknown kind: ${a.kind}`);
|
||||
if (!TIERS.has(a.tier)) throw new Error(`invalid tier: ${a.tier}`);
|
||||
if (a.kind === 'guest_power' && !OPS.has(a.op)) throw new Error(`invalid op: ${a.op}`);
|
||||
if (byId.has(a.id)) throw new Error(`duplicate action id: ${a.id}`);
|
||||
byId.set(a.id, a);
|
||||
}
|
||||
return {
|
||||
list: () => [...byId.values()],
|
||||
get: (id) => byId.get(id),
|
||||
hostIp: (h) => hosts[h]
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user