Skip to content

Commit fc01993

Browse files
authored
Merge pull request #670 from dataelement/codex/fix-agent-settings-permissions-603
fix agent settings permissions
2 parents 18cc83a + cf75609 commit fc01993

5 files changed

Lines changed: 64 additions & 41 deletions

File tree

frontend/src/pages/OpenClawSettings.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ function fetchAuth<T>(url: string, options?: RequestInit): Promise<T> {
1515
interface OpenClawSettingsProps {
1616
agent: any;
1717
agentId: string;
18+
canManage: boolean;
1819
}
1920

20-
export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsProps) {
21+
export default function OpenClawSettings({ agent, agentId, canManage }: OpenClawSettingsProps) {
2122
const { t, i18n } = useTranslation();
2223
const queryClient = useQueryClient();
2324
const navigate = useNavigate();
@@ -34,6 +35,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
3435
const hasKey = agent?.has_api_key || false;
3536

3637
const handleRegenerate = async (autoCopy = false) => {
38+
if (!canManage) return;
3739
setRegenerating(true);
3840
try {
3941
const result = await fetchAuth<{ api_key: string }>(`/agents/${agentId}/api-key`, { method: 'POST' });
@@ -56,6 +58,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
5658
};
5759

5860
const handleDelete = async () => {
61+
if (!canManage) return;
5962
setDeleting(true);
6063
try {
6164
await agentApi.delete(agentId);
@@ -75,6 +78,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
7578
});
7679

7780
const handleScopeChange = async (newScope: string) => {
81+
if (!canManage || !isOwner) return;
7882
try {
7983
await fetchAuth(`/agents/${agentId}/permissions`, {
8084
method: 'PUT',
@@ -89,6 +93,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
8993
};
9094

9195
const handleAccessLevelChange = async (newLevel: string) => {
96+
if (!canManage || !isOwner) return;
9297
try {
9398
await fetchAuth(`/agents/${agentId}/permissions`, {
9499
method: 'PUT',
@@ -103,6 +108,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
103108
};
104109

105110
const isOwner = permData?.is_owner ?? false;
111+
const canEditPermissions = canManage && isOwner;
106112
const currentScope = permData?.scope_type === 'user' ? 'private' : (permData?.scope_type || 'company');
107113
const currentAccessLevel = permData?.access_level || 'use';
108114

@@ -146,13 +152,13 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
146152
copiedLabel="Copied"
147153
style={{ padding: '4px 12px', fontSize: '12px', whiteSpace: 'nowrap', minWidth: '70px', height: 'fit-content' }}
148154
/>
149-
<button
155+
{canManage && <button
150156
className="btn btn-secondary"
151157
onClick={() => setShowConfirm(true)}
152158
style={{ padding: '4px 12px', fontSize: '12px', whiteSpace: 'nowrap' }}
153159
>
154160
{isChinese ? '重新生成' : 'Regenerate'}
155-
</button>
161+
</button>}
156162
</div>
157163
);
158164
}
@@ -169,15 +175,15 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
169175
? (isChinese ? '旧版密钥(已加密隐藏),请重新生成以查看明文' : 'Legacy key (encrypted), please regenerate to view')
170176
: (isChinese ? '未生成' : 'Not generated')}
171177
</div>
172-
<button
178+
{canManage && <button
173179
className="btn btn-secondary"
174180
onClick={() => setShowConfirm(true)}
175181
style={{ padding: '6px 16px', fontSize: '12px', whiteSpace: 'nowrap' }}
176182
>
177183
{isLegacyHash
178184
? (isChinese ? '重新生成' : 'Regenerate')
179185
: (isChinese ? '生成' : 'Generate')}
180-
</button>
186+
</button>}
181187
</div>
182188
);
183189
})()}
@@ -214,7 +220,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
214220
<button
215221
className="btn btn-primary"
216222
onClick={() => handleRegenerate(false)}
217-
disabled={regenerating}
223+
disabled={!canManage || regenerating}
218224
style={{ padding: '5px 14px', fontSize: '12px' }}
219225
>
220226
{regenerating
@@ -243,22 +249,22 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
243249
style={{
244250
display: 'flex', alignItems: 'center', gap: '10px',
245251
padding: '12px 14px', borderRadius: '8px',
246-
cursor: isOwner ? 'pointer' : 'default',
252+
cursor: canEditPermissions ? 'pointer' : 'default',
247253
border: currentScope === scope
248254
? '1px solid var(--accent-primary)'
249255
: '1px solid var(--border-subtle)',
250256
background: currentScope === scope
251257
? 'rgba(99,102,241,0.06)'
252258
: 'transparent',
253-
opacity: isOwner ? 1 : 0.7,
259+
opacity: canEditPermissions ? 1 : 0.7,
254260
transition: 'all 0.15s',
255261
}}
256262
>
257263
<input
258264
type="radio"
259265
name="perm_scope_oc"
260266
checked={currentScope === scope}
261-
disabled={!isOwner}
267+
disabled={!canEditPermissions}
262268
onChange={() => handleScopeChange(scope)}
263269
style={{ accentColor: 'var(--accent-primary)' }}
264270
/>
@@ -281,7 +287,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
281287
</div>
282288

283289
{/* Access Level for company scope */}
284-
{currentScope === 'company' && isOwner && (
290+
{currentScope === 'company' && canEditPermissions && (
285291
<div style={{ borderTop: '1px solid var(--border-subtle)', paddingTop: '12px' }}>
286292
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '8px' }}>
287293
{t('agent.settings.perm.defaultAccess', 'Default Access Level')}
@@ -332,7 +338,7 @@ export default function OpenClawSettings({ agent, agentId }: OpenClawSettingsPro
332338
</div>
333339

334340
{/* ── Danger Zone: Delete Agent ── */}
335-
{isOwner && (
341+
{canEditPermissions && (
336342
<div className="card" style={{
337343
marginBottom: '12px',
338344
border: '1px solid rgba(255,80,80,0.2)',

frontend/src/pages/agent-detail/AgentDetailPage.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4814,11 +4814,12 @@ export default function AgentDetailPage() {
48144814
</>
48154815
{(agent as any)?.agent_type !== 'openclaw' && (
48164816
<>
4817-
{agent.status === 'stopped' ? (
4817+
{canManage && agent.status === 'stopped' && (
48184818
<button className="btn btn-secondary" onClick={async () => { await agentApi.start(id!); queryClient.invalidateQueries({ queryKey: ['agent', id] }); }}>{t('agent.actions.start')}</button>
4819-
) : agent.status === 'running' ? (
4819+
)}
4820+
{canManage && agent.status === 'running' && (
48204821
<button className="btn btn-secondary" onClick={async () => { await agentApi.stop(id!); queryClient.invalidateQueries({ queryKey: ['agent', id] }); }}>{t('agent.actions.stop')}</button>
4821-
) : null}
4822+
)}
48224823
</>
48234824
)}
48244825
</div>
@@ -4829,7 +4830,7 @@ export default function AgentDetailPage() {
48294830
{activeTab !== 'chat' && <div className="tabs">
48304831
{AGENT_DETAIL_TABS.filter(tab => {
48314832
if (['aware', 'workspace', 'chat'].includes(tab)) return false;
4832-
// 'use' access: hide settings and approvals tabs
4833+
// 'use' access keeps the existing tab bar unchanged; settings remains available via its own entry.
48334834
if ((agent as any)?.access_level === 'use') {
48344835
if (tab === 'settings' || tab === 'approvals') return false;
48354836
}
@@ -5038,7 +5039,7 @@ export default function AgentDetailPage() {
50385039
{/* Quick Actions */}
50395040
<div style={{ display: 'flex', gap: '10px', marginTop: '20px' }}>
50405041
<button className="btn btn-secondary" onClick={() => setActiveTab('chat')}>{t('agent.actions.chat')}</button>
5041-
<button className="btn btn-secondary" onClick={() => setActiveTab('settings')}>{t('agent.tabs.settings')}</button>
5042+
{canManage && <button className="btn btn-secondary" onClick={() => setActiveTab('settings')}>{t('agent.tabs.settings')}</button>}
50425043
</div>
50435044
</div>
50445045
);
@@ -5282,9 +5283,10 @@ export default function AgentDetailPage() {
52825283
{trig.is_enabled ? t('agent.aware.inProgress') : t('agent.aware.completed')}
52835284
</span>
52845285
<div style={{ display: 'flex', gap: '4px' }}>
5285-
{!trig.is_system && <button className="btn btn-ghost" style={{ padding: '2px 6px', fontSize: '11px', color: 'var(--error)' }}
5286+
{canManage && !trig.is_system && <button className="btn btn-ghost" style={{ padding: '2px 6px', fontSize: '11px', color: 'var(--error)' }}
52865287
onClick={async (e) => {
52875288
e.stopPropagation();
5289+
if (!canManage) return;
52885290
const ok = await dialog.confirm(t('agent.aware.deleteTriggerConfirm', { name: trig.name }), { title: '删除触发器', danger: true, confirmLabel: '删除' });
52895291
if (ok) {
52905292
await triggerApi.delete(id!, trig.id);
@@ -5711,7 +5713,7 @@ export default function AgentDetailPage() {
57115713
upload: (file, path, onProgress) => fileApi.upload(id!, file, path + '/', onProgress),
57125714
downloadUrl: (p) => fileApi.downloadUrl(id!, p),
57135715
};
5714-
return <FileBrowser api={adapter} rootPath="workspace" features={{ upload: true, newFile: true, newFolder: true, edit: true, delete: canManage, directoryNavigation: true }} />;
5716+
return <FileBrowser api={adapter} rootPath="workspace" features={{ upload: canManage, newFile: canManage, newFolder: canManage, edit: canManage, delete: canManage, directoryNavigation: true }} />;
57155717
})()
57165718
}
57175719

@@ -6656,7 +6658,7 @@ export default function AgentDetailPage() {
66566658

66576659
{/* ── Approvals Tab ── */}
66586660
{
6659-
activeTab === 'approvals' && id && <ApprovalsTab agentId={id} />
6661+
activeTab === 'approvals' && id && <ApprovalsTab agentId={id} canManage={canManage} />
66606662
}
66616663

66626664
{/* ── Settings Tab ── */}

frontend/src/pages/agent-detail/tabs/ApprovalsTab.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
33

44
import { fetchAuth } from '../utils/fetchAuth';
55

6-
export default function ApprovalsTab({ agentId }: { agentId: string }) {
6+
export default function ApprovalsTab({ agentId, canManage }: { agentId: string; canManage: boolean }) {
77
const { i18n } = useTranslation();
88
const queryClient = useQueryClient();
99
const isChinese = i18n.language?.startsWith('zh');
@@ -16,6 +16,7 @@ export default function ApprovalsTab({ agentId }: { agentId: string }) {
1616

1717
const resolveMut = useMutation({
1818
mutationFn: async ({ approvalId, action }: { approvalId: string; action: string }) => {
19+
if (!canManage) return;
1920
const token = localStorage.getItem('token');
2021
return fetch(`/api/agents/${agentId}/approvals/${approvalId}/resolve`, {
2122
method: 'POST',
@@ -79,24 +80,28 @@ export default function ApprovalsTab({ agentId }: { agentId: string }) {
7980
{typeof approval.details === 'string' ? approval.details : JSON.stringify(approval.details, null, 2)}
8081
</div>
8182
)}
82-
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
83+
{canManage && <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
8384
<button
8485
className="btn btn-primary"
8586
style={{ padding: '6px 16px', fontSize: '12px' }}
86-
onClick={() => resolveMut.mutate({ approvalId: approval.id, action: 'approve' })}
87-
disabled={resolveMut.isPending}
87+
onClick={() => {
88+
if (canManage) resolveMut.mutate({ approvalId: approval.id, action: 'approve' });
89+
}}
90+
disabled={!canManage || resolveMut.isPending}
8891
>
8992
{isChinese ? '批准' : 'Approve'}
9093
</button>
9194
<button
9295
className="btn btn-danger"
9396
style={{ padding: '6px 16px', fontSize: '12px' }}
94-
onClick={() => resolveMut.mutate({ approvalId: approval.id, action: 'reject' })}
95-
disabled={resolveMut.isPending}
97+
onClick={() => {
98+
if (canManage) resolveMut.mutate({ approvalId: approval.id, action: 'reject' });
99+
}}
100+
disabled={!canManage || resolveMut.isPending}
96101
>
97102
{isChinese ? '拒绝' : 'Reject'}
98103
</button>
99-
</div>
104+
</div>}
100105
</div>
101106
))}
102107
<div style={{ borderTop: '1px solid var(--border-subtle)', margin: '16px 0' }} />

0 commit comments

Comments
 (0)