vibecore 0.3.0__py3-none-any.whl → 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. vibecore/agents/default.py +3 -3
  2. vibecore/agents/task.py +3 -3
  3. vibecore/cli.py +67 -43
  4. vibecore/context.py +74 -11
  5. vibecore/flow.py +335 -73
  6. vibecore/handlers/stream_handler.py +35 -56
  7. vibecore/main.py +70 -272
  8. vibecore/session/jsonl_session.py +3 -1
  9. vibecore/session/loader.py +2 -2
  10. vibecore/settings.py +48 -1
  11. vibecore/tools/file/executor.py +59 -13
  12. vibecore/tools/file/tools.py +9 -9
  13. vibecore/tools/path_validator.py +251 -0
  14. vibecore/tools/python/helpers.py +2 -2
  15. vibecore/tools/python/tools.py +2 -2
  16. vibecore/tools/shell/executor.py +63 -7
  17. vibecore/tools/shell/tools.py +9 -9
  18. vibecore/tools/task/executor.py +2 -2
  19. vibecore/tools/task/tools.py +2 -2
  20. vibecore/tools/todo/manager.py +2 -10
  21. vibecore/tools/todo/models.py +5 -14
  22. vibecore/tools/todo/tools.py +5 -5
  23. vibecore/tools/webfetch/tools.py +1 -4
  24. vibecore/tools/websearch/ddgs/backend.py +1 -1
  25. vibecore/tools/websearch/tools.py +1 -4
  26. vibecore/widgets/core.py +3 -17
  27. vibecore/widgets/feedback.py +164 -0
  28. vibecore/widgets/feedback.tcss +121 -0
  29. vibecore/widgets/messages.py +22 -2
  30. vibecore/widgets/messages.tcss +28 -0
  31. vibecore/widgets/tool_messages.py +19 -4
  32. vibecore/widgets/tool_messages.tcss +23 -0
  33. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/METADATA +122 -29
  34. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/RECORD +37 -34
  35. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/WHEEL +0 -0
  36. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/entry_points.txt +0 -0
  37. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/licenses/LICENSE +0 -0
vibecore/main.py CHANGED
@@ -1,30 +1,28 @@
1
1
  import asyncio
2
2
  import traceback
3
3
  from collections import deque
4
- from typing import ClassVar, Literal
4
+ from typing import TYPE_CHECKING, ClassVar, Literal
5
5
 
6
6
  from agents import (
7
- Agent,
8
- ModelSettings,
9
- Runner,
10
7
  RunResultStreaming,
8
+ Session,
11
9
  StreamEvent,
12
- TResponseInputItem,
13
10
  )
14
- from openai.types import Reasoning
15
11
  from openai.types.responses.response_output_message import Content
16
12
  from textual import log, work
17
13
  from textual.app import App, ComposeResult
18
14
  from textual.binding import Binding
19
15
  from textual.reactive import reactive
16
+ from textual.selection import Selection
17
+ from textual.widget import Widget
20
18
  from textual.widgets import Header
21
19
  from textual.worker import Worker
22
20
 
23
- from vibecore.context import VibecoreContext
21
+ if TYPE_CHECKING:
22
+ from vibecore.flow import TWorkflowReturn, VibecoreTextualRunner
23
+
24
24
  from vibecore.handlers import AgentStreamHandler
25
- from vibecore.session import JSONLSession
26
25
  from vibecore.session.loader import SessionLoader
27
- from vibecore.settings import settings
28
26
  from vibecore.utils.text import TextExtractor
29
27
  from vibecore.widgets.core import AppFooter, MainScroll, MyTextArea
30
28
  from vibecore.widgets.info import Welcome
@@ -37,34 +35,13 @@ class AppIsExiting(Exception):
37
35
  pass
38
36
 
39
37
 
40
- def detect_reasoning_effort(prompt: str) -> Literal["low", "medium", "high"] | None:
41
- """Detect reasoning effort level from user prompt keywords.
42
-
43
- Args:
44
- prompt: User input text
45
-
46
- Returns:
47
- Reasoning effort level or None if no keywords detected
48
- """
49
- prompt_lower = prompt.lower()
50
-
51
- # Check for highest priority keywords first
52
- if "ultrathink" in prompt_lower:
53
- return "high"
54
- elif "think hard" in prompt_lower:
55
- return "medium"
56
- elif "think" in prompt_lower:
57
- return "low"
58
-
59
- return None
60
-
61
-
62
38
  class VibecoreApp(App):
63
39
  """A Textual app to manage stopwatches."""
64
40
 
65
41
  CSS_PATH: ClassVar = [
66
42
  "widgets/core.tcss",
67
43
  "widgets/messages.tcss",
44
+ "widgets/feedback.tcss",
68
45
  "widgets/tool_messages.tcss",
69
46
  "widgets/expandable.tcss",
70
47
  "widgets/info.tcss",
@@ -82,10 +59,7 @@ class VibecoreApp(App):
82
59
 
83
60
  def __init__(
84
61
  self,
85
- context: VibecoreContext,
86
- agent: Agent,
87
- session_id: str | None = None,
88
- print_mode: bool = False,
62
+ runner: "VibecoreTextualRunner[TWorkflowReturn]",
89
63
  show_welcome: bool = True,
90
64
  ) -> None:
91
65
  """Initialize the Vibecore app with context and agent.
@@ -94,72 +68,45 @@ class VibecoreApp(App):
94
68
  context: The VibecoreContext instance
95
69
  agent: The Agent instance to use
96
70
  session_id: Optional session ID to load existing session
97
- print_mode: Whether to run in print mode (useful for pipes)
98
71
  show_welcome: Whether to show the welcome message (default: True)
99
72
  """
100
- self.context = context
101
- self.context.app = self # Set the app reference in context
102
- self.agent = agent
103
- self.input_items: list[TResponseInputItem] = []
73
+ self.runner = runner
74
+ if runner.context:
75
+ runner.context.app = self # Set the app reference in context
104
76
  self.current_result: RunResultStreaming | None = None
105
77
  self.current_worker: Worker[None] | None = None
106
- self._session_id_provided = session_id is not None # Track if continuing session
107
- self.print_mode = print_mode
108
78
  self.show_welcome = show_welcome
109
79
  self.message_queue: deque[str] = deque() # Queue for user messages
80
+ self.user_input_event = asyncio.Event() # Initialize event for user input coordination
110
81
 
111
- # Initialize session based on settings
112
- if settings.session.storage_type == "jsonl":
113
- if session_id is None:
114
- # Generate a new session ID based on current date/time
115
- import datetime
82
+ super().__init__()
116
83
 
117
- session_id = f"chat-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
84
+ def on_mouse_up(self) -> None:
85
+ if not self.screen.selections:
86
+ return None
118
87
 
119
- self.session = JSONLSession(
120
- session_id=session_id,
121
- project_path=None, # Will use current working directory
122
- base_dir=settings.session.base_dir,
123
- )
124
- else:
125
- raise NotImplementedError("SQLite session support will be added later")
88
+ widget_text: list[str] = []
89
+ for widget, selection in self.screen.selections.items():
90
+ assert isinstance(widget, Widget) and isinstance(selection, Selection)
91
+ if "copy-button" in widget.classes: # Skip copy buttons
92
+ continue
93
+ selected_text_in_widget = widget.get_selection(selection)
94
+ if selected_text_in_widget is not None:
95
+ widget_text.extend(selected_text_in_widget)
126
96
 
127
- super().__init__()
97
+ selected_text = "".join(widget_text)
98
+ self.copy_to_clipboard(selected_text)
99
+ self.notify("Copied to clipboard")
128
100
 
129
101
  def compose(self) -> ComposeResult:
130
102
  """Create child widgets for the app."""
131
103
  yield Header()
132
104
  yield AppFooter()
133
- with MainScroll(id="messages"):
105
+ with MainScroll(id="messages") as main_scroll:
106
+ main_scroll.anchor()
134
107
  if self.show_welcome:
135
108
  yield Welcome()
136
109
 
137
- async def on_mount(self) -> None:
138
- """Called when the app is mounted."""
139
- # Connect to MCP servers if configured
140
- if self.context.mcp_manager:
141
- try:
142
- await self.context.mcp_manager.connect()
143
- log(f"Connected to {len(self.context.mcp_manager.servers)} MCP servers")
144
- except Exception as e:
145
- log(f"Failed to connect to MCP servers: {e}")
146
- # Continue without MCP servers rather than crashing
147
-
148
- # Load session history if we're continuing from a previous session
149
- if self._session_id_provided:
150
- await self.load_session_history()
151
-
152
- async def on_unmount(self) -> None:
153
- """Called when the app is being unmounted (shutdown)."""
154
- # Cleanup MCP servers during unmount
155
- if self.context.mcp_manager:
156
- try:
157
- log("Disconnecting from MCP servers...")
158
- await self.context.mcp_manager.disconnect()
159
- log("Disconnected from MCP servers")
160
- except Exception as e:
161
- log(f"Error disconnecting from MCP servers during unmount: {e}")
162
-
163
110
  def extract_text_from_content(self, content: list[Content]) -> str:
164
111
  """Extract text from various content formats."""
165
112
  return TextExtractor.extract_from_content(content)
@@ -179,10 +126,9 @@ class VibecoreApp(App):
179
126
  """Add a message widget to the main scroll area."""
180
127
  await self.add_message(message)
181
128
 
182
- async def handle_agent_update(self, new_agent: Agent) -> None:
183
- """Handle agent updates."""
184
- log(f"Agent updated: {new_agent.name}")
185
- self.agent = new_agent
129
+ async def handle_agent_message_update(self, message: BaseMessage) -> None:
130
+ """Message in the widget's message list is updated with new delta or status"""
131
+ pass
186
132
 
187
133
  async def handle_agent_error(self, error: Exception) -> None:
188
134
  """Handle errors during streaming."""
@@ -207,14 +153,16 @@ class VibecoreApp(App):
207
153
  try:
208
154
  last_message = main_scroll.query_one("BaseMessage:last-child", BaseMessage)
209
155
  if last_message.status == MessageStatus.EXECUTING:
156
+ # XXX(serialx): Consider marking it as cancelled instead
157
+ # last_message.status = MessageStatus.ERROR
210
158
  last_message.remove()
211
159
  except Exception:
212
160
  # No messages to clean up
213
161
  pass
214
162
 
215
- async def load_session_history(self) -> None:
163
+ async def load_session_history(self, session: Session) -> None:
216
164
  """Load and display messages from session history."""
217
- loader = SessionLoader(self.session)
165
+ loader = SessionLoader(session)
218
166
  messages = await loader.load_history()
219
167
 
220
168
  # Remove Welcome widget if we have messages
@@ -237,10 +185,24 @@ class VibecoreApp(App):
237
185
 
238
186
  async def wait_for_user_input(self) -> str:
239
187
  """Used in flow mode. See examples/basic_agent.py"""
188
+ if self.message_queue:
189
+ user_input = self.message_queue.popleft()
190
+
191
+ user_message = UserMessage(user_input)
192
+ await self.add_message(user_message)
193
+ self.get_child_by_id("messages").scroll_end()
194
+
195
+ return user_input
196
+
240
197
  self.agent_status = "waiting_user_input"
241
- self.user_input_event = asyncio.Event()
198
+ self.user_input_event.clear() # Reset the event for next wait
242
199
  await self.user_input_event.wait()
243
- user_input = self.message_queue.pop()
200
+ user_input = self.message_queue.popleft()
201
+
202
+ user_message = UserMessage(user_input)
203
+ await self.add_message(user_message)
204
+ self.get_child_by_id("messages").scroll_end()
205
+
244
206
  return user_input
245
207
 
246
208
  async def on_my_text_area_user_message(self, event: MyTextArea.UserMessage) -> None:
@@ -248,12 +210,8 @@ class VibecoreApp(App):
248
210
  if event.text:
249
211
  # Check for special commands
250
212
  text_strip = event.text.strip()
251
- if text_strip == "/clear":
252
- await self.handle_clear_command()
253
- return
254
- elif text_strip == "/help":
213
+ if text_strip == "/help":
255
214
  help_text = "Available commands:\n"
256
- help_text += "• /clear - Clear the current session and start a new one\n"
257
215
  help_text += "• /help - Show this help message\n\n"
258
216
  help_text += "Keyboard shortcuts:\n"
259
217
  help_text += "• Esc - Cancel current agent operation\n"
@@ -262,13 +220,6 @@ class VibecoreApp(App):
262
220
  await self.add_message(SystemMessage(help_text))
263
221
  return
264
222
 
265
- user_message = UserMessage(event.text)
266
- await self.add_message(user_message)
267
- user_message.scroll_visible()
268
-
269
- if self.agent_status == "waiting_user_input":
270
- self.message_queue.append(event.text)
271
- self.user_input_event.set()
272
223
  if self.agent_status == "running":
273
224
  # If agent is running, queue the message
274
225
  self.message_queue.append(event.text)
@@ -280,41 +231,8 @@ class VibecoreApp(App):
280
231
  status="Generating…", metadata=f"{queued_count} message{'s' if queued_count > 1 else ''} queued"
281
232
  )
282
233
  else:
283
- # Detect reasoning effort from prompt keywords
284
- detected_effort = detect_reasoning_effort(event.text)
285
- reasoning_effort = detected_effort or settings.reasoning_effort
286
-
287
- # Create agent with appropriate reasoning effort
288
- agent_to_use = self.agent
289
- if reasoning_effort is not None:
290
- # Create a copy of the agent with updated model settings
291
- current_settings = self.agent.model_settings or ModelSettings()
292
- new_reasoning = Reasoning(effort=reasoning_effort, summary=settings.reasoning_summary)
293
- updated_settings = ModelSettings(
294
- include_usage=current_settings.include_usage,
295
- reasoning=new_reasoning,
296
- )
297
- agent_to_use = Agent[VibecoreContext](
298
- name=self.agent.name,
299
- handoff_description=self.agent.handoff_description,
300
- instructions=self.agent.instructions,
301
- tools=self.agent.tools,
302
- model=self.agent.model,
303
- model_settings=updated_settings,
304
- handoffs=self.agent.handoffs,
305
- mcp_servers=self.agent.mcp_servers,
306
- )
307
-
308
- # Process the message immediately
309
- result = Runner.run_streamed(
310
- agent_to_use,
311
- input=event.text, # Pass string directly when using session
312
- context=self.context,
313
- max_turns=settings.max_turns,
314
- session=self.session,
315
- )
316
-
317
- self.current_worker = self.handle_streamed_response(result)
234
+ self.message_queue.append(event.text)
235
+ self.user_input_event.set()
318
236
 
319
237
  @work(exclusive=True)
320
238
  async def handle_streamed_response(self, result: RunResultStreaming) -> None:
@@ -324,45 +242,27 @@ class VibecoreApp(App):
324
242
  self.agent_stream_handler = AgentStreamHandler(self)
325
243
  await self.agent_stream_handler.process_stream(result)
326
244
 
327
- used = result.context_wrapper.usage.total_tokens
245
+ # Determine usage based on the last model response rather than the aggregated usage
246
+ # from the entire session so that context fullness reflects the most recent request.
247
+ used_tokens: float = 0.0
248
+ if result.raw_responses:
249
+ last_response = result.raw_responses[-1]
250
+ last_usage = getattr(last_response, "usage", None)
251
+ if last_usage:
252
+ used_tokens = float(last_usage.total_tokens)
253
+
328
254
  max_ctx = self._get_model_context_window()
329
- log(f"Context usage: {used} / {max_ctx} total tokens")
330
- self.context.context_fullness = min(1.0, float(used) / float(max_ctx))
255
+ log(f"Context usage: {used_tokens} / {max_ctx} total tokens")
256
+ context_fullness = min(1.0, used_tokens / float(max_ctx))
331
257
  footer = self.query_one(AppFooter)
332
- footer.set_context_progress(self.context.context_fullness)
258
+ footer.set_context_progress(context_fullness)
333
259
 
334
260
  self.agent_status = "idle"
335
261
  self.current_result = None
336
262
  self.current_worker = None
337
263
 
338
- await self.process_message_queue()
339
-
340
- async def process_message_queue(self) -> None:
341
- """Process any messages that were queued while the agent was running."""
342
- if self.message_queue:
343
- # Get the next message from the queue
344
- next_message = self.message_queue.popleft()
345
- log(f"Processing queued message: {next_message}")
346
-
347
- # Process the message
348
- result = Runner.run_streamed(
349
- self.agent,
350
- input=next_message,
351
- context=self.context,
352
- max_turns=settings.max_turns,
353
- session=self.session,
354
- )
355
-
356
- self.current_worker = self.handle_streamed_response(result)
357
-
358
- def on_click(self) -> None:
359
- self.query_one("#input-textarea").focus()
360
-
361
264
  def _get_model_context_window(self) -> int:
362
- from vibecore.settings import settings
363
-
364
- model_name = settings.default_model
365
- log(f"Getting context window for model: {model_name}")
265
+ # TODO(serialx): Implement later
366
266
  return 200000
367
267
 
368
268
  def action_toggle_dark(self) -> None:
@@ -402,7 +302,7 @@ class VibecoreApp(App):
402
302
  """Reset exit confirmation after 1 second and remove the message."""
403
303
  try:
404
304
  # Wait for 1 second
405
- await asyncio.sleep(1.0)
305
+ await asyncio.sleep(2.0)
406
306
 
407
307
  # Reset confirmation state
408
308
  self._exit_confirmation_active = False
@@ -413,55 +313,6 @@ class VibecoreApp(App):
413
313
  # Task was cancelled (new Ctrl-D pressed)
414
314
  pass
415
315
 
416
- async def run_print(self, prompt: str | None = None) -> str:
417
- """Run the agent and return the raw output for printing.
418
-
419
- Args:
420
- prompt: Optional prompt text. If not provided, reads from stdin.
421
-
422
- Returns:
423
- The agent's text output as a string
424
- """
425
- import sys
426
-
427
- # Use provided prompt or read from stdin
428
- input_text = prompt.strip() if prompt else sys.stdin.read().strip()
429
-
430
- if not input_text:
431
- return ""
432
-
433
- # Import needed event types
434
- from agents import RawResponsesStreamEvent
435
- from openai.types.responses import ResponseTextDeltaEvent
436
-
437
- if self.context.mcp_manager:
438
- await self.context.mcp_manager.connect()
439
-
440
- # Run the agent
441
- result = Runner.run_streamed(
442
- self.agent,
443
- input=input_text,
444
- context=self.context,
445
- max_turns=settings.max_turns,
446
- session=self.session,
447
- )
448
-
449
- # Collect all agent text output
450
- agent_output = ""
451
-
452
- async for event in result.stream_events():
453
- # Handle text output from agent
454
- match event:
455
- case RawResponsesStreamEvent(data=data):
456
- match data:
457
- case ResponseTextDeltaEvent(delta=delta) if delta:
458
- agent_output += delta
459
-
460
- if self.context.mcp_manager:
461
- await self.context.mcp_manager.disconnect()
462
-
463
- return agent_output.strip()
464
-
465
316
  async def handle_task_tool_event(self, tool_name: str, tool_call_id: str, event: StreamEvent) -> None:
466
317
  """Handle streaming events from task tool sub-agents.
467
318
 
@@ -473,56 +324,3 @@ class VibecoreApp(App):
473
324
  Note: The main app receives this event from the agent's task tool handler.
474
325
  """
475
326
  await self.agent_stream_handler.handle_task_tool_event(tool_name, tool_call_id, event)
476
-
477
- async def handle_clear_command(self) -> None:
478
- """Handle the /clear command to create a new session and clear the UI."""
479
- log("Clearing session and creating new session")
480
-
481
- # Cancel any running agent
482
- if self.agent_status == "running":
483
- self.action_cancel_agent()
484
-
485
- # Clear message queue
486
- self.message_queue.clear()
487
-
488
- # Generate a new session ID
489
- import datetime
490
-
491
- new_session_id = f"chat-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
492
-
493
- # Create new session
494
- if settings.session.storage_type == "jsonl":
495
- self.session = JSONLSession(
496
- session_id=new_session_id,
497
- project_path=None, # Will use current working directory
498
- base_dir=settings.session.base_dir,
499
- )
500
- else:
501
- raise NotImplementedError("SQLite session support will be added later")
502
-
503
- # Reset context state
504
- self.context.reset_state()
505
-
506
- # Clear input items
507
- self.input_items.clear()
508
-
509
- # Clear the UI - remove all messages and add welcome back
510
- main_scroll = self.query_one("#messages", MainScroll)
511
-
512
- # Remove all existing messages
513
- for message in main_scroll.query("BaseMessage"):
514
- message.remove()
515
-
516
- # Remove welcome if it exists
517
- for welcome in main_scroll.query("Welcome"):
518
- welcome.remove()
519
-
520
- # Add welcome widget back if show_welcome is True
521
- if self.show_welcome:
522
- await main_scroll.mount(Welcome())
523
-
524
- # Show system message to confirm the clear operation
525
- system_message = SystemMessage(f"✨ Session cleared! Started new session: {new_session_id}")
526
- await main_scroll.mount(system_message)
527
-
528
- log(f"New session created: {new_session_id}")
@@ -5,6 +5,8 @@ import logging
5
5
  from pathlib import Path
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from agents import Session
9
+
8
10
  if TYPE_CHECKING:
9
11
  from openai.types.responses import ResponseInputItemParam as TResponseInputItem
10
12
 
@@ -14,7 +16,7 @@ from .path_utils import get_session_file_path
14
16
  logger = logging.getLogger(__name__)
15
17
 
16
18
 
17
- class JSONLSession:
19
+ class JSONLSession(Session):
18
20
  """JSONL-based implementation of the agents.Session protocol.
19
21
 
20
22
  Stores conversation history in JSON Lines format, with one JSON object
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing import Any
4
4
 
5
+ from agents import Session
5
6
  from openai.types.responses import (
6
7
  ResponseFunctionToolCall,
7
8
  ResponseInputItemParam,
@@ -12,7 +13,6 @@ from openai.types.responses import (
12
13
  from pydantic import TypeAdapter
13
14
  from textual import log
14
15
 
15
- from vibecore.session.jsonl_session import JSONLSession
16
16
  from vibecore.utils.text import TextExtractor
17
17
  from vibecore.widgets.messages import (
18
18
  AgentMessage,
@@ -28,7 +28,7 @@ from vibecore.widgets.tool_messages import BaseToolMessage
28
28
  class SessionLoader:
29
29
  """Loads and parses session history into message widgets."""
30
30
 
31
- def __init__(self, session: JSONLSession):
31
+ def __init__(self, session: Session):
32
32
  """Initialize SessionLoader with a session.
33
33
 
34
34
  Args:
vibecore/settings.py CHANGED
@@ -39,6 +39,48 @@ class SessionSettings(BaseModel):
39
39
  )
40
40
 
41
41
 
42
+ class PathConfinementSettings(BaseModel):
43
+ """Configuration for path confinement."""
44
+
45
+ enabled: bool = Field(
46
+ default=True,
47
+ description="Enable path confinement for file and shell tools",
48
+ )
49
+
50
+ allowed_directories: list[Path] = Field(
51
+ default_factory=lambda: [Path.cwd()],
52
+ description="List of directories that tools can access",
53
+ )
54
+
55
+ allow_home: bool = Field(
56
+ default=False,
57
+ description="Allow access to user's home directory",
58
+ )
59
+
60
+ allow_temp: bool = Field(
61
+ default=True,
62
+ description="Allow access to system temp directories",
63
+ )
64
+
65
+ strict_mode: bool = Field(
66
+ default=False,
67
+ description="Strict mode prevents any path traversal attempts",
68
+ )
69
+
70
+ @field_validator("allowed_directories", mode="before")
71
+ @classmethod
72
+ def resolve_paths(cls, v: list[str | Path]) -> list[Path]:
73
+ """Resolve and validate directory paths."""
74
+ paths = []
75
+ for p in v:
76
+ path = Path(p).expanduser().resolve()
77
+ if not path.exists():
78
+ # Create directory if it doesn't exist
79
+ path.mkdir(parents=True, exist_ok=True)
80
+ paths.append(path)
81
+ return paths
82
+
83
+
42
84
  class MCPServerConfig(BaseModel):
43
85
  """Configuration for an MCP server."""
44
86
 
@@ -155,6 +197,12 @@ class Settings(BaseSettings):
155
197
  description="List of MCP servers to connect to",
156
198
  )
157
199
 
200
+ # Path confinement configuration
201
+ path_confinement: PathConfinementSettings = Field(
202
+ default_factory=PathConfinementSettings,
203
+ description="Path confinement configuration",
204
+ )
205
+
158
206
  rich_tool_names: list[str] = Field(
159
207
  default_factory=list,
160
208
  description="List of tools to render with RichToolMessage (temporary settings)",
@@ -207,7 +255,6 @@ class Settings(BaseSettings):
207
255
  return (
208
256
  init_settings,
209
257
  env_settings,
210
- dotenv_settings,
211
258
  YamlConfigSettingsSource(settings_cls),
212
259
  file_secret_settings,
213
260
  )