mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 836866915e |
@@ -108,7 +108,7 @@ describe("Content", () => {
|
||||
});
|
||||
|
||||
describe("Advanced form", () => {
|
||||
it("should conditionally show security analyzer based on confirmation mode", async () => {
|
||||
it("should conditionally show security analyzer based on security policy", async () => {
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
@@ -116,26 +116,37 @@ describe("Content", () => {
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
|
||||
const confirmation = screen.getByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
const securityPolicy = screen.getByTestId("security-policy-input");
|
||||
|
||||
// Initially confirmation mode is false, so security analyzer should not be visible
|
||||
expect(confirmation).not.toBeChecked();
|
||||
// Initially security policy is "never", so security analyzer should not be visible
|
||||
expect(securityPolicy).toHaveValue("SETTINGS$SECURITY_POLICY_NEVER");
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Enable confirmation mode
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).toBeChecked();
|
||||
// Change security policy to "always"
|
||||
await userEvent.click(securityPolicy);
|
||||
const alwaysOption = screen.getByText("SETTINGS$SECURITY_POLICY_ALWAYS");
|
||||
await userEvent.click(alwaysOption);
|
||||
expect(securityPolicy).toHaveValue("SETTINGS$SECURITY_POLICY_ALWAYS");
|
||||
|
||||
// Security analyzer should now be visible
|
||||
screen.getByTestId("security-analyzer-input");
|
||||
|
||||
// Disable confirmation mode again
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).not.toBeChecked();
|
||||
// Change security policy to "risky"
|
||||
await userEvent.click(securityPolicy);
|
||||
const riskyOption = screen.getByText("SETTINGS$SECURITY_POLICY_RISKY");
|
||||
await userEvent.click(riskyOption);
|
||||
expect(securityPolicy).toHaveValue("SETTINGS$SECURITY_POLICY_RISKY");
|
||||
|
||||
// Security analyzer should still be visible
|
||||
screen.getByTestId("security-analyzer-input");
|
||||
|
||||
// Change security policy back to "never"
|
||||
await userEvent.click(securityPolicy);
|
||||
const neverOption = screen.getByText("SETTINGS$SECURITY_POLICY_NEVER");
|
||||
await userEvent.click(neverOption);
|
||||
expect(securityPolicy).toHaveValue("SETTINGS$SECURITY_POLICY_NEVER");
|
||||
|
||||
// Security analyzer should be hidden again
|
||||
expect(
|
||||
@@ -224,7 +235,7 @@ describe("Content", () => {
|
||||
llm_base_url: "https://api.openai.com/v1/chat/completions",
|
||||
llm_api_key_set: true,
|
||||
agent: "CoActAgent",
|
||||
confirmation_mode: true,
|
||||
security_policy: "always",
|
||||
enable_default_condenser: false,
|
||||
security_analyzer: "none",
|
||||
});
|
||||
@@ -236,11 +247,9 @@ describe("Content", () => {
|
||||
const baseUrl = screen.getByTestId("base-url-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
const agent = screen.getByTestId("agent-input");
|
||||
const confirmation = screen.getByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
const securityPolicy = screen.getByTestId("security-policy-input");
|
||||
const condensor = screen.getByTestId("enable-memory-condenser-switch");
|
||||
const securityAnalyzer = screen.getByTestId("security-analyzer-input");
|
||||
const securityAnalyzer = await screen.findByTestId("security-analyzer-input");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(model).toHaveValue("openai/gpt-4o");
|
||||
@@ -250,7 +259,7 @@ describe("Content", () => {
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(apiKey).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(agent).toHaveValue("CoActAgent");
|
||||
expect(confirmation).toBeChecked();
|
||||
expect(securityPolicy).toHaveValue("SETTINGS$SECURITY_POLICY_ALWAYS");
|
||||
expect(condensor).not.toBeChecked();
|
||||
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE");
|
||||
});
|
||||
@@ -310,7 +319,7 @@ describe("Form submission", () => {
|
||||
const baseUrl = screen.getByTestId("base-url-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
const agent = screen.getByTestId("agent-input");
|
||||
const confirmation = screen.getByTestId("enable-confirmation-mode-switch");
|
||||
const securityPolicy = screen.getByTestId("security-policy-input");
|
||||
const condensor = screen.getByTestId("enable-memory-condenser-switch");
|
||||
|
||||
// enter custom model
|
||||
@@ -325,9 +334,11 @@ describe("Form submission", () => {
|
||||
// enter api key
|
||||
await userEvent.type(apiKey, "test-api-key");
|
||||
|
||||
// toggle confirmation mode
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).toBeChecked();
|
||||
// select security policy "always"
|
||||
await userEvent.click(securityPolicy);
|
||||
const alwaysOption = screen.getByText("SETTINGS$SECURITY_POLICY_ALWAYS");
|
||||
await userEvent.click(alwaysOption);
|
||||
expect(securityPolicy).toHaveValue("SETTINGS$SECURITY_POLICY_ALWAYS");
|
||||
|
||||
// toggle memory condensor
|
||||
await userEvent.click(condensor);
|
||||
@@ -355,7 +366,7 @@ describe("Form submission", () => {
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_base_url: "https://api.openai.com/v1/chat/completions",
|
||||
agent: "CoActAgent",
|
||||
confirmation_mode: true,
|
||||
security_policy: "always",
|
||||
enable_default_condenser: false,
|
||||
security_analyzer: null,
|
||||
}),
|
||||
@@ -412,7 +423,7 @@ describe("Form submission", () => {
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_base_url: "https://api.openai.com/v1/chat/completions",
|
||||
llm_api_key_set: true,
|
||||
confirmation_mode: true,
|
||||
security_policy: "always",
|
||||
});
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
@@ -430,10 +441,8 @@ describe("Form submission", () => {
|
||||
"enable-memory-condenser-switch",
|
||||
);
|
||||
|
||||
// Confirmation mode switch is now in basic settings, always visible
|
||||
const confirmation = await screen.findByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
// Security policy dropdown in advanced settings
|
||||
const securityPolicy = await screen.findByTestId("security-policy-input");
|
||||
|
||||
// enter custom model
|
||||
await userEvent.type(model, "-mini");
|
||||
@@ -489,12 +498,18 @@ describe("Form submission", () => {
|
||||
expect(agent).toHaveValue("CodeActAgent");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// toggle confirmation mode
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).not.toBeChecked();
|
||||
// change security policy
|
||||
await userEvent.click(securityPolicy);
|
||||
const neverOption = screen.getByText("SETTINGS$SECURITY_POLICY_NEVER");
|
||||
await userEvent.click(neverOption);
|
||||
expect(securityPolicy).toHaveValue("SETTINGS$SECURITY_POLICY_NEVER");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).toBeChecked();
|
||||
|
||||
// reset security policy back to "always"
|
||||
await userEvent.click(securityPolicy);
|
||||
const alwaysOption = screen.getByText("SETTINGS$SECURITY_POLICY_ALWAYS");
|
||||
await userEvent.click(alwaysOption);
|
||||
expect(securityPolicy).toHaveValue("SETTINGS$SECURITY_POLICY_ALWAYS");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// toggle memory condensor
|
||||
@@ -591,7 +606,7 @@ describe("Form submission", () => {
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_base_url: "https://api.openai.com/v1/chat/completions",
|
||||
llm_api_key_set: true,
|
||||
confirmation_mode: true,
|
||||
security_policy: "always",
|
||||
});
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
renderLlmSettingsScreen();
|
||||
@@ -620,7 +635,7 @@ describe("Form submission", () => {
|
||||
expect.objectContaining({
|
||||
llm_model: "openhands/claude-sonnet-4-20250514",
|
||||
llm_base_url: "",
|
||||
confirmation_mode: false, // Confirmation mode is now an advanced setting, should be cleared when saving basic settings
|
||||
security_policy: null, // Security policy is now an advanced setting, should be cleared when saving basic settings
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
|
||||
language: settings.LANGUAGE || DEFAULT_SETTINGS.LANGUAGE,
|
||||
confirmation_mode: settings.CONFIRMATION_MODE,
|
||||
security_analyzer: settings.SECURITY_ANALYZER,
|
||||
security_policy: settings.SECURITY_POLICY,
|
||||
llm_api_key:
|
||||
settings.llm_api_key === ""
|
||||
? ""
|
||||
|
||||
@@ -17,6 +17,7 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
|
||||
LANGUAGE: apiSettings.language,
|
||||
CONFIRMATION_MODE: apiSettings.confirmation_mode,
|
||||
SECURITY_ANALYZER: apiSettings.security_analyzer,
|
||||
SECURITY_POLICY: apiSettings.security_policy,
|
||||
LLM_API_KEY_SET: apiSettings.llm_api_key_set,
|
||||
SEARCH_API_KEY_SET: apiSettings.search_api_key_set,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
|
||||
|
||||
@@ -392,6 +392,11 @@ export enum I18nKey {
|
||||
SETTINGS$CONFIRMATION_MODE_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_TOOLTIP",
|
||||
SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP",
|
||||
SETTINGS$AGENT_SELECT_ENABLED = "SETTINGS$AGENT_SELECT_ENABLED",
|
||||
SETTINGS$SECURITY_POLICY = "SETTINGS$SECURITY_POLICY",
|
||||
SETTINGS$SECURITY_POLICY_NEVER = "SETTINGS$SECURITY_POLICY_NEVER",
|
||||
SETTINGS$SECURITY_POLICY_ALWAYS = "SETTINGS$SECURITY_POLICY_ALWAYS",
|
||||
SETTINGS$SECURITY_POLICY_RISKY = "SETTINGS$SECURITY_POLICY_RISKY",
|
||||
SETTINGS$SECURITY_POLICY_TOOLTIP = "SETTINGS$SECURITY_POLICY_TOOLTIP",
|
||||
SETTINGS$SECURITY_ANALYZER = "SETTINGS$SECURITY_ANALYZER",
|
||||
SETTINGS$SECURITY_ANALYZER_PLACEHOLDER = "SETTINGS$SECURITY_ANALYZER_PLACEHOLDER",
|
||||
SETTINGS$SECURITY_ANALYZER_TOOLTIP = "SETTINGS$SECURITY_ANALYZER_TOOLTIP",
|
||||
|
||||
@@ -6271,6 +6271,86 @@
|
||||
"ja": "エージェント選択を有効化",
|
||||
"uk": "Увімкнути вибір агента – Досвідчені користувачі"
|
||||
},
|
||||
"SETTINGS$SECURITY_POLICY": {
|
||||
"en": "Security Policy",
|
||||
"zh-CN": "安全策略",
|
||||
"zh-TW": "安全策略",
|
||||
"de": "Sicherheitsrichtlinie",
|
||||
"ko-KR": "보안 정책",
|
||||
"no": "Sikkerhetspolicy",
|
||||
"it": "Politica di sicurezza",
|
||||
"pt": "Política de segurança",
|
||||
"es": "Política de seguridad",
|
||||
"ar": "سياسة الأمان",
|
||||
"fr": "Politique de sécurité",
|
||||
"tr": "Güvenlik Politikası",
|
||||
"ja": "セキュリティポリシー",
|
||||
"uk": "Політика безпеки"
|
||||
},
|
||||
"SETTINGS$SECURITY_POLICY_NEVER": {
|
||||
"en": "Never confirm",
|
||||
"zh-CN": "从不确认",
|
||||
"zh-TW": "從不確認",
|
||||
"de": "Nie bestätigen",
|
||||
"ko-KR": "확인 안 함",
|
||||
"no": "Aldri bekreft",
|
||||
"it": "Non confermare mai",
|
||||
"pt": "Nunca confirmar",
|
||||
"es": "Nunca confirmar",
|
||||
"ar": "عدم التأكيد مطلقًا",
|
||||
"fr": "Ne jamais confirmer",
|
||||
"tr": "Asla onaylama",
|
||||
"ja": "確認しない",
|
||||
"uk": "Ніколи не підтверджувати"
|
||||
},
|
||||
"SETTINGS$SECURITY_POLICY_ALWAYS": {
|
||||
"en": "Always confirm",
|
||||
"zh-CN": "总是确认",
|
||||
"zh-TW": "總是確認",
|
||||
"de": "Immer bestätigen",
|
||||
"ko-KR": "항상 확인",
|
||||
"no": "Alltid bekreft",
|
||||
"it": "Conferma sempre",
|
||||
"pt": "Sempre confirmar",
|
||||
"es": "Siempre confirmar",
|
||||
"ar": "التأكيد دائمًا",
|
||||
"fr": "Toujours confirmer",
|
||||
"tr": "Her zaman onayla",
|
||||
"ja": "常に確認する",
|
||||
"uk": "Завжди підтверджувати"
|
||||
},
|
||||
"SETTINGS$SECURITY_POLICY_RISKY": {
|
||||
"en": "Confirm risky actions",
|
||||
"zh-CN": "确认危险操作",
|
||||
"zh-TW": "確認危險操作",
|
||||
"de": "Riskante Aktionen bestätigen",
|
||||
"ko-KR": "위험한 작업 확인",
|
||||
"no": "Bekreft risikable handlinger",
|
||||
"it": "Conferma azioni rischiose",
|
||||
"pt": "Confirmar ações arriscadas",
|
||||
"es": "Confirmar acciones riesgosas",
|
||||
"ar": "تأكيد الإجراءات الخطرة",
|
||||
"fr": "Confirmer les actions risquées",
|
||||
"tr": "Riskli eylemleri onayla",
|
||||
"ja": "危険なアクションを確認",
|
||||
"uk": "Підтверджувати небезпечні дії"
|
||||
},
|
||||
"SETTINGS$SECURITY_POLICY_TOOLTIP": {
|
||||
"en": "Control when the agent should ask for user confirmation before executing actions",
|
||||
"zh-CN": "控制代理在执行操作前何时请求用户确认",
|
||||
"zh-TW": "控制智慧代理在執行操作前何時請求使用者確認",
|
||||
"de": "Steuern Sie, wann der Agent vor der Ausführung von Aktionen eine Bestätigung anfordern soll",
|
||||
"ko-KR": "에이전트가 작업을 실행하기 전에 사용자 확인을 요청해야 하는 시점을 제어합니다",
|
||||
"no": "Kontroller når agenten skal be om brukerbekreftelse før utføring av handlinger",
|
||||
"it": "Controlla quando l'agente deve chiedere conferma all'utente prima di eseguire azioni",
|
||||
"pt": "Controle quando o agente deve solicitar confirmação do usuário antes de executar ações",
|
||||
"es": "Controle cuándo el agente debe solicitar confirmación del usuario antes de ejecutar acciones",
|
||||
"ar": "التحكم في وقت طلب الوكيل لتأكيد المستخدم قبل تنفيذ الإجراءات",
|
||||
"fr": "Contrôler quand l'agent doit demander la confirmation de l'utilisateur avant d'exécuter des actions",
|
||||
"tr": "Ajanın eylemleri yürütmeden önce kullanıcı onayı isteyeceği zamanı kontrol edin",
|
||||
"ja": "エージェントがアクションを実行する前にユーザー確認を求めるタイミングを制御します",
|
||||
"uk": "Контролюйте, коли агент повинен запитувати підтвердження користувача перед виконанням дій"
|
||||
},
|
||||
"SETTINGS$SECURITY_ANALYZER": {
|
||||
"en": "Enable Security Analyzer",
|
||||
"de": "Sicherheitsanalysator aktivieren",
|
||||
|
||||
@@ -24,6 +24,7 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
|
||||
language: DEFAULT_SETTINGS.LANGUAGE,
|
||||
confirmation_mode: DEFAULT_SETTINGS.CONFIRMATION_MODE,
|
||||
security_analyzer: DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
security_policy: DEFAULT_SETTINGS.SECURITY_POLICY,
|
||||
remote_runtime_resource_factor:
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
provider_tokens_set: {},
|
||||
|
||||
@@ -87,7 +87,7 @@ function LlmSettingsScreen() {
|
||||
searchApiKey: false,
|
||||
baseUrl: false,
|
||||
agent: false,
|
||||
confirmationMode: false,
|
||||
securityPolicy: false,
|
||||
enableDefaultCondenser: false,
|
||||
securityAnalyzer: false,
|
||||
condenserMaxSize: false,
|
||||
@@ -98,9 +98,9 @@ function LlmSettingsScreen() {
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
// Track confirmation mode state to control security analyzer visibility
|
||||
const [confirmationModeEnabled, setConfirmationModeEnabled] = React.useState(
|
||||
settings?.CONFIRMATION_MODE ?? DEFAULT_SETTINGS.CONFIRMATION_MODE,
|
||||
// Track security policy state to control security analyzer visibility
|
||||
const [selectedSecurityPolicy, setSelectedSecurityPolicy] = React.useState(
|
||||
settings?.SECURITY_POLICY ?? DEFAULT_SETTINGS.SECURITY_POLICY ?? "never",
|
||||
);
|
||||
|
||||
// Track selected security analyzer for form submission
|
||||
@@ -144,10 +144,10 @@ function LlmSettingsScreen() {
|
||||
|
||||
// Update confirmation mode state when settings change
|
||||
React.useEffect(() => {
|
||||
if (settings?.CONFIRMATION_MODE !== undefined) {
|
||||
setConfirmationModeEnabled(settings.CONFIRMATION_MODE);
|
||||
if (settings?.SECURITY_POLICY !== undefined) {
|
||||
setSelectedSecurityPolicy(settings.SECURITY_POLICY ?? "never");
|
||||
}
|
||||
}, [settings?.CONFIRMATION_MODE]);
|
||||
}, [settings?.SECURITY_POLICY]);
|
||||
|
||||
// Update selected security analyzer state when settings change
|
||||
React.useEffect(() => {
|
||||
@@ -177,7 +177,7 @@ function LlmSettingsScreen() {
|
||||
searchApiKey: false,
|
||||
baseUrl: false,
|
||||
agent: false,
|
||||
confirmationMode: false,
|
||||
securityPolicy: false,
|
||||
enableDefaultCondenser: false,
|
||||
securityAnalyzer: false,
|
||||
condenserMaxSize: false,
|
||||
@@ -197,8 +197,6 @@ function LlmSettingsScreen() {
|
||||
const model = formData.get("llm-model-input")?.toString();
|
||||
const apiKey = formData.get("llm-api-key-input")?.toString();
|
||||
const searchApiKey = formData.get("search-api-key-input")?.toString();
|
||||
const confirmationMode =
|
||||
formData.get("enable-confirmation-mode-switch")?.toString() === "on";
|
||||
const securityAnalyzer = formData
|
||||
.get("security-analyzer-input")
|
||||
?.toString();
|
||||
@@ -210,7 +208,6 @@ function LlmSettingsScreen() {
|
||||
LLM_MODEL: fullLlmModel,
|
||||
llm_api_key: apiKey || null,
|
||||
SEARCH_API_KEY: searchApiKey || "",
|
||||
CONFIRMATION_MODE: confirmationMode,
|
||||
SECURITY_ANALYZER:
|
||||
securityAnalyzer === "none"
|
||||
? null
|
||||
@@ -219,6 +216,7 @@ function LlmSettingsScreen() {
|
||||
// reset advanced settings
|
||||
LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL,
|
||||
AGENT: DEFAULT_SETTINGS.AGENT,
|
||||
SECURITY_POLICY: null,
|
||||
ENABLE_DEFAULT_CONDENSER: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
},
|
||||
{
|
||||
@@ -234,8 +232,11 @@ function LlmSettingsScreen() {
|
||||
const apiKey = formData.get("llm-api-key-input")?.toString();
|
||||
const searchApiKey = formData.get("search-api-key-input")?.toString();
|
||||
const agent = formData.get("agent-input")?.toString();
|
||||
const confirmationMode =
|
||||
formData.get("enable-confirmation-mode-switch")?.toString() === "on";
|
||||
const securityPolicy = formData.get("security-policy-input")?.toString() as
|
||||
| "always"
|
||||
| "never"
|
||||
| "risky"
|
||||
| undefined;
|
||||
const enableDefaultCondenser =
|
||||
formData.get("enable-memory-condenser-switch")?.toString() === "on";
|
||||
const condenserMaxSizeStr = formData
|
||||
@@ -260,7 +261,7 @@ function LlmSettingsScreen() {
|
||||
llm_api_key: apiKey || null,
|
||||
SEARCH_API_KEY: searchApiKey || "",
|
||||
AGENT: agent,
|
||||
CONFIRMATION_MODE: confirmationMode,
|
||||
SECURITY_POLICY: securityPolicy || DEFAULT_SETTINGS.SECURITY_POLICY,
|
||||
ENABLE_DEFAULT_CONDENSER: enableDefaultCondenser,
|
||||
CONDENSER_MAX_SIZE:
|
||||
condenserMaxSize ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
|
||||
@@ -284,7 +285,7 @@ function LlmSettingsScreen() {
|
||||
searchApiKey: false,
|
||||
baseUrl: false,
|
||||
agent: false,
|
||||
confirmationMode: false,
|
||||
securityPolicy: false,
|
||||
enableDefaultCondenser: false,
|
||||
securityAnalyzer: false,
|
||||
condenserMaxSize: false,
|
||||
@@ -347,16 +348,21 @@ function LlmSettingsScreen() {
|
||||
}));
|
||||
};
|
||||
|
||||
const handleConfirmationModeIsDirty = (isToggled: boolean) => {
|
||||
const confirmationModeIsDirty = isToggled !== settings?.CONFIRMATION_MODE;
|
||||
const handleSecurityPolicyIsDirty = (
|
||||
policy: "always" | "never" | "risky",
|
||||
) => {
|
||||
const securityPolicyIsDirty = policy !== settings?.SECURITY_POLICY;
|
||||
setDirtyInputs((prev) => ({
|
||||
...prev,
|
||||
confirmationMode: confirmationModeIsDirty,
|
||||
securityPolicy: securityPolicyIsDirty,
|
||||
}));
|
||||
setConfirmationModeEnabled(isToggled);
|
||||
setSelectedSecurityPolicy(policy);
|
||||
|
||||
// When confirmation mode is enabled, set default security analyzer to "llm" if not already set
|
||||
if (isToggled && !selectedSecurityAnalyzer) {
|
||||
// When security policy is "always" or "risky", set default security analyzer to "llm" if not already set
|
||||
if (
|
||||
(policy === "always" || policy === "risky") &&
|
||||
!selectedSecurityAnalyzer
|
||||
) {
|
||||
setSelectedSecurityAnalyzer(DEFAULT_SETTINGS.SECURITY_ANALYZER);
|
||||
setDirtyInputs((prev) => ({
|
||||
...prev,
|
||||
@@ -397,6 +403,21 @@ function LlmSettingsScreen() {
|
||||
|
||||
const formIsDirty = Object.values(dirtyInputs).some((isDirty) => isDirty);
|
||||
|
||||
const getSecurityPolicyOptions = () => [
|
||||
{
|
||||
key: "never",
|
||||
label: t(I18nKey.SETTINGS$SECURITY_POLICY_NEVER),
|
||||
},
|
||||
{
|
||||
key: "always",
|
||||
label: t(I18nKey.SETTINGS$SECURITY_POLICY_ALWAYS),
|
||||
},
|
||||
{
|
||||
key: "risky",
|
||||
label: t(I18nKey.SETTINGS$SECURITY_POLICY_RISKY),
|
||||
},
|
||||
];
|
||||
|
||||
const getSecurityAnalyzerOptions = () => {
|
||||
const analyzers = resources?.securityAnalyzers || [];
|
||||
const orderedItems = [];
|
||||
@@ -659,28 +680,50 @@ function LlmSettingsScreen() {
|
||||
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
|
||||
</SettingsSwitch>
|
||||
|
||||
{/* Confirmation mode and security analyzer */}
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
name="enable-confirmation-mode-switch"
|
||||
onToggle={handleConfirmationModeIsDirty}
|
||||
defaultIsToggled={settings.CONFIRMATION_MODE}
|
||||
isBeta
|
||||
{/* Security policy and security analyzer */}
|
||||
<div className="w-full max-w-[680px]">
|
||||
<div className="flex items-center gap-2 mb-2.5">
|
||||
<span className="text-sm">
|
||||
{t(I18nKey.SETTINGS$SECURITY_POLICY)}
|
||||
</span>
|
||||
<TooltipButton
|
||||
tooltip={t(I18nKey.SETTINGS$SECURITY_POLICY_TOOLTIP)}
|
||||
ariaLabel={t(I18nKey.SETTINGS$SECURITY_POLICY)}
|
||||
className="text-[#9099AC] hover:text-white cursor-help"
|
||||
>
|
||||
<QuestionCircleIcon width={16} height={16} />
|
||||
</TooltipButton>
|
||||
</div>
|
||||
<SettingsDropdownInput
|
||||
testId="security-policy-input"
|
||||
name="security-policy-display"
|
||||
items={getSecurityPolicyOptions()}
|
||||
selectedKey={selectedSecurityPolicy}
|
||||
isClearable={false}
|
||||
isDisabled={shouldShowUpgradeBanner}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</SettingsSwitch>
|
||||
<TooltipButton
|
||||
tooltip={t(I18nKey.SETTINGS$CONFIRMATION_MODE_TOOLTIP)}
|
||||
ariaLabel={t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
className="text-[#9099AC] hover:text-white cursor-help"
|
||||
>
|
||||
<QuestionCircleIcon width={16} height={16} />
|
||||
</TooltipButton>
|
||||
onSelectionChange={(key) => {
|
||||
const newValue = key?.toString() as
|
||||
| "always"
|
||||
| "never"
|
||||
| "risky";
|
||||
if (newValue) {
|
||||
handleSecurityPolicyIsDirty(newValue);
|
||||
}
|
||||
}}
|
||||
wrapperClassName="w-full"
|
||||
/>
|
||||
{/* Hidden input to store the actual key value for form submission */}
|
||||
<input
|
||||
type="hidden"
|
||||
name="security-policy-input"
|
||||
value={selectedSecurityPolicy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{confirmationModeEnabled && (
|
||||
{(selectedSecurityPolicy === "always" ||
|
||||
selectedSecurityPolicy === "risky" ||
|
||||
settings?.SECURITY_POLICY === "always" ||
|
||||
settings?.SECURITY_POLICY === "risky") && (
|
||||
<>
|
||||
<div className="w-full max-w-[680px]">
|
||||
<SettingsDropdownInput
|
||||
|
||||
@@ -11,6 +11,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
SEARCH_API_KEY_SET: false,
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "llm",
|
||||
SECURITY_POLICY: null,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
|
||||
PROVIDER_TOKENS_SET: {},
|
||||
ENABLE_DEFAULT_CONDENSER: true,
|
||||
|
||||
@@ -10,6 +10,7 @@ export type ApiSettings = {
|
||||
search_api_key_set: boolean;
|
||||
confirmation_mode: boolean;
|
||||
security_analyzer: string | null;
|
||||
security_policy: "always" | "never" | "risky" | null;
|
||||
remote_runtime_resource_factor: number | null;
|
||||
enable_default_condenser: boolean;
|
||||
// Max size for condenser in backend settings
|
||||
|
||||
@@ -45,6 +45,7 @@ export type Settings = {
|
||||
SEARCH_API_KEY_SET: boolean;
|
||||
CONFIRMATION_MODE: boolean;
|
||||
SECURITY_ANALYZER: string | null;
|
||||
SECURITY_POLICY: "always" | "never" | "risky" | null;
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
|
||||
PROVIDER_TOKENS_SET: Partial<Record<Provider, string | null>>;
|
||||
ENABLE_DEFAULT_CONDENSER: boolean;
|
||||
|
||||
@@ -57,7 +57,10 @@ from openhands.integrations.provider import ProviderType
|
||||
from openhands.sdk import LocalWorkspace
|
||||
from openhands.sdk.conversation.secret_source import LookupSecret, StaticSecret
|
||||
from openhands.sdk.llm import LLM
|
||||
from openhands.sdk.security.confirmation_policy import AlwaysConfirm
|
||||
from openhands.sdk.security.confirmation_policy import (
|
||||
AlwaysConfirm,
|
||||
ConfirmRisky,
|
||||
)
|
||||
from openhands.tools.preset.default import get_default_agent
|
||||
|
||||
_conversation_info_type_adapter = TypeAdapter(list[ConversationInfo | None])
|
||||
@@ -420,6 +423,33 @@ class LiveStatusAppConversationService(GitAppConversationService):
|
||||
)
|
||||
return agent_server_url
|
||||
|
||||
def _get_confirmation_policy(self, user):
|
||||
"""Determine the confirmation policy based on user settings.
|
||||
|
||||
Priority:
|
||||
1. Use security_policy if set ('always', 'never', 'risky')
|
||||
2. Fall back to confirmation_mode for backward compatibility
|
||||
|
||||
Returns:
|
||||
A confirmation policy instance (AlwaysConfirm, NeverConfirm, or ConfirmRisky)
|
||||
"""
|
||||
# New security_policy field takes priority
|
||||
if user.security_policy:
|
||||
if user.security_policy == 'always':
|
||||
return AlwaysConfirm()
|
||||
elif user.security_policy == 'never':
|
||||
return NeverConfirm()
|
||||
elif user.security_policy == 'risky':
|
||||
# Default to HIGH threshold with confirm_unknown=True
|
||||
# These could be additional fields in the future
|
||||
return ConfirmRisky()
|
||||
|
||||
# Fall back to legacy confirmation_mode for backward compatibility
|
||||
if user.confirmation_mode:
|
||||
return AlwaysConfirm()
|
||||
|
||||
return NeverConfirm()
|
||||
|
||||
async def _build_start_conversation_request_for_user(
|
||||
self,
|
||||
initial_message: SendMessageRequest | None,
|
||||
@@ -473,9 +503,7 @@ class LiveStatusAppConversationService(GitAppConversationService):
|
||||
conversation_id=conversation_id,
|
||||
agent=agent,
|
||||
workspace=workspace,
|
||||
confirmation_policy=(
|
||||
AlwaysConfirm() if user.confirmation_mode else NeverConfirm()
|
||||
),
|
||||
confirmation_policy=self._get_confirmation_policy(user),
|
||||
initial_message=initial_message,
|
||||
secrets=secrets,
|
||||
)
|
||||
|
||||
@@ -7,10 +7,12 @@ class SecurityConfig(BaseModel):
|
||||
Attributes:
|
||||
confirmation_mode: Whether to enable confirmation mode.
|
||||
security_analyzer: The security analyzer to use.
|
||||
security_policy: The security policy to use ('always', 'never', or 'risky').
|
||||
"""
|
||||
|
||||
confirmation_mode: bool = Field(default=False)
|
||||
security_analyzer: str | None = Field(default=None)
|
||||
security_policy: str | None = Field(default=None)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class Settings(BaseModel):
|
||||
max_iterations: int | None = None
|
||||
security_analyzer: str | None = None
|
||||
confirmation_mode: bool | None = None
|
||||
security_policy: str | None = None
|
||||
llm_model: str | None = None
|
||||
llm_api_key: SecretStr | None = None
|
||||
llm_base_url: str | None = None
|
||||
@@ -144,6 +145,7 @@ class Settings(BaseModel):
|
||||
max_iterations=app_config.max_iterations,
|
||||
security_analyzer=security.security_analyzer,
|
||||
confirmation_mode=security.confirmation_mode,
|
||||
security_policy=security.security_policy,
|
||||
llm_model=llm_config.model,
|
||||
llm_api_key=llm_config.api_key,
|
||||
llm_base_url=llm_config.base_url,
|
||||
|
||||
Reference in New Issue
Block a user