Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6226ed4
Add generated tool admission checks
vaddisrinivas May 23, 2026
21a7bf3
Address generated tool admission feedback
vaddisrinivas May 24, 2026
4663136
Normalize generated tool provider admission
vaddisrinivas May 24, 2026
f6fd8a0
Merge remote-tracking branch 'upstream/main' into pr/2549
senamakel May 25, 2026
618a519
Merge remote-tracking branch 'upstream/main' into pr/2549
senamakel May 25, 2026
9b72e8c
Wait for backend session in mega flow
vaddisrinivas May 25, 2026
01eb678
feat(tool_policy): add diagnostics RPC and Developer Options panel
May 26, 2026
3c63b46
chore(app): format diagnostics panel files
May 26, 2026
4dacb92
fix(tool_policy): address PR review — i18n, redaction, test regex
May 26, 2026
4e7a457
chore: apply pre-push auto-fixes
May 26, 2026
a739c7d
test(tool_policy): clear denial buffer before registry tests
May 26, 2026
9d4bbec
Merge remote-tracking branch 'origin/main' into codex/oh-2542-generat…
vaddisrinivas May 26, 2026
6842137
test(e2e): remove unused backend session helper
vaddisrinivas May 26, 2026
622bac6
Merge remote-tracking branch 'upstream/main'
sanil-23 May 28, 2026
3dee2e6
Merge branch 'main' into pr/2715
sanil-23 May 28, 2026
d517226
fix(i18n): propagate toolPolicyDiagnostics keys to locale chunks
sanil-23 May 28, 2026
a7fa47b
Merge remote-tracking branch 'upstream/main' into pr/2715
sanil-23 May 28, 2026
896cf15
fix(tool_registry): gate ops_test.rs with #[cfg(test)]
sanil-23 May 28, 2026
e3900e8
Merge remote-tracking branch 'upstream/main' into pr/2715
sanil-23 May 28, 2026
b74dfee
Merge remote-tracking branch 'upstream/main' into pr/2715
sanil-23 May 28, 2026
6a36ae2
Merge remote-tracking branch 'upstream/main' into pr/2715
sanil-23 May 28, 2026
3442402
test(tool_policy): lift ToolPolicyDiagnosticsPanel coverage above 80%…
sanil-23 May 28, 2026
a067ebf
style: prettier --write on ToolPolicyDiagnosticsPanel test
sanil-23 May 28, 2026
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
16 changes: 16 additions & 0 deletions app/src/components/settings/panels/DeveloperOptionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,22 @@ const developerItems = [
</svg>
),
},
{
id: 'tool-policy-diagnostics',
titleKey: 'devOptions.diagnostics',
descriptionKey: 'devOptions.toolPolicyDiagnosticsDesc',
route: 'tool-policy-diagnostics',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 17v-5a2 2 0 012-2h2a2 2 0 012 2v5m-8 0h8m-8 0H7a2 2 0 01-2-2V7a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2"
/>
</svg>
),
},
{
id: 'intelligence',
titleKey: 'settings.developerMenu.intelligence.title',
Expand Down
275 changes: 275 additions & 0 deletions app/src/components/settings/panels/ToolPolicyDiagnosticsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import { useEffect, useMemo, useState } from 'react';

import { useT } from '../../../lib/i18n/I18nContext';
import { callCoreRpc } from '../../../services/coreRpcClient';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';

type ToolPolicyDiagnostics = {
total_tools: number;
enabled_tools: number;
mcp_stdio_tools: number;
json_rpc_tools: number;
possible_write_surfaces: string[];
policy_surfaces: string[];
posture: {
autonomy_level: string;
workspace_only: boolean;
max_actions_per_hour: number;
require_approval_for_medium_risk: boolean;
block_high_risk_commands: boolean;
};
mcp_allowlists: {
enabled: boolean;
server_count: number;
enabled_server_count: number;
servers: {
name: string;
enabled: boolean;
allowed_tools_count: number;
disallowed_tools_count: number;
has_allowlist: boolean;
has_denylist: boolean;
}[];
};
mcp_write_audit: { enabled: boolean; recent_rows: number | null; last_error: string | null };
recent_denials: {
timestamp_ms: number;
tool_name: string;
policy: string;
action: string;
reason: string;
}[];
};

const ToolPolicyDiagnosticsPanel = () => {
const { t } = useT();
const { navigateBack, breadcrumbs } = useSettingsNavigation();

const [status, setStatus] = useState<
| { kind: 'loading' }
| { kind: 'ready'; diagnostics: ToolPolicyDiagnostics }
| { kind: 'error'; message: string }
>({ kind: 'loading' });

useEffect(() => {
let cancelled = false;
(async () => {
try {
const diagnostics = await callCoreRpc<ToolPolicyDiagnostics>({
method: 'tool_registry.diagnostics',
params: {},
timeoutMs: 10_000,
});
if (cancelled) return;
setStatus({ kind: 'ready', diagnostics });
} catch (err) {
if (cancelled) return;
setStatus({ kind: 'error', message: err instanceof Error ? err.message : String(err) });
}
})();
return () => {
cancelled = true;
};
}, []);

const body = useMemo(() => {
if (status.kind === 'loading') {
return (
<div className="px-4 py-3 text-sm text-sage-700 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.loading')}
</div>
);
}
if (status.kind === 'error') {
return (
<div className="px-4 py-3 rounded-lg border border-coral-300 dark:border-coral-500/40 bg-coral-50 dark:bg-coral-500/10">
<div className="text-sm font-semibold text-coral-900 dark:text-coral-200">
{t('devOptions.toolPolicyDiagnostics.unavailable')}
</div>
<div className="text-xs text-coral-800 dark:text-coral-200 mt-1 font-mono break-words">
{status.message}
</div>
</div>
);
}

const d = status.diagnostics;
const recentRows =
d.mcp_write_audit.recent_rows === null ? '—' : String(d.mcp_write_audit.recent_rows);

return (
<div className="px-4 pt-3 pb-6 flex flex-col gap-3">
<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-sage-50 dark:bg-sage-500/10">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.posture.title')}
</div>
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-xs">
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.autonomy')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">
{d.posture.autonomy_level}
</dd>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.workspaceOnly')}
</dt>
<dd className="text-sage-900 dark:text-sage-200">{String(d.posture.workspace_only)}</dd>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.maxActionsPerHour')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">
{d.posture.max_actions_per_hour}
</dd>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.approvalMediumRisk')}
</dt>
<dd className="text-sage-900 dark:text-sage-200">
{String(d.posture.require_approval_for_medium_risk)}
</dd>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.blockHighRisk')}
</dt>
<dd className="text-sage-900 dark:text-sage-200">
{String(d.posture.block_high_risk_commands)}
</dd>
</dl>
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.inventory.title')}
</div>
<dl className="mt-2 grid grid-cols-2 gap-x-6 gap-y-1 text-xs">
<div>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.inventory.totalTools')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">{d.total_tools}</dd>
</div>
<div>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.inventory.enabledTools')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">{d.enabled_tools}</dd>
</div>
<div>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.inventory.mcpStdioTools')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">{d.mcp_stdio_tools}</dd>
</div>
<div>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.inventory.jsonRpcTools')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">{d.json_rpc_tools}</dd>
</div>
</dl>
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.mcpAllowlists.title')}
</div>
<div className="mt-1 text-xs text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.mcpAllowlists.summary')
.replace('{enabled}', String(d.mcp_allowlists.enabled))
.replace('{enabledCount}', String(d.mcp_allowlists.enabled_server_count))
.replace('{totalCount}', String(d.mcp_allowlists.server_count))}
</div>
{d.mcp_allowlists.servers.length > 0 && (
<ul className="mt-2 text-xs space-y-1">
{d.mcp_allowlists.servers.slice(0, 10).map(s => (
<li key={s.name} className="flex items-center justify-between gap-3">
<span
className="font-mono text-sage-900 dark:text-sage-200 truncate"
title={s.name}>
{s.name || t('devOptions.toolPolicyDiagnostics.mcpAllowlists.unnamed')}
</span>
<span className="text-sage-700 dark:text-sage-300 font-mono">
{t('devOptions.toolPolicyDiagnostics.mcpAllowlists.allowDeny')
.replace('{allowCount}', String(s.allowed_tools_count))
.replace('{denyCount}', String(s.disallowed_tools_count))}
</span>
</li>
))}
</ul>
)}
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.mcpWriteAudit.title')}
</div>
<div className="mt-1 text-xs text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.mcpWriteAudit.summary')
.replace('{enabled}', String(d.mcp_write_audit.enabled))
.replace('{recentRows}', recentRows)}
</div>
{d.mcp_write_audit.last_error && (
<div className="mt-2 text-xs text-coral-700 dark:text-coral-200 font-mono break-words">
{d.mcp_write_audit.last_error}
</div>
)}
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.recentBlocked.title')}
</div>
{d.recent_denials.length === 0 ? (
<div className="mt-1 text-xs text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.recentBlocked.empty')}
</div>
) : (
<ul className="mt-2 text-xs space-y-1">
{d.recent_denials.slice(0, 10).map(entry => (
<li
key={`${entry.timestamp_ms}:${entry.tool_name}`}
className="flex flex-col gap-0.5">
<div className="flex items-center justify-between gap-3">
<span
className="font-mono text-sage-900 dark:text-sage-200 truncate"
title={entry.tool_name}>
{entry.tool_name}
</span>
<span className="text-sage-700 dark:text-sage-300 font-mono">
{entry.policy}:{entry.action}
</span>
</div>
<div className="text-sage-700 dark:text-sage-300 break-words">{entry.reason}</div>
</li>
))}
</ul>
)}
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.redactedSurfaces.title')}
</div>
<div className="mt-1 text-xs text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.redactedSurfaces.summary')
.replace('{writeCount}', String(d.possible_write_surfaces.length))
.replace('{policyCount}', String(d.policy_surfaces.length))}
</div>
</div>
</div>
);
}, [status, t]);

return (
<div className="z-10 relative">
<SettingsHeader
title={t('devOptions.diagnostics')}
showBackButton={true}
onBack={navigateBack}
breadcrumbs={breadcrumbs}
/>
{body}
</div>
);
};

export default ToolPolicyDiagnosticsPanel;
Loading
Loading