From 53084d6fdd7f60d6dd38429df8fa1e3351145110 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Sat, 25 Jul 2026 10:41:05 +0300 Subject: [PATCH 1/8] Bump pydantic-monty to 0.0.19 0.0.19 runs sandboxed code in a subprocess worker pool (AsyncMonty / AsyncMontySession) and drops MontyRepl. The sandbox checks out a session, drives it with feed_run, and closes it to return the worker; close() is now async. Worker crashes surface as a failed SandboxResult. The document VFS (OSAccess/CallbackFile/MemoryFile) is unchanged. --- CHANGELOG.md | 1 + .../haiku/rag/capabilities/analysis.py | 2 +- haiku_rag_slim/haiku/rag/sandbox/sandbox.py | 86 +++++++----- haiku_rag_slim/pyproject.toml | 2 +- tests/sandbox/test_sandbox.py | 18 +-- uv.lock | 123 ++++++++++++------ 6 files changed, 146 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0d0637..e75d8907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changed - Require `pydantic-ai-slim>=2.18,<3`. +- `pydantic-monty` bumped to `>=0.0.19`; sandbox code now runs in a subprocess worker pool. - `enable_thinking` maps onto Pydantic AI's unified `thinking` setting for the `anthropic`, `gemini`, `groq` and `bedrock` providers. The Anthropic thinking budget is now Pydantic AI's default of 10000 tokens, was 4096, and `max_tokens` must exceed it on budget-based Claude models; `enable_thinking: false` disables Gemini thinking rather than only hiding thoughts; Groq maps reasoning effort rather than `groq_reasoning_format`; Bedrock Qwen with `enable_thinking: false` no longer sends `reasoning_config`, while Bedrock-served Claude keeps an explicit `thinking: disabled`. The `openai` and `ollama` providers still map to `openai_reasoning_effort`. - `provider: bedrock` with a proprietary OpenAI model such as `openai.o3-mini-v1:0` raises `UserError`: Bedrock Converse serves only the `gpt-oss` family. Use `provider: bedrock-mantle`. - Tool failures raise `pydantic_ai.ToolFailed` instead of returning failure text: search and code-execution limits, sandbox execution errors, and `get_document`/`summarize_document` misses. diff --git a/haiku_rag_slim/haiku/rag/capabilities/analysis.py b/haiku_rag_slim/haiku/rag/capabilities/analysis.py index de250785..8c8e133c 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/analysis.py +++ b/haiku_rag_slim/haiku/rag/capabilities/analysis.py @@ -66,7 +66,7 @@ class AnalysisCapability(RAGCapabilityBase[AnalysisState]): async def _close(self) -> None: if self.sandbox is not None: - self.sandbox.close() + await self.sandbox.close() self.sandbox = None await super()._close() diff --git a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py index 76486690..a32b4973 100644 --- a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py +++ b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py @@ -8,7 +8,13 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Literal import pydantic_monty -from pydantic_monty import CallbackFile, MemoryFile, MontyRepl, OSAccess +from pydantic_monty import ( + AsyncMonty, + AsyncMontySession, + CallbackFile, + MemoryFile, + OSAccess, +) from haiku.rag.config.models import AppConfig from haiku.rag.sandbox.dependencies import AnalysisContext @@ -109,24 +115,26 @@ class Sandbox: """Execute code in a sandboxed Python interpreter. Uses pydantic-monty, a minimal secure Python interpreter written in Rust. - External functions (search, list_documents) are called by Monty code + The interpreter runs in a subprocess worker checked out of an ``AsyncMonty`` + pool. External functions (search, list_documents) are called by Monty code using ``await`` and resolved asynchronously on the host. Documents are exposed via a virtual filesystem at ``/documents/{id}/``. - The interpreter uses a REPL session — variables persist across - ``execute()`` calls within the same Sandbox instance. + The session persists across ``execute()`` calls within the same Sandbox + instance — variables carry over. Call ``close()`` to return the worker to + the pool and shut the pool down. sandbox = Sandbox(db_path, config, context) result = await sandbox.execute("x = await search('query')") result = await sandbox.execute("print(x[0]['content'])") # x persists + await sandbox.close() All database access runs on the event loop that drives ``execute()``. Monty's - file callbacks are synchronous and run on the interpreter's worker thread, so - they bridge back to that loop via ``run_coroutine_threadsafe``; the loop is - free during ``feed_run_async`` (the VM runs on the worker thread), so the - bridge does not deadlock. When a ``rag`` connection is supplied it is used - for every read, so an analysis run drives a single connection on a single - loop; otherwise each read opens an ephemeral read-only connection. + file callbacks are synchronous and run off that loop while ``feed_run`` is + awaited, so they bridge back to it via ``run_coroutine_threadsafe`` without + deadlocking. When a ``rag`` connection is supplied it is used for every read, + so an analysis run drives a single connection on a single loop; otherwise + each read opens an ephemeral read-only connection. """ _db_path: Path @@ -139,7 +147,8 @@ class Sandbox: _doc_chunk_index: dict[str, dict[str, list[str]]] _items_jsonl_cache: dict[str, str] _toc_json_cache: dict[str, str] - _repl: MontyRepl | None + _pool: AsyncMonty | None + _session: AsyncMontySession | None _vfs: OSAccess | None _loop: asyncio.AbstractEventLoop | None @@ -161,7 +170,8 @@ class Sandbox: self._doc_chunk_index = {} self._items_jsonl_cache = {} self._toc_json_cache = {} - self._repl = None + self._pool = None + self._session = None self._vfs = None self._loop = None @@ -185,17 +195,22 @@ class Sandbox: def _run_on_loop(self, coro: Coroutine[Any, Any, Any]) -> Any: """Run a coroutine on the execute() loop from a synchronous callback. - Called from Monty's worker thread while ``feed_run_async`` leaves the - loop free, so scheduling onto it and blocking for the result is safe. + Called off the loop while ``feed_run`` is awaited, so scheduling onto it + and blocking for the result is safe. """ assert self._loop is not None, ( "VFS reads happen during execute(); the loop must be captured first." ) return asyncio.run_coroutine_threadsafe(coro, self._loop).result() - def close(self) -> None: - """Retained for API compatibility; the sandbox owns no resources.""" - return + async def close(self) -> None: + """Return the worker to the pool and shut the pool down. Idempotent.""" + if self._session is not None: + await self._session.__aexit__(None, None, None) + self._session = None + if self._pool is not None: + await self._pool.__aexit__(None, None, None) + self._pool = None def _build_external_functions(self) -> dict[str, Any]: """Build async external functions for the Monty interpreter.""" @@ -413,48 +428,47 @@ class Sandbox: return OSAccess(files) - async def _ensure_initialized(self) -> tuple[MontyRepl, OSAccess]: - """Initialize the REPL session and VFS on first use.""" - if self._repl is None: + async def _ensure_initialized(self) -> tuple[AsyncMontySession, OSAccess]: + """Check out a worker session and build the VFS on first use.""" + if self._session is None: self._vfs = await self._build_vfs() - self._repl = MontyRepl( - limits={ - "max_duration_secs": self._config.analysis.code_timeout, - }, + self._pool = AsyncMonty() + await self._pool.__aenter__() + session = self._pool.checkout( + limits={"max_duration_secs": self._config.analysis.code_timeout}, ) - assert self._repl is not None and self._vfs is not None - return self._repl, self._vfs + await session.__aenter__() + self._session = session + assert self._session is not None and self._vfs is not None + return self._session, self._vfs async def execute(self, code: str) -> SandboxResult: - """Execute Python code in the Monty REPL. + """Execute Python code in the Monty worker session. Variables persist across calls within the same Sandbox instance. """ # Monty's synchronous file callbacks bridge DB reads back to this loop. self._loop = asyncio.get_running_loop() - repl, vfs = await self._ensure_initialized() + session, vfs = await self._ensure_initialized() external_fns = self._build_external_functions() stdout_lines: list[str] = [] - def print_callback( # pragma: no cover - runs on Monty's Rust thread - _stream: Literal["stdout"], text: str + def print_callback( # pragma: no cover - runs on Monty's worker thread + _stream: Literal["stdout", "stderr"], text: str ) -> None: stdout_lines.append(text) max_chars = self._config.analysis.max_output_chars try: - output = await repl.feed_run_async( + output = await session.feed_run( code, - external_functions=external_fns, + external_lookup=external_fns, print_callback=print_callback, os=vfs, ) - except ( - pydantic_monty.MontySyntaxError, - pydantic_monty.MontyRuntimeError, - ) as e: + except pydantic_monty.MontyError as e: stdout = "".join(stdout_lines) if len(stdout) > max_chars: stdout = stdout[:max_chars] + "\n... (output truncated)" diff --git a/haiku_rag_slim/pyproject.toml b/haiku_rag_slim/pyproject.toml index 827ebaf3..c1e87d84 100644 --- a/haiku_rag_slim/pyproject.toml +++ b/haiku_rag_slim/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "pathspec>=1.0.4", "pydantic>=2.12.5", "pydantic-ai-slim[openai,logfire,ag-ui]>=2.18.0,<3.0.0", - "pydantic-monty>=0.0.17", + "pydantic-monty>=0.0.19", "pypdfium2>=5.0", "python-dotenv>=1.2.2", "pyyaml>=6.0.3", diff --git a/tests/sandbox/test_sandbox.py b/tests/sandbox/test_sandbox.py index b996fe9d..7f65ce43 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -453,7 +453,7 @@ class TestSandboxHeldConnection: assert int(lines[1]) > 0 assert lines[2] == "True" finally: - sb.close() + await sb.close() @pytest.mark.asyncio @pytest.mark.vcr() @@ -557,7 +557,7 @@ class TestSandboxHeldConnection: assert first.stdout == second.stdout assert not any(t.name == "sandbox-vfs" for t in threading.enumerate()) finally: - sb.close() + await sb.close() @pytest.mark.asyncio @pytest.mark.vcr() @@ -584,21 +584,21 @@ class TestSandboxHeldConnection: assert second.success, second.stderr assert int(second.stdout.strip()) > 0 finally: - sb.close() + await sb.close() @pytest.mark.asyncio async def test_close_is_safe_without_vfs_read(self, temp_db_path): - """close() is a no-op (and safe to call twice) when no VFS read happened.""" + """close() is safe and idempotent before any code has run.""" async with HaikuRAG(temp_db_path, create=True): config = AppConfig() sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) - sb.close() - sb.close() + await sb.close() + await sb.close() @pytest.mark.asyncio @pytest.mark.vcr() async def test_close_is_idempotent_after_vfs_read(self, temp_db_path): - """close() is a safe no-op after a VFS read; no background thread lingers.""" + """close() tears down the worker and is idempotent; no thread lingers.""" config = AppConfig() async with HaikuRAG(temp_db_path, create=True) as client: doc = await client.create_document( @@ -616,5 +616,5 @@ class TestSandboxHeldConnection: ) assert result.success assert not any(t.name == "sandbox-vfs" for t in threading.enumerate()) - sb.close() - sb.close() + await sb.close() + await sb.close() diff --git a/uv.lock b/uv.lock index 03a7efcd..d2fb894a 100644 --- a/uv.lock +++ b/uv.lock @@ -1770,7 +1770,7 @@ requires-dist = [ { name = "pydantic-ai-slim", extras = ["mistral"], marker = "extra == 'mistral'" }, { name = "pydantic-ai-slim", extras = ["openai", "logfire", "ag-ui"], specifier = ">=2.18.0,<3.0.0" }, { name = "pydantic-ai-slim", extras = ["voyageai"], marker = "extra == 'voyageai'" }, - { name = "pydantic-monty", specifier = ">=0.0.17" }, + { name = "pydantic-monty", specifier = ">=0.0.19" }, { name = "pypdfium2", specifier = ">=5.0" }, { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "pyyaml", specifier = ">=6.0.3" }, @@ -3981,49 +3981,94 @@ wheels = [ [[package]] name = "pydantic-monty" -version = "0.0.17" +version = "0.0.19" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "pydantic-monty-runtime" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/f8/431ba0b79d02922811392c4e3d283d6508f7052ceaa1936cc34878703ecc/pydantic_monty-0.0.17.tar.gz", hash = "sha256:9c4904a8fbc63282793f3afd2d180124494c7fc371783f365e5691c9586360af", size = 1007724, upload-time = "2026-04-22T20:13:48.915Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/f8/c04df414086488834d102ab85cf8172c114108f842fb142ac92a490213fd/pydantic_monty-0.0.19.tar.gz", hash = "sha256:f3f9e256058b4085349dd4ad347795d5203a8310e9eaad9a2bff93890ac5b86d", size = 1484032, upload-time = "2026-07-24T10:00:13.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/31/95827babdb35149f076c5d191b6b1e7a7c58f4bc72432f905e02e4e3231e/pydantic_monty-0.0.17-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27c2254fa7a7b05e969f79578889230d293c62e0b1ee28371ec4f3c54b14426a", size = 7342248, upload-time = "2026-04-22T20:15:18.775Z" }, - { url = "https://files.pythonhosted.org/packages/cb/67/ca9cfc07cd445d22def53e9db86912f9ae3e11ef772ce41c2ff41a47eac5/pydantic_monty-0.0.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:445cc471ce6f5a88ef06741b7ebc7002a2253d182f55a2f47094d4adedaaf497", size = 7311255, upload-time = "2026-04-22T20:15:27.913Z" }, - { url = "https://files.pythonhosted.org/packages/df/96/abc9c4972d91a9673435b84e12b99d038e42d1f99648fc9e5f242e09d00e/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39121038405911f59da7bf61164251f59bad3fb1b0cd28f43c42c3949eee2c8a", size = 7868109, upload-time = "2026-04-22T20:15:04.779Z" }, - { url = "https://files.pythonhosted.org/packages/75/82/9e4d55529bb99d882b9277a721762537a8bc1345ab1d052bb614a88bd15b/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dafc8ffe57c257002f623afdb7d0e41f73de850179ebd90b42611e4f2b6f9884", size = 7139709, upload-time = "2026-04-22T20:15:25.386Z" }, - { url = "https://files.pythonhosted.org/packages/96/97/f1af6acefb7bb38d73934d6853998bbd327de7418b811372519080d9fd84/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea00838ef8f37dcd8085defcbdfe89fdd05297a6533f1d3f4cad857d13cedc7b", size = 7450444, upload-time = "2026-04-22T20:13:37.974Z" }, - { url = "https://files.pythonhosted.org/packages/1d/91/af92ef409e1c065345cf1451bbcf19e00f70a250b8372ec65143ca9a9238/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683d18089acf14d0de293245b9e37c7f0ec64e6d266f6773144211931aa3ec97", size = 7967525, upload-time = "2026-04-22T20:13:42.674Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1f/23ecd6e268ef24ce6b0fe4a1e76a314990d2e923ac5791e29d657418243d/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1829993dd50cf497cbed66ea9f6c8ff7d157d22592a05c7399f92fb8a549e3c", size = 8199124, upload-time = "2026-04-22T20:15:00.02Z" }, - { url = "https://files.pythonhosted.org/packages/42/2a/36b694ea0c7e202250a81a57faf00f218738da6c5d070c752f2d81cd34ce/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7869e3f41a54cc588096c52a8a4de25ecd81e75c867ea4164b14ea1ae1a57f", size = 7739623, upload-time = "2026-04-22T20:14:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/98/e5/090357d7bc0f0751d1afbb71330695fa26554699c88ba56ecaad91657088/pydantic_monty-0.0.17-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9c17663e2c6f07aec5bc54cd7e39a9e20f250a97a9081e1b2b932eb00d0afc5", size = 7317755, upload-time = "2026-04-22T20:14:48.367Z" }, - { url = "https://files.pythonhosted.org/packages/15/63/67200070cf33325ecfda81d4aee3bf312250ce80bd73058103e04e0f3587/pydantic_monty-0.0.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5d2cf98afe2fb124f6ade91d9663d54277478cad417164f83df1854a41ef450c", size = 7769158, upload-time = "2026-04-22T20:14:39.611Z" }, - { url = "https://files.pythonhosted.org/packages/58/ce/9ecfbc2f45406cfb247fafdea4f4a8412db3e559a22c4385eb15266ba2c1/pydantic_monty-0.0.17-cp312-cp312-win32.whl", hash = "sha256:b2185cc4effbbd6793eed4e0f0bcb6a3dbfbb3289ea4d47888708813f0a3dd47", size = 7227917, upload-time = "2026-04-22T20:14:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/d3/80/9be3bef8273817ccc17da25c3ce4ff5d5d45e5629c17eacc90cdef073821/pydantic_monty-0.0.17-cp312-cp312-win_amd64.whl", hash = "sha256:7833daed757ec9b09b627cc3577a4a76b114c5148f779531d7cfdb1095bcf0a9", size = 8043469, upload-time = "2026-04-22T20:13:22.102Z" }, - { url = "https://files.pythonhosted.org/packages/b5/44/0e106b8b27eb93b66e4f3d279486464e05ba5ee31088848e58b5f506f879/pydantic_monty-0.0.17-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0cdac8c3c16477596bc96ee1cec4f2fbaccd089e2daa1e7b9f227cc89f97cb1", size = 7341507, upload-time = "2026-04-22T20:14:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/e5/88/a0315fa08e62e2d1ef00c03d8202d7bef3f1f71543bebfb916fea265c0a2/pydantic_monty-0.0.17-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37290d6a1c35aba5cfb8b490bb31c0d822e8ddca8f3ca9ea068e30930d80dd1e", size = 7311916, upload-time = "2026-04-22T20:13:34.022Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e5/e4da6acb408594cbfbfb8dd3c0491b9b2ee54e9183e7ebc5f584baa07af9/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:49f252b2fb918686d3e8f76cb30245e782d1560a7fa68dbc0f6940d83c12bd41", size = 7867465, upload-time = "2026-04-22T20:13:31.466Z" }, - { url = "https://files.pythonhosted.org/packages/58/d4/64c2f8eb708a743b0944ea8f71dfd51bc655285b4be28d55577dafbb29fa/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2254d25c34463d67069f5f1567157bde9311654cd5371f8933b8ea9815bfa26a", size = 7139262, upload-time = "2026-04-22T20:14:10.715Z" }, - { url = "https://files.pythonhosted.org/packages/0c/67/5d766f9cd304e871a5dfe5f0a85eaa533538ef07e9b2858fdf9f37f83694/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0688a1fa5dc045ac7b7e996d7269b94f74f116d7b1e352c7b5bb5ad53d4fe03", size = 7450119, upload-time = "2026-04-22T20:13:44.515Z" }, - { url = "https://files.pythonhosted.org/packages/33/98/fa16779021d93edb19807e87cdba56bbec6adfad21f10b41a212572ce513/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b35121ac555ed201405c69d531e4cb916da6984b8cd2e15c8a319117349faf6", size = 7967398, upload-time = "2026-04-22T20:13:55.576Z" }, - { url = "https://files.pythonhosted.org/packages/92/64/287a42720bc9e975ab5b52625aa9fc6bcef8298dd821022cc45c6ee1808d/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c97dc44af25d4392b474902fc40f78f09fe8f44ba791364670334fe12abc077", size = 8198835, upload-time = "2026-04-22T20:15:02.072Z" }, - { url = "https://files.pythonhosted.org/packages/97/37/03edb1fd582b79b2b462afc3fea5e1c8fea73afefc4870dc35fc3c7c492e/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ed2fb365ef9ca921de9a17786ecfa2efe06e65678e6ca57be51658a2a880f31", size = 7739241, upload-time = "2026-04-22T20:13:46.925Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/b765c9ca2ae27def1caa07345aba073ae1239fc2d9cca7a375f3dc2195f7/pydantic_monty-0.0.17-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5bf9f07b38dd12747e3c95b169a5afb3e2e9107622e01e548246c84d19a69c99", size = 7316719, upload-time = "2026-04-22T20:14:13.058Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3b/64fe872cd575ab5262e1ba2959554ead198c939cfaa425f7f8e9b1ad2694/pydantic_monty-0.0.17-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a5e9bafd4b5acbc0a8e12ee8403a3ce37281c3b0fa5909d3f412bee76c69003c", size = 7769150, upload-time = "2026-04-22T20:13:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/76/b4/b6a0bb41f39bac2e11e6a2fd42ca0886893fafa6344fb17c3f0a94e22e83/pydantic_monty-0.0.17-cp313-cp313-win32.whl", hash = "sha256:1c239ae3e610d3f39cd1609285209a4e2d046b465ac1bfed0d4374c615eed0fa", size = 7227705, upload-time = "2026-04-22T20:15:12.262Z" }, - { url = "https://files.pythonhosted.org/packages/86/e7/d8cd62f537f7ab17714ee19ea221a0e341dab407215376ab1c41d79794c9/pydantic_monty-0.0.17-cp313-cp313-win_amd64.whl", hash = "sha256:1886c3590b02f359ae991f1e76691064f167330eda4fbf22762127ce17d0eb48", size = 8043469, upload-time = "2026-04-22T20:14:24.86Z" }, - { url = "https://files.pythonhosted.org/packages/69/c0/8354baf835e1a04c4b9e11d253f82df7d625a9305e6a23a177fb895b1484/pydantic_monty-0.0.17-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a166bd04d1996f0d144fbb5e1391cd1c0fbdacd4fa3b689dd48931388679fe98", size = 7341303, upload-time = "2026-04-22T20:14:35.653Z" }, - { url = "https://files.pythonhosted.org/packages/00/ac/d58221b5e17915421ca00bb08b805ac121b6accb194785d5422da4a2f5fc/pydantic_monty-0.0.17-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d82f319e3fd79707a7b81bbb68596509d14ac73502d2f0daf4ab5d281efdfbdc", size = 7318912, upload-time = "2026-04-22T20:13:59.966Z" }, - { url = "https://files.pythonhosted.org/packages/81/3f/8eeb8f652f6cd6e06a737aa9f00de2949e37a669b31756bc51b6182457b4/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f38b9875f7ff56fe69538b60b2ecbdcbe2b8b7780407ceb05fe0ee1414bf8d19", size = 7867027, upload-time = "2026-04-22T20:13:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/55/ba/ec6620c27c8b4cada6ce53378c52c245e238e50a20b968cafdc1b8573c4e/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74a778bb5a4dcdbc85b3b9002f9a72a43fb6ffd88635bfdd502e8d3053008337", size = 7137542, upload-time = "2026-04-22T20:13:35.939Z" }, - { url = "https://files.pythonhosted.org/packages/41/78/5419785630511b54b15cfb094871bcb53ec9025ba8d91bd7ab5b22b6c98f/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e2ab074a65738e9c1b4be9a432e7ca1e9987a6706018dc7a5af6d4ce7cecdf3", size = 7450222, upload-time = "2026-04-22T20:13:53.378Z" }, - { url = "https://files.pythonhosted.org/packages/61/04/cec11fa96a47034da3c21af53e73f43d2270f5ce96cd710809859fe9c0b2/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00fd1cd28b4200c9ccd02629868486b366af1bfe1f0584d4c9e513b7a941a868", size = 7967405, upload-time = "2026-04-22T20:14:03.826Z" }, - { url = "https://files.pythonhosted.org/packages/81/e3/f2be0fb975100b6936ca36a8410098f10fab3b26729c0b0d1de2fac59ff3/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ea6684555bfd00cbe9d2df3e73caada3d82200c168c63cc475197b55b88401", size = 8199028, upload-time = "2026-04-22T20:13:26.802Z" }, - { url = "https://files.pythonhosted.org/packages/2c/43/358bdaa9c50d21fe4a25a71d43ce9af2d6796616fa47aca84f807433564e/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f804a03a3bbd0cf0ade1d4ce11b50ca6858e9c4440b27c746faf1d3c0a272954", size = 7749903, upload-time = "2026-04-22T20:15:09.626Z" }, - { url = "https://files.pythonhosted.org/packages/9a/da/8bcd0a78abf13edceca36aaf5c180fad963e4c2e042fd7247b9e96048306/pydantic_monty-0.0.17-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:b5476e6c08b86b0bea554b97ce9b142aac1177447f2d3c5751864b27991fd1b1", size = 7312704, upload-time = "2026-04-22T20:14:33.668Z" }, - { url = "https://files.pythonhosted.org/packages/88/49/5de8bb7f8b82c3ebb8f2485e0b7a40055b072193039d95dcb5d35fcba72c/pydantic_monty-0.0.17-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b5fcdcca45439844bee268686f37226dbf5803ec7a5945f5536c41419f151dac", size = 7768902, upload-time = "2026-04-22T20:14:29.389Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7c/500a002f1a52f17c8b8a989875da3e1590c18ce95c89856aa967e2348a36/pydantic_monty-0.0.17-cp314-cp314-win32.whl", hash = "sha256:4dd3e6e80a415e7272f7a7583a4f8e045096653f6074e181117eb61fc8fe3b45", size = 7227049, upload-time = "2026-04-22T20:14:01.871Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/b8f552f2a863778f49ebb708f18aaf0bfb275480a48965786e06efacf1c4/pydantic_monty-0.0.17-cp314-cp314-win_amd64.whl", hash = "sha256:36a8090a628e8cf91df8f66c721a71050ac8f48473d4992b9afbd9585941a647", size = 8062999, upload-time = "2026-04-22T20:14:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/36/bf/152bbb3315dfa46d4e4aae71779230e50c67a34d859a3470fd75c01b795c/pydantic_monty-0.0.19-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b073e64edfd62cca918d792d6fe559512472f981f949e53a7aec673201f5f554", size = 2492733, upload-time = "2026-07-24T09:56:49.612Z" }, + { url = "https://files.pythonhosted.org/packages/46/16/9d37f1bf94c47c593a06ecb918bb64a2c8a38af24d6fa2b0d0e031938edf/pydantic_monty-0.0.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5825ae6c40270166f0f31b6e62d59b0fe47201d12b1be5842efc22d3b7dadcee", size = 2234610, upload-time = "2026-07-24T09:56:51.257Z" }, + { url = "https://files.pythonhosted.org/packages/82/4f/31732f4b9c2b6574eb564e420593b9be3f80d92440211970fab23e882bc5/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5b6821c0035ff2c02cf0f37823fb905890f7238ce923bbc3ca937740f9554836", size = 2303116, upload-time = "2026-07-24T09:56:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/21/2c/c21e6179361100dd8b9ad410df775d176a29e0b85f2ffc3144fd29840ee9/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:5fb4514d99a10e304237a82baeb6072e934ce8462dcd5fb5c3fea0ee0ef16aeb", size = 2007947, upload-time = "2026-07-24T09:56:54.006Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f0/ad64f4894334499f689bdce7e5b5dda6b680d98989424092dcbf21666564/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:2b0f154ac2e6450befa337a99a8519e2f1195cc59f88bd73d780067ace0c4c97", size = 2151468, upload-time = "2026-07-24T09:56:55.626Z" }, + { url = "https://files.pythonhosted.org/packages/2a/97/4ff5f9170e4865ec28fb152bc6ebaa8bd1873e695ee98af2103607ba42e8/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:6fcb4e3a312397432f7c5a0aa04a765670d9f37f4e23f37cb580b3faa2f218b1", size = 2300164, upload-time = "2026-07-24T09:56:57.318Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/c9bfb6708bc5d9f6b040db46156c6e3f16de68ab22f07e2de4f25782414b/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:955d308171ba0260f75d2dede18c46d9732647f94b8f7fba3076bb539f155dc8", size = 2164984, upload-time = "2026-07-24T09:56:58.66Z" }, + { url = "https://files.pythonhosted.org/packages/35/2d/8c1492216632f53e229cb2886c7fd09613d5990f4856f93f7ae0364da9bc/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3d7e1a6cc1977b24e01b5322888a5ba3f05b112a04a1646ba51f2c34c562a3d9", size = 2357823, upload-time = "2026-07-24T09:57:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/9b/cc/ae4adaaf343de00748fc3598402c1523e48db7094a9c1e0fa26177699440/pydantic_monty-0.0.19-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4904785a34f71f59aa1eb6dc081bafd09e4caa2f028cd379aa3aa846b8a1c27d", size = 2497387, upload-time = "2026-07-24T09:57:01.686Z" }, + { url = "https://files.pythonhosted.org/packages/ec/09/eaef87ed6cbffed9c720dd3cb850e748b0aab11f5ec1ecffb56250973f7d/pydantic_monty-0.0.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce15a5326e24cf7045ec9c1456ddb92abd09df91708771c3ae5e72d8e6374c81", size = 2738391, upload-time = "2026-07-24T09:57:03.3Z" }, + { url = "https://files.pythonhosted.org/packages/63/af/58be6fd6ea87e27bd57435013ca1f63d645a0c990c2f23708bcdd048af24/pydantic_monty-0.0.19-cp312-cp312-win32.whl", hash = "sha256:600eb259415e8b2dfef4be38d030c945b3fbb4fb85e727cb97131e7329ab017f", size = 1908274, upload-time = "2026-07-24T09:57:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/20/b7/1cb54e43113cb69c40fb765cfee3be1c222d81153b432131f50508569aee/pydantic_monty-0.0.19-cp312-cp312-win_amd64.whl", hash = "sha256:2c98b1c99994f92ab487a762b107067ab70036f64231e05aa3f6d2b16018688e", size = 2111335, upload-time = "2026-07-24T09:57:06.614Z" }, + { url = "https://files.pythonhosted.org/packages/24/17/0926da051f34ccaa45bf528777dc99e5ea611669cdd7b715be1e086c1fe0/pydantic_monty-0.0.19-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:c01fb1162cf87dbf145b875450eabfde6b35b26f27ed63468398cb4c37732064", size = 2496669, upload-time = "2026-07-24T09:57:07.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/c38f79e24971b4d2205ee156df6e5c6ccdcc720152f54a956cc26e7488b0/pydantic_monty-0.0.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e97dc97de32410003fb5517b72ab47799b4546a3ab48a75e60260cf73734da76", size = 2234599, upload-time = "2026-07-24T09:57:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/89/fb2ed1677ad2c3e2801aa119fdc280d1c64f3480e1015811e0942c9a7d20/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:06318e6add780f66259067829ab16062ce7a556dba0884372a881881b4a7c3bf", size = 2305696, upload-time = "2026-07-24T09:57:10.97Z" }, + { url = "https://files.pythonhosted.org/packages/24/ec/e36ff1c2c97f46420d57680199e7ae2e1eb306d2cfda22be2448e53e7484/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:5a765a36141bbeb074d89b424d5488bed4e358c603c72606cc9d81ee44d83de2", size = 2007600, upload-time = "2026-07-24T09:57:12.333Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/b47670d3e28f99dc2f4c2686dc42233843dce1a607f3d3cc8a387f3b9b74/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:48f5ebc048779c854f993834586ba372df3b65500d1ed7f147023abc29aeb2c4", size = 2151382, upload-time = "2026-07-24T09:57:13.8Z" }, + { url = "https://files.pythonhosted.org/packages/9f/18/559d71fc66a769c22f6b2a8470515aa6e69a141ab617900fe28a806080b7/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2eb7502f93d8bb568d255fccca95e3b9daca05bb674b9c5323fcba14d088932c", size = 2302643, upload-time = "2026-07-24T09:57:15.42Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/171be42e2ec211bd01fe4be7b6de9ab0c346081edc33830f9f9b781341ab/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:608994600a839dd863940ac84810c48d378390a4fea8c77e5145feb84037e610", size = 2169103, upload-time = "2026-07-24T09:57:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/84/ce/8c47253c8f3f528f0fa2d87dde85623dc3f211189a372e2d69cb6ad896b1/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:13a652f9dffa6d25ea458fec83108f60f29682caf42cecef91955b5b8cb04365", size = 2358155, upload-time = "2026-07-24T09:57:18.51Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/239107b83bae3a20e4f5424f6c6f3f88bae1fd1e734603c298167985f8be/pydantic_monty-0.0.19-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6167c966db7d8d2fe03940f8da596de6dcd184dcf5cf6209f0a1a9af8792550d", size = 2500858, upload-time = "2026-07-24T09:57:19.878Z" }, + { url = "https://files.pythonhosted.org/packages/48/55/02a7059f20e7198e511ac0b88a3957a2c01b8991326359b7c1bb590a09b8/pydantic_monty-0.0.19-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3102b44307d04f41897ae51d4daeab4fc9b5bf84a78c036781b488876ef998c3", size = 2742212, upload-time = "2026-07-24T09:57:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/a8/28/b632fe0e8eeba3f2b4dbec6ebd4c9b569c20e9597007f2d0fbbf04101307/pydantic_monty-0.0.19-cp313-cp313-win32.whl", hash = "sha256:e43da52776796a894f40533a7e5a322e98d9aaf7d8f6fbb7dc21a0de60a93f41", size = 1908553, upload-time = "2026-07-24T09:57:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d3/b90872f017871339ceb03e70fa4915ef8682128a476a66adffedfff874d8/pydantic_monty-0.0.19-cp313-cp313-win_amd64.whl", hash = "sha256:fd8c195875f8f44d55bc7d1d53c4e43248b184b3ac803d8131c92d4cc05a1aef", size = 2111345, upload-time = "2026-07-24T09:57:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/eb/11/c2aed55502bfc9620837312f0e2fca7a3d4bc959824a66d81b72144d7256/pydantic_monty-0.0.19-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:2692dc4452937cf2200afd257297e9ac3ccff122b80aa7a69935e8275d684193", size = 2497017, upload-time = "2026-07-24T09:57:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d0/d4b44a81c71308109cfa642a24b803057615ca1609530c5e66c376780efe/pydantic_monty-0.0.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5a4717f829b35c4bc5d9f6f52a52d19b90729bd494bcb69133bd4c0afcab9c76", size = 2247468, upload-time = "2026-07-24T09:57:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/a9/89/52848dce3acbb1d58df34496db3c4718814cc39f6db01a5025bfdc12b530/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:99f7282213ad6ebf7daf149a88d1517e6328afbf6780180512b9de62f30d292b", size = 2306181, upload-time = "2026-07-24T09:57:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/90/05/f7791c79c7be2240c43a9287196ed49034f773e3f4158492f028e486ebc2/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:978788b1c56fa0c49927e0a35151f0a635ef2f8d947d16915541ff864d2112f5", size = 2008738, upload-time = "2026-07-24T09:57:30.423Z" }, + { url = "https://files.pythonhosted.org/packages/93/a1/d714258eeb2583acaab2035834acc54ac6baa65bac456f897d901bc0c03a/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:8b821cac39deeb1abb2994d5a65f117ce26e55c586d0f570d425ff469ff48e3c", size = 2152275, upload-time = "2026-07-24T09:57:31.725Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/cdb1fa761e992a489b07a17adb80c21bf99eabd1286ca62f9e73acda1f4b/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:b43c4ffa5651f0eca97458dc673d7952064ae5eb5b836d23967a7d41483bb8f4", size = 2303533, upload-time = "2026-07-24T09:57:33.131Z" }, + { url = "https://files.pythonhosted.org/packages/db/9b/e6685cf82521e68e0dcb94e0c97a1a20410b1266fbce7008c0caa3484039/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:4054610601358943a3dd740a8d59a6812cc682e94d6903cb72baadae1ef5d2ac", size = 2169585, upload-time = "2026-07-24T09:57:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/6f031a628d3de72d95bedbb18292ccf998d9a422aff58eedf37347a0b367/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0b2a34c320968a3cef3d933737e99c932f13c55667315128f366afdaeea2be04", size = 2375171, upload-time = "2026-07-24T09:57:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d3/4367bccdf2c06a977c0d5ddf816190d570a07199f011034df16d51c84723/pydantic_monty-0.0.19-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ffaf950f4284bb193a54f18fa4e3e8c225b5b52265813abffe492074e90c65fb", size = 2501475, upload-time = "2026-07-24T09:57:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/7a0e1d5c016848afc9a8605aa4f16e6960d68806e775865b986aacaf8f88/pydantic_monty-0.0.19-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:bb1e5ee6762f9494bfb0f4cc316ad3e9a8c7917e3c984a23eb2e2322d401e5cb", size = 2742547, upload-time = "2026-07-24T09:57:39.284Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/92f77cf088f1df72a5dbab109c8c0e82f7ddeb18e65835a8dffcf967bb4a/pydantic_monty-0.0.19-cp314-cp314-win32.whl", hash = "sha256:b68ef6503b39f2f014162e3d8e7f48b9722a43ceb7b6d7199dc6d545f4c37b34", size = 1907832, upload-time = "2026-07-24T09:57:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3e/85e1a914f81659ea25c34916fdef27fcda2b00323dafb76cf2a5321f7c11/pydantic_monty-0.0.19-cp314-cp314-win_amd64.whl", hash = "sha256:14ef37b43c5bf90966ca51bcad0fae892a3c2546cd151fadc039e0a25ca74073", size = 2123187, upload-time = "2026-07-24T09:57:42.352Z" }, +] + +[[package]] +name = "pydantic-monty-runtime" +version = "0.0.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/29/e44460fd934584ddecc6b8a8acddbc0605e86ea9d027910acdbf526c8f14/pydantic_monty_runtime-0.0.19.tar.gz", hash = "sha256:717e11349d7234575750cec881ac390961de02a40bd09f875dc5e097705b896b", size = 1322628, upload-time = "2026-07-24T10:00:14.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/11/a6f4e12982b2232b9036db334fbcfecbacf46b9acaf311f9c3110e431c53/pydantic_monty_runtime-0.0.19-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:be34905548e31237fc683f5a34986489c127727e8f65481e8c87c4ad0b3a4dc2", size = 9449108, upload-time = "2026-07-24T09:58:44.394Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a0/1e720b1bba457c6a1ab4fca20c571ae058238c7df3e1892b5efebad05a8b/pydantic_monty_runtime-0.0.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0f402f20672c1e25d4915de7e97023519461b9ffbedacd08c427d40daa77bdd6", size = 9735875, upload-time = "2026-07-24T09:58:46.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/97/4439b312d9463881630dd9d998d2c8fe2b8ef457b451202c71816e25688c/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ec0d25dfbe65b9a5dfe77ff86353e95a86774d786a1d496206163f828f072fd", size = 9171496, upload-time = "2026-07-24T09:58:49.504Z" }, + { url = "https://files.pythonhosted.org/packages/1e/67/fa7b99afe1917b89eec3b4b6375f468c76eeea8227dd48361b7f2db90d97/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:7ecc2f8eeabf6483908db4cc69b7d4c156a95675dcbdda2a7540a22ed904bfb2", size = 9565495, upload-time = "2026-07-24T09:58:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a6/83a9dceb8d9f5dffd9a082b60590bb61b0ac48dca19aa02847ebbab1ad46/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:fc1e508b9dc2cab64e27004d0f4ce44c7e1d255e85bafba390884fa07d696319", size = 10199598, upload-time = "2026-07-24T09:58:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/bafd9dfa9a9a0d26b9882053aaea2280d791225c1e3b33b4b48f23fd5f92/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:9e3f6e366f62e54d7bd0fb995f007f42d23c26eee560d57c04d75d5adde3b45c", size = 10355698, upload-time = "2026-07-24T09:58:56.713Z" }, + { url = "https://files.pythonhosted.org/packages/0a/00/ef55cdc9f5ade20475622bb8dba617efce00ef9da2b94b36a76172edadce/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:2ddf4f2d1d063f330bb54b2dc00f6d5bcfbdd3defdc5988b856d350f62160e26", size = 10197859, upload-time = "2026-07-24T09:58:59.158Z" }, + { url = "https://files.pythonhosted.org/packages/a8/67/d1c359658458cf97296fda4359bfc71de8c9f968fb3db68eb7ce00136d43/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ce2c9550c30a5c94b63dfe578243d0823511feeecb020723f5140f9737505410", size = 10661715, upload-time = "2026-07-24T09:59:01.576Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/9841d93f956bfbd0bbbac75edf42bb3b13b5bad7d25b09d9a221b5e54d71/pydantic_monty_runtime-0.0.19-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:05563e178b6be783de088abe4a25cef9cfbc04811f7c1e1873860252e711edac", size = 9143212, upload-time = "2026-07-24T09:59:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ce/0cd3643cc5051456b7baad6f16d85fb1d05daefd0556e4c82ea8f22cc9a2/pydantic_monty_runtime-0.0.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:de74aa4df47147d104bccdac4b39c1f3fd543091be3fd0156f77eeea3e483bc3", size = 9731590, upload-time = "2026-07-24T09:59:06.323Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5a/69f4eabec2538df364242395ab3ef77b30a124a0e4b461231589651a1e97/pydantic_monty_runtime-0.0.19-cp312-cp312-win32.whl", hash = "sha256:5208056d9e23d951768ba4b94df3caf7fd84bfe951f68ec4a1803eb03377bbeb", size = 9227834, upload-time = "2026-07-24T09:59:08.694Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4b/221a21f477aef0c488cbe1467111b0988658bc4a42cfc6b404201bc432af/pydantic_monty_runtime-0.0.19-cp312-cp312-win_amd64.whl", hash = "sha256:e4bba0c6024a3a8bd8c8a8cba25233a19cf686218e97afb4c059aa0c625a4b8b", size = 10941520, upload-time = "2026-07-24T09:59:10.959Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a8/1ba497c35eca33273f2144b8e78d94832cc33aec5f69d8bef56968a61933/pydantic_monty_runtime-0.0.19-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:84dd041652581335503af7c60ee7362a462d946a62e8c4a44a939984034d0d25", size = 9449108, upload-time = "2026-07-24T09:59:13.497Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6e/08c05c9729a35d6c9831bfd57d535bd1257f601d15b62936c27f1c0cfb47/pydantic_monty_runtime-0.0.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:263ab33dce2008ca6c83b4013b0280a98a1991d4071c55413d00b539940fe8aa", size = 9735874, upload-time = "2026-07-24T09:59:15.918Z" }, + { url = "https://files.pythonhosted.org/packages/c0/64/63fbdb4c069ceb2af6261fe5923864b5be309799087f16a5dccc96141c12/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:64f56d48097d8a158125534b20e8b22003c5e727082a303907da8e61aae0c7e3", size = 9171498, upload-time = "2026-07-24T09:59:18.799Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cf/82390fe7f0e1100662e6cac7a1c0de716ea4bf851a53aa2b0ef324fc4e3e/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:a4d80fb4598524cd5039a596f8e6900adb2a5a2da66d012a3734a2b441ad06c3", size = 9565494, upload-time = "2026-07-24T09:59:21.232Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/caa8bf32ba0c0e8ce31ef2773b1a1f60d688e0237cd396e48bbef9f7161f/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:1c5cd0b140c765772e0606a7047fbf95cc49d62d0d8b79b4f520dae0e38b3ba7", size = 10199597, upload-time = "2026-07-24T09:59:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/48/25/ae9bb52518b2ce8fe7509d6cad4f4916b4458b748fecb180bef2d78165e9/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:7d80975c6e792088285f49a91e26de483ef95e5bffc4939b02d4c5ea1059749b", size = 10355699, upload-time = "2026-07-24T09:59:26.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/0875b1239b8ea57e4ea6de421cd240fc5a88c02e381f88d858f00439b1e9/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:f826cb911f78fe4fddf09fa3f38518484041908d08de927fa7dd134c002a2c77", size = 10197858, upload-time = "2026-07-24T09:59:28.889Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/9365473fe31e53a6dc4d6fea406d5d8f3f03a9b7d84d1f29a9e6d664c862/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:fdf56c6cd8c6163737d1ce284f9acb00feff77b7e952f041dc281b94336c4fc9", size = 10661715, upload-time = "2026-07-24T09:59:31.498Z" }, + { url = "https://files.pythonhosted.org/packages/20/f9/8d85b8f77d4006a8d80517a0781c49e6ca026f68747f801b63298e64197e/pydantic_monty_runtime-0.0.19-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a675f165773af0a2e0209dd96014aa9e613115cdc6dad297a7bb96cf87f83315", size = 9143210, upload-time = "2026-07-24T09:59:33.843Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/ded73742ee9fa2160c711141c28ea2ee083f073019e89724049c5e67cd84/pydantic_monty_runtime-0.0.19-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9e42ab0287c0497d9d5529e8a53dd2f63f9d1f92f6592170baab894ea9956da6", size = 9731590, upload-time = "2026-07-24T09:59:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/4b/06/67be8320dc592b8c4caa8c4c1544c8ddf2760029d46843d078aa5a4cdb14/pydantic_monty_runtime-0.0.19-cp313-cp313-win32.whl", hash = "sha256:1f5ff1b9585e648304096705045fb6bd90d43b561568b0d373265e8d201b1234", size = 9227833, upload-time = "2026-07-24T09:59:39.022Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/bab34897974d83640f8773f03d2001142bc13e80e517bfce8c8c4a57157e/pydantic_monty_runtime-0.0.19-cp313-cp313-win_amd64.whl", hash = "sha256:7181a2153ff257fe34109685148167b6e8219d4acfe8f346115b58152fd26aa8", size = 10941519, upload-time = "2026-07-24T09:59:41.538Z" }, + { url = "https://files.pythonhosted.org/packages/63/8d/f46ac4778b2ac64183607bc63ecc783f16ca9981de30eb8ec9aa5e7132cd/pydantic_monty_runtime-0.0.19-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4cc92139b476469c0d7e5caa147d38c2929de39bf1724cb22bbab80297823963", size = 9449107, upload-time = "2026-07-24T09:59:44.139Z" }, + { url = "https://files.pythonhosted.org/packages/a5/14/34a7bb4630d1bac0d055049568ecc888a87954c176428bde5764a7ff6ed7/pydantic_monty_runtime-0.0.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a97e00bd46305f34a85f2e2e3ac4c92dbd3340e7af694aafb192ff58c4fb40e", size = 9735874, upload-time = "2026-07-24T09:59:46.546Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/977de0b258c0858f52b23bd32afbfdf8a8c4614daff5d0b7d5c86332ce6e/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:46ad28b1b3c41113c9e89373da18bcc883a24da371d4eeb9b32d3d194286f1a5", size = 9171497, upload-time = "2026-07-24T09:59:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/39/39/4374200ee4b938fc8b5026056f13bb677e15796bca1323a16210738431ad/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:dc61844187a32c2f9c2b69846c5d55679eb838b4f7e495b4d03509f89fbf41f9", size = 9565495, upload-time = "2026-07-24T09:59:51.149Z" }, + { url = "https://files.pythonhosted.org/packages/04/55/c4ee4b0a10610359e09cad9d327db19095914a3bf427fb3d8164ec2bcae0/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:b23b52a79ee6be0a943e4b8e5d996e6c489a37b23a337838164f22c9e8ed11a7", size = 10199598, upload-time = "2026-07-24T09:59:53.619Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/cc9df084e9f930bbb2873b6dd832b377f76071aec309b1545589d818fd90/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:484496817f5f238c42aaea8a1945b545a17d8ef2a21e9e81799d55e481d25485", size = 10355699, upload-time = "2026-07-24T09:59:56.043Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/bb13e6f655fcaee340032ad6a3cd1524957d0fa471ddc7e27bdb1f4c240b/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:c3e7cbad58ae9bd3402581faa7d54d719f9c140dc1888f5c9439d45bff528ea1", size = 10197859, upload-time = "2026-07-24T09:59:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/c532715987383668ee835337e1485f51585bc8bf189f033370a05eff17f1/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5534067dc7ffdae809293da95d0e95b6d8481f4c88aff59385e19f466ba3c0f0", size = 10661715, upload-time = "2026-07-24T10:00:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/9a0ab28a061efc184398a0b4db34e459572bb2316cf766f0d6ce65b475e3/pydantic_monty_runtime-0.0.19-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c1ae32b06a4456ab223bafa54d29a349066d625d09063c28ba14ee9019f9b7c2", size = 9143211, upload-time = "2026-07-24T10:00:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/23/5f/af5b3e6395834572975d98f4d1a00a57ee8029bf68ca5732347550f32b35/pydantic_monty_runtime-0.0.19-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:497cf8c3f30992f9aafc8707084eaffe2391b7b5dec067d04d5715f9a562c56b", size = 9731591, upload-time = "2026-07-24T10:00:06.351Z" }, + { url = "https://files.pythonhosted.org/packages/1a/98/96797fd269342cfdb49f03c22fd9a05ef91f711089081c4b3861b9c520e2/pydantic_monty_runtime-0.0.19-cp314-cp314-win32.whl", hash = "sha256:942feb948df8edb61ae7ba6ae77dc655e6985be06d72eef886562fe573ba3086", size = 9227832, upload-time = "2026-07-24T10:00:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/39/fc/02d15281c8e00b48df9af8f75a4fe06f3f8f33ef6a910507a45a19f2b61b/pydantic_monty_runtime-0.0.19-cp314-cp314-win_amd64.whl", hash = "sha256:91d93339c70483ed9256b3b15e3375f6597ae65be280f9b89ba9ca0355f95f54", size = 10941519, upload-time = "2026-07-24T10:00:11.226Z" }, ] [[package]] From 5c8df37af10aa10f96fc1e72c4e41091695f8384 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Sat, 25 Jul 2026 10:41:27 +0300 Subject: [PATCH 2/8] Support open()/with in the analysis sandbox Document files can be read with open() and with-blocks (.read(), .readline(), .readlines()); writes raise PermissionError. File objects remain non-iterable and the collections module is still unavailable. --- CHANGELOG.md | 4 ++ .../rag/capabilities/instructions/analysis.md | 6 +- .../TestSandboxVFS.test_open_read.yaml | 42 +++++++++++ .../TestSandboxVFS.test_open_readlines.yaml | 42 +++++++++++ ...TestSandboxVFS.test_open_write_denied.yaml | 42 +++++++++++ tests/sandbox/test_sandbox.py | 71 +++++++++++++++++++ 6 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 tests/cassettes/test_sandbox/TestSandboxVFS.test_open_read.yaml create mode 100644 tests/cassettes/test_sandbox/TestSandboxVFS.test_open_readlines.yaml create mode 100644 tests/cassettes/test_sandbox/TestSandboxVFS.test_open_write_denied.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index e75d8907..f40fd167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Added + +- Analysis sandbox supports `open()` and `with` blocks for reading document files, including `.read()`, `.readline()`, and `.readlines()`. + ### Changed - Require `pydantic-ai-slim>=2.18,<3`. diff --git a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md index 92cbf702..fb4b00a8 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md +++ b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md @@ -17,7 +17,7 @@ Inside the code, these functions are available (use `await`): - `await list_documents()` → list of dicts with keys: id, title, uri, created_at Available modules: `json`, `re`, `math`, `pathlib` -Not supported: class definitions, generators/yield, match statements, decorators, `with` statements +Not supported: class definitions, generators/yield, match statements, decorators, `collections` ### analysis_search Search the knowledge base directly (outside code execution). Each result has a `Type:` (paragraph, table, code, list_item, picture). When the Type is `picture`, the corresponding figure may also be attached to the tool response as an image alongside the text — use it directly to answer questions about figures, diagrams, charts, screenshots. @@ -48,7 +48,7 @@ All documents are mounted as a virtual filesystem at `/documents/`: `{document_id}` is an internal identifier, not the user-facing `uri` (filename, URL, etc.). When you only know a document by its URI or title, use `await list_documents()` to enumerate ids and match against `uri` / `title` — that's a single call to the host. Iterating `/documents/` and reading every `metadata.json` works too but is much slower on portal-scale corpora. ### Reading files -Always use `Path.read_text()` — do NOT use `open()` or `with` statements (they are not supported). +Read with `Path.read_text()` or `open()` (including `with` blocks); file objects support `.read()`, `.readline()`, and `.readlines()`. File objects are NOT iterable — do not write `for line in f`; use `.readlines()` or `text.split(chr(10))` for line-wise processing. Files are read-only; writing raises `PermissionError`. ```python from pathlib import Path @@ -112,6 +112,6 @@ You MUST call `analysis_cite` with at least one chunk ID before producing your f - Use `print()` to output results — the output is your only feedback - When you write code, execute it — don't describe what code would do. But not every question needs code; simple lookups are best answered by `analysis_search → analysis_cite`. - Use `await` for all async functions inside `analysis_execute_code` (`search`, `list_documents`) -- Use `Path.read_text()` to read files — do NOT use `open()`, `with` statements, or `collections` module +- Read files with `Path.read_text()` or `open()`/`with`; file objects are not iterable (no `for line in f`) and the `collections` module is unavailable - Do NOT include chunk IDs or UUIDs in your answer text — your answer should read naturally. Use the `analysis_cite` tool separately to register citations. `cite{...}` markdown-style inline references do nothing; only an actual `analysis_cite` tool call registers a citation. - **Before you write your final answer, invoke the `analysis_cite` tool with the supporting chunk_ids.** This is the last tool call before answering whenever your answer draws on retrieved evidence. diff --git a/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_read.yaml b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_read.yaml new file mode 100644 index 00000000..67dccd0c --- /dev/null +++ b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_read.yaml @@ -0,0 +1,42 @@ +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/cassettes/test_sandbox/TestSandboxVFS.test_open_readlines.yaml b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_readlines.yaml new file mode 100644 index 00000000..634000ab --- /dev/null +++ b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_readlines.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '114' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - The quick brown fox jumps over the lazy dog. + 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: 11 + total_tokens: 11 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_write_denied.yaml b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_write_denied.yaml new file mode 100644 index 00000000..67dccd0c --- /dev/null +++ b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_write_denied.yaml @@ -0,0 +1,42 @@ +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: 8rMKubdpLD0ZUI28QtHAvBo9Wbqysxw9izhkPRjPjLyJA9s8wj/ZO6A8tLxQ1Jc72j+ZO2uDG71eV5U8F3StOodmFz2BTuW8093/vDC0ALyHc8K8Fc7MOhwBCDyBf0o73NjTPLozRbxfl9e8Pbt8O+WGkTy1eRK8ChwUvYfBAr0q9Qs9CqQ7vDI4aDsYKrC8U0AIO5kVqbvKFEo7t6eOvDi+yDta7BO9pDzVPHLzgDpne9C8yKMWu3Q6tDsTZJw84z4cvUwWB71SL5w70rBOPE1lZjxbQ7K8hNLuPI/HVLvuMGA9jugHvL+mt7psQxS708U/vA5QA71//I67nSdnvCzJIruXx5+8PWhTvIfvcTp2m348zGsBvWiZvbuGhhU9vq3EvMYvXTuQo+w75aPtvHrrGrxiFsw8n6rAPNhGHzzPqOg8naKdPFG/EDs7YSo9JV8/PcObgjuc5hw96NafO3wCqrxTd7g8EhdVPCNlJTyIxr677fHvu/yRurrHBiI82/SRvGGcjrxe/TW8sxebPMIip7tXP7m8T37TPAPPgbxjVR28IboEvVu0l7yiRl68kP54u4I/H7xOp4a7m/SJPGaq7DxbTt+82DZUu3Lc2LvaA9Y8WNNcPcNu9ztF7QU9gdXju5pVOjw8sCG8LrC8PJxq0jvasta8G0d0vNNz0LxrQmq8Ma8oPPqZvjzloY68Jwb3PPa7JzqJnY88PBJXPLuvdjuWtGC8gDYjvIX9WjwypgC8eOujuyAmAztx/xo89EzovEtsnLtEvkO8vqVVPHQrTzyTPQ885778PB9o5buz1+s7PPfePM9EDDyXP948S4zXvHBylTviAjU7SStaPDBwzTuFUeA8zNUUPCG7QDwwcpk83mTyO5AkD71Kpoy8YE32uqRRjrzsuIG83KW8vDcMqjkjWIC8uVNRvIcubbrEjRW9TFW/O4g5B7wPEbQ6IoDCO6pvG7z9a4I8RqZCO6pO7DzOOMG7PvQRuxFZXDzGV4+5EDOqOx0cvLysj4k8Jm4VOlcCF7x3e2A7bQ1bvHbzXLze6go842KovNJ8Bj0qtRY7jiyMvHnUIbwCCUe8TKJXPPpKK7xnq8i7kRfFvBDrtDsAB7A7tvy3PIcRMDuwLZW8+EGovKNSmjvvwjq8xeKwvCF/rrxAXxw9HCUDPbf5wrkpYB48O/X5u9w2fjsxtse8lCUIPABEBjxFsEg8ziCXO6F0hLwYPvI83mbOujMsMDzHfI28+2u8u1gFSTznbI88riw2u5RFIbxNaUS8FyomvWzvx7zyn5e8e9iFPChWAjv0frG8jrsAO9UdlLui3Ae8By3LvI0Kmrs+b5S6lE0uPPrUJLyXmFO8VQMQvJIFm7yd5We82E2YvL2dBD2upOg6+APaO1Ayq7p0Jv27X0UHO3RZcrz3DMM8sHfWuzUNgjwECMW8P1lZPcvdoLuE7oY8emV7PEPzhjpLKsq7K0UbOwOxOLwJxag7cCfkPHq9ZrxSlMO7HzZkvCf9kzskMJ44bo8uurSeXrt3SM88WNnHvDiOVDxBRZU8ctYMvWxk0jxrO4+8hjrZuvxdrzwRfuO4RMDcvIjtSbzS4eq8S26QvDJGXrzHviW7VQuMPKKnpTzu1Rg9wcY/O2NcVTz9AQS9S3a+vD7isry4uxK7EbgFO0Kd4TuHWL08OeFBvTqmibvR8L25532YvBAAAr2M1iM57x3WvCiC27vRExq5AiVavBGOUjyANgA88PEvPOBcrbyxHzI9BEtoPJ99tjxtZQW9ISyRvCDzmbvvpxi7nUKKu0TKDz2DJrk8YxlhPGtfRbwuJkm8g8KKPHM7FrzAPrm8Wsw+O1V1RjxdXc27kzJevPrfxbyVXRK7SgmevPpCx7ztE9S7IRZfuns0sjzTkOC7bG3dPNH3vjxJvO68axpKvFKAiDpiJwY8CDKtPBuo+Lv36VA7psvMuw/6Iz18Wjy95lFQvCqT8TgpQX081BsRPLvIU7yGdrs8D7qbvCtOBz0wHgG9sLWGvNcjpbv6YmE8Zg+8OkN+g7zXjnk6fZyZu3TOWDuZwMq8EQ1UPCK45Duq4uw7a3vSPOKcsLvBqdw7m+DUPMYHD7z1Vok8wEtVPKBUijxnK2Y9AUAfvEFcv7w68Yq72i8ZvbzwdryvNp885z1CuyUBm7wG4Yo9bTcqPODPKrwjJbW8/DOEPBJdejuHbl2802+JO+4RWjx5jbY85Gbvu3Srnbwz74o8ZXLyun0CCrwqIxY8VCU8O1NtdzwFG5M8yV2+u8q1rzxj78688e4CvVerfLxnJIC8jzaIPFrXUT3ELT+8hL2MPOUbTLyzH2a8477tOnVnjbyjQFC89frou+JnyTypHlo8T1OMOmKNkzuvKUM8JzeJvC75IbpTDUS9hKMGvYiSOzxuxkI8sqafu+WwCb1phh881rLAO1DVo7zGzqK8/eizOk+As73G5JM6uDTSuzvKxryDvfU7b7LqvE6yprwffFC88YNcPZWkAz0/Nim9Cl+svFDUw7zMpOE8R9rCvK0+gLuR7/I7fpL0OyXgkjztlRS8Ue+APHmgdjv1VF68YvGsu0rvKbsriUc8UWKWPFVI0DyP67U7grCGu83HdzwztCg6zsu4u5fCs7wFvDs8aMMQPNdaDDwfB8e8MAzquK8B77oKBG27mLUIPYmYCTxgSsq8gsiOvBQcjDw5MO48hBfyvNwirDvgDRw9XvjHOS6/hjx17cw8AIvdvHQN6zzg3OW7bh8EvT4c4Lzf3F27UJ3Eu7HTwbx2Jze8JfeFPGFC8LyZViC96IMbPXC0mjwr5+G8qBdwvCJzBLwny6e8yNOxPOBDhrohWd67JafkvLd/4branAE9rk3tPKWXnTwZ6eO7vWe+PDuRZDrMloQ7IjHdO95IdDxK3fG84T68PNhtLjxOOoq8flr+PF8SEzwmpKy8B2EfO5H7+rzjbYw6StC9PJIyP7yfUDq7x7FwuhMsGbrPTrO5v5ZRPCxRILtFfOc7joG1PEzv3Lz8zNO89eZtvLTmBb1+zZm8atkNPHh4Hz3YpEQ80PGCPCFS97zTqS+8oWTXPDRShrwiCTW9zdIuPA3ipTuXeMM8U+R9PFLGErydb+o6A7AVPM0itDyRqI06JkZsPPGEwLw/6Xa7FV2UO1RG+Tqt+jq9i47uPG4PrLyLE5G7h/G4vFQ2UjxR1O87PDGhPW2ae7xu1Q+9LyQDPEeTNLvEuAW8jXtfPAU5rDyT8g+9k6sKPRHv5bz2/QK9BculvMcN9jy7jLs8MIeVvArquLwopRG9xbN/vMTDrrwuoBK9Z3sdvJl4GLoiXve74Jg5PJNxkr1SFr08PAP/O8AQ5jurJv285n69vDI6u7ld90U9B1ygO5hUCjsnPAe8qF8DvZp5Kz3+HNs7Rw0ZvfNd5zyF8ei5C7SyOzoFpDvTEHg8RGfMu6YqX7welTG8QYHmu2gsAT2hSge8iDXeusTcPbyWqXQ7WGOxPDL9ajt+1zc9/eYAPB5KXDyhAb85FvxLPC+cgryZLbc8vkS6PIFXdLsrPu08o9rDvJ4ez7vlbvY8rz1vPNgTeTxC4gw9pH9ivIuDW7y56Qk8AmvzvDyHXrxqCEa8lg+Qu077QzsD+Ta8OcD+uzK2wzz2zD29aFDQOsgD3LtnGRy8W7LjPEsR9DvfdV88W+D9O6KZojqmBv+8V9XsPHqm/rsWM9s73W44PDQYizx/UrC8BaPNvA3KqbzqvOQ8Dpx4PLsmHzxEhaq7xwMIPXhJhTxJZZM8KNEXPDSOAj2k+Gs7qHwTvd46n7yj6W47iudYvLeVt7yn6BS93xdjvE4KrzyVuH+84lVsvPPgGjxTnSe9FptEvOfzhby1pWQ7TSuTPB/hSrw5haW8/vlSPdHR2jz6zh88mLyqvDWuIDzeZzS9pLQVPMqeUrxy0wO8FmbAO2h3hDvl1zG8qvMsvPOVhjzLPio8ftPkvOYtCz2T3Aa9KAYpvUYBhTxTR9E8dC3uPJlY7jfEAQi8byK1vNrnmrzem+I89sHWOw48fDur9AY9PRoCPB+RLTuRFeW64M4HuxiihrxpjjO8ukGCvE8CljyJxEa8W4D7vCTcqTxm9sC8LIvIPEcBL7yP1Ta8k7l2OynsyDu/dF+8vFTdvPdbcTwlLmK8rI1Zu8uLA706iWE8MxHkOou09bzXa8A7mSfNvPHkzLzslrS54+lXveuO6jz1g8Q8S4OUvPYswLwtfMI7+5WNvLpfhbslP228iXcnvQAEWzsS6287AqqtvEwclbx7mGI946JrPLhlaDz6Qzc7Z+EoPJUI0DuDgJA8k7f2OcS447wSNLC8j121PHsmPbyRyEy8lJAbPeWzkby/b7K6IrXiPCrJGLy26ho93smzvGLuGLzAV6Y82uZpPIEEojyii0i8w/RqO373uDznHAk9+CbWOy7e/TutPUo8aMoOvGqamrxAaMe7lxw8vd9CJL28jyC8v+hFPDRKG7xxIgU8aFOWvFosALwefiK8tfH1u1YVkzofqbG7NEwmPGDzzrjkOWA9sRnmOtGeZbsEpam8hHQ1O4Dy9jxk0CQ8yp7YO8k2J7wU0r08r5v5vFdRmbuv7/q8CbtFPC6PxDvTEDu8T3lIvEkQGjx4koW8DyVUPPfQ1DxnNL48cwAfvBdqozwqewm7RkgGPHhmO7zma467PjE/vBHgPTzD4yg71vRRPdUEFjwB8fG8Io35PObJTDxajcG6kXJYu7AU8LqEn1W8sU0Zu7NhK72GVI68c03su7bnCDyY1Q49AgHtvHKOjTxP9268VQy1vLA+NTx1bPg62dGxO6+5gryVioo8hWVMvOGVkrxUVg88kGZBvH1rR7qHrAC9CznvOr6jYjwfaQ+9RkMEPLTPuzvfZI083HTFvKSvyLzHxM88yrqpvI+1E73hMki8KWR2PezaJL3CG328JtYnPH6E9LyljPu7RQS6vK/zlryVyGS7mUMuvGqV1rupKi28AasyvA1cILsXaO886J+9uyxrszupYWc8MdLduvcsUD3+Yhe9jyIlPGqzPjykYF67Hw1SO9K4hjx2JJC6AJuCPP8MGbxE7WU8MZyAu5/xTLxelda7ep1WPJCW8zp0gKO7HoAHvIrt0DutOso7Hb+jO2G0nzz621Q75c6fOtPZG7yiTyA7Iu/CvBLqzDrWig69NkuGOz/V9zvR2c+6EawIPWKPfDtbBT49id4UPcxSEz2MWA+90mwJvF6aNDwmkjS8scOevOKdAL28noS8fPhvuxxqeTya8fc7GvHwOLpv7TryHaQ71im7u3cJLTufvO4890oJvQFj7Tzwwao8QVL4OwmIJb2S3qM8w9sEPGHDsTqEJAY8OrBEvb/9xzyvNaS8/sPYPEAn5jri9L+8KAfQu5w6arx19BW7KS4zPFtdBzxPVGq7j+5bPExtLL34+80890IavZujj7uIo1o8zeAWvCVDiLtW2X68+ZH7PAiQ5zsfy7O8M4SmPJJJPbzEWyY8SJcpPBYQDjwaAXw8GBt1PEc4oLy6UfA71DZUO/ohQTwvEqO8RbOou7USybxFLpG8WtNkvGQGBbstRra6uoecO3S1+7xBGyC8/mqaPNrggjsqcQY8E5OHPJqRNDzykU88CEkEvBgqobu45zw8Gde5PNOZwzuVOXC8mYe5u+CXizx1dAO7WAyEvGMMkLvB7y29Eg57umfeFjwiRye9MZLNOkVVzbsAXA0838XXPI2weryXi588WimnvObZobuP2m27ZVUkvcXFabwekWq8IiVKu7t06ruMrI6893vzPAhSZzv/TXc6OFXZPOcSPTz19BW8ILK4vEuT3Do9heU8lUfWu4cQ3Ltam4w8N+zlPGbvbbwbWi08gGcYPMTQCrzufOi8DlWlvBg/Izs9Dg888mijPNA5kzu2NEE9kHOZvIwpQDy38Di6Tzd/POQqpbwvBBi7grNbvHtMGzwx02S8+fXguzHCsLx/Sps8ZB9Tu+HjCj2k6jK8RjKBuzDxRzyME6O8spX/vF5SnDqKbf+7pS0NPQJuajyNnQs6FduGPTnbYTxHn1C8aUKmPOj7ED1/Dg06L9xNu1ffPTzNpUm8WfP3vAVxOrslhvo7EiGZu09UcDz8RMQ75x6TPFrFCb1Ngxi8K/W9O32+n7qqC3u8mO1gO2rw5zsmDM48e8jeOmwwbzyLUoS8tBLQO/QZjzxMf6M8UgguvW/P0DyXu/U60VgHu7HlyDuzR+q82NLmOmHprjqSBrg8DPU8O9SFo7ycEAQ9MsksPYSjvzlH3zI8BK0VvcGBQrsKqOE5n2ZNvK4uwbzKwr+7Qil/PAEOO7tAZxO85eauvLlYbTsMNNY8wVG/vIsogDuEgv+6CA06PKc+trzneTU878ywvMt2gLsRZDC9UTVGO8nlVb3IZdo7DReCvNnm0DtuLQ48qdEyPNVJSDxylCs8Lu8rPbyImryB7aW8UVcxur0Ltjx2wsG8B+TUO6c7tzyPBSo8FIpCPCAgVry17dw7qn0uPDoubzy4E8m8WRgTvGKr9TwEpR+7HXwtPZyLzDwpa1K8uWdsPFjrMTxVt3Q8goCYPNkgLr3OT3y7KSYEvR9HGL1TTsI81qUbPfm9yDpW1x483vgyu1ZLHj2o0WE9BQ9HOyt9P7woebk7AKOMPLrfkTyHx/K7qI4GPO+jJzyx3AW8aU6TPAtfoLxXpEu6RblvvN4X8DvctPA8iuMjvCtHzbt9n3e8SdNrvP8CJ7zc9UW7tPPgPLYUi7odlBu69UBUO9AJnTwzZOE8r6ftu/7CEDyD7r28Nh/AvI3rOrwdEzm9D02mur0LorwM26g8FssIPSXXp7smJEc8jHsAvaHxhrxgyNa83YIHPTZbBLztVha9Vf25PKp7WbzLhui8p3APvZEpcbxgqTO9M5R3PDbzObzd7bi8lGS9vIbNfDvdSp+8XMUnPBDHELwGJG28Uo/FvI7k1jwIgkG6iSi+vLtSUrxveRa7B8+3OoyDfbyYz2W8VB0VPTQGijp8lyG8u/o5vB50ebyJOS28/gdSvBRVtry8fVU8Zu+pO5zIpTwqGOe8eBOwux3d+bzqUS05jVTfunC0PbyTLDQ8QUJaPIvguzxGXTK8DzS2u5FZpTxjcj673F4SPVZUgbygyGe8X3kTPL/fibzzfYg8U5yAPHHUpTwJxgi9GMzRPIkkmzw93wO9grh/POT/ELySvOK5q8WpOxpFxTyfn0K8qRtlPHZwFDzdMoU8/n6+OwoisTw8rRm9/gWCPHnjlLsNApO8hPKAPMFjQDzm9So81Xn3O3R4vjwnQFi88bEkPS7PiLy5TpO7MH1TPL7WJbzlh5y8i2YEO4tNlbxpr0C8umSSOzjngLzNiwa8Z1OMPK0+mzyzHds7ssIOvRELoTwpnMI84AYJPYXnQbv5AJC8/h0ZvbWLmDzwEqy7oWrfu14/Ez18pTu8n7w4vPsCELvP/lC6lE+UPEn4Bbz4xK08UyifO0PEbrydV3c8SHUfuj7g6rxXrEW9de5zuUflPj0TDoy8b7oAverUkLx5RLq7whGSulWDlLzwpVe8YjD5u1lDwjw4phO7lMu2vFM6B7zvB667HFYEvMBXIjwN5kY86ohHvJgcWbw6lsM8+mKRvDeJULkhyQa8HM0FPGzpfTsNt5E8ECS4PAGrJj3ym4Q8q6j8vNSg6bzvj9w8qmx/uhVjM7yCWxC9NiGdu2+Kt7vyOXo8BBgZPHTi6rxUCzY8cbfWPOGXiDsiSgI9QD4YPMNKHT1djd67DKuJOlazmbuScoA8VKVsO9qYTjzTUWu7g4BePXQGoLvbrni8qY0XO8o7Rzt43C69bOmlvJ9IUDx+M6K7u1uSvOuFOLoyf189RZXSPFNNITxB3Sq8dfamvDe/qDnHjSE9fHcrPDvSVTzCKzu9LKwxu46bV7puFp28wolzPPvRgTzq4QE90UZavFsZIbwqP5i6OsYOPW9Jk7wNpLq8H9xfO8JS3Tv0aza992JAvCZKybqn6Pm8iApPPN5TlLwAF/e8abnLPOGVjryvC9w7kIo4OyA46LtJBHS7g40DPOW5+TwC9py8brlfPHAgCLxVE5I8ChyfvMo+ZLsBwQc8lhkYvST3KTxnmNw7HYbhO6Y5Yry6dYY8DQhQPKTJFzyMkkI8cbCLuyJqTbzwSJ88hcGevHpFw7yjHn48WB4VO5xcKDxuoYg5nYhpvBF4dbxHKj08U6qZPHtVsjyW3WA89GuMPIs6OzyePnw82JhXu3f/xLuCDEm8Mb7MPJFEUTuqRVy7AhA7vcuqEz1dSnU8HaxmPGSWpbwpuAI89zikvDeGST3EEKa8U83ZOMJpFTrhdNK7ZEv0uQxrIj2QTzi7rIg3u2nsG72KCEY7OzG+PG7VPj0GzeK7PZo9vCRN9Dwe/u471aT9u0smrjxQUsk8DGukuyp/bjujdck7iR0KOuEvVzzeTLq7g7SOPJv3ejtTism8YJAcPJ6oEbvUQjo8tc7VvEAadrv8FHe8x7RTu4bX8juE6y65oDVjPEdmGToV4PO8x5ROvLhWJz3gz129uOaJvE7gjzzzRJa8xRTNvABKLDu/BGQ82mBlu2KeiDxLYtO7BFARPK4R2rwtzk48gIc+PEGIxDwc9oe8ofogPS/TFzzbuXy8MgzVvMV/5Dw/JEs9UHKru/w87zvHITK9PbnhvAj4pDxsq/Q7qVsAPax2lry9Mru8tzs8u7jyJb22KAQ9PEoFvCSA6rtsmbY7hO2Fu3nfnTpuey69PnQyvIe/mby39LC8JL7OO5EtEjwkAoU8KgQzPYfn2joWjHg7XXk5OvfH6jshobI82t6AvPXHZ7nIfIu8tjQ3O12dn7zakVu8FlI+PVnO7TxUy6+8TSkEvEReB7xxozi6J0+3vFVkAT2z2Ze75sn0vJ9jQj3tbkE81zI9PEPuE71GFiK968l8PFXua73zfb68EzEVvJ2LwLt0yrq8TS0JPR2xTTxqHGS97IPaOwj7rTzjpDW8fdkBvFkdK7wusi68G3vavEzwGT3PM+q6GjHQPPItMLzklaq8VX/ZvAZ8+js4kQK9t5erO6y3sju0tz08QuwnPQX69jy3R5u8Vo1iuwGEHL39Jzw8zDxxu3MpHzywgIi8K+zxvNlRi7s9tI48LZo0vdKXrbzlrZc8Cnx6PDZcmju6krU89PbtvOpemrrBPqU6EqW7PKG5KT2Daes8L98ePPr1pTx614A8HV+HO3qEBDySVva8FpKjPNsQu7z5mX467iW8vPz5hrzlZAO8/4ACvVb0KT3Umea8msnQvOcTNDySXiC8s6T7O061fjwAwTs8l/SAvLTPzLvykA497WqAuvLv0byfKu28Tcy5PJrR6TyEl3O7BuaxPKQjBzw2rEu8cPQCvE8iY7yghEy9AewLvXQ1fjvHAfG8MPqqPCtrjLwmewW82gv0u1Fh0zw0Tgk8NDDAO2H5MTu/QGG81Tx6vHzJ4zwbyI2817CovGU1bLsMuzM8h4vrvADXkbrLQRE832/zPIwchjzRPS46+3mbPAgRkrwYIwu9FXuTvIc+tjt3qJO8AYP8uSD/4jyytbO8M/FIPTU+kryEcP88gsxqO3T78rytmOQ75qgAvKuJQD1HAJW7F5RIvNwSmjuLxe279D4SvHWBJD3ZtLU8zzN0PC083DuaKS85eCeuuqcLRzxQak489XcSvW3MxboegOG8ivWLvH7KAr3jJtC8WKQjvcMjC70WdpS8iGq7vCzZo7tRjKs8oROEvLDCBTx5A169vjcEvMg4SDt8I368+8zcOz0Rj7yT8jw8ub6ovBw+2DxETTC9rBzQuvy7gbrYuSe99hOEPIL3gbziwM87y9KvvBupWbkebsc7s+VSvGV8nrz9RIe85QHhu36Q0LwEA8A6M02POw7jtDsqqjW7ZbD9vP16GLzyJrC7hS9kPD9zRzuh1FS8JUG5PB6NvTyKlja9hlifvDIShroqHq86Q63JOzJ9HTwrpwO85vjwPNEUpjyus8Y8kfQOPZENLbw7sBc7r/kHPbIzET278+88H93sPBzJ8Dv6EbA8Y9zmPFWqnLuqWc67rv08OGNpST3GFqY8vzZovIogqTxEzK880ibeu8FP2Dr2f4+5e5FVvGSTH7tBIKW8KqD9vMPxvbn3U3C6XsOCu8pHwzuzBLa8b8Z5vCIXID2hO9e6S7MIPMp1wTsUrAO97NJdvN5YvzsScA+7jzv4u6IHdTxEYtK8bO2gOrsFHT3/opQ7KvONOqDa7bv4GfY8QWczPFINj7wRG4S7HhpavNlJ7jt+sfm8TqYdvEWJzLsfb887zsCUPMuo8Txhx808E4M8PK8hwzk8PME7dc4+PRdO1LtNUgI8O6YaPScYG71ZUzo8apO3u7aBlDyE7JO6g0wFvYoqHL1DXiw8EihrOzlrBL0VT0I8a/4yvJJWmTtHG9473H7KO7/Qk7uNfEc7e9kRvdl/zrx3Tpq8L5ObPFloeDwPtPm80CFYOnxkMrxqzum7CQ+ivPJ26zvoBBA7uoYDPR+ccjwcxMe81byRPI9l2Lvwgpe99B0KPQ2PZLxnsza88PEmPO94ozwKaLy8YbxDPdNNtLzDe7c6Am0SPT7rljmhXwy9deFDvKjM5DvrPCa8jePqvBF5zTx2lHI81nnuu1l/jjo58Re9yjbOPIxKJT0KDgU8jRkWvKz2WLvHCP68Jz/TPAn1ajxro5Y7NQC5uye66Lyjm8o7EEhhvOhLZL2ND4O87NRnPF2hiDy994q8G7uAvBlzWDoWW/47pjNRvLQDzbsp8Z47FqVCvUuekTzQKeu74Y2mPHffRjtR4r48+nVxu9KkkryXP+k8FJl1PDYG9LytlAS9iRJQu41sEryigaK87GUQu8mlDTwYA8m7/syvO9gKQjo9dfe8uyRNO/SLQ7vadZ87+szpu0F/n7pIS/M8lYTvOyaDWTx2NKQ70vn7vLFnDT14WAU8af09OvepBj3qMAG9ANvmOonyYDySUyy9RLQYPDXkz7vEXuG8l9Oeuwv7DD2ttD28Bb4zvRScqbylkyG9/xLHuyALory20F28nK1Qu4DlAD28HZS8m4HlvGXHnbx+lT28GlsTvCcQOTyeDgO9oqZyvOI3kDzc0BA8KMDDuzu7Db2lyrK7ISibPHgvOrq4j4Q7HrEFPSq+frxBk0c8BmKhO1QrjrwQzaK8s40VPAc5qjvL3A672TutOjuevTx1Qls8EE3ZOzLy2zzNP+y7OUERvHLeWruCxuO8AW7qu565SzzH1+o7B+yrPGgtl7yCxMW7YzRtPD7ctjvOKWE8yKXMvHFHury3jyI9EnM5PAqMrrvEHIk8flasO35SArzqojo7/zg9O2o7wLvgu9A8FpsWvWiukzx5bHu8fM7au4ZhJ7te9J68UQfmPMTIdjzPTOC8yW7wu5mNPD0ooB06mVA1u4g7Jz1WdYy8FrAPPbE/qzxYmZo8eKyGO+XimrybNgE8YCkvOxoPw7zrK4Q8vOI1vK4rHDub+EU81zxEu81Z2rsXp5u7uQ+ovKubuLmvG4O8dQLBu0i33Ly1eqO7TJMVvb+wjDu8Aoe8vdf1OwQFh7obz4E8kSV1vBbzrbxEoNk6hSG7vC9y1rsmdpW8Oqicu+U1GL1vr148lT2mu9LTVDu3TWs7dTp3PPi3PjyyAOU8VeNAvKx4q7wZ4lS76lB8vHUIHjs59jk7qzS9PLnMfryyNkq9laCovEdwpryVeoc88v4ivRNOArynP0A76wm/vK44qzwGGAO9nJ2gO8CnH7wFIc+5fciCvCqy8LyMxPI85UkIvSnIozzpcrw8pL9Xul1A3rpHS2G7LAjZPIIpDTx3GK86Fm0AvBpaXbtVTdA8DwCXPAKxtzrWOWY8zwZPusBlKrxU2Ey8GBraO4q9mLymD7Q8smcSPQwB4bz6B5+7cdmsOqmVtzyDKHC7IyBqvOWw1rzY6O288bcrvLMeNbwFOFE88oruu1i8pbynvPI8iH/yvJ6KgLw/Adq8g4tZurSPM7skb827E560vI/injvMbqo8xRCMvOkWpbuqMB08l8VYPJiK4TxW4qW8hc6ju3YSirqsSKS86DLNvE7ZKr2s+Ju8mqshOyk3Db2K18I71NTYPE5GMj2J95u8MkoePJPDf7sJ3TO7PTDSPJIIDrzQ4fw8Qq40PHzYWDtMRDO86gNqvER0ojy9qtI7znGzPLdWi7zSTMc82k67OSqU+DwG6fk6KW1rOrrwHjthDTs8PZP4u+0HSTyqnoO8E8+DO9tnuDtu/Lg67PMCvN7CFrmO0hW801jUvKkm67vgQyE8Tu6PO2RIijwq9XY7Q/OCvIpiFb1oX8W7/CW9vHPxtDxNYgA9M2JxueDlKLyDCZc8rWPeO5gwnjwKsHI7KyUqvAj4hTx+BFm7VM+8u50OTzyXGgi8TIvou39kijumBny828wpvSVjBLw9gzk88A3zvM9Zajut+xA82OKcPNOU5LqBqQC9tfsVPJK+krvn9qO7zcgCPKkdmzvP4hK9uinOPIvsJzwwMpA6M+MjO4/lFzy+EDo7MdrOvBWfPDx9YL28lWIovJqFULiWyxW9PUIJvBSfAbse5WK6YINgvMj2Dbs9MpK8hl6MPDqwHryg/Ra9/MuIvD72pTx31qy8J5cTPcTSGLxfcGE8kvTRO7vFtzz/LAU9d36wOySoeTy+5Q49cHlbuz9CAz0rTxe63eKiu5RDHzySkmg6EKFBu2fzwztUciw8yJ7YO8zARTzWEEQ8y+wuPAk13zxFcfg8wRZgOkK6OTtPqhK6+bTePNVhBrvRrm07TEXau786wLzP1527Fv1ZveoXkDwYlqg7NcYGuYFejTzXL8o7ft3zu+LxpDpZ1WO7QHkeO1yeGzzcp6+7fZYWPCc9urzd+Ii8z+pyvByGibyk7/88Z9qbvJxpS73OGfi7TyqvvFxrITr+0va7iIREvELHEb1JiiU8zvnnur2GvzyN/o67jjj5PLJY6byHZxm92Pk6O+RhGDycU/s7QtNoPBZeWLxt9Vk8aNbMPB+OWjs6PsA82EcgPK69NjyQeHE7iFbQPDG28rv5yba7i/B5PLo5LLvOW7O7kU7+u+txnzzBIs87wJo1vEiVTbtogkc84pz5PB6IhTziSXs8Ciriu+IDY7x0yNU88kaFvAON27zVI2E7tyEauuUpHzwlNAm9p80wvImorzw5lLq8WoKAO92UxTwn+Ta7F/CUPKjsYbxFXkI8WlMgPE7mCLte+PO6vrkevFauRDyF6Y25hdJ0PGqL07qLHDw9T2/aPKUgjzzjvEq8mOB1PKniC7vR2/y8ULJMPPJ7CDzf8Au8WO0Huedm5zweWHG8CqXgu52IGLuqhKC8uYxsvJXaIroV/hO86aPhPNaqITwCytW5u93BvIkZ7rv/4iO8ixqRvLQfdjz756a81Gi6OvNPSTx1joa8D6rCu3MQ4TpR0K8818UsPKn6EL3BduK5imhZuo9N7DvIljQ6G/DEPC8UWzzVKNu7JA8MvAvRPrzDDzQ8aaC8u1YFNDzAhls8SBeTucocy7w+J8u8P+9JPGejPTxSmJq7ygdcvI6g2bv4De07RWdTvMvDyLsYl3s8qjWAu9VNP7wjIpC8vYeXu/EkAjwXnIK8ruc/ug== + 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 7f65ce43..27c4fa5c 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -383,6 +383,77 @@ class TestSandboxVFS: assert result.success assert result.stdout.count("True") == 6 + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_open_read(self, temp_db_path): + """open() and a with-block read document files through the VFS.""" + 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( + f"with open('/documents/{doc.id}/content.txt') as f:\n" + " data = f.read()\n" + "print('foxes' in data.lower())" + ) + assert result.success + assert "True" in result.stdout + + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_open_readlines(self, temp_db_path): + """readlines() splits a newline-delimited VFS file into lines.""" + config = AppConfig() + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.create_document( + content="The quick brown fox jumps over the lazy dog.", + uri="test://animals", + title="Animals", + ) + + context = AnalysisContext() + sb = Sandbox(db_path=temp_db_path, config=config, context=context) + result = await sb.execute( + f"lines = open('/documents/{doc.id}/items.jsonl').readlines()\n" + "print(len(lines) > 0)\n" + "import json\n" + "print('self_ref' in json.loads(lines[0]))" + ) + assert result.success + 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 @pytest.mark.vcr() async def test_context_filter_limits_vfs(self, temp_db_path): From feaca386d3073b2ce7e7195d71b2ddf21ca2d77b Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 28 Jul 2026 15:22:27 +0300 Subject: [PATCH 3/8] Document that file objects cannot be iterated Monty 0.0.19 supports open() and with blocks, but a file object is still not iterable. See pydantic/monty#490, which is still open. The instructions now state the limitation in three places and give the alternative next to each one: readlines() or read().split("\n"). Replace chr(10) with "\n" in the prose and in the example. Both work on 0.0.19, and chr(10) implies that the escape is broken. Add a test that pins the limitation. The test fails when Monty gains iteration support, which is the signal to relax the instructions. --- .../rag/capabilities/instructions/analysis.md | 8 ++-- tests/sandbox/test_sandbox.py | 44 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md index fb4b00a8..5dec8401 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md +++ b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md @@ -17,7 +17,7 @@ Inside the code, these functions are available (use `await`): - `await list_documents()` → list of dicts with keys: id, title, uri, created_at Available modules: `json`, `re`, `math`, `pathlib` -Not supported: class definitions, generators/yield, match statements, decorators, `collections` +Not supported: class definitions, generators/yield, match statements, decorators, `collections`, iterating a file object (`for line in f`) ### analysis_search Search the knowledge base directly (outside code execution). Each result has a `Type:` (paragraph, table, code, list_item, picture). When the Type is `picture`, the corresponding figure may also be attached to the tool response as an image alongside the text — use it directly to answer questions about figures, diagrams, charts, screenshots. @@ -48,7 +48,7 @@ All documents are mounted as a virtual filesystem at `/documents/`: `{document_id}` is an internal identifier, not the user-facing `uri` (filename, URL, etc.). When you only know a document by its URI or title, use `await list_documents()` to enumerate ids and match against `uri` / `title` — that's a single call to the host. Iterating `/documents/` and reading every `metadata.json` works too but is much slower on portal-scale corpora. ### Reading files -Read with `Path.read_text()` or `open()` (including `with` blocks); file objects support `.read()`, `.readline()`, and `.readlines()`. File objects are NOT iterable — do not write `for line in f`; use `.readlines()` or `text.split(chr(10))` for line-wise processing. Files are read-only; writing raises `PermissionError`. +Read with `Path.read_text()` or `open()` (including `with` blocks); file objects support `.read()`, `.readline()`, and `.readlines()`. A file object cannot be iterated, so read line-wise with `.readlines()` or `.read().split("\n")` instead of `for line in f`. Files are read-only; writing raises `PermissionError`. ```python from pathlib import Path @@ -63,7 +63,7 @@ for doc_dir in Path('/documents').iterdir(): content = Path(f'/documents/{doc_id}/content.txt').read_text() # Read and parse items -for line in Path(f'/documents/{doc_id}/items.jsonl').read_text().strip().split(chr(10)): +for line in Path(f'/documents/{doc_id}/items.jsonl').read_text().strip().split("\n"): item = json.loads(line) if item['label'] == 'table': print(item['text'][:200]) @@ -112,6 +112,6 @@ You MUST call `analysis_cite` with at least one chunk ID before producing your f - Use `print()` to output results — the output is your only feedback - When you write code, execute it — don't describe what code would do. But not every question needs code; simple lookups are best answered by `analysis_search → analysis_cite`. - Use `await` for all async functions inside `analysis_execute_code` (`search`, `list_documents`) -- Read files with `Path.read_text()` or `open()`/`with`; file objects are not iterable (no `for line in f`) and the `collections` module is unavailable +- Read files with `Path.read_text()` or `open()`/`with`. For lines use `.readlines()` or `.read().split("\n")`, never `for line in f`. The `collections` module is unavailable. - Do NOT include chunk IDs or UUIDs in your answer text — your answer should read naturally. Use the `analysis_cite` tool separately to register citations. `cite{...}` markdown-style inline references do nothing; only an actual `analysis_cite` tool call registers a citation. - **Before you write your final answer, invoke the `analysis_cite` tool with the supporting chunk_ids.** This is the last tool call before answering whenever your answer draws on retrieved evidence. diff --git a/tests/sandbox/test_sandbox.py b/tests/sandbox/test_sandbox.py index 27c4fa5c..0a19328c 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -7,6 +7,7 @@ import pytest from haiku.rag.client import HaikuRAG from haiku.rag.config.models import AppConfig from haiku.rag.sandbox import AnalysisContext, Sandbox, SandboxResult +from haiku.rag.store.models.chunk import Chunk @pytest.fixture(scope="module") @@ -454,6 +455,49 @@ class TestSandboxVFS: 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. + """ + 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") + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.import_document( + docling, + [ + Chunk( + content="one\ntwo", + embedding=[0.1] * config.embeddings.model.vector_dim, + order=0, + ) + ], + uri="test://lines", + ) + + 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()))" + ) + assert result.success, result.stderr + finally: + await sb.close() + @pytest.mark.asyncio @pytest.mark.vcr() async def test_context_filter_limits_vfs(self, temp_db_path): From 522959d9b4ab7717afe961832515d0073360ab8e Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 28 Jul 2026 15:55:44 +0300 Subject: [PATCH 4/8] Check the code timeout before each document read The VFS bridge suspends the Monty worker for the length of a read. Monty checks its duration budget between interpreter steps, so it cannot check while a read is in flight. Code that reads in a loop overran a 60s budget by minutes. A read takes about 20ms on a 2789-document corpus, so a full scan spends about 55s in reads alone. Check the deadline before each read. Raising from inside the callback answers the worker's suspension, which keeps the session usable. Monty also spends max_duration_secs across the session rather than per call, and the sandbox reuses the session so that variables persist. Budget it for code_timeout * max_executions. At the old per-call value the first slow call starved every later one. Do not wrap feed_run in asyncio.wait_for. Cancelling during pure compute is clean, but cancelling while a read waits for an answer wedges the session with a protocol RuntimeError that escapes execute(). A call that computes without reading stays bounded by the session budget alone. --- CHANGELOG.md | 2 + docs/configuration/qa.md | 4 +- haiku_rag_slim/haiku/rag/sandbox/sandbox.py | 35 +++++++++- tests/sandbox/test_sandbox.py | 74 +++++++++++++++++++++ 4 files changed, 110 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f40fd167..807fd72d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Require `pydantic-ai-slim>=2.18,<3`. - `pydantic-monty` bumped to `>=0.0.19`; sandbox code now runs in a subprocess worker pool. +- The analysis sandbox gives Monty a duration budget of `analysis.code_timeout * analysis.max_executions` for the session, was `analysis.code_timeout`. - `enable_thinking` maps onto Pydantic AI's unified `thinking` setting for the `anthropic`, `gemini`, `groq` and `bedrock` providers. The Anthropic thinking budget is now Pydantic AI's default of 10000 tokens, was 4096, and `max_tokens` must exceed it on budget-based Claude models; `enable_thinking: false` disables Gemini thinking rather than only hiding thoughts; Groq maps reasoning effort rather than `groq_reasoning_format`; Bedrock Qwen with `enable_thinking: false` no longer sends `reasoning_config`, while Bedrock-served Claude keeps an explicit `thinking: disabled`. The `openai` and `ollama` providers still map to `openai_reasoning_effort`. - `provider: bedrock` with a proprietary OpenAI model such as `openai.o3-mini-v1:0` raises `UserError`: Bedrock Converse serves only the `gpt-oss` family. Use `provider: bedrock-mantle`. - Tool failures raise `pydantic_ai.ToolFailed` instead of returning failure text: search and code-execution limits, sandbox execution errors, and `get_document`/`summarize_document` misses. @@ -20,6 +21,7 @@ - `evaluations run` opens the database read-only outside the population phase, so an embedder identity differing from the stored one warns instead of aborting the run. - `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. ### Removed diff --git a/docs/configuration/qa.md b/docs/configuration/qa.md index 601a0165..afc4ea05 100644 --- a/docs/configuration/qa.md +++ b/docs/configuration/qa.md @@ -50,13 +50,13 @@ analysis: provider: anthropic name: claude-sonnet-4-20250514 temperature: 0.0 # Default: 0.0 (deterministic for code generation) - code_timeout: 60.0 # Max seconds for code execution + code_timeout: 60.0 # Max seconds a call may spend reading documents max_output_chars: 50000 # Truncate output after this many chars max_executions: 15 # Max execute_code calls per question ``` - **model**: LLM configuration (see [Providers](providers.md#model-settings)). When unset, falls back to `qa.model`. -- **code_timeout**: Maximum seconds for each code execution (default: 60) +- **code_timeout**: Seconds a single `execute_code` call may spend reading documents (default: 60). The sandbox refuses further reads past this point. Code that computes without reading is bounded instead by the session budget of `code_timeout * max_executions`. - **max_output_chars**: Truncate code output after this many characters (default: 50000) - **max_executions**: Maximum `execute_code` calls per question before the capability is told to answer from what it has (default: 15) diff --git a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py index a32b4973..e788ea7a 100644 --- a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py +++ b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py @@ -14,6 +14,7 @@ from pydantic_monty import ( CallbackFile, MemoryFile, OSAccess, + ResourceLimits, ) from haiku.rag.config.models import AppConfig @@ -151,6 +152,7 @@ class Sandbox: _session: AsyncMontySession | None _vfs: OSAccess | None _loop: asyncio.AbstractEventLoop | None + _deadline: float | None def __init__( self, @@ -174,6 +176,7 @@ class Sandbox: self._session = None self._vfs = None self._loop = None + self._deadline = None @asynccontextmanager async def _connection(self) -> "AsyncIterator[HaikuRAG]": @@ -197,10 +200,24 @@ class Sandbox: Called off the loop while ``feed_run`` is awaited, so scheduling onto it and blocking for the result is safe. + + Blocking here suspends the worker, and Monty checks its duration budget + between interpreter steps, so it cannot check while a read is in flight. + Enforce the budget before starting another read, or code that reads in a + loop overruns it by however long the outstanding reads take. Raising from + inside the callback answers the worker's suspension, which keeps the + session usable — cancelling ``feed_run`` from outside does not, and wedges + the protocol. """ assert self._loop is not None, ( "VFS reads happen during execute(); the loop must be captured first." ) + if self._deadline is not None and self._loop.time() > self._deadline: + coro.close() + raise TimeoutError( + "time limit exceeded: no further document reads after " + f"{self._config.analysis.code_timeout}s" + ) return asyncio.run_coroutine_threadsafe(coro, self._loop).result() async def close(self) -> None: @@ -428,15 +445,26 @@ class Sandbox: return OSAccess(files) + def _session_limits(self) -> ResourceLimits: + """Resource limits for the worker session. + + Monty spends ``max_duration_secs`` across the session's whole life, and + the session is reused so variables persist between calls. Budget it for + the run rather than for one call, or the first slow call starves every + later one. ``code_timeout`` is enforced per call by the read deadline in + ``_run_on_loop``. This is the backstop for code that computes without + reading, and one such call can spend all of it. + """ + analysis = self._config.analysis + return {"max_duration_secs": analysis.code_timeout * analysis.max_executions} + async def _ensure_initialized(self) -> tuple[AsyncMontySession, OSAccess]: """Check out a worker session and build the VFS on first use.""" if self._session is None: self._vfs = await self._build_vfs() self._pool = AsyncMonty() await self._pool.__aenter__() - session = self._pool.checkout( - limits={"max_duration_secs": self._config.analysis.code_timeout}, - ) + session = self._pool.checkout(limits=self._session_limits()) await session.__aenter__() self._session = session assert self._session is not None and self._vfs is not None @@ -449,6 +477,7 @@ class Sandbox: """ # Monty's synchronous file callbacks bridge DB reads back to this loop. self._loop = asyncio.get_running_loop() + self._deadline = self._loop.time() + self._config.analysis.code_timeout session, vfs = await self._ensure_initialized() external_fns = self._build_external_functions() diff --git a/tests/sandbox/test_sandbox.py b/tests/sandbox/test_sandbox.py index 0a19328c..a2a5fac3 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -733,3 +733,77 @@ class TestSandboxHeldConnection: assert not any(t.name == "sandbox-vfs" for t in threading.enumerate()) await sb.close() await sb.close() + + +class TestSandboxReadDeadline: + """The VFS bridge suspends the worker for the length of a read, so Monty + cannot check its duration budget while one is in flight. The sandbox + enforces the budget itself, before each read.""" + + @pytest.mark.asyncio + async def test_read_after_deadline_raises_without_scheduling(self, sandbox): + """A read attempted past the deadline fails instead of querying.""" + scheduled = False + + async def _never_runs(): + nonlocal scheduled + scheduled = True + + sandbox._loop = asyncio.get_running_loop() + sandbox._deadline = sandbox._loop.time() - 1.0 + + coro = _never_runs() + with pytest.raises(TimeoutError, match="time limit exceeded"): + sandbox._run_on_loop(coro) + + coro.close() + assert scheduled is False + + def test_session_budget_covers_every_permitted_execution(self, temp_db_path): + """Monty spends its duration budget across the session's whole life, so a + per-call value would let the first call starve the rest.""" + config = AppConfig() + config.analysis.code_timeout = 5.0 + config.analysis.max_executions = 3 + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + + assert sb._session_limits() == {"max_duration_secs": 15.0} + + @pytest.mark.asyncio + async def test_document_read_past_the_deadline_fails_the_execution( + self, temp_db_path + ): + """The overrun surfaces as a failed result, not a raised exception.""" + from docling_core.types.doc.document import DoclingDocument + from docling_core.types.doc.labels import DocItemLabel + + config = AppConfig() + config.analysis.code_timeout = 0.0 + + docling = DoclingDocument(name="d") + 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="Foxes and dogs.", + embedding=[0.1] * config.embeddings.model.vector_dim, + order=0, + ) + ], + uri="test://deadline", + ) + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + result = await sb.execute( + "from pathlib import Path\n" + f"print(Path('/documents/{doc.id}/content.txt').read_text())" + ) + assert result.success is False + assert "no further document reads" in result.stderr + finally: + await sb.close() From 94befc0dfde7c4523aa5f89e0d8ed7b0f7b223a3 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 28 Jul 2026 17:01:53 +0300 Subject: [PATCH 5/8] Recover from worker death and deny metadata writes A crashed worker used to poison the rest of the run. execute() reported the crash as a failed result, but kept the dead session, and every later call then raised RuntimeError out of the tool. Clear the session so the next call checks out a replacement. Keep the session for a syntax or runtime error, which leaves the worker healthy, and say in the failure text that a restart loses the variables. A MemoryFile accepts writes, so metadata.json took them while the other three document files refused. Mount it through the same read and deny pair. The write-denial test now covers all four files. --- CHANGELOG.md | 2 + haiku_rag_slim/haiku/rag/sandbox/sandbox.py | 48 ++++++++- ...TestSandboxVFS.test_open_write_denied.yaml | 42 -------- tests/sandbox/test_sandbox.py | 98 +++++++++++-------- tests/sandbox/test_sandbox_toc.py | 17 ++++ 5 files changed, 118 insertions(+), 89 deletions(-) delete mode 100644 tests/cassettes/test_sandbox/TestSandboxVFS.test_open_write_denied.yaml 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.""" From d9e4d1e57c4e012d8d693a0d04012af95dc0441a Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 28 Jul 2026 17:33:08 +0300 Subject: [PATCH 6/8] Bound a call that never reads and restore the iteration test The read deadline only gets control at a read, so code that computes without reading escaped it. Give the pool a request_timeout above code_timeout. The watchdog kills the worker, and execute() already replaces a dead session, so a runaway call fails and the next call recovers. A read refusal keeps the variables, so it has to win the race whenever code does read. Restore test_open_file_objects_are_not_iterable. Replacing the neighbouring write test by text range deleted it, which left the instructions carrying a prohibition with nothing to signal when monty lifts it. Reuse the VFS and the pool when a session is replaced, so recovery skips the document scan. Mount metadata.json with a lambda rather than a factory. Cover open() in write mode. Fold the crash entry into the pydantic-monty bullet, because no release shipped the worker without it. --- CHANGELOG.md | 3 +- haiku_rag_slim/haiku/rag/sandbox/sandbox.py | 37 +++--- tests/sandbox/test_sandbox.py | 118 ++++++++++++++++++++ 3 files changed, 142 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6dabef9..d42fc52a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Changed - Require `pydantic-ai-slim>=2.18,<3`. -- `pydantic-monty` bumped to `>=0.0.19`; sandbox code now runs in a subprocess worker pool. +- `pydantic-monty` bumped to `>=0.0.19`; sandbox code now runs in a subprocess worker pool. A crashed or timed-out worker fails one call and the next call gets a replacement session. - The analysis sandbox gives Monty a duration budget of `analysis.code_timeout * analysis.max_executions` for the session, was `analysis.code_timeout`. - `enable_thinking` maps onto Pydantic AI's unified `thinking` setting for the `anthropic`, `gemini`, `groq` and `bedrock` providers. The Anthropic thinking budget is now Pydantic AI's default of 10000 tokens, was 4096, and `max_tokens` must exceed it on budget-based Claude models; `enable_thinking: false` disables Gemini thinking rather than only hiding thoughts; Groq maps reasoning effort rather than `groq_reasoning_format`; Bedrock Qwen with `enable_thinking: false` no longer sends `reasoning_config`, while Bedrock-served Claude keeps an explicit `thinking: disabled`. The `openai` and `ollama` providers still map to `openai_reasoning_effort`. - `provider: bedrock` with a proprietary OpenAI model such as `openai.o3-mini-v1:0` raises `UserError`: Bedrock Converse serves only the `gpt-oss` family. Use `provider: bedrock-mantle`. @@ -22,7 +22,6 @@ - `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 a4256579..9e1cbfac 100644 --- a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py +++ b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py @@ -28,6 +28,10 @@ if TYPE_CHECKING: from haiku.rag.client import HaikuRAG +_REQUEST_TIMEOUT_MARGIN_S = 30.0 +"""Grace above ``code_timeout`` before the pool watchdog kills the worker.""" + + @dataclass class SandboxResult: """Result of executing code in the sandbox.""" @@ -417,20 +421,11 @@ class Sandbox: ) # 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 - + # same read and deny pair as the rest. The content is already built. files.append( CallbackFile( f"{doc_dir}/metadata.json", - read=_make_metadata_reader(metadata), + read=lambda _path, text=metadata: text, write=_deny_write, ) ) @@ -476,6 +471,17 @@ class Sandbox: return OSAccess(files) + def _request_timeout(self) -> float: + """Hard per-call deadline for the pool watchdog. + + The read deadline refuses the next read at ``code_timeout`` and keeps the + session, so it wins for code that reads. This watchdog is the fallback + for code that computes without reading: it kills the worker, which loses + the variables the session held. Leave room for one read that is already + in flight, so the graceful refusal wins the race. + """ + return self._config.analysis.code_timeout + _REQUEST_TIMEOUT_MARGIN_S + def _session_limits(self) -> ResourceLimits: """Resource limits for the worker session. @@ -491,10 +497,13 @@ class Sandbox: async def _ensure_initialized(self) -> tuple[AsyncMontySession, OSAccess]: """Check out a worker session and build the VFS on first use.""" - if self._session is None: + if self._vfs is None: self._vfs = await self._build_vfs() - self._pool = AsyncMonty() - await self._pool.__aenter__() + if self._pool is None: + pool = AsyncMonty(request_timeout=self._request_timeout()) + await pool.__aenter__() + self._pool = pool + if self._session is None: session = self._pool.checkout(limits=self._session_limits()) await session.__aenter__() self._session = session diff --git a/tests/sandbox/test_sandbox.py b/tests/sandbox/test_sandbox.py index 107ae8bf..03d4f93f 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -471,6 +471,88 @@ class TestSandboxVFS: finally: await sb.close() + @pytest.mark.asyncio + async def test_open_for_writing_is_denied(self, temp_db_path): + """`open()` in write mode is refused, not only `Path.write_text`.""" + 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="Foxes and dogs.") + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.import_document( + docling, + [ + Chunk( + content="Foxes and dogs.", + embedding=[0.1] * config.embeddings.model.vector_dim, + order=0, + ) + ], + uri="test://openwrite", + ) + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + 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, result.stderr + assert "DENIED" in result.stdout + assert "WROTE" not in result.stdout + finally: + await sb.close() + + @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. + """ + 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") + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.import_document( + docling, + [ + Chunk( + content="one\ntwo", + embedding=[0.1] * config.embeddings.model.vector_dim, + order=0, + ) + ], + uri="test://lines", + ) + + 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()))" + ) + assert result.success, result.stderr + finally: + await sb.close() + @pytest.mark.asyncio @pytest.mark.vcr() async def test_context_filter_limits_vfs(self, temp_db_path): @@ -819,3 +901,39 @@ class TestSandboxWorkerCrash: assert lost.success is False finally: await sb.close() + + +class TestSandboxRequestTimeout: + """The pool watchdog bounds a call that never reads.""" + + def test_request_timeout_sits_above_the_read_deadline(self, temp_db_path): + """A read refusal keeps the session, so it must win the race.""" + config = AppConfig() + config.analysis.code_timeout = 5.0 + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + + assert sb._request_timeout() > config.analysis.code_timeout + + @pytest.mark.asyncio + async def test_runaway_compute_is_killed_and_the_next_call_recovers( + self, temp_db_path, monkeypatch + ): + """Code that never reads escapes the read deadline. The watchdog kills it.""" + config = AppConfig() + async with HaikuRAG(temp_db_path, create=True): + pass + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + monkeypatch.setattr(sb, "_request_timeout", lambda: 1.0) + try: + runaway = await sb.execute( + "x = 0\nfor i in range(500000000):\n x += i\nprint(x)" + ) + assert runaway.success is False + assert "restarted" in runaway.stderr + + recovered = await sb.execute("print('alive')") + assert recovered.success, recovered.stderr + assert "alive" in recovered.stdout + finally: + await sb.close() From 221c90af72355e631700bc726416cef630da2676 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 28 Jul 2026 18:11:33 +0300 Subject: [PATCH 7/8] Bound a compute-only call at code_timeout The pool watchdog counts only the time a worker spends running code, so a read that blocks the worker never trips it. The margin above code_timeout therefore guarded a race that cannot happen, and only bought a runaway call 90s where 60s was configured. Pass code_timeout straight through. The two limits are disjoint: the watchdog bounds a call that computes, and the read deadline bounds a call that reads. Drop the ordering test, which was true for any positive margin. The containment test no longer sets code_timeout to zero, because that value now also disables the watchdog and kills the worker before the read guard can refuse. It patches the guard instead. Monty 0.0.19 runs a plain class and a class with __enter__ and __exit__. Only inheritance and metaclasses raise. Say that in the instructions. MemoryFile is no longer constructed, so drop the import, the annotation, and the stale references in the sandbox docstring and CLAUDE.md. That CLAUDE.md line also still claimed a ThreadPoolExecutor, _run_async, and a fresh interpreter per call. --- docs/configuration/qa.md | 2 +- .../rag/capabilities/instructions/analysis.md | 2 +- haiku_rag_slim/haiku/rag/sandbox/sandbox.py | 26 +++++------------- tests/sandbox/test_sandbox.py | 27 ++++++++----------- 4 files changed, 20 insertions(+), 37 deletions(-) diff --git a/docs/configuration/qa.md b/docs/configuration/qa.md index afc4ea05..aa4f4681 100644 --- a/docs/configuration/qa.md +++ b/docs/configuration/qa.md @@ -56,7 +56,7 @@ analysis: ``` - **model**: LLM configuration (see [Providers](providers.md#model-settings)). When unset, falls back to `qa.model`. -- **code_timeout**: Seconds a single `execute_code` call may spend reading documents (default: 60). The sandbox refuses further reads past this point. Code that computes without reading is bounded instead by the session budget of `code_timeout * max_executions`. +- **code_timeout**: Seconds a single `execute_code` call may spend reading documents (default: 60). The sandbox refuses further reads past this point. Code that computes without reading is killed by the worker watchdog at the same limit. `code_timeout * max_executions` is the cumulative ceiling across all calls in one question. - **max_output_chars**: Truncate code output after this many characters (default: 50000) - **max_executions**: Maximum `execute_code` calls per question before the capability is told to answer from what it has (default: 15) diff --git a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md index 5dec8401..b16547e4 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md +++ b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md @@ -17,7 +17,7 @@ Inside the code, these functions are available (use `await`): - `await list_documents()` → list of dicts with keys: id, title, uri, created_at Available modules: `json`, `re`, `math`, `pathlib` -Not supported: class definitions, generators/yield, match statements, decorators, `collections`, iterating a file object (`for line in f`) +Not supported: class inheritance and metaclasses, generators/yield, match statements, decorators, `collections`, iterating a file object (`for line in f`) ### analysis_search Search the knowledge base directly (outside code execution). Each result has a `Type:` (paragraph, table, code, list_item, picture). When the Type is `picture`, the corresponding figure may also be attached to the tool response as an image alongside the text — use it directly to answer questions about figures, diagrams, charts, screenshots. diff --git a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py index 9e1cbfac..f808ccf9 100644 --- a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py +++ b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py @@ -12,7 +12,6 @@ from pydantic_monty import ( AsyncMonty, AsyncMontySession, CallbackFile, - MemoryFile, OSAccess, ResourceLimits, ) @@ -28,10 +27,6 @@ if TYPE_CHECKING: from haiku.rag.client import HaikuRAG -_REQUEST_TIMEOUT_MARGIN_S = 30.0 -"""Grace above ``code_timeout`` before the pool watchdog kills the worker.""" - - @dataclass class SandboxResult: """Result of executing code in the sandbox.""" @@ -304,12 +299,12 @@ class Sandbox: """Build the virtual filesystem with document data. Mounts per-document directories with: - - metadata.json: MemoryFile (eager, small) + - metadata.json: CallbackFile (eager, small) - content.txt: CallbackFile (lazy, can be large) - items.jsonl: CallbackFile (lazy, bulk-cached) - toc.json: CallbackFile (lazy, bulk-cached) """ - files: list[MemoryFile | CallbackFile] = [] + files: list[CallbackFile] = [] def _deny_write(_path: "PurePosixPath", _content: str | bytes) -> None: raise PermissionError(f"Document files are read-only: {_path}") @@ -471,17 +466,6 @@ class Sandbox: return OSAccess(files) - def _request_timeout(self) -> float: - """Hard per-call deadline for the pool watchdog. - - The read deadline refuses the next read at ``code_timeout`` and keeps the - session, so it wins for code that reads. This watchdog is the fallback - for code that computes without reading: it kills the worker, which loses - the variables the session held. Leave room for one read that is already - in flight, so the graceful refusal wins the race. - """ - return self._config.analysis.code_timeout + _REQUEST_TIMEOUT_MARGIN_S - def _session_limits(self) -> ResourceLimits: """Resource limits for the worker session. @@ -500,7 +484,11 @@ class Sandbox: if self._vfs is None: self._vfs = await self._build_vfs() if self._pool is None: - pool = AsyncMonty(request_timeout=self._request_timeout()) + # The watchdog counts only time the worker spends running code, so a + # read that blocks the worker never trips it. That leaves the two + # limits disjoint: this one bounds a call that computes, and the read + # deadline bounds a call that reads. + pool = AsyncMonty(request_timeout=self._config.analysis.code_timeout) await pool.__aenter__() self._pool = pool if self._session is None: diff --git a/tests/sandbox/test_sandbox.py b/tests/sandbox/test_sandbox.py index 03d4f93f..21d148a6 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -826,16 +826,12 @@ class TestSandboxReadDeadline: assert sb._session_limits() == {"max_duration_secs": 15.0} @pytest.mark.asyncio - async def test_document_read_past_the_deadline_fails_the_execution( - self, temp_db_path - ): - """The overrun surfaces as a failed result, not a raised exception.""" + async def test_refused_read_fails_the_execution(self, temp_db_path, monkeypatch): + """The refusal surfaces as a failed result, not a raised exception.""" from docling_core.types.doc.document import DoclingDocument from docling_core.types.doc.labels import DocItemLabel config = AppConfig() - config.analysis.code_timeout = 0.0 - docling = DoclingDocument(name="d") docling.add_text(label=DocItemLabel.TEXT, text="Foxes and dogs.") @@ -852,6 +848,13 @@ class TestSandboxReadDeadline: uri="test://deadline", ) + def _past_deadline(*_args, **_kwargs): + raise TimeoutError( + "time limit exceeded: no further document reads after 60.0s" + ) + + monkeypatch.setattr(Sandbox, "_run_on_loop", _past_deadline) + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) try: result = await sb.execute( @@ -906,25 +909,17 @@ class TestSandboxWorkerCrash: class TestSandboxRequestTimeout: """The pool watchdog bounds a call that never reads.""" - def test_request_timeout_sits_above_the_read_deadline(self, temp_db_path): - """A read refusal keeps the session, so it must win the race.""" - config = AppConfig() - config.analysis.code_timeout = 5.0 - sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) - - assert sb._request_timeout() > config.analysis.code_timeout - @pytest.mark.asyncio async def test_runaway_compute_is_killed_and_the_next_call_recovers( - self, temp_db_path, monkeypatch + self, temp_db_path ): """Code that never reads escapes the read deadline. The watchdog kills it.""" config = AppConfig() + config.analysis.code_timeout = 1.0 async with HaikuRAG(temp_db_path, create=True): pass sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) - monkeypatch.setattr(sb, "_request_timeout", lambda: 1.0) try: runaway = await sb.execute( "x = 0\nfor i in range(500000000):\n x += i\nprint(x)" From d1ad14f44b578a0e2ec8ca6183610256ddb844f0 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 28 Jul 2026 19:09:04 +0300 Subject: [PATCH 8/8] Point the limit comments at the right limits _session_limits still called the session budget the backstop for code that computes, which stopped being true when the pool gained a request_timeout in the same commit. The watchdog kills such a call at code_timeout, so it can never spend a budget of code_timeout * max_executions. The docstring now names the two per-call limits and claims neither. The crash test said the discard prevents a RuntimeError escaping execute(). The handler catches RuntimeError too, so without the discard every later call returns a failed result instead. Name MemoryFile's missing write hook as the reason metadata.json takes a reader and a deny pair. Drop the clause in the CHANGELOG that narrated how the old overrun accumulated. --- CHANGELOG.md | 2 +- haiku_rag_slim/haiku/rag/sandbox/sandbox.py | 10 +++++----- tests/sandbox/test_sandbox.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d42fc52a..2b775f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ - `evaluations run` opens the database read-only outside the population phase, so an embedder identity differing from the stored one warns instead of aborting the run. - `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. +- `analysis.code_timeout` is enforced before each document read, bounding a call that reads in a loop. - `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 f808ccf9..0812e250 100644 --- a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py +++ b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py @@ -415,8 +415,8 @@ class Sandbox: ensure_ascii=False, ) - # A MemoryFile accepts writes, so mount metadata.json through the - # same read and deny pair as the rest. The content is already built. + # MemoryFile has no write hook, so metadata.json goes through the + # same read and deny pair as the rest. Its content is already built. files.append( CallbackFile( f"{doc_dir}/metadata.json", @@ -472,9 +472,9 @@ class Sandbox: Monty spends ``max_duration_secs`` across the session's whole life, and the session is reused so variables persist between calls. Budget it for the run rather than for one call, or the first slow call starves every - later one. ``code_timeout`` is enforced per call by the read deadline in - ``_run_on_loop``. This is the backstop for code that computes without - reading, and one such call can spend all of it. + later one. ``code_timeout`` is enforced per call elsewhere: the read + deadline in ``_run_on_loop`` bounds a call that reads, and the pool's + ``request_timeout`` bounds one that computes. """ analysis = self._config.analysis return {"max_duration_secs": analysis.code_timeout * analysis.max_executions} diff --git a/tests/sandbox/test_sandbox.py b/tests/sandbox/test_sandbox.py index 21d148a6..90423a5a 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -894,7 +894,7 @@ class TestSandboxWorkerCrash: assert crashed.success is False assert "restarted" in crashed.stderr - # Without the discard this raises RuntimeError out of execute(). + # Without the discard every later call fails on the dead session. recovered = await sb.execute("print(3)") assert recovered.success, recovered.stderr assert "3" in recovered.stdout