thinkpool-pair 0.7.144 → 0.7.145

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/bridge.mjs CHANGED
@@ -353,6 +353,18 @@ if (!argv[0] || argv[0].startsWith('-')) { const { runAccount } = await import('
353
353
 
354
354
  const room = (argv[0] || '').toUpperCase().trim()
355
355
  if (!room) { console.error('usage: npx thinkpool-pair <ROOM> [--headless] [--continue|--fresh] [-- <command…>] | npx thinkpool-pair (account mode)'); process.exit(1) }
356
+ // Security (audit 2026-07-02): `room` becomes a filesystem path segment under
357
+ // ~/.thinkpool-pair (session-store) AND is interpolated into launchctl/systemd
358
+ // service labels. sessions.id is unconstrained TEXT, so refuse path-unsafe / shell-
359
+ // active characters at the gate instead of trusting the upstream session id.
360
+ // Mirrors service.mjs's ROOM_CODE_RE, broadened for the dash in code_sessions
361
+ // codes and slightly longer ids. Every room-interpolating path is now guarded.
362
+ // NOTE: pure startup gate — runs before any realtime channel exists; does not
363
+ // alter emit topic/event/presence (BR4 surface untouched).
364
+ if (!/^[A-Z0-9_-]{2,32}$/.test(room)) {
365
+ console.error(`refused: invalid room code ${JSON.stringify(room)} (expected 2–32 alphanumerics, dash, or underscore)`)
366
+ process.exit(1)
367
+ }
356
368
  // ── Supervisor mode (--supervise / --keep-alive): keep the bridge alive across
357
369
  // crashes. We re-exec ourselves without the flag and respawn the child on any
358
370
  // non-clean exit with exponential backoff. Zero-dependency, cross-platform. With
@@ -1006,7 +1006,18 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
1006
1006
  return {
1007
1007
  // A lazy (restored-idle) session cold-boots the query on its first turn. input.push
1008
1008
  // is queue-backed, so the pushed turn buffers and runs once the query is ready.
1009
- sendTurn(text) { if (!closed) { if (!started) runQuery(); turnActive = true; sawSuggestion = false; lastTurnText = String(text); if (sugTimer) { clearTimeout(sugTimer); sugTimer = null } lastEvtTs = Date.now(); stalledSent = false; input.push([{ type: 'text', text: String(text) }, { type: 'text', text: roomReminder() }]) } },
1009
+ sendTurn(text) { if (!closed) { if (!started) runQuery(); turnActive = true; sawSuggestion = false; lastTurnText = String(text); if (sugTimer) { clearTimeout(sugTimer); sugTimer = null } lastEvtTs = Date.now(); stalledSent = false;
1010
+ const t = String(text)
1011
+ // SDK 0.3.198 only recognizes a slash command (/compact, /clear, custom /cmds) when the
1012
+ // user message is a SINGLE text block. Appending the per-turn ROOM NOW <system-reminder>
1013
+ // as a 2nd block made 0.3.198 treat /compact as a plain turn → compaction silently
1014
+ // no-op'd (Max, 2026-07-02; 0.3.185 tolerated the extra block, 0.3.198 tightened it —
1015
+ // verified against the SDK: single-block → the compact command RUNS, two-block → not
1016
+ // recognized). So a slash command goes CLEAN; conversational turns keep the reminder.
1017
+ // Normal turns never start with "/" (composeAgentStdin prepends the preamble), and the
1018
+ // web already routes "/"-prefixed input as a command (pane.jsx), so this matches intent.
1019
+ input.push(/^\s*\//.test(t) ? [{ type: 'text', text: t }] : [{ type: 'text', text: t }, { type: 'text', text: roomReminder() }])
1020
+ } },
1010
1021
  // Cold-boot the query WITHOUT sending a turn — the background warmer calls this on
1011
1022
  // lazily-restored idle terminals so they're ready before the user clicks them.
1012
1023
  warm() { if (!started && !closed) runQuery() },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.144",
3
+ "version": "0.7.145",
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/session-store.mjs CHANGED
@@ -17,7 +17,20 @@ import path from 'node:path'
17
17
  // can point each case at a fresh tempdir after importing this module. Production leaves
18
18
  // it unset → the real home-dir store.
19
19
  const ROOT = () => process.env.TP_PAIR_ROOT || path.join(os.homedir(), '.thinkpool-pair')
20
- const dir = (room) => path.join(ROOT(), room || 'default')
20
+ // Security (audit 2026-07-02): `room` is an external value (a session/code id)
21
+ // used as a filesystem path segment. sessions.id is unconstrained TEXT in the
22
+ // DB and client-generated, so a crafted id like '../../foo' would otherwise let
23
+ // a hostile room write session files OUTSIDE ~/.thinkpool-pair. Contain EVERY
24
+ // derived path under ROOT: resolve and refuse if the result escapes. Pair of
25
+ // the bridge.mjs startup format check — belt + suspenders.
26
+ const dir = (room) => {
27
+ const root = path.resolve(ROOT())
28
+ const target = path.resolve(root, room || 'default')
29
+ if (target !== root && !target.startsWith(root + path.sep)) {
30
+ throw new Error(`refused: room path escapes sandbox: ${JSON.stringify(room)}`)
31
+ }
32
+ return target
33
+ }
21
34
  const archiveDir = (room) => path.join(dir(room), '.archive')
22
35
  // Only resume the live SDK context for recent sessions — an expired session id
23
36
  // fails ("No conversation found"); past this window we restore transcript only.