Skip to content
Open
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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
AAIS_RUNTIME_DIR=/app/.runtime/aais-data \
AAIS_WORKSPACE_ROOT=/app \
PATH=/usr/local/bin:$PATH \
PORT=8000

Expand Down
1 change: 1 addition & 0 deletions deploy/pilot/Dockerfile.aais
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
AAIS_RUNTIME_DIR=/app/.runtime/aais-data \
AAIS_WORKSPACE_ROOT=/app \
PIP_NO_CACHE_DIR=1

WORKDIR /app
Expand Down
1 change: 1 addition & 0 deletions deploy/platform/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
AAIS_WORKSPACE_ROOT=/app \
PIP_NO_CACHE_DIR=1

WORKDIR /app
Expand Down
2 changes: 2 additions & 0 deletions render.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ services:
value: "8000"
- key: AAIS_RUNTIME_DIR
value: /app/.runtime/aais-data
- key: AAIS_WORKSPACE_ROOT
value: /app
- key: JARVIS_DATA_DIR
value: /app/.runtime/aais-data
- key: ENVIRONMENT
Expand Down
30 changes: 22 additions & 8 deletions src/evolving_workbench.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def _wrap_ul_payload(payload: dict) -> dict:
from typing import Any
from uuid import uuid4

from src.workspace_root import resolve_workspace_root

PRIMARY_PROJECT_ENV = "AAIS_PRIMARY_PROJECT"
APPROVAL_AUDIT_FILENAME = "evolving-approval-audit.json"
MAX_SYMBOL_RESULTS = 40
Expand All @@ -39,6 +41,13 @@ def _wrap_ul_payload(payload: dict) -> dict:
"build",
"dist",
"_archives",
"proc",
"sys",
"dev",
"map_files",
"run",
"boot",
"lost+found",
}

CODE_EXTENSIONS = {
Expand Down Expand Up @@ -203,12 +212,10 @@ def __init__(self, workspace_root: str | Path | None = None):
self.workspace_root = Path(workspace_root) if workspace_root else None

def _resolve_workspace_root(self) -> Path:
configured = os.getenv("AAIS_WORKSPACE_ROOT")
if configured:
return Path(configured).expanduser().resolve()
if self.workspace_root is not None:
return self.workspace_root.expanduser().resolve()
return Path(__file__).resolve().parents[2]
return resolve_workspace_root(
self.workspace_root,
module_file=Path(__file__),
)

def _preferred_project_name(self) -> str:
configured = str(os.getenv(PRIMARY_PROJECT_ENV, "")).strip()
Expand All @@ -225,15 +232,22 @@ def _preferred_project_name(self) -> str:
def _iter_visible_files(self) -> list[str]:
root = self._resolve_workspace_root()
visible: list[str] = []
for current_root, dirs, files in os.walk(root):

def _skip_unreadable(_error: OSError) -> None:
return None

for current_root, dirs, files in os.walk(root, onerror=_skip_unreadable):
dirs[:] = [
directory
for directory in dirs
if directory not in IGNORED_DIR_NAMES and not directory.startswith(".")
]
for filename in files:
path = Path(current_root) / filename
relative = _normalize_posix_path(path.relative_to(root))
try:
relative = _normalize_posix_path(path.relative_to(root))
except (OSError, ValueError):
continue
if relative:
visible.append(relative)
return sorted(visible)
Expand Down
120 changes: 80 additions & 40 deletions src/jarvis_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from src.patch_execution_preview import PatchExecutionPreview
from src.patch_review_store import PatchReviewStore
from src.project_infi_law import ProjectInfiLaw
from src.workspace_root import resolve_workspace_root
from src.forge_repo_governance import (
build_forge_contractor_payload,
build_forge_eval_payload,
Expand Down Expand Up @@ -267,6 +268,14 @@ def _filter_workspace_context_for_forge(
"build",
"dist",
"_archives",
# Container/host pseudo-fs — walking these hits /proc/*/map_files (EPERM).
"proc",
"sys",
"dev",
"map_files",
"run",
"boot",
"lost+found",
}

TEXT_EXTENSIONS = {
Expand Down Expand Up @@ -2730,13 +2739,11 @@ def __init__(self, workspace_root: str | Path | None = None):

def _resolve_workspace_root(self):
"""Resolve the workspace root that Jarvis may inspect."""
if os.getenv(WORKSPACE_ROOT_ENV):
return Path(os.getenv(WORKSPACE_ROOT_ENV)).expanduser().resolve()

if self.workspace_root is not None:
return self.workspace_root.expanduser().resolve()

return Path(__file__).resolve().parents[2]
return resolve_workspace_root(
self.workspace_root,
env_var=WORKSPACE_ROOT_ENV,
module_file=Path(__file__),
)

def _preferred_project_name(self):
"""Return the project folder that should rank highest in ambiguous searches."""
Expand Down Expand Up @@ -2765,7 +2772,11 @@ def _iter_files(self):
"""Yield candidate files under the workspace root, skipping bulky/system dirs."""
root = self._resolve_workspace_root()

for current_root, dirs, files in os.walk(root):
def _skip_unreadable(_error: OSError) -> None:
# Fail-open: container /proc/*/map_files raises EPERM; keep chat alive.
return None

for current_root, dirs, files in os.walk(root, onerror=_skip_unreadable):
dirs[:] = [
directory
for directory in dirs
Expand All @@ -2779,21 +2790,28 @@ def _iter_files(self):

def _is_text_file(self, path: Path):
"""Check whether a file is safe and useful to preview/search as text."""
if not path.is_file():
return False
try:
if not path.is_file():
return False

normalized = str(path).replace("/", "\\").lower()
if "\\training\\out\\" in normalized or "\\checkpoint-" in normalized:
return False
normalized = str(path).replace("/", "\\").lower()
if "\\training\\out\\" in normalized or "\\checkpoint-" in normalized:
return False
# Defense-in-depth if a walk somehow enters proc/sys/dev.
parts = {part.lower() for part in path.parts}
if parts & {"proc", "sys", "dev", "map_files"}:
return False

if path.stat().st_size > MAX_FILE_BYTES:
return False
if path.stat().st_size > MAX_FILE_BYTES:
return False

suffix = path.suffix.lower()
if suffix in TEXT_EXTENSIONS:
return True
suffix = path.suffix.lower()
if suffix in TEXT_EXTENSIONS:
return True

return path.name.lower().startswith("readme")
return path.name.lower().startswith("readme")
except OSError:
return False

def _read_text_file(self, path: Path, max_chars: int | None = MAX_FILE_CHARS):
"""Read a bounded text preview from disk."""
Expand Down Expand Up @@ -2951,7 +2969,10 @@ def search(
kind = "path"
snippet = f"Exact file match in {relative_path}"

content = self._read_text_file(path, max_chars=None)
try:
content = self._read_text_file(path, max_chars=None)
except (OSError, ValueError):
continue
lower_content = content.lower()
content_score = _score_text_match(query_tokens, lower_content)

Expand Down Expand Up @@ -5167,21 +5188,25 @@ def build_workspace_context(
return None

preferred_project = self.workspace_tools._preferred_project_name()
search_result = self.workspace_tools.search(
query,
limit=result_limit,
project_name=preferred_project,
prefer_project=preferred_project,
)
results = search_result.get("results", [])
scoped_project = preferred_project if results else None
if not results:
try:
search_result = self.workspace_tools.search(
query,
limit=result_limit,
project_name=preferred_project,
prefer_project=preferred_project,
)
results = search_result.get("results", [])
scoped_project = preferred_project if results else None
if not results:
search_result = self.workspace_tools.search(
query,
limit=result_limit,
prefer_project=preferred_project,
)
results = search_result.get("results", [])
except OSError:
# Fail-open: blocked /proc map reads must not abort a Jarvis reply.
return None

if not results:
return None
Expand Down Expand Up @@ -5345,21 +5370,24 @@ def _build_visual_workspace_context(
return None

preferred_project = self.workspace_tools._preferred_project_name()
search_result = self.workspace_tools.search(
cleaned_query,
limit=result_limit,
project_name=preferred_project,
prefer_project=preferred_project,
)
results = search_result.get("results", [])
scoped_project = preferred_project if results else None
if not results:
try:
search_result = self.workspace_tools.search(
cleaned_query,
limit=result_limit,
project_name=preferred_project,
prefer_project=preferred_project,
)
results = search_result.get("results", [])
scoped_project = preferred_project if results else None
if not results:
search_result = self.workspace_tools.search(
cleaned_query,
limit=result_limit,
prefer_project=preferred_project,
)
results = search_result.get("results", [])
except OSError:
return None

if not results:
return None
Expand Down Expand Up @@ -6105,7 +6133,13 @@ def handle_command(

if lower.startswith("search workspace for "):
query = cleaned[21:].strip()
search_result = self.workspace_tools.search(query, limit=6)
try:
search_result = self.workspace_tools.search(query, limit=6)
except OSError:
return {
"response": "Workspace search is unavailable in this runtime (filesystem introspection blocked).",
"tool_result": {"type": "workspace_search", "query": query, "results": []},
}
lines = [
f"- {result['relative_path']}: {result['snippet']}"
for result in search_result["results"]
Expand All @@ -6122,7 +6156,13 @@ def handle_command(

if lower.startswith("find file "):
query = cleaned[10:].strip()
search_result = self.workspace_tools.search(query, limit=6)
try:
search_result = self.workspace_tools.search(query, limit=6)
except OSError:
return {
"response": "File search is unavailable in this runtime (filesystem introspection blocked).",
"tool_result": {"type": "workspace_search", "query": query, "results": []},
}
lines = [
f"- {result['relative_path']}: {result['snippet']}"
for result in search_result["results"]
Expand Down
13 changes: 6 additions & 7 deletions src/patch_apply_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from datetime import datetime
from src.datetime_compat import UTC
import hashlib
import os
from pathlib import Path
from typing import Any

from src.workspace_root import resolve_workspace_root


WORKSPACE_ROOT_ENV = "AAIS_WORKSPACE_ROOT"
DISALLOWED_AFTER_SNIPPET_MARKERS = (
"review-first patch placeholder",
"review first patch placeholder",
Expand All @@ -34,11 +34,10 @@ def configure_workspace_root(self, workspace_root: str | Path | None) -> None:
self.workspace_root = Path(workspace_root) if workspace_root else None

def _resolve_workspace_root(self) -> Path:
if os.getenv(WORKSPACE_ROOT_ENV):
return Path(os.getenv(WORKSPACE_ROOT_ENV)).expanduser().resolve()
if self.workspace_root is not None:
return self.workspace_root.expanduser().resolve()
return Path(__file__).resolve().parents[2]
return resolve_workspace_root(
self.workspace_root,
module_file=Path(__file__),
)

def _resolve_target_path(self, relative_path: str) -> tuple[Path, str]:
root = self._resolve_workspace_root()
Expand Down
13 changes: 6 additions & 7 deletions src/patch_execution_preview.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from __future__ import annotations

import hashlib
import os
from pathlib import Path
from typing import Any

from src.workspace_root import resolve_workspace_root


WORKSPACE_ROOT_ENV = "AAIS_WORKSPACE_ROOT"
MAX_PREVIEW_BYTES = 256_000
MAX_EXCERPT_CHARS = 220

Expand All @@ -32,11 +32,10 @@ def configure_workspace_root(self, workspace_root: str | Path | None) -> None:
self.workspace_root = Path(workspace_root) if workspace_root else None

def _resolve_workspace_root(self) -> Path:
if os.getenv(WORKSPACE_ROOT_ENV):
return Path(os.getenv(WORKSPACE_ROOT_ENV)).expanduser().resolve()
if self.workspace_root is not None:
return self.workspace_root.expanduser().resolve()
return Path(__file__).resolve().parents[2]
return resolve_workspace_root(
self.workspace_root,
module_file=Path(__file__),
)

def _resolve_target_path(self, relative_path: str) -> tuple[Path, str]:
root = self._resolve_workspace_root()
Expand Down
Loading
Loading