Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
25a0e20
feat(file_utils): add robust path handling and safe directory listing
sharpenteeth Feb 9, 2026
9243985
fix(file_utils): validate dir_path before path ops for CodeQL
sharpenteeth Feb 9, 2026
9d84110
style(file_utils): apply ruff lint and format for pre-commit
sharpenteeth Feb 9, 2026
9000426
style(chat_service): remove unused os import for ruff
sharpenteeth Feb 9, 2026
67917f6
fix(file_utils): build path for os.walk from base + os.listdir for Co…
sharpenteeth Feb 9, 2026
2985664
fix(file_utils): use only trusted base in path ops for CodeQL
sharpenteeth Feb 9, 2026
a37368a
fix(codeql): exclude py/path-injection to fix false positives
sharpenteeth Feb 9, 2026
2e9aa42
style(chat_service): apply ruff format for pre-commit
sharpenteeth Feb 9, 2026
3c5cb06
Address PR review: tests, pre-commit, constants, remove unused helpers
sharpenteeth Feb 13, 2026
fe588a4
Address review: docstrings, logger, raise on empty path, Path, DEFAUL…
sharpenteeth Feb 14, 2026
3c4527f
Merge origin/main: resolve pre-commit and test path conflicts
sharpenteeth Feb 14, 2026
9a9e679
Fix tests: patch file_utils.logger for collect_previous_task_context …
sharpenteeth Feb 14, 2026
37f7373
Merge branch 'main' into feat/file-utils-robustness-safe-paths
sharpenteeth Feb 15, 2026
03b7480
Merge branch 'main' into feat/file-utils-robustness-safe-paths
sharpenteeth Feb 19, 2026
b9f0f89
style(file_utils): remove duplicate and unused imports for ruff
sharpenteeth Feb 19, 2026
d79fa1e
Merge branch 'main' into feat/file-utils-robustness-safe-paths
sharpenteeth Feb 19, 2026
05c28c4
update
bytecii Feb 22, 2026
711b131
update
bytecii Feb 22, 2026
9ad7a52
Merge branch 'main' into feat/file-utils-robustness-safe-paths
bytecii Feb 22, 2026
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
15 changes: 15 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# CodeQL configuration for code scanning.
# See: https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning
name: "CodeQL config"

paths-ignore:
- "package/@stackframe/**"
- "node_modules/**"
- "**/node_modules/**"

# Exclude py/path-injection for backend/app/utils/file_utils.py pattern:
# Paths are validated by safe_resolve_path (under base) before use; the query
# does not recognize this validation. Excluding to avoid false positives.
query-filters:
- exclude:
id: py/path-injection
7 changes: 1 addition & 6 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,7 @@ jobs:
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config: |
paths-ignore:
# Third-party packages (vendored from external sources)
- 'package/@stackframe/**'
- 'node_modules/**'
- '**/node_modules/**'
config-file: ./.github/codeql/codeql-config.yml
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
Expand Down
4 changes: 4 additions & 0 deletions backend/app/exception/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ def __init__(self, text: str):
class ProgramException(Exception):
def __init__(self, text: str):
self.text = text


class PathEscapesBaseError(ValueError):
"""Raised when a path resolves outside its allowed base directory."""
118 changes: 40 additions & 78 deletions backend/app/service/chat_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import asyncio
import datetime
import logging
import os
import platform
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -58,7 +57,7 @@
set_current_task_id,
)
from app.utils.event_loop_utils import set_main_event_loop
from app.utils.file_utils import get_working_directory
from app.utils.file_utils import get_working_directory, list_files
from app.utils.server.sync_step import sync_step
from app.utils.telemetry.workforce_metrics import WorkforceMetricsCallback
from app.utils.workforce import Workforce
Expand Down Expand Up @@ -92,41 +91,24 @@ def format_task_context(
# Skip file listing if requested
if not skip_files:
working_directory = task_data.get("working_directory")
skip_ext = (".pyc", ".tmp")
if working_directory:
try:
if os.path.exists(working_directory):
generated_files = []
for root, dirs, files in os.walk(working_directory):
dirs[:] = [
d
for d in dirs
if not d.startswith(".")
and d
not in ["node_modules", "__pycache__", "venv"]
]
for file in files:
if not file.startswith(".") and not file.endswith(
skip_ext
):
file_path = os.path.join(root, file)
absolute_path = os.path.abspath(file_path)

# Only add if not seen before
if (
seen_files is None
or absolute_path not in seen_files
):
generated_files.append(absolute_path)
if seen_files is not None:
seen_files.add(absolute_path)

if generated_files:
context_parts.append(
"Generated Files from Previous Task:"
)
for file_path in sorted(generated_files):
context_parts.append(f" - {file_path}")
generated_files = list_files(
working_directory,
base=working_directory,
skip_dirs={"node_modules", "__pycache__", "venv"},
skip_extensions=(".pyc", ".tmp"),
skip_prefix=".",
)
if seen_files is not None:
generated_files = [
p for p in generated_files if p not in seen_files
]
seen_files.update(generated_files)
if generated_files:
context_parts.append("Generated Files from Previous Task:")
for file_path in sorted(generated_files):
context_parts.append(f" - {file_path}")
except Exception as e:
logger.warning(f"Failed to collect generated files: {e}")

Expand Down Expand Up @@ -172,31 +154,20 @@ def collect_previous_task_context(
f"Previous Task Result:\n{previous_task_result}\n"
)

# Collect generated files from working directory
# Collect generated files from working directory (safe listing)
try:
if os.path.exists(working_directory):
generated_files = []
for root, dirs, files in os.walk(working_directory):
dirs[:] = [
d
for d in dirs
if not d.startswith(".")
and d not in ["node_modules", "__pycache__", "venv"]
]
skip_ext = (".pyc", ".tmp")
for file in files:
if not file.startswith(".") and not file.endswith(
skip_ext
):
file_path = os.path.join(root, file)
absolute_path = os.path.abspath(file_path)
generated_files.append(absolute_path)

if generated_files:
context_parts.append("Generated Files from Previous Task:")
for file_path in sorted(generated_files):
context_parts.append(f" - {file_path}")
context_parts.append("")
generated_files = list_files(
working_directory,
base=working_directory,
skip_dirs={"node_modules", "__pycache__", "venv"},
skip_extensions=(".pyc", ".tmp"),
skip_prefix=".",
)
if generated_files:
context_parts.append("Generated Files from Previous Task:")
for file_path in sorted(generated_files):
context_parts.append(f" - {file_path}")
context_parts.append("")
except Exception as e:
logger.warning(f"Failed to collect generated files: {e}")

Expand Down Expand Up @@ -272,30 +243,21 @@ def build_conversation_context(
context += f"Assistant: {entry['content']}\n\n"

if working_directories:
all_generated_files = set() # Use set to avoid duplicates
all_generated_files: set[str] = set()
for working_directory in working_directories:
try:
if os.path.exists(working_directory):
for root, dirs, files in os.walk(working_directory):
dirs[:] = [
d
for d in dirs
if not d.startswith(".")
and d
not in ["node_modules", "__pycache__", "venv"]
]
for file in files:
if not file.startswith(
"."
) and not file.endswith((".pyc", ".tmp")):
file_path = os.path.join(root, file)
absolute_path = os.path.abspath(file_path)
all_generated_files.add(absolute_path)
files_list = list_files(
working_directory,
base=working_directory,
skip_dirs={"node_modules", "__pycache__", "venv"},
skip_extensions=(".pyc", ".tmp"),
skip_prefix=".",
)
all_generated_files.update(files_list)
except Exception as e:
logger.warning(
"Failed to collect generated "
f"files from {working_directory}"
f": {e}"
f"files from {working_directory}: {e}"
)

if all_generated_files:
Expand Down
Loading