fix(task-tool): add task ID validation and improve lock acquisition safety
- Add task ID pattern validation (T-[A-Za-z0-9-]+) to prevent path traversal - Refactor lock mechanism to use UUID-based IDs for reliable ownership tracking - Implement atomic lock creation with stale lock detection and cleanup - Add lock acquisition checks in create/update/delete handlers - Expand task-reminder hook to track split tool names and clean up on session deletion - Add comprehensive test coverage for validation and lock handling
This commit is contained in:
@@ -40,9 +40,9 @@ interface Task {
|
||||
|
||||
## STORAGE UTILITIES
|
||||
|
||||
### getTaskDir(teamName, config)
|
||||
### getTaskDir(config)
|
||||
|
||||
Returns: `.sisyphus/tasks/{teamName}` (or custom path from config)
|
||||
Returns: `.sisyphus/tasks` (or custom path from config)
|
||||
|
||||
### readJsonSafe(filePath, schema)
|
||||
|
||||
@@ -80,7 +80,7 @@ Returns: `.sisyphus/tasks/{teamName}` (or custom path from config)
|
||||
```typescript
|
||||
import { TaskSchema, getTaskDir, readJsonSafe, writeJsonAtomic, acquireLock } from "./features/claude-tasks"
|
||||
|
||||
const taskDir = getTaskDir("my-team", config)
|
||||
const taskDir = getTaskDir(config)
|
||||
const lock = acquireLock(taskDir)
|
||||
|
||||
try {
|
||||
|
||||
@@ -73,37 +73,69 @@ export function listTaskFiles(config: Partial<OhMyOpenCodeConfig> = {}): string[
|
||||
|
||||
export function acquireLock(dirPath: string): { acquired: boolean; release: () => void } {
|
||||
const lockPath = join(dirPath, ".lock")
|
||||
const now = Date.now()
|
||||
const lockId = randomUUID()
|
||||
|
||||
if (existsSync(lockPath)) {
|
||||
const createLock = (timestamp: number) => {
|
||||
writeFileSync(lockPath, JSON.stringify({ id: lockId, timestamp }), {
|
||||
encoding: "utf-8",
|
||||
flag: "wx",
|
||||
})
|
||||
}
|
||||
|
||||
const isStale = () => {
|
||||
try {
|
||||
const lockContent = readFileSync(lockPath, "utf-8")
|
||||
const lockData = JSON.parse(lockContent)
|
||||
const lockAge = now - lockData.timestamp
|
||||
|
||||
if (lockAge <= STALE_LOCK_THRESHOLD_MS) {
|
||||
return {
|
||||
acquired: false,
|
||||
release: () => {
|
||||
// No-op release for failed acquisition
|
||||
},
|
||||
}
|
||||
}
|
||||
const lockAge = Date.now() - lockData.timestamp
|
||||
return lockAge > STALE_LOCK_THRESHOLD_MS
|
||||
} catch {
|
||||
// If lock file is corrupted, treat as stale and override
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const tryAcquire = () => {
|
||||
const now = Date.now()
|
||||
try {
|
||||
createLock(now)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
||||
return false
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
ensureDir(dirPath)
|
||||
writeFileSync(lockPath, JSON.stringify({ timestamp: now }), "utf-8")
|
||||
|
||||
let acquired = tryAcquire()
|
||||
if (!acquired && isStale()) {
|
||||
try {
|
||||
unlinkSync(lockPath)
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
acquired = tryAcquire()
|
||||
}
|
||||
|
||||
if (!acquired) {
|
||||
return {
|
||||
acquired: false,
|
||||
release: () => {
|
||||
// No-op release for failed acquisition
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
acquired: true,
|
||||
release: () => {
|
||||
try {
|
||||
if (existsSync(lockPath)) {
|
||||
unlinkSync(lockPath)
|
||||
}
|
||||
if (!existsSync(lockPath)) return
|
||||
const lockContent = readFileSync(lockPath, "utf-8")
|
||||
const lockData = JSON.parse(lockContent)
|
||||
if (lockData.id !== lockId) return
|
||||
unlinkSync(lockPath)
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user