fix(delivery): validate project_path in set instead of mkdir-ing whatever it is given (#493) - #508
Conversation
…ever it is given (fujibee#493) set built a hooks-file path straight from an unvalidated project_path and mkdir -p'd it, so a malformed literal - the report's case is a trailing newline from an agent-composed command - created a bogus sibling directory and installed hooks into it. agmsg_validate_project_path rejects rather than corrects: trimming is used to DETECT surrounding whitespace, not to absorb it, so a caller that generated a bad command gets a loud error naming the value instead of a lucky save that hides the bug upstream. A path that does not already exist is refused, never created. The status of the traversability check is tested explicitly rather than folded into a command substitution - printf returns 0 regardless, which would let a permission failure pass as a successful validation of an empty path. The caller's own path spelling is echoed back; canonicalizing would be a second, unrequested behavioral change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — the diagnosis and the coverage argument both hold up. We checked the sweep independently: One change we'd like before merging, and it narrows the rejection rather than widening it. The validator currently refuses any value whose trimmed form differs from what was passed, which means a leading or trailing space or tab is rejected. Those are legal POSIX path characters, and a directory whose name begins or ends with one can be created and used today — Concretely, please reject only: (1) an empty or whitespace-only value, (2) a value containing CR or LF, whether leading, trailing, or embedded — this is the actual repro, (3) a path that does not already exist as a directory, and (4) a path that exists but cannot be entered. Accept a path that merely carries a leading or trailing space or tab, using it as the literal value passed. Your "reject, don't correct" framing is the right one and we are not asking you to drop it — it is exactly right for the newline case, where the value is almost certainly the product of a broken command composition. We are only asking that the set of things treated as malformed be the set the report is about. Worth noting for your test pass: internal spaces are already covered on |
…failure shapes Per maintainer review: reject only (1) empty/whitespace-only, (2) any CR or LF -- leading, trailing, or embedded, (3) a path that does not already exist as a directory, (4) an existing directory that cannot be entered. A plain leading/trailing space or tab is a legal POSIX path byte and is now accepted and used literally (the old trim-then-compare rejection is gone). Also hardens the traversability probe against CDPATH: with it set, a relative path could cd into a same-named directory elsewhere on CDPATH instead of the one -d just checked, letting an un-enterable local directory validate against a different, enterable one. Tests: positive acceptance for space- and tab-padded names; a no-fallback pin proving a padded spelling of an existing dir is used literally (and rejected as nonexistent) rather than trimmed into the real one; trailing CR, leading LF, embedded CR; and a CDPATH-decoy regression test (fire-verified: fails with the CDPATH fix reverted).
The CDPATH regression test now skips under root (permission bits do not restrict traversal there), matching the existing non-enterable test. The no-fallback test's comment no longer mislabels a space-led relative pathname as a sibling, and the CR/LF policy comment stops overclaiming that such bytes cannot round-trip -- they are refused as the likely product of broken command composition, not as unrepresentable.
|
Thank you — and completely agreed on keeping #493's fix to #493's failure. Pushed exactly that narrowing: the validator now rejects only the four conditions you listed (empty/whitespace-only, any CR or LF, not an existing directory, exists but not enterable) and accepts a leading or trailing space or tab as literal path bytes. Also added the missing positive case for a space-padded name, plus a pin that a padded spelling of an EXISTING dir is used literally and never silently trimmed into the real one. CI is green (14/14). Really appreciate you independently re-checking the sweep coverage — that confirmation made this a much easier change to keep small. |
Part of #493.
delivery.sh setdidn't validate<project_path>at all, soresolve_hooks_fileconcatenated whatever it was given andagmsg_delivery_applymkdir -p'd the result — which is how a directory literally namedmyproject\nended up next to the real one, containing only.codex/hooks.jsonwith the newline baked into the hook commands' project path.agmsg_validate_project_path()now runs once indo_set, before any type-specific apply logic.The policy, and why
Reject, don't correct. A trailing newline is detected by trimming, then the value is refused because the trimmed form differs from what was passed. Silently "fixing" it would make a malformed command work this one time and hide the bug in whatever generated it. As the issue notes, agmsg's callers are largely LLM agents composing commands from SKILL.md — for that audience a loud error naming the exact value is worth more than a lucky save.
Never create implicitly. A path that doesn't already exist as a directory is refused rather than created. That is the actual defect from the report: a directory nobody asked for.
Also rejected: empty or whitespace-only, an embedded newline or carriage return, and a directory that exists but can't be entered (
-dpasses for a directory with no execute bit, and every apply implementation then tries to write inside it — better to fail here with a clear message than later with a confusingmkdirerror).Two things I changed after review
printf '%s' "$(cd "$raw" && pwd)".printfreturns 0 regardless, so a permission failure or race inside the substitution would have produced a successful validation of an empty path. The status is now checked explicitly, andcd -- "$raw"keeps a real directory named like an option (-P,-L) from being parsed as one. A validator that fails open is worse than no validator.cd && pwd, mirroringspawn.sh:188. But that's a second, unrequested behavioral change — it rewrites relative paths to absolute and collapses./.., so anything downstream comparing or persisting this value would start seeing a different string than the caller passed.delivery.sh setsilently mkdirs a bogus project dir when passed a malformed path (no existence check / trim) #493 is about refusing malformed input, not normalizing well-formed input. The function now echoes the caller's own spelling back.Verification
bats tests/test_delivery.bats→ 148 ok, 0 not ok, exit 0.myproject\nrepro (asserting no directory is created), nonexistent path, empty, whitespace-only, leading/trailing spaces where the trimmed path does exist (proving it doesn't quietly fall back), embedded newline, and the un-enterable directory (skipped when running as root, where permission bits don't restrict traversal).codex exec(gpt-5.6-sol, high). The fail-openprintfand thecd --hardening are its findings.Scope notes
do_setrather than in each apply implementation. That covers the default JSON-hooks path andrulefile_applyas far as I traced, but I have not done a full call-graph audit, so I'm not claiming categorically that no other entry point can reach the samemkdir. If you know of one, say so and I'll extend it.pwd-style normalization can rewriteC:/repoto/c/repo; dropping the canonicalization sidesteps that question entirely rather than answering it.