diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 20:37:54 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 20:37:54 +0200 |
| commit | 4d717018ea8a748b8bb16ad71c3deab6d8c20545 (patch) | |
| tree | dc4996d6904d2f89120b07cfa2c59c0fb41c8e75 /scripts/chat.py | |
| parent | e34c1203500451e65f0439435df55a2fa4c0f4ab (diff) | |
fix(chat): GLM review batch — corruption, traversal, cursor truncation, double print (v4.6.78-80)
12 findings accepted, 3 refuted with evidence (see _docs/chat-review-glm-2026-10-23.md).
Data loss: a corrupt thread read as [] and the next append rewrote it with one message —
now 500 and the file is left alone. Cursor after a >50-message truncation pointed at the
thread tail, making the oldest permanently unfetchable — now the last message actually
sent, plus truncated:true. Two posts in the same millisecond shared a cursor — epoch is
now strictly increasing.
Security: chat.py show() had no id validation and joined argv onto CHAT_DIR ('../../x'
read any .json as darkbot) — THREAD_RE added. The auto-reply prompt now treats message
text as untrusted data and forbids acting on instructions found in it.
Ops: relay housekeeping swept neither .lock-* nor orphaned .tmp-*; a crashed listing
looked identical to an empty queue; grep AWAITING also matched message bodies; hermes-poll
printed every reply twice.
Verified: corrupt file -> 500 with the file untouched; traversal -> 'invalid thread id';
hermes-poll -> exactly one line per reply.
Diffstat (limited to 'scripts/chat.py')
| -rwxr-xr-x | scripts/chat.py | 39 |
1 files changed, 33 insertions, 6 deletions
diff --git a/scripts/chat.py b/scripts/chat.py index 5b44b64..821bb31 100755 --- a/scripts/chat.py +++ b/scripts/chat.py @@ -24,11 +24,22 @@ def now_ms(): def load(path): - try: - with open(path) as f: - return json.load(f) - except Exception: + """Read a thread. Missing is empty; corrupt is an error. + + Returning [] for both let reply() rewrite a damaged thread with a single message — + history gone, and nothing said so. + """ + if not os.path.exists(path): return [] + with open(path) as f: + raw = f.read() + try: + data = json.loads(raw) + except json.JSONDecodeError as exc: + raise SystemExit('thread file is corrupt (%s): %s' % (path, exc)) + if not isinstance(data, list): + raise SystemExit('thread file is not a list: ' + path) + return data def threads(): @@ -38,7 +49,14 @@ def threads(): for name in sorted(os.listdir(CHAT_DIR)): if not name.endswith('.json') or name.startswith('.'): continue - msgs = load(os.path.join(CHAT_DIR, name)) + try: + msgs = load(os.path.join(CHAT_DIR, name)) + except SystemExit as exc: + # one unreadable file must not make the whole listing fail: that is how + # awaiting messages silently stop being listed + out.append({'thread': name[:-5], 'count': -1, 'last_ts': '', 'last_author': 'corrupt', + 'awaiting': True, 'preview': 'CORRUPT: ' + str(exc)[:90]}) + continue if not msgs: continue last = msgs[-1] @@ -62,6 +80,10 @@ def reply(thread, text): raise SystemExit('no such thread: ' + thread) lockpath = os.path.join(CHAT_DIR, '.lock-' + thread) with open(lockpath, 'a+') as lock: + # created here as darkbot (0644 under umask 022); PHP opens the same lock as + # www-data, so group write is mandatory or later posts to this thread 500 + try: os.chmod(lockpath, 0o664) + except OSError: pass fcntl.flock(lock, fcntl.LOCK_EX) try: msgs = load(path) @@ -102,10 +124,15 @@ def main(): return 0 for t in rows: flag = 'AWAITING' if t['awaiting'] else 'replied ' - print(f"{t['thread']} {flag} {t['count']:>3} msg {t['last_ts']} {t['preview']}") + if t['count'] == -1: + flag = 'CORRUPT ' + print(f"{t['thread']} {flag} {max(t['count'], 0):>3} msg {t['last_ts']} {t['preview']}") return 0 if cmd == 'show' and len(sys.argv) >= 3: + # reply() validated the id, show() did not: '../../x' joined straight onto CHAT_DIR + if not THREAD_RE.match(sys.argv[2]): + raise SystemExit('invalid thread id (expected 16 lowercase hex chars)') msgs = load(os.path.join(CHAT_DIR, sys.argv[2] + '.json')) if not msgs: print('(empty or unknown thread)') |