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
Draft
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Auto-resolves three self-contained code-review bugs picked up from newly-opened issues.
#499 — path traversal in partition directories (bug, security)
encodeIdtreats.as a safe byte, soencodeId("..") === "..". At filename call sites the.jsonsuffix neutralises it, but stores that join a caller-supplied id as a partition directory let aproject_id/requirement_idof..escape the partition — landing requirement rows in the sibling flat conversation dir and permanently 500ingGET /v1/conversations(the leaked file has nomessages).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-draftstores). Filename call sites keep usingencodeId.#502 — concurrent
atomicWritesilently drops a write (bug)atomicWritederived its staging file solely from the target path, so all concurrent writers of one row shared a single<path>.tmp; the rename-race loser hitENOENTand 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, plusrmon failure so a rejected write leaves no litter. New tmp names still end in.tmp(not.json), so everyreaddirloop keeps excluding them. This helper also backs@jarvis/observabilityand@jarvis/learning, which inherit the fix.#506 — malformed workflow step kinds return 500 instead of 400 (bug)
walkAgentSteps/normalizeStepstrusted the TypeScript step-kind union, butPOST/PATCH /v1/workflowsfeed them unvalidated JSON. A typo'dkind.typeor a container kind (pipeline/phase/parallel) missing itsstepsarray surfaced as an opaque 500 (steps is not iterable), and deeply-nested trees could blow the stack (RangeError) on an unauthenticated surface.Fix: add
validateStepKindsat the route boundary — unknown kinds, container kinds without an arraysteps, and nesting pastMAX_WORKFLOW_STEP_DEPTH(64) all throwWorkflowError→ 400 with an actionable message; runs beforenormalizeStepsin both create and update handlers. Also hardenedwalkAgentSteps(array guard + depth cap) soagentStepCountcan never 500 regardless of caller.Tests
..intact and it is joined as a directory component → a project_id of..writes requirement rows into the conversation dir and permanently 500s GET /v1/conversations #499:encodeIdSegmentmapping;project_id: ".."stays in its partition, base conversation dir keeps only its own.json,list()still parses, requirement retrievable from its encoded partition.<path>.tmpshared by all writers of a target → concurrent writes to one id deterministically ENOENT on rename and the loser's turn is silently dropped #502: 20 rounds of two concurrent same-idatomicWrites — noENOENT, honest last-writer-wins, no leftover.tmp.steps→ an unknown/incomplete step kind from POST /v1/workflows throws TypeError as a 500 instead of a 400 #506: three new 400 paths on create + update (unknown kind, missingsteps, over-depth); crash-proofagentStepCounton malformed and 5000-deep input.Verification
@jarvis/store,@jarvis/workflow,@jarvis/servertypecheck 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-existingsqlite:suite (nativebetter-sqlite3binary 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