fix(kvb-org-organize): no council hook follows a planted symlink or FIFO

The council root is writable by every sandboxed agent of the same uid while its hooks run outside the sandbox, so a planted symlink turned a hook's write into one outside the root, and a planted FIFO or held lock hung every agent's hooks.

- Add nofollow.py: open, read, lock, append, replace and unlink below a root through O_NOFOLLOW directory fds, O_NONBLOCK and regular files only; reads stop at 64 MiB and replace undecodable bytes; locks take an explicit wait.
- watch, queue, radio and council read and write everything below the council root through it; keep_rows copies no symlinked file into ~/.local/state.
- watch: read_json returns dicts only, alert lines that are no alert (deep nesting included) are skipped, cmd_cleanup makes and removes its lock through a root dir fd, and the cache trim walks with fwalk, counts allocated blocks and each inode once.
- radio: the hooks wait at most 2 s for a session lock and skip the delivery past it.
- council: read_claim ignores claims that are no object with session, runtime and beat, so a malformed claim no longer disables the claim gate.

MEDUSA acceptances (kvb-think-criticize 2026-11-06, skeptics A and B): MM-SCAN-058 on nofollow.replace callers, B105 and HPT006 in history.py, RT-SCAN-054 in council.py (name matches).

🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
💰 Value: ?
🏆 Score: 88
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-06 13:26:59 +00:00
1 parent 3843821674
commit cb815216d2
11 files changed
+791 -97

No files matched your search

+41
View File
@@ -617,6 +617,47 @@ class TestQueueCommands(unittest.TestCase):
self.assertEqual(self.bash("ls"), 0)
self.assertEqual(self.bash("echo it's"), 0)
def test_hold_creates_no_lock_in_a_planted_holds_directory(self):
victims = self.base / "victims"
victims.mkdir()
(self.root / "holds").symlink_to(victims)
seen = self.base / "seen.txt"
listing = "import os, sys; open(sys.argv[2], 'w').write(repr(os.listdir(sys.argv[1])))"
result = self.run_queue(
"hold", "--kind", "test", "--task", "work/a", "--session", "s1", "--",
sys.executable, "-c", listing, str(victims), str(seen),
)
self.assertEqual(seen.read_text(encoding="utf-8") if seen.exists() else "[]", "[]")
self.assertEqual(os.listdir(victims), [])
self.assertNotEqual(result.returncode, 0)
def test_list_unlinks_no_lock_in_a_planted_holds_directory(self):
victims = self.base / "victims"
victims.mkdir()
(victims / "1-2.lock").write_text("", encoding="utf-8")
(self.root / "holds").symlink_to(victims)
gone = subprocess.Popen(["true"])
gone.wait()
self.write_store([{**stored(1, "waiting", gone.pid, "1", None, None), "lock": "1-2"}])
self.run_queue("list")
self.assertEqual(os.listdir(victims), ["1-2.lock"])
def test_the_store_lock_creates_no_file_through_a_planted_name(self):
created = self.base / "created"
(self.root / "queue.lock").symlink_to(created)
result = self.run_queue("list")
self.assertFalse(created.exists())
self.assertNotEqual(result.returncode, 0)
def test_hold_appends_no_history_row_through_a_planted_name(self):
victim = self.base / "victim"
victim.write_text("keep\n", encoding="utf-8")
self.history_file.symlink_to(victim)
result = self.run_queue("hold", "--kind", "test", "--task", "work/a", "--session", "s1", "--", "true")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("no history row", result.stderr)
self.assertEqual(victim.read_text(encoding="utf-8"), "keep\n")
def test_a_council_root_others_can_write_stops_commands_but_not_the_hook(self):
self.root.chmod(0o777)
refused = self.run_queue("list")