Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/desktop/src/common/adapter/ipcBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,30 @@ export const acpConversation = {
),
};

// ---------------------------------------------------------------------------
// Agent permission policy — routed to /api/agents/permission-policy/*
// (OpenCode pilot for iOfficeAI/AionUi#4018)
// ---------------------------------------------------------------------------

export const permissionPolicy = {
/** List all known agents' permission-policy read-models. */
list: httpGet<import('@/renderer/utils/model/agentPermissionPolicy').AgentPermissionPolicy[], void>(
'/api/agents/permission-policy'
),
/** Write-through a permission level to the agent's own config file. */
setLevel: httpPut<
import('@/renderer/utils/model/agentPermissionPolicy').AgentPermissionPolicy,
{ agent: string; level: import('@/renderer/utils/model/agentPermissionPolicy').AgentPermissionLevel }
>(
(p) => `/api/agents/permission-policy/${encodeURIComponent(p.agent)}`,
(p) => ({ level: p.level })
),
/** Remove the agent-side policy so the agent uses its default behaviour. */
clear: httpPost<import('@/renderer/utils/model/agentPermissionPolicy').AgentPermissionPolicy, { agent: string }>(
(p) => `/api/agents/permission-policy/${encodeURIComponent(p.agent)}/clear`
),
};

// ---------------------------------------------------------------------------
// MCP Service — routed to /api/mcp/*
// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @license
* Copyright 2025 AionUi (aionui.com)
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { Message, Select, Typography } from '@arco-design/web-react';
import { useTranslation } from 'react-i18next';
import { ipcBridge } from '@/common';
import {
AGENT_PERMISSION_LEVEL_OPTIONS,
type AgentPermissionPolicy,
isPermissionPolicyActionable,
} from '@/renderer/utils/model/agentPermissionPolicy';

const UNMANAGED = '__unmanaged__';

interface AgentPermissionControlProps {
/** Read-model for this agent's permission policy. */
policy: AgentPermissionPolicy | undefined;
/** Called after a write-through succeeds so the parent can refresh the read-model. */
onChanged: () => void;
}

/**
* Write-through permission-level selector for an external agent (OpenCode pilot).
*
* Renders nothing when the backend reports the agent is unsupported or not
* installed. `onChanged` lets the parent re-poll the policy so the echoed
* `current_level` stays in sync with the agent's config file.
*/
export const AgentPermissionControl: React.FC<AgentPermissionControlProps> = ({ policy, onChanged }) => {
const { t } = useTranslation();

if (!isPermissionPolicyActionable(policy)) {
return null;
}

const apply = async (level: string) => {
const agent = policy.agent;
try {
if (level === UNMANAGED) {
await ipcBridge.permissionPolicy.clear.invoke({ agent });
Message.success(t('settings.agentManagement.permissionCleared'));
} else {
await ipcBridge.permissionPolicy.setLevel.invoke({ agent, level: level as 'ask' | 'auto_edit' | 'full_auto' });
Message.success(t('settings.agentManagement.permissionSaved'));
}
onChanged();
} catch {
Message.error(t('settings.agentManagement.permissionSaveFailed'));
}
};

const current = policy.current_level ?? UNMANAGED;

return (
<div data-testid='agent-permission-control' className='flex items-center gap-8px'>
<Typography.Text className='text-12px text-t-secondary'>
{t('settings.agentManagement.permissionPolicyLabel')}
</Typography.Text>
<Select
data-testid='agent-permission-select'
size='small'
value={current}
onChange={apply}
className='!w-140px'
options={[
{ value: UNMANAGED, label: t('settings.agentManagement.permissionNotSet') },
...AGENT_PERMISSION_LEVEL_OPTIONS.map((opt) => ({
value: opt.value,
label: t(opt.labelKey),
})),
]}
/>
</div>
);
};

export default AgentPermissionControl;
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import { useManagedAgents } from '@/renderer/hooks/agent/useManagedAgents';
import { openExternalUrl } from '@/renderer/utils/platform';
import { Button, Message, Typography } from '@arco-design/web-react';
import TalkToButlerButton from '@/renderer/components/base/TalkToButlerButton';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { AgentPermissionPolicy } from '@/renderer/utils/model/agentPermissionPolicy';
import AgentCard from './AgentCard';
import { AgentPermissionControl } from './AgentPermissionControl';
import { isDeprecatedRuntimeAgentType } from '@/renderer/utils/model/agentTypeSupportPolicy';
import InlineAgentEditor, { type CustomAgentDraft } from './InlineAgentEditor';
import { getBoundAssistants, useAssistantsForAgents } from './BoundAssistants';
Expand All @@ -44,6 +46,26 @@ const LocalAgents: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const { assistants } = useAssistantsForAgents();

// Agent-side permission policy (OpenCode pilot): fetch the read-models and
// let the controls re-poll after a write-through so the echoed level stays
// in sync with the agent's own config file.
const [permissionPolicies, setPermissionPolicies] = useState<AgentPermissionPolicy[]>([]);
const refreshPermissionPolicies = useCallback(async () => {
try {
const list = await ipcBridge.permissionPolicy.list.invoke();
setPermissionPolicies(Array.isArray(list) ? list : []);
} catch {
// Backend does not expose policy management — leave the list empty.
}
}, []);
useEffect(() => {
void refreshPermissionPolicies();
}, [refreshPermissionPolicies]);
const permissionPolicyFor = useCallback(
(agent: ManagedAgent) => permissionPolicies.find((p) => p.agent === agent.backend),
[permissionPolicies]
);

// Management view: includes user-disabled custom agents so they stay
// listed (greyed) with a working re-enable toggle. `refreshCatalog`
// also refreshes assistant list caches because generated-assistant availability
Expand Down Expand Up @@ -272,15 +294,20 @@ const LocalAgents: React.FC = () => {
<div data-testid='agent-management-official-section'>
<div className='flex flex-col gap-8px rounded-12px border border-border-2 bg-2 p-8px md:rounded-16px md:p-10px'>
{visibleOfficialAgents.map((agent) => (
<AgentCard
key={agent.id}
type='official'
agent={agent}
boundAssistants={getBoundAssistants(agent, assistants)}
onTestConnection={() => void handleTestConnection(agent.id)}
onConfigure={() => openAgentConfig(agent.id)}
isTesting={testingAgentId === agent.id}
/>
<div key={agent.id} className='flex flex-col gap-6px'>
<AgentCard
type='official'
agent={agent}
boundAssistants={getBoundAssistants(agent, assistants)}
onTestConnection={() => void handleTestConnection(agent.id)}
onConfigure={() => openAgentConfig(agent.id)}
isTesting={testingAgentId === agent.id}
/>
<AgentPermissionControl
policy={permissionPolicyFor(agent)}
onChanged={() => void refreshPermissionPolicies()}
/>
</div>
))}
{visibleOfficialAgents.length === 0 && (
<Typography.Text type='secondary' className='block py-16px text-center text-12px'>
Expand Down
8 changes: 8 additions & 0 deletions packages/desktop/src/renderer/services/i18n/i18n-keys.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,14 @@ export type I18nKey =
| 'settings.agentManagement.marketUpdate'
| 'settings.agentManagement.noSearchResults'
| 'settings.agentManagement.officialAgents'
| 'settings.agentManagement.permissionAsk'
| 'settings.agentManagement.permissionAutoEdit'
| 'settings.agentManagement.permissionCleared'
| 'settings.agentManagement.permissionFullAuto'
| 'settings.agentManagement.permissionNotSet'
| 'settings.agentManagement.permissionPolicyLabel'
| 'settings.agentManagement.permissionSaveFailed'
| 'settings.agentManagement.permissionSaved'
| 'settings.agentManagement.refreshingStatuses'
| 'settings.agentManagement.remoteAgents'
| 'settings.agentManagement.remoteAgentsDescription'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,14 @@
"statusOnline": "Available",
"statusNeedsAuth": "Needs Sign-in",
"statusOffline": "Unavailable",
"permissionPolicyLabel": "Berechtigungsrichtlinie",
"permissionNotSet": "Nicht festgelegt",
"permissionAsk": "Fragen",
"permissionAutoEdit": "Auto-bearbeiten",
"permissionFullAuto": "Voll automatisch",
"permissionSaved": "Berechtigungsrichtlinie gespeichert",
"permissionCleared": "Berechtigungsrichtlinie entfernt",
"permissionSaveFailed": "Speichern der Berechtigungsrichtlinie fehlgeschlagen",
"statusMissing": "Not Installed",
"statusUnchecked": "Not Checked",
"statusUnknown": "Unknown",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,14 @@
"statusOnline": "Available",
"statusNeedsAuth": "Needs Sign-in",
"statusOffline": "Unavailable",
"permissionPolicyLabel": "Permission policy",
"permissionNotSet": "Not set",
"permissionAsk": "Ask",
"permissionAutoEdit": "Auto-edit",
"permissionFullAuto": "Full auto",
"permissionSaved": "Permission policy saved",
"permissionCleared": "Permission policy cleared",
"permissionSaveFailed": "Failed to save permission policy",
"statusMissing": "Not Installed",
"statusUnchecked": "Not Checked",
"statusUnknown": "Unknown",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,14 @@
"statusOnline": "Available",
"statusNeedsAuth": "Needs Sign-in",
"statusOffline": "Unavailable",
"permissionPolicyLabel": "Política de permisos",
"permissionNotSet": "Sin definir",
"permissionAsk": "Preguntar",
"permissionAutoEdit": "Edición automática",
"permissionFullAuto": "Totalmente automático",
"permissionSaved": "Política de permisos guardada",
"permissionCleared": "Política de permisos eliminada",
"permissionSaveFailed": "Error al guardar la política de permisos",
"statusMissing": "Not Installed",
"statusUnchecked": "Not Checked",
"statusUnknown": "Unknown",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,14 @@
"statusOnline": "در دسترس",
"statusNeedsAuth": "نیاز به ورود",
"statusOffline": "در دسترس نیست",
"permissionPolicyLabel": "سیاست مجوزها",
"permissionNotSet": "تنظیم نشده",
"permissionAsk": "پرسیدن",
"permissionAutoEdit": "ویرایش خودکار",
"permissionFullAuto": "خودکار کامل",
"permissionSaved": "سیاست مجوزها ذخیره شد",
"permissionCleared": "سیاست مجوزها حذف شد",
"permissionSaveFailed": "ذخیره سیاست مجوزها ناموفق بود",
"statusMissing": "نصب نشده",
"statusUnchecked": "بررسی نشده",
"statusUnknown": "نامشخص",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,14 @@
"statusOnline": "Disponible",
"statusNeedsAuth": "Besoins de connexion",
"statusOffline": "Indisponible",
"permissionPolicyLabel": "Politique d'autorisations",
"permissionNotSet": "Non défini",
"permissionAsk": "Demander",
"permissionAutoEdit": "Édition automatique",
"permissionFullAuto": "Entièrement automatique",
"permissionSaved": "Politique d'autorisations enregistrée",
"permissionCleared": "Politique d'autorisations supprimée",
"permissionSaveFailed": "Échec de l'enregistrement de la politique d'autorisations",
"statusMissing": "Non installé",
"statusUnchecked": "Non vérifié",
"statusUnknown": "Inconnu",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,14 @@
"statusOnline": "利用可能",
"statusNeedsAuth": "要サインイン",
"statusOffline": "利用不可",
"permissionPolicyLabel": "権限ポリシー",
"permissionNotSet": "未設定",
"permissionAsk": "確認を取る",
"permissionAutoEdit": "自動編集",
"permissionFullAuto": "全自動",
"permissionSaved": "権限ポリシーを保存しました",
"permissionCleared": "権限ポリシーを解除しました",
"permissionSaveFailed": "権限ポリシーの保存に失敗",
"statusMissing": "未インストール",
"statusUnchecked": "未確認",
"statusUnknown": "不明",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,14 @@
"statusOnline": "사용 가능",
"statusNeedsAuth": "로그인 필요",
"statusOffline": "사용 불가",
"permissionPolicyLabel": "권한 정책",
"permissionNotSet": "설정 안 함",
"permissionAsk": "확인",
"permissionAutoEdit": "자동 편집",
"permissionFullAuto": "완전 자동",
"permissionSaved": "권한 정책이 저장되었습니다",
"permissionCleared": "권한 정책이 해제되었습니다",
"permissionSaveFailed": "권한 정책 저장 실패",
"statusMissing": "미설치",
"statusUnchecked": "미확인",
"statusUnknown": "알 수 없음",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,14 @@
"statusOnline": "Disponível",
"statusNeedsAuth": "Requer Login",
"statusOffline": "Indisponível",
"permissionPolicyLabel": "Política de permissões",
"permissionNotSet": "Não definido",
"permissionAsk": "Perguntar",
"permissionAutoEdit": "Edição automática",
"permissionFullAuto": "Totalmente automático",
"permissionSaved": "Política de permissões salva",
"permissionCleared": "Política de permissões removida",
"permissionSaveFailed": "Falha ao salvar a política de permissões",
"statusMissing": "Não Instalado",
"statusUnchecked": "Não Verificado",
"statusUnknown": "Desconhecido",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,14 @@
"statusOnline": "Доступен",
"statusNeedsAuth": "Требуется вход",
"statusOffline": "Недоступен",
"permissionPolicyLabel": "Политика разрешений",
"permissionNotSet": "Не задано",
"permissionAsk": "Спрашивать",
"permissionAutoEdit": "Авто-правка",
"permissionFullAuto": "Полный авто",
"permissionSaved": "Политика разрешений сохранена",
"permissionCleared": "Политика разрешений удалена",
"permissionSaveFailed": "Не удалось сохранить политику разрешений",
"statusMissing": "Не установлен",
"statusUnchecked": "Не проверено",
"statusUnknown": "Неизвестно",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,14 @@
"statusOnline": "Kullanılabilir",
"statusNeedsAuth": "Giriş Gerekli",
"statusOffline": "Kullanılamıyor",
"permissionPolicyLabel": "İzin politikası",
"permissionNotSet": "Ayarlanmadı",
"permissionAsk": "Sor",
"permissionAutoEdit": "Otomatik düzenle",
"permissionFullAuto": "Tam otomatik",
"permissionSaved": "İzin politikası kaydedildi",
"permissionCleared": "İzin politikası kaldırıldı",
"permissionSaveFailed": "İzin politikası kaydedilemedi",
"statusMissing": "Yüklü Değil",
"statusUnchecked": "Kontrol Edilmedi",
"statusUnknown": "Bilinmiyor",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,14 @@
"statusOnline": "Доступний",
"statusNeedsAuth": "Потрібен вхід",
"statusOffline": "Недоступний",
"permissionPolicyLabel": "Політика дозволів",
"permissionNotSet": "Не задано",
"permissionAsk": "Питати",
"permissionAutoEdit": "Авто-редагування",
"permissionFullAuto": "Повний авто",
"permissionSaved": "Політику дозволів збережено",
"permissionCleared": "Політику дозволів вилучено",
"permissionSaveFailed": "Не вдалося зберегти політику дозволів",
"statusMissing": "Не встановлено",
"statusUnchecked": "Не перевірено",
"statusUnknown": "Невідомо",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,14 @@
"statusOnline": "可用",
"statusNeedsAuth": "需登录",
"statusOffline": "不可用",
"permissionPolicyLabel": "权限策略",
"permissionNotSet": "未设置",
"permissionAsk": "询问",
"permissionAutoEdit": "自动编辑",
"permissionFullAuto": "全自动",
"permissionSaved": "权限策略已保存",
"permissionCleared": "权限策略已清除",
"permissionSaveFailed": "保存权限策略失败",
"statusMissing": "未安装",
"statusUnchecked": "未检测",
"statusUnknown": "未知",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,14 @@
"statusOnline": "可用",
"statusNeedsAuth": "需登入",
"statusOffline": "不可用",
"permissionPolicyLabel": "權限策略",
"permissionNotSet": "未設定",
"permissionAsk": "詢問",
"permissionAutoEdit": "自動編輯",
"permissionFullAuto": "全自動",
"permissionSaved": "權限策略已儲存",
"permissionCleared": "權限策略已清除",
"permissionSaveFailed": "儲存權限策略失敗",
"statusMissing": "未安裝",
"statusUnchecked": "未檢測",
"statusUnknown": "未知",
Expand Down
Loading
Loading