Refactor permission management across UI components and stores (#221)
* refactor: normalize permission rules and improve command draft handling Add normalized rule set construction for permissions in Agents pages Track initial command draft state to detect unsaved changes Prefill draft fields when duplicating or selecting existing commands * feat: add global session-level edit mode for agent permissions
This commit is contained in:
committed by
GitHub
parent
74511abfda
commit
a9952fd8da
File diff suppressed because it is too large
Load Diff
@@ -35,14 +35,6 @@ type PermissionRule = { permission: string; pattern: string; action: PermissionA
|
||||
|
||||
type PermissionConfigValue = PermissionAction | Record<string, PermissionAction>;
|
||||
|
||||
// OpenCode's built-in defaults for permissions that differ from "allow"
|
||||
const getOpenCodeDefaultActionForPermission = (permissionName: string): PermissionAction => {
|
||||
if (permissionName === 'doom_loop' || permissionName === 'external_directory') {
|
||||
return 'ask';
|
||||
}
|
||||
return 'allow';
|
||||
};
|
||||
|
||||
const toPermissionRuleset = (ruleset: unknown): PermissionRule[] => {
|
||||
if (!Array.isArray(ruleset)) {
|
||||
return [];
|
||||
@@ -66,8 +58,22 @@ const toPermissionRuleset = (ruleset: unknown): PermissionRule[] => {
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const normalizeRuleset = (ruleset: PermissionRule[]): PermissionRule[] => {
|
||||
const map = new Map<string, PermissionRule>();
|
||||
for (const rule of ruleset) {
|
||||
if (!rule.permission || rule.permission === 'invalid') {
|
||||
continue;
|
||||
}
|
||||
if (!rule.pattern) {
|
||||
continue;
|
||||
}
|
||||
map.set(`${rule.permission}::${rule.pattern}`, rule);
|
||||
}
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
const rulesetToPermissionConfig = (ruleset: unknown): AgentDraft['permission'] => {
|
||||
const parsed = toPermissionRuleset(ruleset);
|
||||
const parsed = normalizeRuleset(toPermissionRuleset(ruleset));
|
||||
if (parsed.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -80,48 +86,14 @@ const rulesetToPermissionConfig = (ruleset: unknown): AgentDraft['permission'] =
|
||||
(byPermission[rule.permission] ||= {})[rule.pattern] = rule.action;
|
||||
}
|
||||
|
||||
// Get the global default (wildcard * with pattern *)
|
||||
const globalDefault = byPermission['*']?.['*'];
|
||||
|
||||
const permissionNames = Object.keys(byPermission);
|
||||
if (
|
||||
permissionNames.length === 1 &&
|
||||
permissionNames[0] === '*' &&
|
||||
Object.keys(byPermission['*'] || {}).length === 1 &&
|
||||
byPermission['*']?.['*']
|
||||
) {
|
||||
return byPermission['*']['*'];
|
||||
}
|
||||
|
||||
const result: Record<string, PermissionConfigValue> = {};
|
||||
for (const permissionName of permissionNames) {
|
||||
const map = byPermission[permissionName];
|
||||
for (const [permissionName, map] of Object.entries(byPermission)) {
|
||||
const patterns = Object.keys(map);
|
||||
|
||||
// For wildcard-only entries, check if they're redundant
|
||||
if (patterns.length === 1 && patterns[0] === '*' && permissionName !== '*') {
|
||||
const action = map['*'];
|
||||
const opencodeDefault = getOpenCodeDefaultActionForPermission(permissionName);
|
||||
|
||||
// Skip if this permission is redundant (matches effective default)
|
||||
if (globalDefault) {
|
||||
if (action === globalDefault) continue;
|
||||
} else {
|
||||
if (action === opencodeDefault) continue;
|
||||
}
|
||||
|
||||
result[permissionName] = action;
|
||||
} else if (permissionName === '*') {
|
||||
// Include global default
|
||||
if (patterns.length === 1 && patterns[0] === '*') {
|
||||
result[permissionName] = map['*'];
|
||||
} else {
|
||||
result[permissionName] = map;
|
||||
}
|
||||
} else {
|
||||
// Non-wildcard patterns - include as-is
|
||||
result[permissionName] = map;
|
||||
if (patterns.length === 1 && patterns[0] === '*') {
|
||||
result[permissionName] = map['*'];
|
||||
continue;
|
||||
}
|
||||
result[permissionName] = map;
|
||||
}
|
||||
|
||||
return Object.keys(result).length > 0 ? (result as AgentDraft['permission']) : undefined;
|
||||
|
||||
@@ -31,26 +31,89 @@ export const CommandsPage: React.FC = () => {
|
||||
const [template, setTemplate] = React.useState('');
|
||||
const [subtask, setSubtask] = React.useState(false);
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
const initialStateRef = React.useRef<{
|
||||
draftName: string;
|
||||
draftScope: CommandScope;
|
||||
description: string;
|
||||
agent: string;
|
||||
model: string;
|
||||
template: string;
|
||||
subtask: boolean;
|
||||
} | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isNewCommand && commandDraft) {
|
||||
// Prefill from draft (for new or duplicated commands)
|
||||
setDraftName(commandDraft.name || '');
|
||||
setDraftScope(commandDraft.scope || 'user');
|
||||
setDescription(commandDraft.description || '');
|
||||
setAgent(commandDraft.agent || '');
|
||||
setModel(commandDraft.model || '');
|
||||
setTemplate(commandDraft.template || '');
|
||||
setSubtask(commandDraft.subtask || false);
|
||||
const draftNameValue = commandDraft.name || '';
|
||||
const draftScopeValue = commandDraft.scope || 'user';
|
||||
const descriptionValue = commandDraft.description || '';
|
||||
const agentValue = commandDraft.agent || '';
|
||||
const modelValue = commandDraft.model || '';
|
||||
const templateValue = commandDraft.template || '';
|
||||
const subtaskValue = commandDraft.subtask || false;
|
||||
|
||||
setDraftName(draftNameValue);
|
||||
setDraftScope(draftScopeValue);
|
||||
setDescription(descriptionValue);
|
||||
setAgent(agentValue);
|
||||
setModel(modelValue);
|
||||
setTemplate(templateValue);
|
||||
setSubtask(subtaskValue);
|
||||
|
||||
initialStateRef.current = {
|
||||
draftName: draftNameValue,
|
||||
draftScope: draftScopeValue,
|
||||
description: descriptionValue,
|
||||
agent: agentValue,
|
||||
model: modelValue,
|
||||
template: templateValue,
|
||||
subtask: subtaskValue,
|
||||
};
|
||||
} else if (selectedCommand) {
|
||||
setDescription(selectedCommand.description || '');
|
||||
setAgent(selectedCommand.agent || '');
|
||||
setModel(selectedCommand.model || '');
|
||||
setTemplate(selectedCommand.template || '');
|
||||
setSubtask(selectedCommand.subtask || false);
|
||||
const descriptionValue = selectedCommand.description || '';
|
||||
const agentValue = selectedCommand.agent || '';
|
||||
const modelValue = selectedCommand.model || '';
|
||||
const templateValue = selectedCommand.template || '';
|
||||
const subtaskValue = selectedCommand.subtask || false;
|
||||
|
||||
setDescription(descriptionValue);
|
||||
setAgent(agentValue);
|
||||
setModel(modelValue);
|
||||
setTemplate(templateValue);
|
||||
setSubtask(subtaskValue);
|
||||
|
||||
initialStateRef.current = {
|
||||
draftName: '',
|
||||
draftScope: 'user',
|
||||
description: descriptionValue,
|
||||
agent: agentValue,
|
||||
model: modelValue,
|
||||
template: templateValue,
|
||||
subtask: subtaskValue,
|
||||
};
|
||||
}
|
||||
}, [selectedCommand, isNewCommand, selectedCommandName, commands, commandDraft]);
|
||||
|
||||
const isDirty = React.useMemo(() => {
|
||||
const initial = initialStateRef.current;
|
||||
if (!initial) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNewCommand) {
|
||||
if (draftName !== initial.draftName) return true;
|
||||
if (draftScope !== initial.draftScope) return true;
|
||||
}
|
||||
|
||||
if (description !== initial.description) return true;
|
||||
if (agent !== initial.agent) return true;
|
||||
if (model !== initial.model) return true;
|
||||
if (template !== initial.template) return true;
|
||||
if (subtask !== initial.subtask) return true;
|
||||
|
||||
return false;
|
||||
}, [agent, description, draftName, draftScope, isNewCommand, model, subtask, template]);
|
||||
|
||||
const handleSave = async () => {
|
||||
const commandName = isNewCommand ? draftName.trim().replace(/\s+/g, '-') : selectedCommandName?.trim();
|
||||
|
||||
@@ -352,7 +415,7 @@ Use @filename to include file contents.`}
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
disabled={isSaving || !isDirty}
|
||||
className="gap-2 h-6 px-2 text-xs w-fit"
|
||||
>
|
||||
<RiSaveLine className="h-3 w-3" />
|
||||
|
||||
Reference in New Issue
Block a user