diff --git a/CHANGELOG.md b/CHANGELOG.md index 807fd72d..c6dabef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ - `docs/installation.md` documents the `jina`, `s3` and `ingester` extras, names the extras the full package actually pulls, drops the removed MixedBread AI reranker, and no longer lists Anthropic as a built-in provider. - `docs/tuning.md` no longer points at the removed `claim_timeout_s` setting. - `analysis.code_timeout` is checked before each document read; code that reads in a loop no longer overruns it by the duration of the outstanding reads. +- A crashed Monty worker no longer poisons the analysis sandbox for the rest of the run: `execute` reports the crash and the next call checks out a replacement session. +- `metadata.json` in the document VFS rejects writes, matching `content.txt`, `items.jsonl` and `toc.json`. ### Removed diff --git a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py index e788ea7a..a4256579 100644 --- a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py +++ b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py @@ -2,7 +2,7 @@ import asyncio import json import os from collections.abc import AsyncIterator, Callable, Coroutine -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, suppress from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, Literal @@ -220,6 +220,19 @@ class Sandbox: ) return asyncio.run_coroutine_threadsafe(coro, self._loop).result() + async def _discard_session(self) -> None: + """Drop a session whose worker is gone. + + The session object is unusable once its worker dies: it answers every + later call with ``RuntimeError: this checkout has already been + finished``. Clearing it makes ``_ensure_initialized`` check out a + replacement, at the cost of the variables the dead worker held. + """ + session, self._session = self._session, None + if session is not None: + with suppress(Exception): + await session.__aexit__(None, None, None) + async def close(self) -> None: """Return the worker to the pool and shut the pool down. Idempotent.""" if self._session is not None: @@ -402,7 +415,25 @@ class Sandbox: }, ensure_ascii=False, ) - files.append(MemoryFile(f"{doc_dir}/metadata.json", metadata)) + + # A MemoryFile accepts writes, so mount metadata.json through the + # same read/deny pair as the rest. The content is already built, + # so the reader stays eager. + def _make_metadata_reader( + text: str, + ) -> Callable[["PurePosixPath"], str]: + def read_metadata(_path: "PurePosixPath") -> str: + return text + + return read_metadata + + files.append( + CallbackFile( + f"{doc_dir}/metadata.json", + read=_make_metadata_reader(metadata), + write=_deny_write, + ) + ) def _make_content_reader( did: str, @@ -497,11 +528,20 @@ class Sandbox: print_callback=print_callback, os=vfs, ) - except pydantic_monty.MontyError as e: + except (pydantic_monty.MontyError, RuntimeError) as e: stdout = "".join(stdout_lines) if len(stdout) > max_chars: stdout = stdout[:max_chars] + "\n... (output truncated)" - return SandboxResult(stdout=stdout, stderr=str(e), success=False) + stderr = str(e) + # A crash kills the worker, and a protocol error leaves it out of + # step. Both poison the session. Bad user code does not. + if isinstance(e, pydantic_monty.MontyCrashedError | RuntimeError): + await self._discard_session() + stderr = ( + f"{stderr}\n\nThe interpreter restarted. Variables from " + "earlier calls are gone." + ) + return SandboxResult(stdout=stdout, stderr=stderr, success=False) stdout = "".join(stdout_lines) if output is not None: diff --git a/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_write_denied.yaml b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_write_denied.yaml deleted file mode 100644 index 67dccd0c..00000000 --- a/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_write_denied.yaml +++ /dev/null @@ -1,42 +0,0 @@ -interactions: -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '99' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - Content about foxes and dogs. - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: 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 - index: 0 - object: embedding - model: qwen3-embedding:4b - object: list - usage: - prompt_tokens: 8 - total_tokens: 8 - status: - code: 200 - message: OK -version: 1 diff --git a/tests/sandbox/test_sandbox.py b/tests/sandbox/test_sandbox.py index a2a5fac3..107ae8bf 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -430,71 +430,44 @@ class TestSandboxVFS: assert result.stdout.count("True") == 2 @pytest.mark.asyncio - @pytest.mark.vcr() - async def test_open_write_denied(self, temp_db_path): - """Opening a document file for writing raises PermissionError.""" - config = AppConfig() - async with HaikuRAG(temp_db_path, create=True) as client: - doc = await client.create_document( - content="Content about foxes and dogs.", - uri="test://doc", - title="Fox Document", - ) - - context = AnalysisContext() - sb = Sandbox(db_path=temp_db_path, config=config, context=context) - result = await sb.execute( - "try:\n" - f" with open('/documents/{doc.id}/content.txt', 'w') as f:\n" - " f.write('nope')\n" - " print('WROTE')\n" - "except PermissionError:\n" - " print('DENIED')" - ) - assert result.success - assert "DENIED" in result.stdout - assert "WROTE" not in result.stdout - - @pytest.mark.asyncio - async def test_open_file_objects_are_not_iterable(self, temp_db_path): - """Pins the limitation the instructions warn about: pydantic/monty#490. - - A failure here means Monty gained iteration support and the - `for line in f` prohibition in the analysis instructions is now wrong. - """ + @pytest.mark.parametrize( + "filename", ["content.txt", "items.jsonl", "toc.json", "metadata.json"] + ) + async def test_write_denied_for_every_document_file(self, temp_db_path, filename): + """Every file in the document VFS is read-only, metadata.json included.""" from docling_core.types.doc.document import DoclingDocument from docling_core.types.doc.labels import DocItemLabel config = AppConfig() docling = DoclingDocument(name="d") - docling.add_text(label=DocItemLabel.TEXT, text="one\ntwo") + docling.add_text(label=DocItemLabel.TEXT, text="Foxes and dogs.") async with HaikuRAG(temp_db_path, create=True) as client: doc = await client.import_document( docling, [ Chunk( - content="one\ntwo", + content="Foxes and dogs.", embedding=[0.1] * config.embeddings.model.vector_dim, order=0, ) ], - uri="test://lines", + uri="test://readonly", ) sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) try: result = await sb.execute( - f"for line in open('/documents/{doc.id}/content.txt'):\n print(line)" - ) - assert result.success is False - assert "not iterable" in result.stderr - - # The documented alternatives do work. - result = await sb.execute( - f"print(len(open('/documents/{doc.id}/content.txt').readlines()))" + "from pathlib import Path\n" + "try:\n" + f" Path('/documents/{doc.id}/{filename}').write_text('nope')\n" + " print('WROTE')\n" + "except PermissionError:\n" + " print('DENIED')" ) assert result.success, result.stderr + assert "DENIED" in result.stdout + assert "WROTE" not in result.stdout finally: await sb.close() @@ -807,3 +780,42 @@ class TestSandboxReadDeadline: assert "no further document reads" in result.stderr finally: await sb.close() + + +class TestSandboxWorkerCrash: + """A dead worker must not poison every later call in the run.""" + + @pytest.mark.asyncio + async def test_crashed_worker_is_replaced(self, temp_db_path): + """The crash fails one call. The next call gets a fresh session.""" + import os + import signal + + config = AppConfig() + async with HaikuRAG(temp_db_path, create=True): + pass + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + first = await sb.execute("x = 1\nprint(x)") + assert first.success, first.stderr + assert sb._session is not None + pid = sb._session.worker_pid + assert pid is not None + + os.kill(pid, signal.SIGKILL) + + crashed = await sb.execute("print(2)") + assert crashed.success is False + assert "restarted" in crashed.stderr + + # Without the discard this raises RuntimeError out of execute(). + recovered = await sb.execute("print(3)") + assert recovered.success, recovered.stderr + assert "3" in recovered.stdout + + # The replacement worker starts empty, which the failure said. + lost = await sb.execute("print(x)") + assert lost.success is False + finally: + await sb.close() diff --git a/tests/sandbox/test_sandbox_toc.py b/tests/sandbox/test_sandbox_toc.py index 25ae2a04..2d359212 100644 --- a/tests/sandbox/test_sandbox_toc.py +++ b/tests/sandbox/test_sandbox_toc.py @@ -83,6 +83,23 @@ def _flatten(tree: list[dict]) -> list[dict]: return out +@pytest.mark.asyncio +class TestMetadataJson: + """metadata.json is served by a reader callback, like the other VFS files.""" + + async def test_metadata_reader_returns_document_fields(self, temp_db_path): + async with HaikuRAG(temp_db_path, create=True) as client: + doc_id = await _empty_doc(client, uri="test://meta", title="Meta Doc") + + sandbox = Sandbox(temp_db_path, AppConfig(), AnalysisContext()) + raw = await _read_vfs_text(sandbox, f"/documents/{doc_id}/metadata.json") + meta = json.loads(raw) + + assert meta["id"] == doc_id + assert meta["title"] == "Meta Doc" + assert meta["uri"] == "test://meta" + + @pytest.mark.asyncio class TestTocShape: """toc.json builds a section tree from heading_level + position."""