Skip to content
Merged
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
7 changes: 1 addition & 6 deletions crates/goose/src/agents/summarize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use super::Agent;
use crate::agents::capabilities::Capabilities;
use crate::agents::extension::{ExtensionConfig, ExtensionResult};
use crate::config::Config;
use crate::config::ExperimentManager;
use crate::memory_condense::condense_messages;
use crate::message::{Message, ToolRequest};
use crate::providers::base::Provider;
Expand Down Expand Up @@ -283,11 +282,7 @@ impl Agent for SummarizeAgent {
let mode = goose_mode.clone();
match mode.as_str() {
"approve" => {
let mut read_only_tools = Vec::new();
// Process each tool request sequentially with confirmation
if ExperimentManager::is_enabled("GOOSE_SMART_APPROVE")? {
read_only_tools = detect_read_only_tools(&capabilities, tool_requests.clone()).await;
}
let read_only_tools = detect_read_only_tools(&capabilities, tool_requests.clone()).await;
for request in &tool_requests {
if let Ok(tool_call) = request.tool_call.clone() {
// Skip confirmation if the tool_call.name is in the read_only_tools list
Expand Down
3 changes: 1 addition & 2 deletions crates/goose/src/agents/truncate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use crate::agents::capabilities::Capabilities;
use crate::agents::extension::{ExtensionConfig, ExtensionResult};
use crate::agents::ToolPermissionStore;
use crate::config::Config;
use crate::config::ExperimentManager;
use crate::message::{Message, ToolRequest};
use crate::providers::base::Provider;
use crate::providers::base::ProviderUsage;
Expand Down Expand Up @@ -299,7 +298,7 @@ impl Agent for TruncateAgent {
}

// Only check read-only status for tools needing confirmation
if !needs_confirmation.is_empty() && ExperimentManager::is_enabled("GOOSE_SMART_APPROVE")? {
if !needs_confirmation.is_empty() {
read_only_tools = detect_read_only_tools(&capabilities, needs_confirmation.clone()).await;
}

Expand Down
2 changes: 1 addition & 1 deletion crates/goose/src/config/experiments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::collections::HashMap;
/// It is the ground truth for init experiments. The experiment names in users' experiment list but not
/// in the list will be remove from user list; The experiment names in the ground-truth list but not
/// in users' experiment list will be added to user list with default value false;
const ALL_EXPERIMENTS: &[(&str, bool)] = &[("GOOSE_SMART_APPROVE", true)];
const ALL_EXPERIMENTS: &[(&str, bool)] = &[];

/// Experiment configuration management
pub struct ExperimentManager;
Expand Down
70 changes: 1 addition & 69 deletions documentation/docs/guides/goose-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,76 +96,8 @@ Here's how to configure:
4. Under `Mode Selection`, choose the mode you'd like

:::info
If you choose `Approve` mode, you will see "Allow" and "Deny" buttons in your session windows during tool calls with write operations.
If you choose `Approve` mode, you will see "Allow" and "Deny" buttons in your session windows during tool calls. Goose will only ask for permission before tool call for tools that it deems are 'write' tools, for example any 'text editor write', 'text editor edit', 'bash - rm, cp, mv' commands, as an example. Read write approval makes best effort attempt at classifying read or write tools- this is interpreted by your LLM provider.
:::

</TabItem>
</Tabs>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we keep a note here explaining 'Read Write approve' -

goose will only ask for permission before tool call for tools that it deems are 'write' tools, for example any 'text editor write', 'text editor edit', 'bash - rm, cp, mv' commands, as an example. Read write approval makes best effort attempt at classifying read or write tools- this is interpreted by your LLM provider. 



## Smart Approve

Goose introduces the **Smart Approve** feature when the Goose mode is set to `Approve`. With Smart Approve enabled, Goose evaluates the risk level of a tool call before execution.

- **If the tool call is deemed risky (e.g. tool requires Goose to write)**, Goose will prompt you for confirmation before proceeding.
- **If the tool call is considered safe**, Goose will execute it directly without any notification.

This feature is enabled by default. If you wish to disable Smart Approve, you can

1. Run the following command:

```sh
goose configure
```

2. Select `Goose Settings` from the menu and press Enter.

```sh
┌ goose-configure
◆ What would you like to configure?
| ○ Configure Providers
| ○ Add Extension
| ○ Toggle Extensions
| ○ Remove Extension
// highlight-start
| ● Goose Settings (Set the Goose Mode, Tool Output, Experiment and more)
// highlight-end
```

3. Choose `Toggle Experiment` from the menu and press Enter.

```sh
┌ goose-configure
◇ What would you like to configure?
│ Goose Settings
◆ What setting would you like to configure?
│ ○ Goose Mode
│ ○ Tool Output
// highlight-start
│ ● Toggle Experiment (Enable or disable an experiment feature)
// highlight-end
```

4. Toggle `GOOSE_SMART_APPROVE` and press Enter.

```sh
┌ goose-configure
┌ goose-configure
◇ What would you like to configure?
│ Goose Settings
◇ What setting would you like to configure?
│ Toggle Experiment
◆ enable experiments: (use "space" to toggle and "enter" to submit)
// highlight-start
│ ◼ GOOSE_SMART_APPROVE
// highlight-end
```
54 changes: 2 additions & 52 deletions ui/desktop/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import BackButton from '../ui/BackButton';
import { RecentModelsRadio } from './models/RecentModels';
import { ExtensionItem } from './extensions/ExtensionItem';
import type { View } from '../../App';
import ModeSelection from './basic/ModeSelection';
import { getApiUrl, getSecretKey } from '../../config';
import { ModeSelection } from './basic/ModeSelection';

const EXTENSIONS_DESCRIPTION =
'The Model Context Protocol (MCP) is a system that allows AI models to securely connect with local or remote resources using standard server setups. It works like a client-server setup and expands AI capabilities using three main components: Prompts, Resources, and Tools.';
Expand Down Expand Up @@ -62,55 +61,6 @@ export default function SettingsView({
setView: (view: View) => void;
viewOptions: SettingsViewOptions;
}) {
const [mode, setMode] = useState('auto');

const handleModeChange = async (newMode: string) => {
const storeResponse = await fetch(getApiUrl('/configs/store'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
body: JSON.stringify({
key: 'GOOSE_MODE',
value: newMode,
isSecret: false,
}),
});

if (!storeResponse.ok) {
const errorText = await storeResponse.text();
console.error('Store response error:', errorText);
throw new Error(`Failed to store new goose mode: ${newMode}`);
}
setMode(newMode);
};

useEffect(() => {
const fetchCurrentMode = async () => {
try {
const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
});

if (response.ok) {
const { value } = await response.json();
if (value) {
setMode(value);
}
}
} catch (error) {
console.error('Error fetching current mode:', error);
}
};

fetchCurrentMode();
}, []);

const [settings, setSettings] = React.useState<SettingsType>(() => {
const saved = localStorage.getItem('user_settings');
window.electron.logInfo('Settings: ' + saved);
Expand Down Expand Up @@ -304,7 +254,7 @@ export default function SettingsView({
Others setting like Goose Mode, Tool Output, Experiment and more
</p>

<ModeSelection value={mode} onChange={handleModeChange} />
<ModeSelection />
</div>
</section>
</div>
Expand Down
67 changes: 60 additions & 7 deletions ui/desktop/src/components/settings/basic/ModeSelection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as RadioGroup from '@radix-ui/react-radio-group';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { getApiUrl, getSecretKey } from '../../../config';

const ModeSelection = ({ value, onChange }) => {
export const ModeSelection = () => {
const modes = [
{
value: 'auto',
Expand All @@ -11,7 +12,8 @@ const ModeSelection = ({ value, onChange }) => {
{
value: 'approve',
label: 'Approval needed',
description: 'Editing, creating, and deleting files will require human approval.',
description:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'Classifies the tool as either a read-only tool or write tool. Write tools will ask for human approval'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sg, done

'Classifies the tool as either a read-only tool or write tool. Write tools will ask for human approval.',
},
{
value: 'chat',
Expand All @@ -20,11 +22,64 @@ const ModeSelection = ({ value, onChange }) => {
},
];

const [currentMode, setCurrentMode] = useState('auto');

const handleModeChange = async (newMode: string) => {
const storeResponse = await fetch(getApiUrl('/configs/store'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
body: JSON.stringify({
key: 'GOOSE_MODE',
value: newMode,
isSecret: false,
}),
});

if (!storeResponse.ok) {
const errorText = await storeResponse.text();
console.error('Store response error:', errorText);
throw new Error(`Failed to store new goose mode: ${newMode}`);
}
setCurrentMode(newMode);
};

useEffect(() => {
const fetchCurrentMode = async () => {
try {
const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
});

if (response.ok) {
const { value } = await response.json();
if (value) {
setCurrentMode(value);
}
}
} catch (error) {
console.error('Error fetching current mode:', error);
}
};

fetchCurrentMode();
}, []);

return (
<div>
<h4 className="font-medium mb-4 text-textStandard">Mode Selection</h4>

<RadioGroup.Root className="flex flex-col space-y-2" value={value} onValueChange={onChange}>
<RadioGroup.Root
className="flex flex-col space-y-2"
value={currentMode}
onValueChange={handleModeChange}
>
{modes.map((mode) => (
<RadioGroup.Item
key={mode.value}
Expand All @@ -41,7 +96,7 @@ const ModeSelection = ({ value, onChange }) => {
</div>
<div className="flex-shrink-0">
<div className="w-4 h-4 flex items-center justify-center rounded-full border border-gray-500 dark:border-gray-400">
{value === mode.value && (
{currentMode === mode.value && (
<div className="w-2 h-2 bg-black dark:bg-white rounded-full" />
)}
</div>
Expand All @@ -52,5 +107,3 @@ const ModeSelection = ({ value, onChange }) => {
</div>
);
};

export default ModeSelection;
Loading