Pre-flight validation with distinct error codes: * *errorCode 0* -- notebook file (redirect to NotebookEdit) * *errorCode 2* -- file not read yet (or only partially read) * *errorCode 3* -- file modified since last read
(tool_input: dict[str, Any], context: ToolContext)
| 58 | # --------------------------------------------------------------------------- |
| 59 | |
| 60 | def _validate_input(tool_input: dict[str, Any], context: ToolContext) -> ValidationResult: |
| 61 | """Pre-flight validation with distinct error codes: |
| 62 | |
| 63 | * *errorCode 0* -- notebook file (redirect to NotebookEdit) |
| 64 | * *errorCode 2* -- file not read yet (or only partially read) |
| 65 | * *errorCode 3* -- file modified since last read |
| 66 | """ |
| 67 | file_path = tool_input.get("file_path") |
| 68 | if not isinstance(file_path, str): |
| 69 | return ValidationResult.fail("file_path must be a string") |
| 70 | |
| 71 | content = tool_input.get("content") |
| 72 | if not isinstance(content, str): |
| 73 | return ValidationResult.fail("content must be a string") |
| 74 | |
| 75 | # Reject .ipynb files -- redirect to NotebookEdit |
| 76 | if file_path.lower().endswith(".ipynb"): |
| 77 | return ValidationResult.fail( |
| 78 | "Cannot write to Jupyter notebook (.ipynb) files with the Write tool. " |
| 79 | "Use the NotebookEdit tool instead to modify notebook cells.", |
| 80 | error_code=0, |
| 81 | ) |
| 82 | |
| 83 | # Resolve the path for filesystem checks. Auto-memory paths are |
| 84 | # outside the workspace allowlist so ensure_allowed_path raises; |
| 85 | # short-circuit to the expanded path so the staleness check below |
| 86 | # still runs (otherwise auto-memory writes would silently bypass |
| 87 | # the "read before write" invariant). |
| 88 | if _is_auto_memory_write(file_path): |
| 89 | path = Path(_expand_path(file_path)) |
| 90 | else: |
| 91 | try: |
| 92 | path = context.ensure_allowed_path(file_path) |
| 93 | except ToolPermissionError: |
| 94 | # Permission errors are handled later by check_permissions / call |
| 95 | return ValidationResult.ok() |
| 96 | |
| 97 | if not path.exists(): |
| 98 | # New file -- no staleness concern |
| 99 | return ValidationResult.ok() |
| 100 | |
| 101 | status = context.file_read_status(path) |
| 102 | if status == "not_read" or status == "partial": |
| 103 | return ValidationResult.fail( |
| 104 | "File has not been read yet. Read it first before writing to it.", |
| 105 | error_code=2, |
| 106 | ) |
| 107 | if status == "modified": |
| 108 | return ValidationResult.fail( |
| 109 | "File has been modified since read, either by the user or by a linter. " |
| 110 | "Read it again before attempting to write it.", |
| 111 | error_code=3, |
| 112 | ) |
| 113 | return ValidationResult.ok() |
| 114 | |
| 115 | |
| 116 | # --------------------------------------------------------------------------- |