• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dcdpr / jp / 26881567179

03 Jun 2026 11:24AM UTC coverage: 66.675% (+0.08%) from 66.596%
26881567179

Pull #727

github

web-flow
Merge cd4b6770c into 5876e5146
Pull Request #727: feat(cli, config, tool): Add `--mount` flag and `access.fs` tool policy

737 of 1044 new or added lines in 20 files covered. (70.59%)

456 existing lines in 8 files now uncovered.

34365 of 51541 relevant lines covered (66.68%)

249.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

30.14
/crates/jp_cli/src/cmd/query/tool/executor.rs
1
//! Single tool execution for the query stream pipeline.
2
//!
3
//! The `ToolExecutor` handles execution of a single tool call, including:
4
//!
5
//! - Permission prompts (run mode configuration)
6
//! - Input prompts (tool-specific questions)
7
//! - Result formatting
8
//!
9
//! # Lifecycle State Machine
10
//!
11
//! ```text
12
//!                     ┌─────────────────────────────────────────────────────┐
13
//!                     │                  ToolExecutor                       │
14
//!                     │                                                     │
15
//!   ┌─────────┐       │  ┌─────────┐    ┌──────────────────┐    ┌─────────┐ │
16
//!   │ new()   │──────▶│  │ Pending │───▶│AwaitingPermission│───▶│ Running │ │
17
//!   └─────────┘       │  └─────────┘    └──────────────────┘    └────┬────┘ │
18
//!                     │                         │                    │      │
19
//!                     │                         │ (skip)             │      │
20
//!                     │                         ▼                    ▼      │
21
//!                     │                   ┌───────────┐      ┌─────────────┐│
22
//!                     │                   │ Completed │◀─────│AwaitingInput││
23
//!                     │                   └───────────┘      └─────────────┘│
24
//!                     │                         ▲                    │      │
25
//!                     │                         │                    │      │
26
//!                     │                   ┌───────────────────┐      │      │
27
//!                     │                   │AwaitingResultEdit │◀─────┘      │
28
//!                     │                   └───────────────────┘             │
29
//!                     └─────────────────────────────────────────────────────┘
30
//! ```
31
//!
32
//! # Thread Safety
33
//!
34
//! The executor works with `SharedTurnState` (`Arc<RwLock<TurnState>>`) to
35
//! support parallel execution.
36
//! Lock durations are minimized to avoid blocking other executors.
37
//!
38
//! # Testing
39
//!
40
//! The [`Executor`] trait allows for mock implementations in tests.
41
//! See [`MockExecutor`] for testing parallel execution behavior.
42
//!
43
//! [`MockExecutor`]: jp_llm::tool::executor::MockExecutor
44

45
use std::sync::Arc;
46

47
use async_trait::async_trait;
48
use camino::Utf8Path;
49
use indexmap::IndexMap;
50
use jp_config::conversation::tool::{RunMode, ToolConfigWithDefaults};
51
use jp_conversation::event::{ToolCallRequest, ToolCallResponse};
52
use jp_llm::{
53
    ExecutionOutcome,
54
    tool::{
55
        ToolDefinition,
56
        builtin::BuiltinExecutors,
57
        executor::{Executor, ExecutorResult, ExecutorSource, PermissionInfo},
58
    },
59
};
60
use jp_mcp::Client;
61
use serde_json::Value;
62
use tokio_util::sync::CancellationToken;
63

64
use crate::access::{approvals::ApprovalStore, compile::compile_tool_policy};
65

66
/// Terminal executor source that creates real [`ToolExecutor`] instances.
67
///
68
/// Holds pre-resolved tool definitions so executors don't need to re-resolve
69
/// (avoiding redundant MCP server fetches).
70
pub struct TerminalExecutorSource {
71
    builtin_executors: BuiltinExecutors,
72
    definitions: IndexMap<String, ToolDefinition>,
73
    approvals: Arc<ApprovalStore>,
74
}
75

76
impl TerminalExecutorSource {
77
    #[must_use]
78
    pub fn new(
38✔
79
        builtin_executors: BuiltinExecutors,
38✔
80
        definitions: &[ToolDefinition],
38✔
81
        approvals: Arc<ApprovalStore>,
38✔
82
    ) -> Self {
38✔
83
        let definitions = definitions
38✔
84
            .iter()
38✔
85
            .map(|d| (d.name.clone(), d.clone()))
38✔
86
            .collect();
38✔
87
        Self {
38✔
88
            builtin_executors,
38✔
89
            definitions,
38✔
90
            approvals,
38✔
91
        }
38✔
92
    }
38✔
93
}
94

95
impl ExecutorSource for TerminalExecutorSource {
96
    fn create(
2✔
97
        &self,
2✔
98
        request: ToolCallRequest,
2✔
99
        config: ToolConfigWithDefaults,
2✔
100
    ) -> Option<Box<dyn Executor>> {
2✔
101
        let definition = self.definitions.get(&request.name)?.clone();
2✔
102

103
        Some(Box::new(ToolExecutor::new(
×
104
            request,
×
105
            config,
×
106
            definition,
×
107
            Arc::new(self.builtin_executors.clone()),
×
NEW
108
            self.approvals.clone(),
×
UNCOV
109
        )))
×
110
    }
2✔
111
}
112

113
/// Executes a single tool call.
114
///
115
/// The executor handles the execution lifecycle including permission prompts,
116
/// input questions, and result formatting.
117
///
118
/// # Note
119
///
120
/// Interactive prompts currently happen inside `ToolDefinition::call()`.
121
/// In the future, prompts will be driven by the `ToolCoordinator`, and the
122
/// executor will only handle pure execution.
123
pub struct ToolExecutor {
124
    request: ToolCallRequest,
125
    config: ToolConfigWithDefaults,
126
    definition: ToolDefinition,
127
    builtin_executors: Arc<BuiltinExecutors>,
128
    approvals: Arc<ApprovalStore>,
129
}
130

131
impl ToolExecutor {
132
    fn new(
×
133
        request: ToolCallRequest,
×
134
        config: ToolConfigWithDefaults,
×
135
        definition: ToolDefinition,
×
136
        builtin_executors: Arc<BuiltinExecutors>,
×
NEW
137
        approvals: Arc<ApprovalStore>,
×
138
    ) -> Self {
×
139
        Self {
×
140
            request,
×
141
            config,
×
142
            definition,
×
143
            builtin_executors,
×
NEW
144
            approvals,
×
145
        }
×
146
    }
×
147
}
148

149
#[async_trait]
150
impl Executor for ToolExecutor {
151
    fn tool_id(&self) -> &str {
×
152
        &self.request.id
×
153
    }
×
154

155
    fn tool_name(&self) -> &str {
×
156
        &self.request.name
×
157
    }
×
158

159
    fn arguments(&self) -> &serde_json::Map<String, Value> {
×
160
        &self.request.arguments
×
161
    }
×
162

163
    fn permission_info(&self) -> Option<PermissionInfo> {
×
164
        let run_mode = self.config.run();
×
165

166
        // No prompt needed for these modes
167
        if matches!(run_mode, RunMode::Unattended | RunMode::Skip) {
×
168
            return None;
×
169
        }
×
170

171
        Some(PermissionInfo {
×
172
            tool_id: self.request.id.clone(),
×
173
            tool_name: self.request.name.clone(),
×
174
            tool_source: self.config.source().clone(),
×
175
            run_mode,
×
176
            arguments: self.request.arguments.clone().into(),
×
177
        })
×
178
    }
×
179

180
    fn set_arguments(&mut self, args: Value) {
×
181
        if let Value::Object(map) = args {
×
182
            self.request.arguments = map;
×
183
        }
×
184
        // If not an object, ignore (preserve original arguments)
185
    }
×
186

187
    async fn execute(
188
        &self,
189
        answers: &IndexMap<String, Value>,
190
        mcp_client: &Client,
191
        root: &Utf8Path,
192
        cancellation_token: CancellationToken,
193
    ) -> ExecutorResult {
×
194
        // Compile this tool's access grants into a runtime policy, baking
195
        // approved external targets in. The policy travels to the tool in its
196
        // context so the tool can self-enforce. A policy that fails to compile
197
        // (invalid config) fails the tool rather than running it unenforced.
198
        let access = match compile_tool_policy(self.config.access(), root, &self.approvals) {
199
            Ok(access) => access,
200
            Err(error) => {
201
                return ExecutorResult::Completed(ToolCallResponse {
202
                    id: self.request.id.clone(),
203
                    result: Err(format!(
204
                        "invalid access policy for tool '{}': {error}",
205
                        self.request.name
206
                    )),
207
                });
208
            }
209
        };
210

211
        let result = self
212
            .definition
213
            .execute(
214
                self.request.id.clone(),
215
                Value::Object(self.request.arguments.clone()),
216
                answers,
217
                &self.config,
218
                mcp_client,
219
                root,
220
                cancellation_token,
221
                &self.builtin_executors,
222
                access.as_ref(),
223
            )
224
            .await;
225

226
        match result {
227
            Ok(ExecutionOutcome::Completed { id, result }) => {
228
                ExecutorResult::Completed(ToolCallResponse { id, result })
229
            }
230
            Ok(ExecutionOutcome::Cancelled { id }) => ExecutorResult::Completed(ToolCallResponse {
231
                id,
232
                result: Ok("Tool execution cancelled.".to_string()),
233
            }),
234
            Ok(ExecutionOutcome::NeedsInput { id: _, question }) => ExecutorResult::NeedsInput {
235
                tool_id: self.request.id.clone(),
236
                tool_name: self.request.name.clone(),
237
                question,
238
                accumulated_answers: answers.clone(),
239
            },
240
            Err(e) => ExecutorResult::Completed(ToolCallResponse {
241
                id: self.request.id.clone(),
242
                result: Err(e.to_string()),
243
            }),
244
        }
245
    }
×
246
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc