mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 00:49:37 +00:00
fix(skillgen): prevent monolith watch path injection
This commit is contained in:
1 parent
4000de1546
commit
a898e6890c
8 files changed
+229
-37
No files matched your search
+10
-2
@@ -90,6 +90,14 @@ fi
|
||||
# Write interpreter path for all subsequent steps (persists across invocations)
|
||||
mkdir -p graphify-out
|
||||
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
|
||||
# Save scan root so `graphify update` (no args) knows where to look next time.
|
||||
# INPUT_PATH is passed through a quoted heredoc, never substituted into the
|
||||
# command line itself: a bare `cd INPUT_PATH` (or an unquoted heredoc, which
|
||||
# still expands $()/backticks in its body) would let a malicious path execute
|
||||
# as shell code the moment this line runs.
|
||||
"$PYTHON" -c "import os, sys; out_path = os.path.abspath('graphify-out/.graphify_root'); os.chdir(sys.stdin.readline().rstrip('\n')); open(out_path, 'w', encoding='utf-8').write(os.getcwd())" <<'GRAPHIFY_ROOT_EOF'
|
||||
INPUT_PATH
|
||||
GRAPHIFY_ROOT_EOF
|
||||
```
|
||||
|
||||
If the import succeeds, print nothing and move straight to Step 2.
|
||||
@@ -1226,10 +1234,10 @@ Supported URL types (auto-detected):
|
||||
Start a background watcher that monitors a folder and auto-updates the graph when files change.
|
||||
|
||||
```bash
|
||||
python3 -m graphify.watch INPUT_PATH --debounce 3
|
||||
$(cat graphify-out/.graphify_python) -m graphify.watch "$(cat graphify-out/.graphify_root)" --debounce 3
|
||||
```
|
||||
|
||||
Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
|
||||
This watches the same folder graphify extracted, read from the trusted `graphify-out/.graphify_root` that Step 1 resolved - there is no path to substitute, so a scan root containing shell metacharacters can never be re-interpreted here. Behavior depends on what changed:
|
||||
|
||||
- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically.
|
||||
- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required).
|
||||
|
||||
+10
-2
@@ -103,6 +103,14 @@ mkdir -p graphify-out
|
||||
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
|
||||
# Force UTF-8 I/O on Windows (prevents garbled CJK/non-ASCII output)
|
||||
export PYTHONUTF8=1
|
||||
# Save scan root so `graphify update` (no args) knows where to look next time.
|
||||
# INPUT_PATH is passed through a quoted heredoc, never substituted into the
|
||||
# command line itself: a bare `cd INPUT_PATH` (or an unquoted heredoc, which
|
||||
# still expands $()/backticks in its body) would let a malicious path execute
|
||||
# as shell code the moment this line runs.
|
||||
"$PYTHON" -c "import os, sys; out_path = os.path.abspath('graphify-out/.graphify_root'); os.chdir(sys.stdin.readline().rstrip('\n')); open(out_path, 'w', encoding='utf-8').write(os.getcwd())" <<'GRAPHIFY_ROOT_EOF'
|
||||
INPUT_PATH
|
||||
GRAPHIFY_ROOT_EOF
|
||||
```
|
||||
|
||||
If the import succeeds, print nothing and move straight to Step 2.
|
||||
@@ -1358,10 +1366,10 @@ Supported URL types (auto-detected):
|
||||
Start a background watcher that monitors a folder and auto-updates the graph when files change.
|
||||
|
||||
```bash
|
||||
python3 -m graphify.watch INPUT_PATH --debounce 3
|
||||
$(cat graphify-out/.graphify_python) -m graphify.watch "$(cat graphify-out/.graphify_root)" --debounce 3
|
||||
```
|
||||
|
||||
Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
|
||||
This watches the same folder graphify extracted, read from the trusted `graphify-out/.graphify_root` that Step 1 resolved - there is no path to substitute, so a scan root containing shell metacharacters can never be re-interpreted here. Behavior depends on what changed:
|
||||
|
||||
- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically.
|
||||
- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required).
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
"""#3642: Step 1's INPUT_PATH substitution into a bash command is a command
|
||||
"""#3642, #3742, #3844: INPUT_PATH substitution into bash commands is a command
|
||||
injection vector.
|
||||
|
||||
INPUT_PATH is a literal placeholder in every generated skill file, meant to
|
||||
INPUT_PATH is a literal placeholder in generated skill files, meant to
|
||||
be substituted by the agent following the instructions with the resolved
|
||||
scan path before it runs the bash block. A malicious or merely
|
||||
untrusted-source path substituted into `echo "$(cd INPUT_PATH && pwd)" ...`
|
||||
executed as shell code the moment Step 1 ran, before any Python code was
|
||||
ever reached -- the outer double quotes do not stop `$()`/backtick
|
||||
expansion, and the bare `cd INPUT_PATH` was itself unquoted.
|
||||
scan path before it runs bash blocks. A malicious or merely
|
||||
untrusted-source path substituted into an unquoted shell command line
|
||||
executes as shell code the moment the line runs, before any Python code
|
||||
is reached.
|
||||
|
||||
These tests extract the actual Step 1 bash block from a committed,
|
||||
generated skill file, substitute INPUT_PATH the way an agent would, and
|
||||
execute it for real, so the fix is verified against the artifact users
|
||||
actually receive rather than against the fragment source alone.
|
||||
These tests extract the actual Step 1 and --watch bash blocks from committed,
|
||||
generated skill files (including the Aider and Devin monoliths), verify that
|
||||
the artifacts users actually receive are safe against hostile inputs, and
|
||||
execute Step 1 with hostile paths to prove no injected commands run.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -27,14 +26,28 @@ import pytest
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SKILL_MD = REPO_ROOT / "graphify" / "skill.md"
|
||||
|
||||
MONOLITH_SKILL_FILES = ("skill-aider.md", "skill-devin.md")
|
||||
ALL_TESTED_SKILL_FILES = ("skill.md", "skill-aider.md", "skill-devin.md")
|
||||
|
||||
def _extract_step1_bash_block() -> str:
|
||||
text = SKILL_MD.read_text(encoding="utf-8")
|
||||
|
||||
def _extract_step1_bash_block(filename: str = "skill.md") -> str:
|
||||
text = (REPO_ROOT / "graphify" / filename).read_text(encoding="utf-8")
|
||||
for match in re.finditer(r"```bash\n(.*?)\n```", text, re.DOTALL):
|
||||
block = match.group(1)
|
||||
if "graphify_root" in block:
|
||||
return block
|
||||
raise AssertionError("could not find the Step 1 bash block in skill.md")
|
||||
raise AssertionError(f"could not find the Step 1 bash block in {filename}")
|
||||
|
||||
|
||||
def _extract_watch_bash_block(filename: str) -> str:
|
||||
text = (REPO_ROOT / "graphify" / filename).read_text(encoding="utf-8")
|
||||
m = re.search(r"## For --watch.*?(```bash\n.*?\n```)", text, re.DOTALL)
|
||||
if not m:
|
||||
raise AssertionError(f"could not find the --watch section in {filename}")
|
||||
block = re.search(r"```bash\n(.*?)\n```", m.group(1), re.DOTALL)
|
||||
if not block:
|
||||
raise AssertionError(f"could not find the --watch bash block in {filename}")
|
||||
return block.group(1).strip()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -43,7 +56,22 @@ def step1_script() -> str:
|
||||
|
||||
|
||||
def _run_step1(script: str, input_path_value: str, cwd: Path) -> subprocess.CompletedProcess:
|
||||
substituted = script.replace("INPUT_PATH", input_path_value)
|
||||
if sys.platform == "win32" and len(input_path_value) > 2 and input_path_value[1] == ":":
|
||||
# Convert Windows absolute path (C:\...) to WSL mount path (/mnt/c/...)
|
||||
drive = input_path_value[0].lower()
|
||||
rest = input_path_value[2:].replace("\\", "/")
|
||||
input_path_value = f"/mnt/{drive}{rest}"
|
||||
substituted = script.replace("INPUT_PATH", input_path_value).replace("\r\n", "\n")
|
||||
if sys.platform == "win32":
|
||||
# On Windows, writing to a temp script file avoids CreateProcess quote-escaping
|
||||
# mangling double quotes inside `bash -c "..."`. Must use LF newlines for bash.
|
||||
script_file = cwd / "_run_step1.sh"
|
||||
script_file.write_text(substituted, encoding="utf-8", newline="\n")
|
||||
return subprocess.run(
|
||||
["bash", "_run_step1.sh"],
|
||||
cwd=cwd, capture_output=True, text=True,
|
||||
env={**os.environ, "PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
return subprocess.run(
|
||||
["bash", "-c", substituted],
|
||||
cwd=cwd, capture_output=True, text=True,
|
||||
@@ -51,7 +79,69 @@ def _run_step1(script: str, input_path_value: str, cwd: Path) -> subprocess.Comp
|
||||
)
|
||||
|
||||
|
||||
def test_step1_does_not_execute_a_command_substitution_in_input_path(tmp_path):
|
||||
# --- #3844: Monolith --watch contract tests ----------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("skill_file", MONOLITH_SKILL_FILES)
|
||||
def test_monolith_watch_does_not_contain_raw_input_path_placeholder(skill_file: str):
|
||||
"""The monolith --watch block must not interpolate raw INPUT_PATH into shell code."""
|
||||
block = _extract_watch_bash_block(skill_file)
|
||||
assert "INPUT_PATH" not in block, (
|
||||
f"{skill_file} --watch command still contains raw INPUT_PATH placeholder: {block!r}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("skill_file", MONOLITH_SKILL_FILES)
|
||||
def test_monolith_watch_uses_trusted_graphify_root(skill_file: str):
|
||||
"""The monolith --watch block must read from trusted .graphify_root and .graphify_python."""
|
||||
block = _extract_watch_bash_block(skill_file)
|
||||
assert "graphify-out/.graphify_root" in block, (
|
||||
f"{skill_file} --watch command does not reference graphify-out/.graphify_root"
|
||||
)
|
||||
assert "graphify-out/.graphify_python" in block, (
|
||||
f"{skill_file} --watch command does not reference graphify-out/.graphify_python"
|
||||
)
|
||||
assert block == (
|
||||
'$(cat graphify-out/.graphify_python) -m graphify.watch '
|
||||
'"$(cat graphify-out/.graphify_root)" --debounce 3'
|
||||
)
|
||||
|
||||
|
||||
# --- Step 1 hostile INPUT_PATH injection resistance --------------------------
|
||||
|
||||
|
||||
HOSTILE_PAYLOADS = [
|
||||
("cmd_subst", lambda s: f"$(touch {s})"),
|
||||
("backticks", lambda s: f"`touch {s}`"),
|
||||
("semicolon", lambda s: f"nonexistent; touch {s} #"),
|
||||
("and_chain", lambda s: f"nonexistent && touch {s}"),
|
||||
("pipe_chain", lambda s: f"nonexistent | touch {s}"),
|
||||
("newline", lambda s: f"nonexistent\ntouch {s}\n"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("skill_file", ALL_TESTED_SKILL_FILES)
|
||||
@pytest.mark.parametrize("attack_name,payload_fn", HOSTILE_PAYLOADS)
|
||||
def test_step1_does_not_execute_hostile_input_path(
|
||||
tmp_path: Path, skill_file: str, attack_name: str, payload_fn
|
||||
):
|
||||
"""A hostile INPUT_PATH containing shell metacharacters must never execute."""
|
||||
script = _extract_step1_bash_block(skill_file)
|
||||
sentinel = tmp_path / f"PWNED_{attack_name}_{skill_file.replace('.', '_')}"
|
||||
malicious = payload_fn(sentinel)
|
||||
|
||||
_run_step1(script, malicious, cwd=tmp_path)
|
||||
|
||||
assert not sentinel.exists(), (
|
||||
f"hostile {attack_name} inside substituted INPUT_PATH for {skill_file} "
|
||||
f"must never execute as shell code"
|
||||
)
|
||||
|
||||
|
||||
# --- Backwards-compatible legacy test entry points ---------------------------
|
||||
|
||||
|
||||
def test_step1_does_not_execute_a_command_substitution_in_input_path(tmp_path: Path):
|
||||
"""A malicious path containing $(...) must never run as shell code."""
|
||||
script = _extract_step1_bash_block()
|
||||
sentinel = tmp_path / "PWNED"
|
||||
@@ -65,7 +155,7 @@ def test_step1_does_not_execute_a_command_substitution_in_input_path(tmp_path):
|
||||
)
|
||||
|
||||
|
||||
def test_step1_does_not_execute_a_semicolon_separated_command_in_input_path(tmp_path):
|
||||
def test_step1_does_not_execute_a_semicolon_separated_command_in_input_path(tmp_path: Path):
|
||||
"""A malicious path using `;` to chain a second command must never run."""
|
||||
script = _extract_step1_bash_block()
|
||||
sentinel = tmp_path / "PWNED2"
|
||||
@@ -79,26 +169,40 @@ def test_step1_does_not_execute_a_semicolon_separated_command_in_input_path(tmp_
|
||||
)
|
||||
|
||||
|
||||
def test_step1_still_resolves_a_legitimate_path(tmp_path):
|
||||
"""The fix must not break the ordinary, non-malicious case."""
|
||||
script = _extract_step1_bash_block()
|
||||
project = tmp_path / "my project"
|
||||
# --- Legitimate path handling & non-existent path failure ---------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("skill_file", ALL_TESTED_SKILL_FILES)
|
||||
def test_step1_still_resolves_a_legitimate_path(tmp_path: Path, skill_file: str):
|
||||
"""The fix must not break ordinary paths, including paths with spaces."""
|
||||
script = _extract_step1_bash_block(skill_file)
|
||||
project = tmp_path / "my project with spaces"
|
||||
project.mkdir()
|
||||
|
||||
result = _run_step1(script, str(project), cwd=tmp_path)
|
||||
|
||||
marker = tmp_path / "graphify-out" / ".graphify_root"
|
||||
assert marker.exists(), (
|
||||
f"a legitimate path must still be resolved and written; "
|
||||
f"a legitimate path must still be resolved and written for {skill_file}; "
|
||||
f"stdout={result.stdout!r} stderr={result.stderr!r}"
|
||||
)
|
||||
assert marker.read_text(encoding="utf-8") == str(project.resolve())
|
||||
marker_content = marker.read_text(encoding="utf-8").strip()
|
||||
drive_letter = project.drive[0].lower()
|
||||
if sys.platform == "win32" and marker_content.startswith(f"/mnt/{drive_letter}/"):
|
||||
resolved_marker = Path(f"{drive_letter.upper()}:{marker_content[6:]}").resolve()
|
||||
elif sys.platform == "win32" and marker_content.startswith(f"/{drive_letter}/"):
|
||||
resolved_marker = Path(f"{drive_letter.upper()}:{marker_content[2:]}").resolve()
|
||||
else:
|
||||
resolved_marker = Path(marker_content).resolve()
|
||||
assert resolved_marker == project.resolve()
|
||||
assert marker_content.endswith("my project with spaces")
|
||||
|
||||
|
||||
def test_step1_still_fails_loudly_on_a_nonexistent_path(tmp_path):
|
||||
@pytest.mark.parametrize("skill_file", ALL_TESTED_SKILL_FILES)
|
||||
def test_step1_still_fails_loudly_on_a_nonexistent_path(tmp_path: Path, skill_file: str):
|
||||
"""A path that does not exist must still fail, matching the original
|
||||
`cd INPUT_PATH` behavior, not silently write a bogus marker."""
|
||||
script = _extract_step1_bash_block()
|
||||
script = _extract_step1_bash_block(skill_file)
|
||||
|
||||
result = _run_step1(script, "does/not/exist", cwd=tmp_path)
|
||||
|
||||
|
||||
@@ -90,6 +90,14 @@ fi
|
||||
# Write interpreter path for all subsequent steps (persists across invocations)
|
||||
mkdir -p graphify-out
|
||||
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
|
||||
# Save scan root so `graphify update` (no args) knows where to look next time.
|
||||
# INPUT_PATH is passed through a quoted heredoc, never substituted into the
|
||||
# command line itself: a bare `cd INPUT_PATH` (or an unquoted heredoc, which
|
||||
# still expands $()/backticks in its body) would let a malicious path execute
|
||||
# as shell code the moment this line runs.
|
||||
"$PYTHON" -c "import os, sys; out_path = os.path.abspath('graphify-out/.graphify_root'); os.chdir(sys.stdin.readline().rstrip('\n')); open(out_path, 'w', encoding='utf-8').write(os.getcwd())" <<'GRAPHIFY_ROOT_EOF'
|
||||
INPUT_PATH
|
||||
GRAPHIFY_ROOT_EOF
|
||||
```
|
||||
|
||||
If the import succeeds, print nothing and move straight to Step 2.
|
||||
@@ -1226,10 +1234,10 @@ Supported URL types (auto-detected):
|
||||
Start a background watcher that monitors a folder and auto-updates the graph when files change.
|
||||
|
||||
```bash
|
||||
python3 -m graphify.watch INPUT_PATH --debounce 3
|
||||
$(cat graphify-out/.graphify_python) -m graphify.watch "$(cat graphify-out/.graphify_root)" --debounce 3
|
||||
```
|
||||
|
||||
Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
|
||||
This watches the same folder graphify extracted, read from the trusted `graphify-out/.graphify_root` that Step 1 resolved - there is no path to substitute, so a scan root containing shell metacharacters can never be re-interpreted here. Behavior depends on what changed:
|
||||
|
||||
- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically.
|
||||
- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required).
|
||||
|
||||
@@ -103,6 +103,14 @@ mkdir -p graphify-out
|
||||
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
|
||||
# Force UTF-8 I/O on Windows (prevents garbled CJK/non-ASCII output)
|
||||
export PYTHONUTF8=1
|
||||
# Save scan root so `graphify update` (no args) knows where to look next time.
|
||||
# INPUT_PATH is passed through a quoted heredoc, never substituted into the
|
||||
# command line itself: a bare `cd INPUT_PATH` (or an unquoted heredoc, which
|
||||
# still expands $()/backticks in its body) would let a malicious path execute
|
||||
# as shell code the moment this line runs.
|
||||
"$PYTHON" -c "import os, sys; out_path = os.path.abspath('graphify-out/.graphify_root'); os.chdir(sys.stdin.readline().rstrip('\n')); open(out_path, 'w', encoding='utf-8').write(os.getcwd())" <<'GRAPHIFY_ROOT_EOF'
|
||||
INPUT_PATH
|
||||
GRAPHIFY_ROOT_EOF
|
||||
```
|
||||
|
||||
If the import succeeds, print nothing and move straight to Step 2.
|
||||
@@ -1358,10 +1366,10 @@ Supported URL types (auto-detected):
|
||||
Start a background watcher that monitors a folder and auto-updates the graph when files change.
|
||||
|
||||
```bash
|
||||
python3 -m graphify.watch INPUT_PATH --debounce 3
|
||||
$(cat graphify-out/.graphify_python) -m graphify.watch "$(cat graphify-out/.graphify_root)" --debounce 3
|
||||
```
|
||||
|
||||
Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
|
||||
This watches the same folder graphify extracted, read from the trusted `graphify-out/.graphify_root` that Step 1 resolved - there is no path to substitute, so a scan root containing shell metacharacters can never be re-interpreted here. Behavior depends on what changed:
|
||||
|
||||
- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically.
|
||||
- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required).
|
||||
|
||||
@@ -90,6 +90,14 @@ fi
|
||||
# Write interpreter path for all subsequent steps (persists across invocations)
|
||||
mkdir -p graphify-out
|
||||
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
|
||||
# Save scan root so `graphify update` (no args) knows where to look next time.
|
||||
# INPUT_PATH is passed through a quoted heredoc, never substituted into the
|
||||
# command line itself: a bare `cd INPUT_PATH` (or an unquoted heredoc, which
|
||||
# still expands $()/backticks in its body) would let a malicious path execute
|
||||
# as shell code the moment this line runs.
|
||||
"$PYTHON" -c "import os, sys; out_path = os.path.abspath('graphify-out/.graphify_root'); os.chdir(sys.stdin.readline().rstrip('\n')); open(out_path, 'w', encoding='utf-8').write(os.getcwd())" <<'GRAPHIFY_ROOT_EOF'
|
||||
INPUT_PATH
|
||||
GRAPHIFY_ROOT_EOF
|
||||
```
|
||||
|
||||
If the import succeeds, print nothing and move straight to Step 2.
|
||||
@@ -1226,10 +1234,10 @@ Supported URL types (auto-detected):
|
||||
Start a background watcher that monitors a folder and auto-updates the graph when files change.
|
||||
|
||||
```bash
|
||||
python3 -m graphify.watch INPUT_PATH --debounce 3
|
||||
$(cat graphify-out/.graphify_python) -m graphify.watch "$(cat graphify-out/.graphify_root)" --debounce 3
|
||||
```
|
||||
|
||||
Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
|
||||
This watches the same folder graphify extracted, read from the trusted `graphify-out/.graphify_root` that Step 1 resolved - there is no path to substitute, so a scan root containing shell metacharacters can never be re-interpreted here. Behavior depends on what changed:
|
||||
|
||||
- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically.
|
||||
- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required).
|
||||
|
||||
@@ -103,6 +103,14 @@ mkdir -p graphify-out
|
||||
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
|
||||
# Force UTF-8 I/O on Windows (prevents garbled CJK/non-ASCII output)
|
||||
export PYTHONUTF8=1
|
||||
# Save scan root so `graphify update` (no args) knows where to look next time.
|
||||
# INPUT_PATH is passed through a quoted heredoc, never substituted into the
|
||||
# command line itself: a bare `cd INPUT_PATH` (or an unquoted heredoc, which
|
||||
# still expands $()/backticks in its body) would let a malicious path execute
|
||||
# as shell code the moment this line runs.
|
||||
"$PYTHON" -c "import os, sys; out_path = os.path.abspath('graphify-out/.graphify_root'); os.chdir(sys.stdin.readline().rstrip('\n')); open(out_path, 'w', encoding='utf-8').write(os.getcwd())" <<'GRAPHIFY_ROOT_EOF'
|
||||
INPUT_PATH
|
||||
GRAPHIFY_ROOT_EOF
|
||||
```
|
||||
|
||||
If the import succeeds, print nothing and move straight to Step 2.
|
||||
@@ -1358,10 +1366,10 @@ Supported URL types (auto-detected):
|
||||
Start a background watcher that monitors a folder and auto-updates the graph when files change.
|
||||
|
||||
```bash
|
||||
python3 -m graphify.watch INPUT_PATH --debounce 3
|
||||
$(cat graphify-out/.graphify_python) -m graphify.watch "$(cat graphify-out/.graphify_root)" --debounce 3
|
||||
```
|
||||
|
||||
Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
|
||||
This watches the same folder graphify extracted, read from the trusted `graphify-out/.graphify_root` that Step 1 resolved - there is no path to substitute, so a scan root containing shell metacharacters can never be re-interpreted here. Behavior depends on what changed:
|
||||
|
||||
- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically.
|
||||
- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required).
|
||||
|
||||
@@ -1142,6 +1142,44 @@ def _is_community_label_export_fix_line(line: str) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _is_step1_root_marker_fix_line(line: str) -> bool:
|
||||
"""Whether a line is part of Step 1 writing .graphify_root via quoted heredoc (#3642/#3844).
|
||||
|
||||
The v8 monoliths did not persist .graphify_root in Step 1. The fix adds the
|
||||
safe quoted-heredoc scan-root writer to Step 1, persisting
|
||||
graphify-out/.graphify_root so that `graphify update` and `--watch` can consume
|
||||
the trusted resolved path.
|
||||
"""
|
||||
stripped = line.strip()
|
||||
return (
|
||||
stripped.startswith('"$PYTHON" -c "import os, sys; out_path = os.path.abspath(\'graphify-out/.graphify_root\')')
|
||||
or stripped in ("INPUT_PATH", "GRAPHIFY_ROOT_EOF")
|
||||
or stripped == "# Save scan root so `graphify update` (no args) knows where to look next time."
|
||||
or stripped == "# INPUT_PATH is passed through a quoted heredoc, never substituted into the"
|
||||
or stripped == "# command line itself: a bare `cd INPUT_PATH` (or an unquoted heredoc, which"
|
||||
or stripped == "# still expands $()/backticks in its body) would let a malicious path execute"
|
||||
or stripped == "# as shell code the moment this line runs."
|
||||
)
|
||||
|
||||
|
||||
def _is_watch_injection_fix_line(line: str) -> bool:
|
||||
"""Whether a line is part of the --watch INPUT_PATH injection fix (#3844).
|
||||
|
||||
The v8 monoliths passed the raw, agent-substituted INPUT_PATH placeholder
|
||||
unquoted to `graphify.watch` in bash (`python3 -m graphify.watch INPUT_PATH --debounce 3`),
|
||||
which executed command substitutions in hostile paths. The fix consumes the
|
||||
trusted `graphify-out/.graphify_root` resolved in Step 1 via $(cat ...).
|
||||
Both the removed v8 command/prose and the added trusted-root command/prose match here.
|
||||
"""
|
||||
stripped = line.strip()
|
||||
return (
|
||||
stripped == "python3 -m graphify.watch INPUT_PATH --debounce 3"
|
||||
or stripped == '$(cat graphify-out/.graphify_python) -m graphify.watch "$(cat graphify-out/.graphify_root)" --debounce 3'
|
||||
or stripped == "Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:"
|
||||
or stripped == "This watches the same folder graphify extracted, read from the trusted `graphify-out/.graphify_root` that Step 1 resolved - there is no path to substitute, so a scan root containing shell metacharacters can never be re-interpreted here. Behavior depends on what changed:"
|
||||
)
|
||||
|
||||
|
||||
# Every line that may differ between a rendered monolith and its pristine v8
|
||||
# baseline. Each predicate documents one sanctioned change-class; a blank line is
|
||||
# allowed because the multi-line fix blocks insert spacing. Anything else failing
|
||||
@@ -1163,6 +1201,8 @@ _SANCTIONED_MONOLITH_DIFFS = (
|
||||
_is_uv_from_interpreter_fix_line,
|
||||
_is_semantic_cache_scope_fix_line,
|
||||
_is_community_label_export_fix_line,
|
||||
_is_step1_root_marker_fix_line,
|
||||
_is_watch_injection_fix_line,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user