affaan-m/ECC · error · ValueError
artifact path must be canonical and absolute
Error message
artifact path must be canonical and absolute
What it means
`_read_local` refuses to open any artifact whose path string is not already a canonical absolute path. It rejects relative paths, symlink-style segments such as `..`, and non-canonical spellings (e.g. trailing slashes, `//`, `/a/./b`). The library throws this to guarantee that the TOCTOU-safe `dir_fd`-based reading logic operates on exactly one unambiguous filesystem location.
Solutions
- Normalize and absolutize first: `raw = str(Path(raw).expanduser().resolve())`, ensuring it does not escape the intended directory.
- Use `os.path.realpath` to canonicalize, then confirm `".." not in Path(raw).parts`.
- Reject or resolve relative paths at the configuration boundary (CLI parser / config loader) before they reach the artifact reader.
Example fix
// before
load_application_request("../out/request.json")
// after
from pathlib import Path
canonical = str(Path("../out/request.json").expanduser().resolve())
load_application_request(canonical) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path, PurePosixPath
def canonical_absolute(raw: str) -> str:
p = Path(raw).expanduser().resolve()
s = str(p)
if not p.is_absolute() or s != raw and ".." in PurePosixPath(raw).parts:
raise ValueError(f"path must be canonical and absolute: {raw!r}")
return s Type guard
def is_canonical_absolute(raw: str) -> bool:
from pathlib import Path
p = Path(raw)
return p.is_absolute() and str(p) == raw and ".." not in p.parts Try / catch
try:
req = load_application_request(raw_path)
except ValueError as e:
if str(e) == "artifact path must be canonical and absolute":
req = load_application_request(str(Path(raw_path).expanduser().resolve()))
else:
raise Prevention
- Never build artifact paths by string concatenation; use Path.resolve()/os.path.realpath.
- Normalize paths once, at the boundary where users or configs supply them.
- Reject user-supplied paths containing `..` at input-validation time, not at read time.
- Expand `~` and environment variables explicitly before passing paths into the library.
When it happens
Trigger: Passing a relative path like `build/artifact.json`; a path containing `..` (e.g. `/tmp/build/../artifact.json`); a non-normalized string such as `/tmp//artifact.json` or `/tmp/artifact.json/` to `_read_local` via `_artifact` or `load_application_request`.
Common situations: Building paths by string concatenation instead of `os.path.abspath`/`Path.resolve()`; accepting a user-supplied path from a CLI flag or config and forwarding it as-is; environment-relative defaults (`~`, `$VAR`) left unexpanded.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- artifact must be a resident regular file
- gate.variant_invalid
- Memory destination changed while it was being created.
- output artifact must be a regular file
- output bundle contains a symlink
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/b5d9137d4f65e93f.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/tasteforge/integration.py:116
def _parent_fd(path: Path) -> int:
flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_DIRECTORY
parent = os.open(path.anchor, flags)
try:
for part in path.parts[1:-1]:
child = os.open(part, flags, dir_fd=parent)
os.close(parent)
parent = child
return parent
except BaseException:
os.close(parent)
raise
def _read_local(raw: str, *, parse_json: bool, expected_size: int | None = None,
expected_hash: str | None = None) -> Any:
path = Path(raw)
if not path.is_absolute() or str(path) != raw or ".." in path.parts:
raise ValueError("artifact path must be canonical and absolute")
parent = descriptor = None
try:
flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK
parent = _parent_fd(path)
before = os.stat(path.name, dir_fd=parent, follow_symlinks=False)
if not stat.S_ISREG(before.st_mode) or getattr(before, "st_flags", 0) & 0x40000000:
raise ValueError("artifact must be a resident regular file")
if expected_size is None:
expected_size = before.st_size
if parse_json and expected_size > _MAX_JSON:
raise ValueError("JSON artifact exceeds local size limit")
if before.st_size != expected_size:
raise ValueError("artifact byte count mismatch")
descriptor = os.open(path.name, flags, dir_fd=parent)
if _identity(before) != _identity(os.fstat(descriptor)):
raise ValueError("artifact changed before reading")
digest, chunks, count = hashlib.sha256(), [], 0
while data := os.read(descriptor, 65536):View on GitHub (pinned to 8321021c54)