The 'one-time migration' fallback that runs on every call, forever A developer's cross-vendor AI code review setup caught a distributed-lock defect in which a fallback intended as a one-time schema migration runs on every release call, allowing an unrelated caller sharing a session string to silently release a lease held under the old empty epoch. The reviewing agent returned REQUEST_CHANGES, noting the test suite proves only one direction of the asymmetric fallback and never tests a tokened release wiping a live lease owned by a different logical owner. Two lower-severity findings covered a brittle contract test and a skill duplication guard lost in a file rename. A full session transcript, a week late, with nothing taken out. No summary, no cleanup. Tool calls included. This one is a code review. The setup matters. The code was written by one AI agent, Codex. The review was done by a different one, from a different vendor, with no tools and no ability to read anything beyond the diffs pasted into the prompt. The rule in my setup is simple. Nobody reviews their own work. That includes agents reviewing their own work. The change under review is a distributed lock. Two agents share one directory. The lease stops them writing the same file at once. One owner wins. The loser gets BUSY and the name of the holder. This version adds fencing epochs, so a stale process cannot release a lease that has since been handed to someone else. The verdict was REQUEST CHANGES. It rests on one finding, and I think it is a genuinely good catch. The lock had a fallback for old database rows. If the exact session and epoch pair misses, delete where the epoch is empty. The docstring calls this a one time schema handoff. Nothing in the code scopes it to migration time. So it runs on every release call. Forever. A permanent wildcard match. Any caller legitimately holding a lease under the old empty epoch, and the code's own comment names one, can have that lease silently released by an unrelated call that happens to share the session string. The part I find most useful is what the reviewer said about the tests. The suite does prove the intended direction. It also proves that an untokened release cannot touch a tokened lease. It never tests the reverse: a tokened release wiping a live lease belonging to a different logical owner. A test suite that covers one direction of an asymmetric fallback reads as coverage and is not. Two more findings are here. Both flagged low, not inflated. A contract test that will break if the hook config is reorganised into an equally valid shape. And a skill duplication guard that quietly vanished in a file rename, with no replacement. One note on language. My working sessions are usually in Russian, with English code and tooling. This one is almost entirely English, since the review prompt and the verdict contract are English. The whole corpus of these transcripts lives at github.com/tonydzi/agent-sessions https://github.com/tonydzi/agent-sessions , which is where this one is canonical. The transcript Антон: You are an INDEPENDENT senior code reviewer from a DIFFERENT vendor than the author. The author Codex wrote the two-repository change below. Review ONLY the supplied diffs; do not use tools or read other files. Be concrete and skeptical. Task: local Windows Claude/Codex shared-file write protection. Required properties: two concurrent writers of one absolute file produce one owner and BUSY rc=7 naming the holder; foreign release and delayed old-turn release are harmless; old-schema migration is race-safe and token-aware Stop can release a migrated legacy row; real Codex shell/apply patch hook wiring is tested; counters are attributable; child-script, relative-path, Windows-only, per-checkout and local-only boundaries are honest. Check correctness, security bypasses, caller breakage, races, and whether tests really kill their named mutations. Prioritize real MED/HIGH defects; do not block on style. CRITICAL OUTPUT CONTRACT: the VERY LAST line of your reply must be exactly ===VERDICT=== where is one bare word: APPROVE no blocking issues or REQUEST CHANGES one or more MED/HIGH issues . No backticks, no bold, no brackets, nothing else on that line. State it exactly once, on that final line only. Format before the final line: SUMMARY: FINDINGS: - file:line | severity | issue | fix or none --- SCRIPTS DIFF START ---diff --git a/ test codex shared workspace safety.py b/ test codex shared workspace safety.py deleted file mode 100644 index 4d7d597e5..000000000 --- a/ test codex shared workspace safety.py +++ /dev/null @@ -1,273 +0,0 @@ - - - coding: utf-8 - - -"""Integration acceptance for Claude/Codex shared-workspace safety. -Purpose: prove the active Codex registry has one local shelf, both harness configs -route writes through the same guard, canonical apply patch is really parsed, and a -Stop event releases the turn lease. Input: live configs plus an isolated temp DB. -Output: PASS/FAIL and evidence; never edits a real workspace file. -Caller: /tt after shared-workspace changes. Rail: local Python + codex debug, 0 LLM. -updated: 2026-09-11 -""" -from future import annotations -import json -import os -import re -import shutil -import subprocess -import sys -import tempfile -import time -HOME = os.path.expanduser "~" -SCRIPTS = os.path.join HOME, ".claude", "scripts" -HOOKS = os.path.join HOME, ".claude", "hooks" -GUARD = os.path.join HOOKS, "workspace write guard.py" -CONSTITUTION = os.path.join HOOKS, "constitution guard.py" -ORPHAN = os.path.join HOOKS, "orphan check hook.py" -TURNSTATE = os.path.join HOOKS, "turnstate hook.py" -CODEX HOOKS = os.path.join HOME, ".codex", "hooks.json" -CLAUDE SETTINGS = os.path.join HOME, ".claude", "settings.json" -R1 = os.path.join HOME, ".agents", "skills" -RESULTS = - -def check name, ok, detail="" : - RESULTS.append name, bool ok , detail - print " %s %s%s" % "OK " if ok else "FAIL", name, - " -- " + str detail if detail else "" - -def json path : - with open path, encoding="utf-8" as fh: - return json.load fh - -def commands entries : - out = - for group in entries or : - for hook in group.get "hooks" or : - out.append group.get "matcher" or "", hook.get "command" or "" - return out - -def run guard payload, phase="pre", env=None : - return subprocess.run sys.executable, GUARD, "--phase", phase , - input=json.dumps payload , capture output=True, text=True, - encoding="utf-8", errors="replace", env=env, timeout=30 - -def run constitution payload, env=None : - return subprocess.run sys.executable, CONSTITUTION , input=json.dumps payload , - capture output=True, text=True, encoding="utf-8", - errors="replace", env=env, timeout=30 - -def wait until ts : - while True: - left = float ts - time.time - if left <= 0: - return - time.sleep min 0.02, left / 2 - -def guard child root, start, session, target : - env = dict os.environ - env "WORKSPACE WRITE LEASE DB" = os.path.join root, "race.sqlite3" - env "WORKSPACE WRITE LEASE COUNTER" = os.path.join root, "race-counter.jsonl" - wait until start - payload = {"session id": session, "cwd": root, "tool name": "Write", - "tool input": {"file path": target}} - run = run guard payload, env=env - print "WIN" if run.returncode == 0 else "LOSE" - return 0 - -def guard race root, target, racers=8 : - start = time.time + 1.2 - procs = subprocess.Popen - sys.executable, os.path.abspath file , "--guard-child", root, - "%.6f" % start, "hook-session-%d" % i, target , - stdout=subprocess.PIPE, stderr=subprocess.PIPE - for i in range racers - wins, errors = 0, - for proc in procs: - out, err = proc.communicate timeout=60 - answer = out.decode "utf-8", "replace" .strip - if answer.endswith "WIN" : - wins += 1 - elif not answer.endswith "LOSE" : - errors.append "rc=%s out=%r err=%r" % - proc.returncode, answer, err.decode "utf-8", "replace" :120 - return wins, errors - -def main : - root = tempfile.mkdtemp prefix="codex-shared-safety-test-" - env = dict os.environ - env "WORKSPACE WRITE LEASE DB" = os.path.join root, "leases.sqlite3" - env "WORKSPACE WRITE LEASE COUNTER" = os.path.join root, "counter.jsonl" - target = os.path.join root, "shared file.md" - try: - r1 skills = - if os.path.isdir R1 : - r1 skills = n for n in os.listdir R1 - if os.path.isfile os.path.join R1, n, "SKILL.md" - check "r1/.agents не содержит активных копий SKILL.md", not r1 skills, - "active=%d first=%s" % len r1 skills , r1 skills :5 - check "общий workspace guard существует", os.path.isfile GUARD , GUARD - codex = json CODEX HOOKS - claude = json CLAUDE SETTINGS - cpre = commands codex.get "hooks", {} .get "PreToolUse" - clpre = commands claude.get "hooks", {} .get "PreToolUse" - check "Codex PreToolUse явно матчится на canonical apply patch", - any "constitution guard" in cmd and "apply patch" in matcher - for matcher, cmd in cpre , cpre :3 - check "Codex shell тоже идёт через guard", - any "constitution guard" in cmd and - "Bash" in matcher or "PowerShell" in matcher for matcher, cmd in cpre , cpre :3 - check "Claude file tools идут через тот же guard", - any "constitution guard" in cmd and "Write" in matcher for matcher, cmd in clpre - check "Claude shell идёт через тот же guard", - any "constitution guard" in cmd and - "Bash" in matcher or "PowerShell" in matcher for matcher, cmd in clpre - cpost = commands codex.get "hooks", {} .get "PostToolUse" - check "Codex PostToolUse явно матчится на canonical apply patch", - any "orphan check hook" in cmd and "apply patch" in matcher - for matcher, cmd in cpost , cpost :3 - if os.path.isfile GUARD : - race root = os.path.join root, "parallel-hook" - os.makedirs race root, exist ok=True - race target = os.path.join race root, "one-file.md" - wins, errors = guard race race root, race target - check "параллельный hook: 8 turn - ровно один проходит", wins == 1, - "wins=%d errors=%s" % wins, errors - patch = " Begin Patch\n Update File: %s\n@@\n-old\n+new\n End Patch" % target - p1 = {"session id": "codex-A", "cwd": root, "tool name": "apply patch", - "tool input": {"command": patch}} - p2 = {"session id": "claude-B", "cwd": root, "tool name": "Edit", - "tool input": {"file path": target}} - a = run guard p1, env=env - b = run guard p2, env=env - check "canonical apply patch реально получает lease", a.returncode == 0, - "rc=%s stderr=%s" % a.returncode, a.stderr :200 - check "второй агент блокируется на том же файле", b.returncode == 2 and - "codex-A" in b.stderr, "rc=%s stderr=%s" % b.returncode, b.stderr :300 - codex b = dict p2 - codex b.update {"tool name": "apply patch", "turn id": "turn-B", - "tool use id": "call-B", - "tool input": {"command": patch}} - structured = run constitution codex b, env - try: - structured json = json.loads structured.stdout - permission = structured json "hookSpecificOutput" "permissionDecision" - except Exception: - permission = "" - check "Codex 0.147 получает structured deny, не hook Failed", - structured.returncode == 0 and permission == "deny" and - "codex-A" in structured.stdout, - "rc=%s out=%s err=%s" % structured.returncode, - structured.stdout :300 , structured.stderr :120 - stop payload = {"session id": "codex-A", "cwd": root, - "hook event name": "Stop"} - stop = subprocess.run sys.executable, TURNSTATE , input=json.dumps stop payload , - capture output=True, text=True, encoding="utf-8", - errors="replace", env=env, timeout=30 - after = run guard p2, env=env - check "реальный turnstate Stop освобождает turn-lease", - stop.returncode == 0 and after.returncode == 0, - "stop=%s after=%s" % stop.returncode, after.returncode - run guard {"session id": "claude-B", "cwd": root}, "stop", env - post payload = dict p1 - post payload.update {"turn id": "turn-post", "tool use id": "call-post", - "hook event name": "PostToolUse", "tool response": {"ok": True}} - post = subprocess.run sys.executable, ORPHAN , input=json.dumps post payload , - capture output=True, text=True, encoding="utf-8", - errors="replace", env=env, timeout=30 - check "Codex PostToolUse no-op = пустой stdout, не invalid approve", - post.returncode == 0 and not post.stdout.strip , - "rc=%s out=%r err=%r" % post.returncode, post.stdout, post.stderr :120 - malformed = subprocess.run sys.executable, GUARD, "--phase", "pre" , - input="not-json", capture output=True, text=True, - env=env, timeout=30 - check "битый write-payload блокируется fail-closed", malformed.returncode == 2 - readonly = run guard {"session id": "reader", "cwd": root, - "tool name": "Bash", - "tool input": {"command": "git status --short"}}, env=env - check "read-only shell не берёт лишний замок", readonly.returncode == 0 - if HOOKS not in sys.path: - sys.path.insert 0, HOOKS - import workspace write guard as guard module - check "read-only whitelist не пропускает PowerShell script-block", - not guard module.shell is read only - "Get-Content x | Where-Object { Remove-Item x; $true }" - check "read-only whitelist не пропускает git --output", - not guard module.shell is read only "git diff --output=stolen.patch" - 14.09.2026, класс lease-storm. ЗДЕСЬ БЫЛ ОБРАТНЫЙ ИНВАРИАНТ: python mystery.py - лизовал cwd ЦЕЛИКОМ, и тест требовал, чтобы сосед после этого не мог править - СВОЙ файл в той же папке. Это и есть шторм, записанный как «защита»: замер 14.09 - -- 198 блокировок, 69% из них запрос ПАПКИ, 36 пострадавших сессий, среди жертв - чтение файла. Защита была ещё и мнимой: путь цели в такой команде не назван, так - что от РЕАЛЬНОЙ гонки за файл папочная лиза не спасала -- она лишь запирала - непричастных. Новый инвариант: неизвестная команда без признака записи не лизует - ничего, а гонку за конкретный файл по-прежнему ловит файловый инструмент. - unknown writer = run guard {"session id": "script-A", "cwd": root, - "tool name": "Bash", - "tool input": {"command": "python mystery.py"}}, env=env - neighbour = run guard {"session id": "script-B", "cwd": root, - "tool name": "Edit", - "tool input": {"file path": target}}, env=env - check "неизвестный shell-процесс НЕ запирает соседей по папке", - unknown writer.returncode == 0 and neighbour.returncode == 0, - "writer=%s neighbour=%s" % unknown writer.returncode, neighbour.returncode - same file = run guard {"session id": "script-C", "cwd": root, - "tool name": "Edit", - "tool input": {"file path": target}}, env=env - check "гонка за ОДИН файл по-прежнему отбивается", - same file.returncode == 2, "same file=%s" % same file.returncode - run guard {"session id": "script-B", "cwd": root}, "stop", env - run guard {"session id": "script-A", "cwd": root}, "stop", env - env sid = dict env - env sid "CODEX THREAD ID" = "env-only-session" - env target = os.path.join root, "env-session.md" - env acquire = run guard {"cwd": root, "tool name": "Edit", - "tool input": {"file path": env target}}, env=env sid - env stop = subprocess.run sys.executable, TURNSTATE , - input=json.dumps {"cwd": root, "hook event name": "Stop"} , - capture output=True, text=True, encoding="utf-8", - errors="replace", env=env sid, timeout=30 - env after = run guard {"session id": "after-env", "cwd": root, - "tool name": "Edit", - "tool input": {"file path": env target}}, env=env - check "Stop использует тот же env fallback session-id", - env acquire.returncode == 0 and env stop.returncode == 0 and - env after.returncode == 0, - "acquire=%s stop=%s after=%s" % env acquire.returncode, - env stop.returncode, env after.returncode - help run = subprocess.run sys.executable, GUARD, "--help" , - capture output=True, text=True, timeout=30 - bad flag = subprocess.run sys.executable, GUARD, "--wat" , - capture output=True, text=True, timeout=30 - check "guard --help", help run.returncode == 0 and "usage:" in help run.stdout - check "guard отвергает неизвестный флаг", bad flag.returncode == 2 - finally: - shutil.rmtree root, ignore errors=True - bad = name for name, ok, in RESULTS if not ok - print "\nВЕРДИКТ: %s" % "PASS" if not bad else "FAIL: " + "; ".join bad - return 0 if not bad else 1 - -if name == " main ": - try: - sys.stdout.reconfigure encoding="utf-8" - sys.stderr.reconfigure encoding="utf-8" - except Exception: - pass - if len sys.argv 1 and sys.argv 1 == "--guard-child": - sys.exit guard child sys.argv 2 , sys.argv 3 , sys.argv 4 , sys.argv 5 - sys.path.insert 0, SCRIPTS - sys.exit main diff --git a/ test workspace write guard.py b/ test workspace write guard.py new file mode 100644 index 000000000..e2c264e78 --- /dev/null +++ b/ test workspace write guard.py @@ -0,0 +1,830 @@ + - - coding: utf-8 - - +"""Integration acceptance for Claude/Codex shared-workspace safety. + +Purpose: prove both harness configs route canonical Bash/exec command/shell and +apply patch through one guard, Codex turn id becomes a fencing epoch, and a delayed +old Stop cannot release the next turn. Input: live configs plus an isolated temp DB. +Output: PASS/FAIL and evidence; never edits a real workspace file. A separate fresh +Codex-session canary is still required to prove the harness actually invoked hooks. +Caller: nightly regress grid and /tt after shared-workspace changes. Rail: local +Python stdlib, 0 LLM/network. + +KILL-LIST source/config mutation - case that must fail : +- remove Bash/PowerShell/exec command/shell from Codex PreToolUse matcher - - mutation: удаление shell matcher обязано стать красным ; +- disable/remove the persisted Codex PreToolUse trust state - - Codex PreToolUse trust state включён ; +- replace the configured hook by a missing command, dropped stdin, or echo - - live hooks.json command исполняется и три command mutants красные ; +- release by session while ignoring turn id - - поздний Stop старого turn id не снимает новую эпоху ; +- keep agent/