griptape-nodes 0.61.0__py3-none-any.whl → 0.62.0__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.
@@ -1,12 +1,11 @@
1
1
  """Events for project template management."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from dataclasses import dataclass
4
6
  from enum import StrEnum
5
- from pathlib import Path
6
- from typing import Any
7
+ from typing import TYPE_CHECKING, Any
7
8
 
8
- from griptape_nodes.common.macro_parser import MacroMatchFailure, MacroParseFailure, VariableInfo
9
- from griptape_nodes.common.project_templates import ProjectTemplate, ProjectValidationInfo
10
9
  from griptape_nodes.retained_mode.events.base_events import (
11
10
  RequestPayload,
12
11
  ResultPayloadFailure,
@@ -15,6 +14,13 @@ from griptape_nodes.retained_mode.events.base_events import (
15
14
  )
16
15
  from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
17
16
 
17
+ if TYPE_CHECKING:
18
+ from pathlib import Path
19
+
20
+ from griptape_nodes.common.macro_parser import MacroMatchFailure, ParsedMacro, VariableInfo
21
+ from griptape_nodes.common.project_templates import ProjectTemplate, ProjectValidationInfo, SituationTemplate
22
+ from griptape_nodes.retained_mode.managers.project_manager import ProjectID, ProjectInfo
23
+
18
24
  # Type alias for macro variable dictionaries (used by ParsedMacro)
19
25
  MacroVariables = dict[str, str | int]
20
26
 
@@ -49,12 +55,12 @@ class LoadProjectTemplateResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuc
49
55
  """Project template loaded successfully.
50
56
 
51
57
  Args:
52
- project_path: Path to the loaded project.yml
58
+ project_id: The identifier for the loaded project
53
59
  template: The merged ProjectTemplate (system defaults + user customizations)
54
60
  validation: Validation info with status and any problems encountered
55
61
  """
56
62
 
57
- project_path: Path
63
+ project_id: ProjectID
58
64
  template: ProjectTemplate
59
65
  validation: ProjectValidationInfo
60
66
 
@@ -65,28 +71,26 @@ class LoadProjectTemplateResultFailure(WorkflowNotAlteredMixin, ResultPayloadFai
65
71
  """Project template loading failed.
66
72
 
67
73
  Args:
68
- project_path: Path to the project.yml that failed to load
69
74
  validation: Validation info with error details
70
75
  """
71
76
 
72
- project_path: Path
73
77
  validation: ProjectValidationInfo
74
78
 
75
79
 
76
80
  @dataclass
77
81
  @PayloadRegistry.register
78
82
  class GetProjectTemplateRequest(RequestPayload):
79
- """Get cached project template for a workspace path.
83
+ """Get cached project template for a project ID.
80
84
 
81
85
  Use when: Querying current project configuration, checking validation status.
82
86
 
83
87
  Args:
84
- project_path: Path to the project.yml file
88
+ project_id: Identifier of the project
85
89
 
86
90
  Results: GetProjectTemplateResultSuccess | GetProjectTemplateResultFailure
87
91
  """
88
92
 
89
- project_path: Path
93
+ project_id: ProjectID
90
94
 
91
95
 
92
96
  @dataclass
@@ -109,40 +113,80 @@ class GetProjectTemplateResultFailure(WorkflowNotAlteredMixin, ResultPayloadFail
109
113
  """Project template retrieval failed (not loaded yet)."""
110
114
 
111
115
 
116
+ @dataclass
117
+ class ProjectTemplateInfo:
118
+ """Information about a loaded or failed project template."""
119
+
120
+ project_id: ProjectID
121
+ validation: ProjectValidationInfo
122
+
123
+
124
+ @dataclass
125
+ @PayloadRegistry.register
126
+ class ListProjectTemplatesRequest(RequestPayload):
127
+ """List all project templates that have been loaded or attempted to load.
128
+
129
+ Use when: Displaying available projects, checking which projects are loaded.
130
+
131
+ Args:
132
+ include_system_builtins: Whether to include system builtin templates like SYSTEM_DEFAULTS_KEY
133
+
134
+ Results: ListProjectTemplatesResultSuccess
135
+ """
136
+
137
+ include_system_builtins: bool = False
138
+
139
+
140
+ @dataclass
141
+ @PayloadRegistry.register
142
+ class ListProjectTemplatesResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
143
+ """List of all project templates retrieved.
144
+
145
+ Args:
146
+ successfully_loaded: List of templates that loaded successfully
147
+ failed_to_load: List of templates that failed to load with validation errors
148
+ """
149
+
150
+ successfully_loaded: list[ProjectTemplateInfo]
151
+ failed_to_load: list[ProjectTemplateInfo]
152
+
153
+
112
154
  @dataclass
113
155
  @PayloadRegistry.register
114
- class GetMacroForSituationRequest(RequestPayload):
115
- """Get the macro schema for a specific situation.
156
+ class GetSituationRequest(RequestPayload):
157
+ """Get the full situation template for a specific situation.
158
+
159
+ Returns the complete SituationTemplate including macro and policy.
116
160
 
117
- Use when: Need to know what variables a situation requires, or get schema for custom resolution.
161
+ Use when: Need situation macro and/or policy for file operations.
162
+ Uses the current project for context.
118
163
 
119
164
  Args:
120
- project_path: Path to the project.yml to use
121
165
  situation_name: Name of the situation template (e.g., "save_node_output")
122
166
 
123
- Results: GetMacroForSituationResultSuccess | GetMacroForSituationResultFailure
167
+ Results: GetSituationResultSuccess | GetSituationResultFailure
124
168
  """
125
169
 
126
- project_path: Path
127
170
  situation_name: str
128
171
 
129
172
 
130
173
  @dataclass
131
174
  @PayloadRegistry.register
132
- class GetMacroForSituationResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
133
- """Situation macro retrieved successfully.
175
+ class GetSituationResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
176
+ """Situation template retrieved successfully.
134
177
 
135
178
  Args:
136
- macro_schema: The macro template string (e.g., "{inputs}/{file_name}.{file_ext}")
179
+ situation: The complete situation template including macro and policy.
180
+ Access via situation.macro, situation.policy.create_dirs, etc.
137
181
  """
138
182
 
139
- macro_schema: str
183
+ situation: SituationTemplate
140
184
 
141
185
 
142
186
  @dataclass
143
187
  @PayloadRegistry.register
144
- class GetMacroForSituationResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
145
- """Situation macro retrieval failed (situation not found or template not loaded)."""
188
+ class GetSituationResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
189
+ """Situation template retrieval failed (situation not found or template not loaded)."""
146
190
 
147
191
 
148
192
  @dataclass
@@ -152,16 +196,17 @@ class GetPathForMacroRequest(RequestPayload):
152
196
 
153
197
  Use when: Resolving paths, saving files. Works with any macro string, not tied to situations.
154
198
 
199
+ Uses the current project for context. Caller must parse the macro string
200
+ into a ParsedMacro before creating this request.
201
+
155
202
  Args:
156
- project_path: Path to the project.yml (used for directory resolution)
157
- macro_schema: The macro template string to resolve (e.g., "{inputs}/{file_name}.{file_ext}")
203
+ parsed_macro: The parsed macro to resolve
158
204
  variables: Variable values for macro substitution (e.g., {"file_name": "output", "file_ext": "png"})
159
205
 
160
206
  Results: GetPathForMacroResultSuccess | GetPathForMacroResultFailure
161
207
  """
162
208
 
163
- project_path: Path
164
- macro_schema: str
209
+ parsed_macro: ParsedMacro
165
210
  variables: MacroVariables
166
211
 
167
212
 
@@ -171,10 +216,12 @@ class GetPathForMacroResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess
171
216
  """Path resolved successfully from macro.
172
217
 
173
218
  Args:
174
- resolved_path: The final Path after macro substitution
219
+ resolved_path: The relative project path after macro substitution (e.g., "outputs/file.png")
220
+ absolute_path: The absolute filesystem path (e.g., "/workspace/outputs/file.png")
175
221
  """
176
222
 
177
223
  resolved_path: Path
224
+ absolute_path: Path
178
225
 
179
226
 
180
227
  @dataclass
@@ -186,29 +233,27 @@ class GetPathForMacroResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure
186
233
  failure_reason: Specific reason for failure
187
234
  missing_variables: List of required variable names that were not provided (for MISSING_REQUIRED_VARIABLES)
188
235
  conflicting_variables: List of variables that conflict with directory names (for DIRECTORY_OVERRIDE_ATTEMPTED)
189
- error_details: Additional error details from ParsedMacro (for MACRO_RESOLUTION_ERROR)
190
236
  """
191
237
 
192
238
  failure_reason: PathResolutionFailureReason
193
- missing_variables: list[str] | None = None
194
- conflicting_variables: list[str] | None = None
195
- error_details: str | None = None
239
+ missing_variables: set[str] | None = None
240
+ conflicting_variables: set[str] | None = None
196
241
 
197
242
 
198
243
  @dataclass
199
244
  @PayloadRegistry.register
200
245
  class SetCurrentProjectRequest(RequestPayload):
201
- """Set which project.yml user has currently selected.
246
+ """Set which project user has currently selected.
202
247
 
203
248
  Use when: User switches between projects, opens a new workspace.
204
249
 
205
250
  Args:
206
- project_path: Path to the project.yml to set as current (None to clear)
251
+ project_id: Identifier of the project to set as current (None to clear)
207
252
 
208
253
  Results: SetCurrentProjectResultSuccess
209
254
  """
210
255
 
211
- project_path: Path | None
256
+ project_id: ProjectID | None
212
257
 
213
258
 
214
259
  @dataclass
@@ -224,7 +269,7 @@ class GetCurrentProjectRequest(RequestPayload):
224
269
 
225
270
  Use when: Need to know which project user is working with.
226
271
 
227
- Results: GetCurrentProjectResultSuccess
272
+ Results: GetCurrentProjectResultSuccess | GetCurrentProjectResultFailure
228
273
  """
229
274
 
230
275
 
@@ -234,10 +279,16 @@ class GetCurrentProjectResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSucce
234
279
  """Current project retrieved.
235
280
 
236
281
  Args:
237
- project_path: The currently selected project path ("No Project" if None)
282
+ project_info: Complete information about the current project
238
283
  """
239
284
 
240
- project_path: Path | None
285
+ project_info: ProjectInfo
286
+
287
+
288
+ @dataclass
289
+ @PayloadRegistry.register
290
+ class GetCurrentProjectResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
291
+ """No current project is set."""
241
292
 
242
293
 
243
294
  @dataclass
@@ -261,13 +312,7 @@ class SaveProjectTemplateRequest(RequestPayload):
261
312
  @dataclass
262
313
  @PayloadRegistry.register
263
314
  class SaveProjectTemplateResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
264
- """Project template saved successfully.
265
-
266
- Args:
267
- project_path: Path where project.yml was saved
268
- """
269
-
270
- project_path: Path
315
+ """Project template saved successfully."""
271
316
 
272
317
 
273
318
  @dataclass
@@ -281,122 +326,192 @@ class SaveProjectTemplateResultFailure(WorkflowNotAlteredMixin, ResultPayloadFai
281
326
  - Disk full
282
327
  """
283
328
 
284
- project_path: Path
285
-
286
329
 
287
330
  @dataclass
288
331
  @PayloadRegistry.register
289
- class MatchPathAgainstMacroRequest(RequestPayload):
290
- """Check if a path matches a macro schema and extract variables.
332
+ class AttemptMatchPathAgainstMacroRequest(RequestPayload):
333
+ """Attempt to match a path against a macro schema and extract variables.
291
334
 
292
335
  Use when: Validating paths, extracting info from file paths,
293
336
  identifying which schema produced a file.
294
337
 
338
+ Uses the current project for context. Caller must parse the macro string
339
+ into a ParsedMacro before creating this request.
340
+
341
+ Pattern non-matches are returned as success with match_failure populated.
342
+ Only true system errors (missing SecretsManager, etc.) return failure.
343
+
295
344
  Args:
296
- project_path: Path to project.yml (for directory resolution)
297
- macro_schema: Macro template string
298
- file_path: Path to test
345
+ parsed_macro: Parsed macro template to match against
346
+ file_path: Path string to test
299
347
  known_variables: Variables we already know
300
348
 
301
- Results: MatchPathAgainstMacroResultSuccess | MatchPathAgainstMacroResultFailure
349
+ Results: AttemptMatchPathAgainstMacroResultSuccess | AttemptMatchPathAgainstMacroResultFailure
302
350
  """
303
351
 
304
- project_path: Path
305
- macro_schema: str
352
+ parsed_macro: ParsedMacro
306
353
  file_path: str
307
354
  known_variables: MacroVariables
308
355
 
309
356
 
310
357
  @dataclass
311
358
  @PayloadRegistry.register
312
- class MatchPathAgainstMacroResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
313
- """Path matched the macro schema."""
359
+ class AttemptMatchPathAgainstMacroResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
360
+ """Attempt completed (match succeeded or pattern didn't match).
314
361
 
315
- extracted_variables: MacroVariables
362
+ Check match_failure to determine outcome:
363
+ - match_failure is None: Pattern matched, extracted_variables contains results
364
+ - match_failure is not None: Pattern didn't match (normal case, not an error)
365
+ """
366
+
367
+ extracted_variables: MacroVariables | None
368
+ match_failure: MacroMatchFailure | None
316
369
 
317
370
 
318
371
  @dataclass
319
372
  @PayloadRegistry.register
320
- class MatchPathAgainstMacroResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
321
- """Path did not match the macro schema."""
322
-
323
- match_failure: MacroMatchFailure
373
+ class AttemptMatchPathAgainstMacroResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
374
+ """System error occurred (missing SecretsManager, invalid configuration, etc.)."""
324
375
 
325
376
 
326
377
  @dataclass
327
378
  @PayloadRegistry.register
328
- class GetVariablesForMacroRequest(RequestPayload):
329
- """Get list of all variables in a macro schema.
379
+ class GetStateForMacroRequest(RequestPayload):
380
+ """Analyze a macro and return comprehensive state information.
330
381
 
331
- Use when: Building UI forms, showing what variables a schema needs,
332
- validating before resolution.
382
+ Use when: Building UI forms, real-time validation, checking if resolution
383
+ would succeed before actually resolving.
384
+
385
+ Uses the current project for context. Caller must parse the macro string
386
+ into a ParsedMacro before creating this request.
333
387
 
334
388
  Args:
335
- macro_schema: Macro template string to inspect
389
+ parsed_macro: The parsed macro to analyze
390
+ variables: Currently provided variable values
336
391
 
337
- Results: GetVariablesForMacroResultSuccess | GetVariablesForMacroResultFailure
392
+ Results: GetStateForMacroResultSuccess | GetStateForMacroResultFailure
338
393
  """
339
394
 
340
- macro_schema: str
395
+ parsed_macro: ParsedMacro
396
+ variables: MacroVariables
341
397
 
342
398
 
343
399
  @dataclass
344
400
  @PayloadRegistry.register
345
- class GetVariablesForMacroResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
346
- """Variables found in the macro schema."""
401
+ class GetStateForMacroResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
402
+ """Macro state analysis completed successfully.
403
+
404
+ Args:
405
+ all_variables: All variables found in the macro
406
+ satisfied_variables: Variables that have values (from user, directories, or builtins)
407
+ missing_required_variables: Required variables that are missing values
408
+ conflicting_variables: Variables that conflict (e.g., user overriding builtin with different value)
409
+ can_resolve: Whether the macro can be fully resolved (no missing required vars, no conflicts)
410
+ """
347
411
 
348
- variables: list[VariableInfo]
412
+ all_variables: set[VariableInfo]
413
+ satisfied_variables: set[str]
414
+ missing_required_variables: set[str]
415
+ conflicting_variables: set[str]
416
+ can_resolve: bool
349
417
 
350
418
 
351
419
  @dataclass
352
420
  @PayloadRegistry.register
353
- class GetVariablesForMacroResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
354
- """Failed to parse macro schema."""
421
+ class GetStateForMacroResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
422
+ """Macro state analysis failed.
355
423
 
356
- parse_failure: MacroParseFailure
424
+ Failure occurs when:
425
+ - No current project is set
426
+ - Current project template is not loaded
427
+ - A builtin variable cannot be resolved (RuntimeError or NotImplementedError)
428
+ """
357
429
 
358
430
 
359
431
  @dataclass
360
432
  @PayloadRegistry.register
361
- class ValidateMacroSyntaxRequest(RequestPayload):
362
- """Validate a macro schema string for syntax errors.
433
+ class AttemptMapAbsolutePathToProjectRequest(RequestPayload):
434
+ """Find out if an absolute path exists anywhere within a Project directory.
435
+
436
+ Use when: User selects or types an absolute path via FilePicker and you need to know:
437
+ 1. Is this path inside any project directory?
438
+ 2. If yes, what's the macro form (e.g., {outputs}/file.png)?
363
439
 
364
- Use when: User editing schemas in UI, before saving templates,
365
- real-time validation.
440
+ This enables automatic conversion of absolute paths to portable macro form for workflow portability.
441
+
442
+ Uses longest prefix matching to find the most specific directory match.
443
+ Returns Success with mapped_path if inside project, or Success with None if outside.
444
+ Returns Failure if operation cannot be performed (no project loaded, secrets unavailable).
366
445
 
367
446
  Args:
368
- macro_schema: Schema string to validate
447
+ absolute_path: The absolute filesystem path to check
448
+
449
+ Results: AttemptMapAbsolutePathToProjectResultSuccess | AttemptMapAbsolutePathToProjectResultFailure
369
450
 
370
- Results: ValidateMacroSyntaxResultSuccess | ValidateMacroSyntaxResultFailure
451
+ Examples:
452
+ Path inside project directory:
453
+ Request: absolute_path = /Users/james/project/outputs/renders/image.png
454
+ Result: mapped_path = "{outputs}/renders/image.png"
455
+
456
+ Path outside project:
457
+ Request: absolute_path = /Users/james/Downloads/image.png
458
+ Result: mapped_path = None
459
+
460
+ Path at directory root:
461
+ Request: absolute_path = /Users/james/project/outputs
462
+ Result: mapped_path = "{outputs}"
371
463
  """
372
464
 
373
- macro_schema: str
465
+ absolute_path: Path
374
466
 
375
467
 
376
468
  @dataclass
377
469
  @PayloadRegistry.register
378
- class ValidateMacroSyntaxResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
379
- """Syntax is valid."""
470
+ class AttemptMapAbsolutePathToProjectResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
471
+ """Path check completed successfully.
472
+
473
+ Success means the check was performed (not necessarily that a match was found).
474
+ - mapped_path is NOT None: Path is inside a project directory (macro form returned)
475
+ - mapped_path is None: Path is outside all project directories (valid answer)
476
+
477
+ Args:
478
+ mapped_path: The macro form if path is inside a project directory (e.g., "{outputs}/file.png"),
479
+ or None if path is outside all project directories
480
+
481
+ Examples:
482
+ Path inside project:
483
+ mapped_path = "{outputs}/renders/image.png"
484
+ result_details = "Successfully mapped absolute path to '{outputs}/renders/image.png'"
380
485
 
381
- variables: list[VariableInfo]
382
- warnings: list[str]
486
+ Path outside project:
487
+ mapped_path = None
488
+ result_details = "Attempted to map absolute path '/Users/james/Downloads/image.png'. Path is outside all project directories"
489
+ """
490
+
491
+ mapped_path: str | None
383
492
 
384
493
 
385
494
  @dataclass
386
495
  @PayloadRegistry.register
387
- class ValidateMacroSyntaxResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
388
- """Syntax is invalid."""
496
+ class AttemptMapAbsolutePathToProjectResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
497
+ """Path mapping attempt failed.
389
498
 
390
- parse_failure: MacroParseFailure
391
- partial_variables: list[VariableInfo]
499
+ Returned when the operation cannot be performed (no current project, secrets manager unavailable).
500
+ This is distinct from "path is outside project" which returns Success with None values.
501
+
502
+ Examples:
503
+ No current project:
504
+ result_details = "Attempted to map absolute path. Failed because no current project is set"
505
+
506
+ Secrets manager unavailable:
507
+ result_details = "Attempted to map absolute path. Failed because SecretsManager not available"
508
+ """
392
509
 
393
510
 
394
511
  @dataclass
395
512
  @PayloadRegistry.register
396
513
  class GetAllSituationsForProjectRequest(RequestPayload):
397
- """Get all situation names and schemas from a project template."""
398
-
399
- project_path: Path
514
+ """Get all situation names and schemas from current project template."""
400
515
 
401
516
 
402
517
  @dataclass
@@ -155,7 +155,49 @@ class OSManager:
155
155
  """
156
156
  # Expand environment variables first, then tilde
157
157
  expanded_vars = os.path.expandvars(path_str)
158
- return Path(expanded_vars).expanduser().resolve()
158
+ return self.resolve_path_safely(Path(expanded_vars).expanduser())
159
+
160
+ def resolve_path_safely(self, path: Path) -> Path:
161
+ """Resolve a path consistently across platforms.
162
+
163
+ Unlike Path.resolve() which behaves differently on Windows vs Unix
164
+ for non-existent paths, this method provides consistent behavior:
165
+ - Converts relative paths to absolute (using CWD as base)
166
+ - Normalizes path separators and removes . and ..
167
+ - Does NOT resolve symlinks if path doesn't exist
168
+ - Does NOT change path based on CWD for absolute paths
169
+
170
+ Use this instead of .resolve() when:
171
+ - Path might not exist (file creation, validation, user input)
172
+ - You need consistent cross-platform comparison
173
+ - You're about to create the file/directory
174
+
175
+ Use .resolve() when:
176
+ - Path definitely exists and you need symlink resolution
177
+ - You're checking actual file locations
178
+
179
+ Args:
180
+ path: Path to resolve (relative or absolute, existing or not)
181
+
182
+ Returns:
183
+ Absolute, normalized Path object
184
+
185
+ Examples:
186
+ # Relative path
187
+ resolve_path_safely(Path("relative/file.txt"))
188
+ → Path("/current/dir/relative/file.txt")
189
+
190
+ # Absolute non-existent path (Windows safe)
191
+ resolve_path_safely(Path("/abs/nonexistent/path"))
192
+ → Path("/abs/nonexistent/path") # NOT resolved relative to CWD
193
+ """
194
+ # Convert to absolute if relative
195
+ if not path.is_absolute():
196
+ path = Path.cwd() / path
197
+
198
+ # Normalize (remove . and .., collapse slashes) without resolving symlinks
199
+ # This works consistently even for non-existent paths on Windows
200
+ return Path(os.path.normpath(path))
159
201
 
160
202
  def _resolve_file_path(self, path_str: str, *, workspace_only: bool = False) -> Path:
161
203
  """Resolve a file path, handling absolute, relative, and tilde paths.
@@ -172,7 +214,7 @@ class OSManager:
172
214
  # Expand tilde and environment variables for absolute paths or paths starting with ~
173
215
  return self._expand_path(path_str)
174
216
  # Both workspace and system-wide modes resolve relative to current directory
175
- return (self._get_workspace_path() / path_str).resolve()
217
+ return self.resolve_path_safely(self._get_workspace_path() / path_str)
176
218
  except (ValueError, RuntimeError):
177
219
  if workspace_only:
178
220
  msg = f"Path '{path_str}' not found, using workspace directory: {self._get_workspace_path()}"
@@ -193,8 +235,11 @@ class OSManager:
193
235
  workspace = GriptapeNodes.ConfigManager().workspace_path
194
236
 
195
237
  # Ensure both paths are resolved for comparison
238
+ # Both path and workspace should use .resolve() to follow symlinks consistently
239
+ # (e.g., /var -> /private/var on macOS). Even if path doesn't exist yet,
240
+ # .resolve() will resolve parent directories and symlinks in the path.
196
241
  path = path.resolve()
197
- workspace = workspace.resolve()
242
+ workspace = workspace.resolve() # Workspace should always exist
198
243
 
199
244
  msg = f"Validating path: {path} against workspace: {workspace}"
200
245
  logger.debug(msg)
@@ -217,6 +262,9 @@ class OSManager:
217
262
  need the \\?\ prefix to work correctly. This method transparently adds
218
263
  the prefix when needed on Windows.
219
264
 
265
+ Note: This method assumes the path exists or will exist. For non-existent
266
+ paths that need cross-platform normalization, use resolve_path_safely() first.
267
+
220
268
  Args:
221
269
  path: Path object to convert to string
222
270
 
@@ -443,7 +491,7 @@ class OSManager:
443
491
  directory = self._expand_path(request.directory_path)
444
492
  else:
445
493
  # Both workspace and system-wide modes resolve relative to current directory
446
- directory = (self._get_workspace_path() / request.directory_path).resolve()
494
+ directory = self.resolve_path_safely(self._get_workspace_path() / request.directory_path)
447
495
 
448
496
  # Check if directory exists
449
497
  if not directory.exists():
@@ -1056,9 +1104,9 @@ class OSManager:
1056
1104
 
1057
1105
  # Resolve path - if absolute, use as-is; if relative, align to workspace
1058
1106
  if is_absolute:
1059
- file_path = Path(full_path_str).resolve()
1107
+ file_path = self.resolve_path_safely(Path(full_path_str))
1060
1108
  else:
1061
- file_path = (self._get_workspace_path() / full_path_str).resolve()
1109
+ file_path = self.resolve_path_safely(self._get_workspace_path() / full_path_str)
1062
1110
 
1063
1111
  # Check if it already exists - warn but treat as success
1064
1112
  if file_path.exists():