@@ -124,6 +124,7 @@ class _PermissionSandboxedExecutor(SandboxedExecutor):
124124 ("ws" , "read_output" ),
125125 ("ws" , "replace" ),
126126 ("ws" , "run" ),
127+ ("ws" , "run_trusted_readonly" ),
127128 ("ws" , "search" ),
128129 ("ws" , "write" ),
129130 ("ws" , "write_file" ),
@@ -469,6 +470,7 @@ def __init__(
469470 config .permission_rules ,
470471 mode = config .mode ,
471472 auto_approve = config .auto_approve ,
473+ yolo = config .yolo ,
472474 )
473475 self ._engine .mode = config .mode
474476 self ._approvals = approvals or ApprovalBroker (
@@ -859,14 +861,22 @@ async def handle(self, notification: dict[str, list]) -> RespondResult:
859861 in the notification. Understand the requested end state before acting.
860862 Conversational questions are first-class: answer them with
861863 ``self.message(...)`` then return DONE. Do not emit bare assistant prose.
864+ IMPORTANT: ``self.message(...)`` is SYNCHRONOUS — call it WITHOUT
865+ ``await`` (``self.message("...")``). Prefixing ``await`` raises
866+ ``TypeError: object NoneType can't be used in 'await' expression``
867+ because it returns ``None``. All ``self.ws.*``, ``self.web.*``,
868+ ``self.lsp.*``, ``self.processes.*`` calls ARE async and need ``await``.
862869
863870 Minimal tool cookbook:
864871 - ``await self.ws.list("**/*.py")`` lists files.
865872 - ``await self.ws.search("symbol")`` returns locations as text.
866873 - ``match = await self.ws.read("path.py", lines=(10, 30))`` returns an
867874 editable Match; ``await self.ws.replace(match, "replacement")`` edits it.
868875 - ``await self.ws.edit("path.py", "unique old text", "new text")`` is the
869- simple string-edit form. ``await self.ws.write("new.py", content)`` creates files.
876+ simple string-edit form and takes exactly three arguments (path, old,
877+ new); two-argument calls are invalid. For a Match from read(), edit
878+ with ``await self.ws.replace(match, "replacement")`` instead.
879+ ``await self.ws.write("new.py", content)`` creates files.
870880 - Prefer ``await self.ws.apply_patch(changes)`` for coherent edits. Each change is
871881 ``{"path": ..., "old": exact_text_or_None, "new": replacement_or_None}``;
872882 one call validates and atomically commits the full batch.
@@ -878,6 +888,19 @@ async def handle(self, notification: dict[str, list]) -> RespondResult:
878888 long-running commands. Consume logs by cursor; do not poll without new work.
879889 - ``result = await self.ws.run("pytest -q")`` runs validation; inspect
880890 ``result.returncode``, ``result.stdout``, and ``result.stderr``.
891+ ``read_only=True`` skips approval ONLY for commands the engine
892+ recognizes as read-only (``git status/log/diff/show``, ``rg``,
893+ ``grep``, ``ls``, ``find`` (no -delete/-exec), ``head``, ``tail``,
894+ ``wc``, ``sed``, ``awk``, ``sort``, ``uniq``, ``cut``, ``tr``,
895+ ``tac``, ``column``, ``pwd``, ``file``, ``stat``, ``test``). It is
896+ REJECTED for anything else — including ``pytest``, ``uv``, ``python``,
897+ and build commands — so do NOT pass ``read_only=True`` for those; run
898+ them with plain ``await self.ws.run(cmd)`` (YOLO auto-approves;
899+ ``--auto`` prompts).
900+ - If the host was launched with ``--yolo``, every approval is granted
901+ automatically without prompting. That mode exists for throwaway or
902+ sandboxed environments only; do not assume it is active — write code
903+ that works under normal permission gating.
881904 - ``await self.web.fetch(url)`` reads a page; ``await self.web.search(query)``
882905 searches the public web. Both are read-only and allowed by default.
883906 - ``await self.github.list()`` / ``view(number)`` inspect pull requests.
@@ -919,10 +942,28 @@ async def handle(self, notification: dict[str, list]) -> RespondResult:
919942 or mutating ``gh pr`` through the shell.
920943 - Do not read secrets or expose sensitive environment values.
921944
945+ Forbidden inside sandboxed code cells (do NOT attempt - they always fail
946+ and burn turns): ``import os``, ``import sys``, ``import subprocess``,
947+ ``import shutil``, ``import nooa``, ``import noah_code``, and any of
948+ ``eval()``, ``exec()``, ``compile()``, ``__import__()``. If you need to
949+ run shell logic or a host feature, do it with ``self.ws.run(...)``
950+ (read-only shell is auto-approved), ``self.web``, ``self.lsp``, or the
951+ dedicated tools - never by importing a blocked module. Use
952+ ``self.ws.inspect(...)`` on Python data; do not try to reach host
953+ objects from a cell.
954+
922955 Return exactly one valid RespondResult:
923956 - DONE - request complete
924957 - NEED_INPUT - user input genuinely required
925958 - WAIT - a registered background job is still running
959+
960+ To end the turn, call ``return_result(RespondReason.DONE,
961+ explanation="...")`` — ``return_result`` takes a ``RespondReason``
962+ and an ``explanation`` string, NEVER a bare string as ``result=``
963+ (that raises ``return_result validation error: 'result' has wrong
964+ type``). To show the user text, call the synchronous
965+ ``self.message("...")`` first (no ``await``), then
966+ ``return_result(RespondReason.DONE, explanation="summary")``.
926967 """
927968 ...
928969
0 commit comments