Files
claude-code-conduit/server/policy_overrides.mjs
mikael-lovqvists-claude-agent dd7743501a Add runtime policy overrides and TUI policy management view
- 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>
2026-05-21 16:01:45 +00:00

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 };
}