fix: P2 bugs - heartbeat timing, symlink check, conflict detection

worker.nim:
- Start heartbeat before state transition in start command
  (prevents WORKING state without heartbeat if startup fails)

context.nim:
- Reject symlinks when reading context file (security)
  (prevents reading arbitrary files via symlinked .worker-ctx.json)

git.nim:
- Use case-insensitive conflict detection in rebase/merge
  (toLowerAscii instead of checking "CONFLICT" and "conflict" separately)

Closes: skills-qekj, skills-16zf, skills-n3qp

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dan 2026-01-10 20:55:15 -08:00
parent c44f2106ee
commit 3afd621bb4
3 changed files with 12 additions and 5 deletions

View file

@ -204,10 +204,10 @@ proc start(task: string = "") =
echo "Already WORKING: ", taskId
quit(ExitSuccess)
db.transition(taskId, wsAssigned, wsWorking)
# Start heartbeat
# Start heartbeat before transition so we're heartbeating when state changes
startGlobalHeartbeat(BusDbPath, taskId)
db.transition(taskId, wsAssigned, wsWorking)
updateGlobalHeartbeat(hsWorking, taskId)
echo "Started work on ", taskId

View file

@ -21,6 +21,10 @@ proc readContext*(worktree: string = ""): WorkerContext =
if not fileExists(path):
raise newException(IOError, &"Context file not found: {path}")
# Security: reject symlinks to prevent reading arbitrary files
if getFileInfo(path).kind == pcLinkToFile:
raise newException(IOError, &"Context file is a symlink (rejected): {path}")
try:
let content = readFile(path)
let j = parseJson(content)
@ -38,6 +42,9 @@ proc findContext*(): WorkerContext =
while dir != "" and dir != "/":
let path = dir / ContextFileName
if fileExists(path):
# Security: reject symlinks to prevent reading arbitrary files
if getFileInfo(path).kind == pcLinkToFile:
raise newException(IOError, &"Context file is a symlink (rejected): {path}")
try:
let content = readFile(path)
let j = parseJson(content)

View file

@ -78,7 +78,7 @@ proc rebaseOnIntegration*(worktree: string): bool =
let (output, code) = runGit("rebase", "origin/integration", workDir = worktree)
if code != 0:
if "CONFLICT" in output or "conflict" in output:
if "conflict" in output.toLowerAscii():
return false
# Other error - abort and raise
discard runGit("rebase", "--abort", workDir = worktree)
@ -111,7 +111,7 @@ proc mergeToIntegration*(taskId: string, maxRetries: int = 3): bool =
let (mergeOutput, mergeCode) = runGit("merge", "--no-ff", branch,
"-m", &"Merge {branch}")
if mergeCode != 0:
if "CONFLICT" in mergeOutput:
if "conflict" in mergeOutput.toLowerAscii():
discard runGit("merge", "--abort")
return false
raise newException(GitError, &"Merge failed: {mergeOutput}")