fix: mini hardening

This commit is contained in:
Alex committed 2026-08-23 11:22:12 +01:00
1 parent 33d93ba0ca
commit b47bfa8a37
12 files changed
+353 -74

No files matched your search

+32 -3
View File
@@ -17,12 +17,14 @@ import Trash from '../assets/red-trash.svg';
import ThreeDots from '../assets/three-dots.svg';
import UnPin from '../assets/unpin.svg';
import { Avatar } from '../components/ui/avatar';
import { Button } from '../components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../components/ui/dropdown-menu';
import { Modal } from '../components/ui/modal';
import ConfirmationModal from '../modals/ConfirmationModal';
import MoveToFolderModal from '../modals/MoveToFolderModal';
import { ActiveState } from '../models/misc';
@@ -67,6 +69,7 @@ export default function AgentCard({
useState<ActiveState>('INACTIVE');
const [moveModalState, setMoveModalState] = useState<ActiveState>('INACTIVE');
const [shareModalOpen, setShareModalOpen] = useState(false);
const [exportError, setExportError] = useState<string | null>(null);
const menuOptionsConfig: Record<string, AgentMenuOption[]> = {
template: [
@@ -302,7 +305,12 @@ export default function AgentCard({
.json()
.then((data) => data?.message)
.catch(() => null);
throw new Error(message || t('agents.exportAgentFailed'));
// Server-side refusals (e.g. a workflow referencing too many
// resources) carry an actionable message; flag it so the catch can
// tell it apart from a transport error like "Failed to fetch".
const error = new Error(message || t('agents.exportAgentFailed'));
error.name = 'ExportRefused';
throw error;
}
const yamlText = await response.text();
const blob = new Blob([yamlText], { type: 'application/x-yaml' });
@@ -316,8 +324,10 @@ export default function AgentCard({
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error:', error);
alert(
error instanceof Error ? error.message : t('agents.exportAgentFailed'),
setExportError(
error instanceof Error && error.name === 'ExportRefused'
? error.message
: t('agents.exportAgentFailed'),
);
}
};
@@ -470,6 +480,25 @@ export default function AgentCard({
cancelLabel="Cancel"
variant="danger"
/>
<Modal
open={exportError !== null}
onOpenChange={(open) => {
if (!open) setExportError(null);
}}
title={t('agents.exportAgentFailed')}
size="sm"
footer={
<Button
type="button"
onClick={() => setExportError(null)}
className="rounded-3xl px-5"
>
{t('agents.close')}
</Button>
}
>
<p className="text-muted-foreground text-sm">{exportError}</p>
</Modal>
<MoveToFolderModal
modalState={moveModalState}
setModalState={setMoveModalState}
+2
View File
@@ -1032,6 +1032,7 @@
"willUpdate": "Updates an existing agent (matched by {{matchedBy}}).",
"workflowCreate": "Includes a workflow with {{nodes}} nodes — it will be created for you.",
"workflowUpdate": "Includes a workflow with {{nodes}} nodes — your existing workflow will be updated.",
"workflowDelete": "Includes no workflow — the agent's existing workflow ({{nodes}} nodes), its run history and its artifacts will be permanently deleted.",
"sources": "Sources",
"tools": "Tools",
"models": "Models",
@@ -1152,6 +1153,7 @@
"importAgent": "Import",
"exportAgent": "Export as YAML",
"exportAgentFailed": "Failed to export agent",
"close": "Schließen",
"shareWithTeam": "Mit Team teilen",
"backToAll": "Zurück zu allen Agenten",
"pageHeader": {
+2
View File
@@ -1037,6 +1037,7 @@
"willUpdate": "Updates an existing agent (matched by {{matchedBy}}).",
"workflowCreate": "Includes a workflow with {{nodes}} nodes — it will be created for you.",
"workflowUpdate": "Includes a workflow with {{nodes}} nodes — your existing workflow will be updated.",
"workflowDelete": "Includes no workflow — the agent's existing workflow ({{nodes}} nodes), its run history and its artifacts will be permanently deleted.",
"sources": "Sources",
"tools": "Tools",
"models": "Models",
@@ -1161,6 +1162,7 @@
"importAgent": "Import",
"exportAgent": "Export as YAML",
"exportAgentFailed": "Failed to export agent",
"close": "Close",
"shareWithTeam": "Share with team",
"backToAll": "Back to all agents",
"pageHeader": {
+2
View File
@@ -1032,6 +1032,7 @@
"willUpdate": "Updates an existing agent (matched by {{matchedBy}}).",
"workflowCreate": "Includes a workflow with {{nodes}} nodes — it will be created for you.",
"workflowUpdate": "Includes a workflow with {{nodes}} nodes — your existing workflow will be updated.",
"workflowDelete": "Includes no workflow — the agent's existing workflow ({{nodes}} nodes), its run history and its artifacts will be permanently deleted.",
"sources": "Sources",
"tools": "Tools",
"models": "Models",
@@ -1152,6 +1153,7 @@
"importAgent": "Import",
"exportAgent": "Export as YAML",
"exportAgentFailed": "Failed to export agent",
"close": "Cerrar",
"shareWithTeam": "Compartir con el equipo",
"backToAll": "Volver a todos los agentes",
"pageHeader": {
+2
View File
@@ -1032,6 +1032,7 @@
"willUpdate": "Updates an existing agent (matched by {{matchedBy}}).",
"workflowCreate": "Includes a workflow with {{nodes}} nodes — it will be created for you.",
"workflowUpdate": "Includes a workflow with {{nodes}} nodes — your existing workflow will be updated.",
"workflowDelete": "Includes no workflow — the agent's existing workflow ({{nodes}} nodes), its run history and its artifacts will be permanently deleted.",
"sources": "Sources",
"tools": "Tools",
"models": "Models",
@@ -1152,6 +1153,7 @@
"importAgent": "Import",
"exportAgent": "Export as YAML",
"exportAgentFailed": "Failed to export agent",
"close": "閉じる",
"shareWithTeam": "チームと共有",
"backToAll": "すべてのエージェントに戻る",
"pageHeader": {
+2
View File
@@ -1032,6 +1032,7 @@
"willUpdate": "Updates an existing agent (matched by {{matchedBy}}).",
"workflowCreate": "Includes a workflow with {{nodes}} nodes — it will be created for you.",
"workflowUpdate": "Includes a workflow with {{nodes}} nodes — your existing workflow will be updated.",
"workflowDelete": "Includes no workflow — the agent's existing workflow ({{nodes}} nodes), its run history and its artifacts will be permanently deleted.",
"sources": "Sources",
"tools": "Tools",
"models": "Models",
@@ -1152,6 +1153,7 @@
"importAgent": "Import",
"exportAgent": "Export as YAML",
"exportAgentFailed": "Failed to export agent",
"close": "Закрыть",
"shareWithTeam": "Поделиться с командой",
"backToAll": "Вернуться ко всем агентам",
"pageHeader": {
+2
View File
@@ -1032,6 +1032,7 @@
"willUpdate": "Updates an existing agent (matched by {{matchedBy}}).",
"workflowCreate": "Includes a workflow with {{nodes}} nodes — it will be created for you.",
"workflowUpdate": "Includes a workflow with {{nodes}} nodes — your existing workflow will be updated.",
"workflowDelete": "Includes no workflow — the agent's existing workflow ({{nodes}} nodes), its run history and its artifacts will be permanently deleted.",
"sources": "Sources",
"tools": "Tools",
"models": "Models",
@@ -1152,6 +1153,7 @@
"importAgent": "Import",
"exportAgent": "Export as YAML",
"exportAgentFailed": "Failed to export agent",
"close": "關閉",
"shareWithTeam": "與團隊分享",
"backToAll": "返回所有代理",
"pageHeader": {
+2
View File
@@ -1032,6 +1032,7 @@
"willUpdate": "Updates an existing agent (matched by {{matchedBy}}).",
"workflowCreate": "Includes a workflow with {{nodes}} nodes — it will be created for you.",
"workflowUpdate": "Includes a workflow with {{nodes}} nodes — your existing workflow will be updated.",
"workflowDelete": "Includes no workflow — the agent's existing workflow ({{nodes}} nodes), its run history and its artifacts will be permanently deleted.",
"sources": "Sources",
"tools": "Tools",
"models": "Models",
@@ -1152,6 +1153,7 @@
"importAgent": "Import",
"exportAgent": "Export as YAML",
"exportAgentFailed": "Failed to export agent",
"close": "关闭",
"shareWithTeam": "与团队共享",
"backToAll": "返回所有代理",
"pageHeader": {
+51 -28
View File
@@ -1,6 +1,6 @@
import { AlertTriangle, CheckCircle2 } from 'lucide-react';
import { useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { type FileRejection, useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
@@ -58,7 +58,7 @@ type ImportPlan = {
workflow?: {
nodes: number;
edges: number;
action: 'create' | 'update';
action: 'create' | 'update' | 'delete';
} | null;
};
@@ -124,19 +124,28 @@ export default function ImportAgentModal({
setYamlText(await selectedFile.text());
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop: (acceptedFiles: File[]) => {
if (acceptedFiles[0]) processFile(acceptedFiles[0]);
},
multiple: false,
// Declared here (not via getInputProps) so drag-and-drop is filtered
// too, with drag-over rejection feedback; processFile's extension check
// stays as a backstop for odd MIME reports.
accept: {
'application/x-yaml': ['.yaml', '.yml'],
'text/yaml': ['.yaml', '.yml'],
},
});
const { getRootProps, getInputProps, isDragActive, isDragReject } =
useDropzone({
onDrop: (acceptedFiles: File[], fileRejections: FileRejection[]) => {
// A rejected file never reaches acceptedFiles, so without this the
// drop is a silent no-op and any previously picked file stays staged.
if (fileRejections.length > 0) {
setFileName('');
setYamlText('');
setError(t('modals.importAgent.invalidFileType'));
return;
}
if (acceptedFiles[0]) processFile(acceptedFiles[0]);
},
multiple: false,
// Declared here (not via getInputProps) so drag-and-drop is filtered
// too; processFile's extension check stays as a backstop for odd MIME
// reports.
accept: {
'application/x-yaml': ['.yaml', '.yml'],
'text/yaml': ['.yaml', '.yml'],
},
});
const handleAnalyze = async () => {
if (!yamlText) return;
@@ -320,7 +329,11 @@ export default function ImportAgentModal({
<div
{...getRootProps({
className: `border-border hover:border-primary flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 transition-colors ${
isDragActive ? 'border-primary' : ''
isDragReject
? 'border-destructive'
: isDragActive
? 'border-primary'
: ''
}`,
})}
>
@@ -346,18 +359,28 @@ export default function ImportAgentModal({
: t('modals.importAgent.willCreate')}
</div>
{plan.workflow && (
<p className="flex items-center gap-2 text-sm">
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
{plan.workflow.action === 'update'
? t('modals.importAgent.workflowUpdate', {
nodes: plan.workflow.nodes,
})
: t('modals.importAgent.workflowCreate', {
nodes: plan.workflow.nodes,
})}
</p>
)}
{plan.workflow &&
(plan.workflow.action === 'delete' ? (
// Irreversible: the graph, its run history and its artifacts
// all go. Warn rather than confirming with a green check.
<p className="text-destructive flex items-center gap-2 text-sm">
<AlertTriangle className="h-4 w-4 shrink-0" />
{t('modals.importAgent.workflowDelete', {
nodes: plan.workflow.nodes,
})}
</p>
) : (
<p className="flex items-center gap-2 text-sm">
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
{plan.workflow.action === 'update'
? t('modals.importAgent.workflowUpdate', {
nodes: plan.workflow.nodes,
})
: t('modals.importAgent.workflowCreate', {
nodes: plan.workflow.nodes,
})}
</p>
))}
{plan.sources.length > 0 && (
<Section title={t('modals.importAgent.sources')}>
+28 -15
View File
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { type FileRejection, useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
@@ -73,19 +73,28 @@ export default function ImportSpecModal({
setParsedResult(null);
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop: (acceptedFiles: File[]) => {
if (acceptedFiles[0]) processFile(acceptedFiles[0]);
},
multiple: false,
// Declared here (not via getInputProps) so drag-and-drop is filtered
// too; processFile's extension check stays as a backstop.
accept: {
'application/json': ['.json'],
'application/x-yaml': ['.yaml', '.yml'],
'text/yaml': ['.yaml', '.yml'],
},
});
const { getRootProps, getInputProps, isDragActive, isDragReject } =
useDropzone({
onDrop: (acceptedFiles: File[], fileRejections: FileRejection[]) => {
// A rejected file never reaches acceptedFiles, so without this the
// drop is a silent no-op and any previously picked file stays staged.
if (fileRejections.length > 0) {
setFile(null);
setParsedResult(null);
setError(t('modals.importSpec.invalidFileType'));
return;
}
if (acceptedFiles[0]) processFile(acceptedFiles[0]);
},
multiple: false,
// Declared here (not via getInputProps) so drag-and-drop is filtered
// too; processFile's extension check stays as a backstop.
accept: {
'application/json': ['.json'],
'application/x-yaml': ['.yaml', '.yml'],
'text/yaml': ['.yaml', '.yml'],
},
});
const handleParse = async () => {
if (!file) return;
@@ -209,7 +218,11 @@ export default function ImportSpecModal({
<div
{...getRootProps({
className: `border-border dark:border-border hover:border-primary dark:hover:border-primary flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 transition-colors ${
isDragActive ? 'border-primary' : ''
isDragReject
? 'border-destructive'
: isDragActive
? 'border-primary'
: ''
}`,
})}
>