Commit Graph

22 Commits

Author SHA1 Message Date
Maciej Lisiewski
0006a5e229 ci: fix PLW1509, PLW1510, PLW2101 linter errors 2026-10-10 15:20:13 +02:00
kshitijk4poor
05d8d0a266 revert: stale worktree reaping revalidation (#132260)
Reverts the nine commits merged by #132260 (merge 05ab8766e7), from
31194ea00e "fix(cli): revalidate worktree cleanup gates before removal"
through 05ab8766e7, restoring hermes_cli/worktree_gc.py,
hermes_cli/worktree_ops.py and tests/security/test_gitspawn_config_injection.py
to their pre-merge state and removing
tests/hermes_cli/test_worktree_reclaim_revalidation.py.

Maintainer decision: back the behaviour change out (trees kept when their
state changed since the audit, plain `git worktree remove` under the tree's
git env, dirty submodule trees kept). No commit on main touched these files
after the merge, so this is the exact inverse of the merged diff.

The contributor mapping for @rudycelekli stays (attribution record only).
2026-10-03 22:03:44 +05:30
kshitijk4poor
a2ecf83336 fix(worktree): run the attended plain remove under the tree's git env
The attended reclaim's plain `worktree remove` (no --force) reads the
linked tree's index, so it re-hashes stat-dirty files through clean
filters. It ran under noninteractive_repo_git_env(repo_root), which only
discovers filters visible from the main checkout: a filter defined in the
tree's config.worktree (extensions.worktreeConfig) ran its command
(gate r2 2c, Medium). Build the env from the tree itself, as the startup
path already does, keep the tree when discovery fails, and run both the
plain remove and the submodule --force retry under it.

The security test now plants a tree-scoped clean filter on the trees the
startup sweep and the attended reclaim remove.
2026-10-03 20:53:08 +05:30
kshitijk4poor
5a4b350480 fix(worktree): run the startup plain remove under the repo-safe git env
Gate finding (High, 2ab): dropping --force from the startup reap's
`git worktree remove` makes git read the tree's index, which executes the
repository's core.fsmonitor (GHSA-7x36-8jrh-v4pw) on the unattended
startup sweep. tests/security/test_gitspawn_config_injection.py went red.

Route the remove through noninteractive_repo_git_env(entry) +
stdin=DEVNULL, the same pattern as _worktree_is_dirty; when the env
cannot be built, keep the tree. Extend the security test to also drive
the attended reclaim_worktrees remove (worktree_gc._git already uses the
safe env) so both plain-remove paths are pinned.
2026-10-03 20:53:08 +05:30
kshitijk4poor
c646bc29a9 fix(git): resolve include origins against the top level, cap and dedupe include targets
Gate follow-ups for the includeIf walk: git prints repo-local origins relative to the
worktree top level, so discovery from a subdirectory looked for the target in the wrong
place and skipped it (fail open). Resolve relative origins against `rev-parse
--show-toplevel` (one extra spawn, only when an include is present). Read each target once,
refuse a non-regular target (FIFO/device/directory) and more than 16 targets, so a repo
cannot make every hardened call spawn without bound. One discovery-regexp constant and a
single include-key matcher.
2026-10-02 02:31:03 +05:30
kshitijk4poor
22a81ca50a fix(git): neutralize filters named in includeIf targets instead of refusing the repo
Filter discovery refused every repository with a repo-local includeIf, so `hermes -w`,
kanban/subagent worktrees and the recovery hint stopped working inside actions/checkout
checkouts (which write an includeIf credential include) and similar setups. Read each
includeIf target directly, whatever its condition, and neutralize the filter commands it
defines alongside the ones found through --includes. One probe now lists both the filter
keys and the include paths (with --show-origin, so relative paths resolve against the file
that names them). A missing target is skipped as git skips it; an include nested inside an
include target, an unreadable target or a non-file origin still refuses.
2026-10-02 02:31:03 +05:30
kshitijk4poor
6c51075929 test(security): pass repo_root to _worktree_is_dirty after the include-symlink change
#130595 made repo_root a required argument; the filter-injection test still called the
one-argument form.
2026-10-02 01:24:41 +05:30
kshitijk4poor
0d5b75a6dd test(security): correct the broken_config exit-code comment
git config dies with rc 128 on a malformed .git/config, not 3; the
row is caught by the not-in-(0, 1) gate either way.
2026-10-02 01:24:41 +05:30
kshitijk4poor
0c0230e33e test(security): cover the filter-discovery rc gate and fold the ssh reclaim test
A malformed .git/config makes `git config --get-regexp` exit 3; the
rc gate in noninteractive_repo_git_env must refuse rather than treat it
as "no filters", but no row exercised it, so removing the gate stayed
green. Add a broken_config row.

Parametrize the filter rows with their data (attrs, config, refused)
instead of a string-keyed dict plus a restated id tuple, hoist
_CLEAN_GIT_ENV above its first use and use it everywhere.

Fold the core.sshCommand reclaim test into the index-reading test: the
same malicious repo gets an ssh origin, a hostile core.sshCommand and a
.git/shallow file, so ls-remote and fetch --unshallow are exercised
without a separate two-commit source repo and depth-1 clone. Keeps the
stack at two new tests.
2026-10-02 01:24:41 +05:30
kshitijk4poor
21c4b228b1 fix(git): make filter discovery case-exact, fail closed on any includeIf, bound the probe
Three gaps in noninteractive_repo_git_env, two probe-verified:

- Dedup was on key.lower(), but git keeps the subsection's case:
  [filter "evil"] + [filter "Evil"] with `filter=Evil` overrode only the
  lowercase driver and the hostile smudge ran. Dedup on the exact name
  `git config --name-only` prints.
- Only includeIf "onbranch:" failed closed. includeIf "gitdir:<repo>/.git/
  worktrees/" matches the new worktree's git dir during `worktree add`, so it
  loads a filter discovery never saw. Refuse on ANY repo-local includeIf
  (global/system config is already /dev/null).
- The discovery probe used subprocess.run(timeout=2), which on Windows can
  deadlock in the post-timeout communicate() and flashes a console. Use
  bounded_probe_run (rc 1 = "no filters" stays a verdict, so not
  bounded_git_probe).

Also share the refusal stderr as FILTER_DISCOVERY_FAILED across the kanban
and worktree-gc call sites and the test, and fold the fail-closed test into
the filter test (shared repo/env helpers) with case_collision and
gitdir_include rows.
2026-10-02 01:24:41 +05:30
kshitijk4poor
5dbd816f7e fix(git): cap discovered filter keys so filter discovery cannot hit E2BIG
Every discovered filter key adds GIT_CONFIG_KEY/VALUE env entries. A repository
with tens of thousands of filter sections makes the spawned git fail with
OSError(7, "Argument list too long") inside kanban/worktree-gc `_git`, which do
not expect it. Past 256 keys, fail closed like any other untrusted discovery.

One parametrized invariant covers both fail-closed paths (onbranch include and
filter flood) through the kanban `worktree add` call.

Fixes #126017
2026-10-02 01:24:41 +05:30
John Paul Soliva
f3767103e8 fix(git): delete worktree branches under noninteractive_git_env
The reclaim sweep (_reap_prune_verdicts), the orphaned-branch pass
(_prune_orphaned_branches) and the cleanup after a failed worktree add
(_cleanup_failed_worktree_add) ran `git branch -D` with the inherited
environment. A ref update runs the repository's reference-transaction hook
through core.hooksPath, so the unattended sweep executed whatever the
checkout's own config named. The three calls now use the same hardened env
as the other worktree_ops calls, with stdin closed.

(cherry picked from commit 31eec2f67e47a39c1b715f55d4fa649208076473)
2026-10-02 01:24:41 +05:30
John Paul Soliva
13ea517514 fix(git): run the worktree reclaim ls-remote, unshallow fetch and prune under noninteractive_git_env
The reclaim sweep's ls-remote, the shallow-repo fetch --unshallow and both worktree prune calls ran with the inherited environment, so a repository-level core.sshCommand or credential.helper ran unattended. They now use the same pinned env as the base fetch.

(cherry picked from commit a0d8e2a592790a90f6b666445017ea73f88cbc63)
2026-10-02 01:24:41 +05:30
John Paul Soliva
1ff01f4cf7 fix(git): neutralize repository-named filters in subagent finalization, worktree reclaim and hermes -w
finalize_subagent_worktree's status probe re-hashes files the child touched, so it ran a
repository-named clean filter after every delegated subagent. worktree_ops._worktree_is_dirty, the
dirty probe kanban teardown and the stale-worktree sweep run unattended, called git status with no
hardened env at all: core.fsmonitor ran as well. worktree_ops._worktree_add (hermes -w) had the
fixed pins only, so its checkout ran a named smudge filter.

All three now use noninteractive_repo_git_env. When filter discovery fails, finalization keeps the
worktree as unproven, the dirty probe reports dirty (the reclaimers keep the tree), and hermes -w
refuses to create the worktree. worktree remove --force at prune runs no filter once the status
probe is neutralized.

(cherry picked from commit e727281137a495602457fe4a86b51212af2fdbb7)
2026-10-02 01:24:41 +05:30
John Paul Soliva
62803f720d test(security): one fixture test covers the four index-reading sites
The recovery-hint/completion test and the kanban/worktree-gc test ran the same malicious-repo fixture; one test now runs all four sites. Reverting any one site alone still turns it red, so the PR keeps two invariant tests.

(cherry picked from commit 0b1a5da4055b26aa278bcba444635aeb05a943a1)
2026-10-02 01:24:41 +05:30
John Paul Soliva
803cd03521 fix(git): neutralize repository-named filters in the session snapshot and subagent worktrees
The session-start workspace snapshot runs git status, which re-hashes a
re-timestamped file through the repo-named clean filter, and a delegated
subagent's worktree add runs the smudge filter. Both now use
noninteractive_repo_git_env and skip the call when filter discovery fails.

(cherry picked from commit 04170a83d19a026fe6c5f968ce6e61b61f53c418)
2026-10-02 01:24:41 +05:30
John Paul Soliva
9c589b91bd fix(git): neutralize repository-named filters at the kanban, worktree-gc and recovery-hint calls
worktree add runs a repo-configured smudge filter and status runs its clean filter;
the fixed env pins cannot name the driver. These three sites now use
noninteractive_repo_git_env and skip the call when filter discovery fails. The Cmd-P
ls-files probe stays on the plain env: it never hashes or checks out content.

(cherry picked from commit 8d1233886a477f8fc623caee65dd28ce5e080eeb)
2026-10-02 01:24:41 +05:30
John Paul Soliva
bba715b42d fix(git): run the remaining index-reading git calls under noninteractive_git_env
The recovery-hint git status, the TUI fuzzy-completion ls-files, the kanban dispatcher's worktree add and the worktree gc wrapper spawned git with the inherited environment, unlike the other Hermes-initiated git calls hardened in #101483.

(cherry picked from commit 3a8a79e6815f802b62195a24a05bc4239947fe17)
2026-10-02 01:24:41 +05:30
ethernet
b4a294fff9 Merge origin/main; keep PM as plugin dependency owner
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
2026-09-17 13:52:05 -04:00
teknium1
bb745a0e9b fix(goals): failed quality gates re-run every boundary instead of replaying a status fingerprint
`_check_gates()` skipped a failed gate whenever sha256(git HEAD + `git status
--porcelain`) matched the last failure. Porcelain sees neither the contents
of an untracked or already-modified file nor inputs outside the repo, so a
repaired input replayed the stale failure and burned retries until the goal
auto-paused (#110649). The gate now runs on every eligible boundary; the
retry cap still bounds a genuinely stuck red suite. `workspace_fingerprint`
and `GoalGate.last_failed_fingerprint` are removed with their only consumer
(old persisted state ignores the extra key on load). Based on the analysis
in #110649 (JsonDaRula69) and PR #110658 (KoNit-K), whose `git diff HEAD`
hash still misses untracked contents and adds a full diff per boundary.
2026-09-15 03:59:50 -07:00
ethernet
92686159d1 fix(pm): integrate audited runtime and lifecycle repairs
Prepare dependency generations before selecting them. Keep shipped tool
bytes separate from writable additions, and store facts beside their entries.
Validate proposed plugin sets before config publication. Restore the previous
config if the facts write fails.

Consolidate duplicate updater, backup, setup, and voice helpers. Repair
launcher selection, dependency consumers, download ownership, update feeds,
and native Windows process and file handling.

Verification: 206 changed/prior-failing Python files reported 4630 passed,
one failed, and 330 skipped. Fix the remaining Hindsight fixture boundary.
The final targeted rerun reported 234 passed and two skipped. The store
review regression batch reported 83 passed and one skipped. Desktop
TypeScript checks, 56 selected Electron tests, 24 release tests, and the
removed-import/compatibility guards passed.

This is an integration checkpoint, not full audit acceptance. The complete
Python suite has not run on this fixed tree. Crash-atomic plugin publication,
generation cleanup, receipt correlation, and packaged lifecycle acceptance
remain open in docs/pm-audit-status.md.
2026-09-05 22:36:48 -04:00
Teknium
f6234d00c5 fix(security): close GitSpawn RCE class — malicious repo .git/config no longer executes on context gathering (GHSA-7x36-8jrh-v4pw)
Hermes gathers workspace context by running git against the session
directory automatically — the coding-workspace snapshot, gateway
project-tree build, /diff, @diff|@staged context refs, goal-gate
fingerprint, and -w startup worktree add — before any prompt, tool call,
approval, or trust gate. Those probes ran the system git without
stripping the repository's own config, so a repo delivered as files with
its .git directory intact (a shared zip, sync folder, or USB stick;
git clone never transfers .git/config) could set an execution-sink git
setting and get arbitrary host code execution as the user with nothing
on screen.

- core.fsmonitor / core.hooksPath / pager / editor / credential helper:
  neutralized by routing every automatic probe through
  noninteractive_git_env(), which pins those keys to inert values via
  GIT_CONFIG_* and ignores global/system config. bounded_git_probe (the
  reported sink, coding_context._git + tui_gateway.git_probe) now defaults
  to that env; worktree-add, working_diff, web_git, context_references,
  goals, and subagent_worktree route through it too.
- Attribute-scoped [diff "x"] command=/textconv= drivers: the attacker
  names the driver in .gitattributes, so GIT_CONFIG_KEY overrides can't
  enumerate them. Added harden_git_argv(), which inserts
  --no-ext-diff --no-textconv on diff-rendering subcommands (diff/show/
  log/blame) only — status et al reject the flags. Both flags required
  (verified empirically; each alone leaves the other live).

Builds on the noninteractive_git_env config-scrubbing from the
gemini-cli #28792 port. Real-git E2E regression suite arms a malicious
repo and asserts every automatic path neutralizes fsmonitor, hooks,
external-diff, and textconv; a baseline test proves the repo is armed.
2026-09-02 10:33:43 -07:00