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 +12 -0
- package/claude-session.mjs +12 -1
- package/package.json +1 -1
- package/session-store.mjs +14 -1
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
|
package/claude-session.mjs
CHANGED
|
@@ -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;
|
|
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
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
|
-
|
|
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.
|