mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(usage): write usage cache snapshots and sidecars owner-only (0600) (#31027)
Usage caches and their source sidecars hold session ids and project paths, but their durable temp files were created with the process default mode, so a replace under umask 022 published them as 0644. Thread an optional creation mode through writeFileDurable / writeFileDurableIfCurrent and pass 0o600 from UsageCacheSnapshotWriter and the usage source sidecar writer. Other durable writers are unchanged. Fixes #31026
This commit is contained in:
@@ -79,9 +79,10 @@ export async function writeTempFileDurable(
|
||||
export async function writeFileDurable(
|
||||
tmpPath: string,
|
||||
finalPath: string,
|
||||
payload: string | Uint8Array
|
||||
payload: string | Uint8Array,
|
||||
mode?: number
|
||||
): Promise<void> {
|
||||
await writeFileDurableIfCurrent(tmpPath, finalPath, payload, () => true)
|
||||
await writeFileDurableIfCurrent(tmpPath, finalPath, payload, () => true, mode)
|
||||
}
|
||||
|
||||
/** Veto superseded writes before rename and remove their temp files on every uncommitted path. */
|
||||
@@ -89,12 +90,14 @@ export async function writeFileDurableIfCurrent(
|
||||
tmpPath: string,
|
||||
finalPath: string,
|
||||
payload: string | Uint8Array,
|
||||
isCurrent: () => boolean
|
||||
isCurrent: () => boolean,
|
||||
/** Creation mode for the new temp file, e.g. 0o600 for state other users must not read. */
|
||||
mode?: number
|
||||
): Promise<boolean> {
|
||||
let renamed = false
|
||||
try {
|
||||
// Fsync before publication so power loss cannot expose an empty file.
|
||||
await writeTempFileDurable(tmpPath, payload)
|
||||
await writeTempFileDurable(tmpPath, payload, mode)
|
||||
if (!(await renameFileWithWindowsRetryAsync(tmpPath, finalPath, isCurrent))) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { UsageCacheSnapshotWriter } from './usage-cache-snapshot-writer'
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'replaces a world-readable usage cache with an owner-only snapshot',
|
||||
async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-usage-cache-mode-'))
|
||||
try {
|
||||
const file = join(directory, 'orca-claude-usage.json')
|
||||
writeFileSync(file, '{}')
|
||||
chmodSync(file, 0o644)
|
||||
const writer = new UsageCacheSnapshotWriter('[test]', () => file)
|
||||
|
||||
await writer.write(() => '{"sessionId":"s"}')
|
||||
|
||||
expect(readFileSync(file, 'utf8')).toBe('{"sessionId":"s"}')
|
||||
expect(statSync(file).mode & 0o777).toBe(0o600)
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -47,7 +47,9 @@ export class UsageCacheSnapshotWriter {
|
||||
durableWriteTempPath(usageFile),
|
||||
usageFile,
|
||||
payload,
|
||||
() => generation === this.generation
|
||||
() => generation === this.generation,
|
||||
// Usage caches hold session ids and project paths.
|
||||
0o600
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
@@ -40,6 +40,12 @@ describe('usage source cache file', () => {
|
||||
await expect(readUsageSourceCache(ref)).resolves.toEqual([{ path: 'a.jsonl' }])
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')('writes the sidecar owner-only', async () => {
|
||||
await writeUsageSourceCache(ref, [{ path: 'a.jsonl' }])
|
||||
|
||||
expect(statSync(ref.path).mode & 0o777).toBe(0o600)
|
||||
})
|
||||
|
||||
it('rejects attribution from a sidecar committed ahead of its report', async () => {
|
||||
await writeUsageSourceCache({ ...ref, worktreeFingerprint: 'new-worktrees' }, [
|
||||
{ path: 'a.jsonl', repoId: 'new-repo' }
|
||||
|
||||
@@ -78,7 +78,8 @@ export async function writeUsageSourceCacheData(
|
||||
await writeFileDurable(
|
||||
durableWriteTempPath(ref.path, threadId > 0 ? String(threadId) : undefined),
|
||||
ref.path,
|
||||
data
|
||||
data,
|
||||
0o600
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user