fix(gateway): refuse to boot when the data home cannot persist state - #2279
Conversation
b073e3c to
3a359f7
Compare
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Design Review (Fable 5, fork) — ✅ PASSAdvisory design-level review of Design assessment complete. The change is a boot-time persistence preflight ( Design-Verdict: PASS Real observed harm, guarded with the exact production primitives, at the right layer, before the first consumer — loud, scoped, and trivially reversible. [DESIGN-REVIEWED] 5e0c688 |
3a359f7 to
03c431a
Compare
|
Disposition — GPT round 1 (SHA 3a359f7 → 03c431a):
|
Opus 5 Review (fork) — ✅ no blocking findingsReviewed |
03c431a to
b7860fc
Compare
|
Disposition — GPT round 2 (SHA 03c431a → b7860fc):
|
bolichen97
left a comment
There was a problem hiding this comment.
Approved for rc.3: green CI, targeted fix, reviewed for insider release cut.
Head branch was pushed to by a user without write access
375ec8f to
872a57c
Compare
|
Disposition — GPT round 4 (SHA 375ec8f → 872a57c):
This closes the enumeration: the probe now covers create, write, lock, atomic-replace, and delete — every filesystem primitive the persistence layer performs. |
872a57c to
70b6694
Compare
|
Disposition — GPT round 5 (SHA 872a57c → 70b6694):
|
A gateway whose environment breaks basic file syscalls still binds its port and serves traffic, but every save silently fails: chat-slot history writes, cron-history appends, and session-PID tracking all need mkstemp + an advisory lock in the data home. Observed on Linux when a gateway is spawned from inside a sandboxed agent session: the sandbox's seccomp filter survives fork/exec (nohup included), so fcntl.flock and tempfile.mkstemp raise OSError(ENOSYS) while writes to already-open fds keep working. The process limps along for its whole lifetime dropping chat history on the floor, and the shutdown save of every slot fails. A second gateway started from the same environment crashes with a raw traceback in cleanup_orphaned_sessions instead of a useful error. Add probe_file_persistence() to platform_compat (mkstemp + file_lock in the data home, probe file removed in all outcomes) and run it at the top of GatewayOrchestrator.run(), before the first lock consumer. On failure the gateway logs CRITICAL, prints an actionable one-liner (naming the inherited-seccomp cause when errno is ENOSYS), and exits 1 -- loud and early instead of silent data loss.
70b6694 to
5e0c688
Compare
|
Disposition — GPT round 6 (SHA 70b6694 → 5e0c688):
With this the probe covers create → write → fsync → lock → atomic-replace (via |
) The 0.2.0 section was written in #2305, the commit that became v0.2.0-rc.4. Seventy-one commits have landed on main since, nineteen of them feat:, and the section was never revisited. It therefore both omitted shipped features and described one that no longer exists as written. The wrong entry mattered most: the Webhooks bullet told the reader to manage inbound automation "from Settings", but #2343 moved that page behind a per-device Preview pages toggle under Developer and hides it by default. A 0.2.0 user following the release notes would have gone looking for a page that is not there. Added, all from the rc.6 range: opt-in Slack setup and the multi-channel repositioning (#2340), Telegram multi-account (#2203) and inbound attachments (#2201), sub-agent completions reaching non-Slack parents (#2352), Discord reply continuation (#2326), Slack OPTIONS as a control (#1467), the Agent Templates two-pane inspector, project-local agent discovery (#2167), send-a-copy-to-another-instance, Jira and setting link chips (#2019, #1907), CJK emphasis rendering, the MCP Apps switch (#2293, #2337), the Connections provider registry (#2285), GitHub Enterprise Server support in Code Review Sage (#2154), operator notes on user deny patterns (#2341), the locked git-publish floor rules (#2369), the persist-or-refuse boot guard (#2279), and the turn-ceiling bounds on the approval and stall windows (#2372, #2373). Scope is exactly 5fe4bd5..ab20b4e, the range v0.2.0-rc.6 ships. The three commits main carries beyond rc.6, including meeting deletion (#2268), belong to the next release and are deliberately not described here.
…irodotdev#2279) A gateway whose environment breaks basic file syscalls still binds its port and serves traffic, but every save silently fails: chat-slot history writes, cron-history appends, and session-PID tracking all need mkstemp + an advisory lock in the data home. Observed on Linux when a gateway is spawned from inside a sandboxed agent session: the sandbox's seccomp filter survives fork/exec (nohup included), so fcntl.flock and tempfile.mkstemp raise OSError(ENOSYS) while writes to already-open fds keep working. The process limps along for its whole lifetime dropping chat history on the floor, and the shutdown save of every slot fails. A second gateway started from the same environment crashes with a raw traceback in cleanup_orphaned_sessions instead of a useful error. Add probe_file_persistence() to platform_compat (mkstemp + file_lock in the data home, probe file removed in all outcomes) and run it at the top of GatewayOrchestrator.run(), before the first lock consumer. On failure the gateway logs CRITICAL, prints an actionable one-liner (naming the inherited-seccomp cause when errno is ENOSYS), and exits 1 -- loud and early instead of silent data loss.
…rodotdev#2412) The 0.2.0 section was written in kirodotdev#2305, the commit that became v0.2.0-rc.4. Seventy-one commits have landed on main since, nineteen of them feat:, and the section was never revisited. It therefore both omitted shipped features and described one that no longer exists as written. The wrong entry mattered most: the Webhooks bullet told the reader to manage inbound automation "from Settings", but kirodotdev#2343 moved that page behind a per-device Preview pages toggle under Developer and hides it by default. A 0.2.0 user following the release notes would have gone looking for a page that is not there. Added, all from the rc.6 range: opt-in Slack setup and the multi-channel repositioning (kirodotdev#2340), Telegram multi-account (kirodotdev#2203) and inbound attachments (kirodotdev#2201), sub-agent completions reaching non-Slack parents (kirodotdev#2352), Discord reply continuation (kirodotdev#2326), Slack OPTIONS as a control (kirodotdev#1467), the Agent Templates two-pane inspector, project-local agent discovery (kirodotdev#2167), send-a-copy-to-another-instance, Jira and setting link chips (kirodotdev#2019, kirodotdev#1907), CJK emphasis rendering, the MCP Apps switch (kirodotdev#2293, kirodotdev#2337), the Connections provider registry (kirodotdev#2285), GitHub Enterprise Server support in Code Review Sage (kirodotdev#2154), operator notes on user deny patterns (kirodotdev#2341), the locked git-publish floor rules (kirodotdev#2369), the persist-or-refuse boot guard (kirodotdev#2279), and the turn-ceiling bounds on the approval and stall windows (kirodotdev#2372, kirodotdev#2373). Scope is exactly 5fe4bd5..ab20b4e, the range v0.2.0-rc.6 ships. The three commits main carries beyond rc.6, including meeting deletion (kirodotdev#2268), belong to the next release and are deliberately not described here.
Problem
A gateway whose environment breaks basic file syscalls still binds its port and serves traffic — but every save silently fails. Chat-slot history writes, cron-history appends, and session-PID tracking all require
tempfile.mkstemp(viaatomic_write) plus an advisory lock (platform_compat.file_lock) in the data home.Observed in the wild (Linux dev host, 2026-08-08): a gateway spawned from inside a sandboxed agent session inherits the sandbox's seccomp filter — seccomp survives
fork/exec,nohup/disownincluded — sofcntl.flockandmkstempraiseOSError(ENOSYS)while writes to already-open fds keep working. The result over one 5-minute lifetime:Failed to save slot ... to history× N — silent chat-history loss),[Errno 38] Function not implemented,cleanup_orphaned_sessionsinstead of anything actionable.The process never exits on its own — it limps for its whole lifetime dropping writes.
Fix
platform_compat.probe_file_persistence(directory): creates a probe file withmkstempand locks it withfile_lock— the exact two primitives the persistence paths use. ReturnsNoneon success or a human-readable failure description; whenerrno == ENOSYS, the message names the inherited-seccomp cause and the remedy (start from a regular shell or the system service). Probe file removed in all outcomes.GatewayOrchestrator.run()runs the probe againstdata_home()before the first lock consumer (cleanup_orphaned_sessions, where the raw-traceback crash happened). On failure:CRITICALlog + one-line stderr message +SystemExit(1).Fail-loud over fail-silent, matching the repo's existing lock philosophy (
file_lockalready "FAILS CLOSED" on Windows rather than proceeding unserialized) and the pidfdENOSYSprobe precedent incli.py.Tests
test/test_persistence_probe.py(11 tests): healthy dir passes / leaves no litter / creates missing dirs; ENOSYS on create and on lock produce step-specific messages with the seccomp hint; non-ENOSYS errors get no misleading hint; probe file removed even when locking fails; orchestrator wiring —run()exits 1 on probe failure and does so beforecleanup_orphaned_sessionsruns.Verified: new module green,
test_no_config_dir_in_asyncratchet green (probe usesdata_home(), issue #1057), isort/flake8/mypy clean (mypy 1.14.1, CI-parity venv, 847 files). Full suite run: 38,395 passed; 69 failures reproduced identically on cleanorigin/main@ 6590a1c on the same host (git/network-dependent modules timing out in the sandboxed environment) — environmental, not from this diff.