From 9876b1f66e34b17acc4d51dc2db0638423bb5c3b Mon Sep 17 00:00:00 2001 From: Prince <418962xxx+prince2-ai@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:11:06 +0530 Subject: [PATCH] fix: redact all tool results and persist compacted messages Tool outputs from every skill, memory, and extension tool now pass through the central redactor before entering history. Compaction persists each compacted message so a resumed session no longer loses summary state. --- argus/harness.py | 3 ++- argus/loop.py | 2 ++ tests/test_harness.py | 52 +++++++++++++++++++++++++++++++++++++++++++ tests/test_loop.py | 17 ++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/argus/harness.py b/argus/harness.py index 7af7685..36f08da 100644 --- a/argus/harness.py +++ b/argus/harness.py @@ -122,7 +122,8 @@ def before_turn(messages): if compacted is not messages: messages[:] = compacted if self.persist: - session.append(self.session_path, messages[0]) + for message in messages: + session.append(self.session_path, message) recorded_index = len(messages) record() return messages diff --git a/argus/loop.py b/argus/loop.py index 4371e93..6c65d97 100644 --- a/argus/loop.py +++ b/argus/loop.py @@ -1,6 +1,7 @@ """The agent turn loop with explicit tool-error containment.""" from .providers import Provider, ensure_call_ids, get_provider +from .tools import redact def run( @@ -63,6 +64,7 @@ def run( result = str(tool.run(**args)) except Exception as error: result = f"ERROR: {type(error).__name__}: {error}" + result = redact(result) messages.append( { "role": "tool", diff --git a/tests/test_harness.py b/tests/test_harness.py index ce7cc63..ce7ec6f 100644 --- a/tests/test_harness.py +++ b/tests/test_harness.py @@ -106,6 +106,58 @@ def test_compaction_is_persisted_and_reloadable(self): self.assertTrue((resumed.messages[0].get("meta") or {}).get("compaction")) self.assertEqual(harness.usage, {"input": 2, "output": 2, "total": 4}) + def test_compaction_restores_retained_turns_on_resume(self): + provider = ScriptedProvider([ + reply("", [tool_call("read_file", {"path": "note.txt"}, "c1")]), + reply("", [tool_call("read_file", {"path": "note.txt"}, "c2")]), + reply("compressed facts"), + reply("finished"), + ]) + harness = Harness( + self.root, provider=provider, policy=Policy("yolo"), + budget_tokens=1, + ) + with mock.patch.object(context, "KEEP_RECENT", 3): + self.assertEqual(harness.run("long task"), "finished") + + resumed = Harness(self.root, provider=ScriptedProvider(), persist=False) + self.assertTrue(resumed.resume(harness.session_path)) + self.assertTrue((resumed.messages[0].get("meta") or {}).get("compaction")) + assistant_calls = [ + call + for message in resumed.messages + if message.get("role") == "assistant" + for call in message.get("tool_calls") or [] + ] + tool_rows = [ + message for message in resumed.messages if message.get("role") == "tool" + ] + self.assertEqual([call["id"] for call in assistant_calls], ["c2"]) + self.assertEqual([message["call_id"] for message in tool_rows], ["c2"]) + self.assertEqual(resumed.messages[-1]["text"], "finished") + + def test_use_skill_output_is_redacted_before_persistence(self): + secret = "sk-abcdefghijklmnop1234" + skill_dir = os.path.join(self.root, "skills", "demo") + os.makedirs(skill_dir) + with open(os.path.join(skill_dir, "SKILL.md"), "w", encoding="utf-8") as stream: + stream.write(f"---\ndescription: Demo skill\n---\nUse {secret}.\n") + provider = ScriptedProvider([ + reply("", [tool_call("use_skill", {"name": "demo"}, "c1")]), + reply("done"), + ]) + harness = Harness(self.root, provider=provider, policy=Policy("yolo")) + harness.run("load the demo skill") + + tool_message = next( + message for message in harness.messages if message.get("role") == "tool" + ) + self.assertIn("[REDACTED_OPENAI_KEY]", tool_message["text"]) + with open(harness.session_path, encoding="utf-8") as stream: + persisted = stream.read() + self.assertNotIn(secret, persisted) + self.assertIn("[REDACTED_OPENAI_KEY]", persisted) + def test_remember_tool_writes_and_dedupes(self): harness = Harness( self.root, provider=ScriptedProvider(), persist=False diff --git a/tests/test_loop.py b/tests/test_loop.py index ae903e1..1c4d42f 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -53,6 +53,23 @@ def test_unknown_tool_is_reported_to_model(self): loop.run("system", messages, self.tools, provider=provider) self.assertTrue(messages[2]["text"].startswith("ERROR: unknown tool")) + def test_tool_results_are_redacted_in_history_and_events(self): + secret = "sk-abcdefghijklmnop1234" + provider = ScriptedProvider([ + reply("", [tool_call("echo", {"text": secret}, "c1")]), + reply("done"), + ]) + messages = [{"role": "user", "text": "go"}] + events = [] + loop.run( + "system", messages, self.tools, provider=provider, + on_event=lambda kind, payload: events.append((kind, payload)), + ) + self.assertNotIn(secret, messages[2]["text"]) + self.assertIn("[REDACTED_OPENAI_KEY]", messages[2]["text"]) + tool_end = next(payload for kind, payload in events if kind == "tool_end") + self.assertNotIn(secret, tool_end["result"]) + def test_tool_exception_is_contained(self): provider = ScriptedProvider([ reply("", [tool_call("boom", {}, "c1")]),