Round-4 follow-up on the hardening PR after a third 20-reviewer pass.
Closes the high-impact items from that review while preserving the
"do not regress legitimate tool calling" floor; lower-vote items that
would have measurable regression on legit code paths (broad shell
glob ?/*, $VAR in dynamic paths, ANSI-C $'...') are intentionally
deferred.
Parent-directory traversal: _normalize_path_separators now follows
.. segments through posixpath.normpath and reattaches the tilde or
${HOME} prefix, so cat /etc/apt/../shadow and
Path('/proc/self/fd/../environ').read_text() both reach the
canonical regex.
Built-in open() accepts PathLike: open(Path('/etc/shadow')) and
open(file=Path('/etc/shadow')) now flow through the pathlib resolver
the same way receiver reads do.
Pathlib home and transforms: Path.home() resolves to ~ so
(Path.home() / '.aws/credentials') hits the home regex;
.expanduser() / .resolve() / .absolute() are pass-throughs.
Pathlib semantics: _join_path_parts() now matches pathlib's
absolute-segment reset so Path('/tmp') / '/etc/shadow' resolves to
/etc/shadow as it does at runtime.
from builtins import exec as e: tracked in both visitors via
eval_exec_aliases so the aliased call still routes through the
literal-payload recursion.
Process state extensions: /proc/self/cmdline,
/proc/thread-self/*, and /proc/<pid>/task/<tid>/* are added to
_ABSOLUTE_SENSITIVE.
Numeric f-strings: f'/proc/{1}/environ' folds to a literal because
numeric ast.Constant values inside ast.FormattedValue are now
stringified.
os.path.join / os.path.expanduser: resolved statically by
_extract_string_from_node so the stdlib-helper construction paths
do not hide sensitive targets.
Variable assignment tracking: a pre-pass collects ``name = literal``
and ``name = eval`` / ``name = exec`` bindings; the visitors and the
pathlib resolver consult those bindings. The trusted-host gate
intentionally uses a separate strict literal extractor so legit
patterns like ``url = some_input; requests.get(url)`` still pass.
shutil.copyfile / copy / copy2 / copytree / move: the source argument
is gated the same way open() is, blocking file-copy exfil.
Concrete pathlib classes: PosixPath / WindowsPath / PurePath / etc.
are registered in path_aliases by default.
requests.request positional+keyword: for URL-second APIs, args[0] is
the HTTP method (not the URL); when there is only one positional, the
URL extraction falls through to the url= keyword instead of grabbing
the method.
Tests grow from 281 to 357 hardening cases; combined sweep 487 / 487.
Every fix has positive and negative coverage; legit tool calls
(open(Path('data.csv')), os.path.join('logs', 'today.log'),
url = some_input; requests.get(url), shutil.copyfile('a.txt', 'b.txt'))
continue to pass.