mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
feat(frontend): Add trust confirmation checkbox to plugin launch modal
- Add checkbox at bottom of launch dialog with text: 'I trust this skill from [org/repo] with the agent secrets defined in my account.' - Disable launch button unless checkbox is checked (unchecked by default) - Display unique source repos in checkbox label (e.g., owner/repo1, owner/repo2) - Add LAUNCH$TRUST_SKILL_CHECKBOX translation key in 14 languages - Update tests to verify checkbox behavior Addresses security concern raised in PR review. Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -319,8 +319,72 @@ describe("LaunchRoute", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Trust Checkbox", () => {
|
||||
it("should display trust checkbox unchecked by default", () => {
|
||||
const plugins = [{ source: "github:owner/repo" }];
|
||||
const encoded = btoa(JSON.stringify(plugins));
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
const checkbox = screen.getByTestId("trust-checkbox");
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should display trust checkbox with associated label", () => {
|
||||
const plugins = [{ source: "github:owner/repo" }];
|
||||
const encoded = btoa(JSON.stringify(plugins));
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
// Check that the checkbox has a label associated with it
|
||||
const checkbox = screen.getByTestId("trust-checkbox");
|
||||
const label = document.querySelector('label[for="trust-checkbox"]');
|
||||
expect(label).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have trust checkbox label that references the translation key", () => {
|
||||
const plugins = [
|
||||
{ source: "github:owner/repo1" },
|
||||
{ source: "github:owner/repo2" },
|
||||
];
|
||||
const encoded = btoa(JSON.stringify(plugins));
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
// In test environment, the translation key is shown
|
||||
const label = document.querySelector('label[for="trust-checkbox"]');
|
||||
expect(label).toBeInTheDocument();
|
||||
expect(label?.textContent).toContain("LAUNCH$TRUST_SKILL_CHECKBOX");
|
||||
});
|
||||
|
||||
it("should disable start button when trust checkbox is unchecked", () => {
|
||||
const plugins = [{ source: "github:owner/repo" }];
|
||||
const encoded = btoa(JSON.stringify(plugins));
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
const button = screen.getByTestId("start-conversation-button");
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should enable start button when trust checkbox is checked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const plugins = [{ source: "github:owner/repo" }];
|
||||
const encoded = btoa(JSON.stringify(plugins));
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
const checkbox = screen.getByTestId("trust-checkbox");
|
||||
await user.click(checkbox);
|
||||
|
||||
const button = screen.getByTestId("start-conversation-button");
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Conversation Creation", () => {
|
||||
it("should call createConversation with plugins when start button clicked", async () => {
|
||||
it("should call createConversation with plugins when start button clicked after checking trust", async () => {
|
||||
const user = userEvent.setup();
|
||||
const plugins = [
|
||||
{ source: "github:owner/repo", parameters: { apiKey: "test" } },
|
||||
@@ -329,6 +393,8 @@ describe("LaunchRoute", () => {
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
// First check the trust checkbox
|
||||
await user.click(screen.getByTestId("trust-checkbox"));
|
||||
await user.click(screen.getByTestId("start-conversation-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -354,6 +420,8 @@ describe("LaunchRoute", () => {
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}&message=${encodeURIComponent(message)}`);
|
||||
|
||||
// First check the trust checkbox
|
||||
await user.click(screen.getByTestId("trust-checkbox"));
|
||||
await user.click(screen.getByTestId("start-conversation-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -380,12 +448,31 @@ describe("LaunchRoute", () => {
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
// First check the trust checkbox
|
||||
await user.click(screen.getByTestId("trust-checkbox"));
|
||||
await user.click(screen.getByTestId("start-conversation-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith("/conversations/new-conv-456");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call createConversation when trust checkbox is not checked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const plugins = [{ source: "github:owner/repo" }];
|
||||
const encoded = btoa(JSON.stringify(plugins));
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
// Try to click without checking trust (button should be disabled)
|
||||
const button = screen.getByTestId("start-conversation-button");
|
||||
expect(button).toBeDisabled();
|
||||
|
||||
// Even attempting to click shouldn't call the mutation
|
||||
await user.click(button);
|
||||
|
||||
expect(mockMutateAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
@@ -414,6 +501,8 @@ describe("LaunchRoute", () => {
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
// First check the trust checkbox
|
||||
await user.click(screen.getByTestId("trust-checkbox"));
|
||||
await user.click(screen.getByTestId("start-conversation-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -431,6 +520,8 @@ describe("LaunchRoute", () => {
|
||||
|
||||
renderLaunchRoute(`?plugins=${encoded}`);
|
||||
|
||||
// First check the trust checkbox
|
||||
await user.click(screen.getByTestId("trust-checkbox"));
|
||||
await user.click(screen.getByTestId("start-conversation-button"));
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -42,6 +42,7 @@ export function PluginLaunchModal({
|
||||
return initial;
|
||||
},
|
||||
);
|
||||
const [trustConfirmed, setTrustConfirmed] = React.useState(false);
|
||||
|
||||
const pluginsWithParams = pluginConfigs.filter(
|
||||
(p) => p.parameters && Object.keys(p.parameters).length > 0,
|
||||
@@ -106,6 +107,11 @@ export function PluginLaunchModal({
|
||||
return source;
|
||||
};
|
||||
|
||||
const getUniqueSources = (): string[] => {
|
||||
const sources = pluginConfigs.map((plugin) => getPluginSourceInfo(plugin));
|
||||
return [...new Set(sources)];
|
||||
};
|
||||
|
||||
const handleStartConversation = () => {
|
||||
onStartConversation(pluginConfigs, message);
|
||||
};
|
||||
@@ -305,19 +311,36 @@ export function PluginLaunchModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-end gap-2 pt-4 border-t border-tertiary">
|
||||
<BrandButton
|
||||
testId="start-conversation-button"
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleStartConversation}
|
||||
isDisabled={isLoading}
|
||||
className="px-4"
|
||||
>
|
||||
{isLoading
|
||||
? t(I18nKey.LAUNCH$STARTING)
|
||||
: t(I18nKey.LAUNCH$START_CONVERSATION)}
|
||||
</BrandButton>
|
||||
<div className="pt-4 border-t border-tertiary">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<input
|
||||
id="trust-checkbox"
|
||||
data-testid="trust-checkbox"
|
||||
type="checkbox"
|
||||
checked={trustConfirmed}
|
||||
onChange={(e) => setTrustConfirmed(e.target.checked)}
|
||||
className="mt-1 h-4 w-4 rounded border-tertiary bg-base-secondary accent-primary flex-shrink-0"
|
||||
/>
|
||||
<label htmlFor="trust-checkbox" className="text-sm text-white">
|
||||
{t(I18nKey.LAUNCH$TRUST_SKILL_CHECKBOX, {
|
||||
sources: getUniqueSources().join(", "),
|
||||
})}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex w-full justify-end gap-2">
|
||||
<BrandButton
|
||||
testId="start-conversation-button"
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleStartConversation}
|
||||
isDisabled={isLoading || !trustConfirmed}
|
||||
className="px-4"
|
||||
>
|
||||
{isLoading
|
||||
? t(I18nKey.LAUNCH$STARTING)
|
||||
: t(I18nKey.LAUNCH$START_CONVERSATION)}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBackdrop>
|
||||
|
||||
@@ -1037,6 +1037,7 @@ export enum I18nKey {
|
||||
LAUNCH$TRY_AGAIN = "LAUNCH$TRY_AGAIN",
|
||||
LAUNCH$PLUGIN_REF = "LAUNCH$PLUGIN_REF",
|
||||
LAUNCH$PLUGIN_PATH = "LAUNCH$PLUGIN_PATH",
|
||||
LAUNCH$TRUST_SKILL_CHECKBOX = "LAUNCH$TRUST_SKILL_CHECKBOX",
|
||||
ONBOARDING$STEP1_TITLE = "ONBOARDING$STEP1_TITLE",
|
||||
ONBOARDING$STEP1_SUBTITLE = "ONBOARDING$STEP1_SUBTITLE",
|
||||
ONBOARDING$SOFTWARE_ENGINEER = "ONBOARDING$SOFTWARE_ENGINEER",
|
||||
|
||||
@@ -16595,6 +16595,22 @@
|
||||
"de": "path:",
|
||||
"uk": "path:"
|
||||
},
|
||||
"LAUNCH$TRUST_SKILL_CHECKBOX": {
|
||||
"en": "I trust this skill from {{sources}} with the agent secrets defined in my account.",
|
||||
"ja": "私は{{sources}}からのこのスキルを、アカウントに定義されたエージェントシークレットとともに信頼します。",
|
||||
"zh-CN": "我信任来自 {{sources}} 的此技能,并允许其使用我账户中定义的代理密钥。",
|
||||
"zh-TW": "我信任來自 {{sources}} 的此技能,並允許其使用我帳戶中定義的代理密鑰。",
|
||||
"ko-KR": "{{sources}}의 이 스킬을 내 계정에 정의된 에이전트 비밀과 함께 신뢰합니다.",
|
||||
"no": "Jeg stoler på denne ferdigheten fra {{sources}} med agenthemmelighetene definert i kontoen min.",
|
||||
"it": "Mi fido di questa skill da {{sources}} con i segreti dell'agente definiti nel mio account.",
|
||||
"pt": "Eu confio nesta habilidade de {{sources}} com os segredos do agente definidos em minha conta.",
|
||||
"es": "Confío en esta habilidad de {{sources}} con los secretos del agente definidos en mi cuenta.",
|
||||
"ar": "أثق في هذه المهارة من {{sources}} مع أسرار الوكيل المحددة في حسابي.",
|
||||
"fr": "Je fais confiance à cette compétence de {{sources}} avec les secrets d'agent définis dans mon compte.",
|
||||
"tr": "{{sources}} kaynağından gelen bu beceriye hesabımda tanımlı ajan sırlarıyla güveniyorum.",
|
||||
"de": "Ich vertraue dieser Fähigkeit von {{sources}} mit den in meinem Konto definierten Agenten-Geheimnissen.",
|
||||
"uk": "Я довіряю цій навичці з {{sources}} із секретами агента, визначеними в моєму обліковому записі."
|
||||
},
|
||||
"ONBOARDING$STEP1_TITLE": {
|
||||
"en": "What's your role?",
|
||||
"ja": "あなたの役割は?",
|
||||
|
||||
Reference in New Issue
Block a user