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:
1 parent
3843821674
commit
cb815216d2
11 files changed
+791
-97
No files matched your search
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -85,6 +86,23 @@ class TestCouncil(unittest.TestCase):
|
||||
self._run("s1", "claim", "work/x", "--work", "w")
|
||||
self.assertEqual(self._run("s1", "gate", stdin=inside).returncode, 0)
|
||||
|
||||
def test_gate_still_blocks_past_a_planted_fifo_or_malformed_claim(self):
|
||||
repo = Path(self.tmp.name) / "repo"
|
||||
(repo / ".git").mkdir(parents=True)
|
||||
inside = json.dumps({"session_id": "s1", "tool_input": {"file_path": str(repo / "f")}})
|
||||
(self.root / "work" / "fifo").mkdir(parents=True)
|
||||
os.mkfifo(self.root / "work" / "fifo" / "claim.json", 0o600)
|
||||
for claim in ({}, [], {"session": "s2", "runtime": "claude-code"}):
|
||||
with self.subTest(claim=claim):
|
||||
(self.root / "work" / "bad").mkdir(exist_ok=True)
|
||||
(self.root / "work" / "bad" / "claim.json").write_text(json.dumps(claim), encoding="utf-8")
|
||||
env = {"PATH": "/usr/bin:/bin", "HOME": str(self.home), "AGENTS_COUNCIL_ROOT": str(self.root)}
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(COUNCIL), "gate"],
|
||||
capture_output=True, text=True, env=env, input=inside, check=False, timeout=30,
|
||||
)
|
||||
self.assertEqual(result.returncode, 2, result.stderr)
|
||||
|
||||
def _two_claims(self) -> str:
|
||||
work = "fix the failing deploy of web-app-x on branch fix/web-app-x/deploy"
|
||||
self._run("s1", "claim", "work/x", "--work", work)
|
||||
|
||||
Reference in new issue
Block a user