Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .claude/hooks/safety-guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,22 @@ def check_file_safety(path: str) -> tuple[bool, str]:
if is_safe_path(path):
return True, ""

# Check dangerous patterns
path_lower = path.lower()
# Check dangerous patterns against the basename only, not the full path.
# Matching the full path causes false positives when a directory name contains
# security-related words (e.g. "secrets-injector", "stackland-secrets-webhook"):
# a legitimate file like "values.yaml" inside such a directory would be blocked
# even though the file itself is not a credential file. The patterns are designed
# to catch files with dangerous *names* — anchoring to the basename is correct.
basename_lower = os.path.basename(path).lower()
if DANGEROUS_FILE_PATTERNS == _DEFAULT_DANGEROUS_FILE_PATTERNS and not any(
marker in path_lower for marker in _DEFAULT_DANGEROUS_FILE_MARKERS
marker in basename_lower for marker in _DEFAULT_DANGEROUS_FILE_MARKERS
):
return True, ""

import re

for pattern in DANGEROUS_FILE_PATTERNS:
if re.search(pattern, path_lower, re.IGNORECASE):
if re.search(pattern, basename_lower, re.IGNORECASE):
return (
False,
f"Blocked: Access to sensitive file pattern '{pattern}' in path: {path}",
Expand Down
29 changes: 26 additions & 3 deletions .map/scripts/map_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2230,12 +2230,29 @@ def set_waves(branch: str, blueprint_path: Optional[str] = None) -> dict:
from mapify_cli.dependency_graph import DependencyGraph, SubtaskNode
except ImportError:
# When running as a standalone script, dependency_graph.py may not be
# importable from sys.path. Walk upward and look for src/mapify_cli/.
# importable from sys.path. Try in order:
# 1. Source-checkout layout: src/mapify_cli/ relative to this file or cwd.
# 2. Installed-package layout: uv tool install / pipx venv locations
# (~/.local/share/uv/tools/mapify-cli/... or
# ~/.local/pipx/venvs/mapify-cli/...).
import importlib.util

dg_candidates = [Path("src/mapify_cli/dependency_graph.py")]
dg_candidates: list[Path] = [Path("src/mapify_cli/dependency_graph.py")]
for parent in Path(__file__).resolve().parents:
dg_candidates.append(parent / "src" / "mapify_cli" / "dependency_graph.py")

# Common installed-package locations (uv tool install, pipx install).
_home = Path.home()
for _tool_dir in [
_home / ".local" / "share" / "uv" / "tools" / "mapify-cli",
_home / ".local" / "pipx" / "venvs" / "mapify-cli",
]:
if _tool_dir.exists():
for _py_dir in sorted(_tool_dir.glob("lib/python3.*"), reverse=True)[:1]:
dg_candidates.append(
_py_dir / "site-packages" / "mapify_cli" / "dependency_graph.py"
)

loaded = False
for candidate in dg_candidates:
if candidate.exists():
Expand All @@ -2252,7 +2269,13 @@ def set_waves(branch: str, blueprint_path: Optional[str] = None) -> dict:
if not loaded:
return {
"status": "error",
"message": "Cannot import dependency_graph module",
"message": (
"Cannot import dependency_graph module. "
"If mapify-cli was installed via 'uv tool install', invoke this "
"script with the uv-tool Python interpreter directly: "
"~/.local/share/uv/tools/mapify-cli/bin/python3 "
".map/scripts/map_orchestrator.py ..."
),
}

if blueprint_path is None:
Expand Down
35 changes: 31 additions & 4 deletions .map/scripts/map_step_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2797,18 +2797,22 @@ def parse_requirements_index(spec_text: str) -> dict[str, object]:
(open) and ``<!-- /mapify:requirements-index:v1 -->`` (close). Only the
fenced ```yaml block between those two sentinels is authoritative; a
sentinel-shaped string anywhere else in the prose is ignored.
- Parses the inner YAML via a lazy ``import yaml``; both ``ImportError`` and
``yaml.YAMLError`` yield ``status='malformed'`` — never an uncaught exception.
- Parses the inner YAML via a lazy ``import yaml``. ``ImportError`` (PyYAML not
installed) yields ``status='pyyaml_missing'`` with an honest message; genuine
``yaml.YAMLError`` yields ``status='malformed'`` — never an uncaught exception.
- Returns::

{
'requirements': [{'id': str, 'kind': str}, ...],
'status': 'absent' | 'malformed' | 'present_empty' | 'present_nonempty',
'status': 'absent' | 'pyyaml_missing' | 'malformed' | 'present_empty' | 'present_nonempty',
'warnings': [str, ...],
}

Status semantics:
- ``absent`` — sentinel pair not found in the text.
- ``pyyaml_missing`` — open sentinel found, YAML block present, but PyYAML is
not installed in the running Python environment. This is
an environment problem, NOT a spec formatting problem.
- ``malformed`` — open sentinel found but: no close sentinel, no inner
yaml fence, YAML parse error, or top-level shape is not
``{requirements: list}``.
Expand Down Expand Up @@ -2854,9 +2858,23 @@ def parse_requirements_index(spec_text: str) -> dict[str, object]:

yaml_text = after_fence[:fence_close]

# Step 4: parse YAML (lazy import; treat ImportError == YAMLError == malformed).
# Step 4: parse YAML (lazy import).
# ImportError means PyYAML is not installed — an environment problem, not a spec
# formatting problem. Return a distinct status so callers can surface an honest
# "install pyyaml" message instead of the misleading "Requirements Index is malformed".
try:
import yaml # noqa: PLC0415
except ImportError:
return {
"requirements": [],
"status": "pyyaml_missing",
"warnings": warnings
+ [
"PyYAML is not installed; cannot parse Requirements Index. "
"Run: pip install pyyaml"
],
}
try:
data = yaml.safe_load(yaml_text)
except Exception:
return {"requirements": [], "status": "malformed", "warnings": warnings}
Expand Down Expand Up @@ -3497,6 +3515,15 @@ def _constraint_body(c: dict) -> str:
errors.append(_fc_msg)
else:
warnings.append(_fc_msg)
elif _fc_status == "pyyaml_missing":
# PyYAML not installed — environment problem, not a spec formatting problem.
_fc_confidence = "low"
_fc_basis = "PyYAML not installed; cannot parse Requirements Index"
errors.append(
"Forward-coverage: Cannot validate Requirements Index — "
"PyYAML is not installed in this Python environment. "
"Run: pip install pyyaml"
)
elif _fc_status == "malformed":
# HC-5 / HC-3: malformed is always a hard error regardless of strict flag.
_fc_confidence = "low"
Expand Down
13 changes: 9 additions & 4 deletions src/mapify_cli/templates/hooks/safety-guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,22 @@ def check_file_safety(path: str) -> tuple[bool, str]:
if is_safe_path(path):
return True, ""

# Check dangerous patterns
path_lower = path.lower()
# Check dangerous patterns against the basename only, not the full path.
# Matching the full path causes false positives when a directory name contains
# security-related words (e.g. "secrets-injector", "stackland-secrets-webhook"):
# a legitimate file like "values.yaml" inside such a directory would be blocked
# even though the file itself is not a credential file. The patterns are designed
# to catch files with dangerous *names* — anchoring to the basename is correct.
basename_lower = os.path.basename(path).lower()
if DANGEROUS_FILE_PATTERNS == _DEFAULT_DANGEROUS_FILE_PATTERNS and not any(
marker in path_lower for marker in _DEFAULT_DANGEROUS_FILE_MARKERS
marker in basename_lower for marker in _DEFAULT_DANGEROUS_FILE_MARKERS
):
return True, ""

import re

for pattern in DANGEROUS_FILE_PATTERNS:
if re.search(pattern, path_lower, re.IGNORECASE):
if re.search(pattern, basename_lower, re.IGNORECASE):
return (
False,
f"Blocked: Access to sensitive file pattern '{pattern}' in path: {path}",
Expand Down
29 changes: 26 additions & 3 deletions src/mapify_cli/templates/map/scripts/map_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2230,12 +2230,29 @@ def set_waves(branch: str, blueprint_path: Optional[str] = None) -> dict:
from mapify_cli.dependency_graph import DependencyGraph, SubtaskNode
except ImportError:
# When running as a standalone script, dependency_graph.py may not be
# importable from sys.path. Walk upward and look for src/mapify_cli/.
# importable from sys.path. Try in order:
# 1. Source-checkout layout: src/mapify_cli/ relative to this file or cwd.
# 2. Installed-package layout: uv tool install / pipx venv locations
# (~/.local/share/uv/tools/mapify-cli/... or
# ~/.local/pipx/venvs/mapify-cli/...).
import importlib.util

dg_candidates = [Path("src/mapify_cli/dependency_graph.py")]
dg_candidates: list[Path] = [Path("src/mapify_cli/dependency_graph.py")]
for parent in Path(__file__).resolve().parents:
dg_candidates.append(parent / "src" / "mapify_cli" / "dependency_graph.py")

# Common installed-package locations (uv tool install, pipx install).
_home = Path.home()
for _tool_dir in [
_home / ".local" / "share" / "uv" / "tools" / "mapify-cli",
_home / ".local" / "pipx" / "venvs" / "mapify-cli",
]:
if _tool_dir.exists():
for _py_dir in sorted(_tool_dir.glob("lib/python3.*"), reverse=True)[:1]:
dg_candidates.append(
_py_dir / "site-packages" / "mapify_cli" / "dependency_graph.py"
)

loaded = False
for candidate in dg_candidates:
if candidate.exists():
Expand All @@ -2252,7 +2269,13 @@ def set_waves(branch: str, blueprint_path: Optional[str] = None) -> dict:
if not loaded:
return {
"status": "error",
"message": "Cannot import dependency_graph module",
"message": (
"Cannot import dependency_graph module. "
"If mapify-cli was installed via 'uv tool install', invoke this "
"script with the uv-tool Python interpreter directly: "
"~/.local/share/uv/tools/mapify-cli/bin/python3 "
".map/scripts/map_orchestrator.py ..."
),
}

if blueprint_path is None:
Expand Down
35 changes: 31 additions & 4 deletions src/mapify_cli/templates/map/scripts/map_step_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2797,18 +2797,22 @@ def parse_requirements_index(spec_text: str) -> dict[str, object]:
(open) and ``<!-- /mapify:requirements-index:v1 -->`` (close). Only the
fenced ```yaml block between those two sentinels is authoritative; a
sentinel-shaped string anywhere else in the prose is ignored.
- Parses the inner YAML via a lazy ``import yaml``; both ``ImportError`` and
``yaml.YAMLError`` yield ``status='malformed'`` — never an uncaught exception.
- Parses the inner YAML via a lazy ``import yaml``. ``ImportError`` (PyYAML not
installed) yields ``status='pyyaml_missing'`` with an honest message; genuine
``yaml.YAMLError`` yields ``status='malformed'`` — never an uncaught exception.
- Returns::

{
'requirements': [{'id': str, 'kind': str}, ...],
'status': 'absent' | 'malformed' | 'present_empty' | 'present_nonempty',
'status': 'absent' | 'pyyaml_missing' | 'malformed' | 'present_empty' | 'present_nonempty',
'warnings': [str, ...],
}

Status semantics:
- ``absent`` — sentinel pair not found in the text.
- ``pyyaml_missing`` — open sentinel found, YAML block present, but PyYAML is
not installed in the running Python environment. This is
an environment problem, NOT a spec formatting problem.
- ``malformed`` — open sentinel found but: no close sentinel, no inner
yaml fence, YAML parse error, or top-level shape is not
``{requirements: list}``.
Expand Down Expand Up @@ -2854,9 +2858,23 @@ def parse_requirements_index(spec_text: str) -> dict[str, object]:

yaml_text = after_fence[:fence_close]

# Step 4: parse YAML (lazy import; treat ImportError == YAMLError == malformed).
# Step 4: parse YAML (lazy import).
# ImportError means PyYAML is not installed — an environment problem, not a spec
# formatting problem. Return a distinct status so callers can surface an honest
# "install pyyaml" message instead of the misleading "Requirements Index is malformed".
try:
import yaml # noqa: PLC0415
except ImportError:
return {
"requirements": [],
"status": "pyyaml_missing",
"warnings": warnings
+ [
"PyYAML is not installed; cannot parse Requirements Index. "
"Run: pip install pyyaml"
],
}
try:
data = yaml.safe_load(yaml_text)
except Exception:
return {"requirements": [], "status": "malformed", "warnings": warnings}
Expand Down Expand Up @@ -3497,6 +3515,15 @@ def _constraint_body(c: dict) -> str:
errors.append(_fc_msg)
else:
warnings.append(_fc_msg)
elif _fc_status == "pyyaml_missing":
# PyYAML not installed — environment problem, not a spec formatting problem.
_fc_confidence = "low"
_fc_basis = "PyYAML not installed; cannot parse Requirements Index"
errors.append(
"Forward-coverage: Cannot validate Requirements Index — "
"PyYAML is not installed in this Python environment. "
"Run: pip install pyyaml"
)
elif _fc_status == "malformed":
# HC-5 / HC-3: malformed is always a hard error regardless of strict flag.
_fc_confidence = "low"
Expand Down
13 changes: 9 additions & 4 deletions src/mapify_cli/templates_src/hooks/safety-guardrails.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,22 @@ def check_file_safety(path: str) -> tuple[bool, str]:
if is_safe_path(path):
return True, ""

# Check dangerous patterns
path_lower = path.lower()
# Check dangerous patterns against the basename only, not the full path.
# Matching the full path causes false positives when a directory name contains
# security-related words (e.g. "secrets-injector", "stackland-secrets-webhook"):
# a legitimate file like "values.yaml" inside such a directory would be blocked
# even though the file itself is not a credential file. The patterns are designed
# to catch files with dangerous *names* — anchoring to the basename is correct.
basename_lower = os.path.basename(path).lower()
if DANGEROUS_FILE_PATTERNS == _DEFAULT_DANGEROUS_FILE_PATTERNS and not any(
marker in path_lower for marker in _DEFAULT_DANGEROUS_FILE_MARKERS
marker in basename_lower for marker in _DEFAULT_DANGEROUS_FILE_MARKERS
):
return True, ""

import re

for pattern in DANGEROUS_FILE_PATTERNS:
if re.search(pattern, path_lower, re.IGNORECASE):
if re.search(pattern, basename_lower, re.IGNORECASE):
return (
False,
f"Blocked: Access to sensitive file pattern '{pattern}' in path: {path}",
Expand Down
29 changes: 26 additions & 3 deletions src/mapify_cli/templates_src/map/scripts/map_orchestrator.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2230,12 +2230,29 @@ def set_waves(branch: str, blueprint_path: Optional[str] = None) -> dict:
from mapify_cli.dependency_graph import DependencyGraph, SubtaskNode
except ImportError:
# When running as a standalone script, dependency_graph.py may not be
# importable from sys.path. Walk upward and look for src/mapify_cli/.
# importable from sys.path. Try in order:
# 1. Source-checkout layout: src/mapify_cli/ relative to this file or cwd.
# 2. Installed-package layout: uv tool install / pipx venv locations
# (~/.local/share/uv/tools/mapify-cli/... or
# ~/.local/pipx/venvs/mapify-cli/...).
import importlib.util

dg_candidates = [Path("src/mapify_cli/dependency_graph.py")]
dg_candidates: list[Path] = [Path("src/mapify_cli/dependency_graph.py")]
for parent in Path(__file__).resolve().parents:
dg_candidates.append(parent / "src" / "mapify_cli" / "dependency_graph.py")

# Common installed-package locations (uv tool install, pipx install).
_home = Path.home()
for _tool_dir in [
_home / ".local" / "share" / "uv" / "tools" / "mapify-cli",
_home / ".local" / "pipx" / "venvs" / "mapify-cli",
]:
if _tool_dir.exists():
for _py_dir in sorted(_tool_dir.glob("lib/python3.*"), reverse=True)[:1]:
dg_candidates.append(
_py_dir / "site-packages" / "mapify_cli" / "dependency_graph.py"
)

loaded = False
for candidate in dg_candidates:
if candidate.exists():
Expand All @@ -2252,7 +2269,13 @@ def set_waves(branch: str, blueprint_path: Optional[str] = None) -> dict:
if not loaded:
return {
"status": "error",
"message": "Cannot import dependency_graph module",
"message": (
"Cannot import dependency_graph module. "
"If mapify-cli was installed via 'uv tool install', invoke this "
"script with the uv-tool Python interpreter directly: "
"~/.local/share/uv/tools/mapify-cli/bin/python3 "
".map/scripts/map_orchestrator.py ..."
),
}

if blueprint_path is None:
Expand Down
Loading
Loading