fix: persist agent permission edits

Prevents permission changes from being overwritten by other agent fields
Writes built-in and custom agent permissions to the correct config source
Refreshes agent state after permission saves
This commit is contained in:
Bohdan Triapitsyn
2026-06-08 00:12:53 +03:00
parent 14e75359a5
commit b2b71198ca
4 changed files with 207 additions and 29 deletions
@@ -122,6 +122,10 @@ const filterRulesAgainstGlobal = (ruleset: PermissionRule[], globalAction: Permi
);
const permissionConfigToRuleset = (value: unknown): PermissionRule[] => {
if (Array.isArray(value)) {
return normalizeRuleset(value as PermissionRule[]);
}
if (isPermissionAction(value)) {
return [{ permission: '*', pattern: '*', action: value }];
}
@@ -162,9 +166,7 @@ const buildPermissionConfigWithGlobal = (
(grouped[rule.permission] ||= {})[rule.pattern] = rule.action;
}
const result: Record<string, PermissionConfigValue> = {
'*': globalAction,
};
const result: Record<string, PermissionConfigValue> = {};
for (const [permissionName, patterns] of Object.entries(grouped)) {
if (permissionName === '*') {
@@ -179,6 +181,14 @@ const buildPermissionConfigWithGlobal = (
result[permissionName] = patterns;
}
if (Object.keys(result).length === 0) {
return globalAction;
}
if (globalAction !== 'allow') {
result['*'] = globalAction;
}
return result as AgentConfig['permission'];
};
@@ -273,7 +283,7 @@ export const AgentsPage: React.FC = () => {
const names = new Set<string>();
for (const agent of agents) {
const rules = normalizeRuleset(Array.isArray(agent.permission) ? agent.permission as PermissionRule[] : []);
const rules = normalizeRuleset(permissionConfigToRuleset(agent.permission));
for (const rule of rules) {
if (rule.permission && rule.permission !== '*' && rule.permission !== 'invalid') {
names.add(rule.permission);
@@ -509,7 +519,7 @@ export const AgentsPage: React.FC = () => {
setPrompt(promptValue);
const permissionState = applyPermissionState(
Array.isArray(selectedAgent.permission) ? selectedAgent.permission as PermissionRule[] : [],
permissionConfigToRuleset(selectedAgent.permission),
);
initialStateRef.current = {
+16
View File
@@ -70,12 +70,24 @@ const getAgentsCacheKey = (directory: string | null): string => {
return directory?.trim() || DEFAULT_AGENTS_CACHE_KEY;
};
const invalidateAgentsLoadCache = (directory: string | null = getConfigDirectory()) => {
agentsLastLoadedAt.delete(getAgentsCacheKey(directory));
};
const buildAgentsSignature = (agents: Agent[]): string => {
return agents
.map((agent) => {
const extended = agent as AgentWithExtras;
return [
agent.name,
extended.mode ?? '',
typeof extended.model === 'object' && extended.model
? `${extended.model.providerID ?? ''}/${extended.model.modelID ?? ''}`
: String(extended.model ?? ''),
String(extended.temperature ?? ''),
String((extended as { topP?: unknown; top_p?: unknown }).topP ?? (extended as { topP?: unknown; top_p?: unknown }).top_p ?? ''),
extended.prompt ?? '',
JSON.stringify(extended.permission ?? null),
extended.scope ?? '',
extended.group ?? '',
extended.description ?? '',
@@ -345,6 +357,7 @@ export const useAgentsStore = create<AgentsStore>()(
}
const needsReload = payload?.requiresReload ?? true;
invalidateAgentsLoadCache(configDirectory);
if (needsReload) {
requiresReload = true;
await refreshAfterOpenCodeRestart({
@@ -406,6 +419,7 @@ export const useAgentsStore = create<AgentsStore>()(
}
const needsReload = payload?.requiresReload ?? true;
invalidateAgentsLoadCache(configDirectory);
if (needsReload) {
requiresReload = true;
await refreshAfterOpenCodeRestart({
@@ -452,6 +466,7 @@ export const useAgentsStore = create<AgentsStore>()(
}
const needsReload = payload?.requiresReload ?? true;
invalidateAgentsLoadCache(configDirectory);
if (needsReload) {
requiresReload = true;
await refreshAfterOpenCodeRestart({
@@ -629,6 +644,7 @@ async function performConfigRefresh(options: {
const uiRefreshTasks: Promise<void>[] = [];
if (refreshAgentConfigs) {
invalidateAgentsLoadCache(currentDirectory);
uiRefreshTasks.push(agentConfigStore.loadAgents().then(() => undefined));
}
if (refreshCommands) {
+145 -1
View File
@@ -1431,6 +1431,103 @@ const getJsonWriteTarget = (
return { config: userConfig, path: paths.userPath };
};
const getAgentPermissionSource = (agentName: string, workingDirectory?: string) => {
if (workingDirectory) {
const projectMdPath = getProjectAgentPath(workingDirectory, agentName);
if (fs.existsSync(projectMdPath)) {
const { frontmatter } = parseMdFile(projectMdPath);
if (frontmatter.permission !== undefined) {
return { source: 'md' as const, scope: AGENT_SCOPE.PROJECT, path: projectMdPath };
}
}
}
const userMdPath = getUserAgentPath(agentName);
if (fs.existsSync(userMdPath)) {
const { frontmatter } = parseMdFile(userMdPath);
if (frontmatter.permission !== undefined) {
return { source: 'md' as const, scope: AGENT_SCOPE.USER, path: userMdPath };
}
}
const layers = readConfigLayers(workingDirectory);
const customAgent = ((layers.customConfig as Record<string, unknown>)?.agent as Record<string, unknown> | undefined)?.[agentName] as Record<string, unknown> | undefined;
if (customAgent?.permission !== undefined && layers.paths.customPath) {
return { source: 'json' as const, scope: 'custom' as const, path: layers.paths.customPath };
}
const projectAgent = ((layers.projectConfig as Record<string, unknown>)?.agent as Record<string, unknown> | undefined)?.[agentName] as Record<string, unknown> | undefined;
if (projectAgent?.permission !== undefined && layers.paths.projectPath) {
return { source: 'json' as const, scope: AGENT_SCOPE.PROJECT, path: layers.paths.projectPath };
}
const userAgent = ((layers.userConfig as Record<string, unknown>)?.agent as Record<string, unknown> | undefined)?.[agentName] as Record<string, unknown> | undefined;
if (userAgent?.permission !== undefined) {
return { source: 'json' as const, scope: AGENT_SCOPE.USER, path: layers.paths.userPath };
}
return { source: null, scope: null, path: null };
};
const mergePermissionWithNonWildcards = (newPermission: unknown, permissionSource: ReturnType<typeof getAgentPermissionSource>, agentName: string) => {
if (!permissionSource.source || !permissionSource.path) {
return newPermission;
}
let existingPermission: unknown = null;
if (permissionSource.source === 'md') {
const { frontmatter } = parseMdFile(permissionSource.path);
existingPermission = frontmatter.permission;
} else if (permissionSource.source === 'json') {
const config = readConfigFile(permissionSource.path) as Record<string, unknown>;
existingPermission = (((config.agent as Record<string, unknown> | undefined)?.[agentName] as Record<string, unknown> | undefined)?.permission);
}
if (!existingPermission || typeof existingPermission === 'string' || newPermission == null || typeof newPermission === 'string') {
return newPermission;
}
if (typeof existingPermission !== 'object' || Array.isArray(existingPermission) || typeof newPermission !== 'object' || Array.isArray(newPermission)) {
return newPermission;
}
const nonWildcardPatterns: Record<string, Record<string, unknown>> = {};
for (const [permKey, permValue] of Object.entries(existingPermission as Record<string, unknown>)) {
if (permKey === '*' || typeof permValue !== 'object' || permValue === null || Array.isArray(permValue)) continue;
const nonWildcards: Record<string, unknown> = {};
for (const [pattern, action] of Object.entries(permValue as Record<string, unknown>)) {
if (pattern !== '*') {
nonWildcards[pattern] = action;
}
}
if (Object.keys(nonWildcards).length > 0) {
nonWildcardPatterns[permKey] = nonWildcards;
}
}
if (Object.keys(nonWildcardPatterns).length === 0) {
return newPermission;
}
const merged: Record<string, unknown> = { ...(newPermission as Record<string, unknown>) };
for (const [permKey, patterns] of Object.entries(nonWildcardPatterns)) {
const newValue = merged[permKey];
if (typeof newValue === 'string') {
merged[permKey] = { '*': newValue, ...patterns };
} else if (typeof newValue === 'object' && newValue !== null && !Array.isArray(newValue)) {
merged[permKey] = { ...patterns, ...(newValue as Record<string, unknown>) };
} else {
const existingValue = (existingPermission as Record<string, unknown>)[permKey];
if (typeof existingValue === 'object' && existingValue !== null && !Array.isArray(existingValue)) {
const wildcard = (existingValue as Record<string, unknown>)['*'];
merged[permKey] = wildcard ? { '*': wildcard, ...patterns } : patterns;
}
}
}
return merged;
};
const parseMdFile = (filePath: string): { frontmatter: Record<string, unknown>; body: string } => {
const content = fs.readFileSync(filePath, 'utf8');
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -1545,7 +1642,7 @@ export const updateAgent = (agentName: string, updates: Record<string, unknown>,
const hasJsonFields = Boolean(jsonSource.exists && jsonSection && Object.keys(jsonSection).length > 0);
const jsonTarget = jsonSource.exists
? { config: jsonSource.config, path: jsonSource.path }
: getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER);
: getJsonWriteTarget(layers, AGENT_SCOPE.USER);
const config = (jsonTarget.config || {}) as Record<string, unknown>;
// Determine if we should create a new md file:
@@ -1594,6 +1691,53 @@ export const updateAgent = (agentName: string, updates: Record<string, unknown>,
continue;
}
if (field === 'permission') {
const permissionSource = getAgentPermissionSource(agentName, workingDirectory);
const newPermission = mergePermissionWithNonWildcards(value, permissionSource, agentName);
if (permissionSource.source === 'md' && permissionSource.path) {
if (mdData && permissionSource.path === targetPath) {
mdData.frontmatter.permission = newPermission;
mdModified = true;
} else {
const existingMdData = parseMdFile(permissionSource.path);
existingMdData.frontmatter.permission = newPermission;
writeMdFile(permissionSource.path, existingMdData.frontmatter, existingMdData.body);
}
} else if (permissionSource.source === 'json' && permissionSource.path) {
if (permissionSource.path === (jsonTarget.path || CONFIG_FILE)) {
if (!config.agent) config.agent = {};
const current = ((config.agent as Record<string, unknown>)[agentName] as Record<string, unknown> | undefined) ?? {};
(config.agent as Record<string, unknown>)[agentName] = { ...current, permission: newPermission };
jsonModified = true;
} else {
const existingConfig = readConfigFile(permissionSource.path) as Record<string, unknown>;
const agentMap = (existingConfig.agent as Record<string, unknown> | undefined) ?? {};
const current = (agentMap[agentName] as Record<string, unknown> | undefined) ?? {};
agentMap[agentName] = { ...current, permission: newPermission };
existingConfig.agent = agentMap;
writeConfig(existingConfig, permissionSource.path);
}
} else if (mdExists && mdData) {
mdData.frontmatter.permission = newPermission;
mdModified = true;
} else if (hasJsonFields) {
if (!config.agent) config.agent = {};
const current = ((config.agent as Record<string, unknown>)[agentName] as Record<string, unknown> | undefined) ?? {};
(config.agent as Record<string, unknown>)[agentName] = { ...current, permission: newPermission };
jsonModified = true;
} else {
const writeTarget = getJsonWriteTarget(layers, AGENT_SCOPE.USER);
const targetConfig = (writeTarget.config || {}) as Record<string, unknown>;
const agentMap = (targetConfig.agent as Record<string, unknown> | undefined) ?? {};
const current = (agentMap[agentName] as Record<string, unknown> | undefined) ?? {};
agentMap[agentName] = { ...current, permission: newPermission };
targetConfig.agent = agentMap;
writeConfig(targetConfig, writeTarget.path || CONFIG_FILE);
}
continue;
}
const hasMdField = Boolean(mdData?.frontmatter?.[field] !== undefined);
const hasJsonField = Boolean(jsonSection?.[field] !== undefined);
+31 -23
View File
@@ -193,27 +193,25 @@ function getAgentPermissionSource(agentName, workingDirectory, lookupCache = nul
}
}
// Check JSON layers (project > user)
// Check JSON layers in effective override order. readConfigLayers merges
// user -> project -> custom, so custom wins over project, project over user.
const layers = readConfigLayers(workingDirectory);
// Project opencode.json
const customJsonPermission = layers.customConfig?.agent?.[agentName]?.permission;
if (customJsonPermission !== undefined && layers.paths.customPath) {
return { source: 'json', scope: 'custom', path: layers.paths.customPath };
}
const projectJsonPermission = layers.projectConfig?.agent?.[agentName]?.permission;
if (projectJsonPermission !== undefined && layers.paths.projectPath) {
return { source: 'json', scope: AGENT_SCOPE.PROJECT, path: layers.paths.projectPath };
}
// User opencode.json
const userJsonPermission = layers.userConfig?.agent?.[agentName]?.permission;
if (userJsonPermission !== undefined) {
return { source: 'json', scope: AGENT_SCOPE.USER, path: layers.paths.userPath };
}
// Custom config (env var)
const customJsonPermission = layers.customConfig?.agent?.[agentName]?.permission;
if (customJsonPermission !== undefined && layers.paths.customPath) {
return { source: 'json', scope: 'custom', path: layers.paths.customPath };
}
return { source: null, scope: null, path: null };
}
@@ -486,19 +484,31 @@ function updateAgent(agentName, updates, workingDirectory) {
const newPermission = mergePermissionWithNonWildcards(value, permissionSource, agentName);
if (permissionSource.source === 'md') {
const existingMdData = parseMdFile(permissionSource.path);
existingMdData.frontmatter.permission = newPermission;
writeMdFile(permissionSource.path, existingMdData.frontmatter, existingMdData.body);
console.log(`Updated permission in .md file: ${permissionSource.path}`);
if (mdData && permissionSource.path === targetPath) {
mdData.frontmatter.permission = newPermission;
mdModified = true;
} else {
const existingMdData = parseMdFile(permissionSource.path);
existingMdData.frontmatter.permission = newPermission;
writeMdFile(permissionSource.path, existingMdData.frontmatter, existingMdData.body);
console.log(`Updated permission in .md file: ${permissionSource.path}`);
}
} else if (permissionSource.source === 'json') {
const existingConfig = readConfigFile(permissionSource.path);
if (!existingConfig.agent) existingConfig.agent = {};
if (!existingConfig.agent[agentName]) existingConfig.agent[agentName] = {};
existingConfig.agent[agentName].permission = newPermission;
writeConfig(existingConfig, permissionSource.path);
console.log(`Updated permission in JSON: ${permissionSource.path}`);
if (permissionSource.path === (jsonTarget.path || CONFIG_FILE)) {
if (!config.agent) config.agent = {};
if (!config.agent[agentName]) config.agent[agentName] = {};
config.agent[agentName].permission = newPermission;
jsonModified = true;
} else {
const existingConfig = readConfigFile(permissionSource.path);
if (!existingConfig.agent) existingConfig.agent = {};
if (!existingConfig.agent[agentName]) existingConfig.agent[agentName] = {};
existingConfig.agent[agentName].permission = newPermission;
writeConfig(existingConfig, permissionSource.path);
console.log(`Updated permission in JSON: ${permissionSource.path}`);
}
} else {
if ((mdExists || creatingNewMd) && mdData) {
if (mdExists && mdData) {
mdData.frontmatter.permission = newPermission;
mdModified = true;
} else if (hasJsonFields) {
@@ -507,9 +517,7 @@ function updateAgent(agentName, updates, workingDirectory) {
config.agent[agentName].permission = newPermission;
jsonModified = true;
} else {
const writeTarget = workingDirectory
? { config: layers.projectConfig || {}, path: layers.paths.projectPath || layers.paths.userPath }
: { config: layers.userConfig || {}, path: layers.paths.userPath };
const writeTarget = getJsonWriteTarget(layers, AGENT_SCOPE.USER);
if (!writeTarget.config.agent) writeTarget.config.agent = {};
if (!writeTarget.config.agent[agentName]) writeTarget.config.agent[agentName] = {};
writeTarget.config.agent[agentName].permission = newPermission;