Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions apps/desktop/src/components/settings/experimental-page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"use client";

import { ConfirmDialog } from "@llm-space/ui/components/confirm-dialog";
import {
getFullAccessAcknowledged,
getFullAccessMode,
setFullAccessAcknowledged,
setFullAccessMode,
} from "@llm-space/ui/components/thread-playground/stores/run-mode";
import { Separator } from "@llm-space/ui/ui/separator";
import { useState } from "react";

Expand All @@ -18,6 +24,33 @@ export function ExperimentalPage() {
useExperimental();
const { executeCommand } = useCommands();
const [reloadPromptOpen, setReloadPromptOpen] = useState(false);
const [fullAccessMode, setFullAccessModeState] = useState(() =>
getFullAccessMode()
);
const [fullAccessDialogOpen, setFullAccessDialogOpen] = useState(false);

const handleFullAccessChange = (next: boolean) => {
if (!next) {
setFullAccessMode(false);
setFullAccessModeState(false);
return;
}
// First-ever enable asks for the risk acknowledgement; afterwards the
// switch flips directly (and reinstalling/clearing storage re-asks).
if (getFullAccessAcknowledged()) {
setFullAccessMode(true);
setFullAccessModeState(true);
return;
}
setFullAccessDialogOpen(true);
};

const confirmFullAccess = () => {
setFullAccessAcknowledged(true);
setFullAccessMode(true);
setFullAccessModeState(true);
setFullAccessDialogOpen(false);
};

const handleReactScanChange = (next: boolean) => {
setReactScanEnabled(next);
Expand All @@ -33,6 +66,13 @@ export function ExperimentalPage() {
className="overflow-y-auto"
>
<div className="flex flex-col gap-6 pb-2">
<SettingsToggleRow
title={t.experimental.fullAccess}
hint={t.experimental.fullAccessHint}
checked={fullAccessMode}
onCheckedChange={handleFullAccessChange}
/>
<Separator />
<SettingsToggleRow
title={t.experimental.tracing}
hint={t.experimental.tracingHint}
Expand All @@ -51,6 +91,21 @@ export function ExperimentalPage() {
</>
) : null}
</div>
<ConfirmDialog
open={fullAccessDialogOpen}
onOpenChange={(open) => {
setFullAccessDialogOpen(open);
// Dismissing without confirming leaves the switch off.
if (!open) setFullAccessModeState(false);
}}
dimBackground={false}
title={t.experimental.fullAccessDialogTitle}
description={t.experimental.fullAccessDialogDescription}
cancelLabel={t.experimental.fullAccessCancel}
confirmLabel={t.experimental.fullAccessConfirm}
confirmVariant="destructive"
onConfirm={confirmFullAccess}
/>
<ConfirmDialog
open={reloadPromptOpen}
onOpenChange={setReloadPromptOpen}
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ const APP_MESSAGES = {
title: "Experimental",
description:
"Configure preview features that are still under development.",
fullAccess: "Full access mode",
fullAccessHint:
"Tools — including bash commands flagged as destructive — run automatically without pausing for confirmation. Only enable this in an environment you can afford to lose (your dev machine, a VM, or a container). You can turn it off here at any time.",
fullAccessDialogTitle: "Enable full access mode?",
fullAccessDialogDescription:
"In this mode the agent executes every tool without confirmation, including irreversible actions such as deleting files, wiping disks, running sudo, or executing remote scripts. These actions are decided by the model and can be wrong. Any data loss or system damage is your own responsibility. It is recommended to enable this only in a disposable environment. You can turn it off in settings at any time.",
fullAccessConfirm: "I understand, enable",
fullAccessCancel: "Cancel",
tracing: "Tracing",
tracingHint:
"Enable to connect Langfuse or create a manual project for JSON exports.",
Expand Down Expand Up @@ -893,6 +901,14 @@ const APP_MESSAGES = {
experimental: {
title: "实验性",
description: "配置仍在开发中的预览功能。",
fullAccess: "完全访问模式",
fullAccessHint:
"所有工具——包括被判定为危险的 bash 命令——都会自动执行,不再暂停等待确认。请仅在可承受损失的环境(本机开发机、虚拟机或容器)中开启;可随时在这里关闭。",
fullAccessDialogTitle: "开启完全访问模式?",
fullAccessDialogDescription:
"开启后,Agent 将不经确认自动执行任何工具,包括删除文件、格式化磁盘、使用 sudo、执行远程脚本等不可逆操作。这些操作由模型决定,可能出错,由此造成的任何数据丢失或系统损坏由你自行承担。建议仅在可丢弃的环境中开启;可随时在设置中关闭。",
fullAccessConfirm: "我已了解,开启",
fullAccessCancel: "取消",
tracing: "Tracing",
tracingHint: "启用后可连接 Langfuse,或创建用于 JSON 导出的手动项目。",
reactScan: "React Scan",
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/i18n/playground-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const PLAYGROUND_LABELS: Record<AppLanguage, PlaygroundLabels> = {
runSettings: "Run settings",
enableReActLoop: "Enable ReAct loop",
autoRunTools: "Auto run tools",
fullAccessBadge: "Full access",
dialogs: {
add: "Add",
remove: "Remove",
Expand Down Expand Up @@ -453,6 +454,7 @@ export const PLAYGROUND_LABELS: Record<AppLanguage, PlaygroundLabels> = {
runSettings: "运行设置",
enableReActLoop: "启用 ReAct 循环",
autoRunTools: "自动运行工具",
fullAccessBadge: "完全访问",
dialogs: {
add: "添加",
remove: "移除",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"./components/thread-playground": "./src/components/thread-playground/index.tsx",
"./components/thread-playground/playground-labels": "./src/components/thread-playground/playground-labels.tsx",
"./components/thread-playground/examples/prompts": "./src/components/thread-playground/examples/prompts.ts",
"./components/thread-playground/stores/run-mode": "./src/components/thread-playground/stores/run-mode.ts",
"./components/code-editor": "./src/components/code-editor/index.tsx",
"./components/*": "./src/components/*.tsx",
"./styles/globals.css": "./src/styles/globals.css",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface PlaygroundLabels {
runSettings: string;
enableReActLoop: string;
autoRunTools: string;
/** The persistent warning badge shown while full access mode is enabled. */
fullAccessBadge: string;
dialogs: {
add: string;
remove: string;
Expand Down Expand Up @@ -418,6 +420,7 @@ export const DEFAULT_PLAYGROUND_LABELS: PlaygroundLabels = {
runSettings: "Run settings",
enableReActLoop: "Enable ReAct loop",
autoRunTools: "Auto run tools",
fullAccessBadge: "Full access",
dialogs: {
add: "Add",
remove: "Remove",
Expand Down
40 changes: 40 additions & 0 deletions packages/ui/src/components/thread-playground/stores/run-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,33 @@ export function getEffectiveAutoRunTools(): boolean {
return getReactLoop() || getAutoRunTools();
}

/**
* Whether full access mode is enabled. Opt-in and off by default: when on,
* tools auto-run without pausing for commands flagged as destructive (see
* `isDangerousBashCommand`). Requires a one-time risk acknowledgement before
* it can be switched on (see `getFullAccessAcknowledged`).
*/
export function getFullAccessMode(): boolean {
return _read(LOCAL_STORAGE_KEYS.fullAccessMode);
}

export function setFullAccessMode(value: boolean): void {
_write(LOCAL_STORAGE_KEYS.fullAccessMode, value);
}

/**
* Whether the user has confirmed the full-access-mode risk disclaimer. The
* switch only turns the mode on after this is set, so a reinstall or cleared
* storage asks for acknowledgement again.
*/
export function getFullAccessAcknowledged(): boolean {
return _read(LOCAL_STORAGE_KEYS.fullAccessAcknowledged);
}

export function setFullAccessAcknowledged(value: boolean): void {
_write(LOCAL_STORAGE_KEYS.fullAccessAcknowledged, value);
}

function _subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => {
Expand All @@ -74,8 +101,14 @@ export interface RunMode {
/** The effective flag: `true` whenever the ReAct loop is on. */
effectiveAutoRunTools: boolean;
reactLoop: boolean;
/**
* Full access mode: auto-run never pauses for commands flagged destructive.
* Off by default; enabling it in settings requires a one-time acknowledgement.
*/
fullAccessMode: boolean;
setAutoRunTools: (value: boolean) => void;
setReactLoop: (value: boolean) => void;
setFullAccessMode: (value: boolean) => void;
}

/**
Expand All @@ -89,11 +122,18 @@ export function useRunMode(): RunMode {
() => false
);
const reactLoop = useSyncExternalStore(_subscribe, getReactLoop, () => false);
const fullAccessMode = useSyncExternalStore(
_subscribe,
getFullAccessMode,
() => false
);
return {
autoRunTools,
effectiveAutoRunTools: reactLoop || autoRunTools,
reactLoop,
fullAccessMode,
setAutoRunTools,
setReactLoop,
setFullAccessMode,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ export function createThreadStore(
* Read fresh at run time. Defaults to `false`.
*/
getReactLoop?: () => boolean;
/**
* Whether full access mode is enabled: auto-run executes every tool call,
* including bash commands flagged destructive, without pausing. Opt-in via
* Experimental settings (with a one-time risk acknowledgement); off by
* default. Read fresh at run time. Defaults to `false`.
*/
getFullAccessMode?: () => boolean;
/**
* Execute an MCP or built-in tool call, returning structured model-facing
* content. Only used by the auto-run-tools path; manual tool runs go through
Expand Down Expand Up @@ -568,6 +575,7 @@ export function createThreadStore(
toolCall: ToolCall;
tool: McpTool | BuiltinTool | PluginTool;
}[] = [];
const fullAccessMode = options.getFullAccessMode?.() ?? false;
for (const toolCall of toolCalls) {
const tool = toolsByName.get(toolCall.input.name);
if (!tool || !isExecutableTool(tool)) {
Expand All @@ -576,8 +584,13 @@ export function createThreadStore(
// A destructive `bash` command must never be auto-executed, even under
// "auto run tools" or the ReAct loop — treat it like a `terminate`
// tool: stop the loop and leave it pending for the user to review and
// run by hand.
if (tool.type === "builtin" && tool.name === "bash") {
// run by hand. Full access mode is the deliberate opt-out: the user
// acknowledged the risk and accepted that every tool runs unattended.
if (
tool.type === "builtin" &&
tool.name === "bash" &&
!fullAccessMode
) {
const command = (toolCall.input.arguments as { command?: unknown })
?.command;
if (
Expand Down Expand Up @@ -1172,6 +1185,13 @@ export function createThreadStore(
streamingMessage: null,
executingToolCallIds: [],
});
// Persistent reminder (dismissible) that this run may execute
// anything without pausing — see the full access mode acknowledgement.
if (options.getFullAccessMode?.()) {
toast.warning(
"Full access mode is on — tools, including commands flagged destructive, run without confirmation."
);
}

// Commit the truncation while running so it folds into the run's
// single undo step instead of becoming its own snapshot.
Expand Down
23 changes: 21 additions & 2 deletions packages/ui/src/components/thread-playground/thread-playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
canUndo,
createThreadStore,
getAutoRunTools,
getFullAccessMode,
getReactLoop,
ThreadStoreContext,
useRunMode,
Expand Down Expand Up @@ -214,6 +215,7 @@ function _ThreadPlaygroundStore({
),
getAutoRunTools,
getReactLoop,
getFullAccessMode,
getProfileId,
runtimeId: ownerRuntimeId,
executeTool: toolExecutor ?? undefined,
Expand Down Expand Up @@ -284,8 +286,13 @@ function ThreadPlaygroundContent({
() => planCompaction(messages, 0, { hasMetaUserPrompt }).turnCount >= 2,
[hasMetaUserPrompt, messages]
);
const { effectiveAutoRunTools, reactLoop, setAutoRunTools, setReactLoop } =
useRunMode();
const {
effectiveAutoRunTools,
reactLoop,
fullAccessMode,
setAutoRunTools,
setReactLoop,
} = useRunMode();
const { run, abort, undo, redo, syncTitle } = useThreadStoreActions();
const [systemPromptStreaming, setSystemPromptStreaming] = useState(false);
const title = useMemo(
Expand Down Expand Up @@ -464,6 +471,18 @@ function ThreadPlaygroundContent({
/>
</div>
<div className="flex items-center gap-1 px-3">
{/* Persistent warning badge while full access mode is on, so the
user never forgets tools run without confirmation. */}
{fullAccessMode && !readonlyFromProps ? (
<Tooltip content={labels.fullAccessBadge}>
<span
className="inline-flex items-center rounded-full border border-amber-400/40 bg-amber-400/10 px-2.5 py-0.5 text-xs font-medium text-amber-500 dark:text-amber-400"
role="status"
>
{labels.fullAccessBadge}
</span>
</Tooltip>
) : null}
<ButtonGroup
className={cn(
"transition-transform active:translate-y-px",
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/lib/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const LOCAL_STORAGE_KEYS = {
renderingFidelity: "llm-space-rendering-fidelity",
autoRunTools: "llm-space-auto-run-tools",
reactLoop: "llm-space-react-loop",
fullAccessMode: "llm-space-full-access",
fullAccessAcknowledged: "llm-space-full-access-acknowledged",
messageStatsSummaryMode: "llm-space-message-stats-summary-mode",
language: "llm-space-language",
landingLanguage: "llm-space-lang",
Expand Down
Loading