From aacf1206e8d3daaac9d6049701a245c9ae29c01f Mon Sep 17 00:00:00 2001 From: Geoffrey R Plymale <106821302+badMade@users.noreply.github.com> Date: Sun, 31 May 2026 18:31:17 -0400 Subject: [PATCH 1/2] fix(copilot): honor configured base URL for pool credentials --- agent/credential_pool.py | 21 ++++++- hermes_cli/runtime_provider.py | 17 +++++- tests/agent/test_credential_pool.py | 55 +++++++++++++++++++ .../test_runtime_provider_resolution.py | 53 ++++++++++++++++++ 4 files changed, 141 insertions(+), 5 deletions(-) diff --git a/agent/credential_pool.py b/agent/credential_pool.py index aeda76225c85..27660fd29190 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -1270,10 +1270,17 @@ def _is_suppressed(_p, _s): # type: ignore[misc] token, source = resolve_copilot_token() if token: api_token = get_copilot_api_token(token) - source_name = "gh_cli" if "gh" in source.lower() else f"env:{source}" + source_name = "gh_cli" if source.lower() == "gh auth token" else f"env:{source}" if not _is_suppressed(provider, source_name): active_sources.add(source_name) pconfig = PROVIDER_REGISTRY.get(provider) + env_url = "" + if pconfig and pconfig.base_url_env_var: + env_url = ( + get_env_value(pconfig.base_url_env_var) + or os.getenv(pconfig.base_url_env_var, "") + ).strip().rstrip("/") + base_url = env_url or (pconfig.inference_base_url if pconfig else "") changed |= _upsert_entry( entries, provider, @@ -1282,7 +1289,7 @@ def _is_suppressed(_p, _s): # type: ignore[misc] "source": source_name, "auth_type": AUTH_TYPE_API_KEY, "access_token": api_token, - "base_url": pconfig.inference_base_url if pconfig else "", + "base_url": base_url, "label": source, }, ) @@ -1468,6 +1475,14 @@ def _is_source_suppressed(_p, _s): # type: ignore[misc] continue active_sources.add(source) auth_type = AUTH_TYPE_OAUTH if provider == "anthropic" and not token.startswith("sk-ant-api") else AUTH_TYPE_API_KEY + access_token = token + if provider == "copilot": + try: + from hermes_cli.copilot_auth import get_copilot_api_token + + access_token = get_copilot_api_token(token) + except Exception: + access_token = token base_url = env_url or pconfig.inference_base_url if provider == "kimi-coding": base_url = _resolve_kimi_base_url(token, pconfig.inference_base_url, env_url) @@ -1480,7 +1495,7 @@ def _is_source_suppressed(_p, _s): # type: ignore[misc] { "source": source, "auth_type": auth_type, - "access_token": token, + "access_token": access_token, "base_url": base_url, "label": env_var, }, diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index 1cc41ceae95d..6d0b93ed5146 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -27,7 +27,7 @@ resolve_external_process_provider_credentials, has_usable_secret, ) -from hermes_cli.config import get_compatible_custom_providers, load_config +from hermes_cli.config import get_compatible_custom_providers, get_env_value, load_config from hermes_constants import OPENROUTER_BASE_URL from utils import base_url_host_matches, base_url_hostname @@ -220,7 +220,20 @@ def _resolve_runtime_from_pool_entry( api_mode = "chat_completions" elif provider == "copilot": api_mode = _copilot_runtime_api_mode(model_cfg, getattr(entry, "runtime_api_key", "")) - base_url = base_url or PROVIDER_REGISTRY["copilot"].inference_base_url + pconfig = PROVIDER_REGISTRY["copilot"] + pool_url_is_default = base_url.rstrip("/") == pconfig.inference_base_url.rstrip("/") + if not base_url or pool_url_is_default: + cfg_provider = str(model_cfg.get("provider") or "").strip().lower() + cfg_base_url = "" + if cfg_provider == "copilot": + cfg_base_url = str(model_cfg.get("base_url") or "").strip().rstrip("/") + env_url = "" + if pconfig.base_url_env_var: + env_url = ( + get_env_value(pconfig.base_url_env_var) + or os.getenv(pconfig.base_url_env_var, "") + ).strip().rstrip("/") + base_url = cfg_base_url or env_url or base_url or pconfig.inference_base_url elif provider == "azure-foundry": # Azure Foundry: read api_mode and base_url from config cfg_provider = str(model_cfg.get("provider") or "").strip().lower() diff --git a/tests/agent/test_credential_pool.py b/tests/agent/test_credential_pool.py index 299567a9a6ff..ef899114373e 100644 --- a/tests/agent/test_credential_pool.py +++ b/tests/agent/test_credential_pool.py @@ -1158,6 +1158,10 @@ def test_load_pool_seeds_copilot_via_gh_auth_token(tmp_path, monkeypatch): "hermes_cli.copilot_auth.resolve_copilot_token", lambda: ("gho_fake_token_abc123", "gh auth token"), ) + monkeypatch.setattr( + "hermes_cli.copilot_auth.get_copilot_api_token", + lambda token: token, + ) from agent.credential_pool import load_pool pool = load_pool("copilot") @@ -1170,6 +1174,57 @@ def test_load_pool_seeds_copilot_via_gh_auth_token(tmp_path, monkeypatch): assert entries[0].base_url == "https://api.githubcopilot.com" +def test_load_pool_seeds_copilot_gh_token_as_env_source_with_base_url_override(tmp_path, monkeypatch): + """GH_TOKEN should not mask COPILOT_API_BASE_URL behind a gh_cli entry.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + monkeypatch.setenv("COPILOT_API_BASE_URL", "https://enterprise-proxy.example/copilot/") + monkeypatch.setenv("GH_TOKEN", "gho_fake_token_abc123") + _write_auth_store(tmp_path, {"version": 1, "credential_pool": {}}) + + monkeypatch.setattr( + "hermes_cli.copilot_auth.resolve_copilot_token", + lambda: ("gho_fake_token_abc123", "GH_TOKEN"), + ) + monkeypatch.setattr( + "hermes_cli.copilot_auth.get_copilot_api_token", + lambda token: f"api-{token}", + ) + + from agent.credential_pool import load_pool + pool = load_pool("copilot") + + assert pool.has_credentials() + entries = pool.entries() + assert len(entries) == 1 + assert entries[0].source == "env:GH_TOKEN" + assert entries[0].access_token == "api-gho_fake_token_abc123" + assert entries[0].base_url == "https://enterprise-proxy.example/copilot" + + +def test_load_pool_seeds_copilot_gh_cli_with_base_url_override(tmp_path, monkeypatch): + """gh auth token credentials should honor COPILOT_API_BASE_URL.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + monkeypatch.setenv("COPILOT_API_BASE_URL", "https://enterprise-proxy.example/copilot/") + _write_auth_store(tmp_path, {"version": 1, "credential_pool": {}}) + + monkeypatch.setattr( + "hermes_cli.copilot_auth.resolve_copilot_token", + lambda: ("gho_fake_token_abc123", "gh auth token"), + ) + monkeypatch.setattr( + "hermes_cli.copilot_auth.get_copilot_api_token", + lambda token: token, + ) + + from agent.credential_pool import load_pool + pool = load_pool("copilot") + + entries = pool.entries() + assert len(entries) == 1 + assert entries[0].source == "gh_cli" + assert entries[0].base_url == "https://enterprise-proxy.example/copilot" + + def test_load_pool_does_not_seed_copilot_when_no_token(tmp_path, monkeypatch): """Copilot pool should be empty when resolve_copilot_token() returns nothing.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) diff --git a/tests/hermes_cli/test_runtime_provider_resolution.py b/tests/hermes_cli/test_runtime_provider_resolution.py index d17b1a41e3a8..0d0f2707e5e2 100644 --- a/tests/hermes_cli/test_runtime_provider_resolution.py +++ b/tests/hermes_cli/test_runtime_provider_resolution.py @@ -95,6 +95,59 @@ def _unexpected_anthropic_token(): assert resolved.get("credential_pool") is None +def test_resolve_runtime_provider_copilot_pool_respects_env_base_url(monkeypatch): + class _Entry: + access_token = "pool-token" + source = "gh_cli" + base_url = "https://api.githubcopilot.com" + + class _Pool: + def has_credentials(self): + return True + + def select(self): + return _Entry() + + monkeypatch.setenv("COPILOT_API_BASE_URL", "https://enterprise-proxy.example/copilot/") + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "copilot") + monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool()) + monkeypatch.setattr(rp, "_copilot_runtime_api_mode", lambda *a, **k: "chat_completions") + + resolved = rp.resolve_runtime_provider(requested="copilot") + + assert resolved["provider"] == "copilot" + assert resolved["api_key"] == "pool-token" + assert resolved["source"] == "gh_cli" + assert resolved["base_url"] == "https://enterprise-proxy.example/copilot" + + +def test_resolve_runtime_provider_copilot_pool_respects_config_base_url(monkeypatch): + class _Entry: + access_token = "pool-token" + source = "gh_cli" + base_url = "https://api.githubcopilot.com" + + class _Pool: + def has_credentials(self): + return True + + def select(self): + return _Entry() + + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "copilot") + monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool()) + monkeypatch.setattr(rp, "_copilot_runtime_api_mode", lambda *a, **k: "chat_completions") + monkeypatch.setattr( + rp, + "_get_model_config", + lambda: {"provider": "copilot", "base_url": "https://config-proxy.example/copilot/"}, + ) + + resolved = rp.resolve_runtime_provider(requested="copilot") + + assert resolved["base_url"] == "https://config-proxy.example/copilot" + + def test_resolve_runtime_provider_falls_back_when_pool_empty(monkeypatch): class _Pool: def has_credentials(self): From e8d421713a8b73a8b31828dbd1956538b784bf03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:55:40 +0000 Subject: [PATCH 2/2] fix: address failing gateway and process-registry checks --- gateway/platforms/qqbot/adapter.py | 30 +++++++++++++++++++++++++----- gateway/platforms/webhook.py | 9 +++++++++ gateway/run.py | 2 ++ tools/process_registry.py | 10 +++++++++- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/gateway/platforms/qqbot/adapter.py b/gateway/platforms/qqbot/adapter.py index 38e58ffc46e9..30209051c67c 100644 --- a/gateway/platforms/qqbot/adapter.py +++ b/gateway/platforms/qqbot/adapter.py @@ -1131,6 +1131,30 @@ async def _handle_c2c_message( return text = content + source = self.build_source( + chat_id=user_openid, + user_id=user_openid, + chat_type="dm", + ) + if ( + self.gateway_runner is not None + and hasattr(self.gateway_runner, "_is_user_authorized") + and not self.gateway_runner._is_user_authorized(source) + ): + self._chat_type_map[user_openid] = "c2c" + event = MessageEvent( + source=source, + text=text, + message_type=self._detect_message_type([], []), + raw_message=d, + message_id=msg_id, + media_urls=[], + media_types=[], + timestamp=self._parse_qq_timestamp(timestamp), + ) + await self.handle_message(event) + return + attachments_raw = d.get("attachments") logger.info( "[%s] C2C message: id=%s content=%r attachments=%s", @@ -1195,11 +1219,7 @@ async def _handle_c2c_message( self._chat_type_map[user_openid] = "c2c" event = MessageEvent( - source=self.build_source( - chat_id=user_openid, - user_id=user_openid, - chat_type="dm", - ), + source=source, text=text, message_type=self._detect_message_type(image_urls, image_media_types), raw_message=d, diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index 83aa93e94cb3..0725d9c71523 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -590,6 +590,15 @@ def _validate_signature( self, request: "web.Request", body: bytes, secret: str ) -> bool: """Validate webhook signature (GitHub, GitLab, generic HMAC-SHA256).""" + stripped_secret = secret.strip() + if ( + not stripped_secret + or (stripped_secret.startswith("${") and stripped_secret.endswith("}")) + or (stripped_secret.startswith("{{") and stripped_secret.endswith("}}")) + ): + logger.warning("[webhook] Secret is empty or unresolved placeholder") + return False + # GitHub: X-Hub-Signature-256 = sha256= gh_sig = request.headers.get("X-Hub-Signature-256", "") if gh_sig: diff --git a/gateway/run.py b/gateway/run.py index 8c884307c1f4..291ab7faa8f5 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -5458,6 +5458,8 @@ def _is_user_authorized(self, source: SessionSource) -> bool: ): return True + team_id = (getattr(source, "guild_id", None) or "").strip() + # Check pairing store (always checked, regardless of allowlists) platform_name = source.platform.value if source.platform else "" auth_user_id = user_id diff --git a/tools/process_registry.py b/tools/process_registry.py index a5006180848b..1615b40ed105 100644 --- a/tools/process_registry.py +++ b/tools/process_registry.py @@ -1162,8 +1162,16 @@ def _check_stdin_guards(self, session: ProcessSession, payload: str) -> Optional if not payload: return None approval = check_all_command_guards(self._stdin_guard_command(session, payload), "local") - if approval.get("approved"): + if not approval: return None + approved = approval.get("approved") + if approved is True: + return None + if approved is None: + return { + "approved": False, + "message": "BLOCKED: malformed stdin approval response", + } return approval def write_stdin(self, session_id: str, data: str) -> dict: