fix(gateway): routing entries persist the receiving bot; restored lanes deliver through it or fail closed

After a restart the routing index rebuilt every lane from `SessionEntry.origin`,
which carries the runtime profile (key namespace) but not the bot that received
the conversation. Delivery then fell to `_is_shared_bot_satellite`: a lane owned
by a secondary bot whose runtime profile is ALSO a satellite of the default bot
was handed to the default bot, and authorization read the wrong allowlist.

- `SessionEntry.transport_profile` (routing JSON) + nullable
  `sessions.transport_profile` (SCHEMA_SQL, reconciled by the existing column
  path; `agent:main` keys untouched, standalone gateways write nothing). Stamped
  from the pinned `RoutingIdentity` at create, reset/switch, DB recovery and
  every peer refresh; compression forks inherit it like the other routing columns.
- `session_identity.restore_identity()` re-pins a `RoutingIdentity(transport=None)`
  from the persisted transport profile; `authz_mixin._restored_source(entry)` is
  the one seam every revive path uses (auto-resume, heartbeat restore, plugin
  injection, background-process events).
- `_adapter_for_source` / `_adapter_profile_for_source` honour a restored identity:
  the persisted bot's adapter or None — never the default bot by heuristic.
  Entries written before the column exist keep the old chain.

Phase 5 of #88715.
This commit is contained in:
teknium1
2026-09-19 00:10:24 -07:00
committed by Teknium
parent a05000810a
commit 8ab22a95b1
13 changed files with 279 additions and 28 deletions

View File

@@ -213,7 +213,7 @@ _SAME_KEY_NAMESPACE_SQL = (
_UPSERT_KEEP_EXISTING_SQL = ",\n".join(
f" {col} = COALESCE(sessions.{col}, excluded.{col})" for col in (
"session_key", "chat_id", "chat_type", "thread_id", "parent_session_id", "cwd", "profile_name",
"git_repo_root", "origin_json", "display_name",
"transport_profile", "git_repo_root", "origin_json", "display_name",
)
)
@@ -240,6 +240,7 @@ _INHERIT_PARENT_ROUTING_SQL = (
"UPDATE sessions\n SET "
+ _INHERIT_SEP.join(_inherit_col_sql(c) for c in (
"user_id", "session_key", "chat_id", "chat_type", "thread_id", "display_name", "origin_json",
"transport_profile",
))
+ "\n WHERE id = ? AND parent_session_id IS NOT NULL\n"
" AND EXISTS (\n"
@@ -285,6 +286,7 @@ class SessionSessionsMixin:
chat_id: str = None, chat_type: str = None, thread_id: str = None,
parent_session_id: str = None, cwd: str = None, profile_name: Optional[str] = None,
git_repo_root: str = None, origin_json: str = None, display_name: str = None,
transport_profile: Optional[str] = None,
) -> None:
"""Upsert a session row, never overwriting what an earlier writer set (the gateway creates a
bare row before create_session carries the real model/prompt) — the one exception is the
@@ -323,10 +325,10 @@ class SessionSessionsMixin:
"""INSERT INTO sessions (
id, source, user_id, session_key, chat_id, chat_type, thread_id,
model, model_config, system_prompt, system_prompt_hash,
parent_session_id, cwd, profile_name, git_repo_root,
parent_session_id, cwd, profile_name, transport_profile, git_repo_root,
origin_json, display_name, started_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
source = CASE
WHEN sessions.source = 'unknown'
@@ -367,8 +369,8 @@ class SessionSessionsMixin:
(
session_id, source, user_id, session_key, chat_id, chat_type, thread_id, model,
json.dumps(model_config) if model_config else None, system_prompt_hash,
parent_session_id, cwd, profile_name, git_repo_root, origin_json, display_name,
time.time(),
parent_session_id, cwd, profile_name, transport_profile, git_repo_root, origin_json,
display_name, time.time(),
),
)
if system_prompt_hash is not None: