diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index d80b69fbe..6af9629aa 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -61,6 +61,14 @@ class ExecTool(Tool): r">\s*/dev/sd", # write to disk r"\b(shutdown|reboot|poweroff)\b", # system power r":\(\)\s*\{.*\};\s*:", # fork bomb + # Block writes to nanobot internal state files (#2989). + # history.jsonl / .dream_cursor are managed by append_history(); + # direct writes corrupt the cursor format and crash /dream. + r">>?\s*\S*(?:history\.jsonl|\.dream_cursor)", # > / >> redirect + r"\btee\b[^|;&<>]*(?:history\.jsonl|\.dream_cursor)", # tee / tee -a + r"\b(?:cp|mv)\b[^|;&<>]*(?:history\.jsonl|\.dream_cursor)", # cp/mv target + r"\bdd\b[^|;&<>]*\bof=\S*(?:history\.jsonl|\.dream_cursor)", # dd of= + r"\bsed\s+-i[^|;&<>]*(?:history\.jsonl|\.dream_cursor)", # sed -i ] self.allow_patterns = allow_patterns or [] self.restrict_to_workspace = restrict_to_workspace diff --git a/tests/tools/test_exec_security.py b/tests/tools/test_exec_security.py index e65d57565..bb8fc21ec 100644 --- a/tests/tools/test_exec_security.py +++ b/tests/tools/test_exec_security.py @@ -67,3 +67,49 @@ async def test_exec_blocks_chained_internal_url(): command="echo start && curl http://169.254.169.254/latest/meta-data/ && echo done" ) assert "Error" in result + + +# --- #2989: block writes to nanobot internal state files ----------------- + + +@pytest.mark.parametrize( + "command", + [ + "cat foo >> history.jsonl", + "echo '{}' > history.jsonl", + "echo '{}' > memory/history.jsonl", + "echo '{}' > ./workspace/memory/history.jsonl", + "tee -a history.jsonl < foo", + "tee history.jsonl", + "cp /tmp/fake.jsonl history.jsonl", + "mv backup.jsonl memory/history.jsonl", + "dd if=/dev/zero of=memory/history.jsonl", + "sed -i 's/old/new/' history.jsonl", + "echo x > .dream_cursor", + "cp /tmp/x memory/.dream_cursor", + ], +) +def test_exec_blocks_writes_to_history_jsonl(command): + """Direct writes to history.jsonl / .dream_cursor must be blocked (#2989).""" + tool = ExecTool() + result = tool._guard_command(command, "/tmp") + assert result is not None + assert "dangerous pattern" in result.lower() + + +@pytest.mark.parametrize( + "command", + [ + "cat history.jsonl", + "wc -l history.jsonl", + "tail -n 5 history.jsonl", + "grep foo history.jsonl", + "ls memory/", + "echo history.jsonl", + ], +) +def test_exec_allows_reads_of_history_jsonl(command): + """Read-only access to history.jsonl must still be allowed.""" + tool = ExecTool() + result = tool._guard_command(command, "/tmp") + assert result is None