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:
YeonGyu-Kim
2026-02-01 23:48:48 +09:00
parent 172446795c
commit f853d885fa
9 changed files with 206 additions and 54 deletions
+3 -3
View File
@@ -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 {
+49 -17
View File
@@ -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
}