mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-19 19:32:18 +02:00
fix(browse): migrate hand-rolled atomic writes to lib/fs-atomic
Seven sites, each audited for its existing throw-vs-swallow contract before
migrating: writeSessionState + the four fire-and-forget tab/state writers use
atomicWriteQuiet (they swallowed before); writeAgentRecord + the boot-time
port-file write use atomicWriteSync (they threw before — and writeAgentRecord
previously leaked its tmp file on rename failure, which the helper cleans).
All carry {mode: 0o600} plus restrictFilePermissions after successful writes,
preserving the Windows ACL hardening that writeSecureFile provided (mode bits
are POSIX-only). server.ts untouched: its three state writes route through
tmpStatePath(), pinned by server-tmp-state-path.test.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ef0fa9e9ff
commit
2b27d89ae3
@@ -23,7 +23,8 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { writeSecureFile, mkdirSecure } from './file-permissions';
|
||||
import { writeSecureFile, restrictFilePermissions, mkdirSecure } from './file-permissions';
|
||||
import { atomicWriteSync, atomicWriteQuiet } from '../../lib/fs-atomic';
|
||||
import { safeUnlink } from './error-handling';
|
||||
import { writeAgentRecord, clearAgentRecord } from './terminal-agent-control';
|
||||
import { extractPtyCookie } from './pty-session-cookie';
|
||||
@@ -267,12 +268,9 @@ function writeClaudeAvailable(): void {
|
||||
checked_at: new Date().toISOString(),
|
||||
};
|
||||
const target = path.join(stateDir, 'claude-available.json');
|
||||
const tmp = path.join(stateDir, `.tmp-claude-${process.pid}`);
|
||||
try {
|
||||
writeSecureFile(tmp, JSON.stringify(status, null, 2));
|
||||
fs.renameSync(tmp, target);
|
||||
} catch {
|
||||
safeUnlink(tmp);
|
||||
// Fire-and-forget state file: a failed write must not break boot.
|
||||
if (atomicWriteQuiet(target, JSON.stringify(status, null, 2), { mode: 0o600 })) {
|
||||
restrictFilePermissions(target); // Windows ACL hardening
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,12 +879,10 @@ function handleTabState(msg: {
|
||||
})),
|
||||
};
|
||||
const target = path.join(stateDir, 'tabs.json');
|
||||
const tmp = path.join(stateDir, `.tmp-tabs-${process.pid}`);
|
||||
try {
|
||||
writeSecureFile(tmp, JSON.stringify(payload, null, 2));
|
||||
fs.renameSync(tmp, target);
|
||||
} catch {
|
||||
safeUnlink(tmp);
|
||||
// Fire-and-forget state file: atomic write (via lib/fs-atomic) so
|
||||
// claude never reads a half-written JSON document; failures swallowed.
|
||||
if (atomicWriteQuiet(target, JSON.stringify(payload, null, 2), { mode: 0o600 })) {
|
||||
restrictFilePermissions(target); // Windows ACL hardening
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,17 +892,12 @@ function handleTabState(msg: {
|
||||
const active = msg.active;
|
||||
if (active && active.url && !active.url.startsWith('chrome://') && !active.url.startsWith('chrome-extension://')) {
|
||||
const ctxFile = path.join(stateDir, 'active-tab.json');
|
||||
const tmp = path.join(stateDir, `.tmp-tab-${process.pid}`);
|
||||
try {
|
||||
writeSecureFile(tmp, JSON.stringify({
|
||||
tabId: active.tabId ?? null,
|
||||
url: active.url,
|
||||
title: active.title ?? '',
|
||||
}));
|
||||
fs.renameSync(tmp, ctxFile);
|
||||
} catch {
|
||||
safeUnlink(tmp);
|
||||
}
|
||||
const ok = atomicWriteQuiet(ctxFile, JSON.stringify({
|
||||
tabId: active.tabId ?? null,
|
||||
url: active.url,
|
||||
title: active.title ?? '',
|
||||
}), { mode: 0o600 });
|
||||
if (ok) restrictFilePermissions(ctxFile); // Windows ACL hardening
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,17 +907,13 @@ function handleTabSwitch(msg: { tabId?: number; url?: string; title?: string }):
|
||||
|
||||
const stateDir = path.dirname(STATE_FILE);
|
||||
const ctxFile = path.join(stateDir, 'active-tab.json');
|
||||
const tmp = path.join(stateDir, `.tmp-tab-${process.pid}`);
|
||||
try {
|
||||
writeSecureFile(tmp, JSON.stringify({
|
||||
tabId: msg.tabId ?? null,
|
||||
url,
|
||||
title: msg.title ?? '',
|
||||
}));
|
||||
fs.renameSync(tmp, ctxFile);
|
||||
} catch {
|
||||
safeUnlink(tmp);
|
||||
}
|
||||
// Fire-and-forget: atomic write via lib/fs-atomic, failures swallowed.
|
||||
const ok = atomicWriteQuiet(ctxFile, JSON.stringify({
|
||||
tabId: msg.tabId ?? null,
|
||||
url,
|
||||
title: msg.title ?? '',
|
||||
}), { mode: 0o600 });
|
||||
if (ok) restrictFilePermissions(ctxFile); // Windows ACL hardening
|
||||
|
||||
// Best-effort sync to parent server so its activeTabId tracking matches.
|
||||
// No await; this is fire-and-forget.
|
||||
@@ -964,11 +951,11 @@ function main() {
|
||||
}
|
||||
|
||||
// Write port file atomically so the parent server can pick it up.
|
||||
// Throws on failure — a boot without a discoverable port file is broken.
|
||||
const dir = path.dirname(PORT_FILE);
|
||||
try { mkdirSecure(dir); } catch {}
|
||||
const tmp = `${PORT_FILE}.tmp-${process.pid}`;
|
||||
writeSecureFile(tmp, String(port));
|
||||
fs.renameSync(tmp, PORT_FILE);
|
||||
atomicWriteSync(PORT_FILE, String(port), { mode: 0o600 });
|
||||
restrictFilePermissions(PORT_FILE); // Windows ACL hardening
|
||||
|
||||
// Write identity-based agent record (pid + per-boot gen). Replaces the
|
||||
// v1.43- `pkill -f terminal-agent\.ts` regex teardown that could kill
|
||||
|
||||
Reference in New Issue
Block a user