Skip to content

fix: auto-resolve three code-review bugs — store path-traversal (#499), atomicWrite loss (#502), workflow 500-on-bad-input (#506) - #519

Draft
TYRMars wants to merge 2 commits into
mainfrom
claude/vibrant-dijkstra-k68s7n
Draft

fix: auto-resolve three code-review bugs — store path-traversal (#499), atomicWrite loss (#502), workflow 500-on-bad-input (#506)#519
TYRMars wants to merge 2 commits into
mainfrom
claude/vibrant-dijkstra-k68s7n

Conversation

@TYRMars

@TYRMars TYRMars commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary

Auto-resolves three self-contained code-review bugs picked up from newly-opened issues.

#499 — path traversal in partition directories (bug, security)

encodeId treats . as a safe byte, so encodeId("..") === "..". At filename call sites the .json suffix neutralises it, but stores that join a caller-supplied id as a partition directory let a project_id/requirement_id of .. escape the partition — landing requirement rows in the sibling flat conversation dir and permanently 500ing GET /v1/conversations (the leaked file has no messages).

Fix: add encodeIdSegment, which percent-encodes the dots in exactly the two reserved segments . and ... Every other id is unchanged (only an all-dots segment traverses — UUIDs, __memory__.summary: keys, and ids merely containing dots are untouched). Routed all six partition-directory helpers through it (requirement, comment, activity, label, requirement-run, doc-draft stores). Filename call sites keep using encodeId.

#502 — concurrent atomicWrite silently drops a write (bug)

atomicWrite derived its staging file solely from the target path, so all concurrent writers of one row shared a single <path>.tmp; the rename-race loser hit ENOENT and its write vanished (deterministic, 30/30). The REST message routes catch-and-ignore the failure, so a double-submit or two open tabs lost a whole turn silently.

Fix: unique staging name per writer (${filePath}.${pid}.${randomUUID()}.tmp) for honest last-writer-wins, plus rm on failure so a rejected write leaves no litter. New tmp names still end in .tmp (not .json), so every readdir loop keeps excluding them. This helper also backs @jarvis/observability and @jarvis/learning, which inherit the fix.

#506 — malformed workflow step kinds return 500 instead of 400 (bug)

walkAgentSteps/normalizeSteps trusted the TypeScript step-kind union, but POST/PATCH /v1/workflows feed them unvalidated JSON. A typo'd kind.type or a container kind (pipeline/phase/parallel) missing its steps array surfaced as an opaque 500 (steps is not iterable), and deeply-nested trees could blow the stack (RangeError) on an unauthenticated surface.

Fix: add validateStepKinds at the route boundary — unknown kinds, container kinds without an array steps, and nesting past MAX_WORKFLOW_STEP_DEPTH (64) all throw WorkflowError400 with an actionable message; runs before normalizeSteps in both create and update handlers. Also hardened walkAgentSteps (array guard + depth cap) so agentStepCount can never 500 regardless of caller.

Tests

Verification

@jarvis/store, @jarvis/workflow, @jarvis/server typecheck clean; lint clean on all changed files; new tests pass. The store commit's CI ("Node" workflow) went green (Typecheck + Lint + Test). Locally the only failures were the pre-existing sqlite: suite (native better-sqlite3 binary not built — install scripts skipped; unrelated, and green in CI).

Closes #499
Closes #502
Closes #506

🤖 Generated with Claude Code

https://claude.ai/code/session_01CoRkweyxMkc5cd2P8xHaQw

claude added 2 commits July 22, 2026 01:30
…omicWrite loss

Resolves two JSON-file store defects.

#499 (security): `encodeId` leaves `.`/`..` intact because `.` is a valid
filename byte. At filename call sites the `.json` suffix neutralises it, but
stores that join an id as a *partition directory* (requirement / comment /
activity / label / requirement-run / doc-draft) let a caller-supplied
`project_id`/`requirement_id` of `..` escape the partition — writing rows into
a sibling store's flat conversation dir and permanently 500ing
`GET /v1/conversations`. Add `encodeIdSegment`, which percent-encodes the dots
in exactly the two reserved `.`/`..` segments (all other ids, including
`__memory__.summary:` keys and ids merely containing dots, are unchanged), and
route every partition-directory helper through it.

#502: `atomicWrite` derived its staging file solely from the target path, so
all concurrent writers of one row shared one `.tmp`; the loser's rename hit
ENOENT and its write was silently dropped (deterministic, 30/30). Make the
staging name unique per writer (`pid` + random UUID) for honest
last-writer-wins, and remove the tmp on failure so a rejected write leaves no
litter. New tmp names still end in `.tmp` (not `.json`), so every readdir loop
keeps excluding them.

Tests cover the traversal containment, the `encodeIdSegment` mapping, and 20
rounds of concurrent same-id writes with no ENOENT and no tmp litter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CoRkweyxMkc5cd2P8xHaQw
`walkAgentSteps`/`normalizeSteps` trusted the TypeScript step-kind union, but
POST/PATCH /v1/workflows feed them unvalidated JSON. A typo'd `kind.type` or a
container kind (pipeline/phase/parallel) missing its `steps` array surfaced as
an opaque 500 (`steps is not iterable` / `Cannot read properties of undefined`),
and deeply-nested trees could blow the stack (RangeError) on an unauthenticated
surface.

- Add `validateStepKinds` at the route boundary: unknown kinds → 400
  `WorkflowError.invalidKind`, container kinds without an array `steps` → 400
  `WorkflowError.missingSteps`, nesting past `MAX_WORKFLOW_STEP_DEPTH` (64) →
  400 `WorkflowError.tooDeep`. Runs before `normalizeSteps` in both create and
  update handlers, inside the existing WorkflowError→400 catch.
- Harden `walkAgentSteps` (array guard + depth cap) so `agentStepCount` can
  never 500 regardless of caller.

Tests cover the three new 400 paths on create + update and the crash-proof
`agentStepCount` on malformed/pathologically-deep input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CoRkweyxMkc5cd2P8xHaQw
@TYRMars TYRMars changed the title fix(store): stop partition ids escaping their dir (#499) + fix concurrent atomicWrite loss (#502) fix: auto-resolve three code-review bugs — store path-traversal (#499), atomicWrite loss (#502), workflow 500-on-bad-input (#506) Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment