- server/policy_overrides.mjs: persistent group/action override store - Group toggle: disabling a group forces all its actions to auto-deny without touching individual overrides; re-enabling restores them intact - Action overrides: cycle auto-accept/queue/auto-deny/clear per action - Effective policy resolution in index.mjs: group > action override > hardcoded default - auth.mjs: add check_can_manage_policies (requires canApprove.length > 0) - Three new endpoints: GET /policies, POST /policies/group/:group, POST /policies/action/:action - client/conduit.mjs: get_policies, set_group_policy, set_action_policy - ccc-queue.mjs: policy view (Tab to switch); group/action panel with space/e/←/→ - actions.mjs: group fields on desktop/email/calendar actions; list-actions includes group - config.example.json: add policy_overrides key - Bump version to 1.3.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
70 lines
2.1 KiB
JavaScript
70 lines
2.1 KiB
JavaScript
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
|
|
// Manages runtime policy overrides for groups and individual actions.
|
|
// Groups and action overrides are independent: disabling a group does not
|
|
// touch action-level overrides. Re-enabling a group restores them intact.
|
|
export function load_policy_overrides(file_path) {
|
|
let groups = {}; // { group_name: 'disabled' }
|
|
let overrides = {}; // { action_name: policy }
|
|
|
|
if (file_path && existsSync(file_path)) {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(file_path, 'utf8'));
|
|
groups = parsed.groups ?? {};
|
|
overrides = parsed.overrides ?? {};
|
|
} catch (err) {
|
|
throw new Error(`Cannot load policy overrides from ${file_path}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
function write() {
|
|
if (!file_path) { return; }
|
|
writeFileSync(file_path, JSON.stringify({ groups, overrides }, null, '\t') + '\n', 'utf8');
|
|
}
|
|
|
|
// Resolve effective policy for an action at request time.
|
|
// Precedence: group disabled > action override > hardcoded default
|
|
function effective(action_name, default_policy, group_name) {
|
|
if (group_name && groups[group_name] === 'disabled') { return 'auto-deny'; }
|
|
return overrides[action_name] ?? default_policy;
|
|
}
|
|
|
|
function set_group(group_name, disabled) {
|
|
if (disabled) {
|
|
groups[group_name] = 'disabled';
|
|
} else {
|
|
delete groups[group_name];
|
|
}
|
|
write();
|
|
}
|
|
|
|
// policy = null clears the override (falls back to hardcoded default)
|
|
function set_action(action_name, policy) {
|
|
if (policy === null) {
|
|
delete overrides[action_name];
|
|
} else {
|
|
overrides[action_name] = policy;
|
|
}
|
|
write();
|
|
}
|
|
|
|
// Returns full policy state suitable for the TUI and /policies endpoint.
|
|
function get_all(action_defs) {
|
|
const known_groups = [...new Set(
|
|
Object.values(action_defs).map(d => d.group).filter(Boolean)
|
|
)].sort();
|
|
|
|
return {
|
|
groups: Object.fromEntries(known_groups.map(g => [g, groups[g] === 'disabled'])),
|
|
action_overrides: { ...overrides },
|
|
effective: Object.fromEntries(
|
|
Object.entries(action_defs).map(([name, def]) => [
|
|
name, effective(name, def.policy, def.group)
|
|
])
|
|
),
|
|
};
|
|
}
|
|
|
|
return { effective, set_group, set_action, get_all };
|
|
}
|