fix(environments): exclude multiline session env from terminal snapshots

Line-based grep on export -p only drops the first declare -x line, so a
newline in HERMES_SESSION_CHAT_NAME/USER_NAME leaves shell payload in the
shared snapshot and runs it on the next source. Unset bridged vars in a
subshell before export -p instead (issue #71296).
This commit is contained in:
HexLab98 2026-07-25 16:02:37 +07:00 committed by Teknium
parent 59482ea800
commit 9677495004
1 changed files with 30 additions and 21 deletions

View File

@ -397,7 +397,9 @@ def _cwd_marker(session_id: str) -> str:
# set), not Hermes' per-turn session identity.
#
# Kept in sync with gateway.session_context._VAR_MAP: every bridged name starts
# with one of these prefixes.
# with one of these prefixes (or is HERMES_UI_SESSION_ID). Used by unit tests
# as the Python-side contract for the exclusion set; the dump path unsets by
# name/prefix instead of grepping declare lines (see below / issue #71296).
_SNAPSHOT_EXCLUDED_ENV_REGEX = (
"^declare -x (HERMES_SESSION_|HERMES_UI_SESSION_ID|HERMES_CRON_AUTO_DELIVER_)"
)
@ -407,24 +409,31 @@ def _export_dump_excluding_session_vars(tmp_path: str) -> str:
"""Return a shell snippet that dumps ``export -p`` to *tmp_path* minus the
per-session bridged vars (see ``_SNAPSHOT_EXCLUDED_ENV_REGEX``).
``export -p`` emits one ``declare -x NAME="value"`` line per exported var.
We drop the HERMES_SESSION_* / UI / CRON_AUTO_DELIVER lines so they never
persist across sessions in the shared snapshot. ``grep -vE`` returns exit 1
when it filters everything, so ``|| true`` keeps the pipeline's success
contract intact for the callers that chain on it.
Unset the bridged vars in a subshell *before* ``export -p``. A line-based
``grep -vE`` filter is unsafe: bash 3.2 prints a value containing a newline
as a multi-line ``declare -x NAME="…`` block, so only the opener matches the
regex and continuation lines (e.g. ``curl | bash #`` smuggled into a
Matrix room/display name via ``HERMES_SESSION_CHAT_NAME``) land in the
snapshot and execute on the next ``source`` (issue #71296). Unsetting first
means ``export -p`` never emits those vars including any continuation
lines. ``|| true`` keeps the success contract for callers that chain on it.
The pipeline MUST be wrapped in a brace group with the redirection applied
to the group, not to the last pipeline segment. *tmp_path* typically embeds
``$BASHPID`` for concurrency-safe temp names; a redirection attached
directly to ``grep`` is expanded inside grep's own pipeline subshell, where
``$BASHPID`` resolves to the grep subshell's PID — while the caller's
follow-up ``mv $tmp`` expands in the parent shell to a DIFFERENT PID. The
dump then lands in an orphaned temp file and the snapshot silently never
updates (all exported-env persistence breaks). The brace-group redirect is
expanded in the current shell, keeping both expansions consistent.
The dump MUST be wrapped in a brace group with the redirection applied to
the group. *tmp_path* typically embeds ``$BASHPID`` for concurrency-safe
temp names; a redirection attached to a pipeline segment would expand
``$BASHPID`` inside that segment's subshell (a different PID than the
parent that expands the follow-up ``mv``), silently orphaning the dump.
The brace-group redirect is expanded in the current shell, keeping both
expansions consistent.
"""
# ${!PREFIX*} is bash 3.2+ name-prefix expansion; empty matches are fine
# because ``unset`` with only missing names is ignored under 2>/dev/null.
return (
f"{{ export -p | grep -vE '{_SNAPSHOT_EXCLUDED_ENV_REGEX}' || true; }} "
"{ ( "
"unset ${!HERMES_SESSION_*} ${!HERMES_CRON_AUTO_DELIVER_*} "
"HERMES_UI_SESSION_ID 2>/dev/null; "
"export -p; "
") || true; } "
f"> {tmp_path}"
)
@ -689,11 +698,11 @@ class BaseEnvironment(ABC):
# Chain mv on the export succeeding so a failed/partial dump never
# replaces a good snapshot; drop the temp on failure so it isn't
# orphaned (cleaned up wholesale in LocalEnvironment.cleanup too).
# NOTE: the redirection must be attached to a brace group, not to the
# grep pipeline segment — ``_snap_tmp`` embeds ``$BASHPID``, and a
# redirect on grep is expanded inside grep's pipeline subshell (a
# different PID than the parent shell that expands the ``mv`` operand),
# silently orphaning the dump. See _export_dump_excluding_session_vars.
# NOTE: the redirection must be attached to a brace group — ``_snap_tmp``
# embeds ``$BASHPID``, and a redirect on a pipeline segment expands
# inside that segment's subshell (a different PID than the parent that
# expands the ``mv`` operand), silently orphaning the dump. See
# _export_dump_excluding_session_vars.
if self._snapshot_ready:
parts.append(
f"{{ {_export_dump_excluding_session_vars(_snap_tmp)} "