Skip to content

fix(gateway): cron runs work again when state.db is symlinked outside the profile home (salvage #145238) - #150992

Open
teknium1 wants to merge 7 commits into
mainfrom
salvage/145075-symlinked-store-home
Open

teknium1 wants to merge 7 commits into
mainfrom
salvage/145075-symlinked-store-home

Conversation

@teknium1

@teknium1 teknium1 commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #145075

What was wrong

When a profile's state.db is a symlink to a folder outside the profile home (e.g. ~/.hermes/state.db → ~/.hermes-db/state.db), every in-gateway cron run failed with ProviderNotConfiguredError and "skill not found". The gateway worked out the profile home as Path(authority.db.db_path).resolve().parent. resolve() follows the link, so the cron run was scoped to ~/.hermes-db/, which has no .env, no skills/ and no credential pool. Config reads, the stored credential reference, /model alias lookups, orphan-grace, the single-authority lookup and the groups/Bots owner check all used the same pattern.

Fix

  • _build_profile_authority records the reserved profile home on the authority (SessionAuthority.home), and session_authorities.authority_home(authority) returns it. Every former .resolve().parent site calls it: session_cron bind/unbind/execute, session_mutation_model ×3, session_policy, session_policy_credentials ×2, session_policy_refresh, session_viewer_close, authority_for_home, session_group_controls. Authorities built outside that path (single store, no reservation) fall back to the store's own directory, resolved.
    • Why not path arithmetic on db_path: the runner opens every store through hermes_state_registry.acquire(), which resolves the path. On a symlinked store db_path is already the link target, so no "don't resolve" variant can recover the home.
    • Why not profile_id: single-profile and test authorities carry a bare name ('default', 'p'), not a path.
  • run_runtime._build_profile_authority checks store ownership by file identity (samefile(home/'state.db')), so a symlinked store is no longer refused as "does not belong to the reserved profile". It also refuses a store another served profile already owns: two homes linking one file would otherwise both build authorities, and the second epoch would make the first one's admissions stale.
  • The retained-media sweep guard compares stores with samefile().

Salvage credit

Changes on top

Test evidence

Red on origin/main, green on this head. 13 tests in test_session_cron_symlinked_store.py, test_authority_home_symlinked_store.py (sibling sites + group controls) and test_profile_authority_symlinked_store.py (boot ownership + shared store):

  • main: 1 passed (the foreign-store rejection, which is unchanged), 9 failed, 3 errors
  • head: 13 passed

Wider sweep: every test file in tests/{gateway,tui_gateway,tools,acp_adapter,plugins} that touches the changed modules or builds a SessionAuthority (437 files) gave 2608 passed, 0 failed, 8 skipped.

Live probe on an isolated HERMES_HOME (hermes-home/state.db → ../hermes-db/state.db, with an OpenRouter key in hermes-home/.env and one skill). It went through the real acquire(), the real run_runtime._build_profile_authority → initialize_session_authority → bind_owner, and the real session_cron.execute + _profile_runtime_scope, then ran resolve_runtime_provider() inside the job:

== MAIN  RuntimeError: session authority database does not belong to the reserved profile .../hermes-home
         (and with only that check fixed: HERMES_HOME=.../hermes-db, skill_visible=false, has_key=false)
== HEAD  owner bound for home: True
         {"HERMES_HOME": ".../hermes-home", "skill_visible": true, "provider": "openrouter", "has_key": true}

Infographic

infographic

Peter Wilbrink and others added 5 commits November 7, 2026 17:35
…d state.db parent

Deriving a profile's home from Path(authority.db.db_path).resolve().parent
breaks when state.db is a symlink outside the profile home: the runtime
scope, config reads and owner keys then point at the link target's
directory (no .env, no skills), so multiplex cron agent runs die with
ProviderNotConfiguredError and 'skill not found' warnings.

Add session_authorities.authority_home(): the store path's own parent,
unresolved. All former resolve().parent sites use it; the media sweep
guard compares stores with samefile.

Closes #145075
_build_profile_authority() compared the resolved database parent with the
profile home, rejecting a state.db symlinked onto a separate dataset
(ZFS/NFS volume): the gateway aborted with 'session authority database
does not belong to the reserved profile' even though the opened database
was exactly the profile's own store.

Decide ownership by file identity instead: the opened database must be
the same file as the profile's state.db (os.path.samefile), wherever it
physically lives. A handle for a genuinely different file is still
rejected, as the new regression tests pin down.
…groups owner check

authority_home() returned the unresolved store parent, so every caller still had
to remember to resolve() before comparing or keying it (bind_owner did; config
reads and the stored credential reference did not). Resolving the directory keeps
the canonical home run_runtime serves and stamps into existing credential
references for every non-symlinked store, while still never following the
state.db link itself.

dispatch_group_control's owner check had the same .resolve().parent bug and
refused every Bots/groups call on a symlinked store with profile_mismatch.
…ed store (#145075)

Sibling-site coverage from #145834 (authority_for_home, profile_config,
orphan_close_grace, config_reference, provider resolution, cron owner key),
plus the group-controls owner check.

Co-authored-by: Prince Jain <[email protected]>
@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on 8357372 — test(gateway): release the registry handles the symlinked-st

⚠️ Warnings

CI timings · View report · View job

Wall time 8m14s vs 6m2s (+36.5%). 5 job(s) slower, 6 faster, 2 unchanged.

  • Python tests / Run tests (3/3): -23.0s
  • Python lints / Python lints: +23.0s
  • OS-specific tests / Windows-only tests: -18.0s
  • Python tests / Run tests (2/3): -15.0s
  • Python tests / Run tests (1/3): -14.0s

ℹ️ Info

CI caches · View report · View job

27 / 27 cache hits (0 misses)
4707.20 MiB restored
0 B not restored.

per-job cache results

hits

JobStepLogsRestoredΔ vs main
Catalog tests / Catalog testsSet up locked Python and test dependencies / Restore verified PM tools without savingview step80.77 MiB+0 B
Catalog tests / Catalog testsSet up locked Python and test dependencies / Restore uv dependency downloads and buildsview step78.48 MiB+0 B
Repository guards / Repository guardsRun ./.github/actions/setup-pm / Restore verified PM tools without savingview step153.06 MiB+0 B
Repository guards / Repository guardsRun ./.github/actions/setup-pm / Restore uv dependency downloads and buildsview step78.48 MiB+0 B
Repository guards / Repository guardsRun ./.github/actions/setup-pm / Restore node dependency downloadsview step180.79 MiB+0 B
Python tests / Run tests (3/3)Set up locked Python and test dependencies / Restore verified PM tools without savingview step152.33 MiB+0 B
Python tests / Run tests (3/3)Set up locked Python and test dependencies / Restore uv dependency downloads and buildsview step78.48 MiB+0 B
Python tests / Run tests (3/3)Run testsview step0.09 MiB+0 B
Python lints / Python lintsRun ./.github/actions/setup-pm / Restore verified PM tools without savingview step80.77 MiB+0 B
Python tests / Run tests (1/3)Set up locked Python and test dependencies / Restore verified PM tools without savingview step152.33 MiB+0 B
Python tests / Run tests (1/3)Set up locked Python and test dependencies / Restore uv dependency downloads and buildsview step78.48 MiB+0 B
Python tests / Run tests (1/3)Run testsview step0.09 MiB+173 B
Python tests / Run tests (2/3)Set up locked Python and test dependencies / Restore verified PM tools without savingview step152.33 MiB+0 B
Python tests / Run tests (2/3)Set up locked Python and test dependencies / Restore uv dependency downloads and buildsview step78.48 MiB+0 B
Python tests / Run tests (2/3)Run testsview step0.09 MiB+61 B
OS-specific tests / macOS-only tests (1/2)Set up locked Python and test dependencies / Restore verified PM tools without savingview step105.12 MiB+0 B
OS-specific tests / macOS-only tests (1/2)Set up locked Python and test dependencies / Restore uv dependency downloads and buildsview step165.31 MiB+0 B
OS-specific tests / Windows-only testsSet up locked Python and test dependencies / Restore verified PM tools without savingview step231.08 MiB+0 B
OS-specific tests / Windows-only testsSet up locked Python and test dependencies / Restore uv dependency downloads and buildsview step394.47 MiB+0 B
OS-specific tests / macOS-only tests (2/2)Set up locked Python and test dependencies / Restore verified PM tools without savingview step105.12 MiB+0 B
OS-specific tests / macOS-only tests (2/2)Set up locked Python and test dependencies / Restore uv dependency downloads and buildsview step165.31 MiB+0 B
OS-specific tests / Windows-only tests (arm64, 2/2)Set up native ARM64 build dependencies / Cache ARM64 OpenSSL development filesview step26.23 MiB+0 B
OS-specific tests / Windows-only tests (arm64, 2/2)Set up locked Python and test dependencies / Restore verified PM tools without savingview step218.46 MiB+0 B
OS-specific tests / Windows-only tests (arm64, 2/2)Set up locked Python and test dependencies / Restore uv dependency downloads and buildsview step853.18 MiB+0 B
OS-specific tests / Windows-only tests (arm64, 1/2)Set up native ARM64 build dependencies / Cache ARM64 OpenSSL development filesview step26.23 MiB+0 B
OS-specific tests / Windows-only tests (arm64, 1/2)Set up locked Python and test dependencies / Restore verified PM tools without savingview step218.46 MiB+0 B
OS-specific tests / Windows-only tests (arm64, 1/2)Set up locked Python and test dependencies / Restore uv dependency downloads and buildsview step853.18 MiB+0 B

misses

none

Compressed archive bytes. Not-restored bytes use the main-size reference; missing sizes remain unknown.

@teknium1 teknium1 added the ci-reviewed applied to manually approve dangerous changes label Nov 8, 2026
@greptile-apps

greptile-apps Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the test cleanup finding is fixed and no new actionable issues remain.

Summary

The PR keeps the profile home separate from the physical database path, so linked stores still use the owning profile's settings, skills, and credentials.

  • The gateway accepts linked profile databases without sharing one store.
  • Cron and profile settings stay tied to the home that owns the database link.
  • The image sweep recognizes the active store through its symlink.
  • Contributor attribution can now resolve this email address.

The previous cleanup finding is fully addressed. No new actionable issues or confirmed repository-rule violations were found. This review used code inspection; no tests were run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  H[Reserved profile home] --> A[SessionAuthority.home]
  H --> L[state.db symlink]
  L --> D[External database file]
  D --> S[Shared database handle]
  S --> B[SessionAuthority.db]
  A --> C[authority_home]
  C --> P[Profile settings, credentials, and cron scope]
Loading

Reviews (4) · Last reviewed commit: "test(gateway): release the registry hand..." · Reviewed by Greptile

Comment thread gateway/session_authorities.py Resolved
Comment thread gateway/run_runtime.py Resolved
@blacksmith-sh

This comment has been minimized.

…e a shared store

The runner opens every store through hermes_state_registry.acquire(), which
resolves the path, so on a symlinked state.db authority.db.db_path is already the
link target: authority_home() derived from it still scoped cron to the store's
directory in the real gateway. _build_profile_authority now records the reserved
home on the authority and authority_home() returns it, falling back to the store's
directory only for authorities built outside that path.

File-identity ownership admits a linked store, so two homes linking one file
would both build authorities and the second epoch would stale the first; refuse a
store another served profile already owns.

Regression tests now open through acquire() as the gateway does.
@alt-glitch alt-glitch added area/profiles Multi-profile isolation, HERMES_HOME scoping area/sessions Session lifecycle, resume, persistence, history comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have type/bug Something isn't working labels Nov 8, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping area/sessions Session lifecycle, resume, persistence, history ci-reviewed applied to manually approve dangerous changes comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: cron agent-run fails with ProviderNotConfiguredError when state.db is a symlink outside profile home (resolve().parent follows symlink)

2 participants