mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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:
commit
67863777ca
317 changed files with 18682 additions and 1937 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue