fix(gateway): bind the session profile once around command.dispatch and bundle routing (#110695)

Follow-up on the salvaged #110698 (which scoped `_dispatch_skill` alone):

- One `_session_home_scope(session)` binding around the whole stage loop in
  `command.dispatch`, so quick commands (`_load_cfg` → `_active_config_path`
  honours the override), bundles (`skill-bundles/` is home-relative) and skills
  all resolve against the SAME profile the routing guard used.
- `slash.exec` resolves `_bundle_key_for(base)` under the same scope, so a
  bundle that exists only in the secondary profile is routed to dispatch at all.
- `_dispatch_skill` uses the home-keyed `get_skill_commands()` (the guard's
  reader) instead of an unconditional `scan_skill_commands()`.
- Test (red on origin/main): a bundle only under profile B's `skill-bundles/`
  is dispatched for a session bound to B.
This commit is contained in:
teknium1
2026-09-14 10:57:42 -07:00
committed by Teknium
parent a21747fe4e
commit 23ef64081c
2 changed files with 73 additions and 32 deletions
+33
View File
@@ -1222,6 +1222,39 @@ def test_command_dispatch_scopes_skill_lookup_to_session_profile(server, tmp_pat
assert resp["result"]["name"] == "b-only"
def test_slash_exec_routes_a_secondary_only_bundle_to_dispatch(server, tmp_path, monkeypatch):
"""A skill bundle that exists only under the session profile's ``skill-bundles/`` must be
resolved (and routed to command.dispatch) against that profile, not the launch home (#110695)."""
import agent.skill_bundles as sb_mod
import agent.skill_commands as sc_mod
monkeypatch.delenv("HERMES_BUNDLES_DIR", raising=False)
profile_b = tmp_path / "profile_b"
external_b = tmp_path / "external_b"
for name in ("one", "two"):
d = external_b / name
d.mkdir(parents=True)
(d / "SKILL.md").write_text(f"---\nname: {name}\ndescription: {name}.\n---\n\n# {name}\n")
(profile_b / "skill-bundles").mkdir(parents=True)
(profile_b / "skill-bundles" / "b-pack.yaml").write_text("name: b-pack\nskills: [one, two]\n")
(profile_b / "config.yaml").write_text(f"skills:\n external_dirs:\n - {external_b}\n")
sid = "test-session-profile-b-bundle"
server._sessions[sid] = {"session_key": sid, "agent": None, "profile_home": str(profile_b)}
with (
patch("tools.skills_tool.SKILLS_DIR", tmp_path / "no-local-skills"),
patch.object(sb_mod, "_bundles_cache", {}),
patch.object(sb_mod, "_bundles_cache_mtime", None),
patch.object(sc_mod, "_skill_commands", {}),
patch.object(sc_mod, "_skill_commands_home", None),
):
resp = server.handle_request({
"id": "r1", "method": "slash.exec", "params": {"command": "/b-pack go", "session_id": sid}})
assert "error" not in resp, resp
assert resp["result"]["type"] == "send" and "b-pack" in resp["result"]["notice"]
def test_command_dispatch_queue_sends_message(server):
"""command.dispatch /queue returns {type: 'send', message: ...} for the TUI."""
sid = "test-session"