Source code for yoker.tools.mkdir
"""Mkdir tool implementation for Yoker.
Provides the MkdirTool for creating directories with guardrail validation,
recursive parent creation, and graceful handling of existing directories.
"""
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any
from yoker.logging import get_logger
from yoker.tools.base import Tool, ToolResult
if TYPE_CHECKING:
from yoker.tools.guardrails import Guardrail
log = get_logger(__name__)
[docs]
class MkdirTool(Tool):
"""Tool for creating directories.
Creates a directory at the given path with optional recursive parent
creation. When a guardrail is provided, validates parameters before
creating. Resolves paths with realpath and rejects symlinks by default.
Returns a structured result indicating success, creation status, and
the resolved path for debugging.
"""
def __init__(self, guardrail: "Guardrail | None" = None) -> None:
"""Initialize MkdirTool with optional guardrail.
Args:
guardrail: Optional guardrail for parameter validation.
"""
super().__init__(guardrail=guardrail)
@property
def name(self) -> str:
return "mkdir"
@property
def description(self) -> str:
return "Create a directory at the given path"
[docs]
def get_schema(self) -> dict[str, Any]:
"""Return Ollama-compatible schema for the mkdir tool.
Returns:
Dict with 'type': 'function' and function metadata.
"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory to create",
},
"recursive": {
"type": "boolean",
"description": "If true, create parent directories as needed (like mkdir -p). Defaults to false.",
},
},
"required": ["path"],
},
},
}
[docs]
async def execute(self, **kwargs: Any) -> ToolResult:
"""Create a directory.
Steps:
1. Validate parameters via guardrail if provided.
2. Extract and validate path parameter.
3. Reject symlinks before resolving.
4. Resolve the path with os.path.realpath().
5. Check if path already exists (file or directory).
6. Create directory, optionally with parents.
7. Return structured result with creation status.
Args:
**kwargs: Must contain 'path' key.
May contain 'recursive' (default False).
Returns:
ToolResult with creation result or error message.
"""
path_str = kwargs.get("path", "")
recursive = bool(kwargs.get("recursive", False))
# Defense-in-depth: validate via guardrail if provided
if self._guardrail is not None:
validation = self._guardrail.validate(self.name, kwargs)
if not validation.valid:
log.info(
"mkdir_guardrail_blocked",
path=path_str,
reason=validation.reason,
)
return ToolResult(
success=False,
result="",
error=validation.reason,
)
# Validate path parameter type
if not isinstance(path_str, str):
log.warning("mkdir_invalid_path_type", path_type=type(path_str).__name__)
return ToolResult(
success=False,
result="",
error="Invalid path parameter",
)
# Validate path is not empty
if not path_str.strip():
log.warning("mkdir_empty_path")
return ToolResult(
success=False,
result="",
error="Parameter 'path' cannot be empty",
)
# Reject symlinks before resolving to prevent traversal via symlinks
original_path = Path(path_str)
if original_path.is_symlink():
log.warning("mkdir_symlink_rejected", path=path_str)
return ToolResult(
success=False,
result="",
error="Path not accessible",
)
# Resolve the path to normalize
try:
resolved = Path(os.path.realpath(path_str))
except (OSError, ValueError):
log.warning("mkdir_invalid_path", path=path_str)
return ToolResult(
success=False,
result="",
error="Invalid path",
)
# Check what exists at the path
try:
if resolved.exists():
if resolved.is_file():
log.warning("mkdir_path_is_file", path=str(resolved))
return ToolResult(
success=False,
result="",
error="Path not accessible",
)
elif resolved.is_dir():
# Directory already exists - idempotent success
log.info(
"mkdir_already_exists",
path=str(resolved),
recursive=recursive,
)
return ToolResult(
success=True,
result={
"created": False,
"path": str(resolved),
"message": "Directory already exists",
},
)
except PermissionError:
log.warning("mkdir_permission_denied_check", path=str(resolved))
return ToolResult(
success=False,
result="",
error="Permission denied",
)
# Check parent exists for non-recursive mode
parent = resolved.parent
if not recursive and not parent.exists():
log.info("mkdir_parent_missing", path=str(resolved))
return ToolResult(
success=False,
result="",
error="Parent directory does not exist",
)
# Create directory
try:
if recursive:
resolved.mkdir(parents=True, exist_ok=True)
log.info("mkdir_created_recursive", path=str(resolved))
else:
resolved.mkdir(parents=False, exist_ok=False)
log.info("mkdir_created", path=str(resolved))
return ToolResult(
success=True,
result={
"created": True,
"path": str(resolved),
},
)
except PermissionError:
log.warning("mkdir_permission_denied", path=str(resolved))
return ToolResult(
success=False,
result="",
error="Permission denied",
)
except ValueError as e:
# Windows raises ValueError for paths with null bytes
log.warning("mkdir_invalid_path", path=str(resolved), error=str(e))
return ToolResult(
success=False,
result="",
error="Invalid path",
)
except OSError as e:
log.error("mkdir_os_error", path=str(resolved), error=str(e))
return ToolResult(
success=False,
result="",
error="Error creating directory",
)