Merge remote-tracking branch 'origin/main' into tmp/hermes-relay-pr-merge-20260717

Signed-off-by: Alex Fournier <afournier@nvidia.com>
This commit is contained in:
Alex Fournier 2026-07-20 11:07:39 -07:00
commit 67863777ca
317 changed files with 18682 additions and 1937 deletions

View file

@ -37,6 +37,27 @@ TUI_CONTEXT_DIRS = [
"tui_gateway/",
]
# User plugin roots — scanned at runtime if they exist. Plugins load from
# ``get_hermes_home() / "plugins"`` (user) and ``./.hermes/plugins/`` (project,
# gated behind ``HERMES_ENABLE_PROJECT_PLUGINS``) — see
# ``hermes_cli/plugins.py:10-12``. The guard only checked the bundled
# ``plugins/`` dir, missing user-installed code that spawns subprocesses
# (gap reported in #67639).
#
# Import is deferred to ``main()`` (after ``os.chdir(repo_root)``) because
# this script runs as a standalone subprocess — ``hermes_constants`` isn't
# on ``sys.path`` until the repo root is added.
# subprocess and os APIs that inherit stdin by default when called without
# an explicit stdin= argument. The original regex only covered run/Popen
# (gap #1 in #67639); call, check_output, check_call, os.system, and
# asyncio.create_subprocess_* all inherit fd 0 equally.
_SUBPROCESS_PATTERNS = [
r"subprocess\.(run|Popen|call|check_output|check_call)\s*\([\"'a-zA-Z_\[\(]",
r"os\.system\s*\([\"'a-zA-Z_\[\(]",
r"asyncio\.create_subprocess_(exec|shell)\s*\([\"'a-zA-Z_\[\(]",
]
# Files with intentional stdin= override (e.g. input= creates a pipe).
# Format: "filepath:line" or just "filepath" to skip the whole file.
KNOWN_SAFE = {
@ -64,16 +85,14 @@ SKIP_DIRS = {
def find_subprocess_calls(content: str, filepath: str) -> list[dict]:
"""Find all subprocess.run/Popen calls missing stdin= in content."""
"""Find all subprocess/os/asyncio calls missing stdin= in content."""
violations = []
lines = content.split("\n")
# Match only actual function calls — not comments, docstrings, or prose.
# The pattern requires an opening paren followed by an arg character
# (quote, bracket, letter, or closing paren for empty calls).
# This excludes ``subprocess.Popen(...)`` in docstrings and
# subprocess.run(...) in comments.
pattern = re.compile(r'subprocess\.(run|Popen)\s*\(["\'a-zA-Z_\[\(]')
# Multiple patterns cover subprocess.run/Popen/call/check_output/check_call,
# os.system, and asyncio.create_subprocess_exec/shell.
patterns = [re.compile(p) for p in _SUBPROCESS_PATTERNS]
for i, line in enumerate(lines):
# Skip comments.
@ -85,7 +104,7 @@ def find_subprocess_calls(content: str, filepath: str) -> list[dict]:
if "``subprocess" in line:
continue
if not pattern.search(line):
if not any(p.search(line) for p in patterns):
continue
# Collect the full call (may span multiple lines).
@ -138,6 +157,11 @@ def main() -> int:
repo_root = Path(__file__).resolve().parent.parent
os.chdir(repo_root)
# Add repo root to sys.path so we can import hermes_constants (this script
# runs as a standalone subprocess, not as a module).
sys.path.insert(0, str(repo_root))
from hermes_constants import get_hermes_home
all_violations = []
for tui_dir in TUI_CONTEXT_DIRS:
@ -161,6 +185,32 @@ def main() -> int:
violations = find_subprocess_calls(content, rel)
all_violations.extend(violations)
# Scan user plugin directories (Gap 1: guard missed user-installed
# plugins in get_hermes_home()/plugins/ and project plugins in
# ./.hermes/plugins/, where code like ori/hooks.py can spawn
# subprocesses with inherited stdin — #67639).
plugin_roots: list[Path] = [get_hermes_home() / "plugins"]
if os.environ.get("HERMES_ENABLE_PROJECT_PLUGINS"):
plugin_roots.append(Path.cwd() / ".hermes" / "plugins")
seen_roots: set[Path] = set()
for plugin_root in plugin_roots:
resolved = plugin_root.resolve()
if resolved in seen_roots or not resolved.is_dir():
continue
seen_roots.add(resolved)
for py_file in resolved.rglob("*.py"):
rel = str(py_file)
if py_file.name in ("conftest.py",) or "/tests/" in rel:
continue
try:
content = py_file.read_text()
except Exception:
continue
violations = find_subprocess_calls(content, rel)
all_violations.extend(violations)
if all_violations:
print(f"{len(all_violations)} subprocess calls missing stdin=:")
for v in all_violations:

View file

@ -1686,11 +1686,54 @@ function Install-Repository {
Move-Item $extractedDir.FullName $InstallDir -Force
Write-Success "Downloaded and extracted"
# Initialize git repo so updates work later
# Initialize git repo so updates work later. A bare
# `git init` leaves NO HEAD -- desktop's write-build-stamp
# then hard-fails with "could not determine git commit"
# (#50823 / #61657). Fetch the requested ref and force-check
# it out (-f) so untracked ZIP files cannot block checkout.
Push-Location $InstallDir
git -c windows.appendAtomically=false init 2>$null
git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null
# Pin autocrlf=false BEFORE the checkout below. Git for Windows
# defaults to core.autocrlf=true, which would renormalize the
# repo's LF text files to CRLF in the working tree during
# `checkout -f FETCH_HEAD` -- leaving this freshly-created
# managed checkout dirty vs HEAD and aborting the next
# `hermes update` (see the notes at the shared clone-path
# config below and install.ps1:1461-1469). The later pin on
# the shared path is idempotent and still covers git clones.
git -c windows.appendAtomically=false config core.autocrlf false 2>$null
git remote add origin $RepoUrlHttps 2>$null
$fetchRef = if ($Commit) { $Commit } elseif ($Tag) { "refs/tags/$Tag" } else { $Branch }
Write-Info "Fetching $fetchRef so the ZIP checkout has a resolvable HEAD..."
$prevZipEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
git -c windows.appendAtomically=false fetch --depth 1 origin $fetchRef 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
if ($Commit -or $Tag) {
git -c windows.appendAtomically=false checkout -f --detach FETCH_HEAD 2>&1 | Out-Null
} else {
git -c windows.appendAtomically=false checkout -f -B $Branch FETCH_HEAD 2>&1 | Out-Null
}
if ($LASTEXITCODE -eq 0) {
Write-Success "ZIP checkout pinned to $fetchRef"
} else {
# Checkout blocked, but FETCH_HEAD still has a SHA we can stamp with.
$fetchSha = & git -c windows.appendAtomically=false rev-parse FETCH_HEAD 2>$null
if ($LASTEXITCODE -eq 0 -and $fetchSha) {
if (-not $env:GITHUB_SHA) { $env:GITHUB_SHA = ("$fetchSha").Trim() }
Write-Warn "ZIP checkout failed; seeded GITHUB_SHA from FETCH_HEAD for desktop stamp"
} else {
Write-Warn "ZIP extract succeeded but git checkout failed -- desktop build may need `$env:GITHUB_SHA"
}
}
} else {
Write-Warn "ZIP extract succeeded but git fetch of $fetchRef failed -- desktop build may need `$env:GITHUB_SHA"
}
} finally {
$ErrorActionPreference = $prevZipEAP
}
Pop-Location
Write-Success "Git repo initialized for future updates"
@ -2888,6 +2931,41 @@ function Install-Desktop {
# for some other tool, electron-builder would still try to sign.
Write-Info "Building desktop app (this takes 1-3 minutes)..."
$buildLog = "$env:TEMP\hermes-desktop-build-$(Get-Random).log"
# Seed GITHUB_SHA for write-build-stamp.mjs. The stamp prefers CI env vars
# over `git rev-parse`, so this covers: (1) node can't find git.exe on PATH
# even though this PowerShell session can, (2) ZIP/init trees that still
# lack a HEAD after a failed post-extract fetch. Without it the desktop
# pack dies with "could not determine git commit" (#50823).
if (-not $env:GITHUB_SHA) {
if ($Commit) {
$env:GITHUB_SHA = $Commit
} else {
Push-Location $InstallDir
try {
$global:LASTEXITCODE = 0
$resolvedSha = & git -c windows.appendAtomically=false rev-parse HEAD 2>$null
if ($LASTEXITCODE -ne 0 -or -not $resolvedSha) {
# ZIP path may have FETCH_HEAD after a fetch even when HEAD is unset.
$global:LASTEXITCODE = 0
$resolvedSha = & git -c windows.appendAtomically=false rev-parse FETCH_HEAD 2>$null
}
if ($LASTEXITCODE -eq 0 -and $resolvedSha) {
$env:GITHUB_SHA = ("$resolvedSha").Trim()
}
} catch { } finally {
Pop-Location
}
}
}
if (-not $env:GITHUB_REF_NAME) {
$env:GITHUB_REF_NAME = if ($Branch) { $Branch } else { "main" }
}
if ($env:GITHUB_SHA) {
$shaPreview = if ($env:GITHUB_SHA.Length -ge 12) { $env:GITHUB_SHA.Substring(0, 12) } else { $env:GITHUB_SHA }
Write-Info "Desktop build stamp: $shaPreview ($($env:GITHUB_REF_NAME))"
} else {
Write-Warn "Could not resolve a git commit for the desktop stamp -- write-build-stamp will use its non-git fallback"
}
Push-Location $desktopDir
$prevEAP = $ErrorActionPreference
$prevCSCAuto = $env:CSC_IDENTITY_AUTO_DISCOVERY