feat(frontend): Implement LLM risk analyzer UI (#10569)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: llamantino <213239228+llamantino@users.noreply.github.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ryan H. Tran <descience.thh10@gmail.com>
Co-authored-by: Neeraj Panwar <49247372+npneeraj@users.noreply.github.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Insop <1240382+insop@users.noreply.github.com>
Co-authored-by: test <test@test.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Zhonghao Jiang <zhonghao.J@outlook.com>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
This commit is contained in:
Hiep Le
2025-08-23 02:08:45 +07:00
committed by GitHub
parent df86fd275d
commit f5cd7b256d
21 changed files with 677 additions and 331 deletions

View File

@@ -79,6 +79,35 @@ describe("Content", () => {
expect(screen.getByTestId("set-indicator")).toBeInTheDocument();
});
});
it("should conditionally show security analyzer based on confirmation mode", async () => {
renderLlmSettingsScreen();
await screen.findByTestId("llm-settings-screen");
const confirmation = screen.getByTestId("enable-confirmation-mode-switch");
// Initially confirmation mode is false, so security analyzer should not be visible
expect(confirmation).not.toBeChecked();
expect(
screen.queryByTestId("security-analyzer-input"),
).not.toBeInTheDocument();
// Enable confirmation mode
await userEvent.click(confirmation);
expect(confirmation).toBeChecked();
// Security analyzer should now be visible
screen.getByTestId("security-analyzer-input");
// Disable confirmation mode again
await userEvent.click(confirmation);
expect(confirmation).not.toBeChecked();
// Security analyzer should be hidden again
expect(
screen.queryByTestId("security-analyzer-input"),
).not.toBeInTheDocument();
});
});
describe("Advanced form", () => {
@@ -107,7 +136,6 @@ describe("Content", () => {
within(advancedForm).getByTestId("llm-api-key-input");
within(advancedForm).getByTestId("llm-api-key-help-anchor-advanced");
within(advancedForm).getByTestId("agent-input");
within(advancedForm).getByTestId("enable-confirmation-mode-switch");
within(advancedForm).getByTestId("enable-memory-condenser-switch");
await userEvent.click(advancedSwitch);
@@ -130,9 +158,6 @@ 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 condensor = screen.getByTestId("enable-memory-condenser-switch");
expect(model).toHaveValue("openhands/claude-sonnet-4-20250514");
@@ -140,15 +165,7 @@ describe("Content", () => {
expect(apiKey).toHaveValue("");
expect(apiKey).toHaveProperty("placeholder", "");
expect(agent).toHaveValue("CodeActAgent");
expect(confirmation).not.toBeChecked();
expect(condensor).toBeChecked();
// check that security analyzer is present
expect(
screen.queryByTestId("security-analyzer-input"),
).not.toBeInTheDocument();
await userEvent.click(confirmation);
screen.getByTestId("security-analyzer-input");
});
it("should render the advanced form if existings settings are advanced", async () => {
@@ -177,7 +194,7 @@ describe("Content", () => {
agent: "CoActAgent",
confirmation_mode: true,
enable_default_condenser: false,
security_analyzer: "mock-invariant",
security_analyzer: "none",
});
renderLlmSettingsScreen();
@@ -203,7 +220,7 @@ describe("Content", () => {
expect(agent).toHaveValue("CoActAgent");
expect(confirmation).toBeChecked();
expect(condensor).not.toBeChecked();
expect(securityAnalyzer).toHaveValue("mock-invariant");
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE");
});
});
});
@@ -293,7 +310,7 @@ describe("Form submission", () => {
// select security analyzer
const securityAnalyzer = screen.getByTestId("security-analyzer-input");
await userEvent.click(securityAnalyzer);
const securityAnalyzerOption = screen.getByText("mock-invariant");
const securityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE");
await userEvent.click(securityAnalyzerOption);
const submitButton = screen.getByTestId("submit-button");
@@ -306,7 +323,7 @@ describe("Form submission", () => {
agent: "CoActAgent",
confirmation_mode: true,
enable_default_condenser: false,
security_analyzer: "mock-invariant",
security_analyzer: null,
}),
);
});
@@ -375,9 +392,11 @@ describe("Form submission", () => {
const baseUrl = await screen.findByTestId("base-url-input");
const apiKey = await screen.findByTestId("llm-api-key-input");
const agent = await screen.findByTestId("agent-input");
const confirmation = await screen.findByTestId("enable-confirmation-mode-switch");
const condensor = await screen.findByTestId("enable-memory-condenser-switch");
// Confirmation mode switch is now in basic settings, always visible
const confirmation = await screen.findByTestId("enable-confirmation-mode-switch");
// enter custom model
await userEvent.type(model, "-mini");
expect(model).toHaveValue("openai/gpt-4o-mini");
@@ -451,14 +470,17 @@ describe("Form submission", () => {
// select security analyzer
const securityAnalyzer = await screen.findByTestId("security-analyzer-input");
await userEvent.click(securityAnalyzer);
const securityAnalyzerOption = screen.getByText("mock-invariant");
const securityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE");
await userEvent.click(securityAnalyzerOption);
expect(securityAnalyzer).toHaveValue("mock-invariant");
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE");
expect(submitButton).not.toBeDisabled();
await userEvent.clear(securityAnalyzer);
expect(securityAnalyzer).toHaveValue("");
// revert back to original value
await userEvent.click(securityAnalyzer);
const originalSecurityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT");
await userEvent.click(originalSecurityAnalyzerOption);
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT");
expect(submitButton).toBeDisabled();
});
@@ -552,7 +574,7 @@ describe("Form submission", () => {
expect.objectContaining({
llm_model: "openhands/claude-sonnet-4-20250514",
llm_base_url: "",
confirmation_mode: false,
confirmation_mode: true, // Confirmation mode is now a basic setting, should be preserved
}),
);
});

View File

@@ -107,9 +107,7 @@ describe("Content", () => {
expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument(),
);
const button = await screen.findByTestId("connect-git-button");
await userEvent.click(button);
screen.getByTestId("git-settings-screen");
expect(button).toHaveAttribute("href", "/settings/integrations");
});
it("should render an empty table when there are no existing secrets", async () => {

View File

@@ -29,23 +29,5 @@ describe("hasAdvancedSettingsSet", () => {
}),
).toBe(true);
});
test("CONFIRMATION_MODE is true", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
CONFIRMATION_MODE: true,
}),
).toBe(true);
});
test("SECURITY_ANALYZER is set", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
SECURITY_ANALYZER: "test",
}),
).toBe(true);
});
});
});

View File

@@ -7,11 +7,10 @@ import { ConversationCard } from "../conversation-panel/conversation-card";
import { Provider } from "#/types/settings";
interface ControlsProps {
setSecurityOpen: (isOpen: boolean) => void;
showSecurityLock: boolean;
}
export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
export function Controls({ showSecurityLock }: ControlsProps) {
const { data: conversation } = useActiveConversation();
const [contextMenuOpen, setContextMenuOpen] = React.useState(false);
@@ -21,9 +20,7 @@ export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
<AgentControlBar />
<AgentStatusBar />
{showSecurityLock && (
<SecurityLock onClick={() => setSecurityOpen(true)} />
)}
{showSecurityLock && <SecurityLock />}
</div>
<ConversationCard

View File

@@ -1,17 +1,28 @@
import { IoLockClosed } from "react-icons/io5";
import { Tooltip } from "@heroui/react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { I18nKey } from "#/i18n/declaration";
interface SecurityLockProps {
onClick: () => void;
}
export function SecurityLock() {
const { t } = useTranslation();
export function SecurityLock({ onClick }: SecurityLockProps) {
return (
<div
className="cursor-pointer hover:opacity-80 transition-all"
style={{ marginRight: "8px" }}
onClick={onClick}
<Tooltip
content={
<div className="max-w-xs p-2">
{t(I18nKey.SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP)}
</div>
}
placement="top"
>
<IoLockClosed size={20} />
</div>
<Link
to="/settings"
className="mr-2 cursor-pointer hover:opacity-80 transition-all"
aria-label={t(I18nKey.SETTINGS$TITLE)}
>
<IoLockClosed size={20} />
</Link>
</Tooltip>
);
}

View File

@@ -1,8 +1,7 @@
import { Tooltip } from "@heroui/react";
import { useTranslation } from "react-i18next";
import ConfirmIcon from "#/assets/confirm";
import RejectIcon from "#/assets/reject";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
interface ActionTooltipProps {
type: "confirm" | "reject";
@@ -12,25 +11,35 @@ interface ActionTooltipProps {
export function ActionTooltip({ type, onClick }: ActionTooltipProps) {
const { t } = useTranslation();
const content =
type === "confirm"
? t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED)
: t(I18nKey.CHAT_INTERFACE$USER_REJECTED);
const isConfirm = type === "confirm";
const ariaLabel = isConfirm
? t(I18nKey.ACTION$CONFIRM)
: t(I18nKey.ACTION$REJECT);
const content = isConfirm
? t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED)
: t(I18nKey.CHAT_INTERFACE$USER_REJECTED);
const buttonLabel = isConfirm
? `${t(I18nKey.CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE)} ⌘↩`
: `${t(I18nKey.BUTTON$CANCEL)} ⇧⌘⌫`;
return (
<Tooltip content={content} closeDelay={100}>
<button
data-testid={`action-${type}-button`}
type="button"
aria-label={
aria-label={ariaLabel}
className={cn(
"rounded px-2 h-6.5 text-sm font-medium leading-5 cursor-pointer hover:opacity-80",
type === "confirm"
? t(I18nKey.ACTION$CONFIRM)
: t(I18nKey.ACTION$REJECT)
}
className="bg-tertiary rounded-full p-1 hover:bg-base-secondary"
? "bg-tertiary text-white"
: "bg-white text-[#0D0F11]",
)}
onClick={onClick}
>
{type === "confirm" ? <ConfirmIcon /> : <RejectIcon />}
{buttonLabel}
</button>
</Tooltip>
);

View File

@@ -1,31 +1,120 @@
import { useDispatch, useSelector } from "react-redux";
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { AgentState } from "#/types/agent-state";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { useWsClient } from "#/context/ws-client-provider";
import { ActionTooltip } from "../action-tooltip";
import { isOpenHandsAction } from "#/types/core/guards";
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
import { RiskAlert } from "#/components/shared/risk-alert";
import WarningIcon from "#/icons/u-warning.svg?react";
import { RootState } from "#/store";
import { addSubmittedEventId } from "#/state/event-message-slice";
export function ConfirmationButtons() {
const { t } = useTranslation();
const { send } = useWsClient();
const submittedEventIds = useSelector(
(state: RootState) => state.eventMessage.submittedEventIds,
);
const handleStateChange = (state: AgentState) => {
const event = generateAgentStateChangeEvent(state);
send(event);
};
const dispatch = useDispatch();
const { t } = useTranslation();
const { send, parsedEvents } = useWsClient();
// Find the most recent action awaiting confirmation
const awaitingAction = parsedEvents
.slice()
.reverse()
.find((ev) => {
if (!isOpenHandsAction(ev) || ev.source !== "agent") return false;
const args = ev.args as Record<string, unknown>;
return args?.confirmation_state === "awaiting_confirmation";
});
const handleStateChange = useCallback(
(state: AgentState) => {
if (!awaitingAction) {
return;
}
dispatch(addSubmittedEventId(awaitingAction.id));
send(generateAgentStateChangeEvent(state));
},
[send],
);
// Handle keyboard shortcuts
useEffect(() => {
if (!awaitingAction) {
return undefined;
}
const handleCancelShortcut = (event: KeyboardEvent) => {
if (event.shiftKey && event.metaKey && event.key === "Backspace") {
event.preventDefault();
handleStateChange(AgentState.USER_REJECTED);
}
};
const handleContinueShortcut = (event: KeyboardEvent) => {
if (event.metaKey && event.key === "Enter") {
event.preventDefault();
handleStateChange(AgentState.USER_CONFIRMED);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
// Cancel: Shift+Cmd+Backspace (⇧⌘⌫)
handleCancelShortcut(event);
// Continue: Cmd+Enter (⌘↩)
handleContinueShortcut(event);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [awaitingAction, handleStateChange]);
if (!awaitingAction || submittedEventIds.includes(awaitingAction.id)) {
return null;
}
const { args } = awaitingAction as { args: Record<string, unknown> };
const risk = args?.security_risk;
const isHighRisk =
typeof risk === "string"
? risk.toLowerCase() === "high"
: Number(risk) === ActionSecurityRisk.HIGH;
return (
<div className="flex justify-between items-center pt-4">
<p>{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}</p>
<div className="flex items-center gap-3">
<ActionTooltip
type="confirm"
onClick={() => handleStateChange(AgentState.USER_CONFIRMED)}
/>
<ActionTooltip
type="reject"
onClick={() => handleStateChange(AgentState.USER_REJECTED)}
<div className="flex flex-col gap-2 pt-4">
{isHighRisk && (
<RiskAlert
content={t(I18nKey.CHAT_INTERFACE$HIGH_RISK_WARNING)}
icon={<WarningIcon width={16} height={16} color="#fff" />}
severity="high"
title={t(I18nKey.COMMON$HIGH_RISK)}
/>
)}
<div className="flex justify-between items-center">
<p className="text-sm font-normal text-white">
{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}
</p>
<div className="flex items-center gap-3">
<ActionTooltip
type="reject"
onClick={() => handleStateChange(AgentState.USER_REJECTED)}
/>
<ActionTooltip
type="confirm"
onClick={() => handleStateChange(AgentState.USER_CONFIRMED)}
/>
</div>
</div>
</div>
);

View File

@@ -93,14 +93,14 @@ function SecurityInvariant() {
(risk: ActionSecurityRisk) => {
switch (risk) {
case ActionSecurityRisk.LOW:
return t(I18nKey.SECURITY_ANALYZER$LOW_RISK);
return t(I18nKey.SECURITY$LOW_RISK);
case ActionSecurityRisk.MEDIUM:
return t(I18nKey.SECURITY_ANALYZER$MEDIUM_RISK);
return t(I18nKey.SECURITY$MEDIUM_RISK);
case ActionSecurityRisk.HIGH:
return t(I18nKey.SECURITY_ANALYZER$HIGH_RISK);
return t(I18nKey.SECURITY$HIGH_RISK);
case ActionSecurityRisk.UNKNOWN:
default:
return t(I18nKey.SECURITY_ANALYZER$UNKNOWN_RISK);
return t(I18nKey.SECURITY$UNKNOWN_RISK);
}
},
[t],

View File

@@ -0,0 +1,36 @@
import { ReactNode } from "react";
import { cn } from "#/utils/utils";
interface RiskAlertProps {
className?: string;
content: ReactNode;
icon?: ReactNode;
severity: "high" | "medium" | "low";
title: string;
}
export function RiskAlert({
className,
content,
icon,
severity,
title,
}: RiskAlertProps) {
// Currently, we are only supporting the high risk alert. If we use want to support other risk levels, we can add them here and use cva to create different variants of this component.
if (severity === "high") {
return (
<div
className={cn(
"flex items-center gap-3.5 bg-[#4A0709] border border-[#FF0006] text-red-400 rounded-xl px-3.5 h-13 text-sm text-white",
className,
)}
>
{icon && <span className="">{icon}</span>}
<span className="font-bold">{title}</span>
<span className="font-normal">{content}</span>
</div>
);
}
return null;
}

View File

@@ -357,6 +357,7 @@ export enum I18nKey {
CHAT_INTERFACE$INPUT_PLACEHOLDER = "CHAT_INTERFACE$INPUT_PLACEHOLDER",
CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE = "CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE",
CHAT_INTERFACE$USER_ASK_CONFIRMATION = "CHAT_INTERFACE$USER_ASK_CONFIRMATION",
CHAT_INTERFACE$HIGH_RISK_WARNING = "CHAT_INTERFACE$HIGH_RISK_WARNING",
CHAT_INTERFACE$USER_CONFIRMED = "CHAT_INTERFACE$USER_CONFIRMED",
CHAT_INTERFACE$USER_REJECTED = "CHAT_INTERFACE$USER_REJECTED",
CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT = "CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT",
@@ -371,10 +372,6 @@ export enum I18nKey {
CHAT_INTERFACE$MESSAGE_ARIA_LABEL = "CHAT_INTERFACE$MESSAGE_ARIA_LABEL",
CHAT_INTERFACE$CHAT_CONVERSATION = "CHAT_INTERFACE$CHAT_CONVERSATION",
CHAT_INTERFACE$UNKNOWN_SENDER = "CHAT_INTERFACE$UNKNOWN_SENDER",
SECURITY_ANALYZER$UNKNOWN_RISK = "SECURITY_ANALYZER$UNKNOWN_RISK",
SECURITY_ANALYZER$LOW_RISK = "SECURITY_ANALYZER$LOW_RISK",
SECURITY_ANALYZER$MEDIUM_RISK = "SECURITY_ANALYZER$MEDIUM_RISK",
SECURITY_ANALYZER$HIGH_RISK = "SECURITY_ANALYZER$HIGH_RISK",
SETTINGS$MODEL_TOOLTIP = "SETTINGS$MODEL_TOOLTIP",
SETTINGS$AGENT_TOOLTIP = "SETTINGS$AGENT_TOOLTIP",
SETTINGS$LANGUAGE_TOOLTIP = "SETTINGS$LANGUAGE_TOOLTIP",
@@ -385,9 +382,12 @@ export enum I18nKey {
SETTINGS$REFRESH_LLM_API_KEY = "SETTINGS$REFRESH_LLM_API_KEY",
SETTINGS$CONFIRMATION_MODE = "SETTINGS$CONFIRMATION_MODE",
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_ANALYZER = "SETTINGS$SECURITY_ANALYZER",
SETTINGS$SECURITY_ANALYZER_PLACEHOLDER = "SETTINGS$SECURITY_ANALYZER_PLACEHOLDER",
SETTINGS$SECURITY_ANALYZER_TOOLTIP = "SETTINGS$SECURITY_ANALYZER_TOOLTIP",
SETTINGS$SECURITY_ANALYZER_DESCRIPTION = "SETTINGS$SECURITY_ANALYZER_DESCRIPTION",
SETTINGS$DONT_KNOW_API_KEY = "SETTINGS$DONT_KNOW_API_KEY",
SETTINGS$CLICK_FOR_INSTRUCTIONS = "SETTINGS$CLICK_FOR_INSTRUCTIONS",
SETTINGS$SAVED = "SETTINGS$SAVED",
@@ -781,8 +781,6 @@ export enum I18nKey {
PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR = "PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR",
PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR = "PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR",
MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT = "MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT",
SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT = "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT",
SETTINGS$MCP_ERROR_URL_DUPLICATE = "SETTINGS$MCP_ERROR_URL_DUPLICATE",
SETTINGS$MCP_SERVER_TYPE_SSE = "SETTINGS$MCP_SERVER_TYPE_SSE",
SETTINGS$MCP_SERVER_TYPE_STDIO = "SETTINGS$MCP_SERVER_TYPE_STDIO",
SETTINGS$MCP_SERVER_TYPE_SHTTP = "SETTINGS$MCP_SERVER_TYPE_SHTTP",
@@ -794,6 +792,8 @@ export enum I18nKey {
SETTINGS$MCP_ERROR_NAME_DUPLICATE = "SETTINGS$MCP_ERROR_NAME_DUPLICATE",
SETTINGS$MCP_ERROR_COMMAND_REQUIRED = "SETTINGS$MCP_ERROR_COMMAND_REQUIRED",
SETTINGS$MCP_ERROR_COMMAND_NO_SPACES = "SETTINGS$MCP_ERROR_COMMAND_NO_SPACES",
SETTINGS$MCP_ERROR_URL_DUPLICATE = "SETTINGS$MCP_ERROR_URL_DUPLICATE",
SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT = "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT",
SETTINGS$MCP_SERVER_TYPE = "SETTINGS$MCP_SERVER_TYPE",
SETTINGS$MCP_API_KEY_PLACEHOLDER = "SETTINGS$MCP_API_KEY_PLACEHOLDER",
SETTINGS$MCP_COMMAND_ARGUMENTS = "SETTINGS$MCP_COMMAND_ARGUMENTS",
@@ -814,4 +814,8 @@ export enum I18nKey {
MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW = "MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW",
MICROAGENT_MANAGEMENT$PR_NOT_CREATED = "MICROAGENT_MANAGEMENT$PR_NOT_CREATED",
MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT = "MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT",
SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT = "SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT",
SETTINGS$SECURITY_ANALYZER_NONE = "SETTINGS$SECURITY_ANALYZER_NONE",
SETTINGS$SECURITY_ANALYZER_INVARIANT = "SETTINGS$SECURITY_ANALYZER_INVARIANT",
COMMON$HIGH_RISK = "COMMON$HIGH_RISK",
}

View File

@@ -432,68 +432,68 @@
"uk": "Повторний вхід до OpenHands..."
},
"SECURITY$LOW_RISK": {
"en": "Low Risk",
"ja": "リスク",
"zh-CN": "风险",
"zh-TW": "風險",
"ko-KR": "낮은 위험",
"no": "Lav risiko",
"it": "Rischio basso",
"pt": "Baixo risco",
"es": "Riesgo bajo",
"ar": "مخاطر منخفضة",
"fr": "Risque faible",
"tr": "Düşük risk",
"de": "Geringes Risiko",
"uk": "Низький ризик"
"en": "Risk: Low",
"ja": "リスク: 低",
"zh-CN": "风险: 低",
"zh-TW": "風險: 低",
"ko-KR": "위험: 낮음",
"no": "Risiko: Lav",
"it": "Rischio: Basso",
"pt": "Risco: Baixo",
"es": "Riesgo: Bajo",
"ar": "المخاطر: منخفضة",
"fr": "Risque : Faible",
"tr": "Risk: Düşük",
"de": "Risiko: Gering",
"uk": "Ризик: Низький"
},
"SECURITY$MEDIUM_RISK": {
"en": "Medium Risk",
"ja": "リスク",
"zh-CN": "中等风险",
"zh-TW": "中等風險",
"ko-KR": "중간 위험",
"no": "Middels risiko",
"it": "Rischio medio",
"pt": "Risco médio",
"es": "Riesgo medio",
"ar": "مخاطر متوسطة",
"fr": "Risque moyen",
"tr": "Orta risk",
"de": "Mittleres Risiko",
"uk": "Середній ризик"
"en": "Risk: Medium",
"ja": "リスク: 中",
"zh-CN": "风险: 中等",
"zh-TW": "風險: 中等",
"ko-KR": "위험: 중간",
"no": "Risiko: Middels",
"it": "Rischio: Medio",
"pt": "Risco: Médio",
"es": "Riesgo: Medio",
"ar": "المخاطر: متوسطة",
"fr": "Risque : Moyen",
"tr": "Risk: Orta",
"de": "Risiko: Mittel",
"uk": "Ризик: Середній"
},
"SECURITY$HIGH_RISK": {
"en": "High Risk",
"ja": "リスク",
"zh-CN": "风险",
"zh-TW": "風險",
"ko-KR": "높은 위험",
"no": "Høy risiko",
"it": "Rischio alto",
"pt": "Alto risco",
"es": "Riesgo alto",
"ar": "مخاطر عالية",
"fr": "Risque élevé",
"tr": "Yüksek risk",
"de": "Hohes Risiko",
"uk": "Високий ризик"
"en": "Risk: High",
"ja": "リスク: 高",
"zh-CN": "风险: 高",
"zh-TW": "風險: 高",
"ko-KR": "위험: 높음",
"no": "Risiko: Høy",
"it": "Rischio: Alto",
"pt": "Risco: Alto",
"es": "Riesgo: Alto",
"ar": "المخاطر: عالية",
"fr": "Risque : Élevé",
"tr": "Risk: Yüksek",
"de": "Risiko: Hoch",
"uk": "Ризик: Високий"
},
"SECURITY$UNKNOWN_RISK": {
"en": "Unknown Risk",
"ja": "不明なリスク",
"zh-CN": "未知风险",
"zh-TW": "未知風險",
"ko-KR": "알 수 없는 위험",
"no": "Ukjent risiko",
"it": "Rischio sconosciuto",
"pt": "Risco desconhecido",
"es": "Riesgo desconocido",
"ar": "مخاطر غير معروفة",
"fr": "Risque inconnu",
"tr": "Bilinmeyen risk",
"de": "Unbekanntes Risiko",
"uk": "Невідомий ризик"
"en": "Risk: Unknown",
"ja": "リスク: 不明",
"zh-CN": "风险: 未知",
"zh-TW": "風險: 未知",
"ko-KR": "위험: 알 수 없",
"no": "Risiko: Ukjent",
"it": "Rischio: Sconosciuto",
"pt": "Risco: Desconhecido",
"es": "Riesgo: Desconocido",
"ar": "المخاطر: غير معروفة",
"fr": "Risque : Inconnu",
"tr": "Risk: Bilinmeyen",
"de": "Risiko: Unbekannt",
"uk": "Ризик: Невідомий"
},
"FINISH$TASK_COMPLETED_SUCCESSFULLY": {
"en": "I believe that the task was **completed successfully**.",
@@ -2432,20 +2432,20 @@
"uk": "Git налаштування"
},
"SETTINGS$GIT_SETTINGS_DESCRIPTION": {
"en": "Configure Git integration settings",
"ja": "Git統合設定を構成する",
"zh-CN": "配置Git集成设置",
"zh-TW": "配置Git整合設定",
"ko-KR": "Git 통합 설정 구성",
"de": "Git-Integrationseinstellungen konfigurieren",
"no": "Konfigurer Git-integrasjonsinnstillinger",
"it": "Configura le impostazioni di integrazione Git",
"pt": "Configure as configurações de integração Git",
"es": "Configure los ajustes de integración Git",
"ar": "تكوين إعدادات تكامل Git",
"fr": "Configurer les paramètres d'intégration Git",
"tr": "Git entegrasyon ayarlarını yapılandırın",
"uk": "Налаштуйте параметри інтеграції Git"
"en": "Configure the username and email that OpenHands uses to commit changes.",
"ja": "OpenHandsがコミットに使用するユーザー名とメールを設定します。",
"zh-CN": "配置OpenHands用于提交更改的用户名和电子邮件。",
"zh-TW": "配置OpenHands用於提交更改的用戶名和電子郵件。",
"ko-KR": "OpenHands가 변경 사항을 커밋할 때 사용하는 사용자 이름과 이메일을 구성합니다.",
"de": "Konfigurieren Sie den Benutzernamen und die E-Mail, die OpenHands zum Committen von Änderungen verwendet.",
"no": "Konfigurer brukernavnet og e-posten som OpenHands bruker for å committe endringer.",
"it": "Configura il nome utente e l'email che OpenHands utilizza per committare le modifiche.",
"pt": "Configure o nome de usuário e o email que o OpenHands usa para fazer commits de alterações.",
"es": "Configure el nombre de usuario y el correo electrónico que OpenHands utiliza para confirmar cambios.",
"ar": "قم بتكوين اسم المستخدم والبريد الإلكتروني الذي يستخدمه OpenHands لارتكاب التغييرات.",
"fr": "Configurez le nom d'utilisateur et l'email qu'OpenHands utilise pour valider les modifications.",
"tr": "OpenHands'ın değişiklikleri commit etmek için kullandığı kullanıcı adını ve e-postayı yapılandırın.",
"uk": "Налаштуйте ім'я користувача та електронну пошту, які OpenHands використовує для фіксації змін."
},
"SETTINGS$SOUND_NOTIFICATIONS": {
"en": "Sound Notifications",
@@ -2520,11 +2520,11 @@
"de": "Lösbarkeitsanalyse aktivieren",
"no": "Aktiver løsningsanalyse",
"it": "Abilita analisi di risolvibilità",
"pt": "Ativar análise de resolubilidade",
"es": "Habilitar análisis de resolubilidad",
"pt": "Ativar análise de solucionabilidade",
"es": "Habilitar análisis de solvencia",
"ar": "تمكين تحليل القابلية للحل",
"fr": "Activer l'analyse de solvabilité",
"tr": "Çözülebilirlik analizini etkinleştir",
"fr": "Activer l'analyse de solvabilité",
"tr": "Çözürlük Analizini Etkinleştir",
"uk": "Увімкнути аналіз розв'язності"
},
"SETTINGS$SEARCH_API_KEY": {
@@ -5711,6 +5711,22 @@
"ja": "このアクションを実行してもよろしいですか?",
"uk": "Ви хочете продовжити цю дію?"
},
"CHAT_INTERFACE$HIGH_RISK_WARNING": {
"en": "Review carefully before proceeding.",
"zh-CN": "在继续之前请仔细检查。",
"de": "Überprüfen Sie sorgfältig, bevor Sie fortfahren.",
"zh-TW": "在繼續之前請仔細檢查。",
"ko-KR": "계속하기 전에 신중히 검토하세요.",
"no": "Gå nøye gjennom før du fortsetter.",
"it": "Esamina attentamente prima di procedere.",
"pt": "Revise cuidadosamente antes de prosseguir.",
"es": "Revise cuidadosamente antes de continuar.",
"ar": "يرجى المراجعة بعناية قبل المتابعة.",
"fr": "Examinez attentivement avant de continuer.",
"tr": "Devam etmeden önce dikkatlice gözden geçirin.",
"ja": "続行する前に慎重に確認してください。",
"uk": "Уважно перевірте перед продовженням."
},
"CHAT_INTERFACE$USER_CONFIRMED": {
"en": "Confirm the requested action",
"de": "Bestätigen Sie die angeforderte Aktion",
@@ -5935,70 +5951,6 @@
"ja": "不明な送信者",
"uk": "Невідомий"
},
"SECURITY_ANALYZER$UNKNOWN_RISK": {
"en": "Unknown Risk",
"de": "Unbekanntes Risiko",
"zh-CN": "未知风险",
"ko-KR": "알 수 없는 위험",
"no": "Ukjent risiko",
"zh-TW": "未知風險",
"it": "Rischio sconosciuto",
"pt": "Risco desconhecido",
"es": "Riesgo desconocido",
"ar": "مخاطر غير معروفة",
"fr": "Risque inconnu",
"tr": "Bilinmeyen risk",
"ja": "不明なリスク",
"uk": "Невідомий ризик"
},
"SECURITY_ANALYZER$LOW_RISK": {
"en": "Low Risk",
"de": "Niedriges Risiko",
"zh-CN": "低风险",
"ko-KR": "낮은 위험",
"no": "Lav risiko",
"zh-TW": "低風險",
"it": "Rischio basso",
"pt": "Baixo risco",
"es": "Riesgo bajo",
"ar": "مخاطر منخفضة",
"fr": "Risque faible",
"tr": "Düşük risk",
"ja": "低リスク",
"uk": "Низький ризик"
},
"SECURITY_ANALYZER$MEDIUM_RISK": {
"en": "Medium Risk",
"de": "Mittleres Risiko",
"zh-CN": "中等风险",
"ko-KR": "중간 위험",
"no": "Middels risiko",
"zh-TW": "中等風險",
"it": "Rischio medio",
"pt": "Risco médio",
"es": "Riesgo medio",
"ar": "مخاطر متوسطة",
"fr": "Risque moyen",
"tr": "Orta risk",
"ja": "中リスク",
"uk": "Середній ризик"
},
"SECURITY_ANALYZER$HIGH_RISK": {
"en": "High Risk",
"de": "Hohes Risiko",
"zh-CN": "高风险",
"ko-KR": "높은 위험",
"no": "Høy risiko",
"zh-TW": "高風險",
"it": "Rischio elevato",
"pt": "Alto risco",
"es": "Riesgo alto",
"ar": "مخاطر عالية",
"fr": "Risque élevé",
"tr": "Yüksek risk",
"ja": "高リスク",
"uk": "Високий ризик"
},
"SETTINGS$MODEL_TOOLTIP": {
"en": "Select the language model to use.",
"zh-CN": "选择要使用的语言模型",
@@ -6159,6 +6111,22 @@
"ja": "エージェントのアクションを実行前に確認",
"uk": "Очікує підтвердження користувача перед виконанням коду."
},
"SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP": {
"en": "The agent is in confirmation mode. It will prompt the user to confirm certain actions when security analyzer policy detected a high-risk action. Click this icon to go to settings tab for more information.",
"de": "Der Agent befindet sich im Bestätigungsmodus. Er wird den Benutzer auffordern, bestimmte Aktionen zu bestätigen, wenn die Sicherheitsanalysator-Richtlinie eine risikoreiche Aktion erkannt hat. Weitere Informationen finden Sie auf der Registerkarte Einstellungen.",
"zh-CN": "代理处于确认模式。当安全分析器策略检测到高风险操作时,它会提示用户确认某些操作。查看设置选项卡了解更多信息。",
"zh-TW": "代理處於確認模式。當安全分析器策略檢測到高風險操作時,它會提示使用者確認某些操作。查看設定選項卡了解更多資訊。",
"ko-KR": "에이전트가 확인 모드에 있습니다. 보안 분석기 정책이 고위험 작업을 감지하면 사용자에게 특정 작업을 확인하도록 요청합니다. 자세한 내용은 설정 탭을 확인하세요.",
"no": "Agenten er i bekreftelsesmodus. Den vil be brukeren om å bekrefte visse handlinger når sikkerhetsanalysatorpolitikken oppdager en høyrisiko-handling. Sjekk innstillingsfanen for mer informasjon.",
"it": "L'agente è in modalità di conferma. Chiederà all'utente di confermare certe azioni quando la politica dell'analizzatore di sicurezza rileva un'azione ad alto rischio. Controlla la scheda impostazioni per maggiori informazioni.",
"pt": "O agente está no modo de confirmação. Ele solicitará ao usuário que confirme certas ações quando a política do analisador de segurança detectar uma ação de alto risco. Verifique a aba de configurações para mais informações.",
"es": "El agente está en modo de confirmación. Solicitará al usuario que confirme ciertas acciones cuando la política del analizador de seguridad detecte una acción de alto riesgo. Consulte la pestaña de configuración para obtener más información.",
"ar": "الوكيل في وضع التأكيد. سيطلب من المستخدم تأكيد إجراءات معينة عندما تكتشف سياسة محلل الأمان إجراءً عالي المخاطر. تحقق من علامة تبويب الإعدادات للحصول على مزيد من المعلومات.",
"fr": "L'agent est en mode de confirmation. Il demandera à l'utilisateur de confirmer certaines actions lorsque la politique de l'analyseur de sécurité détecte une action à haut risque. Consultez l'onglet paramètres pour plus d'informations.",
"tr": "Ajan onay modunda. Güvenlik analizörü politikası yüksek riskli bir eylem tespit ettiğinde kullanıcıdan belirli eylemleri onaylamasını isteyecek. Daha fazla bilgi için ayarlar sekmesini kontrol edin.",
"ja": "エージェントは確認モードです。セキュリティアナライザーポリシーが高リスクアクションを検出した場合、特定のアクションの確認をユーザーに求めます。詳細については設定タブを確認してください。",
"uk": "Агент знаходиться в режимі підтвердження. Він попросить користувача підтвердити певні дії, коли політика аналізатора безпеки виявить дію високого ризику. Перевірте вкладку налаштувань для отримання додаткової інформації."
},
"SETTINGS$AGENT_SELECT_ENABLED": {
"en": "Enable Agent Selection - Advanced Users",
"zh-CN": "启用智能体选择 - 高级用户",
@@ -6207,6 +6175,38 @@
"ja": "セキュリティアナライザーを選択…",
"uk": "Виберіть аналізатор безпеки…"
},
"SETTINGS$SECURITY_ANALYZER_TOOLTIP": {
"en": "When enabled, the agent will pause and ask for confirmation when it tries to execute high-risk actions",
"de": "Wenn aktiviert, pausiert der Agent und fragt nach Bestätigung, wenn er versucht, risikoreiche Aktionen auszuführen",
"zh-CN": "启用后,代理在尝试执行高风险操作时会暂停并要求确认",
"zh-TW": "啟用後,代理在嘗試執行高風險操作時會暫停並要求確認",
"ko-KR": "활성화되면 에이전트가 고위험 작업을 실행하려고 할 때 일시 중지하고 확인을 요청합니다",
"no": "Når aktivert, vil agenten pause og be om bekreftelse når den prøver å utføre høyrisiko-handlinger",
"it": "Quando abilitato, l'agente si fermerà e chiederà conferma quando tenta di eseguire azioni ad alto rischio",
"pt": "Quando ativado, o agente pausará e pedirá confirmação quando tentar executar ações de alto risco",
"es": "Cuando está habilitado, el agente se pausará y pedirá confirmación cuando trate de ejecutar acciones de alto riesgo",
"ar": "عند التمكين، سيتوقف الوكيل ويطلب التأكيد عندما يحاول تنفيذ إجراءات عالية المخاطر",
"fr": "Lorsqu'il est activé, l'agent se mettra en pause et demandera confirmation lorsqu'il tentera d'exécuter des actions à haut risque",
"tr": "Etkinleştirildiğinde, ajan yüksek riskli eylemleri gerçekleştirmeye çalıştığında duraklar ve onay ister",
"ja": "有効にすると、エージェントは高リスクなアクションを実行しようとする際に一時停止し、確認を求めます",
"uk": "Коли увімкнено, агент зупиниться і попросить підтвердження, коли спробує виконати дії високого ризику"
},
"SETTINGS$SECURITY_ANALYZER_DESCRIPTION": {
"en": "The security analyzer will be used in conjunction with confirmation mode. By default, it utilizes LLM-predicted action risk to determine whether to prompt the user for confirmation. If the risk is HIGH, it will prompt the user for confirmation by default.",
"de": "Der Sicherheitsanalysator wird in Verbindung mit dem Bestätigungsmodus verwendet. Standardmäßig nutzt er LLM-vorhergesagtes Aktionsrisiko, um zu bestimmen, ob der Benutzer zur Bestätigung aufgefordert werden soll. Wenn das Risiko HOCH ist, wird er standardmäßig zur Bestätigung auffordern.",
"zh-CN": "安全分析器将与确认模式结合使用。默认情况下它利用LLM预测的操作风险来确定是否提示用户确认。如果风险为高它将默认提示用户确认。",
"zh-TW": "安全分析器將與確認模式結合使用。預設情況下它利用LLM預測的操作風險來確定是否提示用戶確認。如果風險為高它將預設提示用戶確認。",
"ko-KR": "보안 분석기는 확인 모드와 함께 사용됩니다. 기본적으로 LLM이 예측한 작업 위험을 활용하여 사용자에게 확인을 요청할지 결정합니다. 위험이 높으면 기본적으로 사용자에게 확인을 요청합니다.",
"no": "Sikkerhetsanalysatoren vil bli brukt i forbindelse med bekreftelsesmodus. Som standard bruker den LLM-forutsagt handlingsrisiko for å bestemme om brukeren skal bli bedt om bekreftelse. Hvis risikoen er HØY, vil den be om bekreftelse som standard.",
"it": "L'analizzatore di sicurezza verrà utilizzato insieme alla modalità di conferma. Per impostazione predefinita, utilizza il rischio di azione previsto dall'LLM per determinare se richiedere conferma all'utente. Se il rischio è ALTO, richiederà conferma per impostazione predefinita.",
"pt": "O analisador de segurança será usado em conjunto com o modo de confirmação. Por padrão, utiliza o risco de ação previsto pelo LLM para determinar se deve solicitar confirmação ao usuário. Se o risco for ALTO, solicitará confirmação por padrão.",
"es": "El analizador de seguridad se utilizará junto con el modo de confirmación. Por defecto, utiliza el riesgo de acción predicho por LLM para determinar si solicitar confirmación al usuario. Si el riesgo es ALTO, solicitará confirmación por defecto.",
"ar": "سيتم استخدام محلل الأمان بالتزامن مع وضع التأكيد. افتراضياً، يستخدم مخاطر الإجراء المتوقعة من LLM لتحديد ما إذا كان يجب مطالبة المستخدم بالتأكيد. إذا كان الخطر عالياً، فسيطالب بالتأكيد افتراضياً.",
"fr": "L'analyseur de sécurité sera utilisé en conjonction avec le mode de confirmation. Par défaut, il utilise le risque d'action prédit par LLM pour déterminer s'il faut demander confirmation à l'utilisateur. Si le risque est ÉLEVÉ, il demandera confirmation par défaut.",
"tr": "Güvenlik analizörü onay modu ile birlikte kullanılacaktır. Varsayılan olarak, kullanıcıdan onay istenip istenmeyeceğini belirlemek için LLM tarafından tahmin edilen eylem riskini kullanır. Risk YÜKSEK ise, varsayılan olarak kullanıcıdan onay isteyecektir.",
"ja": "セキュリティアナライザーは確認モードと組み合わせて使用されます。デフォルトでは、LLMが予測したアクションリスクを利用して、ユーザーに確認を求めるかどうかを決定します。リスクが高い場合、デフォルトでユーザーに確認を求めます。",
"uk": "Аналізатор безпеки буде використовуватися разом з режимом підтвердження. За замовчуванням він використовує передбачений LLM ризик дії для визначення, чи потрібно запитувати підтвердження у користувача. Якщо ризик ВИСОКИЙ, він запитуватиме підтвердження за замовчуванням."
},
"SETTINGS$DONT_KNOW_API_KEY": {
"en": "Don't know your API key?",
"ja": "APIキーがわかりませんか",
@@ -12495,38 +12495,6 @@
"de": "Fehler beim Laden des Microagent-Inhalts.",
"uk": "Помилка під час завантаження вмісту мікроагента."
},
"SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT": {
"en": "Environment variables must follow KEY=value format",
"ja": "Environment variables must follow KEY=value format",
"zh-CN": "Environment variables must follow KEY=value format",
"zh-TW": "Environment variables must follow KEY=value format",
"ko-KR": "Environment variables must follow KEY=value format",
"no": "Environment variables must follow KEY=value format",
"it": "Environment variables must follow KEY=value format",
"pt": "Environment variables must follow KEY=value format",
"es": "Environment variables must follow KEY=value format",
"ar": "Environment variables must follow KEY=value format",
"fr": "Environment variables must follow KEY=value format",
"tr": "Environment variables must follow KEY=value format",
"de": "Environment variables must follow KEY=value format",
"uk": "Environment variables must follow KEY=value format"
},
"SETTINGS$MCP_ERROR_URL_DUPLICATE": {
"en": "A server with this URL already exists for the selected type",
"ja": "A server with this URL already exists for the selected type",
"zh-CN": "A server with this URL already exists for the selected type",
"zh-TW": "A server with this URL already exists for the selected type",
"ko-KR": "A server with this URL already exists for the selected type",
"no": "A server with this URL already exists for the selected type",
"it": "A server with this URL already exists for the selected type",
"pt": "A server with this URL already exists for the selected type",
"es": "A server with this URL already exists for the selected type",
"ar": "A server with this URL already exists for the selected type",
"fr": "A server with this URL already exists for the selected type",
"tr": "A server with this URL already exists for the selected type",
"de": "A server with this URL already exists for the selected type",
"uk": "A server with this URL already exists for the selected type"
},
"SETTINGS$MCP_SERVER_TYPE_SSE": {
"en": "SSE",
"ja": "SSE",
@@ -12703,6 +12671,38 @@
"de": "Befehl darf keine Leerzeichen enthalten",
"uk": "Команда не може містити пробіли"
},
"SETTINGS$MCP_ERROR_URL_DUPLICATE": {
"en": "A server with this URL already exists for the selected type",
"ja": "A server with this URL already exists for the selected type",
"zh-CN": "A server with this URL already exists for the selected type",
"zh-TW": "A server with this URL already exists for the selected type",
"ko-KR": "A server with this URL already exists for the selected type",
"no": "A server with this URL already exists for the selected type",
"it": "A server with this URL already exists for the selected type",
"pt": "A server with this URL already exists for the selected type",
"es": "A server with this URL already exists for the selected type",
"ar": "A server with this URL already exists for the selected type",
"fr": "A server with this URL already exists for the selected type",
"tr": "A server with this URL already exists for the selected type",
"de": "A server with this URL already exists for the selected type",
"uk": "A server with this URL already exists for the selected type"
},
"SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT": {
"en": "Environment variables must follow KEY=value format",
"ja": "Environment variables must follow KEY=value format",
"zh-CN": "Environment variables must follow KEY=value format",
"zh-TW": "Environment variables must follow KEY=value format",
"ko-KR": "Environment variables must follow KEY=value format",
"no": "Environment variables must follow KEY=value format",
"it": "Environment variables must follow KEY=value format",
"pt": "Environment variables must follow KEY=value format",
"es": "Environment variables must follow KEY=value format",
"ar": "Environment variables must follow KEY=value format",
"fr": "Environment variables must follow KEY=value format",
"tr": "Environment variables must follow KEY=value format",
"de": "Environment variables must follow KEY=value format",
"uk": "Environment variables must follow KEY=value format"
},
"SETTINGS$MCP_SERVER_TYPE": {
"en": "Server Type",
"ja": "サーバータイプ",
@@ -13022,5 +13022,69 @@
"tr": "Bir şeyler ters gitti. Mikro ajanı tekrar başlatmayı deneyin.",
"de": "Etwas ist schiefgelaufen. Versuchen Sie, den Microagenten erneut zu starten.",
"uk": "Щось пішло не так. Спробуйте ініціювати мікроагента ще раз."
},
"SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT": {
"en": "LLM Analyzer (Default)",
"ja": "LLMアナライザーデフォルト",
"zh-CN": "LLM 分析器(默认)",
"zh-TW": "LLM 分析器(預設)",
"ko-KR": "LLM 분석기(기본)",
"no": "LLM-analysator (standard)",
"it": "Analizzatore LLM (Predefinito)",
"pt": "Analisador LLM (Padrão)",
"es": "Analizador LLM (Predeterminado)",
"ar": "محلل LLM (افتراضي)",
"fr": "Analyseur LLM (Par défaut)",
"tr": "LLM Analizörü (Varsayılan)",
"de": "LLM-Analysator (Standard)",
"uk": "Аналізатор LLM (За замовчуванням)"
},
"SETTINGS$SECURITY_ANALYZER_NONE": {
"en": "None (Ask for every command)",
"ja": "なし(すべてのコマンドで確認)",
"zh-CN": "无(每条命令都询问)",
"zh-TW": "無(每個指令都詢問)",
"ko-KR": "없음(모든 명령마다 확인)",
"no": "Ingen (Spør for hver kommando)",
"it": "Nessuno (Chiedi per ogni comando)",
"pt": "Nenhum (Perguntar para cada comando)",
"es": "Ninguno (Preguntar para cada comando)",
"ar": "لا شيء (اسأل عن كل أمر)",
"fr": "Aucun (Demander pour chaque commande)",
"tr": "Yok (Her komutta sor)",
"de": "Keine (Bei jedem Befehl nachfragen)",
"uk": "Немає (Запитувати для кожної команди)"
},
"SETTINGS$SECURITY_ANALYZER_INVARIANT": {
"en": "Invariant Rule-based Analyzer",
"ja": "不変ルールベース分析器",
"zh-CN": "Invariant 规则分析器",
"zh-TW": "Invariant 規則式分析器",
"ko-KR": "Invariant 규칙 기반 분석기",
"no": "Invariant regelbasert analysator",
"it": "Analizzatore basato su regole Invariant",
"pt": "Analisador baseado em regras Invariant",
"es": "Analizador basado en reglas Invariant",
"ar": "محلل قائم على القواعد Invariant",
"fr": "Analyseur à base de règles Invariant",
"tr": "Invariant Kural Tabanlı Analizör",
"de": "Invariant regelbasierter Analysator",
"uk": "Аналізатор на основі правил Invariant"
},
"COMMON$HIGH_RISK": {
"en": "High Risk",
"ja": "高リスク",
"zh-CN": "高风险",
"zh-TW": "高風險",
"ko-KR": "고위험",
"no": "Høy risiko",
"it": "Alto rischio",
"pt": "Alto risco",
"es": "Alto riesgo",
"ar": "مخاطر عالية",
"fr": "Risque élevé",
"tr": "Yüksek Risk",
"de": "Hohes Risiko",
"uk": "Високий ризик"
}
}

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 14C11.4477 14 11 13.5523 11 13V10C11 9.44772 11.4477 9 12 9C12.5523 9 13 9.44772 13 10V13C13 13.5523 12.5523 14 12 14Z" fill="currentColor"/>
<path d="M10.5 16.5C10.5 15.6716 11.1716 15 12 15C12.8284 15 13.5 15.6716 13.5 16.5C13.5 17.3284 12.8284 18 12 18C11.1716 18 10.5 17.3284 10.5 16.5Z" fill="currentColor"/>
<path d="M10.2301 3.2156C10.98 1.79093 13.02 1.79092 13.7698 3.2156L22.1135 19.0685C22.8144 20.4003 21.8486 22 20.3436 22H3.65635C2.15133 22 1.18556 20.4003 1.88651 19.0685L10.2301 3.2156ZM20.3436 20L12 4.1471L3.65635 20L20.3436 20Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 692 B

View File

@@ -123,7 +123,7 @@ const openHandsHandlers = [
),
http.get("/api/options/security-analyzers", async () =>
HttpResponse.json(["mock-invariant"]),
HttpResponse.json(["llm", "none"]),
),
http.post("http://localhost:3001/api/submit-feedback", async () => {

View File

@@ -1,4 +1,3 @@
import { useDisclosure } from "@heroui/react";
import React from "react";
import { useNavigate } from "react-router";
import { useDispatch } from "react-redux";
@@ -18,7 +17,7 @@ import {
Orientation,
ResizablePanel,
} from "#/components/layout/resizable-panel";
import Security from "#/components/shared/modals/security/security";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { useSettings } from "#/hooks/query/use-settings";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
@@ -83,12 +82,6 @@ function AppContent() {
};
}, []);
const {
isOpen: securityModalIsOpen,
onOpen: onSecurityModalOpen,
onOpenChange: onSecurityModalOpenChange,
} = useDisclosure();
function renderMain() {
if (width <= 1024) {
return (
@@ -106,7 +99,7 @@ function AppContent() {
<ResizablePanel
orientation={Orientation.HORIZONTAL}
className="grow h-full min-h-0 min-w-0"
initialSize={500}
initialSize={564}
firstClassName="rounded-xl overflow-hidden border border-neutral-600 bg-base-secondary"
secondClassName="flex flex-col overflow-hidden"
firstChild={<ChatInterface />}
@@ -122,17 +115,7 @@ function AppContent() {
<div data-testid="app-route" className="flex flex-col h-full gap-3">
<div className="flex h-full overflow-auto">{renderMain()}</div>
<Controls
setSecurityOpen={onSecurityModalOpen}
showSecurityLock={!!settings?.SECURITY_ANALYZER}
/>
{settings && (
<Security
isOpen={securityModalIsOpen}
onOpenChange={onSecurityModalOpenChange}
securityAnalyzer={settings.SECURITY_ANALYZER}
/>
)}
<Controls showSecurityLock={!!settings?.CONFIRMATION_MODE} />
</div>
</EventHandler>
</ConversationSubscriptionsProvider>

View File

@@ -8,6 +8,8 @@ import { useSettings } from "#/hooks/query/use-settings";
import { hasAdvancedSettingsSet } from "#/utils/has-advanced-settings-set";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { SettingsSwitch } from "#/components/features/settings/settings-switch";
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
import QuestionCircleIcon from "#/icons/question-circle.svg?react";
import { I18nKey } from "#/i18n/declaration";
import { SettingsInput } from "#/components/features/settings/settings-input";
import { HelpLink } from "#/components/features/settings/help-link";
@@ -36,8 +38,6 @@ function LlmSettingsScreen() {
const { data: config } = useConfig();
const [view, setView] = React.useState<"basic" | "advanced">("basic");
const [securityAnalyzerInputIsVisible, setSecurityAnalyzerInputIsVisible] =
React.useState(false);
const [dirtyInputs, setDirtyInputs] = React.useState({
model: false,
@@ -55,6 +55,19 @@ 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 selected security analyzer for form submission
const [selectedSecurityAnalyzer, setSelectedSecurityAnalyzer] =
React.useState(
settings?.SECURITY_ANALYZER === null
? "none"
: (settings?.SECURITY_ANALYZER ?? DEFAULT_SETTINGS.SECURITY_ANALYZER),
);
const modelsAndProviders = organizeModelsAndProviders(
resources?.models || [],
);
@@ -74,7 +87,6 @@ function LlmSettingsScreen() {
};
const userSettingsIsAdvanced = determineWhetherToToggleAdvancedSettings();
if (settings) setSecurityAnalyzerInputIsVisible(settings.CONFIRMATION_MODE);
if (userSettingsIsAdvanced) setView("advanced");
else setView("basic");
@@ -87,6 +99,20 @@ function LlmSettingsScreen() {
}
}, [settings?.LLM_MODEL]);
// Update confirmation mode state when settings change
React.useEffect(() => {
if (settings?.CONFIRMATION_MODE !== undefined) {
setConfirmationModeEnabled(settings.CONFIRMATION_MODE);
}
}, [settings?.CONFIRMATION_MODE]);
// Update selected security analyzer state when settings change
React.useEffect(() => {
if (settings?.SECURITY_ANALYZER !== undefined) {
setSelectedSecurityAnalyzer(settings.SECURITY_ANALYZER || "none");
}
}, [settings?.SECURITY_ANALYZER]);
const handleSuccessfulMutation = () => {
displaySuccessToast(t(I18nKey.SETTINGS$SAVED_WARNING));
setDirtyInputs({
@@ -114,6 +140,11 @@ 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();
const fullLlmModel = provider && model && `${provider}/${model}`;
@@ -122,12 +153,15 @@ function LlmSettingsScreen() {
LLM_MODEL: fullLlmModel,
llm_api_key: apiKey || null,
SEARCH_API_KEY: searchApiKey || "",
CONFIRMATION_MODE: confirmationMode,
SECURITY_ANALYZER:
securityAnalyzer === "none"
? null
: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER,
// reset advanced settings
LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL,
AGENT: DEFAULT_SETTINGS.AGENT,
CONFIRMATION_MODE: DEFAULT_SETTINGS.CONFIRMATION_MODE,
SECURITY_ANALYZER: DEFAULT_SETTINGS.SECURITY_ANALYZER,
ENABLE_DEFAULT_CONDENSER: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
},
{
@@ -160,7 +194,10 @@ function LlmSettingsScreen() {
AGENT: agent,
CONFIRMATION_MODE: confirmationMode,
ENABLE_DEFAULT_CONDENSER: enableDefaultCondenser,
SECURITY_ANALYZER: confirmationMode ? securityAnalyzer : undefined,
SECURITY_ANALYZER:
securityAnalyzer === "none"
? null
: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER,
},
{
onSuccess: handleSuccessfulMutation,
@@ -175,7 +212,6 @@ function LlmSettingsScreen() {
};
const handleToggleAdvancedSettings = (isToggled: boolean) => {
setSecurityAnalyzerInputIsVisible(!!settings?.CONFIRMATION_MODE);
setView(isToggled ? "advanced" : "basic");
setDirtyInputs({
model: false,
@@ -246,12 +282,21 @@ function LlmSettingsScreen() {
};
const handleConfirmationModeIsDirty = (isToggled: boolean) => {
setSecurityAnalyzerInputIsVisible(isToggled);
const confirmationModeIsDirty = isToggled !== settings?.CONFIRMATION_MODE;
setDirtyInputs((prev) => ({
...prev,
confirmationMode: confirmationModeIsDirty,
}));
setConfirmationModeEnabled(isToggled);
// When confirmation mode is enabled, set default security analyzer to "llm" if not already set
if (isToggled && !selectedSecurityAnalyzer) {
setSelectedSecurityAnalyzer(DEFAULT_SETTINGS.SECURITY_ANALYZER);
setDirtyInputs((prev) => ({
...prev,
securityAnalyzer: true,
}));
}
};
const handleEnableDefaultCondenserIsDirty = (isToggled: boolean) => {
@@ -274,6 +319,47 @@ function LlmSettingsScreen() {
const formIsDirty = Object.values(dirtyInputs).some((isDirty) => isDirty);
const getSecurityAnalyzerOptions = () => {
const analyzers = resources?.securityAnalyzers || [];
const orderedItems = [];
// Add LLM analyzer first
if (analyzers.includes("llm")) {
orderedItems.push({
key: "llm",
label: t(I18nKey.SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT),
});
}
// Add None option second
orderedItems.push({
key: "none",
label: t(I18nKey.SETTINGS$SECURITY_ANALYZER_NONE),
});
// Add Invariant analyzer third
if (analyzers.includes("invariant")) {
orderedItems.push({
key: "invariant",
label: t(I18nKey.SETTINGS$SECURITY_ANALYZER_INVARIANT),
});
}
// Add any other analyzers that might exist
analyzers.forEach((analyzer) => {
if (!["llm", "invariant", "none"].includes(analyzer)) {
// For unknown analyzers, use the analyzer name as fallback
// In the future, add specific i18n keys for new analyzers
orderedItems.push({
key: analyzer,
label: analyzer, // TODO: Add i18n support for new analyzers
});
}
});
return orderedItems;
};
if (!settings || isFetching) return <LlmSettingsInputsSkeleton />;
return (
@@ -452,7 +538,7 @@ function LlmSettingsScreen() {
items={
resources?.agents.map((agent) => ({
key: agent,
label: agent,
label: agent, // TODO: Add i18n support for agent names
})) || []
}
defaultSelectedKey={settings.AGENT}
@@ -487,39 +573,67 @@ function LlmSettingsScreen() {
>
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
</SettingsSwitch>
</div>
)}
<SettingsSwitch
testId="enable-confirmation-mode-switch"
name="enable-confirmation-mode-switch"
onToggle={handleConfirmationModeIsDirty}
defaultIsToggled={settings.CONFIRMATION_MODE}
isBeta
>
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
</SettingsSwitch>
{/* Confirmation mode and security analyzer - always visible */}
<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
>
{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>
</div>
{securityAnalyzerInputIsVisible && (
{confirmationModeEnabled && (
<>
<div className="w-full max-w-[680px]">
<SettingsDropdownInput
testId="security-analyzer-input"
name="security-analyzer-input"
name="security-analyzer-display"
label={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
items={
resources?.securityAnalyzers.map((analyzer) => ({
key: analyzer,
label: analyzer,
})) || []
}
items={getSecurityAnalyzerOptions()}
placeholder={t(
I18nKey.SETTINGS$SECURITY_ANALYZER_PLACEHOLDER,
)}
defaultSelectedKey={settings.SECURITY_ANALYZER}
isClearable
showOptionalTag
onInputChange={handleSecurityAnalyzerIsDirty}
wrapperClassName="w-full max-w-[680px]"
selectedKey={selectedSecurityAnalyzer || "none"}
isClearable={false}
onSelectionChange={(key) => {
const newValue = key?.toString() || "";
setSelectedSecurityAnalyzer(newValue);
handleSecurityAnalyzerIsDirty(newValue);
}}
onInputChange={(value) => {
// Handle when input is cleared
if (!value) {
setSelectedSecurityAnalyzer("");
handleSecurityAnalyzerIsDirty("");
}
}}
wrapperClassName="w-full"
/>
)}
</div>
{/* Hidden input to store the actual key value for form submission */}
<input
type="hidden"
name="security-analyzer-input"
value={selectedSecurityAnalyzer || ""}
/>
</div>
<p className="text-xs text-tertiary-alt max-w-[680px]">
{t(I18nKey.SETTINGS$SECURITY_ANALYZER_DESCRIPTION)}
</p>
</>
)}
</div>

View File

@@ -10,7 +10,7 @@ export const DEFAULT_SETTINGS: Settings = {
LLM_API_KEY_SET: false,
SEARCH_API_KEY_SET: false,
CONFIRMATION_MODE: false,
SECURITY_ANALYZER: "",
SECURITY_ANALYZER: "llm",
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
PROVIDER_TOKENS_SET: {},
ENABLE_DEFAULT_CONDENSER: true,

View File

@@ -0,0 +1,23 @@
import { createSlice } from "@reduxjs/toolkit";
export const eventMessageSlice = createSlice({
name: "eventMessage",
initialState: {
submittedEventIds: [] as number[], // Avoid the flashing issue of the confirmation buttons
},
reducers: {
addSubmittedEventId: (state, action) => {
state.submittedEventIds.push(action.payload);
},
removeSubmittedEventId: (state, action) => {
state.submittedEventIds = state.submittedEventIds.filter(
(id) => id !== action.payload,
);
},
},
});
export const { addSubmittedEventId, removeSubmittedEventId } =
eventMessageSlice.actions;
export default eventMessageSlice.reducer;

View File

@@ -10,6 +10,7 @@ import securityAnalyzerReducer from "./state/security-analyzer-slice";
import statusReducer from "./state/status-slice";
import metricsReducer from "./state/metrics-slice";
import microagentManagementReducer from "./state/microagent-management-slice";
import eventMessageReducer from "./state/event-message-slice";
export const rootReducer = combineReducers({
fileState: fileStateReducer,
@@ -23,6 +24,7 @@ export const rootReducer = combineReducers({
status: statusReducer,
metrics: metricsReducer,
microagentManagement: microagentManagementReducer,
eventMessage: eventMessageReducer,
});
const store = configureStore({

View File

@@ -43,7 +43,7 @@ export type Settings = {
LLM_API_KEY_SET: boolean;
SEARCH_API_KEY_SET: boolean;
CONFIRMATION_MODE: boolean;
SECURITY_ANALYZER: string;
SECURITY_ANALYZER: string | null;
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
PROVIDER_TOKENS_SET: Partial<Record<Provider, string | null>>;
ENABLE_DEFAULT_CONDENSER: boolean;
@@ -70,7 +70,7 @@ export type ApiSettings = {
llm_api_key_set: boolean;
search_api_key_set: boolean;
confirmation_mode: boolean;
security_analyzer: string;
security_analyzer: string | null;
remote_runtime_resource_factor: number | null;
enable_default_condenser: boolean;
enable_sound_notifications: boolean;

View File

@@ -3,7 +3,4 @@ import { Settings } from "#/types/settings";
export const hasAdvancedSettingsSet = (settings: Partial<Settings>): boolean =>
Object.keys(settings).length > 0 &&
(!!settings.LLM_BASE_URL ||
settings.AGENT !== DEFAULT_SETTINGS.AGENT ||
settings.CONFIRMATION_MODE ||
!!settings.SECURITY_ANALYZER);
(!!settings.LLM_BASE_URL || settings.AGENT !== DEFAULT_SETTINGS.AGENT);

View File

@@ -193,6 +193,8 @@ class AgentController:
# replay-related
self._replay_manager = ReplayManager(replay_events)
self.confirmation_mode = confirmation_mode
# security analyzer for direct access
self.security_analyzer = security_analyzer
@@ -231,13 +233,13 @@ class AgentController:
if hasattr(action, 'security_risk'):
action.security_risk = ActionSecurityRisk.UNKNOWN
else:
# When no security analyzer is configured, treat all actions as HIGH risk
# When no security analyzer is configured, treat all actions as UNKNOWN risk
# This is a fail-safe approach that ensures confirmation is required
logger.debug(
f'No security analyzer configured, setting HIGH risk for action: {action}'
f'No security analyzer configured, setting UNKNOWN risk for action: {action}'
)
if hasattr(action, 'security_risk'):
action.security_risk = ActionSecurityRisk.HIGH
action.security_risk = ActionSecurityRisk.UNKNOWN
def _add_system_message(self):
for event in self.event_stream.search_events(start_id=self.state.start_id):
@@ -928,6 +930,12 @@ class AgentController:
action, 'security_risk', ActionSecurityRisk.UNKNOWN
)
is_high_security_risk = security_risk == ActionSecurityRisk.HIGH
is_ask_for_every_action = (
security_risk == ActionSecurityRisk.UNKNOWN
and not self.security_analyzer
)
# If security_risk is HIGH, requires confirmation
# UNLESS it is CLI which will handle action risks it itself
if self.agent.config.cli_mode:
@@ -938,7 +946,9 @@ class AgentController:
ActionConfirmationStatus.AWAITING_CONFIRMATION
)
# Only HIGH security risk actions require confirmation
elif security_risk == ActionSecurityRisk.HIGH:
elif (
is_high_security_risk or is_ask_for_every_action
) and self.confirmation_mode:
logger.debug(
f'[non-CLI mode] Detected HIGH security risk in action: {action}. Ask for confirmation'
)