thinkpool-pair 0.7.158 → 0.7.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/account.mjs CHANGED
@@ -16,6 +16,7 @@ import os from 'node:os'
16
16
  import { saveAuth, loadAuth, loadDirs, bindDir, loadServed, rememberServed, loadDefaultDir, saveDefaultDir, claudeRecentProjectDir } from './auth-store.mjs'
17
17
  import { isSafeToRestart } from './update-gate.mjs'
18
18
  import { makeThrottledTrack } from './presence.mjs'
19
+ import { supervisorServes } from './serve-consent.mjs'
19
20
  import { resolveServeDir } from './serve-dir.mjs'
20
21
  import { pairKeyFor, pairTopic, CROSSROOM_BUS } from './cross-terminal.mjs'
21
22
 
@@ -529,26 +530,32 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
529
530
  return
530
531
  }
531
532
  let rooms = []
533
+ const me = session.user.id
532
534
  try {
533
- // Owner-only discovery (Contract C-CODE-2): a host bridges ONLY the sessions
534
- // it CREATED. RLS on code_sessions returns owned OR participant rooms, so
535
- // without this owner filter the supervisor auto-serves rooms you merely JOINED
536
- // as a guest — spawning your machine into another owner's session that already
537
- // runs its own bridge. The web watches tpacct:<session-owner>, so a
538
- // participant's bridge is duplication the UI never even looks at.
539
- const { data } = await sb.from('code_sessions').select('code,name,last_active_at,owner_id,participants').eq('owner_id', session.user.id).order('last_active_at', { ascending: false }).limit(20)
535
+ // Discovery (Contract C-CODE-2, evolved owner-only → owner-OR-granted): a host
536
+ // bridges the sessions it OWNS *plus* any it has been explicitly granted to serve
537
+ // (code_sessions.serve_grant_uid = me — the owner-consent grant, #206/#208). RLS
538
+ // returns owned OR participant rooms, and a grantee is ALWAYS a participant
539
+ // (set_serve_grant requires it), so a granted row is visible to this login. Without
540
+ // a filter the supervisor would auto-serve rooms you merely JOINED as a guest —
541
+ // spawning your machine into another owner's session that already runs its own
542
+ // bridge; the .or keeps un-granted joins out while letting a consented grant IN.
543
+ // The web watches tpacct:<session-owner>; a granted bridge serves on the owner's
544
+ // behalf (the grant IS the consent), so it is wanted duplication, not stray.
545
+ const { data } = await sb.from('code_sessions').select('code,name,last_active_at,owner_id,serve_grant_uid,participants').or(`owner_id.eq.${me},serve_grant_uid.eq.${me}`).order('last_active_at', { ascending: false }).limit(20)
540
546
  rooms = data || []
541
- // Prune refused entries for rooms we no longer own (only on a SUCCESSFUL fetch,
542
- // so a transient query error never wipes the set).
543
- const ownedCodes = new Set(rooms.map((r) => r.code))
544
- for (const c of [...refused.keys()]) if (!ownedCodes.has(c)) refused.delete(c)
547
+ // Prune refused entries for rooms we can no longer serve — owned OR granted (only
548
+ // on a SUCCESSFUL fetch, so a transient query error never wipes the set).
549
+ const servableCodes = new Set(rooms.map((r) => r.code))
550
+ for (const c of [...refused.keys()]) if (!servableCodes.has(c)) refused.delete(c)
545
551
  } catch { /* transient — keep existing children, retry next tick */ }
546
552
  const dirs = loadDirs()
547
553
  const served = loadServed()
548
554
  for (const r of rooms) {
549
555
  const room = r.code
550
556
  if (!room || children.has(room)) { if (room) refused.delete(room); continue } // served → not refused
551
- if (r.owner_id && r.owner_id !== session.user.id) continue // never serve a room we don't own (defense-in-depth behind the .eq above)
557
+ // Defense-in-depth behind the .or above: serve iff we OWN the room or hold its grant.
558
+ if (!supervisorServes({ ownerId: r.owner_id, grantUid: r.serve_grant_uid, myUid: me })) continue
552
559
  // Resolve the serve dir: explicit bind > last-served memory > launch dir. REFUSE
553
560
  // to auto-serve an unbound room from $HOME — that strips the agent's resume context
554
561
  // (cwd-keyed SDK sessions) and shows your home folder as the repo (FVIHV1DE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.158",
3
+ "version": "0.7.159",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {
package/serve-consent.mjs CHANGED
@@ -35,6 +35,18 @@ export function serveDecision ({ ownerId, grantUid, myUid }) {
35
35
  return { serve: false, reason: 'not-granted' }
36
36
  }
37
37
 
38
+ // supervisorServes — the account supervisor's per-row discovery decision: may this
39
+ // login auto-serve THIS discovered room? True iff the login OWNS the room or HOLDS its
40
+ // serve grant. This is the defense-in-depth per-row check behind the supervisor's
41
+ // `.or(owner_id.eq.me,serve_grant_uid.eq.me)` query — a broad RLS row (owned OR merely
42
+ // participant) must still resolve to owned-or-granted before a child is spawned. Mirrors
43
+ // serveDecision's owner|grant serve set, evaluated against a discovery row rather than a
44
+ // gate fetch (Contract C-CODE-2, evolved owner-only → owner-or-granted).
45
+ export function supervisorServes ({ ownerId, grantUid, myUid }) {
46
+ if (!myUid) return false
47
+ return ownerId === myUid || grantUid === myUid
48
+ }
49
+
38
50
  // amGrantee — this bridge serves by grant (not as owner). Grantees are the only
39
51
  // bridges that ever stand down; the owner's bridge never yields.
40
52
  export function amGrantee ({ ownerId, grantUid, myUid }) {