Skip to content
44 changes: 39 additions & 5 deletions app/src/components/settings/panels/AgentAccessPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const AgentAccessPanel = () => {

const [level, setLevel] = useState<AutonomyLevel>('supervised');
const [workspaceOnly, setWorkspaceOnly] = useState(false);
const [requireTaskPlanApproval, setRequireTaskPlanApproval] = useState(true);
const [trustedRoots, setTrustedRoots] = useState<TrustedRoot[]>([]);
// "Always allow" allowlist — populated by the in-chat "Always allow" button;
// shown here read-only with a Remove action (the re-protect path).
Expand Down Expand Up @@ -78,6 +79,7 @@ const AgentAccessPanel = () => {
if (cancelled) return;
setLevel(resp.result.level);
setWorkspaceOnly(resp.result.workspace_only);
setRequireTaskPlanApproval(resp.result.require_task_plan_approval ?? true);
setTrustedRoots(resp.result.trusted_roots ?? []);
setAutoApprove(resp.result.auto_approve ?? []);
} catch (e) {
Expand All @@ -100,6 +102,7 @@ const AgentAccessPanel = () => {
const persist = async (next: {
level: AutonomyLevel;
workspaceOnly: boolean;
requireTaskPlanApproval: boolean;
trustedRoots: TrustedRoot[];
// Only sent when the allowlist itself is being changed. Omitting it leaves
// the server's `auto_approve` untouched (partial patch) — important so a
Expand All @@ -118,6 +121,7 @@ const AgentAccessPanel = () => {
workspace_only: next.workspaceOnly,
trusted_roots: next.trustedRoots,
allow_tool_install: ALLOW_TOOL_INSTALL,
require_task_plan_approval: next.requireTaskPlanApproval,
...(next.autoApprove !== undefined ? { auto_approve: next.autoApprove } : {}),
});
// Only the most recent persist may write UI state back.
Expand All @@ -137,12 +141,17 @@ const AgentAccessPanel = () => {

const selectTier = (next: AutonomyLevel) => {
setLevel(next);
void persist({ level: next, workspaceOnly, trustedRoots });
void persist({ level: next, workspaceOnly, requireTaskPlanApproval, trustedRoots });
};

const toggleWorkspaceOnly = (next: boolean) => {
setWorkspaceOnly(next);
void persist({ level, workspaceOnly: next, trustedRoots });
void persist({ level, workspaceOnly: next, requireTaskPlanApproval, trustedRoots });
};

const toggleTaskPlanApproval = (next: boolean) => {
setRequireTaskPlanApproval(next);
void persist({ level, workspaceOnly, requireTaskPlanApproval: next, trustedRoots });
};

const addRoot = () => {
Expand All @@ -156,19 +165,25 @@ const AgentAccessPanel = () => {
setTrustedRoots(nextRoots);
setNewRootPath('');
setNewRootAccess('read');
void persist({ level, workspaceOnly, trustedRoots: nextRoots });
void persist({ level, workspaceOnly, requireTaskPlanApproval, trustedRoots: nextRoots });
};

const removeRoot = (path: string) => {
const nextRoots = trustedRoots.filter(r => r.path !== path);
setTrustedRoots(nextRoots);
void persist({ level, workspaceOnly, trustedRoots: nextRoots });
void persist({ level, workspaceOnly, requireTaskPlanApproval, trustedRoots: nextRoots });
};

const removeAutoApprove = (tool: string) => {
const nextList = autoApprove.filter(name => name !== tool);
setAutoApprove(nextList);
void persist({ level, workspaceOnly, trustedRoots, autoApprove: nextList });
void persist({
level,
workspaceOnly,
requireTaskPlanApproval,
trustedRoots,
autoApprove: nextList,
});
};

return (
Expand Down Expand Up @@ -248,6 +263,25 @@ const AgentAccessPanel = () => {
</label>
</section>

<section className="space-y-1">
<label className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
className="mt-0.5 cursor-pointer"
checked={requireTaskPlanApproval}
onChange={e => toggleTaskPlanApproval(e.target.checked)}
/>
<span>
<span className="text-sm font-medium text-ink">
{t('settings.agentAccess.requireTaskPlanApproval.label')}
</span>
<span className="block text-xs text-ink-soft">
{t('settings.agentAccess.requireTaskPlanApproval.desc')}
</span>
</span>
</label>
</section>
Comment thread
senamakel marked this conversation as resolved.

{/* Granted folders (trusted roots) — extra read/write reach. */}
<section className="space-y-2">
<h2 className="text-sm font-semibold text-ink">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,23 @@ describe('AgentAccessPanel', () => {
it('toggling "confine to workspace" persists workspace_only', async () => {
renderWithProviders(<AgentAccessPanel />);
await screen.findByText('Read-only');
fireEvent.click(screen.getByRole('checkbox'));
fireEvent.click(screen.getByRole('checkbox', { name: /confine to workspace/i }));
await waitFor(() =>
expect(mockUpdate).toHaveBeenCalledWith(expect.objectContaining({ workspace_only: true }))
);
});

it('toggling task plan approval persists require_task_plan_approval', async () => {
renderWithProviders(<AgentAccessPanel />);
await screen.findByText('Read-only');
fireEvent.click(screen.getByRole('checkbox', { name: /require task plan approval/i }));
await waitFor(() =>
expect(mockUpdate).toHaveBeenCalledWith(
expect.objectContaining({ require_task_plan_approval: false })
)
);
});

it('adding then removing a granted folder persists the updated list', async () => {
renderWithProviders(<AgentAccessPanel />);
await screen.findByText('Granted folders');
Expand Down Expand Up @@ -111,7 +122,9 @@ describe('AgentAccessPanel', () => {
});
renderWithProviders(<AgentAccessPanel />);
expect(await screen.findByText('/home/u/notes')).toBeInTheDocument();
expect((screen.getByRole('checkbox') as HTMLInputElement).checked).toBe(true);
expect(
(screen.getByRole('checkbox', { name: /confine to workspace/i }) as HTMLInputElement).checked
).toBe(true);
});

it('shows the empty "always-allow" state when no tools are allow-listed', async () => {
Expand Down
22 changes: 22 additions & 0 deletions app/src/lib/i18n/chunks/ar-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,28 @@ const ar4: TranslationMap = {
'pages.settings.composioSection.title': 'Composio',
'pages.settings.composioSection.description':
'التوجيه والمشغلات وسجل عمليات التكامل المدعومة بواسطة Composio.',
'conversations.taskKanban.approval.default': 'Default',
'conversations.taskKanban.approval.notRequired': 'Not required',
'conversations.taskKanban.approval.notRequiredBadge': 'no approval',
'conversations.taskKanban.approval.required': 'Required',
'conversations.taskKanban.approval.requiredBadge': 'approval',
'conversations.taskKanban.approval.requiredBeforeExecution': 'Required before execution',
'conversations.taskKanban.briefButton': 'Task brief',
'conversations.taskKanban.briefTitle': 'Task brief',
'conversations.taskKanban.closeBrief': 'Close task brief',
'conversations.taskKanban.field.acceptanceCriteria': 'Acceptance criteria',
'conversations.taskKanban.field.allowedTools': 'Allowed tools',
'conversations.taskKanban.field.approval': 'Approval',
'conversations.taskKanban.field.assignedAgent': 'Assigned agent',
'conversations.taskKanban.field.blocker': 'Blocker',
'conversations.taskKanban.field.evidence': 'Evidence',
'conversations.taskKanban.field.notes': 'Notes',
'conversations.taskKanban.field.objective': 'Objective',
'conversations.taskKanban.field.plan': 'Plan',
'conversations.taskKanban.field.status': 'Status',
'conversations.taskKanban.field.title': 'Title',
'conversations.taskKanban.saveChanges': 'Save changes',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
};

export default ar4;
3 changes: 3 additions & 0 deletions app/src/lib/i18n/chunks/ar-5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,9 @@ const ar5: TranslationMap = {
'skills.new.title': 'Create a skill',
'skills.new.placeholderBody':
'Authoring form arrives soon. For now, use the “New skill” button on the runner page.',
'settings.agentAccess.requireTaskPlanApproval.label': 'Require task plan approval',
'settings.agentAccess.requireTaskPlanApproval.desc':
'Pause before an assigned agent executes an agent-authored task brief.',
};

export default ar5;
22 changes: 22 additions & 0 deletions app/src/lib/i18n/chunks/bn-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,28 @@ const bn4: TranslationMap = {
'pages.settings.composioSection.title': 'Composio',
'pages.settings.composioSection.description':
'Composio দ্বারা চালিত ইন্টিগ্রেশনের জন্য রাউটিং, ট্রিগার এবং ইতিহাস।',
'conversations.taskKanban.approval.default': 'Default',
'conversations.taskKanban.approval.notRequired': 'Not required',
'conversations.taskKanban.approval.notRequiredBadge': 'no approval',
'conversations.taskKanban.approval.required': 'Required',
'conversations.taskKanban.approval.requiredBadge': 'approval',
'conversations.taskKanban.approval.requiredBeforeExecution': 'Required before execution',
'conversations.taskKanban.briefButton': 'Task brief',
'conversations.taskKanban.briefTitle': 'Task brief',
'conversations.taskKanban.closeBrief': 'Close task brief',
'conversations.taskKanban.field.acceptanceCriteria': 'Acceptance criteria',
'conversations.taskKanban.field.allowedTools': 'Allowed tools',
'conversations.taskKanban.field.approval': 'Approval',
'conversations.taskKanban.field.assignedAgent': 'Assigned agent',
'conversations.taskKanban.field.blocker': 'Blocker',
'conversations.taskKanban.field.evidence': 'Evidence',
'conversations.taskKanban.field.notes': 'Notes',
'conversations.taskKanban.field.objective': 'Objective',
'conversations.taskKanban.field.plan': 'Plan',
'conversations.taskKanban.field.status': 'Status',
'conversations.taskKanban.field.title': 'Title',
'conversations.taskKanban.saveChanges': 'Save changes',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
};

export default bn4;
3 changes: 3 additions & 0 deletions app/src/lib/i18n/chunks/bn-5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,9 @@ const bn5: TranslationMap = {
'skills.new.title': 'Create a skill',
'skills.new.placeholderBody':
'Authoring form arrives soon. For now, use the “New skill” button on the runner page.',
'settings.agentAccess.requireTaskPlanApproval.label': 'Require task plan approval',
'settings.agentAccess.requireTaskPlanApproval.desc':
'Pause before an assigned agent executes an agent-authored task brief.',
};

export default bn5;
22 changes: 22 additions & 0 deletions app/src/lib/i18n/chunks/de-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,28 @@ const de4: TranslationMap = {
'pages.settings.composioSection.title': 'Composio',
'pages.settings.composioSection.description':
'Routing, Trigger und Verlauf für Integrationen, die von Composio unterstützt werden.',
'conversations.taskKanban.approval.default': 'Default',
'conversations.taskKanban.approval.notRequired': 'Not required',
'conversations.taskKanban.approval.notRequiredBadge': 'no approval',
'conversations.taskKanban.approval.required': 'Required',
'conversations.taskKanban.approval.requiredBadge': 'approval',
'conversations.taskKanban.approval.requiredBeforeExecution': 'Required before execution',
'conversations.taskKanban.briefButton': 'Task brief',
'conversations.taskKanban.briefTitle': 'Task brief',
'conversations.taskKanban.closeBrief': 'Close task brief',
'conversations.taskKanban.field.acceptanceCriteria': 'Acceptance criteria',
'conversations.taskKanban.field.allowedTools': 'Allowed tools',
'conversations.taskKanban.field.approval': 'Approval',
'conversations.taskKanban.field.assignedAgent': 'Assigned agent',
'conversations.taskKanban.field.blocker': 'Blocker',
'conversations.taskKanban.field.evidence': 'Evidence',
'conversations.taskKanban.field.notes': 'Notes',
'conversations.taskKanban.field.objective': 'Objective',
'conversations.taskKanban.field.plan': 'Plan',
'conversations.taskKanban.field.status': 'Status',
'conversations.taskKanban.field.title': 'Title',
'conversations.taskKanban.saveChanges': 'Save changes',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
};

export default de4;
3 changes: 3 additions & 0 deletions app/src/lib/i18n/chunks/de-5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,9 @@ const de5: TranslationMap = {
'skills.new.title': 'Create a skill',
'skills.new.placeholderBody':
'Authoring form arrives soon. For now, use the “New skill” button on the runner page.',
'settings.agentAccess.requireTaskPlanApproval.label': 'Require task plan approval',
'settings.agentAccess.requireTaskPlanApproval.desc':
'Pause before an assigned agent executes an agent-authored task brief.',
};

export default de5;
22 changes: 22 additions & 0 deletions app/src/lib/i18n/chunks/en-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ const en4: TranslationMap = {
'conversations.taskKanban.moveLeft': 'Move left',
'conversations.taskKanban.moveRight': 'Move right',
'conversations.taskKanban.title': 'Tasks',
'conversations.taskKanban.approval.default': 'Default',
'conversations.taskKanban.approval.notRequired': 'Not required',
'conversations.taskKanban.approval.notRequiredBadge': 'no approval',
'conversations.taskKanban.approval.required': 'Required',
'conversations.taskKanban.approval.requiredBadge': 'approval',
'conversations.taskKanban.approval.requiredBeforeExecution': 'Required before execution',
'conversations.taskKanban.briefButton': 'Task brief',
'conversations.taskKanban.briefTitle': 'Task brief',
'conversations.taskKanban.closeBrief': 'Close task brief',
'conversations.taskKanban.field.acceptanceCriteria': 'Acceptance criteria',
'conversations.taskKanban.field.allowedTools': 'Allowed tools',
'conversations.taskKanban.field.approval': 'Approval',
'conversations.taskKanban.field.assignedAgent': 'Assigned agent',
'conversations.taskKanban.field.blocker': 'Blocker',
'conversations.taskKanban.field.evidence': 'Evidence',
'conversations.taskKanban.field.notes': 'Notes',
'conversations.taskKanban.field.objective': 'Objective',
'conversations.taskKanban.field.plan': 'Plan',
'conversations.taskKanban.field.status': 'Status',
'conversations.taskKanban.field.title': 'Title',
'conversations.taskKanban.saveChanges': 'Save changes',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
'conversations.toolTimeline.turn': 'turn',
'conversations.toolTimeline.workerThread': 'worker thread',
'daemon.serviceBlockingGate.body': 'Body',
Expand Down
3 changes: 3 additions & 0 deletions app/src/lib/i18n/chunks/en-5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ const en5: TranslationMap = {
'settings.agentAccess.confine.label': 'Confine to workspace',
'settings.agentAccess.confine.desc':
'Restrict the agent to the workspace directory (plus any granted folders), whichever access mode is selected. When off, it can reach anywhere your user can — except the always-blocked credential and system directories.',
'settings.agentAccess.requireTaskPlanApproval.label': 'Require task plan approval',
'settings.agentAccess.requireTaskPlanApproval.desc':
'Pause before an assigned agent executes an agent-authored task brief.',
'settings.agentAccess.grantedFolders': 'Granted folders',
'settings.agentAccess.alwaysAllow': 'Always-allowed tools',
'settings.agentAccess.alwaysAllowDesc':
Expand Down
22 changes: 22 additions & 0 deletions app/src/lib/i18n/chunks/es-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,28 @@ const es4: TranslationMap = {
'pages.settings.composioSection.title': 'Composio',
'pages.settings.composioSection.description':
'Enrutamiento, activadores e historial para integraciones impulsadas por Composio.',
'conversations.taskKanban.approval.default': 'Default',
'conversations.taskKanban.approval.notRequired': 'Not required',
'conversations.taskKanban.approval.notRequiredBadge': 'no approval',
'conversations.taskKanban.approval.required': 'Required',
'conversations.taskKanban.approval.requiredBadge': 'approval',
'conversations.taskKanban.approval.requiredBeforeExecution': 'Required before execution',
'conversations.taskKanban.briefButton': 'Task brief',
'conversations.taskKanban.briefTitle': 'Task brief',
'conversations.taskKanban.closeBrief': 'Close task brief',
'conversations.taskKanban.field.acceptanceCriteria': 'Acceptance criteria',
'conversations.taskKanban.field.allowedTools': 'Allowed tools',
'conversations.taskKanban.field.approval': 'Approval',
'conversations.taskKanban.field.assignedAgent': 'Assigned agent',
'conversations.taskKanban.field.blocker': 'Blocker',
'conversations.taskKanban.field.evidence': 'Evidence',
'conversations.taskKanban.field.notes': 'Notes',
'conversations.taskKanban.field.objective': 'Objective',
'conversations.taskKanban.field.plan': 'Plan',
'conversations.taskKanban.field.status': 'Status',
'conversations.taskKanban.field.title': 'Title',
'conversations.taskKanban.saveChanges': 'Save changes',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
};

export default es4;
3 changes: 3 additions & 0 deletions app/src/lib/i18n/chunks/es-5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,9 @@ const es5: TranslationMap = {
'skills.new.title': 'Create a skill',
'skills.new.placeholderBody':
'Authoring form arrives soon. For now, use the “New skill” button on the runner page.',
'settings.agentAccess.requireTaskPlanApproval.label': 'Require task plan approval',
'settings.agentAccess.requireTaskPlanApproval.desc':
'Pause before an assigned agent executes an agent-authored task brief.',
};

export default es5;
22 changes: 22 additions & 0 deletions app/src/lib/i18n/chunks/fr-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,28 @@ const fr4: TranslationMap = {
'pages.settings.composioSection.title': 'Composio',
'pages.settings.composioSection.description':
'Routage, déclencheurs et historique pour les intégrations optimisées par Composio.',
'conversations.taskKanban.approval.default': 'Default',
'conversations.taskKanban.approval.notRequired': 'Not required',
'conversations.taskKanban.approval.notRequiredBadge': 'no approval',
'conversations.taskKanban.approval.required': 'Required',
'conversations.taskKanban.approval.requiredBadge': 'approval',
'conversations.taskKanban.approval.requiredBeforeExecution': 'Required before execution',
'conversations.taskKanban.briefButton': 'Task brief',
'conversations.taskKanban.briefTitle': 'Task brief',
'conversations.taskKanban.closeBrief': 'Close task brief',
'conversations.taskKanban.field.acceptanceCriteria': 'Acceptance criteria',
'conversations.taskKanban.field.allowedTools': 'Allowed tools',
'conversations.taskKanban.field.approval': 'Approval',
'conversations.taskKanban.field.assignedAgent': 'Assigned agent',
'conversations.taskKanban.field.blocker': 'Blocker',
'conversations.taskKanban.field.evidence': 'Evidence',
'conversations.taskKanban.field.notes': 'Notes',
'conversations.taskKanban.field.objective': 'Objective',
'conversations.taskKanban.field.plan': 'Plan',
'conversations.taskKanban.field.status': 'Status',
'conversations.taskKanban.field.title': 'Title',
'conversations.taskKanban.saveChanges': 'Save changes',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
};

export default fr4;
3 changes: 3 additions & 0 deletions app/src/lib/i18n/chunks/fr-5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,9 @@ const fr5: TranslationMap = {
'skills.new.title': 'Create a skill',
'skills.new.placeholderBody':
'Authoring form arrives soon. For now, use the “New skill” button on the runner page.',
'settings.agentAccess.requireTaskPlanApproval.label': 'Require task plan approval',
'settings.agentAccess.requireTaskPlanApproval.desc':
'Pause before an assigned agent executes an agent-authored task brief.',
};

export default fr5;
Loading
Loading