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:
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user