Commit Graph

10006 Commits

Author SHA1 Message Date
teknium1
85e11aaa00 fix(profiles): carry install records and skip install staging when cloning plugins
The light clone now copies plugins/ through the profile copier: bytecode caches,
sockets and the installer's in-flight .install-*/.update-* staging dirs stay
behind, while .install-metadata.json (revision + catalog provenance) travels, so
a catalog-installed memory provider runs at the source's revision in the clone
instead of resolving nowhere. The CLI names the plugins it carried.

Co-authored-by: Eri Barrett <eri@plasticlabs.ai>
2026-10-02 01:29:41 -07:00
CortonKwok
983bc7528c fix(profiles): clone plugins/ when cloning a profile
When creating a profile with --clone or --clone-from, the source
profile's plugins/ directory is now copied to the new profile
alongside config, skills, and memory files.

Symlinks in the source plugins/ are preserved as symlinks
(symlinks=True). The destination is not overwritten if it already
exists (dirs_exist_ok=True), matching the skills clone pattern.

Fixes plugin hooks not firing on non-default profiles (#50937, #65593)
by ensuring plugins are present in the new profile's plugins/
directory.
2026-10-02 01:29:41 -07:00
kshitijk4poor
b7200efef0 docs(models): get_codex_model_ids documents its fallback return 2026-10-02 13:34:54 +05:30
kshitijk4poor
7793c19660 refactor(models): one fallback marker for every catalog fetcher
get_codex_model_ids returns CuratedFallbackModels directly instead of a
second codex-only marker that _codex_catalog re-wrapped; the class moves to
the static catalog leaf so codex_models can use it. The catalog identity
normalizes its route with normalize_route_base_url like every other route
key.
2026-10-02 13:34:54 +05:30
AhmetArif0
ba2a38e771 fix(models): mark the Anthropic curated fallback as a placeholder
_anthropic_catalog() returned the curated list as a plain list when the live
/v1/models fetch failed, so a transient outage on a proxy base_url was cached
with the full 1h TTL as if it were the account's/proxy's real catalog, and
neither a same-credentials restart nor Refresh Models could shake it before the
TTL lapsed. Wrap the fallback return in CuratedFallbackModels, the placeholder
type #107391 introduced for the same class of bug.

Salvaged from #121248 (the Anthropic half; the Codex half lands via the
typed-fallback commit before this one).
2026-10-02 13:34:54 +05:30
FroRaut
3fbace2e76 fix(models): keep account-gated Codex rows when discovery falls back
A failed ChatGPT Codex catalog fetch returned the static offline hints as if
they were the account's catalog, so the picker cached them and dropped
account-gated rows such as GPT-6 Astra until the cache expired. The hints are
now typed (CodexFallbackModels) and cached as short-lived curated fallback
rows that never replace a verified account fetch, and the catalog cache key
includes the resolved route, not only the ChatGPT principal.
2026-10-02 13:34:54 +05:30
ethernet
00373b5376 fix(plugins): memory provider setup lists the dependencies PM would install
The memory-provider setup UI labelled a provider's Python dependencies from the manifest's
`pip_dependencies` / `python_dependencies` only. PM installs from an authored `pyproject.toml`
when one exists (it wins over the manifest lists), so a provider that declares its requirements
there showed an empty list, or a stale one, while the install button prepared different packages.

Read the same `read_python_declaration(...).install_requirements` PM consumes, so the label
matches what would be installed, including PM's own filtering (hermes-agent self-dependency,
direct URLs, `python_runtime: external`).
2026-10-01 23:27:08 -07:00
teknium1
00302e636c fix(update): a same-commit update that failed still owes its tail on launch
The completed-tree shortcut treated "stamp names HEAD" as "nothing is
owed". But `hermes update` at an unchanged commit ("Already up to date")
re-arms the completion marker before running the tail; when that tail
fails or is killed, the old stamp still names HEAD and the next launch
silently cleared the marker instead of retrying it.

The stamp only proves the tail that wrote it. A marker is now discharged
only when it predates the stamp (the #123314 leftover from a previous
install); one armed after it is retried as before. The shortcut also
requires a clean tree on both sides: a stamp recorded dirty, or local
edits since, are no proof that the built products match the checkout.

Reported by @unsupportedpastels in review.
2026-10-01 22:35:29 -07:00
teknium1
38254027bf fix(update): an adoption stamp never discharges an owed completion tail
Boot-time adoption of an unstamped blessed checkout publishes the same
identity a finished install/update does (commit == HEAD) but builds
nothing. Read as completion evidence, the launch that adopted (and every
later one) cleared a pending source-completion marker whose tail never
ran, and nothing on disk recorded the debt afterwards.

Adoption now stamps `adoptedAt`; the completed-tree check ignores such
stamps, and the next real completion rewrites the stamp without it. The
check also drops its blanket try/except: both reads it wraps never raise.
2026-10-01 22:35:29 -07:00
finn763
6770bc9578 fix(update): skip source-update completion when the stamp names HEAD
A fresh install records its tree in install-stamp.json; a first launch under a preserved or pristine home then re-entered the completion tail against the same SHA (stale pending marker, or no per-home facts). When the stamp commit is HEAD the products were already built for this tree: provision a stale venv without arming, discharge leftover markers, and run the agent. Fixes #123314.
2026-10-01 22:35:29 -07:00
teknium1
25aa38c008 fix(status): one row per messaging platform, judged by the gateway (#96190)
`hermes status --full` printed two lists for the same platforms: a legacy
table judged by one env var, then "(plugin)" rows judged by the gateway.
Slack configured in config.yaml showed "✗ not configured" and
"✓ configured (plugin)" in the same section, and the legacy verdict could
disagree with the summary's Platforms line.

Render one list from the gateway config instead: registry entries bring
their labels, a small label table covers built-in adapters without one,
"configured" is get_connected_platforms() (the summary's verdict), and the
home channel comes from the loaded config, which also fixes the legacy
table reading QQ_HOME_CHANNEL where the gateway reads QQBOT_HOME_CHANNEL.

Dedupe idea from #54131; root-cause analysis of the check_fn verdict from
#96200.

Co-authored-by: igparkernz-coder <297591502+igparkernz-coder@users.noreply.github.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-10-01 22:27:03 -07:00
teknium1
a3149156ec feat(cli): make the summary the default hermes status, --full for every section
`hermes status` now prints the one-screen summary (model, provider, connected
providers, gateway, platforms, jobs); `--full` (alias `--all`, which never
changed the output) and `--deep` print every section as before. Hermes
Console keeps the full report since it accepts no flags.

The summary reads the canonical sources instead of re-deriving them:
- Providers: list_authenticated_providers, the list the /model picker offers
  (cached catalogs only, no custom-endpoint probes), so it honours
  model_catalog.excluded_providers, pools and plugin providers for free.
- Platforms: GatewayConfig.get_connected_platforms, the gateway's own
  decision about which adapters to start (it also rejects placeholder tokens).

The full Messaging Platforms section now uses the same verdict for plugin
rows. It called check_fn, a passive "SDK importable" probe that is True for
every bundled plugin, so a fresh install listed Discord, Slack, Matrix,
Teams, WhatsApp and ~9 more as "configured" with no credentials.
2026-10-01 22:03:11 -07:00
PhoeniXAbhisheK
c82ad613ec feat(cli): add hermes status --short
Compact, secret-free summary of the component status: model, provider,
connected providers, gateway, messaging platforms, and scheduled jobs.

The provider list is registry-driven (PROVIDER_REGISTRY API-key rows,
refresh-free OAuth getters, plugin-mirrored OAuth providers, the
OpenRouter rung, configured custom endpoints), so new providers and
endpoints surface without touching the short renderer. Plugin-platform
configuredness prefers the is_connected hook over the deps probe (same
rule as hermes setup and the #102183 fix); every lookup is fail-soft,
so a failing probe degrades one row, never the command.

Full `hermes status` output and the console_engine call shape are
unchanged. Docs: website/docs/reference/cli-commands.md. Tests: 8 new
cases in tests/hermes_cli/test_status.py (25/25 pass via run_tests.sh).
2026-10-01 22:03:11 -07:00
teknium1
d49da55d4c fix(update): skip recompiling the TUI and web UI when their inputs are unchanged
The source update rebuilt every frontend unconditionally, although each
compiler writes a receipt that the launch path already trusts to answer
"would a rebuild produce these bytes?". Reuse it in the update too, as the
pre-PM updater did for the web UI (_web_ui_build_needed). Desktop keeps its
unconditional build: the baked install stamp carries the commit, so every
update that moves HEAD changes a desktop input anyway.

Credit: #125749 by @Froraut proposed the same receipt-gated TUI/web skip first.

Co-authored-by: Froraut <79215537+Froraut@users.noreply.github.com>
2026-10-01 15:28:55 -07:00
kshitijk4poor
e5e2d5508f fix(whatsapp): tighten cloud webhook identity binding and its diagnostics
The identity guard skipped non-dict change values with a warning that
claimed an identity mismatch, and the mismatch warning gave operators no
way to tell which number/WABA was arriving versus configured. A shared
Meta app also delivers status receipts for sibling numbers, so warning
on every one is noise: warn only when the foreign change carries
messages, debug otherwise. Malformed values are now skipped silently.

The phone_number_id arm's "not configured" escape was dead (the adapter
cannot start without a phone number id), so compare it directly.

The setup wizard STEP 4 header and module docstring still described the
WABA ID as analytics-only; it now binds inbound webhooks.
2026-10-02 02:58:18 +05:30
FroRaut
4152c69636 fix(whatsapp): bind cloud webhooks to configured identity
(cherry picked from commit a2efc1d62c5be444274ae58fda1aa86d1d9793ee)
2026-10-02 02:58:18 +05:30
kshitijk4poor
ed7909bad9 refactor(webhook): drop dead revision snapshot, lock-free reads, CAS owns the disable rule
The sha256 revision returned by _load_subscriptions_unlocked was never compared by
anyone, so _load_subscriptions_snapshot was dead weight; the loader now returns the
dict and folds its duplicate excepts. _load_subscriptions reads without the store
lock: writers publish by atomic rename, so readers always see one whole snapshot,
and read-only homes (where the lock file cannot be created) keep listing routes.
Nothing re-enters the lock any more, so the thread lock is a plain Lock.

The "keep an explicit enabled: False" rule was copied into both the CLI subscribe
path and the dashboard create path; it now lives in _replace_subscription after the
CAS check (where the current record is authoritative) and the published route is
returned, so the dashboard summary reflects what was actually stored.

The rebind secret selection collapses to one expression (same result for all four
profile-changed x --secret combinations), and the test-only _save_subscriptions is
removed; tests seed through _mutate_subscriptions.

The gateway's dynamic-route reload now records the stat key for a malformed or
non-dict file too, so a bad version is parsed once rather than on every POST while
the last good routes stay live. Docs describe the reload as stat-gated and lock-free.
2026-10-02 02:54:18 +05:30
FroRaut
3082d00c82 fix(webhook): transact subscription mutations
(cherry picked from commit 8c4ee86733a1193687f379e158e870ee4bcf56b5)
2026-10-02 02:54:18 +05:30
kshitijk4poor
b7fd5527dc fix(git): keep trailing spaces in the top-level path; refuse relative origins under a caller GIT_DIR
r2 gate: Path(stdout.strip()) dropped a trailing space in the checkout path, so include
targets resolved to a missing file and were skipped. With a caller-set GIT_DIR or
GIT_WORK_TREE git prints origins relative to another base; refuse instead of guessing.
2026-10-02 02:31:03 +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
teknium1
8edfe92501 Revert "feat(cli): approval and clarify panels wait until answered"
This reverts commit ea5b89fa35.
2026-10-01 13:23:12 -07:00
teknium1
a743ca54c3 Revert "docs: approvals.timeout and agent.clarify_timeout are the messaging-platform deadlines"
This reverts commit d7fdebf45a.
2026-10-01 13:23:12 -07:00
teknium1
734a94400a Revert "fix(cli): /bg turns hold their prompts open like the foreground turn"
This reverts commit 553bd72f33.
2026-10-01 13:23:12 -07:00
teknium1
e028d0fddb Revert "fix(cli): chat -q approvals keep the approvals.timeout deadline"
This reverts commit 82c63c505a.
2026-10-01 13:23:12 -07:00
kshitijk4poor
157c3c1d0b refactor(git): drop dead fallbacks in noninteractive_repo_git_env
bounded_probe_run returns a CompletedProcess from communicate() with
text=True, so stdout is always a str once proc is not None; the
`or ""` guard could never fire. GIT_CONFIG_COUNT is always written by
noninteractive_git_env (it pops any caller value and sets
str(len(overrides))), so the defensive `.get(..., "0") or 0` parse
only hid that invariant.
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
kshitijk4poor
a085553695 fix(git): fail closed on branch-conditional includes in filter discovery
noninteractive_repo_git_env discovers repository-named filter keys with
`git config --includes`, which evaluates includeIf "onbranch:..." against the
branch checked out NOW. A `worktree add -b X` then checks out X, git loads the
X-only include, and its smudge filter runs with no override pinned. Any
onbranch include in the effective config means the inventory cannot be trusted,
so return None and let callers skip the automatic git call.

Refs #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
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
JoaoMarcos44
c77a2dda9f fix(git): add noninteractive_repo_git_env to neutralize repository-named filters
A repository names its clean/smudge/process filter drivers in .gitattributes, so the
fixed GIT_CONFIG_KEY pins in noninteractive_git_env cannot enumerate them. Discover the
checkout's filter command keys under the isolated env and append empty command overrides
plus required=false. Discovery failure returns None so callers skip the git call.

Ported from #126075.

(cherry picked from commit ebdb7feb263512a2e76f06d9ee20d483af6adc24)
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
kshitijk4poor
0ecc5c644a fix(backup): keep cache/generated in hermes backup archives
cache/generated holds provider-generated images/videos that are now
deliberately never swept (they are the only copy of the user's output).
_KEPT_CACHE_SUBDIRS did not list it, so `hermes backup` treated it as a
regenerable cache and dropped it — for the root home and profile homes.
Add GENERATED_SUBDIR (stdlib-only agent.provider_media) to the kept set
and pin both locations in the existing cache-pruning backup test.
2026-10-02 01:18:03 +05:30
kshitijk4poor
5d579c5144 refactor(tools): pass the Unicode edit plan explicitly; tighten JSON constant scan
Behaviour-neutral review cleanups for the stack:

- fuzzy_match: _preserve_unicode_in_replacement now requires the plan
  (it is always precomputed by the caller), so the unused old_string param
  and the plan-less fallback go away. _apply_replacements takes the
  precomputed unicode_plan instead of a preserve_unicode flag, keeping the
  strategy decision in fuzzy_find_and_replace where it is made.
- file_operations: _json_nonstandard_constant skips the full json.loads
  when the text cannot contain NaN/Infinity (every JSON write pays this
  scan), strips the BOM via the shared _strip_bom helper, and catches only
  the errors json.loads can raise on str input instead of a blanket
  Exception.
- backup: the --keep validator comment now states why it exists (a
  negative keep slices away the newest archives).
2026-10-02 01:15:39 +05:30
kshitijk4poor
db3f9482e6 fix(cli): validate keep once, and reject negative /snapshot prune counts
`/snapshot prune -1` passed keep=-1 to prune_quick_snapshots, whose slice
then deletes the newest snapshots instead of the oldest - the same bug
class #128886 fixed for `hermes backup --keep`.

Use isdecimal() for both entry points. For `backup --keep` the argparse
type is now the single validation (cmd_backup is run_backup's only
caller), so the duplicate run_backup guard is removed; non-numeric input
still exits 2 with a clear message.

The test is folded into tests/hermes_cli/test_backup.py, replacing
test_backup_retention_validation.py.
2026-10-02 01:15:39 +05:30
Rudy Celekli
447b9ee7d6 fix(cli): reject negative full-backup retention before pruning
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
(cherry picked from commit c9113abe8d65dbb50b0c4aefaade1b9e1ac7b87e)
2026-10-02 01:15:39 +05:30
teknium1
aa859fd4c8 feat(plugin-catalog): refuse plugins that override Hermes core or Desktop at runtime
Catalog policy (Teknium): a listed plugin extends Hermes only through public
surfaces. A plugin that replaces core functions, methods or Desktop UI in place
collides with every other plugin patching the same seam and breaks on any core
release.

- New `no core override` admission check (hermes_cli/plugin_validate_core_override.py):
  static taint pass over the import closure of __init__.py and dashboard/*.py.
  Core values are Hermes modules (plus hermes_plugins.*) and attribute chains off
  them, reached by import, importlib/sys.modules lookups, getattr, iteration, or a
  plugin helper that returns one. Rebinds are attribute assignment/deletion,
  setattr/delattr, sys.modules writes, writes into a core module's tables,
  mock.patch, and passing a core value to a helper that setattr's its parameter.
  Tests, benchmarks and CI/smoke scripts outside the import closure are not judged.
- Desktop lint: new `app DOM reach` rule (document-level queries of the app's own
  data-slot/data-tour/data-sidebar/data-testid markup, and document.body
  childList/subtree MutationObservers).
- Desktop lint bug: the comment stripper did not skip string literals, so a string
  holding `/*` (a glob like '**/*.md') opened a fake comment and hid every line up
  to the next `*/` from all rules. Strings are now matched first and kept.
- README rule 9, the plugin-catalog user guide and the plugin developer guide's
  compatibility contract state the rule.

A/B over 466 pinned trees (121 open catalog PR entries + 349 listed entries +
humalike): the new checks flag exactly the hand-verified overrides; zero crashes;
no previously reported Desktop finding disappears.
2026-10-01 12:41:57 -07:00
kshitijk4poor
1223ffbe38 refactor(auth): table-drive loopback redirect host canonicalisation
The host check was a three-branch if/elif mapping each accepted loopback
literal to its URL authority spelling; a lookup table states the same
allow-list in one place. The final urlunparse also re-set scheme, netloc
and fragment even though every earlier check already guarantees they are
canonical (http, netloc == canonical_netloc, '#' rejected), so only the
empty-path -> "/" normalisation is applied. Behaviour and 400 messages are
unchanged.
2026-10-02 01:05:58 +05:30
FroRaut
37fea25c2b fix(auth): validate native loopback redirect URIs
(cherry picked from commit c719988403d173782f17bb95f5b01e0a8afb2260)
2026-10-02 01:05:58 +05:30
kshitijk4poor
4de541df1a refactor(utils): stage every atomic writer's temp through one mkstemp_beside helper
Five writers (cron jobs store, utils._atomic_write, config._write_env_lines,
the env_loader sanitize rewrite, backup _extract_member_atomically) each
hand-rolled "stage beside the resolved symlink target", and only cron kept the
PermissionError fallback. A symlink into another filesystem whose real
directory is read-only to us therefore made utils/config/backup raise and the
.env sanitize silently skip (NUL bytes left on disk), where base saved by copy.

mkstemp_beside picks the same directory atomic_replace renames into (resolved
only when the target is a link) and, on PermissionError, retries beside the
link so the base EXDEV -> copy fallback still saves. One rule, one docstring,
one place to test.
2026-10-02 01:02:00 +05:30
kshitijk4poor
9a4c2892b6 fix: stage atomic temp files beside the resolved symlink target
Same class as the jobs.json fix: atomic_replace renames onto the
symlink's real target, but the shared writers staged the temp beside the
link. When the link points into another filesystem the rename fails with
EXDEV and atomic_replace degrades to its copy fallback, which rewrites the
target in place and can tear it on a crash.

Stage beside os.path.realpath(target) in utils._atomic_write (all
atomic_*_write helpers), hermes_cli/config._write_env_lines,
hermes_cli/env_loader's .env sanitize rewrite, and
backup_restore._extract_member_atomically. utils keeps creating only the
link's own parent via mkdir_under_hermes_home (no mkdir of the real
parent), so the deleted-profile tombstone guard is unchanged.
2026-10-02 01:02:00 +05:30
kshitijk4poor
a567c3ac0c refactor(worktree): drop the include-symlink check's dead timeout parameter
Its only user was the deleted _main_checkout_root fallback.
2026-10-02 00:54:45 +05:30
kshitijk4poor
c3655fece6 refactor(worktree): require repo_root for the dirty/include-symlink checks
Every caller (cli._cleanup_worktree, _prune_stale_worktrees, kanban
teardown) already passes the main checkout, so the git-discovery fallback
(_main_checkout_root) was dead code that could silently disagree with the
caller's root. Make repo_root a required positional and drop it.

Also normalise .worktreeinclude entries with Path(...).as_posix() so an
entry written with backslashes on Windows still matches git status
output, which always uses "/".
2026-10-02 00:54:45 +05:30
kshitijk4poor
99839aad85 fix(worktree): resolve the main checkout from git, not --git-common-dir's parent
_include_symlink_paths took the main checkout as the parent of
--git-common-dir. For a submodule (common dir .git/modules/<name>) or a
--separate-git-dir repo that is not the checkout, so no .worktreeinclude
symlink matched and every include-only worktree was kept forever as
"dirty" — a regression against BASE, which removed it.

_worktree_is_dirty / _include_symlink_paths now take repo_root, and the
CLI exit cleanup, the startup pruner and kanban teardown pass the one
they already hold. When it is None, fall back to the first
`git worktree list --porcelain` entry resolved through
`rev-parse --show-toplevel` (a submodule lists its git dir there).

Also share the .worktreeinclude parsing between the copy step and the
symlink check (_worktreeinclude_entries), compare with rstrip('/') only,
drop the backslash rewrite, and drop the redundant inner try/except —
_worktree_is_dirty's outer handler already fails safe to dirty.
2026-10-02 00:54:45 +05:30
kshitijk4poor
ebb876bb74 fix(worktree): don't count .worktreeinclude symlinks as uncommitted changes
_copy_worktree_includes symlinks included directories (node_modules,
.venv) into the worktree. A trailing-slash gitignore pattern never
matches a symlink, so `git status --porcelain` lists each one as `??`
and _worktree_is_dirty reported the worktree dirty forever. With exit
cleanup now honouring dirtiness, every session in such a repo kept its
worktree; the startup pruner and kanban teardown had the same bug.

Ignore an untracked entry only when it is a symlink at a path listed in
the main repo's .worktreeinclude that resolves to that same entry in the
main checkout. Any other change, a non-zero git exit, or an error still
counts as dirty (fail-safe). Fixes all three callers at once.
2026-10-02 00:54:45 +05:30
kshitijk4poor
509d0f12d4 fix(web): give the managed-search refusal an accurate reason
The refusal named a cause that cannot happen. `_vendor_gateway` never contacts the
network, so "unreachable" was wrong for the no-identity, failed-lookup and
not-entitled cases alike — and a guest was told it had no Nous identity when it
holds a perfectly good one.

Root of the guest case: `_error_info` dropped `account_tier`, so a guest whose
account lookup failed — precisely the state where the tier can no longer be read
from the JWT, because the token is inside its refresh window — came back looking
like a registered identity. Carry the stored tier through the error snapshot, and
both this refusal and every other tier-aware surface see it.

Pick the reason in priority order (guest / no usable identity / unreachable) in
one helper beside `fast_search_entitled`, so the predicate and its explanation
cannot drift, and phrase it as a clause for `selection_error`'s "but …" template —
the canonical `FREE_TIER_NEEDS_ACCOUNT` strings are standalone sentences and read
wrong embedded mid-sentence.

Also record why the `error` clause in `fast_search_entitled` is load-bearing
rather than defensive, so it is not removed as belt-and-braces.

The test helper now mirrors what anon_auth actually persists (the tier alongside
the token), which is what makes the failed-lookup case reproducible at all.
2026-10-01 23:55:54 +05:30