Skip to content

Commit ea4984e

Browse files
feat(permissions): remove timeouts from permissions (#2220)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 379d7f5 commit ea4984e

File tree

2 files changed

+64
-56
lines changed

2 files changed

+64
-56
lines changed

crates/forge_app/src/tool_executor.rs

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
use std::path::PathBuf;
22
use std::sync::Arc;
33

4-
use forge_domain::{
5-
CodebaseQueryResult, TitleFormat, ToolCallContext, ToolCallFull, ToolCatalog, ToolOutput,
6-
};
7-
use forge_template::Element;
4+
use forge_domain::{CodebaseQueryResult, ToolCallContext, ToolCatalog, ToolOutput};
85

96
use crate::fmt::content::FormatContent;
107
use crate::operation::{TempContentFiles, ToolOperation};
118
use crate::services::ShellService;
12-
use crate::utils::format_display_path;
139
use crate::{
1410
ConversationService, EnvironmentService, FollowUpService, FsCreateService, FsPatchService,
1511
FsReadService, FsRemoveService, FsSearchService, FsUndoService, ImageReadService,
16-
NetFetchService, PlanCreateService, PolicyService, SkillFetchService, WorkspaceService,
12+
NetFetchService, PlanCreateService, SkillFetchService, WorkspaceService,
1713
};
1814

1915
pub struct ToolExecutor<S> {
@@ -35,41 +31,13 @@ impl<
3531
+ ConversationService
3632
+ EnvironmentService
3733
+ PlanCreateService
38-
+ PolicyService
3934
+ SkillFetchService,
4035
> ToolExecutor<S>
4136
{
4237
pub fn new(services: Arc<S>) -> Self {
4338
Self { services }
4439
}
4540

46-
/// Check if a tool operation is allowed based on the workflow policies
47-
async fn check_tool_permission(
48-
&self,
49-
tool_input: &ToolCatalog,
50-
context: &ToolCallContext,
51-
) -> anyhow::Result<bool> {
52-
let cwd = self.services.get_environment().cwd;
53-
let operation = tool_input.to_policy_operation(cwd.clone());
54-
if let Some(operation) = operation {
55-
let decision = self.services.check_operation_permission(&operation).await?;
56-
57-
// Send custom policy message to the user when a policy file was created
58-
if let Some(policy_path) = decision.path {
59-
context
60-
.send_title(
61-
TitleFormat::debug("Permissions Update")
62-
.sub_title(format_display_path(policy_path.as_path(), &cwd)),
63-
)
64-
.await?;
65-
}
66-
if !decision.allowed {
67-
return Ok(true);
68-
}
69-
}
70-
Ok(false)
71-
}
72-
7341
async fn dump_operation(&self, operation: &ToolOperation) -> anyhow::Result<TempContentFiles> {
7442
match operation {
7543
ToolOperation::NetFetch { input: _, output } => {
@@ -321,29 +289,11 @@ impl<
321289

322290
pub async fn execute(
323291
&self,
324-
input: ToolCallFull,
292+
tool_input: ToolCatalog,
325293
context: &ToolCallContext,
326294
) -> anyhow::Result<ToolOutput> {
327-
let tool_input: ToolCatalog = ToolCatalog::try_from(input)?;
328295
let tool_kind = tool_input.kind();
329296
let env = self.services.get_environment();
330-
if let Some(content) = tool_input.to_content(&env) {
331-
context.send(content).await?;
332-
}
333-
334-
// Check permissions before executing the tool (only in restricted mode)
335-
if self.services.is_restricted() && self.check_tool_permission(&tool_input, context).await?
336-
{
337-
// Send formatted output message for policy denial
338-
context
339-
.send(TitleFormat::error("Permission Denied"))
340-
.await?;
341-
342-
return Ok(ToolOutput::text(
343-
Element::new("permission_denied")
344-
.cdata("User has denied the permission to execute this tool"),
345-
));
346-
}
347297

348298
let execution_result = self.call_internal(tool_input.clone()).await;
349299

crates/forge_app/src/tool_registry.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,20 @@ use forge_domain::{
77
Agent, AgentId, AgentInput, ChatResponse, ChatResponseContent, ToolCallContext, ToolCallFull,
88
ToolCatalog, ToolDefinition, ToolName, ToolOutput, ToolResult,
99
};
10+
use forge_template::Element;
1011
use futures::future::join_all;
1112
use strum::IntoEnumIterator;
1213
use tokio::time::timeout;
1314

1415
use crate::agent_executor::AgentExecutor;
1516
use crate::dto::ToolsOverview;
1617
use crate::error::Error;
18+
use crate::fmt::content::FormatContent;
1719
use crate::mcp_executor::McpExecutor;
1820
use crate::tool_executor::ToolExecutor;
19-
use crate::{EnvironmentService, McpService, Services, ToolResolver, WorkspaceService};
21+
use crate::{
22+
EnvironmentService, McpService, PolicyService, Services, ToolResolver, WorkspaceService,
23+
};
2024

2125
pub struct ToolRegistry<S> {
2226
tool_executor: ToolExecutor<S>,
@@ -54,6 +58,36 @@ impl<S: Services> ToolRegistry<S> {
5458
})?
5559
}
5660

61+
/// Check if a tool operation is allowed based on the workflow policies
62+
async fn check_tool_permission(
63+
&self,
64+
tool_input: &ToolCatalog,
65+
context: &ToolCallContext,
66+
) -> anyhow::Result<bool> {
67+
let cwd = self.services.get_environment().cwd;
68+
let operation = tool_input.to_policy_operation(cwd.clone());
69+
if let Some(operation) = operation {
70+
let decision = self.services.check_operation_permission(&operation).await?;
71+
72+
// Send custom policy message to the user when a policy file was created
73+
if let Some(policy_path) = decision.path {
74+
use forge_domain::TitleFormat;
75+
76+
use crate::utils::format_display_path;
77+
context
78+
.send_title(
79+
TitleFormat::debug("Permissions Update")
80+
.sub_title(format_display_path(policy_path.as_path(), &cwd)),
81+
)
82+
.await?;
83+
}
84+
if !decision.allowed {
85+
return Ok(true);
86+
}
87+
}
88+
Ok(false)
89+
}
90+
5791
async fn call_inner(
5892
&self,
5993
agent: &Agent,
@@ -67,8 +101,32 @@ impl<S: Services> ToolRegistry<S> {
67101

68102
// First, try to call a Forge tool
69103
if ToolCatalog::contains(&input.name) {
70-
self.call_with_timeout(&tool_name, || self.tool_executor.execute(input, context))
71-
.await
104+
let tool_input: ToolCatalog = ToolCatalog::try_from(input)?;
105+
let env = self.services.get_environment();
106+
if let Some(content) = tool_input.to_content(&env) {
107+
context.send(content).await?;
108+
}
109+
110+
// Check permissions before executing the tool (only in restricted mode)
111+
// This is done BEFORE the timeout to ensure permissions are never timed out
112+
if self.services.is_restricted()
113+
&& self.check_tool_permission(&tool_input, context).await?
114+
{
115+
// Send formatted output message for policy denial
116+
context
117+
.send(forge_domain::TitleFormat::error("Permission Denied"))
118+
.await?;
119+
120+
return Ok(ToolOutput::text(
121+
Element::new("permission_denied")
122+
.cdata("User has denied the permission to execute this tool"),
123+
));
124+
}
125+
126+
self.call_with_timeout(&tool_name, || {
127+
self.tool_executor.execute(tool_input, context)
128+
})
129+
.await
72130
} else if self.agent_executor.contains_tool(&input.name).await? {
73131
// Handle agent delegation tool calls
74132
let agent_input = AgentInput::try_from(&input)?;

0 commit comments

Comments
 (0)