diff --git a/pyproject.toml b/pyproject.toml index 62a30cd..145605c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,4 +77,5 @@ where = ["src"] [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = ["src"] asyncio_mode = "auto" diff --git a/src/factor/app.py b/src/factor/app.py index 4d80f8e..cd31961 100644 --- a/src/factor/app.py +++ b/src/factor/app.py @@ -117,8 +117,18 @@ async def analyze_documents(files: list[UploadFile] = File(...)): detail=f"File {f.filename} exceeds {settings.factor_max_upload_mb}MB limit", ) - file_path = upload_dir / (f.filename or f"document_{len(saved_paths)}") + safe_name = Path(f.filename).name if f.filename else "" + if not safe_name or safe_name in (".", ".."): + safe_name = f"document_{len(saved_paths)}" + file_path = (upload_dir / safe_name).resolve() + if not file_path.is_relative_to(upload_dir.resolve()): + raise HTTPException(status_code=400, detail=f"Invalid filename: {f.filename}") content = await f.read() + if len(content) > settings.max_upload_bytes: + raise HTTPException( + status_code=400, + detail=f"File {f.filename} exceeds {settings.factor_max_upload_mb}MB limit", + ) file_path.write_bytes(content) saved_paths.append(str(file_path)) @@ -169,7 +179,7 @@ async def event_stream() -> AsyncGenerator[dict, None]: for doc_id, provisions in all_provisions.items(): detected_types = [] - for prov in provisions: + for prov_index, prov in enumerate(provisions): detection = detect_provision_type(provision_text=prov["text"]) prov["provision_type"] = detection["provision_type"] detected_types.append(detection["provision_type"]) @@ -179,7 +189,10 @@ async def event_stream() -> AsyncGenerator[dict, None]: all_risk_scores.append(risk) if breaker: - breaker.record_step(action="score_risk", meta={"doc_id": doc_id}) + breaker.record_step( + action="score_risk", + meta={"doc_id": doc_id, "provision_index": prov_index}, + ) gaps = find_gaps(detected_provisions=detected_types, doc_type="unknown") for gap in gaps: diff --git a/src/factor/harness/circuit_breaker.py b/src/factor/harness/circuit_breaker.py index ed4f7eb..412ced7 100644 --- a/src/factor/harness/circuit_breaker.py +++ b/src/factor/harness/circuit_breaker.py @@ -55,7 +55,10 @@ def record_step( ) -> None: """Record a reasoning step and check trip conditions.""" self.budget.record(input_tokens, output_tokens, meta) - self.loop_detector.record(action) + signature = action + if meta: + signature = f"{action}:{sorted(meta.items())!r}" + self.loop_detector.record(signature) logger.debug( "Session %s step: action=%s in=%d out=%d cost=$%.4f", diff --git a/src/factor/tools/export.py b/src/factor/tools/export.py index 43a4b94..cc3d2b5 100644 --- a/src/factor/tools/export.py +++ b/src/factor/tools/export.py @@ -228,17 +228,17 @@ def export_html(report: dict, output_path: str) -> str:
| {{ key | replace('_', ' ') | title }} | {% endfor %}
|---|
| {{ val }} | @@ -260,7 +260,7 @@ def export_html(report: dict, output_path: str) -> str: