cowork-dash 0.1.6__py3-none-any.whl → 0.1.8__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.
cowork_dash/tools.py CHANGED
@@ -3,12 +3,54 @@ import sys
3
3
  import io
4
4
  import traceback
5
5
  import subprocess
6
+ import threading
6
7
  from contextlib import redirect_stdout, redirect_stderr
7
8
 
8
- from .config import WORKSPACE_ROOT
9
+ from .config import WORKSPACE_ROOT, VIRTUAL_FS
9
10
  from .canvas import parse_canvas_object, generate_canvas_id
10
11
 
11
12
 
13
+ # Thread-local storage for current session context
14
+ # This allows tools to know which session they're operating in
15
+ _tool_context = threading.local()
16
+
17
+
18
+ def set_tool_session_context(session_id: Optional[str]) -> None:
19
+ """Set the current session context for tool operations.
20
+
21
+ This should be called before invoking agent tools in virtual FS mode.
22
+ """
23
+ _tool_context.session_id = session_id
24
+
25
+
26
+ def get_tool_session_context() -> Optional[str]:
27
+ """Get the current session context for tool operations."""
28
+ return getattr(_tool_context, 'session_id', None)
29
+
30
+
31
+ def clear_tool_session_context() -> None:
32
+ """Clear the current session context."""
33
+ _tool_context.session_id = None
34
+
35
+
36
+ def _get_workspace_root_for_context() -> Any:
37
+ """Get the appropriate workspace root based on current context.
38
+
39
+ Returns VirtualFilesystem in virtual FS mode with session context,
40
+ otherwise returns the physical WORKSPACE_ROOT path.
41
+ """
42
+ # Import config dynamically to get current value (in case it changed)
43
+ from . import config as cfg
44
+ if cfg.VIRTUAL_FS:
45
+ session_id = get_tool_session_context()
46
+ if session_id:
47
+ from .virtual_fs import get_session_manager
48
+ fs = get_session_manager().get_filesystem(session_id)
49
+ if fs is not None:
50
+ return fs
51
+ return cfg.WORKSPACE_ROOT
52
+
53
+
12
54
  # =============================================================================
13
55
  # JUPYTER-LIKE CODE EXECUTION TOOLS
14
56
  # =============================================================================
@@ -22,20 +64,59 @@ class NotebookState:
22
64
  - A persistent namespace for variable state across cells
23
65
  - Execution history and outputs
24
66
  - Canvas items generated during cell execution
67
+
68
+ In virtual filesystem mode, file operations are restricted and virtual
69
+ filesystem helpers are provided instead.
25
70
  """
26
71
 
27
- def __init__(self):
72
+ def __init__(self, session_id: Optional[str] = None):
28
73
  self._cells: List[Dict[str, Any]] = []
29
74
  self._namespace: Dict[str, Any] = {}
30
75
  self._execution_count: int = 0
31
76
  self._ipython_shell = None
32
77
  self._canvas_items: List[Dict[str, Any]] = [] # Collected canvas items
78
+ self._session_id = session_id
33
79
  self._initialize_namespace()
34
80
 
35
81
  def _initialize_namespace(self):
36
82
  """Initialize the namespace with common imports and utilities."""
37
83
  # Pre-populate with commonly used modules
38
- init_code = """
84
+ # In virtual FS mode, we provide virtual filesystem helpers
85
+ if VIRTUAL_FS:
86
+ init_code = """
87
+ import sys
88
+ import json
89
+
90
+ # Data science essentials (imported if available)
91
+ try:
92
+ import pandas as pd
93
+ except (ImportError, AttributeError):
94
+ pd = None
95
+
96
+ try:
97
+ import numpy as np
98
+ except (ImportError, AttributeError):
99
+ np = None
100
+
101
+ try:
102
+ import matplotlib
103
+ matplotlib.use('Agg') # Non-interactive backend
104
+ import matplotlib.pyplot as plt
105
+ except (ImportError, AttributeError):
106
+ plt = None
107
+
108
+ try:
109
+ import plotly.express as px
110
+ import plotly.graph_objects as go
111
+ except (ImportError, AttributeError):
112
+ px = None
113
+ go = None
114
+
115
+ # Note: In virtual filesystem mode, standard file operations (open, os.listdir, etc.)
116
+ # are not available. Use the provided vfs_* functions instead.
117
+ """
118
+ else:
119
+ init_code = """
39
120
  import sys
40
121
  import os
41
122
  import json
@@ -44,26 +125,27 @@ from pathlib import Path
44
125
  # Data science essentials (imported if available)
45
126
  try:
46
127
  import pandas as pd
47
- except ImportError:
48
- pass
128
+ except (ImportError, AttributeError):
129
+ pd = None
49
130
 
50
131
  try:
51
132
  import numpy as np
52
- except ImportError:
53
- pass
133
+ except (ImportError, AttributeError):
134
+ np = None
54
135
 
55
136
  try:
56
137
  import matplotlib
57
138
  matplotlib.use('Agg') # Non-interactive backend
58
139
  import matplotlib.pyplot as plt
59
- except ImportError:
60
- pass
140
+ except (ImportError, AttributeError):
141
+ plt = None
61
142
 
62
143
  try:
63
144
  import plotly.express as px
64
145
  import plotly.graph_objects as go
65
- except ImportError:
66
- pass
146
+ except (ImportError, AttributeError):
147
+ px = None
148
+ go = None
67
149
  """
68
150
  # Execute initialization silently
69
151
  try:
@@ -71,6 +153,10 @@ except ImportError:
71
153
  except Exception:
72
154
  pass # Ignore import errors for optional packages
73
155
 
156
+ # In virtual FS mode, inject virtual filesystem helpers
157
+ if VIRTUAL_FS:
158
+ self._inject_virtual_fs_helpers()
159
+
74
160
  # Inject add_to_canvas function that captures items
75
161
  def _add_to_canvas_wrapper(content: Any) -> Dict[str, Any]:
76
162
  """Add content to the canvas for visualization.
@@ -79,7 +165,16 @@ except ImportError:
79
165
  PIL images, and markdown strings.
80
166
  """
81
167
  try:
82
- parsed = parse_canvas_object(content, workspace_root=WORKSPACE_ROOT)
168
+ # Use session's VirtualFilesystem in virtual FS mode, otherwise physical path
169
+ if VIRTUAL_FS and self._session_id:
170
+ from .virtual_fs import get_session_manager
171
+ workspace_root = get_session_manager().get_filesystem(self._session_id)
172
+ if workspace_root is None:
173
+ raise RuntimeError(f"Session {self._session_id} not found")
174
+ else:
175
+ workspace_root = WORKSPACE_ROOT
176
+
177
+ parsed = parse_canvas_object(content, workspace_root=workspace_root)
83
178
  self._canvas_items.append(parsed)
84
179
  return parsed
85
180
  except Exception as e:
@@ -93,6 +188,67 @@ except ImportError:
93
188
 
94
189
  self._namespace["add_to_canvas"] = _add_to_canvas_wrapper
95
190
 
191
+ def _inject_virtual_fs_helpers(self):
192
+ """Inject virtual filesystem helper functions into the namespace."""
193
+ from .virtual_fs import get_session_manager
194
+
195
+ session_id = self._session_id
196
+
197
+ def vfs_read_file(path: str) -> str:
198
+ """Read a text file from the virtual filesystem."""
199
+ if not session_id:
200
+ raise RuntimeError("No session ID available for virtual filesystem")
201
+ fs = get_session_manager().get_filesystem(session_id)
202
+ if not fs:
203
+ raise RuntimeError(f"Session {session_id} not found")
204
+ return fs.read_text(path)
205
+
206
+ def vfs_write_file(path: str, content: str) -> int:
207
+ """Write content to a file in the virtual filesystem."""
208
+ if not session_id:
209
+ raise RuntimeError("No session ID available for virtual filesystem")
210
+ fs = get_session_manager().get_filesystem(session_id)
211
+ if not fs:
212
+ raise RuntimeError(f"Session {session_id} not found")
213
+ return fs.write_text(path, content)
214
+
215
+ def vfs_list_dir(path: str = "/workspace") -> list:
216
+ """List files in a directory in the virtual filesystem."""
217
+ if not session_id:
218
+ raise RuntimeError("No session ID available for virtual filesystem")
219
+ fs = get_session_manager().get_filesystem(session_id)
220
+ if not fs:
221
+ raise RuntimeError(f"Session {session_id} not found")
222
+ return fs.listdir(path)
223
+
224
+ def vfs_exists(path: str) -> bool:
225
+ """Check if a file or directory exists in the virtual filesystem."""
226
+ if not session_id:
227
+ raise RuntimeError("No session ID available for virtual filesystem")
228
+ fs = get_session_manager().get_filesystem(session_id)
229
+ if not fs:
230
+ raise RuntimeError(f"Session {session_id} not found")
231
+ return fs.exists(path)
232
+
233
+ def vfs_mkdir(path: str, parents: bool = True) -> None:
234
+ """Create a directory in the virtual filesystem."""
235
+ if not session_id:
236
+ raise RuntimeError("No session ID available for virtual filesystem")
237
+ fs = get_session_manager().get_filesystem(session_id)
238
+ if not fs:
239
+ raise RuntimeError(f"Session {session_id} not found")
240
+ fs.mkdir(path, parents=parents, exist_ok=True)
241
+
242
+ # Inject the helpers
243
+ self._namespace["vfs_read_file"] = vfs_read_file
244
+ self._namespace["vfs_write_file"] = vfs_write_file
245
+ self._namespace["vfs_list_dir"] = vfs_list_dir
246
+ self._namespace["vfs_exists"] = vfs_exists
247
+ self._namespace["vfs_mkdir"] = vfs_mkdir
248
+
249
+ # Add a notice about virtual FS mode
250
+ self._namespace["__VFS_MODE__"] = True
251
+
96
252
  def _get_ipython(self):
97
253
  """Get or create an IPython InteractiveShell for enhanced execution."""
98
254
  if self._ipython_shell is None:
@@ -339,8 +495,25 @@ except ImportError:
339
495
  return {"status": "reset", "message": "Notebook state cleared"}
340
496
 
341
497
 
342
- # Global notebook state instance
498
+ # Global notebook state instance (for physical FS mode)
499
+ # In virtual FS mode, each session should have its own NotebookState
343
500
  _notebook_state = NotebookState()
501
+ _session_notebook_states: Dict[str, NotebookState] = {}
502
+
503
+
504
+ def get_notebook_state(session_id: Optional[str] = None) -> NotebookState:
505
+ """Get the notebook state for a session.
506
+
507
+ In virtual FS mode, returns a session-specific NotebookState.
508
+ In physical FS mode, returns the global shared NotebookState.
509
+ """
510
+ if not VIRTUAL_FS or not session_id:
511
+ return _notebook_state
512
+
513
+ if session_id not in _session_notebook_states:
514
+ _session_notebook_states[session_id] = NotebookState(session_id=session_id)
515
+
516
+ return _session_notebook_states[session_id]
344
517
 
345
518
 
346
519
  def create_cell(code: str, cell_type: str = "code") -> Dict[str, Any]:
@@ -625,10 +798,13 @@ def add_to_canvas(content: Any, title: Optional[str] = None, item_id: Optional[s
625
798
  add_to_canvas(new_fig, item_id="growth_chart") # Replaces the previous chart
626
799
  """
627
800
  try:
801
+ # Get appropriate workspace root (VirtualFilesystem in virtual FS mode)
802
+ workspace_root = _get_workspace_root_for_context()
803
+
628
804
  # Parse the content into canvas format with optional title and ID
629
805
  parsed = parse_canvas_object(
630
806
  content,
631
- workspace_root=WORKSPACE_ROOT,
807
+ workspace_root=workspace_root,
632
808
  title=title,
633
809
  item_id=item_id
634
810
  )
@@ -668,9 +844,12 @@ def update_canvas_item(item_id: str, content: Any, title: Optional[str] = None)
668
844
  update_canvas_item("progress_chart", final_fig, title="Final Results")
669
845
  """
670
846
  try:
847
+ # Get appropriate workspace root (VirtualFilesystem in virtual FS mode)
848
+ workspace_root = _get_workspace_root_for_context()
849
+
671
850
  parsed = parse_canvas_object(
672
851
  content,
673
- workspace_root=WORKSPACE_ROOT,
852
+ workspace_root=workspace_root,
674
853
  title=title,
675
854
  item_id=item_id
676
855
  )
@@ -719,6 +898,8 @@ def bash(command: str, timeout: int = 60) -> Dict[str, Any]:
719
898
  Runs the command in the workspace directory. Use this for file operations,
720
899
  git commands, installing packages, or any shell operations.
721
900
 
901
+ Note: This tool is disabled in virtual filesystem mode for security reasons.
902
+
722
903
  Args:
723
904
  command: The bash command to execute
724
905
  timeout: Maximum time in seconds to wait for the command (default: 60)
@@ -743,6 +924,16 @@ def bash(command: str, timeout: int = 60) -> Dict[str, Any]:
743
924
  # Run a script
744
925
  bash("python script.py")
745
926
  """
927
+ # Disable bash in virtual filesystem mode for security
928
+ if VIRTUAL_FS:
929
+ return {
930
+ "stdout": "",
931
+ "stderr": "Bash commands are disabled in virtual filesystem mode for security reasons. "
932
+ "Use the built-in file tools (read_file, write_file, list_directory) instead.",
933
+ "return_code": 1,
934
+ "status": "error"
935
+ }
936
+
746
937
  try:
747
938
  result = subprocess.run(
748
939
  command,