ostruct-cli 0.8.8__py3-none-any.whl → 1.0.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.
- ostruct/cli/__init__.py +3 -15
- ostruct/cli/attachment_processor.py +455 -0
- ostruct/cli/attachment_template_bridge.py +973 -0
- ostruct/cli/cli.py +187 -33
- ostruct/cli/click_options.py +775 -692
- ostruct/cli/code_interpreter.py +195 -12
- ostruct/cli/commands/__init__.py +0 -3
- ostruct/cli/commands/run.py +289 -62
- ostruct/cli/config.py +23 -22
- ostruct/cli/constants.py +89 -0
- ostruct/cli/errors.py +191 -6
- ostruct/cli/explicit_file_processor.py +0 -15
- ostruct/cli/file_info.py +118 -14
- ostruct/cli/file_list.py +82 -1
- ostruct/cli/file_search.py +68 -2
- ostruct/cli/help_json.py +235 -0
- ostruct/cli/mcp_integration.py +13 -16
- ostruct/cli/params.py +217 -0
- ostruct/cli/plan_assembly.py +335 -0
- ostruct/cli/plan_printing.py +385 -0
- ostruct/cli/progress_reporting.py +8 -56
- ostruct/cli/quick_ref_help.py +128 -0
- ostruct/cli/rich_config.py +299 -0
- ostruct/cli/runner.py +397 -190
- ostruct/cli/security/__init__.py +2 -0
- ostruct/cli/security/allowed_checker.py +41 -0
- ostruct/cli/security/normalization.py +13 -9
- ostruct/cli/security/security_manager.py +558 -17
- ostruct/cli/security/types.py +15 -0
- ostruct/cli/template_debug.py +283 -261
- ostruct/cli/template_debug_help.py +233 -142
- ostruct/cli/template_env.py +46 -5
- ostruct/cli/template_filters.py +415 -8
- ostruct/cli/template_processor.py +240 -619
- ostruct/cli/template_rendering.py +49 -73
- ostruct/cli/template_validation.py +2 -1
- ostruct/cli/token_validation.py +35 -15
- ostruct/cli/types.py +15 -19
- ostruct/cli/unicode_compat.py +283 -0
- ostruct/cli/upload_manager.py +448 -0
- ostruct/cli/utils.py +30 -0
- ostruct/cli/validators.py +272 -54
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/METADATA +292 -126
- ostruct_cli-1.0.0.dist-info/RECORD +80 -0
- ostruct/cli/commands/quick_ref.py +0 -54
- ostruct/cli/template_optimizer.py +0 -478
- ostruct_cli-0.8.8.dist-info/RECORD +0 -71
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/WHEEL +0 -0
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/security/__init__.py
CHANGED
@@ -19,6 +19,7 @@ from .errors import (
|
|
19
19
|
from .normalization import normalize_path
|
20
20
|
from .safe_joiner import safe_join
|
21
21
|
from .security_manager import SecurityManager
|
22
|
+
from .types import PathSecurity
|
22
23
|
|
23
24
|
__all__ = [
|
24
25
|
"normalize_path",
|
@@ -29,4 +30,5 @@ __all__ = [
|
|
29
30
|
"DirectoryNotFoundError",
|
30
31
|
"SecurityErrorReasons",
|
31
32
|
"SecurityManager",
|
33
|
+
"PathSecurity",
|
32
34
|
]
|
@@ -4,11 +4,14 @@ This module provides functionality to verify that a given path is within
|
|
4
4
|
one of a set of allowed directories.
|
5
5
|
"""
|
6
6
|
|
7
|
+
import logging
|
7
8
|
from pathlib import Path
|
8
9
|
from typing import List, Union
|
9
10
|
|
10
11
|
from .normalization import normalize_path
|
11
12
|
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
12
15
|
|
13
16
|
def is_path_in_allowed_dirs(
|
14
17
|
path: Union[str, Path], allowed_dirs: List[Path]
|
@@ -17,6 +20,8 @@ def is_path_in_allowed_dirs(
|
|
17
20
|
|
18
21
|
This function normalizes both the input path and allowed directories
|
19
22
|
before comparison to ensure consistent results across platforms.
|
23
|
+
It also handles symlink resolution to ensure that resolved paths
|
24
|
+
can be compared correctly with allowed directories.
|
20
25
|
|
21
26
|
Args:
|
22
27
|
path: The path to check.
|
@@ -43,6 +48,7 @@ def is_path_in_allowed_dirs(
|
|
43
48
|
norm_path = normalize_path(path)
|
44
49
|
norm_allowed = [normalize_path(d) for d in allowed_dirs]
|
45
50
|
|
51
|
+
# Check with normalized paths first
|
46
52
|
for allowed in norm_allowed:
|
47
53
|
try:
|
48
54
|
# If path.relative_to(allowed) does not raise an error,
|
@@ -52,4 +58,39 @@ def is_path_in_allowed_dirs(
|
|
52
58
|
except ValueError:
|
53
59
|
continue
|
54
60
|
|
61
|
+
# If normalized comparison failed, try with resolved paths
|
62
|
+
# This handles the case where the input path is already resolved (e.g., from validate_file_access)
|
63
|
+
# but the allowed directories are not resolved
|
64
|
+
try:
|
65
|
+
# Check if the input path might already be resolved by comparing it with its own resolution
|
66
|
+
path_as_path = Path(path) if isinstance(path, str) else path
|
67
|
+
|
68
|
+
# If the input path is already resolved, resolve allowed dirs to match
|
69
|
+
# We determine this by checking if the path starts with common temp resolved prefixes
|
70
|
+
path_str = str(path_as_path)
|
71
|
+
if path_str.startswith("/var/folders/") or path_str.startswith(
|
72
|
+
"/private/"
|
73
|
+
):
|
74
|
+
# This looks like an already-resolved path on macOS, resolve allowed dirs
|
75
|
+
resolved_allowed = [d.resolve() for d in norm_allowed]
|
76
|
+
for allowed in resolved_allowed:
|
77
|
+
try:
|
78
|
+
path_as_path.relative_to(allowed)
|
79
|
+
return True
|
80
|
+
except ValueError:
|
81
|
+
continue
|
82
|
+
else:
|
83
|
+
# Try resolving both path and allowed dirs
|
84
|
+
resolved_path = norm_path.resolve()
|
85
|
+
resolved_allowed = [d.resolve() for d in norm_allowed]
|
86
|
+
for allowed in resolved_allowed:
|
87
|
+
try:
|
88
|
+
resolved_path.relative_to(allowed)
|
89
|
+
return True
|
90
|
+
except ValueError:
|
91
|
+
continue
|
92
|
+
except (OSError, RuntimeError):
|
93
|
+
# If resolution fails, fall back to normalized comparison result
|
94
|
+
pass
|
95
|
+
|
55
96
|
return False
|
@@ -68,7 +68,9 @@ _BACKSLASH_PATTERN = re.compile(r"\\")
|
|
68
68
|
_MULTIPLE_SLASH_PATTERN = re.compile(r"/+")
|
69
69
|
|
70
70
|
|
71
|
-
def normalize_path(
|
71
|
+
def normalize_path(
|
72
|
+
path: Union[str, Path], allow_traversal: bool = False
|
73
|
+
) -> Path:
|
72
74
|
"""Normalize a path string with security checks.
|
73
75
|
|
74
76
|
This function:
|
@@ -94,6 +96,7 @@ def normalize_path(path: Union[str, Path]) -> Path:
|
|
94
96
|
|
95
97
|
Args:
|
96
98
|
path: A string or Path object representing a file path.
|
99
|
+
allow_traversal: If True, allow ".." components in paths (for explicitly allowed files).
|
97
100
|
|
98
101
|
Returns:
|
99
102
|
A pathlib.Path object for the normalized absolute path.
|
@@ -132,14 +135,15 @@ def normalize_path(path: Union[str, Path]) -> Path:
|
|
132
135
|
if match := _UNICODE_SAFETY_PATTERN.search(normalized):
|
133
136
|
matched_text = match.group(0)
|
134
137
|
if ".." in matched_text:
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
138
|
+
if not allow_traversal:
|
139
|
+
raise PathSecurityError(
|
140
|
+
"Directory traversal not allowed",
|
141
|
+
path=path_str,
|
142
|
+
context={
|
143
|
+
"reason": SecurityErrorReasons.PATH_TRAVERSAL,
|
144
|
+
"matched": matched_text,
|
145
|
+
},
|
146
|
+
)
|
143
147
|
else:
|
144
148
|
raise PathSecurityError(
|
145
149
|
"Path contains unsafe characters",
|