Compare commits

...

1 Commits

13 changed files with 260 additions and 79 deletions
+50 -35
View File
@@ -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 === ""
? ""
+1
View File
@@ -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,
+5
View File
@@ -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",
+80
View File
@@ -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",
+1
View File
@@ -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: {},
+83 -40
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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,
)
+2
View File
@@ -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,