From dea74b777ef4eb0fd4458de26690568954431df8 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Fri, 6 Nov 2026 11:45:25 +0700 Subject: [PATCH] fix(server-mode): a session-only share resumes in a free slot only A start that names no slot means pane 0; for a user with only a session of the workspace that was let through and answered ERUNLIVE whenever another user's agent held pane 0 (found by the browser run). The client already sends slot: "auto"; the scope now requires it. --- server/__tests__/lane-shares.test.js | 8 ++++++-- server/lib/scope.js | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/server/__tests__/lane-shares.test.js b/server/__tests__/lane-shares.test.js index d78ccd3b..cc2cfcc3 100644 --- a/server/__tests__/lane-shares.test.js +++ b/server/__tests__/lane-shares.test.js @@ -479,6 +479,7 @@ describe("sharing one session", () => { }); await refused("POST", "/api/run", { laneId: laneS, initialPrompt: "hi" }); await refused("POST", `/api/lanes/${laneS}/start`, { resumeSessionId: "s1-shared", slot: 0 }); + await refused("POST", `/api/lanes/${laneS}/start`, { resumeSessionId: "s1-shared" }); await refused("GET", `/api/lanes/${laneS}/git/tree?source=1`); await refused("GET", `/api/artifacts/candidates?laneId=${laneS}`); await refused("PATCH", "/api/sessions/s1-shared", { name: "mine now" }); @@ -494,7 +495,10 @@ describe("sharing one session", () => { db.prepare("INSERT INTO session_shares (session_id, user_id) VALUES ('sT-other', ?)").run( users.carol.id ); - await refused("POST", `/api/lanes/${laneT}/start`, { resumeSessionId: "s1-shared" }); + await refused("POST", `/api/lanes/${laneT}/start`, { + resumeSessionId: "s1-shared", + slot: "auto", + }); db.prepare("DELETE FROM session_shares WHERE session_id = 'sT-other'").run(); // Another session of it is not even there. const other = await call("POST", `/api/lanes/${laneS}/start`, { @@ -505,7 +509,7 @@ describe("sharing one session", () => { // A resume of the shared one gets past the scope (and then needs their own account). const resume = await call("POST", `/api/lanes/${laneS}/start`, { as: "carol", - body: { resumeSessionId: "s1-shared", prompt: "hi" }, + body: { resumeSessionId: "s1-shared", prompt: "hi", slot: "auto" }, }); assert.ok(![403, 404].includes(resume.status), JSON.stringify(resume.body)); // Reads of the workspace's git and its own row pass. diff --git a/server/lib/scope.js b/server/lib/scope.js index 79bba7f9..edf491f3 100644 --- a/server/lib/scope.js +++ b/server/lib/scope.js @@ -399,8 +399,9 @@ function sessionShareAllows(req, rest, laneId) { b.forkSession !== true && b.shell !== true && b.resume !== true && - // A free slot the server picks: pane 0 is the workspace's own run. - (b.slot === undefined || b.slot === "auto") && + // A free slot the server picks (no slot means pane 0, the workspace's + // own run — another user's live agent there would answer ERUNLIVE). + b.slot === "auto" && sessionSharedWith(b.resumeSessionId, memberId(req)) && Boolean( sql("SELECT 1 FROM sessions WHERE id = ? AND lane_id = ?").get(b.resumeSessionId, laneId)