mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-10 07:18:10 -05:00
* feat: Initial work on security analyzer * feat: Add remote invariant client * chore: improve fault tolerance of client * feat: Add button to enable Invariant Security Analyzer * [feat] confirmation mode for bash actions * feat: Add Invariant Tab with security risk outputs * feat: Add modal setting for Confirmation Mode * fix: frontend tests for confirmation mode switch * fix: add missing CONFIRMATION_MODE value in SettingsModal.test.tsx * fix: update test to integrate new setting * feat: Initial work on security analyzer * feat: Add remote invariant client * chore: improve fault tolerance of client * feat: Add button to enable Invariant Security Analyzer * feat: Add Invariant Tab with security risk outputs * feat: integrate security analyzer with confirmation mode * feat: improve invariant analyzer tab * feat: Implement user confirmation for running bash/python code * fix: don't display rejected actions * fix: make confirmation show only on assistant messages * feat: download traces, update policy, implement settings, auto-approve based on defined risk * Fix: low risk not being shown because it's 0 * fix: duplicate logs in tab * fix: log duplication * chore: prepare for merge, remove logging * Merge confirmation_mode from OpenDevin main * test: update tests to pass * chore: finish merging changes, security analyzer now operational again * feat: document Security Analyzers * refactor: api, monitor * chore: lint, fix risk None, revert policy * fix: check security_risk for None * refactor: rename instances of invariant to security analyzer * feat: add /api/options/security-analyzers endpoint * Move security analyzer from tab to modal * Temporary fix lock when security analyzer is not chosen * feat: don't show lock at all when security analyzer is not enabled * refactor: - Frontend: * change type of SECURITY_ANALYZER from bool to string * add combobox to select SECURITY_ANALYZER, current options are "invariant and "" (no security analyzer) * Security is now a modal, lock in bottom right is visible only if there's a security analyzer selected - Backend: * add close to SecurityAnalyzer * instantiate SecurityAnalyzer based on provided string from frontend * fix: update close to be async, to be consistent with other close on resources * fix: max height of modal (prevent overflow) * feat: add logo * small fixes * update docs for creating a security analyzer module * fix linting * update timeout for http client * fix: move security_analyzer config from agent to session * feat: add security_risk to browser actions * add optional remark on combobox * fix: asdict not called on dataclass, remove invariant dependency * fix: exclude None values when serializing * feat: take default policy from invariant-server instead of being hardcoded * fix: check if policy is None * update image name * test: fix some failing runs * fix: security analyzer tests * refactor: merge confirmation_mode and security_analyzer into SecurityConfig. Change invariant error message for docker * test: add tests for invariant parsing actions / observations * fix: python linting for test_security.py * Apply suggestions from code review Co-authored-by: Engel Nyst <enyst@users.noreply.github.com> * use ActionSecurityRisk | None intead of Optional * refactor action parsing * add extra check * lint parser.py * test: add field keep_prompt to test_security * docs: add information about how to enable the analyzer * test: Remove trailing whitespace in README.md text --------- Co-authored-by: Mislav Balunovic <mislav.balunovic@gmail.com> Co-authored-by: Engel Nyst <enyst@users.noreply.github.com> Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
204 lines
6.1 KiB
TypeScript
204 lines
6.1 KiB
TypeScript
import { Spinner } from "@nextui-org/react";
|
|
import i18next from "i18next";
|
|
import React, { useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useSelector } from "react-redux";
|
|
import {
|
|
fetchAgents,
|
|
fetchModels,
|
|
fetchSecurityAnalyzers,
|
|
} from "#/services/options";
|
|
import { AvailableLanguages } from "#/i18n";
|
|
import { I18nKey } from "#/i18n/declaration";
|
|
import Session from "#/services/session";
|
|
import { RootState } from "../../../store";
|
|
import AgentState from "../../../types/AgentState";
|
|
import {
|
|
Settings,
|
|
getSettings,
|
|
getDefaultSettings,
|
|
getSettingsDifference,
|
|
settingsAreUpToDate,
|
|
maybeMigrateSettings,
|
|
saveSettings,
|
|
} from "#/services/settings";
|
|
import toast from "#/utils/toast";
|
|
import BaseModal from "../base-modal/BaseModal";
|
|
import SettingsForm from "./SettingsForm";
|
|
|
|
interface SettingsProps {
|
|
isOpen: boolean;
|
|
onOpenChange: (isOpen: boolean) => void;
|
|
}
|
|
|
|
const REQUIRED_SETTINGS = ["LLM_MODEL", "AGENT"];
|
|
|
|
function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const [models, setModels] = React.useState<string[]>([]);
|
|
const [agents, setAgents] = React.useState<string[]>([]);
|
|
const [securityAnalyzers, setSecurityAnalyzers] = React.useState<string[]>(
|
|
[],
|
|
);
|
|
const [settings, setSettings] = React.useState<Settings>({} as Settings);
|
|
const [agentIsRunning, setAgentIsRunning] = React.useState<boolean>(false);
|
|
const [loading, setLoading] = React.useState(true);
|
|
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
|
|
|
useEffect(() => {
|
|
maybeMigrateSettings();
|
|
setSettings(getSettings());
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const isRunning =
|
|
curAgentState === AgentState.RUNNING ||
|
|
curAgentState === AgentState.PAUSED ||
|
|
curAgentState === AgentState.AWAITING_USER_INPUT ||
|
|
curAgentState === AgentState.AWAITING_USER_CONFIRMATION;
|
|
setAgentIsRunning(isRunning);
|
|
}, [curAgentState]);
|
|
|
|
React.useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
setModels(await fetchModels());
|
|
setAgents(await fetchAgents());
|
|
setSecurityAnalyzers(await fetchSecurityAnalyzers());
|
|
} catch (error) {
|
|
toast.error("settings", t(I18nKey.CONFIGURATION$ERROR_FETCH_MODELS));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
const handleModelChange = (model: string) => {
|
|
setSettings((prev) => ({
|
|
...prev,
|
|
LLM_MODEL: model,
|
|
}));
|
|
};
|
|
|
|
const handleAgentChange = (agent: string) => {
|
|
setSettings((prev) => ({ ...prev, AGENT: agent }));
|
|
};
|
|
|
|
const handleLanguageChange = (language: string) => {
|
|
const key =
|
|
AvailableLanguages.find((lang) => lang.label === language)?.value ||
|
|
language;
|
|
// The appropriate key is assigned when the user selects a language.
|
|
// Otherwise, their input is reflected in the inputValue field of the Autocomplete component.
|
|
setSettings((prev) => ({ ...prev, LANGUAGE: key }));
|
|
};
|
|
|
|
const handleAPIKeyChange = (key: string) => {
|
|
setSettings((prev) => ({ ...prev, LLM_API_KEY: key }));
|
|
};
|
|
|
|
const handleConfirmationModeChange = (confirmationMode: boolean) => {
|
|
setSettings((prev) => ({ ...prev, CONFIRMATION_MODE: confirmationMode }));
|
|
};
|
|
|
|
const handleSecurityAnalyzerChange = (securityAnalyzer: string) => {
|
|
setSettings((prev) => ({
|
|
...prev,
|
|
CONFIRMATION_MODE: true,
|
|
SECURITY_ANALYZER: securityAnalyzer,
|
|
}));
|
|
};
|
|
|
|
const handleResetSettings = () => {
|
|
setSettings(getDefaultSettings);
|
|
};
|
|
|
|
const handleSaveSettings = () => {
|
|
const updatedSettings = getSettingsDifference(settings);
|
|
saveSettings(settings);
|
|
i18next.changeLanguage(settings.LANGUAGE);
|
|
Session.startNewSession();
|
|
|
|
const sensitiveKeys = ["LLM_API_KEY"];
|
|
|
|
Object.entries(updatedSettings).forEach(([key, value]) => {
|
|
if (!sensitiveKeys.includes(key)) {
|
|
toast.settingsChanged(`${key} set to "${value}"`);
|
|
} else {
|
|
toast.settingsChanged(`${key} has been updated securely.`);
|
|
}
|
|
});
|
|
|
|
localStorage.setItem(
|
|
`API_KEY_${settings.LLM_MODEL || models[0]}`,
|
|
settings.LLM_API_KEY,
|
|
);
|
|
};
|
|
|
|
let subtitle = t(I18nKey.CONFIGURATION$MODAL_SUB_TITLE);
|
|
if (loading) {
|
|
subtitle = t(I18nKey.CONFIGURATION$AGENT_LOADING);
|
|
} else if (agentIsRunning) {
|
|
subtitle = t(I18nKey.CONFIGURATION$AGENT_RUNNING);
|
|
} else if (!settingsAreUpToDate()) {
|
|
subtitle = t(I18nKey.CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE);
|
|
}
|
|
const saveIsDisabled = REQUIRED_SETTINGS.some(
|
|
(key) => !settings[key as keyof Settings],
|
|
);
|
|
|
|
return (
|
|
<BaseModal
|
|
isOpen={isOpen}
|
|
onOpenChange={onOpenChange}
|
|
title={t(I18nKey.CONFIGURATION$MODAL_TITLE)}
|
|
isDismissable={settingsAreUpToDate()}
|
|
subtitle={subtitle}
|
|
actions={[
|
|
{
|
|
label: t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL),
|
|
action: handleSaveSettings,
|
|
isDisabled: saveIsDisabled,
|
|
closeAfterAction: true,
|
|
className: "bg-primary rounded-lg",
|
|
},
|
|
{
|
|
label: t(I18nKey.CONFIGURATION$MODAL_RESET_BUTTON_LABEL),
|
|
action: handleResetSettings,
|
|
closeAfterAction: false,
|
|
className: "bg-neutral-500 rounded-lg",
|
|
},
|
|
{
|
|
label: t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL),
|
|
action: () => {
|
|
setSettings(getSettings()); // reset settings from any changes
|
|
},
|
|
isDisabled: !settingsAreUpToDate(),
|
|
closeAfterAction: true,
|
|
className: "bg-rose-600 rounded-lg",
|
|
},
|
|
]}
|
|
>
|
|
{loading && <Spinner />}
|
|
{!loading && (
|
|
<SettingsForm
|
|
disabled={agentIsRunning}
|
|
settings={settings}
|
|
models={models}
|
|
agents={agents}
|
|
securityAnalyzers={securityAnalyzers}
|
|
onModelChange={handleModelChange}
|
|
onAgentChange={handleAgentChange}
|
|
onLanguageChange={handleLanguageChange}
|
|
onAPIKeyChange={handleAPIKeyChange}
|
|
onConfirmationModeChange={handleConfirmationModeChange}
|
|
onSecurityAnalyzerChange={handleSecurityAnalyzerChange}
|
|
/>
|
|
)}
|
|
</BaseModal>
|
|
);
|
|
}
|
|
|
|
export default SettingsModal;
|