Repository navigation
fix(server): atomic session ownership claim - deny a lost claim instead of streaming in a foreign session (Eve2-F1) - #616
Merged
Conversation
…nstead of streaming in a foreign session (Eve2-F1) ownsSession() checked sessionOwner() and sessionStored() across awaits and then called claimSession(), which silently no-opped when the id was already claimed. Two concurrent first-touch POST /chat calls for one unowned session both passed: the losing principal streamed a full turn inside the winner's session and received its transcript. claimSession() now reports whether the caller holds the claim afterward. ownsSession() returns its answer, so a concurrent first touch that registered another owner first is refused (403 LOUSHO_SESSION_FORBIDDEN). claimOnCommit() checks its re-claim too: when the claim was forgotten past the owner cap and taken by another principal while the turn ran, the turn's transcript is cleared again and the stream ends on the same coded error.
4 tasks done
LinuxDevil
added a commit
that referenced
this pull request
Oct 12, 2026
…ata modes for the rest (#620) - products-admin: new driver that points the coding-agent machinery (plan mode, approval-gated fs+shell, durable store, loop guard) at a real greenfield build — a products admin dashboard CRUD (JSON store, node:http REST, single-page dashboard, node --test suite). The harness boots the agent-built server and verifies the full CRUD over real HTTP. - repo-maintainer: REPO_FIXTURE=<dir> reviews a real checkout + diff; ships real-fixture/ (the merged PR #616 diff + files at head). B5 adapts: a real diff has no planted bugs, so the check asserts a coherent typed review. - research-analyst: CORPUS_DIR=<dir> researches real .md/.txt/.mdx docs (verified against ../docs). Cost check fixed: providers that bill per call (OpenRouter usage.cost applies cache discounts) can't bit-match a fresh registry estimate — re-estimate with cachedInputTokens and allow a 15% band; exact equality still required when nothing was billed. - incident-responder: INCIDENT_TELEMETRY=<file.json> replaces canned telemetry; real-telemetry.json is built from actual run logs + git history. - REPORT.md: real-task round results, including the honest finding that gpt-4o-mini cannot finish open-ended builds (use AUDIT_MODEL override). Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Finding (Eve2-F1, critical)
Session ownership was a check-then-act race.
ownsSession()(src/server/fetchRoutes.ts) checkedsessionOwner()+sessionStored()across awaits and then calledclaimSession()(src/server/requestGuard.ts), which silently no-opped when the id was already claimed;ownsSessionthen returnedtrueunconditionally.Two concurrent first-touch
POST /chatcalls for one unowned session both passed: the losing principal streamed a full turn inside a session owned by the other caller and received its transcript content (verified byaudit/eve2/repro/f1-session-squat-toctou.tsonaudit/eve2-findings: mallory's SSE carried alice's secret).claimOnCommit()had the same unguarded re-claim.Fix
claimSession()now reports whether this caller holds the claim afterward:truewhen it just took (or already held) it,falsewhen another principal owns it. The check-and-set is one synchronous step, so it is atomic.ownsSession()returns the claim's answer, so a concurrent first touch that registered another owner first is refused with403 LOUSHO_SESSION_FORBIDDEN— never streamed.claimOnCommit()checks its re-claim: when the claim was evicted past the owner cap and taken by another principal while the turn ran, the turn's transcript is cleared (session.clear(), best-effort) and the stream ends on the same coded error instead of a cleandone.POST /chat, the deprecated{ message }body,GET /chat/:id, approvals, thePOST <base>/uiuseChat endpoint andserveFetch()(deployed node server / Worker). Channel routes (mountChannels/mountFetchChannels) do not use the session-owner guard — verified unaffected.Test
Two new regression tests in
src/server/requestGuard.test.ts(both failed before the fix):LOUSHO_SESSION_FORBIDDENfor the loser, none of the loser's input in the winner's stream or the stored transcript, and a 403 on the loser's sequential retry.{ message }path while a gated turn is in flight — asserts the stream ends on the coded error and the stored transcript is cleared.Existing guard tests (squatting, fail-closed stored sessions, bounded owner map, /ui) all still pass.
Gates
npx tsc --noEmit -p tsconfig.json✅npm run lint✅npx vitest run src/server✅ 89 tests (7 files)malloryStatus: 403,mallorySeesAliceSecret: false, transcript holds only alice's turn → "no bug" (exit 1)claimSessionis internal, absent fromapi/*.api.md) — noapi:updateneeded.